docs(plan): v0.2 adds Pi subagents and scheduled-jobs board
Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
1 parent
b839ed4056
commit
16ab2a6c84
97 files changed
+592
-648
No files matched your search
@@ -109,8 +109,8 @@ 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.readOnlyReconcile) 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 };
|
||||
|
||||
|
||||
@@ -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} 个作品。演示模型不会提交下载;需要真实执行请在设置里配置 Pi 上游。`;
|
||||
return `[fake] 演示模式:库存查询返回 ${n} 个作品。演示模型不提交下载;要真实执行,在设置里配置 Agent 上游。`;
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -126,11 +126,10 @@ async function relatedTask(ctx: ServerContext, d: DecisionRow) {
|
||||
}
|
||||
|
||||
function suggestionFor(d: DecisionRow, reason: string): string {
|
||||
if (d.kind === "identity_ambiguous")
|
||||
return `${reason}。建议在待处理中心人工选择身份,或发布识别策略授权 identity.resolve。`;
|
||||
if (d.kind === "identity_ambiguous") return `${reason}。建议在待处理中人工选择身份,或为 identity.resolve 发放授权。`;
|
||||
return (
|
||||
`${reason}。建议:在待处理中心选择"强制重新校验"(download.recheck),或为自动恢复发放持续授权` +
|
||||
`(action=download.recheck / task.retry,可绑定已发布的 automation 策略);换源、删除仍需人工决定。`
|
||||
`${reason}。建议在待处理中选择"强制重新校验"(download.recheck),或为 download.recheck / task.retry 发放持续授权` +
|
||||
`(可绑定已发布的 automation 策略);换源、删除仍需人工决定。`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -234,9 +233,9 @@ export async function autoInvestigate(
|
||||
dt ? `下载任务 ${dt.id}「${dt.name ?? ""}」当前 ${dt.status}(qB 状态 ${dt.raw_state ?? "未知"})。` : "",
|
||||
taskId ? `关联任务 ${taskId}。` : "",
|
||||
mode === "recover"
|
||||
? "在已授权范围内做有界恢复:先用 task.inspect 等只读能力查证,再按需调用 download.recheck(强制重新校验 / 恢复暂停)、" +
|
||||
? "在授权范围内做有界恢复:先用 task.inspect 等只读能力查证,再按需调用 download.recheck(强制重新校验 / 恢复暂停)、" +
|
||||
"task.retry(重试失败任务)或 identity.resolve,每种最多一次;不换源、不删除、不清理;恢复结果由系统核实。"
|
||||
: "只做有界的只读调查:查证据、给出结论与处理建议;不要提交下载或变更文件。",
|
||||
: "只做只读调查:查证据,给出结论与处理建议;不提交下载,不改文件。",
|
||||
].join("");
|
||||
const { run, created } = await startRun(ctx, {
|
||||
goal,
|
||||
@@ -382,14 +381,14 @@ export async function applyRecoveryOutcome(
|
||||
outcome === "recovered"
|
||||
? `已恢复(系统核实):执行 ${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,13 +31,13 @@ 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" : "站点"}并发已满,稍后重试`);
|
||||
}
|
||||
}
|
||||
|
||||
export class UpstreamTimeoutError extends Error {
|
||||
constructor(readonly upstream: Upstream) {
|
||||
super(`调用${upstream === "tmdb" ? " TMDB" : "站点"}超时(${EXTERNAL_INVOKE_TIMEOUT_MS / 1000}s),已放弃等待`);
|
||||
super(`调用${upstream === "tmdb" ? " TMDB" : "站点"}超时(${EXTERNAL_INVOKE_TIMEOUT_MS / 1000}s)`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -102,10 +102,10 @@ 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" ? "已提交≠已入库:仍在等待下载完成与核验入库。" : "",
|
||||
state === "waiting" ? "已提交≠已入库:等待下载完成并核验入库。" : "",
|
||||
done.length ? `整理记录:${done.map((r) => `${r.id}(${r.status})`).join("、")}。` : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
|
||||
@@ -227,7 +227,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({
|
||||
@@ -302,7 +302,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
|
||||
@@ -361,16 +361,15 @@ export function registerAgentRoutes(
|
||||
.select(["id", "media_entity_id"])
|
||||
.where("id", "=", body.sourceReleaseId)
|
||||
.executeTakeFirst();
|
||||
if (!rel) throw new ApiException("not_found", "来源资源不存在");
|
||||
if (!rel) throw new ApiException("not_found", "资源不存在");
|
||||
let slotIds: string[];
|
||||
if (body.slotId) {
|
||||
const s = await db().selectFrom("acquisition_slot").select("id").where("id", "=", body.slotId).executeTakeFirst();
|
||||
if (!s) throw new ApiException("not_found", "收录目标(Slot)不存在");
|
||||
if (!s) throw new ApiException("not_found", "版本目标不存在");
|
||||
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")
|
||||
@@ -382,7 +381,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: "该作品没有版本目标(Slot),请先在订阅里建目标" });
|
||||
rejected.push({ slotId: null, code: "no_slot", message: "该作品没有版本目标,先在订阅里新建" });
|
||||
const invokeCtx = {
|
||||
actorId,
|
||||
taskId: `http:${c.get("requestId") ?? "req"}`,
|
||||
|
||||
@@ -123,7 +123,7 @@ async function agentSettings(
|
||||
const pi = s.pi;
|
||||
let apiKey = "fake-key-not-used";
|
||||
if (!fake) {
|
||||
if (!pi.apiKeySecretRef) throw new Error("未配置 Pi 上游密钥(设置 → pi.apiKey)");
|
||||
if (!pi.apiKeySecretRef) throw new Error("未配置 Agent 上游密钥(设置 → pi.apiKey)");
|
||||
apiKey = await env.ctx.secrets.resolve(pi.apiKeySecretRef);
|
||||
if (!pi.model) throw new Error("未配置模型(设置 → pi.model)");
|
||||
}
|
||||
@@ -250,17 +250,16 @@ export async function reconcilePending(
|
||||
function systemPromptFor(run: AgentRunRow, scope: RunScope): string {
|
||||
const constraints: string[] = [
|
||||
`可用能力:${scope.capabilities.join(", ")}`,
|
||||
"有副作用的能力(提交下载、执行整理、发布策略、重试/取消任务)需要服务端授权;被拒绝时如实说明缺少哪项授权。",
|
||||
"有副作用的能力(提交下载、执行整理、发布策略、重试/取消任务)需要服务端授权;被拒时说明缺少哪项授权。",
|
||||
];
|
||||
if (run.mode === "plan_only")
|
||||
constraints.push("本次为“先给方案”:只调查与规划,不提交下载、不做文件变更、不发布策略。");
|
||||
if (run.mode === "plan_only") constraints.push("本次只给方案:只调查与规划,不提交下载、不改文件、不发布策略。");
|
||||
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("本任务由后台事件触发,只做只读调查:收集证据,给出结论与建议,不要反复重试同一操作。");
|
||||
return buildSystemPrompt({
|
||||
role: run.trigger === "event" ? "媒体库异常调查员" : "媒体库自动化助手",
|
||||
goal: run.goal,
|
||||
|
||||
@@ -333,7 +333,7 @@ export async function autoInvestigate(
|
||||
`调查待处理事项 ${d.id}(${d.kind}):${d.title}。`,
|
||||
d.summary ? `摘要:${d.summary}。` : "",
|
||||
d.subject_type && d.subject_id ? `对象:${d.subject_type} ${d.subject_id}。` : "",
|
||||
"只做有界的只读调查:查证据、给出结论与处理建议;不要提交下载或变更文件。",
|
||||
"只做只读调查:查证据,给出结论与处理建议;不提交下载,不改文件。",
|
||||
].join("");
|
||||
const { run, created } = await startRun(ctx, {
|
||||
goal,
|
||||
|
||||
Reference in new issue
Block a user