@sfmc-bds/db-server 0.2.1 → 0.2.3

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.
@@ -2,8 +2,8 @@
2
2
  * domain/bridge.ts — MC → QQ 桥接(双后端)
3
3
  *
4
4
  * 数据流:
5
- * SAPI ──POST──→ db-server:3001/api/sfmc/messages
6
- * └─ 写库 + forwardToQQBridge()
5
+ * 游戏发言 ──db.tx 插入──→ afterCommit 按频道 forward_to_qq 调用 forwardToQQBridge()
6
+ * QQ 入站 ──POST──→ /api/sfmc/messages,写入消息来源为 QQ 的频道(不回推群)
7
7
  * ├─ llbot → LLBot:3004/send_group_msg
8
8
  * └─ official → OpenAPI /v2/groups/{openid}/messages
9
9
  *
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/domain/bridge.ts"],
4
- "sourcesContent": ["/**\n * domain/bridge.ts \u2014 MC \u2192 QQ \u6865\u63A5\uFF08\u53CC\u540E\u7AEF\uFF09\n *\n * \u6570\u636E\u6D41:\n * SAPI \u2500\u2500POST\u2500\u2500\u2192 db-server:3001/api/sfmc/messages\n * \u2514\u2500 \u5199\u5E93 + forwardToQQBridge()\n * \u251C\u2500 llbot \u2192 LLBot:3004/send_group_msg\n * \u2514\u2500 official \u2192 OpenAPI /v2/groups/{openid}/messages\n *\n * \u4E8B\u4EF6\u63A8\u9001\u590D\u7528 sendGroupOutbound()\uFF08\u6B63\u6587\u539F\u6837\uFF0C\u4E0D\u52A0\u804A\u5929\u524D\u7F00\uFF09\u3002\n * \u5931\u8D25\u4EC5 warn\uFF0C\u4E0D\u629B\u9519\u3001\u4E0D\u91CD\u8BD5\uFF08\u4E0E\u5386\u53F2 LLBot \u884C\u4E3A\u4E00\u81F4\uFF09\u3002\n */\n\nimport { request } from \"node:http\";\nimport {\n sendGroupTextMessage,\n type QqOfficialCredentials,\n} from \"@sfmc-bds/sdk/node/qq-official\";\nimport type { QQBackend } from \"@sfmc-bds/sdk/node/config\";\n\nimport { log } from \"../lib/log.js\";\n\ntype LLBotConfig = {\n host: string;\n port: number;\n token: string;\n groupId: string;\n};\n\nexport type OfficialOutboundConfig = {\n backend: \"official\";\n creds: QqOfficialCredentials;\n groupOpenid: string;\n};\n\nexport type LlbotOutboundConfig = {\n backend: \"llbot\";\n llbot: LLBotConfig;\n};\n\nexport type OutboundConfig = OfficialOutboundConfig | LlbotOutboundConfig;\n\n/**\n * \u6784\u9020 LLBot \u6865\u63A5\u914D\u7F6E\u5BF9\u8C61\u3002\n *\n * @param env \u73AF\u5883\u53D8\u91CF\u6216\u914D\u7F6E\u5B57\u5178\u3002\n * @returns \u89C4\u8303\u5316\u7684 LLBotConfig \u914D\u7F6E\u3002\n */\nexport function makeLLBotConfig(env: {\n LLBOT_HOST: string;\n LLBOT_PORT: number;\n LLBOT_TOKEN: string;\n QQ_GROUP_ID: string;\n}): LLBotConfig {\n return {\n host: env.LLBOT_HOST,\n port: env.LLBOT_PORT,\n token: env.LLBOT_TOKEN,\n groupId: env.QQ_GROUP_ID,\n };\n}\n\n/**\n * \u6839\u636E\u73AF\u5883\u53D8\u91CF\u4E0E\u914D\u7F6E\u7EC4\u88C5\u51FA\u7AD9\u914D\u7F6E\uFF08\u652F\u6301 official \u5B98\u65B9\u5F00\u653E\u5E73\u53F0\u4E0E llbot \u53CC\u540E\u7AEF\uFF09\u3002\n *\n * @param env \u73AF\u5883\u53D8\u91CF\u4E0E\u914D\u7F6E\u5B57\u5178\u3002\n * @returns \u89C4\u8303\u5316\u7684 OutboundConfig \u51FA\u7AD9\u914D\u7F6E\u3002\n */\nexport function makeOutboundConfig(env: {\n QQ_BACKEND: QQBackend;\n LLBOT_HOST: string;\n LLBOT_PORT: number;\n LLBOT_TOKEN: string;\n QQ_GROUP_ID: string;\n QQ_APP_ID: string;\n QQ_APP_SECRET: string;\n QQ_SANDBOX: boolean;\n QQ_GROUP_OPENID: string;\n}): OutboundConfig {\n\n if (env.QQ_BACKEND === \"llbot\") {\n return {\n backend: \"llbot\",\n llbot: makeLLBotConfig(env),\n };\n }\n return {\n backend: \"official\",\n creds: {\n appId: env.QQ_APP_ID,\n appSecret: env.QQ_APP_SECRET,\n sandbox: env.QQ_SANDBOX,\n },\n groupOpenid: env.QQ_GROUP_OPENID,\n };\n}\n\nfunction sendViaLlbot(config: LLBotConfig, text: string, logCtx: string): void {\n if (!config.groupId || config.groupId === \"0\") {\n log.warn(`QQ \u7FA4\u672A\u914D\u7F6E (llbot.group_id),\u8DF3\u8FC7 MC\u2192QQ (${logCtx})`);\n return;\n }\n\n const payload = JSON.stringify({\n group_id: parseInt(config.groupId, 10),\n message: [{ type: \"text\", data: { text } }],\n });\n\n const headers: Record<string, string | number> = {\n \"Content-Type\": \"application/json\",\n \"Content-Length\": Buffer.byteLength(payload),\n };\n if (config.token) {\n headers[\"Authorization\"] = `Bearer ${config.token}`;\n }\n\n const req = request(\n {\n hostname: config.host,\n port: config.port,\n path: \"/send_group_msg\",\n method: \"POST\",\n headers,\n },\n (res) => {\n let body = \"\";\n res.on(\"data\", (c) => (body += c));\n res.on(\"end\", () => {\n if (res.statusCode !== 200) {\n log.warn(\n `LLBot send_group_msg \u2192 ${res.statusCode}: ${String(body).slice(0, 200)} (${logCtx})`\n );\n }\n });\n }\n );\n req.on(\"error\", (err) => {\n log.warn(`LLBot \u4E0D\u53EF\u8FBE (${config.host}:${config.port}): ${err.message}`);\n });\n req.write(payload);\n req.end();\n}\n\nfunction sendViaOfficial(config: OfficialOutboundConfig, text: string, logCtx: string): void {\n if (!config.groupOpenid) {\n log.warn(`\u5B98\u65B9\u7FA4 openid \u672A\u914D\u7F6E (official.group_openid),\u8DF3\u8FC7 MC\u2192QQ (${logCtx})`);\n return;\n }\n if (!config.creds.appId || !config.creds.appSecret) {\n log.warn(`\u5B98\u65B9 AppID/Secret \u672A\u914D\u7F6E,\u8DF3\u8FC7 MC\u2192QQ (${logCtx})`);\n return;\n }\n\n void sendGroupTextMessage(config.creds, {\n groupOpenid: config.groupOpenid,\n content: text,\n }).then((result) => {\n if (!result.ok) {\n if (result.status === 400 && String(result.error).includes(\"40034105\")) {\n log.warn(\"\u5B98\u65B9\u7FA4\u4E3B\u52A8\u53D1\u6D88\u606F\u65E0\u6743\u9650\uFF1A\u8BF7\u5728 QQ \u7FA4\u673A\u5668\u4EBA\u8BBE\u7F6E\u4E2D\u5F00\u542F\u5141\u8BB8\u4E3B\u52A8\u53D1\u9001\");\n }\n log.warn(`\u5B98\u65B9\u53D1\u7FA4\u5931\u8D25: ${result.error} (${logCtx}, status=${result.status})`);\n }\n });\n}\n\n/**\n * \u5C06\u6587\u672C\u5185\u5BB9\u76F4\u63A5\u53D1\u9001\u81F3\u76EE\u6807 QQ \u7FA4\uFF08\u4F9B\u670D\u52A1\u5668\u4E8B\u4EF6\u901A\u77E5\u4F7F\u7528\uFF0C\u4E0D\u9644\u52A0\u73A9\u5BB6\u804A\u5929\u524D\u7F00\uFF09\u3002\n *\n * @param config \u51FA\u7AD9\u914D\u7F6E\u5BF9\u8C61\u3002\n * @param text \u5F85\u53D1\u9001\u7684\u6587\u672C\u5185\u5BB9\u3002\n */\nexport function sendGroupOutbound(config: OutboundConfig | LLBotConfig, text: string): void {\n const logCtx = \"outbound\";\n if (!(\"backend\" in config)) {\n sendViaLlbot(config, text, logCtx);\n return;\n }\n if (config.backend === \"llbot\") {\n sendViaLlbot(config.llbot, text, logCtx);\n return;\n }\n sendViaOfficial(config, text, logCtx);\n}\n\nfunction forwardViaLlbot(\n config: LLBotConfig,\n channelId: string,\n prefix: string,\n fromName: string,\n content: string,\n fromId: string\n): void {\n const text = `[${prefix}] ${fromName}: ${content}`;\n sendViaLlbot(config, text, `from=${fromId}, channel=${channelId}`);\n}\n\nfunction forwardViaOfficial(\n config: OfficialOutboundConfig,\n channelId: string,\n prefix: string,\n fromName: string,\n content: string,\n fromId: string\n): void {\n const text = `[${prefix}] ${fromName}: ${content}`;\n sendViaOfficial(config, text, `from=${fromId}, channel=${channelId}`);\n}\n\n/**\n * \u5C06\u6E38\u620F\u5185\u73A9\u5BB6\u804A\u5929\u6D88\u606F\u9644\u52A0\u524D\u7F00\u540E\u8F6C\u53D1\u81F3 QQ \u7FA4\u3002\n *\n * @param config \u51FA\u7AD9\u914D\u7F6E\u5BF9\u8C61\u3002\n * @param channelId \u6765\u6E90\u9891\u9053 ID\u3002\n * @param fromName \u6D88\u606F\u53D1\u9001\u8005\u73A9\u5BB6\u540D\u79F0\u3002\n * @param content \u804A\u5929\u6D88\u606F\u6B63\u6587\u3002\n * @param fromId \u53D1\u9001\u8005\u552F\u4E00\u6807\u8BC6\uFF08\u5982 XUID\uFF09\u3002\n */\nexport function forwardToQQBridge(\n config: OutboundConfig | LLBotConfig,\n channelId: string,\n prefix: string,\n fromName: string,\n content: string,\n fromId: string\n): void {\n\n // \u517C\u5BB9\u65E7\u8C03\u7528\uFF1A\u76F4\u63A5\u4F20 LLBotConfig\n if (!(\"backend\" in config)) {\n forwardViaLlbot(config, channelId, prefix, fromName, content, fromId);\n return;\n }\n if (config.backend === \"llbot\") {\n forwardViaLlbot(config.llbot, channelId, prefix, fromName, content, fromId);\n return;\n }\n forwardViaOfficial(config, channelId, prefix, fromName, content, fromId);\n}\n"],
4
+ "sourcesContent": ["/**\n * domain/bridge.ts \u2014 MC \u2192 QQ \u6865\u63A5\uFF08\u53CC\u540E\u7AEF\uFF09\n *\n * \u6570\u636E\u6D41:\n * \u6E38\u620F\u53D1\u8A00 \u2500\u2500db.tx \u63D2\u5165\u2500\u2500\u2192 afterCommit \u6309\u9891\u9053 forward_to_qq \u8C03\u7528 forwardToQQBridge()\n * QQ \u5165\u7AD9 \u2500\u2500POST\u2500\u2500\u2192 /api/sfmc/messages\uFF0C\u5199\u5165\u6D88\u606F\u6765\u6E90\u4E3A QQ \u7684\u9891\u9053\uFF08\u4E0D\u56DE\u63A8\u7FA4\uFF09\n * \u251C\u2500 llbot \u2192 LLBot:3004/send_group_msg\n * \u2514\u2500 official \u2192 OpenAPI /v2/groups/{openid}/messages\n *\n * \u4E8B\u4EF6\u63A8\u9001\u590D\u7528 sendGroupOutbound()\uFF08\u6B63\u6587\u539F\u6837\uFF0C\u4E0D\u52A0\u804A\u5929\u524D\u7F00\uFF09\u3002\n * \u5931\u8D25\u4EC5 warn\uFF0C\u4E0D\u629B\u9519\u3001\u4E0D\u91CD\u8BD5\uFF08\u4E0E\u5386\u53F2 LLBot \u884C\u4E3A\u4E00\u81F4\uFF09\u3002\n */\n\nimport { request } from \"node:http\";\nimport {\n sendGroupTextMessage,\n type QqOfficialCredentials,\n} from \"@sfmc-bds/sdk/node/qq-official\";\nimport type { QQBackend } from \"@sfmc-bds/sdk/node/config\";\n\nimport { log } from \"../lib/log.js\";\n\ntype LLBotConfig = {\n host: string;\n port: number;\n token: string;\n groupId: string;\n};\n\nexport type OfficialOutboundConfig = {\n backend: \"official\";\n creds: QqOfficialCredentials;\n groupOpenid: string;\n};\n\nexport type LlbotOutboundConfig = {\n backend: \"llbot\";\n llbot: LLBotConfig;\n};\n\nexport type OutboundConfig = OfficialOutboundConfig | LlbotOutboundConfig;\n\n/**\n * \u6784\u9020 LLBot \u6865\u63A5\u914D\u7F6E\u5BF9\u8C61\u3002\n *\n * @param env \u73AF\u5883\u53D8\u91CF\u6216\u914D\u7F6E\u5B57\u5178\u3002\n * @returns \u89C4\u8303\u5316\u7684 LLBotConfig \u914D\u7F6E\u3002\n */\nexport function makeLLBotConfig(env: {\n LLBOT_HOST: string;\n LLBOT_PORT: number;\n LLBOT_TOKEN: string;\n QQ_GROUP_ID: string;\n}): LLBotConfig {\n return {\n host: env.LLBOT_HOST,\n port: env.LLBOT_PORT,\n token: env.LLBOT_TOKEN,\n groupId: env.QQ_GROUP_ID,\n };\n}\n\n/**\n * \u6839\u636E\u73AF\u5883\u53D8\u91CF\u4E0E\u914D\u7F6E\u7EC4\u88C5\u51FA\u7AD9\u914D\u7F6E\uFF08\u652F\u6301 official \u5B98\u65B9\u5F00\u653E\u5E73\u53F0\u4E0E llbot \u53CC\u540E\u7AEF\uFF09\u3002\n *\n * @param env \u73AF\u5883\u53D8\u91CF\u4E0E\u914D\u7F6E\u5B57\u5178\u3002\n * @returns \u89C4\u8303\u5316\u7684 OutboundConfig \u51FA\u7AD9\u914D\u7F6E\u3002\n */\nexport function makeOutboundConfig(env: {\n QQ_BACKEND: QQBackend;\n LLBOT_HOST: string;\n LLBOT_PORT: number;\n LLBOT_TOKEN: string;\n QQ_GROUP_ID: string;\n QQ_APP_ID: string;\n QQ_APP_SECRET: string;\n QQ_SANDBOX: boolean;\n QQ_GROUP_OPENID: string;\n}): OutboundConfig {\n\n if (env.QQ_BACKEND === \"llbot\") {\n return {\n backend: \"llbot\",\n llbot: makeLLBotConfig(env),\n };\n }\n return {\n backend: \"official\",\n creds: {\n appId: env.QQ_APP_ID,\n appSecret: env.QQ_APP_SECRET,\n sandbox: env.QQ_SANDBOX,\n },\n groupOpenid: env.QQ_GROUP_OPENID,\n };\n}\n\nfunction sendViaLlbot(config: LLBotConfig, text: string, logCtx: string): void {\n if (!config.groupId || config.groupId === \"0\") {\n log.warn(`QQ \u7FA4\u672A\u914D\u7F6E (llbot.group_id),\u8DF3\u8FC7 MC\u2192QQ (${logCtx})`);\n return;\n }\n\n const payload = JSON.stringify({\n group_id: parseInt(config.groupId, 10),\n message: [{ type: \"text\", data: { text } }],\n });\n\n const headers: Record<string, string | number> = {\n \"Content-Type\": \"application/json\",\n \"Content-Length\": Buffer.byteLength(payload),\n };\n if (config.token) {\n headers[\"Authorization\"] = `Bearer ${config.token}`;\n }\n\n const req = request(\n {\n hostname: config.host,\n port: config.port,\n path: \"/send_group_msg\",\n method: \"POST\",\n headers,\n },\n (res) => {\n let body = \"\";\n res.on(\"data\", (c) => (body += c));\n res.on(\"end\", () => {\n if (res.statusCode !== 200) {\n log.warn(\n `LLBot send_group_msg \u2192 ${res.statusCode}: ${String(body).slice(0, 200)} (${logCtx})`\n );\n }\n });\n }\n );\n req.on(\"error\", (err) => {\n log.warn(`LLBot \u4E0D\u53EF\u8FBE (${config.host}:${config.port}): ${err.message}`);\n });\n req.write(payload);\n req.end();\n}\n\nfunction sendViaOfficial(config: OfficialOutboundConfig, text: string, logCtx: string): void {\n if (!config.groupOpenid) {\n log.warn(`\u5B98\u65B9\u7FA4 openid \u672A\u914D\u7F6E (official.group_openid),\u8DF3\u8FC7 MC\u2192QQ (${logCtx})`);\n return;\n }\n if (!config.creds.appId || !config.creds.appSecret) {\n log.warn(`\u5B98\u65B9 AppID/Secret \u672A\u914D\u7F6E,\u8DF3\u8FC7 MC\u2192QQ (${logCtx})`);\n return;\n }\n\n void sendGroupTextMessage(config.creds, {\n groupOpenid: config.groupOpenid,\n content: text,\n }).then((result) => {\n if (!result.ok) {\n if (result.status === 400 && String(result.error).includes(\"40034105\")) {\n log.warn(\"\u5B98\u65B9\u7FA4\u4E3B\u52A8\u53D1\u6D88\u606F\u65E0\u6743\u9650\uFF1A\u8BF7\u5728 QQ \u7FA4\u673A\u5668\u4EBA\u8BBE\u7F6E\u4E2D\u5F00\u542F\u5141\u8BB8\u4E3B\u52A8\u53D1\u9001\");\n }\n log.warn(`\u5B98\u65B9\u53D1\u7FA4\u5931\u8D25: ${result.error} (${logCtx}, status=${result.status})`);\n }\n });\n}\n\n/**\n * \u5C06\u6587\u672C\u5185\u5BB9\u76F4\u63A5\u53D1\u9001\u81F3\u76EE\u6807 QQ \u7FA4\uFF08\u4F9B\u670D\u52A1\u5668\u4E8B\u4EF6\u901A\u77E5\u4F7F\u7528\uFF0C\u4E0D\u9644\u52A0\u73A9\u5BB6\u804A\u5929\u524D\u7F00\uFF09\u3002\n *\n * @param config \u51FA\u7AD9\u914D\u7F6E\u5BF9\u8C61\u3002\n * @param text \u5F85\u53D1\u9001\u7684\u6587\u672C\u5185\u5BB9\u3002\n */\nexport function sendGroupOutbound(config: OutboundConfig | LLBotConfig, text: string): void {\n const logCtx = \"outbound\";\n if (!(\"backend\" in config)) {\n sendViaLlbot(config, text, logCtx);\n return;\n }\n if (config.backend === \"llbot\") {\n sendViaLlbot(config.llbot, text, logCtx);\n return;\n }\n sendViaOfficial(config, text, logCtx);\n}\n\nfunction forwardViaLlbot(\n config: LLBotConfig,\n channelId: string,\n prefix: string,\n fromName: string,\n content: string,\n fromId: string\n): void {\n const text = `[${prefix}] ${fromName}: ${content}`;\n sendViaLlbot(config, text, `from=${fromId}, channel=${channelId}`);\n}\n\nfunction forwardViaOfficial(\n config: OfficialOutboundConfig,\n channelId: string,\n prefix: string,\n fromName: string,\n content: string,\n fromId: string\n): void {\n const text = `[${prefix}] ${fromName}: ${content}`;\n sendViaOfficial(config, text, `from=${fromId}, channel=${channelId}`);\n}\n\n/**\n * \u5C06\u6E38\u620F\u5185\u73A9\u5BB6\u804A\u5929\u6D88\u606F\u9644\u52A0\u524D\u7F00\u540E\u8F6C\u53D1\u81F3 QQ \u7FA4\u3002\n *\n * @param config \u51FA\u7AD9\u914D\u7F6E\u5BF9\u8C61\u3002\n * @param channelId \u6765\u6E90\u9891\u9053 ID\u3002\n * @param fromName \u6D88\u606F\u53D1\u9001\u8005\u73A9\u5BB6\u540D\u79F0\u3002\n * @param content \u804A\u5929\u6D88\u606F\u6B63\u6587\u3002\n * @param fromId \u53D1\u9001\u8005\u552F\u4E00\u6807\u8BC6\uFF08\u5982 XUID\uFF09\u3002\n */\nexport function forwardToQQBridge(\n config: OutboundConfig | LLBotConfig,\n channelId: string,\n prefix: string,\n fromName: string,\n content: string,\n fromId: string\n): void {\n\n // \u517C\u5BB9\u65E7\u8C03\u7528\uFF1A\u76F4\u63A5\u4F20 LLBotConfig\n if (!(\"backend\" in config)) {\n forwardViaLlbot(config, channelId, prefix, fromName, content, fromId);\n return;\n }\n if (config.backend === \"llbot\") {\n forwardViaLlbot(config.llbot, channelId, prefix, fromName, content, fromId);\n return;\n }\n forwardViaOfficial(config, channelId, prefix, fromName, content, fromId);\n}\n"],
5
5
  "mappings": "AAaA,SAAS,eAAe;AACxB;AAAA,EACE;AAAA,OAEK;AAGP,SAAS,WAAW;AA4Bb,SAAS,gBAAgB,KAKhB;AACd,SAAO;AAAA,IACL,MAAM,IAAI;AAAA,IACV,MAAM,IAAI;AAAA,IACV,OAAO,IAAI;AAAA,IACX,SAAS,IAAI;AAAA,EACf;AACF;AAQO,SAAS,mBAAmB,KAUhB;AAEjB,MAAI,IAAI,eAAe,SAAS;AAC9B,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OAAO,gBAAgB,GAAG;AAAA,IAC5B;AAAA,EACF;AACA,SAAO;AAAA,IACL,SAAS;AAAA,IACT,OAAO;AAAA,MACL,OAAO,IAAI;AAAA,MACX,WAAW,IAAI;AAAA,MACf,SAAS,IAAI;AAAA,IACf;AAAA,IACA,aAAa,IAAI;AAAA,EACnB;AACF;AAEA,SAAS,aAAa,QAAqB,MAAc,QAAsB;AAC7E,MAAI,CAAC,OAAO,WAAW,OAAO,YAAY,KAAK;AAC7C,QAAI,KAAK,yEAAsC,MAAM,GAAG;AACxD;AAAA,EACF;AAEA,QAAM,UAAU,KAAK,UAAU;AAAA,IAC7B,UAAU,SAAS,OAAO,SAAS,EAAE;AAAA,IACrC,SAAS,CAAC,EAAE,MAAM,QAAQ,MAAM,EAAE,KAAK,EAAE,CAAC;AAAA,EAC5C,CAAC;AAED,QAAM,UAA2C;AAAA,IAC/C,gBAAgB;AAAA,IAChB,kBAAkB,OAAO,WAAW,OAAO;AAAA,EAC7C;AACA,MAAI,OAAO,OAAO;AAChB,YAAQ,eAAe,IAAI,UAAU,OAAO,KAAK;AAAA,EACnD;AAEA,QAAM,MAAM;AAAA,IACV;AAAA,MACE,UAAU,OAAO;AAAA,MACjB,MAAM,OAAO;AAAA,MACb,MAAM;AAAA,MACN,QAAQ;AAAA,MACR;AAAA,IACF;AAAA,IACA,CAAC,QAAQ;AACP,UAAI,OAAO;AACX,UAAI,GAAG,QAAQ,CAAC,MAAO,QAAQ,CAAE;AACjC,UAAI,GAAG,OAAO,MAAM;AAClB,YAAI,IAAI,eAAe,KAAK;AAC1B,cAAI;AAAA,YACF,+BAA0B,IAAI,UAAU,KAAK,OAAO,IAAI,EAAE,MAAM,GAAG,GAAG,CAAC,KAAK,MAAM;AAAA,UACpF;AAAA,QACF;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AACA,MAAI,GAAG,SAAS,CAAC,QAAQ;AACvB,QAAI,KAAK,6BAAc,OAAO,IAAI,IAAI,OAAO,IAAI,MAAM,IAAI,OAAO,EAAE;AAAA,EACtE,CAAC;AACD,MAAI,MAAM,OAAO;AACjB,MAAI,IAAI;AACV;AAEA,SAAS,gBAAgB,QAAgC,MAAc,QAAsB;AAC3F,MAAI,CAAC,OAAO,aAAa;AACvB,QAAI,KAAK,iGAAoD,MAAM,GAAG;AACtE;AAAA,EACF;AACA,MAAI,CAAC,OAAO,MAAM,SAAS,CAAC,OAAO,MAAM,WAAW;AAClD,QAAI,KAAK,yEAAiC,MAAM,GAAG;AACnD;AAAA,EACF;AAEA,OAAK,qBAAqB,OAAO,OAAO;AAAA,IACtC,aAAa,OAAO;AAAA,IACpB,SAAS;AAAA,EACX,CAAC,EAAE,KAAK,CAAC,WAAW;AAClB,QAAI,CAAC,OAAO,IAAI;AACd,UAAI,OAAO,WAAW,OAAO,OAAO,OAAO,KAAK,EAAE,SAAS,UAAU,GAAG;AACtE,YAAI,KAAK,oLAAmC;AAAA,MAC9C;AACA,UAAI,KAAK,yCAAW,OAAO,KAAK,KAAK,MAAM,YAAY,OAAO,MAAM,GAAG;AAAA,IACzE;AAAA,EACF,CAAC;AACH;AAQO,SAAS,kBAAkB,QAAsC,MAAoB;AAC1F,QAAM,SAAS;AACf,MAAI,EAAE,aAAa,SAAS;AAC1B,iBAAa,QAAQ,MAAM,MAAM;AACjC;AAAA,EACF;AACA,MAAI,OAAO,YAAY,SAAS;AAC9B,iBAAa,OAAO,OAAO,MAAM,MAAM;AACvC;AAAA,EACF;AACA,kBAAgB,QAAQ,MAAM,MAAM;AACtC;AAEA,SAAS,gBACP,QACA,WACA,QACA,UACA,SACA,QACM;AACN,QAAM,OAAO,IAAI,MAAM,KAAK,QAAQ,KAAK,OAAO;AAChD,eAAa,QAAQ,MAAM,QAAQ,MAAM,aAAa,SAAS,EAAE;AACnE;AAEA,SAAS,mBACP,QACA,WACA,QACA,UACA,SACA,QACM;AACN,QAAM,OAAO,IAAI,MAAM,KAAK,QAAQ,KAAK,OAAO;AAChD,kBAAgB,QAAQ,MAAM,QAAQ,MAAM,aAAa,SAAS,EAAE;AACtE;AAWO,SAAS,kBACd,QACA,WACA,QACA,UACA,SACA,QACM;AAGN,MAAI,EAAE,aAAa,SAAS;AAC1B,oBAAgB,QAAQ,WAAW,QAAQ,UAAU,SAAS,MAAM;AACpE;AAAA,EACF;AACA,MAAI,OAAO,YAAY,SAAS;AAC9B,oBAAgB,OAAO,OAAO,WAAW,QAAQ,UAAU,SAAS,MAAM;AAC1E;AAAA,EACF;AACA,qBAAmB,QAAQ,WAAW,QAAQ,UAAU,SAAS,MAAM;AACzE;",
6
6
  "names": []
7
7
  }
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * domain/installed-content.ts — 只读汇总已装模块与世界行为包、资源包。
3
3
  *
4
- * 供 QQ「模块 / 世界包」查询。模块启停仍以 catalog + lock 为准;
5
- * 世界包名单与显示名跟 CLI 同一条路径:listInstalledWorldPacks 会解析 pack.name 这类语言键。
4
+ * 供 QQ「模块 / 资源包」查询。模块启停仍以 catalog + lock 为准;
5
+ * 资源包名单与显示名跟 CLI 同一条路径:listInstalledWorldPacks 会解析 pack.name 这类语言键。
6
6
  */
7
7
  /** 一条已安装模块。has_resource_pack 表示该模块目录下有 resource_pack。 */
