Compare commits
2 Commits
277c375f82
...
6d7cbffc3b
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d7cbffc3b | |||
| 08f31dd5b6 |
@ -26,3 +26,9 @@ SWAGGER_PASSWORD=change_me
|
||||
|
||||
STORAGE_DRIVER=local
|
||||
STORAGE_LOCAL_PATH=./uploads
|
||||
|
||||
STORAGE_COS_SECRET_ID=your-cos-secret-id
|
||||
STORAGE_COS_SECRET_KEY=your-cos-secret-key
|
||||
STORAGE_COS_BUCKET=your-bucket-name-appid
|
||||
STORAGE_COS_REGION=ap-guangzhou
|
||||
STORAGE_COS_DOMAIN=your-custom-domain.example.com
|
||||
|
||||
@ -32,20 +32,7 @@ jobs:
|
||||
--network zhixi-net \
|
||||
--restart unless-stopped \
|
||||
-p 3001:3000 \
|
||||
-e NODE_ENV=production \
|
||||
-e PORT=3000 \
|
||||
-e DATABASE_URL='mysql://zhixi_user:Zhixi@2026!App@mysql-zhixi:3306/zhixi' \
|
||||
-e REDIS_HOST=redis-zhixi \
|
||||
-e REDIS_PORT=6379 \
|
||||
-e REDIS_PASSWORD='Rds@nTsgKrcqAkbuf6PwJIFMZQzF' \
|
||||
-e JWT_SECRET=98b1e7e377a40021ad7c46c55e467d2a218a89db7afc7c912780152ad64bdc45 \
|
||||
-e AI_PROVIDER=mock \
|
||||
-e APPLE_BUNDLE_ID=cloud.longde.AIStudyApp \
|
||||
-e APPLE_ISSUER=https://appleid.apple.com \
|
||||
-e APPLE_JWKS_URL=https://appleid.apple.com/auth/keys \
|
||||
-e ENABLE_SWAGGER=true \
|
||||
-e SWAGGER_USER=admin \
|
||||
-e SWAGGER_PASSWORD='Swgr@fmDentAYVXQUpG6oZDpJ' \
|
||||
--env-file /etc/zhixi/.env.production \
|
||||
zhixi-api:latest
|
||||
|
||||
- name: Health check
|
||||
|
||||
692
package-lock.json
generated
692
package-lock.json
generated
@ -23,6 +23,7 @@
|
||||
"bcryptjs": "^3.0.3",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.15.1",
|
||||
"cos-nodejs-sdk-v5": "^2.15.4",
|
||||
"helmet": "^8.1.0",
|
||||
"ioredis": "^5.10.1",
|
||||
"jose": "^6.2.3",
|
||||
@ -4083,7 +4084,6 @@
|
||||
"version": "6.15.0",
|
||||
"resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.15.0.tgz",
|
||||
"integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
@ -4271,11 +4271,52 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/asn1": {
|
||||
"version": "0.2.6",
|
||||
"resolved": "https://registry.npmmirror.com/asn1/-/asn1-0.2.6.tgz",
|
||||
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": "~2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/assert-plus": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/assert-plus/-/assert-plus-1.0.0.tgz",
|
||||
"integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/atomically": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmmirror.com/atomically/-/atomically-1.7.0.tgz",
|
||||
"integrity": "sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-sign2": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmmirror.com/aws-sign2/-/aws-sign2-0.7.0.tgz",
|
||||
"integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/aws4": {
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmmirror.com/aws4/-/aws4-1.13.2.tgz",
|
||||
"integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/babel-jest": {
|
||||
@ -4418,6 +4459,15 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bcrypt-pbkdf": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
|
||||
"integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"tweetnacl": "^0.14.3"
|
||||
}
|
||||
},
|
||||
"node_modules/bcryptjs": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/bcryptjs/-/bcryptjs-3.0.3.tgz",
|
||||
@ -4690,6 +4740,12 @@
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/caseless": {
|
||||
"version": "0.12.0",
|
||||
"resolved": "https://registry.npmmirror.com/caseless/-/caseless-0.12.0.tgz",
|
||||
"integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz",
|
||||
@ -4938,7 +4994,6 @@
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
@ -5003,6 +5058,94 @@
|
||||
"typedarray": "^0.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/conf": {
|
||||
"version": "9.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/conf/-/conf-9.0.2.tgz",
|
||||
"integrity": "sha512-rLSiilO85qHgaTBIIHQpsv8z+NnVfZq3cKuYNCXN1AOqPzced0GWZEe/A517VldRLyQYXUMyV+vszavE2jSAqw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ajv": "^7.0.3",
|
||||
"ajv-formats": "^1.5.1",
|
||||
"atomically": "^1.7.0",
|
||||
"debounce-fn": "^4.0.0",
|
||||
"dot-prop": "^6.0.1",
|
||||
"env-paths": "^2.2.0",
|
||||
"json-schema-typed": "^7.0.3",
|
||||
"make-dir": "^3.1.0",
|
||||
"onetime": "^5.1.2",
|
||||
"pkg-up": "^3.1.0",
|
||||
"semver": "^7.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/conf/node_modules/ajv": {
|
||||
"version": "7.2.4",
|
||||
"resolved": "https://registry.npmmirror.com/ajv/-/ajv-7.2.4.tgz",
|
||||
"integrity": "sha512-nBeQgg/ZZA3u3SYxyaDvpvDtgZ/EZPF547ARgZBrG9Bhu1vKDwAIjtIf+sDtJUKa2zOcEbmRLBRSyMraS/Oy1A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"json-schema-traverse": "^1.0.0",
|
||||
"require-from-string": "^2.0.2",
|
||||
"uri-js": "^4.2.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/conf/node_modules/ajv-formats": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmmirror.com/ajv-formats/-/ajv-formats-1.6.1.tgz",
|
||||
"integrity": "sha512-4CjkH20If1lhR5CGtqkrVg3bbOtFEG80X9v6jDOIUhbzzbB+UzPBGy8GQhUNVZ0yvMHdMpawCOcy5ydGMsagGQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ajv": "^7.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ajv": "^7.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"ajv": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/conf/node_modules/json-schema-traverse": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/conf/node_modules/make-dir": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/make-dir/-/make-dir-3.1.0.tgz",
|
||||
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"semver": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/conf/node_modules/make-dir/node_modules/semver": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz",
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/consola": {
|
||||
"version": "3.4.2",
|
||||
"resolved": "https://registry.npmmirror.com/consola/-/consola-3.4.2.tgz",
|
||||
@ -5066,6 +5209,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/core-util-is": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.2.tgz",
|
||||
"integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cors": {
|
||||
"version": "2.8.6",
|
||||
"resolved": "https://registry.npmmirror.com/cors/-/cors-2.8.6.tgz",
|
||||
@ -5083,6 +5232,42 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/cos-nodejs-sdk-v5": {
|
||||
"version": "2.15.4",
|
||||
"resolved": "https://registry.npmmirror.com/cos-nodejs-sdk-v5/-/cos-nodejs-sdk-v5-2.15.4.tgz",
|
||||
"integrity": "sha512-TP/iYTvKKKhRK89on9SRfSMGEw/9SFAAU8EC1kdT5Fmpx7dAwaCNM2+R2H1TSYoQt+03rwOs8QEfNkX8GOHjHQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"conf": "^9.0.0",
|
||||
"fast-xml-parser": "4.2.5",
|
||||
"mime-types": "^2.1.24",
|
||||
"request": "^2.88.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/cos-nodejs-sdk-v5/node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cos-nodejs-sdk-v5/node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cosmiconfig": {
|
||||
"version": "8.3.6",
|
||||
"resolved": "https://registry.npmmirror.com/cosmiconfig/-/cosmiconfig-8.3.6.tgz",
|
||||
@ -5144,6 +5329,42 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/dashdash": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmmirror.com/dashdash/-/dashdash-1.14.1.tgz",
|
||||
"integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"assert-plus": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/debounce-fn": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/debounce-fn/-/debounce-fn-4.0.0.tgz",
|
||||
"integrity": "sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mimic-fn": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/debounce-fn/node_modules/mimic-fn": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/mimic-fn/-/mimic-fn-3.1.0.tgz",
|
||||
"integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
|
||||
@ -5210,7 +5431,6 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
@ -5275,6 +5495,21 @@
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/dot-prop": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/dot-prop/-/dot-prop-6.0.1.tgz",
|
||||
"integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-obj": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "17.4.1",
|
||||
"resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-17.4.1.tgz",
|
||||
@ -5335,6 +5570,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ecc-jsbn": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
|
||||
"integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jsbn": "~0.1.0",
|
||||
"safer-buffer": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ecdsa-sig-formatter": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmmirror.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||
@ -5400,6 +5645,15 @@
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/env-paths": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/env-paths/-/env-paths-2.2.1.tgz",
|
||||
"integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/error-ex": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmmirror.com/error-ex/-/error-ex-1.3.4.tgz",
|
||||
@ -5831,11 +6085,25 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/extend": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/extend/-/extend-3.0.2.tgz",
|
||||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/extsprintf": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/extsprintf/-/extsprintf-1.3.0.tgz",
|
||||
"integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==",
|
||||
"engines": [
|
||||
"node >=0.6.0"
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-diff": {
|
||||
@ -5849,7 +6117,6 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
||||
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-levenshtein": {
|
||||
@ -5882,6 +6149,28 @@
|
||||
],
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/fast-xml-parser": {
|
||||
"version": "4.2.5",
|
||||
"resolved": "https://registry.npmmirror.com/fast-xml-parser/-/fast-xml-parser-4.2.5.tgz",
|
||||
"integrity": "sha512-B9/wizE4WngqQftFPmdaMYlXoJlJOYxGQOanC77fq9k8+Z0v5dDSVh+3glErdIROP//s/jgb7ZuxKfB8nVyo0g==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "paypal",
|
||||
"url": "https://paypal.me/naturalintelligence"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"strnum": "^1.0.5"
|
||||
},
|
||||
"bin": {
|
||||
"fxparser": "src/cli/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/fb-watchman": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/fb-watchman/-/fb-watchman-2.0.2.tgz",
|
||||
@ -6030,6 +6319,15 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/forever-agent": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmmirror.com/forever-agent/-/forever-agent-0.6.1.tgz",
|
||||
"integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/fork-ts-checker-webpack-plugin": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-9.1.0.tgz",
|
||||
@ -6267,6 +6565,15 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/getpass": {
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmmirror.com/getpass/-/getpass-0.1.7.tgz",
|
||||
"integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"assert-plus": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "13.0.6",
|
||||
"resolved": "https://registry.npmmirror.com/glob/-/glob-13.0.6.tgz",
|
||||
@ -6408,6 +6715,29 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/har-schema": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/har-schema/-/har-schema-2.0.0.tgz",
|
||||
"integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/har-validator": {
|
||||
"version": "5.1.5",
|
||||
"resolved": "https://registry.npmmirror.com/har-validator/-/har-validator-5.1.5.tgz",
|
||||
"integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==",
|
||||
"deprecated": "this library is no longer supported",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ajv": "^6.12.3",
|
||||
"har-schema": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz",
|
||||
@ -6494,6 +6824,21 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/http-signature": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/http-signature/-/http-signature-1.2.0.tgz",
|
||||
"integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"assert-plus": "^1.0.0",
|
||||
"jsprim": "^1.2.2",
|
||||
"sshpk": "^1.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8",
|
||||
"npm": ">=1.3.7"
|
||||
}
|
||||
},
|
||||
"node_modules/human-signals": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/human-signals/-/human-signals-2.1.0.tgz",
|
||||
@ -6718,6 +7063,15 @@
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-obj": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/is-obj/-/is-obj-2.0.0.tgz",
|
||||
"integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-promise": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/is-promise/-/is-promise-4.0.0.tgz",
|
||||
@ -6737,6 +7091,12 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-typedarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/is-typedarray/-/is-typedarray-1.0.0.tgz",
|
||||
"integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-unicode-supported": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
|
||||
@ -6757,6 +7117,12 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"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/istanbul-lib-coverage": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmmirror.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
|
||||
@ -7627,6 +7993,12 @@
|
||||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/jsbn": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/jsbn/-/jsbn-0.1.1.tgz",
|
||||
"integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jsesc": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz",
|
||||
@ -7654,13 +8026,24 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-schema": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/json-schema/-/json-schema-0.4.0.tgz",
|
||||
"integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
|
||||
"license": "(AFL-2.1 OR BSD-3-Clause)"
|
||||
},
|
||||
"node_modules/json-schema-traverse": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-schema-typed": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/json-schema-typed/-/json-schema-typed-7.0.3.tgz",
|
||||
"integrity": "sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/json-stable-stringify-without-jsonify": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
|
||||
@ -7668,6 +8051,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/json-stringify-safe": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
|
||||
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/json5": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz",
|
||||
@ -7723,6 +8112,21 @@
|
||||
"npm": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/jsprim": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmmirror.com/jsprim/-/jsprim-1.4.2.tgz",
|
||||
"integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"assert-plus": "1.0.0",
|
||||
"extsprintf": "1.3.0",
|
||||
"json-schema": "0.4.0",
|
||||
"verror": "1.10.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jwa": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/jwa/-/jwa-2.0.1.tgz",
|
||||
@ -8122,7 +8526,6 @@
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/mimic-fn/-/mimic-fn-2.1.0.tgz",
|
||||
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@ -8377,6 +8780,15 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/oauth-sign": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmmirror.com/oauth-sign/-/oauth-sign-0.9.0.tgz",
|
||||
"integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz",
|
||||
@ -8423,7 +8835,6 @@
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/onetime/-/onetime-5.1.2.tgz",
|
||||
"integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mimic-fn": "^2.1.0"
|
||||
@ -8513,7 +8924,6 @@
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz",
|
||||
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@ -8686,6 +9096,12 @@
|
||||
"resolved": "https://registry.npmmirror.com/pause/-/pause-0.0.1.tgz",
|
||||
"integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
|
||||
},
|
||||
"node_modules/performance-now": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/performance-now/-/performance-now-2.1.0.tgz",
|
||||
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@ -8785,6 +9201,79 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/pkg-up": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/pkg-up/-/pkg-up-3.1.0.tgz",
|
||||
"integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"find-up": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/pkg-up/node_modules/find-up": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/find-up/-/find-up-3.0.0.tgz",
|
||||
"integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"locate-path": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/pkg-up/node_modules/locate-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-3.0.0.tgz",
|
||||
"integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-locate": "^3.0.0",
|
||||
"path-exists": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/pkg-up/node_modules/p-limit": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz",
|
||||
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-try": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/pkg-up/node_modules/p-locate": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-3.0.0.tgz",
|
||||
"integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-limit": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/pkg-up/node_modules/path-exists": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-3.0.0.tgz",
|
||||
"integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/pluralize": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/pluralize/-/pluralize-8.0.0.tgz",
|
||||
@ -8898,11 +9387,22 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/psl": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmmirror.com/psl/-/psl-1.15.0.tgz",
|
||||
"integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"punycode": "^2.3.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/lupomontero"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz",
|
||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@ -9045,6 +9545,82 @@
|
||||
"license": "Apache-2.0",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/request": {
|
||||
"version": "2.88.2",
|
||||
"resolved": "https://registry.npmmirror.com/request/-/request-2.88.2.tgz",
|
||||
"integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==",
|
||||
"deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"aws-sign2": "~0.7.0",
|
||||
"aws4": "^1.8.0",
|
||||
"caseless": "~0.12.0",
|
||||
"combined-stream": "~1.0.6",
|
||||
"extend": "~3.0.2",
|
||||
"forever-agent": "~0.6.1",
|
||||
"form-data": "~2.3.2",
|
||||
"har-validator": "~5.1.3",
|
||||
"http-signature": "~1.2.0",
|
||||
"is-typedarray": "~1.0.0",
|
||||
"isstream": "~0.1.2",
|
||||
"json-stringify-safe": "~5.0.1",
|
||||
"mime-types": "~2.1.19",
|
||||
"oauth-sign": "~0.9.0",
|
||||
"performance-now": "^2.1.0",
|
||||
"qs": "~6.5.2",
|
||||
"safe-buffer": "^5.1.2",
|
||||
"tough-cookie": "~2.5.0",
|
||||
"tunnel-agent": "^0.6.0",
|
||||
"uuid": "^3.3.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/request/node_modules/form-data": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmmirror.com/form-data/-/form-data-2.3.3.tgz",
|
||||
"integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.6",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.12"
|
||||
}
|
||||
},
|
||||
"node_modules/request/node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/request/node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/request/node_modules/qs": {
|
||||
"version": "6.5.5",
|
||||
"resolved": "https://registry.npmmirror.com/qs/-/qs-6.5.5.tgz",
|
||||
"integrity": "sha512-mzR4sElr1bfCaPJe7m8ilJ6ZXdDaGoObcYR0ZHSsktM/Lt21MVHj5De30GQH2eiZ1qGRTO7LCAzQsUeXTNexWQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz",
|
||||
@ -9059,7 +9635,6 @@
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@ -9409,6 +9984,31 @@
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/sshpk": {
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmmirror.com/sshpk/-/sshpk-1.18.0.tgz",
|
||||
"integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asn1": "~0.2.3",
|
||||
"assert-plus": "^1.0.0",
|
||||
"bcrypt-pbkdf": "^1.0.0",
|
||||
"dashdash": "^1.12.0",
|
||||
"ecc-jsbn": "~0.1.1",
|
||||
"getpass": "^0.1.1",
|
||||
"jsbn": "~0.1.0",
|
||||
"safer-buffer": "^2.0.2",
|
||||
"tweetnacl": "~0.14.0"
|
||||
},
|
||||
"bin": {
|
||||
"sshpk-conv": "bin/sshpk-conv",
|
||||
"sshpk-sign": "bin/sshpk-sign",
|
||||
"sshpk-verify": "bin/sshpk-verify"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stack-utils": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmmirror.com/stack-utils/-/stack-utils-2.0.6.tgz",
|
||||
@ -9569,6 +10169,18 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/strnum": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/strnum/-/strnum-1.1.2.tgz",
|
||||
"integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/strtok3": {
|
||||
"version": "10.3.5",
|
||||
"resolved": "https://registry.npmmirror.com/strtok3/-/strtok3-10.3.5.tgz",
|
||||
@ -9993,6 +10605,19 @@
|
||||
"url": "https://github.com/sponsors/Borewit"
|
||||
}
|
||||
},
|
||||
"node_modules/tough-cookie": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmmirror.com/tough-cookie/-/tough-cookie-2.5.0.tgz",
|
||||
"integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"psl": "^1.1.28",
|
||||
"punycode": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-api-utils": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
|
||||
@ -10185,6 +10810,24 @@
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tunnel-agent": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmmirror.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/tweetnacl": {
|
||||
"version": "0.14.5",
|
||||
"resolved": "https://registry.npmmirror.com/tweetnacl/-/tweetnacl-0.14.5.tgz",
|
||||
"integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==",
|
||||
"license": "Unlicense"
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz",
|
||||
@ -10413,7 +11056,6 @@
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz",
|
||||
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"punycode": "^2.1.0"
|
||||
@ -10434,6 +11076,16 @@
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/uuid/-/uuid-3.4.0.tgz",
|
||||
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
|
||||
"deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/v8-compile-cache-lib": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||
@ -10474,6 +11126,20 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/verror": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmmirror.com/verror/-/verror-1.10.0.tgz",
|
||||
"integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==",
|
||||
"engines": [
|
||||
"node >=0.6.0"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"assert-plus": "^1.0.0",
|
||||
"core-util-is": "1.0.2",
|
||||
"extsprintf": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/walker": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmmirror.com/walker/-/walker-1.0.8.tgz",
|
||||
|
||||
@ -34,6 +34,7 @@
|
||||
"bcryptjs": "^3.0.3",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.15.1",
|
||||
"cos-nodejs-sdk-v5": "^2.15.4",
|
||||
"helmet": "^8.1.0",
|
||||
"ioredis": "^5.10.1",
|
||||
"jose": "^6.2.3",
|
||||
|
||||
@ -227,6 +227,8 @@ model UploadedFile {
|
||||
filename String @db.VarChar(255)
|
||||
mimeType String? @db.VarChar(100)
|
||||
storagePath String @db.VarChar(500)
|
||||
objectKey String? @db.VarChar(500)
|
||||
bucket String? @db.VarChar(100)
|
||||
sizeBytes BigInt @default(0)
|
||||
checksum String? @db.VarChar(255)
|
||||
createdAt DateTime @default(now())
|
||||
@ -234,6 +236,7 @@ model UploadedFile {
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
|
||||
@@index([userId])
|
||||
@@index([objectKey])
|
||||
}
|
||||
|
||||
model DocumentImport {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { APP_FILTER, APP_GUARD, APP_PIPE } from '@nestjs/core';
|
||||
import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
|
||||
import { PrismaModule } from './infrastructure/database/prisma.module';
|
||||
@ -24,11 +24,19 @@ import { FocusItemsModule } from './modules/focus-items/focus-items.module';
|
||||
import { LearningActivityModule } from './modules/learning-activity/learning-activity.module';
|
||||
import { NotificationsModule } from './modules/notifications/notifications.module';
|
||||
import { FeedbackModule } from './modules/feedback/feedback.module';
|
||||
import { FilesModule } from './modules/files/files.module';
|
||||
import { WaitlistModule } from './modules/waitlist/waitlist.module';
|
||||
|
||||
import { JwtAuthGuard } from './common/guards/jwt-auth.guard';
|
||||
import { RolesGuard } from './common/guards/roles.guard';
|
||||
import { GlobalExceptionFilter } from './common/filters/global-exception.filter';
|
||||
import { StrictValidationPipe } from './common/pipes/strict-validation.pipe';
|
||||
import { RateLimitService } from './common/utils/rate-limit.service';
|
||||
import { ResponseInterceptor } from './common/interceptors/response.interceptor';
|
||||
|
||||
import { AiAnalysisWorker } from './workers/ai-analysis.worker';
|
||||
import { DocumentImportWorker } from './workers/document-import.worker';
|
||||
import { NotificationWorker } from './workers/notification.worker';
|
||||
|
||||
import appConfig from './config/app.config';
|
||||
import databaseConfig from './config/database.config';
|
||||
@ -81,12 +89,19 @@ import appleConfig from './config/apple.config';
|
||||
LearningActivityModule,
|
||||
NotificationsModule,
|
||||
FeedbackModule,
|
||||
FilesModule,
|
||||
WaitlistModule,
|
||||
],
|
||||
providers: [
|
||||
{ provide: APP_GUARD, useClass: JwtAuthGuard },
|
||||
{ provide: APP_GUARD, useClass: RolesGuard },
|
||||
{ provide: APP_FILTER, useClass: GlobalExceptionFilter },
|
||||
{ provide: APP_PIPE, useClass: StrictValidationPipe },
|
||||
{ provide: APP_INTERCEPTOR, useClass: ResponseInterceptor },
|
||||
RateLimitService,
|
||||
AiAnalysisWorker,
|
||||
DocumentImportWorker,
|
||||
NotificationWorker,
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
5
src/common/decorators/roles.decorator.ts
Normal file
5
src/common/decorators/roles.decorator.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
import { Role } from '../types/role.enum';
|
||||
|
||||
export const ROLES_KEY = 'roles';
|
||||
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);
|
||||
34
src/common/guards/roles.guard.ts
Normal file
34
src/common/guards/roles.guard.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { ROLES_KEY } from '../decorators/roles.decorator';
|
||||
import { Role, hasRole } from '../types/role.enum';
|
||||
|
||||
@Injectable()
|
||||
export class RolesGuard implements CanActivate {
|
||||
constructor(private readonly reflector: Reflector) {}
|
||||
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
|
||||
if (!requiredRoles || requiredRoles.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const user = request.user;
|
||||
|
||||
if (!user) {
|
||||
throw new ForbiddenException('请先登录');
|
||||
}
|
||||
|
||||
const hasRequiredRole = requiredRoles.some((role) => hasRole(user.role, role));
|
||||
if (!hasRequiredRole) {
|
||||
throw new ForbiddenException('权限不足');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
18
src/common/types/role.enum.ts
Normal file
18
src/common/types/role.enum.ts
Normal file
@ -0,0 +1,18 @@
|
||||
export enum Role {
|
||||
USER = 'USER',
|
||||
ADMIN = 'ADMIN',
|
||||
SUPER_ADMIN = 'SUPER_ADMIN',
|
||||
}
|
||||
|
||||
export const ROLE_HIERARCHY: Record<Role, Role[]> = {
|
||||
[Role.USER]: [Role.USER],
|
||||
[Role.ADMIN]: [Role.USER, Role.ADMIN],
|
||||
[Role.SUPER_ADMIN]: [Role.USER, Role.ADMIN, Role.SUPER_ADMIN],
|
||||
};
|
||||
|
||||
export function hasRole(userRole: string | undefined, required: Role): boolean {
|
||||
if (!userRole) return false;
|
||||
const resolved = ROLE_HIERARCHY[userRole as Role];
|
||||
if (!resolved) return false;
|
||||
return resolved.includes(required);
|
||||
}
|
||||
@ -3,6 +3,13 @@ import { registerAs } from '@nestjs/config';
|
||||
export default registerAs('storage', () => ({
|
||||
driver: process.env.STORAGE_DRIVER || 'local',
|
||||
localPath: process.env.STORAGE_LOCAL_PATH || './uploads',
|
||||
cos: {
|
||||
secretId: process.env.STORAGE_COS_SECRET_ID || '',
|
||||
secretKey: process.env.STORAGE_COS_SECRET_KEY || '',
|
||||
bucket: process.env.STORAGE_COS_BUCKET || 'zhixi-1259685406',
|
||||
region: process.env.STORAGE_COS_REGION || 'ap-guangzhou',
|
||||
domain: process.env.STORAGE_COS_DOMAIN || 'cos.longde.cloud',
|
||||
},
|
||||
s3: {
|
||||
bucket: process.env.STORAGE_S3_BUCKET,
|
||||
region: process.env.STORAGE_S3_REGION,
|
||||
|
||||
@ -1,9 +1,35 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { QueueService } from './queue.service';
|
||||
import { BullModule } from '@nestjs/bullmq';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { QueueService, QUEUE_AI_ANALYSIS, QUEUE_DOCUMENT_IMPORT, QUEUE_NOTIFICATION } from './queue.service';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [
|
||||
BullModule.forRootAsync({
|
||||
inject: [ConfigService],
|
||||
useFactory: (config: ConfigService) => {
|
||||
const url = config.get<string>('redis.url');
|
||||
if (url) {
|
||||
return { connection: { url } };
|
||||
}
|
||||
return {
|
||||
connection: {
|
||||
host: config.get<string>('redis.host', 'localhost'),
|
||||
port: config.get<number>('redis.port', 6379),
|
||||
password: config.get<string>('redis.password'),
|
||||
db: config.get<number>('redis.db', 0),
|
||||
},
|
||||
};
|
||||
},
|
||||
}),
|
||||
BullModule.registerQueue(
|
||||
{ name: QUEUE_AI_ANALYSIS },
|
||||
{ name: QUEUE_DOCUMENT_IMPORT },
|
||||
{ name: QUEUE_NOTIFICATION },
|
||||
),
|
||||
],
|
||||
providers: [QueueService],
|
||||
exports: [QueueService],
|
||||
exports: [QueueService, BullModule],
|
||||
})
|
||||
export class QueueModule {}
|
||||
|
||||
@ -1,23 +1,39 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectQueue } from '@nestjs/bullmq';
|
||||
import { Queue } from 'bullmq';
|
||||
|
||||
export const QUEUE_AI_ANALYSIS = 'ai-analysis';
|
||||
export const QUEUE_DOCUMENT_IMPORT = 'document-import';
|
||||
export const QUEUE_NOTIFICATION = 'notification';
|
||||
|
||||
@Injectable()
|
||||
export class QueueService {
|
||||
private queues: Map<string, any[]> = new Map();
|
||||
private readonly logger = new Logger(QueueService.name);
|
||||
|
||||
add(queueName: string, data: any) {
|
||||
if (!this.queues.has(queueName)) {
|
||||
this.queues.set(queueName, []);
|
||||
constructor(
|
||||
@InjectQueue(QUEUE_AI_ANALYSIS) private readonly aiQueue: Queue,
|
||||
@InjectQueue(QUEUE_DOCUMENT_IMPORT) private readonly importQueue: Queue,
|
||||
@InjectQueue(QUEUE_NOTIFICATION) private readonly notifyQueue: Queue,
|
||||
) {}
|
||||
|
||||
async add(queueName: string, data: any) {
|
||||
const queue = this.getQueue(queueName);
|
||||
const job = await queue.add(queueName, data);
|
||||
this.logger.log(`Job ${job.id} added to ${queueName}`);
|
||||
return job;
|
||||
}
|
||||
|
||||
async getJob(queueName: string, jobId: string) {
|
||||
const queue = this.getQueue(queueName);
|
||||
return queue.getJob(jobId);
|
||||
}
|
||||
|
||||
private getQueue(name: string): Queue {
|
||||
switch (name) {
|
||||
case QUEUE_AI_ANALYSIS: return this.aiQueue;
|
||||
case QUEUE_DOCUMENT_IMPORT: return this.importQueue;
|
||||
case QUEUE_NOTIFICATION: return this.notifyQueue;
|
||||
default: throw new Error(`Unknown queue: ${name}`);
|
||||
}
|
||||
this.queues.get(queueName)!.push(data);
|
||||
}
|
||||
|
||||
async processNext(queueName: string): Promise<any | null> {
|
||||
const queue = this.queues.get(queueName);
|
||||
if (!queue || queue.length === 0) return null;
|
||||
return queue.shift();
|
||||
}
|
||||
|
||||
getQueueNames(): string[] {
|
||||
return Array.from(this.queues.keys());
|
||||
}
|
||||
}
|
||||
|
||||
173
src/infrastructure/storage/cos-storage.provider.ts
Normal file
173
src/infrastructure/storage/cos-storage.provider.ts
Normal file
@ -0,0 +1,173 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import COS from 'cos-nodejs-sdk-v5';
|
||||
|
||||
export interface CosUploadUrlResult {
|
||||
uploadUrl: string;
|
||||
bucket: string;
|
||||
region: string;
|
||||
objectKey: string;
|
||||
expireSeconds: number;
|
||||
}
|
||||
|
||||
export interface CosObjectInfo {
|
||||
size: number;
|
||||
etag: string;
|
||||
contentType: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class CosStorageProvider {
|
||||
private readonly logger = new Logger(CosStorageProvider.name);
|
||||
private readonly cos: COS;
|
||||
private readonly bucket: string;
|
||||
private readonly region: string;
|
||||
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
const secretId = this.configService.get<string>('storage.cos.secretId', '');
|
||||
const secretKey = this.configService.get<string>('storage.cos.secretKey', '');
|
||||
const domain = this.configService.get<string>('storage.cos.domain', '');
|
||||
this.bucket = this.configService.get<string>('storage.cos.bucket', '');
|
||||
this.region = this.configService.get<string>('storage.cos.region', '');
|
||||
|
||||
this.cos = new COS({
|
||||
SecretId: secretId,
|
||||
SecretKey: secretKey,
|
||||
Domain: domain,
|
||||
Protocol: 'https:',
|
||||
});
|
||||
|
||||
this.logger.log(`COS provider initialized, bucket=${this.bucket}, region=${this.region}`);
|
||||
}
|
||||
|
||||
getBucket(): string {
|
||||
return this.bucket;
|
||||
}
|
||||
|
||||
getRegion(): string {
|
||||
return this.region;
|
||||
}
|
||||
|
||||
async generateUploadUrl(
|
||||
objectKey: string,
|
||||
expiresInSeconds = 3600,
|
||||
): Promise<CosUploadUrlResult> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.cos.getObjectUrl(
|
||||
{
|
||||
Bucket: this.bucket,
|
||||
Region: this.region,
|
||||
Key: objectKey,
|
||||
Method: 'PUT',
|
||||
Sign: true,
|
||||
Expires: expiresInSeconds,
|
||||
},
|
||||
(err, data) => {
|
||||
if (err) {
|
||||
this.logger.error(`Failed to generate upload URL for ${objectKey}`, err);
|
||||
return reject(err);
|
||||
}
|
||||
resolve({
|
||||
uploadUrl: data.Url,
|
||||
bucket: this.bucket,
|
||||
region: this.region,
|
||||
objectKey,
|
||||
expireSeconds: expiresInSeconds,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async generateDownloadUrl(
|
||||
objectKey: string,
|
||||
expiresInSeconds = 86400,
|
||||
): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.cos.getObjectUrl(
|
||||
{
|
||||
Bucket: this.bucket,
|
||||
Region: this.region,
|
||||
Key: objectKey,
|
||||
Method: 'GET',
|
||||
Sign: true,
|
||||
Expires: expiresInSeconds,
|
||||
},
|
||||
(err, data) => {
|
||||
if (err) {
|
||||
this.logger.error(`Failed to generate download URL for ${objectKey}`, err);
|
||||
return reject(err);
|
||||
}
|
||||
resolve(data.Url);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async headObject(objectKey: string): Promise<CosObjectInfo | null> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.cos.headObject(
|
||||
{
|
||||
Bucket: this.bucket,
|
||||
Region: this.region,
|
||||
Key: objectKey,
|
||||
},
|
||||
(err, data) => {
|
||||
if (err) {
|
||||
if (err.statusCode === 404) {
|
||||
return resolve(null);
|
||||
}
|
||||
this.logger.error(`headObject failed for ${objectKey}`, err);
|
||||
return reject(err);
|
||||
}
|
||||
resolve({
|
||||
size: parseInt(data.headers?.['content-length'] || '0', 10),
|
||||
etag: (data.ETag || '').replace(/"/g, ''),
|
||||
contentType: data.headers?.['content-type'] || 'application/octet-stream',
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async deleteObject(objectKey: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.cos.deleteObject(
|
||||
{
|
||||
Bucket: this.bucket,
|
||||
Region: this.region,
|
||||
Key: objectKey,
|
||||
},
|
||||
(err) => {
|
||||
if (err) {
|
||||
if (err.statusCode === 404) {
|
||||
this.logger.warn(`deleteObject: object not found ${objectKey}`);
|
||||
return resolve();
|
||||
}
|
||||
this.logger.error(`deleteObject failed for ${objectKey}`, err);
|
||||
return reject(err);
|
||||
}
|
||||
resolve();
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async healthCheck(): Promise<boolean> {
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
this.cos.headBucket(
|
||||
{ Bucket: this.bucket, Region: this.region },
|
||||
(err) => {
|
||||
if (err) return reject(err);
|
||||
resolve();
|
||||
},
|
||||
);
|
||||
});
|
||||
return true;
|
||||
} catch (err) {
|
||||
this.logger.warn('COS health check failed', err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,10 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { CosStorageProvider } from './cos-storage.provider';
|
||||
import { StorageService } from './storage.service';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [StorageService],
|
||||
exports: [StorageService],
|
||||
providers: [CosStorageProvider, StorageService],
|
||||
exports: [CosStorageProvider, StorageService],
|
||||
})
|
||||
export class StorageModule {}
|
||||
|
||||
@ -1,9 +1,39 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, BadRequestException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { CosStorageProvider } from './cos-storage.provider';
|
||||
import { RateLimitService } from '../../common/utils/rate-limit.service';
|
||||
import {
|
||||
sanitizeFilename,
|
||||
validateFileUpload,
|
||||
} from '../../common/utils/security.util';
|
||||
|
||||
export interface CreateUploadUrlInput {
|
||||
filename: string;
|
||||
mimeType: string;
|
||||
sizeBytes: number;
|
||||
}
|
||||
|
||||
export interface UploadUrlResult {
|
||||
uploadUrl: string;
|
||||
objectKey: string;
|
||||
bucket: string;
|
||||
region: string;
|
||||
expiresIn: number;
|
||||
}
|
||||
|
||||
export interface VerifiedUpload {
|
||||
size: number;
|
||||
etag: string;
|
||||
contentType: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class StorageService {
|
||||
constructor(private configService: ConfigService) {}
|
||||
constructor(
|
||||
private readonly cos: CosStorageProvider,
|
||||
private readonly configService: ConfigService,
|
||||
private readonly rateLimit: RateLimitService,
|
||||
) {}
|
||||
|
||||
getUploadPath(filename: string): string {
|
||||
const basePath = this.configService.get<string>(
|
||||
@ -13,7 +43,53 @@ export class StorageService {
|
||||
return `${basePath}/${filename}`;
|
||||
}
|
||||
|
||||
generateObjectKey(userId: string, originalFilename: string): string {
|
||||
const date = new Date();
|
||||
const yearMonth = `${date.getFullYear()}${String(date.getMonth() + 1).padStart(2, '0')}`;
|
||||
const safeName = sanitizeFilename(originalFilename);
|
||||
return `${userId}/${yearMonth}/${safeName}`;
|
||||
}
|
||||
|
||||
async createUploadUrl(
|
||||
userId: string,
|
||||
input: CreateUploadUrlInput,
|
||||
expiresIn = 3600,
|
||||
): Promise<UploadUrlResult> {
|
||||
validateFileUpload(input.mimeType, input.sizeBytes);
|
||||
await this.rateLimit.fileUploadLimit(userId);
|
||||
|
||||
const objectKey = this.generateObjectKey(userId, input.filename);
|
||||
const result = await this.cos.generateUploadUrl(objectKey, expiresIn);
|
||||
|
||||
return {
|
||||
uploadUrl: result.uploadUrl,
|
||||
objectKey: result.objectKey,
|
||||
bucket: result.bucket,
|
||||
region: result.region,
|
||||
expiresIn: expiresIn,
|
||||
};
|
||||
}
|
||||
|
||||
async verifyUpload(objectKey: string): Promise<VerifiedUpload> {
|
||||
const info = await this.cos.headObject(objectKey);
|
||||
if (!info) {
|
||||
throw new BadRequestException('文件未被上传到存储服务');
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
async getDownloadUrl(
|
||||
objectKey: string,
|
||||
expiresInSeconds?: number,
|
||||
): Promise<string> {
|
||||
return this.cos.generateDownloadUrl(objectKey, expiresInSeconds);
|
||||
}
|
||||
|
||||
async deleteObject(objectKey: string): Promise<void> {
|
||||
await this.cos.deleteObject(objectKey);
|
||||
}
|
||||
|
||||
async healthCheck(): Promise<boolean> {
|
||||
return true;
|
||||
return this.cos.healthCheck();
|
||||
}
|
||||
}
|
||||
|
||||
@ -44,6 +44,7 @@ async function bootstrap() {
|
||||
.addTag('learning-activity', '学习活跃')
|
||||
.addTag('notifications', '消息通知')
|
||||
.addTag('feedback', '用户反馈')
|
||||
.addTag('files', '文件管理')
|
||||
.addTag('waitlist', '等待名单')
|
||||
.build();
|
||||
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
|
||||
import { Controller, Get, Post, Body, Param, Query } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation } from '@nestjs/swagger';
|
||||
import { ActiveRecallService } from './active-recall.service';
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||
import { PaginationDto } from '../../common/dto/pagination.dto';
|
||||
import type { UserPayload } from '../../common/types';
|
||||
|
||||
@ApiTags('active-recall')
|
||||
@ -11,8 +12,8 @@ export class ActiveRecallController {
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: '获取主动回忆问题列表' })
|
||||
async findAll(@CurrentUser() user: UserPayload) {
|
||||
return this.service.findByUserId(String(user?.id || 'anonymous'));
|
||||
async findAll(@CurrentUser() user: UserPayload, @Query() pagination: PaginationDto) {
|
||||
return this.service.findByUserId(String(user?.id || 'anonymous'), pagination);
|
||||
}
|
||||
|
||||
@Post(':id/submit')
|
||||
|
||||
@ -5,10 +5,14 @@ import { PrismaService } from '../../infrastructure/database/prisma.service';
|
||||
export class ActiveRecallRepository {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async findByUserId(userId: string) {
|
||||
async findByUserId(userId: string, pagination?: { page?: number; limit?: number }) {
|
||||
const page = pagination?.page ?? 1;
|
||||
const limit = pagination?.limit ?? 20;
|
||||
return this.prisma.activeRecallQuestion.findMany({
|
||||
where: { userId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (page - 1) * limit,
|
||||
take: limit,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
|
||||
import { ActiveRecallRepository } from './active-recall.repository';
|
||||
import { ActiveRecallAnalysisWorkflow } from '../ai/workflows/active-recall-analysis.workflow';
|
||||
import type { PaginationDto } from '../../common/dto/pagination.dto';
|
||||
|
||||
@Injectable()
|
||||
export class ActiveRecallService {
|
||||
@ -11,8 +12,8 @@ export class ActiveRecallService {
|
||||
private readonly analysisWorkflow: ActiveRecallAnalysisWorkflow,
|
||||
) {}
|
||||
|
||||
async findByUserId(userId: string) {
|
||||
return this.repository.findByUserId(userId);
|
||||
async findByUserId(userId: string, pagination: PaginationDto) {
|
||||
return this.repository.findByUserId(userId, pagination);
|
||||
}
|
||||
|
||||
async submit(userId: string, questionId: string, body: { answerText: string }) {
|
||||
@ -21,17 +22,23 @@ export class ActiveRecallService {
|
||||
|
||||
const answer = await this.repository.createAnswer(userId, questionId, body);
|
||||
|
||||
this.analysisWorkflow.execute({
|
||||
userId,
|
||||
questionText: question.questionText,
|
||||
knowledgeItemContent: '',
|
||||
userAnswer: body.answerText,
|
||||
}).then((result) => {
|
||||
this.logger.log(`Analysis complete for answer ${answer.id}: score=${result.score}`);
|
||||
}).catch((err) => {
|
||||
this.logger.error(`Analysis failed for answer ${answer.id}: ${err.message}`);
|
||||
});
|
||||
// Fire-and-forget: answer is saved, analysis runs async
|
||||
void this.runAnalysis(answer.id, userId, question.questionText, body.answerText);
|
||||
|
||||
return answer;
|
||||
}
|
||||
|
||||
private async runAnalysis(answerId: string, userId: string, questionText: string, userAnswer: string) {
|
||||
try {
|
||||
const result = await this.analysisWorkflow.execute({
|
||||
userId,
|
||||
questionText,
|
||||
knowledgeItemContent: '',
|
||||
userAnswer,
|
||||
});
|
||||
this.logger.log(`Analysis complete for answer ${answerId}: score=${result.score}`);
|
||||
} catch (err: any) {
|
||||
this.logger.error(`Analysis failed for answer ${answerId}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,6 +8,6 @@ import { AiAnalysisRepository } from './ai-analysis.repository';
|
||||
imports: [AiModule],
|
||||
controllers: [AiAnalysisController],
|
||||
providers: [AiAnalysisService, AiAnalysisRepository],
|
||||
exports: [AiAnalysisService],
|
||||
exports: [AiAnalysisService, AiAnalysisRepository],
|
||||
})
|
||||
export class AiAnalysisModule {}
|
||||
|
||||
@ -6,6 +6,6 @@ import { DocumentImportRepository } from './document-import.repository';
|
||||
@Module({
|
||||
controllers: [DocumentImportController],
|
||||
providers: [DocumentImportService, DocumentImportRepository],
|
||||
exports: [DocumentImportService],
|
||||
exports: [DocumentImportService, DocumentImportRepository],
|
||||
})
|
||||
export class DocumentImportModule {}
|
||||
|
||||
13
src/modules/files/dto/complete-upload.dto.ts
Normal file
13
src/modules/files/dto/complete-upload.dto.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsString, IsOptional } from 'class-validator';
|
||||
|
||||
export class CompleteUploadDto {
|
||||
@ApiProperty({ description: 'COS 对象键(上传 URL 响应中返回)' })
|
||||
@IsString()
|
||||
objectKey: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '文件 SHA256 校验和' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
checksum?: string;
|
||||
}
|
||||
19
src/modules/files/dto/create-upload-url.dto.ts
Normal file
19
src/modules/files/dto/create-upload-url.dto.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsString, IsNumber, Min, Max } from 'class-validator';
|
||||
import { MAX_FILE_SIZE } from '../../../common/utils/security.util';
|
||||
|
||||
export class CreateUploadUrlDto {
|
||||
@ApiProperty({ description: '原始文件名', example: 'document.pdf' })
|
||||
@IsString()
|
||||
filename: string;
|
||||
|
||||
@ApiProperty({ description: 'MIME 类型', example: 'application/pdf' })
|
||||
@IsString()
|
||||
mimeType: string;
|
||||
|
||||
@ApiProperty({ description: '文件大小(字节)', example: 1048576 })
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
@Max(MAX_FILE_SIZE)
|
||||
sizeBytes: number;
|
||||
}
|
||||
2
src/modules/files/dto/index.ts
Normal file
2
src/modules/files/dto/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { CreateUploadUrlDto } from './create-upload-url.dto';
|
||||
export { CompleteUploadDto } from './complete-upload.dto';
|
||||
58
src/modules/files/files.controller.ts
Normal file
58
src/modules/files/files.controller.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth, ApiResponse } from '@nestjs/swagger';
|
||||
import { FilesService } from './files.service';
|
||||
import { CreateUploadUrlDto, CompleteUploadDto } from './dto';
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||
import type { UserPayload } from '../../common/types';
|
||||
|
||||
@ApiTags('files')
|
||||
@Controller('files')
|
||||
@ApiBearerAuth()
|
||||
export class FilesController {
|
||||
constructor(private readonly filesService: FilesService) {}
|
||||
|
||||
@Post('upload-url')
|
||||
@ApiOperation({ summary: '获取预签名上传 URL' })
|
||||
@ApiResponse({ status: 201, description: '返回预签名 URL,客户端直接 PUT 文件到 COS' })
|
||||
async createUploadUrl(
|
||||
@CurrentUser() user: UserPayload,
|
||||
@Body() dto: CreateUploadUrlDto,
|
||||
) {
|
||||
return this.filesService.requestUploadUrl(user.id, dto);
|
||||
}
|
||||
|
||||
@Post('complete')
|
||||
@ApiOperation({ summary: '确认上传完成' })
|
||||
@ApiResponse({ status: 201, description: '验证 COS 中文件存在并创建数据库记录' })
|
||||
async completeUpload(
|
||||
@CurrentUser() user: UserPayload,
|
||||
@Body() dto: CompleteUploadDto,
|
||||
) {
|
||||
return this.filesService.confirmUpload(user.id, dto);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: '获取文件信息及下载 URL' })
|
||||
async getFile(
|
||||
@CurrentUser() user: UserPayload,
|
||||
@Param('id') id: string,
|
||||
) {
|
||||
return this.filesService.getFile(user.id, id);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@ApiOperation({ summary: '删除文件(COS + 数据库)' })
|
||||
async deleteFile(
|
||||
@CurrentUser() user: UserPayload,
|
||||
@Param('id') id: string,
|
||||
) {
|
||||
return this.filesService.deleteFile(user.id, id);
|
||||
}
|
||||
}
|
||||
11
src/modules/files/files.module.ts
Normal file
11
src/modules/files/files.module.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { FilesController } from './files.controller';
|
||||
import { FilesService } from './files.service';
|
||||
import { FilesRepository } from './files.repository';
|
||||
|
||||
@Module({
|
||||
controllers: [FilesController],
|
||||
providers: [FilesService, FilesRepository],
|
||||
exports: [FilesService],
|
||||
})
|
||||
export class FilesModule {}
|
||||
39
src/modules/files/files.repository.ts
Normal file
39
src/modules/files/files.repository.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PrismaService } from '../../infrastructure/database/prisma.service';
|
||||
|
||||
@Injectable()
|
||||
export class FilesRepository {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async create(data: {
|
||||
userId: string;
|
||||
filename: string;
|
||||
mimeType?: string;
|
||||
storagePath: string;
|
||||
objectKey: string;
|
||||
bucket: string;
|
||||
sizeBytes: number;
|
||||
checksum?: string;
|
||||
}) {
|
||||
return this.prisma.uploadedFile.create({ data });
|
||||
}
|
||||
|
||||
async findById(id: string) {
|
||||
return this.prisma.uploadedFile.findUnique({ where: { id } });
|
||||
}
|
||||
|
||||
async findByObjectKey(objectKey: string) {
|
||||
return this.prisma.uploadedFile.findFirst({ where: { objectKey } });
|
||||
}
|
||||
|
||||
async findByUserId(userId: string) {
|
||||
return this.prisma.uploadedFile.findMany({
|
||||
where: { userId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
}
|
||||
|
||||
async delete(id: string) {
|
||||
return this.prisma.uploadedFile.delete({ where: { id } });
|
||||
}
|
||||
}
|
||||
74
src/modules/files/files.service.ts
Normal file
74
src/modules/files/files.service.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
ForbiddenException,
|
||||
} from '@nestjs/common';
|
||||
import { FilesRepository } from './files.repository';
|
||||
import { StorageService } from '../../infrastructure/storage/storage.service';
|
||||
import { CosStorageProvider } from '../../infrastructure/storage/cos-storage.provider';
|
||||
import { CreateUploadUrlDto, CompleteUploadDto } from './dto';
|
||||
|
||||
@Injectable()
|
||||
export class FilesService {
|
||||
constructor(
|
||||
private readonly repository: FilesRepository,
|
||||
private readonly storage: StorageService,
|
||||
private readonly cos: CosStorageProvider,
|
||||
) {}
|
||||
|
||||
async requestUploadUrl(userId: string, dto: CreateUploadUrlDto) {
|
||||
return this.storage.createUploadUrl(userId, {
|
||||
filename: dto.filename,
|
||||
mimeType: dto.mimeType,
|
||||
sizeBytes: dto.sizeBytes,
|
||||
});
|
||||
}
|
||||
|
||||
async confirmUpload(userId: string, dto: CompleteUploadDto) {
|
||||
const info = await this.storage.verifyUpload(dto.objectKey);
|
||||
|
||||
const parts = dto.objectKey.split('/');
|
||||
const originalFilename = parts[parts.length - 1];
|
||||
|
||||
return this.repository.create({
|
||||
userId,
|
||||
filename: originalFilename,
|
||||
mimeType: info.contentType,
|
||||
storagePath: dto.objectKey,
|
||||
objectKey: dto.objectKey,
|
||||
bucket: this.cos.getBucket(),
|
||||
sizeBytes: info.size,
|
||||
checksum: dto.checksum,
|
||||
});
|
||||
}
|
||||
|
||||
async getFile(userId: string, fileId: string) {
|
||||
const file = await this.repository.findById(fileId);
|
||||
if (!file) {
|
||||
throw new NotFoundException('文件不存在');
|
||||
}
|
||||
if (file.userId !== userId) {
|
||||
throw new ForbiddenException('无权访问该文件');
|
||||
}
|
||||
|
||||
const downloadUrl = await this.storage.getDownloadUrl(file.objectKey!);
|
||||
return { file, downloadUrl };
|
||||
}
|
||||
|
||||
async deleteFile(userId: string, fileId: string) {
|
||||
const file = await this.repository.findById(fileId);
|
||||
if (!file) {
|
||||
throw new NotFoundException('文件不存在');
|
||||
}
|
||||
if (file.userId !== userId) {
|
||||
throw new ForbiddenException('无权操作该文件');
|
||||
}
|
||||
|
||||
await this.storage.deleteObject(file.objectKey!);
|
||||
await this.repository.delete(fileId);
|
||||
}
|
||||
|
||||
async findByUserId(userId: string) {
|
||||
return this.repository.findByUserId(userId);
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,8 @@
|
||||
import { Controller, Get, Post, Patch, Body, Param } from '@nestjs/common';
|
||||
import { Controller, Get, Post, Patch, Body, Param, Query } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation } from '@nestjs/swagger';
|
||||
import { FocusItemsService } from './focus-items.service';
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||
import { PaginationDto } from '../../common/dto/pagination.dto';
|
||||
import type { UserPayload } from '../../common/types';
|
||||
|
||||
@ApiTags('focus-items')
|
||||
@ -11,8 +12,8 @@ export class FocusItemsController {
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: '获取待巩固项列表' })
|
||||
async findAll(@CurrentUser() user: UserPayload) {
|
||||
return this.focusItemsService.findAll(String(user?.id || 'anonymous'));
|
||||
async findAll(@CurrentUser() user: UserPayload, @Query() pagination: PaginationDto) {
|
||||
return this.focusItemsService.findAll(String(user?.id || 'anonymous'), pagination);
|
||||
}
|
||||
|
||||
@Post()
|
||||
|
||||
@ -5,10 +5,14 @@ import { PrismaService } from '../../infrastructure/database/prisma.service';
|
||||
export class FocusItemsRepository {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async findAll(userId: string) {
|
||||
async findAll(userId: string, pagination?: { page?: number; limit?: number }) {
|
||||
const page = pagination?.page ?? 1;
|
||||
const limit = pagination?.limit ?? 20;
|
||||
return this.prisma.focusItem.findMany({
|
||||
where: { userId, deletedAt: null },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (page - 1) * limit,
|
||||
take: limit,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -5,8 +5,8 @@ import { FocusItemsRepository } from './focus-items.repository';
|
||||
export class FocusItemsService {
|
||||
constructor(private readonly repository: FocusItemsRepository) {}
|
||||
|
||||
async findAll(userId: string) {
|
||||
return this.repository.findAll(userId);
|
||||
async findAll(userId: string, pagination?: { page?: number; limit?: number }) {
|
||||
return this.repository.findAll(userId, pagination);
|
||||
}
|
||||
|
||||
async create(userId: string, dto: any) {
|
||||
|
||||
@ -2,6 +2,7 @@ import { Controller, Get, Post, Patch, Delete, Body, Param, Query } from '@nestj
|
||||
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||||
import { KnowledgeBaseService } from './knowledge-base.service';
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||
import { PaginationDto } from '../../common/dto/pagination.dto';
|
||||
import type { UserPayload } from '../../common/types';
|
||||
|
||||
@ApiTags('knowledge-base')
|
||||
@ -17,8 +18,8 @@ export class KnowledgeBaseController {
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: '获取知识库列表' })
|
||||
async findAll(@CurrentUser() user: UserPayload, @Query() query: any) {
|
||||
return this.service.findAll(String(user?.id || 'anonymous'), query);
|
||||
async findAll(@CurrentUser() user: UserPayload, @Query() pagination: PaginationDto) {
|
||||
return this.service.findAll(String(user?.id || 'anonymous'), pagination);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
|
||||
@ -21,10 +21,14 @@ export class KnowledgeBaseRepository {
|
||||
return this.prisma.knowledgeBase.findUnique({ where: { id } });
|
||||
}
|
||||
|
||||
async findAllByUserId(userId: string) {
|
||||
async findAllByUserId(userId: string, pagination?: { page?: number; limit?: number }) {
|
||||
const page = pagination?.page ?? 1;
|
||||
const limit = pagination?.limit ?? 20;
|
||||
return this.prisma.knowledgeBase.findMany({
|
||||
where: { userId, deletedAt: null },
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
skip: (page - 1) * limit,
|
||||
take: limit,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -14,8 +14,8 @@ export class KnowledgeBaseService {
|
||||
return this.repository.create(userId, dto);
|
||||
}
|
||||
|
||||
async findAll(userId: string, query: any) {
|
||||
return this.repository.findAllByUserId(userId);
|
||||
async findAll(userId: string, pagination: { page?: number; limit?: number }) {
|
||||
return this.repository.findAllByUserId(userId, pagination);
|
||||
}
|
||||
|
||||
async findOne(userId: string, id: string) {
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
|
||||
import { Controller, Get, Post, Body, Param, Query } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||||
import { LearningSessionService } from './learning-session.service';
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||
import { PaginationDto } from '../../common/dto/pagination.dto';
|
||||
import type { UserPayload } from '../../common/types';
|
||||
|
||||
@ApiTags('learning-session')
|
||||
@ -23,7 +24,7 @@ export class LearningSessionController {
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: '获取学习会话列表' })
|
||||
async findAll(@CurrentUser() user: UserPayload) {
|
||||
return this.service.findByUserId(String(user?.id || 'anonymous'));
|
||||
async findAll(@CurrentUser() user: UserPayload, @Query() pagination: PaginationDto) {
|
||||
return this.service.findByUserId(String(user?.id || 'anonymous'), pagination);
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,10 +38,14 @@ export class LearningSessionRepository {
|
||||
});
|
||||
}
|
||||
|
||||
async findByUserId(userId: string) {
|
||||
async findByUserId(userId: string, pagination?: { page?: number; limit?: number }) {
|
||||
const page = pagination?.page ?? 1;
|
||||
const limit = pagination?.limit ?? 20;
|
||||
return this.prisma.learningSession.findMany({
|
||||
where: { userId },
|
||||
orderBy: { startedAt: 'desc' },
|
||||
skip: (page - 1) * limit,
|
||||
take: limit,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { LearningSessionRepository } from './learning-session.repository';
|
||||
import type { PaginationDto } from '../../common/dto/pagination.dto';
|
||||
|
||||
@Injectable()
|
||||
export class LearningSessionService {
|
||||
@ -15,7 +16,7 @@ export class LearningSessionService {
|
||||
return session;
|
||||
}
|
||||
|
||||
async findByUserId(userId: string) {
|
||||
return this.repository.findByUserId(userId);
|
||||
async findByUserId(userId: string, pagination: PaginationDto) {
|
||||
return this.repository.findByUserId(userId, pagination);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import { Controller, Get, Post, Param, HttpCode, HttpStatus } from '@nestjs/common';
|
||||
import { Controller, Get, Post, Param, Query, HttpCode, HttpStatus } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation } from '@nestjs/swagger';
|
||||
import { NotificationsService } from './notifications.service';
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||
import { PaginationDto } from '../../common/dto/pagination.dto';
|
||||
import type { UserPayload } from '../../common/types';
|
||||
|
||||
@ApiTags('notifications')
|
||||
@ -11,8 +12,8 @@ export class NotificationsController {
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: '获取通知列表' })
|
||||
async list(@CurrentUser() user: UserPayload) {
|
||||
return this.service.list(String(user?.id || 'anonymous'));
|
||||
async list(@CurrentUser() user: UserPayload, @Query() pagination: PaginationDto) {
|
||||
return this.service.list(String(user?.id || 'anonymous'), pagination);
|
||||
}
|
||||
|
||||
@Post(':id/read')
|
||||
|
||||
@ -5,10 +5,14 @@ import { PrismaService } from '../../infrastructure/database/prisma.service';
|
||||
export class NotificationsRepository {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async findAll(userId: string) {
|
||||
async findAll(userId: string, pagination?: { page?: number; limit?: number }) {
|
||||
const page = pagination?.page ?? 1;
|
||||
const limit = pagination?.limit ?? 20;
|
||||
return this.prisma.notification.findMany({
|
||||
where: { userId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (page - 1) * limit,
|
||||
take: limit,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { Injectable, NotFoundException, Logger } from '@nestjs/common';
|
||||
import { NotificationsRepository } from './notifications.repository';
|
||||
import type { PaginationDto } from '../../common/dto/pagination.dto';
|
||||
|
||||
@Injectable()
|
||||
export class NotificationsService {
|
||||
@ -7,8 +8,8 @@ export class NotificationsService {
|
||||
|
||||
constructor(private readonly repository: NotificationsRepository) {}
|
||||
|
||||
async list(userId: string) {
|
||||
return this.repository.findAll(userId);
|
||||
async list(userId: string, pagination: PaginationDto) {
|
||||
return this.repository.findAll(userId, pagination);
|
||||
}
|
||||
|
||||
async markRead(id: string) {
|
||||
|
||||
@ -28,4 +28,17 @@ export class UsersController {
|
||||
async updatePreferences(@CurrentUser() user: UserPayload, @Body() body: any) {
|
||||
return this.usersService.updatePreferences(String(user.id), body);
|
||||
}
|
||||
|
||||
@Get('me/profile')
|
||||
@ApiOperation({ summary: '获取用户学习档案' })
|
||||
@ApiResponse({ status: 200, description: '用户学习档案' })
|
||||
async getProfileDetail(@CurrentUser() user: UserPayload) {
|
||||
return this.usersService.getProfileDetail(String(user.id));
|
||||
}
|
||||
|
||||
@Patch('me/profile')
|
||||
@ApiOperation({ summary: '更新用户学习档案' })
|
||||
async updateProfileDetail(@CurrentUser() user: UserPayload, @Body() body: any) {
|
||||
return this.usersService.updateProfileDetail(String(user.id), body);
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,6 +17,8 @@ export class UsersRepository {
|
||||
status: true,
|
||||
onboardingCompleted: true,
|
||||
createdAt: true,
|
||||
profile: true,
|
||||
preferences: true,
|
||||
},
|
||||
});
|
||||
|
||||
@ -24,15 +26,11 @@ export class UsersRepository {
|
||||
throw new NotFoundException('用户不存在');
|
||||
}
|
||||
|
||||
const { profile, preferences, ...rest } = user;
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
nickname: user.nickname,
|
||||
avatarUrl: user.avatarUrl,
|
||||
role: user.role,
|
||||
status: user.status,
|
||||
onboardingCompleted: user.onboardingCompleted,
|
||||
createdAt: user.createdAt,
|
||||
...rest,
|
||||
profile: profile ?? null,
|
||||
preferences: preferences ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
@ -46,6 +44,26 @@ export class UsersRepository {
|
||||
});
|
||||
}
|
||||
|
||||
async findUserProfile(userId: string) {
|
||||
const profile = await this.prisma.userProfile.findUnique({
|
||||
where: { userId },
|
||||
});
|
||||
return profile ?? null;
|
||||
}
|
||||
|
||||
async upsertUserProfile(userId: string, dto: {
|
||||
learningIdentity?: string;
|
||||
learningDirection?: string;
|
||||
bio?: string;
|
||||
currentGoal?: string;
|
||||
}) {
|
||||
return this.prisma.userProfile.upsert({
|
||||
where: { userId },
|
||||
create: { userId, ...dto },
|
||||
update: dto,
|
||||
});
|
||||
}
|
||||
|
||||
async updatePreferences(userId: string, dto: any) {
|
||||
return this.prisma.userPreference.upsert({
|
||||
where: { userId },
|
||||
|
||||
@ -13,6 +13,14 @@ export class UsersService {
|
||||
return this.usersRepository.updateProfile(userId, dto);
|
||||
}
|
||||
|
||||
async getProfileDetail(userId: string) {
|
||||
return this.usersRepository.findUserProfile(userId);
|
||||
}
|
||||
|
||||
async updateProfileDetail(userId: string, dto: any) {
|
||||
return this.usersRepository.upsertUserProfile(userId, dto);
|
||||
}
|
||||
|
||||
async updatePreferences(userId: string, dto: any) {
|
||||
return this.usersRepository.updatePreferences(userId, dto);
|
||||
}
|
||||
|
||||
@ -1,5 +1,31 @@
|
||||
console.log('[Worker] AI Analysis Worker started');
|
||||
import { Processor, WorkerHost } from '@nestjs/bullmq';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { Job } from 'bullmq';
|
||||
import { QUEUE_AI_ANALYSIS } from '../infrastructure/queue/queue.service';
|
||||
import { ActiveRecallAnalysisWorkflow } from '../modules/ai/workflows/active-recall-analysis.workflow';
|
||||
import { AiAnalysisRepository } from '../modules/ai-analysis/ai-analysis.repository';
|
||||
|
||||
setInterval(() => {
|
||||
console.log('[Worker] AI Analysis Worker is running...');
|
||||
}, 60000);
|
||||
@Processor(QUEUE_AI_ANALYSIS)
|
||||
export class AiAnalysisWorker extends WorkerHost {
|
||||
private readonly logger = new Logger(AiAnalysisWorker.name);
|
||||
|
||||
constructor(
|
||||
private readonly workflow: ActiveRecallAnalysisWorkflow,
|
||||
private readonly repository: AiAnalysisRepository,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async process(job: Job<{
|
||||
userId: string;
|
||||
questionText: string;
|
||||
knowledgeItemContent: string;
|
||||
userAnswer: string;
|
||||
}>) {
|
||||
this.logger.log(`Processing AI analysis job ${job.id}`);
|
||||
const result = await this.workflow.execute(job.data);
|
||||
await this.repository.createResult(job.data.userId, result);
|
||||
this.logger.log(`AI analysis job ${job.id} completed, score=${result.score}`);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,32 @@
|
||||
console.log('[Worker] Document Import Worker started');
|
||||
import { Processor, WorkerHost } from '@nestjs/bullmq';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { Job } from 'bullmq';
|
||||
import { QUEUE_DOCUMENT_IMPORT } from '../infrastructure/queue/queue.service';
|
||||
import { DocumentImportRepository } from '../modules/document-import/document-import.repository';
|
||||
|
||||
setInterval(() => {
|
||||
console.log('[Worker] Document Import Worker is running...');
|
||||
}, 60000);
|
||||
@Processor(QUEUE_DOCUMENT_IMPORT)
|
||||
export class DocumentImportWorker extends WorkerHost {
|
||||
private readonly logger = new Logger(DocumentImportWorker.name);
|
||||
|
||||
constructor(
|
||||
private readonly repository: DocumentImportRepository,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async process(job: Job<{ importId: string; userId: string }>) {
|
||||
this.logger.log(`Processing document import job ${job.id}, importId=${job.data.importId}`);
|
||||
await this.repository.updateStatus(job.data.importId, 'processing');
|
||||
|
||||
try {
|
||||
// TODO: actual file parsing + AI knowledge generation
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
await this.repository.updateStatus(job.data.importId, 'completed');
|
||||
this.logger.log(`Document import job ${job.id} completed`);
|
||||
} catch (err: any) {
|
||||
this.logger.error(`Document import job ${job.id} failed: ${err.message}`);
|
||||
await this.repository.updateStatus(job.data.importId, 'failed');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,22 @@
|
||||
console.log('[Worker] Notification Worker started');
|
||||
import { Processor, WorkerHost } from '@nestjs/bullmq';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { Job } from 'bullmq';
|
||||
import { QUEUE_NOTIFICATION } from '../infrastructure/queue/queue.service';
|
||||
import { NotificationsService } from '../modules/notifications/notifications.service';
|
||||
|
||||
setInterval(() => {
|
||||
console.log('[Worker] Notification Worker is running...');
|
||||
}, 60000);
|
||||
@Processor(QUEUE_NOTIFICATION)
|
||||
export class NotificationWorker extends WorkerHost {
|
||||
private readonly logger = new Logger(NotificationWorker.name);
|
||||
|
||||
constructor(
|
||||
private readonly notificationsService: NotificationsService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async process(job: Job<{ userId: string; type: string; title: string; body: string }>) {
|
||||
this.logger.log(`Processing notification job ${job.id}`);
|
||||
await this.notificationsService.send(job.data);
|
||||
this.logger.log(`Notification job ${job.id} completed`);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user