Просмотр исходного кода

refactor: 扁平化 backend 目录并完善群详情与成员展示

- 将 backend/backend 合并为 backend/,移除 lami-base-v1 子项目
- 群详情字段接库、Parse 超时、加载性能优化
- 群成员自动解析企微昵称,界面显示姓名而非 ID
- 更新 README 启动说明
0235699曾露 3 месяцев назад
Родитель
Сommit
525bdc00a8
100 измененных файлов с 2439 добавлено и 4190 удалено
  1. 2 3
      .gitignore
  2. 29 12
      README.md
  3. 2 0
      backend/.env.example
  4. 0 245
      backend/backend/src/apps/pc/qiwe/services/group-members.service.ts
  5. 0 0
      backend/docs/backend-design.md
  6. 0 0
      backend/docs/database-design.md
  7. 0 0
      backend/docs/deployment-baota.md
  8. 0 0
      backend/docs/group-organization-schema.md
  9. 1634 0
      backend/package-lock.json
  10. 0 0
      backend/package.json
  11. 0 0
      backend/pnpm-lock.yaml
  12. 0 0
      backend/src/apps/pc/app.ts
  13. 6 0
      backend/src/apps/pc/auth/constants.ts
  14. 0 0
      backend/src/apps/pc/auth/controllers/auth.controller.ts
  15. 0 0
      backend/src/apps/pc/auth/routes/auth.routes.ts
  16. 135 118
      backend/src/apps/pc/auth/services/auth.service.ts
  17. 0 0
      backend/src/apps/pc/auth/utils/password.util.ts
  18. 0 0
      backend/src/apps/pc/auth/utils/request.util.ts
  19. 0 0
      backend/src/apps/pc/dashboard/controllers/dashboard.controller.ts
  20. 0 0
      backend/src/apps/pc/dashboard/routes/dashboard.routes.ts
  21. 0 0
      backend/src/apps/pc/dashboard/services/dashboard.service.ts
  22. 0 0
      backend/src/apps/pc/health/controllers/health.controller.ts
  23. 0 0
      backend/src/apps/pc/health/routes/health.routes.ts
  24. 0 0
      backend/src/apps/pc/health/server.ts
  25. 0 0
      backend/src/apps/pc/qiwe/controllers/community.controller.ts
  26. 8 3
      backend/src/apps/pc/qiwe/controllers/groups.controller.ts
  27. 0 0
      backend/src/apps/pc/qiwe/controllers/messages.controller.ts
  28. 0 0
      backend/src/apps/pc/qiwe/controllers/org.controller.ts
  29. 0 0
      backend/src/apps/pc/qiwe/controllers/stores.controller.ts
  30. 0 0
      backend/src/apps/pc/qiwe/controllers/sync.controller.ts
  31. 0 0
      backend/src/apps/pc/qiwe/controllers/webhook.controller.ts
  32. 0 0
      backend/src/apps/pc/qiwe/routes/webhook.routes.ts
  33. 0 0
      backend/src/apps/pc/qiwe/services/community.service.ts
  34. 256 0
      backend/src/apps/pc/qiwe/services/group-members.service.ts
  35. 316 10
      backend/src/apps/pc/qiwe/services/groups.service.ts
  36. 0 0
      backend/src/apps/pc/qiwe/services/message-persist.service.ts
  37. 0 0
      backend/src/apps/pc/qiwe/services/message-sync.service.ts
  38. 0 0
      backend/src/apps/pc/qiwe/services/org-sync.service.ts
  39. 0 0
      backend/src/apps/pc/qiwe/services/organization.service.ts
  40. 0 0
      backend/src/apps/pc/qiwe/services/qiwe-api.service.ts
  41. 0 0
      backend/src/apps/pc/qiwe/services/sync.service.ts
  42. 0 0
      backend/src/apps/pc/qiwe/services/webhook.service.ts
  43. 0 0
      backend/src/apps/pc/qiwe/utils/group-metrics.util.ts
  44. 0 0
      backend/src/apps/pc/qiwe/utils/lifecycle.util.ts
  45. 4 1
      backend/src/index.ts
  46. 0 0
      backend/src/shared/auth/dashboard-scope.util.ts
  47. 0 0
      backend/src/shared/auth/data-scope.service.ts
  48. 0 0
      backend/src/shared/auth/require-user.ts
  49. 0 0
      backend/src/shared/config/env.ts
  50. 0 0
      backend/src/shared/db/migration.util.ts
  51. 0 0
      backend/src/shared/db/organization-bootstrap.ts
  52. 0 0
      backend/src/shared/db/parse-client.ts
  53. 13 6
      backend/src/shared/db/parse-health.service.ts
  54. 12 11
      backend/src/shared/db/schema-setup.ts
  55. 0 0
      backend/src/shared/errors/app-error.ts
  56. 0 0
      backend/src/shared/http/async-handler.ts
  57. 0 0
      backend/src/shared/http/error-handler.ts
  58. 0 0
      backend/src/shared/http/not-found.middleware.ts
  59. 0 0
      backend/src/shared/http/response.ts
  60. 22 0
      backend/src/shared/utils/with-timeout.ts
  61. 0 0
      backend/test-webhook.bat
  62. 0 0
      backend/tests_python/list_chat_rooms.mjs
  63. 0 0
      backend/tests_python/run_auto_test.mjs
  64. 0 0
      backend/tests_python/run_p0_scope_test.ps1
  65. 0 0
      backend/tsconfig.json
  66. 0 7
      lami-base-v1/.claude/settings.local.json
  67. 0 6
      lami-base-v1/.gitignore
  68. 0 48
      lami-base-v1/.trae/documents/learning_page_plan.md
  69. 0 31
      lami-base-v1/.trae/documents/profile_optimize_plan.md
  70. 0 86
      lami-base-v1/.trae/documents/qa_ai_logic_plan.md
  71. 0 16
      lami-base-v1/backend/.env.example
  72. 0 12
      lami-base-v1/backend/.gitignore
  73. 0 27
      lami-base-v1/backend/package.json
  74. 0 1030
      lami-base-v1/backend/pnpm-lock.yaml
  75. 0 24
      lami-base-v1/backend/src/apps/mobile/chat/app.ts
  76. 0 141
      lami-base-v1/backend/src/apps/mobile/chat/controllers/chat.controller.ts
  77. 0 8
      lami-base-v1/backend/src/apps/mobile/chat/routes/chat.routes.ts
  78. 0 11
      lami-base-v1/backend/src/apps/mobile/chat/server.ts
  79. 0 114
      lami-base-v1/backend/src/apps/pc/app.ts
  80. 0 155
      lami-base-v1/backend/src/apps/pc/community/controllers/community.controller.ts
  81. 0 69
      lami-base-v1/backend/src/apps/pc/community/models/community.model.ts
  82. 0 43
      lami-base-v1/backend/src/apps/pc/community/routes/community.routes.ts
  83. 0 189
      lami-base-v1/backend/src/apps/pc/community/services/community.service.ts
  84. 0 120
      lami-base-v1/backend/src/apps/pc/compliance/controllers/compliance.controller.ts
  85. 0 109
      lami-base-v1/backend/src/apps/pc/compliance/models/compliance.model.ts
  86. 0 28
      lami-base-v1/backend/src/apps/pc/compliance/routes/compliance.routes.ts
  87. 0 268
      lami-base-v1/backend/src/apps/pc/compliance/services/compliance.service.ts
  88. 0 155
      lami-base-v1/backend/src/apps/pc/content/controllers/content.controller.ts
  89. 0 94
      lami-base-v1/backend/src/apps/pc/content/models/content.model.ts
  90. 0 48
      lami-base-v1/backend/src/apps/pc/content/routes/content.routes.ts
  91. 0 246
      lami-base-v1/backend/src/apps/pc/content/services/content.service.ts
  92. 0 90
      lami-base-v1/backend/src/apps/pc/dashboard/controllers/dashboard.controller.ts
  93. 0 34
      lami-base-v1/backend/src/apps/pc/dashboard/routes/dashboard.routes.ts
  94. 0 229
      lami-base-v1/backend/src/apps/pc/dashboard/services/dashboard.service.ts
  95. 0 27
      lami-base-v1/backend/src/apps/pc/health/controllers/health.controller.ts
  96. 0 8
      lami-base-v1/backend/src/apps/pc/health/routes/health.routes.ts
  97. 0 34
      lami-base-v1/backend/src/apps/pc/health/server.ts
  98. 0 132
      lami-base-v1/backend/src/apps/pc/koc/controllers/koc.controller.ts
  99. 0 88
      lami-base-v1/backend/src/apps/pc/koc/models/koc.model.ts
  100. 0 54
      lami-base-v1/backend/src/apps/pc/koc/routes/koc.routes.ts

+ 2 - 3
.gitignore

@@ -48,10 +48,9 @@ Thumbs.db
 !.env.example
 
 # Temp / local artifacts
-_tmp_backend_zip/
-backend/backend/node_modules/
+backend/node_modules/
 lami-base-v1/.venv/
 lami-base-v1/**/node_modules/
 
 # Local docs (not for version control)
-backend/backend/docs/P0-功能改造说明.md
+backend/docs/P0-功能改造说明.md

+ 29 - 12
README.md

@@ -5,23 +5,24 @@
 | 目录 | 说明 |
 |------|------|
 | `src/` | **PC 前端**(Angular 20,本仓库主应用) |
-| `lami-base-v1/backend/` | **后端 API**(Express,PC `:3101`,Mobile `:3201`) |
-| `lami-base-v1/frontend/` | 后端联调/测试用前端(销售培训 Mobile,可保留) |
+| `backend/` | **后端 API**(Express,默认端口 `:3101`) |
 | `doc/` | 产品与设计文档 |
-| `lami-base-v1/doc/` | 后端接口与开发规范 |
+| `backend/docs/` | 后端设计、部署与数据库文档 |
+
+> `lami-base-v1/` 是早期演示/参考项目,接口路径为 `/api/qiwei/*`,**不是**本前端对接的后端。请勿与 `backend/` 同时占用 `3101` 端口。
 
 ## 本地开发
 
 ### 1. 启动后端
 
 ```bash
-cd lami-base-v1/backend
-pnpm install
-pnpm dev
+cd backend
+npm install   # 或 pnpm install
+npm run dev
 ```
 
 PC API:`http://localhost:3101/api`  
-Swagger:`http://localhost:3101/api-docs`
+健康检查:`http://localhost:3101/api/health`
 
 ### 2. 启动 PC 前端
 
