@max1874/feishu 0.1.7 → 0.2.1

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/docx.ts +262 -8
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@max1874/feishu",
3
- "version": "0.1.7",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "description": "OpenClaw Feishu/Lark channel plugin",
6
6
  "license": "MIT",
package/src/docx.ts CHANGED
@@ -232,6 +232,101 @@ async function processImages(
232
232
  return processed;
233
233
  }
234
234
 
235
+ // ============ Wiki Functions ============
236
+
237
+ type WikiNodeInfo = {
238
+ node_token: string;
239
+ obj_token: string;
240
+ obj_type: string;
241
+ space_id: string;
242
+ title?: string;
243
+ parent_node_token?: string;
244
+ node_type?: string;
245
+ origin_space_id?: string;
246
+ };
247
+
248
+ /** Get wiki node info by token (from /wiki/XXX URL) */
249
+ async function getWikiNode(client: Lark.Client, wikiToken: string): Promise<WikiNodeInfo> {
250
+ const res = await client.wiki.space.getNode({
251
+ params: { token: wikiToken },
252
+ });
253
+ if (res.code !== 0) throw new Error(res.msg);
254
+ const node = res.data?.node;
255
+ if (!node) throw new Error("Wiki node not found");
256
+ return {
257
+ node_token: node.node_token ?? wikiToken,
258
+ obj_token: node.obj_token ?? "",
259
+ obj_type: node.obj_type ?? "",
260
+ space_id: node.space_id ?? "",
261
+ title: node.title,
262
+ parent_node_token: node.parent_node_token,
263
+ node_type: node.node_type,
264
+ origin_space_id: node.origin_space_id,
265
+ };
266
+ }
267
+
268
+ /** Read wiki page content (resolves to underlying docx) */
269
+ async function readWiki(client: Lark.Client, wikiToken: string) {
270
+ // 1. Get wiki node info to find the underlying document
271
+ const node = await getWikiNode(client, wikiToken);
272
+
273
+ if (node.obj_type !== "docx") {
274
+ return {
275
+ node,
276
+ error: `Wiki node is of type '${node.obj_type}', only 'docx' is supported for reading content. Use the obj_token with the appropriate API.`,
277
+ };
278
+ }
279
+
280
+ // 2. Read the underlying docx content
281
+ const docContent = await readDoc(client, node.obj_token);
282
+
283
+ return {
284
+ wiki_token: wikiToken,
285
+ title: node.title,
286
+ obj_type: node.obj_type,
287
+ obj_token: node.obj_token,
288
+ space_id: node.space_id,
289
+ ...docContent,
290
+ };
291
+ }
292
+
293
+ /** List wiki spaces */
294
+ async function listWikiSpaces(client: Lark.Client) {
295
+ const res = await client.wiki.space.list({});
296
+ if (res.code !== 0) throw new Error(res.msg);
297
+
298
+ return {
299
+ spaces:
300
+ res.data?.items?.map((s) => ({
301
+ space_id: s.space_id,
302
+ name: s.name,
303
+ description: s.description,
304
+ type: s.space_type === "team" ? "team" : "personal",
305
+ visibility: s.visibility,
306
+ })) ?? [],
307
+ };
308
+ }
309
+
310
+ /** List wiki nodes under a parent */
311
+ async function listWikiNodes(client: Lark.Client, spaceId: string, parentNodeToken?: string) {
312
+ const res = await client.wiki.spaceNode.list({
313
+ path: { space_id: spaceId },
314
+ params: { parent_node_token: parentNodeToken },
315
+ });
316
+ if (res.code !== 0) throw new Error(res.msg);
317
+
318
+ return {
319
+ nodes:
320
+ res.data?.items?.map((n) => ({
321
+ node_token: n.node_token,
322
+ obj_token: n.obj_token,
323
+ obj_type: n.obj_type,
324
+ title: n.title,
325
+ has_child: n.has_child,
326
+ })) ?? [],
327
+ };
328
+ }
329
+
235
330
  // ============ Actions ============
236
331
 
237
332
  // Block types that are NOT included in rawContent (plain text) output
@@ -278,16 +373,45 @@ async function readDoc(client: Lark.Client, docToken: string) {
278
373
  };
279
374
  }
280
375
 