8
8
  export type ContentModule = {
@@ -1,6 +1,6 @@
1
+ import { listInstalledWorldPacks } from "@sfmc-bds/bds-tools/world-packs";
1
2
  import fs from "node:fs";
2
3
  import path from "node:path";
3
- import { listInstalledWorldPacks } from "@sfmc-bds/bds-tools/world-packs";
4
4
  function parseLevelName(serverPropertiesText) {
5
5
  const matched = serverPropertiesText.match(/^level-name=(.*)$/m);
6
6
  const name = matched?.[1]?.trim();
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/domain/installed-content.ts"],
4
- "sourcesContent": ["/**\n * domain/installed-content.ts \u2014 \u53EA\u8BFB\u6C47\u603B\u5DF2\u88C5\u6A21\u5757\u4E0E\u4E16\u754C\u884C\u4E3A\u5305\u3001\u8D44\u6E90\u5305\u3002\n *\n * \u4F9B QQ\u300C\u6A21\u5757 / \u4E16\u754C\u5305\u300D\u67E5\u8BE2\u3002\u6A21\u5757\u542F\u505C\u4ECD\u4EE5 catalog + lock \u4E3A\u51C6\uFF1B\n * \u4E16\u754C\u5305\u540D\u5355\u4E0E\u663E\u793A\u540D\u8DDF CLI \u540C\u4E00\u6761\u8DEF\u5F84\uFF1AlistInstalledWorldPacks \u4F1A\u89E3\u6790 pack.name \u8FD9\u7C7B\u8BED\u8A00\u952E\u3002\n */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { listInstalledWorldPacks } from \"@sfmc-bds/bds-tools/world-packs\";\n\n/** \u4E00\u6761\u5DF2\u5B89\u88C5\u6A21\u5757\u3002has_resource_pack \u8868\u793A\u8BE5\u6A21\u5757\u76EE\u5F55\u4E0B\u6709 resource_pack\u3002 */\nexport type ContentModule = {\n id: string;\n display_name: string;\n enabled: boolean;\n has_resource_pack: boolean;\n};\n\n/** \u4E16\u754C\u91CC\u5DF2\u5B89\u88C5\u7684\u884C\u4E3A\u5305\u6216\u8D44\u6E90\u5305\u3002name \u662F\u89E3\u6790\u540E\u7684\u663E\u793A\u540D\uFF0C\u4E0D\u518D\u4FDD\u7559 pack.name \u8FD9\u79CD\u8BED\u8A00\u952E\u3002 */\nexport type WorldPackKind = \"behavior\" | \"resource\";\n\nexport type WorldPackEntry = {\n kind: WorldPackKind;\n name: string;\n folder_name: string;\n pack_id: string;\n version: string;\n enabled: boolean;\n};\n\n/** \u4E16\u754C world_resource_packs.json \u91CC\u7684\u4E00\u6761\u5DF2\u542F\u7528\u8D44\u6E90\u5305\u3002\u65E7\u67E5\u8BE2\u4ECD\u8BFB\u8FD9\u4E2A\u5B57\u6BB5\u3002 */\nexport type WorldResourcePack = {\n name: string;\n pack_id: string;\n version: string;\n};\n\nexport type ContentSnapshot = {\n success: true;\n modules: ContentModule[];\n /** \u884C\u4E3A\u5305\u4E0E\u8D44\u6E90\u5305\u5206\u5F00\u5217\u51FA\uFF0C\u987A\u5E8F\u4E0E CLI list \u4E00\u81F4\u3002 */\n packs: WorldPackEntry[];\n resource_packs: WorldResourcePack[];\n /** \u8BFB\u4E0D\u5230 BDS \u4E16\u754C\u65F6\u8BF4\u660E\u539F\u56E0\uFF1B\u6A21\u5757\u5217\u8868\u4ECD\u7136\u8FD4\u56DE\u3002 */\n note?: \"bds_unconfigured\" | \"world_unread\";\n};\n\ntype CatalogModule = {\n id: string;\n display_name: string;\n enabled: boolean;\n};\n\n/**\n * \u4ECE server.properties \u6587\u672C\u53D6\u51FA level-name\u3002\n * \u7F3A\u7701\u65F6\u7528 BDS \u9ED8\u8BA4\u4E16\u754C\u540D\uFF0C\u4F9B\u62FC worlds/<level>/world_resource_packs.json\u3002\n */\nexport function parseLevelName(serverPropertiesText: string): string {\n const matched = serverPropertiesText.match(/^level-name=(.*)$/m);\n const name = matched?.[1]?.trim();\n return name && name.length > 0 ? name : \"Bedrock level\";\n}\n\n/** \u89E3\u6790\u4E16\u754C\u8D44\u6E90\u5305\u542F\u7528\u5217\u8868\u3002\u635F\u574F\u6216\u4E0D\u662F\u6570\u7EC4\u65F6\u8FD4\u56DE\u7A7A\u5217\u8868\u3002 */\nexport function parseWorldResourcePackList(raw: unknown): Array<{ pack_id: string; version: string }> {\n if (!Array.isArray(raw)) return [];\n const out: Array<{ pack_id: string; version: string }> = [];\n for (const entry of raw) {\n if (!entry || typeof entry !== \"object\") continue;\n const packId = (entry as { pack_id?: unknown }).pack_id;\n if (typeof packId !== \"string\" || !packId.trim()) continue;\n const ver = (entry as { version?: unknown }).version;\n const version =\n Array.isArray(ver) && ver.length >= 3 ? `${Number(ver[0])}.${Number(ver[1])}.${Number(ver[2])}` : \"\";\n out.push({ pack_id: packId.trim(), version });\n }\n return out;\n}\n\n/**\n * \u8BED\u8A00\u952E\u6CA1\u89E3\u6790\u51FA\u6765\u65F6\uFF0C\u7528\u6587\u4EF6\u5939\u540D\u4EE3\u66FF\u3002\n * \u4E0E CLI \u81EA\u68C0\u91CC\u5BF9 pack / pack.* \u7684\u56DE\u9000\u4E00\u81F4\uFF0C\u907F\u514D QQ \u4E0A\u7EE7\u7EED\u663E\u793A pack.name\u3002\n */\nexport function worldPackDisplayName(name: string, folderName: string): string {\n const raw = name.trim();\n if (!raw || raw.startsWith(\"pack.\") || raw === \"pack\") return folderName;\n return raw;\n}\n\nfunction readJsonObject(file: string): Record<string, unknown> | null {\n try {\n const parsed = JSON.parse(fs.readFileSync(file, \"utf8\")) as unknown;\n if (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) return null;\n return parsed as Record<string, unknown>;\n } catch {\n return null;\n }\n}\n\n/**\n * \u6536\u96C6\u5E26 resource_pack \u76EE\u5F55\u7684\u6A21\u5757\u6807\u8BC6\u3002\n * \u540C\u65F6\u6536\u5F55\u6587\u4EF6\u5939\u540D\u548C sapi/manifest.json \u7684 id\uFF0C\u4FBF\u4E8E\u548C catalog \u7684\u6A21\u5757 id \u5BF9\u4E0A\u3002\n */\nexport function indexModuleResourcePacks(packagesDir: string): Set<string> {\n const ids = new Set<string>();\n let entries: fs.Dirent[];\n try {\n entries = fs.readdirSync(packagesDir, { withFileTypes: true });\n } catch {\n return ids;\n }\n for (const entry of entries) {\n if (entry.name.startsWith(\".\")) continue;\n const modPath = path.join(packagesDir, entry.name);\n try {\n if (!fs.statSync(modPath).isDirectory()) continue;\n } catch {\n continue;\n }\n const rpDir = path.join(modPath, \"resource_pack\");\n try {\n if (!fs.statSync(rpDir).isDirectory()) continue;\n } catch {\n continue;\n }\n ids.add(entry.name);\n const manifest = readJsonObject(path.join(modPath, \"sapi\", \"manifest.json\"));\n const logicalId = manifest && typeof manifest.id === \"string\" ? manifest.id.trim() : \"\";\n if (logicalId) ids.add(logicalId);\n }\n return ids;\n}\n\n/** \u7ED9 catalog \u6A21\u5757\u6807\u4E0A\u662F\u5426\u81EA\u5E26\u8D44\u6E90\u5305\u3002\u5F53\u524D\u67E5\u8BE2\u9875\u7528\u8FD9\u4E2A\u6807\u8BB0\u533A\u5206\u300C\u542B\u8D44\u6E90\u5305\u300D\u3002 */\nexport function markModuleResourcePacks(modules: CatalogModule[], resourcePackIds: Set<string>): ContentModule[] {\n return modules.map((mod) => ({\n id: mod.id,\n display_name: mod.display_name,\n enabled: mod.enabled,\n has_resource_pack: resourcePackIds.has(mod.id),\n }));\n}\n\n/** \u626B\u63CF\u4E16\u754C\u5DF2\u5B89\u88C5\u7684\u884C\u4E3A\u5305\u548C\u8D44\u6E90\u5305\u3002\u663E\u793A\u540D\u8D70 CLI \u7684 manifest \u8BED\u8A00\u89E3\u6790\u3002 */\nexport function readInstalledWorldPacks(bdsRoot: string, levelName: string): WorldPackEntry[] {\n return listInstalledWorldPacks(bdsRoot, levelName).map((pack) => ({\n kind: pack.kind,\n name: worldPackDisplayName(pack.name, pack.folderName),\n folder_name: pack.folderName,\n pack_id: pack.uuid,\n version: `${pack.version[0]}.${pack.version[1]}.${pack.version[2]}`,\n enabled: pack.enabled,\n }));\n}\n\n/**\n * \u8BFB\u53D6\u67D0\u4E2A\u4E16\u754C\u5DF2\u542F\u7528\u7684\u8D44\u6E90\u5305\uFF0C\u5E76\u7528 CLI \u89E3\u6790\u51FA\u7684\u663E\u793A\u540D\u8865\u4E0A\u3002\n * \u76EE\u5F55\u5BF9\u4E0D\u4E0A\u65F6\u4ECD\u8FD4\u56DE pack_id\uFF0C\u907F\u514D\u542F\u7528\u5217\u8868\u88AB\u541E\u6389\u3002\n * installed \u4F20\u5165\u65F6\u590D\u7528\u540C\u4E00\u6B21\u626B\u63CF\uFF0C\u907F\u514D\u518D\u8BFB\u4E00\u904D\u5305\u76EE\u5F55\u3002\n */\nexport function readEnabledWorldResourcePacks(\n bdsRoot: string,\n levelName: string,\n installed?: WorldPackEntry[]\n): WorldResourcePack[] {\n const byUuid = new Map(\n (installed ?? readInstalledWorldPacks(bdsRoot, levelName))\n .filter((pack) => pack.kind === \"resource\")\n .map((pack) => [pack.pack_id.toLowerCase(), pack])\n );\n const listFile = path.join(bdsRoot, \"worlds\", levelName, \"world_resource_packs.json\");\n let listed: Array<{ pack_id: string; version: string }> = [];\n try {\n listed = parseWorldResourcePackList(JSON.parse(fs.readFileSync(listFile, \"utf8\")) as unknown);\n } catch {\n return [...byUuid.values()]\n .filter((pack) => pack.enabled)\n .map((pack) => ({ name: pack.name, pack_id: pack.pack_id, version: pack.version }));\n }\n return listed.map((entry) => {\n const known = byUuid.get(entry.pack_id.toLowerCase());\n return {\n name: known?.name || entry.pack_id,\n pack_id: entry.pack_id,\n version: known?.version || entry.version,\n };\n });\n}\n\n/**\n * \u7EC4\u88C5 QQ \u67E5\u8BE2\u7528\u7684\u5FEB\u7167\u3002\n * packagesDir \u662F modules/packages\uFF1BbdsRoot \u4E3A\u7A7A\u65F6\u53EA\u8FD4\u56DE\u6A21\u5757\uFF0C\u5E76\u5E26 bds_unconfigured\u3002\n */\nexport function loadContentSnapshot(opts: {\n packagesDir: string;\n modules: CatalogModule[];\n bdsRoot: string | null;\n}): ContentSnapshot {\n const modules = markModuleResourcePacks(opts.modules, indexModuleResourcePacks(opts.packagesDir));\n const unread = (note: \"bds_unconfigured\" | \"world_unread\"): ContentSnapshot => ({\n success: true,\n modules,\n packs: [],\n resource_packs: [],\n note,\n });\n if (!opts.bdsRoot) return unread(\"bds_unconfigured\");\n const propsFile = path.join(opts.bdsRoot, \"server.properties\");\n let levelName = \"Bedrock level\";\n try {\n levelName = parseLevelName(fs.readFileSync(propsFile, \"utf8\"));\n } catch {\n return unread(\"world_unread\");\n }\n const worldDir = path.join(opts.bdsRoot, \"worlds\", levelName);\n try {\n if (!fs.statSync(worldDir).isDirectory()) return unread(\"world_unread\");\n } catch {\n return unread(\"world_unread\");\n }\n let packs: WorldPackEntry[];\n try {\n packs = readInstalledWorldPacks(opts.bdsRoot, levelName);\n } catch {\n return unread(\"world_unread\");\n }\n return {\n success: true,\n modules,\n packs,\n resource_packs: readEnabledWorldResourcePacks(opts.bdsRoot, levelName, packs),\n };\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * domain/installed-content.ts \u2014 \u53EA\u8BFB\u6C47\u603B\u5DF2\u88C5\u6A21\u5757\u4E0E\u4E16\u754C\u884C\u4E3A\u5305\u3001\u8D44\u6E90\u5305\u3002\n *\n * \u4F9B QQ\u300C\u6A21\u5757 / \u8D44\u6E90\u5305\u300D\u67E5\u8BE2\u3002\u6A21\u5757\u542F\u505C\u4ECD\u4EE5 catalog + lock \u4E3A\u51C6\uFF1B\n * \u8D44\u6E90\u5305\u540D\u5355\u4E0E\u663E\u793A\u540D\u8DDF CLI \u540C\u4E00\u6761\u8DEF\u5F84\uFF1AlistInstalledWorldPacks \u4F1A\u89E3\u6790 pack.name \u8FD9\u7C7B\u8BED\u8A00\u952E\u3002\n */\n\nimport { listInstalledWorldPacks } from \"@sfmc-bds/bds-tools/world-packs\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\n\n/** \u4E00\u6761\u5DF2\u5B89\u88C5\u6A21\u5757\u3002has_resource_pack \u8868\u793A\u8BE5\u6A21\u5757\u76EE\u5F55\u4E0B\u6709 resource_pack\u3002 */\nexport type ContentModule = {\n id: string;\n display_name: string;\n enabled: boolean;\n has_resource_pack: boolean;\n};\n\n/** \u4E16\u754C\u91CC\u5DF2\u5B89\u88C5\u7684\u884C\u4E3A\u5305\u6216\u8D44\u6E90\u5305\u3002name \u662F\u89E3\u6790\u540E\u7684\u663E\u793A\u540D\uFF0C\u4E0D\u518D\u4FDD\u7559 pack.name \u8FD9\u79CD\u8BED\u8A00\u952E\u3002 */\nexport type WorldPackKind = \"behavior\" | \"resource\";\n\nexport type WorldPackEntry = {\n kind: WorldPackKind;\n name: string;\n folder_name: string;\n pack_id: string;\n version: string;\n enabled: boolean;\n};\n\n/** \u4E16\u754C world_resource_packs.json \u91CC\u7684\u4E00\u6761\u5DF2\u542F\u7528\u8D44\u6E90\u5305\u3002\u65E7\u67E5\u8BE2\u4ECD\u8BFB\u8FD9\u4E2A\u5B57\u6BB5\u3002 */\nexport type WorldResourcePack = {\n name: string;\n pack_id: string;\n version: string;\n};\n\nexport type ContentSnapshot = {\n success: true;\n modules: ContentModule[];\n /** \u884C\u4E3A\u5305\u4E0E\u8D44\u6E90\u5305\u5206\u5F00\u5217\u51FA\uFF0C\u987A\u5E8F\u4E0E CLI list \u4E00\u81F4\u3002 */\n packs: WorldPackEntry[];\n resource_packs: WorldResourcePack[];\n /** \u8BFB\u4E0D\u5230 BDS \u4E16\u754C\u65F6\u8BF4\u660E\u539F\u56E0\uFF1B\u6A21\u5757\u5217\u8868\u4ECD\u7136\u8FD4\u56DE\u3002 */\n note?: \"bds_unconfigured\" | \"world_unread\";\n};\n\ntype CatalogModule = {\n id: string;\n display_name: string;\n enabled: boolean;\n};\n\n/**\n * \u4ECE server.properties \u6587\u672C\u53D6\u51FA level-name\u3002\n * \u7F3A\u7701\u65F6\u7528 BDS \u9ED8\u8BA4\u4E16\u754C\u540D\uFF0C\u4F9B\u62FC worlds/<level>/world_resource_packs.json\u3002\n */\nexport function parseLevelName(serverPropertiesText: string): string {\n const matched = serverPropertiesText.match(/^level-name=(.*)$/m);\n const name = matched?.[1]?.trim();\n return name && name.length > 0 ? name : \"Bedrock level\";\n}\n\n/** \u89E3\u6790\u4E16\u754C\u8D44\u6E90\u5305\u542F\u7528\u5217\u8868\u3002\u635F\u574F\u6216\u4E0D\u662F\u6570\u7EC4\u65F6\u8FD4\u56DE\u7A7A\u5217\u8868\u3002 */\nexport function parseWorldResourcePackList(raw: unknown): Array<{ pack_id: string; version: string }> {\n if (!Array.isArray(raw)) return [];\n const out: Array<{ pack_id: string; version: string }> = [];\n for (const entry of raw) {\n if (!entry || typeof entry !== \"object\") continue;\n const packId = (entry as { pack_id?: unknown }).pack_id;\n if (typeof packId !== \"string\" || !packId.trim()) continue;\n const ver = (entry as { version?: unknown }).version;\n const version =\n Array.isArray(ver) && ver.length >= 3 ? `${Number(ver[0])}.${Number(ver[1])}.${Number(ver[2])}` : \"\";\n out.push({ pack_id: packId.trim(), version });\n }\n return out;\n}\n\n/**\n * \u8BED\u8A00\u952E\u6CA1\u89E3\u6790\u51FA\u6765\u65F6\uFF0C\u7528\u6587\u4EF6\u5939\u540D\u4EE3\u66FF\u3002\n * \u4E0E CLI \u81EA\u68C0\u91CC\u5BF9 pack / pack.* \u7684\u56DE\u9000\u4E00\u81F4\uFF0C\u907F\u514D QQ \u4E0A\u7EE7\u7EED\u663E\u793A pack.name\u3002\n */\nexport function worldPackDisplayName(name: string, folderName: string): string {\n const raw = name.trim();\n if (!raw || raw.startsWith(\"pack.\") || raw === \"pack\") return folderName;\n return raw;\n}\n\nfunction readJsonObject(file: string): Record<string, unknown> | null {\n try {\n const parsed = JSON.parse(fs.readFileSync(file, \"utf8\")) as unknown;\n if (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) return null;\n return parsed as Record<string, unknown>;\n } catch {\n return null;\n }\n}\n\n/**\n * \u6536\u96C6\u5E26 resource_pack \u76EE\u5F55\u7684\u6A21\u5757\u6807\u8BC6\u3002\n * \u540C\u65F6\u6536\u5F55\u6587\u4EF6\u5939\u540D\u548C sapi/manifest.json \u7684 id\uFF0C\u4FBF\u4E8E\u548C catalog \u7684\u6A21\u5757 id \u5BF9\u4E0A\u3002\n */\nexport function indexModuleResourcePacks(packagesDir: string): Set<string> {\n const ids = new Set<string>();\n let entries: fs.Dirent[];\n try {\n entries = fs.readdirSync(packagesDir, { withFileTypes: true });\n } catch {\n return ids;\n }\n for (const entry of entries) {\n if (entry.name.startsWith(\".\")) continue;\n const modPath = path.join(packagesDir, entry.name);\n try {\n if (!fs.statSync(modPath).isDirectory()) continue;\n } catch {\n continue;\n }\n const rpDir = path.join(modPath, \"resource_pack\");\n try {\n if (!fs.statSync(rpDir).isDirectory()) continue;\n } catch {\n continue;\n }\n ids.add(entry.name);\n const manifest = readJsonObject(path.join(modPath, \"sapi\", \"manifest.json\"));\n const logicalId = manifest && typeof manifest.id === \"string\" ? manifest.id.trim() : \"\";\n if (logicalId) ids.add(logicalId);\n }\n return ids;\n}\n\n/** \u7ED9 catalog \u6A21\u5757\u6807\u4E0A\u662F\u5426\u81EA\u5E26\u8D44\u6E90\u5305\u3002\u5F53\u524D\u67E5\u8BE2\u9875\u7528\u8FD9\u4E2A\u6807\u8BB0\u533A\u5206\u300C\u542B\u8D44\u6E90\u5305\u300D\u3002 */\nexport function markModuleResourcePacks(modules: CatalogModule[], resourcePackIds: Set<string>): ContentModule[] {\n return modules.map((mod) => ({\n id: mod.id,\n display_name: mod.display_name,\n enabled: mod.enabled,\n has_resource_pack: resourcePackIds.has(mod.id),\n }));\n}\n\n/** \u626B\u63CF\u4E16\u754C\u5DF2\u5B89\u88C5\u7684\u884C\u4E3A\u5305\u548C\u8D44\u6E90\u5305\u3002\u663E\u793A\u540D\u8D70 CLI \u7684 manifest \u8BED\u8A00\u89E3\u6790\u3002 */\nexport function readInstalledWorldPacks(bdsRoot: string, levelName: string): WorldPackEntry[] {\n return listInstalledWorldPacks(bdsRoot, levelName).map((pack) => ({\n kind: pack.kind,\n name: worldPackDisplayName(pack.name, pack.folderName),\n folder_name: pack.folderName,\n pack_id: pack.uuid,\n version: `${pack.version[0]}.${pack.version[1]}.${pack.version[2]}`,\n enabled: pack.enabled,\n }));\n}\n\n/**\n * \u8BFB\u53D6\u67D0\u4E2A\u4E16\u754C\u5DF2\u542F\u7528\u7684\u8D44\u6E90\u5305\uFF0C\u5E76\u7528 CLI \u89E3\u6790\u51FA\u7684\u663E\u793A\u540D\u8865\u4E0A\u3002\n * \u76EE\u5F55\u5BF9\u4E0D\u4E0A\u65F6\u4ECD\u8FD4\u56DE pack_id\uFF0C\u907F\u514D\u542F\u7528\u5217\u8868\u88AB\u541E\u6389\u3002\n * installed \u4F20\u5165\u65F6\u590D\u7528\u540C\u4E00\u6B21\u626B\u63CF\uFF0C\u907F\u514D\u518D\u8BFB\u4E00\u904D\u5305\u76EE\u5F55\u3002\n */\nexport function readEnabledWorldResourcePacks(\n bdsRoot: string,\n levelName: string,\n installed?: WorldPackEntry[]\n): WorldResourcePack[] {\n const byUuid = new Map(\n (installed ?? readInstalledWorldPacks(bdsRoot, levelName))\n .filter((pack) => pack.kind === \"resource\")\n .map((pack) => [pack.pack_id.toLowerCase(), pack])\n );\n const listFile = path.join(bdsRoot, \"worlds\", levelName, \"world_resource_packs.json\");\n let listed: Array<{ pack_id: string; version: string }> = [];\n try {\n listed = parseWorldResourcePackList(JSON.parse(fs.readFileSync(listFile, \"utf8\")) as unknown);\n } catch {\n return [...byUuid.values()]\n .filter((pack) => pack.enabled)\n .map((pack) => ({ name: pack.name, pack_id: pack.pack_id, version: pack.version }));\n }\n return listed.map((entry) => {\n const known = byUuid.get(entry.pack_id.toLowerCase());\n return {\n name: known?.name || entry.pack_id,\n pack_id: entry.pack_id,\n version: known?.version || entry.version,\n };\n });\n}\n\n/**\n * \u7EC4\u88C5 QQ \u67E5\u8BE2\u7528\u7684\u5FEB\u7167\u3002\n * packagesDir \u662F modules/packages\uFF1BbdsRoot \u4E3A\u7A7A\u65F6\u53EA\u8FD4\u56DE\u6A21\u5757\uFF0C\u5E76\u5E26 bds_unconfigured\u3002\n */\nexport function loadContentSnapshot(opts: {\n packagesDir: string;\n modules: CatalogModule[];\n bdsRoot: string | null;\n}): ContentSnapshot {\n const modules = markModuleResourcePacks(opts.modules, indexModuleResourcePacks(opts.packagesDir));\n const unread = (note: \"bds_unconfigured\" | \"world_unread\"): ContentSnapshot => ({\n success: true,\n modules,\n packs: [],\n resource_packs: [],\n note,\n });\n if (!opts.bdsRoot) return unread(\"bds_unconfigured\");\n const propsFile = path.join(opts.bdsRoot, \"server.properties\");\n let levelName = \"Bedrock level\";\n try {\n levelName = parseLevelName(fs.readFileSync(propsFile, \"utf8\"));\n } catch {\n return unread(\"world_unread\");\n }\n const worldDir = path.join(opts.bdsRoot, \"worlds\", levelName);\n try {\n if (!fs.statSync(worldDir).isDirectory()) return unread(\"world_unread\");\n } catch {\n return unread(\"world_unread\");\n }\n let packs: WorldPackEntry[];\n try {\n packs = readInstalledWorldPacks(opts.bdsRoot, levelName);\n } catch {\n return unread(\"world_unread\");\n }\n return {\n success: true,\n modules,\n packs,\n resource_packs: readEnabledWorldResourcePacks(opts.bdsRoot, levelName, packs),\n };\n}\n\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAgBH,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEtD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAExD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AA6B1C,QAAA,MAAM,GAAG,8BAAY,CAAC;AACtB,QAAA,MAAM,EAAE,oCAA4B,CAAC;AAGrC,QAAA,MAAM,KAAK,mCAAkB,CAAC;AAwB9B,QAAA,MAAM,gBAAgB,8DAA0E,CAAC;AAQjG,QAAA,MAAM,UAAU,0CAId,CAAC;AAGH,QAAA,MAAM,cAAc,gBAAyB,CAAC;AAC9C,QAAA,MAAM,eAAe,iBAAwB,CAAC;AAE9C,QAAA,MAAM,QAAQ,UAMZ,CAAC;AA4cH,OAAO,EAAE,EAAE,EAAE,gBAAgB,EAAE,GAAG,EAAE,UAAU,EAAE,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,QAAQ,EAAE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAgBH,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEtD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAExD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AA6B1C,QAAA,MAAM,GAAG,8BAAY,CAAC;AACtB,QAAA,MAAM,EAAE,oCAA4B,CAAC;AAGrC,QAAA,MAAM,KAAK,mCAAkB,CAAC;AAwB9B,QAAA,MAAM,gBAAgB,8DAA0E,CAAC;AAQjG,QAAA,MAAM,UAAU,0CAId,CAAC;AAGH,QAAA,MAAM,cAAc,gBAAyB,CAAC;AAC9C,QAAA,MAAM,eAAe,iBAAwB,CAAC;AAE9C,QAAA,MAAM,QAAQ,UAMZ,CAAC;AA6eH,OAAO,EAAE,EAAE,EAAE,gBAAgB,EAAE,GAAG,EAAE,UAAU,EAAE,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,QAAQ,EAAE,CAAC"}
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import http from "node:http";
2
2
  import { SQL } from "sql-template-strings";
3
3
  import { createPlatformTables } from "./db-tables.js";
4
- import { initSchema } from "./domain/schema.js";
5
4
  import { syncPlayerSession } from "./domain/player-session.js";
5
+ import { initSchema } from "./domain/schema.js";
6
6
  import { loadEnv } from "./env.js";
7
7
  import { createIdempotencyStore } from "./lib/idempotency-store.js";
8
8
  import { log } from "./lib/log.js";
@@ -23,14 +23,14 @@ import { createDbRoutes } from "./routes/db-routes.js";
23
23
  import { createModuleConfigRoutes } from "./routes/module-config-routes.js";
24
24
  import { createServiceRoutes } from "./routes/service-routes.js";
25
25
  import { forwardToQQBridge, makeOutboundConfig } from "./domain/bridge.js";
26
+ import { loadContentSnapshot } from "./domain/installed-content.js";
26
27
  import { createQqEventsAggregator, resolveQqEventsConfig } from "./domain/qq-events.js";
27
28
  import { body as sharedBody, json as sharedJson } from "./lib/http.js";
28
29
  import { isEnabled, loadModuleLock, saveModuleLock, updateModuleState } from "./lib/module-state.js";
29
- import { loadContentSnapshot } from "./domain/installed-content.js";
30
30
  import { createConfigRoutes } from "./routes/config.js";
31
31
  import { createContentRoutes } from "./routes/content.js";
32
32
  import { createHealthRoutes } from "./routes/health.js";
33
- import { createMessagesRoutes } from "./routes/messages.js";
33
+ import { createMessagesRoutes, gameChatForwardPayload } from "./routes/messages.js";
34
34
  import { createModuleRoutes } from "./routes/modules.js";
35
35
  import { createQqBindRoutes } from "./routes/qq-bind.js";
36
36
  import { createQqEventsRoutes } from "./routes/qq-events.js";
@@ -190,8 +190,8 @@ const QQ_LINK_CONFIG_KEY = "qq_link";
190
190
  const QQ_LINK_DEFAULTS = {
191
191
  allowlist_enabled: true,
192
192
  require_approval: true,
193
- /** 是否将 QQ 群主/群管视作 SFMC 管理员;仅改本文件,API/群聊不可写 */
194
- treat_group_admins_as_admins: false
193
+ /** 是否将 QQ 群主/群管视作 SFMC 管理员;缺省开启,仅改本文件可关闭,API/群聊不可写 */
194
+ treat_group_admins_as_admins: true
195
195
  };
196
196
  function asConfigBool(v, fallback) {
197
197
  if (v === void 0 || v === null) return fallback;
@@ -284,18 +284,48 @@ const qqEventsRoutes = createQqEventsRoutes({
284
284
  return Array.isArray(admins) && admins.includes(openid) || asGroupAdmin && readJoinFlags().treatGroupAdminsAsAdmins;
285
285
  }
286
286
  });
287
+ function readChatChannel(channelId) {
288
+ try {
289
+ const rows = query(SQL`SELECT * FROM sfmc_chat_channels WHERE id = ${channelId}`);
290
+ return Array.isArray(rows) ? rows[0] ?? null : null;
291
+ } catch (err) {
292
+ log.warn(`\u8BFB\u53D6\u804A\u5929\u9891\u9053\u5931\u8D25: ${err.message}`);
293
+ return null;
294
+ }
295
+ }
296
+ function listQQSourceChannels() {
297
+ try {
298
+ const rows = query(SQL`SELECT * FROM sfmc_chat_channels`);
299
+ if (!Array.isArray(rows)) return [];
300
+ return rows.filter((row) => row.source_qq === 1);
301
+ } catch (err) {
302
+ log.warn(`\u8BFB\u53D6 QQ \u6765\u6E90\u9891\u9053\u5931\u8D25: ${err.message}`);
303
+ return [];
304
+ }
305
+ }
287
306
  const messagesRoutes = createMessagesRoutes({
288
307
  query,
289
308
  body,
290
309
  json,
291
- getChannelForQQ: (channelId) => {
292
- const rows = query(
293
- SQL`SELECT * FROM sfmc_chat_channels WHERE id = ${channelId}`
294
- );
295
- return Array.isArray(rows) ? rows[0] ?? null : null;
296
- },
310
+ getChannelForQQ: readChatChannel,
311
+ listQQSourceChannels,
297
312
  forwardToQQBridge: (channelId, prefix, fromName, content, fromId) => forwardToQQBridge(currentOutbound(), channelId, prefix, fromName, content, fromId)
298
313
  });
314
+ txRunner.setAfterCommit((inserts) => {
315
+ for (const item of inserts) {
316
+ if (item.table !== "sfmc_chat_messages") continue;
317
+ const payload = gameChatForwardPayload(item.row, readChatChannel(String(item.row.channel_id ?? "")));
318
+ if (!payload) continue;
319
+ forwardToQQBridge(
320
+ currentOutbound(),
321
+ payload.channelId,
322
+ payload.prefix,
323
+ payload.fromName,
324
+ payload.content,
325
+ payload.fromId
326
+ );
327
+ }
328
+ });
299
329
  const configRoutes = createConfigRoutes({
300
330
  json,
301
331
  projectRoot: env.PROJECT_ROOT,
package/dist/index.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/index.ts"],
4
- "sourcesContent": ["/**\n *\n * \u5DE5\u4F5C\u6D41:\n * 1. \u52A0\u8F7D env\n * 2. \u6821\u9A8C Node \u7248\u672C\n * 3. openDatabase + createPlatformTables(sfmc__audit / sfmc__idempotent)\n * 4. loadManifestV2() \u2014 \u5931\u8D25 = \u542F\u52A8\u5931\u8D25\n * 5. filterEnabled(loaded, lockFileEnabled)\n * 6. buildModuleAuth({auth_token, enabled_modules}) \u2190 \u4E0E DB \u540C\u76EE\u5F55 module-tokens.json\n * 7. \u5B9E\u4F8B\u5316:SchemaRegistry / ServiceRegistry / IdempotencyStore / TxRunner\n * 8. \u88C5\u914D v2 \u8DEF\u7531 (/api/sfmc/db/* + /api/sfmc/services* + /api/sfmc/configs/:key/*)\n * \u53E6\u4FDD\u7559 messages(qq-bridge) + config/modules/health \u5E73\u53F0\u8DEF\u7531\n * 9. createServer + listen\n *\n * \u9274\u6743(handle \u5C42):\n * - /api/sfmc/db/*, /api/sfmc/services*, /api/sfmc/configs/:key(/set|notify)*\n * \u2192 module token (Authorization: Bearer ...) + ?moduleId=...\n * - \u5176\u4ED6:env.AUTH_TOKEN \u65E7 auth(NEEDS_AUTH)\n */\n\nimport http from \"node:http\";\nimport { SQL } from \"sql-template-strings\";\n\nimport { createPlatformTables } from \"./db-tables.js\";\nimport { initSchema } from \"./domain/schema.js\";\nimport { syncPlayerSession } from \"./domain/player-session.js\";\nimport { loadEnv } from \"./env.js\";\nimport { createIdempotencyStore } from \"./lib/idempotency-store.js\";\nimport { log } from \"./lib/log.js\";\nimport { assertNodeVersion } from \"./lib/runtime.js\";\nimport { createQuery, openDatabase } from \"./lib/sqlite.js\";\nimport { loadManifestV2 } from \"./manifest-loader.js\";\nimport { buildModuleAuth, verifyModuleAuth } from \"./module-auth.js\";\nimport { syncModuleRuntimeState } from \"./module-runtime-sync.js\";\nimport { SchemaRegistry } from \"./schema-registry.js\";\nimport { createServer, startConsole } from \"./server.js\";\nimport { ServiceRegistry } from \"./service-registry.js\";\nimport { registerEnabledBuiltinServices } from \"./services/builtin-handlers.js\";\nimport { TxRunner } from \"./tx-runner.js\";\n\nimport { configPath, ensureJson, patchJson, readJson } from \"@sfmc-bds/sdk/node/config\";\nimport path, { join } from \"node:path\";\n\nimport { jsonV2Fail } from \"./routes/_shared.js\";\nimport { createDbRoutes } from \"./routes/db-routes.js\";\nimport { createModuleConfigRoutes } from \"./routes/module-config-routes.js\";\nimport { createServiceRoutes } from \"./routes/service-routes.js\";\n\nimport { forwardToQQBridge, makeOutboundConfig } from \"./domain/bridge.js\";\nimport { createQqEventsAggregator, resolveQqEventsConfig, type ResolvedQqEventsConfig } from \"./domain/qq-events.js\";\nimport { body as sharedBody, json as sharedJson } from \"./lib/http.js\";\nimport { isEnabled, loadModuleLock, saveModuleLock, updateModuleState } from \"./lib/module-state.js\";\nimport { loadContentSnapshot } from \"./domain/installed-content.js\";\nimport { createConfigRoutes } from \"./routes/config.js\";\nimport { createContentRoutes } from \"./routes/content.js\";\nimport { createHealthRoutes } from \"./routes/health.js\";\nimport { createMessagesRoutes } from \"./routes/messages.js\";\nimport { createModuleRoutes } from \"./routes/modules.js\";\nimport { createQqBindRoutes } from \"./routes/qq-bind.js\";\nimport { createQqEventsRoutes } from \"./routes/qq-events.js\";\nimport { createQqJoinRoutes, type JoinFeatureFlags } from \"./routes/qq-join.js\";\nimport { createStatusRoutes } from \"./routes/status.js\";\n\nif (!assertNodeVersion(22, 13)) {\n process.exit(2);\n}\n\nconst env = loadEnv();\nconst db = openDatabase(env.DB_PATH);\ncreatePlatformTables(db); // sfmc__audit / sfmc__idempotent\ninitSchema(db); // \u5E73\u53F0\u4E1A\u52A1 bootstrap \u8868(players/world/chat/...)\u2014\u2014qq-bridge \u5728 SAPI defineTable \u4E4B\u524D\u5C31\u9700\u8981\nconst query = createQuery(db);\n\n// \u2500\u2500 v2 manifest \u52A0\u8F7D(\u5931\u8D25 = \u542F\u52A8\u5931\u8D25)\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\nconst loadedManifest = loadManifestV2(); // throws on violation\nlog.success(\n `[manifest v2] loaded ${Object.keys(loadedManifest.modules).length} modules; provides ${loadedManifest.providesMap.size} services`\n);\n\n// \u2500\u2500 enabled \u96C6\u5408(\u4ECE lock file)\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\nconst lockFile = loadModuleLock(env.MODULE_LOCK_PATH);\nconst moduleCatalog = readJson<{ modules?: unknown[] }>(env.MODULE_CATALOG_PATH) ?? { modules: [] };\nconst catalogIds = new Set(\n Array.isArray(moduleCatalog.modules)\n ? (moduleCatalog.modules as Array<{ id?: string }>).map((m) => String(m.id || \"\")).filter((id) => id.length > 0)\n : []\n);\nconst enabledSet = new Set<string>();\nfor (const id of Object.keys(loadedManifest.modules)) {\n if (!catalogIds.has(id)) continue;\n const catalogEntry = (moduleCatalog.modules as Array<Record<string, unknown>>).find((m) => String(m.id) === id);\n if (!catalogEntry) continue;\n const defaultEnabled = catalogEntry.enabledByDefault !== false;\n if (isEnabled(lockFile, id, defaultEnabled)) enabledSet.add(id);\n}\nconst enabledManifests = new Map<string, NonNullable<(typeof loadedManifest.modules)[string]>>();\nfor (const id of enabledSet) {\n const m = loadedManifest.modules[id];\n if (m) enabledManifests.set(id, m);\n}\nlog.info(`[manifest v2] enabled: ${[...enabledSet].sort().join(\", \") || \"(none)\"}`);\n\n// \u2500\u2500 \u6A21\u5757 HMAC token map(\u4E0E DB \u540C\u76EE\u5F55 module-tokens.json)\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst moduleAuth = buildModuleAuth({\n dbPath: env.DB_PATH,\n envAuthToken: env.AUTH_TOKEN,\n enabledModuleIds: [...enabledSet],\n});\n\n// \u2500\u2500 \u4E09\u4EF6\u5957 + \u8DEF\u7531\u5DE5\u5382 \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\nconst schemaRegistry = new SchemaRegistry(db);\nconst serviceRegistry = new ServiceRegistry();\nconst idempotent = createIdempotencyStore(db);\nconst txRunner = new TxRunner({\n db,\n query,\n schema: schemaRegistry,\n serviceRegistry,\n enabled: enabledManifests,\n});\n\n// \u2500\u2500 \u8FDB\u7A0B\u5185\u7F6E service handler(\u6269\u5C55\u70B9:BUILTIN_SERVICE_PLUGINS) \u2500\u2500\n{\n const plugins = registerEnabledBuiltinServices(serviceRegistry, { query, db }, enabledSet);\n if (plugins > 0) {\n log.success(`[service] registered ${serviceRegistry.list().length} handlers from ${plugins} builtin plugin(s)`);\n }\n}\n\nconst json = sharedJson;\nconst body = sharedBody;\n\n// \u2500\u2500 \u5DE5\u5177\u51FD\u6570 \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\u2500\u2500\u2500\u2500\nfunction loadModuleCatalog() {\n return Array.isArray(moduleCatalog.modules) ? moduleCatalog.modules : [];\n}\n\nfunction buildModuleList() {\n const catalog = loadModuleCatalog();\n return catalog\n .map((raw) => {\n const entry =\n (raw as Record<string, unknown>).entry && typeof (raw as Record<string, unknown>).entry === \"object\"\n ? ((raw as Record<string, unknown>).entry as Record<string, unknown>)\n : {};\n const id = String((raw as Record<string, unknown>).id || \"\").trim();\n const configKey = String(\n (raw as Record<string, unknown>).configKey || (raw as Record<string, unknown>).config_key || \"\"\n ).trim();\n if (!id || !configKey) return null;\n const state = lockFile.modules[id];\n const enabled = isEnabled(lockFile, id, (raw as Record<string, unknown>).enabledByDefault !== false);\n return {\n id,\n module_id: id,\n name: configKey,\n configKey,\n config_key: configKey,\n display_name: String((raw as Record<string, unknown>).name || configKey),\n description: String((raw as Record<string, unknown>).description || \"\"),\n default_enabled: (raw as Record<string, unknown>).enabledByDefault !== false,\n can_disable: moduleCanDisable(raw as Record<string, unknown>),\n // ConfigManager \u8BA4 installed!==false;\u5DF2\u88C5\u5305\u9ED8\u8BA4 true\n installed: true,\n requires: Array.isArray((raw as Record<string, unknown>).requires)\n ? ((raw as Record<string, unknown>).requires as unknown[]).filter(Boolean).map(String)\n : [],\n optional: Array.isArray((raw as Record<string, unknown>).optional)\n ? ((raw as Record<string, unknown>).optional as unknown[]).filter(Boolean).map(String)\n : [],\n commands: Array.isArray((raw as Record<string, unknown>).commands)\n ? ((raw as Record<string, unknown>).commands as unknown[]).filter(Boolean).map(String)\n : [],\n entry: {\n kind: String(entry.kind || \"\"),\n path: String(entry.path || \"\"),\n init: String(entry.init || \"\"),\n },\n updated_at: state?.updatedAt ?? null,\n enabled: !!enabled,\n };\n })\n .filter(Boolean);\n}\n\n/**\n * QQ\u300C\u6A21\u5757 / \u4E16\u754C\u5305\u300D\u5FEB\u7167\u3002\u6BCF\u6B21\u8BF7\u6C42\u73B0\u8BFB catalog \u4E0E\u78C1\u76D8\uFF0C\u88C5\u5305\u540E\u4E0D\u7528\u91CD\u542F db-server\u3002\n */\nfunction loadContentSnapshotForRequest() {\n const modules = (buildModuleList() ?? []).flatMap((raw) => {\n if (!raw || typeof raw !== \"object\") return [];\n const row = raw as { id?: unknown; display_name?: unknown; enabled?: unknown };\n const id = String(row.id ?? \"\").trim();\n if (!id) return [];\n return [\n {\n id,\n display_name: String(row.display_name || id),\n enabled: row.enabled !== false,\n },\n ];\n });\n const cfg = readJson<{ bds_path?: string }>(configPath(env.PROJECT_ROOT, \"bds_updater.json\"));\n const rawBds = typeof cfg?.bds_path === \"string\" ? cfg.bds_path.trim() : \"\";\n const bdsRoot = rawBds ? (path.isAbsolute(rawBds) ? rawBds : path.resolve(env.PROJECT_ROOT, rawBds)) : null;\n return loadContentSnapshot({\n packagesDir: join(env.MODULES_DIR, \"packages\"),\n modules,\n bdsRoot,\n });\n}\n\n/** catalog \u7701\u7565 canDisable \u65F6\u9ED8\u8BA4\u5141\u8BB8\u7981\u7528(\u4E0E buildModuleList.can_disable \u540C\u6E90)\u3002 */\nfunction moduleCanDisable(raw: Record<string, unknown>): boolean {\n return raw.canDisable !== false;\n}\n\nfunction resolveModuleByKey(key: string) {\n const k = String(key || \"\").trim();\n const catalog = loadModuleCatalog();\n const raw = catalog.find(\n (m) =>\n String((m as Record<string, unknown>).id || \"\") === k ||\n String((m as Record<string, unknown>).configKey || (m as Record<string, unknown>).config_key || \"\") === k\n ) as Record<string, unknown> | undefined;\n if (!raw) return null;\n const id = String(raw.id || \"\").trim();\n const configKey = String(raw.configKey || raw.config_key || \"\").trim();\n if (!id || !configKey) return null;\n /* LSP:\u4E0E buildModuleList.can_disable \u540C\u6E90 \u2014 \u7701\u7565\u5B57\u6BB5\u89C6\u4E3A\u53EF\u7981\u7528 */\n return { id, configKey, canDisable: moduleCanDisable(raw) };\n}\n\nfunction setModuleEnabled(mod: { id: string; canDisable: boolean }, enabled: boolean) {\n // \u76F4\u63A5\u66F4\u65B0\u542F\u52A8\u65F6\u7F13\u5B58\u7684 lockFile(\u800C\u975E\u53E6\u8BFB\u4E00\u4EFD\u65B0\u526F\u672C),\n // \u5426\u5219 buildModuleList() \u8BFB\u7684\u4ECD\u662F\u65E7\u7F13\u5B58,\u5BFC\u81F4\u542F\u505C\u540E enabled \u72B6\u6001\u4E0D\u7FFB\u8F6C\u3002\n updateModuleState(lockFile, mod.id, { enabled: !!enabled });\n // DRY:\u4E0E loadModuleLock \u5BF9\u79F0\u8D70 saveModuleLock,\u52FF\u6563\u843D writeJson\n saveModuleLock(env.MODULE_LOCK_PATH, lockFile);\n\n // DIP:\u70ED\u540C\u6B65 enabledSet / tokens / manifests / builtin handlers(\u4E0D\u91CD\u542F db-server)\n syncModuleRuntimeState({\n moduleId: mod.id,\n enabled: !!enabled,\n dbPath: env.DB_PATH,\n envAuthToken: env.AUTH_TOKEN,\n enabledSet,\n enabledManifests,\n loadedManifest,\n moduleAuth,\n serviceRegistry,\n builtinDeps: { query, db },\n });\n}\n\n// \u2500\u2500 \u5E73\u53F0\u8DEF\u7531(\u975E\u6A21\u5757\u4E1A\u52A1) \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\nconst healthRoutes = createHealthRoutes();\nconst statusRoutes = createStatusRoutes({});\nconst qqBindRoutes = createQqBindRoutes({ query, body, json });\n\n/** \u6A21\u5757 qq-link \u7684\u914D\u7F6E\u6587\u4EF6\uFF08\u975E SDK qq_config\uFF09 */\nconst QQ_LINK_CONFIG_KEY = \"qq_link\";\nconst QQ_LINK_DEFAULTS = {\n allowlist_enabled: true,\n require_approval: true,\n /** \u662F\u5426\u5C06 QQ \u7FA4\u4E3B/\u7FA4\u7BA1\u89C6\u4F5C SFMC \u7BA1\u7406\u5458\uFF1B\u4EC5\u6539\u672C\u6587\u4EF6\uFF0CAPI/\u7FA4\u804A\u4E0D\u53EF\u5199 */\n treat_group_admins_as_admins: false,\n} as const;\n\nfunction asConfigBool(v: unknown, fallback: boolean): boolean {\n if (v === undefined || v === null) return fallback;\n if (typeof v === \"boolean\") return v;\n const s = String(v).trim().toLowerCase();\n if (s === \"true\" || s === \"1\" || s === \"yes\" || s === \"on\") return true;\n if (s === \"false\" || s === \"0\" || s === \"no\" || s === \"off\") return false;\n return fallback;\n}\n\nfunction qqLinkConfigFile(): string {\n return join(env.PROJECT_ROOT, \"configs\", `${QQ_LINK_CONFIG_KEY}.json`);\n}\n\nfunction readJoinFlags(): JoinFeatureFlags {\n // \u6A21\u5757\u914D\u7F6E configs/qq_link.json\uFF08\u975E SDK qq_config\uFF09\n const disk = ensureJson<Record<string, unknown>>(qqLinkConfigFile(), { ...QQ_LINK_DEFAULTS });\n return {\n allowlistEnabled: asConfigBool(disk.allowlist_enabled, QQ_LINK_DEFAULTS.allowlist_enabled),\n requireApproval: asConfigBool(disk.require_approval, QQ_LINK_DEFAULTS.require_approval),\n treatGroupAdminsAsAdmins: asConfigBool(\n disk.treat_group_admins_as_admins,\n QQ_LINK_DEFAULTS.treat_group_admins_as_admins\n ),\n };\n}\n\nfunction writeJoinFlags(partial: Partial<JoinFeatureFlags>): JoinFeatureFlags {\n const file = qqLinkConfigFile();\n ensureJson<Record<string, unknown>>(file, { ...QQ_LINK_DEFAULTS });\n const patch: Record<string, boolean> = {};\n if (partial.allowlistEnabled !== undefined) patch.allowlist_enabled = partial.allowlistEnabled;\n if (partial.requireApproval !== undefined) patch.require_approval = partial.requireApproval;\n // \u6545\u610F\u4E0D\u5199 treat_group_admins_as_admins\uFF1A\u4EC5\u4EBA\u5DE5\u6539 configs/qq_link.json\n if (Object.keys(patch).length > 0) {\n patchJson(file, patch);\n }\n return readJoinFlags();\n}\n\n// \u542F\u52A8\u5373\u843D\u76D8\u6A21\u5757\u9ED8\u8BA4\u914D\u7F6E\uFF0C\u907F\u514D\u300C\u4ECE\u672A\u70B9\u8FC7\u914D\u7F6E/\u7533\u8BF7\u300D\u65F6 configs/qq_link.json \u4E0D\u5B58\u5728\n{\n const created = !readJson(qqLinkConfigFile());\n ensureJson<Record<string, unknown>>(qqLinkConfigFile(), { ...QQ_LINK_DEFAULTS });\n if (created) {\n log.info(`\u5DF2\u5199\u5165\u6A21\u5757\u914D\u7F6E\u9AA8\u67B6: configs/${QQ_LINK_CONFIG_KEY}.json`);\n }\n}\n\nconst qqJoinRoutes = createQqJoinRoutes({\n query,\n body,\n json,\n getAdminOpenids: () => {\n const raw = (env.qqconfig[\"official\"] as { admin_openids?: unknown } | undefined)?.admin_openids;\n if (!Array.isArray(raw)) return [];\n return raw.map((x) => String(x).trim()).filter(Boolean);\n },\n getJoinFlags: () => readJoinFlags(),\n setJoinFlags: (partial) => writeJoinFlags(partial),\n});\n\nfunction readQqEventsSettings(): ResolvedQqEventsConfig {\n const disk = readJson<Record<string, unknown>>(join(env.PROJECT_ROOT, \"configs\", \"qq_config.json\"));\n if (!disk) throw new Error(\"qq_config.json \u65E0\u6CD5\u8BFB\u53D6\uFF0C\u4E8B\u4EF6\u8BBE\u7F6E\u672A\u4FEE\u6539\");\n return resolveQqEventsConfig(disk.qq_events);\n}\n\nfunction writeQqEventsSettings(patch: Partial<ResolvedQqEventsConfig>): ResolvedQqEventsConfig {\n const file = join(env.PROJECT_ROOT, \"configs\", \"qq_config.json\");\n const settings = { ...readQqEventsSettings(), ...patch };\n patchJson(file, { qq_events: settings });\n return settings;\n}\n\nfunction currentOutbound() {\n return makeOutboundConfig({\n QQ_BACKEND: env.QQ_BACKEND,\n LLBOT_HOST: env.LLBOT_HOST,\n LLBOT_PORT: env.LLBOT_PORT,\n LLBOT_TOKEN: env.LLBOT_TOKEN,\n QQ_GROUP_ID: env.QQ_GROUP_ID,\n QQ_APP_ID: env.QQ_APP_ID,\n QQ_APP_SECRET: env.QQ_APP_SECRET,\n QQ_SANDBOX: env.QQ_SANDBOX,\n QQ_GROUP_OPENID: env.QQ_GROUP_OPENID,\n });\n}\n\nconst qqEventsAggregator = createQqEventsAggregator({\n getConfig: readQqEventsSettings,\n getOutbound: () => currentOutbound(),\n});\nconst qqEventsRoutes = createQqEventsRoutes({\n body,\n json,\n aggregator: qqEventsAggregator,\n getSettings: readQqEventsSettings,\n setSettings: writeQqEventsSettings,\n isAdmin: (openid, asGroupAdmin) => {\n const admins = (env.qqconfig[\"official\"] as { admin_openids?: unknown } | undefined)?.admin_openids;\n return (\n (Array.isArray(admins) && admins.includes(openid)) ||\n (asGroupAdmin && readJoinFlags().treatGroupAdminsAsAdmins)\n );\n },\n});\n\nconst messagesRoutes = createMessagesRoutes({\n query,\n body,\n json,\n getChannelForQQ: (channelId: string) => {\n const rows = query(\n SQL`SELECT * FROM sfmc_chat_channels WHERE id = ${channelId}`\n );\n return Array.isArray(rows)\n ? (rows[0] as { prefix: string; forward_to_qq: number; source_game?: number; source_qq?: number; source_system?: number } | undefined) ?? null\n : null;\n },\n forwardToQQBridge: (channelId: string, prefix: string, fromName: string, content: string, fromId: string) =>\n forwardToQQBridge(currentOutbound(), channelId, prefix, fromName, content, fromId),\n});\nconst configRoutes = createConfigRoutes({\n json,\n projectRoot: env.PROJECT_ROOT,\n // DIP:\u8DEF\u7531\u4E0D\u8BFB lock/catalog/token \u6587\u4EF6,\u7531\u5165\u53E3\u6CE8\u5165\u4E0E /modules \u540C\u6E90\u6570\u636E\n listModules: () => buildModuleList() as Array<Record<string, unknown>>,\n getModuleTokens: () => ({ ...moduleAuth.tokens }),\n});\nconst contentRoutes = createContentRoutes({\n json,\n loadSnapshot: loadContentSnapshotForRequest,\n});\nconst moduleRoutesInstance = createModuleRoutes({\n loadModuleCatalog,\n buildModuleList: buildModuleList as unknown as () => Array<Record<string, unknown>>,\n resolveModuleByKey,\n setModuleEnabled,\n body,\n json,\n});\n\n// \u2500\u2500 v2 \u8DEF\u7531\u5DE5\u5382 \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\u2500\u2500\u2500\u2500\u2500\n// \u7C7B\u578B\u65AD\u8A00\u6210 unknown \u51FD\u6570 \u2014 v2 routes \u7684 ctx \u7C7B\u578B\u4E0E RouteCtx \u4E0D\u517C\u5BB9,\n// \u4F46\u8C03\u7528\u5951\u7EA6({path, method, params, req, res, body?})\u662F\u7A33\u5B9A\u7684\u3002\nconst dbRoutes = createDbRoutes({ schemaRegistry, txRunner, idempotent, json }) as unknown as (\n ctx: Record<string, unknown>\n) => Promise<boolean>;\nconst serviceRoutes = createServiceRoutes({\n serviceRegistry,\n enabled: enabledManifests,\n json,\n}) as unknown as (ctx: Record<string, unknown>) => Promise<boolean>;\nconst moduleConfigRoutes = createModuleConfigRoutes({\n projectRoot: env.PROJECT_ROOT,\n enabled: enabledManifests,\n json,\n}) as unknown as (ctx: Record<string, unknown>) => Promise<boolean>;\n\n// \u2500\u2500 \u4E3B\u8BF7\u6C42\u5904\u7406\u5668 \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\u2500\u2500\nasync function handle(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {\n const url = new URL(req.url || \"/\", `http://${req.headers.host || \"localhost\"}`);\n const path = url.pathname;\n const method = req.method || \"GET\";\n const params = url.searchParams;\n\n // loopback \u7ED1\u5B9A\n const remote = req.socket.remoteAddress || \"\";\n if (remote && !remote.startsWith(\"127.\") && remote !== \"::1\" && remote !== \"::ffff:127.\") {\n json(res, { success: false, error: \"forbidden\" }, 403);\n return;\n }\n\n // \u2500\u2500 \u9884\u8BFB body(\u6240\u6709\u8DEF\u7531\u5171\u4EAB) \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 await body(req);\n\n // \u2500\u2500 v2 \u6A21\u5757\u8EAB\u4EFD\u6821\u9A8C \u2192 \u5199\u5165\u8DEF\u7531 ctx.moduleAuth(LoD:\u4E0D\u6302 req \u79C1\u6709\u5B57\u6BB5) \u2500\u2500\n // \u6CE8\u610F:`/api/sfmc/configs/all` \u662F\u65E7\u7684\u4E00\u6B21\u6027\u914D\u7F6E\u5FEB\u7167\u7AEF\u70B9(SAPI ConfigManager.init\n // \u542F\u52A8\u5FC5\u7528),\u4E0D\u5C5E\u4E8E v2 \u6A21\u5757\u914D\u7F6E\u547D\u540D\u7A7A\u95F4(configs/<\u6A21\u5757 configKey>),\u5FC5\u987B\u8C41\u514D,\n // \u5426\u5219\u4F1A\u88AB\u6A21\u5757\u9274\u6743\u7F51\u5173\u62E6\u6210 401,\u5BFC\u81F4\u63D2\u4EF6\u7AEF\u8D77\u4E0D\u6765\u3002\n const isLegacyConfigAll = path === \"/api/sfmc/configs/all\";\n const needsModuleAuth =\n path.startsWith(\"/api/sfmc/db/\") ||\n path.startsWith(\"/api/sfmc/services\") ||\n (/^\\/api\\/sfmc\\/configs\\/[A-Za-z0-9_-]+(?:\\/(?:set|notify))?$/.test(path) && !isLegacyConfigAll);\n let moduleAuthCtx: { id: string; permissions: string[] } | null = null;\n if (needsModuleAuth) {\n const id = verifyModuleAuth({\n headers: req.headers,\n params,\n auth: moduleAuth,\n enabledModuleIds: enabledSet,\n });\n if (!id) {\n // LSP: v2 \u6A21\u5757\u95E8\u4E0E service-routes \u540C\u7528 ok \u65B9\u8A00\n jsonV2Fail(res, \"unauthorized: module identity invalid\", 401, \"unauthorized\");\n return;\n }\n const manifest = enabledManifests.get(id);\n moduleAuthCtx = {\n id,\n permissions: manifest?.permissions ?? [],\n };\n } else {\n // \u2500\u2500 \u65E7 env token \u9274\u6743(\u53EA\u5BF9 POST/PUT \u751F\u6548) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const PUBLIC_GET =\n path === \"/api/health\" ||\n path === \"/api/sfmc/status\" ||\n path === \"/api/sfmc/qq/join/apply-queue\" ||\n path === \"/api/sfmc/qq/join/settings\" ||\n path === \"/api/sfmc/qq/admin/action-queue\" ||\n (method === \"GET\" &&\n (path === \"/api/sfmc/content\" ||\n path === \"/api/sfmc/modules\" ||\n path === \"/api/sfmc/modules/catalog\" ||\n path.startsWith(\"/api/sfmc/modules/\") ||\n path === \"/api/sfmc/qq/bind/me\" ||\n path === \"/api/sfmc/qq/join/pending\"));\n // bind/join/events POST \u4E0E messages \u4E00\u6837\u8D70\u65E7 AUTH_TOKEN\uFF08\u82E5\u914D\u7F6E\uFF09\uFF1B\u6E38\u620F/\u6865\u540C\u4E3A loopback\n const NEEDS_AUTH =\n !PUBLIC_GET &&\n method !== \"GET\" &&\n !(\n path.startsWith(\"/api/sfmc/qq/bind/\") ||\n path.startsWith(\"/api/sfmc/qq/join/\") ||\n path.startsWith(\"/api/sfmc/qq/admin/\") ||\n path.startsWith(\"/api/sfmc/qq/events\")\n );\n if (env.AUTH_TOKEN && NEEDS_AUTH) {\n const auth = req.headers[\"authorization\"] || \"\";\n const provided = auth.startsWith(\"Bearer \") ? auth.slice(7) : (req.headers[\"x-db-token\"] as string) || \"\";\n if (provided !== env.AUTH_TOKEN) {\n json(res, { success: false, error: \"unauthorized\" }, 401);\n return;\n }\n }\n }\n\n try {\n if (path === \"/api/sfmc/player-session\" && method === \"POST\") {\n const data = (await body(req)) as Record<string, unknown>;\n const playerName = String(data.playerName ?? \"\").trim();\n const xuid = String(data.xuid ?? \"\").trim();\n if (!playerName || !xuid) {\n json(res, { success: false, error: \"playerName_and_xuid_required\" }, 400);\n return;\n }\n syncPlayerSession(query, { playerName, xuid });\n json(res, { success: true });\n return;\n }\n // \u2500\u2500 v2 \u8DEF\u7531(\u4F18\u5148\u5339\u914D) \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 (\n moduleAuthCtx &&\n (path.startsWith(\"/api/sfmc/db/\") ||\n path.startsWith(\"/api/sfmc/services\") ||\n (/^\\/api\\/sfmc\\/configs\\/[A-Za-z0-9_-]+/.test(path) && !isLegacyConfigAll))\n ) {\n const ctx: Record<string, unknown> = {\n path,\n method,\n params,\n req,\n res,\n moduleAuth: moduleAuthCtx,\n };\n // body \u5DF2\u5728\u4E0A\u9762 `await body(req)` \u9884\u8BFB\u5E76\u7F13\u5B58\u5230 req._bodyPromise;\n // \u8FD9\u91CC\u590D\u7528\u7F13\u5B58(\u539F\u5B9E\u73B0\u8BFB\u7684 req._body \u4ECE\u672A\u88AB\u8D4B\u503C,\u5BFC\u81F4\u6240\u6709 v2 \u8DEF\u7531 body \u6052\u4E3A\u7A7A)\u3002\n ctx[\"body\"] = await body(req);\n if (path.startsWith(\"/api/sfmc/db/\")) {\n if (await dbRoutes(ctx)) return;\n } else if (path.startsWith(\"/api/sfmc/services\")) {\n if (await serviceRoutes(ctx)) return;\n } else {\n if (await moduleConfigRoutes(ctx)) return;\n }\n }\n\n // \u2500\u2500 \u5E73\u53F0\u8DEF\u7531 \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 const ctxBase = { path, method, params, req, res } as {\n path: string;\n method: string;\n params: URLSearchParams;\n req: http.IncomingMessage;\n res: http.ServerResponse;\n };\n if (await contentRoutes(ctxBase)) return;\n if (await moduleRoutesInstance(ctxBase)) return;\n if (await healthRoutes(ctxBase)) return;\n if (await statusRoutes(ctxBase)) return;\n if (await qqBindRoutes(ctxBase)) return;\n if (await qqJoinRoutes(ctxBase)) return;\n if (await qqEventsRoutes(ctxBase)) return;\n if (await messagesRoutes(ctxBase)) return;\n if (await configRoutes(ctxBase)) return;\n\n json(res, { success: false, error: \"not_found\" }, 404);\n } catch (err) {\n log.err(err, \"DogeDB\");\n json(res, { success: false, error: (err as Error).message }, 500);\n }\n}\n\n// \u2500\u2500 \u542F\u52A8 \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst server = createServer({\n env: { PORT: env.PORT, HOST: env.HOST, AUTH_TOKEN: env.AUTH_TOKEN },\n handle,\n});\nstartConsole(server, db);\n\nexport { db, enabledManifests, env, moduleAuth, query, schemaRegistry, serviceRegistry, txRunner };\n"],
5
- "mappings": "AAoBA,OAAO,UAAU;AACjB,SAAS,WAAW;AAEpB,SAAS,4BAA4B;AACrC,SAAS,kBAAkB;AAC3B,SAAS,yBAAyB;AAClC,SAAS,eAAe;AACxB,SAAS,8BAA8B;AACvC,SAAS,WAAW;AACpB,SAAS,yBAAyB;AAClC,SAAS,aAAa,oBAAoB;AAC1C,SAAS,sBAAsB;AAC/B,SAAS,iBAAiB,wBAAwB;AAClD,SAAS,8BAA8B;AACvC,SAAS,sBAAsB;AAC/B,SAAS,cAAc,oBAAoB;AAC3C,SAAS,uBAAuB;AAChC,SAAS,sCAAsC;AAC/C,SAAS,gBAAgB;AAEzB,SAAS,YAAY,YAAY,WAAW,gBAAgB;AAC5D,OAAO,QAAQ,YAAY;AAE3B,SAAS,kBAAkB;AAC3B,SAAS,sBAAsB;AAC/B,SAAS,gCAAgC;AACzC,SAAS,2BAA2B;AAEpC,SAAS,mBAAmB,0BAA0B;AACtD,SAAS,0BAA0B,6BAA0D;AAC7F,SAAS,QAAQ,YAAY,QAAQ,kBAAkB;AACvD,SAAS,WAAW,gBAAgB,gBAAgB,yBAAyB;AAC7E,SAAS,2BAA2B;AACpC,SAAS,0BAA0B;AACnC,SAAS,2BAA2B;AACpC,SAAS,0BAA0B;AACnC,SAAS,4BAA4B;AACrC,SAAS,0BAA0B;AACnC,SAAS,0BAA0B;AACnC,SAAS,4BAA4B;AACrC,SAAS,0BAAiD;AAC1D,SAAS,0BAA0B;AAEnC,IAAI,CAAC,kBAAkB,IAAI,EAAE,GAAG;AAC9B,UAAQ,KAAK,CAAC;AAChB;AAEA,MAAM,MAAM,QAAQ;AACpB,MAAM,KAAK,aAAa,IAAI,OAAO;AACnC,qBAAqB,EAAE;AACvB,WAAW,EAAE;AACb,MAAM,QAAQ,YAAY,EAAE;AAG5B,MAAM,iBAAiB,eAAe;AACtC,IAAI;AAAA,EACF,wBAAwB,OAAO,KAAK,eAAe,OAAO,EAAE,MAAM,sBAAsB,eAAe,YAAY,IAAI;AACzH;AAGA,MAAM,WAAW,eAAe,IAAI,gBAAgB;AACpD,MAAM,gBAAgB,SAAkC,IAAI,mBAAmB,KAAK,EAAE,SAAS,CAAC,EAAE;AAClG,MAAM,aAAa,IAAI;AAAA,EACrB,MAAM,QAAQ,cAAc,OAAO,IAC9B,cAAc,QAAmC,IAAI,CAAC,MAAM,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,IAC7G,CAAC;AACP;AACA,MAAM,aAAa,oBAAI,IAAY;AACnC,WAAW,MAAM,OAAO,KAAK,eAAe,OAAO,GAAG;AACpD,MAAI,CAAC,WAAW,IAAI,EAAE,EAAG;AACzB,QAAM,eAAgB,cAAc,QAA2C,KAAK,CAAC,MAAM,OAAO,EAAE,EAAE,MAAM,EAAE;AAC9G,MAAI,CAAC,aAAc;AACnB,QAAM,iBAAiB,aAAa,qBAAqB;AACzD,MAAI,UAAU,UAAU,IAAI,cAAc,EAAG,YAAW,IAAI,EAAE;AAChE;AACA,MAAM,mBAAmB,oBAAI,IAAkE;AAC/F,WAAW,MAAM,YAAY;AAC3B,QAAM,IAAI,eAAe,QAAQ,EAAE;AACnC,MAAI,EAAG,kBAAiB,IAAI,IAAI,CAAC;AACnC;AACA,IAAI,KAAK,0BAA0B,CAAC,GAAG,UAAU,EAAE,KAAK,EAAE,KAAK,IAAI,KAAK,QAAQ,EAAE;AAGlF,MAAM,aAAa,gBAAgB;AAAA,EACjC,QAAQ,IAAI;AAAA,EACZ,cAAc,IAAI;AAAA,EAClB,kBAAkB,CAAC,GAAG,UAAU;AAClC,CAAC;AAGD,MAAM,iBAAiB,IAAI,eAAe,EAAE;AAC5C,MAAM,kBAAkB,IAAI,gBAAgB;AAC5C,MAAM,aAAa,uBAAuB,EAAE;AAC5C,MAAM,WAAW,IAAI,SAAS;AAAA,EAC5B;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA,SAAS;AACX,CAAC;AAGD;AACE,QAAM,UAAU,+BAA+B,iBAAiB,EAAE,OAAO,GAAG,GAAG,UAAU;AACzF,MAAI,UAAU,GAAG;AACf,QAAI,QAAQ,wBAAwB,gBAAgB,KAAK,EAAE,MAAM,kBAAkB,OAAO,oBAAoB;AAAA,EAChH;AACF;AAEA,MAAM,OAAO;AACb,MAAM,OAAO;AAGb,SAAS,oBAAoB;AAC3B,SAAO,MAAM,QAAQ,cAAc,OAAO,IAAI,cAAc,UAAU,CAAC;AACzE;AAEA,SAAS,kBAAkB;AACzB,QAAM,UAAU,kBAAkB;AAClC,SAAO,QACJ,IAAI,CAAC,QAAQ;AACZ,UAAM,QACH,IAAgC,SAAS,OAAQ,IAAgC,UAAU,WACtF,IAAgC,QAClC,CAAC;AACP,UAAM,KAAK,OAAQ,IAAgC,MAAM,EAAE,EAAE,KAAK;AAClE,UAAM,YAAY;AAAA,MACf,IAAgC,aAAc,IAAgC,cAAc;AAAA,IAC/F,EAAE,KAAK;AACP,QAAI,CAAC,MAAM,CAAC,UAAW,QAAO;AAC9B,UAAM,QAAQ,SAAS,QAAQ,EAAE;AACjC,UAAM,UAAU,UAAU,UAAU,IAAK,IAAgC,qBAAqB,KAAK;AACnG,WAAO;AAAA,MACL;AAAA,MACA,WAAW;AAAA,MACX,MAAM;AAAA,MACN;AAAA,MACA,YAAY;AAAA,MACZ,cAAc,OAAQ,IAAgC,QAAQ,SAAS;AAAA,MACvE,aAAa,OAAQ,IAAgC,eAAe,EAAE;AAAA,MACtE,iBAAkB,IAAgC,qBAAqB;AAAA,MACvE,aAAa,iBAAiB,GAA8B;AAAA;AAAA,MAE5D,WAAW;AAAA,MACX,UAAU,MAAM,QAAS,IAAgC,QAAQ,IAC3D,IAAgC,SAAuB,OAAO,OAAO,EAAE,IAAI,MAAM,IACnF,CAAC;AAAA,MACL,UAAU,MAAM,QAAS,IAAgC,QAAQ,IAC3D,IAAgC,SAAuB,OAAO,OAAO,EAAE,IAAI,MAAM,IACnF,CAAC;AAAA,MACL,UAAU,MAAM,QAAS,IAAgC,QAAQ,IAC3D,IAAgC,SAAuB,OAAO,OAAO,EAAE,IAAI,MAAM,IACnF,CAAC;AAAA,MACL,OAAO;AAAA,QACL,MAAM,OAAO,MAAM,QAAQ,EAAE;AAAA,QAC7B,MAAM,OAAO,MAAM,QAAQ,EAAE;AAAA,QAC7B,MAAM,OAAO,MAAM,QAAQ,EAAE;AAAA,MAC/B;AAAA,MACA,YAAY,OAAO,aAAa;AAAA,MAChC,SAAS,CAAC,CAAC;AAAA,IACb;AAAA,EACF,CAAC,EACA,OAAO,OAAO;AACnB;AAKA,SAAS,gCAAgC;AACvC,QAAM,WAAW,gBAAgB,KAAK,CAAC,GAAG,QAAQ,CAAC,QAAQ;AACzD,QAAI,CAAC,OAAO,OAAO,QAAQ,SAAU,QAAO,CAAC;AAC7C,UAAM,MAAM;AACZ,UAAM,KAAK,OAAO,IAAI,MAAM,EAAE,EAAE,KAAK;AACrC,QAAI,CAAC,GAAI,QAAO,CAAC;AACjB,WAAO;AAAA,MACL;AAAA,QACE;AAAA,QACA,cAAc,OAAO,IAAI,gBAAgB,EAAE;AAAA,QAC3C,SAAS,IAAI,YAAY;AAAA,MAC3B;AAAA,IACF;AAAA,EACF,CAAC;AACD,QAAM,MAAM,SAAgC,WAAW,IAAI,cAAc,kBAAkB,CAAC;AAC5F,QAAM,SAAS,OAAO,KAAK,aAAa,WAAW,IAAI,SAAS,KAAK,IAAI;AACzE,QAAM,UAAU,SAAU,KAAK,WAAW,MAAM,IAAI,SAAS,KAAK,QAAQ,IAAI,cAAc,MAAM,IAAK;AACvG,SAAO,oBAAoB;AAAA,IACzB,aAAa,KAAK,IAAI,aAAa,UAAU;AAAA,IAC7C;AAAA,IACA;AAAA,EACF,CAAC;AACH;AAGA,SAAS,iBAAiB,KAAuC;AAC/D,SAAO,IAAI,eAAe;AAC5B;AAEA,SAAS,mBAAmB,KAAa;AACvC,QAAM,IAAI,OAAO,OAAO,EAAE,EAAE,KAAK;AACjC,QAAM,UAAU,kBAAkB;AAClC,QAAM,MAAM,QAAQ;AAAA,IAClB,CAAC,MACC,OAAQ,EAA8B,MAAM,EAAE,MAAM,KACpD,OAAQ,EAA8B,aAAc,EAA8B,cAAc,EAAE,MAAM;AAAA,EAC5G;AACA,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,KAAK,OAAO,IAAI,MAAM,EAAE,EAAE,KAAK;AACrC,QAAM,YAAY,OAAO,IAAI,aAAa,IAAI,cAAc,EAAE,EAAE,KAAK;AACrE,MAAI,CAAC,MAAM,CAAC,UAAW,QAAO;AAE9B,SAAO,EAAE,IAAI,WAAW,YAAY,iBAAiB,GAAG,EAAE;AAC5D;AAEA,SAAS,iBAAiB,KAA0C,SAAkB;AAGpF,oBAAkB,UAAU,IAAI,IAAI,EAAE,SAAS,CAAC,CAAC,QAAQ,CAAC;AAE1D,iBAAe,IAAI,kBAAkB,QAAQ;AAG7C,yBAAuB;AAAA,IACrB,UAAU,IAAI;AAAA,IACd,SAAS,CAAC,CAAC;AAAA,IACX,QAAQ,IAAI;AAAA,IACZ,cAAc,IAAI;AAAA,IAClB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,aAAa,EAAE,OAAO,GAAG;AAAA,EAC3B,CAAC;AACH;AAGA,MAAM,eAAe,mBAAmB;AACxC,MAAM,eAAe,mBAAmB,CAAC,CAAC;AAC1C,MAAM,eAAe,mBAAmB,EAAE,OAAO,MAAM,KAAK,CAAC;AAG7D,MAAM,qBAAqB;AAC3B,MAAM,mBAAmB;AAAA,EACvB,mBAAmB;AAAA,EACnB,kBAAkB;AAAA;AAAA,EAElB,8BAA8B;AAChC;AAEA,SAAS,aAAa,GAAY,UAA4B;AAC5D,MAAI,MAAM,UAAa,MAAM,KAAM,QAAO;AAC1C,MAAI,OAAO,MAAM,UAAW,QAAO;AACnC,QAAM,IAAI,OAAO,CAAC,EAAE,KAAK,EAAE,YAAY;AACvC,MAAI,MAAM,UAAU,MAAM,OAAO,MAAM,SAAS,MAAM,KAAM,QAAO;AACnE,MAAI,MAAM,WAAW,MAAM,OAAO,MAAM,QAAQ,MAAM,MAAO,QAAO;AACpE,SAAO;AACT;AAEA,SAAS,mBAA2B;AAClC,SAAO,KAAK,IAAI,cAAc,WAAW,GAAG,kBAAkB,OAAO;AACvE;AAEA,SAAS,gBAAkC;AAEzC,QAAM,OAAO,WAAoC,iBAAiB,GAAG,EAAE,GAAG,iBAAiB,CAAC;AAC5F,SAAO;AAAA,IACL,kBAAkB,aAAa,KAAK,mBAAmB,iBAAiB,iBAAiB;AAAA,IACzF,iBAAiB,aAAa,KAAK,kBAAkB,iBAAiB,gBAAgB;AAAA,IACtF,0BAA0B;AAAA,MACxB,KAAK;AAAA,MACL,iBAAiB;AAAA,IACnB;AAAA,EACF;AACF;AAEA,SAAS,eAAe,SAAsD;AAC5E,QAAM,OAAO,iBAAiB;AAC9B,aAAoC,MAAM,EAAE,GAAG,iBAAiB,CAAC;AACjE,QAAM,QAAiC,CAAC;AACxC,MAAI,QAAQ,qBAAqB,OAAW,OAAM,oBAAoB,QAAQ;AAC9E,MAAI,QAAQ,oBAAoB,OAAW,OAAM,mBAAmB,QAAQ;AAE5E,MAAI,OAAO,KAAK,KAAK,EAAE,SAAS,GAAG;AACjC,cAAU,MAAM,KAAK;AAAA,EACvB;AACA,SAAO,cAAc;AACvB;AAGA;AACE,QAAM,UAAU,CAAC,SAAS,iBAAiB,CAAC;AAC5C,aAAoC,iBAAiB,GAAG,EAAE,GAAG,iBAAiB,CAAC;AAC/E,MAAI,SAAS;AACX,QAAI,KAAK,mEAAsB,kBAAkB,OAAO;AAAA,EAC1D;AACF;AAEA,MAAM,eAAe,mBAAmB;AAAA,EACtC;AAAA,EACA;AAAA,EACA;AAAA,EACA,iBAAiB,MAAM;AACrB,UAAM,MAAO,IAAI,SAAS,UAAU,GAA+C;AACnF,QAAI,CAAC,MAAM,QAAQ,GAAG,EAAG,QAAO,CAAC;AACjC,WAAO,IAAI,IAAI,CAAC,MAAM,OAAO,CAAC,EAAE,KAAK,CAAC,EAAE,OAAO,OAAO;AAAA,EACxD;AAAA,EACA,cAAc,MAAM,cAAc;AAAA,EAClC,cAAc,CAAC,YAAY,eAAe,OAAO;AACnD,CAAC;AAED,SAAS,uBAA+C;AACtD,QAAM,OAAO,SAAkC,KAAK,IAAI,cAAc,WAAW,gBAAgB,CAAC;AAClG,MAAI,CAAC,KAAM,OAAM,IAAI,MAAM,yFAA6B;AACxD,SAAO,sBAAsB,KAAK,SAAS;AAC7C;AAEA,SAAS,sBAAsB,OAAgE;AAC7F,QAAM,OAAO,KAAK,IAAI,cAAc,WAAW,gBAAgB;AAC/D,QAAM,WAAW,EAAE,GAAG,qBAAqB,GAAG,GAAG,MAAM;AACvD,YAAU,MAAM,EAAE,WAAW,SAAS,CAAC;AACvC,SAAO;AACT;AAEA,SAAS,kBAAkB;AACzB,SAAO,mBAAmB;AAAA,IACxB,YAAY,IAAI;AAAA,IAChB,YAAY,IAAI;AAAA,IAChB,YAAY,IAAI;AAAA,IAChB,aAAa,IAAI;AAAA,IACjB,aAAa,IAAI;AAAA,IACjB,WAAW,IAAI;AAAA,IACf,eAAe,IAAI;AAAA,IACnB,YAAY,IAAI;AAAA,IAChB,iBAAiB,IAAI;AAAA,EACvB,CAAC;AACH;AAEA,MAAM,qBAAqB,yBAAyB;AAAA,EAClD,WAAW;AAAA,EACX,aAAa,MAAM,gBAAgB;AACrC,CAAC;AACD,MAAM,iBAAiB,qBAAqB;AAAA,EAC1C;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ,aAAa;AAAA,EACb,aAAa;AAAA,EACb,SAAS,CAAC,QAAQ,iBAAiB;AACjC,UAAM,SAAU,IAAI,SAAS,UAAU,GAA+C;AACtF,WACG,MAAM,QAAQ,MAAM,KAAK,OAAO,SAAS,MAAM,KAC/C,gBAAgB,cAAc,EAAE;AAAA,EAErC;AACF,CAAC;AAED,MAAM,iBAAiB,qBAAqB;AAAA,EAC1C;AAAA,EACA;AAAA,EACA;AAAA,EACA,iBAAiB,CAAC,cAAsB;AACtC,UAAM,OAAO;AAAA,MACX,kDAAkD,SAAS;AAAA,IAC7D;AACA,WAAO,MAAM,QAAQ,IAAI,IACpB,KAAK,CAAC,KAAiI,OACxI;AAAA,EACN;AAAA,EACA,mBAAmB,CAAC,WAAmB,QAAgB,UAAkB,SAAiB,WACxF,kBAAkB,gBAAgB,GAAG,WAAW,QAAQ,UAAU,SAAS,MAAM;AACrF,CAAC;AACD,MAAM,eAAe,mBAAmB;AAAA,EACtC;AAAA,EACA,aAAa,IAAI;AAAA;AAAA,EAEjB,aAAa,MAAM,gBAAgB;AAAA,EACnC,iBAAiB,OAAO,EAAE,GAAG,WAAW,OAAO;AACjD,CAAC;AACD,MAAM,gBAAgB,oBAAoB;AAAA,EACxC;AAAA,EACA,cAAc;AAChB,CAAC;AACD,MAAM,uBAAuB,mBAAmB;AAAA,EAC9C;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAKD,MAAM,WAAW,eAAe,EAAE,gBAAgB,UAAU,YAAY,KAAK,CAAC;AAG9E,MAAM,gBAAgB,oBAAoB;AAAA,EACxC;AAAA,EACA,SAAS;AAAA,EACT;AACF,CAAC;AACD,MAAM,qBAAqB,yBAAyB;AAAA,EAClD,aAAa,IAAI;AAAA,EACjB,SAAS;AAAA,EACT;AACF,CAAC;AAGD,eAAe,OAAO,KAA2B,KAAyC;AACxF,QAAM,MAAM,IAAI,IAAI,IAAI,OAAO,KAAK,UAAU,IAAI,QAAQ,QAAQ,WAAW,EAAE;AAC/E,QAAMA,QAAO,IAAI;AACjB,QAAM,SAAS,IAAI,UAAU;AAC7B,QAAM,SAAS,IAAI;AAGnB,QAAM,SAAS,IAAI,OAAO,iBAAiB;AAC3C,MAAI,UAAU,CAAC,OAAO,WAAW,MAAM,KAAK,WAAW,SAAS,WAAW,eAAe;AACxF,SAAK,KAAK,EAAE,SAAS,OAAO,OAAO,YAAY,GAAG,GAAG;AACrD;AAAA,EACF;AAGA,QAAM,KAAK,GAAG;AAMd,QAAM,oBAAoBA,UAAS;AACnC,QAAM,kBACJA,MAAK,WAAW,eAAe,KAC/BA,MAAK,WAAW,oBAAoB,KACnC,8DAA8D,KAAKA,KAAI,KAAK,CAAC;AAChF,MAAI,gBAA8D;AAClE,MAAI,iBAAiB;AACnB,UAAM,KAAK,iBAAiB;AAAA,MAC1B,SAAS,IAAI;AAAA,MACb;AAAA,MACA,MAAM;AAAA,MACN,kBAAkB;AAAA,IACpB,CAAC;AACD,QAAI,CAAC,IAAI;AAEP,iBAAW,KAAK,yCAAyC,KAAK,cAAc;AAC5E;AAAA,IACF;AACA,UAAM,WAAW,iBAAiB,IAAI,EAAE;AACxC,oBAAgB;AAAA,MACd;AAAA,MACA,aAAa,UAAU,eAAe,CAAC;AAAA,IACzC;AAAA,EACF,OAAO;AAEL,UAAM,aACJA,UAAS,iBACTA,UAAS,sBACTA,UAAS,mCACTA,UAAS,gCACTA,UAAS,qCACR,WAAW,UACTA,UAAS,uBACRA,UAAS,uBACTA,UAAS,+BACTA,MAAK,WAAW,oBAAoB,KACpCA,UAAS,0BACTA,UAAS;AAEf,UAAM,aACJ,CAAC,cACD,WAAW,SACX,EACEA,MAAK,WAAW,oBAAoB,KACpCA,MAAK,WAAW,oBAAoB,KACpCA,MAAK,WAAW,qBAAqB,KACrCA,MAAK,WAAW,qBAAqB;AAEzC,QAAI,IAAI,cAAc,YAAY;AAChC,YAAM,OAAO,IAAI,QAAQ,eAAe,KAAK;AAC7C,YAAM,WAAW,KAAK,WAAW,SAAS,IAAI,KAAK,MAAM,CAAC,IAAK,IAAI,QAAQ,YAAY,KAAgB;AACvG,UAAI,aAAa,IAAI,YAAY;AAC/B,aAAK,KAAK,EAAE,SAAS,OAAO,OAAO,eAAe,GAAG,GAAG;AACxD;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,MAAI;AACF,QAAIA,UAAS,8BAA8B,WAAW,QAAQ;AAC5D,YAAM,OAAQ,MAAM,KAAK,GAAG;AAC5B,YAAM,aAAa,OAAO,KAAK,cAAc,EAAE,EAAE,KAAK;AACtD,YAAM,OAAO,OAAO,KAAK,QAAQ,EAAE,EAAE,KAAK;AAC1C,UAAI,CAAC,cAAc,CAAC,MAAM;AACxB,aAAK,KAAK,EAAE,SAAS,OAAO,OAAO,+BAA+B,GAAG,GAAG;AACxE;AAAA,MACF;AACA,wBAAkB,OAAO,EAAE,YAAY,KAAK,CAAC;AAC7C,WAAK,KAAK,EAAE,SAAS,KAAK,CAAC;AAC3B;AAAA,IACF;AAEA,QACE,kBACCA,MAAK,WAAW,eAAe,KAC9BA,MAAK,WAAW,oBAAoB,KACnC,wCAAwC,KAAKA,KAAI,KAAK,CAAC,oBAC1D;AACA,YAAM,MAA+B;AAAA,QACnC,MAAAA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,YAAY;AAAA,MACd;AAGA,UAAI,MAAM,IAAI,MAAM,KAAK,GAAG;AAC5B,UAAIA,MAAK,WAAW,eAAe,GAAG;AACpC,YAAI,MAAM,SAAS,GAAG,EAAG;AAAA,MAC3B,WAAWA,MAAK,WAAW,oBAAoB,GAAG;AAChD,YAAI,MAAM,cAAc,GAAG,EAAG;AAAA,MAChC,OAAO;AACL,YAAI,MAAM,mBAAmB,GAAG,EAAG;AAAA,MACrC;AAAA,IACF;AAGA,UAAM,UAAU,EAAE,MAAAA,OAAM,QAAQ,QAAQ,KAAK,IAAI;AAOjD,QAAI,MAAM,cAAc,OAAO,EAAG;AAClC,QAAI,MAAM,qBAAqB,OAAO,EAAG;AACzC,QAAI,MAAM,aAAa,OAAO,EAAG;AACjC,QAAI,MAAM,aAAa,OAAO,EAAG;AACjC,QAAI,MAAM,aAAa,OAAO,EAAG;AACjC,QAAI,MAAM,aAAa,OAAO,EAAG;AACjC,QAAI,MAAM,eAAe,OAAO,EAAG;AACnC,QAAI,MAAM,eAAe,OAAO,EAAG;AACnC,QAAI,MAAM,aAAa,OAAO,EAAG;AAEjC,SAAK,KAAK,EAAE,SAAS,OAAO,OAAO,YAAY,GAAG,GAAG;AAAA,EACvD,SAAS,KAAK;AACZ,QAAI,IAAI,KAAK,QAAQ;AACrB,SAAK,KAAK,EAAE,SAAS,OAAO,OAAQ,IAAc,QAAQ,GAAG,GAAG;AAAA,EAClE;AACF;AAGA,MAAM,SAAS,aAAa;AAAA,EAC1B,KAAK,EAAE,MAAM,IAAI,MAAM,MAAM,IAAI,MAAM,YAAY,IAAI,WAAW;AAAA,EAClE;AACF,CAAC;AACD,aAAa,QAAQ,EAAE;",
4
+ "sourcesContent": ["/**\n *\n * \u5DE5\u4F5C\u6D41:\n * 1. \u52A0\u8F7D env\n * 2. \u6821\u9A8C Node \u7248\u672C\n * 3. openDatabase + createPlatformTables(sfmc__audit / sfmc__idempotent)\n * 4. loadManifestV2() \u2014 \u5931\u8D25 = \u542F\u52A8\u5931\u8D25\n * 5. filterEnabled(loaded, lockFileEnabled)\n * 6. buildModuleAuth({auth_token, enabled_modules}) \u2190 \u4E0E DB \u540C\u76EE\u5F55 module-tokens.json\n * 7. \u5B9E\u4F8B\u5316:SchemaRegistry / ServiceRegistry / IdempotencyStore / TxRunner\n * 8. \u88C5\u914D v2 \u8DEF\u7531 (/api/sfmc/db/* + /api/sfmc/services* + /api/sfmc/configs/:key/*)\n * \u53E6\u4FDD\u7559 messages(qq-bridge) + config/modules/health \u5E73\u53F0\u8DEF\u7531\n * 9. createServer + listen\n *\n * \u9274\u6743(handle \u5C42):\n * - /api/sfmc/db/*, /api/sfmc/services*, /api/sfmc/configs/:key(/set|notify)*\n * \u2192 module token (Authorization: Bearer ...) + ?moduleId=...\n * - \u5176\u4ED6:env.AUTH_TOKEN \u65E7 auth(NEEDS_AUTH)\n */\n\nimport http from \"node:http\";\nimport { SQL } from \"sql-template-strings\";\n\nimport { createPlatformTables } from \"./db-tables.js\";\nimport { syncPlayerSession } from \"./domain/player-session.js\";\nimport { initSchema } from \"./domain/schema.js\";\nimport { loadEnv } from \"./env.js\";\nimport { createIdempotencyStore } from \"./lib/idempotency-store.js\";\nimport { log } from \"./lib/log.js\";\nimport { assertNodeVersion } from \"./lib/runtime.js\";\nimport { createQuery, openDatabase } from \"./lib/sqlite.js\";\nimport { loadManifestV2 } from \"./manifest-loader.js\";\nimport { buildModuleAuth, verifyModuleAuth } from \"./module-auth.js\";\nimport { syncModuleRuntimeState } from \"./module-runtime-sync.js\";\nimport { SchemaRegistry } from \"./schema-registry.js\";\nimport { createServer, startConsole } from \"./server.js\";\nimport { ServiceRegistry } from \"./service-registry.js\";\nimport { registerEnabledBuiltinServices } from \"./services/builtin-handlers.js\";\nimport { TxRunner } from \"./tx-runner.js\";\n\nimport { configPath, ensureJson, patchJson, readJson } from \"@sfmc-bds/sdk/node/config\";\nimport path, { join } from \"node:path\";\n\nimport { jsonV2Fail } from \"./routes/_shared.js\";\nimport { createDbRoutes } from \"./routes/db-routes.js\";\nimport { createModuleConfigRoutes } from \"./routes/module-config-routes.js\";\nimport { createServiceRoutes } from \"./routes/service-routes.js\";\n\nimport { forwardToQQBridge, makeOutboundConfig } from \"./domain/bridge.js\";\nimport { loadContentSnapshot } from \"./domain/installed-content.js\";\nimport { createQqEventsAggregator, resolveQqEventsConfig, type ResolvedQqEventsConfig } from \"./domain/qq-events.js\";\nimport { body as sharedBody, json as sharedJson } from \"./lib/http.js\";\nimport { isEnabled, loadModuleLock, saveModuleLock, updateModuleState } from \"./lib/module-state.js\";\nimport { createConfigRoutes } from \"./routes/config.js\";\nimport { createContentRoutes } from \"./routes/content.js\";\nimport { createHealthRoutes } from \"./routes/health.js\";\nimport { createMessagesRoutes, gameChatForwardPayload, type ChatChannelQQFlags } from \"./routes/messages.js\";\nimport { createModuleRoutes } from \"./routes/modules.js\";\nimport { createQqBindRoutes } from \"./routes/qq-bind.js\";\nimport { createQqEventsRoutes } from \"./routes/qq-events.js\";\nimport { createQqJoinRoutes, type JoinFeatureFlags } from \"./routes/qq-join.js\";\nimport { createStatusRoutes } from \"./routes/status.js\";\n\nif (!assertNodeVersion(22, 13)) {\n process.exit(2);\n}\n\nconst env = loadEnv();\nconst db = openDatabase(env.DB_PATH);\ncreatePlatformTables(db); // sfmc__audit / sfmc__idempotent\ninitSchema(db); // \u5E73\u53F0\u4E1A\u52A1 bootstrap \u8868(players/world/chat/...)\u2014\u2014qq-bridge \u5728 SAPI defineTable \u4E4B\u524D\u5C31\u9700\u8981\nconst query = createQuery(db);\n\n// \u2500\u2500 v2 manifest \u52A0\u8F7D(\u5931\u8D25 = \u542F\u52A8\u5931\u8D25)\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\nconst loadedManifest = loadManifestV2(); // throws on violation\nlog.success(\n `[manifest v2] loaded ${Object.keys(loadedManifest.modules).length} modules; provides ${loadedManifest.providesMap.size} services`\n);\n\n// \u2500\u2500 enabled \u96C6\u5408(\u4ECE lock file)\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\nconst lockFile = loadModuleLock(env.MODULE_LOCK_PATH);\nconst moduleCatalog = readJson<{ modules?: unknown[] }>(env.MODULE_CATALOG_PATH) ?? { modules: [] };\nconst catalogIds = new Set(\n Array.isArray(moduleCatalog.modules)\n ? (moduleCatalog.modules as Array<{ id?: string }>).map((m) => String(m.id || \"\")).filter((id) => id.length > 0)\n : []\n);\nconst enabledSet = new Set<string>();\nfor (const id of Object.keys(loadedManifest.modules)) {\n if (!catalogIds.has(id)) continue;\n const catalogEntry = (moduleCatalog.modules as Array<Record<string, unknown>>).find((m) => String(m.id) === id);\n if (!catalogEntry) continue;\n const defaultEnabled = catalogEntry.enabledByDefault !== false;\n if (isEnabled(lockFile, id, defaultEnabled)) enabledSet.add(id);\n}\nconst enabledManifests = new Map<string, NonNullable<(typeof loadedManifest.modules)[string]>>();\nfor (const id of enabledSet) {\n const m = loadedManifest.modules[id];\n if (m) enabledManifests.set(id, m);\n}\nlog.info(`[manifest v2] enabled: ${[...enabledSet].sort().join(\", \") || \"(none)\"}`);\n\n// \u2500\u2500 \u6A21\u5757 HMAC token map(\u4E0E DB \u540C\u76EE\u5F55 module-tokens.json)\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst moduleAuth = buildModuleAuth({\n dbPath: env.DB_PATH,\n envAuthToken: env.AUTH_TOKEN,\n enabledModuleIds: [...enabledSet],\n});\n\n// \u2500\u2500 \u4E09\u4EF6\u5957 + \u8DEF\u7531\u5DE5\u5382 \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\nconst schemaRegistry = new SchemaRegistry(db);\nconst serviceRegistry = new ServiceRegistry();\nconst idempotent = createIdempotencyStore(db);\nconst txRunner = new TxRunner({\n db,\n query,\n schema: schemaRegistry,\n serviceRegistry,\n enabled: enabledManifests,\n});\n\n// \u2500\u2500 \u8FDB\u7A0B\u5185\u7F6E service handler(\u6269\u5C55\u70B9:BUILTIN_SERVICE_PLUGINS) \u2500\u2500\n{\n const plugins = registerEnabledBuiltinServices(serviceRegistry, { query, db }, enabledSet);\n if (plugins > 0) {\n log.success(`[service] registered ${serviceRegistry.list().length} handlers from ${plugins} builtin plugin(s)`);\n }\n}\n\nconst json = sharedJson;\nconst body = sharedBody;\n\n// \u2500\u2500 \u5DE5\u5177\u51FD\u6570 \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\u2500\u2500\u2500\u2500\nfunction loadModuleCatalog() {\n return Array.isArray(moduleCatalog.modules) ? moduleCatalog.modules : [];\n}\n\nfunction buildModuleList() {\n const catalog = loadModuleCatalog();\n return catalog\n .map((raw) => {\n const entry =\n (raw as Record<string, unknown>).entry && typeof (raw as Record<string, unknown>).entry === \"object\"\n ? ((raw as Record<string, unknown>).entry as Record<string, unknown>)\n : {};\n const id = String((raw as Record<string, unknown>).id || \"\").trim();\n const configKey = String(\n (raw as Record<string, unknown>).configKey || (raw as Record<string, unknown>).config_key || \"\"\n ).trim();\n if (!id || !configKey) return null;\n const state = lockFile.modules[id];\n const enabled = isEnabled(lockFile, id, (raw as Record<string, unknown>).enabledByDefault !== false);\n return {\n id,\n module_id: id,\n name: configKey,\n configKey,\n config_key: configKey,\n display_name: String((raw as Record<string, unknown>).name || configKey),\n description: String((raw as Record<string, unknown>).description || \"\"),\n default_enabled: (raw as Record<string, unknown>).enabledByDefault !== false,\n can_disable: moduleCanDisable(raw as Record<string, unknown>),\n // ConfigManager \u8BA4 installed!==false;\u5DF2\u88C5\u5305\u9ED8\u8BA4 true\n installed: true,\n requires: Array.isArray((raw as Record<string, unknown>).requires)\n ? ((raw as Record<string, unknown>).requires as unknown[]).filter(Boolean).map(String)\n : [],\n optional: Array.isArray((raw as Record<string, unknown>).optional)\n ? ((raw as Record<string, unknown>).optional as unknown[]).filter(Boolean).map(String)\n : [],\n commands: Array.isArray((raw as Record<string, unknown>).commands)\n ? ((raw as Record<string, unknown>).commands as unknown[]).filter(Boolean).map(String)\n : [],\n entry: {\n kind: String(entry.kind || \"\"),\n path: String(entry.path || \"\"),\n init: String(entry.init || \"\"),\n },\n updated_at: state?.updatedAt ?? null,\n enabled: !!enabled,\n };\n })\n .filter(Boolean);\n}\n\n/**\n * QQ\u300C\u6A21\u5757 / \u8D44\u6E90\u5305\u300D\u5FEB\u7167\u3002\u6BCF\u6B21\u8BF7\u6C42\u73B0\u8BFB catalog \u4E0E\u78C1\u76D8\uFF0C\u88C5\u5305\u540E\u4E0D\u7528\u91CD\u542F db-server\u3002\n */\nfunction loadContentSnapshotForRequest() {\n const modules = (buildModuleList() ?? []).flatMap((raw) => {\n if (!raw || typeof raw !== \"object\") return [];\n const row = raw as { id?: unknown; display_name?: unknown; enabled?: unknown };\n const id = String(row.id ?? \"\").trim();\n if (!id) return [];\n return [\n {\n id,\n display_name: String(row.display_name || id),\n enabled: row.enabled !== false,\n },\n ];\n });\n const cfg = readJson<{ bds_path?: string }>(configPath(env.PROJECT_ROOT, \"bds_updater.json\"));\n const rawBds = typeof cfg?.bds_path === \"string\" ? cfg.bds_path.trim() : \"\";\n const bdsRoot = rawBds ? (path.isAbsolute(rawBds) ? rawBds : path.resolve(env.PROJECT_ROOT, rawBds)) : null;\n return loadContentSnapshot({\n packagesDir: join(env.MODULES_DIR, \"packages\"),\n modules,\n bdsRoot,\n });\n}\n\n/** catalog \u7701\u7565 canDisable \u65F6\u9ED8\u8BA4\u5141\u8BB8\u7981\u7528(\u4E0E buildModuleList.can_disable \u540C\u6E90)\u3002 */\nfunction moduleCanDisable(raw: Record<string, unknown>): boolean {\n return raw.canDisable !== false;\n}\n\nfunction resolveModuleByKey(key: string) {\n const k = String(key || \"\").trim();\n const catalog = loadModuleCatalog();\n const raw = catalog.find(\n (m) =>\n String((m as Record<string, unknown>).id || \"\") === k ||\n String((m as Record<string, unknown>).configKey || (m as Record<string, unknown>).config_key || \"\") === k\n ) as Record<string, unknown> | undefined;\n if (!raw) return null;\n const id = String(raw.id || \"\").trim();\n const configKey = String(raw.configKey || raw.config_key || \"\").trim();\n if (!id || !configKey) return null;\n /* LSP:\u4E0E buildModuleList.can_disable \u540C\u6E90 \u2014 \u7701\u7565\u5B57\u6BB5\u89C6\u4E3A\u53EF\u7981\u7528 */\n return { id, configKey, canDisable: moduleCanDisable(raw) };\n}\n\nfunction setModuleEnabled(mod: { id: string; canDisable: boolean }, enabled: boolean) {\n // \u76F4\u63A5\u66F4\u65B0\u542F\u52A8\u65F6\u7F13\u5B58\u7684 lockFile(\u800C\u975E\u53E6\u8BFB\u4E00\u4EFD\u65B0\u526F\u672C),\n // \u5426\u5219 buildModuleList() \u8BFB\u7684\u4ECD\u662F\u65E7\u7F13\u5B58,\u5BFC\u81F4\u542F\u505C\u540E enabled \u72B6\u6001\u4E0D\u7FFB\u8F6C\u3002\n updateModuleState(lockFile, mod.id, { enabled: !!enabled });\n // DRY:\u4E0E loadModuleLock \u5BF9\u79F0\u8D70 saveModuleLock,\u52FF\u6563\u843D writeJson\n saveModuleLock(env.MODULE_LOCK_PATH, lockFile);\n\n // DIP:\u70ED\u540C\u6B65 enabledSet / tokens / manifests / builtin handlers(\u4E0D\u91CD\u542F db-server)\n syncModuleRuntimeState({\n moduleId: mod.id,\n enabled: !!enabled,\n dbPath: env.DB_PATH,\n envAuthToken: env.AUTH_TOKEN,\n enabledSet,\n enabledManifests,\n loadedManifest,\n moduleAuth,\n serviceRegistry,\n builtinDeps: { query, db },\n });\n}\n\n// \u2500\u2500 \u5E73\u53F0\u8DEF\u7531(\u975E\u6A21\u5757\u4E1A\u52A1) \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\nconst healthRoutes = createHealthRoutes();\nconst statusRoutes = createStatusRoutes({});\nconst qqBindRoutes = createQqBindRoutes({ query, body, json });\n\n/** \u6A21\u5757 qq-link \u7684\u914D\u7F6E\u6587\u4EF6\uFF08\u975E SDK qq_config\uFF09 */\nconst QQ_LINK_CONFIG_KEY = \"qq_link\";\nconst QQ_LINK_DEFAULTS = {\n allowlist_enabled: true,\n require_approval: true,\n /** \u662F\u5426\u5C06 QQ \u7FA4\u4E3B/\u7FA4\u7BA1\u89C6\u4F5C SFMC \u7BA1\u7406\u5458\uFF1B\u7F3A\u7701\u5F00\u542F\uFF0C\u4EC5\u6539\u672C\u6587\u4EF6\u53EF\u5173\u95ED\uFF0CAPI/\u7FA4\u804A\u4E0D\u53EF\u5199 */\n treat_group_admins_as_admins: true,\n} as const;\n\nfunction asConfigBool(v: unknown, fallback: boolean): boolean {\n if (v === undefined || v === null) return fallback;\n if (typeof v === \"boolean\") return v;\n const s = String(v).trim().toLowerCase();\n if (s === \"true\" || s === \"1\" || s === \"yes\" || s === \"on\") return true;\n if (s === \"false\" || s === \"0\" || s === \"no\" || s === \"off\") return false;\n return fallback;\n}\n\nfunction qqLinkConfigFile(): string {\n return join(env.PROJECT_ROOT, \"configs\", `${QQ_LINK_CONFIG_KEY}.json`);\n}\n\nfunction readJoinFlags(): JoinFeatureFlags {\n // \u6A21\u5757\u914D\u7F6E configs/qq_link.json\uFF08\u975E SDK qq_config\uFF09\n const disk = ensureJson<Record<string, unknown>>(qqLinkConfigFile(), { ...QQ_LINK_DEFAULTS });\n return {\n allowlistEnabled: asConfigBool(disk.allowlist_enabled, QQ_LINK_DEFAULTS.allowlist_enabled),\n requireApproval: asConfigBool(disk.require_approval, QQ_LINK_DEFAULTS.require_approval),\n treatGroupAdminsAsAdmins: asConfigBool(\n disk.treat_group_admins_as_admins,\n QQ_LINK_DEFAULTS.treat_group_admins_as_admins\n ),\n };\n}\n\nfunction writeJoinFlags(partial: Partial<JoinFeatureFlags>): JoinFeatureFlags {\n const file = qqLinkConfigFile();\n ensureJson<Record<string, unknown>>(file, { ...QQ_LINK_DEFAULTS });\n const patch: Record<string, boolean> = {};\n if (partial.allowlistEnabled !== undefined) patch.allowlist_enabled = partial.allowlistEnabled;\n if (partial.requireApproval !== undefined) patch.require_approval = partial.requireApproval;\n // \u6545\u610F\u4E0D\u5199 treat_group_admins_as_admins\uFF1A\u4EC5\u4EBA\u5DE5\u6539 configs/qq_link.json\n if (Object.keys(patch).length > 0) {\n patchJson(file, patch);\n }\n return readJoinFlags();\n}\n\n// \u542F\u52A8\u5373\u843D\u76D8\u6A21\u5757\u9ED8\u8BA4\u914D\u7F6E\uFF0C\u907F\u514D\u300C\u4ECE\u672A\u70B9\u8FC7\u914D\u7F6E/\u7533\u8BF7\u300D\u65F6 configs/qq_link.json \u4E0D\u5B58\u5728\n{\n const created = !readJson(qqLinkConfigFile());\n ensureJson<Record<string, unknown>>(qqLinkConfigFile(), { ...QQ_LINK_DEFAULTS });\n if (created) {\n log.info(`\u5DF2\u5199\u5165\u6A21\u5757\u914D\u7F6E\u9AA8\u67B6: configs/${QQ_LINK_CONFIG_KEY}.json`);\n }\n}\n\nconst qqJoinRoutes = createQqJoinRoutes({\n query,\n body,\n json,\n getAdminOpenids: () => {\n const raw = (env.qqconfig[\"official\"] as { admin_openids?: unknown } | undefined)?.admin_openids;\n if (!Array.isArray(raw)) return [];\n return raw.map((x) => String(x).trim()).filter(Boolean);\n },\n getJoinFlags: () => readJoinFlags(),\n setJoinFlags: (partial) => writeJoinFlags(partial),\n});\n\nfunction readQqEventsSettings(): ResolvedQqEventsConfig {\n const disk = readJson<Record<string, unknown>>(join(env.PROJECT_ROOT, \"configs\", \"qq_config.json\"));\n if (!disk) throw new Error(\"qq_config.json \u65E0\u6CD5\u8BFB\u53D6\uFF0C\u4E8B\u4EF6\u8BBE\u7F6E\u672A\u4FEE\u6539\");\n return resolveQqEventsConfig(disk.qq_events);\n}\n\nfunction writeQqEventsSettings(patch: Partial<ResolvedQqEventsConfig>): ResolvedQqEventsConfig {\n const file = join(env.PROJECT_ROOT, \"configs\", \"qq_config.json\");\n const settings = { ...readQqEventsSettings(), ...patch };\n patchJson(file, { qq_events: settings });\n return settings;\n}\n\nfunction currentOutbound() {\n return makeOutboundConfig({\n QQ_BACKEND: env.QQ_BACKEND,\n LLBOT_HOST: env.LLBOT_HOST,\n LLBOT_PORT: env.LLBOT_PORT,\n LLBOT_TOKEN: env.LLBOT_TOKEN,\n QQ_GROUP_ID: env.QQ_GROUP_ID,\n QQ_APP_ID: env.QQ_APP_ID,\n QQ_APP_SECRET: env.QQ_APP_SECRET,\n QQ_SANDBOX: env.QQ_SANDBOX,\n QQ_GROUP_OPENID: env.QQ_GROUP_OPENID,\n });\n}\n\nconst qqEventsAggregator = createQqEventsAggregator({\n getConfig: readQqEventsSettings,\n getOutbound: () => currentOutbound(),\n});\nconst qqEventsRoutes = createQqEventsRoutes({\n body,\n json,\n aggregator: qqEventsAggregator,\n getSettings: readQqEventsSettings,\n setSettings: writeQqEventsSettings,\n isAdmin: (openid, asGroupAdmin) => {\n const admins = (env.qqconfig[\"official\"] as { admin_openids?: unknown } | undefined)?.admin_openids;\n return (\n (Array.isArray(admins) && admins.includes(openid)) || (asGroupAdmin && readJoinFlags().treatGroupAdminsAsAdmins)\n );\n },\n});\n\n/** \u8BFB\u53D6\u5355\u4E2A\u804A\u5929\u9891\u9053\u3002\u8868\u5C1A\u672A\u7531\u804A\u5929\u6A21\u5757\u5EFA\u51FA\u65F6\u8FD4\u56DE null\uFF0C\u4E0D\u8BA9\u8BF7\u6C42 500\u3002 */\nfunction readChatChannel(channelId: string): ChatChannelQQFlags | null {\n try {\n const rows = query(SQL`SELECT * FROM sfmc_chat_channels WHERE id = ${channelId}`);\n return Array.isArray(rows) ? ((rows[0] as ChatChannelQQFlags | undefined) ?? null) : null;\n } catch (err) {\n log.warn(`\u8BFB\u53D6\u804A\u5929\u9891\u9053\u5931\u8D25: ${(err as Error).message}`);\n return null;\n }\n}\n\n/** \u6D88\u606F\u6765\u6E90\u4E3A QQ \u7684\u9891\u9053\u3002QQ \u5165\u7AD9\u53EA\u6295\u5230\u8FD9\u4E9B\u9891\u9053\u3002 */\nfunction listQQSourceChannels(): ChatChannelQQFlags[] {\n try {\n const rows = query(SQL`SELECT * FROM sfmc_chat_channels`);\n if (!Array.isArray(rows)) return [];\n return (rows as ChatChannelQQFlags[]).filter((row) => row.source_qq === 1);\n } catch (err) {\n log.warn(`\u8BFB\u53D6 QQ \u6765\u6E90\u9891\u9053\u5931\u8D25: ${(err as Error).message}`);\n return [];\n }\n}\n\nconst messagesRoutes = createMessagesRoutes({\n query,\n body,\n json,\n getChannelForQQ: readChatChannel,\n listQQSourceChannels,\n forwardToQQBridge: (channelId: string, prefix: string, fromName: string, content: string, fromId: string) =>\n forwardToQQBridge(currentOutbound(), channelId, prefix, fromName, content, fromId),\n});\n\n// \u6E38\u620F\u53D1\u8A00\u8D70 db.tx \u63D2\u5165\uFF0C\u4E0D\u7ECF\u8FC7 /api/sfmc/messages\u3002\u63D0\u4EA4\u540E\u518D\u6309\u9891\u9053\u5F00\u5173\u8F6C\u53D1\u5230 QQ\u3002\ntxRunner.setAfterCommit((inserts) => {\n for (const item of inserts) {\n if (item.table !== \"sfmc_chat_messages\") continue;\n const payload = gameChatForwardPayload(item.row, readChatChannel(String(item.row.channel_id ?? \"\")));\n if (!payload) continue;\n forwardToQQBridge(\n currentOutbound(),\n payload.channelId,\n payload.prefix,\n payload.fromName,\n payload.content,\n payload.fromId,\n );\n }\n});\nconst configRoutes = createConfigRoutes({\n json,\n projectRoot: env.PROJECT_ROOT,\n // DIP:\u8DEF\u7531\u4E0D\u8BFB lock/catalog/token \u6587\u4EF6,\u7531\u5165\u53E3\u6CE8\u5165\u4E0E /modules \u540C\u6E90\u6570\u636E\n listModules: () => buildModuleList() as Array<Record<string, unknown>>,\n getModuleTokens: () => ({ ...moduleAuth.tokens }),\n});\nconst contentRoutes = createContentRoutes({\n json,\n loadSnapshot: loadContentSnapshotForRequest,\n});\nconst moduleRoutesInstance = createModuleRoutes({\n loadModuleCatalog,\n buildModuleList: buildModuleList as unknown as () => Array<Record<string, unknown>>,\n resolveModuleByKey,\n setModuleEnabled,\n body,\n json,\n});\n\n// \u2500\u2500 v2 \u8DEF\u7531\u5DE5\u5382 \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\u2500\u2500\u2500\u2500\u2500\n// \u7C7B\u578B\u65AD\u8A00\u6210 unknown \u51FD\u6570 \u2014 v2 routes \u7684 ctx \u7C7B\u578B\u4E0E RouteCtx \u4E0D\u517C\u5BB9,\n// \u4F46\u8C03\u7528\u5951\u7EA6({path, method, params, req, res, body?})\u662F\u7A33\u5B9A\u7684\u3002\nconst dbRoutes = createDbRoutes({ schemaRegistry, txRunner, idempotent, json }) as unknown as (\n ctx: Record<string, unknown>\n) => Promise<boolean>;\nconst serviceRoutes = createServiceRoutes({\n serviceRegistry,\n enabled: enabledManifests,\n json,\n}) as unknown as (ctx: Record<string, unknown>) => Promise<boolean>;\nconst moduleConfigRoutes = createModuleConfigRoutes({\n projectRoot: env.PROJECT_ROOT,\n enabled: enabledManifests,\n json,\n}) as unknown as (ctx: Record<string, unknown>) => Promise<boolean>;\n\n// \u2500\u2500 \u4E3B\u8BF7\u6C42\u5904\u7406\u5668 \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\u2500\u2500\nasync function handle(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {\n const url = new URL(req.url || \"/\", `http://${req.headers.host || \"localhost\"}`);\n const path = url.pathname;\n const method = req.method || \"GET\";\n const params = url.searchParams;\n\n // loopback \u7ED1\u5B9A\n const remote = req.socket.remoteAddress || \"\";\n if (remote && !remote.startsWith(\"127.\") && remote !== \"::1\" && remote !== \"::ffff:127.\") {\n json(res, { success: false, error: \"forbidden\" }, 403);\n return;\n }\n\n // \u2500\u2500 \u9884\u8BFB body(\u6240\u6709\u8DEF\u7531\u5171\u4EAB) \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 await body(req);\n\n // \u2500\u2500 v2 \u6A21\u5757\u8EAB\u4EFD\u6821\u9A8C \u2192 \u5199\u5165\u8DEF\u7531 ctx.moduleAuth(LoD:\u4E0D\u6302 req \u79C1\u6709\u5B57\u6BB5) \u2500\u2500\n // \u6CE8\u610F:`/api/sfmc/configs/all` \u662F\u65E7\u7684\u4E00\u6B21\u6027\u914D\u7F6E\u5FEB\u7167\u7AEF\u70B9(SAPI ConfigManager.init\n // \u542F\u52A8\u5FC5\u7528),\u4E0D\u5C5E\u4E8E v2 \u6A21\u5757\u914D\u7F6E\u547D\u540D\u7A7A\u95F4(configs/<\u6A21\u5757 configKey>),\u5FC5\u987B\u8C41\u514D,\n // \u5426\u5219\u4F1A\u88AB\u6A21\u5757\u9274\u6743\u7F51\u5173\u62E6\u6210 401,\u5BFC\u81F4\u63D2\u4EF6\u7AEF\u8D77\u4E0D\u6765\u3002\n const isLegacyConfigAll = path === \"/api/sfmc/configs/all\";\n const needsModuleAuth =\n path.startsWith(\"/api/sfmc/db/\") ||\n path.startsWith(\"/api/sfmc/services\") ||\n (/^\\/api\\/sfmc\\/configs\\/[A-Za-z0-9_-]+(?:\\/(?:set|notify))?$/.test(path) && !isLegacyConfigAll);\n let moduleAuthCtx: { id: string; permissions: string[] } | null = null;\n if (needsModuleAuth) {\n const id = verifyModuleAuth({\n headers: req.headers,\n params,\n auth: moduleAuth,\n enabledModuleIds: enabledSet,\n });\n if (!id) {\n // LSP: v2 \u6A21\u5757\u95E8\u4E0E service-routes \u540C\u7528 ok \u65B9\u8A00\n jsonV2Fail(res, \"unauthorized: module identity invalid\", 401, \"unauthorized\");\n return;\n }\n const manifest = enabledManifests.get(id);\n moduleAuthCtx = {\n id,\n permissions: manifest?.permissions ?? [],\n };\n } else {\n // \u2500\u2500 \u65E7 env token \u9274\u6743(\u53EA\u5BF9 POST/PUT \u751F\u6548) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n const PUBLIC_GET =\n path === \"/api/health\" ||\n path === \"/api/sfmc/status\" ||\n path === \"/api/sfmc/qq/join/apply-queue\" ||\n path === \"/api/sfmc/qq/join/settings\" ||\n path === \"/api/sfmc/qq/admin/action-queue\" ||\n (method === \"GET\" &&\n (path === \"/api/sfmc/content\" ||\n path === \"/api/sfmc/modules\" ||\n path === \"/api/sfmc/modules/catalog\" ||\n path.startsWith(\"/api/sfmc/modules/\") ||\n path === \"/api/sfmc/qq/bind/me\" ||\n path === \"/api/sfmc/qq/join/pending\"));\n // bind/join/events POST \u4E0E messages \u4E00\u6837\u8D70\u65E7 AUTH_TOKEN\uFF08\u82E5\u914D\u7F6E\uFF09\uFF1B\u6E38\u620F/\u6865\u540C\u4E3A loopback\n const NEEDS_AUTH =\n !PUBLIC_GET &&\n method !== \"GET\" &&\n !(\n path.startsWith(\"/api/sfmc/qq/bind/\") ||\n path.startsWith(\"/api/sfmc/qq/join/\") ||\n path.startsWith(\"/api/sfmc/qq/admin/\") ||\n path.startsWith(\"/api/sfmc/qq/events\")\n );\n if (env.AUTH_TOKEN && NEEDS_AUTH) {\n const auth = req.headers[\"authorization\"] || \"\";\n const provided = auth.startsWith(\"Bearer \") ? auth.slice(7) : (req.headers[\"x-db-token\"] as string) || \"\";\n if (provided !== env.AUTH_TOKEN) {\n json(res, { success: false, error: \"unauthorized\" }, 401);\n return;\n }\n }\n }\n\n try {\n if (path === \"/api/sfmc/player-session\" && method === \"POST\") {\n const data = (await body(req)) as Record<string, unknown>;\n const playerName = String(data.playerName ?? \"\").trim();\n const xuid = String(data.xuid ?? \"\").trim();\n if (!playerName || !xuid) {\n json(res, { success: false, error: \"playerName_and_xuid_required\" }, 400);\n return;\n }\n syncPlayerSession(query, { playerName, xuid });\n json(res, { success: true });\n return;\n }\n // \u2500\u2500 v2 \u8DEF\u7531(\u4F18\u5148\u5339\u914D) \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 (\n moduleAuthCtx &&\n (path.startsWith(\"/api/sfmc/db/\") ||\n path.startsWith(\"/api/sfmc/services\") ||\n (/^\\/api\\/sfmc\\/configs\\/[A-Za-z0-9_-]+/.test(path) && !isLegacyConfigAll))\n ) {\n const ctx: Record<string, unknown> = {\n path,\n method,\n params,\n req,\n res,\n moduleAuth: moduleAuthCtx,\n };\n // body \u5DF2\u5728\u4E0A\u9762 `await body(req)` \u9884\u8BFB\u5E76\u7F13\u5B58\u5230 req._bodyPromise;\n // \u8FD9\u91CC\u590D\u7528\u7F13\u5B58(\u539F\u5B9E\u73B0\u8BFB\u7684 req._body \u4ECE\u672A\u88AB\u8D4B\u503C,\u5BFC\u81F4\u6240\u6709 v2 \u8DEF\u7531 body \u6052\u4E3A\u7A7A)\u3002\n ctx[\"body\"] = await body(req);\n if (path.startsWith(\"/api/sfmc/db/\")) {\n if (await dbRoutes(ctx)) return;\n } else if (path.startsWith(\"/api/sfmc/services\")) {\n if (await serviceRoutes(ctx)) return;\n } else {\n if (await moduleConfigRoutes(ctx)) return;\n }\n }\n\n // \u2500\u2500 \u5E73\u53F0\u8DEF\u7531 \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 const ctxBase = { path, method, params, req, res } as {\n path: string;\n method: string;\n params: URLSearchParams;\n req: http.IncomingMessage;\n res: http.ServerResponse;\n };\n if (await contentRoutes(ctxBase)) return;\n if (await moduleRoutesInstance(ctxBase)) return;\n if (await healthRoutes(ctxBase)) return;\n if (await statusRoutes(ctxBase)) return;\n if (await qqBindRoutes(ctxBase)) return;\n if (await qqJoinRoutes(ctxBase)) return;\n if (await qqEventsRoutes(ctxBase)) return;\n if (await messagesRoutes(ctxBase)) return;\n if (await configRoutes(ctxBase)) return;\n\n json(res, { success: false, error: \"not_found\" }, 404);\n } catch (err) {\n log.err(err, \"DogeDB\");\n json(res, { success: false, error: (err as Error).message }, 500);\n }\n}\n\n// \u2500\u2500 \u542F\u52A8 \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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst server = createServer({\n env: { PORT: env.PORT, HOST: env.HOST, AUTH_TOKEN: env.AUTH_TOKEN },\n handle,\n});\nstartConsole(server, db);\n\nexport { db, enabledManifests, env, moduleAuth, query, schemaRegistry, serviceRegistry, txRunner };\n"],
5
+ "mappings": "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",
6
6
  "names": ["path"]
7
7
  }
@@ -6,25 +6,50 @@
6
6
  * POST /api/sfmc/messages — 批量 INSERT OR REPLACE 消息(按频道开关转发至 QQ)
7
7
  */
8
8
  import type { QueryFn } from "../lib/sqlite.js";
9
+ /** 频道上与 QQ 互通相关的列。游戏发言出站和 QQ 入站都读这份结构。 */
10
+ export interface ChatChannelQQFlags {
11
+ id?: string;
12
+ prefix: string;
13
+ forward_to_qq: number;
14
+ source_game?: number;
15
+ source_qq?: number;
16
+ source_system?: number;
17
+ /** 私聊不提供转发开关,提交后的出站要跳过。 */
18
+ type?: string;
19
+ }
9
20
  interface Deps {
10
21
  query: QueryFn;
11
22
  body: (req: import("http").IncomingMessage) => Promise<Record<string, unknown>>;
12
23
  json: (res: import("http").ServerResponse, data: Record<string, unknown>, status?: number) => void;
13
24
  forwardToQQBridge: (channelId: string, prefix: string, fromName: string, content: string, fromId: string) => void;
14
25
  /** 查询来源频道的前缀和 QQ 转发开关。 */
15
- getChannelForQQ: (channelId: string) => {
16
- prefix: string;
17
- forward_to_qq: number;
18
- source_game?: number;
19
- source_qq?: number;
20
- source_system?: number;
21
- } | null;
26
+ getChannelForQQ: (channelId: string) => ChatChannelQQFlags | null;
27
+ /** 消息来源为 QQ 的频道。入站不看请求里的频道 id。 */
28
+ listQQSourceChannels: () => ChatChannelQQFlags[];
22
29
  }
23
30
  /** 是否应将本条消息推到 QQ 群 */
24
31
  export declare function shouldForwardChatToQQ(channel: {
25
32
  forward_to_qq: number;
26
33
  } | null, fromId: string): boolean;
27
- declare function createMessagesRoutes({ query, body, json, forwardToQQBridge, getChannelForQQ }: Deps): ({ path, method, params, req, res, }: {
34
+ /**
35
+ * 只保留消息来源为 QQ 的频道。
36
+ * 使用场景:QQ 入站扇出,不使用桥接请求里写死的频道 id。
37
+ */
38
+ export declare function channelsAcceptingQQ<T extends {
39
+ source_qq?: number;
40
+ }>(channels: T[]): T[];
41
+ /**
42
+ * 游戏侧 db.tx 插入的聊天行是否要转发到 QQ。
43
+ * 字段名是蛇形(from_id / channel_id),与 /api/sfmc/messages 的驼峰载荷不同。
44
+ */
45
+ export declare function gameChatForwardPayload(row: Record<string, unknown>, channel: ChatChannelQQFlags | null): {
46
+ channelId: string;
47
+ prefix: string;
48
+ fromName: string;
49
+ content: string;
50
+ fromId: string;
51
+ } | null;
52
+ declare function createMessagesRoutes({ query, body, json, forwardToQQBridge, getChannelForQQ, listQQSourceChannels }: Deps): ({ path, method, params, req, res, }: {
28
53
  path: string;
29
54
  method: string;
30
55
  params: URLSearchParams;
@@ -1 +1 @@
1
- {"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../src/routes/messages.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAEhD,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;IACnG,iBAAiB,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAClH,0BAA0B;IAC1B,eAAe,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK;QACtC,MAAM,EAAE,MAAM,CAAC;QACf,aAAa,EAAE,MAAM,CAAC;QACtB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,aAAa,CAAC,EAAE,MAAM,CAAC;KACxB,GAAG,IAAI,CAAC;CACV;AAED,sBAAsB;AACtB,wBAAgB,qBAAqB,CACnC,OAAO,EAAE;IAAE,aAAa,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,EACzC,MAAM,EAAE,MAAM,GACb,OAAO,CAQT;AAED,iBAAS,oBAAoB,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,iBAAiB,EAAE,eAAe,EAAE,EAAE,IAAI,yCAOxF;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,CA8ErB;AAED,OAAO,EAAE,oBAAoB,EAAE,CAAC"}
1
+ {"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../src/routes/messages.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAEhD,0CAA0C;AAC1C,MAAM,WAAW,kBAAkB;IACjC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,2BAA2B;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,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;IACnG,iBAAiB,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAClH,0BAA0B;IAC1B,eAAe,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,kBAAkB,GAAG,IAAI,CAAC;IAClE,kCAAkC;IAClC,oBAAoB,EAAE,MAAM,kBAAkB,EAAE,CAAC;CAClD;AAED,sBAAsB;AACtB,wBAAgB,qBAAqB,CACnC,OAAO,EAAE;IAAE,aAAa,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,EACzC,MAAM,EAAE,MAAM,GACb,OAAO,CAQT;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,SAAS;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,GAAG,CAAC,EAAE,CAExF;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5B,OAAO,EAAE,kBAAkB,GAAG,IAAI,GACjC;IACD,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB,GAAG,IAAI,CAcP;AAED,iBAAS,oBAAoB,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,iBAAiB,EAAE,eAAe,EAAE,oBAAoB,EAAE,EAAE,IAAI,yCAO9G;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,CAqGrB;AAED,OAAO,EAAE,oBAAoB,EAAE,CAAC"}
@@ -1,4 +1,5 @@
1
1
  import { SQL } from "sql-template-strings";
2
+ import { log } from "../lib/log.js";
2
3
  function shouldForwardChatToQQ(channel, fromId) {
3
4
  if (!channel || channel.forward_to_qq !== 1) return false;
4
5
  const fid = String(fromId ?? "").trim();
@@ -6,7 +7,24 @@ function shouldForwardChatToQQ(channel, fromId) {
6
7
  if (fid.startsWith("qq_")) return false;
7
8
  return true;
8
9
  }
9
- function createMessagesRoutes({ query, body, json, forwardToQQBridge, getChannelForQQ }) {
10
+ function channelsAcceptingQQ(channels) {
11
+ return channels.filter((channel) => channel.source_qq === 1);
12
+ }
13
+ function gameChatForwardPayload(row, channel) {
14
+ const channelId = String(row.channel_id ?? row.channelId ?? "").trim();
15
+ const fromId = String(row.from_id ?? row.fromid ?? "");
16
+ if (fromId === "system" || fromId === "SYSTEM" || channel?.type === "private") return null;
17
+ if (!channelId || !shouldForwardChatToQQ(channel, fromId)) return null;
18
+ const prefix = String(channel?.prefix ?? "").trim() || channelId;
19
+ return {
20
+ channelId,
21
+ prefix,
22
+ fromName: String(row.from_name ?? row.fromName ?? ""),
23
+ content: String(row.content ?? ""),
24
+ fromId
25
+ };
26
+ }
27
+ function createMessagesRoutes({ query, body, json, forwardToQQBridge, getChannelForQQ, listQQSourceChannels }) {
10
28
  return async function handle({
11
29
  path,
12
30
  method,
@@ -43,27 +61,48 @@ function createMessagesRoutes({ query, body, json, forwardToQQBridge, getChannel
43
61
  json(res, { success: false, error: "too many requests" }, 413);
44
62
  return true;
45
63
  }
46
- for (const m of messages) {
47
- const channel = getChannelForQQ(String(m.channelId ?? ""));
64
+ const incoming = messages;
65
+ const qqDestinations = channelsAcceptingQQ(listQQSourceChannels()).filter(
66
+ (channel) => String(channel.id ?? "").trim() !== ""
67
+ );
68
+ for (const m of incoming) {
48
69
  const fromId = String(m.fromid ?? "");
49
- const sourceField = fromId.startsWith("qq_") ? "source_qq" : fromId === "system" || fromId === "SYSTEM" ? "source_system" : "source_game";
70
+ if (fromId.startsWith("qq_")) continue;
71
+ const channel = getChannelForQQ(String(m.channelId ?? ""));
72
+ const sourceField = fromId === "system" || fromId === "SYSTEM" ? "source_system" : "source_game";
50
73
  if (channel?.[sourceField] === 0) {
51
74
  json(res, { success: false, error: "channel source disabled" }, 403);
52
75
  return true;
53
76
  }
54
77
  }
55
- for (const m of messages) {
78
+ const insertMessage = (id, channelId, m) => {
56
79
  query(
57
80
  SQL`INSERT OR REPLACE INTO sfmc_chat_messages (
58
81
  id, channel_id, from_id, from_name, type, content, attachment, show_timestamp, created_at
59
82
  ) VALUES (
60
- ${m.id}, ${m.channelId}, ${m.fromid}, ${m.fromName},
83
+ ${id}, ${channelId}, ${m.fromid}, ${m.fromName},
61
84
  ${m.type ?? "text"}, ${m.content}, ${m.attachment ?? null},
62
85
  ${m.showTimestamp ? 1 : 0}, ${m.timestamp}
63
86
  )`
64
87
  );
88
+ };
89
+ let sawQQInbound = false;
90
+ for (const m of incoming) {
91
+ const fromId = String(m.fromid ?? "");
92
+ if (fromId.startsWith("qq_")) {
93
+ sawQQInbound = true;
94
+ for (const channel of qqDestinations) {
95
+ const channelId = String(channel.id);
96
+ insertMessage(`${String(m.id ?? "")}__${channelId}`, channelId, m);
97
+ }
98
+ continue;
99
+ }
100
+ insertMessage(m.id, m.channelId, m);
101
+ }
102
+ if (sawQQInbound && qqDestinations.length === 0) {
103
+ log.warn("\u6CA1\u6709\u6D88\u606F\u6765\u6E90\u4E3A QQ \u7684\u9891\u9053\uFF0CQQ \u5165\u7AD9\u672A\u5199\u5165");
65
104
  }
66
- for (const m of messages) {
105
+ for (const m of incoming) {
67
106
  const channelId = String(m.channelId ?? "");
68
107
  const fromId = String(m.fromid ?? "");
69
108
  if (!fromId.trim() || fromId.startsWith("qq_")) continue;
@@ -77,7 +116,7 @@ function createMessagesRoutes({ query, body, json, forwardToQQBridge, getChannel
77
116
  fromId
78
117
  );
79
118
  }
80
- json(res, { success: true });
119
+ json(res, { success: true, qqChannels: sawQQInbound ? qqDestinations.length : void 0 });
81
120
  } else {
82
121
  json(res, { success: false, error: "not_found" }, 404);
83
122
  }
@@ -87,7 +126,9 @@ function createMessagesRoutes({ query, body, json, forwardToQQBridge, getChannel
87
126
  };
88
127
  }
89
128
  export {
129
+ channelsAcceptingQQ,
90
130
  createMessagesRoutes,
131
+ gameChatForwardPayload,
91
132
  shouldForwardChatToQQ
92
133
  };
93
134
  //# sourceMappingURL=messages.js.map
@@ -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\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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",
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 { log } from \"../lib/log.js\";\nimport type { QueryFn } from \"../lib/sqlite.js\";\n\n/** \u9891\u9053\u4E0A\u4E0E QQ \u4E92\u901A\u76F8\u5173\u7684\u5217\u3002\u6E38\u620F\u53D1\u8A00\u51FA\u7AD9\u548C QQ \u5165\u7AD9\u90FD\u8BFB\u8FD9\u4EFD\u7ED3\u6784\u3002 */\nexport interface ChatChannelQQFlags {\n id?: string;\n prefix: string;\n forward_to_qq: number;\n source_game?: number;\n source_qq?: number;\n source_system?: number;\n /** \u79C1\u804A\u4E0D\u63D0\u4F9B\u8F6C\u53D1\u5F00\u5173\uFF0C\u63D0\u4EA4\u540E\u7684\u51FA\u7AD9\u8981\u8DF3\u8FC7\u3002 */\n type?: string;\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 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) => ChatChannelQQFlags | null;\n /** \u6D88\u606F\u6765\u6E90\u4E3A QQ \u7684\u9891\u9053\u3002\u5165\u7AD9\u4E0D\u770B\u8BF7\u6C42\u91CC\u7684\u9891\u9053 id\u3002 */\n listQQSourceChannels: () => ChatChannelQQFlags[];\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\n/**\n * \u53EA\u4FDD\u7559\u6D88\u606F\u6765\u6E90\u4E3A QQ \u7684\u9891\u9053\u3002\n * \u4F7F\u7528\u573A\u666F\uFF1AQQ \u5165\u7AD9\u6247\u51FA\uFF0C\u4E0D\u4F7F\u7528\u6865\u63A5\u8BF7\u6C42\u91CC\u5199\u6B7B\u7684\u9891\u9053 id\u3002\n */\nexport function channelsAcceptingQQ<T extends { source_qq?: number }>(channels: T[]): T[] {\n return channels.filter((channel) => channel.source_qq === 1);\n}\n\n/**\n * \u6E38\u620F\u4FA7 db.tx \u63D2\u5165\u7684\u804A\u5929\u884C\u662F\u5426\u8981\u8F6C\u53D1\u5230 QQ\u3002\n * \u5B57\u6BB5\u540D\u662F\u86C7\u5F62\uFF08from_id / channel_id\uFF09\uFF0C\u4E0E /api/sfmc/messages \u7684\u9A7C\u5CF0\u8F7D\u8377\u4E0D\u540C\u3002\n */\nexport function gameChatForwardPayload(\n row: Record<string, unknown>,\n channel: ChatChannelQQFlags | null,\n): {\n channelId: string;\n prefix: string;\n fromName: string;\n content: string;\n fromId: string;\n} | null {\n const channelId = String(row.channel_id ?? row.channelId ?? \"\").trim();\n const fromId = String(row.from_id ?? row.fromid ?? \"\");\n // \u5E7F\u64AD / SYS \u4E0D\u662F\u73A9\u5BB6\u53D1\u8A00\u3002\u79C1\u804A\u6CA1\u6709\u8F6C\u53D1\u5F00\u5173\uFF0C\u907F\u514D\u65E7\u6570\u636E\u628A\u79C1\u804A\u63A8\u5230\u7FA4\u91CC\u3002\n if (fromId === \"system\" || fromId === \"SYSTEM\" || channel?.type === \"private\") return null;\n if (!channelId || !shouldForwardChatToQQ(channel, fromId)) return null;\n const prefix = String(channel?.prefix ?? \"\").trim() || channelId;\n return {\n channelId,\n prefix,\n fromName: String(row.from_name ?? row.fromName ?? \"\"),\n content: String(row.content ?? \"\"),\n fromId,\n };\n}\n\nfunction createMessagesRoutes({ query, body, json, forwardToQQBridge, getChannelForQQ, listQQSourceChannels }: 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 const incoming = messages as Array<Record<string, unknown>>;\n // QQ \u5165\u7AD9\u4E0D\u770B\u8BF7\u6C42\u9891\u9053\uFF0C\u53EA\u5199\u5165\u6D88\u606F\u6765\u6E90\u4E3A QQ \u7684\u9891\u9053\u3002\n const qqDestinations = channelsAcceptingQQ(listQQSourceChannels()).filter(\n (channel) => String(channel.id ?? \"\").trim() !== \"\",\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 incoming) {\n const fromId = String(m.fromid ?? \"\");\n if (fromId.startsWith(\"qq_\")) continue;\n const channel = getChannelForQQ(String(m.channelId ?? \"\"));\n const sourceField =\n fromId === \"system\" || fromId === \"SYSTEM\" ? \"source_system\" : \"source_game\";\n if (channel?.[sourceField] === 0) {\n json(res, { success: false, error: \"channel source disabled\" }, 403);\n return true;\n }\n }\n const insertMessage = (\n id: unknown,\n channelId: unknown,\n m: Record<string, unknown>,\n ) => {\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 ${id}, ${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 let sawQQInbound = false;\n for (const m of incoming) {\n const fromId = String(m.fromid ?? \"\");\n if (fromId.startsWith(\"qq_\")) {\n sawQQInbound = true;\n // \u6BCF\u4E2A\u76EE\u6807\u9891\u9053\u4E00\u884C\uFF0C\u4E3B\u952E\u5E26\u4E0A\u9891\u9053 id\uFF0C\u907F\u514D\u591A\u9891\u9053\u4E92\u76F8\u8986\u76D6\u3002\n for (const channel of qqDestinations) {\n const channelId = String(channel.id);\n insertMessage(`${String(m.id ?? \"\")}__${channelId}`, channelId, m);\n }\n continue;\n }\n insertMessage(m.id, m.channelId, m);\n }\n if (sawQQInbound && qqDestinations.length === 0) {\n log.warn(\"\u6CA1\u6709\u6D88\u606F\u6765\u6E90\u4E3A QQ \u7684\u9891\u9053\uFF0CQQ \u5165\u7AD9\u672A\u5199\u5165\");\n }\n for (const m of incoming) {\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, qqChannels: sawQQInbound ? qqDestinations.length : undefined });\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
  }
@@ -19,7 +19,7 @@ export type JoinFeatureFlags = {
19
19
  /** 是否需要管理员审批 */
20
20
  requireApproval: boolean;
21
21
  /**
22
- * 是否将 QQ 群主/群管理员视作 SFMC 管理员。
22
+ * 是否将 QQ 群主/群管理员视作 SFMC 管理员。缺省开启。
23
23
  * 仅 configs/qq_link.json 可改,API/群聊不可写。
24
24
  */
25
25
  treatGroupAdminsAsAdmins: boolean;
@@ -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 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"],
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\u7F3A\u7701\u5F00\u542F\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
5
  "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -118,6 +118,11 @@ export interface TxRequest {
118
118
  moduleId: string;
119
119
  steps: TxStep[];
120
120
  }
121
+ /** 事务提交成功后的插入行。聊天消息据此决定是否转发到 QQ。 */
122
+ export interface CommittedInsert {
123
+ table: string;
124
+ row: Record<string, unknown>;
125
+ }
121
126
  export interface TxRunnerOptions {
122
127
  /** 等待全局 SQLite 事务槽的最长时间。 */
123
128
  slotWaitTimeoutMs?: number;
@@ -138,7 +143,17 @@ export declare class TxRunner {
138
143
  private readonly transactionWaiters;
139
144
  private readonly slotWaitTimeoutMs;
140
145
  private readonly sessionIdleTimeoutMs;
146
+ /**
147
+ * 提交成功后的插入通知。游戏聊天经 db.tx 落库,不走 /api/sfmc/messages,
148
+ * 因此 MC→QQ 要在这里挂钩。钩子不得抛错影响已提交结果。
149
+ */
150
+ private afterCommit?;
141
151
  constructor(deps: TxRunnerDeps, options?: TxRunnerOptions);
152
+ /**
153
+ * 注册提交成功后的插入回调。
154
+ * 使用场景:db-server 启动后把聊天行转发给 QQ,避免事务层依赖具体桥接实现。
155
+ */
156
+ setAfterCommit(hook: (inserts: CommittedInsert[]) => void): void;
142
157
  run(req: TxRequest): Promise<TxResponse | TxError>;
143
158
  /**
144
159
  * 开启交互式事务会话(协议流程:begin → step* → commit | rollback)。
@@ -172,6 +187,8 @@ export declare class TxRunner {
172
187
  * @returns 提交结果响应。
173
188
  */
174
189
  commitSession(txId: string, moduleId: string): TxResponse | TxError;
190
+ /** 只通知插入行。回调失败只记日志,事务已经提交。 */
191
+ private emitAfterCommit;
175
192
  /**
176
193
  * 回滚当前交互式事务会话,撤销所有未提交的数据库变更。
177
194
  * 具有幂等性:会话已结束或已回滚时同样返回成功。
@@ -1 +1 @@
1
- {"version":3,"file":"tx-runner.d.ts","sourceRoot":"","sources":["../src/tx-runner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGhD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAE7D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAiB,KAAK,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC5E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAS5C,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,OAAO,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE;QACL,KAAK,CAAC,EAAE,SAAS,CAAC;QAClB,+CAA+C;QAC/C,OAAO,CAAC,EACJ;YAAE,KAAK,CAAC,EAAE,MAAM,CAAC;YAAC,GAAG,CAAC,EAAE,MAAM,CAAC;YAAC,GAAG,CAAC,EAAE,KAAK,GAAG,MAAM,CAAA;SAAE,GACtD,KAAK,CAAC;YAAE,KAAK,CAAC,EAAE,MAAM,CAAC;YAAC,GAAG,CAAC,EAAE,MAAM,CAAC;YAAC,GAAG,CAAC,EAAE,KAAK,GAAG,MAAM,CAAA;SAAE,CAAC,CAAC;QAClE,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC;CACH;AAED,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,KAAK,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,QAAQ,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC9B;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,QAAQ,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,QAAQ,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,OAAO,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,OAAO,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,SAAS,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED,MAAM,MAAM,MAAM,GAAG,WAAW,GAAG,SAAS,GAAG,YAAY,GAAG,YAAY,GAAG,YAAY,GAAG,WAAW,GAAG,aAAa,CAAC;AAExH,MAAM,MAAM,YAAY,GACpB;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAA;CAAE,GAChD;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;CAAE,GAClD;IAAE,EAAE,EAAE,QAAQ,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,GAC9C;IAAE,EAAE,EAAE,QAAQ,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,GAC9C;IAAE,EAAE,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACjC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAChC;IAAE,EAAE,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,CAAC;AAEvC,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,IAAI,CAAC;IACT,OAAO,EAAE,YAAY,EAAE,CAAC;CACzB;AAED,MAAM,MAAM,OAAO,GAAG;IACpB,EAAE,EAAE,KAAK,CAAC;IACV,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EACA,mBAAmB,GACnB,WAAW,GACX,iBAAiB,GACjB,iBAAiB,GACjB,cAAc,GACd,kBAAkB,GAClB,UAAU,GACV,eAAe,CAAC;CACrB,CAAC;AAEF,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAaD,MAAM,WAAW,eAAe;IAC9B,4BAA4B;IAC5B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,4BAA4B;IAC5B,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,YAAY,CAAC;IACjB,KAAK,EAAE,OAAO,iBAAiB,EAAE,OAAO,CAAC;IACzC,MAAM,EAAE,cAAc,CAAC;IACvB,eAAe,EAAE,eAAe,CAAC;IACjC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;CACxC;AAED,qBAAa,QAAQ;IAQjB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAPvB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAgC;IACzD,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAA2B;IAC9D,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAS;IAC3C,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAS;IAE9C,YACmB,IAAI,EAAE,YAAY,EACnC,OAAO,GAAE,eAAoB,EAI9B;IAEK,GAAG,CAAC,GAAG,EAAE,SAAS,GAAG,OAAO,CAAC,UAAU,GAAG,OAAO,CAAC,CA+CvD;IAED;;;;;;;OAOG;IACG,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,CAuBlF;IAED;;;;;;;OAOG;IACG,WAAW,CACf,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,MAAM,EAAE,YAAY,CAAA;KAAE,GAAG,OAAO,CAAC,CA0BvD;IAED;;;;;;OAMG;IACH,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,UAAU,GAAG,OAAO,CAoBlE;IAED;;;;;;;OAOG;IACH,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG;QAAE,EAAE,EAAE,IAAI,CAAA;KAAE,GAAG,OAAO,CAWtE;IAED,OAAO,CAAC,YAAY;YAUN,sBAAsB;IAkBpC,OAAO,CAAC,sBAAsB;IAU9B,OAAO,CAAC,sBAAsB;IAU9B,OAAO,CAAC,YAAY;IAkBpB,OAAO,CAAC,QAAQ;IAQhB,gCAAgC;YAClB,MAAM;IAmBpB,OAAO,CAAC,gBAAgB;IAKxB,OAAO,CAAC,iBAAiB;IAKzB,OAAO,CAAC,qBAAqB;IAS7B,OAAO,CAAC,OAAO;IA+Bf,4DAA4D;IAC5D,OAAO,CAAC,OAAO;IA0Bf,OAAO,CAAC,KAAK;IASb,OAAO,CAAC,QAAQ;IAmBhB,OAAO,CAAC,QAAQ;IAkBhB,OAAO,CAAC,QAAQ;IAoBhB,OAAO,CAAC,OAAO;YAYD,SAAS;CAUxB"}
1
+ {"version":3,"file":"tx-runner.d.ts","sourceRoot":"","sources":["../src/tx-runner.ts"],"names":[],"mappings":"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"}
package/dist/tx-runner.js CHANGED
@@ -20,6 +20,18 @@ class TxRunner {
20
20
  transactionWaiters = [];
21
21
  slotWaitTimeoutMs;
22
22
  sessionIdleTimeoutMs;
23
+ /**
24
+ * 提交成功后的插入通知。游戏聊天经 db.tx 落库,不走 /api/sfmc/messages,
25
+ * 因此 MC→QQ 要在这里挂钩。钩子不得抛错影响已提交结果。
26
+ */
27
+ afterCommit;
28
+ /**
29
+ * 注册提交成功后的插入回调。
30
+ * 使用场景:db-server 启动后把聊天行转发给 QQ,避免事务层依赖具体桥接实现。
31
+ */
32
+ setAfterCommit(hook) {
33
+ this.afterCommit = hook;
34
+ }
23
35
  async run(req) {
24
36
  const { moduleId, steps } = req;
25
37
  const manifest = this.deps.enabled.get(moduleId);
@@ -56,6 +68,7 @@ class TxRunner {
56
68
  return { ok: false, step: -1, error: `COMMIT \u5931\u8D25: ${e.message}`, code: "internal" };
57
69
  }
58
70
  this.releaseTransactionSlot();
71
+ this.emitAfterCommit(steps);
59
72
  log.info(`[tx ${traceId}] module=${moduleId} ${steps.length} steps OK`);
60
73
  return { ok: true, results };
61
74
  }
@@ -82,6 +95,7 @@ class TxRunner {
82
95
  const txId = randomUUID();
83
96
  this.sessions.set(txId, {
84
97
  moduleId,
98
+ steps: [],
85
99
  results: [],
86
100
  idleTimer: this.createSessionIdleTimer(txId, moduleId)
87
101
  });
@@ -113,6 +127,7 @@ class TxRunner {
113
127
  const stepIndex = session.results.length;
114
128
  try {
115
129
  const r = await this.runOne(moduleId, manifest, step);
130
+ session.steps.push(step);
116
131
  session.results.push(r);
117
132
  session.idleTimer = this.createSessionIdleTimer(txId, moduleId);
118
133
  return { ok: true, result: r };
@@ -146,11 +161,28 @@ class TxRunner {
146
161
  return { ok: false, step: -1, error: `COMMIT \u5931\u8D25: ${e.message}`, code: "internal" };
147
162
  }
148
163
  const results = session.results;
164
+ const steps = session.steps;
149
165
  this.sessions.delete(txId);
150
166
  this.releaseTransactionSlot();
167
+ this.emitAfterCommit(steps);
151
168
  log.info(`[tx-session ${txId.slice(0, 8)}] commit ${results.length} steps OK`);
152
169
  return { ok: true, results };
153
170
  }
171
+ /** 只通知插入行。回调失败只记日志,事务已经提交。 */
172
+ emitAfterCommit(steps) {
173
+ const hook = this.afterCommit;
174
+ if (!hook) return;
175
+ const inserts = [];
176
+ for (const step of steps) {
177
+ if (step.op === "insert") inserts.push({ table: step.table, row: step.row });
178
+ }
179
+ if (inserts.length === 0) return;
180
+ try {
181
+ hook(inserts);
182
+ } catch (err) {
183
+ log.warn(`[tx] afterCommit: ${err.message}`);
184
+ }
185
+ }
154
186
  /**
155
187
  * 回滚当前交互式事务会话,撤销所有未提交的数据库变更。
156
188
  * 具有幂等性:会话已结束或已回滚时同样返回成功。
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/tx-runner.ts"],
4
- "sourcesContent": ["/**\n * tx-runner.ts \u2014 \u4E8B\u52A1 RPC \u5904\u7406\u5668\n *\n * \u534F\u8BAE(\u4E24\u79CD):\n * A. \u6279\u91CF: POST /api/sfmc/db/tx body={ steps } \u2192 { ok, results? } | { ok:false, step, error, code }\n * B. \u4EA4\u4E92: begin \u2192 step* \u2192 commit|rollback(SDK db.tx \u7528;\u56DE\u8C03\u5185\u53EF\u8BFB\u56DE query/get/call)\n *\n * \u6D41\u7A0B(\u6279\u91CF):\n * 1. \u6821\u9A8C moduleId in enabled\n * 2. BEGIN IMMEDIATE\n * 3. \u987A\u5E8F\u8DD1 steps \u2014 \u4EFB\u4E00\u629B\u9519 \u2192 ROLLBACK + reply { ok:false, step:<i>, ... }\n * 4. \u5168\u8FC7 \u2192 COMMIT + reply { ok:true, results }\n *\n * step.op:\n * - query { table, opts? }\n * - get { table, id }\n * - insert { table, row }\n * - update { table, id, patch }\n * - delete { table, id, hard }\n * - audit { table, rowId, action, data? } \u2192 \u5E73\u53F0 sfmc__audit \u8868\n * - service { name, input } \u2192 \u6D3E\u53D1;\u5931\u8D25\u629B\u9519\u56DE\u9000\u6574\u4E8B\u52A1\n *\n * \u4E8B\u52A1\u5185 service call:\u4E0D\u6301\u6709 db handle \u7ED9 handler(\u907F\u514D\u5E76\u53D1\u5199) \u2014\n * \u5B9E\u73B0:handler \u8FD4\u56DE result \u5199\u56DE step \u8F93\u51FA;\u82E5 handler \u8FD8\u8981\u5199 db,\u5E94\u8D70\u5176\u5B83\u63A5\u53E3\u5728\u4E8B\u52A1\u5916\u505A\u3002\n */\n\nimport { randomUUID } from \"node:crypto\";\nimport type { DatabaseSync } from \"node:sqlite\";\nimport { log } from \"./lib/log.js\";\nimport { normalizeOrderBy } from \"./lib/order-by.js\";\nimport type { ModuleManifestV2 } from \"./manifest-loader.js\";\nimport { Perm, PermissionDeniedError, assertModulePermission } from \"./permission-gate.js\";\nimport type { SchemaRegistry } from \"./schema-registry.js\";\nimport { DispatchError, type ServiceRegistry } from \"./service-registry.js\";\nimport type { WhereExpr } from \"./where.js\";\nimport { compile } from \"./where.js\";\n\nconst IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;\n\nfunction identOk(t: string): void {\n if (!IDENT.test(t)) throw new Error(`[tx] invalid table name \"${t}\"`);\n}\n\nexport interface TxStepQuery {\n op: \"query\";\n table: string;\n opts?: {\n where?: WhereExpr;\n /** SDK \u7528 field;\u5386\u53F2 col \u4ECD\u63A5\u53D7(normalizeOrderBy) */\n orderBy?:\n | { field?: string; col?: string; dir?: \"asc\" | \"desc\" }\n | Array<{ field?: string; col?: string; dir?: \"asc\" | \"desc\" }>;\n limit?: number;\n offset?: number;\n };\n}\n\nexport interface TxStepGet {\n op: \"get\";\n table: string;\n id: string;\n}\n\nexport interface TxStepInsert {\n op: \"insert\";\n table: string;\n row: Record<string, unknown>;\n}\n\nexport interface TxStepUpdate {\n op: \"update\";\n table: string;\n id: string;\n patch: Record<string, unknown>;\n}\n\nexport interface TxStepDelete {\n op: \"delete\";\n table: string;\n id: string;\n hard: boolean;\n}\n\nexport interface TxStepAudit {\n op: \"audit\";\n table: string;\n rowId: string;\n action: string;\n data?: Record<string, unknown>;\n}\n\nexport interface TxStepService {\n op: \"service\";\n name: string;\n input: Record<string, unknown>;\n}\n\nexport type TxStep = TxStepQuery | TxStepGet | TxStepInsert | TxStepUpdate | TxStepDelete | TxStepAudit | TxStepService;\n\nexport type TxStepResult =\n | { op: \"query\"; rows: Record<string, unknown>[] }\n | { op: \"get\"; row: Record<string, unknown> | null }\n | { op: \"insert\"; row: Record<string, unknown> }\n | { op: \"update\"; row: Record<string, unknown> }\n | { op: \"delete\"; changes: number }\n | { op: \"audit\"; changes: number }\n | { op: \"service\"; result: unknown };\n\nexport interface TxResponse {\n ok: true;\n results: TxStepResult[];\n}\n\nexport type TxError = {\n ok: false;\n step: number;\n error: string;\n code:\n | \"permission_denied\"\n | \"forbidden\"\n | \"no_such_service\"\n | \"not_in_requires\"\n | \"domain_error\"\n | \"transaction_busy\"\n | \"internal\"\n | \"no_such_table\";\n};\n\nexport interface TxRequest {\n moduleId: string;\n steps: TxStep[];\n}\n\ninterface TxSession {\n moduleId: string;\n results: TxStepResult[];\n idleTimer: ReturnType<typeof setTimeout>;\n}\n\ninterface TransactionWaiter {\n resolve: (acquired: boolean) => void;\n timer: ReturnType<typeof setTimeout>;\n}\n\nexport interface TxRunnerOptions {\n /** \u7B49\u5F85\u5168\u5C40 SQLite \u4E8B\u52A1\u69FD\u7684\u6700\u957F\u65F6\u95F4\u3002 */\n slotWaitTimeoutMs?: number;\n /** \u4EA4\u4E92\u5F0F\u4E8B\u52A1\u4E24\u6B21\u8BF7\u6C42\u4E4B\u95F4\u5141\u8BB8\u7A7A\u95F2\u7684\u6700\u957F\u65F6\u95F4\u3002 */\n sessionIdleTimeoutMs?: number;\n}\n\nexport interface TxRunnerDeps {\n db: DatabaseSync;\n query: import(\"./lib/sqlite.js\").QueryFn;\n schema: SchemaRegistry;\n serviceRegistry: ServiceRegistry;\n enabled: Map<string, ModuleManifestV2>;\n}\n\nexport class TxRunner {\n private readonly sessions = new Map<string, TxSession>();\n private transactionBusy = false;\n private readonly transactionWaiters: TransactionWaiter[] = [];\n private readonly slotWaitTimeoutMs: number;\n private readonly sessionIdleTimeoutMs: number;\n\n constructor(\n private readonly deps: TxRunnerDeps,\n options: TxRunnerOptions = {}\n ) {\n this.slotWaitTimeoutMs = options.slotWaitTimeoutMs ?? 2_000;\n this.sessionIdleTimeoutMs = options.sessionIdleTimeoutMs ?? 15_000;\n }\n\n async run(req: TxRequest): Promise<TxResponse | TxError> {\n const { moduleId, steps } = req;\n const manifest = this.deps.enabled.get(moduleId);\n if (!manifest) return { ok: false, step: -1, error: \"\u6A21\u5757\u672A enabled\", code: \"forbidden\" };\n // \u6CE8\u610F:\u4E0D\u5728\u6B64\u5904\u505A\u300C\u6574\u4F53 db:write:*\u300D\u65AD\u8A00\u3002\n // \u901A\u914D `db:write:*` \u65E0\u6CD5\u901A\u8FC7 permission-gate \u7684 validPermissionKey \u6821\u9A8C(\u6A21\u5757\u58F0\u660E\u5B83\u4F1A\u542F\u52A8\u5931\u8D25),\n // \u56E0\u6B64\u8BE5\u65AD\u8A00\u4F1A\u8BA9\u6240\u6709\u4E8B\u52A1(\u542B\u53EA\u8BFB\u4E8B\u52A1)\u6052\u88AB\u62D2\u3002\u771F\u6B63\u7684\u8BFB\u5199\u6743\u9650\u7531\u6BCF\u4E2A step \u7684\n // requireTableRead / requireTableWrite \u6309\u5177\u4F53\u8868\u7CBE\u786E gate\u3002\n\n const traceId = randomUUID().slice(0, 8);\n const results: TxStepResult[] = [];\n\n if (!(await this.acquireTransactionSlot())) {\n return { ok: false, step: -1, error: \"\u4E8B\u52A1\u7E41\u5FD9\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\", code: \"transaction_busy\" };\n }\n try {\n this.deps.db.exec(\"BEGIN IMMEDIATE\");\n } catch (e) {\n this.releaseTransactionSlot();\n return { ok: false, step: -1, error: `BEGIN \u5931\u8D25: ${(e as Error).message}`, code: \"internal\" };\n }\n\n for (let i = 0; i < steps.length; i++) {\n const step: TxStep = steps[i]!;\n try {\n const r = await this.runOne(moduleId, manifest, step);\n results.push(r);\n } catch (err) {\n this.rollback();\n this.releaseTransactionSlot();\n const code = this.mapErrorCode(err);\n log.warn(`[tx ${traceId}] step=${i} failed: ${(err as Error).message}`);\n return { ok: false, step: i, error: (err as Error).message, code };\n }\n }\n\n try {\n this.deps.db.exec(\"COMMIT\");\n } catch (e) {\n this.rollback();\n this.releaseTransactionSlot();\n return { ok: false, step: -1, error: `COMMIT \u5931\u8D25: ${(e as Error).message}`, code: \"internal\" };\n }\n\n this.releaseTransactionSlot();\n log.info(`[tx ${traceId}] module=${moduleId} ${steps.length} steps OK`);\n return { ok: true, results };\n }\n\n /**\n * \u5F00\u542F\u4EA4\u4E92\u5F0F\u4E8B\u52A1\u4F1A\u8BDD\uFF08\u534F\u8BAE\u6D41\u7A0B\uFF1Abegin \u2192 step* \u2192 commit | rollback\uFF09\u3002\n * \u4F9B\u5BA2\u6237\u7AEF `db.tx` \u5728\u56DE\u8C03\u5185\u90E8\u5F02\u6B65\u6267\u884C\u5E76\u5B9E\u65F6\u8BFB\u56DE query / get / call \u7684\u670D\u52A1\u7AEF\u6267\u884C\u7ED3\u679C\u3002\n * \u5355\u8FDE\u63A5\u4E92\u65A5\u4FDD\u62A4\uFF1A\u4E0E\u6279\u91CF run() \u5171\u7528\u4E8B\u52A1\u961F\u5217\uFF0C\u5E76\u5728\u63D0\u4EA4/\u56DE\u6EDA\u65F6\u5524\u9192\u4E0B\u4E00\u4E2A\u7B49\u5F85\u8005\u3002\n *\n * @param moduleId \u8BF7\u6C42\u6A21\u5757\u552F\u4E00\u6807\u8BC6\u7B26\u3002\n * @returns \u6210\u529F\u8FD4\u56DE\u5206\u914D\u7684\u4F1A\u8BDD txId\uFF0C\u5931\u8D25\u8FD4\u56DE\u9519\u8BEF\u54CD\u5E94\u3002\n */\n async beginSession(moduleId: string): Promise<{ ok: true; txId: string } | TxError> {\n const manifest = this.deps.enabled.get(moduleId);\n if (!manifest) return { ok: false, step: -1, error: \"\u6A21\u5757\u672A enabled\", code: \"forbidden\" };\n\n // run() \u4E0E\u4EA4\u4E92\u5F0F\u4F1A\u8BDD\u5171\u7528\u540C\u4E00 SQLite \u8FDE\u63A5\uFF0C\u5FC5\u987B\u5171\u7528\u540C\u4E00\u4E2A\u4E8B\u52A1\u69FD\u3002\n if (!(await this.acquireTransactionSlot())) {\n return { ok: false, step: -1, error: \"\u4E8B\u52A1\u7E41\u5FD9\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\", code: \"transaction_busy\" };\n }\n\n try {\n this.deps.db.exec(\"BEGIN IMMEDIATE\");\n } catch (e) {\n this.releaseTransactionSlot();\n return { ok: false, step: -1, error: `BEGIN \u5931\u8D25: ${(e as Error).message}`, code: \"internal\" };\n }\n const txId = randomUUID();\n this.sessions.set(txId, {\n moduleId,\n results: [],\n idleTimer: this.createSessionIdleTimer(txId, moduleId),\n });\n log.info(`[tx-session ${txId.slice(0, 8)}] begin module=${moduleId}`);\n return { ok: true, txId };\n }\n\n /**\n * \u5728\u5F53\u524D\u4EA4\u4E92\u5F0F\u4E8B\u52A1\u4F1A\u8BDD\u4E2D\u987A\u5E8F\u6267\u884C\u5355\u4E2A\u6B65\u9AA4\u3002\n *\n * @param txId \u4F1A\u8BDD\u552F\u4E00\u6807\u8BC6\u7B26\u3002\n * @param moduleId \u8C03\u7528\u6A21\u5757\u6807\u8BC6\u3002\n * @param step \u5F85\u6267\u884C\u7684\u64CD\u4F5C\u6B65\u9AA4\u5BF9\u8C61\u3002\n * @returns \u6B65\u9AA4\u6267\u884C\u7ED3\u679C\u6216\u9519\u8BEF\u54CD\u5E94\u3002\n */\n async stepSession(\n txId: string,\n moduleId: string,\n step: TxStep\n ): Promise<{ ok: true; result: TxStepResult } | TxError> {\n const session = this.sessions.get(txId);\n if (!session) {\n return { ok: false, step: -1, error: \"\u4E8B\u52A1\u4F1A\u8BDD\u4E0D\u5B58\u5728\u6216\u5DF2\u7ED3\u675F\", code: \"internal\" };\n }\n if (session.moduleId !== moduleId) {\n return { ok: false, step: session.results.length, error: \"moduleId \u4E0E\u4F1A\u8BDD\u4E0D\u7B26\", code: \"forbidden\" };\n }\n clearTimeout(session.idleTimer);\n const manifest = this.deps.enabled.get(moduleId);\n if (!manifest) {\n this.abortSession(txId);\n return { ok: false, step: session.results.length, error: \"\u6A21\u5757\u672A enabled\", code: \"forbidden\" };\n }\n const stepIndex = session.results.length;\n try {\n const r = await this.runOne(moduleId, manifest, step);\n session.results.push(r);\n session.idleTimer = this.createSessionIdleTimer(txId, moduleId);\n return { ok: true, result: r };\n } catch (err) {\n this.abortSession(txId);\n const code = this.mapErrorCode(err);\n log.warn(`[tx-session ${txId.slice(0, 8)}] step=${stepIndex} failed: ${(err as Error).message}`);\n return { ok: false, step: stepIndex, error: (err as Error).message, code };\n }\n }\n\n /**\n * \u63D0\u4EA4\u5F53\u524D\u4EA4\u4E92\u5F0F\u4E8B\u52A1\u4F1A\u8BDD\uFF0C\u5C06\u6240\u6709\u6B65\u9AA4\u7684\u4FEE\u6539\u6301\u4E45\u5316\u5E76\u91CA\u653E\u8FDE\u63A5\u3002\n *\n * @param txId \u4F1A\u8BDD\u552F\u4E00\u6807\u8BC6\u7B26\u3002\n * @param moduleId \u8C03\u7528\u6A21\u5757\u6807\u8BC6\u3002\n * @returns \u63D0\u4EA4\u7ED3\u679C\u54CD\u5E94\u3002\n */\n commitSession(txId: string, moduleId: string): TxResponse | TxError {\n const session = this.sessions.get(txId);\n if (!session) {\n return { ok: false, step: -1, error: \"\u4E8B\u52A1\u4F1A\u8BDD\u4E0D\u5B58\u5728\u6216\u5DF2\u7ED3\u675F\", code: \"internal\" };\n }\n if (session.moduleId !== moduleId) {\n return { ok: false, step: -1, error: \"moduleId \u4E0E\u4F1A\u8BDD\u4E0D\u7B26\", code: \"forbidden\" };\n }\n clearTimeout(session.idleTimer);\n try {\n this.deps.db.exec(\"COMMIT\");\n } catch (e) {\n this.abortSession(txId);\n return { ok: false, step: -1, error: `COMMIT \u5931\u8D25: ${(e as Error).message}`, code: \"internal\" };\n }\n const results = session.results;\n this.sessions.delete(txId);\n this.releaseTransactionSlot();\n log.info(`[tx-session ${txId.slice(0, 8)}] commit ${results.length} steps OK`);\n return { ok: true, results };\n }\n\n /**\n * \u56DE\u6EDA\u5F53\u524D\u4EA4\u4E92\u5F0F\u4E8B\u52A1\u4F1A\u8BDD\uFF0C\u64A4\u9500\u6240\u6709\u672A\u63D0\u4EA4\u7684\u6570\u636E\u5E93\u53D8\u66F4\u3002\n * \u5177\u6709\u5E42\u7B49\u6027\uFF1A\u4F1A\u8BDD\u5DF2\u7ED3\u675F\u6216\u5DF2\u56DE\u6EDA\u65F6\u540C\u6837\u8FD4\u56DE\u6210\u529F\u3002\n *\n * @param txId \u4F1A\u8BDD\u552F\u4E00\u6807\u8BC6\u7B26\u3002\n * @param moduleId \u8C03\u7528\u6A21\u5757\u6807\u8BC6\u3002\n * @returns \u56DE\u6EDA\u786E\u8BA4\u7ED3\u679C\u3002\n */\n rollbackSession(txId: string, moduleId: string): { ok: true } | TxError {\n const session = this.sessions.get(txId);\n if (!session) {\n // \u5E42\u7B49\uFF1A\u5DF2\u7ED3\u675F\u89C6\u4E3A\u6210\u529F\u56DE\u6EDA\n return { ok: true };\n }\n if (session.moduleId !== moduleId) {\n return { ok: false, step: -1, error: \"moduleId \u4E0E\u4F1A\u8BDD\u4E0D\u7B26\", code: \"forbidden\" };\n }\n this.abortSession(txId);\n return { ok: true };\n }\n\n private abortSession(txId: string): void {\n const session = this.sessions.get(txId);\n if (!session) return;\n clearTimeout(session.idleTimer);\n this.sessions.delete(txId);\n this.rollback();\n this.releaseTransactionSlot();\n log.info(`[tx-session ${txId.slice(0, 8)}] aborted`);\n }\n\n private async acquireTransactionSlot(): Promise<boolean> {\n if (!this.transactionBusy) {\n this.transactionBusy = true;\n return true;\n }\n return new Promise<boolean>((resolve) => {\n const waiter: TransactionWaiter = {\n resolve,\n timer: setTimeout(() => {\n const index = this.transactionWaiters.indexOf(waiter);\n if (index >= 0) this.transactionWaiters.splice(index, 1);\n resolve(false);\n }, this.slotWaitTimeoutMs),\n };\n this.transactionWaiters.push(waiter);\n });\n }\n\n private releaseTransactionSlot(): void {\n const next = this.transactionWaiters.shift();\n if (next) {\n clearTimeout(next.timer);\n next.resolve(true);\n return;\n }\n this.transactionBusy = false;\n }\n\n private createSessionIdleTimer(txId: string, moduleId: string): ReturnType<typeof setTimeout> {\n const timer = setTimeout(() => {\n if (!this.sessions.has(txId)) return;\n log.warn(`[tx-session ${txId.slice(0, 8)}] idle timeout module=${moduleId} after ${this.sessionIdleTimeoutMs}ms`);\n this.abortSession(txId);\n }, this.sessionIdleTimeoutMs);\n timer.unref?.();\n return timer;\n }\n\n private mapErrorCode(err: unknown): TxError[\"code\"] {\n return err instanceof PermissionDeniedError\n ? \"permission_denied\"\n : err instanceof DispatchError &&\n (err.code === \"no_such_service\" ||\n err.code === \"not_in_requires\" ||\n err.code === \"forbidden\" ||\n err.code === \"domain_error\")\n ? err.code\n : (err as { code?: string }).code === \"no_such_service\"\n ? \"no_such_service\"\n : (err as { code?: string }).code === \"not_in_requires\"\n ? \"not_in_requires\"\n : (err as { code?: string }).code === \"no_such_table\"\n ? \"no_such_table\"\n : \"internal\";\n }\n\n private rollback(): void {\n try {\n this.deps.db.exec(\"ROLLBACK\");\n } catch {\n // best-effort; fall through\n }\n }\n\n /** \u5355 step \u7531\u5BF9\u5E94\u6743\u9650\u68C0\u67E5 + SQL \u8DEF\u5F84\u6267\u884C */\n private async runOne(moduleId: string, manifest: ModuleManifestV2, step: TxStep): Promise<TxStepResult> {\n switch (step.op) {\n case \"query\":\n return this.doQuery(moduleId, manifest, step);\n case \"get\":\n return this.doGet(moduleId, manifest, step);\n case \"insert\":\n return this.doInsert(moduleId, manifest, step);\n case \"update\":\n return this.doUpdate(moduleId, manifest, step);\n case \"delete\":\n return this.doDelete(moduleId, manifest, step);\n case \"audit\":\n return this.doAudit(moduleId, manifest, step);\n case \"service\":\n return this.doService(moduleId, manifest, step);\n }\n }\n\n private requireTableRead(mod: ModuleManifestV2, table: string): void {\n identOk(table);\n this.assertTableRegistered(table);\n assertModulePermission(mod.id, mod.permissions, Perm.dbRead(table));\n }\n private requireTableWrite(mod: ModuleManifestV2, table: string): void {\n identOk(table);\n this.assertTableRegistered(table);\n assertModulePermission(mod.id, mod.permissions, Perm.dbWrite(table));\n }\n private assertTableRegistered(table: string): void {\n if (table.startsWith(\"sfmc_\")) return; // \u5E73\u53F0\u8868 \u2014 \u59CB\u7EC8\u53EF\u8BFB\u5199(\u5E73\u53F0\u5C42)\n if (!this.deps.schema.isRegistered(table)) {\n const err = new Error(`\u8868 \"${table}\" \u672A\u6CE8\u518C\u5230 schema-registry`);\n (err as { code?: string }).code = \"no_such_table\";\n throw err;\n }\n }\n\n private doQuery(_mid: string, mod: ModuleManifestV2, step: TxStepQuery): TxStepResult {\n this.requireTableRead(mod, step.table);\n const where = compile(step.opts?.where);\n // LSP:\u4E0E SDK QueryOptions.orderBy({field}|[]) \u5BF9\u9F50,\u517C\u6536\u9057\u7559 {col}\n const orders = normalizeOrderBy(step.opts?.orderBy);\n const limit = typeof step.opts?.limit === \"number\" ? step.opts?.limit : undefined;\n const offset = typeof step.opts?.offset === \"number\" ? step.opts?.offset : undefined;\n\n const whereClause = where.sql;\n const params = [...where.values];\n let extra = \"\";\n if (orders.length > 0) {\n const parts: string[] = [];\n for (const o of orders) {\n if (!IDENT.test(o.col)) throw new Error(`[tx] orderBy bad column \"${o.col}\"`);\n parts.push(`\"${o.col}\" ${o.dir === \"desc\" ? \"DESC\" : \"ASC\"}`);\n }\n extra += ` ORDER BY ${parts.join(\", \")}`;\n }\n if (typeof limit === \"number\") {\n extra += \" LIMIT \" + Math.max(0, Math.floor(limit));\n }\n if (typeof offset === \"number\") {\n extra += \" OFFSET \" + Math.max(0, Math.floor(offset));\n }\n\n const sql = `SELECT * FROM \"${step.table}\" WHERE ${whereClause}${extra}`;\n const rows = this.deps.db.prepare(sql).all(...(params as never[])) as Record<string, unknown>[];\n return { op: \"query\", rows };\n }\n\n /** \u89E3\u6790\u4E3B\u952E WHERE\u3002\u5355\u5217\u76F4\u63A5\u7B49\u503C;\u8054\u5408\u4E3B\u952E\u652F\u6301 `a|b|c`(\u4E0E data-backup \u7EA6\u5B9A\u4E00\u81F4)\u3002 */\n private pkWhere(table: string, id: string | number): { clause: string; values: unknown[] } {\n const cols = this.deps.db.prepare(`PRAGMA table_info(\"${table}\")`).all() as Array<{\n name: string;\n pk: number;\n }>;\n const pkCols = cols\n .filter((c) => c.pk > 0)\n .sort((a, b) => a.pk - b.pk)\n .map((c) => c.name);\n if (pkCols.length === 0) {\n throw new Error(`[tx] ${table} \u6CA1\u6709 primary key`);\n }\n if (pkCols.length === 1) {\n return { clause: `\"${pkCols[0]}\" = ?`, values: [id] };\n }\n const parts = String(id).split(\"|\");\n if (parts.length === pkCols.length) {\n return {\n clause: pkCols.map((c) => `\"${c}\" = ?`).join(\" AND \"),\n values: parts,\n };\n }\n // \u8054\u5408\u4E3B\u952E\u4F46\u53EA\u4F20\u4E86\u9996\u5217(\u9891\u9053 id \u7B49):\u6309\u7B2C\u4E00\u4E3B\u952E\u5217\u5339\u914D\n return { clause: `\"${pkCols[0]}\" = ?`, values: [id] };\n }\n\n private doGet(_mid: string, mod: ModuleManifestV2, step: TxStepGet): TxStepResult {\n this.requireTableRead(mod, step.table);\n const where = this.pkWhere(step.table, step.id);\n const row = this.deps.db\n .prepare(`SELECT * FROM \"${step.table}\" WHERE ${where.clause}`)\n .get(...(where.values as never[])) as Record<string, unknown> | undefined;\n return { op: \"get\", row: row ?? null };\n }\n\n private doInsert(_mid: string, mod: ModuleManifestV2, step: TxStepInsert): TxStepResult {\n this.requireTableWrite(mod, step.table);\n const cols = Object.keys(step.row);\n if (cols.length === 0) throw new Error(\"[tx] insert row \u5FC5\u987B\u81F3\u5C11\u4E00\u4E2A\u5B57\u6BB5\");\n for (const c of cols) {\n if (!IDENT.test(c)) throw new Error(`[tx] invalid column name \"${c}\"`);\n }\n const placeholders = cols.map(() => \"?\").join(\",\");\n const quoted = cols.map((c) => `\"${c}\"`).join(\",\");\n const values = cols.map((c) => step.row[c]);\n const sql = `INSERT INTO \"${step.table}\" (${quoted}) VALUES (${placeholders})`;\n try {\n this.deps.db.prepare(sql).run(...(values as never[]));\n } catch (e) {\n throw new Error(`INSERT ${step.table} \u5931\u8D25: ${(e as Error).message}`);\n }\n return { op: \"insert\", row: { ...step.row } };\n }\n\n private doUpdate(_mid: string, mod: ModuleManifestV2, step: TxStepUpdate): TxStepResult {\n this.requireTableWrite(mod, step.table);\n const cols = Object.keys(step.patch);\n if (cols.length === 0) throw new Error(\"[tx] update patch \u4E0D\u80FD\u4E3A\u7A7A\");\n for (const c of cols) {\n if (!IDENT.test(c)) throw new Error(`[tx] invalid column name \"${c}\"`);\n }\n const where = this.pkWhere(step.table, step.id);\n const sets = cols.map((c) => `\"${c}\" = ?`).join(\",\");\n const params = [...cols.map((c) => step.patch[c]), ...where.values];\n const sql = `UPDATE \"${step.table}\" SET ${sets} WHERE ${where.clause}`;\n const result = this.deps.db.prepare(sql).run(...(params as never[]));\n if (result.changes === 0) {\n throw new Error(`UPDATE ${step.table} id=${step.id} \u5F71\u54CD 0 \u884C(\u627E\u4E0D\u5230 / \u5DF2\u5220?)`);\n }\n return { op: \"update\", row: { ...step.patch, id: step.id } as unknown as Record<string, unknown> };\n }\n\n private doDelete(_mid: string, mod: ModuleManifestV2, step: TxStepDelete): TxStepResult {\n this.requireTableWrite(mod, step.table);\n const where = this.pkWhere(step.table, step.id);\n if (step.hard) {\n const sql = `DELETE FROM \"${step.table}\" WHERE ${where.clause}`;\n const r = this.deps.db.prepare(sql).run(...(where.values as never[]));\n return { op: \"delete\", changes: Number(r.changes) };\n }\n const cols = this.deps.db.prepare(`PRAGMA table_info(\"${step.table}\")`).all() as Array<{\n name: string;\n }>;\n const hasDeletedAt = cols.find((c) => c.name === \"_deleted_at\");\n if (!hasDeletedAt) {\n throw new Error(`[tx] ${step.table} \u6CA1\u542F\u7528 softDelete,\u5220\u4E0D\u6389;\u8BF7 hard=true \u6216 schema \u8BBE softDelete=true`);\n }\n const sql = `UPDATE \"${step.table}\" SET \"_deleted_at\" = ?, \"_version\" = COALESCE(\"_version\",0)+1 WHERE ${where.clause}`;\n const r = this.deps.db.prepare(sql).run(Date.now() as never, ...(where.values as never[]));\n return { op: \"delete\", changes: Number(r.changes) };\n }\n\n private doAudit(_mid: string, mod: ModuleManifestV2, step: TxStepAudit): TxStepResult {\n this.requireTableWrite(mod, step.table);\n const dataJson = step.data ? JSON.stringify(step.data) : null;\n const r = this.deps.db\n .prepare(\n `INSERT INTO sfmc__audit (module_id, table_name, row_id, action, data, created_at)\n VALUES (?, ?, ?, ?, ?, ?)`\n )\n .run(mod.id, step.table, step.rowId, step.action, dataJson, new Date().toISOString());\n return { op: \"audit\", changes: Number(r.changes) };\n }\n\n private async doService(_mid: string, mod: ModuleManifestV2, step: TxStepService): Promise<TxStepResult> {\n // requires / \u81EA\u8C03\u7528\u8C41\u514D\u7684\u552F\u4E00\u6743\u5A01\u5728 ServiceRegistry.dispatch(DRY/LSP);\n // \u6B64\u5904\u53EA\u8865 dispatch \u4E0D\u505A\u7684 service:<name> \u6743\u9650\u95E8(\u4E0E HTTP service-routes \u5BF9\u9F50)\u3002\n assertModulePermission(mod.id, mod.permissions, Perm.service(step.name));\n const result = await this.deps.serviceRegistry.dispatch(this.deps.enabled, mod.id, step.name, step.input, {\n query: this.deps.query,\n db: this.deps.db,\n });\n return { op: \"service\", result: result.result };\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * tx-runner.ts \u2014 \u4E8B\u52A1 RPC \u5904\u7406\u5668\n *\n * \u534F\u8BAE(\u4E24\u79CD):\n * A. \u6279\u91CF: POST /api/sfmc/db/tx body={ steps } \u2192 { ok, results? } | { ok:false, step, error, code }\n * B. \u4EA4\u4E92: begin \u2192 step* \u2192 commit|rollback(SDK db.tx \u7528;\u56DE\u8C03\u5185\u53EF\u8BFB\u56DE query/get/call)\n *\n * \u6D41\u7A0B(\u6279\u91CF):\n * 1. \u6821\u9A8C moduleId in enabled\n * 2. BEGIN IMMEDIATE\n * 3. \u987A\u5E8F\u8DD1 steps \u2014 \u4EFB\u4E00\u629B\u9519 \u2192 ROLLBACK + reply { ok:false, step:<i>, ... }\n * 4. \u5168\u8FC7 \u2192 COMMIT + reply { ok:true, results }\n *\n * step.op:\n * - query { table, opts? }\n * - get { table, id }\n * - insert { table, row }\n * - update { table, id, patch }\n * - delete { table, id, hard }\n * - audit { table, rowId, action, data? } \u2192 \u5E73\u53F0 sfmc__audit \u8868\n * - service { name, input } \u2192 \u6D3E\u53D1;\u5931\u8D25\u629B\u9519\u56DE\u9000\u6574\u4E8B\u52A1\n *\n * \u4E8B\u52A1\u5185 service call:\u4E0D\u6301\u6709 db handle \u7ED9 handler(\u907F\u514D\u5E76\u53D1\u5199) \u2014\n * \u5B9E\u73B0:handler \u8FD4\u56DE result \u5199\u56DE step \u8F93\u51FA;\u82E5 handler \u8FD8\u8981\u5199 db,\u5E94\u8D70\u5176\u5B83\u63A5\u53E3\u5728\u4E8B\u52A1\u5916\u505A\u3002\n */\n\nimport { randomUUID } from \"node:crypto\";\nimport type { DatabaseSync } from \"node:sqlite\";\nimport { log } from \"./lib/log.js\";\nimport { normalizeOrderBy } from \"./lib/order-by.js\";\nimport type { ModuleManifestV2 } from \"./manifest-loader.js\";\nimport { Perm, PermissionDeniedError, assertModulePermission } from \"./permission-gate.js\";\nimport type { SchemaRegistry } from \"./schema-registry.js\";\nimport { DispatchError, type ServiceRegistry } from \"./service-registry.js\";\nimport type { WhereExpr } from \"./where.js\";\nimport { compile } from \"./where.js\";\n\nconst IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;\n\nfunction identOk(t: string): void {\n if (!IDENT.test(t)) throw new Error(`[tx] invalid table name \"${t}\"`);\n}\n\nexport interface TxStepQuery {\n op: \"query\";\n table: string;\n opts?: {\n where?: WhereExpr;\n /** SDK \u7528 field;\u5386\u53F2 col \u4ECD\u63A5\u53D7(normalizeOrderBy) */\n orderBy?:\n | { field?: string; col?: string; dir?: \"asc\" | \"desc\" }\n | Array<{ field?: string; col?: string; dir?: \"asc\" | \"desc\" }>;\n limit?: number;\n offset?: number;\n };\n}\n\nexport interface TxStepGet {\n op: \"get\";\n table: string;\n id: string;\n}\n\nexport interface TxStepInsert {\n op: \"insert\";\n table: string;\n row: Record<string, unknown>;\n}\n\nexport interface TxStepUpdate {\n op: \"update\";\n table: string;\n id: string;\n patch: Record<string, unknown>;\n}\n\nexport interface TxStepDelete {\n op: \"delete\";\n table: string;\n id: string;\n hard: boolean;\n}\n\nexport interface TxStepAudit {\n op: \"audit\";\n table: string;\n rowId: string;\n action: string;\n data?: Record<string, unknown>;\n}\n\nexport interface TxStepService {\n op: \"service\";\n name: string;\n input: Record<string, unknown>;\n}\n\nexport type TxStep = TxStepQuery | TxStepGet | TxStepInsert | TxStepUpdate | TxStepDelete | TxStepAudit | TxStepService;\n\nexport type TxStepResult =\n | { op: \"query\"; rows: Record<string, unknown>[] }\n | { op: \"get\"; row: Record<string, unknown> | null }\n | { op: \"insert\"; row: Record<string, unknown> }\n | { op: \"update\"; row: Record<string, unknown> }\n | { op: \"delete\"; changes: number }\n | { op: \"audit\"; changes: number }\n | { op: \"service\"; result: unknown };\n\nexport interface TxResponse {\n ok: true;\n results: TxStepResult[];\n}\n\nexport type TxError = {\n ok: false;\n step: number;\n error: string;\n code:\n | \"permission_denied\"\n | \"forbidden\"\n | \"no_such_service\"\n | \"not_in_requires\"\n | \"domain_error\"\n | \"transaction_busy\"\n | \"internal\"\n | \"no_such_table\";\n};\n\nexport interface TxRequest {\n moduleId: string;\n steps: TxStep[];\n}\n\n/** \u4E8B\u52A1\u63D0\u4EA4\u6210\u529F\u540E\u7684\u63D2\u5165\u884C\u3002\u804A\u5929\u6D88\u606F\u636E\u6B64\u51B3\u5B9A\u662F\u5426\u8F6C\u53D1\u5230 QQ\u3002 */\nexport interface CommittedInsert {\n table: string;\n row: Record<string, unknown>;\n}\n\ninterface TxSession {\n moduleId: string;\n /** \u5DF2\u6210\u529F\u6267\u884C\u7684\u6B65\u9AA4\uFF0C\u63D0\u4EA4\u540E\u7528\u4E8E afterCommit\uFF0C\u4E0D\u8FDB\u5165\u54CD\u5E94\u4F53\u3002 */\n steps: TxStep[];\n results: TxStepResult[];\n idleTimer: ReturnType<typeof setTimeout>;\n}\n\ninterface TransactionWaiter {\n resolve: (acquired: boolean) => void;\n timer: ReturnType<typeof setTimeout>;\n}\n\nexport interface TxRunnerOptions {\n /** \u7B49\u5F85\u5168\u5C40 SQLite \u4E8B\u52A1\u69FD\u7684\u6700\u957F\u65F6\u95F4\u3002 */\n slotWaitTimeoutMs?: number;\n /** \u4EA4\u4E92\u5F0F\u4E8B\u52A1\u4E24\u6B21\u8BF7\u6C42\u4E4B\u95F4\u5141\u8BB8\u7A7A\u95F2\u7684\u6700\u957F\u65F6\u95F4\u3002 */\n sessionIdleTimeoutMs?: number;\n}\n\nexport interface TxRunnerDeps {\n db: DatabaseSync;\n query: import(\"./lib/sqlite.js\").QueryFn;\n schema: SchemaRegistry;\n serviceRegistry: ServiceRegistry;\n enabled: Map<string, ModuleManifestV2>;\n}\n\nexport class TxRunner {\n private readonly sessions = new Map<string, TxSession>();\n private transactionBusy = false;\n private readonly transactionWaiters: TransactionWaiter[] = [];\n private readonly slotWaitTimeoutMs: number;\n private readonly sessionIdleTimeoutMs: number;\n /**\n * \u63D0\u4EA4\u6210\u529F\u540E\u7684\u63D2\u5165\u901A\u77E5\u3002\u6E38\u620F\u804A\u5929\u7ECF db.tx \u843D\u5E93\uFF0C\u4E0D\u8D70 /api/sfmc/messages\uFF0C\n * \u56E0\u6B64 MC\u2192QQ \u8981\u5728\u8FD9\u91CC\u6302\u94A9\u3002\u94A9\u5B50\u4E0D\u5F97\u629B\u9519\u5F71\u54CD\u5DF2\u63D0\u4EA4\u7ED3\u679C\u3002\n */\n private afterCommit?: (inserts: CommittedInsert[]) => void;\n\n constructor(\n private readonly deps: TxRunnerDeps,\n options: TxRunnerOptions = {}\n ) {\n this.slotWaitTimeoutMs = options.slotWaitTimeoutMs ?? 2_000;\n this.sessionIdleTimeoutMs = options.sessionIdleTimeoutMs ?? 15_000;\n }\n\n /**\n * \u6CE8\u518C\u63D0\u4EA4\u6210\u529F\u540E\u7684\u63D2\u5165\u56DE\u8C03\u3002\n * \u4F7F\u7528\u573A\u666F\uFF1Adb-server \u542F\u52A8\u540E\u628A\u804A\u5929\u884C\u8F6C\u53D1\u7ED9 QQ\uFF0C\u907F\u514D\u4E8B\u52A1\u5C42\u4F9D\u8D56\u5177\u4F53\u6865\u63A5\u5B9E\u73B0\u3002\n */\n setAfterCommit(hook: (inserts: CommittedInsert[]) => void): void {\n this.afterCommit = hook;\n }\n\n async run(req: TxRequest): Promise<TxResponse | TxError> {\n const { moduleId, steps } = req;\n const manifest = this.deps.enabled.get(moduleId);\n if (!manifest) return { ok: false, step: -1, error: \"\u6A21\u5757\u672A enabled\", code: \"forbidden\" };\n // \u6CE8\u610F:\u4E0D\u5728\u6B64\u5904\u505A\u300C\u6574\u4F53 db:write:*\u300D\u65AD\u8A00\u3002\n // \u901A\u914D `db:write:*` \u65E0\u6CD5\u901A\u8FC7 permission-gate \u7684 validPermissionKey \u6821\u9A8C(\u6A21\u5757\u58F0\u660E\u5B83\u4F1A\u542F\u52A8\u5931\u8D25),\n // \u56E0\u6B64\u8BE5\u65AD\u8A00\u4F1A\u8BA9\u6240\u6709\u4E8B\u52A1(\u542B\u53EA\u8BFB\u4E8B\u52A1)\u6052\u88AB\u62D2\u3002\u771F\u6B63\u7684\u8BFB\u5199\u6743\u9650\u7531\u6BCF\u4E2A step \u7684\n // requireTableRead / requireTableWrite \u6309\u5177\u4F53\u8868\u7CBE\u786E gate\u3002\n\n const traceId = randomUUID().slice(0, 8);\n const results: TxStepResult[] = [];\n\n if (!(await this.acquireTransactionSlot())) {\n return { ok: false, step: -1, error: \"\u4E8B\u52A1\u7E41\u5FD9\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\", code: \"transaction_busy\" };\n }\n try {\n this.deps.db.exec(\"BEGIN IMMEDIATE\");\n } catch (e) {\n this.releaseTransactionSlot();\n return { ok: false, step: -1, error: `BEGIN \u5931\u8D25: ${(e as Error).message}`, code: \"internal\" };\n }\n\n for (let i = 0; i < steps.length; i++) {\n const step: TxStep = steps[i]!;\n try {\n const r = await this.runOne(moduleId, manifest, step);\n results.push(r);\n } catch (err) {\n this.rollback();\n this.releaseTransactionSlot();\n const code = this.mapErrorCode(err);\n log.warn(`[tx ${traceId}] step=${i} failed: ${(err as Error).message}`);\n return { ok: false, step: i, error: (err as Error).message, code };\n }\n }\n\n try {\n this.deps.db.exec(\"COMMIT\");\n } catch (e) {\n this.rollback();\n this.releaseTransactionSlot();\n return { ok: false, step: -1, error: `COMMIT \u5931\u8D25: ${(e as Error).message}`, code: \"internal\" };\n }\n\n this.releaseTransactionSlot();\n this.emitAfterCommit(steps);\n log.info(`[tx ${traceId}] module=${moduleId} ${steps.length} steps OK`);\n return { ok: true, results };\n }\n\n /**\n * \u5F00\u542F\u4EA4\u4E92\u5F0F\u4E8B\u52A1\u4F1A\u8BDD\uFF08\u534F\u8BAE\u6D41\u7A0B\uFF1Abegin \u2192 step* \u2192 commit | rollback\uFF09\u3002\n * \u4F9B\u5BA2\u6237\u7AEF `db.tx` \u5728\u56DE\u8C03\u5185\u90E8\u5F02\u6B65\u6267\u884C\u5E76\u5B9E\u65F6\u8BFB\u56DE query / get / call \u7684\u670D\u52A1\u7AEF\u6267\u884C\u7ED3\u679C\u3002\n * \u5355\u8FDE\u63A5\u4E92\u65A5\u4FDD\u62A4\uFF1A\u4E0E\u6279\u91CF run() \u5171\u7528\u4E8B\u52A1\u961F\u5217\uFF0C\u5E76\u5728\u63D0\u4EA4/\u56DE\u6EDA\u65F6\u5524\u9192\u4E0B\u4E00\u4E2A\u7B49\u5F85\u8005\u3002\n *\n * @param moduleId \u8BF7\u6C42\u6A21\u5757\u552F\u4E00\u6807\u8BC6\u7B26\u3002\n * @returns \u6210\u529F\u8FD4\u56DE\u5206\u914D\u7684\u4F1A\u8BDD txId\uFF0C\u5931\u8D25\u8FD4\u56DE\u9519\u8BEF\u54CD\u5E94\u3002\n */\n async beginSession(moduleId: string): Promise<{ ok: true; txId: string } | TxError> {\n const manifest = this.deps.enabled.get(moduleId);\n if (!manifest) return { ok: false, step: -1, error: \"\u6A21\u5757\u672A enabled\", code: \"forbidden\" };\n\n // run() \u4E0E\u4EA4\u4E92\u5F0F\u4F1A\u8BDD\u5171\u7528\u540C\u4E00 SQLite \u8FDE\u63A5\uFF0C\u5FC5\u987B\u5171\u7528\u540C\u4E00\u4E2A\u4E8B\u52A1\u69FD\u3002\n if (!(await this.acquireTransactionSlot())) {\n return { ok: false, step: -1, error: \"\u4E8B\u52A1\u7E41\u5FD9\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\", code: \"transaction_busy\" };\n }\n\n try {\n this.deps.db.exec(\"BEGIN IMMEDIATE\");\n } catch (e) {\n this.releaseTransactionSlot();\n return { ok: false, step: -1, error: `BEGIN \u5931\u8D25: ${(e as Error).message}`, code: \"internal\" };\n }\n const txId = randomUUID();\n this.sessions.set(txId, {\n moduleId,\n steps: [],\n results: [],\n idleTimer: this.createSessionIdleTimer(txId, moduleId),\n });\n log.info(`[tx-session ${txId.slice(0, 8)}] begin module=${moduleId}`);\n return { ok: true, txId };\n }\n\n /**\n * \u5728\u5F53\u524D\u4EA4\u4E92\u5F0F\u4E8B\u52A1\u4F1A\u8BDD\u4E2D\u987A\u5E8F\u6267\u884C\u5355\u4E2A\u6B65\u9AA4\u3002\n *\n * @param txId \u4F1A\u8BDD\u552F\u4E00\u6807\u8BC6\u7B26\u3002\n * @param moduleId \u8C03\u7528\u6A21\u5757\u6807\u8BC6\u3002\n * @param step \u5F85\u6267\u884C\u7684\u64CD\u4F5C\u6B65\u9AA4\u5BF9\u8C61\u3002\n * @returns \u6B65\u9AA4\u6267\u884C\u7ED3\u679C\u6216\u9519\u8BEF\u54CD\u5E94\u3002\n */\n async stepSession(\n txId: string,\n moduleId: string,\n step: TxStep\n ): Promise<{ ok: true; result: TxStepResult } | TxError> {\n const session = this.sessions.get(txId);\n if (!session) {\n return { ok: false, step: -1, error: \"\u4E8B\u52A1\u4F1A\u8BDD\u4E0D\u5B58\u5728\u6216\u5DF2\u7ED3\u675F\", code: \"internal\" };\n }\n if (session.moduleId !== moduleId) {\n return { ok: false, step: session.results.length, error: \"moduleId \u4E0E\u4F1A\u8BDD\u4E0D\u7B26\", code: \"forbidden\" };\n }\n clearTimeout(session.idleTimer);\n const manifest = this.deps.enabled.get(moduleId);\n if (!manifest) {\n this.abortSession(txId);\n return { ok: false, step: session.results.length, error: \"\u6A21\u5757\u672A enabled\", code: \"forbidden\" };\n }\n const stepIndex = session.results.length;\n try {\n const r = await this.runOne(moduleId, manifest, step);\n session.steps.push(step);\n session.results.push(r);\n session.idleTimer = this.createSessionIdleTimer(txId, moduleId);\n return { ok: true, result: r };\n } catch (err) {\n this.abortSession(txId);\n const code = this.mapErrorCode(err);\n log.warn(`[tx-session ${txId.slice(0, 8)}] step=${stepIndex} failed: ${(err as Error).message}`);\n return { ok: false, step: stepIndex, error: (err as Error).message, code };\n }\n }\n\n /**\n * \u63D0\u4EA4\u5F53\u524D\u4EA4\u4E92\u5F0F\u4E8B\u52A1\u4F1A\u8BDD\uFF0C\u5C06\u6240\u6709\u6B65\u9AA4\u7684\u4FEE\u6539\u6301\u4E45\u5316\u5E76\u91CA\u653E\u8FDE\u63A5\u3002\n *\n * @param txId \u4F1A\u8BDD\u552F\u4E00\u6807\u8BC6\u7B26\u3002\n * @param moduleId \u8C03\u7528\u6A21\u5757\u6807\u8BC6\u3002\n * @returns \u63D0\u4EA4\u7ED3\u679C\u54CD\u5E94\u3002\n */\n commitSession(txId: string, moduleId: string): TxResponse | TxError {\n const session = this.sessions.get(txId);\n if (!session) {\n return { ok: false, step: -1, error: \"\u4E8B\u52A1\u4F1A\u8BDD\u4E0D\u5B58\u5728\u6216\u5DF2\u7ED3\u675F\", code: \"internal\" };\n }\n if (session.moduleId !== moduleId) {\n return { ok: false, step: -1, error: \"moduleId \u4E0E\u4F1A\u8BDD\u4E0D\u7B26\", code: \"forbidden\" };\n }\n clearTimeout(session.idleTimer);\n try {\n this.deps.db.exec(\"COMMIT\");\n } catch (e) {\n this.abortSession(txId);\n return { ok: false, step: -1, error: `COMMIT \u5931\u8D25: ${(e as Error).message}`, code: \"internal\" };\n }\n const results = session.results;\n const steps = session.steps;\n this.sessions.delete(txId);\n this.releaseTransactionSlot();\n this.emitAfterCommit(steps);\n log.info(`[tx-session ${txId.slice(0, 8)}] commit ${results.length} steps OK`);\n return { ok: true, results };\n }\n\n /** \u53EA\u901A\u77E5\u63D2\u5165\u884C\u3002\u56DE\u8C03\u5931\u8D25\u53EA\u8BB0\u65E5\u5FD7\uFF0C\u4E8B\u52A1\u5DF2\u7ECF\u63D0\u4EA4\u3002 */\n private emitAfterCommit(steps: TxStep[]): void {\n const hook = this.afterCommit;\n if (!hook) return;\n const inserts: CommittedInsert[] = [];\n for (const step of steps) {\n if (step.op === \"insert\") inserts.push({ table: step.table, row: step.row });\n }\n if (inserts.length === 0) return;\n try {\n hook(inserts);\n } catch (err) {\n log.warn(`[tx] afterCommit: ${(err as Error).message}`);\n }\n }\n\n /**\n * \u56DE\u6EDA\u5F53\u524D\u4EA4\u4E92\u5F0F\u4E8B\u52A1\u4F1A\u8BDD\uFF0C\u64A4\u9500\u6240\u6709\u672A\u63D0\u4EA4\u7684\u6570\u636E\u5E93\u53D8\u66F4\u3002\n * \u5177\u6709\u5E42\u7B49\u6027\uFF1A\u4F1A\u8BDD\u5DF2\u7ED3\u675F\u6216\u5DF2\u56DE\u6EDA\u65F6\u540C\u6837\u8FD4\u56DE\u6210\u529F\u3002\n *\n * @param txId \u4F1A\u8BDD\u552F\u4E00\u6807\u8BC6\u7B26\u3002\n * @param moduleId \u8C03\u7528\u6A21\u5757\u6807\u8BC6\u3002\n * @returns \u56DE\u6EDA\u786E\u8BA4\u7ED3\u679C\u3002\n */\n rollbackSession(txId: string, moduleId: string): { ok: true } | TxError {\n const session = this.sessions.get(txId);\n if (!session) {\n // \u5E42\u7B49\uFF1A\u5DF2\u7ED3\u675F\u89C6\u4E3A\u6210\u529F\u56DE\u6EDA\n return { ok: true };\n }\n if (session.moduleId !== moduleId) {\n return { ok: false, step: -1, error: \"moduleId \u4E0E\u4F1A\u8BDD\u4E0D\u7B26\", code: \"forbidden\" };\n }\n this.abortSession(txId);\n return { ok: true };\n }\n\n private abortSession(txId: string): void {\n const session = this.sessions.get(txId);\n if (!session) return;\n clearTimeout(session.idleTimer);\n this.sessions.delete(txId);\n this.rollback();\n this.releaseTransactionSlot();\n log.info(`[tx-session ${txId.slice(0, 8)}] aborted`);\n }\n\n private async acquireTransactionSlot(): Promise<boolean> {\n if (!this.transactionBusy) {\n this.transactionBusy = true;\n return true;\n }\n return new Promise<boolean>((resolve) => {\n const waiter: TransactionWaiter = {\n resolve,\n timer: setTimeout(() => {\n const index = this.transactionWaiters.indexOf(waiter);\n if (index >= 0) this.transactionWaiters.splice(index, 1);\n resolve(false);\n }, this.slotWaitTimeoutMs),\n };\n this.transactionWaiters.push(waiter);\n });\n }\n\n private releaseTransactionSlot(): void {\n const next = this.transactionWaiters.shift();\n if (next) {\n clearTimeout(next.timer);\n next.resolve(true);\n return;\n }\n this.transactionBusy = false;\n }\n\n private createSessionIdleTimer(txId: string, moduleId: string): ReturnType<typeof setTimeout> {\n const timer = setTimeout(() => {\n if (!this.sessions.has(txId)) return;\n log.warn(`[tx-session ${txId.slice(0, 8)}] idle timeout module=${moduleId} after ${this.sessionIdleTimeoutMs}ms`);\n this.abortSession(txId);\n }, this.sessionIdleTimeoutMs);\n timer.unref?.();\n return timer;\n }\n\n private mapErrorCode(err: unknown): TxError[\"code\"] {\n return err instanceof PermissionDeniedError\n ? \"permission_denied\"\n : err instanceof DispatchError &&\n (err.code === \"no_such_service\" ||\n err.code === \"not_in_requires\" ||\n err.code === \"forbidden\" ||\n err.code === \"domain_error\")\n ? err.code\n : (err as { code?: string }).code === \"no_such_service\"\n ? \"no_such_service\"\n : (err as { code?: string }).code === \"not_in_requires\"\n ? \"not_in_requires\"\n : (err as { code?: string }).code === \"no_such_table\"\n ? \"no_such_table\"\n : \"internal\";\n }\n\n private rollback(): void {\n try {\n this.deps.db.exec(\"ROLLBACK\");\n } catch {\n // best-effort; fall through\n }\n }\n\n /** \u5355 step \u7531\u5BF9\u5E94\u6743\u9650\u68C0\u67E5 + SQL \u8DEF\u5F84\u6267\u884C */\n private async runOne(moduleId: string, manifest: ModuleManifestV2, step: TxStep): Promise<TxStepResult> {\n switch (step.op) {\n case \"query\":\n return this.doQuery(moduleId, manifest, step);\n case \"get\":\n return this.doGet(moduleId, manifest, step);\n case \"insert\":\n return this.doInsert(moduleId, manifest, step);\n case \"update\":\n return this.doUpdate(moduleId, manifest, step);\n case \"delete\":\n return this.doDelete(moduleId, manifest, step);\n case \"audit\":\n return this.doAudit(moduleId, manifest, step);\n case \"service\":\n return this.doService(moduleId, manifest, step);\n }\n }\n\n private requireTableRead(mod: ModuleManifestV2, table: string): void {\n identOk(table);\n this.assertTableRegistered(table);\n assertModulePermission(mod.id, mod.permissions, Perm.dbRead(table));\n }\n private requireTableWrite(mod: ModuleManifestV2, table: string): void {\n identOk(table);\n this.assertTableRegistered(table);\n assertModulePermission(mod.id, mod.permissions, Perm.dbWrite(table));\n }\n private assertTableRegistered(table: string): void {\n if (table.startsWith(\"sfmc_\")) return; // \u5E73\u53F0\u8868 \u2014 \u59CB\u7EC8\u53EF\u8BFB\u5199(\u5E73\u53F0\u5C42)\n if (!this.deps.schema.isRegistered(table)) {\n const err = new Error(`\u8868 \"${table}\" \u672A\u6CE8\u518C\u5230 schema-registry`);\n (err as { code?: string }).code = \"no_such_table\";\n throw err;\n }\n }\n\n private doQuery(_mid: string, mod: ModuleManifestV2, step: TxStepQuery): TxStepResult {\n this.requireTableRead(mod, step.table);\n const where = compile(step.opts?.where);\n // LSP:\u4E0E SDK QueryOptions.orderBy({field}|[]) \u5BF9\u9F50,\u517C\u6536\u9057\u7559 {col}\n const orders = normalizeOrderBy(step.opts?.orderBy);\n const limit = typeof step.opts?.limit === \"number\" ? step.opts?.limit : undefined;\n const offset = typeof step.opts?.offset === \"number\" ? step.opts?.offset : undefined;\n\n const whereClause = where.sql;\n const params = [...where.values];\n let extra = \"\";\n if (orders.length > 0) {\n const parts: string[] = [];\n for (const o of orders) {\n if (!IDENT.test(o.col)) throw new Error(`[tx] orderBy bad column \"${o.col}\"`);\n parts.push(`\"${o.col}\" ${o.dir === \"desc\" ? \"DESC\" : \"ASC\"}`);\n }\n extra += ` ORDER BY ${parts.join(\", \")}`;\n }\n if (typeof limit === \"number\") {\n extra += \" LIMIT \" + Math.max(0, Math.floor(limit));\n }\n if (typeof offset === \"number\") {\n extra += \" OFFSET \" + Math.max(0, Math.floor(offset));\n }\n\n const sql = `SELECT * FROM \"${step.table}\" WHERE ${whereClause}${extra}`;\n const rows = this.deps.db.prepare(sql).all(...(params as never[])) as Record<string, unknown>[];\n return { op: \"query\", rows };\n }\n\n /** \u89E3\u6790\u4E3B\u952E WHERE\u3002\u5355\u5217\u76F4\u63A5\u7B49\u503C;\u8054\u5408\u4E3B\u952E\u652F\u6301 `a|b|c`(\u4E0E data-backup \u7EA6\u5B9A\u4E00\u81F4)\u3002 */\n private pkWhere(table: string, id: string | number): { clause: string; values: unknown[] } {\n const cols = this.deps.db.prepare(`PRAGMA table_info(\"${table}\")`).all() as Array<{\n name: string;\n pk: number;\n }>;\n const pkCols = cols\n .filter((c) => c.pk > 0)\n .sort((a, b) => a.pk - b.pk)\n .map((c) => c.name);\n if (pkCols.length === 0) {\n throw new Error(`[tx] ${table} \u6CA1\u6709 primary key`);\n }\n if (pkCols.length === 1) {\n return { clause: `\"${pkCols[0]}\" = ?`, values: [id] };\n }\n const parts = String(id).split(\"|\");\n if (parts.length === pkCols.length) {\n return {\n clause: pkCols.map((c) => `\"${c}\" = ?`).join(\" AND \"),\n values: parts,\n };\n }\n // \u8054\u5408\u4E3B\u952E\u4F46\u53EA\u4F20\u4E86\u9996\u5217(\u9891\u9053 id \u7B49):\u6309\u7B2C\u4E00\u4E3B\u952E\u5217\u5339\u914D\n return { clause: `\"${pkCols[0]}\" = ?`, values: [id] };\n }\n\n private doGet(_mid: string, mod: ModuleManifestV2, step: TxStepGet): TxStepResult {\n this.requireTableRead(mod, step.table);\n const where = this.pkWhere(step.table, step.id);\n const row = this.deps.db\n .prepare(`SELECT * FROM \"${step.table}\" WHERE ${where.clause}`)\n .get(...(where.values as never[])) as Record<string, unknown> | undefined;\n return { op: \"get\", row: row ?? null };\n }\n\n private doInsert(_mid: string, mod: ModuleManifestV2, step: TxStepInsert): TxStepResult {\n this.requireTableWrite(mod, step.table);\n const cols = Object.keys(step.row);\n if (cols.length === 0) throw new Error(\"[tx] insert row \u5FC5\u987B\u81F3\u5C11\u4E00\u4E2A\u5B57\u6BB5\");\n for (const c of cols) {\n if (!IDENT.test(c)) throw new Error(`[tx] invalid column name \"${c}\"`);\n }\n const placeholders = cols.map(() => \"?\").join(\",\");\n const quoted = cols.map((c) => `\"${c}\"`).join(\",\");\n const values = cols.map((c) => step.row[c]);\n const sql = `INSERT INTO \"${step.table}\" (${quoted}) VALUES (${placeholders})`;\n try {\n this.deps.db.prepare(sql).run(...(values as never[]));\n } catch (e) {\n throw new Error(`INSERT ${step.table} \u5931\u8D25: ${(e as Error).message}`);\n }\n return { op: \"insert\", row: { ...step.row } };\n }\n\n private doUpdate(_mid: string, mod: ModuleManifestV2, step: TxStepUpdate): TxStepResult {\n this.requireTableWrite(mod, step.table);\n const cols = Object.keys(step.patch);\n if (cols.length === 0) throw new Error(\"[tx] update patch \u4E0D\u80FD\u4E3A\u7A7A\");\n for (const c of cols) {\n if (!IDENT.test(c)) throw new Error(`[tx] invalid column name \"${c}\"`);\n }\n const where = this.pkWhere(step.table, step.id);\n const sets = cols.map((c) => `\"${c}\" = ?`).join(\",\");\n const params = [...cols.map((c) => step.patch[c]), ...where.values];\n const sql = `UPDATE \"${step.table}\" SET ${sets} WHERE ${where.clause}`;\n const result = this.deps.db.prepare(sql).run(...(params as never[]));\n if (result.changes === 0) {\n throw new Error(`UPDATE ${step.table} id=${step.id} \u5F71\u54CD 0 \u884C(\u627E\u4E0D\u5230 / \u5DF2\u5220?)`);\n }\n return { op: \"update\", row: { ...step.patch, id: step.id } as unknown as Record<string, unknown> };\n }\n\n private doDelete(_mid: string, mod: ModuleManifestV2, step: TxStepDelete): TxStepResult {\n this.requireTableWrite(mod, step.table);\n const where = this.pkWhere(step.table, step.id);\n if (step.hard) {\n const sql = `DELETE FROM \"${step.table}\" WHERE ${where.clause}`;\n const r = this.deps.db.prepare(sql).run(...(where.values as never[]));\n return { op: \"delete\", changes: Number(r.changes) };\n }\n const cols = this.deps.db.prepare(`PRAGMA table_info(\"${step.table}\")`).all() as Array<{\n name: string;\n }>;\n const hasDeletedAt = cols.find((c) => c.name === \"_deleted_at\");\n if (!hasDeletedAt) {\n throw new Error(`[tx] ${step.table} \u6CA1\u542F\u7528 softDelete,\u5220\u4E0D\u6389;\u8BF7 hard=true \u6216 schema \u8BBE softDelete=true`);\n }\n const sql = `UPDATE \"${step.table}\" SET \"_deleted_at\" = ?, \"_version\" = COALESCE(\"_version\",0)+1 WHERE ${where.clause}`;\n const r = this.deps.db.prepare(sql).run(Date.now() as never, ...(where.values as never[]));\n return { op: \"delete\", changes: Number(r.changes) };\n }\n\n private doAudit(_mid: string, mod: ModuleManifestV2, step: TxStepAudit): TxStepResult {\n this.requireTableWrite(mod, step.table);\n const dataJson = step.data ? JSON.stringify(step.data) : null;\n const r = this.deps.db\n .prepare(\n `INSERT INTO sfmc__audit (module_id, table_name, row_id, action, data, created_at)\n VALUES (?, ?, ?, ?, ?, ?)`\n )\n .run(mod.id, step.table, step.rowId, step.action, dataJson, new Date().toISOString());\n return { op: \"audit\", changes: Number(r.changes) };\n }\n\n private async doService(_mid: string, mod: ModuleManifestV2, step: TxStepService): Promise<TxStepResult> {\n // requires / \u81EA\u8C03\u7528\u8C41\u514D\u7684\u552F\u4E00\u6743\u5A01\u5728 ServiceRegistry.dispatch(DRY/LSP);\n // \u6B64\u5904\u53EA\u8865 dispatch \u4E0D\u505A\u7684 service:<name> \u6743\u9650\u95E8(\u4E0E HTTP service-routes \u5BF9\u9F50)\u3002\n assertModulePermission(mod.id, mod.permissions, Perm.service(step.name));\n const result = await this.deps.serviceRegistry.dispatch(this.deps.enabled, mod.id, step.name, step.input, {\n query: this.deps.query,\n db: this.deps.db,\n });\n return { op: \"service\", result: result.result };\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["sql", "r"]
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sfmc-bds/db-server",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "SFMC SQLite HTTP REST backend — configs, modules, gameplay data",
5
5
  "author": "Shiroha7z",
6
6
  "license": "AGPL-3.0-only",
@@ -32,11 +32,11 @@
32
32
  ],
33
33
  "dependencies": {
34
34
  "sql-template-strings": "^2.2.2",
35
- "@sfmc-bds/bds-tools": "0.2.1",
36
- "@sfmc-bds/sdk": "0.2.1"
35
+ "@sfmc-bds/bds-tools": "0.2.3",
36
+ "@sfmc-bds/sdk": "0.2.3"
37
37
  },
38
38
  "devDependencies": {
39
- "@sfmc-bds/tools": "0.2.1"
39
+ "@sfmc-bds/tools": "0.2.3"
40
40
  },
41
41
  "publishConfig": {
42
42
  "access": "public",