feat(storage): integrate Alibaba Cloud OSS for file persistence - Add OSSAdapter and LocalAdapter with StorageFactory pattern - Integrate PKB module with OSS upload - Rename difyDocumentId to storageKey - Create 4 OSS buckets and development specification

This commit is contained in:
2026-01-22 22:02:20 +08:00
parent 483c62fb6f
commit 9c96f75c52
309 changed files with 4583 additions and 172 deletions

View File

@@ -53,6 +53,9 @@ Status: Day 1 complete (11/11 tasks), ready for Day 2

View File

@@ -283,6 +283,9 @@

View File

@@ -229,6 +229,9 @@ https://iit.xunzhengyixue.com/api/v1/iit/health

4
backend/.gitignore vendored
View File

@@ -1,5 +1,3 @@
node_modules
node_modules
# Keep environment variables out of version control
.env
/src/generated/prisma

View File

@@ -158,6 +158,9 @@ https://iit.xunzhengyixue.com/api/v1/iit/health

View File

@@ -59,6 +59,9 @@

View File

@@ -319,6 +319,9 @@ npx tsx src/modules/iit-manager/test-patient-wechat-url-verify.ts

View File

@@ -181,6 +181,9 @@ npm run dev

View File

@@ -62,3 +62,6 @@ main()

View File

@@ -56,3 +56,6 @@ main()

View File

@@ -51,3 +51,6 @@ main()

View File

@@ -83,3 +83,6 @@ main()

View File

@@ -46,3 +46,6 @@ main()

View File

@@ -87,3 +87,6 @@ main()

View File

@@ -34,3 +34,6 @@ main()

View File

@@ -122,3 +122,6 @@ main()

View File

@@ -93,3 +93,6 @@ main()

View File

@@ -79,3 +79,6 @@ main()

View File

@@ -121,3 +121,6 @@ main()

View File

@@ -32,3 +32,6 @@ ON CONFLICT (id) DO NOTHING;

View File

@@ -64,3 +64,6 @@ ON CONFLICT (id) DO NOTHING;

79
backend/env.example.md Normal file
View File

