@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
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import * as http from 'node:http';
3
+ import { Sql } from 'postgres';
3
4
  import { Hono } from 'hono';
4
5
 
5
6
  /**
@@ -55,8 +56,8 @@ interface Scope {
55
56
  /**
56
57
  * StateStore:宿主注入的状态存储接口。
57
58
  *
58
- * CF = KV / Docker = SQLite / SDK 内嵌 = 内存。core 只依赖此接口;
59
- * 一切树配置、SK 哈希表、加密 secret 都经它读写。异步签名以兼容 KV。
59
+ * CF = D1 / Docker = SQLite 或 PG / SDK 内嵌 = 内存。core 只依赖此接口;
60
+ * 一切树配置、SK 哈希表、加密 secret 都经它读写。异步签名以兼容远端后端。
60
61
  *
61
62
  * key 布局:
62
63
  * sk:h:<sha256hex> → SecretKey(认证热路径)
@@ -84,6 +85,29 @@ interface StateStore {
84
85
  }>;
85
86
  }>;
86
87
  put(key: string, value: unknown): Promise<void>;
88
+ /**
89
+ * 可选原子原语:key 不存在则写入并返回 true;已存在则不覆盖并返回 false。
90
+ * 用途是多副本/多 isolate 并发引导的 winner-takes-all 去重(如 Admin SK 铸造)。
91
+ * 当前三宿主(D1/SQLite/PG)均原子实现;无 CAS 能力的自定义后端可不实现——
92
+ * 调用方必须容忍回退到非原子的 get-miss→put,并保证重复写幂等或后果可接受。
93
+ * 可选而非必选:必选会破坏 SDK 消费者已实现的自定义 StateStore。
94
+ */
95
+ putIfAbsent?(key: string, value: unknown): Promise<boolean>;
96
+ }
97
+
98
+ /**
99
+ * TBError:平台统一错误类型与 TBError↔HTTP 映射。
100
+ *
101
+ * core 是纯逻辑内核,不依赖任何运行时(Workers / Node)——错误的 HTTP 呈现
102
+ * 由网关层读取此处的 `httpStatus` 与 `toJSON()` 完成,core 只定义契约。
103
+ */
104
+ /** 7 个规范错误码。 */
105
+ type TBErrorCode = 'not_found' | 'permission_denied' | 'invalid_argument' | 'conflict' | 'unavailable' | 'rate_limited' | 'internal';
106
+ /** 线上 body 形状:~help、返回值、错误响应统一用它。 */
107
+ interface TBErrorBody {
108
+ code: TBErrorCode;
109
+ message: string;
110
+ retryable: boolean;
87
111
  }
88
112
 
