@tool-bridge/server 0.7.0 → 0.10.1

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 (108) hide show
  1. package/dist/{airtable-BNVPDLF2.js → airtable-SLT4UDB3.js} +4 -4
  2. package/dist/{alt_text_generator_ai-RMQPHK2Y.js → alt_text_generator_ai-HCFYQNEN.js} +4 -4
  3. package/dist/{amap-PRNZPQ34.js → amap-ZOOTZCI2.js} +4 -4
  4. package/dist/{anthropic-A76VIXR7.js → anthropic-IUWXKV76.js} +4 -4
  5. package/dist/{apify-2Q2CTLKE.js → apify-HQXUP3ZL.js} +4 -4
  6. package/dist/{brave_search-5YJJVPXH.js → brave_search-HG5PMEDZ.js} +4 -4
  7. package/dist/{chunk-76YX7XSX.js → chunk-AWXZMW6M.js} +246 -52
  8. package/dist/{chunk-7ASYWVYP.js → chunk-EMUJLM32.js} +599 -38
  9. package/dist/{chunk-EBO6Q4NR.js → chunk-THDEI7S7.js} +1 -1
  10. package/dist/{chunk-BUHY4IE6.js → chunk-V4VHC3Q6.js} +2 -2
  11. package/dist/{chunk-GMHELPK3.js → chunk-WIH5OHFG.js} +1 -1
  12. package/dist/{circleci-TDXP3CPQ.js → circleci-XA4GXPOT.js} +4 -4
  13. package/dist/{clerk-JRSGMXMS.js → clerk-MUAETUIY.js} +4 -4
  14. package/dist/{cloudflare_dns-G4CHNLO7.js → cloudflare_dns-BRYRH55W.js} +4 -4
  15. package/dist/{cohere-3KN27F2B.js → cohere-XKVGQJQH.js} +4 -4
  16. package/dist/{confluence-QV6J2PBV.js → confluence-SWP6UUEK.js} +4 -4
  17. package/dist/{convertapi-HETITAHL.js → convertapi-42FNXPAS.js} +4 -4
  18. package/dist/{currencyapi-647DEY6G.js → currencyapi-7IECLAKK.js} +4 -4
  19. package/dist/{deepseek-G63RUHM6.js → deepseek-VD3VY3D2.js} +4 -4
  20. package/dist/{docker_hub-RH72CRWO.js → docker_hub-YCEWCV5D.js} +4 -4
  21. package/dist/{dropbox-T4Y5KSZW.js → dropbox-KCAEQENT.js} +4 -4
  22. package/dist/{dub-ECKQT3DC.js → dub-AVLJI73G.js} +4 -4
  23. package/dist/{e2b-N5SRTPLF.js → e2b-NZPEKNY3.js} +4 -4
  24. package/dist/{exa-525ZDM7G.js → exa-MHHTRY4Y.js} +4 -4
  25. package/dist/{fal_ai-BFMVHAUW.js → fal_ai-VKVUKXE2.js} +4 -4
  26. package/dist/{feishu-NIDOD7EW.js → feishu-AFLX4VZX.js} +3 -3
  27. package/dist/{feishu_custom_bot-V5RGAIUH.js → feishu_custom_bot-S4XJYZVB.js} +4 -4
  28. package/dist/{figma-GH7TXJMP.js → figma-6LBMPYPL.js} +4 -4
  29. package/dist/{firehydrant-5T3UHAQM.js → firehydrant-JRAJ4CMG.js} +4 -4
  30. package/dist/{fixer-3RMJLMFO.js → fixer-BTWM3K4Z.js} +4 -4
  31. package/dist/{fly-NPHJC5MH.js → fly-FUY25D36.js} +4 -4
  32. package/dist/{front-PYGXJAQK.js → front-SFQD4V63.js} +4 -4
  33. package/dist/{geocodio-T6G3FVIF.js → geocodio-JZTFWKOE.js} +4 -4
  34. package/dist/{ghost-L3XIF4SJ.js → ghost-HNZTH6AR.js} +4 -4
  35. package/dist/{github-7YDUB6N6.js → github-3J6ZYHGY.js} +4 -4
  36. package/dist/{gmail-RY2ZDG3B.js → gmail-Y4GWZ2UJ.js} +4 -4
  37. package/dist/{googlecalendar-EHOFMXHE.js → googlecalendar-JK6B3ZCO.js} +4 -4
  38. package/dist/{googledocs-BA3OAMKA.js → googledocs-VERSNFGF.js} +4 -4
  39. package/dist/{grafana-PYZQ4CKI.js → grafana-WABWHNKT.js} +4 -4
  40. package/dist/{graphhopper-3P4YU45N.js → graphhopper-PWZOVQQF.js} +4 -4
  41. package/dist/index.d.ts +372 -28
  42. package/dist/index.js +14 -2
  43. package/dist/{ip2proxy-GQO6VF4P.js → ip2proxy-IJKSNZVR.js} +4 -4
  44. package/dist/{ipgeolocation_io-VOZU6E7H.js → ipgeolocation_io-I27LWP7M.js} +4 -4
  45. package/dist/{ipqualityscore-XDJ3TTMM.js → ipqualityscore-THLB64L6.js} +4 -4
  46. package/dist/{jina_ai-ELXPSTUJ.js → jina_ai-KHC4WQAK.js} +4 -4
  47. package/dist/{jira-INDXWO4I.js → jira-NAGZYZLK.js} +4 -4
  48. package/dist/{langsmith-PMCQBCIK.js → langsmith-5BZ4CAWL.js} +4 -4
  49. package/dist/{lemlist-VWZXIHJX.js → lemlist-4D5DLIWT.js} +4 -4
  50. package/dist/{linear-YXG4KZR4.js → linear-5A6IZTNW.js} +4 -4
  51. package/dist/{logsnag-XENBYHHK.js → logsnag-Q7CU4J7A.js} +4 -4
  52. package/dist/main.js +113 -106
  53. package/dist/{mattermost-BHVJP5MA.js → mattermost-36YSAHMQ.js} +4 -4
  54. package/dist/{mem0-7AP5UBJV.js → mem0-EVJ7KY5E.js} +4 -4
  55. package/dist/{memos-MNVRYSLG.js → memos-KDNZATPM.js} +4 -4
  56. package/dist/{metabase-NSXM7EID.js → metabase-MRBJG473.js} +4 -4
  57. package/dist/{mistral_ai-HXN625YX.js → mistral_ai-PDT25XCR.js} +4 -4
  58. package/dist/{mother_duck-46VQ3V3D.js → mother_duck-MN3OHZDF.js} +4 -4
  59. package/dist/{ngrok-BWCVMS3T.js → ngrok-PEGRSO2H.js} +4 -4
  60. package/dist/{notes-AVHKCFLU.js → notes-UUDL64JP.js} +2 -2
  61. package/dist/{notion-BSEN3KSU.js → notion-XRTFGM3Q.js} +4 -4
  62. package/dist/{open_exchange_rates-DWO4T5TF.js → open_exchange_rates-TLNDRZ54.js} +4 -4
  63. package/dist/{openai-HPZYRS3M.js → openai-EUDWNNU6.js} +4 -4
  64. package/dist/{opengraph_io-OLUAYD5P.js → opengraph_io-MKH5AUR3.js} +4 -4
  65. package/dist/{openrouter-4DQYZIG3.js → openrouter-YMGWGPI3.js} +4 -4
  66. package/dist/{outline-ZDNMKXZ6.js → outline-MTH7SZBU.js} +4 -4
  67. package/dist/{paddle-HCOOYW3F.js → paddle-PWSGQPO6.js} +4 -4
  68. package/dist/{perplexity-2GJJPDA7.js → perplexity-NBUHF5Z6.js} +4 -4
  69. package/dist/{pinecone-ZPOBKL5T.js → pinecone-5FVBDTET.js} +4 -4
  70. package/dist/{pipedrive-3WN2RFMB.js → pipedrive-IJUNMGFQ.js} +4 -4
  71. package/dist/{polar-DJFGBTXW.js → polar-L72J7ISM.js} +4 -4
  72. package/dist/{posthog-G4OXNEXP.js → posthog-4GDGMSSQ.js} +4 -4
  73. package/dist/{postmark-NJMGDQKO.js → postmark-25TFX6LC.js} +4 -4
  74. package/dist/{prerender-2TUPKRK5.js → prerender-ZY7S5S4R.js} +4 -4
  75. package/dist/{pubmed-CCXZZT34.js → pubmed-KCPFV76N.js} +4 -4
  76. package/dist/{railway-ZUUY5YYR.js → railway-IWOCVOXL.js} +4 -4
  77. package/dist/{readwise-PYSRGJ4J.js → readwise-JCN5J4ON.js} +4 -4
  78. package/dist/{render-SFA3EMGM.js → render-FUM4QWKI.js} +4 -4
  79. package/dist/{replicate-KGZHYBPK.js → replicate-V5VIAEYB.js} +4 -4
  80. package/dist/{resend-HENMZC5U.js → resend-F3XQOI24.js} +4 -4
  81. package/dist/{rootly-HV2PXIDP.js → rootly-XEDRHK66.js} +4 -4
  82. package/dist/{scrapfly-FOZFHUN3.js → scrapfly-PXDWBCOD.js} +4 -4
  83. package/dist/{scrapingbee-VHUMSO2J.js → scrapingbee-X5QCNZSS.js} +4 -4
  84. package/dist/{screenshot_fyi-FFDXAKHD.js → screenshot_fyi-V6CE6WQS.js} +4 -4
  85. package/dist/{semantic_scholar-GHO37BZF.js → semantic_scholar-NHEQ5J72.js} +4 -4
  86. package/dist/{sentry-MZVZW3YE.js → sentry-XDLPYMQI.js} +4 -4
  87. package/dist/{shodan-IDICOPGU.js → shodan-H2YTSK7R.js} +4 -4
  88. package/dist/{shopify-WQLO23JO.js → shopify-Q5YDMDAD.js} +4 -4
  89. package/dist/{stripe-XOAKLAKU.js → stripe-ZXSIKY6A.js} +4 -4
  90. package/dist/{supabase-6DKGSLST.js → supabase-TV2ZEVH5.js} +4 -4
  91. package/dist/{tavily-76QZGEHM.js → tavily-KKBMTUW6.js} +4 -4
  92. package/dist/{telegram-O536CLFR.js → telegram-JM4VE5FR.js} +4 -4
  93. package/dist/{telnyx-XOPYXF3K.js → telnyx-NOXAAP6B.js} +4 -4
  94. package/dist/{todoist-YXHVWPP3.js → todoist-ATYSMZ2W.js} +4 -4
  95. package/dist/{trello-WMTM3WA2.js → trello-BWRSV7VZ.js} +4 -4
  96. package/dist/{turso-CG7PR5HM.js → turso-P7Z4X5C5.js} +4 -4
  97. package/dist/{twilio-ZW4SD36D.js → twilio-YL7NUXCX.js} +4 -4
  98. package/dist/{umami-IJQ5VH77.js → umami-JPWFBAPI.js} +4 -4
  99. package/dist/{unsplash-JBQY7NKO.js → unsplash-CJBI3UGZ.js} +4 -4
  100. package/dist/{upstash_redis-2BAO776T.js → upstash_redis-J5IGSHWB.js} +4 -4
  101. package/dist/{uptimerobot-BRIHMPB3.js → uptimerobot-HEFFBT3N.js} +4 -4
  102. package/dist/{v2ex-HWIHF4HX.js → v2ex-MRYW7RM6.js} +4 -4
  103. package/dist/{vercel-MGMRY2DJ.js → vercel-RNY4ZRTH.js} +4 -4
  104. package/dist/{wolfram_alpha_api-JG6JD33L.js → wolfram_alpha_api-AF346YID.js} +4 -4
  105. package/dist/{wordpress-ZSYJERWV.js → wordpress-YWKGCQA5.js} +4 -4
  106. package/dist/{workos-QFEOSJAN.js → workos-Y7AORGEG.js} +4 -4
  107. package/dist/{zhihu-5RF7BBFV.js → zhihu-X4FAKOAQ.js} +4 -4
  108. package/package.json +8 -6