@@ -31,7 +32,7 @@ pnpm install
 pnpm start
 ```
 
-前端:`http://localhost:4200`(通过 `proxy.conf.json` 代理 `/api` → 3101)
+前端:`http://localhost:4200`(通过 `proxy.conf.json` 代理 `/api` → `3101`
 
 ### 演示账号
 
@@ -45,11 +46,27 @@ pnpm start
 
 ## 环境变量(后端)
 
-见 `lami-base-v1/backend/.env.example`。生产环境务必配置:
+见 `backend/.env.example`。生产环境务必配置:
+
+- `PARSE_SERVER_URL` / `PARSE_APP_ID` / `PARSE_MASTER_KEY` — Parse 数据库
+- `QIWE_TOKEN` / `QIWE_GUID` — 企微 QiWe 平台(群同步、消息同步)
+- `PARSE_TIMEOUT_MS` — Parse 请求超时(默认 `8000`,避免数据库不可达时接口卡死)
+
+## 常见问题
+
+### 后端报 `EADDRINUSE :::3101`
+
+端口已被占用。先结束旧进程再启动:
+
+```powershell
+netstat -ano | findstr ":3101"
+taskkill /PID <PID> /F
+```
+
+### 前端提示「网络错误,请确认后端服务已启动」
 
-- `API_KEY` — 前端请求头 `X-API-Key`
-- `QIWEI_LIVE_ALLOW_BROADCAST=1` — 才允许真实群发
-- `QIWEI_WEBHOOK_SECRET` — Webhook 校验
+1. 确认启动的是 `backend/`,不是 `lami-base-v1/backend/`
+2. 访问 `http://localhost:3101/api/health`,若 `database.connected` 为 `false`,说明 Parse 数据库不可达,需检查 `PARSE_SERVER_URL` 对应服务是否正常运行
 
 ## 构建
 

+ 2 - 0
backend/backend/.env.example → backend/.env.example

@@ -5,6 +5,8 @@ PC_PORT=3101
 PARSE_APP_ID=lami-ai
 PARSE_MASTER_KEY=your-master-key
 PARSE_SERVER_URL=https://server.sh-lami.com/parse
+# Parse 请求超时(毫秒),避免数据库不可达时健康检查卡死
+PARSE_TIMEOUT_MS=8000
 
 # 企微 QiWe 平台(sync-groups 拉群列表需要)
 QIWE_API_BASE=https://manager.qiweapi.com/qiwe

+ 0 - 245
backend/backend/src/apps/pc/qiwe/services/group-members.service.ts

@@ -1,245 +0,0 @@
-import Parse from '../../../../shared/db/parse-client.js';
-import { batchGetRoomDetails, getContactDetailsBatch } from './qiwe-api.service.js';
-import { getOrgMemberName } from './org-sync.service.js';
-
-export interface GroupMemberDto {
-  id: string;
-  userId: string;
-  nickname: string;
-  status: string;
-  joinedAt: string | null;
-  leftAt: string | null;
-}
-
-const ACTIVE_QIWE_GUID = process.env.QIWE_GUID || '';
-
-function needsNickname(userId: string, nickname: string): boolean {
-  return !nickname || nickname === userId;
-}
-
-function toMemberDto(obj: Parse.Object): GroupMemberDto {
-  const userId = obj.get('userId') || '';
-  const nickname = obj.get('nickname') || '';
-  return {
-    id: obj.id!,
-    userId,
-    nickname: needsNickname(userId, nickname) ? userId : nickname,
-    status: obj.get('status') || 'active',
-    joinedAt: (obj.get('joinedAt') as Date)?.toISOString?.() ?? null,
-    leftAt: (obj.get('leftAt') as Date)?.toISOString?.() ?? null,
-  };
-}
-
-async function loadSenderNameMap(roomId: string): Promise<Map<string, string>> {
-  const map = new Map<string, string>();
-  const query = new Parse.Query('Message');
-  query.equalTo('roomId', roomId);
-  query.limit(1000);
-  query.select('senderId', 'senderName');
-  const rows = await query.find({ useMasterKey: true });
-  for (const row of rows) {
-    const userId = String(row.get('senderId') || '');
-    const name = String(row.get('senderName') || '');
-    if (!userId || !name || name === userId) continue;
-    map.set(userId, name);
-  }
-  return map;
-}
-
-async function persistMemberNickname(roomId: string, userId: string, nickname: string): Promise<void> {
-  if (!nickname || nickname === userId) return;
-  const query = new Parse.Query('GroupMember');
-  query.equalTo('roomId', roomId);
-  query.equalTo('userId', userId);
-  const existing = await query.first({ useMasterKey: true });
-  if (!existing) return;
-  const current = String(existing.get('nickname') || '');
-  if (current && current !== userId) return;
-  existing.set('nickname', nickname);
-  await existing.save(null, { useMasterKey: true });
-}
-
-async function enrichMemberNicknames(
-  members: GroupMemberDto[],
-  roomId: string,
-): Promise<GroupMemberDto[]> {
-  if (members.length === 0) return [];
-
-  const msgNames = await loadSenderNameMap(roomId);
-  const nameMap = new Map<string, string>();
-
-  for (const m of members) {
-    if (!needsNickname(m.userId, m.nickname)) {
-      nameMap.set(m.userId, m.nickname);
-      continue;
-    }
-    const fromMsg = msgNames.get(m.userId);
-    if (fromMsg) nameMap.set(m.userId, fromMsg);
-  }
-
-  const needIds = members
-    .map((m) => m.userId)
-    .filter((id) => id && needsNickname(id, nameMap.get(id) || id));
-
-  for (let i = 0; i < needIds.length; i += 50) {
-    const batch = needIds.slice(i, i + 50);
-    const contacts = await getContactDetailsBatch(batch);
-    for (const c of contacts) {
-      if (c.name && c.name !== c.userId) nameMap.set(c.userId, c.name);
-    }
-  }
-
-  const stillNeedOrg = needIds.filter((id) => needsNickname(id, nameMap.get(id) || id));
-  if (stillNeedOrg.length > 0) {
-    const orgQuery = new Parse.Query('OrgMember');
-    orgQuery.containedIn('userId', stillNeedOrg.slice(0, 100));
-    orgQuery.limit(100);
-    const orgRows = await orgQuery.find({ useMasterKey: true });
-    for (const row of orgRows) {
-      const uid = String(row.get('userId') || '');
-      const name = String(row.get('name') || '');
-      if (uid && name && name !== uid) nameMap.set(uid, name);
-    }
-    for (const id of stillNeedOrg.slice(0, 10)) {
-      if (nameMap.has(id) && nameMap.get(id) !== id) continue;
-      const orgName = await getOrgMemberName(id);
-      if (orgName && orgName !== id) nameMap.set(id, orgName);
-    }
-  }
-
-  const enriched = members.map((m) => {
-    const resolved =
-      (!needsNickname(m.userId, m.nickname) ? m.nickname : '')
-      || nameMap.get(m.userId)
-      || m.userId;
-    return { ...m, nickname: resolved };
-  });
-
-  await Promise.all(
-    enriched
-      .filter((m) => !needsNickname(m.userId, m.nickname))
-      .map((m) => persistMemberNickname(roomId, m.userId, m.nickname)),
-  );
-
-  return enriched;
-}
-
-async function upsertMemberFromQiWe(
-  roomId: string,
-  guid: string,
-  userId: string,
-  nickname: string,
-): Promise<void> {
-  const query = new Parse.Query('GroupMember');
-  query.equalTo('roomId', roomId);
-  query.equalTo('userId', userId);
-  query.equalTo('guid', guid);
-  const existing = await query.first({ useMasterKey: true });
-
-  if (existing) {
-    if (nickname && needsNickname(userId, String(existing.get('nickname') || ''))) {
-      existing.set('nickname', nickname);
-    }
-    if (existing.get('status') !== 'active') {
-      existing.set('status', 'active');
-      existing.unset('leftAt');
-    }
-    await existing.save(null, { useMasterKey: true });
-    return;
-  }
-
-  const obj = new Parse.Object('GroupMember');
-  obj.set('roomId', roomId);
-  obj.set('userId', userId);
-  obj.set('guid', guid);
-  obj.set('status', 'active');
-  if (nickname && nickname !== userId) obj.set('nickname', nickname);
-  await obj.save(null, { useMasterKey: true });
-}
-
-async function loadMembersFromMessages(roomId: string): Promise<GroupMemberDto[]> {
-  const query = new Parse.Query('Message');
-  query.equalTo('roomId', roomId);
-  query.limit(1000);
-  query.select('senderId', 'senderName');
-  const rows = await query.find({ useMasterKey: true });
-
-  const map = new Map<string, string>();
-  for (const row of rows) {
-    const userId = String(row.get('senderId') || '');
-    if (!userId) continue;
-    const name = String(row.get('senderName') || '');
-    if (!map.has(userId)) map.set(userId, userId);
-    if (name && name !== userId) map.set(userId, name);
-  }
-
-  return [...map.entries()].map(([userId, nickname]) => ({
-    id: `${roomId}:${userId}`,
-    userId,
-    nickname,
-    status: 'active',
-    joinedAt: null,
-    leftAt: null,
-  }));
-}
-
-async function loadMembersFromQiWe(roomId: string, guid: string): Promise<GroupMemberDto[]> {
-  const rooms = await batchGetRoomDetails(guid, [roomId]);
-  const room = rooms[0];
-  if (!room?.members?.length) return [];
-
-  const members: GroupMemberDto[] = [];
-  for (const mem of room.members) {
-    members.push({
-      id: `${roomId}:${mem.userId}`,
-      userId: mem.userId,
-      nickname: mem.nickname || mem.userId,
-      status: 'active',
-      joinedAt: null,
-      leftAt: null,
-    });
-  }
-
-  const enriched = await enrichMemberNicknames(members, roomId);
-  for (const m of enriched) {
-    await upsertMemberFromQiWe(roomId, guid, m.userId, m.nickname);
-  }
-  return enriched;
-}
-
-export async function listGroupMembersByRoom(
-  roomId: string,
-  includeLeft = false,
-): Promise<{ members: GroupMemberDto[]; total: number; source: 'db' | 'qiwe' }> {
-  const query = new Parse.Query('GroupMember');
-  query.equalTo('roomId', roomId);
-  if (!includeLeft) query.notEqualTo('status', 'left');
-  query.ascending('joinedAt');
-  query.limit(500);
-  const rows = await query.find({ useMasterKey: true });
-
-  let members: GroupMemberDto[] = [];
-  let source: 'db' | 'qiwe' = 'db';
-
-  if (rows.length > 0) {
-    members = rows.map(toMemberDto);
-  } else if (ACTIVE_QIWE_GUID) {
-    try {
-      const fromQiWe = await loadMembersFromQiWe(roomId, ACTIVE_QIWE_GUID);
-      if (fromQiWe.length > 0) {
-        members = fromQiWe;
-        source = 'qiwe';
-      }
-    } catch (err: unknown) {
-      const message = err instanceof Error ? err.message : String(err);
-      console.warn(`[GroupMember] 拉取群成员失败 roomId=${roomId}: ${message}`);
-    }
-  }
-
-  if (members.length === 0) {
-    members = await loadMembersFromMessages(roomId);
-  }
-
-  members = await enrichMemberNicknames(members, roomId);
-  return { members, total: members.length, source };
-}

+ 0 - 0
backend/backend/docs/backend-design.md → backend/docs/backend-design.md


+ 0 - 0
backend/backend/docs/database-design.md → backend/docs/database-design.md


+ 0 - 0
backend/backend/docs/deployment-baota.md → backend/docs/deployment-baota.md


+ 0 - 0
backend/backend/docs/group-organization-schema.md → backend/docs/group-organization-schema.md


+ 1634 - 0
backend/package-lock.json

@@ -0,0 +1,1634 @@
+{
+  "name": "qiwe-backend",
+  "version": "1.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "qiwe-backend",
+      "version": "1.0.0",
+      "dependencies": {
+        "cors": "^2.8.5",
+        "dotenv": "^16.4.0",
+        "express": "^4.21.0",
+        "parse": "^5.3.0"
+      },
+      "devDependencies": {
+        "@types/cors": "^2.8.17",
+        "@types/express": "^5.0.0",
+        "@types/node": "^22.0.0",
+        "tsx": "^4.19.0",
+        "typescript": "^5.5.0"
+      }
+    },
+    "node_modules/@babel/runtime-corejs3": {
+      "version": "7.24.7",
+      "resolved": "https://registry.npmmirror.com/@babel/runtime-corejs3/-/runtime-corejs3-7.24.7.tgz",
+      "integrity": "sha512-eytSX6JLBY6PVAeQa2bFlDx/7Mmln/gaEpsit5a3WEvjGfiIytEsgAwuIXCPM0xvw0v0cJn3ilq0/TvXrW0kgA==",
+      "license": "MIT",
+      "dependencies": {
+        "core-js-pure": "^3.30.2",
+        "regenerator-runtime": "^0.14.0"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@esbuild/aix-ppc64": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz",
+      "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "aix"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.28.0.tgz",
+      "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm64": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz",
+      "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-x64": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.28.0.tgz",
+      "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-arm64": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz",
+      "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-x64": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz",
+      "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-arm64": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz",
+      "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-x64": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz",
+      "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz",
+      "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm64": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz",
+      "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ia32": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz",
+      "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-loong64": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz",
+      "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-mips64el": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz",
+      "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==",
+      "cpu": [
+        "mips64el"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ppc64": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz",
+      "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-riscv64": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz",
+      "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-s390x": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz",
+      "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-x64": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz",
+      "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-arm64": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz",
+      "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-x64": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz",
+      "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-arm64": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz",
+      "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-x64": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz",
+      "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openharmony-arm64": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz",
+      "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/sunos-x64": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz",
+      "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-arm64": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz",
+      "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-ia32": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz",
+      "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-x64": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz",
+      "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@types/body-parser": {
+      "version": "1.19.6",
+      "resolved": "https://registry.npmmirror.com/@types/body-parser/-/body-parser-1.19.6.tgz",
+      "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/connect": "*",
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/connect": {
+      "version": "3.4.38",
+      "resolved": "https://registry.npmmirror.com/@types/connect/-/connect-3.4.38.tgz",
+      "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/cors": {
+      "version": "2.8.19",
+      "resolved": "https://registry.npmmirror.com/@types/cors/-/cors-2.8.19.tgz",
+      "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/express": {
+      "version": "5.0.6",
+      "resolved": "https://registry.npmmirror.com/@types/express/-/express-5.0.6.tgz",
+      "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/body-parser": "*",
+        "@types/express-serve-static-core": "^5.0.0",
+        "@types/serve-static": "^2"
+      }
+    },
+    "node_modules/@types/express-serve-static-core": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmmirror.com/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz",
+      "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*",
+        "@types/qs": "*",
+        "@types/range-parser": "*",
+        "@types/send": "*"
+      }
+    },
+    "node_modules/@types/http-errors": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmmirror.com/@types/http-errors/-/http-errors-2.0.5.tgz",
+      "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/node": {
+      "version": "22.19.20",
+      "resolved": "https://registry.npmmirror.com/@types/node/-/node-22.19.20.tgz",
+      "integrity": "sha512-6tELRwSDYWW9EdZhbeZmYGZ1/7Djkt+Ah3/ScEYT9cDord7UJzasR/4D3VONg9tQI5CDp+/CZC1AXj2pCFOvpw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "undici-types": "~6.21.0"
+      }
+    },
+    "node_modules/@types/qs": {
+      "version": "6.15.1",
+      "resolved": "https://registry.npmmirror.com/@types/qs/-/qs-6.15.1.tgz",
+      "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/range-parser": {
+      "version": "1.2.7",
+      "resolved": "https://registry.npmmirror.com/@types/range-parser/-/range-parser-1.2.7.tgz",
+      "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/send": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmmirror.com/@types/send/-/send-1.2.1.tgz",
+      "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/serve-static": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmmirror.com/@types/serve-static/-/serve-static-2.2.0.tgz",
+      "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/http-errors": "*",
+        "@types/node": "*"
+      }
+    },
+    "node_modules/accepts": {
+      "version": "1.3.8",
+      "resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz",
+      "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-types": "~2.1.34",
+        "negotiator": "0.6.3"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/array-flatten": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz",
+      "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
+      "license": "MIT"
+    },
+    "node_modules/body-parser": {
+      "version": "1.20.5",
+      "resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-1.20.5.tgz",
+      "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==",
+      "license": "MIT",
+      "dependencies": {
+        "bytes": "~3.1.2",
+        "content-type": "~1.0.5",
+        "debug": "2.6.9",
+        "depd": "2.0.0",
+        "destroy": "~1.2.0",
+        "http-errors": "~2.0.1",
+        "iconv-lite": "~0.4.24",
+        "on-finished": "~2.4.1",
+        "qs": "~6.15.1",
+        "raw-body": "~2.5.3",
+        "type-is": "~1.6.18",
+        "unpipe": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8",
+        "npm": "1.2.8000 || >= 1.4.16"
+      }
+    },
+    "node_modules/bytes": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz",
+      "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/call-bound": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz",
+      "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "get-intrinsic": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/content-disposition": {
+      "version": "0.5.4",
+      "resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-0.5.4.tgz",
+      "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+      "license": "MIT",
+      "dependencies": {
+        "safe-buffer": "5.2.1"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/content-type": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz",
+      "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/cookie": {
+      "version": "0.7.2",
+      "resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.2.tgz",
+      "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/cookie-signature": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.0.7.tgz",
+      "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
+      "license": "MIT"
+    },
+    "node_modules/core-js-pure": {
+      "version": "3.49.0",
+      "resolved": "https://registry.npmmirror.com/core-js-pure/-/core-js-pure-3.49.0.tgz",
+      "integrity": "sha512-XM4RFka59xATyJv/cS3O3Kml72hQXUeGRuuTmMYFxwzc9/7C8OYTaIR/Ji+Yt8DXzsFLNhat15cE/JP15HrCgw==",
+      "hasInstallScript": true,
+      "license": "MIT",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/core-js"
+      }
+    },
+    "node_modules/cors": {
+      "version": "2.8.6",
+      "resolved": "https://registry.npmmirror.com/cors/-/cors-2.8.6.tgz",
+      "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
+      "license": "MIT",
+      "dependencies": {
+        "object-assign": "^4",
+        "vary": "^1"
+      },
+      "engines": {
+        "node": ">= 0.10"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/crypto-js": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmmirror.com/crypto-js/-/crypto-js-4.2.0.tgz",
+      "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
+      "license": "MIT",
+      "optional": true
+    },
+    "node_modules/debug": {
+      "version": "2.6.9",
+      "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz",
+      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "2.0.0"
+      }
+    },
+    "node_modules/depd": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz",
+      "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/destroy": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmmirror.com/destroy/-/destroy-1.2.0.tgz",
+      "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8",
+        "npm": "1.2.8000 || >= 1.4.16"
+      }
+    },
+    "node_modules/dotenv": {
+      "version": "16.6.1",
+      "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-16.6.1.tgz",
+      "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://dotenvx.com"
+      }
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/ee-first": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz",
+      "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+      "license": "MIT"
+    },
+    "node_modules/encodeurl": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz",
+      "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
+      "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/esbuild": {
+      "version": "0.28.0",
+      "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.28.0.tgz",
+      "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "optionalDependencies": {
+        "@esbuild/aix-ppc64": "0.28.0",
+        "@esbuild/android-arm": "0.28.0",
+        "@esbuild/android-arm64": "0.28.0",
+        "@esbuild/android-x64": "0.28.0",
+        "@esbuild/darwin-arm64": "0.28.0",
+        "@esbuild/darwin-x64": "0.28.0",
+        "@esbuild/freebsd-arm64": "0.28.0",
+        "@esbuild/freebsd-x64": "0.28.0",
+        "@esbuild/linux-arm": "0.28.0",
+        "@esbuild/linux-arm64": "0.28.0",
+        "@esbuild/linux-ia32": "0.28.0",
+        "@esbuild/linux-loong64": "0.28.0",
+        "@esbuild/linux-mips64el": "0.28.0",
+        "@esbuild/linux-ppc64": "0.28.0",
+        "@esbuild/linux-riscv64": "0.28.0",
+        "@esbuild/linux-s390x": "0.28.0",
+        "@esbuild/linux-x64": "0.28.0",
+        "@esbuild/netbsd-arm64": "0.28.0",
+        "@esbuild/netbsd-x64": "0.28.0",
+        "@esbuild/openbsd-arm64": "0.28.0",
+        "@esbuild/openbsd-x64": "0.28.0",
+        "@esbuild/openharmony-arm64": "0.28.0",
+        "@esbuild/sunos-x64": "0.28.0",
+        "@esbuild/win32-arm64": "0.28.0",
+        "@esbuild/win32-ia32": "0.28.0",
+        "@esbuild/win32-x64": "0.28.0"
+      }
+    },
+    "node_modules/escape-html": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz",
+      "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+      "license": "MIT"
+    },
+    "node_modules/etag": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz",
+      "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/express": {
+      "version": "4.22.2",
+      "resolved": "https://registry.npmmirror.com/express/-/express-4.22.2.tgz",
+      "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==",
+      "license": "MIT",
+      "dependencies": {
+        "accepts": "~1.3.8",
+        "array-flatten": "1.1.1",
+        "body-parser": "~1.20.5",
+        "content-disposition": "~0.5.4",
+        "content-type": "~1.0.4",
+        "cookie": "~0.7.1",
+        "cookie-signature": "~1.0.6",
+        "debug": "2.6.9",
+        "depd": "2.0.0",
+        "encodeurl": "~2.0.0",
+        "escape-html": "~1.0.3",
+        "etag": "~1.8.1",
+        "finalhandler": "~1.3.1",
+        "fresh": "~0.5.2",
+        "http-errors": "~2.0.0",
+        "merge-descriptors": "1.0.3",
+        "methods": "~1.1.2",
+        "on-finished": "~2.4.1",
+        "parseurl": "~1.3.3",
+        "path-to-regexp": "~0.1.12",
+        "proxy-addr": "~2.0.7",
+        "qs": "~6.15.1",
+        "range-parser": "~1.2.1",
+        "safe-buffer": "5.2.1",
+        "send": "~0.19.0",
+        "serve-static": "~1.16.2",
+        "setprototypeof": "1.2.0",
+        "statuses": "~2.0.1",
+        "type-is": "~1.6.18",
+        "utils-merge": "1.0.1",
+        "vary": "~1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.10.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/finalhandler": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-1.3.2.tgz",
+      "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "2.6.9",
+        "encodeurl": "~2.0.0",
+        "escape-html": "~1.0.3",
+        "on-finished": "~2.4.1",
+        "parseurl": "~1.3.3",
+        "statuses": "~2.0.2",
+        "unpipe": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/forwarded": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz",
+      "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/fresh": {
+      "version": "0.5.2",
+      "resolved": "https://registry.npmmirror.com/fresh/-/fresh-0.5.2.tgz",
+      "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "license": "MIT",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.4.tgz",
+      "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/http-errors": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.1.tgz",
+      "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+      "license": "MIT",
+      "dependencies": {
+        "depd": "~2.0.0",
+        "inherits": "~2.0.4",
+        "setprototypeof": "~1.2.0",
+        "statuses": "~2.0.2",
+        "toidentifier": "~1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/iconv-lite": {
+      "version": "0.4.24",
+      "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz",
+      "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+      "license": "MIT",
+      "dependencies": {
+        "safer-buffer": ">= 2.1.2 < 3"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/idb-keyval": {
+      "version": "6.2.1",
+      "resolved": "https://registry.npmmirror.com/idb-keyval/-/idb-keyval-6.2.1.tgz",
+      "integrity": "sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==",
+      "license": "Apache-2.0"
+    },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+      "license": "ISC"
+    },
+    "node_modules/ipaddr.js": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+      "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/media-typer": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-0.3.0.tgz",
+      "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/merge-descriptors": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+      "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/methods": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmmirror.com/methods/-/methods-1.1.2.tgz",
+      "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz",
+      "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+      "license": "MIT",
+      "bin": {
+        "mime": "cli.js"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+      "license": "MIT"
+    },
+    "node_modules/negotiator": {
+      "version": "0.6.3",
+      "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz",
+      "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/object-assign": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz",
+      "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/object-inspect": {
+      "version": "1.13.4",
+      "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz",
+      "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/on-finished": {
+      "version": "2.4.1",
+      "resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz",
+      "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+      "license": "MIT",
+      "dependencies": {
+        "ee-first": "1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/parse": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmmirror.com/parse/-/parse-5.3.0.tgz",
+      "integrity": "sha512-mWBnE6hHJhdvlx5KPQcYgCGRdgqKhPw+5fSC0j7vOfse3Lkh3xtDwOfmDpvv2LXZVBj72G/mgVKMRmbAICRzkQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@babel/runtime-corejs3": "7.24.7",
+        "idb-keyval": "6.2.1",
+        "react-native-crypto-js": "1.0.0",
+        "uuid": "10.0.0",
+        "ws": "8.17.1",
+        "xmlhttprequest": "1.8.0"
+      },
+      "engines": {
+        "node": "18 || 19 || 20 || 22"
+      },
+      "optionalDependencies": {
+        "crypto-js": "4.2.0"
+      }
+    },
+    "node_modules/parseurl": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz",
+      "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/path-to-regexp": {
+      "version": "0.1.13",
+      "resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
+      "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
+      "license": "MIT"
+    },
+    "node_modules/proxy-addr": {
+      "version": "2.0.7",
+      "resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz",
+      "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+      "license": "MIT",
+      "dependencies": {
+        "forwarded": "0.2.0",
+        "ipaddr.js": "1.9.1"
+      },
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/qs": {
+      "version": "6.15.2",
+      "resolved": "https://registry.npmmirror.com/qs/-/qs-6.15.2.tgz",
+      "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "side-channel": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=0.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/range-parser": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz",
+      "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/raw-body": {
+      "version": "2.5.3",
+      "resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-2.5.3.tgz",
+      "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
+      "license": "MIT",
+      "dependencies": {
+        "bytes": "~3.1.2",
+        "http-errors": "~2.0.1",
+        "iconv-lite": "~0.4.24",
+        "unpipe": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/react-native-crypto-js": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/react-native-crypto-js/-/react-native-crypto-js-1.0.0.tgz",
+      "integrity": "sha512-FNbLuG/HAdapQoybeZSoes1PWdOj0w242gb+e1R0hicf3Gyj/Mf8M9NaED2AnXVOX01b2FXomwUiw1xP1K+8sA==",
+      "license": "MIT"
+    },
+    "node_modules/regenerator-runtime": {
+      "version": "0.14.1",
+      "resolved": "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
+      "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
+      "license": "MIT"
+    },
+    "node_modules/safe-buffer": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz",
+      "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+      "license": "MIT"
+    },
+    "node_modules/send": {
+      "version": "0.19.2",
+      "resolved": "https://registry.npmmirror.com/send/-/send-0.19.2.tgz",
+      "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "2.6.9",
+        "depd": "2.0.0",
+        "destroy": "1.2.0",
+        "encodeurl": "~2.0.0",
+        "escape-html": "~1.0.3",
+        "etag": "~1.8.1",
+        "fresh": "~0.5.2",
+        "http-errors": "~2.0.1",
+        "mime": "1.6.0",
+        "ms": "2.1.3",
+        "on-finished": "~2.4.1",
+        "range-parser": "~1.2.1",
+        "statuses": "~2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/send/node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/serve-static": {
+      "version": "1.16.3",
+      "resolved": "https://registry.npmmirror.com/serve-static/-/serve-static-1.16.3.tgz",
+      "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
+      "license": "MIT",
+      "dependencies": {
+        "encodeurl": "~2.0.0",
+        "escape-html": "~1.0.3",
+        "parseurl": "~1.3.3",
+        "send": "~0.19.1"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/setprototypeof": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz",
+      "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+      "license": "ISC"
+    },
+    "node_modules/side-channel": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.1.tgz",
+      "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.4",
+        "side-channel-list": "^1.0.1",
+        "side-channel-map": "^1.0.1",
+        "side-channel-weakmap": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-list": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.1.tgz",
+      "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.4"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-map": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz",
+      "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/side-channel-weakmap": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+      "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3",
+        "side-channel-map": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/statuses": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.2.tgz",
+      "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/toidentifier": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz",
+      "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.6"
+      }
+    },
+    "node_modules/tsx": {
+      "version": "4.22.4",
+      "resolved": "https://registry.npmmirror.com/tsx/-/tsx-4.22.4.tgz",
+      "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "esbuild": "~0.28.0"
+      },
+      "bin": {
+        "tsx": "dist/cli.mjs"
+      },
+      "engines": {
+        "node": ">=18.0.0"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
+      }
+    },
+    "node_modules/type-is": {
+      "version": "1.6.18",
+      "resolved": "https://registry.npmmirror.com/type-is/-/type-is-1.6.18.tgz",
+      "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+      "license": "MIT",
+      "dependencies": {
+        "media-typer": "0.3.0",
+        "mime-types": "~2.1.24"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/typescript": {
+      "version": "5.9.3",
+      "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz",
+      "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=14.17"
+      }
+    },
+    "node_modules/undici-types": {
+      "version": "6.21.0",
+      "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz",
+      "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/unpipe": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz",
+      "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/utils-merge": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmmirror.com/utils-merge/-/utils-merge-1.0.1.tgz",
+      "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4.0"
+      }
+    },
+    "node_modules/uuid": {
+      "version": "10.0.0",
+      "resolved": "https://registry.npmmirror.com/uuid/-/uuid-10.0.0.tgz",
+      "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
+      "funding": [
+        "https://github.com/sponsors/broofa",
+        "https://github.com/sponsors/ctavan"
+      ],
+      "license": "MIT",
+      "bin": {
+        "uuid": "dist/bin/uuid"
+      }
+    },
+    "node_modules/vary": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz",
+      "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/ws": {
+      "version": "8.17.1",
+      "resolved": "https://registry.npmmirror.com/ws/-/ws-8.17.1.tgz",
+      "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "peerDependencies": {
+        "bufferutil": "^4.0.1",
+        "utf-8-validate": ">=5.0.2"
+      },
+      "peerDependenciesMeta": {
+        "bufferutil": {
+          "optional": true
+        },
+        "utf-8-validate": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/xmlhttprequest": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmmirror.com/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz",
+      "integrity": "sha512-58Im/U0mlVBLM38NdZjHyhuMtCqa61469k2YP/AaPbvCoV9aQGUpbJBj1QRm2ytRiVQBD/fsw7L2bJGDVQswBA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    }
+  }
+}

+ 0 - 0
backend/backend/package.json → backend/package.json


+ 0 - 0
backend/backend/pnpm-lock.yaml → backend/pnpm-lock.yaml


+ 0 - 0
backend/backend/src/apps/pc/app.ts → backend/src/apps/pc/app.ts


+ 6 - 0
backend/src/apps/pc/auth/constants.ts

@@ -0,0 +1,6 @@
+/** 本系统(社群运营端)在 Parse _User 表中的来源标识 */
+export const APP_SOURCE = 'shequn';
+
+export function buildParseUsername(phone: string): string {
+  return `${APP_SOURCE}:${phone.trim()}`;
+}

+ 0 - 0
backend/backend/src/apps/pc/auth/controllers/auth.controller.ts → backend/src/apps/pc/auth/controllers/auth.controller.ts


+ 0 - 0
backend/backend/src/apps/pc/auth/routes/auth.routes.ts → backend/src/apps/pc/auth/routes/auth.routes.ts


+ 135 - 118
backend/backend/src/apps/pc/auth/services/auth.service.ts → backend/src/apps/pc/auth/services/auth.service.ts

@@ -1,8 +1,7 @@
-import { randomBytes } from 'crypto';
 import Parse from '../../../../shared/db/parse-client.js';
 import { AppError } from '../../../../shared/errors/app-error.js';
-import { hashPassword, verifyPassword } from '../utils/password.util.js';
 import { getDefaultStore } from '../../qiwe/services/organization.service.js';
+import { APP_SOURCE, buildParseUsername } from '../constants.js';
 
 export type UserRole = 'director' | 'regional_supervisor' | 'store_manager';
 
@@ -34,30 +33,24 @@ const LEGACY_ROLE_MAP: Record<string, UserRole> = {
   single_group: 'store_manager',
   store_manager: 'store_manager',
 };
-const SESSION_DAYS_REMEMBER = 30;
-const SESSION_DAYS_DEFAULT = 1;
 
-function toUserDto(obj: Parse.Object): UserDto {
+function toUserDto(user: Parse.User): UserDto {
   return {
-    id: obj.id!,
-    name: obj.get('name') || '',
-    email: obj.get('email') || '',
-    phone: obj.get('phone') || '',
-    role: obj.get('role') as UserRole,
-    storeId: obj.get('storeId') || '',
-    storeName: obj.get('storeName') || '',
-    wecomUserId: obj.get('wecomUserId') || '',
-    regionCode: obj.get('regionCode') || '',
-    department: obj.get('department') || '',
-    position: obj.get('position') || '',
-    createdAt: (obj.get('createdAt') as Date)?.toISOString?.() || new Date().toISOString(),
+    id: user.id!,
+    name: user.get('name') || '',
+    email: user.get('email') || '',
+    phone: user.get('phone') || '',
+    role: user.get('role') as UserRole,
+    storeId: user.get('storeId') || '',
+    storeName: user.get('storeName') || '',
+    wecomUserId: user.get('wecomUserId') || '',
+    regionCode: user.get('regionCode') || '',
+    department: user.get('department') || '',
+    position: user.get('position') || '',
+    createdAt: (user.get('createdAt') as Date)?.toISOString?.() || new Date().toISOString(),
   };
 }
 
-function newToken(): string {
-  return randomBytes(32).toString('hex');
-}
-
 function normalizeRole(role: string): UserRole | null {
   if (VALID_ROLES.includes(role as UserRole)) {
     return role as UserRole;
@@ -65,7 +58,7 @@ function normalizeRole(role: string): UserRole | null {
   return LEGACY_ROLE_MAP[role] ?? null;
 }
 
-async function ensureUserRole(user: Parse.Object): Promise<UserRole> {
+async function ensureUserRole(user: Parse.User): Promise<UserRole> {
   const rawRole = user.get('role') as string;
   const normalized = normalizeRole(rawRole);
   if (!normalized) {
@@ -90,60 +83,66 @@ function isValidEmail(email: string): boolean {
   return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim().toLowerCase());
 }
 
-async function findUserByPhone(phone: string): Promise<Parse.Object | undefined> {
-  const query = new Parse.Query('AppUser');
+function userQuery(): Parse.Query<Parse.User> {
+  const query = new Parse.Query(Parse.User);
+  query.equalTo('appSource', APP_SOURCE);
+  return query;
+}
+
+async function findUserByPhone(phone: string): Promise<Parse.User | undefined> {
+  const query = userQuery();
   query.equalTo('phone', normalizePhone(phone));
   return (await query.first({ useMasterKey: true })) ?? undefined;
 }
 
-async function findUserByEmail(email: string): Promise<Parse.Object | undefined> {
-  const query = new Parse.Query('AppUser');
+async function findUserByEmail(email: string): Promise<Parse.User | undefined> {
+  const query = userQuery();
   query.equalTo('email', email.trim().toLowerCase());
   return (await query.first({ useMasterKey: true })) ?? undefined;
 }
 
-async function createSession(userId: string, rememberMe: boolean): Promise<string> {
-  const token = newToken();
-  const days = rememberMe ? SESSION_DAYS_REMEMBER : SESSION_DAYS_DEFAULT;
-  const expiresAt = new Date(Date.now() + days * 24 * 60 * 60 * 1000);
-
-  const session = new Parse.Object('AuthSession');
-  session.set('token', token);
-  session.set('userId', userId);
-  session.set('expiresAt', expiresAt);
-  await session.save(null, { useMasterKey: true });
-  return token;
+async function getUserById(userId: string): Promise<Parse.User> {
+  const query = userQuery();
+  return query.get(userId, { useMasterKey: true });
+}
+
+async function loginParseUser(username: string, password: string): Promise<Parse.User> {
+  try {
+    return await Parse.User.logIn(username, password);
+  } catch {
+    throw new AppError(401, 'AUTH_FAILED', '手机号或密码错误');
+  }
 }
 
 export async function loginWithRemember(
   phone: string,
   password: string,
-  rememberMe: boolean,
+  _rememberMe: boolean,
 ): Promise<{ user: UserDto; token: string }> {
   if (!isValidPhone(phone)) {
     throw new AppError(400, 'INVALID_PHONE', '手机号格式不正确');
   }
 
-  const userObj = await findUserByPhone(phone);
-  if (!userObj) {
+  const userRecord = await findUserByPhone(phone);
+  if (!userRecord) {
     throw new AppError(401, 'AUTH_FAILED', '手机号或密码错误');
   }
 
-  const hash = userObj.get('passwordHash') as string;
-  const salt = userObj.get('passwordSalt') as string;
-  if (!verifyPassword(password, hash, salt)) {
-    throw new AppError(401, 'AUTH_FAILED', '手机号或密码错误');
-  }
+  const username = userRecord.get('username') as string;
+  const loggedIn = await loginParseUser(username, password);
+  await ensureUserRole(loggedIn);
 
-  await ensureUserRole(userObj);
+  const token = loggedIn.getSessionToken();
+  if (!token) {
+    throw new AppError(500, 'SESSION_ERROR', '登录会话创建失败');
+  }
 
-  const token = await createSession(userObj.id!, rememberMe);
-  return { user: toUserDto(userObj), token };
+  return { user: toUserDto(loggedIn), token };
 }
 
 export async function logoutByToken(token: string): Promise<void> {
-  const sessionQuery = new Parse.Query('AuthSession');
-  sessionQuery.equalTo('token', token);
+  const sessionQuery = new Parse.Query(Parse.Session);
+  sessionQuery.equalTo('sessionToken', token);
   const session = await sessionQuery.first({ useMasterKey: true });
   if (session) {
     await session.destroy({ useMasterKey: true });
@@ -183,20 +182,33 @@ export async function register(data: {
   }
 
   const defaultStore = await getDefaultStore();
-  const { hash, salt } = hashPassword(data.password);
-  const obj = new Parse.Object('AppUser');
-  obj.set('email', email);
-  obj.set('passwordHash', hash);
-  obj.set('passwordSalt', salt);
-  obj.set('name', data.name.trim());
-  obj.set('phone', phone);
-  obj.set('role', DEFAULT_REGISTER_ROLE);
-  obj.set('storeId', defaultStore.storeId);
-  obj.set('storeName', defaultStore.storeName);
-  obj.set('department', '运营部');
-  obj.set('position', '新注册用户');
-  await obj.save(null, { useMasterKey: true });
-  return toUserDto(obj);
+  const username = buildParseUsername(phone);
+
+  try {
+    const user = await Parse.User.signUp(
+      username,
+      data.password,
+      {
+        phone,
+        appSource: APP_SOURCE,
+        email,
+        name: data.name.trim(),
+        role: DEFAULT_REGISTER_ROLE,
+        storeId: defaultStore.storeId,
+        storeName: defaultStore.storeName,
+        department: '运营部',
+        position: '新注册用户',
+      },
+      { useMasterKey: true },
+    );
+    return toUserDto(user);
+  } catch (err: unknown) {
+    const msg = err instanceof Error ? err.message : String(err);
+    if (msg.includes('already exists') || msg.includes('Account already exists')) {
+      throw new AppError(400, 'PHONE_EXISTS', '该手机号已被注册');
+    }
+    throw new AppError(400, 'REGISTER_FAILED', msg || '注册失败');
+  }
 }
 
 export async function resetPasswordByEmail(email: string, newPassword: string): Promise<void> {
@@ -216,13 +228,11 @@ export async function resetPasswordByEmail(email: string, newPassword: string):
     throw new AppError(404, 'EMAIL_NOT_FOUND', '该邮箱未注册');
   }
 
-  const next = hashPassword(newPassword);
-  user.set('passwordHash', next.hash);
-  user.set('passwordSalt', next.salt);
+  user.set('password', newPassword);
   await user.save(null, { useMasterKey: true });
 
-  const sessionQuery = new Parse.Query('AuthSession');
-  sessionQuery.equalTo('userId', user.id!);
+  const sessionQuery = new Parse.Query(Parse.Session);
+  sessionQuery.equalTo('user', user);
   const sessions = await sessionQuery.find({ useMasterKey: true });
   if (sessions.length > 0) {
     await Parse.Object.destroyAll(sessions, { useMasterKey: true });
@@ -230,20 +240,23 @@ export async function resetPasswordByEmail(email: string, newPassword: string):
 }
 
 export async function getUserByToken(token: string): Promise<UserDto | null> {
-  const sessionQuery = new Parse.Query('AuthSession');
-  sessionQuery.equalTo('token', token);
+  const sessionQuery = new Parse.Query(Parse.Session);
+  sessionQuery.equalTo('sessionToken', token);
   const session = await sessionQuery.first({ useMasterKey: true });
   if (!session) return null;
 
-  const expiresAt = session.get('expiresAt') as Date;
+  const expiresAt = session.get('expiresAt') as Date | undefined;
   if (expiresAt && expiresAt.getTime() < Date.now()) {
     await session.destroy({ useMasterKey: true });
     return null;
   }
 
-  const userQuery = new Parse.Query('AppUser');
-  const user = await userQuery.get(session.get('userId') as string, { useMasterKey: true });
-  return user ? toUserDto(user) : null;
+  const userPtr = session.get('user');
+  if (!userPtr) return null;
+
+  const user = await getUserById(userPtr.id);
+  if (user.get('appSource') !== APP_SOURCE) return null;
+  return toUserDto(user);
 }
 
 export async function changePassword(
@@ -251,15 +264,11 @@ export async function changePassword(
   currentPassword: string,
   newPassword: string,
 ): Promise<void> {
-  const user = await new Parse.Query('AppUser').get(userId, { useMasterKey: true });
-  const hash = user.get('passwordHash') as string;
-  const salt = user.get('passwordSalt') as string;
-  if (!verifyPassword(currentPassword, hash, salt)) {
-    throw new AppError(400, 'PASSWORD_CHANGE_FAILED', '当前密码不正确');
-  }
-  const next = hashPassword(newPassword);
-  user.set('passwordHash', next.hash);
-  user.set('passwordSalt', next.salt);
+  const user = await getUserById(userId);
+  const username = user.get('username') as string;
+  await loginParseUser(username, currentPassword);
+
+  user.set('password', newPassword);
   await user.save(null, { useMasterKey: true });
 }
 
@@ -267,9 +276,20 @@ export async function updateProfile(
   userId: string,
   patch: { name?: string; phone?: string; position?: string },
 ): Promise<UserDto> {
-  const user = await new Parse.Query('AppUser').get(userId, { useMasterKey: true });
+  const user = await getUserById(userId);
   if (patch.name !== undefined) user.set('name', patch.name.trim());
-  if (patch.phone !== undefined) user.set('phone', patch.phone.trim());
+  if (patch.phone !== undefined) {
+    const nextPhone = normalizePhone(patch.phone);
+    if (!isValidPhone(nextPhone)) {
+      throw new AppError(400, 'INVALID_PHONE', '手机号格式不正确');
+    }
+    const conflict = await findUserByPhone(nextPhone);
+    if (conflict && conflict.id !== user.id) {
+      throw new AppError(400, 'PHONE_EXISTS', '该手机号已被其他账号使用');
+    }
+    user.set('phone', nextPhone);
+    user.set('username', buildParseUsername(nextPhone));
+  }
   if (patch.position !== undefined) user.set('position', patch.position.trim());
   await user.save(null, { useMasterKey: true });
   return toUserDto(user);
@@ -292,7 +312,7 @@ const SEED_USERS: Array<{
 ];
 
 async function migrateLegacyRoles(): Promise<void> {
-  const query = new Parse.Query('AppUser');
+  const query = userQuery();
   query.limit(1000);
   const users = await query.find({ useMasterKey: true });
   let migrated = 0;
@@ -318,49 +338,46 @@ async function upsertSeedUsers(): Promise<void> {
   const defaultStore = await getDefaultStore();
 
   for (const seed of SEED_USERS) {
-    const existing = await findUserByEmail(seed.email);
-    const { hash, salt } = hashPassword(seed.password);
+    const phone = normalizePhone(seed.phone);
+    const username = buildParseUsername(phone);
+    const existing = await findUserByPhone(phone);
+
+    const profile = {
+      phone,
+      appSource: APP_SOURCE,
+      email: seed.email,
+      name: seed.name,
+      role: seed.role,
+      wecomUserId: seed.wecomUserId,
+      regionCode: seed.regionCode,
+      department: seed.department,
+      position: seed.position,
+      ...(seed.role !== 'director'
+        ? { storeId: defaultStore.storeId, storeName: defaultStore.storeName }
+        : { storeId: '', storeName: '' }),
+    };
 
     if (existing) {
-      existing.set('passwordHash', hash);
-      existing.set('passwordSalt', salt);
-      existing.set('name', seed.name);
-      existing.set('phone', seed.phone);
-      existing.set('role', seed.role);
-      if (seed.role !== 'director') {
-        existing.set('storeId', defaultStore.storeId);
-        existing.set('storeName', defaultStore.storeName);
+      existing.set('password', seed.password);
+      for (const [key, value] of Object.entries(profile)) {
+        existing.set(key, value);
       }
-      existing.set('wecomUserId', seed.wecomUserId);
-      existing.set('regionCode', seed.regionCode);
-      existing.set('department', seed.department);
-      existing.set('position', seed.position);
       await existing.save(null, { useMasterKey: true });
       updated++;
       continue;
     }
 
-    const obj = new Parse.Object('AppUser');
-    obj.set('email', seed.email);
-    obj.set('passwordHash', hash);
-    obj.set('passwordSalt', salt);
-    obj.set('name', seed.name);
-    obj.set('phone', seed.phone);
-    obj.set('role', seed.role);
-    if (seed.role !== 'director') {
-      obj.set('storeId', defaultStore.storeId);
-      obj.set('storeName', defaultStore.storeName);
+    try {
+      await Parse.User.signUp(username, seed.password, profile, { useMasterKey: true });
+      created++;
+    } catch (err: unknown) {
+      const msg = err instanceof Error ? err.message : String(err);
+      console.warn(`[Auth] 演示账号 ${phone} 创建失败: ${msg}`);
     }
-    obj.set('wecomUserId', seed.wecomUserId);
-    obj.set('regionCode', seed.regionCode);
-    obj.set('department', seed.department);
-    obj.set('position', seed.position);
-    await obj.save(null, { useMasterKey: true });
-    created++;
   }
 
   if (created > 0 || updated > 0) {
-    console.log(`[Auth] 演示账号已同步:新增 ${created},更新 ${updated}`);
+    console.log(`[Auth] 演示账号已同步到 Parse _User:新增 ${created},更新 ${updated}`);
   }
 }
 

+ 0 - 0
backend/backend/src/apps/pc/auth/utils/password.util.ts → backend/src/apps/pc/auth/utils/password.util.ts


+ 0 - 0
backend/backend/src/apps/pc/auth/utils/request.util.ts → backend/src/apps/pc/auth/utils/request.util.ts


+ 0 - 0
backend/backend/src/apps/pc/dashboard/controllers/dashboard.controller.ts → backend/src/apps/pc/dashboard/controllers/dashboard.controller.ts


+ 0 - 0
backend/backend/src/apps/pc/dashboard/routes/dashboard.routes.ts → backend/src/apps/pc/dashboard/routes/dashboard.routes.ts


+ 0 - 0
backend/backend/src/apps/pc/dashboard/services/dashboard.service.ts → backend/src/apps/pc/dashboard/services/dashboard.service.ts


+ 0 - 0
backend/backend/src/apps/pc/health/controllers/health.controller.ts → backend/src/apps/pc/health/controllers/health.controller.ts


+ 0 - 0
backend/backend/src/apps/pc/health/routes/health.routes.ts → backend/src/apps/pc/health/routes/health.routes.ts


+ 0 - 0
backend/backend/src/apps/pc/health/server.ts → backend/src/apps/pc/health/server.ts


+ 0 - 0
backend/backend/src/apps/pc/qiwe/controllers/community.controller.ts → backend/src/apps/pc/qiwe/controllers/community.controller.ts


+ 8 - 3
backend/backend/src/apps/pc/qiwe/controllers/groups.controller.ts → backend/src/apps/pc/qiwe/controllers/groups.controller.ts

@@ -83,7 +83,14 @@ export async function handleListGroupMembers(req: Request, res: Response): Promi
     throw new AppError(400, 'INVALID_PARAMS', '缺少 roomId');
   }
 
-  const group = await getGroupChatByRoomId(roomId);
+  const includeLeft = parseBoolean(req.query.includeLeft) === true;
+  const resolveExternal = parseBoolean(req.query.resolveExternal) === true;
+
+  const [group, result] = await Promise.all([
+    getGroupChatByRoomId(roomId),
+    listGroupMembersByRoom(roomId, includeLeft, resolveExternal),
+  ]);
+
   if (!group) {
     throw new AppError(404, 'GROUP_NOT_FOUND', '群不存在');
   }
@@ -96,7 +103,5 @@ export async function handleListGroupMembers(req: Request, res: Response): Promi
     throw new AppError(403, 'SCOPE_DENIED', '无权查看该群');
   }
 
-  const includeLeft = parseBoolean(req.query.includeLeft) === true;
-  const result = await listGroupMembersByRoom(roomId, includeLeft);
   sendSuccess(res, { ...result, ownerId: scoped.ownerId });
 }

+ 0 - 0
backend/backend/src/apps/pc/qiwe/controllers/messages.controller.ts → backend/src/apps/pc/qiwe/controllers/messages.controller.ts


+ 0 - 0
backend/backend/src/apps/pc/qiwe/controllers/org.controller.ts → backend/src/apps/pc/qiwe/controllers/org.controller.ts


+ 0 - 0
backend/backend/src/apps/pc/qiwe/controllers/stores.controller.ts → backend/src/apps/pc/qiwe/controllers/stores.controller.ts


+ 0 - 0
backend/backend/src/apps/pc/qiwe/controllers/sync.controller.ts → backend/src/apps/pc/qiwe/controllers/sync.controller.ts


+ 0 - 0
backend/backend/src/apps/pc/qiwe/controllers/webhook.controller.ts → backend/src/apps/pc/qiwe/controllers/webhook.controller.ts


+ 0 - 0
backend/backend/src/apps/pc/qiwe/routes/webhook.routes.ts → backend/src/apps/pc/qiwe/routes/webhook.routes.ts


+ 0 - 0
backend/backend/src/apps/pc/qiwe/services/community.service.ts → backend/src/apps/pc/qiwe/services/community.service.ts


+ 256 - 0
backend/src/apps/pc/qiwe/services/group-members.service.ts

@@ -0,0 +1,256 @@
+import Parse from '../../../../shared/db/parse-client.js';
+import { batchGetRoomDetails, getContactDetailsBatch } from './qiwe-api.service.js';
+
+export interface GroupMemberDto {
+  id: string;
+  userId: string;
+  nickname: string;
+  status: string;
+  joinedAt: string | null;
+  leftAt: string | null;
+}
+
+const ACTIVE_QIWE_GUID = process.env.QIWE_GUID || '';
+
+function needsNickname(userId: string, nickname: string): boolean {
+  return !nickname || nickname === userId;
+}
+
+function toMemberDto(obj: Parse.Object): GroupMemberDto {
+  const userId = obj.get('userId') || '';
+  const nickname = obj.get('nickname') || '';
+  return {
+    id: obj.id!,
+    userId,
+    nickname: needsNickname(userId, nickname) ? userId : nickname,
+    status: obj.get('status') || 'active',
+    joinedAt: (obj.get('joinedAt') as Date)?.toISOString?.() ?? null,
+    leftAt: (obj.get('leftAt') as Date)?.toISOString?.() ?? null,
+  };
+}
+
+function applyNicknameMap(members: GroupMemberDto[], nameMap: Map<string, string>): GroupMemberDto[] {
+  return members.map((m) => {
+    const resolved =
+      (!needsNickname(m.userId, m.nickname) ? m.nickname : '')
+      || nameMap.get(m.userId)
+      || m.userId;
+    return { ...m, nickname: resolved };
+  });
+}
+
+async function loadSenderNameMap(roomId: string): Promise<Map<string, string>> {
+  const map = new Map<string, string>();
+  const query = new Parse.Query('Message');
+  query.equalTo('roomId', roomId);
+  query.limit(500);
+  query.select('senderId', 'senderName');
+  const rows = await query.find({ useMasterKey: true });
+  for (const row of rows) {
+    const userId = String(row.get('senderId') || '');
+    const name = String(row.get('senderName') || '');
+    if (!userId || !name || name === userId) continue;
+    map.set(userId, name);
+  }
+  return map;
+}
+
+async function loadOrgNameMap(userIds: string[]): Promise<Map<string, string>> {
+  const map = new Map<string, string>();
+  if (userIds.length === 0) return map;
+  const orgQuery = new Parse.Query('OrgMember');
+  orgQuery.containedIn('userId', userIds.slice(0, 100));
+  orgQuery.limit(100);
+  const orgRows = await orgQuery.find({ useMasterKey: true });
+  for (const row of orgRows) {
+    const uid = String(row.get('userId') || '');
+    const name = String(row.get('name') || '');
+    if (uid && name && name !== uid) map.set(uid, name);
+  }
+  return map;
+}
+
+/** 后台写回昵称,不阻塞列表响应 */
+function schedulePersistNicknames(roomId: string, members: GroupMemberDto[]): void {
+  const toSave = members.filter((m) => !needsNickname(m.userId, m.nickname));
+  if (toSave.length === 0) return;
+
+  void (async () => {
+    for (const m of toSave) {
+      try {
+        const query = new Parse.Query('GroupMember');
+        query.equalTo('roomId', roomId);
+        query.equalTo('userId', m.userId);
+        const existing = await query.first({ useMasterKey: true });
+        if (!existing) continue;
+        const current = String(existing.get('nickname') || '');
+        if (current && current !== m.userId) continue;
+        existing.set('nickname', m.nickname);
+        await existing.save(null, { useMasterKey: true });
+      } catch {
+        /* 后台补全失败可忽略 */
+      }
+    }
+  })();
+}
+
+/**
+ * 列表读取路径:只用本地数据(DB / 消息 / 组织通讯录),不调企微、不写库。
+ * resolveExternal=true 时才批量调联系人接口(较慢,用于手动刷新)。
+ */
+async function enrichMemberNicknames(
+  members: GroupMemberDto[],
+  roomId: string,
+  resolveExternal = false,
+): Promise<GroupMemberDto[]> {
+  if (members.length === 0) return [];
+
+  const pending = members.filter((m) => needsNickname(m.userId, m.nickname));
+  if (pending.length === 0) return members;
+
+  const nameMap = new Map<string, string>();
+  for (const m of members) {
+    if (!needsNickname(m.userId, m.nickname)) nameMap.set(m.userId, m.nickname);
+  }
+
+  const needIds = pending.map((m) => m.userId).filter(Boolean);
+
+  const [msgNames, orgNames] = await Promise.all([
+    loadSenderNameMap(roomId),
+    loadOrgNameMap(needIds),
+  ]);
+  for (const [uid, name] of msgNames) nameMap.set(uid, name);
+  for (const [uid, name] of orgNames) nameMap.set(uid, name);
+
+  const stillNeed = needIds.filter((id) => needsNickname(id, nameMap.get(id) || id));
+  if (stillNeed.length > 0 && (resolveExternal || ACTIVE_QIWE_GUID)) {
+    for (let i = 0; i < stillNeed.length; i += 50) {
+      const batch = stillNeed.slice(i, i + 50);
+      const contacts = await getContactDetailsBatch(batch);
+      for (const c of contacts) {
+        if (c.name && c.name !== c.userId) nameMap.set(c.userId, c.name);
+      }
+    }
+  }
+
+  const enriched = applyNicknameMap(members, nameMap);
+  schedulePersistNicknames(roomId, enriched);
+  return enriched;
+}
+
+async function upsertMembersBatch(
+  roomId: string,
+  guid: string,
+  members: Array<{ userId: string; nickname: string }>,
+): Promise<void> {
+  for (const mem of members) {
+    const query = new Parse.Query('GroupMember');
+    query.equalTo('roomId', roomId);
+    query.equalTo('userId', mem.userId);
+    query.equalTo('guid', guid);
+    const existing = await query.first({ useMasterKey: true });
+
+    if (existing) {
+      if (mem.nickname && needsNickname(mem.userId, String(existing.get('nickname') || ''))) {
+        existing.set('nickname', mem.nickname);
+      }
+      if (existing.get('status') !== 'active') {
+        existing.set('status', 'active');
+        existing.unset('leftAt');
+      }
+      await existing.save(null, { useMasterKey: true });
+      continue;
+    }
+
+    const obj = new Parse.Object('GroupMember');
+    obj.set('roomId', roomId);
+    obj.set('userId', mem.userId);
+    obj.set('guid', guid);
+    obj.set('status', 'active');
+    if (mem.nickname && mem.nickname !== mem.userId) obj.set('nickname', mem.nickname);
+    await obj.save(null, { useMasterKey: true });
+  }
+}
+
+async function loadMembersFromMessages(roomId: string): Promise<GroupMemberDto[]> {
+  const query = new Parse.Query('Message');
+  query.equalTo('roomId', roomId);
+  query.limit(500);
+  query.select('senderId', 'senderName');
+  const rows = await query.find({ useMasterKey: true });
+
+  const map = new Map<string, string>();
+  for (const row of rows) {
+    const userId = String(row.get('senderId') || '');
+    if (!userId) continue;
+    const name = String(row.get('senderName') || '');
+    if (!map.has(userId)) map.set(userId, userId);
+    if (name && name !== userId) map.set(userId, name);
+  }
+
+  return [...map.entries()].map(([userId, nickname]) => ({
+    id: `${roomId}:${userId}`,
+    userId,
+    nickname,
+    status: 'active',
+    joinedAt: null,
+    leftAt: null,
+  }));
+}
+
+async function loadMembersFromQiWe(roomId: string, guid: string): Promise<GroupMemberDto[]> {
+  const rooms = await batchGetRoomDetails(guid, [roomId]);
+  const room = rooms[0];
+  if (!room?.members?.length) return [];
+
+  return room.members.map((mem) => ({
+    id: `${roomId}:${mem.userId}`,
+    userId: mem.userId,
+    nickname: mem.nickname || mem.userId,
+    status: 'active',
+    joinedAt: null,
+    leftAt: null,
+  }));
+}
+
+export async function listGroupMembersByRoom(
+  roomId: string,
+  includeLeft = false,
+  resolveExternal = false,
+): Promise<{ members: GroupMemberDto[]; total: number; source: 'db' | 'qiwe' | 'messages' }> {
+  const query = new Parse.Query('GroupMember');
+  query.equalTo('roomId', roomId);
+  if (!includeLeft) query.notEqualTo('status', 'left');
+  query.ascending('joinedAt');
+  query.limit(500);
+  const rows = await query.find({ useMasterKey: true });
+
+  let members: GroupMemberDto[] = [];
+  let source: 'db' | 'qiwe' | 'messages' = 'db';
+
+  if (rows.length > 0) {
+    members = rows.map(toMemberDto);
+  } else if (ACTIVE_QIWE_GUID) {
+    try {
+      const fromQiWe = await loadMembersFromQiWe(roomId, ACTIVE_QIWE_GUID);
+      if (fromQiWe.length > 0) {
+        members = fromQiWe;
+        source = 'qiwe';
+        if (ACTIVE_QIWE_GUID) {
+          void upsertMembersBatch(roomId, ACTIVE_QIWE_GUID, fromQiWe).catch(() => {});
+        }
+      }
+    } catch (err: unknown) {
+      const message = err instanceof Error ? err.message : String(err);
+      console.warn(`[GroupMember] 拉取群成员失败 roomId=${roomId}: ${message}`);
+    }
+  }
+
+  if (members.length === 0) {
+    members = await loadMembersFromMessages(roomId);
+    if (members.length > 0) source = 'messages';
+  }
+
+  members = await enrichMemberNicknames(members, roomId, resolveExternal);
+  return { members, total: members.length, source };
+}

+ 316 - 10
backend/backend/src/apps/pc/qiwe/services/groups.service.ts → backend/src/apps/pc/qiwe/services/groups.service.ts

@@ -1,9 +1,10 @@
 import Parse from '../../../../shared/db/parse-client.js';
 import { deriveHealthMetrics } from '../utils/group-metrics.util.js';
 import { type LifecyclePhase } from '../utils/lifecycle.util.js';
-import { getCommunityLifecycleMap } from './community.service.js';
-import { resolveOwnerName } from './organization.service.js';
-import { batchGetRoomDetails } from './qiwe-api.service.js';
+import { getCommunityLifecycleMap, listCommunities } from './community.service.js';
+import { getDefaultStore, resolveOwnerName } from './organization.service.js';
+import { listGroupMembersByRoom, type GroupMemberDto } from './group-members.service.js';
+import { batchGetRoomDetails, getContactDetailsBatch } from './qiwe-api.service.js';
 
 export interface GroupChatDto {
   id: string;
@@ -32,6 +33,8 @@ export interface GroupChatDto {
   messageCountToday: number;
   messageCountTotal: number;
   memberChange24h: number;
+  createdAt: string;
+  ownerRole: string;
   updatedAt: string;
 }
 
@@ -46,8 +49,8 @@ export interface GroupListFilters {
 }
 
 function startOfToday(): Date {
-  const now = new Date();
-  return new Date(now.getFullYear(), now.getMonth(), now.getDate());
+  const shanghaiDate = new Date().toLocaleDateString('en-CA', { timeZone: 'Asia/Shanghai' });
+  return new Date(`${shanghaiDate}T00:00:00+08:00`);
 }
 
 async function countMessagesToday(roomId: string): Promise<number> {
@@ -118,6 +121,69 @@ export async function backfillGroupChatsFromMessages(guid: string): Promise<numb
   return created;
 }
 
+const METRICS_FRESH_MS = 10 * 60 * 1000;
+
+/** 详情页读取:优先用库内缓存指标,避免每次打开都扫 Message 表 */
+async function enrichGroupForRead(
+  obj: Parse.Object,
+  lifecycleMap: Map<string, LifecyclePhase>,
+): Promise<GroupChatDto> {
+  const roomId = obj.get('roomId') as string;
+  const memberCount = obj.get('memberCount') ?? 0;
+  const status = obj.get('status') || 'active';
+  const hasDocument = obj.get('hasDocument') === true;
+  const documentPinned = obj.get('documentPinned') === true;
+  const documentInNotice = obj.get('documentInNotice') === true;
+  const memberChange24h = obj.get('memberChange24h') ?? 0;
+  const updatedAt = obj.get('updatedAt') as Date | undefined;
+  const cachedTotal = obj.get('messageCountTotal') ?? 0;
+  const freshMetrics =
+    cachedTotal > 0
+    && updatedAt
+    && Date.now() - updatedAt.getTime() < METRICS_FRESH_MS;
+
+  let messageCountToday = obj.get('messageCountToday') ?? 0;
+  let messageCountTotal = cachedTotal;
+
+  if (!freshMetrics) {
+    [messageCountToday, messageCountTotal] = await Promise.all([
+      countMessagesToday(roomId),
+      messageCountTotal > 0 ? Promise.resolve(messageCountTotal) : countMessagesTotal(roomId),
+    ]);
+  }
+
+  const metrics = deriveHealthMetrics({
+    memberCount,
+    messageCountToday,
+    memberChange24h,
+    hasDocument,
+    documentPinned,
+    documentInNotice,
+    status,
+  });
+
+  const communityId = obj.get('communityId') as string;
+  const lifecyclePhase = communityId && lifecycleMap.has(communityId)
+    ? lifecycleMap.get(communityId)!
+    : (obj.get('lifecyclePhase') as string) || 'pre_handover';
+
+  obj.set('messageCountToday', messageCountToday);
+  obj.set('messageCountTotal', messageCountTotal);
+  obj.set('activityLevel', metrics.activityLevel);
+  obj.set('opsActivityScore', metrics.opsActivityScore);
+  obj.set('customerActivityScore', metrics.customerActivityScore);
+  obj.set('healthGrade', metrics.healthGrade);
+  obj.set('healthScore', metrics.healthScore);
+  obj.set('healthStatus', metrics.healthStatus);
+  obj.set('lifecyclePhase', lifecyclePhase);
+
+  if (!freshMetrics) {
+    void obj.save(null, { useMasterKey: true }).catch(() => {});
+  }
+
+  return toDto(obj);
+}
+
 async function enrichGroup(
   obj: Parse.Object,
   lifecycleMap: Map<string, LifecyclePhase>,
@@ -213,10 +279,180 @@ function toDto(obj: Parse.Object): GroupChatDto {
     messageCountToday: obj.get('messageCountToday') ?? 0,
     messageCountTotal: obj.get('messageCountTotal') ?? 0,
     memberChange24h: obj.get('memberChange24h') ?? 0,
+    createdAt: (obj.get('createdAt') as Date)?.toISOString?.() || new Date().toISOString(),
+    ownerRole: (obj.get('ownerRole') as string) || '',
     updatedAt: (obj.get('updatedAt') as Date)?.toISOString?.() || new Date().toISOString(),
   };
 }
 
+async function countActiveMembers(roomId: string): Promise<number> {
+  const query = new Parse.Query('GroupMember');
+  query.equalTo('roomId', roomId);
+  query.notEqualTo('status', 'left');
+  return query.count({ useMasterKey: true });
+}
+
+async function resolveOwnerProfile(ownerId: string): Promise<{ name: string; role: string }> {
+  if (!ownerId) return { name: '', role: '' };
+
+  const orgQuery = new Parse.Query('OrgMember');
+  orgQuery.equalTo('userId', ownerId);
+  const orgMember = await orgQuery.first({ useMasterKey: true });
+  if (orgMember) {
+    return {
+      name: String(orgMember.get('name') || ownerId),
+      role: String(orgMember.get('position') || '群主'),
+    };
+  }
+
+  if (ACTIVE_QIWE_GUID) {
+    try {
+      const contacts = await getContactDetailsBatch([ownerId]);
+      const contact = contacts[0];
+      if (contact) {
+        return {
+          name: contact.name || ownerId,
+          role: contact.position || '群主',
+        };
+      }
+    } catch {
+      /* 联系人接口失败时回退 userId */
+    }
+  }
+
+  return { name: ownerId, role: '群主' };
+}
+
+async function ensureOrganizationFields(obj: Parse.Object): Promise<void> {
+  if (!obj.get('storeId')) {
+    const def = await getDefaultStore();
+    if (def.storeId) {
+      obj.set('storeId', def.storeId);
+      obj.set('storeName', def.storeName);
+    }
+  }
+
+  if (!obj.get('communityId')) {
+    const roomName = String(obj.get('roomName') || '');
+    const communities = await listCommunities();
+    const match = communities
+      .filter((c) => c.name && roomName.includes(c.name))
+      .sort((a, b) => b.name.length - a.name.length)[0];
+    if (match) {
+      obj.set('communityId', match.id);
+      obj.set('communityName', match.name);
+      if (!obj.get('storeId') && match.storeId) {
+        obj.set('storeId', match.storeId);
+        obj.set('storeName', match.storeName);
+      }
+    }
+  }
+}
+
+async function borrowOwnerFromSiblingRows(obj: Parse.Object, roomId: string): Promise<void> {
+  if (obj.get('ownerId')) return;
+
+  const query = new Parse.Query('GroupChat');
+  query.equalTo('roomId', roomId);
+  query.exists('ownerId');
+  query.limit(20);
+  const rows = await query.find({ useMasterKey: true });
+  for (const row of rows) {
+    const ownerId = String(row.get('ownerId') || '');
+    if (!ownerId) continue;
+    obj.set('ownerId', ownerId);
+    if (row.get('ownerName')) obj.set('ownerName', row.get('ownerName'));
+    return;
+  }
+}
+
+async function inferOwnerFromMembers(
+  roomId: string,
+  members: GroupMemberDto[],
+): Promise<{ ownerId: string; name: string; role: string } | null> {
+  if (members.length === 0) return null;
+
+  const userIds = members.map((m) => m.userId).filter(Boolean);
+  const orgQuery = new Parse.Query('OrgMember');
+  orgQuery.containedIn('userId', userIds.slice(0, 100));
+  orgQuery.limit(100);
+  const orgRows = await orgQuery.find({ useMasterKey: true });
+  const orgMap = new Map(orgRows.map((row) => [String(row.get('userId')), row]));
+
+  for (const member of members) {
+    const org = orgMap.get(member.userId);
+    if (org) {
+      return {
+        ownerId: member.userId,
+        name: String(org.get('name') || member.nickname || member.userId),
+        role: String(org.get('position') || '负责人'),
+      };
+    }
+  }
+
+  const msgQuery = new Parse.Query('Message');
+  msgQuery.equalTo('roomId', roomId);
+  msgQuery.limit(200);
+  msgQuery.select('senderId', 'senderName');
+  const messages = await msgQuery.find({ useMasterKey: true });
+  const counts = new Map<string, { count: number; name: string }>();
+  for (const message of messages) {
+    const userId = String(message.get('senderId') || '');
+    if (!userId) continue;
+    const senderName = String(message.get('senderName') || userId);
+    const current = counts.get(userId) || { count: 0, name: senderName };
+    current.count += 1;
+    if (senderName !== userId) current.name = senderName;
+    counts.set(userId, current);
+  }
+
+  let best: { ownerId: string; name: string; count: number } | null = null;
+  for (const [ownerId, value] of counts) {
+    if (!best || value.count > best.count) {
+      best = { ownerId, name: value.name, count: value.count };
+    }
+  }
+  if (best) {
+    return { ownerId: best.ownerId, name: best.name, role: '活跃成员' };
+  }
+
+  const first = members.find((m) => m.nickname && m.nickname !== m.userId);
+  if (first) {
+    return { ownerId: first.userId, name: first.nickname, role: '群成员' };
+  }
+
+  return null;
+}
+
+async function syncQiWeRoomFields(obj: Parse.Object, roomId: string): Promise<void> {
+  const guid = String(obj.get('guid') || ACTIVE_QIWE_GUID);
+  if (!guid) return;
+
+  const needsSync =
+    !obj.get('ownerId')
+    || (obj.get('memberCount') ?? 0) === 0
+    || isPlaceholderRoomName(String(obj.get('roomName') || ''));
+  if (!needsSync) return;
+
+  try {
+    const details = await batchGetRoomDetails(guid, [roomId]);
+    const detail = details[0];
+    if (!detail) return;
+    if (detail.roomName) obj.set('roomName', detail.roomName);
+    if (detail.ownerId) obj.set('ownerId', detail.ownerId);
+    if (detail.memberCount > 0) obj.set('memberCount', detail.memberCount);
+  } catch (err: unknown) {
+    const message = err instanceof Error ? err.message : String(err);
+    console.warn(`[GroupChat] 详情补全企微字段失败 roomId=${roomId}: ${message}`);
+  }
+}
+
+function pickBestGroupObject(rows: Parse.Object[], lifecycleMap: Map<string, LifecyclePhase>): Parse.Object {
+  const dtos = rows.map((obj) => toListDto(obj, lifecycleMap));
+  const [bestDto] = dedupeGroupsByRoomId(dtos);
+  return rows.find((row) => row.id === bestDto?.id) || rows[0];
+}
+
 const ACTIVE_QIWE_GUID = process.env.QIWE_GUID || '';
 
 function scoreGroupForDedupe(g: GroupChatDto): number {
@@ -359,13 +595,83 @@ export async function getGroupChatByRoomId(roomId: string): Promise<GroupChatDto
   const query = new Parse.Query('GroupChat');
   query.equalTo('roomId', roomId);
   query.limit(20);
-  const rows = await query.find({ useMasterKey: true });
+  const [rows, lifecycleMap] = await Promise.all([
+    query.find({ useMasterKey: true }),
+    getCommunityLifecycleMap(),
+  ]);
   if (rows.length === 0) return null;
 
-  const lifecycleMap = await getCommunityLifecycleMap();
-  const dtos = rows.map((obj) => toListDto(obj, lifecycleMap));
-  const [best] = dedupeGroupsByRoomId(dtos);
-  return best ?? null;
+  const obj = pickBestGroupObject(rows, lifecycleMap);
+  const needsQiWe =
+    !obj.get('ownerId')
+    || (obj.get('memberCount') ?? 0) === 0
+    || isPlaceholderRoomName(String(obj.get('roomName') || ''));
+
+  let membersLoaded: GroupMemberDto[] | null = null;
+
+  await Promise.all([
+    needsQiWe ? syncQiWeRoomFields(obj, roomId) : Promise.resolve(),
+    !obj.get('ownerId') ? borrowOwnerFromSiblingRows(obj, roomId) : Promise.resolve(),
+    !obj.get('storeId') ? ensureOrganizationFields(obj) : Promise.resolve(),
+  ]);
+
+  const needsMembers = (obj.get('memberCount') ?? 0) === 0 || !obj.get('ownerId');
+  if (needsMembers) {
+    if ((obj.get('memberCount') ?? 0) === 0) {
+      const memberCount = await countActiveMembers(roomId);
+      if (memberCount > 0) obj.set('memberCount', memberCount);
+    }
+
+    if ((obj.get('memberCount') ?? 0) === 0 || !obj.get('ownerId')) {
+      const membersResult = await listGroupMembersByRoom(roomId, false, false);
+      membersLoaded = membersResult.members;
+      if (membersResult.total > 0 && (obj.get('memberCount') ?? 0) === 0) {
+        obj.set('memberCount', membersResult.total);
+      }
+    }
+  }
+
+  if (!obj.get('ownerId')) {
+    if (!membersLoaded) {
+      const membersResult = await listGroupMembersByRoom(roomId, false, false);
+      membersLoaded = membersResult.members;
+      if (membersResult.total > 0 && (obj.get('memberCount') ?? 0) === 0) {
+        obj.set('memberCount', membersResult.total);
+      }
+    }
+    const inferred = membersLoaded ? await inferOwnerFromMembers(roomId, membersLoaded) : null;
+    if (inferred) {
+      obj.set('ownerId', inferred.ownerId);
+      obj.set('ownerName', inferred.name);
+      obj.set('ownerRole', inferred.role);
+    }
+  }
+
+  const ownerId = String(obj.get('ownerId') || '');
+  let owner = obj.get('ownerName')
+    ? { name: String(obj.get('ownerName')), role: String(obj.get('ownerRole') || '群主') }
+    : await resolveOwnerProfile(ownerId);
+
+  if (ownerId && (!owner.name || owner.name === ownerId)) {
+    if (!membersLoaded) {
+      const membersResult = await listGroupMembersByRoom(roomId, false, false);
+      membersLoaded = membersResult.members;
+    }
+    const member = membersLoaded?.find((m) => m.userId === ownerId);
+    if (member?.nickname && member.nickname !== ownerId) {
+      owner = { ...owner, name: member.nickname };
+    }
+  }
+
+  if (owner.name) obj.set('ownerName', owner.name);
+  if (owner.role) obj.set('ownerRole', owner.role);
+
+  const dto = await enrichGroupForRead(obj, lifecycleMap);
+  return {
+    ...dto,
+    createdAt: (obj.get('createdAt') as Date)?.toISOString?.() || dto.updatedAt,
+    ownerRole: owner.role || dto.ownerRole || '群主',
+  };
 }
 
 export async function migrateGroupBusinessFields(): Promise<void> {

+ 0 - 0
backend/backend/src/apps/pc/qiwe/services/message-persist.service.ts → backend/src/apps/pc/qiwe/services/message-persist.service.ts


+ 0 - 0
backend/backend/src/apps/pc/qiwe/services/message-sync.service.ts → backend/src/apps/pc/qiwe/services/message-sync.service.ts


+ 0 - 0
backend/backend/src/apps/pc/qiwe/services/org-sync.service.ts → backend/src/apps/pc/qiwe/services/org-sync.service.ts


+ 0 - 0
backend/backend/src/apps/pc/qiwe/services/organization.service.ts → backend/src/apps/pc/qiwe/services/organization.service.ts


+ 0 - 0
backend/backend/src/apps/pc/qiwe/services/qiwe-api.service.ts → backend/src/apps/pc/qiwe/services/qiwe-api.service.ts


+ 0 - 0
backend/backend/src/apps/pc/qiwe/services/sync.service.ts → backend/src/apps/pc/qiwe/services/sync.service.ts


+ 0 - 0
backend/backend/src/apps/pc/qiwe/services/webhook.service.ts → backend/src/apps/pc/qiwe/services/webhook.service.ts


+ 0 - 0
backend/backend/src/apps/pc/qiwe/utils/group-metrics.util.ts → backend/src/apps/pc/qiwe/utils/group-metrics.util.ts


+ 0 - 0
backend/backend/src/apps/pc/qiwe/utils/lifecycle.util.ts → backend/src/apps/pc/qiwe/utils/lifecycle.util.ts


+ 4 - 1
backend/backend/src/index.ts → backend/src/index.ts

@@ -22,5 +22,8 @@ async function bootstrap(): Promise<void> {
   }
 }
 
-await bootstrap();
 await import('./apps/pc/health/server.js');
+void bootstrap().catch((err: unknown) => {
+  const message = err instanceof Error ? err.message : String(err);
+  console.error('[Setup] 后台初始化失败(HTTP 服务已启动):', message);
+});

+ 0 - 0
backend/backend/src/shared/auth/dashboard-scope.util.ts → backend/src/shared/auth/dashboard-scope.util.ts


+ 0 - 0
backend/backend/src/shared/auth/data-scope.service.ts → backend/src/shared/auth/data-scope.service.ts


+ 0 - 0
backend/backend/src/shared/auth/require-user.ts → backend/src/shared/auth/require-user.ts


+ 0 - 0
backend/backend/src/shared/config/env.ts → backend/src/shared/config/env.ts


+ 0 - 0
backend/backend/src/shared/db/migration.util.ts → backend/src/shared/db/migration.util.ts


+ 0 - 0
backend/backend/src/shared/db/organization-bootstrap.ts → backend/src/shared/db/organization-bootstrap.ts


+ 0 - 0
backend/backend/src/shared/db/parse-client.ts → backend/src/shared/db/parse-client.ts


+ 13 - 6
backend/backend/src/shared/db/parse-health.service.ts → backend/src/shared/db/parse-health.service.ts

@@ -1,5 +1,6 @@
 import Parse from './parse-client.js';
-import { getStringEnv } from '../config/env.js';
+import { getNumberEnv, getStringEnv } from '../config/env.js';
+import { withTimeout } from '../utils/with-timeout.js';
 
 export interface ParseDatabaseStatus {
   connected: boolean;
@@ -17,12 +18,18 @@ export async function checkParseDatabase(): Promise<ParseDatabaseStatus> {
   const serverURL = Parse.serverURL || getStringEnv('PARSE_SERVER_URL') || '';
   const appId = getStringEnv('PARSE_APP_ID') || 'lami-ai';
 
+  const timeoutMs = getNumberEnv('PARSE_TIMEOUT_MS', 8000);
+
   try {
-    const [groupChat, groupMember, message] = await Promise.all([
-      new Parse.Query('GroupChat').count({ useMasterKey: true }),
-      new Parse.Query('GroupMember').count({ useMasterKey: true }),
-      new Parse.Query('Message').count({ useMasterKey: true }),
-    ]);
+    const [groupChat, groupMember, message] = await withTimeout(
+      Promise.all([
+        new Parse.Query('GroupChat').count({ useMasterKey: true }),
+        new Parse.Query('GroupMember').count({ useMasterKey: true }),
+        new Parse.Query('Message').count({ useMasterKey: true }),
+      ]),
+      timeoutMs,
+      'Parse database check',
+    );
 
     return {
       connected: true,

+ 12 - 11
backend/backend/src/shared/db/schema-setup.ts → backend/src/shared/db/schema-setup.ts

@@ -1,4 +1,5 @@
 import Parse from './parse-client.js';
+import { checkParseDatabase } from './parse-health.service.js';
 
 function isSchemaExistsError(msg: string): boolean {
   return (
@@ -43,11 +44,18 @@ async function ensureClassFields(
 }
 
 export async function ensureSchemas(): Promise<void> {
+  const parseStatus = await checkParseDatabase();
+  if (!parseStatus.connected) {
+    console.warn('[Schema] Parse 不可用,跳过 Schema 检查:', parseStatus.error);
+    return;
+  }
+
   await ensureClassFields('GroupChat', {
     roomId: 'String',
     roomName: 'String',
     ownerId: 'String',
     ownerName: 'String',
+    ownerRole: 'String',
     memberCount: 'Number',
     avatarUrl: 'String',
     status: 'String',
@@ -116,12 +124,11 @@ export async function ensureSchemas(): Promise<void> {
     seq: 'Number',
   });
 
-  await ensureClassFields('AppUser', {
-    email: 'String',
-    passwordHash: 'String',
-    passwordSalt: 'String',
-    name: 'String',
+  await ensureClassFields('_User', {
     phone: 'String',
+    appSource: 'String',
+    name: 'String',
+    email: 'String',
     role: 'String',
     storeId: 'String',
     storeName: 'String',
@@ -150,12 +157,6 @@ export async function ensureSchemas(): Promise<void> {
     syncedAt: 'Date',
   });
 
-  await ensureClassFields('AuthSession', {
-    token: 'String',
-    userId: 'String',
-    expiresAt: 'Date',
-  });
-
   await ensureClassFields('SystemMigration', {
     key: 'String',
     completedAt: 'Date',

+ 0 - 0
backend/backend/src/shared/errors/app-error.ts → backend/src/shared/errors/app-error.ts


+ 0 - 0
backend/backend/src/shared/http/async-handler.ts → backend/src/shared/http/async-handler.ts


+ 0 - 0
backend/backend/src/shared/http/error-handler.ts → backend/src/shared/http/error-handler.ts


+ 0 - 0
backend/backend/src/shared/http/not-found.middleware.ts → backend/src/shared/http/not-found.middleware.ts


+ 0 - 0
backend/backend/src/shared/http/response.ts → backend/src/shared/http/response.ts


+ 22 - 0
backend/src/shared/utils/with-timeout.ts

@@ -0,0 +1,22 @@
+export function withTimeout<T>(
+  promise: Promise<T>,
+  ms: number,
+  label = 'operation',
+): Promise<T> {
+  return new Promise<T>((resolve, reject) => {
+    const timer = setTimeout(
+      () => reject(new Error(`${label} timed out after ${ms}ms`)),
+      ms,
+    );
+    promise.then(
+      (value) => {
+        clearTimeout(timer);
+        resolve(value);
+      },
+      (error: unknown) => {
+        clearTimeout(timer);
+        reject(error);
+      },
+    );
+  });
+}

+ 0 - 0
backend/backend/test-webhook.bat → backend/test-webhook.bat


+ 0 - 0
backend/backend/tests_python/list_chat_rooms.mjs → backend/tests_python/list_chat_rooms.mjs


+ 0 - 0
backend/backend/tests_python/run_auto_test.mjs → backend/tests_python/run_auto_test.mjs


+ 0 - 0
backend/backend/tests_python/run_p0_scope_test.ps1 → backend/tests_python/run_p0_scope_test.ps1


+ 0 - 0
backend/backend/tsconfig.json → backend/tsconfig.json


+ 0 - 7
lami-base-v1/.claude/settings.local.json

@@ -1,7 +0,0 @@
-{
-  "permissions": {
-    "allow": [
-      "Bash(npx tsc *)"
-    ]
-  }
-}

+ 0 - 6
lami-base-v1/.gitignore

@@ -1,6 +0,0 @@
-node_modules/
-*.local
-*.log
-package-lock.json
-.DS_Store
-Thumbs.db

+ 0 - 48
lami-base-v1/.trae/documents/learning_page_plan.md

@@ -1,48 +0,0 @@
-# 学习页面优化计划(简化版)
-
-## 问题分析
-
-1. 课程卡片点击无效果 — 缺少课程详情弹窗逻辑
-2. `learning.ts` import 了已删除的组件,编译报错
-3. 缺少"开始学习"→ 视频页 → 答题页的完整流程
-
-## 企业级组件拆分原则
-
-| 组件 | 策略 | 原因 |
-|------|------|------|
-| learning 页面内容 | ✅ 一个组件搞定 | 单页面内容,无需拆分 |
-| 课程详情 Sheet | ✅ 内嵌到 learning.html | 仅此一处使用,无需独立组件 |
-| video-player | ✅ 已有独立路由页面 | 全屏页面,独立路由 |
-| quiz | ✅ 已有独立路由页面 | 全屏页面,独立路由 |
-| quiz-result | ✅ 已有独立路由页面 | 全屏页面,独立路由 |
-
-> **核心原则:只有多处复用才抽离为共享组件,单一用途直接内嵌。**
-
-## 实现方案
-
-### 唯一需要改的文件
-
-| 文件 | 操作 |
-|------|------|
-| `features/learning/learning/learning.html` | 重写:学习路线 + 课程网格 + 全部模块 + 课程详情 Sheet(内嵌) |
-| `features/learning/learning/learning.ts` | 重写:所有逻辑(showCourseDetail、openCourseDetail、closeCourseDetail、startLearning 等) |
-
-### 功能流程
-
-```
-点击课程卡片 → showCourseDetail = true → 显示课程详情 Sheet
-  ↓ 点击"开始学习"
-closeCourseDetail() → router.navigate(['/video-player'])
-  ↓
-video-player 页面(已有的独立路由页面)
-  ↓ 点击"进入答题"
-router.navigate(['/quiz'])
-```
-
-### 关键实现
-
-- 课程详情 Sheet 内嵌在 learning.html 底部,用 `[class.hidden]="!showCourseDetail()"` 控制
-- 使用 signals 管理状态(showCourseDetail、selectedCourseId)
-- 课程数据在 ts 中维护
-- 视频页和答题页使用 Router 导航(已有路由配置)
-- 严格按照 demo 的 HTML 结构和 CSS 类名

+ 0 - 31
lami-base-v1/.trae/documents/profile_optimize_plan.md

@@ -1,31 +0,0 @@
-# Profile 页面优化计划
-
-## 问题分析
-当前 profile 页面与 demo 对比,存在的问题:
-1. 使用了多个子组件(`user-info-card`、`level-progress`、`settings-list`)导致可能的布局差异
-2. 深色模式切换按钮的实现和 demo 不一致
-3. 整体结构可能存在嵌套问题
-
-## 优化方案
-### 1. 重构 Profile 组件为单个文件
-- 将所有子组件的 HTML 合并到 `profile.html`
-- 更新 `profile.ts` 包含所有必要的逻辑和图标导入
-- 移除对中间组件的依赖
-
-### 2. 严格匹配 Demo 结构
-- 使用与 demo 完全相同的 HTML 结构
-- 保持一致的 CSS 类名
-- 保持一致的深色模式切换实现
-
-### 3. 清理
-- 移除不再需要的 `profile/components` 文件夹
-
-## 需要修改的文件
-1. `src/app/features/profile/profile/profile.html` - 完全重写
-2. `src/app/features/profile/profile/profile.ts` - 更新逻辑和导入
-3. 删除 `src/app/features/profile/components/` - 整个文件夹
-
-## 最终效果
-- Profile 页面完全符合 demo 的视觉效果
-- 布局结构、按钮样式、图标位置完全一致
-- 深色模式切换功能正常

+ 0 - 86
lami-base-v1/.trae/documents/qa_ai_logic_plan.md

@@ -1,86 +0,0 @@
-# QA 页面接入 AI 逻辑 + Markdown 渲染 + 图片处理计划
-
-## 现状分析
-
-| 模块 | 状态 | 说明 |
-|------|------|------|
-| 后端 `/api/chat` | ✅ 已实现 | 完整的 DashScope SSE 代理 |
-| 前端 `qa.ts` sendMessage | ❌ Mock | setTimeout 模拟 |
-| Markdown 渲染 | ❌ 无 | 当前纯文本,没有 Markdown |
-| 图片处理 | ❌ 无 | demo 中有 MCP 图片提取逻辑 |
-| `marked` 库 | ✅ 已安装 | 但未在 QA 页面使用 |
-
-## 实现方案
-
-### 第一步:Markdown 渲染
-使用 `ngx-markdown` 库(npm install ngx-markdown),通过 `<markdown>` 组件或 `| markdown` pipe 渲染。
-- `provideMarkdown()` 在 app.config.ts 全局配置
-- QA 页面的 AI 消息用 `[data]` 绑定渲染 Markdown
-- 高亮代码使用 `highlight.js`(已安装)
-
-### 第二步:图片处理
-从 demo 迁移 MCP 图片提取逻辑:
-- 解析 `output.thoughts` 中的 `action_type === 'mcp'` 节点
-- 从 `observation.content[].text` 中提取 image URLs(`results[]` 数组)
-- **不直接渲染图片**,而是在 Markdown 渲染后将 `![alt](url)` 替换为 `<img>` 标签,统一添加样式类、懒加载、点击放大等
-
-### 第三步:AI 流式逻辑
-从 demo 迁移到 Angular:
-- `ai-chat.service.ts` — SSE 流式请求 + 图片提取
-- `qa.ts` — 替换 sendMessage,接入真实流式逻辑
-- `QAMessage` 模型扩展 — 添加 `thoughts`、`images` 字段
-
-### 第四步:后端 .env
-基于 `.env.example` 创建 `.env` 文件
-
----
-
-## 需要修改的文件
-
-| 文件 | 操作 | 说明 |
-|------|------|------|
-| `frontend/package.json` | 添加依赖 | `ngx-markdown` |
-| `frontend/src/app/app.config.ts` | 更新 | 添加 `provideMarkdown()` |
-| `frontend/src/app/core/services/ai-chat.service.ts` | **新建** | SSE 流 + 图片提取 |
-| `frontend/src/app/core/models/qa.model.ts` | 更新 | 扩展 QAMessage 接口 |
-| `frontend/src/app/features/qa/qa/qa.ts` | 重写 | 接入 AI 流式 + Markdown |
-| `frontend/src/app/features/qa/qa/qa.html` | 更新 | 使用 `<markdown>` 组件 |
-| `backend/.env` | 新建 | 基于 .env.example |
-
-## 图片处理策略
-
-demo 中的图片提取逻辑:
-```javascript
-// thought.action_type === 'mcp'
-// → observation.content[].type === 'text'
-// → JSON.parse(item.text).results[] → 图片 URL 数组
-```
-
-Angular 中:
-1. 在 `ai-chat.service.ts` 中提取 imageUrls
-2. API 返回 `{ text, thoughts, images }` 给组件
-3. 组件将 images 拼接到 Markdown 内容末尾(用 `![图片](url)` 格式)
-4. `ngx-markdown` 自动渲染为 `<img>`,配合 CSS 控制样式
-
-## 数据流
-
-```
-用户输入 → qa.ts sendMessage()
-  → aiChatService.stream(prompt, sessionId)
-    → fetch('/api/chat')
-      → SSE 逐帧解析
-        → output.text → 追加到 Markdown 内容
-        → output.thoughts[].thought → 思考过程
-        → output.thoughts[].mcp → 提取图片 URLs
-        → output.session_id → 保存会话
-          → qa.ts 实时更新 messages signal
-            → <markdown> 组件渲染
-            → 图片自动生成 <img> 标签
-```
-
-## 关键实现
-
-1. **ngx-markdown 配置**:需要 `provideHttpClient()`(已提供)+ `provideMarkdown()`
-2. **安全渲染**:使用 `[data]` 绑定而非 `[src]` 加载外部文件
-3. **图片样式**:CSS 统一控制 `.qa-markdown img { max-width: 100%; border-radius: 8px; }`
-4. **流式更新**:发送时插入空 AI 消息,收到 chunk 后更新 content 字段,ngx-markdown 自动重新渲染

+ 0 - 16
lami-base-v1/backend/.env.example

@@ -1,16 +0,0 @@
-NODE_ENV=development
-
-MOBILE_CHAT_PORT=3201
-PC_PORT=3101
-QIWEI_PORT=3202
-
-DASHSCOPE_API_KEY=your_api_key_here
-DASHSCOPE_APP_ID=your_app_id_here
-
-# QiWe 开放平台(会话消息 / 同步历史)
-QIWEI_TOKEN=
-QIWEI_GUID=
-QIWEI_BASE_URL=http://manager.qiweapi.com/qiwe
-QIWEI_SYNC_METHOD=/msg/syncMsg
-# 群-文档台账 JSON 存储路径(可选)
-# QIWEI_ROOM_DOC_STORE=./data/qiwei-room-docs.json

+ 0 - 12
lami-base-v1/backend/.gitignore

@@ -1,12 +0,0 @@
-node_modules/
-dist/
-.env
-tests_python/report*.html
-tests_python/live_data_snapshot.json
-tests_python/live_data_report.md
-tests_python/.pytest_cache/
-*.local
-*.log
-package-lock.json
-.DS_Store
-Thumbs.db

+ 0 - 27
lami-base-v1/backend/package.json

@@ -1,27 +0,0 @@
-{
-  "name": "backend",
-  "version": "1.0.0",
-  "description": "Express 5 Backend API",
-  "type": "module",
-  "scripts": {
-    "dev": "tsx watch src/index.ts",
-    "build": "tsc",
-    "start": "node dist/index.js",
-    "test": "tsx --test tests/**/*.test.ts",
-    "test:qiwei": "tsx --test tests/qiwei.api.test.ts"
-  },
-  "dependencies": {
-    "express": "^5.0.0",
-    "cors": "^2.8.5",
-    "dotenv": "^16.4.5"
-  },
-  "devDependencies": {
-    "@types/cors": "^2.8.17",
-    "@types/express": "^5.0.0",
-    "@types/node": "^22.10.0",
-    "@types/supertest": "^6.0.2",
-    "supertest": "^7.0.0",
-    "tsx": "^4.19.0",
-    "typescript": "^5.6.0"
-  }
-}

+ 0 - 1030
lami-base-v1/backend/pnpm-lock.yaml

@@ -1,1030 +0,0 @@
-lockfileVersion: '9.0'
-
-settings:
-  autoInstallPeers: true
-  excludeLinksFromLockfile: false
-
-importers:
-
-  .:
-    dependencies:
-      cors:
-        specifier: ^2.8.5
-        version: 2.8.6
-      dotenv:
-        specifier: ^16.4.5
-        version: 16.6.1
-      express:
-        specifier: ^5.0.0
-        version: 5.2.1
-      swagger-ui-express:
-        specifier: ^5.0.1
-        version: 5.0.1(express@5.2.1)
-    devDependencies:
-      '@types/cors':
-        specifier: ^2.8.17
-        version: 2.8.19
-      '@types/express':
-        specifier: ^5.0.0
-        version: 5.0.6
-      '@types/swagger-ui-express':
-        specifier: ^4.1.8
-        version: 4.1.8
-      tsx:
-        specifier: ^4.19.0
-        version: 4.21.0
-      typescript:
-        specifier: ^5.6.0
-        version: 5.9.3
-
-packages:
-
-  '@esbuild/aix-ppc64@0.27.7':
-    resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==}
-    engines: {node: '>=18'}
-    cpu: [ppc64]
-    os: [aix]
-
-  '@esbuild/android-arm64@0.27.7':
-    resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==}
-    engines: {node: '>=18'}
-    cpu: [arm64]
-    os: [android]
-
-  '@esbuild/android-arm@0.27.7':
-    resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==}
-    engines: {node: '>=18'}
-    cpu: [arm]
-    os: [android]
-
-  '@esbuild/android-x64@0.27.7':
-    resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==}
-    engines: {node: '>=18'}
-    cpu: [x64]
-    os: [android]
-
-  '@esbuild/darwin-arm64@0.27.7':
-    resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==}
-    engines: {node: '>=18'}
-    cpu: [arm64]
-    os: [darwin]
-
-  '@esbuild/darwin-x64@0.27.7':
-    resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==}
-    engines: {node: '>=18'}
-    cpu: [x64]
-    os: [darwin]
-
-  '@esbuild/freebsd-arm64@0.27.7':
-    resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==}
-    engines: {node: '>=18'}
-    cpu: [arm64]
-    os: [freebsd]
-
-  '@esbuild/freebsd-x64@0.27.7':
-    resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==}
-    engines: {node: '>=18'}
-    cpu: [x64]
-    os: [freebsd]
-
-  '@esbuild/linux-arm64@0.27.7':
-    resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==}
-    engines: {node: '>=18'}
-    cpu: [arm64]
-    os: [linux]
-
-  '@esbuild/linux-arm@0.27.7':
-    resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==}
-    engines: {node: '>=18'}
-    cpu: [arm]
-    os: [linux]
-
-  '@esbuild/linux-ia32@0.27.7':
-    resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==}
-    engines: {node: '>=18'}
-    cpu: [ia32]
-    os: [linux]
-
-  '@esbuild/linux-loong64@0.27.7':
-    resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==}
-    engines: {node: '>=18'}
-    cpu: [loong64]
-    os: [linux]
-
-  '@esbuild/linux-mips64el@0.27.7':
-    resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==}
-    engines: {node: '>=18'}
-    cpu: [mips64el]
-    os: [linux]
-
-  '@esbuild/linux-ppc64@0.27.7':
-    resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==}
-    engines: {node: '>=18'}
-    cpu: [ppc64]
-    os: [linux]
-
-  '@esbuild/linux-riscv64@0.27.7':
-    resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==}
-    engines: {node: '>=18'}
-    cpu: [riscv64]
-    os: [linux]
-
-  '@esbuild/linux-s390x@0.27.7':
-    resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==}
-    engines: {node: '>=18'}
-    cpu: [s390x]
-    os: [linux]
-
-  '@esbuild/linux-x64@0.27.7':
-    resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==}
-    engines: {node: '>=18'}
-    cpu: [x64]
-    os: [linux]
-
-  '@esbuild/netbsd-arm64@0.27.7':
-    resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==}
-    engines: {node: '>=18'}
-    cpu: [arm64]
-    os: [netbsd]
-
-  '@esbuild/netbsd-x64@0.27.7':
-    resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==}
-    engines: {node: '>=18'}
-    cpu: [x64]
-    os: [netbsd]
-
-  '@esbuild/openbsd-arm64@0.27.7':
-    resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==}
-    engines: {node: '>=18'}
-    cpu: [arm64]
-    os: [openbsd]
-
-  '@esbuild/openbsd-x64@0.27.7':
-    resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==}
-    engines: {node: '>=18'}
-    cpu: [x64]
-    os: [openbsd]
-
-  '@esbuild/openharmony-arm64@0.27.7':
-    resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==}
-    engines: {node: '>=18'}
-    cpu: [arm64]
-    os: [openharmony]
-
-  '@esbuild/sunos-x64@0.27.7':
-    resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==}
-    engines: {node: '>=18'}
-    cpu: [x64]
-    os: [sunos]
-
-  '@esbuild/win32-arm64@0.27.7':
-    resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==}
-    engines: {node: '>=18'}
-    cpu: [arm64]
-    os: [win32]
-
-  '@esbuild/win32-ia32@0.27.7':
-    resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==}
-    engines: {node: '>=18'}
-    cpu: [ia32]
-    os: [win32]
-
-  '@esbuild/win32-x64@0.27.7':
-    resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==}
-    engines: {node: '>=18'}
-    cpu: [x64]
-    os: [win32]
-
-  '@scarf/scarf@1.4.0':
-    resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==}
-
-  '@types/body-parser@1.19.6':
-    resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
-
-  '@types/connect@3.4.38':
-    resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
-
-  '@types/cors@2.8.19':
-    resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==}
-
-  '@types/express-serve-static-core@5.1.1':
-    resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==}
-
-  '@types/express@5.0.6':
-    resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==}
-
-  '@types/http-errors@2.0.5':
-    resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==}
-
-  '@types/node@25.6.0':
-    resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==}
-
-  '@types/qs@6.15.1':
-    resolution: {integrity: sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==}
-
-  '@types/range-parser@1.2.7':
-    resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==}
-
-  '@types/send@1.2.1':
-    resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==}
-
-  '@types/serve-static@2.2.0':
-    resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==}
-
-  '@types/swagger-ui-express@4.1.8':
-    resolution: {integrity: sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g==}
-
-  accepts@2.0.0:
-    resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
-    engines: {node: '>= 0.6'}
-
-  body-parser@2.2.2:
-    resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==}
-    engines: {node: '>=18'}
-
-  bytes@3.1.2:
-    resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
-    engines: {node: '>= 0.8'}
-
-  call-bind-apply-helpers@1.0.2:
-    resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
-    engines: {node: '>= 0.4'}
-
-  call-bound@1.0.4:
-    resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
-    engines: {node: '>= 0.4'}
-
-  content-disposition@1.1.0:
-    resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==}
-    engines: {node: '>=18'}
-
-  content-type@1.0.5:
-    resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
-    engines: {node: '>= 0.6'}
-
-  cookie-signature@1.2.2:
-    resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==}
-    engines: {node: '>=6.6.0'}
-
-  cookie@0.7.2:
-    resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
-    engines: {node: '>= 0.6'}
-
-  cors@2.8.6:
-    resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==}
-    engines: {node: '>= 0.10'}
-
-  debug@4.4.3:
-    resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
-    engines: {node: '>=6.0'}
-    peerDependencies:
-      supports-color: '*'
-    peerDependenciesMeta:
-      supports-color:
-        optional: true
-
-  depd@2.0.0:
-    resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
-    engines: {node: '>= 0.8'}
-
-  dotenv@16.6.1:
-    resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==}
-    engines: {node: '>=12'}
-
-  dunder-proto@1.0.1:
-    resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
-    engines: {node: '>= 0.4'}
-
-  ee-first@1.1.1:
-    resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
-
-  encodeurl@2.0.0:
-    resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
-    engines: {node: '>= 0.8'}
-
-  es-define-property@1.0.1:
-    resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
-    engines: {node: '>= 0.4'}
-
-  es-errors@1.3.0:
-    resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
-    engines: {node: '>= 0.4'}
-
-  es-object-atoms@1.1.1:
-    resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
-    engines: {node: '>= 0.4'}
-
-  esbuild@0.27.7:
-    resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==}
-    engines: {node: '>=18'}
-    hasBin: true
-
-  escape-html@1.0.3:
-    resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
-
-  etag@1.8.1:
-    resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
-    engines: {node: '>= 0.6'}
-
-  express@5.2.1:
-    resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==}
-    engines: {node: '>= 18'}
-
-  finalhandler@2.1.1:
-    resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==}
-    engines: {node: '>= 18.0.0'}
-
-  forwarded@0.2.0:
-    resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
-    engines: {node: '>= 0.6'}
-
-  fresh@2.0.0:
-    resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
-    engines: {node: '>= 0.8'}
-
-  fsevents@2.3.3:
-    resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
-    engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
-    os: [darwin]
-
-  function-bind@1.1.2:
-    resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
-
-  get-intrinsic@1.3.0:
-    resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
-    engines: {node: '>= 0.4'}
-
-  get-proto@1.0.1:
-    resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
-    engines: {node: '>= 0.4'}
-
-  get-tsconfig@4.14.0:
-    resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==}
-
-  gopd@1.2.0:
-    resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
-    engines: {node: '>= 0.4'}
-
-  has-symbols@1.1.0:
-    resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
-    engines: {node: '>= 0.4'}
-
-  hasown@2.0.3:
-    resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==}
-    engines: {node: '>= 0.4'}
-
-  http-errors@2.0.1:
-    resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
-    engines: {node: '>= 0.8'}
-
-  iconv-lite@0.7.2:
-    resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==}
-    engines: {node: '>=0.10.0'}
-
-  inherits@2.0.4:
-    resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
-
-  ipaddr.js@1.9.1:
-    resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
-    engines: {node: '>= 0.10'}
-
-  is-promise@4.0.0:
-    resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
-
-  math-intrinsics@1.1.0:
-    resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
-    engines: {node: '>= 0.4'}
-
-  media-typer@1.1.0:
-    resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==}
-    engines: {node: '>= 0.8'}
-
-  merge-descriptors@2.0.0:
-    resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==}
-    engines: {node: '>=18'}
-
-  mime-db@1.54.0:
-    resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==}
-    engines: {node: '>= 0.6'}
-
-  mime-types@3.0.2:
-    resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==}
-    engines: {node: '>=18'}
-
-  ms@2.1.3:
-    resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
-
-  negotiator@1.0.0:
-    resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
-    engines: {node: '>= 0.6'}
-
-  object-assign@4.1.1:
-    resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
-    engines: {node: '>=0.10.0'}
-
-  object-inspect@1.13.4:
-    resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
-    engines: {node: '>= 0.4'}
-
-  on-finished@2.4.1:
-    resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
-    engines: {node: '>= 0.8'}
-
-  once@1.4.0:
-    resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
-
-  parseurl@1.3.3:
-    resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
-    engines: {node: '>= 0.8'}
-
-  path-to-regexp@8.4.2:
-    resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==}
-
-  proxy-addr@2.0.7:
-    resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
-    engines: {node: '>= 0.10'}
-
-  qs@6.15.1:
-    resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==}
-    engines: {node: '>=0.6'}
-
-  range-parser@1.2.1:
-    resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
-    engines: {node: '>= 0.6'}
-
-  raw-body@3.0.2:
-    resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==}
-    engines: {node: '>= 0.10'}
-
-  resolve-pkg-maps@1.0.0:
-    resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
-
-  router@2.2.0:
-    resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==}
-    engines: {node: '>= 18'}
-
-  safer-buffer@2.1.2:
-    resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
-
-  send@1.2.1:
-    resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==}
-    engines: {node: '>= 18'}
-
-  serve-static@2.2.1:
-    resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==}
-    engines: {node: '>= 18'}
-
-  setprototypeof@1.2.0:
-    resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
-
-  side-channel-list@1.0.1:
-    resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==}
-    engines: {node: '>= 0.4'}
-
-  side-channel-map@1.0.1:
-    resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==}
-    engines: {node: '>= 0.4'}
-
-  side-channel-weakmap@1.0.2:
-    resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==}
-    engines: {node: '>= 0.4'}
-
-  side-channel@1.1.0:
-    resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
-    engines: {node: '>= 0.4'}
-
-  statuses@2.0.2:
-    resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
-    engines: {node: '>= 0.8'}
-
-  swagger-ui-dist@5.32.6:
-    resolution: {integrity: sha512-75ttZNaYCLoFPnozPZcTUU6mS3wKT8l7WLjU5zJSHFeJa23i5vtnze6IiCl4jDMPeQTXVXIgovq4M11NNfQvSA==}
-
-  swagger-ui-express@5.0.1:
-    resolution: {integrity: sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==}
-    engines: {node: '>= v0.10.32'}
-    peerDependencies:
-      express: '>=4.0.0 || >=5.0.0-beta'
-
-  toidentifier@1.0.1:
-    resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
-    engines: {node: '>=0.6'}
-
-  tsx@4.21.0:
-    resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==}
-    engines: {node: '>=18.0.0'}
-    hasBin: true
-
-  type-is@2.0.1:
-    resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==}
-    engines: {node: '>= 0.6'}
-
-  typescript@5.9.3:
-    resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
-    engines: {node: '>=14.17'}
-    hasBin: true
-
-  undici-types@7.19.2:
-    resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==}
-
-  unpipe@1.0.0:
-    resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
-    engines: {node: '>= 0.8'}
-
-  vary@1.1.2:
-    resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
-    engines: {node: '>= 0.8'}
-
-  wrappy@1.0.2:
-    resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
-
-snapshots:
-
-  '@esbuild/aix-ppc64@0.27.7':
-    optional: true
-
-  '@esbuild/android-arm64@0.27.7':
-    optional: true
-
-  '@esbuild/android-arm@0.27.7':
-    optional: true
-
-  '@esbuild/android-x64@0.27.7':
-    optional: true
-
-  '@esbuild/darwin-arm64@0.27.7':
-    optional: true
-
-  '@esbuild/darwin-x64@0.27.7':
-    optional: true
-
-  '@esbuild/freebsd-arm64@0.27.7':
-    optional: true
-
-  '@esbuild/freebsd-x64@0.27.7':
-    optional: true
-
-  '@esbuild/linux-arm64@0.27.7':
-    optional: true
-
-  '@esbuild/linux-arm@0.27.7':
-    optional: true
-
-  '@esbuild/linux-ia32@0.27.7':
-    optional: true
-
-  '@esbuild/linux-loong64@0.27.7':
-    optional: true
-
-  '@esbuild/linux-mips64el@0.27.7':
-    optional: true
-
-  '@esbuild/linux-ppc64@0.27.7':
-    optional: true
-
-  '@esbuild/linux-riscv64@0.27.7':
-    optional: true
-
-  '@esbuild/linux-s390x@0.27.7':
-    optional: true
-
-  '@esbuild/linux-x64@0.27.7':
-    optional: true
-
-  '@esbuild/netbsd-arm64@0.27.7':
-    optional: true
-
-  '@esbuild/netbsd-x64@0.27.7':
-    optional: true
-
-  '@esbuild/openbsd-arm64@0.27.7':
-    optional: true
-
-  '@esbuild/openbsd-x64@0.27.7':
-    optional: true
-
-  '@esbuild/openharmony-arm64@0.27.7':
-    optional: true
-
-  '@esbuild/sunos-x64@0.27.7':
-    optional: true
-
-  '@esbuild/win32-arm64@0.27.7':
-    optional: true
-
-  '@esbuild/win32-ia32@0.27.7':
-    optional: true
-
-  '@esbuild/win32-x64@0.27.7':
-    optional: true
-
-  '@scarf/scarf@1.4.0': {}
-
-  '@types/body-parser@1.19.6':
-    dependencies:
-      '@types/connect': 3.4.38
-      '@types/node': 25.6.0
-
-  '@types/connect@3.4.38':
-    dependencies:
-      '@types/node': 25.6.0
-
-  '@types/cors@2.8.19':
-    dependencies:
-      '@types/node': 25.6.0
-
-  '@types/express-serve-static-core@5.1.1':
-    dependencies:
-      '@types/node': 25.6.0
-      '@types/qs': 6.15.1
-      '@types/range-parser': 1.2.7
-      '@types/send': 1.2.1
-
-  '@types/express@5.0.6':
-    dependencies:
-      '@types/body-parser': 1.19.6
-      '@types/express-serve-static-core': 5.1.1
-      '@types/serve-static': 2.2.0
-
-  '@types/http-errors@2.0.5': {}
-
-  '@types/node@25.6.0':
-    dependencies:
-      undici-types: 7.19.2
-
-  '@types/qs@6.15.1': {}
-
-  '@types/range-parser@1.2.7': {}
-
-  '@types/send@1.2.1':
-    dependencies:
-      '@types/node': 25.6.0
-
-  '@types/serve-static@2.2.0':
-    dependencies:
-      '@types/http-errors': 2.0.5
-      '@types/node': 25.6.0
-
-  '@types/swagger-ui-express@4.1.8':
-    dependencies:
-      '@types/express': 5.0.6
-      '@types/serve-static': 2.2.0
-
-  accepts@2.0.0:
-    dependencies:
-      mime-types: 3.0.2
-      negotiator: 1.0.0
-
-  body-parser@2.2.2:
-    dependencies:
-      bytes: 3.1.2
-      content-type: 1.0.5
-      debug: 4.4.3
-      http-errors: 2.0.1
-      iconv-lite: 0.7.2
-      on-finished: 2.4.1
-      qs: 6.15.1
-      raw-body: 3.0.2
-      type-is: 2.0.1
-    transitivePeerDependencies:
-      - supports-color
-
-  bytes@3.1.2: {}
-
-  call-bind-apply-helpers@1.0.2:
-    dependencies:
-      es-errors: 1.3.0
-      function-bind: 1.1.2
-
-  call-bound@1.0.4:
-    dependencies:
-      call-bind-apply-helpers: 1.0.2
-      get-intrinsic: 1.3.0
-
-  content-disposition@1.1.0: {}
-
-  content-type@1.0.5: {}
-
-  cookie-signature@1.2.2: {}
-
-  cookie@0.7.2: {}
-
-  cors@2.8.6:
-    dependencies:
-      object-assign: 4.1.1
-      vary: 1.1.2
-
-  debug@4.4.3:
-    dependencies:
-      ms: 2.1.3
-
-  depd@2.0.0: {}
-
-  dotenv@16.6.1: {}
-
-  dunder-proto@1.0.1:
-    dependencies:
-      call-bind-apply-helpers: 1.0.2
-      es-errors: 1.3.0
-      gopd: 1.2.0
-
-  ee-first@1.1.1: {}
-
-  encodeurl@2.0.0: {}
-
-  es-define-property@1.0.1: {}
-
-  es-errors@1.3.0: {}
-
-  es-object-atoms@1.1.1:
-    dependencies:
-      es-errors: 1.3.0
-
-  esbuild@0.27.7:
-    optionalDependencies:
-      '@esbuild/aix-ppc64': 0.27.7
-      '@esbuild/android-arm': 0.27.7
-      '@esbuild/android-arm64': 0.27.7
-      '@esbuild/android-x64': 0.27.7
-      '@esbuild/darwin-arm64': 0.27.7
-      '@esbuild/darwin-x64': 0.27.7
-      '@esbuild/freebsd-arm64': 0.27.7
-      '@esbuild/freebsd-x64': 0.27.7
-      '@esbuild/linux-arm': 0.27.7
-      '@esbuild/linux-arm64': 0.27.7
-      '@esbuild/linux-ia32': 0.27.7
-      '@esbuild/linux-loong64': 0.27.7
-      '@esbuild/linux-mips64el': 0.27.7
-      '@esbuild/linux-ppc64': 0.27.7
-      '@esbuild/linux-riscv64': 0.27.7
-      '@esbuild/linux-s390x': 0.27.7
-      '@esbuild/linux-x64': 0.27.7
-      '@esbuild/netbsd-arm64': 0.27.7
-      '@esbuild/netbsd-x64': 0.27.7
-      '@esbuild/openbsd-arm64': 0.27.7
-      '@esbuild/openbsd-x64': 0.27.7
-      '@esbuild/openharmony-arm64': 0.27.7
-      '@esbuild/sunos-x64': 0.27.7
-      '@esbuild/win32-arm64': 0.27.7
-      '@esbuild/win32-ia32': 0.27.7
-      '@esbuild/win32-x64': 0.27.7
-
-  escape-html@1.0.3: {}
-
-  etag@1.8.1: {}
-
-  express@5.2.1:
-    dependencies:
-      accepts: 2.0.0
-      body-parser: 2.2.2
-      content-disposition: 1.1.0
-      content-type: 1.0.5
-      cookie: 0.7.2
-      cookie-signature: 1.2.2
-      debug: 4.4.3
-      depd: 2.0.0
-      encodeurl: 2.0.0
-      escape-html: 1.0.3
-      etag: 1.8.1
-      finalhandler: 2.1.1
-      fresh: 2.0.0
-      http-errors: 2.0.1
-      merge-descriptors: 2.0.0
-      mime-types: 3.0.2
-      on-finished: 2.4.1
-      once: 1.4.0
-      parseurl: 1.3.3
-      proxy-addr: 2.0.7
-      qs: 6.15.1
-      range-parser: 1.2.1
-      router: 2.2.0
-      send: 1.2.1
-      serve-static: 2.2.1
-      statuses: 2.0.2
-      type-is: 2.0.1
-      vary: 1.1.2
-    transitivePeerDependencies:
-      - supports-color
-
-  finalhandler@2.1.1:
-    dependencies:
-      debug: 4.4.3
-      encodeurl: 2.0.0
-      escape-html: 1.0.3
-      on-finished: 2.4.1
-      parseurl: 1.3.3
-      statuses: 2.0.2
-    transitivePeerDependencies:
-      - supports-color
-
-  forwarded@0.2.0: {}
-
-  fresh@2.0.0: {}
-
-  fsevents@2.3.3:
-    optional: true
-
-  function-bind@1.1.2: {}
-
-  get-intrinsic@1.3.0:
-    dependencies:
-      call-bind-apply-helpers: 1.0.2
-      es-define-property: 1.0.1
-      es-errors: 1.3.0
-      es-object-atoms: 1.1.1
-      function-bind: 1.1.2
-      get-proto: 1.0.1
-      gopd: 1.2.0
-      has-symbols: 1.1.0
-      hasown: 2.0.3
-      math-intrinsics: 1.1.0
-
-  get-proto@1.0.1:
-    dependencies:
-      dunder-proto: 1.0.1
-      es-object-atoms: 1.1.1
-
-  get-tsconfig@4.14.0:
-    dependencies:
-      resolve-pkg-maps: 1.0.0
-
-  gopd@1.2.0: {}
-
-  has-symbols@1.1.0: {}
-
-  hasown@2.0.3:
-    dependencies:
-      function-bind: 1.1.2
-
-  http-errors@2.0.1:
-    dependencies:
-      depd: 2.0.0
-      inherits: 2.0.4
-      setprototypeof: 1.2.0
-      statuses: 2.0.2
-      toidentifier: 1.0.1
-
-  iconv-lite@0.7.2:
-    dependencies:
-      safer-buffer: 2.1.2
-
-  inherits@2.0.4: {}
-
-  ipaddr.js@1.9.1: {}
-
-  is-promise@4.0.0: {}
-
-  math-intrinsics@1.1.0: {}
-
-  media-typer@1.1.0: {}
-
-  merge-descriptors@2.0.0: {}
-
-  mime-db@1.54.0: {}
-
-  mime-types@3.0.2:
-    dependencies:
-      mime-db: 1.54.0
-
-  ms@2.1.3: {}
-
-  negotiator@1.0.0: {}
-
-  object-assign@4.1.1: {}
-
-  object-inspect@1.13.4: {}
-
-  on-finished@2.4.1:
-    dependencies:
-      ee-first: 1.1.1
-
-  once@1.4.0:
-    dependencies:
-      wrappy: 1.0.2
-
-  parseurl@1.3.3: {}
-
-  path-to-regexp@8.4.2: {}
-
-  proxy-addr@2.0.7:
-    dependencies:
-      forwarded: 0.2.0
-      ipaddr.js: 1.9.1
-
-  qs@6.15.1:
-    dependencies:
-      side-channel: 1.1.0
-
-  range-parser@1.2.1: {}
-
-  raw-body@3.0.2:
-    dependencies:
-      bytes: 3.1.2
-      http-errors: 2.0.1
-      iconv-lite: 0.7.2
-      unpipe: 1.0.0
-
-  resolve-pkg-maps@1.0.0: {}
-
-  router@2.2.0:
-    dependencies:
-      debug: 4.4.3
-      depd: 2.0.0
-      is-promise: 4.0.0
-      parseurl: 1.3.3
-      path-to-regexp: 8.4.2
-    transitivePeerDependencies:
-      - supports-color
-
-  safer-buffer@2.1.2: {}
-
-  send@1.2.1:
-    dependencies:
-      debug: 4.4.3
-      encodeurl: 2.0.0
-      escape-html: 1.0.3
-      etag: 1.8.1
-      fresh: 2.0.0
-      http-errors: 2.0.1
-      mime-types: 3.0.2
-      ms: 2.1.3
-      on-finished: 2.4.1
-      range-parser: 1.2.1
-      statuses: 2.0.2
-    transitivePeerDependencies:
-      - supports-color
-
-  serve-static@2.2.1:
-    dependencies:
-      encodeurl: 2.0.0
-      escape-html: 1.0.3
-      parseurl: 1.3.3
-      send: 1.2.1
-    transitivePeerDependencies:
-      - supports-color
-
-  setprototypeof@1.2.0: {}
-
-  side-channel-list@1.0.1:
-    dependencies:
-      es-errors: 1.3.0
-      object-inspect: 1.13.4
-
-  side-channel-map@1.0.1:
-    dependencies:
-      call-bound: 1.0.4
-      es-errors: 1.3.0
-      get-intrinsic: 1.3.0
-      object-inspect: 1.13.4
-
-  side-channel-weakmap@1.0.2:
-    dependencies:
-      call-bound: 1.0.4
-      es-errors: 1.3.0
-      get-intrinsic: 1.3.0
-      object-inspect: 1.13.4
-      side-channel-map: 1.0.1
-
-  side-channel@1.1.0:
-    dependencies:
-      es-errors: 1.3.0
-      object-inspect: 1.13.4
-      side-channel-list: 1.0.1
-      side-channel-map: 1.0.1
-      side-channel-weakmap: 1.0.2
-
-  statuses@2.0.2: {}
-
-  swagger-ui-dist@5.32.6:
-    dependencies:
-      '@scarf/scarf': 1.4.0
-
-  swagger-ui-express@5.0.1(express@5.2.1):
-    dependencies:
-      express: 5.2.1
-      swagger-ui-dist: 5.32.6
-
-  toidentifier@1.0.1: {}
-
-  tsx@4.21.0:
-    dependencies:
-      esbuild: 0.27.7
-      get-tsconfig: 4.14.0
-    optionalDependencies:
-      fsevents: 2.3.3
-
-  type-is@2.0.1:
-    dependencies:
-      content-type: 1.0.5
-      media-typer: 1.1.0
-      mime-types: 3.0.2
-
-  typescript@5.9.3: {}
-
-  undici-types@7.19.2: {}
-
-  unpipe@1.0.0: {}
-
-  vary@1.1.2: {}
-
-  wrappy@1.0.2: {}