89
113
  /**
@@ -489,6 +513,26 @@ interface DeviceCallContext {
489
513
  traceId: string;
490
514
  }
491
515
 
516
+ /**
517
+ * 网关侧设备会话状态机(纯逻辑,I/O 与时间注入,DO 只做胶水)。
518
+ *
519
+ * - awaiting-hello → ready:hello 经 io.onHello 交胶水层做 Check(register),
520
+ * 通过则 accept(mountPath)(回 ready 帧),否则 reject(error)(拒绝帧 + close(1008))。
521
+ * - 未 hello 先发非 hello 帧 / hello 重复 / 设备发出网关方向帧 → 拒绝帧 + 关闭。
522
+ * - call:requestId 幂等表——已有结果的重复 id 立即以首次结果应答;结果未到的重复 id
523
+ * 挂到同一待决项,不重复下发 call 帧。
524
+ * - 超时(DEVICE_CALL_TIMEOUT_MS):给调用方 unavailable(retryable:true),给设备 cancel 帧;
525
+ * 迟到的 result 仅入幂等表(后续重复 id 以它应答)。
526
+ */
527
+
528
+ type DeviceCallResult = {
529
+ ok: true;
530
+ value: unknown;
531
+ } | {
532
+ error: TBErrorBody;
533
+ ok: false;
534
+ };
535
+
492
536
  /**
493
537
  * Tool Layer 的中立类型。
494
538
  *
@@ -607,6 +651,52 @@ interface SqlSearchStatement {
607
651
  readonly params: readonly unknown[];
608
652
  readonly sql: string;
609
653
  }
654
+ /**
655
+ * 固定形态语句:参数个数与语义在 core 编排里写死,方言只提供 SQL 文本。
656
+ * 占位符语法(`?` / `$n`)与字符串函数由各方言自行决定。
657
+ */
658
+ interface SqlSearchStatements {
659
+ readonly bumpRevision: string;
660
+ readonly completeRebuild: string;
661
+ readonly deleteAllSnapshots: string;
662
+ readonly deleteAllTools: string;
663
+ readonly deleteSnapshot: string;
664
+ readonly deleteSnapshotPrefix: string;
665
+ readonly deleteTools: string;
666
+ readonly deleteToolsPrefix: string;
667
+ /** 单 path 的 path→digest 快照插入(replace 内联使用)。 */
668
+ readonly insertSnapshot: string;
669
+ /** 读 meta 单例(revision/seeded/cursor_secret)。 */
670
+ readonly meta: string;
671
+ /** replace 前置探测:本 path 的 digest、是否残留 source 行、已用节点数。 */
672
+ readonly pathState: string;
673
+ /** 单 path 是否存在 source 行。 */
674
+ readonly present: string;
675
+ /** path 或其子树是否存在 source 行。 */
676
+ readonly presentPrefix: string;
677
+ /** 全量快照 digest,按 path 升序。 */
678
+ readonly snapshotDigests: string;
679
+ }
680
+ /**
681
+ * SQL 方言:把与具体引擎绑定的 SQL 文本从数据库无关的编排里分出来。
682
+ *
683
+ * `SqlSearchIndex` 只认识本接口——候选查询 + 一组固定语句 + 建表 DDL。
684
+ * D1 与 better-sqlite3 同为 SQLite,共用 {@link sqliteSearchDialect};其它引擎
685
+ * (如 Postgres)实现自己的方言,替换全文检索与占位符语法。方言只管 SQL 文本,
686
+ * 具体驱动的同步/异步、批量预算、结果整形仍由 {@link SqlSearchDriver} 吸收。
687
+ */
688
+ interface SqlSearchDialect {
689
+ /**
690
+ * 把已 normalize 的 query 变成候选查询语句(含排序与分页)。
691
+ * prepare(分词、短/长词切分、方言专属语法)由各方言自理——SQLite 走 trigram
692
+ * FTS + 短词 LIKE,PG 走 ILIKE 子串;编排层只给 normalized query,不预设方言。
693
+ */
694
+ candidateStatement(query: string, limit: number, offset: number): SqlSearchStatement;
695
+ /** 建表(幂等),元素不带尾分号。 */
696
+ readonly schemaStatements: readonly string[];
697
+ /** 编排直接下发的固定语句。 */
698
+ readonly statements: SqlSearchStatements;
699
+ }
610
700
  /**
611
701
  * 宿主驱动:把 `SqlSearchStatement` 落到具体数据库上。
612
702
  *
@@ -635,7 +725,8 @@ interface SqlSearchDriver {
635
725
  declare class SqlSearchIndex implements MutableSearchIndex {
636
726
  protected readonly driver: SqlSearchDriver;
637
727
  readonly capabilities: readonly SearchCapability[];
638
- constructor(driver: SqlSearchDriver);
728
+ protected readonly dialect: SqlSearchDialect;
729
+ constructor(driver: SqlSearchDriver, dialect?: SqlSearchDialect);
639
730
  /** 容量 trigger 的 ABORT 在各驱动里错误形状不同,统一按标记串归一。 */
640
731
  private writeOrCapacity;
641
732
  private meta;
@@ -742,6 +833,18 @@ interface LocalProviderHooks {
742
833
  /** kind:'tool' 节点按路径取进程内工具源;undefined → 走 plugin 解析。 */
743
834
  tool?(nodePath: TreePath): UpstreamProvider | undefined;
744
835
  }
836
+ /**
837
+ * 就绪探测报告(GET /readyz 的响应体)。checks 逐后端给出连通结果;
838
+ * ready=false → 503,编排器(k8s readiness / LB)据此摘除流量。
839
+ * detail 只放短语级原因(如 "timeout after 1000ms"),不放凭证或拓扑细节。
840
+ */
841
+ interface ReadinessReport {
842
+ checks: Record<string, {
843
+ detail?: string;
844
+ ok: boolean;
845
+ }>;
846
+ ready: boolean;
847
+ }
745
848
  /**
746
849
  * tb app 的宿主注入面(五注入点 + 解析后的部署配置)。
747
850
  * 核心业务逻辑零分叉:Workers 适配层(app.ts)与 SDK(packages/sdk)都注入此形状。
@@ -783,6 +886,12 @@ interface TbAppDeps {
783
886
  * 那个插件解析不出 export;反之则解析得出但调不动(unavailable)。宿主该两者同源装配。
784
887
  */
785
888
  pluginCatalog?: BuiltinCatalog;
889
+ /**
890
+ * 后端连通性探测(GET /readyz;k8s readiness)。宿主注入闭包,自行探测其长连接
891
+ * 后端(PG 池 / Redis)并叠加 draining 状态。缺省 → /readyz 恒 200:请求期绑定
892
+ * 宿主(Workers 的 KV/D1/R2)无"断连"态,嵌入宿主自理。
893
+ */
894
+ readiness?: () => Promise<ReadinessReport>;
786
895
  /** context Get 的 $ref 内联阈值(字节,缺省 1 MiB)。 */
787
896
  refThresholdBytes?: number;
