@botcord/daemon 0.2.35 → 0.2.37

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 (68) hide show
  1. package/dist/config.d.ts +30 -1
  2. package/dist/config.js +27 -0
  3. package/dist/daemon-config-map.d.ts +3 -0
  4. package/dist/daemon-config-map.js +30 -0
  5. package/dist/daemon.d.ts +15 -1
  6. package/dist/daemon.js +56 -11
  7. package/dist/gateway/channels/botcord.js +44 -0
  8. package/dist/gateway/channels/http-types.d.ts +19 -0
  9. package/dist/gateway/channels/http-types.js +1 -0
  10. package/dist/gateway/channels/index.d.ts +5 -0
  11. package/dist/gateway/channels/index.js +5 -0
  12. package/dist/gateway/channels/login-session.d.ts +83 -0
  13. package/dist/gateway/channels/login-session.js +99 -0
  14. package/dist/gateway/channels/secret-store.d.ts +21 -0
  15. package/dist/gateway/channels/secret-store.js +75 -0
  16. package/dist/gateway/channels/state-store.d.ts +60 -0
  17. package/dist/gateway/channels/state-store.js +173 -0
  18. package/dist/gateway/channels/telegram.d.ts +31 -0
  19. package/dist/gateway/channels/telegram.js +371 -0
  20. package/dist/gateway/channels/text-split.d.ts +13 -0
  21. package/dist/gateway/channels/text-split.js +33 -0
  22. package/dist/gateway/channels/url-guard.d.ts +18 -0
  23. package/dist/gateway/channels/url-guard.js +53 -0
  24. package/dist/gateway/channels/wechat-http.d.ts +18 -0
  25. package/dist/gateway/channels/wechat-http.js +28 -0
  26. package/dist/gateway/channels/wechat-login.d.ts +36 -0
  27. package/dist/gateway/channels/wechat-login.js +62 -0
  28. package/dist/gateway/channels/wechat.d.ts +40 -0
  29. package/dist/gateway/channels/wechat.js +472 -0
  30. package/dist/gateway/runtimes/openclaw-acp.js +211 -6
  31. package/dist/gateway/types.d.ts +10 -0
  32. package/dist/gateway-control.d.ts +53 -0
  33. package/dist/gateway-control.js +638 -0
  34. package/dist/openclaw-discovery.js +1 -1
  35. package/dist/provision.d.ts +7 -0
  36. package/dist/provision.js +255 -5
  37. package/package.json +1 -1
  38. package/src/__tests__/gateway-control.test.ts +499 -0
  39. package/src/__tests__/openclaw-acp.test.ts +63 -0
  40. package/src/__tests__/openclaw-discovery.test.ts +36 -0
  41. package/src/__tests__/provision.test.ts +179 -0
  42. package/src/__tests__/secret-store.test.ts +70 -0
  43. package/src/__tests__/state-store.test.ts +119 -0
  44. package/src/__tests__/third-party-gateway.test.ts +126 -0
  45. package/src/__tests__/url-guard.test.ts +85 -0
  46. package/src/__tests__/wechat-channel.test.ts +1134 -0
  47. package/src/config.ts +72 -1
  48. package/src/daemon-config-map.ts +24 -0
  49. package/src/daemon.ts +70 -11
  50. package/src/gateway/__tests__/botcord-channel.test.ts +1 -1
  51. package/src/gateway/__tests__/telegram-channel.test.ts +555 -0
  52. package/src/gateway/channels/botcord.ts +39 -0
  53. package/src/gateway/channels/http-types.ts +22 -0
  54. package/src/gateway/channels/index.ts +22 -0
  55. package/src/gateway/channels/login-session.ts +135 -0
  56. package/src/gateway/channels/secret-store.ts +100 -0
  57. package/src/gateway/channels/state-store.ts +213 -0
  58. package/src/gateway/channels/telegram.ts +469 -0
  59. package/src/gateway/channels/text-split.ts +29 -0
  60. package/src/gateway/channels/url-guard.ts +55 -0
  61. package/src/gateway/channels/wechat-http.ts +35 -0
  62. package/src/gateway/channels/wechat-login.ts +90 -0
  63. package/src/gateway/channels/wechat.ts +572 -0
  64. package/src/gateway/runtimes/openclaw-acp.ts +211 -7
  65. package/src/gateway/types.ts +10 -0
  66. package/src/gateway-control.ts +709 -0
  67. package/src/openclaw-discovery.ts +1 -1
  68. package/src/provision.ts +336 -5
