feat(#179): Feishu channel for claude-agent-sdk runtime — RFC-020 first-cut [WIP]#258
Merged
Conversation
…cut) Per Vincent 2026-06-24 decision: first-cut path is agent-node direct bridge (WSClient long-connection mode), deferring the full commhub-gateway §2.9 schema 增量 to a follow-up PR after demo lands. M1 — contract scaffold, 4 files: - src/im/feishu/config.ts — channel config loader (.env + access.json) - src/im/feishu/adapter.ts — FeishuAdapter implements IMAdapter (M2/M3 stubs) - src/im/feishu/bridge.ts — bridge worker entry stub (M2-M4 wiring stubs) - src/im/feishu/index.ts — public surface Typechecks under existing tsconfig (src/im/**/*.ts already in include). Each method body documents its target milestone (M2 WSClient, M3 send/edit, M4 agent-node spawn integration, M5 group @bot + images). Per RFC §12.6, M2 ports the community vansin/claude-code-feishu-channel Feishu SDK call layer (WSClient / im.message.* / im.image) rather than greenfielding it. Refs: - #179 parent RFC issue - #182 P0 tracker - RFC-020 §3.1 (飞书) / §2.5 (bridge worker model) / §5.2 (config landing) - RFC-002 §2.2 (telegram-bridge precedent) Agent: 通信IM马
…RFC-020 §3.1)
M2 fills the M1 stubs with real lark SDK wiring:
- FeishuAdapter.init — instantiates lark.Client from .platformConfig
- FeishuAdapter.start — opens lark.WSClient (long connection, no public IP),
registers EventDispatcher for `im.message.receive_v1`,
normalizes raw events to NormalizedIMEvent, gates via
the access whitelist (allowFrom + allowChats), and
dispatches via the caller-supplied handler.
- FeishuAdapter.stop — marks disconnected + drops references (lark 1.42 has
no public WSClient close).
- normalizeMessageEvent — supports text / file / sticker; image text-content
stays empty for M5 (`im.messageResource.get`).
`mentioned` is naive (mentions.length > 0); M5 will
refine by matching the bot's actual open_id.
- isAccessAllowed — sender.open_id ∈ allowFrom OR (group AND chat_id ∈ allowChats)
- auditLog — stderr-tagged audit trail for deny / error verdicts
- startFeishuBridge — accepts an optional onEvent; default logs to stderr so
the bridge process is observable without an agent
attached (useful for M2-level smoke).
Adds dep: @larksuiteoapi/node-sdk@^1.42.0 (official Feishu SDK).
Typechecks clean. No runtime behavior change for callers; M1 callers that
hit the throw-stubs now reach real wiring but still throw on .send (M3).
Refs: #179, #182, RFC-020 §2.3 (normalized event), §4.1 (access gate),
§4.3 (group trigger), §5.3 (secrets agent-local).
Agent: 通信IM马
…RFC-020 §4.2)
M3 closes the inbound→outbound loop:
- FeishuAdapter.send — text via `im.message.create` (open_id for DM,
chat_id for group). Threaded reply via
`im.message.reply` when the message carries a
replyToMessageId or target.threadRootId, preserving
Feishu root_id thread context.
- FeishuAdapter.edit — `im.message.update` to promote a "⏳ 处理中…"
placeholder into the final reply (≤20 edits/msg
budget is the caller's responsibility).
- Bridge IPC contract — BridgeIncomingEnvelope (bridge → parent) and
BridgeReplyEnvelope (parent → bridge) keyed on the
event's idempotencyKey. Pending events expire after
5min to bound memory.
- startFeishuBridge — picks the event handler automatically:
opts.onEvent > IPC (when process.send exists) >
stderr logger. No flag needed at fork time.
Image / file / card sending stays a M5 deliverable; M3 send/edit explicitly
throw on non-text payloads with a forward-pointing message.
Typechecks clean. End-to-end demo requires M4 (anet channel add feishu CLI +
agent-node fork wiring) before it can be smoke-tested against real Feishu
credentials.
Refs: #179, #182, RFC-020 §4.2 (outbound), §2.5 (bridge worker), §4.4 (event
key idempotency window).
Agent: 通信IM马
…er entry
CLI side (agent-network/bin/cli.ts):
- channelCommand now switches on type (telegram | feishu) instead of the
P0-only telegram gate; help text + usage examples cover both.
- writeFeishuChannelConfig writes `.anet/nodes/<n>/channels/feishu/`:
.env — FEISHU_APP_ID + FEISHU_APP_SECRET (chmod 600)
access.json — { allowFrom: [open_id], allowChats: [chat_id] }
- Interactive `anet channel add feishu <node>` prompts for app id / secret /
allow open_id; non-interactive supports `--app-id`, `--app-secret`,
`--allow <open-id>`, `--allow-chat <chat-id>`.
- attachChannel(profile, "feishu") wires it into config.json so the bridge
picks it up on next node start.
Worker side (agent-network/src/im/feishu/worker.ts):
- New standalone entry point that calls startFeishuBridge. Usable as:
1. Forked child of agent-node (stdio: [...,'ipc']) — bridge auto-switches
to BridgeIncoming/BridgeReply IPC handler.
2. Standalone debug runner — bridge falls back to stderr event logger.
- Parses --channel-dir + --node-alias from argv.
Out of M4 (folded into M5):
- agent-node spawn integration (fork(worker) when profile.channels includes
"feishu") — lands alongside the group @bot + image up/down work so it can
be smoke-tested end-to-end against the mock adapter in one pass.
Typechecks clean. End-to-end demo path is now wireable:
anet channel add feishu <node> --app-id ... --app-secret ... --allow ou_...
node dist/src/im/feishu/worker.js --channel-dir <path> --node-alias <alias>
→ WSClient connects → events normalized → access-gated → logged to stderr.
Refs: #179, #182, RFC-020 §3.1 / §5.1 / §5.2.
Agent: 通信IM马
23 tasks
…e-agent-sdk)
Lift the "P0: telegram only" channel gate in agent-node and add a forked
worker integration for Feishu:
- initFeishuChannel(spec) — validates `.env` + `access.json` presence,
hardens `.env` permissions (chmod 600). Mirrors initTelegramChannel.
- FEISHU_CHANNELS array, populated from CHANNELS by type filter.
- UNSUPPORTED_CHANNEL gate widened: `type !== "telegram" && type !== "feishu"`
→ existing nodes with only telegram see zero behavior change.
- Startup banner enumerates both kinds; "(none)" preserved.
connectFeishu(channel):
- Resolves the worker script via ANET_FEISHU_WORKER_PATH env override, dev
sibling layout, or installed npm layout (4 candidates total; warns + skips
if none exist so unrelated channels are not knocked offline).
- spawn(process.execPath, [worker, "--channel-dir", "--node-alias"], stdio
[..., "ipc"]) — gives the bridge IPC parent contract automatically.
- Parent IPC handler logs the inbound event and replies with a clear
placeholder so the full inbound→IPC→adapter.send round-trip is demoable
end-to-end on Vincent's credentials. M5b replaces the placeholder with
real claude-agent-sdk query() routing.
- on("exit") + on("error") warn instead of crashing the host node.
- isFeishuIncomingEnvelope() type-guards incoming IPC messages.
Build verifies clean (bun build src/cli.ts → 102 modules, 0.65 MB; existing
externals preserved).
Refs: #179, #182, RFC-020 §2.5 (bridge worker), §3.1 (Feishu).
Agent: 通信IM马
…20 §4.3) Refine group `mentioned` from M2's naive `mentions.length > 0` to compare each mention's `id.open_id` against the bot's actual identity: - FeishuAdapter.init now calls `fetchBotOpenId(client)` once, caching the result on `botOpenId`. Uses /open-apis/bot/v3/info via the SDK's request shim (lark 1.42 untyped envelope — accepts both `r.bot.open_id` and `r.data.bot.open_id` shapes). - normalizeMessageEvent now takes `botOpenId: string | null`; when non-null, `mentioned = mentions.some(m => m.id.open_id === botOpenId)`. When null (lookup failed — e.g. app not yet approved), it falls back to the M2 naive check so the bridge still functions. - adapter.start threads the cached `botOpenId` into the EventDispatcher handler. Effect: with RFC-020 §12.4 `groupPolicy: mention` (default), group messages that @ someone else no longer trigger the bot. M2 over-triggered any group message with a mention. Typechecks clean. Refs: #179, #182, RFC-020 §4.3 (group trigger), §12.4 (mention default). Agent: 通信IM马
Closes the M2 image-content TODO and adds outbound image support:
Inbound (download):
- FeishuChannelConfig gains `channelDir: string`, populated by the loader.
Adapter derives `mediaDir = <channelDir>/media/` at init.
- maybeAttachImages(rawEvent, normalized, client, mediaDir) — best-effort
resolves `image_key` from the raw payload, calls im.messageResource.get,
drains the response stream, writes `img_<ts>_<rand>.png` under mediaDir,
and patches `normalized.content.images = [localPath]`. Failure is
non-fatal; the text flow proceeds.
- Wired into the EventDispatcher handler in adapter.start.
Outbound (upload):
- adapter.send now branches on `message.imagePath`: uploadImage runs
im.image.create with a Readable stream from the file, captures the
returned `image_key`, and posts `msg_type: "image"` with content
`{image_key}`. Existing text path is unchanged and still preferred when
no imagePath is set.
Helpers (downloadImage / uploadImage) are non-throwing — they return null
on any failure path so callers can degrade gracefully (text flow keeps
working even if media misbehaves).
Typechecks clean.
Refs: #179, #182, RFC-020 §3.1.
Agent: 通信IM马
agent-network/docs/feishu-quickstart.md (new): - Feishu 开放平台「自建应用」 + WebSocket event subscription pre-flight - `anet channel add feishu` invocation (interactive + flag forms) - Startup log expectations + ANET_FEISHU_WORKER_PATH override - Trigger policy (DM allowlist + group mention) + bot open_id resolution - Outbound mechanics (create / reply / edit / image) - Troubleshooting matrix - Known first-cut limitations (no Dashboard topology, no active push, only claude-agent-sdk runtime) - E2E smoke checklist for the test handoff (L0–L10): env / config / start / inbound text / inbound group @bot / inbound image / access deny / reconnect / worker crash / outbound text / outbound image - Links to RFC-020 / RFC-002 / #179 / #182 / community SDK prior art agent-network/README.md: - One-line pointer next to the existing `anet channel add telegram` example. Closes the docs leg of M5. Remaining: dispatch the L0–L10 smoke checklist to a Docker-isolated tester (per [[feedback_delegate_testing]] / [[feedback_no_host_test_nodes]]); Vincent's live credentials still pending and unblocked of code work. Refs: #179, #182, RFC-020. Agent: 通信IM马
Single-line example next to the existing 'anet channel add telegram' example so readers discover the new feishu channel without scrolling into the quickstart guide. Agent: 通信IM马
The M5a placeholder reply ("[agent-node M5a placeholder ...]") is
replaced with a real think() invocation. Feishu inbound IM events
now run through the same processTask → think() → thinkQueue pipeline
that commhub-inbox messages and /loop wakes use.
Three-state error handling per IM马 IPC contract #5 ("never silent-drop"):
- think success → reply with the raw LLM text
- think failed=true → reply with "[agent-node 处理失败] " + text
- think threw exception → reply with "[agent-node 异常] " + msg
Empty-content edge (sticker / unsupported kind) replies a brief
notice instead of silently dropping the eventKey.
eventKey on the outbound reply echoes the inbound `event.idempotencyKey`
verbatim — required by the bridge's pending Map lookup
(src/im/feishu/bridge.ts REPLY_PENDING_TTL_MS).
Concurrency design (per 通信龙 ack — discussed in commhub thread):
- Uses the existing `thinkQueue` (cli.ts ~2189) which serializes
every think() call process-wide. Feishu messages now serialize
with commhub SSE inbox + /loop wakes — strictly stronger than
IM马's "per-conversation serial" requirement (per-conv ⊆
process-wide). Two concurrent feishu DMs won't drive the SDK
concurrently — they wait their turn behind whatever the agent
is currently thinking about.
- Cross-conversation parallelism is a follow-up (#182 / RFC-020
§4.4) once the SDK concurrency story is verified. The first-cut
bottleneck is acceptable + avoids unverified parallel-SDK risk.
Known limitation (M5c follow-up — also per 通信龙 ack):
- bridge.ts REPLY_PENDING_TTL_MS is 5 min. think() turns that
exceed 5 min will have their reply dropped by the bridge.
Vincent UAT bounded to simple tasks (<5 min) for M5b sign-off.
M5c options: progress-ack via adapter.edit OR a bumped TTL.
Verification — mock IPC self-test driving the handler logic with a
stub processTask and 4 inbound envelopes:
- "hello" (success) → reply "Hi from agent-node!" ✅
- "broken" (failed=true) → reply "[agent-node 处理失败] tool registry not ready" ✅
- "boom" (throws) → reply "[agent-node 异常] simulated SDK explosion" ✅
- "" empty content → reply "[agent-node] 收到事件但没有可处理的文本/图片内容。" ✅
- NO outbound reply contains the M5a placeholder string ✅
agent-node `bun test src/` → 222/222 pass (no new tests; the change
is entirely call-site wiring, behaviour covered by mock IPC self-test).
bun build clean — dist/cli.js 0.38 MB.
Stack on `feat/179-feishu-agent-sdk-channel` with IM马's M1-M5d work
already in place. Not shipped — awaiting 通信龙 diff review + Vincent
UAT (mock IPC + L0-L10 Docker smoke) before promote bumps agent-node
2.4.13 → 2.4.14-preview.0.
s2agi
pushed a commit
that referenced
this pull request
Jun 24, 2026
通信龙 task 0c61c552. Docker smoke for PR #258 (feat/179-feishu-agent-sdk-channel @ 3e57598 — M5b real think() IPC). In-scope subset (L0-L2, L6, L8, L9/L10 + Phase 0 typecheck/test/build): ✅ L0 env / L1 config loader / L2 worker startup ✅ L6 whitelist gate (config-level — live audit-log 待凭证) ✅ L8 worker crash recovery (SIGKILL → parent child.on('exit') fires) ✅ L9/L10 IPC envelope round-trip — KEY M5b gate: - fork stub child → emits {type:"event", event:{idempotencyKey,...}} - parent (mock processTask) → {type:"reply", eventKey:<echo>, text:<non-placeholder>} - assertions: eventKey === idempotencyKey, text non-empty + not in placeholder list (["", "ack", "[placeholder]", "[ack]", "[agent-node 占位]", "M5a 占位"]), child exits 0 ✅ Phase 0: agent-network tsc + bun test src/ + bun build worker.ts ⚠ Self-disclosed test-design bugs (not PR #258 regressions): - L1 asserted cfg.env?.FEISHU_APP_ID; loader returns flattened {appId, appSecret, access, ...}. Loader didn't throw → env DID load. - agent-node typecheck via naked bunx tsc --noEmit failed (no tsconfig, no typecheck script in agent-node package.json). Not Feishu-related. ⚠ Pre-existing test fragility (not PR #258 regression): - agent-node bun test src/: 221/222 pass. 1 fail = prepareGrokIsolatedCwd (#204 preview.7) mkdir-failure fallback test. PR commit msg itself states 222/222 in author env. Likely Docker permission env-sensitive. Outside #179 scope. ⏭ 待凭证 (Vincent's Feishu app required): - L3 inbound text DM / L4 group @bot / L5 inbound image / L7 reconnect 红线 compliance: - Docker container isolated (--rm, oven/bun:latest) - COMMHUB_DB=/tmp/feishu-smoke-commhub.db - No prod hub 47.x, no host hub - All test workdirs in /tmp Net: ✅ PR #258 ship-ready for no-creds subset. Live E2E needs Vincent's 凭证 next round. Artifacts: docs/tests/p-179-feishu-smoke/{REPORT.md, matrix.md, harness/, per-level logs}. Author-Agent: 通信测试马 Refs: #179 (parent), PR #258, RFC-020 §3.1 (Feishu adapter)
The M4 worker entry was committed without a corresponding bun build step in package.json. Result: npm consumers had no dist/src/im/feishu/worker.js to fork from agent-node, so live bring-up failed at the build step inside the Docker image. Add one bun build stanza right after node-server.js, mirroring the externals pattern (@larksuiteoapi/node-sdk stays as a runtime dep, not inlined). Verified: `npm run build` now emits dist/src/im/feishu/worker.js (8.4 KB, 4 modules bundled). Companion .d.ts files were already emitted by the tsc --emitDeclarationOnly step. Refs: #179 M4 (worker entry), #258. Agent: 通信IM马
…xt-only)
通信牛 review 必改2 — claude-agent-sdk runtime silently dropped images
because processWithClaude's signature was (task, from) — no images param.
think() at line 2218 already had `images?: string[]` flowing through to
the codex / grok branches; the claude branch was the only missing piece.
Pick (通信龙 ack): option C, mirror the Grok pattern. Real multimodal
wiring (AsyncIterable<SDKUserMessage> with image content blocks) is the
follow-up — its blast radius spans all claude-agent-sdk callers and
needs vendor verification (e.g. deepseek-v4-pro via /anthropic may not
accept image blocks on its Anthropic-compat shim).
Changes (1 file, +19/-2):
- processWithClaude(task, from) → processWithClaude(task, from, images?)
- On non-empty images: log a warn line in the same shape Grok already
uses (`[claude] image attachments (N) received but ... text-only ...
Real multimodal wiring is tracked in a follow-up issue.`). Images
remain on disk (adapter already persists them); the LLM does not
see them this round.
- think() line 2232: pass images through to processWithClaude.
Tests: agent-node `bun test src/` → 222/222 pass. bun build clean.
Stack on `feat/179-feishu-agent-sdk-channel` with M5b real-think + M5c
image up/download. Not shipped — awaiting IM马 必改1 + 必改3 (bridge
TTL bump + on-expire user-visible reply) + re-smoke before 2.4.15.
…non-DM (RFC-020 §4.3)
通信牛 review caught that isAccessAllowed only gated on whitelist
membership — any message in a whitelisted group would trigger the agent
regardless of whether the bot was @-mentioned. With groupPolicy=mention
(the default, RFC-020 §12.4), that's exactly the bug it's meant to prevent.
Rename + refactor:
- isAccessAllowed(event, access) → checkAccess(event, access, groupPolicy)
returns { allow, reason } so the audit log surfaces *why* a message was
denied (helps Vincent triage "I sent but got nothing" — saw it directly
in the dry-run when we forgot to allow his open_id).
- DM path: unchanged semantics (sender must be in allowFrom).
- Non-DM (group / channel / thread):
1. chat must be in allowChats (whitelist gate, unchanged)
2. THEN apply groupPolicy:
- "all" → trigger
- "mention" → require event.mentioned (M5b real bot open_id match)
- "command" → behaves as "mention" until slash-command parser lands
(TODO post-M5)
- "observe" → never trigger; chat is whitelisted for sidecar audit
visibility only
- Caller in adapter.start threads `groupPolicy` from feishuConfig into the
EventDispatcher closure alongside `access` and uses `verdict.reason` for
the audit-log line.
Typechecks clean.
Refs: #179, #182, RFC-020 §4.1 / §4.3 / §12.4.
Agent: 通信IM马
…e timeout
通信牛 review combined fixes — both touch the bridge IPC handler:
dedup (RFC-020 §4.4 socket-reconnect protection):
- withDedup() wrapper sits in front of any onEvent handler. It tracks
seen idempotencyKeys in a Map<key, ts>, GCs entries older than
DEDUP_WINDOW_MS (2 min) when the map grows past 200, and drops repeat
events with a stderr log line ([feishu:bridge] dedup drop <key>).
- Socket reconnect can replay events the platform already delivered;
without dedup, parent agent-node would think + reply twice. Now: drop
silently after the first.
必改3-a (TTL bound to channelConfig.taskTimeoutMs):
- REPLY_PENDING_TTL_MS const renamed to DEFAULT_REPLY_PENDING_TTL_MS and
bumped from 5min → 15min default. Covers the 95th-percentile think
duration for non-trivial tasks.
- startFeishuBridge reads channelConfig.taskTimeoutMs (loader default also
bumped accordingly) and threads `ttlMs` through to createIPCEventHandler.
- Users can override via .anet/nodes/<n>/channels/feishu/config.json
"taskTimeoutMs": 900000
必改3-b (TTL expiry — never silent-drop):
- Previously: expiry just evicted the pending entry. If the parent never
replied (think hung / failed silently), the Feishu user saw nothing.
- Now: on expiry, bridge sends
[处理超时,任务可能仍在后台运行]
into the originating conversation as a thread reply, then evicts. If a
late real reply arrives, the lookup misses (entry already evicted) and
the late reply is dropped — the user already knows.
- Failure-tolerant: the timeout-notify adapter.send is wrapped so a
send failure logs to stderr but doesn't kill the bridge.
Compositional shape:
adapter.start(withDedup(opts.onEvent ?? selectDefaultEventHandler(adapter, ttlMs)))
───────── ────────
dedup wrap 必改3 ttl threading
Both changes share the createIPCEventHandler infrastructure so they land
as one bridge.ts commit rather than two artificial splits.
Typechecks clean.
Refs: #179, #182, RFC-020 §2.5 (bridge worker), §4.4 (dedup), §4.5 (timeout).
Agent: 通信IM马
s2agi
pushed a commit
that referenced
this pull request
Jun 24, 2026
通信龙 task 10d3ff46. Branch HEAD 85538aa (4 new commits on top of R1 baseline 3e57598): - b875a16 必改2-C processWithClaude images + warn-only - 81d11bc 必改1 isAccessAllowed → checkAccess refactor - 85538aa dedup (withDedup) + 必改3 (TTL 15min + timeout-notify) R1 baseline re-run on R2 HEAD — all PASS, confirms checkAccess refactor doesn't break prior gates: Phase0 anet typecheck/test/build worker.ts ✓ Phase0 agent-node bun test src/ ✓ (221/1 fail = #204 pre-existing) Phase0 agent-node typecheck SKIP (no tsconfig, documented) L0 env / L1 config+chmod600 / L2 'bridge online' / L6 whitelist / L8 crash recovery / L9/L10 IPC eventKey echo + non-placeholder ✓ R2 new regression (3 mock-testable, no creds): R2.1 必改1 checkAccess group mentioned-gate — 7/7 cases PASS (DM allowed/denied, group mentioned=true/false, policy=all/observe, chat-not-in-allowChats) R2.2 dedup idempotencyKey 2-min window — 2 expected = 2 actual (same key dropped 2x, distinct key fires once) R2.3 必改3 TTL expire timeout-notify — '[处理超时]' sent on expiry, reply takes precedence when in-time, no silent drop Test approach: R2.1/2.2/2.3 use MIRROR scripts (checkAccess + withDedup + createIPCEventHandler are module-internal in adapter.ts/bridge.ts). Each mirror cites source line ref + "keep in sync" annotation. Live audit via real adapter happens in 待凭证 round. Strict per-level early-exit honored: 0 FAIL means no early-exit needed. Red lines: Docker --no-cache build (fresh git clone), --rm, COMMHUB_DB =/tmp/feishu-smoke-r2.db (isolated from R1). Artifacts: docs/tests/p-179-feishu-smoke/REPORT-R2.md + run-r2/ + harness/{entry-r2.sh, r2-checkaccess-test.mjs, r2-dedup-test.mjs, r2-timeout-notify-test.mjs}. Net: ✅ 13 PASS / 0 FAIL / 1 SKIP. PR #258 ship-ready for 2.4.15-preview.0 promote (no-creds gate). L4/L5 group@bot + image real-feishu still 待后续 per dispatch. Author-Agent: 通信测试马 Refs: #179, PR #258, RFC-020 §4.3 / §4.4
s2agi
pushed a commit
that referenced
this pull request
Jun 24, 2026
…eview.0 — bundle (loop-fix + feishu first-cut)
Bundle preview goes out for Vincent UAT (per 通信龙 + Vincent 拍 A):
- agent-node 2.4.13 → 2.4.15-preview.0
(skips 2.4.14 — loop-fix never got its standalone bump, rolled
forward into this bundle)
- agent-network 2.2.21 → 2.2.22-preview.0
(adds src/im/feishu/* — adapter/bridge/worker/config/index +
types from RFC-020 §3.1 M1-M5d work, plus M5b real think() wire-up
in agent-node and 必改 1/2-C/3+dedup from PR #258 review)
- commhub-server: unchanged (0.8.8)
What's in this preview:
1. /loop completion regex fix — `agent-node/src/goals/completion-
detect.ts` (single-line GOAL_COMPLETE / 目标已完成 sentinel
instead of bare 'completed' word-match that ended /loop after
one wake on every claude-agent-sdk node). 14 unit tests.
2. Feishu channel first-cut (#179) — `anet channel add feishu`,
bridge worker fork-integration, real think() via processTask
with thinkQueue serialization, group @mention policy gate,
bridge idempotency dedup, TTL bumped to 15min + user-visible
timeout reply on expiry, image upload/download path (images
persisted on disk; claude-agent-sdk runtime currently warn-only
downgrades to text-only — real multimodal wiring tracked in
#259, blocked on per-vendor capability matrix since deepseek's
/anthropic endpoint explicitly marks image content blocks
"Not Supported").
3. processWithClaude(task, from, images?) symmetric signature
(mirrors Grok pattern, keeps warn-on-non-empty contract).
PINNED_SERVER_VERSION stays "0.8.8" — commhub-server is byte-identical
to current latest, no chain re-pin needed.
This preview is NOT promoted to @latest. Vincent UAT bounded by the
RFC-020 §4.5 5-min default → 15-min new default task timeout; if any
single think exceeds 15min the bridge sends a user-visible timeout
reply (no more silent-drop). Promote-to-latest gates on Vincent UAT
sign-off as usual.
Refs: PR #258 (feishu first-cut), fix/loop-completed-bareword branch
(closed merged here), commhub thread b8d8tre7+.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Author
Agent: 通信IM马 (claude-code-cli runtime, RFC-020 主笔)
Co-owner: 通信IM牛 (codex runtime, peer review)
Scope (per Vincent 2026-06-24 decision)
claude-agent-sdkruntime + 飞书 + 私聊 + 群 @bot + 文本 + 图片. ETA ~9-13 工程小时 (see milestones below).第一刀路径 = agent-node 直 bridge + WSClient 长连接. 完整 RFC-020 §2.9 commhub-gateway schema 增量 deferred to follow-up PR after demo ships.
Vincent decisions on the 5 unknowns from the eval (relayed via 通信龙):
Milestones
45819dc)4 files, ~260 lines,
tsc --noEmitclean.src/im/feishu/{config,adapter,bridge,index}.ts@larksuiteoapi/node-sdkdep +adapter.init/adapter.start实装 +im.message.receive_v1normalize →NormalizedIMEvent+ access gatethink()bridge + outbound send + edit (~1-2h)adapter.send/adapter.edit+ bridge ↔ agent-node IPC for think round-tripanet channel add feishuCLI + agent-node fork integration (~2-3h)bin/cli.ts:4492switch overtype+ agent-node spawn bridge worker on node startmessage.mentionsparse +im.image.create/im.messageResource.get+docker mock smoke (派测试号) + README 片段
Refs
docs/rfcs/RFC-020-im-platform-integration.md(commit5216370, v3.1 Final)vansin/claude-code-feishu-channelSDK layer (validated)Test plan
Out of scope
anet im send新工具)Branch policy
从
origin/main(8afe9f6) 切出 fresh feature branch. PR 标 Draft 至 M5 完, 期间每 milestone 一个 commit + commhub 进度 ping. Promote latest 前 Vincent confirm (runtime 级).