hongjr03
|
a766d99573
|
fix(hub): 修复 Hub 部署脚本失败处理
|
2026-07-08 15:14:23 +08:00 |
|
hongjr03
|
1cfda3ccd2
|
ci(hub): 补齐 Hub 检查和健康 smoke
|
2026-07-08 15:14:06 +08:00 |
|
hongjr03
|
ead5cf04d6
|
fix(hub): 稳定 Feishu trigger 集成测试
|
2026-07-08 15:13:50 +08:00 |
|
hongjr03
|
ecbc2c8666
|
fix(hub): 对齐 ADR-0018 agent 执行面边界
runner.ts: 启用 SDK 内置沙箱(bubblewrap/seatbelt),写入限制在 workspace,
denyRead 敏感路径,failIfUnavailable 硬拒非沙箱运行。bypassPermissions 保留
(headless 无交互),沙箱是硬边界而非权限提示层。
install_service.sh: 默认 service user 从 root 改为 cph-hub;env 模板修正
OPENROUTER_API_KEY → ANTHROPIC_AUTH_TOKEN/ANTHROPIC_BASE_URL/ANTHROPIC_API_KEY
(与 server.ts 实际读取一致);补 __BASE_DIR__ sed 替换。
cph-hub.service: AssertPathExists=/usr/bin/bwrap 强制沙箱依赖;NoNewPrivileges。
不加 ProtectSystem/ProtectHome(会破坏 cph render-cache 与 bwrap user-namespace)。
trigger.ts: 权限闸门去掉 if (senderOpenId !== '') 短路——缺 open_id 一律
fail-closed 拒绝,不再静默跳过;role gate 同步去掉冗余守卫(已由上游保证)。
顺手把 JSON.parse(msg.content) 的 inline cast 换成 Zod schema parse
(FileMessageContentSchema / TextMessageContentSchema)。
ADR-0018: 状态改为 Accepted+implemented,机制段写定 SDK 沙箱,网络决定为开放,
Open Questions 更新。
|
2026-07-08 13:11:14 +08:00 |
|
hongjr03
|
6178664696
|
fix(hub): send Feishu files explicitly
|
2026-07-08 11:48:25 +08:00 |
|
hongjr03
|
c73b41e1da
|
fix(hub): resume Claude SDK sessions
|
2026-07-08 11:48:20 +08:00 |
|
hongjr03
|
012c8a7d89
|
fix(hub): default to OpenRouter Sonnet model
|
2026-07-08 11:48:16 +08:00 |
|
hongjr03
|
250f918346
|
chore: delete dead code
|
2026-07-08 02:37:44 +08:00 |
|
hongjr03
|
30d20421b3
|
fix(hub): busy 表情从 Hourglass 改为 OnIt(飞书支持的 emoji_type)
|
2026-07-08 02:22:05 +08:00 |
|
hongjr03
|
20bf7e271f
|
fix(hub): 启动重置状态 + lazy-init 修复空白消息 + busy 改表情
三个修复:
1. 启动时清锁+标死run为FAILED(防重启后卡死)
2. 空白消息修复:lazy-init 改为第一个 text-delta 立即创建消息
(不走 throttle);.then 加 fallback:没收到 text-delta 时用
result.text 发完整回复
3. busy 提示从文本'项目正在处理中'改为⏳表情回复
tsc rc=0。
|
2026-07-08 02:20:49 +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
|
1ad78dbcce
|
test(hub): 适配 Claude Code SDK 的 mock 修复
helpers.ts: MockLanguageModel 加 doStream 实现(返回 text-delta +
finish part);runner.test.ts: 加 stubPrisma(必填字段)。
这些测试在 Claude Code SDK 迁移后还需要进一步适配,先提交 buffer。
|
2026-07-08 01:52:21 +08:00 |
|
hongjr03
|
085f7c7186
|
fix(hub): 去掉冗余 OPENROUTER_API_KEY,直接用 ANTHROPIC_AUTH_TOKEN
|
2026-07-08 01:48:01 +08:00 |
|
hongjr03
|
b9f50407be
|
fix(hub): Claude Code SDK 经 OpenRouter Anthropic Skin,保留 provider-agnostic
我之前判断错了——OpenRouter 有 Anthropic Messages API 兼容端点
(https://openrouter.ai/api),Claude Code SDK 直连不需要代理。
ANTHROPIC_AUTH_TOKEN=OpenRouter key,ANTHROPIC_API_KEY='' 即可。
而且 ANTHROPIC_DEFAULT_SONNET_MODEL 可设 OpenRouter model ID
(z-ai/glm-4.7 等),所以 provider-agnostic 没丢。
ADR-0017 重写:从'Anthropic 专有'改为'Claude Code SDK via OpenRouter,
保留 provider-agnostic'。tsc rc=0。
|
2026-07-08 01:46:09 +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
|
1abcc495f4
|
test(hub): 真实 model 集成测试 + Gitea CI workflow
real-model.test.ts: 用真 OpenRouter model + 真 cph + 真 workspace 跑 agent
tool-calls 循环,验证 MockProvider 测不到的路径(model 返回 tool_calls →
dispatch → tool_result 回传 → 继续/stop)。没 OPENROUTER_API_KEY 时自动
skip,不阻塞 CI;有 key 时自动跑。
两个 test:(1) list files → cph check → 回答;(2) write file → read back。
hub-check.yml: Gitea CI workflow,对齐 checker-check/spec-check 的纪律:
npm ci → prisma generate → tsc → prisma validate → 装 cph → vitest unit →
vitest integration(postgres service container)→ real-model(可选,从
secrets 注入 key)。
全 suite: 60 passed + 2 skipped(无 key)。tsc rc=0。
|
2026-07-07 23:45:59 +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 |
|
hongjr03
|
09cadd0148
|
test(hub): Vitest suite + 三组 unit 测试(22 tests)
用 Vitest 组织测试,对齐 Courseware 半边的测试纪律(非散脚本):
- test/unit/workspace.test.ts: 路径 confinement(读写回环/..逃逸/绝对路径逃逸/
父目录创建/列举/不存在 error JSON)—— 11 tests
- test/unit/prompt.test.ts: @bot 提取(单 mention/多 mention/纯@bot/无 mention/
非 JSON/无 text 字段)—— 8 tests
- test/unit/permission.test.ts: roleGrantsEdit 单调性(READ 不含 / EDIT 含 /
MANAGE 含,ADR-0004 can_mono)—— 3 tests
extractPrompt 导出供测试。修了 test helper 默认参数 bug(显式传 undefined 触发
默认值,绕过 mentions 检查)。
vitest run 22/22 通过;tsc rc=0。
Integration 层(trigger 全链路 + lock WellFormed + 真 cph)待本地 postgres
后再起。
|
2026-07-07 15:26:53 +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
|
a4d52e1850
|
feat(hub): workspace 文件工具 + cph 子进程工具(hub↔courseware 耦合点)
agent 的窄工具面补全:
- workspace.ts: read_file/write_file/list_files,路径全 confinement 到
ctx.workspaceDir(ADR-0007 目录树);.. 与绝对路径逃逸返 error JSON 不中断 run
- cph.ts: cph_check/cph_build 子进程,cwd=workspace;ADR-0016 版本契约由 cph
自身在 load 阶段校验(cphVersionMismatch),Hub 透传诊断不重复校验;
CPH_BIN 可配,缺二进制返 exitCode 127 不抛
- server.ts: 注册全部 5 个工具
修 dynamic import(rule: ts-no-dynamic-import)——mkdir 改顶部静态 import;
三个 handler 的 confine 包进 try(操作性错误返 JSON 而非冒泡中断 run)。
tsc rc=0;smoke 通过(逃逸拒绝/读写回环/列举/cph ENOENT)。
|
2026-07-06 23:19:28 +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 |
|
hongjr03
|
b4dd8f09e1
|
feat(hub): Prisma schema + DB singleton,对齐 spec/System 契约
按 ADR-0001..0004/0017 重塑 schema,非照搬旧服务:
- ADR-0001: ProjectGroupBinding project↔chat 1:1(chatId @unique 钉单射不变式)
- ADR-0002: ProjectAgentLock projectId @id(at-most-one)+runId @unique;holder=run 非 session
- ADR-0004: PermissionGrant(resource×principal×role)+PermissionSettings(六旋钮)新落;
PermissionRole=read/edit/manage 与 PlatformRole=admin/teacher 分离(旧 schema 混淆了)
- ADR-0017: AgentSession 无 claudeSessionId,改 provider+model(切 model 即新 session)
- RunState 对齐 spec:加 WAITING_FOR_USER/TIMED_OUT(枚举完整性 OPEN)
- Audit:runId 是 PINNED 关系,其余 OPEN
旧 schema 的 FeishuProjectBinding(user/chat 混表)拆为单一 ProjectGroupBinding。
prisma validate 通过;generate 通过;tsc --noEmit rc=0;
类型级 schema 约束检查全部通过(ADR-0002 主键/唯一、ADR-0017 无 claudeSessionId 等)。
|
2026-07-06 22:48:28 +08:00 |
|
hongjr03
|
a08c33205b
|
refactor(hub): role 改为数据条目,不再是写死的闭集枚举
ADR-0017 说 role-based routing 是 product/admin config,不是 spec invariant。
之前 RunRole = "draft"|"review"|"triage" 闭集违反这条——role 钉死在代码里,
教师/admin 无法自定义。改为:
- RoleEntry(id+label+defaultModel)是数据,registry 持有 role 集合
- roleId 退化为 string,新 role 是配置改动不是代码改动
- runner 不再带 role 字段(model 已由 caller resolve 好传入,role 透传无意义)
- 未知 role 优雅降级到首个启用 model,不抛
tsc --noEmit rc=0;role-as-data + ADR-0003 不变式 smoke 通过。
|
2026-07-06 22:41:39 +08:00 |
|
hongjr03
|
3bca137b48
|
feat(hub): provider-agnostic agent seam 骨架(ADR-0001/0003/0017)
hub/ TS 新部件,平级于 spec/。本仓从零 TS 重写(非迁移旧服务),栈:
Fastify+Prisma 待补;本提交落 provider-agnostic agent runtime 骨架:
- provider.ts: AgentProvider seam,OpenAI-compatible messages+tools+tool_calls
形状(语言通解,非 OpenAI 承诺);@Claude 仅为触发品牌(ADR-0017)
- openrouter-provider.ts: openai SDK 指向 OpenRouter 的具体适配器,仅做
Message↔SDK 翻译;agent loop 不 import SDK,换 provider 不改 loop
- runner.ts: 自有 agent loop(dispatch tools/循环到 stop/budget),per-run
model(ADR-0017 role-based routing,run 带其 model)
- tools.ts: 窄工具面 + ADR-0003 McpReadRequest.Authorized 落代码:
feishu_read_context 拒绝非本项目绑定 chat 的读取(实测错 chat 抛
UnauthorizedChatRead,对 chat 放行)
- models.ts: ModelRegistry seam(role→default 映射策略 OPEN)
- server.ts: 接线入口(stub Feishu read,无活凭证即可编译)
tsc --noEmit rc=0;ADR-0003 不变式 smoke 通过。
|
2026-07-06 22:38:04 +08:00 |
|