Skip to content

feat(#179): Feishu channel for claude-agent-sdk runtime — RFC-020 first-cut [WIP]#258

Merged
s2agi merged 14 commits into
mainfrom
feat/179-feishu-agent-sdk-channel
Jun 24, 2026
Merged

feat(#179): Feishu channel for claude-agent-sdk runtime — RFC-020 first-cut [WIP]#258
s2agi merged 14 commits into
mainfrom
feat/179-feishu-agent-sdk-channel

Conversation

@s2agi

@s2agi s2agi commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

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-sdk runtime + 飞书 + 私聊 + 群 @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 通信龙):

Q Decision
Q1 群聊 @bot 含 (+1h)
Q2 飞书消息进 Dashboard 第一刀不做; 走完整 §2.9 收尾 PR
Q3 群聊触发策略 mention (RFC §12.4 默认)
Q4 图片上下行 含 (+1h, 社区 SDK 已有)
Q5 飞书 App ID/Secret Vincent 那侧出, 工程并行不等

Milestones

  • M1 — Adapter + Bridge scaffold (~30min actual, commit 45819dc)
    4 files, ~260 lines, tsc --noEmit clean.
    src/im/feishu/{config,adapter,bridge,index}.ts
  • M2 — WSClient + EventDispatcher + access whitelist + audit log (~2-3h)
    @larksuiteoapi/node-sdk dep + adapter.init/adapter.start 实装 +
    im.message.receive_v1 normalize → NormalizedIMEvent + access gate
  • M3 — think() bridge + outbound send + edit (~1-2h)
    adapter.send / adapter.edit + bridge ↔ agent-node IPC for think round-trip
  • M4 — anet channel add feishu CLI + agent-node fork integration (~2-3h)
    bin/cli.ts:4492 switch over type + agent-node spawn bridge worker on node start
  • M5 — Group @bot + image up/down + Docker smoke + docs (~2-3h)
    message.mentions parse + im.image.create / im.messageResource.get +
    docker mock smoke (派测试号) + README 片段

Refs

Test plan

  • Docker mock + 派测试号 (不在本机起测试节点; 不连生产 hub)
  • WSClient unit + 集成 (mock 飞书事件) at M5
  • 活体 E2E 等 Vincent 凭证 (不阻塞 M1-M5 工程)

Out of scope

  • WhatsApp / 企微 / Slack (post-MVP)
  • 飞书消息进 Dashboard 拓扑 (走完整 §2.9 收尾 PR)
  • agent 主动推 IM (RFC §12.9: P1 不做, P1.5 加 anet im send 新工具)
  • 富交互卡片 (post-MVP)

Branch policy

origin/main (8afe9f6) 切出 fresh feature branch. PR 标 Draft 至 M5 完, 期间每 milestone 一个 commit + commhub 进度 ping. Promote latest 前 Vincent confirm (runtime 级).

…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马
…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 s2agi merged commit dbea642 into main Jun 24, 2026
0 of 3 checks passed
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+.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants