Files
curriculum-project-hub/hub/src/server.ts
T
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

61 lines
1.9 KiB
TypeScript

/**
* Hub entry — skeleton.
*
* Wires the provider-agnostic agent seam end-to-end with a no-op Feishu read, so
* the invariant path is exercised without live credentials. Real wiring (Fastify
* server, Feishu OAuth/event callbacks, Prisma, admin-web) lands incrementally;
* this file proves the seam compiles and the pieces connect.
*/
import { OpenRouterProvider } from "./agent/openrouter-provider.js";
import { InMemoryModelRegistry } from "./agent/models.js";
import { ToolRegistry, feishuContextTool } from "./agent/tools.js";
import { runAgent } from "./agent/runner.js";
function main(): void {
const apiKey = process.env["OPENROUTER_API_KEY"];
if (apiKey === undefined || apiKey === "") {
console.error("[hub] OPENROUTER_API_KEY not set; nothing to run. Exiting.");
process.exit(1);
}
const provider = new OpenRouterProvider({ apiKey });
const models = new InMemoryModelRegistry(
[
{ id: "z-ai/glm-4.6", label: "GLM-4.6", toolCapable: true },
{ id: "anthropic/claude-3.5-sonnet", label: "Claude 3.5 Sonnet", toolCapable: true },
],
{ draft: "z-ai/glm-4.6", review: "anthropic/claude-3.5-sonnet" },
);
const tools = new ToolRegistry();
tools.register(
feishuContextTool(async (args) => {
// Skeleton: no live Feishu calls. Real impl wraps @larksuiteoapi/node-sdk.
return JSON.stringify({ stub: true, anchor: args.anchor, id: args.id });
}),
);
const model = models.resolve(undefined, "draft");
runAgent(provider, tools, {
prompt: "ping",
model,
role: "draft",
project: {
projectId: "proj-skeleton",
boundChatId: "chat-skeleton",
workspaceDir: "/tmp/cph-skeleton",
},
})
.then((r) => {
console.log("[hub] run finished", { status: r.status, model, usage: r.usage });
if (r.error !== undefined) console.error("[hub] error:", r.error);
})
.catch((e) => {
console.error("[hub] run threw:", e);
process.exit(1);
});
}
main();