@sfmc-bds/qq-bridge 0.2.0-beta.11 → 0.2.0-beta.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +68 -1
  2. package/deploy/install-nginx-webhook.sh +34 -0
  3. package/deploy/nginx-webhook-location.conf +10 -0
  4. package/deploy/qq-bridge.service +23 -0
  5. package/deploy/qq-config-sync.ps1 +106 -0
  6. package/deploy/qq-tunnel.ps1 +15 -0
  7. package/deploy/sync-qq-config.mjs +229 -0
  8. package/dist/commands/account-format.js +41 -0
  9. package/dist/commands/account-format.js.map +7 -0
  10. package/dist/commands/content-format.js +114 -0
  11. package/dist/commands/content-format.js.map +7 -0
  12. package/dist/commands/db-api.js +21 -6
  13. package/dist/commands/db-api.js.map +2 -2
  14. package/dist/commands/handlers.js +392 -524
  15. package/dist/commands/handlers.js.map +2 -2
  16. package/dist/commands/index.js +5 -5
  17. package/dist/commands/index.js.map +3 -3
  18. package/dist/commands/interaction-router.js +38 -133
  19. package/dist/commands/interaction-router.js.map +2 -2
  20. package/dist/commands/join-settings-ui.js +1 -24
  21. package/dist/commands/join-settings-ui.js.map +2 -2
  22. package/dist/commands/menu-format.js +36 -33
  23. package/dist/commands/menu-format.js.map +2 -2
  24. package/dist/commands/menu-panel-sync.js +39 -13
  25. package/dist/commands/menu-panel-sync.js.map +2 -2
  26. package/dist/commands/message-text.js +91 -0
  27. package/dist/commands/message-text.js.map +7 -0
  28. package/dist/commands/pending.js +23 -16
  29. package/dist/commands/pending.js.map +2 -2
  30. package/dist/commands/permissions.js +17 -0
  31. package/dist/commands/permissions.js.map +7 -0
  32. package/dist/commands/registry.js +1 -1
  33. package/dist/commands/registry.js.map +2 -2
  34. package/dist/commands/render.js +7 -16
  35. package/dist/commands/render.js.map +2 -2
  36. package/dist/commands/reply-llbot.js +18 -7
  37. package/dist/commands/reply-llbot.js.map +2 -2
  38. package/dist/commands/reply-official.js +55 -39
  39. package/dist/commands/reply-official.js.map +3 -3
  40. package/dist/commands/router.js +116 -23
  41. package/dist/commands/router.js.map +2 -2
  42. package/dist/config.js +35 -27
  43. package/dist/config.js.map +2 -2
  44. package/dist/console.js +3 -3
  45. package/dist/console.js.map +2 -2
  46. package/dist/index.js +30 -16
  47. package/dist/index.js.map +2 -2
  48. package/dist/llbot-ws-api.js +2 -2
  49. package/dist/llbot-ws-api.js.map +2 -2
  50. package/dist/official/events.js +32 -7
  51. package/dist/official/events.js.map +2 -2
  52. package/dist/official/gateway.js +10 -1
  53. package/dist/official/gateway.js.map +2 -2
  54. package/dist/official/webhook.js +165 -0
  55. package/dist/official/webhook.js.map +7 -0
  56. package/dist/onebot.js +0 -4
  57. package/dist/onebot.js.map +2 -2
  58. package/package.json +4 -4
  59. package/dist/commands/join-settings-ui.test.js +0 -43
  60. package/dist/commands/join-settings-ui.test.js.map +0 -7
  61. package/dist/commands/menu-panel-sync.test.js +0 -27
  62. package/dist/commands/menu-panel-sync.test.js.map +0 -7
  63. package/dist/commands/render.test.js +0 -44
  64. package/dist/commands/render.test.js.map +0 -7
  65. package/dist/commands/router.test.js +0 -92
  66. package/dist/commands/router.test.js.map +0 -7
  67. package/dist/llbot-ws-api.test.js +0 -32
  68. package/dist/llbot-ws-api.test.js.map +0 -7
  69. package/dist/official/events.test.js +0 -123
  70. package/dist/official/events.test.js.map +0 -7
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/commands/render.ts"],
4
- "sourcesContent": ["/**\n * commands/render.ts \u2014 CommandResult \u2192 \u5404\u540E\u7AEF\u5448\u73B0\u8F7D\u8377\uFF08\u65E0\u526F\u4F5C\u7528\uFF09\n */\n\nimport type { QqKeyboardButton } from \"@sfmc-bds/sdk/node/qq-official\";\nimport type { CommandResult } from \"./types.js\";\n\nexport type OfficialRenderPayload = {\n msgType: 0 | 2;\n content?: string;\n markdown?: string;\n keyboardButtons?: QqKeyboardButton[];\n};\n\nexport type LlbotRenderPayload = {\n text: string;\n};\n\n/** official\uFF1A\u6709 buttons/markdown \u7528 msg_type=2\uFF0C\u5426\u5219\u7EAF\u6587\u672C */\nexport function renderOfficial(result: CommandResult): OfficialRenderPayload {\n const buttons = result.buttons?.map((b) => ({\n id: b.id,\n label: b.label,\n data: b.command,\n ...(b.actionType !== undefined ? { actionType: b.actionType } : {}),\n ...(b.style !== undefined ? { style: b.style } : {}),\n ...(b.visitedLabel ? { visitedLabel: b.visitedLabel } : {}),\n ...(b.permission ? { permission: b.permission } : {}),\n }));\n const md = result.markdown?.trim() || undefined;\n if (md || (buttons && buttons.length > 0)) {\n const payload: OfficialRenderPayload = {\n msgType: 2,\n markdown: md ?? result.text,\n };\n if (buttons && buttons.length > 0) payload.keyboardButtons = buttons;\n return payload;\n }\n return { msgType: 0, content: result.text };\n}\n\n/** llbot\uFF1A\u6B63\u6587\u5DF2\u542B\u7F16\u53F7\u5217\u8868\u65F6\u4E0D\u518D\u91CD\u590D\uFF1B\u5426\u5219\u8865\u7F16\u53F7\u884C */\nexport function renderLlbot(result: CommandResult): LlbotRenderPayload {\n if (!result.buttons || result.buttons.length === 0) {\n return { text: result.text };\n }\n // \u4E3B/\u7BA1\u7406\u83DC\u5355\u6B63\u6587\u5DF2\u5E26\u300C1. \u6807\u7B7E \u2014 \u8BF4\u660E\u300D\uFF0C\u907F\u514D\u518D\u5806\u4E00\u5C42 [1] label\n if (/^\\d+\\.\\s/m.test(result.text)) {\n return {\n text: `${result.text}\\n\\n\u56DE\u590D\u6570\u5B57\u6267\u884C\uFF0860 \u79D2\u5185\u6709\u6548\uFF09`,\n };\n }\n const lines = result.buttons.map((b, i) => `[${i + 1}] ${b.label}`);\n return {\n text: `${result.text}\\n\\n${lines.join(\"\\n\")}\\n\\n\u56DE\u590D\u6570\u5B57\u6267\u884C\uFF0860 \u79D2\u5185\u6709\u6548\uFF09`,\n };\n}\n"],
5
- "mappings": "AAmBO,SAAS,eAAe,QAA8C;AAC3E,QAAM,UAAU,OAAO,SAAS,IAAI,CAAC,OAAO;AAAA,IAC1C,IAAI,EAAE;AAAA,IACN,OAAO,EAAE;AAAA,IACT,MAAM,EAAE;AAAA,IACR,GAAI,EAAE,eAAe,SAAY,EAAE,YAAY,EAAE,WAAW,IAAI,CAAC;AAAA,IACjE,GAAI,EAAE,UAAU,SAAY,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;AAAA,IAClD,GAAI,EAAE,eAAe,EAAE,cAAc,EAAE,aAAa,IAAI,CAAC;AAAA,IACzD,GAAI,EAAE,aAAa,EAAE,YAAY,EAAE,WAAW,IAAI,CAAC;AAAA,EACrD,EAAE;AACF,QAAM,KAAK,OAAO,UAAU,KAAK,KAAK;AACtC,MAAI,MAAO,WAAW,QAAQ,SAAS,GAAI;AACzC,UAAM,UAAiC;AAAA,MACrC,SAAS;AAAA,MACT,UAAU,MAAM,OAAO;AAAA,IACzB;AACA,QAAI,WAAW,QAAQ,SAAS,EAAG,SAAQ,kBAAkB;AAC7D,WAAO;AAAA,EACT;AACA,SAAO,EAAE,SAAS,GAAG,SAAS,OAAO,KAAK;AAC5C;AAGO,SAAS,YAAY,QAA2C;AACrE,MAAI,CAAC,OAAO,WAAW,OAAO,QAAQ,WAAW,GAAG;AAClD,WAAO,EAAE,MAAM,OAAO,KAAK;AAAA,EAC7B;AAEA,MAAI,YAAY,KAAK,OAAO,IAAI,GAAG;AACjC,WAAO;AAAA,MACL,MAAM,GAAG,OAAO,IAAI;AAAA;AAAA;AAAA,IACtB;AAAA,EACF;AACA,QAAM,QAAQ,OAAO,QAAQ,IAAI,CAAC,GAAG,MAAM,IAAI,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE;AAClE,SAAO;AAAA,IACL,MAAM,GAAG,OAAO,IAAI;AAAA;AAAA,EAAO,MAAM,KAAK,IAAI,CAAC;AAAA;AAAA;AAAA,EAC7C;AACF;",
4
+ "sourcesContent": ["/**\n * commands/render.ts \u2014 CommandResult \u2192 \u5404\u540E\u7AEF\u5448\u73B0\u8F7D\u8377\uFF08\u65E0\u526F\u4F5C\u7528\uFF09\n */\n\nimport type { QqKeyboardButton } from \"@sfmc-bds/sdk/node/qq-official\";\nimport type { CommandResult } from \"./types.js\";\n\nexport type OfficialRenderPayload = {\n msgType: 0 | 2;\n content?: string;\n markdown?: string;\n keyboardButtons?: QqKeyboardButton[];\n};\n\nexport type LlbotRenderPayload = {\n text: string;\n};\n\n/** official\uFF1A\u6709 buttons/markdown \u7528 msg_type=2\uFF0C\u5426\u5219\u7EAF\u6587\u672C */\nexport function renderOfficial(result: CommandResult): OfficialRenderPayload {\n const buttons = result.buttons?.map((b) => ({\n id: b.id,\n label: b.label,\n data: b.command,\n ...(b.actionType !== undefined ? { actionType: b.actionType } : {}),\n ...(b.style !== undefined ? { style: b.style } : {}),\n ...(b.visitedLabel ? { visitedLabel: b.visitedLabel } : {}),\n ...(b.permission ? { permission: b.permission } : {}),\n }));\n const md = result.markdown?.trim() || undefined;\n if (md || (buttons && buttons.length > 0)) {\n const payload: OfficialRenderPayload = {\n msgType: 2,\n markdown: md ?? result.text,\n };\n if (buttons && buttons.length > 0) payload.keyboardButtons = buttons;\n return payload;\n }\n return { msgType: 0, content: result.text };\n}\n\n/** LLBot \u53EA\u8F93\u51FA\u4E00\u4EFD\u64CD\u4F5C\u5217\u8868\uFF0C\u83DC\u5355\u8BF4\u660E\u4E0E\u64CD\u4F5C\u63D0\u793A\u5404\u51FA\u73B0\u4E00\u6B21\u3002 */\nexport function renderLlbot(result: CommandResult): LlbotRenderPayload {\n if (!result.buttons?.length) return { text: result.text };\n const lines = result.buttons.map((button, index) => {\n const detail = result.menu && button.description ? `\\n ${button.description}` : \"\";\n return `${index + 1}. ${button.label}${detail}`;\n });\n return {\n text: [result.text, \"\", result.menu ? \"\" : \"\u53EF\u9009\u64CD\u4F5C\", lines.join(\"\\n\"), \"\", \"\u8BF7\u5728 60 \u79D2\u5185\u56DE\u590D\u7F16\u53F7\u9009\u62E9\"]\n .filter((line, index, all) => line !== \"\" || all[index - 1] !== \"\")\n .join(\"\\n\"),\n };\n}\n"],
5
+ "mappings": "AAmBO,SAAS,eAAe,QAA8C;AAC3E,QAAM,UAAU,OAAO,SAAS,IAAI,CAAC,OAAO;AAAA,IAC1C,IAAI,EAAE;AAAA,IACN,OAAO,EAAE;AAAA,IACT,MAAM,EAAE;AAAA,IACR,GAAI,EAAE,eAAe,SAAY,EAAE,YAAY,EAAE,WAAW,IAAI,CAAC;AAAA,IACjE,GAAI,EAAE,UAAU,SAAY,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;AAAA,IAClD,GAAI,EAAE,eAAe,EAAE,cAAc,EAAE,aAAa,IAAI,CAAC;AAAA,IACzD,GAAI,EAAE,aAAa,EAAE,YAAY,EAAE,WAAW,IAAI,CAAC;AAAA,EACrD,EAAE;AACF,QAAM,KAAK,OAAO,UAAU,KAAK,KAAK;AACtC,MAAI,MAAO,WAAW,QAAQ,SAAS,GAAI;AACzC,UAAM,UAAiC;AAAA,MACrC,SAAS;AAAA,MACT,UAAU,MAAM,OAAO;AAAA,IACzB;AACA,QAAI,WAAW,QAAQ,SAAS,EAAG,SAAQ,kBAAkB;AAC7D,WAAO;AAAA,EACT;AACA,SAAO,EAAE,SAAS,GAAG,SAAS,OAAO,KAAK;AAC5C;AAGO,SAAS,YAAY,QAA2C;AACrE,MAAI,CAAC,OAAO,SAAS,OAAQ,QAAO,EAAE,MAAM,OAAO,KAAK;AACxD,QAAM,QAAQ,OAAO,QAAQ,IAAI,CAAC,QAAQ,UAAU;AAClD,UAAM,SAAS,OAAO,QAAQ,OAAO,cAAc;AAAA,KAAQ,OAAO,WAAW,KAAK;AAClF,WAAO,GAAG,QAAQ,CAAC,KAAK,OAAO,KAAK,GAAG,MAAM;AAAA,EAC/C,CAAC;AACD,SAAO;AAAA,IACL,MAAM,CAAC,OAAO,MAAM,IAAI,OAAO,OAAO,KAAK,4BAAQ,MAAM,KAAK,IAAI,GAAG,IAAI,kEAAgB,EACtF,OAAO,CAAC,MAAM,OAAO,QAAQ,SAAS,MAAM,IAAI,QAAQ,CAAC,MAAM,EAAE,EACjE,KAAK,IAAI;AAAA,EACd;AACF;",
6
6
  "names": []
