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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +1 -1
  2. package/dist/domain/bridge.d.ts +1 -5
  3. package/dist/domain/bridge.d.ts.map +1 -1
  4. package/dist/domain/bridge.js +15 -14
  5. package/dist/domain/bridge.js.map +2 -2
  6. package/dist/domain/installed-content.d.ts +84 -0
  7. package/dist/domain/installed-content.d.ts.map +1 -0
  8. package/dist/domain/installed-content.js +149 -0
  9. package/dist/domain/installed-content.js.map +7 -0
  10. package/dist/domain/player-session.d.ts +8 -0
  11. package/dist/domain/player-session.d.ts.map +1 -0
  12. package/dist/domain/player-session.js +16 -0
  13. package/dist/domain/player-session.js.map +7 -0
  14. package/dist/domain/qq-account-profile.d.ts +51 -0
  15. package/dist/domain/qq-account-profile.d.ts.map +1 -0
  16. package/dist/domain/qq-account-profile.js +129 -0
  17. package/dist/domain/qq-account-profile.js.map +7 -0
  18. package/dist/domain/qq-events.d.ts +2 -2
  19. package/dist/domain/qq-events.d.ts.map +1 -1
  20. package/dist/domain/qq-events.js +6 -3
  21. package/dist/domain/qq-events.js.map +2 -2
  22. package/dist/domain/schema.d.ts.map +1 -1
  23. package/dist/domain/schema.js +48 -0
  24. package/dist/domain/schema.js.map +2 -2
  25. package/dist/domain/system-status.d.ts +3 -0
  26. package/dist/domain/system-status.d.ts.map +1 -1
  27. package/dist/domain/system-status.js +27 -12
  28. package/dist/domain/system-status.js.map +2 -2
  29. package/dist/env.d.ts +0 -2
  30. package/dist/env.d.ts.map +1 -1
  31. package/dist/env.js +12 -24
  32. package/dist/env.js.map +2 -2
  33. package/dist/index.d.ts +1 -1
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +108 -52
  36. package/dist/index.js.map +3 -3
  37. package/dist/routes/config.d.ts +1 -1
  38. package/dist/routes/config.d.ts.map +1 -1
  39. package/dist/routes/config.js +0 -4
  40. package/dist/routes/config.js.map +2 -2
  41. package/dist/routes/content.d.ts +23 -0
  42. package/dist/routes/content.d.ts.map +1 -0
  43. package/dist/routes/content.js +19 -0
  44. package/dist/routes/content.js.map +7 -0
  45. package/dist/routes/db-routes.d.ts +2 -2
  46. package/dist/routes/db-routes.d.ts.map +1 -1
  47. package/dist/routes/db-routes.js +3 -12
  48. package/dist/routes/db-routes.js.map +2 -2
  49. package/dist/routes/messages.d.ts +14 -6
  50. package/dist/routes/messages.d.ts.map +1 -1
  51. package/dist/routes/messages.js +22 -8
  52. package/dist/routes/messages.js.map +2 -2
  53. package/dist/routes/qq-bind.d.ts.map +1 -1
  54. package/dist/routes/qq-bind.js +29 -1
  55. package/dist/routes/qq-bind.js.map +2 -2
  56. package/dist/routes/qq-events.d.ts +6 -2
  57. package/dist/routes/qq-events.d.ts.map +1 -1
  58. package/dist/routes/qq-events.js +35 -1
  59. package/dist/routes/qq-events.js.map +3 -3
  60. package/dist/routes/qq-join.js +1 -1
  61. package/dist/routes/qq-join.js.map +2 -2
  62. package/dist/routes/status.d.ts +3 -11
  63. package/dist/routes/status.d.ts.map +1 -1
  64. package/dist/routes/status.js +39 -49
  65. package/dist/routes/status.js.map +2 -2
  66. package/dist/tx-runner.d.ts +20 -7
  67. package/dist/tx-runner.d.ts.map +1 -1
  68. package/dist/tx-runner.js +71 -25
  69. package/dist/tx-runner.js.map +2 -2
  70. package/package.json +4 -5
  71. package/dist/domain/bridge.test.d.ts +0 -2
  72. package/dist/domain/bridge.test.d.ts.map +0 -1
  73. package/dist/domain/bridge.test.js +0 -87
  74. package/dist/domain/bridge.test.js.map +0 -7
  75. package/dist/domain/messages-bridge-filter.test.d.ts +0 -5
  76. package/dist/domain/messages-bridge-filter.test.d.ts.map +0 -1
  77. package/dist/domain/messages-bridge-filter.test.js +0 -20
  78. package/dist/domain/messages-bridge-filter.test.js.map +0 -7
  79. package/dist/domain/qq-bind-status.test.d.ts +0 -2
  80. package/dist/domain/qq-bind-status.test.d.ts.map +0 -1
  81. package/dist/domain/qq-bind-status.test.js +0 -211
  82. package/dist/domain/qq-bind-status.test.js.map +0 -7
  83. package/dist/domain/qq-events.test.d.ts +0 -5
  84. package/dist/domain/qq-events.test.d.ts.map +0 -1
  85. package/dist/domain/qq-events.test.js +0 -170
  86. package/dist/domain/qq-events.test.js.map +0 -7
  87. package/dist/domain/qq-join.test.d.ts +0 -2
  88. package/dist/domain/qq-join.test.d.ts.map +0 -1
  89. package/dist/domain/qq-join.test.js +0 -353
  90. package/dist/domain/qq-join.test.js.map +0 -7
  91. package/dist/domain/system-status.test.d.ts +0 -5
  92. package/dist/domain/system-status.test.d.ts.map +0 -1
  93. package/dist/domain/system-status.test.js +0 -19
  94. package/dist/domain/system-status.test.js.map +0 -7
  95. package/dist/runtime.test.d.ts +0 -2
  96. package/dist/runtime.test.d.ts.map +0 -1
  97. package/dist/runtime.test.js +0 -265
  98. package/dist/runtime.test.js.map +0 -7
