@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.
- package/dist/config.d.ts +4 -1
- package/dist/config.js +2 -2
- package/dist/cross-room.js +3 -1
- package/dist/daemon-config-map.js +6 -0
- package/dist/daemon.js +21 -1
- package/dist/gateway/channels/botcord.d.ts +7 -0
- package/dist/gateway/channels/botcord.js +3 -1
- package/dist/gateway/channels/feishu-registration.d.ts +35 -0
- package/dist/gateway/channels/feishu-registration.js +101 -0
- package/dist/gateway/channels/feishu.d.ts +16 -0
- package/dist/gateway/channels/feishu.js +459 -0
- package/dist/gateway/channels/index.d.ts +2 -0
- package/dist/gateway/channels/index.js +2 -0
- package/dist/gateway/channels/login-session.d.ts +9 -1
- package/dist/gateway/channels/login-session.js +1 -1
- package/dist/gateway/dispatcher.js +7 -3
- package/dist/gateway/policy-resolver.d.ts +10 -6
- package/dist/gateway/types.d.ts +2 -1
- package/dist/gateway-control.d.ts +8 -1
- package/dist/gateway-control.js +171 -18
- package/dist/provision.js +7 -1
- package/package.json +2 -1
- package/src/__tests__/cross-room.test.ts +2 -0
- package/src/__tests__/gateway-control.test.ts +84 -0
- package/src/__tests__/policy-updated-handler.test.ts +5 -7
- package/src/__tests__/third-party-gateway.test.ts +28 -0
- package/src/config.ts +6 -3
- package/src/cross-room.ts +3 -1
- package/src/daemon-config-map.ts +3 -0
- package/src/daemon.ts +24 -3
- package/src/gateway/__tests__/botcord-channel.test.ts +77 -0
- package/src/gateway/__tests__/dispatcher.test.ts +14 -4
- package/src/gateway/__tests__/feishu-channel.test.ts +306 -0
- package/src/gateway/channels/botcord.ts +10 -1
- package/src/gateway/channels/feishu-registration.ts +155 -0
- package/src/gateway/channels/feishu.ts +554 -0
- package/src/gateway/channels/index.ts +6 -0
- package/src/gateway/channels/login-session.ts +10 -2
- package/src/gateway/dispatcher.ts +7 -3
- package/src/gateway/policy-resolver.ts +19 -11
- package/src/gateway/types.ts +2 -1
- package/src/gateway-control.ts +188 -17
- 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 "
|
|
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})`);
|
package/dist/cross-room.js
CHANGED
|
@@ -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.
|
|
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 =
|
|
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;
|