feishu-user-plugin 1.3.6 → 1.3.8

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 (71) hide show
  1. package/.claude-plugin/plugin.json +2 -2
  2. package/CHANGELOG.md +71 -0
  3. package/README.md +72 -41
  4. package/package.json +10 -3
  5. package/scripts/capture-feishu-protobuf.js +86 -0
  6. package/scripts/check-changelog.js +31 -0
  7. package/scripts/check-docs-sync.js +41 -0
  8. package/scripts/check-tool-count.js +40 -0
  9. package/scripts/check-version.js +40 -0
  10. package/scripts/decode-feishu-protobuf.js +115 -0
  11. package/scripts/smoke.js +224 -0
  12. package/scripts/sync-claude-md.sh +12 -0
  13. package/scripts/sync-server-json.js +71 -0
  14. package/scripts/sync-team-skills.sh +22 -0
  15. package/scripts/test-all-tools.js +158 -0
  16. package/scripts/test-wiki-attach-fallback.js +71 -0
  17. package/scripts/test-ws-events.js +84 -0
  18. package/skills/feishu-user-plugin/SKILL.md +5 -5
  19. package/skills/feishu-user-plugin/references/CLAUDE.md +248 -318
  20. package/skills/feishu-user-plugin/references/table.md +18 -9
  21. package/src/auth/cookie.js +30 -0
  22. package/src/auth/credentials.js +399 -0
  23. package/src/auth/profile-router.js +248 -0
  24. package/src/auth/uat.js +231 -0
  25. package/src/cli.js +45 -13
  26. package/src/clients/official/base.js +188 -0
  27. package/src/clients/official/bitable.js +269 -0
  28. package/src/clients/official/calendar.js +176 -0
  29. package/src/clients/official/contacts.js +54 -0
  30. package/src/clients/official/docs.js +301 -0
  31. package/src/clients/official/drive.js +77 -0
  32. package/src/clients/official/groups.js +68 -0
  33. package/src/clients/official/im.js +414 -0
  34. package/src/clients/official/index.js +30 -0
  35. package/src/clients/official/okr.js +127 -0
  36. package/src/clients/official/tasks.js +142 -0
  37. package/src/clients/official/uploads.js +260 -0
  38. package/src/clients/official/wiki.js +207 -0
  39. package/src/{client.js → clients/user.js} +25 -33
  40. package/src/config.js +13 -8
  41. package/src/events/event-buffer.js +100 -0
  42. package/src/events/index.js +5 -0
  43. package/src/events/ws-server.js +86 -0
  44. package/src/index.js +4 -1977
  45. package/src/logger.js +20 -0
  46. package/src/oauth.js +5 -1
  47. package/src/official.js +5 -1944
  48. package/src/prompts/_registry.js +69 -0
  49. package/src/prompts/index.js +54 -0
  50. package/src/server.js +305 -0
  51. package/src/setup.js +16 -1
  52. package/src/test-all.js +2 -2
  53. package/src/test-comprehensive.js +3 -3
  54. package/src/test-send.js +1 -1
  55. package/src/tools/_registry.js +31 -0
  56. package/src/tools/bitable.js +246 -0
  57. package/src/tools/calendar.js +207 -0
  58. package/src/tools/contacts.js +66 -0
  59. package/src/tools/diagnostics.js +172 -0
  60. package/src/tools/docs.js +158 -0
  61. package/src/tools/drive.js +111 -0
  62. package/src/tools/events.js +64 -0
  63. package/src/tools/groups.js +81 -0
  64. package/src/tools/im-read.js +259 -0
  65. package/src/tools/messaging-bot.js +151 -0
  66. package/src/tools/messaging-user.js +292 -0
  67. package/src/tools/okr.js +159 -0
  68. package/src/tools/profile.js +74 -0
  69. package/src/tools/tasks.js +168 -0
  70. package/src/tools/uploads.js +63 -0
  71. package/src/tools/wiki.js +191 -0
