@sfmc-bds/db-server 0.5.3 → 0.6.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 (55) hide show
  1. package/dist/domain/bind-providers.d.ts +59 -0
  2. package/dist/domain/bind-providers.d.ts.map +1 -0
  3. package/dist/domain/bind-providers.js +33 -0
  4. package/dist/domain/bind-providers.js.map +7 -0
  5. package/dist/domain/metrics.d.ts +5 -0
  6. package/dist/domain/metrics.d.ts.map +1 -1
  7. package/dist/domain/metrics.js +28 -2
  8. package/dist/domain/metrics.js.map +2 -2
  9. package/dist/domain/player-level.d.ts +29 -0
  10. package/dist/domain/player-level.d.ts.map +1 -0
  11. package/dist/domain/player-level.js +30 -0
  12. package/dist/domain/player-level.js.map +7 -0
  13. package/dist/domain/process-resources.d.ts +12 -0
  14. package/dist/domain/process-resources.d.ts.map +1 -1
  15. package/dist/domain/process-resources.js +72 -1
  16. package/dist/domain/process-resources.js.map +2 -2
  17. package/dist/domain/qq-bind.d.ts +21 -0
  18. package/dist/domain/qq-bind.d.ts.map +1 -0
  19. package/dist/domain/qq-bind.js +54 -0
  20. package/dist/domain/qq-bind.js.map +7 -0
  21. package/dist/domain/schema.d.ts +1 -0
  22. package/dist/domain/schema.d.ts.map +1 -1
  23. package/dist/domain/schema.js +26 -0
  24. package/dist/domain/schema.js.map +2 -2
  25. package/dist/domain/tracked-processes.d.ts +30 -0
  26. package/dist/domain/tracked-processes.d.ts.map +1 -0
  27. package/dist/domain/tracked-processes.js +78 -0
  28. package/dist/domain/tracked-processes.js.map +7 -0
  29. package/dist/domain/wechat-bind.d.ts +56 -0
  30. package/dist/domain/wechat-bind.d.ts.map +1 -0
  31. package/dist/domain/wechat-bind.js +84 -0
  32. package/dist/domain/wechat-bind.js.map +7 -0
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +37 -7
  35. package/dist/index.js.map +2 -2
  36. package/dist/lib/sqlite.d.ts +10 -0
  37. package/dist/lib/sqlite.d.ts.map +1 -1
  38. package/dist/lib/sqlite.js +17 -2
  39. package/dist/lib/sqlite.js.map +2 -2
  40. package/dist/routes/bind-confirm.d.ts +30 -0
  41. package/dist/routes/bind-confirm.d.ts.map +1 -0
  42. package/dist/routes/bind-confirm.js +27 -0
  43. package/dist/routes/bind-confirm.js.map +7 -0
  44. package/dist/routes/qq-bind.d.ts +3 -1
  45. package/dist/routes/qq-bind.d.ts.map +1 -1
  46. package/dist/routes/qq-bind.js +8 -64
  47. package/dist/routes/qq-bind.js.map +2 -2
  48. package/dist/routes/status.d.ts.map +1 -1
  49. package/dist/routes/status.js +10 -2
  50. package/dist/routes/status.js.map +2 -2
  51. package/dist/routes/wechat-bind.d.ts +39 -0
  52. package/dist/routes/wechat-bind.d.ts.map +1 -0
  53. package/dist/routes/wechat-bind.js +56 -0
  54. package/dist/routes/wechat-bind.js.map +7 -0
  55. package/package.json +5 -4
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/domain/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAmJjD"}
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/domain/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAuKjD"}
@@ -160,6 +160,32 @@ function initSchema(db) {
160
160
  `
161
161
  CREATE INDEX IF NOT EXISTS idx_qq_admin_actions_status ON sfmc_qq_admin_actions(status, created_at)`
162
162
  );
