Commit Graph

22 Commits

Author SHA1 Message Date
hongjr03 519ea8144f feat: add Feishu project onboarding card 2026-07-10 00:32:09 +08:00
hongjr03 5d315fff21 fix: enforce role tool whitelist 2026-07-09 20:48:03 +08:00
hongjr03 716101eed0 feat: add agent cost reporting 2026-07-09 20:29:56 +08:00
hongjr03 fc5a5365d8 fix: reply to Feishu trigger messages 2026-07-09 20:04:13 +08:00
hongjr03 fc0df7b823 fix: label Feishu trigger senders in agent prompts 2026-07-09 19:56:47 +08:00
hongjr03 f1d29176d3 feat: add slash command help 2026-07-09 19:43:13 +08:00
hongjr03 f260195439 fix: ack Feishu card actions before interrupt work 2026-07-09 19:26:57 +08:00
hongjr03 d01ea0e1cb feat: interrupt running agent via card button with confirm dialog
Add a  中断 button to live streaming cards; Feishu's native confirm
dialog is the confirmation step, so no extra card round-trip is needed.

- builder.ts: render interrupt action (danger button + confirm) while the
  run is live; new `interrupted` flag renders a 已中断 footer instead of
  完成/失败 on the complete card.
- streaming-card.ts: finish(text, { interrupted }) threads the flag into
  the final patch; overflow cards suppress the button.
- runner.ts: RunRequest gains abortController, passed to the SDK query;
  abort surfaces as RunStatus "interrupted" (no error) in the catch.
- trigger.ts: activeRuns registry maps runId → AbortController; onCardAction
  resolves the interrupt button, authorizes agent.cancel, aborts the run.
  Interrupted runs persist as CANCELED (spec RunState.canceled, terminal →
  lock released per ADR-0002).
- authorizer.ts: agent.cancel now consults PermissionSettings.agentCancel
  (MANAGE_ONLY/DISABLED) — previously only agentTrigger was honored, but
  spec/PermissionGrant deliberately splits these knobs ("谁能触发" ≠
  "谁能取消"). Default role remains MANAGE (Capability.normalCancel).

Tests: runner abort unit test, trigger interrupt + denied integration
tests, three agent.cancel authorization tests (manage allowed, edit denied,
DISABLED policy denies even manage). 26 files / 175 tests pass.
2026-07-09 18:59:31 +08:00
hongjr03 346aee5a68 fix: bind agent sessions to role 2026-07-09 16:06:06 +08:00
hongjr03 79505e6d44 fix: use startedAt instead of createdAt in trigger integration test
AgentRun model has startedAt, not createdAt. Codex introduced this
typo when refactoring the trigger queue integration tests.
2026-07-08 23:30:28 +08:00
hongjr03 36c0801e6f feat: add per-project message trigger wait queue
- TriggerQueue: FIFO per-project queue, max 5 items, 5min TTL expiry
- Replace reject-when-locked with queue: users get position feedback
- Run completion auto-starts next queued trigger (skips expired items)
- /reset clears project queue; 60s periodic purge interval
- Add unit tests (10) for queue lifecycle and edge cases
2026-07-08 17:31:43 +08:00
hongjr03 ad65d93b2e feat: add message burst debouncing (MessageBatcher)
- Create MessageBatcher: debounce 600ms, adaptive delay for long chunks,
  max 8 messages/4000 chars before immediate flush, per (chatId, sender) key
- Integrate into trigger: text messages enqueued, slash commands and
  file/image bypass batching, locked projects respond immediately
- Add unit tests (8) for batcher lifecycle and edge cases
2026-07-08 16:40:39 +08:00
hongjr03 a00e4f9871 fix(hub): 保留飞书回复上下文 2026-07-08 16:03:52 +08:00
hongjr03 dfcc2d70f8 feat(hub): 支持教师团队权限 2026-07-08 15:42:53 +08:00
hongjr03 3f486232a8 feat(hub): 抽出运行时配置 2026-07-08 15:25:49 +08:00
hongjr03 ead5cf04d6 fix(hub): 稳定 Feishu trigger 集成测试 2026-07-08 15:13:50 +08:00
hongjr03 e7924f78d5 feat(hub): 结构化运行历史(A-3) + lock 心跳(A-5)
A-3 结构化历史:
- 新增 AgentMessage 表(每条消息的 queryable 投影,transcript 文件仍是 agent 读回记忆)
- 新增 AgentFileChange 表(run 期间文件变更 before/after hash + operation)
- runner.ts: generateText 后 persistAgentMessages 落库 result.responseMessages
- workspace.ts writeFileTool: 写前算 beforeHash(SHA-256),写后算 afterHash,通过 ctx.onFileChange sink 记录
- runner.ts flushFileChanges: 收集 sink 的变更批量写入 AgentFileChange
- ToolContext 加 onFileChange sink 字段
- trigger.ts 接线 runId/sessionId/prisma 到 RunRequest

A-5 lock 心跳:
- ProjectAgentLock 加 heartbeatAt 列
- runner.ts onStepFinish 回调:每步 model step 后更新 heartbeatAt(吞错,lock 可能被 force-release)