@@ -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
```

View File

@@ -78,6 +78,9 @@ WHERE table_schema = 'dc_schema'

View File

@@ -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",

View File

@@ -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",

View File

@@ -116,6 +116,9 @@ ORDER BY ordinal_position;

View File

@@ -129,6 +129,9 @@ runMigration()

View File

@@ -63,6 +63,9 @@ COMMENT ON COLUMN "dc_schema"."dc_tool_c_sessions"."column_mapping" IS '列名

View File

@@ -90,6 +90,9 @@ COMMENT ON COLUMN dc_schema.dc_tool_c_sessions.expires_at IS '过期时间(创

View File

@@ -62,3 +62,6 @@ USING gin (metadata jsonb_path_ops);
-- SELECT indexname, indexdef FROM pg_indexes WHERE schemaname = 'ekb_schema';

View File

@@ -29,3 +29,6 @@ ON "ekb_schema"."ekb_document"
USING gin (tags);

View File

@@ -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")

View File

@@ -130,6 +130,9 @@ Write-Host ""

View File

@@ -240,6 +240,9 @@ function extractCodeBlocks(obj, blocks = []) {

View File

@@ -41,3 +41,6 @@ CREATE TABLE IF NOT EXISTS platform_schema.job_common (

View File

@@ -115,3 +115,6 @@ CREATE OR REPLACE FUNCTION platform_schema.delete_queue(queue_name text) RETURNS

View File

@@ -259,6 +259,9 @@ checkDCTables();

View File

@@ -16,3 +16,6 @@ CREATE SCHEMA IF NOT EXISTS capability_schema;

View File

@@ -211,6 +211,9 @@ createAiHistoryTable()

View File

@@ -198,6 +198,9 @@ createToolCTable()

View File

@@ -195,6 +195,9 @@ createToolCTable()

View File

@@ -319,3 +319,6 @@ main()

View File

@@ -126,3 +126,6 @@ main()

163
backend/scripts/test-oss.ts Normal file
View File

@@ -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)

View File

@@ -342,6 +342,9 @@ runTests().catch(error => {

View File

@@ -92,3 +92,6 @@ testAPI().catch(console.error);

View File

@@ -122,3 +122,6 @@ testDeepSearch().catch(console.error);

View File

@@ -307,6 +307,9 @@ verifySchemas()

View File

@@ -199,3 +199,6 @@ export const jwtService = new JWTService();

View File

@@ -327,6 +327,9 @@ export function getBatchItems<T>(

View File

@@ -82,3 +82,6 @@ export interface VariableValidation {

View File

@@ -352,3 +352,6 @@ export function chunkMarkdown(markdown: string, config?: ChunkConfig): TextChunk
export default ChunkService;

View File

@@ -48,3 +48,6 @@ class DeprecatedDifyClient {
export const difyClient = new DeprecatedDifyClient();
export const DifyClient = DeprecatedDifyClient;

View File

@@ -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
* - 内网EndpointSAE部署零流量费
* - 静态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
/** 是否使用内网EndpointSAE部署必须为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.comSAE零流量费
// 公网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<string> {
// ⚠️ TODO: 待实现
// const result = await this.client.put(key, buffer)
// return result.url
async upload(key: string, buffer: Buffer): Promise<string> {
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<string> {
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<Buffer> {
// ⚠️ 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<Buffer> {
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<void> {
// ⚠️ 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<void> {
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<boolean> {
// ⚠️ TODO: 待实现
// try {
// await this.client.head(key)
// return true
// } catch (error) {
// return false
// }
async exists(key: string): Promise<boolean> {
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<void> {
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<void> {
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. 测试:
* - 上传小文件
* - 下载文件
* - 删除文件
* - 检查文件是否存在
*/

View File

@@ -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
}
}

View File

@@ -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()

View File

@@ -203,3 +203,6 @@ export function createOpenAIStreamAdapter(

View File

@@ -209,3 +209,6 @@ export async function streamChat(

View File

@@ -27,3 +27,6 @@ export { THINKING_TAGS } from './types';

View File

@@ -102,3 +102,6 @@ export type SSEEventType =

View File

@@ -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内网EndpointSAE部署必须为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配置

View File

@@ -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);
// 过滤结果

View File

@@ -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);
});

View File

@@ -88,3 +88,6 @@ export async function moduleRoutes(fastify: FastifyInstance) {

View File

@@ -118,3 +118,6 @@ export interface PaginatedResponse<T> {

View File

@@ -165,3 +165,6 @@ export const ROLE_DISPLAY_NAMES: Record<UserRole, string> = {

View File

@@ -240,3 +240,6 @@ async function matchIntent(query: string): Promise<{

View File

@@ -94,3 +94,6 @@ export async function uploadAttachment(

View File

@@ -23,3 +23,6 @@ export { aiaRoutes };

View File

@@ -363,6 +363,9 @@ runTests().catch((error) => {

View File

@@ -342,6 +342,9 @@ Content-Type: application/json

View File

@@ -278,6 +278,9 @@ export const conflictDetectionService = new ConflictDetectionService();

View File

@@ -228,6 +228,9 @@ curl -X POST http://localhost:3000/api/v1/dc/tool-c/test/execute \

View File

@@ -282,6 +282,9 @@ export const streamAIController = new StreamAIController();

View File

@@ -191,6 +191,9 @@ logger.info('[SessionMemory] 会话记忆管理器已启动', {

View File

@@ -125,6 +125,9 @@ checkTableStructure();

View File

@@ -112,6 +112,9 @@ checkProjectConfig().catch(console.error);

View File

@@ -94,6 +94,9 @@ main();

View File

@@ -551,6 +551,9 @@ URL: https://iit.xunzhengyixue.com/api/v1/iit/patient-wechat/callback

View File

@@ -186,6 +186,9 @@ console.log('');

View File

@@ -503,6 +503,9 @@ export const patientWechatService = new PatientWechatService();

View File

@@ -148,6 +148,9 @@ testDifyIntegration().catch(error => {

View File

@@ -177,6 +177,9 @@ testIitDatabase()

View File

@@ -163,6 +163,9 @@ if (hasError) {

View File

@@ -189,6 +189,9 @@ async function testUrlVerification() {

View File

@@ -270,6 +270,9 @@ main().catch((error) => {

View File

@@ -154,6 +154,9 @@ Write-Host ""

View File

@@ -247,6 +247,9 @@ export interface CachedProtocolRules {

View File

@@ -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}`);

View File

@@ -60,6 +60,9 @@ export default async function healthRoutes(fastify: FastifyInstance) {

View File

@@ -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,
},

View File

@@ -140,5 +140,8 @@ Content-Type: application/json

View File

@@ -125,5 +125,8 @@ Write-Host " - 删除任务: DELETE $BaseUrl/api/v1/rvw/tasks/{taskId}" -Foregr

View File

@@ -39,5 +39,8 @@ export * from './services/utils.js';

View File

@@ -130,5 +130,8 @@ export function validateAgentSelection(agents: string[]): void {

View File

@@ -428,6 +428,9 @@ SET session_replication_role = 'origin';

View File

@@ -110,3 +110,6 @@ async function testCrossLanguageSearch() {
testCrossLanguageSearch();

View File

@@ -172,3 +172,6 @@ async function testQueryRewrite() {
testQueryRewrite();

View File

@@ -118,3 +118,6 @@ async function testRerank() {
testRerank();

Some files were not shown because too many files have changed in this diff Show More