From 23bd8cfe45947ff6ed7c9f7e182b2828532040c6 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 30 Sep 2026 00:00:03 +0800 Subject: [PATCH] =?UTF-8?q?fix(web):=20=E5=B7=A1=E6=A3=80=E7=9B=AE?= =?UTF-8?q?=E6=A0=87=E6=A8=A1=E6=9D=BF=E7=9A=84=E5=8D=A0=E4=BD=8D=E7=AC=A6?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E6=88=90=E5=90=8D=E7=A7=B0(FIX-7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 执行时 renderGoal 已正确替换(补单测);界面显示的是模板本身,{{now}} 等改成「当前时间」标签。 Co-Authored-By: Claude Opus 5.5 --- .../server/test/schedules/render-goal.test.ts | 26 ++++++++++++++++++ .../components/schedules/agent-check-card.tsx | 27 ++++++++++++++++--- .../schedules/agent-check-dialog.tsx | 6 +++-- .../app/components/schedules/labels.test.ts | 19 ++++++++++++- apps/web/app/components/schedules/labels.ts | 25 +++++++++++++++++ 5 files changed, 97 insertions(+), 6 deletions(-) create mode 100644 apps/server/test/schedules/render-goal.test.ts diff --git a/apps/server/test/schedules/render-goal.test.ts b/apps/server/test/schedules/render-goal.test.ts new file mode 100644 index 00000000..8d17ba22 --- /dev/null +++ b/apps/server/test/schedules/render-goal.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from "vitest"; +import { BUILTIN_AGENT_CHECKS, renderGoal } from "../../src/modules/schedules/agent-checks.ts"; + +// FIX-7:界面上看到的 {{now}} / {{openDecisions}} 是模板本身;执行时必须全部替换成实际值。 + +describe("renderGoal(巡检目标模板)", () => { + const now = new Date("2026-09-28T18:53:50.468Z"); + const facts = { openDecisions: 3, stalledDownloads: 1, activeSubscriptions: 5 }; + + it("内置巡检的模板执行时不留占位符", () => { + for (const c of BUILTIN_AGENT_CHECKS) { + const goal = renderGoal(c.config.goalTemplate, now, facts); + expect(goal).not.toMatch(/\{\{/); + } + const pending = BUILTIN_AGENT_CHECKS.find((c) => c.key === "check.pending"); + expect(renderGoal(pending?.config.goalTemplate ?? "", now, facts)).toContain( + "巡检待处理(2026-09-28T18:53:50.468Z):有 3 条未关闭的待处理事项", + ); + }); + + it("容许空格;不认识的占位符原样保留", () => { + expect(renderGoal("{{ date }} 待处理 {{openDecisions}} 条 {{other}}", now, facts)).toBe( + "2026-09-28 待处理 3 条 {{other}}", + ); + }); +}); diff --git a/apps/web/app/components/schedules/agent-check-card.tsx b/apps/web/app/components/schedules/agent-check-card.tsx index f605743c..26d9c9f8 100644 --- a/apps/web/app/components/schedules/agent-check-card.tsx +++ b/apps/web/app/components/schedules/agent-check-card.tsx @@ -15,7 +15,7 @@ import { agentRunLabel, formatRelative } from "~/lib/labels.ts"; import { cn } from "~/lib/utils.ts"; import { NextRun, useJobActions } from "./auto-task-list.tsx"; import { IntervalSelect } from "./interval-select.tsx"; -import { CHECK_TRIGGER_LABEL, formatUsd, RUN_STATUS } from "./labels.ts"; +import { CHECK_TRIGGER_LABEL, formatUsd, goalTemplateParts, RUN_STATUS } from "./labels.ts"; // 计划页「智能体巡检」:到点按目标模板发起一次智能体会话(kind=agent_check),一个巡检一张卡片。 // 卡片默认只显示名称、开关、间隔、上次结果与费用;目标、能力、预算、模型、最近会话收进「详情」。 @@ -43,6 +43,27 @@ export type DefaultUpstream = { const modelsTitle = (models: { provider: string | null; model: string; count: number }[]) => models.map((m) => `${m.provider ? `${m.provider} · ` : ""}${m.model}:${m.count} 次`).join("\n"); +/** 目标模板:占位符显示成名称标签(执行时替换成实际值),不露 `{{now}}` */ +function GoalTemplate({ template }: { template: string }) { + return ( + <> + {goalTemplateParts(template).map((p, i) => + "text" in p ? ( + {p.text} + ) : ( + + {p.label} + + ), + )} + + ); +} + function Field({ label, children }: { label: string; children: ReactNode }) { return ( <> @@ -151,8 +172,8 @@ export function AgentCheckCard({
- - {c.goalTemplate ?? "—"} + + {c.goalTemplate ? : "—"} diff --git a/apps/web/app/components/schedules/agent-check-dialog.tsx b/apps/web/app/components/schedules/agent-check-dialog.tsx index 0563b8eb..cd80cdb5 100644 --- a/apps/web/app/components/schedules/agent-check-dialog.tsx +++ b/apps/web/app/components/schedules/agent-check-dialog.tsx @@ -25,13 +25,15 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~ import { Switch } from "~/components/ui/switch.tsx"; import { Textarea } from "~/components/ui/textarea.tsx"; import { errorMessage } from "~/lib/api.ts"; -import { CHECK_TRIGGER_LABEL, splitInterval, UNIT_MS } from "./labels.ts"; +import { CHECK_TRIGGER_LABEL, GOAL_PLACEHOLDER_LABEL, splitInterval, UNIT_MS } from "./labels.ts"; /** 巡检只开放只读 / 预览 / 提案类能力 */ const READONLY_CAPS = Object.values(CAPABILITY_CATALOG).filter((c) => (["read", "preview", "propose"] as string[]).includes(c.kind), ); -const PLACEHOLDERS = "{{now}} {{date}} {{openDecisions}} {{stalledDownloads}} {{activeSubscriptions}}"; +const PLACEHOLDERS = Object.entries(GOAL_PLACEHOLDER_LABEL) + .map(([k, v]) => `{{${k}}} ${v}`) + .join("、"); type Unit = "m" | "h"; type Draft = { diff --git a/apps/web/app/components/schedules/labels.test.ts b/apps/web/app/components/schedules/labels.test.ts index e4f24173..16745b2a 100644 --- a/apps/web/app/components/schedules/labels.test.ts +++ b/apps/web/app/components/schedules/labels.test.ts @@ -1,7 +1,24 @@ import { describe, expect, it } from "vitest"; -import { describeSchedule, formatUsd, INTERVAL_PRESETS, planPath, scheduleStreamPath } from "./labels.ts"; +import { + describeSchedule, + formatUsd, + goalTemplateParts, + INTERVAL_PRESETS, + planPath, + scheduleStreamPath, +} from "./labels.ts"; describe("计划页标签", () => { + it("巡检目标模板:占位符显示成名称(FIX-7)", () => { + expect(goalTemplateParts("巡检待处理({{now}}):有 {{ openDecisions }} 条,{{unknown}}")).toEqual([ + { text: "巡检待处理(" }, + { placeholder: "now", label: "当前时间" }, + { text: "):有 " }, + { placeholder: "openDecisions", label: "待处理事项数" }, + { text: " 条,{{unknown}}" }, + ]); + }); + it("planPath:节锚点与 ?job= 高亮", () => { expect(planPath()).toBe("/console/schedules"); expect(planPath({ section: "auto" })).toBe("/console/schedules#auto"); diff --git a/apps/web/app/components/schedules/labels.ts b/apps/web/app/components/schedules/labels.ts index 1b675515..4b552589 100644 --- a/apps/web/app/components/schedules/labels.ts +++ b/apps/web/app/components/schedules/labels.ts @@ -129,3 +129,28 @@ export function formatClock(iso: string | null | undefined): string { second: "2-digit", }); } + +/** 巡检目标模板的占位符(服务端 renderGoal 执行时替换);界面上显示成名称,不露 `{{now}}` */ +export const GOAL_PLACEHOLDER_LABEL: Record = { + now: "当前时间", + date: "今天日期", + openDecisions: "待处理事项数", + stalledDownloads: "异常下载数", + activeSubscriptions: "追踪中的订阅数", +}; + +/** 目标模板拆成文字与占位符两段(纯函数,单测覆盖);不认识的占位符原样当文字 */ +export function goalTemplateParts(template: string): ({ text: string } | { placeholder: string; label: string })[] { + const out: ({ text: string } | { placeholder: string; label: string })[] = []; + let last = 0; + for (const m of template.matchAll(/\{\{\s*(\w+)\s*\}\}/g)) { + const key = m[1] ?? ""; + const label = GOAL_PLACEHOLDER_LABEL[key]; + if (!label) continue; + if (m.index > last) out.push({ text: template.slice(last, m.index) }); + out.push({ placeholder: key, label }); + last = m.index + m[0].length; + } + if (last < template.length) out.push({ text: template.slice(last) }); + return out; +}