package/src/config.ts CHANGED
@@ -95,10 +95,35 @@ export interface OpenclawDiscoveryConfig {
95
95
  searchPaths?: string[];
96
96
  /** Overrides the local loopback ports to probe. */
97
97
  defaultPorts?: number[];
98
- /** Defaults to true. When false, discovery only persists gateways. */
98
+ /** Defaults to false. When false, discovery only persists gateways. */
99
99
  autoProvision?: boolean;
100
100
  }
101
101
 
102
+ /** Third-party messaging provider supported by the daemon's channel factory. */
103
+ export type ThirdPartyGatewayType = "telegram" | "wechat";
104
+
105
+ /**
106
+ * One third-party gateway profile bound to a BotCord agent. `id` is the
107
+ * channel id (typically `gw_...` minted by the Hub); `accountId` is the
108
+ * BotCord agent the inbound traffic should be attributed to. Secrets and
109
+ * provider cursors live outside this struct — see `secretFile` and
110
+ * `stateFile`. When omitted, the daemon derives them as
111
+ * `~/.botcord/daemon/gateways/{id}.json` and `{id}.state.json`.
112
+ */
113
+ export interface ThirdPartyGatewayProfile {
114
+ id: string;
115
+ type: ThirdPartyGatewayType;
116
+ accountId: string;
117
+ label?: string;
118
+ enabled?: boolean;
119
+ secretFile?: string;
120
+ stateFile?: string;
121
+ allowedSenderIds?: string[];
122
+ allowedChatIds?: string[];
123
+ splitAt?: number;
124
+ baseUrl?: string;
125
+ }
126
+
102
127
  export interface DaemonConfig {
103
128
  /**
104
129
  * @deprecated Kept for backward compatibility with pre-multi-agent configs.
@@ -148,6 +173,13 @@ export interface DaemonConfig {
148
173
  * search paths/ports and automatic adoption of discovered agents.
149
174
  */
150
175
  openclawDiscovery?: OpenclawDiscoveryConfig;
176
+
177
+ /**
178
+ * Third-party messaging gateways (Telegram, WeChat, …) bound to BotCord
179
+ * agents on this daemon. Each entry becomes one channel in the gateway
180
+ * runtime; `enabled === false` entries are filtered out at boot.
181
+ */
182
+ thirdPartyGateways?: ThirdPartyGatewayProfile[];
151
183
  }
152
184
 
153
185
  /**
@@ -393,6 +425,45 @@ export function loadConfig(): DaemonConfig {
393
425
  }
394
426
  out.openclawDiscovery = copy;
395
427
  }
428
+ const tpg = (parsed as Partial<DaemonConfig>).thirdPartyGateways;
429
+ if (tpg !== undefined) {
430
+ if (!Array.isArray(tpg)) {
431
+ throw new Error(
432
+ `daemon config "thirdPartyGateways" must be an array (${CONFIG_PATH})`,
433
+ );
434
+ }
435
+ const seen = new Set<string>();
436
+ for (const [i, g] of tpg.entries()) {
437
+ if (!g || typeof g !== "object") {
438
+ throw new Error(
439
+ `daemon config thirdPartyGateways[${i}] is not an object (${CONFIG_PATH})`,
440
+ );
441
+ }
442
+ const gg = g as Partial<ThirdPartyGatewayProfile>;
443
+ if (typeof gg.id !== "string" || gg.id.length === 0) {
444
+ throw new Error(
445
+ `daemon config thirdPartyGateways[${i}].id must be a non-empty string (${CONFIG_PATH})`,
446
+ );
447
+ }
448
+ if (gg.type !== "telegram" && gg.type !== "wechat") {
449
+ throw new Error(
450
+ `daemon config thirdPartyGateways[${i}].type must be "telegram" or "wechat" (${CONFIG_PATH})`,
451
+ );
452
+ }
453
+ if (typeof gg.accountId !== "string" || gg.accountId.length === 0) {
454
+ throw new Error(
455
+ `daemon config thirdPartyGateways[${i}].accountId must be a non-empty string (${CONFIG_PATH})`,
456
+ );
457
+ }
458
+ if (seen.has(gg.id)) {
459
+ throw new Error(
460
+ `daemon config thirdPartyGateways[${i}].id "${gg.id}" duplicated (${CONFIG_PATH})`,
461
+ );
462
+ }
463
+ seen.add(gg.id);
464
+ }
465
+ out.thirdPartyGateways = (tpg as ThirdPartyGatewayProfile[]).map((g) => ({ ...g }));
466
+ }
396
467
  return out;
397
468
  }
398
469
 
@@ -135,6 +135,10 @@ export const DEFAULT_BOTCORD_CHANNEL_ID = "botcord-main";
135
135
  /** Channel `type` tag used by `createBotCordChannel`. */