package/README.md CHANGED
@@ -5,7 +5,7 @@ SFMC SQLite HTTP REST 后端:配置快照、模块启停、游戏数据路由
5
5
  ## 安装
6
6
 
7
7
  ```bash
8
- npm install @sfmc-bds/db-server
8
+ pnpm add @sfmc-bds/db-server
9
9
  node node_modules/@sfmc-bds/db-server/dist/index.js
10
10
  ```
11
11
 
@@ -17,13 +17,11 @@ type LLBotConfig = {
17
17
  port: number;
18
18
  token: string;
19
19
  groupId: string;
20
- prefix: string;
21
20
  };
22
21
  export type OfficialOutboundConfig = {
23
22
  backend: "official";
24
23
  creds: QqOfficialCredentials;
25
24
  groupOpenid: string;
26
- prefix: string;
27
25
  };
28
26
  export type LlbotOutboundConfig = {
29
27
  backend: "llbot";
@@ -41,7 +39,6 @@ export declare function makeLLBotConfig(env: {
41
39
  LLBOT_PORT: number;
42
40
  LLBOT_TOKEN: string;
43
41
  QQ_GROUP_ID: string;
44
- MCTOQQ_PREFIX?: string;
45
42
  }): LLBotConfig;
46
43
  /**
47
44
  * 根据环境变量与配置组装出站配置(支持 official 官方开放平台与 llbot 双后端)。
@@ -59,7 +56,6 @@ export declare function makeOutboundConfig(env: {
59
56
  QQ_APP_SECRET: string;
60
57
  QQ_SANDBOX: boolean;
61
58
  QQ_GROUP_OPENID: string;
62
- MCTOQQ_PREFIX: string;
63
59
  }): OutboundConfig;
64
60
  /**
65
61
  * 将文本内容直接发送至目标 QQ 群(供服务器事件通知使用,不附加玩家聊天前缀)。
@@ -77,6 +73,6 @@ export declare function sendGroupOutbound(config: OutboundConfig | LLBotConfig,
77
73
  * @param content 聊天消息正文。
78
74
  * @param fromId 发送者唯一标识(如 XUID)。
79
75
  */
80
- export declare function forwardToQQBridge(config: OutboundConfig | LLBotConfig, channelId: string, fromName: string, content: string, fromId: string): void;
76
+ export declare function forwardToQQBridge(config: OutboundConfig | LLBotConfig, channelId: string, prefix: string, fromName: string, content: string, fromId: string): void;
81
77
  export {};
82
78
  //# sourceMappingURL=bridge.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../../src/domain/bridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,EAEL,KAAK,qBAAqB,EAC3B,MAAM,gCAAgC,CAAC;AACxC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAI3D,KAAK,WAAW,GAAG;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,OAAO,EAAE,UAAU,CAAC;IACpB,KAAK,EAAE,qBAAqB,CAAC;IAC7B,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,WAAW,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG,sBAAsB,GAAG,mBAAmB,CAAC;AAE1E;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,GAAG,WAAW,CAQd;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE;IACtC,UAAU,EAAE,SAAS,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,OAAO,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,aAAa,EAAE,MAAM,CAAC;CACvB,GAAG,cAAc,CAkBjB;AAoED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,cAAc,GAAG,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAW1F;AAwBD;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,cAAc,GAAG,WAAW,EACpC,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,GACb,IAAI,CAYN"}
1
+ {"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../../src/domain/bridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,EAEL,KAAK,qBAAqB,EAC3B,MAAM,gCAAgC,CAAC;AACxC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAI3D,KAAK,WAAW,GAAG;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,OAAO,EAAE,UAAU,CAAC;IACpB,KAAK,EAAE,qBAAqB,CAAC;IAC7B,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,WAAW,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG,sBAAsB,GAAG,mBAAmB,CAAC;AAE1E;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;CACrB,GAAG,WAAW,CAOd;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE;IACtC,UAAU,EAAE,SAAS,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,OAAO,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;CACzB,GAAG,cAAc,CAiBjB;AAuED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,cAAc,GAAG,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAW1F;AA0BD;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,cAAc,GAAG,WAAW,EACpC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,GACb,IAAI,CAYN"}
@@ -8,8 +8,7 @@ function makeLLBotConfig(env) {
8
8
  host: env.LLBOT_HOST,
9
9
  port: env.LLBOT_PORT,
10
10
  token: env.LLBOT_TOKEN,
11
- groupId: env.QQ_GROUP_ID,
12
- prefix: env.MCTOQQ_PREFIX ?? "[MC]"
11
+ groupId: env.QQ_GROUP_ID
13
12
  };
14
13
  }
