copy: second-round copy review across console, settings, agent and server messages
- Trim descriptions, empty states, confirm buttons and toasts to facts - Unify terms per glossary: 待处理/待识别/整理记录/运行记录/凭证/子会话/Agent 上游 - Subscription-login termsNote states facts only (borrowed client identity, ToS/ban risk) - Shorter, concrete goal templates for built-in Agent checks - Human-readable nouns instead of table names in instance CRUD errors - Audit labels for pi_upstream / schedule / automation_grant actions - Glossary: add 上游、订阅登录、凭证、定时任务、巡检、运行记录、子会话、停止、持续授权 Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
c67f6a9682
commit
31a54186c7
58 files changed
+200
-188
No files matched your search
@@ -209,10 +209,7 @@ export function createAuthorizer(deps: {
|
||||
}
|
||||
return {
|
||||
allowed: false,
|
||||
reason:
|
||||
reasons.length > 0
|
||||
? `没有可用的授权:${reasons.join(";")}`
|
||||
: `缺少 ${d.id} 的持续授权(automation_grant)或有效的单次审批(approval)`,
|
||||
reason: reasons.length > 0 ? `没有可用的授权:${reasons.join(";")}` : `缺少 ${d.id} 的持续授权或有效的单次审批`,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -126,10 +126,11 @@ 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),或为 download.recheck / task.retry 发放持续授权` +
|
||||
`(可绑定已发布的 automation 策略);换源、删除仍需人工决定。`
|
||||
`${reason}。可在待处理中选择"强制重新校验"(download.recheck),或为 download.recheck / task.retry 发放持续授权;` +
|
||||
`换源、删除需人工决定。`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -421,7 +421,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"}`,
|
||||
|
||||
@@ -129,9 +129,9 @@ async function agentSettings(
|
||||
const pi = s.pi;
|
||||
let apiKey: string | undefined;
|
||||
if (!fake && !upstream) {
|
||||
if (!pi.apiKeySecretRef) throw new Error("未配置 Pi 上游(设置 → 模型上游)");
|
||||
if (!pi.apiKeySecretRef) throw new Error("未配置 Agent 上游(设置 → Agent → 上游)");
|
||||
apiKey = await env.ctx.secrets.resolve(pi.apiKeySecretRef);
|
||||
if (!pi.model) throw new Error("未配置模型(设置 → pi.model)");
|
||||
if (!pi.model) throw new Error("未配置模型(设置 → Agent → 上游)");
|
||||
}
|
||||
const maxTokens = budget.maxTokens ?? s.agent.maxTokensPerRun ?? undefined;
|
||||
const maxCost = budget.maxCostUsd ?? s.agent.maxCostUsdPerRun ?? undefined;
|
||||
@@ -278,7 +278,7 @@ function systemPromptFor(
|
||||
else if (run.trigger === "event")
|
||||
constraints.push("本任务由后台事件触发,只做只读调查:收集证据,给出结论与建议,不要反复重试同一操作。");
|
||||
return buildSystemPrompt({
|
||||
role: run.trigger === "event" ? "媒体库异常调查员" : "媒体库自动化助手",
|
||||
role: run.trigger === "event" ? "媒体库异常调查员" : "媒体库自动化 Agent",
|
||||
goal: run.goal,
|
||||
constraints,
|
||||
});
|
||||
|
||||
@@ -222,7 +222,7 @@ 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}`;
|
||||
|
||||
@@ -583,7 +583,7 @@ 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", "本会话已取消或已结束,不能派生子会话");
|
||||
const dedupeKey = `subagent:${c.idempotencyKey ?? `${parent.id}:${Date.now()}`}`;
|
||||
|
||||
@@ -64,6 +64,12 @@ const test = (path: string, summary: string, tag: string) => ({
|
||||
});
|
||||
|
||||
type Table = "library_root" | "downloader_instance" | "media_server_instance" | "path_mapping";
|
||||
const NOUN: Record<Table, string> = {
|
||||
library_root: "库根",
|
||||
downloader_instance: "下载器实例",
|
||||
media_server_instance: "媒体服务器实例",
|
||||
path_mapping: "路径映射",
|
||||
};
|
||||
const JSON_COLS = ["settings", "version_info", "health_detail", "verify_detail"] as const;
|
||||
|
||||
const errMessage = (e: unknown) => redactString(String((e as Error)?.message ?? e)).slice(0, 500);
|
||||
@@ -111,7 +117,7 @@ export function registerInstanceRoutes(app: ModuleApp, ctx: ServerContext) {
|
||||
const getRow = async (table: Table, id: string) => {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: 同上
|
||||
const r = await (db() as any).selectFrom(table).selectAll().where("id", "=", id).executeTakeFirst();
|
||||
if (!r) throw new ApiException("not_found", `${table} ${id} 不存在`);
|
||||
if (!r) throw new ApiException("not_found", `${NOUN[table]}不存在`);
|
||||
return r as Record<string, unknown> & { id: string; secret_ref?: string | null };
|
||||
};
|
||||
const get = (table: Table, tag: string, def: Def) =>
|
||||
@@ -119,7 +125,7 @@ export function registerInstanceRoutes(app: ModuleApp, ctx: ServerContext) {
|
||||
toApi(await getRow(table, (params as { id: string }).id)),
|
||||
);
|
||||
const remove = (table: Table, tag: string, path: string, action: AuditAction) =>
|
||||
endpoint(app, del(path, `删除 ${table}`, tag), async ({ params, actor }) => {
|
||||
endpoint(app, del(path, `删除${NOUN[table]}`, tag), async ({ params, actor }) => {
|
||||
const id = (params as { id: string }).id;
|
||||
const row = await getRow(table, id);
|
||||
// biome-ignore lint/suspicious/noExplicitAny: 同上
|
||||
@@ -127,7 +133,7 @@ export function registerInstanceRoutes(app: ModuleApp, ctx: ServerContext) {
|
||||
.deleteFrom(table)
|
||||
.where("id", "=", id)
|
||||
.execute()
|
||||
.catch((e: unknown) => pgConflict(e, `${table} 仍被引用,不能删除`));
|
||||
.catch((e: unknown) => pgConflict(e, `${NOUN[table]}仍被引用,不能删除`));
|
||||
if (row.secret_ref) await ctx.secrets.remove(row.secret_ref).catch(() => {});
|
||||
await audit(ctx, actor, action, { type: table, id });
|
||||
return { ok: true as const };
|
||||
@@ -139,7 +145,10 @@ export function registerInstanceRoutes(app: ModuleApp, ctx: ServerContext) {
|
||||
.values(values)
|
||||
.returningAll()
|
||||
.executeTakeFirstOrThrow()
|
||||
.catch((e: unknown) => pgConflict(e, `${table} 名称/路径重复或引用不存在`))) as Record<string, unknown> & {
|
||||
.catch((e: unknown) => pgConflict(e, `${NOUN[table]}的名称或路径重复,或关联的对象不存在`))) as Record<
|
||||
string,
|
||||
unknown
|
||||
> & {
|
||||
id: string;
|
||||
};
|
||||
const update = async (table: Table, id: string, values: Record<string, unknown>) => {
|
||||
@@ -151,8 +160,8 @@ export function registerInstanceRoutes(app: ModuleApp, ctx: ServerContext) {
|
||||
.where("id", "=", id)
|
||||
.returningAll()
|
||||
.executeTakeFirst()
|
||||
.catch((e: unknown) => pgConflict(e, `${table} 名称/路径重复或引用不存在`));
|
||||
if (!r) throw new ApiException("not_found", `${table} ${id} 不存在`);
|
||||
.catch((e: unknown) => pgConflict(e, `${NOUN[table]}的名称或路径重复,或关联的对象不存在`));
|
||||
if (!r) throw new ApiException("not_found", `${NOUN[table]}不存在`);
|
||||
return r as Record<string, unknown> & { id: string };
|
||||
};
|
||||
|
||||
|
||||
@@ -60,12 +60,12 @@ export function readableLoginError(provider: string, err: unknown): string {
|
||||
const raw = err instanceof Error ? err.message : String(err);
|
||||
if (/EADDRINUSE|address already in use/i.test(raw)) {
|
||||
const port = OAUTH_CALLBACK_PORTS[provider] ?? raw.match(/:(\d{2,5})\b/)?.[1];
|
||||
return `服务端回调端口${port ? ` ${port}` : ""} 被占用(可能另有登录进行中,或容器内有 Claude Code / Codex CLI 占用该端口),请释放端口或稍后重试`;
|
||||
return `服务端回调端口${port ? ` ${port}` : ""} 被占用。结束占用该端口的进程(如另一个登录流程、Claude Code / Codex CLI)后重试`;
|
||||
}
|
||||
if (/cancel|abort/i.test(raw)) return "登录已取消";
|
||||
if (/state mismatch/i.test(raw)) return "粘贴的地址与本次登录不匹配(state 不一致),请用本次打开的授权链接重新登录";
|
||||
if (/state mismatch/i.test(raw)) return "粘贴的地址不属于本次登录(state 不一致)。用本次打开的授权链接重新登录";
|
||||
if (/\b404\b/.test(raw) && provider === "openai-codex")
|
||||
return "该账号未开启设备码登录(Codex 返回 404),请改用浏览器登录";
|
||||
return "这个账号未开启设备码登录(Codex 返回 404)。改用浏览器登录";
|
||||
return classifyUpstreamError(err).message;
|
||||
}
|
||||
|
||||
@@ -105,10 +105,7 @@ export class OAuthSessions {
|
||||
async start(row: PiUpstreamRow, req: { mode?: PiOAuthMode; enterpriseDomain?: string }): Promise<PiOAuthStartResult> {
|
||||
const info = providerInfo(row.provider);
|
||||
if (row.auth_kind !== "oauth" || !info?.oauth)
|
||||
throw new ApiException(
|
||||
"precondition_failed",
|
||||
"该上游不是订阅登录型(authKind 需为 oauth,且 provider 支持订阅登录)",
|
||||
);
|
||||
throw new ApiException("precondition_failed", "这个上游不是订阅登录类型");
|
||||
if (!row.enabled) throw new ApiException("precondition_failed", "上游已停用");
|
||||
const mode = req.mode ?? info.oauth.modes[0]!;
|
||||
if (!info.oauth.modes.includes(mode))
|
||||
@@ -122,7 +119,7 @@ export class OAuthSessions {
|
||||
if (busy.upstreamId !== row.id)
|
||||
throw new ApiException(
|
||||
"conflict",
|
||||
`${info.name} 已有登录进行中(同一 provider 的登录需串行),请先完成或等待其过期(${new Date(busy.expiresAt).toISOString()})`,
|
||||
`${info.name} 已有登录进行中。完成它,或等到 ${new Date(busy.expiresAt).toISOString()} 过期后重试`,
|
||||
);
|
||||
this.cancel(busy, "cancelled", "已被新的登录请求取代");
|
||||
await busy.done.catch(() => undefined);
|
||||
@@ -147,7 +144,7 @@ export class OAuthSessions {
|
||||
pasteWaiters: [],
|
||||
firstEvent: Promise.withResolvers<void>(),
|
||||
done: Promise.resolve(),
|
||||
timer: setTimeout(() => this.cancel(s, "expired", "登录会话已超时,请重新开始"), ttl),
|
||||
timer: setTimeout(() => this.cancel(s, "expired", "登录会话已超时,重新开始登录"), ttl),
|
||||
};
|
||||
(s.timer as { unref?: () => void }).unref?.();
|
||||
this.sessions.set(s.id, s);
|
||||
@@ -195,7 +192,7 @@ export class OAuthSessions {
|
||||
for (const w of s.pasteWaiters.splice(0)) w();
|
||||
});
|
||||
}
|
||||
throw new Error("该登录流程需要额外输入(secret),服务端登录不支持");
|
||||
throw new Error("这个登录流程需要额外输入(secret),服务端不支持");
|
||||
}
|
||||
|
||||
private notify(s: Session, e: UpstreamAuthEvent) {
|
||||
@@ -238,9 +235,14 @@ export class OAuthSessions {
|
||||
): Promise<{ sessionId: string; state: PiOAuthState; message: string | null; progress: string[] }> {
|
||||
const s = this.sessions.get(req.sessionId);
|
||||
if (!s || s.upstreamId !== upstreamId)
|
||||
return { sessionId: req.sessionId, state: "expired", message: "登录会话不存在或已过期,请重新开始", progress: [] };
|
||||
return {
|
||||
sessionId: req.sessionId,
|
||||
state: "expired",
|
||||
message: "登录会话不存在或已过期,重新开始登录",
|
||||
progress: [],
|
||||
};
|
||||
if (req.pastedUrlOrCode && s.state === "pending") {
|
||||
if (s.mode !== "browser") throw new ApiException("bad_request", "设备码登录不需要粘贴,等待授权完成即可");
|
||||
if (s.mode !== "browser") throw new ApiException("bad_request", "设备码登录不需要粘贴,等待授权完成");
|
||||
if (!s.paste) {
|
||||
// manual_code 提示可能还没发出:短暂等待
|
||||
await Promise.race([
|
||||
|
||||
@@ -24,13 +24,13 @@ const DECISION_KEY = (id: string) => `pi_upstream_expired:${id}`;
|
||||
|
||||
/** 与其它模块的 upsertDecision 同一写法(跨模块不 import):同键未关闭时只累加 attempts */
|
||||
async function upsertExpiredDecision(ctx: ServerContext, row: PiUpstreamRow, message: string) {
|
||||
const title = `模型上游「${row.name}」登录已失效,需要重新登录`;
|
||||
const title = `Agent 上游「${row.name}」登录已失效,需要重新登录`;
|
||||
await ctx.db.transaction().execute(async (trx) => {
|
||||
const r = await sql<{ id: string; inserted: boolean }>`
|
||||
insert into decision (kind, severity, title, summary, subject_type, subject_id, dedupe_key, evidence, options)
|
||||
values ('other', 'warning', ${title}, ${message}, 'pi_upstream', ${row.id}, ${DECISION_KEY(row.id)},
|
||||
${JSON.stringify({ provider: row.provider, upstreamId: row.id, error: message })}::jsonb,
|
||||
${JSON.stringify([{ key: "relogin", label: "去设置页重新登录", description: "设置 → 模型上游 → 登录" }])}::jsonb)
|
||||
${JSON.stringify([{ key: "relogin", label: "重新登录", description: "设置 → Agent → 上游" }])}::jsonb)
|
||||
on conflict (dedupe_key) where status in ('open', 'investigating', 'awaiting_user')
|
||||
do update set attempts = decision.attempts + 1, evidence = excluded.evidence, summary = excluded.summary,
|
||||
updated_at = now()
|
||||
@@ -102,7 +102,7 @@ export async function refreshPiTokens(ctx: ServerContext, opts: RefreshOptions =
|
||||
if (r.ok) report.refreshed += 1;
|
||||
else {
|
||||
report.failed.push({ upstreamId: row.id, message: r.message });
|
||||
ctx.logger.warn({ upstreamId: row.id, provider: row.provider, message: r.message }, "Pi 上游 token 刷新失败");
|
||||
ctx.logger.warn({ upstreamId: row.id, provider: row.provider, message: r.message }, "Agent 上游 token 刷新失败");
|
||||
}
|
||||
}
|
||||
return report;
|
||||
|
||||
@@ -109,7 +109,7 @@ export function registerPiRoutes(
|
||||
source: t.source,
|
||||
model: t.model,
|
||||
errorKind: null,
|
||||
message: t.inCatalog ? null : `模型 ${t.model} 不在随包目录中,将按手填 id 调用`,
|
||||
message: t.inCatalog ? null : `模型 ${t.model} 不在内置模型目录中,按手填 id 调用`,
|
||||
};
|
||||
await setStatus(ctx, row.id, { status: "ok", verified: true });
|
||||
} catch (err) {
|
||||
|
||||
@@ -213,10 +213,10 @@ export async function resolveRunUpstream(ctx: ServerContext, upstreamId?: string
|
||||
const id = upstreamId ?? (await defaultUpstreamId(ctx));
|
||||
if (!id) return null;
|
||||
const row = await getRow(ctx, id);
|
||||
if (!row) throw new Error(upstreamId ? `指定的上游 ${upstreamId} 不存在` : "默认上游已不存在,请在设置里重新选择");
|
||||
if (!row) throw new Error(upstreamId ? `指定的上游 ${upstreamId} 不存在` : "默认上游已不存在,在设置里重新选择");
|
||||
if (!row.enabled) throw new Error(`上游「${row.name}」已停用`);
|
||||
if (row.status === "expired") throw new Error(`上游「${row.name}」登录已失效,请在设置里重新登录`);
|
||||
if (row.status === "expired") throw new Error(`上游「${row.name}」登录已失效,在设置里重新登录`);
|
||||
if (row.status === "auth_required")
|
||||
throw new Error(`上游「${row.name}」${row.auth_kind === "oauth" ? "尚未登录" : "未配置 API Key"}`);
|
||||
throw new Error(`上游「${row.name}」${row.auth_kind === "oauth" ? "未登录" : "未配置 API Key"}`);
|
||||
return { row, spec: { ...specOf(row), extra: parseExtra(row.extra) }, credentials: dbCredentialStore(ctx, row.id) };
|
||||
}
|
||||
@@ -59,7 +59,7 @@ async function readSecret(ctx: Ctx, row: Pick<PiUpstreamRow, "auth_kind" | "secr
|
||||
try {
|
||||
return decode(row.auth_kind, await ctx.secrets.resolve(row.secret_ref));
|
||||
} catch (err) {
|
||||
ctx.logger.warn({ secretRef: row.secret_ref, err: { message: (err as Error).message } }, "上游凭据读取失败");
|
||||
ctx.logger.warn({ secretRef: row.secret_ref, err: { message: (err as Error).message } }, "上游凭证读取失败");
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,8 +33,8 @@ export const BUILTIN_AGENT_CHECKS: {
|
||||
everyMs: 6 * HOUR,
|
||||
config: {
|
||||
goalTemplate:
|
||||
"巡检待处理中心({{now}}):当前有 {{openDecisions}} 条未关闭事项。逐条查看证据,按紧急程度排序," +
|
||||
"给出每条的处理建议与理由;只读,不执行任何变更。",
|
||||
"巡检待处理({{now}}):有 {{openDecisions}} 条未关闭的待处理事项。逐条查看证据,按紧急程度排序," +
|
||||
"给出处理建议和理由。只读,不做任何变更。",
|
||||
capabilityAllowlist: [...INVESTIGATE_CAPABILITIES],
|
||||
budget: { maxToolCalls: 20, maxDurationSec: 300 },
|
||||
triggerCondition: "open_decisions",
|
||||
@@ -47,7 +47,7 @@ export const BUILTIN_AGENT_CHECKS: {
|
||||
everyMs: 2 * HOUR,
|
||||
config: {
|
||||
goalTemplate:
|
||||
"巡检下载({{now}}):有 {{stalledDownloads}} 个下载无进展或失败。用只读能力查明原因(站点、做种、磁盘、任务错误)," +
|
||||
"巡检下载({{now}}):有 {{stalledDownloads}} 个下载任务无进展或失败。用只读能力查明原因(站点、做种、磁盘、任务错误)," +
|
||||
"给出诊断与建议;若本次允许恢复,只在授权内做重试或强制重新校验,每个任务最多一次,不换源、不删除。",
|
||||
capabilityAllowlist: [...INVESTIGATE_CAPABILITIES],
|
||||
budget: { maxToolCalls: 24, maxDurationSec: 420 },
|
||||
@@ -62,8 +62,8 @@ export const BUILTIN_AGENT_CHECKS: {
|
||||
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",
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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()` })
|
||||
|
||||
Reference in new issue
Block a user