@bobotu/feishu-fork 0.1.0
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/LICENSE +21 -0
- package/README.md +922 -0
- package/index.ts +65 -0
- package/openclaw.plugin.json +10 -0
- package/package.json +72 -0
- package/skills/feishu-doc/SKILL.md +161 -0
- package/skills/feishu-doc/references/block-types.md +102 -0
- package/skills/feishu-drive/SKILL.md +96 -0
- package/skills/feishu-perm/SKILL.md +90 -0
- package/skills/feishu-task/SKILL.md +210 -0
- package/skills/feishu-wiki/SKILL.md +96 -0
- package/src/accounts.ts +140 -0
- package/src/bitable-tools/actions.ts +199 -0
- package/src/bitable-tools/common.ts +90 -0
- package/src/bitable-tools/index.ts +1 -0
- package/src/bitable-tools/meta.ts +80 -0
- package/src/bitable-tools/register.ts +195 -0
- package/src/bitable-tools/schemas.ts +221 -0
- package/src/bot.ts +1125 -0
- package/src/channel.ts +334 -0
- package/src/client.ts +114 -0
- package/src/config-schema.ts +237 -0
- package/src/dedup.ts +54 -0
- package/src/directory.ts +165 -0
- package/src/doc-tools/actions.ts +341 -0
- package/src/doc-tools/common.ts +33 -0
- package/src/doc-tools/index.ts +2 -0
- package/src/doc-tools/register.ts +90 -0
- package/src/doc-tools/schemas.ts +85 -0
- package/src/doc-write-service.ts +711 -0
- package/src/drive-tools/actions.ts +182 -0
- package/src/drive-tools/common.ts +18 -0
- package/src/drive-tools/index.ts +2 -0
- package/src/drive-tools/register.ts +71 -0
- package/src/drive-tools/schemas.ts +67 -0
- package/src/dynamic-agent.ts +135 -0
- package/src/external-keys.ts +19 -0
- package/src/media.ts +510 -0
- package/src/mention.ts +121 -0
- package/src/monitor.ts +323 -0
- package/src/onboarding.ts +449 -0
- package/src/outbound.ts +40 -0
- package/src/perm-tools/actions.ts +111 -0
- package/src/perm-tools/common.ts +18 -0
- package/src/perm-tools/index.ts +2 -0
- package/src/perm-tools/register.ts +65 -0
- package/src/perm-tools/schemas.ts +52 -0
- package/src/policy.ts +117 -0
- package/src/probe.ts +147 -0
- package/src/reactions.ts +160 -0
- package/src/reply-dispatcher.ts +240 -0
- package/src/runtime.ts +14 -0
- package/src/send.ts +391 -0
- package/src/streaming-card.ts +211 -0
- package/src/targets.ts +58 -0
- package/src/task-tools/actions.ts +590 -0
- package/src/task-tools/common.ts +18 -0
- package/src/task-tools/constants.ts +13 -0
- package/src/task-tools/index.ts +1 -0
- package/src/task-tools/register.ts +263 -0
- package/src/task-tools/schemas.ts +567 -0
- package/src/text/markdown-links.ts +104 -0
- package/src/tools-common/feishu-api.ts +184 -0
- package/src/tools-common/tool-context.ts +23 -0
- package/src/tools-common/tool-exec.ts +73 -0
- package/src/tools-config.ts +22 -0
- package/src/types.ts +79 -0
- package/src/typing.ts +75 -0
- package/src/wiki-tools/actions.ts +166 -0
- package/src/wiki-tools/common.ts +18 -0
- package/src/wiki-tools/index.ts +2 -0
- package/src/wiki-tools/register.ts +66 -0
- package/src/wiki-tools/schemas.ts +55 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { createAndWriteDoc } from "../doc-write-service.js";
|
|
2
|
+
import { runDriveApiCall, type DriveClient } from "./common.js";
|
|
3
|
+
import type { FeishuDriveParams } from "./schemas.js";
|
|
4
|
+
|
|
5
|
+
type DriveMoveType = "doc" | "docx" | "sheet" | "bitable" | "folder" | "file" | "mindnote" | "slides";
|
|
6
|
+
type DriveDeleteType = DriveMoveType | "shortcut";
|
|
7
|
+
|
|
8
|
+
async function getRootFolderToken(client: DriveClient): Promise<string> {
|
|
9
|
+
// Use generic HTTP client to call the root folder meta API
|
|
10
|
+
// as it's not directly exposed in the SDK.
|
|
11
|
+
const domain = (client as any).domain ?? "https://open.feishu.cn";
|
|
12
|
+
const res = await runDriveApiCall("drive.explorer.v2.root_folder.meta", () =>
|
|
13
|
+
(client as any).httpInstance.get(`${domain}/open-apis/drive/explorer/v2/root_folder/meta`) as Promise<{
|
|
14
|
+
code?: number;
|
|
15
|
+
msg?: string;
|
|
16
|
+
data?: { token?: string };
|
|
17
|
+
}>,
|
|
18
|
+
);
|
|
19
|
+
const token = res.data?.token;
|
|
20
|
+
if (!token) throw new Error("Root folder token not found");
|
|
21
|
+
return token;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function listFolder(client: DriveClient, folderToken?: string) {
|
|
25
|
+
// Filter out invalid folder_token values (empty, "0", etc.)
|
|
26
|
+
const validFolderToken = folderToken && folderToken !== "0" ? folderToken : undefined;
|
|
27
|
+
const res = await runDriveApiCall("drive.file.list", () =>
|
|
28
|
+
client.drive.file.list({
|
|
29
|
+
params: validFolderToken ? { folder_token: validFolderToken } : {},
|
|
30
|
+
}),
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
files:
|
|
35
|
+
res.data?.files?.map((f) => ({
|
|
36
|
+
token: f.token,
|
|
37
|
+
name: f.name,
|
|
38
|
+
type: f.type,
|
|
39
|
+
url: f.url,
|
|
40
|
+
created_time: f.created_time,
|
|
41
|
+
modified_time: f.modified_time,
|
|
42
|
+
owner_id: f.owner_id,
|
|
43
|
+
})) ?? [],
|
|
44
|
+
next_page_token: res.data?.next_page_token,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function getFileInfo(client: DriveClient, fileToken: string, folderToken?: string) {
|
|
49
|
+
// Use list with folder_token to find file info.
|
|
50
|
+
const res = await runDriveApiCall("drive.file.list", () =>
|
|
51
|
+
client.drive.file.list({
|
|
52
|
+
params: folderToken ? { folder_token: folderToken } : {},
|
|
53
|
+
}),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const file = res.data?.files?.find((f) => f.token === fileToken);
|
|
57
|
+
if (!file) {
|
|
58
|
+
throw new Error(`File not found: ${fileToken}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
token: file.token,
|
|
63
|
+
name: file.name,
|
|
64
|
+
type: file.type,
|
|
65
|
+
url: file.url,
|
|
66
|
+
created_time: file.created_time,
|
|
67
|
+
modified_time: file.modified_time,
|
|
68
|
+
owner_id: file.owner_id,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function createFolder(client: DriveClient, name: string, folderToken?: string) {
|
|
73
|
+
// Feishu supports using folder_token="0" as the root folder.
|
|
74
|
+
// We try to resolve the real root token (explorer API), but fall back to "0"
|
|
75
|
+
// because some tenants/apps return 400 for that explorer endpoint.
|
|
76
|
+
let effectiveToken = folderToken && folderToken !== "0" ? folderToken : "0";
|
|
77
|
+
if (effectiveToken === "0") {
|
|
78
|
+
try {
|
|
79
|
+
effectiveToken = await getRootFolderToken(client);
|
|
80
|
+
} catch {
|
|
81
|
+
// ignore and keep "0"
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const res = await runDriveApiCall("drive.file.createFolder", () =>
|
|
86
|
+
client.drive.file.createFolder({
|
|
87
|
+
data: {
|
|
88
|
+
name,
|
|
89
|
+
folder_token: effectiveToken,
|
|
90
|
+
},
|
|
91
|
+
}),
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
token: res.data?.token,
|
|
96
|
+
url: res.data?.url,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function moveFile(
|
|
101
|
+
client: DriveClient,
|
|
102
|
+
fileToken: string,
|
|
103
|
+
type: string,
|
|
104
|
+
folderToken: string,
|
|
105
|
+
) {
|
|
106
|
+
const res = await runDriveApiCall("drive.file.move", () =>
|
|
107
|
+
client.drive.file.move({
|
|
108
|
+
path: { file_token: fileToken },
|
|
109
|
+
data: {
|
|
110
|
+
type: type as DriveMoveType,
|
|
111
|
+
folder_token: folderToken,
|
|
112
|
+
},
|
|
113
|
+
}),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
success: true,
|
|
118
|
+
task_id: res.data?.task_id,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function deleteFile(client: DriveClient, fileToken: string, type: string) {
|
|
123
|
+
const res = await runDriveApiCall("drive.file.delete", () =>
|
|
124
|
+
client.drive.file.delete({
|
|
125
|
+
path: { file_token: fileToken },
|
|
126
|
+
params: {
|
|
127
|
+
type: type as DriveDeleteType,
|
|
128
|
+
},
|
|
129
|
+
}),
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
success: true,
|
|
134
|
+
task_id: res.data?.task_id,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Import markdown content as a new Feishu document.
|
|
140
|
+
* Uses create + write approach for reliable content import.
|
|
141
|
+
* Note: docType parameter is accepted for API compatibility but docx is always used.
|
|
142
|
+
*/
|
|
143
|
+
async function importDocument(
|
|
144
|
+
client: DriveClient,
|
|
145
|
+
title: string,
|
|
146
|
+
content: string,
|
|
147
|
+
mediaMaxBytes: number,
|
|
148
|
+
folderToken?: string,
|
|
149
|
+
_docType?: "docx" | "doc",
|
|
150
|
+
) {
|
|
151
|
+
return createAndWriteDoc(client, title, content, mediaMaxBytes, folderToken);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function runDriveAction(
|
|
155
|
+
client: DriveClient,
|
|
156
|
+
params: FeishuDriveParams,
|
|
157
|
+
mediaMaxBytes: number,
|
|
158
|
+
) {
|
|
159
|
+
switch (params.action) {
|
|
160
|
+
case "list":
|
|
161
|
+
return listFolder(client, params.folder_token);
|
|
162
|
+
case "info":
|
|
163
|
+
return getFileInfo(client, params.file_token);
|
|
164
|
+
case "create_folder":
|
|
165
|
+
return createFolder(client, params.name, params.folder_token);
|
|
166
|
+
case "move":
|
|
167
|
+
return moveFile(client, params.file_token, params.type, params.folder_token);
|
|
168
|
+
case "delete":
|
|
169
|
+
return deleteFile(client, params.file_token, params.type);
|
|
170
|
+
case "import_document":
|
|
171
|
+
return importDocument(
|
|
172
|
+
client,
|
|
173
|
+
params.title,
|
|
174
|
+
params.content,
|
|
175
|
+
mediaMaxBytes,
|
|
176
|
+
params.folder_token,
|
|
177
|
+
params.doc_type || "docx",
|
|
178
|
+
);
|
|
179
|
+
default:
|
|
180
|
+
return { error: `Unknown action: ${(params as any).action}` };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createFeishuClient } from "../client.js";
|
|
2
|
+
import {
|
|
3
|
+
errorResult,
|
|
4
|
+
json,
|
|
5
|
+
runFeishuApiCall,
|
|
6
|
+
type FeishuApiResponse,
|
|
7
|
+
} from "../tools-common/feishu-api.js";
|
|
8
|
+
|
|
9
|
+
export type DriveClient = ReturnType<typeof createFeishuClient>;
|
|
10
|
+
|
|
11
|
+
export { json, errorResult };
|
|
12
|
+
|
|
13
|
+
export async function runDriveApiCall<T extends FeishuApiResponse>(
|
|
14
|
+
context: string,
|
|
15
|
+
fn: () => Promise<T>,
|
|
16
|
+
): Promise<T> {
|
|
17
|
+
return runFeishuApiCall(context, fn);
|
|
18
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { TSchema } from "@sinclair/typebox";
|
|
2
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
3
|
+
import type { ResolvedFeishuAccount } from "../types.js";
|
|
4
|
+
import { hasFeishuToolEnabledForAnyAccount, withFeishuToolClient } from "../tools-common/tool-exec.js";
|
|
5
|
+
import { runDriveAction } from "./actions.js";
|
|
6
|
+
import { errorResult, json, type DriveClient } from "./common.js";
|
|
7
|
+
import { FeishuDriveSchema, type FeishuDriveParams } from "./schemas.js";
|
|
8
|
+
|
|
9
|
+
type DriveToolSpec<P> = {
|
|
10
|
+
name: string;
|
|
11
|
+
label: string;
|
|
12
|
+
description: string;
|
|
13
|
+
parameters: TSchema;
|
|
14
|
+
run: (args: { client: DriveClient; account: ResolvedFeishuAccount }, params: P) => Promise<unknown>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function registerDriveTool<P>(api: OpenClawPluginApi, spec: DriveToolSpec<P>) {
|
|
18
|
+
api.registerTool(
|
|
19
|
+
{
|
|
20
|
+
name: spec.name,
|
|
21
|
+
label: spec.label,
|
|
22
|
+
description: spec.description,
|
|
23
|
+
parameters: spec.parameters,
|
|
24
|
+
async execute(_toolCallId, params) {
|
|
25
|
+
try {
|
|
26
|
+
return await withFeishuToolClient({
|
|
27
|
+
api,
|
|
28
|
+
toolName: spec.name,
|
|
29
|
+
requiredTool: "drive",
|
|
30
|
+
run: async ({ client, account }) =>
|
|
31
|
+
json(await spec.run({ client: client as DriveClient, account }, params as P)),
|
|
32
|
+
});
|
|
33
|
+
} catch (err) {
|
|
34
|
+
return errorResult(err);
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{ name: spec.name },
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function registerFeishuDriveTools(api: OpenClawPluginApi) {
|
|
43
|
+
if (!api.config) {
|
|
44
|
+
api.logger.debug?.("feishu_drive: No config available, skipping drive tools");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!hasFeishuToolEnabledForAnyAccount(api.config)) {
|
|
49
|
+
api.logger.debug?.("feishu_drive: No Feishu accounts configured, skipping drive tools");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!hasFeishuToolEnabledForAnyAccount(api.config, "drive")) {
|
|
54
|
+
api.logger.debug?.("feishu_drive: drive tool disabled in config");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
registerDriveTool<FeishuDriveParams>(api, {
|
|
59
|
+
name: "feishu_drive",
|
|
60
|
+
label: "Feishu Drive",
|
|
61
|
+
description:
|
|
62
|
+
"Feishu cloud storage operations. Actions: list, info, create_folder, move, delete, import_document. Use 'import_document' to create documents from Markdown with better structure preservation than block-by-block writing.",
|
|
63
|
+
parameters: FeishuDriveSchema,
|
|
64
|
+
run: async ({ client, account }, params) => {
|
|
65
|
+
const mediaMaxBytes = (account.config?.mediaMaxMb ?? 30) * 1024 * 1024;
|
|
66
|
+
return runDriveAction(client, params, mediaMaxBytes);
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
api.logger.debug?.("feishu_drive: Registered feishu_drive tool");
|
|
71
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Type, type Static } from "@sinclair/typebox";
|
|
2
|
+
|
|
3
|
+
const FileType = Type.Union([
|
|
4
|
+
Type.Literal("doc"),
|
|
5
|
+
Type.Literal("docx"),
|
|
6
|
+
Type.Literal("sheet"),
|
|
7
|
+
Type.Literal("bitable"),
|
|
8
|
+
Type.Literal("folder"),
|
|
9
|
+
Type.Literal("file"),
|
|
10
|
+
Type.Literal("mindnote"),
|
|
11
|
+
Type.Literal("shortcut"),
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
const DocType = Type.Union([
|
|
15
|
+
Type.Literal("docx", { description: "New generation document (default)" }),
|
|
16
|
+
Type.Literal("doc", { description: "Legacy document" }),
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
export const FeishuDriveSchema = Type.Union([
|
|
20
|
+
Type.Object({
|
|
21
|
+
action: Type.Literal("list"),
|
|
22
|
+
folder_token: Type.Optional(
|
|
23
|
+
Type.String({ description: "Folder token (optional, omit for root directory)" }),
|
|
24
|
+
),
|
|
25
|
+
}),
|
|
26
|
+
Type.Object({
|
|
27
|
+
action: Type.Literal("info"),
|
|
28
|
+
file_token: Type.String({ description: "File or folder token" }),
|
|
29
|
+
type: FileType,
|
|
30
|
+
}),
|
|
31
|
+
Type.Object({
|
|
32
|
+
action: Type.Literal("create_folder"),
|
|
33
|
+
name: Type.String({ description: "Folder name" }),
|
|
34
|
+
folder_token: Type.Optional(
|
|
35
|
+
Type.String({ description: "Parent folder token (optional, omit for root)" }),
|
|
36
|
+
),
|
|
37
|
+
}),
|
|
38
|
+
Type.Object({
|
|
39
|
+
action: Type.Literal("move"),
|
|
40
|
+
file_token: Type.String({ description: "File token to move" }),
|
|
41
|
+
type: FileType,
|
|
42
|
+
folder_token: Type.String({ description: "Target folder token" }),
|
|
43
|
+
}),
|
|
44
|
+
Type.Object({
|
|
45
|
+
action: Type.Literal("delete"),
|
|
46
|
+
file_token: Type.String({ description: "File token to delete" }),
|
|
47
|
+
type: FileType,
|
|
48
|
+
}),
|
|
49
|
+
Type.Object({
|
|
50
|
+
action: Type.Literal("import_document"),
|
|
51
|
+
title: Type.String({
|
|
52
|
+
description: "Document title",
|
|
53
|
+
}),
|
|
54
|
+
content: Type.String({
|
|
55
|
+
description:
|
|
56
|
+
"Markdown content to import. Supports full Markdown syntax including tables, lists, code blocks, etc.",
|
|
57
|
+
}),
|
|
58
|
+
folder_token: Type.Optional(
|
|
59
|
+
Type.String({
|
|
60
|
+
description: "Target folder token (optional, defaults to root). Use 'list' to find folder tokens.",
|
|
61
|
+
}),
|
|
62
|
+
),
|
|
63
|
+
doc_type: Type.Optional(DocType),
|
|
64
|
+
}),
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
export type FeishuDriveParams = Static<typeof FeishuDriveSchema>;
|
|
@@ -0,0 +1,135 @@
|
|
|
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
|
+
accountId?: string;
|
|
23
|
+
log: (msg: string) => void;
|
|
24
|
+
}): Promise<MaybeCreateDynamicAgentResult> {
|
|
25
|
+
const { cfg, runtime, senderOpenId, dynamicCfg, accountId, log } = params;
|
|
26
|
+
|
|
27
|
+
// Check if there's already a binding for this user
|
|
28
|
+
const existingBindings = cfg.bindings ?? [];
|
|
29
|
+
const hasBinding = existingBindings.some(
|
|
30
|
+
(b) =>
|
|
31
|
+
b.match?.channel === "feishu" &&
|
|
32
|
+
(!accountId || b.match?.accountId === accountId) &&
|
|
33
|
+
b.match?.peer?.kind === "direct" &&
|
|
34
|
+
b.match?.peer?.id === senderOpenId,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
if (hasBinding) {
|
|
38
|
+
return { created: false, updatedCfg: cfg };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Check maxAgents limit if configured
|
|
42
|
+
if (dynamicCfg.maxAgents !== undefined) {
|
|
43
|
+
const feishuAgentCount = (cfg.agents?.list ?? []).filter((a) =>
|
|
44
|
+
a.id.startsWith("feishu-"),
|
|
45
|
+
).length;
|
|
46
|
+
if (feishuAgentCount >= dynamicCfg.maxAgents) {
|
|
47
|
+
log(
|
|
48
|
+
`feishu: maxAgents limit (${dynamicCfg.maxAgents}) reached, not creating agent for ${senderOpenId}`,
|
|
49
|
+
);
|
|
50
|
+
return { created: false, updatedCfg: cfg };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Use full OpenID as agent ID suffix (OpenID format: ou_xxx is already filesystem-safe)
|
|
55
|
+
const agentId = `feishu-${senderOpenId}`;
|
|
56
|
+
|
|
57
|
+
// Check if agent already exists (but binding was missing)
|
|
58
|
+
const existingAgent = (cfg.agents?.list ?? []).find((a) => a.id === agentId);
|
|
59
|
+
if (existingAgent) {
|
|
60
|
+
// Agent exists but binding doesn't - just add the binding
|
|
61
|
+
log(`feishu: agent "${agentId}" exists, adding missing binding for ${senderOpenId}`);
|
|
62
|
+
|
|
63
|
+
const updatedCfg: OpenClawConfig = {
|
|
64
|
+
...cfg,
|
|
65
|
+
bindings: [
|
|
66
|
+
...existingBindings,
|
|
67
|
+
{
|
|
68
|
+
agentId,
|
|
69
|
+
match: {
|
|
70
|
+
channel: "feishu",
|
|
71
|
+
...(accountId ? { accountId } : {}),
|
|
72
|
+
peer: { kind: "direct", id: senderOpenId },
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
await runtime.config.writeConfigFile(updatedCfg);
|
|
79
|
+
return { created: true, updatedCfg, agentId };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Resolve path templates with substitutions
|
|
83
|
+
const workspaceTemplate = dynamicCfg.workspaceTemplate ?? "~/.openclaw/workspace-{agentId}";
|
|
84
|
+
const agentDirTemplate = dynamicCfg.agentDirTemplate ?? "~/.openclaw/agents/{agentId}/agent";
|
|
85
|
+
|
|
86
|
+
const workspace = resolveUserPath(
|
|
87
|
+
workspaceTemplate.replace("{userId}", senderOpenId).replace("{agentId}", agentId),
|
|
88
|
+
);
|
|
89
|
+
const agentDir = resolveUserPath(
|
|
90
|
+
agentDirTemplate.replace("{userId}", senderOpenId).replace("{agentId}", agentId),
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
log(`feishu: creating dynamic agent "${agentId}" for user ${senderOpenId}`);
|
|
94
|
+
log(` workspace: ${workspace}`);
|
|
95
|
+
log(` agentDir: ${agentDir}`);
|
|
96
|
+
|
|
97
|
+
// Create directories
|
|
98
|
+
await fs.promises.mkdir(workspace, { recursive: true });
|
|
99
|
+
await fs.promises.mkdir(agentDir, { recursive: true });
|
|
100
|
+
|
|
101
|
+
// Update configuration with new agent and binding
|
|
102
|
+
const updatedCfg: OpenClawConfig = {
|
|
103
|
+
...cfg,
|
|
104
|
+
agents: {
|
|
105
|
+
...cfg.agents,
|
|
106
|
+
list: [...(cfg.agents?.list ?? []), { id: agentId, workspace, agentDir }],
|
|
107
|
+
},
|
|
108
|
+
bindings: [
|
|
109
|
+
...existingBindings,
|
|
110
|
+
{
|
|
111
|
+
agentId,
|
|
112
|
+
match: {
|
|
113
|
+
channel: "feishu",
|
|
114
|
+
...(accountId ? { accountId } : {}),
|
|
115
|
+
peer: { kind: "direct", id: senderOpenId },
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// Write updated config using PluginRuntime API
|
|
122
|
+
await runtime.config.writeConfigFile(updatedCfg);
|
|
123
|
+
|
|
124
|
+
return { created: true, updatedCfg, agentId };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Resolve a path that may start with ~ to the user's home directory.
|
|
129
|
+
*/
|
|
130
|
+
function resolveUserPath(p: string): string {
|
|
131
|
+
if (p.startsWith("~/")) {
|
|
132
|
+
return path.join(os.homedir(), p.slice(2));
|
|
133
|
+
}
|
|
134
|
+
return p;
|
|
135
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const CONTROL_CHARS_RE = /[\u0000-\u001f\u007f]/;
|
|
2
|
+
const MAX_EXTERNAL_KEY_LENGTH = 512;
|
|
3
|
+
|
|
4
|
+
export function normalizeFeishuExternalKey(value: unknown): string | undefined {
|
|
5
|
+
if (typeof value !== "string") {
|
|
6
|
+
return undefined;
|
|
7
|
+
}
|
|
8
|
+
const normalized = value.trim();
|
|
9
|
+
if (!normalized || normalized.length > MAX_EXTERNAL_KEY_LENGTH) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
if (CONTROL_CHARS_RE.test(normalized)) {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
if (normalized.includes("/") || normalized.includes("\\") || normalized.includes("..")) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
return normalized;
|
|
19
|
+
}
|