@botcord/daemon 0.2.59 → 0.2.61

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 (43) hide show
  1. package/dist/config.d.ts +4 -1
  2. package/dist/config.js +2 -2
  3. package/dist/cross-room.js +3 -1
  4. package/dist/daemon-config-map.js +6 -0
  5. package/dist/daemon.js +21 -1
  6. package/dist/gateway/channels/botcord.d.ts +7 -0
  7. package/dist/gateway/channels/botcord.js +3 -1
  8. package/dist/gateway/channels/feishu-registration.d.ts +35 -0
  9. package/dist/gateway/channels/feishu-registration.js +101 -0
  10. package/dist/gateway/channels/feishu.d.ts +16 -0
  11. package/dist/gateway/channels/feishu.js +459 -0
  12. package/dist/gateway/channels/index.d.ts +2 -0
  13. package/dist/gateway/channels/index.js +2 -0
  14. package/dist/gateway/channels/login-session.d.ts +9 -1
  15. package/dist/gateway/channels/login-session.js +1 -1
  16. package/dist/gateway/dispatcher.js +7 -3
  17. package/dist/gateway/policy-resolver.d.ts +10 -6
  18. package/dist/gateway/types.d.ts +2 -1
  19. package/dist/gateway-control.d.ts +8 -1
  20. package/dist/gateway-control.js +171 -18
  21. package/dist/provision.js +7 -1
  22. package/package.json +2 -1
  23. package/src/__tests__/cross-room.test.ts +2 -0
  24. package/src/__tests__/gateway-control.test.ts +84 -0
  25. package/src/__tests__/policy-updated-handler.test.ts +5 -7
  26. package/src/__tests__/third-party-gateway.test.ts +28 -0
  27. package/src/config.ts +6 -3
  28. package/src/cross-room.ts +3 -1
  29. package/src/daemon-config-map.ts +3 -0
  30. package/src/daemon.ts +24 -3
  31. package/src/gateway/__tests__/botcord-channel.test.ts +77 -0
  32. package/src/gateway/__tests__/dispatcher.test.ts +14 -4
  33. package/src/gateway/__tests__/feishu-channel.test.ts +306 -0
  34. package/src/gateway/channels/botcord.ts +10 -1
  35. package/src/gateway/channels/feishu-registration.ts +155 -0
  36. package/src/gateway/channels/feishu.ts +554 -0
  37. package/src/gateway/channels/index.ts +6 -0
  38. package/src/gateway/channels/login-session.ts +10 -2
  39. package/src/gateway/dispatcher.ts +7 -3
  40. package/src/gateway/policy-resolver.ts +19 -11
  41. package/src/gateway/types.ts +2 -1
  42. package/src/gateway-control.ts +188 -17
  43. package/src/provision.ts +13 -1
package/dist/config.d.ts CHANGED
@@ -85,7 +85,7 @@ export interface OpenclawDiscoveryConfig {
85
85
  autoProvision?: boolean;
86
86
  }
87
87
  /** Third-party messaging provider supported by the daemon's channel factory. */
