feishu-user-plugin 1.3.6 → 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.
Files changed (55) hide show
  1. package/.claude-plugin/plugin.json +2 -2
  2. package/CHANGELOG.md +22 -0
  3. package/README.md +55 -40
  4. package/package.json +10 -3
  5. package/scripts/check-tool-count.js +15 -0
  6. package/scripts/check-version.js +40 -0
  7. package/scripts/smoke.js +224 -0
  8. package/scripts/sync-claude-md.sh +12 -0
  9. package/scripts/sync-team-skills.sh +22 -0
  10. package/scripts/test-all-tools.js +158 -0
  11. package/skills/feishu-user-plugin/SKILL.md +5 -5
  12. package/skills/feishu-user-plugin/references/CLAUDE.md +138 -99
  13. package/skills/feishu-user-plugin/references/table.md +18 -9
  14. package/src/auth/credentials.js +350 -0
  15. package/src/cli.js +42 -13
  16. package/src/clients/official/base.js +424 -0
  17. package/src/clients/official/bitable.js +269 -0
  18. package/src/clients/official/calendar.js +176 -0
  19. package/src/clients/official/contacts.js +54 -0
  20. package/src/clients/official/docs.js +301 -0
  21. package/src/clients/official/drive.js +77 -0
  22. package/src/clients/official/groups.js +68 -0
  23. package/src/clients/official/im.js +414 -0
  24. package/src/clients/official/index.js +30 -0
  25. package/src/clients/official/okr.js +127 -0
  26. package/src/clients/official/tasks.js +142 -0
  27. package/src/clients/official/uploads.js +260 -0
  28. package/src/clients/official/wiki.js +207 -0
  29. package/src/{client.js → clients/user.js} +23 -17
  30. package/src/index.js +4 -1977
  31. package/src/logger.js +20 -0
  32. package/src/oauth.js +5 -1
  33. package/src/official.js +5 -1944
  34. package/src/prompts/_registry.js +69 -0
  35. package/src/prompts/index.js +54 -0
  36. package/src/server.js +242 -0
  37. package/src/test-all.js +2 -2
  38. package/src/test-comprehensive.js +3 -3
  39. package/src/test-send.js +1 -1
  40. package/src/tools/_registry.js +30 -0
  41. package/src/tools/bitable.js +246 -0
  42. package/src/tools/calendar.js +207 -0
  43. package/src/tools/contacts.js +66 -0
  44. package/src/tools/diagnostics.js +172 -0
  45. package/src/tools/docs.js +158 -0
  46. package/src/tools/drive.js +111 -0
  47. package/src/tools/groups.js +81 -0
  48. package/src/tools/im-read.js +259 -0
  49. package/src/tools/messaging-bot.js +151 -0
  50. package/src/tools/messaging-user.js +292 -0
  51. package/src/tools/okr.js +159 -0
  52. package/src/tools/profile.js +43 -0
  53. package/src/tools/tasks.js +168 -0
  54. package/src/tools/uploads.js +63 -0
  55. package/src/tools/wiki.js +191 -0
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env node
2
+ // scripts/test-all-tools.js — semi-automated tool regression.
3
+ //
4
+ // Spawns the MCP server (src/index.js) as a stdio child, sends `initialize` +
5
+ // `tools/list`, then calls a curated set of READ tools to verify each domain
6
+ // is wired up. Writes a per-tool pass/fail summary to stdout.
7
+ //
8
+ // Read-only by design: this script does NOT create / modify / delete any
9
+ // Feishu resources. For write-tool regression, see docs/TESTING-METHODOLOGY.md
10
+ // "Live regression checklist".
11
+ //
12
+ // Usage:
13
+ // node scripts/test-all-tools.js
14
+ // node scripts/test-all-tools.js --user-id <open_id> # to also test list_user_okrs
15
+ // node scripts/test-all-tools.js --json # machine-readable output
16
+ //
17
+ // Exit code: 0 if all calls succeed, 1 if any failed.
18
+
19
+ const { spawn } = require('child_process');
20
+ const path = require('path');
21
+ const { readCredentials } = require('../src/config');
22
+
23
+ const SERVER_PATH = path.join(__dirname, '..', 'src', 'index.js');
24
+
25
+ function jsonrpc(id, method, params) {
26
+ return JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n';
27
+ }
28
+
29
+ function waitFor(fn, timeoutMs) {
30
+ return new Promise((resolve, reject) => {
31
+ const start = Date.now();
32
+ const tick = () => {
33
+ if (fn()) return resolve();
34
+ if (Date.now() - start > timeoutMs) return reject(new Error(`timeout after ${timeoutMs}ms`));
35
+ setTimeout(tick, 50);
36
+ };
37
+ tick();
38
+ });
39
+ }
40
+
41
+ async function runRegression() {
42
+ const cliArgs = process.argv.slice(2);
43
+ const wantJson = cliArgs.includes('--json');
44
+ const userIdIdx = cliArgs.indexOf('--user-id');
45
+ const userId = userIdIdx >= 0 ? cliArgs[userIdIdx + 1] : null;
46
+
47
+ const creds = readCredentials() || {};
48
+ const childEnv = { ...process.env };
49
+ for (const k of ['LARK_COOKIE', 'LARK_APP_ID', 'LARK_APP_SECRET', 'LARK_USER_ACCESS_TOKEN', 'LARK_USER_REFRESH_TOKEN', 'LARK_PROFILES_JSON']) {
50
+ if (creds[k] && !childEnv[k]) childEnv[k] = creds[k];
51
+ }
52
+
53
+ const child = spawn('node', [SERVER_PATH], {
54
+ stdio: ['pipe', 'pipe', 'pipe'],
55
+ env: childEnv,
56
+ });
57
+ let buf = '';
58
+ const responses = new Map();
59
+ child.stdout.on('data', (d) => {
60
+ buf += d.toString();
61
+ const lines = buf.split('\n');
62
+ buf = lines.pop();
63
+ for (const line of lines) {
64
+ if (!line.trim()) continue;
65
+ try {
66
+ const msg = JSON.parse(line);
67
+ if (msg.id != null) responses.set(msg.id, msg);
68
+ } catch {}
69
+ }
70
+ });
71
+ child.stderr.on('data', () => {});
72
+
73
+ let nextId = 1;
74
+ function call(method, params, timeoutMs = 15000) {
75
+ const id = nextId++;
76
+ child.stdin.write(jsonrpc(id, method, params));
77
+ return waitFor(() => responses.has(id), timeoutMs).then(() => responses.get(id));
78
+ }
79
+
80
+ const init = await call('initialize', {
81
+ protocolVersion: '2024-11-05',
82
+ capabilities: {},
83
+ clientInfo: { name: 'test-all-tools', version: '0' },
84
+ });
85
+ if (init.error) throw new Error(`initialize failed: ${JSON.stringify(init.error)}`);
86
+
87
+ const toolsResp = await call('tools/list', {});
88
+ const allTools = (toolsResp.result?.tools || []).map((t) => t.name).sort();
89
+
90
+ // Curated read-only suite. Each entry: [name, args, optional notes].
91
+ const SUITE = [
92
+ ['get_login_status', {}],
93
+ ['list_profiles', {}],
94
+ ['list_chats', { page_size: 5 }],
95
+ ['search_contacts', { query: 'feishu' }],
96
+ ['list_calendars', { page_size: 50 }],
97
+ ['list_okr_periods', {}],
98
+ ['list_wiki_spaces', {}],
99
+ ['search_docs', { query: 'README' }],
100
+ ['list_files', {}],
101
+ ];
102
+ if (userId) SUITE.push(['list_user_okrs', { user_id: userId, limit: 1 }]);
103
+ // list_tasks (v1.3.7) is safe but only meaningful if Tasks scope is granted.
104
+ if (allTools.includes('list_tasks')) SUITE.push(['list_tasks', { page_size: 1 }]);
105
+
106
+ const results = [];
107
+ for (const [name, args] of SUITE) {
108
+ if (!allTools.includes(name)) {
109
+ results.push({ tool: name, ok: false, skipped: true, reason: 'tool not registered' });
110
+ continue;
111
+ }
112
+ const t0 = Date.now();
113
+ try {
114
+ const r = await call('tools/call', { name, arguments: args }, 30000);
115
+ const ms = Date.now() - t0;
116
+ if (r.error) {
117
+ results.push({ tool: name, ok: false, ms, error: r.error.message });
118
+ } else {
119
+ const isError = r.result?.isError === true;
120
+ results.push({ tool: name, ok: !isError, ms, summary: summarize(r.result) });
121
+ }
122
+ } catch (e) {
123
+ results.push({ tool: name, ok: false, ms: Date.now() - t0, error: e.message });
124
+ }
125
+ }
126
+
127
+ child.kill('SIGTERM');
128
+
129
+ if (wantJson) {
130
+ process.stdout.write(JSON.stringify({ allTools: allTools.length, results }, null, 2) + '\n');
131
+ } else {
132
+ const okCount = results.filter((r) => r.ok).length;
133
+ const failCount = results.filter((r) => !r.ok && !r.skipped).length;
134
+ const skipCount = results.filter((r) => r.skipped).length;
135
+ console.log(`Tool registry size: ${allTools.length}`);
136
+ console.log(`Suite: ${okCount} ok, ${failCount} fail, ${skipCount} skipped (out of ${results.length} planned)\n`);
137
+ for (const r of results) {
138
+ const status = r.skipped ? 'SKIP' : (r.ok ? ' OK ' : 'FAIL');
139
+ const ms = r.ms !== undefined ? ` ${r.ms}ms` : '';
140
+ const tail = r.error ? ` — ${r.error}` : (r.reason ? ` — ${r.reason}` : (r.summary ? ` — ${r.summary}` : ''));
141
+ console.log(` [${status}]${ms.padStart(8)} ${r.tool}${tail}`);
142
+ }
143
+ if (failCount > 0) process.exit(1);
144
+ }
145
+ }
146
+
147
+ function summarize(result) {
148
+ const txt = result?.content?.[0]?.text;
149
+ if (!txt) return '';
150
+ // Crop multi-line summaries to the first line + length.
151
+ const firstLine = txt.split('\n', 1)[0];
152
+ return firstLine.length > 80 ? firstLine.slice(0, 77) + '…' : firstLine;
153
+ }
154
+
155
+ runRegression().catch((e) => {
156
+ console.error('Regression failed:', e.message);
157
+ process.exit(2);
158
+ });
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  name: feishu-user-plugin
3
- version: "1.3.6"
4
- description: "All-in-one Feishu plugin — send messages as yourself (incl. batch_send), read group/P2P chats (auto-expands merge_forward), manage docs/tables/wiki/drive (image + file blocks, drive uploads, bitable attachments), OKR, calendar, multi-profile. v1.3.6: upload completeness, sheets:spreadsheet scope, batch_send, multi-profile, send_card_as_user (bot-default)."
5
- allowed-tools: send_to_user, send_to_group, send_as_user, send_image_as_user, send_file_as_user, send_post_as_user, send_sticker_as_user, send_audio_as_user, batch_send, send_card_as_user, search_contacts, create_p2p_chat, get_chat_info, get_user_info, get_login_status, list_profiles, switch_profile, read_p2p_messages, list_user_chats, list_chats, read_messages, reply_message, forward_message, search_docs, read_doc, create_doc, create_doc_block, update_doc_block, list_bitable_tables, list_bitable_fields, search_bitable_records, batch_create_bitable_records, batch_update_bitable_records, upload_bitable_attachment, list_wiki_spaces, search_wiki, list_wiki_nodes, get_wiki_node, list_files, create_folder, upload_drive_file, find_user, download_image, download_file, list_user_okrs, get_okrs, list_okr_periods, list_calendars, list_calendar_events, get_calendar_event
3
+ version: "1.3.7"
4
+ description: "All-in-one Feishu plugin — send messages as yourself (incl. batch_send), read group/P2P chats (auto-expands merge_forward), manage docs/tables/wiki (full CRUD)/drive, OKR (with progress writes), calendar (read+write), Tasks v2, multi-profile. v1.3.7: tool consolidation (82→80), wiki write, OKR progress writes, calendar write, Tasks v2, oc_xxx auto-resolver for cookie sends."
5
+ allowed-tools: send_to_user, send_to_group, send_as_user, send_image_as_user, send_file_as_user, send_post_as_user, batch_send, send_card_as_user, search_contacts, create_p2p_chat, get_chat_info, get_user_info, get_login_status, list_profiles, switch_profile, read_p2p_messages, list_user_chats, list_chats, read_messages, send_message_as_bot, reply_message, forward_message, delete_message, update_message, add_reaction, delete_reaction, pin_message, create_group, update_group, list_members, manage_members, search_docs, read_doc, get_doc_blocks, create_doc, manage_doc_block, manage_bitable_app, manage_bitable_table, manage_bitable_field, manage_bitable_view, manage_bitable_record, upload_bitable_attachment, list_wiki_spaces, search_wiki, list_wiki_nodes, get_wiki_node, create_wiki_node, update_wiki_node, move_wiki_node, copy_wiki_node, delete_wiki_node, list_files, create_folder, upload_drive_file, manage_drive_file, upload_image, upload_file, download_message_resource, download_doc_image, list_user_okrs, get_okrs, list_okr_periods, create_okr_progress_record, list_okr_progress_records, delete_okr_progress_record, list_calendars, list_calendar_events, get_calendar_event, create_calendar_event, update_calendar_event, delete_calendar_event, respond_calendar_event, get_freebusy, list_tasks, get_task, create_task, update_task, complete_task, delete_task, manage_task_members
6
6
  user_invocable: true
