style(copy): fix remaining punctuation violations and make check:copy strict

Auto-fix of 602 string literals across 74 files (half-width punctuation next to
Chinese, parenthesised Chinese, quotes); biome reflow; e2/e6 assertions follow
the new full-width summaries. check:copy now runs with --strict in ci.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-28 21:34:51 +08:00
1 parent 0fccfe257d
commit fb84a71f80
76 files changed
+582 -570

No files matched your search

+4 -4
View File
@@ -111,7 +111,7 @@ export function createAuthorizer(deps: {
// 自动化总开关 / 只读对账:自动来源(agent / 后台任务)一律不执行写操作
if (ctx.source !== "web" && d.kind === "execute") {
const a = await deps.settings.get("automation");
if (!a.enabled) return { allowed: false, reason: "自动化总开关已关闭,不自动执行" };
if (!a.enabled) return { allowed: false, reason: "自动化总开关已关闭,不自动执行" };
if (a.readOnlyReconcile) return { allowed: false, reason: "只读对账模式下不自动执行" };
}
if (ctx.source === "web" && /^(user|admin):/.test(ctx.actorId)) return { allowed: true };
@@ -154,9 +154,9 @@ export function createAuthorizer(deps: {
if (a.status !== "approved") return { allowed: false, reason: `审批 ${approvalId} 状态为 ${a.status}` };
if (a.plan_type !== ap.planType || a.plan_id !== planId)
return { allowed: false, reason: "审批绑定的计划与本次调用不一致" };
if (a.payload_hash !== target.payloadHash) return { allowed: false, reason: "计划内容已变化,旧审批失效" };
if (a.payload_hash !== target.payloadHash) return { allowed: false, reason: "计划内容已变化,旧审批失效" };
if (a.expected_revision !== target.revision)
return { allowed: false, reason: "计划 revision 已变化,旧审批失效" };
return { allowed: false, reason: "计划 revision 已变化,旧审批失效" };
if (a.expires_at && a.expires_at.getTime() <= now().getTime()) return { allowed: false, reason: "审批已过期" };
return { allowed: true, approvalId: a.id };
}
@@ -227,7 +227,7 @@ export function createAuthorizer(deps: {
}
return {
allowed: false,
reason: reasons.length > 0 ? `没有可用的授权:${reasons.join(";")}` : `缺少 ${d.id} 的持续授权或有效的单次审批`,
reason: reasons.length > 0 ? `没有可用的授权:${reasons.join(";")}` : `缺少 ${d.id} 的持续授权或有效的单次审批`,
};
};
}
@@ -39,7 +39,7 @@ async function checkGrantScope(ctx: ServerContext, grantId: string, planId: stri
.where("p.id", "=", planId)
.executeTakeFirst()
: undefined;
if (!e) throw new CapabilityError("unauthorized", `授权 ${grantId} 限定了对象范围,但下载任务没有关联作品`);
if (!e) throw new CapabilityError("unauthorized", `授权 ${grantId} 限定了对象范围,但下载任务没有关联作品`);
if (scope.mediaEntityIds?.length && !scope.mediaEntityIds.includes(e.id))
throw new CapabilityError("unauthorized", `授权 ${grantId} 的对象范围不含目标作品`);
if (scope.mediaKinds?.length) {
@@ -87,7 +87,7 @@ export function registerDownloadRecoveryCapabilities(
if (input.action === "recheck") await client.recheck([dt.torrent_hash]);
else await client.resume([dt.torrent_hash]);
} catch (err) {
throw new CapabilityError("internal", `下载器请求失败:${(err as Error).message}`);
throw new CapabilityError("internal", `下载器请求失败:${(err as Error).message}`);
}
return {
downloadTaskId: dt.id,
@@ -58,7 +58,7 @@ export async function taskTimeline(ctx: ServerContext, t: TaskRow) {
out.push({
at: t.finished_at.toISOString(),
type: `task.${t.status}`,
summary: t.status === "failed" ? `失败:${JSON.stringify(t.last_error ?? {}).slice(0, 200)}` : t.status,
summary: t.status === "failed" ? `失败:${JSON.stringify(t.last_error ?? {}).slice(0, 200)}` : t.status,
});
if (t.subject_type === "acquisition_plan" && t.subject_id) {
const p = await db
@@ -82,7 +82,7 @@ export async function taskTimeline(ctx: ServerContext, t: TaskRow) {
out.push({
at: dt.updated_at.toISOString(),
type: "download.status",
summary: `下载任务 ${dt.status}(进度 ${Math.round(Number(dt.progress ?? 0) * 100)}%)`,
summary: `下载任务 ${dt.status}(进度 ${Math.round(Number(dt.progress ?? 0) * 100)}%)`,
data: { downloadTaskId: dt.id, status: dt.status },
});
const recs = dts.length
@@ -125,7 +125,7 @@ export async function retryTask(ctx: ServerContext, taskId: string, by: string,
const t = await ctx.db.selectFrom("task").selectAll().where("id", "=", taskId).executeTakeFirst();
if (!t) throw new CapabilityError("not_found", "任务不存在");
if (!["failed", "dead", "cancelled"].includes(t.status))
throw new CapabilityError("precondition", `只能重试失败/死信/已取消的任务(当前 ${t.status})`);
throw new CapabilityError("precondition", `只能重试失败/死信/已取消的任务(当前 ${t.status})`);
const input =
t.kind === "agentRun" ? { agentRunId: t.subject_id } : { ...((t.input ?? {}) as Record<string, unknown>), taskId };
const { runId } = await ctx.engine.enqueue(engineTaskName(t.kind), input, {
@@ -79,7 +79,7 @@ export const DEMO_SCRIPTS: FakeScript[] = [
finalText: (results) => {
const r = results.find((x) => x.capabilityId === "inventory.query");
const n = (r?.json as { items?: unknown[] } | undefined)?.items?.length ?? 0;
return `[fake] 演示模式:库存查询返回 ${n} 个作品。演示模型不提交下载;要真实执行,在设置里配置 Agent 上游。`;
return `[fake] 演示模式:库存查询返回 ${n} 个作品。演示模型不提交下载;要真实执行,在设置里配置 Agent 上游。`;
},
},
];
+13 -13
View File
@@ -289,7 +289,7 @@ async function planSummary(db: Kysely<DB>, planType: string, planId: string): Pr
}
if (planType === "organize_plan") {
const r = await db.selectFrom("artifact").select("title").where("id", "=", planId).executeTakeFirst();
if (r) return safe(`整理:${r.title}`, 300);
if (r) return safe(`整理:${r.title}`, 300);
}
if (planType === "policy_revision") {
const r = await db.selectFrom("policy_revision").select("revision").where("id", "=", planId).executeTakeFirst();
@@ -353,8 +353,8 @@ export async function inlineApproval(
return { allowed: false, reason: "审批绑定的计划与本次调用不一致" };
}
if (existing.payload_hash !== b.payloadHash || existing.expected_revision !== b.revision) {
await resolved("invalid", "计划在审批期间变化,旧审批失效");
return { allowed: false, reason: "计划在审批期间变化,旧审批失效;重新生成计划后再提交" };
await resolved("invalid", "计划在审批期间变化,旧审批失效");
return { allowed: false, reason: "计划在审批期间变化,旧审批失效;重新生成计划后再提交" };
}
if (existing.expires_at && existing.expires_at.getTime() <= now.getTime()) {
await resolved("expired", null);
@@ -383,7 +383,7 @@ export async function inlineApproval(
if (existing.status === "expired" || (existing.expires_at && existing.expires_at.getTime() <= now.getTime())) {
await expireApproval(sctx, existing.id);
await resolved("expired", null);
return { allowed: false, reason: "审批已过期,没有执行" };
return { allowed: false, reason: "审批已过期,没有执行" };
}
// 仍在等待:重新挂起(崩溃重放 / 兜底轮询提前醒来)
if (live?.suspend && !inv.startsWith("reconcile:"))
@@ -405,7 +405,7 @@ export async function inlineApproval(
const summary = await planSummary(db, plan.planType, plan.planId);
const decision = await insertDecision(sctx, {
kind: "approval_required",
title: `Agent 请求审批:${CAPABILITY_CATALOG[d.id as KnownCapabilityId]?.title ?? d.id}`,
title: `Agent 请求审批:${CAPABILITY_CATALOG[d.id as KnownCapabilityId]?.title ?? d.id}`,
summary,
runId,
dedupeKey: `${APPROVAL_KEY}:${runId}:${inv}`,
@@ -450,7 +450,7 @@ export async function inlineApproval(
deadline: expiresAt.toISOString(),
approvalId: a.id,
});
return { allowed: false, reason: "已在步骤处发起审批,等待管理员批准" };
return { allowed: false, reason: "已在步骤处发起审批,等待管理员批准" };
}
async function expireApproval(ctx: ServerContext, approvalId: string) {
@@ -524,7 +524,7 @@ export async function proposeFirstBlock(db: Kysely<DB>, runId: string): Promise<
.where("dedupe_key", "like", `${OPTIONS_KEY}:%`)
.where("status", "=", "resolved")
.executeTakeFirst();
return chosen ? null : "本会话要求先看方案再执行:先用 agent.propose_options 给出方案,用户选定后才能执行";
return chosen ? null : "本会话要求先看方案再执行:先用 agent.propose_options 给出方案,用户选定后才能执行";
}
// ── 挂起唤醒条件(runAgent 的 waitForFact) ──
@@ -569,7 +569,7 @@ export function registerInteractionCapabilities(registry: CapabilityRegistry, ct
registry.register(capabilitySpec("agent.plan"), async (input, c) => {
const run = await runOf(ctx, c);
const live = liveRun(run.id);
if (!live) throw new CapabilityError("precondition", "会话不在运行中,不能更新步骤清单");
if (!live) throw new CapabilityError("precondition", "会话不在运行中,不能更新步骤清单");
const cur = await currentPlan(ctx, live, run.id);
const byId = new Map(input.replace ? [] : cur.steps.map((s) => [s.id, s] as const));
const order = input.replace ? [] : cur.steps.map((s) => s.id);
@@ -583,7 +583,7 @@ export function registerInteractionCapabilities(registry: CapabilityRegistry, ct
});
if (!order.includes(s.id)) order.push(s.id);
}
if (order.length > 30) throw new CapabilityError("invalid_input", "步骤最多 30 个;合并细碎步骤后再更新");
if (order.length > 30) throw new CapabilityError("invalid_input", "步骤最多 30 个;合并细碎步骤后再更新");
const next = { steps: order.map((id) => byId.get(id)!), revision: cur.revision + 1 };
live.plan = next;
live.handle.appendEvent({ type: "plan_updated", steps: next.steps, revision: next.revision });
@@ -594,7 +594,7 @@ export function registerInteractionCapabilities(registry: CapabilityRegistry, ct
registry.register({ ...capabilitySpec("agent.propose_options"), replay: "safe" }, async (input, c) => {
const run = await runOf(ctx, c);
if (!canAskUser(run))
throw new CapabilityError("precondition", "子会话和后台会话不能让用户选方案:自己按证据决定,在汇报里说明取舍");
throw new CapabilityError("precondition", "子会话和后台会话不能让用户选方案:自己按证据决定,在汇报里说明取舍");
const live = liveRun(run.id);
const inv = c.invocationId;
if (!live?.suspend || !inv) throw new CapabilityError("precondition", "会话不在运行中");
@@ -625,7 +625,7 @@ export function registerInteractionCapabilities(registry: CapabilityRegistry, ct
const stepId = await currentStepId(ctx, live, run.id);
d = await insertDecision(ctx, {
kind: "other",
title: `Agent 等你选方案:${clip(input.question, 80)}`,
title: `Agent 等你选方案:${clip(input.question, 80)}`,
summary: input.options.map((o) => `${o.id}. ${o.title}`).join(";"),
runId: run.id,
dedupeKey,
@@ -660,8 +660,8 @@ export function registerInteractionCapabilities(registry: CapabilityRegistry, ct
const pick = ev.recommendedId ?? ev.options[0]?.id ?? null;
const res: OptionsResolution =
ev.timeoutAction === "recommended" && pick
? { optionKey: pick, outcome: "chosen", reason: "超时未选,按设置自动选推荐项", timeout: true }
: { optionKey: null, outcome: "stop", reason: "超时未选,按设置停止", timeout: true };
? { optionKey: pick, outcome: "chosen", reason: "超时未选,按设置自动选推荐项", timeout: true }
: { optionKey: null, outcome: "stop", reason: "超时未选,按设置停止", timeout: true };
await closeDecision(ctx, d.id, res.outcome === "chosen" ? "resolved" : "expired", { ...res }, "system:timeout");
result = optionsResult(ev, res.outcome === "chosen" ? "resolved" : "expired", res, "timeout");
}
+16 -16
View File
@@ -127,9 +127,9 @@ async function relatedTask(ctx: ServerContext, d: DecisionRow) {
function suggestionFor(d: DecisionRow, reason: string): string {
if (d.kind === "identity_ambiguous")
return `${reason}。可在待处理中手动选择身份,或为 identity.resolve 发放持续授权。`;
return `${reason}。可在待处理中手动选择身份,或为 identity.resolve 发放持续授权。`;
return (
`${reason}。可在待处理中选择"强制重新校验"(download.recheck),或为 download.recheck / task.retry 发放持续授权;` +
`${reason}。可在待处理中选择"强制重新校验"(download.recheck),或为 download.recheck / task.retry 发放持续授权;` +
`换源、删除需人工决定。`
);
}
@@ -144,7 +144,7 @@ async function addRecheckOption(ctx: ServerContext, d: DecisionRow) {
{
key: "recheck",
label: "强制重新校验",
description: "让下载器重新校验已有文件(不换源、不删除)",
description: "让下载器重新校验已有文件(不换源、不删除)",
capabilityId: "download.recheck",
input: { downloadTaskId: d.subject_id, action: "recheck" },
},
@@ -184,7 +184,7 @@ export async function autoInvestigate(
and (evidence->'autoRecover'->>'recoverAttempted')::boolean is true`.execute(ctx.db);
attempt = (prior.rows[0]?.n ?? 0) + 1;
if (attempt > s.investigateMaxAttempts) {
const reason = `同一根因已自动恢复 ${attempt - 1} 次(上限 ${s.investigateMaxAttempts}),不再自动处理`;
const reason = `同一根因已自动恢复 ${attempt - 1} 次(上限 ${s.investigateMaxAttempts}),不再自动处理`;
await mergeEvidence(ctx, d.id, {
mode: "investigate",
requestedMode: requested,
@@ -229,14 +229,14 @@ export async function autoInvestigate(
const taskId = await relatedTask(ctx, d);
const goal = [
`调查待处理事项 ${d.id}(${d.kind}):${d.title}。`,
d.summary ? `摘要:${d.summary}。` : "",
d.subject_type && d.subject_id ? `对象:${d.subject_type} ${d.subject_id}。` : "",
dt ? `下载任务 ${dt.id}「${dt.name ?? ""}」当前 ${dt.status}(qB 状态 ${dt.raw_state ?? "未知"})。` : "",
d.summary ? `摘要:${d.summary}。` : "",
d.subject_type && d.subject_id ? `对象:${d.subject_type} ${d.subject_id}。` : "",
dt ? `下载任务 ${dt.id}「${dt.name ?? ""}」当前 ${dt.status}(qB 状态 ${dt.raw_state ?? "未知"})。` : "",
taskId ? `关联任务 ${taskId}。` : "",
mode === "recover"
? "在授权范围内做有界恢复:先用 task.inspect 等只读能力查证,再按需调用 download.recheck(强制重新校验 / 恢复暂停)、" +
"task.retry(重试失败任务)或 identity.resolve,每种最多一次;不换源、不删除、不清理;恢复结果由系统核实。"
: "只做只读调查:查证据,给出结论与处理建议;不提交下载,不改文件。",
? "在授权范围内做有界恢复:先用 task.inspect 等只读能力查证,再按需调用 download.recheck(强制重新校验 / 恢复暂停)、" +
"task.retry(重试失败任务)或 identity.resolve,每种最多一次;不换源、不删除、不清理;恢复结果由系统核实。"
: "只做只读调查:查证据,给出结论与处理建议;不提交下载,不改文件。",
].join("");
const { run, created } = await startRun(ctx, {
goal,
@@ -380,16 +380,16 @@ export async function applyRecoveryOutcome(
const what = done.map((a) => a.capabilityId).join(" + ");
const summary =
outcome === "recovered"
? `已恢复(系统核实):执行 ${what} 后业务状态已恢复。`
? `已恢复(系统核实):执行 ${what} 后业务状态已恢复。`
: outcome === "failed"
? `未恢复:执行 ${what} 后业务状态仍为失败,已退回待处理。`
? `未恢复:执行 ${what} 后业务状态仍为失败,已退回待处理。`
: outcome === "unverified"
? `未恢复:执行 ${what} 后在核实时限内业务状态未恢复,已退回待处理。`
? `未恢复:执行 ${what} 后在核实时限内业务状态未恢复,已退回待处理。`
: outcome === "budget_exceeded"
? `未恢复:调查超出预算(${res.message ?? ""}),未执行恢复动作,已退回待处理。`
? `未恢复:调查超出预算(${res.message ?? ""}),未执行恢复动作,已退回待处理。`
: outcome === "cancelled"
? "未恢复:运行已取消。"
: `未恢复:没有执行恢复动作${acts.denied.length ? `(被拒绝:${acts.denied.map((x) => x.capabilityId).join(", ")})` : ""},已退回待处理。`;
? "未恢复:运行已取消。"
: `未恢复:没有执行恢复动作${acts.denied.length ? `(被拒绝:${acts.denied.map((x) => x.capabilityId).join(", ")})` : ""},已退回待处理。`;
const evidence: Partial<AutoRecoverEvidence> = {
runId,
outcome,
@@ -31,7 +31,7 @@ export function externalUpstream(capabilityId: string, input: unknown): Upstream
export class UpstreamBusyError extends Error {
constructor(readonly upstream: Upstream) {
super(`${upstream === "tmdb" ? "TMDB" : "站点"}并发已满,稍后重试`);
super(`${upstream === "tmdb" ? "TMDB" : "站点"}并发已满,稍后重试`);
}
}
+4 -4
View File
@@ -102,11 +102,11 @@ export async function buildGoalReport(
const done = records.filter((r) => r.status === "succeeded" || r.status === "partial");
const summary =
state === "no_actions"
? "本次没有提交下载或整理,结论见模型汇报。"
? "本次没有提交下载或整理,结论见模型汇报。"
: [
`已提交下载 ${submitted} 个,已入库 ${imported} 个,进行中 ${inProgress} 个,失败 ${failed} 个。`,
state === "waiting" ? "已提交≠已入库:等待下载完成并核验入库。" : "",
done.length ? `整理记录:${done.map((r) => `${r.id}(${r.status})`).join("、")}。` : "",
`已提交下载 ${submitted} 个,已入库 ${imported} 个,进行中 ${inProgress} 个,失败 ${failed} 个。`,
state === "waiting" ? "已提交≠已入库:等待下载完成并核验入库。" : "",
done.length ? `整理记录:${done.map((r) => `${r.id}(${r.status})`).join("、")}。` : "",
]
.filter(Boolean)
.join("");
+7 -6
View File
@@ -224,7 +224,7 @@ export function registerAgentRoutes(
const run = await db().selectFrom("agent_run").selectAll().where("id", "=", params.id).executeTakeFirst();
if (!run) throw new ApiException("not_found", "Agent 运行不存在");
if (body.optionId === null && !body.reason?.trim())
throw new ApiException("validation_failed", "都不要时写一句原因,Agent 按它调整方案");
throw new ApiException("validation_failed", "都不要时写一句原因,Agent 按它调整方案");
const r = await answerOptions(ctx, run.id, body, actorId);
if (r === "not_found") throw new ApiException("not_found", "这个会话没有这项方案选择");
if (r === "bad_option") throw new ApiException("validation_failed", "没有这个方案");
@@ -287,7 +287,7 @@ export function registerAgentRoutes(
const cur = await db().selectFrom("automation_grant").selectAll().where("id", "=", params.id).executeTakeFirst();
if (!cur) throw new ApiException("not_found", "授权不存在");
if (body.expiresAt && cur.expires_at && new Date(body.expiresAt) > cur.expires_at)
throw new ApiException("validation_failed", "只能缩短有效期;扩大范围需新建授权");
throw new ApiException("validation_failed", "只能缩短有效期;扩大范围需新建授权");
const r = await db()
.updateTable("automation_grant")
.set({
@@ -362,7 +362,7 @@ export function registerAgentRoutes(
.executeTakeFirst();
if (!cur) throw new ApiException("not_found", "审批不存在");
if (cur.payload_hash !== body.payloadHash)
throw new ApiException("conflict", "计划已变化(payloadHash 不一致),重新申请审批");
throw new ApiException("conflict", "计划已变化(payloadHash 不一致),重新申请审批");
const allowed = body.status === "revoked" ? ["pending", "approved"] : ["pending"];
if (!allowed.includes(cur.status)) throw new ApiException("conflict", `审批已是 ${cur.status}`);
return trx
@@ -388,7 +388,7 @@ export function registerAgentRoutes(
}));
route(app, Api.capabilities.invoke, async ({ params, body, actor, actorId, c }) => {
const entry = ctx.capabilities.get(params.capabilityId);
if (!entry) throw new ApiException("not_found", `能力未注册:${params.capabilityId}`);
if (!entry) throw new ApiException("not_found", `能力未注册:${params.capabilityId}`);
const scopes = (c.get("principal")?.scopes ?? actor?.scopes ?? []) as string[];
if (!hasScope(scopes, entry.descriptor.scope))
throw new ApiException("forbidden", `调用 ${entry.descriptor.id} 需要 scope: ${entry.descriptor.scope}`);
@@ -431,7 +431,8 @@ export function registerAgentRoutes(
slotIds = [s.id];
} else {
const entityId = body.entityId ?? rel.media_entity_id;
if (!entityId) throw new ApiException("precondition_failed", "资源还没有关联本地作品,先在“身份与元数据”指定作品");
if (!entityId)
throw new ApiException("precondition_failed", "资源还没有关联本地作品,先在「身份与元数据」指定作品");
slotIds = (
await db()
.selectFrom("acquisition_slot")
@@ -443,7 +444,7 @@ export function registerAgentRoutes(
}
const rejected: { slotId: string | null; code: string; message: string }[] = [];
if (slotIds.length === 0)
rejected.push({ slotId: null, code: "no_slot", message: "这个作品没有版本目标,先在订阅里新建" });
rejected.push({ slotId: null, code: "no_slot", message: "这个作品没有版本目标,先在订阅里新建" });
const invokeCtx = {
actorId,
taskId: `http:${c.get("requestId") ?? "req"}`,
+13 -12
View File
@@ -86,7 +86,7 @@ export function scopeFor(mode: "execute" | "plan_only", allowlist?: readonly str
const d = CAPABILITY_CATALOG[id as KnownCapabilityId];
return !d || !PLAN_ONLY_KINDS.includes(d.kind);
});
if (bad.length) throw new Error(`plan_only 模式只允许 read/preview/propose 类能力,不允许:${bad.join(", ")}`);
if (bad.length) throw new Error(`plan_only 模式只允许 read/preview/propose 类能力,不允许:${bad.join(", ")}`);
return {
capabilities: allowlist?.length
? [...allowlist]
@@ -252,23 +252,24 @@ function systemPromptFor(
sub: { canSpawn: boolean; maxConcurrent: number; maxDepth: number },
): string {
const constraints: string[] = [
`可用能力:${scope.capabilities.join(", ")}`,
"有副作用的能力(提交下载、执行整理、发布策略、重试/取消任务)需要服务端授权;被拒时说明缺少哪项授权。",
`可用能力:${scope.capabilities.join(", ")}`,
"有副作用的能力(提交下载、执行整理、发布策略、重试/取消任务)需要服务端授权;被拒时说明缺少哪项授权。",
];
if (run.mode === "plan_only") constraints.push("本次只读:只调查与规划,不提交下载、不改文件、不发布策略。");
if (run.mode === "plan_only") constraints.push("本次只读:只调查与规划,不提交下载、不改文件、不发布策略。");
if (sub.canSpawn)
constraints.push(
`可以用 agent.spawn 把彼此独立的有界子目标交给子会话(同时最多 ${sub.maxConcurrent} 个,派生层数上限 ${sub.maxDepth});` +
"子会话的能力、模式与预算都不会超过本会话,且会占用本会话预算;派生后用 agent.await 等结果再汇总,不要为小事派生。",
`可以用 agent.spawn 把彼此独立的有界子目标交给子会话(同时最多 ${sub.maxConcurrent} 个,派生层数上限 ${sub.maxDepth});` +
"子会话的能力、模式与预算都不会超过本会话,且会占用本会话预算;派生后用 agent.await 等结果再汇总,不要为小事派生。",
);
if (run.parent_run_id) constraints.push("你是子会话:只完成本目标,完成后简要汇报结论与证据(对象 ID),由父会话汇总。");
if (run.parent_run_id)
constraints.push("你是子会话:只完成本目标,完成后简要汇报结论与证据(对象 ID),由父会话汇总。");
if (run.trigger === "event" && run.mode === "execute")
constraints.push(
"本任务由后台事件触发,只做授权内的有界恢复:先查证据,再在授权范围内执行恢复(task.retry / download.recheck / identity.resolve)," +
"每种动作最多一次;不换源、不删除、不清理;是否恢复由系统按业务状态核实,不要自行宣称已恢复。",
"本任务由后台事件触发,只做授权内的有界恢复:先查证据,再在授权范围内执行恢复(task.retry / download.recheck / identity.resolve)," +
"每种动作最多一次;不换源、不删除、不清理;是否恢复由系统按业务状态核实,不要自行宣称已恢复。",
);
else if (run.trigger === "event")
constraints.push("本任务由后台事件触发,只做只读调查:收集证据,给出结论与建议,不要反复重试同一操作。");
constraints.push("本任务由后台事件触发,只做只读调查:收集证据,给出结论与建议,不要反复重试同一操作。");
const opts = sessionOptions(run);
return buildSystemPrompt({
role: run.trigger === "event" ? "媒体库异常调查员" : "媒体库自动化 Agent",
@@ -340,7 +341,7 @@ export async function drive(
let settings: AgentSettings;
try {
const resolved = await resolveRunUpstream(ctx, run.upstream_id);
if (!resolved) throw new Error("未配置 Agent 上游(设置 → Agent → 上游)");
if (!resolved) throw new Error("未配置 Agent 上游(设置 → Agent → 上游)");
upstream = resolved;
fake = upstream.row.provider === "fake";
settings = agentSettings(s, (run.budget ?? {}) as AgentBudget, fake, upstream);
@@ -373,7 +374,7 @@ export async function drive(
status: "failed",
reason: "error",
budget: null,
message: `fakeScript 不合法:${(err as Error).message}`,
message: `fakeScript 不合法:${(err as Error).message}`,
finalText: "",
};
}
+9 -9
View File
@@ -142,7 +142,7 @@ async function enqueueRun(ctx: ServerContext, run: AgentRunRow, key: string) {
} catch (err) {
await ctx.db
.updateTable("agent_run")
.set({ status: "failed", error: JSON.stringify({ message: `入队失败:${(err as Error).message}` }) })
.set({ status: "failed", error: JSON.stringify({ message: `入队失败:${(err as Error).message}` }) })
.where("id", "=", run.id)
.execute();
throw err;
@@ -237,13 +237,13 @@ export async function finalize(
let summary: string;
if (res.status === "cancelled" || run.cancel_requested_at) {
status = "cancelled";
summary = `已停止(后续步骤不再执行,已完成的动作不撤销)。${report.state === "no_actions" ? "" : report.summary}`;
summary = `已停止(后续步骤不再执行,已完成的动作不撤销)。${report.state === "no_actions" ? "" : report.summary}`;
} else if (res.reason === "budget") {
status = "budget_exceeded";
summary = `超出预算:${res.message ?? ""}。${report.state === "no_actions" ? "" : report.summary}`;
summary = `超出预算:${res.message ?? ""}。${report.state === "no_actions" ? "" : report.summary}`;
} else if (res.status === "failed") {
status = "failed";
summary = `运行失败:${res.message ?? ""}`;
summary = `运行失败:${res.message ?? ""}`;
} else if (report.state === "waiting") {
status = "waiting";
summary = report.summary;
@@ -252,7 +252,7 @@ export async function finalize(
summary = report.summary;
}
// 模型文本只作参考:附在业务核查结论之后,明确标注未经核查
if (res.finalText && res.status !== "failed") summary += `\n模型汇报(未经核查):${res.finalText}`;
if (res.finalText && res.status !== "failed") summary += `\n 模型汇报(未经核查):${res.finalText}`;
// 子代理:父会话总预算含子会话用量
const kids = await descendants(ctx, runId);
if (kids.length) {
@@ -264,7 +264,7 @@ export async function finalize(
.executeTakeFirst();
const ownTokens = ((fresh?.usage ?? own) as { totalTokens?: number }).totalTokens ?? 0;
const ext = await descendantUsage(ctx, runId);
summary += `\n子会话 ${kids.length} 个;会话树合计工具调用 ${(fresh?.tool_call_count ?? run.tool_call_count) + ext.toolCalls} 次、token ${ownTokens + ext.totalTokens}。`;
summary += `\n 子会话 ${kids.length} 个;会话树合计工具调用 ${(fresh?.tool_call_count ?? run.tool_call_count) + ext.toolCalls} 次、token ${ownTokens + ext.totalTokens}。`;
}
// 系统核实结论(授权内恢复)与终态同一次写入:观察者看到终态时汇总已是最终文本
if (opts.summaryPrefix) summary = `${opts.summaryPrefix}\n${summary}`;
@@ -391,9 +391,9 @@ export async function autoInvestigate(
if (!s.autoInvestigate) return { runId: null, created: false };
const goal = [
`调查待处理事项 ${d.id}(${d.kind}):${d.title}。`,
d.summary ? `摘要:${d.summary}。` : "",
d.subject_type && d.subject_id ? `对象:${d.subject_type} ${d.subject_id}。` : "",
"只做只读调查:查证据,给出结论与处理建议;不提交下载,不改文件。",
d.summary ? `摘要:${d.summary}。` : "",
d.subject_type && d.subject_id ? `对象:${d.subject_type} ${d.subject_id}。` : "",
"只做只读调查:查证据,给出结论与处理建议;不提交下载,不改文件。",
].join("");
const { run, created } = await startRun(ctx, {
goal,
+14 -14
View File
@@ -75,7 +75,7 @@ const safe = (s: string | null | undefined, n = 1500) => clip(redactString(s ??
*/
export class AwaitSuspendedError extends CapabilityError {
constructor() {
super("precondition", "会话已挂起等待子会话结束,恢复后重放本调用");
super("precondition", "会话已挂起等待子会话结束,恢复后重放本调用");
}
}
@@ -321,7 +321,7 @@ export async function cascadeCancel(ctx: ServerContext, runId: string, reason: s
.updateTable("agent_run")
.set({
cancel_requested_at: now,
error: JSON.stringify({ abortReason: `父会话已取消:${reason}`, by: `agent:${runId}` }),
error: JSON.stringify({ abortReason: `父会话已取消:${reason}`, by: `agent:${runId}` }),
})
.where("id", "in", ids)
.where("cancel_requested_at", "is", null)
@@ -330,7 +330,7 @@ export async function cascadeCancel(ctx: ServerContext, runId: string, reason: s
if (taskIds.length)
await ctx.db
.updateTable("task")
.set({ cancel_requested_at: now, cancel_reason: `父会话已取消:${reason}` })
.set({ cancel_requested_at: now, cancel_reason: `父会话已取消:${reason}` })
.where("id", "in", taskIds)
.where("cancel_requested_at", "is", null)
.execute();
@@ -489,7 +489,7 @@ export async function allocateBudget(
if (want !== undefined) out[k] = want;
return;
}
if (left < min) throw new CapabilityError("budget", `本会话剩余${label}不足,不能再派生子会话(剩余 ${left})`);
if (left < min) throw new CapabilityError("budget", `本会话剩余${label}不足,不能再派生子会话(剩余 ${left})`);
if (want !== undefined && want > left)
throw new CapabilityError("budget", `子会话${label} ${want} 超过本会话剩余预算 ${left}`);
const v = want ?? (k === "maxCostUsd" ? left / 2 : Math.max(min, Math.floor(left / 2)));
@@ -498,7 +498,7 @@ export async function allocateBudget(
pick("maxToolCalls", remain.maxToolCalls, "工具调用次数", 1);
pick("maxTokens", remain.maxTokens, " token", 1);
pick("maxCostUsd", remain.maxCostUsd, "费用", 0.0001);
pick("maxDurationSec", remain.maxDurationSec, "时长(秒)", 5);
pick("maxDurationSec", remain.maxDurationSec, "时长(秒)", 5);
return out;
}
@@ -528,10 +528,10 @@ export function spawnScope(
const kindOf = (id: string) => CAPABILITY_CATALOG[id as KnownCapabilityId]?.kind;
if (requested?.length) {
const outside = requested.filter((id) => !parentCaps.includes(id));
if (outside.length) throw new CapabilityError("unauthorized", `子会话能力超出本会话白名单:${outside.join(", ")}`);
if (outside.length) throw new CapabilityError("unauthorized", `子会话能力超出本会话白名单:${outside.join(", ")}`);
const tooStrong = requested.filter((id) => !kinds.includes(kindOf(id) as CapabilityKind));
if (tooStrong.length)
throw new CapabilityError("unauthorized", `子会话模式/授权上限不允许这些能力:${tooStrong.join(", ")}`);
throw new CapabilityError("unauthorized", `子会话模式/授权上限不允许这些能力:${tooStrong.join(", ")}`);
}
const caps = (requested?.length ? [...new Set(requested)] : parentCaps).filter((id) =>
kinds.includes(kindOf(id) as CapabilityKind),
@@ -587,7 +587,7 @@ export async function awaitChildren(
const rows = await q.orderBy("created_at").execute();
if (ids?.length && rows.length !== new Set(ids).size) {
const missing = ids.filter((id) => !rows.some((r) => r.id === id));
throw new CapabilityError("not_found", `不是本会话派生的子会话:${missing.join(", ")}`);
throw new CapabilityError("not_found", `不是本会话派生的子会话:${missing.join(", ")}`);
}
await liveRun(parentId)?.pump();
const allDone = rows.every((r) => SETTLED.includes(r.status));
@@ -644,7 +644,7 @@ export async function wakeParent(ctx: ServerContext, childRunId: string): Promis
await ctx.engine.signal(w.engineRunId, w.fact, { childRunId });
return true;
} catch (err) {
ctx.logger.warn({ childRunId, err: { message: (err as Error).message } }, "唤醒父会话失败(父会话会兜底轮询)");
ctx.logger.warn({ childRunId, err: { message: (err as Error).message } }, "唤醒父会话失败(父会话会兜底轮询)");
return false;
}
}
@@ -662,7 +662,7 @@ export async function wakeRun(
await ctx.engine.signal(w.engineRunId, w.fact, payload);
return true;
} catch (err) {
ctx.logger.warn({ runId, err: { message: (err as Error).message } }, "唤醒会话失败(会话会兜底轮询)");
ctx.logger.warn({ runId, err: { message: (err as Error).message } }, "唤醒会话失败(会话会兜底轮询)");
return false;
}
}
@@ -719,9 +719,9 @@ export function registerSubagentCapabilities(registry: CapabilityRegistry, ctx:
const parent = await parentOf(ctx, c);
const s = await ctx.settings.all();
const cfg = subagentSettings(s);
if (!cfg.enabled) throw new CapabilityError("precondition", "子会话未开启(设置 → Agent → 允许子会话)");
if (!cfg.enabled) throw new CapabilityError("precondition", "子会话未开启(设置 → Agent → 允许子会话)");
if (parent.cancel_requested_at || TERMINAL.includes(parent.status))
throw new CapabilityError("precondition", "本会话已取消或已结束,不能派生子会话");
throw new CapabilityError("precondition", "本会话已取消或已结束,不能派生子会话");
const dedupeKey = `subagent:${c.idempotencyKey ?? `${parent.id}:${Date.now()}`}`;
const existing = await ctx.db
.selectFrom("agent_run")
@@ -731,7 +731,7 @@ export function registerSubagentCapabilities(registry: CapabilityRegistry, ctx:
let child = existing;
if (!child) {
if (parent.depth + 1 > cfg.maxDepth)
throw new CapabilityError("precondition", `派生层数已达上限 ${cfg.maxDepth},子会话不能再派生`);
throw new CapabilityError("precondition", `派生层数已达上限 ${cfg.maxDepth},子会话不能再派生`);
const mode: AgentRunMode = input.mode ?? parent.mode;
if (parent.mode === "plan_only" && mode === "execute")
throw new CapabilityError("unauthorized", "只给方案的会话只能派生只给方案的子会话");
@@ -812,7 +812,7 @@ export async function exceedsAncestors(
for (const a of chain) {
const sc = (a.scope ?? {}) as Partial<RunScope>;
if (sc.capabilities?.length && !sc.capabilities.includes(capabilityId))
return `超出父会话 ${a.id} 的能力白名单:${capabilityId}`;
return `超出父会话 ${a.id} 的能力白名单:${capabilityId}`;
if (sc.allowedKinds && !sc.allowedKinds.includes(kind)) return `父会话 ${a.id} 不允许 ${kind} 类能力`;
}
return null;