1 line
131 KiB
JSON
1 line
131 KiB
JSON
{"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"}}}} |