mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-07 06:52:28 +00:00
* feat(web): add UriConfirmDialog component
Add a Radix Dialog-based confirmation modal for custom URI scheme
navigation. Follows the RenameSessionDialog pattern.
- UriConfirmDialog: shows URI, scheme label, Cancel/Open/Always-allow buttons
- i18n keys: dialog.uri.{title,description,open,alwaysAllow}
* feat(web): autolink non-https URI schemes in markdown
Add a remark plugin that converts raw `scheme://...` text nodes into
link nodes for non-http(s) schemes. GFM already handles http/https;
this plugin handles the remainder (obsidian://, vscode://, slack://, etc.).
- No scheme allowlist: every `scheme://` pattern is converted; the
sanitize layer (urlTransform) and onClick layer (classifyScheme) handle
blocking/confirmation downstream.
- Runs before remarkStripCjkAutolink so the CJK-strip plugin sees the
new link nodes and can trim trailing CJK punctuation from them.
- Trailing punctuation (.,;!?) stripped from matched URIs.
- Unit tests: conversion, partial-match, escape, explicit link bypass,
code-block bypass, trailing-punct trimming.
* feat(web): linkify custom URI schemes via markdown <a> handler
Wire up 4-layer URI security policy in the markdown renderer:
1. URL sanitize (deny-only): urlTransform strips javascript:/data:/vbscript:/file:
using classifyScheme as single source of truth (handles percent-encoding,
case-insensitive, whitespace-prefix bypass patterns).
2. onClick intercept: custom <A> component classifies each href —
- IANA safe (https/http/irc/ircs/mailto/xmpp): navigate directly.
- Deny (javascript/data/vbscript/file): preventDefault silently.
- Custom (obsidian/vscode/slack/…): preventDefault + open UriConfirmDialog.
3. UriConfirmProvider: one dialog lifted to each markdown root (MarkdownText,
Reasoning, MarkdownRenderer). Shared isAllowed state across all <a> tags in
the subtree — "Always allow" click updates every link in one React commit.
4. Intra-tab cross-provider sync (P7e.1): module-level schemeListeners Set so
sibling UriConfirmProviders (MarkdownText + Reasoning in AssistantMessage)
receive allowed-scheme updates synchronously without waiting for the window
storage event (which only fires in other tabs). Cross-tab sync continues via
the existing window storage event listener.
5. "Always allow" persisted to localStorage (hapi-allowed-schemes). Custom
schemes once allowed navigate directly on subsequent clicks, no dialog gate.
href="#" in DOM for unallowed custom schemes prevents middle-click bypass.
Deny-scheme href="" prevents any navigation even if localStorage tampered.
Security: classifyScheme decodes percent-encoding before scheme extraction,
blocking %6Aavascript:, jav%61script:, javascript%3A (single-encoded colon)
and double-encoded variants. DENY_SCHEMES checked after localStorage lookup so
tampered allowed-list cannot promote deny schemes.
Tests: classifyScheme 6-axis security bypass, denyOnlyTransform, localStorage
roundtrip, cross-tab storage event, <A> click handler cases.
* fix(web): block control-char-spliced deny schemes in classifyScheme
Browsers silently strip ASCII control characters (\t, \n, \r) and
whitespace from URL scheme names during navigation. A scheme like
`java\nscript:alert(1)` was navigated as `javascript:` while our
literal string comparison classified it as 'custom', allowing it
past the deny list and into window.open().
Introduce normalizedScheme() that:
- applies 2 rounds of decodeURIComponent so double-encoded schemes
(javascript%253A → javascript%3A → javascript:) are fully unwrapped
before comparison
- strips [\x00-\x1F\x7F\s] from the extracted scheme name, matching
the browser's own normalization
classifyScheme() now delegates to normalizedScheme() so both the
denyOnlyTransform (urlTransform) path and the <A> onClick path benefit
from the same normalization.
Tests added for \n / \t / \r / space spliced into scheme, and verify
that double-encoded colon is now caught via scheme-match (not just
the no-colon fallback).
* fix(web): preserve relative markdown links from being blocked
Relative / no-scheme hrefs (/settings, ./foo, #section, ?q=1) were
silently preventDefault'd in <A>'s onClick handler. denyOnlyTransform
correctly passed them through (no colon → not a scheme URL), but the
click handler called classifyScheme(href) which returned 'deny' for
any input with no valid scheme separator — then the deny branch fired.
Add hasScheme(href): checks whether the first ':' appears before any
path/query/fragment boundary ('/', '?', '#'). When hasScheme is false
the href is treated as 'iana' so the browser or SPA router can navigate
normally with no dialog and no preventDefault.
Also wrap renderA() with <I18nProvider> so the UriConfirmDialog that
UriConfirmProvider may render does not throw outside its translation
context during tests.
Fixes a regression that broke all relative-path markdown links once the
custom-URI-scheme onClick handler was added.
* test(web): cover percent-encoded scheme control char + protocol-relative href
Round-5 internal hostile review noted two coverage gaps on the bot-fixup commits:
- `java%0Ascript:alert(1)` (percent-encoded newline in the scheme name) takes the
same decode→strip code path as the literal `java\nscript:` case but was only
tested literally. Add an explicit test so a future refactor that drops the
decode-then-strip ordering would be caught.
- Protocol-relative URLs (`//host/path`) have no colon, so `hasScheme` returns
false and `<A>` treats them as scheme-less — browsers then navigate them as
the current origin's protocol. Existing relative-href tests covered absolute
paths, hashes, queries, and colon-in-path, but not the protocol-relative
variant. Add one assertion.
Also extend the `hasScheme` JSDoc to note that protocol-relative URLs are
intentionally treated as scheme-less.
* fix(web): preserve balanced parens/brackets in autolinked URIs
The trailing-punctuation strip used to drop every `)` / `]` from the end
of a matched URI, even when the URL body had an unmatched opener. So a
URI like `obsidian://open?file=Note(1)` was rendered with href
`obsidian://open?file=Note(1` plus a separate `)` text node, opening a
broken deep link.
Match the GFM autolink-literal behaviour: when the trailing character is
`)` or `]`, keep it iff the URL body has more opening counterparts than
closers (so the trailing closer balances an earlier opener and belongs
to the URL). Other trailing punctuation (`.,;!?:>'"`) and unmatched
closers still strip as before.
Add tests for the balanced cases (`Note(1)`, `Note[1]`, nested
`(a(b)c)`), the "balanced URL followed by a period" case, and a
regression test that an unmatched `).` after a URL is still stripped.
453 lines
19 KiB
TypeScript
453 lines
19 KiB
TypeScript
export default {
|
||
// Loading states
|
||
'loading': '加载中…',
|
||
'authorizing': '认证中…',
|
||
'loading.session': '加载会话…',
|
||
'loading.git': '加载 Git 状态…',
|
||
'loading.file': '加载文件…',
|
||
'loading.files': '加载文件…',
|
||
'loading.messages': '加载消息…',
|
||
'loading.machines': '加载机器…',
|
||
|
||
// Login / Auth
|
||
'login.title': 'HAPI',
|
||
'login.subtitle': '随时随地,Vibe Coding',
|
||
'login.bind.title': '绑定 Telegram',
|
||
'login.submit': '登录',
|
||
'login.bind.submit': '绑定',
|
||
'login.submitting': '登录中…',
|
||
'login.bind.submitting': '绑定中…',
|
||
'login.placeholder': '访问令牌',
|
||
'login.error.enterToken': '请输入访问令牌',
|
||
'login.error.bindingUnavailable': '绑定服务不可用。',
|
||
'login.error.loginUnavailable': '登录服务不可用。',
|
||
'login.error.bindFailed': '绑定失败',
|
||
'login.error.authFailed': '认证失败',
|
||
'login.help': '需要帮助?',
|
||
'login.server.default': '(默认)',
|
||
'login.server.custom': '(自定义)',
|
||
'login.server.title': '服务器地址',
|
||
'login.server.description': '设置 HAPI 服务器地址以使用 API 和实时更新。',
|
||
'login.server.current': '当前:',
|
||
'login.server.origin': '服务器地址',
|
||
'login.server.placeholder': 'https://hapi.example.com',
|
||
'login.server.hint': '仅使用 http(s) 协议。任何路径将被忽略。',
|
||
'login.server.required': '必须先填写服务器地址才能登录。',
|
||
'login.server.useSameOrigin': '使用同源',
|
||
'login.server.save': '保存',
|
||
'login.footer': '用',
|
||
'login.footer.for': '设计,Vibe Coding',
|
||
'login.footer.copyright': '©',
|
||
|
||
// Sessions page
|
||
'sessions.count': '{n} 个会话,{m} 个项目',
|
||
'sessions.new': '新建会话',
|
||
'sessions.empty.title': '还没有会话',
|
||
'sessions.empty.hint': '在 workspace 下任意目录启动一个会话,或先浏览目录树看看。',
|
||
'sessions.empty.startSession': '启动会话',
|
||
'sessions.empty.browse': '浏览 workspace',
|
||
'sessions.search.placeholder': '搜索会话…',
|
||
'sessions.search.clear': '清除搜索',
|
||
'sessions.search.count': '{n} / {total} 个会话',
|
||
'sessions.search.noResults': '没有匹配的会话。',
|
||
'sessions.group.showMore': '再显示 {n} 个',
|
||
'sessions.group.showLess': '收起',
|
||
'sessions.group.new': '在此目录新建会话',
|
||
|
||
// Session list
|
||
'session.item.path': '路径',
|
||
'session.item.agent': '代理',
|
||
'session.item.model': '模型',
|
||
'session.item.worktree': '工作树',
|
||
'session.item.pending': '待处理',
|
||
'session.item.thinking': '思考中',
|
||
'session.time.justNow': '刚刚',
|
||
'session.time.minutesAgo': '{n} 分钟前',
|
||
'session.time.hoursAgo': '{n} 小时前',
|
||
'session.time.daysAgo': '{n} 天前',
|
||
|
||
// Session header
|
||
'session.title': '文件',
|
||
'session.more': '更多操作',
|
||
'session.outline.open': '会话大纲',
|
||
'session.outline.close': '关闭大纲',
|
||
'session.outline.title': '大纲',
|
||
'session.outline.loadOlder': '加载更早',
|
||
'session.outline.empty': '已加载消息中暂无大纲项',
|
||
'session.outline.kind.user': '用户',
|
||
|
||
// Session actions
|
||
'session.action.rename': '重命名',
|
||
'session.action.archive': '归档',
|
||
'session.action.delete': '删除',
|
||
'session.action.copy': '复制',
|
||
|
||
// Dialogs
|
||
'dialog.uri.title': '打开此链接?',
|
||
'dialog.uri.description': '此链接使用自定义 URI 协议。请确认您信任此链接后再打开。',
|
||
'dialog.uri.open': '打开',
|
||
'dialog.uri.alwaysAllow': '始终允许 {scheme}:',
|
||
'dialog.rename.title': '重命名会话',
|
||
'dialog.rename.placeholder': '会话名称',
|
||
'dialog.rename.save': '保存',
|
||
'dialog.rename.saving': '保存中…',
|
||
'dialog.rename.error': '重命名失败,请重试。',
|
||
|
||
'dialog.archive.title': '归档会话',
|
||
'dialog.archive.description': '确定要归档 "{name}" 吗?这将断开活动会话。',
|
||
'dialog.archive.confirm': '归档',
|
||
'dialog.archive.confirming': '归档中…',
|
||
|
||
'dialog.delete.title': '删除会话',
|
||
'dialog.delete.description': '确定要删除 "{name}" 吗?此操作无法撤销。',
|
||
'dialog.delete.confirm': '删除',
|
||
'dialog.delete.confirming': '删除中…',
|
||
'dialog.error.default': '操作失败,请重试。',
|
||
|
||
// Common buttons
|
||
'button.cancel': '取消',
|
||
'button.save': '保存',
|
||
'button.confirm': '确认',
|
||
'button.close': '关闭',
|
||
'button.dismiss': '忽略',
|
||
'button.copy': '复制',
|
||
'button.paste': '粘贴',
|
||
|
||
// New session form
|
||
'newSession.title': '创建会话',
|
||
'newSession.machine': '机器',
|
||
'newSession.directory': '目录',
|
||
'newSession.placeholder': '/path/to/project',
|
||
'newSession.browse': '浏览',
|
||
'newSession.recent': '最近路径',
|
||
'newSession.type': '会话类型',
|
||
'newSession.type.simple': '简单',
|
||
'newSession.type.simple.desc': '直接使用选定的目录',
|
||
'newSession.type.worktree': '工作树',
|
||
'newSession.type.worktree.desc': '在仓库旁创建新工作树',
|
||
'newSession.type.worktree.placeholder': 'feature-x (默认 1228-xxxx)',
|
||
'newSession.agent': '代理',
|
||
'newSession.model': '模型',
|
||
'newSession.effort': '思考强度',
|
||
'newSession.model.optional': '可选',
|
||
'newSession.model.loadFailed': '加载 Codex 模型失败',
|
||
'newSession.opencodeModel.loading': '正在发现 OpenCode 模型…',
|
||
'newSession.opencodeModel.loadFailed': '加载 OpenCode 模型失败',
|
||
'newSession.opencodeModel.retry': '重试',
|
||
'newSession.opencodeModel.empty': '未在此目录发现 OpenCode 模型',
|
||
'newSession.opencodeModel.default': '默认',
|
||
'newSession.reasoningEffort': '推理强度',
|
||
'newSession.yolo': 'YOLO 模式',
|
||
'newSession.yolo.title': '跳过审批和沙箱',
|
||
'newSession.yolo.desc': '启动时使用危险的代理标志。',
|
||
'newSession.create': '创建',
|
||
'newSession.creating': '创建中…',
|
||
|
||
// Spawn session (old component)
|
||
'spawn.title': '创建会话',
|
||
'spawn.sessionType': '会话类型',
|
||
'spawn.useAsIs': '直接使用选定的目录',
|
||
'spawn.createWorktree': '在仓库旁创建新工作树',
|
||
'spawn.worktreePlaceholder': 'feature-x (默认 1228-xxxx)',
|
||
'spawn.cancel': '取消',
|
||
'spawn.create': '创建会话',
|
||
'spawn.creating': '创建中…',
|
||
'session.directoryMissingSimple': '目录不存在,创建会话时将自动创建。',
|
||
'session.directoryMissingSimpleConfirm': '目录不存在。再次点击按钮将自动新建该目录。',
|
||
'session.directoryMissingWorktree': 'worktree 需要已存在的仓库目录。',
|
||
'session.codexModelsLoadFailed': '加载 Codex 模型失败',
|
||
'session.createAndCreateDirectory': '创建并新建目录',
|
||
|
||
// Machine
|
||
'machine.unknown': '未知平台',
|
||
|
||
// Chat
|
||
'chat.placeholder': '输入消息…',
|
||
'chat.send': '发送',
|
||
'chat.abort': '中止',
|
||
'chat.settings': '设置',
|
||
'chat.terminal': '终端',
|
||
'chat.switchRemote': '切换到远程模式',
|
||
|
||
// Codex review
|
||
'codexReview.title': 'Codex review',
|
||
'codexReview.findings': '{count} 条发现',
|
||
'codexReview.confidence': '置信度 {value}',
|
||
'codexReview.location.missing': '无位置',
|
||
|
||
// Terminal
|
||
'terminal.commandName': '命令',
|
||
'terminal.commandMessage': '命令消息',
|
||
'terminal.commandArgs': '命令参数',
|
||
'terminal.stdout': '标准输出',
|
||
'terminal.stderr': '标准错误',
|
||
'terminal.unsupportedWindows': 'Windows 主机暂不支持远程终端。',
|
||
'terminal.paste.fallbackTitle': '粘贴输入',
|
||
'terminal.paste.fallbackDescription': '无法读取剪贴板,请在下方粘贴文本。',
|
||
'terminal.paste.placeholder': '在此粘贴终端输入…',
|
||
|
||
// Code block
|
||
'code.copy': '复制',
|
||
'code.truncated': '预览已截断 — 打开详情查看完整输出',
|
||
|
||
// Diff view
|
||
'diff.title': '差异',
|
||
'diff.view': '查看',
|
||
|
||
// Files page
|
||
'files.page.title': '文件',
|
||
'files.page.refresh': '刷新',
|
||
'files.page.searchPlaceholder': '搜索文件',
|
||
'files.projectRoot': '项目根目录',
|
||
'files.branch.detached': '游离 HEAD',
|
||
'files.branch.summary': '{staged} 个已暂存,{unstaged} 个未暂存',
|
||
'files.tab.changes': '变更',
|
||
'files.tab.directories': '目录',
|
||
'files.search.empty': '没有匹配搜索的文件。',
|
||
'files.search.error.failed': '搜索文件失败。',
|
||
'files.search.error.failedWithDetail': '搜索文件失败:{error}',
|
||
'files.changes.section.staged': '已暂存变更({n})',
|
||
'files.changes.section.unstaged': '未暂存变更({n})',
|
||
'files.changes.empty.unavailable': 'Git 状态不可用。请使用“目录”浏览全部文件,或直接搜索。',
|
||
'files.changes.empty.none': '未检测到变更。请使用“目录”浏览全部文件,或直接搜索。',
|
||
'files.changes.error.gitStatusUnavailable': 'Git 状态不可用。',
|
||
'files.changes.error.gitStatusUnavailableWithDetail': 'Git 状态不可用:{error}',
|
||
'files.changes.error.unstagedDiffUnavailableWithDetail': '未暂存 Diff 不可用:{error}',
|
||
'files.changes.error.stagedDiffUnavailableWithDetail': '已暂存 Diff 不可用:{error}',
|
||
'files.directories.empty': '空目录。',
|
||
'files.directories.error.listFailed': '加载目录失败。',
|
||
'files.directories.error.listFailedWithDetail': '加载目录失败:{error}',
|
||
|
||
// File page
|
||
'file.page.fallbackName': '文件',
|
||
'file.page.unknownPath': '未知路径',
|
||
'file.page.copyPath': '复制路径',
|
||
'file.page.copyContent': '复制文件内容',
|
||
'file.page.tab.diff': 'Diff',
|
||
'file.page.tab.file': '文件',
|
||
'file.page.missingPath': '未提供文件路径。',
|
||
'file.page.binary': '该文件看起来是二进制文件,无法显示。',
|
||
'file.page.imagePreviewAlt': '{name} 图片预览',
|
||
'file.page.empty': '文件为空。',
|
||
'file.page.noChanges': '没有可显示的变更。',
|
||
'file.error.readFailed': '读取文件失败。',
|
||
'file.error.readFailedWithDetail': '读取文件失败:{error}',
|
||
'file.error.diffUnavailable': 'Diff 不可用。',
|
||
'file.error.diffUnavailableWithDetail': 'Diff 不可用:{error}',
|
||
|
||
// Tool card
|
||
'tool.askQuestion': '其他',
|
||
'tool.edit': '编辑',
|
||
'tool.multiedit': '多文件编辑',
|
||
'tool.write': '写入',
|
||
'tool.todo': '任务',
|
||
'tool.confirm': '确认',
|
||
'tool.deny': '拒绝',
|
||
'tool.abort': '中止',
|
||
'tool.allow': '允许',
|
||
'tool.allowAll': '允许所有编辑',
|
||
'tool.denyAll': '拒绝所有编辑',
|
||
'tool.approved': '已批准',
|
||
'tool.rejected': '已拒绝',
|
||
'tool.cancel': '取消',
|
||
'tool.exitPlan': '退出计划模式',
|
||
'tool.patch': '补丁',
|
||
'tool.input': '输入',
|
||
'tool.trace': '追踪',
|
||
'tool.trace.callsSuffix': '次调用',
|
||
'tool.result': '结果',
|
||
'tool.semanticTitle.readFile': '读取文件',
|
||
'tool.semanticTitle.runShell': '运行命令',
|
||
'tool.semanticTitle.search': '搜索',
|
||
'tool.semanticTitle.openUrl': '打开链接',
|
||
'tool.semanticTitle.query': '查询',
|
||
'tool.questionsAnswers': '问答',
|
||
'tool.submit': '提交',
|
||
'tool.submitting': '提交中…',
|
||
'tool.prev': '← 上一个',
|
||
'tool.next': '下一个 →',
|
||
'tool.yes': '是',
|
||
'tool.yesForSession': '本会话允许',
|
||
'tool.allowForSession': '本会话允许',
|
||
'tool.abortLabel': '中止',
|
||
'tool.other': '其他',
|
||
'tool.otherDescription': '输入您自己的答案',
|
||
'tool.question': '问题',
|
||
'tool.selectOption': '请至少选择一个选项或输入答案。',
|
||
'tool.waitingForApproval': '等待审批…',
|
||
'tool.canceled': '已取消',
|
||
'tool.approvedForSession': '本会话已批准',
|
||
'tool.aborted': '已中止',
|
||
'tool.approvedAllowAllEdits': '已批准:允许所有编辑',
|
||
'tool.requestFailed': '请求失败',
|
||
'tool.askUserQuestion.fallback': 'AskUserQuestion 格式不正确。请输入您的答案:',
|
||
'tool.askUserQuestion.placeholder': '输入您的答案…',
|
||
'tool.askUserQuestion.otherPlaceholder': '或输入您自己的答案…',
|
||
'tool.requestUserInput.textPlaceholder': '输入您的答案…',
|
||
'tool.requestUserInput.noteLabel': '补充说明(可选)',
|
||
'tool.requestUserInput.notePlaceholder': '添加备注…',
|
||
'toolGroup.title': '工具活动',
|
||
'toolGroup.primary.fileTargets': '{target} 等 +{n}',
|
||
'toolGroup.primary.commandTargets': '{target} 等 +{n}',
|
||
'toolGroup.primary.searchTargets': '{target} 等 +{n}',
|
||
'toolGroup.primary.urlTargets': '{target} 等 +{n}',
|
||
'toolGroup.primary.otherTargets': '{target} 等 +{n}',
|
||
'toolGroup.summary.read': '读取 {n}',
|
||
'toolGroup.summary.mutation': '编辑 {n}',
|
||
'toolGroup.summary.command': '执行 {n}',
|
||
'toolGroup.summary.search': '搜索 {n}',
|
||
'toolGroup.summary.web': '访问 {n}',
|
||
'toolGroup.summary.other': '工具 {n}',
|
||
'toolGroup.friendly.inspectFiles': '检查项目文件',
|
||
'toolGroup.friendly.searchContent': '搜索项目内容',
|
||
'toolGroup.friendly.runCommands': '执行项目命令',
|
||
'toolGroup.friendly.editFiles': '修改项目文件',
|
||
'toolGroup.friendly.openWeb': '浏览网页内容',
|
||
'toolGroup.friendly.genericCommand': '执行命令',
|
||
'toolGroup.friendly.genericTool': '使用工具',
|
||
'toolGroup.badge.running': '运行中 {n}',
|
||
'toolGroup.badge.pending': '等待中 {n}',
|
||
'toolGroup.badge.error': '错误 {n}',
|
||
'toolGroup.badge.fileTargets': '{n} 文件',
|
||
'toolGroup.toolCount': '{n} 次操作',
|
||
'toolGroup.loadingOlderHistory': '正在补加载更早的工具活动…',
|
||
'toolGroup.historyUnavailable': '更早的工具活动已不可用。',
|
||
'toolGroup.rowStatus.running': '运行中',
|
||
'toolGroup.rowStatus.pending': '等待中',
|
||
'toolGroup.rowStatus.error': '错误',
|
||
|
||
// Composer buttons
|
||
'composer.settings': '设置',
|
||
'composer.terminal': '终端',
|
||
'composer.abort': '中止',
|
||
'composer.switchRemote': '切换到远程模式',
|
||
'composer.attach': '添加文件',
|
||
'composer.send': '发送',
|
||
'composer.stop': '停止',
|
||
'composer.voice': '语音助手',
|
||
'composer.scheduleSend': '定时发送',
|
||
'composer.scheduleRelativeTab': '相对时间',
|
||
'composer.scheduleSpecificTab': '指定时间',
|
||
'composer.scheduleSpecificHint': '最多 7 天。需 Hub 运行;CLI 下次连接时会接收消息。',
|
||
'composer.scheduleErrorPast': '发送时间必须在未来。',
|
||
'composer.scheduleErrorTooFar': '最多只能定时 7 天。',
|
||
'queuedMessages.scheduledFor': '定时发送: {time}',
|
||
'queuedMessages.editAlreadyInvoked': '消息已发送,无法编辑',
|
||
'composer.codexSlashUnsupported.title': '无法执行 Codex 命令',
|
||
'composer.codexSlashUnsupported.body': 'HAPI 远程模式暂不支持 {command} 这类 Codex 内建 slash command,请改用自然语言,或在本地 Codex TUI 中执行。',
|
||
|
||
// Voice assistant
|
||
'voice.connecting': '连接中...',
|
||
'voice.active': '活跃',
|
||
'voice.muted': '已静音',
|
||
'voice.error': '错误',
|
||
'voice.mute': '静音',
|
||
'voice.unmute': '取消静音',
|
||
'voice.end': '结束',
|
||
'voice.error.micPermission': '麦克风权限被拒绝',
|
||
'voice.error.network': '网络错误',
|
||
'voice.error.notInitialized': '语音会话未初始化',
|
||
'voice.error.startFailed': '启动语音会话失败',
|
||
'voice.error.notAllowed': '语音功能不可用',
|
||
'voice.error.connection': '连接错误',
|
||
'voice.dismiss': '关闭',
|
||
|
||
// Banners
|
||
'offline.title': '离线',
|
||
'offline.message': '您当前处于离线状态。某些功能可能受限。',
|
||
'syncing.title': '同步中…',
|
||
'syncing.message': '正在同步您的数据。',
|
||
'reconnecting.message': '正在重新连接...',
|
||
'reconnecting.reason.error': '流连接错误',
|
||
'reconnecting.reason.closed': '流连接已关闭',
|
||
'reconnecting.reason.heartbeatTimeout': '心跳超时',
|
||
'reconnecting.reason.visibilityRecovery': '后台恢复中',
|
||
|
||
// Send blocked
|
||
'send.blocked.title': '无法发送消息',
|
||
'send.blocked.noConnection': '未连接到服务器',
|
||
'resume.failed.title': '恢复会话失败',
|
||
'toast.ready.title': '等待输入',
|
||
'toast.ready.body': '{agent} 正在 {session} 等待你的输入',
|
||
'toast.permission.title': '权限请求',
|
||
'toast.task.completed': '任务完成',
|
||
'toast.task.failed': '任务失败',
|
||
|
||
// Install prompt
|
||
'install.title': '安装 HAPI',
|
||
'install.description': '将 HAPI 添加到主屏幕以便快速访问。',
|
||
'install.button': '安装',
|
||
|
||
// Language switcher
|
||
'language.title': '语言',
|
||
'language.english': 'English',
|
||
'language.chinese': '简体中文',
|
||
|
||
// Settings page
|
||
'settings.title': '设置',
|
||
'settings.language.title': '语言',
|
||
'settings.language.label': '语言',
|
||
'settings.display.title': '显示',
|
||
'settings.display.appearance': '外观',
|
||
'settings.display.appearance.system': '跟随系统',
|
||
'settings.display.appearance.dark': '深色',
|
||
'settings.display.appearance.light': '浅色',
|
||
'settings.display.fontSize': '字体大小',
|
||
'settings.display.terminalFontSize': '终端字体大小',
|
||
'settings.display.sessionPreviewLimit': '会话折叠阈值',
|
||
'settings.display.sessionPreviewLimit.decrease': '减少折叠前显示的会话数',
|
||
'settings.display.sessionPreviewLimit.increase': '增加折叠前显示的会话数',
|
||
'settings.chat.title': '聊天',
|
||
'settings.chat.enterBehavior': '回车键行为',
|
||
'settings.chat.enterBehavior.send': '发送消息',
|
||
'settings.chat.enterBehavior.newline': '插入换行',
|
||
'settings.chat.terminalToolDisplay': '终端工具卡片',
|
||
'settings.chat.terminalToolDisplay.compact': '简洁(仅命令)',
|
||
'settings.chat.terminalToolDisplay.detailed': '详细(显示输出预览)',
|
||
'settings.chat.groupedToolBackground': '聚合 Tool Use 背景',
|
||
'settings.chat.userMessageBackground': '用户消息背景',
|
||
'settings.chat.surfaceColor.default': '默认颜色',
|
||
'settings.chat.surfaceColor.softBlue': '柔和蓝',
|
||
'settings.chat.surfaceColor.softGreen': '柔和绿',
|
||
'settings.chat.surfaceColor.softYellow': '柔和黄',
|
||
'settings.chat.surfaceColor.custom': '自定义颜色',
|
||
'settings.voice.title': '语音助手',
|
||
'settings.voice.language': '语音语言',
|
||
'settings.voice.autoDetect': '自动检测',
|
||
'settings.about.title': '关于',
|
||
'settings.about.website': '官方网站',
|
||
'settings.about.appVersion': '应用版本',
|
||
'settings.about.protocolVersion': '协议版本',
|
||
|
||
// Browse / Workspace
|
||
'browse.title': '浏览',
|
||
'browse.goUp': '返回上层',
|
||
'browse.empty': '未找到子目录',
|
||
'browse.refresh': '刷新',
|
||
'browse.startSession': '启动会话',
|
||
'browse.nav': '浏览',
|
||
'browse.noRootTitle': '未启用 workspace 浏览',
|
||
'browse.noRootHint': '浏览功能是可选的。带一个或多个 --workspace-root 参数重启 runner,即可启用文件树浏览和受限的会话启动。',
|
||
'browse.noRootFooter': '你仍然可以在「新建会话」页面直接创建会话。',
|
||
'browse.noMachinesConnected': '没有已连接的 CLI。在某台机器上运行 `hapi runner start --workspace-root /path/a --workspace-root /path/b` 来开始。',
|
||
|
||
// Misc
|
||
'misc.noMachines': '无可用机器',
|
||
'misc.machine': '机器',
|
||
'misc.collaborationMode': '协作模式',
|
||
'misc.permissionMode': '权限模式',
|
||
'misc.model': '模型',
|
||
'misc.reasoningEffort': '推理强度',
|
||
'misc.effort': '思考强度',
|
||
'misc.loading': '加载中…',
|
||
'misc.loadOlder': '加载更早的',
|
||
'misc.newMessage': '{n} 条新消息',
|
||
'misc.loadingMessages': '加载消息中…',
|
||
'misc.typeMessage': "输入 'continue' 继续...",
|
||
'misc.typeAMessage': '输入消息...',
|
||
'misc.offline': '离线',
|
||
'misc.permissionRequired': '需要权限',
|
||
'misc.percentLeft': '剩余 {percent}%',
|
||
'misc.online': '在线',
|
||
} as const
|