@sfmc-bds/db-server 0.2.0-beta.5 → 0.2.0-beta.6

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 (143) hide show
  1. package/LICENSE +663 -0
  2. package/dist/domain/bridge.d.ts +57 -23
  3. package/dist/domain/bridge.d.ts.map +1 -1
  4. package/dist/domain/bridge.js +77 -6
  5. package/dist/domain/bridge.js.map +2 -2
  6. package/dist/domain/bridge.test.d.ts +2 -0
  7. package/dist/domain/bridge.test.d.ts.map +1 -0
  8. package/dist/domain/bridge.test.js +87 -0
  9. package/dist/domain/bridge.test.js.map +7 -0
  10. package/dist/domain/economy.d.ts +38 -7
  11. package/dist/domain/economy.d.ts.map +1 -1
  12. package/dist/domain/economy.js.map +2 -2
  13. package/dist/domain/messages-bridge-filter.test.d.ts +5 -0
  14. package/dist/domain/messages-bridge-filter.test.d.ts.map +1 -0
  15. package/dist/domain/messages-bridge-filter.test.js +20 -0
  16. package/dist/domain/messages-bridge-filter.test.js.map +7 -0
  17. package/dist/domain/qq-bind-status.test.d.ts +2 -0
  18. package/dist/domain/qq-bind-status.test.d.ts.map +1 -0
  19. package/dist/domain/qq-bind-status.test.js +211 -0
  20. package/dist/domain/qq-bind-status.test.js.map +7 -0
  21. package/dist/domain/qq-events.d.ts +84 -0
  22. package/dist/domain/qq-events.d.ts.map +1 -0
  23. package/dist/domain/qq-events.js +193 -0
  24. package/dist/domain/qq-events.js.map +7 -0
  25. package/dist/domain/qq-events.test.d.ts +5 -0
  26. package/dist/domain/qq-events.test.d.ts.map +1 -0
  27. package/dist/domain/qq-events.test.js +170 -0
  28. package/dist/domain/qq-events.test.js.map +7 -0
  29. package/dist/domain/qq-join.test.d.ts +2 -0
  30. package/dist/domain/qq-join.test.d.ts.map +1 -0
  31. package/dist/domain/qq-join.test.js +353 -0
  32. package/dist/domain/qq-join.test.js.map +7 -0
  33. package/dist/domain/schema.d.ts +17 -19
  34. package/dist/domain/schema.d.ts.map +1 -1
  35. package/dist/domain/schema.js +69 -0
  36. package/dist/domain/schema.js.map +2 -2
  37. package/dist/domain/system-status.d.ts +75 -0
  38. package/dist/domain/system-status.d.ts.map +1 -0
  39. package/dist/domain/system-status.js +181 -0
  40. package/dist/domain/system-status.js.map +7 -0
  41. package/dist/domain/system-status.test.d.ts +5 -0
  42. package/dist/domain/system-status.test.d.ts.map +1 -0
  43. package/dist/domain/system-status.test.js +19 -0
  44. package/dist/domain/system-status.test.js.map +7 -0
  45. package/dist/domain/transaction.d.ts +5 -4
  46. package/dist/domain/transaction.d.ts.map +1 -1
  47. package/dist/domain/transaction.js.map +2 -2
  48. package/dist/env.d.ts +7 -0
  49. package/dist/env.d.ts.map +1 -1
  50. package/dist/env.js +55 -3
  51. package/dist/env.js.map +2 -2
  52. package/dist/index.d.ts +0 -1
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +105 -19
  55. package/dist/index.js.map +2 -2
  56. package/dist/lib/idempotency-store.d.ts +17 -11
  57. package/dist/lib/idempotency-store.d.ts.map +1 -1
  58. package/dist/lib/idempotency-store.js.map +2 -2
  59. package/dist/lib/idempotency.d.ts +3 -4
  60. package/dist/lib/idempotency.d.ts.map +1 -1
  61. package/dist/lib/idempotency.js.map +2 -2
  62. package/dist/lib/module-state.d.ts +38 -8
  63. package/dist/lib/module-state.d.ts.map +1 -1
  64. package/dist/lib/module-state.js.map +2 -2
  65. package/dist/lib/order-by.d.ts +8 -6
  66. package/dist/lib/order-by.d.ts.map +1 -1
  67. package/dist/lib/order-by.js.map +2 -2
  68. package/dist/lib/sql-helpers.d.ts +25 -13
  69. package/dist/lib/sql-helpers.d.ts.map +1 -1
  70. package/dist/lib/sql-helpers.js.map +2 -2
  71. package/dist/lib/sqlite.d.ts +14 -5
  72. package/dist/lib/sqlite.d.ts.map +1 -1
  73. package/dist/lib/sqlite.js.map +2 -2
  74. package/dist/manifest-loader.d.ts +5 -2
  75. package/dist/manifest-loader.d.ts.map +1 -1
  76. package/dist/manifest-loader.js +16 -6
  77. package/dist/manifest-loader.js.map +2 -2
  78. package/dist/manifest.d.ts +1 -2
  79. package/dist/manifest.d.ts.map +1 -1
  80. package/dist/manifest.js +13 -8
  81. package/dist/manifest.js.map +2 -2
  82. package/dist/project-root.d.ts +1 -1
  83. package/dist/project-root.d.ts.map +1 -1
  84. package/dist/project-root.js +2 -2
  85. package/dist/project-root.js.map +2 -2
  86. package/dist/routes/_shared.d.ts +14 -5
  87. package/dist/routes/_shared.d.ts.map +1 -1
  88. package/dist/routes/_shared.js.map +2 -2
  89. package/dist/routes/config.d.ts +8 -11
  90. package/dist/routes/config.d.ts.map +1 -1
  91. package/dist/routes/config.js +17 -23
  92. package/dist/routes/config.js.map +2 -2
  93. package/dist/routes/db-routes.d.ts +6 -0
  94. package/dist/routes/db-routes.d.ts.map +1 -1
  95. package/dist/routes/db-routes.js.map +2 -2
  96. package/dist/routes/messages.d.ts +6 -2
  97. package/dist/routes/messages.d.ts.map +1 -1
  98. package/dist/routes/messages.js +17 -3
  99. package/dist/routes/messages.js.map +2 -2
  100. package/dist/routes/module-config-routes.d.ts +13 -9
  101. package/dist/routes/module-config-routes.d.ts.map +1 -1
  102. package/dist/routes/module-config-routes.js.map +2 -2
  103. package/dist/routes/modules.d.ts +6 -0
  104. package/dist/routes/modules.d.ts.map +1 -1
  105. package/dist/routes/modules.js.map +2 -2
  106. package/dist/routes/qq-bind.d.ts +24 -0
  107. package/dist/routes/qq-bind.d.ts.map +1 -0
  108. package/dist/routes/qq-bind.js +179 -0
  109. package/dist/routes/qq-bind.js.map +7 -0
  110. package/dist/routes/qq-events.d.ts +20 -0
  111. package/dist/routes/qq-events.d.ts.map +1 -0
  112. package/dist/routes/qq-events.js +39 -0
  113. package/dist/routes/qq-events.js.map +7 -0
  114. package/dist/routes/qq-join.d.ts +46 -0
  115. package/dist/routes/qq-join.d.ts.map +1 -0
  116. package/dist/routes/qq-join.js +277 -0
  117. package/dist/routes/qq-join.js.map +7 -0
  118. package/dist/routes/service-routes.d.ts +6 -0
  119. package/dist/routes/service-routes.d.ts.map +1 -1
  120. package/dist/routes/service-routes.js.map +2 -2
  121. package/dist/routes/status.d.ts +19 -0
  122. package/dist/routes/status.d.ts.map +1 -0
  123. package/dist/routes/status.js +74 -0
  124. package/dist/routes/status.js.map +7 -0
  125. package/dist/schema-registry.d.ts +16 -15
  126. package/dist/schema-registry.d.ts.map +1 -1
  127. package/dist/schema-registry.js +7 -1
  128. package/dist/schema-registry.js.map +2 -2
  129. package/dist/server.d.ts +19 -0
  130. package/dist/server.d.ts.map +1 -1
  131. package/dist/server.js.map +2 -2
  132. package/dist/service-registry.d.ts +39 -9
  133. package/dist/service-registry.d.ts.map +1 -1
  134. package/dist/service-registry.js +30 -2
  135. package/dist/service-registry.js.map +2 -2
  136. package/dist/services/builtin-handlers.d.ts +27 -7
  137. package/dist/services/builtin-handlers.d.ts.map +1 -1
  138. package/dist/services/builtin-handlers.js.map +2 -2
  139. package/dist/tx-runner.d.ts +29 -3
  140. package/dist/tx-runner.d.ts.map +1 -1
  141. package/dist/tx-runner.js +29 -3
  142. package/dist/tx-runner.js.map +2 -2
  143. package/package.json +17 -15
