feishu-user-plugin 1.3.5 → 1.3.7
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/.claude-plugin/plugin.json +2 -2
- package/CHANGELOG.md +22 -0
- package/README.md +66 -40
- package/package.json +10 -3
- package/scripts/check-tool-count.js +15 -0
- package/scripts/check-version.js +40 -0
- package/scripts/smoke.js +224 -0
- package/scripts/sync-claude-md.sh +12 -0
- package/scripts/sync-team-skills.sh +22 -0
- package/scripts/test-all-tools.js +158 -0
- package/skills/feishu-user-plugin/SKILL.md +5 -5
- package/skills/feishu-user-plugin/references/CLAUDE.md +152 -96
- package/skills/feishu-user-plugin/references/table.md +18 -9
- package/src/auth/credentials.js +350 -0
- package/src/cli.js +42 -13
- package/src/clients/official/base.js +424 -0
- package/src/clients/official/bitable.js +269 -0
- package/src/clients/official/calendar.js +176 -0
- package/src/clients/official/contacts.js +54 -0
- package/src/clients/official/docs.js +301 -0
- package/src/clients/official/drive.js +77 -0
- package/src/clients/official/groups.js +68 -0
- package/src/clients/official/im.js +414 -0
- package/src/clients/official/index.js +30 -0
- package/src/clients/official/okr.js +127 -0
- package/src/clients/official/tasks.js +142 -0
- package/src/clients/official/uploads.js +260 -0
- package/src/clients/official/wiki.js +207 -0
- package/src/{client.js → clients/user.js} +23 -17
- package/src/doc-blocks.js +20 -5
- package/src/index.js +4 -1744
- package/src/logger.js +20 -0
- package/src/oauth.js +8 -1
- package/src/official.js +5 -1734
- package/src/prompts/_registry.js +69 -0
- package/src/prompts/index.js +54 -0
- package/src/server.js +242 -0
- package/src/test-all.js +2 -2
- package/src/test-comprehensive.js +3 -3
- package/src/test-send.js +1 -1
- package/src/tools/_registry.js +30 -0
- package/src/tools/bitable.js +246 -0
- package/src/tools/calendar.js +207 -0
- package/src/tools/contacts.js +66 -0
- package/src/tools/diagnostics.js +172 -0
- package/src/tools/docs.js +158 -0
- package/src/tools/drive.js +111 -0
- package/src/tools/groups.js +81 -0
- package/src/tools/im-read.js +259 -0
- package/src/tools/messaging-bot.js +151 -0
- package/src/tools/messaging-user.js +292 -0
- package/src/tools/okr.js +159 -0
- package/src/tools/profile.js +43 -0
- package/src/tools/tasks.js +168 -0
- package/src/tools/uploads.js +63 -0
- package/src/tools/wiki.js +191 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// src/tools/uploads.js — upload helpers (image, file, bitable attachment).
|
|
2
|
+
|
|
3
|
+
const { text, json } = require('./_registry');
|
|
4
|
+
|
|
5
|
+
const schemas = [
|
|
6
|
+
{
|
|
7
|
+
name: 'upload_image',
|
|
8
|
+
description: '[Official API] Upload an image file to Feishu. Returns image_key for use with send_image_as_user.',
|
|
9
|
+
inputSchema: {
|
|
10
|
+
type: 'object',
|
|
11
|
+
properties: {
|
|
12
|
+
image_path: { type: 'string', description: 'Absolute path to the image file on disk' },
|
|
13
|
+
image_type: { type: 'string', enum: ['message', 'avatar'], description: 'Image usage type (default: message)' },
|
|
14
|
+
},
|
|
15
|
+
required: ['image_path'],
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: 'upload_file',
|
|
20
|
+
description: '[Official API] Upload a file to Feishu. Returns file_key for use with send_file_as_user.',
|
|
21
|
+
inputSchema: {
|
|
22
|
+
type: 'object',
|
|
23
|
+
properties: {
|
|
24
|
+
file_path: { type: 'string', description: 'Absolute path to the file on disk' },
|
|
25
|
+
file_type: { type: 'string', enum: ['opus', 'mp4', 'pdf', 'doc', 'xls', 'ppt', 'stream'], description: 'File type (default: stream for generic files)' },
|
|
26
|
+
file_name: { type: 'string', description: 'Display file name (optional, defaults to basename)' },
|
|
27
|
+
},
|
|
28
|
+
required: ['file_path'],
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: 'upload_bitable_attachment',
|
|
33
|
+
description: '[Official API] Upload a file as a Bitable attachment (drive/v1/medias/upload_all with parent_type=bitable_image or bitable_file). Returns file_token suitable for writing into a Bitable Attachment-type field via batch_create/update_bitable_records (the field value should be [{file_token}]).',
|
|
34
|
+
inputSchema: {
|
|
35
|
+
type: 'object',
|
|
36
|
+
properties: {
|
|
37
|
+
app_token: { type: 'string', description: 'Bitable app token (the bascn... or basc... id)' },
|
|
38
|
+
file_path: { type: 'string', description: 'Absolute path to the file on disk' },
|
|
39
|
+
kind: { type: 'string', enum: ['image', 'file'], description: 'Whether the attachment is an image (bitable_image) or a generic file (bitable_file). Default: file.' },
|
|
40
|
+
},
|
|
41
|
+
required: ['app_token', 'file_path'],
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const handlers = {
|
|
47
|
+
async upload_image(args, ctx) {
|
|
48
|
+
const r = await ctx.getOfficialClient().uploadImage(args.image_path, args.image_type);
|
|
49
|
+
return text(`Image uploaded: ${r.imageKey}\nUse this image_key with send_image_as_user to send it.`);
|
|
50
|
+
},
|
|
51
|
+
async upload_file(args, ctx) {
|
|
52
|
+
const r = await ctx.getOfficialClient().uploadFile(args.file_path, args.file_type, args.file_name);
|
|
53
|
+
return text(`File uploaded: ${r.fileKey}\nUse this file_key with send_file_as_user to send it.`);
|
|
54
|
+
},
|
|
55
|
+
async upload_bitable_attachment(args, ctx) {
|
|
56
|
+
const kind = args.kind === 'image' ? 'bitable_image' : 'bitable_file';
|
|
57
|
+
const appToken = await ctx.resolveDocId(args.app_token);
|
|
58
|
+
const up = await ctx.getOfficialClient().uploadMedia(args.file_path, appToken, kind);
|
|
59
|
+
return json({ fileToken: up.fileToken, viaUser: up.viaUser, parentType: kind, hint: `Pass [{ file_token: "${up.fileToken}" }] as the value of an Attachment-type Bitable field.` });
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
module.exports = { schemas, handlers };
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// src/tools/wiki.js — Wiki space + node read tools.
|
|
2
|
+
|
|
3
|
+
const { json } = require('./_registry');
|
|
4
|
+
|
|
5
|
+
const schemas = [
|
|
6
|
+
{
|
|
7
|
+
name: 'list_wiki_spaces',
|
|
8
|
+
description: '[Official API] List all accessible Wiki spaces.',
|
|
9
|
+
inputSchema: { type: 'object', properties: {} },
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
name: 'search_wiki',
|
|
13
|
+
description: '[Official API] Search Wiki nodes by keyword.',
|
|
14
|
+
inputSchema: {
|
|
15
|
+
type: 'object',
|
|
16
|
+
properties: { query: { type: 'string', description: 'Search keyword' } },
|
|
17
|
+
required: ['query'],
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: 'list_wiki_nodes',
|
|
22
|
+
description: '[Official API] List nodes in a Wiki space.',
|
|
23
|
+
inputSchema: {
|
|
24
|
+
type: 'object',
|
|
25
|
+
properties: {
|
|
26
|
+
space_id: { type: 'string', description: 'Wiki space ID' },
|
|
27
|
+
parent_node_token: { type: 'string', description: 'Parent node token (optional)' },
|
|
28
|
+
},
|
|
29
|
+
required: ['space_id'],
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'get_wiki_node',
|
|
34
|
+
description: '[Official API] Resolve a Wiki node token to its underlying object (docx / bitable / sheet / mindnote / file). Returns obj_type + obj_token + space_id so you can read/write the real resource via the usual docx / bitable tools. Accepts bare wiki node token (wikcnXXX), an underlying obj_token (docxXXX / bascnXXX from search_wiki), or a full Feishu /wiki/ URL — the handler tries the wiki endpoint first and falls back to a synthesized node-shape for non-wiki tokens.',
|
|
35
|
+
inputSchema: {
|
|
36
|
+
type: 'object',
|
|
37
|
+
properties: {
|
|
38
|
+
node_token: { type: 'string', description: 'Wiki node token (wikcnXXX / wikmXXX / wiknXXX), underlying obj_token (docxXXX / bascnXXX), or full Feishu /wiki/<token> URL' },
|
|
39
|
+
},
|
|
40
|
+
required: ['node_token'],
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'create_wiki_node',
|
|
45
|
+
description: '[Official API] Create a new Wiki node inside a space. obj_type picks the underlying resource (doc/sheet/bitable/mindnote/file/docx/slides). UAT-first so the resource is owned by the user.',
|
|
46
|
+
inputSchema: {
|
|
47
|
+
type: 'object',
|
|
48
|
+
properties: {
|
|
49
|
+
space_id: { type: 'string', description: 'Wiki space ID (from list_wiki_spaces)' },
|
|
50
|
+
obj_type: { type: 'string', enum: ['doc', 'sheet', 'bitable', 'mindnote', 'file', 'docx', 'slides'], description: 'Underlying resource type' },
|
|
51
|
+
title: { type: 'string', description: 'Node title (optional; Feishu generates a default if absent)' },
|
|
52
|
+
parent_node_token: { type: 'string', description: 'Parent wiki node under which to create (optional; root if omitted)' },
|
|
53
|
+
node_type: { type: 'string', enum: ['origin', 'shortcut'], description: 'origin = real resource, shortcut = pointer to existing node (default: origin)', default: 'origin' },
|
|
54
|
+
origin_node_token: { type: 'string', description: 'Required when node_type=shortcut — the wiki node this shortcut points at' },
|
|
55
|
+
},
|
|
56
|
+
required: ['space_id', 'obj_type'],
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: 'update_wiki_node',
|
|
61
|
+
description: '[Official API] Rename a Wiki node (only `title` is updatable via the wiki API; the underlying resource content is edited via docx/bitable/sheet tools).',
|
|
62
|
+
inputSchema: {
|
|
63
|
+
type: 'object',
|
|
64
|
+
properties: {
|
|
65
|
+
space_id: { type: 'string', description: 'Wiki space ID' },
|
|
66
|
+
node_token: { type: 'string', description: 'Wiki node token (wikcnXXX)' },
|
|
67
|
+
title: { type: 'string', description: 'New title' },
|
|
68
|
+
},
|
|
69
|
+
required: ['space_id', 'node_token', 'title'],
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'move_wiki_node',
|
|
74
|
+
description: '[Official API] Move a Wiki node to a different parent (within the same space) or to a different space. Pass at least one of target_parent_token / target_space_id.',
|
|
75
|
+
inputSchema: {
|
|
76
|
+
type: 'object',
|
|
77
|
+
properties: {
|
|
78
|
+
space_id: { type: 'string', description: 'Source space ID' },
|
|
79
|
+
node_token: { type: 'string', description: 'Wiki node token to move' },
|
|
80
|
+
target_parent_token: { type: 'string', description: 'New parent wiki node token (optional)' },
|
|
81
|
+
target_space_id: { type: 'string', description: 'New target space ID (optional; same-space move if omitted)' },
|
|
82
|
+
},
|
|
83
|
+
required: ['space_id', 'node_token'],
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: 'copy_wiki_node',
|
|
88
|
+
description: '[Official API] Deep-copy a Wiki node into a different location (and optionally a different space). Underlying resource is duplicated.',
|
|
89
|
+
inputSchema: {
|
|
90
|
+
type: 'object',
|
|
91
|
+
properties: {
|
|
92
|
+
space_id: { type: 'string', description: 'Source space ID' },
|
|
93
|
+
node_token: { type: 'string', description: 'Wiki node token to copy' },
|
|
94
|
+
target_parent_token: { type: 'string', description: 'Destination parent wiki node token (optional)' },
|
|
95
|
+
target_space_id: { type: 'string', description: 'Destination space ID (optional; same-space copy if omitted)' },
|
|
96
|
+
title: { type: 'string', description: 'Title for the copy (optional; defaults to source title)' },
|
|
97
|
+
},
|
|
98
|
+
required: ['space_id', 'node_token'],
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: 'delete_wiki_node',
|
|
103
|
+
description: '[Official API, v1.3.7] Delete a Wiki node. Calls `DELETE /open-apis/wiki/v2/spaces/{space_id}/nodes/{node_token}`. The Feishu SDK does not type this endpoint, so the call goes through raw REST (UAT-first; bot fallback uses `client.request`). **The underlying drive resource (docx / sheet / bitable / file) is NOT deleted** — Feishu treats wiki nodes as pointers. To delete the actual resource as well, follow up with `manage_drive_file(action=delete, type=<obj_type>, file_token=<obj_token>)` (use `get_wiki_node` first to get obj_type / obj_token).',
|
|
104
|
+
inputSchema: {
|
|
105
|
+
type: 'object',
|
|
106
|
+
properties: {
|
|
107
|
+
space_id: { type: 'string', description: 'Wiki space ID' },
|
|
108
|
+
node_token: { type: 'string', description: 'Wiki node token to delete' },
|
|
109
|
+
},
|
|
110
|
+
required: ['space_id', 'node_token'],
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
const { parseFeishuInput } = require('../resolver');
|
|
116
|
+
|
|
117
|
+
const handlers = {
|
|
118
|
+
async list_wiki_spaces(_args, ctx) {
|
|
119
|
+
return json(await ctx.getOfficialClient().listWikiSpaces());
|
|
120
|
+
},
|
|
121
|
+
async search_wiki(args, ctx) {
|
|
122
|
+
return json(await ctx.getOfficialClient().searchWiki(args.query));
|
|
123
|
+
},
|
|
124
|
+
async list_wiki_nodes(args, ctx) {
|
|
125
|
+
return json(await ctx.getOfficialClient().listWikiNodes(args.space_id, { parentNodeToken: args.parent_node_token }));
|
|
126
|
+
},
|
|
127
|
+
async create_wiki_node(args, ctx) {
|
|
128
|
+
return json(await ctx.getOfficialClient().createWikiNode(args.space_id, {
|
|
129
|
+
obj_type: args.obj_type,
|
|
130
|
+
node_type: args.node_type || 'origin',
|
|
131
|
+
parent_node_token: args.parent_node_token,
|
|
132
|
+
origin_node_token: args.origin_node_token,
|
|
133
|
+
title: args.title,
|
|
134
|
+
}));
|
|
135
|
+
},
|
|
136
|
+
async update_wiki_node(args, ctx) {
|
|
137
|
+
return json(await ctx.getOfficialClient().updateWikiNodeTitle(args.space_id, args.node_token, args.title));
|
|
138
|
+
},
|
|
139
|
+
async move_wiki_node(args, ctx) {
|
|
140
|
+
return json(await ctx.getOfficialClient().moveWikiNode(args.space_id, args.node_token, {
|
|
141
|
+
target_parent_token: args.target_parent_token,
|
|
142
|
+
target_space_id: args.target_space_id,
|
|
143
|
+
}));
|
|
144
|
+
},
|
|
145
|
+
async copy_wiki_node(args, ctx) {
|
|
146
|
+
return json(await ctx.getOfficialClient().copyWikiNode(args.space_id, args.node_token, {
|
|
147
|
+
target_parent_token: args.target_parent_token,
|
|
148
|
+
target_space_id: args.target_space_id,
|
|
149
|
+
title: args.title,
|
|
150
|
+
}));
|
|
151
|
+
},
|
|
152
|
+
async delete_wiki_node(args, ctx) {
|
|
153
|
+
return json(await ctx.getOfficialClient().deleteWikiNode(args.space_id, args.node_token));
|
|
154
|
+
},
|
|
155
|
+
async get_wiki_node(args, ctx) {
|
|
156
|
+
const parsed = parseFeishuInput(args.node_token);
|
|
157
|
+
const token = (parsed.kind === 'wiki' || parsed.kind === 'raw') ? parsed.token : args.node_token;
|
|
158
|
+
try {
|
|
159
|
+
return json(await ctx.getOfficialClient().getWikiNode(token));
|
|
160
|
+
} catch (e) {
|
|
161
|
+
// search_wiki returns underlying obj_tokens (docxXXX / bascnXXX), which
|
|
162
|
+
// wiki.v2.getNode rejects. Detect the wiki-only error codes and return
|
|
163
|
+
// a synthesized node-shape so callers can pass either token kind.
|
|
164
|
+
const msg = String(e.message || '');
|
|
165
|
+
if (/95300\d|invalid.*token|node.*not.*found/i.test(msg)) {
|
|
166
|
+
const objType = inferObjTypeFromToken(token);
|
|
167
|
+
if (objType) {
|
|
168
|
+
return json({
|
|
169
|
+
obj_type: objType,
|
|
170
|
+
obj_token: token,
|
|
171
|
+
note: `Token does not look like a wiki node token; treating as a direct ${objType} obj_token. Pass it to ${objType === 'bitable' ? 'list_bitable_tables / search_bitable_records' : objType === 'docx' ? 'read_doc / get_doc_blocks' : 'the matching read tool'} directly.`,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
throw e;
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
function inferObjTypeFromToken(token) {
|
|
181
|
+
if (!token || typeof token !== 'string') return null;
|
|
182
|
+
if (token.startsWith('docx')) return 'docx';
|
|
183
|
+
if (token.startsWith('doccn') || token.startsWith('doc')) return 'doc';
|
|
184
|
+
if (token.startsWith('bascn') || token.startsWith('bas')) return 'bitable';
|
|
185
|
+
if (token.startsWith('shtcn') || token.startsWith('sht')) return 'sheet';
|
|
186
|
+
if (token.startsWith('mind') || token.startsWith('mn')) return 'mindnote';
|
|
187
|
+
if (token.startsWith('boxcn') || token.startsWith('boxbn')) return 'file';
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
module.exports = { schemas, handlers };
|