fix(frontend): 补齐英文失败路径

This commit is contained in:
2026-09-05 09:48:27 +08:00
parent ef961d322b
commit 89e475c0c2
17 changed files with 83 additions and 70 deletions
+3 -1
View File
@@ -1,3 +1,5 @@
import { t } from '@/i18n'
export interface ServiceStatus {
name: string
version: string
@@ -10,7 +12,7 @@ const apiBaseUrl = import.meta.env.VITE_API_BASE_URL ?? ''
export async function getServiceStatus(): Promise<ServiceStatus> {
const response = await fetch(`${apiBaseUrl}/api/status`)
if (!response.ok) {
throw new Error(`后端请求失败:HTTP ${response.status}`)
throw new Error(`${t('后端请求失败:', 'Backend request failed: ')}HTTP ${response.status}`)
}
return response.json() as Promise<ServiceStatus>
}
+30 -29
View File
@@ -1,4 +1,5 @@
import type { McpServerInput } from '@/contracts'
import { t } from '@/i18n'
export type SecretKind = 'environment' | 'header'
export interface ImportedSecret { kind: SecretKind; key: string; value: string }
@@ -26,38 +27,38 @@ export function emptyMcpConfig(): McpServerInput {
}
function object(value: unknown, label: string): Record<string, unknown> {
if (!value || Array.isArray(value) || typeof value !== 'object') throw new Error(`${label}必须是 JSON 对象`)
if (!value || Array.isArray(value) || typeof value !== 'object') throw new Error(`${label}${t('必须是 JSON 对象', ' must be a JSON object')}`)
return value as Record<string, unknown>
}
function strings(value: unknown, label: string): string[] {
if (value === undefined) return []
if (!Array.isArray(value) || value.some(item => typeof item !== 'string')) throw new Error(`${label}必须是字符串数组`)
if (!Array.isArray(value) || value.some(item => typeof item !== 'string')) throw new Error(`${label}${t('必须是字符串数组', ' must be a string array')}`)
return [...value]
}
function entries(value: unknown, label: string): Record<string, string> {
if (value === undefined) return {}
const result = object(value, label)
if (Object.values(result).some(item => typeof item !== 'string')) throw new Error(`${label}必须是字符串键值 JSON 对象`)
if (Object.values(result).some(item => typeof item !== 'string')) throw new Error(`${label}${t('必须是字符串键值 JSON 对象', ' must be a JSON object with string keys and values')}`)
return { ...result } as Record<string, string>
}
function timeout(value: unknown, fallback: number, max: number, label: string): number {
if (value === undefined) return fallback
if (typeof value !== 'number' || !Number.isFinite(value) || value < 1 || value > max) throw new Error(`${label}必须是 1${max} 秒之间的数字`)
if (typeof value !== 'number' || !Number.isFinite(value) || value < 1 || value > max) throw new Error(`${label}${t(`必须是 1${max} 秒之间的数字`, ` must be a number from 1 to ${max} seconds`)}`)
return value
}
// Do not silently rewrite executable arguments or secret values copied from chat.
function checkUrl(value: string, label: string) {
if (/^\[https?:\/\//i.test(value)) throw new Error(`${label}请填写纯 URL,不要粘贴 Markdown 链接`)
if (/^\[https?:\/\//i.test(value)) throw new Error(`${label}${t('请填写纯 URL,不要粘贴 Markdown 链接', ': enter a plain URL instead of a Markdown link')}`)
}
export function parseMcpJson(raw: string, fallbackName = '', requireConnection = true) {
let parsed: unknown
try { parsed = JSON.parse(raw) }
catch { throw new Error('服务器配置不是有效 JSON;请检查逗号、引号和无效的 \\_ 转义') }
catch { throw new Error(t('服务器配置不是有效 JSON;请检查逗号、引号和无效的 \\_ 转义', 'The server configuration is not valid JSON. Check commas, quotes, and invalid \\_ escapes.')) }
return normalizeMcpConfig(parsed, fallbackName, requireConnection)
}
@@ -65,54 +66,54 @@ export function parseMcpJson(raw: string, fallbackName = '', requireConnection =
* Inline secrets leave the public config here and are sent only to the Secret API.
*/
export function normalizeMcpConfig(parsed: unknown, fallbackName = '', requireConnection = true) {
let raw = object(parsed, '服务器配置')
let raw = object(parsed, t('服务器配置', 'Server configuration'))
if ('mcpServers' in raw) {
const servers = Object.entries(object(raw.mcpServers, 'mcpServers'))
if (servers.length !== 1) throw new Error('请一次导入一个 MCP 服务器')
if (servers.length !== 1) throw new Error(t('请一次导入一个 MCP 服务器', 'Import one MCP server at a time'))
fallbackName = servers[0]![0]
raw = object(servers[0]![1], '服务器配置')
raw = object(servers[0]![1], t('服务器配置', 'Server configuration'))
}
const allowed = new Set([...Object.keys(emptyMcpConfig()), 'version', 'env', 'type', 'timeout', 'sse_read_timeout'])
if (Object.keys(raw).some(key => !allowed.has(key))) {
// Never echo arbitrary unknown keys: pasted secrets sometimes become JSON keys.
throw new Error('服务器配置含不支持的字段;API Key 请放在 env/environment 的对应变量中,不要放在顶层')
throw new Error(t('服务器配置含不支持的字段;API Key 请放在 env/environment 的对应变量中,不要放在顶层', 'The server configuration contains unsupported fields. Put API keys in the corresponding env/environment variables, not at the top level.'))
}
if (raw.env !== undefined && raw.environment !== undefined) throw new Error('env 与 environment 请只保留一个,避免覆盖配置')
if (raw.env !== undefined && raw.environment !== undefined) throw new Error(t('env 与 environment 请只保留一个,避免覆盖配置', 'Keep either env or environment, not both'))
const transport = raw.transport ?? raw.type ?? (raw.url ? 'streamable_http' : 'stdio')
if (!['stdio', 'streamable_http', 'sse'].includes(transport as string)) throw new Error('transport 必须是 stdio、streamable_http 或 sse')
if (!['stdio', 'streamable_http', 'sse'].includes(transport as string)) throw new Error(t('transport 必须是 stdio、streamable_http 或 sse', 'transport must be stdio, streamable_http, or sse'))
const config = emptyMcpConfig()
config.transport = transport as McpServerInput['transport']
const name = raw.name ?? (fallbackName || (typeof raw.command === 'string' ? raw.command : 'MCP 服务器'))
if (typeof name !== 'string' || (requireConnection && !name.trim()) || name.trim().length > 80) throw new Error('服务器名称必须为 180 个字符')
const name = raw.name ?? (fallbackName || (typeof raw.command === 'string' ? raw.command : t('MCP 服务器', 'MCP Server')))
if (typeof name !== 'string' || (requireConnection && !name.trim()) || name.trim().length > 80) throw new Error(t('服务器名称必须为 180 个字符', 'The server name must contain 180 characters'))
config.name = name.trim()
for (const key of ['command', 'url'] as const) {
const value = raw[key]
if (value !== undefined && value !== null && typeof value !== 'string') throw new Error(`${key}必须是字符串`)
if (value !== undefined && value !== null && typeof value !== 'string') throw new Error(`${key}${t('必须是字符串', ' must be a string')}`)
config[key] = typeof value === 'string' ? value.trim() : null
}
config.args = strings(raw.args, 'args')
if (config.args.length > 64) throw new Error('args 最多允许 64 项')
for (const value of config.args) checkUrl(value, 'args 中的地址')
if (config.args.length > 64) throw new Error(t('args 最多允许 64 项', 'args allows at most 64 items'))
for (const value of config.args) checkUrl(value, t('args 中的地址', 'URL in args'))
config.environment = entries(raw.environment ?? raw.env, 'environment/env')
config.headers = entries(raw.headers, 'headers')
config.secret_environment_keys = [...new Set(strings(raw.secret_environment_keys, 'secret_environment_keys'))]
config.secret_header_keys = [...new Set(strings(raw.secret_header_keys, 'secret_header_keys'))]
config.permissions = strings(raw.permissions, 'permissions')
config.startup_timeout_seconds = timeout(raw.startup_timeout_seconds ?? raw.timeout, 15, 120, '启动超时')
config.startup_timeout_seconds = timeout(raw.startup_timeout_seconds ?? raw.timeout, 15, 120, t('启动超时', 'Startup timeout'))
// Compatibility policy: legacy read timeout becomes the tool wait budget, not an SSE transport setting.
config.tool_timeout_seconds = timeout(raw.tool_timeout_seconds ?? raw.sse_read_timeout, 30, 300, '工具超时')
config.tool_timeout_seconds = timeout(raw.tool_timeout_seconds ?? raw.sse_read_timeout, 30, 300, t('工具超时', 'Tool timeout'))
if (config.transport === 'stdio') {
if (requireConnection && !config.command) throw new Error('stdio 配置必须填写 command')
if (config.url || Object.keys(config.headers).length || config.secret_header_keys.length) throw new Error('stdio 配置不能包含 URL 或 HTTP Header')
if (requireConnection && !config.command) throw new Error(t('stdio 配置必须填写 command', 'stdio configuration requires command'))
if (config.url || Object.keys(config.headers).length || config.secret_header_keys.length) throw new Error(t('stdio 配置不能包含 URL 或 HTTP Header', 'stdio configuration cannot contain a URL or HTTP headers'))
} else {
if (requireConnection && !config.url) throw new Error('HTTP/SSE 配置必须填写 url')
if (requireConnection && !config.url) throw new Error(t('HTTP/SSE 配置必须填写 url', 'HTTP/SSE configuration requires a URL'))
if (config.url) {
checkUrl(config.url, 'url')
let url: URL
try { url = new URL(config.url) } catch { throw new Error('url 必须是有效的 HTTP(S) 地址') }
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.hash) throw new Error('url 必须为不含账号密码或片段的 HTTP(S) 地址')
try { url = new URL(config.url) } catch { throw new Error(t('url 必须是有效的 HTTP(S) 地址', 'url must be a valid HTTP(S) address')) }
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.hash) throw new Error(t('url 必须为不含账号密码或片段的 HTTP(S) 地址', 'url must be an HTTP(S) address without credentials or a fragment'))
}
if (config.command || config.args.length || Object.keys(config.environment).length || config.secret_environment_keys.length) throw new Error('HTTP/SSE 配置不能包含 command、args 或环境变量')
if (config.command || config.args.length || Object.keys(config.environment).length || config.secret_environment_keys.length) throw new Error(t('HTTP/SSE 配置不能包含 command、args 或环境变量', 'HTTP/SSE configuration cannot contain command, args, or environment variables'))
}
const secrets: ImportedSecret[] = []
for (const kind of ['environment', 'header'] as const) {
@@ -120,19 +121,19 @@ export function normalizeMcpConfig(parsed: unknown, fallbackName = '', requireCo
const keys = kind === 'environment' ? config.secret_environment_keys : config.secret_header_keys
const identity = (key: string) => kind === 'header' ? key.toLowerCase() : key
const allKeys = [...Object.keys(values), ...keys]
if (kind === 'header' && (new Set(keys.map(identity)).size !== keys.length || new Set(Object.keys(values).map(identity)).size !== Object.keys(values).length)) throw new Error('HTTP Header 名称不能仅大小写不同而重复声明')
if (kind === 'header' && (new Set(keys.map(identity)).size !== keys.length || new Set(Object.keys(values).map(identity)).size !== Object.keys(values).length)) throw new Error(t('HTTP Header 名称不能仅大小写不同而重复声明', 'HTTP header names cannot be duplicated with case-only differences'))
const validKey = kind === 'environment' ? /^[A-Za-z_][A-Za-z0-9_]{0,127}$/ : /^[!#$%&'*+.^_`|~0-9A-Za-z-]{1,128}$/
if (allKeys.some(key => !validKey.test(key))) throw new Error(`${kind === 'environment' ? '环境变量' : 'Header'}名称无效;敏感变量名只能填名称,不能填密钥值`)
if (allKeys.some(key => !validKey.test(key))) throw new Error(`${kind === 'environment' ? t('环境变量', 'Environment variable') : 'Header'}${t('名称无效;敏感变量名只能填名称,不能填密钥值', ' name is invalid; secret variable declarations accept names only, not secret values')}`)
for (const [key, value] of Object.entries(values)) {
const declared = keys.find(item => identity(item) === identity(key))
const sensitive = /api[_-]?key|token|secret|password|authorization|cookie|credential/i.test(key)
if (declared || sensitive) {
if (!value || value.length > 32768) throw new Error('密钥值必须为 132768 个字符')
if (!value || value.length > 32768) throw new Error(t('密钥值必须为 132768 个字符', 'Secret values must contain 132768 characters'))
const secretKey = declared ?? key
if (!declared) keys.push(key)
secrets.push({ kind, key: secretKey, value })
delete values[key]
} else if (/host|url|endpoint/i.test(key)) checkUrl(value, '环境变量或 Header 地址')
} else if (/host|url|endpoint/i.test(key)) checkUrl(value, t('环境变量或 Header 地址', 'Environment variable or Header URL'))
}
}
return { config, secrets }
@@ -81,7 +81,7 @@ async function loadActive() {
}
}
} catch (reason) {
if (version === loadVersion) feedback(message(reason, 'MCP 数据加载失败'))
if (version === loadVersion) feedback(message(reason, t('MCP 数据加载失败', 'Failed to load MCP data')))
} finally {
if (version === loadVersion) loading.value = false
}
@@ -94,7 +94,7 @@ async function restartHost() {
host.value = await pluginService.getPluginHostStatus(props.plugin.plugin_id)
await pluginStore.loadPlugins()
notice.value = t('MCP Host 已重启。', 'MCP Host restarted.')
} catch (reason) { feedback(message(reason, 'MCP Host 重启失败')) } finally { busy.value = '' }
} catch (reason) { feedback(message(reason, t('MCP Host 重启失败', 'Failed to restart MCP Host'))) } finally { busy.value = '' }
}
function updateValue(field: PluginSettingField, raw: string | boolean) {
values.value[field.key] = field.type === 'number' && typeof raw === 'string' ? (raw === '' ? null : Number(raw)) : raw
@@ -107,7 +107,7 @@ async function saveSettings() {
schema.value = await pluginService.updatePluginSettings(props.plugin.plugin_id, schema.value.schema_version, values.value)
values.value = { ...schema.value.values }
notice.value = t('普通设置已保存。', 'Settings saved.')
} catch (reason) { feedback(message(reason, '设置保存失败')) } finally { busy.value = '' }
} catch (reason) { feedback(message(reason, t('设置保存失败', 'Failed to save settings'))) } finally { busy.value = '' }
}
async function saveSecret(field: PluginSettingField) {
const secret = secrets.value[field.key]?.trim()
@@ -119,7 +119,7 @@ async function saveSecret(field: PluginSettingField) {
if (schema.value) schema.value.secrets[field.key] = { configured: state.configured }
secrets.value[field.key] = ''
notice.value = field.label + t('已加密保存。', ' encrypted and saved.')
} catch (reason) { feedback(message(reason, '密钥保存失败')) } finally { busy.value = '' }
} catch (reason) { feedback(message(reason, t('密钥保存失败', 'Failed to save secret'))) } finally { busy.value = '' }
}
async function deleteSecret(field: PluginSettingField) {
if (!confirm(t('删除已保存的', 'Delete saved ') + field.label + '')) return
@@ -130,7 +130,7 @@ async function deleteSecret(field: PluginSettingField) {
if (schema.value) schema.value.secrets[field.key] = { configured: state.configured }
secrets.value[field.key] = ''
notice.value = field.label + t('已删除。', ' deleted.')
} catch (reason) { feedback(message(reason, '密钥删除失败')) } finally { busy.value = '' }
} catch (reason) { feedback(message(reason, t('密钥删除失败', 'Failed to delete secret'))) } finally { busy.value = '' }
}
function properties(command: PluginCommand): Record<string, Record<string, unknown>> {
const result = command.parameters.properties
@@ -178,7 +178,7 @@ async function execute(command: PluginCommand) {
await loadActive()
notice.value = t('相关数据已刷新。', 'Related data refreshed.')
} else notice.value = t('命令执行完成。', 'Command completed.')
} catch (reason) { feedback(message(reason, '命令执行失败')) } finally { busy.value = '' }
} catch (reason) { feedback(message(reason, t('命令执行失败', 'Command failed'))) } finally { busy.value = '' }
}
</script>
@@ -54,7 +54,7 @@ async function load() {
applyResponse(routing)
conflict.value = false
} catch (reason) {
if (active) error.value = `加载失败:${reason instanceof Error ? reason.message : '无法读取模型路由或提供商'}`
if (active) error.value = `${t('加载失败:', 'Load failed: ')}${reason instanceof Error ? reason.message : t('无法读取模型路由或提供商', 'Could not read model routes or providers')}`
} finally { loading.value = false }
}
@@ -74,11 +74,11 @@ function bindingFor(capability: RoutingCapability): ModelBinding | null {
if (!draft.provider_id) return null
if (!available.value.some(provider => provider.provider_id === draft.provider_id)) throw new Error(t('请选择已启用且协议可用的提供商,或切换到本地。', 'Select an enabled provider with a supported protocol, or switch to local.'))
if (!draft.model.trim()) throw new Error(t('请填写所选 API 的模型 ID。', 'Enter the model ID for the selected API.'))
if (!/^\/[A-Za-z0-9_/-]+$/.test(draft.endpoint) || draft.endpoint.startsWith('//')) throw new Error('Endpoint 必须是以 / 开头的相对路径,只能包含字母、数字、下划线、连字符和 /。')
if (!/^\/[A-Za-z0-9_/-]+$/.test(draft.endpoint) || draft.endpoint.startsWith('//')) throw new Error(t('Endpoint 必须是以 / 开头的相对路径,只能包含字母、数字、下划线、连字符和 /。', 'Endpoint must be a relative path beginning with / and containing only letters, numbers, underscores, hyphens, and /.'))
const binding: ModelBinding = { provider_id: draft.provider_id, model: draft.model.trim(), endpoint: draft.endpoint }
if (capability === 'embedding') {
const dimension = String(draft.dimensions).trim()
if (dimension && (!/^\d+$/.test(dimension) || !Number.isSafeInteger(Number(dimension)) || Number(dimension) < 1 || Number(dimension) > 16384)) throw new Error('嵌入维度必须为 1–16384 的整数,或留空使用 API 默认值。')
if (dimension && (!/^\d+$/.test(dimension) || !Number.isSafeInteger(Number(dimension)) || Number(dimension) < 1 || Number(dimension) > 16384)) throw new Error(t('嵌入维度必须为 1–16384 的整数,或留空使用 API 默认值。', 'Embedding dimensions must be an integer from 1 to 16384, or blank to use the API default.'))
binding.dimensions = dimension ? Number(dimension) : null
}
return binding
@@ -100,8 +100,8 @@ async function save() {
if (!active) return
conflict.value = reason instanceof ApiErrorClass && /CONFLICT|VERSION|HTTP_409/i.test(reason.code)
error.value = conflict.value
? '配置版本冲突:其他窗口已修改路由。当前输入尚未保存,请重新加载最新配置后再编辑。'
: `保存失败:${reason instanceof Error ? reason.message : '请重试'}`
? t('配置版本冲突:其他窗口已修改路由。当前输入尚未保存,请重新加载最新配置后再编辑。', 'Configuration conflict: another window changed these routes. Your input is unsaved; reload the latest settings before editing.')
: `${t('保存失败:', 'Save failed: ')}${reason instanceof Error ? reason.message : t('请重试', 'please retry')}`
} finally { saving.value = false }
}
</script>
@@ -40,7 +40,7 @@ async function previewRequest() {
try {
if (!requestJsonValid.value) throw new Error(t('请先修正 JSON。', 'Fix the JSON first.'))
const response = await apiClient.post<{body:Record<string,unknown>}>('/api/providers/request-preview', {
provider: {provider_type:form.provider_type,name:form.name || '预览',base_url:form.base_url || null,
provider: {provider_type:form.provider_type,name:form.name || t('预览', 'Preview'),base_url:form.base_url || null,
default_model:form.default_model || null,request_overrides:requestOverrides.value}, stream:previewStream.value, capability:previewCapability.value,
})
if (active && generation === draftGeneration) requestPreview.value = JSON.stringify(response.body, null, 2)
@@ -54,7 +54,7 @@ async function probeRequest() {
if (!requestJsonValid.value) throw new Error(t('请先修正 JSON。', 'Fix the JSON first.'))
if (apiKey.value.trim()) throw new Error(t('请先保存新的 API Key,再进行推理验证。', 'Save the new API key before testing inference.'))
const result = await apiClient.post<{message:string}>('/api/providers/request-probe', {
provider: {provider_type:form.provider_type,name:form.name || '推理验证',base_url:form.base_url || null,
provider: {provider_type:form.provider_type,name:form.name || t('推理验证', 'Inference test'),base_url:form.base_url || null,
default_model:form.default_model || null,request_overrides:JSON.parse(JSON.stringify(requestOverrides.value)),
credential_id:configured.value ? credentialId.value : null}, stream:previewStream.value,
})
+2 -1
View File
@@ -1,4 +1,5 @@
import { apiClient, resolveApiUrl } from './apiClient'
import { t } from '@/i18n'
export interface Segment { segment_id: string; start_time: number; end_time: number; text: string; speaker: string | null; language?: string }
export interface MediaJob {
@@ -26,7 +27,7 @@ export const mediaService = {
const response = await fetch(resolveApiUrl(`/api/media/attachments?filename=${encodeURIComponent(file.name)}`), {
method: 'POST', headers: {'Content-Type': 'application/octet-stream', ...(idempotencyKey ? {'Idempotency-Key': idempotencyKey} : {})}, body: file,
})
if (!response.ok) throw new Error((await response.json())?.error?.message || '附件上传失败')
if (!response.ok) throw new Error((await response.json())?.error?.message || t('附件上传失败', 'Attachment upload failed'))
return await response.json() as {attachment_id: string}
},
}
+3 -2
View File
@@ -7,6 +7,7 @@ import type {
OperationResponse,
} from '@/contracts'
import apiClient from './apiClient'
import { t } from '@/i18n'
import * as noteService from './noteService'
/** Web 联调只连接 AI Core 配置的单一 Vault;多 Vault 选择由 Tauri Host 接管。 */
@@ -72,7 +73,7 @@ async function requireNoteId(filePath: string): Promise<string> {
await refreshTree()
noteId = noteIdByPath.get(path)
}
if (!noteId) throw new Error(`笔记尚未建立后端索引:${path}`)
if (!noteId) throw new Error(`${t('笔记尚未建立后端索引:', 'The note has not been indexed by the backend: ')}${path}`)
return noteId
}
@@ -174,7 +175,7 @@ export async function deleteFile(pathValue: string): Promise<void> {
export async function moveFile(sourcePath: string, targetPath: string): Promise<void> {
const source = normalizePublicPath(sourcePath)
if (typeByPath.get(source) !== 'file') {
throw new Error('当前阶段只支持移动笔记文件。')
throw new Error(t('当前阶段只支持移动笔记文件。', 'Only note files can be moved at this stage.'))
}
await noteService.moveNote(await requireNoteId(source), relativePath(targetPath))
await refreshTree()
+2 -1
View File
@@ -3,6 +3,7 @@ import { ref, computed } from 'vue'
import type { AgentRun, AgentEvent, ToolDefinition, PermissionRequest, ToolCall } from '@/contracts'
import * as agentService from '@/services/agentService'
import type { SseClient } from '@/services/sseClient'
import { t } from '@/i18n'
export const useAgentStore = defineStore('agent', () => {
const runs = ref<AgentRun[]>([])
@@ -93,7 +94,7 @@ export const useAgentStore = defineStore('agent', () => {
tool_name: String(call.name ?? 'unknown'),
permission: String(data.permission ?? ''),
parameters: (call.arguments ?? {}) as Record<string, unknown>,
impact: '该工具需要获得权限后才能继续执行。',
impact: t('该工具需要获得权限后才能继续执行。', 'This tool requires permission before it can continue.'),
}
if (run) run.status = 'waiting_permission'
} else if (['RunCompleted', 'RunFailed', 'RunCancelled'].includes(event.event)) {
+4 -3
View File
@@ -3,6 +3,7 @@ import { ref, computed, reactive } from 'vue'
import type { ChatMessage, Conversation } from '@/contracts'
import { streamChat } from '@/services/chatService'
import type { SseClient } from '@/services/sseClient'
import { t } from '@/i18n'
export const useChatStore = defineStore('chat', () => {
const conversations = ref<Conversation[]>([])
@@ -128,11 +129,11 @@ export const useChatStore = defineStore('chat', () => {
content: String(event.data.content ?? event.data.snippet ?? ''),
})
}
if (event.event === 'Error') aiMsg.content += `\n\n生成失败:${String(event.data.message ?? '未知错误')}`
if (event.event === 'Error') aiMsg.content += `\n\n${t('生成失败:', 'Generation failed: ')}${String(event.data.message ?? t('未知错误', 'Unknown error'))}`
},
onError(error) {
if (version !== streamVersion) return
aiMsg.content += `\n\n连接失败:${error.message}`
aiMsg.content += `\n\n${t('连接失败:', 'Connection failed: ')}${error.message}`
isStreaming.value = false
sseClient = null
},
@@ -162,7 +163,7 @@ export const useChatStore = defineStore('chat', () => {
stopGeneration()
const newConv: Conversation = {
conversation_id: crypto.randomUUID(),
title: '新对话',
title: t('新对话', 'New conversation'),
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
message_count: 0,
+3 -2
View File
@@ -2,6 +2,7 @@ import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { SaveStatus } from '@/contracts'
import * as workspaceService from '@/services/workspaceService'
import { t } from '@/i18n'
export const useEditorStore = defineStore('editor', () => {
const mode = ref<'wysiwyg' | 'source'>('wysiwyg')
@@ -76,12 +77,12 @@ export const useEditorStore = defineStore('editor', () => {
saveTimer = null
}
if (saveStatus.value === 'conflict') {
throw new Error('当前文件存在编辑冲突,请处理后再切换文件。')
throw new Error(t('当前文件存在编辑冲突,请处理后再切换文件。', 'The current file has an editing conflict. Resolve it before switching files.'))
}
if (pendingSave) await pendingSave
if (saveStatus.value === 'dirty' || saveStatus.value === 'save_failed') await save()
if (saveStatus.value === 'dirty' || saveStatus.value === 'save_failed') {
throw new Error('当前文件保存失败,已阻止切换以避免内容丢失。')
throw new Error(t('当前文件保存失败,已阻止切换以避免内容丢失。', 'The current file could not be saved. Switching was blocked to prevent data loss.'))
}
// 版本号使较慢的旧读取不能覆盖用户后选择的新文件。
const version = ++loadVersion
+2 -1
View File
@@ -2,6 +2,7 @@ import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { Plugin } from '@/contracts'
import * as pluginService from '@/services/pluginService'
import { t } from '@/i18n'
export const usePluginStore = defineStore('plugin', () => {
const plugins = ref<Plugin[]>([])
@@ -23,7 +24,7 @@ export const usePluginStore = defineStore('plugin', () => {
plugins.value = await pluginService.listPlugins()
error.value = null
} catch (reason) {
error.value = reason instanceof Error ? reason.message : 'Plugin 加载失败'
error.value = reason instanceof Error ? reason.message : t('Plugin 加载失败', 'Failed to load Plugins')
} finally {
isLoading.value = false
}
+6 -5
View File
@@ -3,6 +3,7 @@ import { ref, computed } from 'vue'
import type { ProviderConfig, ModelInfo, ProviderPreset } from '@/contracts'
import { createProvider, deleteProvider as deleteProviderRequest, getCredentialStatus, listModels, listProviderPresets, listProviders, putCredential, testProvider as testProviderRequest, updateProvider as updateProviderRequest } from '@/services/providerService'
import { ApiErrorClass } from '@/services/apiClient'
import { t } from '@/i18n'
export const useProviderStore = defineStore('provider', () => {
const providers = ref<ProviderConfig[]>([])
@@ -29,7 +30,7 @@ export const useProviderStore = defineStore('provider', () => {
}
error.value = null
} catch (reason) {
error.value = reason instanceof Error ? reason.message : 'Provider 加载失败'
error.value = reason instanceof Error ? reason.message : t('Provider 加载失败', 'Failed to load Providers')
} finally {
isLoading.value = false
}
@@ -39,7 +40,7 @@ export const useProviderStore = defineStore('provider', () => {
try {
presets.value = await listProviderPresets()
} catch (reason) {
error.value = reason instanceof Error ? reason.message : 'Provider 预设加载失败'
error.value = reason instanceof Error ? reason.message : t('Provider 预设加载失败', 'Failed to load Provider presets')
}
}
@@ -55,11 +56,11 @@ export const useProviderStore = defineStore('provider', () => {
} catch (reason) {
const provider = providers.value.find((item) => item.provider_id === providerId)
const credentialId = provider?.credential_id
let message = reason instanceof Error ? reason.message : '模型列表获取失败'
let message = reason instanceof Error ? reason.message : t('模型列表获取失败', 'Failed to load the model list')
if (reason instanceof ApiErrorClass && reason.code === 'PROVIDER_CREDENTIAL_MISSING') {
message = '尚未配置 API Key,请编辑该 Provider 后填写并保存。'
message = t('尚未配置 API Key,请编辑该 Provider 后填写并保存。', 'No API key is configured. Edit this Provider, enter a key, and save it.')
} else if (reason instanceof ApiErrorClass && reason.code === 'PROVIDER_AUTH_FAILED') {
message = `鉴权失败,请检查凭据“${credentialId || '未设置'}”对应的 API Key 是否有效。`
message = t(`鉴权失败,请检查凭据“${credentialId || '未设置'}”对应的 API Key 是否有效。`, `Authentication failed. Check the API key for credential “${credentialId || 'not set'}”.`)
}
modelErrorsByProvider.value[providerId] = message
throw reason
+5 -4
View File
@@ -3,6 +3,7 @@ import { ref } from 'vue'
import type { SearchResult, SearchRequest } from '@/contracts'
import * as searchService from '@/services/searchService'
import { ApiErrorClass } from '@/services/apiClient'
import { t } from '@/i18n'
const VECTOR_ERROR_CODES = new Set([
'SEMANTIC_INDEX_UNAVAILABLE',
@@ -28,7 +29,7 @@ export const useSearchStore = defineStore('search', () => {
if (version !== historyVersion) return
recentQueries.value = response.queries
historyError.value = ''
} catch { if (version === historyVersion) historyError.value = '无法读取应用搜索记录,请检查后端连接。' }
} catch { if (version === historyVersion) historyError.value = t('无法读取应用搜索记录,请检查后端连接。', 'Could not load search history. Check the backend connection.') }
}
async function clearHistory() {
const version = ++historyVersion
@@ -36,7 +37,7 @@ export const useSearchStore = defineStore('search', () => {
await searchService.clearHistory()
if (version !== historyVersion) return
recentQueries.value = []; historyError.value = ''
} catch { if (version === historyVersion) historyError.value = '清空搜索记录失败,请重试。' }
} catch { if (version === historyVersion) historyError.value = t('清空搜索记录失败,请重试。', 'Failed to clear search history. Please retry.') }
}
const error = ref<string | null>(null)
const vectorUnavailable = ref(false)
@@ -71,12 +72,12 @@ export const useSearchStore = defineStore('search', () => {
selectedIndex.value = 0
} catch (fallbackError) {
if (version !== searchVersion) return
error.value = fallbackError instanceof Error ? fallbackError.message : '全文检索降级失败'
error.value = fallbackError instanceof Error ? fallbackError.message : t('全文检索降级失败', 'Full-text search fallback failed')
results.value = []
total.value = 0
}
} else {
error.value = reason instanceof Error ? reason.message : '搜索失败'
error.value = reason instanceof Error ? reason.message : t('搜索失败', 'Search failed')
results.value = []
total.value = 0
}
+5 -5
View File
@@ -5,7 +5,7 @@ import { resolveApiUrl } from '@/services/apiClient'
import packageInfo from '../../package.json'
import * as indexService from '@/services/indexService'
import * as systemService from '@/services/systemService'
import { appLocale } from '@/i18n'
import { appLocale, t } from '@/i18n'
export const useSettingsStore = defineStore('settings', () => {
const saved = (() => {
@@ -17,7 +17,7 @@ export const useSettingsStore = defineStore('settings', () => {
const autoSaveInterval = ref(typeof saved.autoSaveInterval === 'number' ? saved.autoSaveInterval : 1500)
const language = appLocale
const appVersion = ref(packageInfo.version)
const aiCoreVersion = ref('未获取')
const aiCoreVersion = ref('')
// Editor
const defaultEditorMode = ref<'wysiwyg' | 'source'>(saved.defaultEditorMode === 'source' ? 'source' : 'wysiwyg')
@@ -48,10 +48,10 @@ export const useSettingsStore = defineStore('settings', () => {
])
const [health, status, index, policy] = results
aiCoreStatus.value = health.status === 'fulfilled' && health.value.status === 'ok' ? 'running' : 'error'
aiCoreVersion.value = status.status === 'fulfilled' ? status.value.version : '未获取'
aiCoreVersion.value = status.status === 'fulfilled' ? status.value.version : ''
indexStatus.value = index.status === 'fulfilled' ? index.value : emptyIndex()
permissionPolicy.value = policy.status === 'fulfilled' ? policy.value : {}
diagnosticsError.value = results.filter(item => item.status === 'rejected').map(item => item.reason instanceof Error ? item.reason.message : '后端请求失败').join('') || null
diagnosticsError.value = results.filter(item => item.status === 'rejected').map(item => item.reason instanceof Error ? item.reason.message : t('后端请求失败', 'Backend request failed')).join(t('', '; ')) || null
}
function setAutoSaveInterval(ms: number) {
@@ -69,7 +69,7 @@ export const useSettingsStore = defineStore('settings', () => {
indexStatus.value = await indexService.getIndexStatus()
} catch (reason) {
indexStatus.value.status = 'error'
indexStatus.value.error = reason instanceof Error ? reason.message : '索引重建失败'
indexStatus.value.error = reason instanceof Error ? reason.message : t('索引重建失败', 'Index rebuild failed')
}
}
+2 -1
View File
@@ -2,6 +2,7 @@ import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { Skill } from '@/contracts'
import * as skillService from '@/services/skillService'
import { t } from '@/i18n'
export const useSkillStore = defineStore('skill', () => {
const skills = ref<Skill[]>([])
@@ -23,7 +24,7 @@ export const useSkillStore = defineStore('skill', () => {
skills.value = await skillService.listSkills()
error.value = null
} catch (reason) {
error.value = reason instanceof Error ? reason.message : 'Skill 加载失败'
error.value = reason instanceof Error ? reason.message : t('Skill 加载失败', 'Failed to load Skills')
} finally {
isLoading.value = false
}
+2 -1
View File
@@ -2,6 +2,7 @@ import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { TaskItem, TaskStatus, TaskPriority, TaskSource } from '@/contracts'
import { createTask as createTaskRequest, deleteTask as deleteTaskRequest, listTasks, updateTask as updateTaskRequest } from '@/services/taskService'
import { t } from '@/i18n'
export const useTaskStore = defineStore('task', () => {
const tasks = ref<TaskItem[]>([])
@@ -31,7 +32,7 @@ export const useTaskStore = defineStore('task', () => {
tasks.value = resp.items
error.value = null
} catch (reason) {
error.value = reason instanceof Error ? reason.message : '任务加载失败'
error.value = reason instanceof Error ? reason.message : t('任务加载失败', 'Failed to load tasks')
} finally {
isLoading.value = false
}