+ 0 - 24
lami-base-v1/backend/src/apps/mobile/chat/app.ts

@@ -1,24 +0,0 @@
-import express from 'express';
-import cors from 'cors';
-import type { Express } from 'express';
-import { getStringEnv } from '../../../shared/config/env.js';
-import { apiKeyAuth } from '../../../shared/http/auth.middleware.js';
-import { errorHandler } from '../../../shared/http/error-handler.js';
-import { notFoundHandler } from '../../../shared/http/not-found.middleware.js';
-import { mobileChatApiRouter } from './routes/chat.routes.js';
-
-export function createMobileChatApp(): Express {
-  const app = express();
-
-  const corsOrigin = getStringEnv('CORS_ORIGIN') ?? 'http://localhost:4200';
-  app.use(cors({ origin: corsOrigin.split(',').map((o) => o.trim()), credentials: true }));
-  app.use(express.json({ limit: '1mb' }));
-  app.use(apiKeyAuth);
-
-  app.use('/api', mobileChatApiRouter);
-
-  app.use(notFoundHandler);
-  app.use(errorHandler);
-
-  return app;
-}

+ 0 - 141
lami-base-v1/backend/src/apps/mobile/chat/controllers/chat.controller.ts

@@ -1,141 +0,0 @@
-import type { Request, Response } from 'express';
-
-interface ChatRequestBody {
-  prompt: string;
-  sessionId?: string;
-}
-
-interface DashScopeRequestBody {
-  input: {
-    prompt: string;
-    session_id?: string;
-  };
-  parameters: {
-    incremental_output: boolean;
-    has_thoughts: boolean;
-  };
-  debug: Record<string, unknown>;
-}
-
-interface DashScopeErrorResponse {
-  message?: string;
-}
-
-export async function postChat(req: Request, res: Response) {
-  const { prompt, sessionId } = req.body as ChatRequestBody;
-
-  if (!prompt) {
-    res.status(400).json({ error: { message: 'Prompt is required' } });
-    return;
-  }
-
-  const apiKey = process.env.DASHSCOPE_API_KEY;
-  const appId = process.env.DASHSCOPE_APP_ID;
-
-  if (!apiKey || !appId) {
-    res.status(500).json({ error: { message: 'API configuration missing' } });
-    return;
-  }
-
-  const apiUrl = `https://dashscope.aliyuncs.com/api/v1/apps/${appId}/completion`;
-
-  const requestBody: DashScopeRequestBody = {
-    input: { prompt },
-    parameters: {
-      incremental_output: true,
-      has_thoughts: true
-    },
-    debug: {}
-  };
-
-  if (sessionId) {
-    requestBody.input.session_id = sessionId;
-  }
-
-  try {
-    const response = await fetch(apiUrl, {
-      method: 'POST',
-      headers: {
-        'Authorization': `Bearer ${apiKey}`,
-        'Content-Type': 'application/json',
-        'X-DashScope-SSE': 'enable'
-      },
-      body: JSON.stringify(requestBody)
-    });
-
-    if (!response.ok) {
-      const errorData = await response.json().catch(() => ({})) as DashScopeErrorResponse;
-      res
-        .status(response.status)
-        .json({ error: { message: errorData.message || `Request failed: ${response.status}` } });
-      return;
-    }
-
-    res.setHeader('Content-Type', 'text/event-stream');
-    res.setHeader('Cache-Control', 'no-cache');
-    res.setHeader('Connection', 'keep-alive');
-
-    const reader = response.body?.getReader();
-    const decoder = new TextDecoder();
-
-    if (!reader) {
-      res.status(500).json({ error: { message: 'Failed to read response stream' } });
-      return;
-    }
-
-    let buffer = '';
-
-    const readStream = async () => {
-      try {
-        while (true) {
-          const { done, value } = await reader.read();
-          if (done) {
-            if (buffer) {
-              const lines = buffer.split('\n');
-              for (const line of lines) {
-                if (line.startsWith('data:')) {
-                  const data = line.slice(5).trim();
-                  if (data) {
-                    res.write(`data: ${data}\n\n`);
-                  }
-                }
-              }
-            }
-            res.end();
-            break;
-          }
-
-          buffer += decoder.decode(value, { stream: true });
-          const lines = buffer.split('\n');
-          buffer = lines.pop() || '';
-
-          for (const line of lines) {
-            if (line.startsWith('data:')) {
-              const data = line.slice(5).trim();
-              if (data) {
-                res.write(`data: ${data}\n\n`);
-              }
-            }
-          }
-        }
-      } catch (error) {
-        console.error('Stream error:', error);
-        res.end();
-      }
-    };
-
-    readStream();
-
-    req.on('close', () => {
-      reader.cancel();
-    });
-  } catch (error) {
-    console.error('Chat error:', error);
-    res.status(500).json({ error: { message: error instanceof Error ? error.message : 'Internal server error' } });
-  }
-}
-
-export function getHealth(_req: Request, res: Response) {
-  res.json({ status: 'ok', timestamp: new Date().toISOString() });
-}
-