788
897
  /** $ref URL(presign 与 /~ref 中转)有效期秒(缺省 900)。 */
@@ -815,7 +924,8 @@ declare function createTbApp(deps: TbAppDeps): Hono<{
815
924
 
816
925
  /**
817
926
  * Node 宿主的 env 配置面。变量名与语义对齐 CF 宿主(gateway/src/app.ts 的 Env),
818
- * 仅新增宿主形态相关的 TB_PORT / TB_HOST / TB_DATA_DIR / TB_UI_DIR。
927
+ * 仅新增宿主形态相关的 TB_PORT / TB_HOST / TB_DATA_DIR / TB_UI_DIR / TB_DATABASE_URL
928
+ * 端口额外兜底平台注入的 PORT(PaaS 通行约定)。
819
929
  * 解析函数镜像 app.ts 的 allowInsecure / remoteSettingsFromEnv / positiveIntEnv。
820
930
  */
821
931
 
@@ -834,6 +944,11 @@ interface ServerConfig {
834
944
  * configFromEnv 抛错,进程拒绝启动(fail closed,不静默回退到请求期 origin)。
835
945
  */
836
946
  canonicalOrigin?: string;
947
+ /**
948
+ * Postgres 连接串(TB_DATABASE_URL)。给出则 StateStore 与 SearchIndex 都走 PG
949
+ * (ILIKE 子串检索,无扩展依赖);缺省回退到 dataDir 下的 SQLite。
950
+ */
951
+ databaseUrl?: string;
837
952
  /** SQLite 库与 fs 对象根所在目录(state.sqlite3 + objects/)。 */
838
953
  dataDir: string;
839
954
  /** 设备断线后未重连的回收秒数(缺省 24h)。 */
@@ -841,6 +956,18 @@ interface ServerConfig {
841
956
  /** SecretStore 主密钥 + $ref 中转 token 签名密钥(base64url 32B)。 */
842
957
  encryptionKey?: string;
843
958
  host: string;
959
+ /**
960
+ * 平台对象存储(context `$ref` 大对象落点)。给出则用 S3/R2 兼容端点,
961
+ * 缺省回退 dataDir 下的本地 FS。配 S3 后容器可无状态横向扩容 —— FS 落点
962
+ * 在多副本间互不可见、容器重建即丢。
963
+ */
964
+ objectStore?: {
965
+ accessKeyId: string;
966
+ bucket: string;
967
+ endpoint: string;
968
+ region?: string;
969
+ secretAccessKey: string;
970
+ };
844
971
  /**
845
972
  * 进程内插件装配表(binding 名 → fetch handler)。缺省时 bin 入口装配**全量内置目录**
846
973
  * (见 `main.ts`);程序化嵌入方可给自己的表来覆盖或裁剪。
@@ -852,6 +979,14 @@ interface ServerConfig {
852
979
  */
853
980
  pluginCatalog?: BuiltinCatalog;
854
981
  port: number;
982
+ /**
983
+ * 多副本设备通道路由的 Redis 连接串(TB_REDIS_URL)。
984
+ *
985
+ * 设备的 WebSocket 是活 socket、只存在于接受它的进程里,故多副本时打给"连在别的
986
+ * 副本上的设备"的调用必须跨副本转发。配上即启用路由表 + pub/sub 转发;
987
+ * 单副本部署不需要配(本地直连恒命中)。
988
+ */
989
+ redisUrl?: string;
855
990
  refThresholdBytes?: number;
856
991
  refTtlSec?: number;
857
992
  remote: {
@@ -860,12 +995,110 @@ interface ServerConfig {
860
995
  instanceId?: string;
861
996
  maxHops: number;
862
997
  };
998
+ /**
999
+ * 本副本标识(TB_REPLICA_ID),路由表的 value。缺省用 hostname —— 容器平台上
1000
+ * 每个副本的 hostname 天然唯一;同机多进程需显式区分。
1001
+ */
1002
+ replicaId?: string;
1003
+ /**
1004
+ * SIGTERM 后先置 draining(/readyz 转 503)再关停的等待秒数(TB_SHUTDOWN_DRAIN_SEC,
1005
+ * 缺省 0)。k8s 滚动更新时 endpoint 摘除有传播延迟,不等一拍就关会把仍被路由过来的
1006
+ * 请求吃闭门羹;单机/本地开发保持 0,关停立即进行。
1007
+ */
1008
+ shutdownDrainSec?: number;
863
1009
  toolCacheTtlSec?: number;
864
1010
  /** Dashboard 静态资源目录覆盖(缺省经 @tool-bridge/dashboard 包解析)。 */
865
1011
  uiDir?: string;
866
1012
  }
867
1013
  declare function configFromEnv(env?: NodeJS.ProcessEnv): ServerConfig;
868
1014
 
1015
+ /**
1016
+ * DeviceRouter:多副本部署下的设备调用路由。
1017
+ *
1018
+ * 为什么不能只换个存储 —— 设备的 WebSocket 是**活 socket**,只存在于接受它的那个
1019
+ * 进程里,不可序列化。副本 B 收到打给"连在副本 A 上的设备"的 HTTP 调用时,B 手里
1020
+ * 没有那个 socket,必须把调用**转发**给 A、再把结果收回来。所以这里是两件事:
1021
+ *
1022
+ * 1. 路由表(Redis string,带 TTL):deviceId → 持有者副本 ID。TTL + 周期续期让
1023
+ * 副本崩溃后条目自动过期,不需要墓碑清理。
1024
+ * 2. 请求/响应转发(Redis pub/sub):B 往 A 的频道发 call、A 执行完往 B 的回执
1025
+ * 频道发 result。跨副本的请求-响应用 correlationId 关联,并有独立超时兜底 ——
1026
+ * A 在执行途中崩溃时 B 不能永久挂起。
1027
+ *
1028
+ * 本副本自己持有连接时**完全不经这里**(DeviceHub 先查本地 Map),故单副本部署零额外
1029
+ * 开销、也不需要配 Redis。
1030
+ *
1031
+ * 与 CF 宿主的对照:那边用 Durable Object,天然"每设备一个单点",不存在这个问题。
1032
+ * 本模块是 Node 多副本的等价物。
1033
+ */
1034
+
1035
+ /** 路由条目 TTL;续期周期取其 1/3,保证正常情况下不会因抖动过期。 */
1036
+ declare const DEVICE_ROUTE_TTL_SEC = 30;
1037
+ /** 跨副本转发的等待上限(毫秒)。持有者副本崩溃时靠它兜底,不能永久挂起。 */
1038
+ declare const DEVICE_FORWARD_TIMEOUT_MS = 65000;
1039
+ /**
1040
+ * DeviceHub 需要的 Redis 能力子集。
1041
+ *
1042
+ * 抽成接口而非直接用 ioredis:让 DeviceHub 不依赖具体客户端,单测可注入内存实现,
1043
+ * 也便于将来换 Valkey / 其它 pub/sub 后端。
1044
+ */
1045
+ interface DeviceRouterBackend {
1046
+ /** 删除本副本持有的路由条目(仅当当前值仍是自己,避免删掉已顶替者的条目)。 */
1047
+ clearRoute(deviceId: string, replicaId: string): Promise<void>;
1048
+ close(): Promise<void>;
1049
+ /** 查 deviceId 的持有者副本;无则 null。 */
1050
+ lookupRoute(deviceId: string): Promise<string | null>;
1051
+ /** 向某副本的频道发布消息。 */
1052
+ publish(channel: string, payload: string): Promise<void>;
1053
+ /** 声明/续期:deviceId 由本副本持有,TTL 秒后过期。 */
1054
+ setRoute(deviceId: string, replicaId: string, ttlSec: number): Promise<void>;
1055
+ /** 订阅频道;handler 收到原始 payload。 */
1056
+ subscribe(channel: string, handler: (payload: string) => void): Promise<void>;
1057
+ }
1058
+ /**
1059
+ * 跨副本调用的发起侧与接收侧编排。
1060
+ *
1061
+ * DeviceHub 只跟本类打交道:`invoke` 时若本地无连接就问 `forward`,
1062
+ * 并把"本副本能否执行某设备调用"的能力通过 `onLocalCall` 交回来。
1063
+ */
1064
+ declare class DeviceRouter {
1065
+ readonly replicaId: string;
1066
+ private readonly backend;
1067
+ private readonly opts;
1068
+ private readonly pending;
1069
+ private renewTimer;
1070
+ /** 本副本当前持有的设备(需周期续期路由条目)。 */
1071
+ private readonly owned;
1072
+ constructor(replicaId: string, backend: DeviceRouterBackend, opts: {
1073
+ forwardTimeoutMs?: number;
1074
+ /** 收到转发来的调用时,在本副本执行(本地无该连接则返回 offline)。 */
1075
+ onLocalCall: (deviceId: string, req: DeviceInvokeRequest) => Promise<DeviceCallResult>;
1076
+ ttlSec?: number;
1077
+ });
1078
+ private get ttlSec();
1079
+ /** 订阅本副本的两个频道并启动路由续期。 */
1080
+ start(): Promise<void>;
1081
+ /** 本副本接受了某设备的连接:声明所有权。 */
1082
+ claim(deviceId: string): Promise<void>;
1083
+ /** 本副本失去某设备的连接:释放所有权。 */
1084
+ release(deviceId: string): Promise<void>;
1085
+ /** 某设备当前是否在**任一**副本上在线(reclaim 判定用,不能只看本副本)。 */
1086
+ isOnlineAnywhere(deviceId: string): Promise<boolean>;
1087
+ /**
1088
+ * 把调用转发给持有该设备的副本并等结果。
1089
+ * 无人持有 → null(调用方按 deviceOffline 处理)。
1090
+ */
1091
+ forward(deviceId: string, req: DeviceInvokeRequest): Promise<DeviceCallResult | null>;
1092
+ close(): Promise<void>;
1093
+ /** correlationId 的进程内序号(与 replicaId 组合后全局唯一)。 */
1094
+ private static seq;
1095
+ private renewOwned;
1096
+ /** 作为持有者:执行本地调用并回执。 */
1097
+ private handleForwardedCall;
1098
+ /** 作为发起方:收到回执,唤醒等待者。 */
1099
+ private handleReply;
1100
+ }
1101
+
869
1102
  /**
870
1103
  * DeviceHub:Node 宿主的设备通道(DeviceChannel 实现,对位 CF 的 DeviceSession DO)。
871
1104
  *
@@ -882,6 +1115,8 @@ declare function configFromEnv(env?: NodeJS.ProcessEnv): ServerConfig;
882
1115
  * 探活(isAlive + 周期 ping + terminate)踢半开死连接,避免调用一律吃 60s 超时。
883
1116
  */
884
1117
 
1118
+ /** router 回调本副本执行设备调用的签名。 */
1119
+ type DeviceLocalCall = (deviceId: string, req: DeviceInvokeRequest) => Promise<DeviceCallResult>;
885
1120
  declare const DEVICE_WS_PATH = "/system/device/ws";
886
1121
  declare class DeviceHub {
887
1122
  private readonly store;
@@ -892,16 +1127,38 @@ declare class DeviceHub {
892
1127
  private readonly connections;
893
1128
  private readonly reclaimTimers;
894
1129
  private heartbeat;
1130
+ /** 多副本路由;缺省(单副本)时所有跨副本逻辑短路,行为与之前完全一致。 */
1131
+ private router;
895
1132
  constructor(opts: {
896
1133
  heartbeatMs?: number;
897
1134
  reclaimSec: number;
1135
+ /** 多副本部署的调用路由工厂;拿到 onLocalCall 回调后构造(避免循环依赖)。 */
1136
+ router?: (onLocalCall: DeviceLocalCall) => DeviceRouter;
898
1137
  search?: MutableSearchIndex;
899
1138
  store: StateStore;
900
1139
  });
1140
+ /**
1141
+ * 启动多副本路由(订阅本副本频道 + 路由续期)。单副本时是 no-op。
1142
+ * 必须在开始服务前完成:订阅没建好时转发进来的调用会丢。
1143
+ */
1144
+ startRouter(): Promise<void>;
901
1145
  /** 挂到 http.Server 的 'upgrade' 事件(仅处理 DEVICE_WS_PATH,其余 404)。 */
902
1146
  attach(server: http.Server): void;
903
- /** DeviceChannel.invoke:HTTP→WS 调用转发(无活连接 → deviceOffline)。 */
1147
+ /**
1148
+ * DeviceChannel.invoke:HTTP→WS 调用转发。
1149
+ *
1150
+ * 本副本持有该设备 → 直接走本地 socket(单副本部署恒走这条,零额外开销)。
1151
+ * 本地没有但配了 router → 问路由表并转发给持有者副本(socket 不可序列化,
1152
+ * 只能把调用送到 socket 所在进程)。都没有 → deviceOffline。
1153
+ */
904
1154
  invoke(deviceId: string, req: DeviceInvokeRequest): Promise<unknown>;
1155
+ /**
1156
+ * 只走本副本 socket 的调用执行。
1157
+ *
1158
+ * router 收到转发来的调用时回调这里 —— **不能**回调 `invoke`:本地连接刚断时
1159
+ * 那会再次查路由并转发,在两个副本间弹来弹去。这里没有连接就直接 offline。
1160
+ */
1161
+ private invokeLocal;
905
1162
  /** DeviceChannel.ws:Node 宿主的升级在 http 层处理,永不应命中此路由。 */
906
1163
  ws(): Promise<Response>;
907
1164
  /**
@@ -935,6 +1192,114 @@ declare class DeviceHub {
935
1192
 
936
1193
  declare function createDataObjectStore(dataDir: string): ObjectStore;
937
1194
 
1195
+ /**
1196
+ * PgSearchIndex:postgres.js 驱动的 SearchIndex(ILIKE 子串检索)。
1197
+ *
1198
+ * 方言(SQL 文本、$n 占位符、ILIKE 检索)在 core 的 pgSearchDialect;
1199
+ * 本文件只吸收 postgres.js 的驱动差异:全异步、写入走 sql.begin() 事务、多行 VALUES
1200
+ * 批量插入(PG 无 D1 的 50 查询预算,故不实现 assertInsertBudget)。
1201
+ *
1202
+ * 不需要任何 PG 扩展:检索是纯 ILIKE,cursor_secret 用内置 gen_random_uuid()。
1203
+ * 因此连接角色无需建扩展权限(受限托管环境常不给),只要能建表。
1204
+ */
1205
+
1206
+ /** Node 自托管宿主的 postgres.js SearchIndex。 */
1207
+ declare class PgSearchIndex extends SqlSearchIndex {
1208
+ constructor(sql: Sql);
1209
+ }
1210
+
1211
+ /**
1212
+ * PgStateStore:postgres.js 实现的 StateStore(自托管 Node 宿主的 PG 后端)。
1213
+ *
1214
+ * 与 SqliteStateStore 同形:单表 kv(key text primary key, value jsonb),值以
1215
+ * JSON 存取,强一致。list 用 key 范围扫描(>= prefix AND < successor(prefix)),
1216
+ * 不用 LIKE——key 里的路径段可含 '_'/'%',通配符转义是坑。
1217
+ *
1218
+ * 关键差异——排序 collation:PG 默认按 libc locale 排序,`<` 与 `ORDER BY` 的
1219
+ * 顺序会和 JS(UTF-16 code unit)/SQLite(UTF-8 字节)不一致,直接让前缀范围扫描和
1220
+ * cursor 分页错行漏行。故 key 列显式 `COLLATE "C"`(纯字节序),与 core
1221
+ * MemoryStateStore / SqliteStateStore 的 cursor 语义对齐。防御性地对返回行再做
1222
+ * startsWith 过滤。
1223
+ */
1224
+
1225
+ declare class PgStateStore implements StateStore {
1226
+ private readonly sql;
1227
+ constructor(sql: Sql);
1228
+ /** 建表(幂等)。key COLLATE "C" 是字节序正确性的前提,不能省。 */
1229
+ ensureSchema(): Promise<void>;
1230
+ get(key: string): Promise<unknown | null>;
1231
+ getMany(keys: readonly string[]): Promise<Map<string, unknown>>;
1232
+ put(key: string, value: unknown): Promise<void>;
1233
+ putIfAbsent(key: string, value: unknown): Promise<boolean>;
1234
+ delete(key: string): Promise<void>;
1235
+ list(prefix: string, opts?: {
1236
+ cursor?: string;
1237
+ limit?: number;
1238
+ }): Promise<{
1239
+ cursor?: string;
1240
+ items: Array<{
1241
+ key: string;
1242
+ value: unknown;
1243
+ }>;
1244
+ }>;
1245
+ }
1246
+
1247
+ /**
1248
+ * DeviceRouterBackend 的 Redis(ioredis)实现。
1249
+ *
1250
+ * 两条连接是**必须**的:Redis 连接一旦进入 subscribe 模式就不能再发普通命令,
1251
+ * 而本副本既要订阅自己的频道、又要 GET/SET 路由并向别人 publish。
1252
+ *
1253
+ * clearRoute 用 Lua 做「值匹配才删」:设备刚从副本 A 迁到 B 时,A 的收尾不能删掉
1254
+ * B 刚写入的条目 —— 否则设备明明在线却被判离线。GET+DEL 两步做不到原子,故用脚本。
1255
+ */
1256
+
1257
+ declare class RedisDeviceRouterBackend implements DeviceRouterBackend {
1258
+ /** 命令连接(GET/SET/DEL/PUBLISH)。 */
1259
+ private readonly commands;
1260
+ /** 订阅连接(进入 subscribe 模式后不能发普通命令)。 */
1261
+ private readonly subscriber;
1262
+ private readonly handlers;
1263
+ constructor(url: string);
1264
+ setRoute(deviceId: string, replicaId: string, ttlSec: number): Promise<void>;
1265
+ lookupRoute(deviceId: string): Promise<string | null>;
1266
+ clearRoute(deviceId: string, replicaId: string): Promise<void>;
1267
+ publish(channel: string, payload: string): Promise<void>;
1268
+ /** 就绪探测(/readyz):命令连接 PING 一次。断连时 ioredis 会抛/挂起,由调用方限时。 */
1269
+ ping(): Promise<void>;
1270
+ subscribe(channel: string, handler: (payload: string) => void): Promise<void>;
1271
+ close(): Promise<void>;
1272
+ }
1273
+
1274
+ /**
1275
+ * createTbServer:Node 宿主装配(对位 gateway/src/app.ts 的 depsFromEnv)。
1276
+ *
1277
+ * StateStore/SearchIndex 后端二选一:给 TB_DATABASE_URL 走 Postgres,
1278
+ * 否则走 dataDir 下的 SQLite。ObjectStore 始终是 fs。HTTP 用 @hono/node-server;
1279
+ * 引导在 start() 时直调宿主中立 runBootstrap(Node 有真实启动点,不需要 Workers 的
1280
+ * per-request once,故不注入 deps.ensureReady)。设备通道(DeviceHub)与 /ui 静态托管
1281
+ * 由后续装配点注入(deps.device / deps.assets)。
1282
+ */
1283
+
1284
+ interface TbServer {
1285
+ app: ReturnType<typeof createTbApp>;
1286
+ close(): Promise<void>;
1287
+ deviceHub: DeviceHub;
1288
+ search: MutableSearchIndex;
1289
+ /** 引导(幂等)+ 孤儿设备回收排程 + 监听;返回实际端口(config.port=0 时由系统分配)。 */
1290
+ start(): Promise<{
1291
+ port: number;
1292
+ }>;
1293
+ /**
1294
+ * 进入 draining:/readyz 立即转 503(编排器摘流量),但继续服务既有与新到请求。
1295
+ * SIGTERM 处理器先调它、等一拍(TB_SHUTDOWN_DRAIN_SEC)再 close(),避免 k8s
1296
+ * endpoint 摘除传播期间仍被路由过来的请求吃闭门羹。幂等。
1297
+ */
1298
+ startDraining(): void;
1299
+ state: StateStore;
1300
+ }
1301
+ declare function createTbServer(config: ServerConfig): TbServer;
1302
+
938
1303
  /** Node 宿主的 better-sqlite3 FTS5/trigram SearchIndex。 */
939
1304
  declare class SqliteSearchIndex extends SqlSearchIndex {
940
1305
  private readonly db;
@@ -967,6 +1332,7 @@ declare class SqliteStateStore implements StateStore {
967
1332
  get(key: string): Promise<unknown | null>;
968
1333
  getMany(keys: readonly string[]): Promise<Map<string, unknown>>;
969
1334
  put(key: string, value: unknown): Promise<void>;
1335
+ putIfAbsent(key: string, value: unknown): Promise<boolean>;
970
1336
  delete(key: string): Promise<void>;
971
1337
  list(prefix: string, opts?: {
972
1338
  cursor?: string;
@@ -981,26 +1347,4 @@ declare class SqliteStateStore implements StateStore {
981
1347
  close(): void;
982
1348
  }
983
1349
 
984
- /**
985
- * createTbServer:Node 宿主装配(对位 gateway/src/app.ts 的 depsFromEnv)。
986
- *
987
- * SQLite StateStore + fs ObjectStore + @hono/node-server;引导在 start() 时
988
- * 直调宿主中立 runBootstrap(Node 有真实启动点,不需要 Workers 的 per-request once,
989
- * 故不注入 deps.ensureReady)。设备通道(DeviceHub)与 /ui 静态托管由后续
990
- * 装配点注入(deps.device / deps.assets)。
991
- */
992
-
993
- interface TbServer {
994
- app: ReturnType<typeof createTbApp>;
995
- close(): Promise<void>;
996
- deviceHub: DeviceHub;
997
- search: SqliteSearchIndex;
998
- /** 引导(幂等)+ 孤儿设备回收排程 + 监听;返回实际端口(config.port=0 时由系统分配)。 */
999
- start(): Promise<{
1000
- port: number;
1001
- }>;
1002
- state: SqliteStateStore;
1003
- }
1004
- declare function createTbServer(config: ServerConfig): TbServer;
1005
-
1006
- export { DEVICE_WS_PATH, DeviceHub, type ServerConfig, SqliteSearchIndex, SqliteStateStore, type TbServer, configFromEnv, createDataObjectStore, createTbServer };
1350
+ export { DEVICE_FORWARD_TIMEOUT_MS, DEVICE_ROUTE_TTL_SEC, DEVICE_WS_PATH, DeviceHub, type DeviceLocalCall, DeviceRouter, type DeviceRouterBackend, PgSearchIndex, PgStateStore, RedisDeviceRouterBackend, type ServerConfig, SqliteSearchIndex, SqliteStateStore, type TbServer, configFromEnv, createDataObjectStore, createTbServer };
package/dist/index.js CHANGED
@@ -1,17 +1,29 @@
1
1
  import {
2
+ DEVICE_FORWARD_TIMEOUT_MS,
3
+ DEVICE_ROUTE_TTL_SEC,
2
4
  DEVICE_WS_PATH,
3
5
  DeviceHub,
6
+ DeviceRouter,
7
+ PgSearchIndex,
8
+ PgStateStore,
9
+ RedisDeviceRouterBackend,
4
10
  SqliteSearchIndex,
5
11
  SqliteStateStore,
6
12
  configFromEnv,
7
13
  createDataObjectStore,
8
14
  createTbServer
9
- } from "./chunk-7ASYWVYP.js";
10
- import "./chunk-76YX7XSX.js";
15
+ } from "./chunk-EMUJLM32.js";
16
+ import "./chunk-AWXZMW6M.js";
11
17
  import "./chunk-MLKGABMK.js";
12
18
  export {
19
+ DEVICE_FORWARD_TIMEOUT_MS,
20
+ DEVICE_ROUTE_TTL_SEC,
13
21
  DEVICE_WS_PATH,
14
22
  DeviceHub,
23
+ DeviceRouter,
24
+ PgSearchIndex,
25
+ PgStateStore,
26
+ RedisDeviceRouterBackend,
15
27
  SqliteSearchIndex,
16
28
  SqliteStateStore,
17
29
  configFromEnv,
@@ -2,15 +2,15 @@ import {
2
2
  createProviderPlugin,
3
3
  requireApiKey,
4
4
  upstreamError
5
- } from "./chunk-BUHY4IE6.js";
5
+ } from "./chunk-V4VHC3Q6.js";
6
6
  import {
7
7
  guardedFetch
8
- } from "./chunk-EBO6Q4NR.js";
9
- import "./chunk-GMHELPK3.js";
8
+ } from "./chunk-THDEI7S7.js";
9
+ import "./chunk-WIH5OHFG.js";
10
10
  import {
11
11
  TBError,
12
12
  external_exports
13
- } from "./chunk-76YX7XSX.js";
13
+ } from "./chunk-AWXZMW6M.js";
14
14
  import "./chunk-MLKGABMK.js";
15
15
 
16
16
  // ../plugins/src/ip2proxy/schema.ts
@@ -2,15 +2,15 @@ import {
2
2
  createProviderPlugin,
3
3
  requireApiKey,
4
4
  upstreamError
5
- } from "./chunk-BUHY4IE6.js";
5
+ } from "./chunk-V4VHC3Q6.js";
6
6
  import {
7
7
  guardedFetch
8
- } from "./chunk-EBO6Q4NR.js";
9
- import "./chunk-GMHELPK3.js";
8
+ } from "./chunk-THDEI7S7.js";
9
+ import "./chunk-WIH5OHFG.js";
10
10
  import {
11
11
  TBError,
12
12
  external_exports
13
- } from "./chunk-76YX7XSX.js";
13
+ } from "./chunk-AWXZMW6M.js";
14
14
  import "./chunk-MLKGABMK.js";
15
15
 
16
16
  // ../plugins/src/ipgeolocation_io/api.ts
@@ -2,15 +2,15 @@ import {
2
2
  createProviderPlugin,
3
3
  requireApiKey,
4
4
  upstreamError
5
- } from "./chunk-BUHY4IE6.js";
5
+ } from "./chunk-V4VHC3Q6.js";
6
6
  import {
7
7
  guardedFetch
8
- } from "./chunk-EBO6Q4NR.js";
9
- import "./chunk-GMHELPK3.js";
8
+ } from "./chunk-THDEI7S7.js";
9
+ import "./chunk-WIH5OHFG.js";
10
10
  import {
11
11
  TBError,
12
12
  external_exports
13
- } from "./chunk-76YX7XSX.js";
13
+ } from "./chunk-AWXZMW6M.js";
14
14
  import "./chunk-MLKGABMK.js";