163
+ db.exec(
164
+ /* sql */
165
+ `
166
+ CREATE TABLE IF NOT EXISTS sfmc_wechat_bind_pending (
167
+ code TEXT PRIMARY KEY NOT NULL,
168
+ wechat_openid TEXT NOT NULL,
169
+ created_at INTEGER NOT NULL,
170
+ expires_at INTEGER NOT NULL
171
+ )`
172
+ );
173
+ db.exec(
174
+ /* sql */
175
+ `
176
+ CREATE TABLE IF NOT EXISTS sfmc_wechat_bindings (
177
+ wechat_openid TEXT PRIMARY KEY NOT NULL,
178
+ player_xuid TEXT NOT NULL UNIQUE,
179
+ player_name TEXT NOT NULL DEFAULT '',
180
+ bound_at INTEGER NOT NULL
181
+ )`
182
+ );
183
+ db.exec(
184
+ /* sql */
185
+ `
186
+ CREATE INDEX IF NOT EXISTS idx_wechat_bind_pending_openid ON sfmc_wechat_bind_pending(wechat_openid);
187
+ CREATE INDEX IF NOT EXISTS idx_wechat_bindings_xuid ON sfmc_wechat_bindings(player_xuid)`
188
+ );
163
189
  db.exec(
164
190
  /* sql */
165
191
  `
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/domain/schema.ts"],
4
- "sourcesContent": ["/**\n * domain/schema.ts \u2014 \u6570\u636E\u5E93 Schema \u521D\u59CB\u5316\u5B9A\u4E49\uFF08\u5E73\u53F0\u6838\u5FC3\u5E95\u5EA7\u8868\uFF09\n *\n * \u96C6\u4E2D\u7BA1\u7406\u5E73\u53F0\u6838\u5FC3\u6570\u636E\u8868\u7684 CREATE TABLE IF NOT EXISTS \u4E0E CREATE INDEX IF NOT EXISTS DDL \u8BED\u53E5\u3002\n * \u8C03\u7528\u65B9\u5F0F\uFF1AinitSchema(db) \u2014\u2014 \u5728 openDatabase \u4E4B\u540E\u3001\u4E1A\u52A1\u67E5\u8BE2\u542F\u52A8\u524D\u6267\u884C\u3002\n *\n * \u67B6\u6784\u7EA6\u5B9A\uFF1A\n * \u4E1A\u52A1\u6A21\u5757\uFF08activity-log, coop, land, chat, economy \u7B49\uFF09\u79C1\u6709\u6570\u636E\u8868\u5747\u7531\u5404\u81EA\u6A21\u5757\u5728 SAPI \u8FD0\u884C\u671F\u901A\u8FC7\n * db.defineTable(...) \u81EA\u4E3B\u58F0\u660E\u4E0E\u7EF4\u62A4\uFF0C\u4E0D\u5728\u6B64\u5904\u9759\u6001\u786C\u7F16\u7801\uFF0C\u907F\u514D\u5217\u5B9A\u4E49\u4E0E\u7D22\u5F15\u51B2\u7A81\u3002\n *\n * \u6B64\u5904\u4EC5\u5B9A\u4E49\u5E73\u53F0\u6838\u5FC3\u8FD0\u884C\u3001\u8DE8\u670D\u52A1\u5171\u4EAB\uFF08qq-bridge / HTTP \u7AEF\u70B9 / status \u63A2\u6D3B\uFF09\u7684\u57FA\u7840\u5E95\u5EA7\u8868\uFF1A\n * 1. \u4E16\u754C\u4E0E\u73A9\u5BB6\u8FD0\u884C\u671F\u72B6\u6001\u5FEB\u7167\uFF08sfmc_world, sfmc_players\uFF09\n * 2. \u8DE8\u8FDB\u7A0B\u804A\u5929\u6D88\u606F\u805A\u5408\uFF08sfmc_chat_messages\uFF09\n * 3. QQ \u8EAB\u4EFD\u7ED1\u5B9A\u4E0E\u5BA1\u6279\u961F\u5217\uFF08sfmc_qq_*\uFF09\n */\n\nimport type { DatabaseSync } from \"node:sqlite\";\n\n/**\n * \u521D\u59CB\u5316\u6570\u636E\u5E93\u7ED3\u6784\uFF0C\u521B\u5EFA\u5E73\u53F0\u6838\u5FC3\u5E95\u5EA7\u8868\u4E0E\u7D22\u5F15\u3002\n *\n * @param db SQLite \u6570\u636E\u5E93\u8FDE\u63A5\u5B9E\u4F8B\u3002\n */\nexport function initSchema(db: DatabaseSync): void {\n // (1) \u4E16\u754C\u8FD0\u884C\u671F\u72B6\u6001\uFF08\u4F9B status \u8DEF\u7531\u4E0E qq-bridge \u8BFB\u53D6\uFF0C\u5B57\u6BB5\u4E0E data-backup \u517C\u5BB9\uFF09\n db.exec(/* sql */ `\n CREATE TABLE IF NOT EXISTS sfmc_world (\n id TEXT PRIMARY KEY,\n seed TEXT DEFAULT '',\n difficulty TEXT DEFAULT '',\n spawn_x INTEGER DEFAULT 0,\n spawn_y INTEGER DEFAULT 0,\n spawn_z INTEGER DEFAULT 0,\n gamerules TEXT DEFAULT '{}',\n day INTEGER DEFAULT 0,\n absolute_time INTEGER DEFAULT 0,\n moon_phase INTEGER DEFAULT 0,\n updated_at INTEGER DEFAULT 0\n )`);\n\n // (2) \u73A9\u5BB6\u8FD0\u884C\u671F\u72B6\u6001\uFF08\u4F9B status \u8DEF\u7531\u4E0E qq-bridge \u8BFB\u53D6\uFF0C\u5B57\u6BB5\u4E0E data-backup \u517C\u5BB9\uFF09\n db.exec(/* sql */ `\n CREATE TABLE IF NOT EXISTS sfmc_players (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL DEFAULT '',\n xuid TEXT DEFAULT '',\n last_online INTEGER DEFAULT 0,\n level INTEGER DEFAULT 0,\n total_xp INTEGER DEFAULT 0,\n tags TEXT DEFAULT '',\n spawn_dimension TEXT DEFAULT '',\n spawn_x REAL,\n spawn_y REAL,\n spawn_z REAL,\n dimension TEXT DEFAULT '',\n x REAL,\n y REAL,\n z REAL,\n game_mode TEXT DEFAULT '',\n snapshot_hash TEXT DEFAULT '',\n active_channel TEXT DEFAULT '',\n subscribed_channels TEXT DEFAULT '[]',\n updated_at INTEGER DEFAULT 0\n )`);\n const playerColumns = new Map(\n (db.prepare(`PRAGMA table_info(\"sfmc_players\")`).all() as Array<{ name: string }>).map((column) => [\n column.name,\n true,\n ])\n );\n const missingPlayerColumns: Array<[string, string]> = [\n [\"spawn_dimension\", \"TEXT DEFAULT ''\"],\n [\"spawn_x\", \"REAL\"],\n [\"spawn_y\", \"REAL\"],\n [\"spawn_z\", \"REAL\"],\n [\"dimension\", \"TEXT DEFAULT ''\"],\n [\"x\", \"REAL\"],\n [\"y\", \"REAL\"],\n [\"z\", \"REAL\"],\n [\"game_mode\", \"TEXT DEFAULT ''\"],\n [\"snapshot_hash\", \"TEXT DEFAULT ''\"],\n [\"active_channel\", \"TEXT DEFAULT ''\"],\n [\"subscribed_channels\", \"TEXT DEFAULT '[]'\"],\n ];\n for (const [name, definition] of missingPlayerColumns) {\n if (!playerColumns.has(name)) db.exec(`ALTER TABLE sfmc_players ADD COLUMN \"${name}\" ${definition}`);\n }\n db.exec(/* sql */ `\n CREATE INDEX IF NOT EXISTS idx_players_name ON sfmc_players(name)`);\n\n // (3) \u804A\u5929\u6D88\u606F\u805A\u5408\uFF08\u4F9B routes/messages \u4E0E qq-bridge \u53CC\u5411\u540C\u6B65\uFF09\n db.exec(/* sql */ `\n CREATE TABLE IF NOT EXISTS sfmc_chat_messages (\n id TEXT PRIMARY KEY NOT NULL,\n channel_id TEXT NOT NULL DEFAULT '',\n from_id TEXT NOT NULL DEFAULT '',\n from_name TEXT NOT NULL DEFAULT '',\n type TEXT NOT NULL DEFAULT 'text',\n content TEXT NOT NULL DEFAULT '',\n attachment TEXT DEFAULT NULL,\n show_timestamp INTEGER NOT NULL DEFAULT 0,\n created_at INTEGER NOT NULL\n )`);\n db.exec(/* sql */ `\n CREATE INDEX IF NOT EXISTS idx_chat_messages_channel_time ON sfmc_chat_messages(channel_id, created_at ASC);\n CREATE INDEX IF NOT EXISTS idx_chat_messages_created_at ON sfmc_chat_messages(created_at ASC)`);\n\n // (4) QQ\u2194MC \u8EAB\u4EFD\u7ED1\u5B9A\uFF08\u5E73\u53F0\u8868\uFF1Bqq-bridge / \u6E38\u620F\u6A21\u5757\u5171\u7528 HTTP\uFF0C\u975E\u6A21\u5757\u79C1\u6709\u8868\uFF09\n db.exec(/* sql */ `\n CREATE TABLE IF NOT EXISTS sfmc_qq_bind_pending (\n code TEXT PRIMARY KEY NOT NULL,\n qq_user_openid TEXT NOT NULL,\n qq_backend TEXT NOT NULL DEFAULT 'official',\n created_at INTEGER NOT NULL,\n expires_at INTEGER NOT NULL\n )`);\n db.exec(/* sql */ `\n CREATE TABLE IF NOT EXISTS sfmc_qq_bindings (\n qq_user_openid TEXT PRIMARY KEY NOT NULL,\n player_xuid TEXT NOT NULL UNIQUE,\n player_name TEXT NOT NULL DEFAULT '',\n qq_backend TEXT NOT NULL DEFAULT 'official',\n bound_at INTEGER NOT NULL\n )`);\n db.exec(/* sql */ `\n CREATE INDEX IF NOT EXISTS idx_qq_bind_pending_openid ON sfmc_qq_bind_pending(qq_user_openid);\n CREATE INDEX IF NOT EXISTS idx_qq_bindings_xuid ON sfmc_qq_bindings(player_xuid)`);\n\n // (5) QQ \u5165\u670D\u5BA1\u6279\u3002\u65E7\u8E22\u4EBA\u961F\u5217\u8868\u4FDD\u7559\uFF0C\u907F\u514D\u5DF2\u6709\u5E93\u7F3A\u8868\uFF1B\u5E73\u53F0\u4E0D\u518D\u5199\u5165\u6216\u6D88\u8D39\u3002\n db.exec(/* sql */ `\n CREATE TABLE IF NOT EXISTS sfmc_qq_join_requests (\n id TEXT PRIMARY KEY NOT NULL,\n applicant_openid TEXT NOT NULL,\n player_name TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'pending',\n qq_backend TEXT NOT NULL DEFAULT 'official',\n created_at INTEGER NOT NULL,\n decided_at INTEGER,\n decided_by TEXT,\n applied_at INTEGER,\n apply_error TEXT\n )`);\n db.exec(/* sql */ `\n CREATE INDEX IF NOT EXISTS idx_qq_join_status ON sfmc_qq_join_requests(status, created_at)`);\n db.exec(/* sql */ `\n CREATE TABLE IF NOT EXISTS sfmc_qq_admin_actions (\n id TEXT PRIMARY KEY NOT NULL,\n kind TEXT NOT NULL,\n target_name TEXT NOT NULL,\n reason TEXT NOT NULL DEFAULT '',\n requested_by TEXT NOT NULL DEFAULT '',\n status TEXT NOT NULL DEFAULT 'pending',\n created_at INTEGER NOT NULL,\n applied_at INTEGER,\n apply_error TEXT\n )`);\n db.exec(/* sql */ `\n CREATE INDEX IF NOT EXISTS idx_qq_admin_actions_status ON sfmc_qq_admin_actions(status, created_at)`);\n\n // QQ \u8D26\u53F7\u9875\u5FEB\u7167\uFF1Aqq-link \u8C03\u7528\u7ECF\u6D4E / \u5728\u7EBF\u65F6\u957F\u670D\u52A1\u540E\u5199\u5165\uFF0C\u4F9B\u300C\u6211\u7684\u8D26\u53F7\u300D\u8BFB\u6700\u65B0\u6570\u5B57\u3002\n db.exec(/* sql */ `\n CREATE TABLE IF NOT EXISTS sfmc_qq_account_profile (\n player_xuid TEXT PRIMARY KEY NOT NULL,\n player_name TEXT NOT NULL DEFAULT '',\n balance INTEGER,\n today_seconds INTEGER,\n month_seconds INTEGER,\n total_seconds INTEGER,\n updated_at INTEGER NOT NULL\n )`);\n}\n"],
5
- "mappings": "AAuBO,SAAS,WAAW,IAAwB;AAEjD,KAAG;AAAA;AAAA,IAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAad;AAGJ,KAAG;AAAA;AAAA,IAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAsBd;AACJ,QAAM,gBAAgB,IAAI;AAAA,IACvB,GAAG,QAAQ,mCAAmC,EAAE,IAAI,EAA8B,IAAI,CAAC,WAAW;AAAA,MACjG,OAAO;AAAA,MACP;AAAA,IACF,CAAC;AAAA,EACH;AACA,QAAM,uBAAgD;AAAA,IACpD,CAAC,mBAAmB,iBAAiB;AAAA,IACrC,CAAC,WAAW,MAAM;AAAA,IAClB,CAAC,WAAW,MAAM;AAAA,IAClB,CAAC,WAAW,MAAM;AAAA,IAClB,CAAC,aAAa,iBAAiB;AAAA,IAC/B,CAAC,KAAK,MAAM;AAAA,IACZ,CAAC,KAAK,MAAM;AAAA,IACZ,CAAC,KAAK,MAAM;AAAA,IACZ,CAAC,aAAa,iBAAiB;AAAA,IAC/B,CAAC,iBAAiB,iBAAiB;AAAA,IACnC,CAAC,kBAAkB,iBAAiB;AAAA,IACpC,CAAC,uBAAuB,mBAAmB;AAAA,EAC7C;AACA,aAAW,CAAC,MAAM,UAAU,KAAK,sBAAsB;AACrD,QAAI,CAAC,cAAc,IAAI,IAAI,EAAG,IAAG,KAAK,wCAAwC,IAAI,KAAK,UAAU,EAAE;AAAA,EACrG;AACA,KAAG;AAAA;AAAA,IAAe;AAAA;AAAA,EACkD;AAGpE,KAAG;AAAA;AAAA,IAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWd;AACJ,KAAG;AAAA;AAAA,IAAe;AAAA;AAAA;AAAA,EAE8E;AAGhG,KAAG;AAAA;AAAA,IAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOd;AACJ,KAAG;AAAA;AAAA,IAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOd;AACJ,KAAG;AAAA;AAAA,IAAe;AAAA;AAAA;AAAA,EAEiE;AAGnF,KAAG;AAAA;AAAA,IAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYd;AACJ,KAAG;AAAA;AAAA,IAAe;AAAA;AAAA,EAC2E;AAC7F,KAAG;AAAA;AAAA,IAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWd;AACJ,KAAG;AAAA;AAAA,IAAe;AAAA;AAAA,EACoF;AAGtG,KAAG;AAAA;AAAA,IAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASd;AACN;",
4
+ "sourcesContent": ["/**\n * domain/schema.ts \u2014 \u6570\u636E\u5E93 Schema \u521D\u59CB\u5316\u5B9A\u4E49\uFF08\u5E73\u53F0\u6838\u5FC3\u5E95\u5EA7\u8868\uFF09\n *\n * \u96C6\u4E2D\u7BA1\u7406\u5E73\u53F0\u6838\u5FC3\u6570\u636E\u8868\u7684 CREATE TABLE IF NOT EXISTS \u4E0E CREATE INDEX IF NOT EXISTS DDL \u8BED\u53E5\u3002\n * \u8C03\u7528\u65B9\u5F0F\uFF1AinitSchema(db) \u2014\u2014 \u5728 openDatabase \u4E4B\u540E\u3001\u4E1A\u52A1\u67E5\u8BE2\u542F\u52A8\u524D\u6267\u884C\u3002\n *\n * \u67B6\u6784\u7EA6\u5B9A\uFF1A\n * \u4E1A\u52A1\u6A21\u5757\uFF08activity-log, coop, land, chat, economy \u7B49\uFF09\u79C1\u6709\u6570\u636E\u8868\u5747\u7531\u5404\u81EA\u6A21\u5757\u5728 SAPI \u8FD0\u884C\u671F\u901A\u8FC7\n * db.defineTable(...) \u81EA\u4E3B\u58F0\u660E\u4E0E\u7EF4\u62A4\uFF0C\u4E0D\u5728\u6B64\u5904\u9759\u6001\u786C\u7F16\u7801\uFF0C\u907F\u514D\u5217\u5B9A\u4E49\u4E0E\u7D22\u5F15\u51B2\u7A81\u3002\n *\n * \u6B64\u5904\u4EC5\u5B9A\u4E49\u5E73\u53F0\u6838\u5FC3\u8FD0\u884C\u3001\u8DE8\u670D\u52A1\u5171\u4EAB\uFF08qq-bridge / HTTP \u7AEF\u70B9 / status \u63A2\u6D3B\uFF09\u7684\u57FA\u7840\u5E95\u5EA7\u8868\uFF1A\n * 1. \u4E16\u754C\u4E0E\u73A9\u5BB6\u8FD0\u884C\u671F\u72B6\u6001\u5FEB\u7167\uFF08sfmc_world, sfmc_players\uFF09\n * 2. \u8DE8\u8FDB\u7A0B\u804A\u5929\u6D88\u606F\u805A\u5408\uFF08sfmc_chat_messages\uFF09\n * 3. QQ \u8EAB\u4EFD\u7ED1\u5B9A\u4E0E\u5BA1\u6279\u961F\u5217\uFF08sfmc_qq_*\uFF09\n * 4. \u5FAE\u4FE1\u8EAB\u4EFD\u7ED1\u5B9A\uFF08sfmc_wechat_*\uFF0C\u5C0F\u7A0B\u5E8F\u73A9\u5BB6\u7AEF\u4F7F\u7528\uFF09\n */\n\nimport type { DatabaseSync } from \"node:sqlite\";\n\n/**\n * \u521D\u59CB\u5316\u6570\u636E\u5E93\u7ED3\u6784\uFF0C\u521B\u5EFA\u5E73\u53F0\u6838\u5FC3\u5E95\u5EA7\u8868\u4E0E\u7D22\u5F15\u3002\n *\n * @param db SQLite \u6570\u636E\u5E93\u8FDE\u63A5\u5B9E\u4F8B\u3002\n */\nexport function initSchema(db: DatabaseSync): void {\n // (1) \u4E16\u754C\u8FD0\u884C\u671F\u72B6\u6001\uFF08\u4F9B status \u8DEF\u7531\u4E0E qq-bridge \u8BFB\u53D6\uFF0C\u5B57\u6BB5\u4E0E data-backup \u517C\u5BB9\uFF09\n db.exec(/* sql */ `\n CREATE TABLE IF NOT EXISTS sfmc_world (\n id TEXT PRIMARY KEY,\n seed TEXT DEFAULT '',\n difficulty TEXT DEFAULT '',\n spawn_x INTEGER DEFAULT 0,\n spawn_y INTEGER DEFAULT 0,\n spawn_z INTEGER DEFAULT 0,\n gamerules TEXT DEFAULT '{}',\n day INTEGER DEFAULT 0,\n absolute_time INTEGER DEFAULT 0,\n moon_phase INTEGER DEFAULT 0,\n updated_at INTEGER DEFAULT 0\n )`);\n\n // (2) \u73A9\u5BB6\u8FD0\u884C\u671F\u72B6\u6001\uFF08\u4F9B status \u8DEF\u7531\u4E0E qq-bridge \u8BFB\u53D6\uFF0C\u5B57\u6BB5\u4E0E data-backup \u517C\u5BB9\uFF09\n db.exec(/* sql */ `\n CREATE TABLE IF NOT EXISTS sfmc_players (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL DEFAULT '',\n xuid TEXT DEFAULT '',\n last_online INTEGER DEFAULT 0,\n level INTEGER DEFAULT 0,\n total_xp INTEGER DEFAULT 0,\n tags TEXT DEFAULT '',\n spawn_dimension TEXT DEFAULT '',\n spawn_x REAL,\n spawn_y REAL,\n spawn_z REAL,\n dimension TEXT DEFAULT '',\n x REAL,\n y REAL,\n z REAL,\n game_mode TEXT DEFAULT '',\n snapshot_hash TEXT DEFAULT '',\n active_channel TEXT DEFAULT '',\n subscribed_channels TEXT DEFAULT '[]',\n updated_at INTEGER DEFAULT 0\n )`);\n const playerColumns = new Map(\n (db.prepare(`PRAGMA table_info(\"sfmc_players\")`).all() as Array<{ name: string }>).map((column) => [\n column.name,\n true,\n ])\n );\n const missingPlayerColumns: Array<[string, string]> = [\n [\"spawn_dimension\", \"TEXT DEFAULT ''\"],\n [\"spawn_x\", \"REAL\"],\n [\"spawn_y\", \"REAL\"],\n [\"spawn_z\", \"REAL\"],\n [\"dimension\", \"TEXT DEFAULT ''\"],\n [\"x\", \"REAL\"],\n [\"y\", \"REAL\"],\n [\"z\", \"REAL\"],\n [\"game_mode\", \"TEXT DEFAULT ''\"],\n [\"snapshot_hash\", \"TEXT DEFAULT ''\"],\n [\"active_channel\", \"TEXT DEFAULT ''\"],\n [\"subscribed_channels\", \"TEXT DEFAULT '[]'\"],\n ];\n for (const [name, definition] of missingPlayerColumns) {\n if (!playerColumns.has(name)) db.exec(`ALTER TABLE sfmc_players ADD COLUMN \"${name}\" ${definition}`);\n }\n db.exec(/* sql */ `\n CREATE INDEX IF NOT EXISTS idx_players_name ON sfmc_players(name)`);\n\n // (3) \u804A\u5929\u6D88\u606F\u805A\u5408\uFF08\u4F9B routes/messages \u4E0E qq-bridge \u53CC\u5411\u540C\u6B65\uFF09\n db.exec(/* sql */ `\n CREATE TABLE IF NOT EXISTS sfmc_chat_messages (\n id TEXT PRIMARY KEY NOT NULL,\n channel_id TEXT NOT NULL DEFAULT '',\n from_id TEXT NOT NULL DEFAULT '',\n from_name TEXT NOT NULL DEFAULT '',\n type TEXT NOT NULL DEFAULT 'text',\n content TEXT NOT NULL DEFAULT '',\n attachment TEXT DEFAULT NULL,\n show_timestamp INTEGER NOT NULL DEFAULT 0,\n created_at INTEGER NOT NULL\n )`);\n db.exec(/* sql */ `\n CREATE INDEX IF NOT EXISTS idx_chat_messages_channel_time ON sfmc_chat_messages(channel_id, created_at ASC);\n CREATE INDEX IF NOT EXISTS idx_chat_messages_created_at ON sfmc_chat_messages(created_at ASC)`);\n\n // (4) QQ\u2194MC \u8EAB\u4EFD\u7ED1\u5B9A\uFF08\u5E73\u53F0\u8868\uFF1Bqq-bridge / \u6E38\u620F\u6A21\u5757\u5171\u7528 HTTP\uFF0C\u975E\u6A21\u5757\u79C1\u6709\u8868\uFF09\n db.exec(/* sql */ `\n CREATE TABLE IF NOT EXISTS sfmc_qq_bind_pending (\n code TEXT PRIMARY KEY NOT NULL,\n qq_user_openid TEXT NOT NULL,\n qq_backend TEXT NOT NULL DEFAULT 'official',\n created_at INTEGER NOT NULL,\n expires_at INTEGER NOT NULL\n )`);\n db.exec(/* sql */ `\n CREATE TABLE IF NOT EXISTS sfmc_qq_bindings (\n qq_user_openid TEXT PRIMARY KEY NOT NULL,\n player_xuid TEXT NOT NULL UNIQUE,\n player_name TEXT NOT NULL DEFAULT '',\n qq_backend TEXT NOT NULL DEFAULT 'official',\n bound_at INTEGER NOT NULL\n )`);\n db.exec(/* sql */ `\n CREATE INDEX IF NOT EXISTS idx_qq_bind_pending_openid ON sfmc_qq_bind_pending(qq_user_openid);\n CREATE INDEX IF NOT EXISTS idx_qq_bindings_xuid ON sfmc_qq_bindings(player_xuid)`);\n\n // (5) QQ \u5165\u670D\u5BA1\u6279\u3002\u65E7\u8E22\u4EBA\u961F\u5217\u8868\u4FDD\u7559\uFF0C\u907F\u514D\u5DF2\u6709\u5E93\u7F3A\u8868\uFF1B\u5E73\u53F0\u4E0D\u518D\u5199\u5165\u6216\u6D88\u8D39\u3002\n db.exec(/* sql */ `\n CREATE TABLE IF NOT EXISTS sfmc_qq_join_requests (\n id TEXT PRIMARY KEY NOT NULL,\n applicant_openid TEXT NOT NULL,\n player_name TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'pending',\n qq_backend TEXT NOT NULL DEFAULT 'official',\n created_at INTEGER NOT NULL,\n decided_at INTEGER,\n decided_by TEXT,\n applied_at INTEGER,\n apply_error TEXT\n )`);\n db.exec(/* sql */ `\n CREATE INDEX IF NOT EXISTS idx_qq_join_status ON sfmc_qq_join_requests(status, created_at)`);\n db.exec(/* sql */ `\n CREATE TABLE IF NOT EXISTS sfmc_qq_admin_actions (\n id TEXT PRIMARY KEY NOT NULL,\n kind TEXT NOT NULL,\n target_name TEXT NOT NULL,\n reason TEXT NOT NULL DEFAULT '',\n requested_by TEXT NOT NULL DEFAULT '',\n status TEXT NOT NULL DEFAULT 'pending',\n created_at INTEGER NOT NULL,\n applied_at INTEGER,\n apply_error TEXT\n )`);\n db.exec(/* sql */ `\n CREATE INDEX IF NOT EXISTS idx_qq_admin_actions_status ON sfmc_qq_admin_actions(status, created_at)`);\n\n // (6) \u5FAE\u4FE1\u2194MC \u8EAB\u4EFD\u7ED1\u5B9A\uFF08\u5C0F\u7A0B\u5E8F\u73A9\u5BB6\u7AEF\u7ECF mp-gateway \u7533\u8BF7\u77ED\u7801\uFF0C\u6E38\u620F\u5185 /c:bind \u786E\u8BA4\uFF09\u3002\n // \u4E0E QQ \u7ED1\u5B9A\u72EC\u7ACB\uFF1A\u540C\u4E00\u6E38\u620F\u8D26\u53F7\u53EF\u540C\u65F6\u7ED1\u5B9A QQ \u4E0E\u5FAE\u4FE1\uFF0CQQ \u8868\u4E0E\u6D41\u7A0B\u4E0D\u53D7\u5F71\u54CD\u3002\n db.exec(/* sql */ `\n CREATE TABLE IF NOT EXISTS sfmc_wechat_bind_pending (\n code TEXT PRIMARY KEY NOT NULL,\n wechat_openid TEXT NOT NULL,\n created_at INTEGER NOT NULL,\n expires_at INTEGER NOT NULL\n )`);\n db.exec(/* sql */ `\n CREATE TABLE IF NOT EXISTS sfmc_wechat_bindings (\n wechat_openid TEXT PRIMARY KEY NOT NULL,\n player_xuid TEXT NOT NULL UNIQUE,\n player_name TEXT NOT NULL DEFAULT '',\n bound_at INTEGER NOT NULL\n )`);\n db.exec(/* sql */ `\n CREATE INDEX IF NOT EXISTS idx_wechat_bind_pending_openid ON sfmc_wechat_bind_pending(wechat_openid);\n CREATE INDEX IF NOT EXISTS idx_wechat_bindings_xuid ON sfmc_wechat_bindings(player_xuid)`);\n\n // QQ \u8D26\u53F7\u9875\u5FEB\u7167\uFF1Aqq-link \u8C03\u7528\u7ECF\u6D4E / \u5728\u7EBF\u65F6\u957F\u670D\u52A1\u540E\u5199\u5165\uFF0C\u4F9B\u300C\u6211\u7684\u8D26\u53F7\u300D\u8BFB\u6700\u65B0\u6570\u5B57\u3002\n db.exec(/* sql */ `\n CREATE TABLE IF NOT EXISTS sfmc_qq_account_profile (\n player_xuid TEXT PRIMARY KEY NOT NULL,\n player_name TEXT NOT NULL DEFAULT '',\n balance INTEGER,\n today_seconds INTEGER,\n month_seconds INTEGER,\n total_seconds INTEGER,\n updated_at INTEGER NOT NULL\n )`);\n}\n"],
5
+ "mappings": "AAwBO,SAAS,WAAW,IAAwB;AAEjD,KAAG;AAAA;AAAA,IAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAad;AAGJ,KAAG;AAAA;AAAA,IAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAsBd;AACJ,QAAM,gBAAgB,IAAI;AAAA,IACvB,GAAG,QAAQ,mCAAmC,EAAE,IAAI,EAA8B,IAAI,CAAC,WAAW;AAAA,MACjG,OAAO;AAAA,MACP;AAAA,IACF,CAAC;AAAA,EACH;AACA,QAAM,uBAAgD;AAAA,IACpD,CAAC,mBAAmB,iBAAiB;AAAA,IACrC,CAAC,WAAW,MAAM;AAAA,IAClB,CAAC,WAAW,MAAM;AAAA,IAClB,CAAC,WAAW,MAAM;AAAA,IAClB,CAAC,aAAa,iBAAiB;AAAA,IAC/B,CAAC,KAAK,MAAM;AAAA,IACZ,CAAC,KAAK,MAAM;AAAA,IACZ,CAAC,KAAK,MAAM;AAAA,IACZ,CAAC,aAAa,iBAAiB;AAAA,IAC/B,CAAC,iBAAiB,iBAAiB;AAAA,IACnC,CAAC,kBAAkB,iBAAiB;AAAA,IACpC,CAAC,uBAAuB,mBAAmB;AAAA,EAC7C;AACA,aAAW,CAAC,MAAM,UAAU,KAAK,sBAAsB;AACrD,QAAI,CAAC,cAAc,IAAI,IAAI,EAAG,IAAG,KAAK,wCAAwC,IAAI,KAAK,UAAU,EAAE;AAAA,EACrG;AACA,KAAG;AAAA;AAAA,IAAe;AAAA;AAAA,EACkD;AAGpE,KAAG;AAAA;AAAA,IAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWd;AACJ,KAAG;AAAA;AAAA,IAAe;AAAA;AAAA;AAAA,EAE8E;AAGhG,KAAG;AAAA;AAAA,IAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOd;AACJ,KAAG;AAAA;AAAA,IAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOd;AACJ,KAAG;AAAA;AAAA,IAAe;AAAA;AAAA;AAAA,EAEiE;AAGnF,KAAG;AAAA;AAAA,IAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAYd;AACJ,KAAG;AAAA;AAAA,IAAe;AAAA;AAAA,EAC2E;AAC7F,KAAG;AAAA;AAAA,IAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWd;AACJ,KAAG;AAAA;AAAA,IAAe;AAAA;AAAA,EACoF;AAItG,KAAG;AAAA;AAAA,IAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMd;AACJ,KAAG;AAAA;AAAA,IAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMd;AACJ,KAAG;AAAA;AAAA,IAAe;AAAA;AAAA;AAAA,EAEyE;AAG3F,KAAG;AAAA;AAAA,IAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASd;AACN;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * tracked-processes.ts — 采集本实例已登记的其它 SFMC 进程内存。
3
+ *
4
+ * 使用场景:运行指标除 BDS、数据服务外,还要列出守护进程、QQ 桥、隧道、LLBot 和模块附属进程。
5
+ * 只读这些进程自己写下的运行态文件,不按进程名全机扫描,避免把同机另一个程序算进来。
6
+ * 界面名称与桌面端 metric-series.ts 的 sfmcPartLabel 一致。
7
+ */
8
+ /** 一条已确认仍在运行的进程。内存采不到的不返回,避免把过期 PID 显示成「资源未知」。 */
9
+ export interface TrackedProcess {
10
+ /** daemon、qq、tunnel、llbot,或 sidecar:<模块 id> */
11
+ key: string;
12
+ /** 侧栏和曲线用的名称 */
13
+ label: string;
14
+ pid: number;
15
+ running: true;
16
+ memoryMb: number;
17
+ cpuSeconds: number | null;
18
+ }
19
+ /**
20
+ * 从运行态 JSON 里取出 pid。
21
+ * 使用场景:daemon.json、qq.runtime.json 等只借 pid 字段,其余内容(例如鉴权 token)不读取、不返回。
22
+ */
23
+ export declare function readRecordedPid(text: string): number;
24
+ /**
25
+ * 采集仍在运行的已登记进程。
26
+ * @param root 实例根目录。
27
+ * @param skip 已经单独统计过的 PID(数据服务、本实例 BDS),这里不再重复计入。
28
+ */
29
+ export declare function collectTrackedProcesses(root: string, skip: ReadonlySet<number>): Promise<TrackedProcess[]>;
30
+ //# sourceMappingURL=tracked-processes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tracked-processes.d.ts","sourceRoot":"","sources":["../../src/domain/tracked-processes.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAOH,kDAAkD;AAClD,MAAM,WAAW,cAAc;IAC7B,+CAA+C;IAC/C,GAAG,EAAE,MAAM,CAAC;IACZ,gBAAgB;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,IAAI,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAUD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAOpD;AA6BD;;;;GAIG;AACH,wBAAsB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CA8BhH"}
@@ -0,0 +1,78 @@
1
+ import { qqRuntimeStatusPath, stateDir } from "@sfmc-bds/sdk/node/config";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { collectProcessResourcesMany } from "./process-resources.js";
5
+ const ROLE_FILES = [
6
+ { key: "daemon", label: "\u5B88\u62A4\u8FDB\u7A0B", file: (root) => path.join(stateDir(root), "daemon.json") },
7
+ { key: "qq", label: "QQ \u6865", file: (root) => qqRuntimeStatusPath(root) },
8
+ { key: "tunnel", label: "\u96A7\u9053", file: (root) => path.join(stateDir(root), "tunnel.runtime.json") },
9
+ { key: "llbot", label: "LLBot", file: (root) => path.join(stateDir(root), "llbot.runtime.json") }
10
+ ];
11
+ function readRecordedPid(text) {
12
+ try {
13
+ const pid = JSON.parse(text).pid;
14
+ return typeof pid === "number" && Number.isSafeInteger(pid) && pid > 0 ? pid : 0;
15
+ } catch {
16
+ return 0;
17
+ }
18
+ }
19
+ function readSidecars(text) {
20
+ try {
21
+ const processes = JSON.parse(text).processes;
22
+ if (!Array.isArray(processes)) return [];
23
+ const rows = [];
24
+ for (const item of processes) {
25
+ if (!item || typeof item !== "object") continue;
26
+ const row = item;
27
+ if (typeof row.id !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(row.id)) continue;
28
+ if (typeof row.pid !== "number" || !Number.isSafeInteger(row.pid) || row.pid <= 0) continue;
29
+ rows.push({ id: row.id, pid: row.pid });
30
+ }
31
+ return rows;
32
+ } catch {
33
+ return [];
34
+ }
35
+ }
36
+ function readText(file) {
37
+ try {
38
+ return fs.readFileSync(file, "utf8");
39
+ } catch {
40
+ return "";
41
+ }
42
+ }
43
+ async function collectTrackedProcesses(root, skip) {
44
+ const wanted = [];
45
+ const seen = new Set(skip);
46
+ for (const role of ROLE_FILES) {
47
+ const pid = readRecordedPid(readText(role.file(root)));
48
+ if (!pid || seen.has(pid)) continue;
49
+ seen.add(pid);
50
+ wanted.push({ key: role.key, label: role.label, pid });
51
+ }
52
+ for (const row of readSidecars(readText(path.join(stateDir(root), "sidecars.runtime.json")))) {
53
+ if (seen.has(row.pid)) continue;
54
+ seen.add(row.pid);
55
+ wanted.push({ key: `sidecar:${row.id}`, label: row.id, pid: row.pid });
56
+ }
57
+ if (!wanted.length) return [];
58
+ const measured = await collectProcessResourcesMany(wanted.map((row) => row.pid));
59
+ const found = [];
60
+ for (const row of wanted) {
61
+ const usage = measured.get(row.pid);
62
+ if (!usage) continue;
63
+ found.push({
64
+ key: row.key,
65
+ label: row.label,
66
+ pid: row.pid,
67
+ running: true,
68
+ memoryMb: usage.memoryMb,
69
+ cpuSeconds: usage.cpuSeconds
70
+ });
71
+ }
72
+ return found;
73
+ }
74
+ export {
75
+ collectTrackedProcesses,
76
+ readRecordedPid
77
+ };
78
+ //# sourceMappingURL=tracked-processes.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/domain/tracked-processes.ts"],
4
+ "sourcesContent": ["/**\n * tracked-processes.ts \u2014 \u91C7\u96C6\u672C\u5B9E\u4F8B\u5DF2\u767B\u8BB0\u7684\u5176\u5B83 SFMC \u8FDB\u7A0B\u5185\u5B58\u3002\n *\n * \u4F7F\u7528\u573A\u666F\uFF1A\u8FD0\u884C\u6307\u6807\u9664 BDS\u3001\u6570\u636E\u670D\u52A1\u5916\uFF0C\u8FD8\u8981\u5217\u51FA\u5B88\u62A4\u8FDB\u7A0B\u3001QQ \u6865\u3001\u96A7\u9053\u3001LLBot \u548C\u6A21\u5757\u9644\u5C5E\u8FDB\u7A0B\u3002\n * \u53EA\u8BFB\u8FD9\u4E9B\u8FDB\u7A0B\u81EA\u5DF1\u5199\u4E0B\u7684\u8FD0\u884C\u6001\u6587\u4EF6\uFF0C\u4E0D\u6309\u8FDB\u7A0B\u540D\u5168\u673A\u626B\u63CF\uFF0C\u907F\u514D\u628A\u540C\u673A\u53E6\u4E00\u4E2A\u7A0B\u5E8F\u7B97\u8FDB\u6765\u3002\n * \u754C\u9762\u540D\u79F0\u4E0E\u684C\u9762\u7AEF metric-series.ts \u7684 sfmcPartLabel \u4E00\u81F4\u3002\n */\n\nimport { qqRuntimeStatusPath, stateDir } from \"@sfmc-bds/sdk/node/config\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { collectProcessResourcesMany } from \"./process-resources.js\";\n\n/** \u4E00\u6761\u5DF2\u786E\u8BA4\u4ECD\u5728\u8FD0\u884C\u7684\u8FDB\u7A0B\u3002\u5185\u5B58\u91C7\u4E0D\u5230\u7684\u4E0D\u8FD4\u56DE\uFF0C\u907F\u514D\u628A\u8FC7\u671F PID \u663E\u793A\u6210\u300C\u8D44\u6E90\u672A\u77E5\u300D\u3002 */\nexport interface TrackedProcess {\n /** daemon\u3001qq\u3001tunnel\u3001llbot\uFF0C\u6216 sidecar:<\u6A21\u5757 id> */\n key: string;\n /** \u4FA7\u680F\u548C\u66F2\u7EBF\u7528\u7684\u540D\u79F0 */\n label: string;\n pid: number;\n running: true;\n memoryMb: number;\n cpuSeconds: number | null;\n}\n\n/** \u5355\u8FDB\u7A0B\u8FD0\u884C\u6001\u6587\u4EF6\u3002\u8DEF\u5F84\u90FD\u5728 `<\u6839>/.sfmc` \u4E0B\uFF0C\u4E0E\u5199\u5165\u65B9\u4F7F\u7528\u7684\u6587\u4EF6\u540D\u4E00\u81F4\u3002 */\nconst ROLE_FILES: Array<{ key: string; label: string; file: (root: string) => string }> = [\n { key: \"daemon\", label: \"\u5B88\u62A4\u8FDB\u7A0B\", file: (root) => path.join(stateDir(root), \"daemon.json\") },\n { key: \"qq\", label: \"QQ \u6865\", file: (root) => qqRuntimeStatusPath(root) },\n { key: \"tunnel\", label: \"\u96A7\u9053\", file: (root) => path.join(stateDir(root), \"tunnel.runtime.json\") },\n { key: \"llbot\", label: \"LLBot\", file: (root) => path.join(stateDir(root), \"llbot.runtime.json\") },\n];\n\n/**\n * \u4ECE\u8FD0\u884C\u6001 JSON \u91CC\u53D6\u51FA pid\u3002\n * \u4F7F\u7528\u573A\u666F\uFF1Adaemon.json\u3001qq.runtime.json \u7B49\u53EA\u501F pid \u5B57\u6BB5\uFF0C\u5176\u4F59\u5185\u5BB9\uFF08\u4F8B\u5982\u9274\u6743 token\uFF09\u4E0D\u8BFB\u53D6\u3001\u4E0D\u8FD4\u56DE\u3002\n */\nexport function readRecordedPid(text: string): number {\n try {\n const pid = (JSON.parse(text) as { pid?: unknown }).pid;\n return typeof pid === \"number\" && Number.isSafeInteger(pid) && pid > 0 ? pid : 0;\n } catch {\n return 0;\n }\n}\n\n/** \u9644\u5C5E\u8FDB\u7A0B\u6E05\u5355\u3002id \u53EA\u63A5\u53D7\u6A21\u5757\u6807\u8BC6\uFF0C\u907F\u514D\u628A\u4EFB\u610F\u5B57\u7B26\u4E32\u5199\u8FDB\u6307\u6807\u952E\u3002 */\nfunction readSidecars(text: string): Array<{ id: string; pid: number }> {\n try {\n const processes = (JSON.parse(text) as { processes?: unknown }).processes;\n if (!Array.isArray(processes)) return [];\n const rows: Array<{ id: string; pid: number }> = [];\n for (const item of processes) {\n if (!item || typeof item !== \"object\") continue;\n const row = item as { id?: unknown; pid?: unknown };\n if (typeof row.id !== \"string\" || !/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(row.id)) continue;\n if (typeof row.pid !== \"number\" || !Number.isSafeInteger(row.pid) || row.pid <= 0) continue;\n rows.push({ id: row.id, pid: row.pid });\n }\n return rows;\n } catch {\n return [];\n }\n}\n\nfunction readText(file: string): string {\n try {\n return fs.readFileSync(file, \"utf8\");\n } catch {\n return \"\";\n }\n}\n\n/**\n * \u91C7\u96C6\u4ECD\u5728\u8FD0\u884C\u7684\u5DF2\u767B\u8BB0\u8FDB\u7A0B\u3002\n * @param root \u5B9E\u4F8B\u6839\u76EE\u5F55\u3002\n * @param skip \u5DF2\u7ECF\u5355\u72EC\u7EDF\u8BA1\u8FC7\u7684 PID\uFF08\u6570\u636E\u670D\u52A1\u3001\u672C\u5B9E\u4F8B BDS\uFF09\uFF0C\u8FD9\u91CC\u4E0D\u518D\u91CD\u590D\u8BA1\u5165\u3002\n */\nexport async function collectTrackedProcesses(root: string, skip: ReadonlySet<number>): Promise<TrackedProcess[]> {\n const wanted: Array<{ key: string; label: string; pid: number }> = [];\n const seen = new Set<number>(skip);\n for (const role of ROLE_FILES) {\n const pid = readRecordedPid(readText(role.file(root)));\n if (!pid || seen.has(pid)) continue;\n seen.add(pid);\n wanted.push({ key: role.key, label: role.label, pid });\n }\n for (const row of readSidecars(readText(path.join(stateDir(root), \"sidecars.runtime.json\")))) {\n if (seen.has(row.pid)) continue;\n seen.add(row.pid);\n wanted.push({ key: `sidecar:${row.id}`, label: row.id, pid: row.pid });\n }\n if (!wanted.length) return [];\n const measured = await collectProcessResourcesMany(wanted.map((row) => row.pid));\n const found: TrackedProcess[] = [];\n for (const row of wanted) {\n const usage = measured.get(row.pid);\n if (!usage) continue;\n found.push({\n key: row.key,\n label: row.label,\n pid: row.pid,\n running: true,\n memoryMb: usage.memoryMb,\n cpuSeconds: usage.cpuSeconds,\n });\n }\n return found;\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * domain/wechat-bind.ts — 微信 openid ↔ 游戏账号绑定的数据操作
3
+ *
4
+ * 使用场景:
5
+ * - routes/wechat-bind.ts(mp-gateway 代小程序玩家调用)申请短码、解绑、查询;
6
+ * - 作为 BindProvider 注册到统一确认入口,游戏内 /c:bind 输入微信侧短码时由这里完成绑定。
7
+ * 与 QQ 绑定完全独立(独立表),同一游戏账号可同时绑定 QQ 与微信。
8
+ */
9
+ import type { QueryFn } from "../lib/sqlite.js";
10
+ import { type BindConfirmInput, type BindConfirmResult, type BindProvider } from "./bind-providers.js";
11
+ /** 一条微信绑定记录 */
12
+ export interface WechatBinding {
13
+ wechat_openid: string;
14
+ player_xuid: string;
15
+ player_name: string;
16
+ bound_at: number;
17
+ }
18
+ /** 申请短码的结果 */
19
+ export type WechatBindRequestResult = {
20
+ ok: true;
21
+ code: string;
22
+ expires_at: number;
23
+ ttl_ms: number;
24
+ } | {
25
+ ok: false;
26
+ status: number;
27
+ error: string;
28
+ player_name?: string;
29
+ };
30
+ /** 按 openid 读取绑定;未绑定返回 null */
31
+ export declare function findWechatBindingByOpenid(query: QueryFn, openid: string): WechatBinding | null;
32
+ /** 微信待确认表中是否存在该短码 */
33
+ export declare function hasWechatPendingCode(query: QueryFn, code: string): boolean;
34
+ /**
35
+ * 为微信用户申请绑定短码。已绑定则拒绝(须先解绑);同一 openid 旧的短码作废。
36
+ *
37
+ * @param isCodeTaken 跨渠道查重(QQ + 微信),由入口注入。
38
+ */
39
+ export declare function requestWechatBindCode(query: QueryFn, openid: string, isCodeTaken: (code: string) => boolean, now?: number): WechatBindRequestResult;
40
+ /**
41
+ * 用短码确认微信绑定(游戏侧)。判断顺序与错误码风格与 QQ 确认一致,便于游戏侧复用提示文案(LSP)。
42
+ */
43
+ export declare function confirmWechatBind(query: QueryFn, { code, xuid, name, now }: BindConfirmInput): BindConfirmResult;
44
+ /** 解绑微信(同时作废该 openid 未用完的短码),返回删除的绑定条数 */
45
+ export declare function unbindWechat(query: QueryFn, openid: string): number;
46
+ /**
47
+ * 玩家当前名字:优先取运行期快照 sfmc_players(玩家改名后会更新),没有时用绑定时记录的名字。
48
+ * 使用场景:计算 SFMC 覆盖等级(按玩家名索引)与展示。
49
+ */
50
+ export declare function currentPlayerName(query: QueryFn, xuid: string, fallback: string): string;
51
+ /**
52
+ * 微信绑定提供方。
53
+ * 使用场景:index.ts 注册到统一确认入口与跨渠道短码查重。
54
+ */
55
+ export declare function createWechatBindProvider(query: QueryFn): BindProvider;
56
+ //# sourceMappingURL=wechat-bind.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wechat-bind.d.ts","sourceRoot":"","sources":["../../src/domain/wechat-bind.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAChD,OAAO,EAAsC,KAAK,gBAAgB,EAAE,KAAK,iBAAiB,EAAE,KAAK,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAE3I,eAAe;AACf,MAAM,WAAW,aAAa;IAC5B,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,cAAc;AACd,MAAM,MAAM,uBAAuB,GAC/B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC9D;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEvE,+BAA+B;AAC/B,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAG9F;AAED,qBAAqB;AACrB,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAE1E;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,EACtC,GAAG,SAAa,GACf,uBAAuB,CAYzB;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE,gBAAgB,GAAG,iBAAiB,CA+BhH;AAED,0CAA0C;AAC1C,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAKnE;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CASxF;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,GAAG,YAAY,CAMrE"}
@@ -0,0 +1,84 @@
1
+ import { SQL } from "sql-template-strings";
2
+ import { BIND_CODE_TTL_MS, generateBindCode } from "./bind-providers.js";
3
+ function findWechatBindingByOpenid(query, openid) {
4
+ const rows = query(SQL`SELECT * FROM sfmc_wechat_bindings WHERE wechat_openid = ${openid} LIMIT 1`);
5
+ return rows[0] ?? null;
6
+ }
7
+ function hasWechatPendingCode(query, code) {
8
+ return query(SQL`SELECT 1 AS ok FROM sfmc_wechat_bind_pending WHERE code = ${code} LIMIT 1`).length > 0;
9
+ }
10
+ function requestWechatBindCode(query, openid, isCodeTaken, now = Date.now()) {
11
+ const existing = findWechatBindingByOpenid(query, openid);
12
+ if (existing) return { ok: false, status: 409, error: "already_bound", player_name: existing.player_name };
13
+ query(SQL`DELETE FROM sfmc_wechat_bind_pending WHERE wechat_openid = ${openid} OR expires_at < ${now}`);
14
+ const code = generateBindCode(isCodeTaken);
15
+ if (!code) return { ok: false, status: 500, error: "code_gen_failed" };
16
+ const expiresAt = now + BIND_CODE_TTL_MS;
17
+ query(
18
+ SQL`INSERT INTO sfmc_wechat_bind_pending (code, wechat_openid, created_at, expires_at)
19
+ VALUES (${code}, ${openid}, ${now}, ${expiresAt})`
20
+ );
21
+ return { ok: true, code, expires_at: expiresAt, ttl_ms: BIND_CODE_TTL_MS };
22
+ }
23
+ function confirmWechatBind(query, { code, xuid, name, now }) {
24
+ const rows = query(SQL`SELECT * FROM sfmc_wechat_bind_pending WHERE code = ${code} LIMIT 1`);
25
+ const row = rows[0];
26
+ if (!row) return { ok: false, status: 400, error: "invalid_code" };
27
+ if (Number(row.expires_at) < now) {
28
+ query(SQL`DELETE FROM sfmc_wechat_bind_pending WHERE code = ${code}`);
29
+ return { ok: false, status: 400, error: "code_expired" };
30
+ }
31
+ const byOpenid = findWechatBindingByOpenid(query, row.wechat_openid);
32
+ if (byOpenid && byOpenid.player_xuid !== xuid) {
33
+ return { ok: false, status: 409, error: "wechat_already_bound", hint: "\u8BF7\u5148\u5728\u5C0F\u7A0B\u5E8F\u89E3\u7ED1" };
34
+ }
35
+ const byXuid = query(
36
+ SQL`SELECT wechat_openid FROM sfmc_wechat_bindings WHERE player_xuid = ${xuid} LIMIT 1`
37
+ );
38
+ if (byXuid[0] && byXuid[0].wechat_openid !== row.wechat_openid) {
39
+ return { ok: false, status: 409, error: "player_already_bound_wechat", hint: "\u8BF7\u5148\u89E3\u7ED1\u5F53\u524D\u5FAE\u4FE1" };
40
+ }
41
+ query(SQL`DELETE FROM sfmc_wechat_bindings WHERE wechat_openid = ${row.wechat_openid} OR player_xuid = ${xuid}`);
42
+ query(
43
+ SQL`INSERT INTO sfmc_wechat_bindings (wechat_openid, player_xuid, player_name, bound_at)
44
+ VALUES (${row.wechat_openid}, ${xuid}, ${name}, ${now})`
45
+ );
46
+ query(SQL`DELETE FROM sfmc_wechat_bind_pending WHERE code = ${code}`);
47
+ return {
48
+ ok: true,
49
+ binding: { wechat_openid: row.wechat_openid, player_xuid: xuid, player_name: name, bound_at: now }
50
+ };
51
+ }
52
+ function unbindWechat(query, openid) {
53
+ const before = query(SQL`SELECT 1 AS ok FROM sfmc_wechat_bindings WHERE wechat_openid = ${openid}`);
54
+ query(SQL`DELETE FROM sfmc_wechat_bindings WHERE wechat_openid = ${openid}`);
55
+ query(SQL`DELETE FROM sfmc_wechat_bind_pending WHERE wechat_openid = ${openid}`);
56
+ return before.length;
57
+ }
58
+ function currentPlayerName(query, xuid, fallback) {
59
+ try {
60
+ const rows = query(
61
+ SQL`SELECT name FROM sfmc_players WHERE (id = ${xuid} OR xuid = ${xuid}) AND name <> '' LIMIT 1`
62
+ );
63
+ return rows[0]?.name || fallback;
64
+ } catch {
65
+ return fallback;
66
+ }
67
+ }
68
+ function createWechatBindProvider(query) {
69
+ return {
70
+ id: "wechat",
71
+ hasPendingCode: (code) => hasWechatPendingCode(query, code),
72
+ confirm: (input) => confirmWechatBind(query, input)
73
+ };
74
+ }
75
+ export {
76
+ confirmWechatBind,
77
+ createWechatBindProvider,
78
+ currentPlayerName,
79
+ findWechatBindingByOpenid,
80
+ hasWechatPendingCode,
81
+ requestWechatBindCode,
82
+ unbindWechat
83
+ };
84
+ //# sourceMappingURL=wechat-bind.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/domain/wechat-bind.ts"],
4
+ "sourcesContent": ["/**\n * domain/wechat-bind.ts \u2014 \u5FAE\u4FE1 openid \u2194 \u6E38\u620F\u8D26\u53F7\u7ED1\u5B9A\u7684\u6570\u636E\u64CD\u4F5C\n *\n * \u4F7F\u7528\u573A\u666F\uFF1A\n * - routes/wechat-bind.ts\uFF08mp-gateway \u4EE3\u5C0F\u7A0B\u5E8F\u73A9\u5BB6\u8C03\u7528\uFF09\u7533\u8BF7\u77ED\u7801\u3001\u89E3\u7ED1\u3001\u67E5\u8BE2\uFF1B\n * - \u4F5C\u4E3A BindProvider \u6CE8\u518C\u5230\u7EDF\u4E00\u786E\u8BA4\u5165\u53E3\uFF0C\u6E38\u620F\u5185 /c:bind \u8F93\u5165\u5FAE\u4FE1\u4FA7\u77ED\u7801\u65F6\u7531\u8FD9\u91CC\u5B8C\u6210\u7ED1\u5B9A\u3002\n * \u4E0E QQ \u7ED1\u5B9A\u5B8C\u5168\u72EC\u7ACB\uFF08\u72EC\u7ACB\u8868\uFF09\uFF0C\u540C\u4E00\u6E38\u620F\u8D26\u53F7\u53EF\u540C\u65F6\u7ED1\u5B9A QQ \u4E0E\u5FAE\u4FE1\u3002\n */\n\nimport { SQL } from \"sql-template-strings\";\nimport type { QueryFn } from \"../lib/sqlite.js\";\nimport { BIND_CODE_TTL_MS, generateBindCode, type BindConfirmInput, type BindConfirmResult, type BindProvider } from \"./bind-providers.js\";\n\n/** \u4E00\u6761\u5FAE\u4FE1\u7ED1\u5B9A\u8BB0\u5F55 */\nexport interface WechatBinding {\n wechat_openid: string;\n player_xuid: string;\n player_name: string;\n bound_at: number;\n}\n\n/** \u7533\u8BF7\u77ED\u7801\u7684\u7ED3\u679C */\nexport type WechatBindRequestResult =\n | { ok: true; code: string; expires_at: number; ttl_ms: number }\n | { ok: false; status: number; error: string; player_name?: string };\n\n/** \u6309 openid \u8BFB\u53D6\u7ED1\u5B9A\uFF1B\u672A\u7ED1\u5B9A\u8FD4\u56DE null */\nexport function findWechatBindingByOpenid(query: QueryFn, openid: string): WechatBinding | null {\n const rows = query(SQL`SELECT * FROM sfmc_wechat_bindings WHERE wechat_openid = ${openid} LIMIT 1`) as WechatBinding[];\n return rows[0] ?? null;\n}\n\n/** \u5FAE\u4FE1\u5F85\u786E\u8BA4\u8868\u4E2D\u662F\u5426\u5B58\u5728\u8BE5\u77ED\u7801 */\nexport function hasWechatPendingCode(query: QueryFn, code: string): boolean {\n return (query(SQL`SELECT 1 AS ok FROM sfmc_wechat_bind_pending WHERE code = ${code} LIMIT 1`) as unknown[]).length > 0;\n}\n\n/**\n * \u4E3A\u5FAE\u4FE1\u7528\u6237\u7533\u8BF7\u7ED1\u5B9A\u77ED\u7801\u3002\u5DF2\u7ED1\u5B9A\u5219\u62D2\u7EDD\uFF08\u987B\u5148\u89E3\u7ED1\uFF09\uFF1B\u540C\u4E00 openid \u65E7\u7684\u77ED\u7801\u4F5C\u5E9F\u3002\n *\n * @param isCodeTaken \u8DE8\u6E20\u9053\u67E5\u91CD\uFF08QQ + \u5FAE\u4FE1\uFF09\uFF0C\u7531\u5165\u53E3\u6CE8\u5165\u3002\n */\nexport function requestWechatBindCode(\n query: QueryFn,\n openid: string,\n isCodeTaken: (code: string) => boolean,\n now = Date.now()\n): WechatBindRequestResult {\n const existing = findWechatBindingByOpenid(query, openid);\n if (existing) return { ok: false, status: 409, error: \"already_bound\", player_name: existing.player_name };\n query(SQL`DELETE FROM sfmc_wechat_bind_pending WHERE wechat_openid = ${openid} OR expires_at < ${now}`);\n const code = generateBindCode(isCodeTaken);\n if (!code) return { ok: false, status: 500, error: \"code_gen_failed\" };\n const expiresAt = now + BIND_CODE_TTL_MS;\n query(\n SQL`INSERT INTO sfmc_wechat_bind_pending (code, wechat_openid, created_at, expires_at)\n VALUES (${code}, ${openid}, ${now}, ${expiresAt})`\n );\n return { ok: true, code, expires_at: expiresAt, ttl_ms: BIND_CODE_TTL_MS };\n}\n\n/**\n * \u7528\u77ED\u7801\u786E\u8BA4\u5FAE\u4FE1\u7ED1\u5B9A\uFF08\u6E38\u620F\u4FA7\uFF09\u3002\u5224\u65AD\u987A\u5E8F\u4E0E\u9519\u8BEF\u7801\u98CE\u683C\u4E0E QQ \u786E\u8BA4\u4E00\u81F4\uFF0C\u4FBF\u4E8E\u6E38\u620F\u4FA7\u590D\u7528\u63D0\u793A\u6587\u6848\uFF08LSP\uFF09\u3002\n */\nexport function confirmWechatBind(query: QueryFn, { code, xuid, name, now }: BindConfirmInput): BindConfirmResult {\n const rows = query(SQL`SELECT * FROM sfmc_wechat_bind_pending WHERE code = ${code} LIMIT 1`) as Array<{\n wechat_openid: string;\n expires_at: number;\n }>;\n const row = rows[0];\n if (!row) return { ok: false, status: 400, error: \"invalid_code\" };\n if (Number(row.expires_at) < now) {\n query(SQL`DELETE FROM sfmc_wechat_bind_pending WHERE code = ${code}`);\n return { ok: false, status: 400, error: \"code_expired\" };\n }\n const byOpenid = findWechatBindingByOpenid(query, row.wechat_openid);\n if (byOpenid && byOpenid.player_xuid !== xuid) {\n return { ok: false, status: 409, error: \"wechat_already_bound\", hint: \"\u8BF7\u5148\u5728\u5C0F\u7A0B\u5E8F\u89E3\u7ED1\" };\n }\n const byXuid = query(\n SQL`SELECT wechat_openid FROM sfmc_wechat_bindings WHERE player_xuid = ${xuid} LIMIT 1`\n ) as Array<{ wechat_openid: string }>;\n if (byXuid[0] && byXuid[0].wechat_openid !== row.wechat_openid) {\n return { ok: false, status: 409, error: \"player_already_bound_wechat\", hint: \"\u8BF7\u5148\u89E3\u7ED1\u5F53\u524D\u5FAE\u4FE1\" };\n }\n query(SQL`DELETE FROM sfmc_wechat_bindings WHERE wechat_openid = ${row.wechat_openid} OR player_xuid = ${xuid}`);\n query(\n SQL`INSERT INTO sfmc_wechat_bindings (wechat_openid, player_xuid, player_name, bound_at)\n VALUES (${row.wechat_openid}, ${xuid}, ${name}, ${now})`\n );\n query(SQL`DELETE FROM sfmc_wechat_bind_pending WHERE code = ${code}`);\n return {\n ok: true,\n binding: { wechat_openid: row.wechat_openid, player_xuid: xuid, player_name: name, bound_at: now },\n };\n}\n\n/** \u89E3\u7ED1\u5FAE\u4FE1\uFF08\u540C\u65F6\u4F5C\u5E9F\u8BE5 openid \u672A\u7528\u5B8C\u7684\u77ED\u7801\uFF09\uFF0C\u8FD4\u56DE\u5220\u9664\u7684\u7ED1\u5B9A\u6761\u6570 */\nexport function unbindWechat(query: QueryFn, openid: string): number {\n const before = query(SQL`SELECT 1 AS ok FROM sfmc_wechat_bindings WHERE wechat_openid = ${openid}`) as unknown[];\n query(SQL`DELETE FROM sfmc_wechat_bindings WHERE wechat_openid = ${openid}`);\n query(SQL`DELETE FROM sfmc_wechat_bind_pending WHERE wechat_openid = ${openid}`);\n return before.length;\n}\n\n/**\n * \u73A9\u5BB6\u5F53\u524D\u540D\u5B57\uFF1A\u4F18\u5148\u53D6\u8FD0\u884C\u671F\u5FEB\u7167 sfmc_players\uFF08\u73A9\u5BB6\u6539\u540D\u540E\u4F1A\u66F4\u65B0\uFF09\uFF0C\u6CA1\u6709\u65F6\u7528\u7ED1\u5B9A\u65F6\u8BB0\u5F55\u7684\u540D\u5B57\u3002\n * \u4F7F\u7528\u573A\u666F\uFF1A\u8BA1\u7B97 SFMC \u8986\u76D6\u7B49\u7EA7\uFF08\u6309\u73A9\u5BB6\u540D\u7D22\u5F15\uFF09\u4E0E\u5C55\u793A\u3002\n */\nexport function currentPlayerName(query: QueryFn, xuid: string, fallback: string): string {\n try {\n const rows = query(\n SQL`SELECT name FROM sfmc_players WHERE (id = ${xuid} OR xuid = ${xuid}) AND name <> '' LIMIT 1`\n ) as Array<{ name: string }>;\n return rows[0]?.name || fallback;\n } catch {\n return fallback;\n }\n}\n\n/**\n * \u5FAE\u4FE1\u7ED1\u5B9A\u63D0\u4F9B\u65B9\u3002\n * \u4F7F\u7528\u573A\u666F\uFF1Aindex.ts \u6CE8\u518C\u5230\u7EDF\u4E00\u786E\u8BA4\u5165\u53E3\u4E0E\u8DE8\u6E20\u9053\u77ED\u7801\u67E5\u91CD\u3002\n */\nexport function createWechatBindProvider(query: QueryFn): BindProvider {\n return {\n id: \"wechat\",\n hasPendingCode: (code) => hasWechatPendingCode(query, code),\n confirm: (input) => confirmWechatBind(query, input),\n };\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAiBH,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEtD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAExD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AA6B1C,QAAA,MAAM,GAAG,8BAAY,CAAC;AACtB,QAAA,MAAM,EAAE,oCAA4B,CAAC;AAGrC,QAAA,MAAM,KAAK,mCAAkB,CAAC;AAwB9B,QAAA,MAAM,gBAAgB,8DAA0E,CAAC;AAQjG,QAAA,MAAM,UAAU,0CAId,CAAC;AAQH,QAAA,MAAM,cAAc,gBAAyB,CAAC;AAC9C,QAAA,MAAM,eAAe,iBAAwB,CAAC;AAE9C,QAAA,MAAM,QAAQ,UAMZ,CAAC;AAufH,OAAO,EAAE,EAAE,EAAE,gBAAgB,EAAE,GAAG,EAAE,UAAU,EAAE,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,QAAQ,EAAE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAiBH,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEtD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAExD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAmC1C,QAAA,MAAM,GAAG,8BAAY,CAAC;AACtB,QAAA,MAAM,EAAE,oCAA4B,CAAC;AAGrC,QAAA,MAAM,KAAK,mCAAkB,CAAC;AAwB9B,QAAA,MAAM,gBAAgB,8DAA0E,CAAC;AAQjG,QAAA,MAAM,UAAU,0CAId,CAAC;AAQH,QAAA,MAAM,cAAc,gBAAyB,CAAC;AAC9C,QAAA,MAAM,eAAe,iBAAwB,CAAC;AAE9C,QAAA,MAAM,QAAQ,UAMZ,CAAC;AA6hBH,OAAO,EAAE,EAAE,EAAE,gBAAgB,EAAE,GAAG,EAAE,UAAU,EAAE,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,QAAQ,EAAE,CAAC"}
package/dist/index.js CHANGED
@@ -33,7 +33,13 @@ import { createContentRoutes } from "./routes/content.js";
33
33
  import { createHealthRoutes } from "./routes/health.js";
34
34
  import { createMessagesRoutes, gameChatForwardPayload } from "./routes/messages.js";
35
35
  import { createModuleRoutes } from "./routes/modules.js";
36
+ import { codeTakenBy } from "./domain/bind-providers.js";
37
+ import { resolvePlayerLevel } from "./domain/player-level.js";
38
+ import { createQqBindProvider } from "./domain/qq-bind.js";
39
+ import { createWechatBindProvider } from "./domain/wechat-bind.js";
40
+ import { createBindConfirmRoutes } from "./routes/bind-confirm.js";
36
41
  import { createQqBindRoutes } from "./routes/qq-bind.js";
42
+ import { createWechatBindRoutes } from "./routes/wechat-bind.js";
37
43
  import { createQqEventsRoutes } from "./routes/qq-events.js";
38
44
  import { createQqJoinRoutes } from "./routes/qq-join.js";
39
45
  import { createStatusRoutes } from "./routes/status.js";
@@ -148,15 +154,17 @@ function loadContentSnapshotForRequest() {
148
154
  }
149
155
  ];
150
156
  });