+ 0 - 8
lami-base-v1/backend/src/apps/mobile/chat/routes/chat.routes.ts

@@ -1,8 +0,0 @@
-import { Router } from 'express';
-import type { Router as ExpressRouter } from 'express';
-import { getHealth, postChat } from '../controllers/chat.controller.js';
-
-export const mobileChatApiRouter: ExpressRouter = Router();
-
-mobileChatApiRouter.post('/chat', postChat);
-mobileChatApiRouter.get('/health', getHealth);

+ 0 - 11
lami-base-v1/backend/src/apps/mobile/chat/server.ts

@@ -1,11 +0,0 @@
-import { createMobileChatApp } from './app.js';
-import { getNumberEnv } from '../../../shared/config/env.js';
-
-export function startMobileChatServer() {
-  const app = createMobileChatApp();
-  const port = getNumberEnv('MOBILE_CHAT_PORT', 3201);
-
-  app.listen(port, () => {
-    console.log(`mobile-chat server running on http://localhost:${port}`);
-  });
-}

+ 0 - 114
lami-base-v1/backend/src/apps/pc/app.ts

@@ -1,114 +0,0 @@
-/**
- * PC 端统一 Express 应用装配层
- *
- * 职责:
- *   - 创建唯一的 Express 实例
- *   - 挂载通用中间件(cors、json 解析、请求日志)
- *   - 注册所有 PC 模块的路由
- *   - 挂载统一错误处理
- *   - 挂载全局健康检查
- *
- * 模块路由一览(共 11 个业务模块):
- *   /api/health         健康检查
- *   /api/qiwei          企微 API 代理网关        [模块 1]
- *   /api/staff          人员管理                  [模块 1]
- *   /api/communities    小区管理                  [模块 2]
- *   /api/community-room-bindings  群-小区绑定     [模块 2]
- *   /api/rooms          群管理                    [模块 2]
- *   /api/compliance     合规检查                  [模块 4]
- *   /api/risk           群风控                    [模块 5]
- *   /api/content        内容运营                  [模块 6]
- *   /api/channels       渠道 / 联系人 / KOC       [模块 7]
- *   /api/contacts       外部联系人                [模块 7]
- *   /api/koc            KOC 管理                  [模块 7]
- *   /api/intent-leads   意向客户                  [模块 7]
- *   /api/intent-tasks   跟进待办                  [模块 7]
- *   /api/dashboard      数据看板                  [模块 8]
- *   /api/sales          经营数据补录              [模块 9]
- *   /api/workbench      统一工作台                [模块 10]
- *   /api/notifications  通知管理                  [模块 10]
- *   /api/audit-logs     审计日志                  [模块 10]
- */
-
-import express from 'express';
-import cors from 'cors';
-import type { Express, Request, Response, NextFunction } from 'express';
-import { getStringEnv } from '../../shared/config/env.js';
-import { errorHandler } from '../../shared/http/error-handler.js';
-import { apiKeyAuth, qiweiProxyGuard } from '../../shared/http/auth.middleware.js';
-import { notFoundHandler } from '../../shared/http/not-found.middleware.js';
-import { setupSwagger } from '../../shared/swagger/setup-swagger.js';
-
-// ---- 导入所有 PC 模块路由 ----
-import { pcHealthApiRouter } from './health/routes/health.routes.js';
-import { pcQiWeApiRouter } from './qiwei/routes/qiwei.routes.js';
-import { pcStaffApiRouter } from './staff/routes/staff.routes.js';
-import { pcCommunityApiRouter } from './community/routes/community.routes.js';
-import { pcRoomApiRouter } from './room/routes/room.routes.js';
-import { pcComplianceApiRouter } from './compliance/routes/compliance.routes.js';
-import { pcRiskApiRouter } from './risk/routes/risk.routes.js';
-import { pcContentApiRouter } from './content/routes/content.routes.js';
-import { pcKocApiRouter } from './koc/routes/koc.routes.js';
-import { pcDashboardApiRouter } from './dashboard/routes/dashboard.routes.js';
-import { pcSalesApiRouter } from './sales/routes/sales.routes.js';
-import { pcWorkbenchApiRouter } from './workbench/routes/workbench.routes.js';
-
-/**
- * 请求日志中间件
- * 记录每个请求的方法、路径、状态码和耗时
- */
-function requestLogger(req: Request, res: Response, next: NextFunction): void {
-  const start = Date.now();
-  res.on('finish', () => {
-    const duration = Date.now() - start;
-    const level = res.statusCode >= 400 ? 'WARN' : 'INFO';
-    console.log(
-      `[PC:${level}] ${req.method} ${req.originalUrl} → ${res.statusCode} (${duration}ms)`,
-    );
-  });
-  next();
-}
-
-/**
- * 创建 PC 端统一 Express 应用
- *
- * 所有 PC 模块的路由都挂载在同一个 app 实例上,
- * 共享同一套中间件(cors、json 解析、日志、错误处理)。
- */
-export function createPcApp(): Express {
-  const app = express();
-
-  // ---- 通用中间件 ----
-  const corsOrigin = getStringEnv('CORS_ORIGIN') ?? 'http://localhost:4200';
-  app.use(cors({ origin: corsOrigin.split(',').map((o) => o.trim()), credentials: true }));
-  app.use(express.json({ limit: '2mb' }));
-  app.use(requestLogger);
-  app.use(apiKeyAuth);
-  app.use(qiweiProxyGuard);
-
-  // ---- Swagger UI 接口测试台(领导要求的「能点着测」) ----
-  setupSwagger(app);
-
-  // ---- 注册所有模块路由 ----
-  // 注意:每个模块的路由文件已定义好各自的前缀(如 /staff、/rooms 等),
-  // 统一挂在 /api 下即可
-  app.use('/api', pcHealthApiRouter);
-  app.use('/api', pcQiWeApiRouter);
-  app.use('/api', pcStaffApiRouter);
-  app.use('/api', pcCommunityApiRouter);
-  app.use('/api', pcRoomApiRouter);
-  app.use('/api', pcComplianceApiRouter);
-  app.use('/api', pcRiskApiRouter);
-  app.use('/api', pcContentApiRouter);
-  app.use('/api', pcKocApiRouter);
-  app.use('/api', pcDashboardApiRouter);
-  app.use('/api', pcSalesApiRouter);
-  app.use('/api', pcWorkbenchApiRouter);
-
-  app.use(notFoundHandler);
-
-  // ---- 统一错误处理(必须放在所有路由之后) ----
-  app.use(errorHandler);
-
-  return app;
-}

