From bbacfc9322b19d4e5e82f79848ab54c778408375 Mon Sep 17 00:00:00 2001 From: tianma-if Date: Sun, 11 Oct 2026 09:59:51 +0800 Subject: [PATCH] fix: reject unusable WorkBuddy ACP login credentials --- apps/desktop/src/main/acp-host.mjs | 23 +++++++++-- apps/desktop/src/main/acp-host.test.mjs | 38 ++++++++++++++++++- apps/desktop/src/main/workbuddy-auth.mjs | 13 +++++-- apps/desktop/src/main/workbuddy-auth.test.mjs | 34 ++++++++++------- .../src/components/ai-sidebar/AiSidebar.tsx | 1 + .../settings/DesktopAcpAgentCard.tsx | 1 + apps/web/src/lib/infographic-local-agent.ts | 1 + packages/shared/src/i18n/en-US.ts | 1 + packages/shared/src/i18n/ja.ts | 1 + packages/shared/src/i18n/pl.ts | 1 + packages/shared/src/i18n/zh-CN.ts | 1 + 11 files changed, 92 insertions(+), 23 deletions(-) diff --git a/apps/desktop/src/main/acp-host.mjs b/apps/desktop/src/main/acp-host.mjs index b1378bc0..585ae9be 100644 --- a/apps/desktop/src/main/acp-host.mjs +++ b/apps/desktop/src/main/acp-host.mjs @@ -723,7 +723,7 @@ export function createAcpHostRuntime(options = {}) { return adapt(resolveAcpCommand(input, commandDeps)); }; - const connect = async (command, requestId, emit, signal, authMethodId, mcpServers = [], allowPermissions = false, workBuddyAuth = null) => { + const connect = async (command, requestId, emit, signal, authMethodId, mcpServers = [], allowPermissions = false, workBuddyAuth = null, verifyWorkBuddy = false) => { const cwd = await createAcpWorkspace(mkdtempImpl, options.tmpRoot); let child = null; let authMethods = []; @@ -739,10 +739,18 @@ export function createAcpHostRuntime(options = {}) { void connection.closed?.catch(() => {}); const initialized = await connection.initialize(acpInitializeParams(version)); authMethods = publicAuthMethods(initialized); + const readCredential = async () => { + const result = await connection.extMethod("_codebuddy.ai/getUserInfo", {}); + const token = result?.userInfo?.accessToken ?? result?.userInfo?.token; + // The bundled CLI can restore account metadata without decrypting tokens. + if (result?.userInfo && !(typeof token === "string" && token)) throw new Error("workbuddy_credentials_unavailable"); + return typeof token === "string" && token ? createHash("sha256").update(token).digest("hex") : null; + }; + if (verifyWorkBuddy) await readCredential(); if (authMethodId) { if (!authMethods.some((method) => method.id === authMethodId)) throw new Error("invalid_auth_method"); authenticationPending = true; - if (workBuddyAuth) await authenticateWorkBuddy(workBuddyAuth, authMethodId, signal); + if (workBuddyAuth) await authenticateWorkBuddy(workBuddyAuth, authMethodId, signal, { readCredential }); else await connection.authenticate({ methodId: authMethodId }); authenticationPending = false; } @@ -867,7 +875,7 @@ export function createAcpHostRuntime(options = {}) { if (!resolved.ok) return adapterFromResolution(id, resolved); let connected; try { - connected = await withHandshakeTimeout((signal) => connect(resolved.command, `probe-${id}`, () => {}, signal)); + connected = await withHandshakeTimeout((signal) => connect(resolved.command, `probe-${id}`, () => {}, signal, undefined, [], false, null, id === "workbuddyCn" || id === "workbuddyIntl")); } catch (error) { const failure = { ...adapterShell(id), ...failureFields(classifyAcpFailure(error)), ...(isAuthRequiredError(error) ? { authMethods: error.authMethods ?? [] } : {}), ...(resolved.version ? { version: resolved.version, managed: true } : {}) }; latestStatus.set(id, failure); @@ -898,6 +906,13 @@ export function createAcpHostRuntime(options = {}) { const workBuddyAuth = id === "workbuddyCn" || id === "workbuddyIntl" ? await prepareWorkBuddyAuth(resolved.command) : null; return connect(workBuddyAuth?.command ?? resolved.command, `auth-${id}`, () => {}, signal, input.methodId, [], false, workBuddyAuth); }, authenticationTimeoutMs); + if (id === "workbuddyCn" || id === "workbuddyIntl") { + connected.stop(); + await removeAcpWorkspace(connected.cwd, rmImpl); + connected = null; + // Login is useful only if a fresh chat process can restore credentials. + connected = await withHandshakeTimeout((signal) => connect(resolved.command, `auth-check-${id}`, () => {}, signal, undefined, [], false, null, true)); + } const adapter = { ...adapterShell(id), state: "available", promptCapabilities: connected.promptCapabilities, authMethods: connected.authMethods, ...(resolved.version ? { version: resolved.version, managed: true } : {}) }; latestStatus.set(id, adapter); return adapter; @@ -951,7 +966,7 @@ export function createAcpHostRuntime(options = {}) { try { connected = await withHandshakeTimeout((signal) => connect( resolved.command, requestId, notify, signal, undefined, - mcpBridge ? [mcpServerFor(mcpBridge)] : [], true, + mcpBridge ? [mcpServerFor(mcpBridge)] : [], true, null, input.adapterId === "workbuddyCn" || input.adapterId === "workbuddyIntl", )); rememberPromptStatus({ state: "available", diff --git a/apps/desktop/src/main/acp-host.test.mjs b/apps/desktop/src/main/acp-host.test.mjs index 673692e1..cf1f574c 100644 --- a/apps/desktop/src/main/acp-host.test.mjs +++ b/apps/desktop/src/main/acp-host.test.mjs @@ -84,9 +84,9 @@ const collector = () => { }; }; -const fakeAgentSource = ({ reportPath, secretPath, allowImage, allowEmbedded, hold, requireAuth, advertiseAuth, promptRefusal, hangAuth, hangSession, workBuddyAccountApi, workBuddyLoginFailure, authMethodId = "browser" }) => `#!/usr/bin/env bun +const fakeAgentSource = ({ reportPath, secretPath, allowImage, allowEmbedded, hold, requireAuth, advertiseAuth, promptRefusal, hangAuth, hangSession, workBuddyAccountApi, workBuddyLoginFailure, workBuddyUnreadable, workBuddyUnreadableAfterLogin, authMethodId = "browser" }) => `#!/usr/bin/env bun import * as acp from ${JSON.stringify(sdkHref)}; -import { writeFileSync } from "node:fs"; +import { writeFileSync, readFileSync } from "node:fs"; const reportPath = ${JSON.stringify(reportPath)}; const secretPath = ${JSON.stringify(secretPath)}; @@ -98,6 +98,11 @@ const advertiseAuth = ${advertiseAuth ? "true" : "false"}; const promptRefusal = ${promptRefusal ? JSON.stringify(promptRefusal) : "null"}; let authenticated = false; const report = { initialize: null, newSession: null, permission: null, readError: null, readResult: null, prompt: null, authMethod: null }; +if (${workBuddyAccountApi ? "true" : "false"}) { + try { Object.assign(report, JSON.parse(readFileSync(reportPath, "utf8"))); authenticated = report.accountLogin === true && ${workBuddyLoginFailure ? "false" : "true"}; } catch {} +} +if (${workBuddyUnreadable ? "true" : "false"}) authenticated = true; +const restoredAccount = authenticated; const save = () => writeFileSync(reportPath, JSON.stringify(report)); if (${workBuddyAccountApi ? "true" : "false"}) { const { createServer } = await import("node:http"); @@ -164,6 +169,7 @@ acp.agent({ name: "edgeever-fake-agent" }) authMethods: requireAuth || advertiseAuth ? [{ id: ${JSON.stringify(authMethodId)}, name: "Browser" }] : [], }; }) + .onRequest("_codebuddy.ai/getUserInfo", (params) => params, () => ({ userInfo: authenticated ? { ${workBuddyUnreadable ? 'userId: "saved-user"' : 'accessToken: "new-token"'} , ...(${workBuddyUnreadableAfterLogin ? "true" : "false"} && restoredAccount ? { accessToken: undefined, userId: "saved-user" } : {}) } : undefined })) .onRequest("authenticate", (ctx) => { report.authMethod = ctx.params.methodId; if (${hangAuth ? "true" : "false"}) { @@ -913,6 +919,34 @@ describe("ACP stdio session", () => { } }, 15_000); + sessionTest("WorkBuddy encrypted metadata cannot count as a usable connection or successful login", async () => { + const directory = await mkdtemp(path.join(tmpdir(), "edgeever-acp-unreadable-auth-")); + const reportPath = path.join(directory, "report.json"); + try { + const scriptPath = await writeFakeAgent(directory, { reportPath, requireAuth: true, workBuddyAccountApi: true, workBuddyUnreadable: true, authMethodId: "external" }); + const runtime = createAcpHostRuntime({ adapterManager: { get: () => ({ command: { command: process.execPath, args: [scriptPath] } }) } }); + expect(await runtime.probeAdapter({ id: "workbuddyIntl" })).toMatchObject({ state: "failed", detail: "workbuddy_credentials_unavailable" }); + expect(await runtime.authenticateAdapter({ id: "workbuddyIntl", methodId: "external" })).toMatchObject({ state: "failed", detail: "workbuddy_credentials_unavailable" }); + const events = []; + await runtime.prompt({ adapterId: "workbuddyIntl", prompt: "hello", noteAccess: false }, (event) => events.push(event)); + expect(events).toMatchObject([{ type: "error", message: "workbuddy_credentials_unavailable" }]); + const report = await readReport(reportPath); + expect(report.accountLogin).toBeUndefined(); + expect(report.newSession).toBeNull(); + } finally { await rm(directory, { recursive: true, force: true }); } + }, 10_000); + + sessionTest("WorkBuddy login must survive a fresh chat process before reporting success", async () => { + const directory = await mkdtemp(path.join(tmpdir(), "edgeever-acp-auth-restore-")); + const reportPath = path.join(directory, "report.json"); + try { + const scriptPath = await writeFakeAgent(directory, { reportPath, requireAuth: true, workBuddyAccountApi: true, workBuddyUnreadableAfterLogin: true, authMethodId: "external" }); + const runtime = createAcpHostRuntime({ adapterManager: { get: () => ({ command: { command: process.execPath, args: [scriptPath] } }) } }); + expect(await runtime.authenticateAdapter({ id: "workbuddyIntl", methodId: "external" })).toMatchObject({ state: "failed", detail: "workbuddy_credentials_unavailable" }); + expect((await readReport(reportPath)).accountLogin).toBe(true); + } finally { await rm(directory, { recursive: true, force: true }); } + }, 10_000); + for (const adapterId of ["workbuddyIntl", "workbuddyCn"]) for (const loginFailure of [false, true]) sessionTest(`${adapterId} account login ${loginFailure ? "returns failure without hanging" : "verifies ACP session without the broken authenticate RPC"}`, async () => { const directory = await mkdtemp(path.join(tmpdir(), "edgeever-acp-workbuddy-auth-")); const reportPath = path.join(directory, "report.json"); diff --git a/apps/desktop/src/main/workbuddy-auth.mjs b/apps/desktop/src/main/workbuddy-auth.mjs index 32a8987a..e8931745 100644 --- a/apps/desktop/src/main/workbuddy-auth.mjs +++ b/apps/desktop/src/main/workbuddy-auth.mjs @@ -24,7 +24,7 @@ export async function prepareWorkBuddyAuth(command) { // WorkBuddy's ACP authenticate handler ignores login() returning false and then // waits forever for a session. Its local account API reports that failure and // starts login without first logging out (which can cancel login via its watcher). -export async function authenticateWorkBuddy({ endpoint, password }, methodId, signal, { fetchImpl = fetch, pollIntervalMs = 500 } = {}) { +export async function authenticateWorkBuddy({ endpoint, password }, methodId, signal, { fetchImpl = fetch, pollIntervalMs = 500, readCredential } = {}) { const request = async (route, body) => { const response = await fetchImpl(`${endpoint}/api/v1/auth/account/${route}`, { method: body ? "POST" : "GET", @@ -39,14 +39,19 @@ export async function authenticateWorkBuddy({ endpoint, password }, methodId, si return result; }; const signedIn = (status) => status.authenticated === true && typeof status.account?.userId === "string" && Boolean(status.account.userId); - const status = await request("status"); - if (signedIn(status)) return; + // A saved account can contain an expired token. An explicit login must start + // a new flow, and the old account must not satisfy polling while it is open. + if (typeof readCredential !== "function") throw new Error("workbuddy_auth_unavailable"); + const previousCredential = await readCredential(); const started = await request("login", { method: methodId }); if (started.loginError || started.success !== true) throw new Error("authentication_failed"); for (;;) { await delay(pollIntervalMs, undefined, { signal }); const status = await request("status"); - if (signedIn(status)) return; if (status.loginError) throw new Error("authentication_failed"); + if (signedIn(status)) { + const credential = await readCredential(); + if (credential && credential !== previousCredential) return; + } } } diff --git a/apps/desktop/src/main/workbuddy-auth.test.mjs b/apps/desktop/src/main/workbuddy-auth.test.mjs index 97b70357..0c1cbff4 100644 --- a/apps/desktop/src/main/workbuddy-auth.test.mjs +++ b/apps/desktop/src/main/workbuddy-auth.test.mjs @@ -1,10 +1,11 @@ import { expect, test } from "bun:test"; import { authenticateWorkBuddy, prepareWorkBuddyAuth } from "./workbuddy-auth.mjs"; -const fixture = (responses) => { +const fixture = (responses, credentials = [null, "new-token"]) => { const calls = []; return { calls, + readCredential: async () => credentials.shift(), fetchImpl: async (url, options) => { calls.push({ url, ...options }); const response = responses.shift(); @@ -16,40 +17,47 @@ const fixture = (responses) => { const auth = { endpoint: "http://127.0.0.1:12345", password: "private-password" }; test("starts login without logout and waits for an authenticated account", async () => { - const f = fixture([{ authenticated: false }, { success: true }, { authenticated: false }, { authenticated: true, account: { userId: "user" } }]); + const f = fixture([{ success: true }, { authenticated: false }, { authenticated: true, account: { userId: "user" } }]); await authenticateWorkBuddy(auth, "external", new AbortController().signal, { ...f, pollIntervalMs: 1 }); expect(f.calls.map((call) => new URL(call.url).pathname)).toEqual([ - "/api/v1/auth/account/status", "/api/v1/auth/account/login", "/api/v1/auth/account/status", "/api/v1/auth/account/status", + "/api/v1/auth/account/login", "/api/v1/auth/account/status", "/api/v1/auth/account/status", ]); - expect(JSON.parse(f.calls[1].body)).toEqual({ method: "external" }); + expect(JSON.parse(f.calls[0].body)).toEqual({ method: "external" }); expect(f.calls.every((call) => call.headers.Authorization === "Bearer private-password" && call.headers["x-codebuddy-request"] === "1" && call.redirect === "error")).toBe(true); }); -test("preserves an existing authenticated account without signing it out", async () => { - const f = fixture([{ authenticated: true, account: { userId: "saved-user" } }]); - await authenticateWorkBuddy(auth, "external", new AbortController().signal, f); - expect(f.calls).toHaveLength(1); +test("explicit login replaces a stale saved credential without logout", async () => { + const f = fixture([{ success: true }, { authenticated: true, account: { userId: "saved-user" } }, { authenticated: true, account: { userId: "saved-user" } }], ["old-token", "old-token", "new-token"]); + await authenticateWorkBuddy(auth, "external", new AbortController().signal, { ...f, pollIntervalMs: 1 }); + expect(f.calls.map((call) => new URL(call.url).pathname)).toEqual([ + "/api/v1/auth/account/login", "/api/v1/auth/account/status", "/api/v1/auth/account/status", + ]); +}); + +test("a failed re-login is not masked by a saved authenticated account", async () => { + const f = fixture([{ success: true }, { authenticated: true, account: { userId: "saved-user" }, loginError: "failed" }], ["old-token"]); + await expect(authenticateWorkBuddy(auth, "external", new AbortController().signal, { ...f, pollIntervalMs: 1 })).rejects.toThrow("authentication_failed"); }); test("returns a swallowed or cancelled login failure before the overall timeout", async () => { - const f = fixture([{ authenticated: false }, { success: true }, { authenticated: false, loginError: "account.login.failed" }]); + const f = fixture([{ success: true }, { authenticated: false, loginError: "account.login.failed" }]); await expect(authenticateWorkBuddy(auth, "external", new AbortController().signal, { ...f, pollIntervalMs: 1 })).rejects.toThrow("authentication_failed"); }); test("does not treat an opened browser or an accountless status as login success", async () => { - const f = fixture([{ authenticated: true }, { success: true, authUrl: "https://example.com" }, { authenticated: true }, { loginError: "failed" }]); + const f = fixture([{ success: true, authUrl: "https://example.com" }, { authenticated: true }, { loginError: "failed" }]); await expect(authenticateWorkBuddy(auth, "external", new AbortController().signal, { ...f, pollIntervalMs: 1 })).rejects.toThrow("authentication_failed"); }); test("stops pending polling when authentication times out", async () => { - const f = fixture([{ authenticated: false }, { success: true }]); + const f = fixture([{ success: true }]); const signal = AbortSignal.timeout(20); await expect(authenticateWorkBuddy(auth, "external", signal, { ...f, pollIntervalMs: 100 })).rejects.toThrow(); - expect(f.calls).toHaveLength(2); + expect(f.calls).toHaveLength(1); }); test("fails closed when the installed CLI lacks the account API", async () => { - await expect(authenticateWorkBuddy(auth, "external", new AbortController().signal, { fetchImpl: async () => new Response("unsupported", { status: 404 }) })).rejects.toThrow("workbuddy_auth_unavailable"); + await expect(authenticateWorkBuddy(auth, "external", new AbortController().signal, { readCredential: async () => null, fetchImpl: async () => new Response("unsupported", { status: 404 }) })).rejects.toThrow("workbuddy_auth_unavailable"); }); test("binds the login server to loopback with a fresh password and keeps the edition", async () => { diff --git a/apps/web/src/components/ai-sidebar/AiSidebar.tsx b/apps/web/src/components/ai-sidebar/AiSidebar.tsx index 29e1e397..398b8965 100644 --- a/apps/web/src/components/ai-sidebar/AiSidebar.tsx +++ b/apps/web/src/components/ai-sidebar/AiSidebar.tsx @@ -996,6 +996,7 @@ function AiSidebarSession({ : turn)); if (event.type === "error" && event.message) { setError(event.message === "note_access_unavailable" ? t("aiAssistant.sidebar.noteAccessUnavailable") + : event.message === "workbuddy_credentials_unavailable" ? t("aiAssistant.agentSource.workbuddyCredentialsUnavailable") : event.message === "needs_login" ? t(localAdapterId === "workbuddyCn" || localAdapterId === "workbuddyIntl" ? "aiAssistant.sidebar.workbuddyLoginRequired" : "aiAssistant.sidebar.localLoginRequired") : event.message === "agent_refused" ? t("aiAssistant.sidebar.agentRefused") : event.message); diff --git a/apps/web/src/components/settings/DesktopAcpAgentCard.tsx b/apps/web/src/components/settings/DesktopAcpAgentCard.tsx index d2a1e850..86ebfde9 100644 --- a/apps/web/src/components/settings/DesktopAcpAgentCard.tsx +++ b/apps/web/src/components/settings/DesktopAcpAgentCard.tsx @@ -44,6 +44,7 @@ const readAdapterId = (): DesktopAcpAdapterId => { const statusKey = (adapter: DesktopAcpAdapter | undefined, probing: boolean) => { if (probing) return "aiAssistant.agentSource.probing"; if (!adapter || adapter.detail === "not_probed") return "aiAssistant.agentSource.notProbed"; + if (adapter.detail === "workbuddy_credentials_unavailable") return "aiAssistant.agentSource.workbuddyCredentialsUnavailable"; if (adapter.detail === "authentication_timeout") return "aiAssistant.agentSource.authenticationTimedOut"; if (adapter.detail === "authentication_failed") return "aiAssistant.agentSource.authenticationFailed"; if (adapter.detail === "invalid_path") return "aiAssistant.agentSource.invalidPath"; diff --git a/apps/web/src/lib/infographic-local-agent.ts b/apps/web/src/lib/infographic-local-agent.ts index 9f286a30..7d22aa91 100644 --- a/apps/web/src/lib/infographic-local-agent.ts +++ b/apps/web/src/lib/infographic-local-agent.ts @@ -103,6 +103,7 @@ export const parseInfographicLocalReply = (raw: string, candidates: string[], pr }; export const infographicLocalAgentErrorKey = (message: string, adapterId: string | null) => { + if (message === "workbuddy_credentials_unavailable") return "aiAssistant.agentSource.workbuddyCredentialsUnavailable"; if (message === "needs_login") { return adapterId === "workbuddyCn" || adapterId === "workbuddyIntl" ? "aiAssistant.sidebar.workbuddyLoginRequired" diff --git a/packages/shared/src/i18n/en-US.ts b/packages/shared/src/i18n/en-US.ts index 79f102dc..4d95117b 100644 --- a/packages/shared/src/i18n/en-US.ts +++ b/packages/shared/src/i18n/en-US.ts @@ -1732,6 +1732,7 @@ export const enUS = { workbuddyMissingHint: "The WorkBuddy desktop app includes the ACP program; no separate CodeBuddy installation is needed. A standalone CodeBuddy CLI also works.", workbuddyCnLoginHint: "ACP tries saved sign-in first. If chat still requires login, use WeChat or your organization's ACP sign-in method.", workbuddyIntlLoginHint: "ACP tries saved sign-in first. If chat still requires login, use Google/GitHub or your organization's ACP sign-in method.", + workbuddyCredentialsUnavailable: "WorkBuddy saved sign-in credentials could not be read, so ACP is unavailable. Use another local Agent for now; retrying sign-in cannot resolve this issue.", workbuddyConnectionReady: "ACP connected; sign-in is checked during chat", installing: "Installing and checking…", installFailed: "Installation or connection check failed. The previous version was kept.", diff --git a/packages/shared/src/i18n/ja.ts b/packages/shared/src/i18n/ja.ts index 8d71b213..c12f22bf 100644 --- a/packages/shared/src/i18n/ja.ts +++ b/packages/shared/src/i18n/ja.ts @@ -1721,6 +1721,7 @@ export const ja = { workbuddyMissingHint: "WorkBuddy デスクトップアプリには ACP プログラムが含まれるため、CodeBuddy の別途インストールは不要です。単体の CodeBuddy CLI も利用できます。", workbuddyCnLoginHint: "ACP は保存済みのログインを試します。チャットでログインが必要な場合は WeChat または組織指定の方法で ACP 認証を行ってください。", workbuddyIntlLoginHint: "ACP は保存済みのログインを試します。チャットでログインが必要な場合は Google/GitHub または組織指定の方法で ACP 認証を行ってください。", + workbuddyCredentialsUnavailable: "WorkBuddy の保存済みログイン情報を読み取れないため、ACP は利用できません。当面は別のローカル Agent を使用してください。再ログインでは解決できません。", workbuddyConnectionReady: "ACP 接続済み・ログインはチャット時に確認", installing: "インストールして確認中…", installFailed: "インストールまたは接続確認に失敗しました。以前のバージョンは保持されています。", diff --git a/packages/shared/src/i18n/pl.ts b/packages/shared/src/i18n/pl.ts index 12c38723..0b9872c3 100644 --- a/packages/shared/src/i18n/pl.ts +++ b/packages/shared/src/i18n/pl.ts @@ -1721,6 +1721,7 @@ export const pl = { workbuddyMissingHint: "Aplikacja desktopowa WorkBuddy zawiera program ACP; osobna instalacja CodeBuddy nie jest potrzebna. Działa też samodzielny CodeBuddy CLI.", workbuddyCnLoginHint: "ACP najpierw próbuje użyć zapisanego logowania. Jeśli czat nadal wymaga zalogowania, użyj WeChat lub metody logowania ACP swojej organizacji.", workbuddyIntlLoginHint: "ACP najpierw próbuje użyć zapisanego logowania. Jeśli czat nadal wymaga zalogowania, użyj Google/GitHub lub metody logowania ACP swojej organizacji.", + workbuddyCredentialsUnavailable: "Nie można odczytać zapisanych danych logowania WorkBuddy, więc ACP jest niedostępne. Na razie użyj innego lokalnego agenta; ponowne logowanie nie rozwiąże problemu.", workbuddyConnectionReady: "Połączono z ACP; logowanie jest sprawdzane podczas czatu", installing: "Instalowanie i sprawdzanie…", installFailed: "Instalacja lub sprawdzenie połączenia nie powiodło się. Zachowano poprzednią wersję.", diff --git a/packages/shared/src/i18n/zh-CN.ts b/packages/shared/src/i18n/zh-CN.ts index 924ed974..56c1e734 100644 --- a/packages/shared/src/i18n/zh-CN.ts +++ b/packages/shared/src/i18n/zh-CN.ts @@ -1732,6 +1732,7 @@ export const zhCN = { workbuddyMissingHint: "安装 WorkBuddy 桌面版即可使用其内置 ACP,无需另装 CodeBuddy;也可单独安装 CodeBuddy CLI。", workbuddyCnLoginHint: "ACP 会尝试使用已保存的登录;若聊天仍提示需要登录,请使用微信或组织指定方式完成 ACP 认证。", workbuddyIntlLoginHint: "ACP 会尝试使用已保存的登录;若聊天仍提示需要登录,请使用 Google/GitHub 或组织指定方式完成 ACP 认证。", + workbuddyCredentialsUnavailable: "无法读取 WorkBuddy 保存的登录凭据,ACP 当前不可用。请先改用其他本机 Agent;重复点击登录无法解决此问题。", workbuddyConnectionReady: "ACP 已连接,聊天时验证登录", installing: "正在安装并检查…", installFailed: "安装或连接检查失败,原版本已保留。",