@tool-bridge/server 0.7.0 → 0.10.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.
- package/dist/{airtable-BNVPDLF2.js → airtable-5W6SJMSM.js} +4 -4
- package/dist/{alt_text_generator_ai-RMQPHK2Y.js → alt_text_generator_ai-YYVJM3RG.js} +4 -4
- package/dist/{amap-PRNZPQ34.js → amap-EREMQZHQ.js} +4 -4
- package/dist/{anthropic-A76VIXR7.js → anthropic-53H7WGSE.js} +4 -4
- package/dist/{apify-2Q2CTLKE.js → apify-CMRNU7VZ.js} +4 -4
- package/dist/{brave_search-5YJJVPXH.js → brave_search-AFZVZ3QK.js} +4 -4
- package/dist/{chunk-BUHY4IE6.js → chunk-2DA2UI37.js} +2 -2
- package/dist/{chunk-GMHELPK3.js → chunk-Q44CYUSI.js} +1 -1
- package/dist/{chunk-7ASYWVYP.js → chunk-Q7YHZ2SD.js} +624 -36
- package/dist/{chunk-EBO6Q4NR.js → chunk-SBJMMSPA.js} +1 -1
- package/dist/{chunk-76YX7XSX.js → chunk-TPGTA43F.js} +228 -48
- package/dist/{circleci-TDXP3CPQ.js → circleci-JG7RV2RU.js} +4 -4
- package/dist/{clerk-JRSGMXMS.js → clerk-U7OS5ALG.js} +4 -4
- package/dist/{cloudflare_dns-G4CHNLO7.js → cloudflare_dns-ADCTFMMI.js} +4 -4
- package/dist/{cohere-3KN27F2B.js → cohere-Z7ZEC2YV.js} +4 -4
- package/dist/{confluence-QV6J2PBV.js → confluence-FT22JGYQ.js} +4 -4
- package/dist/{convertapi-HETITAHL.js → convertapi-UQQCWVWP.js} +4 -4
- package/dist/{currencyapi-647DEY6G.js → currencyapi-Y63O7JTP.js} +4 -4
- package/dist/{deepseek-G63RUHM6.js → deepseek-7TSAMZJ6.js} +4 -4
- package/dist/{docker_hub-RH72CRWO.js → docker_hub-AJYTVI25.js} +4 -4
- package/dist/{dropbox-T4Y5KSZW.js → dropbox-CNGGNR5J.js} +4 -4
- package/dist/{dub-ECKQT3DC.js → dub-WTADKHA7.js} +4 -4
- package/dist/{e2b-N5SRTPLF.js → e2b-75EKXMN5.js} +4 -4
- package/dist/{exa-525ZDM7G.js → exa-OFLA5XND.js} +4 -4
- package/dist/{fal_ai-BFMVHAUW.js → fal_ai-5542HB7N.js} +4 -4
- package/dist/{feishu-NIDOD7EW.js → feishu-QQIZ7G24.js} +3 -3
- package/dist/{feishu_custom_bot-V5RGAIUH.js → feishu_custom_bot-EVDKIZCR.js} +4 -4
- package/dist/{figma-GH7TXJMP.js → figma-ZEBDUMWI.js} +4 -4
- package/dist/{firehydrant-5T3UHAQM.js → firehydrant-CTBNY6FL.js} +4 -4
- package/dist/{fixer-3RMJLMFO.js → fixer-ITDYMRBW.js} +4 -4
- package/dist/{fly-NPHJC5MH.js → fly-IBFMSAAO.js} +4 -4
- package/dist/{front-PYGXJAQK.js → front-AZQLLUJQ.js} +4 -4
- package/dist/{geocodio-T6G3FVIF.js → geocodio-XHZHEWQW.js} +4 -4
- package/dist/{ghost-L3XIF4SJ.js → ghost-RTIKXD3T.js} +4 -4
- package/dist/{github-7YDUB6N6.js → github-WRJBJ5N7.js} +4 -4
- package/dist/{gmail-RY2ZDG3B.js → gmail-527RVDXV.js} +4 -4
- package/dist/{googlecalendar-EHOFMXHE.js → googlecalendar-WZJSK2MR.js} +4 -4
- package/dist/{googledocs-BA3OAMKA.js → googledocs-LFBVIHIK.js} +4 -4
- package/dist/{grafana-PYZQ4CKI.js → grafana-3JEKO7NT.js} +4 -4
- package/dist/{graphhopper-3P4YU45N.js → graphhopper-TOJN6K2Q.js} +4 -4
- package/dist/index.d.ts +370 -26
- package/dist/index.js +14 -2
- package/dist/{ip2proxy-GQO6VF4P.js → ip2proxy-KOA2JUNV.js} +4 -4
- package/dist/{ipgeolocation_io-VOZU6E7H.js → ipgeolocation_io-KNOQD7R7.js} +4 -4
- package/dist/{ipqualityscore-XDJ3TTMM.js → ipqualityscore-GF2MQKXI.js} +4 -4
- package/dist/{jina_ai-ELXPSTUJ.js → jina_ai-GB3CQPJ4.js} +4 -4
- package/dist/{jira-INDXWO4I.js → jira-35XYV6G4.js} +4 -4
- package/dist/{langsmith-PMCQBCIK.js → langsmith-W5234OE7.js} +4 -4
- package/dist/{lemlist-VWZXIHJX.js → lemlist-QB6W2G7N.js} +4 -4
- package/dist/{linear-YXG4KZR4.js → linear-MHBD6A7X.js} +4 -4
- package/dist/{logsnag-XENBYHHK.js → logsnag-RN57XIJS.js} +4 -4
- package/dist/main.js +113 -106
- package/dist/{mattermost-BHVJP5MA.js → mattermost-QRXSZMZA.js} +4 -4
- package/dist/{mem0-7AP5UBJV.js → mem0-K63NN3LA.js} +4 -4
- package/dist/{memos-MNVRYSLG.js → memos-4IOUOFKH.js} +4 -4
- package/dist/{metabase-NSXM7EID.js → metabase-IOE44EKL.js} +4 -4
- package/dist/{mistral_ai-HXN625YX.js → mistral_ai-X7ASGV5I.js} +4 -4
- package/dist/{mother_duck-46VQ3V3D.js → mother_duck-CNN3QWQH.js} +4 -4
- package/dist/{ngrok-BWCVMS3T.js → ngrok-QNE4AHEH.js} +4 -4
- package/dist/{notes-AVHKCFLU.js → notes-Q6OLBS2L.js} +2 -2
- package/dist/{notion-BSEN3KSU.js → notion-ADDRQTCE.js} +4 -4
- package/dist/{open_exchange_rates-DWO4T5TF.js → open_exchange_rates-WR5PJQTG.js} +4 -4
- package/dist/{openai-HPZYRS3M.js → openai-UZBNNAAH.js} +4 -4
- package/dist/{opengraph_io-OLUAYD5P.js → opengraph_io-2JNEXCPJ.js} +4 -4
- package/dist/{openrouter-4DQYZIG3.js → openrouter-EF62BDIH.js} +4 -4
- package/dist/{outline-ZDNMKXZ6.js → outline-O4TIRWJT.js} +4 -4
- package/dist/{paddle-HCOOYW3F.js → paddle-SEFKNENH.js} +4 -4
- package/dist/{perplexity-2GJJPDA7.js → perplexity-FZYOIPDS.js} +4 -4
- package/dist/{pinecone-ZPOBKL5T.js → pinecone-EXDPYDY7.js} +4 -4
- package/dist/{pipedrive-3WN2RFMB.js → pipedrive-EDO4C3AM.js} +4 -4
- package/dist/{polar-DJFGBTXW.js → polar-QJXHVFIP.js} +4 -4
- package/dist/{posthog-G4OXNEXP.js → posthog-U6QEXJE2.js} +4 -4
- package/dist/{postmark-NJMGDQKO.js → postmark-PFZIWXMP.js} +4 -4
- package/dist/{prerender-2TUPKRK5.js → prerender-SK2AXNJY.js} +4 -4
- package/dist/{pubmed-CCXZZT34.js → pubmed-CB7OJNBH.js} +4 -4
- package/dist/{railway-ZUUY5YYR.js → railway-BMMNQ6XH.js} +4 -4
- package/dist/{readwise-PYSRGJ4J.js → readwise-YQY7O5QR.js} +4 -4
- package/dist/{render-SFA3EMGM.js → render-7CRNEEJ3.js} +4 -4
- package/dist/{replicate-KGZHYBPK.js → replicate-V6AX7WFC.js} +4 -4
- package/dist/{resend-HENMZC5U.js → resend-ZBH56UAQ.js} +4 -4
- package/dist/{rootly-HV2PXIDP.js → rootly-C4FIXIF7.js} +4 -4
- package/dist/{scrapfly-FOZFHUN3.js → scrapfly-UE434KD7.js} +4 -4
- package/dist/{scrapingbee-VHUMSO2J.js → scrapingbee-PQCNKALD.js} +4 -4
- package/dist/{screenshot_fyi-FFDXAKHD.js → screenshot_fyi-U6QGI6ZH.js} +4 -4
- package/dist/{semantic_scholar-GHO37BZF.js → semantic_scholar-PSMUOG2W.js} +4 -4
- package/dist/{sentry-MZVZW3YE.js → sentry-F5CNVQA2.js} +4 -4
- package/dist/{shodan-IDICOPGU.js → shodan-NOWG7NV5.js} +4 -4
- package/dist/{shopify-WQLO23JO.js → shopify-GGPLJIGC.js} +4 -4
- package/dist/{stripe-XOAKLAKU.js → stripe-72K2UMK3.js} +4 -4
- package/dist/{supabase-6DKGSLST.js → supabase-H7NRCSAO.js} +4 -4
- package/dist/{tavily-76QZGEHM.js → tavily-ISP24QUX.js} +4 -4
- package/dist/{telegram-O536CLFR.js → telegram-POII6NKE.js} +4 -4
- package/dist/{telnyx-XOPYXF3K.js → telnyx-3D7TOVNA.js} +4 -4
- package/dist/{todoist-YXHVWPP3.js → todoist-LKYPKRKB.js} +4 -4
- package/dist/{trello-WMTM3WA2.js → trello-WW74UYIQ.js} +4 -4
- package/dist/{turso-CG7PR5HM.js → turso-SUFRHBHW.js} +4 -4
- package/dist/{twilio-ZW4SD36D.js → twilio-QB7PBL2B.js} +4 -4
- package/dist/{umami-IJQ5VH77.js → umami-CAHHP3SV.js} +4 -4
- package/dist/{unsplash-JBQY7NKO.js → unsplash-ZCGEPHGQ.js} +4 -4
- package/dist/{upstash_redis-2BAO776T.js → upstash_redis-DZ4AKZWT.js} +4 -4
- package/dist/{uptimerobot-BRIHMPB3.js → uptimerobot-KVEQJGTZ.js} +4 -4
- package/dist/{v2ex-HWIHF4HX.js → v2ex-BCYYBPH7.js} +4 -4
- package/dist/{vercel-MGMRY2DJ.js → vercel-GH4G7EUS.js} +4 -4
- package/dist/{wolfram_alpha_api-JG6JD33L.js → wolfram_alpha_api-XFG2MQBL.js} +4 -4
- package/dist/{wordpress-ZSYJERWV.js → wordpress-DZ2TVNAA.js} +4 -4
- package/dist/{workos-QFEOSJAN.js → workos-EHUAGTGN.js} +4 -4
- package/dist/{zhihu-5RF7BBFV.js → zhihu-OJJW3ZIH.js} +4 -4
- package/package.json +5 -3
|
@@ -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,6 +112,8 @@ import {
|
|
|
106
112
|
parseNodeInput,
|
|
107
113
|
parseTokenResponse,
|
|
108
114
|
parseVia,
|
|
115
|
+
pgBulkInsertSql,
|
|
116
|
+
pgSearchDialect,
|
|
109
117
|
pluginTokenSecretName,
|
|
110
118
|
preprocess,
|
|
111
119
|
record,
|
|
@@ -132,7 +140,7 @@ import {
|
|
|
132
140
|
url,
|
|
133
141
|
validatePath,
|
|
134
142
|
virtualizeTools
|
|
135
|
-
} from "./chunk-
|
|
143
|
+
} from "./chunk-TPGTA43F.js";
|
|
136
144
|
|
|
137
145
|
// src/config.ts
|
|
138
146
|
var DEFAULT_PORT = 8787;
|
|
@@ -151,7 +159,9 @@ function configFromEnv(env = process.env) {
|
|
|
151
159
|
const allowInsecure = env.TB_ALLOW_INSECURE_HTTP === "true";
|
|
152
160
|
const allowlist = (env.TB_REMOTE_ALLOWLIST ?? "").split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
153
161
|
const config = {
|
|
154
|
-
|
|
162
|
+
// TB_PORT 优先,PORT 兜底:Railway / Fly / Cloud Run / CF Container 等 PaaS 只注入
|
|
163
|
+
// PORT,不认识 TB_PORT —— 不兜底的话容器会监听 8787 而平台探活另一个端口,部署直接失败。
|
|
164
|
+
port: portEnv(env.TB_PORT) ?? portEnv(env.PORT) ?? DEFAULT_PORT,
|
|
155
165
|
host: env.TB_HOST !== void 0 && env.TB_HOST.length > 0 ? env.TB_HOST : "0.0.0.0",
|
|
156
166
|
dataDir: env.TB_DATA_DIR !== void 0 && env.TB_DATA_DIR.length > 0 ? env.TB_DATA_DIR : "./data",
|
|
157
167
|
allowInsecureHttp: allowInsecure,
|
|
@@ -162,9 +172,45 @@ function configFromEnv(env = process.env) {
|
|
|
162
172
|
...env.TB_INSTANCE_ID !== void 0 && env.TB_INSTANCE_ID.length > 0 ? { instanceId: env.TB_INSTANCE_ID } : {},
|
|
163
173
|
allowInsecure
|
|
164
174
|
},
|
|
165
|
-
deviceReclaimSec: positiveIntEnv(env.TB_DEVICE_RECLAIM_SEC) ?? DEFAULT_DEVICE_RECLAIM_SEC
|
|
175
|
+
deviceReclaimSec: positiveIntEnv(env.TB_DEVICE_RECLAIM_SEC) ?? DEFAULT_DEVICE_RECLAIM_SEC,
|
|
176
|
+
// 0 合法(立即关停,本地/单机默认);positiveIntEnv 拒 0,故单独解析。
|
|
177
|
+
shutdownDrainSec: (() => {
|
|
178
|
+
const n = Number(env.TB_SHUTDOWN_DRAIN_SEC);
|
|
179
|
+
return Number.isInteger(n) && n >= 0 ? n : 0;
|
|
180
|
+
})()
|
|
166
181
|
};
|
|
167
182
|
if (env.TB_UI_DIR !== void 0 && env.TB_UI_DIR.length > 0) config.uiDir = env.TB_UI_DIR;
|
|
183
|
+
if (env.TB_DATABASE_URL !== void 0 && env.TB_DATABASE_URL.length > 0) {
|
|
184
|
+
config.databaseUrl = env.TB_DATABASE_URL;
|
|
185
|
+
}
|
|
186
|
+
if (env.TB_REDIS_URL !== void 0 && env.TB_REDIS_URL.length > 0) {
|
|
187
|
+
config.redisUrl = env.TB_REDIS_URL;
|
|
188
|
+
}
|
|
189
|
+
if (env.TB_REPLICA_ID !== void 0 && env.TB_REPLICA_ID.length > 0) {
|
|
190
|
+
config.replicaId = env.TB_REPLICA_ID;
|
|
191
|
+
}
|
|
192
|
+
const s3Vars = [
|
|
193
|
+
["TB_OBJECT_STORE_ENDPOINT", env.TB_OBJECT_STORE_ENDPOINT],
|
|
194
|
+
["TB_OBJECT_STORE_BUCKET", env.TB_OBJECT_STORE_BUCKET],
|
|
195
|
+
["TB_OBJECT_STORE_ACCESS_KEY_ID", env.TB_OBJECT_STORE_ACCESS_KEY_ID],
|
|
196
|
+
["TB_OBJECT_STORE_SECRET_ACCESS_KEY", env.TB_OBJECT_STORE_SECRET_ACCESS_KEY]
|
|
197
|
+
];
|
|
198
|
+
const present = s3Vars.filter(([, value]) => value !== void 0 && value.length > 0);
|
|
199
|
+
if (present.length > 0) {
|
|
200
|
+
if (present.length < s3Vars.length) {
|
|
201
|
+
const missing = s3Vars.filter(([, value]) => value === void 0 || value.length === 0).map(([name]) => name);
|
|
202
|
+
throw new Error(
|
|
203
|
+
`\u5E73\u53F0\u5BF9\u8C61\u5B58\u50A8\u914D\u7F6E\u4E0D\u5B8C\u6574(\u7ED9\u4E86 ${present.length}/${s3Vars.length} \u9879),\u7F3A\u5C11:${missing.join(", ")}`
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
config.objectStore = {
|
|
207
|
+
accessKeyId: env.TB_OBJECT_STORE_ACCESS_KEY_ID,
|
|
208
|
+
bucket: env.TB_OBJECT_STORE_BUCKET,
|
|
209
|
+
endpoint: env.TB_OBJECT_STORE_ENDPOINT,
|
|
210
|
+
secretAccessKey: env.TB_OBJECT_STORE_SECRET_ACCESS_KEY,
|
|
211
|
+
...env.TB_OBJECT_STORE_REGION !== void 0 && env.TB_OBJECT_STORE_REGION.length > 0 ? { region: env.TB_OBJECT_STORE_REGION } : {}
|
|
212
|
+
};
|
|
213
|
+
}
|
|
168
214
|
if (env.TB_BOOTSTRAP_ADMIN_SK !== void 0 && env.TB_BOOTSTRAP_ADMIN_SK.length > 0) {
|
|
169
215
|
config.adminSk = env.TB_BOOTSTRAP_ADMIN_SK;
|
|
170
216
|
}
|
|
@@ -396,8 +442,15 @@ var BUILTIN_DESCRIPTIONS = {
|
|
|
396
442
|
async function mintAdminWithPlaintext(store, plaintext, now) {
|
|
397
443
|
const { key } = await mintKey(adminBootstrapInput(), now);
|
|
398
444
|
const adminKey = { ...key, hash: await sha256Hex(plaintext) };
|
|
399
|
-
|
|
400
|
-
|
|
445
|
+
const hashKey = KEY_SK_HASH + adminKey.hash;
|
|
446
|
+
let won;
|
|
447
|
+
if (store.putIfAbsent !== void 0) {
|
|
448
|
+
won = await store.putIfAbsent(hashKey, adminKey);
|
|
449
|
+
} else {
|
|
450
|
+
won = await store.get(hashKey) === null;
|
|
451
|
+
if (won) await store.put(hashKey, adminKey);
|
|
452
|
+
}
|
|
453
|
+
if (won) await store.put(KEY_SK_ID + adminKey.id, adminKey.hash);
|
|
401
454
|
}
|
|
402
455
|
async function ensureBuiltinNodes(registry, now) {
|
|
403
456
|
const ensure = async (node) => {
|
|
@@ -14208,8 +14261,8 @@ function applyPostAuth(clientId, clientSecret, params) {
|
|
|
14208
14261
|
function applyPublicAuth(clientId, params) {
|
|
14209
14262
|
params.set("client_id", clientId);
|
|
14210
14263
|
}
|
|
14211
|
-
function isLoopbackHost(
|
|
14212
|
-
return
|
|
14264
|
+
function isLoopbackHost(hostname2) {
|
|
14265
|
+
return hostname2 === "localhost" || hostname2 === "127.0.0.1" || hostname2 === "[::1]" || hostname2 === "::1";
|
|
14213
14266
|
}
|
|
14214
14267
|
function assertSecureTokenEndpoint(tokenEndpoint) {
|
|
14215
14268
|
const url2 = new URL(String(tokenEndpoint));
|
|
@@ -36477,6 +36530,17 @@ function registerPublicRoutes(app, env) {
|
|
|
36477
36530
|
...catalog !== void 0 && digest !== void 0 ? { catalog: { count: Object.keys(catalog).length, digest } } : {}
|
|
36478
36531
|
});
|
|
36479
36532
|
});
|
|
36533
|
+
app.get("/livez", (c) => c.json({ live: true }));
|
|
36534
|
+
app.get("/readyz", async (c) => {
|
|
36535
|
+
const probe = deps.readiness;
|
|
36536
|
+
if (probe === void 0) return c.json({ checks: {}, ready: true });
|
|
36537
|
+
try {
|
|
36538
|
+
const report = await probe();
|
|
36539
|
+
return c.json(report, report.ready ? 200 : 503);
|
|
36540
|
+
} catch {
|
|
36541
|
+
return c.json({ checks: {}, ready: false }, 503);
|
|
36542
|
+
}
|
|
36543
|
+
});
|
|
36480
36544
|
app.get(
|
|
36481
36545
|
"/~ref/:token",
|
|
36482
36546
|
(c) => runHandler(async () => {
|
|
@@ -37711,14 +37775,26 @@ var DeviceHub = class {
|
|
|
37711
37775
|
connections = /* @__PURE__ */ new Set();
|
|
37712
37776
|
reclaimTimers = /* @__PURE__ */ new Map();
|
|
37713
37777
|
heartbeat;
|
|
37778
|
+
/** 多副本路由;缺省(单副本)时所有跨副本逻辑短路,行为与之前完全一致。 */
|
|
37779
|
+
router;
|
|
37714
37780
|
constructor(opts) {
|
|
37715
37781
|
this.store = opts.store;
|
|
37716
37782
|
this.search = opts.search;
|
|
37717
37783
|
this.reclaimSec = opts.reclaimSec;
|
|
37784
|
+
this.router = opts.router?.(
|
|
37785
|
+
async (deviceId, req) => await this.invokeLocal(deviceId, req)
|
|
37786
|
+
);
|
|
37718
37787
|
const heartbeatMs = opts.heartbeatMs ?? DEFAULT_HEARTBEAT_MS;
|
|
37719
37788
|
this.heartbeat = setInterval(() => this.pingConnections(), heartbeatMs);
|
|
37720
37789
|
this.heartbeat.unref?.();
|
|
37721
37790
|
}
|
|
37791
|
+
/**
|
|
37792
|
+
* 启动多副本路由(订阅本副本频道 + 路由续期)。单副本时是 no-op。
|
|
37793
|
+
* 必须在开始服务前完成:订阅没建好时转发进来的调用会丢。
|
|
37794
|
+
*/
|
|
37795
|
+
async startRouter() {
|
|
37796
|
+
await this.router?.start();
|
|
37797
|
+
}
|
|
37722
37798
|
/** 挂到 http.Server 的 'upgrade' 事件(仅处理 DEVICE_WS_PATH,其余 404)。 */
|
|
37723
37799
|
attach(server) {
|
|
37724
37800
|
server.on("upgrade", (req, socket, head) => {
|
|
@@ -37730,8 +37806,26 @@ var DeviceHub = class {
|
|
|
37730
37806
|
});
|
|
37731
37807
|
});
|
|
37732
37808
|
}
|
|
37733
|
-
/**
|
|
37809
|
+
/**
|
|
37810
|
+
* DeviceChannel.invoke:HTTP→WS 调用转发。
|
|
37811
|
+
*
|
|
37812
|
+
* 本副本持有该设备 → 直接走本地 socket(单副本部署恒走这条,零额外开销)。
|
|
37813
|
+
* 本地没有但配了 router → 问路由表并转发给持有者副本(socket 不可序列化,
|
|
37814
|
+
* 只能把调用送到 socket 所在进程)。都没有 → deviceOffline。
|
|
37815
|
+
*/
|
|
37734
37816
|
async invoke(deviceId, req) {
|
|
37817
|
+
if (this.activeByDevice.has(deviceId)) return await this.invokeLocal(deviceId, req);
|
|
37818
|
+
const offline = { ok: false, error: TBError.deviceOffline().toJSON() };
|
|
37819
|
+
if (this.router === void 0) return offline;
|
|
37820
|
+
return await this.router.forward(deviceId, req) ?? offline;
|
|
37821
|
+
}
|
|
37822
|
+
/**
|
|
37823
|
+
* 只走本副本 socket 的调用执行。
|
|
37824
|
+
*
|
|
37825
|
+
* router 收到转发来的调用时回调这里 —— **不能**回调 `invoke`:本地连接刚断时
|
|
37826
|
+
* 那会再次查路由并转发,在两个副本间弹来弹去。这里没有连接就直接 offline。
|
|
37827
|
+
*/
|
|
37828
|
+
async invokeLocal(deviceId, req) {
|
|
37735
37829
|
const offline = { ok: false, error: TBError.deviceOffline().toJSON() };
|
|
37736
37830
|
const conn = this.activeByDevice.get(deviceId);
|
|
37737
37831
|
if (conn === void 0) return offline;
|
|
@@ -37769,6 +37863,9 @@ var DeviceHub = class {
|
|
|
37769
37863
|
if (!isDeviceMeta(item.value)) continue;
|
|
37770
37864
|
const meta = item.value;
|
|
37771
37865
|
if (this.activeByDevice.has(meta.deviceId)) continue;
|
|
37866
|
+
if (this.router !== void 0 && await this.router.isOnlineAnywhere(meta.deviceId)) {
|
|
37867
|
+
continue;
|
|
37868
|
+
}
|
|
37772
37869
|
if (meta.disconnectedAt === void 0) {
|
|
37773
37870
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
37774
37871
|
try {
|
|
@@ -37794,6 +37891,10 @@ var DeviceHub = class {
|
|
|
37794
37891
|
for (const conn of this.connections) conn.ws.terminate();
|
|
37795
37892
|
this.connections.clear();
|
|
37796
37893
|
this.activeByDevice.clear();
|
|
37894
|
+
const router = this.router;
|
|
37895
|
+
this.router = void 0;
|
|
37896
|
+
await router?.close().catch(() => {
|
|
37897
|
+
});
|
|
37797
37898
|
await new Promise((resolve3) => this.wss.close(() => resolve3()));
|
|
37798
37899
|
}
|
|
37799
37900
|
async handleUpgrade(req, socket, head) {
|
|
@@ -37880,6 +37981,8 @@ var DeviceHub = class {
|
|
|
37880
37981
|
const prev = this.activeByDevice.get(conn.deviceId);
|
|
37881
37982
|
if (prev !== void 0 && prev !== conn) prev.ws.close(1e3, "replaced");
|
|
37882
37983
|
this.activeByDevice.set(conn.deviceId, conn);
|
|
37984
|
+
await this.router?.claim(conn.deviceId).catch(() => {
|
|
37985
|
+
});
|
|
37883
37986
|
conn.session.accept(mountPath);
|
|
37884
37987
|
}
|
|
37885
37988
|
/** 连接失效收尾(对位 DO markDisconnected):registry 下线 + meta 记断线 + 排回收。 */
|
|
@@ -37888,6 +37991,8 @@ var DeviceHub = class {
|
|
|
37888
37991
|
this.connections.delete(conn);
|
|
37889
37992
|
if (this.activeByDevice.get(conn.deviceId) !== conn) return;
|
|
37890
37993
|
this.activeByDevice.delete(conn.deviceId);
|
|
37994
|
+
await this.router?.release(conn.deviceId).catch(() => {
|
|
37995
|
+
});
|
|
37891
37996
|
const raw = await this.store.get(KEY_DEVICE_META + conn.deviceId);
|
|
37892
37997
|
if (!isDeviceMeta(raw)) return;
|
|
37893
37998
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -37918,6 +38023,13 @@ var DeviceHub = class {
|
|
|
37918
38023
|
async reclaim(deviceId) {
|
|
37919
38024
|
this.reclaimTimers.delete(deviceId);
|
|
37920
38025
|
if (this.activeByDevice.has(deviceId)) return;
|
|
38026
|
+
if (this.router !== void 0) {
|
|
38027
|
+
try {
|
|
38028
|
+
if (await this.router.isOnlineAnywhere(deviceId)) return;
|
|
38029
|
+
} catch {
|
|
38030
|
+
return;
|
|
38031
|
+
}
|
|
38032
|
+
}
|
|
37921
38033
|
const raw = await this.store.get(KEY_DEVICE_META + deviceId);
|
|
37922
38034
|
if (!isDeviceMeta(raw)) return;
|
|
37923
38035
|
const searchSync = this.search === void 0 ? void 0 : new SearchSynchronizer(this.store, this.search);
|
|
@@ -37942,6 +38054,146 @@ var DeviceHub = class {
|
|
|
37942
38054
|
}
|
|
37943
38055
|
};
|
|
37944
38056
|
|
|
38057
|
+
// src/deviceRouter.ts
|
|
38058
|
+
var DEVICE_ROUTE_TTL_SEC = 30;
|
|
38059
|
+
var DEVICE_FORWARD_TIMEOUT_MS = 65e3;
|
|
38060
|
+
var deviceRouteKey = (deviceId) => `tb:device:route:${deviceId}`;
|
|
38061
|
+
var deviceCallChannel = (replicaId) => `tb:device:call:${replicaId}`;
|
|
38062
|
+
var deviceReplyChannel = (replicaId) => `tb:device:reply:${replicaId}`;
|
|
38063
|
+
var DeviceRouter = class _DeviceRouter {
|
|
38064
|
+
constructor(replicaId, backend, opts) {
|
|
38065
|
+
this.replicaId = replicaId;
|
|
38066
|
+
this.backend = backend;
|
|
38067
|
+
this.opts = opts;
|
|
38068
|
+
}
|
|
38069
|
+
replicaId;
|
|
38070
|
+
backend;
|
|
38071
|
+
opts;
|
|
38072
|
+
pending = /* @__PURE__ */ new Map();
|
|
38073
|
+
renewTimer;
|
|
38074
|
+
/** 本副本当前持有的设备(需周期续期路由条目)。 */
|
|
38075
|
+
owned = /* @__PURE__ */ new Set();
|
|
38076
|
+
get ttlSec() {
|
|
38077
|
+
return this.opts.ttlSec ?? DEVICE_ROUTE_TTL_SEC;
|
|
38078
|
+
}
|
|
38079
|
+
/** 订阅本副本的两个频道并启动路由续期。 */
|
|
38080
|
+
async start() {
|
|
38081
|
+
await this.backend.subscribe(deviceCallChannel(this.replicaId), (payload) => {
|
|
38082
|
+
void this.handleForwardedCall(payload);
|
|
38083
|
+
});
|
|
38084
|
+
await this.backend.subscribe(deviceReplyChannel(this.replicaId), (payload) => {
|
|
38085
|
+
this.handleReply(payload);
|
|
38086
|
+
});
|
|
38087
|
+
const renewMs = Math.max(1e3, Math.floor(this.ttlSec * 1e3 / 3));
|
|
38088
|
+
this.renewTimer = setInterval(() => {
|
|
38089
|
+
void this.renewOwned();
|
|
38090
|
+
}, renewMs);
|
|
38091
|
+
this.renewTimer.unref?.();
|
|
38092
|
+
}
|
|
38093
|
+
/** 本副本接受了某设备的连接:声明所有权。 */
|
|
38094
|
+
async claim(deviceId) {
|
|
38095
|
+
this.owned.add(deviceId);
|
|
38096
|
+
await this.backend.setRoute(deviceId, this.replicaId, this.ttlSec);
|
|
38097
|
+
}
|
|
38098
|
+
/** 本副本失去某设备的连接:释放所有权。 */
|
|
38099
|
+
async release(deviceId) {
|
|
38100
|
+
this.owned.delete(deviceId);
|
|
38101
|
+
await this.backend.clearRoute(deviceId, this.replicaId);
|
|
38102
|
+
}
|
|
38103
|
+
/** 某设备当前是否在**任一**副本上在线(reclaim 判定用,不能只看本副本)。 */
|
|
38104
|
+
async isOnlineAnywhere(deviceId) {
|
|
38105
|
+
return await this.backend.lookupRoute(deviceId) !== null;
|
|
38106
|
+
}
|
|
38107
|
+
/**
|
|
38108
|
+
* 把调用转发给持有该设备的副本并等结果。
|
|
38109
|
+
* 无人持有 → null(调用方按 deviceOffline 处理)。
|
|
38110
|
+
*/
|
|
38111
|
+
async forward(deviceId, req) {
|
|
38112
|
+
const owner = await this.backend.lookupRoute(deviceId);
|
|
38113
|
+
if (owner === null) return null;
|
|
38114
|
+
if (owner === this.replicaId) return null;
|
|
38115
|
+
const correlationId = `${this.replicaId}:${deviceId}:${++_DeviceRouter.seq}`;
|
|
38116
|
+
const call = {
|
|
38117
|
+
correlationId,
|
|
38118
|
+
deviceId,
|
|
38119
|
+
replyTo: this.replicaId,
|
|
38120
|
+
req
|
|
38121
|
+
};
|
|
38122
|
+
return await new Promise((resolve3) => {
|
|
38123
|
+
const timer = setTimeout(() => {
|
|
38124
|
+
this.pending.delete(correlationId);
|
|
38125
|
+
resolve3({ ok: false, error: new TBError("unavailable", "\u8BBE\u5907\u8C03\u7528\u8F6C\u53D1\u8D85\u65F6").toJSON() });
|
|
38126
|
+
}, this.opts.forwardTimeoutMs ?? DEVICE_FORWARD_TIMEOUT_MS);
|
|
38127
|
+
timer.unref?.();
|
|
38128
|
+
this.pending.set(correlationId, { resolve: resolve3, timer });
|
|
38129
|
+
void this.backend.publish(deviceCallChannel(owner), JSON.stringify(call)).catch(() => {
|
|
38130
|
+
const waiter = this.pending.get(correlationId);
|
|
38131
|
+
if (waiter === void 0) return;
|
|
38132
|
+
this.pending.delete(correlationId);
|
|
38133
|
+
clearTimeout(waiter.timer);
|
|
38134
|
+
waiter.resolve({
|
|
38135
|
+
ok: false,
|
|
38136
|
+
error: new TBError("unavailable", "\u8BBE\u5907\u8C03\u7528\u8F6C\u53D1\u5931\u8D25").toJSON()
|
|
38137
|
+
});
|
|
38138
|
+
});
|
|
38139
|
+
});
|
|
38140
|
+
}
|
|
38141
|
+
async close() {
|
|
38142
|
+
if (this.renewTimer !== void 0) clearInterval(this.renewTimer);
|
|
38143
|
+
this.renewTimer = void 0;
|
|
38144
|
+
for (const [, waiter] of this.pending) {
|
|
38145
|
+
clearTimeout(waiter.timer);
|
|
38146
|
+
waiter.resolve({ ok: false, error: new TBError("unavailable", "\u7F51\u5173\u6B63\u5728\u5173\u95ED").toJSON() });
|
|
38147
|
+
}
|
|
38148
|
+
this.pending.clear();
|
|
38149
|
+
await Promise.all([...this.owned].map(async (id) => await this.release(id)));
|
|
38150
|
+
await this.backend.close();
|
|
38151
|
+
}
|
|
38152
|
+
/** correlationId 的进程内序号(与 replicaId 组合后全局唯一)。 */
|
|
38153
|
+
static seq = 0;
|
|
38154
|
+
async renewOwned() {
|
|
38155
|
+
for (const deviceId of this.owned) {
|
|
38156
|
+
try {
|
|
38157
|
+
await this.backend.setRoute(deviceId, this.replicaId, this.ttlSec);
|
|
38158
|
+
} catch {
|
|
38159
|
+
}
|
|
38160
|
+
}
|
|
38161
|
+
}
|
|
38162
|
+
/** 作为持有者:执行本地调用并回执。 */
|
|
38163
|
+
async handleForwardedCall(payload) {
|
|
38164
|
+
let call;
|
|
38165
|
+
try {
|
|
38166
|
+
call = JSON.parse(payload);
|
|
38167
|
+
} catch {
|
|
38168
|
+
return;
|
|
38169
|
+
}
|
|
38170
|
+
if (typeof call?.correlationId !== "string" || typeof call?.replyTo !== "string") return;
|
|
38171
|
+
let result;
|
|
38172
|
+
try {
|
|
38173
|
+
result = await this.opts.onLocalCall(call.deviceId, call.req);
|
|
38174
|
+
} catch {
|
|
38175
|
+
result = { ok: false, error: new TBError("internal", "\u8BBE\u5907\u8C03\u7528\u6267\u884C\u5931\u8D25").toJSON() };
|
|
38176
|
+
}
|
|
38177
|
+
const reply = { correlationId: call.correlationId, result };
|
|
38178
|
+
await this.backend.publish(deviceReplyChannel(call.replyTo), JSON.stringify(reply)).catch(() => {
|
|
38179
|
+
});
|
|
38180
|
+
}
|
|
38181
|
+
/** 作为发起方:收到回执,唤醒等待者。 */
|
|
38182
|
+
handleReply(payload) {
|
|
38183
|
+
let reply;
|
|
38184
|
+
try {
|
|
38185
|
+
reply = JSON.parse(payload);
|
|
38186
|
+
} catch {
|
|
38187
|
+
return;
|
|
38188
|
+
}
|
|
38189
|
+
const waiter = this.pending.get(reply?.correlationId);
|
|
38190
|
+
if (waiter === void 0) return;
|
|
38191
|
+
this.pending.delete(reply.correlationId);
|
|
38192
|
+
clearTimeout(waiter.timer);
|
|
38193
|
+
waiter.resolve(reply.result);
|
|
38194
|
+
}
|
|
38195
|
+
};
|
|
38196
|
+
|
|
37945
38197
|
// ../core/src/node/fsObjectStore.ts
|
|
37946
38198
|
import { mkdir, readdir, readFile, realpath, rm, stat, writeFile } from "fs/promises";
|
|
37947
38199
|
import { basename, dirname, join, resolve, sep } from "path";
|
|
@@ -38226,6 +38478,219 @@ function createDataObjectStore(dataDir) {
|
|
|
38226
38478
|
};
|
|
38227
38479
|
}
|
|
38228
38480
|
|
|
38481
|
+
// src/pgSearchIndex.ts
|
|
38482
|
+
var PgSearchDriver = class {
|
|
38483
|
+
constructor(sql) {
|
|
38484
|
+
this.sql = sql;
|
|
38485
|
+
}
|
|
38486
|
+
sql;
|
|
38487
|
+
schemaReady;
|
|
38488
|
+
async all(statement) {
|
|
38489
|
+
return await this.sql.unsafe(statement.sql, statement.params);
|
|
38490
|
+
}
|
|
38491
|
+
async initializeSchema() {
|
|
38492
|
+
try {
|
|
38493
|
+
await this.sql.begin(async (tx) => {
|
|
38494
|
+
for (const ddl of pgSearchDialect.schemaStatements) {
|
|
38495
|
+
await tx.unsafe(ddl);
|
|
38496
|
+
}
|
|
38497
|
+
});
|
|
38498
|
+
} catch (error) {
|
|
38499
|
+
this.schemaReady = void 0;
|
|
38500
|
+
throw error;
|
|
38501
|
+
}
|
|
38502
|
+
}
|
|
38503
|
+
ensureSchema() {
|
|
38504
|
+
this.schemaReady ??= this.initializeSchema();
|
|
38505
|
+
return this.schemaReady;
|
|
38506
|
+
}
|
|
38507
|
+
async first(statement) {
|
|
38508
|
+
const rows = await this.sql.unsafe(statement.sql, statement.params);
|
|
38509
|
+
return rows[0] ?? null;
|
|
38510
|
+
}
|
|
38511
|
+
/**
|
|
38512
|
+
* 攒成多行 VALUES 批量插入,而非逐条。
|
|
38513
|
+
* 逐条对 4000 行需 ~920ms(每行一次事务内往返),批量后 ~12ms。
|
|
38514
|
+
*/
|
|
38515
|
+
bulk(items, table, columns, toParams) {
|
|
38516
|
+
const statements = [];
|
|
38517
|
+
for (let i = 0; i < items.length; i += PG_SEARCH_INSERT_ROWS_MAX) {
|
|
38518
|
+
const chunk = items.slice(i, i + PG_SEARCH_INSERT_ROWS_MAX);
|
|
38519
|
+
statements.push({
|
|
38520
|
+
params: chunk.flatMap(toParams),
|
|
38521
|
+
sql: pgBulkInsertSql(table, columns, chunk.length)
|
|
38522
|
+
});
|
|
38523
|
+
}
|
|
38524
|
+
return statements;
|
|
38525
|
+
}
|
|
38526
|
+
insertRecords(records) {
|
|
38527
|
+
return this.bulk(
|
|
38528
|
+
records,
|
|
38529
|
+
PG_SEARCH_TOOLS_TABLE,
|
|
38530
|
+
PG_SEARCH_TOOLS_COLUMNS,
|
|
38531
|
+
(record2) => [record2.path, record2.name, record2.description, record2.feedback]
|
|
38532
|
+
);
|
|
38533
|
+
}
|
|
38534
|
+
insertSnapshots(digests) {
|
|
38535
|
+
return this.bulk(
|
|
38536
|
+
[...digests],
|
|
38537
|
+
PG_SEARCH_SNAPSHOTS_TABLE,
|
|
38538
|
+
PG_SEARCH_SNAPSHOTS_COLUMNS,
|
|
38539
|
+
([path, digest]) => [path, digest]
|
|
38540
|
+
);
|
|
38541
|
+
}
|
|
38542
|
+
async write(statements) {
|
|
38543
|
+
await this.sql.begin(async (tx) => {
|
|
38544
|
+
await tx`SELECT pg_advisory_xact_lock(${PG_SEARCH_WRITE_LOCK_KEY})`;
|
|
38545
|
+
for (const statement of statements) {
|
|
38546
|
+
await tx.unsafe(statement.sql, statement.params);
|
|
38547
|
+
}
|
|
38548
|
+
});
|
|
38549
|
+
}
|
|
38550
|
+
};
|
|
38551
|
+
var PgSearchIndex = class extends SqlSearchIndex {
|
|
38552
|
+
constructor(sql) {
|
|
38553
|
+
super(new PgSearchDriver(sql), pgSearchDialect);
|
|
38554
|
+
}
|
|
38555
|
+
};
|
|
38556
|
+
|
|
38557
|
+
// src/pgStateStore.ts
|
|
38558
|
+
var DEFAULT_LIST_LIMIT = 1e3;
|
|
38559
|
+
var MAX_CODE_POINT = 1114111;
|
|
38560
|
+
var SURROGATE_START = 55296;
|
|
38561
|
+
var SURROGATE_END = 57343;
|
|
38562
|
+
function prefixUpperBound(prefix) {
|
|
38563
|
+
const points = [...prefix];
|
|
38564
|
+
for (let i = points.length - 1; i >= 0; i--) {
|
|
38565
|
+
const code = points[i]?.codePointAt(0);
|
|
38566
|
+
if (code === void 0 || code >= MAX_CODE_POINT) continue;
|
|
38567
|
+
let next = code + 1;
|
|
38568
|
+
if (next >= SURROGATE_START && next <= SURROGATE_END) next = SURROGATE_END + 1;
|
|
38569
|
+
return points.slice(0, i).join("") + String.fromCodePoint(next);
|
|
38570
|
+
}
|
|
38571
|
+
return void 0;
|
|
38572
|
+
}
|
|
38573
|
+
var PgStateStore = class {
|
|
38574
|
+
constructor(sql) {
|
|
38575
|
+
this.sql = sql;
|
|
38576
|
+
}
|
|
38577
|
+
sql;
|
|
38578
|
+
/** 建表(幂等)。key COLLATE "C" 是字节序正确性的前提,不能省。 */
|
|
38579
|
+
async ensureSchema() {
|
|
38580
|
+
await this.sql`
|
|
38581
|
+
CREATE TABLE IF NOT EXISTS tb_kv (
|
|
38582
|
+
key text COLLATE "C" PRIMARY KEY,
|
|
38583
|
+
value jsonb NOT NULL
|
|
38584
|
+
)
|
|
38585
|
+
`;
|
|
38586
|
+
}
|
|
38587
|
+
async get(key) {
|
|
38588
|
+
const rows = await this.sql`
|
|
38589
|
+
SELECT value FROM tb_kv WHERE key = ${key}
|
|
38590
|
+
`;
|
|
38591
|
+
const row = rows[0];
|
|
38592
|
+
return row === void 0 ? null : row.value;
|
|
38593
|
+
}
|
|
38594
|
+
async getMany(keys) {
|
|
38595
|
+
const unique = [...new Set(keys)];
|
|
38596
|
+
if (unique.length === 0) return /* @__PURE__ */ new Map();
|
|
38597
|
+
const rows = await this.sql`
|
|
38598
|
+
SELECT key, value FROM tb_kv WHERE key = ANY(${this.sql.array(unique)})
|
|
38599
|
+
`;
|
|
38600
|
+
const byKey = new Map(rows.map((row) => [row.key, row.value]));
|
|
38601
|
+
const out = /* @__PURE__ */ new Map();
|
|
38602
|
+
for (const key of unique) {
|
|
38603
|
+
if (byKey.has(key)) out.set(key, byKey.get(key));
|
|
38604
|
+
}
|
|
38605
|
+
return out;
|
|
38606
|
+
}
|
|
38607
|
+
async put(key, value) {
|
|
38608
|
+
await this.sql`
|
|
38609
|
+
INSERT INTO tb_kv (key, value) VALUES (${key}, ${this.sql.json(value)})
|
|
38610
|
+
ON CONFLICT (key) DO UPDATE SET value = excluded.value
|
|
38611
|
+
`;
|
|
38612
|
+
}
|
|
38613
|
+
async putIfAbsent(key, value) {
|
|
38614
|
+
const result = await this.sql`
|
|
38615
|
+
INSERT INTO tb_kv (key, value) VALUES (${key}, ${this.sql.json(value)})
|
|
38616
|
+
ON CONFLICT (key) DO NOTHING
|
|
38617
|
+
`;
|
|
38618
|
+
return result.count > 0;
|
|
38619
|
+
}
|
|
38620
|
+
async delete(key) {
|
|
38621
|
+
await this.sql`DELETE FROM tb_kv WHERE key = ${key}`;
|
|
38622
|
+
}
|
|
38623
|
+
async list(prefix, opts) {
|
|
38624
|
+
const limit = opts?.limit ?? DEFAULT_LIST_LIMIT;
|
|
38625
|
+
const lowerByCursor = opts?.cursor !== void 0 && opts.cursor >= prefix;
|
|
38626
|
+
const upper = prefix === "" ? void 0 : prefixUpperBound(prefix);
|
|
38627
|
+
const rows = await this.sql`
|
|
38628
|
+
SELECT key, value FROM tb_kv
|
|
38629
|
+
WHERE ${lowerByCursor ? this.sql`key > ${opts?.cursor ?? ""}` : this.sql`key >= ${prefix}`}
|
|
38630
|
+
${upper !== void 0 ? this.sql`AND key < ${upper}` : this.sql``}
|
|
38631
|
+
ORDER BY key
|
|
38632
|
+
LIMIT ${limit + 1}
|
|
38633
|
+
`;
|
|
38634
|
+
const matched = rows.filter((r) => r.key.startsWith(prefix));
|
|
38635
|
+
const hasMore = matched.length > limit;
|
|
38636
|
+
const page = hasMore ? matched.slice(0, limit) : matched;
|
|
38637
|
+
const items = page.map((r) => ({ key: r.key, value: r.value }));
|
|
38638
|
+
const last = page[page.length - 1];
|
|
38639
|
+
return hasMore && last !== void 0 ? { items, cursor: last.key } : { items };
|
|
38640
|
+
}
|
|
38641
|
+
};
|
|
38642
|
+
|
|
38643
|
+
// src/redisDeviceRouter.ts
|
|
38644
|
+
import { Redis } from "ioredis";
|
|
38645
|
+
var CLEAR_IF_OWNER = `
|
|
38646
|
+
if redis.call('get', KEYS[1]) == ARGV[1] then
|
|
38647
|
+
return redis.call('del', KEYS[1])
|
|
38648
|
+
end
|
|
38649
|
+
return 0
|
|
38650
|
+
`;
|
|
38651
|
+
var RedisDeviceRouterBackend = class {
|
|
38652
|
+
/** 命令连接(GET/SET/DEL/PUBLISH)。 */
|
|
38653
|
+
commands;
|
|
38654
|
+
/** 订阅连接(进入 subscribe 模式后不能发普通命令)。 */
|
|
38655
|
+
subscriber;
|
|
38656
|
+
handlers = /* @__PURE__ */ new Map();
|
|
38657
|
+
constructor(url2) {
|
|
38658
|
+
this.commands = new Redis(url2, { maxRetriesPerRequest: 2 });
|
|
38659
|
+
this.subscriber = new Redis(url2, { maxRetriesPerRequest: 2 });
|
|
38660
|
+
this.subscriber.on("message", (channel, payload) => {
|
|
38661
|
+
this.handlers.get(channel)?.(payload);
|
|
38662
|
+
});
|
|
38663
|
+
this.commands.on("error", () => {
|
|
38664
|
+
});
|
|
38665
|
+
this.subscriber.on("error", () => {
|
|
38666
|
+
});
|
|
38667
|
+
}
|
|
38668
|
+
async setRoute(deviceId, replicaId, ttlSec) {
|
|
38669
|
+
await this.commands.set(deviceRouteKey(deviceId), replicaId, "EX", ttlSec);
|
|
38670
|
+
}
|
|
38671
|
+
async lookupRoute(deviceId) {
|
|
38672
|
+
return await this.commands.get(deviceRouteKey(deviceId));
|
|
38673
|
+
}
|
|
38674
|
+
async clearRoute(deviceId, replicaId) {
|
|
38675
|
+
await this.commands.eval(CLEAR_IF_OWNER, 1, deviceRouteKey(deviceId), replicaId);
|
|
38676
|
+
}
|
|
38677
|
+
async publish(channel, payload) {
|
|
38678
|
+
await this.commands.publish(channel, payload);
|
|
38679
|
+
}
|
|
38680
|
+
/** 就绪探测(/readyz):命令连接 PING 一次。断连时 ioredis 会抛/挂起,由调用方限时。 */
|
|
38681
|
+
async ping() {
|
|
38682
|
+
await this.commands.ping();
|
|
38683
|
+
}
|
|
38684
|
+
async subscribe(channel, handler) {
|
|
38685
|
+
this.handlers.set(channel, handler);
|
|
38686
|
+
await this.subscriber.subscribe(channel);
|
|
38687
|
+
}
|
|
38688
|
+
async close() {
|
|
38689
|
+
this.handlers.clear();
|
|
38690
|
+
await Promise.allSettled([this.subscriber.quit(), this.commands.quit()]);
|
|
38691
|
+
}
|
|
38692
|
+
};
|
|
38693
|
+
|
|
38229
38694
|
// src/sqliteSearchIndex.ts
|
|
38230
38695
|
import Database from "better-sqlite3";
|
|
38231
38696
|
var SCHEMA_SQL = TOOL_SEARCH_SCHEMA_STATEMENTS.map((sql) => `${sql};`).join("\n");
|
|
@@ -38287,13 +38752,18 @@ var SqliteSearchIndex = class extends SqlSearchIndex {
|
|
|
38287
38752
|
|
|
38288
38753
|
// src/sqliteStateStore.ts
|
|
38289
38754
|
import Database2 from "better-sqlite3";
|
|
38290
|
-
var
|
|
38291
|
-
|
|
38292
|
-
|
|
38293
|
-
|
|
38294
|
-
|
|
38295
|
-
|
|
38296
|
-
|
|
38755
|
+
var DEFAULT_LIST_LIMIT2 = 1e3;
|
|
38756
|
+
var MAX_CODE_POINT2 = 1114111;
|
|
38757
|
+
var SURROGATE_START2 = 55296;
|
|
38758
|
+
var SURROGATE_END2 = 57343;
|
|
38759
|
+
function prefixUpperBound2(prefix) {
|
|
38760
|
+
const points = [...prefix];
|
|
38761
|
+
for (let i = points.length - 1; i >= 0; i--) {
|
|
38762
|
+
const code = points[i]?.codePointAt(0);
|
|
38763
|
+
if (code === void 0 || code >= MAX_CODE_POINT2) continue;
|
|
38764
|
+
let next = code + 1;
|
|
38765
|
+
if (next >= SURROGATE_START2 && next <= SURROGATE_END2) next = SURROGATE_END2 + 1;
|
|
38766
|
+
return points.slice(0, i).join("") + String.fromCodePoint(next);
|
|
38297
38767
|
}
|
|
38298
38768
|
return void 0;
|
|
38299
38769
|
}
|
|
@@ -38336,13 +38806,17 @@ var SqliteStateStore = class {
|
|
|
38336
38806
|
async put(key, value) {
|
|
38337
38807
|
this.stmtPut.run(key, JSON.stringify(value));
|
|
38338
38808
|
}
|
|
38809
|
+
async putIfAbsent(key, value) {
|
|
38810
|
+
const info = this.db.prepare("INSERT OR IGNORE INTO kv (key, value) VALUES (?, ?)").run(key, JSON.stringify(value));
|
|
38811
|
+
return info.changes > 0;
|
|
38812
|
+
}
|
|
38339
38813
|
async delete(key) {
|
|
38340
38814
|
this.stmtDelete.run(key);
|
|
38341
38815
|
}
|
|
38342
38816
|
async list(prefix, opts) {
|
|
38343
|
-
const limit = opts?.limit ??
|
|
38817
|
+
const limit = opts?.limit ?? DEFAULT_LIST_LIMIT2;
|
|
38344
38818
|
const lowerByCursor = opts?.cursor !== void 0 && opts.cursor >= prefix;
|
|
38345
|
-
const upper = prefix === "" ? void 0 :
|
|
38819
|
+
const upper = prefix === "" ? void 0 : prefixUpperBound2(prefix);
|
|
38346
38820
|
const conditions = [];
|
|
38347
38821
|
const params = { n: limit + 1 };
|
|
38348
38822
|
if (lowerByCursor) {
|
|
@@ -38371,14 +38845,16 @@ var SqliteStateStore = class {
|
|
|
38371
38845
|
|
|
38372
38846
|
// src/server.ts
|
|
38373
38847
|
import { serve } from "@hono/node-server";
|
|
38848
|
+
import postgres from "postgres";
|
|
38374
38849
|
import { mkdirSync as mkdirSync2 } from "fs";
|
|
38850
|
+
import { hostname } from "os";
|
|
38375
38851
|
import { join as join4 } from "path";
|
|
38376
38852
|
|
|
38377
38853
|
// package.json
|
|
38378
38854
|
var package_default = {
|
|
38379
38855
|
name: "@tool-bridge/server",
|
|
38380
|
-
version: "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",
|
|
38856
|
+
version: "0.10.0",
|
|
38857
|
+
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
38858
|
type: "module",
|
|
38383
38859
|
license: "MIT",
|
|
38384
38860
|
main: "src/index.ts",
|
|
@@ -38426,6 +38902,8 @@ var package_default = {
|
|
|
38426
38902
|
aws4fetch: "catalog:",
|
|
38427
38903
|
"better-sqlite3": "^12.6.0",
|
|
38428
38904
|
hono: "catalog:",
|
|
38905
|
+
ioredis: "^6.0.0",
|
|
38906
|
+
postgres: "^3.4.9",
|
|
38429
38907
|
ws: "catalog:"
|
|
38430
38908
|
},
|
|
38431
38909
|
devDependencies: {
|
|
@@ -38490,24 +38968,116 @@ function resolveUiAssets(uiDirOverride) {
|
|
|
38490
38968
|
}
|
|
38491
38969
|
|
|
38492
38970
|
// src/server.ts
|
|
38493
|
-
function
|
|
38494
|
-
|
|
38495
|
-
|
|
38496
|
-
const state = new
|
|
38497
|
-
|
|
38971
|
+
function pgBackends(databaseUrl) {
|
|
38972
|
+
const sql = postgres(databaseUrl, { onnotice: () => {
|
|
38973
|
+
} });
|
|
38974
|
+
const state = new PgStateStore(sql);
|
|
38975
|
+
const search = new PgSearchIndex(sql);
|
|
38976
|
+
return {
|
|
38977
|
+
state: {
|
|
38978
|
+
value: state,
|
|
38979
|
+
ensureReady: async () => await state.ensureSchema(),
|
|
38980
|
+
ping: async () => {
|
|
38981
|
+
await sql`SELECT 1`;
|
|
38982
|
+
},
|
|
38983
|
+
close: async () => await sql.end({ timeout: 5 })
|
|
38984
|
+
},
|
|
38985
|
+
search: {
|
|
38986
|
+
value: search,
|
|
38987
|
+
ensureReady: async () => {
|
|
38988
|
+
await search.initialized();
|
|
38989
|
+
},
|
|
38990
|
+
close: async () => {
|
|
38991
|
+
}
|
|
38992
|
+
}
|
|
38993
|
+
};
|
|
38994
|
+
}
|
|
38995
|
+
function sqliteStateBackend(dataDir) {
|
|
38996
|
+
const store = new SqliteStateStore(join4(dataDir, "state.sqlite3"));
|
|
38997
|
+
return {
|
|
38998
|
+
value: store,
|
|
38999
|
+
ensureReady: async () => {
|
|
39000
|
+
},
|
|
39001
|
+
close: async () => store.close()
|
|
39002
|
+
};
|
|
39003
|
+
}
|
|
39004
|
+
function sqliteSearchBackend(dataDir) {
|
|
39005
|
+
const index = new SqliteSearchIndex(join4(dataDir, "state.sqlite3"));
|
|
39006
|
+
return {
|
|
39007
|
+
value: index,
|
|
39008
|
+
ensureReady: async () => {
|
|
39009
|
+
},
|
|
39010
|
+
close: async () => index.close()
|
|
39011
|
+
};
|
|
39012
|
+
}
|
|
39013
|
+
function resolveBackends(config) {
|
|
39014
|
+
if (config.databaseUrl !== void 0) return pgBackends(config.databaseUrl);
|
|
39015
|
+
const state = sqliteStateBackend(config.dataDir);
|
|
38498
39016
|
try {
|
|
38499
|
-
|
|
39017
|
+
return { search: sqliteSearchBackend(config.dataDir), state };
|
|
38500
39018
|
} catch (error) {
|
|
38501
|
-
state.close()
|
|
39019
|
+
state.close().catch(() => {
|
|
39020
|
+
});
|
|
38502
39021
|
throw error;
|
|
38503
39022
|
}
|
|
39023
|
+
}
|
|
39024
|
+
function createTbServer(config) {
|
|
39025
|
+
mkdirSync2(config.dataDir, { recursive: true });
|
|
39026
|
+
const backends = resolveBackends(config);
|
|
39027
|
+
const state = backends.state.value;
|
|
39028
|
+
const search = backends.search.value;
|
|
38504
39029
|
const secrets = new SecretStoreImpl(state, config.encryptionKey);
|
|
38505
|
-
const objects = createDataObjectStore(config.dataDir);
|
|
39030
|
+
const objects = config.objectStore === void 0 ? createDataObjectStore(config.dataDir) : createS3ObjectStore(config.objectStore, { allowInsecure: config.allowInsecureHttp });
|
|
39031
|
+
const redisBackend = config.redisUrl === void 0 ? void 0 : new RedisDeviceRouterBackend(config.redisUrl);
|
|
38506
39032
|
const hub = new DeviceHub({
|
|
38507
39033
|
store: state,
|
|
38508
39034
|
search,
|
|
38509
|
-
reclaimSec: config.deviceReclaimSec
|
|
38510
|
-
|
|
39035
|
+
reclaimSec: config.deviceReclaimSec,
|
|
39036
|
+
// 配了 Redis 才启用多副本路由;缺省单副本,设备调用恒走本地 socket。
|
|
39037
|
+
...redisBackend === void 0 ? {} : {
|
|
39038
|
+
router: (onLocalCall) => new DeviceRouter(
|
|
39039
|
+
config.replicaId ?? hostname(),
|
|
39040
|
+
redisBackend,
|
|
39041
|
+
{ onLocalCall }
|
|
39042
|
+
)
|
|
39043
|
+
}
|
|
39044
|
+
});
|
|
39045
|
+
let draining = false;
|
|
39046
|
+
const probeOne = async (fn) => {
|
|
39047
|
+
let timer;
|
|
39048
|
+
try {
|
|
39049
|
+
await Promise.race([
|
|
39050
|
+
fn(),
|
|
39051
|
+
new Promise((_, reject) => {
|
|
39052
|
+
timer = setTimeout(() => reject(new Error("timeout after 1000ms")), 1e3);
|
|
39053
|
+
timer.unref?.();
|
|
39054
|
+
})
|
|
39055
|
+
]);
|
|
39056
|
+
return { ok: true };
|
|
39057
|
+
} catch (err) {
|
|
39058
|
+
return { ok: false, detail: err instanceof Error ? err.message : "probe failed" };
|
|
39059
|
+
} finally {
|
|
39060
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
39061
|
+
}
|
|
39062
|
+
};
|
|
39063
|
+
const readiness = async () => {
|
|
39064
|
+
const checks = {};
|
|
39065
|
+
const probes = [];
|
|
39066
|
+
const statePing = backends.state.ping;
|
|
39067
|
+
if (statePing !== void 0) {
|
|
39068
|
+
probes.push(probeOne(statePing).then((r) => {
|
|
39069
|
+
checks.state = r;
|
|
39070
|
+
}));
|
|
39071
|
+
}
|
|
39072
|
+
if (redisBackend !== void 0) {
|
|
39073
|
+
probes.push(probeOne(async () => await redisBackend.ping()).then((r) => {
|
|
39074
|
+
checks.redis = r;
|
|
39075
|
+
}));
|
|
39076
|
+
}
|
|
39077
|
+
await Promise.all(probes);
|
|
39078
|
+
if (draining) checks.draining = { ok: false, detail: "shutting down" };
|
|
39079
|
+
return { checks, ready: !draining && Object.values(checks).every((c) => c.ok) };
|
|
39080
|
+
};
|
|
38511
39081
|
const deps = {
|
|
38512
39082
|
state,
|
|
38513
39083
|
secrets,
|
|
@@ -38516,7 +39086,8 @@ function createTbServer(config) {
|
|
|
38516
39086
|
search,
|
|
38517
39087
|
allowInsecureHttp: config.allowInsecureHttp,
|
|
38518
39088
|
objects: () => objects,
|
|
38519
|
-
device: hub
|
|
39089
|
+
device: hub,
|
|
39090
|
+
readiness
|
|
38520
39091
|
};
|
|
38521
39092
|
if (config.encryptionKey !== void 0) deps.encryptionKey = config.encryptionKey;
|
|
38522
39093
|
if (config.pluginBindings !== void 0) deps.pluginBindings = config.pluginBindings;
|
|
@@ -38535,6 +39106,9 @@ function createTbServer(config) {
|
|
|
38535
39106
|
state,
|
|
38536
39107
|
deviceHub: hub,
|
|
38537
39108
|
async start() {
|
|
39109
|
+
await backends.state.ensureReady();
|
|
39110
|
+
await backends.search.ensureReady();
|
|
39111
|
+
await hub.startRouter();
|
|
38538
39112
|
await runBootstrap(state, {
|
|
38539
39113
|
...config.adminSk !== void 0 ? { adminSk: config.adminSk } : {},
|
|
38540
39114
|
requireAdminSk: !config.allowInsecureBootstrap
|
|
@@ -38547,16 +39121,24 @@ function createTbServer(config) {
|
|
|
38547
39121
|
hub.attach(server);
|
|
38548
39122
|
});
|
|
38549
39123
|
},
|
|
39124
|
+
startDraining() {
|
|
39125
|
+
draining = true;
|
|
39126
|
+
},
|
|
38550
39127
|
async close() {
|
|
38551
|
-
|
|
39128
|
+
draining = true;
|
|
39129
|
+
let closed;
|
|
38552
39130
|
if (server !== void 0) {
|
|
38553
|
-
|
|
38554
|
-
|
|
39131
|
+
const s = server;
|
|
39132
|
+
closed = new Promise((resolve3, reject) => {
|
|
39133
|
+
s.close((err) => err ? reject(err) : resolve3());
|
|
38555
39134
|
});
|
|
38556
|
-
|
|
39135
|
+
s.closeIdleConnections?.();
|
|
38557
39136
|
}
|
|
38558
|
-
|
|
38559
|
-
|
|
39137
|
+
await hub.close();
|
|
39138
|
+
if (closed !== void 0) await closed;
|
|
39139
|
+
server = void 0;
|
|
39140
|
+
await backends.search.close();
|
|
39141
|
+
await backends.state.close();
|
|
38560
39142
|
}
|
|
38561
39143
|
};
|
|
38562
39144
|
}
|
|
@@ -38565,7 +39147,13 @@ export {
|
|
|
38565
39147
|
configFromEnv,
|
|
38566
39148
|
DEVICE_WS_PATH,
|
|
38567
39149
|
DeviceHub,
|
|
39150
|
+
DEVICE_ROUTE_TTL_SEC,
|
|
39151
|
+
DEVICE_FORWARD_TIMEOUT_MS,
|
|
39152
|
+
DeviceRouter,
|
|
38568
39153
|
createDataObjectStore,
|
|
39154
|
+
PgSearchIndex,
|
|
39155
|
+
PgStateStore,
|
|
39156
|
+
RedisDeviceRouterBackend,
|
|
38569
39157
|
SqliteSearchIndex,
|
|
38570
39158
|
SqliteStateStore,
|
|
38571
39159
|
createTbServer
|