@sfmc-bds/db-server 0.2.0-beta.8 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +1 -1
  2. package/dist/domain/bridge.d.ts +1 -5
  3. package/dist/domain/bridge.d.ts.map +1 -1
  4. package/dist/domain/bridge.js +15 -14
  5. package/dist/domain/bridge.js.map +2 -2
  6. package/dist/domain/installed-content.d.ts +84 -0
  7. package/dist/domain/installed-content.d.ts.map +1 -0
  8. package/dist/domain/installed-content.js +149 -0
  9. package/dist/domain/installed-content.js.map +7 -0
  10. package/dist/domain/player-session.d.ts +8 -0
  11. package/dist/domain/player-session.d.ts.map +1 -0
  12. package/dist/domain/player-session.js +16 -0
  13. package/dist/domain/player-session.js.map +7 -0
  14. package/dist/domain/qq-account-profile.d.ts +51 -0
  15. package/dist/domain/qq-account-profile.d.ts.map +1 -0
  16. package/dist/domain/qq-account-profile.js +129 -0
  17. package/dist/domain/qq-account-profile.js.map +7 -0
  18. package/dist/domain/qq-events.d.ts +2 -2
  19. package/dist/domain/qq-events.d.ts.map +1 -1
  20. package/dist/domain/qq-events.js +6 -3
  21. package/dist/domain/qq-events.js.map +2 -2
  22. package/dist/domain/schema.d.ts.map +1 -1
  23. package/dist/domain/schema.js +48 -0
  24. package/dist/domain/schema.js.map +2 -2
  25. package/dist/domain/system-status.d.ts +3 -0
  26. package/dist/domain/system-status.d.ts.map +1 -1
  27. package/dist/domain/system-status.js +27 -12
  28. package/dist/domain/system-status.js.map +2 -2
  29. package/dist/env.d.ts +0 -2
  30. package/dist/env.d.ts.map +1 -1
  31. package/dist/env.js +12 -24
  32. package/dist/env.js.map +2 -2
  33. package/dist/index.d.ts +1 -1
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +108 -52
  36. package/dist/index.js.map +3 -3
  37. package/dist/routes/config.d.ts +1 -1
  38. package/dist/routes/config.d.ts.map +1 -1
  39. package/dist/routes/config.js +0 -4
  40. package/dist/routes/config.js.map +2 -2
  41. package/dist/routes/content.d.ts +23 -0
  42. package/dist/routes/content.d.ts.map +1 -0
  43. package/dist/routes/content.js +19 -0
  44. package/dist/routes/content.js.map +7 -0
  45. package/dist/routes/db-routes.d.ts +2 -2
  46. package/dist/routes/db-routes.d.ts.map +1 -1
  47. package/dist/routes/db-routes.js +3 -12
  48. package/dist/routes/db-routes.js.map +2 -2
  49. package/dist/routes/messages.d.ts +14 -6
  50. package/dist/routes/messages.d.ts.map +1 -1
  51. package/dist/routes/messages.js +22 -8
  52. package/dist/routes/messages.js.map +2 -2
  53. package/dist/routes/qq-bind.d.ts.map +1 -1
  54. package/dist/routes/qq-bind.js +29 -1
  55. package/dist/routes/qq-bind.js.map +2 -2
  56. package/dist/routes/qq-events.d.ts +6 -2
  57. package/dist/routes/qq-events.d.ts.map +1 -1
  58. package/dist/routes/qq-events.js +35 -1
  59. package/dist/routes/qq-events.js.map +3 -3
  60. package/dist/routes/qq-join.js +1 -1
  61. package/dist/routes/qq-join.js.map +2 -2
  62. package/dist/routes/status.d.ts +3 -11
  63. package/dist/routes/status.d.ts.map +1 -1
  64. package/dist/routes/status.js +39 -49
  65. package/dist/routes/status.js.map +2 -2
  66. package/dist/tx-runner.d.ts +20 -7
  67. package/dist/tx-runner.d.ts.map +1 -1
  68. package/dist/tx-runner.js +71 -25
  69. package/dist/tx-runner.js.map +2 -2
  70. package/package.json +4 -5
  71. package/dist/domain/bridge.test.d.ts +0 -2
  72. package/dist/domain/bridge.test.d.ts.map +0 -1
  73. package/dist/domain/bridge.test.js +0 -87
  74. package/dist/domain/bridge.test.js.map +0 -7
  75. package/dist/domain/messages-bridge-filter.test.d.ts +0 -5
  76. package/dist/domain/messages-bridge-filter.test.d.ts.map +0 -1
  77. package/dist/domain/messages-bridge-filter.test.js +0 -20
  78. package/dist/domain/messages-bridge-filter.test.js.map +0 -7
  79. package/dist/domain/qq-bind-status.test.d.ts +0 -2
  80. package/dist/domain/qq-bind-status.test.d.ts.map +0 -1
  81. package/dist/domain/qq-bind-status.test.js +0 -211
  82. package/dist/domain/qq-bind-status.test.js.map +0 -7
  83. package/dist/domain/qq-events.test.d.ts +0 -5
  84. package/dist/domain/qq-events.test.d.ts.map +0 -1
  85. package/dist/domain/qq-events.test.js +0 -170
  86. package/dist/domain/qq-events.test.js.map +0 -7
  87. package/dist/domain/qq-join.test.d.ts +0 -2
  88. package/dist/domain/qq-join.test.d.ts.map +0 -1
  89. package/dist/domain/qq-join.test.js +0 -353
  90. package/dist/domain/qq-join.test.js.map +0 -7
  91. package/dist/domain/system-status.test.d.ts +0 -5
  92. package/dist/domain/system-status.test.d.ts.map +0 -1
  93. package/dist/domain/system-status.test.js +0 -19
  94. package/dist/domain/system-status.test.js.map +0 -7
  95. package/dist/runtime.test.d.ts +0 -2
  96. package/dist/runtime.test.d.ts.map +0 -1
  97. package/dist/runtime.test.js +0 -265
  98. package/dist/runtime.test.js.map +0 -7
@@ -1,14 +1,12 @@
1
1
  import { SQL } from "sql-template-strings";
