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 全过。
This commit is contained in:
2026-07-07 20:07:02 +08:00
parent afaf5bee09
commit a2c8fa8eaf
17 changed files with 516 additions and 808 deletions
+12 -12
View File
@@ -2,8 +2,8 @@
* Hub entry — Fastify server + Feishu WebSocket listener.
*
* Wires the full trigger path: lark ws receives `im.message.receive_v1` →
* trigger handler resolves chatproject (ADR-0001), creates AgentRun + acquires
* the project lock (ADR-0002), runs the provider-agnostic agent loop
* trigger handler resolves chat->project (ADR-0001), creates AgentRun + acquires
* the project lock (ADR-0002), runs the AI SDK-backed agent runner
* (ADR-0017), and releases the lock on finish. Feishu context reads inside the
* loop are ADR-0003-authorized (chat must match binding).
*
@@ -13,7 +13,7 @@
import "dotenv/config";
import Fastify from "fastify";
import { prisma } from "./db.js";
import { OpenRouterProvider } from "./agent/openrouter-provider.js";
import { createModelFactory } from "./agent/provider.js";
import { InMemoryModelRegistry } from "./agent/models.js";
import { ToolRegistry, feishuContextTool } from "./agent/tools.js";
import { readFileTool, writeFileTool, listFilesTool } from "./agent/workspace.js";
@@ -34,7 +34,7 @@ function requireEnv(name: string): string {
async function main(): Promise<void> {
const databaseUrl = requireEnv("DATABASE_URL");
void databaseUrl; // prisma reads DATABASE_URL from env directly
const openrouterKey = requireEnv("OPENROUTER_API_KEY");
requireEnv("OPENROUTER_API_KEY");
const feishuAppId = requireEnv("FEISHU_APP_ID");
const feishuAppSecret = requireEnv("FEISHU_APP_SECRET");
const feishuBotOpenId = requireEnv("FEISHU_BOT_OPEN_ID");
@@ -45,7 +45,7 @@ async function main(): Promise<void> {
app.get("/api/healthz", async () => ({ ok: true, ts: Date.now() }));
// --- Agent seam wiring ---
const provider = new OpenRouterProvider({ apiKey: openrouterKey });
const modelFactory = createModelFactory();
const models = new InMemoryModelRegistry(
[
{ id: "z-ai/glm-4.6", label: "GLM-4.6", toolCapable: true },
@@ -79,17 +79,17 @@ async function main(): Promise<void> {
const tools = new ToolRegistry();
// ADR-0003: the handler enforces chat==boundChat before any API call;
// real read wraps @larksuiteoapi/node-sdk's im.v1.message get/list.
tools.register(feishuContextTool(readFeishuContext, feishuRt));
tools.register("feishu_read_context", feishuContextTool(readFeishuContext, feishuRt));
// Workspace file tools (ADR-0007 directory tree, path-confined).
tools.register(readFileTool());
tools.register(writeFileTool());
tools.register(listFilesTool());
tools.register("read_file", readFileTool);
tools.register("write_file", writeFileTool);
tools.register("list_files", listFilesTool);
// cph CLI tools (ADR-0016 version contract enforced by cph itself).
tools.register(cphCheckTool());
tools.register(cphBuildTool());
tools.register("cph_check", cphCheckTool);
tools.register("cph_build", cphBuildTool);
// --- Feishu listener (reuses the same lark client) ---
const trigger = makeTriggerHandler({ prisma, provider, tools, models, logger: app.log });
const trigger = makeTriggerHandler({ prisma, modelFactory, tools, models, logger: app.log });
startFeishuListenerWithClient(feishuConfig, larkClient, app.log, trigger);
app.listen({ port, host: "0.0.0.0" }, (err, address) => {