Commit Graph

18 Commits

Author SHA1 Message Date
hongjr03 fc5a5365d8 fix: reply to Feishu trigger messages 2026-07-09 20:04:13 +08:00
hongjr03 f260195439 fix: ack Feishu card actions before interrupt work 2026-07-09 19:26:57 +08:00
hongjr03 d6724e5a83 feat: streaming agent card with tool-use trace, reasoning panel, and tool results
Replace text-only PatchableTextStream with a unified StreamingAgentCard that
renders the full agent run lifecycle in a single Feishu interactive card:

- Tool-use panel: collapsible, shows each tool call with status (running/
  success/error), input summary, and result/error in code blocks
- Reasoning panel: collapsible, streams thinking text inline then collapses
  on completion
- Answer text: rendered via native Feishu markdown component (no post-row
  round-trip)

Runner now captures previously-discarded SDK events:
- thinking_delta → reasoning stream
- tool_use id + input on content_block_start / assistant message
- tool_result from user messages (was entirely unhandled)

New files:
- card/trace-store.ts: per-run in-memory tool-use trace with secret redaction
- card/builder.ts: Feishu card JSON builder (tool panel + reasoning + text)
- card/streaming-card.ts: StreamingAgentCard controller (400ms throttled)

client.ts: add sendCard/patchCard raw JSON primitives
2026-07-09 17:18:40 +08:00
hongjr03 491fd13ca8 fix: resolve Feishu sender names via basic profile API 2026-07-08 23:59:17 +08:00
hongjr03 a0df8b6bcf fix: repair streaming output broken by markdown rendering changes
Two bugs were introduced when sendTextMessage was changed to auto-detect
message format (text/post/interactive):

1. Message type mismatch: sendTextMessage could create a text or post
   message, but patchTextMessage only works on interactive cards.
   Fix: add sendInteractiveCardMessage for streaming create (always
   interactive, always patchable).

2. flushTextToSink state corruption: the text management after flush
   had a flawed appendedDuringFlush heuristic that could lose text or
   duplicate it. Fix: properly track sent vs unsent text by removing
   the sent prefix length, keeping only the last chunk for future patches.

Also fix finish() to flush pending text regardless of currentMessageId.
2026-07-08 17:48:55 +08:00
hongjr03 4479f88f4f feat: add sender profile cache with LRU eviction
- SenderNameCache: TTL 10min, LRU max 2048 entries, getOrFetch pattern
- resolveSenderName: API call with cache integration
- Trigger uses cache for approval card resolutions and audit metadata
- Add unit tests (9) for cache lifecycle, LRU, expiry, and null handling
2026-07-08 17:22:23 +08:00
hongjr03 2736006262 feat: add retry with exponential backoff for file send/download
- withRetry helper: configurable maxAttempts, baseDelayMs, shouldRetry
- sendFile: upload and message send wrapped with 3 retries, 1s/2s/4s backoff
- downloadMessageFile: wrapped with 3 retries
- Add unit tests (6) for retry behavior and sendFile retry integration
2026-07-08 17:15:30 +08:00
hongjr03 294d51dbfe feat: parse inbound post (rich text) messages
- parsePostMessage: full Feishu post payload parser with locale fallback,
  supports text/a/at/img/media/file/code_block/br/hr/emotion elements
- Trigger accepts post messages, extracts text + downloads attachments
- Post messages with attachments bypass batching, text-only posts batch
- Add unit tests (10) for all element types and edge cases
2026-07-08 17:05:51 +08:00
hongjr03 bef10149a9 feat: improve outbound message chunking with code-block-aware splitting
- Increase max message length from 4000 to 8000 chars
- Add splitAtBoundary: paragraph > newline > space priority, never split
  inside fenced code blocks (move split to before opening fence)
- Add sendLongText: sends multi-chunk long messages sequentially
- Update PatchableTextStream.flush to use splitAtBoundary
- Add unit tests (6) for splitting edge cases
2026-07-08 16:57:53 +08:00
hongjr03 4736610cf3 feat: add interactive approval/confirmation cards
- sendApprovalCard: interactive card with configurable buttons
- resolveCard: patch card to show resolution state (green/red)
- ApprovalManager: register/resolve lifecycle with 5min timeout
- request_approval MCP tool: agent can ask user for confirmation
- Wire onCardAction in trigger handler and server startup
- Add unit tests (5) for card JSON, manager, and routing
2026-07-08 16:47:31 +08:00
hongjr03 6227e1931b feat: improve Feishu markdown rendering and processing status feedback
- Add buildOutboundPayload: detect markdown format, isolate code blocks,
  force plain text for tables, fallback from post to text on API rejection