7
7
  }
@@ -1,20 +1,23 @@
1
1
  import { request } from "node:http";
2
2
  import { llbotWsApi } from "../llbot-ws-api.js";
3
3
  import { log } from "../log.js";
4
+ import { splitMessage } from "./message-text.js";
4
5
  import { renderLlbot } from "./render.js";
5
6
  function createLlbotReplyPort(cfg) {
6
7
  return {
7
8
  async send(_target, result, _inbound) {
8
9
  if (!cfg.groupId || cfg.groupId === "0") {
9
- throw new Error("llbot qq_group_id \u672A\u914D\u7F6E");
10
+ throw new Error("llbot llbot.group_id \u672A\u914D\u7F6E");
10
11
  }
11
12
  const { text } = renderLlbot(result);
12
- if (llbotWsApi.connected) {
13
- await llbotWsApi.sendGroupMsg(cfg.groupId, text);
14
- return;
13
+ for (const part of splitMessage(text)) {
14
+ if (llbotWsApi.connected) {
15
+ await llbotWsApi.sendGroupMsg(cfg.groupId, part);
16
+ } else {
17
+ log.warn("LLBot reverse-ws \u672A\u8FDE\u63A5\uFF0C\u56DE\u9000 HTTP send_group_msg");
18
+ await sendGroupMsgHttp(cfg, part);
19
+ }
15
20
  }
16
- log.warn("LLBot reverse-ws \u672A\u8FDE\u63A5\uFF0C\u56DE\u9000 HTTP send_group_msg");
17
- await sendGroupMsgHttp(cfg, text);
18
21
  }
19
22
  };
20
23
  }
@@ -37,6 +40,7 @@ function sendGroupMsgHttp(cfg, text) {
37
40
  port: cfg.port,
38
41
  path: "/send_group_msg",
39
42
  method: "POST",
43
+ timeout: 5e3,
40
44
  headers
41
45
  },
42
46
  (res) => {
@@ -48,10 +52,17 @@ function sendGroupMsgHttp(cfg, text) {
48
52
  reject(new Error(`LLBot HTTP ${res.statusCode}`));
49
53
  return;
50
54
  }
51
- resolve();
55
+ try {
56
+ const result = JSON.parse(body);
57
+ if (result.status !== "ok" || result.retcode !== 0) throw new Error("LLBot \u53D1\u9001\u5931\u8D25");
58
+ resolve();
59
+ } catch (error) {
60
+ reject(error);
61
+ }
52
62
  });
53
63
  }
54
64
  );
65
+ req.on("timeout", () => req.destroy(new Error("LLBot \u8BF7\u6C42\u8D85\u65F6")));
55
66
  req.on("error", reject);
56
67
  req.write(payload);
