@m1heng-clawd/feishu 0.1.7 → 0.1.9
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/README.md +180 -8
- package/package.json +3 -3
- package/src/accounts.ts +100 -13
- package/src/bot.ts +124 -28
- package/src/channel.ts +163 -53
- package/src/client.ts +83 -35
- package/src/config-schema.ts +73 -1
- package/src/directory.ts +15 -9
- package/src/docx.ts +16 -6
- package/src/drive.ts +12 -6
- package/src/dynamic-agent.ts +131 -0
- package/src/media.ts +43 -35
- package/src/monitor.ts +215 -70
- package/src/outbound.ts +7 -7
- package/src/perm.ts +12 -6
- package/src/probe.ts +6 -9
- package/src/reactions.ts +19 -16
- package/src/reply-dispatcher.ts +19 -11
- package/src/send.ts +49 -43
- package/src/types.ts +16 -2
- package/src/typing.ts +11 -9
- package/src/wiki.ts +12 -6
package/src/client.ts
CHANGED
|
@@ -1,66 +1,114 @@
|
|
|
1
1
|
import * as Lark from "@larksuiteoapi/node-sdk";
|
|
2
|
-
import type {
|
|
3
|
-
import { resolveFeishuCredentials } from "./accounts.js";
|
|
2
|
+
import type { FeishuDomain, ResolvedFeishuAccount } from "./types.js";
|
|
4
3
|
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
// Multi-account client cache
|
|
5
|
+
const clientCache = new Map<
|
|
6
|
+
string,
|
|
7
|
+
{
|
|
8
|
+
client: Lark.Client;
|
|
9
|
+
config: { appId: string; appSecret: string; domain?: FeishuDomain };
|
|
10
|
+
}
|
|
11
|
+
>();
|
|
7
12
|
|
|
8
|
-
function resolveDomain(domain: FeishuDomain) {
|
|
9
|
-
|
|
13
|
+
function resolveDomain(domain: FeishuDomain | undefined): Lark.Domain | string {
|
|
14
|
+
if (domain === "lark") return Lark.Domain.Lark;
|
|
15
|
+
if (domain === "feishu" || !domain) return Lark.Domain.Feishu;
|
|
16
|
+
return domain.replace(/\/+$/, ""); // Custom URL for private deployment
|
|
10
17
|
}
|
|
11
18
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Credentials needed to create a Feishu client.
|
|
21
|
+
* Both FeishuConfig and ResolvedFeishuAccount satisfy this interface.
|
|
22
|
+
*/
|
|
23
|
+
export type FeishuClientCredentials = {
|
|
24
|
+
accountId?: string;
|
|
25
|
+
appId?: string;
|
|
26
|
+
appSecret?: string;
|
|
27
|
+
domain?: FeishuDomain;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Create or get a cached Feishu client for an account.
|
|
32
|
+
* Accepts any object with appId, appSecret, and optional domain/accountId.
|
|
33
|
+
*/
|
|
34
|
+
export function createFeishuClient(creds: FeishuClientCredentials): Lark.Client {
|
|
35
|
+
const { accountId = "default", appId, appSecret, domain } = creds;
|
|
36
|
+
|
|
37
|
+
if (!appId || !appSecret) {
|
|
38
|
+
throw new Error(`Feishu credentials not configured for account "${accountId}"`);
|
|
16
39
|
}
|
|
17
40
|
|
|
41
|
+
// Check cache
|
|
42
|
+
const cached = clientCache.get(accountId);
|
|
18
43
|
if (
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
cachedConfig.domain === creds.domain
|
|
44
|
+
cached &&
|
|
45
|
+
cached.config.appId === appId &&
|
|
46
|
+
cached.config.appSecret === appSecret &&
|
|
47
|
+
cached.config.domain === domain
|
|
24
48
|
) {
|
|
25
|
-
return
|
|
49
|
+
return cached.client;
|
|
26
50
|
}
|
|
27
51
|
|
|
52
|
+
// Create new client
|
|
28
53
|
const client = new Lark.Client({
|
|
29
|
-
appId
|
|
30
|
-
appSecret
|
|
54
|
+
appId,
|
|
55
|
+
appSecret,
|
|
31
56
|
appType: Lark.AppType.SelfBuild,
|
|
32
|
-
domain: resolveDomain(
|
|
57
|
+
domain: resolveDomain(domain),
|
|
33
58
|
});
|
|
34
59
|
|
|
35
|
-
|
|
36
|
-
|
|
60
|
+
// Cache it
|
|
61
|
+
clientCache.set(accountId, {
|
|
62
|
+
client,
|
|
63
|
+
config: { appId, appSecret, domain },
|
|
64
|
+
});
|
|
37
65
|
|
|
38
66
|
return client;
|
|
39
67
|
}
|
|
40
68
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Create a Feishu WebSocket client for an account.
|
|
71
|
+
* Note: WSClient is not cached since each call creates a new connection.
|
|
72
|
+
*/
|
|
73
|
+
export function createFeishuWSClient(account: ResolvedFeishuAccount): Lark.WSClient {
|
|
74
|
+
const { accountId, appId, appSecret, domain } = account;
|
|
75
|
+
|
|
76
|
+
if (!appId || !appSecret) {
|
|
77
|
+
throw new Error(`Feishu credentials not configured for account "${accountId}"`);
|
|
45
78
|
}
|
|
46
79
|
|
|
47
80
|
return new Lark.WSClient({
|
|
48
|
-
appId
|
|
49
|
-
appSecret
|
|
50
|
-
domain: resolveDomain(
|
|
81
|
+
appId,
|
|
82
|
+
appSecret,
|
|
83
|
+
domain: resolveDomain(domain),
|
|
51
84
|
loggerLevel: Lark.LoggerLevel.info,
|
|
52
85
|
});
|
|
53
86
|
}
|
|
54
87
|
|
|
55
|
-
|
|
56
|
-
|
|
88
|
+
/**
|
|
89
|
+
* Create an event dispatcher for an account.
|
|
90
|
+
*/
|
|
91
|
+
export function createEventDispatcher(account: ResolvedFeishuAccount): Lark.EventDispatcher {
|
|
57
92
|
return new Lark.EventDispatcher({
|
|
58
|
-
encryptKey:
|
|
59
|
-
verificationToken:
|
|
93
|
+
encryptKey: account.encryptKey,
|
|
94
|
+
verificationToken: account.verificationToken,
|
|
60
95
|
});
|
|
61
96
|
}
|
|
62
97
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
98
|
+
/**
|
|
99
|
+
* Get a cached client for an account (if exists).
|
|
100
|
+
*/
|
|
101
|
+
export function getFeishuClient(accountId: string): Lark.Client | null {
|
|
102
|
+
return clientCache.get(accountId)?.client ?? null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Clear client cache for a specific account or all accounts.
|
|
107
|
+
*/
|
|
108
|
+
export function clearClientCache(accountId?: string): void {
|
|
109
|
+
if (accountId) {
|
|
110
|
+
clientCache.delete(accountId);
|
|
111
|
+
} else {
|
|
112
|
+
clientCache.clear();
|
|
113
|
+
}
|
|
66
114
|
}
|
package/src/config-schema.ts
CHANGED
|
@@ -3,7 +3,10 @@ export { z };
|
|
|
3
3
|
|
|
4
4
|
const DmPolicySchema = z.enum(["open", "pairing", "allowlist"]);
|
|
5
5
|
const GroupPolicySchema = z.enum(["open", "allowlist", "disabled"]);
|
|
6
|
-
const FeishuDomainSchema = z.
|
|
6
|
+
const FeishuDomainSchema = z.union([
|
|
7
|
+
z.enum(["feishu", "lark"]),
|
|
8
|
+
z.string().url().startsWith("https://"),
|
|
9
|
+
]);
|
|
7
10
|
const FeishuConnectionModeSchema = z.enum(["websocket", "webhook"]);
|
|
8
11
|
|
|
9
12
|
const ToolPolicySchema = z
|
|
@@ -50,6 +53,20 @@ const ChannelHeartbeatVisibilitySchema = z
|
|
|
50
53
|
.strict()
|
|
51
54
|
.optional();
|
|
52
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Dynamic agent creation configuration.
|
|
58
|
+
* When enabled, a new agent is created for each unique DM user.
|
|
59
|
+
*/
|
|
60
|
+
const DynamicAgentCreationSchema = z
|
|
61
|
+
.object({
|
|
62
|
+
enabled: z.boolean().optional(),
|
|
63
|
+
workspaceTemplate: z.string().optional(),
|
|
64
|
+
agentDirTemplate: z.string().optional(),
|
|
65
|
+
maxAgents: z.number().int().positive().optional(),
|
|
66
|
+
})
|
|
67
|
+
.strict()
|
|
68
|
+
.optional();
|
|
69
|
+
|
|
53
70
|
/**
|
|
54
71
|
* Feishu tools configuration.
|
|
55
72
|
* Controls which tool categories are enabled.
|
|
@@ -69,6 +86,16 @@ const FeishuToolsConfigSchema = z
|
|
|
69
86
|
.strict()
|
|
70
87
|
.optional();
|
|
71
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Topic session isolation mode for group chats.
|
|
91
|
+
* - "disabled" (default): All messages in a group share one session
|
|
92
|
+
* - "enabled": Messages in different topics get separate sessions
|
|
93
|
+
*
|
|
94
|
+
* When enabled, the session key becomes `chat:{chatId}:topic:{rootId}`
|
|
95
|
+
* for messages within a topic thread, allowing isolated conversations.
|
|
96
|
+
*/
|
|
97
|
+
const TopicSessionModeSchema = z.enum(["disabled", "enabled"]).optional();
|
|
98
|
+
|
|
72
99
|
export const FeishuGroupSchema = z
|
|
73
100
|
.object({
|
|
74
101
|
requireMention: z.boolean().optional(),
|
|
@@ -77,12 +104,52 @@ export const FeishuGroupSchema = z
|
|
|
77
104
|
enabled: z.boolean().optional(),
|
|
78
105
|
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
|
79
106
|
systemPrompt: z.string().optional(),
|
|
107
|
+
topicSessionMode: TopicSessionModeSchema,
|
|
108
|
+
})
|
|
109
|
+
.strict();
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Per-account configuration.
|
|
113
|
+
* All fields are optional - missing fields inherit from top-level config.
|
|
114
|
+
*/
|
|
115
|
+
export const FeishuAccountConfigSchema = z
|
|
116
|
+
.object({
|
|
117
|
+
enabled: z.boolean().optional(),
|
|
118
|
+
name: z.string().optional(), // Display name for this account
|
|
119
|
+
appId: z.string().optional(),
|
|
120
|
+
appSecret: z.string().optional(),
|
|
121
|
+
encryptKey: z.string().optional(),
|
|
122
|
+
verificationToken: z.string().optional(),
|
|
123
|
+
domain: FeishuDomainSchema.optional(),
|
|
124
|
+
connectionMode: FeishuConnectionModeSchema.optional(),
|
|
125
|
+
webhookPath: z.string().optional(),
|
|
126
|
+
webhookPort: z.number().int().positive().optional(),
|
|
127
|
+
capabilities: z.array(z.string()).optional(),
|
|
128
|
+
markdown: MarkdownConfigSchema,
|
|
129
|
+
configWrites: z.boolean().optional(),
|
|
130
|
+
dmPolicy: DmPolicySchema.optional(),
|
|
131
|
+
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
|
132
|
+
groupPolicy: GroupPolicySchema.optional(),
|
|
133
|
+
groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
|
134
|
+
requireMention: z.boolean().optional(),
|
|
135
|
+
groups: z.record(z.string(), FeishuGroupSchema.optional()).optional(),
|
|
136
|
+
historyLimit: z.number().int().min(0).optional(),
|
|
137
|
+
dmHistoryLimit: z.number().int().min(0).optional(),
|
|
138
|
+
dms: z.record(z.string(), DmConfigSchema).optional(),
|
|
139
|
+
textChunkLimit: z.number().int().positive().optional(),
|
|
140
|
+
chunkMode: z.enum(["length", "newline"]).optional(),
|
|
141
|
+
blockStreamingCoalesce: BlockStreamingCoalesceSchema,
|
|
142
|
+
mediaMaxMb: z.number().positive().optional(),
|
|
143
|
+
heartbeat: ChannelHeartbeatVisibilitySchema,
|
|
144
|
+
renderMode: RenderModeSchema,
|
|
145
|
+
tools: FeishuToolsConfigSchema,
|
|
80
146
|
})
|
|
81
147
|
.strict();
|
|
82
148
|
|
|
83
149
|
export const FeishuConfigSchema = z
|
|
84
150
|
.object({
|
|
85
151
|
enabled: z.boolean().optional(),
|
|
152
|
+
// Top-level credentials (backward compatible for single-account mode)
|
|
86
153
|
appId: z.string().optional(),
|
|
87
154
|
appSecret: z.string().optional(),
|
|
88
155
|
encryptKey: z.string().optional(),
|
|
@@ -100,6 +167,7 @@ export const FeishuConfigSchema = z
|
|
|
100
167
|
groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
|
101
168
|
requireMention: z.boolean().optional().default(true),
|
|
102
169
|
groups: z.record(z.string(), FeishuGroupSchema.optional()).optional(),
|
|
170
|
+
topicSessionMode: TopicSessionModeSchema,
|
|
103
171
|
historyLimit: z.number().int().min(0).optional(),
|
|
104
172
|
dmHistoryLimit: z.number().int().min(0).optional(),
|
|
105
173
|
dms: z.record(z.string(), DmConfigSchema).optional(),
|
|
@@ -110,6 +178,10 @@ export const FeishuConfigSchema = z
|
|
|
110
178
|
heartbeat: ChannelHeartbeatVisibilitySchema,
|
|
111
179
|
renderMode: RenderModeSchema, // raw = plain text (default), card = interactive card with markdown
|
|
112
180
|
tools: FeishuToolsConfigSchema,
|
|
181
|
+
// Dynamic agent creation for DM users
|
|
182
|
+
dynamicAgentCreation: DynamicAgentCreationSchema,
|
|
183
|
+
// Multi-account configuration
|
|
184
|
+
accounts: z.record(z.string(), FeishuAccountConfigSchema.optional()).optional(),
|
|
113
185
|
})
|
|
114
186
|
.strict()
|
|
115
187
|
.superRefine((value, ctx) => {
|
package/src/directory.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ClawdbotConfig } from "openclaw/plugin-sdk";
|
|
2
|
-
import type { FeishuConfig } from "./types.js";
|
|
3
2
|
import { createFeishuClient } from "./client.js";
|
|
3
|
+
import { resolveFeishuAccount } from "./accounts.js";
|
|
4
4
|
import { normalizeFeishuTarget } from "./targets.js";
|
|
5
5
|
|
|
6
6
|
export type FeishuDirectoryPeer = {
|
|
@@ -19,8 +19,10 @@ export async function listFeishuDirectoryPeers(params: {
|
|
|
19
19
|
cfg: ClawdbotConfig;
|
|
20
20
|
query?: string;
|
|
21
21
|
limit?: number;
|
|
22
|
+
accountId?: string;
|
|
22
23
|
}): Promise<FeishuDirectoryPeer[]> {
|
|
23
|
-
const
|
|
24
|
+
const account = resolveFeishuAccount({ cfg: params.cfg, accountId: params.accountId });
|
|
25
|
+
const feishuCfg = account.config;
|
|
24
26
|
const q = params.query?.trim().toLowerCase() || "";
|
|
25
27
|
const ids = new Set<string>();
|
|
26
28
|
|
|
@@ -47,8 +49,10 @@ export async function listFeishuDirectoryGroups(params: {
|
|
|
47
49
|
cfg: ClawdbotConfig;
|
|
48
50
|
query?: string;
|
|
49
51
|
limit?: number;
|
|
52
|
+
accountId?: string;
|
|
50
53
|
}): Promise<FeishuDirectoryGroup[]> {
|
|
51
|
-
const
|
|
54
|
+
const account = resolveFeishuAccount({ cfg: params.cfg, accountId: params.accountId });
|
|
55
|
+
const feishuCfg = account.config;
|
|
52
56
|
const q = params.query?.trim().toLowerCase() || "";
|
|
53
57
|
const ids = new Set<string>();
|
|
54
58
|
|
|
@@ -74,14 +78,15 @@ export async function listFeishuDirectoryPeersLive(params: {
|
|
|
74
78
|
cfg: ClawdbotConfig;
|
|
75
79
|
query?: string;
|
|
76
80
|
limit?: number;
|
|
81
|
+
accountId?: string;
|
|
77
82
|
}): Promise<FeishuDirectoryPeer[]> {
|
|
78
|
-
const
|
|
79
|
-
if (!
|
|
83
|
+
const account = resolveFeishuAccount({ cfg: params.cfg, accountId: params.accountId });
|
|
84
|
+
if (!account.configured) {
|
|
80
85
|
return listFeishuDirectoryPeers(params);
|
|
81
86
|
}
|
|
82
87
|
|
|
83
88
|
try {
|
|
84
|
-
const client = createFeishuClient(
|
|
89
|
+
const client = createFeishuClient(account);
|
|
85
90
|
const peers: FeishuDirectoryPeer[] = [];
|
|
86
91
|
const limit = params.limit ?? 50;
|
|
87
92
|
|
|
@@ -118,14 +123,15 @@ export async function listFeishuDirectoryGroupsLive(params: {
|
|
|
118
123
|
cfg: ClawdbotConfig;
|
|
119
124
|
query?: string;
|
|
120
125
|
limit?: number;
|
|
126
|
+
accountId?: string;
|
|
121
127
|
}): Promise<FeishuDirectoryGroup[]> {
|
|
122
|
-
const
|
|
123
|
-
if (!
|
|
128
|
+
const account = resolveFeishuAccount({ cfg: params.cfg, accountId: params.accountId });
|
|
129
|
+
if (!account.configured) {
|
|
124
130
|
return listFeishuDirectoryGroups(params);
|
|
125
131
|
}
|
|
126
132
|
|
|
127
133
|
try {
|
|
128
|
-
const client = createFeishuClient(
|
|
134
|
+
const client = createFeishuClient(account);
|
|
129
135
|
const groups: FeishuDirectoryGroup[] = [];
|
|
130
136
|
const limit = params.limit ?? 50;
|
|
131
137
|
|
package/src/docx.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
3
3
|
import { createFeishuClient } from "./client.js";
|
|
4
|
-
import
|
|
4
|
+
import { resolveFeishuAccount, listEnabledFeishuAccounts } from "./accounts.js";
|
|
5
5
|
import type * as Lark from "@larksuiteoapi/node-sdk";
|
|
6
6
|
import { Readable } from "stream";
|
|
7
7
|
import { FeishuDocSchema, type FeishuDocParams } from "./doc-schema.js";
|
|
@@ -388,14 +388,24 @@ async function listAppScopes(client: Lark.Client) {
|
|
|
388
388
|
// ============ Tool Registration ============
|
|
389
389
|
|
|
390
390
|
export function registerFeishuDocTools(api: OpenClawPluginApi) {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
api.logger.debug?.("feishu_doc: Feishu credentials not configured, skipping doc tools");
|
|
391
|
+
if (!api.config) {
|
|
392
|
+
api.logger.debug?.("feishu_doc: No config available, skipping doc tools");
|
|
394
393
|
return;
|
|
395
394
|
}
|
|
396
395
|
|
|
397
|
-
|
|
398
|
-
const
|
|
396
|
+
// Check if any account is configured
|
|
397
|
+
const accounts = listEnabledFeishuAccounts(api.config);
|
|
398
|
+
if (accounts.length === 0) {
|
|
399
|
+
api.logger.debug?.("feishu_doc: No Feishu accounts configured, skipping doc tools");
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Use first account's config for tools configuration
|
|
404
|
+
const firstAccount = accounts[0];
|
|
405
|
+
const toolsCfg = resolveToolsConfig(firstAccount.config.tools);
|
|
406
|
+
|
|
407
|
+
// Helper to get client for the default account
|
|
408
|
+
const getClient = () => createFeishuClient(firstAccount);
|
|
399
409
|
const registered: string[] = [];
|
|
400
410
|
|
|
401
411
|
// Main document tool with action-based dispatch
|
package/src/drive.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
2
|
import { createFeishuClient } from "./client.js";
|
|
3
|
-
import
|
|
3
|
+
import { listEnabledFeishuAccounts } from "./accounts.js";
|
|
4
4
|
import type * as Lark from "@larksuiteoapi/node-sdk";
|
|
5
5
|
import { FeishuDriveSchema, type FeishuDriveParams } from "./drive-schema.js";
|
|
6
6
|
import { resolveToolsConfig } from "./tools-config.js";
|
|
@@ -150,19 +150,25 @@ async function deleteFile(client: Lark.Client, fileToken: string, type: string)
|
|
|
150
150
|
// ============ Tool Registration ============
|
|
151
151
|
|
|
152
152
|
export function registerFeishuDriveTools(api: OpenClawPluginApi) {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
api.logger.debug?.("feishu_drive: Feishu credentials not configured, skipping drive tools");
|
|
153
|
+
if (!api.config) {
|
|
154
|
+
api.logger.debug?.("feishu_drive: No config available, skipping drive tools");
|
|
156
155
|
return;
|
|
157
156
|
}
|
|
158
157
|
|
|
159
|
-
const
|
|
158
|
+
const accounts = listEnabledFeishuAccounts(api.config);
|
|
159
|
+
if (accounts.length === 0) {
|
|
160
|
+
api.logger.debug?.("feishu_drive: No Feishu accounts configured, skipping drive tools");
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const firstAccount = accounts[0];
|
|
165
|
+
const toolsCfg = resolveToolsConfig(firstAccount.config.tools);
|
|
160
166
|
if (!toolsCfg.drive) {
|
|
161
167
|
api.logger.debug?.("feishu_drive: drive tool disabled in config");
|
|
162
168
|
return;
|
|
163
169
|
}
|
|
164
170
|
|
|
165
|
-
const getClient = () => createFeishuClient(
|
|
171
|
+
const getClient = () => createFeishuClient(firstAccount);
|
|
166
172
|
|
|
167
173
|
api.registerTool(
|
|
168
174
|
{
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
|
|
5
|
+
import type { DynamicAgentCreationConfig } from "./types.js";
|
|
6
|
+
|
|
7
|
+
export type MaybeCreateDynamicAgentResult = {
|
|
8
|
+
created: boolean;
|
|
9
|
+
updatedCfg: OpenClawConfig;
|
|
10
|
+
agentId?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Check if a dynamic agent should be created for a DM user and create it if needed.
|
|
15
|
+
* This creates a unique agent instance with its own workspace for each DM user.
|
|
16
|
+
*/
|
|
17
|
+
export async function maybeCreateDynamicAgent(params: {
|
|
18
|
+
cfg: OpenClawConfig;
|
|
19
|
+
runtime: PluginRuntime;
|
|
20
|
+
senderOpenId: string;
|
|
21
|
+
dynamicCfg: DynamicAgentCreationConfig;
|
|
22
|
+
log: (msg: string) => void;
|
|
23
|
+
}): Promise<MaybeCreateDynamicAgentResult> {
|
|
24
|
+
const { cfg, runtime, senderOpenId, dynamicCfg, log } = params;
|
|
25
|
+
|
|
26
|
+
// Check if there's already a binding for this user
|
|
27
|
+
const existingBindings = cfg.bindings ?? [];
|
|
28
|
+
const hasBinding = existingBindings.some(
|
|
29
|
+
(b) =>
|
|
30
|
+
b.match?.channel === "feishu" &&
|
|
31
|
+
b.match?.peer?.kind === "dm" &&
|
|
32
|
+
b.match?.peer?.id === senderOpenId,
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
if (hasBinding) {
|
|
36
|
+
return { created: false, updatedCfg: cfg };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Check maxAgents limit if configured
|
|
40
|
+
if (dynamicCfg.maxAgents !== undefined) {
|
|
41
|
+
const feishuAgentCount = (cfg.agents?.list ?? []).filter((a) =>
|
|
42
|
+
a.id.startsWith("feishu-"),
|
|
43
|
+
).length;
|
|
44
|
+
if (feishuAgentCount >= dynamicCfg.maxAgents) {
|
|
45
|
+
log(
|
|
46
|
+
`feishu: maxAgents limit (${dynamicCfg.maxAgents}) reached, not creating agent for ${senderOpenId}`,
|
|
47
|
+
);
|
|
48
|
+
return { created: false, updatedCfg: cfg };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Use full OpenID as agent ID suffix (OpenID format: ou_xxx is already filesystem-safe)
|
|
53
|
+
const agentId = `feishu-${senderOpenId}`;
|
|
54
|
+
|
|
55
|
+
// Check if agent already exists (but binding was missing)
|
|
56
|
+
const existingAgent = (cfg.agents?.list ?? []).find((a) => a.id === agentId);
|
|
57
|
+
if (existingAgent) {
|
|
58
|
+
// Agent exists but binding doesn't - just add the binding
|
|
59
|
+
log(`feishu: agent "${agentId}" exists, adding missing binding for ${senderOpenId}`);
|
|
60
|
+
|
|
61
|
+
const updatedCfg: OpenClawConfig = {
|
|
62
|
+
...cfg,
|
|
63
|
+
bindings: [
|
|
64
|
+
...existingBindings,
|
|
65
|
+
{
|
|
66
|
+
agentId,
|
|
67
|
+
match: {
|
|
68
|
+
channel: "feishu",
|
|
69
|
+
peer: { kind: "dm", id: senderOpenId },
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
await runtime.config.writeConfigFile(updatedCfg);
|
|
76
|
+
return { created: true, updatedCfg, agentId };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Resolve path templates with substitutions
|
|
80
|
+
const workspaceTemplate = dynamicCfg.workspaceTemplate ?? "~/.openclaw/workspace-{agentId}";
|
|
81
|
+
const agentDirTemplate = dynamicCfg.agentDirTemplate ?? "~/.openclaw/agents/{agentId}/agent";
|
|
82
|
+
|
|
83
|
+
const workspace = resolveUserPath(
|
|
84
|
+
workspaceTemplate.replace("{userId}", senderOpenId).replace("{agentId}", agentId),
|
|
85
|
+
);
|
|
86
|
+
const agentDir = resolveUserPath(
|
|
87
|
+
agentDirTemplate.replace("{userId}", senderOpenId).replace("{agentId}", agentId),
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
log(`feishu: creating dynamic agent "${agentId}" for user ${senderOpenId}`);
|
|
91
|
+
log(` workspace: ${workspace}`);
|
|
92
|
+
log(` agentDir: ${agentDir}`);
|
|
93
|
+
|
|
94
|
+
// Create directories
|
|
95
|
+
await fs.promises.mkdir(workspace, { recursive: true });
|
|
96
|
+
await fs.promises.mkdir(agentDir, { recursive: true });
|
|
97
|
+
|
|
98
|
+
// Update configuration with new agent and binding
|
|
99
|
+
const updatedCfg: OpenClawConfig = {
|
|
100
|
+
...cfg,
|
|
101
|
+
agents: {
|
|
102
|
+
...cfg.agents,
|
|
103
|
+
list: [...(cfg.agents?.list ?? []), { id: agentId, workspace, agentDir }],
|
|
104
|
+
},
|
|
105
|
+
bindings: [
|
|
106
|
+
...existingBindings,
|
|
107
|
+
{
|
|
108
|
+
agentId,
|
|
109
|
+
match: {
|
|
110
|
+
channel: "feishu",
|
|
111
|
+
peer: { kind: "dm", id: senderOpenId },
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Write updated config using PluginRuntime API
|
|
118
|
+
await runtime.config.writeConfigFile(updatedCfg);
|
|
119
|
+
|
|
120
|
+
return { created: true, updatedCfg, agentId };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Resolve a path that may start with ~ to the user's home directory.
|
|
125
|
+
*/
|
|
126
|
+
function resolveUserPath(p: string): string {
|
|
127
|
+
if (p.startsWith("~/")) {
|
|
128
|
+
return path.join(os.homedir(), p.slice(2));
|
|
129
|
+
}
|
|
130
|
+
return p;
|
|
131
|
+
}
|