Files

1 line
131 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "bobzhou-source/parsifal-demo", "files": {"packages/client/src/i18n/locales/zh-TW.ts": {"09d34511c5561945f8be97fc0b1ffaaadda18636": {"t": 1785595303.6996555, "n": 69580, "h": "460e2c4daae342d5", "c": "export default {\n // 登入\n login: {\n title: 'Parsifal Web UI',\n description: '輸入使用者名稱和密碼以繼續。',\n placeholder: '存取權杖',\n submit: '登入',\n tokenRequired: '請輸入存取權杖',\n invalidToken: '權杖無效',\n connectionFailed: '無法連線至伺服器',\n passwordLogin: '密碼登入',\n tokenLogin: '權杖登入',\n usernamePlaceholder: '使用者名稱',\n passwordPlaceholder: '密碼',\n defaultCredentialsHint: '預設登入名:admin,預設密碼:123456',\n credentialsRequired: '請輸入使用者名稱和密碼',\n invalidCredentials: '使用者名稱或密碼錯誤',\n tooManyAttempts: '登入失敗次數過多,請稍後再試',\n lockResetHint: '如果這是你的伺服器,可以執行以下命令清除登入鎖定:',\n defaultLoginResetHint: '如需重置預設 admin 密碼,可以執行:',\n sessionExpired: '登入已過期,請重新登入',\n accessDenied: '你沒有權限存取此資源',\n passwordMismatch: '兩次密碼不一致',\n passwordTooShort: '密碼長度至少 6 個字元',\n setupSuccess: '密碼登入設定成功',\n passwordChanged: '密碼修改成功',\n passwordRemoved: '密碼登入已移除',\n setupPassword: '設定密碼登入',\n changePassword: '修改密碼',\n changeUsername: '修改使用者名稱',\n removePasswordLogin: '移除',\n username: '使用者名稱',\n currentPassword: '目前密碼',\n newPassword: '新密碼',\n confirmPassword: '確認密碼',\n newUsername: '新使用者名稱',\n usernameChanged: '使用者名稱修改成功',\n usernameTooShort: '使用者名稱至少 2 個字元',\n setupDescription: '管理用於登入的使用者名稱和密碼。',\n removeConfirm: '使用者帳號必須保留密碼登入。',\n passwordLoginNotConfigured: '密碼登入未設定',\n passwordLoginConfigured: '目前帳號:{username}',\n defaultCredentialTitle: '請修改預設帳號和密碼',\n defaultCredentialMessage: '目前登入帳號仍在使用預設使用者名稱或預設密碼。為避免未授權存取,請盡快進入目前帳號修改使用者名稱和密碼。',\n defaultCredentialAction: '去修改',\n defaultCredentialLater: '稍後提醒',\n },\n\n users: {\n title: '帳號管理',\n description: '建立使用者、分配角色,並控制一般管理員可存取的設定檔。',\n create: '建立使用者',\n edit: '編輯使用者',\n username: '使用者名稱',\n role: '角色',\n statusLabel: '狀態',\n profiles: '可存取設定檔',\n profilesPlaceholder: '選擇可存取的設定檔',\n allProfiles: '全部設定檔',\n noProfiles: '未關聯設定檔',\n lastLogin: '最後登入',\n newPasswordOptional: '新密碼(留空不修改)',\n loadFailed: '使用者列表載入失敗',\n deleteConfirm: '確認刪除此使用者?',\n enable: '啟用',\n disable: '停用',\n roles: {\n superAdmin: '超級管理員',\n admin: '一般管理員',\n },\n status: {\n active: '啟用',\n disabled: '停用',\n },\n },\n\n // 通用\n common: {\n loading: '載入中...',\n cancel: '取消',\n delete: '刪除',\n retry: '重試',\n edit: '編輯',\n save: '儲存',\n saved: '已儲存',\n saveFailed: '儲存失敗',\n deleteFailed: '刪除失敗',\n ok: '確定',\n copied: '已複製',\n copy: '複製',\n update: '更新',\n create: '建立',\n noData: '目前無資料',\n expired: '已過期',\n fetch: '取得',\n add: '新增',\n enable: '啟用',\n disable: '停用',\n configured: '已設定',\n notConfigured: '未設定',\n confirm: '確定',\n expand: '展開',\n collapse: '收起',\n start: '啟動',\n stop: '停止',\n },\n\n // 側邊欄\n // MCP 管理\n mcp: {\n title: 'MCP 伺服器',\n loadFailed: '載入 MCP 伺服器失敗',\n reloadAll: '全部重載',\n refresh: '重新整理',\n total: '總計',\n connected: '已連線',\n disconnected: '未連線',\n tools: '工具',\n tool: '工具',\n searchPlaceholder: '搜尋伺服器...',\n addServer: '+ 新增伺服器',\n zeroTools: '0 個工具',\n loading: '載入中...',\n empty: '暫無 MCP 伺服器設定',\n reloaded: '已重載 {server}',\n reloadedAll: '所有 MCP 伺服器已重載',\n reloadFailed: '重載失敗',\n serverAdded: '伺服器 \"{name}\" 已新增',\n addFailed: '新增伺服器失敗',\n serverUpdated: '伺服器 \"{name}\" 已更新',\n updateFailed: '更新伺服器失敗',\n saveFailed: '儲存失敗',\n serverRemoved: '已移除 \"{name}\"',\n enabled: \"已啟用 {name}\",\n disabled: \"已禁用 {name}\",\n connectedStatus: '已連線',\n disconnectedStatus: '未連線',\n disabledStatus: '已停用',\n toolList: '工具列表',\n count: '個',\n more: '更多',\n removeFailed: '移除伺服器失敗',\n testOk: '測試成功 — {count} 個工具可用',\n testEmpty: '測試未回傳工具',\n testFailed: '測試失敗',\n edit: '編輯',\n test: '測試',\n reload: '重載',\n remove: '移除',\n confirmRemove: '確認刪除伺服器 \"{name}\"?',\n cancel: '取消',\n add: '新增',\n save: '儲存',\n addTitle: '新增 MCP 伺服器',\n editTitle: '編輯 MCP 伺服器',\n invalidJson: 'JSON 格式錯誤',\n invalidYaml: 'YAML 格式錯誤',\n invalidConfig: '配置格式錯誤',\n invalidServerConfig: '伺服器配置無效',\n missingCommandOrUrl: '必須包含 command 或 url',\n manageTools: '管理工具',\n toolsVisibilityTitle: '工具可見性管理',\n fetchTools: '獲取工具列表',\n fetchToolsFailed: '獲取工具列表失敗',\n toolsMode: '模式:',\n toolsModeAll: '全部',\n toolsModeInclude: '包含',\n toolsModeExclude: '排除',\n toolsListHeader: '工具名稱',\n toolsEmpty: '暫無工具,請先獲取工具列表',\n toolsSummaryAll: '共 {count} 個工具,全部啟用',\n toolsSummaryInclude: '共 {total} 個工具,已選 {count} 個',\n toolsSummaryExclude: '共 {total} 個工具,已排除 {count} 個',\n toolsVisibilitySaved: '工具可見性已儲存',\n toolsSelectAll: '全選',\n toolsClearSelection: '取消全選',\n toolsExcludeAll: '全部排除',\n toolsClearExcluded: '清空排除',\n },\n\n sidebar: {\n chat: '對話',\n backToChat: '返回',\n search: '搜尋',\n apiRelay: '中轉站',\n history: '歷史',\n jobs: '任務',\n kanban: '任務板',\n models: '模型',\n profiles: '使用者',\n plugins: '插件',\n mcp: 'MCP',\n skills: '技能',\n memory: '記憶',\n logs: '日誌',\n usage: '用量',\n performance: '效能監控',\n skillsUsage: '技能用量',\n channels: '頻道',\n gateways: '閘道',\n terminal: '終端機',\n singleChat: '單聊',\n globalAgent: '全局',\n groupChat: '群聊',\n files: '檔案',\n devices: '裝置',\n groupConversation: '對話',\n groupConversationShort: '對話',\n groupPlatform: '平台',\n groupAgent: '代理',\n groupAgentShort: '代理',\n groupSystem: '系統',\n groupSystemShort: '系統',\n groupMonitoring: '監控',\n groupMonitoringShort: '監控',\n groupTools: '工具',\n groupToolsShort: \"工具\",\n codingAgents: \"編程工具\",\n versionPreview: \"版本預覽\",\n settings: '設定',\n connected: '已連線',\n disconnected: '未連線',\n collapse: '收起選單',\n expand: '展開選單',\n updateTip: '在終端機執行 \"parsifal-web-ui update\" 即可更新',\n updateVersion: '升級版本 v{version}',\n reloadClientVersion: '重新整理到 v{version}',\n updating: '正在更新...',\n updateSuccess: '更新成功,請稍後重新整理頁面,如長時間未啟動,請手動啟動',\n updateFailed: '更新失敗',\n logout: '登出',\n nodeVersionWarning: '偵測到 Node.js v{version},請升級至 23 以上版本。',\n changelog: '更新日誌',\n noChangelog: '目前無更新日誌',\n versionManagement: '版本管理',\n },\n\n runtimeVersions: {\n title: '版本管理',\n runtimeTitle: 'Parsifal Runtime',\n webUiTitle: 'Web UI',\n platform: '平台',\n currentWebUi: '目前 Web UI',\n activeVersion: '目前啟用版本',\n active: '已啟用',\n installed: '已安裝',\n download: '下載',\n downloadGithub: 'GitHub 下載',\n downloadCf: 'Cloudflare 下載',\n github: 'GitHub',\n cf: 'Cloudflare',\n useVersion: '下次啟動使用',\n deleteVersion: '刪除',\n deleteRuntimeConfirm: '刪除 Parsifal Runtime {version}?',\n deleteWebUiConfirm: '刪除 Web UI {version}?',\n deleteRuntimeSuccess: 'Runtime 已刪除',\n deleteWebUiSuccess: 'Web UI 已刪除',\n refresh: '重新整理',\n noVersions: '暫無可用版本',\n remoteLoadFailed: '遠端版本載入失敗',\n downloadTasks: '下載任務',\n downloadStarted: '已開始下載',\n activateSuccess: '版本選擇已儲存,重啟 Parsifal Studio 後生效。',\n jobStatus: {\n queued: '排隊中',\n running: '下載中',\n completed: '已完成',\n failed: '失敗',\n },\n jobStageStatus: {\n queued: '排隊中',\n resolve: '解析中',\n download: '下載中',\n verify: '校驗中',\n extract: '解壓中',\n install: '安裝中',\n completed: '已完成',\n failed: '失敗',\n },\n jobStage: {\n queued: '排隊中',\n download: '下載中',\n resolveRuntime: '正在解析 Runtime 包',\n verifyRuntime: '正在校驗 Runtime 壓縮包',\n extractRuntime: '正在解壓 Runtime 壓縮包',\n installRuntime: '正在安裝 Runtime',\n resolveWebUi: '正在解析 Web UI 包',\n verifyWebUi: '正在校驗 Web UI 壓縮包',\n extractWebUi: '正在解壓 Web UI 壓縮包',\n installWebUi: '正在安裝 Web UI',\n completed: '已完成',\n failed: '失敗',\n },\n },\n\n devices: {\n title: '裝置',\n count: '{count} 台裝置',\n lastScanned: '上次掃描:{time}',\n refresh: '重新整理',\n empty: '暫無裝置',\n computer: '電腦',\n endpointLabel: '端',\n statusLabel: '狀態',\n online: '線上',\n offline: '離線',\n address: '位址',\n os: '系統',\n agentVersion: 'Agent',\n webUiVersion: 'Web UI',\n responseMs: '延遲',\n lastSeen: '最後發現',\n open: '開啟',\n never: '從未',\n unknown: '未知',\n loadFailed: '載入裝置失敗',\n scanFailed: '掃描裝置失敗',\n updateFailed: '更新裝置失敗',\n duplicateRequest: '已存在待處理的配對申請',\n manualPairing: '遠端配對',\n copyPairingLink: '複製配對連結',\n manualUrlPlaceholder: 'https://device.example.com',\n manualUrlRequired: '請輸入裝置連結',\n manualRequestSent: '配對申請已送出',\n manualRequestFailed: '遠端配對申請失敗',\n pairingLinkCopied: '配對連結已複製',\n pairingLinkCopyFailed: '複製配對連結失敗',\n pairingCodeTitle: '輸入配對碼',\n pairingCodePlaceholder: '配對碼',\n pairingCodeRequired: '請輸入配對碼',\n submitPairingRequest: '提交申請',\n processed: '已處理',\n unprocessed: '未處理',\n deleteHistory: '刪除記錄',\n deleteHistoryConfirm: '確認刪除這條申請記錄並撤銷該裝置連線權限?',\n requests: '申請列表',\n requestsWithCount: '申請列表({count})',\n noRequests: '暫無申請',\n requestPairing: '申請配對',\n paired: '已配對',\n approve: '通過',\n reject: '拒絕',\n block: '拉黑',\n unblock: '解除拉黑',\n inboundStatus: {\n none: '允許申請',\n pending: '待處理',\n approved: '已通過',\n rejected: '已拒絕',\n blocked: '黑名單',\n },\n outboundStatus: {\n none: '未配對',\n pending: '已申請',\n approved: '已配對',\n rejected: '已拒絕',\n blocked: '被拉黑',\n },\n endpoint: {\n web: 'Web 端',\n desktop: '桌面端',\n custom: '自訂',\n },\n },\n\n performance: {\n title: '效能監控',\n subtitle: '查看系統資源、Bridge Broker、Workers 和活躍會話',\n refresh: '重新整理',\n autoRefreshOn: '自動重新整理',\n autoRefreshOff: '手動重新整理',\n loadFailed: '效能資料載入失敗',\n systemCpu: '系統 CPU',\n systemMemory: '系統記憶體',\n activeSessions: '活躍會話',\n runningSessions: '執行中 {count}',\n workers: 'Workers',\n totalWorkerMemory: 'Worker 總記憶體',\n processes: '程序',\n uptime: '執行',\n running: '執行中',\n stopped: '已停止',\n workerMemory: 'Worker 記憶體',\n lastUpdated: '更新時間',\n profile: 'Profile',\n memory: '記憶體',\n sessions: '會話',\n runningActiveSessions: '執行中 / 活躍',\n lastUsed: '最後使用',\n status: '狀態',\n noWorkers: '暫無 Worker',\n sessionsByProfile: '按 Profile 統計會話',\n noActiveSessions: '暫無活躍會話',\n },\n\n // 抽屜\n drawer: {\n terminal: '終端機',\n files: '工作區',\n powerbi: 'Power BI',\n },\n\n powerbi: {\n title: 'Power BI Report',\n loadFailed: 'Failed to load Power BI reports',\n empty: 'No PBIP/PBIR reports found in this workspace',\n progress: 'MCP Progress',\n live: 'Live',\n event: 'Event',\n run: 'Run',\n project: 'Project',\n pages: 'Pages',\n visuals: 'Visuals',\n tables: 'Tables',\n modelFields: 'Model fields',\n artifacts: 'Artifacts',\n download: 'Download',\n semanticModel: 'Semantic model',\n modelSummary: '{columns} columns / {measures} measures',\n tableMeta: '{columns} columns / {measures} measures',\n filesCount: '{count} files',\n copyFailed: 'Copy failed',\n },\n\n // 對話\n chat: {\n contextRemaining: '剩餘',\n contextClickToEdit: '點擊編輯上下文長度',\n contextEditTitle: '編輯上下文長度',\n contextEditDesc: '設定目前模型的上下文長度限制(token 數量)',\n contextEditPlaceholder: '請輸入上下文長度',\n contextEditHint: '常見值:256k (Parsifal 預設), 128k (GPT-4), 32k (GPT-3.5)',\n contextEditSave: '儲存',\n contextEditCancel: '取消',\n contextEditInvalid: '請輸入有效的上下文長度',\n contextEditSuccess: '上下文長度已更新',\n contextEditFailed: '更新失敗',\n emptyState: '開始與 Parsifal Agent 對話',\n emptyStateAgent: '開始與 {agent} 對話',\n viewOlderInHistory: '更早的訊息請到歷史記錄查看',\n scrollToBottom: '捲動到底部',\n outlineTitle: '會話大綱',\n outlineEmpty: '暫無會話內容',\n outlineUserQuestion: '使用者問題',\n inputPlaceholder: '輸入訊息... (Enter 發送,Shift+Enter 換行)',\n slashCommandArgs: {\n message: '<訊息>',\n title: '<標題>',\n text: '<文字>',\n },\n slashCommands: {\n usage: '計算目前會話用量',\n status: '查看會話狀態和佇列',\n abort: '停止目前 Bridge 執行',\n queue: '將訊息加入目前執行後的佇列',\n plan: '產生一份 Markdown 實作計畫',\n goal: '設定一個跨輪次持續推進的目標',\n goalStatus: '查看目前目標狀態',\n goalPause: '暫停目前目標循環',\n goalResume: '繼續已暫停的目標循環',\n goalDone: '完成並清除目前目標',\n goalClear: '清除目前目標',\n subgoal: '為目前目標追加驗收條件',\n clear: '清空目前顯示內容',\n clearHistory: '刪除目前會話已儲存的訊息歷史',\n title: '重新命名目前會話',\n compress: '空閒時觸發上下文壓縮',\n steer: '向目前 Bridge 執行傳送引導文字',\n destroy: '釋放目前會話的 Bridge Agent',\n reloadMcp: '重載 MCP 伺服器',\n reloadSkills: '重載技能命令',\n },\n attachFiles: '新增附件',\n reasoningEffort: {\n tooltip: '推理強度',\n defaultLabel: '預設',\n options: {\n default: '預設 (config.yaml)',\n none: '無',\n minimal: '極低',\n low: '低',\n medium: '中',\n high: '高',\n xhigh: '超高',\n },\n },\n autoPlaySpeech: '自動播放語音',\n voiceInput: {\n startCapture: '開始語音輸入',\n stopCaptureAndInsert: '停止語音輸入並插入到輸入框',\n cancelCapture: '取消語音輸入',\n statusLabel: '狀態:{status}',\n transcriptLabel: '轉寫:{text}',\n errorLabel: '錯誤:{error}',\n recentEvents: '最近事件',\n status: {\n idle: '閒置',\n capturing: '正在聆聽',\n transcribing: '正在轉寫',\n sending: '正在插入輸入框',\n error: '錯誤',\n },\n browserSpeechUnsupported: '目前瀏覽器不支援內建語音辨識。',\n browserSpeechFailed: '瀏覽器語音辨識失敗。',\n browserSpeechFailedWithReason: '瀏覽器語音辨識失敗:{error}。',\n microphoneUnsupported: '目前瀏覽器不支援麥克風錄音。',\n microphoneRecordingFailed: '麥克風錄音失敗。',\n },\n showToolCalls: '顯示工具呼叫',\n hideToolCalls: '隱藏工具呼叫',\n messageQueue: '訊息佇列',\n removeQueuedMessage: '移除佇列訊息',\n stop: '停止',\n start: '啟動',\n stopGateway: '停止閘道',\n send: '發送',\n contextUsed: '上下文已用:',\n sessions: '工作階段',\n webUiSessions: '工作階段',\n allProfiles: '全部設定',\n profileMissingModelsTip: '此工作階段所屬設定「{profile}」沒有可用的 provider 或模型',\n sessionScopeHint: '這裡只顯示目前工作階段;CLI、Telegram、Discord、Cron 等頻道工作階段在歷史中以唯讀方式查看。',\n openHistory: '開啟歷史',\n parsifalHistory: 'Parsifal 歷史',\n historyScopeHint: '這裡按來源以唯讀方式查看目前 profile 的 Parsifal 歷史工作階段。',\n noSessions: '目前無工作階段',\n searchTitle: '搜尋工作階段',\n searchSubtitle: '依標題或訊息內容搜尋',\n searchScope: '搜尋範圍:僅 Web UI 本地工作階段資料庫;不包含唯讀 Parsifal 歷史工作階段。',\n searchHint: 'Cmd/Ctrl+K',\n searchPlaceholder: '搜尋工作階段...',\n searchEmpty: '最近工作階段',\n searchRecent: '最近工作階段',\n searchNoResults: '沒有符合的工作階段',\n searchNoSnippet: '沒有可顯示的摘要',\n searchEnterHint: 'Enter 開啟 · Esc 關閉',\n searchFailed: '搜尋工作階段失敗',\n newChat: '新增對話',\n agent: 'Agent',\n approvalKicker: '終端授權',\n approvalTitle: '執行前請確認命令',\n approvalAgree: '同意',\n approvalAllowOnce: '僅本次允許',\n approvalAllowSession: '本工作階段允許',\n approvalAlways: '永遠允許',\n approvalDeny: '拒絕',\n clarifyKicker: 'Agent 需要確認',\n clarifyTitle: 'Agent 有一個問題需要您回答',\n clarifyPlaceholder: '輸入你的回答...',\n clarifySubmit: '回覆',\n clarifyDismiss: '忽略',\n deleteSession: '確定刪除此工作階段?',\n sessionDeleted: '工作階段已刪除',\n toggleBatchMode: '批次選取',\n selectAll: '全選',\n confirmBatchDelete: '確定刪除選取的 {count} 個工作階段?',\n batchDeleteSuccess: '已刪除 {count} 個工作階段',\n batchDeletePartial: '{failed} 個工作階段刪除失敗',\n batchDeleteFailed: '批次刪除失敗',\n importToWebUi: '匯入到 Web UI',\n importSessionSuccess: '工作階段已匯入 Web UI',\n importSessionAlreadyExists: '工作階段已存在於 Web UI',\n importSessionFailed: '匯入工作階段失敗',\n rename: '重新命名',\n pin: '釘選',\n unpin: '取消釘選',\n pinned: '已釘選',\n chatMode: '聊天',\n liveMode: '即時',\n liveSessions: '即時工作階段',\n recentBadge: '最近',\n linkedSessions: '關聯 {count} 個工作階段',\n noVisibleMessages: '沒有人類可見訊息。',\n monitorRoleUser: '使用者',\n monitorRoleAssistant: '助手',\n copySessionLink: '複製工作階段連結',\n openSessionInNewTab: '在新分頁開啟',\n sessionLinkCopied: 'Session link copied',\n copySessionId: '複製工作階段 ID',\n export: '匯出',\n exportFull: '完整匯出 (JSON)',\n exportCompressed: '壓縮匯出 (TXT)',\n exportCompressing: '正在壓縮上下文,請稍候...',\n exportSuccess: '工作階段已匯出',\n exportFailed: '匯出失敗',\n renamed: '已重新命名',\n renameFailed: '重新命名失敗',\n renameSession: '重新命名工作階段',\n sessionNotFound: '找不到工作階段',\n enterNewTitle: '輸入新標題',\n workspace: '工作區',\n setWorkspace: '設定工作區',\n setWorkspaceTitle: '設定工作階段工作區',\n workspacePlaceholder: '輸入專案路徑,例如 /home/user/project',\n folderPickerEmpty: '(空)',\n folderPickerNoFolders: '暫無工作區資料夾',\n folderPickerSelected: '已選擇:',\n workspaceSet: '工作區已設定',\n workspaceSetFailed: '設定工作區失敗',\n other: '其他',\n runFailed: '執行失敗',\n error: '錯誤',\n tool: '工具',\n arguments: '參數',\n result: '結果',\n truncated: '... (已截斷)',\n unchangedLines: '{count} 行未修改',\n executionDuration: '執行時長',\n thinkingLabel: '思考過程',\n thinkingInProgress: '正在思考',\n thinkingShow: '展開思考過程',\n thinkingHide: '收起思考過程',\n thinkingDuration: '已觀察 {duration}',\n thinkingChars: '{count} 字',\n copyBubble: '複製訊息',\n copiedBubble: '已複製',\n copyFailed: '複製失敗',\n playSpeech: '播放語音',\n pauseSpeech: '暫停',\n resumeSpeech: '繼續',\n stopSpeech: '停止',\n speechNotSupported: '此瀏覽器不支援語音播放',\n modelSetFailed: '設定模型失敗',\n modelSet: '模型已設定',\n setModelTitle: '設定工作階段模型',\n setModel: '設定模型',\n newCliChat: '新增 CLI',\n cliEmptyState: '開始 CLI 對話',\n },\n\n // 看板\n kanban: {\n title: '任務板',\n createTask: '新增任務',\n noTasks: '目前無任務',\n allStatuses: '所有狀態',\n allAssignees: '所有負責人',\n board: {\n create: '新增看板',\n archive: '封存看板',\n slugPlaceholder: '看板識別碼,例如 project-a',\n namePlaceholder: '顯示名稱(選填)',\n slugRequired: '看板識別碼不能為空',\n created: '看板已建立',\n archived: '看板已封存',\n archiveConfirm: '確定封存目前看板?',\n },\n columns: {\n triage: '待分類',\n todo: '待辦',\n scheduled: '已排程',\n ready: '就緒',\n running: '進行中',\n blocked: '阻塞',\n review: '待審查',\n done: '已完成',\n archived: '已封存',\n },\n form: {\n title: '標題',\n titlePlaceholder: '任務標題',\n titleRequired: '標題不能為空',\n body: '描述',\n bodyPlaceholder: '任務描述(選填)',\n assignee: '負責人',\n selectAssignee: '選擇負責人...',\n priority: '優先級',\n selectPriority: '選擇優先級...',\n tenant: '租戶',\n tenantPlaceholder: '租戶(選填)',\n workspace: '工作區',\n workspaceScratch: '臨時工作區',\n workspaceDir: '使用目錄',\n workspaceWorktree: '使用 worktree',\n workspacePath: '工作區路徑',\n workspacePathPlaceholder: '絕對路徑(選填)',\n branch: '分支',\n branchPlaceholder: 'Worktree 分支',\n skills: '技能',\n skillsPlaceholder: '選擇技能...',\n triage: '建立到分類狀態',\n goalMode: '目標模式',\n maxRuntime: '最長執行時間',\n maxRuntimePlaceholder: '例如 30m、2h',\n maxRetries: '最大重試次數',\n maxRetriesPlaceholder: '重試次數',\n goalMaxTurns: '目標最大輪次',\n goalMaxTurnsPlaceholder: '最大輪次',\n },\n card: {\n assigneeTooltip: '負責人',\n priority: {\n low: '低',\n medium: '中',\n high: '高',\n },\n timeAgo: {\n justNow: '剛剛',\n minutes: '{count} 分鐘前',\n hours: '{count} 小時前',\n days: '{count} 天前',\n },\n },\n detail: {\n status: '狀態',\n assignee: '負責人',\n priority: '優先級',\n tenant: '租戶',\n createdAt: '建立時間',\n startedAt: '開始時間',\n completedAt: '完成時間',\n comments: '評論',\n events: '事件',\n runs: '執行記錄',\n result: '完成結果',\n sessions: '關聯工作階段',\n sessionMessages: '工作階段記錄',\n noSessions: '找不到關聯工作階段。',\n artifacts: '產出檔案',\n sources: '資料來源',\n highlights: '關鍵資訊',\n noLog: '找不到此任務的日誌。',\n addComment: '新增評論',\n commentPlaceholder: '輸入任務評論...',\n operations: '操作',\n },\n action: {\n title: '操作',\n complete: '完成',\n completeSummary: '完成摘要(選填)',\n block: '阻塞',\n blockReason: '阻塞原因',\n unblock: '解除阻塞',\n assign: '指派',\n assignTo: '指派給...',\n dispatch: '調度',\n recoveryReason: '恢復原因(選填)',\n reclaim: '回收',\n reassign: '重新指派',\n specify: '細化',\n loadLog: '載入日誌',\n loadDiagnostics: '載入診斷',\n },\n message: {\n taskCreated: '任務已建立',\n taskCompleted: '任務已完成',\n taskBlocked: '任務已阻塞',\n taskUnblocked: '任務已解除阻塞',\n taskAssigned: '任務已指派',\n dispatchNudged: '已觸發調度器',\n commentAdded: '評論已新增',\n taskReclaimed: '任務已回收',\n taskReassigned: '任務已重新指派',\n taskSpecified: '任務已細化',\n loadFailed: '載入任務失敗',\n },\n stats: {\n total: '總計',\n tasks: '任務數',\n },\n },\n\n // 排程任務\n jobs: {\n title: '排程任務',\n createJob: '建立任務',\n editJob: '編輯任務',\n noJobs: '目前無排程任務,建立一個開始吧。',\n name: '名稱',\n namePlaceholder: '任務名稱',\n schedule: '排程運算式 (Cron)',\n schedulePlaceholder: '例如 0 9 * * *',\n quickPresets: '快速預設',\n selectPreset: '選擇預設...',\n presetEveryMinute: '每分鐘',\n presetEvery5Min: '每 5 分鐘',\n presetEveryHour: '每小時',\n presetEveryDay: '每天 00:00',\n presetEveryDay9: '每天 09:00',\n presetEveryMonday: '每週一 09:00',\n presetEveryMonth: '每月 1 日 09:00',\n prompt: '提示詞',\n promptPlaceholder: '要執行的內容',\n skills: '技能',\n skillsPlaceholder: '選擇技能...',\n deliverTarget: '投遞目標',\n origin: '來源',\n local: '本地',\n repeatCount: '重複次數(選填)',\n modelPlaceholder: '預設模型',\n repeatPlaceholder: '留空表示無限重複',\n jobCreated: '任務已建立',\n jobUpdated: '任務已更新',\n nameRequired: '名稱為必填項',\n scheduleRequired: '排程運算式為必填項',\n loadFailed: '載入任務失敗',\n jobPaused: '任務已暫停',\n jobResumed: '任務已恢復',\n jobTriggered: '任務已觸發',\n modelUpdated: '模型已更新',\n jobDeleted: '任務已刪除',\n status: {\n running: '執行中',\n paused: '已暫停',\n disabled: '已停用',\n scheduled: '已排程',\n },\n info: {\n model: '模型',\n schedule: '排程',\n lastRun: '上次執行',\n nextRun: '下次執行',\n deliver: '投遞',\n repeat: '重複',\n },\n action: {\n pause: '暫停',\n pauseJob: '暫停任務',\n resume: '恢復',\n resumeJob: '恢復任務',\n runNow: '立即執行',\n triggerImmediately: '立即觸發',\n },\n runHistory: {\n title: '執行歷史',\n runs: '次執行',\n noRuns: '目前無執行歷史。',\n },\n },\n\n // 技能\n skills: {\n title: '技能',\n searchPlaceholder: '搜尋技能...',\n noMatch: '沒有符合的技能',\n noSkills: '目前無技能',\n backTo: '返回',\n attachedFiles: '附件檔案',\n loadFailed: '載入技能失敗',\n fileLoadFailed: '載入檔案失敗',\n modified: '使用者已修改',\n archived: '已封存',\n pinned: '已釘選',\n pin: '釘選技能',\n unpin: '取消釘選',\n pinFailed: '變更釘選狀態失敗',\n toggleFailed: '切換技能狀態失敗',\n source: {\n builtin: '內建',\n hub: 'Hub 安裝',\n local: '本地安裝',\n external: '外部目錄',\n },\n delete: '刪除',\n deleteConfirm: '確定要刪除技能 \"{name}\" 嗎?此操作無法撤銷。',\n deleteSuccess: '已刪除技能',\n deleteFailed: '刪除失敗',\n import: '匯入',\n importTitle: '匯入技能',\n importModeZip: 'Zip 包',\n importModeFolder: '資料夾',\n importHintZip: '上傳 .zip 包,應包含含 SKILL.md 的目錄',\n importHintFolder: '選擇本地技能資料夾(需含 SKILL.md)',\n importTargetCategory: '目標分類(選填)',\n importTargetCategoryPlaceholder: '留空則平鋪到 misc',\n importSelectFile: '選擇 .zip 檔案',\n importSelectFolder: '選擇資料夾',\n importInvalidFile: '僅支援 .zip 包',\n importFileCount: '個檔案',\n importNoSelection: '請先選擇檔案或資料夾',\n importSuccess: '已匯入技能',\n importFailed: '匯入失敗',\n path: {\n local: '本地',\n external: '外部',\n },\n externalDirs: {\n manage: '外部目錄',\n title: '外部目錄管理',\n hint: '在此管理 config.skills.external_dirs。支援 ~ 與 $VAR;不存在的路徑會被標灰。',\n add: '新增外部目錄',\n placeholder: '/path/to/skills 或 ~/my-skills',\n empty: '尚無外部目錄',\n missing: '目錄不存在',\n notDir: '不是目錄',\n saveSuccess: '外部目錄已儲存',\n saveFailed: '儲存失敗',\n loadFailed: '載入外部目錄失敗',\n removeRow: '移除',\n },\n pathCopied: '路徑已複製',\n pathCopyFailed: '複製路徑失敗',\n reloadHint: '活躍會話請執行 /reload-skills;新會話會自動載入。',\n writeApprovalButton: '待審核 {count}',\n writeApprovalTitle: '待審核寫入',\n writeApprovalDescription: '批准或拒絕 Parsifal Agent 暫存的記憶和技能寫入。',\n writeApprovalRefresh: '重新整理',\n writeApprovalEmpty: '暫無待審核寫入。',\n writeApprovalUnsupported: '目前 Parsifal Agent 版本不支援寫入審核。請升級 Parsifal Agent 後再使用。',\n writeApprovalMemory: '記憶',\n writeApprovalSkills: '技能',\n writeApprovalViewDiff: '查看',\n writeApprovalHideDiff: '收合',\n writeApprovalNoDiff: '沒有可顯示的差異。',\n writeApprovalLoadFailed: '待審核寫入載入失敗',\n writeApprovalDiffFailed: '寫入詳情載入失敗',\n writeApprovalActionFailed: '待審核寫入處理失敗',\n writeApprovalApproved: '已批准寫入',\n writeApprovalRejected: '已拒絕寫入',\n writeApprovalApprove: '批准',\n writeApprovalReject: '拒絕',\n writeApprovalPendingMemoryWrite: '待審核記憶寫入',\n writeApprovalCurrentFile: '目前 {file}',\n writeApprovalProposedFile: '批准後 {file}',\n writeApprovalDiffSection: '差異',\n writeApprovalPatchOldStringMissing: '目前檔案 {file} 中找不到這次補丁的 old_string,批准後檔案內容不會變更。',\n writeApprovalRequestedOldString: '本次請求匹配的 old_string',\n writeApprovalCurrentReadFailed: '目前檔案 {file} 無法以 UTF-8 文字讀取。',\n writeApprovalDeleteSkill: '這次審核會刪除技能 {skill}。',\n writeApprovalRemoveFile: '這次審核會從技能 {skill} 中移除檔案 {file}。',\n },\n\n // 插件\n plugins: {\n title: '插件',\n refresh: '重新整理',\n notice: '唯讀顯示可發現的 Parsifal 插件 manifest。發現元資料讀取不會載入插件程式碼。v1 管理動作仍保留在 CLI,新 Parsifal 工作階段生效。',\n loadFailed: '載入插件失敗',\n commandCopied: '指令已複製',\n searchPlaceholder: '搜尋 key、名稱、描述、路徑...',\n source: '來源',\n kind: '類型',\n statusTitle: '狀態',\n configStatus: '設定:{status}',\n notAvailable: '無',\n copyCommand: '複製指令',\n managedElsewhere: '由其他位置管理',\n noMatch: '沒有符合目前篩選條件的插件',\n enabled: '已啟用',\n disabled: '已停用',\n summary: {\n total: '總數',\n active: '已啟用 / 自動',\n inactive: '未啟用',\n disabled: '已停用',\n providerManaged: 'Provider 管理',\n },\n status: {\n enabled: '已啟用',\n 'auto-active': '自動啟用',\n inactive: '未啟用',\n disabled: '已停用',\n 'provider-managed': 'Provider 管理',\n },\n statusLabel: {\n enabled: '設定啟用',\n 'auto-active': '自動啟用',\n inactive: '未啟用',\n disabled: '已停用',\n 'provider-managed': 'Provider 管理',\n },\n configStatuses: {\n enabled: '已啟用',\n disabled: '已停用',\n 'not-enabled': '未啟用',\n auto: '自動',\n 'provider-managed': 'Provider 管理',\n },\n table: {\n plugin: '插件',\n status: '狀態',\n source: '來源',\n kind: '類型',\n capabilities: '能力',\n path: '路徑 / 入口',\n cli: 'CLI',\n },\n capabilities: {\n tools: '{count} 個工具',\n hooks: '{count} 個 hook',\n env: '{count} 個環境變數',\n },\n metadata: {\n agentRoot: 'Agent 根目錄',\n python: 'Python',\n scanCwd: '掃描 cwd',\n projectPlugins: '專案插件',\n },\n },\n\n // 記憶\n memory: {\n title: '記憶',\n refresh: '重新整理',\n loadFailed: '載入記憶失敗',\n myNotes: '我的筆記',\n noNotes: '目前無筆記。',\n notesPlaceholder: '輸入筆記內容...',\n userProfile: '使用者畫像',\n noProfile: '目前無畫像。',\n profilePlaceholder: '輸入使用者畫像...',\n soul: '靈魂',\n noSoul: '目前無靈魂設定。',\n soulPlaceholder: '輸入靈魂設定...',\n },\n\n // 模型\n models: {\n title: '模型',\n searchPlaceholder: '搜尋模型...',\n addProvider: '新增 Provider',\n refreshModelCache: '刷新模型快取',\n refreshModelCacheLoading: '正在刷新模型快取...',\n refreshModelCacheSuccess: '模型快取已刷新',\n refreshModelCacheFailed: '刷新模型快取失敗',\n providerType: 'Provider 類型',\n preset: '預設',\n custom: '自訂',\n selectProvider: '選擇 Provider',\n chooseProvider: '選擇一個 provider...',\n getApiKey: '取得 API Key',\n name: '名稱',\n autoGeneratedName: '依 Base URL 自動產生',\n baseUrl: 'Base URL',\n region: '區域',\n regionIntl: '國際版',\n regionCn: '中國大陸',\n baseUrlPlaceholder: '例如 https://api.example.com/v1',\n apiKey: 'API Key',\n apiKeyPlaceholder: 'sk-...',\n defaultModel: '預設模型',\n selectOrInput: '選擇或輸入模型名稱...',\n selectModel: '選擇模型...',\n providerAdded: 'Provider 已新增',\n providerDeleted: 'Provider 已刪除',\n deleteProvider: '刪除 Provider',\n deleteConfirm: '確定刪除「{name}」嗎?',\n codexLoginTitle: 'OpenAI Codex 登入',\n codexWaiting: '在授權頁面輸入以下代碼完成登入:',\n codexCopyCode: '代碼已複製',\n codexOpenLink: '開啟授權頁面',\n codexApproved: '登入成功',\n codexExpired: '授權已過期,請重試。',\n nousLoginTitle: 'Nous Portal 登入',\n nousWaiting: '在授權頁面輸入此代碼完成登入:',\n nousCopyCode: '代碼已複製',\n nousOpenLink: '開啟授權頁面',\n nousApproved: '登入成功',\n nousDenied: '授權被拒絕,請重試。',\n nousExpired: '授權已過期,請重試。',\n copilotLoginTitle: 'GitHub Copilot 登入',\n copilotWaiting: '請前往 GitHub 輸入下方裝置代碼完成授權。授權完成後視窗會自動關閉。',\n copilotCopyCode: '代碼已複製',\n copilotOpenLink: '開啟 GitHub 授權頁',\n copilotApproved: '登入成功!',\n copilotDenied: '授權被拒絕。',\n copilotExpired: '授權連結已過期,請重試。',\n copilotAddDetectedTitle: '偵測到 GitHub Copilot',\n copilotAddDetected: '已在本機偵測到 GitHub Copilot OAuth 憑證,點擊「新增」即可在 Parsifal 中啟用 Copilot。',\n copilotAddSourceEnv: '來源:~/.hermes/.env(COPILOT_GITHUB_TOKEN)',\n copilotAddSourceGhCli: '來源:gh CLI(gh auth token)',\n copilotAddSourceAppsJson: '來源:VS Code Copilot 插件(apps.json)',\n copilotDeleteHintEnv: '此操作會清除 ~/.hermes/.env 中的 COPILOT_GITHUB_TOKEN,不影響其他工具。',\n copilotDeleteHintGhCli: 'Copilot 將從 Parsifal 清單移除。不會影響 gh CLI —— `gh auth status` 仍顯示已登入。',\n copilotDeleteHintAppsJson: 'Copilot 將從 Parsifal 清單移除。不會影響 VS Code Copilot 插件的登入。',\n customBadge: '自訂',\n previewBadge: '預覽',\n disabledBadge: '不可用',\n disabledTooltip: '此模型目前帳號不可用',\n customModelPlaceholder: '自訂模型名稱',\n customModelHint: '按 Enter 載入',\n removeCustomModel: '移除這個未列出的模型',\n refresh: '重新整理模型列表(重新載入 config.yaml)',\n noProviders: '目前無 Provider,新增一個開始吧。',\n models: '模型清單',\n count: '個模型',\n more: '個更多',\n builtIn: '內建',\n customType: '自訂',\n provider: 'Provider',\n contextLength: '上下文長度',\n contextLengthPlaceholder: '例如 256000(選填)',\n local: '本地 ({host})',\n selectProviderRequired: '請選擇 Provider',\n baseUrlRequired: 'Base URL 為必填項',\n apiKeyRequired: 'API Key 為必填項',\n modelRequired: '預設模型為必填項',\n enterBaseUrl: '請先輸入 Base URL',\n unexpectedFormat: '回應格式異常',\n foundModels: '找到 {count} 個模型',\n fetchFailed: '取得模型失敗',\n manageVisibleModels: '管理可見模型',\n manageVisibleModelsFor: '管理 {name} 可見模型',\n visibilityHint: '僅影響 Web UI 的模型選擇器和模型頁展示,不會改寫 Parsifal CLI 的 provider/model 配置。實際呼叫仍使用原始模型 ID。',\n visibilitySaved: '可見模型已儲存',\n visibilitySaveFailed: '儲存可見模型失敗',\n showAllModels: '顯示全部模型',\n clearVisibleModels: '取消全選',\n auxiliaryTitle: '輔助模型',\n auxiliarySubtitle: '為壓縮、視覺、審批、MCP 和後台維護等輔助任務單獨指定模型。',\n auxiliaryRefresh: '刷新',\n auxiliaryTask: '任務',\n auxiliaryTimeout: '逾時',\n auxiliaryDownloadTimeout: '下載逾時',\n auxiliaryActions: '操作',\n auxiliaryDefault: '預設',\n auxiliaryCustomEndpoint: '自訂端點',\n auxiliaryProviderAuto: '自動',\n auxiliaryProviderMain: '主模型',\n auxiliaryProviderPlaceholder: 'auto、main 或 provider key',\n auxiliaryDownloadShort: '下載',\n auxiliaryExtraBody: 'Extra body JSON',\n auxiliaryClear: '清除',\n auxiliarySaved: '輔助模型已儲存',\n auxiliaryLoadFailed: '載入輔助模型失敗',\n auxiliarySaveFailed: '儲存輔助模型失敗',\n auxiliaryInvalidExtraBody: 'Extra body 必須是 JSON 物件',\n auxiliaryTaskCompression: '壓縮',\n auxiliaryTaskVision: '視覺',\n auxiliaryTaskWebExtract: '網頁擷取',\n auxiliaryTaskSkillsHub: '技能中心',\n auxiliaryTaskApproval: '審批',\n auxiliaryTaskMcp: 'MCP',\n auxiliaryTaskTitleGeneration: '標題產生',\n auxiliaryTaskTriageSpecifier: 'Triage 擴寫',\n auxiliaryTaskKanbanDecomposer: '看板拆解',\n auxiliaryTaskProfileDescriber: 'Profile 描述',\n auxiliaryTaskCurator: '策展',\n auxiliaryTaskSessionSearch: '會話搜尋',\n auxiliaryTaskFlushMemories: '寫入記憶',\n currentDefault: '目前預設',\n defaultShort: '預設',\n aliasEdit: '重新命名',\n aliasTitle: '模型顯示名',\n aliasTitleFor: '{model} 的顯示名',\n aliasPlaceholder: '留空則使用原始模型 ID',\n aliasHint: '僅修改 Web UI 顯示名,傳送給 Parsifal 的仍是原始模型 ID。',\n aliasCanonical: '原始 ID:{model}',\n aliasUseOriginal: '恢復原始 ID',\n aliasManage: '顯示名',\n aliasManageFor: '{provider} 的顯示名',\n aliasSaveFailed: '儲存顯示名失敗',\n visibilitySelectOne: '至少保留一個可見模型',\n xaiWaiting: '請在開啟的 xAI 頁面完成授權。授權完成後視窗會自動關閉。',\n xaiOpenLink: '開啟 xAI 授權頁',\n xaiLoginTitle: 'xAI Grok OAuth 登入',\n xaiExpired: '授權連結已過期,請重試。',\n xaiCopyLink: '複製授權連結',\n xaiApproved: '登入成功!',\n anthropicLoginTitle: 'Claude 授權登入',\n anthropicWaiting: '請在 Claude 授權頁完成授權,然後將返回的授權代碼貼到這裡。',\n anthropicOpenLink: '開啟 Claude 授權頁',\n anthropicCopyLink: '複製授權連結',\n anthropicCodePlaceholder: '貼上授權代碼...',\n anthropicSubmitCode: '提交代碼',\n anthropicApproved: '登入成功!',\n anthropicExpired: '授權已過期,請重試。',\n geminiLoginTitle: 'Google Gemini OAuth 登入',\n geminiWaiting: '請在開啟的 Google 頁面完成授權。授權完成後視窗會自動關閉。',\n geminiOpenLink: '開啟 Google 授權頁',\n geminiCopyLink: '複製授權連結',\n geminiApproved: '登入成功!',\n geminiExpired: '授權連結已過期,請重試。',\n },\n\n // 設定檔\n profiles: {\n title: '設定檔',\n create: '建立設定檔',\n import: '匯入',\n export: '匯出',\n rename: '重新命名',\n delete: '刪除',\n switchTo: '切換 Parsifal Profile',\n switchConfirm: '將執行 `parsifal profile use {name}` 並切換 Parsifal CLI 的 active profile,是否繼續?',\n switchSuccess: 'Parsifal active profile 已切換為「{name}」',\n switchFailed: '切換 Parsifal Profile 失敗,閘道可能需要手動重新啟動',\n createSuccess: '設定檔「{name}」已建立',\n createFailed: '建立設定檔失敗',\n renameSuccess: '設定檔已重新命名',\n renameFailed: '重新命名設定檔失敗',\n deleteConfirm: '確定刪除設定檔「{name}」嗎?',\n deleteSuccess: '設定檔已刪除',\n deleteFailed: '刪除設定檔失敗',\n exportSuccess: '設定檔已匯出',\n exportFailed: '匯出設定檔失敗',\n importSuccess: '設定檔已匯入',\n importFailed: '匯入設定檔失敗',\n importSelectFile: '選擇封存檔案',\n importInvalidFile: '請選擇有效的封存檔案 (.tar.gz, .tgz, .gz, .zip)',\n name: '設定檔名稱',\n namePlaceholder: '僅限小寫字母、數字、連字號',\n nameValidation: '設定檔名稱只能包含小寫字母、數字、底線和連字號',\n newName: '新名稱',\n newNamePlaceholder: '小寫字母、數字、連字號',\n cloneFromCurrent: '從目前設定檔複製',\n cloneCleanupNotice: '複製時會自動略過獨占型平台憑證(Weixin / Telegram / Slack 等),避免與來源設定檔衝突',\n cloneStrippedCredentials: '已清理 {count} 項獨占憑證:{list}',\n cloneDisabledPlatforms: '已停用 {count} 個平台:{list}',\n cloneStrippedConfigCredentials: '已清理 config.yaml 中 {count} 項嵌入憑證:{list}',\n archivePath: '封存路徑',\n archivePathPlaceholder: '封存檔案的伺服器路徑',\n importName: '設定檔名稱(選填)',\n importNamePlaceholder: '留空則使用封存名稱',\n active: '使用中',\n model: '模型',\n gateway: '閘道',\n alias: '別名',\n provider: 'Provider',\n path: '路徑',\n skills: '技能',\n hasEnv: '有 .env',\n hasSoulMd: '有 soul.md',\n noProfiles: '目前無設定檔,建立一個開始吧。',\n avatar: {\n title: '自訂頭像',\n customize: '頭像',\n upload: '上傳圖片',\n random: '隨機產生',\n reset: '恢復預設',\n hint: '支援 PNG、JPEG、WebP,最大 1MB',\n invalidType: '請選擇 PNG、JPEG 或 WebP 圖片',\n tooLarge: '頭像圖片不能超過 1MB',\n saveSuccess: '頭像已儲存',\n saveFailed: '儲存頭像失敗',\n resetSuccess: '已恢復預設頭像',\n resetFailed: '恢復預設頭像失敗',\n },\n runtime: {\n activeProfile: '目前:{name}',\n bridgeWorker: '橋接狀態',\n gateway: '閘道',\n active: '使用中',\n activeTag: '目前',\n idle: '閒置',\n checking: '檢測中',\n running: '執行中',\n stopped: '已停止',\n restartGateway: '重啟閘道',\n restartProfile: '重啟設定檔',\n switchProfile: '切換前端設定檔',\n gatewayRestarted: '閘道已重啟:{name}',\n gatewayRestartFailed: '重啟閘道失敗',\n profileRestarted: '設定檔已重啟:{name}',\n profileRestartFailed: '重啟設定檔失敗',\n },\n },\n\n // 日誌\n logs: {\n title: '日誌',\n all: '全部',\n searchPlaceholder: '搜尋...',\n refresh: '重新整理',\n noEntries: '目前無日誌',\n },\n\n // 設定\n settings: {\n title: '設定',\n saved: '已儲存',\n saveFailed: '儲存失敗',\n tabs: {\n display: '顯示',\n account: '目前帳號',\n users: '帳號管理',\n agent: '代理',\n memory: '記憶',\n compression: '上下文壓縮',\n session: '工作階段',\n privacy: '隱私',\n apiServer: 'API 伺服器',\n models: '模型',\n voice: '語音',\n },\n models: {\n apiKey: 'API Key',\n apiKeyPlaceholder: '輸入 API Key',\n save: '儲存',\n saved: '已儲存',\n saveFailed: '儲存失敗',\n noProviders: '目前無已設定的模型',\n },\n display: {\n streaming: '串流回應',\n streamingHint: '即時顯示 AI 回覆',\n compact: '緊湊模式',\n compactHint: '減少訊息間距',\n showReasoning: '顯示推理過程',\n showReasoningHint: '展示模型思考過程',\n showCost: '顯示費用',\n showCostHint: '在回覆中顯示 token 使用量',\n inlineDiffs: '內嵌差異',\n inlineDiffsHint: '程式碼變更以內嵌方式顯示',\n bellOnComplete: '完成提示音',\n bellOnCompleteHint: 'AI 回覆完成時播放提示音',\n notifyOnComplete: '完成彈窗通知',\n notifyOnCompleteHint: 'AI 回覆完成時顯示系統通知',\n notifyOnCompleteMacHint: 'macOS 需要在系統設定中允許瀏覽器通知。',\n notifyOnCompleteDenied: '通知權限已被拒絕',\n notifyOnCompleteInsecure: '瀏覽器通知需要 HTTPS 或 localhost',\n notifyOnCompleteUnsupported: '目前瀏覽器不支援通知',\n notifyOnCompleteTest: '通知已開啟。',\n notifyOnCompleteTestButton: '測試',\n notifyOnCompleteTesting: '正在傳送測試通知...',\n notifyOnCompleteTestSent: '測試通知已傳送',\n notifyOnCompleteTestFailed: '測試通知傳送失敗',\n busyInputMode: '忙碌輸入模式',\n busyInputModeHint: 'AI 處理中仍可輸入',\n theme: '主題',\n themeHint: '選擇淺色、暗色或跟隨系統',\n themeLight: '淺色',\n themeDark: '暗色',\n themeSystem: '跟隨系統',\n },\n agent: {\n maxTurns: '最大輪次',\n maxTurnsHint: '單次對話最大互動輪數',\n gatewayTimeout: '閘道逾時',\n gatewayTimeoutHint: '單次請求逾時時間(秒)',\n restartDrainTimeout: '重啟排空逾時',\n restartDrainTimeoutHint: '重啟前排空請求的逾時時間(秒)',\n toolEnforcement: '工具執行策略',\n toolEnforcementHint: '控制工具呼叫的執行模式',\n auto: '自動',\n always: '始終',\n never: '從不',\n },\n gatewayAutoStart: {\n title: 'Gateway 自動啟動',\n description: '控制 Web UI 啟動時自動拉起哪些本機 Parsifal profile gateway。此項寫入 Web UI 設定,不寫入 profile config.yaml。',\n enabled: '自動啟動 gateway',\n enabledHint: '關閉後,啟動期不會自動拉起任何 profile gateway。',\n mode: 'Profile 策略',\n modeHint: '啟動所有發現到的 profiles,或只啟動白名單內的 profiles。',\n modeAll: '所有發現到的 profiles',\n modeInclude: '僅白名單 profiles',\n include: '白名單 profiles',\n includeHint: '逗號分隔 profile 名。白名單模式下留空表示不自動啟動任何 profile。',\n exclude: '排除 profiles',\n excludeHint: '逗號分隔 profile 名;會在白名單/全量策略之後再排除。',\n profileListPlaceholder: 'default, reviewer',\n },\n memory: {\n enabled: '啟用記憶',\n enabledHint: '允許 AI 記住對話上下文',\n userProfile: '使用者畫像',\n userProfileHint: '允許 AI 記住使用者偏好資訊',\n charLimit: '記憶字元上限',\n charLimitHint: 'MEMORY.md 最大字元數',\n userCharLimit: '使用者畫像字元上限',\n userCharLimitHint: 'USER.md 最大字元數',\n },\n compression: {\n enabled: '啟用壓縮',\n enabledHint: '長對話接近模型上下文上限前自動壓縮歷史',\n threshold: '壓縮閾值',\n thresholdHint: '預估 token 超過上下文比例時開始壓縮',\n targetRatio: '目標比例',\n targetRatioHint: '壓縮後歷史保留到上下文的目標比例',\n protectLastN: '保護最近訊息',\n protectLastNHint: '最近多少則訊息不參與壓縮',\n protectFirstN: '保護開頭訊息',\n protectFirstNHint: '最早多少則訊息不參與壓縮',\n },\n session: {\n mode: '重設模式',\n modeHint: '工作階段重設的觸發條件',\n modeBoth: '閒置 + 定時',\n modeIdle: '僅閒置',\n modeDaily: '僅定時',\n modeNone: '永不(僅手動)',\n idleMinutes: '閒置逾時',\n idleMinutesHint: '無操作後自動重設的等待時間(分鐘)',\n atHour: '定時重設時間',\n humanOnly: '僅顯示人類工作階段',\n humanOnlyHint: '預設隱藏子代理和工作階段監看雜訊',\n liveMonitorHumanOnly: '即時監看:僅顯示人類工作階段',\n liveMonitorHumanOnlyHint: '在即時監看中預設隱藏子代理和工作階段監看雜訊',\n atHourHint: '每天在指定小時重設工作階段',\n requireAuth: '工作階段授權',\n requireAuthHint: '修改工作階段操作是否授權',\n memoryWriteApproval: '記憶寫入審核',\n memoryWriteApprovalHint: 'Parsifal Agent 寫入 MEMORY.md / USER.md 前先進入待審核佇列。',\n skillsWriteApproval: '技能寫入審核',\n skillsWriteApprovalHint: 'Parsifal Agent 建立或修改技能前先進入待審核佇列。',\n writeApprovalTitle: '待審核寫入',\n writeApprovalDescription: '批准或拒絕 Parsifal Agent 暫存的記憶和技能寫入。',\n writeApprovalRefresh: '重新整理',\n writeApprovalEmpty: '暫無待審核寫入。',\n writeApprovalMemory: '記憶',\n writeApprovalSkills: '技能',\n writeApprovalViewDiff: '查看',\n writeApprovalHideDiff: '收起',\n writeApprovalNoDiff: '沒有可顯示的差異。',\n writeApprovalLoadFailed: '待審核寫入載入失敗',\n writeApprovalDiffFailed: '寫入詳情載入失敗',\n writeApprovalActionFailed: '待審核寫入處理失敗',\n writeApprovalApproved: '已批准寫入',\n writeApprovalRejected: '已拒絕寫入',\n writeApprovalApprove: '批准',\n writeApprovalReject: '拒絕',\n writeApprovalPendingMemoryWrite: '待審核記憶寫入',\n writeApprovalCurrentFile: '目前 {file}',\n writeApprovalProposedFile: '批准後 {file}',\n writeApprovalDiffSection: '差異',\n writeApprovalPatchOldStringMissing: '目前檔案 {file} 中找不到這次補丁的 old_string,批准後檔案內容不會變更。',\n writeApprovalRequestedOldString: '本次請求匹配的 old_string',\n writeApprovalCurrentReadFailed: '目前檔案 {file} 無法以 UTF-8 文字讀取。',\n writeApprovalDeleteSkill: '這次審核會刪除技能 {skill}。',\n writeApprovalRemoveFile: '這次審核會從技能 {skill} 中移除檔案 {file}。',\n },\n privacy: {\n redactPii: '遮蔽 PII',\n redactPiiHint: '自動偵測並隱藏敏感資訊(密碼、金鑰等)',\n },\n apiServer: {\n enable: '啟用',\n enableHint: '啟用 API 伺服器',\n host: '主機',\n hostHint: '監聽位址',\n port: '連接埠',\n portHint: '監聽連接埠',\n key: '金鑰',\n keyHint: 'API 存取金鑰',\n cors: 'CORS 來源',\n corsHint: '允許的跨域來源',\n },\n lockedIps: {\n title: '鎖定 IP 管理',\n count: '{count} 個 IP 被鎖定',\n empty: '目前無鎖定 IP',\n unlock: '解鎖',\n unlockAll: '全部解鎖',\n unlockAllConfirm: '確認解鎖所有鎖定的 IP?',\n unlocked: 'IP 已解鎖',\n allUnlocked: '已解鎖 {count} 個 IP',\n type: {\n password: '密碼',\n token: 'API Token',\n pairing: '裝置配對',\n },\n },\n userAvatar: {\n title: '使用者頭像',\n upload: '上傳圖片',\n random: '隨機產生',\n reset: '恢復預設',\n hint: '支援 PNG、JPEG、WebP,最大 1MB,會自動壓縮到 500KB 以下',\n invalidType: '請選擇 PNG、JPEG 或 WebP 圖片',\n tooLarge: '頭像圖片不能超過 1MB',\n saveSuccess: '頭像已儲存',\n saveFailed: '儲存頭像失敗',\n resetSuccess: '已恢復預設頭像',\n resetFailed: '恢復預設頭像失敗',\n },\n voice: {\n activeTtsApi: '目前 TTS API',\n activeSttApi: '目前 STT API',\n ttsProvidersTitle: 'TTS 提供方',\n ttsProvidersDescription: '管理用於播放助理回覆和語音預覽的文字轉語音整合。',\n sttProvidersTitle: 'STT 提供方',\n sttProvidersDescription: '管理用於麥克風和語音輸入流程的語音轉文字整合。',\n noneSelected: '未選擇',\n keyStored: '已儲存',\n keyMissing: '未設定',\n customApi: 'API',\n localBuiltin: '本機內建',\n ttsCardHint: '語音合成提供方',\n sttCardHint: '語音轉寫提供方',\n setActive: '設為目前使用',\n testAction: '測試',\n connect: '連線',\n presetEdgeTtsLabel: 'Edge TTS',\n presetEdgeTtsDescription: 'Microsoft Edge 提供的免費高品質雲端音色。',\n presetOpenaiTtsLabel: 'OpenAI TTS',\n presetMimoTtsLabel: 'MiMo TTS',\n presetCustomTtsLabel: '自訂 TTS',\n presetBrowserSttLabel: '瀏覽器 STT',\n presetBrowserSttDescription: '瀏覽器原生語音辨識 API。',\n presetOpenaiWhisperLabel: 'OpenAI Whisper',\n presetGroqSttLabel: 'Groq STT',\n presetCustomSttLabel: '自訂 STT',\n apiCompatibility: 'API 相容性',\n openaiCompatible: 'OpenAI 相容',\n manualCustomEndpoint: '手動自訂端點',\n connectionStep: '連線',\n discoveryManualHint: '已選擇手動自訂端點,無法自動探索模型;請手動輸入模型名稱。',\n discoveryCredentialHint: '輸入 Base URL 和 API key 後,點擊連線取得可用模型。',\n discoveryNoAutoProbeHint: '輸入過程中不會送出請求;憑證準備好後點擊連線。',\n apiKeyConnectPlaceholder: '貼上 API key 後連線;已儲存的 key 不會顯示',\n connectFetchModels: '連線並取得模型',\n modelRecommendedSuffix: '推薦',\n modelConnectManualHint: '連線後自動填入,或手動輸入。',\n modelRequiredConnectManual: '模型為必填。請連線後自動填入,或手動輸入。',\n apiKeyRequired: 'API key 為必填。',\n apiKeyRequiredForDiscovery: '取得模型需要 API key。',\n baseUrlRequired: 'Base URL 為必填。',\n baseUrlHttpsRequired: '除 localhost 端點外,請使用 https://。',\n baseUrlInvalid: '請輸入有效的 Base URL,包含 https://。',\n discoveryFailedManualFallback: '無法取得模型。你仍可以手動輸入模型名稱。',\n completeRequiredProviderFields: '儲存前請補齊必填 provider 欄位。',\n fetchingModels: '正在取得模型……',\n recommendedModel: '推薦:{model}。',\n useRecommendedModel: '使用此模型。',\n modelsDiscovered: '已發現 {count} 個模型。欄位為空時會自動選擇推薦模型。',\n remove: '移除',\n testSuccess: '測試完成',\n testTextRequired: '測試前請輸入文字。',\n keyMissingForTest: '測試此提供方前請先新增 API key。',\n testing: '測試中……',\n transcribing: '轉寫中……',\n browserSttTestHint: '瀏覽器 STT 請在聊天輸入框麥克風中測試。',\n sttEmptyAudio: '未錄到音訊,請重試。',\n sttTestSuccess: '轉寫完成',\n sttRecordingHint: '正在錄音……點擊停止後轉寫。',\n keepStoredKeyPlaceholder: '留空則保留已儲存 key',\n apiKeyPlaceholder: '輸入 API key',\n lastTest: '最近測試',\n notTested: '—',\n recording: '錄音中',\n testFailedShort: '失敗',\n testFailedSummary: '測試失敗,請檢查 provider 設定後重試。',\n showDetails: '顯示詳情',\n hideDetails: '隱藏詳情',\n moreActions: '更多',\n moreActionsFor: '{provider} 的更多操作',\n addTtsApi: '新增 TTS API',\n addSttApi: '新增 STT API',\n active: '目前使用',\n builtin: '內建',\n connected: '已連線',\n disconnected: '未連線',\n source: '來源',\n model: '模型',\n voice: '音色',\n apiKey: 'API key',\n confirmDeleteSecret: '確認刪除這個語音 API 已儲存的 API key?',\n ttsTitle: '文字轉語音',\n ttsSaved: 'TTS 設定已儲存到伺服端',\n ttsSaveFailed: '儲存 TTS 設定失敗',\n ttsTestSaved: '試聽成功,TTS 設定已儲存到伺服端',\n ttsTestSaving: '正在儲存 TTS 設定後再試聽…',\n ttsMissingBaseUrl: '試聽前需要填寫基礎 URL。請輸入服務商 endpoint 後再測試。',\n ttsMissingApiKey: '試聽前需要 API key。請貼上 key,或先儲存服務端已存 key。',\n ttsCleared: '已清除伺服端 TTS 金鑰',\n ttsClearFailed: '清除伺服端 TTS 金鑰失敗',\n storedBaseUrlsTitle: '已存 Base URLs',\n storedBaseUrlsHint: '這裡集中管理已保存的語音 provider endpoint。上方輸入框只清除目前值,刪除已存 URL 在這裡操作。',\n storedBaseUrlsEmpty: '服務端還沒有保存自訂語音 Base URL。',\n storedBaseUrlDelete: '刪除 URL',\n storedApiKeysTitle: '已存 API keys',\n storedApiKeysHint: '這裡列出服務端已保存的語音 API key,只顯示用途和來源,不顯示完整 key。',\n storedApiKeysEmpty: '服務端還沒有保存語音 API key。',\n storedKeyKindTts: 'TTS',\n storedKeyKindStt: 'STT',\n storedApiKeyMasked: '[已儲存]',\n storedApiKeyDelete: '刪除 key',\n baseUrlPresetClear: '清除 URL',\n baseUrlPresetCleared: '已清除目前 Base URL。',\n baseUrlPresetDelete: '刪除 URL',\n baseUrlPresetDeleted: '已從儲存列表刪除基礎 URL。',\n baseUrlPresetDeleteFailed: '刪除已儲存基礎 URL 失敗。',\n ttsPendingSecret: '將儲存新金鑰',\n ttsStoredSecret: '伺服端已有儲存金鑰:{value}',\n ttsClearKey: '清除已儲存金鑰',\n sttTitle: '語音轉文字',\n sttProvider: 'STT 提供方',\n sttProviderHint: '選擇語音輸入的轉寫方式',\n sttProviderBrowser: '瀏覽器',\n sttProviderOpenai: 'OpenAI 相容',\n sttProviderCustom: '自訂',\n sttBrowserHint: '可用時使用瀏覽器內建語音辨識。',\n sttApiKey: 'API Key',\n sttApiKeyHint: '僅在伺服器端保存,用於需要驗證的 STT 提供方。',\n sttStoredSecret: '伺服器端已儲存金鑰:{value}',\n sttPendingSecret: '伺服器端已儲存金鑰:儲存後更新',\n sttClearKey: '清除已儲存金鑰',\n sttModel: '模型',\n sttModelHint: '用於語音轉文字的模型。',\n sttLanguage: '語言',\n sttLanguageHint: '可選語言提示,例如 en 或 zh。',\n sttPrompt: '提示詞',\n sttPromptHint: '可選轉寫提示詞。',\n sttBaseUrl: '基礎 URL',\n sttBaseUrlHint: '自訂 OpenAI 相容語音轉文字端點。',\n sttSave: '儲存 STT 設定',\n sttSaved: 'STT 設定已儲存到伺服器。',\n sttSaveFailed: '儲存 STT 設定失敗。',\n sttCleared: '伺服器端已儲存的 STT 金鑰已清除。',\n sttClearFailed: '清除伺服器端已儲存的 STT 金鑰失敗。',\n sttActiveSaved: '目前 STT 提供方已儲存到伺服器。',\n sttSaveActive: '儲存目前 STT 提供方',\n sttEndpointHint: '最終轉寫 endpoint:{endpoint}',\n sttTestTitle: '測試語音轉文字',\n sttTestButton: '測試 STT API',\n sttTestStopButton: '停止並轉寫',\n sttTestBackendOnly: 'STT API 測試僅適用於 OpenAI 相容和自訂提供方。',\n sttTestRecording: '正在錄製測試音訊……錄完後點擊停止並轉寫。',\n sttTestTranscribing: '正在轉寫測試音訊……',\n sttTestEmpty: '沒有錄到可用於 STT 測試的音訊。',\n sttTestSucceeded: 'STT 測試成功。',\n sttTestFailed: 'STT 測試失敗:{error} —— {hint}',\n\n sttFailureHintMissingConfig: '請確認已儲存 provider 的 Base URL、模型和 API key 後再測試。',\n\n sttFailureHintAuth: '請檢查 API key;如果頁面顯示已儲存 key,請重新貼上目前 key 並儲存後再測。',\n\n sttFailureHintForbidden: 'key 已到達供應商但沒有該 STT 模型/帳號權限,請檢查 provider 權限、額度或 billing。',\n\n sttFailureHintEndpointModel: '請檢查 Base URL 和模型。Groq 建議使用 {endpoint},模型用 whisper-large-v3-turbo 或其他 Groq STT 模型。',\n\n sttFailureHintModel: '請檢查所選 STT 模型是否存在,且目前帳號有權限使用。',\n\n sttFailureHintQuota: '供應商返回額度、billing 或限流問題,請檢查帳號額度/billing,或稍後重試。',\n\n sttFailureHintNetworkCustom: 'Web UI server 無法存取 custom STT endpoint。請確認 {endpoint} 可從伺服器存取,並相容 OpenAI audio/transcriptions。',\n\n sttFailureHintNetwork: 'Web UI server 無法存取 STT provider,請檢查網路、代理或防火牆後重試。',\n\n sttFailureHintAudio: '錄音為空或音訊格式被拒絕。請允許麥克風權限,錄一段清晰短句,或換 Chrome 重試。',\n\n sttFailureHintGenericCustom: '請檢查 API key、Base URL、模型,以及最終 endpoint {endpoint}。',\n\n sttFailureHintGeneric: '請檢查 API key、模型和 provider 帳號權限後重試。',\n sttTestRecordFailed: '無法開始麥克風錄音:{error}',\n sttTestTranscript: '轉寫結果:{text}',\n ttsProvider: 'TTS 提供者',\n ttsProviderHint: '選擇訊息朗讀使用的語音引擎',\n providerWebSpeech: 'WebSpeech API(瀏覽器內建)',\n providerOpenai: 'OpenAI TTS',\n providerCustom: '自訂端點(相容 OpenAI)',\n providerEdge: 'Edge TTS(免費,無需 API Key)',\n\n // WebSpeech\n webspeechVoice: '音色',\n webspeechVoiceHint: '從瀏覽器或系統提供的語音中選擇',\n webspeechVoicePlaceholder: '自動(預設語音)',\n\n // OpenAI\n openaiKey: 'API 金鑰',\n openaiKeyHint: '具有 TTS 權限的 OpenAI API Key',\n openaiUrl: 'API 基礎位址',\n openaiUrlHint: '例如 https://api.openai.com/v1/audio/speech',\n openaiModel: '模型',\n openaiModelHint: 'tts-1(快速)/ tts-1-hd(高音質)',\n openaiVoice: '音色',\n openaiVoiceHint: '用於語音合成的音色',\n\n // 自訂端點\n customHint: '支援任何 OpenAI 相容的 TTS 服務——可用於 GPT-SoVITS、CosyVoice 等自部署服務。',\n customUrl: 'API 位址',\n customUrlHint: 'TTS 服務的完整基礎位址',\n customUrlPlaceholder: '本地適配器中設定的位址,如:http://127.0.0.1:9880',\n customApiKey: 'API 金鑰(選填)',\n customApiKeyHint: '部分自部署服務需要身份驗證',\n customApiKeyPlaceholder: '不需要則留空',\n // Edge TTS\n edgeHint: '由 Microsoft Edge TTS 驅動(node-edge-tts)。',\n edgeUrl: '適配器位址',\n edgeUrlHint: 'Edge TTS 適配器位址,例如 http://127.0.0.1:9882',\n edgeUrlPlaceholder: 'http://127.0.0.1:9882',\n edgeVoice: '音色',\n edgeVoiceHint: '選擇用於語音合成的音色',\n edgeRate: '語速',\n edgeRateHint: '調整語音速度(0.5~2.0 倍)',\n edgePitch: '音調',\n edgePitchHint: '調整語音音調(-20~+20 Hz)',\n\n // 試聽\n testTitle: '試聽測試',\n testText: '測試文字',\n testTextPlaceholder: '輸入測試文字...',\n testTextDefault: '你好,這是一個語音測試。',\n testButton: '試聽',\n testButtonPlaying: '播放中...',\n testFailed: '測試失敗:{error}',\n\n // MiMo TTS\n providerMimo: 'MiMo TTS',\n mimoHint: '小米 MiMo TTS,支援預設音色、音色設計、音色複製三種模式',\n mimoApiKey: 'API Key',\n mimoApiKeyHint: '在 platform.xiaomimimo.com 取得',\n mimoApiKeyPlaceholder: 'MiMo API Key',\n mimoAuthMode: '認證方式',\n mimoAuthModeHint: '所選 MiMo 端點要求的鑑權請求標頭格式',\n mimoAuthModeBearer: 'Bearer Token(Authorization)',\n mimoAuthModeApiKey: 'API Key 標頭(api-key)',\n mimoAuthModeBoth: '同時傳送兩種標頭',\n mimoBaseUrl: 'Base URL',\n mimoBaseUrlHint: 'MiMo API 端點位址',\n mimoModel: '模型',\n mimoModelHint: '選擇語音合成模型',\n mimoModelPreset: '預設音色',\n mimoModelVoiceDesign: '音色設計',\n mimoModelVoiceClone: '音色複製',\n mimoVoice: '音色',\n mimoVoiceHint: '選擇預設音色',\n mimoVoiceDesignPrompt: '音色描述',\n mimoVoiceDesignPromptHint: '描述你想要的音色特徵',\n mimoVoiceDesignPromptPlaceholder: '例如:溫柔的年輕女聲,語速稍慢,帶著磁性',\n mimoCloneAudio: '上傳音訊',\n mimoCloneAudioHint: '上傳音訊樣本用於音色複製,支援 mp3/wav,最大 10MB',\n mimoCloneAudioUpload: '選擇檔案',\n mimoCloneAudioClear: '清除音訊',\n mimoStylePrompt: '風格指令',\n mimoStylePromptHint: '可選,用自然語言描述語音風格',\n mimoStylePromptPlaceholder: '例如:用輕快上揚的語調,語速稍快',\n },\n },\n githubPreview: {\n title: \"版本預覽\",\n description: \"將選取的 GitHub tag 複製到 Web UI 預覽工作目錄,安裝依賴並以開發連接埠執行。\",\n refresh: \"重新整理\",\n selectTag: \"選擇 tag\",\n prepare: \"準備程式碼\",\n install: \"安裝依賴\",\n start: \"開啟預覽\",\n stop: \"停止\",\n note: \"預覽程式碼存放在 Web UI 資料目錄下。正式環境仍使用 8648,預覽開發環境使用前端 8651、後端 8650。\",\n path: \"預覽路徑\",\n webuiHome: \"預覽資料目錄\",\n currentTag: \"目前 Tag\",\n repoReady: \"倉庫就緒\",\n dependencies: \"依賴已安裝\",\n running: \"執行狀態\",\n notRunning: \"未執行\",\n open: \"開啟預覽\",\n log: \"操作日誌路徑\",\n logOutput: \"日誌輸出\",\n actionLog: \"操作日誌\",\n devLog: \"開發服務日誌\",\n yes: \"是\",\n no: \"否\",\n actionFailed: \"操作失敗\",\n nodeEnvironmentMissing: \"未偵測到可用的 Node/npm 環境,請先安裝 Node.js 後重試。\",\n prepareSuccess: \"預覽程式碼已準備好\",\n installSuccess: \"依賴安裝完成\",\n startSuccess: \"預覽已完成\",\n stopSuccess: \"預覽已停止\",\n },\n\n codingAgents: {\n title: \"編程工具\",\n notice: \"並非所有提供商和模型都相容。\",\n claudeDescription: \"Anthropic CLI,適合 print mode 單次任務和互動式編程工作階段。\",\n codexDescription: \"OpenAI CLI,以及 Parsifal openai-codex 提供商的倉庫任務流程。\",\n copyCommand: \"複製\",\n commandCopied: \"命令已複製\",\n commandCopyFailed: \"複製失敗\",\n refresh: \"重新整理\",\n checking: \"偵測中\",\n installStatus: \"安裝狀態\",\n installed: \"已安裝\",\n notInstalled: \"未安裝\",\n installRequired: \"{agent} 未安裝,請先安裝後再建立會話。\",\n installNow: \"安裝\",\n installing: \"安裝中\",\n installSuccess: \"安裝完成\",\n installFailed: \"安裝失敗\",\n installFailedParsifalHint: \"安裝失敗,請讓 Parsifal 幫你安裝。\",\n installFailureReason: \"錯誤原因\",\n nodeEnvironmentMissing: \"未偵測到可用的 Node/npm 環境,請先安裝 Node.js 後重試。\",\n deleteNow: \"刪除\",\n deleting: \"刪除中\",\n deleteSuccess: \"刪除完成\",\n deleteFailed: \"刪除失敗\",\n configFiles: \"設定檔\",\n profileScope: \"設定\",\n providerScope: \"提供商\",\n providerPlaceholder: \"例如 custom:glm\",\n modelScope: \"模型\",\n modelPlaceholder: \"選擇模型\",\n launchModeScope: \"啟動方式\",\n launchModeGlobal: \"全域預設設定\",\n launchModeScoped: \"選擇提供商和模型\",\n protocolScope: \"協議\",\n protocolOpenAiChat: \"OpenAI Chat Completions (/v1/chat/completions)\",\n protocolOpenAiResponses: \"OpenAI Responses (/v1/responses)\",\n protocolAnthropicMessages: \"Anthropic Messages (/v1/messages)\",\n reloadConfig: \"重新讀取設定\",\n configFileNotCreated: \"未建立\",\n configLoadFailed: \"讀取設定檔失敗\",\n loadFailed: \"偵測編程工具失敗\",\n launch: \"啟動\",\n launchTitle: \"啟動編程工具\",\n nativeTerminal: \"原生終端機\",\n builtInTerminal: \"內建終端機\",\n launchPrepared: \"啟動設定已產生\",\n launchPrepareFailed: \"產生啟動設定失敗\",\n nativeLaunchStarted: \"已開啟原生終端機\",\n nativeLaunchFailed: \"開啟原生終端機失敗\",\n terminalTitle: \"編程工具終端機\",\n loadProvidersFailed: \"讀取目前設定的提供商失敗\",\n selectProviderModel: \"請選擇提供商和模型\",\n launchConfigDir: \"啟動設定目錄\",\n launchCommand: \"啟動命令\",\n table: {\n tool: \"工具\",\n kind: \"步驟\",\n command: \"命令\",\n note: \"說明\",\n action: \"操作\",\n },\n kinds: {\n install: \"安裝\",\n auth: \"認證\",\n health: \"檢查\",\n run: \"執行\",\n },\n notes: {\n claudeInstall: \"全域安裝 Claude Code CLI。\",\n codexInstall: \"全域安裝 Codex CLI。\",\n claudeAuth: \"檢查 Claude Code 登入狀態;未登入時先執行 claude。\",\n codexAuth: \"新增 Parsifal 管理的 OpenAI Codex OAuth 憑證。\",\n claudeHealth: \"檢查自動更新器和本機 CLI 健康狀態。\",\n codexHealth: \"確認 Codex CLI 已在 PATH 中可用。\",\n claudeRun: \"Print mode 最適合 API 驅動的單次任務。\",\n codexRun: \"Codex 單次任務需要在 git 倉庫中執行。\",\n },\n },\n\n // 平台頻道設定\n platform: {\n requireMention: \"需要 {'@'}提及\",\n requireMentionGroup: \"群組中需要 {'@'}機器人 才會回應\",\n requireMentionChannel: \"頻道中需要 {'@'}機器人 才會回應\",\n requireMentionRoom: \"房間中需要 {'@'}機器人 才會回應\",\n reactions: '表情回應',\n reactionsHint: '對訊息新增表情回應',\n freeResponseChats: '自由回應聊天',\n freeResponseChatsHint: \"不需要 {'@'}提及即回應的聊天 ID(逗號分隔)\",\n freeResponseChannels: '自由回應頻道',\n freeResponseChannelsHint: \"不需要 {'@'}提及即回應的頻道 ID(逗號分隔)\",\n freeResponseRooms: '自由回應房間',\n freeResponseRoomsHint: \"不需要 {'@'}提及即回應的房間 ID(逗號分隔)\",\n mentionPatterns: '自訂提及模式',\n mentionPatternsHint: '額外的觸發模式清單',\n autoThread: '自動建立討論串',\n autoThreadHint: \"{'@'}提及後自動建立回覆討論串\",\n autoThreadHintRoom: '在房間中自動建立回覆討論串',\n dmMentionThreads: 'DM 提及討論串',\n dmMentionThreadsHint: '在私聊中也使用討論串回覆提及',\n allowBots: '允許機器人訊息',\n allowBotsHint: '回應其他機器人傳送的訊息',\n allowedChannels: '允許的頻道',\n allowedChannelsHint: '白名單頻道 ID(逗號分隔)',\n ignoredChannels: '忽略的頻道',\n ignoredChannelsHint: '不回應的頻道 ID(逗號分隔)',\n noThreadChannels: '無討論串頻道',\n noThreadChannelsHint: '不建立討論串的頻道 ID(逗號分隔)',\n exclusiveTokenWarning: '此平台使用獨占 token 鎖。每個 profile 必須使用不同的身份 token,否則會與其他 profile 衝突導致 gateway 啟動失敗。',\n botToken: 'Bot Token',\n botTokenHint: '開發者入口網站取得的 Bot Token',\n accessToken: 'Access Token',\n accessTokenHint: 'Matrix Access Token',\n homeserver: 'Homeserver URL',\n homeserverHint: 'Matrix 伺服器位址',\n appId: 'App ID',\n appIdHint: '飛書 App ID',\n appSecret: 'App Secret',\n appSecretHint: '飛書 App Secret',\n clientId: 'Client ID',\n clientIdHint: '釘釘 Client ID',\n clientSecret: 'Client Secret',\n clientSecretHint: '釘釘 Client Secret',\n cardTemplateId: 'AI 卡片範本 ID',\n cardTemplateIdHint: '釘釘 AI 卡片範本 ID;留空則不啟用 AI 卡片',\n allowedUsers: '允許使用者',\n allowedUsersHint: '使用者 ID 或 OpenID 白名單,多個請用英文逗號分隔',\n allowAllUsers: '允許所有使用者',\n allowAllUsersHint: '允許任意使用者發起訊息;關閉後使用白名單',\n botId: 'Bot ID',\n botIdHint: '企業微信 Bot ID',\n wecomSecretHint: '企業微信 Bot Secret',\n waEnabled: '啟用 WhatsApp',\n waEnabledHint: '透過 QR Code 配對啟用 WhatsApp',\n weixinToken: '微信 Token',\n weixinTokenHint: '透過 weixin CLI 掃碼登入取得 (parsifal weixin)',\n accountId: 'Account ID',\n accountIdHint: '微信 Account ID',\n qrLogin: '掃碼登入',\n qrRelogin: '重新登入',\n qrFetching: '正在取得 QR Code...',\n qrScanHint: '使用微信掃描 QR Code 登入',\n qrScanedHint: '已掃描,請在手機上確認...',\n // QQ\n qqAppId: 'App ID',\n qqAppIdHint: 'QQ 開放平台機器人 App ID',\n qqAppSecret: 'App Secret',\n qqAppSecretHint: 'QQ 開放平台機器人 App Secret',\n qqMarkdown: 'Markdown 支援',\n qqMarkdownHint: '啟用 Markdown 格式訊息(部分客戶端可能不支援)',\n qqSandbox: '沙箱模式',\n qqSandboxHint: '啟用沙箱環境(測試用)',\n qqQrScanHint: '使用 QQ 掃描上方 QR Code,或在手機上開啟連結完成綁定',\n },\n\n // 閘道\n gateways: {\n title: '閘道',\n running: '執行中',\n stopped: '已停止',\n started: '已啟動',\n startFailed: '啟動失敗',\n stopFailed: '停止失敗',\n },\n\n // 語言\n language: {\n label: '語言',\n zh: '中文(簡體)',\n 'zh-TW': '繁體中文',\n en: 'English',\n },\n\n // 終端機\n terminal: {\n sessions: '工作階段',\n newTab: '新增終端機',\n closeSession: '關閉此工作階段?',\n sessionExited: '已退出',\n processExited: '程序已退出,代碼 {code}',\n noSessions: '目前無終端機工作階段',\n connectionFailed: '終端機服務連線失敗',\n connectionClosed: '終端機連線已關閉',\n connectionError: '終端機連線錯誤',\n },\n\n // 群聊\n groupChat: {\n title: '群聊',\n createRoom: '建立房間',\n joinByCode: '透過邀請碼加入',\n roomName: '房間名稱',\n roomNamePlaceholder: '輸入房間名稱',\n inviteCode: '邀請碼',\n autoGenerate: '自動產生',\n noRooms: '目前無房間',\n selectOrCreate: '選擇或建立一個房間開始聊天',\n agents: '智慧代理',\n addAgent: '新增智慧代理',\n selectProfile: '選擇一個設定檔',\n agentAdded: '智慧代理已新增',\n agentAlreadyInRoom: '該智慧代理已在房間中',\n agentAddFailedCount: '{count} 個智慧代理未新增:{details}',\n noAgents: '目前房間無智慧代理',\n members: '成員',\n roomCreated: '房間已建立',\n roomDeleted: '房間已刪除',\n roomCloned: '房間已複製',\n cloneRoom: '複製房間',\n copyRoomLink: '複製房間連結',\n deleteRoomConfirm: '確定刪除這個房間嗎?',\n clearContext: '清理上下文',\n clearContextConfirm: '確定清理目前房間上下文嗎?訊息和壓縮快照會被刪除,智慧代理和成員會保留。',\n contextCleared: '上下文已清理',\n you: '你',\n joined: '已加入房間',\n joinFailed: '加入房間失敗',\n inputPlaceholder: '輸入訊息... (Enter 發送)',\n messageDisplayLimit: '群聊最多顯示 600 則訊息',\n enterCode: '輸入邀請碼',\n yourName: '你的名稱',\n yourNamePlaceholder: '輸入你的群聊暱稱',\n yourDescription: '自我介紹(選填)',\n yourDescriptionPlaceholder: '介紹一下你自己...',\n agentName: 'Agent 名稱',\n agentNamePlaceholder: '自訂名稱(留空則使用 profile 名稱)',\n agentDesc: 'Agent 描述',\n agentDescPlaceholder: '描述這個 agent 的作用...',\n agentReplying: '正在回覆...',\n agentCompressing: '正在壓縮上下文...',\n compressionSettings: '壓縮設定',\n triggerTokens: '觸發壓縮 Token 數',\n triggerTokensDesc: '訊息 token 數超過此值時觸發上下文壓縮',\n maxHistoryTokens: '最大歷史 Token 數',\n maxHistoryTokensDesc: '壓縮後傳送給 LLM 的最大 token 數',\n tailMessageCount: '保留最近訊息數',\n tailMessageCountDesc: '壓縮後保留最近的原始訊息條數',\n compressionConfig: '壓縮設定',\n compressionSaved: '壓縮設定已儲存',\n compressNow: '立即壓縮',\n compressingInProgress: '正在壓縮中,請稍後',\n },\n\n // 用量統計\n usage: {\n title: '用量統計',\n refresh: '重新整理',\n totalTokens: '總 Token 數',\n inputTokens: '輸入',\n outputTokens: '輸出',\n totalSessions: '總工作階段數',\n avgPerDay: '日均 ~{n}',\n estimatedCost: '預估費用',\n cacheHitRate: '快取命中率',\n modelBreakdown: '模型分布',\n dailyTrend: '每日用量',\n date: '日期',\n tokens: 'Token',\n cache: '快取',\n cacheRead: '快取讀取',\n cacheWrite: '快取寫入',\n sessions: '工作階段',\n cost: '費用',\n noData: '目前無用量資料',\n },\n\n skillsUsage: {\n title: '技能用量',\n subtitle: '追蹤 Parsifal 工作階段中的技能載入與編輯',\n refresh: '重新整理',\n periodSelector: '技能用量期間',\n periodLabel: '{days}天',\n summary: '總覽',\n totalActions: '操作',\n loads: '載入',\n edits: '編輯',\n distinctSkills: '技能數',\n topSkills: '熱門',\n dailyTrend: '趨勢',\n periodSummary: '最近 {days} 天',\n skill: '技能',\n share: '占比',\n lastUsed: '最近',\n noData: '暫無技能用量資料',\n loadFailed: '技能用量載入失敗',\n otherSkills: '其他技能',\n },\n\n // 檔案管理\n files: {\n title: '檔案',\n fileTree: '檔案樹',\n tree: '目錄樹',\n list: '檔案清單',\n breadcrumbRoot: '根目錄',\n newFile: '新增檔案',\n newFolder: '新增資料夾',\n upload: '上傳',\n refresh: '重新整理',\n open: '開啟',\n edit: '編輯',\n preview: '預覽',\n download: '下載',\n copyPath: '複製路徑',\n rename: '重新命名',\n delete: '刪除',\n name: '名稱',\n size: '大小',\n modified: '修改時間',\n actions: '操作',\n emptyDir: '空目錄',\n loading: '載入中...',\n confirmDelete: '確定要刪除「{name}」嗎?',\n confirmDeleteDir: '確定要刪除目錄「{name}」及其所有內容嗎?',\n deleteFailed: '刪除失敗',\n deleted: '已刪除',\n renameTo: '重新命名為',\n newFileName: '檔案名稱',\n newFolderName: '資料夾名稱',\n created: '已建立',\n createFailed: '建立失敗',\n renamed: '已重新命名',\n renameFailed: '重新命名失敗',\n uploadSuccess: '已上傳 {count} 個檔案',\n uploadFailed: '上傳失敗',\n saveFailed: '儲存失敗',\n saved: '已儲存',\n unsavedChanges: '有未儲存的變更,是否捨棄?',\n pathCopied: '路徑已複製',\n fileTooLarge: '檔案過大(最大 10MB)',\n permissionDenied: '無法修改受保護的檔案',\n notFound: '檔案或目錄不存在',\n backendError: '檔案操作失敗',\n dragDropHint: '拖曳檔案至此處上傳',\n closeEditor: '關閉編輯器',\n closePreview: '關閉',\n saveFile: '儲存',\n },\n\n // 下載\n download: {\n downloading: '正在下載...',\n downloadFailed: '下載失敗',\n fileNotFound: '檔案不存在或已被刪除',\n fileTooLarge: '檔案過大(超過限制)',\n backendError: '檔案讀取失敗,遠端環境可能不可用',\n backendTimeout: '檔案讀取逾時',\n unsupportedBackend: '目前 terminal backend 暫不支援檔案下載',\n invalidPath: '無效的檔案路徑',\n contentDisplay: '內容展示',\n download: '下載',\n downloadFile: '下載檔案',\n },\n\n // 更新日誌\n changelog: {\n new_0_6_15_1: '聊天側欄完成重新設計,並最佳化歷史載入控制,長對話中的導覽更清晰',\n new_0_6_15_2: '新增技能命令選擇器,可以更快發現並從輸入框插入可用的 skill 命令',\n new_0_6_15_3: '新增 Claude 和 Gemini OAuth Provider,修正 Gemini 模型 ID 正規化,並支援在模型選擇器中手動刷新 config.yaml 變更',\n new_0_6_15_4: 'Thinking 狀態更新了指示器和工具列布局,reasoning 進度更容易閱讀,也不會打斷訊息流',\n new_0_6_15_5: '深色主題下 input 和 select 等表單控制項的邊框恢復可見',\n new_0_6_15_6: 'Runtime 啟動流程會先檢查本機 runtime,再展示下載來源選擇,並在啟動繼續時保持檢查狀態可見',\n new_0_6_15_7: '桌面端自動更新不再被殘留應用程式行程阻塞,Windows 退出時的 gateway 清理也更完整',\n new_0_6_15_8: '應用退出時會停止託管的 gateways;在託管運行期間,異常退出的 gateway 仍會自動拉起',\n new_0_6_15_9: '桌面端退出時會更平滑地關閉 server,減少 backend、bridge 和 gateway 孤兒行程殘留',\n new_0_6_15_10: 'README 和官網展示改為一張動圖 walkthrough,官網頂部 Hero 使用單獨的靜態預覽圖',\n new_0_6_14_1: '官網和文件完成更新:重新設計落地頁、強制淺色展示,並新增 Parsifal Studio 用戶端手冊',\n new_0_6_14_2: '完成通知現在可在桌面殼和支援的瀏覽器中提示任務已結束',\n new_0_6_14_3: '桌面端新增自訂標題列、啟動閃屏、更寬的授權抽屜、置中的載入狀態,並修復 Linux 視窗控制按鈕',\n new_0_6_14_4: 'Workspace 資料夾增加直接操作,檔案和目錄在介面中更容易開啟、管理和使用',\n new_0_6_14_5: 'Profile 與運行時啟動更穩定:拒絕保留名稱重新命名、修復 Linux deb 啟動路徑、移除舊 gateway 清理邏輯,並減少 Coding Agent 恢復時不必要的 bridge 檢查',\n new_0_6_14_6: '群聊身份在更換瀏覽器後更可靠,agent 和成員名稱會跟隨帳號/Profile 狀態保持一致',\n new_0_6_14_7: '靜態資源壓縮處理已修復,打包 Web UI 檔案交付更精簡也更安全',\n new_0_6_14_8: '裝置配對更安全:啟動時生成配對碼,請求進入審批前需要校驗,頻繁錯誤會被限制,同時保留可信區域網路發現邏輯',\n new_0_6_14_9: '桌面端 Claude Code 偵測會讀取使用者 shell PATH 和常見套件管理器目錄,macOS 與 Linux 上更容易找到已安裝的 coding agent',\n new_0_6_14_10: 'Parsifal 寫入門禁覆蓋記憶和技能寫入,支援待審核列表、bridge 模組拆分、本地化授權介面,並改善桌面啟動體驗',\n new_0_6_13_1: 'Alibaba Coding Plan 新增 qwen3.7-plus 模型預設',\n new_0_6_13_2: '檔案處理更安全:路徑校驗支援字面量點號,並拒絕異常編碼的技能匯入檔名',\n new_0_6_13_3: '聊天在行動端和不支援 speech synthesis 的瀏覽器中更穩定',\n new_0_6_13_4: 'Agent 失敗時會保留 bridge 錯誤尾部資訊,Claude Code root 權限啟動也更可靠',\n new_0_6_13_5: '桌面更新更可靠:Windows 升級會關閉殘留 Parsifal Studio 行程,優先檢查 Cloudflare,並保留 GitHub 更新源兜底',\n new_0_6_13_6: '發佈自動化調整:Web UI 和 Docker release 不再標記為 GitHub Latest,完整桌面 release 手動建置成功後才提升為 Latest',\n new_0_6_12_1: 'Coding Agent 升級為更完整的聊天體驗,支援獨立會話、更清晰的歷史記錄、保留工具/最終輸出,並最佳化聊天狀態恢復',\n new_0_6_12_2: '桌面端在 Windows 下啟動 Claude Code 和 Codex 更可靠,修復命令解析問題,並提供更可讀的失敗資訊',\n new_0_6_12_3: 'Coding Agent 的 reasoning、佇列狀態和錯誤展示更容易追蹤,支援 reasoning 內容儲存,並使用不阻塞捲動的佇列浮層',\n new_0_6_12_4: '桌面端運行時版本管理服務於 Coding Agent 工作流,支援切換 active version、官網版本清單、發佈打包,並且只在桌面環境顯示',\n new_0_6_12_5: '聊天支援按會話設定 reasoning effort,兼容模型可單獨調整;Coding Agent 場景會隱藏不適用的控制項',\n new_0_6_12_6: '群聊更穩定,修復視窗尺寸變化後的 mention 定位,並讓上傳請求跟隨目前 active Profile',\n new_0_6_12_7: '檔案、下載和 Markdown 預覽更穩定,修復檔案列表欄位對齊、原始百分號下載 URL,以及本地音訊連結的 inline 渲染',\n new_0_6_12_8: '遠端裝置配對更方便,支援手動配對和 LAN HTTP fallback',\n new_0_6_12_9: '聊天渲染繼續打磨,修復高亮程式碼區塊和超長連續字元撐出螢幕的問題;diff 程式碼區塊仍保留橫向捲動以保持行對齊',\n new_0_6_12_10: '其他前端最佳化包括遠端裝置配對 fallback、中文側邊欄文案、登出身份展示,以及移除未接線的 Busy Input 選項',\n new_0_6_11_1: 'LAN 裝置發現新增配對審批、申請歷史、重複申請處理、狀態同步,以及撤銷裝置權限時的清理邏輯',\n new_0_6_11_2: 'LAN Peer 已接入 MCP,包含內建 MCP 啟動、命令 shim、Peer Socket 重連、本地連線列表,以及終端會話數量限制',\n new_0_6_11_3: '統一後端 TTS 合成和 MiMo 音色複刻設定已可用,並支援保存設定前試聽',\n new_0_6_11_4: 'Gateway 自動啟動設定現在支援按 Profile 多選過濾、白名單行為和啟動 Profile 選擇測試',\n new_0_6_11_5: '聊天滾動和動效更穩定,修復初次進入滾動到底部、滾動鎖定、底部跟隨時機和 reduced-motion 淡入處理',\n new_0_6_11_6: 'Agent Bridge 和運行時流程更可靠,修復 abort 超時、恢復時誤啟動、execute-code 授權記憶、Windows Gateway 恢復和開發終端代理',\n new_0_6_11_7: '安全性增強:加固 Web UI Origin 策略、安全回應標頭,並限制 server token 只能走本地回環訪問',\n new_0_6_11_8: '群聊和工具結果渲染更穩定,支援重連後重新加入房間、保留 reasoning/tool 邊界、加固工具 payload 渲染並優化 diff 展示',\n new_0_6_11_9: '技能和 Workspace 流程支援外部技能目錄、匯入/刪除自動刷新、按 sourcePath 分組、本地化資料夾選擇文案和移動端技能操作圖示',\n new_0_6_11_10: '裝置頁布局、原生會話標題、瀏覽器分頁標題、Kanban 父任務 ID 和 OpenRouter 歸屬文案做了介面優化',\n new_0_6_10_1: '桌面運行時啟動更可靠,支援下載來源選擇、更安全的壓縮包解壓、修正運行時快取路徑、Windows CLI 啟動修復,以及本地化進度和錯誤介面',\n new_0_6_10_2: '輔助壓縮模型設定允許每個 Profile 為聊天上下文壓縮選擇專用 provider/model,不再只能使用目前聊天模型',\n new_0_6_10_3: 'Provider 模型目錄現在可在介面中手動刷新',\n new_0_6_10_4: '使用者可以上傳帳號頭像,群聊成員也會一致同步並展示帳號頭像',\n new_0_6_10_5: 'Workspace 文件瀏覽支援更完整的文字預覽,並擴展源碼和文件檔案的右鍵選單覆蓋',\n new_0_6_10_6: 'Agent Bridge 更可靠:會刷新工具審批 allowlist,並相容 Windows tasklist/taskkill 輸出解碼,避免 UnicodeDecodeError 崩潰',\n new_0_6_10_7: '官網文案統一展示為 Parsifal Studio,同時保留 npm package、CLI、倉庫和環境變數等技術標識不變',\n new_0_6_10_8: 'OpenAPI 文件補充頭像、輔助模型配置、Provider 模型快取刷新和 Parsifal 會話匯入端點',\n new_0_6_10_9: '記憶備註現在可以保存為空內容,清空個人備註時不會再保存失敗',\n new_0_6_10_10: '倉庫 harness 文件現在會追蹤 Chat 鏈路變更,bridge 和 chat runtime 更新需要先記錄後才能通過 CI',\n },\n governanceDemo: {\n title: 'Governance Demo',\n runAnalysis: 'Run analysis',\n reset: 'Reset',\n notePlaceholder: 'Optional note override...',\n loading: 'Analyzing...',\n error: 'Error: {message}',\n detectorTitle: 'Layer 2 — privacy-filter.cpp NER (local PII model)',\n detectorProvisioned: 'Provisioned',\n detectorNotProvisioned: 'Not provisioned',\n detectorNote: 'Note',\n detectorBinaryPath: 'Binary',\n detectorModelPath: 'Model',\n detectorFallback: 'Deterministic layers active; NER detector not provisioned',\n stage1Title: 'Stage 1 — Input',\n stage1Structured: 'Structured payload',\n stage1Note: 'Note',\n stage2Title: 'Stage 2 — Detection',\n stage2Source: 'Source',\n stage2Kind: 'Kind',\n stage2Real: 'Real value',\n stage2Pseudonym: 'Pseudonym',\n stage2Occurrences: 'Occurrences',\n sourceDictionary: 'Dictionary',\n sourceRegex: 'Regex',\n sourcePrivacyFilter: 'Privacy filter',\n stage3Title: 'Stage 3 — Masking',\n stage3Raw: 'RAW (what the user sent)',\n stage3Masked: 'MASKED (what the model sees)',\n stage3Caption: 'The model never sees raw client data.',\n stage4Title: 'Stage 4 — Round-trip',\n stage4Restored: 'Restored for display',\n stage4Reversible: 'Reversible',\n stage4Failed: 'Round-trip failed',\n stage5Title: 'Stage 5 — Audit log',\n stage5Seq: 'Seq',\n stage5Kind: 'Kind',\n stage5Tool: 'Tool',\n stage5Payload: 'Payload',\n stage5PrevHash: 'Prev hash',\n stage5Hash: 'Hash',\n stage5ChainVerified: 'Chain verified',\n stage5ChainBroken: 'Chain BROKEN at seq {seq}',\n stage5VerifyChain: 'Verify chain',\n stage5Tamper: 'Tamper (demo attack)',\n stage5ResetDemo: 'Reset demo',\n stage6Title: 'Stage 6 — Read-only mode',\n stage6Description: 'Read-only audit mode prevents destructive exports while the audit chain is under review.',\n stage6Export: 'Try export (Cartus billing run)',\n stage6Blocked: 'Blocked ({status}) — read-only audit mode',\n stage6Allowed: 'Allowed (would export)',\n introTitle: 'How client data stays private',\n introBody: 'Walk through the six steps an auditor sees — from the original client information to the tamper-proof audit log.',\n navStart: 'Start the walkthrough',\n navNext: 'Next',\n navBack: 'Back',\n navRestart: 'Start over',\n stepIndicator: 'Step {current} of {total}',\n step1Headline: 'The client information',\n step1Caption: 'This is the client information an auditor wants the AI to analyze — company names, a person, bank and contact details.',\n step2Headline: 'Finding what\\'s sensitive',\n step2Caption: 'Before anything is sent, the system automatically spots every sensitive item.',\n step3Headline: 'What the AI actually sees',\n step3Caption: 'Each sensitive item is swapped for a codename. The AI works only with codenames — it never sees the real data.',\n step4Headline: 'Putting the real names back',\n step4Caption: 'When results return, codenames are swapped back to real names for you. Nothing is lost or altered.',\n step5Headline: 'A tamper-proof record',\n step5Caption: 'Every action is written to a sealed log. Press \\'Simulate tampering\\' to watch the system catch a changed record.',\n step6Headline: 'Read-only by design',\n step6Caption: 'The AI can read records but can never export or change them. Try the export — it\\'s blocked.',\n upload: {\n useSample: 'Use sample',\n useUpload: 'Upload a file',\n dropzoneTitle: 'Drag and drop a file here, or click to select',\n dropzoneHint: 'PDF, DOCX, XLSX, CSV, TXT, MD, JSON, images',\n analyzing: 'Analyzing file...',\n filename: 'File',\n kind: 'Type',\n pages: 'Pages',\n chars: 'Characters',\n ocrUsed: 'OCR used',\n truncated: 'Truncated',\n scannedNote: 'Note',\n fileTooLarge: 'File is too large',\n unsupported: 'Unsupported file type',\n failed: 'Upload failed',\n },\n },\n}"}}, "tests/server/coding-agents-launch.test.ts": {"09d34511c5561945f8be97fc0b1ffaaadda18636": {"t": 1785746583.9279525, "n": 36057, "h": "a0baa50687e26ef1", "c": "import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs'\nimport { tmpdir } from 'os'\nimport { dirname, join } from 'path'\nimport { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'\nimport { claudeProxyMessages, claudeProxyModels, registerClaudeCodeProxyTarget } from '../../packages/server/src/services/agent-runner/proxies/claude-code-proxy'\nimport { codexProxyModels, codexProxyResponses, registerCodexProxyTarget } from '../../packages/server/src/services/agent-runner/proxies/codex-proxy'\nimport { prepareCodingAgentLaunch } from '../../packages/server/src/services/coding-agents'\n\nconst homes: string[] = []\n\nfunction mockProcessUid(uid: number) {\n if (typeof process.getuid !== 'function') {\n Object.defineProperty(process, 'getuid', { configurable: true, value: () => uid })\n }\n if (typeof process.geteuid !== 'function') {\n Object.defineProperty(process, 'geteuid', { configurable: true, value: () => uid })\n }\n vi.spyOn(process, 'getuid').mockReturnValue(uid)\n vi.spyOn(process, 'geteuid').mockReturnValue(uid)\n}\n\nfunction makeHome() {\n const home = mkdtempSync(join(tmpdir(), 'parsifal-coding-agent-launch-'))\n homes.push(home)\n process.env.HERMES_WEB_UI_HOME = home\n process.env.HERMES_CODING_AGENT_GLOBAL_HOME = join(home, 'global-home')\n return home\n}\n\nbeforeEach(() => {\n mockProcessUid(1000)\n})\n\nafterEach(() => {\n delete process.env.HERMES_WEB_UI_HOME\n delete process.env.HERMES_CODING_AGENT_GLOBAL_HOME\n vi.restoreAllMocks()\n vi.unstubAllGlobals()\n for (const home of homes.splice(0)) rmSync(home, { recursive: true, force: true })\n})\n\nfunction makeProxyContext(routeKey: string, token: string, body: any): any {\n return {\n params: { key: routeKey },\n request: { body },\n responseHeaders: {} as Record<string, string>,\n get(name: string) {\n if (name.toLowerCase() === 'authorization') return `Bearer ${token}`\n return ''\n },\n set(name: string, value: string) {\n this.responseHeaders[name] = value\n },\n }\n}\n\ndescribe('coding agent launch preparation', () => {\n it('launches Claude Code with the global config when requested', async () => {\n const home = makeHome()\n\n const result = await prepareCodingAgentLaunch('claude-code', {\n mode: 'global',\n profile: 'default',\n })\n\n expect(result).toMatchObject({\n agentId: 'claude-code',\n mode: 'global',\n profile: 'default',\n provider: 'global',\n model: '',\n rootDir: join(home, 'coding-agent', 'workspace', 'default', 'global'),\n workspaceDir: join(home, 'coding-agent', 'workspace', 'default', 'global'),\n command: 'claude',\n args: ['--dangerously-skip-permissions'],\n env: {},\n shellCommand: `cd ${join(home, 'coding-agent', 'workspace', 'default', 'global')} && claude --dangerously-skip-permissions`,\n files: [{\n key: 'prompt',\n path: '~/.claude/CLAUDE.md',\n absolutePath: join(home, 'global-home', '.claude', 'CLAUDE.md'),\n }],\n })\n const prompt = readFileSync(join(home, 'global-home', '.claude', 'CLAUDE.md'), 'utf-8')\n expect(prompt).toContain('<!-- BEGIN PARSIFAL WEB UI PROMPT -->')\n expect(prompt).toContain('# 输出格式规范')\n })\n\n it('uses Claude Code auto permission mode instead of dangerous bypass when running as root', async () => {\n mockProcessUid(0)\n const home = makeHome()\n\n const result = await prepareCodingAgentLaunch('claude-code', {\n mode: 'global',\n profile: 'default',\n })\n\n expect(result).toMatchObject({\n agentId: 'claude-code',\n mode: 'global',\n rootDir: join(home, 'coding-agent', 'workspace', 'default', 'global'),\n command: 'claude',\n args: ['--permission-mode', 'auto'],\n shellCommand: `cd ${join(home, 'coding-agent', 'workspace', 'default', 'global')} && claude --permission-mode auto`,\n })\n })\n\n it('launches Codex with the global config when requested', async () => {\n const home = makeHome()\n\n const result = await prepareCodingAgentLaunch('codex', {\n mode: 'global',\n profile: 'default',\n })\n\n expect(result).toMatchObject({\n agentId: 'codex',\n mode: 'global',\n profile: 'default',\n provider: 'global',\n model: '',\n rootDir: join(home, 'coding-agent', 'workspace', 'default', 'global'),\n workspaceDir: join(home, 'coding-agent', 'workspace', 'default', 'global'),\n command: 'codex',\n args: [],\n env: {},\n shellCommand: `cd ${join(home, 'coding-agent', 'workspace', 'default', 'global')} && codex`,\n files: [],\n })\n })\n\n it('preserves existing global Claude Code prompt files while updating the Parsifal block', async () => {\n const home = makeHome()\n const claudePromptPath = join(home, 'global-home', '.claude', 'CLAUDE.md')\n mkdirSync(dirname(claudePromptPath), { recursive: true })\n writeFileSync(claudePromptPath, 'Existing Claude notes\\n')\n\n await prepareCodingAgentLaunch('claude-code', { mode: 'global', profile: 'default' })\n await prepareCodingAgentLaunch('claude-code', { mode: 'global', profile: 'default' })\n\n const claudePrompt = readFileSync(claudePromptPath, 'utf-8')\n expect(claudePrompt).toContain('Existing Claude notes')\n expect(claudePrompt.match(/BEGIN PARSIFAL WEB UI PROMPT/g)).toHaveLength(1)\n })\n\n it('uses a selected workspace directory when launching a coding agent', async () => {\n const home = makeHome()\n const workspace = join(home, 'selected workspace')\n\n const result = await prepareCodingAgentLaunch('codex', {\n profile: 'default',\n provider: 'openrouter',\n model: 'openai/gpt-oss-20b:free',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n workspace,\n })\n\n expect(result.rootDir).toBe(join(home, 'coding-agent', 'model', 'default', 'openrouter', 'codex'))\n expect(result.workspaceDir).toBe(workspace)\n expect(result.shellCommand).toContain(workspace)\n })\n\n it('launches Claude Code with scoped settings instead of a CLI --model override', async () => {\n const home = makeHome()\n\n const result = await prepareCodingAgentLaunch('claude-code', {\n profile: 'default',\n provider: 'openrouter',\n model: 'cognitivecomputations/dolphin-mistral-24b-venice-edition:free',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n })\n\n expect(result.rootDir).toBe(join(home, 'coding-agent', 'model', 'default', 'openrouter', 'claude-code'))\n expect(result.workspaceDir).toBe(join(home, 'coding-agent', 'workspace', 'default', 'openrouter'))\n expect(result.args).toEqual([\n '--settings',\n join(result.rootDir, 'settings.json'),\n '--mcp-config',\n join(result.rootDir, 'mcp.json'),\n '--dangerously-skip-permissions',\n ])\n expect(result.shellCommand).toContain(`cd ${join(home, 'coding-agent', 'workspace', 'default', 'openrouter')} &&`)\n expect(result.shellCommand).toContain(join(result.rootDir, 'launch.sh'))\n expect(result.shellCommand).not.toContain('ANTHROPIC_API_KEY')\n expect(result.shellCommand).not.toContain('hwui_')\n expect(result.shellCommand).not.toContain('--model')\n const launcher = readFileSync(join(result.rootDir, 'launch.sh'), 'utf-8')\n expect(launcher).toContain('exec claude --settings')\n expect(launcher).toContain('--dangerously-skip-permissions')\n expect(launcher).not.toContain('--model')\n\n const settings = JSON.parse(readFileSync(join(result.rootDir, 'settings.json'), 'utf-8'))\n expect(settings.model).toBe('cognitivecomputations/dolphin-mistral-24b-venice-edition:free')\n expect(settings.env.ANTHROPIC_API_KEY).toMatch(/^hwui_/)\n expect(settings.env).not.toHaveProperty('ANTHROPIC_AUTH_TOKEN')\n expect(settings.env.ANTHROPIC_BASE_URL).toMatch(/^http:\\/\\/127\\.0\\.0\\.1:\\d+\\/api\\/claude-code-proxy\\/.+$/)\n expect(settings.env).toMatchObject({\n ANTHROPIC_MODEL: 'cognitivecomputations/dolphin-mistral-24b-venice-edition:free',\n ANTHROPIC_CUSTOM_MODEL_OPTION: 'cognitivecomputations/dolphin-mistral-24b-venice-edition:free',\n ANTHROPIC_CUSTOM_MODEL_OPTION_NAME: 'Dolphin Mistral 24b Venice Edition:Free',\n ANTHROPIC_DEFAULT_HAIKU_MODEL: 'cognitivecomputations/dolphin-mistral-24b-venice-edition:free',\n ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME: 'Dolphin Mistral 24b Venice Edition:Free',\n ANTHROPIC_DEFAULT_SONNET_MODEL: 'cognitivecomputations/dolphin-mistral-24b-venice-edition:free',\n ANTHROPIC_DEFAULT_SONNET_MODEL_NAME: 'Dolphin Mistral 24b Venice Edition:Free',\n ANTHROPIC_DEFAULT_OPUS_MODEL: 'cognitivecomputations/dolphin-mistral-24b-venice-edition:free',\n ANTHROPIC_DEFAULT_OPUS_MODEL_NAME: 'Dolphin Mistral 24b Venice Edition:Free',\n })\n expect(settings.env.ANTHROPIC_DEFAULT_SONNET_MODEL).not.toBe('claude-sonnet-4-6')\n\n const mcp = JSON.parse(readFileSync(join(result.rootDir, 'mcp.json'), 'utf-8'))\n expect(mcp.mcpServers['parsifal-studio']).toMatchObject({\n command: process.execPath,\n args: [join(process.cwd(), 'bin/parsifal-web-ui-mcp.mjs')],\n env: {\n HERMES_WEB_UI_URL: 'http://127.0.0.1:8648',\n HERMES_WEB_UI_HOME: home,\n HERMES_WEBUI_STATE_DIR: home,\n HERMES_WEB_UI_PROFILE: 'default',\n HERMES_MCP_SERVER_NAME: 'parsifal-studio-mcp',\n HERMES_WEB_UI_MANAGED_MCP: '1',\n },\n })\n\n const prompt = readFileSync(join(result.rootDir, 'CLAUDE.md'), 'utf-8')\n expect(prompt).toContain('# 输出格式规范')\n expect(prompt).toContain('当你的回复中包含图片、视频或文件引用时')\n })\n\n it('isolates Claude Code settings for hidden chat runs only', async () => {\n const home = makeHome()\n\n const result = await prepareCodingAgentLaunch('claude-code', {\n profile: 'default',\n provider: 'openrouter',\n model: 'anthropic/claude-sonnet-4.6',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n isolateSettings: true,\n })\n\n expect(result.args).toEqual([\n '--settings',\n join(result.rootDir, 'settings.json'),\n '--setting-sources',\n 'local',\n '--mcp-config',\n join(result.rootDir, 'mcp.json'),\n '--dangerously-skip-permissions',\n ])\n expect(result.shellCommand).not.toContain('--setting-sources local')\n const launcher = readFileSync(join(result.rootDir, 'launch.sh'), 'utf-8')\n expect(launcher).toContain('--setting-sources local')\n expect(result.rootDir).toBe(join(home, 'coding-agent', 'model', 'default', 'openrouter', 'claude-code'))\n })\n\n it('uses Claude Code auto permission mode for scoped root launches', async () => {\n mockProcessUid(0)\n const home = makeHome()\n\n const result = await prepareCodingAgentLaunch('claude-code', {\n profile: 'default',\n provider: 'openrouter',\n model: 'anthropic/claude-sonnet-4.6',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n isolateSettings: true,\n })\n\n expect(result.args).toEqual([\n '--settings',\n join(result.rootDir, 'settings.json'),\n '--setting-sources',\n 'local',\n '--mcp-config',\n join(result.rootDir, 'mcp.json'),\n '--permission-mode',\n 'auto',\n ])\n const launcher = readFileSync(join(result.rootDir, 'launch.sh'), 'utf-8')\n expect(launcher).toContain('--permission-mode auto')\n expect(launcher).not.toContain('--dangerously-skip-permissions')\n expect(result.rootDir).toBe(join(home, 'coding-agent', 'model', 'default', 'openrouter', 'claude-code'))\n })\n\n it('keeps Claude Code protocol overrides behind the local proxy', async () => {\n const home = makeHome()\n\n const result = await prepareCodingAgentLaunch('claude-code', {\n profile: 'default',\n provider: 'openrouter',\n model: 'anthropic/claude-sonnet-4.6',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n apiMode: 'anthropic_messages',\n })\n\n const settings = JSON.parse(readFileSync(join(result.rootDir, 'settings.json'), 'utf-8'))\n expect(settings.env.ANTHROPIC_API_KEY).toMatch(/^hwui_/)\n expect(settings.env).not.toHaveProperty('ANTHROPIC_AUTH_TOKEN')\n expect(settings.env.ANTHROPIC_BASE_URL).toMatch(/^http:\\/\\/127\\.0\\.0\\.1:\\d+\\/api\\/claude-code-proxy\\/.+$/)\n })\n\n it('keeps Codex model selection on the CLI while isolating CODEX_HOME', async () => {\n const home = makeHome()\n\n const result = await prepareCodingAgentLaunch('codex', {\n profile: 'default',\n provider: 'openrouter',\n model: 'openai/gpt-oss-20b:free',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-test',\n })\n\n expect(result.rootDir).toBe(join(home, 'coding-agent', 'model', 'default', 'openrouter', 'codex'))\n expect(result.workspaceDir).toBe(join(home, 'coding-agent', 'workspace', 'default', 'openrouter'))\n expect(result.args).toEqual(['--model', 'openai/gpt-oss-20b:free'])\n expect(result.env).toEqual({ CODEX_HOME: result.rootDir })\n\n const config = readFileSync(join(result.rootDir, 'config.toml'), 'utf-8')\n expect(config).toContain('requires_openai_auth = false')\n expect(config).toContain(`model_catalog_json = \"${join(result.rootDir, 'codex-model-catalog.json')}\"`)\n expect(config).toContain('model_reasoning_summary = \"auto\"')\n expect(config).toContain('[mcp_servers.parsifal-studio]')\n expect(config).toContain(`command = \"${process.execPath}\"`)\n expect(config).toContain(`args = [\"${join(process.cwd(), 'bin/parsifal-web-ui-mcp.mjs')}\"]`)\n expect(config).toContain(`env = { HERMES_WEB_UI_URL = \"http://127.0.0.1:8648\", HERMES_WEB_UI_HOME = \"${home}\"`)\n expect(config).toContain('HERMES_WEBUI_STATE_DIR = \"')\n expect(config).toContain('HERMES_WEB_UI_PROFILE = \"default\"')\n expect(config).toContain('HERMES_MCP_SERVER_NAME = \"parsifal-studio-mcp\"')\n expect(config).toContain('HERMES_WEB_UI_MANAGED_MCP = \"1\"')\n\n expect(result.files.some(file => file.key === 'agents')).toBe(false)\n\n const catalog = JSON.parse(readFileSync(join(result.rootDir, 'codex-model-catalog.json'), 'utf-8'))\n expect(catalog.models.some((entry: any) => entry.slug === 'openai/gpt-oss-20b:free')).toBe(true)\n expect(catalog.models[0]).toHaveProperty('base_instructions')\n expect(catalog.models[0]).toHaveProperty('model_messages')\n expect(catalog.models[0]).toHaveProperty('default_reasoning_summary', 'auto')\n })\n\n it('points Codex Chat Completions providers at the local Responses proxy', async () => {\n const home = makeHome()\n\n const result = await prepareCodingAgentLaunch('codex', {\n profile: 'default',\n provider: 'deepseek',\n model: 'deepseek-v4-pro',\n baseUrl: 'https://api.deepseek.com',\n apiKey: 'sk-upstream',\n apiMode: 'chat_completions',\n })\n\n const config = readFileSync(join(result.rootDir, 'config.toml'), 'utf-8')\n expect(config).toContain(`base_url = \"http://127.0.0.1:8648/api/codex-proxy/`)\n expect(config).toContain('wire_api = \"responses\"')\n expect(config).toContain('requires_openai_auth = false')\n expect(config).toMatch(/experimental_bearer_token = \"hwui_[^\"]+\"/)\n expect(result.rootDir).toBe(join(home, 'coding-agent', 'model', 'default', 'deepseek', 'codex'))\n\n const catalog = JSON.parse(readFileSync(join(result.rootDir, 'codex-model-catalog.json'), 'utf-8'))\n const deepseekModel = catalog.models.find((entry: any) => entry.slug === 'deepseek-v4-pro')\n expect(deepseekModel).toMatchObject({\n display_name: 'Deepseek V4 Pro',\n })\n expect(deepseekModel.context_window).toBeGreaterThan(0)\n expect(deepseekModel.max_context_window).toBe(deepseekModel.context_window)\n expect(deepseekModel.model_messages.instructions_template).toContain('{{ base_instructions }}')\n })\n\n it('points Codex Responses providers at the local Responses proxy for stream capture', async () => {\n const home = makeHome()\n\n const result = await prepareCodingAgentLaunch('codex', {\n profile: 'default',\n provider: 'openai-api',\n model: 'gpt-5.5',\n baseUrl: 'https://api.openai.com/v1',\n apiKey: 'sk-upstream',\n apiMode: 'codex_responses',\n sessionId: 'chat-session-1',\n agentSessionId: 'agent-session-1',\n })\n\n const config = readFileSync(join(result.rootDir, 'config.toml'), 'utf-8')\n expect(config).toContain(`base_url = \"http://127.0.0.1:8648/api/codex-proxy/`)\n expect(config).toMatch(/experimental_bearer_token = \"hwui_[^\"]+\"/)\n expect(config).not.toContain('base_url = \"https://api.openai.com/v1\"')\n expect(result.rootDir).toBe(join(home, 'coding-agent', 'model', 'default', 'openai-api', 'codex'))\n })\n\n it('points Codex Anthropic Messages providers at the local Responses proxy', async () => {\n const home = makeHome()\n\n const result = await prepareCodingAgentLaunch('codex', {\n profile: 'default',\n provider: 'anthropic-compatible',\n model: 'claude-sonnet-4-6',\n baseUrl: 'https://api.example.com',\n apiKey: 'sk-upstream',\n apiMode: 'anthropic_messages',\n })\n\n const config = readFileSync(join(result.rootDir, 'config.toml'), 'utf-8')\n expect(config).toContain(`base_url = \"http://127.0.0.1:8648/api/codex-proxy/`)\n expect(config).toContain('wire_api = \"responses\"')\n expect(config).toContain('requires_openai_auth = false')\n expect(config).toMatch(/experimental_bearer_token = \"hwui_[^\"]+\"/)\n expect(result.rootDir).toBe(join(home, 'coding-agent', 'model', 'default', 'anthropic-compatible', 'codex'))\n })\n\n it('adapts Codex Responses requests to OpenAI Chat Completions', async () => {\n makeHome()\n const launch = await prepareCodingAgentLaunch('codex', {\n profile: 'default',\n provider: 'deepseek',\n model: 'deepseek-v4-pro',\n baseUrl: 'https://api.deepseek.com',\n apiKey: 'sk-upstream',\n apiMode: 'chat_completions',\n })\n const config = readFileSync(join(launch.rootDir, 'config.toml'), 'utf-8')\n const routeKey = config.match(/\\/api\\/codex-proxy\\/([^/]+)\\/v1/)?.[1] || ''\n const token = config.match(/experimental_bearer_token = \"([^\"]+)\"/)?.[1] || ''\n const fetchMock = vi.fn(async () => new Response(JSON.stringify({\n id: 'chatcmpl_test',\n choices: [{\n finish_reason: 'stop',\n message: { role: 'assistant', content: 'ok' },\n }],\n usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 },\n }), { status: 200, headers: { 'Content-Type': 'application/json' } }))\n vi.stubGlobal('fetch', fetchMock)\n\n const ctx = makeProxyContext(routeKey, token, {\n max_output_tokens: 16,\n input: [\n { role: 'user', content: [{ type: 'input_text', text: 'hello' }] },\n { role: 'developer', content: [{ type: 'input_text', text: 'be terse' }] },\n ],\n })\n\n await codexProxyResponses(ctx)\n\n expect(fetchMock).toHaveBeenCalledWith('https://api.deepseek.com/v1/chat/completions', expect.objectContaining({\n method: 'POST',\n headers: expect.objectContaining({ Authorization: 'Bearer sk-upstream' }),\n }))\n const requestBody = JSON.parse(fetchMock.mock.calls[0][1].body)\n expect(requestBody).toMatchObject({\n model: 'deepseek-v4-pro',\n max_tokens: 16,\n messages: [\n { role: 'user', content: 'hello' },\n { role: 'system', content: 'be terse' },\n ],\n })\n expect(ctx.body.output[0].content[0].text).toBe('ok')\n expect(ctx.body.usage).toMatchObject({ input_tokens: 3, output_tokens: 1, total_tokens: 4 })\n })\n\n it('adapts Codex Responses requests to Anthropic Messages', async () => {\n makeHome()\n const launch = await prepareCodingAgentLaunch('codex', {\n profile: 'default',\n provider: 'anthropic-compatible',\n model: 'claude-sonnet-4-6',\n baseUrl: 'https://api.example.com',\n apiKey: 'sk-upstream',\n apiMode: 'anthropic_messages',\n })\n const config = readFileSync(join(launch.rootDir, 'config.toml'), 'utf-8')\n const routeKey = config.match(/\\/api\\/codex-proxy\\/([^/]+)\\/v1/)?.[1] || ''\n const token = config.match(/experimental_bearer_token = \"([^\"]+)\"/)?.[1] || ''\n const fetchMock = vi.fn(async () => new Response(JSON.stringify({\n id: 'msg_test',\n type: 'message',\n role: 'assistant',\n model: 'claude-sonnet-4-6',\n content: [\n { type: 'text', text: 'ok' },\n { type: 'tool_use', id: 'toolu_1', name: 'search', input: { query: 'repo' } },\n ],\n stop_reason: 'tool_use',\n usage: { input_tokens: 5, output_tokens: 2 },\n }), { status: 200, headers: { 'Content-Type': 'application/json' } }))\n vi.stubGlobal('fetch', fetchMock)\n\n const ctx = makeProxyContext(routeKey, token, {\n instructions: 'be terse',\n max_output_tokens: 64,\n input: [\n { role: 'user', content: [{ type: 'input_text', text: 'hello' }] },\n { type: 'function_call_output', call_id: 'call_0', output: 'done' },\n ],\n tools: [{\n type: 'function',\n name: 'search',\n description: 'Search files',\n parameters: { type: 'object', properties: { query: { type: 'string' } } },\n }],\n })\n\n await codexProxyResponses(ctx)\n\n expect(fetchMock).toHaveBeenCalledWith('https://api.example.com/v1/messages', expect.objectContaining({\n method: 'POST',\n headers: expect.objectContaining({\n Authorization: 'Bearer sk-upstream',\n 'x-api-key': 'sk-upstream',\n 'anthropic-version': '2023-06-01',\n }),\n }))\n const requestBody = JSON.parse(fetchMock.mock.calls[0][1].body)\n expect(requestBody).toMatchObject({\n model: 'claude-sonnet-4-6',\n system: 'be terse',\n max_tokens: 64,\n messages: [\n { role: 'user', content: [{ type: 'text', text: 'hello' }] },\n { role: 'user', content: [{ type: 'tool_result', tool_use_id: 'call_0', content: 'done' }] },\n ],\n tools: [{\n name: 'search',\n description: 'Search files',\n input_schema: { type: 'object', properties: { query: { type: 'string' } } },\n }],\n })\n expect(ctx.body.output[0].content[0].text).toBe('ok')\n expect(ctx.body.output[1]).toMatchObject({\n type: 'function_call',\n call_id: 'toolu_1',\n name: 'search',\n arguments: '{\"query\":\"repo\"}',\n })\n expect(ctx.body.usage).toMatchObject({ input_tokens: 5, output_tokens: 2, total_tokens: 7 })\n })\n\n it('streams Codex proxy text as complete Responses message events', async () => {\n makeHome()\n const launch = await prepareCodingAgentLaunch('codex', {\n profile: 'default',\n provider: 'deepseek',\n model: 'deepseek-v4-pro',\n baseUrl: 'https://api.deepseek.com',\n apiKey: 'sk-upstream',\n apiMode: 'chat_completions',\n })\n const config = readFileSync(join(launch.rootDir, 'config.toml'), 'utf-8')\n const routeKey = config.match(/\\/api\\/codex-proxy\\/([^/]+)\\/v1/)?.[1] || ''\n const token = config.match(/experimental_bearer_token = \"([^\"]+)\"/)?.[1] || ''\n const encoder = new TextEncoder()\n const fetchMock = vi.fn(async () => new Response(new ReadableStream({\n start(controller) {\n controller.enqueue(encoder.encode('data: {\"choices\":[{\"delta\":{\"content\":\"p\"}}]}\\n\\n'))\n controller.enqueue(encoder.encode('data: {\"choices\":[{\"delta\":{\"content\":\"ong\"}}]}\\n\\n'))\n controller.enqueue(encoder.encode('data: [DONE]\\n\\n'))\n controller.close()\n },\n }), { status: 200, headers: { 'Content-Type': 'text/event-stream' } }))\n vi.stubGlobal('fetch', fetchMock)\n\n const ctx = makeProxyContext(routeKey, token, {\n stream: true,\n input: [{ role: 'user', content: [{ type: 'input_text', text: 'ping' }] }],\n })\n\n await codexProxyResponses(ctx)\n\n const chunks: string[] = []\n for await (const chunk of ctx.body) chunks.push(String(chunk))\n const sse = chunks.join('')\n expect(sse).toContain('event: response.output_item.added')\n expect(sse).toContain('event: response.content_part.added')\n expect(sse).toContain('\"delta\":\"p\"')\n expect(sse).toContain('\"delta\":\"ong\"')\n expect(sse).toContain('event: response.output_text.done')\n expect(sse).toContain('\"text\":\"pong\"')\n expect(sse).toContain('event: response.output_item.done')\n expect(sse).toContain('\"output\":[{\"type\":\"message\"')\n })\n\n it('streams Codex proxy Anthropic text as Responses message events', async () => {\n makeHome()\n const launch = await prepareCodingAgentLaunch('codex', {\n profile: 'default',\n provider: 'anthropic-compatible',\n model: 'claude-sonnet-4-6',\n baseUrl: 'https://api.example.com',\n apiKey: 'sk-upstream',\n apiMode: 'anthropic_messages',\n })\n const config = readFileSync(join(launch.rootDir, 'config.toml'), 'utf-8')\n const routeKey = config.match(/\\/api\\/codex-proxy\\/([^/]+)\\/v1/)?.[1] || ''\n const token = config.match(/experimental_bearer_token = \"([^\"]+)\"/)?.[1] || ''\n const encoder = new TextEncoder()\n const fetchMock = vi.fn(async () => new Response(new ReadableStream({\n start(controller) {\n controller.enqueue(encoder.encode('event: message_start\\ndata: {\"type\":\"message_start\",\"message\":{\"id\":\"msg_test\",\"usage\":{\"input_tokens\":3,\"output_tokens\":0}}}\\n\\n'))\n controller.enqueue(encoder.encode('event: content_block_start\\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}\\n\\n'))\n controller.enqueue(encoder.encode('event: content_block_delta\\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"he\"}}\\n\\n'))\n controller.enqueue(encoder.encode('event: content_block_delta\\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"llo\"}}\\n\\n'))\n controller.enqueue(encoder.encode('event: message_stop\\ndata: {\"type\":\"message_stop\"}\\n\\n'))\n controller.close()\n },\n }), { status: 200, headers: { 'Content-Type': 'text/event-stream' } }))\n vi.stubGlobal('fetch', fetchMock)\n\n const ctx = makeProxyContext(routeKey, token, {\n stream: true,\n input: [{ role: 'user', content: [{ type: 'input_text', text: 'ping' }] }],\n })\n\n await codexProxyResponses(ctx)\n\n const chunks: string[] = []\n for await (const chunk of ctx.body) chunks.push(String(chunk))\n const sse = chunks.join('')\n expect(fetchMock).toHaveBeenCalledWith('https://api.example.com/v1/messages', expect.objectContaining({\n method: 'POST',\n headers: expect.objectContaining({ 'anthropic-version': '2023-06-01' }),\n }))\n expect(sse).toContain('event: response.output_item.added')\n expect(sse).toContain('\"delta\":\"he\"')\n expect(sse).toContain('\"delta\":\"llo\"')\n expect(sse).toContain('event: response.output_text.done')\n expect(sse).toContain('\"text\":\"hello\"')\n expect(sse).toContain('event: response.completed')\n })\n\n it('exposes Codex proxy models with route-token authentication', async () => {\n makeHome()\n const launch = await prepareCodingAgentLaunch('codex', {\n profile: 'default',\n provider: 'deepseek',\n model: 'deepseek-v4-pro',\n baseUrl: 'https://api.deepseek.com',\n apiKey: 'sk-upstream',\n apiMode: 'chat_completions',\n })\n const config = readFileSync(join(launch.rootDir, 'config.toml'), 'utf-8')\n const routeKey = config.match(/\\/api\\/codex-proxy\\/([^/]+)\\/v1/)?.[1] || ''\n const token = config.match(/experimental_bearer_token = \"([^\"]+)\"/)?.[1] || ''\n const ctx = makeProxyContext(routeKey, token, {})\n\n await codexProxyModels(ctx)\n\n expect(ctx.body).toMatchObject({\n object: 'list',\n data: [{ id: 'deepseek-v4-pro', object: 'model', owned_by: 'deepseek' }],\n })\n })\n\n it('adapts Claude Code streaming requests to the Responses API for codex_responses providers', async () => {\n const target = registerClaudeCodeProxyTarget({\n provider: 'fun-codex',\n model: 'gpt-5.5',\n baseUrl: 'https://api.apikey.fun/v1',\n apiKey: 'sk-upstream',\n apiMode: 'codex_responses',\n })\n const encoder = new TextEncoder()\n const fetchMock = vi.fn(async () => new Response(new ReadableStream({\n start(controller) {\n controller.enqueue(encoder.encode('data: {\"type\":\"response.output_text.delta\",\"delta\":\"hi\"}\\n\\n'))\n controller.enqueue(encoder.encode('data: {\"type\":\"response.completed\",\"response\":{\"status\":\"completed\",\"usage\":{\"output_tokens\":1}}}\\n\\n'))\n controller.close()\n },\n }), { status: 200 }))\n vi.stubGlobal('fetch', fetchMock)\n\n const ctx = makeProxyContext(target.routeKey, target.token, {\n stream: true,\n max_tokens: 32,\n messages: [{ role: 'user', content: 'hello' }],\n })\n\n await claudeProxyMessages(ctx)\n\n expect(fetchMock).toHaveBeenCalledWith('https://api.apikey.fun/v1/responses', expect.objectContaining({\n method: 'POST',\n headers: expect.objectContaining({ Authorization: 'Bearer sk-upstream' }),\n }))\n const requestBody = JSON.parse(fetchMock.mock.calls[0][1].body)\n expect(requestBody).toMatchObject({\n model: 'gpt-5.5',\n stream: true,\n store: false,\n max_output_tokens: 32,\n input: [{ role: 'user', content: 'hello' }],\n })\n\n const chunks: string[] = []\n for await (const chunk of ctx.body) chunks.push(String(chunk))\n const sse = chunks.join('')\n expect(ctx.responseHeaders['Content-Type']).toContain('text/event-stream')\n expect(sse).toContain('event: message_start')\n expect(sse).toContain('\"type\":\"text_delta\",\"text\":\"hi\"')\n expect(sse).toContain('event: message_stop')\n })\n\n it('round-trips reasoning_content for DeepSeek-style OpenAI Chat tool calls', async () => {\n const target = registerClaudeCodeProxyTarget({\n provider: 'deepseek',\n model: 'deepseek-reasoner',\n baseUrl: 'https://api.deepseek.com/v1',\n apiKey: 'sk-upstream',\n apiMode: 'chat_completions',\n })\n const fetchMock = vi.fn(async () => new Response(JSON.stringify({\n id: 'chatcmpl_test',\n choices: [{\n finish_reason: 'tool_calls',\n message: {\n role: 'assistant',\n reasoning_content: 'Need to inspect the repository first.',\n content: null,\n tool_calls: [{\n id: 'call_2',\n type: 'function',\n function: { name: 'search', arguments: '{\"query\":\"proxy\"}' },\n }],\n },\n }],\n usage: { prompt_tokens: 12, completion_tokens: 8 },\n }), { status: 200, headers: { 'Content-Type': 'application/json' } }))\n vi.stubGlobal('fetch', fetchMock)\n\n const ctx = makeProxyContext(target.routeKey, target.token, {\n max_tokens: 32,\n messages: [\n { role: 'user', content: 'check it' },\n {\n role: 'assistant',\n content: [\n { type: 'thinking', thinking: 'Need the current repo files.' },\n { type: 'tool_use', id: 'call_1', name: 'search', input: { query: 'reasoning_content' } },\n ],\n },\n {\n role: 'user',\n content: [\n { type: 'tool_result', tool_use_id: 'call_1', content: 'found one file' },\n ],\n },\n ],\n })\n\n await claudeProxyMessages(ctx)\n\n const requestBody = JSON.parse(fetchMock.mock.calls[0][1].body)\n expect(requestBody.messages[1]).toMatchObject({\n role: 'assistant',\n reasoning_content: 'Need the current repo files.',\n tool_calls: [{\n id: 'call_1',\n type: 'function',\n function: { name: 'search', arguments: '{\"query\":\"reasoning_content\"}' },\n }],\n })\n expect(ctx.body.content[0]).toEqual({\n type: 'thinking',\n thinking: 'Need to inspect the repository first.',\n })\n expect(ctx.body.content[1]).toMatchObject({\n type: 'tool_use',\n id: 'call_2',\n name: 'search',\n input: { query: 'proxy' },\n })\n })\n\n it('passes Anthropic Messages providers through the local proxy without exposing upstream credentials', async () => {\n const target = registerClaudeCodeProxyTarget({\n provider: 'fun-claude',\n model: 'claude-sonnet-4-6',\n baseUrl: 'https://api.apikey.fun',\n apiKey: 'sk-upstream',\n apiMode: 'anthropic_messages',\n })\n const fetchMock = vi.fn(async () => new Response(JSON.stringify({\n id: 'msg_test',\n type: 'message',\n role: 'assistant',\n model: 'claude-sonnet-4-6',\n content: [{ type: 'text', text: 'hi' }],\n stop_reason: 'end_turn',\n usage: { input_tokens: 1, output_tokens: 1 },\n }), { status: 200, headers: { 'Content-Type': 'application/json' } }))\n vi.stubGlobal('fetch', fetchMock)\n\n const ctx = makeProxyContext(target.routeKey, target.token, {\n model: 'ignored-client-model',\n max_tokens: 32,\n messages: [{ role: 'user', content: 'hello' }],\n })\n\n await claudeProxyMessages(ctx)\n\n expect(fetchMock).toHaveBeenCalledWith('https://api.apikey.fun/v1/messages', expect.objectContaining({\n method: 'POST',\n headers: expect.objectContaining({\n Authorization: 'Bearer sk-upstream',\n 'x-api-key': 'sk-upstream',\n }),\n }))\n const requestBody = JSON.parse(fetchMock.mock.calls[0][1].body)\n expect(requestBody.model).toBe('claude-sonnet-4-6')\n expect(ctx.body.content[0].text).toBe('hi')\n })\n\n it('keeps Claude proxy routes separate for the same model with different protocols', () => {\n const chat = registerClaudeCodeProxyTarget({\n provider: 'same-provider',\n model: 'same-model',\n baseUrl: 'https://api.example.com/v1',\n apiKey: 'sk-chat',\n apiMode: 'chat_completions',\n })\n const anthropic = registerClaudeCodeProxyTarget({\n provider: 'same-provider',\n model: 'same-model',\n baseUrl: 'https://api.example.com/v1',\n apiKey: 'sk-anthropic',\n apiMode: 'anthropic_messages',\n })\n\n expect(chat.routeKey).not.toBe(anthropic.routeKey)\n expect(chat.token).not.toBe(anthropic.token)\n })\n\n it('keeps proxy routes separate for different hidden agent sessions', () => {\n const first = registerClaudeCodeProxyTarget({\n provider: 'same-provider',\n model: 'same-model',\n baseUrl: 'https://api.example.com/v1',\n apiKey: 'sk-one',\n apiMode: 'chat_completions',\n agentSessionId: 'agent-one',\n chatSessionId: 'chat-one',\n })\n const second = registerClaudeCodeProxyTarget({\n provider: 'same-provider',\n model: 'same-model',\n baseUrl: 'https://api.example.com/v1',\n apiKey: 'sk-two',\n apiMode: 'chat_completions',\n agentSessionId: 'agent-two',\n chatSessionId: 'chat-two',\n })\n\n expect(first.routeKey).not.toBe(second.routeKey)\n expect(first.token).not.toBe(second.token)\n })\n\n it('keeps Codex proxy routes separate for the same model with different upstream URLs', () => {\n const first = registerCodexProxyTarget({\n profile: 'default',\n provider: 'same-provider',\n model: 'same-model',\n baseUrl: 'https://api-one.example.com/v1',\n apiKey: 'sk-one',\n apiMode: 'chat_completions',\n })\n const second = registerCodexProxyTarget({\n profile: 'default',\n provider: 'same-provider',\n model: 'same-model',\n baseUrl: 'https://api-two.example.com/v1',\n apiKey: 'sk-two',\n apiMode: 'chat_completions',\n })\n\n expect(first.routeKey).not.toBe(second.routeKey)\n expect(first.token).not.toBe(second.token)\n })\n\n it('exposes Claude-visible alias models from the local proxy models endpoint', async () => {\n const target = registerClaudeCodeProxyTarget({\n provider: 'openrouter',\n model: 'cognitivecomputations/dolphin-mistral-24b-venice-edition:free',\n baseUrl: 'https://openrouter.ai/api/v1',\n apiKey: 'sk-upstream',\n apiMode: 'codex_responses',\n })\n const ctx = makeProxyContext(target.routeKey, target.token, {})\n\n await claudeProxyModels(ctx)\n\n const ids = ctx.body.data.map((model: any) => model.id)\n expect(ids).toContain('claude-haiku-4-5')\n expect(ids).toContain('claude-sonnet-4-6')\n expect(ids).toContain('claude-opus-4-7')\n expect(ids).toContain('cognitivecomputations/dolphin-mistral-24b-venice-edition:free')\n })\n})\n"}}}}