136
136
  export const BOTCORD_CHANNEL_TYPE = "botcord";
137
137
 
138
+ /** Channel `type` tags for built-in third-party providers. */
139
+ export const TELEGRAM_CHANNEL_TYPE = "telegram";
140
+ export const WECHAT_CHANNEL_TYPE = "wechat";
141
+
138
142
  /**
139
143
  * Map daemon's historical narrower TrustLevel ("owner" | "untrusted") onto
140
144
  * gateway's ("owner" | "trusted" | "public"). Matches the adapter-level
@@ -241,6 +245,26 @@ export function toGatewayConfig(
241
245
  agentId,
242
246
  }));
243
247
 
248
+ // Append one channel per enabled third-party gateway. Disabled entries are
249
+ // dropped here so the gateway runtime never sees them; re-enabling requires
250
+ // an `upsert_gateway` (Phase B) or a config reload.
251
+ for (const g of cfg.thirdPartyGateways ?? []) {
252
+ if (g.enabled === false) continue;
253
+ const ch: GatewayChannelConfig = {
254
+ id: g.id,
255
+ type: g.type,
256
+ accountId: g.accountId,
257
+ };
258
+ if (g.label !== undefined) ch.label = g.label;
259
+ if (g.secretFile !== undefined) ch.secretFile = g.secretFile;
260
+ if (g.stateFile !== undefined) ch.stateFile = g.stateFile;
261
+ if (g.allowedSenderIds !== undefined) ch.allowedSenderIds = g.allowedSenderIds;
262
+ if (g.allowedChatIds !== undefined) ch.allowedChatIds = g.allowedChatIds;
263
+ if (g.splitAt !== undefined) ch.splitAt = g.splitAt;
264
+ if (g.baseUrl !== undefined) ch.baseUrl = g.baseUrl;
265
+ channels.push(ch);
266
+ }
267
+
244
268
  // DaemonConfig's typed surface doesn't carry `trustLevel`, but we read it
245
269
  // defensively so future config extensions can propagate without a shape bump.
246
270
  const profiles = prepareGatewayProfiles(cfg.openclawGateways);
package/src/daemon.ts CHANGED
@@ -6,6 +6,8 @@ import {
6
6
  import {
7
7
  Gateway,
8
8
  createBotCordChannel,
9
+ createTelegramChannel,
10
+ createWechatChannel,
9
11
  resolveTranscriptEnabled,
10
12
  sanitizeUntrustedContent,
11
13
  type ChannelAdapter,
@@ -116,6 +118,69 @@ export function createActivityRecorder(opts: {
116
118
  };
117
119
  }
118
120
 
121
+ /** Per-call dependencies for {@link createDaemonChannel}. */
122
+ export interface CreateDaemonChannelDeps {
123
+ credentialPathByAgentId: Map<string, string>;
124
+ defaultCredentialsPath?: string;
125
+ hubBaseUrl?: string;
126
+ }
127
+
128
+ /**
129
+ * Dispatch a `GatewayChannelConfig` to the right adapter constructor based on
130
+ * `chCfg.type`. Phase A wires up the BotCord adapter and stub constructors
131
+ * for telegram/wechat (which throw "not implemented"); Phase B will fill the
132
+ * latter in. Unknown types throw so misconfigured channels fail loudly at
133
+ * boot rather than silently dropping inbound traffic.
134
+ */
135
+ export function createDaemonChannel(
136
+ chCfg: GatewayChannelConfig,
137
+ deps: CreateDaemonChannelDeps,
138
+ ): ChannelAdapter {
139
+ switch (chCfg.type) {
140
+ case "botcord": {
141
+ const agentId =
142
+ typeof chCfg.agentId === "string" ? chCfg.agentId : chCfg.accountId;
143
+ return createBotCordChannel({
144
+ id: chCfg.id,
145
+ accountId: chCfg.accountId,
146
+ agentId,
147
+ credentialsPath:
148
+ deps.credentialPathByAgentId.get(agentId) ?? deps.defaultCredentialsPath,
149
+ hubBaseUrl: deps.hubBaseUrl,
150
+ });
151
+ }
152
+ case "telegram":
153
+ return createTelegramChannel({
154
+ id: chCfg.id,
155
+ accountId: chCfg.accountId,
156
+ ...(typeof chCfg.baseUrl === "string" ? { baseUrl: chCfg.baseUrl } : {}),
157
+ ...(Array.isArray(chCfg.allowedSenderIds)
158
+ ? { allowedSenderIds: chCfg.allowedSenderIds as string[] }
159
+ : {}),
160
+ ...(Array.isArray(chCfg.allowedChatIds)
161
+ ? { allowedChatIds: chCfg.allowedChatIds as string[] }
162
+ : {}),
163
+ ...(typeof chCfg.splitAt === "number" ? { splitAt: chCfg.splitAt } : {}),
164
+ ...(typeof chCfg.secretFile === "string" ? { secretFile: chCfg.secretFile } : {}),
165
+ ...(typeof chCfg.stateFile === "string" ? { stateFile: chCfg.stateFile } : {}),
166
+ });
167
+ case "wechat":
168
+ return createWechatChannel({
169
+ id: chCfg.id,
170
+ accountId: chCfg.accountId,
171
+ ...(typeof chCfg.baseUrl === "string" ? { baseUrl: chCfg.baseUrl } : {}),
172
+ ...(Array.isArray(chCfg.allowedSenderIds)
173
+ ? { allowedSenderIds: chCfg.allowedSenderIds as string[] }
174
+ : {}),
175
+ ...(typeof chCfg.splitAt === "number" ? { splitAt: chCfg.splitAt } : {}),
176
+ ...(typeof chCfg.secretFile === "string" ? { secretFile: chCfg.secretFile } : {}),
177
+ ...(typeof chCfg.stateFile === "string" ? { stateFile: chCfg.stateFile } : {}),
178
+ });
179
+ default:
180
+ throw new Error(`unknown channel type "${chCfg.type}"`);
181
+ }
182
+ }
183
+
119
184
  /**
120
185
  * Minimal send-capable surface used by {@link pushRuntimeSnapshot}.
121
186
  * Exists so the helper is trivially mockable from unit tests without needing
@@ -414,18 +479,12 @@ export async function startDaemon(opts: DaemonRuntimeOptions): Promise<DaemonHan
414
479
  const gateway = new Gateway({
415
480
  config: gwConfig,
416
481
  sessionStorePath: opts.sessionStorePath ?? SESSIONS_PATH,
417
- createChannel: (chCfg: GatewayChannelConfig): ChannelAdapter => {
418
- const agentId =
419
- typeof chCfg.agentId === "string" ? chCfg.agentId : chCfg.accountId;
420
- return createBotCordChannel({
421
- id: chCfg.id,
422
- accountId: chCfg.accountId,
423
- agentId,
424
- credentialsPath:
425
- credentialPathByAgentId.get(agentId) ?? opts.credentialsPath,
482
+ createChannel: (chCfg: GatewayChannelConfig): ChannelAdapter =>
483
+ createDaemonChannel(chCfg, {
484
+ credentialPathByAgentId,
485
+ defaultCredentialsPath: opts.credentialsPath,
426
486
  hubBaseUrl: opts.hubBaseUrl,
427
- });
428
- },
487
+ }),
429
488
  log: logger,
430
489
  turnTimeoutMs: DEFAULT_TURN_TIMEOUT_MS,
431
490
  buildSystemContext,
@@ -648,7 +648,7 @@ describe("createBotCordChannel — streamBlock()", () => {
648
648
  expect(body.block).toEqual({
649
649
  kind: "thinking",
650
650
  seq: 7,
651
- payload: { phase: "updated", label: "Searching web", source: "runtime" },
651
+ payload: { phase: "updated", label: "Searching web", source: "runtime", details: "Searching web" },
652
652
  });
653
653
  } finally {
654
654
  globalThis.fetch = realFetch;