57
68
  req.end();
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/commands/reply-llbot.ts"],
4
- "sourcesContent": ["/**\n * commands/reply-llbot.ts \u2014 LLBot ReplyPort\n *\n * \u4F18\u5148\u7ECF reverse-WS \u8C03 send_group_msg\uFF1BWS \u4E0D\u53EF\u7528\u65F6\u518D\u56DE\u9000 HTTP(llbot_port)\u3002\n */\n\nimport { request } from \"node:http\";\nimport { llbotWsApi } from \"../llbot-ws-api.js\";\nimport { log } from \"../log.js\";\nimport { renderLlbot } from \"./render.js\";\nimport type { CommandResult, InboundMessage, ReplyPort, ReplyTarget } from \"./types.js\";\n\nexport type LlbotReplyConfig = {\n host: string;\n port: number;\n token: string;\n groupId: string;\n};\n\nexport function createLlbotReplyPort(cfg: LlbotReplyConfig): ReplyPort {\n return {\n async send(_target: ReplyTarget, result: CommandResult, _inbound: InboundMessage): Promise<void> {\n if (!cfg.groupId || cfg.groupId === \"0\") {\n throw new Error(\"llbot qq_group_id \u672A\u914D\u7F6E\");\n }\n const { text } = renderLlbot(result);\n\n if (llbotWsApi.connected) {\n await llbotWsApi.sendGroupMsg(cfg.groupId, text);\n return;\n }\n\n log.warn(\"LLBot reverse-ws \u672A\u8FDE\u63A5\uFF0C\u56DE\u9000 HTTP send_group_msg\");\n await sendGroupMsgHttp(cfg, text);\n },\n };\n}\n\nfunction sendGroupMsgHttp(cfg: LlbotReplyConfig, text: string): Promise<void> {\n return new Promise((resolve, reject) => {\n const payload = JSON.stringify({\n group_id: parseInt(cfg.groupId, 10),\n message: [{ type: \"text\", data: { text } }],\n });\n const headers: Record<string, string | number> = {\n \"Content-Type\": \"application/json\",\n \"Content-Length\": Buffer.byteLength(payload),\n };\n if (cfg.token) {\n headers[\"Authorization\"] = `Bearer ${cfg.token}`;\n }\n const req = request(\n {\n hostname: cfg.host,\n port: cfg.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(`LLBot HTTP send_group_msg \u2192 ${res.statusCode}: ${body.slice(0, 200)}`);\n reject(new Error(`LLBot HTTP ${res.statusCode}`));\n return;\n }\n resolve();\n });\n }\n );\n req.on(\"error\", reject);\n req.write(payload);\n req.end();\n });\n}\n"],
5
- "mappings": "AAMA,SAAS,eAAe;AACxB,SAAS,kBAAkB;AAC3B,SAAS,WAAW;AACpB,SAAS,mBAAmB;AAUrB,SAAS,qBAAqB,KAAkC;AACrE,SAAO;AAAA,IACL,MAAM,KAAK,SAAsB,QAAuB,UAAyC;AAC/F,UAAI,CAAC,IAAI,WAAW,IAAI,YAAY,KAAK;AACvC,cAAM,IAAI,MAAM,sCAAuB;AAAA,MACzC;AACA,YAAM,EAAE,KAAK,IAAI,YAAY,MAAM;AAEnC,UAAI,WAAW,WAAW;AACxB,cAAM,WAAW,aAAa,IAAI,SAAS,IAAI;AAC/C;AAAA,MACF;AAEA,UAAI,KAAK,2EAA6C;AACtD,YAAM,iBAAiB,KAAK,IAAI;AAAA,IAClC;AAAA,EACF;AACF;AAEA,SAAS,iBAAiB,KAAuB,MAA6B;AAC5E,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,UAAU,KAAK,UAAU;AAAA,MAC7B,UAAU,SAAS,IAAI,SAAS,EAAE;AAAA,MAClC,SAAS,CAAC,EAAE,MAAM,QAAQ,MAAM,EAAE,KAAK,EAAE,CAAC;AAAA,IAC5C,CAAC;AACD,UAAM,UAA2C;AAAA,MAC/C,gBAAgB;AAAA,MAChB,kBAAkB,OAAO,WAAW,OAAO;AAAA,IAC7C;AACA,QAAI,IAAI,OAAO;AACb,cAAQ,eAAe,IAAI,UAAU,IAAI,KAAK;AAAA,IAChD;AACA,UAAM,MAAM;AAAA,MACV;AAAA,QACE,UAAU,IAAI;AAAA,QACd,MAAM,IAAI;AAAA,QACV,MAAM;AAAA,QACN,QAAQ;AAAA,QACR;AAAA,MACF;AAAA,MACA,CAAC,QAAQ;AACP,YAAI,OAAO;AACX,YAAI,GAAG,QAAQ,CAAC,MAAO,QAAQ,CAAE;AACjC,YAAI,GAAG,OAAO,MAAM;AAClB,cAAI,IAAI,eAAe,KAAK;AAC1B,gBAAI,KAAK,oCAA+B,IAAI,UAAU,KAAK,KAAK,MAAM,GAAG,GAAG,CAAC,EAAE;AAC/E,mBAAO,IAAI,MAAM,cAAc,IAAI,UAAU,EAAE,CAAC;AAChD;AAAA,UACF;AACA,kBAAQ;AAAA,QACV,CAAC;AAAA,MACH;AAAA,IACF;AACA,QAAI,GAAG,SAAS,MAAM;AACtB,QAAI,MAAM,OAAO;AACjB,QAAI,IAAI;AAAA,EACV,CAAC;AACH;",
4
+ "sourcesContent": ["/**\n * commands/reply-llbot.ts \u2014 LLBot ReplyPort\n *\n * \u4F18\u5148\u7ECF reverse-WS \u8C03 send_group_msg\uFF1BWS \u4E0D\u53EF\u7528\u65F6\u518D\u56DE\u9000 HTTP(llbot.port)\u3002\n */\n\nimport { request } from \"node:http\";\nimport { llbotWsApi } from \"../llbot-ws-api.js\";\nimport { log } from \"../log.js\";\nimport { splitMessage } from \"./message-text.js\";\nimport { renderLlbot } from \"./render.js\";\nimport type { CommandResult, InboundMessage, ReplyPort, ReplyTarget } from \"./types.js\";\n\nexport type LlbotReplyConfig = {\n host: string;\n port: number;\n token: string;\n groupId: string;\n};\n\nexport function createLlbotReplyPort(cfg: LlbotReplyConfig): ReplyPort {\n return {\n async send(_target: ReplyTarget, result: CommandResult, _inbound: InboundMessage): Promise<void> {\n if (!cfg.groupId || cfg.groupId === \"0\") {\n throw new Error(\"llbot llbot.group_id \u672A\u914D\u7F6E\");\n }\n const { text } = renderLlbot(result);\n\n for (const part of splitMessage(text)) {\n if (llbotWsApi.connected) {\n await llbotWsApi.sendGroupMsg(cfg.groupId, part);\n } else {\n log.warn(\"LLBot reverse-ws \u672A\u8FDE\u63A5\uFF0C\u56DE\u9000 HTTP send_group_msg\");\n await sendGroupMsgHttp(cfg, part);\n }\n }\n },\n };\n}\n\nfunction sendGroupMsgHttp(cfg: LlbotReplyConfig, text: string): Promise<void> {\n return new Promise((resolve, reject) => {\n const payload = JSON.stringify({\n group_id: parseInt(cfg.groupId, 10),\n message: [{ type: \"text\", data: { text } }],\n });\n const headers: Record<string, string | number> = {\n \"Content-Type\": \"application/json\",\n \"Content-Length\": Buffer.byteLength(payload),\n };\n if (cfg.token) {\n headers[\"Authorization\"] = `Bearer ${cfg.token}`;\n }\n const req = request(\n {\n hostname: cfg.host,\n port: cfg.port,\n path: \"/send_group_msg\",\n method: \"POST\",\n timeout: 5_000,\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(`LLBot HTTP send_group_msg \u2192 ${res.statusCode}: ${body.slice(0, 200)}`);\n reject(new Error(`LLBot HTTP ${res.statusCode}`));\n return;\n }\n try {\n const result = JSON.parse(body) as { status?: string; retcode?: number };\n if (result.status !== \"ok\" || result.retcode !== 0) throw new Error(\"LLBot \u53D1\u9001\u5931\u8D25\");\n resolve();\n } catch (error) {\n reject(error);\n }\n });\n }\n );\n req.on(\"timeout\", () => req.destroy(new Error(\"LLBot \u8BF7\u6C42\u8D85\u65F6\")));\n req.on(\"error\", reject);\n req.write(payload);\n req.end();\n });\n}\n"],
5
+ "mappings": "AAMA,SAAS,eAAe;AACxB,SAAS,kBAAkB;AAC3B,SAAS,WAAW;AACpB,SAAS,oBAAoB;AAC7B,SAAS,mBAAmB;AAUrB,SAAS,qBAAqB,KAAkC;AACrE,SAAO;AAAA,IACL,MAAM,KAAK,SAAsB,QAAuB,UAAyC;AAC/F,UAAI,CAAC,IAAI,WAAW,IAAI,YAAY,KAAK;AACvC,cAAM,IAAI,MAAM,yCAA0B;AAAA,MAC5C;AACA,YAAM,EAAE,KAAK,IAAI,YAAY,MAAM;AAEnC,iBAAW,QAAQ,aAAa,IAAI,GAAG;AACrC,YAAI,WAAW,WAAW;AACxB,gBAAM,WAAW,aAAa,IAAI,SAAS,IAAI;AAAA,QACjD,OAAO;AACL,cAAI,KAAK,2EAA6C;AACtD,gBAAM,iBAAiB,KAAK,IAAI;AAAA,QAClC;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAEA,SAAS,iBAAiB,KAAuB,MAA6B;AAC5E,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,UAAU,KAAK,UAAU;AAAA,MAC7B,UAAU,SAAS,IAAI,SAAS,EAAE;AAAA,MAClC,SAAS,CAAC,EAAE,MAAM,QAAQ,MAAM,EAAE,KAAK,EAAE,CAAC;AAAA,IAC5C,CAAC;AACD,UAAM,UAA2C;AAAA,MAC/C,gBAAgB;AAAA,MAChB,kBAAkB,OAAO,WAAW,OAAO;AAAA,IAC7C;AACA,QAAI,IAAI,OAAO;AACb,cAAQ,eAAe,IAAI,UAAU,IAAI,KAAK;AAAA,IAChD;AACA,UAAM,MAAM;AAAA,MACV;AAAA,QACE,UAAU,IAAI;AAAA,QACd,MAAM,IAAI;AAAA,QACV,MAAM;AAAA,QACN,QAAQ;AAAA,QACR,SAAS;AAAA,QACT;AAAA,MACF;AAAA,MACA,CAAC,QAAQ;AACP,YAAI,OAAO;AACX,YAAI,GAAG,QAAQ,CAAC,MAAO,QAAQ,CAAE;AACjC,YAAI,GAAG,OAAO,MAAM;AAClB,cAAI,IAAI,eAAe,KAAK;AAC1B,gBAAI,KAAK,oCAA+B,IAAI,UAAU,KAAK,KAAK,MAAM,GAAG,GAAG,CAAC,EAAE;AAC/E,mBAAO,IAAI,MAAM,cAAc,IAAI,UAAU,EAAE,CAAC;AAChD;AAAA,UACF;AACA,cAAI;AACF,kBAAM,SAAS,KAAK,MAAM,IAAI;AAC9B,gBAAI,OAAO,WAAW,QAAQ,OAAO,YAAY,EAAG,OAAM,IAAI,MAAM,gCAAY;AAChF,oBAAQ;AAAA,UACV,SAAS,OAAO;AACd,mBAAO,KAAK;AAAA,UACd;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF;AACA,QAAI,GAAG,WAAW,MAAM,IAAI,QAAQ,IAAI,MAAM,gCAAY,CAAC,CAAC;AAC5D,QAAI,GAAG,SAAS,MAAM;AACtB,QAAI,MAAM,OAAO;AACjB,QAAI,IAAI;AAAA,EACV,CAAC;AACH;",
6
6
  "names": []