2
- function shouldForwardChatToQQ(bridgeChannelId, channelId, fromId) {
3
- const bridge = String(bridgeChannelId ?? "").trim();
4
- if (!bridge) return false;
5
- if (String(channelId ?? "").trim() !== bridge) return false;
2
+ function shouldForwardChatToQQ(channel, fromId) {
3
+ if (!channel || channel.forward_to_qq !== 1) return false;
6
4
  const fid = String(fromId ?? "").trim();
7
5
  if (!fid) return false;
8
6
  if (fid.startsWith("qq_")) return false;
9
7
  return true;
10
8
  }
11
- function createMessagesRoutes({ query, body, json, forwardToQQBridge, getBridgeChannelId }) {
9
+ function createMessagesRoutes({ query, body, json, forwardToQQBridge, getChannelForQQ }) {
12
10
  return async function handle({
13
11
  path,
14
12
  method,
@@ -45,6 +43,15 @@ function createMessagesRoutes({ query, body, json, forwardToQQBridge, getBridgeC
45
43
  json(res, { success: false, error: "too many requests" }, 413);
46
44
  return true;
47
45
  }
46
+ for (const m of messages) {
47
+ const channel = getChannelForQQ(String(m.channelId ?? ""));
48
+ const fromId = String(m.fromid ?? "");
49
+ const sourceField = fromId.startsWith("qq_") ? "source_qq" : fromId === "system" || fromId === "SYSTEM" ? "source_system" : "source_game";
50
+ if (channel?.[sourceField] === 0) {
51
+ json(res, { success: false, error: "channel source disabled" }, 403);
52
+ return true;
53
+ }
54
+ }
48
55
  for (const m of messages) {
49
56
  query(
50
57
  SQL`INSERT OR REPLACE INTO sfmc_chat_messages (
@@ -56,12 +63,19 @@ function createMessagesRoutes({ query, body, json, forwardToQQBridge, getBridgeC
56
63
  )`
57
64
  );
58
65
  }
59
- const bridgeId = getBridgeChannelId();
60
66
  for (const m of messages) {
61
67
  const channelId = String(m.channelId ?? "");
62
68
  const fromId = String(m.fromid ?? "");
63
- if (!shouldForwardChatToQQ(bridgeId, channelId, fromId)) continue;
64
- forwardToQQBridge(channelId, String(m.fromName ?? ""), String(m.content ?? ""), fromId);
69
+ if (!fromId.trim() || fromId.startsWith("qq_")) continue;
70
+ const channel = getChannelForQQ(channelId);
71
+ if (!shouldForwardChatToQQ(channel, fromId)) continue;
72
+ forwardToQQBridge(
73
+ channelId,
74
+ channel.prefix || channelId,
75
+ String(m.fromName ?? ""),
76
+ String(m.content ?? ""),
77
+ fromId
78
+ );
65
79
  }
66
80
  json(res, { success: true });
67
81
  } else {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/routes/messages.ts"],
4
- "sourcesContent": ["/**\n * routes/messages.ts \u2014 \u804A\u5929\u6D88\u606F (sfmc_chat_messages)\n *\n * \u8DEF\u7531\u5217\u8868\uFF1A\n * GET /api/sfmc/messages \u2014 \u6A21\u7CCA/\u8FC7\u6EE4\u67E5\u8BE2\u6D88\u606F\n * POST /api/sfmc/messages \u2014 \u6279\u91CF INSERT OR REPLACE \u6D88\u606F\uFF08\u5339\u914D bridge \u9891\u9053\u65F6\u8F6C\u53D1\u81F3 QQ\uFF09\n */\n\nimport { SQL } from \"sql-template-strings\";\nimport type { QueryFn } from \"../lib/sqlite.js\";\n\ninterface Deps {\n query: QueryFn;\n body: (req: import(\"http\").IncomingMessage) => Promise<Record<string, unknown>>;\n json: (res: import(\"http\").ServerResponse, data: Record<string, unknown>, status?: number) => void;\n forwardToQQBridge: (channelId: string, fromName: string, content: string, fromId: string) => void;\n /** \u5F53\u524D\u6865\u63A5\u9891\u9053\uFF1B\u7A7A\u5219\u4E0D\u505A MC\u2192QQ \u804A\u5929\u51FA\u7AD9\uFF08\u4E8B\u4EF6\u901A\u9053\u4E0D\u53D7\u5F71\u54CD\uFF09 */\n getBridgeChannelId: () => string;\n}\n\n/** \u662F\u5426\u5E94\u5C06\u672C\u6761\u6D88\u606F\u63A8\u5230 QQ \u7FA4 */\nexport function shouldForwardChatToQQ(\n bridgeChannelId: string,\n channelId: string,\n fromId: string\n): boolean {\n const bridge = String(bridgeChannelId ?? \"\").trim();\n if (!bridge) return false;\n if (String(channelId ?? \"\").trim() !== bridge) return false;\n const fid = String(fromId ?? \"\").trim();\n // \u65E0\u53D1\u9001\u8005 id \u4E0D\u51FA\u7AD9\uFF08\u907F\u514D\u89E3\u6790\u5931\u8D25\u65F6\u8BEF\u63A8\uFF09\n if (!fid) return false;\n // QQ\u2192MC \u5199\u5165\u7684 fromid \u5F62\u5982 qq_*\uFF0C\u7981\u6B62\u56DE\u73AF\u63A8\u7FA4\n if (fid.startsWith(\"qq_\")) return false;\n return true;\n}\n\nfunction createMessagesRoutes({ query, body, json, forwardToQQBridge, getBridgeChannelId }: Deps) {\n return async function handle({\n path,\n method,\n params,\n req,\n res,\n }: {\n path: string;\n method: string;\n params: URLSearchParams;\n req: import(\"http\").IncomingMessage;\n res: import(\"http\").ServerResponse;\n }): Promise<boolean> {\n if (path === \"/api/sfmc/messages\") {\n if (method === \"GET\") {\n const stmt = SQL`SELECT * FROM sfmc_chat_messages WHERE 1=1`;\n const search = params.get(\"search\")?.trim();\n if (search) stmt.append(SQL` AND (content LIKE ${`%${search}%`})`);\n const type = params.get(\"type\")?.trim();\n if (type) stmt.append(SQL` AND type = ${type}`);\n const channelId = params.get(\"channelId\")?.trim();\n if (channelId) stmt.append(SQL` AND channel_id = ${channelId}`);\n const from = params.get(\"from\")?.trim();\n if (from) stmt.append(SQL` AND from_id = ${from}`);\n const minCreatedAt = params.get(\"minCreatedAt\")?.trim();\n if (minCreatedAt) stmt.append(SQL` AND created_at >= ${Number(minCreatedAt)}`);\n const minSentAt = params.get(\"minSentAt\")?.trim();\n if (minSentAt) stmt.append(SQL` AND created_at >= ${Number(minSentAt)}`);\n const maxCreatedAt = params.get(\"maxCreatedAt\")?.trim();\n if (maxCreatedAt) stmt.append(SQL` AND created_at <= ${Number(maxCreatedAt)}`);\n stmt.append(SQL` ORDER BY created_at ASC`);\n json(res, { messages: query(stmt) });\n } else if (method === \"POST\") {\n const { messages } = await body(req);\n if (!Array.isArray(messages) || messages.length === 0) {\n json(res, { success: false, error: \"invalid\" }, 400);\n return true;\n }\n if ((messages as unknown[]).length > 100) {\n json(res, { success: false, error: \"too many requests\" }, 413);\n return true;\n }\n // \u591A\u884C INSERT \u6539\u6210\u5FAA\u73AF\u5355\u6761 INSERT OR REPLACE \u2014\u2014 \u7B80\u5355\u4F18\u5148\n for (const m of messages as Array<Record<string, unknown>>) {\n query(\n SQL`INSERT OR REPLACE INTO sfmc_chat_messages (\n id, channel_id, from_id, from_name, type, content, attachment, show_timestamp, created_at\n ) VALUES (\n ${m.id}, ${m.channelId}, ${m.fromid}, ${m.fromName},\n ${m.type ?? \"text\"}, ${m.content}, ${m.attachment ?? null},\n ${m.showTimestamp ? 1 : 0}, ${m.timestamp}\n )`\n );\n }\n const bridgeId = getBridgeChannelId();\n for (const m of messages as Array<Record<string, unknown>>) {\n const channelId = String(m.channelId ?? \"\");\n const fromId = String(m.fromid ?? \"\");\n if (!shouldForwardChatToQQ(bridgeId, channelId, fromId)) continue;\n forwardToQQBridge(channelId, String(m.fromName ?? \"\"), String(m.content ?? \"\"), fromId);\n }\n json(res, { success: true });\n } else {\n json(res, { success: false, error: \"not_found\" }, 404);\n }\n return true;\n }\n return false;\n };\n}\n\nexport { createMessagesRoutes };\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * routes/messages.ts \u2014 \u804A\u5929\u6D88\u606F (sfmc_chat_messages)\n *\n * \u8DEF\u7531\u5217\u8868\uFF1A\n * GET /api/sfmc/messages \u2014 \u6A21\u7CCA/\u8FC7\u6EE4\u67E5\u8BE2\u6D88\u606F\n * POST /api/sfmc/messages \u2014 \u6279\u91CF INSERT OR REPLACE \u6D88\u606F\uFF08\u6309\u9891\u9053\u5F00\u5173\u8F6C\u53D1\u81F3 QQ\uFF09\n */\n\nimport { SQL } from \"sql-template-strings\";\nimport type { QueryFn } from \"../lib/sqlite.js\";\n\ninterface Deps {\n query: QueryFn;\n body: (req: import(\"http\").IncomingMessage) => Promise<Record<string, unknown>>;\n json: (res: import(\"http\").ServerResponse, data: Record<string, unknown>, status?: number) => void;\n forwardToQQBridge: (channelId: string, prefix: string, fromName: string, content: string, fromId: string) => void;\n /** \u67E5\u8BE2\u6765\u6E90\u9891\u9053\u7684\u524D\u7F00\u548C QQ \u8F6C\u53D1\u5F00\u5173\u3002 */\n getChannelForQQ: (channelId: string) => {\n prefix: string;\n forward_to_qq: number;\n source_game?: number;\n source_qq?: number;\n source_system?: number;\n } | null;\n}\n\n/** \u662F\u5426\u5E94\u5C06\u672C\u6761\u6D88\u606F\u63A8\u5230 QQ \u7FA4 */\nexport function shouldForwardChatToQQ(\n channel: { forward_to_qq: number } | null,\n fromId: string\n): boolean {\n if (!channel || channel.forward_to_qq !== 1) return false;\n const fid = String(fromId ?? \"\").trim();\n // \u65E0\u53D1\u9001\u8005 id \u4E0D\u51FA\u7AD9\uFF08\u907F\u514D\u89E3\u6790\u5931\u8D25\u65F6\u8BEF\u63A8\uFF09\n if (!fid) return false;\n // QQ\u2192MC \u5199\u5165\u7684 fromid \u5F62\u5982 qq_*\uFF0C\u7981\u6B62\u56DE\u73AF\u63A8\u7FA4\n if (fid.startsWith(\"qq_\")) return false;\n return true;\n}\n\nfunction createMessagesRoutes({ query, body, json, forwardToQQBridge, getChannelForQQ }: Deps) {\n return async function handle({\n path,\n method,\n params,\n req,\n res,\n }: {\n path: string;\n method: string;\n params: URLSearchParams;\n req: import(\"http\").IncomingMessage;\n res: import(\"http\").ServerResponse;\n }): Promise<boolean> {\n if (path === \"/api/sfmc/messages\") {\n if (method === \"GET\") {\n const stmt = SQL`SELECT * FROM sfmc_chat_messages WHERE 1=1`;\n const search = params.get(\"search\")?.trim();\n if (search) stmt.append(SQL` AND (content LIKE ${`%${search}%`})`);\n const type = params.get(\"type\")?.trim();\n if (type) stmt.append(SQL` AND type = ${type}`);\n const channelId = params.get(\"channelId\")?.trim();\n if (channelId) stmt.append(SQL` AND channel_id = ${channelId}`);\n const from = params.get(\"from\")?.trim();\n if (from) stmt.append(SQL` AND from_id = ${from}`);\n const minCreatedAt = params.get(\"minCreatedAt\")?.trim();\n if (minCreatedAt) stmt.append(SQL` AND created_at >= ${Number(minCreatedAt)}`);\n const minSentAt = params.get(\"minSentAt\")?.trim();\n if (minSentAt) stmt.append(SQL` AND created_at >= ${Number(minSentAt)}`);\n const maxCreatedAt = params.get(\"maxCreatedAt\")?.trim();\n if (maxCreatedAt) stmt.append(SQL` AND created_at <= ${Number(maxCreatedAt)}`);\n stmt.append(SQL` ORDER BY created_at ASC`);\n json(res, { messages: query(stmt) });\n } else if (method === \"POST\") {\n const { messages } = await body(req);\n if (!Array.isArray(messages) || messages.length === 0) {\n json(res, { success: false, error: \"invalid\" }, 400);\n return true;\n }\n if ((messages as unknown[]).length > 100) {\n json(res, { success: false, error: \"too many requests\" }, 413);\n return true;\n }\n // \u5148\u68C0\u67E5\u6574\u4E2A\u6279\u6B21\uFF0C\u907F\u514D\u524D\u51E0\u6761\u5DF2\u5199\u5165\u540E\u624D\u53D1\u73B0\u88AB\u7981\u7528\u7684\u6765\u6E90\u3002\n for (const m of messages as Array<Record<string, unknown>>) {\n const channel = getChannelForQQ(String(m.channelId ?? \"\"));\n const fromId = String(m.fromid ?? \"\");\n const sourceField = fromId.startsWith(\"qq_\")\n ? \"source_qq\"\n : fromId === \"system\" || fromId === \"SYSTEM\"\n ? \"source_system\"\n : \"source_game\";\n if (channel?.[sourceField] === 0) {\n json(res, { success: false, error: \"channel source disabled\" }, 403);\n return true;\n }\n }\n // \u591A\u884C INSERT \u6539\u6210\u5FAA\u73AF\u5355\u6761 INSERT OR REPLACE \u2014\u2014 \u7B80\u5355\u4F18\u5148\n for (const m of messages as Array<Record<string, unknown>>) {\n query(\n SQL`INSERT OR REPLACE INTO sfmc_chat_messages (\n id, channel_id, from_id, from_name, type, content, attachment, show_timestamp, created_at\n ) VALUES (\n ${m.id}, ${m.channelId}, ${m.fromid}, ${m.fromName},\n ${m.type ?? \"text\"}, ${m.content}, ${m.attachment ?? null},\n ${m.showTimestamp ? 1 : 0}, ${m.timestamp}\n )`\n );\n }\n for (const m of messages as Array<Record<string, unknown>>) {\n const channelId = String(m.channelId ?? \"\");\n const fromId = String(m.fromid ?? \"\");\n if (!fromId.trim() || fromId.startsWith(\"qq_\")) continue;\n const channel = getChannelForQQ(channelId);\n if (!shouldForwardChatToQQ(channel, fromId)) continue;\n forwardToQQBridge(\n channelId,\n channel!.prefix || channelId,\n String(m.fromName ?? \"\"),\n String(m.content ?? \"\"),\n fromId\n );\n }\n json(res, { success: true });\n } else {\n json(res, { success: false, error: \"not_found\" }, 404);\n }\n return true;\n }\n return false;\n };\n}\n\nexport { createMessagesRoutes };\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1 +1 @@
1
- {"version":3,"file":"qq-bind.d.ts","sourceRoot":"","sources":["../../src/routes/qq-bind.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAEhD,QAAA,MAAM,WAAW,QAAgB,CAAC;AAGlC,UAAU,IAAI;IACZ,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,EAAE,CAAC,GAAG,EAAE,OAAO,MAAM,EAAE,eAAe,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAChF,IAAI,EAAE,CAAC,GAAG,EAAE,OAAO,MAAM,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CACpG;AAQD,iBAAS,kBAAkB,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,IAAI,yCAOlD;IACD,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,eAAe,CAAC;IACxB,GAAG,EAAE,OAAO,MAAM,EAAE,eAAe,CAAC;IACpC,GAAG,EAAE,OAAO,MAAM,EAAE,cAAc,CAAC;CACpC,KAAG,OAAO,CAAC,OAAO,CAAC,CAsLrB;AAED,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,CAAC"}
1
+ {"version":3,"file":"qq-bind.d.ts","sourceRoot":"","sources":["../../src/routes/qq-bind.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAEhD,QAAA,MAAM,WAAW,QAAgB,CAAC;AAGlC,UAAU,IAAI;IACZ,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,EAAE,CAAC,GAAG,EAAE,OAAO,MAAM,EAAE,eAAe,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAChF,IAAI,EAAE,CAAC,GAAG,EAAE,OAAO,MAAM,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CACpG;AAQD,iBAAS,kBAAkB,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,IAAI,yCAOlD;IACD,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,eAAe,CAAC;IACxB,GAAG,EAAE,OAAO,MAAM,EAAE,eAAe,CAAC;IACpC,GAAG,EAAE,OAAO,MAAM,EAAE,cAAc,CAAC;CACpC,KAAG,OAAO,CAAC,OAAO,CAAC,CAmNrB;AAED,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,CAAC"}
@@ -1,5 +1,6 @@
1
1
  import { randomInt } from "node:crypto";
2
2
  import { SQL } from "sql-template-strings";
3
+ import { loadQqAccountProfile, saveQqAccountProfile } from "../domain/qq-account-profile.js";
3
4
  const CODE_TTL_MS = 5 * 60 * 1e3;
4
5
  const CODE_LEN = 6;
5
6
  function genCode() {
@@ -25,6 +26,8 @@ function createQqBindRoutes({ query, body, json }) {
25
26
  }
26
27
  const rows = openid ? query(SQL`SELECT * FROM sfmc_qq_bindings WHERE qq_user_openid = ${openid} LIMIT 1`) : query(SQL`SELECT * FROM sfmc_qq_bindings WHERE player_xuid = ${xuid} LIMIT 1`);
27
28
  const row = rows[0] ?? null;
29
+ const playerId = row ? String(row.player_xuid ?? "") : "";
30
+ const playerName = row ? String(row.player_name ?? "") : "";
28
31
  json(res, {
29
32
  success: true,
30
33
  bound: !!row,
@@ -34,10 +37,35 @@ function createQqBindRoutes({ query, body, json }) {
34
37
  player_name: row.player_name,
35
38
  qq_backend: row.qq_backend,
36
39
  bound_at: row.bound_at
37
- } : null
40
+ } : null,
41
+ ...row ? { profile: loadQqAccountProfile(query, playerId, playerName) } : {}
38
42
  });
39
43
  return true;
40
44
  }
45
+ if (path === "/api/sfmc/qq/bind/profile" && method === "POST") {
46
+ const data = await body(req);
47
+ const playerId = String(data.xuid ?? data.player_xuid ?? "").trim();
48
+ const playerName = String(data.name ?? data.player_name ?? "").trim();
49
+ if (!playerId) {
50
+ json(res, { success: false, error: "xuid_required" }, 400);
51
+ return true;
52
+ }
53
+ const optionalNumber = (value) => {
54
+ if (value == null || value === "") return null;
55
+ const n = Number(value);
56
+ return Number.isFinite(n) && n >= 0 ? Math.floor(n) : null;
57
+ };
58
+ saveQqAccountProfile(query, {
59
+ playerId,
60
+ playerName,
61
+ balance: optionalNumber(data.balance),
62
+ todaySeconds: optionalNumber(data.today_seconds),
63
+ monthSeconds: optionalNumber(data.month_seconds),
64
+ totalSeconds: optionalNumber(data.total_seconds)
65
+ });
66
+ json(res, { success: true });
67
+ return true;
68
+ }
41
69
  if (path === "/api/sfmc/qq/bind/request" && method === "POST") {
42
70
  const data = await body(req);
43
71
  const openid = String(data.openid ?? data.qq_user_openid ?? "").trim();
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/routes/qq-bind.ts"],
4
- "sourcesContent": ["/**\n * routes/qq-bind.ts \u2014 QQ\u2194MC \u8EAB\u4EFD\u7ED1\u5B9A\uFF08\u5E73\u53F0 HTTP\uFF09\n *\n * POST /api/sfmc/qq/bind/request \u2014 QQ \u4FA7\u7533\u8BF7\u77ED\u7801\n * POST /api/sfmc/qq/bind/confirm \u2014 \u6E38\u620F\u4FA7\u786E\u8BA4\n * POST /api/sfmc/qq/bind/unbind \u2014 \u89E3\u7ED1\n * GET /api/sfmc/qq/bind/me \u2014 \u67E5\u8BE2\n */\n\nimport { randomInt } from \"node:crypto\";\nimport { SQL } from \"sql-template-strings\";\nimport type { QueryFn } from \"../lib/sqlite.js\";\n\nconst CODE_TTL_MS = 5 * 60 * 1000;\nconst CODE_LEN = 6;\n\ninterface Deps {\n query: QueryFn;\n body: (req: import(\"http\").IncomingMessage) => Promise<Record<string, unknown>>;\n json: (res: import(\"http\").ServerResponse, data: Record<string, unknown>, status?: number) => void;\n}\n\nfunction genCode(): string {\n let s = \"\";\n for (let i = 0; i < CODE_LEN; i++) s += String(randomInt(0, 10));\n return s;\n}\n\nfunction createQqBindRoutes({ query, body, json }: Deps) {\n return async function handle({\n path,\n method,\n params,\n req,\n res,\n }: {\n path: string;\n method: string;\n params: URLSearchParams;\n req: import(\"http\").IncomingMessage;\n res: import(\"http\").ServerResponse;\n }): Promise<boolean> {\n if (!path.startsWith(\"/api/sfmc/qq/bind\")) return false;\n\n if (path === \"/api/sfmc/qq/bind/me\" && method === \"GET\") {\n const openid = String(params.get(\"openid\") ?? \"\").trim();\n const xuid = String(params.get(\"xuid\") ?? \"\").trim();\n if (!openid && !xuid) {\n json(res, { success: false, error: \"openid_or_xuid_required\" }, 400);\n return true;\n }\n const rows = openid\n ? (query(SQL`SELECT * FROM sfmc_qq_bindings WHERE qq_user_openid = ${openid} LIMIT 1`) as Array<\n Record<string, unknown>\n >)\n : (query(SQL`SELECT * FROM sfmc_qq_bindings WHERE player_xuid = ${xuid} LIMIT 1`) as Array<\n Record<string, unknown>\n >);\n const row = rows[0] ?? null;\n json(res, {\n success: true,\n bound: !!row,\n binding: row\n ? {\n qq_user_openid: row.qq_user_openid,\n player_xuid: row.player_xuid,\n player_name: row.player_name,\n qq_backend: row.qq_backend,\n bound_at: row.bound_at,\n }\n : null,\n });\n return true;\n }\n\n if (path === \"/api/sfmc/qq/bind/request\" && method === \"POST\") {\n const data = await body(req);\n const openid = String(data.openid ?? data.qq_user_openid ?? \"\").trim();\n const backend = String(data.qq_backend ?? \"official\").trim() || \"official\";\n if (!openid) {\n json(res, { success: false, error: \"openid_required\" }, 400);\n return true;\n }\n // \u5DF2\u7ED1\u5B9A\u5219\u62D2\u7EDD\u518D\u7533\u8BF7\uFF08\u987B\u5148\u89E3\u7ED1\uFF09\n const existing = query(\n SQL`SELECT player_name FROM sfmc_qq_bindings WHERE qq_user_openid = ${openid} LIMIT 1`\n ) as Array<{ player_name: string }>;\n if (existing[0]) {\n json(\n res,\n {\n success: false,\n error: \"already_bound\",\n player_name: existing[0].player_name,\n },\n 409\n );\n return true;\n }\n\n const now = Date.now();\n query(SQL`DELETE FROM sfmc_qq_bind_pending WHERE qq_user_openid = ${openid} OR expires_at < ${now}`);\n\n let code = \"\";\n for (let attempt = 0; attempt < 8; attempt++) {\n const candidate = genCode();\n const clash = query(\n SQL`SELECT 1 AS ok FROM sfmc_qq_bind_pending WHERE code = ${candidate} LIMIT 1`\n ) as unknown[];\n if (clash.length === 0) {\n code = candidate;\n break;\n }\n }\n if (!code) {\n json(res, { success: false, error: \"code_gen_failed\" }, 500);\n return true;\n }\n\n const expiresAt = now + CODE_TTL_MS;\n query(\n SQL`INSERT INTO sfmc_qq_bind_pending (code, qq_user_openid, qq_backend, created_at, expires_at)\n VALUES (${code}, ${openid}, ${backend}, ${now}, ${expiresAt})`\n );\n json(res, { success: true, code, expires_at: expiresAt, ttl_ms: CODE_TTL_MS });\n return true;\n }\n\n if (path === \"/api/sfmc/qq/bind/confirm\" && method === \"POST\") {\n const data = await body(req);\n const code = String(data.code ?? \"\").trim();\n const xuid = String(data.xuid ?? data.player_xuid ?? \"\").trim();\n const name = String(data.name ?? data.player_name ?? \"\").trim();\n if (!code || !xuid) {\n json(res, { success: false, error: \"code_and_xuid_required\" }, 400);\n return true;\n }\n\n const now = Date.now();\n const pending = query(\n SQL`SELECT * FROM sfmc_qq_bind_pending WHERE code = ${code} LIMIT 1`\n ) as Array<{\n code: string;\n qq_user_openid: string;\n qq_backend: string;\n expires_at: number;\n }>;\n const row = pending[0];\n if (!row) {\n json(res, { success: false, error: \"invalid_code\" }, 400);\n return true;\n }\n if (Number(row.expires_at) < now) {\n query(SQL`DELETE FROM sfmc_qq_bind_pending WHERE code = ${code}`);\n json(res, { success: false, error: \"code_expired\" }, 400);\n return true;\n }\n\n const byOpenid = query(\n SQL`SELECT player_xuid FROM sfmc_qq_bindings WHERE qq_user_openid = ${row.qq_user_openid} LIMIT 1`\n ) as Array<{ player_xuid: string }>;\n if (byOpenid[0] && byOpenid[0].player_xuid !== xuid) {\n json(res, { success: false, error: \"qq_already_bound\", hint: \"\u8BF7\u5148\u5728 QQ \u4FA7\u89E3\u7ED1\" }, 409);\n return true;\n }\n const byXuid = query(\n SQL`SELECT qq_user_openid FROM sfmc_qq_bindings WHERE player_xuid = ${xuid} LIMIT 1`\n ) as Array<{ qq_user_openid: string }>;\n if (byXuid[0] && byXuid[0].qq_user_openid !== row.qq_user_openid) {\n json(res, { success: false, error: \"player_already_bound\", hint: \"\u8BF7\u5148\u89E3\u7ED1\u5F53\u524D QQ\" }, 409);\n return true;\n }\n\n query(SQL`DELETE FROM sfmc_qq_bindings WHERE qq_user_openid = ${row.qq_user_openid} OR player_xuid = ${xuid}`);\n query(\n SQL`INSERT INTO sfmc_qq_bindings (qq_user_openid, player_xuid, player_name, qq_backend, bound_at)\n VALUES (${row.qq_user_openid}, ${xuid}, ${name}, ${row.qq_backend}, ${now})`\n );\n query(SQL`DELETE FROM sfmc_qq_bind_pending WHERE code = ${code}`);\n json(res, {\n success: true,\n binding: {\n qq_user_openid: row.qq_user_openid,\n player_xuid: xuid,\n player_name: name,\n qq_backend: row.qq_backend,\n bound_at: now,\n },\n });\n return true;\n }\n\n if (path === \"/api/sfmc/qq/bind/unbind\" && method === \"POST\") {\n const data = await body(req);\n const openid = String(data.openid ?? data.qq_user_openid ?? \"\").trim();\n const xuid = String(data.xuid ?? data.player_xuid ?? \"\").trim();\n if (!openid && !xuid) {\n json(res, { success: false, error: \"openid_or_xuid_required\" }, 400);\n return true;\n }\n let removed = 0;\n if (openid) {\n const before = query(\n SQL`SELECT qq_user_openid FROM sfmc_qq_bindings WHERE qq_user_openid = ${openid}`\n ) as unknown[];\n query(SQL`DELETE FROM sfmc_qq_bindings WHERE qq_user_openid = ${openid}`);\n query(SQL`DELETE FROM sfmc_qq_bind_pending WHERE qq_user_openid = ${openid}`);\n removed += before.length;\n }\n if (xuid) {\n const before = query(\n SQL`SELECT qq_user_openid FROM sfmc_qq_bindings WHERE player_xuid = ${xuid}`\n ) as unknown[];\n query(SQL`DELETE FROM sfmc_qq_bindings WHERE player_xuid = ${xuid}`);\n removed += before.length;\n }\n json(res, { success: true, unbound: removed > 0, removed });\n return true;\n }\n\n json(res, { success: false, error: \"not_found\" }, 404);\n return true;\n };\n}\n\nexport { createQqBindRoutes, CODE_TTL_MS };\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * routes/qq-bind.ts \u2014 QQ\u2194MC \u8EAB\u4EFD\u7ED1\u5B9A\uFF08\u5E73\u53F0 HTTP\uFF09\n *\n * POST /api/sfmc/qq/bind/request \u2014 QQ \u4FA7\u7533\u8BF7\u77ED\u7801\n * POST /api/sfmc/qq/bind/confirm \u2014 \u6E38\u620F\u4FA7\u786E\u8BA4\n * POST /api/sfmc/qq/bind/unbind \u2014 \u89E3\u7ED1\n * GET /api/sfmc/qq/bind/me \u2014 \u67E5\u8BE2\n */\n\nimport { randomInt } from \"node:crypto\";\nimport { SQL } from \"sql-template-strings\";\nimport { loadQqAccountProfile, saveQqAccountProfile } from \"../domain/qq-account-profile.js\";\nimport type { QueryFn } from \"../lib/sqlite.js\";\n\nconst CODE_TTL_MS = 5 * 60 * 1000;\nconst CODE_LEN = 6;\n\ninterface Deps {\n query: QueryFn;\n body: (req: import(\"http\").IncomingMessage) => Promise<Record<string, unknown>>;\n json: (res: import(\"http\").ServerResponse, data: Record<string, unknown>, status?: number) => void;\n}\n\nfunction genCode(): string {\n let s = \"\";\n for (let i = 0; i < CODE_LEN; i++) s += String(randomInt(0, 10));\n return s;\n}\n\nfunction createQqBindRoutes({ query, body, json }: Deps) {\n return async function handle({\n path,\n method,\n params,\n req,\n res,\n }: {\n path: string;\n method: string;\n params: URLSearchParams;\n req: import(\"http\").IncomingMessage;\n res: import(\"http\").ServerResponse;\n }): Promise<boolean> {\n if (!path.startsWith(\"/api/sfmc/qq/bind\")) return false;\n\n if (path === \"/api/sfmc/qq/bind/me\" && method === \"GET\") {\n const openid = String(params.get(\"openid\") ?? \"\").trim();\n const xuid = String(params.get(\"xuid\") ?? \"\").trim();\n if (!openid && !xuid) {\n json(res, { success: false, error: \"openid_or_xuid_required\" }, 400);\n return true;\n }\n const rows = openid\n ? (query(SQL`SELECT * FROM sfmc_qq_bindings WHERE qq_user_openid = ${openid} LIMIT 1`) as Array<\n Record<string, unknown>\n >)\n : (query(SQL`SELECT * FROM sfmc_qq_bindings WHERE player_xuid = ${xuid} LIMIT 1`) as Array<\n Record<string, unknown>\n >);\n const row = rows[0] ?? null;\n const playerId = row ? String(row.player_xuid ?? \"\") : \"\";\n const playerName = row ? String(row.player_name ?? \"\") : \"\";\n json(res, {\n success: true,\n bound: !!row,\n binding: row\n ? {\n qq_user_openid: row.qq_user_openid,\n player_xuid: row.player_xuid,\n player_name: row.player_name,\n qq_backend: row.qq_backend,\n bound_at: row.bound_at,\n }\n : null,\n ...(row ? { profile: loadQqAccountProfile(query, playerId, playerName) } : {}),\n });\n return true;\n }\n\n if (path === \"/api/sfmc/qq/bind/profile\" && method === \"POST\") {\n const data = await body(req);\n const playerId = String(data.xuid ?? data.player_xuid ?? \"\").trim();\n const playerName = String(data.name ?? data.player_name ?? \"\").trim();\n if (!playerId) {\n json(res, { success: false, error: \"xuid_required\" }, 400);\n return true;\n }\n const optionalNumber = (value: unknown): number | null => {\n if (value == null || value === \"\") return null;\n const n = Number(value);\n return Number.isFinite(n) && n >= 0 ? Math.floor(n) : null;\n };\n saveQqAccountProfile(query, {\n playerId,\n playerName,\n balance: optionalNumber(data.balance),\n todaySeconds: optionalNumber(data.today_seconds),\n monthSeconds: optionalNumber(data.month_seconds),\n totalSeconds: optionalNumber(data.total_seconds),\n });\n json(res, { success: true });\n return true;\n }\n\n if (path === \"/api/sfmc/qq/bind/request\" && method === \"POST\") {\n const data = await body(req);\n const openid = String(data.openid ?? data.qq_user_openid ?? \"\").trim();\n const backend = String(data.qq_backend ?? \"official\").trim() || \"official\";\n if (!openid) {\n json(res, { success: false, error: \"openid_required\" }, 400);\n return true;\n }\n // \u5DF2\u7ED1\u5B9A\u5219\u62D2\u7EDD\u518D\u7533\u8BF7\uFF08\u987B\u5148\u89E3\u7ED1\uFF09\n const existing = query(\n SQL`SELECT player_name FROM sfmc_qq_bindings WHERE qq_user_openid = ${openid} LIMIT 1`\n ) as Array<{ player_name: string }>;\n if (existing[0]) {\n json(\n res,\n {\n success: false,\n error: \"already_bound\",\n player_name: existing[0].player_name,\n },\n 409\n );\n return true;\n }\n\n const now = Date.now();\n query(SQL`DELETE FROM sfmc_qq_bind_pending WHERE qq_user_openid = ${openid} OR expires_at < ${now}`);\n\n let code = \"\";\n for (let attempt = 0; attempt < 8; attempt++) {\n const candidate = genCode();\n const clash = query(\n SQL`SELECT 1 AS ok FROM sfmc_qq_bind_pending WHERE code = ${candidate} LIMIT 1`\n ) as unknown[];\n if (clash.length === 0) {\n code = candidate;\n break;\n }\n }\n if (!code) {\n json(res, { success: false, error: \"code_gen_failed\" }, 500);\n return true;\n }\n\n const expiresAt = now + CODE_TTL_MS;\n query(\n SQL`INSERT INTO sfmc_qq_bind_pending (code, qq_user_openid, qq_backend, created_at, expires_at)\n VALUES (${code}, ${openid}, ${backend}, ${now}, ${expiresAt})`\n );\n json(res, { success: true, code, expires_at: expiresAt, ttl_ms: CODE_TTL_MS });\n return true;\n }\n\n if (path === \"/api/sfmc/qq/bind/confirm\" && method === \"POST\") {\n const data = await body(req);\n const code = String(data.code ?? \"\").trim();\n const xuid = String(data.xuid ?? data.player_xuid ?? \"\").trim();\n const name = String(data.name ?? data.player_name ?? \"\").trim();\n if (!code || !xuid) {\n json(res, { success: false, error: \"code_and_xuid_required\" }, 400);\n return true;\n }\n\n const now = Date.now();\n const pending = query(\n SQL`SELECT * FROM sfmc_qq_bind_pending WHERE code = ${code} LIMIT 1`\n ) as Array<{\n code: string;\n qq_user_openid: string;\n qq_backend: string;\n expires_at: number;\n }>;\n const row = pending[0];\n if (!row) {\n json(res, { success: false, error: \"invalid_code\" }, 400);\n return true;\n }\n if (Number(row.expires_at) < now) {\n query(SQL`DELETE FROM sfmc_qq_bind_pending WHERE code = ${code}`);\n json(res, { success: false, error: \"code_expired\" }, 400);\n return true;\n }\n\n const byOpenid = query(\n SQL`SELECT player_xuid FROM sfmc_qq_bindings WHERE qq_user_openid = ${row.qq_user_openid} LIMIT 1`\n ) as Array<{ player_xuid: string }>;\n if (byOpenid[0] && byOpenid[0].player_xuid !== xuid) {\n json(res, { success: false, error: \"qq_already_bound\", hint: \"\u8BF7\u5148\u5728 QQ \u4FA7\u89E3\u7ED1\" }, 409);\n return true;\n }\n const byXuid = query(\n SQL`SELECT qq_user_openid FROM sfmc_qq_bindings WHERE player_xuid = ${xuid} LIMIT 1`\n ) as Array<{ qq_user_openid: string }>;\n if (byXuid[0] && byXuid[0].qq_user_openid !== row.qq_user_openid) {\n json(res, { success: false, error: \"player_already_bound\", hint: \"\u8BF7\u5148\u89E3\u7ED1\u5F53\u524D QQ\" }, 409);\n return true;\n }\n\n // \u7ED1\u5B9A\u8BB0\u5F55\u5C31\u662F\u81EA\u6709\u767D\u540D\u5355\uFF1A\u5199\u5165\u540E\u6E38\u620F\u4FA7\u5141\u8BB8\u79FB\u52A8\u5E76\u6062\u590D\u6210\u5458/\u64CD\u4F5C\u5458\u6743\u9650\u3002\n query(SQL`DELETE FROM sfmc_qq_bindings WHERE qq_user_openid = ${row.qq_user_openid} OR player_xuid = ${xuid}`);\n query(\n SQL`INSERT INTO sfmc_qq_bindings (qq_user_openid, player_xuid, player_name, qq_backend, bound_at)\n VALUES (${row.qq_user_openid}, ${xuid}, ${name}, ${row.qq_backend}, ${now})`\n );\n query(SQL`DELETE FROM sfmc_qq_bind_pending WHERE code = ${code}`);\n json(res, {\n success: true,\n binding: {\n qq_user_openid: row.qq_user_openid,\n player_xuid: xuid,\n player_name: name,\n qq_backend: row.qq_backend,\n bound_at: now,\n },\n });\n return true;\n }\n\n if (path === \"/api/sfmc/qq/bind/unbind\" && method === \"POST\") {\n const data = await body(req);\n const openid = String(data.openid ?? data.qq_user_openid ?? \"\").trim();\n const xuid = String(data.xuid ?? data.player_xuid ?? \"\").trim();\n if (!openid && !xuid) {\n json(res, { success: false, error: \"openid_or_xuid_required\" }, 400);\n return true;\n }\n let removed = 0;\n if (openid) {\n const before = query(\n SQL`SELECT qq_user_openid FROM sfmc_qq_bindings WHERE qq_user_openid = ${openid}`\n ) as unknown[];\n query(SQL`DELETE FROM sfmc_qq_bindings WHERE qq_user_openid = ${openid}`);\n query(SQL`DELETE FROM sfmc_qq_bind_pending WHERE qq_user_openid = ${openid}`);\n removed += before.length;\n }\n if (xuid) {\n const before = query(\n SQL`SELECT qq_user_openid FROM sfmc_qq_bindings WHERE player_xuid = ${xuid}`\n ) as unknown[];\n query(SQL`DELETE FROM sfmc_qq_bindings WHERE player_xuid = ${xuid}`);\n removed += before.length;\n }\n json(res, { success: true, unbound: removed > 0, removed });\n return true;\n }\n\n json(res, { success: false, error: \"not_found\" }, 404);\n return true;\n };\n}\n\nexport { createQqBindRoutes, CODE_TTL_MS };\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -2,14 +2,18 @@
2
2
  * routes/qq-events.ts — MC 事件入站
3
3
  *
4
4
  * POST /api/sfmc/qq/events — 单条或 { events: [...] }(≤100)
5
+ * GET/POST /api/sfmc/qq/events/settings — 查询或修改推送开关和聚合间隔
5
6
  */
6
- import type { QqEventsAggregator } from "../domain/qq-events.js";
7
+ import type { QqEventsAggregator, ResolvedQqEventsConfig } from "../domain/qq-events.js";
7
8
  interface Deps {
8
9
  body: (req: import("http").IncomingMessage) => Promise<Record<string, unknown>>;
9
10
  json: (res: import("http").ServerResponse, data: Record<string, unknown>, status?: number) => void;
10
11
  aggregator: QqEventsAggregator;
12
+ getSettings: () => ResolvedQqEventsConfig;
13
+ setSettings: (patch: Partial<ResolvedQqEventsConfig>) => ResolvedQqEventsConfig;
14
+ isAdmin: (openid: string, asGroupAdmin: boolean) => boolean;
11
15
  }
12
- declare function createQqEventsRoutes({ body, json, aggregator }: Deps): ({ path, method, req, res, }: {
16
+ declare function createQqEventsRoutes({ body, json, aggregator, getSettings, setSettings, isAdmin }: Deps): ({ path, method, req, res, }: {
13
17
  path: string;
14
18
  method: string;
15
19
  params: URLSearchParams;
@@ -1 +1 @@
1
- {"version":3,"file":"qq-events.d.ts","sourceRoot":"","sources":["../../src/routes/qq-events.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAkB,MAAM,wBAAwB,CAAC;AAGjF,UAAU,IAAI;IACZ,IAAI,EAAE,CAAC,GAAG,EAAE,OAAO,MAAM,EAAE,eAAe,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAChF,IAAI,EAAE,CAAC,GAAG,EAAE,OAAO,MAAM,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IACnG,UAAU,EAAE,kBAAkB,CAAC;CAChC;AAaD,iBAAS,oBAAoB,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,EAAE,IAAI,iCAMzD;IACD,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,eAAe,CAAC;IACxB,GAAG,EAAE,OAAO,MAAM,EAAE,eAAe,CAAC;IACpC,GAAG,EAAE,OAAO,MAAM,EAAE,cAAc,CAAC;CACpC,KAAG,OAAO,CAAC,OAAO,CAAC,CAwBrB;AAED,OAAO,EAAE,oBAAoB,EAAE,CAAC"}
1
+ {"version":3,"file":"qq-events.d.ts","sourceRoot":"","sources":["../../src/routes/qq-events.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAkB,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAGzG,UAAU,IAAI;IACZ,IAAI,EAAE,CAAC,GAAG,EAAE,OAAO,MAAM,EAAE,eAAe,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAChF,IAAI,EAAE,CAAC,GAAG,EAAE,OAAO,MAAM,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IACnG,UAAU,EAAE,kBAAkB,CAAC;IAC/B,WAAW,EAAE,MAAM,sBAAsB,CAAC;IAC1C,WAAW,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,sBAAsB,CAAC,KAAK,sBAAsB,CAAC;IAChF,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,OAAO,KAAK,OAAO,CAAC;CAC7D;AAeD,iBAAS,oBAAoB,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,EAAE,WAAW,EAAE,OAAO,EAAE,EAAE,IAAI,iCAM5F;IACD,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,eAAe,CAAC;IACxB,GAAG,EAAE,OAAO,MAAM,EAAE,eAAe,CAAC;IACpC,GAAG,EAAE,OAAO,MAAM,EAAE,cAAc,CAAC;CACpC,KAAG,OAAO,CAAC,OAAO,CAAC,CAyDrB;AAED,OAAO,EAAE,oBAAoB,EAAE,CAAC"}
@@ -1,4 +1,5 @@
1
1
  import { normalizeEventPayload } from "../domain/qq-events.js";
2
+ const SWITCHES = ["enabled", "join", "leave", "death", "crash", "start", "stop"];
2
3
  function collectPayloads(data) {
3
4
  if (Array.isArray(data.events)) {
4
5
  return data.events.map((x) => normalizeEventPayload(x)).filter((x) => x != null);
@@ -6,13 +7,46 @@ function collectPayloads(data) {
6
7
  const one = normalizeEventPayload(data);
7
8
  return one ? [one] : [];
8
9
  }
9
- function createQqEventsRoutes({ body, json, aggregator }) {
10
+ function createQqEventsRoutes({ body, json, aggregator, getSettings, setSettings, isAdmin }) {
10
11
  return async function handle({
11
12
  path,
12
13
  method,
13
14
  req,
14
15
  res
15
16
  }) {
17
+ if (path === "/api/sfmc/qq/events/settings") {
18
+ if (method === "GET") {
19
+ json(res, { success: true, settings: getSettings() });
20
+ return true;
21
+ }
22
+ if (method === "POST") {
23
+ const data2 = await body(req);
24
+ const openid = typeof data2.openid === "string" ? data2.openid.trim() : "";
25
+ if (!isAdmin(openid, data2.as_group_admin === true)) {
26
+ json(res, { success: false, error: "not_admin" }, 403);
27
+ return true;
28
+ }
29
+ const field = data2.field;
30
+ if (field === "window_sec") {
31
+ const seconds = data2.value;
32
+ if (typeof seconds !== "number" || !Number.isInteger(seconds) || seconds < 5 || seconds > 600) {
33
+ json(res, { success: false, error: "invalid_setting" }, 400);
34
+ return true;
35
+ }
36
+ json(res, { success: true, settings: setSettings({ window_sec: seconds }) });
37
+ return true;
38
+ }
39
+ if (!SWITCHES.some((key) => key === field) || typeof data2.value !== "boolean") {
40
+ json(res, { success: false, error: "invalid_setting" }, 400);
41
+ return true;
42
+ }
43
+ const settings = setSettings({ [field]: data2.value });
44
+ json(res, { success: true, settings });
45
+ return true;
46
+ }
47
+ json(res, { success: false, error: "not_found" }, 404);
48
+ return true;
49
+ }
16
50
  if (path !== "/api/sfmc/qq/events") return false;
17
51
  if (method !== "POST") {
18
52
  json(res, { success: false, error: "not_found" }, 404);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/routes/qq-events.ts"],
4
- "sourcesContent": ["/**\n * routes/qq-events.ts \u2014 MC \u4E8B\u4EF6\u5165\u7AD9\n *\n * POST /api/sfmc/qq/events \u2014 \u5355\u6761\u6216 { events: [...] }\uFF08\u2264100\uFF09\n */\n\nimport type { QqEventsAggregator, QqEventPayload } from \"../domain/qq-events.js\";\nimport { normalizeEventPayload } from \"../domain/qq-events.js\";\n\ninterface Deps {\n body: (req: import(\"http\").IncomingMessage) => Promise<Record<string, unknown>>;\n json: (res: import(\"http\").ServerResponse, data: Record<string, unknown>, status?: number) => void;\n aggregator: QqEventsAggregator;\n}\n\nfunction collectPayloads(data: Record<string, unknown>): QqEventPayload[] {\n if (Array.isArray(data.events)) {\n return (data.events as unknown[])\n .map((x) => normalizeEventPayload(x))\n .filter((x): x is QqEventPayload => x != null);\n }\n // \u5355\u6761\uFF1A\u9876\u5C42\u5373\u4E8B\u4EF6\n const one = normalizeEventPayload(data);\n return one ? [one] : [];\n}\n\nfunction createQqEventsRoutes({ body, json, aggregator }: Deps) {\n return async function handle({\n path,\n method,\n req,\n res,\n }: {\n path: string;\n method: string;\n params: URLSearchParams;\n req: import(\"http\").IncomingMessage;\n res: import(\"http\").ServerResponse;\n }): Promise<boolean> {\n if (path !== \"/api/sfmc/qq/events\") return false;\n\n if (method !== \"POST\") {\n json(res, { success: false, error: \"not_found\" }, 404);\n return true;\n }\n\n const data = await body(req);\n if (Array.isArray(data.events) && (data.events as unknown[]).length > 100) {\n json(res, { success: false, error: \"too many requests\" }, 413);\n return true;\n }\n\n const payloads = collectPayloads(data);\n if (payloads.length === 0) {\n json(res, { success: false, error: \"invalid\" }, 400);\n return true;\n }\n\n const result = aggregator.ingestMany(payloads);\n json(res, { success: true, ...result });\n return true;\n };\n}\n\nexport { createQqEventsRoutes };\n"],
5
- "mappings": "AAOA,SAAS,6BAA6B;AAQtC,SAAS,gBAAgB,MAAiD;AACxE,MAAI,MAAM,QAAQ,KAAK,MAAM,GAAG;AAC9B,WAAQ,KAAK,OACV,IAAI,CAAC,MAAM,sBAAsB,CAAC,CAAC,EACnC,OAAO,CAAC,MAA2B,KAAK,IAAI;AAAA,EACjD;AAEA,QAAM,MAAM,sBAAsB,IAAI;AACtC,SAAO,MAAM,CAAC,GAAG,IAAI,CAAC;AACxB;AAEA,SAAS,qBAAqB,EAAE,MAAM,MAAM,WAAW,GAAS;AAC9D,SAAO,eAAe,OAAO;AAAA,IAC3B;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,GAMqB;AACnB,QAAI,SAAS,sBAAuB,QAAO;AAE3C,QAAI,WAAW,QAAQ;AACrB,WAAK,KAAK,EAAE,SAAS,OAAO,OAAO,YAAY,GAAG,GAAG;AACrD,aAAO;AAAA,IACT;AAEA,UAAM,OAAO,MAAM,KAAK,GAAG;AAC3B,QAAI,MAAM,QAAQ,KAAK,MAAM,KAAM,KAAK,OAAqB,SAAS,KAAK;AACzE,WAAK,KAAK,EAAE,SAAS,OAAO,OAAO,oBAAoB,GAAG,GAAG;AAC7D,aAAO;AAAA,IACT;AAEA,UAAM,WAAW,gBAAgB,IAAI;AACrC,QAAI,SAAS,WAAW,GAAG;AACzB,WAAK,KAAK,EAAE,SAAS,OAAO,OAAO,UAAU,GAAG,GAAG;AACnD,aAAO;AAAA,IACT;AAEA,UAAM,SAAS,WAAW,WAAW,QAAQ;AAC7C,SAAK,KAAK,EAAE,SAAS,MAAM,GAAG,OAAO,CAAC;AACtC,WAAO;AAAA,EACT;AACF;",
6
- "names": []
4
+ "sourcesContent": ["/**\n * routes/qq-events.ts \u2014 MC \u4E8B\u4EF6\u5165\u7AD9\n *\n * POST /api/sfmc/qq/events \u2014 \u5355\u6761\u6216 { events: [...] }\uFF08\u2264100\uFF09\n * GET/POST /api/sfmc/qq/events/settings \u2014 \u67E5\u8BE2\u6216\u4FEE\u6539\u63A8\u9001\u5F00\u5173\u548C\u805A\u5408\u95F4\u9694\n */\n\nimport type { QqEventsAggregator, QqEventPayload, ResolvedQqEventsConfig } from \"../domain/qq-events.js\";\nimport { normalizeEventPayload } from \"../domain/qq-events.js\";\n\ninterface Deps {\n body: (req: import(\"http\").IncomingMessage) => Promise<Record<string, unknown>>;\n json: (res: import(\"http\").ServerResponse, data: Record<string, unknown>, status?: number) => void;\n aggregator: QqEventsAggregator;\n getSettings: () => ResolvedQqEventsConfig;\n setSettings: (patch: Partial<ResolvedQqEventsConfig>) => ResolvedQqEventsConfig;\n isAdmin: (openid: string, asGroupAdmin: boolean) => boolean;\n}\n\nconst SWITCHES = [\"enabled\", \"join\", \"leave\", \"death\", \"crash\", \"start\", \"stop\"] as const;\n\nfunction collectPayloads(data: Record<string, unknown>): QqEventPayload[] {\n if (Array.isArray(data.events)) {\n return (data.events as unknown[])\n .map((x) => normalizeEventPayload(x))\n .filter((x): x is QqEventPayload => x != null);\n }\n // \u5355\u6761\uFF1A\u9876\u5C42\u5373\u4E8B\u4EF6\n const one = normalizeEventPayload(data);\n return one ? [one] : [];\n}\n\nfunction createQqEventsRoutes({ body, json, aggregator, getSettings, setSettings, isAdmin }: Deps) {\n return async function handle({\n path,\n method,\n req,\n res,\n }: {\n path: string;\n method: string;\n params: URLSearchParams;\n req: import(\"http\").IncomingMessage;\n res: import(\"http\").ServerResponse;\n }): Promise<boolean> {\n if (path === \"/api/sfmc/qq/events/settings\") {\n if (method === \"GET\") {\n json(res, { success: true, settings: getSettings() });\n return true;\n }\n if (method === \"POST\") {\n const data = await body(req);\n const openid = typeof data.openid === \"string\" ? data.openid.trim() : \"\";\n if (!isAdmin(openid, data.as_group_admin === true)) {\n json(res, { success: false, error: \"not_admin\" }, 403);\n return true;\n }\n const field = data.field;\n if (field === \"window_sec\") {\n const seconds = data.value;\n if (typeof seconds !== \"number\" || !Number.isInteger(seconds) || seconds < 5 || seconds > 600) {\n json(res, { success: false, error: \"invalid_setting\" }, 400);\n return true;\n }\n json(res, { success: true, settings: setSettings({ window_sec: seconds }) });\n return true;\n }\n if (!SWITCHES.some((key) => key === field) || typeof data.value !== \"boolean\") {\n json(res, { success: false, error: \"invalid_setting\" }, 400);\n return true;\n }\n const settings = setSettings({ [field as (typeof SWITCHES)[number]]: data.value });\n json(res, { success: true, settings });\n return true;\n }\n json(res, { success: false, error: \"not_found\" }, 404);\n return true;\n }\n if (path !== \"/api/sfmc/qq/events\") return false;\n\n if (method !== \"POST\") {\n json(res, { success: false, error: \"not_found\" }, 404);\n return true;\n }\n\n const data = await body(req);\n if (Array.isArray(data.events) && (data.events as unknown[]).length > 100) {\n json(res, { success: false, error: \"too many requests\" }, 413);\n return true;\n }\n\n const payloads = collectPayloads(data);\n if (payloads.length === 0) {\n json(res, { success: false, error: \"invalid\" }, 400);\n return true;\n }\n\n const result = aggregator.ingestMany(payloads);\n json(res, { success: true, ...result });\n return true;\n };\n}\n\nexport { createQqEventsRoutes };\n"],
5
+ "mappings": "AAQA,SAAS,6BAA6B;AAWtC,MAAM,WAAW,CAAC,WAAW,QAAQ,SAAS,SAAS,SAAS,SAAS,MAAM;AAE/E,SAAS,gBAAgB,MAAiD;AACxE,MAAI,MAAM,QAAQ,KAAK,MAAM,GAAG;AAC9B,WAAQ,KAAK,OACV,IAAI,CAAC,MAAM,sBAAsB,CAAC,CAAC,EACnC,OAAO,CAAC,MAA2B,KAAK,IAAI;AAAA,EACjD;AAEA,QAAM,MAAM,sBAAsB,IAAI;AACtC,SAAO,MAAM,CAAC,GAAG,IAAI,CAAC;AACxB;AAEA,SAAS,qBAAqB,EAAE,MAAM,MAAM,YAAY,aAAa,aAAa,QAAQ,GAAS;AACjG,SAAO,eAAe,OAAO;AAAA,IAC3B;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,GAMqB;AACnB,QAAI,SAAS,gCAAgC;AAC3C,UAAI,WAAW,OAAO;AACpB,aAAK,KAAK,EAAE,SAAS,MAAM,UAAU,YAAY,EAAE,CAAC;AACpD,eAAO;AAAA,MACT;AACA,UAAI,WAAW,QAAQ;AACrB,cAAMA,QAAO,MAAM,KAAK,GAAG;AAC3B,cAAM,SAAS,OAAOA,MAAK,WAAW,WAAWA,MAAK,OAAO,KAAK,IAAI;AACtE,YAAI,CAAC,QAAQ,QAAQA,MAAK,mBAAmB,IAAI,GAAG;AAClD,eAAK,KAAK,EAAE,SAAS,OAAO,OAAO,YAAY,GAAG,GAAG;AACrD,iBAAO;AAAA,QACT;AACA,cAAM,QAAQA,MAAK;AACnB,YAAI,UAAU,cAAc;AAC1B,gBAAM,UAAUA,MAAK;AACrB,cAAI,OAAO,YAAY,YAAY,CAAC,OAAO,UAAU,OAAO,KAAK,UAAU,KAAK,UAAU,KAAK;AAC7F,iBAAK,KAAK,EAAE,SAAS,OAAO,OAAO,kBAAkB,GAAG,GAAG;AAC3D,mBAAO;AAAA,UACT;AACA,eAAK,KAAK,EAAE,SAAS,MAAM,UAAU,YAAY,EAAE,YAAY,QAAQ,CAAC,EAAE,CAAC;AAC3E,iBAAO;AAAA,QACT;AACA,YAAI,CAAC,SAAS,KAAK,CAAC,QAAQ,QAAQ,KAAK,KAAK,OAAOA,MAAK,UAAU,WAAW;AAC7E,eAAK,KAAK,EAAE,SAAS,OAAO,OAAO,kBAAkB,GAAG,GAAG;AAC3D,iBAAO;AAAA,QACT;AACA,cAAM,WAAW,YAAY,EAAE,CAAC,KAAkC,GAAGA,MAAK,MAAM,CAAC;AACjF,aAAK,KAAK,EAAE,SAAS,MAAM,SAAS,CAAC;AACrC,eAAO;AAAA,MACT;AACA,WAAK,KAAK,EAAE,SAAS,OAAO,OAAO,YAAY,GAAG,GAAG;AACrD,aAAO;AAAA,IACT;AACA,QAAI,SAAS,sBAAuB,QAAO;AAE3C,QAAI,WAAW,QAAQ;AACrB,WAAK,KAAK,EAAE,SAAS,OAAO,OAAO,YAAY,GAAG,GAAG;AACrD,aAAO;AAAA,IACT;AAEA,UAAM,OAAO,MAAM,KAAK,GAAG;AAC3B,QAAI,MAAM,QAAQ,KAAK,MAAM,KAAM,KAAK,OAAqB,SAAS,KAAK;AACzE,WAAK,KAAK,EAAE,SAAS,OAAO,OAAO,oBAAoB,GAAG,GAAG;AAC7D,aAAO;AAAA,IACT;AAEA,UAAM,WAAW,gBAAgB,IAAI;AACrC,QAAI,SAAS,WAAW,GAAG;AACzB,WAAK,KAAK,EAAE,SAAS,OAAO,OAAO,UAAU,GAAG,GAAG;AACnD,aAAO;AAAA,IACT;AAEA,UAAM,SAAS,WAAW,WAAW,QAAQ;AAC7C,SAAK,KAAK,EAAE,SAAS,MAAM,GAAG,OAAO,CAAC;AACtC,WAAO;AAAA,EACT;AACF;",
6
+ "names": ["data"]
7
7
  }
@@ -116,7 +116,7 @@ function createQqJoinRoutes({ query, body, json, getAdminOpenids, getJoinFlags,
116
116
  status,
117
117
  auto_approved: autoApproved,
118
118
  admin_notify: !autoApproved && admins.length > 0,
119
- note: autoApproved ? "\u5DF2\u5173\u95ED\u5BA1\u6279\uFF0C\u7533\u8BF7\u5DF2\u81EA\u52A8\u901A\u8FC7\uFF0C\u7B49\u5F85 BDS \u5199\u5165\u767D\u540D\u5355" : admins.length === 0 ? "\u672A\u914D\u7F6E qq_admin_openids\uFF0C\u65E0\u6CD5\u901A\u77E5\u7BA1\u7406\u5458" : void 0
119
+ note: autoApproved ? "\u5DF2\u5173\u95ED\u5BA1\u6279\uFF0C\u7533\u8BF7\u5DF2\u81EA\u52A8\u901A\u8FC7\uFF0C\u7B49\u5F85 BDS \u5199\u5165\u767D\u540D\u5355" : admins.length === 0 ? "\u672A\u914D\u7F6E official.admin_openids\uFF0C\u65E0\u6CD5\u901A\u77E5\u7BA1\u7406\u5458" : void 0
120
120
  });
121
121
  return true;
122
122
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/routes/qq-join.ts"],
4
- "sourcesContent": ["/**\n * routes/qq-join.ts \u2014 \u5165\u670D\u5BA1\u6279\u72B6\u6001\u673A + BDS \u751F\u6548\u961F\u5217\uFF08\u4E0D\u5199 allowlist \u6587\u4EF6\uFF09\n *\n * GET /api/sfmc/qq/join/settings\n * POST /api/sfmc/qq/join/settings\n * POST /api/sfmc/qq/join/request\n * POST /api/sfmc/qq/join/decide\n * GET /api/sfmc/qq/join/pending\n * GET /api/sfmc/qq/join/apply-queue\n * POST /api/sfmc/qq/join/applied\n * POST /api/sfmc/qq/admin/kick\n * GET /api/sfmc/qq/admin/action-queue\n * POST /api/sfmc/qq/admin/action-done\n */\n\nimport { randomBytes } from \"node:crypto\";\nimport { SQL } from \"sql-template-strings\";\nimport type { QueryFn } from \"../lib/sqlite.js\";\n\nexport type JoinFeatureFlags = {\n /** \u5165\u670D\u767D\u540D\u5355\u603B\u5F00\u5173 */\n allowlistEnabled: boolean;\n /** \u662F\u5426\u9700\u8981\u7BA1\u7406\u5458\u5BA1\u6279 */\n requireApproval: boolean;\n /**\n * \u662F\u5426\u5C06 QQ \u7FA4\u4E3B/\u7FA4\u7BA1\u7406\u5458\u89C6\u4F5C SFMC \u7BA1\u7406\u5458\u3002\n * \u4EC5 configs/qq_link.json \u53EF\u6539\uFF0CAPI/\u7FA4\u804A\u4E0D\u53EF\u5199\u3002\n */\n treatGroupAdminsAsAdmins: boolean;\n};\n\ninterface Deps {\n query: QueryFn;\n body: (req: import(\"http\").IncomingMessage) => Promise<Record<string, unknown>>;\n json: (res: import(\"http\").ServerResponse, data: Record<string, unknown>, status?: number) => void;\n /** \u5F53\u524D\u7BA1\u7406\u5458 openid \u5217\u8868\uFF08\u6765\u81EA qq_config\uFF09 */\n getAdminOpenids: () => string[];\n /** \u8BFB\u53D6\u5165\u670D\u5F00\u5173\uFF08\u5E94\u8BFB\u6700\u65B0 qq_config\uFF09 */\n getJoinFlags: () => JoinFeatureFlags;\n /** \u5199\u56DE\u5165\u670D\u5F00\u5173\uFF08\u6301\u4E45\u5316 + \u5185\u5B58\uFF09 */\n setJoinFlags: (partial: Partial<JoinFeatureFlags>) => JoinFeatureFlags;\n}\n\nfunction newId(prefix: string): string {\n return `${prefix}_${Date.now().toString(36)}_${randomBytes(4).toString(\"hex\")}`;\n}\n\nfunction isAdmin(getAdminOpenids: () => string[], openid: string): boolean {\n const list = getAdminOpenids();\n if (!openid || list.length === 0) return false;\n return list.includes(openid);\n}\n\n/** openid \u5728\u540D\u5355\u5185\uFF0C\u6216\uFF08\u5F00\u5173\u5F00\u4E14 bridge \u58F0\u660E as_group_admin\uFF09 */\nfunction authorizeAdmin(\n getAdminOpenids: () => string[],\n getJoinFlags: () => JoinFeatureFlags,\n openid: string,\n data: Record<string, unknown>\n): boolean {\n if (isAdmin(getAdminOpenids, openid)) return true;\n const asGroup =\n data.as_group_admin === true ||\n data.as_group_admin === \"true\" ||\n data.is_group_admin === true ||\n data.is_group_admin === \"true\";\n return asGroup && getJoinFlags().treatGroupAdminsAsAdmins === true;\n}\n\nfunction createQqJoinRoutes({ query, body, json, getAdminOpenids, getJoinFlags, setJoinFlags }: Deps) {\n return async function handle({\n path,\n method,\n params,\n req,\n res,\n }: {\n path: string;\n method: string;\n params: URLSearchParams;\n req: import(\"http\").IncomingMessage;\n res: import(\"http\").ServerResponse;\n }): Promise<boolean> {\n const isJoin = path.startsWith(\"/api/sfmc/qq/join\");\n const isAdminPath = path.startsWith(\"/api/sfmc/qq/admin\");\n if (!isJoin && !isAdminPath) return false;\n\n // \u2500\u2500 \u5165\u670D\u5F00\u5173\uFF08\u6587\u4EF6 + \u673A\u5668\u4EBA\u53EF\u6539\u767D\u540D\u5355/\u5BA1\u6279\uFF1B\u7FA4\u7BA1\u5F00\u5173\u53EA\u8BFB\uFF09\u2500\u2500\n if (path === \"/api/sfmc/qq/join/settings\" && method === \"GET\") {\n const flags = getJoinFlags();\n json(res, {\n success: true,\n allowlist_enabled: flags.allowlistEnabled,\n require_approval: flags.requireApproval,\n treat_group_admins_as_admins: flags.treatGroupAdminsAsAdmins,\n });\n return true;\n }\n\n if (path === \"/api/sfmc/qq/join/settings\" && method === \"POST\") {\n const data = await body(req);\n const openid = String(data.openid ?? data.decided_by ?? \"\").trim();\n if (\n Object.prototype.hasOwnProperty.call(data, \"treat_group_admins_as_admins\") ||\n Object.prototype.hasOwnProperty.call(data, \"treatGroupAdminsAsAdmins\")\n ) {\n json(\n res,\n {\n success: false,\n error: \"immutable_field\",\n note: \"treat_group_admins_as_admins \u53EA\u80FD\u6539 configs/qq_link.json\uFF0C\u7FA4\u804A\u4E0D\u53EF\u6539\",\n },\n 400\n );\n return true;\n }\n if (!authorizeAdmin(getAdminOpenids, getJoinFlags, openid, data)) {\n json(res, { success: false, error: \"not_admin\" }, 403);\n return true;\n }\n const partial: Partial<JoinFeatureFlags> = {};\n if (Object.prototype.hasOwnProperty.call(data, \"allowlist_enabled\")) {\n partial.allowlistEnabled = data.allowlist_enabled === true || data.allowlist_enabled === \"true\";\n }\n if (Object.prototype.hasOwnProperty.call(data, \"require_approval\")) {\n partial.requireApproval = data.require_approval === true || data.require_approval === \"true\";\n }\n if (partial.allowlistEnabled === undefined && partial.requireApproval === undefined) {\n json(res, { success: false, error: \"no_settings\" }, 400);\n return true;\n }\n const flags = setJoinFlags(partial);\n json(res, {\n success: true,\n allowlist_enabled: flags.allowlistEnabled,\n require_approval: flags.requireApproval,\n treat_group_admins_as_admins: flags.treatGroupAdminsAsAdmins,\n });\n return true;\n }\n\n // \u2500\u2500 \u5165\u670D\u7533\u8BF7 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n if (path === \"/api/sfmc/qq/join/request\" && method === \"POST\") {\n const flags = getJoinFlags();\n if (!flags.allowlistEnabled) {\n json(res, { success: false, error: \"join_allowlist_disabled\" }, 403);\n return true;\n }\n const data = await body(req);\n const openid = String(data.openid ?? data.applicant_openid ?? \"\").trim();\n const playerName = String(data.player_name ?? data.name ?? \"\").trim();\n const backend = String(data.qq_backend ?? \"official\").trim() || \"official\";\n if (!openid) {\n json(res, { success: false, error: \"openid_required\" }, 400);\n return true;\n }\n if (!playerName || playerName.length > 32) {\n json(res, { success: false, error: \"player_name_required\" }, 400);\n return true;\n }\n const id = newId(\"join\");\n const now = Date.now();\n const autoApproved = !flags.requireApproval;\n const status = autoApproved ? \"approved\" : \"pending\";\n if (autoApproved) {\n query(SQL`\n INSERT INTO sfmc_qq_join_requests\n (id, applicant_openid, player_name, status, qq_backend, created_at, decided_at, decided_by)\n VALUES (${id}, ${openid}, ${playerName}, ${status}, ${backend}, ${now}, ${now}, 'auto')\n `);\n } else {\n query(SQL`\n INSERT INTO sfmc_qq_join_requests\n (id, applicant_openid, player_name, status, qq_backend, created_at)\n VALUES (${id}, ${openid}, ${playerName}, ${status}, ${backend}, ${now})\n `);\n }\n const admins = getAdminOpenids();\n json(res, {\n success: true,\n id,\n player_name: playerName,\n status,\n auto_approved: autoApproved,\n admin_notify: !autoApproved && admins.length > 0,\n note: autoApproved\n ? \"\u5DF2\u5173\u95ED\u5BA1\u6279\uFF0C\u7533\u8BF7\u5DF2\u81EA\u52A8\u901A\u8FC7\uFF0C\u7B49\u5F85 BDS \u5199\u5165\u767D\u540D\u5355\"\n : admins.length === 0\n ? \"\u672A\u914D\u7F6E qq_admin_openids\uFF0C\u65E0\u6CD5\u901A\u77E5\u7BA1\u7406\u5458\"\n : undefined,\n });\n return true;\n }\n\n if (path === \"/api/sfmc/qq/join/decide\" && method === \"POST\") {\n const data = await body(req);\n const id = String(data.id ?? data.request_id ?? \"\").trim();\n const decision = String(data.decision ?? \"\").trim().toLowerCase();\n const decidedBy = String(data.decided_by ?? data.openid ?? \"\").trim();\n if (!id || (decision !== \"approve\" && decision !== \"reject\")) {\n json(res, { success: false, error: \"id_and_decision_required\" }, 400);\n return true;\n }\n if (!authorizeAdmin(getAdminOpenids, getJoinFlags, decidedBy, data)) {\n json(res, { success: false, error: \"not_admin\" }, 403);\n return true;\n }\n const rows = query(SQL`SELECT * FROM sfmc_qq_join_requests WHERE id = ${id} LIMIT 1`) as Array<\n Record<string, unknown>\n >;\n const row = rows[0];\n if (!row) {\n json(res, { success: false, error: \"not_found\" }, 404);\n return true;\n }\n if (String(row.status) !== \"pending\") {\n json(res, { success: false, error: \"already_decided\", status: row.status }, 409);\n return true;\n }\n const next = decision === \"approve\" ? \"approved\" : \"rejected\";\n const now = Date.now();\n query(SQL`\n UPDATE sfmc_qq_join_requests\n SET status = ${next}, decided_at = ${now}, decided_by = ${decidedBy}\n WHERE id = ${id}\n `);\n json(res, {\n success: true,\n id,\n status: next,\n player_name: row.player_name,\n applicant_openid: row.applicant_openid,\n });\n return true;\n }\n\n if (path === \"/api/sfmc/qq/join/pending\" && method === \"GET\") {\n const openid = String(params.get(\"openid\") ?? \"\").trim();\n if (openid) {\n const asGroup =\n params.get(\"as_group_admin\") === \"true\" || params.get(\"is_group_admin\") === \"true\";\n if (!authorizeAdmin(getAdminOpenids, getJoinFlags, openid, { as_group_admin: asGroup })) {\n json(res, { success: false, error: \"not_admin\" }, 403);\n return true;\n }\n }\n const rows = query(\n SQL`SELECT id, applicant_openid, player_name, status, created_at\n FROM sfmc_qq_join_requests WHERE status = 'pending'\n ORDER BY created_at ASC LIMIT 50`\n ) as Array<Record<string, unknown>>;\n json(res, { success: true, pending: rows });\n return true;\n }\n\n if (path === \"/api/sfmc/qq/join/apply-queue\" && method === \"GET\") {\n const flags = getJoinFlags();\n if (!flags.allowlistEnabled) {\n json(res, { success: true, queue: [], note: \"join_allowlist_disabled\" });\n return true;\n }\n const rows = query(\n SQL`SELECT id, player_name, applicant_openid, decided_at\n FROM sfmc_qq_join_requests WHERE status = 'approved'\n ORDER BY decided_at ASC LIMIT 32`\n ) as Array<Record<string, unknown>>;\n json(res, { success: true, queue: rows });\n return true;\n }\n\n if (path === \"/api/sfmc/qq/join/applied\" && method === \"POST\") {\n const data = await body(req);\n const id = String(data.id ?? \"\").trim();\n const ok = data.ok !== false && data.error == null;\n const err = String(data.error ?? \"\").slice(0, 200);\n if (!id) {\n json(res, { success: false, error: \"id_required\" }, 400);\n return true;\n }\n const now = Date.now();\n if (ok) {\n query(SQL`\n UPDATE sfmc_qq_join_requests\n SET status = 'applied', applied_at = ${now}, apply_error = NULL\n WHERE id = ${id} AND status = 'approved'\n `);\n } else {\n query(SQL`\n UPDATE sfmc_qq_join_requests\n SET status = 'apply_failed', applied_at = ${now}, apply_error = ${err}\n WHERE id = ${id} AND status = 'approved'\n `);\n }\n json(res, { success: true });\n return true;\n }\n\n // \u2500\u2500 \u8E22\u4EBA\u961F\u5217 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n if (path === \"/api/sfmc/qq/admin/kick\" && method === \"POST\") {\n const data = await body(req);\n const openid = String(data.openid ?? data.requested_by ?? \"\").trim();\n const target = String(data.target_name ?? data.name ?? \"\").trim();\n const reason = String(data.reason ?? \"QQ \u7BA1\u7406\u5458\u8E22\u51FA\").trim().slice(0, 80);\n if (!authorizeAdmin(getAdminOpenids, getJoinFlags, openid, data)) {\n json(res, { success: false, error: \"not_admin\" }, 403);\n return true;\n }\n if (!target) {\n json(res, { success: false, error: \"target_name_required\" }, 400);\n return true;\n }\n const id = newId(\"act\");\n const now = Date.now();\n query(SQL`\n INSERT INTO sfmc_qq_admin_actions\n (id, kind, target_name, reason, requested_by, status, created_at)\n VALUES (${id}, 'kick', ${target}, ${reason}, ${openid}, 'pending', ${now})\n `);\n json(res, { success: true, id });\n return true;\n }\n\n if (path === \"/api/sfmc/qq/admin/action-queue\" && method === \"GET\") {\n const rows = query(\n SQL`SELECT id, kind, target_name, reason, created_at\n FROM sfmc_qq_admin_actions WHERE status = 'pending'\n ORDER BY created_at ASC LIMIT 32`\n ) as Array<Record<string, unknown>>;\n json(res, { success: true, queue: rows });\n return true;\n }\n\n if (path === \"/api/sfmc/qq/admin/action-done\" && method === \"POST\") {\n const data = await body(req);\n const id = String(data.id ?? \"\").trim();\n const ok = data.ok !== false && data.error == null;\n const err = String(data.error ?? \"\").slice(0, 200);\n if (!id) {\n json(res, { success: false, error: \"id_required\" }, 400);\n return true;\n }\n const now = Date.now();\n const status = ok ? \"done\" : \"failed\";\n query(SQL`\n UPDATE sfmc_qq_admin_actions\n SET status = ${status}, applied_at = ${now}, apply_error = ${ok ? \"\" : err}\n WHERE id = ${id} AND status = 'pending'\n `);\n json(res, { success: true });\n return true;\n }\n\n json(res, { success: false, error: \"not_found\" }, 404);\n return true;\n };\n}\n\nexport { createQqJoinRoutes };\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * routes/qq-join.ts \u2014 \u5165\u670D\u5BA1\u6279\u72B6\u6001\u673A + BDS \u751F\u6548\u961F\u5217\uFF08\u4E0D\u5199 allowlist \u6587\u4EF6\uFF09\n *\n * GET /api/sfmc/qq/join/settings\n * POST /api/sfmc/qq/join/settings\n * POST /api/sfmc/qq/join/request\n * POST /api/sfmc/qq/join/decide\n * GET /api/sfmc/qq/join/pending\n * GET /api/sfmc/qq/join/apply-queue\n * POST /api/sfmc/qq/join/applied\n * POST /api/sfmc/qq/admin/kick\n * GET /api/sfmc/qq/admin/action-queue\n * POST /api/sfmc/qq/admin/action-done\n */\n\nimport { randomBytes } from \"node:crypto\";\nimport { SQL } from \"sql-template-strings\";\nimport type { QueryFn } from \"../lib/sqlite.js\";\n\nexport type JoinFeatureFlags = {\n /** \u5165\u670D\u767D\u540D\u5355\u603B\u5F00\u5173 */\n allowlistEnabled: boolean;\n /** \u662F\u5426\u9700\u8981\u7BA1\u7406\u5458\u5BA1\u6279 */\n requireApproval: boolean;\n /**\n * \u662F\u5426\u5C06 QQ \u7FA4\u4E3B/\u7FA4\u7BA1\u7406\u5458\u89C6\u4F5C SFMC \u7BA1\u7406\u5458\u3002\n * \u4EC5 configs/qq_link.json \u53EF\u6539\uFF0CAPI/\u7FA4\u804A\u4E0D\u53EF\u5199\u3002\n */\n treatGroupAdminsAsAdmins: boolean;\n};\n\ninterface Deps {\n query: QueryFn;\n body: (req: import(\"http\").IncomingMessage) => Promise<Record<string, unknown>>;\n json: (res: import(\"http\").ServerResponse, data: Record<string, unknown>, status?: number) => void;\n /** \u5F53\u524D\u7BA1\u7406\u5458 openid \u5217\u8868\uFF08\u6765\u81EA qq_config\uFF09 */\n getAdminOpenids: () => string[];\n /** \u8BFB\u53D6\u5165\u670D\u5F00\u5173\uFF08\u5E94\u8BFB\u6700\u65B0 qq_config\uFF09 */\n getJoinFlags: () => JoinFeatureFlags;\n /** \u5199\u56DE\u5165\u670D\u5F00\u5173\uFF08\u6301\u4E45\u5316 + \u5185\u5B58\uFF09 */\n setJoinFlags: (partial: Partial<JoinFeatureFlags>) => JoinFeatureFlags;\n}\n\nfunction newId(prefix: string): string {\n return `${prefix}_${Date.now().toString(36)}_${randomBytes(4).toString(\"hex\")}`;\n}\n\nfunction isAdmin(getAdminOpenids: () => string[], openid: string): boolean {\n const list = getAdminOpenids();\n if (!openid || list.length === 0) return false;\n return list.includes(openid);\n}\n\n/** openid \u5728\u540D\u5355\u5185\uFF0C\u6216\uFF08\u5F00\u5173\u5F00\u4E14 bridge \u58F0\u660E as_group_admin\uFF09 */\nfunction authorizeAdmin(\n getAdminOpenids: () => string[],\n getJoinFlags: () => JoinFeatureFlags,\n openid: string,\n data: Record<string, unknown>\n): boolean {\n if (isAdmin(getAdminOpenids, openid)) return true;\n const asGroup =\n data.as_group_admin === true ||\n data.as_group_admin === \"true\" ||\n data.is_group_admin === true ||\n data.is_group_admin === \"true\";\n return asGroup && getJoinFlags().treatGroupAdminsAsAdmins === true;\n}\n\nfunction createQqJoinRoutes({ query, body, json, getAdminOpenids, getJoinFlags, setJoinFlags }: Deps) {\n return async function handle({\n path,\n method,\n params,\n req,\n res,\n }: {\n path: string;\n method: string;\n params: URLSearchParams;\n req: import(\"http\").IncomingMessage;\n res: import(\"http\").ServerResponse;\n }): Promise<boolean> {\n const isJoin = path.startsWith(\"/api/sfmc/qq/join\");\n const isAdminPath = path.startsWith(\"/api/sfmc/qq/admin\");\n if (!isJoin && !isAdminPath) return false;\n\n // \u2500\u2500 \u5165\u670D\u5F00\u5173\uFF08\u6587\u4EF6 + \u673A\u5668\u4EBA\u53EF\u6539\u767D\u540D\u5355/\u5BA1\u6279\uFF1B\u7FA4\u7BA1\u5F00\u5173\u53EA\u8BFB\uFF09\u2500\u2500\n if (path === \"/api/sfmc/qq/join/settings\" && method === \"GET\") {\n const flags = getJoinFlags();\n json(res, {\n success: true,\n allowlist_enabled: flags.allowlistEnabled,\n require_approval: flags.requireApproval,\n treat_group_admins_as_admins: flags.treatGroupAdminsAsAdmins,\n });\n return true;\n }\n\n if (path === \"/api/sfmc/qq/join/settings\" && method === \"POST\") {\n const data = await body(req);\n const openid = String(data.openid ?? data.decided_by ?? \"\").trim();\n if (\n Object.prototype.hasOwnProperty.call(data, \"treat_group_admins_as_admins\") ||\n Object.prototype.hasOwnProperty.call(data, \"treatGroupAdminsAsAdmins\")\n ) {\n json(\n res,\n {\n success: false,\n error: \"immutable_field\",\n note: \"treat_group_admins_as_admins \u53EA\u80FD\u6539 configs/qq_link.json\uFF0C\u7FA4\u804A\u4E0D\u53EF\u6539\",\n },\n 400\n );\n return true;\n }\n if (!authorizeAdmin(getAdminOpenids, getJoinFlags, openid, data)) {\n json(res, { success: false, error: \"not_admin\" }, 403);\n return true;\n }\n const partial: Partial<JoinFeatureFlags> = {};\n if (Object.prototype.hasOwnProperty.call(data, \"allowlist_enabled\")) {\n partial.allowlistEnabled = data.allowlist_enabled === true || data.allowlist_enabled === \"true\";\n }\n if (Object.prototype.hasOwnProperty.call(data, \"require_approval\")) {\n partial.requireApproval = data.require_approval === true || data.require_approval === \"true\";\n }\n if (partial.allowlistEnabled === undefined && partial.requireApproval === undefined) {\n json(res, { success: false, error: \"no_settings\" }, 400);\n return true;\n }\n const flags = setJoinFlags(partial);\n json(res, {\n success: true,\n allowlist_enabled: flags.allowlistEnabled,\n require_approval: flags.requireApproval,\n treat_group_admins_as_admins: flags.treatGroupAdminsAsAdmins,\n });\n return true;\n }\n\n // \u2500\u2500 \u5165\u670D\u7533\u8BF7 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n if (path === \"/api/sfmc/qq/join/request\" && method === \"POST\") {\n const flags = getJoinFlags();\n if (!flags.allowlistEnabled) {\n json(res, { success: false, error: \"join_allowlist_disabled\" }, 403);\n return true;\n }\n const data = await body(req);\n const openid = String(data.openid ?? data.applicant_openid ?? \"\").trim();\n const playerName = String(data.player_name ?? data.name ?? \"\").trim();\n const backend = String(data.qq_backend ?? \"official\").trim() || \"official\";\n if (!openid) {\n json(res, { success: false, error: \"openid_required\" }, 400);\n return true;\n }\n if (!playerName || playerName.length > 32) {\n json(res, { success: false, error: \"player_name_required\" }, 400);\n return true;\n }\n const id = newId(\"join\");\n const now = Date.now();\n const autoApproved = !flags.requireApproval;\n const status = autoApproved ? \"approved\" : \"pending\";\n if (autoApproved) {\n query(SQL`\n INSERT INTO sfmc_qq_join_requests\n (id, applicant_openid, player_name, status, qq_backend, created_at, decided_at, decided_by)\n VALUES (${id}, ${openid}, ${playerName}, ${status}, ${backend}, ${now}, ${now}, 'auto')\n `);\n } else {\n query(SQL`\n INSERT INTO sfmc_qq_join_requests\n (id, applicant_openid, player_name, status, qq_backend, created_at)\n VALUES (${id}, ${openid}, ${playerName}, ${status}, ${backend}, ${now})\n `);\n }\n const admins = getAdminOpenids();\n json(res, {\n success: true,\n id,\n player_name: playerName,\n status,\n auto_approved: autoApproved,\n admin_notify: !autoApproved && admins.length > 0,\n note: autoApproved\n ? \"\u5DF2\u5173\u95ED\u5BA1\u6279\uFF0C\u7533\u8BF7\u5DF2\u81EA\u52A8\u901A\u8FC7\uFF0C\u7B49\u5F85 BDS \u5199\u5165\u767D\u540D\u5355\"\n : admins.length === 0\n ? \"\u672A\u914D\u7F6E official.admin_openids\uFF0C\u65E0\u6CD5\u901A\u77E5\u7BA1\u7406\u5458\"\n : undefined,\n });\n return true;\n }\n\n if (path === \"/api/sfmc/qq/join/decide\" && method === \"POST\") {\n const data = await body(req);\n const id = String(data.id ?? data.request_id ?? \"\").trim();\n const decision = String(data.decision ?? \"\").trim().toLowerCase();\n const decidedBy = String(data.decided_by ?? data.openid ?? \"\").trim();\n if (!id || (decision !== \"approve\" && decision !== \"reject\")) {\n json(res, { success: false, error: \"id_and_decision_required\" }, 400);\n return true;\n }\n if (!authorizeAdmin(getAdminOpenids, getJoinFlags, decidedBy, data)) {\n json(res, { success: false, error: \"not_admin\" }, 403);\n return true;\n }\n const rows = query(SQL`SELECT * FROM sfmc_qq_join_requests WHERE id = ${id} LIMIT 1`) as Array<\n Record<string, unknown>\n >;\n const row = rows[0];\n if (!row) {\n json(res, { success: false, error: \"not_found\" }, 404);\n return true;\n }\n if (String(row.status) !== \"pending\") {\n json(res, { success: false, error: \"already_decided\", status: row.status }, 409);\n return true;\n }\n const next = decision === \"approve\" ? \"approved\" : \"rejected\";\n const now = Date.now();\n query(SQL`\n UPDATE sfmc_qq_join_requests\n SET status = ${next}, decided_at = ${now}, decided_by = ${decidedBy}\n WHERE id = ${id}\n `);\n json(res, {\n success: true,\n id,\n status: next,\n player_name: row.player_name,\n applicant_openid: row.applicant_openid,\n });\n return true;\n }\n\n if (path === \"/api/sfmc/qq/join/pending\" && method === \"GET\") {\n const openid = String(params.get(\"openid\") ?? \"\").trim();\n if (openid) {\n const asGroup =\n params.get(\"as_group_admin\") === \"true\" || params.get(\"is_group_admin\") === \"true\";\n if (!authorizeAdmin(getAdminOpenids, getJoinFlags, openid, { as_group_admin: asGroup })) {\n json(res, { success: false, error: \"not_admin\" }, 403);\n return true;\n }\n }\n const rows = query(\n SQL`SELECT id, applicant_openid, player_name, status, created_at\n FROM sfmc_qq_join_requests WHERE status = 'pending'\n ORDER BY created_at ASC LIMIT 50`\n ) as Array<Record<string, unknown>>;\n json(res, { success: true, pending: rows });\n return true;\n }\n\n if (path === \"/api/sfmc/qq/join/apply-queue\" && method === \"GET\") {\n const flags = getJoinFlags();\n if (!flags.allowlistEnabled) {\n json(res, { success: true, queue: [], note: \"join_allowlist_disabled\" });\n return true;\n }\n const rows = query(\n SQL`SELECT id, player_name, applicant_openid, decided_at\n FROM sfmc_qq_join_requests WHERE status = 'approved'\n ORDER BY decided_at ASC LIMIT 32`\n ) as Array<Record<string, unknown>>;\n json(res, { success: true, queue: rows });\n return true;\n }\n\n if (path === \"/api/sfmc/qq/join/applied\" && method === \"POST\") {\n const data = await body(req);\n const id = String(data.id ?? \"\").trim();\n const ok = data.ok !== false && data.error == null;\n const err = String(data.error ?? \"\").slice(0, 200);\n if (!id) {\n json(res, { success: false, error: \"id_required\" }, 400);\n return true;\n }\n const now = Date.now();\n if (ok) {\n query(SQL`\n UPDATE sfmc_qq_join_requests\n SET status = 'applied', applied_at = ${now}, apply_error = NULL\n WHERE id = ${id} AND status = 'approved'\n `);\n } else {\n query(SQL`\n UPDATE sfmc_qq_join_requests\n SET status = 'apply_failed', applied_at = ${now}, apply_error = ${err}\n WHERE id = ${id} AND status = 'approved'\n `);\n }\n json(res, { success: true });\n return true;\n }\n\n // \u2500\u2500 \u8E22\u4EBA\u961F\u5217 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n if (path === \"/api/sfmc/qq/admin/kick\" && method === \"POST\") {\n const data = await body(req);\n const openid = String(data.openid ?? data.requested_by ?? \"\").trim();\n const target = String(data.target_name ?? data.name ?? \"\").trim();\n const reason = String(data.reason ?? \"QQ \u7BA1\u7406\u5458\u8E22\u51FA\").trim().slice(0, 80);\n if (!authorizeAdmin(getAdminOpenids, getJoinFlags, openid, data)) {\n json(res, { success: false, error: \"not_admin\" }, 403);\n return true;\n }\n if (!target) {\n json(res, { success: false, error: \"target_name_required\" }, 400);\n return true;\n }\n const id = newId(\"act\");\n const now = Date.now();\n query(SQL`\n INSERT INTO sfmc_qq_admin_actions\n (id, kind, target_name, reason, requested_by, status, created_at)\n VALUES (${id}, 'kick', ${target}, ${reason}, ${openid}, 'pending', ${now})\n `);\n json(res, { success: true, id });\n return true;\n }\n\n if (path === \"/api/sfmc/qq/admin/action-queue\" && method === \"GET\") {\n const rows = query(\n SQL`SELECT id, kind, target_name, reason, created_at\n FROM sfmc_qq_admin_actions WHERE status = 'pending'\n ORDER BY created_at ASC LIMIT 32`\n ) as Array<Record<string, unknown>>;\n json(res, { success: true, queue: rows });\n return true;\n }\n\n if (path === \"/api/sfmc/qq/admin/action-done\" && method === \"POST\") {\n const data = await body(req);\n const id = String(data.id ?? \"\").trim();\n const ok = data.ok !== false && data.error == null;\n const err = String(data.error ?? \"\").slice(0, 200);\n if (!id) {\n json(res, { success: false, error: \"id_required\" }, 400);\n return true;\n }\n const now = Date.now();\n const status = ok ? \"done\" : \"failed\";\n query(SQL`\n UPDATE sfmc_qq_admin_actions\n SET status = ${status}, applied_at = ${now}, apply_error = ${ok ? \"\" : err}\n WHERE id = ${id} AND status = 'pending'\n `);\n json(res, { success: true });\n return true;\n }\n\n json(res, { success: false, error: \"not_found\" }, 404);\n return true;\n };\n}\n\nexport { createQqJoinRoutes };\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,19 +1,11 @@
1
- /**
2
- * routes/status.ts — 只读运维摘要(供 QQ status/online)
3
- *
4
- * GET /api/sfmc/status — 公开,无需 Bearer
5
- */
6
- import type { QueryFn } from "../lib/sqlite.js";
1
+ /** GET /api/sfmc/status 与 BDS 实时快照入口。 */
7
2
  import { type SystemStatusSnapshot } from "../domain/system-status.js";
8
3
  import { type RouteFactory } from "./_shared.js";
9
- /** 玩家行新鲜度阈值:超过则视为离线快照过期 */
10
- declare const FRESH_MS: number;
4
+ declare const FRESH_MS = 60000;
11
5
  interface Deps {
12
- query: QueryFn;
13
- /** 测试可注入,避免真实探活 */
14
6
  collectSystem?: (projectRoot: string) => Promise<SystemStatusSnapshot>;
15
7
  projectRoot?: string;
16
8
  }
17
- declare function createStatusRoutes({ query, collectSystem, projectRoot }: Deps): ReturnType<RouteFactory>;
9
+ declare function createStatusRoutes({ collectSystem, projectRoot }: Deps): ReturnType<RouteFactory>;
18
10
  export { createStatusRoutes, FRESH_MS };
19
11
  //# sourceMappingURL=status.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/routes/status.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAEhD,OAAO,EAAuB,KAAK,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAC5F,OAAO,EAAQ,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAEvD,2BAA2B;AAC3B,QAAA,MAAM,QAAQ,QAAgB,CAAC;AAE/B,UAAU,IAAI;IACZ,KAAK,EAAE,OAAO,CAAC;IACf,mBAAmB;IACnB,aAAa,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,oBAAoB,CAAC,CAAC;IACvE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,iBAAS,kBAAkB,CAAC,EAAE,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,EAAE,IAAI,GAAG,UAAU,CAAC,YAAY,CAAC,CA0EjG;AAED,OAAO,EAAE,kBAAkB,EAAE,QAAQ,EAAE,CAAC"}
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/routes/status.ts"],"names":[],"mappings":"AAAA,yCAAyC;AAEzC,OAAO,EAAuB,KAAK,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAE5F,OAAO,EAAc,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAE7D,QAAA,MAAM,QAAQ,QAAS,CAAC;AAQxB,UAAU,IAAI;IACZ,aAAa,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,oBAAoB,CAAC,CAAC;IACvE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,iBAAS,kBAAkB,CAAC,EAAE,aAAa,EAAE,WAAW,EAAE,EAAE,IAAI,GAAG,UAAU,CAAC,YAAY,CAAC,CAmE1F;AAED,OAAO,EAAE,kBAAkB,EAAE,QAAQ,EAAE,CAAC"}