style(copy): punctuation rules and site-wide fix with a CI check (QA U11)
- glossary: 10 punctuation rules (full-width in Chinese, CJK/Latin spacing, full-width parens around Chinese, single ellipsis, 「」 quotes, period rules) - scripts/check-copy-punctuation.mjs: checks string literals, templates and JSX text (not comments, types, keys, match/compare strings); --fix, --strict, --by-file; wired into deno task ci as check:copy in warn mode - --fix applied to web, server and contracts; files other lanes were editing (subscriptions, sites, browser, agent, auth/account, inventory) left for them - tests updated for the changed messages Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
9b017906cf
commit
52402864d6
143 files changed
+985
-606
No files matched your search
@@ -29,12 +29,12 @@ export const BUILTIN_AGENT_CHECKS: {
|
||||
{
|
||||
key: "check.pending",
|
||||
name: "待处理巡检",
|
||||
description: "检查待处理事项并给出建议(只读)",
|
||||
description: "检查待处理事项并给出建议(只读)",
|
||||
everyMs: 6 * HOUR,
|
||||
config: {
|
||||
goalTemplate:
|
||||
"巡检待处理({{now}}):有 {{openDecisions}} 条未关闭的待处理事项。逐条查看证据,按紧急程度排序," +
|
||||
"给出处理建议和理由。只读,不做任何变更。",
|
||||
"巡检待处理({{now}}):有 {{openDecisions}} 条未关闭的待处理事项。逐条查看证据,按紧急程度排序," +
|
||||
"给出处理建议和理由。只读,不做任何变更。",
|
||||
capabilityAllowlist: [...INVESTIGATE_CAPABILITIES],
|
||||
budget: { maxToolCalls: 20, maxDurationSec: 300 },
|
||||
triggerCondition: "open_decisions",
|
||||
@@ -43,12 +43,12 @@ export const BUILTIN_AGENT_CHECKS: {
|
||||
{
|
||||
key: "check.downloads",
|
||||
name: "下载诊断",
|
||||
description: "检查无进展或失败的下载并诊断;开启恢复后在授权内重试或重新校验",
|
||||
description: "检查无进展或失败的下载并诊断;开启恢复后在授权内重试或重新校验",
|
||||
everyMs: 2 * HOUR,
|
||||
config: {
|
||||
goalTemplate:
|
||||
"巡检下载({{now}}):有 {{stalledDownloads}} 个下载任务无进展或失败。用只读能力查明原因(站点、做种、磁盘、任务错误)," +
|
||||
"给出诊断与建议;若本次允许恢复,只在授权内做重试或强制重新校验,每个任务最多一次,不换源、不删除。",
|
||||
"巡检下载({{now}}):有 {{stalledDownloads}} 个下载任务无进展或失败。用只读能力查明原因(站点、做种、磁盘、任务错误)," +
|
||||
"给出诊断与建议;若本次允许恢复,只在授权内做重试或强制重新校验,每个任务最多一次,不换源、不删除。",
|
||||
capabilityAllowlist: [...INVESTIGATE_CAPABILITIES],
|
||||
budget: { maxToolCalls: 24, maxDurationSec: 420 },
|
||||
triggerCondition: "stalled_downloads",
|
||||
@@ -58,12 +58,12 @@ export const BUILTIN_AGENT_CHECKS: {
|
||||
{
|
||||
key: "check.weekly",
|
||||
name: "订阅与站点周报",
|
||||
description: "检查订阅缺集与站点健康,汇总写入待处理(只读)",
|
||||
description: "检查订阅缺集与站点健康,汇总写入待处理(只读)",
|
||||
everyMs: 7 * 24 * HOUR,
|
||||
config: {
|
||||
goalTemplate:
|
||||
"生成本周巡检周报({{date}}):追踪中的订阅 {{activeSubscriptions}} 个。列出缺集的订阅及可能原因、" +
|
||||
"健康异常的站点与 Feed,给出本周建议。只读。最后用不超过 300 字的中文汇总。",
|
||||
"生成本周巡检周报({{date}}):追踪中的订阅 {{activeSubscriptions}} 个。列出缺集的订阅及可能原因、" +
|
||||
"健康异常的站点与 Feed,给出本周建议。只读。最后用不超过 300 字的中文汇总。",
|
||||
capabilityAllowlist: [...INVESTIGATE_CAPABILITIES],
|
||||
budget: { maxToolCalls: 30, maxDurationSec: 600 },
|
||||
triggerCondition: "subscriptions_active",
|
||||
@@ -106,7 +106,7 @@ const isPlanOnly = (id: string) => {
|
||||
/** agent_check 的执行体(内置与自定义共用) */
|
||||
export async function runAgentCheck(ctx: ServerContext, c: JobRunContext): Promise<JobRunResult> {
|
||||
const parsed = AgentCheckConfig.safeParse(c.config);
|
||||
if (!parsed.success) throw new Error(`巡检参数无效:${parsed.error.issues[0]?.message ?? "未知"}`);
|
||||
if (!parsed.success) throw new Error(`巡检参数无效:${parsed.error.issues[0]?.message ?? "未知"}`);
|
||||
const cfg = parsed.data;
|
||||
const f = await facts(ctx);
|
||||
const unmet =
|
||||
@@ -114,7 +114,7 @@ export async function runAgentCheck(ctx: ServerContext, c: JobRunContext): Promi
|
||||
(cfg.triggerCondition === "stalled_downloads" && f.stalledDownloads === 0) ||
|
||||
(cfg.triggerCondition === "subscriptions_active" && f.activeSubscriptions === 0);
|
||||
// 手动执行不看触发条件
|
||||
if (unmet && c.trigger === "schedule") return { status: "skipped", summary: "触发条件不满足,未唤醒 Agent" };
|
||||
if (unmet && c.trigger === "schedule") return { status: "skipped", summary: "触发条件不满足,未唤醒 Agent" };
|
||||
|
||||
let mode: "execute" | "plan_only" = "plan_only";
|
||||
let allowlist = cfg.capabilityAllowlist.filter(isPlanOnly);
|
||||
@@ -124,7 +124,7 @@ export async function runAgentCheck(ctx: ServerContext, c: JobRunContext): Promi
|
||||
if (grants.length > 0) {
|
||||
mode = "execute";
|
||||
allowlist = [...new Set([...(allowlist.length ? allowlist : INVESTIGATE_CAPABILITIES), ...RECOVER_CAPABILITIES])];
|
||||
} else note = "(没有覆盖恢复动作的有效授权,改为只读)";
|
||||
} else note = "(没有覆盖恢复动作的有效授权,改为只读)";
|
||||
}
|
||||
const goal = renderGoal(cfg.goalTemplate, c.now, f);
|
||||
const dedupeKey = c.trigger === "manual" ? `schedule:${c.jobId}:manual:${c.runId}` : `schedule:${c.jobId}:${c.slot}`;
|
||||
|
||||
@@ -23,7 +23,7 @@ function parseField(src: string, [lo, hi]: [number, number], label: string): Set
|
||||
const out = new Set<number>();
|
||||
for (const part of src.split(",")) {
|
||||
const m = /^(\*|(\d+)(?:-(\d+))?)(?:\/(\d+))?$/.exec(part);
|
||||
if (!m) throw new Error(`cron ${label} 段无效:${part}`);
|
||||
if (!m) throw new Error(`cron ${label} 段无效:${part}`);
|
||||
let a = lo;
|
||||
let b = hi;
|
||||
if (m[1] !== "*") {
|
||||
@@ -31,7 +31,7 @@ function parseField(src: string, [lo, hi]: [number, number], label: string): Set
|
||||
b = m[3] !== undefined ? Number(m[3]) : m[4] !== undefined ? hi : a;
|
||||
}
|
||||
const step = m[4] !== undefined ? Number(m[4]) : 1;
|
||||
if (a < lo || b > hi || a > b || step < 1) throw new Error(`cron ${label} 段越界:${part}`);
|
||||
if (a < lo || b > hi || a > b || step < 1) throw new Error(`cron ${label} 段越界:${part}`);
|
||||
for (let v = a; v <= b; v += step) out.add(v);
|
||||
}
|
||||
return out;
|
||||
@@ -39,7 +39,7 @@ function parseField(src: string, [lo, hi]: [number, number], label: string): Set
|
||||
|
||||
export function parseCron(expr: string): CronFields {
|
||||
const parts = expr.trim().split(/\s+/);
|
||||
if (parts.length !== 5) throw new Error("cron 需要 5 段:分 时 日 月 周");
|
||||
if (parts.length !== 5) throw new Error("cron 需要 5 段:分 时 日 月 周");
|
||||
const [mi, h, d, mo, w] = parts as [string, string, string, string, string];
|
||||
const weekday = parseField(w, RANGES[4]!, "周");
|
||||
if (weekday.has(7)) weekday.add(0);
|
||||
@@ -85,5 +85,5 @@ export function nextCronAfter(expr: string | CronFields, after: Date): Date {
|
||||
}
|
||||
return t;
|
||||
}
|
||||
throw new Error(`cron 在 4 年内没有触发时间:${typeof expr === "string" ? expr : "(parsed)"}`);
|
||||
throw new Error(`cron 在 4 年内没有触发时间:${typeof expr === "string" ? expr : "(parsed)"}`);
|
||||
}
|
||||
@@ -37,7 +37,7 @@ export function createSchedulesModule(opts: SchedulesModuleOptions = {}): Server
|
||||
key: "maintain.backup",
|
||||
defaults: {
|
||||
name: "备份",
|
||||
description: "数据库与数据目录备份;目前用 umt backup(宿主机 cron 调用),未接入调度",
|
||||
description: "数据库与数据目录备份;目前用 umt backup(宿主机 cron 调用),未接入调度",
|
||||
group: "maintain",
|
||||
schedule: { type: "cron", cron: "30 3 * * *" },
|
||||
enabled: false,
|
||||
|
||||
@@ -71,7 +71,7 @@ class MemoryRegistry implements ScheduleRegistry {
|
||||
return [...this.defs.values()];
|
||||
}
|
||||
register(def: ScheduledJobDef) {
|
||||
if (!/^[a-z][a-z0-9_.:-]{1,120}$/.test(def.key)) throw new Error(`定时任务 key 不合法:${def.key}`);
|
||||
if (!/^[a-z][a-z0-9_.:-]{1,120}$/.test(def.key)) throw new Error(`定时任务 key 不合法:${def.key}`);
|
||||
this.defs.set(def.key, def);
|
||||
this.version += 1;
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ export class Scheduler {
|
||||
});
|
||||
} catch (err) {
|
||||
this.leader = false;
|
||||
this.log.warn({ err: { message: errText(err) } }, "调度器循环出错,稍后重试");
|
||||
this.log.warn({ err: { message: errText(err) } }, "调度器循环出错,稍后重试");
|
||||
if (!this.stopped) await this.sleep(retry);
|
||||
}
|
||||
}
|
||||
@@ -121,7 +121,7 @@ export class Scheduler {
|
||||
|
||||
/** 上一个执行者中断时留下的 running 状态:标为失败 */
|
||||
private async recoverInterrupted(): Promise<void> {
|
||||
const msg = "调度器重启,上次运行中断";
|
||||
const msg = "调度器重启,上次运行中断";
|
||||
await this.db
|
||||
.updateTable("scheduled_job_run")
|
||||
.set({ status: "failed", error: msg, finished_at: this.now() })
|
||||
@@ -296,7 +296,7 @@ export class Scheduler {
|
||||
where r.agent_status is null and a.status = any(${TERMINAL_AGENT})
|
||||
limit 50`.execute(this.db);
|
||||
for (const r of rows.rows) {
|
||||
const summary = (r.summary ?? "").trim() || `Agent 运行结束:${r.status}`;
|
||||
const summary = (r.summary ?? "").trim() || `Agent 运行结束:${r.status}`;
|
||||
await this.db
|
||||
.updateTable("scheduled_job_run")
|
||||
.set({ agent_status: r.status, summary: summary.slice(0, 4000) })
|
||||
@@ -305,7 +305,7 @@ export class Scheduler {
|
||||
if (r.config?.reportAsDecision === true && r.status === "succeeded") {
|
||||
const ins = await sql<{ id: string }>`
|
||||
insert into decision (kind, severity, title, summary, subject_type, subject_id, dedupe_key, evidence, agent_run_id)
|
||||
values ('other', 'info', ${`巡检报告:${r.job_name}`}, ${summary.slice(0, 4000)}, 'agent_run', ${r.agent_run_id},
|
||||
values ('other', 'info', ${`巡检报告:${r.job_name}`}, ${summary.slice(0, 4000)}, 'agent_run', ${r.agent_run_id},
|
||||
${`schedule-report:${r.id}`}, ${JSON.stringify({ scheduledJobId: r.job_id, scheduledRunId: r.id })}::jsonb,
|
||||
${r.agent_run_id})
|
||||
on conflict do nothing
|
||||
@@ -315,7 +315,7 @@ export class Scheduler {
|
||||
await this.ctx.outbox.emit(
|
||||
this.db,
|
||||
"decision.created",
|
||||
{ decisionId: d.id, kind: "other", severity: "info", title: `巡检报告:${r.job_name}` },
|
||||
{ decisionId: d.id, kind: "other", severity: "info", title: `巡检报告:${r.job_name}` },
|
||||
{ idempotencyKey: `decision.created:${d.id}` },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ export async function syncDefinitions(db: Kysely<DB>, defs: readonly ScheduledJo
|
||||
|
||||
export async function getJob(db: Kysely<DB>, id: string): Promise<ScheduledJobRow> {
|
||||
const row = await db.selectFrom("scheduled_job").selectAll().where("id", "=", id).executeTakeFirst();
|
||||
if (!row) throw new ApiException("not_found", `定时任务不存在:${id}`);
|
||||
if (!row) throw new ApiException("not_found", `定时任务不存在:${id}`);
|
||||
return row;
|
||||
}
|
||||
|
||||
@@ -201,7 +201,7 @@ export async function updateJob(
|
||||
});
|
||||
}
|
||||
if (patch.enabled !== undefined) {
|
||||
if (patch.enabled && !row.available) throw new ApiException("conflict", "任务未配置,不能启用");
|
||||
if (patch.enabled && !row.available) throw new ApiException("conflict", "任务未配置,不能启用");
|
||||
set.enabled = patch.enabled;
|
||||
set.customized = true;
|
||||
if (patch.enabled && !row.enabled && !set.next_run_at) {
|
||||
@@ -228,7 +228,7 @@ export async function updateJob(
|
||||
/** 立即执行:置 run_requested_at(已在排队或运行中则合并,不重复) */
|
||||
export async function requestRun(db: Kysely<DB>, id: string): Promise<{ row: ScheduledJobRow; queued: boolean }> {
|
||||
const row = await getJob(db, id);
|
||||
if (!row.available) throw new ApiException("conflict", "任务未配置,不能执行");
|
||||
if (!row.available) throw new ApiException("conflict", "任务未配置,不能执行");
|
||||
const upd = await db
|
||||
.updateTable("scheduled_job")
|
||||
.set({ run_requested_at: sql`now()` })
|
||||
@@ -262,7 +262,7 @@ export async function skipNext(db: Kysely<DB>, id: string, now: Date): Promise<S
|
||||
started_at: now,
|
||||
finished_at: now,
|
||||
duration_ms: 0,
|
||||
summary: `跳过 ${base.toISOString()},下次 ${next.toISOString()}`,
|
||||
summary: `跳过 ${base.toISOString()},下次 ${next.toISOString()}`,
|
||||
})
|
||||
.execute();
|
||||
await trimRuns(trx, id);
|
||||
@@ -317,6 +317,6 @@ export async function createAgentCheck(db: Kysely<DB>, body: AgentCheckCreate, n
|
||||
|
||||
export async function deleteJob(db: Kysely<DB>, id: string): Promise<void> {
|
||||
const row = await getJob(db, id);
|
||||
if (row.builtin || row.kind !== "agent_check") throw new ApiException("conflict", "内置任务不能删除,可以停用");
|
||||
if (row.builtin || row.kind !== "agent_check") throw new ApiException("conflict", "内置任务不能删除,可以停用");
|
||||
await db.deleteFrom("scheduled_job").where("id", "=", id).execute();
|
||||
}
|
||||
Reference in new issue
Block a user