7
7
  ---
8
8
 
9
9
  # Feishu User Plugin
10
10
 
11
11
  All-in-one Feishu plugin for Claude Code with three auth layers:
12
- - **User Identity** (cookie auth): Send messages as yourself — text, image, file, rich text, sticker, audio
12
+ - **User Identity** (cookie auth): Send messages as yourself — text, image, file, rich text (post)
13
13
  - **Official API** (app credentials): Read group messages, docs, tables, wiki, drive, contacts
14
14
  - **User OAuth** (user_access_token): Read P2P (direct message) chat history
15
15
 
@@ -43,7 +43,7 @@ Search users and groups by name, display results grouped by type.
43
43
  Search (search_docs), read (read_doc), create (create_doc) — three in one.
44
44
 
45
45
  ### /table — Bitable operations
46
- Query (list tables → list fields → search records), create records, update records.
46
+ Query (`manage_bitable_table(action=list)``manage_bitable_field(action=list)``manage_bitable_record(action=search)`), create / update / delete records via `manage_bitable_record(action=create|update|delete)`.
47
47
 
48
48
  ### /wiki — Wiki management
49
49
  List spaces (list_wiki_spaces), search content (search_wiki), browse nodes (list_wiki_nodes).
@@ -2,11 +2,29 @@
2
2
 
3
3
  ## What This Is
4
4
  All-in-one Feishu plugin for Claude Code with three auth layers:
5
- - **User Identity** (cookie auth): Send messages (text, image, file, post, sticker, audio) as yourself
5
+ - **User Identity** (cookie auth): Send messages (text, image, file, post) as yourself
6
6
  - **Official API** (app credentials): Read group messages, docs, tables, wiki, drive, contacts, upload files
7
7
  - **User OAuth UAT** (user_access_token): Read P2P chat history, list all user's chats
8
8
 
9
- ## Tool Categories (81 tools)
9
+ ## MCP Prompts (v1.3.7)
10
+
11
+ The 9 Claude Code skills are also exposed as MCP prompts (`prompts/list` + `prompts/get`) so Codex, Cursor, OpenClaw, and Windsurf — which cannot load Claude Code skills — get the same guided UX. Prompt bodies are read at server start from `skills/feishu-user-plugin/references/`.
12
+
13
+ | Prompt | Description |
14
+ |--------|-------------|
15
+ | `/send` | Send a message as yourself (non-bot) |
16
+ | `/reply` | Read recent messages and reply |
17
+ | `/digest` | Summarise recent group or P2P messages |
18
+ | `/search` | Search Feishu contacts or groups |
19
+ | `/doc` | Search, read, or create a Feishu document |
20
+ | `/table` | Operate on a Feishu Bitable (multi-dimensional table) |
21
+ | `/wiki` | Search and browse a Feishu Wiki space |
22
+ | `/drive` | List files or create folders in Feishu Drive |
23
+ | `/status` | Check all three auth layers (cookie, app, UAT) |
24
+
25
+ Each prompt accepts a single `arguments` free-form string (mirroring the `$ARGUMENTS` convention used by Claude Code skills). `status` has no arguments.
26
+
27
+ ## Tool Categories (80 tools)
10
28
 
11
29
  ### User Identity — Messaging (reverse-engineered, cookie-based)
12
30
  - `send_to_user` — Search user + send text (one step, most common). Returns candidates if multiple matches.
@@ -17,8 +35,7 @@ All-in-one Feishu plugin for Claude Code with three auth layers:
17
35
  - `send_file_as_user` — Send file (requires file_key from `upload_file`)