7
7
  }
@@ -1,8 +1,6 @@
1
- import {
2
- sendC2cMessage,
3
- sendGroupMessage
4
- } from "@sfmc-bds/sdk/node/qq-official";
1
+ import { sendC2cMessage, sendGroupMessage } from "@sfmc-bds/sdk/node/qq-official";
5
2
  import { log } from "../log.js";
3
+ import { officialPlainFallback, splitMarkdownMessage, splitMessage } from "./message-text.js";
6
4
  import { renderOfficial } from "./render.js";
7
5
  function isC2c(inbound) {
8
6
  return inbound.scene === "c2c" || inbound.groupId.startsWith("c2c:");
@@ -15,47 +13,65 @@ function createOfficialReplyPort(creds) {
15
13
  let msgSeq = 0;
16
14
  return {
17
15
  async send(target, result, inbound) {
18
- const rendered = renderOfficial(result);
19
- msgSeq += 1;
20
- const c2c = isC2c(inbound);
21
- const common = {
22
- msgType: rendered.msgType,
23
- ...rendered.msgType === 2 ? { markdown: rendered.markdown ?? result.text } : { content: rendered.content ?? result.text },
24
- ...rendered.keyboardButtons ? { keyboardButtons: rendered.keyboardButtons } : {},
25
- ...target.msgId ? { msgId: target.msgId, msgSeq } : {}
26
- };
27
- const sendOnce = async (plainFallback) => {
28
- if (c2c) {
29
- const userOpenid = c2cUserOpenid(inbound);
16
+ const markdown = result.markdown?.trim() ?? "";
17
+ const parts = markdown ? splitMarkdownMessage(markdown) : splitMessage(result.text);
18
+ for (let index = 0; index < parts.length; index++) {
19
+ const content = parts[index];
20
+ const isLast = index === parts.length - 1;
21
+ const rendered = markdown ? renderOfficial({
22
+ text: result.text,
23
+ markdown: content,
24
+ ...isLast && result.buttons ? { buttons: result.buttons } : {}
25
+ }) : parts.length === 1 ? renderOfficial({ ...result, text: content }) : renderOfficial({
26
+ text: content,
27
+ ...isLast && result.buttons ? { buttons: result.buttons } : {}
28
+ });
29
+ msgSeq += 1;
30
+ const c2c = isC2c(inbound);
31
+ const common = {
32
+ msgType: rendered.msgType,
33
+ ...rendered.msgType === 2 ? { markdown: rendered.markdown ?? result.text } : { content: rendered.content ?? result.text },
34
+ ...rendered.keyboardButtons ? {
35
+ keyboardRows: Array.from(
36
+ { length: Math.ceil(rendered.keyboardButtons.length / 2) },
37
+ (_, index2) => rendered.keyboardButtons.slice(index2 * 2, index2 * 2 + 2)
38
+ )
39
+ } : {},
40
+ ...target.msgId ? { msgId: target.msgId, msgSeq } : {}
41
+ };
42
+ const sendOnce = async (plainFallback) => {
43
+ if (c2c) {
44
+ const userOpenid = c2cUserOpenid(inbound);
45
+ if (plainFallback) {
46
+ return sendC2cMessage(creds, {
47
+ userOpenid,
48
+ msgType: 0,
49
+ content: parts.length === 1 ? officialPlainFallback(result) : content,
50
+ ...target.msgId ? { msgId: target.msgId, msgSeq: ++msgSeq } : {}
51
+ });
52
+ }
53
+ return sendC2cMessage(creds, { userOpenid, ...common });
54
+ }
30
55
  if (plainFallback) {
31
- return sendC2cMessage(creds, {
32
- userOpenid,
56
+ return sendGroupMessage(creds, {
57
+ groupOpenid: target.groupId,
33
58
  msgType: 0,
34
- content: result.text,
59
+ content: parts.length === 1 ? officialPlainFallback(result) : content,
35
60
  ...target.msgId ? { msgId: target.msgId, msgSeq: ++msgSeq } : {}
36
61
  });
37
62
  }
38
- return sendC2cMessage(creds, { userOpenid, ...common });
39
- }
40
- if (plainFallback) {
41
- return sendGroupMessage(creds, {
42
- groupOpenid: target.groupId,
43
- msgType: 0,
44
- content: result.text,
45
- ...target.msgId ? { msgId: target.msgId, msgSeq: ++msgSeq } : {}
46
- });
47
- }
48
- return sendGroupMessage(creds, { groupOpenid: target.groupId, ...common });
49
- };
50
- const res = await sendOnce(false);
51
- if (!res.ok) {
52
- if (rendered.msgType === 2) {
53
- log.warn(`\u5B98\u65B9 Markdown \u53D1\u9001\u5931\u8D25\uFF0C\u964D\u7EA7\u6587\u672C: ${res.error}`);
54
- const plain = await sendOnce(true);
55
- if (!plain.ok) throw new Error(plain.error);
56
- return;
63
+ return sendGroupMessage(creds, { groupOpenid: target.groupId, ...common });
64
+ };
65
+ const res = await sendOnce(false);
66
+ if (!res.ok) {
67
+ if (rendered.msgType === 2) {
68
+ log.warn(`\u5B98\u65B9 Markdown \u53D1\u9001\u5931\u8D25\uFF0C\u964D\u7EA7\u6587\u672C: ${res.error}`);
69
+ const plain = await sendOnce(true);
70
+ if (!plain.ok) throw new Error(plain.error);
71
+ continue;
72
+ }
73
+ throw new Error(res.error);
57
74
  }
58
- throw new Error(res.error);
59
75
  }
60
76
  }
61
77
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/commands/reply-official.ts"],
4
- "sourcesContent": ["/**\n * commands/reply-official.ts \u2014 \u5B98\u65B9 ReplyPort\uFF08\u7FA4 / C2C\uFF09\n */\n\nimport {\n sendC2cMessage,\n sendGroupMessage,\n type QqOfficialCredentials,\n} from \"@sfmc-bds/sdk/node/qq-official\";\nimport { log } from \"../log.js\";\nimport { renderOfficial } from \"./render.js\";\nimport type { CommandResult, InboundMessage, ReplyPort, ReplyTarget } from \"./types.js\";\n\nfunction isC2c(inbound: InboundMessage): boolean {\n return inbound.scene === \"c2c\" || inbound.groupId.startsWith(\"c2c:\");\n}\n\nfunction c2cUserOpenid(inbound: InboundMessage): string {\n if (inbound.groupId.startsWith(\"c2c:\")) return inbound.groupId.slice(\"c2c:\".length);\n return inbound.userId;\n}\n\nexport function createOfficialReplyPort(creds: QqOfficialCredentials): ReplyPort {\n let msgSeq = 0;\n return {\n async send(target: ReplyTarget, result: CommandResult, inbound: InboundMessage): Promise<void> {\n const rendered = renderOfficial(result);\n msgSeq += 1;\n const c2c = isC2c(inbound);\n const common = {\n msgType: rendered.msgType as 0 | 2,\n ...(rendered.msgType === 2\n ? { markdown: rendered.markdown ?? result.text }\n : { content: rendered.content ?? result.text }),\n ...(rendered.keyboardButtons ? { keyboardButtons: rendered.keyboardButtons } : {}),\n ...(target.msgId ? { msgId: target.msgId, msgSeq } : {}),\n };\n\n const sendOnce = async (plainFallback: boolean) => {\n if (c2c) {\n const userOpenid = c2cUserOpenid(inbound);\n if (plainFallback) {\n return sendC2cMessage(creds, {\n userOpenid,\n msgType: 0,\n content: result.text,\n ...(target.msgId ? { msgId: target.msgId, msgSeq: ++msgSeq } : {}),\n });\n }\n return sendC2cMessage(creds, { userOpenid, ...common });\n }\n if (plainFallback) {\n return sendGroupMessage(creds, {\n groupOpenid: target.groupId,\n msgType: 0,\n content: result.text,\n ...(target.msgId ? { msgId: target.msgId, msgSeq: ++msgSeq } : {}),\n });\n }\n return sendGroupMessage(creds, { groupOpenid: target.groupId, ...common });\n };\n\n const res = await sendOnce(false);\n if (!res.ok) {\n if (rendered.msgType === 2) {\n log.warn(`\u5B98\u65B9 Markdown \u53D1\u9001\u5931\u8D25\uFF0C\u964D\u7EA7\u6587\u672C: ${res.error}`);\n const plain = await sendOnce(true);\n if (!plain.ok) throw new Error(plain.error);\n return;\n }\n throw new Error(res.error);\n }\n },\n };\n}\n"],
5
- "mappings": "AAIA;AAAA,EACE;AAAA,EACA;AAAA,OAEK;AACP,SAAS,WAAW;AACpB,SAAS,sBAAsB;AAG/B,SAAS,MAAM,SAAkC;AAC/C,SAAO,QAAQ,UAAU,SAAS,QAAQ,QAAQ,WAAW,MAAM;AACrE;AAEA,SAAS,cAAc,SAAiC;AACtD,MAAI,QAAQ,QAAQ,WAAW,MAAM,EAAG,QAAO,QAAQ,QAAQ,MAAM,OAAO,MAAM;AAClF,SAAO,QAAQ;AACjB;AAEO,SAAS,wBAAwB,OAAyC;AAC/E,MAAI,SAAS;AACb,SAAO;AAAA,IACL,MAAM,KAAK,QAAqB,QAAuB,SAAwC;AAC7F,YAAM,WAAW,eAAe,MAAM;AACtC,gBAAU;AACV,YAAM,MAAM,MAAM,OAAO;AACzB,YAAM,SAAS;AAAA,QACb,SAAS,SAAS;AAAA,QAClB,GAAI,SAAS,YAAY,IACrB,EAAE,UAAU,SAAS,YAAY,OAAO,KAAK,IAC7C,EAAE,SAAS,SAAS,WAAW,OAAO,KAAK;AAAA,QAC/C,GAAI,SAAS,kBAAkB,EAAE,iBAAiB,SAAS,gBAAgB,IAAI,CAAC;AAAA,QAChF,GAAI,OAAO,QAAQ,EAAE,OAAO,OAAO,OAAO,OAAO,IAAI,CAAC;AAAA,MACxD;AAEA,YAAM,WAAW,OAAO,kBAA2B;AACjD,YAAI,KAAK;AACP,gBAAM,aAAa,cAAc,OAAO;AACxC,cAAI,eAAe;AACjB,mBAAO,eAAe,OAAO;AAAA,cAC3B;AAAA,cACA,SAAS;AAAA,cACT,SAAS,OAAO;AAAA,cAChB,GAAI,OAAO,QAAQ,EAAE,OAAO,OAAO,OAAO,QAAQ,EAAE,OAAO,IAAI,CAAC;AAAA,YAClE,CAAC;AAAA,UACH;AACA,iBAAO,eAAe,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC;AAAA,QACxD;AACA,YAAI,eAAe;AACjB,iBAAO,iBAAiB,OAAO;AAAA,YAC7B,aAAa,OAAO;AAAA,YACpB,SAAS;AAAA,YACT,SAAS,OAAO;AAAA,YAChB,GAAI,OAAO,QAAQ,EAAE,OAAO,OAAO,OAAO,QAAQ,EAAE,OAAO,IAAI,CAAC;AAAA,UAClE,CAAC;AAAA,QACH;AACA,eAAO,iBAAiB,OAAO,EAAE,aAAa,OAAO,SAAS,GAAG,OAAO,CAAC;AAAA,MAC3E;AAEA,YAAM,MAAM,MAAM,SAAS,KAAK;AAChC,UAAI,CAAC,IAAI,IAAI;AACX,YAAI,SAAS,YAAY,GAAG;AAC1B,cAAI,KAAK,iFAA0B,IAAI,KAAK,EAAE;AAC9C,gBAAM,QAAQ,MAAM,SAAS,IAAI;AACjC,cAAI,CAAC,MAAM,GAAI,OAAM,IAAI,MAAM,MAAM,KAAK;AAC1C;AAAA,QACF;AACA,cAAM,IAAI,MAAM,IAAI,KAAK;AAAA,MAC3B;AAAA,IACF;AAAA,EACF;AACF;",
6
- "names": []
4
+ "sourcesContent": ["/**\n * commands/reply-official.ts \u2014 \u5B98\u65B9 ReplyPort\uFF08\u7FA4 / C2C\uFF09\n */\n\nimport { sendC2cMessage, sendGroupMessage, type QqOfficialCredentials } from \"@sfmc-bds/sdk/node/qq-official\";\nimport { log } from \"../log.js\";\nimport { officialPlainFallback, splitMarkdownMessage, splitMessage } from \"./message-text.js\";\nimport { renderOfficial } from \"./render.js\";\nimport type { CommandResult, InboundMessage, ReplyPort, ReplyTarget } from \"./types.js\";\n\nfunction isC2c(inbound: InboundMessage): boolean {\n return inbound.scene === \"c2c\" || inbound.groupId.startsWith(\"c2c:\");\n}\n\nfunction c2cUserOpenid(inbound: InboundMessage): string {\n if (inbound.groupId.startsWith(\"c2c:\")) return inbound.groupId.slice(\"c2c:\".length);\n return inbound.userId;\n}\n\nexport function createOfficialReplyPort(creds: QqOfficialCredentials): ReplyPort {\n let msgSeq = 0;\n return {\n async send(target: ReplyTarget, result: CommandResult, inbound: InboundMessage): Promise<void> {\n const markdown = result.markdown?.trim() ?? \"\";\n // \u6709 Markdown \u65F6\u6309\u8868\u683C\u8FB9\u754C\u62C6\u6BB5\uFF0C\u907F\u514D\u8D85\u957F\u4E16\u754C\u5305\u5217\u8868\u88AB\u964D\u6210\u7EAF\u6587\u672C\u3002\n const parts = markdown ? splitMarkdownMessage(markdown) : splitMessage(result.text);\n for (let index = 0; index < parts.length; index++) {\n const content = parts[index]!;\n const isLast = index === parts.length - 1;\n const rendered = markdown\n ? renderOfficial({\n text: result.text,\n markdown: content,\n ...(isLast && result.buttons ? { buttons: result.buttons } : {}),\n })\n : parts.length === 1\n ? renderOfficial({ ...result, text: content })\n : renderOfficial({\n text: content,\n ...(isLast && result.buttons ? { buttons: result.buttons } : {}),\n });\n msgSeq += 1;\n const c2c = isC2c(inbound);\n const common = {\n msgType: rendered.msgType as 0 | 2,\n ...(rendered.msgType === 2\n ? { markdown: rendered.markdown ?? result.text }\n : { content: rendered.content ?? result.text }),\n ...(rendered.keyboardButtons\n ? {\n keyboardRows: Array.from({ length: Math.ceil(rendered.keyboardButtons.length / 2) }, (_, index) =>\n rendered.keyboardButtons!.slice(index * 2, index * 2 + 2)\n ),\n }\n : {}),\n ...(target.msgId ? { msgId: target.msgId, msgSeq } : {}),\n };\n\n const sendOnce = async (plainFallback: boolean) => {\n if (c2c) {\n const userOpenid = c2cUserOpenid(inbound);\n if (plainFallback) {\n return sendC2cMessage(creds, {\n userOpenid,\n msgType: 0,\n content: parts.length === 1 ? officialPlainFallback(result) : content,\n ...(target.msgId ? { msgId: target.msgId, msgSeq: ++msgSeq } : {}),\n });\n }\n return sendC2cMessage(creds, { userOpenid, ...common });\n }\n if (plainFallback) {\n return sendGroupMessage(creds, {\n groupOpenid: target.groupId,\n msgType: 0,\n content: parts.length === 1 ? officialPlainFallback(result) : content,\n ...(target.msgId ? { msgId: target.msgId, msgSeq: ++msgSeq } : {}),\n });\n }\n return sendGroupMessage(creds, { groupOpenid: target.groupId, ...common });\n };\n\n const res = await sendOnce(false);\n if (!res.ok) {\n if (rendered.msgType === 2) {\n log.warn(`\u5B98\u65B9 Markdown \u53D1\u9001\u5931\u8D25\uFF0C\u964D\u7EA7\u6587\u672C: ${res.error}`);\n const plain = await sendOnce(true);\n if (!plain.ok) throw new Error(plain.error);\n continue;\n }\n throw new Error(res.error);\n }\n }\n },\n };\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["index"]
7
7
  }
@@ -1,48 +1,141 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import { log } from "../log.js";
3
+ import { formatCard } from "./menu-format.js";
4
+ import { authorizeAdmin } from "./permissions.js";
2
5
  import { normalizeTrigger } from "./registry.js";
3
6
  class CommandRouter {
4
- opts;
5
7
  constructor(opts) {
6
8
  this.opts = opts;
7
9
  }
8
- /** 供 menu/panel sync 读取权威命令表 */
10
+ opts;
11
+ confirmations = /* @__PURE__ */ new Map();
12
+ queues = /* @__PURE__ */ new Map();
9
13
  get registry() {
10
14
  return this.opts.registry;
11
15
  }
16
+ /** 同一人的请求串行处理,避免旧回复覆盖新菜单及重复确认。 */
12
17
  async handle(inbound) {
18
+ const key = JSON.stringify([inbound.backend, inbound.groupId, inbound.userId]);
19
+ const previous = this.queues.get(key) ?? Promise.resolve();
20
+ const task = previous.catch(() => void 0).then(() => this.dispatch(inbound, key));
21
+ this.queues.set(key, task);
22
+ try {
23
+ return await task;
24
+ } finally {
25
+ if (this.queues.get(key) === task) this.queues.delete(key);
26
+ }
27
+ }
28
+ async dispatch(inbound, key) {
13
29
  const raw = String(inbound.text ?? "").trim();
14
30
  if (!raw) return false;
15
31
  let effective = inbound;
16
- const digit = /^[1-9]$/.exec(raw);
17
- if (digit) {
18
- const chosen = this.opts.pending.take(inbound.backend, inbound.groupId, inbound.userId, Number(digit[0]));
19
- if (!chosen) return false;
20
- effective = { ...inbound, text: chosen };
32
+ if (/^\d+$/.test(raw) && inbound.backend === "llbot") {
33
+ const choice = this.opts.pending.choose(inbound.backend, inbound.groupId, inbound.userId, Number(raw));
34
+ if (choice.kind === "missing") return false;
35
+ if (choice.kind !== "chosen") {
36
+ await this.send(
37
+ inbound,
38
+ {
39
+ text: choice.kind === "expired" ? "\u83DC\u5355\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u53D1\u9001\u300C\u83DC\u5355\u300D\u91CD\u65B0\u6253\u5F00\u3002" : "\u6CA1\u6709\u8FD9\u4E2A\u7F16\u53F7\uFF0C\u8BF7\u9009\u62E9\u5F53\u524D\u83DC\u5355\u4E2D\u7684\u7F16\u53F7\u3002"
40
+ },
41
+ false
42
+ );
43
+ return true;
44
+ }
45
+ effective = { ...inbound, text: choice.command };
46
+ }
47
+ const text = effective.text.trim().replace(/^[//]+/, "");
48
+ const confirmationMatch = /^(confirm|确认|cancel|取消)(?:\s+(\S+))?$/i.exec(text);
49
+ const resolved = this.registry.resolve(text);
50
+ if (!confirmationMatch && !resolved) {
51
+ if (!/^[//]/.test(raw)) return false;
52
+ this.confirmations.delete(key);
53
+ await this.send(inbound, { text: "\u6CA1\u6709\u627E\u5230\u8FD9\u4E2A\u6307\u4EE4\u3002\u53D1\u9001\u300C\u5E2E\u52A9\u300D\u67E5\u770B\u53EF\u7528\u547D\u4EE4\uFF0C\u6216\u53D1\u9001\u300C\u83DC\u5355\u300D\u8FD4\u56DE\u9996\u9875\u3002" });
54
+ return true;
21
55
  }
22
- const resolved = this.opts.registry.resolve(effective.text);
23
- if (!resolved) return false;
24
56
  const ctx = {
25
57
  inbound: effective,
26
58
  startedAt: this.opts.startedAt,
27
59
  runtimeInfo: this.opts.runtimeInfo
28
60
  };
29
- const result = await resolved.handler(ctx);
30
- if (effective.backend === "llbot" && result.buttons && result.buttons.length > 0) {
31
- const choices = /* @__PURE__ */ new Map();
32
- result.buttons.forEach((b, i) => {
33
- choices.set(i + 1, b.command);
34
- });
35
- this.opts.pending.set(effective.backend, effective.groupId, effective.userId, choices);
61
+ try {
62
+ let result;
63
+ if (confirmationMatch) {
64
+ const pending = this.confirmations.get(key);
65
+ const token = confirmationMatch[2];
66
+ if (!pending || Date.now() >= pending.expiresAt || token && token !== pending.token) {
67
+ if (pending && Date.now() >= pending.expiresAt) this.confirmations.delete(key);
68
+ await this.send(inbound, { text: "\u6B64\u786E\u8BA4\u5DF2\u5931\u6548\u6216\u4E0D\u5C5E\u4E8E\u4F60\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77\u64CD\u4F5C\u3002" }, false);
69
+ return true;
70
+ }
71
+ this.confirmations.delete(key);
72
+ if (/^(cancel|取消)$/i.test(confirmationMatch[1])) {
73
+ result = { text: "\u5DF2\u53D6\u6D88\u64CD\u4F5C\u3002" };
74
+ } else {
75
+ ctx.adminAuthorized = pending.admin ? await authorizeAdmin(ctx) : false;
76
+ result = pending.admin && !ctx.adminAuthorized ? { text: "\u5F53\u524D\u6CA1\u6709\u7BA1\u7406\u6743\u9650\u6216\u6682\u65F6\u65E0\u6CD5\u6838\u9A8C\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458\u6216\u7A0D\u540E\u91CD\u65B0\u53D1\u8D77\u64CD\u4F5C\u3002" } : await pending.execute(ctx);
77
+ }
78
+ } else if (resolved) {
79
+ this.confirmations.delete(key);
80
+ ctx.inbound = { ...effective, text: text.replace(/^\S+/, resolved.name) };
81
+ ctx.adminAuthorized = resolved.permission === "admin" || resolved.name === "menu" || resolved.name === "help" ? await authorizeAdmin(ctx) : false;
82
+ result = resolved.permission === "admin" && !ctx.adminAuthorized ? { text: "\u5F53\u524D\u6CA1\u6709\u7BA1\u7406\u6743\u9650\u6216\u6682\u65F6\u65E0\u6CD5\u6838\u9A8C\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458\u6216\u7A0D\u540E\u91CD\u8BD5\u3002" } : await resolved.handler(ctx);
83
+ if (result.confirmation) {
84
+ for (const [oldKey, entry] of this.confirmations) {
85
+ if (entry.expiresAt <= Date.now()) this.confirmations.delete(oldKey);
86
+ }
87
+ const token = randomUUID();
88
+ this.confirmations.set(key, {
89
+ token,
90
+ expiresAt: Date.now() + 6e4,
91
+ execute: result.confirmation.execute,
92
+ admin: resolved.permission === "admin"
93
+ });
94
+ result = {
95
+ ...formatCard("\u64CD\u4F5C\u786E\u8BA4", [result.confirmation.summary, "", "\u8BF7\u672C\u4EBA\u5728 60 \u79D2\u5185\u786E\u8BA4\uFF0C\u6216\u53D6\u6D88\u64CD\u4F5C\u3002"]),
96
+ buttons: [
97
+ { id: "confirm", label: "\u786E\u8BA4\u64CD\u4F5C", command: `/confirm ${token}` },
98
+ { id: "cancel", label: "\u53D6\u6D88", command: `/cancel ${token}` }
99
+ ]
100
+ };
101
+ }
102
+ } else return false;
103
+ if (!await this.send(inbound, result)) this.confirmations.delete(key);
104
+ } catch (error) {
105
+ log.warn(`\u6307\u4EE4\u5904\u7406\u5931\u8D25: ${String(error)}`);
106
+ await this.send(inbound, { text: "\u64CD\u4F5C\u6682\u65F6\u672A\u5B8C\u6210\uFF0C\u8BF7\u7A0D\u540E\u67E5\u8BE2\u5F53\u524D\u72B6\u6001\uFF1B\u6D89\u53CA\u4FEE\u6539\u65F6\u8BF7\u52FF\u8FDE\u7EED\u91CD\u590D\u63D0\u4EA4\u3002" });
107
+ }
108
+ return true;
109
+ }
110
+ async send(inbound, result, replaceMenu = true) {
111
+ if (replaceMenu) {
112
+ const buttons = [...result.buttons ?? []];
113
+ if (result.menu !== "home" && !buttons.some((button) => button.command === "/menu"))
114
+ buttons.push({ id: "home", label: "\u8FD4\u56DE\u9996\u9875", command: "/menu" });
115
+ result = {
116
+ ...result,
117
+ buttons: buttons.map((button) => ({ ...button, permission: { type: 0, specify_user_ids: [inbound.userId] } }))
118
+ };
119
+ this.opts.pending.clear(inbound.backend, inbound.groupId, inbound.userId);
36
120
  }
37
- const target = { groupId: effective.groupId };
38
- if (effective.msgId) target.msgId = effective.msgId;
39
121
  try {
40
- await this.opts.reply.send(target, result, effective);
41
- log.info(`cmd=${resolved.name} handled user=${effective.userId} group=${effective.groupId}`);
42
- return true;
43
- } catch (e) {
44
- log.warn(`cmd=${resolved.name} \u56DE\u590D\u5931\u8D25: ${e.message}`);
122
+ await this.opts.reply.send(
123
+ { groupId: inbound.groupId, ...inbound.msgId ? { msgId: inbound.msgId } : {} },
124
+ result,
125
+ inbound
126
+ );
127
+ if (replaceMenu && inbound.backend === "llbot" && result.buttons?.length) {
128
+ this.opts.pending.set(
129
+ inbound.backend,
130
+ inbound.groupId,
131
+ inbound.userId,
132
+ new Map(result.buttons.map((button, index) => [index + 1, button.command]))
133
+ );
134
+ }
45
135
  return true;
136
+ } catch (error) {
137
+ log.warn(`\u6307\u4EE4\u56DE\u590D\u5931\u8D25: ${String(error)}`);
138
+ return false;
46
139
  }
47
140
  }
48
141
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/commands/router.ts"],
4
- "sourcesContent": ["/**\n * commands/router.ts \u2014 \u89E3\u6790\u89E6\u53D1\u8BCD / \u7F16\u53F7\u4F1A\u8BDD\uFF0C\u6267\u884C handler\n */\n\nimport { log } from \"../log.js\";\nimport type { PendingChoiceStore } from \"./pending.js\";\nimport { normalizeTrigger, type CommandRegistry } from \"./registry.js\";\nimport type {\n CommandContext,\n CommandResult,\n InboundMessage,\n ReplyPort,\n ReplyTarget,\n} from \"./types.js\";\n\nexport type CommandRouterOptions = {\n registry: CommandRegistry;\n pending: PendingChoiceStore;\n reply: ReplyPort;\n startedAt: number;\n runtimeInfo: CommandContext[\"runtimeInfo\"];\n};\n\n/**\n * \u82E5\u6587\u672C\u4E3A\u7EAF\u6570\u5B57 1\u20139\uFF0C\u5C1D\u8BD5 pending\uFF1B\u5426\u5219\u6309\u89E6\u53D1\u8BCD\u89E3\u6790\u3002\n * \u547D\u4E2D\u5E76\u56DE\u590D\u6210\u529F\u8FD4\u56DE true\uFF08\u8C03\u7528\u65B9\u5E94\u62E6\u622A\u8F6C\u53D1\uFF09\u3002\n */\nexport class CommandRouter {\n private readonly opts: CommandRouterOptions;\n\n constructor(opts: CommandRouterOptions) {\n this.opts = opts;\n }\n\n /** \u4F9B menu/panel sync \u8BFB\u53D6\u6743\u5A01\u547D\u4EE4\u8868 */\n get registry(): CommandRegistry {\n return this.opts.registry;\n }\n\n async handle(inbound: InboundMessage): Promise<boolean> {\n const raw = String(inbound.text ?? \"\").trim();\n if (!raw) return false;\n\n // \u7F16\u53F7\u4F1A\u8BDD\uFF1A\u7528\u7F13\u5B58\u7684\u5B8C\u6574\u547D\u4EE4\u66FF\u6362\u6B63\u6587\uFF08\u542B\u53C2\u6570\uFF0C\u5982\u300C\u914D\u7F6E \u767D\u540D\u5355 \u5173\u300D\uFF09\n let effective: InboundMessage = inbound;\n const digit = /^[1-9]$/.exec(raw);\n if (digit) {\n const chosen = this.opts.pending.take(inbound.backend, inbound.groupId, inbound.userId, Number(digit[0]));\n if (!chosen) return false;\n effective = { ...inbound, text: chosen };\n }\n\n const resolved = this.opts.registry.resolve(effective.text);\n if (!resolved) return false;\n\n const ctx: CommandContext = {\n inbound: effective,\n startedAt: this.opts.startedAt,\n runtimeInfo: this.opts.runtimeInfo,\n };\n const result: CommandResult = await resolved.handler(ctx);\n\n // llbot\uFF1A\u6709\u6309\u94AE\u65F6\u5199\u5165\u7F16\u53F7\u4F1A\u8BDD\n if (effective.backend === \"llbot\" && result.buttons && result.buttons.length > 0) {\n const choices = new Map<number, string>();\n result.buttons.forEach((b, i) => {\n choices.set(i + 1, b.command);\n });\n this.opts.pending.set(effective.backend, effective.groupId, effective.userId, choices);\n }\n\n const target: ReplyTarget = { groupId: effective.groupId };\n if (effective.msgId) target.msgId = effective.msgId;\n\n try {\n await this.opts.reply.send(target, result, effective);\n log.info(`cmd=${resolved.name} handled user=${effective.userId} group=${effective.groupId}`);\n return true;\n } catch (e) {\n log.warn(`cmd=${resolved.name} \u56DE\u590D\u5931\u8D25: ${(e as Error).message}`);\n return true; // \u4ECD\u89C6\u4E3A\u5DF2\u5904\u7406\uFF0C\u907F\u514D\u518D\u8F6C\u53BB MC\n }\n }\n}\n\nexport { normalizeTrigger };\n"],
5
- "mappings": "AAIA,SAAS,WAAW;AAEpB,SAAS,wBAA8C;AAqBhD,MAAM,cAAc;AAAA,EACR;AAAA,EAEjB,YAAY,MAA4B;AACtC,SAAK,OAAO;AAAA,EACd;AAAA;AAAA,EAGA,IAAI,WAA4B;AAC9B,WAAO,KAAK,KAAK;AAAA,EACnB;AAAA,EAEA,MAAM,OAAO,SAA2C;AACtD,UAAM,MAAM,OAAO,QAAQ,QAAQ,EAAE,EAAE,KAAK;AAC5C,QAAI,CAAC,IAAK,QAAO;AAGjB,QAAI,YAA4B;AAChC,UAAM,QAAQ,UAAU,KAAK,GAAG;AAChC,QAAI,OAAO;AACT,YAAM,SAAS,KAAK,KAAK,QAAQ,KAAK,QAAQ,SAAS,QAAQ,SAAS,QAAQ,QAAQ,OAAO,MAAM,CAAC,CAAC,CAAC;AACxG,UAAI,CAAC,OAAQ,QAAO;AACpB,kBAAY,EAAE,GAAG,SAAS,MAAM,OAAO;AAAA,IACzC;AAEA,UAAM,WAAW,KAAK,KAAK,SAAS,QAAQ,UAAU,IAAI;AAC1D,QAAI,CAAC,SAAU,QAAO;AAEtB,UAAM,MAAsB;AAAA,MAC1B,SAAS;AAAA,MACT,WAAW,KAAK,KAAK;AAAA,MACrB,aAAa,KAAK,KAAK;AAAA,IACzB;AACA,UAAM,SAAwB,MAAM,SAAS,QAAQ,GAAG;AAGxD,QAAI,UAAU,YAAY,WAAW,OAAO,WAAW,OAAO,QAAQ,SAAS,GAAG;AAChF,YAAM,UAAU,oBAAI,IAAoB;AACxC,aAAO,QAAQ,QAAQ,CAAC,GAAG,MAAM;AAC/B,gBAAQ,IAAI,IAAI,GAAG,EAAE,OAAO;AAAA,MAC9B,CAAC;AACD,WAAK,KAAK,QAAQ,IAAI,UAAU,SAAS,UAAU,SAAS,UAAU,QAAQ,OAAO;AAAA,IACvF;AAEA,UAAM,SAAsB,EAAE,SAAS,UAAU,QAAQ;AACzD,QAAI,UAAU,MAAO,QAAO,QAAQ,UAAU;AAE9C,QAAI;AACF,YAAM,KAAK,KAAK,MAAM,KAAK,QAAQ,QAAQ,SAAS;AACpD,UAAI,KAAK,OAAO,SAAS,IAAI,iBAAiB,UAAU,MAAM,UAAU,UAAU,OAAO,EAAE;AAC3F,aAAO;AAAA,IACT,SAAS,GAAG;AACV,UAAI,KAAK,OAAO,SAAS,IAAI,8BAAW,EAAY,OAAO,EAAE;AAC7D,aAAO;AAAA,IACT;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import { randomUUID } from \"node:crypto\";\nimport { log } from \"../log.js\";\nimport { formatCard } from \"./menu-format.js\";\nimport type { PendingChoiceStore } from \"./pending.js\";\nimport { authorizeAdmin } from \"./permissions.js\";\nimport { normalizeTrigger, type CommandRegistry } from \"./registry.js\";\nimport type { CommandContext, CommandResult, InboundMessage, ReplyPort } from \"./types.js\";\n\nexport type CommandRouterOptions = {\n registry: CommandRegistry;\n pending: PendingChoiceStore;\n reply: ReplyPort;\n startedAt: number;\n runtimeInfo: CommandContext[\"runtimeInfo\"];\n};\ntype Confirmation = {\n token: string;\n expiresAt: number;\n execute: NonNullable<CommandResult[\"confirmation\"]>[\"execute\"];\n admin: boolean;\n};\nexport class CommandRouter {\n private readonly confirmations = new Map<string, Confirmation>();\n private readonly queues = new Map<string, Promise<unknown>>();\n constructor(private readonly opts: CommandRouterOptions) {}\n get registry(): CommandRegistry {\n return this.opts.registry;\n }\n\n /** \u540C\u4E00\u4EBA\u7684\u8BF7\u6C42\u4E32\u884C\u5904\u7406\uFF0C\u907F\u514D\u65E7\u56DE\u590D\u8986\u76D6\u65B0\u83DC\u5355\u53CA\u91CD\u590D\u786E\u8BA4\u3002 */\n async handle(inbound: InboundMessage): Promise<boolean> {\n const key = JSON.stringify([inbound.backend, inbound.groupId, inbound.userId]);\n const previous = this.queues.get(key) ?? Promise.resolve();\n const task = previous.catch(() => undefined).then(() => this.dispatch(inbound, key));\n this.queues.set(key, task);\n try {\n return await task;\n } finally {\n if (this.queues.get(key) === task) this.queues.delete(key);\n }\n }\n private async dispatch(inbound: InboundMessage, key: string): Promise<boolean> {\n const raw = String(inbound.text ?? \"\").trim();\n if (!raw) return false;\n let effective = inbound;\n if (/^\\d+$/.test(raw) && inbound.backend === \"llbot\") {\n const choice = this.opts.pending.choose(inbound.backend, inbound.groupId, inbound.userId, Number(raw));\n if (choice.kind === \"missing\") return false;\n if (choice.kind !== \"chosen\") {\n await this.send(\n inbound,\n {\n text:\n choice.kind === \"expired\"\n ? \"\u83DC\u5355\u5DF2\u8FC7\u671F\uFF0C\u8BF7\u53D1\u9001\u300C\u83DC\u5355\u300D\u91CD\u65B0\u6253\u5F00\u3002\"\n : \"\u6CA1\u6709\u8FD9\u4E2A\u7F16\u53F7\uFF0C\u8BF7\u9009\u62E9\u5F53\u524D\u83DC\u5355\u4E2D\u7684\u7F16\u53F7\u3002\",\n },\n false\n );\n return true;\n }\n effective = { ...inbound, text: choice.command };\n }\n const text = effective.text.trim().replace(/^[/\uFF0F]+/, \"\");\n const confirmationMatch = /^(confirm|\u786E\u8BA4|cancel|\u53D6\u6D88)(?:\\s+(\\S+))?$/i.exec(text);\n const resolved = this.registry.resolve(text);\n if (!confirmationMatch && !resolved) {\n if (!/^[/\uFF0F]/.test(raw)) return false;\n this.confirmations.delete(key);\n await this.send(inbound, { text: \"\u6CA1\u6709\u627E\u5230\u8FD9\u4E2A\u6307\u4EE4\u3002\u53D1\u9001\u300C\u5E2E\u52A9\u300D\u67E5\u770B\u53EF\u7528\u547D\u4EE4\uFF0C\u6216\u53D1\u9001\u300C\u83DC\u5355\u300D\u8FD4\u56DE\u9996\u9875\u3002\" });\n return true;\n }\n const ctx: CommandContext = {\n inbound: effective,\n startedAt: this.opts.startedAt,\n runtimeInfo: this.opts.runtimeInfo,\n };\n try {\n let result: CommandResult;\n if (confirmationMatch) {\n const pending = this.confirmations.get(key);\n const token = confirmationMatch[2];\n if (!pending || Date.now() >= pending.expiresAt || (token && token !== pending.token)) {\n if (pending && Date.now() >= pending.expiresAt) this.confirmations.delete(key);\n await this.send(inbound, { text: \"\u6B64\u786E\u8BA4\u5DF2\u5931\u6548\u6216\u4E0D\u5C5E\u4E8E\u4F60\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77\u64CD\u4F5C\u3002\" }, false);\n return true;\n }\n this.confirmations.delete(key);\n if (/^(cancel|\u53D6\u6D88)$/i.test(confirmationMatch[1]!)) {\n result = { text: \"\u5DF2\u53D6\u6D88\u64CD\u4F5C\u3002\" };\n } else {\n ctx.adminAuthorized = pending.admin ? await authorizeAdmin(ctx) : false;\n result =\n pending.admin && !ctx.adminAuthorized\n ? { text: \"\u5F53\u524D\u6CA1\u6709\u7BA1\u7406\u6743\u9650\u6216\u6682\u65F6\u65E0\u6CD5\u6838\u9A8C\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458\u6216\u7A0D\u540E\u91CD\u65B0\u53D1\u8D77\u64CD\u4F5C\u3002\" }\n : await pending.execute(ctx);\n }\n } else if (resolved) {\n this.confirmations.delete(key);\n // \u4EC5\u89C4\u8303\u5316\u9996\u8BCD\uFF0C\u4FDD\u7559\u73A9\u5BB6\u540D\u7B49\u53C2\u6570\u7684\u5927\u5C0F\u5199\u3002\n ctx.inbound = { ...effective, text: text.replace(/^\\S+/, resolved.name) };\n ctx.adminAuthorized =\n resolved.permission === \"admin\" || resolved.name === \"menu\" || resolved.name === \"help\"\n ? await authorizeAdmin(ctx)\n : false;\n result =\n resolved.permission === \"admin\" && !ctx.adminAuthorized\n ? { text: \"\u5F53\u524D\u6CA1\u6709\u7BA1\u7406\u6743\u9650\u6216\u6682\u65F6\u65E0\u6CD5\u6838\u9A8C\uFF0C\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458\u6216\u7A0D\u540E\u91CD\u8BD5\u3002\" }\n : await resolved.handler(ctx);\n if (result.confirmation) {\n for (const [oldKey, entry] of this.confirmations) {\n if (entry.expiresAt <= Date.now()) this.confirmations.delete(oldKey);\n }\n const token = randomUUID();\n this.confirmations.set(key, {\n token,\n expiresAt: Date.now() + 60_000,\n execute: result.confirmation.execute,\n admin: resolved.permission === \"admin\",\n });\n result = {\n ...formatCard(\"\u64CD\u4F5C\u786E\u8BA4\", [result.confirmation.summary, \"\", \"\u8BF7\u672C\u4EBA\u5728 60 \u79D2\u5185\u786E\u8BA4\uFF0C\u6216\u53D6\u6D88\u64CD\u4F5C\u3002\"]),\n buttons: [\n { id: \"confirm\", label: \"\u786E\u8BA4\u64CD\u4F5C\", command: `/confirm ${token}` },\n { id: \"cancel\", label: \"\u53D6\u6D88\", command: `/cancel ${token}` },\n ],\n };\n }\n } else return false;\n if (!(await this.send(inbound, result))) this.confirmations.delete(key);\n } catch (error) {\n log.warn(`\u6307\u4EE4\u5904\u7406\u5931\u8D25: ${String(error)}`);\n await this.send(inbound, { text: \"\u64CD\u4F5C\u6682\u65F6\u672A\u5B8C\u6210\uFF0C\u8BF7\u7A0D\u540E\u67E5\u8BE2\u5F53\u524D\u72B6\u6001\uFF1B\u6D89\u53CA\u4FEE\u6539\u65F6\u8BF7\u52FF\u8FDE\u7EED\u91CD\u590D\u63D0\u4EA4\u3002\" });\n }\n return true;\n }\n private async send(inbound: InboundMessage, result: CommandResult, replaceMenu = true): Promise<boolean> {\n if (replaceMenu) {\n const buttons = [...(result.buttons ?? [])];\n if (result.menu !== \"home\" && !buttons.some((button) => button.command === \"/menu\"))\n buttons.push({ id: \"home\", label: \"\u8FD4\u56DE\u9996\u9875\", command: \"/menu\" });\n result = {\n ...result,\n buttons: buttons.map((button) => ({ ...button, permission: { type: 0, specify_user_ids: [inbound.userId] } })),\n };\n this.opts.pending.clear(inbound.backend, inbound.groupId, inbound.userId);\n }\n try {\n await this.opts.reply.send(\n { groupId: inbound.groupId, ...(inbound.msgId ? { msgId: inbound.msgId } : {}) },\n result,\n inbound\n );\n if (replaceMenu && inbound.backend === \"llbot\" && result.buttons?.length) {\n this.opts.pending.set(\n inbound.backend,\n inbound.groupId,\n inbound.userId,\n new Map(result.buttons.map((button, index) => [index + 1, button.command]))\n );\n }\n return true;\n } catch (error) {\n log.warn(`\u6307\u4EE4\u56DE\u590D\u5931\u8D25: ${String(error)}`);\n return false;\n }\n }\n}\nexport { normalizeTrigger };\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/dist/config.js CHANGED
@@ -1,9 +1,4 @@
1
- import {
2
- configPath,
3
- DEFAULT_QQ_CONFIG,
4
- loadEnsuredConfig,
5
- stripConfigMeta
6
- } from "@sfmc-bds/sdk/node/config";
1
+ import { configPath, DEFAULT_QQ_CONFIG, loadEnsuredConfig, stripConfigMeta } from "@sfmc-bds/sdk/node/config";
7
2
  import { log } from "./log.js";
8
3
  import { PROJECT_ROOT } from "./project-root.js";
9
4
  const ROOT_DIR = PROJECT_ROOT;
@@ -13,32 +8,45 @@ function applyDefaults(raw) {
13
8
  const stripped = stripConfigMeta(merged);
14
9
  const backendRaw = String(stripped.qq_backend ?? DEFAULT_QQ_CONFIG.qq_backend ?? "official");
15
10
  const qq_backend = backendRaw === "llbot" ? "llbot" : "official";
11
+ const official = raw.official ?? {};
12
+ const officialDefaults = DEFAULT_QQ_CONFIG.official ?? {};
13
+ const llbot = raw.llbot ?? {};
14
+ const llbotDefaults = DEFAULT_QQ_CONFIG.llbot ?? {};
15
+ const publicRaw = raw.public_server;
16
+ const publicServer = publicRaw && typeof publicRaw === "object" ? publicRaw : {};
16
17
  return {
17
- ...stripped,
18
- qq_enabled: stripped.qq_enabled !== false,
18
+ public_server: {
19
+ address: typeof publicServer.address === "string" ? publicServer.address.trim().slice(0, 253) : "",
20
+ version: typeof publicServer.version === "string" ? publicServer.version.trim().slice(0, 120) : "",
21
+ port: Number.isInteger(publicServer.port) && publicServer.port > 0 && publicServer.port <= 65535 ? publicServer.port : 19132
22
+ },
23
+ qq_enabled: raw.qq_enabled !== false,
19
24
  qq_backend,
20
- qq_app_id: String(stripped.qq_app_id ?? DEFAULT_QQ_CONFIG.qq_app_id ?? ""),
21
- qq_app_secret: String(stripped.qq_app_secret ?? DEFAULT_QQ_CONFIG.qq_app_secret ?? ""),
22
- qq_sandbox: stripped.qq_sandbox === true,
23
- qq_group_openid: String(stripped.qq_group_openid ?? DEFAULT_QQ_CONFIG.qq_group_openid ?? ""),
24
- qq_group_panel_id: String(stripped.qq_group_panel_id ?? DEFAULT_QQ_CONFIG.qq_group_panel_id ?? ""),
25
- qq_sync_menu_panel: stripped.qq_sync_menu_panel !== false,
26
- qq_admin_openids: Array.isArray(stripped.qq_admin_openids) ? stripped.qq_admin_openids.map((x) => String(x).trim()).filter(Boolean) : [],
27
- qq_ws_port: parseInt(String(stripped.qq_ws_port ?? DEFAULT_QQ_CONFIG.qq_ws_port ?? 3002), 10),
28
- qq_group_id: String(stripped.qq_group_id ?? DEFAULT_QQ_CONFIG.qq_group_id ?? "0"),
29
- bridge_channel_id: String(stripped.bridge_channel_id ?? DEFAULT_QQ_CONFIG.bridge_channel_id ?? ""),
30
- db_host: String(stripped.db_host ?? "127.0.0.1"),
31
- db_port: parseInt(String(stripped.db_port ?? "3001"), 10),
32
- mctoqq_prefix: String(stripped.mctoqq_prefix ?? DEFAULT_QQ_CONFIG.mctoqq_prefix ?? "[MC]")
25
+ qq_official_transport: official.transport === "webhook" ? "webhook" : "websocket",
26
+ qq_webhook_port: Number(official.webhook?.port ?? officialDefaults.webhook?.port ?? 3005),
27
+ qq_webhook_path: String(official.webhook?.path ?? officialDefaults.webhook?.path ?? "/qqbot/webhook"),
28
+ qq_app_id: String(official.app_id ?? officialDefaults.app_id ?? ""),
29
+ qq_app_secret: String(official.app_secret ?? officialDefaults.app_secret ?? ""),
30
+ qq_sandbox: official.sandbox === true,
31
+ qq_group_openid: String(official.group_openid ?? officialDefaults.group_openid ?? ""),
32
+ qq_group_panel_id: String(official.group_panel_id ?? officialDefaults.group_panel_id ?? ""),
33
+ qq_sync_menu_panel: official.sync_menu_panel !== false,
34
+ qq_admin_openids: Array.isArray(official.admin_openids) ? official.admin_openids.map((x) => String(x).trim()).filter(Boolean) : [],
35
+ qq_ws_port: Number(llbot.ws_port ?? llbotDefaults.ws_port ?? 3002),
36
+ qq_group_id: String(llbot.group_id ?? llbotDefaults.group_id ?? "0"),
37
+ llbot_enabled: llbot.enabled ?? llbotDefaults.enabled ?? false,
38
+ llbot_path: llbot.path ?? llbotDefaults.path ?? "",
39
+ llbot_cwd: llbot.cwd ?? llbotDefaults.cwd ?? "",
40
+ llbot_host: llbot.host ?? llbotDefaults.host ?? "127.0.0.1",
41
+ llbot_port: llbot.port ?? llbotDefaults.port ?? 3004,
42
+ llbot_token: llbot.token ?? llbotDefaults.token ?? "",
43
+ llbot_http: llbot.http ?? llbotDefaults.http ?? "",
44
+ db_host: String(raw.db_host ?? "127.0.0.1"),
45
+ db_port: Number(raw.db_port ?? 3001)
33
46
  };
34
47
  }
35
48
  function readFromDisk() {
36
- const raw = loadEnsuredConfig(
37
- ROOT_DIR,
38
- "qq_config.json",
39
- "qq_config",
40
- { ...DEFAULT_QQ_CONFIG }
41
- );
49
+ const raw = loadEnsuredConfig(ROOT_DIR, "qq_config.json", "qq_config", { ...DEFAULT_QQ_CONFIG });
42
50
  return applyDefaults(raw);
43
51
  }
44
52
  function loadInitialConfig() {