feishu-user-plugin 1.3.8 → 1.3.10

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 (42) hide show
  1. package/.claude-plugin/plugin.json +12 -2
  2. package/CHANGELOG.md +100 -12
  3. package/README.en.md +610 -0
  4. package/README.md +292 -532
  5. package/package.json +12 -5
  6. package/proto/lark.proto +10 -0
  7. package/scripts/explore-card-protobuf.js +144 -0
  8. package/scripts/explore-image-minimize.js +163 -0
  9. package/scripts/generate-og-image.js +39 -0
  10. package/scripts/generate-release-artifacts.js +318 -0
  11. package/scripts/probe-feishu-docx.js +203 -0
  12. package/scripts/sync-team-skills.sh +109 -7
  13. package/skills/feishu-user-plugin/SKILL.md +76 -4
  14. package/skills/feishu-user-plugin/references/CLAUDE.md +74 -54
  15. package/src/auth/credentials.js +36 -0
  16. package/src/cli.js +86 -45
  17. package/src/clients/user.js +15 -13
  18. package/src/events/cursor.js +103 -0
  19. package/src/events/event-buffer.js +8 -5
  20. package/src/events/event-log.js +151 -0
  21. package/src/events/index.js +8 -1
  22. package/src/events/lockfile.js +126 -0
  23. package/src/events/owner.js +73 -0
  24. package/src/events/ws-server.js +95 -25
  25. package/src/oauth.js +48 -7
  26. package/src/resolver.js +10 -0
  27. package/src/server.js +248 -29
  28. package/src/setup.js +99 -25
  29. package/src/test-all.js +12 -9
  30. package/src/test-events-cursor.js +56 -0
  31. package/src/test-events-lockfile.js +36 -0
  32. package/src/test-events-log.js +67 -0
  33. package/src/test-events-owner.js +64 -0
  34. package/src/test-fixtures/doc-blocks/sample-1.json +1256 -0
  35. package/src/test-read-doc-markdown.js +61 -0
  36. package/src/test-switch-profile.js +171 -0
  37. package/src/tools/diagnostics.js +10 -3
  38. package/src/tools/docs.js +93 -3
  39. package/src/tools/events.js +143 -33
  40. package/src/tools/messaging-bot.js +2 -3
  41. package/src/tools/messaging-user.js +23 -14
  42. package/src/tools/profile.js +12 -7
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  name: feishu-user-plugin
3
- version: "1.3.8"
3
+ version: "1.3.10"
4
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 auto-switch, real-time WS events. v1.3.8: multi-profile auto-switch on read errors (B), WebSocket realtime im.message events via get_new_events (C), credential pointer-only mode (E), CI gates (F), auth/uat.js + auth/cookie.js extracts (D)."
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, manage_profile_hints, 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, get_new_events
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, manage_profile_hints, 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, read_doc_markdown, 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, get_new_events, manage_ws_status
6
6
  user_invocable: true
7
7
  ---
8
8
 
@@ -24,6 +24,7 @@ Activate when the user mentions:
24
24
  - Feishu tables ("查飞书表格", "query Bitable")
25
25
  - Feishu wiki ("搜飞书知识库", "search wiki")
26
26
  - Login status ("飞书登录状态", "check Feishu login")
27
+ - **Multi-account** ("加一个飞书账号", "切换到 work 账号", "add another Feishu account", "switch to my work account") — see Multi-Account Workflow below
27
28
 
28
29
  ## 9 Built-in Skills
29
30
 
@@ -97,8 +98,79 @@ Then restart Claude Code.
97
98
  3. Copy the App ID and App Secret to your `.mcp.json`
98
99
  4. Add the bot to any group chats you want to read
99
100
 