- Add addReaction/removeReaction for processing status lifecycle
- Replace THUMBSUP with Typing→(remove on success | CrossMark on failure)
- Add unit tests for markdown rendering (15) and reactions (6)
2026-07-08 16:31:07 +08:00
hongjr03 a00e4f9871 fix(hub): 保留飞书回复上下文 2026-07-08 16:03:52 +08:00
hongjr03 6178664696 fix(hub): send Feishu files explicitly 2026-07-08 11:48:25 +08:00
hongjr03 eaa7fd4fd8 feat(hub): 文件收发 + 卡片回调 + 更好的流式
client.ts 重写:
- downloadMessageFile: 下载飞书消息里的文件/图片到本地
- sendFile: 上传文件到飞书 + 发送文件消息(msg_type=file)
- CardActionEvent 类型 + startFeishuListenerWithClient 加 onCardAction
  回调参数,注册 card.action.trigger 事件(消除了 'no card.action.trigger
  handle' 警告)
- 去掉未使用的 card builder 函数

trigger.ts:
- 处理 file/image 消息:下载到 workspace/.cph/inbox/,prompt 里告诉
  agent 文件路径
- 流式 throttle 从 800ms 降到 400ms,初始占位从 '…' 改为空(更干净)
- run 完成后扫描 workspace/build/ 下 5 分钟内的新文件,自动发送给
  老师(cph build 产出的 PDF 自动回传)

tsc rc=0。
2026-07-08 02:13:51 +08:00
hongjr03 4bd5b03bb1 refactor(hub): Vercel AI SDK → Claude Code SDK
用 @anthropic-ai/claude-agent-sdk 的 query() 替换 Vercel AI SDK 的
streamText。SDK 自带 Read/Write/Bash/Glob/Grep 工具——read_file/
write_file/list_files/cph_check/cph_build 全部不需要了,Bash 跑
cph 直接。

UX 同时改成 workbuddy 风格:
- 收到 @bot → 表情回复(👍)确认收到,不发'已开始处理'
- agent 回复作为文本流式发送(text-as-card patch,markdown 渲染)
- 超长自动续发新消息,不截断
- 完成/出错不再额外发状态消息
- 去掉卡片(model选择器/取消按钮)

ADR-0017 更新:放弃 provider-agnostic,Claude Code SDK 是 Anthropic
专有。换来:compaction、工具审批、partial message 流式、成熟 agent
loop。

tsc rc=0。旧文件(workspace.ts/cph.ts/provider.ts/tools.ts)暂留,
下个 commit 清理。
2026-07-08 01:36:59 +08:00
hongjr03 f8c3e16a52 feat(hub): 事件去重(A-4) + 审计写入路径(A-8)
A-4 FeishuEventReceipt:
- 新增 FeishuEventReceipt 表(eventId @unique),lark ws 至少一次投递的幂等兜底
- MessageReceiveEvent 加 header.event_id 字段
- trigger 入口查重:已存在的 event_id → 跳过(不建第二次 run)
- resetDb 加入 FeishuEventReceipt

A-8 审计写入:
- AuditEntry 加 projectId(权限拒绝/slash 命令等无 run 的审计点可定位)
- 新建 src/audit.ts:writeAudit(prisma, entry) best-effort 写入(吞错,不阻断 trigger)
- trigger 五个审计点:trigger.denied / trigger.role_denied / run.created / run.lock_race / run.finished / run.failed
- 新增 audit unit 测试(3) + trigger 集成测试(去重 + 审计,2)

59 tests 全过。
2026-07-07 21:34:15 +08:00
hongjr03 ce767e9cab feat(hub): 真实飞书读取 + 状态卡片 + 权限 gate + 部署脚本
FeishuRead(ADR-0003): feishuContextTool 从 stub 换成真实 lark
  im.v1.message.get(单条 trigger/reply/status_card)+ list(thread 回复);
  ADR-0003 chat 授权不变式不变(handler 已 gate)。
StatusCard: sendCard + buildRunStatusCard——完成/出错发 interactive card
  (status 文案 + model 选择器 + 取消按钮带 runId),替代纯文本。
Permission(ADR-0004, project-wise): triggerAgent 查 PermissionGrant 对
  project 的 edit+ 能力(manage⊇edit 单调);sender open_id 当 principal
  (子类型学 OPEN,务实选择,permission.ts 标注);per-artifact OPEN;
  settings 组合规则 OPEN。无权限回 '无权限触发'。
Deploy: cph-hub.service systemd unit(ExecStartPre 跑 prisma migrate)+
  install_service.sh(idempotent,seed env)+ deploy_platform.sh
  (rsync + npm ci + build + restart + healthz)。
client.ts 抽 createLarkClient/startFeishuListenerWithClient,tools 与
  ws 共用同一 client。
tsc rc=0;prisma validate 通过;deploy 脚本 bash -n 通过。
2026-07-06 23:44:07 +08:00
hongjr03 313e890b6c feat(hub): 飞书 @bot 触发链路 + ProjectAgentLock 不变式(ADR-0001/0002)
群消息事件 → 创建 AgentRun → 获锁 → 调 runAgent → 释放锁 + 状态回写:
- feishu/client.ts: lark WSClient 长连接接收 im.message.receive_v1;
  sendText 经 client.im.v1 发送(SDK 运行时动态 API,类型弱化务实收口)
- feishu/trigger.ts: @bot 识别(chat 绑定解析 ADR-0001)→ 创建 run+
  session(provider-bound ADR-0017)→ acquireLock(ADR-0002,竞态回退 busy)→
  runAgent(fire-and-forget)→ finally releaseLock;非项目群/无 @bot/非文本
  全部忽略
- lock.ts: ADR-0002 WellFormed 落代码——currentLockRunId 读锁时校验
  持锁 run 非终止,终止 run 持锁即 release-path bug(抛而非静默)
- models.ts: resolve 提到 ModelRegistry 接口(之前漏在实现上)
- server.ts: Fastify + lark ws 启动,/api/healthz
prompt 提取 smoke 通过(@bot+prompt→prompt;纯@bot→null;无mention→null;
非文本→null)。修了 mention 正则 bug(/@_\w+_/ 的回溯错误)。tsc rc=0。
2026-07-06 23:11:35 +08:00