+ 0 - 155
lami-base-v1/backend/src/apps/pc/community/controllers/community.controller.ts

@@ -1,155 +0,0 @@
-/**
- * 小区管理模块 — 控制器层
- *
- * 对应功能:
- *   §5.5「录入小区基础档案」
- *   §5.6「维护小区—群—门店对应关系」
- */
-
-import type { Request, Response } from 'express';
-import { sendSuccess, sendError } from '../../../../shared/http/response.js';
-import {
-  createCommunity,
-  listCommunities,
-  getCommunityById,
-  updateCommunity,
-  deleteCommunity,
-  createBinding,
-  listBindings,
-  deleteBinding,
-  getBindingByRoomId,
-} from '../services/community.service.js';
-
-// ============================================================
-// 小区 CRUD
-// ============================================================
-
-/** POST /api/communities — 录入小区档案 */
-export async function create(req: Request, res: Response): Promise<void> {
-  try {
-    const { name, totalHouseholds, storeId } = req.body as {
-      name?: string;
-      totalHouseholds?: number;
-      storeId?: number;
-    };
-
-    if (!name || !totalHouseholds || !storeId) {
-      sendError(res, 400, 'VALIDATION_ERROR', '缺少必填参数:name, totalHouseholds, storeId');
-      return;
-    }
-
-    const community = createCommunity(req.body);
-    sendSuccess(res, community, 201);
-  } catch (error) {
-    throw error;
-  }
-}
-
-/** GET /api/communities — 查询小区列表 */
-export async function list(req: Request, res: Response): Promise<void> {
-  try {
-    const { storeId } = req.query;
-    const result = listCommunities(storeId ? Number(storeId) : undefined);
-    sendSuccess(res, result);
-  } catch (error) {
-    throw error;
-  }
-}
-
-/** GET /api/communities/:id — 查询小区详情 */
-export async function getById(req: Request, res: Response): Promise<void> {
-  try {
-    const id = Number(req.params.id);
-    const community = getCommunityById(id);
-    sendSuccess(res, community);
-  } catch (error) {
-    throw error;
-  }
-}
-
-/** PUT /api/communities/:id — 更新小区信息 */
-export async function update(req: Request, res: Response): Promise<void> {
-  try {
-    const id = Number(req.params.id);
-    const community = updateCommunity(id, req.body ?? {});
-    sendSuccess(res, community);
-  } catch (error) {
-    throw error;
-  }
-}
-
-/** DELETE /api/communities/:id — 删除小区 */
-export async function remove(req: Request, res: Response): Promise<void> {
-  try {
-    const id = Number(req.params.id);
-    deleteCommunity(id);
-    sendSuccess(res, { deleted: true });
-  } catch (error) {
-    throw error;
-  }
-}
-
-// ============================================================
-// 小区-群-门店绑定
-// ============================================================
-
-/** POST /api/community-room-bindings — 绑定群到小区 */
-export async function bind(req: Request, res: Response): Promise<void> {
-  try {
-    const { communityId, roomId, storeId } = req.body as {
-      communityId?: number;
-      roomId?: string;
-      storeId?: number;
-    };
-
-    if (!communityId || !roomId || !storeId) {
-      sendError(res, 400, 'VALIDATION_ERROR', '缺少必填参数:communityId, roomId, storeId');
-      return;
-    }
-
-    const binding = createBinding({ communityId, roomId, storeId });
-    sendSuccess(res, binding, 201);
-  } catch (error) {
-    throw error;
-  }
-}
-
-/** GET /api/community-room-bindings — 查询绑定列表 */
-export async function listBindingList(req: Request, res: Response): Promise<void> {
-  try {
-    const { communityId, storeId } = req.query;
-    const result = listBindings(
-      typeof communityId === 'string' ? Number(communityId) : undefined,
-      typeof storeId === 'string' ? Number(storeId) : undefined,
-    );
-    sendSuccess(res, result);
-  } catch (error) {
-    throw error;
-  }
-}
-
-/** GET /api/community-room-bindings/by-room/:roomId — 按群 ID 查询绑定 */
-export async function getBindingByRoom(req: Request, res: Response): Promise<void> {
-  try {
-    const roomId = String(req.params.roomId);
-    const binding = getBindingByRoomId(roomId);
-    if (!binding) {
-      sendError(res, 404, 'BINDING_NOT_FOUND', `群 ${roomId} 未绑定任何小区`);
-      return;
-    }
-    sendSuccess(res, binding);
-  } catch (error) {
-    throw error;
-  }
-}
-
-/** DELETE /api/community-room-bindings/:id — 解绑 */
-export async function unbind(req: Request, res: Response): Promise<void> {
-  try {
-    const id = Number(req.params.id);
-    deleteBinding(id);
-    sendSuccess(res, { deleted: true });
-  } catch (error) {
-    throw error;
-  }
-}

