forked from EduCraft/curriculum-project-hub
ce767e9cab
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 通过。
94 lines
3.8 KiB
TypeScript
94 lines
3.8 KiB
TypeScript
/**
|
|
* Hub entry — Fastify server + Feishu WebSocket listener.
|
|
*
|
|
* Wires the full trigger path: lark ws receives `im.message.receive_v1` →
|
|
* trigger handler resolves chat→project (ADR-0001), creates AgentRun + acquires
|
|
* the project lock (ADR-0002), runs the provider-agnostic agent loop
|
|
* (ADR-0017), and releases the lock on finish. Feishu context reads inside the
|
|
* loop are ADR-0003-authorized (chat must match binding).
|
|
*
|
|
* Env (see .env.example):
|
|
* DATABASE_URL, OPENROUTER_API_KEY, FEISHU_APP_ID/SECRET/BOT_OPEN_ID, PORT
|
|
*/
|
|
import "dotenv/config";
|
|
import Fastify from "fastify";
|
|
import { prisma } from "./db.js";
|
|
import { OpenRouterProvider } from "./agent/openrouter-provider.js";
|
|
import { InMemoryModelRegistry } from "./agent/models.js";
|
|
import { ToolRegistry, feishuContextTool } from "./agent/tools.js";
|
|
import { readFileTool, writeFileTool, listFilesTool } from "./agent/workspace.js";
|
|
import { cphCheckTool, cphBuildTool } from "./agent/cph.js";
|
|
import { createLarkClient, startFeishuListenerWithClient, type FeishuRuntime } from "./feishu/client.js";
|
|
import { readFeishuContext } from "./feishu/read.js";
|
|
import { makeTriggerHandler } from "./feishu/trigger.js";
|
|
|
|
function requireEnv(name: string): string {
|
|
const v = process.env[name];
|
|
if (v === undefined || v === "") {
|
|
console.error(`[hub] missing required env: ${name}`);
|
|
process.exit(1);
|
|
}
|
|
return v;
|
|
}
|
|
|
|
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");
|
|
const feishuAppId = requireEnv("FEISHU_APP_ID");
|
|
const feishuAppSecret = requireEnv("FEISHU_APP_SECRET");
|
|
const feishuBotOpenId = requireEnv("FEISHU_BOT_OPEN_ID");
|
|
const port = Number(process.env["PORT"] ?? "8788");
|
|
|
|
const app = Fastify({ logger: true });
|
|
|
|
app.get("/api/healthz", async () => ({ ok: true, ts: Date.now() }));
|
|
|
|
// --- Agent seam wiring ---
|
|
const provider = new OpenRouterProvider({ apiKey: openrouterKey });
|
|
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 },
|
|
],
|
|
[
|
|
{ id: "draft", label: "草稿", defaultModel: "z-ai/glm-4.6" },
|
|
{ id: "review", label: "审校", defaultModel: "anthropic/claude-3.5-sonnet" },
|
|
],
|
|
);
|
|
|
|
// Build the lark client first — tools that call Feishu APIs hold it from boot.
|
|
const feishuConfig = { appId: feishuAppId, appSecret: feishuAppSecret, botOpenId: feishuBotOpenId };
|
|
const larkClient = createLarkClient(feishuConfig);
|
|
const feishuRt: FeishuRuntime = { client: larkClient, logger: app.log };
|
|
|
|
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));
|
|
// Workspace file tools (ADR-0007 directory tree, path-confined).
|
|
tools.register(readFileTool());
|
|
tools.register(writeFileTool());
|
|
tools.register(listFilesTool());
|
|
// cph CLI tools (ADR-0016 version contract enforced by cph itself).
|
|
tools.register(cphCheckTool());
|
|
tools.register(cphBuildTool());
|
|
|
|
// --- Feishu listener (reuses the same lark client) ---
|
|
const trigger = makeTriggerHandler({ prisma, provider, tools, models, logger: app.log });
|
|
startFeishuListenerWithClient(feishuConfig, larkClient, app.log, trigger);
|
|
|
|
app.listen({ port, host: "0.0.0.0" }, (err, address) => {
|
|
if (err !== null) {
|
|
app.log.error({ err }, "listen failed");
|
|
process.exit(1);
|
|
}
|
|
app.log.info({ address }, "hub listening");
|
|
});
|
|
}
|
|
|
|
main().catch((e) => {
|
|
console.error("[hub] fatal:", e);
|
|
process.exit(1);
|
|
});
|