admin_app.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682
  1. # -*- coding: utf-8 -*-
  2. from flask import Flask, render_template, request, redirect, url_for, flash, session, jsonify, abort
  3. from minio import Minio
  4. from pymilvus import MilvusClient
  5. import os
  6. from dotenv import load_dotenv
  7. from datetime import timezone
  8. from functools import wraps
  9. import pytz
  10. # 加载环境变量
  11. load_dotenv()
  12. # 管理员账号
  13. ADMIN_USERNAME = os.getenv('ADMIN_USERNAME')
  14. ADMIN_PASSWORD = os.getenv('ADMIN_PASSWORD')
  15. # Minio 配置
  16. MINIO_BUCKET = os.getenv('MINIO_BUCKET')
  17. MINIO_ENDPOINT = os.getenv('MINIO_ENDPOINT')
  18. MINIO_ACCESS_KEY = os.getenv('MINIO_ACCESS_KEY')
  19. MINIO_SECRET_KEY = os.getenv('MINIO_SECRET_KEY')
  20. MINIO_REGION = os.getenv('MINIO_REGION')
  21. MINIO_USE_VIRTUAL_HOST = os.getenv('MINIO_USE_VIRTUAL_HOST', 'false').lower() == 'true'
  22. MINIO_USE_SSL = os.getenv('MINIO_USE_SSL', 'false').lower() == 'true'
  23. # Milvus 配置
  24. MILVUS_HOST = os.getenv("MILVUS_HOST")
  25. MILVUS_PORT = os.getenv("MILVUS_PORT")
  26. MILVUS_USER = os.getenv("MILVUS_USER")
  27. MILVUS_PASSWORD = os.getenv("MILVUS_PASSWORD")
  28. MILVUS_DATABASE = os.getenv("MILVUS_DATABASE")
  29. MILVUS_COLLECTION = os.getenv("MILVUS_COLLECTION")
  30. MILVUS_INDEX_NAME = os.getenv("MILVUS_INDEX_NAME")
  31. app = Flask(__name__)
  32. app.secret_key = os.getenv('FLASK_SECRET_KEY', 'default-secret-key')
  33. @app.before_request
  34. def before_request():
  35. """在每个请求之前执行域名白名单检查"""
  36. app.logger.info(f"before_request: {request.url}")
  37. def _get_minio_client() -> Minio:
  38. client = Minio(
  39. endpoint=MINIO_ENDPOINT,
  40. access_key=MINIO_ACCESS_KEY,
  41. secret_key=MINIO_SECRET_KEY,
  42. secure=MINIO_USE_SSL,
  43. region=MINIO_REGION,
  44. )
  45. if MINIO_USE_VIRTUAL_HOST:
  46. client.enable_virtual_style_endpoint()
  47. return client
  48. # 初始化 Minio 客户端
  49. minio_client = _get_minio_client()
  50. # 初始化 Milvus API
  51. milvus_client = MilvusClient(
  52. uri=f"http://{MILVUS_HOST}:{MILVUS_PORT}",
  53. token=f"{MILVUS_USER}:{MILVUS_PASSWORD}",
  54. db_name=MILVUS_DATABASE
  55. )
  56. def convert_utc_to_local(utc_datetime, timezone_name='Asia/Shanghai'):
  57. """
  58. 将UTC时间转换为本地时间
  59. Args:
  60. utc_datetime: UTC时间对象(datetime)
  61. timezone_name: 目标时区名称,默认为'Asia/Shanghai'(中国标准时间)
  62. Returns:
  63. str: 格式化后的本地时间字符串(YYYY-MM-DD HH:MM:SS)
  64. """
  65. if not utc_datetime:
  66. return ''
  67. try:
  68. # 确保UTC时间有时区信息
  69. if utc_datetime.tzinfo is None:
  70. utc_time = utc_datetime.replace(tzinfo=timezone.utc)
  71. else:
  72. utc_time = utc_datetime
  73. # 转换为目标时区
  74. local_tz = pytz.timezone(timezone_name)
  75. local_time = utc_time.astimezone(local_tz)
  76. # 格式化为字符串
  77. return local_time.strftime('%Y-%m-%d %H:%M:%S')
  78. except Exception as e:
  79. app.logger.error(f"时区转换失败: {str(e)}")
  80. # 如果转换失败,返回原始时间的字符串格式
  81. return utc_datetime.strftime('%Y-%m-%d %H:%M:%S') if utc_datetime else ''
  82. def login_required(f):
  83. """登录验证装饰器"""
  84. @wraps(f)
  85. def decorated_function(*args, **kwargs):
  86. if 'username' not in session:
  87. return redirect(url_for('login'))
  88. return f(*args, **kwargs)
  89. return decorated_function
  90. @app.route('/')
  91. def index():
  92. """首页,重定向到登录页面或文件浏览页面"""
  93. if 'username' in session:
  94. return redirect(url_for('file_browser'))
  95. return redirect(url_for('login'))
  96. @app.route('/login', methods=['GET', 'POST'])
  97. def login():
  98. """登录页面"""
  99. if request.method == 'POST':
  100. username = request.form['username']
  101. password = request.form['password']
  102. if username == ADMIN_USERNAME and password == ADMIN_PASSWORD:
  103. session['username'] = username
  104. flash('登录成功!', 'success')
  105. return redirect(url_for('file_browser'))
  106. else:
  107. flash('用户名或密码错误', 'error')
  108. return render_template('login.html')
  109. @app.route('/logout')
  110. def logout():
  111. """退出登录"""
  112. session.pop('username', None)
  113. flash('已退出登录', 'info')
  114. return redirect(url_for('login'))
  115. # 企业微信登录相关接口,暂时不使用
  116. #app.route('/wxwork/callback')
  117. #app.route('/wxwork/login')
  118. #app.route('/wxwork/verify', methods=['GET', 'POST'])
  119. #app.route('/api/wxwork/user')
  120. # 文件解析测试 API,暂时不使用
  121. #app.route('/api/file/<path:file_path>/parse-test', methods=['POST'])
  122. # 文件浏览页面
  123. @app.route('/files')
  124. @login_required
  125. def file_browser():
  126. """文件浏览页面"""
  127. path = request.args.get('path', '')
  128. return render_template('file_browser.html', current_path=path)
  129. # 获取文件列表 API
  130. @app.route('/api/files')
  131. @login_required
  132. def api_files():
  133. """获取文件列表 API"""
  134. path = request.args.get('path', '')
  135. try:
  136. # 测试 MinIO 连接
  137. if not minio_client.bucket_exists(MINIO_BUCKET):
  138. return jsonify({'error': f'桶 "{MINIO_BUCKET}" 不存在,请检查配置'}), 500
  139. # 获取 Minio 中的文件列表
  140. # 构建搜索前缀
  141. search_prefix = path if path == '' else (path if path.endswith('/') else path + '/')
  142. objects = minio_client.list_objects(MINIO_BUCKET, prefix=search_prefix, recursive=False)
  143. files = []
  144. folders = set()
  145. for obj in objects:
  146. # 跳过当前目录的标记对象
  147. if obj.object_name == search_prefix.rstrip('/'):
  148. continue
  149. # 计算相对路径
  150. if search_prefix:
  151. if not obj.object_name.startswith(search_prefix):
  152. continue
  153. relative_path = obj.object_name[len(search_prefix):]
  154. else:
  155. relative_path = obj.object_name
  156. # 跳过空路径
  157. if not relative_path:
  158. continue
  159. if '/' in relative_path:
  160. # 这是一个子目录中的对象,我们只需要目录名
  161. folder_name = relative_path.split('/')[0]
  162. if folder_name and folder_name not in folders:
  163. folders.add(folder_name)
  164. folder_path = path + '/' + folder_name if path else folder_name
  165. files.append({
  166. 'name': folder_name,
  167. 'path': folder_path,
  168. 'type': 'folder',
  169. 'size': 0,
  170. 'modified': ''
  171. })
  172. else:
  173. # 这是当前目录下的文件
  174. files.append({
  175. 'name': relative_path,
  176. 'path': obj.object_name,
  177. 'type': 'file',
  178. 'size': obj.size,
  179. 'modified': convert_utc_to_local(obj.last_modified)
  180. })
  181. return jsonify({'files': files})
  182. except Exception as e:
  183. app.logger.error(f"获取文件列表失败: {str(e)}")
  184. if "SignatureDoesNotMatch" in str(e):
  185. return jsonify({'error': 'MinIO 认证失败,请检查 ACCESS_KEY 和 SECRET_KEY 配置'}), 500
  186. elif "Connection" in str(e):
  187. return jsonify({'error': f'无法连接到 MinIO 服务器 ({os.getenv("ENDPOINT")}),请检查网络和地址配置'}), 500
  188. else:
  189. return jsonify({'error': f'获取文件列表失败: {str(e)}'}), 500
  190. # 文件详情页面
  191. @app.route('/file/<path:file_path>')
  192. @login_required
  193. def file_detail(file_path):
  194. """文件详情页面"""
  195. return render_template('file_detail.html', file_path=file_path)
  196. # 获取文件详情 API
  197. @app.route('/api/file/<path:file_path>')
  198. @login_required
  199. def api_file_detail(file_path):
  200. """获取文件详情 API"""
  201. try:
  202. # URL 解码文件路径(处理可能的双重编码)
  203. from urllib.parse import unquote
  204. # 先尝试解码一次,如果结果仍然是编码格式,再解码一次
  205. decoded_path = unquote(file_path)
  206. if '%' in decoded_path:
  207. decoded_path = unquote(decoded_path)
  208. file_path = decoded_path
  209. app.logger.debug(f"原始路径: {file_path}")
  210. app.logger.debug(f"解码后路径: {decoded_path}")
  211. # 从 Minio 获取文件基本信息
  212. try:
  213. file_stat = minio_client.stat_object(MINIO_BUCKET, file_path)
  214. file_info = {
  215. 'doc_name': os.path.basename(file_path),
  216. 'doc_path_name': file_path,
  217. 'file_size': file_stat.size,
  218. 'file_md5': file_stat.etag.strip('"'),
  219. 'last_modified': convert_utc_to_local(file_stat.last_modified),
  220. 'indexed': False,
  221. 'index_time': '',
  222. 'chunks': []
  223. }
  224. except Exception as e:
  225. app.logger.error(f"获取文件信息失败: {str(e)}")
  226. if "SignatureDoesNotMatch" in str(e):
  227. return jsonify({'error': 'MinIO 认证失败,请检查 ACCESS_KEY 和 SECRET_KEY 配置'}), 500
  228. elif "NoSuchKey" in str(e) or "not found" in str(e).lower():
  229. return jsonify({'error': f'文件不存在: {file_path}'}), 404
  230. else:
  231. return jsonify({'error': f'获取文件信息失败: {str(e)}'}), 500
  232. # 从 Milvus 查询文件索引信息
  233. try:
  234. milvus_client.load_collection(collection_name=MILVUS_COLLECTION)
  235. filter_expr = f'doc_path_name == "{file_path}"'
  236. results = milvus_client.query(
  237. collection_name=MILVUS_COLLECTION,
  238. filter=filter_expr,
  239. output_fields=["doc_name", "doc_path_name", "doc_type", "doc_md5", "doc_length", "content", "embedding_model"],
  240. limit=100
  241. )
  242. if results:
  243. file_info['indexed'] = True
  244. # 取第一条记录的基本信息
  245. first_record = results[0]
  246. file_info['doc_type'] = first_record.get('doc_type', '')
  247. file_info['indexed_md5'] = first_record.get('doc_md5', '')
  248. file_info['doc_length'] = first_record.get('doc_length', 0)
  249. file_info['embedding_model'] = first_record.get('embedding_model', '')
  250. # 处理所有分片
  251. for result in results:
  252. file_info['chunks'].append({
  253. 'content': result.get('content', ''),
  254. 'length': len(result.get('content', ''))
  255. })
  256. except Exception as e:
  257. print(f"查询 Milvus 失败: {e}")
  258. return jsonify(file_info)
  259. except Exception as e:
  260. return jsonify({'error': str(e)}), 500
  261. # 文件下载 API
  262. @app.route('/api/file/<path:file_path>/download')
  263. @login_required
  264. def api_download_file(file_path):
  265. """文件下载 API"""
  266. try:
  267. # URL 解码文件路径(处理可能的双重编码)
  268. from urllib.parse import unquote
  269. decoded_path = unquote(file_path)
  270. if '%' in decoded_path:
  271. decoded_path = unquote(decoded_path)
  272. file_path = decoded_path
  273. app.logger.info(f"下载文件: {file_path}")
  274. # 检查文件是否存在
  275. try:
  276. file_stat = minio_client.stat_object(MINIO_BUCKET, file_path)
  277. except Exception as e:
  278. if "NoSuchKey" in str(e) or "not found" in str(e).lower():
  279. return jsonify({'error': f'文件不存在: {file_path}'}), 404
  280. else:
  281. return jsonify({'error': f'获取文件信息失败: {str(e)}'}), 500
  282. # 从 MinIO 获取文件
  283. from flask import Response
  284. import io
  285. try:
  286. response = minio_client.get_object(MINIO_BUCKET, file_path)
  287. file_data = response.data
  288. # 获取文件名
  289. filename = os.path.basename(file_path)
  290. # 设置正确的 Content-Type
  291. content_type = file_stat.content_type or 'application/octet-stream'
  292. # 对文件名进行URL编码以支持中文字符
  293. from urllib.parse import quote
  294. encoded_filename = quote(filename, safe='')
  295. # 创建响应
  296. return Response(
  297. file_data,
  298. mimetype=content_type,
  299. headers={
  300. 'Content-Disposition': f'attachment; filename*=UTF-8\'\'{encoded_filename}',
  301. 'Content-Length': str(len(file_data)),
  302. 'Cache-Control': 'no-cache'
  303. }
  304. )
  305. except Exception as e:
  306. app.logger.error(f"下载文件失败: {str(e)}")
  307. return jsonify({'error': f'下载文件失败: {str(e)}'}), 500
  308. finally:
  309. if 'response' in locals():
  310. response.close()
  311. response.release_conn()
  312. except Exception as e:
  313. app.logger.error(f"下载请求处理失败: {str(e)}")
  314. return jsonify({'error': f'下载请求处理失败: {str(e)}'}), 500
  315. # 文件删除 API
  316. @app.route('/api/file/<path:file_path>/delete', methods=['DELETE'])
  317. @login_required
  318. def api_delete_file(file_path):
  319. """文件删除 API"""
  320. try:
  321. # URL 解码文件路径(处理可能的双重编码)
  322. from urllib.parse import unquote
  323. decoded_path = unquote(file_path)
  324. if '%' in decoded_path:
  325. decoded_path = unquote(decoded_path)
  326. file_path = decoded_path
  327. app.logger.info(f"删除文件: {file_path}")
  328. # 检查文件是否存在
  329. try:
  330. file_stat = minio_client.stat_object(MINIO_BUCKET, file_path)
  331. file_name = os.path.basename(file_path)
  332. except Exception as e:
  333. if "NoSuchKey" in str(e) or "not found" in str(e).lower():
  334. return jsonify({'error': f'文件不存在: {file_path}'}), 404
  335. else:
  336. return jsonify({'error': f'获取文件信息失败: {str(e)}'}), 500
  337. # 从 MinIO 删除文件
  338. try:
  339. minio_client.remove_object(MINIO_BUCKET, file_path)
  340. app.logger.info(f"文件删除成功: {file_path}")
  341. # 如果文件在 Milvus 中有索引,也删除索引记录
  342. try:
  343. milvus_client.load_collection(collection_name=MILVUS_COLLECTION)
  344. filter_expr = f'doc_path_name == "{file_path}"'
  345. # 查询是否存在索引记录
  346. results = milvus_client.query(
  347. collection_name=MILVUS_COLLECTION,
  348. filter=filter_expr,
  349. output_fields=["id"],
  350. limit=1000
  351. )
  352. if results:
  353. # 删除 Milvus 中的记录
  354. ids_to_delete = [str(result['id']) for result in results]
  355. milvus_client.delete(
  356. collection_name=MILVUS_COLLECTION,
  357. filter=f'id in {ids_to_delete}'
  358. )
  359. app.logger.info(f"已删除 Milvus 中的 {len(ids_to_delete)} 条记录")
  360. except Exception as e:
  361. app.logger.warning(f"删除 Milvus 索引时出错 (文件已删除): {str(e)}")
  362. return jsonify({
  363. 'success': True,
  364. 'message': f'文件 "{file_name}" 删除成功',
  365. 'file_name': file_name,
  366. 'file_path': file_path
  367. })
  368. except Exception as e:
  369. app.logger.error(f"删除文件失败: {str(e)}")
  370. return jsonify({'error': f'删除文件失败: {str(e)}'}), 500
  371. except Exception as e:
  372. app.logger.error(f"删除请求处理失败: {str(e)}")
  373. return jsonify({'error': f'删除请求处理失败: {str(e)}'}), 500
  374. # 创建目录 API
  375. @app.route('/api/create-directory', methods=['POST'])
  376. @login_required
  377. def api_create_directory():
  378. """创建目录 API"""
  379. try:
  380. data = request.get_json()
  381. if not data:
  382. return jsonify({'success': False, 'error': '无效的请求数据'}), 400
  383. current_path = data.get('path', '')
  384. dir_name = data.get('name', '').strip()
  385. # 验证目录名称
  386. if not dir_name:
  387. return jsonify({'success': False, 'error': '目录名称不能为空'}), 400
  388. # 检查非法字符
  389. import re
  390. if re.search(r'[/\\:*?"<>|]', dir_name):
  391. return jsonify({'success': False, 'error': '目录名称不能包含以下字符: / \\ : * ? " < > |'}), 400
  392. # 构建完整路径
  393. if current_path:
  394. full_path = current_path.rstrip('/') + '/' + dir_name + '/'
  395. else:
  396. full_path = dir_name + '/'
  397. app.logger.info(f"创建目录: {full_path}")
  398. # 检查目录是否已存在
  399. try:
  400. minio_client.stat_object(MINIO_BUCKET, full_path)
  401. return jsonify({'success': False, 'error': '目录已存在'}), 400
  402. except Exception:
  403. # 目录不存在,可以创建
  404. pass
  405. # 创建目录(通过上传一个空对象)
  406. from io import BytesIO
  407. minio_client.put_object(
  408. MINIO_BUCKET,
  409. full_path,
  410. BytesIO(b''),
  411. 0,
  412. content_type='application/x-directory'
  413. )
  414. app.logger.info(f"目录创建成功: {full_path}")
  415. return jsonify({'success': True, 'message': '目录创建成功'})
  416. except Exception as e:
  417. app.logger.error(f"创建目录失败: {str(e)}")
  418. return jsonify({'success': False, 'error': f'创建目录失败: {str(e)}'}), 500
  419. # 删除目录 API
  420. @app.route('/api/delete-directory', methods=['DELETE'])
  421. @login_required
  422. def api_delete_directory():
  423. """删除目录 API"""
  424. try:
  425. data = request.get_json()
  426. if not data:
  427. return jsonify({'success': False, 'error': '无效的请求数据'}), 400
  428. dir_path = data.get('path', '').strip()
  429. # 验证路径
  430. if not dir_path:
  431. return jsonify({'success': False, 'error': '目录路径不能为空'}), 400
  432. # 确保路径以 / 结尾
  433. if not dir_path.endswith('/'):
  434. dir_path += '/'
  435. app.logger.info(f"删除目录: {dir_path}")
  436. # 检查目录是否存在
  437. try:
  438. minio_client.stat_object(MINIO_BUCKET, dir_path)
  439. except Exception:
  440. return jsonify({'success': False, 'error': '目录不存在'}), 404
  441. # 检查目录是否为空(不包含任何文件或子目录)
  442. objects = list(minio_client.list_objects(MINIO_BUCKET, prefix=dir_path, recursive=True))
  443. # 过滤掉目录本身
  444. content_objects = [obj for obj in objects if obj.object_name != dir_path]
  445. if content_objects:
  446. return jsonify({'success': False, 'error': '目录不为空,无法删除'}), 400
  447. # 删除目录
  448. minio_client.remove_object(MINIO_BUCKET, dir_path)
  449. app.logger.info(f"目录删除成功: {dir_path}")
  450. return jsonify({'success': True, 'message': '目录删除成功'})
  451. except Exception as e:
  452. app.logger.error(f"删除目录失败: {str(e)}")
  453. return jsonify({'success': False, 'error': f'删除目录失败: {str(e)}'}), 500
  454. # 上传文件 API
  455. @app.route('/api/upload', methods=['POST'])
  456. @login_required
  457. def api_upload_files():
  458. """文件上传 API"""
  459. try:
  460. # 获取上传路径
  461. upload_path = request.form.get('path', '')
  462. # 检查是否有文件
  463. if 'files' not in request.files:
  464. return jsonify({'error': '没有选择文件'}), 400
  465. files = request.files.getlist('files')
  466. if not files or all(f.filename == '' for f in files):
  467. return jsonify({'error': '没有选择有效的文件'}), 400
  468. app.logger.info(f"开始上传 {len(files)} 个文件到路径: {upload_path}")
  469. uploaded_files = []
  470. failed_files = []
  471. for file in files:
  472. if file.filename == '':
  473. continue
  474. try:
  475. # 构建完整的对象路径
  476. if upload_path:
  477. object_name = f"{upload_path}/{file.filename}"
  478. else:
  479. object_name = file.filename
  480. # 检查文件是否已存在
  481. try:
  482. minio_client.stat_object(MINIO_BUCKET, object_name)
  483. failed_files.append({
  484. 'filename': file.filename,
  485. 'error': '文件已存在'
  486. })
  487. continue
  488. except:
  489. # 文件不存在,可以上传
  490. pass
  491. # 上传文件到 MinIO
  492. file.seek(0) # 重置文件指针
  493. file_data = file.read()
  494. file_size = len(file_data)
  495. # 检查文件大小限制 (500MB)
  496. max_size = 500 * 1024 * 1024
  497. if file_size > max_size:
  498. failed_files.append({
  499. 'filename': file.filename,
  500. 'error': f'文件过大 ({file_size / 1024 / 1024:.2f} MB > 500 MB)'
  501. })
  502. continue
  503. # 重置文件指针并上传
  504. file.seek(0)
  505. minio_client.put_object(
  506. MINIO_BUCKET,
  507. object_name,
  508. file,
  509. file_size,
  510. content_type=file.content_type or 'application/octet-stream'
  511. )
  512. uploaded_files.append({
  513. 'filename': file.filename,
  514. 'object_name': object_name,
  515. 'size': file_size
  516. })
  517. app.logger.info(f"文件上传成功: {object_name}")
  518. except Exception as e:
  519. app.logger.error(f"上传文件 {file.filename} 失败: {str(e)}")
  520. failed_files.append({
  521. 'filename': file.filename,
  522. 'error': str(e)
  523. })
  524. # 构造返回结果
  525. result = {
  526. 'success': len(uploaded_files) > 0,
  527. 'uploaded_count': len(uploaded_files),
  528. 'failed_count': len(failed_files),
  529. 'uploaded_files': uploaded_files,
  530. 'failed_files': failed_files,
  531. 'message': f'成功上传 {len(uploaded_files)} 个文件'
  532. }
  533. if failed_files:
  534. result['message'] += f',{len(failed_files)} 个文件上传失败'
  535. return jsonify(result)
  536. except Exception as e:
  537. app.logger.error(f"文件上传处理失败: {str(e)}")
  538. return jsonify({'error': f'文件上传处理失败: {str(e)}'}), 500
  539. if __name__ == '__main__':
  540. # 获取主机和端口配置
  541. host = os.getenv('FLASK_HOST')
  542. port = int(os.getenv('FLASK_PORT'))
  543. print("=" * 60)
  544. print("Mildoc 管理后台启动中...")
  545. print(f"访问端口: {port}")
  546. print("=" * 60)
  547. app.run(
  548. host=host,
  549. port=port,
  550. debug=os.getenv('FLASK_DEBUG', 'False').lower() == 'true',
  551. threaded=True,
  552. processes=1
  553. )