api_app.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556
  1. import datetime
  2. import gc
  3. import logging
  4. import os
  5. import subprocess
  6. import time
  7. import socket
  8. import pythoncom
  9. import pywintypes
  10. import requests
  11. import win32com.client
  12. import webbrowser
  13. from flask import Flask, jsonify, request, current_app, send_from_directory, Response, stream_with_context
  14. from flask_cors import CORS
  15. from requests_toolbelt import MultipartEncoder
  16. from win10toast import ToastNotifier
  17. from config import file_type_map, TARGET_URL, SERVER_POINT, API_SERVER_PORT
  18. from tools.check import is_file_open_in_wps, enable_wps_login_force
  19. from tools.file_manager import get_file_md5
  20. from tools.logger_handle import logger
  21. socket.getfqdn = lambda name=None: 'localhost'
  22. toaster = ToastNotifier()
  23. app = Flask(__name__, static_folder='../static')
  24. CORS(app, resources=r'/*')
  25. class InterceptHandler(logging.Handler):
  26. def emit(self, record):
  27. # 获取对应的 loguru 级别
  28. logger_opt = logger.opt(depth=6, exception=record.exc_info)
  29. logger_opt.log(record.levelname, record.getMessage())
  30. # 移除 Flask 默认的所有 Handler,并添加自定义的 InterceptHandler
  31. app.logger.handlers = []
  32. app.logger.addHandler(InterceptHandler())
  33. app.logger.setLevel(logging.INFO)
  34. # class InterceptHandler(logging.Handler):
  35. # def emit(self, record):
  36. # # 将标准日志记录转为 loguru
  37. # try:
  38. # level = logger.level(record.levelname).name
  39. # except ValueError:
  40. # level = record.levelno
  41. # logger.opt(depth=6, exception=record.exc_info).log(level, record.getMessage())
  42. #
  43. #
  44. # logging.basicConfig(handlers=[InterceptHandler()], level=logging.INFO)
  45. # for name in ("flask", "werkzeug", "watchdog"):
  46. # logging.getLogger(name).handlers = [InterceptHandler()]
  47. # logging.getLogger(name).propagate = False
  48. @app.route('/')
  49. def index():
  50. return send_from_directory(app.static_folder, 'index.html')
  51. # 代理静态文件(如 css, js, images 等)
  52. @app.route('/<path:path>')
  53. def static_proxy(path):
  54. return send_from_directory(app.static_folder, path)
  55. @logger.catch()
  56. @app.route('/pyapi/<path:router_path>', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH'])
  57. def proxy(router_path):
  58. url = f'{TARGET_URL}/{router_path}'
  59. logger.info(f'接收到请求{url}')
  60. raw_body = request.get_data(cache=True)
  61. # 获取 headers(排除 Host 避免冲突)
  62. headers = {key: value for key, value in request.headers if key.lower() != 'host'}
  63. headers[
  64. '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'
  65. try:
  66. if request.files or (request.content_type and 'multipart/form-data' in request.content_type.lower()):
  67. # 构造 Multipart 表单
  68. fields = {}
  69. # 添加普通字段
  70. if request.files:
  71. for key, value in request.form.items():
  72. fields[key] = value
  73. # 添加文件字段
  74. for key, file in request.files.items():
  75. fields[key] = (file.filename, file.stream, file.mimetype)
  76. form_data = MultipartEncoder(fields=fields)
  77. headers['Content-Type'] = form_data.content_type
  78. else:
  79. form_data = raw_body
  80. resp = requests.request(
  81. method=request.method,
  82. url=url,
  83. headers=headers,
  84. params=request.args,
  85. data=form_data,
  86. cookies=request.cookies,
  87. allow_redirects=False
  88. )
  89. else:
  90. # JSON 或普通表单
  91. json_data = None
  92. form_data = None
  93. if request.content_type and 'application/json' in request.content_type.lower():
  94. json_data = request.get_json(silent=True)
  95. else:
  96. form_data = request.form.to_dict()
  97. stream_flag = False
  98. for item in ['dify/plugins/docExpand', 'dify/plugins/translate', 'dify/chat/checkDoc',
  99. 'dify/chat/streamChat']:
  100. if item in router_path:
  101. stream_flag = True
  102. resp = requests.request(
  103. method=request.method,
  104. url=url,
  105. headers=headers,
  106. params=request.args,
  107. data=form_data,
  108. json=json_data,
  109. cookies=request.cookies,
  110. allow_redirects=False,
  111. stream=stream_flag
  112. )
  113. if stream_flag:
  114. def generate():
  115. try:
  116. # 对于SSE,通常按行处理更合适
  117. for line in resp.iter_lines(decode_unicode=True):
  118. if line:
  119. logger.info(line)
  120. # 确保以正确的SSE格式转发
  121. if line.startswith('data:'):
  122. yield f"{line}\n\n"
  123. else:
  124. yield f"{line}\n"
  125. # else:
  126. # yield '\n' # 保持心跳
  127. except Exception as e:
  128. logger.error(f"流式传输中断: {e}")
  129. finally:
  130. resp.close()
  131. response_headers = {}
  132. for key, value in resp.raw.headers.items():
  133. if key.lower() not in ['content-encoding', 'content-length', 'transfer-encoding', 'connection']:
  134. response_headers[key] = value
  135. return Response(
  136. stream_with_context(generate()),
  137. status=resp.status_code,
  138. headers=response_headers
  139. )
  140. except Exception as e:
  141. logger.error(e)
  142. return jsonify({'code': -1})
  143. # 构建 Flask 的响应
  144. excluded_headers = ['content-encoding', 'content-length', 'transfer-encoding', 'connection']
  145. response_headers = [(k, v) for k, v in resp.raw.headers.items() if k.lower() not in excluded_headers]
  146. return Response(resp.content, resp.status_code, response_headers)
  147. @logger.catch()
  148. @app.route('/health', methods=['GET'])
  149. def health():
  150. return {'code': 200, 'msg': 'health'}
  151. @logger.catch()
  152. def open_file_by_wps(file_path):
  153. enable_wps_login_force()
  154. ext = os.path.splitext(file_path)[1][1:].lower()
  155. if ext not in file_type_map:
  156. return {'code': 3001, 'msg': '不支持该类型的文件'}
  157. if not os.path.exists(file_path):
  158. return {'code': 3006, 'msg': '文件不存在'}
  159. # 先检测 WPS 是否可用(可缓存结果,避免重复检测)
  160. # if not is_wps_available():
  161. # return {'code': 3007, 'msg': '未检测到可用的 WPS Office,请安装 WPS 并确保已正确注册 COM 组件'}
  162. try:
  163. subprocess.Popen([file_type_map[ext][2], file_path])
  164. return {'code': 1000, 'msg': '操作完成'}
  165. except Exception as e:
  166. logger.exception(f"未知错误: {e}")
  167. return {'code': 3009, 'msg': f'打开文件时发生未知错误: {e}'}
  168. # ext = os.path.splitext(file_path)[1][1:].lower()
  169. # if ext not in file_type_map:
  170. # return {'code': 3001, 'msg': '不支持该类型的文件'}
  171. #
  172. # if not os.path.exists(file_path):
  173. # return {'code': 3006, 'msg': '文件不存在'}
  174. #
  175. # # 先检测 WPS 是否可用(可缓存结果,避免重复检测)
  176. # if not is_wps_available():
  177. # return {'code': 3007, 'msg': '未检测到可用的 WPS Office,请安装 WPS 并确保已正确注册 COM 组件'}
  178. #
  179. # progid, method_name = file_type_map[ext]
  180. #
  181. # pythoncom.CoInitialize()
  182. # app = None
  183. # try:
  184. # app = win32com.client.Dispatch(progid)
  185. # app.Visible = True
  186. # # 调用对应方法打开文件
  187. # getattr(app, method_name).Open(os.path.abspath(file_path))
  188. # return {'code': 1000, 'msg': '操作完成'}
  189. # except pywintypes.com_error as e:
  190. # logger.exception(f"WPS COM 操作失败: {e}")
  191. # return {'code': 3008, 'msg': f'WPS 打开文件失败,请检查文件是否损坏或 WPS 是否正常工作。错误: {e}'}
  192. # except Exception as e:
  193. # logger.exception(f"未知错误: {e}")
  194. # return {'code': 3009, 'msg': f'打开文件时发生未知错误: {e}'}
  195. # finally:
  196. # pythoncom.CoUninitialize()
  197. @logger.catch()
  198. @app.route('/download_and_open_file', methods=['POST'])
  199. def download_and_open_with_wps():
  200. '''
  201. 1000: 操作完成
  202. 2001: 检测到文档正在被编辑,无法下载
  203. 2002: 检测到远程文件已经更新本地文件也发生改动,终止操作
  204. 3001: 不支持该类型的文件
  205. 3002: 下载文件失败
  206. :return:
  207. '''
  208. file_id = request.get_json()['file_id']
  209. is_template = request.get_json()['is_template']
  210. # 判断是否为exe模式执行
  211. if is_template:
  212. file_info = app.config['serve_client'].get_template_file_info(file_id)
  213. time_str = datetime.datetime.now().strftime('%Y-%m-%dT%H-%M-%S')
  214. local_path = os.path.join(current_app.config['work_path'],
  215. f'{file_info["name"]}({time_str}).{file_info["nameSuffix"]}')
  216. if app.config['serve_client'].download_template(file_info['fileLink'], local_path):
  217. return jsonify(open_file_by_wps(local_path))
  218. else:
  219. return jsonify({'code': 3006, 'msg': '文件不存在'})
  220. file_info = app.config['serve_client'].get_file_info(file_id)
  221. if not file_info:
  222. return jsonify({'code': 3006, 'msg': '文件不存在'})
  223. local_file = os.path.join(current_app.config['work_path'], file_info['filePath'].replace('/', '_'))
  224. # 判断本地是否存在已下载的同名文件
  225. if not os.path.exists(local_file) or not os.path.exists(local_file + '.metadata.json'):
  226. app.config['serve_client'].download_file(file_info,
  227. current_app.config['work_path'])
  228. return jsonify(open_file_by_wps(local_file))
  229. # 判断本地文件是否被打开
  230. if is_file_open_in_wps(local_file):
  231. return jsonify({'code': 1000, 'msg': '检测到文件已被打开'})
  232. # 判断本地文件和线上文件是否一致
  233. local_file_metadata = current_app.config['serve_client'].load_metadata(local_file)
  234. if local_file_metadata['update_time'] == file_info['updateTime']:
  235. return jsonify(open_file_by_wps(local_file))
  236. if local_file_metadata['md5'] == app.config['serve_client'].get_file_md5(local_file):
  237. app.config['serve_client'].download_file(file_info, current_app.config['work_path'])
  238. return jsonify(open_file_by_wps(local_file))
  239. return jsonify({'code': 2002, 'msg': '检测到远程文件已经更新本地文件也发生改动,终止操作'})
  240. @logger.catch()
  241. @app.route('/upload_local_file', methods=['POST'])
  242. def upload_local_file():
  243. file_name = request.get_json()['file_name']
  244. if is_file_open_in_wps(os.path.join(current_app.config['work_path'], file_name)):
  245. return jsonify({'code': 3004, 'msg': '操作失败,文件已被打开无法操作'})
  246. metadata = app.config['serve_client'].load_metadata(os.path.join(current_app.config['work_path'], file_name))
  247. code = app.config['serve_client'].upload_file(os.path.join(current_app.config['work_path'], file_name))
  248. if not code:
  249. return jsonify({'code': 3003, 'msg': '操作失败,上传文件失败'})
  250. try:
  251. os.remove(os.path.join(current_app.config['work_path'], file_name))
  252. os.remove(os.path.join(current_app.config['work_path'], file_name + '.metadata'))
  253. file_info = app.config['serve_client'].get_file_info(metadata['file_id'])
  254. if not file_info:
  255. return jsonify({'code': 3006, 'msg': '文件不存在'})
  256. app.config['serve_client'].download_file(file_info, current_app.config['work_path'])
  257. except Exception as e:
  258. logger.exception(e)
  259. return jsonify({'code': 1000, 'msg': '文件上传成功,本地文件清除失败'})
  260. return jsonify({'code': 1000, 'msg': '操作成功'})
  261. @logger.catch()
  262. @app.route('/update_local_file', methods=['POST'])
  263. def update_local_file():
  264. file_name = request.get_json()['file_name']
  265. if is_file_open_in_wps(os.path.join(current_app.config['work_path'], file_name)):
  266. return jsonify({'code': 3004, 'msg': '操作失败,文件已被打开无法操作'})
  267. try:
  268. metadata = current_app.config['serve_client'].load_metadata(
  269. os.path.join(current_app.config['work_path'], file_name))
  270. os.remove(os.path.join(current_app.config['work_path'], file_name))
  271. os.remove(os.path.join(current_app.config['work_path'], file_name + '.metadata'))
  272. except Exception as e:
  273. logger.exception(e)
  274. return jsonify({'code': 3005, 'msg': '操作失败,无权限操作本地文件'})
  275. try:
  276. file_info = app.config['serve_client'].get_file_info(metadata['file_id'])
  277. if not app.config['serve_client'].download_file(file_info, current_app.config['work_path']):
  278. return jsonify({'code': 3005, 'msg': '文件下载失败'})
  279. except Exception as e:
  280. logger.exception(e)
  281. return jsonify({'code': 3005, 'msg': '文件下载失败'})
  282. return jsonify({'code': 1000, 'msg': '操作成功'})
  283. @logger.catch()
  284. @app.route('/file_state_list', methods=['GET'])
  285. def file_state_list():
  286. '''
  287. state : 0 正常, 1: 云端有更新 2: 本地有变动且无冲突可以上传 3: 本地文件和云端文件有冲突
  288. :return:
  289. '''
  290. file_info_list = []
  291. for file_name in os.listdir(current_app.config['work_path']):
  292. suffer = file_name.split('.')[-1]
  293. if suffer not in file_type_map:
  294. continue
  295. if not os.path.exists(os.path.join(current_app.config['work_path'], file_name + '.metadata')):
  296. continue
  297. metadata = current_app.config['serve_client'].load_metadata(
  298. os.path.join(current_app.config['work_path'], file_name))
  299. file_info = {
  300. 'file_name': file_name,
  301. 'show_name': metadata['file_name'],
  302. 'cloud_update_time': metadata['update_time'],
  303. 'source_md5': metadata['md5'],
  304. 'state': 0
  305. }
  306. cloud_file_info = current_app.config['serve_client'].get_file_info(metadata['file_id'])
  307. if cloud_file_info == 2:
  308. file_info['state'] = 4
  309. file_info_list.append(file_info)
  310. continue
  311. if not cloud_file_info:
  312. file_info_list.append(file_info)
  313. continue
  314. if cloud_file_info['updateTime'] != metadata['update_time']:
  315. file_info['state'] = 1
  316. try:
  317. file_md5 = get_file_md5(os.path.join(current_app.config['work_path'], file_name))
  318. if file_md5 != metadata['md5']:
  319. if file_info['state'] == 1:
  320. file_info['state'] = 3
  321. else:
  322. file_info['state'] = 2
  323. except Exception as e:
  324. logger.exception(e)
  325. pass
  326. file_info_list.append(file_info)
  327. return jsonify({'code': 1000, 'msg': '操作成功', 'data': file_info_list})
  328. @logger.catch()
  329. @app.route('/remove_file', methods=['POST'])
  330. def remove_file():
  331. file_name = request.get_json()['file_name']
  332. file_path = os.path.join(current_app.config['work_path'], file_name)
  333. if not os.path.exists(file_path):
  334. return jsonify({'code': '3006', 'msg': '文件不存在'})
  335. try:
  336. os.remove(file_path)
  337. os.remove(f'{file_path}.metadata')
  338. except Exception as e:
  339. logger.exception(e)
  340. return jsonify({'code': 3005, 'msg': '操作失败,无权限操作本地文件'})
  341. return jsonify({'code': 1000, 'msg': '操作成功'})
  342. @logger.catch()
  343. @app.route('/start_wps_server', methods=['POST'])
  344. def start_wps_server():
  345. logger.info(f'start_wps_server:')
  346. try:
  347. url = f"ksowpscloudsvr://start=RelayHttpServer&serverId=aef5ac0d-d5a3-49ee-b02f-c31eeb063f9b"
  348. os.startfile(url)
  349. time.sleep(1)
  350. requests.post('http://127.0.0.1:58890/version', data='{"serverId":"aef5ac0d-d5a3-49ee-b02f-c31eeb063f9b"}')
  351. requests.post('http://127.0.0.1:58890/redirect/runParams', data='{"serverId":"9b6f627b-68cc-40e9-8cae-2921f01d4cd9","data":"eyJtZXRob2QiOiJnZXQiLCJ1cmwiOiJodHRwOi8vMTI3LjAuMC4xOjU4NTUvcmliYm9uLnhtbCIsImRhdGEiOiIifQ=="}')
  352. except Exception as e:
  353. logger.error('start_wps_server with an error ')
  354. logger.exception(e)
  355. return jsonify({'code': 1000, 'msg': '操作成功'})
  356. @logger.catch()
  357. @app.route('/open_file', methods=['POST'])
  358. def open_file():
  359. file_name = request.get_json()['file_name']
  360. file_path = os.path.join(current_app.config['work_path'], file_name)
  361. if not os.path.exists(file_path):
  362. return jsonify({'code': '3006', 'msg': '文件不存在'})
  363. if is_file_open_in_wps(file_path):
  364. return jsonify({'code': 1000, 'msg': '检测到文件已被打开'})
  365. return jsonify(open_file_by_wps(file_path))
  366. @logger.catch()
  367. @app.route('/create_cloud_file', methods=['POST'])
  368. def create_cloud_file():
  369. params = request.get_json()
  370. flag = app.config['serve_client'].create_cloud_file(local_file=params['local_file'], folder_id=params['folder_id'])
  371. return jsonify({'code': 1000, 'msg': '文件上传成功' if flag else '网络错误,上传失败。'})
  372. @logger.catch()
  373. @app.route('/get_folder_tree', methods=['POST'])
  374. def get_base_path():
  375. res = app.config['serve_client'].get_folder_tree()
  376. return jsonify(res)
  377. @logger.catch()
  378. @app.route('/get_token', methods=['POST'])
  379. def get_token():
  380. return jsonify(app.config['serve_client'].login_reply)
  381. @logger.catch()
  382. @app.route('/refresh_token', methods=['POST'])
  383. def refresh_token():
  384. app.config['serve_client'].login()
  385. return jsonify(app.config['serve_client'].login_reply)
  386. @logger.catch()
  387. @app.route('/get_server_url', methods=['GET'])
  388. def get_server_url():
  389. return jsonify({'code': 1000, 'msg': '操作成功', 'data': {'server_url': SERVER_POINT}})
  390. @logger.catch()
  391. @app.route('/open_platform', methods=['POST'])
  392. def open_platform():
  393. url = request.get_json().get('url', '')
  394. webbrowser.open(url)
  395. return jsonify({'code': 1000, 'msg': '操作成功'})
  396. @logger.catch()
  397. @app.route('/download_cloud_file', methods=['POST'])
  398. def download_cloud_file():
  399. '''
  400. 1000: 操作完成
  401. 2001: 检测到文档正在被编辑,无法下载
  402. 2002: 检测到远程文件已经更新本地文件也发生改动,终止操作
  403. 3001: 不支持该类型的文件
  404. 3002: 下载文件失败
  405. :return:
  406. '''
  407. file_id = request.get_json()['file_id']
  408. is_template = request.get_json()['is_template']
  409. # 判断是否为exe模式执行
  410. if is_template:
  411. file_info = app.config['serve_client'].get_template_file_info(file_id)
  412. time_str = datetime.datetime.now().strftime('%Y-%m-%dT%H-%M-%S')
  413. local_path = os.path.join(current_app.config['work_path'],
  414. f'{file_info["name"]}({time_str}).{file_info["nameSuffix"]}')
  415. if app.config['serve_client'].download_template(file_info['fileLink'], local_path):
  416. return jsonify({'code': 1000, 'msg': '操作成功', 'file_path': local_path})
  417. else:
  418. return jsonify({'code': 3006, 'msg': '文件不存在'})
  419. file_info = app.config['serve_client'].get_file_info(file_id)
  420. if not file_info:
  421. return jsonify({'code': 3006, 'msg': '文件不存在'})
  422. local_file = os.path.join(current_app.config['work_path'], file_info['filePath'].replace('/', '_'))
  423. # 判断本地是否存在已下载的同名文件
  424. if not os.path.exists(local_file) or not os.path.exists(local_file + '.metadata.json'):
  425. try:
  426. app.config['serve_client'].download_file(file_info,
  427. current_app.config['work_path'])
  428. except Exception as e:
  429. logger.exception(e)
  430. return jsonify({'code': 3002, 'msg': '下载文件失败'})
  431. return jsonify({'code': 1000, 'msg': '操作成功', 'file_path': local_file})
  432. # 判断本地文件是否被打开
  433. if is_file_open_in_wps(local_file):
  434. return jsonify({'code': 2001, 'msg': '检测到文档正在被编辑,无法下载'})
  435. # 判断本地文件和线上文件是否一致
  436. local_file_metadata = current_app.config['serve_client'].load_metadata(local_file)
  437. if local_file_metadata['update_time'] == file_info['updateTime']:
  438. return jsonify({'code': 1000, 'msg': '操作成功', 'file_path': local_file})
  439. if local_file_metadata['md5'] == app.config['serve_client'].get_file_md5(local_file):
  440. app.config['serve_client'].download_file(file_info, current_app.config['work_path'])
  441. return jsonify({'code': 1000, 'msg': '操作成功', 'file_path': local_file})
  442. return jsonify({'code': 2002, 'msg': '检测到远程文件已经更新本地文件也发生改动,终止操作'})
  443. def start_flask(serve_client, work_path):
  444. app.config['serve_client'] = serve_client
  445. app.config['work_path'] = work_path
  446. try:
  447. app.run(host='127.0.0.1', port=API_SERVER_PORT)
  448. except KeyboardInterrupt:
  449. app.shutdown()