15
15
 
16
16
  // ../plugins/src/ipqualityscore/api.ts
@@ -2,15 +2,15 @@ import {
2
2
  createProviderPlugin,
3
3
  requireApiKey,
4
4
  upstreamError
5
- } from "./chunk-BUHY4IE6.js";
5
+ } from "./chunk-V4VHC3Q6.js";
6
6
  import {
7
7
  guardedFetch
8
- } from "./chunk-EBO6Q4NR.js";
9
- import "./chunk-GMHELPK3.js";
8
+ } from "./chunk-THDEI7S7.js";
9
+ import "./chunk-WIH5OHFG.js";
10
10
  import {
11
11
  TBError,
12
12
  external_exports
13
- } from "./chunk-76YX7XSX.js";
13
+ } from "./chunk-AWXZMW6M.js";
14
14
  import "./chunk-MLKGABMK.js";
15
15
 
16
16
  // ../plugins/src/jina_ai/api.ts
@@ -2,16 +2,16 @@ import {
2
2
  createProviderPlugin,
3
3
  requireCredential,
4
4
  upstreamError
5
- } from "./chunk-BUHY4IE6.js";
5
+ } from "./chunk-V4VHC3Q6.js";
6
6
  import {
7
7
  assertPublicHttpUrl,
8
8
  guardedFetch
9
- } from "./chunk-EBO6Q4NR.js";
10
- import "./chunk-GMHELPK3.js";
9
+ } from "./chunk-THDEI7S7.js";
10
+ import "./chunk-WIH5OHFG.js";
11
11
  import {
12
12
  TBError,
13
13
  external_exports
14
- } from "./chunk-76YX7XSX.js";
14
+ } from "./chunk-AWXZMW6M.js";
15
15
  import "./chunk-MLKGABMK.js";
16
16
 
17
17
  // ../plugins/src/jira/api.ts