diff --git a/COMMIT_DAY1.txt b/COMMIT_DAY1.txt index 9ac5e37f..c27cc2d2 100644 --- a/COMMIT_DAY1.txt +++ b/COMMIT_DAY1.txt @@ -53,6 +53,9 @@ Status: Day 1 complete (11/11 tasks), ready for Day 2 + + + diff --git a/DC模块代码恢复指南.md b/DC模块代码恢复指南.md index 86b4a545..101fa591 100644 --- a/DC模块代码恢复指南.md +++ b/DC模块代码恢复指南.md @@ -283,6 +283,9 @@ + + + diff --git a/SAE_WECHAT_MP_DEPLOY_STEPS.md b/SAE_WECHAT_MP_DEPLOY_STEPS.md index fb0f4dd8..f888b4b6 100644 --- a/SAE_WECHAT_MP_DEPLOY_STEPS.md +++ b/SAE_WECHAT_MP_DEPLOY_STEPS.md @@ -229,6 +229,9 @@ https://iit.xunzhengyixue.com/api/v1/iit/health + + + diff --git a/backend/.gitignore b/backend/.gitignore index 126419de..6170bb29 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -1,5 +1,3 @@ -node_modules +node_modules # Keep environment variables out of version control -.env - /src/generated/prisma diff --git a/backend/DEPLOY_TO_SAE_FOR_WECHAT_MP.md b/backend/DEPLOY_TO_SAE_FOR_WECHAT_MP.md index c853ed3e..e17c5a16 100644 --- a/backend/DEPLOY_TO_SAE_FOR_WECHAT_MP.md +++ b/backend/DEPLOY_TO_SAE_FOR_WECHAT_MP.md @@ -158,6 +158,9 @@ https://iit.xunzhengyixue.com/api/v1/iit/health + + + diff --git a/backend/RESTART_SERVER_NOW.md b/backend/RESTART_SERVER_NOW.md index e064c141..226660dd 100644 --- a/backend/RESTART_SERVER_NOW.md +++ b/backend/RESTART_SERVER_NOW.md @@ -59,6 +59,9 @@ + + + diff --git a/backend/WECHAT_MP_CONFIG_READY.md b/backend/WECHAT_MP_CONFIG_READY.md index 49b1df7e..abbd8461 100644 --- a/backend/WECHAT_MP_CONFIG_READY.md +++ b/backend/WECHAT_MP_CONFIG_READY.md @@ -319,6 +319,9 @@ npx tsx src/modules/iit-manager/test-patient-wechat-url-verify.ts + + + diff --git a/backend/WECHAT_MP_QUICK_FIX.md b/backend/WECHAT_MP_QUICK_FIX.md index 42ca4e41..1ea4a126 100644 --- a/backend/WECHAT_MP_QUICK_FIX.md +++ b/backend/WECHAT_MP_QUICK_FIX.md @@ -181,6 +181,9 @@ npm run dev + + + diff --git a/backend/check_db.ts b/backend/check_db.ts index a11cb650..74aa50ff 100644 --- a/backend/check_db.ts +++ b/backend/check_db.ts @@ -62,3 +62,6 @@ main() + + + diff --git a/backend/check_db_data.ts b/backend/check_db_data.ts index 3a84a859..22a9dc60 100644 --- a/backend/check_db_data.ts +++ b/backend/check_db_data.ts @@ -56,3 +56,6 @@ main() + + + diff --git a/backend/check_iit.ts b/backend/check_iit.ts index 76752126..06a73861 100644 --- a/backend/check_iit.ts +++ b/backend/check_iit.ts @@ -51,3 +51,6 @@ main() + + + diff --git a/backend/check_iit_asl_data.ts b/backend/check_iit_asl_data.ts index 3a776398..a017b742 100644 --- a/backend/check_iit_asl_data.ts +++ b/backend/check_iit_asl_data.ts @@ -83,3 +83,6 @@ main() + + + diff --git a/backend/check_queue_table.ts b/backend/check_queue_table.ts index 9a1e1801..d2dc142c 100644 --- a/backend/check_queue_table.ts +++ b/backend/check_queue_table.ts @@ -46,3 +46,6 @@ main() + + + diff --git a/backend/check_rvw_issue.ts b/backend/check_rvw_issue.ts index f201f83d..03b9fe23 100644 --- a/backend/check_rvw_issue.ts +++ b/backend/check_rvw_issue.ts @@ -87,3 +87,6 @@ main() + + + diff --git a/backend/check_tables.ts b/backend/check_tables.ts index fb9486dc..0635ec56 100644 --- a/backend/check_tables.ts +++ b/backend/check_tables.ts @@ -34,3 +34,6 @@ main() + + + diff --git a/backend/compare_db.ts b/backend/compare_db.ts index a2b71dfe..664a1ea8 100644 --- a/backend/compare_db.ts +++ b/backend/compare_db.ts @@ -122,3 +122,6 @@ main() + + + diff --git a/backend/compare_dc_asl.ts b/backend/compare_dc_asl.ts index 494c24d7..8b254c03 100644 --- a/backend/compare_dc_asl.ts +++ b/backend/compare_dc_asl.ts @@ -93,3 +93,6 @@ main() + + + diff --git a/backend/compare_pkb_aia_rvw.ts b/backend/compare_pkb_aia_rvw.ts index 2678af6e..438ee7a3 100644 --- a/backend/compare_pkb_aia_rvw.ts +++ b/backend/compare_pkb_aia_rvw.ts @@ -79,3 +79,6 @@ main() + + + diff --git a/backend/compare_schema_db.ts b/backend/compare_schema_db.ts index 51f419c6..e6318487 100644 --- a/backend/compare_schema_db.ts +++ b/backend/compare_schema_db.ts @@ -121,3 +121,6 @@ main() + + + diff --git a/backend/create_mock_user.sql b/backend/create_mock_user.sql index 6b4003fd..52212f6e 100644 --- a/backend/create_mock_user.sql +++ b/backend/create_mock_user.sql @@ -32,3 +32,6 @@ ON CONFLICT (id) DO NOTHING; + + + diff --git a/backend/create_mock_user_platform.sql b/backend/create_mock_user_platform.sql index eafc20e2..ff038be9 100644 --- a/backend/create_mock_user_platform.sql +++ b/backend/create_mock_user_platform.sql @@ -64,3 +64,6 @@ ON CONFLICT (id) DO NOTHING; + + + diff --git a/backend/env.example.md b/backend/env.example.md new file mode 100644 index 00000000..6d53e2d1 --- /dev/null +++ b/backend/env.example.md @@ -0,0 +1,79 @@ +# 环境变量配置示例 + +复制以下内容到 `.env` 文件中: + +```bash +# =========================================== +# AI Clinical Research Platform - 环境变量配置 +# =========================================== + +# ==================== 应用配置 ==================== +NODE_ENV=development +PORT=3001 +HOST=0.0.0.0 +LOG_LEVEL=debug +SERVICE_NAME=aiclinical-backend + +# ==================== 数据库配置 ==================== +DATABASE_URL=postgresql://postgres:postgres@localhost:5432/ai_clinical + +# ==================== 存储配置 ==================== +# 存储类型:local | oss +STORAGE_TYPE=local + +# --- 本地存储配置(STORAGE_TYPE=local)--- +LOCAL_STORAGE_DIR=uploads +LOCAL_STORAGE_URL=http://localhost:3001/uploads + +# --- 阿里云OSS配置(STORAGE_TYPE=oss)--- +# OSS_REGION=oss-cn-beijing +# OSS_BUCKET=ai-clinical-data-dev +# OSS_BUCKET_STATIC=ai-clinical-static-dev +# OSS_ACCESS_KEY_ID=your-access-key-id +# OSS_ACCESS_KEY_SECRET=your-access-key-secret +# OSS_INTERNAL=false +# OSS_SIGNED_URL_EXPIRES=3600 + +# ==================== 安全配置 ==================== +JWT_SECRET=your-secret-key-change-in-production +JWT_EXPIRES_IN=7d +CORS_ORIGIN=http://localhost:5173 + +# ==================== LLM API配置 ==================== +DEEPSEEK_API_KEY= +DEEPSEEK_BASE_URL=https://api.deepseek.com +DASHSCOPE_API_KEY= +GEMINI_API_KEY= + +# ==================== 文件上传配置 ==================== +UPLOAD_MAX_SIZE=31457280 +``` + +## OSS 开发环境配置 + +如果要测试 OSS,将 `STORAGE_TYPE` 改为 `oss` 并填写以下配置: + +```bash +STORAGE_TYPE=oss +OSS_REGION=oss-cn-beijing +OSS_BUCKET=ai-clinical-data-dev +OSS_BUCKET_STATIC=ai-clinical-static-dev +OSS_ACCESS_KEY_ID=LTAI5tBHkL39GjdLfcr77Y3f +OSS_ACCESS_KEY_SECRET=<从安全渠道获取> +OSS_INTERNAL=false +OSS_SIGNED_URL_EXPIRES=3600 +``` + +## OSS 生产环境配置(SAE) + +```bash +STORAGE_TYPE=oss +OSS_REGION=oss-cn-beijing +OSS_BUCKET=ai-clinical-data +OSS_BUCKET_STATIC=ai-clinical-static +OSS_ACCESS_KEY_ID=LTAI5tBHkL39GjdLfcr77Y3f +OSS_ACCESS_KEY_SECRET=<从安全渠道获取> +OSS_INTERNAL=true # 🔴 生产必须用内网 +OSS_SIGNED_URL_EXPIRES=3600 +``` + diff --git a/backend/migrations/add_data_stats_to_tool_c_session.sql b/backend/migrations/add_data_stats_to_tool_c_session.sql index d79eff35..a8fc4a83 100644 --- a/backend/migrations/add_data_stats_to_tool_c_session.sql +++ b/backend/migrations/add_data_stats_to_tool_c_session.sql @@ -78,6 +78,9 @@ WHERE table_schema = 'dc_schema' + + + diff --git a/backend/package-lock.json b/backend/package-lock.json index 27dd7b1d..8815978f 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -16,6 +16,7 @@ "@types/form-data": "^2.2.1", "@wecom/crypto": "^1.0.1", "ajv": "^8.17.1", + "ali-oss": "^6.23.0", "axios": "^1.12.2", "bcryptjs": "^2.4.3", "bullmq": "^5.65.0", @@ -41,6 +42,7 @@ "zod": "^4.1.12" }, "devDependencies": { + "@types/ali-oss": "^6.23.1", "@types/bcryptjs": "^2.4.6", "@types/js-yaml": "^4.0.9", "@types/jsonwebtoken": "^9.0.7", @@ -1024,6 +1026,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/ali-oss": { + "version": "6.23.1", + "resolved": "https://registry.npmmirror.com/@types/ali-oss/-/ali-oss-6.23.1.tgz", + "integrity": "sha512-4mmCq2gUPBaPo6UlLIS/wMc7LxzDCzEUlRJAbLEk68Ntw7KWuF4RUwrQz3gtJkAeIYQ/R6PN3IvPTqk8daVVKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/bcryptjs": { "version": "2.4.6", "resolved": "https://registry.npmmirror.com/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", @@ -1166,6 +1175,15 @@ "node": ">=0.4.0" } }, + "node_modules/address": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/address/-/address-1.2.2.tgz", + "integrity": "sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/adler-32": { "version": "1.3.1", "resolved": "https://registry.npmmirror.com/adler-32/-/adler-32-1.3.1.tgz", @@ -1175,6 +1193,18 @@ "node": ">=0.8" } }, + "node_modules/agentkeepalive": { + "version": "3.5.3", + "resolved": "https://registry.npmmirror.com/agentkeepalive/-/agentkeepalive-3.5.3.tgz", + "integrity": "sha512-yqXL+k5rr8+ZRpOAntkaaRgWgE5o8ESAj5DyRmVTCSoZxXmqemb9Dd7T4i5UzwuERdLAJUy6XzR9zFVuf0kzkw==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/ajv": { "version": "8.17.1", "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.17.1.tgz", @@ -1208,6 +1238,57 @@ } } }, + "node_modules/ali-oss": { + "version": "6.23.0", + "resolved": "https://registry.npmmirror.com/ali-oss/-/ali-oss-6.23.0.tgz", + "integrity": "sha512-FipRmyd16Pr/tEey/YaaQ/24Pc3HEpLM9S1DRakEuXlSLXNIJnu1oJtHM53eVYpvW3dXapSjrip3xylZUTIZVQ==", + "license": "MIT", + "dependencies": { + "address": "^1.2.2", + "agentkeepalive": "^3.4.1", + "bowser": "^1.6.0", + "copy-to": "^2.0.1", + "dateformat": "^2.0.0", + "debug": "^4.3.4", + "destroy": "^1.0.4", + "end-or-error": "^1.0.1", + "get-ready": "^1.0.0", + "humanize-ms": "^1.2.0", + "is-type-of": "^1.4.0", + "js-base64": "^2.5.2", + "jstoxml": "^2.0.0", + "lodash": "^4.17.21", + "merge-descriptors": "^1.0.1", + "mime": "^2.4.5", + "platform": "^1.3.1", + "pump": "^3.0.0", + "qs": "^6.4.0", + "sdk-base": "^2.0.1", + "stream-http": "2.8.2", + "stream-wormhole": "^1.0.4", + "urllib": "^2.44.0", + "utility": "^1.18.0", + "xml2js": "^0.6.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ali-oss/node_modules/dateformat": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/dateformat/-/dateformat-2.2.0.tgz", + "integrity": "sha512-GODcnWq3YGoTnygPfi02ygEiRxqUxpJwuRHjdhJYuxpcZmDq4rjBiXYmbCCzStxo176ixfLT6i4NPwQooRySnw==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "license": "MIT" + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz", @@ -1482,6 +1563,12 @@ "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", "license": "MIT" }, + "node_modules/bowser": { + "version": "1.9.4", + "resolved": "https://registry.npmmirror.com/bowser/-/bowser-1.9.4.tgz", + "integrity": "sha512-9IdMmj2KjigRq6oWhmwv1W36pDuA4STQZ8q6YO9um+x07xgYNCD3Oou+WP/3L1HNz7iqythGet3/p4wvc8AAwQ==", + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -1561,6 +1648,12 @@ "node": ">=0.2.0" } }, + "node_modules/builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==", + "license": "MIT" + }, "node_modules/bullmq": { "version": "5.65.0", "resolved": "https://registry.npmmirror.com/bullmq/-/bullmq-5.65.0.tgz", @@ -1642,6 +1735,22 @@ "node": ">= 0.4" } }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/canvg": { "version": "3.0.11", "resolved": "https://registry.npmmirror.com/canvg/-/canvg-3.0.11.tgz", @@ -1837,6 +1946,15 @@ "node": "^14.18.0 || >=16.10.0" } }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cookie": { "version": "1.0.2", "resolved": "https://registry.npmmirror.com/cookie/-/cookie-1.0.2.tgz", @@ -1846,6 +1964,12 @@ "node": ">=18" } }, + "node_modules/copy-to": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/copy-to/-/copy-to-2.0.1.tgz", + "integrity": "sha512-3DdaFaU/Zf1AnpLiFDeNCD4TOWe3Zl2RZaTzUvWiIk5ERzcCodOE20Vqq4fzCbNoHURFHT4/us/Lfq+S2zyY4w==", + "license": "MIT" + }, "node_modules/core-js": { "version": "3.46.0", "resolved": "https://registry.npmmirror.com/core-js/-/core-js-3.46.0.tgz", @@ -1985,6 +2109,18 @@ "node": ">=16.0.0" } }, + "node_modules/default-user-agent": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/default-user-agent/-/default-user-agent-1.0.0.tgz", + "integrity": "sha512-bDF7bg6OSNcSwFWPu4zYKpVkJZQYVrAANMYB8bc9Szem1D0yKdm4sa/rOCs2aC9+2GMqQ7KnwtZRvDhmLF0dXw==", + "license": "MIT", + "dependencies": { + "os-name": "~1.0.3" + }, + "engines": { + "node": ">= 0.10.0" + } + }, "node_modules/defu": { "version": "6.1.4", "resolved": "https://registry.npmmirror.com/defu/-/defu-6.1.4.tgz", @@ -2024,6 +2160,16 @@ "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", "license": "MIT" }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", @@ -2050,6 +2196,15 @@ "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==", "license": "Apache-2.0" }, + "node_modules/digest-header": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/digest-header/-/digest-header-1.1.0.tgz", + "integrity": "sha512-glXVh42vz40yZb9Cq2oMOt70FIoWiv+vxNvdKdU8CwjLad25qHM3trLxhl9bVjdr6WaslIXhWpn0NO8T/67Qjg==", + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/dompurify": { "version": "3.3.0", "resolved": "https://registry.npmmirror.com/dompurify/-/dompurify-3.3.0.tgz", @@ -2134,6 +2289,12 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, "node_modules/effect": { "version": "3.16.12", "resolved": "https://registry.npmmirror.com/effect/-/effect-3.16.12.tgz", @@ -2168,6 +2329,15 @@ "once": "^1.4.0" } }, + "node_modules/end-or-error": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/end-or-error/-/end-or-error-1.0.1.tgz", + "integrity": "sha512-OclLMSug+k2A0JKuf494im25ANRBVW8qsjmwbgX7lQ8P82H21PQ1PWkoYwb9y5yMBS69BPlwtzdIFClo3+7kOQ==", + "license": "MIT", + "engines": { + "node": ">= 0.11.14" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", @@ -2255,6 +2425,12 @@ "@esbuild/win32-x64": "0.25.10" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, "node_modules/eventemitter3": { "version": "5.0.1", "resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-5.0.1.tgz", @@ -2297,6 +2473,18 @@ "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", "license": "MIT" }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/fast-check": { "version": "3.23.2", "resolved": "https://registry.npmmirror.com/fast-check/-/fast-check-3.23.2.tgz", @@ -2611,6 +2799,18 @@ "node": ">= 6" } }, + "node_modules/formstream": { + "version": "1.5.2", + "resolved": "https://registry.npmmirror.com/formstream/-/formstream-1.5.2.tgz", + "integrity": "sha512-NASf0lgxC1AyKNXQIrXTEYkiX99LhCEXTkiGObXAkpBui86a4u8FjH1o2bGb3PpqI3kafC+yw4zWeK6l6VHTgg==", + "license": "MIT", + "dependencies": { + "destroy": "^1.0.4", + "mime": "^2.5.2", + "node-hex": "^1.0.1", + "pause-stream": "~0.0.11" + } + }, "node_modules/frac": { "version": "1.1.2", "resolved": "https://registry.npmmirror.com/frac/-/frac-1.1.2.tgz", @@ -2709,6 +2909,12 @@ "node": ">= 0.4" } }, + "node_modules/get-ready": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/get-ready/-/get-ready-1.0.0.tgz", + "integrity": "sha512-mFXCZPJIlcYcth+N8267+mghfYN9h3EhsDa6JSnbA3Wrhh/XFpuowviFcsDeYZtKspQyWyJqfs4O6P8CHeTwzw==", + "license": "MIT" + }, "node_modules/get-tsconfig": { "version": "4.12.0", "resolved": "https://registry.npmmirror.com/get-tsconfig/-/get-tsconfig-4.12.0.tgz", @@ -2888,6 +3094,27 @@ "node": ">=8.0.0" } }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz", @@ -2997,6 +3224,21 @@ "node": ">=8" } }, + "node_modules/is-class-hotfix": { + "version": "0.0.6", + "resolved": "https://registry.npmmirror.com/is-class-hotfix/-/is-class-hotfix-0.0.6.tgz", + "integrity": "sha512-0n+pzCC6ICtVr/WXnN2f03TK/3BfXY7me4cjCAqT8TYXEl0+JBRoqBo94JJHXcyDSLUeWbNX8Fvy5g5RJdAstQ==", + "license": "MIT" + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmmirror.com/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3042,12 +3284,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-type-of": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/is-type-of/-/is-type-of-1.4.0.tgz", + "integrity": "sha512-EddYllaovi5ysMLMEN7yzHEKh8A850cZ7pykrY1aNRQGn/CDjRDE9qEWbIdt7xGEVJmjBXzU/fNnC4ABTm8tEQ==", + "license": "MIT", + "dependencies": { + "core-util-is": "^1.0.2", + "is-class-hotfix": "~0.0.6", + "isstream": "~0.1.2" + } + }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "license": "MIT" }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmmirror.com/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "license": "MIT" + }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmmirror.com/jiti/-/jiti-2.6.1.tgz", @@ -3067,6 +3326,12 @@ "node": ">=10" } }, + "node_modules/js-base64": { + "version": "2.6.4", + "resolved": "https://registry.npmmirror.com/js-base64/-/js-base64-2.6.4.tgz", + "integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==", + "license": "BSD-3-Clause" + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.0.tgz", @@ -3152,6 +3417,12 @@ "html2canvas": "^1.0.0-rc.5" } }, + "node_modules/jstoxml": { + "version": "2.2.9", + "resolved": "https://registry.npmmirror.com/jstoxml/-/jstoxml-2.2.9.tgz", + "integrity": "sha512-OYWlK0j+roh+eyaMROlNbS5cd5R25Y+IUpdl7cNdB8HNrkgwQzIS7L9MegxOiWNBj9dQhA/yAxiMwCC5mwNoBw==", + "license": "MIT" + }, "node_modules/jszip": { "version": "3.10.1", "resolved": "https://registry.npmmirror.com/jszip/-/jszip-3.10.1.tgz", @@ -3321,6 +3592,12 @@ "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==", "license": "ISC" }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" + }, "node_modules/lodash.defaults": { "version": "4.2.0", "resolved": "https://registry.npmmirror.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz", @@ -3478,6 +3755,27 @@ "node": ">= 0.4" } }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmmirror.com/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", @@ -3604,6 +3902,17 @@ "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" } }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmmirror.com/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, "node_modules/napi-build-utils": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/napi-build-utils/-/napi-build-utils-2.0.0.tgz", @@ -3657,6 +3966,15 @@ "node-gyp-build-optional-packages-test": "build-test.js" } }, + "node_modules/node-hex": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/node-hex/-/node-hex-1.0.1.tgz", + "integrity": "sha512-iwpZdvW6Umz12ICmu9IYPRxg0tOLGmU3Tq2tKetejCj3oZd7b2nUXwP3a7QA5M9glWy8wlPS1G3RwM/CdsUbdQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/nodemon": { "version": "3.1.10", "resolved": "https://registry.npmmirror.com/nodemon/-/nodemon-3.1.10.tgz", @@ -3752,6 +4070,27 @@ "node": "^14.16.0 || >=16.10.0" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/obliterator": { "version": "2.0.5", "resolved": "https://registry.npmmirror.com/obliterator/-/obliterator-2.0.5.tgz", @@ -3812,6 +4151,37 @@ } } }, + "node_modules/os-name": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/os-name/-/os-name-1.0.3.tgz", + "integrity": "sha512-f5estLO2KN8vgtTRaILIgEGBoBrMnZ3JQ7W9TMZCnOIGwHe8TRGSpcagnWDo+Dfhd/z08k9Xe75hvciJJ8Qaew==", + "license": "MIT", + "dependencies": { + "osx-release": "^1.0.0", + "win-release": "^1.0.0" + }, + "bin": { + "os-name": "cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/osx-release": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/osx-release/-/osx-release-1.1.0.tgz", + "integrity": "sha512-ixCMMwnVxyHFQLQnINhmIpWqXIfS2YOXchwQrk+OFzmo6nDjQ0E4KXAyyUh0T0MZgV4bUhkRrAbVqlE4yLVq4A==", + "license": "MIT", + "dependencies": { + "minimist": "^1.1.0" + }, + "bin": { + "osx-release": "cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/p-queue": { "version": "9.0.1", "resolved": "https://registry.npmmirror.com/p-queue/-/p-queue-9.0.1.tgz", @@ -3861,6 +4231,18 @@ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "license": "MIT" }, + "node_modules/pause-stream": { + "version": "0.0.11", + "resolved": "https://registry.npmmirror.com/pause-stream/-/pause-stream-0.0.11.tgz", + "integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==", + "license": [ + "MIT", + "Apache2" + ], + "dependencies": { + "through": "~2.3" + } + }, "node_modules/perfect-debounce": { "version": "1.0.0", "resolved": "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz", @@ -4075,6 +4457,12 @@ "pathe": "^2.0.3" } }, + "node_modules/platform": { + "version": "1.3.6", + "resolved": "https://registry.npmmirror.com/platform/-/platform-1.3.6.tgz", + "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", + "license": "MIT" + }, "node_modules/postgres-array": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/postgres-array/-/postgres-array-2.0.0.tgz", @@ -4205,7 +4593,6 @@ "version": "3.0.3", "resolved": "https://registry.npmmirror.com/pump/-/pump-3.0.3.tgz", "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", - "dev": true, "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", @@ -4228,6 +4615,21 @@ ], "license": "MIT" }, + "node_modules/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmmirror.com/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/quick-format-unescaped": { "version": "4.0.4", "resolved": "https://registry.npmmirror.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", @@ -4513,6 +4915,15 @@ "node": ">=10" } }, + "node_modules/sdk-base": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/sdk-base/-/sdk-base-2.0.1.tgz", + "integrity": "sha512-eeG26wRwhtwYuKGCDM3LixCaxY27Pa/5lK4rLKhQa7HBjJ3U3Y+f81MMZQRsDw/8SC2Dao/83yJTXJ8aULuN8Q==", + "license": "MIT", + "dependencies": { + "get-ready": "~1.0.0" + } + }, "node_modules/secure-json-parse": { "version": "4.1.0", "resolved": "https://registry.npmmirror.com/secure-json-parse/-/secure-json-parse-4.1.0.tgz", @@ -4568,6 +4979,78 @@ "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", "license": "MIT" }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/simple-concat/-/simple-concat-1.0.1.tgz", @@ -4698,6 +5181,15 @@ "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", "license": "MIT" }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/steed": { "version": "1.1.3", "resolved": "https://registry.npmmirror.com/steed/-/steed-1.1.3.tgz", @@ -4711,6 +5203,58 @@ "reusify": "^1.0.0" } }, + "node_modules/stream-http": { + "version": "2.8.2", + "resolved": "https://registry.npmmirror.com/stream-http/-/stream-http-2.8.2.tgz", + "integrity": "sha512-QllfrBhqF1DPcz46WxKTs6Mz1Bpc+8Qm6vbqOpVav5odAXwbyzwnEczoWqtxrsmlO+cJqtPrp/8gWKWjaKLLlA==", + "license": "MIT", + "dependencies": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.3.6", + "to-arraybuffer": "^1.0.0", + "xtend": "^4.0.0" + } + }, + "node_modules/stream-http/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/stream-http/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/stream-http/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/stream-wormhole": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/stream-wormhole/-/stream-wormhole-1.1.0.tgz", + "integrity": "sha512-gHFfL3px0Kctd6Po0M8TzEvt3De/xu6cnRrjlfYNhwbhLPLwigI2t1nc6jrzNuaYg5C4YF78PPFuQPzRiqn9ew==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz", @@ -4800,6 +5344,27 @@ "utrie": "^1.0.2" } }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/thread-stream": { "version": "3.1.0", "resolved": "https://registry.npmmirror.com/thread-stream/-/thread-stream-3.1.0.tgz", @@ -4809,6 +5374,12 @@ "real-require": "^0.2.0" } }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmmirror.com/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "license": "MIT" + }, "node_modules/tiktoken": { "version": "1.0.22", "resolved": "https://registry.npmmirror.com/tiktoken/-/tiktoken-1.0.22.tgz", @@ -4830,6 +5401,12 @@ "node": ">=14.14" } }, + "node_modules/to-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha512-okFlQcoGTi4LQBG/PgSYblw9VOyptsz2KJZqc6qtgGdes8VktzUQkj4BI2blit072iS8VODNcMA+tvnS9dnuMA==", + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -5015,6 +5592,18 @@ "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", "license": "MIT" }, + "node_modules/unescape": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/unescape/-/unescape-1.0.1.tgz", + "integrity": "sha512-O0+af1Gs50lyH1nUu3ZyYS1cRh01Q/kUKatTOkSs7jukXE6/NebucDVxyiDsA9AQ4JC1V1jUH9EO8JX2nMDgGQ==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/unzipper": { "version": "0.10.14", "resolved": "https://registry.npmmirror.com/unzipper/-/unzipper-0.10.14.tgz", @@ -5063,12 +5652,59 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/urllib": { + "version": "2.44.0", + "resolved": "https://registry.npmmirror.com/urllib/-/urllib-2.44.0.tgz", + "integrity": "sha512-zRCJqdfYllRDA9bXUtx+vccyRqtJPKsw85f44zH7zPD28PIvjMqIgw9VwoTLV7xTBWZsbebUFVHU5ghQcWku2A==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.3.0", + "content-type": "^1.0.2", + "default-user-agent": "^1.0.0", + "digest-header": "^1.0.0", + "ee-first": "~1.1.1", + "formstream": "^1.1.0", + "humanize-ms": "^1.2.0", + "iconv-lite": "^0.6.3", + "pump": "^3.0.0", + "qs": "^6.4.0", + "statuses": "^1.3.1", + "utility": "^1.16.1" + }, + "engines": { + "node": ">= 0.10.0" + }, + "peerDependencies": { + "proxy-agent": "^5.0.0" + }, + "peerDependenciesMeta": { + "proxy-agent": { + "optional": true + } + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/utility": { + "version": "1.18.0", + "resolved": "https://registry.npmmirror.com/utility/-/utility-1.18.0.tgz", + "integrity": "sha512-PYxZDA+6QtvRvm//++aGdmKG/cI07jNwbROz0Ql+VzFV1+Z0Dy55NI4zZ7RHc9KKpBePNFwoErqIuqQv/cjiTA==", + "license": "MIT", + "dependencies": { + "copy-to": "^2.0.1", + "escape-html": "^1.0.3", + "mkdirp": "^0.5.1", + "mz": "^2.7.0", + "unescape": "^1.0.1" + }, + "engines": { + "node": ">= 0.12.0" + } + }, "node_modules/utrie": { "version": "1.0.2", "resolved": "https://registry.npmmirror.com/utrie/-/utrie-1.0.2.tgz", @@ -5094,6 +5730,27 @@ "dev": true, "license": "MIT" }, + "node_modules/win-release": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/win-release/-/win-release-1.1.1.tgz", + "integrity": "sha512-iCRnKVvGxOQdsKhcQId2PXV1vV3J/sDPXKA4Oe9+Eti2nb2ESEsYHRYls/UjoUW3bIc5ZDO8dTH50A/5iVN+bw==", + "license": "MIT", + "dependencies": { + "semver": "^5.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/win-release/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmmirror.com/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, "node_modules/winston": { "version": "3.18.3", "resolved": "https://registry.npmmirror.com/winston/-/winston-3.18.3.tgz", diff --git a/backend/package.json b/backend/package.json index 92064b0b..63b15426 100644 --- a/backend/package.json +++ b/backend/package.json @@ -33,6 +33,7 @@ "@types/form-data": "^2.2.1", "@wecom/crypto": "^1.0.1", "ajv": "^8.17.1", + "ali-oss": "^6.23.0", "axios": "^1.12.2", "bcryptjs": "^2.4.3", "bullmq": "^5.65.0", @@ -58,6 +59,7 @@ "zod": "^4.1.12" }, "devDependencies": { + "@types/ali-oss": "^6.23.1", "@types/bcryptjs": "^2.4.6", "@types/js-yaml": "^4.0.9", "@types/jsonwebtoken": "^9.0.7", diff --git a/backend/prisma/manual-migrations/001_add_postgres_cache_and_checkpoint.sql b/backend/prisma/manual-migrations/001_add_postgres_cache_and_checkpoint.sql index 0adfb633..84059cf1 100644 --- a/backend/prisma/manual-migrations/001_add_postgres_cache_and_checkpoint.sql +++ b/backend/prisma/manual-migrations/001_add_postgres_cache_and_checkpoint.sql @@ -116,6 +116,9 @@ ORDER BY ordinal_position; + + + diff --git a/backend/prisma/manual-migrations/run-migration-002.ts b/backend/prisma/manual-migrations/run-migration-002.ts index 177cdb3a..157f421b 100644 --- a/backend/prisma/manual-migrations/run-migration-002.ts +++ b/backend/prisma/manual-migrations/run-migration-002.ts @@ -129,6 +129,9 @@ runMigration() + + + diff --git a/backend/prisma/migrations/20251208_add_column_mapping/migration.sql b/backend/prisma/migrations/20251208_add_column_mapping/migration.sql index 8cdb665d..29c3a130 100644 --- a/backend/prisma/migrations/20251208_add_column_mapping/migration.sql +++ b/backend/prisma/migrations/20251208_add_column_mapping/migration.sql @@ -63,6 +63,9 @@ COMMENT ON COLUMN "dc_schema"."dc_tool_c_sessions"."column_mapping" IS '列名 + + + diff --git a/backend/prisma/migrations/create_tool_c_session.sql b/backend/prisma/migrations/create_tool_c_session.sql index 774f29d6..06696b3c 100644 --- a/backend/prisma/migrations/create_tool_c_session.sql +++ b/backend/prisma/migrations/create_tool_c_session.sql @@ -90,6 +90,9 @@ COMMENT ON COLUMN dc_schema.dc_tool_c_sessions.expires_at IS '过期时间(创 + + + diff --git a/backend/prisma/migrations/manual/ekb_create_indexes.sql b/backend/prisma/migrations/manual/ekb_create_indexes.sql index 4bd1f670..716b850a 100644 --- a/backend/prisma/migrations/manual/ekb_create_indexes.sql +++ b/backend/prisma/migrations/manual/ekb_create_indexes.sql @@ -62,3 +62,6 @@ USING gin (metadata jsonb_path_ops); -- SELECT indexname, indexdef FROM pg_indexes WHERE schemaname = 'ekb_schema'; + + + diff --git a/backend/prisma/migrations/manual/ekb_create_indexes_mvp.sql b/backend/prisma/migrations/manual/ekb_create_indexes_mvp.sql index 434c7035..aa3fa357 100644 --- a/backend/prisma/migrations/manual/ekb_create_indexes_mvp.sql +++ b/backend/prisma/migrations/manual/ekb_create_indexes_mvp.sql @@ -29,3 +29,6 @@ ON "ekb_schema"."ekb_document" USING gin (tags); + + + diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 941a8968..9ffc6178 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -152,7 +152,7 @@ model Document { fileType String @map("file_type") fileSizeBytes BigInt @map("file_size_bytes") fileUrl String @map("file_url") - difyDocumentId String @map("dify_document_id") + storageKey String @map("dify_document_id") // 原 difyDocumentId,现存储 OSS 路径 status String @default("uploading") progress Int @default(0) errorMessage String? @map("error_message") @@ -168,7 +168,7 @@ model Document { batchResults BatchResult[] @relation("DocumentBatchResults") knowledgeBase KnowledgeBase @relation("KnowledgeBaseDocuments", fields: [kbId], references: [id], onDelete: Cascade) - @@index([difyDocumentId], map: "idx_pkb_documents_dify_document_id") + @@index([storageKey], map: "idx_pkb_documents_dify_document_id") // 保留原索引名 @@index([extractionMethod], map: "idx_pkb_documents_extraction_method") @@index([kbId], map: "idx_pkb_documents_kb_id") @@index([status], map: "idx_pkb_documents_status") diff --git a/backend/rebuild-and-push.ps1 b/backend/rebuild-and-push.ps1 index 866947ee..046dfa65 100644 --- a/backend/rebuild-and-push.ps1 +++ b/backend/rebuild-and-push.ps1 @@ -130,6 +130,9 @@ Write-Host "" + + + diff --git a/backend/recover-code-from-cursor-db.js b/backend/recover-code-from-cursor-db.js index 3156a4ff..0bcbba22 100644 --- a/backend/recover-code-from-cursor-db.js +++ b/backend/recover-code-from-cursor-db.js @@ -240,6 +240,9 @@ function extractCodeBlocks(obj, blocks = []) { + + + diff --git a/backend/restore_job_common.sql b/backend/restore_job_common.sql index 2e7db2ad..090b8608 100644 --- a/backend/restore_job_common.sql +++ b/backend/restore_job_common.sql @@ -41,3 +41,6 @@ CREATE TABLE IF NOT EXISTS platform_schema.job_common ( + + + diff --git a/backend/restore_pgboss_functions.sql b/backend/restore_pgboss_functions.sql index 5f7a4ab8..cd6b03b1 100644 --- a/backend/restore_pgboss_functions.sql +++ b/backend/restore_pgboss_functions.sql @@ -115,3 +115,6 @@ CREATE OR REPLACE FUNCTION platform_schema.delete_queue(queue_name text) RETURNS + + + diff --git a/backend/scripts/check-dc-tables.mjs b/backend/scripts/check-dc-tables.mjs index 3c9a5ad4..e1ab44e2 100644 --- a/backend/scripts/check-dc-tables.mjs +++ b/backend/scripts/check-dc-tables.mjs @@ -259,6 +259,9 @@ checkDCTables(); + + + diff --git a/backend/scripts/create-capability-schema.sql b/backend/scripts/create-capability-schema.sql index eb8cdd79..115b527e 100644 --- a/backend/scripts/create-capability-schema.sql +++ b/backend/scripts/create-capability-schema.sql @@ -16,3 +16,6 @@ CREATE SCHEMA IF NOT EXISTS capability_schema; + + + diff --git a/backend/scripts/create-tool-c-ai-history-table.mjs b/backend/scripts/create-tool-c-ai-history-table.mjs index 6c6a481e..6786889b 100644 --- a/backend/scripts/create-tool-c-ai-history-table.mjs +++ b/backend/scripts/create-tool-c-ai-history-table.mjs @@ -211,6 +211,9 @@ createAiHistoryTable() + + + diff --git a/backend/scripts/create-tool-c-table.js b/backend/scripts/create-tool-c-table.js index cea10343..1ab957d8 100644 --- a/backend/scripts/create-tool-c-table.js +++ b/backend/scripts/create-tool-c-table.js @@ -198,6 +198,9 @@ createToolCTable() + + + diff --git a/backend/scripts/create-tool-c-table.mjs b/backend/scripts/create-tool-c-table.mjs index 286afcaf..b5052618 100644 --- a/backend/scripts/create-tool-c-table.mjs +++ b/backend/scripts/create-tool-c-table.mjs @@ -195,6 +195,9 @@ createToolCTable() + + + diff --git a/backend/scripts/migrate-aia-prompts.ts b/backend/scripts/migrate-aia-prompts.ts index a88c529f..53ef20f7 100644 --- a/backend/scripts/migrate-aia-prompts.ts +++ b/backend/scripts/migrate-aia-prompts.ts @@ -319,3 +319,6 @@ main() + + + diff --git a/backend/scripts/setup-prompt-system.ts b/backend/scripts/setup-prompt-system.ts index bc186db6..37646844 100644 --- a/backend/scripts/setup-prompt-system.ts +++ b/backend/scripts/setup-prompt-system.ts @@ -126,3 +126,6 @@ main() + + + diff --git a/backend/scripts/test-oss.ts b/backend/scripts/test-oss.ts new file mode 100644 index 00000000..0376511b --- /dev/null +++ b/backend/scripts/test-oss.ts @@ -0,0 +1,163 @@ +/** + * OSS存储适配器测试脚本 + * + * 使用方法: + * 1. 配置环境变量(.env 文件) + * 2. 运行:npx tsx scripts/test-oss.ts + * + * 测试项: + * - 上传文件(按 MVP 目录结构) + * - 下载文件 + * - 检查文件存在 + * - 获取签名URL + * - 【可选】删除文件 + */ + +import dotenv from 'dotenv' +import path from 'path' +import fs from 'fs' +import { fileURLToPath } from 'url' +import { randomUUID } from 'crypto' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +// 加载环境变量 +dotenv.config({ path: path.join(__dirname, '../.env') }) + +import { StorageFactory } from '../src/common/storage/StorageFactory.js' + +/** + * 生成存储 Key(按 MVP 目录结构) + * + * 格式:tenants/{tenantId}/users/{userId}/{module}/{uuid}.{ext} + */ +function generateStorageKey( + tenantId: string, + userId: string | null, + module: string, + filename: string +): string { + const uuid = randomUUID().replace(/-/g, '').substring(0, 16) + const ext = path.extname(filename) + + if (userId) { + // 用户私有数据 + return `tenants/${tenantId}/users/${userId}/${module}/${uuid}${ext}` + } else { + // 租户共享数据 + return `tenants/${tenantId}/shared/${module}/${uuid}${ext}` + } +} + +async function main() { + console.log('='.repeat(60)) + console.log('OSS 存储适配器测试 - MVP 目录结构验证') + console.log('='.repeat(60)) + + // 检查环境变量 + const storageType = process.env.STORAGE_TYPE || 'local' + console.log(`\n📦 存储类型: ${storageType}`) + + if (storageType === 'oss') { + console.log(` Region: ${process.env.OSS_REGION}`) + console.log(` Bucket: ${process.env.OSS_BUCKET}`) + console.log(` Internal: ${process.env.OSS_INTERNAL}`) + } + + // 获取存储实例 + const storage = StorageFactory.getInstance() + console.log(`\n✅ 存储实例创建成功`) + + // ============================================================ + // 测试文件:真实 PDF 文献 + // ============================================================ + const testPdfPath = path.join(__dirname, '../../docs/06-测试文档/Ihl 2011.pdf') + + if (!fs.existsSync(testPdfPath)) { + console.error(`\n❌ 测试文件不存在: ${testPdfPath}`) + process.exit(1) + } + + const pdfBuffer = fs.readFileSync(testPdfPath) + console.log(`\n📄 测试文件: Ihl 2011.pdf`) + console.log(` 大小: ${(pdfBuffer.length / 1024).toFixed(2)} KB`) + + // ============================================================ + // 按 MVP 目录结构生成 Key + // ============================================================ + // 模拟:租户 yizhengxun,用户 test-user-001,模块 pkb + const tenantId = 'yizhengxun' + const userId = 'test-user-001' + const module = 'pkb' + + const storageKey = generateStorageKey(tenantId, userId, module, 'Ihl 2011.pdf') + + console.log(`\n📁 目录结构验证:`) + console.log(` 租户ID: ${tenantId}`) + console.log(` 用户ID: ${userId}`) + console.log(` 模块: ${module}`) + console.log(` 生成Key: ${storageKey}`) + + try { + // 1. 上传测试 + console.log(`\n📤 上传文件到 OSS...`) + const uploadUrl = await storage.upload(storageKey, pdfBuffer) + console.log(` ✅ 上传成功!`) + console.log(` 签名URL: ${uploadUrl.substring(0, 80)}...`) + + // 2. 检查存在 + console.log(`\n🔍 验证文件存在...`) + const exists = await storage.exists(storageKey) + console.log(` 存在: ${exists ? '✅ 是' : '❌ 否'}`) + + // 3. 下载验证 + console.log(`\n📥 下载验证...`) + const downloadBuffer = await storage.download(storageKey) + const sizeMatch = downloadBuffer.length === pdfBuffer.length + console.log(` 下载大小: ${(downloadBuffer.length / 1024).toFixed(2)} KB`) + console.log(` 大小匹配: ${sizeMatch ? '✅ 是' : '❌ 否'}`) + + // 4. 获取URL(不带原始文件名) + console.log(`\n🔗 获取访问URL...`) + const url = storage.getUrl(storageKey) + console.log(` URL: ${url.substring(0, 80)}...`) + + // 5. 获取URL(带原始文件名 - 下载时恢复文件名) + console.log(`\n📎 获取带原始文件名的URL...`) + // 类型断言访问 OSSAdapter 的 getSignedUrl 方法 + const ossAdapter = storage as any + if (typeof ossAdapter.getSignedUrl === 'function') { + const urlWithFilename = ossAdapter.getSignedUrl(storageKey, 3600, 'Ihl 2011.pdf') + console.log(` 原始文件名: Ihl 2011.pdf`) + console.log(` URL: ${urlWithFilename.substring(0, 80)}...`) + console.log(` ✅ 下载此URL时,浏览器会保存为 "Ihl 2011.pdf"`) + } + + // ============================================================ + // 🔴 不删除文件!保留在 OSS 中供验证 + // ============================================================ + console.log(`\n⚠️ 文件已保留在 OSS 中,不删除!`) + console.log(` 请登录阿里云 OSS 控制台查看:`) + console.log(` https://oss.console.aliyun.com/bucket/oss-cn-beijing/ai-clinical-data-dev/object`) + console.log(`\n 文件路径: ${storageKey}`) + + // 测试完成 + console.log('\n' + '='.repeat(60)) + console.log('🎉 测试完成!请到 OSS 控制台验证目录结构') + console.log('='.repeat(60)) + + // 输出完整信息供验证 + console.log(`\n📋 验证信息:`) + console.log(` Bucket: ai-clinical-data-dev`) + console.log(` Key: ${storageKey}`) + console.log(` 预期目录: tenants/yizhengxun/users/test-user-001/pkb/`) + + } catch (error) { + console.error('\n❌ 测试失败:', error) + process.exit(1) + } +} + +// 运行测试 +main().catch(console.error) diff --git a/backend/scripts/test-pkb-apis-simple.ts b/backend/scripts/test-pkb-apis-simple.ts index de0d02a3..be902210 100644 --- a/backend/scripts/test-pkb-apis-simple.ts +++ b/backend/scripts/test-pkb-apis-simple.ts @@ -342,6 +342,9 @@ runTests().catch(error => { + + + diff --git a/backend/scripts/test-prompt-api.ts b/backend/scripts/test-prompt-api.ts index 44f089b7..8eb2b88f 100644 --- a/backend/scripts/test-prompt-api.ts +++ b/backend/scripts/test-prompt-api.ts @@ -92,3 +92,6 @@ testAPI().catch(console.error); + + + diff --git a/backend/scripts/test-unifuncs-deepsearch.ts b/backend/scripts/test-unifuncs-deepsearch.ts index 1943d655..95e3f7b1 100644 --- a/backend/scripts/test-unifuncs-deepsearch.ts +++ b/backend/scripts/test-unifuncs-deepsearch.ts @@ -122,3 +122,6 @@ testDeepSearch().catch(console.error); + + + diff --git a/backend/scripts/verify-pkb-rvw-schema.ts b/backend/scripts/verify-pkb-rvw-schema.ts index 5d8e32b5..38c39832 100644 --- a/backend/scripts/verify-pkb-rvw-schema.ts +++ b/backend/scripts/verify-pkb-rvw-schema.ts @@ -307,6 +307,9 @@ verifySchemas() + + + diff --git a/backend/src/common/auth/jwt.service.ts b/backend/src/common/auth/jwt.service.ts index 04f771cf..ebac7aeb 100644 --- a/backend/src/common/auth/jwt.service.ts +++ b/backend/src/common/auth/jwt.service.ts @@ -199,3 +199,6 @@ export const jwtService = new JWTService(); + + + diff --git a/backend/src/common/jobs/utils.ts b/backend/src/common/jobs/utils.ts index 2b45e2be..bd69232f 100644 --- a/backend/src/common/jobs/utils.ts +++ b/backend/src/common/jobs/utils.ts @@ -327,6 +327,9 @@ export function getBatchItems( + + + diff --git a/backend/src/common/prompt/prompt.types.ts b/backend/src/common/prompt/prompt.types.ts index 00fa0161..07ddb7b8 100644 --- a/backend/src/common/prompt/prompt.types.ts +++ b/backend/src/common/prompt/prompt.types.ts @@ -82,3 +82,6 @@ export interface VariableValidation { + + + diff --git a/backend/src/common/rag/ChunkService.ts b/backend/src/common/rag/ChunkService.ts index 0bb47322..adb681b2 100644 --- a/backend/src/common/rag/ChunkService.ts +++ b/backend/src/common/rag/ChunkService.ts @@ -352,3 +352,6 @@ export function chunkMarkdown(markdown: string, config?: ChunkConfig): TextChunk export default ChunkService; + + + diff --git a/backend/src/common/rag/DifyClient.ts b/backend/src/common/rag/DifyClient.ts index 2c3e7e8e..05e804d0 100644 --- a/backend/src/common/rag/DifyClient.ts +++ b/backend/src/common/rag/DifyClient.ts @@ -48,3 +48,6 @@ class DeprecatedDifyClient { export const difyClient = new DeprecatedDifyClient(); export const DifyClient = DeprecatedDifyClient; + + + diff --git a/backend/src/common/storage/OSSAdapter.ts b/backend/src/common/storage/OSSAdapter.ts index 67a3f8c7..856e541a 100644 --- a/backend/src/common/storage/OSSAdapter.ts +++ b/backend/src/common/storage/OSSAdapter.ts @@ -1,135 +1,372 @@ import { StorageAdapter } from './StorageAdapter.js' -// import OSS from 'ali-oss' // ⚠️ 需要安装:npm install ali-oss +import OSS from 'ali-oss' /** - * 阿里云OSS适配器 + * 阿里云OSS存储适配器 * - * 适用场景: - * - 云端SaaS部署(阿里云Serverless) - * - 高可用、高并发场景 - * - 需要CDN加速 + * 支持功能: + * - 文件上传/下载/删除 + * - 私有Bucket签名URL + * - 内网Endpoint(SAE部署零流量费) + * - 静态Bucket公开访问 * - * 配置要求: - * - OSS_REGION: OSS地域(如:oss-cn-hangzhou) - * - OSS_BUCKET: OSS Bucket名称 - * - OSS_ACCESS_KEY_ID: AccessKey ID - * - OSS_ACCESS_KEY_SECRET: AccessKey Secret + * Bucket策略: + * - ai-clinical-data: 私有,核心数据(文献、病历、报告) + * - ai-clinical-static: 公共读,静态资源(头像、Logo、图片) * * @example * ```typescript * const adapter = new OSSAdapter({ - * region: 'oss-cn-hangzhou', - * bucket: 'aiclinical-prod', + * region: 'oss-cn-beijing', + * bucket: 'ai-clinical-data', * accessKeyId: process.env.OSS_ACCESS_KEY_ID!, - * accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET! + * accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET!, + * internal: true // SAE内网 * }) - * await adapter.upload('literature/123.pdf', buffer) - * ``` * - * ⚠️ 当前为预留实现,待云端部署时完善 + * // 上传文件 + * const url = await adapter.upload('pkb/tenant123/user456/doc.pdf', buffer) + * + * // 获取签名URL(私有Bucket) + * const signedUrl = adapter.getSignedUrl('pkb/tenant123/user456/doc.pdf') + * ``` */ + +export interface OSSAdapterConfig { + /** OSS地域,如 oss-cn-beijing */ + region: string + /** Bucket名称 */ + bucket: string + /** AccessKey ID */ + accessKeyId: string + /** AccessKey Secret */ + accessKeySecret: string + /** 是否使用内网Endpoint(SAE部署必须为true) */ + internal?: boolean + /** 签名URL过期时间(秒),默认3600 */ + signedUrlExpires?: number +} + export class OSSAdapter implements StorageAdapter { - // private readonly client: OSS + private readonly client: OSS private readonly bucket: string private readonly region: string + private readonly internal: boolean + private readonly signedUrlExpires: number - constructor(config: { - region: string - bucket: string - accessKeyId: string - accessKeySecret: string - }) { + constructor(config: OSSAdapterConfig) { this.region = config.region this.bucket = config.bucket + this.internal = config.internal ?? false + this.signedUrlExpires = config.signedUrlExpires ?? 3600 - // ⚠️ TODO: 待安装 ali-oss 后取消注释 - // this.client = new OSS({ - // region: config.region, - // bucket: config.bucket, - // accessKeyId: config.accessKeyId, - // accessKeySecret: config.accessKeySecret - // }) + // 构建Endpoint + // 内网:oss-cn-beijing-internal.aliyuncs.com(SAE零流量费) + // 公网:oss-cn-beijing.aliyuncs.com(本地开发) + const endpoint = this.internal + ? `${config.region}-internal.aliyuncs.com` + : `${config.region}.aliyuncs.com` + + this.client = new OSS({ + region: config.region, + bucket: config.bucket, + accessKeyId: config.accessKeyId, + accessKeySecret: config.accessKeySecret, + endpoint, + // 禁用自动URL编码,避免中文文件名问题 + secure: true, + }) + + console.log(`[OSSAdapter] Initialized: bucket=${config.bucket}, region=${config.region}, internal=${this.internal}`) } /** * 上传文件到OSS + * + * @param key 存储路径,如 pkb/tenant123/user456/doc.pdf + * @param buffer 文件内容 + * @returns 文件访问URL(私有Bucket返回签名URL) */ - async upload(_key: string, _buffer: Buffer): Promise { - // ⚠️ TODO: 待实现 - // const result = await this.client.put(key, buffer) - // return result.url + async upload(key: string, buffer: Buffer): Promise { + try { + const normalizedKey = this.normalizeKey(key) + + // 使用 put 方法上传 Buffer(适合 < 30MB 文件) + const result = await this.client.put(normalizedKey, buffer) + + console.log(`[OSSAdapter] Upload success: ${normalizedKey}, size=${buffer.length}`) + + // 返回签名URL(假设是私有Bucket) + return this.getSignedUrl(normalizedKey) + } catch (error) { + console.error(`[OSSAdapter] Upload failed: ${key}`, error) + throw new Error(`OSS上传失败: ${key}`) + } + } - throw new Error('[OSSAdapter] Not implemented yet. Please install ali-oss and configure OSS.') + /** + * 流式上传(适合大文件 > 30MB) + * + * @param key 存储路径 + * @param stream 可读流 + * @returns 文件访问URL + */ + async uploadStream(key: string, stream: NodeJS.ReadableStream): Promise { + try { + const normalizedKey = this.normalizeKey(key) + + // 使用 putStream 方法上传流 + const result = await this.client.putStream(normalizedKey, stream) + + console.log(`[OSSAdapter] Stream upload success: ${normalizedKey}`) + + return this.getSignedUrl(normalizedKey) + } catch (error) { + console.error(`[OSSAdapter] Stream upload failed: ${key}`, error) + throw new Error(`OSS流式上传失败: ${key}`) + } } /** * 从OSS下载文件 + * + * @param key 存储路径 + * @returns 文件内容 Buffer */ - async download(_key: string): Promise { - // ⚠️ TODO: 待实现 - // const result = await this.client.get(key) - // return result.content as Buffer - - throw new Error('[OSSAdapter] Not implemented yet. Please install ali-oss and configure OSS.') + async download(key: string): Promise { + try { + const normalizedKey = this.normalizeKey(key) + + const result = await this.client.get(normalizedKey) + + // result.content 可能是 Buffer 或 string + if (Buffer.isBuffer(result.content)) { + return result.content + } + + // 如果是字符串,转换为 Buffer + return Buffer.from(result.content as string) + } catch (error: any) { + // 处理文件不存在的情况 + if (error.code === 'NoSuchKey') { + throw new Error(`文件不存在: ${key}`) + } + console.error(`[OSSAdapter] Download failed: ${key}`, error) + throw new Error(`OSS下载失败: ${key}`) + } } /** * 从OSS删除文件 + * + * @param key 存储路径 */ - async delete(_key: string): Promise { - // ⚠️ TODO: 待实现 - // await this.client.delete(key) - - throw new Error('[OSSAdapter] Not implemented yet. Please install ali-oss and configure OSS.') + async delete(key: string): Promise { + try { + const normalizedKey = this.normalizeKey(key) + + await this.client.delete(normalizedKey) + + console.log(`[OSSAdapter] Delete success: ${normalizedKey}`) + } catch (error: any) { + // 删除不存在的文件不报错(幂等) + if (error.code === 'NoSuchKey') { + console.log(`[OSSAdapter] Delete skipped (not exists): ${key}`) + return + } + console.error(`[OSSAdapter] Delete failed: ${key}`, error) + throw new Error(`OSS删除失败: ${key}`) + } } /** * 获取文件访问URL + * + * 私有Bucket:返回签名URL + * 公共Bucket:返回直接URL + * + * @param key 存储路径 + * @returns 文件访问URL */ getUrl(key: string): string { - // 返回OSS公开访问URL - // 格式:https://{bucket}.{region}.aliyuncs.com/{key} - return `https://${this.bucket}.${this.region}.aliyuncs.com/${key}` + // 默认返回签名URL(适合私有Bucket) + return this.getSignedUrl(key) + } + + /** + * 获取签名URL(私有Bucket访问) + * + * @param key 存储路径 + * @param expires 过期时间(秒),默认使用配置值 + * @param originalFilename 原始文件名(可选,用于下载时恢复文件名) + * @returns 签名URL + */ + getSignedUrl(key: string, expires?: number, originalFilename?: string): string { + const normalizedKey = this.normalizeKey(key) + const expireTime = expires ?? this.signedUrlExpires + + // 生成签名URL选项 + const options: any = { + expires: expireTime, + } + + // 如果提供了原始文件名,添加 Content-Disposition 头 + // 下载时浏览器会使用这个文件名 + if (originalFilename) { + // RFC 5987 编码,支持中文文件名 + const encodedFilename = encodeURIComponent(originalFilename) + options.response = { + 'content-disposition': `attachment; filename*=UTF-8''${encodedFilename}`, + } + } + + // 生成签名URL + const url = this.client.signatureUrl(normalizedKey, options) + + return url + } + + /** + * 获取公开访问URL(适合静态资源Bucket) + * + * 格式:https://{bucket}.{region}.aliyuncs.com/{key} + * + * @param key 存储路径 + * @returns 公开URL + */ + getPublicUrl(key: string): string { + const normalizedKey = this.normalizeKey(key) + return `https://${this.bucket}.${this.region}.aliyuncs.com/${normalizedKey}` } /** * 检查文件是否存在 + * + * @param key 存储路径 + * @returns 是否存在 */ - async exists(_key: string): Promise { - // ⚠️ TODO: 待实现 - // try { - // await this.client.head(key) - // return true - // } catch (error) { - // return false - // } + async exists(key: string): Promise { + try { + const normalizedKey = this.normalizeKey(key) + + await this.client.head(normalizedKey) + return true + } catch (error: any) { + if (error.code === 'NoSuchKey') { + return false + } + // 其他错误抛出 + throw error + } + } - throw new Error('[OSSAdapter] Not implemented yet. Please install ali-oss and configure OSS.') + /** + * 获取文件元信息 + * + * @param key 存储路径 + * @returns 文件元信息(大小、类型、最后修改时间等) + */ + async getMetadata(key: string): Promise<{ + size: number + contentType: string + lastModified: Date + } | null> { + try { + const normalizedKey = this.normalizeKey(key) + + const result = await this.client.head(normalizedKey) + + return { + size: parseInt(result.res.headers['content-length'] as string, 10), + contentType: result.res.headers['content-type'] as string, + lastModified: new Date(result.res.headers['last-modified'] as string), + } + } catch (error: any) { + if (error.code === 'NoSuchKey') { + return null + } + throw error + } + } + + /** + * 批量删除文件 + * + * @param keys 存储路径数组 + */ + async deleteMany(keys: string[]): Promise { + if (keys.length === 0) return + + try { + const normalizedKeys = keys.map(k => this.normalizeKey(k)) + + // OSS支持批量删除,最多1000个 + await this.client.deleteMulti(normalizedKeys, { + quiet: true, // 静默模式,不返回删除结果 + }) + + console.log(`[OSSAdapter] Batch delete success: ${keys.length} files`) + } catch (error) { + console.error(`[OSSAdapter] Batch delete failed`, error) + throw new Error(`OSS批量删除失败`) + } + } + + /** + * 列出目录下的文件 + * + * @param prefix 目录前缀,如 pkb/tenant123/ + * @param maxKeys 最大返回数量 + * @returns 文件列表 + */ + async list(prefix: string, maxKeys: number = 1000): Promise<{ + key: string + size: number + lastModified: Date + }[]> { + try { + const normalizedPrefix = this.normalizeKey(prefix) + + const result = await this.client.list({ + prefix: normalizedPrefix, + 'max-keys': maxKeys, + }, {}) + + return (result.objects || []).map(obj => ({ + key: obj.name, + size: obj.size, + lastModified: new Date(obj.lastModified), + })) + } catch (error) { + console.error(`[OSSAdapter] List failed: ${prefix}`, error) + throw new Error(`OSS列表失败: ${prefix}`) + } + } + + /** + * 复制文件 + * + * @param sourceKey 源路径 + * @param targetKey 目标路径 + */ + async copy(sourceKey: string, targetKey: string): Promise { + try { + const normalizedSource = this.normalizeKey(sourceKey) + const normalizedTarget = this.normalizeKey(targetKey) + + await this.client.copy(normalizedTarget, normalizedSource) + + console.log(`[OSSAdapter] Copy success: ${sourceKey} -> ${targetKey}`) + } catch (error) { + console.error(`[OSSAdapter] Copy failed: ${sourceKey} -> ${targetKey}`, error) + throw new Error(`OSS复制失败`) + } + } + + /** + * 规范化Key(移除开头的斜杠) + */ + private normalizeKey(key: string): string { + return key.replace(/^\/+/, '') } } - -/** - * ⚠️ 实施说明: - * - * 1. 安装依赖: - * npm install ali-oss - * npm install -D @types/ali-oss - * - * 2. 取消注释代码: - * - import OSS from 'ali-oss' - * - new OSS({ ... }) - * - 所有方法的实现代码 - * - * 3. 配置环境变量: - * OSS_REGION=oss-cn-hangzhou - * OSS_BUCKET=aiclinical-prod - * OSS_ACCESS_KEY_ID=your-access-key-id - * OSS_ACCESS_KEY_SECRET=your-access-key-secret - * - * 4. 测试: - * - 上传小文件 - * - 下载文件 - * - 删除文件 - * - 检查文件是否存在 - */ - diff --git a/backend/src/common/storage/StorageFactory.ts b/backend/src/common/storage/StorageFactory.ts index b41cdbb7..724add7c 100644 --- a/backend/src/common/storage/StorageFactory.ts +++ b/backend/src/common/storage/StorageFactory.ts @@ -1,6 +1,6 @@ import { StorageAdapter } from './StorageAdapter.js' import { LocalAdapter } from './LocalAdapter.js' -import { OSSAdapter } from './OSSAdapter.js' +import { OSSAdapter, OSSAdapterConfig } from './OSSAdapter.js' /** * 存储工厂类 @@ -9,23 +9,39 @@ import { OSSAdapter } from './OSSAdapter.js' * - STORAGE_TYPE=local: 使用LocalAdapter(本地文件系统) * - STORAGE_TYPE=oss: 使用OSSAdapter(阿里云OSS) * + * 支持双Bucket策略: + * - Data Bucket(私有): 核心数据,文献、病历、报告 + * - Static Bucket(公共读): 静态资源,头像、Logo、图片 + * * 零代码切换: * - 本地开发:不配置STORAGE_TYPE,默认使用local * - 云端部署:配置STORAGE_TYPE=oss,自动切换到OSS * * @example * ```typescript - * import { storage } from '@/common/storage' + * import { storage, staticStorage } from '@/common/storage' * - * // 业务代码不关心是local还是oss - * const url = await storage.upload('literature/123.pdf', buffer) + * // 上传私有文件(自动使用签名URL) + * const url = await storage.upload('pkb/tenant/user/doc.pdf', buffer) + * + * // 上传静态资源(公开访问) + * const avatarUrl = await staticStorage.upload('avatars/user123.jpg', buffer) * ``` */ export class StorageFactory { + /** 主存储实例(私有数据) */ private static instance: StorageAdapter | null = null + + /** 静态存储实例(公共资源) */ + private static staticInstance: StorageAdapter | null = null /** - * 获取存储适配器实例(单例模式) + * 获取主存储适配器实例(单例模式) + * + * 用于存储私有数据: + * - PKB文献文件 + * - 审稿PDF报告 + * - 临床数据文件 */ static getInstance(): StorageAdapter { if (!this.instance) { @@ -35,7 +51,22 @@ export class StorageFactory { } /** - * 创建存储适配器 + * 获取静态资源存储适配器实例(单例模式) + * + * 用于存储公共资源: + * - 用户头像 + * - 系统Logo + * - RAG引用的图片 + */ + static getStaticInstance(): StorageAdapter { + if (!this.staticInstance) { + this.staticInstance = this.createStaticAdapter() + } + return this.staticInstance + } + + /** + * 创建主存储适配器(私有数据) */ private static createAdapter(): StorageAdapter { const storageType = process.env.STORAGE_TYPE || 'local' @@ -54,41 +85,99 @@ export class StorageFactory { } /** - * 创建本地适配器 + * 创建静态资源存储适配器 */ - private static createLocalAdapter(): LocalAdapter { - const baseDir = process.env.LOCAL_STORAGE_DIR || 'uploads' - const baseUrl = process.env.LOCAL_STORAGE_URL || 'http://localhost:3001/uploads' - - console.log(`[StorageFactory] Using LocalAdapter (baseDir: ${baseDir})`) - - return new LocalAdapter(baseDir, baseUrl) + private static createStaticAdapter(): StorageAdapter { + const storageType = process.env.STORAGE_TYPE || 'local' + + switch (storageType) { + case 'local': + // 本地模式下,静态资源也用同一个目录 + return this.createLocalAdapter('uploads/static', 'http://localhost:3001/uploads/static') + + case 'oss': + return this.createOSSStaticAdapter() + + default: + return this.createLocalAdapter('uploads/static', 'http://localhost:3001/uploads/static') + } } /** - * 创建OSS适配器 + * 创建本地适配器 + */ + private static createLocalAdapter( + baseDir?: string, + baseUrl?: string + ): LocalAdapter { + const dir = baseDir || process.env.LOCAL_STORAGE_DIR || 'uploads' + const url = baseUrl || process.env.LOCAL_STORAGE_URL || 'http://localhost:3001/uploads' + + console.log(`[StorageFactory] Using LocalAdapter (baseDir: ${dir})`) + + return new LocalAdapter(dir, url) + } + + /** + * 创建OSS适配器(私有数据Bucket) */ private static createOSSAdapter(): OSSAdapter { + const config = this.getOSSConfig() + + console.log(`[StorageFactory] Using OSSAdapter (region: ${config.region}, bucket: ${config.bucket}, internal: ${config.internal})`) + + return new OSSAdapter(config) + } + + /** + * 创建OSS静态资源适配器(公共Bucket) + */ + private static createOSSStaticAdapter(): OSSAdapter { + const baseConfig = this.getOSSConfig() + + // 静态资源使用独立的Bucket + const staticBucket = process.env.OSS_BUCKET_STATIC || `${baseConfig.bucket}-static` + + console.log(`[StorageFactory] Using OSSAdapter for static (bucket: ${staticBucket})`) + + return new OSSAdapter({ + ...baseConfig, + bucket: staticBucket, + }) + } + + /** + * 获取OSS配置(带验证) + */ + private static getOSSConfig(): OSSAdapterConfig { const region = process.env.OSS_REGION const bucket = process.env.OSS_BUCKET const accessKeyId = process.env.OSS_ACCESS_KEY_ID const accessKeySecret = process.env.OSS_ACCESS_KEY_SECRET + const internal = process.env.OSS_INTERNAL === 'true' + const signedUrlExpires = parseInt(process.env.OSS_SIGNED_URL_EXPIRES || '3600', 10) // 验证必需的环境变量 if (!region || !bucket || !accessKeyId || !accessKeySecret) { + const missing = [] + if (!region) missing.push('OSS_REGION') + if (!bucket) missing.push('OSS_BUCKET') + if (!accessKeyId) missing.push('OSS_ACCESS_KEY_ID') + if (!accessKeySecret) missing.push('OSS_ACCESS_KEY_SECRET') + throw new Error( - '[StorageFactory] OSS configuration incomplete. Required: OSS_REGION, OSS_BUCKET, OSS_ACCESS_KEY_ID, OSS_ACCESS_KEY_SECRET' + `[StorageFactory] OSS configuration incomplete. Missing: ${missing.join(', ')}` ) } - console.log(`[StorageFactory] Using OSSAdapter (region: ${region}, bucket: ${bucket})`) - - return new OSSAdapter({ + return { region, bucket, accessKeyId, - accessKeySecret - }) + accessKeySecret, + internal, + signedUrlExpires, + } } /** @@ -96,6 +185,6 @@ export class StorageFactory { */ static reset(): void { this.instance = null + this.staticInstance = null } } - diff --git a/backend/src/common/storage/index.ts b/backend/src/common/storage/index.ts index 3bbf9bec..41aa1bc7 100644 --- a/backend/src/common/storage/index.ts +++ b/backend/src/common/storage/index.ts @@ -3,40 +3,63 @@ * * 提供平台级的文件存储能力,支持本地和云端无缝切换。 * + * 双Bucket策略: + * - storage: 私有数据(文献、病历、报告),使用签名URL + * - staticStorage: 静态资源(头像、Logo、图片),公开访问 + * * @module storage * * @example * ```typescript * // 方式1:使用单例(推荐) - * import { storage } from '@/common/storage' - * const url = await storage.upload('literature/123.pdf', buffer) + * import { storage, staticStorage } from '@/common/storage' * - * // 方式2:直接使用适配器 - * import { LocalAdapter } from '@/common/storage' - * const adapter = new LocalAdapter() - * const url = await adapter.upload('literature/123.pdf', buffer) + * // 上传私有文件 + * const url = await storage.upload('pkb/tenant/user/doc.pdf', buffer) * - * // 方式3:使用工厂 + * // 上传静态资源 + * const avatarUrl = await staticStorage.upload('avatars/user123.jpg', buffer) + * + * // 方式2:使用工厂 * import { StorageFactory } from '@/common/storage' - * const storage = StorageFactory.getInstance() - * const url = await storage.upload('literature/123.pdf', buffer) + * const dataStorage = StorageFactory.getInstance() + * const staticStorage = StorageFactory.getStaticInstance() * ``` */ export type { StorageAdapter } from './StorageAdapter.js' export { LocalAdapter } from './LocalAdapter.js' export { OSSAdapter } from './OSSAdapter.js' +export type { OSSAdapterConfig } from './OSSAdapter.js' export { StorageFactory } from './StorageFactory.js' // Import for usage below import { StorageFactory } from './StorageFactory.js' /** - * 全局存储实例(推荐使用) + * 全局主存储实例(私有数据) * * 自动根据环境变量选择存储实现: * - STORAGE_TYPE=local: 本地文件系统 - * - STORAGE_TYPE=oss: 阿里云OSS + * - STORAGE_TYPE=oss: 阿里云OSS(私有Bucket,签名URL) + * + * 使用场景: + * - PKB文献文件 + * - 审稿PDF报告 + * - 临床数据文件 */ export const storage = StorageFactory.getInstance() +/** + * 全局静态资源存储实例 + * + * 自动根据环境变量选择存储实现: + * - STORAGE_TYPE=local: 本地文件系统 + * - STORAGE_TYPE=oss: 阿里云OSS(公共读Bucket) + * + * 使用场景: + * - 用户头像 + * - 系统Logo + * - RAG引用的图片 + */ +export const staticStorage = StorageFactory.getStaticInstance() diff --git a/backend/src/common/streaming/OpenAIStreamAdapter.ts b/backend/src/common/streaming/OpenAIStreamAdapter.ts index 1b9041fe..f9aca224 100644 --- a/backend/src/common/streaming/OpenAIStreamAdapter.ts +++ b/backend/src/common/streaming/OpenAIStreamAdapter.ts @@ -203,3 +203,6 @@ export function createOpenAIStreamAdapter( + + + diff --git a/backend/src/common/streaming/StreamingService.ts b/backend/src/common/streaming/StreamingService.ts index 09b8d088..66344eec 100644 --- a/backend/src/common/streaming/StreamingService.ts +++ b/backend/src/common/streaming/StreamingService.ts @@ -209,3 +209,6 @@ export async function streamChat( + + + diff --git a/backend/src/common/streaming/index.ts b/backend/src/common/streaming/index.ts index 97b3f6b5..f0d98ab2 100644 --- a/backend/src/common/streaming/index.ts +++ b/backend/src/common/streaming/index.ts @@ -27,3 +27,6 @@ export { THINKING_TAGS } from './types'; + + + diff --git a/backend/src/common/streaming/types.ts b/backend/src/common/streaming/types.ts index 704dd400..31bc1f9d 100644 --- a/backend/src/common/streaming/types.ts +++ b/backend/src/common/streaming/types.ts @@ -102,3 +102,6 @@ export type SSEEventType = + + + diff --git a/backend/src/config/env.ts b/backend/src/config/env.ts index 2e62b567..31cf5808 100644 --- a/backend/src/config/env.ts +++ b/backend/src/config/env.ts @@ -69,14 +69,23 @@ export const config = { /** 阿里云OSS地域 */ ossRegion: process.env.OSS_REGION, - /** 阿里云OSS Bucket名称 */ + /** 阿里云OSS Bucket名称(私有数据) */ ossBucket: process.env.OSS_BUCKET, + /** 阿里云OSS 静态资源Bucket名称(公共读) */ + ossBucketStatic: process.env.OSS_BUCKET_STATIC, + /** 阿里云OSS AccessKey ID */ ossAccessKeyId: process.env.OSS_ACCESS_KEY_ID, /** 阿里云OSS AccessKey Secret */ ossAccessKeySecret: process.env.OSS_ACCESS_KEY_SECRET, + + /** 是否使用OSS内网Endpoint(SAE部署必须为true) */ + ossInternal: process.env.OSS_INTERNAL === 'true', + + /** OSS签名URL过期时间(秒) */ + ossSignedUrlExpires: parseInt(process.env.OSS_SIGNED_URL_EXPIRES || '3600', 10), // ==================== 缓存配置(平台基础设施)==================== @@ -150,8 +159,8 @@ export const config = { // ==================== 文件上传配置(Legacy兼容)==================== - /** 文件上传大小限制 */ - uploadMaxSize: parseInt(process.env.UPLOAD_MAX_SIZE || '10485760', 10), // 10MB + /** 文件上传大小限制(30MB) */ + uploadMaxSize: parseInt(process.env.UPLOAD_MAX_SIZE || '31457280', 10), // 30MB /** 文件上传目录(Legacy兼容,新模块使用storage) */ uploadDir: process.env.UPLOAD_DIR || './uploads', @@ -185,6 +194,16 @@ export function validateEnv(): void { if (!config.ossBucket) errors.push('OSS_BUCKET is required when STORAGE_TYPE=oss') if (!config.ossAccessKeyId) errors.push('OSS_ACCESS_KEY_ID is required when STORAGE_TYPE=oss') if (!config.ossAccessKeySecret) errors.push('OSS_ACCESS_KEY_SECRET is required when STORAGE_TYPE=oss') + + // 可选配置警告 + if (!config.ossBucketStatic) { + warnings.push('OSS_BUCKET_STATIC not set, static resources will use main bucket') + } + + // 生产环境必须使用内网 + if (config.nodeEnv === 'production' && !config.ossInternal) { + warnings.push('OSS_INTERNAL should be true in production (SAE内网访问免流量费)') + } } // 如果使用Redis,验证Redis配置 diff --git a/backend/src/legacy/controllers/chatController.ts b/backend/src/legacy/controllers/chatController.ts index ef182b7d..06b72c76 100644 --- a/backend/src/legacy/controllers/chatController.ts +++ b/backend/src/legacy/controllers/chatController.ts @@ -275,11 +275,12 @@ export class ChatController { select: { id: true, filename: true, - difyDocumentId: true, + storageKey: true, // 原 difyDocumentId,现为 OSS 路径 }, }); - const difyDocIds = documents.map(d => d.difyDocumentId).filter(Boolean); + // Legacy: 此代码已废弃,Dify 已移除 + const difyDocIds = documents.map(d => d.storageKey).filter(Boolean); console.log(`📄 [ChatController] 目标Dify文档ID:`, difyDocIds); // 过滤结果 diff --git a/backend/src/legacy/services/documentService.ts b/backend/src/legacy/services/documentService.ts index f7552c67..8dd0b3da 100644 --- a/backend/src/legacy/services/documentService.ts +++ b/backend/src/legacy/services/documentService.ts @@ -48,7 +48,7 @@ export async function uploadDocument( fileType, fileSizeBytes, fileUrl, - difyDocumentId: '', // 暂时为空,稍后更新 + storageKey: '', // 原 difyDocumentId,现为 OSS 路径(Legacy 代码已废弃) status: 'uploading', progress: 0, }, @@ -88,11 +88,11 @@ export async function uploadDocument( filename ); - // 6. 更新文档记录(更新difyDocumentId、状态和Phase 2字段) + // 6. 更新文档记录(Legacy:此代码已废弃,Dify 已移除) const updatedDocument = await prisma.document.update({ where: { id: document.id }, data: { - difyDocumentId: difyResult.document.id, + storageKey: difyResult.document.id, // Legacy: 原为 difyDocumentId status: difyResult.document.indexing_status, progress: 50, // Phase 2新增字段 @@ -138,7 +138,7 @@ async function pollDocumentStatus( userId: string, kbId: string, documentId: string, - difyDocumentId: string, + legacyDifyDocId: string, // Legacy: 原为 difyDocumentId maxAttempts: number = 30 ) { const knowledgeBase = await prisma.knowledgeBase.findFirst({ @@ -156,7 +156,7 @@ async function pollDocumentStatus( // 查询Dify中的文档状态 const difyDocument = await difyClient.getDocument( knowledgeBase.difyDatasetId, - difyDocumentId + legacyDifyDocId ); // 更新数据库中的状态 @@ -264,12 +264,12 @@ export async function deleteDocument(userId: string, documentId: string) { throw new Error('Document not found or access denied'); } - // 2. 删除Dify中的文档 - if (document.difyDocumentId) { + // 2. 删除Dify中的文档(Legacy:此代码已废弃,Dify 已移除) + if (document.storageKey) { try { await difyClient.deleteDocument( document.knowledgeBase.difyDatasetId, - document.difyDocumentId + document.storageKey // Legacy: 原为 difyDocumentId ); } catch (error) { console.error('Failed to delete Dify document:', error); @@ -305,12 +305,12 @@ export async function reprocessDocument(userId: string, documentId: string) { throw new Error('Document not found or access denied'); } - // 2. 触发Dify重新索引 - if (document.difyDocumentId) { + // 2. 触发Dify重新索引(Legacy:此代码已废弃,Dify 已移除) + if (document.storageKey) { try { await difyClient.updateDocument( document.knowledgeBase.difyDatasetId, - document.difyDocumentId + document.storageKey // Legacy: 原为 difyDocumentId ); // 3. 更新状态为processing @@ -328,7 +328,7 @@ export async function reprocessDocument(userId: string, documentId: string) { userId, document.kbId, documentId, - document.difyDocumentId + document.storageKey // Legacy: 原为 difyDocumentId ).catch(error => { console.error('Failed to poll document status:', error); }); diff --git a/backend/src/modules/admin/routes/tenantRoutes.ts b/backend/src/modules/admin/routes/tenantRoutes.ts index 81689ce4..33d13ed8 100644 --- a/backend/src/modules/admin/routes/tenantRoutes.ts +++ b/backend/src/modules/admin/routes/tenantRoutes.ts @@ -88,3 +88,6 @@ export async function moduleRoutes(fastify: FastifyInstance) { + + + diff --git a/backend/src/modules/admin/types/tenant.types.ts b/backend/src/modules/admin/types/tenant.types.ts index eb086a08..4ab42908 100644 --- a/backend/src/modules/admin/types/tenant.types.ts +++ b/backend/src/modules/admin/types/tenant.types.ts @@ -118,3 +118,6 @@ export interface PaginatedResponse { + + + diff --git a/backend/src/modules/admin/types/user.types.ts b/backend/src/modules/admin/types/user.types.ts index 254dac0a..f44b3aa8 100644 --- a/backend/src/modules/admin/types/user.types.ts +++ b/backend/src/modules/admin/types/user.types.ts @@ -165,3 +165,6 @@ export const ROLE_DISPLAY_NAMES: Record = { + + + diff --git a/backend/src/modules/aia/controllers/agentController.ts b/backend/src/modules/aia/controllers/agentController.ts index fd467d54..ac267463 100644 --- a/backend/src/modules/aia/controllers/agentController.ts +++ b/backend/src/modules/aia/controllers/agentController.ts @@ -240,3 +240,6 @@ async function matchIntent(query: string): Promise<{ + + + diff --git a/backend/src/modules/aia/controllers/attachmentController.ts b/backend/src/modules/aia/controllers/attachmentController.ts index 84e376b4..c25cb67a 100644 --- a/backend/src/modules/aia/controllers/attachmentController.ts +++ b/backend/src/modules/aia/controllers/attachmentController.ts @@ -94,3 +94,6 @@ export async function uploadAttachment( + + + diff --git a/backend/src/modules/aia/index.ts b/backend/src/modules/aia/index.ts index 158349bf..af3289cd 100644 --- a/backend/src/modules/aia/index.ts +++ b/backend/src/modules/aia/index.ts @@ -23,3 +23,6 @@ export { aiaRoutes }; + + + diff --git a/backend/src/modules/asl/fulltext-screening/__tests__/api-integration-test.ts b/backend/src/modules/asl/fulltext-screening/__tests__/api-integration-test.ts index 8699418f..683f83e1 100644 --- a/backend/src/modules/asl/fulltext-screening/__tests__/api-integration-test.ts +++ b/backend/src/modules/asl/fulltext-screening/__tests__/api-integration-test.ts @@ -363,6 +363,9 @@ runTests().catch((error) => { + + + diff --git a/backend/src/modules/asl/fulltext-screening/__tests__/e2e-real-test-v2.ts b/backend/src/modules/asl/fulltext-screening/__tests__/e2e-real-test-v2.ts index 028540ed..7f9afac9 100644 --- a/backend/src/modules/asl/fulltext-screening/__tests__/e2e-real-test-v2.ts +++ b/backend/src/modules/asl/fulltext-screening/__tests__/e2e-real-test-v2.ts @@ -304,6 +304,9 @@ runTest() + + + diff --git a/backend/src/modules/asl/fulltext-screening/__tests__/fulltext-screening-api.http b/backend/src/modules/asl/fulltext-screening/__tests__/fulltext-screening-api.http index 82766b82..3023c8e7 100644 --- a/backend/src/modules/asl/fulltext-screening/__tests__/fulltext-screening-api.http +++ b/backend/src/modules/asl/fulltext-screening/__tests__/fulltext-screening-api.http @@ -342,6 +342,9 @@ Content-Type: application/json + + + diff --git a/backend/src/modules/dc/tool-b/services/ConflictDetectionService.ts b/backend/src/modules/dc/tool-b/services/ConflictDetectionService.ts index 1decee08..805ad3ab 100644 --- a/backend/src/modules/dc/tool-b/services/ConflictDetectionService.ts +++ b/backend/src/modules/dc/tool-b/services/ConflictDetectionService.ts @@ -278,6 +278,9 @@ export const conflictDetectionService = new ConflictDetectionService(); + + + diff --git a/backend/src/modules/dc/tool-c/README.md b/backend/src/modules/dc/tool-c/README.md index 75d21ce8..59a799b9 100644 --- a/backend/src/modules/dc/tool-c/README.md +++ b/backend/src/modules/dc/tool-c/README.md @@ -228,6 +228,9 @@ curl -X POST http://localhost:3000/api/v1/dc/tool-c/test/execute \ + + + diff --git a/backend/src/modules/dc/tool-c/controllers/StreamAIController.ts b/backend/src/modules/dc/tool-c/controllers/StreamAIController.ts index 19ad5f29..2e49bb16 100644 --- a/backend/src/modules/dc/tool-c/controllers/StreamAIController.ts +++ b/backend/src/modules/dc/tool-c/controllers/StreamAIController.ts @@ -282,6 +282,9 @@ export const streamAIController = new StreamAIController(); + + + diff --git a/backend/src/modules/iit-manager/agents/SessionMemory.ts b/backend/src/modules/iit-manager/agents/SessionMemory.ts index fbf5bcb9..b035e9fd 100644 --- a/backend/src/modules/iit-manager/agents/SessionMemory.ts +++ b/backend/src/modules/iit-manager/agents/SessionMemory.ts @@ -191,6 +191,9 @@ logger.info('[SessionMemory] 会话记忆管理器已启动', { + + + diff --git a/backend/src/modules/iit-manager/check-iit-table-structure.ts b/backend/src/modules/iit-manager/check-iit-table-structure.ts index 30e44f4e..6564d850 100644 --- a/backend/src/modules/iit-manager/check-iit-table-structure.ts +++ b/backend/src/modules/iit-manager/check-iit-table-structure.ts @@ -125,6 +125,9 @@ checkTableStructure(); + + + diff --git a/backend/src/modules/iit-manager/check-project-config.ts b/backend/src/modules/iit-manager/check-project-config.ts index c245ad40..981dcfcf 100644 --- a/backend/src/modules/iit-manager/check-project-config.ts +++ b/backend/src/modules/iit-manager/check-project-config.ts @@ -112,6 +112,9 @@ checkProjectConfig().catch(console.error); + + + diff --git a/backend/src/modules/iit-manager/check-test-project-in-db.ts b/backend/src/modules/iit-manager/check-test-project-in-db.ts index c6b59214..fc8abd1f 100644 --- a/backend/src/modules/iit-manager/check-test-project-in-db.ts +++ b/backend/src/modules/iit-manager/check-test-project-in-db.ts @@ -94,6 +94,9 @@ main(); + + + diff --git a/backend/src/modules/iit-manager/docs/微信服务号接入指南.md b/backend/src/modules/iit-manager/docs/微信服务号接入指南.md index 68e98373..7f65ed4c 100644 --- a/backend/src/modules/iit-manager/docs/微信服务号接入指南.md +++ b/backend/src/modules/iit-manager/docs/微信服务号接入指南.md @@ -551,6 +551,9 @@ URL: https://iit.xunzhengyixue.com/api/v1/iit/patient-wechat/callback + + + diff --git a/backend/src/modules/iit-manager/generate-wechat-tokens.ts b/backend/src/modules/iit-manager/generate-wechat-tokens.ts index bcdabca1..dc32a9fb 100644 --- a/backend/src/modules/iit-manager/generate-wechat-tokens.ts +++ b/backend/src/modules/iit-manager/generate-wechat-tokens.ts @@ -186,6 +186,9 @@ console.log(''); + + + diff --git a/backend/src/modules/iit-manager/services/PatientWechatService.ts b/backend/src/modules/iit-manager/services/PatientWechatService.ts index 43dff7b3..c4b8aa33 100644 --- a/backend/src/modules/iit-manager/services/PatientWechatService.ts +++ b/backend/src/modules/iit-manager/services/PatientWechatService.ts @@ -503,6 +503,9 @@ export const patientWechatService = new PatientWechatService(); + + + diff --git a/backend/src/modules/iit-manager/test-chatservice-dify.ts b/backend/src/modules/iit-manager/test-chatservice-dify.ts index 8110cb9b..26c3a3c8 100644 --- a/backend/src/modules/iit-manager/test-chatservice-dify.ts +++ b/backend/src/modules/iit-manager/test-chatservice-dify.ts @@ -148,6 +148,9 @@ testDifyIntegration().catch(error => { + + + diff --git a/backend/src/modules/iit-manager/test-iit-database.ts b/backend/src/modules/iit-manager/test-iit-database.ts index f34888f8..8910877f 100644 --- a/backend/src/modules/iit-manager/test-iit-database.ts +++ b/backend/src/modules/iit-manager/test-iit-database.ts @@ -177,6 +177,9 @@ testIitDatabase() + + + diff --git a/backend/src/modules/iit-manager/test-patient-wechat-config.ts b/backend/src/modules/iit-manager/test-patient-wechat-config.ts index 2c18e760..6131fe25 100644 --- a/backend/src/modules/iit-manager/test-patient-wechat-config.ts +++ b/backend/src/modules/iit-manager/test-patient-wechat-config.ts @@ -163,6 +163,9 @@ if (hasError) { + + + diff --git a/backend/src/modules/iit-manager/test-patient-wechat-url-verify.ts b/backend/src/modules/iit-manager/test-patient-wechat-url-verify.ts index 33ef62c8..cae85043 100644 --- a/backend/src/modules/iit-manager/test-patient-wechat-url-verify.ts +++ b/backend/src/modules/iit-manager/test-patient-wechat-url-verify.ts @@ -189,6 +189,9 @@ async function testUrlVerification() { + + + diff --git a/backend/src/modules/iit-manager/test-redcap-query-from-db.ts b/backend/src/modules/iit-manager/test-redcap-query-from-db.ts index aeb47f7f..d60c1d1f 100644 --- a/backend/src/modules/iit-manager/test-redcap-query-from-db.ts +++ b/backend/src/modules/iit-manager/test-redcap-query-from-db.ts @@ -270,6 +270,9 @@ main().catch((error) => { + + + diff --git a/backend/src/modules/iit-manager/test-wechat-mp-local.ps1 b/backend/src/modules/iit-manager/test-wechat-mp-local.ps1 index 3ab16325..81510e9d 100644 --- a/backend/src/modules/iit-manager/test-wechat-mp-local.ps1 +++ b/backend/src/modules/iit-manager/test-wechat-mp-local.ps1 @@ -154,6 +154,9 @@ Write-Host "" + + + diff --git a/backend/src/modules/iit-manager/types/index.ts b/backend/src/modules/iit-manager/types/index.ts index 90cbf629..7f9af3ce 100644 --- a/backend/src/modules/iit-manager/types/index.ts +++ b/backend/src/modules/iit-manager/types/index.ts @@ -247,6 +247,9 @@ export interface CachedProtocolRules { + + + diff --git a/backend/src/modules/pkb/controllers/documentController.ts b/backend/src/modules/pkb/controllers/documentController.ts index 085cd3e6..71785ee4 100644 --- a/backend/src/modules/pkb/controllers/documentController.ts +++ b/backend/src/modules/pkb/controllers/documentController.ts @@ -1,5 +1,8 @@ import type { FastifyRequest, FastifyReply } from 'fastify'; import * as documentService from '../services/documentService.js'; +import { storage } from '../../../common/storage/index.js'; +import { randomUUID } from 'crypto'; +import path from 'path'; /** * 获取用户ID(从JWT Token中获取) @@ -12,6 +15,30 @@ function getUserId(request: FastifyRequest): string { return userId; } +/** + * 获取租户ID(从JWT Token中获取) + */ +function getTenantId(request: FastifyRequest): string { + const tenantId = (request as any).user?.tenantId; + // 如果没有租户ID,使用默认值 + return tenantId || 'default'; +} + +/** + * 生成 PKB 文档存储 Key + * 格式:tenants/{tenantId}/users/{userId}/pkb/{kbId}/{uuid}.{ext} + */ +function generatePkbStorageKey( + tenantId: string, + userId: string, + kbId: string, + filename: string +): string { + const uuid = randomUUID().replace(/-/g, '').substring(0, 16); + const ext = path.extname(filename).toLowerCase(); + return `tenants/${tenantId}/users/${userId}/pkb/${kbId}/${uuid}${ext}`; +} + /** * 上传文档 */ @@ -45,15 +72,15 @@ export async function uploadDocument( const fileType = data.mimetype; const fileSizeBytes = file.length; - // 文件大小限制(10MB) - const maxSize = 10 * 1024 * 1024; - console.log(`📊 文件大小: ${(fileSizeBytes / 1024 / 1024).toFixed(2)}MB (限制: 10MB)`); + // 文件大小限制(30MB - 按 OSS 规范) + const maxSize = 30 * 1024 * 1024; + console.log(`📊 文件大小: ${(fileSizeBytes / 1024 / 1024).toFixed(2)}MB (限制: 30MB)`); if (fileSizeBytes > maxSize) { console.error(`❌ 文件太大: ${(fileSizeBytes / 1024 / 1024).toFixed(2)}MB`); return reply.status(400).send({ success: false, - message: 'File size exceeds 10MB limit', + message: 'File size exceeds 30MB limit', }); } @@ -75,9 +102,30 @@ export async function uploadDocument( }); } - // 上传文档(这里fileUrl暂时为空,实际应该上传到对象存储) - console.log(`⚙️ 调用文档服务上传文件...`); + // 获取用户信息 const userId = getUserId(request); + const tenantId = getTenantId(request); + + // 生成 OSS 存储 Key(包含 kbId) + const storageKey = generatePkbStorageKey(tenantId, userId, kbId, filename); + console.log(`📦 OSS 存储路径: ${storageKey}`); + + // 上传到 OSS + console.log(`☁️ 上传文件到存储服务...`); + let fileUrl = ''; + try { + fileUrl = await storage.upload(storageKey, file); + console.log(`✅ 文件已上传到存储服务`); + } catch (storageError) { + console.error(`❌ 存储服务上传失败:`, storageError); + return reply.status(500).send({ + success: false, + message: 'Failed to upload file to storage', + }); + } + + // 调用文档服务处理(传入 storageKey) + console.log(`⚙️ 调用文档服务处理文件...`); const document = await documentService.uploadDocument( userId, kbId, @@ -85,7 +133,8 @@ export async function uploadDocument( filename, fileType, fileSizeBytes, - '' // fileUrl - 可以上传到OSS后填入 + fileUrl, + storageKey // 新增:存储路径 ); console.log(`✅ 文档上传成功: ${document.id}`); diff --git a/backend/src/modules/pkb/routes/health.ts b/backend/src/modules/pkb/routes/health.ts index a66fb04f..b56f95e2 100644 --- a/backend/src/modules/pkb/routes/health.ts +++ b/backend/src/modules/pkb/routes/health.ts @@ -60,6 +60,9 @@ export default async function healthRoutes(fastify: FastifyInstance) { + + + diff --git a/backend/src/modules/pkb/services/documentService.ts b/backend/src/modules/pkb/services/documentService.ts index fd344608..fecbbc86 100644 --- a/backend/src/modules/pkb/services/documentService.ts +++ b/backend/src/modules/pkb/services/documentService.ts @@ -2,6 +2,7 @@ import { prisma } from '../../../config/database.js'; import { logger } from '../../../common/logging/index.js'; import { extractionClient } from '../../../common/document/ExtractionClient.js'; import { ingestDocument as ragIngestDocument } from './ragService.js'; +import { storage } from '../../../common/storage/index.js'; /** * 文档服务 @@ -11,6 +12,15 @@ import { ingestDocument as ragIngestDocument } from './ragService.js'; /** * 上传文档到知识库 + * + * @param userId - 用户ID + * @param kbId - 知识库ID + * @param file - 文件内容 Buffer + * @param filename - 原始文件名 + * @param fileType - MIME 类型 + * @param fileSizeBytes - 文件大小(字节) + * @param fileUrl - 文件访问 URL(签名URL) + * @param storageKey - OSS 存储路径(新增) */ export async function uploadDocument( userId: string, @@ -19,7 +29,8 @@ export async function uploadDocument( filename: string, fileType: string, fileSizeBytes: number, - fileUrl: string + fileUrl: string, + storageKey?: string // 新增:OSS 存储路径 ) { // 1. 验证知识库权限 const knowledgeBase = await prisma.knowledgeBase.findFirst({ @@ -65,7 +76,7 @@ export async function uploadDocument( fileType, fileSizeBytes, fileUrl, - difyDocumentId: '', // 不再使用 + storageKey: storageKey || '', // OSS 存储路径 status: 'uploading', progress: 0, }, @@ -107,10 +118,10 @@ export async function uploadDocument( }); // 7. 更新文档记录 - pgvector 模式立即完成 + // 注意:storageKey 已在创建时设置,这里不需要更新 const updatedDocument = await prisma.document.update({ where: { id: document.id }, data: { - difyDocumentId: ingestResult.documentId || '', status: 'completed', progress: 100, // 提取信息 @@ -234,13 +245,23 @@ export async function deleteDocument(userId: string, documentId: string) { logger.info(`[PKB] 删除文档: documentId=${documentId}`); + // 1.5 删除 OSS 文件 + if (document.storageKey) { + try { + await storage.delete(document.storageKey); + logger.info(`[PKB] OSS 文件已删除: ${document.storageKey}`); + } catch (storageError) { + logger.warn(`[PKB] OSS 文件删除失败,继续删除数据库记录`, { storageError }); + } + } + // 2. 删除 EKB 中的文档和 Chunks try { // 查找 EKB 文档(通过 filename 和 kbId 匹配) const ekbDoc = await prisma.ekbDocument.findFirst({ where: { filename: document.filename, - kb: { + knowledgeBase: { ownerId: userId, name: document.knowledgeBase.name, }, @@ -311,7 +332,7 @@ export async function reprocessDocument(userId: string, documentId: string) { const ekbDoc = await prisma.ekbDocument.findFirst({ where: { filename: document.filename, - kb: { + knowledgeBase: { ownerId: userId, name: document.knowledgeBase.name, }, diff --git a/backend/src/modules/rvw/__tests__/api.http b/backend/src/modules/rvw/__tests__/api.http index 423d069b..b8138e81 100644 --- a/backend/src/modules/rvw/__tests__/api.http +++ b/backend/src/modules/rvw/__tests__/api.http @@ -140,5 +140,8 @@ Content-Type: application/json + + + diff --git a/backend/src/modules/rvw/__tests__/test-api.ps1 b/backend/src/modules/rvw/__tests__/test-api.ps1 index 0f5a21bb..772b5b39 100644 --- a/backend/src/modules/rvw/__tests__/test-api.ps1 +++ b/backend/src/modules/rvw/__tests__/test-api.ps1 @@ -125,5 +125,8 @@ Write-Host " - 删除任务: DELETE $BaseUrl/api/v1/rvw/tasks/{taskId}" -Foregr + + + diff --git a/backend/src/modules/rvw/index.ts b/backend/src/modules/rvw/index.ts index e1912bfc..2b715071 100644 --- a/backend/src/modules/rvw/index.ts +++ b/backend/src/modules/rvw/index.ts @@ -39,5 +39,8 @@ export * from './services/utils.js'; + + + diff --git a/backend/src/modules/rvw/services/utils.ts b/backend/src/modules/rvw/services/utils.ts index b885effa..2d8d71df 100644 --- a/backend/src/modules/rvw/services/utils.ts +++ b/backend/src/modules/rvw/services/utils.ts @@ -130,5 +130,8 @@ export function validateAgentSelection(agents: string[]): void { + + + diff --git a/backend/src/tests/README.md b/backend/src/tests/README.md index b2f5c3c9..eae948cb 100644 --- a/backend/src/tests/README.md +++ b/backend/src/tests/README.md @@ -428,6 +428,9 @@ SET session_replication_role = 'origin'; + + + diff --git a/backend/src/tests/test-cross-language-search.ts b/backend/src/tests/test-cross-language-search.ts index 1678f6f2..7a38ee57 100644 --- a/backend/src/tests/test-cross-language-search.ts +++ b/backend/src/tests/test-cross-language-search.ts @@ -110,3 +110,6 @@ async function testCrossLanguageSearch() { testCrossLanguageSearch(); + + + diff --git a/backend/src/tests/test-query-rewrite.ts b/backend/src/tests/test-query-rewrite.ts index 7978becf..0028436d 100644 --- a/backend/src/tests/test-query-rewrite.ts +++ b/backend/src/tests/test-query-rewrite.ts @@ -172,3 +172,6 @@ async function testQueryRewrite() { testQueryRewrite(); + + + diff --git a/backend/src/tests/test-rerank.ts b/backend/src/tests/test-rerank.ts index f8469726..93ea3a71 100644 --- a/backend/src/tests/test-rerank.ts +++ b/backend/src/tests/test-rerank.ts @@ -118,3 +118,6 @@ async function testRerank() { testRerank(); + + + diff --git a/backend/src/tests/verify-test1-database.sql b/backend/src/tests/verify-test1-database.sql index 03a5c62a..5203503f 100644 --- a/backend/src/tests/verify-test1-database.sql +++ b/backend/src/tests/verify-test1-database.sql @@ -130,6 +130,9 @@ WHERE key = 'verify_test'; + + + diff --git a/backend/src/tests/verify-test1-database.ts b/backend/src/tests/verify-test1-database.ts index 14439b07..ddd958fe 100644 --- a/backend/src/tests/verify-test1-database.ts +++ b/backend/src/tests/verify-test1-database.ts @@ -273,6 +273,9 @@ verifyDatabase() + + + diff --git a/backend/src/types/global.d.ts b/backend/src/types/global.d.ts index f6775e1f..2ee915bd 100644 --- a/backend/src/types/global.d.ts +++ b/backend/src/types/global.d.ts @@ -63,6 +63,9 @@ export {} + + + diff --git a/backend/sync-dc-database.ps1 b/backend/sync-dc-database.ps1 index afd8568e..21c9bb56 100644 --- a/backend/sync-dc-database.ps1 +++ b/backend/sync-dc-database.ps1 @@ -86,6 +86,9 @@ Write-Host "✅ 完成!" -ForegroundColor Green + + + diff --git a/backend/temp_check.sql b/backend/temp_check.sql index 11579160..391e589a 100644 --- a/backend/temp_check.sql +++ b/backend/temp_check.sql @@ -15,3 +15,6 @@ SELECT schema_name FROM information_schema.schemata WHERE schema_name NOT IN ('p + + + diff --git a/backend/test-pkb-migration.http b/backend/test-pkb-migration.http index cde49374..123f0af5 100644 --- a/backend/test-pkb-migration.http +++ b/backend/test-pkb-migration.http @@ -174,6 +174,9 @@ DELETE {{baseUrl}}/api/v1/pkb/knowledge/knowledge-bases/{{testKbId}} + + + diff --git a/backend/test-tool-c-advanced-scenarios.mjs b/backend/test-tool-c-advanced-scenarios.mjs index cf400788..1e5ccf30 100644 --- a/backend/test-tool-c-advanced-scenarios.mjs +++ b/backend/test-tool-c-advanced-scenarios.mjs @@ -373,6 +373,9 @@ runAdvancedTests().catch(error => { + + + diff --git a/backend/test-tool-c-day2.mjs b/backend/test-tool-c-day2.mjs index eff118b3..5655344f 100644 --- a/backend/test-tool-c-day2.mjs +++ b/backend/test-tool-c-day2.mjs @@ -439,6 +439,9 @@ runAllTests() + + + diff --git a/backend/test-tool-c-day3.mjs b/backend/test-tool-c-day3.mjs index adc64098..910ad244 100644 --- a/backend/test-tool-c-day3.mjs +++ b/backend/test-tool-c-day3.mjs @@ -397,6 +397,9 @@ runAllTests() + + + diff --git a/backend/verify_all_users.ts b/backend/verify_all_users.ts index 58e37939..8dc9ed09 100644 --- a/backend/verify_all_users.ts +++ b/backend/verify_all_users.ts @@ -35,3 +35,6 @@ main() + + + diff --git a/backend/verify_functions.ts b/backend/verify_functions.ts index 4a806316..640c9ced 100644 --- a/backend/verify_functions.ts +++ b/backend/verify_functions.ts @@ -33,3 +33,6 @@ main() + + + diff --git a/backend/verify_job_common.ts b/backend/verify_job_common.ts index b0364fc6..1665063d 100644 --- a/backend/verify_job_common.ts +++ b/backend/verify_job_common.ts @@ -45,3 +45,6 @@ main() + + + diff --git a/backend/verify_mock_user.ts b/backend/verify_mock_user.ts index 44cb16c7..b184aabf 100644 --- a/backend/verify_mock_user.ts +++ b/backend/verify_mock_user.ts @@ -34,3 +34,6 @@ main() + + + diff --git a/backend/verify_system.ts b/backend/verify_system.ts index 7ead9bfe..b3180522 100644 --- a/backend/verify_system.ts +++ b/backend/verify_system.ts @@ -174,3 +174,6 @@ main() + + + diff --git a/deploy-to-sae.ps1 b/deploy-to-sae.ps1 index 95bc3474..359fbc87 100644 --- a/deploy-to-sae.ps1 +++ b/deploy-to-sae.ps1 @@ -181,6 +181,9 @@ Set-Location .. + + + diff --git a/docs/00-系统总体设计/00-系统当前状态与开发指南.md b/docs/00-系统总体设计/00-系统当前状态与开发指南.md index d57f1c63..b463b336 100644 --- a/docs/00-系统总体设计/00-系统当前状态与开发指南.md +++ b/docs/00-系统总体设计/00-系统当前状态与开发指南.md @@ -1,15 +1,19 @@ # AIclinicalresearch 系统当前状态与开发指南 -> **文档版本:** v4.0 +> **文档版本:** v4.1 > **创建日期:** 2025-11-28 > **维护者:** 开发团队 -> **最后更新:** 2026-01-21 -> **🎉 重大里程碑:** **成功替换 Dify!PKB 模块完全使用自研 pgvector RAG 引擎!** -> - ✅ **Dify 已移除**:PKB 模块不再依赖外部 RAG 服务 -> - ✅ ekb_schema 第13个独立Schema,3张表,HNSW 向量索引 -> - ✅ 完整 RAG 链路:文档处理 → 向量化 → 检索 → Rerank -> - ✅ 跨语言支持:DeepSeek V3 查询理解 + text-embedding-v4 -> - ✅ 端到端测试通过,生产就绪 +> **最后更新:** 2026-01-22 +> **🎉 重大里程碑:** +> - **2026-01-22:OSS 存储集成完成!** 阿里云 OSS 正式接入平台基础层 +> - **2026-01-21:成功替换 Dify!** PKB 模块完全使用自研 pgvector RAG 引擎 +> +> **最新进展(OSS 集成):** +> - ✅ **4个 Bucket 已创建**:生产/开发 × 数据/静态资源 +> - ✅ **存储适配器架构**:OSSAdapter + LocalAdapter,支持云端和私有化部署 +> - ✅ **PKB 模块集成**:文档上传到 OSS,目录结构规范化 +> - ✅ **开发规范建立**:`docs/04-开发规范/11-OSS存储开发规范.md` +> > **部署状态:** ✅ 生产环境运行中 | 公网地址:http://8.140.53.236/ > **文档目的:** 快速了解系统当前状态,为新AI助手提供上下文 @@ -84,6 +88,11 @@ │ ├── 任务管理:job.data 统一存储 ✅ │ │ └── 断点续传:CheckpointService 通用化 ✅ │ │ │ +│ 🆕 **OSS 存储服务**(2026-01-22) │ +│ ├── 阿里云 OSS:4 Bucket(生产/开发 × 数据/静态) │ +│ ├── StorageAdapter:OSSAdapter + LocalAdapter │ +│ └── 私有化部署:STORAGE_TYPE=local 支持本地存储 │ +│ │ │ 存储 | 日志 | 缓存 | 任务 | 健康检查 | 监控 | 连接池 │ │ ✅ ✅ ✅ ✅ ✅ ✅ ✅ │ └─────────────────────────────────────────────────────────┘ @@ -115,7 +124,7 @@ - ✅ 前端Nginx(v1.0)- 内网:172.17.173.72:80 - ✅ CLB负载均衡 - 公网:http://8.140.53.236/ - RDS PostgreSQL 15(生产环境运行中) -- OSS对象存储(已配置) +- OSS对象存储(✅ 2026-01-22 已集成,4个Bucket) - ACR容器镜像仓库(已推送3个镜像) - 阿里云 ACR (容器镜像服务) ✅ 已推送3个镜像(Frontend、Backend、Python) - 阿里云 RDS (PostgreSQL 15) ✅ 已迁移数据 @@ -125,9 +134,49 @@ --- -## 🚀 当前开发状态(2026-01-21) +## 🚀 当前开发状态(2026-01-22) -### 🏆 最新进展:成功替换 Dify!PKB 完全使用自研 RAG 引擎(2026-01-21) +### 🆕 最新进展:OSS 存储集成完成(2026-01-22) + +#### ✅ 阿里云 OSS 正式接入平台基础层 + +**重大里程碑**: +- 🎉 **存储服务上线**:文件持久化从本地存储升级到云端 OSS +- 🎉 **双模式架构**:支持 SaaS 云端部署和医疗机构私有化部署 +- 🎉 **PKB 首个集成**:个人知识库文档已对接 OSS 存储 + +**核心组件**: +| 组件 | 说明 | 状态 | +|------|------|------| +| OSSAdapter | 阿里云 OSS 存储实现 | ✅ | +| LocalAdapter | 本地文件系统实现(私有化部署) | ✅ | +| StorageFactory | 根据环境变量自动选择适配器 | ✅ | +| 签名URL | 支持原始文件名下载 | ✅ | + +**Bucket 配置**: +| Bucket | 用途 | 权限 | +|--------|------|------| +| ai-clinical-data | 生产数据 | 私有 + SSE-OSS 加密 | +| ai-clinical-data-dev | 开发数据 | 私有 | +| ai-clinical-static | 生产静态资源 | 公共读 | +| ai-clinical-static-dev | 开发静态资源 | 公共读 | + +**目录结构规范**: +``` +tenants/{tenantId}/users/{userId}/pkb/{kbId}/{uuid}.{ext} +``` + +**相关文档**: +- 实施方案:`docs/01-平台基础层/02-存储服务/OSS存储实施方案-MVP版.md` +- 开发规范:`docs/04-开发规范/11-OSS存储开发规范.md` +- 开发记录:`docs/01-平台基础层/02-存储服务/OSS集成开发记录-2026-01-22.md` + +**数据库变更**: +- `difyDocumentId` 字段重命名为 `storageKey`(存储 OSS 路径) + +--- + +### 🏆 里程碑:成功替换 Dify!PKB 完全使用自研 RAG 引擎(2026-01-21) #### ✅ Dify 已完全移除,pgvector RAG 引擎生产可用 @@ -507,6 +556,7 @@ data: [DONE]\n\n - ✅ **pgvector 0.8.1 已集成**(2026-01-19) - ✅ **自研 RAG 引擎上线,Dify 已移除**(2026-01-21) - ✅ **跨语言检索**:DeepSeek V3 查询理解 + 中英双语 +- ✅ **OSS 存储集成**(2026-01-22):文档存储云端化 **待解决问题**: - 🔧 OSS 存储集成待完善 @@ -1004,7 +1054,8 @@ AIclinicalresearch/ | **2026-01-07 下午** | **PKB批处理完善** 🏆 | ✅ 批处理完整流程调试通过(执行+进度+结果导出)+ 文档上传功能 + UI优化 | | **2026-01-19** | **pgvector集成** 🎉 | ✅ pgvector 0.8.1 安装成功,PKB RAG基础设施就绪 | | **2026-01-21** | **🎉 Dify替换完成** | ✅ PKB 成功替换 Dify,完全使用自研 pgvector RAG 引擎 | -| **当前** | **PKB模块生产可用** | ✅ 核心功能全部实现(95%),Dify已移除,自研RAG引擎上线 | +| **2026-01-22** | **🆕 OSS存储集成** | ✅ 阿里云OSS接入,PKB文档存储云端化,建立存储开发规范 | +| **当前** | **PKB模块生产可用** | ✅ 核心功能全部实现(95%),自研RAG+OSS存储上线 | | **2026-01-07 晚** | **RVW模块开发完成** 🎉 | ✅ Phase 1-3完成(后端迁移+数据库扩展+前端重构) | --- @@ -1309,6 +1360,11 @@ if (items.length >= 50) { - ✅ **彻底移除 Dify 依赖**:PKB 模块完全使用自研 pgvector 引擎 - pgvector 向量检索 + DeepSeek V3 查询理解 + qwen3-rerank 重排序 - 跨语言支持:中文查询匹配英文文档(准确率 +20.5%) +4. ✅ **🆕 OSS 存储集成完成!** 🏆 **2026-01-22** + - ✅ **阿里云 OSS 接入**:4个 Bucket(生产/开发 × 数据/静态) + - ✅ **存储适配器架构**:OSSAdapter + LocalAdapter,支持云端和私有化部署 + - ✅ **PKB 集成**:文档存储云端化,目录结构规范化 + - ✅ **开发规范建立**:`11-OSS存储开发规范.md` - Brain-Hand 架构:业务层思考,引擎层执行 - 成本:¥0.0025/次,延迟:2.5秒 4. ✅ **适配器模式**:存储/缓存/日志支持本地↔云端零代码切换 @@ -1328,9 +1384,9 @@ if (items.length >= 50) { --- -**文档版本**:v4.0 -**最后更新**:2026-01-21 -**下次更新**:OSS 存储集成 或 pg_bigm 扩展安装 +**文档版本**:v4.1 +**最后更新**:2026-01-22 +**下次更新**:pg_bigm 扩展安装 或 Tool C/RVW OSS 集成 --- diff --git a/docs/01-平台基础层/02-存储服务/OSS存储实施方案-MVP版.md b/docs/01-平台基础层/02-存储服务/OSS存储实施方案-MVP版.md new file mode 100644 index 00000000..79f16951 --- /dev/null +++ b/docs/01-平台基础层/02-存储服务/OSS存储实施方案-MVP版.md @@ -0,0 +1,742 @@ +# OSS 存储实施方案 - MVP版 + +> **文档版本:** v1.0 +> **创建日期:** 2026-01-22 +> **状态:** 待实施 +> **适用团队:** 2人开发团队 + +--- + +## 1. 背景与目标 + +### 1.1 当前问题 + +| 问题 | 影响 | 严重程度 | +|------|------|----------| +| `OSSAdapter` 未实现 | 无法部署到阿里云 | 🔴 高 | +| PKB 文件未持久化 | 原始文件丢失,无法重新处理 | 🔴 高 | +| `ali-oss` 依赖未安装 | OSS 代码无法运行 | 🔴 高 | +| 缺少签名 URL 方法 | 私有文件无法安全访问 | 🟡 中 | + +### 1.2 实施目标 + +1. **完成 OSSAdapter 实现** - 支持本地/云端无缝切换 +2. **PKB 文件持久化** - 上传时存储原始文件到 OSS +3. **开发环境可测试** - 本地开发使用 LocalAdapter,云端使用 OSSAdapter + +--- + +## 2. 架构决策 + +### 2.1 核心原则 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ MVP 极简原则 │ +├─────────────────────────────────────────────────────────────┤ +│ ✅ 后端统一管控:前端只提交 FormData,不接触 OSS │ +│ ✅ 工厂模式切换:STORAGE_TYPE 环境变量控制本地/云端 │ +│ ✅ 串行处理优先:先存储再处理,简单可靠 │ +│ ❌ 不做过早优化:不搞分层存储、不搞归档、不搞双流分发 │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 2.2 Bucket 规划 + +保持 4 Bucket 物理隔离(安全底线): + +| 环境 | Bucket | ACL | 用途 | +|------|--------|-----|------| +| 生产 | `ai-clinical-data` | 私有 | 核心数据(文献、病历、统计结果) | +| 生产 | `ai-clinical-static` | 公共读 | 静态资源(头像、Logo、RAG图片) | +| 开发 | `ai-clinical-data-dev` | 私有 | 开发测试 | +| 开发 | `ai-clinical-static-dev` | 公共读 | 开发测试 | + +### 2.3 目录结构 + +基于现有租户模型,统一使用以下结构: + +``` +# 1. 用户私有数据 (User-Level Data) +# 逻辑:归属于特定租户下的特定用户 +tenants/{tenantId}/users/{userId}/{module}/{uuid}.{ext} + +# 示例 +tenants/t001/users/u123/pkb/a1b2c3d4.pdf # PKB 文献(个人) +tenants/t001/users/u123/asl/e5f6g7h8.pdf # ASL 文献(个人) +tenants/t001/users/u123/rvw/i9j0k1l2.docx # RVW 稿件(个人) +tenants/t001/users/u123/ssa/m3n4o5p6.xlsx # SSA 统计数据(个人) + +# 2. 租户共享数据 (Tenant-Level Shared Data) +# 逻辑:归属于租户,通常由管理员上传或全员共享 +tenants/{tenantId}/shared/{module}/{uuid}.{ext} + +# 示例 +tenants/t001/shared/ekb/q7r8s9t0.pdf # 租户知识库 EKB(全员共享) +tenants/t001/shared/emr/u1v2w3x4.json # 原始病历数据 EMR(机构数据) +tenants/t001/shared/templates/y5z6a7b8.docx # 机构模板文件 + +# 3. 临时文件(OSS 生命周期 1 天自动删除) +temp/{date}/{uuid}.{ext} + +# 示例 +temp/20260122/c3d4e5f6.xlsx # Tool C 上传 / ASL 导入 + +# 4. 系统文件(平台级资源) +system/{category}/{filename} + +# 示例 +system/templates/gcp_guide.pdf # 系统预置模板 +system/samples/demo_data.xlsx # 演示数据 +``` + +### 2.4 Key 生成规则 + +```typescript +// 后端生成 Key 的统一方法 +function generateStorageKey( + tenantId: string, + userId: string | null, // null 表示租户共享 + module: string, + filename: string +): string { + const uuid = generateUUID(); + const ext = path.extname(filename); + + if (userId) { + // 用户私有数据 + return `tenants/${tenantId}/users/${userId}/${module}/${uuid}${ext}`; + } else { + // 租户共享数据 + return `tenants/${tenantId}/shared/${module}/${uuid}${ext}`; + } +} + +// 使用示例 +const key1 = generateStorageKey('t001', 'u123', 'pkb', 'paper.pdf'); +// → tenants/t001/users/u123/pkb/a1b2c3d4.pdf + +const key2 = generateStorageKey('t001', null, 'ekb', 'guideline.pdf'); +// → tenants/t001/shared/ekb/q7r8s9t0.pdf +``` + +--- + +## 3. 红线规则(修订版) + +### 🔴 红线 1:内网连接 + +```bash +# SAE 生产环境必须配置内网 Endpoint +OSS_ENDPOINT=oss-cn-beijing-internal.aliyuncs.com + +# 本地开发使用公网 Endpoint +OSS_ENDPOINT=oss-cn-beijing.aliyuncs.com +``` + +**违反后果**:流量费用暴增,大文件上传卡死 + +### 🔴 红线 2:私有存储 + +``` +ai-clinical-data Bucket 必须设置为 Private +绝对不能为了测试方便而开放公共读 +``` + +**违反后果**:医疗数据泄露,合规风险 + +### 🟡 红线 3:内存安全(分层策略) + +| 文件大小 | 处理方式 | 说明 | +|----------|---------|------| +| < 30MB | `toBuffer()` ✅ | MVP 阶段可用,简单优先 | +| 30-50MB | 两者皆可 | 视场景选择 | +| > 50MB | `stream.pipeline` 🚨 | 必须用流式,否则 OOM | + +**关键**:在 Fastify 层严格限制文件大小 + +```typescript +// server.ts +fastify.register(multipart, { + limits: { + fileSize: 30 * 1024 * 1024 // 30MB 硬限制 + } +}); +``` + +--- + +## 4. 实施计划 + +### 时间预估总览 + +| Phase | 内容 | 预计时间 | +|-------|------|---------| +| Phase 1 | 基础设施(OSSAdapter 实现) | 2 小时 | +| Phase 2 | 业务模块集成(PKB/Tool C/RVW/ASL) | 3 小时 | +| Phase 2.5 | 前端消费链路 | 1 小时 | +| Phase 3 | 环境配置 | 30 分钟 | +| **总计** | | **6.5 小时** | + +--- + +### Phase 1:基础设施(预计 2 小时) + +#### 1.1 安装依赖 + +```bash +cd backend +npm install ali-oss +npm install -D @types/ali-oss +``` + +#### 1.2 完善 StorageAdapter 接口 + +```typescript +// common/storage/StorageAdapter.ts +export interface StorageAdapter { + // 现有方法 + upload(key: string, buffer: Buffer): Promise + download(key: string): Promise + delete(key: string): Promise + getUrl(key: string): string + exists(key: string): Promise + + // 新增方法 + getSignedUrl(key: string, expires?: number): string // 签名 URL(私有文件访问) +} +``` + +#### 1.3 实现 OSSAdapter + +```typescript +// common/storage/OSSAdapter.ts +import OSS from 'ali-oss'; +import { StorageAdapter } from './StorageAdapter.js'; + +export class OSSAdapter implements StorageAdapter { + private readonly client: OSS; + + constructor(config: { + region: string; + bucket: string; + accessKeyId: string; + accessKeySecret: string; + internal?: boolean; + }) { + this.client = new OSS({ + region: config.region, + bucket: config.bucket, + accessKeyId: config.accessKeyId, + accessKeySecret: config.accessKeySecret, + internal: config.internal ?? false, + }); + } + + async upload(key: string, buffer: Buffer): Promise { + const result = await this.client.put(key, buffer); + return result.url; + } + + async download(key: string): Promise { + const result = await this.client.get(key); + return result.content as Buffer; + } + + async delete(key: string): Promise { + await this.client.delete(key); + } + + getUrl(key: string): string { + return this.client.generateObjectUrl(key); + } + + getSignedUrl(key: string, expires: number = 900): string { + // 默认 15 分钟过期(医疗数据安全考虑) + return this.client.signatureUrl(key, { expires }); + } + + async exists(key: string): Promise { + try { + await this.client.head(key); + return true; + } catch (error: any) { + if (error.code === 'NoSuchKey') { + return false; + } + throw error; + } + } +} +``` + +#### 1.4 更新 LocalAdapter(添加 getSignedUrl) + +```typescript +// common/storage/LocalAdapter.ts +getSignedUrl(key: string, expires?: number): string { + // 本地开发环境直接返回 URL(无需签名) + return this.getUrl(key); +} +``` + +#### 1.5 更新 StorageFactory + +```typescript +// common/storage/StorageFactory.ts +private static createOSSAdapter(): OSSAdapter { + const region = process.env.OSS_REGION; + const bucket = process.env.OSS_BUCKET; + const accessKeyId = process.env.OSS_ACCESS_KEY_ID; + const accessKeySecret = process.env.OSS_ACCESS_KEY_SECRET; + const internal = process.env.OSS_INTERNAL === 'true'; + + if (!region || !bucket || !accessKeyId || !accessKeySecret) { + throw new Error('[StorageFactory] OSS configuration incomplete'); + } + + return new OSSAdapter({ + region, + bucket, + accessKeyId, + accessKeySecret, + internal, + }); +} +``` + +### Phase 2:业务模块文件持久化(预计 3 小时) + +覆盖 **PKB**、**Tool C**、**RVW**、**ASL** 四个核心模块。 + +#### 2.1 PKB 文档上传 + +```typescript +// modules/pkb/controllers/documentController.ts +import { storage } from '../../../common/storage/index.js'; +import { v4 as uuid } from 'uuid'; + +export async function uploadDocument(request, reply) { + const { kbId } = request.params; + const data = await request.file(); + const buffer = await data.toBuffer(); // 文件 < 30MB,可接受 + + const userId = getUserId(request); + const tenantId = request.user.tenantId || 'default'; + + // 生成存储 Key(用户私有数据) + const fileId = uuid(); + const ext = path.extname(data.filename); + const storageKey = `tenants/${tenantId}/users/${userId}/pkb/${fileId}${ext}`; + + // 1. 先存储到 OSS/本地 + const fileUrl = await storage.upload(storageKey, buffer); + + // 2. 再调用文档服务处理 + const document = await documentService.uploadDocument( + userId, kbId, buffer, data.filename, data.mimetype, + buffer.length, fileUrl, storageKey + ); + + return reply.status(201).send({ success: true, data: document }); +} +``` + +#### 2.2 Tool C 数据清洗(临时文件) + +```typescript +// modules/dc/tool-c/services/SessionService.ts +// 当前已使用 storage,需确认 Key 格式符合规范 + +// 上传时使用临时目录(1天自动删除) +const storageKey = `temp/${formatDate(new Date())}/${uuid()}.xlsx`; +await storage.upload(storageKey, fileBuffer); + +// 清洗结果也存临时目录 +const cleanDataKey = `temp/${formatDate(new Date())}/${uuid()}_clean.json`; +await storage.upload(cleanDataKey, cleanDataBuffer); +``` + +#### 2.3 RVW 审稿报告(持久存储) + +```typescript +// modules/rvw/services/reviewService.ts +// 审稿报告需要持久存储 + +// 原始稿件 +const sourceKey = `tenants/${tenantId}/users/${userId}/rvw/${taskId}/source${ext}`; +await storage.upload(sourceKey, sourceBuffer); + +// 审稿报告(生成的 Word) +const reportKey = `tenants/${tenantId}/users/${userId}/rvw/${taskId}/report.docx`; +await storage.upload(reportKey, reportBuffer); +``` + +#### 2.4 ASL 文献导入 + +```typescript +// modules/asl/services/importService.ts + +// Excel 导入文件(临时,1天删除) +const tempKey = `temp/${formatDate(new Date())}/${uuid()}.xlsx`; +await storage.upload(tempKey, excelBuffer); + +// PDF 文献文件(持久存储) +const pdfKey = `tenants/${tenantId}/users/${userId}/asl/${projectId}/${uuid()}.pdf`; +await storage.upload(pdfKey, pdfBuffer); +``` + +#### 2.5 更新数据库 Schema + +```prisma +// schema.prisma + +// PKB 文档 +model Document { + // ... 现有字段 + storageKey String? // 新增:OSS 存储路径 +} + +// RVW 审稿任务 +model ReviewTask { + // ... 现有字段 + sourceStorageKey String? // 原始稿件路径 + reportStorageKey String? // 审稿报告路径 +} + +// ASL 文献 +model ASLDocument { + // ... 现有字段 + pdfStorageKey String? // PDF 文件路径 +} +``` + +--- + +### Phase 2.5:前端文件消费链路(预计 1 小时) + +#### 2.5.1 统一文件下载 API + +```typescript +// 后端:通用文件下载接口 +// GET /api/v1/storage/signed-url?key={storageKey} + +export async function getSignedUrl(request, reply) { + const { key } = request.query; + const userId = getUserId(request); + + // 权限校验:确保用户有权访问该文件 + const hasAccess = await checkFileAccess(userId, key); + if (!hasAccess) { + return reply.status(403).send({ success: false, message: 'Access denied' }); + } + + // 根据文件类型设置不同过期时间 + const ext = path.extname(key).toLowerCase(); + const expires = getExpiresForFileType(ext); + + const signedUrl = storage.getSignedUrl(key, expires); + + return reply.send({ + success: true, + data: { + url: signedUrl, + filename: path.basename(key), + expiresIn: expires, + contentType: getContentType(ext) + } + }); +} + +// 过期时间策略 +function getExpiresForFileType(ext: string): number { + switch (ext) { + case '.pdf': + return 3600; // PDF 预览:1小时 + case '.docx': + case '.xlsx': + return 300; // Office 下载:5分钟 + default: + return 900; // 默认:15分钟 + } +} +``` + +#### 2.5.2 前端消费策略 + +| 文件类型 | 消费方式 | 实现方法 | +|----------|---------|---------| +| **PDF** | 内嵌预览 | `