101
+ ## Multi-Account Workflow (v1.3.9+)
102
+
103
+ The plugin supports multiple Feishu organization accounts via named profiles
104
+ in `~/.feishu-user-plugin/credentials.json`. Each profile has its own
105
+ COOKIE / APP_ID / APP_SECRET / UAT.
106
+
107
+ ### When the user says "add another Feishu account" / "加一个飞书账号"
108
+
109
+ Drive this end-to-end via the Bash tool — DO NOT just print commands and
110
+ ask the user to type them. Specifically:
111
+
112
+ **1. Confirm what's needed**, then collect:
113
+ - Profile name (default suggestion: `work2`, `personal`, etc.; let user pick)
114
+ - The new account's APP_ID and APP_SECRET (user must register a Custom App
115
+ on https://open.feishu.cn/app for that account's tenant — the existing
116
+ app from the default profile WON'T work for a different tenant)
117
+ - The new account's COOKIE — drive Playwright MCP to extract it (see
118
+ "Getting Your Cookie" above; note the **clear cookies first** caveat
119
+ to avoid stale-account contamination)
120
+
121
+ **2. Run setup (no `--activate` — keep current account active so user
122
+ isn't yanked off mid-session):**
123
+ ```bash
124
+ npx feishu-user-plugin setup --profile <name> --app-id <X2> --app-secret <S2> --cookie <C2>
125
+ ```
126
+
127
+ **3. Run OAuth for the new profile** (this opens a browser tab; user must
128
+ click "授权" in the consent page — that part is unavoidable):
129
+ ```bash
130
+ npx feishu-user-plugin oauth --profile <name>
131
+ ```
132
+ After consent, UAT is written to `credentials.json::profiles[<name>]`.
133
+
134
+ **4. Confirm via list_profiles MCP tool** — should now see both `default`
135
+ and `<name>`, with `default` still active.
136
+
137
+ **5. Tell the user how to switch later** — call `switch_profile(name="<name>")`
138
+ MCP tool from Claude Code; cross-process MCP processes auto-sync within ms
139
+ via dispatcher mtime hook.
140
+
141
+ ### When the user says "switch to <profile>" / "切到 work 账号"
142
+
143
+ Just call `switch_profile(name="<profile>")` MCP tool. Don't run any CLI
144
+ command. Cached clients reset; next tool call uses the new account.
145
+
146
+ If the named profile doesn't exist, list_profiles first to show the user
147
+ their actual profile names, then ask which they meant.
148
+
149
+ ### When the user says "show all my Feishu accounts" / "我有几个飞书账号"
150
+
151
+ Call `list_profiles` MCP tool. Show the active marker.
152
+
153
+ ### Optional cron for keepalive (multi-profile)
154
+
155
+ If the user has multiple profiles with UAT, suggest:
156
+ ```bash
157
+ crontab -e # add this line:
158
+ 0 */4 * * * npx feishu-user-plugin keepalive --all >> /tmp/feishu-keepalive.log 2>&1
159
+ ```
160
+ The `--all` flag iterates every profile in credentials.json (without it,
161
+ only the active profile gets refreshed — sufficient for single-account
162
+ users but multi-account users will see other profiles' UATs expire).
163
+
100
164
  ## Known Limitations
101
165
 
102
166
  - Image/file sending requires uploading via Official API first to get keys
103
- - CARD message type (type=14) not yet supported
104
- - Cookie session valid for ~12h, auto-refreshed via built-in heartbeat (4h interval)
167
+ (`upload_image` `send_image_as_user(image_key=...)`).
168
+ - `send_card_as_user` always routes through bot identity. User-identity
169
+ (cookie protobuf) card sending was confirmed server-side disabled in
170
+ v1.3.9 (exhaustive brute-force).
171
+ - Cookie session valid for ~12h; auto-refreshed via built-in heartbeat
172
+ (4h interval). UAT valid 2h, refresh_token valid 7 days; run `keepalive`
173
+ cron weekly to prevent refresh_token expiration.
174
+ - "Seamless" auto-switch tied to which account is active in Feishu Desktop
175
+ is **not yet implemented** (designed for v1.3.10; see ROADMAP). For now,
176
+ call `switch_profile` MCP tool when you want to flip.
@@ -24,7 +24,7 @@ The 9 Claude Code skills are also exposed as MCP prompts (`prompts/list` + `prom
24
24
 
25
25
  Each prompt accepts a single `arguments` free-form string (mirroring the `$ARGUMENTS` convention used by Claude Code skills). `status` has no arguments.
26
26
 
27
- ## Tool Categories (82 tools)
27
+ ## Tool Categories (84 tools)
28
28
 
29
29
  Per-tool descriptions live in each tool's MCP `inputSchema.description`. This section lists names + cross-domain caveats only.
30
30
 
@@ -34,7 +34,7 @@ Per-tool descriptions live in each tool's MCP `inputSchema.description`. This se
34
34
  - All cookie sends auto-resolve `oc_xxx` chat IDs to numeric since v1.3.7 (C1.4: `getChatInfo → search → numeric`, cached).
35
35
  - Plain-text sends accept `ats:[{userId,name}]` — the marker `@<name>` must appear in `text`; spliced into a real AT element that triggers notifications.
36
36
  - `send_post_as_user` paragraphs accept `{tag:"text"}` / `{tag:"a",href,text}` / `{tag:"at",userId,name}` elements; `at` element triggers a real notification.
37
- - `send_image_as_user` is **broken via cookie protobuf** (HTTP 400wire format incomplete). Workaround: `send_message_as_bot(msg_type="image")`. Wire-format reverse-engineering deferred to v1.3.8. See Known Limitations.
37
+ - `send_image_as_user` works as-of v1.3.9 (cookie protobuf reverse-engineered via brute-force probe see `scripts/explore-image-minimize.js`). Required Content fields: `imageKey` + `thumbnailKey`. Plugin defaults thumbnailKey = imageKey when caller omits it. Optional metadata: width / height / mime / size — all auto-derivable on Feishu's side, no pre-compute needed.
38
38
 
39
39
  ### User Identity — Contacts & Info (5 tools)
40
40
  `search_contacts` / `create_p2p_chat` / `get_chat_info` / `get_user_info` / `get_login_status`
@@ -56,9 +56,10 @@ Per-tool descriptions live in each tool's MCP `inputSchema.description`. This se
56
56
  - `manage_members` requires `member_id_type` to match the IDs you pass (`open_id` default; pass `union_id`/`user_id` explicitly to avoid 9499).
57
57
  - `download_message_resource(kind=image|file)` MUST pass `save_path` when payload > 2 MiB (Anthropic 5 MB inline cap). For `merge_forward` children use `parentMessageId`, not child id.
58
58
 
59
- ### Official API — Docs (5 tools)
60
- `search_docs` / `read_doc` / `get_doc_blocks` / `create_doc` / `manage_doc_block` / `download_doc_image`
59
+ ### Official API — Docs (7 tools)
60
+ `search_docs` / `read_doc` / `read_doc_markdown` / `get_doc_blocks` / `create_doc` / `manage_doc_block` / `download_doc_image`
61
61
 
62
+ - `read_doc_markdown` returns a markdown string instead of structured JSON — saves ~60% tokens for RAG / digest / summarisation. Embedded images / files surface as `feishu://image_token/<TOKEN>` / `feishu://file_token/<TOKEN>` placeholders; pair with `download_doc_image` for binaries. `document_id` accepts native token / wiki node / Feishu URL same as the others.
62
63
  - `manage_doc_block(action=create)` has image (`image_path`/`image_token`) and file (`file_path`/`file_token`) shortcuts; FILE blocks (block_type=23) are auto-wrapped in VIEW container (block_type=33), plugin walks into the inner file block before `replace_file` PATCH.
63
64
  - `download_doc_image` same 2 MiB cap as `download_message_resource`.
64
65
  - All `document_id` / `app_token` accept native token / wiki node token / full Feishu URL (resolved via `getWikiNode`, 10 min cache).
@@ -111,13 +112,14 @@ Per-tool descriptions live in each tool's MCP `inputSchema.description`. This se
111
112
  - `switch_profile` invalidates cached client instances; next call rebuilds against the new profile. Multi-profile registered via `LARK_PROFILES_JSON` env or `credentials.json` profiles map.
112
113
  - `manage_profile_hints(action=list|set|clear, resource_key?, profile?)` (v1.3.8) inspects / edits the resourceKey → profile cache the auto-switch middleware uses. No-op when credentials.json doesn't exist.
113
114
 
114
- ### Plugin — Realtime Events (1 tool, v1.3.8)
115
- `get_new_events`
115
+ ### Plugin — Realtime Events (2 tools, v1.3.9)
116
+ `get_new_events` / `manage_ws_status`
116
117
 
117
- - WS connection started at MCP boot when APP_ID + APP_SECRET are configured. Connects to feishu.cnLark international not supported.
118
- - Buffer cap 1000 events; oldest dropped. Drain semantics: consumers see each event once.
119
- - Currently emits `im.message.receive_v1` only. Future: approval / calendar / docs comments behind config flag.
120
- - Filter by `event_type` / `event_types` / `chat_id` / `since_seconds`. `peek=true` keeps events in buffer.
118
+ - **v1.3.9 machine-level**: a single MCP process per machine owns the WS via `~/.feishu-user-plugin/ws-owner.lock`. Events written to `~/.feishu-user-plugin/events.jsonl` (append-only, 10 MB soft / 20 MB hard cap). All harnesses read through a shared `events.cursor.json` **every event delivered exactly once across all MCP processes**, no more duplicates.
119
+ - WS connection started at MCP boot when APP_ID + APP_SECRET configured. Connects to feishu.cn Lark international not supported.
120
+ - Default subscriptions = `["im.message.receive_v1"]`. Edit `credentials.json::profiles[<active>].events` to add others (`approval.instance.created_v4`, `calendar.calendar.event.changed_v4`, etc.); call `manage_ws_status(action=reconfig)` to apply without restart.
121
+ - `get_new_events` filter by `event_type` / `event_types` / `chat_id` / `since_seconds` / `profile`. `peek=true` keeps cursor unchanged. **Default `profile` filter = current active**.
122
+ - `manage_ws_status(action=info|reconnect|claim|rotate|reconfig)` — diagnose / control the WS owner. `claim --force` steals an active lock; `rotate` forces events.jsonl rotation.
121
123
 
122
124
  ## Usage Patterns
123
125
 
@@ -177,6 +179,8 @@ For users with ≥2 profiles in `~/.feishu-user-plugin/credentials.json`. Read-o
177
179
 
178
180
  Override per call with `via_profile: "<name>"` to pin, or `via_profile: "auto"` to allow auto-switch on a write. Hints persist in `credentials.json::profileHints` and are inspectable via `manage_profile_hints`.
179
181
 
182
+ v1.3.9: `FEISHU_PLUGIN_PROFILE` env is bootstrap-only — `credentials.json::active` is authoritative once the file exists. Cross-process sync via dispatcher mtime check (~10μs/call).
183
+
180
184
  ### Multi-profile registration
181
185
  For more profiles beyond the default, set `LARK_PROFILES_JSON` in the MCP env (or use `credentials.json` profiles map):
182
186
  ```json
@@ -190,6 +194,9 @@ For more profiles beyond the default, set `LARK_PROFILES_JSON` in the MCP env (o
190
194
  - Cookie expiry: sl_session has 12h max-age, auto-refreshed by heartbeat every 4h.
191
195
  - UAT expiry: 2h, auto-refreshed via refresh_token.
192
196
  - Refresh token expiry: 7 days. Use `keepalive` cron to prevent expiration.
197
+ - `~/.feishu-user-plugin/ws-owner.lock`: lock file owned by the one MCP process driving the WS connection (O_CREAT|O_EXCL, 30 s stale).
198
+ - `~/.feishu-user-plugin/events.jsonl`: append-only event log written by the WS owner; 10 MB soft / 20 MB hard cap then rotated to `events.jsonl.old`.
199
+ - `~/.feishu-user-plugin/events.cursor.json`: global drain cursor shared across all MCP processes — advancing it marks events as consumed for all harnesses on the machine.
193
200
 
194
201
  ### Credentials store (v1.3.7+)
195
202
  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.
@@ -289,6 +296,8 @@ v1.3.5+ hardening means the "6 MCP processes racing on UAT refresh and burning t
289
296
  ### Multiple / duplicate MCP server processes
290
297
  Codex + Claude Code both can respawn the server per tool session without cleanup; 6 concurrent processes isn't unusual. v1.3.5 neutralises the damage (file lock above) but stale processes still hold memory. **Manual cleanup when you notice**: `pkill -f 'feishu-user-plugin/src/index.js'`. Also: a team-skills plugin must NOT ship `.mcp.json` — if both `~/.claude.json` and team-skills register the same MCP, you get duplicates; delete `.mcp.json` from the team-skills plugin dir.
291
298
 
299
+ v1.3.9: events are now machine-level; each event delivered exactly once across all MCP processes. Old per-process duplicates issue resolved.
300
+
292
301
  ### `create_*` tool warns "UAT failed, created as BOT"
293
302
  UAT is failing (expired / scope missing / race), so the plugin fell back to bot. Resource is now owned by the shared bot, tenant-readable. **Fix**: `npx feishu-user-plugin oauth`, restart, delete the bot-owned copy and recreate.
294
303
 
@@ -304,7 +313,7 @@ Expected — Feishu API only returns groups. P2P flow: `search_contacts` → `cr
304
313
  - Lark international tenant (lark.com) — not supported by Feishu's WSClient. No fix; use polling tools (`read_messages`) instead.
305
314
  - Network restriction — corporate proxy blocking outbound WSS.
306
315
  - **Bot not in the chat where the message was sent**: `im.message.receive_v1` only fires for chats the bot is a member of. Add the bot to the chat to receive events.
307
- - **Multiple MCP processes**: Each process has its own WS, so events are duplicated. De-dupe on `event_id` in your consumer.
316
+ - **Multiple MCP processes**: v1.3.9: events are now machine-level; each event delivered exactly once across all MCP processes. Old per-process duplicates issue resolved.
308
317
 
309
318
  ## Architecture
310
319
 
@@ -411,69 +420,80 @@ See `docs/TESTING-METHODOLOGY.md` for the full regression playbook (when to use
411
420
  - `chore:` dependencies, CI, config changes
412
421
 
413
422
  ### Publishing
414
- **IMPORTANT: Version number must ALWAYS be confirmed with the user before publishing.**
415
- Any operation involving `npm version`, modifying `package.json` version, `git tag v*`, or `git push --tags` requires explicit user confirmation of the target version number. Do not auto-decide version numbers.
423
+ **IMPORTANT: User confirmation is required exactly TWICE per release** — once on target version (before any publish operation), once on the announcement card before sending. Don't ask between steps; run end-to-end.
416
424
 
417
425
  Three-layer version safety:
418
- 1. **Claude rule** (this section): Ask user to confirm version before any publish-related operation
426
+ 1. **Claude rule** (this section): Confirm version once with user. Then run all publish steps without asking. Stop only on (a) failure, or (b) the announcement-preview gate.
419
427
  2. **Local gate** (`prepublishOnly`): Interactive confirmation when running `npm publish` locally (skipped in CI)
420
428
  3. **CI gate** (`.github/workflows/publish.yml`): Tag must match `package.json` version or publish fails
421
429
 
422
430
  Steps:
423
- 1. Confirm target version with user
424
- 2. Update `version` in `package.json`
425
- 3. `git add <files> && git commit -m "v1.x.x: description"`
426
- 4. `git tag v1.x.x && git push && git push --tags`
427
- 5. GitHub Actions verifies tag matches package.json, then auto-publishes to npm
431
+ 1. Confirm target version with user (once)
432
+ 2. Bump `version` in `package.json` + `.claude-plugin/plugin.json` + `skills/feishu-user-plugin/SKILL.md` (single commit; `scripts/check-version.js` enforces triangle equality)
433
+ 3. Open release PR, wait for CI green (auto-merge enabled on this repo, so `gh pr merge --auto --squash`)
434
+ 4. After merge, `git tag vX.Y.Z && git push origin vX.Y.Z` triggers GitHub Actions `Publish to npm` workflow
435
+ 5. Verify: `npm view feishu-user-plugin version` returns the new version
436
+ 6. post-merge hook runs `scripts/sync-team-skills.sh` which auto-syncs team-skills (skills + plugin.json + child README changelog + root README catalog row + catalog.yaml regen + `gh pr merge --admin --squash` on the sync PR). No manual touches in team-skills.
437
+ 7. Run `node scripts/generate-release-artifacts.js` to produce `/tmp/feishu-release/v$VERSION/feishu-card.json`
438
+ 8. Present the card preview to user. Wait for "发"
439
+ 9. `send_card_as_user(chat_id="oc_0fab8e155f500f28bd437e8686921870", card=<JSON>)` — only after user explicitly approves
428
440
  6. **After npm confirms the new version is live, draft a release announcement in Chinese for the "AI技术解决(内部)" Feishu group and show it to the user for approval BEFORE sending.** Do not send until the user explicitly approves.
429
441
 
430
442
  ### Release announcement rules (every release)
431
- After a successful publish, draft a group announcement to "AI技术解决(内部)" (chat_id `7599552782038813643`) and ALWAYS show it to the user for review first. Only send after explicit approval.
432
443
 
433
- **Transport**: `send_post_as_user` (rich-text post). No @-mentions announcements are impersonal broadcasts. No emojis. No marketing language.
444
+ After successful publish, send announcement to "AI技术解决(内部)" group (chat_id `oc_0fab8e155f500f28bd437e8686921870`). **Never send without explicit user approval** show preview first, wait for "发".
445
+
446
+ **Transport (v1.3.9+)**: `send_card_as_user` (interactive Feishu card). No @-mentions, no emojis, no marketing.
434
447
 
435
- **Structure** (in this order; omit a section if it doesn't apply this release):
448
+ **Source of truth**: `CHANGELOG.md` v$VERSION section. **Never hand-write announcements** the generator script extracts the text deterministically:
436
449
 
450
+ ```bash
451
+ node scripts/generate-release-artifacts.js [version]
452
+ # Outputs to /tmp/feishu-release/v<version>/:
453
+ # feishu-card.json ← full Feishu card payload, ready for send_card_as_user
454
+ # team-skills-changelog.md ← markdown block injected into team-skills child README by post-merge hook
455
+ # team-skills-readme-row.md ← root README catalog row replacement
437
456
  ```
438
- feishu-user-plugin vX.Y.Z 发布
439
457
 
440
- <一到两句开篇总结本次发布的主题,陈述语气,不推销>
458
+ **CHANGELOG conventions** (the generator parses these — keep the convention or output diverges):
441
459
 
442
- 修复
443
- <缺陷描述>:<根因与修复机制,引用具体错误码/接口名/参数>
444
- • ...
460
+ ```markdown
461
+ ## [X.Y.Z] - YYYY-MM-DD
445
462
 
446
- 新增
447
- • 新增 <tool 名> 工具:<一句话功能描述>。<关键约束或调用条件>
448
- • ...
463
+ <一到两句陈述式开篇,可空,generator 用作 card 第一段;不宣传不夸大>
449
464
 
450
- 调整
451
- <行为变化的描述>
452
- ...
465
+ ### Added (翻译为"新增")
466
+ - **简短标题 (代号)**:用户可见现象。底层机制 / 错误码 / 接口名 / 文件路径。
467
+ - ...
453
468
 
454
- 下版本计划
455
- <条目>
456
- ...
469
+ ### Changed ("调整")
470
+ ### Fixed ("修复")
471
+ ### Removed | Deprecated | Security ("移除" / "废弃" / "安全")
472
+ ### Deferred to vN.M.P ("下版本计划 (vN.M.P)",从上版本拷过来 - 本版完成的条目)
457
473
 
458
- 升级方式
459
- 重启 Claude Code / Codex 即可自动拉取 X.Y.Z
460
- <若有相关新日志/错误提示,说明怎么应对>
461
- • 建议复测 N 个场景:<场景 1>、<场景 2>、<场景 3>
474
+ ### Test scenarios (可选;用作"升级方式"段的"建议复测"行)
475
+ - 调用 X 时观察 Y 出现 Z
476
+ - ...
462
477
  ```
463
478
 
464
- **写作规范**:
465
- - **开篇**:一到两句陈述式总结,不宣传、不夸大。参考 v1.3.2:"本次更新主要补齐了 X 能力,并修复了 Y 问题;同时将 Z 统一调整为 ..."
466
- - **每条 bullet**:先写用户可见现象,再写底层机制。引用具体错误码(如 1770032 / 91403)、接口名(如 manage_doc_block)、参数名(如 RichText.atIds)——专业读者信赖的是细节
467
- - **字符**:bullet `•`(U+2022),不用 `-` `*`;代码/工具名在正文中直接写,不加反引号
468
- - **禁用**:emoji、🔴🟡🟢 之类严重度标记、`@` 任何人、营销词("强大"、"全新"、"重磅")、夸张修辞
469
- - **语气**:技术 release note 的中性语气,像写给同行的内部更新。参考 v1.3.2 全文
470
- - **长度**:单屏为宜,一般 400–700 汉字。每条 bullet 一到三行
471
- - **下版本计划**:复制自上一版公告仍未完成的条目 + 本次发布中暴露的新方向。本版已完成的条目必须删除
472
- - **升级方式**:至少包含重启指令;若本次修了某类错误(如 APP_ID 校验),列出对应诊断日志字样;以"建议复测 N 个场景"收尾,场景要具体可操作
473
-
474
- **结尾**:不加 CHANGELOG 链接(v1.3.2 风格未含链接,群内读者不需要)。
475
-
476
- **发送前**:始终先用 `send_to_user` 或类似工具发给用户自己审核,或直接以文本形式贴在对话里等用户批准。用户说"发"才调 `send_post_as_user` 到目标群。
479
+ **写作规范** (writes flow into the card directly):
480
+ - 每条 bullet:先用户可见现象,再底层机制。引用具体错误码 (91403 / 1254301)、接口名 (`manage_bitable_record`)、参数名 (`via_profile`)、文件路径 (`src/auth/profile-router.js`)
481
+ - 代号语:`(B)` `(D.1)` 等可保留,对应 ROADMAP / plan 编号
482
+ - 禁用:emoji / `@` 任何人 / "强大"等营销词 / 夸张修辞
483
+ - 长度:单屏,整段 400-800 汉字。每条 bullet 1-3 行
484
+
485
+ **升级方式** is generated automatically by the script:
486
+ - "重启 Claude Code / Codex 自动拉取 X.Y.Z" — always
487
+ - "推荐运行 npx feishu-user-plugin migrate --confirm ..." added when bullets mention migrate / credentials.json / FEISHU_PLUGIN_PROFILE
488
+ - "启动看 stderr 带 WS connected ..." — added when bullets mention WS / WebSocket / get_new_events
489
+ - "建议复测 N 个场景:..." — uses `### Test scenarios` bullets if present, otherwise top-3 Added bullet titles
490
+
491
+ **Step-by-step at release time**:
492
+ 1. Bump version → tag → push → wait for `Publish to npm` workflow success → confirm `npm view feishu-user-plugin version`
493
+ 2. post-merge hook on this repo runs `scripts/sync-team-skills.sh`, which calls `scripts/generate-release-artifacts.js` and auto-injects v$VERSION block into team-skills child README + updates root README catalog row + opens & `--admin --squash`-merges the team-skills sync PR. Zero manual steps in team-skills.
494
+ 3. `node scripts/generate-release-artifacts.js` (idempotent — same input gives same output) on this repo to (re)produce `feishu-card.json`
495
+ 4. **Show the rendered card preview to user** — paste a summary or re-render via `cat /tmp/feishu-release/v$VERSION/feishu-card.json | jq` and let user inspect. Do not send.
496
+ 5. User says "发" → `send_card_as_user(chat_id=oc_0fab8e155f500f28bd437e8686921870, card=<JSON content>)`
477
497
 
478
498
  ## OAuth Scopes (when re-running `npx feishu-user-plugin oauth`)
479
499
 
@@ -500,4 +520,4 @@ If a tool returns `access_denied` or error code `99991672` (scope not granted),
500
520
  - `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)
501
521
  - `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.
502
522
  - `search_wiki` uses same API as `search_docs` — `docs_types` filter may not work as expected
503
- - `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.)
523
+ - `send_card_as_user` only routes through bot. User-identity (cookie protobuf) card sending was confirmed server-side disabled in v1.3.9 (exhaustive brute-force, `scripts/explore-card-protobuf.js`). The "as_user" suffix is historical naming kept for backward compat the `via` parameter and the user-path codepath were removed; the tool is bot-only.
@@ -327,6 +327,39 @@ function migrate({ dryRun = true } = {}) {
327
327
  return { ok: true, credentials };
328
328
  }
329
329
 
330
+ // --- Per-profile events list (v1.3.9 A.4) ---
331
+ //
332
+ // Each profile may optionally declare an `events` array listing the Feishu
333
+ // real-time event types to subscribe to. When absent the default
334
+ // `["im.message.receive_v1"]` is used. This array is read by
335
+ // `_getProfileEventsList` in server.js to configure the WebSocket client.
336
+ //
337
+ // Example credentials.json profile entry with events:
338
+ // "default": {
339
+ // "LARK_APP_ID": "...",
340
+ // ...,
341
+ // "events": ["im.message.receive_v1", "approval.instance.created_v4"]
342
+ // }
343
+
344
+ function getProfileEvents(name) {
345
+ const f = _readFile();
346
+ const target = name || (f ? f.active : 'default');
347
+ if (f && f.profiles[target] && Array.isArray(f.profiles[target].events)) {
348
+ return f.profiles[target].events.slice();
349
+ }
350
+ return ['im.message.receive_v1'];
351
+ }
352
+
353
+ function setProfileEvents(name, eventList) {
354
+ if (!Array.isArray(eventList)) throw new Error('setProfileEvents: eventList must be an array');
355
+ const f = _readFile();
356
+ if (!f) throw new Error('No credentials.json — cannot set profile events.');
357
+ if (!f.profiles[name]) throw new Error(`Profile "${name}" not found.`);
358
+ f.profiles[name].events = eventList.slice();
359
+ _atomicWriteJson(_credentialsPath(), f);
360
+ return true;
361
+ }
362
+
330
363
  // --- Profile hints (v1.3.8) ---
331
364
  //
332
365
  // profileHints maps resourceKey → profileName, persisted in credentials.json.
@@ -383,6 +416,9 @@ module.exports = {
383
416
  setActiveProfile,
384
417
  persistProfileUpdate,
385
418
  migrate,
419
+ // per-profile events list (v1.3.9 A.4)
420
+ getProfileEvents,
421
+ setProfileEvents,
386
422
  // profile hints (v1.3.8)
387
423
  getProfileHints,
388
424
  setProfileHint,
package/src/cli.js CHANGED
@@ -67,9 +67,21 @@ Setup options:
67
67
  --app-secret <s> App Secret (non-interactive mode)
68
68
  --cookie <c> Cookie string (optional)
69
69
  --client <target> Config target: claude (default), codex, or both
70
- --pointer-only Write only FEISHU_PLUGIN_PROFILE=default to harness env.
71
- Real creds live in ~/.feishu-user-plugin/credentials.json
72
- (run "migrate --confirm" first if not yet migrated).
70
+ --force Overwrite existing default profile in credentials.json
71
+ --profile <name> Create or update a named profile (replaces LARK_PROFILES_JSON
72
+ for new setups). Without --activate, leaves the active
73
+ profile unchanged so adding work2 doesn't yank you off default.
74
+ --activate When used with --profile, also flip credentials.json::active
75
+ to the named profile.
76
+
77
+ OAuth options (v1.3.9):
78
+ npx feishu-user-plugin oauth --profile <name>
79
+ Get UAT for a specific profile. Default = currently active.
80
+
81
+ Keepalive options (v1.3.9):
82
+ npx feishu-user-plugin keepalive --all
83
+ Refresh cookie + UAT for ALL profiles in credentials.json.
84
+ Default (no flag) = active profile only (back-compat).
73
85
 
74
86
  Quick Start (Claude Code):
75
87
  1. npx feishu-user-plugin setup
@@ -81,9 +93,16 @@ Quick Start (Codex):
81
93
  2. Follow the prompts to configure credentials
82
94
  3. Restart Codex
83
95
 
96
+ Multi-account (v1.3.9):
97
+ 1. npx feishu-user-plugin setup --app-id X1 --app-secret S1 --cookie C1
98
+ 2. npx feishu-user-plugin oauth # default profile UAT
99
+ 3. npx feishu-user-plugin setup --profile work2 --app-id X2 --app-secret S2 --cookie C2
100
+ 4. npx feishu-user-plugin oauth --profile work2 # work2 profile UAT
101
+ 5. In Claude Code: switch_profile(name="work2") MCP tool to flip live
102
+
84
103
  Auto-renewal (optional):
85
104
  Add to crontab to keep tokens alive even when Claude Code is closed:
86
- crontab -e → add: 0 */4 * * * npx feishu-user-plugin keepalive >> /tmp/feishu-keepalive.log 2>&1
105
+ crontab -e → add: 0 */4 * * * npx feishu-user-plugin keepalive --all >> /tmp/feishu-keepalive.log 2>&1
87
106
  `);
88
107
  }
89
108
 
@@ -97,53 +116,75 @@ function migrate() {
97
116
  async function keepalive() {
98
117
  const { LarkUserClient } = require('./clients/user');
99
118
  const { LarkOfficialClient } = require('./clients/official');
100
- const { readCredentials, persistToConfig } = require('./auth/credentials');
101
-
102
- const creds = readCredentials();
103
- if (!creds.LARK_COOKIE && !creds.LARK_APP_ID) {
104
- console.error('[keepalive] No credentials found. Run: npx feishu-user-plugin setup');
105
- process.exit(1);
106
- }
107
- let ok = true;
119
+ const cred = require('./auth/credentials');
120
+
121
+ // v1.3.9: --all flag iterates every profile in credentials.json,
122
+ // refreshing cookie + UAT for each. Default behavior (no flag) refreshes
123
+ // only the active profile (back-compat with v1.3.6+ cron usage).
124
+ const all = process.argv.includes('--all');
125
+ const targetProfiles = all ? cred.listProfileNames() : [cred.getActiveProfileName() || 'default'];
126
+
127
+ let totalOk = true;
128
+ for (const profileName of targetProfiles) {
129
+ let env;
130
+ try { env = cred.getActiveProfileEnv(profileName); }
131
+ catch (e) {
132
+ console.error(`[keepalive][${profileName}] cannot read profile: ${e.message}`);
133
+ totalOk = false;
134
+ continue;
135
+ }
136
+ if (!env.LARK_COOKIE && !env.LARK_APP_ID) {
137
+ console.error(`[keepalive][${profileName}] no credentials. Run: npx feishu-user-plugin setup --profile ${profileName} ...`);
138
+ totalOk = false;
139
+ continue;
140
+ }
141
+ let ok = true;
108
142
 
109
- // 1. Refresh Cookie
110
- const cookie = creds.LARK_COOKIE;
111
- if (cookie && cookie !== 'SETUP_NEEDED') {
112
- try {
113
- const client = new LarkUserClient(cookie);
114
- await client.init();
115
- // init() calls _getCsrfToken which refreshes sl_session
116
- persistToConfig({ LARK_COOKIE: client.cookieStr });
117
- console.log(`[keepalive] Cookie refreshed (user: ${client.userName})`);
118
- } catch (e) {
119
- console.error(`[keepalive] Cookie refresh FAILED: ${e.message}`);
120
- ok = false;
143
+ // 1. Refresh Cookie
144
+ if (env.LARK_COOKIE && env.LARK_COOKIE !== 'SETUP_NEEDED') {
145
+ try {
146
+ const client = new LarkUserClient(env.LARK_COOKIE);
147
+ await client.init();
148
+ cred.persistProfileUpdate(profileName, { LARK_COOKIE: client.cookieStr });
149
+ console.log(`[keepalive][${profileName}] cookie refreshed (user: ${client.userName})`);
150
+ } catch (e) {
151
+ console.error(`[keepalive][${profileName}] cookie refresh FAILED: ${e.message}`);
152
+ ok = false;
153
+ }
121
154
  }
122
- }
123
155
 
124
- // 2. Refresh UAT
125
- const appId = creds.LARK_APP_ID;
126
- const appSecret = creds.LARK_APP_SECRET;
127
- const uat = creds.LARK_USER_ACCESS_TOKEN;
128
- const rt = creds.LARK_USER_REFRESH_TOKEN;
129
- if (appId && appSecret && uat && uat !== 'SETUP_NEEDED' && rt) {
130
- try {
131
- const official = new LarkOfficialClient(appId, appSecret);
132
- official._uat = uat;
133
- official._uatRefresh = rt;
134
- official._uatExpires = 0; // force refresh
135
- await official._refreshUAT(); // refreshes + persists automatically
136
- console.log('[keepalive] UAT refreshed');
137
- } catch (e) {
138
- console.error(`[keepalive] UAT refresh FAILED: ${e.message}`);
139
- ok = false;
156
+ // 2. Refresh UAT (also writes to the same profile via auth/uat.js → persistToConfig
157
+ // which goes through the active profile path. For --all we need to switch
158
+ // active temporarily so the write lands on the right profile.)
159
+ if (env.LARK_APP_ID && env.LARK_APP_SECRET && env.LARK_USER_ACCESS_TOKEN && env.LARK_USER_ACCESS_TOKEN !== 'SETUP_NEEDED' && env.LARK_USER_REFRESH_TOKEN) {
160
+ const prevActive = cred.getActiveProfileName();
161
+ const needSwitch = all && prevActive !== profileName;
162
+ try {
163
+ if (needSwitch) cred.setActiveProfile(profileName);
164
+ // Set process.env so LarkOfficialClient.loadUAT() picks the right tokens
165
+ process.env.LARK_USER_ACCESS_TOKEN = env.LARK_USER_ACCESS_TOKEN;
166
+ process.env.LARK_USER_REFRESH_TOKEN = env.LARK_USER_REFRESH_TOKEN;
167
+ const official = new LarkOfficialClient(env.LARK_APP_ID, env.LARK_APP_SECRET);
168
+ official._uat = env.LARK_USER_ACCESS_TOKEN;
169
+ official._uatRefresh = env.LARK_USER_REFRESH_TOKEN;
170
+ official._uatExpires = 0; // force refresh
171
+ await official._refreshUAT();
172
+ console.log(`[keepalive][${profileName}] UAT refreshed`);
173
+ } catch (e) {
174
+ console.error(`[keepalive][${profileName}] UAT refresh FAILED: ${e.message}`);
175
+ ok = false;
176
+ } finally {
177
+ if (needSwitch) {
178
+ try { cred.setActiveProfile(prevActive); } catch (_) {}
179
+ }
180
+ }
140
181
  }
182
+ if (!ok) totalOk = false;
141
183
  }
142
184
 
143
- if (ok) {
144
- console.log('[keepalive] All tokens refreshed successfully');
145
- }
146
- process.exit(ok ? 0 : 1);
185
+ if (totalOk) console.log(`[keepalive] all profiles refreshed (${targetProfiles.length} profile${targetProfiles.length === 1 ? '' : 's'})`);
186
+ else console.error(`[keepalive] one or more profiles failed`);
187
+ process.exit(totalOk ? 0 : 1);
147
188
  }
148
189
 
149
190
  async function checkStatus() {
@@ -185,18 +185,6 @@ class LarkUserClient {
185
185
  if (parentId) req.parentId = parentId;
186
186
  const { packet, ok } = await this._gateway(5, 'PutMessageRequest', req, '5.7.0');
187
187
  if (!ok) {
188
- // The cookie protobuf gateway returns HTTP 400 when our wire format is
189
- // missing required fields. Verified for IMAGE (v1.3.7 testing): the
190
- // simple {imageKey} content payload is rejected — Feishu Web encodes
191
- // images with extra metadata (image dimensions, mime type, etc.) that
192
- // we don't have in proto/lark.proto. v1.3.8 shipped the capture/decode
193
- // tooling (scripts/decode-feishu-protobuf.js + capture-feishu-protobuf.js
194
- // + docs/COOKIE-PROTOBUF-CAPTURES.md). Actual reverse-engineering moved
195
- // to v1.3.9. Surface a clear error routing the user to
196
- // send_message_as_bot, which works.
197
- if (type === MsgType.IMAGE) {
198
- throw new Error('send_image_as_user: Feishu cookie protobuf gateway rejected the IMAGE wire format (HTTP 400). User-identity image sends are not yet supported — wire format reverse-engineering is deferred to v1.3.9 (v1.3.8 shipped the capture/decode tooling at scripts/decode-feishu-protobuf.js). Workaround: use send_message_as_bot(chat_id, msg_type="image", payload={image_key:"..."}).');
199
- }
200
188
  throw new Error(`_sendMsg: cookie protobuf gateway returned non-2xx for type=${type}. The wire format likely doesn't match what Feishu expects.`);
201
189
  }
202
190
  return { success: true, status: packet.status };
@@ -268,9 +256,23 @@ class LarkUserClient {
268
256
  }
269
257
 
270
258
  // --- Send Image ---
259
+ // v1.3.9: cookie protobuf path now works. Gateway requires Content.imageKey
260
+ // (field 2) + Content.thumbnailKey (field 10). Width/height/mime/size are
261
+ // optional but accepted. We default thumbnailKey to imageKey when caller
262
+ // doesn't supply one — Feishu accepts that for already-thumbnail-sized
263
+ // uploads. See proto/lark.proto Content + scripts/explore-image-minimize.js.
271
264
 
272
265
  async sendImage(chatId, imageKey, opts = {}) {
273
- return this._sendMsg(MsgType.IMAGE, chatId, { imageKey }, opts);
266
+ if (!imageKey) throw new Error('sendImage: imageKey required');
267
+ const content = {
268
+ imageKey,
269
+ thumbnailKey: opts.thumbnailKey || imageKey,
270
+ };
271
+ if (opts.width != null) content.imageWidth = opts.width;
272
+ if (opts.height != null) content.imageHeight = opts.height;
273
+ if (opts.mime) content.mimeType = opts.mime;
274
+ if (opts.size != null) content.fileSize = opts.size;
275
+ return this._sendMsg(MsgType.IMAGE, chatId, content, opts);
274
276
  }
275
277
 
276
278
  // --- Send File ---