@@ -0,0 +1,168 @@
1
+ // src/tools/tasks.js — Feishu Tasks v2 tools (v1.3.7 new domain).
2
+ //
3
+ // 7 tools backed by clients/official/tasks.js. All UAT-first.
4
+ // Requires `task:task` scope on the OAuth — re-run `npx feishu-user-plugin oauth`
5
+ // after enabling the scope on the Feishu app console.
6
+
7
+ const { json, text } = require('./_registry');
8
+
9
+ const schemas = [
10
+ {
11
+ name: 'list_tasks',
12
+ description: '[Official API + UAT, v1.3.7] List the current user\'s tasks. Filter by completion or type.',
13
+ inputSchema: {
14
+ type: 'object',
15
+ properties: {
16
+ completed: { type: 'boolean', description: 'true → only completed; false → only pending; omit → all' },
17
+ type: { type: 'string', description: 'Filter by task type (optional). E.g. "all" / "personal".' },
18
+ page_size: { type: 'number', description: 'Items per page (default Feishu default)' },
19
+ page_token: { type: 'string', description: 'Pagination token' },
20
+ },
21
+ },
22
+ },
23
+ {
24
+ name: 'get_task',
25
+ description: '[Official API + UAT, v1.3.7] Get full details of a single task by GUID.',
26
+ inputSchema: {
27
+ type: 'object',
28
+ properties: {
29
+ task_guid: { type: 'string', description: 'Task GUID (from list_tasks / create_task / Feishu URL)' },
30
+ },
31
+ required: ['task_guid'],
32
+ },
33
+ },
34
+ {
35
+ name: 'create_task',
36
+ description: '[Official API + UAT, v1.3.7] Create a new task. summary is required; due / members / etc. are optional.',
37
+ inputSchema: {
38
+ type: 'object',
39
+ properties: {
40
+ summary: { type: 'string', description: 'Task title' },
41
+ description: { type: 'string', description: 'Task description (optional)' },
42
+ due: { type: 'object', description: 'Due time (optional). {timestamp:"<unix-millis>", is_all_day?:true|false}' },
43
+ members: {
44
+ type: 'array',
45
+ description: 'Initial members (optional). Each: {id:"<open_id>", role:"assignee"|"follower", type?:"user", name?:"..."}',
46
+ items: { type: 'object' },
47
+ },
48
+ repeat_rule: { type: 'string', description: 'Recurrence (optional, RFC5545 RRULE)' },
49
+ extra: { type: 'string', description: 'Free-form extra metadata (optional)' },
50
+ },
51
+ required: ['summary'],
52
+ },
53
+ },
54
+ {
55
+ name: 'update_task',
56
+ description: '[Official API + UAT, v1.3.7] Patch a task. **update_fields** is required by Feishu — list which fields to update (e.g. ["summary","due","completed_at"]).',
57
+ inputSchema: {
58
+ type: 'object',
59
+ properties: {
60
+ task_guid: { type: 'string', description: 'Task GUID' },
61
+ update_fields: {
62
+ type: 'array',
63
+ description: 'Required. Names of fields to update. E.g. ["summary","description","due","completed_at","start","extra","repeat_rule"]. Feishu only patches fields listed here, ignoring other keys in `task`.',
64
+ items: { type: 'string' },
65
+ },
66
+ task: {
67
+ type: 'object',
68
+ description: 'Field values. E.g. {summary:"new title", due:{timestamp:"1717939200000"}}.',
69
+ },
70
+ },
71
+ required: ['task_guid', 'update_fields', 'task'],
72
+ },
73
+ },
74
+ {
75
+ name: 'complete_task',
76
+ description: '[Official API + UAT, v1.3.7] Mark a task complete (or uncomplete it). Convenience wrapper around update_task with completed_at.',
77
+ inputSchema: {
78
+ type: 'object',
79
+ properties: {
80
+ task_guid: { type: 'string', description: 'Task GUID' },
81
+ completed: { type: 'boolean', description: 'true → mark complete (uses Date.now()); false → uncomplete (sets completed_at to "0"). Default true.' },
82
+ },
83
+ required: ['task_guid'],
84
+ },
85
+ },
86
+ {
87
+ name: 'delete_task',
88
+ description: '[Official API + UAT, v1.3.7] Permanently delete a task.',
89
+ inputSchema: {
90
+ type: 'object',
91
+ properties: {
92
+ task_guid: { type: 'string', description: 'Task GUID' },
93
+ },
94
+ required: ['task_guid'],
95
+ },
96
+ },
97
+ {
98
+ name: 'manage_task_members',
99
+ description: '[Official API + UAT, v1.3.7] Add or remove members on a task. Members are objects {id:"<open_id>", role:"assignee"|"follower", type?:"user", name?:""}.',
100
+ inputSchema: {
101
+ type: 'object',
102
+ properties: {
103
+ action: { type: 'string', enum: ['add', 'remove'], description: 'add or remove' },
104
+ task_guid: { type: 'string', description: 'Task GUID' },
105
+ members: {
106
+ type: 'array',
107
+ description: 'Members to add/remove. Each: {id, role, type?, name?}.',
108
+ items: { type: 'object' },
109
+ },
110
+ },
111
+ required: ['action', 'task_guid', 'members'],
112
+ },
113
+ },
114
+ ];
115
+
116
+ const handlers = {
117
+ async list_tasks(args, ctx) {
118
+ return json(await ctx.getOfficialClient().listTasks({
119
+ completed: args.completed,
120
+ type: args.type,
121
+ pageSize: args.page_size,
122
+ pageToken: args.page_token,
123
+ }));
124
+ },
125
+ async get_task(args, ctx) {
126
+ return json(await ctx.getOfficialClient().getTask(args.task_guid));
127
+ },
128
+ async create_task(args, ctx) {
129
+ const data = { summary: args.summary };
130
+ if (args.description !== undefined) data.description = args.description;
131
+ if (args.due !== undefined) data.due = args.due;
132
+ if (args.members !== undefined) data.members = args.members;
133
+ if (args.repeat_rule !== undefined) data.repeat_rule = args.repeat_rule;
134
+ if (args.extra !== undefined) data.extra = args.extra;
135
+ const r = await ctx.getOfficialClient().createTask(data);
136
+ const ownership = r.viaUser ? ' (as user)' : ' (as app — UAT unavailable or failed; task created by the app, not you)';
137
+ const warn = r.fallbackWarning ? `\n\n${r.fallbackWarning}` : '';
138
+ return text(`Task created${ownership}: ${r.task?.guid || '(no guid returned)'}\n${JSON.stringify(r.task, null, 2)}${warn}`);
139
+ },
140
+ async update_task(args, ctx) {
141
+ const r = await ctx.getOfficialClient().updateTask(args.task_guid, args.task, args.update_fields);
142
+ const warn = r.fallbackWarning ? `\n\n${r.fallbackWarning}` : '';
143
+ return text(`Task updated: ${args.task_guid}\n${JSON.stringify(r.task, null, 2)}${warn}`);
144
+ },
145
+ async complete_task(args, ctx) {
146
+ const completed = args.completed === undefined ? true : !!args.completed;
147
+ const r = await ctx.getOfficialClient().completeTask(args.task_guid, completed);
148
+ return text(`Task ${completed ? 'completed' : 'uncompleted'}: ${args.task_guid}`);
149
+ },
150
+ async delete_task(args, ctx) {
151
+ await ctx.getOfficialClient().deleteTask(args.task_guid);
152
+ return text(`Task deleted: ${args.task_guid}`);
153
+ },
154
+ async manage_task_members(args, ctx) {
155
+ const c = ctx.getOfficialClient();
156
+ if (args.action === 'add') {
157
+ const r = await c.addTaskMembers(args.task_guid, args.members);
158
+ return text(`Members added to ${args.task_guid}: ${args.members.length}\n${JSON.stringify(r.task?.members, null, 2)}`);
159
+ }
160
+ if (args.action === 'remove') {
161
+ const r = await c.removeTaskMembers(args.task_guid, args.members);
162
+ return text(`Members removed from ${args.task_guid}: ${args.members.length}\n${JSON.stringify(r.task?.members, null, 2)}`);
163
+ }
164
+ throw new Error('manage_task_members: action must be add or remove');
165
+ },
166
+ };
167
+
168
+ module.exports = { schemas, handlers };
@@ -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 };