feat(agent): 用对话起草巡检(schedule.draft_check + 起草巡检 Skill + 计划页入口与对话卡片)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-10-03 00:04:31 +08:00
1 parent 8eb8a68295
commit 7c4ad901d9
14 files changed
+408 -7

No files matched your search

@@ -217,6 +217,14 @@ export function createAuthorizer(deps: {
}
// metadata.enrich 只存补全建议(不写作品表);次数由每日预算约束,应用建议另走控制台会话
if (d.id === "metadata.enrich") return { allowed: true };
// schedule.draft_check 只建未启用的巡检(不会自己运行,用户检查后启用);只放行用户发起的对话,
// 定时巡检 / 事件触发的会话不能再造巡检
if (d.id === "schedule.draft_check") {
if (!runId || (ctx.source !== "agent" && ctx.source !== "task"))
return { allowed: false, reason: "只有智能体对话可以起草巡检" };
const r = await db.selectFrom("agent_run").select("trigger").where("id", "=", runId).executeTakeFirst();
return r?.trigger === "user" ? { allowed: true } : { allowed: false, reason: "只在用户发起的对话里起草巡检" };
}
// 先看方案再执行:用户选定方案前,Agent 的执行类调用一律拒绝
if (runId && d.kind === "execute" && (ctx.source === "agent" || ctx.source === "task")) {
const block = await proposeFirstBlock(db, runId);
@@ -0,0 +1,56 @@
import { CapabilityError, type CapabilityRegistry } from "@umt/agent";
import { CAPABILITY_CATALOG, capabilitySpec, KnownCapabilityId } from "@umt/contracts";
import type { ServerContext } from "../types.ts";
import { createAgentCheck } from "./store.ts";
// schedule.draft_check:智能体按用户的自然语言需求起草巡检。建成「未启用」,不会自己运行;
// 用户在计划页检查、修改后再启用(授权只放行用户发起的对话,见 agent/authorize.ts)。
/** 巡检只开放只读 / 预览 / 提案类能力(与计划页表单一致),且不能再起草巡检 */
const ALLOWED = new Set(
Object.values(CAPABILITY_CATALOG)
.filter((c) => ["read", "preview", "propose"].includes(c.kind) && c.id !== "schedule.draft_check")
.map((c) => c.id as string),
);
export function registerScheduleCapabilities(registry: CapabilityRegistry, ctx: ServerContext): void {
registry.register(capabilitySpec("schedule.draft_check"), async (input) => {
const asked = [...new Set(input.capabilities ?? [])];
const caps = asked.filter((c) => KnownCapabilityId.safeParse(c).success && ALLOWED.has(c));
const dropped = asked.filter((c) => !caps.includes(c));
let row: Awaited<ReturnType<typeof createAgentCheck>>;
try {
row = await createAgentCheck(
ctx.db,
{
name: input.name,
description: "智能体按对话起草",
enabled: false,
schedule: { type: "interval", everyMs: Math.round(input.everyHours * 3600_000) },
config: {
goalTemplate: input.goal,
capabilityAllowlist: caps as KnownCapabilityId[],
budget: {
...(input.maxToolCalls ? { maxToolCalls: input.maxToolCalls } : {}),
...(input.maxDurationSec ? { maxDurationSec: input.maxDurationSec } : {}),
},
triggerCondition: input.triggerCondition ?? "always",
reportAsDecision: input.reportAsDecision ?? false,
},
},
new Date(),
);
} catch (e) {
throw new CapabilityError("precondition", `巡检没建成:${(e as Error).message}`);
}
return {
jobId: row.id,
name: row.name,
everyHours: input.everyHours,
capabilities: caps,
dropped,
enabled: false as const,
next: "已建成未启用的巡检。请用户到「计划」页检查目标、间隔和能力,确认后打开开关启用。",
};
});
}
@@ -1,5 +1,6 @@
import type { ServerContext, ServerModule } from "../types.ts";
import { builtinAgentCheckDefs } from "./agent-checks.ts";
import { registerScheduleCapabilities } from "./capabilities.ts";
import { registerScheduledJob, scheduleRegistry } from "./registry.ts";
import { registerScheduleRoutes } from "./routes.ts";
import { Scheduler, type SchedulerOptions } from "./scheduler.ts";
@@ -30,6 +31,9 @@ export function createSchedulesModule(opts: SchedulesModuleOptions = {}): Server
registerScheduleRoutes(app, ctx, () => scheduleRegistry(ctx));
},
registerTasks() {},
registerCapabilities(registry, ctx) {
registerScheduleCapabilities(registry, ctx);
},
start(ctx: ServerContext) {
if (opts.builtinChecks !== false) for (const def of builtinAgentCheckDefs(ctx)) registerScheduledJob(ctx, def);
// 备份目前由宿主机 scripts/backup.sh(cron / 手动)执行,这里只占位,看板上显示"未配置"