mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-07 06:52:28 +00:00
* feat(web): add workspace browser for multi-directory navigation Add /browse route with a folder browser that lets users navigate filesystem directories on connected machines and launch sessions from any folder. Supports saved workspace paths and direct path input. The "Start Session" action pre-fills the NewSession form. - CLI: register machine-level `list-directory` RPC handler - Hub: add POST /machines/:id/list-directory route - Web: add WorkspaceBrowser component with git repo detection - Web: add /browse route with navigation from sessions sidebar - Web: support initialDirectory/initialMachineId in NewSession Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add --workspace-root opt-in scoping for /browse and session spawn Adds a single new flag, \`--workspace-root <path>\` (with \`~\` / \`~/foo\` expansion), on \`hapi runner start\` and \`hapi runner start-sync\`. When set: - The runner reports the path in machine metadata. - The list-directory and spawn-session RPC handlers reject paths outside the root, so the web UI can't escape the configured tree even if someone crafts a request manually. - The /browse page in the web UI auto-opens that root, restricts the breadcrumb / go-up to its subtree, and shows directory entries with git-repo annotations. - The /sessions/new form keeps its existing free-text directory input plus autocomplete + recent-paths chips, and gains a small "Browse" button (next to the input) that opens /browse for picking a folder. - Reconnect-time metadata sync ensures stale records get the field filled in (or cleared when the flag is dropped on a later restart), so the hub state matches the CLI's intent. When unset: - Runner behaves like the legacy hapi (no scoping, no browse feature). - /browse renders an informative state pointing at the flag instead of blocking the user. - The /sessions/new form looks identical to the pre-change behavior; the "Browse" button is hidden. Includes a startup banner so \`runner start-sync\` no longer looks like it hung, and surfaces the workspace-root sync result on stdout. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(hub): preserve workspaceRoot when rehydrating machines from store MachineCache.refreshMachine() rebuilt the metadata object from an explicit field allowlist, so any field not in the list (including the new workspaceRoot) was silently dropped on every read — even though it was correctly written to the store. Add workspaceRoot to the zod schema, the Machine interface, and the hand-rolled projection. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(web): friendlier empty state on /sessions When there are zero sessions the page used to be a vast blank rectangle with just the "0 sessions in 0 projects" caption. Render a centered empty state instead: a calendar/agenda icon, a short heading and hint, and two buttons — "Start a session" (→ /sessions/new) and "Browse workspace" (→ /browse). SessionList gains an optional onBrowse prop. Router wires it on the sessions page so the secondary button resolves; other callers can leave it unset to hide that button. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: document --workspace-root flag in cli/README and root README Add a short paragraph under "Runner management" in cli/README.md explaining what \`--workspace-root\` enables (scoped /browse tree, list/spawn enforcement, tilde expansion) and that omitting it keeps the legacy behavior. Mention the workspace browser in the top-level README's Features list. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: address PR #526 review feedback Three findings from the review bot: 1. [Major] Workspace-scope check was lexical only. With workspaceRoot = /safe, a symlink such as /safe/out -> /etc would pass the relative- path test and let list-directory / spawn-happy-session reach paths outside the configured root. realpath the workspaceRoot at construction time, and resolve every incoming path through realpath (walking up to the nearest existing parent for spawn targets that haven't been created yet) before the containment check. 2. [Minor] \`hapi runner start --workspace-root\` with no value used to drop the flag silently and start the runner unscoped. Now treats a missing or flag-shaped next argument as an error. 3. [Minor] /sessions/new's "Browse" button always opened /browse using localStorage's last-used machine, ignoring the user's current selection. NewSession already passes machineId in its callback; forward it through the /browse search params and seed WorkspaceBrowser with it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(cli): gate list-directory RPC behind --workspace-root opt-in Without a configured workspaceRoot, isWithinWorkspaceRoot() returns true unconditionally, leaving the new list-directory RPC able to enumerate any path on the runner. The Web UI already hides Browse for these machines, but the backend should enforce the opt-in too. Refuse the RPC up front when no workspace root is configured. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
315 lines
12 KiB
TypeScript
315 lines
12 KiB
TypeScript
export default {
|
|
// Loading states
|
|
'loading': '加载中…',
|
|
'authorizing': '认证中…',
|
|
'loading.session': '加载会话…',
|
|
'loading.git': '加载 Git 状态…',
|
|
'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',
|
|
|
|
// 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 actions
|
|
'session.action.rename': '重命名',
|
|
'session.action.archive': '归档',
|
|
'session.action.delete': '删除',
|
|
'session.action.copy': '复制',
|
|
|
|
// Dialogs
|
|
'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.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': '切换到远程模式',
|
|
|
|
// 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': '复制',
|
|
|
|
// Diff view
|
|
'diff.title': '差异',
|
|
'diff.view': '查看',
|
|
|
|
// 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.result': '结果',
|
|
'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': '添加备注…',
|
|
|
|
// Composer buttons
|
|
'composer.settings': '设置',
|
|
'composer.terminal': '终端',
|
|
'composer.abort': '中止',
|
|
'composer.switchRemote': '切换到远程模式',
|
|
'composer.attach': '添加文件',
|
|
'composer.send': '发送',
|
|
'composer.stop': '停止',
|
|
'composer.voice': '语音助手',
|
|
'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': '未连接到服务器',
|
|
|
|
// 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.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` 来开始。',
|
|
|
|
// 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
|