import datetime import gc import logging import os import subprocess import time import socket import pythoncom import pywintypes import requests import win32com.client import webbrowser from flask import Flask, jsonify, request, current_app, send_from_directory, Response, stream_with_context from flask_cors import CORS from requests_toolbelt import MultipartEncoder from win10toast import ToastNotifier from config import file_type_map, TARGET_URL, SERVER_POINT, API_SERVER_PORT from tools.check import is_file_open_in_wps, enable_wps_login_force from tools.file_manager import get_file_md5 from tools.logger_handle import logger socket.getfqdn = lambda name=None: 'localhost' toaster = ToastNotifier() app = Flask(__name__, static_folder='../static') CORS(app, resources=r'/*') class InterceptHandler(logging.Handler): def emit(self, record): # 获取对应的 loguru 级别 logger_opt = logger.opt(depth=6, exception=record.exc_info) logger_opt.log(record.levelname, record.getMessage()) # 移除 Flask 默认的所有 Handler,并添加自定义的 InterceptHandler app.logger.handlers = [] app.logger.addHandler(InterceptHandler()) app.logger.setLevel(logging.INFO) # class InterceptHandler(logging.Handler): # def emit(self, record): # # 将标准日志记录转为 loguru # try: # level = logger.level(record.levelname).name # except ValueError: # level = record.levelno # logger.opt(depth=6, exception=record.exc_info).log(level, record.getMessage()) # # # logging.basicConfig(handlers=[InterceptHandler()], level=logging.INFO) # for name in ("flask", "werkzeug", "watchdog"): # logging.getLogger(name).handlers = [InterceptHandler()] # logging.getLogger(name).propagate = False @app.route('/') def index(): return send_from_directory(app.static_folder, 'index.html') # 代理静态文件(如 css, js, images 等) @app.route('/') def static_proxy(path): return send_from_directory(app.static_folder, path) @logger.catch() @app.route('/pyapi/', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH']) def proxy(router_path): url = f'{TARGET_URL}/{router_path}' logger.info(f'接收到请求{url}') raw_body = request.get_data(cache=True) # 获取 headers(排除 Host 避免冲突) headers = {key: value for key, value in request.headers if key.lower() != 'host'} headers[ 'User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36' try: if request.files or (request.content_type and 'multipart/form-data' in request.content_type.lower()): # 构造 Multipart 表单 fields = {} # 添加普通字段 if request.files: for key, value in request.form.items(): fields[key] = value # 添加文件字段 for key, file in request.files.items(): fields[key] = (file.filename, file.stream, file.mimetype) form_data = MultipartEncoder(fields=fields) headers['Content-Type'] = form_data.content_type else: form_data = raw_body resp = requests.request( method=request.method, url=url, headers=headers, params=request.args, data=form_data, cookies=request.cookies, allow_redirects=False ) else: # JSON 或普通表单 json_data = None form_data = None if request.content_type and 'application/json' in request.content_type.lower(): json_data = request.get_json(silent=True) else: form_data = request.form.to_dict() stream_flag = False for item in ['dify/plugins/docExpand', 'dify/plugins/translate', 'dify/chat/checkDoc', 'dify/chat/streamChat']: if item in router_path: stream_flag = True resp = requests.request( method=request.method, url=url, headers=headers, params=request.args, data=form_data, json=json_data, cookies=request.cookies, allow_redirects=False, stream=stream_flag ) if stream_flag: def generate(): try: # 对于SSE,通常按行处理更合适 for line in resp.iter_lines(decode_unicode=True): if line: logger.info(line) # 确保以正确的SSE格式转发 if line.startswith('data:'): yield f"{line}\n\n" else: yield f"{line}\n" # else: # yield '\n' # 保持心跳 except Exception as e: logger.error(f"流式传输中断: {e}") finally: resp.close() response_headers = {} for key, value in resp.raw.headers.items(): if key.lower() not in ['content-encoding', 'content-length', 'transfer-encoding', 'connection']: response_headers[key] = value return Response( stream_with_context(generate()), status=resp.status_code, headers=response_headers ) except Exception as e: logger.error(e) return jsonify({'code': -1}) # 构建 Flask 的响应 excluded_headers = ['content-encoding', 'content-length', 'transfer-encoding', 'connection'] response_headers = [(k, v) for k, v in resp.raw.headers.items() if k.lower() not in excluded_headers] return Response(resp.content, resp.status_code, response_headers) @logger.catch() @app.route('/health', methods=['GET']) def health(): return {'code': 200, 'msg': 'health'} @logger.catch() def open_file_by_wps(file_path): enable_wps_login_force() ext = os.path.splitext(file_path)[1][1:].lower() if ext not in file_type_map: return {'code': 3001, 'msg': '不支持该类型的文件'} if not os.path.exists(file_path): return {'code': 3006, 'msg': '文件不存在'} # 先检测 WPS 是否可用(可缓存结果,避免重复检测) # if not is_wps_available(): # return {'code': 3007, 'msg': '未检测到可用的 WPS Office,请安装 WPS 并确保已正确注册 COM 组件'} try: subprocess.Popen([file_type_map[ext][2], file_path]) return {'code': 1000, 'msg': '操作完成'} except Exception as e: logger.exception(f"未知错误: {e}") return {'code': 3009, 'msg': f'打开文件时发生未知错误: {e}'} # ext = os.path.splitext(file_path)[1][1:].lower() # if ext not in file_type_map: # return {'code': 3001, 'msg': '不支持该类型的文件'} # # if not os.path.exists(file_path): # return {'code': 3006, 'msg': '文件不存在'} # # # 先检测 WPS 是否可用(可缓存结果,避免重复检测) # if not is_wps_available(): # return {'code': 3007, 'msg': '未检测到可用的 WPS Office,请安装 WPS 并确保已正确注册 COM 组件'} # # progid, method_name = file_type_map[ext] # # pythoncom.CoInitialize() # app = None # try: # app = win32com.client.Dispatch(progid) # app.Visible = True # # 调用对应方法打开文件 # getattr(app, method_name).Open(os.path.abspath(file_path)) # return {'code': 1000, 'msg': '操作完成'} # except pywintypes.com_error as e: # logger.exception(f"WPS COM 操作失败: {e}") # return {'code': 3008, 'msg': f'WPS 打开文件失败,请检查文件是否损坏或 WPS 是否正常工作。错误: {e}'} # except Exception as e: # logger.exception(f"未知错误: {e}") # return {'code': 3009, 'msg': f'打开文件时发生未知错误: {e}'} # finally: # pythoncom.CoUninitialize() @logger.catch() @app.route('/download_and_open_file', methods=['POST']) def download_and_open_with_wps(): ''' 1000: 操作完成 2001: 检测到文档正在被编辑,无法下载 2002: 检测到远程文件已经更新本地文件也发生改动,终止操作 3001: 不支持该类型的文件 3002: 下载文件失败 :return: ''' file_id = request.get_json()['file_id'] is_template = request.get_json()['is_template'] # 判断是否为exe模式执行 if is_template: file_info = app.config['serve_client'].get_template_file_info(file_id) time_str = datetime.datetime.now().strftime('%Y-%m-%dT%H-%M-%S') local_path = os.path.join(current_app.config['work_path'], f'{file_info["name"]}({time_str}).{file_info["nameSuffix"]}') if app.config['serve_client'].download_template(file_info['fileLink'], local_path): return jsonify(open_file_by_wps(local_path)) else: return jsonify({'code': 3006, 'msg': '文件不存在'}) file_info = app.config['serve_client'].get_file_info(file_id) if not file_info: return jsonify({'code': 3006, 'msg': '文件不存在'}) local_file = os.path.join(current_app.config['work_path'], file_info['filePath'].replace('/', '_')) # 判断本地是否存在已下载的同名文件 if not os.path.exists(local_file) or not os.path.exists(local_file + '.metadata.json'): app.config['serve_client'].download_file(file_info, current_app.config['work_path']) return jsonify(open_file_by_wps(local_file)) # 判断本地文件是否被打开 if is_file_open_in_wps(local_file): return jsonify({'code': 1000, 'msg': '检测到文件已被打开'}) # 判断本地文件和线上文件是否一致 local_file_metadata = current_app.config['serve_client'].load_metadata(local_file) if local_file_metadata['update_time'] == file_info['updateTime']: return jsonify(open_file_by_wps(local_file)) if local_file_metadata['md5'] == app.config['serve_client'].get_file_md5(local_file): app.config['serve_client'].download_file(file_info, current_app.config['work_path']) return jsonify(open_file_by_wps(local_file)) return jsonify({'code': 2002, 'msg': '检测到远程文件已经更新本地文件也发生改动,终止操作'}) @logger.catch() @app.route('/upload_local_file', methods=['POST']) def upload_local_file(): file_name = request.get_json()['file_name'] if is_file_open_in_wps(os.path.join(current_app.config['work_path'], file_name)): return jsonify({'code': 3004, 'msg': '操作失败,文件已被打开无法操作'}) metadata = app.config['serve_client'].load_metadata(os.path.join(current_app.config['work_path'], file_name)) code = app.config['serve_client'].upload_file(os.path.join(current_app.config['work_path'], file_name)) if not code: return jsonify({'code': 3003, 'msg': '操作失败,上传文件失败'}) try: os.remove(os.path.join(current_app.config['work_path'], file_name)) os.remove(os.path.join(current_app.config['work_path'], file_name + '.metadata')) file_info = app.config['serve_client'].get_file_info(metadata['file_id']) if not file_info: return jsonify({'code': 3006, 'msg': '文件不存在'}) app.config['serve_client'].download_file(file_info, current_app.config['work_path']) except Exception as e: logger.exception(e) return jsonify({'code': 1000, 'msg': '文件上传成功,本地文件清除失败'}) return jsonify({'code': 1000, 'msg': '操作成功'}) @logger.catch() @app.route('/update_local_file', methods=['POST']) def update_local_file(): file_name = request.get_json()['file_name'] if is_file_open_in_wps(os.path.join(current_app.config['work_path'], file_name)): return jsonify({'code': 3004, 'msg': '操作失败,文件已被打开无法操作'}) try: metadata = current_app.config['serve_client'].load_metadata( os.path.join(current_app.config['work_path'], file_name)) os.remove(os.path.join(current_app.config['work_path'], file_name)) os.remove(os.path.join(current_app.config['work_path'], file_name + '.metadata')) except Exception as e: logger.exception(e) return jsonify({'code': 3005, 'msg': '操作失败,无权限操作本地文件'}) try: file_info = app.config['serve_client'].get_file_info(metadata['file_id']) if not app.config['serve_client'].download_file(file_info, current_app.config['work_path']): return jsonify({'code': 3005, 'msg': '文件下载失败'}) except Exception as e: logger.exception(e) return jsonify({'code': 3005, 'msg': '文件下载失败'}) return jsonify({'code': 1000, 'msg': '操作成功'}) @logger.catch() @app.route('/file_state_list', methods=['GET']) def file_state_list(): ''' state : 0 正常, 1: 云端有更新 2: 本地有变动且无冲突可以上传 3: 本地文件和云端文件有冲突 :return: ''' file_info_list = [] for file_name in os.listdir(current_app.config['work_path']): suffer = file_name.split('.')[-1] if suffer not in file_type_map: continue if not os.path.exists(os.path.join(current_app.config['work_path'], file_name + '.metadata')): continue metadata = current_app.config['serve_client'].load_metadata( os.path.join(current_app.config['work_path'], file_name)) file_info = { 'file_name': file_name, 'show_name': metadata['file_name'], 'cloud_update_time': metadata['update_time'], 'source_md5': metadata['md5'], 'state': 0 } cloud_file_info = current_app.config['serve_client'].get_file_info(metadata['file_id']) if cloud_file_info == 2: file_info['state'] = 4 file_info_list.append(file_info) continue if not cloud_file_info: file_info_list.append(file_info) continue if cloud_file_info['updateTime'] != metadata['update_time']: file_info['state'] = 1 try: file_md5 = get_file_md5(os.path.join(current_app.config['work_path'], file_name)) if file_md5 != metadata['md5']: if file_info['state'] == 1: file_info['state'] = 3 else: file_info['state'] = 2 except Exception as e: logger.exception(e) pass file_info_list.append(file_info) return jsonify({'code': 1000, 'msg': '操作成功', 'data': file_info_list}) @logger.catch() @app.route('/remove_file', methods=['POST']) def remove_file(): file_name = request.get_json()['file_name'] file_path = os.path.join(current_app.config['work_path'], file_name) if not os.path.exists(file_path): return jsonify({'code': '3006', 'msg': '文件不存在'}) try: os.remove(file_path) os.remove(f'{file_path}.metadata') except Exception as e: logger.exception(e) return jsonify({'code': 3005, 'msg': '操作失败,无权限操作本地文件'}) return jsonify({'code': 1000, 'msg': '操作成功'}) @logger.catch() @app.route('/start_wps_server', methods=['POST']) def start_wps_server(): logger.info(f'start_wps_server:') try: url = f"ksowpscloudsvr://start=RelayHttpServer&serverId=aef5ac0d-d5a3-49ee-b02f-c31eeb063f9b" os.startfile(url) time.sleep(1) requests.post('http://127.0.0.1:58890/version', data='{"serverId":"aef5ac0d-d5a3-49ee-b02f-c31eeb063f9b"}') requests.post('http://127.0.0.1:58890/redirect/runParams', data='{"serverId":"9b6f627b-68cc-40e9-8cae-2921f01d4cd9","data":"eyJtZXRob2QiOiJnZXQiLCJ1cmwiOiJodHRwOi8vMTI3LjAuMC4xOjU4NTUvcmliYm9uLnhtbCIsImRhdGEiOiIifQ=="}') except Exception as e: logger.error('start_wps_server with an error ') logger.exception(e) return jsonify({'code': 1000, 'msg': '操作成功'}) @logger.catch() @app.route('/open_file', methods=['POST']) def open_file(): file_name = request.get_json()['file_name'] file_path = os.path.join(current_app.config['work_path'], file_name) if not os.path.exists(file_path): return jsonify({'code': '3006', 'msg': '文件不存在'}) if is_file_open_in_wps(file_path): return jsonify({'code': 1000, 'msg': '检测到文件已被打开'}) return jsonify(open_file_by_wps(file_path)) @logger.catch() @app.route('/create_cloud_file', methods=['POST']) def create_cloud_file(): params = request.get_json() flag = app.config['serve_client'].create_cloud_file(local_file=params['local_file'], folder_id=params['folder_id']) return jsonify({'code': 1000, 'msg': '文件上传成功' if flag else '网络错误,上传失败。'}) @logger.catch() @app.route('/get_folder_tree', methods=['POST']) def get_base_path(): res = app.config['serve_client'].get_folder_tree() return jsonify(res) @logger.catch() @app.route('/get_token', methods=['POST']) def get_token(): return jsonify(app.config['serve_client'].login_reply) @logger.catch() @app.route('/refresh_token', methods=['POST']) def refresh_token(): app.config['serve_client'].login() return jsonify(app.config['serve_client'].login_reply) @logger.catch() @app.route('/get_server_url', methods=['GET']) def get_server_url(): return jsonify({'code': 1000, 'msg': '操作成功', 'data': {'server_url': SERVER_POINT}}) @logger.catch() @app.route('/open_platform', methods=['POST']) def open_platform(): url = request.get_json().get('url', '') webbrowser.open(url) return jsonify({'code': 1000, 'msg': '操作成功'}) @logger.catch() @app.route('/download_cloud_file', methods=['POST']) def download_cloud_file(): ''' 1000: 操作完成 2001: 检测到文档正在被编辑,无法下载 2002: 检测到远程文件已经更新本地文件也发生改动,终止操作 3001: 不支持该类型的文件 3002: 下载文件失败 :return: ''' file_id = request.get_json()['file_id'] is_template = request.get_json()['is_template'] # 判断是否为exe模式执行 if is_template: file_info = app.config['serve_client'].get_template_file_info(file_id) time_str = datetime.datetime.now().strftime('%Y-%m-%dT%H-%M-%S') local_path = os.path.join(current_app.config['work_path'], f'{file_info["name"]}({time_str}).{file_info["nameSuffix"]}') if app.config['serve_client'].download_template(file_info['fileLink'], local_path): return jsonify({'code': 1000, 'msg': '操作成功', 'file_path': local_path}) else: return jsonify({'code': 3006, 'msg': '文件不存在'}) file_info = app.config['serve_client'].get_file_info(file_id) if not file_info: return jsonify({'code': 3006, 'msg': '文件不存在'}) local_file = os.path.join(current_app.config['work_path'], file_info['filePath'].replace('/', '_')) # 判断本地是否存在已下载的同名文件 if not os.path.exists(local_file) or not os.path.exists(local_file + '.metadata.json'): try: app.config['serve_client'].download_file(file_info, current_app.config['work_path']) except Exception as e: logger.exception(e) return jsonify({'code': 3002, 'msg': '下载文件失败'}) return jsonify({'code': 1000, 'msg': '操作成功', 'file_path': local_file}) # 判断本地文件是否被打开 if is_file_open_in_wps(local_file): return jsonify({'code': 2001, 'msg': '检测到文档正在被编辑,无法下载'}) # 判断本地文件和线上文件是否一致 local_file_metadata = current_app.config['serve_client'].load_metadata(local_file) if local_file_metadata['update_time'] == file_info['updateTime']: return jsonify({'code': 1000, 'msg': '操作成功', 'file_path': local_file}) if local_file_metadata['md5'] == app.config['serve_client'].get_file_md5(local_file): app.config['serve_client'].download_file(file_info, current_app.config['work_path']) return jsonify({'code': 1000, 'msg': '操作成功', 'file_path': local_file}) return jsonify({'code': 2002, 'msg': '检测到远程文件已经更新本地文件也发生改动,终止操作'}) def start_flask(serve_client, work_path): app.config['serve_client'] = serve_client app.config['work_path'] = work_path try: app.run(host='127.0.0.1', port=API_SERVER_PORT) except KeyboardInterrupt: app.shutdown()