@@ -0,0 +1,277 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { SQL } from "sql-template-strings";
3
+ function newId(prefix) {
4
+ return `${prefix}_${Date.now().toString(36)}_${randomBytes(4).toString("hex")}`;
5
+ }
6
+ function isAdmin(getAdminOpenids, openid) {
7
+ const list = getAdminOpenids();
8
+ if (!openid || list.length === 0) return false;
9
+ return list.includes(openid);
10
+ }
11
+ function authorizeAdmin(getAdminOpenids, getJoinFlags, openid, data) {
12
+ if (isAdmin(getAdminOpenids, openid)) return true;
13
+ const asGroup = data.as_group_admin === true || data.as_group_admin === "true" || data.is_group_admin === true || data.is_group_admin === "true";
14
+ return asGroup && getJoinFlags().treatGroupAdminsAsAdmins === true;
15
+ }
16
+ function createQqJoinRoutes({ query, body, json, getAdminOpenids, getJoinFlags, setJoinFlags }) {
17
+ return async function handle({
18
+ path,
19
+ method,
20
+ params,
21
+ req,
22
+ res
23
+ }) {
24
+ const isJoin = path.startsWith("/api/sfmc/qq/join");
25
+ const isAdminPath = path.startsWith("/api/sfmc/qq/admin");
26
+ if (!isJoin && !isAdminPath) return false;
27
+ if (path === "/api/sfmc/qq/join/settings" && method === "GET") {
28
+ const flags = getJoinFlags();
29
+ json(res, {
30
+ success: true,
31
+ allowlist_enabled: flags.allowlistEnabled,
32
+ require_approval: flags.requireApproval,
33
+ treat_group_admins_as_admins: flags.treatGroupAdminsAsAdmins
34
+ });
35
+ return true;
36
+ }
37
+ if (path === "/api/sfmc/qq/join/settings" && method === "POST") {
38
+ const data = await body(req);
39
+ const openid = String(data.openid ?? data.decided_by ?? "").trim();
40
+ if (Object.prototype.hasOwnProperty.call(data, "treat_group_admins_as_admins") || Object.prototype.hasOwnProperty.call(data, "treatGroupAdminsAsAdmins")) {
41
+ json(
42
+ res,
43
+ {
44
+ success: false,
45
+ error: "immutable_field",
46
+ note: "treat_group_admins_as_admins \u53EA\u80FD\u6539 configs/qq_link.json\uFF0C\u7FA4\u804A\u4E0D\u53EF\u6539"
47
+ },
48
+ 400
49
+ );
50
+ return true;
51
+ }
52
+ if (!authorizeAdmin(getAdminOpenids, getJoinFlags, openid, data)) {
53
+ json(res, { success: false, error: "not_admin" }, 403);
54
+ return true;
55
+ }
56
+ const partial = {};
57
+ if (Object.prototype.hasOwnProperty.call(data, "allowlist_enabled")) {
58
+ partial.allowlistEnabled = data.allowlist_enabled === true || data.allowlist_enabled === "true";
59
+ }
60
+ if (Object.prototype.hasOwnProperty.call(data, "require_approval")) {
61
+ partial.requireApproval = data.require_approval === true || data.require_approval === "true";
62
+ }
63
+ if (partial.allowlistEnabled === void 0 && partial.requireApproval === void 0) {
64
+ json(res, { success: false, error: "no_settings" }, 400);
65
+ return true;
66
+ }
67
+ const flags = setJoinFlags(partial);
68
+ json(res, {
69
+ success: true,
70
+ allowlist_enabled: flags.allowlistEnabled,
71
+ require_approval: flags.requireApproval,
72
+ treat_group_admins_as_admins: flags.treatGroupAdminsAsAdmins
73
+ });
74
+ return true;
75
+ }
76
+ if (path === "/api/sfmc/qq/join/request" && method === "POST") {
77
+ const flags = getJoinFlags();
78
+ if (!flags.allowlistEnabled) {
79
+ json(res, { success: false, error: "join_allowlist_disabled" }, 403);
80
+ return true;
81
+ }
82
+ const data = await body(req);
83
+ const openid = String(data.openid ?? data.applicant_openid ?? "").trim();
84
+ const playerName = String(data.player_name ?? data.name ?? "").trim();
85
+ const backend = String(data.qq_backend ?? "official").trim() || "official";
86
+ if (!openid) {
87
+ json(res, { success: false, error: "openid_required" }, 400);
88
+ return true;
89
+ }
90
+ if (!playerName || playerName.length > 32) {
91
+ json(res, { success: false, error: "player_name_required" }, 400);
92
+ return true;
93
+ }
94
+ const id = newId("join");
95
+ const now = Date.now();
96
+ const autoApproved = !flags.requireApproval;
97
+ const status = autoApproved ? "approved" : "pending";
98
+ if (autoApproved) {
99
+ query(SQL`
100
+ INSERT INTO sfmc_qq_join_requests
101
+ (id, applicant_openid, player_name, status, qq_backend, created_at, decided_at, decided_by)
102
+ VALUES (${id}, ${openid}, ${playerName}, ${status}, ${backend}, ${now}, ${now}, 'auto')
103
+ `);
104
+ } else {
105
+ query(SQL`
106
+ INSERT INTO sfmc_qq_join_requests
107
+ (id, applicant_openid, player_name, status, qq_backend, created_at)
108
+ VALUES (${id}, ${openid}, ${playerName}, ${status}, ${backend}, ${now})
109
+ `);
110
+ }
111
+ const admins = getAdminOpenids();
112
+ json(res, {
113
+ success: true,
114
+ id,
115
+ player_name: playerName,
116
+ status,
117
+ auto_approved: autoApproved,
118
+ admin_notify: !autoApproved && admins.length > 0,
119
+ note: autoApproved ? "\u5DF2\u5173\u95ED\u5BA1\u6279\uFF0C\u7533\u8BF7\u5DF2\u81EA\u52A8\u901A\u8FC7\uFF0C\u7B49\u5F85 BDS \u5199\u5165\u767D\u540D\u5355" : admins.length === 0 ? "\u672A\u914D\u7F6E qq_admin_openids\uFF0C\u65E0\u6CD5\u901A\u77E5\u7BA1\u7406\u5458" : void 0
120
+ });
121
+ return true;
122
+ }
123
+ if (path === "/api/sfmc/qq/join/decide" && method === "POST") {
124
+ const data = await body(req);
125
+ const id = String(data.id ?? data.request_id ?? "").trim();
126
+ const decision = String(data.decision ?? "").trim().toLowerCase();
127
+ const decidedBy = String(data.decided_by ?? data.openid ?? "").trim();
128
+ if (!id || decision !== "approve" && decision !== "reject") {
129
+ json(res, { success: false, error: "id_and_decision_required" }, 400);
130
+ return true;
131
+ }
132
+ if (!authorizeAdmin(getAdminOpenids, getJoinFlags, decidedBy, data)) {
133
+ json(res, { success: false, error: "not_admin" }, 403);
134
+ return true;
135
+ }
136
+ const rows = query(SQL`SELECT * FROM sfmc_qq_join_requests WHERE id = ${id} LIMIT 1`);
137
+ const row = rows[0];
138
+ if (!row) {
139
+ json(res, { success: false, error: "not_found" }, 404);
140
+ return true;
141
+ }
142
+ if (String(row.status) !== "pending") {
143
+ json(res, { success: false, error: "already_decided", status: row.status }, 409);
144
+ return true;
145
+ }
146
+ const next = decision === "approve" ? "approved" : "rejected";
147
+ const now = Date.now();
148
+ query(SQL`
149
+ UPDATE sfmc_qq_join_requests
150
+ SET status = ${next}, decided_at = ${now}, decided_by = ${decidedBy}
151
+ WHERE id = ${id}
152
+ `);
153
+ json(res, {
154
+ success: true,
155
+ id,
156
+ status: next,
157
+ player_name: row.player_name,
158
+ applicant_openid: row.applicant_openid
159
+ });
160
+ return true;
161
+ }
162
+ if (path === "/api/sfmc/qq/join/pending" && method === "GET") {
163
+ const openid = String(params.get("openid") ?? "").trim();
164
+ if (openid) {
165
+ const asGroup = params.get("as_group_admin") === "true" || params.get("is_group_admin") === "true";
166
+ if (!authorizeAdmin(getAdminOpenids, getJoinFlags, openid, { as_group_admin: asGroup })) {
167
+ json(res, { success: false, error: "not_admin" }, 403);
168
+ return true;
169
+ }
170
+ }
171
+ const rows = query(
172
+ SQL`SELECT id, applicant_openid, player_name, status, created_at
173
+ FROM sfmc_qq_join_requests WHERE status = 'pending'
174
+ ORDER BY created_at ASC LIMIT 50`
175
+ );
176
+ json(res, { success: true, pending: rows });
177
+ return true;
178
+ }
179
+ if (path === "/api/sfmc/qq/join/apply-queue" && method === "GET") {
180
+ const flags = getJoinFlags();
181
+ if (!flags.allowlistEnabled) {
182
+ json(res, { success: true, queue: [], note: "join_allowlist_disabled" });
183
+ return true;
184
+ }
185
+ const rows = query(
186
+ SQL`SELECT id, player_name, applicant_openid, decided_at
187
+ FROM sfmc_qq_join_requests WHERE status = 'approved'
188
+ ORDER BY decided_at ASC LIMIT 32`
189
+ );
190
+ json(res, { success: true, queue: rows });
191
+ return true;
192
+ }
193
+ if (path === "/api/sfmc/qq/join/applied" && method === "POST") {
194
+ const data = await body(req);
195
+ const id = String(data.id ?? "").trim();
196
+ const ok = data.ok !== false && data.error == null;
197
+ const err = String(data.error ?? "").slice(0, 200);
198
+ if (!id) {
199
+ json(res, { success: false, error: "id_required" }, 400);
200
+ return true;
201
+ }
202
+ const now = Date.now();
203
+ if (ok) {
204
+ query(SQL`
205
+ UPDATE sfmc_qq_join_requests
206
+ SET status = 'applied', applied_at = ${now}, apply_error = NULL
207
+ WHERE id = ${id} AND status = 'approved'
208
+ `);
209
+ } else {
210
+ query(SQL`
211
+ UPDATE sfmc_qq_join_requests
212
+ SET status = 'apply_failed', applied_at = ${now}, apply_error = ${err}
213
+ WHERE id = ${id} AND status = 'approved'
214
+ `);
215
+ }
216
+ json(res, { success: true });
217
+ return true;
218
+ }
219
+ if (path === "/api/sfmc/qq/admin/kick" && method === "POST") {
220
+ const data = await body(req);
221
+ const openid = String(data.openid ?? data.requested_by ?? "").trim();
222
+ const target = String(data.target_name ?? data.name ?? "").trim();
223
+ const reason = String(data.reason ?? "QQ \u7BA1\u7406\u5458\u8E22\u51FA").trim().slice(0, 80);
224
+ if (!authorizeAdmin(getAdminOpenids, getJoinFlags, openid, data)) {
225
+ json(res, { success: false, error: "not_admin" }, 403);
226
+ return true;
227
+ }
228
+ if (!target) {
229
+ json(res, { success: false, error: "target_name_required" }, 400);
230
+ return true;
231
+ }
232
+ const id = newId("act");
233
+ const now = Date.now();
234
+ query(SQL`
235
+ INSERT INTO sfmc_qq_admin_actions
236
+ (id, kind, target_name, reason, requested_by, status, created_at)
237
+ VALUES (${id}, 'kick', ${target}, ${reason}, ${openid}, 'pending', ${now})
238
+ `);
239
+ json(res, { success: true, id });
240
+ return true;
241
+ }
242
+ if (path === "/api/sfmc/qq/admin/action-queue" && method === "GET") {
243
+ const rows = query(
244
+ SQL`SELECT id, kind, target_name, reason, created_at
245
+ FROM sfmc_qq_admin_actions WHERE status = 'pending'
246
+ ORDER BY created_at ASC LIMIT 32`
247
+ );
248
+ json(res, { success: true, queue: rows });
249
+ return true;
250
+ }
251
+ if (path === "/api/sfmc/qq/admin/action-done" && method === "POST") {
252
+ const data = await body(req);
253
+ const id = String(data.id ?? "").trim();
254
+ const ok = data.ok !== false && data.error == null;
255
+ const err = String(data.error ?? "").slice(0, 200);
256
+ if (!id) {
257
+ json(res, { success: false, error: "id_required" }, 400);
258
+ return true;
259
+ }
260
+ const now = Date.now();
261
+ const status = ok ? "done" : "failed";
262
+ query(SQL`
263
+ UPDATE sfmc_qq_admin_actions
264
+ SET status = ${status}, applied_at = ${now}, apply_error = ${ok ? "" : err}
265
+ WHERE id = ${id} AND status = 'pending'
266
+ `);
267
+ json(res, { success: true });
268
+ return true;
269
+ }
270
+ json(res, { success: false, error: "not_found" }, 404);
271
+ return true;
272
+ };
273
+ }
274
+ export {
275
+ createQqJoinRoutes
276
+ };
277
+ //# sourceMappingURL=qq-join.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/routes/qq-join.ts"],
4
+ "sourcesContent": ["/**\n * routes/qq-join.ts \u2014 \u5165\u670D\u5BA1\u6279\u72B6\u6001\u673A + BDS \u751F\u6548\u961F\u5217\uFF08\u4E0D\u5199 allowlist \u6587\u4EF6\uFF09\n *\n * GET /api/sfmc/qq/join/settings\n * POST /api/sfmc/qq/join/settings\n * POST /api/sfmc/qq/join/request\n * POST /api/sfmc/qq/join/decide\n * GET /api/sfmc/qq/join/pending\n * GET /api/sfmc/qq/join/apply-queue\n * POST /api/sfmc/qq/join/applied\n * POST /api/sfmc/qq/admin/kick\n * GET /api/sfmc/qq/admin/action-queue\n * POST /api/sfmc/qq/admin/action-done\n */\n\nimport { randomBytes } from \"node:crypto\";\nimport { SQL } from \"sql-template-strings\";\nimport type { QueryFn } from \"../lib/sqlite.js\";\n\nexport type JoinFeatureFlags = {\n /** \u5165\u670D\u767D\u540D\u5355\u603B\u5F00\u5173 */\n allowlistEnabled: boolean;\n /** \u662F\u5426\u9700\u8981\u7BA1\u7406\u5458\u5BA1\u6279 */\n requireApproval: boolean;\n /**\n * \u662F\u5426\u5C06 QQ \u7FA4\u4E3B/\u7FA4\u7BA1\u7406\u5458\u89C6\u4F5C SFMC \u7BA1\u7406\u5458\u3002\n * \u4EC5 configs/qq_link.json \u53EF\u6539\uFF0CAPI/\u7FA4\u804A\u4E0D\u53EF\u5199\u3002\n */\n treatGroupAdminsAsAdmins: boolean;\n};\n\ninterface Deps {\n query: QueryFn;\n body: (req: import(\"http\").IncomingMessage) => Promise<Record<string, unknown>>;\n json: (res: import(\"http\").ServerResponse, data: Record<string, unknown>, status?: number) => void;\n /** \u5F53\u524D\u7BA1\u7406\u5458 openid \u5217\u8868\uFF08\u6765\u81EA qq_config\uFF09 */\n getAdminOpenids: () => string[];\n /** \u8BFB\u53D6\u5165\u670D\u5F00\u5173\uFF08\u5E94\u8BFB\u6700\u65B0 qq_config\uFF09 */\n getJoinFlags: () => JoinFeatureFlags;\n /** \u5199\u56DE\u5165\u670D\u5F00\u5173\uFF08\u6301\u4E45\u5316 + \u5185\u5B58\uFF09 */\n setJoinFlags: (partial: Partial<JoinFeatureFlags>) => JoinFeatureFlags;\n}\n\nfunction newId(prefix: string): string {\n return `${prefix}_${Date.now().toString(36)}_${randomBytes(4).toString(\"hex\")}`;\n}\n\nfunction isAdmin(getAdminOpenids: () => string[], openid: string): boolean {\n const list = getAdminOpenids();\n if (!openid || list.length === 0) return false;\n return list.includes(openid);\n}\n\n/** openid \u5728\u540D\u5355\u5185\uFF0C\u6216\uFF08\u5F00\u5173\u5F00\u4E14 bridge \u58F0\u660E as_group_admin\uFF09 */\nfunction authorizeAdmin(\n getAdminOpenids: () => string[],\n getJoinFlags: () => JoinFeatureFlags,\n openid: string,\n data: Record<string, unknown>\n): boolean {\n if (isAdmin(getAdminOpenids, openid)) return true;\n const asGroup =\n data.as_group_admin === true ||\n data.as_group_admin === \"true\" ||\n data.is_group_admin === true ||\n data.is_group_admin === \"true\";\n return asGroup && getJoinFlags().treatGroupAdminsAsAdmins === true;\n}\n\nfunction createQqJoinRoutes({ query, body, json, getAdminOpenids, getJoinFlags, setJoinFlags }: Deps) {\n return async function handle({\n path,\n method,\n params,\n req,\n res,\n }: {\n path: string;\n method: string;\n params: URLSearchParams;\n req: import(\"http\").IncomingMessage;\n res: import(\"http\").ServerResponse;\n }): Promise<boolean> {\n const isJoin = path.startsWith(\"/api/sfmc/qq/join\");\n const isAdminPath = path.startsWith(\"/api/sfmc/qq/admin\");\n if (!isJoin && !isAdminPath) return false;\n\n // \u2500\u2500 \u5165\u670D\u5F00\u5173\uFF08\u6587\u4EF6 + \u673A\u5668\u4EBA\u53EF\u6539\u767D\u540D\u5355/\u5BA1\u6279\uFF1B\u7FA4\u7BA1\u5F00\u5173\u53EA\u8BFB\uFF09\u2500\u2500\n if (path === \"/api/sfmc/qq/join/settings\" && method === \"GET\") {\n const flags = getJoinFlags();\n json(res, {\n success: true,\n allowlist_enabled: flags.allowlistEnabled,\n require_approval: flags.requireApproval,\n treat_group_admins_as_admins: flags.treatGroupAdminsAsAdmins,\n });\n return true;\n }\n\n if (path === \"/api/sfmc/qq/join/settings\" && method === \"POST\") {\n const data = await body(req);\n const openid = String(data.openid ?? data.decided_by ?? \"\").trim();\n if (\n Object.prototype.hasOwnProperty.call(data, \"treat_group_admins_as_admins\") ||\n Object.prototype.hasOwnProperty.call(data, \"treatGroupAdminsAsAdmins\")\n ) {\n json(\n res,\n {\n success: false,\n error: \"immutable_field\",\n note: \"treat_group_admins_as_admins \u53EA\u80FD\u6539 configs/qq_link.json\uFF0C\u7FA4\u804A\u4E0D\u53EF\u6539\",\n },\n 400\n );\n return true;\n }\n if (!authorizeAdmin(getAdminOpenids, getJoinFlags, openid, data)) {\n json(res, { success: false, error: \"not_admin\" }, 403);\n return true;\n }\n const partial: Partial<JoinFeatureFlags> = {};\n if (Object.prototype.hasOwnProperty.call(data, \"allowlist_enabled\")) {\n partial.allowlistEnabled = data.allowlist_enabled === true || data.allowlist_enabled === \"true\";\n }\n if (Object.prototype.hasOwnProperty.call(data, \"require_approval\")) {\n partial.requireApproval = data.require_approval === true || data.require_approval === \"true\";\n }\n if (partial.allowlistEnabled === undefined && partial.requireApproval === undefined) {\n json(res, { success: false, error: \"no_settings\" }, 400);\n return true;\n }\n const flags = setJoinFlags(partial);\n json(res, {\n success: true,\n allowlist_enabled: flags.allowlistEnabled,\n require_approval: flags.requireApproval,\n treat_group_admins_as_admins: flags.treatGroupAdminsAsAdmins,\n });\n return true;\n }\n\n // \u2500\u2500 \u5165\u670D\u7533\u8BF7 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n if (path === \"/api/sfmc/qq/join/request\" && method === \"POST\") {\n const flags = getJoinFlags();\n if (!flags.allowlistEnabled) {\n json(res, { success: false, error: \"join_allowlist_disabled\" }, 403);\n return true;\n }\n const data = await body(req);\n const openid = String(data.openid ?? data.applicant_openid ?? \"\").trim();\n const playerName = String(data.player_name ?? data.name ?? \"\").trim();\n const backend = String(data.qq_backend ?? \"official\").trim() || \"official\";\n if (!openid) {\n json(res, { success: false, error: \"openid_required\" }, 400);\n return true;\n }\n if (!playerName || playerName.length > 32) {\n json(res, { success: false, error: \"player_name_required\" }, 400);\n return true;\n }\n const id = newId(\"join\");\n const now = Date.now();\n const autoApproved = !flags.requireApproval;\n const status = autoApproved ? \"approved\" : \"pending\";\n if (autoApproved) {\n query(SQL`\n INSERT INTO sfmc_qq_join_requests\n (id, applicant_openid, player_name, status, qq_backend, created_at, decided_at, decided_by)\n VALUES (${id}, ${openid}, ${playerName}, ${status}, ${backend}, ${now}, ${now}, 'auto')\n `);\n } else {\n query(SQL`\n INSERT INTO sfmc_qq_join_requests\n (id, applicant_openid, player_name, status, qq_backend, created_at)\n VALUES (${id}, ${openid}, ${playerName}, ${status}, ${backend}, ${now})\n `);\n }\n const admins = getAdminOpenids();\n json(res, {\n success: true,\n id,\n player_name: playerName,\n status,\n auto_approved: autoApproved,\n admin_notify: !autoApproved && admins.length > 0,\n note: autoApproved\n ? \"\u5DF2\u5173\u95ED\u5BA1\u6279\uFF0C\u7533\u8BF7\u5DF2\u81EA\u52A8\u901A\u8FC7\uFF0C\u7B49\u5F85 BDS \u5199\u5165\u767D\u540D\u5355\"\n : admins.length === 0\n ? \"\u672A\u914D\u7F6E qq_admin_openids\uFF0C\u65E0\u6CD5\u901A\u77E5\u7BA1\u7406\u5458\"\n : undefined,\n });\n return true;\n }\n\n if (path === \"/api/sfmc/qq/join/decide\" && method === \"POST\") {\n const data = await body(req);\n const id = String(data.id ?? data.request_id ?? \"\").trim();\n const decision = String(data.decision ?? \"\").trim().toLowerCase();\n const decidedBy = String(data.decided_by ?? data.openid ?? \"\").trim();\n if (!id || (decision !== \"approve\" && decision !== \"reject\")) {\n json(res, { success: false, error: \"id_and_decision_required\" }, 400);\n return true;\n }\n if (!authorizeAdmin(getAdminOpenids, getJoinFlags, decidedBy, data)) {\n json(res, { success: false, error: \"not_admin\" }, 403);\n return true;\n }\n const rows = query(SQL`SELECT * FROM sfmc_qq_join_requests WHERE id = ${id} LIMIT 1`) as Array<\n Record<string, unknown>\n >;\n const row = rows[0];\n if (!row) {\n json(res, { success: false, error: \"not_found\" }, 404);\n return true;\n }\n if (String(row.status) !== \"pending\") {\n json(res, { success: false, error: \"already_decided\", status: row.status }, 409);\n return true;\n }\n const next = decision === \"approve\" ? \"approved\" : \"rejected\";\n const now = Date.now();\n query(SQL`\n UPDATE sfmc_qq_join_requests\n SET status = ${next}, decided_at = ${now}, decided_by = ${decidedBy}\n WHERE id = ${id}\n `);\n json(res, {\n success: true,\n id,\n status: next,\n player_name: row.player_name,\n applicant_openid: row.applicant_openid,\n });\n return true;\n }\n\n if (path === \"/api/sfmc/qq/join/pending\" && method === \"GET\") {\n const openid = String(params.get(\"openid\") ?? \"\").trim();\n if (openid) {\n const asGroup =\n params.get(\"as_group_admin\") === \"true\" || params.get(\"is_group_admin\") === \"true\";\n if (!authorizeAdmin(getAdminOpenids, getJoinFlags, openid, { as_group_admin: asGroup })) {\n json(res, { success: false, error: \"not_admin\" }, 403);\n return true;\n }\n }\n const rows = query(\n SQL`SELECT id, applicant_openid, player_name, status, created_at\n FROM sfmc_qq_join_requests WHERE status = 'pending'\n ORDER BY created_at ASC LIMIT 50`\n ) as Array<Record<string, unknown>>;\n json(res, { success: true, pending: rows });\n return true;\n }\n\n if (path === \"/api/sfmc/qq/join/apply-queue\" && method === \"GET\") {\n const flags = getJoinFlags();\n if (!flags.allowlistEnabled) {\n json(res, { success: true, queue: [], note: \"join_allowlist_disabled\" });\n return true;\n }\n const rows = query(\n SQL`SELECT id, player_name, applicant_openid, decided_at\n FROM sfmc_qq_join_requests WHERE status = 'approved'\n ORDER BY decided_at ASC LIMIT 32`\n ) as Array<Record<string, unknown>>;\n json(res, { success: true, queue: rows });\n return true;\n }\n\n if (path === \"/api/sfmc/qq/join/applied\" && method === \"POST\") {\n const data = await body(req);\n const id = String(data.id ?? \"\").trim();\n const ok = data.ok !== false && data.error == null;\n const err = String(data.error ?? \"\").slice(0, 200);\n if (!id) {\n json(res, { success: false, error: \"id_required\" }, 400);\n return true;\n }\n const now = Date.now();\n if (ok) {\n query(SQL`\n UPDATE sfmc_qq_join_requests\n SET status = 'applied', applied_at = ${now}, apply_error = NULL\n WHERE id = ${id} AND status = 'approved'\n `);\n } else {\n query(SQL`\n UPDATE sfmc_qq_join_requests\n SET status = 'apply_failed', applied_at = ${now}, apply_error = ${err}\n WHERE id = ${id} AND status = 'approved'\n `);\n }\n json(res, { success: true });\n return true;\n }\n\n // \u2500\u2500 \u8E22\u4EBA\u961F\u5217 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n if (path === \"/api/sfmc/qq/admin/kick\" && method === \"POST\") {\n const data = await body(req);\n const openid = String(data.openid ?? data.requested_by ?? \"\").trim();\n const target = String(data.target_name ?? data.name ?? \"\").trim();\n const reason = String(data.reason ?? \"QQ \u7BA1\u7406\u5458\u8E22\u51FA\").trim().slice(0, 80);\n if (!authorizeAdmin(getAdminOpenids, getJoinFlags, openid, data)) {\n json(res, { success: false, error: \"not_admin\" }, 403);\n return true;\n }\n if (!target) {\n json(res, { success: false, error: \"target_name_required\" }, 400);\n return true;\n }\n const id = newId(\"act\");\n const now = Date.now();\n query(SQL`\n INSERT INTO sfmc_qq_admin_actions\n (id, kind, target_name, reason, requested_by, status, created_at)\n VALUES (${id}, 'kick', ${target}, ${reason}, ${openid}, 'pending', ${now})\n `);\n json(res, { success: true, id });\n return true;\n }\n\n if (path === \"/api/sfmc/qq/admin/action-queue\" && method === \"GET\") {\n const rows = query(\n SQL`SELECT id, kind, target_name, reason, created_at\n FROM sfmc_qq_admin_actions WHERE status = 'pending'\n ORDER BY created_at ASC LIMIT 32`\n ) as Array<Record<string, unknown>>;\n json(res, { success: true, queue: rows });\n return true;\n }\n\n if (path === \"/api/sfmc/qq/admin/action-done\" && method === \"POST\") {\n const data = await body(req);\n const id = String(data.id ?? \"\").trim();\n const ok = data.ok !== false && data.error == null;\n const err = String(data.error ?? \"\").slice(0, 200);\n if (!id) {\n json(res, { success: false, error: \"id_required\" }, 400);\n return true;\n }\n const now = Date.now();\n const status = ok ? \"done\" : \"failed\";\n query(SQL`\n UPDATE sfmc_qq_admin_actions\n SET status = ${status}, applied_at = ${now}, apply_error = ${ok ? \"\" : err}\n WHERE id = ${id} AND status = 'pending'\n `);\n json(res, { success: true });\n return true;\n }\n\n json(res, { success: false, error: \"not_found\" }, 404);\n return true;\n };\n}\n\nexport { createQqJoinRoutes };\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -15,6 +15,12 @@ export interface ServiceRoutesDeps {
15
15
  enabled: Map<string, import("../manifest-loader.js").ModuleManifestV2>;
16
16
  json?: typeof defaultJson;
17
17
  }