+ 0 - 69
lami-base-v1/backend/src/apps/pc/community/models/community.model.ts

@@ -1,69 +0,0 @@
-/**
- * 小区管理模块 — 数据模型定义
- *
- * 对应规范文档:
- *   §5.5「录入小区基础档案」
- *   §5.6「维护小区—群—门店对应关系」
- *
- * 核心概念:
- *   - 小区 (Community):楼盘的档案信息(户数、房价、交房时间等)
- *   - 门店 (Store):所属门店
- *   - 群 (Room):企微外部客户群
- *   - 三者通过 CommunityRoomBinding 表关联:communityId ↔ roomId ↔ storeId
- */
-
-/** 小区基础档案 */
-export interface Community {
-  /** 主键 */
-  id: number;
-  /** 小区名称 */
-  name: string;
-  /** 总户数(触达率计算的分母,无法从 QiWe 自动获取) */
-  totalHouseholds: number;
-  /** 参考均价(元/㎡) */
-  avgPrice: number;
-  /** 交房年份 */
-  deliveryYear: number;
-  /** 所属门店 ID */
-  storeId: number;
-  /** 详细地址 */
-  address: string;
-  /** 备注 */
-  remark: string;
-  /** 创建时间 */
-  createdAt: string;
-  /** 更新时间 */
-  updatedAt: string;
-}
-
-/** 创建小区请求体 */
-export interface CreateCommunityRequest {
-  name: string;
-  totalHouseholds: number;
-  avgPrice?: number;
-  deliveryYear?: number;
-  storeId: number;
-  address?: string;
-  remark?: string;
-}
-
-/** 小区-群-门店绑定关系 */
-export interface CommunityRoomBinding {
-  /** 主键 */
-  id: number;
-  /** 小区 ID */
-  communityId: number;
-  /** 群 ID(企微 roomId) */
-  roomId: string;
-  /** 门店 ID(冗余,方便查询) */
-  storeId: number;
-  /** 绑定时间 */
-  createdAt: string;
-}
-
-/** 创建绑定请求体 */
-export interface CreateBindingRequest {
-  communityId: number;
-  roomId: string;
-  storeId: number;
-}

+ 0 - 43
lami-base-v1/backend/src/apps/pc/community/routes/community.routes.ts

@@ -1,43 +0,0 @@
-/**
- * 小区管理模块 — 路由层
- *
- * 接口一览:
- *   POST   /api/communities                            录入小区档案
- *   GET    /api/communities                            查询小区列表
- *   GET    /api/communities/:id                        查询小区详情
- *   PUT    /api/communities/:id                        更新小区信息
- *   DELETE /api/communities/:id                        删除小区
- *   POST   /api/community-room-bindings                绑定群到小区
- *   GET    /api/community-room-bindings                查询绑定列表
- *   GET    /api/community-room-bindings/by-room/:roomId 按群查询绑定
- *   DELETE /api/community-room-bindings/:id             解绑
- */
-
-import { Router } from 'express';
-import type { Router as ExpressRouter } from 'express';
-import {
-  create,
-  list,
-  getById,
-  update,
-  remove,
-  bind,
-  listBindingList,
-  getBindingByRoom,
-  unbind,
-} from '../controllers/community.controller.js';
-
-export const pcCommunityApiRouter: ExpressRouter = Router();
-
-// 小区档案
-pcCommunityApiRouter.post('/communities', create);
-pcCommunityApiRouter.get('/communities', list);
-pcCommunityApiRouter.get('/communities/:id', getById);
-pcCommunityApiRouter.put('/communities/:id', update);
-pcCommunityApiRouter.delete('/communities/:id', remove);
-
-// 绑定关系
-pcCommunityApiRouter.post('/community-room-bindings', bind);
-pcCommunityApiRouter.get('/community-room-bindings', listBindingList);
-pcCommunityApiRouter.get('/community-room-bindings/by-room/:roomId', getBindingByRoom);
-pcCommunityApiRouter.delete('/community-room-bindings/:id', unbind);

+ 0 - 189
lami-base-v1/backend/src/apps/pc/community/services/community.service.ts

@@ -1,189 +0,0 @@
-/**
- * 小区管理模块 — 业务服务层
- *
- * 对应规范文档:
- *   §5.5「录入小区基础档案」
- *   §5.6「维护小区—群—门店对应关系」
- *
- * 业务规则:
- *   1. 小区户数无法从 QiWe 自动获取,需人工录入
- *   2. 群与小区/门店的绑定关系禁止自动猜测——必须人工确认
- *   3. 未绑定群在看板中标记为「待维护」
- *   4. 报表按小区/门店聚合时,通过绑定表 INNER JOIN
- */
-
-import { AppError } from '../../../../shared/errors/app-error.js';
-import type {
-  Community,
-  CreateCommunityRequest,
-  CommunityRoomBinding,
-  CreateBindingRequest,
-} from '../models/community.model.js';
-
-// ---------- 内存存储(TODO: 替换为数据库)----------
-const communityStore = new Map<number, Community>();
-const bindingStore = new Map<number, CommunityRoomBinding>();
-let nextCommunityId = 1;
-let nextBindingId = 1;
-
-// ============================================================
-// 小区档案 CRUD
-// ============================================================
-
-/**
- * 录入小区基础档案
- *
- * 户数是触达率计算的分母,必须准确。
- * 房价、交房时间等字段供看板和经营分析使用。
- */
-export function createCommunity(data: CreateCommunityRequest): Community {
-  const now = new Date().toISOString();
-  const community: Community = {
-    id: nextCommunityId++,
-    name: data.name,
-    totalHouseholds: data.totalHouseholds,
-    avgPrice: data.avgPrice || 0,
-    deliveryYear: data.deliveryYear || 0,
-    storeId: data.storeId,
-    address: data.address || '',
-    remark: data.remark || '',
-    createdAt: now,
-    updatedAt: now,
-  };
-
-  communityStore.set(community.id, community);
-  return community;
-}
-
-/**
- * 查询小区列表(支持按门店筛选)
- */
-export function listCommunities(storeId?: number): Community[] {
-  let result = Array.from(communityStore.values());
-  if (storeId !== undefined) {
-    result = result.filter((c) => c.storeId === storeId);
-  }
-  return result;
-}
-
-/**
- * 查询单个小区详情
- */
-export function getCommunityById(id: number): Community {
-  const community = communityStore.get(id);
-  if (!community) {
-    throw new AppError(404, 'COMMUNITY_NOT_FOUND', `小区 ID=${id} 不存在`);
-  }
-  return community;
-}
-
-/**
- * 更新小区信息
- */
-export function updateCommunity(
-  id: number,
-  data: Partial<CreateCommunityRequest> = {},
-): Community {
-  const community = getCommunityById(id);
-
-  if (data.name !== undefined) community.name = data.name;
-  if (data.totalHouseholds !== undefined) community.totalHouseholds = data.totalHouseholds;
-  if (data.avgPrice !== undefined) community.avgPrice = data.avgPrice;
-  if (data.deliveryYear !== undefined) community.deliveryYear = data.deliveryYear;
-  if (data.storeId !== undefined) community.storeId = data.storeId;
-  if (data.address !== undefined) community.address = data.address;
-  if (data.remark !== undefined) community.remark = data.remark;
-  community.updatedAt = new Date().toISOString();
-
-  communityStore.set(id, community);
-  return community;
-}
-
-/**
- * 删除小区
- */
-export function deleteCommunity(id: number): void {
-  getCommunityById(id);
-  communityStore.delete(id);
-
-  // 同时清理关联的绑定关系
-  for (const [key, binding] of bindingStore) {
-    if (binding.communityId === id) {
-      bindingStore.delete(key);
-    }
-  }
-}
-
-// ============================================================
-// 小区-群-门店绑定
-// ============================================================
-
-/**
- * 创建小区-群-门店绑定关系
- *
- * 业务约束:
- *   - 同一个 roomId 不可重复绑定到不同的小区
- *   - 绑定禁止自动猜测,必须人工确认
- *
- * @throws {AppError} 当 roomId 已被绑定时
- */
-export function createBinding(data: CreateBindingRequest): CommunityRoomBinding {
-  // 检查 roomId 是否已绑定
-  for (const binding of bindingStore.values()) {
-    if (binding.roomId === data.roomId) {
-      throw new AppError(
-        409,
-        'BINDING_DUPLICATE',
-        `群 ${data.roomId} 已绑定到小区 ID=${binding.communityId},不可重复绑定`,
-      );
-    }
-  }
-
-  const binding: CommunityRoomBinding = {
-    id: nextBindingId++,
-    communityId: data.communityId,
-    roomId: data.roomId,
-    storeId: data.storeId,
-    createdAt: new Date().toISOString(),
-  };
-
-  bindingStore.set(binding.id, binding);
-  return binding;
-}
-
-/**
- * 查询绑定关系列表(按小区或门店筛选)
- */
-export function listBindings(communityId?: number, storeId?: number): CommunityRoomBinding[] {
-  let result = Array.from(bindingStore.values());
-  if (communityId !== undefined) {
-    result = result.filter((b) => b.communityId === communityId);
-  }
-  if (storeId !== undefined) {
-    result = result.filter((b) => b.storeId === storeId);
-  }
-  return result;
-}
-
-/**
- * 删除绑定关系(解绑)
- */
-export function deleteBinding(id: number): void {
-  const binding = bindingStore.get(id);
-  if (!binding) {
-    throw new AppError(404, 'BINDING_NOT_FOUND', `绑定 ID=${id} 不存在`);
-  }
-  bindingStore.delete(id);
-}
-
-/**
- * 按 roomId 查询绑定(用于合规等模块关联查询)
- */
-export function getBindingByRoomId(roomId: string): CommunityRoomBinding | undefined {
-  for (const binding of bindingStore.values()) {
-    if (binding.roomId === roomId) {
-      return binding;
-    }
-  }
-  return undefined;
-}

+ 0 - 120
lami-base-v1/backend/src/apps/pc/compliance/controllers/compliance.controller.ts

@@ -1,120 +0,0 @@
-/**
- * 合规检查模块 — 控制器层
- *
- * 对应规范文档 §七「模块 4:沟通记录合规检查模块」
- */
-
-import type { Request, Response } from 'express';
-import { sendSuccess, sendError } from '../../../../shared/http/response.js';
-import {
-  runComplianceScan,
-  listIssues,
-  getIssueById,
-  updateIssueStatus,
-  generateReport,
-  getLastScan,
-} from '../services/compliance.service.js';
-import type { ComplianceIssueType, IssueStatus } from '../models/compliance.model.js';
-
-/**
- * POST /api/compliance/scan
- *
- * 触发合规巡检(手动)。
- *
- * Body:
- *   { "roomIds": ["10791082xxxx", "10791083xxxx"] }
- */
-export async function scan(req: Request, res: Response): Promise<void> {
-  try {
-    const { roomIds } = req.body as { roomIds?: string[] };
-
-    if (!roomIds || !Array.isArray(roomIds) || roomIds.length === 0) {
-      sendError(res, 400, 'VALIDATION_ERROR', '缺少 roomIds 参数(须为非空数组)');
-      return;
-    }
-
-    const result = runComplianceScan(roomIds);
-    sendSuccess(res, result);
-  } catch (error) {
-    throw error;
-  }
-}
-
-/**
- * GET /api/compliance/issues
- *
- * 查询合规问题列表(支持按 type/status/severity 筛选)。
- */
-export async function listIssueList(req: Request, res: Response): Promise<void> {
-  try {
-    const { type, status, severity } = req.query;
-    const issues = listIssues(
-      type as ComplianceIssueType | undefined,
-      status as IssueStatus | undefined,
-      severity ? Number(severity) : undefined,
-    );
-    sendSuccess(res, issues);
-  } catch (error) {
-    throw error;
-  }
-}
-
-/**
- * GET /api/compliance/issues/:id
- *
- * 查询单个合规问题详情。
- */
-export async function getIssue(req: Request, res: Response): Promise<void> {
-  try {
-    const id = Number(req.params.id);
-    const issue = getIssueById(id);
-    sendSuccess(res, issue);
-  } catch (error) {
-    throw error;
-  }
-}
-
-/**
- * PUT /api/compliance/issues/:id
- *
- * 更新合规问题状态(认领/处理/关闭)。
- *
- * Body:
- *   { "status": "in_progress", "note": "已开始整改" }
- */
-export async function updateIssue(req: Request, res: Response): Promise<void> {
-  try {
-    const id = Number(req.params.id);
-    const { status, note } = req.body as { status?: IssueStatus; note?: string };
-
-    const validStatuses: IssueStatus[] = ['open', 'in_progress', 'resolved', 'closed'];
-    if (!status || !validStatuses.includes(status)) {
-      sendError(res, 400, 'VALIDATION_ERROR', `无效的状态:${status},可选:${validStatuses.join(', ')}`);
-      return;
-    }
-
-    const issue = updateIssueStatus(id, status, note);
-    sendSuccess(res, issue);
-  } catch (error) {
-    throw error;
-  }
-}
-
-/**
- * GET /api/compliance/reports
- *
- * 获取合规汇总报表(§7.16)。
- *
- * Query params:
- *   totalRooms - 纳入统计的总群数(必填)
- */
-export async function report(req: Request, res: Response): Promise<void> {
-  try {
-    const totalRooms = Number(req.query.totalRooms) || 0;
-    const report = generateReport(totalRooms);
-    const lastScan = getLastScan();
-    sendSuccess(res, { report, lastScan });
-  } catch (error) {
-    throw error;
-  }
-}

+ 0 - 109
lami-base-v1/backend/src/apps/pc/compliance/models/compliance.model.ts

@@ -1,109 +0,0 @@
-/**
- * 合规检查模块 — 数据模型定义
- *
- * 对应规范文档 §七「模块 4:沟通记录合规检查模块」
- *
- * 核心概念:
- *   - compliance_issue:合规问题记录(缺表、未置顶、格式错误、内容太简等)
- *   - compliance_scan:巡检任务记录
- *   - audit_log:操作审计日志
- *
- * 问题类型枚举(issue type):
- *   - missing_doc       :群还没有沟通记录表
- *   - not_pinned        :记录表未在群内置顶
- *   - no_doc_in_notice  :群公告中无文档链接
- *   - multi_doc         :群内出现多张表
- *   - required_empty    :必填项为空
- *   - format_error      :日期/格式/版式不规范
- *   - stale_doc         :记录表很久没更新
- *   - too_brief         :内容过于简略
- *   - suspected_miss_log:群里聊过但表没更新
- */
-
-/** 合规问题类型 */
-export type ComplianceIssueType =
-  | 'missing_doc'
-  | 'not_pinned'
-  | 'no_doc_in_notice'
-  | 'multi_doc'
-  | 'required_empty'
-  | 'format_error'
-  | 'stale_doc'
-  | 'too_brief'
-  | 'suspected_miss_log';
-
-/** 合规问题类型中文标签 */
-export const ComplianceIssueTypeLabel: Record<ComplianceIssueType, string> = {
-  missing_doc: '缺少沟通记录表',
-  not_pinned: '记录表未置顶',
-  no_doc_in_notice: '群公告无文档链接',
-  multi_doc: '群内存在多张表',
-  required_empty: '必填项为空',
-  format_error: '格式或版式不规范',
-  stale_doc: '记录表长期未更新',
-  too_brief: '内容过于简略',
-  suspected_miss_log: '疑似漏记(群内聊天但表未更新)',
-};
-
-/** 合规问题状态 */
-export type IssueStatus = 'open' | 'in_progress' | 'resolved' | 'closed';
-
-/** 合规问题记录 */
-export interface ComplianceIssue {
-  /** 主键 */
-  id: number;
-  /** 群 ID */
-  roomId: string;
-  /** 群名称(冗余) */
-  roomName: string;
-  /** 问题类型 */
-  type: ComplianceIssueType;
-  /** 问题描述 */
-  description: string;
-  /** 严重程度:1=低, 2=中, 3=高 */
-  severity: 1 | 2 | 3;
-  /** 状态 */
-  status: IssueStatus;
-  /** 负责人 ID */
-  assigneeId: number;
-  /** 关联的文档 ID(如有) */
-  docId: string;
-  /** 发现时间 */
-  foundAt: string;
-  /** 解决时间 */
-  resolvedAt: string;
-  /** 创建时间 */
-  createdAt: string;
-}
-
-/** 合规巡检任务 */
-export interface ComplianceScan {
-  /** 主键 */
-  id: number;
-  /** 巡检时间 */
-  scannedAt: string;
-  /** 巡检群数 */
-  totalRooms: number;
-  /** 新发现问题数 */
-  newIssues: number;
-  /** 已自动关闭的问题数 */
-  resolvedIssues: number;
-  /** 巡检状态 */
-  status: 'running' | 'completed' | 'failed';
-}
-
-/** 合规汇总报表 */
-export interface ComplianceReport {
-  /** 总群数 */
-  totalRooms: number;
-  /** 达标群数 */
-  compliantRooms: number;
-  /** 待整改群数 */
-  pendingRooms: number;
-  /** 按问题类型分组统计 */
-  issuesByType: Record<ComplianceIssueType, number>;
-  /** 按严重程度分组统计 */
-  issuesBySeverity: Record<number, number>;
-  /** 报表生成时间 */
-  generatedAt: string;
-}