281
- async function createDoc(client: Lark.Client, title: string, folderToken?: string) {
376
+ /** Set document permission to allow tenant members to edit */
377
+ async function setDocPermissionTenantEditable(client: Lark.Client, docToken: string) {
378
+ const res = await client.drive.permissionPublic.patch({
379
+ path: { token: docToken },
380
+ params: { type: "docx" },
381
+ data: {
382
+ link_share_entity: "tenant_editable",
383
+ share_entity: "same_tenant",
384
+ },
385
+ });
386
+ if (res.code !== 0) throw new Error(res.msg);
387
+ return res.data;
388
+ }
389
+
390
+ export type DocPermission = "private" | "tenant_editable";
391
+
392
+ async function createDoc(
393
+ client: Lark.Client,
394
+ title: string,
395
+ folderToken?: string,
396
+ permission?: DocPermission,
397
+ ) {
282
398
  const res = await client.docx.document.create({
283
399
  data: { title, folder_token: folderToken },
284
400
  });
285
401
  if (res.code !== 0) throw new Error(res.msg);
286
402
  const doc = res.data?.document;
403
+ const docId = doc?.document_id;
404
+
405
+ // Set permission if requested
406
+ if (permission === "tenant_editable" && docId) {
407
+ await setDocPermissionTenantEditable(client, docId);
408
+ }
409
+
287
410
  return {
288
- document_id: doc?.document_id,
411
+ document_id: docId,
289
412
  title: doc?.title,
290
- url: `https://feishu.cn/docx/${doc?.document_id}`,
413
+ url: `https://feishu.cn/docx/${docId}`,
414
+ permission: permission ?? "private",
291
415
  };
292
416
  }
293
417
 
@@ -455,6 +579,12 @@ const DocTokenSchema = Type.Object({
455
579
  const CreateDocSchema = Type.Object({
456
580
  title: Type.String({ description: "Document title" }),
457
581
  folder_token: Type.Optional(Type.String({ description: "Target folder token (optional)" })),
582
+ permission: Type.Optional(
583
+ Type.Union([Type.Literal("private"), Type.Literal("tenant_editable")], {
584
+ description:
585
+ "Document permission: 'private' (default) or 'tenant_editable' (organization members can edit via link)",
586
+ }),
587
+ ),
458
588
  });
459
589
 
460
590
  const WriteDocSchema = Type.Object({
@@ -489,6 +619,25 @@ const FolderTokenSchema = Type.Object({
489
619
  folder_token: Type.String({ description: "Folder token" }),
490
620
  });
491
621
 
622
+ const SetPermissionSchema = Type.Object({
623
+ doc_token: Type.String({ description: "Document token" }),
624
+ permission: Type.Union([Type.Literal("private"), Type.Literal("tenant_editable")], {
625
+ description: "'private' or 'tenant_editable' (organization members can edit via link)",
626
+ }),
627
+ });
628
+
629
+ // Wiki Schemas
630
+ const WikiTokenSchema = Type.Object({
631
+ wiki_token: Type.String({ description: "Wiki token (extract from URL /wiki/XXX)" }),
632
+ });
633
+
634
+ const WikiSpaceIdSchema = Type.Object({
635
+ space_id: Type.String({ description: "Wiki space ID" }),
636
+ parent_node_token: Type.Optional(
637
+ Type.String({ description: "Parent node token (optional, for listing children)" }),
638
+ ),
639
+ });
640
+
492
641
  // ============ Tool Registration ============
493
642
 
494
643
  export function registerFeishuDocTools(api: OpenClawPluginApi) {
@@ -525,12 +674,17 @@ export function registerFeishuDocTools(api: OpenClawPluginApi) {
525
674
  {
526
675
  name: "feishu_doc_create",
527
676
  label: "Feishu Doc Create",
528
- description: "Create a new empty Feishu document",
677
+ description:
678
+ "Create a new empty Feishu document. Use permission='tenant_editable' to allow organization members to edit via link.",
529
679
  parameters: CreateDocSchema,
530
680
  async execute(_toolCallId, params) {
531
- const { title, folder_token } = params as { title: string; folder_token?: string };
681
+ const { title, folder_token, permission } = params as {
682
+ title: string;
683
+ folder_token?: string;
684
+ permission?: DocPermission;
685
+ };
532
686
  try {
533
- const result = await createDoc(getClient(), title, folder_token);
687
+ const result = await createDoc(getClient(), title, folder_token, permission);
534
688
  return json(result);
535
689
  } catch (err) {
536
690
  return json({ error: err instanceof Error ? err.message : String(err) });
@@ -687,7 +841,107 @@ export function registerFeishuDocTools(api: OpenClawPluginApi) {
687
841
  { name: "feishu_folder_list" },
688
842
  );
689
843
 
690
- // Tool 10: feishu_app_scopes
844
+ // Tool 10: feishu_doc_set_permission
845
+ api.registerTool(
846
+ {
847
+ name: "feishu_doc_set_permission",
848
+ label: "Feishu Doc Set Permission",
849
+ description:
850
+ "Set document sharing permission. Use 'tenant_editable' to allow organization members to edit via link.",
851
+ parameters: SetPermissionSchema,
852
+ async execute(_toolCallId, params) {
853
+ const { doc_token, permission } = params as { doc_token: string; permission: DocPermission };
854
+ try {
855
+ if (permission === "tenant_editable") {
856
+ await setDocPermissionTenantEditable(getClient(), doc_token);
857
+ return json({ success: true, permission: "tenant_editable" });
858
+ } else {
859
+ // Reset to private (only owner can access)
860
+ const client = getClient();
861
+ const res = await client.drive.permissionPublic.patch({
862
+ path: { token: doc_token },
863
+ params: { type: "docx" },
864
+ data: {
865
+ link_share_entity: "closed",
866
+ share_entity: "only_full_access",
867
+ },
868
+ });
869
+ if (res.code !== 0) throw new Error(res.msg);
870
+ return json({ success: true, permission: "private" });
871
+ }
872
+ } catch (err) {
873
+ return json({ error: err instanceof Error ? err.message : String(err) });
874
+ }
875
+ },
876
+ },
877
+ { name: "feishu_doc_set_permission" },
878
+ );
879
+
880
+ // Tool 11: feishu_wiki_read
881
+ api.registerTool(
882
+ {
883
+ name: "feishu_wiki_read",
884
+ label: "Feishu Wiki Read",
885
+ description:
886
+ "Read content from a Feishu wiki page. Extract wiki_token from URL /wiki/XXX. Returns wiki metadata and document content if the underlying type is docx.",
887
+ parameters: WikiTokenSchema,
888
+ async execute(_toolCallId, params) {
889
+ const { wiki_token } = params as { wiki_token: string };
890
+ try {
891
+ const result = await readWiki(getClient(), wiki_token);
892
+ return json(result);
893
+ } catch (err) {
894
+ return json({ error: err instanceof Error ? err.message : String(err) });
895
+ }
896
+ },
897
+ },
898
+ { name: "feishu_wiki_read" },
899
+ );
900
+
901
+ // Tool 12: feishu_wiki_spaces
902
+ api.registerTool(
903
+ {
904
+ name: "feishu_wiki_spaces",
905
+ label: "Feishu Wiki Spaces",
906
+ description: "List available wiki spaces (knowledge bases) the app has access to.",
907
+ parameters: Type.Object({}),
908
+ async execute() {
909
+ try {
910
+ const result = await listWikiSpaces(getClient());
911
+ return json(result);
912
+ } catch (err) {
913
+ return json({ error: err instanceof Error ? err.message : String(err) });
914
+ }
915
+ },
916
+ },
917
+ { name: "feishu_wiki_spaces" },
918
+ );
919
+
920
+ // Tool 13: feishu_wiki_nodes
921
+ api.registerTool(
922
+ {
923
+ name: "feishu_wiki_nodes",
924
+ label: "Feishu Wiki Nodes",
925
+ description:
926
+ "List wiki nodes (pages) in a space. Use parent_node_token to list children of a specific node.",
927
+ parameters: WikiSpaceIdSchema,
928
+ async execute(_toolCallId, params) {
929
+ const { space_id, parent_node_token } = params as {
930
+ space_id: string;
931
+ parent_node_token?: string;
932
+ };
933
+ try {
934
+ const result = await listWikiNodes(getClient(), space_id, parent_node_token);
935
+ return json(result);
936
+ } catch (err) {
937
+ return json({ error: err instanceof Error ? err.message : String(err) });
938
+ }
939
+ },
940
+ },
941
+ { name: "feishu_wiki_nodes" },
942
+ );
943
+
944
+ // Tool 14: feishu_app_scopes
691
945
  api.registerTool(
692
946
  {
693
947
  name: "feishu_app_scopes",
@@ -707,5 +961,5 @@ export function registerFeishuDocTools(api: OpenClawPluginApi) {
707
961
  { name: "feishu_app_scopes" },
708
962
  );
709
963
 
710
- api.logger.info?.(`feishu_doc: Registered 10 document tools`);
964
+ api.logger.info?.(`feishu_doc: Registered 14 document/wiki tools`);
711
965
  }