From 49e7a8ce4e19465d64addbc128d4db4fadb61151 Mon Sep 17 00:00:00 2001 From: NekoPunch Date: Thu, 3 Sep 2026 01:34:09 -0700 Subject: [PATCH 1/3] fix(i18n): emit settings expected-result codes from producers Settings-area producers (memory, data, permission center, connection test, computer-use health, dev dialogs) emitted zh prose that reached en users verbatim, and presenters sniffed CJK to decide whether to show it. Producers now return stable codes; each settings catalog maps its codes per locale with an explicit unknown fallback. Generated-by: Claude Code --- apps/desktop/renderer-architecture.json | 1 - .../main/__tests__/computer-use-host.test.ts | 8 +- .../github-copilot-local-credential.test.ts | 6 +- .../main/__tests__/oauth-result-copy.test.ts | 40 +++++++++ .../__tests__/permission-center-copy.test.ts | 26 ++++++ .../runtime-host-artifacts-ipc-main.test.ts | 2 + .../runtime-host-memory-ipc-main.test.ts | 1 + .../settings-test-result-copy.test.ts | 10 +++ apps/desktop/src/main/browser-message-box.ts | 5 +- apps/desktop/src/main/capability-snapshot.ts | 87 +++++++------------ apps/desktop/src/main/computer-use-host.ts | 15 ++-- apps/desktop/src/main/main.ts | 36 +++++--- .../oauth/github-copilot-local-credential.ts | 10 ++- .../main/runtime-host-artifacts-ipc-main.ts | 9 +- apps/desktop/src/main/runtime-host-boot.ts | 4 + .../src/main/runtime-host-config-ipc-main.ts | 11 ++- .../runtime-host-github-copilot-ipc-main.ts | 22 ++--- .../src/main/runtime-host-memory-ipc-main.ts | 34 ++++++-- apps/desktop/src/preload/bridge-contract.d.ts | 10 +-- apps/desktop/src/preload/preload.ts | 10 +-- .../features/connection-settings/index.ts | 2 +- .../provider-panel-shared.ts | 5 +- .../settings-provider-copy.ts | 66 +++++++++++++- .../locales/permission-center-copy.ts | 73 ++++++++++++++++ .../renderer/locales/settings-data-copy.ts | 3 + .../renderer/locales/settings-memory-copy.ts | 29 ++++++- .../locales/settings-test-result-copy.ts | 9 +- .../renderer/settings/data-settings-page.tsx | 5 +- .../settings/permission-center-page.tsx | 56 +++++++++--- .../settings/provider-oauth-section.tsx | 6 +- .../use-memory-settings-controller.ts | 21 +++-- .../renderer/settings/use-oauth-login-flow.ts | 52 ++--------- packages/core/src/capabilities.ts | 43 +++++++++ packages/core/src/health.ts | 28 +++++- packages/core/src/llm-connections.ts | 2 + packages/core/src/oauth-subscription.ts | 8 +- packages/core/src/settings.ts | 1 + packages/runtime/src/test-connection.ts | 3 +- 38 files changed, 558 insertions(+), 201 deletions(-) create mode 100644 apps/desktop/src/main/__tests__/oauth-result-copy.test.ts diff --git a/apps/desktop/renderer-architecture.json b/apps/desktop/renderer-architecture.json index e54ddacfd2..899a1cffcb 100644 --- a/apps/desktop/renderer-architecture.json +++ b/apps/desktop/renderer-architecture.json @@ -4305,7 +4305,6 @@ "../features/connection-settings": 1, "./oauth-login-flow-guard": 1, "./runtime-host-settings-target.js": 1, - "@maka/core/redaction": 1, "@maka/core/ui-locale": 1, "@maka/ui": 1, "react": 1 diff --git a/apps/desktop/src/main/__tests__/computer-use-host.test.ts b/apps/desktop/src/main/__tests__/computer-use-host.test.ts index 51c89b636e..3fe5bd3a1e 100644 --- a/apps/desktop/src/main/__tests__/computer-use-host.test.ts +++ b/apps/desktop/src/main/__tests__/computer-use-host.test.ts @@ -36,7 +36,7 @@ describe('Computer Use host health', () => { it('does not report a binary-only executor as healthy before first use', () => { assert.deepEqual(computerUseServiceHealth('maka-cu', snapshot('idle')), { state: 'not_run', - reason: 'maka-cu 已可用,将在首次调用时启动。', + reason: 'cu_executor_lazy_start', }); }); @@ -44,7 +44,7 @@ describe('Computer Use host health', () => { assert.equal(computerUseServiceHealth('maka-cu', snapshot('ready')).state, 'healthy'); assert.equal( computerUseServiceHealth('maka-cu', snapshot('backing_off')).reason, - 'maka-cu executor 正在启动或恢复。', + 'cu_executor_recovering', ); assert.equal( computerUseServiceHealth('maka-cu', snapshot('starting')).state, @@ -52,11 +52,11 @@ describe('Computer Use host health', () => { ); assert.deepEqual(computerUseServiceHealth('maka-cu', snapshot('unavailable')), { state: 'not_available', - reason: 'maka-cu executor 启动失败或已退出。', + reason: 'cu_executor_start_failed', }); assert.deepEqual(computerUseServiceHealth('maka-cu', snapshot('disposed')), { state: 'not_available', - reason: 'maka-cu executor 已停止。', + reason: 'cu_executor_stopped', }); }); diff --git a/apps/desktop/src/main/__tests__/github-copilot-local-credential.test.ts b/apps/desktop/src/main/__tests__/github-copilot-local-credential.test.ts index a034d7cd1d..37d330c738 100644 --- a/apps/desktop/src/main/__tests__/github-copilot-local-credential.test.ts +++ b/apps/desktop/src/main/__tests__/github-copilot-local-credential.test.ts @@ -71,7 +71,7 @@ describe('importGitHubCopilotLocalCredential', () => { assert.equal(imported.result.ok, false); if (!imported.result.ok) { assert.equal(imported.result.reason, 'token_exchange_failed'); - assert.match(imported.result.message, /不支持 classic PAT/); + assert.equal(imported.result.code, 'copilot_classic_pat_unsupported'); assert.equal(imported.result.message.includes('ghp_classic_pat'), false); } assert.equal(imported.secret, undefined); @@ -83,7 +83,7 @@ describe('importGitHubCopilotLocalCredential', () => { }); assert.equal(imported.result.ok, false); - if (!imported.result.ok) assert.match(imported.result.message, /凭据类型不受支持/); + if (!imported.result.ok) assert.equal(imported.result.code, 'copilot_credential_type_unsupported'); assert.equal(imported.secret, undefined); }); @@ -95,7 +95,7 @@ describe('importGitHubCopilotLocalCredential', () => { }); assert.equal(imported.result.ok, false); - if (!imported.result.ok) assert.match(imported.result.message, /未找到可导入/); + if (!imported.result.ok) assert.equal(imported.result.code, 'copilot_local_credential_missing'); assert.equal(imported.secret, undefined); }); }); diff --git a/apps/desktop/src/main/__tests__/oauth-result-copy.test.ts b/apps/desktop/src/main/__tests__/oauth-result-copy.test.ts new file mode 100644 index 0000000000..c9a4f964e8 --- /dev/null +++ b/apps/desktop/src/main/__tests__/oauth-result-copy.test.ts @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import assert from "node:assert/strict"; +import test from "node:test"; +import { subscriptionResultMessage } from "../../renderer/features/connection-settings/index.js"; + +test("renders a coded Copilot import failure per locale, ignoring its machine message", () => { + const result = { code: "copilot_subscription_unavailable", message: "copilot_subscription_unavailable" }; + assert.equal(subscriptionResultMessage(result, "fallback", "zh"), "当前 GitHub 账号没有可用的 Copilot 订阅权限。"); + assert.equal(subscriptionResultMessage(result, "fallback", "en"), "This GitHub account has no usable Copilot subscription."); +}); + +test("renders the typed experimental_disabled reason per locale", () => { + const result = { reason: "experimental_disabled", message: "enrollment is disabled for this provider" }; + assert.equal(subscriptionResultMessage(result, "fallback", "zh"), "本机未启用该账号登录方式;可改用导入兼容凭据,或由管理员启用后重试。"); + assert.equal(subscriptionResultMessage(result, "fallback", "en"), "This sign-in is not enabled on this install. Import a compatible credential instead, or ask an operator to enable it."); +}); + +test("falls back to catalog copy for an unknown code instead of the raw message", () => { + const result = { code: "not_a_known_code", message: "内部错误" }; + assert.equal(subscriptionResultMessage(result, "fallback", "en"), "fallback"); + assert.equal(subscriptionResultMessage(result, "fallback", "zh"), "fallback"); +}); diff --git a/apps/desktop/src/main/__tests__/permission-center-copy.test.ts b/apps/desktop/src/main/__tests__/permission-center-copy.test.ts index 6333051056..df6f47cf5b 100644 --- a/apps/desktop/src/main/__tests__/permission-center-copy.test.ts +++ b/apps/desktop/src/main/__tests__/permission-center-copy.test.ts @@ -25,3 +25,29 @@ test('presents a granted OS permission as a verified success', () => { assert.equal(getPermissionCenterCopy('zh').osStates.granted.tone, 'success'); assert.equal(getPermissionCenterCopy('en').osStates.granted.tone, 'success'); }); + +test('renders capability reason codes per locale', () => { + const zh = getPermissionCenterCopy('zh'); + const en = getPermissionCenterCopy('en'); + assert.equal(zh.reasons['missing platform credentials'], '未配置平台凭据'); + assert.equal(en.reasons['missing platform credentials'], 'Platform credentials are not configured'); + assert.equal(zh.reasons.cu_executor_recovering, 'maka-cu executor 正在启动或恢复。'); + assert.equal(en.reasons.cu_executor_recovering, 'The maka-cu executor is starting or recovering.'); +}); + +test('composes the computer-use backend status from snapshot facts per locale', () => { + const zh = getPermissionCenterCopy('zh'); + const en = getPermissionCenterCopy('en'); + assert.equal( + zh.cuBackendStatus(['辅助功能', '屏幕录制'], 'healthy'), + 'maka-cu artifact 已通过本地完整性检查。等待辅助功能、屏幕录制权限。操作与截图 service 已就绪;按目标与动作类别授权后可操作本机应用。', + ); + assert.equal( + zh.cuBackendStatus([], 'not_run'), + 'maka-cu artifact 已通过本地完整性检查。service 将在首次调用时启动;按目标与动作类别授权后可操作本机应用。', + ); + assert.equal( + en.cuBackendStatus(['Accessibility'], 'degraded'), + 'The maka-cu artifact passed the local integrity check. Waiting for Accessibility permission. The maka-cu service is starting or recovering.', + ); +}); diff --git a/apps/desktop/src/main/__tests__/runtime-host-artifacts-ipc-main.test.ts b/apps/desktop/src/main/__tests__/runtime-host-artifacts-ipc-main.test.ts index 6c452d5922..3af101aa79 100644 --- a/apps/desktop/src/main/__tests__/runtime-host-artifacts-ipc-main.test.ts +++ b/apps/desktop/src/main/__tests__/runtime-host-artifacts-ipc-main.test.ts @@ -52,6 +52,7 @@ function attachmentReadHandler( ): Handler { const handlers = new Map(); registerRuntimeHostArtifactsIpc({ + uiLocale: () => 'zh' as const, ipcMain: { handle: (channel, handler) => handlers.set(channel, handler as Handler), }, @@ -121,6 +122,7 @@ test("Runtime Host Artifact IPC preserves previews and streams complete exports" try { registerRuntimeHostArtifactsIpc({ + uiLocale: () => 'zh' as const, ipcMain: { handle: (channel, handler) => handlers.set(channel, handler as Handler), }, diff --git a/apps/desktop/src/main/__tests__/runtime-host-memory-ipc-main.test.ts b/apps/desktop/src/main/__tests__/runtime-host-memory-ipc-main.test.ts index 17391c12fc..fe7e199457 100644 --- a/apps/desktop/src/main/__tests__/runtime-host-memory-ipc-main.test.ts +++ b/apps/desktop/src/main/__tests__/runtime-host-memory-ipc-main.test.ts @@ -175,6 +175,7 @@ test('does not project or open remote Runtime Host file paths', async () => { assert.deepEqual(projected.backups.map(({ path }) => path), ['']); assert.deepEqual(opened, { ok: false, + code: 'remote_host_owned', message: 'Memory files are owned by the remote Runtime Host', }); }); diff --git a/apps/desktop/src/main/__tests__/settings-test-result-copy.test.ts b/apps/desktop/src/main/__tests__/settings-test-result-copy.test.ts index 757ced374c..9c1369983d 100644 --- a/apps/desktop/src/main/__tests__/settings-test-result-copy.test.ts +++ b/apps/desktop/src/main/__tests__/settings-test-result-copy.test.ts @@ -37,3 +37,13 @@ test("missing proxy credentials have actionable bilingual copy", () => { "Proxy authentication is enabled. Enter a proxy password before testing.", ); }); + +test("renders the disabled-direct proxy code per locale", () => { + const result = { ok: true, code: "proxy_disabled_direct", message: "direct" } as never; + assert.equal(settingsTestResultMessage(result, "zh"), "代理未启用,当前会直接连接。"); + assert.equal( + settingsTestResultMessage(result, "en"), + "The proxy is disabled; connections go direct.", + ); +}); + diff --git a/apps/desktop/src/main/browser-message-box.ts b/apps/desktop/src/main/browser-message-box.ts index 755ecfb8d4..eb4e0292f1 100644 --- a/apps/desktop/src/main/browser-message-box.ts +++ b/apps/desktop/src/main/browser-message-box.ts @@ -17,6 +17,7 @@ * under the License. */ +import type { UiCatalog } from '@maka/core/ui-locale'; import { randomUUID } from 'node:crypto'; import { readFileSync } from 'node:fs'; import { join } from 'node:path'; @@ -348,7 +349,7 @@ export function buildBrowserMessageBoxHtml( function renderBrowserMessageBoxHtml(input: BrowserMessageBoxPresentation): string { const nonce = randomUUID().replaceAll('-', ''); - const closeLabel = input.locale === 'zh' ? '关闭' : 'Close'; + const closeLabel = CLOSE_LABEL[input.locale]; const closeButton = ``; @@ -638,3 +639,5 @@ function escapeHtml(value: string): string { return entities[character] ?? character; }); } + +const CLOSE_LABEL = { zh: '关闭', en: 'Close' } satisfies UiCatalog; diff --git a/apps/desktop/src/main/capability-snapshot.ts b/apps/desktop/src/main/capability-snapshot.ts index 8c430d9b00..ab150b1c34 100644 --- a/apps/desktop/src/main/capability-snapshot.ts +++ b/apps/desktop/src/main/capability-snapshot.ts @@ -27,6 +27,7 @@ import { type CapabilityFeatureSignal, type CapabilityMemoryAcceptanceSignal, type CapabilityPermissionRequirement, + type CapabilityReasonCode, type CapabilityRuntimeProbeSignal, type CapabilitySnapshot, type CapabilitySnapshotCollection, @@ -80,7 +81,7 @@ export function buildCapabilitySnapshotCollection(input: { feature: { state: 'partial', source: 'runtime', - reason: 'Daily Review 已聚合本地任务 / 工具 / 模型活动;当前不包含屏幕与应用级录制', + reason: 'activity_recorder_partial', }, requiredPermissions: [ { id: 'screen_recording', required: false, status: permissions.screen_recording.status }, @@ -90,7 +91,7 @@ export function buildCapabilitySnapshotCollection(input: { runtimeProbe: { state: 'not_run', source: 'runtime_probe', - reason: '打开 Daily Review 可查看本地活动聚合结果', + reason: 'activity_recorder_probe_hint', }, }), staticCapability({ @@ -100,7 +101,7 @@ export function buildCapabilitySnapshotCollection(input: { feature: { state: 'partial', source: 'runtime', - reason: '本地 MEMORY.md 已可见;自动抽取/写入仍需用户确认', + reason: 'memory_partial', }, requiredPermissions: [], actionApproval: { state: 'not_required', source: 'not_applicable' }, @@ -108,7 +109,7 @@ export function buildCapabilitySnapshotCollection(input: { runtimeProbe: { state: 'not_run', source: 'runtime_probe', - reason: '透明本地记忆为文件读写能力,不做后台探测', + reason: 'memory_no_probe', }, }), ...BOT_PROVIDERS.map((provider) => @@ -138,7 +139,7 @@ function computerUseCapability( feature: { state: artifactAvailable ? 'enabled' : 'not_available', source: 'runtime', - reason: computerUseCapabilityReason(input, permissions), + reason: computerUseCapabilityReason(input), }, requiredPermissions: [ { id: 'accessibility', required: true, status: permissions.accessibility.status }, @@ -153,45 +154,24 @@ function computerUseCapability( state: input?.health.state ?? 'not_available', source: 'runtime_probe', lastCheckedAt: now, - reason: input?.health.reason ?? 'Computer Use 后端当前不可用。', + reason: input?.health.reason ?? 'cu_backend_unavailable', }, }); } +// The presenter composes the full 'cu_backend_status' sentence from data the +// same snapshot already carries (required-permission statuses and the runtime +// probe state), so the reason stays a bare code. function computerUseCapabilityReason( input: { backendId: CuBackendId | 'none'; health: ReturnType; } | undefined, - permissions: PermissionSnapshot['permissions'], ): string { if (input === undefined || input.backendId === 'none') { - return '未找到通过完整性检查的 Computer Use 执行器 artifact。'; - } - - const reasons = [`${input.backendId} artifact 已通过本地完整性检查。`]; - const missingPermissions = [ - ['辅助功能', permissions.accessibility.status], - ['屏幕录制', permissions.screen_recording.status], - ].filter((entry) => entry[1] !== 'granted').map((entry) => entry[0]); - if (missingPermissions.length > 0) { - reasons.push(`等待${missingPermissions.join('、')}权限。`); + return 'cu_artifact_missing'; } - switch (input.health.state) { - case 'not_available': - reasons.push(`${input.backendId} service 启动失败、已退出或已停止。`); - break; - case 'degraded': - reasons.push(`${input.backendId} service 正在启动或恢复。`); - break; - case 'healthy': - reasons.push('操作与截图 service 已就绪;按目标与动作类别授权后可操作本机应用。'); - break; - case 'not_run': - reasons.push('service 将在首次调用时启动;按目标与动作类别授权后可操作本机应用。'); - break; - } - return reasons.join(''); + return 'cu_backend_status'; } function staticCapability(input: { @@ -243,7 +223,7 @@ function botCapability( }; const configuration: CapabilityConfigurationSignal = hasConfig ? { state: 'present', source: 'settings' } - : { state: 'missing', source: 'settings', reason: '未配置平台凭据' }; + : { state: 'missing', source: 'settings', reason: 'missing platform credentials' }; const runtimeProbe = runtimeProbeFromBotReadiness( status.readiness, channel.readinessUpdatedAt, @@ -274,7 +254,7 @@ function botCapability( } function accessibilitySnapshot(now: number, platform: NodeJS.Platform): OsPermissionSnapshot { - if (platform !== 'darwin') return unsupportedPermission('accessibility', now, '仅 macOS TCC 权限适用'); + if (platform !== 'darwin') return unsupportedPermission('accessibility', now, 'macOS TCC only'); try { const granted = systemPreferences.isTrustedAccessibilityClient(false); return { @@ -282,12 +262,12 @@ function accessibilitySnapshot(now: number, platform: NodeJS.Platform): OsPermis status: granted ? 'granted' : 'not_determined', source: 'electron', checkedAt: now, - reason: granted ? undefined : 'macOS 不区分辅助功能权限是未授权还是未申请', + reason: granted ? undefined : 'accessibility_status_ambiguous', canOpenSettings: true, canRequest: false, }; } catch (error) { - return unknownPermission('accessibility', now, generalizedReason(error), true); + return unknownPermission('accessibility', now, error, true); } } @@ -298,11 +278,7 @@ function mediaPermissionSnapshot( platform: NodeJS.Platform, ): OsPermissionSnapshot { if (!supportsMediaPermissionProbe(id, platform)) { - return unsupportedPermission( - id, - now, - '屏幕录制权限状态仅能在 macOS 上读取', - ); + return unsupportedPermission(id, now, 'screen_recording_status_mac_only'); } try { const status = mapMediaAccessStatus(systemPreferences.getMediaAccessStatus(mediaType)); @@ -315,7 +291,7 @@ function mediaPermissionSnapshot( ...actions, }; } catch (error) { - return unknownPermission(id, now, generalizedReason(error), platform === 'darwin'); + return unknownPermission(id, now, error, platform === 'darwin'); } } @@ -328,9 +304,9 @@ function notificationSnapshot(now: number, platform: NodeJS.Platform): OsPermiss checkedAt: now, reason: supported ? platform === 'darwin' - ? 'Electron 无法可靠读取 macOS 通知授权状态,请在系统设置中确认' - : 'Electron 无法可靠读取当前系统的通知授权状态' - : 'Electron 通知能力不可用', + ? 'notifications_status_unreadable_macos' + : 'notifications_status_unreadable' + : 'notifications_unsupported', canOpenSettings: platform === 'darwin', // Showing a Notification is not an authorization API and does not report // whether macOS delivered or suppressed it. Never present that probe as a @@ -340,19 +316,23 @@ function notificationSnapshot(now: number, platform: NodeJS.Platform): OsPermiss } function automationSnapshot(now: number, platform: NodeJS.Platform): OsPermissionSnapshot { - if (platform !== 'darwin') return unsupportedPermission('automation', now, '仅 macOS TCC 权限适用'); + if (platform !== 'darwin') return unsupportedPermission('automation', now, 'macOS TCC only'); return { id: 'automation', status: 'unknown', source: 'static', checkedAt: now, - reason: 'Electron 暂不支持读取逐 App 的 Apple Events 授权状态', + reason: 'no Electron API for per-target Apple Events TCC status', canOpenSettings: true, canRequest: false, }; } -function unsupportedPermission(id: OsPermissionId, now: number, reason: string): OsPermissionSnapshot { +function unsupportedPermission( + id: OsPermissionId, + now: number, + reason: CapabilityReasonCode, +): OsPermissionSnapshot { return { id, status: 'unsupported', @@ -367,7 +347,7 @@ function unsupportedPermission(id: OsPermissionId, now: number, reason: string): function unknownPermission( id: OsPermissionId, now: number, - reason: string, + error: unknown, canOpenSettings: boolean, ): OsPermissionSnapshot { return { @@ -375,12 +355,11 @@ function unknownPermission( status: 'unknown', source: 'electron', checkedAt: now, - reason, + reason: 'permission_probe_failed', + // Raw probe error text passes through verbatim as diagnostic detail; the + // presenter renders it beside the localized 'permission_probe_failed' copy. + ...(error instanceof Error && error.message ? { detail: error.message } : {}), canOpenSettings, canRequest: false, }; } - -function generalizedReason(error: unknown): string { - return error instanceof Error ? error.message : 'permission probe failed'; -} diff --git a/apps/desktop/src/main/computer-use-host.ts b/apps/desktop/src/main/computer-use-host.ts index 5b5c964b74..cb72bb5583 100644 --- a/apps/desktop/src/main/computer-use-host.ts +++ b/apps/desktop/src/main/computer-use-host.ts @@ -150,22 +150,19 @@ export function computerUseServiceHealth( reason: string; } { if (backendId === 'none' || !state) { - return { - state: 'not_available', - reason: '未找到通过完整性检查且可分发的 maka-cu executor。', - }; + return { state: 'not_available', reason: 'cu_executor_undistributable' }; } switch (state.state) { case 'disposed': - return { state: 'not_available', reason: 'maka-cu executor 已停止。' }; + return { state: 'not_available', reason: 'cu_executor_stopped' }; case 'unavailable': - return { state: 'not_available', reason: 'maka-cu executor 启动失败或已退出。' }; + return { state: 'not_available', reason: 'cu_executor_start_failed' }; case 'starting': case 'backing_off': - return { state: 'degraded', reason: 'maka-cu executor 正在启动或恢复。' }; + return { state: 'degraded', reason: 'cu_executor_recovering' }; case 'ready': - return { state: 'healthy', reason: 'maka-cu executor 已就绪。' }; + return { state: 'healthy', reason: 'cu_executor_ready' }; default: - return { state: 'not_run', reason: 'maka-cu 已可用,将在首次调用时启动。' }; + return { state: 'not_run', reason: 'cu_executor_lazy_start' }; } } diff --git a/apps/desktop/src/main/main.ts b/apps/desktop/src/main/main.ts index 16133b4515..0e5559741a 100644 --- a/apps/desktop/src/main/main.ts +++ b/apps/desktop/src/main/main.ts @@ -17,7 +17,7 @@ * under the License. */ -import { resolveSystemUiLocale } from '@maka/core/ui-locale'; +import { resolveSystemUiLocale, type UiCatalog } from '@maka/core/ui-locale'; import { DEV_LOSER_EXIT_CODE, developmentLaunchResultFile, @@ -105,18 +105,14 @@ if (!app.requestSingleInstanceLock()) { .whenReady() .then(() => { const locale = resolveSystemUiLocale(app.getPreferredSystemLanguages()); - const isChinese = locale === 'zh'; + const copy = DEV_SINGLETON_COPY[locale]; return showBrowserMessageBox( { type: 'warning', - title: isChinese ? 'Maka Dev 已在运行' : 'Maka Dev is already running', - message: isChinese - ? '另一个 Maka Dev 实例正在使用此开发配置。' - : 'Another Maka Dev instance is using this development profile.', - detail: isChinese - ? `开发配置:${profilePath}\n\n请先退出正在运行的实例,然后重试。` - : `Development profile: ${profilePath}\n\nQuit the running instance, then retry.`, - buttons: [isChinese ? '退出' : 'Exit'], + title: copy.title, + message: copy.message, + detail: copy.detail(profilePath), + buttons: [copy.exit], defaultId: 0, cancelId: 0, }, @@ -237,3 +233,23 @@ if (!app.requestSingleInstanceLock()) { } }); } + +const DEV_SINGLETON_COPY = { + zh: { + title: 'Maka Dev 已在运行', + message: '另一个 Maka Dev 实例正在使用此开发配置。', + detail: (profilePath: string) => `开发配置:${profilePath}\n\n请先退出正在运行的实例,然后重试。`, + exit: '退出', + }, + en: { + title: 'Maka Dev is already running', + message: 'Another Maka Dev instance is using this development profile.', + detail: (profilePath: string) => `Development profile: ${profilePath}\n\nQuit the running instance, then retry.`, + exit: 'Exit', + }, +} satisfies UiCatalog<{ + title: string; + message: string; + detail(profilePath: string): string; + exit: string; +}>; diff --git a/apps/desktop/src/main/oauth/github-copilot-local-credential.ts b/apps/desktop/src/main/oauth/github-copilot-local-credential.ts index 2a39fed564..21bcc4854a 100644 --- a/apps/desktop/src/main/oauth/github-copilot-local-credential.ts +++ b/apps/desktop/src/main/oauth/github-copilot-local-credential.ts @@ -56,8 +56,8 @@ export async function importGitHubCopilotLocalCredential( result: { ok: false, reason: 'token_exchange_failed', - message: - 'GitHub Copilot 不支持 classic PAT;请使用兼容 OAuth 登录或具有 Copilot Requests 权限的 fine-grained PAT。', + code: 'copilot_classic_pat_unsupported', + message: 'GitHub Copilot does not accept classic PATs.', }, }; } @@ -66,7 +66,8 @@ export async function importGitHubCopilotLocalCredential( result: { ok: false, reason: 'token_exchange_failed', - message: '当前 GitHub 凭据类型不受支持;请使用兼容 OAuth 登录或 fine-grained PAT。', + code: 'copilot_credential_type_unsupported', + message: 'Unsupported GitHub credential type.', }, }; } @@ -79,7 +80,8 @@ export async function importGitHubCopilotLocalCredential( result: { ok: false, reason: 'token_exchange_failed', - message: '未找到可导入的 GitHub 凭据;请先使用 gh 登录或配置兼容凭据。', + code: 'copilot_local_credential_missing', + message: 'No importable GitHub credential found.', }, }; } diff --git a/apps/desktop/src/main/runtime-host-artifacts-ipc-main.ts b/apps/desktop/src/main/runtime-host-artifacts-ipc-main.ts index 4032293ef4..b2906e1bc8 100644 --- a/apps/desktop/src/main/runtime-host-artifacts-ipc-main.ts +++ b/apps/desktop/src/main/runtime-host-artifacts-ipc-main.ts @@ -17,6 +17,7 @@ * under the License. */ +import type { UiCatalog, UiLocale } from '@maka/core/ui-locale'; import { randomUUID } from "node:crypto"; import { open, mkdir, rename, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; @@ -36,6 +37,7 @@ import type { createMainWindowController } from "./main-window.js"; import type { DesktopRuntimeHostClient } from "./runtime-host-client.js"; interface RuntimeHostArtifactsIpcDeps { + uiLocale(): UiLocale; readonly ipcMain: ReconnectableReadIpcMain; readonly client: DesktopRuntimeHostClient; readonly mainWindowController: ReturnType; @@ -130,7 +132,7 @@ export function registerRuntimeHostArtifactsIpc( if (!artifact) return { ok: false, reason: "not_found" }; if (artifact.status === "deleted") return { ok: false, reason: "deleted" }; const result = await deps.mainWindowController.showSaveDialog({ - title: `另存为 ${artifact.name}`, + title: ARTIFACT_DIALOG_COPY[deps.uiLocale()].saveAs(artifact.name), defaultPath: artifact.name, }); if (result.canceled || !result.filePath) { @@ -250,3 +252,8 @@ async function materializeArtifact( throw error; } } + +const ARTIFACT_DIALOG_COPY = { + zh: { saveAs: (name: string) => `另存为 ${name}` }, + en: { saveAs: (name: string) => `Save ${name} as` }, +} satisfies UiCatalog<{ saveAs(name: string): string }>; diff --git a/apps/desktop/src/main/runtime-host-boot.ts b/apps/desktop/src/main/runtime-host-boot.ts index 74e68af3ee..a314ad134b 100644 --- a/apps/desktop/src/main/runtime-host-boot.ts +++ b/apps/desktop/src/main/runtime-host-boot.ts @@ -1026,6 +1026,8 @@ const startLocalRuntimeHostManager = () => startRuntimeHostDesktopManager( const chatId = requireScheduledTaskEffectString(input.chatId, "chatId"); const title = requireScheduledTaskEffectString(input.title, "title"); const body = typeof input.body === "string" ? input.body.trim() : ""; + // Bot-channel notices follow the bot audience language, not the + // desktop UI locale; localization tracked under the locale issue. const text = [`【定时任务】${title}`, ...(body ? ["", body] : [])].join("\n"); const sent = await botRegistry.sendMessage(platform, chatId, text); if (!sent) throw new Error("ScheduledTask bot channel is unavailable"); @@ -1437,6 +1439,7 @@ function registerHostClientIpc( }); registerRuntimeHostRendererIpc({ ipcMain: scopedIpc, client }); registerRuntimeHostArtifactsIpc({ + uiLocale: () => desktopLocale.current(), ipcMain: scopedIpc, client, mainWindowController, @@ -1473,6 +1476,7 @@ function registerHostClientIpc( module: runtimeHostSettings, }); registerRuntimeHostConfigIpc({ + uiLocale: () => desktopLocale.current(), ipcMain: scopedIpc, client, mainWindowController, diff --git a/apps/desktop/src/main/runtime-host-config-ipc-main.ts b/apps/desktop/src/main/runtime-host-config-ipc-main.ts index 073283c34b..055a0d4342 100644 --- a/apps/desktop/src/main/runtime-host-config-ipc-main.ts +++ b/apps/desktop/src/main/runtime-host-config-ipc-main.ts @@ -17,6 +17,7 @@ * under the License. */ +import type { UiCatalog, UiLocale } from '@maka/core/ui-locale'; import { readFile, writeFile } from 'node:fs/promises'; import { isDeepStrictEqual } from 'node:util'; import type { IpcMain } from 'electron'; @@ -67,6 +68,7 @@ import { } from '@maka/storage/config-transfer'; interface RuntimeHostConfigIpcDeps { + uiLocale(): UiLocale; readonly ipcMain: Pick; readonly client: DesktopRuntimeHostClient; readonly mainWindowController: ReturnType; @@ -100,7 +102,7 @@ export function registerRuntimeHostConfigIpc( } const today = new Date().toISOString().slice(0, 10); const result = await deps.mainWindowController.showSaveDialog({ - title: '导出 Maka 配置', + title: CONFIG_DIALOG_COPY[deps.uiLocale()].exportTitle, defaultPath: `maka-config-${today}.json`, filters: [{ name: 'Maka Config', extensions: ['json'] }], }); @@ -129,7 +131,7 @@ export function registerRuntimeHostConfigIpc( 'config:import', async (_event, input: { strategy?: unknown } = {}) => { const result = await deps.mainWindowController.showOpenDialog({ - title: '导入 Maka 配置', + title: CONFIG_DIALOG_COPY[deps.uiLocale()].importTitle, properties: ['openFile'], filters: [{ name: 'Maka Config', extensions: ['json'] }], }); @@ -656,3 +658,8 @@ function sanitizeCategories(value: unknown): ConfigCategory[] { function sanitizeStrategy(value: unknown): ConnectionConflictStrategy { return value === 'overwrite' ? 'overwrite' : 'skip'; } + +const CONFIG_DIALOG_COPY = { + zh: { exportTitle: '导出 Maka 配置', importTitle: '导入 Maka 配置' }, + en: { exportTitle: 'Export Maka configuration', importTitle: 'Import Maka configuration' }, +} satisfies UiCatalog<{ exportTitle: string; importTitle: string }>; diff --git a/apps/desktop/src/main/runtime-host-github-copilot-ipc-main.ts b/apps/desktop/src/main/runtime-host-github-copilot-ipc-main.ts index d463637e23..45de4a1174 100644 --- a/apps/desktop/src/main/runtime-host-github-copilot-ipc-main.ts +++ b/apps/desktop/src/main/runtime-host-github-copilot-ipc-main.ts @@ -53,7 +53,7 @@ export function registerRuntimeHostGitHubCopilotIpc(deps: RuntimeHostGitHubCopil deps.ipcMain.handle('github-copilot:connect-existing-login', async () => { const imported = await importExistingLogin(); if (!imported.result.ok) return imported.result; - if (!imported.secret) return storageFailure('GitHub Copilot login produced no credential'); + if (!imported.secret) return storageFailure('copilot_import_no_credential'); try { const before = await deps.client.loadConnectionCatalog(); @@ -69,23 +69,23 @@ export function registerRuntimeHostGitHubCopilotIpc(deps: RuntimeHostGitHubCopil }); if (adopted.kind === 'rejected') { if (adopted.reason === 'superseded') { - return storageFailure('GitHub Copilot 账号在导入期间发生变化,请重试。'); + return storageFailure('copilot_import_superseded'); } return adopted.reason === 'model_unavailable' - ? actionFailure('当前 GitHub 账号没有可用的 Copilot 订阅权限。') - : actionFailure('当前 GitHub 凭据无法导入,请检查凭据后重试。'); + ? actionFailure('copilot_subscription_unavailable') + : actionFailure('copilot_credential_import_rejected'); } if (adopted.kind === 'failed') { return adopted.errorClass === 'auth' - ? actionFailure('当前 GitHub 账号没有可用的 Copilot 订阅权限。') - : actionFailure('暂时无法验证 GitHub Copilot 订阅状态,请稍后重试。'); + ? actionFailure('copilot_subscription_unavailable') + : actionFailure('copilot_subscription_check_failed'); } await selectAccountDefaultIfMissing(deps.client, adopted.connection.connectionId); deps.emitConnectionListChanged(); return { ok: true as const }; } catch { - return storageFailure('GitHub Copilot login could not be committed to Runtime Host'); + return storageFailure('copilot_import_commit_failed'); } }); } @@ -108,10 +108,10 @@ async function selectAccountDefaultIfMissing( } } -function actionFailure(message: string) { - return { ok: false as const, reason: 'token_exchange_failed' as const, message }; +function actionFailure(code: string) { + return { ok: false as const, reason: 'token_exchange_failed' as const, code, message: code }; } -function storageFailure(message: string) { - return { ok: false as const, reason: 'storage_failed' as const, message }; +function storageFailure(code: string) { + return { ok: false as const, reason: 'storage_failed' as const, code, message: code }; } diff --git a/apps/desktop/src/main/runtime-host-memory-ipc-main.ts b/apps/desktop/src/main/runtime-host-memory-ipc-main.ts index 1004f497e3..2474cfa9f7 100644 --- a/apps/desktop/src/main/runtime-host-memory-ipc-main.ts +++ b/apps/desktop/src/main/runtime-host-memory-ipc-main.ts @@ -54,6 +54,7 @@ type LocalMemoryMutationResult = readonly ok: false; readonly state: LocalMemoryState; readonly reason: string; + readonly code: string; readonly message: string; }; @@ -99,6 +100,7 @@ export function registerRuntimeHostMemoryIpc( return { ok: false as const, state, + code: "no_backup", message: "No Memory backup is available", }; return restoreBackup(deps, backup.kind); @@ -108,6 +110,7 @@ export function registerRuntimeHostMemoryIpc( return { ok: false as const, state: await getMemoryState(deps), + code: "invalid_backup_kind", message: "Invalid Memory backup kind", }; } @@ -140,12 +143,20 @@ export function registerRuntimeHostMemoryIpc( deps, BACKUP_FILES[state.latestBackup.kind], ) - : { ok: false as const, message: "No Memory backup is available" }; + : { + ok: false as const, + code: "no_backup", + message: "No Memory backup is available", + }; }); deps.ipcMain.handle("memory:openBackup", async (_event, kind: unknown) => { return isBackupKind(kind) ? openMemoryPath(deps, BACKUP_FILES[kind]) - : { ok: false as const, message: "Invalid Memory backup kind" }; + : { + ok: false as const, + code: "invalid_backup_kind", + message: "Invalid Memory backup kind", + }; }); } @@ -479,13 +490,14 @@ async function restoreBackup( kind: MemoryBackupKind, ): Promise< | { ok: true; state: LocalMemoryState } - | { ok: false; state: LocalMemoryState; message: string } + | { ok: false; state: LocalMemoryState; code: string; message: string } > { const state = await deps.client.queryMemory({ kind: "state" }); if (state.kind !== "state") { return { ok: false, state: await getMemoryState(deps), + code: "memory_unavailable", message: "Memory is unavailable", }; } @@ -494,6 +506,7 @@ async function restoreBackup( return { ok: false, state: await getMemoryState(deps), + code: "backup_not_found", message: "Memory backup not found", }; } @@ -505,7 +518,7 @@ async function restoreBackup( })); return result.ok ? { ok: true, state: result.state } - : { ok: false, state: result.state, message: result.message }; + : { ok: false, state: result.state, code: result.code, message: result.message }; } async function openMemoryPath( @@ -514,10 +527,11 @@ async function openMemoryPath( "workspaceRoot" | "allowLocalPaths" | "openPath" >, fileName: string, -): Promise<{ ok: true } | { ok: false; message: string }> { +): Promise<{ ok: true } | { ok: false; code: string; message: string }> { if (deps.allowLocalPaths === false) { return { ok: false, + code: "remote_host_owned", message: "Memory files are owned by the remote Runtime Host", }; } @@ -529,15 +543,20 @@ async function openMemoryPath( if (!isPathInside(directory, path) || !(await lstat(path)).isFile()) { return { ok: false, + code: "not_regular_file", message: "Memory path is not an allowed regular file", }; } const error = await deps.openPath(path); return error - ? { ok: false, message: "The system could not open the Memory file" } + ? { + ok: false, + code: "open_failed", + message: "The system could not open the Memory file", + } : { ok: true }; } catch { - return { ok: false, message: "Memory file not found" }; + return { ok: false, code: "file_not_found", message: "Memory file not found" }; } } @@ -600,6 +619,7 @@ async function mutationFailure( ok: false, state: await getMemoryState(deps), reason, + code: reason, message: memoryReasonMessage(reason), }; } diff --git a/apps/desktop/src/preload/bridge-contract.d.ts b/apps/desktop/src/preload/bridge-contract.d.ts index d68e5c75c2..8356ef06e0 100644 --- a/apps/desktop/src/preload/bridge-contract.d.ts +++ b/apps/desktop/src/preload/bridge-contract.d.ts @@ -1511,13 +1511,13 @@ export interface MakaBridge { getState(sessionId?: string, host?: DesktopRuntimeHostRef): Promise; save(content: string, host?: DesktopRuntimeHostRef): Promise; reset(host?: DesktopRuntimeHostRef): Promise; - restoreLatestBackup(host?: DesktopRuntimeHostRef): Promise<{ ok: true; state: LocalMemoryState } | { ok: false; state: LocalMemoryState; message: string }>; - restoreBackup(kind: 'save' | 'reset' | 'restore', host?: DesktopRuntimeHostRef): Promise<{ ok: true; state: LocalMemoryState } | { ok: false; state: LocalMemoryState; message: string }>; + restoreLatestBackup(host?: DesktopRuntimeHostRef): Promise<{ ok: true; state: LocalMemoryState } | { ok: false; state: LocalMemoryState; code: string; message: string }>; + restoreBackup(kind: 'save' | 'reset' | 'restore', host?: DesktopRuntimeHostRef): Promise<{ ok: true; state: LocalMemoryState } | { ok: false; state: LocalMemoryState; code: string; message: string }>; setEnabled(enabled: boolean, host?: DesktopRuntimeHostRef): Promise; setAgentReadEnabled(enabled: boolean, host?: DesktopRuntimeHostRef): Promise; - openFile(host?: DesktopRuntimeHostRef): Promise<{ ok: true } | { ok: false; message: string }>; - openLatestBackup(host?: DesktopRuntimeHostRef): Promise<{ ok: true } | { ok: false; message: string }>; - openBackup(kind: 'save' | 'reset' | 'restore', host?: DesktopRuntimeHostRef): Promise<{ ok: true } | { ok: false; message: string }>; + openFile(host?: DesktopRuntimeHostRef): Promise<{ ok: true } | { ok: false; code: string; message: string }>; + openLatestBackup(host?: DesktopRuntimeHostRef): Promise<{ ok: true } | { ok: false; code: string; message: string }>; + openBackup(kind: 'save' | 'reset' | 'restore', host?: DesktopRuntimeHostRef): Promise<{ ok: true } | { ok: false; code: string; message: string }>; }; attachments: { pickDirectory(): Promise<{ ok: true; reference: import('@maka/core/events').DirectoryReference } | { ok: false; reason: 'cancelled' }>; diff --git a/apps/desktop/src/preload/preload.ts b/apps/desktop/src/preload/preload.ts index b4013452f4..fcd12da409 100644 --- a/apps/desktop/src/preload/preload.ts +++ b/apps/desktop/src/preload/preload.ts @@ -2846,10 +2846,10 @@ const makaBridge = { reset(host?: DesktopRuntimeHostRef): Promise { return invokeSelectedRuntimeHost(host, 'memory:reset'); }, - restoreLatestBackup(host?: DesktopRuntimeHostRef): Promise<{ ok: true; state: LocalMemoryState } | { ok: false; state: LocalMemoryState; message: string }> { + restoreLatestBackup(host?: DesktopRuntimeHostRef): Promise<{ ok: true; state: LocalMemoryState } | { ok: false; state: LocalMemoryState; code: string; message: string }> { return invokeSelectedRuntimeHost(host, 'memory:restoreLatestBackup'); }, - restoreBackup(kind: 'save' | 'reset' | 'restore', host?: DesktopRuntimeHostRef): Promise<{ ok: true; state: LocalMemoryState } | { ok: false; state: LocalMemoryState; message: string }> { + restoreBackup(kind: 'save' | 'reset' | 'restore', host?: DesktopRuntimeHostRef): Promise<{ ok: true; state: LocalMemoryState } | { ok: false; state: LocalMemoryState; code: string; message: string }> { return invokeSelectedRuntimeHost(host, 'memory:restoreBackup', kind); }, setEnabled(enabled: boolean, host?: DesktopRuntimeHostRef): Promise { @@ -2858,13 +2858,13 @@ const makaBridge = { setAgentReadEnabled(enabled: boolean, host?: DesktopRuntimeHostRef): Promise { return invokeSelectedRuntimeHost(host, 'memory:setAgentReadEnabled', enabled); }, - openFile(host?: DesktopRuntimeHostRef): Promise<{ ok: true } | { ok: false; message: string }> { + openFile(host?: DesktopRuntimeHostRef): Promise<{ ok: true } | { ok: false; code: string; message: string }> { return invokeSelectedRuntimeHost(host, 'memory:openFile'); }, - openLatestBackup(host?: DesktopRuntimeHostRef): Promise<{ ok: true } | { ok: false; message: string }> { + openLatestBackup(host?: DesktopRuntimeHostRef): Promise<{ ok: true } | { ok: false; code: string; message: string }> { return invokeSelectedRuntimeHost(host, 'memory:openLatestBackup'); }, - openBackup(kind: 'save' | 'reset' | 'restore', host?: DesktopRuntimeHostRef): Promise<{ ok: true } | { ok: false; message: string }> { + openBackup(kind: 'save' | 'reset' | 'restore', host?: DesktopRuntimeHostRef): Promise<{ ok: true } | { ok: false; code: string; message: string }> { return invokeSelectedRuntimeHost(host, 'memory:openBackup', kind); }, }, diff --git a/apps/desktop/src/renderer/features/connection-settings/index.ts b/apps/desktop/src/renderer/features/connection-settings/index.ts index fa93449442..2e468737b7 100644 --- a/apps/desktop/src/renderer/features/connection-settings/index.ts +++ b/apps/desktop/src/renderer/features/connection-settings/index.ts @@ -37,7 +37,7 @@ export { connectionTestFailureMessage, providerPanelActionErrorMessage, } from './provider-panel-shared.js'; -export { getProviderSettingsCopy } from './settings-provider-copy.js'; +export { getProviderSettingsCopy, subscriptionActionErrorMessage, subscriptionResultMessage } from './settings-provider-copy.js'; export type { ProviderSettingsCopy } from './settings-provider-copy.js'; export type { CredentialPresenceStatus, diff --git a/apps/desktop/src/renderer/features/connection-settings/provider-panel-shared.ts b/apps/desktop/src/renderer/features/connection-settings/provider-panel-shared.ts index 92dc3a9231..cca6f46a13 100644 --- a/apps/desktop/src/renderer/features/connection-settings/provider-panel-shared.ts +++ b/apps/desktop/src/renderer/features/connection-settings/provider-panel-shared.ts @@ -67,6 +67,7 @@ export function connectionTestFailureFallback( locale: UiLocale = 'zh', ): string { const shared = getProviderSettingsCopy(locale).shared; + if (result.errorCode === 'oauth_rate_limited') return shared.oauthRateLimit; if (result.statusCode === 429) return shared.rateLimit; if (result.errorClass === 'timeout') return shared.timeout; if (result.errorClass === 'auth' || result.statusCode === 401 || result.statusCode === 403) { @@ -85,7 +86,9 @@ export function connectionTestFailureMessage( locale: UiLocale = 'zh', ): string { const fallback = connectionTestFailureFallback(result, copy, locale); - if (!result.errorMessage) return fallback; + // A coded result already resolved to specific per-locale copy above; the + // errorMessage is only its machine-readable twin. + if (result.errorCode || !result.errorMessage) return fallback; return locale === 'zh' ? generalizedErrorMessageChinese(new Error(result.errorMessage), fallback) : generalizedErrorMessage(new Error(result.errorMessage), fallback); diff --git a/apps/desktop/src/renderer/features/connection-settings/settings-provider-copy.ts b/apps/desktop/src/renderer/features/connection-settings/settings-provider-copy.ts index ad35d5a432..7ce00a875e 100644 --- a/apps/desktop/src/renderer/features/connection-settings/settings-provider-copy.ts +++ b/apps/desktop/src/renderer/features/connection-settings/settings-provider-copy.ts @@ -17,6 +17,7 @@ * under the License. */ +import { generalizedErrorMessage, generalizedErrorMessageChinese, redactSecrets } from '@maka/core/redaction'; import type { UiCatalog, UiLocale } from '@maka/core/ui-locale'; type WidenCopy = T extends string @@ -157,7 +158,7 @@ const zhCopy = { modelKeyAria: (name: string) => `${name} 模型密钥`, }, shared: { - actionFallback: '模型连接服务暂时不可用,请稍后重试。', rateLimit: '当前账号或模型服务触发速率限制,请稍后重试。', + actionFallback: '模型连接服务暂时不可用,请稍后重试。', rateLimit: '当前账号或模型服务触发速率限制,请稍后重试。', oauthRateLimit: 'OAuth 已登录,但当前账号或 provider 正在 rate limit。请稍后重试,或先切换到其它可用模型。', timeout: '请求超时,请检查网络或代理后重试。', unavailable: '模型服务暂时不可用,请稍后重试。', network: '网络错误,请检查服务地址或代理设置后重试。', statusUnavailable: '连接测试状态暂时无法显示,请重新测试。', categories: { oauth: 'OAuth', domestic: '国内', overseas: '海外', local: '本地', custom: 'Custom' }, @@ -216,6 +217,20 @@ const zhCopy = { loggedOut: '已退出登录', credentialsCleared: '本地凭据已清除。', logoutFailed: '退出失败', logoutFailedRetry: '退出登录失败,请稍后重试。', serviceUnavailable: '登录服务暂时不可用,请检查网络后重试。', logoutTitle: (name: string) => `退出 ${name} 登录?`, + loginConflict: '上一轮浏览器登录仍在进行或已切换,请再点一次登录,或稍后再试。', + browserPresentFailed: '无法打开系统浏览器完成登录,请检查是否拦截了弹窗后重试。', + resultCodes: { + copilot_classic_pat_unsupported: 'GitHub Copilot 不支持 classic PAT;请使用兼容 OAuth 登录或具有 Copilot Requests 权限的 fine-grained PAT。', + copilot_credential_type_unsupported: '当前 GitHub 凭据类型不受支持;请使用兼容 OAuth 登录或 fine-grained PAT。', + copilot_local_credential_missing: '未找到可导入的 GitHub 凭据;请先使用 gh 登录或配置兼容凭据。', + copilot_import_no_credential: 'GitHub Copilot 登录没有产生可用凭据。', + copilot_import_superseded: 'GitHub Copilot 账号在导入期间发生变化,请重试。', + copilot_subscription_unavailable: '当前 GitHub 账号没有可用的 Copilot 订阅权限。', + copilot_credential_import_rejected: '当前 GitHub 凭据无法导入,请检查凭据后重试。', + copilot_subscription_check_failed: '暂时无法验证 GitHub Copilot 订阅状态,请稍后重试。', + copilot_import_commit_failed: 'GitHub Copilot 登录未能写入 Runtime Host。', + experimental_disabled: '本机未启用该账号登录方式;可改用导入兼容凭据,或由管理员启用后重试。', + }, }, oauthSection: { signedIn: '已登录', codexDescription: '使用 ChatGPT Plus / Pro 账号添加连接。', xaiDescription: '使用 SuperGrok / X Premium 账号添加连接。', @@ -317,7 +332,7 @@ const enCopy: ProviderSettingsCopy = { modelKeyAria: (name: string) => `${name} model key`, }, shared: { - actionFallback: 'The model connection service is temporarily unavailable. Try again later.', rateLimit: 'This account or model service is rate-limited. Try again later.', + actionFallback: 'The model connection service is temporarily unavailable. Try again later.', rateLimit: 'This account or model service is rate-limited. Try again later.', oauthRateLimit: 'Signed in, but the account or provider is currently rate limited. Retry later or switch to another available model.', timeout: 'The request timed out. Check the network or proxy and try again.', unavailable: 'The model service is temporarily unavailable. Try again later.', network: 'Network error. Check the service URL or proxy settings and try again.', statusUnavailable: 'The connection test status is temporarily unavailable. Test again.', categories: { oauth: 'OAuth', domestic: 'China', overseas: 'Global', local: 'Local', custom: 'Custom' }, @@ -376,6 +391,20 @@ const enCopy: ProviderSettingsCopy = { loggedOut: 'Signed out', credentialsCleared: 'Local credentials cleared.', logoutFailed: 'Sign-out failed', logoutFailedRetry: 'Sign-out failed. Try again later.', serviceUnavailable: 'The sign-in service is temporarily unavailable. Check the network and try again.', logoutTitle: (name: string) => `Sign out of ${name}?`, + loginConflict: 'A previous browser login is still running or was superseded. Try logging in again shortly.', + browserPresentFailed: 'Could not open the system browser for login. Check popup blockers and try again.', + resultCodes: { + copilot_classic_pat_unsupported: 'GitHub Copilot does not accept classic PATs. Use a compatible OAuth login or a fine-grained PAT with the Copilot Requests permission.', + copilot_credential_type_unsupported: 'This GitHub credential type is not supported. Use a compatible OAuth login or a fine-grained PAT.', + copilot_local_credential_missing: 'No importable GitHub credential was found. Sign in with gh or configure a compatible credential first.', + copilot_import_no_credential: 'The GitHub Copilot login produced no usable credential.', + copilot_import_superseded: 'The GitHub Copilot account changed during import. Try again.', + copilot_subscription_unavailable: 'This GitHub account has no usable Copilot subscription.', + copilot_credential_import_rejected: 'This GitHub credential could not be imported. Check it and try again.', + copilot_subscription_check_failed: 'Could not verify the GitHub Copilot subscription right now. Try again later.', + copilot_import_commit_failed: 'The GitHub Copilot login could not be committed to Runtime Host.', + experimental_disabled: 'This sign-in is not enabled on this install. Import a compatible credential instead, or ask an operator to enable it.', + }, }, oauthSection: { signedIn: 'Signed in', codexDescription: 'Use a ChatGPT Plus / Pro account to add a connection.', xaiDescription: 'Use a SuperGrok or X Premium account to add a connection.', @@ -402,3 +431,36 @@ const PROVIDER_SETTINGS_COPY = { zh: zhCopy, en: enCopy } satisfies UiCatalog>; + const mapped = (code && codes[code]) || (reason && codes[reason]); + if (mapped) return mapped; + const raw = redactSecrets(message ?? '').trim(); + if (!raw) return fallback; + // Stable Host messages, matched before the coarse keyword classifier turns + // "authorization" into a generic auth failure that does not tell the user what to do. + if (/enrollment is disabled for this provider/i.test(raw)) return codes.experimental_disabled ?? fallback; + if (/already in progress|superseded by a new attempt/i.test(raw)) return copy.loginConflict; + if (/did not present OAuth|no matching OAuth presentation/i.test(raw)) return copy.browserPresentFailed; + const classified = locale === 'zh' + ? generalizedErrorMessageChinese(new Error(raw), '') + : generalizedErrorMessage(new Error(raw), ''); + return classified || fallback; +} diff --git a/apps/desktop/src/renderer/locales/permission-center-copy.ts b/apps/desktop/src/renderer/locales/permission-center-copy.ts index 3a2e4713ea..b843265249 100644 --- a/apps/desktop/src/renderer/locales/permission-center-copy.ts +++ b/apps/desktop/src/renderer/locales/permission-center-copy.ts @@ -20,9 +20,11 @@ import type { StatusSemantic } from '@maka/ui'; import type { CapabilityReadinessState, + CapabilityReasonCode, CapabilitySnapshot, OsPermissionId, OsPermissionState, + RuntimeProbeState, } from '@maka/core/capabilities'; import type { UiCatalog, UiLocale } from '@maka/core/ui-locale'; @@ -94,6 +96,9 @@ export type PermissionCenterCopy = { /** macOS drag-to-grant onboarding (accessibility / screen recording). */ dragGrant: string; dragGranting: string; + reasons: Record, string>; + cuBackendStatus(missingPermissionLabels: readonly string[], health: RuntimeProbeState): string; + reasonFallback: string; }; const PERMISSION_CENTER_COPY = { @@ -143,6 +148,40 @@ const PERMISSION_CENTER_COPY = { requiredPermissions: '所需系统权限', requiredPermissionsAria: (label) => `${label}所需系统权限列表`, guidance: '处理建议', guidanceAria: (label) => `${label}处理建议列表`, auditSection: '审计记录', noAudit: '暂无审计记录', auditAria: (label) => `${label}审计记录列表`, impact: '影响功能', opening: '打开中…', openSettings: '前往系统设置', requesting: '请求中…', request: '请求授权', dragGrant: '引导授权', dragGranting: '引导中…', + reasons: { + disabled: '该能力当前已关闭。', + 'missing platform credentials': '未配置平台凭据', + 'macOS TCC only': '仅 macOS TCC 权限适用', + 'no Electron API for per-target Apple Events TCC status': 'Electron 暂不支持读取逐 App 的 Apple Events 授权状态', + cu_artifact_missing: '未找到通过完整性检查的 Computer Use 执行器 artifact。', + cu_backend_unavailable: 'Computer Use 后端当前不可用。', + cu_executor_undistributable: '未找到通过完整性检查且可分发的 maka-cu executor。', + cu_executor_stopped: 'maka-cu executor 已停止。', + cu_executor_start_failed: 'maka-cu executor 启动失败或已退出。', + cu_executor_recovering: 'maka-cu executor 正在启动或恢复。', + cu_executor_ready: 'maka-cu executor 已就绪。', + cu_executor_lazy_start: 'maka-cu 已可用,将在首次调用时启动。', + activity_recorder_partial: 'Daily Review 已聚合本地任务 / 工具 / 模型活动;当前不包含屏幕与应用级录制', + activity_recorder_probe_hint: '打开 Daily Review 可查看本地活动聚合结果', + memory_partial: '本地 MEMORY.md 已可见;自动抽取/写入仍需用户确认', + memory_no_probe: '透明本地记忆为文件读写能力,不做后台探测', + accessibility_status_ambiguous: 'macOS 不区分辅助功能权限是未授权还是未申请', + screen_recording_status_mac_only: '屏幕录制权限状态仅能在 macOS 上读取', + notifications_status_unreadable_macos: 'Electron 无法可靠读取 macOS 通知授权状态,请在系统设置中确认', + notifications_status_unreadable: 'Electron 无法可靠读取当前系统的通知授权状态', + notifications_unsupported: 'Electron 通知能力不可用', + permission_probe_failed: '权限探测失败', + }, + cuBackendStatus: (missing, health) => + 'maka-cu artifact 已通过本地完整性检查。' + + (missing.length > 0 ? `等待${missing.join('、')}权限。` : '') + + ({ + not_available: 'maka-cu service 启动失败、已退出或已停止。', + degraded: 'maka-cu service 正在启动或恢复。', + healthy: '操作与截图 service 已就绪;按目标与动作类别授权后可操作本机应用。', + not_run: 'service 将在首次调用时启动;按目标与动作类别授权后可操作本机应用。', + } satisfies Record)[health], + reasonFallback: '状态详情请查看运行日志。', }, en: { readiness: { @@ -190,6 +229,40 @@ const PERMISSION_CENTER_COPY = { requiredPermissions: 'Required system permissions', requiredPermissionsAria: (label) => `${label} required system permissions`, guidance: 'Suggested actions', guidanceAria: (label) => `${label} suggested actions`, auditSection: 'Audit records', noAudit: 'No audit records', auditAria: (label) => `${label} audit records`, impact: 'Affects', opening: 'Opening…', openSettings: 'Open System Settings', requesting: 'Requesting…', request: 'Request permission', dragGrant: 'Guide me', dragGranting: 'Opening…', + reasons: { + disabled: 'This capability is turned off.', + 'missing platform credentials': 'Platform credentials are not configured', + 'macOS TCC only': 'Only macOS TCC permissions apply', + 'no Electron API for per-target Apple Events TCC status': 'Electron cannot read per-app Apple Events authorization status', + cu_artifact_missing: 'No Computer Use executor artifact passed the integrity check.', + cu_backend_unavailable: 'The Computer Use backend is currently unavailable.', + cu_executor_undistributable: 'No distributable maka-cu executor passed the integrity check.', + cu_executor_stopped: 'The maka-cu executor has stopped.', + cu_executor_start_failed: 'The maka-cu executor failed to start or has exited.', + cu_executor_recovering: 'The maka-cu executor is starting or recovering.', + cu_executor_ready: 'The maka-cu executor is ready.', + cu_executor_lazy_start: 'maka-cu is available and starts on first use.', + activity_recorder_partial: 'Daily Review aggregates local task, tool, and model activity; screen and app-level recording is not included.', + activity_recorder_probe_hint: 'Open Daily Review to see the local activity summary.', + memory_partial: 'The local MEMORY.md is visible; automatic extraction and writes still require confirmation.', + memory_no_probe: 'Transparent local memory is plain file access, so no background probe runs.', + accessibility_status_ambiguous: 'macOS does not distinguish denied from never-requested Accessibility permission', + screen_recording_status_mac_only: 'Screen Recording permission status can only be read on macOS', + notifications_status_unreadable_macos: 'Electron cannot reliably read macOS notification authorization; check System Settings', + notifications_status_unreadable: 'Electron cannot reliably read notification authorization on this system', + notifications_unsupported: 'Electron notifications are unavailable', + permission_probe_failed: 'Permission probe failed', + }, + cuBackendStatus: (missing, health) => + 'The maka-cu artifact passed the local integrity check. ' + + (missing.length > 0 ? `Waiting for ${missing.join(', ')} permission. ` : '') + + ({ + not_available: 'The maka-cu service failed to start, exited, or was stopped.', + degraded: 'The maka-cu service is starting or recovering.', + healthy: 'The action and screenshot service is ready; grant by target and action category to operate local apps.', + not_run: 'The service starts on first use; grant by target and action category to operate local apps.', + } satisfies Record)[health], + reasonFallback: 'See the runtime logs for details.', }, } satisfies UiCatalog; diff --git a/apps/desktop/src/renderer/locales/settings-data-copy.ts b/apps/desktop/src/renderer/locales/settings-data-copy.ts index 7f38a86812..2ed52b0470 100644 --- a/apps/desktop/src/renderer/locales/settings-data-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-data-copy.ts @@ -29,6 +29,7 @@ export type DataSettingsCopy = { loadFailed: string; openFailed(label: string): string; pathCopied: string; copyFailed: string; copyFailedDetail: string; historyCleared: string; historyClearedDetail: string; selectCategory: string; exported: string; exportedDetail(items: readonly string[]): string; exportFailed: string; noCategories: string; tryAgain: string; imported: string; importFailed: string; invalidFile: string; + importFailures: Record<'not_json' | 'malformed' | 'unsupported_version', string>; rows: { workspace: string; workspaceDetail: string; loadValueFailed: string; loading: string; history: string; historyDetail: string; @@ -56,6 +57,7 @@ const SETTINGS_DATA_COPY = { historyCleared: '已清空输入历史', historyClearedDetail: '已发送的提示词记录已从本机移除。', selectCategory: '请至少选择一个类别', exported: '已导出配置', exportedDetail: (items) => `包含:${items.join('、')}`, exportFailed: '导出失败', noCategories: '未选择任何类别', tryAgain: '请稍后重试', imported: '已导入配置', importFailed: '导入失败', invalidFile: '文件无效或版本不受支持。', + importFailures: { not_json: '文件不是有效的 JSON。', malformed: '配置文件结构无效。', unsupported_version: '配置文件版本不受支持。' }, rows: { workspace: '工作区路径', workspaceDetail: '任务、设置、凭据和技能文件都存在这个目录下。', loadValueFailed: '载入失败', loading: '正在加载…', history: '输入历史', historyDetail: '上箭头 / 下箭头调出的已发送提示词记录,保存在本机、重启后仍在。清空后无法恢复。', @@ -83,6 +85,7 @@ const SETTINGS_DATA_COPY = { historyCleared: 'Input history cleared', historyClearedDetail: 'Sent prompt history was removed from this device.', selectCategory: 'Select at least one category', exported: 'Configuration exported', exportedDetail: (items) => `Included: ${items.join(', ')}`, exportFailed: 'Export failed', noCategories: 'No categories selected', tryAgain: 'Try again later', imported: 'Configuration imported', importFailed: 'Import failed', invalidFile: 'The file is invalid or its version is unsupported.', + importFailures: { not_json: 'The file is not valid JSON.', malformed: 'The config bundle is malformed.', unsupported_version: 'The config file version is unsupported.' }, rows: { workspace: 'Workspace path', workspaceDetail: 'Tasks, settings, credentials, and skill files are stored in this directory.', loadValueFailed: 'Failed to load', loading: 'Loading…', history: 'Input history', historyDetail: 'Previously sent prompts recalled with the Up and Down arrows are kept on this machine and persist across restarts. Clearing them cannot be undone.', diff --git a/apps/desktop/src/renderer/locales/settings-memory-copy.ts b/apps/desktop/src/renderer/locales/settings-memory-copy.ts index 3c8685a733..07f7a7481d 100644 --- a/apps/desktop/src/renderer/locales/settings-memory-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-memory-copy.ts @@ -45,9 +45,16 @@ type MemoryTextKey = | 'entryRestoreFailed' | 'promptCopied' | 'promptCopiedDetail' | 'restoreDraftAction' | 'archiveDraftAction' | 'restoreAction' | 'archiveAction'; +export type MemoryResultCode = + | 'no_backup' | 'invalid_backup_kind' | 'memory_unavailable' | 'backup_not_found' + | 'remote_host_owned' | 'not_regular_file' | 'open_failed' | 'file_not_found' + | 'disabled' | 'incognito_active' | 'safe_mode' | 'oversize' + | 'revision_conflict' | 'backup_revision_conflict' | 'invalid_state'; + export type MemorySettingsCopy = { intlLocale: string; text: Record; + results: Record; origins: Record['origin'], string>; entryStatuses: Record; backupKinds: Record['kind'], string>; @@ -88,16 +95,36 @@ const enText = { const SETTINGS_MEMORY_COPY = { zh: makeCopy('zh-CN', zhText, { + results: { + no_backup: '当前没有可用的 MEMORY.md 备份。', invalid_backup_kind: '备份类型无法识别。', + memory_unavailable: '本地记忆服务当前不可用。', backup_not_found: '找不到对应的备份文件。', + remote_host_owned: '记忆文件由远程 Runtime Host 管理,无法在本机打开。', not_regular_file: '记忆路径不是允许打开的常规文件。', + open_failed: '系统无法打开记忆文件。', file_not_found: '找不到记忆文件。', + disabled: '本地记忆已关闭。', incognito_active: '隐身模式下不可用。', + safe_mode: 'MEMORY.md 内容过大,已进入安全模式。', oversize: 'MEMORY.md 超出安全上限,请先删减旧内容。', + revision_conflict: '记忆内容刚被其他操作修改,请重试。', backup_revision_conflict: '备份内容刚被其他操作修改,请重试。', + invalid_state: 'Runtime Host 返回了无效的记忆状态。', + }, origins: { manual: '手动记录', imported: '导入记录', extracted: '确认提取', unknown: '手写条目' }, entryStatuses: { draft: '草稿', review_required: '待确认', active: '生效', archived: '已归档', rejected: '已拒绝', unknown: '未识别' }, backupKinds: { reset: '重置前备份', restore: '恢复前备份', save: '保存前备份' }, memoryStatuses: { ok: '本地文件已就绪', disabled: '已关闭', safe_mode: '安全模式', incognito_blocked: '隐身禁用', error: '读取失败' }, promptBlocked: { disabled: '本地记忆已关闭。', incognito: '隐身模式下不会提供本地记忆。', safeMode: 'MEMORY.md 过大,当前不会提供。', agentRead: '模型上下文读取未开启。' }, backupOversize: '备份过大,无法预览条目', previewOversize: '草稿过大,条目预览已暂停;保存前请先删减 MEMORY.md 内容。', previewTruncationMarker: '[本地记忆已按长度截断]', }), en: makeCopy('en-US', enText, { + results: { + no_backup: 'No MEMORY.md backup is available.', invalid_backup_kind: 'Unrecognized backup kind.', + memory_unavailable: 'Local memory is currently unavailable.', backup_not_found: 'The backup file was not found.', + remote_host_owned: 'Memory files are owned by the remote Runtime Host and cannot be opened locally.', not_regular_file: 'The memory path is not an allowed regular file.', + open_failed: 'The system could not open the memory file.', file_not_found: 'The memory file was not found.', + disabled: 'Local memory is disabled.', incognito_active: 'Unavailable in incognito mode.', + safe_mode: 'MEMORY.md is too large and entered safe mode.', oversize: 'MEMORY.md exceeds the safety limit. Remove older content first.', + revision_conflict: 'Memory was just changed by another operation. Try again.', backup_revision_conflict: 'The backup was just changed by another operation. Try again.', + invalid_state: 'The Runtime Host returned an invalid memory state.', + }, origins: { manual: 'Manual entry', imported: 'Imported entry', extracted: 'Confirmed extraction', unknown: 'Handwritten entry' }, entryStatuses: { draft: 'Draft', review_required: 'Needs review', active: 'Active', archived: 'Archived', rejected: 'Rejected', unknown: 'Unrecognized' }, backupKinds: { reset: 'Before reset', restore: 'Before restore', save: 'Before save' }, memoryStatuses: { ok: 'Local file ready', disabled: 'Off', safe_mode: 'Safe mode', incognito_blocked: 'Disabled in incognito', error: 'Read failed' }, promptBlocked: { disabled: 'Local memory is disabled.', incognito: 'Local memory is never added in incognito mode.', safeMode: 'MEMORY.md is too large and will not be added.', agentRead: 'Model context access is disabled.' }, backupOversize: 'Backup is too large to preview entries', previewOversize: 'The draft is too large, so entry preview is paused. Reduce MEMORY.md before saving.', previewTruncationMarker: '[Local memory truncated to the length limit]', }), } satisfies UiCatalog; export function getMemorySettingsCopy(locale: UiLocale): MemorySettingsCopy { return SETTINGS_MEMORY_COPY[locale]; } -function makeCopy(intlLocale: string, text: Record, values: Pick): MemorySettingsCopy { +function makeCopy(intlLocale: string, text: Record, values: Pick): MemorySettingsCopy { const plural = (count: number, one: string, many: string) => `${count} ${count === 1 ? one : many}`; const isZh = intlLocale === 'zh-CN'; return { diff --git a/apps/desktop/src/renderer/locales/settings-test-result-copy.ts b/apps/desktop/src/renderer/locales/settings-test-result-copy.ts index 48a4097fb9..84cf6603cd 100644 --- a/apps/desktop/src/renderer/locales/settings-test-result-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-test-result-copy.ts @@ -28,6 +28,7 @@ type SettingsTestResultCopy = { location: string | undefined, ) => string; disabled: string; + disabledDirect: string; configurationMissing: string; credentialMissing: string; timeout: string; @@ -49,6 +50,7 @@ const COPY = { reachable: (endpoint, location) => ["代理配置有效", endpoint, location].filter(Boolean).join(" · "), disabled: "请先启用代理服务器,再进行测试。", + disabledDirect: "代理未启用,当前会直接连接。", configurationMissing: "请填写代理服务器地址和端口后再测试。", credentialMissing: "代理认证已开启,请输入代理密码后再测试。", timeout: "代理测试超时,请检查代理服务是否可达。", @@ -76,6 +78,7 @@ const COPY = { .filter(Boolean) .join(" · "), disabled: "Enable the proxy server before testing it.", + disabledDirect: "The proxy is disabled; connections go direct.", configurationMissing: "Enter a proxy host and port before testing it.", credentialMissing: "Proxy authentication is enabled. Enter a proxy password before testing.", @@ -117,6 +120,8 @@ export function settingsTestResultMessage( ); case "proxy_disabled": return copy.proxy.disabled; + case "proxy_disabled_direct": + return copy.proxy.disabledDirect; case "proxy_configuration_missing": return copy.proxy.configurationMissing; case "proxy_credential_missing": @@ -138,9 +143,7 @@ export function settingsTestResultMessage( case "bot_connection_failed": return copy.bot.connectionFailed; default: - return locale === "en" && result.message.trim() - ? result.message - : copy.bot.connectionFailed; + return copy.bot.connectionFailed; } } diff --git a/apps/desktop/src/renderer/settings/data-settings-page.tsx b/apps/desktop/src/renderer/settings/data-settings-page.tsx index 517563eb0f..ecf86f43e9 100644 --- a/apps/desktop/src/renderer/settings/data-settings-page.tsx +++ b/apps/desktop/src/renderer/settings/data-settings-page.tsx @@ -220,10 +220,7 @@ export function DataSettingsPage(props: { if (res.ok) { toast.success(copy.imported, summarizeImportResult(res.result, copy)); } else if (res.reason !== 'canceled') { - const detail = res.message && (locale === 'zh' || !/[\u3400-\u9fff]/u.test(res.message)) - ? res.message - : copy.invalidFile; - toast.error(copy.importFailed, detail, undefined, diagnosticTarget); + toast.error(copy.importFailed, copy.importFailures[res.reason], undefined, diagnosticTarget); } } catch (error) { toast.error( diff --git a/apps/desktop/src/renderer/settings/permission-center-page.tsx b/apps/desktop/src/renderer/settings/permission-center-page.tsx index 990026f785..ca5c9e7a5e 100644 --- a/apps/desktop/src/renderer/settings/permission-center-page.tsx +++ b/apps/desktop/src/renderer/settings/permission-center-page.tsx @@ -36,7 +36,11 @@ import type { PermissionSnapshot, } from '@maka/core/capabilities'; import type { UiLocale } from '@maka/core/ui-locale'; -import { isDragGrantPermissionId, OS_PERMISSION_IDS } from '@maka/core/capabilities'; +import { + isCapabilityReasonCode, + isDragGrantPermissionId, + OS_PERMISSION_IDS, +} from '@maka/core/capabilities'; import { Banner, Button, @@ -421,10 +425,10 @@ function CapabilityRow(props: { const { copy, locale } = props; const readinessCopy = copy.readiness[capability.readiness]; const capabilityLabel = localizedCapabilityLabel(capability, locale); - const featureReason = localizedSnapshotText(capability.feature.reason, locale); - const configurationReason = localizedSnapshotText(capability.configuration.reason, locale); - const runtimeReason = localizedSnapshotText(capability.runtimeProbe.reason, locale); - const guidance = localizedCapabilityGuidance(capability, locale, copy); + const featureReason = capabilityReasonText(capability.feature.reason, capability, copy); + const configurationReason = capabilityReasonText(capability.configuration.reason, capability, copy); + const runtimeReason = capabilityReasonText(capability.runtimeProbe.reason, capability, copy); + const guidance = localizedCapabilityGuidance(capability, copy); const layers: Array<{ label: string; value: string; reason?: string }> = [ { @@ -587,7 +591,7 @@ function OsPermissionRow(props: { const purpose = permissionCopy?.purpose ?? ''; const impact = permissionCopy?.impact ?? ''; const stateCopy = props.copy.osStates[snapshot.status]; - const reason = localizedSnapshotText(snapshot.reason, props.locale); + const reason = osPermissionReasonText(snapshot, props.copy); const showRequest = snapshot.canRequest && snapshot.status !== 'granted'; const showOpenSettings = snapshot.canOpenSettings && snapshot.status !== 'granted'; @@ -702,17 +706,47 @@ function localizedCapabilityLabel(capability: CapabilitySnapshot, locale: UiLoca return capability.label; } -function localizedSnapshotText(value: string | undefined, locale: UiLocale): string | undefined { - if (!value || (locale === 'en' && /[\u3400-\u9fff]/u.test(value))) return undefined; - return value; +function capabilityReasonText( + reason: string | undefined, + capability: CapabilitySnapshot, + copy: PermissionCenterCopy, +): string | undefined { + if (!reason) return undefined; + if (reason === 'cu_backend_status') { + const missing = capability.osPermissions + .filter((permission) => permission.required && permission.status !== 'granted') + .map((permission) => copy.osPermissions[permission.id]?.label ?? permission.id); + return copy.cuBackendStatus(missing, capability.runtimeProbe.state); + } + if (isCapabilityReasonCode(reason) && reason !== 'cu_backend_status') { + return copy.reasons[reason]; + } + // Bot capabilities pass bridge status reasons through as machine codes; the + // bot settings page owns their full copy, this summary shows the generic line. + return copy.reasonFallback; } function localizedCapabilityGuidance( capability: CapabilitySnapshot, - locale: UiLocale, copy: PermissionCenterCopy, ): readonly string[] { - return capability.guidance.filter((item) => locale === 'zh' || !/[\u3400-\u9fff]/u.test(item)); + return capability.guidance + .map((code) => capabilityReasonText(code, capability, copy)) + .filter((item): item is string => Boolean(item)); +} + +function osPermissionReasonText( + snapshot: OsPermissionSnapshot, + copy: PermissionCenterCopy, +): string | undefined { + const text = snapshot.reason + ? isCapabilityReasonCode(snapshot.reason) && snapshot.reason !== 'cu_backend_status' + ? copy.reasons[snapshot.reason] + : copy.reasonFallback + : undefined; + // `detail` is raw probe error text and renders verbatim beside the copy. + if (!text) return snapshot.detail; + return snapshot.detail ? `${text} · ${snapshot.detail}` : text; } function featureTone(state: CapabilitySnapshot['feature']['state']): StatusSemantic { diff --git a/apps/desktop/src/renderer/settings/provider-oauth-section.tsx b/apps/desktop/src/renderer/settings/provider-oauth-section.tsx index a5c16a7924..4bc1ac26a7 100644 --- a/apps/desktop/src/renderer/settings/provider-oauth-section.tsx +++ b/apps/desktop/src/renderer/settings/provider-oauth-section.tsx @@ -29,14 +29,14 @@ import { } from '@maka/ui'; import { getProviderSettingsCopy, + subscriptionActionErrorMessage, + subscriptionResultMessage, type ConnectionOAuthProviderBridge, type ConnectionsBridge, type ProviderSettingsCopy, } from '../features/connection-settings'; import { useOAuthLoginFlow, - subscriptionActionErrorMessage, - subscriptionResultMessage, type OAuthAuthorizationFlowBridge, type OAuthConnectionIdentity, type SubscriptionSnapshot, @@ -256,7 +256,7 @@ function GitHubCopilotLoginPanel(props: { if (!result.ok) { flow.reportError( copy.copilotActionFailed, - subscriptionResultMessage(result.message, copy.copilotActionFailed, locale, result.reason), + subscriptionResultMessage(result, copy.copilotActionFailed, locale), ); return; } diff --git a/apps/desktop/src/renderer/settings/use-memory-settings-controller.ts b/apps/desktop/src/renderer/settings/use-memory-settings-controller.ts index 40bec015cd..ff39744d40 100644 --- a/apps/desktop/src/renderer/settings/use-memory-settings-controller.ts +++ b/apps/desktop/src/renderer/settings/use-memory-settings-controller.ts @@ -40,7 +40,7 @@ import { } from './memory-settings-labels'; import { deriveMemorySettingsViewModel } from './memory-settings-view-model'; import { useKeyedActionGuard } from './use-action-guard'; -import { getMemorySettingsCopy } from '../locales/settings-memory-copy'; +import { getMemorySettingsCopy, type MemorySettingsCopy } from '../locales/settings-memory-copy'; import { readScrollMotionBehavior } from '../scroll-motion-policy'; import { useRuntimeHostSettingsErrorReporter, @@ -287,7 +287,7 @@ export function useMemoryDocumentController(props: MemoryDocumentControllerProps } else { reportHostError( copy.text.restoreFailed, - memoryResultMessage(result.message, locale, copy.text.restoreFailed), + memoryResultMessage(result, copy, copy.text.restoreFailed), ); } }); @@ -324,7 +324,7 @@ export function useMemoryDocumentController(props: MemoryDocumentControllerProps } else { reportHostError( copy.text.restoreFailed, - memoryResultMessage(result.message, locale, copy.text.restoreFailed), + memoryResultMessage(result, copy, copy.text.restoreFailed), ); } }); @@ -345,7 +345,7 @@ export function useMemoryDocumentController(props: MemoryDocumentControllerProps if (!result.ok) { reportHostError( copy.text.openFailed, - memoryResultMessage(result.message, locale, copy.text.openFailed), + memoryResultMessage(result, copy, copy.text.openFailed), ); } } catch (error) { @@ -364,7 +364,7 @@ export function useMemoryDocumentController(props: MemoryDocumentControllerProps if (!result.ok) { reportHostError( copy.text.openPreviousFailed, - memoryResultMessage(result.message, locale, copy.text.openPreviousFailed), + memoryResultMessage(result, copy, copy.text.openPreviousFailed), ); } } catch (error) { @@ -386,7 +386,7 @@ export function useMemoryDocumentController(props: MemoryDocumentControllerProps if (!result.ok) { reportHostError( copy.openBackupFailed(localMemoryBackupKindLabel(backup.kind, copy)), - memoryResultMessage(result.message, locale, copy.text.openFailed), + memoryResultMessage(result, copy, copy.text.openFailed), ); } } catch (error) { @@ -688,6 +688,11 @@ export function useMemoryDocumentController(props: MemoryDocumentControllerProps }; } -function memoryResultMessage(message: string, locale: UiLocale, fallback: string): string { - return locale === 'zh' || !/[\u3400-\u9fff]/u.test(message) ? message : fallback; +function memoryResultMessage( + result: { code?: string }, + copy: MemorySettingsCopy, + fallback: string, +): string { + const results = copy.results as Readonly>; + return (result.code && results[result.code]) || fallback; } diff --git a/apps/desktop/src/renderer/settings/use-oauth-login-flow.ts b/apps/desktop/src/renderer/settings/use-oauth-login-flow.ts index fbfdc16031..d399135b6f 100644 --- a/apps/desktop/src/renderer/settings/use-oauth-login-flow.ts +++ b/apps/desktop/src/renderer/settings/use-oauth-login-flow.ts @@ -18,7 +18,6 @@ */ import { useEffect, useRef, useState } from 'react'; -import { generalizedErrorMessage, generalizedErrorMessageChinese, redactSecrets } from '@maka/core/redaction'; import { type UiLocale } from '@maka/core/ui-locale'; import { useMountedRef, @@ -26,7 +25,7 @@ import { useUiLocale, } from '@maka/ui'; import { createOneShotActionGuard, teardownPendingAuthorization } from './oauth-login-flow-guard'; -import { getProviderSettingsCopy } from '../features/connection-settings'; +import { getProviderSettingsCopy, subscriptionActionErrorMessage, subscriptionResultMessage } from '../features/connection-settings'; import { useRuntimeHostSettingsErrorReporter } from './runtime-host-settings-target.js'; // Shared browser-assisted OAuth login-flow controller (device-code polling). @@ -211,7 +210,7 @@ export function useOAuthLoginFlow(params: OAuthLoginFlowParams): OAuthLoginFlowC const payload = await authorizationBridge.getAuthUrl(); if ('ok' in payload) { if (!oauthLoginFlowMountedRef.current) return; - const failureMessage = payload.ok ? copy.retry : subscriptionResultMessage(payload.message, copy.startFailedRetry, locale, payload.reason); + const failureMessage = payload.ok ? copy.retry : subscriptionResultMessage(payload, copy.startFailedRetry, locale); reportHostError(copy.startFailed, failureMessage); setErrorMessage(failureMessage); return; @@ -227,7 +226,7 @@ export function useOAuthLoginFlow(params: OAuthLoginFlowParams): OAuthLoginFlowC const opened = await authorizationBridge.openAuthUrl(payload.authRequestId); if (!oauthLoginFlowMountedRef.current) return; if (!opened.ok) { - const message = subscriptionResultMessage(opened.message, copy.openFailedRetry, locale, opened.reason); + const message = subscriptionResultMessage(opened, copy.openFailedRetry, locale); reportHostError(copy.openFailed, message); setErrorMessage(message); void authorizationBridge.cancelAuthorization(payload.authRequestId); @@ -250,7 +249,7 @@ export function useOAuthLoginFlow(params: OAuthLoginFlowParams): OAuthLoginFlowC if (!oauthLoginFlowMountedRef.current) return; if (params.onLoginSuccess) await params.onLoginSuccess(result.connection); } else { - const message = subscriptionResultMessage(result.message, copy.incompleteRetry, locale, result.reason); + const message = subscriptionResultMessage(result, copy.incompleteRetry, locale); reportHostError(copy.incomplete, message); setErrorMessage(message); } @@ -294,7 +293,7 @@ export function useOAuthLoginFlow(params: OAuthLoginFlowParams): OAuthLoginFlowC } else { reportHostError( copy.logoutFailed, - subscriptionResultMessage(result.message, copy.logoutFailedRetry, locale), + subscriptionResultMessage(result, copy.logoutFailedRetry, locale), ); } } catch (error) { @@ -328,44 +327,3 @@ export function useOAuthLoginFlow(params: OAuthLoginFlowParams): OAuthLoginFlowC refresh, }; } - -export function subscriptionActionErrorMessage(error: unknown, locale: UiLocale = 'zh'): string { - const message = error instanceof Error - ? error.message - : typeof error === 'string' - ? error - : ''; - return subscriptionResultMessage(message, getProviderSettingsCopy(locale).oauthFlow.serviceUnavailable, locale); -} - -export function subscriptionResultMessage(message: string | undefined, fallback: string, locale: UiLocale = 'zh', reason?: string): string { - const raw = redactSecrets(message ?? '').trim(); - // The Host refuses an enrollment this install has not opted into and says so - // with a typed reason. Read the reason, not the English message: a reworded - // string or an added locale must not silently disable this branch. The - // message match stays only as a fallback for callers without a typed reason. - if (reason === 'experimental_disabled' || /enrollment is disabled for this provider/i.test(raw)) { - return locale === 'zh' - ? '本机未启用该账号登录方式;可改用导入兼容凭据,或由管理员启用后重试。' - : 'This sign-in is not enabled on this install. Import a compatible credential instead, or ask an operator to enable it.'; - } - if (!raw) return fallback; - // Host conflict / supersede copy before the coarse keyword classifier turns - // "authorization" into a generic 鉴权失败 that does not tell the user what to do. - // This is error-path copy: do not claim a new login already started. - if (/already in progress|superseded by a new attempt/i.test(raw)) { - return locale === 'zh' - ? '上一轮浏览器登录仍在进行或已切换,请再点一次登录,或稍后再试。' - : 'A previous browser login is still running or was superseded. Try logging in again shortly.'; - } - if (/did not present OAuth|no matching OAuth presentation/i.test(raw)) { - return locale === 'zh' - ? '无法打开系统浏览器完成登录,请检查是否拦截了弹窗后重试。' - : 'Could not open the system browser for login. Check popup blockers and try again.'; - } - const classified = locale === 'zh' - ? generalizedErrorMessageChinese(new Error(raw), '') - : generalizedErrorMessage(new Error(raw), ''); - if (classified) return classified; - return locale === 'zh' || !/[\u4e00-\u9fff]/.test(raw) ? raw : fallback; -} diff --git a/packages/core/src/capabilities.ts b/packages/core/src/capabilities.ts index bdc33a1fd1..050db86dec 100644 --- a/packages/core/src/capabilities.ts +++ b/packages/core/src/capabilities.ts @@ -103,12 +103,55 @@ export type CapabilityId = | 'memory_write' | `bot:${BotProvider}`; +/** + * Stable machine codes for capability and OS-permission reasons. Producers + * emit these instead of locale-bound prose; presenters own the code→copy map + * per locale. Bot capabilities pass their bridge status reasons through as-is + * (`rate-limited`, `gateway-closed-4004`, …), so signal `reason` fields stay + * `string` — this union types the desktop producers and the presenter maps. + */ +export const CAPABILITY_REASON_CODES = [ + 'disabled', + 'missing platform credentials', + 'macOS TCC only', + 'no Electron API for per-target Apple Events TCC status', + 'cu_artifact_missing', + 'cu_backend_status', + 'cu_backend_unavailable', + 'cu_executor_undistributable', + 'cu_executor_stopped', + 'cu_executor_start_failed', + 'cu_executor_recovering', + 'cu_executor_ready', + 'cu_executor_lazy_start', + 'activity_recorder_partial', + 'activity_recorder_probe_hint', + 'memory_partial', + 'memory_no_probe', + 'accessibility_status_ambiguous', + 'screen_recording_status_mac_only', + 'notifications_status_unreadable_macos', + 'notifications_status_unreadable', + 'notifications_unsupported', + 'permission_probe_failed', +] as const; + +export type CapabilityReasonCode = (typeof CAPABILITY_REASON_CODES)[number]; + +export function isCapabilityReasonCode(value: unknown): value is CapabilityReasonCode { + return ( + typeof value === 'string' && (CAPABILITY_REASON_CODES as readonly string[]).includes(value) + ); +} + export interface OsPermissionSnapshot { id: OsPermissionId; status: OsPermissionState; source: 'electron' | 'platform' | 'static'; checkedAt: number; reason?: string; + /** Raw diagnostic pass-through (external error text), rendered verbatim. */ + detail?: string; canOpenSettings: boolean; canRequest: boolean; } diff --git a/packages/core/src/health.ts b/packages/core/src/health.ts index e88582cc56..2007efd43c 100644 --- a/packages/core/src/health.ts +++ b/packages/core/src/health.ts @@ -375,8 +375,34 @@ function userVisibleCapabilityReason(reason: string | undefined): string | undef return '仅 macOS 系统权限可探测。'; case 'no Electron API for per-target Apple Events TCC status': return '系统未提供可直接读取的授权状态。'; + case 'cu_artifact_missing': + return '未找到通过完整性检查的 Computer Use 执行器 artifact。'; + case 'cu_backend_status': + return 'maka-cu artifact 已通过本地完整性检查。'; + case 'cu_backend_unavailable': + return 'Computer Use 后端当前不可用。'; + case 'cu_executor_undistributable': + return '未找到通过完整性检查且可分发的 maka-cu executor。'; + case 'cu_executor_stopped': + return 'maka-cu executor 已停止。'; + case 'cu_executor_start_failed': + return 'maka-cu executor 启动失败或已退出。'; + case 'cu_executor_recovering': + return 'maka-cu executor 正在启动或恢复。'; + case 'cu_executor_ready': + return 'maka-cu executor 已就绪。'; + case 'cu_executor_lazy_start': + return 'maka-cu 已可用,将在首次调用时启动。'; + case 'activity_recorder_partial': + return 'Daily Review 已聚合本地任务 / 工具 / 模型活动;当前不包含屏幕与应用级录制'; + case 'activity_recorder_probe_hint': + return '打开 Daily Review 可查看本地活动聚合结果'; + case 'memory_partial': + return '本地 MEMORY.md 已可见;自动抽取/写入仍需用户确认'; + case 'memory_no_probe': + return '透明本地记忆为文件读写能力,不做后台探测'; default: - return /[\u3400-\u9fff]/.test(raw) ? raw : '状态详情请见对应设置页。'; + return '状态详情请见对应设置页。'; } } diff --git a/packages/core/src/llm-connections.ts b/packages/core/src/llm-connections.ts index b17ffff8ed..21293caa28 100644 --- a/packages/core/src/llm-connections.ts +++ b/packages/core/src/llm-connections.ts @@ -531,6 +531,8 @@ export interface ConnectionTestResult { errorMessage?: string; statusCode?: number; errorClass?: ConnectionTestErrorClass; + /** Stable machine code the presenter maps to per-locale copy. */ + errorCode?: 'oauth_rate_limited'; } /** diff --git a/packages/core/src/oauth-subscription.ts b/packages/core/src/oauth-subscription.ts index c8f271b92d..9459597cce 100644 --- a/packages/core/src/oauth-subscription.ts +++ b/packages/core/src/oauth-subscription.ts @@ -38,7 +38,13 @@ */ export type SubscriptionActionResult = | { ok: true } - | { ok: false; reason: SubscriptionActionFailureReason; message: string }; + | { + ok: false; + reason: SubscriptionActionFailureReason; + /** Stable machine code the presenter maps to per-locale copy. */ + code?: string; + message: string; + }; export type SubscriptionActionFailureReason = | 'authorization_pending' // no startAuthorization called yet diff --git a/packages/core/src/settings.ts b/packages/core/src/settings.ts index b6d2daccec..0d9a44cc97 100644 --- a/packages/core/src/settings.ts +++ b/packages/core/src/settings.ts @@ -682,6 +682,7 @@ export interface SettingsTestResult { export type SettingsTestResultCode = | 'proxy_reachable' | 'proxy_disabled' + | 'proxy_disabled_direct' | 'proxy_configuration_missing' | 'proxy_credential_missing' | 'proxy_timeout' diff --git a/packages/runtime/src/test-connection.ts b/packages/runtime/src/test-connection.ts index 65c2b4dc90..cac4cf52b5 100644 --- a/packages/runtime/src/test-connection.ts +++ b/packages/runtime/src/test-connection.ts @@ -462,8 +462,9 @@ async function httpFailure(r: ConnectionEffectResponse, t0: number): Promise Date: Thu, 3 Sep 2026 02:37:14 -0700 Subject: [PATCH 2/3] fix(i18n): address review on producer codes Gate the remaining CJK passthrough to zh, drop silent locale defaults, redact probe detail at the producer, and remove dead copy. Generated-by: Muse Spark --- .../main/__tests__/oauth-result-copy.test.ts | 4 ++++ apps/desktop/src/main/capability-snapshot.ts | 11 +++++++---- .../provider-panel-shared.ts | 18 ++++++++++-------- .../settings-provider-copy.ts | 8 ++++++-- .../renderer/locales/permission-center-copy.ts | 2 ++ .../src/renderer/locales/settings-data-copy.ts | 6 +++--- .../settings/permission-center-page.tsx | 3 ++- .../settings/use-memory-settings-controller.ts | 3 ++- packages/core/src/health.ts | 5 +++++ 9 files changed, 41 insertions(+), 19 deletions(-) diff --git a/apps/desktop/src/main/__tests__/oauth-result-copy.test.ts b/apps/desktop/src/main/__tests__/oauth-result-copy.test.ts index c9a4f964e8..d71d22fbf9 100644 --- a/apps/desktop/src/main/__tests__/oauth-result-copy.test.ts +++ b/apps/desktop/src/main/__tests__/oauth-result-copy.test.ts @@ -19,6 +19,10 @@ import assert from "node:assert/strict"; import test from "node:test"; +// Lives under main/__tests__ on purpose: desktop main tests run from dist via +// `node --test "dist/main/**/*.test.js"`, and settings-provider-copy is a pure +// copy module (no react), so it is safe to exercise from node. Same precedent +// as permission-center-copy.test.ts. import { subscriptionResultMessage } from "../../renderer/features/connection-settings/index.js"; test("renders a coded Copilot import failure per locale, ignoring its machine message", () => { diff --git a/apps/desktop/src/main/capability-snapshot.ts b/apps/desktop/src/main/capability-snapshot.ts index ab150b1c34..b0cd9f857e 100644 --- a/apps/desktop/src/main/capability-snapshot.ts +++ b/apps/desktop/src/main/capability-snapshot.ts @@ -19,6 +19,7 @@ import { Notification, systemPreferences } from 'electron'; import { BOT_PROVIDERS, type BotProvider } from '@maka/core/bot-chat-settings'; +import { redactSecrets } from '@maka/core/redaction'; import { deriveCapabilityReadiness, runtimeProbeFromBotReadiness, @@ -161,7 +162,8 @@ function computerUseCapability( // The presenter composes the full 'cu_backend_status' sentence from data the // same snapshot already carries (required-permission statuses and the runtime -// probe state), so the reason stays a bare code. +// probe state), so the reason stays a bare code. The backend name is not +// threaded through: CU_BACKEND_IDS is ['maka-cu'] today. function computerUseCapabilityReason( input: { backendId: CuBackendId | 'none'; @@ -356,9 +358,10 @@ function unknownPermission( source: 'electron', checkedAt: now, reason: 'permission_probe_failed', - // Raw probe error text passes through verbatim as diagnostic detail; the - // presenter renders it beside the localized 'permission_probe_failed' copy. - ...(error instanceof Error && error.message ? { detail: error.message } : {}), + // Raw probe error text passes through as diagnostic detail, redacted at + // the producer so every presenter can render it verbatim beside the + // localized 'permission_probe_failed' copy. + ...(error instanceof Error && error.message ? { detail: redactSecrets(error.message) } : {}), canOpenSettings, canRequest: false, }; diff --git a/apps/desktop/src/renderer/features/connection-settings/provider-panel-shared.ts b/apps/desktop/src/renderer/features/connection-settings/provider-panel-shared.ts index cca6f46a13..058fc41149 100644 --- a/apps/desktop/src/renderer/features/connection-settings/provider-panel-shared.ts +++ b/apps/desktop/src/renderer/features/connection-settings/provider-panel-shared.ts @@ -28,7 +28,7 @@ import { cleanErrorMessage } from '../../application/contracts/connection-error- export type CredentialPresenceStatus = boolean | 'loading' | 'error'; -export function providerPanelActionErrorMessage(error: unknown, locale: UiLocale = 'zh'): string { +export function providerPanelActionErrorMessage(error: unknown, locale: UiLocale): string { const shared = getProviderSettingsCopy(locale).shared; // Electron wraps ipcMain.handle rejections as "Error invoking remote method // '': Error: ". Classify the original message, not the @@ -37,9 +37,11 @@ export function providerPanelActionErrorMessage(error: unknown, locale: UiLocale const cleaned = redactSecrets(cleanErrorMessage(error)).trim(); const known = (shared.lastTest as Readonly>)[cleaned.toLowerCase()]; if (known) return known; - // Main-process handlers throw display-ready Chinese copy; keep it instead - // of flattening it into a coarser classification or the generic fallback. - if (/[\u3400-\u9fff]/.test(cleaned)) return cleaned; + // Main-process handlers still throw display-ready Chinese copy; keep it for + // zh only instead of flattening it into a coarser classification. en never + // sees raw CJK here — it falls through to the classifier and the fallback. + // Full removal waits on producer code-ification (locale roadmap W2b). + if (locale === 'zh' && /[\u3400-\u9fff]/.test(cleaned)) return cleaned; if (/connection_stale|Unable to delete Connection: connection_stale/i.test(cleaned)) { return locale === 'zh' ? '连接状态已更新,请刷新列表后再删除。' @@ -64,7 +66,7 @@ export interface ConnectionTestTroubleshootingCopy { export function connectionTestFailureFallback( result: ConnectionTestResult, copy: ConnectionTestTroubleshootingCopy, - locale: UiLocale = 'zh', + locale: UiLocale, ): string { const shared = getProviderSettingsCopy(locale).shared; if (result.errorCode === 'oauth_rate_limited') return shared.oauthRateLimit; @@ -83,7 +85,7 @@ export function connectionTestFailureFallback( export function connectionTestFailureMessage( result: ConnectionTestResult, copy: ConnectionTestTroubleshootingCopy, - locale: UiLocale = 'zh', + locale: UiLocale, ): string { const fallback = connectionTestFailureFallback(result, copy, locale); // A coded result already resolved to specific per-locale copy above; the @@ -94,7 +96,7 @@ export function connectionTestFailureMessage( : generalizedErrorMessage(new Error(result.errorMessage), fallback); } -export function connectionLastTestMessageDisplay(message: string | undefined, locale: UiLocale = 'zh'): string | undefined { +export function connectionLastTestMessageDisplay(message: string | undefined, locale: UiLocale): string | undefined { if (!message) return undefined; const trimmed = message.trim(); if (!trimmed) return undefined; @@ -108,6 +110,6 @@ export function connectionLastTestMessageDisplay(message: string | undefined, lo return classified || copy.statusUnavailable; } -export function categoryLabel(category: ProviderCategory, locale: UiLocale = 'zh'): string { +export function categoryLabel(category: ProviderCategory, locale: UiLocale): string { return getProviderSettingsCopy(locale).shared.categories[category]; } diff --git a/apps/desktop/src/renderer/features/connection-settings/settings-provider-copy.ts b/apps/desktop/src/renderer/features/connection-settings/settings-provider-copy.ts index 7ce00a875e..b223e15c26 100644 --- a/apps/desktop/src/renderer/features/connection-settings/settings-provider-copy.ts +++ b/apps/desktop/src/renderer/features/connection-settings/settings-provider-copy.ts @@ -442,6 +442,8 @@ export function subscriptionActionErrorMessage(error: unknown, locale: UiLocale) } export type SubscriptionResultInput = + // `code`/`reason` stay `string` on the wire: a newer host may send a code + // this client does not know yet, and the guard below maps only known codes. | string | undefined | { readonly code?: string; readonly reason?: string; readonly message?: string }; @@ -449,14 +451,16 @@ export type SubscriptionResultInput = export function subscriptionResultMessage(input: SubscriptionResultInput, fallback: string, locale: UiLocale): string { const { code, reason, message } = typeof input === 'object' && input !== null ? input : { message: input }; const copy = getProviderSettingsCopy(locale).oauthFlow; - const codes = copy.resultCodes as Readonly>; + // Catalog uses exact code keys; the index-signature view maps unknown wire + // codes (host version skew) to undefined without a cast. + const codes: Readonly> = copy.resultCodes; const mapped = (code && codes[code]) || (reason && codes[reason]); if (mapped) return mapped; const raw = redactSecrets(message ?? '').trim(); if (!raw) return fallback; // Stable Host messages, matched before the coarse keyword classifier turns // "authorization" into a generic auth failure that does not tell the user what to do. - if (/enrollment is disabled for this provider/i.test(raw)) return codes.experimental_disabled ?? fallback; + if (/enrollment is disabled for this provider/i.test(raw)) return copy.resultCodes.experimental_disabled; if (/already in progress|superseded by a new attempt/i.test(raw)) return copy.loginConflict; if (/did not present OAuth|no matching OAuth presentation/i.test(raw)) return copy.browserPresentFailed; const classified = locale === 'zh' diff --git a/apps/desktop/src/renderer/locales/permission-center-copy.ts b/apps/desktop/src/renderer/locales/permission-center-copy.ts index b843265249..6c29d225f5 100644 --- a/apps/desktop/src/renderer/locales/permission-center-copy.ts +++ b/apps/desktop/src/renderer/locales/permission-center-copy.ts @@ -97,6 +97,8 @@ export type PermissionCenterCopy = { dragGrant: string; dragGranting: string; reasons: Record, string>; + // Single-backend assumption: CU_BACKEND_IDS is ['maka-cu'], so the backend + // name stays a literal in copy. Revisit when a second backend lands. cuBackendStatus(missingPermissionLabels: readonly string[], health: RuntimeProbeState): string; reasonFallback: string; }; diff --git a/apps/desktop/src/renderer/locales/settings-data-copy.ts b/apps/desktop/src/renderer/locales/settings-data-copy.ts index 2ed52b0470..acf452dcea 100644 --- a/apps/desktop/src/renderer/locales/settings-data-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-data-copy.ts @@ -28,7 +28,7 @@ export type DataSettingsCopy = { }; loadFailed: string; openFailed(label: string): string; pathCopied: string; copyFailed: string; copyFailedDetail: string; historyCleared: string; historyClearedDetail: string; selectCategory: string; exported: string; exportedDetail(items: readonly string[]): string; - exportFailed: string; noCategories: string; tryAgain: string; imported: string; importFailed: string; invalidFile: string; + exportFailed: string; noCategories: string; tryAgain: string; imported: string; importFailed: string; importFailures: Record<'not_json' | 'malformed' | 'unsupported_version', string>; rows: { workspace: string; workspaceDetail: string; loadValueFailed: string; loading: string; @@ -56,7 +56,7 @@ const SETTINGS_DATA_COPY = { loadFailed: '载入数据目录失败', openFailed: (label) => `无法打开${label}`, pathCopied: '已复制工作区路径', copyFailed: '复制失败', copyFailedDetail: '剪贴板不可用或被系统拒绝。', historyCleared: '已清空输入历史', historyClearedDetail: '已发送的提示词记录已从本机移除。', selectCategory: '请至少选择一个类别', exported: '已导出配置', exportedDetail: (items) => `包含:${items.join('、')}`, exportFailed: '导出失败', noCategories: '未选择任何类别', tryAgain: '请稍后重试', - imported: '已导入配置', importFailed: '导入失败', invalidFile: '文件无效或版本不受支持。', + imported: '已导入配置', importFailed: '导入失败', importFailures: { not_json: '文件不是有效的 JSON。', malformed: '配置文件结构无效。', unsupported_version: '配置文件版本不受支持。' }, rows: { workspace: '工作区路径', workspaceDetail: '任务、设置、凭据和技能文件都存在这个目录下。', loadValueFailed: '载入失败', loading: '正在加载…', @@ -84,7 +84,7 @@ const SETTINGS_DATA_COPY = { loadFailed: 'Failed to load data directory', openFailed: (label) => `Could not open ${label}`, pathCopied: 'Workspace path copied', copyFailed: 'Copy failed', copyFailedDetail: 'The clipboard is unavailable or access was denied by the system.', historyCleared: 'Input history cleared', historyClearedDetail: 'Sent prompt history was removed from this device.', selectCategory: 'Select at least one category', exported: 'Configuration exported', exportedDetail: (items) => `Included: ${items.join(', ')}`, exportFailed: 'Export failed', noCategories: 'No categories selected', tryAgain: 'Try again later', - imported: 'Configuration imported', importFailed: 'Import failed', invalidFile: 'The file is invalid or its version is unsupported.', + imported: 'Configuration imported', importFailed: 'Import failed', importFailures: { not_json: 'The file is not valid JSON.', malformed: 'The config bundle is malformed.', unsupported_version: 'The config file version is unsupported.' }, rows: { workspace: 'Workspace path', workspaceDetail: 'Tasks, settings, credentials, and skill files are stored in this directory.', loadValueFailed: 'Failed to load', loading: 'Loading…', diff --git a/apps/desktop/src/renderer/settings/permission-center-page.tsx b/apps/desktop/src/renderer/settings/permission-center-page.tsx index ca5c9e7a5e..fe1769632b 100644 --- a/apps/desktop/src/renderer/settings/permission-center-page.tsx +++ b/apps/desktop/src/renderer/settings/permission-center-page.tsx @@ -744,7 +744,8 @@ function osPermissionReasonText( ? copy.reasons[snapshot.reason] : copy.reasonFallback : undefined; - // `detail` is raw probe error text and renders verbatim beside the copy. + // `detail` is redacted probe diagnostics from the producer and renders + // verbatim beside the copy. if (!text) return snapshot.detail; return snapshot.detail ? `${text} · ${snapshot.detail}` : text; } diff --git a/apps/desktop/src/renderer/settings/use-memory-settings-controller.ts b/apps/desktop/src/renderer/settings/use-memory-settings-controller.ts index ff39744d40..1a745f981f 100644 --- a/apps/desktop/src/renderer/settings/use-memory-settings-controller.ts +++ b/apps/desktop/src/renderer/settings/use-memory-settings-controller.ts @@ -693,6 +693,7 @@ function memoryResultMessage( copy: MemorySettingsCopy, fallback: string, ): string { - const results = copy.results as Readonly>; + // Unknown wire codes (host version skew) map to undefined, then fallback. + const results: Readonly> = copy.results; return (result.code && results[result.code]) || fallback; } diff --git a/packages/core/src/health.ts b/packages/core/src/health.ts index 2007efd43c..dca5eb3a7b 100644 --- a/packages/core/src/health.ts +++ b/packages/core/src/health.ts @@ -366,6 +366,11 @@ function capabilityDetail(capability: CapabilitySnapshot): string | undefined { function userVisibleCapabilityReason(reason: string | undefined): string | undefined { const raw = reason?.trim(); if (!raw) return undefined; + // Locale anchor contract: this module is the zh source of truth and + // settings-health-copy maps en from these exact strings (see + // englishSignalMessage/englishSignalDetail). New codes below intentionally + // add zh branches only — en keeps the generic line there until #4524 + // code-ifies the health presenter. Do not reword without updating both. switch (raw) { case 'disabled': return '该能力当前已关闭。'; From b5264514384eecc62509d353fb2b658804424044 Mon Sep 17 00:00:00 2001 From: NekoPunch Date: Thu, 3 Sep 2026 03:12:44 -0700 Subject: [PATCH 3/3] fix(i18n): tighten producer code boundaries Generated-by: OpenCode --- .../main/__tests__/oauth-result-copy.test.ts | 18 ++++++++++- .../settings-test-result-copy.test.ts | 10 ------ apps/desktop/src/main/capability-snapshot.ts | 32 +++---------------- apps/desktop/src/main/computer-use-host.ts | 3 +- .../provider-panel-shared.ts | 5 +-- .../settings-provider-copy.ts | 4 +-- .../locales/settings-test-result-copy.ts | 5 --- .../settings/permission-center-page.tsx | 5 +-- packages/core/src/capabilities.ts | 2 -- packages/core/src/llm-connections.ts | 2 -- packages/core/src/settings.ts | 1 - packages/runtime/src/test-connection.ts | 3 -- 12 files changed, 28 insertions(+), 62 deletions(-) diff --git a/apps/desktop/src/main/__tests__/oauth-result-copy.test.ts b/apps/desktop/src/main/__tests__/oauth-result-copy.test.ts index d71d22fbf9..b3410f3355 100644 --- a/apps/desktop/src/main/__tests__/oauth-result-copy.test.ts +++ b/apps/desktop/src/main/__tests__/oauth-result-copy.test.ts @@ -23,7 +23,10 @@ import test from "node:test"; // `node --test "dist/main/**/*.test.js"`, and settings-provider-copy is a pure // copy module (no react), so it is safe to exercise from node. Same precedent // as permission-center-copy.test.ts. -import { subscriptionResultMessage } from "../../renderer/features/connection-settings/index.js"; +import { + connectionTestFailureMessage, + subscriptionResultMessage, +} from "../../renderer/features/connection-settings/index.js"; test("renders a coded Copilot import failure per locale, ignoring its machine message", () => { const result = { code: "copilot_subscription_unavailable", message: "copilot_subscription_unavailable" }; @@ -42,3 +45,16 @@ test("falls back to catalog copy for an unknown code instead of the raw message" assert.equal(subscriptionResultMessage(result, "fallback", "en"), "fallback"); assert.equal(subscriptionResultMessage(result, "fallback", "zh"), "fallback"); }); + +test("renders provider rate limits consistently from the stable status code", () => { + const result = { ok: false, statusCode: 429, errorClass: "provider_unavailable" } as const; + const troubleshooting = { auth: "auth", recheck: "recheck" }; + assert.equal( + connectionTestFailureMessage(result, troubleshooting, "zh"), + "当前账号或模型服务触发速率限制,请稍后重试。", + ); + assert.equal( + connectionTestFailureMessage(result, troubleshooting, "en"), + "This account or model service is rate-limited. Try again later.", + ); +}); diff --git a/apps/desktop/src/main/__tests__/settings-test-result-copy.test.ts b/apps/desktop/src/main/__tests__/settings-test-result-copy.test.ts index 9c1369983d..757ced374c 100644 --- a/apps/desktop/src/main/__tests__/settings-test-result-copy.test.ts +++ b/apps/desktop/src/main/__tests__/settings-test-result-copy.test.ts @@ -37,13 +37,3 @@ test("missing proxy credentials have actionable bilingual copy", () => { "Proxy authentication is enabled. Enter a proxy password before testing.", ); }); - -test("renders the disabled-direct proxy code per locale", () => { - const result = { ok: true, code: "proxy_disabled_direct", message: "direct" } as never; - assert.equal(settingsTestResultMessage(result, "zh"), "代理未启用,当前会直接连接。"); - assert.equal( - settingsTestResultMessage(result, "en"), - "The proxy is disabled; connections go direct.", - ); -}); - diff --git a/apps/desktop/src/main/capability-snapshot.ts b/apps/desktop/src/main/capability-snapshot.ts index b0cd9f857e..4d54a92a78 100644 --- a/apps/desktop/src/main/capability-snapshot.ts +++ b/apps/desktop/src/main/capability-snapshot.ts @@ -19,7 +19,6 @@ import { Notification, systemPreferences } from 'electron'; import { BOT_PROVIDERS, type BotProvider } from '@maka/core/bot-chat-settings'; -import { redactSecrets } from '@maka/core/redaction'; import { deriveCapabilityReadiness, runtimeProbeFromBotReadiness, @@ -140,7 +139,7 @@ function computerUseCapability( feature: { state: artifactAvailable ? 'enabled' : 'not_available', source: 'runtime', - reason: computerUseCapabilityReason(input), + reason: input === undefined || input.backendId === 'none' ? 'cu_artifact_missing' : 'cu_backend_status', }, requiredPermissions: [ { id: 'accessibility', required: true, status: permissions.accessibility.status }, @@ -160,22 +159,6 @@ function computerUseCapability( }); } -// The presenter composes the full 'cu_backend_status' sentence from data the -// same snapshot already carries (required-permission statuses and the runtime -// probe state), so the reason stays a bare code. The backend name is not -// threaded through: CU_BACKEND_IDS is ['maka-cu'] today. -function computerUseCapabilityReason( - input: { - backendId: CuBackendId | 'none'; - health: ReturnType; - } | undefined, -): string { - if (input === undefined || input.backendId === 'none') { - return 'cu_artifact_missing'; - } - return 'cu_backend_status'; -} - function staticCapability(input: { id: CapabilitySnapshot['id']; label: string; @@ -268,8 +251,8 @@ function accessibilitySnapshot(now: number, platform: NodeJS.Platform): OsPermis canOpenSettings: true, canRequest: false, }; - } catch (error) { - return unknownPermission('accessibility', now, error, true); + } catch { + return unknownPermission('accessibility', now, true); } } @@ -292,8 +275,8 @@ function mediaPermissionSnapshot( checkedAt: now, ...actions, }; - } catch (error) { - return unknownPermission(id, now, error, platform === 'darwin'); + } catch { + return unknownPermission(id, now, platform === 'darwin'); } } @@ -349,7 +332,6 @@ function unsupportedPermission( function unknownPermission( id: OsPermissionId, now: number, - error: unknown, canOpenSettings: boolean, ): OsPermissionSnapshot { return { @@ -358,10 +340,6 @@ function unknownPermission( source: 'electron', checkedAt: now, reason: 'permission_probe_failed', - // Raw probe error text passes through as diagnostic detail, redacted at - // the producer so every presenter can render it verbatim beside the - // localized 'permission_probe_failed' copy. - ...(error instanceof Error && error.message ? { detail: redactSecrets(error.message) } : {}), canOpenSettings, canRequest: false, }; diff --git a/apps/desktop/src/main/computer-use-host.ts b/apps/desktop/src/main/computer-use-host.ts index cb72bb5583..aefef19cec 100644 --- a/apps/desktop/src/main/computer-use-host.ts +++ b/apps/desktop/src/main/computer-use-host.ts @@ -34,6 +34,7 @@ import { selectComputerUseBackend, type SelectedComputerUseBackend, } from '@maka/computer-use'; +import type { CapabilityReasonCode } from '@maka/core/capabilities'; import type { CuOverlayHook } from '@maka/runtime/computer-use-types'; export interface ComputerUseHostState { @@ -147,7 +148,7 @@ export function computerUseServiceHealth( state: MakaCuServiceSnapshot | undefined, ): { state: 'not_available' | 'not_run' | 'healthy' | 'degraded'; - reason: string; + reason: CapabilityReasonCode; } { if (backendId === 'none' || !state) { return { state: 'not_available', reason: 'cu_executor_undistributable' }; diff --git a/apps/desktop/src/renderer/features/connection-settings/provider-panel-shared.ts b/apps/desktop/src/renderer/features/connection-settings/provider-panel-shared.ts index 058fc41149..71c9e6438e 100644 --- a/apps/desktop/src/renderer/features/connection-settings/provider-panel-shared.ts +++ b/apps/desktop/src/renderer/features/connection-settings/provider-panel-shared.ts @@ -69,7 +69,6 @@ export function connectionTestFailureFallback( locale: UiLocale, ): string { const shared = getProviderSettingsCopy(locale).shared; - if (result.errorCode === 'oauth_rate_limited') return shared.oauthRateLimit; if (result.statusCode === 429) return shared.rateLimit; if (result.errorClass === 'timeout') return shared.timeout; if (result.errorClass === 'auth' || result.statusCode === 401 || result.statusCode === 403) { @@ -88,9 +87,7 @@ export function connectionTestFailureMessage( locale: UiLocale, ): string { const fallback = connectionTestFailureFallback(result, copy, locale); - // A coded result already resolved to specific per-locale copy above; the - // errorMessage is only its machine-readable twin. - if (result.errorCode || !result.errorMessage) return fallback; + if (!result.errorMessage) return fallback; return locale === 'zh' ? generalizedErrorMessageChinese(new Error(result.errorMessage), fallback) : generalizedErrorMessage(new Error(result.errorMessage), fallback); diff --git a/apps/desktop/src/renderer/features/connection-settings/settings-provider-copy.ts b/apps/desktop/src/renderer/features/connection-settings/settings-provider-copy.ts index b223e15c26..d819460c9e 100644 --- a/apps/desktop/src/renderer/features/connection-settings/settings-provider-copy.ts +++ b/apps/desktop/src/renderer/features/connection-settings/settings-provider-copy.ts @@ -158,7 +158,7 @@ const zhCopy = { modelKeyAria: (name: string) => `${name} 模型密钥`, }, shared: { - actionFallback: '模型连接服务暂时不可用,请稍后重试。', rateLimit: '当前账号或模型服务触发速率限制,请稍后重试。', oauthRateLimit: 'OAuth 已登录,但当前账号或 provider 正在 rate limit。请稍后重试,或先切换到其它可用模型。', + actionFallback: '模型连接服务暂时不可用,请稍后重试。', rateLimit: '当前账号或模型服务触发速率限制,请稍后重试。', timeout: '请求超时,请检查网络或代理后重试。', unavailable: '模型服务暂时不可用,请稍后重试。', network: '网络错误,请检查服务地址或代理设置后重试。', statusUnavailable: '连接测试状态暂时无法显示,请重新测试。', categories: { oauth: 'OAuth', domestic: '国内', overseas: '海外', local: '本地', custom: 'Custom' }, @@ -332,7 +332,7 @@ const enCopy: ProviderSettingsCopy = { modelKeyAria: (name: string) => `${name} model key`, }, shared: { - actionFallback: 'The model connection service is temporarily unavailable. Try again later.', rateLimit: 'This account or model service is rate-limited. Try again later.', oauthRateLimit: 'Signed in, but the account or provider is currently rate limited. Retry later or switch to another available model.', + actionFallback: 'The model connection service is temporarily unavailable. Try again later.', rateLimit: 'This account or model service is rate-limited. Try again later.', timeout: 'The request timed out. Check the network or proxy and try again.', unavailable: 'The model service is temporarily unavailable. Try again later.', network: 'Network error. Check the service URL or proxy settings and try again.', statusUnavailable: 'The connection test status is temporarily unavailable. Test again.', categories: { oauth: 'OAuth', domestic: 'China', overseas: 'Global', local: 'Local', custom: 'Custom' }, diff --git a/apps/desktop/src/renderer/locales/settings-test-result-copy.ts b/apps/desktop/src/renderer/locales/settings-test-result-copy.ts index 84cf6603cd..1d921be9df 100644 --- a/apps/desktop/src/renderer/locales/settings-test-result-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-test-result-copy.ts @@ -28,7 +28,6 @@ type SettingsTestResultCopy = { location: string | undefined, ) => string; disabled: string; - disabledDirect: string; configurationMissing: string; credentialMissing: string; timeout: string; @@ -50,7 +49,6 @@ const COPY = { reachable: (endpoint, location) => ["代理配置有效", endpoint, location].filter(Boolean).join(" · "), disabled: "请先启用代理服务器,再进行测试。", - disabledDirect: "代理未启用,当前会直接连接。", configurationMissing: "请填写代理服务器地址和端口后再测试。", credentialMissing: "代理认证已开启,请输入代理密码后再测试。", timeout: "代理测试超时,请检查代理服务是否可达。", @@ -78,7 +76,6 @@ const COPY = { .filter(Boolean) .join(" · "), disabled: "Enable the proxy server before testing it.", - disabledDirect: "The proxy is disabled; connections go direct.", configurationMissing: "Enter a proxy host and port before testing it.", credentialMissing: "Proxy authentication is enabled. Enter a proxy password before testing.", @@ -120,8 +117,6 @@ export function settingsTestResultMessage( ); case "proxy_disabled": return copy.proxy.disabled; - case "proxy_disabled_direct": - return copy.proxy.disabledDirect; case "proxy_configuration_missing": return copy.proxy.configurationMissing; case "proxy_credential_missing": diff --git a/apps/desktop/src/renderer/settings/permission-center-page.tsx b/apps/desktop/src/renderer/settings/permission-center-page.tsx index fe1769632b..ba5b773a3f 100644 --- a/apps/desktop/src/renderer/settings/permission-center-page.tsx +++ b/apps/desktop/src/renderer/settings/permission-center-page.tsx @@ -744,10 +744,7 @@ function osPermissionReasonText( ? copy.reasons[snapshot.reason] : copy.reasonFallback : undefined; - // `detail` is redacted probe diagnostics from the producer and renders - // verbatim beside the copy. - if (!text) return snapshot.detail; - return snapshot.detail ? `${text} · ${snapshot.detail}` : text; + return text; } function featureTone(state: CapabilitySnapshot['feature']['state']): StatusSemantic { diff --git a/packages/core/src/capabilities.ts b/packages/core/src/capabilities.ts index 050db86dec..fbd355e39d 100644 --- a/packages/core/src/capabilities.ts +++ b/packages/core/src/capabilities.ts @@ -150,8 +150,6 @@ export interface OsPermissionSnapshot { source: 'electron' | 'platform' | 'static'; checkedAt: number; reason?: string; - /** Raw diagnostic pass-through (external error text), rendered verbatim. */ - detail?: string; canOpenSettings: boolean; canRequest: boolean; } diff --git a/packages/core/src/llm-connections.ts b/packages/core/src/llm-connections.ts index 21293caa28..b17ffff8ed 100644 --- a/packages/core/src/llm-connections.ts +++ b/packages/core/src/llm-connections.ts @@ -531,8 +531,6 @@ export interface ConnectionTestResult { errorMessage?: string; statusCode?: number; errorClass?: ConnectionTestErrorClass; - /** Stable machine code the presenter maps to per-locale copy. */ - errorCode?: 'oauth_rate_limited'; } /** diff --git a/packages/core/src/settings.ts b/packages/core/src/settings.ts index 0d9a44cc97..b6d2daccec 100644 --- a/packages/core/src/settings.ts +++ b/packages/core/src/settings.ts @@ -682,7 +682,6 @@ export interface SettingsTestResult { export type SettingsTestResultCode = | 'proxy_reachable' | 'proxy_disabled' - | 'proxy_disabled_direct' | 'proxy_configuration_missing' | 'proxy_credential_missing' | 'proxy_timeout' diff --git a/packages/runtime/src/test-connection.ts b/packages/runtime/src/test-connection.ts index cac4cf52b5..514ea4653e 100644 --- a/packages/runtime/src/test-connection.ts +++ b/packages/runtime/src/test-connection.ts @@ -462,9 +462,6 @@ async function httpFailure(r: ConnectionEffectResponse, t0: number): Promise