Skip to content

Commit bf9585d

Browse files
PR: Feishu Plugin - Auto-grant document permissions to requesting user (#28295) thanks @zhoulongchao77
Verified: - pnpm build - pnpm check - pnpm test:macmini Co-authored-by: zhoulongchao77 <[email protected]> Co-authored-by: Tak Hoffman <[email protected]>
1 parent fa5e71d commit bf9585d

File tree

4 files changed

+168
-4
lines changed

4 files changed

+168
-4
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Docs: https://docs.openclaw.ai
1010
- Discord/Thread bindings: replace fixed TTL lifecycle with inactivity (`idleHours`, default 24h) plus optional hard `maxAgeHours` lifecycle controls, and add `/session idle` + `/session max-age` commands for focused thread-bound sessions. (#27845) Thanks @osolmaz.
1111
- Android/Nodes: add `camera.list`, `device.permissions`, `device.health`, and `notifications.actions` (`open`/`dismiss`/`reply`) on Android nodes, plus first-class node-tool actions for the new device/notification commands. (#28260) Thanks @obviyus.
1212
- Android/Gateway capability refresh: add live Android capability integration coverage and node canvas capability refresh wiring, plus runtime hardening for A2UI readiness retries, scoped canvas URL normalization, debug diagnostics JSON, and JavaScript MIME delivery. (#28388) Thanks @obviyus.
13+
- Feishu/Doc permissions: support optional owner permission grant fields on `feishu_doc` create and report permission metadata only when the grant call succeeds, with regression coverage for success/failure/omitted-owner paths. (#28295) Thanks @zhoulongchao77.
1314

1415
### Fixes
1516

extensions/feishu/src/doc-schema.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ export const FeishuDocSchema = Type.Union([
2121
action: Type.Literal("create"),
2222
title: Type.String({ description: "Document title" }),
2323
folder_token: Type.Optional(Type.String({ description: "Target folder token (optional)" })),
24+
owner_open_id: Type.Optional(
25+
Type.String({ description: "Open ID of the user to grant ownership permission" }),
26+
),
27+
owner_perm_type: Type.Optional(
28+
Type.Union([Type.Literal("view"), Type.Literal("edit"), Type.Literal("full_access")], {
29+
description: "Permission type (default: full_access)",
30+
}),
31+
),
2432
}),
2533
Type.Object({
2634
action: Type.Literal("list_blocks"),

extensions/feishu/src/docx.test.ts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@ import { registerFeishuDocTools } from "./docx.js";
2121

2222
describe("feishu_doc image fetch hardening", () => {
2323
const convertMock = vi.hoisted(() => vi.fn());
24+
const documentCreateMock = vi.hoisted(() => vi.fn());
2425
const blockListMock = vi.hoisted(() => vi.fn());
2526
const blockChildrenCreateMock = vi.hoisted(() => vi.fn());
2627
const driveUploadAllMock = vi.hoisted(() => vi.fn());
28+
const permissionMemberCreateMock = vi.hoisted(() => vi.fn());
2729
const blockPatchMock = vi.hoisted(() => vi.fn());
2830
const scopeListMock = vi.hoisted(() => vi.fn());
2931

@@ -34,6 +36,7 @@ describe("feishu_doc image fetch hardening", () => {
3436
docx: {
3537
document: {
3638
convert: convertMock,
39+
create: documentCreateMock,
3740
},
3841
documentBlock: {
3942
list: blockListMock,
@@ -47,6 +50,9 @@ describe("feishu_doc image fetch hardening", () => {
4750
media: {
4851
uploadAll: driveUploadAllMock,
4952
},
53+
permissionMember: {
54+
create: permissionMemberCreateMock,
55+
},
5056
},
5157
application: {
5258
scope: {
@@ -78,6 +84,11 @@ describe("feishu_doc image fetch hardening", () => {
7884
});
7985

8086
driveUploadAllMock.mockResolvedValue({ file_token: "token_1" });
87+
documentCreateMock.mockResolvedValue({
88+
code: 0,
89+
data: { document: { document_id: "doc_created", title: "Created Doc" } },
90+
});
91+
permissionMemberCreateMock.mockResolvedValue({ code: 0 });
8192
blockPatchMock.mockResolvedValue({ code: 0 });
8293
scopeListMock.mockResolvedValue({ code: 0, data: { scopes: [] } });
8394
});
@@ -121,4 +132,107 @@ describe("feishu_doc image fetch hardening", () => {
121132
expect(consoleErrorSpy).toHaveBeenCalled();
122133
consoleErrorSpy.mockRestore();
123134
});
135+
136+
it("reports owner permission details when grant succeeds", async () => {
137+
const registerTool = vi.fn();
138+
registerFeishuDocTools({
139+
config: {
140+
channels: {
141+
feishu: {
142+
appId: "app_id",
143+
appSecret: "app_secret",
144+
},
145+
},
146+
} as any,
147+
logger: { debug: vi.fn(), info: vi.fn() } as any,
148+
registerTool,
149+
} as any);
150+
151+
const feishuDocTool = registerTool.mock.calls
152+
.map((call) => call[0])
153+
.map((tool) => (typeof tool === "function" ? tool({}) : tool))
154+
.find((tool) => tool.name === "feishu_doc");
155+
expect(feishuDocTool).toBeDefined();
156+
157+
const result = await feishuDocTool.execute("tool-call", {
158+
action: "create",
159+
title: "Demo",
160+
owner_open_id: "ou_123",
161+
owner_perm_type: "edit",
162+
});
163+
164+
expect(permissionMemberCreateMock).toHaveBeenCalled();
165+
expect(result.details.owner_permission_added).toBe(true);
166+
expect(result.details.owner_open_id).toBe("ou_123");
167+
expect(result.details.owner_perm_type).toBe("edit");
168+
});
169+
170+
it("does not report owner permission details when grant fails", async () => {
171+
const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
172+
permissionMemberCreateMock.mockRejectedValueOnce(new Error("permission denied"));
173+
174+
const registerTool = vi.fn();
175+
registerFeishuDocTools({
176+
config: {
177+
channels: {
178+
feishu: {
179+
appId: "app_id",
180+
appSecret: "app_secret",
181+
},
182+
},
183+
} as any,
184+
logger: { debug: vi.fn(), info: vi.fn() } as any,
185+
registerTool,
186+
} as any);
187+
188+
const feishuDocTool = registerTool.mock.calls
189+
.map((call) => call[0])
190+
.map((tool) => (typeof tool === "function" ? tool({}) : tool))
191+
.find((tool) => tool.name === "feishu_doc");
192+
expect(feishuDocTool).toBeDefined();
193+
194+
const result = await feishuDocTool.execute("tool-call", {
195+
action: "create",
196+
title: "Demo",
197+
owner_open_id: "ou_123",
198+
owner_perm_type: "edit",
199+
});
200+
201+
expect(permissionMemberCreateMock).toHaveBeenCalled();
202+
expect(result.details.owner_permission_added).toBeUndefined();
203+
expect(result.details.owner_open_id).toBeUndefined();
204+
expect(result.details.owner_perm_type).toBeUndefined();
205+
expect(consoleWarnSpy).toHaveBeenCalled();
206+
consoleWarnSpy.mockRestore();
207+
});
208+
209+
it("skips permission grant when owner_open_id is omitted", async () => {
210+
const registerTool = vi.fn();
211+
registerFeishuDocTools({
212+
config: {
213+
channels: {
214+
feishu: {
215+
appId: "app_id",
216+
appSecret: "app_secret",
217+
},
218+
},
219+
} as any,
220+
logger: { debug: vi.fn(), info: vi.fn() } as any,
221+
registerTool,
222+
} as any);
223+
224+
const feishuDocTool = registerTool.mock.calls
225+
.map((call) => call[0])
226+
.map((tool) => (typeof tool === "function" ? tool({}) : tool))
227+
.find((tool) => tool.name === "feishu_doc");
228+
expect(feishuDocTool).toBeDefined();
229+
230+
const result = await feishuDocTool.execute("tool-call", {
231+
action: "create",
232+
title: "Demo",
233+
});
234+
235+
expect(permissionMemberCreateMock).not.toHaveBeenCalled();
236+
expect(result.details.owner_permission_added).toBeUndefined();
237+
});
124238
});

extensions/feishu/src/docx.ts

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -271,18 +271,51 @@ async function readDoc(client: Lark.Client, docToken: string) {
271271
};
272272
}
273273

274-
async function createDoc(client: Lark.Client, title: string, folderToken?: string) {
274+
async function createDoc(
275+
client: Lark.Client,
276+
title: string,
277+
folderToken?: string,
278+
ownerOpenId?: string,
279+
ownerPermType: "view" | "edit" | "full_access" = "full_access",
280+
) {
275281
const res = await client.docx.document.create({
276282
data: { title, folder_token: folderToken },
277283
});
278284
if (res.code !== 0) {
279285
throw new Error(res.msg);
280286
}
281287
const doc = res.data?.document;
288+
const docToken = doc?.document_id;
289+
let ownerPermissionAdded = false;
290+
291+
// Auto add owner permission if ownerOpenId is provided
292+
if (docToken && ownerOpenId) {
293+
try {
294+
await client.drive.permissionMember.create({
295+
path: { token: docToken },
296+
params: { type: "docx", need_notification: false },
297+
data: {
298+
member_type: "openid",
299+
member_id: ownerOpenId,
300+
perm: ownerPermType,
301+
},
302+
});
303+
ownerPermissionAdded = true;
304+
} catch (err) {
305+
console.warn("Failed to add owner permission (non-critical):", err);
306+
}
307+
}
308+
282309
return {
283-
document_id: doc?.document_id,
310+
document_id: docToken,
284311
title: doc?.title,
285-
url: `https://feishu.cn/docx/${doc?.document_id}`,
312+
url: `https://feishu.cn/docx/${docToken}`,
313+
...(ownerOpenId &&
314+
ownerPermissionAdded && {
315+
owner_permission_added: true,
316+
owner_open_id: ownerOpenId,
317+
owner_perm_type: ownerPermType,
318+
}),
286319
};
287320
}
288321

@@ -512,7 +545,15 @@ export function registerFeishuDocTools(api: OpenClawPluginApi) {
512545
),
513546
);
514547
case "create":
515-
return json(await createDoc(client, p.title, p.folder_token));
548+
return json(
549+
await createDoc(
550+
client,
551+
p.title,
552+
p.folder_token,
553+
p.owner_open_id,
554+
p.owner_perm_type,
555+
),
556+
);
516557
case "list_blocks":
517558
return json(await listBlocks(client, p.doc_token));
518559
case "get_block":

0 commit comments

Comments
 (0)