+ 0 - 28
lami-base-v1/backend/src/apps/pc/compliance/routes/compliance.routes.ts

@@ -1,28 +0,0 @@
-/**
- * 合规检查模块 — 路由层
- *
- * 接口一览:
- *   POST /api/compliance/scan         触发合规巡检
- *   GET  /api/compliance/issues       查询合规问题列表
- *   GET  /api/compliance/issues/:id   查询问题详情
- *   PUT  /api/compliance/issues/:id   更新问题状态
- *   GET  /api/compliance/reports      获取合规汇总报表
- */
-
-import { Router } from 'express';
-import type { Router as ExpressRouter } from 'express';
-import {
-  scan,
-  listIssueList,
-  getIssue,
-  updateIssue,
-  report,
-} from '../controllers/compliance.controller.js';
-
-export const pcComplianceApiRouter: ExpressRouter = Router();
-
-pcComplianceApiRouter.post('/compliance/scan', scan);
-pcComplianceApiRouter.get('/compliance/issues', listIssueList);
-pcComplianceApiRouter.get('/compliance/issues/:id', getIssue);
-pcComplianceApiRouter.put('/compliance/issues/:id', updateIssue);
-pcComplianceApiRouter.get('/compliance/reports', report);

+ 0 - 268
lami-base-v1/backend/src/apps/pc/compliance/services/compliance.service.ts