88
- export type ThirdPartyGatewayType = "telegram" | "wechat";
88
+ export type ThirdPartyGatewayType = "telegram" | "wechat" | "feishu";
89
89
  /**
90
90
  * One third-party gateway profile bound to a BotCord agent. `id` is the
91
91
  * channel id (typically `gw_...` minted by the Hub); `accountId` is the
@@ -106,6 +106,9 @@ export interface ThirdPartyGatewayProfile {
106
106
  allowedChatIds?: string[];
107
107
  splitAt?: number;
108
108
  baseUrl?: string;
109
+ appId?: string;
110
+ domain?: "feishu" | "lark";
111
+ userOpenId?: string;
109
112
  }
110
113
  export interface DaemonConfig {
111
114
  /**
package/dist/config.js CHANGED
@@ -216,8 +216,8 @@ export function loadConfig() {
216
216
  if (typeof gg.id !== "string" || gg.id.length === 0) {
217
217
  throw new Error(`daemon config thirdPartyGateways[${i}].id must be a non-empty string (${CONFIG_PATH})`);
218
218
  }
219
- if (gg.type !== "telegram" && gg.type !== "wechat") {
220
- throw new Error(`daemon config thirdPartyGateways[${i}].type must be "telegram" or "wechat" (${CONFIG_PATH})`);
219
+ if (gg.type !== "telegram" && gg.type !== "wechat" && gg.type !== "feishu") {
220
+ throw new Error(`daemon config thirdPartyGateways[${i}].type must be "telegram", "wechat", or "feishu" (${CONFIG_PATH})`);
221
221
  }
222
222
  if (typeof gg.accountId !== "string" || gg.accountId.length === 0) {
223
223
  throw new Error(`daemon config thirdPartyGateways[${i}].accountId must be a non-empty string (${CONFIG_PATH})`);
@@ -10,7 +10,9 @@ export function buildCrossRoomDigest(opts) {
10
10
  const total = entries.length + 1; // +1 for the current turn's room
11
11
  const lines = [
12
12
  "[BotCord Cross-Room Awareness]",
13
- `You are currently active in ${total} BotCord sessions. Recent activity from other rooms:`,
13
+ `You are currently active in ${total} BotCord sessions. The entries below are latest messages from OTHER rooms, not the current room.`,
14
+ "Do not treat any sender or message below as the current user or current conversation.",
15
+ "Recent activity from other rooms:",
14
16
  ];
15
17
  for (const e of slice) {
16
18
  lines.push(formatEntry(e));
@@ -193,6 +193,12 @@ export function toGatewayConfig(cfg, opts = {}) {
193
193
  ch.splitAt = g.splitAt;
194
194
  if (g.baseUrl !== undefined)
195
195
  ch.baseUrl = g.baseUrl;
196
+ if (g.appId !== undefined)
197
+ ch.appId = g.appId;
198
+ if (g.domain !== undefined)
199
+ ch.domain = g.domain;
200
+ if (g.userOpenId !== undefined)
201
+ ch.userOpenId = g.userOpenId;
196
202
  channels.push(ch);
197
203
  }
198
204
  // DaemonConfig's typed surface doesn't carry `trustLevel`, but we read it
package/dist/daemon.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { CONTROL_FRAME_TYPES, shouldWake, } from "@botcord/protocol-core";
2
- import { Gateway, createBotCordChannel, createTelegramChannel, createWechatChannel, resolveTranscriptEnabled, sanitizeUntrustedContent, } from "./gateway/index.js";
2
+ import { Gateway, createBotCordChannel, createFeishuChannel, createTelegramChannel, createWechatChannel, resolveTranscriptEnabled, sanitizeUntrustedContent, } from "./gateway/index.js";
3
3
  import { ActivityTracker } from "./activity-tracker.js";
4
4
  import { SESSIONS_PATH, SNAPSHOT_PATH } from "./config.js";
5
5
  import { resolveBootAgents } from "./agent-discovery.js";
@@ -118,6 +118,23 @@ export function createDaemonChannel(chCfg, deps) {
118
118
  ...(typeof chCfg.secretFile === "string" ? { secretFile: chCfg.secretFile } : {}),
119
119
  ...(typeof chCfg.stateFile === "string" ? { stateFile: chCfg.stateFile } : {}),
120
120
  });
121
+ case "feishu":
122
+ return createFeishuChannel({
123
+ id: chCfg.id,
124
+ accountId: chCfg.accountId,
125
+ ...(typeof chCfg.appId === "string" ? { appId: chCfg.appId } : {}),
126
+ ...(chCfg.domain === "feishu" || chCfg.domain === "lark"
127
+ ? { domain: chCfg.domain }
128
+ : {}),
129
+ ...(Array.isArray(chCfg.allowedSenderIds)
130
+ ? { allowedSenderIds: chCfg.allowedSenderIds }
131
+ : {}),
132
+ ...(Array.isArray(chCfg.allowedChatIds)
133
+ ? { allowedChatIds: chCfg.allowedChatIds }
134
+ : {}),
135
+ ...(typeof chCfg.splitAt === "number" ? { splitAt: chCfg.splitAt } : {}),
136
+ ...(typeof chCfg.secretFile === "string" ? { secretFile: chCfg.secretFile } : {}),
137
+ });
121
138
  default:
122
139
  throw new Error(`unknown channel type "${chCfg.type}"`);
123
140
  }
@@ -296,6 +313,9 @@ export async function startDaemon(opts) {
296
313
  // effective policy, then defer to the protocol-core `shouldWake` decision.
297
314
  const attentionGate = async (msg) => {
298
315
  const policy = await policyResolver.resolve(msg.accountId, msg.conversation.id);
316
+ if (policy.mode === "allowed_senders") {
317
+ return (policy.allowedSenderIds ?? []).includes(msg.sender.id);
318
+ }
299
319
  const localMention = scanMention(msg.text, {
300
320
  agentId: msg.accountId,
301
321
  displayName: displayNameByAgent.get(msg.accountId),
@@ -23,6 +23,13 @@ export interface BotCordChannelClient {
23
23
  hub_msg_id?: string;
24
24
  message_id?: string;
25
25
  } & Record<string, unknown>>;
26
+ sendTypedMessage?(to: string, type: "result" | "error", text: string, options?: {
27
+ replyTo?: string;
28
+ topic?: string;
29
+ }): Promise<{
30
+ hub_msg_id?: string;
31
+ message_id?: string;
32
+ } & Record<string, unknown>>;
26
33
  getHubUrl(): string;
27
34
  onTokenRefresh?: (token: string, expiresAt: number) => void;
28
35
  }
@@ -674,7 +674,9 @@ export function createBotCordChannel(options) {
674
674
  options.replyTo = message.replyTo;
675
675
  if (message.threadId)
676
676
  options.topic = message.threadId;
677
- const resp = await client.sendMessage(message.conversationId, message.text, options);
677
+ const resp = message.type === "error" && client.sendTypedMessage
678
+ ? await client.sendTypedMessage(message.conversationId, "error", message.text, options)
679
+ : await client.sendMessage(message.conversationId, message.text, options);
678
680
  const providerMessageId = (resp && typeof resp.hub_msg_id === "string" && resp.hub_msg_id) ||
679
681
  (resp && typeof resp.message_id === "string"
680
682
  ? resp.message_id
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Feishu/Lark PersonalAgent app registration helpers.
3
+ *
4
+ * Mirrors the flow used by `@larksuite/openclaw-lark-tools`:
5
+ * POST /oauth/v1/app/registration action=init
6
+ * POST /oauth/v1/app/registration action=begin
7
+ * POST /oauth/v1/app/registration action=poll
8
+ */
9
+ import type { FetchLike } from "./http-types.js";
10
+ export type FeishuDomain = "feishu" | "lark";
11
+ export interface FeishuRegistrationOptions {
12
+ domain?: FeishuDomain;
13
+ fetchImpl?: FetchLike;
14
+ }
15
+ export interface FeishuRegistrationStart {
16
+ deviceCode: string;
17
+ verificationUriComplete: string;
18
+ verificationUri?: string;
19
+ expiresIn: number;
20
+ interval: number;
21
+ domain: FeishuDomain;
22
+ raw: Record<string, unknown>;
23
+ }
24
+ export interface FeishuRegistrationPoll {
25
+ status: "pending" | "confirmed" | "expired" | "denied" | "failed";
26
+ appId?: string;
27
+ appSecret?: string;
28
+ userOpenId?: string;
29
+ domain: FeishuDomain;
30
+ interval?: number;
31
+ error?: string;
32
+ raw: Record<string, unknown>;
33
+ }
34
+ export declare function startFeishuRegistration(opts?: FeishuRegistrationOptions): Promise<FeishuRegistrationStart>;
35
+ export declare function pollFeishuRegistration(deviceCode: string, opts?: FeishuRegistrationOptions): Promise<FeishuRegistrationPoll>;
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Feishu/Lark PersonalAgent app registration helpers.
3
+ *
4
+ * Mirrors the flow used by `@larksuite/openclaw-lark-tools`:
5
+ * POST /oauth/v1/app/registration action=init
6
+ * POST /oauth/v1/app/registration action=begin
7
+ * POST /oauth/v1/app/registration action=poll
8
+ */
9
+ const FEISHU_ACCOUNTS_BASE = "https://accounts.feishu.cn";
10
+ const LARK_ACCOUNTS_BASE = "https://accounts.larksuite.com";
11
+ function baseForDomain(domain) {
12
+ return domain === "lark" ? LARK_ACCOUNTS_BASE : FEISHU_ACCOUNTS_BASE;
13
+ }
14
+ function fetcher(opts) {
15
+ return opts.fetchImpl ?? globalThis.fetch;
16
+ }
17
+ async function postRegistration(action, params, opts) {
18
+ const domain = opts.domain ?? "feishu";
19
+ const res = await fetcher(opts)(`${baseForDomain(domain)}/oauth/v1/app/registration`, {
20
+ method: "POST",
21
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
22
+ body: new URLSearchParams({ action, ...params }).toString(),
23
+ });
24
+ const raw = await res.text();
25
+ if (!raw)
26
+ return {};
27
+ try {
28
+ return JSON.parse(raw);
29
+ }
30
+ catch {
31
+ throw new Error(`feishu registration ${action}: non-json response`);
32
+ }
33
+ }
34
+ export async function startFeishuRegistration(opts = {}) {
35
+ const domain = opts.domain ?? "feishu";
36
+ const init = await postRegistration("init", {}, { ...opts, domain });
37
+ const methods = Array.isArray(init.supported_auth_methods)
38
+ ? init.supported_auth_methods.map(String)
39
+ : [];
40
+ if (methods.length > 0 && !methods.includes("client_secret")) {
41
+ throw new Error("feishu registration: client_secret auth is not supported");
42
+ }
43
+ const begin = await postRegistration("begin", {
44
+ archetype: "PersonalAgent",
45
+ auth_method: "client_secret",
46
+ request_user_info: "open_id",
47
+ }, { ...opts, domain });
48
+ const deviceCode = typeof begin.device_code === "string" ? begin.device_code : "";
49
+ const verificationUriComplete = typeof begin.verification_uri_complete === "string"
50
+ ? begin.verification_uri_complete
51
+ : "";
52
+ if (!deviceCode || !verificationUriComplete) {
53
+ throw new Error("feishu registration: missing device_code or verification_uri_complete");
54
+ }
55
+ return {
56
+ deviceCode,
57
+ verificationUriComplete,
58
+ ...(typeof begin.verification_uri === "string"
59
+ ? { verificationUri: begin.verification_uri }
60
+ : {}),
61
+ expiresIn: typeof begin.expire_in === "number" ? begin.expire_in : 600,
62
+ interval: typeof begin.interval === "number" ? begin.interval : 5,
63
+ domain,
64
+ raw: begin,
65
+ };
66
+ }
67
+ export async function pollFeishuRegistration(deviceCode, opts = {}) {
68
+ const domain = opts.domain ?? "feishu";
69
+ const data = await postRegistration("poll", { device_code: deviceCode }, { ...opts, domain });
70
+ const tenantBrand = typeof data.user_info?.tenant_brand === "string"
71
+ ? String(data.user_info.tenant_brand)
72
+ : "";
73
+ const resolvedDomain = tenantBrand === "lark" ? "lark" : domain;
74
+ const appId = typeof data.client_id === "string" ? data.client_id : undefined;
75
+ const appSecret = typeof data.client_secret === "string" ? data.client_secret : undefined;
76
+ if (appId && appSecret) {
77
+ const userInfo = data.user_info;
78
+ return {
79
+ status: "confirmed",
80
+ appId,
81
+ appSecret,
82
+ userOpenId: typeof userInfo?.open_id === "string" ? userInfo.open_id : undefined,
83
+ domain: resolvedDomain,
84
+ raw: data,
85
+ };
86
+ }
87
+ const error = typeof data.error === "string" ? data.error : "";
88
+ if (!error || error === "authorization_pending") {
89
+ return { status: "pending", domain: resolvedDomain, raw: data };
90
+ }
91
+ if (error === "slow_down") {
92
+ return { status: "pending", domain: resolvedDomain, interval: 10, raw: data };
93
+ }
94
+ if (error === "access_denied") {
95
+ return { status: "denied", domain: resolvedDomain, error, raw: data };
96
+ }
97
+ if (error === "expired_token" || error === "invalid_grant") {
98
+ return { status: "expired", domain: resolvedDomain, error, raw: data };
99
+ }
100
+ return { status: "failed", domain: resolvedDomain, error: error || "unknown", raw: data };
101
+ }
@@ -0,0 +1,16 @@
1
+ import type { ChannelAdapter } from "../types.js";
2
+ import type { FeishuDomain } from "./feishu-registration.js";
3
+ export interface FeishuChannelOptions {
4
+ id: string;
5
+ accountId: string;
6
+ appId?: string;
7
+ appSecret?: string;
8
+ domain?: FeishuDomain;
9
+ allowedSenderIds?: string[];
10
+ allowedChatIds?: string[];
11
+ splitAt?: number;
12
+ secretFile?: string;
13
+ stateFile?: string;
14
+ stateDebounceMs?: number;
15
+ }
16
+ export declare function createFeishuChannel(opts: FeishuChannelOptions): ChannelAdapter;