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.
- package/.claude-plugin/plugin.json +12 -2
- package/CHANGELOG.md +100 -12
- package/README.en.md +610 -0
- package/README.md +292 -532
- package/package.json +12 -5
- package/proto/lark.proto +10 -0
- package/scripts/explore-card-protobuf.js +144 -0
- package/scripts/explore-image-minimize.js +163 -0
- package/scripts/generate-og-image.js +39 -0
- package/scripts/generate-release-artifacts.js +318 -0
- package/scripts/probe-feishu-docx.js +203 -0
- package/scripts/sync-team-skills.sh +109 -7
- package/skills/feishu-user-plugin/SKILL.md +76 -4
- package/skills/feishu-user-plugin/references/CLAUDE.md +74 -54
- package/src/auth/credentials.js +36 -0
- package/src/cli.js +86 -45
- package/src/clients/user.js +15 -13
- package/src/events/cursor.js +103 -0
- package/src/events/event-buffer.js +8 -5
- package/src/events/event-log.js +151 -0
- package/src/events/index.js +8 -1
- package/src/events/lockfile.js +126 -0
- package/src/events/owner.js +73 -0
- package/src/events/ws-server.js +95 -25
- package/src/oauth.js +48 -7
- package/src/resolver.js +10 -0
- package/src/server.js +248 -29
- package/src/setup.js +99 -25
- package/src/test-all.js +12 -9
- package/src/test-events-cursor.js +56 -0
- package/src/test-events-lockfile.js +36 -0
- package/src/test-events-log.js +67 -0
- package/src/test-events-owner.js +64 -0
- package/src/test-fixtures/doc-blocks/sample-1.json +1256 -0
- package/src/test-read-doc-markdown.js +61 -0
- package/src/test-switch-profile.js +171 -0
- package/src/tools/diagnostics.js +10 -3
- package/src/tools/docs.js +93 -3
- package/src/tools/events.js +143 -33
- package/src/tools/messaging-bot.js +2 -3
- package/src/tools/messaging-user.js +23 -14
- package/src/tools/profile.js +12 -7
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: feishu-user-plugin
|
|
3
|
-
version: "1.3.
|
|
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
|
-
|
|
104
|
-
-
|
|
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 (
|
|
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`
|
|
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 (
|
|
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 (
|
|
115
|
-
`get_new_events`
|
|
115
|
+
### Plugin — Realtime Events (2 tools, v1.3.9)
|
|
116
|
+
`get_new_events` / `manage_ws_status`
|
|
116
117
|
|
|
117
|
-
-
|
|
118
|
-
-
|
|
119
|
-
-
|
|
120
|
-
-
|
|
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**:
|
|
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:
|
|
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):
|
|
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.
|
|
425
|
-
3.
|
|
426
|
-
4. `git tag
|
|
427
|
-
5.
|
|
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
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
460
|
-
|
|
461
|
-
• 建议复测 N 个场景:<场景 1>、<场景 2>、<场景 3>
|
|
474
|
+
### Test scenarios (可选;用作"升级方式"段的"建议复测"行)
|
|
475
|
+
- 调用 X 时观察 Y 出现 Z
|
|
476
|
+
- ...
|
|
462
477
|
```
|
|
463
478
|
|
|
464
|
-
|
|
465
|
-
-
|
|
466
|
-
-
|
|
467
|
-
-
|
|
468
|
-
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
-
|
|
472
|
-
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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
|
-
- `
|
|
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.
|
package/src/auth/credentials.js
CHANGED
|
@@ -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
|
-
--
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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 (
|
|
144
|
-
|
|
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() {
|
package/src/clients/user.js
CHANGED
|
@@ -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
|
-
|
|
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 ---
|