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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user