18
36
  - `send_post_as_user` — Send rich text with title + formatted paragraphs. Elements: `{tag:"text"}`, `{tag:"a",href,text}`, `{tag:"at",userId,name}`. **@-mentions trigger real notifications** (fixed by registering AT element IDs in RichText.atIds field 6 — reverse-engineered from Feishu Web bundle's AtProperty + RichText schemas).
19
37
  - `send_as_user` / `send_to_user` / `send_to_group` — plain text sends now accept optional `ats: [{userId, name}]`; the text must contain the `@<name>` marker for each entry. The marker is spliced into a real AT element so the mentioned user is notified. Identity is the cookie user (not bot).
20
- - `send_sticker_as_user` Send sticker/emoji
21
- - `send_audio_as_user` — Send audio message
38
+ - **Cookie sends accept oc_xxx chat IDs (v1.3.7 C1.4)**: `send_as_user`, `send_image_as_user`, `send_file_as_user`, `send_post_as_user`, `send_card_as_user`, and `batch_send` previously required numeric chat IDs from `create_p2p_chat` / cookie search. They now auto-resolve `oc_xxx` via `getChatInfo(name) → cookie search(name) → numeric id` and cache the mapping. Numeric IDs still work and skip the resolver. If resolution fails (chat not in your search index, no group with matching name), the tool throws a clear error with remediation guidance.
22
39
 
23
40
  ### User Identity — Contacts & Info
24
41
  - `search_contacts` — Search users/groups by name
@@ -30,37 +47,39 @@ All-in-one Feishu plugin for Claude Code with three auth layers:
30
47
  ### User OAuth UAT Tools (P2P chat reading + user-identity creation)
31
48
  - `read_p2p_messages` — Read P2P (direct message) chat history. chat_id accepts both numeric IDs (from create_p2p_chat) and oc_xxx format. Returns newest messages first by default.
32
49
  - `list_user_chats` — List group chats the user is in. Note: API only returns groups, not P2P. For P2P, use: `search_contacts` → `create_p2p_chat` → `read_p2p_messages`.
33
- - **All docx + bitable + drive create/read/write tools are UAT-first**: when UAT is configured, every operation (create/edit/delete doc blocks, bitable tables/fields/views/records, drive folders) tries the user's token first and falls back to app token on failure. This keeps resources consistently owned by the user and avoids 403 errors when the app can't access user-created resources. Read-only tools (e.g. `read_doc`, `get_doc_blocks`, `list_bitable_tables`) are also UAT-first so user-owned resources remain readable.
50
+ - **All docx + bitable + drive create/read/write tools are UAT-first**: when UAT is configured, every operation (create/edit/delete doc blocks, bitable tables/fields/views/records, drive folders) tries the user's token first and falls back to app token on failure. This keeps resources consistently owned by the user and avoids 403 errors when the app can't access user-created resources. Read-only tools (e.g. `read_doc`, `get_doc_blocks`, `manage_bitable_table(action=list)`) are also UAT-first so user-owned resources remain readable.
34
51
 
35
52
  ### Official API Tools (app credentials)
36
53
  - `list_chats` / `read_messages` — Chat history (read_messages accepts chat name, oc_ ID, or numeric ID; auto-resolves via bot's group list → im.chat.search → search_contacts). **Auto-falls back to UAT for external groups the bot cannot access.** Returns newest messages first by default. Messages include sender names. **v1.3.5**: `merge_forward` messages now auto-expand into their child messages (2 images + 4 texts, with original sender / time / origin chat preserved); text messages get `urls[]` + `feishuDocs[]` extracted so agents can feed them straight into `read_doc` / WebFetch. Disable expansion with `expand_merge_forward=false`.
37
54
  - `send_message_as_bot` — Bot sends message to any chat (text, post, interactive, etc.)
38
- - `reply_message` / `forward_message` — Message operations (as bot)
39
- - `delete_message` / `update_message` — Recall or edit bot's own messages
55
+ - `reply_message` / `forward_message` — Message operations (as bot). `forward_message` accepts `receive_id_type` (chat_id/open_id/union_id/user_id/email; auto-detects when omitted by inspecting the receive_id prefix).
56
+ - `delete_message` / `update_message` — Recall or edit bot's own messages. `update_message` only supports `msg_type=text` or `interactive` (Feishu API limit; other types are rejected with a clear error before hitting the API).
40
57
  - `add_reaction` / `delete_reaction` — Emoji reactions on messages
41
58
  - `pin_message` — Pin or unpin a message (pinned=true/false)
42
59
  - `create_group` / `update_group` — Create and manage group chats
43
- - `list_members` / `manage_members` — Group membership (manage_members: action=add/remove)
60
+ - `list_members` / `manage_members` — Group membership (manage_members: action=add/remove, member_id_type=open_id|union_id|user_id — default open_id; pass union_id/user_id explicitly when your member_ids use those formats, otherwise Feishu rejects with code 9499)
44
61
  - `search_docs` / `read_doc` / `get_doc_blocks` / `create_doc` — Document operations
45
- - `create_doc_block` / `update_doc_block` / `delete_doc_blocks` Document content editing (insert/update/delete blocks)
46
- - `create_bitable` / `get_bitable_meta` / `copy_bitable` — Bitable app management (create, get info, copy)
47
- - `list_bitable_tables` / `create_bitable_table` / `update_bitable_table` / `delete_bitable_table` — Table management (CRUD + rename)
48
- - `list_bitable_fields` / `create_bitable_field` / `update_bitable_field` / `delete_bitable_field` Field (column) management
49
- - `list_bitable_views` / `create_bitable_view` / `delete_bitable_view` View management (grid, kanban, gallery, form, gantt, calendar)
50
- - `search_bitable_records` / `get_bitable_record` Query records
51
- - `batch_create_bitable_records` / `batch_update_bitable_records` / `batch_delete_bitable_records` — Record CRUD (single or batch, max 500/call)
52
- - `list_wiki_spaces` / `search_wiki` / `list_wiki_nodes` / `get_wiki_node` — Wiki (v1.3.4 adds `get_wiki_node` which resolves a wiki node token to its underlying `obj_type` + `obj_token`, so you can feed the node straight into `read_doc`, bitable tools, etc.)
62
+ - `manage_doc_block(action=create|update|delete)` — Document content editing (v1.3.7 consolidates v1.3.6 create_doc_block / update_doc_block / delete_doc_blocks). Image + file shortcuts (`image_path`/`image_token`/`file_path`/`file_token`) flow through unchanged.
63
+ - `manage_bitable_app(action=create|copy|get_meta)` — Bitable app management. v1.3.7 consolidates create_bitable / copy_bitable / get_bitable_meta.
64
+ - `manage_bitable_table(action=list|create|update|delete)` — Table CRUD + rename.
65
+ - `manage_bitable_field(action=list|create|update|delete)` Field (column) management. Feishu requires `type` for both create AND update (rename).
66
+ - `manage_bitable_view(action=list|create|delete)` — Views (grid / kanban / gallery / form / gantt / calendar).
67
+ - `manage_bitable_record(action=search|get|create|update|delete)` — Record CRUD. create/update/delete accept arrays (single or up to 500 per call).
68
+ - `list_wiki_spaces` / `search_wiki` / `list_wiki_nodes` / `get_wiki_node` Wiki read (v1.3.4 adds `get_wiki_node` which resolves a wiki node token to its underlying `obj_type` + `obj_token`, so you can feed the node straight into `read_doc`, bitable tools, etc. v1.3.7 hardens this: `get_wiki_node` now also accepts underlying `obj_token`s from `search_wiki` (synthesizes a node-shape so callers don't have to know which ID space they hold), and `list_wiki_spaces` is UAT-first with a `scopeHint` field surfaced when the bot returns an empty list — typically because `wiki:wiki:readonly` is missing or the bot was never invited.)
69
+ - `create_wiki_node` / `update_wiki_node` / `move_wiki_node` / `copy_wiki_node` / `delete_wiki_node` — Wiki write (v1.3.7). UAT-first so resources are owned by the user. `create_wiki_node` builds a fresh `doc/sheet/bitable/mindnote/file/docx/slides` inside a wiki space (or a `node_type=shortcut` pointer to an existing node). `update_wiki_node` renames (only `title` is updatable via wiki API; content edits go through docx/bitable/sheet tools). `move_wiki_node` and `copy_wiki_node` accept `target_parent_token` + optional `target_space_id` to re-parent within the same space or migrate to another. `delete_wiki_node` calls `DELETE /open-apis/wiki/v2/spaces/{space_id}/nodes/{token}` via raw REST (the SDK doesn't type it); **only the wiki node pointer is removed the underlying drive resource is NOT deleted**, follow up with `manage_drive_file(action=delete, type=...)` if you also want the resource gone.
53
70
  - `list_files` / `create_folder` — Drive
54
- - `copy_file` / `move_file` / `delete_file` Drive file operations (copy, move, delete)
71
+ - `manage_drive_file(action=copy|move|delete)` — Drive file operations (v1.3.7 consolidates v1.3.6 copy_file / move_file / delete_file). UAT-first. `type` is always required (`file/folder/docx/sheet/bitable/mindnote/slides`) Feishu rejects with 1061002 / 1062501 otherwise.
55
72
  - `upload_image` / `upload_file` — Upload image/file, returns key for send_image/send_file
56
73
  - `upload_drive_file` — Upload a local file into a Drive folder (`drive/v1/files/upload_all`, `parent_type=explorer`). Returns `file_token` + `url`. If `wiki_space_id` is provided, the upload is followed by `attachToWiki(obj_type=file)` so the file lands as a Wiki node atomically. UAT-first with bot fallback.
57
- - `upload_bitable_attachment` — Upload a local file as a Bitable attachment (`drive/v1/medias/upload_all` with `parent_type=bitable_image` or `bitable_file`). Returns `file_token` to write into an Attachment-type field via `batch_create/update_bitable_records` as `[{file_token: "..."}]`.
74
+ - `upload_bitable_attachment` — Upload a local file as a Bitable attachment (`drive/v1/medias/upload_all` with `parent_type=bitable_image` or `bitable_file`). Returns `file_token` to write into an Attachment-type field via `manage_bitable_record(action=create|update, records=[{fields:{<attachment_field>:[{file_token:"..."}]}}])`.
58
75
  - `send_card_as_user` — Send a Feishu interactive card. **v1.3.6 default routes through bot identity** (the `as_user` suffix is reserved for the v1.3.7 reverse-engineered cookie path; default flips when that lands). Pass `card` JSON; `via="user"` returns an explicit deferred error in v1.3.6.
59
- - `download_image` — Download an image and return it as MCP image content so the model **sees the pixels**. Two modes: (1) **message image** pass `message_id` + `image_key` from read_messages. (2) **docx image** — pass `image_token` (from `get_doc_blocks` image block) and optionally `doc_token` (native id / wiki node / Feishu URL). Tries UAT first, falls back to app token. **merge_forward children**: use the child's `parentMessageId` (NOT the child id) — Feishu returns `File not in msg` with the child id.
60
- - `download_file` — Download a file (msg_type=file) attachment. Returns base64 + mimeType + byte count; optional `save_path` writes the file to disk. Same parent-id rule for merge_forward children as download_image.
76
+ - `download_message_resource` — Download a message-attached image OR file. v1.3.7 (C2.4) consolidates v1.3.6 download_image (message-mode) + download_file. Args: `message_id`, `key` (image_key or file_key), `kind=image|file`, optional `save_path`. **Payloads > 2 MiB MUST pass save_path** — the Anthropic API rejects responses > 5 MB; we cap at 2 MiB so the inline image / base64 has multipart headroom. Tries UAT first, falls back to app. **merge_forward children**: use the child's `parentMessageId` (NOT the child id) — Feishu returns `File not in msg` with the child id.
77
+ - `download_doc_image` — Download an image embedded in a docx document so the model sees pixels. Args: `image_token` (from `get_doc_blocks` image block), optional `doc_token` (native id / wiki node / Feishu URL — recommended for permission scoping), optional `save_path`. Same 2 MiB inline cap as `download_message_resource`. UAT-first.
61
78
  - `list_user_okrs` / `get_okrs` / `list_okr_periods` — OKR read. UAT-first (works for the authenticated user's OKRs) with app fallback when OKR scope is granted.
79
+ - `create_okr_progress_record` / `list_okr_progress_records` / `delete_okr_progress_record` — OKR progress writes (v1.3.7). UAT-first. Requires `okr:okr.content:write` scope. `create_okr_progress_record` accepts a simplified `content_text` (auto-wrapped into the Feishu block schema) plus optional `source_title` / `source_url` / `progress_percent`. `list_okr_progress_records` extracts progress_record IDs from `get_okrs` since Feishu has no native list endpoint.
62
80
  - `list_calendars` / `list_calendar_events` / `get_calendar_event` — Calendar read. UAT-first (primary + shared + subscribed); app identity only sees calendars the bot was explicitly invited to.
63
- - `find_user` — Contact lookup by email/mobile
81
+ - `create_calendar_event` / `update_calendar_event` / `delete_calendar_event` / `respond_calendar_event` / `get_freebusy` Calendar write (v1.3.7). UAT-first. Requires `calendar:calendar.event:write` scope (re-run `npx feishu-user-plugin oauth` after enabling on the app console). `get_freebusy` is a query, not a write, but groups here for the calendar domain.
82
+ - `list_tasks` / `get_task` / `create_task` / `update_task` / `complete_task` / `delete_task` / `manage_task_members` — Task v2 (new domain in v1.3.7). UAT-first. Requires `task:task` scope. v2 uses `task_guid` as the identifier (not numeric task_id like v1). `update_task` requires an explicit `update_fields` array (Feishu only patches the listed fields). `complete_task(completed=true|false)` is a convenience wrapper around `update_task` setting `completed_at`.
64
83
 
65
84
  ## Usage Patterns
66
85
 
@@ -72,14 +91,14 @@ All docx and bitable tools now accept three input forms for their `document_id`
72
91
  The plugin resolves wiki nodes to their underlying `obj_token` via `getWikiNode`, then calls the normal docx / bitable endpoint. Results are cached for 10 min to avoid repeated node lookups.
73
92
 
74
93
  Create content directly into a Wiki space:
75
- - `create_doc` / `create_bitable` accept optional `wiki_space_id` (+ `wiki_parent_node_token`). The plugin creates the resource in drive, then calls `wiki/v2/spaces/{space_id}/nodes/move_docs_to_wiki` to attach it — returns `wikiNodeToken` on immediate success, or `wikiAttachTaskId` if Feishu queues the move.
94
+ - `create_doc` / `manage_bitable_app(action=create)` accept optional `wiki_space_id` (+ `wiki_parent_node_token`). The plugin creates the resource in drive, then calls `wiki/v2/spaces/{space_id}/nodes/move_docs_to_wiki` to attach it — returns `wikiNodeToken` on immediate success, or `wikiAttachTaskId` if Feishu queues the move.
76
95
 
77
96
  ### Document images
78
- Read — `download_image` with `doc_token` + `image_token` returns the image as MCP image content (base64 + mimeType). `doc_token` accepts native id / wiki node / URL.
79
- Write — `create_doc_block` now has image shortcuts:
97
+ Read — `download_doc_image(image_token, doc_token?, save_path?)` returns the image as MCP image content (base64 + mimeType). `doc_token` accepts native id / wiki node / URL. Force `save_path` when image > 2 MiB.
98
+ Write — `manage_doc_block(action=create)` has image shortcuts:
80
99
  - `image_path` (absolute local file path) → plugin creates an image block, uploads the pixels via `drive/v1/medias/upload_all`, and patches the block with the uploaded token.
81
100
  - `image_token` (already uploaded) → plugin creates block and attaches token.
82
- `update_doc_block` accepts `image_token` to swap the picture in an existing image block.
101
+ `manage_doc_block(action=update, image_token=...)` swaps the picture in an existing image block.
83
102
 
84
103
  ### OKR
85
104
  1. `list_okr_periods` — find the period id for current quarter.
@@ -87,10 +106,30 @@ Write — `create_doc_block` now has image shortcuts:
87
106
  3. `get_okrs(okr_ids)` — batch fetch full objective + key result structure with progress + alignments.
88
107
  `user_id` is required — use your own open_id (from `get_login_status` / `search_contacts`) to read your own OKRs, or a colleague's open_id for theirs (subject to permissions).
89
108
 
109
+ Write (v1.3.7, requires `okr:okr.content:write` scope):
110
+ 4. `create_okr_progress_record(target_id, target_type=1|2, content_text, source_title?, source_url?, progress_percent?)` — `target_type` is 1 for objectives, 2 for key results. `content_text` is auto-wrapped into Feishu's required block format; pass `content` directly for richer payloads (lists, mentions, docs links, gallery).
111
+ 5. `list_okr_progress_records(okr_id)` — extracts `{progress_id, target_id, target_type}` triples from `get_okrs` (Feishu has no native list endpoint).
112
+ 6. `delete_okr_progress_record(progress_id)`.
113
+
90
114
  ### Calendar
91
115
  1. `list_calendars` — get your calendars; the one with `type=primary` is your personal calendar.
92
116
  2. `list_calendar_events(calendar_id, start_time=<unix_sec>, end_time=<unix_sec>)` — list events in a time window.
93
117
  3. `get_calendar_event(calendar_id, event_id)` — full details (attendees, location, attachments, meeting link).
118
+ 4. `create_calendar_event(calendar_id, summary, start_time, end_time, ...)` — `start_time` / `end_time` are objects: `{timestamp:"<unix-seconds>", timezone?:"Asia/Shanghai"}` or `{date:"YYYY-MM-DD"}` for all-day. v1.3.7+ requires `calendar:calendar.event:write` scope.
119
+ 5. `update_calendar_event(calendar_id, event_id, ...patch)` — pass only the fields to change.
120
+ 6. `delete_calendar_event(calendar_id, event_id, need_notification?)` — pass `meeting_chat_id` to also dissolve the linked meeting chat if any.
121
+ 7. `respond_calendar_event(calendar_id, event_id, rsvp_status=accept|decline|tentative)` — RSVP as the current UAT identity.
122
+ 8. `get_freebusy(time_min, time_max, user_ids=[...])` — freebusy windows in RFC3339; useful for finding meeting slots.
123
+
124
+ ### Tasks (v2, v1.3.7)
125
+ Whole new domain. Identifier is `task_guid` (not numeric task_id like v1). Requires `task:task` scope.
126
+ 1. `list_tasks(completed?, type?)` — current user's tasks, paginated.
127
+ 2. `get_task(task_guid)` — full details.
128
+ 3. `create_task(summary, due?, members?, ...)` — at minimum `summary`; `due` is `{timestamp:"<unix-millis>", is_all_day?}`.
129
+ 4. `update_task(task_guid, update_fields=["summary","due","completed_at"], task={...})` — Feishu only patches the listed fields.
130
+ 5. `complete_task(task_guid, completed=true|false)` — convenience for the completed_at toggle.
131
+ 6. `delete_task(task_guid)`.
132
+ 7. `manage_task_members(action=add|remove, task_guid, members=[{id,role:"assignee"|"follower",type?:"user",name?}])`.
94
133
 
95
134
  ### External-group message read (hardened in v1.3.4)
96
135
  `read_messages` and `read_p2p_messages` now expose a `via` field in the response (`"bot"`, `"user"`, or `"contacts"`) so callers can tell which identity actually read the data. When bot fails with a known code (external tenant / no permission / not in chat) the plugin hops straight to UAT; transient errors (rate limit / 5xx / ECONNRESET / fetch timeout) retry once with a 2 s delay before falling back. When UAT isn't configured, the error message now tells the user to run `npx feishu-user-plugin oauth` instead of leaking the raw Feishu payload.
@@ -111,15 +150,15 @@ Write — `create_doc_block` now has image shortcuts:
111
150
  - Get chat details → `get_chat_info` (supports both oc_xxx and numeric ID)
112
151
 
113
152
  ### Bitable (Multi-dimensional Tables)
114
- - Create a bitable from scratch `create_bitable` `create_bitable_table` `create_bitable_field`
115
- - Get bitable info → `get_bitable_meta`
116
- - Copy a bitable → `copy_bitable` with name and optional folder
117
- - Query data → `list_bitable_tables` `list_bitable_fields` → `search_bitable_records`
118
- - Rename table → `update_bitable_table` with new name
119
- - Read single record → `get_bitable_record`
120
- - Create/update/delete records → `batch_create_bitable_records` / `batch_update_bitable_records` / `batch_delete_bitable_records` (works for single or up to 500)
121
- - Manage fields → `create_bitable_field` / `update_bitable_field` (requires type param) / `delete_bitable_field`
122
- - Manage views → `create_bitable_view` (type: grid/kanban/gallery/form/gantt/calendar) / `delete_bitable_view`
153
+ All bitable ops collapse into 5 `manage_bitable_*` tools (v1.3.7) pick the action.
154
+ - Create from scratch → `manage_bitable_app(action=create)` → `manage_bitable_table(action=create)` → `manage_bitable_field(action=create)`
155
+ - Get info → `manage_bitable_app(action=get_meta)`
156
+ - Duplicate → `manage_bitable_app(action=copy, name=..., folder_id?)`
157
+ - Query `manage_bitable_table(action=list)` → `manage_bitable_field(action=list)` `manage_bitable_record(action=search, filter?, sort?, page_size?)`
158
+ - Read single record → `manage_bitable_record(action=get, record_id=...)`
159
+ - Records CRUD → `manage_bitable_record(action=create|update|delete, records|record_ids=[...])` (single or up to 500/call)
160
+ - Fields → `manage_bitable_field(action=create|update|delete, ...)` `type` required for both create AND update (rename)
161
+ - Views → `manage_bitable_view(action=list|create|delete, view_type=grid|kanban|gallery|form|gantt|calendar)`
123
162
 
124
163
  ### Group Management
125
164
  - Create a group → `create_group` with name and optional member open_ids
@@ -127,12 +166,13 @@ Write — `create_doc_block` now has image shortcuts:
127
166
  - List members → `list_members`
128
167
 
129
168
  ### Document Editing
130
- - Create doc with content `create_doc` `create_doc_block` (use document_id as parent_block_id for root)
131
- - Edit existing block → `get_doc_blocks` to find block_id `update_doc_block`
132
- - Delete blocks → `delete_doc_blocks` with start/end index range
133
- - Insert image → `create_doc_block` with `image_path` (local file) or `image_token` (already uploaded). Three-step flow handled internally.
134
- - Insert file attachment (PDF/zip/xlsx/...) → `create_doc_block` with `file_path` or `file_token`. Feishu auto-wraps the FILE block (block_type=23) inside a VIEW container (block_type=33); the plugin walks into the inner file block automatically before the `replace_file` PATCH so the upload + attach succeed.
135
- - Replace existing image/file in a block `update_doc_block` with `image_token` / `file_token`.
169
+ All block ops go through one tool: `manage_doc_block(action=create|update|delete, ...)`.
170
+ - Create doc with content → `create_doc` `manage_doc_block(action=create, parent_block_id=document_id, children=[...])`
171
+ - Edit existing block → `get_doc_blocks` to find block_id → `manage_doc_block(action=update, block_id=..., update_body={...})`
172
+ - Delete blocks → `manage_doc_block(action=delete, parent_block_id=..., start_index=..., end_index=...)`
173
+ - Insert image → `manage_doc_block(action=create, parent_block_id=..., image_path=...)` (local file) or `image_token=...` (already uploaded). Three-step flow handled internally.
174
+ - Insert file attachment (PDF/zip/xlsx/...) `manage_doc_block(action=create, file_path=...)` or `file_token=...`. Feishu auto-wraps the FILE block (block_type=23) inside a VIEW container (block_type=33); the plugin walks into the inner file block automatically before the `replace_file` PATCH so the upload + attach succeed.
175
+ - Replace existing image/file → `manage_doc_block(action=update, block_id=..., image_token=... | file_token=...)`.
136
176
 
137
177
  ### Diagnostics
138
178
  - Diagnose issues → `get_login_status` first
@@ -148,13 +188,25 @@ To register more profiles, set `LARK_PROFILES_JSON` in the MCP env:
148
188
  ```
149
189
 
150
190
  ## Auth & Session
151
- - **LARK_COOKIE**: Required for user identity tools. Session auto-refreshed every 4h via heartbeat and persisted to config.
191
+ - **LARK_COOKIE**: Required for user identity tools. Session auto-refreshed every 4h via heartbeat and persisted to credentials store.
152
192
  - **LARK_APP_ID + LARK_APP_SECRET**: Required for official API tools.
153
- - **LARK_USER_ACCESS_TOKEN + LARK_USER_REFRESH_TOKEN**: Required for P2P reading. Auto-refreshed on expiry (error codes 99991668/99991663/99991677). Token auto-persisted to MCP config on refresh.
193
+ - **LARK_USER_ACCESS_TOKEN + LARK_USER_REFRESH_TOKEN**: Required for P2P reading. Auto-refreshed on expiry (error codes 99991668/99991663/99991677). Token auto-persisted to credentials store on refresh.
154
194
  - Cookie expiry: sl_session has 12h max-age, auto-refreshed by heartbeat every 4h.
155
195
  - UAT expiry: 2h, auto-refreshed via refresh_token.
156
196
  - Refresh token expiry: 7 days. Use `keepalive` cron to prevent expiration.
157
197
 
198
+ ### Credentials store (v1.3.7+)
199
+ Single source of truth at `~/.feishu-user-plugin/credentials.json` (mode 0600). Schema documented at `docs/CREDENTIALS-FORMAT.md`. The MCP server reads from this file when present; cookie heartbeat and UAT refresh persist back to it atomically. Multiple harnesses (Claude Code, Codex) sharing the same file see token rotations consistently — no more "Codex still has the old UAT" drift after a refresh in Claude Code.
200
+
201
+ Opt-in migration:
202
+ ```bash
203
+ npx feishu-user-plugin migrate # dry-run (default) — prints what would be written
204
+ npx feishu-user-plugin migrate --confirm # writes credentials.json
205
+ ```
206
+ After migration the harness env blocks remain as backward-compat fallback. Delete `~/.feishu-user-plugin/credentials.json` to revert to legacy behaviour.
207
+
208
+ Backward compat: v1.3.6 users without credentials.json see zero behaviour change. The credentials file is preferred only when it exists. The MCP server's `Auth:` startup line on stderr now shows the source (`credentials.json profile=default` vs `env vars (legacy)`) so you can tell at a glance which path is active.
209
+
158
210
  ## Required Environment Variables (ALL are required for full functionality)
159
211
 
160
212
  | Variable | Purpose |
@@ -338,7 +390,7 @@ Two known root causes, both fixed in v1.3.3:
338
390
  - **Manual cleanup when you notice**: `pkill -f 'feishu-user-plugin/src/index.js'` — the client will respawn one fresh process on the next tool call.
339
391
 
340
392
  ### If a create_* tool warns "UAT failed, created as BOT"
341
- - v1.3.5 added an explicit `⚠️` warning to MCP responses whenever `_asUserOrApp` silently fell back to bot identity for a write (create_doc / create_bitable / create_folder / create_doc_block / ...). Before v1.3.5 this was silent and led to the "teammate can read my 'private' doc" issue.
393
+ - v1.3.5 added an explicit `⚠️` warning to MCP responses whenever `_asUserOrApp` silently fell back to bot identity for a write (create_doc / manage_bitable_app(action=create) / create_folder / manage_doc_block(action=create) / ...). Before v1.3.5 this was silent and led to the "teammate can read my 'private' doc" issue.
342
394
  - **Cause**: your UAT is failing (expired / scope missing / race) so the plugin reached for bot credentials. The resulting resource is owned by the shared bot, tenant-readable by default, NOT by you.
343
395
  - **Fix**: run `npx feishu-user-plugin oauth` and restart Claude Code / Codex. If the resource needs to be yours, delete the bot-owned copy and recreate after UAT is valid.
344
396
 
@@ -394,59 +446,42 @@ NPM_TOKEN is stored as a GitHub repo secret.
394
446
 
395
447
  **IMPORTANT: team-skills 仓库禁止直接推送 main。所有变更必须走 PR。**
396
448
 
397
- team-skills 推送规范:
398
- 1. **创建 feature branch**: `git checkout -b fix/feishu-xxx` `sync/feishu-v1.x.x`
399
- 2. **提交变更并推送 branch**: `git push -u origin <branch-name>`
400
- 3. **创建 PR 并设置 auto-merge**: `gh pr create --title "..." --body "..."` 然后 `gh pr merge <number> --auto --merge`
401
- 4. **CI 通过后自动合并**: validate workflow 检查三方版本一致性,通过即自动 merge,无需手动操作
402
- 5. **如 CI 失败**: 修复后 push 到同一 branch,CI 会重跑,通过后自动合并
449
+ What is automatic now (Phase B3 hooks):
450
+ - **pre-commit (this repo)**: any change to `CLAUDE.md` auto-syncs `AGENTS.md` + `skills/feishu-user-plugin/references/CLAUDE.md` (script: `scripts/sync-claude-md.sh`).
451
+ - **post-merge (this repo, on main)**: copies `skills/` + `.claude-plugin/plugin.json` into `team-skills/plugins/feishu-user-plugin/`, creates `sync/feishu-v<version>` branch, opens a PR with `--auto --merge` (script: `scripts/sync-team-skills.sh`).
403
452
 
404
- 三方版本一致性规则:
405
- - `plugins/feishu-user-plugin/.claude-plugin/plugin.json` `version`
406
- - `plugins/feishu-user-plugin/skills/feishu-user-plugin/SKILL.md` frontmatter `version`
407
- - `plugins/feishu-user-plugin/README.md` 更新日志里第一个 `### vX.Y.Z` 标题
408
- - 这三个版本号必须相同,否则 CI 会失败。每次 npm 发包后,team-skills 的版本号也要同步更新。
453
+ What still needs a manual touch in team-skills:
454
+ - `README.md` — team-skills has its own README (with team-shared APP_ID/SECRET hardcoded). Tool count, changelog, install prompt all need hand edits.
455
+ - `skills/feishu-user-plugin/SKILL.md` version + `allowed-tools` list.
409
456
 
410
- 同步内容(每次发版后执行):
457
+ team-skills PR 流程:
458
+ 1. 创建 branch: `git checkout -b sync/feishu-v1.x.x` 或 `fix/feishu-xxx`
459
+ 2. push branch + `gh pr create` + `gh pr merge <number> --auto --merge`
460
+ 3. CI (`validate.yml`) checks the three-way version triangle (`plugin.json` / `SKILL.md` / first `### vX.Y.Z` in README) — must match or CI fails.
461
+ 4. If CI fails: fix + push to same branch, CI re-runs, auto-merge proceeds.
462
+
463
+ Manual sync fallback (hook failed / dry-run / first-time):
411
464
  ```bash
412
- # 1. 同步 skills + plugin.json
413
- cp CLAUDE.md skills/feishu-user-plugin/references/CLAUDE.md
414
- cp -r skills/ /Users/abble/team-skills/plugins/feishu-user-plugin/skills/
465
+ # CLAUDE.md AGENTS.md + skill ref now handled by pre-commit hook
466
+ cp -r skills/. /Users/abble/team-skills/plugins/feishu-user-plugin/skills/
415
467
  cp .claude-plugin/plugin.json /Users/abble/team-skills/plugins/feishu-user-plugin/.claude-plugin/
416
- # 2. 手动更新 team-skills 的 README.md(工具数、更新日志)和 SKILL.md(version + allowed-tools)
417
- # 3. 走 PR 流程推送
418
468
  # Do NOT copy .mcp.json — team-skills plugin should not have one
419
469
  ```
420
470
 
421
471
  ## Development Workflow
422
472
 
423
473
  ### Keeping all docs in sync
424
- When making ANY code change (new tools, bug fixes, features), update ALL of these:
425
474
 
426
- **本仓库内:**
475
+ When making ANY code change (new tools, bug fixes, features), update these in this repo:
427
476
  - `CLAUDE.md` — tool count, tool list, usage patterns, known limitations
428
- - `AGENTS.md` — **每次改 CLAUDE.md 必须同步**:正文(第 2 行起)与 CLAUDE.md 完全一致,仅首行标题保留 `# feishu-user-plugin — Codex Instructions`。同步命令:`tail -n +2 CLAUDE.md > /tmp/body.md && { echo "# feishu-user-plugin — Codex Instructions"; cat /tmp/body.md; } > AGENTS.md`
429
- - `README.md` — tool count (badge + heading + tool table), feature highlights, OpenClaw/Claude Code config examples
477
+ - `README.md` — tool count badge + heading + tool table, feature highlights, OpenClaw/Claude Code config examples
430
478
  - `ROADMAP.md` — check off completed items, add new findings
431
- - `package.json` — version, description (tool count)
432
- - `skills/feishu-user-plugin/references/CLAUDE.md` — always copy from root: `cp CLAUDE.md skills/feishu-user-plugin/references/CLAUDE.md`
433
- - `prompts/openclaw-setup.md` — if OpenClaw 相关配置变了要更新
479
+ - `package.json` — version + description (tool count). All three of `package.json`, `.claude-plugin/plugin.json`, and `skills/feishu-user-plugin/SKILL.md` must agree on version (CI enforces).
480
+ - `prompts/openclaw-setup.md` — only if OpenClaw config changed
434
481
 
435
- **team-skills 仓库 (`/Users/abble/team-skills/plugins/feishu-user-plugin/`):**
436
- - `skills/` — 同步技能文件: `cp -r skills/ /Users/abble/team-skills/plugins/feishu-user-plugin/skills/`
437
- - `README.md` — team-skills 有自己的 README(含团队 APP_ID/SECRET),需要同步更新:工具数量、功能列表、更新日志、安装 prompt
438
- - 两个 README 都必须包含 Claude Code 安装 prompt 和 OpenClaw 安装 prompt
439
- - team-skills README 的安装 prompt 包含团队共享的 APP_ID/SECRET(hardcoded),本仓库 README 用占位符
482
+ `AGENTS.md` (Codex) and `skills/feishu-user-plugin/references/CLAUDE.md` are auto-derived from `CLAUDE.md` by the pre-commit hook — do **not** edit them by hand.
440
483
 
441
- **同步命令(每次发版后执行):**
442
- ```bash
443
- # 1. 同步 skills + plugin.json
444
- cp CLAUDE.md skills/feishu-user-plugin/references/CLAUDE.md
445
- cp -r skills/ /Users/abble/team-skills/plugins/feishu-user-plugin/skills/
446
- cp .claude-plugin/plugin.json /Users/abble/team-skills/plugins/feishu-user-plugin/.claude-plugin/
447
- # 2. 手动更新 team-skills README(工具数、功能列表、更新日志)+ SKILL.md(version + allowed-tools)
448
- # 3. 走 PR 流程推送 team-skills(禁止直接推 main)
449
- ```
484
+ For team-skills repo: see [Syncing to team-skills](#syncing-to-team-skills) above. Bottom line: `skills/` + `plugin.json` auto-sync via post-merge hook; team-skills README + SKILL.md still need manual edits per release.
450
485
 
451
486
  ### Keeping ROADMAP.md up to date
452
487
  - When completing a feature or fixing a bug, check the corresponding item in ROADMAP.md as `[x]` done
@@ -454,19 +489,25 @@ cp .claude-plugin/plugin.json /Users/abble/team-skills/plugins/feishu-user-plugi
454
489
  - When a version is released (tag pushed), move completed items under the "已完成" section with the version number
455
490
  - When researching a direction and deciding not to implement, add it to "已调研但暂不实施" with the reasoning
456
491
 
457
- ### When adding new tools
458
- 1. Add method to `src/official.js`(Official API)or `src/client.js`(Cookie 身份)
459
- 2. Add tool definition to `TOOLS` array in `src/index.js`
460
- 3. Add handler case in `handleTool()` switch in `src/index.js`
461
- 4. Run `node -c src/official.js && node -c src/index.js` to verify syntax
462
- 5. Update this file (CLAUDE.md) tool count, tool list, usage patterns
463
- 6. Update ROADMAP.md if relevant
492
+ ### When adding new tools (post-v1.3.7 layout)
493
+ 1. Add the underlying API method to the right domain file:
494
+ - Official API `src/clients/official/<domain>.js` (im, docs, bitable, drive, wiki, calendar, okr, uploads, contacts, groups). Cross-domain helpers stay in `src/clients/official/base.js`.
495
+ - Cookie identity `src/clients/user.js`.
496
+ 2. Add the MCP tool schema + handler to `src/tools/<domain>.js`. Each module exports `{ schemas: [...], handlers: { [name]: async (args, ctx) => MCPResponse } }` — see existing tools for the pattern. Handlers receive `ctx` (factories, profile state, resolveDocId — see `src/tools/_registry.js` docstring).
497
+ 3. If the new file is a brand-new domain (rare), also append it to the `TOOL_MODULES` list in `src/server.js`.
498
+ 4. Run smoke: `npm run smoke:baseline` to update the baseline (only when adding/removing/renaming tools is intentional), then `npm run smoke` to verify no other regression. For pure body changes (no schema delta) just `npm run smoke` should pass against the existing baseline.
499
+ 5. `node -c` lint each touched file.
500
+ 6. Update this file (CLAUDE.md) — tool count, tool list, usage patterns. See `docs/REFACTOR-NOTES.md` for the file-responsibility matrix.
501
+ 7. Update ROADMAP.md if relevant.
464
502
 
465
503
  ### When fixing bugs
466
504
  1. Write a standalone test script (`node -e "..."`) to reproduce the bug before fixing
467
505
  2. After fixing, verify with the same script
468
506
  3. If the bug affects MCP tool behavior, test via MCP tool call after server restart
469
507
 
508
+ ### Testing methodology
509
+ See `docs/TESTING-METHODOLOGY.md` for the full regression playbook (when to use unit / smoke / live MCP / `scripts/test-all-tools.js`). The semi-automated path is `node scripts/test-all-tools.js`; the smoke gate is `npm run smoke` (regenerate baseline with `npm run smoke:baseline` only when a tool schema delta is intentional).
510
+
470
511
  ### Commit conventions
471
512
  - `feat:` new tools or capabilities
472
513
  - `fix:` bug fixes
@@ -526,7 +567,7 @@ feishu-user-plugin vX.Y.Z 发布
526
567
 
527
568
  **写作规范**:
528
569
  - **开篇**:一到两句陈述式总结,不宣传、不夸大。参考 v1.3.2:"本次更新主要补齐了 X 能力,并修复了 Y 问题;同时将 Z 统一调整为 ..."
529
- - **每条 bullet**:先写用户可见现象,再写底层机制。引用具体错误码(如 1770032 / 91403)、接口名(如 create_doc_block)、参数名(如 RichText.atIds)——专业读者信赖的是细节
570
+ - **每条 bullet**:先写用户可见现象,再写底层机制。引用具体错误码(如 1770032 / 91403)、接口名(如 manage_doc_block)、参数名(如 RichText.atIds)——专业读者信赖的是细节
530
571
  - **字符**:bullet 用 `•`(U+2022),不用 `-` 或 `*`;代码/工具名在正文中直接写,不加反引号
531
572
  - **禁用**:emoji、🔴🟡🟢 之类严重度标记、`@` 任何人、营销词("强大"、"全新"、"重磅")、夸张修辞
532
573
  - **语气**:技术 release note 的中性语气,像写给同行的内部更新。参考 v1.3.2 全文
@@ -538,13 +579,6 @@ feishu-user-plugin vX.Y.Z 发布
538
579
 
539
580
  **发送前**:始终先用 `send_to_user` 或类似工具发给用户自己审核,或直接以文本形式贴在对话里等用户批准。用户说"发"才调 `send_post_as_user` 到目标群。
540
581
 
541
- ### Syncing to team-skills (after any CLAUDE.md or skills change)
542
- 1. Copy CLAUDE.md to skill reference: `cp CLAUDE.md skills/feishu-user-plugin/references/CLAUDE.md`
543
- 2. Sync to team-skills repo: `cp -r skills/ /Users/abble/team-skills/plugins/feishu-user-plugin/skills/`
544
- 3. Also sync plugin.json: `cp .claude-plugin/plugin.json /Users/abble/team-skills/plugins/feishu-user-plugin/.claude-plugin/`
545
- 4. Update SKILL.md version + allowed-tools, README.md changelog + tool count
546
- 5. **走 PR 流程**(创建 branch → push → PR → 等 CI 通过 → merge),禁止直接推 main
547
-
548
582
  ### Testing a tool
549
583
  - For Official API tools: can test directly via MCP tool call or standalone script using `readCredentials()` from `src/config.js`
550
584
  - For Cookie tools: need active session, test via MCP tool call
@@ -557,8 +591,11 @@ The v1.3.4 tools require additional scopes on the app + UAT:
557
591
  | Feature | Scopes to enable on app + include in OAuth |
558
592
  |---------|-------------------------------------------|
559
593
  | OKR read | `okr:okr:readonly`, `okr:period:read` |
594
+ | OKR progress write (v1.3.7: create/delete_okr_progress_record) | `okr:okr.content:write` |
560
595
  | Calendar read | `calendar:calendar:readonly`, `calendar:calendar.event:read` |
561
- | Docx/Bitable/Drive media upload (`uploadMedia`, `upload_drive_file`, `upload_bitable_attachment`, `create_doc_block` image/file) | `drive:drive`, `drive:file:upload`, `docs:document.media:upload`, `sheets:spreadsheet` (only for sheet uploads) |
596
+ | Calendar write (v1.3.7: create/update/delete/respond_calendar_event) | `calendar:calendar.event:write` |
597
+ | Tasks v2 (v1.3.7: list/get/create/update/complete/delete_task, manage_task_members) | `task:task` |
598
+ | Docx/Bitable/Drive media upload (`uploadMedia`, `upload_drive_file`, `upload_bitable_attachment`, `manage_doc_block(action=create, image_path|file_path|...)`) | `drive:drive`, `drive:file:upload`, `docs:document.media:upload`, `sheets:spreadsheet` (only for sheet uploads) |
562
599
  | Wiki attach (`move_docs_to_wiki`) | `wiki:wiki` (edit scope, the readonly one is insufficient) |
563
600
 
564
601
  If a tool returns `access_denied` or error code `99991672` (scope not granted), the scope is missing on either the app or the UAT. Re-run `npx feishu-user-plugin oauth` so the UAT picks up the latest scope list (defined in `src/oauth.js`).
@@ -568,6 +605,8 @@ If a tool returns `access_denied` or error code `99991672` (scope not granted),
568
605
  - External tenant users may not be resolvable via `get_user_info` (contact API scope limitation)
569
606
  - Cookie auth requires human interaction (QR scan) — cannot be fully automated
570
607
  - Refresh token expires after 7 days without use — set up `keepalive` cron to prevent this
571
- - `update_bitable_field` requires `type` parameter even when only changing field name (Feishu API requirement)
572
- - `list_wiki_spaces` may return empty if bot lacks `wiki:wiki:readonly` permission
608
+ - `manage_bitable_field(action=update)` requires `type` parameter even when only changing field name (Feishu API requirement)
609
+ - `list_wiki_spaces` may return empty if bot lacks `wiki:wiki:readonly` permission (v1.3.7+: `scopeHint` field is appended to the response when this happens)
610
+ - `delete_wiki_node` calls an undocumented-in-SDK endpoint (`DELETE /wiki/v2/spaces/{id}/nodes/{token}`); v1.3.7 ships it because Feishu's API console exposes it, but if Feishu retires the endpoint the tool will fail with a clear 404 — fall back to `manage_drive_file(action=delete)` on the underlying obj_token in that case.
573
611
  - `search_wiki` uses same API as `search_docs` — `docs_types` filter may not work as expected
612
+ - `send_image_as_user` is currently broken: Feishu's cookie protobuf gateway rejects the simple `{imageKey}` content payload (HTTP 400) because the Feishu Web client encodes images with extra metadata (image dimensions, mime type, etc.) that we don't have in `proto/lark.proto`. Reverse-engineering needs Chrome DevTools traffic capture and is deferred to v1.3.8. v1.3.7 surfaces a clear error pointing to `send_message_as_bot(msg_type="image", ...)` as the workaround. (`send_file_as_user` and `send_post_as_user` work fine — only IMAGE is affected.)