151
- const cfg = readJson(configPath(env.PROJECT_ROOT, "bds_updater.json"));
152
- const rawBds = typeof cfg?.bds_path === "string" ? cfg.bds_path.trim() : "";
153
- const bdsRoot = rawBds ? path.isAbsolute(rawBds) ? rawBds : path.resolve(env.PROJECT_ROOT, rawBds) : null;
154
157
  return loadContentSnapshot({
155
158
  packagesDir: join(env.MODULES_DIR, "packages"),
156
159
  modules,
157
- bdsRoot
160
+ bdsRoot: currentBdsRoot()
158
161
  });
159
162
  }
163
+ function currentBdsRoot() {
164
+ const cfg = readJson(configPath(env.PROJECT_ROOT, "bds_updater.json"));
165
+ const rawBds = typeof cfg?.bds_path === "string" ? cfg.bds_path.trim() : "";
166
+ return rawBds ? path.isAbsolute(rawBds) ? rawBds : path.resolve(env.PROJECT_ROOT, rawBds) : null;
167
+ }
160
168
  function moduleCanDisable(raw) {
161
169
  return raw.canDisable !== false;
162
170
  }
@@ -201,7 +209,27 @@ for (const name of PLATFORM_MONITORING_SERVICES) serviceRegistry.registerHandler
201
209
  if (name === "tps.status") return { tps, grade, text: `\xA77[TPS] \xA7f${tps?.toFixed(2) ?? "\u672A\u77E5"} \xA77/ 20.00` };
