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:
rootandClaude Opus 5.5 committed 2026-09-28 13:29:42 +08:00
1 parent c67f6a9682
commit 31a54186c7
58 files changed
+200 -188

No files matched your search

+14 -12
View File
@@ -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([
+3 -3
View File
@@ -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;
+1 -1
View File
@@ -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) {
+3 -3
View File
@@ -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) };
}
+1 -1
View File
@@ -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;
}
}