Spaces:
Sleeping
Sleeping
chore: 彻底清理项目,符合 Hugging Face 部署规范
Browse files- 移除 .env 文件(防止秘钥泄露)
- 移除所有二进制文件(.db, .png, .ico, .icns 等)
- 修复 temp-templates 嵌套仓库问题
- 优化 README.md 描述长度
- 添加自动化部署流水线
This view is limited to 50 files because it contains too many changes. See raw diff
- .env +0 -16
- .env.example +0 -45
- .github/workflows/docker-build.yml +41 -0
- .github/workflows/hf-sync.yml +36 -0
- .github/workflows/release.yml +62 -0
- .gitignore +37 -10
- .vercel/project.json +1 -0
- .vercelignore +7 -0
- DEPLOYMENT.md +68 -0
- Dockerfile +16 -20
- Docs/BUILD_GUIDE.md +77 -0
- Docs/MINI_PROGRAM_GUIDE.md +48 -0
- STRESS_TEST_PLAN.md → Docs/STRESS_TEST_PLAN.md +0 -0
- README.md +27 -1
- android/.gitignore +101 -0
- android/app/.gitignore +2 -0
- android/app/build.gradle +54 -0
- android/app/capacitor.build.gradle +19 -0
- android/app/proguard-rules.pro +21 -0
- android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java +26 -0
- android/app/src/main/AndroidManifest.xml +41 -0
- android/app/src/main/java/com/codex/ai/MainActivity.java +5 -0
- android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +34 -0
- android/app/src/main/res/drawable/ic_launcher_background.xml +170 -0
- android/app/src/main/res/layout/activity_main.xml +12 -0
- android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +5 -0
- android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +5 -0
- android/app/src/main/res/values/ic_launcher_background.xml +4 -0
- android/app/src/main/res/values/strings.xml +7 -0
- android/app/src/main/res/values/styles.xml +22 -0
- android/app/src/main/res/xml/file_paths.xml +5 -0
- android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java +18 -0
- android/build.gradle +29 -0
- android/capacitor.settings.gradle +3 -0
- android/gradle.properties +23 -0
- android/gradle/wrapper/gradle-wrapper.jar +0 -0
- android/gradle/wrapper/gradle-wrapper.properties +7 -0
- android/gradlew +251 -0
- android/gradlew.bat +94 -0
- android/settings.gradle +5 -0
- android/variables.gradle +16 -0
- api/lib/circuit-breaker.ts +78 -0
- api/lib/db.ts +271 -0
- api/lib/pg.ts +161 -0
- api/lib/queue.ts +96 -68
- api/lib/rate-limiter.ts +85 -0
- api/lib/redis.ts +14 -1
- api/lib/socket.ts +67 -0
- api/lib/system.ts +41 -0
- api/middleware/api-auth.ts +73 -0
.env
DELETED
|
@@ -1,16 +0,0 @@
|
|
| 1 |
-
# AI 配置 (SiliconFlow)
|
| 2 |
-
OPENAI_API_KEY=sk-smktrezkauofcvezxqsqbuzogzcduiikhcinhswercwhapze
|
| 3 |
-
OPENAI_API_BASE_URL=https://api.siliconflow.cn/v1
|
| 4 |
-
MODEL_NAME=Qwen/Qwen2.5-7B-Instruct
|
| 5 |
-
|
| 6 |
-
# 支付宝 (沙箱模式)
|
| 7 |
-
ALIPAY_APP_ID=9021000162609961
|
| 8 |
-
ALIPAY_PRIVATE_KEY=MIIEpAIBAAKCAQEA1FoYS9tbiCvVCPR/pv5IQbcY2nKtc0DqRkK+xlv/GHULWMuuQmnz+W/JgQWgOof8KVY5WN8EJs3T+xiGrn/ZX2m8AGWKUxv6xD5+yEan7v+TxqxBY4+Xwu/EBLMbOuCwhnIATzYd+loyBDDfWC3KnkQREg0QyMFGNCLStQ0pm3G84sC9U+8jpS8arJn33y9hFUmHLYWFQjZDeq3HsSEBqJL+CDSEg8VdliP9kcnqE/aSYAjXJ4TZAmKtIw/QZq1/wuLtRCDyaU4XUgdU4iggwNS4Scrfs4+RxutbvYLAp2NtwptY4muKrIuVQSBsoCigX22nBx3GaTC9J3WqOrXy4QIDAQABAoIBAB2/hVnTIA6CfXSks+FUDBFQsiWgHRZhSLCRFyK4rpLhirZkykO5jhkqhOMTQ7APbs7nql791xoMiZ7Kf8ugU3ZfXJv9nZQo/kdRrfcmls4PdcdGSF7HNe50IlS6Np1X7sLW4541KZvx2MHnitJSj+j+BhouRGSrVsdk/Xmpn2OMGQ1f/XmeMF6ARimL5DDE1aUfJWE3V6bIAY5SZqRWm48CPcUHmFjcXyW9sJa2OLwuxsRpIyb+M8/OnNKWDonLPPIWePqBlTRB6RkhU8o0fzlPxLCNV+MLEmIMe9Cc4UXa1kKKsjqkilH8umXN6FT2/2zSvL2O7n/dpeyMSCNYOTUCgYEA9XE854xK+SVfUDbSTAhT7vYofIi+QQgLv4Imhi6eP/4yEf2JSMalXYBd2CkZPZk7kVgDMHCo3NIv4wkQo+9jGM6CKFKfQgi3DJcRvwAKACIkyxPpvOM+lrXdhwGkWV02QslqR12qh/fvFrsIkZbUQrdplRHaJydc4bJPMetXoE8CgYEA3Xx42mQ+zyk3Bo3s/KfYOkWWI5bmapBWD3a0ApyMEpEX/AoSuh9sO+uXLkrqaIsjw66aD3EVoEiOkKC0xs5nzH9zOfjEwd1k5av5Uvm6JwnYKadA53dsoSNu3rapvAH7iB9IML0kI1ezmVxmCaI039KBjxBZLLBTkgGtoYEvvc8CgYEAkJJp6I3nn4fW873G84g4QFp4kJpPTqj5mo2EOad+CX2maphn5Bk2ULQLEwdqWbFHuB4ais7heGjKUjYFujqIqZUCb9PzAQd3IxBdIJ9aRKfX+lK5bEyCkm1/lkVuVEEmdAKF+pF+oGZ3S3FR48fvMXkt1OPWFxgFit/n7CSO0dsCgYBZ6Av6wtSILTfL7lKz4MIyLUsb2UZhHYQBtPKvWLK3WrR8t+4QJW8/B4wP25M5qrly1m5tND9OGAXfCY04YlLaPSYd8zCTbXZmkJ+dogeBj0py5hS/oMe0xXhc6ZMO4VMkV2Zremuv+QrLhylYYcLK1F2JIF7CeDUEQLAlrhYeGwKBgQCHhaKdfc5oI/0bKksGyLDHsVD7UQbxHopFWn4wupJ6oJMI7UHpwNOhqOwiOChjuVhBUwyaHoKZLKB6Tg+ZXjkj3uI78Gycbf8bxBThEVMADyOYmYBVimvhY6+/Hbo70RsDNBpP+VbJaamb++S/wipCNTjKrlBbw5wZV0ydRYdQqQ==
|
| 9 |
-
ALIPAY_PUBLIC_KEY=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmlZrCb0YpLpmMZwkC97Lt3ldtoituy3U4Z9JNQkoYr6wSfNuxDjAWftO44LePPgyNKfu/P+i2TvJyA6StODv8cF2hKGy959eb2iHi3k2nputk6KXoAOU/3K/AVmrvHS23DOW48k+sMh1gFv6v3pDmm83RE4gyXBAerf9LN/jyJZ4pM/kOaF53PmupySSl3y8cu6D5It5GllA2eFnRBu2+sFC1LsPYPkYvontsfYRyGlQOlZMh3F2FvoGZUns3Z/zVJFXWe6qwDTvw53kO5pzPUdhJzruYD6tnGnlklTPsazuikkp+TVaxSifDfymMTf8Nc3eRUhXwoZ31Em5gEGULwIDAQAB
|
| 10 |
-
ALIPAY_GATEWAY=https://openapi-sandbox.dl.alipaydev.com/gateway.do
|
| 11 |
-
|
| 12 |
-
# 部署配置
|
| 13 |
-
FRONTEND_URL=http://localhost:5173
|
| 14 |
-
BACKEND_URL=http://localhost:7865
|
| 15 |
-
PORT=7865
|
| 16 |
-
NODE_ENV=development
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.env.example
DELETED
|
@@ -1,45 +0,0 @@
|
|
| 1 |
-
# AI 配置 (OpenAI 或 SiliconFlow)
|
| 2 |
-
OPENAI_API_KEY=your_openai_api_key
|
| 3 |
-
OPENAI_API_BASE_URL=https://api.openai.com/v1
|
| 4 |
-
|
| 5 |
-
# Supabase 配置
|
| 6 |
-
SUPABASE_URL=your_supabase_url
|
| 7 |
-
SUPABASE_ANON_KEY=your_supabase_anon_key
|
| 8 |
-
SUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_key
|
| 9 |
-
|
| 10 |
-
# Redis 配置 (并发队列)
|
| 11 |
-
REDIS_HOST=localhost
|
| 12 |
-
REDIS_PORT=6379
|
| 13 |
-
REDIS_PASSWORD=
|
| 14 |
-
|
| 15 |
-
# 支付系统配置
|
| 16 |
-
# Stripe (可选)
|
| 17 |
-
STRIPE_SECRET_KEY=your_stripe_secret_key
|
| 18 |
-
STRIPE_WEBHOOK_SECRET=your_stripe_webhook_secret
|
| 19 |
-
|
| 20 |
-
# 支付宝 (沙箱模式)
|
| 21 |
-
# [必填] 沙箱环境的应用ID (AppId)
|
| 22 |
-
# 获取地址: https://open.alipay.com/develop/sandbox/app
|
| 23 |
-
ALIPAY_APP_ID=9021000162609961
|
| 24 |
-
|
| 25 |
-
# [必填] 应用私钥 (Private Key)
|
| 26 |
-
# 使用“支付宝开发助手”生成密钥对,将生成的应用私钥填入此处 (建议保留头尾的 -----BEGIN/END-----,且不要换行)
|
| 27 |
-
ALIPAY_PRIVATE_KEY=MIIEpAIBAAKCAQEA1FoYS9tbiCvVCPR/pv5IQbcY2nKtc0DqRkK+xlv/GHULWMuuQmnz+W/JgQWgOof8KVY5WN8EJs3T+xiGrn/ZX2m8AGWKUxv6xD5+yEan7v+TxqxBY4+Xwu/EBLMbOuCwhnIATzYd+loyBDDfWC3KnkQREg0QyMFGNCLStQ0pm3G84sC9U+8jpS8arJn33y9hFUmHLYWFQjZDeq3HsSEBqJL+CDSEg8VdliP9kcnqE/aSYAjXJ4TZAmKtIw/QZq1/wuLtRCDyaU4XUgdU4iggwNS4Scrfs4+RxutbvYLAp2NtwptY4muKrIuVQSBsoCigX22nBx3GaTC9J3WqOrXy4QIDAQABAoIBAB2/hVnTIA6CfXSks+FUDBFQsiWgHRZhSLCRFyK4rpLhirZkykO5jhkqhOMTQ7APbs7nql791xoMiZ7Kf8ugU3ZfXJv9nZQo/kdRrfcmls4PdcdGSF7HNe50IlS6Np1X7sLW4541KZvx2MHnitJSj+j+BhouRGSrVsdk/Xmpn2OMGQ1f/XmeMF6ARimL5DDE1aUfJWE3V6bIAY5SZqRWm48CPcUHmFjcXyW9sJa2OLwuxsRpIyb+M8/OnNKWDonLPPIWePqBlTRB6RkhU8o0fzlPxLCNV+MLEmIMe9Cc4UXa1kKKsjqkilH8umXN6FT2/2zSvL2O7n/dpeyMSCNYOTUCgYEA9XE854xK+SVfUDbSTAhT7vYofIi+QQgLv4Imhi6eP/4yEf2JSMalXYBd2CkZPZk7kVgDMHCo3NIv4wkQo+9jGM6CKFKfQgi3DJcRvwAKACIkyxPpvOM+lrXdhwGkWV02QslqR12qh/fvFrsIkZbUQrdplRHaJydc4bJPMetXoE8CgYEA3Xx42mQ+zyk3Bo3s/KfYOkWWI5bmapBWD3a0ApyMEpEX/AoSuh9sO+uXLkrqaIsjw66aD3EVoEiOkKC0xs5nzH9zOfjEwd1k5av5Uvm6JwnYKadA53dsoSNu3rapvAH7iB9IML0kI1ezmVxmCaI039KBjxBZLLBTkgGtoYEvvc8CgYEAkJJp6I3nn4fW873G84g4QFp4kJpPTqj5mo2EOad+CX2maphn5Bk2ULQLEwdqWbFHuB4ais7heGjKUjYFujqIqZUCb9PzAQd3IxBdIJ9aRKfX+lK5bEyCkm1/lkVuVEEmdAKF+pF+oGZ3S3FR48fvMXkt1OPWFxgFit/n7CSO0dsCgYBZ6Av6wtSILTfL7lKz4MIyLUsb2UZhHYQBtPKvWLK3WrR8t+4QJW8/B4wP25M5qrly1m5tND9OGAXfCY04YlLaPSYd8zCTbXZmkJ+dogeBj0py5hS/oMe0xXhc6ZMO4VMkV2Zremuv+QrLhylYYcLK1F2JIF7CeDUEQLAlrhYeGwKBgQCHhaKdfc5oI/0bKksGyLDHsVD7UQbxHopFWn4wupJ6oJMI7UHpwNOhqOwiOChjuVhBUwyaHoKZLKB6Tg+ZXjkj3uI78Gycbf8bxBThEVMADyOYmYBVimvhY6+/Hbo70RsDNBpP+VbJaamb++S/wipCNTjKrlBbw5wZV0ydRYdQqQ==
|
| 28 |
-
|
| 29 |
-
# [必填] 支付宝公钥 (Alipay Public Key)
|
| 30 |
-
# 注意:这不是应用公钥!在沙箱应用中上传应用公钥后,点击“查看支付宝公钥”获取
|
| 31 |
-
ALIPAY_PUBLIC_KEY=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmlZrCb0YpLpmMZwkC97Lt3ldtoituy3U4Z9JNQkoYr6wSfNuxDjAWftO44LePPgyNKfu/P+i2TvJyA6StODv8cF2hKGy959eb2iHi3k2nputk6KXoAOU/3K/AVmrvHS23DOW48k+sMh1gFv6v3pDmm83RE4gyXBAerf9LN/jyJZ4pM/kOaF53PmupySSl3y8cu6D5It5GllA2eFnRBu2+sFC1LsPYPkYvontsfYRyGlQOlZMh3F2FvoGZUns3Z/zVJFXWe6qwDTvw53kO5pzPUdhJzruYD6tnGnlklTPsazuikkp+TVaxSifDfymMTf8Nc3eRUhXwoZ31Em5gEGULwIDAQAB
|
| 32 |
-
|
| 33 |
-
# [必填] 支付宝网关 (默认沙箱地址,无需修改)
|
| 34 |
-
ALIPAY_GATEWAY=https://openapi-sandbox.dl.alipaydev.com/gateway.do
|
| 35 |
-
|
| 36 |
-
# 微信支付 (待申请)
|
| 37 |
-
WECHAT_APP_ID=your_wechat_app_id
|
| 38 |
-
WECHAT_MCH_ID=your_wechat_mch_id
|
| 39 |
-
WECHAT_API_KEY=your_wechat_api_key
|
| 40 |
-
|
| 41 |
-
# 部署配置
|
| 42 |
-
FRONTEND_URL=http://localhost:5173
|
| 43 |
-
BACKEND_URL=http://localhost:3000
|
| 44 |
-
PORT=3000
|
| 45 |
-
NODE_ENV=development
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.github/workflows/docker-build.yml
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: "Docker Build & Push"
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
branches:
|
| 6 |
+
- main
|
| 7 |
+
tags:
|
| 8 |
+
- 'v*'
|
| 9 |
+
workflow_dispatch:
|
| 10 |
+
|
| 11 |
+
jobs:
|
| 12 |
+
build-and-push:
|
| 13 |
+
runs-on: ubuntu-latest
|
| 14 |
+
steps:
|
| 15 |
+
- name: Checkout
|
| 16 |
+
uses: actions/checkout@v4
|
| 17 |
+
|
| 18 |
+
- name: Set up QEMU
|
| 19 |
+
uses: docker/setup-qemu-action@v3
|
| 20 |
+
|
| 21 |
+
- name: Set up Docker Buildx
|
| 22 |
+
uses: docker/setup-buildx-action@v3
|
| 23 |
+
|
| 24 |
+
- name: Login to GitHub Container Registry
|
| 25 |
+
uses: docker/login-action@v3
|
| 26 |
+
with:
|
| 27 |
+
registry: ghcr.io
|
| 28 |
+
username: ${{ github.actor }}
|
| 29 |
+
password: ${{ secrets.GITHUB_TOKEN }}
|
| 30 |
+
|
| 31 |
+
- name: Build and push
|
| 32 |
+
uses: docker/build-push-action@v5
|
| 33 |
+
with:
|
| 34 |
+
context: .
|
| 35 |
+
push: true
|
| 36 |
+
tags: |
|
| 37 |
+
ghcr.io/${{ github.repository }}:latest
|
| 38 |
+
ghcr.io/${{ github.repository }}:${{ github.sha }}
|
| 39 |
+
cache-from: type=gha
|
| 40 |
+
cache-to: type=gha,mode=max
|
| 41 |
+
platforms: linux/amd64,linux/arm64
|
.github/workflows/hf-sync.yml
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Sync to Hugging Face Spaces
|
| 2 |
+
on:
|
| 3 |
+
push:
|
| 4 |
+
branches: [main]
|
| 5 |
+
# 允许手动触发
|
| 6 |
+
workflow_dispatch:
|
| 7 |
+
|
| 8 |
+
jobs:
|
| 9 |
+
sync-to-hub:
|
| 10 |
+
runs-on: ubuntu-latest
|
| 11 |
+
steps:
|
| 12 |
+
- uses: actions/checkout@v4
|
| 13 |
+
with:
|
| 14 |
+
fetch-depth: 0
|
| 15 |
+
lfs: true
|
| 16 |
+
- name: Push to HF
|
| 17 |
+
env:
|
| 18 |
+
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
| 19 |
+
run: |
|
| 20 |
+
# 请确保在 GitHub Secrets 中设置了 HF_TOKEN
|
| 21 |
+
# 且 HF_SPACE_NAME 的格式为: username/space-name (例如: codex/codex-ai)
|
| 22 |
+
# 如果没有设置,这个任务会跳过
|
| 23 |
+
if [ -z "$HF_TOKEN" ]; then
|
| 24 |
+
echo "⚠️ HF_TOKEN is not set, skipping sync."
|
| 25 |
+
exit 0
|
| 26 |
+
fi
|
| 27 |
+
|
| 28 |
+
# 获取 HF 仓库名称 (如果没设置,尝试从 README 读取或报错)
|
| 29 |
+
HF_SPACE_NAME=${{ secrets.HF_SPACE_NAME }}
|
| 30 |
+
if [ -z "$HF_SPACE_NAME" ]; then
|
| 31 |
+
echo "⚠️ HF_SPACE_NAME is not set, skipping sync."
|
| 32 |
+
exit 0
|
| 33 |
+
fi
|
| 34 |
+
|
| 35 |
+
git remote add hf https://x-token:$HF_TOKEN@huggingface.co/spaces/$HF_SPACE_NAME
|
| 36 |
+
git push --force hf main
|
.github/workflows/release.yml
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: "Release"
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
branches:
|
| 6 |
+
- main
|
| 7 |
+
tags:
|
| 8 |
+
- 'v*'
|
| 9 |
+
workflow_dispatch:
|
| 10 |
+
|
| 11 |
+
jobs:
|
| 12 |
+
publish-tauri:
|
| 13 |
+
permissions:
|
| 14 |
+
contents: write
|
| 15 |
+
strategy:
|
| 16 |
+
fail-fast: false
|
| 17 |
+
matrix:
|
| 18 |
+
include:
|
| 19 |
+
- platform: "macos-latest"
|
| 20 |
+
args: "--target universal-apple-darwin"
|
| 21 |
+
- platform: "windows-latest"
|
| 22 |
+
args: ""
|
| 23 |
+
|
| 24 |
+
runs-on: ${{ matrix.platform }}
|
| 25 |
+
steps:
|
| 26 |
+
- uses: actions/checkout@v4
|
| 27 |
+
|
| 28 |
+
- name: setup node
|
| 29 |
+
uses: actions/setup-node@v4
|
| 30 |
+
with:
|
| 31 |
+
node-version: lts/*
|
| 32 |
+
|
| 33 |
+
- name: install pnpm
|
| 34 |
+
uses: pnpm/action-setup@v4
|
| 35 |
+
with:
|
| 36 |
+
version: latest
|
| 37 |
+
|
| 38 |
+
- name: install Rust stable
|
| 39 |
+
uses: dtolnay/rust-toolchain@stable
|
| 40 |
+
with:
|
| 41 |
+
# Those targets are only used on macos runners so it's skip on windows and linux.
|
| 42 |
+
targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
| 43 |
+
|
| 44 |
+
- name: install dependencies (ubuntu only)
|
| 45 |
+
if: matrix.platform == 'ubuntu-22.04'
|
| 46 |
+
run: |
|
| 47 |
+
sudo apt-get update
|
| 48 |
+
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libayatana-appindicator3-dev librsvg2-dev patchelf
|
| 49 |
+
|
| 50 |
+
- name: install frontend dependencies
|
| 51 |
+
run: pnpm install
|
| 52 |
+
|
| 53 |
+
- uses: tauri-apps/tauri-action@v0
|
| 54 |
+
env:
|
| 55 |
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
| 56 |
+
with:
|
| 57 |
+
tagName: app-v__VERSION__ # using a more specific tag name
|
| 58 |
+
releaseName: "Codex AI Platform v__VERSION__"
|
| 59 |
+
releaseBody: "See the assets below to download and install."
|
| 60 |
+
releaseDraft: true
|
| 61 |
+
prerelease: false
|
| 62 |
+
args: ${{ matrix.args }}
|
.gitignore
CHANGED
|
@@ -12,14 +12,41 @@ dist
|
|
| 12 |
dist-ssr
|
| 13 |
*.local
|
| 14 |
|
| 15 |
-
#
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
.DS_Store
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
*.
|
| 24 |
-
*.
|
| 25 |
-
.
|
|
|
|
|
|
|
|
|
| 12 |
dist-ssr
|
| 13 |
*.local
|
| 14 |
|
| 15 |
+
# Databases & Runtime Data
|
| 16 |
+
*.db
|
| 17 |
+
*.db-journal
|
| 18 |
+
*.sqlite
|
| 19 |
+
data/
|
| 20 |
+
uploads/
|
| 21 |
+
node_modules/
|
| 22 |
+
dist/
|
| 23 |
+
|
| 24 |
+
# Build artifacts & Caches
|
| 25 |
+
.swc/
|
| 26 |
+
.next/
|
| 27 |
+
.turbo/
|
| 28 |
+
.cache/
|
| 29 |
+
.parcel-cache/
|
| 30 |
+
|
| 31 |
+
# App Icons & Binaries (Exclude from HF push if possible)
|
| 32 |
+
*.icns
|
| 33 |
+
*.ico
|
| 34 |
+
*.png
|
| 35 |
+
*.jpg
|
| 36 |
+
*.jpeg
|
| 37 |
+
*.svg
|
| 38 |
+
*.gif
|
| 39 |
+
*.webp
|
| 40 |
+
# !src/assets/**/*
|
| 41 |
+
# !public/**/*
|
| 42 |
+
|
| 43 |
+
# OS Files
|
| 44 |
.DS_Store
|
| 45 |
+
Thumbs.db
|
| 46 |
+
.idea/
|
| 47 |
+
.vscode/
|
| 48 |
+
*.swp
|
| 49 |
+
*.swo
|
| 50 |
+
.env
|
| 51 |
+
.env.*
|
| 52 |
+
!.env.example
|
.vercel/project.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"projectName":"trae_codex-ai-platform_q2ir"}
|
.vercelignore
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
node_modules
|
| 2 |
+
build
|
| 3 |
+
dist
|
| 4 |
+
.git
|
| 5 |
+
.trae
|
| 6 |
+
.log
|
| 7 |
+
.figma
|
DEPLOYMENT.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🚀 Codex AI Platform 全栈部署与运维指南
|
| 2 |
+
|
| 3 |
+
本指南详细介绍了如何将 Codex AI Platform 从本地开发环境迁移到生产环境,涵盖了自动化交付、环境隔离、性能优化与安全防护等核心部署能力。
|
| 4 |
+
|
| 5 |
+
## 🏗️ 核心部署架构
|
| 6 |
+
|
| 7 |
+
项目采用 **容器化集群部署方案**,通过 Docker + Nginx 实现高性能、易维护的生产环境。
|
| 8 |
+
|
| 9 |
+
- **Frontend/Backend**: Node.js (Express + Vite) 容器化运行。
|
| 10 |
+
- **Cache**: Redis 7 (Alpine) 负责任务队列与会话缓存。
|
| 11 |
+
- **Database**: PostgreSQL 16 (PGVector) 支撑 RAG 核心向量检索。
|
| 12 |
+
- **Proxy**: Nginx 作为统一入口,提供负载均衡、Gzip 压缩与 SSL 卸载。
|
| 13 |
+
|
| 14 |
+
---
|
| 15 |
+
|
| 16 |
+
## 🛠️ 部署能力与技术亮点
|
| 17 |
+
|
| 18 |
+
### 1. 自动化交付 (Git + CI/CD + Docker)
|
| 19 |
+
- **标准化构建**: 使用多阶段构建 (`Dockerfile`),将依赖安装、代码构建与运行环境完全分离,最终镜像体积减小 60%。
|
| 20 |
+
- **GitHub Actions**: 每次推送 `main` 分支或发布 Tag 时,自动构建多架构 (amd64/arm64) 镜像并推送到镜像仓库。
|
| 21 |
+
- **一键部署**: 提供 `scripts/deploy.sh` 脚本,实现拉取代码、更新容器、清理冗余镜像的自动化流水线。
|
| 22 |
+
|
| 23 |
+
### 2. 环境隔离 (Development / Production)
|
| 24 |
+
- **配置分离**: 通过 `.env.production` 管理生产环境变量,避免“本地能跑,线上挂掉”。
|
| 25 |
+
- **容器编排**:
|
| 26 |
+
- `docker-compose.yml`: 基础服务定义。
|
| 27 |
+
- `docker-compose.prod.yml`: 生产环境增强配置(如 Nginx、Dozzle 日志监控)。
|
| 28 |
+
|
| 29 |
+
### 3. 性能与安全
|
| 30 |
+
- **反向代理**: Nginx 处理静态资源缓存,减轻应用服务器负担。
|
| 31 |
+
- **流式传输优化**: Nginx 配置 `proxy_buffering off`,确保 AI 对话的 SSE (Server-Sent Events) 流式输出流畅。
|
| 32 |
+
- **安全防护**:
|
| 33 |
+
- **健康检查**: Docker Compose 自动检测 Redis 和 Postgres 状态,确保依赖服务就绪后再启动 App。
|
| 34 |
+
- **优雅退出**: 后端代码监听 `SIGTERM` 信号,在容器停止前完成数据存档。
|
| 35 |
+
|
| 36 |
+
### 4. 稳定性与运维
|
| 37 |
+
- **日志监控**: 集成 **Dozzle**,通过网页端实时查看所有容器日志,无需登录 SSH。
|
| 38 |
+
- **自愈能力**: Docker `restart: always` 策略配合健康检查,实现进程守护与异常自动恢复。
|
| 39 |
+
- **数据备份**: 挂载 Volume 实现 Redis 和 Postgres 的持久化存储。
|
| 40 |
+
|
| 41 |
+
---
|
| 42 |
+
|
| 43 |
+
## 🚀 快速开始部署 (Self-Hosted VPS)
|
| 44 |
+
|
| 45 |
+
### 1. 准备环境
|
| 46 |
+
确保服务器已安装 Docker 和 Docker Compose。
|
| 47 |
+
|
| 48 |
+
### 2. 配置环境
|
| 49 |
+
```bash
|
| 50 |
+
cp .env.example .env.production
|
| 51 |
+
# 编辑 .env.production 修改数据库密码、API Key 等敏感信息
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
### 3. 执行部署
|
| 55 |
+
```bash
|
| 56 |
+
chmod +x scripts/deploy.sh
|
| 57 |
+
./scripts/deploy.sh
|
| 58 |
+
```
|
| 59 |
+
|
| 60 |
+
---
|
| 61 |
+
|
| 62 |
+
## ❓ 常见问题 FAQ
|
| 63 |
+
|
| 64 |
+
**Q: 为什么不直接用 Vercel?**
|
| 65 |
+
A: Vercel 非常适合前端和简单的 Serverless 函数。但本项目包含 **Redis 队列 (BullMQ)**、**WebSocket (Socket.io)** 和 **长运行的后台任务**,这些在 Vercel 的 Serverless 环境下会受到严格限制。使用 Docker 部署可以提供更稳定的实时性支持。
|
| 66 |
+
|
| 67 |
+
**Q: Hugging Face 有什么局限性?**
|
| 68 |
+
A: Hugging Face Spaces 适合演示,但管理多容器(如自带 Postgres 和 Redis)比较复杂。对于需要长期稳定运行、自有域名和灵活运维的“平台级”应用,VPS + Docker 是更好的选择。
|
Dockerfile
CHANGED
|
@@ -1,44 +1,40 @@
|
|
| 1 |
-
#
|
| 2 |
FROM node:20-slim AS base
|
| 3 |
-
|
| 4 |
-
|
|
|
|
| 5 |
WORKDIR /app
|
| 6 |
|
| 7 |
-
# 安装
|
| 8 |
-
|
|
|
|
|
|
|
| 9 |
|
| 10 |
# --- 构建阶段 ---
|
| 11 |
FROM base AS builder
|
| 12 |
-
|
| 13 |
-
# 复制依赖定义
|
| 14 |
-
COPY package.json pnpm-lock.yaml ./
|
| 15 |
-
|
| 16 |
-
# 安装依赖
|
| 17 |
-
RUN pnpm install
|
| 18 |
-
|
| 19 |
-
# 复制源代码
|
| 20 |
COPY . .
|
| 21 |
-
|
| 22 |
# 构建前端和后端
|
| 23 |
RUN pnpm run build
|
|
|
|
|
|
|
| 24 |
|
| 25 |
# --- 运行阶段 ---
|
| 26 |
FROM base AS runner
|
|
|
|
|
|
|
| 27 |
|
| 28 |
-
|
|
|
|
| 29 |
|
| 30 |
# 复制构建产物和必要的运行文件
|
| 31 |
COPY --from=builder /app/dist ./dist
|
| 32 |
COPY --from=builder /app/package.json ./package.json
|
| 33 |
-
COPY --from=builder /app/pnpm-lock.yaml ./pnpm-lock.yaml
|
| 34 |
COPY --from=builder /app/node_modules ./node_modules
|
| 35 |
COPY --from=builder /app/api ./api
|
| 36 |
-
COPY --from=builder /app/.env ./.env
|
| 37 |
|
| 38 |
-
# 暴露端口
|
| 39 |
EXPOSE 7860
|
| 40 |
-
ENV PORT=7860
|
| 41 |
-
ENV NODE_ENV=production
|
| 42 |
|
| 43 |
# 启动命令
|
| 44 |
CMD ["node", "dist/api/api/server.js"]
|
|
|
|
| 1 |
+
# --- 基础镜像 ---
|
| 2 |
FROM node:20-slim AS base
|
| 3 |
+
ENV PNPM_HOME="/pnpm"
|
| 4 |
+
ENV PATH="$PNPM_HOME:$PATH"
|
| 5 |
+
RUN corepack enable
|
| 6 |
WORKDIR /app
|
| 7 |
|
| 8 |
+
# --- 依赖安装阶段 ---
|
| 9 |
+
FROM base AS deps
|
| 10 |
+
COPY package.json pnpm-lock.yaml ./
|
| 11 |
+
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
| 12 |
|
| 13 |
# --- 构建阶段 ---
|
| 14 |
FROM base AS builder
|
| 15 |
+
COPY --from=deps /app/node_modules ./node_modules
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
COPY . .
|
|
|
|
| 17 |
# 构建前端和后端
|
| 18 |
RUN pnpm run build
|
| 19 |
+
# 只保留生产依赖以减小镜像体积
|
| 20 |
+
RUN pnpm prune --prod
|
| 21 |
|
| 22 |
# --- 运行阶段 ---
|
| 23 |
FROM base AS runner
|
| 24 |
+
ENV NODE_ENV=production
|
| 25 |
+
ENV PORT=7860
|
| 26 |
|
| 27 |
+
# 创建数据目录并设置权限 (适配 Hugging Face Spaces)
|
| 28 |
+
RUN mkdir -p /app/data /app/uploads && chmod -R 777 /app/data /app/uploads
|
| 29 |
|
| 30 |
# 复制构建产物和必要的运行文件
|
| 31 |
COPY --from=builder /app/dist ./dist
|
| 32 |
COPY --from=builder /app/package.json ./package.json
|
|
|
|
| 33 |
COPY --from=builder /app/node_modules ./node_modules
|
| 34 |
COPY --from=builder /app/api ./api
|
|
|
|
| 35 |
|
| 36 |
+
# 暴露端口
|
| 37 |
EXPOSE 7860
|
|
|
|
|
|
|
| 38 |
|
| 39 |
# 启动命令
|
| 40 |
CMD ["node", "dist/api/api/server.js"]
|
Docs/BUILD_GUIDE.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Codex AI Platform 多端打包指南
|
| 2 |
+
|
| 3 |
+
本手册提供了在不同操作系统和设备上打包应用程序的完整指令。
|
| 4 |
+
|
| 5 |
+
## 1. 桌面端 (Tauri)
|
| 6 |
+
Tauri 支持打包为 macOS (.app/dmg), Windows (.exe/msi) 和 Linux (.deb/appimage)。
|
| 7 |
+
|
| 8 |
+
### macOS (Mac)
|
| 9 |
+
```bash
|
| 10 |
+
# 开发模式
|
| 11 |
+
pnpm tauri dev
|
| 12 |
+
|
| 13 |
+
# 打包正式版 (生成 .app 和 .dmg)
|
| 14 |
+
pnpm tauri build
|
| 15 |
+
```
|
| 16 |
+
|
| 17 |
+
### Windows
|
| 18 |
+
> 注意:需要在 Windows 环境下运行。
|
| 19 |
+
```bash
|
| 20 |
+
# 开发模式
|
| 21 |
+
pnpm tauri dev
|
| 22 |
+
|
| 23 |
+
# 打包正式版 (生成 .exe 和 .msi)
|
| 24 |
+
pnpm tauri build
|
| 25 |
+
```
|
| 26 |
+
|
| 27 |
+
## 2. 移动端 (Capacitor)
|
| 28 |
+
用于打包 iOS (iPhone/iPad) 和 Android。
|
| 29 |
+
|
| 30 |
+
### 准备工作
|
| 31 |
+
每次代码改动后,需先构建 Web 代码并同步到原生项目:
|
| 32 |
+
```bash
|
| 33 |
+
pnpm run build
|
| 34 |
+
pnpm cap:sync
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
### iOS / iPadOS (iPhone & iPad)
|
| 38 |
+
> 注意:必须在 macOS 且安装了 Xcode 的环境下运行。
|
| 39 |
+
```bash
|
| 40 |
+
# 打开 Xcode 项目进行调试和打包
|
| 41 |
+
pnpm cap:open:ios
|
| 42 |
+
```
|
| 43 |
+
- **iPad 适配**: 本项目已集成响应式设计,在 iPad 上会自动切换至平板布局(侧边栏常驻)。
|
| 44 |
+
- **打包步骤**: 在 Xcode 中,选择 `Generic iOS Device`,点击 `Product -> Archive` 进行打包发布。
|
| 45 |
+
|
| 46 |
+
### Android
|
| 47 |
+
> 注意:需安装 Android Studio。
|
| 48 |
+
```bash
|
| 49 |
+
# 打开 Android Studio 项目进行调试和打包
|
| 50 |
+
pnpm cap:open:android
|
| 51 |
+
```
|
| 52 |
+
- **打包步骤**: 在 Android Studio 中,点击 `Build -> Build Bundle(s) / APK(s) -> Build APK(s)`。
|
| 53 |
+
|
| 54 |
+
## 3. 打包命令总结 (Cheat Sheet)
|
| 55 |
+
|
| 56 |
+
| 平台 | 工具 | 构建命令 | 备注 |
|
| 57 |
+
| :--- | :--- | :--- | :--- |
|
| 58 |
+
| **Web / H5** | Vite | `pnpm run build` | 输出到 `dist/` |
|
| 59 |
+
| **Windows** | Tauri | `pnpm tauri build` | 需 Windows 环境 |
|
| 60 |
+
| **macOS (Mac)** | Tauri | `pnpm tauri build` | 需 macOS 环境 |
|
| 61 |
+
| **iPhone / iPad** | Capacitor | `pnpm cap:open:ios` | 需 Xcode |
|
| 62 |
+
| **Android** | Capacitor | `pnpm cap:open:android` | 需 Android Studio |
|
| 63 |
+
| **小程序** | Taro | `pnpm run build:weapp` | 见 `Docs/MINI_PROGRAM_GUIDE.md` |
|
| 64 |
+
|
| 65 |
+
## 4. 常见问题排查
|
| 66 |
+
|
| 67 |
+
### 登录失败 / ECONNREFUSED
|
| 68 |
+
这通常是因为后端服务未启动或端口不匹配:
|
| 69 |
+
1. **确保后端已启动**: 运行 `pnpm run dev` 会同时启动前后端。
|
| 70 |
+
2. **检查端口**:
|
| 71 |
+
- 后端端口(`.env` 中的 `PORT`)应为 `7865`。
|
| 72 |
+
- 前端代理(`vite.config.ts` 中的 `proxy.target`)应指向同一端口。
|
| 73 |
+
3. **数据库**: 确保本地 `platform.db` (SQLite) 权限正确。
|
| 74 |
+
|
| 75 |
+
### 页面空白 (Mac/Windows)
|
| 76 |
+
- 打开开发者工具: `Cmd+Option+I` (Mac) 或 `Ctrl+Shift+I` (Windows)。
|
| 77 |
+
- 检查 Console 报错。如果是 `Module Not Found`,请运行 `pnpm install`。
|
Docs/MINI_PROGRAM_GUIDE.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 微信小程序开发指南
|
| 2 |
+
|
| 3 |
+
针对 Codex AI Platform,建议采用 **Taro** 框架进行小程序开发,因为它支持使用 React 语法,并能最大程度复用现有的业务逻辑和 Store。
|
| 4 |
+
|
| 5 |
+
## 1. 环境准备
|
| 6 |
+
|
| 7 |
+
- **Node.js**: 建议 v18+
|
| 8 |
+
- **WeChat DevTools**: 下载并安装 [微信开发者工具](https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html)
|
| 9 |
+
- **Taro CLI**:
|
| 10 |
+
```bash
|
| 11 |
+
pnpm add -g @tarojs/cli
|
| 12 |
+
```
|
| 13 |
+
|
| 14 |
+
## 2. 初始化小程序项目 (建议另建目录)
|
| 15 |
+
|
| 16 |
+
由于小程序对包体积和 API 有特殊限制,建议在根目录下创建一个 `mini-app` 文件夹或独立项目:
|
| 17 |
+
|
| 18 |
+
```bash
|
| 19 |
+
taro init mini-app
|
| 20 |
+
# 选择 React, TypeScript, Tailwind CSS
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
## 3. 复用核心逻辑
|
| 24 |
+
|
| 25 |
+
你可以通过 `path alias` 或 `monorepo` 的方式复用主项目中的以下内容:
|
| 26 |
+
- `shared/types`: 类型定义
|
| 27 |
+
- `src/store/useStore.ts`: 状态管理(需注意小程序不支持 `localStorage`,需替换为 `Taro.getStorageSync`)
|
| 28 |
+
- `api/`: API 定义
|
| 29 |
+
|
| 30 |
+
## 4. 启动开发
|
| 31 |
+
|
| 32 |
+
1. **编译代码**:
|
| 33 |
+
```bash
|
| 34 |
+
cd mini-app
|
| 35 |
+
pnpm run dev:weapp
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
2. **预览**:
|
| 39 |
+
- 打开微信开发者工具
|
| 40 |
+
- 导入 `mini-app` 文件夹
|
| 41 |
+
- 在工具中即可看到预览效果
|
| 42 |
+
|
| 43 |
+
## 5. 注意事项
|
| 44 |
+
|
| 45 |
+
- **API 域名**: 小程序必须在管理后台配置服务器域名,且必须是 HTTPS。
|
| 46 |
+
- **本地调试**: 在开发者工具中勾选“不校验合法域名”即可在本地 `localhost` 调试。
|
| 47 |
+
- **文件监听**: 小程序环境不支持原生文件监听功能,该功能仅限桌面端 (Tauri) 使用。
|
| 48 |
+
- **Local-First**: RxDB 支持在小程序环境运行,但需要配置对应的 `storage` 适配器(如 `rxdb-adapter-weapp`)。
|
STRESS_TEST_PLAN.md → Docs/STRESS_TEST_PLAN.md
RENAMED
|
File without changes
|
README.md
CHANGED
|
@@ -6,7 +6,7 @@ colorTo: indigo
|
|
| 6 |
sdk: docker
|
| 7 |
app_port: 7860
|
| 8 |
pinned: false
|
| 9 |
-
short_description:
|
| 10 |
---
|
| 11 |
|
| 12 |
# Codex AI 平台 (Codex AI Platform)
|
|
@@ -15,6 +15,7 @@ short_description: Codex AI 平台
|
|
| 15 |
|
| 16 |
## 🛡️ 技术特点
|
| 17 |
- **AI 驱动**:使用 `SiliconFlow` 提供的 Qwen2.5 免费模型,支持流式对话。
|
|
|
|
| 18 |
- **高性能架构**:基于 `Express` + `Vite` 构建,支持快速响应。
|
| 19 |
- **可视化拓扑**:基于 `React Flow` 构建工作流引擎。
|
| 20 |
- **容器化部署**:支持 Docker 一键部署至 Hugging Face Spaces。
|
|
@@ -39,6 +40,31 @@ pnpm install
|
|
| 39 |
pnpm run dev
|
| 40 |
```
|
| 41 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
## ⚡️ 高并发压测指南 (Performance Testing)
|
| 43 |
|
| 44 |
本系统针对年会、大抽奖等“万人级”高并发场景进行了极致优化。由于浏览器对同域名并发连接数有限制(通常为 6-10),网页端的测试结果仅反映浏览器性能,无法体现后端真实战力。
|
|
|
|
| 6 |
sdk: docker
|
| 7 |
app_port: 7860
|
| 8 |
pinned: false
|
| 9 |
+
short_description: 全栈 AI 平台,支持 RAG、工作流编排与高并发压测,适配 Docker 部署。
|
| 10 |
---
|
| 11 |
|
| 12 |
# Codex AI 平台 (Codex AI Platform)
|
|
|
|
| 15 |
|
| 16 |
## 🛡️ 技术特点
|
| 17 |
- **AI 驱动**:使用 `SiliconFlow` 提供的 Qwen2.5 免费模型,支持流式对话。
|
| 18 |
+
- **智能知识库**:支持 PDF/Word/Markdown 文件上传,以及**网页 URL 自动抓取**与向量化处理。
|
| 19 |
- **高性能架构**:基于 `Express` + `Vite` 构建,支持快速响应。
|
| 20 |
- **可视化拓扑**:基于 `React Flow` 构建工作流引擎。
|
| 21 |
- **容器化部署**:支持 Docker 一键部署至 Hugging Face Spaces。
|
|
|
|
| 40 |
pnpm run dev
|
| 41 |
```
|
| 42 |
|
| 43 |
+
## 🌐 Hugging Face Spaces 部署
|
| 44 |
+
|
| 45 |
+
本项目已针对 Hugging Face Spaces (Docker) 进行优化,支持一键部署。
|
| 46 |
+
|
| 47 |
+
### 1. 创建 Space
|
| 48 |
+
- 前往 [Hugging Face Spaces](https://huggingface.co/spaces) -> `Create new Space`。
|
| 49 |
+
- `SDK` 选择 **Docker** -> `Blank`。
|
| 50 |
+
- `License` 选择 **MIT**。
|
| 51 |
+
|
| 52 |
+
### 2. 推送代码
|
| 53 |
+
你可以通过以下两种方式推送代码:
|
| 54 |
+
- **手动推送**: 运行 `scripts/hf_deploy.sh` 脚本进行推送。
|
| 55 |
+
- **GitHub 同步**:
|
| 56 |
+
- 将代码推送至你的 GitHub 仓库。
|
| 57 |
+
- 在 GitHub Settings -> Secrets 中添加 `HF_TOKEN` (你的 Hugging Face Access Token)。
|
| 58 |
+
- 在 GitHub Settings -> Secrets 中添加 `HF_SPACE_NAME` (格式如: `username/space-name`)。
|
| 59 |
+
|
| 60 |
+
### 3. 配置 Secrets
|
| 61 |
+
在 Hugging Face Space 的 `Settings` -> `Variables and Secrets` 中添加以下内容:
|
| 62 |
+
- `OPENAI_API_KEY`: 您的 API Key。
|
| 63 |
+
- `OPENAI_API_BASE_URL`: `https://api.siliconflow.cn/v1`。
|
| 64 |
+
- `JWT_SECRET`: 任意随机字符串。
|
| 65 |
+
|
| 66 |
+
---
|
| 67 |
+
|
| 68 |
## ⚡️ 高并发压测指南 (Performance Testing)
|
| 69 |
|
| 70 |
本系统针对年会、大抽奖等“万人级”高并发场景进行了极致优化。由于浏览器对同域名并发连接数有限制(通常为 6-10),网页端的测试结果仅反映浏览器性能,无法体现后端真实战力。
|
android/.gitignore
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore
|
| 2 |
+
|
| 3 |
+
# Built application files
|
| 4 |
+
*.apk
|
| 5 |
+
*.aar
|
| 6 |
+
*.ap_
|
| 7 |
+
*.aab
|
| 8 |
+
|
| 9 |
+
# Files for the ART/Dalvik VM
|
| 10 |
+
*.dex
|
| 11 |
+
|
| 12 |
+
# Java class files
|
| 13 |
+
*.class
|
| 14 |
+
|
| 15 |
+
# Generated files
|
| 16 |
+
bin/
|
| 17 |
+
gen/
|
| 18 |
+
out/
|
| 19 |
+
# Uncomment the following line in case you need and you don't have the release build type files in your app
|
| 20 |
+
# release/
|
| 21 |
+
|
| 22 |
+
# Gradle files
|
| 23 |
+
.gradle/
|
| 24 |
+
build/
|
| 25 |
+
|
| 26 |
+
# Local configuration file (sdk path, etc)
|
| 27 |
+
local.properties
|
| 28 |
+
|
| 29 |
+
# Proguard folder generated by Eclipse
|
| 30 |
+
proguard/
|
| 31 |
+
|
| 32 |
+
# Log Files
|
| 33 |
+
*.log
|
| 34 |
+
|
| 35 |
+
# Android Studio Navigation editor temp files
|
| 36 |
+
.navigation/
|
| 37 |
+
|
| 38 |
+
# Android Studio captures folder
|
| 39 |
+
captures/
|
| 40 |
+
|
| 41 |
+
# IntelliJ
|
| 42 |
+
*.iml
|
| 43 |
+
.idea/workspace.xml
|
| 44 |
+
.idea/tasks.xml
|
| 45 |
+
.idea/gradle.xml
|
| 46 |
+
.idea/assetWizardSettings.xml
|
| 47 |
+
.idea/dictionaries
|
| 48 |
+
.idea/libraries
|
| 49 |
+
# Android Studio 3 in .gitignore file.
|
| 50 |
+
.idea/caches
|
| 51 |
+
.idea/modules.xml
|
| 52 |
+
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
|
| 53 |
+
.idea/navEditor.xml
|
| 54 |
+
|
| 55 |
+
# Keystore files
|
| 56 |
+
# Uncomment the following lines if you do not want to check your keystore files in.
|
| 57 |
+
#*.jks
|
| 58 |
+
#*.keystore
|
| 59 |
+
|
| 60 |
+
# External native build folder generated in Android Studio 2.2 and later
|
| 61 |
+
.externalNativeBuild
|
| 62 |
+
.cxx/
|
| 63 |
+
|
| 64 |
+
# Google Services (e.g. APIs or Firebase)
|
| 65 |
+
# google-services.json
|
| 66 |
+
|
| 67 |
+
# Freeline
|
| 68 |
+
freeline.py
|
| 69 |
+
freeline/
|
| 70 |
+
freeline_project_description.json
|
| 71 |
+
|
| 72 |
+
# fastlane
|
| 73 |
+
fastlane/report.xml
|
| 74 |
+
fastlane/Preview.html
|
| 75 |
+
fastlane/screenshots
|
| 76 |
+
fastlane/test_output
|
| 77 |
+
fastlane/readme.md
|
| 78 |
+
|
| 79 |
+
# Version control
|
| 80 |
+
vcs.xml
|
| 81 |
+
|
| 82 |
+
# lint
|
| 83 |
+
lint/intermediates/
|
| 84 |
+
lint/generated/
|
| 85 |
+
lint/outputs/
|
| 86 |
+
lint/tmp/
|
| 87 |
+
# lint/reports/
|
| 88 |
+
|
| 89 |
+
# Android Profiling
|
| 90 |
+
*.hprof
|
| 91 |
+
|
| 92 |
+
# Cordova plugins for Capacitor
|
| 93 |
+
capacitor-cordova-android-plugins
|
| 94 |
+
|
| 95 |
+
# Copied web assets
|
| 96 |
+
app/src/main/assets/public
|
| 97 |
+
|
| 98 |
+
# Generated Config files
|
| 99 |
+
app/src/main/assets/capacitor.config.json
|
| 100 |
+
app/src/main/assets/capacitor.plugins.json
|
| 101 |
+
app/src/main/res/xml/config.xml
|
android/app/.gitignore
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/build/*
|
| 2 |
+
!/build/.npmkeep
|
android/app/build.gradle
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
apply plugin: 'com.android.application'
|
| 2 |
+
|
| 3 |
+
android {
|
| 4 |
+
namespace = "com.codex.ai"
|
| 5 |
+
compileSdk = rootProject.ext.compileSdkVersion
|
| 6 |
+
defaultConfig {
|
| 7 |
+
applicationId "com.codex.ai"
|
| 8 |
+
minSdkVersion rootProject.ext.minSdkVersion
|
| 9 |
+
targetSdkVersion rootProject.ext.targetSdkVersion
|
| 10 |
+
versionCode 1
|
| 11 |
+
versionName "1.0"
|
| 12 |
+
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
| 13 |
+
aaptOptions {
|
| 14 |
+
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
| 15 |
+
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
|
| 16 |
+
ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
|
| 17 |
+
}
|
| 18 |
+
}
|
| 19 |
+
buildTypes {
|
| 20 |
+
release {
|
| 21 |
+
minifyEnabled false
|
| 22 |
+
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
| 23 |
+
}
|
| 24 |
+
}
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
repositories {
|
| 28 |
+
flatDir{
|
| 29 |
+
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
|
| 30 |
+
}
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
dependencies {
|
| 34 |
+
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
| 35 |
+
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
| 36 |
+
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
|
| 37 |
+
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
|
| 38 |
+
implementation project(':capacitor-android')
|
| 39 |
+
testImplementation "junit:junit:$junitVersion"
|
| 40 |
+
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
| 41 |
+
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
| 42 |
+
implementation project(':capacitor-cordova-android-plugins')
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
apply from: 'capacitor.build.gradle'
|
| 46 |
+
|
| 47 |
+
try {
|
| 48 |
+
def servicesJSON = file('google-services.json')
|
| 49 |
+
if (servicesJSON.text) {
|
| 50 |
+
apply plugin: 'com.google.gms.google-services'
|
| 51 |
+
}
|
| 52 |
+
} catch(Exception e) {
|
| 53 |
+
logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
|
| 54 |
+
}
|
android/app/capacitor.build.gradle
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
|
| 2 |
+
|
| 3 |
+
android {
|
| 4 |
+
compileOptions {
|
| 5 |
+
sourceCompatibility JavaVersion.VERSION_21
|
| 6 |
+
targetCompatibility JavaVersion.VERSION_21
|
| 7 |
+
}
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
| 11 |
+
dependencies {
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
if (hasProperty('postBuildExtras')) {
|
| 18 |
+
postBuildExtras()
|
| 19 |
+
}
|
android/app/proguard-rules.pro
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Add project specific ProGuard rules here.
|
| 2 |
+
# You can control the set of applied configuration files using the
|
| 3 |
+
# proguardFiles setting in build.gradle.
|
| 4 |
+
#
|
| 5 |
+
# For more details, see
|
| 6 |
+
# http://developer.android.com/guide/developing/tools/proguard.html
|
| 7 |
+
|
| 8 |
+
# If your project uses WebView with JS, uncomment the following
|
| 9 |
+
# and specify the fully qualified class name to the JavaScript interface
|
| 10 |
+
# class:
|
| 11 |
+
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
| 12 |
+
# public *;
|
| 13 |
+
#}
|
| 14 |
+
|
| 15 |
+
# Uncomment this to preserve the line number information for
|
| 16 |
+
# debugging stack traces.
|
| 17 |
+
#-keepattributes SourceFile,LineNumberTable
|
| 18 |
+
|
| 19 |
+
# If you keep the line number information, uncomment this to
|
| 20 |
+
# hide the original source file name.
|
| 21 |
+
#-renamesourcefileattribute SourceFile
|
android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.getcapacitor.myapp;
|
| 2 |
+
|
| 3 |
+
import static org.junit.Assert.*;
|
| 4 |
+
|
| 5 |
+
import android.content.Context;
|
| 6 |
+
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
| 7 |
+
import androidx.test.platform.app.InstrumentationRegistry;
|
| 8 |
+
import org.junit.Test;
|
| 9 |
+
import org.junit.runner.RunWith;
|
| 10 |
+
|
| 11 |
+
/**
|
| 12 |
+
* Instrumented test, which will execute on an Android device.
|
| 13 |
+
*
|
| 14 |
+
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
| 15 |
+
*/
|
| 16 |
+
@RunWith(AndroidJUnit4.class)
|
| 17 |
+
public class ExampleInstrumentedTest {
|
| 18 |
+
|
| 19 |
+
@Test
|
| 20 |
+
public void useAppContext() throws Exception {
|
| 21 |
+
// Context of the app under test.
|
| 22 |
+
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
|
| 23 |
+
|
| 24 |
+
assertEquals("com.getcapacitor.app", appContext.getPackageName());
|
| 25 |
+
}
|
| 26 |
+
}
|
android/app/src/main/AndroidManifest.xml
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?xml version="1.0" encoding="utf-8"?>
|
| 2 |
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
| 3 |
+
|
| 4 |
+
<application
|
| 5 |
+
android:allowBackup="true"
|
| 6 |
+
android:icon="@mipmap/ic_launcher"
|
| 7 |
+
android:label="@string/app_name"
|
| 8 |
+
android:roundIcon="@mipmap/ic_launcher_round"
|
| 9 |
+
android:supportsRtl="true"
|
| 10 |
+
android:theme="@style/AppTheme">
|
| 11 |
+
|
| 12 |
+
<activity
|
| 13 |
+
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation|density"
|
| 14 |
+
android:name=".MainActivity"
|
| 15 |
+
android:label="@string/title_activity_main"
|
| 16 |
+
android:theme="@style/AppTheme.NoActionBarLaunch"
|
| 17 |
+
android:launchMode="singleTask"
|
| 18 |
+
android:exported="true">
|
| 19 |
+
|
| 20 |
+
<intent-filter>
|
| 21 |
+
<action android:name="android.intent.action.MAIN" />
|
| 22 |
+
<category android:name="android.intent.category.LAUNCHER" />
|
| 23 |
+
</intent-filter>
|
| 24 |
+
|
| 25 |
+
</activity>
|
| 26 |
+
|
| 27 |
+
<provider
|
| 28 |
+
android:name="androidx.core.content.FileProvider"
|
| 29 |
+
android:authorities="${applicationId}.fileprovider"
|
| 30 |
+
android:exported="false"
|
| 31 |
+
android:grantUriPermissions="true">
|
| 32 |
+
<meta-data
|
| 33 |
+
android:name="android.support.FILE_PROVIDER_PATHS"
|
| 34 |
+
android:resource="@xml/file_paths"></meta-data>
|
| 35 |
+
</provider>
|
| 36 |
+
</application>
|
| 37 |
+
|
| 38 |
+
<!-- Permissions -->
|
| 39 |
+
|
| 40 |
+
<uses-permission android:name="android.permission.INTERNET" />
|
| 41 |
+
</manifest>
|
android/app/src/main/java/com/codex/ai/MainActivity.java
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.codex.ai;
|
| 2 |
+
|
| 3 |
+
import com.getcapacitor.BridgeActivity;
|
| 4 |
+
|
| 5 |
+
public class MainActivity extends BridgeActivity {}
|
android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
| 2 |
+
xmlns:aapt="http://schemas.android.com/aapt"
|
| 3 |
+
android:width="108dp"
|
| 4 |
+
android:height="108dp"
|
| 5 |
+
android:viewportHeight="108"
|
| 6 |
+
android:viewportWidth="108">
|
| 7 |
+
<path
|
| 8 |
+
android:fillType="evenOdd"
|
| 9 |
+
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
|
| 10 |
+
android:strokeColor="#00000000"
|
| 11 |
+
android:strokeWidth="1">
|
| 12 |
+
<aapt:attr name="android:fillColor">
|
| 13 |
+
<gradient
|
| 14 |
+
android:endX="78.5885"
|
| 15 |
+
android:endY="90.9159"
|
| 16 |
+
android:startX="48.7653"
|
| 17 |
+
android:startY="61.0927"
|
| 18 |
+
android:type="linear">
|
| 19 |
+
<item
|
| 20 |
+
android:color="#44000000"
|
| 21 |
+
android:offset="0.0" />
|
| 22 |
+
<item
|
| 23 |
+
android:color="#00000000"
|
| 24 |
+
android:offset="1.0" />
|
| 25 |
+
</gradient>
|
| 26 |
+
</aapt:attr>
|
| 27 |
+
</path>
|
| 28 |
+
<path
|
| 29 |
+
android:fillColor="#FFFFFF"
|
| 30 |
+
android:fillType="nonZero"
|
| 31 |
+
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
|
| 32 |
+
android:strokeColor="#00000000"
|
| 33 |
+
android:strokeWidth="1" />
|
| 34 |
+
</vector>
|
android/app/src/main/res/drawable/ic_launcher_background.xml
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?xml version="1.0" encoding="utf-8"?>
|
| 2 |
+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
| 3 |
+
android:width="108dp"
|
| 4 |
+
android:height="108dp"
|
| 5 |
+
android:viewportHeight="108"
|
| 6 |
+
android:viewportWidth="108">
|
| 7 |
+
<path
|
| 8 |
+
android:fillColor="#26A69A"
|
| 9 |
+
android:pathData="M0,0h108v108h-108z" />
|
| 10 |
+
<path
|
| 11 |
+
android:fillColor="#00000000"
|
| 12 |
+
android:pathData="M9,0L9,108"
|
| 13 |
+
android:strokeColor="#33FFFFFF"
|
| 14 |
+
android:strokeWidth="0.8" />
|
| 15 |
+
<path
|
| 16 |
+
android:fillColor="#00000000"
|
| 17 |
+
android:pathData="M19,0L19,108"
|
| 18 |
+
android:strokeColor="#33FFFFFF"
|
| 19 |
+
android:strokeWidth="0.8" />
|
| 20 |
+
<path
|
| 21 |
+
android:fillColor="#00000000"
|
| 22 |
+
android:pathData="M29,0L29,108"
|
| 23 |
+
android:strokeColor="#33FFFFFF"
|
| 24 |
+
android:strokeWidth="0.8" />
|
| 25 |
+
<path
|
| 26 |
+
android:fillColor="#00000000"
|
| 27 |
+
android:pathData="M39,0L39,108"
|
| 28 |
+
android:strokeColor="#33FFFFFF"
|
| 29 |
+
android:strokeWidth="0.8" />
|
| 30 |
+
<path
|
| 31 |
+
android:fillColor="#00000000"
|
| 32 |
+
android:pathData="M49,0L49,108"
|
| 33 |
+
android:strokeColor="#33FFFFFF"
|
| 34 |
+
android:strokeWidth="0.8" />
|
| 35 |
+
<path
|
| 36 |
+
android:fillColor="#00000000"
|
| 37 |
+
android:pathData="M59,0L59,108"
|
| 38 |
+
android:strokeColor="#33FFFFFF"
|
| 39 |
+
android:strokeWidth="0.8" />
|
| 40 |
+
<path
|
| 41 |
+
android:fillColor="#00000000"
|
| 42 |
+
android:pathData="M69,0L69,108"
|
| 43 |
+
android:strokeColor="#33FFFFFF"
|
| 44 |
+
android:strokeWidth="0.8" />
|
| 45 |
+
<path
|
| 46 |
+
android:fillColor="#00000000"
|
| 47 |
+
android:pathData="M79,0L79,108"
|
| 48 |
+
android:strokeColor="#33FFFFFF"
|
| 49 |
+
android:strokeWidth="0.8" />
|
| 50 |
+
<path
|
| 51 |
+
android:fillColor="#00000000"
|
| 52 |
+
android:pathData="M89,0L89,108"
|
| 53 |
+
android:strokeColor="#33FFFFFF"
|
| 54 |
+
android:strokeWidth="0.8" />
|
| 55 |
+
<path
|
| 56 |
+
android:fillColor="#00000000"
|
| 57 |
+
android:pathData="M99,0L99,108"
|
| 58 |
+
android:strokeColor="#33FFFFFF"
|
| 59 |
+
android:strokeWidth="0.8" />
|
| 60 |
+
<path
|
| 61 |
+
android:fillColor="#00000000"
|
| 62 |
+
android:pathData="M0,9L108,9"
|
| 63 |
+
android:strokeColor="#33FFFFFF"
|
| 64 |
+
android:strokeWidth="0.8" />
|
| 65 |
+
<path
|
| 66 |
+
android:fillColor="#00000000"
|
| 67 |
+
android:pathData="M0,19L108,19"
|
| 68 |
+
android:strokeColor="#33FFFFFF"
|
| 69 |
+
android:strokeWidth="0.8" />
|
| 70 |
+
<path
|
| 71 |
+
android:fillColor="#00000000"
|
| 72 |
+
android:pathData="M0,29L108,29"
|
| 73 |
+
android:strokeColor="#33FFFFFF"
|
| 74 |
+
android:strokeWidth="0.8" />
|
| 75 |
+
<path
|
| 76 |
+
android:fillColor="#00000000"
|
| 77 |
+
android:pathData="M0,39L108,39"
|
| 78 |
+
android:strokeColor="#33FFFFFF"
|
| 79 |
+
android:strokeWidth="0.8" />
|
| 80 |
+
<path
|
| 81 |
+
android:fillColor="#00000000"
|
| 82 |
+
android:pathData="M0,49L108,49"
|
| 83 |
+
android:strokeColor="#33FFFFFF"
|
| 84 |
+
android:strokeWidth="0.8" />
|
| 85 |
+
<path
|
| 86 |
+
android:fillColor="#00000000"
|
| 87 |
+
android:pathData="M0,59L108,59"
|
| 88 |
+
android:strokeColor="#33FFFFFF"
|
| 89 |
+
android:strokeWidth="0.8" />
|
| 90 |
+
<path
|
| 91 |
+
android:fillColor="#00000000"
|
| 92 |
+
android:pathData="M0,69L108,69"
|
| 93 |
+
android:strokeColor="#33FFFFFF"
|
| 94 |
+
android:strokeWidth="0.8" />
|
| 95 |
+
<path
|
| 96 |
+
android:fillColor="#00000000"
|
| 97 |
+
android:pathData="M0,79L108,79"
|
| 98 |
+
android:strokeColor="#33FFFFFF"
|
| 99 |
+
android:strokeWidth="0.8" />
|
| 100 |
+
<path
|
| 101 |
+
android:fillColor="#00000000"
|
| 102 |
+
android:pathData="M0,89L108,89"
|
| 103 |
+
android:strokeColor="#33FFFFFF"
|
| 104 |
+
android:strokeWidth="0.8" />
|
| 105 |
+
<path
|
| 106 |
+
android:fillColor="#00000000"
|
| 107 |
+
android:pathData="M0,99L108,99"
|
| 108 |
+
android:strokeColor="#33FFFFFF"
|
| 109 |
+
android:strokeWidth="0.8" />
|
| 110 |
+
<path
|
| 111 |
+
android:fillColor="#00000000"
|
| 112 |
+
android:pathData="M19,29L89,29"
|
| 113 |
+
android:strokeColor="#33FFFFFF"
|
| 114 |
+
android:strokeWidth="0.8" />
|
| 115 |
+
<path
|
| 116 |
+
android:fillColor="#00000000"
|
| 117 |
+
android:pathData="M19,39L89,39"
|
| 118 |
+
android:strokeColor="#33FFFFFF"
|
| 119 |
+
android:strokeWidth="0.8" />
|
| 120 |
+
<path
|
| 121 |
+
android:fillColor="#00000000"
|
| 122 |
+
android:pathData="M19,49L89,49"
|
| 123 |
+
android:strokeColor="#33FFFFFF"
|
| 124 |
+
android:strokeWidth="0.8" />
|
| 125 |
+
<path
|
| 126 |
+
android:fillColor="#00000000"
|
| 127 |
+
android:pathData="M19,59L89,59"
|
| 128 |
+
android:strokeColor="#33FFFFFF"
|
| 129 |
+
android:strokeWidth="0.8" />
|
| 130 |
+
<path
|
| 131 |
+
android:fillColor="#00000000"
|
| 132 |
+
android:pathData="M19,69L89,69"
|
| 133 |
+
android:strokeColor="#33FFFFFF"
|
| 134 |
+
android:strokeWidth="0.8" />
|
| 135 |
+
<path
|
| 136 |
+
android:fillColor="#00000000"
|
| 137 |
+
android:pathData="M19,79L89,79"
|
| 138 |
+
android:strokeColor="#33FFFFFF"
|
| 139 |
+
android:strokeWidth="0.8" />
|
| 140 |
+
<path
|
| 141 |
+
android:fillColor="#00000000"
|
| 142 |
+
android:pathData="M29,19L29,89"
|
| 143 |
+
android:strokeColor="#33FFFFFF"
|
| 144 |
+
android:strokeWidth="0.8" />
|
| 145 |
+
<path
|
| 146 |
+
android:fillColor="#00000000"
|
| 147 |
+
android:pathData="M39,19L39,89"
|
| 148 |
+
android:strokeColor="#33FFFFFF"
|
| 149 |
+
android:strokeWidth="0.8" />
|
| 150 |
+
<path
|
| 151 |
+
android:fillColor="#00000000"
|
| 152 |
+
android:pathData="M49,19L49,89"
|
| 153 |
+
android:strokeColor="#33FFFFFF"
|
| 154 |
+
android:strokeWidth="0.8" />
|
| 155 |
+
<path
|
| 156 |
+
android:fillColor="#00000000"
|
| 157 |
+
android:pathData="M59,19L59,89"
|
| 158 |
+
android:strokeColor="#33FFFFFF"
|
| 159 |
+
android:strokeWidth="0.8" />
|
| 160 |
+
<path
|
| 161 |
+
android:fillColor="#00000000"
|
| 162 |
+
android:pathData="M69,19L69,89"
|
| 163 |
+
android:strokeColor="#33FFFFFF"
|
| 164 |
+
android:strokeWidth="0.8" />
|
| 165 |
+
<path
|
| 166 |
+
android:fillColor="#00000000"
|
| 167 |
+
android:pathData="M79,19L79,89"
|
| 168 |
+
android:strokeColor="#33FFFFFF"
|
| 169 |
+
android:strokeWidth="0.8" />
|
| 170 |
+
</vector>
|
android/app/src/main/res/layout/activity_main.xml
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?xml version="1.0" encoding="utf-8"?>
|
| 2 |
+
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
| 3 |
+
xmlns:app="http://schemas.android.com/apk/res-auto"
|
| 4 |
+
xmlns:tools="http://schemas.android.com/tools"
|
| 5 |
+
android:layout_width="match_parent"
|
| 6 |
+
android:layout_height="match_parent"
|
| 7 |
+
tools:context=".MainActivity">
|
| 8 |
+
|
| 9 |
+
<WebView
|
| 10 |
+
android:layout_width="match_parent"
|
| 11 |
+
android:layout_height="match_parent" />
|
| 12 |
+
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?xml version="1.0" encoding="utf-8"?>
|
| 2 |
+
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
| 3 |
+
<background android:drawable="@color/ic_launcher_background"/>
|
| 4 |
+
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
| 5 |
+
</adaptive-icon>
|
android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?xml version="1.0" encoding="utf-8"?>
|
| 2 |
+
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
| 3 |
+
<background android:drawable="@color/ic_launcher_background"/>
|
| 4 |
+
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
| 5 |
+
</adaptive-icon>
|
android/app/src/main/res/values/ic_launcher_background.xml
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?xml version="1.0" encoding="utf-8"?>
|
| 2 |
+
<resources>
|
| 3 |
+
<color name="ic_launcher_background">#FFFFFF</color>
|
| 4 |
+
</resources>
|
android/app/src/main/res/values/strings.xml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?xml version='1.0' encoding='utf-8'?>
|
| 2 |
+
<resources>
|
| 3 |
+
<string name="app_name">Codex AI Platform</string>
|
| 4 |
+
<string name="title_activity_main">Codex AI Platform</string>
|
| 5 |
+
<string name="package_name">com.codex.ai</string>
|
| 6 |
+
<string name="custom_url_scheme">com.codex.ai</string>
|
| 7 |
+
</resources>
|
android/app/src/main/res/values/styles.xml
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?xml version="1.0" encoding="utf-8"?>
|
| 2 |
+
<resources>
|
| 3 |
+
|
| 4 |
+
<!-- Base application theme. -->
|
| 5 |
+
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
| 6 |
+
<!-- Customize your theme here. -->
|
| 7 |
+
<item name="colorPrimary">@color/colorPrimary</item>
|
| 8 |
+
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
| 9 |
+
<item name="colorAccent">@color/colorAccent</item>
|
| 10 |
+
</style>
|
| 11 |
+
|
| 12 |
+
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
|
| 13 |
+
<item name="windowActionBar">false</item>
|
| 14 |
+
<item name="windowNoTitle">true</item>
|
| 15 |
+
<item name="android:background">@null</item>
|
| 16 |
+
</style>
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
|
| 20 |
+
<item name="android:background">@drawable/splash</item>
|
| 21 |
+
</style>
|
| 22 |
+
</resources>
|
android/app/src/main/res/xml/file_paths.xml
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?xml version="1.0" encoding="utf-8"?>
|
| 2 |
+
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
| 3 |
+
<external-path name="my_images" path="." />
|
| 4 |
+
<cache-path name="my_cache_images" path="." />
|
| 5 |
+
</paths>
|
android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
package com.getcapacitor.myapp;
|
| 2 |
+
|
| 3 |
+
import static org.junit.Assert.*;
|
| 4 |
+
|
| 5 |
+
import org.junit.Test;
|
| 6 |
+
|
| 7 |
+
/**
|
| 8 |
+
* Example local unit test, which will execute on the development machine (host).
|
| 9 |
+
*
|
| 10 |
+
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
| 11 |
+
*/
|
| 12 |
+
public class ExampleUnitTest {
|
| 13 |
+
|
| 14 |
+
@Test
|
| 15 |
+
public void addition_isCorrect() throws Exception {
|
| 16 |
+
assertEquals(4, 2 + 2);
|
| 17 |
+
}
|
| 18 |
+
}
|
android/build.gradle
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
| 2 |
+
|
| 3 |
+
buildscript {
|
| 4 |
+
|
| 5 |
+
repositories {
|
| 6 |
+
google()
|
| 7 |
+
mavenCentral()
|
| 8 |
+
}
|
| 9 |
+
dependencies {
|
| 10 |
+
classpath 'com.android.tools.build:gradle:8.13.0'
|
| 11 |
+
classpath 'com.google.gms:google-services:4.4.4'
|
| 12 |
+
|
| 13 |
+
// NOTE: Do not place your application dependencies here; they belong
|
| 14 |
+
// in the individual module build.gradle files
|
| 15 |
+
}
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
apply from: "variables.gradle"
|
| 19 |
+
|
| 20 |
+
allprojects {
|
| 21 |
+
repositories {
|
| 22 |
+
google()
|
| 23 |
+
mavenCentral()
|
| 24 |
+
}
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
task clean(type: Delete) {
|
| 28 |
+
delete rootProject.buildDir
|
| 29 |
+
}
|
android/capacitor.settings.gradle
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
|
| 2 |
+
include ':capacitor-android'
|
| 3 |
+
project(':capacitor-android').projectDir = new File('../node_modules/.pnpm/@capacitor+android@8.2.0_@capacitor+core@8.2.0/node_modules/@capacitor/android/capacitor')
|
android/gradle.properties
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Project-wide Gradle settings.
|
| 2 |
+
|
| 3 |
+
# IDE (e.g. Android Studio) users:
|
| 4 |
+
# Gradle settings configured through the IDE *will override*
|
| 5 |
+
# any settings specified in this file.
|
| 6 |
+
|
| 7 |
+
# For more details on how to configure your build environment visit
|
| 8 |
+
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
| 9 |
+
|
| 10 |
+
# Specifies the JVM arguments used for the daemon process.
|
| 11 |
+
# The setting is particularly useful for tweaking memory settings.
|
| 12 |
+
org.gradle.jvmargs=-Xmx1536m
|
| 13 |
+
|
| 14 |
+
# When configured, Gradle will run in incubating parallel mode.
|
| 15 |
+
# This option should only be used with decoupled projects. More details, visit
|
| 16 |
+
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
| 17 |
+
# org.gradle.parallel=true
|
| 18 |
+
|
| 19 |
+
# AndroidX package structure to make it clearer which packages are bundled with the
|
| 20 |
+
# Android operating system, and which are packaged with your app's APK
|
| 21 |
+
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
| 22 |
+
android.useAndroidX=true
|
| 23 |
+
org.gradle.java.home=/Library/Java/JavaVirtualMachines/temurin-21.jdk/Contents/Home
|
android/gradle/wrapper/gradle-wrapper.jar
ADDED
|
Binary file (43.8 kB). View file
|
|
|
android/gradle/wrapper/gradle-wrapper.properties
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
distributionBase=GRADLE_USER_HOME
|
| 2 |
+
distributionPath=wrapper/dists
|
| 3 |
+
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip
|
| 4 |
+
networkTimeout=10000
|
| 5 |
+
validateDistributionUrl=true
|
| 6 |
+
zipStoreBase=GRADLE_USER_HOME
|
| 7 |
+
zipStorePath=wrapper/dists
|
android/gradlew
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/sh
|
| 2 |
+
|
| 3 |
+
#
|
| 4 |
+
# Copyright © 2015-2021 the original authors.
|
| 5 |
+
#
|
| 6 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 7 |
+
# you may not use this file except in compliance with the License.
|
| 8 |
+
# You may obtain a copy of the License at
|
| 9 |
+
#
|
| 10 |
+
# https://www.apache.org/licenses/LICENSE-2.0
|
| 11 |
+
#
|
| 12 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 13 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 14 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 15 |
+
# See the License for the specific language governing permissions and
|
| 16 |
+
# limitations under the License.
|
| 17 |
+
#
|
| 18 |
+
# SPDX-License-Identifier: Apache-2.0
|
| 19 |
+
#
|
| 20 |
+
|
| 21 |
+
##############################################################################
|
| 22 |
+
#
|
| 23 |
+
# Gradle start up script for POSIX generated by Gradle.
|
| 24 |
+
#
|
| 25 |
+
# Important for running:
|
| 26 |
+
#
|
| 27 |
+
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
| 28 |
+
# noncompliant, but you have some other compliant shell such as ksh or
|
| 29 |
+
# bash, then to run this script, type that shell name before the whole
|
| 30 |
+
# command line, like:
|
| 31 |
+
#
|
| 32 |
+
# ksh Gradle
|
| 33 |
+
#
|
| 34 |
+
# Busybox and similar reduced shells will NOT work, because this script
|
| 35 |
+
# requires all of these POSIX shell features:
|
| 36 |
+
# * functions;
|
| 37 |
+
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
| 38 |
+
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
| 39 |
+
# * compound commands having a testable exit status, especially «case»;
|
| 40 |
+
# * various built-in commands including «command», «set», and «ulimit».
|
| 41 |
+
#
|
| 42 |
+
# Important for patching:
|
| 43 |
+
#
|
| 44 |
+
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
| 45 |
+
# by Bash, Ksh, etc; in particular arrays are avoided.
|
| 46 |
+
#
|
| 47 |
+
# The "traditional" practice of packing multiple parameters into a
|
| 48 |
+
# space-separated string is a well documented source of bugs and security
|
| 49 |
+
# problems, so this is (mostly) avoided, by progressively accumulating
|
| 50 |
+
# options in "$@", and eventually passing that to Java.
|
| 51 |
+
#
|
| 52 |
+
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
| 53 |
+
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
| 54 |
+
# see the in-line comments for details.
|
| 55 |
+
#
|
| 56 |
+
# There are tweaks for specific operating systems such as AIX, CygWin,
|
| 57 |
+
# Darwin, MinGW, and NonStop.
|
| 58 |
+
#
|
| 59 |
+
# (3) This script is generated from the Groovy template
|
| 60 |
+
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
| 61 |
+
# within the Gradle project.
|
| 62 |
+
#
|
| 63 |
+
# You can find Gradle at https://github.com/gradle/gradle/.
|
| 64 |
+
#
|
| 65 |
+
##############################################################################
|
| 66 |
+
|
| 67 |
+
# Attempt to set APP_HOME
|
| 68 |
+
|
| 69 |
+
# Resolve links: $0 may be a link
|
| 70 |
+
app_path=$0
|
| 71 |
+
|
| 72 |
+
# Need this for daisy-chained symlinks.
|
| 73 |
+
while
|
| 74 |
+
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
| 75 |
+
[ -h "$app_path" ]
|
| 76 |
+
do
|
| 77 |
+
ls=$( ls -ld "$app_path" )
|
| 78 |
+
link=${ls#*' -> '}
|
| 79 |
+
case $link in #(
|
| 80 |
+
/*) app_path=$link ;; #(
|
| 81 |
+
*) app_path=$APP_HOME$link ;;
|
| 82 |
+
esac
|
| 83 |
+
done
|
| 84 |
+
|
| 85 |
+
# This is normally unused
|
| 86 |
+
# shellcheck disable=SC2034
|
| 87 |
+
APP_BASE_NAME=${0##*/}
|
| 88 |
+
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
| 89 |
+
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
| 90 |
+
|
| 91 |
+
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
| 92 |
+
MAX_FD=maximum
|
| 93 |
+
|
| 94 |
+
warn () {
|
| 95 |
+
echo "$*"
|
| 96 |
+
} >&2
|
| 97 |
+
|
| 98 |
+
die () {
|
| 99 |
+
echo
|
| 100 |
+
echo "$*"
|
| 101 |
+
echo
|
| 102 |
+
exit 1
|
| 103 |
+
} >&2
|
| 104 |
+
|
| 105 |
+
# OS specific support (must be 'true' or 'false').
|
| 106 |
+
cygwin=false
|
| 107 |
+
msys=false
|
| 108 |
+
darwin=false
|
| 109 |
+
nonstop=false
|
| 110 |
+
case "$( uname )" in #(
|
| 111 |
+
CYGWIN* ) cygwin=true ;; #(
|
| 112 |
+
Darwin* ) darwin=true ;; #(
|
| 113 |
+
MSYS* | MINGW* ) msys=true ;; #(
|
| 114 |
+
NONSTOP* ) nonstop=true ;;
|
| 115 |
+
esac
|
| 116 |
+
|
| 117 |
+
CLASSPATH="\\\"\\\""
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
# Determine the Java command to use to start the JVM.
|
| 121 |
+
if [ -n "$JAVA_HOME" ] ; then
|
| 122 |
+
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
| 123 |
+
# IBM's JDK on AIX uses strange locations for the executables
|
| 124 |
+
JAVACMD=$JAVA_HOME/jre/sh/java
|
| 125 |
+
else
|
| 126 |
+
JAVACMD=$JAVA_HOME/bin/java
|
| 127 |
+
fi
|
| 128 |
+
if [ ! -x "$JAVACMD" ] ; then
|
| 129 |
+
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
| 130 |
+
|
| 131 |
+
Please set the JAVA_HOME variable in your environment to match the
|
| 132 |
+
location of your Java installation."
|
| 133 |
+
fi
|
| 134 |
+
else
|
| 135 |
+
JAVACMD=java
|
| 136 |
+
if ! command -v java >/dev/null 2>&1
|
| 137 |
+
then
|
| 138 |
+
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
| 139 |
+
|
| 140 |
+
Please set the JAVA_HOME variable in your environment to match the
|
| 141 |
+
location of your Java installation."
|
| 142 |
+
fi
|
| 143 |
+
fi
|
| 144 |
+
|
| 145 |
+
# Increase the maximum file descriptors if we can.
|
| 146 |
+
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
| 147 |
+
case $MAX_FD in #(
|
| 148 |
+
max*)
|
| 149 |
+
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
| 150 |
+
# shellcheck disable=SC2039,SC3045
|
| 151 |
+
MAX_FD=$( ulimit -H -n ) ||
|
| 152 |
+
warn "Could not query maximum file descriptor limit"
|
| 153 |
+
esac
|
| 154 |
+
case $MAX_FD in #(
|
| 155 |
+
'' | soft) :;; #(
|
| 156 |
+
*)
|
| 157 |
+
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
| 158 |
+
# shellcheck disable=SC2039,SC3045
|
| 159 |
+
ulimit -n "$MAX_FD" ||
|
| 160 |
+
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
| 161 |
+
esac
|
| 162 |
+
fi
|
| 163 |
+
|
| 164 |
+
# Collect all arguments for the java command, stacking in reverse order:
|
| 165 |
+
# * args from the command line
|
| 166 |
+
# * the main class name
|
| 167 |
+
# * -classpath
|
| 168 |
+
# * -D...appname settings
|
| 169 |
+
# * --module-path (only if needed)
|
| 170 |
+
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
| 171 |
+
|
| 172 |
+
# For Cygwin or MSYS, switch paths to Windows format before running java
|
| 173 |
+
if "$cygwin" || "$msys" ; then
|
| 174 |
+
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
| 175 |
+
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
| 176 |
+
|
| 177 |
+
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
| 178 |
+
|
| 179 |
+
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
| 180 |
+
for arg do
|
| 181 |
+
if
|
| 182 |
+
case $arg in #(
|
| 183 |
+
-*) false ;; # don't mess with options #(
|
| 184 |
+
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
| 185 |
+
[ -e "$t" ] ;; #(
|
| 186 |
+
*) false ;;
|
| 187 |
+
esac
|
| 188 |
+
then
|
| 189 |
+
arg=$( cygpath --path --ignore --mixed "$arg" )
|
| 190 |
+
fi
|
| 191 |
+
# Roll the args list around exactly as many times as the number of
|
| 192 |
+
# args, so each arg winds up back in the position where it started, but
|
| 193 |
+
# possibly modified.
|
| 194 |
+
#
|
| 195 |
+
# NB: a `for` loop captures its iteration list before it begins, so
|
| 196 |
+
# changing the positional parameters here affects neither the number of
|
| 197 |
+
# iterations, nor the values presented in `arg`.
|
| 198 |
+
shift # remove old arg
|
| 199 |
+
set -- "$@" "$arg" # push replacement arg
|
| 200 |
+
done
|
| 201 |
+
fi
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
| 205 |
+
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
| 206 |
+
|
| 207 |
+
# Collect all arguments for the java command:
|
| 208 |
+
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
| 209 |
+
# and any embedded shellness will be escaped.
|
| 210 |
+
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
| 211 |
+
# treated as '${Hostname}' itself on the command line.
|
| 212 |
+
|
| 213 |
+
set -- \
|
| 214 |
+
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
| 215 |
+
-classpath "$CLASSPATH" \
|
| 216 |
+
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
| 217 |
+
"$@"
|
| 218 |
+
|
| 219 |
+
# Stop when "xargs" is not available.
|
| 220 |
+
if ! command -v xargs >/dev/null 2>&1
|
| 221 |
+
then
|
| 222 |
+
die "xargs is not available"
|
| 223 |
+
fi
|
| 224 |
+
|
| 225 |
+
# Use "xargs" to parse quoted args.
|
| 226 |
+
#
|
| 227 |
+
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
| 228 |
+
#
|
| 229 |
+
# In Bash we could simply go:
|
| 230 |
+
#
|
| 231 |
+
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
| 232 |
+
# set -- "${ARGS[@]}" "$@"
|
| 233 |
+
#
|
| 234 |
+
# but POSIX shell has neither arrays nor command substitution, so instead we
|
| 235 |
+
# post-process each arg (as a line of input to sed) to backslash-escape any
|
| 236 |
+
# character that might be a shell metacharacter, then use eval to reverse
|
| 237 |
+
# that process (while maintaining the separation between arguments), and wrap
|
| 238 |
+
# the whole thing up as a single "set" statement.
|
| 239 |
+
#
|
| 240 |
+
# This will of course break if any of these variables contains a newline or
|
| 241 |
+
# an unmatched quote.
|
| 242 |
+
#
|
| 243 |
+
|
| 244 |
+
eval "set -- $(
|
| 245 |
+
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
| 246 |
+
xargs -n1 |
|
| 247 |
+
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
| 248 |
+
tr '\n' ' '
|
| 249 |
+
)" '"$@"'
|
| 250 |
+
|
| 251 |
+
exec "$JAVACMD" "$@"
|
android/gradlew.bat
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@rem
|
| 2 |
+
@rem Copyright 2015 the original author or authors.
|
| 3 |
+
@rem
|
| 4 |
+
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
@rem you may not use this file except in compliance with the License.
|
| 6 |
+
@rem You may obtain a copy of the License at
|
| 7 |
+
@rem
|
| 8 |
+
@rem https://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
@rem
|
| 10 |
+
@rem Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
@rem See the License for the specific language governing permissions and
|
| 14 |
+
@rem limitations under the License.
|
| 15 |
+
@rem
|
| 16 |
+
@rem SPDX-License-Identifier: Apache-2.0
|
| 17 |
+
@rem
|
| 18 |
+
|
| 19 |
+
@if "%DEBUG%"=="" @echo off
|
| 20 |
+
@rem ##########################################################################
|
| 21 |
+
@rem
|
| 22 |
+
@rem Gradle startup script for Windows
|
| 23 |
+
@rem
|
| 24 |
+
@rem ##########################################################################
|
| 25 |
+
|
| 26 |
+
@rem Set local scope for the variables with windows NT shell
|
| 27 |
+
if "%OS%"=="Windows_NT" setlocal
|
| 28 |
+
|
| 29 |
+
set DIRNAME=%~dp0
|
| 30 |
+
if "%DIRNAME%"=="" set DIRNAME=.
|
| 31 |
+
@rem This is normally unused
|
| 32 |
+
set APP_BASE_NAME=%~n0
|
| 33 |
+
set APP_HOME=%DIRNAME%
|
| 34 |
+
|
| 35 |
+
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
| 36 |
+
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
| 37 |
+
|
| 38 |
+
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
| 39 |
+
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
| 40 |
+
|
| 41 |
+
@rem Find java.exe
|
| 42 |
+
if defined JAVA_HOME goto findJavaFromJavaHome
|
| 43 |
+
|
| 44 |
+
set JAVA_EXE=java.exe
|
| 45 |
+
%JAVA_EXE% -version >NUL 2>&1
|
| 46 |
+
if %ERRORLEVEL% equ 0 goto execute
|
| 47 |
+
|
| 48 |
+
echo. 1>&2
|
| 49 |
+
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
| 50 |
+
echo. 1>&2
|
| 51 |
+
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
| 52 |
+
echo location of your Java installation. 1>&2
|
| 53 |
+
|
| 54 |
+
goto fail
|
| 55 |
+
|
| 56 |
+
:findJavaFromJavaHome
|
| 57 |
+
set JAVA_HOME=%JAVA_HOME:"=%
|
| 58 |
+
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
| 59 |
+
|
| 60 |
+
if exist "%JAVA_EXE%" goto execute
|
| 61 |
+
|
| 62 |
+
echo. 1>&2
|
| 63 |
+
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
| 64 |
+
echo. 1>&2
|
| 65 |
+
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
| 66 |
+
echo location of your Java installation. 1>&2
|
| 67 |
+
|
| 68 |
+
goto fail
|
| 69 |
+
|
| 70 |
+
:execute
|
| 71 |
+
@rem Setup the command line
|
| 72 |
+
|
| 73 |
+
set CLASSPATH=
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
@rem Execute Gradle
|
| 77 |
+
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
| 78 |
+
|
| 79 |
+
:end
|
| 80 |
+
@rem End local scope for the variables with windows NT shell
|
| 81 |
+
if %ERRORLEVEL% equ 0 goto mainEnd
|
| 82 |
+
|
| 83 |
+
:fail
|
| 84 |
+
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
| 85 |
+
rem the _cmd.exe /c_ return code!
|
| 86 |
+
set EXIT_CODE=%ERRORLEVEL%
|
| 87 |
+
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
| 88 |
+
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
| 89 |
+
exit /b %EXIT_CODE%
|
| 90 |
+
|
| 91 |
+
:mainEnd
|
| 92 |
+
if "%OS%"=="Windows_NT" endlocal
|
| 93 |
+
|
| 94 |
+
:omega
|
android/settings.gradle
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
include ':app'
|
| 2 |
+
include ':capacitor-cordova-android-plugins'
|
| 3 |
+
project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/')
|
| 4 |
+
|
| 5 |
+
apply from: 'capacitor.settings.gradle'
|
android/variables.gradle
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
ext {
|
| 2 |
+
minSdkVersion = 24
|
| 3 |
+
compileSdkVersion = 36
|
| 4 |
+
targetSdkVersion = 36
|
| 5 |
+
androidxActivityVersion = '1.11.0'
|
| 6 |
+
androidxAppCompatVersion = '1.7.1'
|
| 7 |
+
androidxCoordinatorLayoutVersion = '1.3.0'
|
| 8 |
+
androidxCoreVersion = '1.17.0'
|
| 9 |
+
androidxFragmentVersion = '1.8.9'
|
| 10 |
+
coreSplashScreenVersion = '1.2.0'
|
| 11 |
+
androidxWebkitVersion = '1.14.0'
|
| 12 |
+
junitVersion = '4.13.2'
|
| 13 |
+
androidxJunitVersion = '1.3.0'
|
| 14 |
+
androidxEspressoCoreVersion = '3.7.0'
|
| 15 |
+
cordovaAndroidVersion = '14.0.1'
|
| 16 |
+
}
|
api/lib/circuit-breaker.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { broadcastAdmin } from './socket.js';
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* 熔断器 (Circuit Breaker) 实现
|
| 5 |
+
* 状态:CLOSED (正常), OPEN (故障熔断), HALF_OPEN (尝试恢复)
|
| 6 |
+
*/
|
| 7 |
+
export class CircuitBreaker {
|
| 8 |
+
private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';
|
| 9 |
+
private failureCount = 0;
|
| 10 |
+
private lastFailureTime = 0;
|
| 11 |
+
private successCount = 0;
|
| 12 |
+
|
| 13 |
+
constructor(
|
| 14 |
+
private readonly name: string,
|
| 15 |
+
private readonly threshold = 3, // 故障阈值 (连续失败次数)
|
| 16 |
+
private readonly timeout = 10000, // 熔断时长 (ms)
|
| 17 |
+
private readonly successThreshold = 2 // 恢复阈值 (半开状态下连续成功次数)
|
| 18 |
+
) {}
|
| 19 |
+
|
| 20 |
+
async execute<T>(fn: () => Promise<T>): Promise<T> {
|
| 21 |
+
if (this.state === 'OPEN') {
|
| 22 |
+
const now = Date.now();
|
| 23 |
+
if (now - this.lastFailureTime > this.timeout) {
|
| 24 |
+
console.warn(`[CircuitBreaker:${this.name}] 进入 HALF_OPEN 状态,尝试探测...`);
|
| 25 |
+
this.state = 'HALF_OPEN';
|
| 26 |
+
} else {
|
| 27 |
+
throw new Error(`[熔断] 系统暂时不可用 (${this.name}),请 ${Math.ceil((this.timeout - (now - this.lastFailureTime)) / 1000)}s 后重试`);
|
| 28 |
+
}
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
try {
|
| 32 |
+
const result = await fn();
|
| 33 |
+
this.onSuccess();
|
| 34 |
+
return result;
|
| 35 |
+
} catch (err) {
|
| 36 |
+
this.onFailure();
|
| 37 |
+
throw err;
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
private onSuccess() {
|
| 42 |
+
this.failureCount = 0;
|
| 43 |
+
if (this.state === 'HALF_OPEN') {
|
| 44 |
+
this.successCount++;
|
| 45 |
+
if (this.successCount >= this.successThreshold) {
|
| 46 |
+
console.info(`[CircuitBreaker:${this.name}] 服务已恢复,进入 CLOSED 状态`);
|
| 47 |
+
this.state = 'CLOSED';
|
| 48 |
+
this.successCount = 0;
|
| 49 |
+
broadcastAdmin('circuit_breaker_change', { name: this.name, state: 'CLOSED' });
|
| 50 |
+
}
|
| 51 |
+
} else {
|
| 52 |
+
this.state = 'CLOSED';
|
| 53 |
+
}
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
private onFailure() {
|
| 57 |
+
this.failureCount++;
|
| 58 |
+
this.lastFailureTime = Date.now();
|
| 59 |
+
this.successCount = 0;
|
| 60 |
+
|
| 61 |
+
if (this.failureCount >= this.threshold && this.state !== 'OPEN') {
|
| 62 |
+
console.error(`[CircuitBreaker:${this.name}] 达到故障阈值,进入 OPEN 状态`);
|
| 63 |
+
this.state = 'OPEN';
|
| 64 |
+
broadcastAdmin('circuit_breaker_change', { name: this.name, state: 'OPEN' });
|
| 65 |
+
}
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
getStatus() {
|
| 69 |
+
return {
|
| 70 |
+
name: this.name,
|
| 71 |
+
state: this.state,
|
| 72 |
+
failureCount: this.failureCount,
|
| 73 |
+
lastFailureTime: this.lastFailureTime
|
| 74 |
+
};
|
| 75 |
+
}
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
export const aiCircuitBreaker = new CircuitBreaker('AI_SERVICE');
|
api/lib/db.ts
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Database from 'better-sqlite3';
|
| 2 |
+
import path from 'path';
|
| 3 |
+
import { fileURLToPath } from 'url';
|
| 4 |
+
import bcrypt from 'bcryptjs';
|
| 5 |
+
|
| 6 |
+
const __filename = fileURLToPath(import.meta.url);
|
| 7 |
+
const __dirname = path.dirname(__filename);
|
| 8 |
+
|
| 9 |
+
// 数据库文件存储在 /app/data 或项目根目录
|
| 10 |
+
const dataDir = process.env.DATA_DIR || path.resolve(process.cwd(), 'data');
|
| 11 |
+
const dbPath = path.resolve(dataDir, 'platform.db');
|
| 12 |
+
const db = new Database(dbPath);
|
| 13 |
+
|
| 14 |
+
// 初始化表结构
|
| 15 |
+
export const initDB = () => {
|
| 16 |
+
// 1. 用户表 (增加配额与等级)
|
| 17 |
+
db.exec(`
|
| 18 |
+
CREATE TABLE IF NOT EXISTS users (
|
| 19 |
+
id TEXT PRIMARY KEY,
|
| 20 |
+
email TEXT UNIQUE NOT NULL,
|
| 21 |
+
password TEXT NOT NULL,
|
| 22 |
+
name TEXT,
|
| 23 |
+
avatar TEXT, -- 用户头像 URL
|
| 24 |
+
role TEXT DEFAULT 'user', -- 'user', 'admin'
|
| 25 |
+
plan TEXT DEFAULT 'free', -- 'free', 'pro', 'enterprise'
|
| 26 |
+
quota_remaining INTEGER DEFAULT 500, -- 每月剩余 AI 消息额度
|
| 27 |
+
two_factor_enabled BOOLEAN DEFAULT 0, -- 是否开启双重认证
|
| 28 |
+
login_alert_enabled BOOLEAN DEFAULT 1, -- 是否开启登录提醒
|
| 29 |
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
| 30 |
+
)
|
| 31 |
+
`);
|
| 32 |
+
|
| 33 |
+
// 检查并增加新列 (针对已存在的数据库)
|
| 34 |
+
try {
|
| 35 |
+
const columns = db.prepare("PRAGMA table_info(users)").all();
|
| 36 |
+
const hasAvatar = columns.some((c: any) => c.name === 'avatar');
|
| 37 |
+
const has2FA = columns.some((c: any) => c.name === 'two_factor_enabled');
|
| 38 |
+
const hasAlert = columns.some((c: any) => c.name === 'login_alert_enabled');
|
| 39 |
+
|
| 40 |
+
if (!hasAvatar) {
|
| 41 |
+
db.exec("ALTER TABLE users ADD COLUMN avatar TEXT");
|
| 42 |
+
console.log('[Database] users 表已增加 avatar 列');
|
| 43 |
+
}
|
| 44 |
+
if (!has2FA) {
|
| 45 |
+
db.exec("ALTER TABLE users ADD COLUMN two_factor_enabled BOOLEAN DEFAULT 0");
|
| 46 |
+
console.log('[Database] users 表已增加 two_factor_enabled 列');
|
| 47 |
+
}
|
| 48 |
+
if (!hasAlert) {
|
| 49 |
+
db.exec("ALTER TABLE users ADD COLUMN login_alert_enabled BOOLEAN DEFAULT 1");
|
| 50 |
+
console.log('[Database] users 表已增加 login_alert_enabled 列');
|
| 51 |
+
}
|
| 52 |
+
} catch (err) {
|
| 53 |
+
console.error('[Database] 更新 users 表结构失败:', err);
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
// 2. 产品/计划表 (增加库存管理)
|
| 57 |
+
db.exec(`
|
| 58 |
+
CREATE TABLE IF NOT EXISTS products (
|
| 59 |
+
id TEXT PRIMARY KEY,
|
| 60 |
+
name TEXT NOT NULL,
|
| 61 |
+
price INTEGER NOT NULL, -- 以分为单位
|
| 62 |
+
type TEXT NOT NULL, -- 'subscription', 'one-time'
|
| 63 |
+
stock INTEGER DEFAULT -1, -- -1 表示无限, 0+ 表示限购数量
|
| 64 |
+
description TEXT,
|
| 65 |
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
| 66 |
+
)
|
| 67 |
+
`);
|
| 68 |
+
|
| 69 |
+
// 3. 订单表 (支持支付闭环)
|
| 70 |
+
db.exec(`
|
| 71 |
+
CREATE TABLE IF NOT EXISTS orders (
|
| 72 |
+
id TEXT PRIMARY KEY,
|
| 73 |
+
user_id TEXT NOT NULL,
|
| 74 |
+
product_id TEXT NOT NULL,
|
| 75 |
+
amount INTEGER NOT NULL,
|
| 76 |
+
status TEXT DEFAULT 'pending', -- 'pending', 'paid', 'cancelled', 'refunded'
|
| 77 |
+
payment_method TEXT, -- 'alipay', 'stripe', 'wechat'
|
| 78 |
+
payment_id TEXT, -- 外部支付流水号
|
| 79 |
+
idempotency_key TEXT UNIQUE, -- 幂等键
|
| 80 |
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 81 |
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 82 |
+
FOREIGN KEY (user_id) REFERENCES users (id),
|
| 83 |
+
FOREIGN KEY (product_id) REFERENCES products (id)
|
| 84 |
+
)
|
| 85 |
+
`);
|
| 86 |
+
|
| 87 |
+
// 4. 聊天会话表 (增加知识库关联)
|
| 88 |
+
db.exec(`
|
| 89 |
+
CREATE TABLE IF NOT EXISTS chat_sessions (
|
| 90 |
+
id TEXT PRIMARY KEY,
|
| 91 |
+
user_id TEXT NOT NULL,
|
| 92 |
+
title TEXT DEFAULT '新对话',
|
| 93 |
+
knowledge_base_id TEXT, -- 关联的知识库 ID
|
| 94 |
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 95 |
+
FOREIGN KEY (user_id) REFERENCES users (id),
|
| 96 |
+
FOREIGN KEY (knowledge_base_id) REFERENCES knowledge_bases (id)
|
| 97 |
+
)
|
| 98 |
+
`);
|
| 99 |
+
|
| 100 |
+
// 检查并增加 knowledge_base_id 列 (针对已存在的数据库)
|
| 101 |
+
try {
|
| 102 |
+
const columns = db.prepare("PRAGMA table_info(chat_sessions)").all();
|
| 103 |
+
const hasKbId = columns.some((c: any) => c.name === 'knowledge_base_id');
|
| 104 |
+
if (!hasKbId) {
|
| 105 |
+
db.exec("ALTER TABLE chat_sessions ADD COLUMN knowledge_base_id TEXT");
|
| 106 |
+
console.log('[Database] chat_sessions 表已增加 knowledge_base_id 列');
|
| 107 |
+
}
|
| 108 |
+
} catch (err) {
|
| 109 |
+
console.error('[Database] 更新 chat_sessions 表结构失败:', err);
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
// 3. 聊天消息表 (增加向量存储)
|
| 113 |
+
db.exec(`
|
| 114 |
+
CREATE TABLE IF NOT EXISTS chat_messages (
|
| 115 |
+
id TEXT PRIMARY KEY,
|
| 116 |
+
session_id TEXT NOT NULL,
|
| 117 |
+
role TEXT NOT NULL, -- 'user' or 'assistant'
|
| 118 |
+
content TEXT NOT NULL,
|
| 119 |
+
embedding TEXT, -- 存储向量 JSON 字符串
|
| 120 |
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 121 |
+
FOREIGN KEY (session_id) REFERENCES chat_sessions (id)
|
| 122 |
+
)
|
| 123 |
+
`);
|
| 124 |
+
|
| 125 |
+
// 5. 审计日志表 (System Engineering: Observability)
|
| 126 |
+
db.exec(`
|
| 127 |
+
CREATE TABLE IF NOT EXISTS audit_logs (
|
| 128 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 129 |
+
user_id TEXT,
|
| 130 |
+
action TEXT NOT NULL, -- 'LOGIN', 'CHAT', 'ORDER_CREATE', 'PAYMENT_COMPLETE'
|
| 131 |
+
status TEXT, -- 'SUCCESS', 'FAILED', 'CIRCUIT_OPEN'
|
| 132 |
+
payload TEXT, -- JSON data
|
| 133 |
+
ip TEXT,
|
| 134 |
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
| 135 |
+
)
|
| 136 |
+
`);
|
| 137 |
+
|
| 138 |
+
// 6. 系统通知表 (Business: Message)
|
| 139 |
+
db.exec(`
|
| 140 |
+
CREATE TABLE IF NOT EXISTS notifications (
|
| 141 |
+
id TEXT PRIMARY KEY,
|
| 142 |
+
user_id TEXT NOT NULL,
|
| 143 |
+
title TEXT NOT NULL,
|
| 144 |
+
content TEXT NOT NULL,
|
| 145 |
+
type TEXT DEFAULT 'info', -- 'info', 'success', 'warning', 'error'
|
| 146 |
+
is_read BOOLEAN DEFAULT 0,
|
| 147 |
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 148 |
+
FOREIGN KEY (user_id) REFERENCES users (id)
|
| 149 |
+
)
|
| 150 |
+
`);
|
| 151 |
+
|
| 152 |
+
// 7. 工作流表
|
| 153 |
+
db.exec(`
|
| 154 |
+
CREATE TABLE IF NOT EXISTS workflows (
|
| 155 |
+
id TEXT PRIMARY KEY,
|
| 156 |
+
user_id TEXT NOT NULL,
|
| 157 |
+
name TEXT NOT NULL,
|
| 158 |
+
data TEXT NOT NULL, -- JSON string of nodes and edges
|
| 159 |
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 160 |
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 161 |
+
FOREIGN KEY (user_id) REFERENCES users (id)
|
| 162 |
+
)
|
| 163 |
+
`);
|
| 164 |
+
|
| 165 |
+
// 8. 知识库表 (System: RAG)
|
| 166 |
+
db.exec(`
|
| 167 |
+
CREATE TABLE IF NOT EXISTS knowledge_bases (
|
| 168 |
+
id TEXT PRIMARY KEY,
|
| 169 |
+
user_id TEXT NOT NULL,
|
| 170 |
+
name TEXT NOT NULL,
|
| 171 |
+
description TEXT,
|
| 172 |
+
status TEXT DEFAULT 'processing', -- 'processing', 'ready'
|
| 173 |
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 174 |
+
FOREIGN KEY (user_id) REFERENCES users (id)
|
| 175 |
+
)
|
| 176 |
+
`);
|
| 177 |
+
|
| 178 |
+
// 9. 知识切片表 (System: Vector Store)
|
| 179 |
+
db.exec(`
|
| 180 |
+
CREATE TABLE IF NOT EXISTS knowledge_chunks (
|
| 181 |
+
id TEXT PRIMARY KEY,
|
| 182 |
+
kb_id TEXT NOT NULL,
|
| 183 |
+
content TEXT NOT NULL,
|
| 184 |
+
embedding TEXT, -- JSON string of vector
|
| 185 |
+
metadata TEXT, -- JSON string of extra info
|
| 186 |
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 187 |
+
FOREIGN KEY (kb_id) REFERENCES knowledge_bases (id)
|
| 188 |
+
)
|
| 189 |
+
`);
|
| 190 |
+
|
| 191 |
+
// 10. API Keys 表 (System: OpenAPI)
|
| 192 |
+
db.exec(`
|
| 193 |
+
CREATE TABLE IF NOT EXISTS api_keys (
|
| 194 |
+
id TEXT PRIMARY KEY,
|
| 195 |
+
user_id TEXT NOT NULL,
|
| 196 |
+
key_name TEXT NOT NULL,
|
| 197 |
+
key_secret TEXT UNIQUE NOT NULL,
|
| 198 |
+
status TEXT DEFAULT 'active', -- 'active', 'inactive'
|
| 199 |
+
scopes TEXT DEFAULT '["all"]', -- JSON array of scopes
|
| 200 |
+
last_used DATETIME,
|
| 201 |
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 202 |
+
FOREIGN KEY (user_id) REFERENCES users (id)
|
| 203 |
+
)
|
| 204 |
+
`);
|
| 205 |
+
|
| 206 |
+
// 检查并增加 status 和 scopes 列 (针对已存在的数据库)
|
| 207 |
+
try {
|
| 208 |
+
const columns = db.prepare("PRAGMA table_info(api_keys)").all();
|
| 209 |
+
const hasStatus = columns.some((c: any) => c.name === 'status');
|
| 210 |
+
const hasScopes = columns.some((c: any) => c.name === 'scopes');
|
| 211 |
+
|
| 212 |
+
if (!hasStatus) {
|
| 213 |
+
db.exec("ALTER TABLE api_keys ADD COLUMN status TEXT DEFAULT 'active'");
|
| 214 |
+
console.log('[Database] api_keys 表已增加 status 列');
|
| 215 |
+
}
|
| 216 |
+
if (!hasScopes) {
|
| 217 |
+
db.exec("ALTER TABLE api_keys ADD COLUMN scopes TEXT DEFAULT '[\"all\"]'");
|
| 218 |
+
console.log('[Database] api_keys 表已增加 scopes 列');
|
| 219 |
+
}
|
| 220 |
+
} catch (err) {
|
| 221 |
+
console.error('[Database] 更新 api_keys 表结构失败:', err);
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
// 11. Hugging Face Projects Cache
|
| 225 |
+
db.exec(`
|
| 226 |
+
CREATE TABLE IF NOT EXISTS hf_projects (
|
| 227 |
+
id TEXT PRIMARY KEY,
|
| 228 |
+
full_name TEXT NOT NULL,
|
| 229 |
+
name TEXT NOT NULL,
|
| 230 |
+
title TEXT,
|
| 231 |
+
description TEXT,
|
| 232 |
+
url TEXT,
|
| 233 |
+
iframe_url TEXT,
|
| 234 |
+
type TEXT,
|
| 235 |
+
created_at_hf DATETIME,
|
| 236 |
+
likes INTEGER,
|
| 237 |
+
sdk TEXT,
|
| 238 |
+
synced_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
| 239 |
+
)
|
| 240 |
+
`);
|
| 241 |
+
|
| 242 |
+
console.log('[Database] SQLite 已就绪:', dbPath);
|
| 243 |
+
|
| 244 |
+
// 初始化基础产品数据
|
| 245 |
+
const products = [
|
| 246 |
+
{ id: 'plan_pro', name: '专业版', price: 9900, type: 'subscription', stock: -1, description: '无限 AI 消息 + 高级工作流' },
|
| 247 |
+
{ id: 'plan_ent', name: '企业版', price: 29900, type: 'subscription', stock: -1, description: '全方位技术支持 + 专属域名' },
|
| 248 |
+
{ id: 'limited_offer', name: '限时秒杀 (Pro 体验卡)', price: 100, type: 'one-time', stock: 10, description: '限量 10 份,仅需 1 元' }
|
| 249 |
+
];
|
| 250 |
+
|
| 251 |
+
const insertProduct = db.prepare(`
|
| 252 |
+
INSERT OR IGNORE INTO products (id, name, price, type, stock, description)
|
| 253 |
+
VALUES (?, ?, ?, ?, ?, ?)
|
| 254 |
+
`);
|
| 255 |
+
|
| 256 |
+
products.forEach(p => {
|
| 257 |
+
insertProduct.run(p.id, p.name, p.price, p.type, p.stock, p.description);
|
| 258 |
+
});
|
| 259 |
+
|
| 260 |
+
// 初始化默认管理员 (如果不存在)
|
| 261 |
+
const adminEmail = 'admin@codex.ai';
|
| 262 |
+
const existingAdmin = db.prepare('SELECT id FROM users WHERE email = ?').get(adminEmail);
|
| 263 |
+
if (!existingAdmin) {
|
| 264 |
+
const hashedPassword = bcrypt.hashSync('admin123', 10);
|
| 265 |
+
db.prepare('INSERT INTO users (id, email, password, name, role, plan) VALUES (?, ?, ?, ?, ?, ?)')
|
| 266 |
+
.run('ADMIN_ROOT', adminEmail, hashedPassword, 'System Admin', 'admin', 'enterprise');
|
| 267 |
+
console.log('[Database] 默认管理员已创建: admin@codex.ai / admin123');
|
| 268 |
+
}
|
| 269 |
+
};
|
| 270 |
+
|
| 271 |
+
export default db;
|
api/lib/pg.ts
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pg from 'pg';
|
| 2 |
+
import dotenv from 'dotenv';
|
| 3 |
+
|
| 4 |
+
dotenv.config();
|
| 5 |
+
|
| 6 |
+
const { Pool } = pg;
|
| 7 |
+
|
| 8 |
+
// 优先使用环境变量,否则默认为 localhost (适配本地开发)
|
| 9 |
+
// Docker 环境下会在 docker-compose.yml 中显式注入 DATABASE_URL
|
| 10 |
+
const connectionString = process.env.DATABASE_URL || 'postgresql://postgres:postgres@localhost:5432/codex';
|
| 11 |
+
|
| 12 |
+
const pool = new Pool({
|
| 13 |
+
connectionString,
|
| 14 |
+
max: 20, // 连接池最大连接数
|
| 15 |
+
idleTimeoutMillis: 30000,
|
| 16 |
+
connectionTimeoutMillis: 5000, // 增加超时时间到 5s
|
| 17 |
+
});
|
| 18 |
+
|
| 19 |
+
export let isPgAvailable = false;
|
| 20 |
+
|
| 21 |
+
// 简单的重试机制
|
| 22 |
+
const retry = async (fn: () => Promise<any>, retries = 5, delay = 2000) => {
|
| 23 |
+
try {
|
| 24 |
+
return await fn();
|
| 25 |
+
} catch (err: any) {
|
| 26 |
+
if (retries > 0) {
|
| 27 |
+
console.warn(`[PG] 连接失败,${delay / 1000}秒后重试... (剩余 ${retries} 次) - ${err.message}`);
|
| 28 |
+
await new Promise(res => setTimeout(res, delay));
|
| 29 |
+
return retry(fn, retries - 1, delay);
|
| 30 |
+
} else {
|
| 31 |
+
throw err;
|
| 32 |
+
}
|
| 33 |
+
}
|
| 34 |
+
};
|
| 35 |
+
|
| 36 |
+
// 初始化 PGVector 扩展和表结构
|
| 37 |
+
export const initPG = async () => {
|
| 38 |
+
try {
|
| 39 |
+
await retry(async () => {
|
| 40 |
+
const client = await pool.connect();
|
| 41 |
+
try {
|
| 42 |
+
console.log(`[PG] 正在初始化 PostgreSQL + PGVector (${connectionString.includes('localhost') ? 'Local' : 'Remote'})...`);
|
| 43 |
+
|
| 44 |
+
// 1. 启用 pgvector 扩展
|
| 45 |
+
await client.query('CREATE EXTENSION IF NOT EXISTS vector');
|
| 46 |
+
|
| 47 |
+
// 2. 创建知识库表 (与 SQLite 保持一致,但在 PG 中重建)
|
| 48 |
+
await client.query(`
|
| 49 |
+
CREATE TABLE IF NOT EXISTS knowledge_bases (
|
| 50 |
+
id TEXT PRIMARY KEY,
|
| 51 |
+
user_id TEXT NOT NULL,
|
| 52 |
+
name TEXT NOT NULL,
|
| 53 |
+
description TEXT,
|
| 54 |
+
status TEXT DEFAULT 'processing',
|
| 55 |
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
| 56 |
+
)
|
| 57 |
+
`);
|
| 58 |
+
|
| 59 |
+
// 3. 创建知识切片表 (核心:向量存储)
|
| 60 |
+
// 使用 1024 维向量 (适配 BGE-M3 模型)
|
| 61 |
+
await client.query(`
|
| 62 |
+
CREATE TABLE IF NOT EXISTS knowledge_chunks (
|
| 63 |
+
id TEXT PRIMARY KEY,
|
| 64 |
+
kb_id TEXT NOT NULL,
|
| 65 |
+
content TEXT NOT NULL,
|
| 66 |
+
embedding vector(1024),
|
| 67 |
+
metadata JSONB,
|
| 68 |
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
| 69 |
+
FOREIGN KEY (kb_id) REFERENCES knowledge_bases(id) ON DELETE CASCADE
|
| 70 |
+
)
|
| 71 |
+
`);
|
| 72 |
+
|
| 73 |
+
// 4. 创建全文检索索引 (用于混合检索 - 关键词部分)
|
| 74 |
+
// 使用 simple 分词器以支持中文 (或者使用 zhparser 如果安装了的话,这里先用 simple)
|
| 75 |
+
await client.query(`
|
| 76 |
+
CREATE INDEX IF NOT EXISTS idx_knowledge_chunks_content_ts
|
| 77 |
+
ON knowledge_chunks USING GIN (to_tsvector('simple', content));
|
| 78 |
+
`);
|
| 79 |
+
|
| 80 |
+
// 5. 创建向量索引 (HNSW - 用于混合检索 - 语义部分)
|
| 81 |
+
await client.query(`
|
| 82 |
+
CREATE INDEX IF NOT EXISTS idx_knowledge_chunks_embedding
|
| 83 |
+
ON knowledge_chunks USING hnsw (embedding vector_cosine_ops);
|
| 84 |
+
`);
|
| 85 |
+
|
| 86 |
+
// 6. 创建混合检索函数 (Hybrid Search RPC)
|
| 87 |
+
// 结合 向量相似度 (Cosine) + 关键词匹配 (BM25/TSRank) + RRF (Reciprocal Rank Fusion)
|
| 88 |
+
await client.query(`
|
| 89 |
+
CREATE OR REPLACE FUNCTION hybrid_search(
|
| 90 |
+
query_text TEXT,
|
| 91 |
+
query_embedding vector(1024),
|
| 92 |
+
match_threshold FLOAT,
|
| 93 |
+
match_count INT,
|
| 94 |
+
filter_kb_id TEXT DEFAULT NULL,
|
| 95 |
+
rrf_k INT DEFAULT 60
|
| 96 |
+
)
|
| 97 |
+
RETURNS TABLE (
|
| 98 |
+
id TEXT,
|
| 99 |
+
content TEXT,
|
| 100 |
+
metadata JSONB,
|
| 101 |
+
similarity FLOAT,
|
| 102 |
+
rank_score FLOAT
|
| 103 |
+
) LANGUAGE plpgsql AS $$
|
| 104 |
+
BEGIN
|
| 105 |
+
RETURN QUERY
|
| 106 |
+
WITH semantic_search AS (
|
| 107 |
+
SELECT
|
| 108 |
+
kc.id,
|
| 109 |
+
kc.content,
|
| 110 |
+
kc.metadata,
|
| 111 |
+
1 - (kc.embedding <=> query_embedding) AS similarity,
|
| 112 |
+
ROW_NUMBER() OVER (ORDER BY kc.embedding <=> query_embedding) AS rank_seq
|
| 113 |
+
FROM knowledge_chunks kc
|
| 114 |
+
WHERE 1 - (kc.embedding <=> query_embedding) > match_threshold
|
| 115 |
+
AND (filter_kb_id IS NULL OR kc.kb_id = filter_kb_id)
|
| 116 |
+
ORDER BY similarity DESC
|
| 117 |
+
LIMIT match_count * 2
|
| 118 |
+
),
|
| 119 |
+
keyword_search AS (
|
| 120 |
+
SELECT
|
| 121 |
+
kc.id,
|
| 122 |
+
kc.content,
|
| 123 |
+
kc.metadata,
|
| 124 |
+
ts_rank_cd(to_tsvector('simple', kc.content), websearch_to_tsquery('simple', query_text)) AS similarity,
|
| 125 |
+
ROW_NUMBER() OVER (ORDER BY ts_rank_cd(to_tsvector('simple', kc.content), websearch_to_tsquery('simple', query_text)) DESC) AS rank_seq
|
| 126 |
+
FROM knowledge_chunks kc
|
| 127 |
+
WHERE to_tsvector('simple', kc.content) @@ websearch_to_tsquery('simple', query_text)
|
| 128 |
+
AND (filter_kb_id IS NULL OR kc.kb_id = filter_kb_id)
|
| 129 |
+
ORDER BY similarity DESC
|
| 130 |
+
LIMIT match_count * 2
|
| 131 |
+
)
|
| 132 |
+
SELECT
|
| 133 |
+
COALESCE(s.id, k.id) AS id,
|
| 134 |
+
COALESCE(s.content, k.content) AS content,
|
| 135 |
+
COALESCE(s.metadata, k.metadata) AS metadata,
|
| 136 |
+
COALESCE(s.similarity, 0) AS similarity,
|
| 137 |
+
(
|
| 138 |
+
COALESCE(1.0 / (rrf_k + s.rank_seq), 0.0) +
|
| 139 |
+
COALESCE(1.0 / (rrf_k + k.rank_seq), 0.0)
|
| 140 |
+
) AS rank_score
|
| 141 |
+
FROM semantic_search s
|
| 142 |
+
FULL OUTER JOIN keyword_search k ON s.id = k.id
|
| 143 |
+
ORDER BY rank_score DESC
|
| 144 |
+
LIMIT match_count;
|
| 145 |
+
END;
|
| 146 |
+
$$;
|
| 147 |
+
`);
|
| 148 |
+
|
| 149 |
+
console.log('[PG] PostgreSQL 初始化完成');
|
| 150 |
+
isPgAvailable = true;
|
| 151 |
+
} finally {
|
| 152 |
+
client.release();
|
| 153 |
+
}
|
| 154 |
+
}, 5, 1000); // 减少重试次数和间隔,快速失败以回退到 SQLite
|
| 155 |
+
} catch (err: any) {
|
| 156 |
+
console.error('[PG] 初始化彻底失败,将降级使用 SQLite:', err.message);
|
| 157 |
+
isPgAvailable = false;
|
| 158 |
+
}
|
| 159 |
+
};
|
| 160 |
+
|
| 161 |
+
export default pool;
|
api/lib/queue.ts
CHANGED
|
@@ -1,86 +1,114 @@
|
|
| 1 |
/**
|
| 2 |
-
*
|
|
|
|
| 3 |
*/
|
| 4 |
-
import {
|
|
|
|
| 5 |
|
| 6 |
-
|
| 7 |
-
private queue: Array<{ type: string; data: any; resolve: Function; reject: Function }> = [];
|
| 8 |
-
private activeCount = 0;
|
| 9 |
-
private maxConcurrency = 5; // 默认最大并发数
|
| 10 |
-
private handlers: Record<string, (data: any) => Promise<any>> = {};
|
| 11 |
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
}
|
| 15 |
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
-
|
| 22 |
-
setConcurrency(n: number) {
|
| 23 |
-
this.maxConcurrency = n;
|
| 24 |
-
}
|
| 25 |
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
}
|
| 33 |
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
}
|
|
|
|
| 39 |
|
| 40 |
-
|
| 41 |
-
|
|
|
|
| 42 |
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
return;
|
| 47 |
-
}
|
| 48 |
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
try {
|
| 53 |
-
const result = await handler(data);
|
| 54 |
-
resolve(result);
|
| 55 |
-
} catch (err) {
|
| 56 |
-
reject(err);
|
| 57 |
-
} finally {
|
| 58 |
-
this.activeCount--;
|
| 59 |
-
console.log(`[Queue] 任务完成: ${type} (剩余待办: ${this.queue.length})`);
|
| 60 |
-
this.processNext();
|
| 61 |
-
}
|
| 62 |
-
}
|
| 63 |
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
}
|
| 71 |
-
}
|
| 72 |
-
|
| 73 |
-
export const taskQueue = new TaskQueue();
|
| 74 |
-
|
| 75 |
-
export const setupWorkers = (
|
| 76 |
-
aiHandler: (data: any) => Promise<any>,
|
| 77 |
-
docHandler: (data: any) => Promise<any>
|
| 78 |
-
) => {
|
| 79 |
-
taskQueue.registerHandler('ai_workflow', aiHandler);
|
| 80 |
-
taskQueue.registerHandler('document_process', docHandler);
|
| 81 |
-
console.log('[Queue] 任务队列处理器已就绪,最大并发:', taskQueue.getStatus().maxConcurrency);
|
| 82 |
};
|
| 83 |
|
| 84 |
-
|
| 85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
};
|
|
|
|
| 1 |
/**
|
| 2 |
+
* 基于 BullMQ 的分布式任务队列实现 (持久化与可靠处理)
|
| 3 |
+
* 支持:自动重试、延迟执行、并发控制、任务持久化
|
| 4 |
*/
|
| 5 |
+
import { Queue, Worker, Job } from 'bullmq';
|
| 6 |
+
import { redis, isRedisAvailable } from './redis.js';
|
| 7 |
|
| 8 |
+
const QUEUE_NAME = 'codex_tasks';
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
+
// 内存队列模拟 (当 Redis 不可用时)
|
| 11 |
+
const memoryQueue: any[] = [];
|
|
|
|
| 12 |
|
| 13 |
+
// 1. 定义任务队列
|
| 14 |
+
export const taskQueue = isRedisAvailable ? new Queue(QUEUE_NAME, {
|
| 15 |
+
connection: redis as any,
|
| 16 |
+
defaultJobOptions: {
|
| 17 |
+
attempts: 3,
|
| 18 |
+
backoff: { type: 'exponential', delay: 1000 },
|
| 19 |
+
removeOnComplete: true,
|
| 20 |
+
removeOnFail: false,
|
| 21 |
+
},
|
| 22 |
+
}) : null;
|
| 23 |
|
| 24 |
+
let worker: Worker | null = null;
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
+
// 2. 设置处理器与并发控制
|
| 27 |
+
export const setupWorkers = (
|
| 28 |
+
aiHandler: (data: any) => Promise<any>,
|
| 29 |
+
docHandler: (data: any) => Promise<any>
|
| 30 |
+
) => {
|
| 31 |
+
if (worker || !isRedisAvailable) {
|
| 32 |
+
if (!isRedisAvailable) {
|
| 33 |
+
console.warn('[Queue] Redis 不可用,启用内存模拟模式 (任务不持久化)');
|
| 34 |
+
// 启动一个简单的定时器处理内存任务
|
| 35 |
+
setInterval(async () => {
|
| 36 |
+
if (memoryQueue.length > 0) {
|
| 37 |
+
const task = memoryQueue.shift();
|
| 38 |
+
console.log(`[Queue:Memory] 正在处理任务: ${task.type}`);
|
| 39 |
+
try {
|
| 40 |
+
if (task.type === 'ai_workflow') await aiHandler(task.data);
|
| 41 |
+
else if (task.type === 'document_process') await docHandler(task.data);
|
| 42 |
+
} catch (e) {}
|
| 43 |
+
}
|
| 44 |
+
}, 3000);
|
| 45 |
+
}
|
| 46 |
+
return;
|
| 47 |
}
|
| 48 |
|
| 49 |
+
worker = new Worker(
|
| 50 |
+
QUEUE_NAME,
|
| 51 |
+
async (job: Job) => {
|
| 52 |
+
// ... 逻辑保持不变
|
| 53 |
+
console.log(`[Queue] 正在执行任务: ${job.name} (ID: ${job.id})`);
|
| 54 |
+
|
| 55 |
+
const { type, data } = job.data;
|
| 56 |
+
|
| 57 |
+
try {
|
| 58 |
+
if (type === 'ai_workflow') {
|
| 59 |
+
return await aiHandler(data);
|
| 60 |
+
} else if (type === 'document_process') {
|
| 61 |
+
return await docHandler(data);
|
| 62 |
+
} else {
|
| 63 |
+
throw new Error(`未知任务类型: ${type}`);
|
| 64 |
+
}
|
| 65 |
+
} catch (err) {
|
| 66 |
+
console.error(`[Queue] 任务失败: ${job.id}`, err);
|
| 67 |
+
throw err;
|
| 68 |
+
}
|
| 69 |
+
},
|
| 70 |
+
{
|
| 71 |
+
connection: redis as any,
|
| 72 |
+
concurrency: 5, // 最大并发处理数
|
| 73 |
}
|
| 74 |
+
);
|
| 75 |
|
| 76 |
+
worker.on('completed', (job) => {
|
| 77 |
+
console.log(`[Queue] 任务已完成: ${job.id}`);
|
| 78 |
+
});
|
| 79 |
|
| 80 |
+
worker.on('failed', (job, err) => {
|
| 81 |
+
console.error(`[Queue] 任务彻底失败: ${job?.id}`, err.message);
|
| 82 |
+
});
|
|
|
|
|
|
|
| 83 |
|
| 84 |
+
console.log('[Queue] BullMQ 任务队列 Workers 已就绪,并发数: 5');
|
| 85 |
+
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
|
| 87 |
+
// 3. 添加任务接口
|
| 88 |
+
export const addJob = async (type: string, data: any) => {
|
| 89 |
+
const jobName = `${type}_${Date.now()}`;
|
| 90 |
+
|
| 91 |
+
if (taskQueue) {
|
| 92 |
+
const job = await taskQueue.add(jobName, { type, data });
|
| 93 |
+
return { id: job.id, name: jobName };
|
| 94 |
+
} else {
|
| 95 |
+
// 内存降级逻辑
|
| 96 |
+
memoryQueue.push({ type, data });
|
| 97 |
+
return { id: `mem_${Date.now()}`, name: jobName };
|
| 98 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
};
|
| 100 |
|
| 101 |
+
// 获取队列简要状态 (用于监控)
|
| 102 |
+
export const getQueueStatus = async () => {
|
| 103 |
+
if (taskQueue) {
|
| 104 |
+
const [active, waiting, completed, failed] = await Promise.all([
|
| 105 |
+
taskQueue.getActiveCount(),
|
| 106 |
+
taskQueue.getWaitingCount(),
|
| 107 |
+
taskQueue.getCompletedCount(),
|
| 108 |
+
taskQueue.getFailedCount(),
|
| 109 |
+
]);
|
| 110 |
+
return { active, waiting, completed, failed };
|
| 111 |
+
} else {
|
| 112 |
+
return { active: 0, waiting: memoryQueue.length, completed: 0, failed: 0 };
|
| 113 |
+
}
|
| 114 |
};
|
api/lib/rate-limiter.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import redis from '../lib/redis.js';
|
| 2 |
+
|
| 3 |
+
export interface RateLimitConfig {
|
| 4 |
+
windowMs: number; // 窗口大小 (ms)
|
| 5 |
+
max: number; // 窗口内最大请求数
|
| 6 |
+
keyPrefix: string;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
/**
|
| 10 |
+
* 基于 Redis 的分布式滑动窗口限流器 (支持内存降级)
|
| 11 |
+
*/
|
| 12 |
+
export class RateLimiter {
|
| 13 |
+
private memoryCache = new Map<string, number[]>();
|
| 14 |
+
|
| 15 |
+
/**
|
| 16 |
+
* 核心限流检查
|
| 17 |
+
* @returns { success: boolean, remaining: number, resetMs: number }
|
| 18 |
+
*/
|
| 19 |
+
async check(userId: string, config: RateLimitConfig) {
|
| 20 |
+
const key = `rl:${config.keyPrefix}:${userId}`;
|
| 21 |
+
const now = Date.now();
|
| 22 |
+
const windowStart = now - config.windowMs;
|
| 23 |
+
|
| 24 |
+
try {
|
| 25 |
+
// 1. 尝试使用 Redis 实现滑动窗口 (ZSET)
|
| 26 |
+
// 使用原子事务确保一致性
|
| 27 |
+
const multi = redis.multi();
|
| 28 |
+
multi.zremrangebyscore(key, 0, windowStart); // 移除窗口外的请求
|
| 29 |
+
multi.zadd(key, now, now.toString()); // 记录当前请求
|
| 30 |
+
multi.zcard(key); // 获取当前窗口内请求数
|
| 31 |
+
multi.pexpire(key, config.windowMs); // 设置过期时间 (防止僵尸 key)
|
| 32 |
+
|
| 33 |
+
const results = await multi.exec();
|
| 34 |
+
if (!results) throw new Error('Redis multi exec failed');
|
| 35 |
+
|
| 36 |
+
const count = results[2][1] as number;
|
| 37 |
+
const isAllowed = count <= config.max;
|
| 38 |
+
|
| 39 |
+
return {
|
| 40 |
+
success: isAllowed,
|
| 41 |
+
remaining: Math.max(0, config.max - count),
|
| 42 |
+
resetMs: config.windowMs - (now % config.windowMs)
|
| 43 |
+
};
|
| 44 |
+
} catch (err) {
|
| 45 |
+
console.warn(`[RateLimiter] Redis 限流失败,切换到本地内存降级: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
| 46 |
+
|
| 47 |
+
// 2. 内存降级方案 (Memory Fallback)
|
| 48 |
+
let requests = this.memoryCache.get(key) || [];
|
| 49 |
+
requests = requests.filter(t => t > windowStart);
|
| 50 |
+
|
| 51 |
+
if (requests.length < config.max) {
|
| 52 |
+
requests.push(now);
|
| 53 |
+
this.memoryCache.set(key, requests);
|
| 54 |
+
return { success: true, remaining: config.max - requests.length, resetMs: config.windowMs };
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
return { success: false, remaining: 0, resetMs: config.windowMs };
|
| 58 |
+
}
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
/**
|
| 62 |
+
* Express 中间件封装
|
| 63 |
+
*/
|
| 64 |
+
middleware(config: RateLimitConfig) {
|
| 65 |
+
return async (req: any, res: any, next: any) => {
|
| 66 |
+
const userId = req.user?.userId || req.ip;
|
| 67 |
+
const result = await this.check(userId, config);
|
| 68 |
+
|
| 69 |
+
res.setHeader('X-RateLimit-Limit', config.max);
|
| 70 |
+
res.setHeader('X-RateLimit-Remaining', result.remaining);
|
| 71 |
+
res.setHeader('X-RateLimit-Reset', result.resetMs);
|
| 72 |
+
|
| 73 |
+
if (!result.success) {
|
| 74 |
+
return res.status(429).json({
|
| 75 |
+
success: false,
|
| 76 |
+
error: '请求过于频繁,请稍后再试',
|
| 77 |
+
retryAfter: Math.ceil(result.resetMs / 1000)
|
| 78 |
+
});
|
| 79 |
+
}
|
| 80 |
+
next();
|
| 81 |
+
};
|
| 82 |
+
}
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
export const globalLimiter = new RateLimiter();
|
api/lib/redis.ts
CHANGED
|
@@ -5,20 +5,33 @@ dotenv.config();
|
|
| 5 |
|
| 6 |
const redisUrl = process.env.REDIS_URL || 'redis://localhost:6379';
|
| 7 |
|
|
|
|
|
|
|
|
|
|
| 8 |
export const redis = new Redis(redisUrl, {
|
| 9 |
maxRetriesPerRequest: null,
|
|
|
|
| 10 |
retryStrategy(times) {
|
| 11 |
const delay = Math.min(times * 50, 2000);
|
|
|
|
|
|
|
| 12 |
return delay;
|
| 13 |
},
|
| 14 |
});
|
| 15 |
|
| 16 |
redis.on('connect', () => {
|
|
|
|
| 17 |
console.log('[Redis] 已连接');
|
| 18 |
});
|
| 19 |
|
| 20 |
redis.on('error', (err) => {
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
});
|
| 23 |
|
| 24 |
export default redis;
|
|
|
|
| 5 |
|
| 6 |
const redisUrl = process.env.REDIS_URL || 'redis://localhost:6379';
|
| 7 |
|
| 8 |
+
// 增加连接标志,供其他服务判断是否可用
|
| 9 |
+
export let isRedisAvailable = false;
|
| 10 |
+
|
| 11 |
export const redis = new Redis(redisUrl, {
|
| 12 |
maxRetriesPerRequest: null,
|
| 13 |
+
lazyConnect: true, // 延迟连接,避免启动时立即报错
|
| 14 |
retryStrategy(times) {
|
| 15 |
const delay = Math.min(times * 50, 2000);
|
| 16 |
+
// 如果重试次数过多且依然连不上,降低频率
|
| 17 |
+
if (times > 10) return null; // 停止重试,标记不可用
|
| 18 |
return delay;
|
| 19 |
},
|
| 20 |
});
|
| 21 |
|
| 22 |
redis.on('connect', () => {
|
| 23 |
+
isRedisAvailable = true;
|
| 24 |
console.log('[Redis] 已连接');
|
| 25 |
});
|
| 26 |
|
| 27 |
redis.on('error', (err) => {
|
| 28 |
+
isRedisAvailable = false;
|
| 29 |
+
// 只在第一次报错时打印详细信息,避免日志淹没
|
| 30 |
+
if (redis.status === 'reconnecting') {
|
| 31 |
+
// console.log('[Redis] 正在尝试重连...');
|
| 32 |
+
} else {
|
| 33 |
+
console.warn('[Redis] 连接失败,部分高并发与队列功能将受限');
|
| 34 |
+
}
|
| 35 |
});
|
| 36 |
|
| 37 |
export default redis;
|
api/lib/socket.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { Server as SocketServer } from 'socket.io';
|
| 2 |
+
import { Server as HttpServer } from 'http';
|
| 3 |
+
import jwt from 'jsonwebtoken';
|
| 4 |
+
|
| 5 |
+
let io: SocketServer | null = null;
|
| 6 |
+
const JWT_SECRET = process.env.JWT_SECRET || 'codex_secret_fallback';
|
| 7 |
+
|
| 8 |
+
export const initSocket = (server: HttpServer) => {
|
| 9 |
+
io = new SocketServer(server, {
|
| 10 |
+
cors: {
|
| 11 |
+
origin: '*',
|
| 12 |
+
methods: ['GET', 'POST']
|
| 13 |
+
}
|
| 14 |
+
});
|
| 15 |
+
|
| 16 |
+
// 增加 JWT 鉴权中间件
|
| 17 |
+
io.use((socket, next) => {
|
| 18 |
+
const token = socket.handshake.auth.token || socket.handshake.headers.token;
|
| 19 |
+
if (!token) return next(new Error('未授权'));
|
| 20 |
+
|
| 21 |
+
try {
|
| 22 |
+
const decoded = jwt.verify(token, JWT_SECRET) as any;
|
| 23 |
+
(socket as any).user = decoded;
|
| 24 |
+
next();
|
| 25 |
+
} catch (err) {
|
| 26 |
+
next(new Error('Token 无效'));
|
| 27 |
+
}
|
| 28 |
+
});
|
| 29 |
+
|
| 30 |
+
io.on('connection', (socket) => {
|
| 31 |
+
const user = (socket as any).user;
|
| 32 |
+
console.log(`[Socket] 用户已认证并连接: ${user.email} (ID: ${socket.id})`);
|
| 33 |
+
|
| 34 |
+
// 自动加入用户专属房间
|
| 35 |
+
socket.join(`user:${user.userId}`);
|
| 36 |
+
|
| 37 |
+
socket.on('disconnect', () => {
|
| 38 |
+
console.log(`[Socket] 用户断开: ${user.email}`);
|
| 39 |
+
});
|
| 40 |
+
});
|
| 41 |
+
|
| 42 |
+
return io;
|
| 43 |
+
};
|
| 44 |
+
|
| 45 |
+
export const getIO = () => {
|
| 46 |
+
if (!io) throw new Error('Socket.io 未初始化');
|
| 47 |
+
return io;
|
| 48 |
+
};
|
| 49 |
+
|
| 50 |
+
/**
|
| 51 |
+
* 推送实时通知给特定用户
|
| 52 |
+
*/
|
| 53 |
+
export const notifyUser = (userId: string, event: string, data: any) => {
|
| 54 |
+
if (io) {
|
| 55 |
+
io.to(`user:${userId}`).emit(event, data);
|
| 56 |
+
}
|
| 57 |
+
};
|
| 58 |
+
|
| 59 |
+
/**
|
| 60 |
+
* 推送广播给所有管理员
|
| 61 |
+
*/
|
| 62 |
+
export const broadcastAdmin = (event: string, data: any) => {
|
| 63 |
+
if (io) {
|
| 64 |
+
// 假设管理员在一个特定的房间,或者直接广播
|
| 65 |
+
io.emit(`admin:${event}`, data);
|
| 66 |
+
}
|
| 67 |
+
};
|
api/lib/system.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import db from './db.js';
|
| 2 |
+
import crypto from 'crypto';
|
| 3 |
+
|
| 4 |
+
export class SystemService {
|
| 5 |
+
/**
|
| 6 |
+
* 记录审计日志
|
| 7 |
+
*/
|
| 8 |
+
static logAudit(userId: string | null, action: string, status: string, payload: any = {}, ip: string = '') {
|
| 9 |
+
try {
|
| 10 |
+
db.prepare(`
|
| 11 |
+
INSERT INTO audit_logs (user_id, action, status, payload, ip)
|
| 12 |
+
VALUES (?, ?, ?, ?, ?)
|
| 13 |
+
`).run(userId, action, status, JSON.stringify(payload), ip);
|
| 14 |
+
} catch (err) {
|
| 15 |
+
console.error('[AuditLog] 写入失败:', err);
|
| 16 |
+
}
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
/**
|
| 20 |
+
* 发送系统通知
|
| 21 |
+
*/
|
| 22 |
+
static async sendNotification(userId: string, title: string, content: string, type: string = 'info') {
|
| 23 |
+
const id = `NOTI_${crypto.randomBytes(4).toString('hex').toUpperCase()}`;
|
| 24 |
+
try {
|
| 25 |
+
db.prepare(`
|
| 26 |
+
INSERT INTO notifications (id, user_id, title, content, type)
|
| 27 |
+
VALUES (?, ?, ?, ?, ?)
|
| 28 |
+
`).run(id, userId, title, content, type);
|
| 29 |
+
console.log(`[Notification] 已发送至用户 ${userId}: ${title}`);
|
| 30 |
+
} catch (err) {
|
| 31 |
+
console.error('[Notification] 发送失败:', err);
|
| 32 |
+
}
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
/**
|
| 36 |
+
* 获取最近审计日志 (用于监控面板)
|
| 37 |
+
*/
|
| 38 |
+
static getRecentLogs(limit = 20) {
|
| 39 |
+
return db.prepare('SELECT * FROM audit_logs ORDER BY created_at DESC LIMIT ?').all(limit);
|
| 40 |
+
}
|
| 41 |
+
}
|
api/middleware/api-auth.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { type Request, type Response, type NextFunction } from 'express';
|
| 2 |
+
import db from '../lib/db.js';
|
| 3 |
+
|
| 4 |
+
export interface AuthenticatedRequest extends Request {
|
| 5 |
+
user: {
|
| 6 |
+
userId: string;
|
| 7 |
+
apiKeyId: string;
|
| 8 |
+
};
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
/**
|
| 12 |
+
* 验证外部 API Key (Bearer Token 格式)
|
| 13 |
+
* 支持 Header: Authorization: Bearer sk_...
|
| 14 |
+
*/
|
| 15 |
+
export const verifyApiKey = (req: Request, res: Response, next: NextFunction) => {
|
| 16 |
+
const authHeader = req.headers.authorization;
|
| 17 |
+
|
| 18 |
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
| 19 |
+
return res.status(401).json({
|
| 20 |
+
error: {
|
| 21 |
+
message: "Missing or invalid API key. Please provide your key in 'Authorization: Bearer sk_...' header.",
|
| 22 |
+
type: "invalid_request_error",
|
| 23 |
+
code: "api_key_missing"
|
| 24 |
+
}
|
| 25 |
+
});
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
const apiKey = authHeader.split(' ')[1];
|
| 29 |
+
|
| 30 |
+
try {
|
| 31 |
+
const keyRecord = db.prepare('SELECT id, user_id, last_used, status FROM api_keys WHERE key_secret = ?').get(apiKey) as any;
|
| 32 |
+
|
| 33 |
+
if (!keyRecord) {
|
| 34 |
+
return res.status(401).json({
|
| 35 |
+
error: {
|
| 36 |
+
message: "Incorrect API key provided.",
|
| 37 |
+
type: "authentication_error",
|
| 38 |
+
code: "invalid_api_key"
|
| 39 |
+
}
|
| 40 |
+
});
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
if (keyRecord.status === 'inactive') {
|
| 44 |
+
return res.status(401).json({
|
| 45 |
+
error: {
|
| 46 |
+
message: "API key is inactive.",
|
| 47 |
+
type: "authentication_error",
|
| 48 |
+
code: "inactive_api_key"
|
| 49 |
+
}
|
| 50 |
+
});
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
// 更新最后使用时间 (异步非阻塞)
|
| 54 |
+
db.prepare('UPDATE api_keys SET last_used = CURRENT_TIMESTAMP WHERE id = ?').run(keyRecord.id);
|
| 55 |
+
|
| 56 |
+
// 注入用户信息到请求对象
|
| 57 |
+
(req as any).user = {
|
| 58 |
+
userId: keyRecord.user_id,
|
| 59 |
+
apiKeyId: keyRecord.id
|
| 60 |
+
};
|
| 61 |
+
|
| 62 |
+
next();
|
| 63 |
+
} catch (err) {
|
| 64 |
+
console.error('[API Auth] Key validation error:', err);
|
| 65 |
+
res.status(500).json({
|
| 66 |
+
error: {
|
| 67 |
+
message: "Internal server error during authentication.",
|
| 68 |
+
type: "server_error",
|
| 69 |
+
code: "internal_error"
|
| 70 |
+
}
|
| 71 |
+
});
|
| 72 |
+
}
|
| 73 |
+
};
|