@@ -18,6 +18,12 @@ import {
18
18
  NEVER,
19
19
  NodeRegistryStore,
20
20
  OAUTH_CLIENT_FIELDS,
21
+ PG_SEARCH_INSERT_ROWS_MAX,
22
+ PG_SEARCH_SNAPSHOTS_COLUMNS,
23
+ PG_SEARCH_SNAPSHOTS_TABLE,
24
+ PG_SEARCH_TOOLS_COLUMNS,
25
+ PG_SEARCH_TOOLS_TABLE,
26
+ PG_SEARCH_WRITE_LOCK_KEY,
21
27
  PRESIGN_TTL_SEC_DEFAULT,
22
28
  RemoteAllowlistStore,
23
29
  SKILLHUB_CAPABILITIES,
@@ -106,7 +112,10 @@ import {
106
112
  parseNodeInput,
107
113
  parseTokenResponse,
108
114
  parseVia,
115
+ pgBulkInsertSql,
116
+ pgSearchDialect,
109
117
  pluginTokenSecretName,
118
+ prefixUpperBound,
110
119
  preprocess,
111
120
  record,
112
121
  renderHelpDsl,
@@ -132,7 +141,7 @@ import {
132
141
  url,
133
142
  validatePath,
134
143
  virtualizeTools
135
- } from "./chunk-76YX7XSX.js";
144
+ } from "./chunk-AWXZMW6M.js";
136
145
 
137
146
  // src/config.ts
138
147
  var DEFAULT_PORT = 8787;
@@ -151,7 +160,9 @@ function configFromEnv(env = process.env) {
151
160
  const allowInsecure = env.TB_ALLOW_INSECURE_HTTP === "true";
152
161
  const allowlist = (env.TB_REMOTE_ALLOWLIST ?? "").split(",").map((s) => s.trim()).filter((s) => s.length > 0);
153
162
  const config = {
154
- port: portEnv(env.TB_PORT) ?? DEFAULT_PORT,
163
+ // TB_PORT 优先,PORT 兜底:Railway / Fly / Cloud Run / CF Container 等 PaaS 只注入
164
+ // PORT,不认识 TB_PORT —— 不兜底的话容器会监听 8787 而平台探活另一个端口,部署直接失败。
165
+ port: portEnv(env.TB_PORT) ?? portEnv(env.PORT) ?? DEFAULT_PORT,
155
166
  host: env.TB_HOST !== void 0 && env.TB_HOST.length > 0 ? env.TB_HOST : "0.0.0.0",
156
167
  dataDir: env.TB_DATA_DIR !== void 0 && env.TB_DATA_DIR.length > 0 ? env.TB_DATA_DIR : "./data",
157
168
  allowInsecureHttp: allowInsecure,
@@ -162,9 +173,45 @@ function configFromEnv(env = process.env) {
162
173
  ...env.TB_INSTANCE_ID !== void 0 && env.TB_INSTANCE_ID.length > 0 ? { instanceId: env.TB_INSTANCE_ID } : {},
163
174
  allowInsecure
164
175
  },
165
- deviceReclaimSec: positiveIntEnv(env.TB_DEVICE_RECLAIM_SEC) ?? DEFAULT_DEVICE_RECLAIM_SEC
176
+ deviceReclaimSec: positiveIntEnv(env.TB_DEVICE_RECLAIM_SEC) ?? DEFAULT_DEVICE_RECLAIM_SEC,
177
+ // 0 合法(立即关停,本地/单机默认);positiveIntEnv 拒 0,故单独解析。
178
+ shutdownDrainSec: (() => {
179
+ const n = Number(env.TB_SHUTDOWN_DRAIN_SEC);
180
+ return Number.isInteger(n) && n >= 0 ? n : 0;
181
+ })()
166
182
  };
167
183
  if (env.TB_UI_DIR !== void 0 && env.TB_UI_DIR.length > 0) config.uiDir = env.TB_UI_DIR;
184
+ if (env.TB_DATABASE_URL !== void 0 && env.TB_DATABASE_URL.length > 0) {
185
+ config.databaseUrl = env.TB_DATABASE_URL;
186
+ }
187
+ if (env.TB_REDIS_URL !== void 0 && env.TB_REDIS_URL.length > 0) {
188
+ config.redisUrl = env.TB_REDIS_URL;
189
+ }
190
+ if (env.TB_REPLICA_ID !== void 0 && env.TB_REPLICA_ID.length > 0) {
191
+ config.replicaId = env.TB_REPLICA_ID;
192
+ }
193
+ const s3Vars = [
194
+ ["TB_OBJECT_STORE_ENDPOINT", env.TB_OBJECT_STORE_ENDPOINT],
195
+ ["TB_OBJECT_STORE_BUCKET", env.TB_OBJECT_STORE_BUCKET],
196
+ ["TB_OBJECT_STORE_ACCESS_KEY_ID", env.TB_OBJECT_STORE_ACCESS_KEY_ID],
197
+ ["TB_OBJECT_STORE_SECRET_ACCESS_KEY", env.TB_OBJECT_STORE_SECRET_ACCESS_KEY]
198
+ ];
199
+ const present = s3Vars.filter(([, value]) => value !== void 0 && value.length > 0);
200
+ if (present.length > 0) {
201
+ if (present.length < s3Vars.length) {
202
+ const missing = s3Vars.filter(([, value]) => value === void 0 || value.length === 0).map(([name]) => name);
203
+ throw new Error(
204
+ `\u5E73\u53F0\u5BF9\u8C61\u5B58\u50A8\u914D\u7F6E\u4E0D\u5B8C\u6574(\u7ED9\u4E86 ${present.length}/${s3Vars.length} \u9879),\u7F3A\u5C11:${missing.join(", ")}`
205
+ );
206
+ }
207
+ config.objectStore = {
208
+ accessKeyId: env.TB_OBJECT_STORE_ACCESS_KEY_ID,
209
+ bucket: env.TB_OBJECT_STORE_BUCKET,
210
+ endpoint: env.TB_OBJECT_STORE_ENDPOINT,
211
+ secretAccessKey: env.TB_OBJECT_STORE_SECRET_ACCESS_KEY,
212
+ ...env.TB_OBJECT_STORE_REGION !== void 0 && env.TB_OBJECT_STORE_REGION.length > 0 ? { region: env.TB_OBJECT_STORE_REGION } : {}
213
+ };
214
+ }
168
215
  if (env.TB_BOOTSTRAP_ADMIN_SK !== void 0 && env.TB_BOOTSTRAP_ADMIN_SK.length > 0) {
169
216
  config.adminSk = env.TB_BOOTSTRAP_ADMIN_SK;
170
217
  }
@@ -396,8 +443,15 @@ var BUILTIN_DESCRIPTIONS = {
396
443
  async function mintAdminWithPlaintext(store, plaintext, now) {
397
444
  const { key } = await mintKey(adminBootstrapInput(), now);
398
445
  const adminKey = { ...key, hash: await sha256Hex(plaintext) };
399
- await store.put(KEY_SK_HASH + adminKey.hash, adminKey);
400
- await store.put(KEY_SK_ID + adminKey.id, adminKey.hash);
446
+ const hashKey = KEY_SK_HASH + adminKey.hash;
447
+ let won;
448
+ if (store.putIfAbsent !== void 0) {
449
+ won = await store.putIfAbsent(hashKey, adminKey);
450
+ } else {
451
+ won = await store.get(hashKey) === null;
452
+ if (won) await store.put(hashKey, adminKey);
453
+ }
454
+ if (won) await store.put(KEY_SK_ID + adminKey.id, adminKey.hash);
401
455
  }
402
456
  async function ensureBuiltinNodes(registry, now) {
403
457
  const ensure = async (node) => {
@@ -14208,8 +14262,8 @@ function applyPostAuth(clientId, clientSecret, params) {
14208
14262
  function applyPublicAuth(clientId, params) {
14209
14263
  params.set("client_id", clientId);
14210
14264
  }
14211
- function isLoopbackHost(hostname) {
14212
- return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname === "::1";
14265
+ function isLoopbackHost(hostname2) {
14266
+ return hostname2 === "localhost" || hostname2 === "127.0.0.1" || hostname2 === "[::1]" || hostname2 === "::1";
14213
14267
  }
14214
14268
  function assertSecureTokenEndpoint(tokenEndpoint) {
14215
14269
  const url2 = new URL(String(tokenEndpoint));
@@ -36477,6 +36531,17 @@ function registerPublicRoutes(app, env) {
36477
36531
  ...catalog !== void 0 && digest !== void 0 ? { catalog: { count: Object.keys(catalog).length, digest } } : {}
36478
36532
  });
36479
36533
  });
36534
+ app.get("/livez", (c) => c.json({ live: true }));
36535
+ app.get("/readyz", async (c) => {
36536
+ const probe = deps.readiness;
36537
+ if (probe === void 0) return c.json({ checks: {}, ready: true });
36538
+ try {
36539
+ const report = await probe();
36540
+ return c.json(report, report.ready ? 200 : 503);
36541
+ } catch {
36542
+ return c.json({ checks: {}, ready: false }, 503);
36543
+ }
36544
+ });
36480
36545
  app.get(
36481
36546
  "/~ref/:token",
36482
36547
  (c) => runHandler(async () => {
@@ -37711,14 +37776,26 @@ var DeviceHub = class {
37711
37776
  connections = /* @__PURE__ */ new Set();
37712
37777
  reclaimTimers = /* @__PURE__ */ new Map();
37713
37778
  heartbeat;
37779
+ /** 多副本路由;缺省(单副本)时所有跨副本逻辑短路,行为与之前完全一致。 */
37780
+ router;
37714
37781
  constructor(opts) {
37715
37782
  this.store = opts.store;
37716
37783
  this.search = opts.search;
37717
37784
  this.reclaimSec = opts.reclaimSec;
37785
+ this.router = opts.router?.(
37786
+ async (deviceId, req) => await this.invokeLocal(deviceId, req)
37787
+ );
37718
37788
  const heartbeatMs = opts.heartbeatMs ?? DEFAULT_HEARTBEAT_MS;
37719
37789
  this.heartbeat = setInterval(() => this.pingConnections(), heartbeatMs);
37720
37790
  this.heartbeat.unref?.();
37721
37791
  }
37792
+ /**
37793
+ * 启动多副本路由(订阅本副本频道 + 路由续期)。单副本时是 no-op。
37794
+ * 必须在开始服务前完成:订阅没建好时转发进来的调用会丢。
37795
+ */
37796
+ async startRouter() {
37797
+ await this.router?.start();
37798
+ }
37722
37799
  /** 挂到 http.Server 的 'upgrade' 事件(仅处理 DEVICE_WS_PATH,其余 404)。 */
37723
37800
  attach(server) {
37724
37801
  server.on("upgrade", (req, socket, head) => {
@@ -37730,8 +37807,26 @@ var DeviceHub = class {
37730
37807
  });
37731
37808
  });
37732
37809
  }
37733
- /** DeviceChannel.invoke:HTTP→WS 调用转发(无活连接 → deviceOffline)。 */
37810
+ /**
37811
+ * DeviceChannel.invoke:HTTP→WS 调用转发。
37812
+ *
37813
+ * 本副本持有该设备 → 直接走本地 socket(单副本部署恒走这条,零额外开销)。
37814
+ * 本地没有但配了 router → 问路由表并转发给持有者副本(socket 不可序列化,
37815
+ * 只能把调用送到 socket 所在进程)。都没有 → deviceOffline。
37816
+ */
37734
37817
  async invoke(deviceId, req) {
37818
+ if (this.activeByDevice.has(deviceId)) return await this.invokeLocal(deviceId, req);
37819
+ const offline = { ok: false, error: TBError.deviceOffline().toJSON() };
37820
+ if (this.router === void 0) return offline;
37821
+ return await this.router.forward(deviceId, req) ?? offline;
37822
+ }
37823
+ /**
37824
+ * 只走本副本 socket 的调用执行。
37825
+ *
37826
+ * router 收到转发来的调用时回调这里 —— **不能**回调 `invoke`:本地连接刚断时
37827
+ * 那会再次查路由并转发,在两个副本间弹来弹去。这里没有连接就直接 offline。
37828
+ */
37829
+ async invokeLocal(deviceId, req) {
37735
37830
  const offline = { ok: false, error: TBError.deviceOffline().toJSON() };
37736
37831
  const conn = this.activeByDevice.get(deviceId);
37737
37832
  if (conn === void 0) return offline;
@@ -37769,6 +37864,9 @@ var DeviceHub = class {
37769
37864
  if (!isDeviceMeta(item.value)) continue;
37770
37865
  const meta = item.value;
37771
37866
  if (this.activeByDevice.has(meta.deviceId)) continue;
37867
+ if (this.router !== void 0 && await this.router.isOnlineAnywhere(meta.deviceId)) {
37868
+ continue;
37869
+ }
37772
37870
  if (meta.disconnectedAt === void 0) {
37773
37871
  const now = (/* @__PURE__ */ new Date()).toISOString();
37774
37872
  try {
@@ -37794,6 +37892,10 @@ var DeviceHub = class {
37794
37892
  for (const conn of this.connections) conn.ws.terminate();
37795
37893
  this.connections.clear();
37796
37894
  this.activeByDevice.clear();
37895
+ const router = this.router;
37896
+ this.router = void 0;
37897
+ await router?.close().catch(() => {
37898
+ });
37797
37899
  await new Promise((resolve3) => this.wss.close(() => resolve3()));
37798
37900
  }
37799
37901
  async handleUpgrade(req, socket, head) {
@@ -37880,6 +37982,8 @@ var DeviceHub = class {
37880
37982
  const prev = this.activeByDevice.get(conn.deviceId);
37881
37983
  if (prev !== void 0 && prev !== conn) prev.ws.close(1e3, "replaced");
37882
37984
  this.activeByDevice.set(conn.deviceId, conn);
37985
+ await this.router?.claim(conn.deviceId).catch(() => {
37986
+ });
37883
37987
  conn.session.accept(mountPath);
37884
37988
  }
37885
37989
  /** 连接失效收尾(对位 DO markDisconnected):registry 下线 + meta 记断线 + 排回收。 */
@@ -37888,6 +37992,8 @@ var DeviceHub = class {
37888
37992
  this.connections.delete(conn);
37889
37993
  if (this.activeByDevice.get(conn.deviceId) !== conn) return;
37890
37994
  this.activeByDevice.delete(conn.deviceId);
37995
+ await this.router?.release(conn.deviceId).catch(() => {
37996
+ });
37891
37997
  const raw = await this.store.get(KEY_DEVICE_META + conn.deviceId);
37892
37998
  if (!isDeviceMeta(raw)) return;
37893
37999
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -37918,6 +38024,13 @@ var DeviceHub = class {
37918
38024
  async reclaim(deviceId) {
37919
38025
  this.reclaimTimers.delete(deviceId);
37920
38026
  if (this.activeByDevice.has(deviceId)) return;
38027
+ if (this.router !== void 0) {
38028
+ try {
38029
+ if (await this.router.isOnlineAnywhere(deviceId)) return;
38030
+ } catch {
38031
+ return;
38032
+ }
38033
+ }
37921
38034
  const raw = await this.store.get(KEY_DEVICE_META + deviceId);
37922
38035
  if (!isDeviceMeta(raw)) return;
37923
38036
  const searchSync = this.search === void 0 ? void 0 : new SearchSynchronizer(this.store, this.search);
@@ -37942,6 +38055,146 @@ var DeviceHub = class {
37942
38055
  }
37943
38056
  };
37944
38057
 
38058
+ // src/deviceRouter.ts
38059
+ var DEVICE_ROUTE_TTL_SEC = 30;
38060
+ var DEVICE_FORWARD_TIMEOUT_MS = 65e3;
38061
+ var deviceRouteKey = (deviceId) => `tb:device:route:${deviceId}`;
38062
+ var deviceCallChannel = (replicaId) => `tb:device:call:${replicaId}`;
38063
+ var deviceReplyChannel = (replicaId) => `tb:device:reply:${replicaId}`;
38064
+ var DeviceRouter = class _DeviceRouter {
38065
+ constructor(replicaId, backend, opts) {
38066
+ this.replicaId = replicaId;
38067
+ this.backend = backend;
38068
+ this.opts = opts;
38069
+ }
38070
+ replicaId;
38071
+ backend;
38072
+ opts;
38073
+ pending = /* @__PURE__ */ new Map();
38074
+ renewTimer;
38075
+ /** 本副本当前持有的设备(需周期续期路由条目)。 */
38076
+ owned = /* @__PURE__ */ new Set();
38077
+ get ttlSec() {
38078
+ return this.opts.ttlSec ?? DEVICE_ROUTE_TTL_SEC;
38079
+ }
38080
+ /** 订阅本副本的两个频道并启动路由续期。 */
38081
+ async start() {
38082
+ await this.backend.subscribe(deviceCallChannel(this.replicaId), (payload) => {
38083
+ void this.handleForwardedCall(payload);
38084
+ });
38085
+ await this.backend.subscribe(deviceReplyChannel(this.replicaId), (payload) => {
38086
+ this.handleReply(payload);
38087
+ });
38088
+ const renewMs = Math.max(1e3, Math.floor(this.ttlSec * 1e3 / 3));
38089
+ this.renewTimer = setInterval(() => {
38090
+ void this.renewOwned();
38091
+ }, renewMs);
38092
+ this.renewTimer.unref?.();
38093
+ }
38094
+ /** 本副本接受了某设备的连接:声明所有权。 */
38095
+ async claim(deviceId) {
38096
+ this.owned.add(deviceId);
38097
+ await this.backend.setRoute(deviceId, this.replicaId, this.ttlSec);
38098
+ }
38099
+ /** 本副本失去某设备的连接:释放所有权。 */
38100
+ async release(deviceId) {
38101
+ this.owned.delete(deviceId);
38102
+ await this.backend.clearRoute(deviceId, this.replicaId);
38103
+ }
38104
+ /** 某设备当前是否在**任一**副本上在线(reclaim 判定用,不能只看本副本)。 */
38105
+ async isOnlineAnywhere(deviceId) {
38106
+ return await this.backend.lookupRoute(deviceId) !== null;
38107
+ }
38108
+ /**
38109
+ * 把调用转发给持有该设备的副本并等结果。
38110
+ * 无人持有 → null(调用方按 deviceOffline 处理)。
38111
+ */
38112
+ async forward(deviceId, req) {
38113
+ const owner = await this.backend.lookupRoute(deviceId);
38114
+ if (owner === null) return null;
38115
+ if (owner === this.replicaId) return null;
38116
+ const correlationId = `${this.replicaId}:${deviceId}:${++_DeviceRouter.seq}`;
38117
+ const call = {
38118
+ correlationId,
38119
+ deviceId,
38120
+ replyTo: this.replicaId,
38121
+ req
38122
+ };
38123
+ return await new Promise((resolve3) => {
38124
+ const timer = setTimeout(() => {
38125
+ this.pending.delete(correlationId);
38126
+ resolve3({ ok: false, error: new TBError("unavailable", "\u8BBE\u5907\u8C03\u7528\u8F6C\u53D1\u8D85\u65F6").toJSON() });
38127
+ }, this.opts.forwardTimeoutMs ?? DEVICE_FORWARD_TIMEOUT_MS);
38128
+ timer.unref?.();
38129
+ this.pending.set(correlationId, { resolve: resolve3, timer });
38130
+ void this.backend.publish(deviceCallChannel(owner), JSON.stringify(call)).catch(() => {
38131
+ const waiter = this.pending.get(correlationId);
38132
+ if (waiter === void 0) return;
38133
+ this.pending.delete(correlationId);
38134
+ clearTimeout(waiter.timer);
38135
+ waiter.resolve({
38136
+ ok: false,
38137
+ error: new TBError("unavailable", "\u8BBE\u5907\u8C03\u7528\u8F6C\u53D1\u5931\u8D25").toJSON()
38138
+ });
38139
+ });
38140
+ });
38141
+ }
38142
+ async close() {
38143
+ if (this.renewTimer !== void 0) clearInterval(this.renewTimer);
38144
+ this.renewTimer = void 0;
38145
+ for (const [, waiter] of this.pending) {
38146
+ clearTimeout(waiter.timer);
38147
+ waiter.resolve({ ok: false, error: new TBError("unavailable", "\u7F51\u5173\u6B63\u5728\u5173\u95ED").toJSON() });
38148
+ }
38149
+ this.pending.clear();
38150
+ await Promise.all([...this.owned].map(async (id) => await this.release(id)));
38151
+ await this.backend.close();
38152
+ }
38153
+ /** correlationId 的进程内序号(与 replicaId 组合后全局唯一)。 */
38154
+ static seq = 0;
38155
+ async renewOwned() {
38156
+ for (const deviceId of this.owned) {
38157
+ try {
38158
+ await this.backend.setRoute(deviceId, this.replicaId, this.ttlSec);
38159
+ } catch {
38160
+ }
38161
+ }
38162
+ }
38163
+ /** 作为持有者:执行本地调用并回执。 */
38164
+ async handleForwardedCall(payload) {
38165
+ let call;
38166
+ try {
38167
+ call = JSON.parse(payload);
38168
+ } catch {
38169
+ return;
38170
+ }
38171
+ if (typeof call?.correlationId !== "string" || typeof call?.replyTo !== "string") return;
38172
+ let result;
38173
+ try {
38174
+ result = await this.opts.onLocalCall(call.deviceId, call.req);
38175
+ } catch {
38176
+ result = { ok: false, error: new TBError("internal", "\u8BBE\u5907\u8C03\u7528\u6267\u884C\u5931\u8D25").toJSON() };
38177
+ }
38178
+ const reply = { correlationId: call.correlationId, result };
38179
+ await this.backend.publish(deviceReplyChannel(call.replyTo), JSON.stringify(reply)).catch(() => {
38180
+ });
38181
+ }
38182
+ /** 作为发起方:收到回执,唤醒等待者。 */
38183
+ handleReply(payload) {
38184
+ let reply;
38185
+ try {
38186
+ reply = JSON.parse(payload);
38187
+ } catch {
38188
+ return;
38189
+ }
38190
+ const waiter = this.pending.get(reply?.correlationId);
38191
+ if (waiter === void 0) return;
38192
+ this.pending.delete(reply.correlationId);
38193
+ clearTimeout(waiter.timer);
38194
+ waiter.resolve(reply.result);
38195
+ }
38196
+ };
38197
+
37945
38198
  // ../core/src/node/fsObjectStore.ts
37946
38199
  import { mkdir, readdir, readFile, realpath, rm, stat, writeFile } from "fs/promises";
37947
38200
  import { basename, dirname, join, resolve, sep } from "path";
@@ -38226,6 +38479,205 @@ function createDataObjectStore(dataDir) {
38226
38479
  };
38227
38480
  }
38228
38481
 
38482
+ // src/pgSearchIndex.ts
38483
+ var PgSearchDriver = class {
38484
+ constructor(sql) {
38485
+ this.sql = sql;
38486
+ }
38487
+ sql;
38488
+ schemaReady;
38489
+ async all(statement) {
38490
+ return await this.sql.unsafe(statement.sql, statement.params);
38491
+ }
38492
+ async initializeSchema() {
38493
+ try {
38494
+ await this.sql.begin(async (tx) => {
38495
+ for (const ddl of pgSearchDialect.schemaStatements) {
38496
+ await tx.unsafe(ddl);
38497
+ }
38498
+ });
38499
+ } catch (error) {
38500
+ this.schemaReady = void 0;
38501
+ throw error;
38502
+ }
38503
+ }
38504
+ ensureSchema() {
38505
+ this.schemaReady ??= this.initializeSchema();
38506
+ return this.schemaReady;
38507
+ }
38508
+ async first(statement) {
38509
+ const rows = await this.sql.unsafe(statement.sql, statement.params);
38510
+ return rows[0] ?? null;
38511
+ }
38512
+ /**
38513
+ * 攒成多行 VALUES 批量插入,而非逐条。
38514
+ * 逐条对 4000 行需 ~920ms(每行一次事务内往返),批量后 ~12ms。
38515
+ */
38516
+ bulk(items, table, columns, toParams) {
38517
+ const statements = [];
38518
+ for (let i = 0; i < items.length; i += PG_SEARCH_INSERT_ROWS_MAX) {
38519
+ const chunk = items.slice(i, i + PG_SEARCH_INSERT_ROWS_MAX);
38520
+ statements.push({
38521
+ params: chunk.flatMap(toParams),
38522
+ sql: pgBulkInsertSql(table, columns, chunk.length)
38523
+ });
38524
+ }
38525
+ return statements;
38526
+ }
38527
+ insertRecords(records) {
38528
+ return this.bulk(
38529
+ records,
38530
+ PG_SEARCH_TOOLS_TABLE,
38531
+ PG_SEARCH_TOOLS_COLUMNS,
38532
+ (record2) => [record2.path, record2.name, record2.description, record2.feedback]
38533
+ );
38534
+ }
38535
+ insertSnapshots(digests) {
38536
+ return this.bulk(
38537
+ [...digests],
38538
+ PG_SEARCH_SNAPSHOTS_TABLE,
38539
+ PG_SEARCH_SNAPSHOTS_COLUMNS,
38540
+ ([path, digest]) => [path, digest]
38541
+ );
38542
+ }
38543
+ async write(statements) {
38544
+ await this.sql.begin(async (tx) => {
38545
+ await tx`SELECT pg_advisory_xact_lock(${PG_SEARCH_WRITE_LOCK_KEY})`;
38546
+ for (const statement of statements) {
38547
+ await tx.unsafe(statement.sql, statement.params);
38548
+ }
38549
+ });
38550
+ }
38551
+ };
38552
+ var PgSearchIndex = class extends SqlSearchIndex {
38553
+ constructor(sql) {
38554
+ super(new PgSearchDriver(sql), pgSearchDialect);
38555
+ }
38556
+ };
38557
+
38558
+ // src/pgStateStore.ts
38559
+ var DEFAULT_LIST_LIMIT = 1e3;
38560
+ var PgStateStore = class {
38561
+ constructor(sql) {
38562
+ this.sql = sql;
38563
+ }
38564
+ sql;
38565
+ /** 建表(幂等)。key COLLATE "C" 是字节序正确性的前提,不能省。 */
38566
+ async ensureSchema() {
38567
+ await this.sql`
38568
+ CREATE TABLE IF NOT EXISTS tb_kv (
38569
+ key text COLLATE "C" PRIMARY KEY,
38570
+ value jsonb NOT NULL
38571
+ )
38572
+ `;
38573
+ }
38574
+ async get(key) {
38575
+ const rows = await this.sql`
38576
+ SELECT value FROM tb_kv WHERE key = ${key}
38577
+ `;
38578
+ const row = rows[0];
38579
+ return row === void 0 ? null : row.value;
38580
+ }
38581
+ async getMany(keys) {
38582
+ const unique = [...new Set(keys)];
38583
+ if (unique.length === 0) return /* @__PURE__ */ new Map();
38584
+ const rows = await this.sql`
38585
+ SELECT key, value FROM tb_kv WHERE key = ANY(${this.sql.array(unique)})
38586
+ `;
38587
+ const byKey = new Map(rows.map((row) => [row.key, row.value]));
38588
+ const out = /* @__PURE__ */ new Map();
38589
+ for (const key of unique) {
38590
+ if (byKey.has(key)) out.set(key, byKey.get(key));
38591
+ }
38592
+ return out;
38593
+ }
38594
+ async put(key, value) {
38595
+ await this.sql`
38596
+ INSERT INTO tb_kv (key, value) VALUES (${key}, ${this.sql.json(value)})
38597
+ ON CONFLICT (key) DO UPDATE SET value = excluded.value
38598
+ `;
38599
+ }
38600
+ async putIfAbsent(key, value) {
38601
+ const result = await this.sql`
38602
+ INSERT INTO tb_kv (key, value) VALUES (${key}, ${this.sql.json(value)})
38603
+ ON CONFLICT (key) DO NOTHING
38604
+ `;
38605
+ return result.count > 0;
38606
+ }
38607
+ async delete(key) {
38608
+ await this.sql`DELETE FROM tb_kv WHERE key = ${key}`;
38609
+ }
38610
+ async list(prefix, opts) {
38611
+ const limit = opts?.limit ?? DEFAULT_LIST_LIMIT;
38612
+ const lowerByCursor = opts?.cursor !== void 0 && opts.cursor >= prefix;
38613
+ const upper = prefix === "" ? void 0 : prefixUpperBound(prefix);
38614
+ const rows = await this.sql`
38615
+ SELECT key, value FROM tb_kv
38616
+ WHERE ${lowerByCursor ? this.sql`key > ${opts?.cursor ?? ""}` : this.sql`key >= ${prefix}`}
38617
+ ${upper !== void 0 ? this.sql`AND key < ${upper}` : this.sql``}
38618
+ ORDER BY key
38619
+ LIMIT ${limit + 1}
38620
+ `;
38621
+ const matched = rows.filter((r) => r.key.startsWith(prefix));
38622
+ const hasMore = matched.length > limit;
38623
+ const page = hasMore ? matched.slice(0, limit) : matched;
38624
+ const items = page.map((r) => ({ key: r.key, value: r.value }));
38625
+ const last = page[page.length - 1];
38626
+ return hasMore && last !== void 0 ? { items, cursor: last.key } : { items };
38627
+ }
38628
+ };
38629
+
38630
+ // src/redisDeviceRouter.ts
38631
+ import { Redis } from "ioredis";
38632
+ var CLEAR_IF_OWNER = `
38633
+ if redis.call('get', KEYS[1]) == ARGV[1] then
38634
+ return redis.call('del', KEYS[1])
38635
+ end
38636
+ return 0
38637
+ `;
38638
+ var RedisDeviceRouterBackend = class {
38639
+ /** 命令连接(GET/SET/DEL/PUBLISH)。 */
38640
+ commands;
38641
+ /** 订阅连接(进入 subscribe 模式后不能发普通命令)。 */
38642
+ subscriber;
38643
+ handlers = /* @__PURE__ */ new Map();
38644
+ constructor(url2) {
38645
+ this.commands = new Redis(url2, { maxRetriesPerRequest: 2 });
38646
+ this.subscriber = new Redis(url2, { maxRetriesPerRequest: 2 });
38647
+ this.subscriber.on("message", (channel, payload) => {
38648
+ this.handlers.get(channel)?.(payload);
38649
+ });
38650
+ this.commands.on("error", () => {
38651
+ });
38652
+ this.subscriber.on("error", () => {
38653
+ });
38654
+ }
38655
+ async setRoute(deviceId, replicaId, ttlSec) {
38656
+ await this.commands.set(deviceRouteKey(deviceId), replicaId, "EX", ttlSec);
38657
+ }
38658
+ async lookupRoute(deviceId) {
38659
+ return await this.commands.get(deviceRouteKey(deviceId));
38660
+ }
38661
+ async clearRoute(deviceId, replicaId) {
38662
+ await this.commands.eval(CLEAR_IF_OWNER, 1, deviceRouteKey(deviceId), replicaId);
38663
+ }
38664
+ async publish(channel, payload) {
38665
+ await this.commands.publish(channel, payload);
38666
+ }
38667
+ /** 就绪探测(/readyz):命令连接 PING 一次。断连时 ioredis 会抛/挂起,由调用方限时。 */
38668
+ async ping() {
38669
+ await this.commands.ping();
38670
+ }
38671
+ async subscribe(channel, handler) {
38672
+ this.handlers.set(channel, handler);
38673
+ await this.subscriber.subscribe(channel);
38674
+ }
38675
+ async close() {
38676
+ this.handlers.clear();
38677
+ await Promise.allSettled([this.subscriber.quit(), this.commands.quit()]);
38678
+ }
38679
+ };
38680
+
38229
38681
  // src/sqliteSearchIndex.ts
38230
38682
  import Database from "better-sqlite3";
38231
38683
  var SCHEMA_SQL = TOOL_SEARCH_SCHEMA_STATEMENTS.map((sql) => `${sql};`).join("\n");
@@ -38287,16 +38739,7 @@ var SqliteSearchIndex = class extends SqlSearchIndex {
38287
38739
 
38288
38740
  // src/sqliteStateStore.ts
38289
38741
  import Database2 from "better-sqlite3";
38290
- var DEFAULT_LIST_LIMIT = 1e3;
38291
- function prefixUpperBound(prefix) {
38292
- for (let i = prefix.length - 1; i >= 0; i--) {
38293
- const code = prefix.charCodeAt(i);
38294
- if (code < 65535) {
38295
- return prefix.slice(0, i) + String.fromCharCode(code + 1);
38296
- }
38297
- }
38298
- return void 0;
38299
- }
38742
+ var DEFAULT_LIST_LIMIT2 = 1e3;
38300
38743
  var SqliteStateStore = class {
38301
38744
  db;
38302
38745
  stmtGet;
@@ -38336,11 +38779,15 @@ var SqliteStateStore = class {
38336
38779
  async put(key, value) {
38337
38780
  this.stmtPut.run(key, JSON.stringify(value));
38338
38781
  }
38782
+ async putIfAbsent(key, value) {
38783
+ const info = this.db.prepare("INSERT OR IGNORE INTO kv (key, value) VALUES (?, ?)").run(key, JSON.stringify(value));
38784
+ return info.changes > 0;
38785
+ }
38339
38786
  async delete(key) {
38340
38787
  this.stmtDelete.run(key);
38341
38788
  }
38342
38789
  async list(prefix, opts) {
38343
- const limit = opts?.limit ?? DEFAULT_LIST_LIMIT;
38790
+ const limit = opts?.limit ?? DEFAULT_LIST_LIMIT2;
38344
38791
  const lowerByCursor = opts?.cursor !== void 0 && opts.cursor >= prefix;
38345
38792
  const upper = prefix === "" ? void 0 : prefixUpperBound(prefix);
38346
38793
  const conditions = [];
@@ -38371,14 +38818,16 @@ var SqliteStateStore = class {
38371
38818
 
38372
38819
  // src/server.ts
38373
38820
  import { serve } from "@hono/node-server";
38821
+ import postgres from "postgres";
38374
38822
  import { mkdirSync as mkdirSync2 } from "fs";
38823
+ import { hostname } from "os";
38375
38824
  import { join as join4 } from "path";
38376
38825
 
38377
38826
  // package.json
38378
38827
  var package_default = {
38379
38828
  name: "@tool-bridge/server",
38380
- version: "0.7.0",
38381
- description: "tool-bridge self-hosted server: run the HTBP gateway on Node with SQLite persistence, filesystem object storage and WebSocket device channel \u2014 single container via docker run",
38829
+ version: "0.10.1",
38830
+ description: "tool-bridge self-hosted server: run the HTBP gateway on Node with SQLite or PostgreSQL persistence, filesystem object storage and WebSocket device channel \u2014 single container via docker run",
38382
38831
  type: "module",
38383
38832
  license: "MIT",
38384
38833
  main: "src/index.ts",
@@ -38426,6 +38875,8 @@ var package_default = {
38426
38875
  aws4fetch: "catalog:",
38427
38876
  "better-sqlite3": "^12.6.0",
38428
38877
  hono: "catalog:",
38878
+ ioredis: "^6.0.0",
38879
+ postgres: "^3.4.9",
38429
38880
  ws: "catalog:"
38430
38881
  },
38431
38882
  devDependencies: {
@@ -38490,24 +38941,116 @@ function resolveUiAssets(uiDirOverride) {
38490
38941
  }
38491
38942
 
38492
38943
  // src/server.ts
38493
- function createTbServer(config) {
38494
- mkdirSync2(config.dataDir, { recursive: true });
38495
- const dbPath = join4(config.dataDir, "state.sqlite3");
38496
- const state = new SqliteStateStore(dbPath);
38497
- let search;
38944
+ function pgBackends(databaseUrl) {
38945
+ const sql = postgres(databaseUrl, { onnotice: () => {
38946
+ } });
38947
+ const state = new PgStateStore(sql);
38948
+ const search = new PgSearchIndex(sql);
38949
+ return {
38950
+ state: {
38951
+ value: state,
38952
+ ensureReady: async () => await state.ensureSchema(),
38953
+ ping: async () => {
38954
+ await sql`SELECT 1`;
38955
+ },
38956
+ close: async () => await sql.end({ timeout: 5 })
38957
+ },
38958
+ search: {
38959
+ value: search,
38960
+ ensureReady: async () => {
38961
+ await search.initialized();
38962
+ },
38963
+ close: async () => {
38964
+ }
38965
+ }
38966
+ };
38967
+ }
38968
+ function sqliteStateBackend(dataDir) {
38969
+ const store = new SqliteStateStore(join4(dataDir, "state.sqlite3"));
38970
+ return {
38971
+ value: store,
38972
+ ensureReady: async () => {
38973
+ },
38974
+ close: async () => store.close()
38975
+ };
38976
+ }
38977
+ function sqliteSearchBackend(dataDir) {
38978
+ const index = new SqliteSearchIndex(join4(dataDir, "state.sqlite3"));
38979
+ return {
38980
+ value: index,
38981
+ ensureReady: async () => {
38982
+ },
38983
+ close: async () => index.close()
38984
+ };
38985
+ }
38986
+ function resolveBackends(config) {
38987
+ if (config.databaseUrl !== void 0) return pgBackends(config.databaseUrl);
38988
+ const state = sqliteStateBackend(config.dataDir);
38498
38989
  try {
38499
- search = new SqliteSearchIndex(dbPath);
38990
+ return { search: sqliteSearchBackend(config.dataDir), state };
38500
38991
  } catch (error) {
38501
- state.close();
38992
+ state.close().catch(() => {
38993
+ });
38502
38994
  throw error;
38503
38995
  }
38996
+ }
38997
+ function createTbServer(config) {
38998
+ mkdirSync2(config.dataDir, { recursive: true });
38999
+ const backends = resolveBackends(config);
39000
+ const state = backends.state.value;
39001
+ const search = backends.search.value;
38504
39002
  const secrets = new SecretStoreImpl(state, config.encryptionKey);
38505
- const objects = createDataObjectStore(config.dataDir);
39003
+ const objects = config.objectStore === void 0 ? createDataObjectStore(config.dataDir) : createS3ObjectStore(config.objectStore, { allowInsecure: config.allowInsecureHttp });
39004
+ const redisBackend = config.redisUrl === void 0 ? void 0 : new RedisDeviceRouterBackend(config.redisUrl);
38506
39005
  const hub = new DeviceHub({
38507
39006
  store: state,
38508
39007
  search,
38509
- reclaimSec: config.deviceReclaimSec
38510
- });
39008
+ reclaimSec: config.deviceReclaimSec,
39009
+ // 配了 Redis 才启用多副本路由;缺省单副本,设备调用恒走本地 socket。
39010
+ ...redisBackend === void 0 ? {} : {
39011
+ router: (onLocalCall) => new DeviceRouter(
39012
+ config.replicaId ?? hostname(),
39013
+ redisBackend,
39014
+ { onLocalCall }
39015
+ )
39016
+ }
39017
+ });
39018
+ let draining = false;
39019
+ const probeOne = async (fn) => {
39020
+ let timer;
39021
+ try {
39022
+ await Promise.race([
39023
+ fn(),
39024
+ new Promise((_, reject) => {
39025
+ timer = setTimeout(() => reject(new Error("timeout after 1000ms")), 1e3);
39026
+ timer.unref?.();
39027
+ })
39028
+ ]);
39029
+ return { ok: true };
39030
+ } catch (err) {
39031
+ return { ok: false, detail: err instanceof Error ? err.message : "probe failed" };
39032
+ } finally {
39033
+ if (timer !== void 0) clearTimeout(timer);
39034
+ }
39035
+ };
39036
+ const readiness = async () => {
39037
+ const checks = {};
39038
+ const probes = [];
39039
+ const statePing = backends.state.ping;
39040
+ if (statePing !== void 0) {
39041
+ probes.push(probeOne(statePing).then((r) => {
39042
+ checks.state = r;
39043
+ }));
39044
+ }
39045
+ if (redisBackend !== void 0) {
39046
+ probes.push(probeOne(async () => await redisBackend.ping()).then((r) => {
39047
+ checks.redis = r;
39048
+ }));
39049
+ }
39050
+ await Promise.all(probes);
39051
+ if (draining) checks.draining = { ok: false, detail: "shutting down" };
39052
+ return { checks, ready: !draining && Object.values(checks).every((c) => c.ok) };
39053
+ };
38511
39054
  const deps = {
38512
39055
  state,
38513
39056
  secrets,
@@ -38516,7 +39059,8 @@ function createTbServer(config) {
38516
39059
  search,
38517
39060
  allowInsecureHttp: config.allowInsecureHttp,
38518
39061
  objects: () => objects,
38519
- device: hub
39062
+ device: hub,
39063
+ readiness
38520
39064
  };
38521
39065
  if (config.encryptionKey !== void 0) deps.encryptionKey = config.encryptionKey;
38522
39066
  if (config.pluginBindings !== void 0) deps.pluginBindings = config.pluginBindings;
@@ -38535,6 +39079,9 @@ function createTbServer(config) {
38535
39079
  state,
38536
39080
  deviceHub: hub,
38537
39081
  async start() {
39082
+ await backends.state.ensureReady();
39083
+ await backends.search.ensureReady();
39084
+ await hub.startRouter();
38538
39085
  await runBootstrap(state, {
38539
39086
  ...config.adminSk !== void 0 ? { adminSk: config.adminSk } : {},
38540
39087
  requireAdminSk: !config.allowInsecureBootstrap
@@ -38547,16 +39094,24 @@ function createTbServer(config) {
38547
39094
  hub.attach(server);
38548
39095
  });
38549
39096
  },
39097
+ startDraining() {
39098
+ draining = true;
39099
+ },
38550
39100
  async close() {
38551
- await hub.close();
39101
+ draining = true;
39102
+ let closed;
38552
39103
  if (server !== void 0) {
38553
- await new Promise((resolve3, reject) => {
38554
- server?.close((err) => err ? reject(err) : resolve3());
39104
+ const s = server;
39105
+ closed = new Promise((resolve3, reject) => {
39106
+ s.close((err) => err ? reject(err) : resolve3());
38555
39107
  });
38556
- server = void 0;
39108
+ s.closeIdleConnections?.();
38557
39109
  }
38558
- search.close();
38559
- state.close();
39110
+ await hub.close();
39111
+ if (closed !== void 0) await closed;
39112
+ server = void 0;
39113
+ await backends.search.close();
39114
+ await backends.state.close();
38560
39115
  }
38561
39116
  };
38562
39117
  }
@@ -38565,7 +39120,13 @@ export {
38565
39120
  configFromEnv,
38566
39121
  DEVICE_WS_PATH,
38567
39122
  DeviceHub,
39123
+ DEVICE_ROUTE_TTL_SEC,
39124
+ DEVICE_FORWARD_TIMEOUT_MS,
39125
+ DeviceRouter,
38568
39126
  createDataObjectStore,
39127
+ PgSearchIndex,
39128
+ PgStateStore,
39129
+ RedisDeviceRouterBackend,
38569
39130
  SqliteSearchIndex,
38570
39131
  SqliteStateStore,
38571
39132
  createTbServer