202
210
  return result.current ? { ...result.current, grade, totalLoadedChunks: result.current.chunkEstimate } : null;
203
211
  });
204
- const qqBindRoutes = createQqBindRoutes({ query, body, json });
212
+ const bindProviders = [createQqBindProvider(query), createWechatBindProvider(query)];
213
+ const isBindCodeTaken = codeTakenBy(() => bindProviders);
214
+ const qqBindRoutes = createQqBindRoutes({ query, body, json, isCodeTaken: isBindCodeTaken });
215
+ const bindConfirmRoutes = createBindConfirmRoutes({ body, json, providers: () => bindProviders });
216
+ const wechatBindRoutes = createWechatBindRoutes({
217
+ query,
218
+ body,
219
+ json,
220
+ isCodeTaken: isBindCodeTaken,
221
+ // 每次现读两份权限文件:管理员调整权限后,小程序管理端授权立即随之变化
222
+ resolveLevel: (player) => resolvePlayerLevel(
223
+ {
224
+ readSfmcPermissions: () => readJson(configPath(env.PROJECT_ROOT, "permissions.json")),
225
+ readBdsPermissions: () => {
226
+ const bdsRoot = currentBdsRoot();
227
+ return bdsRoot ? readJson(join(bdsRoot, "permissions.json")) : null;
228
+ }
229
+ },
230
+ player
231
+ )
232
+ });
205
233
  const QQ_LINK_CONFIG_KEY = "qq_link";