60 tests 全过(tsc 干净)。
2026-07-07 22:20:30 +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 a2c8fa8eaf refactor(hub): 手搓 agent loop 换成 Vercel AI SDK
将 provider/runner/tools 三件手搓轮子替换为 AI SDK v7 的 generateText + tool:
- 删 openrouter-provider.ts(翻译层消失)、provider.ts 自定义消息类型
- runner.ts 循环体改为 generateText({stopWhen: stepCountIs}),保留 RunRequest/RunResult 公共签名
- tools.ts ToolRegistry 改为 ToolFactory + build(ctx, names?) 按 role 白名单构建 tools record
- workspace/cph/feishu 工具改写为 tool() 定义,execute 闭包捕获 per-run ToolContext
- transcript.ts 改为 ModelMessage[] JSONL(0.0.0 cutover,无迁移)
- server.ts/trigger.ts 接 modelFactory + toolWhitelist
- 测试:helpers 写 MockLanguageModel implements LanguageModelV4;5 个测试改写 + 新增 runner.test.ts
- 移除未使用的 openai 依赖
- ADR-0017 Consequences 同步:loop 委托给 AI SDK,provider seam 是 LanguageModel

tsc 干净,54 tests 全过。
2026-07-07 20:07:02 +08:00
hongjr03 afaf5bee09 feat(hub): RoleEntry 完整 bundle + per-run tool 白名单 + per-role 触发权限
RoleEntry 扩成 (model, systemPrompt, tools) bundle,id 兼任 slash command 名。
ToolRegistry.subset() 支持按白名单构造 per-run 视图,模型永远看不到 role 外的工具。
trigger 解析 /<role> 命令,查 RoleEntry 组装 systemPrompt + 子集 registry 传给 runner。
新增 RoleTriggerGrant 表 + canTriggerRole gate,与 ADR-0004 canTriggerAgent 串联:
先问'能不能触发 agent',再问'能触发哪个 role'。未配置 role 放行(back-compat)。

- models.ts: RoleEntry 加 systemPrompt + tools 字段
- tools.ts: ToolRegistry.subset(names) 返回共享 handler 的子集视图
- trigger.ts: extractRole 解析 slash; 传 systemPrompt + runTools; catch 链容错 P2025
- runner.ts: RunRequest.systemPrompt 改 string | undefined (exactOptionalPropertyTypes)
- schema.prisma + migration: RoleTriggerGrant(projectId, roleId, principal, revokedAt)
- permission.ts: canTriggerRole gate (有 grant 记录即白名单模式,含 revoked)
- server.ts: draft/review 两个 role 加 systemPrompt + tools 白名单
- 测试: role-permission.test.ts (5) + trigger.test.ts (+3), 53 全绿
2026-07-07 18:47:51 +08:00
hongjr03 0fe6cabc68 feat(hub): session transcript 连续性 + slash commands (/new /resume /reset)
修复每次 @bot 都是失忆状态的问题。
- transcript.ts: JSONL 文件存 session 对话消息(workspace/.cph/sessions/
  <id>.jsonl),append-only;和 Claude Code 一致;ADR-0003 说的'不存全
  历史'是群聊历史,session 对话是 Hub 自己产生的,不冲突
- runner.ts: RunRequest 加 transcriptPath;run 开始时读历史拼进初始
  messages(system prompt 只在首条);run 结束时 append 本轮新消息
  (loadedCount 区分历史 vs 新增,不重复写);best-effort(写失败不丢
  run 结果)
- trigger.ts: slash commands 在 lock 检查之前(/new /resume /reset
  不创建 run 不需要锁);transcriptPath 传给 runner
  /new = 归档当前 session,下次 @bot 自动建新的
  /resume = 恢复最近归档的 session
  /reset = 同 /new
  未知 /cmd 透传给 agent 当普通 prompt
- unit: transcript.test.ts 读写回环/append 累积/损坏行恢复/路径(6)
- integration: trigger.test.ts 加 /new /resume /reset /unknown(4)
修了 slash command 被 lock 挡住的问题(提到 lock 之前)。
vitest run 45/45 通过;tsc rc=0。
2026-07-07 17:50:05 +08:00
hongjr03 e60aa43731 test(hub): integration 测试套件(13 tests,真 prisma+真 cph)
三层 integration,对齐 unit 层:
- trigger.test.ts: @bot→run→lock→release→card 全链路;权限拒绝(READ
  不能 triggerAgent ADR-0004);竞态 busy(ADR-0002);未绑定 chat 忽略
  (ADR-0001);无 @bot 忽略 —— 5 tests
- lock.test.ts: acquire/release 回环;排他性(同 project 二次 acquire 抛);
  WellFormed(终止 run 持锁即抛);release 幂等;isTerminal 对齐 spec —— 5 tests
- cph.test.ts: 真 cph binary 跑 TH-141 examples 的 check(exit 0)+
  build student PDF(exit 0)+ 空目录 check(非 0)—— 3 tests
helpers: resetDb(truncate CASCADE)、mockFeishuRuntime(记录 sentTexts/
  sentCards)、MockProvider(fixed stop 响应)、seedProject(project→user→grant)
prisma migrate dev init 生成初始 migration 并应用到 cph_hub_test 库。
vitest config 改 fileParallelism=false(integration 共享一 DB,串行避免
truncate/insert 竞态)。
vitest run 35/35(22 unit + 13 integration)通过;tsc rc=0。
2026-07-07 15:40:28 +08:00