15
14
  function makeOutboundConfig(env) {
@@ -26,13 +25,12 @@ function makeOutboundConfig(env) {
26
25
  appSecret: env.QQ_APP_SECRET,
27
26
  sandbox: env.QQ_SANDBOX
28
27
  },
29
- groupOpenid: env.QQ_GROUP_OPENID,
30
- prefix: env.MCTOQQ_PREFIX || "[MC]"
28
+ groupOpenid: env.QQ_GROUP_OPENID
31
29
  };
32
30
  }
33
31
  function sendViaLlbot(config, text, logCtx) {
34
32
  if (!config.groupId || config.groupId === "0") {
35
- log.warn(`QQ \u7FA4\u672A\u914D\u7F6E (qq_group_id),\u8DF3\u8FC7 MC\u2192QQ (${logCtx})`);
33
+ log.warn(`QQ \u7FA4\u672A\u914D\u7F6E (llbot.group_id),\u8DF3\u8FC7 MC\u2192QQ (${logCtx})`);
36
34
  return;
37
35
  }
38
36
  const payload = JSON.stringify({
@@ -74,7 +72,7 @@ function sendViaLlbot(config, text, logCtx) {
74
72
  }
75
73
  function sendViaOfficial(config, text, logCtx) {
76
74
  if (!config.groupOpenid) {
77
- log.warn(`\u5B98\u65B9\u7FA4 openid \u672A\u914D\u7F6E (qq_group_openid),\u8DF3\u8FC7 MC\u2192QQ (${logCtx})`);
75
+ log.warn(`\u5B98\u65B9\u7FA4 openid \u672A\u914D\u7F6E (official.group_openid),\u8DF3\u8FC7 MC\u2192QQ (${logCtx})`);
78
76
  return;
79
77
  }
80
78
  if (!config.creds.appId || !config.creds.appSecret) {
@@ -86,6 +84,9 @@ function sendViaOfficial(config, text, logCtx) {
86
84
  content: text
87
85
  }).then((result) => {
88
86
  if (!result.ok) {
87
+ if (result.status === 400 && String(result.error).includes("40034105")) {
88
+ 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");
89
+ }
89
90
  log.warn(`\u5B98\u65B9\u53D1\u7FA4\u5931\u8D25: ${result.error} (${logCtx}, status=${result.status})`);
90
91
  }
91
92
  });
@@ -102,24 +103,24 @@ function sendGroupOutbound(config, text) {
102
103
  }
103
104
  sendViaOfficial(config, text, logCtx);
104
105
  }
105
- function forwardViaLlbot(config, channelId, fromName, content, fromId) {
106
- const text = `${config.prefix} ${fromName}: ${content}`;
106
+ function forwardViaLlbot(config, channelId, prefix, fromName, content, fromId) {
107
+ const text = `[${prefix}] ${fromName}: ${content}`;
107
108
  sendViaLlbot(config, text, `from=${fromId}, channel=${channelId}`);
108
109
  }
109
- function forwardViaOfficial(config, channelId, fromName, content, fromId) {
110
- const text = `${config.prefix} ${fromName}: ${content}`;
110
+ function forwardViaOfficial(config, channelId, prefix, fromName, content, fromId) {
111
+ const text = `[${prefix}] ${fromName}: ${content}`;
111
112
  sendViaOfficial(config, text, `from=${fromId}, channel=${channelId}`);
112
113
  }
113
- function forwardToQQBridge(config, channelId, fromName, content, fromId) {
114
+ function forwardToQQBridge(config, channelId, prefix, fromName, content, fromId) {
114
115
  if (!("backend" in config)) {
115
- forwardViaLlbot(config, channelId, fromName, content, fromId);
116
+ forwardViaLlbot(config, channelId, prefix, fromName, content, fromId);
116
117
  return;
117
118
  }
118
119
  if (config.backend === "llbot") {
119
- forwardViaLlbot(config.llbot, channelId, fromName, content, fromId);
120
+ forwardViaLlbot(config.llbot, channelId, prefix, fromName, content, fromId);
120
121
  return;
121
122
  }
122
- forwardViaOfficial(config, channelId, fromName, content, fromId);
123
+ forwardViaOfficial(config, channelId, prefix, fromName, content, fromId);
123
124
  }
124
125
  export {
125
126
  forwardToQQBridge,
@@ -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 prefix: string;\n};\n\nexport type OfficialOutboundConfig = {\n backend: \"official\";\n creds: QqOfficialCredentials;\n groupOpenid: string;\n prefix: 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 MCTOQQ_PREFIX?: 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 prefix: env.MCTOQQ_PREFIX ?? \"[MC]\",\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 MCTOQQ_PREFIX: 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 prefix: env.MCTOQQ_PREFIX || \"[MC]\",\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 (qq_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 (qq_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 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 fromName: string,\n content: string,\n fromId: string\n): void {\n const text = `${config.prefix} ${fromName}: ${content}`;\n sendViaLlbot(config, text, `from=${fromId}, channel=${channelId}`);\n}\n\nfunction forwardViaOfficial(\n config: OfficialOutboundConfig,\n channelId: string,\n fromName: string,\n content: string,\n fromId: string\n): void {\n const text = `${config.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 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, fromName, content, fromId);\n return;\n }\n if (config.backend === \"llbot\") {\n forwardViaLlbot(config.llbot, channelId, fromName, content, fromId);\n return;\n }\n forwardViaOfficial(config, channelId, fromName, content, fromId);\n}\n"],
5
- "mappings": "AAaA,SAAS,eAAe;AACxB;AAAA,EACE;AAAA,OAEK;AAGP,SAAS,WAAW;AA8Bb,SAAS,gBAAgB,KAMhB;AACd,SAAO;AAAA,IACL,MAAM,IAAI;AAAA,IACV,MAAM,IAAI;AAAA,IACV,OAAO,IAAI;AAAA,IACX,SAAS,IAAI;AAAA,IACb,QAAQ,IAAI,iBAAiB;AAAA,EAC/B;AACF;AAQO,SAAS,mBAAmB,KAWhB;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,IACjB,QAAQ,IAAI,iBAAiB;AAAA,EAC/B;AACF;AAEA,SAAS,aAAa,QAAqB,MAAc,QAAsB;AAC7E,MAAI,CAAC,OAAO,WAAW,OAAO,YAAY,KAAK;AAC7C,QAAI,KAAK,sEAAmC,MAAM,GAAG;AACrD;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,2FAA8C,MAAM,GAAG;AAChE;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,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,UACA,SACA,QACM;AACN,QAAM,OAAO,GAAG,OAAO,MAAM,IAAI,QAAQ,KAAK,OAAO;AACrD,eAAa,QAAQ,MAAM,QAAQ,MAAM,aAAa,SAAS,EAAE;AACnE;AAEA,SAAS,mBACP,QACA,WACA,UACA,SACA,QACM;AACN,QAAM,OAAO,GAAG,OAAO,MAAM,IAAI,QAAQ,KAAK,OAAO;AACrD,kBAAgB,QAAQ,MAAM,QAAQ,MAAM,aAAa,SAAS,EAAE;AACtE;AAWO,SAAS,kBACd,QACA,WACA,UACA,SACA,QACM;AAGN,MAAI,EAAE,aAAa,SAAS;AAC1B,oBAAgB,QAAQ,WAAW,UAAU,SAAS,MAAM;AAC5D;AAAA,EACF;AACA,MAAI,OAAO,YAAY,SAAS;AAC9B,oBAAgB,OAAO,OAAO,WAAW,UAAU,SAAS,MAAM;AAClE;AAAA,EACF;AACA,qBAAmB,QAAQ,WAAW,UAAU,SAAS,MAAM;AACjE;",
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"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * domain/installed-content.ts — 只读汇总已装模块与世界行为包、资源包。
3
+ *
4
+ * 供 QQ「模块 / 世界包」查询。模块启停仍以 catalog + lock 为准;
5
+ * 世界包名单与显示名跟 CLI 同一条路径:listInstalledWorldPacks 会解析 pack.name 这类语言键。
6
+ */
7
+ /** 一条已安装模块。has_resource_pack 表示该模块目录下有 resource_pack。 */
8
+ export type ContentModule = {
9
+ id: string;
10
+ display_name: string;
11
+ enabled: boolean;
12
+ has_resource_pack: boolean;
13
+ };
14
+ /** 世界里已安装的行为包或资源包。name 是解析后的显示名,不再保留 pack.name 这种语言键。 */
15
+ export type WorldPackKind = "behavior" | "resource";
16
+ export type WorldPackEntry = {
17
+ kind: WorldPackKind;
18
+ name: string;
19
+ folder_name: string;
20
+ pack_id: string;
21
+ version: string;
22
+ enabled: boolean;
23
+ };
24
+ /** 世界 world_resource_packs.json 里的一条已启用资源包。旧查询仍读这个字段。 */
25
+ export type WorldResourcePack = {
26
+ name: string;
27
+ pack_id: string;
28
+ version: string;
29
+ };
30
+ export type ContentSnapshot = {
31
+ success: true;
32
+ modules: ContentModule[];
33
+ /** 行为包与资源包分开列出,顺序与 CLI list 一致。 */
34
+ packs: WorldPackEntry[];
35
+ resource_packs: WorldResourcePack[];
36
+ /** 读不到 BDS 世界时说明原因;模块列表仍然返回。 */
37
+ note?: "bds_unconfigured" | "world_unread";
38
+ };
39
+ type CatalogModule = {
40
+ id: string;
41
+ display_name: string;
42
+ enabled: boolean;
43
+ };
44
+ /**
45
+ * 从 server.properties 文本取出 level-name。
46
+ * 缺省时用 BDS 默认世界名,供拼 worlds/<level>/world_resource_packs.json。
47
+ */
48
+ export declare function parseLevelName(serverPropertiesText: string): string;
49
+ /** 解析世界资源包启用列表。损坏或不是数组时返回空列表。 */
50
+ export declare function parseWorldResourcePackList(raw: unknown): Array<{
51
+ pack_id: string;
52
+ version: string;
53
+ }>;
54
+ /**
55
+ * 语言键没解析出来时,用文件夹名代替。
56
+ * 与 CLI 自检里对 pack / pack.* 的回退一致,避免 QQ 上继续显示 pack.name。
57
+ */
58
+ export declare function worldPackDisplayName(name: string, folderName: string): string;
59
+ /**
60
+ * 收集带 resource_pack 目录的模块标识。
61
+ * 同时收录文件夹名和 sapi/manifest.json 的 id,便于和 catalog 的模块 id 对上。
62
+ */
63
+ export declare function indexModuleResourcePacks(packagesDir: string): Set<string>;
64
+ /** 给 catalog 模块标上是否自带资源包。当前查询页用这个标记区分「含资源包」。 */
65
+ export declare function markModuleResourcePacks(modules: CatalogModule[], resourcePackIds: Set<string>): ContentModule[];
66
+ /** 扫描世界已安装的行为包和资源包。显示名走 CLI 的 manifest 语言解析。 */
67
+ export declare function readInstalledWorldPacks(bdsRoot: string, levelName: string): WorldPackEntry[];
68
+ /**
69
+ * 读取某个世界已启用的资源包,并用 CLI 解析出的显示名补上。
70
+ * 目录对不上时仍返回 pack_id,避免启用列表被吞掉。
71
+ * installed 传入时复用同一次扫描,避免再读一遍包目录。
72
+ */
73
+ export declare function readEnabledWorldResourcePacks(bdsRoot: string, levelName: string, installed?: WorldPackEntry[]): WorldResourcePack[];
74
+ /**
75
+ * 组装 QQ 查询用的快照。
76
+ * packagesDir 是 modules/packages;bdsRoot 为空时只返回模块,并带 bds_unconfigured。
77
+ */
78
+ export declare function loadContentSnapshot(opts: {
79
+ packagesDir: string;
80
+ modules: CatalogModule[];
81
+ bdsRoot: string | null;
82
+ }): ContentSnapshot;
83
+ export {};
84
+ //# sourceMappingURL=installed-content.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"installed-content.d.ts","sourceRoot":"","sources":["../../src/domain/installed-content.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,yDAAyD;AACzD,MAAM,MAAM,aAAa,GAAG;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,iBAAiB,EAAE,OAAO,CAAC;CAC5B,CAAC;AAEF,yDAAyD;AACzD,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,UAAU,CAAC;AAEpD,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,aAAa,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF,yDAAyD;AACzD,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,IAAI,CAAC;IACd,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,mCAAmC;IACnC,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,cAAc,EAAE,iBAAiB,EAAE,CAAC;IACpC,gCAAgC;IAChC,IAAI,CAAC,EAAE,kBAAkB,GAAG,cAAc,CAAC;CAC5C,CAAC;AAEF,KAAK,aAAa,GAAG;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF;;;GAGG;AACH,wBAAgB,cAAc,CAAC,oBAAoB,EAAE,MAAM,GAAG,MAAM,CAInE;AAED,iCAAiC;AACjC,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,OAAO,GAAG,KAAK,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAapG;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAI7E;AAYD;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,WAAW,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CA4BzE;AAED,gDAAgD;AAChD,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,aAAa,EAAE,EAAE,eAAe,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,aAAa,EAAE,CAO/G;AAED,gDAAgD;AAChD,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,cAAc,EAAE,CAS5F;AAED;;;;GAIG;AACH,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,SAAS,CAAC,EAAE,cAAc,EAAE,GAC3B,iBAAiB,EAAE,CAuBrB;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE;IACxC,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB,GAAG,eAAe,CAmClB"}
@@ -0,0 +1,149 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { listInstalledWorldPacks } from "@sfmc-bds/bds-tools/world-packs";
4
+ function parseLevelName(serverPropertiesText) {
5
+ const matched = serverPropertiesText.match(/^level-name=(.*)$/m);
6
+ const name = matched?.[1]?.trim();
7
+ return name && name.length > 0 ? name : "Bedrock level";
8
+ }
9
+ function parseWorldResourcePackList(raw) {
10
+ if (!Array.isArray(raw)) return [];
11
+ const out = [];
12
+ for (const entry of raw) {
13
+ if (!entry || typeof entry !== "object") continue;
14
+ const packId = entry.pack_id;
15
+ if (typeof packId !== "string" || !packId.trim()) continue;
16
+ const ver = entry.version;
17
+ const version = Array.isArray(ver) && ver.length >= 3 ? `${Number(ver[0])}.${Number(ver[1])}.${Number(ver[2])}` : "";
18
+ out.push({ pack_id: packId.trim(), version });
19
+ }
20
+ return out;
21
+ }
22
+ function worldPackDisplayName(name, folderName) {
23
+ const raw = name.trim();
24
+ if (!raw || raw.startsWith("pack.") || raw === "pack") return folderName;
25
+ return raw;
26
+ }
27
+ function readJsonObject(file) {
28
+ try {
29
+ const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
30
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
31
+ return parsed;
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+ function indexModuleResourcePacks(packagesDir) {
37
+ const ids = /* @__PURE__ */ new Set();
38
+ let entries;
39
+ try {
40
+ entries = fs.readdirSync(packagesDir, { withFileTypes: true });
41
+ } catch {
42
+ return ids;
43
+ }
44
+ for (const entry of entries) {
45
+ if (entry.name.startsWith(".")) continue;
46
+ const modPath = path.join(packagesDir, entry.name);
47
+ try {
48
+ if (!fs.statSync(modPath).isDirectory()) continue;
49
+ } catch {
50
+ continue;
51
+ }
52
+ const rpDir = path.join(modPath, "resource_pack");
53
+ try {
54
+ if (!fs.statSync(rpDir).isDirectory()) continue;
55
+ } catch {
56
+ continue;
57
+ }
58
+ ids.add(entry.name);
59
+ const manifest = readJsonObject(path.join(modPath, "sapi", "manifest.json"));
60
+ const logicalId = manifest && typeof manifest.id === "string" ? manifest.id.trim() : "";
61
+ if (logicalId) ids.add(logicalId);
62
+ }
63
+ return ids;
64
+ }
65
+ function markModuleResourcePacks(modules, resourcePackIds) {
66
+ return modules.map((mod) => ({
67
+ id: mod.id,
68
+ display_name: mod.display_name,
69
+ enabled: mod.enabled,
70
+ has_resource_pack: resourcePackIds.has(mod.id)
71
+ }));
72
+ }
73
+ function readInstalledWorldPacks(bdsRoot, levelName) {
74
+ return listInstalledWorldPacks(bdsRoot, levelName).map((pack) => ({
75
+ kind: pack.kind,
76
+ name: worldPackDisplayName(pack.name, pack.folderName),
77
+ folder_name: pack.folderName,
78
+ pack_id: pack.uuid,
79
+ version: `${pack.version[0]}.${pack.version[1]}.${pack.version[2]}`,
80
+ enabled: pack.enabled
81
+ }));
82
+ }
83
+ function readEnabledWorldResourcePacks(bdsRoot, levelName, installed) {
84
+ const byUuid = new Map(
85
+ (installed ?? readInstalledWorldPacks(bdsRoot, levelName)).filter((pack) => pack.kind === "resource").map((pack) => [pack.pack_id.toLowerCase(), pack])
86
+ );
87
+ const listFile = path.join(bdsRoot, "worlds", levelName, "world_resource_packs.json");
88
+ let listed = [];
89
+ try {
90
+ listed = parseWorldResourcePackList(JSON.parse(fs.readFileSync(listFile, "utf8")));
91
+ } catch {
92
+ return [...byUuid.values()].filter((pack) => pack.enabled).map((pack) => ({ name: pack.name, pack_id: pack.pack_id, version: pack.version }));
93
+ }
94
+ return listed.map((entry) => {
95
+ const known = byUuid.get(entry.pack_id.toLowerCase());
96
+ return {
97
+ name: known?.name || entry.pack_id,
98
+ pack_id: entry.pack_id,
99
+ version: known?.version || entry.version
100
+ };
101
+ });
102
+ }
103
+ function loadContentSnapshot(opts) {
104
+ const modules = markModuleResourcePacks(opts.modules, indexModuleResourcePacks(opts.packagesDir));
105
+ const unread = (note) => ({
106
+ success: true,
107
+ modules,
108
+ packs: [],
109
+ resource_packs: [],
110
+ note
111
+ });
112
+ if (!opts.bdsRoot) return unread("bds_unconfigured");
113
+ const propsFile = path.join(opts.bdsRoot, "server.properties");
114
+ let levelName = "Bedrock level";
115
+ try {
116
+ levelName = parseLevelName(fs.readFileSync(propsFile, "utf8"));
117
+ } catch {
118
+ return unread("world_unread");
119
+ }
120
+ const worldDir = path.join(opts.bdsRoot, "worlds", levelName);
121
+ try {
122
+ if (!fs.statSync(worldDir).isDirectory()) return unread("world_unread");
123
+ } catch {
124
+ return unread("world_unread");
125
+ }
126
+ let packs;
127
+ try {
128
+ packs = readInstalledWorldPacks(opts.bdsRoot, levelName);
129
+ } catch {
130
+ return unread("world_unread");
131
+ }
132
+ return {
133
+ success: true,
134
+ modules,
135
+ packs,
136
+ resource_packs: readEnabledWorldResourcePacks(opts.bdsRoot, levelName, packs)
137
+ };
138
+ }
139
+ export {
140
+ indexModuleResourcePacks,
141
+ loadContentSnapshot,
142
+ markModuleResourcePacks,
143
+ parseLevelName,
144
+ parseWorldResourcePackList,
145
+ readEnabledWorldResourcePacks,
146
+ readInstalledWorldPacks,
147
+ worldPackDisplayName
148
+ };
149
+ //# sourceMappingURL=installed-content.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 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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",
6
+ "names": []
7
+ }
@@ -0,0 +1,8 @@
1
+ import type { QueryFn } from "../lib/sqlite.js";
2
+ export interface PlayerSessionInput {
3
+ playerName: string;
4
+ xuid: string;
5
+ }
6
+ /** 将 BDS 日志中的权威 XUID 合并到玩家快照表。 */
7
+ export declare function syncPlayerSession(query: QueryFn, input: PlayerSessionInput, now?: number): void;
8
+ //# sourceMappingURL=player-session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"player-session.d.ts","sourceRoot":"","sources":["../../src/domain/player-session.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAEhD,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,kCAAkC;AAClC,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,kBAAkB,EAAE,GAAG,SAAa,GAAG,IAAI,CAWnG"}
@@ -0,0 +1,16 @@
1
+ function syncPlayerSession(query, input, now = Date.now()) {
2
+ query(
3
+ `INSERT INTO sfmc_players (id, name, xuid, last_online, updated_at)
4
+ VALUES (?, ?, ?, ?, ?)
5
+ ON CONFLICT(id) DO UPDATE SET
6
+ name = excluded.name,
7
+ xuid = excluded.xuid,
8
+ last_online = excluded.last_online,
9
+ updated_at = excluded.updated_at`,
10
+ [input.xuid, input.playerName, input.xuid, now, now]
11
+ );
12
+ }
13
+ export {
14
+ syncPlayerSession
15
+ };
16
+ //# sourceMappingURL=player-session.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/domain/player-session.ts"],
4
+ "sourcesContent": ["import type { QueryFn } from \"../lib/sqlite.js\";\n\nexport interface PlayerSessionInput {\n playerName: string;\n xuid: string;\n}\n\n/** \u5C06 BDS \u65E5\u5FD7\u4E2D\u7684\u6743\u5A01 XUID \u5408\u5E76\u5230\u73A9\u5BB6\u5FEB\u7167\u8868\u3002 */\nexport function syncPlayerSession(query: QueryFn, input: PlayerSessionInput, now = Date.now()): void {\n query(\n `INSERT INTO sfmc_players (id, name, xuid, last_online, updated_at)\n VALUES (?, ?, ?, ?, ?)\n ON CONFLICT(id) DO UPDATE SET\n name = excluded.name,\n xuid = excluded.xuid,\n last_online = excluded.last_online,\n updated_at = excluded.updated_at`,\n [input.xuid, input.playerName, input.xuid, now, now],\n );\n}\n"],
5
+ "mappings": "AAQO,SAAS,kBAAkB,OAAgB,OAA2B,MAAM,KAAK,IAAI,GAAS;AACnG;AAAA,IACE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOA,CAAC,MAAM,MAAM,MAAM,YAAY,MAAM,MAAM,KAAK,GAAG;AAAA,EACrD;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * domain/qq-account-profile.ts — QQ「我的账号」要展示的经济余额和在线时长。
3
+ *
4
+ * 余额读经济插件归档表 sfmc_economy_accounts(计分板对账后的快照)。
5
+ * 在线时长读 online-time 插件表 sfmc_online_time,跨日、跨月的清零规则与插件离线查询一致。
6
+ * qq-link 若已把两个服务的实时结果写入 sfmc_qq_account_profile,且比插件落库更新,则优先用那份。
7
+ */
8
+ import type { QueryFn } from "../lib/sqlite.js";
9
+ /** 与经济插件默认 unitName 一致。配置文件缺失时账号页用这个单位。 */
10
+ export declare const DEFAULT_ECONOMY_UNIT = "\u8282\u64CD";
11
+ export type QqAccountProfile = {
12
+ unit: string;
13
+ /** 没有账户行时按插件约定视为 0;查询失败时为 null。 */
14
+ balance: number | null;
15
+ today_text: string;
16
+ month_text: string;
17
+ total_text: string;
18
+ };
19
+ /** 与 online-time 插件 formatDuration 相同的展示。 */
20
+ export declare function formatOnlineDuration(seconds: number): string;
21
+ /**
22
+ * 插件离线查询的清零:last_date 不是今天则今日为 0,last_month 不是本月则本月为 0。
23
+ * 当前给账号页使用,避免把昨天的时长显示成今日。
24
+ */
25
+ export declare function rollPluginOnlineSeconds(row: {
26
+ today_seconds?: number;
27
+ month_seconds?: number;
28
+ total_seconds?: number;
29
+ last_date?: string;
30
+ last_month?: string;
31
+ }, now?: number): {
32
+ today: number;
33
+ month: number;
34
+ total: number;
35
+ };
36
+ /**
37
+ * 组装一个已绑定角色的账号页数字。
38
+ * 没有账户或时长记录时按插件约定显示 0;表读失败时余额为 null、时长文案为「暂时读不到」。
39
+ */
40
+ export declare function loadQqAccountProfile(query: QueryFn, playerId: string, playerName: string, now?: number): QqAccountProfile;
41
+ /** 写入 qq-link 推来的实时余额和在线时长。缺的数字保留上一次。 */
42
+ export declare function saveQqAccountProfile(query: QueryFn, input: {
43
+ playerId: string;
44
+ playerName: string;
45
+ balance: number | null;
46
+ todaySeconds: number | null;
47
+ monthSeconds: number | null;
48
+ totalSeconds: number | null;
49
+ now?: number;
50
+ }): void;
51
+ //# sourceMappingURL=qq-account-profile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"qq-account-profile.d.ts","sourceRoot":"","sources":["../../src/domain/qq-account-profile.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAEhD,2CAA2C;AAC3C,eAAO,MAAM,oBAAoB,iBAAO,CAAC;AAKzC,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,mCAAmC;IACnC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAmBF,6CAA6C;AAC7C,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAY5D;AAUD;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,GAAG,EAAE;IAAE,aAAa,CAAC,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,EACxH,GAAG,SAAa,GACf;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAQjD;AAwCD;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,OAAO,EACd,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,GAAG,SAAa,GACf,gBAAgB,CA+ClB;AAED,yCAAyC;AACzC,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,OAAO,EACd,KAAK,EAAE;IACL,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,GACA,IAAI,CAeN"}