206
234
  const QQ_LINK_DEFAULTS = {
207
235
  allowlist_enabled: true,
@@ -403,8 +431,8 @@ async function handle(req, res) {
403
431
  permissions: manifest?.permissions ?? []
404
432
  };
405
433
  } else {
406
- const PUBLIC_GET = path2 === "/api/health" || path2 === "/api/sfmc/status" || path2 === "/api/sfmc/qq/join/apply-queue" || path2 === "/api/sfmc/qq/join/settings" || method === "GET" && (path2 === "/api/sfmc/content" || path2 === "/api/sfmc/modules" || path2 === "/api/sfmc/modules/catalog" || path2.startsWith("/api/sfmc/modules/") || path2 === "/api/sfmc/qq/bind/me" || path2 === "/api/sfmc/qq/join/pending");
407
- const NEEDS_AUTH = !PUBLIC_GET && method !== "GET" && !(path2.startsWith("/api/sfmc/qq/bind/") || path2.startsWith("/api/sfmc/qq/join/") || path2.startsWith("/api/sfmc/qq/events"));
434
+ const PUBLIC_GET = path2 === "/api/health" || path2 === "/api/sfmc/status" || path2 === "/api/sfmc/qq/join/apply-queue" || path2 === "/api/sfmc/qq/join/settings" || method === "GET" && (path2 === "/api/sfmc/content" || path2 === "/api/sfmc/modules" || path2 === "/api/sfmc/modules/catalog" || path2.startsWith("/api/sfmc/modules/") || path2 === "/api/sfmc/qq/bind/me" || path2 === "/api/sfmc/wechat/bind/me" || path2 === "/api/sfmc/qq/join/pending");
435
+ const NEEDS_AUTH = !PUBLIC_GET && method !== "GET" && !(path2.startsWith("/api/sfmc/qq/bind/") || path2.startsWith("/api/sfmc/qq/join/") || path2.startsWith("/api/sfmc/qq/events") || path2.startsWith("/api/sfmc/wechat/bind/") || path2 === "/api/sfmc/bind/confirm");
408
436
  if (env.AUTH_TOKEN && NEEDS_AUTH) {
409
437
  const auth = req.headers["authorization"] || "";
410
438
  const provided = auth.startsWith("Bearer ") ? auth.slice(7) : req.headers["x-db-token"] || "";
@@ -451,6 +479,8 @@ async function handle(req, res) {
451
479
  if (await healthRoutes(ctxBase)) return;
452
480
  if (await statusRoutes(ctxBase)) return;
453
481
  if (await qqBindRoutes(ctxBase)) return;
482
+ if (await bindConfirmRoutes(ctxBase)) return;
483
+ if (await wechatBindRoutes(ctxBase)) return;
454
484
  if (await qqJoinRoutes(ctxBase)) return;
455
485
  if (await qqEventsRoutes(ctxBase)) return;
456
486
  if (await messagesRoutes(ctxBase)) return;