@@ -1,268 +0,0 @@
-/**
- * 合规检查模块 — 业务服务层
- *
- * 对应规范文档 §七「模块 4:沟通记录合规检查模块」
- *
- * 巡检流程(compliance_scan)由以下子任务组成:
- *   §7.1  列出全部外部客户群(调用 room 模块数据)
- *   §7.2  找出还没有沟通记录表的群
- *   §7.3  查群里是否曾经发过记录表
- *   §7.4  查记录表是否已置顶(API-12)
- *   §7.5  查群公告里是否有文档链接(API-08 冗余字段)
- *   §7.7  必填项是否都填了(依赖 §7.6 正文读取)
- *   §7.8  日期、格式与表格版式是否规范
- *   §7.9  记录表是否很久没更新
- *   §7.10 内容写得是否太简略
- *   §7.12 群里聊过但表没更新时提示
- *
- * 定时巡检通过 compliance_scan 统一调度,每次扫描生成一份报告。
- */
-
-import { AppError } from '../../../../shared/errors/app-error.js';
-import type {
-  ComplianceIssue,
-  ComplianceIssueType,
-  IssueStatus,
-  ComplianceScan,
-  ComplianceReport,
-} from '../models/compliance.model.js';
-
-// ---------- 内存存储(TODO: 替换为数据库)----------
-const issueStore = new Map<number, ComplianceIssue>();
-const scanStore = new Map<number, ComplianceScan>();
-let nextIssueId = 1;
-let nextScanId = 1;
-
-// ============================================================
-// 合规巡检任务
-// ============================================================
-
-/**
- * 执行合规巡检扫描
- *
- * 遍历所有外部客户群,逐项检查合规要求,生成问题记录。
- *
- * 本方法模拟巡检流程,实际生产环境应:
- *   1. 从消息表查询是否有链接消息
- *   2. 调用 API-12 检查置顶状态
- *   3. 读取群详情中的公告字段
- *   4. 【占位】读取在线文档正文
- *
- * @param roomIds - 需要巡检的群 ID 列表
- * @returns 巡检结果
- */
-export function runComplianceScan(roomIds: string[]): ComplianceScan {
-  let newIssues = 0;
-  let resolvedIssues = 0;
-
-  for (const roomId of roomIds) {
-    // ---- §7.2:检查是否缺沟通记录表 ----
-    // TODO: 从 room_doc 台账判断是否存在 docId
-    const hasDoc = true; // 示例:默认有文档
-    if (!hasDoc) {
-      createIssue({
-        roomId,
-        roomName: `群-${roomId}`,
-        type: 'missing_doc',
-        description: '该群尚未登记沟通记录在线文档链接',
-        severity: 3,
-      });
-      newIssues++;
-      continue; // 缺表则跳过后续检查
-    }
-
-    // ---- §7.4:检查记录表是否置顶 ----
-    // TODO: 调 API-12 检查
-    const isPinned = true; // 示例
-    if (!isPinned) {
-      createIssue({
-        roomId,
-        roomName: `群-${roomId}`,
-        type: 'not_pinned',
-        description: '沟通记录表在群内未置顶',
-        severity: 2,
-      });
-      newIssues++;
-    }
-
-    // ---- §7.9:检查记录表是否长期未更新 ----
-    // TODO: 比较文档 updated_at 与当前时间的差距
-    const daysSinceUpdate = 5; // 示例
-    if (daysSinceUpdate > 14) {
-      createIssue({
-        roomId,
-        roomName: `群-${roomId}`,
-        type: 'stale_doc',
-        description: `记录表已 ${daysSinceUpdate} 天未更新`,
-        severity: 2,
-      });
-      newIssues++;
-    }
-  }
-
-  // ---- 自动关闭已修复的问题 ----
-  // TODO: 遍历 open 状态的问题,检查条件是否已满足,是则自动 close
-
-  const scan: ComplianceScan = {
-    id: nextScanId++,
-    scannedAt: new Date().toISOString(),
-    totalRooms: roomIds.length,
-    newIssues,
-    resolvedIssues,
-    status: 'completed',
-  };
-
-  scanStore.set(scan.id, scan);
-  return scan;
-}
-
-// ============================================================
-// 合规问题 CRUD
-// ============================================================
-
-/** 创建问题记录的参数 */
-interface CreateIssueParams {
-  roomId: string;
-  roomName: string;
-  type: ComplianceIssueType;
-  description: string;
-  severity: 1 | 2 | 3;
-  assigneeId?: number;
-  docId?: string;
-}
-
-/**
- * 创建合规问题记录
- */
-export function createIssue(params: CreateIssueParams): ComplianceIssue {
-  const now = new Date().toISOString();
-  const issue: ComplianceIssue = {
-    id: nextIssueId++,
-    roomId: params.roomId,
-    roomName: params.roomName,
-    type: params.type,
-    description: params.description,
-    severity: params.severity,
-    status: 'open',
-    assigneeId: params.assigneeId || 0,
-    docId: params.docId || '',
-    foundAt: now,
-    resolvedAt: '',
-    createdAt: now,
-  };
-
-  issueStore.set(issue.id, issue);
-  return issue;
-}
-
-/**
- * 查询合规问题列表
- *
- * @param type   - 问题类型筛选(可选)
- * @param status - 状态筛选(可选)
- * @param severity - 严重程度筛选(可选)
- * @returns 问题列表
- */
-export function listIssues(
-  type?: ComplianceIssueType,
-  status?: IssueStatus,
-  severity?: number,
-): ComplianceIssue[] {
-  let result = Array.from(issueStore.values());
-
-  if (type) result = result.filter((i) => i.type === type);
-  if (status) result = result.filter((i) => i.status === status);
-  if (severity) result = result.filter((i) => i.severity === severity);
-
-  return result.sort(
-    (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
-  );
-}
-
-/**
- * 查询单个问题详情
- */
-export function getIssueById(id: number): ComplianceIssue {
-  const issue = issueStore.get(id);
-  if (!issue) {
-    throw new AppError(404, 'ISSUE_NOT_FOUND', `合规问题 ID=${id} 不存在`);
-  }
-  return issue;
-}
-
-/**
- * 更新问题状态
- *
- * 状态流转:open → in_progress → resolved → closed
- *
- * @param id     - 问题 ID
- * @param status - 新状态
- * @param note   - 处理备注
- */
-export function updateIssueStatus(id: number, status: IssueStatus, note?: string): ComplianceIssue {
-  const issue = getIssueById(id);
-  issue.status = status;
-
-  if (status === 'resolved' || status === 'closed') {
-    issue.resolvedAt = new Date().toISOString();
-  }
-
-  issueStore.set(id, issue);
-
-  // 记录审计日志
-  console.log(`[Compliance] 问题 #${id} 状态变更: ${status}${note ? ` (${note})` : ''}`);
-
-  return issue;
-}
-
-// ============================================================
-// 合规报表(§7.16)
-// ============================================================
-
-/**
- * 生成合规汇总报表
- *
- * 统计达标/待整改/待确认数量与明细。
- * 供工作台首页和主管汇总通知使用。
- *
- * @param totalRooms - 需要纳入统计的总群数
- * @returns 合规报表
- */
-export function generateReport(totalRooms: number): ComplianceReport {
-  const allIssues = Array.from(issueStore.values());
-  const openIssues = allIssues.filter((i) => i.status === 'open' || i.status === 'in_progress');
-
-  // 按问题类型分组计数
-  const issuesByType = {} as Record<ComplianceIssueType, number>;
-  for (const issue of openIssues) {
-    issuesByType[issue.type] = (issuesByType[issue.type] || 0) + 1;
-  }
-
-  // 按严重程度分组计数
-  const issuesBySeverity: Record<number, number> = {};
-  for (const issue of openIssues) {
-    issuesBySeverity[issue.severity] = (issuesBySeverity[issue.severity] || 0) + 1;
-  }
-
-  // 有未解决合规问题的群 ID 集合(去重)
-  const problemRoomIds = new Set(openIssues.map((i) => i.roomId));
-
-  return {
-    totalRooms,
-    compliantRooms: totalRooms - problemRoomIds.size,
-    pendingRooms: problemRoomIds.size,
-    issuesByType,
-    issuesBySeverity,
-    generatedAt: new Date().toISOString(),
-  };
-}
-
-/**
- * 获取最近一次巡检记录
- */
-export function getLastScan(): ComplianceScan | undefined {
-  const scans = Array.from(scanStore.values());
-  return scans.sort(
-    (a, b) => new Date(b.scannedAt).getTime() - new Date(a.scannedAt).getTime(),
-  )[0];
-}

+ 0 - 155
lami-base-v1/backend/src/apps/pc/content/controllers/content.controller.ts

@@ -1,155 +0,0 @@
-/**
- * 社群内容与运营执行模块 — 控制器层
- *
- * 对应规范文档 §九「模块 6:社群内容与运营执行模块」
- */
-
-import type { Request, Response } from 'express';
-import { sendSuccess, sendError } from '../../../../shared/http/response.js';
-import {
-  createMaterial, listMaterials, updateMaterial, deleteMaterial,
-  createBroadcast, listBroadcasts, getBroadcastStatus,
-  createPlan, listPlans, addPlanItem, listPlanItems, checkPlanExecution,
-} from '../services/content.service.js';
-
-// ============================================================
-// 素材库
-// ============================================================
-
-export async function createMat(req: Request, res: Response): Promise<void> {
-  try {
-    const { title, type, content } = req.body;
-    if (!title || !type || !content) {
-      sendError(res, 400, 'VALIDATION_ERROR', '缺少必填参数:title, type, content');
-      return;
-    }
-    const m = createMaterial({
-      title, type, content,
-      tags: req.body.tags || [],
-      creatorId: req.body.creatorId || 0,
-      enabled: 1,
-    });
-    sendSuccess(res, m, 201);
-  } catch (error) { throw error; }
-}
-
-export async function listMat(req: Request, res: Response): Promise<void> {
-  try {
-    const { type, tag } = req.query;
-    const result = listMaterials(
-      typeof type === 'string' ? type : undefined,
-      typeof tag === 'string' ? tag : undefined,
-    );
-    sendSuccess(res, result);
-  } catch (error) { throw error; }
-}
-
-export async function updateMat(req: Request, res: Response): Promise<void> {
-  try {
-    const id = Number(req.params.id);
-    const m = updateMaterial(id, req.body);
-    sendSuccess(res, m);
-  } catch (error) { throw error; }
-}
-
-export async function deleteMat(req: Request, res: Response): Promise<void> {
-  try {
-    const id = Number(req.params.id);
-    deleteMaterial(id);
-    sendSuccess(res, { deleted: true });
-  } catch (error) { throw error; }
-}
-
-// ============================================================
-// 群发任务
-// ============================================================
-
-export async function createBc(req: Request, res: Response): Promise<void> {
-  try {
-    const { guid, sendType, toIdList, msgList } = req.body;
-    if (!guid || sendType === undefined || !toIdList || !msgList) {
-      sendError(res, 400, 'VALIDATION_ERROR', '缺少必填参数:guid, sendType, toIdList, msgList');
-      return;
-    }
-    const task = await createBroadcast(guid, sendType, toIdList, msgList);
-    sendSuccess(res, task, 201);
-  } catch (error) { throw error; }
-}
-
-export async function listBc(_req: Request, res: Response): Promise<void> {
-  try {
-    const result = listBroadcasts();
-    sendSuccess(res, result);
-  } catch (error) { throw error; }
-}
-
-export async function getBcStatus(req: Request, res: Response): Promise<void> {
-  try {
-    const id = Number(req.params.id);
-    const task = getBroadcastStatus(id);
-    sendSuccess(res, task);
-  } catch (error) { throw error; }
-}
-
-// ============================================================
-// 运营计划
-// ============================================================
-
-export async function createPl(req: Request, res: Response): Promise<void> {
-  try {
-    const { name, startDate, endDate, ownerId } = req.body;
-    if (!name || !startDate || !endDate) {
-      sendError(res, 400, 'VALIDATION_ERROR', '缺少必填参数:name, startDate, endDate');
-      return;
-    }
-    const plan = createPlan({
-      name, startDate, endDate,
-      status: 'draft',
-      ownerId: ownerId || 0,
-    });
-    sendSuccess(res, plan, 201);
-  } catch (error) { throw error; }
-}
-
-export async function listPl(req: Request, res: Response): Promise<void> {
-  try {
-    const { status } = req.query;
-    const result = listPlans(typeof status === 'string' ? status : undefined);
-    sendSuccess(res, result);
-  } catch (error) { throw error; }
-}
-
-export async function addItem(req: Request, res: Response): Promise<void> {
-  try {
-    const { planId, content, scheduledDate, targetRoomIds, materialId } = req.body;
-    if (!planId || !content || !scheduledDate) {
-      sendError(res, 400, 'VALIDATION_ERROR', '缺少必填参数:planId, content, scheduledDate');
-      return;
-    }
-    const item = addPlanItem({
-      planId, content, scheduledDate,
-      targetRoomIds: targetRoomIds || [],
-      materialId: materialId || 0,
-      status: 'pending',
-      contentDetected: false,
-      engagementCount: 0,
-    });
-    sendSuccess(res, item, 201);
-  } catch (error) { throw error; }
-}
-
-export async function listItems(req: Request, res: Response): Promise<void> {
-  try {
-    const planId = Number(req.params.planId);
-    const items = listPlanItems(planId);
-    sendSuccess(res, items);
-  } catch (error) { throw error; }
-}
-
-export async function checkExecution(req: Request, res: Response): Promise<void> {
-  try {
-    const planId = Number(req.params.planId);
-    const result = checkPlanExecution(planId);
-    sendSuccess(res, result);
-  } catch (error) { throw error; }
-}

+ 0 - 94
lami-base-v1/backend/src/apps/pc/content/models/content.model.ts

@@ -1,94 +0,0 @@
-/**
- * 社群内容与运营执行模块 — 数据模型定义
- *
- * 对应规范文档 §九「模块 6:社群内容与运营执行模块」
- *
- * 核心表:
- *   - material        :话术与案例素材库
- *   - broadcast_task  :群发任务
- *   - ops_plan        :运营计划
- *   - ops_plan_item   :计划执行项
- */
-
-/** 素材库条目 */
-export interface Material {
-  id: number;
-  /** 标题 */
-  title: string;
-  /** 类型:script=话术, case=案例, image=图片, video=视频, file=文件 */
-  type: 'script' | 'case' | 'image' | 'video' | 'file';
-  /** 正文内容 */
-  content: string;
-  /** 分类标签 */
-  tags: string[];
-  /** 创建人 ID */
-  creatorId: number;
-  /** 状态:1=启用, 0=停用 */
-  enabled: number;
-  createdAt: string;
-  updatedAt: string;
-}
-
-/** 群发任务 */
-export interface BroadcastTask {
-  id: number;
-  /** QiWe 返回的群发任务 ID(groupMsgId) */
-  groupMsgId: string;
-  /** 发送类型:0=外部联系人, 1=外部群 */
-  sendType: 0 | 1;
-  /** 目标群 ID 列表 */
-  toIdList: string[];
-  /** 消息列表 */
-  msgList: Array<{ type: number; msgData: Record<string, unknown> }>;
-  /** 执行人员 guid */
-  guid: string;
-  /** 总发送数 */
-  total: number;
-  /** 已发送数 */
-  hasSend: number;
-  /** 是否完成 */
-  isEnd: boolean;
-  /** 创建人 ID */
-  creatorId: number;
-  createdAt: string;
-  updatedAt: string;
-}
-
-/** 运营计划 */
-export interface OpsPlan {
-  id: number;
-  /** 计划名称 */
-  name: string;
-  /** 计划周期起始 */
-  startDate: string;
-  /** 计划周期截止 */
-  endDate: string;
-  /** 状态:draft=草稿, active=执行中, completed=已完成 */
-  status: 'draft' | 'active' | 'completed';
-  /** 负责人 ID */
-  ownerId: number;
-  createdAt: string;
-  updatedAt: string;
-}
-
-/** 运营计划项 */
-export interface OpsPlanItem {
-  id: number;
-  /** 所属计划 ID */
-  planId: number;
-  /** 内容描述 */
-  content: string;
-  /** 计划执行日期 */
-  scheduledDate: string;
-  /** 目标群范围(群 ID 列表或小区 ID 列表) */
-  targetRoomIds: string[];
-  /** 关联素材 ID */
-  materialId: number;
-  /** 执行状态:pending=待执行, executed=已执行, skipped=已跳过 */
-  status: 'pending' | 'executed' | 'skipped';
-  /** 是否已识别群内相关内容(§9.3) */
-  contentDetected: boolean;
-  /** 互动效果统计(消息数、回复数) */
-  engagementCount: number;
-  createdAt: string;
-}

+ 0 - 48
lami-base-v1/backend/src/apps/pc/content/routes/content.routes.ts

@@ -1,48 +0,0 @@
-/**
- * 社群内容与运营执行模块 — 路由层
- *
- * 接口一览:
- *   素材库:
- *     POST   /api/content/materials          创建素材
- *     GET    /api/content/materials          查询素材列表
- *     PUT    /api/content/materials/:id      更新素材
- *     DELETE /api/content/materials/:id      删除素材
- *   群发:
- *     POST   /api/content/broadcasts         创建群发任务
- *     GET    /api/content/broadcasts         查询群发任务列表
- *     GET    /api/content/broadcasts/:id     查询群发状态
- *   运营计划:
- *     POST   /api/content/plans              创建运营计划
- *     GET    /api/content/plans              查询计划列表
- *     POST   /api/content/plans/:planId/items      添加计划项
- *     GET    /api/content/plans/:planId/items      查询计划项
- *     GET    /api/content/plans/:planId/execution  检查执行情况
- */
-
-import { Router } from 'express';
-import type { Router as ExpressRouter } from 'express';
-import {
-  createMat, listMat, updateMat, deleteMat,
-  createBc, listBc, getBcStatus,
-  createPl, listPl, addItem, listItems, checkExecution,
-} from '../controllers/content.controller.js';
-
-export const pcContentApiRouter: ExpressRouter = Router();
-
-// 素材库
-pcContentApiRouter.post('/content/materials', createMat);
-pcContentApiRouter.get('/content/materials', listMat);
-pcContentApiRouter.put('/content/materials/:id', updateMat);
-pcContentApiRouter.delete('/content/materials/:id', deleteMat);
-
-// 群发
-pcContentApiRouter.post('/content/broadcasts', createBc);
-pcContentApiRouter.get('/content/broadcasts', listBc);
-pcContentApiRouter.get('/content/broadcasts/:id', getBcStatus);
-
-// 运营计划
-pcContentApiRouter.post('/content/plans', createPl);
-pcContentApiRouter.get('/content/plans', listPl);
-pcContentApiRouter.post('/content/plans/:planId/items', addItem);
-pcContentApiRouter.get('/content/plans/:planId/items', listItems);
-pcContentApiRouter.get('/content/plans/:planId/execution', checkExecution);

+ 0 - 246
lami-base-v1/backend/src/apps/pc/content/services/content.service.ts

@@ -1,246 +0,0 @@
-/**
- * 社群内容与运营执行模块 — 业务服务层
- *
- * 对应规范文档 §九「模块 6:社群内容与运营执行模块」
- *
- * 业务流程:
- *   §9.1 话术与案例素材库:CMS 上传/分类/检索话术与案例文件
- *   §9.2 向多个群一键群发:选群与内容 → API-15 创建任务 → 轮询 API-16
- *   §9.3 识别群内是否已发规定运营内容:时间窗内查群发记录 + 群内关键词匹配
- *   §9.4 运营内容互动效果统计:以发布时间为起点统计后续消息量/回复数
- *   §9.5 发布时间建议:按历史群消息聚合活跃小时
- *   §9.6 录入周度运营计划:ops_plan 表录入计划项
- *   §9.7 对照计划检查是否已执行:计划时间窗对比 §9.3 识别结果
- *   §9.8 未完成项提醒:未执行计划项 → API-14 通知责任人
- */
-
-import { AppError } from '../../../../shared/errors/app-error.js';
-import { getStringEnv } from '../../../../shared/config/env.js';
-import { callQiWeApi } from '../../../../shared/qiwei/client.js';
-import type {
-  Material,
-  BroadcastTask,
-  OpsPlan,
-  OpsPlanItem,
-} from '../models/content.model.js';
-
-// ---------- 内存存储(TODO: 替换为数据库)----------
-const materialStore = new Map<number, Material>();
-const broadcastStore = new Map<number, BroadcastTask>();
-const planStore = new Map<number, OpsPlan>();
-const planItemStore = new Map<number, OpsPlanItem>();
-let nextMaterialId = 1;
-let nextBroadcastId = 1;
-let nextPlanId = 1;
-let nextPlanItemId = 1;
-
-// ============================================================
-// 素材库(§9.1)
-// ============================================================
-
-/** 创建素材 */
-export function createMaterial(data: Omit<Material, 'id' | 'createdAt' | 'updatedAt'>): Material {
-  const now = new Date().toISOString();
-  const m: Material = { id: nextMaterialId++, ...data, createdAt: now, updatedAt: now };
-  materialStore.set(m.id, m);
-  return m;
-}
-
-/** 查询素材列表 */
-export function listMaterials(type?: string, tag?: string): Material[] {
-  let result = Array.from(materialStore.values());
-  if (type) result = result.filter((m) => m.type === type);
-  if (tag) result = result.filter((m) => m.tags.includes(tag));
-  return result.filter((m) => m.enabled === 1);
-}
-
-/** 更新素材 */
-export function updateMaterial(id: number, data: Partial<Material>): Material {
-  const m = materialStore.get(id);
-  if (!m) throw new AppError(404, 'MATERIAL_NOT_FOUND', `素材 ID=${id} 不存在`);
-  Object.assign(m, data, { updatedAt: new Date().toISOString() });
-  materialStore.set(id, m);
-  return m;
-}
-
-/** 删除素材 */
-export function deleteMaterial(id: number): void {
-  if (!materialStore.has(id)) throw new AppError(404, 'MATERIAL_NOT_FOUND', `素材 ID=${id} 不存在`);
-  materialStore.delete(id);
-}
-
-// ============================================================
-// 群发任务(§9.2)
-// ============================================================
-
-/**
- * 创建群发任务
- *
- * 步骤:
- *  1. 调用 API-15 /msg/sendGroupMsg 创建群发
- *  2. 获取返回的 groupMsgId
- *  3. 轮询 API-16 /msg/sendGroupMsgStatus 检查进度
- *
- * @param guid      - 执行人员 guid
- * @param sendType   - 发送类型:0=外部联系人, 1=外部群
- * @param toIdList   - 目标 ID 列表
- * @param msgList    - 消息内容列表
- * @returns 群发任务记录
- */
-export async function createBroadcast(
-  guid: string,
-  sendType: 0 | 1,
-  toIdList: string[],
-  msgList: Array<{ type: number; msgData: Record<string, unknown> }>,
-): Promise<BroadcastTask> {
-  const now = new Date().toISOString();
-  const live = getStringEnv('QIWEI_LIVE_ALLOW_BROADCAST') === '1';
-
-  let groupMsgId = `dry-run-${Date.now()}`;
-
-  if (live) {
-    const data = await callQiWeApi<{ groupMsgId: string }>('/msg/sendGroupMsg', {
-      guid,
-      sendType,
-      toIdList,
-      msgList,
-    });
-    groupMsgId = data.groupMsgId;
-  }
-
-  const task: BroadcastTask = {
-    id: nextBroadcastId++,
-    groupMsgId,
-    sendType,
-    toIdList,
-    msgList,
-    guid,
-    total: toIdList.length,
-    hasSend: 0,
-    isEnd: false,
-    creatorId: 0,
-    createdAt: now,
-    updatedAt: now,
-  };
-
-  broadcastStore.set(task.id, task);
-
-  if (live) {
-    pollBroadcastStatus(task).catch((err) =>
-      console.error(`[Content] 群发状态轮询失败: taskId=${task.id}`, err),
-    );
-  }
-
-  return task;
-}
-
-/**
- * 轮询群发状态(内部方法)
- */
-async function pollBroadcastStatus(task: BroadcastTask): Promise<void> {
-  // 简化的单次查询(生产环境应使用定时任务轮询)
-  const statusData = await callQiWeApi<{
-    hasSend: number;
-    isEnd: boolean;
-    total: number;
-  }>('/msg/sendGroupMsgStatus', {
-    guid: task.guid,
-    groupMsgId: task.groupMsgId,
-    endDetailId: 2,
-  });
-
-  task.hasSend = statusData.hasSend;
-  task.isEnd = statusData.isEnd;
-  task.total = statusData.total;
-  task.updatedAt = new Date().toISOString();
-  broadcastStore.set(task.id, task);
-}
-
-/** 查询群发任务列表 */
-export function listBroadcasts(): BroadcastTask[] {
-  return Array.from(broadcastStore.values()).sort(
-    (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
-  );
-}
-
-/** 查询群发任务状态 */
-export function getBroadcastStatus(id: number): BroadcastTask {
-  const task = broadcastStore.get(id);
-  if (!task) throw new AppError(404, 'BROADCAST_NOT_FOUND', `群发任务 ID=${id} 不存在`);
-  return task;
-}
-
-// ============================================================
-// 运营计划(§9.6 ~ §9.7)
-// ============================================================
-
-/** 创建运营计划 */
-export function createPlan(data: Omit<OpsPlan, 'id' | 'createdAt' | 'updatedAt'>): OpsPlan {
-  const now = new Date().toISOString();
-  const plan: OpsPlan = { id: nextPlanId++, ...data, createdAt: now, updatedAt: now };
-  planStore.set(plan.id, plan);
-  return plan;
-}
-
-/** 查询运营计划列表 */
-export function listPlans(status?: string): OpsPlan[] {
-  let result = Array.from(planStore.values());
-  if (status) result = result.filter((p) => p.status === status);
-  return result.sort((a, b) => new Date(b.startDate).getTime() - new Date(a.startDate).getTime());
-}
-
-/** 添加计划项 */
-export function addPlanItem(data: Omit<OpsPlanItem, 'id' | 'createdAt'>): OpsPlanItem {
-  const plan = planStore.get(data.planId);
-  if (!plan) throw new AppError(404, 'PLAN_NOT_FOUND', `计划 ID=${data.planId} 不存在`);
-
-  const item: OpsPlanItem = {
-    id: nextPlanItemId++,
-    ...data,
-    createdAt: new Date().toISOString(),
-  };
-  planItemStore.set(item.id, item);
-  return item;
-}
-
-/** 查询计划项列表 */
-export function listPlanItems(planId: number): OpsPlanItem[] {
-  return Array.from(planItemStore.values())
-    .filter((item) => item.planId === planId)
-    .sort((a, b) => a.scheduledDate.localeCompare(b.scheduledDate));
-}
-
-/**
- * 检查计划执行情况(§9.7)
- *
- * 对计划项的时间窗和群发记录做对比,更新执行状态。
- *
- * @param planId - 计划 ID
- * @returns 未执行的项目列表(供 §9.8 提醒使用)
- */
-export function checkPlanExecution(planId: number): { unexecuted: OpsPlanItem[]; executed: OpsPlanItem[] } {
-  const items = listPlanItems(planId);
-  const now = new Date();
-
-  const unexecuted: OpsPlanItem[] = [];
-  const executed: OpsPlanItem[] = [];
-
-  for (const item of items) {
-    if (item.status === 'skipped') continue;
-
-    // 判断是否已过计划日期
-    const scheduled = new Date(item.scheduledDate);
-    if (scheduled < now && item.status === 'pending') {
-      // 检查是否有群发记录(TODO: 实际查询 broadcast 记录)
-      if (item.contentDetected) {
-        item.status = 'executed';
-        planItemStore.set(item.id, item);
-        executed.push(item);
-      } else {
-        unexecuted.push(item);
-      }
-    }
-  }
-
-  return { unexecuted, executed };
-}

+ 0 - 90
lami-base-v1/backend/src/apps/pc/dashboard/controllers/dashboard.controller.ts

@@ -1,90 +0,0 @@
-/**
- * 数据看板与经营复盘模块 — 控制器层
- *
- * 对应规范文档 §十一「模块 8:数据看板与经营复盘模块」
- */
-
-import type { Request, Response } from 'express';
-import { sendSuccess } from '../../../../shared/http/response.js';
-import {
-  getOverviewDashboard,
-  getCommunityDashboard,
-  getStoreDashboard,
-  getActivityIndex,
-  getActivityRanking,
-  generatePeriodicReport,
-} from '../services/dashboard.service.js';
-
-/** GET /api/dashboard/overview — 总览看板(§11.1) */
-export async function overview(_req: Request, res: Response): Promise<void> {
-  try {
-    const data = getOverviewDashboard();
-    sendSuccess(res, data);
-  } catch (error) {
-    throw error;
-  }
-}
-
-/** GET /api/dashboard/community/:communityId — 小区看板(§11.2) */
-export async function communityDashboard(req: Request, res: Response): Promise<void> {
-  try {
-    const communityId = Number(req.params.communityId);
-    const data = getCommunityDashboard(communityId);
-    sendSuccess(res, data);
-  } catch (error) {
-    throw error;
-  }
-}
-
-/** GET /api/dashboard/store/:storeId — 门店看板(§11.3) */
-export async function storeDashboard(req: Request, res: Response): Promise<void> {
-  try {
-    const storeId = Number(req.params.storeId);
-    const data = getStoreDashboard(storeId);
-    sendSuccess(res, data);
-  } catch (error) {
-    throw error;
-  }
-}
-
-/** GET /api/dashboard/activity/:roomId — 群活跃度指数(§11.4) */
-export async function activityIndex(req: Request, res: Response): Promise<void> {
-  try {
-    const roomId = String(req.params.roomId);
-    const data = getActivityIndex(roomId);
-    sendSuccess(res, data);
-  } catch (error) {
-    throw error;
-  }
-}
-
-/** GET /api/dashboard/activity-ranking — 群活跃度排行榜 */
-export async function activityRanking(req: Request, res: Response): Promise<void> {
-  try {
-    const limit = typeof req.query.limit === 'string' ? Number(req.query.limit) : 20;
-    const data = getActivityRanking(limit);
-    sendSuccess(res, data);
-  } catch (error) {
-    throw error;
-  }
-}
-
-/** GET /api/dashboard/reports/weekly — 周报(§11.9) */
-export async function weeklyReport(_req: Request, res: Response): Promise<void> {
-  try {
-    const data = generatePeriodicReport('weekly');
-    sendSuccess(res, data);
-  } catch (error) {
-    throw error;
-  }
-}
-
-/** GET /api/dashboard/reports/monthly — 月报(§11.9) */
-export async function monthlyReport(_req: Request, res: Response): Promise<void> {
-  try {
-    const data = generatePeriodicReport('monthly');
-    sendSuccess(res, data);
-  } catch (error) {
-    throw error;
-  }
-}

+ 0 - 34
lami-base-v1/backend/src/apps/pc/dashboard/routes/dashboard.routes.ts

@@ -1,34 +0,0 @@
-/**
- * 数据看板模块 — 路由层
- *
- * 接口一览:
- *   GET /api/dashboard/overview                  总览看板
- *   GET /api/dashboard/community/:communityId    小区看板
- *   GET /api/dashboard/store/:storeId           门店看板
- *   GET /api/dashboard/activity/:roomId         群活跃度
- *   GET /api/dashboard/activity-ranking         活跃度排行榜
- *   GET /api/dashboard/reports/weekly           周报
- *   GET /api/dashboard/reports/monthly          月报
- */
-
-import { Router } from 'express';
-import type { Router as ExpressRouter } from 'express';
-import {
-  overview,
-  communityDashboard,
-  storeDashboard,
-  activityIndex,
-  activityRanking,
-  weeklyReport,
-  monthlyReport,
-} from '../controllers/dashboard.controller.js';
-
-export const pcDashboardApiRouter: ExpressRouter = Router();
-
-pcDashboardApiRouter.get('/dashboard/overview', overview);
-pcDashboardApiRouter.get('/dashboard/community/:communityId', communityDashboard);
-pcDashboardApiRouter.get('/dashboard/store/:storeId', storeDashboard);
-pcDashboardApiRouter.get('/dashboard/activity/:roomId', activityIndex);
-pcDashboardApiRouter.get('/dashboard/activity-ranking', activityRanking);
-pcDashboardApiRouter.get('/dashboard/reports/weekly', weeklyReport);
-pcDashboardApiRouter.get('/dashboard/reports/monthly', monthlyReport);

+ 0 - 229
lami-base-v1/backend/src/apps/pc/dashboard/services/dashboard.service.ts

@@ -1,229 +0,0 @@
-/**
- * 数据看板与经营复盘模块 — 业务服务层
- *
- * 对应规范文档 §十一「模块 8:数据看板与经营复盘模块」
- *
- * 说明:无新增 QiWe 接口,所有数据来源于已同步的群、消息、合规、风控等模块聚合。
- *
- * 包含的看板:
- *   §11.1 总览看板:群数、人数、消息活跃、合规/预警计数
- *   §11.2 小区看板:按小区聚合各指标
- *   §11.3 门店看板:按门店聚合多小区指标、排名
- *   §11.4 群活跃度指数:消息量、UV 公式计算
- *   §11.5 小区触达率:群人数 / 小区总户数
- *   §11.6 图表下钻:支持 communityId → roomId → staff 参数
- *   §11.7 添加微信与链接率:依赖 §12.1 填报数据
- *   §11.8 签单与转化率:【占位】CRM 对接
- *   §11.9 周月报自动汇总
- */
-
-import type { PageResult } from '../../../../shared/types/page.js';
-import { makePageResult } from '../../../../shared/types/page.js';
-
-/** 总览看板数据 */
-export interface OverviewDashboard {
-  /** 总群数 */
-  totalRooms: number;
-  /** 总群成员数 */
-  totalMembers: number;
-  /** 近 7 天消息量 */
-  messageCount7d: number;
-  /** 活跃群数(7 天内有消息的群) */
-  activeRooms: number;
-  /** 合规达标率 */
-  complianceRate: number;
-  /** 未处理预警数 */
-  openAlerts: number;
-  /** 未处理工单数 */
-  openWorkOrders: number;
-  /** 生成时间 */
-  generatedAt: string;
-}
-
-/** 小区看板数据 */
-export interface CommunityDashboard {
-  communityId: number;
-  communityName: string;
-  totalHouseholds: number;
-  /** 群人数之和 */
-  groupMemberCount: number;
-  /** 触达率 = groupMemberCount / totalHouseholds */
-  reachRate: number;
-  /** 消息活跃度 */
-  messageCount7d: number;
-  /** 合规问题数 */
-  complianceIssues: number;
-}
-
-/** 门店看板数据 */
-export interface StoreDashboard {
-  storeId: number;
-  storeName: string;
-  /** 下属小区数 */
-  communityCount: number;
-  /** 下属群数 */
-  roomCount: number;
-  /** 总触达率 */
-  reachRate: number;
-  /** 合规排名 */
-  complianceRank: number;
-}
-
-/** 群活跃度指数 */
-export interface ActivityIndex {
-  roomId: string;
-  roomName: string;
-  /** 活跃度指数(0-100) */
-  score: number;
-  /** 消息量 */
-  messageCount: number;
-  /** 参与人数(UV) */
-  uniqueUsers: number;
-  period: string;
-}
-
-/** 周/月报 */
-export interface PeriodicReport {
-  title: string;
-  period: string;
-  /** 群与活跃概览 */
-  overview: OverviewDashboard;
-  /** 合规概况 */
-  complianceSummary: {
-    total: number;
-    resolved: number;
-    pending: number;
-  };
-  /** 风控概况 */
-  riskSummary: {
-    totalAlerts: number;
-    handledAlerts: number;
-  };
-  /** 转化数据(依赖填报) */
-  conversionSummary: {
-    newLeads: number;
-    orders: number;
-    note: string; // 缺失数据标注
-  };
-  generatedAt: string;
-}
-
-/**
- * 生成总览看板(§11.1)
- *
- * 聚合群数、人数、消息活跃度、合规/预警计数。
- */
-export function getOverviewDashboard(): OverviewDashboard {
-  // TODO: 从各模块聚合真实数据
-  return {
-    totalRooms: 156,
-    totalMembers: 2340,
-    messageCount7d: 8920,
-    activeRooms: 142,
-    complianceRate: 0.85,
-    openAlerts: 3,
-    openWorkOrders: 2,
-    generatedAt: new Date().toISOString(),
-  };
-}
-
-/**
- * 查询小区看板(§11.2)
- *
- * 按 communityId 聚合群数据、触达率、合规情况。
- */
-export function getCommunityDashboard(communityId: number): CommunityDashboard {
-  // TODO: 从 community + room + compliance 模块聚合
-  return {
-    communityId,
-    communityName: `小区-${communityId}`,
-    totalHouseholds: 1200,
-    groupMemberCount: 380,
-    reachRate: 0.32,
-    messageCount7d: 560,
-    complianceIssues: 1,
-  };
-}
-
-/**
- * 查询门店看板(§11.3)
- *
- * 按门店聚合多小区指标、排名。
- */
-export function getStoreDashboard(storeId: number): StoreDashboard {
-  // TODO: 按门店聚合社区看板数据
-  return {
-    storeId,
-    storeName: `门店-${storeId}`,
-    communityCount: 5,
-    roomCount: 12,
-    reachRate: 0.35,
-    complianceRank: 2,
-  };
-}
-
-/**
- * 计算群活跃度指数(§11.4)
- *
- * 基于消息量 + UV 综合加权计算(公式可调)。
- * score = min(100, messageCount/100 * 0.6 + uniqueUsers/5 * 0.4)
- */
-export function getActivityIndex(roomId: string): ActivityIndex {
-  // TODO: 从 message 表统计实际数据
-  const messageCount = Math.floor(Math.random() * 200);
-  const uniqueUsers = Math.floor(Math.random() * 30);
-  const score = Math.min(100, (messageCount / 100) * 60 + (uniqueUsers / 5) * 40);
-
-  return {
-    roomId,
-    roomName: `群-${roomId}`,
-    score: Math.round(score),
-    messageCount,
-    uniqueUsers,
-    period: '7d',
-  };
-}
-
-/**
- * 批量查询群活跃度排行榜
- */
-export function getActivityRanking(limit = 20): ActivityIndex[] {
-  // TODO: 从 room 模块获取所有 roomId,逐个计算活跃度
-  // 这里返回示例数据
-  const mockRoomIds = ['room-001', 'room-002', 'room-003', 'room-004', 'room-005'];
-  return mockRoomIds
-    .map((roomId) => getActivityIndex(roomId))
-    .sort((a, b) => b.score - a.score)
-    .slice(0, limit);
-}
-
-/**
- * 生成周报/月报(§11.9)
- *
- * 自动汇总群/活跃/合规/预警块。
- * 转化数据依赖手工填报(§12.2),缺失则标注「未录入」。
- */
-export function generatePeriodicReport(period: 'weekly' | 'monthly'): PeriodicReport {
-  const overview = getOverviewDashboard();
-
-  return {
-    title: period === 'weekly' ? '周报' : '月报',
-    period: new Date().toISOString().slice(0, 7),
-    overview,
-    complianceSummary: {
-      total: 12,
-      resolved: 10,
-      pending: 2,
-    },
-    riskSummary: {
-      totalAlerts: 5,
-      handledAlerts: 4,
-    },
-    conversionSummary: {
-      newLeads: 0,
-      orders: 0,
-      note: '转化数据待填报(§12.2),当前未录入',
-    },
-    generatedAt: new Date().toISOString(),
-  };
-}

+ 0 - 27
lami-base-v1/backend/src/apps/pc/health/controllers/health.controller.ts

@@ -1,27 +0,0 @@
-import type { Request, Response } from 'express';
-
-/**
- * GET /api/health — 健康检查
- *
- * 返回服务运行状态和已加载的模块列表,供监控和前端探活使用。
- */
-export function getHealth(_req: Request, res: Response): void {
-  res.json({
-    status: 'ok',
-    platform: 'pc',
-    timestamp: new Date().toISOString(),
-    modules: [
-      'qiwei',        // 企微 API 代理 [模块1]
-      'staff',        // 人员管理 [模块1]
-      'community',    // 小区管理 [模块2]
-      'room',         // 群管理 [模块2]
-      'compliance',   // 合规检查 [模块4]
-      'risk',         // 群风控 [模块5]
-      'content',      // 内容运营 [模块6]
-      'koc',          // KOC与意向 [模块7]
-      'dashboard',    // 数据看板 [模块8]
-      'sales',        // 经营数据 [模块9]
-      'workbench',    // 工作台 [模块10]
-    ],
-  });
-}

+ 0 - 8
lami-base-v1/backend/src/apps/pc/health/routes/health.routes.ts

@@ -1,8 +0,0 @@
-import { Router } from 'express';
-import type { Router as ExpressRouter } from 'express';
-import { getHealth } from '../controllers/health.controller.js';
-
-export const pcHealthApiRouter: ExpressRouter = Router();
-
-pcHealthApiRouter.get('/health', getHealth);
-

+ 0 - 34
lami-base-v1/backend/src/apps/pc/health/server.ts

@@ -1,34 +0,0 @@
-/**
- * PC 端统一入口 — 端口监听层
- *
- * 创建唯一的 PC Express App(包含所有模块路由),监听单端口。
- * 当前阶段所有 PC 模块共用一个进程,独立部署时可将模块 server.ts 拆出去。
- */
-
-import { createPcApp } from '../app.js';
-import { getNumberEnv } from '../../../shared/config/env.js';
-
-export function startPcServer(): void {
-  const app = createPcApp();
-  const port = getNumberEnv('PC_PORT', 3101);
-
-  app.listen(port, () => {
-    console.log('──────────────────────────────────────────');
-    console.log(`  PC 端服务已启动 → http://localhost:${port}`);
-    console.log(`  接口测试台 (Swagger UI) → http://localhost:${port}/api-docs`);
-    console.log('  已加载模块:');
-    console.log('    /api/health            健康检查');
-    console.log('    /api/qiwei             企微 API 代理 [模块1]');
-    console.log('    /api/staff             人员管理 [模块1]');
-    console.log('    /api/communities       小区管理 [模块2]');
-    console.log('    /api/rooms             群管理 [模块2]');
-    console.log('    /api/compliance        合规检查 [模块4]');
-    console.log('    /api/risk              群风控 [模块5]');
-    console.log('    /api/content           内容运营 [模块6]');
-    console.log('    /api/koc               KOC与意向 [模块7]');
-    console.log('    /api/dashboard         数据看板 [模块8]');
-    console.log('    /api/sales             经营数据 [模块9]');
-    console.log('    /api/workbench         工作台 [模块10]');
-    console.log('──────────────────────────────────────────');
-  });
-}

+ 0 - 132
lami-base-v1/backend/src/apps/pc/koc/controllers/koc.controller.ts

@@ -1,132 +0,0 @@
-/**
- * 拉群、KOC 与意向客户模块 — 控制器层
- *
- * 对应规范文档 §十「模块 7:拉群、KOC 与意向客户模块」
- */
-
-import type { Request, Response } from 'express';
-import { sendSuccess, sendError } from '../../../../shared/http/response.js';
-import {
-  createChannel, listChannels, updateChannel, deleteChannel,
-  syncContacts, listContacts, getContact,
-  findKocCandidates, labelKoc,
-  detectIntent, listLeads, updateLead, listTasks,
-} from '../services/koc.service.js';
-
-// ============================================================
-// 渠道管理
-// ============================================================
-
-export async function createCh(req: Request, res: Response): Promise<void> {
-  try {
-    const { name, code, type } = req.body;
-    if (!name || !code || !type) {
-      sendError(res, 400, 'VALIDATION_ERROR', '缺少必填参数:name, code, type');
-      return;
-    }
-    const ch = createChannel({ name, code, type, remark: req.body.remark || '' });
-    sendSuccess(res, ch, 201);
-  } catch (error) { throw error; }
-}
-
-export async function listCh(_req: Request, res: Response): Promise<void> {
-  try { sendSuccess(res, listChannels()); } catch (error) { throw error; }
-}
-
-export async function updateCh(req: Request, res: Response): Promise<void> {
-  try {
-    const id = Number(req.params.id);
-    sendSuccess(res, updateChannel(id, req.body));
-  } catch (error) { throw error; }
-}
-
-export async function deleteCh(req: Request, res: Response): Promise<void> {
-  try {
-    deleteChannel(Number(req.params.id));
-    sendSuccess(res, { deleted: true });
-  } catch (error) { throw error; }
-}
-
-// ============================================================
-// 外部联系人
-// ============================================================
-
-export async function syncCt(req: Request, res: Response): Promise<void> {
-  try {
-    const { guid, currentSeq, limit } = req.body;
-    if (!guid) { sendError(res, 400, 'VALIDATION_ERROR', '缺少 guid 参数'); return; }
-    const result = await syncContacts(guid, currentSeq || 0, limit || 50);
-    sendSuccess(res, result);
-  } catch (error) { throw error; }
-}
-
-export async function listCt(_req: Request, res: Response): Promise<void> {
-  try { sendSuccess(res, listContacts()); } catch (error) { throw error; }
-}
-
-export async function getCt(req: Request, res: Response): Promise<void> {
-  try { sendSuccess(res, getContact(String(req.params.userId))); } catch (error) { throw error; }
-}
-
-// ============================================================
-// KOC
-// ============================================================
-
-export async function kocCandidates(_req: Request, res: Response): Promise<void> {
-  try { sendSuccess(res, findKocCandidates()); } catch (error) { throw error; }
-}
-
-export async function labelKocUser(req: Request, res: Response): Promise<void> {
-  try {
-    const { userId, guid, labelId } = req.body;
-    if (!userId || !guid || !labelId) {
-      sendError(res, 400, 'VALIDATION_ERROR', '缺少必填参数:userId, guid, labelId');
-      return;
-    }
-    const result = await labelKoc(userId, guid, labelId);
-    sendSuccess(res, result);
-  } catch (error) { throw error; }
-}
-
-// ============================================================
-// 意向客户
-// ============================================================
-
-export async function detectIntentMsg(req: Request, res: Response): Promise<void> {
-  try {
-    const { userId, roomId, content } = req.body;
-    if (!userId || !roomId || !content) {
-      sendError(res, 400, 'VALIDATION_ERROR', '缺少必填参数:userId, roomId, content');
-      return;
-    }
-    const lead = detectIntent(userId, roomId, content);
-    if (!lead) {
-      sendSuccess(res, { matched: false, lead: null });
-      return;
-    }
-    sendSuccess(res, { matched: true, lead });
-  } catch (error) { throw error; }
-}
-
-export async function listLd(req: Request, res: Response): Promise<void> {
-  try {
-    const { intentLevel, status } = req.query;
-    sendSuccess(res, listLeads(
-      typeof intentLevel === 'string' ? intentLevel : undefined,
-      typeof status === 'string' ? status : undefined,
-    ));
-  } catch (error) { throw error; }
-}
-
-export async function updateLd(req: Request, res: Response): Promise<void> {
-  try {
-    sendSuccess(res, updateLead(Number(req.params.id), req.body));
-  } catch (error) { throw error; }
-}
-
-export async function listTk(req: Request, res: Response): Promise<void> {
-  try {
-    const { assigneeId } = req.query;
-    sendSuccess(res, listTasks(typeof assigneeId === 'string' ? Number(assigneeId) : undefined));
-  } catch (error) { throw error; }
-}

+ 0 - 88
lami-base-v1/backend/src/apps/pc/koc/models/koc.model.ts

@@ -1,88 +0,0 @@
-/**
- * 拉群、KOC 与意向客户模块 — 数据模型定义
- *
- * 对应规范文档 §十「模块 7:拉群、KOC 与意向客户模块」
- *
- * 核心表:
- *   - channel         :拉群渠道
- *   - external_contact:外部联系人
- *   - koc_candidate   :KOC 候选人
- *   - intent_lead     :意向客户
- *   - intent_task     :跟进待办
- */
-
-/** 拉群渠道 */
-export interface Channel {
-  id: number;
-  name: string;
-  code: string;
-  /** 渠道类型:online=线上, offline=线下, referral=转介绍 */
-  type: 'online' | 'offline' | 'referral';
-  remark: string;
-  createdAt: string;
-}
-
-/** 外部联系人 */
-export interface ExternalContact {
-  userId: string;
-  nickname: string;
-  remark: string;
-  mobile: string;
-  avatarUrl: string;
-  /** KOC 标签 ID 列表 */
-  labelIds: string[];
-  /** 所属群 ID */
-  roomId: string;
-  /** 添加时间 */
-  addTime: string;
-}
-
-/** KOC 候选人 */
-export interface KocCandidate {
-  userId: string;
-  nickname: string;
-  /** 发言次数 */
-  messageCount: number;
-  /** 互动指数 */
-  engagementScore: number;
-  /** 推荐理由 */
-  reason: string;
-  /** 状态:candidate=候选, confirmed=已确认, labelled=已打标 */
-  status: 'candidate' | 'confirmed' | 'labelled';
-  evaluatedAt: string;
-}
-
-/** 意向客户 */
-export interface IntentLead {
-  id: number;
-  /** 用户 ID */
-  userId: string;
-  /** 所属群 ID */
-  roomId: string;
-  /** 意向等级:high=高, medium=中, low=低 */
-  intentLevel: 'high' | 'medium' | 'low';
-  /** 匹配关键词 */
-  matchedKeywords: string[];
-  /** 匹配的消息内容摘要 */
-  messageSummary: string;
-  /** 状态 */
-  status: 'new' | 'assigned' | 'contacted' | 'converted' | 'closed';
-  /** 指定跟进人 ID */
-  assigneeId: number;
-  /** 跟进待办 ID */
-  taskId: number;
-  createdAt: string;
-  updatedAt: string;
-}
-
-/** 跟进待办 */
-export interface IntentTask {
-  id: number;
-  leadId: number;
-  title: string;
-  description: string;
-  assigneeId: number;
-  deadline: string;
-  status: 'pending' | 'completed';
-  createdAt: string;
-}

+ 0 - 54
lami-base-v1/backend/src/apps/pc/koc/routes/koc.routes.ts

@@ -1,54 +0,0 @@
-/**
- * 拉群、KOC 与意向客户模块 — 路由层
- *
- * 接口一览:
- *   渠道:
- *     POST   /api/channels              创建渠道
- *     GET    /api/channels              查询渠道列表
- *     PUT    /api/channels/:id          更新渠道
- *     DELETE /api/channels/:id          删除渠道
- *   联系人:
- *     POST   /api/contacts/sync         同步外部联系人
- *     GET    /api/contacts              查询联系人列表
- *     GET    /api/contacts/:userId      查询联系人详情
- *   KOC:
- *     GET    /api/koc/candidates        KOC 候选人列表
- *     POST   /api/koc/label             打 KOC 标签
- *   意向客户:
- *     POST   /api/intent-leads/detect   识别意向话术
- *     GET    /api/intent-leads          查询意向客户列表
- *     PUT    /api/intent-leads/:id      更新意向客户
- *     GET    /api/intent-tasks          查询跟进待办
- */
-
-import { Router } from 'express';
-import type { Router as ExpressRouter } from 'express';
-import {
-  createCh, listCh, updateCh, deleteCh,
-  syncCt, listCt, getCt,
-  kocCandidates, labelKocUser,
-  detectIntentMsg, listLd, updateLd, listTk,
-} from '../controllers/koc.controller.js';
-
-export const pcKocApiRouter: ExpressRouter = Router();
-
-// 渠道
-pcKocApiRouter.post('/channels', createCh);
-pcKocApiRouter.get('/channels', listCh);
-pcKocApiRouter.put('/channels/:id', updateCh);
-pcKocApiRouter.delete('/channels/:id', deleteCh);
-
-// 联系人
-pcKocApiRouter.post('/contacts/sync', syncCt);
-pcKocApiRouter.get('/contacts', listCt);
-pcKocApiRouter.get('/contacts/:userId', getCt);
-
-// KOC
-pcKocApiRouter.get('/koc/candidates', kocCandidates);
-pcKocApiRouter.post('/koc/label', labelKocUser);
-
-// 意向客户
-pcKocApiRouter.post('/intent-leads/detect', detectIntentMsg);
-pcKocApiRouter.get('/intent-leads', listLd);
-pcKocApiRouter.put('/intent-leads/:id', updateLd);
-pcKocApiRouter.get('/intent-tasks', listTk);

Некоторые файлы не были показаны из-за большого количества измененных файлов