18
+ /**
19
+ * 创建 `/api/sfmc/services/*` 跨模块服务路由请求处理器。
20
+ *
21
+ * @param depsIn 依赖注入项(包含 serviceRegistry、enabled 模块字典等)。
22
+ * @returns 异步路由处理函数。
23
+ */
18
24
  export declare function createServiceRoutes(depsIn: Partial<ServiceRoutesDeps>): (ctx: {
19
25
  path: string;
20
26
  method: Method | string;
@@ -1 +1 @@
1
- {"version":3,"file":"service-routes.d.ts","sourceRoot":"","sources":["../../src/routes/service-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AACjE,OAAO,EAAE,IAAI,IAAI,WAAW,EAAE,KAAK,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAElE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAc,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AAE3D,MAAM,WAAW,iBAAiB;IAChC,eAAe,EAAE,eAAe,CAAC;IACjC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,uBAAuB,EAAE,gBAAgB,CAAC,CAAC;IACvE,IAAI,CAAC,EAAE,OAAO,WAAW,CAAC;CAC3B;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,OAAO,CAAC,iBAAiB,CAAC,SAOjD;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,MAAM,EAAE,eAAe,CAAC;IACxB,GAAG,EAAE,eAAe,CAAC;IACrB,GAAG,EAAE,cAAc,CAAC;IACpB,UAAU,CAAC,EAAE,UAAU,CAAC;CACzB,KAAG,OAAO,CAAC,OAAO,CAAC,CAsDrB"}
1
+ {"version":3,"file":"service-routes.d.ts","sourceRoot":"","sources":["../../src/routes/service-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AACjE,OAAO,EAAE,IAAI,IAAI,WAAW,EAAE,KAAK,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAElE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAc,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AAE3D,MAAM,WAAW,iBAAiB;IAChC,eAAe,EAAE,eAAe,CAAC;IACjC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,uBAAuB,EAAE,gBAAgB,CAAC,CAAC;IACvE,IAAI,CAAC,EAAE,OAAO,WAAW,CAAC;CAC3B;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,OAAO,CAAC,iBAAiB,CAAC,SAOjD;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,MAAM,EAAE,eAAe,CAAC;IACxB,GAAG,EAAE,eAAe,CAAC;IACrB,GAAG,EAAE,cAAc,CAAC;IACpB,UAAU,CAAC,EAAE,UAAU,CAAC;CACzB,KAAG,OAAO,CAAC,OAAO,CAAC,CAqDrB"}
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/routes/service-routes.ts"],
4
- "sourcesContent": ["/**\n * routes/service-routes.ts \u2014 /api/sfmc/services/* \u5904\u7406\u5668\n *\n * \u7AEF\u70B9:\n * GET /api/sfmc/services \u2192 { services: [{name, moduleId}] }\n * GET /api/sfmc/services/:name?input=<urlencoded json>\n * \u2192 { ok: true, result } | { ok: false, error, code } (+ HTTP status)\n */\n\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport { json as defaultJson, type Method } from \"../lib/http.js\";\nimport { assertModulePermission, Perm, PermissionDeniedError } from \"../permission-gate.js\";\nimport type { ServiceRegistry } from \"../service-registry.js\";\nimport { jsonV2Fail, type ModuleAuth } from \"./_shared.js\";\n\nexport interface ServiceRoutesDeps {\n serviceRegistry: ServiceRegistry;\n enabled: Map<string, import(\"../manifest-loader.js\").ModuleManifestV2>;\n json?: typeof defaultJson;\n}\n\nexport function createServiceRoutes(depsIn: Partial<ServiceRoutesDeps>) {\n const deps = depsIn as Partial<ServiceRoutesDeps>;\n if (!deps.serviceRegistry || !deps.enabled) {\n throw new Error(\"createServiceRoutes: \u7F3A\u5C11 serviceRegistry / enabled map\");\n }\n const json = deps.json || defaultJson;\n\n return async (ctx: {\n path: string;\n method: Method | string;\n params: URLSearchParams;\n req: IncomingMessage;\n res: ServerResponse;\n moduleAuth?: ModuleAuth;\n }): Promise<boolean> => {\n const { path, method, params, res } = ctx;\n if (!path.startsWith(\"/api/sfmc/services\")) return false;\n if (method !== \"GET\") return false;\n\n const auth = ctx.moduleAuth ?? null;\n if (!auth) {\n // LSP: \u4E0E\u6210\u529F\u8DEF\u5F84\u540C\u4E00 ok \u65B9\u8A00(\u6587\u6863\u7EA6\u5B9A { ok:false, error, code })\n jsonV2Fail(res, \"unauthorized: module identity missing\", 401, \"unauthorized\");\n return true;\n }\n\n if (path === \"/api/sfmc/services\") {\n json(res, { services: deps.serviceRegistry!.list() });\n return true;\n }\n\n const m = path.match(/^\\/api\\/sfmc\\/services\\/([A-Za-z0-9_.]+)$/);\n if (m && m[1]) {\n const name = m[1];\n const rawInput = params.get(\"input\");\n let payload: unknown = {};\n if (rawInput) {\n try {\n payload = JSON.parse(rawInput);\n } catch (e) {\n jsonV2Fail(res, `invalid input json: ${(e as Error).message}`, 400, \"bad_request\");\n return true;\n }\n }\n try {\n // \u4E0E tx-runner.doService \u5BF9\u9F50(LSP):HTTP service.get \u4E0E tx.call \u5FC5\u987B\u540C\u5951\u7EA6 \u2014\n // \u65E2\u8981\u5728 services.requires \u91CC(\u7531 dispatch \u6821\u9A8C),\u4E5F\u8981\u6709 service:<name> \u6743\u9650\u3002\n assertModulePermission(auth.id, auth.permissions, Perm.service(name));\n const out = await deps.serviceRegistry!.dispatch(\n deps.enabled!,\n auth.id,\n name,\n payload\n );\n json(res, { ok: true, result: out.result });\n } catch (e) {\n if (e instanceof PermissionDeniedError) {\n jsonV2Fail(res, e.message, 403, \"permission_denied\");\n return true;\n }\n const err = e as { status?: number; code?: string; message: string };\n jsonV2Fail(res, err.message, err.status ?? 500, err.code ?? \"internal\");\n }\n return true;\n }\n\n return false;\n };\n}\n"],
5
- "mappings": "AAUA,SAAS,QAAQ,mBAAgC;AACjD,SAAS,wBAAwB,MAAM,6BAA6B;AAEpE,SAAS,kBAAmC;AAQrC,SAAS,oBAAoB,QAAoC;AACtE,QAAM,OAAO;AACb,MAAI,CAAC,KAAK,mBAAmB,CAAC,KAAK,SAAS;AAC1C,UAAM,IAAI,MAAM,iEAAuD;AAAA,EACzE;AACA,QAAM,OAAO,KAAK,QAAQ;AAE1B,SAAO,OAAO,QAOU;AACtB,UAAM,EAAE,MAAM,QAAQ,QAAQ,IAAI,IAAI;AACtC,QAAI,CAAC,KAAK,WAAW,oBAAoB,EAAG,QAAO;AACnD,QAAI,WAAW,MAAO,QAAO;AAE7B,UAAM,OAAO,IAAI,cAAc;AAC/B,QAAI,CAAC,MAAM;AAET,iBAAW,KAAK,yCAAyC,KAAK,cAAc;AAC5E,aAAO;AAAA,IACT;AAEA,QAAI,SAAS,sBAAsB;AACjC,WAAK,KAAK,EAAE,UAAU,KAAK,gBAAiB,KAAK,EAAE,CAAC;AACpD,aAAO;AAAA,IACT;AAEA,UAAM,IAAI,KAAK,MAAM,2CAA2C;AAChE,QAAI,KAAK,EAAE,CAAC,GAAG;AACb,YAAM,OAAO,EAAE,CAAC;AAChB,YAAM,WAAW,OAAO,IAAI,OAAO;AACnC,UAAI,UAAmB,CAAC;AACxB,UAAI,UAAU;AACZ,YAAI;AACF,oBAAU,KAAK,MAAM,QAAQ;AAAA,QAC/B,SAAS,GAAG;AACV,qBAAW,KAAK,uBAAwB,EAAY,OAAO,IAAI,KAAK,aAAa;AACjF,iBAAO;AAAA,QACT;AAAA,MACF;AACA,UAAI;AAGF,+BAAuB,KAAK,IAAI,KAAK,aAAa,KAAK,QAAQ,IAAI,CAAC;AACpE,cAAM,MAAM,MAAM,KAAK,gBAAiB;AAAA,UACtC,KAAK;AAAA,UACL,KAAK;AAAA,UACL;AAAA,UACA;AAAA,QACF;AACA,aAAK,KAAK,EAAE,IAAI,MAAM,QAAQ,IAAI,OAAO,CAAC;AAAA,MAC5C,SAAS,GAAG;AACV,YAAI,aAAa,uBAAuB;AACtC,qBAAW,KAAK,EAAE,SAAS,KAAK,mBAAmB;AACnD,iBAAO;AAAA,QACT;AACA,cAAM,MAAM;AACZ,mBAAW,KAAK,IAAI,SAAS,IAAI,UAAU,KAAK,IAAI,QAAQ,UAAU;AAAA,MACxE;AACA,aAAO;AAAA,IACT;AAEA,WAAO;AAAA,EACT;AACF;",
4
+ "sourcesContent": ["/**\n * routes/service-routes.ts \u2014 /api/sfmc/services/* \u5904\u7406\u5668\n *\n * \u7AEF\u70B9:\n * GET /api/sfmc/services \u2192 { services: [{name, moduleId}] }\n * GET /api/sfmc/services/:name?input=<urlencoded json>\n * \u2192 { ok: true, result } | { ok: false, error, code } (+ HTTP status)\n */\n\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport { json as defaultJson, type Method } from \"../lib/http.js\";\nimport { assertModulePermission, Perm, PermissionDeniedError } from \"../permission-gate.js\";\nimport type { ServiceRegistry } from \"../service-registry.js\";\nimport { jsonV2Fail, type ModuleAuth } from \"./_shared.js\";\n\nexport interface ServiceRoutesDeps {\n serviceRegistry: ServiceRegistry;\n enabled: Map<string, import(\"../manifest-loader.js\").ModuleManifestV2>;\n json?: typeof defaultJson;\n}\n\n/**\n * \u521B\u5EFA `/api/sfmc/services/*` \u8DE8\u6A21\u5757\u670D\u52A1\u8DEF\u7531\u8BF7\u6C42\u5904\u7406\u5668\u3002\n *\n * @param depsIn \u4F9D\u8D56\u6CE8\u5165\u9879\uFF08\u5305\u542B serviceRegistry\u3001enabled \u6A21\u5757\u5B57\u5178\u7B49\uFF09\u3002\n * @returns \u5F02\u6B65\u8DEF\u7531\u5904\u7406\u51FD\u6570\u3002\n */\nexport function createServiceRoutes(depsIn: Partial<ServiceRoutesDeps>) {\n const deps = depsIn as Partial<ServiceRoutesDeps>;\n if (!deps.serviceRegistry || !deps.enabled) {\n throw new Error(\"createServiceRoutes: \u7F3A\u5C11 serviceRegistry / enabled map\");\n }\n const json = deps.json || defaultJson;\n\n return async (ctx: {\n path: string;\n method: Method | string;\n params: URLSearchParams;\n req: IncomingMessage;\n res: ServerResponse;\n moduleAuth?: ModuleAuth;\n }): Promise<boolean> => {\n const { path, method, params, res } = ctx;\n if (!path.startsWith(\"/api/sfmc/services\")) return false;\n if (method !== \"GET\") return false;\n\n const auth = ctx.moduleAuth ?? null;\n if (!auth) {\n jsonV2Fail(res, \"unauthorized: module identity missing\", 401, \"unauthorized\");\n return true;\n }\n\n if (path === \"/api/sfmc/services\") {\n json(res, { services: deps.serviceRegistry!.list() });\n return true;\n }\n\n const m = path.match(/^\\/api\\/sfmc\\/services\\/([A-Za-z0-9_.]+)$/);\n if (m && m[1]) {\n const name = m[1];\n const rawInput = params.get(\"input\");\n let payload: unknown = {};\n if (rawInput) {\n try {\n payload = JSON.parse(rawInput);\n } catch (e) {\n jsonV2Fail(res, `invalid input json: ${(e as Error).message}`, 400, \"bad_request\");\n return true;\n }\n }\n try {\n // \u4E0E tx-runner \u4FDD\u6301\u5951\u7EA6\u4E00\u81F4\uFF1AHTTP GET \u4E0E tx.call \u5747\u987B\u58F0\u660E\u5728 services.requires \u4E2D\u4E14\u5177\u5907\u5BF9\u5E94\u6743\u9650\n assertModulePermission(auth.id, auth.permissions, Perm.service(name));\n const out = await deps.serviceRegistry!.dispatch(\n deps.enabled!,\n auth.id,\n name,\n payload\n );\n\n json(res, { ok: true, result: out.result });\n } catch (e) {\n if (e instanceof PermissionDeniedError) {\n jsonV2Fail(res, e.message, 403, \"permission_denied\");\n return true;\n }\n const err = e as { status?: number; code?: string; message: string };\n jsonV2Fail(res, err.message, err.status ?? 500, err.code ?? \"internal\");\n }\n return true;\n }\n\n return false;\n };\n}\n"],
5
+ "mappings": "AAUA,SAAS,QAAQ,mBAAgC;AACjD,SAAS,wBAAwB,MAAM,6BAA6B;AAEpE,SAAS,kBAAmC;AAcrC,SAAS,oBAAoB,QAAoC;AACtE,QAAM,OAAO;AACb,MAAI,CAAC,KAAK,mBAAmB,CAAC,KAAK,SAAS;AAC1C,UAAM,IAAI,MAAM,iEAAuD;AAAA,EACzE;AACA,QAAM,OAAO,KAAK,QAAQ;AAE1B,SAAO,OAAO,QAOU;AACtB,UAAM,EAAE,MAAM,QAAQ,QAAQ,IAAI,IAAI;AACtC,QAAI,CAAC,KAAK,WAAW,oBAAoB,EAAG,QAAO;AACnD,QAAI,WAAW,MAAO,QAAO;AAE7B,UAAM,OAAO,IAAI,cAAc;AAC/B,QAAI,CAAC,MAAM;AACT,iBAAW,KAAK,yCAAyC,KAAK,cAAc;AAC5E,aAAO;AAAA,IACT;AAEA,QAAI,SAAS,sBAAsB;AACjC,WAAK,KAAK,EAAE,UAAU,KAAK,gBAAiB,KAAK,EAAE,CAAC;AACpD,aAAO;AAAA,IACT;AAEA,UAAM,IAAI,KAAK,MAAM,2CAA2C;AAChE,QAAI,KAAK,EAAE,CAAC,GAAG;AACb,YAAM,OAAO,EAAE,CAAC;AAChB,YAAM,WAAW,OAAO,IAAI,OAAO;AACnC,UAAI,UAAmB,CAAC;AACxB,UAAI,UAAU;AACZ,YAAI;AACF,oBAAU,KAAK,MAAM,QAAQ;AAAA,QAC/B,SAAS,GAAG;AACV,qBAAW,KAAK,uBAAwB,EAAY,OAAO,IAAI,KAAK,aAAa;AACjF,iBAAO;AAAA,QACT;AAAA,MACF;AACA,UAAI;AAEF,+BAAuB,KAAK,IAAI,KAAK,aAAa,KAAK,QAAQ,IAAI,CAAC;AACpE,cAAM,MAAM,MAAM,KAAK,gBAAiB;AAAA,UACtC,KAAK;AAAA,UACL,KAAK;AAAA,UACL;AAAA,UACA;AAAA,QACF;AAEA,aAAK,KAAK,EAAE,IAAI,MAAM,QAAQ,IAAI,OAAO,CAAC;AAAA,MAC5C,SAAS,GAAG;AACV,YAAI,aAAa,uBAAuB;AACtC,qBAAW,KAAK,EAAE,SAAS,KAAK,mBAAmB;AACnD,iBAAO;AAAA,QACT;AACA,cAAM,MAAM;AACZ,mBAAW,KAAK,IAAI,SAAS,IAAI,UAAU,KAAK,IAAI,QAAQ,UAAU;AAAA,MACxE;AACA,aAAO;AAAA,IACT;AAEA,WAAO;AAAA,EACT;AACF;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * routes/status.ts — 只读运维摘要(供 QQ status/online)
3
+ *
4
+ * GET /api/sfmc/status — 公开,无需 Bearer
5
+ */
6
+ import type { QueryFn } from "../lib/sqlite.js";
7
+ import { type SystemStatusSnapshot } from "../domain/system-status.js";
8
+ import { type RouteFactory } from "./_shared.js";
9
+ /** 玩家行新鲜度阈值:超过则视为离线快照过期 */
10
+ declare const FRESH_MS: number;
11
+ interface Deps {
12
+ query: QueryFn;
13
+ /** 测试可注入,避免真实探活 */
14
+ collectSystem?: (projectRoot: string) => Promise<SystemStatusSnapshot>;
15
+ projectRoot?: string;
16
+ }
17
+ declare function createStatusRoutes({ query, collectSystem, projectRoot }: Deps): ReturnType<RouteFactory>;
18
+ export { createStatusRoutes, FRESH_MS };
19
+ //# sourceMappingURL=status.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/routes/status.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAEhD,OAAO,EAAuB,KAAK,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAC5F,OAAO,EAAQ,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAEvD,2BAA2B;AAC3B,QAAA,MAAM,QAAQ,QAAgB,CAAC;AAE/B,UAAU,IAAI;IACZ,KAAK,EAAE,OAAO,CAAC;IACf,mBAAmB;IACnB,aAAa,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,oBAAoB,CAAC,CAAC;IACvE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,iBAAS,kBAAkB,CAAC,EAAE,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,EAAE,IAAI,GAAG,UAAU,CAAC,YAAY,CAAC,CA0EjG;AAED,OAAO,EAAE,kBAAkB,EAAE,QAAQ,EAAE,CAAC"}
@@ -0,0 +1,74 @@
1
+ import { SQL } from "sql-template-strings";
2
+ import { PROJECT_ROOT } from "../project-root.js";
3
+ import { collectSystemStatus } from "../domain/system-status.js";
4
+ import { json } from "./_shared.js";
5
+ const FRESH_MS = 5 * 60 * 1e3;
6
+ function createStatusRoutes({ query, collectSystem, projectRoot }) {
7
+ const root = projectRoot ?? PROJECT_ROOT;
8
+ const collect = collectSystem ?? collectSystemStatus;
9
+ return async function handle({ path, method, res }) {
10
+ if (path !== "/api/sfmc/status") return false;
11
+ if (method !== "GET") {
12
+ json(res, { success: false, error: "not_found" }, 404);
13
+ return true;
14
+ }
15
+ const now = Date.now();
16
+ let world = null;
17
+ let worldUpdatedAt = null;
18
+ try {
19
+ const worlds = query(SQL`SELECT * FROM sfmc_world LIMIT 1`);
20
+ if (worlds.length > 0 && worlds[0]) {
21
+ world = {
22
+ day: worlds[0].day ?? null,
23
+ difficulty: worlds[0].difficulty ?? null,
24
+ absolute_time: worlds[0].absolute_time ?? null,
25
+ moon_phase: worlds[0].moon_phase ?? null
26
+ };
27
+ worldUpdatedAt = worlds[0].updated_at ?? null;
28
+ }
29
+ } catch {
30
+ world = null;
31
+ }
32
+ let players = [];
33
+ try {
34
+ const rows = query(
35
+ SQL`SELECT id, name, updated_at FROM sfmc_players ORDER BY updated_at DESC LIMIT 64`
36
+ );
37
+ players = rows.map((r) => ({
38
+ id: String(r.id ?? ""),
39
+ name: String(r.name ?? ""),
40
+ updated_at: Number(r.updated_at) || 0
41
+ }));
42
+ } catch {
43
+ players = [];
44
+ }
45
+ const fresh = players.filter((p) => p.updated_at > 0 && now - p.updated_at <= FRESH_MS);
46
+ const online = fresh.map((p) => ({ id: p.id, name: p.name }));
47
+ const newestPlayer = players[0]?.updated_at ?? 0;
48
+ const updatedAt = Math.max(newestPlayer, typeof worldUpdatedAt === "number" ? worldUpdatedAt : 0) || now;
49
+ let system = null;
50
+ try {
51
+ system = await collect(root);
52
+ } catch {
53
+ system = null;
54
+ }
55
+ json(res, {
56
+ online,
57
+ world,
58
+ updatedAt,
59
+ host: system?.host ?? null,
60
+ processes: system ? {
61
+ db: system.db,
62
+ bds: system.bds
63
+ } : null,
64
+ source: "sfmc_players/sfmc_world+os",
65
+ note: online.length === 0 ? "\u6682\u65E0\u65B0\u9C9C\u5728\u7EBF\u6570\u636E\uFF08\u9700 BDS \u540C\u6B65\u73A9\u5BB6\u8868\uFF0C\u6216\u6570\u636E\u8D85\u8FC7 5 \u5206\u949F\uFF09" : void 0
66
+ });
67
+ return true;
68
+ };
69
+ }
70
+ export {
71
+ FRESH_MS,
72
+ createStatusRoutes
73
+ };
74
+ //# sourceMappingURL=status.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/routes/status.ts"],
4
+ "sourcesContent": ["/**\n * routes/status.ts \u2014 \u53EA\u8BFB\u8FD0\u7EF4\u6458\u8981\uFF08\u4F9B QQ status/online\uFF09\n *\n * GET /api/sfmc/status \u2014 \u516C\u5F00\uFF0C\u65E0\u9700 Bearer\n */\n\nimport { SQL } from \"sql-template-strings\";\nimport type { QueryFn } from \"../lib/sqlite.js\";\nimport { PROJECT_ROOT } from \"../project-root.js\";\nimport { collectSystemStatus, type SystemStatusSnapshot } from \"../domain/system-status.js\";\nimport { json, type RouteFactory } from \"./_shared.js\";\n\n/** \u73A9\u5BB6\u884C\u65B0\u9C9C\u5EA6\u9608\u503C\uFF1A\u8D85\u8FC7\u5219\u89C6\u4E3A\u79BB\u7EBF\u5FEB\u7167\u8FC7\u671F */\nconst FRESH_MS = 5 * 60 * 1000;\n\ninterface Deps {\n query: QueryFn;\n /** \u6D4B\u8BD5\u53EF\u6CE8\u5165\uFF0C\u907F\u514D\u771F\u5B9E\u63A2\u6D3B */\n collectSystem?: (projectRoot: string) => Promise<SystemStatusSnapshot>;\n projectRoot?: string;\n}\n\nfunction createStatusRoutes({ query, collectSystem, projectRoot }: Deps): ReturnType<RouteFactory> {\n const root = projectRoot ?? PROJECT_ROOT;\n const collect = collectSystem ?? collectSystemStatus;\n\n return async function handle({ path, method, res }): Promise<boolean> {\n if (path !== \"/api/sfmc/status\") return false;\n if (method !== \"GET\") {\n json(res, { success: false, error: \"not_found\" }, 404);\n return true;\n }\n\n const now = Date.now();\n let world: Record<string, unknown> | null = null;\n let worldUpdatedAt: string | number | null = null;\n try {\n const worlds = query(SQL`SELECT * FROM sfmc_world LIMIT 1`) as Array<Record<string, unknown>>;\n if (worlds.length > 0 && worlds[0]) {\n world = {\n day: worlds[0].day ?? null,\n difficulty: worlds[0].difficulty ?? null,\n absolute_time: worlds[0].absolute_time ?? null,\n moon_phase: worlds[0].moon_phase ?? null,\n };\n worldUpdatedAt = (worlds[0].updated_at as string | number | null) ?? null;\n }\n } catch {\n world = null;\n }\n\n let players: Array<{ id: string; name: string; updated_at: number }> = [];\n try {\n const rows = query(\n SQL`SELECT id, name, updated_at FROM sfmc_players ORDER BY updated_at DESC LIMIT 64`\n ) as Array<{ id: string; name: string; updated_at: number }>;\n players = rows.map((r) => ({\n id: String(r.id ?? \"\"),\n name: String(r.name ?? \"\"),\n updated_at: Number(r.updated_at) || 0,\n }));\n } catch {\n players = [];\n }\n\n const fresh = players.filter((p) => p.updated_at > 0 && now - p.updated_at <= FRESH_MS);\n const online = fresh.map((p) => ({ id: p.id, name: p.name }));\n const newestPlayer = players[0]?.updated_at ?? 0;\n const updatedAt = Math.max(newestPlayer, typeof worldUpdatedAt === \"number\" ? worldUpdatedAt : 0) || now;\n\n let system: SystemStatusSnapshot | null = null;\n try {\n system = await collect(root);\n } catch {\n system = null;\n }\n\n json(res, {\n online,\n world,\n updatedAt,\n host: system?.host ?? null,\n processes: system\n ? {\n db: system.db,\n bds: system.bds,\n }\n : null,\n source: \"sfmc_players/sfmc_world+os\",\n note:\n online.length === 0\n ? \"\u6682\u65E0\u65B0\u9C9C\u5728\u7EBF\u6570\u636E\uFF08\u9700 BDS \u540C\u6B65\u73A9\u5BB6\u8868\uFF0C\u6216\u6570\u636E\u8D85\u8FC7 5 \u5206\u949F\uFF09\"\n : undefined,\n });\n return true;\n };\n}\n\nexport { createStatusRoutes, FRESH_MS };\n"],
5
+ "mappings": "AAMA,SAAS,WAAW;AAEpB,SAAS,oBAAoB;AAC7B,SAAS,2BAAsD;AAC/D,SAAS,YAA+B;AAGxC,MAAM,WAAW,IAAI,KAAK;AAS1B,SAAS,mBAAmB,EAAE,OAAO,eAAe,YAAY,GAAmC;AACjG,QAAM,OAAO,eAAe;AAC5B,QAAM,UAAU,iBAAiB;AAEjC,SAAO,eAAe,OAAO,EAAE,MAAM,QAAQ,IAAI,GAAqB;AACpE,QAAI,SAAS,mBAAoB,QAAO;AACxC,QAAI,WAAW,OAAO;AACpB,WAAK,KAAK,EAAE,SAAS,OAAO,OAAO,YAAY,GAAG,GAAG;AACrD,aAAO;AAAA,IACT;AAEA,UAAM,MAAM,KAAK,IAAI;AACrB,QAAI,QAAwC;AAC5C,QAAI,iBAAyC;AAC7C,QAAI;AACF,YAAM,SAAS,MAAM,qCAAqC;AAC1D,UAAI,OAAO,SAAS,KAAK,OAAO,CAAC,GAAG;AAClC,gBAAQ;AAAA,UACN,KAAK,OAAO,CAAC,EAAE,OAAO;AAAA,UACtB,YAAY,OAAO,CAAC,EAAE,cAAc;AAAA,UACpC,eAAe,OAAO,CAAC,EAAE,iBAAiB;AAAA,UAC1C,YAAY,OAAO,CAAC,EAAE,cAAc;AAAA,QACtC;AACA,yBAAkB,OAAO,CAAC,EAAE,cAAyC;AAAA,MACvE;AAAA,IACF,QAAQ;AACN,cAAQ;AAAA,IACV;AAEA,QAAI,UAAmE,CAAC;AACxE,QAAI;AACF,YAAM,OAAO;AAAA,QACX;AAAA,MACF;AACA,gBAAU,KAAK,IAAI,CAAC,OAAO;AAAA,QACzB,IAAI,OAAO,EAAE,MAAM,EAAE;AAAA,QACrB,MAAM,OAAO,EAAE,QAAQ,EAAE;AAAA,QACzB,YAAY,OAAO,EAAE,UAAU,KAAK;AAAA,MACtC,EAAE;AAAA,IACJ,QAAQ;AACN,gBAAU,CAAC;AAAA,IACb;AAEA,UAAM,QAAQ,QAAQ,OAAO,CAAC,MAAM,EAAE,aAAa,KAAK,MAAM,EAAE,cAAc,QAAQ;AACtF,UAAM,SAAS,MAAM,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,MAAM,EAAE,KAAK,EAAE;AAC5D,UAAM,eAAe,QAAQ,CAAC,GAAG,cAAc;AAC/C,UAAM,YAAY,KAAK,IAAI,cAAc,OAAO,mBAAmB,WAAW,iBAAiB,CAAC,KAAK;AAErG,QAAI,SAAsC;AAC1C,QAAI;AACF,eAAS,MAAM,QAAQ,IAAI;AAAA,IAC7B,QAAQ;AACN,eAAS;AAAA,IACX;AAEA,SAAK,KAAK;AAAA,MACR;AAAA,MACA;AAAA,MACA;AAAA,MACA,MAAM,QAAQ,QAAQ;AAAA,MACtB,WAAW,SACP;AAAA,QACE,IAAI,OAAO;AAAA,QACX,KAAK,OAAO;AAAA,MACd,IACA;AAAA,MACJ,QAAQ;AAAA,MACR,MACE,OAAO,WAAW,IACd,6JACA;AAAA,IACR,CAAC;AACD,WAAO;AAAA,EACT;AACF;",
6
+ "names": []
7
+ }
@@ -1,20 +1,15 @@
1
1
  /**
2
- * schema-registry.ts — 收集模块 defineTable,统一建表
2
+ * schema-registry.ts — 动态数据表结构注册与创建管理器
3
3
  *
4
- * 协议:
5
- * - 模块 SAPI 启动时调 db.defineTable(name, columns, {softDelete})
6
- * POST /api/sfmc/db/define-table {moduleId, name, columns, softDelete}
7
- * - schema-registry 收集所有调用,CAS
8
- * - 等所有"已注册模块"都调过(超时 fallback 或 explicit `finalize()`),
9
- * 一次性 CREATE TABLE IF NOT EXISTS + CREATE INDEX IF NOT EXISTS
10
- * - 同一 module 重复 define 同名表 = 幂等(同名 columns 允许)
11
- * - 不同 module 抢同名表 = 启动失败
12
- * - 平台表(moduleId = "__platform__")由 db-tables.ts 在 initSchema 直接 bootstrap
4
+ * 协议与流程:
5
+ * 1. 模块启动时调用 `db.defineTable(name, columns, { softDelete })` 向服务端注册表结构定义。
6
+ * 2. `SchemaRegistry` 校验列定义合法性、外键引用与主键唯一性。
7
+ * 3. 校验通过后自动执行 `CREATE TABLE IF NOT EXISTS` 及索引创建语句。
13
8
  *
14
- * 列类型白名单:
15
- * text / integer / real / blob
16
- * 修饰: primary / notNull / unique / default / index
17
- * softDelete = 自动加 _deleted_at INTEGER(列名带下划线) _version INTEGER
9
+ * 冲突与隔离控制:
10
+ * - 相同模块重复定义同名表:具备幂等性(要求列结构完全一致)
11
+ * - 不同模块定义相同表名:抛出冲突异常拒绝注册
12
+ * - 软删除支持:开启 `softDelete`(默认开启)时自动在末尾统一追加 `_deleted_at` 与乐观锁 `_version`
18
13
  */
19
14
  import type { DatabaseSync } from "node:sqlite";
20
15
  export type ColumnType = "text" | "integer" | "real" | "blob";
@@ -44,7 +39,13 @@ export declare class SchemaRegistry {
44
39
  private readonly tables;
45
40
  private finalized;
46
41
  constructor(db: DatabaseSync);
47
- /** 模块 SAPI 启动时调。 */
42
+ /**
43
+ * 注册并创建模块数据表结构。
44
+ *
45
+ * @param moduleId 注册模块唯一标识符。
46
+ * @param req 表结构定义请求对象。
47
+ * @returns 建表结果与创建的索引列表。
48
+ */
48
49
  define(moduleId: string, req: DefineTableRequest): DefineTableResult;
49
50
  /** 幂等地物理建表 + 建索引(供 define / finalize 共用)。 */
50
51
  private createPhysical;
@@ -1 +1 @@
1
- {"version":3,"file":"schema-registry.d.ts","sourceRoot":"","sources":["../src/schema-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGhD,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;AAE9D,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,UAAU,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC;AAEzD,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACnC,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AASD,eAAO,MAAM,kBAAkB,iBAAiB,CAAC;AA6DjD,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAe;IAClC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAmC;IAC1D,OAAO,CAAC,SAAS,CAAS;IAE1B,YAAY,EAAE,EAAE,YAAY,EAE3B;IAED,oBAAoB;IACpB,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,kBAAkB,GAAG,iBAAiB,CAsCnE;IAED,6CAA6C;IAC7C,OAAO,CAAC,cAAc;IAWtB;;;OAGG;IACH,QAAQ,IAAI;QAAE,UAAU,EAAE,MAAM,EAAE,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE,CA0BtD;IAED,UAAU,IAAI,MAAM,EAAE,CAErB;IAED,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAElC;IAED,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAEzC;CACF"}
1
+ {"version":3,"file":"schema-registry.d.ts","sourceRoot":"","sources":["../src/schema-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGhD,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;AAE9D,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,UAAU,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC;AAEzD,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACnC,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AASD,eAAO,MAAM,kBAAkB,iBAAiB,CAAC;AAkEjD,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAe;IAClC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAmC;IAC1D,OAAO,CAAC,SAAS,CAAS;IAE1B,YAAY,EAAE,EAAE,YAAY,EAE3B;IAED;;;;;;OAMG;IACH,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,kBAAkB,GAAG,iBAAiB,CAuCnE;IAED,6CAA6C;IAC7C,OAAO,CAAC,cAAc;IAWtB;;;OAGG;IACH,QAAQ,IAAI;QAAE,UAAU,EAAE,MAAM,EAAE,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE,CA0BtD;IAED,UAAU,IAAI,MAAM,EAAE,CAErB;IAED,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAElC;IAED,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAEzC;CACF"}
@@ -52,7 +52,13 @@ class SchemaRegistry {
52
52
  constructor(db) {
53
53
  this.db = db;
54
54
  }
55
- /** 模块 SAPI 启动时调。 */
55
+ /**
56
+ * 注册并创建模块数据表结构。
57
+ *
58
+ * @param moduleId 注册模块唯一标识符。
59
+ * @param req 表结构定义请求对象。
60
+ * @returns 建表结果与创建的索引列表。
61
+ */
56
62
  define(moduleId, req) {
57
63
  if (this.finalized) {
58
64
  throw new Error(`[schema] registry \u5DF2 finalize,\u6A21\u5757 ${moduleId} \u4E0D\u80FD\u518D defineTable`);