diff --git a/README.md b/README.md index 49085c76f..6e7b4027c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # 🦞 OpenClaw — Personal AI Assistant +English | [简体中文](README.zh-CN.md) +

diff --git a/README.zh-CN.md b/README.zh-CN.md new file mode 100644 index 000000000..4d2eb2c97 --- /dev/null +++ b/README.zh-CN.md @@ -0,0 +1,171 @@ +# 🦞 OpenClaw CN — 个人 AI 助手 (中文增强版) + +

+ + + OpenClaw + +

+ +**OpenClaw CN** 是 OpenClaw 的中文增强版本,为中国用户提供更好的使用体验,并集成了国内主流的通信渠道和大模型提供商。 + +## 🎯 特性增强 + +### 🇨🇳 全面中文化 +- ✅ **Web UI 汉化**:完整的中文用户界面 +- ✅ **中文文档**:安装指南、配置说明和使用教程 +- ✅ **本地化体验**:针对中文用户优化的交互设计 + +### 🤖 新增大模型支持 +- ✅ **DeepSeek**:完整支持 DeepSeek API + - `deepseek-chat` (V3.2 非思考模式) + - `deepseek-reasoner` (V3.2 思考模式) + - 完全兼容 OpenAI API 格式 + - 极具竞争力的价格 + +### 💬 新增通信渠道 + +#### 已实现 +- 🔵 **企业渠道**(基于官方 API) + - 钉钉 (DingTalk) + - 飞书 (Feishu/Lark) + - 企业微信 (WeChat Work) + +#### 计划中 +- 🟡 **个人渠道**(基于 OneBot 协议) + - 微信(通过 OneBot 适配器如 Gewechat) + - QQ(通过 OneBot 适配器如 NapCatQQ) + - 华为畅连(开发中) + +> **注意**:个人微信和 QQ 需要运行独立的 OneBot 客户端。推荐方案: +> - **QQ**: [NapCatQQ](https://github.com/NapNeko/NapCatQQ), [Lagrange](https://github.com/LagrangeDev/Lagrange.Core) +> - **微信**: [Gewechat](https://github.com/Devo919/Gewechat) + +## 核心功能 (继承自 OpenClaw) + +- **多渠道消息支持**:连接 WhatsApp、Telegram、Slack、Discord、Google Chat、Signal、iMessage、WebChat 等 +- **本地优先网关**:统一的控制平面,管理会话、渠道、工具和事件 +- **语音唤醒与对话**:支持 macOS/iOS/Android 上的语音交互 +- **实时画布**:代理驱动的可视化工作空间 +- **浏览器控制**:通过 Chrome/Chromium 实现网页自动化 +- **技能平台**:可扩展的技能系统,支持自定义工具 + +[完整功能列表](https://docs.openclaw.ai) + +## 快速开始 + +### 环境要求 +- **Node.js** ≥ 22 +- **操作系统**: macOS, Linux, Windows (推荐 WSL2) + +### 安装 + +```bash +npm install -g openclaw@latest + +# 运行安装向导(推荐) +openclaw onboard --install-daemon +``` + +### DeepSeek 配置示例 + +在 `~/.openclaw/openclaw.json` 中添加: + +```json +{ + "models": { + "providers": { + "deepseek": { + "baseUrl": "https://api.deepseek.com/v1", + "apiKey": "${DEEPSEEK_API_KEY}", + "api": "openai-responses", + "models": [ + { + "id": "deepseek-chat", + "name": "DeepSeek Chat", + "contextWindow": 64000, + "maxTokens": 8000 + }, + { + "id": "deepseek-reasoner", + "name": "DeepSeek Reasoner (思考模式)", + "reasoning": true, + "contextWindow": 64000, + "maxTokens": 8000 + } + ] + } + } + }, + "agent": { + "model": "deepseek/deepseek-chat" + } +} +``` + +或使用环境变量: + +```bash +export DEEPSEEK_API_KEY=sk-xxxxxxxxx +``` + +### 启动网关 + +```bash +# 启动网关 +openclaw gateway --port 18789 --verbose + +# 发送消息 +openclaw agent --message "你好,请介绍一下自己" --model deepseek/deepseek-chat +``` + +## 文档 + +- 📘 [DeepSeek 配置指南](docs/zh-CN/deepseek-guide.md) +- 📘 [钉钉配置指南](docs/zh-CN/dingtalk-guide.md) *(即将推出)* +- 📘 [飞书配置指南](docs/zh-CN/feishu-guide.md) *(即将推出)* +- 📘 [官方文档](https://docs.openclaw.ai) (英文) + +## 价格优势 + +DeepSeekV3.2 定价(/百万 tokens): + +| 模型 | 输入 | 输出 | 缓存读 | 缓存写 | +|------|------|------|--------|--------| +| deepseek-chat | $0.14 | $0.28 | $0.014 | $0.14 | +| deepseek-reasoner | $0.55 | $2.19 | - | - | + +**相比 Claude 和 GPT 系列,DeepSeek 价格仅为其几分之一,同时性能优异!** + +## 安全性 + +- **默认安全**:DM 配对机制,未知发送者需要批准 +- **沙箱模式**:支持 Docker 沙箱运行非主会话 +- **权限控制**:基于白名单的渠道访问控制 + +详见 [安全指南](https://docs.openclaw.ai/gateway/security) + +## 贡献 + +欢迎提交 Pull Request!查看 [CONTRIBUTING.md](CONTRIBUTING.md) 了解贡献指南。 + +## 开源协议 + +MIT License - 详见 [LICENSE](LICENSE) + +## 致谢 + +- 感谢 [OpenClaw 官方团队](https://github.com/openclaw/openclaw) +- 感谢 Peter Steinberger 和社区贡献者 +- 特别感谢 DeepSeek 提供优秀的开源大模型 + +## 链接 + +- [OpenClaw 官网](https://openclaw.ai) +- [官方文档](https://docs.openclaw.ai) +- [Discord 社区](https://discord.gg/clawd) +- [GitHub 仓库](https://github.com/openclaw/openclaw) + +--- + +**⚠️ 注意**: 本项目是 OpenClaw 的社区增强版本,主要面向中文用户。如需使用原版 OpenClaw,请访问 [官方仓库](https://github.com/openclaw/openclaw)。 diff --git a/docs/examples/deepseek.json b/docs/examples/deepseek.json new file mode 100644 index 000000000..140fa5086 --- /dev/null +++ b/docs/examples/deepseek.json @@ -0,0 +1,43 @@ +{ + "models": { + "providers": { + "deepseek": { + "baseUrl": "https://api.deepseek.com/v1", + "apiKey": "${DEEPSEEK_API_KEY}", + "api": "openai-responses", + "models": [ + { + "id": "deepseek-chat", + "name": "Deep Seek Chat (V3.2 非思考模式)", + "contextWindow": 64000, + "maxTokens": 8000, + "cost": { + "input": 0.14, + "output": 0.28, + "cacheRead": 0.014, + "cacheWrite": 0.14 + }, + "compat": { + "supportsStore": false + } + }, + { + "id": "deepseek-reasoner", + "name": "DeepSeek Reasoner (V3.2 思考模式)", + "reasoning": true, + "contextWindow": 64000, + "maxTokens": 8000, + "cost": { + "input": 0.55, + "output": 2.19 + }, + "compat": { + "supportsStore": false, + "supportsReasoningEffort": true + } + } + ] + } + } + } +} \ No newline at end of file diff --git a/docs/zh-CN/deepseek-guide.md b/docs/zh-CN/deepseek-guide.md new file mode 100644 index 000000000..dfd9c4c3b --- /dev/null +++ b/docs/zh-CN/deepseek-guide.md @@ -0,0 +1,91 @@ +# DeepSeek 大模型配置指南 + +本文档说明如何在 OpenClaw 中配置 DeepSeek API。 + +## 获取 API 密钥 +1. 访问 [DeepSeek 开放平台](https://platform.deepseek.com/) +2. 注册并登录您的账户 +3. 在 API Keys 页面创建新的 API 密钥 + +## 配置步骤 + +### 方式一:环境变量 +在您的环境中设置 `DEEPSEEK_API_KEY`: + +```bash +export DEEPSEEK_API_KEY=sk-xxxxxxxxxxxxxxxxxx +``` + +### 方式二:配置文件 +在 `~/.openclaw/openclaw.json` 中添加以下配置: + +```json +{ + "models": { + "providers": { + "deepseek": { + "baseUrl": "https://api.deepseek.com/v1", + "apiKey": "sk-xxxxxxxxxxxxxxxxxx", + "api": "openai-responses", + "models": [ + { + "id": "deepseek-chat", + "name": "DeepSeek Chat (V3.2)", + "contextWindow": 64000, + "maxTokens": 8000 + }, + { + "id": "deepseek-reasoner", + "name": "DeepSeek Reasoner (V3.2 思考模式)", + "reasoning": true, + "contextWindow": 64000, + "maxTokens": 8000 + } + ] + } + } + } +} +``` + +## 使用模型 + +配置完成后,您可以在 OpenClaw 中使用以下模型标识符: + +- `deepseek/deepseek-chat` - 标准对话模型(无思考模式) +- `deepseek/deepseek-reasoner` - 推理模型(启用思考模式) + +示例: + +```bash +openclaw chat --model deepseek/deepseek-chat "你好,请介绍一下自己" +``` + +## 定价说明 + +DeepSeek V3.2 定价(以每百万 tokens 计): + +**deepseek-chat** (非思考模式): +- 输入:$0.14/M tokens +- 输出:$0.28/M tokens +- 缓存读取:$0.014/M tokens +- 缓存写入:$0.14/M tokens + +**deepseek-reasoner** (思考模式): +- 输入:$0.55/M tokens +- 输出:$2.19/M tokens + +## 注意事项 + +1. DeepSeek API 完全兼容 OpenAI API 格式 +2. 推荐使用 `deepseek-chat` 进行日常对话 +3. 对于复杂推理任务,使用 `deepseek-reasoner` + +## 故障排查 + +如果遇到问题,请检查: +1. API 密钥是否正确设置 +2. 网络是否能访问 `api.deepseek.com` +3. 配置文件 JSON 格式是否正确 + +更多信息请访问 [DeepSeek API 文档](https://platform.deepseek.com/docs)。 diff --git a/src/agents/auth-health.ts b/src/agents/auth-health.ts index 4f281b7bb..29bcabffc 100644 --- a/src/agents/auth-health.ts +++ b/src/agents/auth-health.ts @@ -44,14 +44,14 @@ export function resolveAuthProfileSource(_profileId: string): AuthProfileSource } export function formatRemainingShort(remainingMs?: number): string { - if (remainingMs === undefined || Number.isNaN(remainingMs)) return "unknown"; - if (remainingMs <= 0) return "0m"; + if (remainingMs === undefined || Number.isNaN(remainingMs)) return "未知"; + if (remainingMs <= 0) return "0分"; const minutes = Math.max(1, Math.round(remainingMs / 60_000)); - if (minutes < 60) return `${minutes}m`; + if (minutes < 60) return `${minutes}分`; const hours = Math.round(minutes / 60); - if (hours < 48) return `${hours}h`; + if (hours < 48) return `${hours}小时`; const days = Math.round(hours / 24); - return `${days}d`; + return `${days}天`; } function resolveOAuthStatus( diff --git a/src/commands/configure.daemon.ts b/src/commands/configure.daemon.ts index c0431c9f1..84ff08f27 100644 --- a/src/commands/configure.daemon.ts +++ b/src/commands/configure.daemon.ts @@ -12,6 +12,7 @@ import { import { guardCancel } from "./onboard-helpers.js"; import { ensureSystemdUserLingerInteractive } from "./systemd-linger.js"; import { loadConfig } from "../config/config.js"; +import { t } from "../wizard/i18n.js"; export async function maybeInstallDaemon(params: { runtime: RuntimeEnv; @@ -27,11 +28,11 @@ export async function maybeInstallDaemon(params: { if (loaded) { const action = guardCancel( await select({ - message: "Gateway service already installed", + message: t("configure.daemon.alreadyInstalled"), options: [ - { value: "restart", label: "Restart" }, - { value: "reinstall", label: "Reinstall" }, - { value: "skip", label: "Skip" }, + { value: "restart", label: t("configure.daemon.restart") }, + { value: "reinstall", label: t("configure.daemon.reinstall") }, + { value: "skip", label: t("configure.daemon.skip") }, ], }), params.runtime, @@ -40,12 +41,12 @@ export async function maybeInstallDaemon(params: { await withProgress( { label: "Gateway service", indeterminate: true, delayMs: 0 }, async (progress) => { - progress.setLabel("Restarting Gateway service…"); + progress.setLabel(t("configure.daemon.restarting")); await service.restart({ env: process.env, stdout: process.stdout, }); - progress.setLabel("Gateway service restarted."); + progress.setLabel(t("configure.daemon.restarted")); }, ); shouldCheckLinger = true; @@ -56,9 +57,9 @@ export async function maybeInstallDaemon(params: { await withProgress( { label: "Gateway service", indeterminate: true, delayMs: 0 }, async (progress) => { - progress.setLabel("Uninstalling Gateway service…"); + progress.setLabel(t("configure.daemon.uninstalling")); await service.uninstall({ env: process.env, stdout: process.stdout }); - progress.setLabel("Gateway service uninstalled."); + progress.setLabel(t("configure.daemon.uninstalled")); }, ); } @@ -72,7 +73,7 @@ export async function maybeInstallDaemon(params: { } else { daemonRuntime = guardCancel( await select({ - message: "Gateway service runtime", + message: t("configure.daemon.selectRuntime"), options: GATEWAY_DAEMON_RUNTIME_OPTIONS, initialValue: DEFAULT_GATEWAY_DAEMON_RUNTIME, }), @@ -83,7 +84,7 @@ export async function maybeInstallDaemon(params: { await withProgress( { label: "Gateway service", indeterminate: true, delayMs: 0 }, async (progress) => { - progress.setLabel("Preparing Gateway service…"); + progress.setLabel(t("configure.daemon.preparing")); const cfg = loadConfig(); const { programArguments, workingDirectory, environment } = await buildGatewayInstallPlan({ @@ -95,7 +96,7 @@ export async function maybeInstallDaemon(params: { config: cfg, }); - progress.setLabel("Installing Gateway service…"); + progress.setLabel(t("configure.daemon.installing")); try { await service.install({ env: process.env, @@ -104,15 +105,15 @@ export async function maybeInstallDaemon(params: { workingDirectory, environment, }); - progress.setLabel("Gateway service installed."); + progress.setLabel(t("configure.daemon.installed")); } catch (err) { installError = err instanceof Error ? err.message : String(err); - progress.setLabel("Gateway service install failed."); + progress.setLabel(t("configure.daemon.installFailed")); } }, ); if (installError) { - note("Gateway service install failed: " + installError, "Gateway"); + note(t("configure.daemon.installFailedNote") + installError, "Gateway"); note(gatewayInstallErrorHint(), "Gateway"); return; } @@ -127,7 +128,7 @@ export async function maybeInstallDaemon(params: { note, }, reason: - "Linux installs use a systemd user service. Without lingering, systemd stops the user session on logout/idle and kills the Gateway.", + t("configure.daemon.lingerReason"), requireConfirm: true, }); } diff --git a/src/commands/configure.gateway-auth.ts b/src/commands/configure.gateway-auth.ts index 508ed6cf0..88b4c04bf 100644 --- a/src/commands/configure.gateway-auth.ts +++ b/src/commands/configure.gateway-auth.ts @@ -11,6 +11,7 @@ import { promptDefaultModel, promptModelAllowlist, } from "./model-picker.js"; +import { t } from "../wizard/i18n.js"; type GatewayAuthChoice = "token" | "password"; @@ -80,7 +81,7 @@ export async function promptAuthConfig( prompter, allowedKeys: anthropicOAuth ? ANTHROPIC_OAUTH_MODEL_KEYS : undefined, initialSelections: anthropicOAuth ? ["anthropic/claude-opus-4-5"] : undefined, - message: anthropicOAuth ? "Anthropic OAuth models" : undefined, + message: anthropicOAuth ? t("configure.auth.anthropicOAuthModels") : undefined, }); if (allowlistSelection.models) { next = applyModelAllowlist(next, allowlistSelection.models); diff --git a/src/commands/configure.gateway.ts b/src/commands/configure.gateway.ts index 9b66a7b34..8b209b49b 100644 --- a/src/commands/configure.gateway.ts +++ b/src/commands/configure.gateway.ts @@ -4,6 +4,7 @@ import { findTailscaleBinary } from "../infra/tailscale.js"; import type { RuntimeEnv } from "../runtime.js"; import { note } from "../terminal/note.js"; import { buildGatewayAuthConfig } from "./configure.gateway-auth.js"; +import { t } from "../wizard/i18n.js"; import { confirm, select, text } from "./configure.shared.js"; import { guardCancel, randomToken } from "./onboard-helpers.js"; @@ -19,9 +20,9 @@ export async function promptGatewayConfig( }> { const portRaw = guardCancel( await text({ - message: "Gateway port", + message: t("onboarding.gatewayConfig.port"), initialValue: String(resolveGatewayPort(cfg)), - validate: (value) => (Number.isFinite(Number(value)) ? undefined : "Invalid port"), + validate: (value) => (Number.isFinite(Number(value)) ? undefined : t("onboarding.gatewayConfig.invalidPort")), }), runtime, ); @@ -29,31 +30,31 @@ export async function promptGatewayConfig( let bind = guardCancel( await select({ - message: "Gateway bind mode", + message: t("onboarding.gatewayConfig.bind"), options: [ { value: "loopback", - label: "Loopback (Local only)", - hint: "Bind to 127.0.0.1 - secure, local-only access", + label: t("onboarding.gateway.bindLoopback"), + hint: t("onboarding.gatewayConfig.bindLoopbackHint") || "Bind to 127.0.0.1 - secure, local-only access", }, { value: "tailnet", - label: "Tailnet (Tailscale IP)", - hint: "Bind to your Tailscale IP only (100.x.x.x)", + label: t("onboarding.gateway.bindTailnet"), + hint: t("onboarding.gatewayConfig.bindTailnetHint") || "Bind to your Tailscale IP only (100.x.x.x)", }, { value: "auto", - label: "Auto (Loopback → LAN)", + label: t("onboarding.gateway.bindAuto"), hint: "Prefer loopback; fall back to all interfaces if unavailable", }, { value: "lan", - label: "LAN (All interfaces)", - hint: "Bind to 0.0.0.0 - accessible from anywhere on your network", + label: t("onboarding.gateway.bindLan"), + hint: t("onboarding.gatewayConfig.bindLanHint") || "Bind to 0.0.0.0 - accessible from anywhere on your network", }, { value: "custom", - label: "Custom IP", + label: t("onboarding.gateway.bindCustom"), hint: "Specify a specific IP address, with 0.0.0.0 fallback if unavailable", }, ], @@ -65,13 +66,13 @@ export async function promptGatewayConfig( if (bind === "custom") { const input = guardCancel( await text({ - message: "Custom IP address", + message: t("onboarding.gatewayConfig.customIp"), placeholder: "192.168.1.100", validate: (value) => { - if (!value) return "IP address is required for custom bind mode"; + if (!value) return t("onboarding.gatewayConfig.customIpRequired"); const trimmed = value.trim(); const parts = trimmed.split("."); - if (parts.length !== 4) return "Invalid IPv4 address (e.g., 192.168.1.100)"; + if (parts.length !== 4) return t("onboarding.gatewayConfig.invalidIp") + " (e.g., 192.168.1.100)"; if ( parts.every((part) => { const n = parseInt(part, 10); @@ -79,7 +80,7 @@ export async function promptGatewayConfig( }) ) return undefined; - return "Invalid IPv4 address (each octet must be 0-255)"; + return t("onboarding.gatewayConfig.invalidIpOctet"); }, }), runtime, @@ -89,10 +90,10 @@ export async function promptGatewayConfig( let authMode = guardCancel( await select({ - message: "Gateway auth", + message: t("onboarding.gatewayConfig.auth"), options: [ - { value: "token", label: "Token", hint: "Recommended default" }, - { value: "password", label: "Password" }, + { value: "token", label: t("onboarding.gatewayConfig.authToken"), hint: t("onboarding.gatewayConfig.authTokenHint") }, + { value: "password", label: t("onboarding.gatewayConfig.authPassword") }, ], initialValue: "token", }), @@ -101,18 +102,18 @@ export async function promptGatewayConfig( const tailscaleMode = guardCancel( await select({ - message: "Tailscale exposure", + message: t("onboarding.gatewayConfig.tsExposure"), options: [ - { value: "off", label: "Off", hint: "No Tailscale exposure" }, + { value: "off", label: t("onboarding.gatewayConfig.tsOff"), hint: t("onboarding.gatewayConfig.tsOffHint") }, { value: "serve", - label: "Serve", - hint: "Private HTTPS for your tailnet (devices on Tailscale)", + label: t("onboarding.gatewayConfig.tsServe"), + hint: t("onboarding.gatewayConfig.tsServeHint"), }, { value: "funnel", - label: "Funnel", - hint: "Public HTTPS via Tailscale Funnel (internet)", + label: t("onboarding.gatewayConfig.tsFunnel"), + hint: t("onboarding.gatewayConfig.tsFunnelHint"), }, ], }), @@ -124,14 +125,8 @@ export async function promptGatewayConfig( const tailscaleBin = await findTailscaleBinary(); if (!tailscaleBin) { note( - [ - "Tailscale binary not found in PATH or /Applications.", - "Ensure Tailscale is installed from:", - " https://tailscale.com/download/mac", - "", - "You can continue setup, but serve/funnel will fail at runtime.", - ].join("\n"), - "Tailscale Warning", + t("onboarding.gatewayConfig.tsNotFound"), + t("onboarding.gatewayConfig.tsWarningTitle"), ); } } @@ -147,7 +142,7 @@ export async function promptGatewayConfig( tailscaleResetOnExit = Boolean( guardCancel( await confirm({ - message: "Reset Tailscale serve/funnel on exit?", + message: t("onboarding.gatewayConfig.tsResetConfirm"), initialValue: false, }), runtime, @@ -156,12 +151,12 @@ export async function promptGatewayConfig( } if (tailscaleMode !== "off" && bind !== "loopback") { - note("Tailscale requires bind=loopback. Adjusting bind to loopback.", "Note"); + note(t("onboarding.gatewayConfig.tsAdjustBind"), "Note"); bind = "loopback"; } if (tailscaleMode === "funnel" && authMode !== "password") { - note("Tailscale funnel requires password auth.", "Note"); + note(t("onboarding.gatewayConfig.tsFunnelAuth"), "Note"); authMode = "password"; } @@ -172,7 +167,7 @@ export async function promptGatewayConfig( if (authMode === "token") { const tokenInput = guardCancel( await text({ - message: "Gateway token (blank to generate)", + message: t("onboarding.gatewayConfig.tokenPlaceholder"), initialValue: randomToken(), }), runtime, @@ -183,8 +178,8 @@ export async function promptGatewayConfig( if (authMode === "password") { const password = guardCancel( await text({ - message: "Gateway password", - validate: (value) => (value?.trim() ? undefined : "Required"), + message: t("onboarding.gatewayConfig.passwordLabel"), + validate: (value) => (value?.trim() ? undefined : t("onboarding.gatewayConfig.passwordRequired")), }), runtime, ); diff --git a/src/commands/configure.shared.ts b/src/commands/configure.shared.ts index bc89529d8..35002a745 100644 --- a/src/commands/configure.shared.ts +++ b/src/commands/configure.shared.ts @@ -7,6 +7,7 @@ import { } from "@clack/prompts"; import { stylePromptHint, stylePromptMessage, stylePromptTitle } from "../terminal/prompt-style.js"; +import { t } from "../wizard/i18n.js"; export const CONFIGURE_WIZARD_SECTIONS = [ "workspace", @@ -33,27 +34,27 @@ export const CONFIGURE_SECTION_OPTIONS: Array<{ label: string; hint: string; }> = [ - { value: "workspace", label: "Workspace", hint: "Set workspace + sessions" }, - { value: "model", label: "Model", hint: "Pick provider + credentials" }, - { value: "web", label: "Web tools", hint: "Configure Brave search + fetch" }, - { value: "gateway", label: "Gateway", hint: "Port, bind, auth, tailscale" }, - { - value: "daemon", - label: "Daemon", - hint: "Install/manage the background service", - }, - { - value: "channels", - label: "Channels", - hint: "Link WhatsApp/Telegram/etc and defaults", - }, - { value: "skills", label: "Skills", hint: "Install/enable workspace skills" }, - { - value: "health", - label: "Health check", - hint: "Run gateway + channel checks", - }, -]; + { value: "workspace", label: t("configure.sections.workspace"), hint: t("configure.sections.workspaceHint") }, + { value: "model", label: t("configure.sections.model"), hint: t("configure.sections.modelHint") }, + { value: "web", label: t("configure.sections.web"), hint: t("configure.sections.webHint") }, + { value: "gateway", label: t("configure.sections.gateway"), hint: t("configure.sections.gatewayHint") }, + { + value: "daemon", + label: t("configure.sections.daemon"), + hint: t("configure.sections.daemonHint"), + }, + { + value: "channels", + label: t("configure.sections.channels"), + hint: t("configure.sections.channelsHint"), + }, + { value: "skills", label: t("configure.sections.skills"), hint: t("configure.sections.skillsHint") }, + { + value: "health", + label: t("configure.sections.health"), + hint: t("configure.sections.healthHint"), + }, + ]; export const intro = (message: string) => clackIntro(stylePromptTitle(message) ?? message); export const outro = (message: string) => clackOutro(stylePromptTitle(message) ?? message); diff --git a/src/commands/configure.wizard.ts b/src/commands/configure.wizard.ts index 505fb7760..23159bc22 100644 --- a/src/commands/configure.wizard.ts +++ b/src/commands/configure.wizard.ts @@ -9,6 +9,7 @@ import { note } from "../terminal/note.js"; import { resolveUserPath } from "../utils.js"; import { createClackPrompter } from "../wizard/clack-prompter.js"; import { WizardCancelledError } from "../wizard/prompts.js"; +import { t } from "../wizard/i18n.js"; import { removeChannelConfigWizard } from "./configure.channels.js"; import { maybeInstallDaemon } from "./configure.daemon.js"; import { promptGatewayConfig } from "./configure.gateway.js"; @@ -51,13 +52,13 @@ async function promptConfigureSection( ): Promise { return guardCancel( await select({ - message: "Select sections to configure", + message: t("configure.sections.title"), options: [ ...CONFIGURE_SECTION_OPTIONS, { value: "__continue", - label: "Continue", - hint: hasSelection ? "Done" : "Skip for now", + label: t("configure.sections.continue"), + hint: hasSelection ? t("configure.sections.continueHint") : t("configure.sections.skipHint"), }, ], initialValue: CONFIGURE_SECTION_OPTIONS[0]?.value, @@ -69,17 +70,17 @@ async function promptConfigureSection( async function promptChannelMode(runtime: RuntimeEnv): Promise { return guardCancel( await select({ - message: "Channels", + message: t("configure.sections.channels"), options: [ { value: "configure", - label: "Configure/link", - hint: "Add/update channels; disable unselected accounts", + label: t("configure.channels.configure"), + hint: t("configure.channels.configureHint"), }, { value: "remove", - label: "Remove channel config", - hint: "Delete channel tokens/settings from openclaw.json", + label: t("configure.channels.remove"), + hint: t("configure.channels.removeHint"), }, ], initialValue: "configure", @@ -107,7 +108,7 @@ async function promptWebToolsConfig( const enableSearch = guardCancel( await confirm({ - message: "Enable web_search (Brave Search)?", + message: t("configure.web.enableSearch"), initialValue: existingSearch?.enabled ?? hasSearchKey, }), runtime, @@ -122,9 +123,9 @@ async function promptWebToolsConfig( const keyInput = guardCancel( await text({ message: hasSearchKey - ? "Brave Search API key (leave blank to keep current or use BRAVE_API_KEY)" - : "Brave Search API key (paste it here; leave blank to use BRAVE_API_KEY)", - placeholder: hasSearchKey ? "Leave blank to keep current" : "BSA...", + ? t("configure.web.keyPrompt") + : t("configure.web.keyPromptEmpty"), + placeholder: hasSearchKey ? t("configure.web.placeholderKey") : t("configure.web.placeholderKeyEmpty"), }), runtime, ); @@ -133,19 +134,15 @@ async function promptWebToolsConfig( nextSearch = { ...nextSearch, apiKey: key }; } else if (!hasSearchKey) { note( - [ - "No key stored yet, so web_search will stay unavailable.", - "Store a key here or set BRAVE_API_KEY in the Gateway environment.", - "Docs: https://docs.openclaw.ai/tools/web", - ].join("\n"), - "Web search", + t("configure.web.noKeyWarning"), + t("configure.sections.web"), ); } } const enableFetch = guardCancel( await confirm({ - message: "Enable web_fetch (keyless HTTP fetch)?", + message: t("configure.web.enableFetch"), initialValue: existingFetch?.enabled ?? true, }), runtime, @@ -175,7 +172,7 @@ export async function runConfigureWizard( ) { try { printWizardHeader(runtime); - intro(opts.command === "update" ? "OpenClaw update wizard" : "OpenClaw configure"); + intro(opts.command === "update" ? t("configure.updateTitle") : t("configure.title")); const prompter = createClackPrompter(); const snapshot = await readConfigFileSnapshot(); @@ -212,30 +209,30 @@ export async function runConfigureWizard( const remoteUrl = baseConfig.gateway?.remote?.url?.trim() ?? ""; const remoteProbe = remoteUrl ? await probeGatewayReachable({ - url: remoteUrl, - token: baseConfig.gateway?.remote?.token, - }) + url: remoteUrl, + token: baseConfig.gateway?.remote?.token, + }) : null; const mode = guardCancel( await select({ - message: "Where will the Gateway run?", + message: t("configure.gateway.modeSelect"), options: [ { value: "local", - label: "Local (this machine)", + label: t("configure.gateway.local"), hint: localProbe.ok - ? `Gateway reachable (${localUrl})` - : `No gateway detected (${localUrl})`, + ? t("configure.gateway.localHintReachable", { url: localUrl }) + : t("configure.gateway.localHintMissing", { url: localUrl }), }, { value: "remote", - label: "Remote (info-only)", + label: t("configure.gateway.remote"), hint: !remoteUrl - ? "No remote URL configured yet" + ? t("configure.gateway.remoteHintNoUrl") : remoteProbe?.ok - ? `Gateway reachable (${remoteUrl})` - : `Configured but unreachable (${remoteUrl})`, + ? t("configure.gateway.remoteHintReachable", { url: remoteUrl }) + : t("configure.gateway.remoteHintConfigured", { url: remoteUrl }), }, ], }), @@ -250,7 +247,7 @@ export async function runConfigureWizard( }); await writeConfigFile(remoteConfig); logConfigUpdated(runtime); - outro("Remote gateway configured."); + outro(t("configure.gateway.remoteConfigured")); return; } @@ -288,7 +285,7 @@ export async function runConfigureWizard( if (opts.sections) { const selected = opts.sections; if (!selected || selected.length === 0) { - outro("No changes selected."); + outro(t("configure.gateway.noChanges")); return; } @@ -530,10 +527,10 @@ export async function runConfigureWizard( if (!ranSection) { if (didSetGatewayMode) { await persistConfig(); - outro("Gateway mode set to local."); + outro(t("configure.gateway.modeSetLocal")); return; } - outro("No changes selected."); + outro(t("configure.gateway.noChanges")); return; } } @@ -582,7 +579,7 @@ export async function runConfigureWizard( "Control UI", ); - outro("Configure complete."); + outro(t("configure.gateway.configureComplete")); } catch (err) { if (err instanceof WizardCancelledError) { runtime.exit(0); diff --git a/src/commands/doctor-auth.ts b/src/commands/doctor-auth.ts index fc62a21c2..4ce0e8083 100644 --- a/src/commands/doctor-auth.ts +++ b/src/commands/doctor-auth.ts @@ -30,9 +30,9 @@ export async function maybeRepairAnthropicOAuthProfileId( }); if (!repair.migrated || repair.changes.length === 0) return cfg; - note(repair.changes.map((c) => `- ${c}`).join("\n"), "Auth profiles"); + note(repair.changes.map((c) => `- ${c}`).join("\n"), "身份验证配置文件"); const apply = await prompter.confirm({ - message: "Update Anthropic OAuth profile id in config now?", + message: "立即更新配置中的 Anthropic OAuth 配置文件 ID?", initialValue: true, }); if (!apply) return cfg; @@ -80,10 +80,10 @@ function pruneAuthProfiles( const nextAuth = nextProfiles || prunedOrder.next ? { - ...cfg.auth, - profiles: nextProfiles && Object.keys(nextProfiles).length > 0 ? nextProfiles : undefined, - order: prunedOrder.next, - } + ...cfg.auth, + profiles: nextProfiles && Object.keys(nextProfiles).length > 0 ? nextProfiles : undefined, + order: prunedOrder.next, + } : undefined; return { @@ -110,23 +110,23 @@ export async function maybeRemoveDeprecatedCliAuthProfiles( if (deprecated.size === 0) return cfg; - const lines = ["Deprecated external CLI auth profiles detected (no longer supported):"]; + const lines = ["检测到已弃用的外部 CLI 身份验证配置文件(不再支持):"]; if (deprecated.has(CLAUDE_CLI_PROFILE_ID)) { lines.push( - `- ${CLAUDE_CLI_PROFILE_ID} (Anthropic): use setup-token → ${formatCliCommand("openclaw models auth setup-token")}`, + `- ${CLAUDE_CLI_PROFILE_ID} (Anthropic): 请使用 setup-token → ${formatCliCommand("openclaw models auth setup-token")}`, ); } if (deprecated.has(CODEX_CLI_PROFILE_ID)) { lines.push( - `- ${CODEX_CLI_PROFILE_ID} (OpenAI Codex): use OAuth → ${formatCliCommand( + `- ${CODEX_CLI_PROFILE_ID} (OpenAI Codex): 请使用 OAuth → ${formatCliCommand( "openclaw models auth login --provider openai-codex", )}`, ); } - note(lines.join("\n"), "Auth profiles"); + note(lines.join("\n"), "身份验证配置文件"); const shouldRemove = await prompter.confirmRepair({ - message: "Remove deprecated CLI auth profiles now?", + message: "立即移除已弃用的 CLI 身份验证配置文件?", initialValue: true, }); if (!shouldRemove) return cfg; @@ -175,7 +175,7 @@ export async function maybeRemoveDeprecatedCliAuthProfiles( Array.from(deprecated.values()) .map((id) => `- removed ${id} from config`) .join("\n"), - "Doctor changes", + "医生修改", ); } return pruned.next; @@ -190,16 +190,16 @@ type AuthIssue = { function formatAuthIssueHint(issue: AuthIssue): string | null { if (issue.provider === "anthropic" && issue.profileId === CLAUDE_CLI_PROFILE_ID) { - return `Deprecated profile. Use ${formatCliCommand("openclaw models auth setup-token")} or ${formatCliCommand( + return `已弃用的配置文件。请使用 ${formatCliCommand("openclaw models auth setup-token")} 或 ${formatCliCommand( "openclaw configure", - )}.`; + )}。`; } if (issue.provider === "openai-codex" && issue.profileId === CODEX_CLI_PROFILE_ID) { - return `Deprecated profile. Use ${formatCliCommand( + return `已弃用的配置文件。请使用 ${formatCliCommand( "openclaw models auth login --provider openai-codex", - )} or ${formatCliCommand("openclaw configure")}.`; + )} 或 ${formatCliCommand("openclaw configure")}。`; } - return `Re-auth via \`${formatCliCommand("openclaw configure")}\` or \`${formatCliCommand("openclaw onboard")}\`.`; + return `通过 \`${formatCliCommand("openclaw configure")}\` 或 \`${formatCliCommand("openclaw onboard")}\` 重新验证。`; } function formatAuthIssueLine(issue: AuthIssue): string { @@ -227,18 +227,18 @@ export async function noteAuthProfileHealth(params: { const remaining = formatRemainingShort(until - now); const kind = typeof stats?.disabledUntil === "number" && now < stats.disabledUntil - ? `disabled${stats.disabledReason ? `:${stats.disabledReason}` : ""}` - : "cooldown"; - const hint = kind.startsWith("disabled:billing") - ? "Top up credits (provider billing) or switch provider." - : "Wait for cooldown or switch provider."; + ? `已禁用${stats.disabledReason ? `:${stats.disabledReason}` : ""}` + : "冷却中"; + const hint = kind.startsWith("已禁用:billing") + ? "充值(提供商计费)或切换提供商。" + : "等待冷却或切换提供商。"; out.push(`- ${profileId}: ${kind} (${remaining})${hint ? ` — ${hint}` : ""}`); } return out; })(); if (unusable.length > 0) { - note(unusable.join("\n"), "Auth profile cooldowns"); + note(unusable.join("\n"), "身份验证配置文件冷却"); } let summary = buildAuthHealthSummary({ @@ -260,7 +260,7 @@ export async function noteAuthProfileHealth(params: { if (issues.length === 0) return; const shouldRefresh = await params.prompter.confirmRepair({ - message: "Refresh expiring OAuth tokens now? (static tokens need re-auth)", + message: "立即刷新过期的 OAuth 令牌?(静态令牌需要重新验证)", initialValue: true, }); @@ -282,7 +282,7 @@ export async function noteAuthProfileHealth(params: { } } if (errors.length > 0) { - note(errors.join("\n"), "OAuth refresh errors"); + note(errors.join("\n"), "OAuth 刷新错误"); } summary = buildAuthHealthSummary({ store: ensureAuthProfileStore(undefined, { @@ -306,7 +306,7 @@ export async function noteAuthProfileHealth(params: { }), ) .join("\n"), - "Model auth", + "模型验证", ); } } diff --git a/src/commands/onboard-helpers.ts b/src/commands/onboard-helpers.ts index 774893213..aba1441e6 100644 --- a/src/commands/onboard-helpers.ts +++ b/src/commands/onboard-helpers.ts @@ -27,10 +27,11 @@ import { } from "../utils.js"; import { VERSION } from "../version.js"; import type { NodeManagerChoice, OnboardMode, ResetScope } from "./onboard-types.js"; +import { t } from "../wizard/i18n.js"; export function guardCancel(value: T | symbol, runtime: RuntimeEnv): T { if (isCancel(value)) { - cancel(stylePromptTitle("Setup cancelled.") ?? "Setup cancelled."); + cancel(stylePromptTitle(t("onboarding.helpers.cancelled")) ?? t("onboarding.helpers.cancelled")); runtime.exit(0); } return value as T; @@ -55,7 +56,7 @@ export function summarizeExistingConfig(config: OpenClawConfig): string { if (config.skills?.install?.nodeManager) { rows.push(shortenHomeInString(`skills.nodeManager: ${config.skills.install.nodeManager}`)); } - return rows.length ? rows.join("\n") : "No key settings detected."; + return rows.length ? rows.join("\n") : t("onboarding.helpers.noSettings"); } export function randomToken(): string { @@ -172,7 +173,7 @@ export function formatControlUiSshHint(params: { const authedUrl = params.token ? `${localUrl}${tokenParam}` : undefined; const sshTarget = resolveSshTargetHint(); return [ - "No GUI detected. Open from your computer:", + t("onboarding.helpers.sshHint"), `ssh -N -L ${params.port}:127.0.0.1:${params.port} ${sshTarget}`, "Then open:", localUrl, @@ -242,10 +243,10 @@ export async function ensureWorkspaceAndSessions( dir: workspaceDir, ensureBootstrapFiles: !options?.skipBootstrap, }); - runtime.log(`Workspace OK: ${shortenHomePath(ws.dir)}`); + runtime.log(`${t("onboarding.helpers.workspaceOk")}: ${shortenHomePath(ws.dir)}`); const sessionsDir = resolveSessionTranscriptsDirForAgent(options?.agentId); await fs.mkdir(sessionsDir, { recursive: true }); - runtime.log(`Sessions OK: ${shortenHomePath(sessionsDir)}`); + runtime.log(`${t("onboarding.helpers.sessionsOk")}: ${shortenHomePath(sessionsDir)}`); } export function resolveNodeManagerOptions(): Array<{ @@ -268,9 +269,9 @@ export async function moveToTrash(pathname: string, runtime: RuntimeEnv): Promis } try { await runCommandWithTimeout(["trash", pathname], { timeoutMs: 5000 }); - runtime.log(`Moved to Trash: ${shortenHomePath(pathname)}`); + runtime.log(`${t("onboarding.helpers.trashOk")}: ${shortenHomePath(pathname)}`); } catch { - runtime.log(`Failed to move to Trash (manual delete): ${shortenHomePath(pathname)}`); + runtime.log(`${t("onboarding.helpers.trashFail")}: ${shortenHomePath(pathname)}`); } } diff --git a/src/commands/onboard.ts b/src/commands/onboard.ts index 915097d83..73dfaa32f 100644 --- a/src/commands/onboard.ts +++ b/src/commands/onboard.ts @@ -8,6 +8,7 @@ import { runInteractiveOnboarding } from "./onboard-interactive.js"; import { runNonInteractiveOnboarding } from "./onboard-non-interactive.js"; import { formatCliCommand } from "../cli/command-format.js"; import type { OnboardOptions } from "./onboard-types.js"; +import { t } from "../wizard/i18n.js"; export async function onboardCommand(opts: OnboardOptions, runtime: RuntimeEnv = defaultRuntime) { assertSupportedRuntime(runtime); @@ -21,18 +22,18 @@ export async function onboardCommand(opts: OnboardOptions, runtime: RuntimeEnv = if (opts.nonInteractive && (authChoice === "claude-cli" || authChoice === "codex-cli")) { runtime.error( [ - `Auth choice "${authChoice}" is deprecated.`, - 'Use "--auth-choice token" (Anthropic setup-token) or "--auth-choice openai-codex".', + t("onboarding.cli.deprecatedAuth").replace("{authChoice}", authChoice), + t("onboarding.cli.useAuthToken"), ].join("\n"), ); runtime.exit(1); return; } if (authChoice === "claude-cli") { - runtime.log('Auth choice "claude-cli" is deprecated; using setup-token flow instead.'); + runtime.log(t("onboarding.cli.authTokenFlow")); } if (authChoice === "codex-cli") { - runtime.log('Auth choice "codex-cli" is deprecated; using OpenAI Codex OAuth instead.'); + runtime.log(t("onboarding.cli.authCodexFlow")); } const flow = opts.flow === "manual" ? ("advanced" as const) : opts.flow; const normalizedOpts = @@ -43,8 +44,7 @@ export async function onboardCommand(opts: OnboardOptions, runtime: RuntimeEnv = if (normalizedOpts.nonInteractive && normalizedOpts.acceptRisk !== true) { runtime.error( [ - "Non-interactive onboarding requires explicit risk acknowledgement.", - "Read: https://docs.openclaw.ai/security", + t("onboarding.cli.nonInteractiveRisk"), `Re-run with: ${formatCliCommand("openclaw onboard --non-interactive --accept-risk ...")}`, ].join("\n"), ); @@ -61,13 +61,7 @@ export async function onboardCommand(opts: OnboardOptions, runtime: RuntimeEnv = } if (process.platform === "win32") { - runtime.log( - [ - "Windows detected.", - "WSL2 is strongly recommended; native Windows is untested and more problematic.", - "Guide: https://docs.openclaw.ai/windows", - ].join("\n"), - ); + runtime.log(t("onboarding.cli.winWarning")); } if (normalizedOpts.nonInteractive) { diff --git a/src/wizard/i18n.ts b/src/wizard/i18n.ts new file mode 100644 index 000000000..adf64f3d2 --- /dev/null +++ b/src/wizard/i18n.ts @@ -0,0 +1,26 @@ + +import { zhCN } from "./locales/zh-CN.js"; + +const currentLocale = "zh-CN"; // Default to Chinese +const locales: Record = { + "zh-CN": zhCN, +}; + +export function t(key: string, args?: Record): string { + const keys = key.split("."); + let value = locales[currentLocale]; + for (const k of keys) { + if (value && typeof value === "object") { + value = value[k]; + } else { + return key; + } + } + let str = typeof value === "string" ? value : key; + if (args) { + for (const [k, v] of Object.entries(args)) { + str = str.replace(new RegExp(`{${k}}`, "g"), String(v)); + } + } + return str; +} diff --git a/src/wizard/locales/zh-CN.ts b/src/wizard/locales/zh-CN.ts new file mode 100644 index 000000000..f436f23fd --- /dev/null +++ b/src/wizard/locales/zh-CN.ts @@ -0,0 +1,301 @@ + +export const zhCN = { + onboarding: { + title: "OpenClaw 引导安装", + intro: "欢迎使用 OpenClaw 引导安装", + security: { + title: "安全警告", + note: [ + "安全警告 — 请仔细阅读。", + "", + "OpenClaw 是一个个人爱好项目,目前处于 Beta 阶段。请做好遇到问题的心理准备。", + "如果启用了技能工具,机器人可以读取您的文件并执行操作。", + "恶意提示(Prompt Injection)可能会诱使机器人执行不安全的操作。", + "", + "如果您对基础安全和访问控制感到不放心,请不要运行 OpenClaw。", + "在启用工具或将其暴露在互联网之前,请先向有经验的人寻求帮助。", + "", + "推荐的安全基准:", + "- 开启配对/白名单机制 + 提及触发(Mention Gating)。", + "- 在沙箱中运行 + 最小权限原则。", + "- 不要让代理程序能接触到敏感的系统密钥和凭证。", + "- 对拥有工具权限或监听不信任渠道的机器人,务必使用最强大的模型。", + "", + "定期运行审计命令:", + "openclaw security audit --deep", + "openclaw security audit --fix", + "", + "必读文档: https://docs.openclaw.ai/gateway/security", + ].join("\n"), + confirm: "我理解这是非常强大的工具,并且具有内在风险。是否继续?", + cancelled: "未接受安全风险,已取消。", + }, + config: { + invalid: "配置文件无效", + issues: "配置问题提示", + repair: "配置无效。请运行 `openclaw doctor` 进行修复,然后重新运行引导安装。", + }, + flow: { + modeSelect: "选择安装模式", + quickstart: "快速上手 (QuickStart)", + quickstartHint: "稍后可以通过 `openclaw configure` 进行详细调整。", + manual: "手动配置 (Manual)", + manualHint: "详细配置端口、网络、Tailscale 及认证选项。", + invalidFlow: "无效的 --flow 参数(请使用 quickstart, manual 或 advanced)。", + remoteSwitch: "快速上手模式仅支持本地网关。正在切换到手动模式。", + }, + existingConfig: { + title: "检测到现有配置", + action: "配置处理方式", + keep: "使用现有值", + modify: "更新配置值", + reset: "重置 (Reset)", + resetScope: "重置范围", + scopeConfig: "仅重置基本配置", + scopeConfigCreds: "重置基本配置 + 凭证 + 会话", + scopeFull: "完整重置 (配置 + 凭证 + 会话 + 工作区)", + }, + gateway: { + keepSettings: "保留当前的网关设置:", + port: "网关端口", + bind: "网关绑定", + auth: "网关认证", + tailscale: "Tailscale 暴露", + chatChannels: "直接前往聊天渠道配置。", + bindLoopback: "本地回环 (127.0.0.1)", + bindLan: "局域网 (LAN)", + bindCustom: "自定义 IP", + bindTailnet: "Tailnet (Tailscale IP)", + bindAuto: "自动", + authToken: "令牌 Token (默认)", + authPassword: "密码 Password", + tsOff: "关闭", + tsServe: "Serve 模式", + tsFunnel: "Funnel 模式", + }, + setup: { + question: "您想设置什么?", + local: "本地网关 (Local gateway - 当前机器)", + localOk: "网关可达", + localFail: "未检测到网关", + remote: "远程网关 (Remote gateway - 仅配置信息)", + remoteNoUrl: "尚未配置远程 URL", + remoteOk: "远程网关可达", + remoteFail: "已配置但无法连接", + remoteDone: "远程网关配置完成。", + workspaceDir: "工作区目录", + skippingChannels: "跳过渠道设置。", + skills: "技能 (Skills)", + skippingSkills: "跳过技能设置。", + }, + cli: { + winWarning: [ + "检测到 Windows 系统。", + "强烈建议使用 WSL2;原生 Windows 环境未经充分测试,可能存在兼容性问题。", + "指南: https://docs.openclaw.ai/windows", + ].join("\n"), + nonInteractiveRisk: [ + "非交互式安装需要明确的技术风险说明(--accept-risk)。", + "详情请阅读: https://docs.openclaw.ai/security", + ].join("\n"), + deprecatedAuth: '身份验证选项 "{authChoice}" 已弃用。', + useAuthToken: '请使用 "--auth-choice token" (Anthropic) 或 "--auth-choice openai-codex"。', + authTokenFlow: '身份验证选项 "claude-cli" 已弃用,将使用令牌(token)流程。', + authCodexFlow: '身份验证选项 "codex-cli" 已弃用,将使用 OpenAI Codex 会话流程。', + }, + helpers: { + cancelled: "设置已取消。", + noSettings: "未检测到关键配置。", + workspaceOk: "工作区确认", + sessionsOk: "会话目录确认", + trashOk: "已移至回收站", + trashFail: "移至回收站失败 (请手动删除)", + sshHint: "未检测到 GUI 环境。请从您的电脑上访问:", + }, + finalize: { + systemdNote: "检测到 Linux,但当前用户似乎无法使用 Systemd。这可能会影响服务安装。", + systemdLinger: "为确保 OpenClaw 服务在您登出后继续运行,我们需要启用用户逗留 (Linger)。", + installService: "是否将 OpenClaw 安装为后台服务?", + serviceNoSystemd: "由于 Systemd不可用,跳过服务安装。您可以手动运行 OpenClaw。", + serviceInstalled: "服务管理", + serviceRuntime: "服务运行时 (Daemon Runtime)", + serviceRuntimeQuickstart: "快速启动模式下,我们将使用 Node.js 运行时。", + restarted: "服务已重启", + restarting: "正在重启服务...", + uninstalled: "服务已卸载", + uninstalling: "正在卸载服务...", + preparing: "正在准备安装...", + installing: "正在安装服务...", + installFail: "安装失败", + installSuccess: "安装成功", + healthHelp: "健康检查失败。请参考文档进行排查:", + healthDocsPrefix: "相关文档:", + optionalApps: "可选组件", + optionalAppsList: "OpenClaw 提供了 Web UI、TUI 等多种管理方式。", + controlUi: "控制面板 (Control UI)", + hatchTui: "启动 TUI (Moltbot)", + hatchTuiNote: [ + "这是定义您的代理人的关键动作。", + "请花点时间。", + "您告诉它的信息越多,体验就越好。", + '我们将发送: "唤醒吧,我的朋友!"', + ].join("\n"), + hatchWeb: "打开 Web UI", + hatchLater: "以后再说", + hatchQuestion: "您想现在启动哪个界面?", + tokenNote: [ + "网关令牌 (Token):用于网关和控制面板的共享身份验证。", + "存储位置:~/.openclaw/openclaw.json (gateway.auth.token) 或环境变量 OPENCLAW_GATEWAY_TOKEN。", + "网页 UI 会在浏览器本地存储中保存一份副本。", + "随时获取带令牌的链接:openclaw dashboard --no-open", + ].join("\n"), + webUiSeeded: "网页 UI 已在后台初始化。稍后可通过以下命令打开:", + dashboardReady: "仪表板已就绪", + dashboardOpened: "已在浏览器中打开。请保留该标签页以控制 OpenClaw。", + dashboardCopy: "请将此 URL 复制到本机的浏览器中以控制 OpenClaw。", + backupNote: "请定期备份您的工作区目录,它包含您的所有 Agent 数据。", + webSearchOptional: "网页搜索功能 (可选)", + webSearchEnabled: "网页搜索已成功启用!代理人可以在需要时在线查询信息。", + webSearchDisabled: [ + "如果您希望代理人能够搜索网页,则需要一个 API 密钥。", + "", + "OpenClaw 使用 Brave Search 进行网页搜索。如果没有 API 密钥,该工具将无法工作。", + "", + "设置方法:", + "- 运行: openclaw configure --section web", + "- 启用 web_search 并粘贴您的 Brave Search API 密钥", + "", + "或者:在网关环境变量中设置 BRAVE_API_KEY。", + ].join("\n"), + webSearchKeyConfig: "已使用配置文件中的 API Key。", + webSearchKeyEnv: "已使用系统环境变量 BRAVE_API_KEY。", + whatNow: "接下来可以做什么?", + onboardingComplete: "OpenClaw 初始化完成!", + onboardingCompleteOpened: "OpenClaw 初始化完成,仪表板已随令牌打开;请保留该标签页以控制 OpenClaw。", + onboardingCompleteSeeded: "OpenClaw 初始化完成,网页 UI 已在后台初始化;随时使用上面的链接打开。", + }, + gatewayConfig: { + port: "网关端口", + invalidPort: "无效的端口号", + bind: "网关绑定 (Bind)", + customIp: "自定义 IP 地址", + customIpRequired: "自定义绑定模式需要提供 IP 地址", + invalidIp: "无效的 IPv4 地址", + invalidIpOctet: "无效的 IPv4 地址 (每段必须是 0-255)", + auth: "网关身份验证", + authToken: "令牌 (Token)", + authTokenHint: "推荐的默认方式 (支持本地和远程)", + authPassword: "密码 (Password)", + tsExposure: "Tailscale 暴露", + tsOff: "关闭", + tsOffHint: "不进行 Tailscale 暴露", + tsServe: "Serve 模式", + tsServeHint: "为您的 Tailnet 提供私有 HTTPS (仅限 Tailscale 里的设备)", + tsFunnel: "Funnel 模式", + tsFunnelHint: "通过 Tailscale Funnel 提供公共 HTTPS (互联网可访问)", + tsWarningTitle: "Tailscale 警告", + tsNotFound: [ + "未在 PATH 中找到 Tailscale 二进制文件。", + "请确保已安装 Tailscale。", + "", + "您可以继续设置,但 serve/funnel 在运行时会失败。", + ].join("\n"), + tsResetConfirm: "退出时重置 Tailscale serve/funnel?", + tsAdjustBind: "Tailscale 需要 bind=loopback。正在自动调整网关绑定为 loopback。", + tsFunnelAuth: "Tailscale Funnel 需要使用密码验证方式。", + tokenPlaceholder: "网关令牌 (留空则自动生成)", + tokenHint: "多机访问或非 127.0.0.1 访问时需要此令牌", + passwordLabel: "网关密码", + passwordRequired: "必须填写密码", + } + }, + configure: { + title: "OpenClaw 配置", + updateTitle: "OpenClaw 更新向导", + auth: { + anthropicOAuthModels: "Anthropic OAuth 模型", + }, + sections: { + title: "选择要配置的部分", + continue: "继续", + continueHint: "完成", + skipHint: "暂时跳过", + workspace: "工作区 (Workspace)", + workspaceHint: "设置工作区 + 会话", + model: "模型 (Model)", + modelHint: "选择提供商 + 凭证", + web: "Web 工具", + webHint: "配置 Brave 搜索 + 用于抓取", + gateway: "网关 (Gateway)", + gatewayHint: "端口、绑定、认证、Tailscale", + daemon: "后台服务 (Daemon)", + daemonHint: "安装/管理后台服务", + channels: "渠道 (Channels)", + channelsHint: "连接 WhatsApp/Telegram 等", + skills: "技能 (Skills)", + skillsHint: "安装/启用工作区技能", + health: "健康检查 (Health check)", + healthHint: "运行网关 + 渠道检查", + }, + gateway: { + modeSelect: "网关将在哪里运行?", + local: "本地 (这台机器)", + localHintReachable: "网关可达 ({url})", + localHintMissing: "未检测到网关 ({url})", + remote: "远程 (仅信息)", + remoteHintNoUrl: "尚未配置远程 URL", + remoteHintReachable: "网关可达 ({url})", + remoteHintConfigured: "已配置但无法连接 ({url})", + remoteConfigured: "远程网关已配置。", + modeSetLocal: "网关模式已设置为本地。", + noChanges: "未选择任何更改。", + configureComplete: "配置完成。", + }, + channels: { + modeTitle: "渠道配置模式", + configure: "配置/连接", + configureHint: "添加/更新渠道;禁用未选中的账户", + remove: "移除渠道配置", + removeHint: "从 openclaw.json 中删除渠道令牌/设置", + }, + web: { + title: "网页搜索", + desc: [ + "网页搜索允许您的 Agent 使用 `web_search` 工具在线查找信息。", + "它需要 Brave Search API 密钥(您可以将其存储在配置中或在网关环境中设置 BRAVE_API_KEY)。", + "文档: https://docs.openclaw.ai/tools/web", + ].join("\n"), + enableSearch: "启用 web_search (Brave Search)?", + keyPrompt: "Brave Search API 密钥 (留空保留当前值或使用 BRAVE_API_KEY)", + keyPromptEmpty: "Brave Search API 密钥 (粘贴到这里;留空使用 BRAVE_API_KEY)", + placeholderKey: "留空以保留当前设置", + placeholderKeyEmpty: "BSA...", + noKeyWarning: [ + "尚未存储密钥,因此 web_search 将不可用。", + "请在此处存储密钥或在网关环境中设置 BRAVE_API_KEY。", + "文档: https://docs.openclaw.ai/tools/web", + ].join("\n"), + enableFetch: "启用 web_fetch (无密钥 HTTP 抓取)?", + }, + daemon: { + alreadyInstalled: "网关服务已安装", + restart: "重启 (Restart)", + reinstall: "重新安装 (Reinstall)", + skip: "跳过 (Skip)", + restarting: "正在重启网关服务…", + restarted: "网关服务已重启。", + uninstalling: "正在卸载网关服务…", + uninstalled: "网关服务已卸载。", + selectRuntime: "网关服务运行时", + preparing: "正在准备网关服务…", + installing: "正在安装网关服务…", + installed: "网关服务已安装。", + installFailed: "网关服务安装失败。", + installFailedNote: "网关服务安装失败: ", + lingerReason: "Linux 安装使用 systemd 用户服务。如果不启用 lingering,systemd 会在注销/空闲时停止用户会话并终止网关。", + } + }, + common: { + configUpdated: "配置已更新。", + } +}; diff --git a/src/wizard/onboarding.finalize.ts b/src/wizard/onboarding.finalize.ts index c5b01d6bf..1f5e63e37 100644 --- a/src/wizard/onboarding.finalize.ts +++ b/src/wizard/onboarding.finalize.ts @@ -33,8 +33,9 @@ import { } from "../commands/daemon-install-helpers.js"; import type { GatewayWizardSettings, WizardFlow } from "./onboarding.types.js"; import type { WizardPrompter } from "./prompts.js"; +import { t } from "./i18n.js"; -type FinalizeOnboardingOptions = { +export type FinalizeOnboardingOptions = { flow: WizardFlow; opts: OnboardOptions; baseConfig: OpenClawConfig; @@ -65,7 +66,7 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption process.platform === "linux" ? await isSystemdUserServiceAvailable() : true; if (process.platform === "linux" && !systemdAvailable) { await prompter.note( - "Systemd user services are unavailable. Skipping lingering checks and service install.", + t("onboarding.finalize.systemdNote"), "Systemd", ); } @@ -78,8 +79,7 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption confirm: prompter.confirm, note: prompter.note, }, - reason: - "Linux installs use a systemd user service by default. Without lingering, systemd stops the user session on logout/idle and kills the Gateway.", + reason: t("onboarding.finalize.systemdLinger"), requireConfirm: false, }); } @@ -95,15 +95,15 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption installDaemon = true; } else { installDaemon = await prompter.confirm({ - message: "Install Gateway service (recommended)", + message: t("onboarding.finalize.installService"), initialValue: true, }); } if (process.platform === "linux" && !systemdAvailable && installDaemon) { await prompter.note( - "Systemd user services are unavailable; skipping service install. Use your container supervisor or `docker compose up -d`.", - "Gateway service", + t("onboarding.finalize.serviceNoSystemd"), + t("onboarding.finalize.serviceInstalled"), ); installDaemon = false; } @@ -113,33 +113,33 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption flow === "quickstart" ? (DEFAULT_GATEWAY_DAEMON_RUNTIME as GatewayDaemonRuntime) : ((await prompter.select({ - message: "Gateway service runtime", - options: GATEWAY_DAEMON_RUNTIME_OPTIONS, - initialValue: opts.daemonRuntime ?? DEFAULT_GATEWAY_DAEMON_RUNTIME, - })) as GatewayDaemonRuntime); + message: t("onboarding.finalize.serviceRuntime"), + options: GATEWAY_DAEMON_RUNTIME_OPTIONS, + initialValue: opts.daemonRuntime ?? DEFAULT_GATEWAY_DAEMON_RUNTIME, + })) as GatewayDaemonRuntime); if (flow === "quickstart") { await prompter.note( - "QuickStart uses Node for the Gateway service (stable + supported).", - "Gateway service runtime", + t("onboarding.finalize.serviceRuntimeQuickstart"), + t("onboarding.finalize.serviceRuntime"), ); } const service = resolveGatewayService(); const loaded = await service.isLoaded({ env: process.env }); if (loaded) { const action = (await prompter.select({ - message: "Gateway service already installed", + message: t("onboarding.finalize.serviceInstalled"), options: [ - { value: "restart", label: "Restart" }, - { value: "reinstall", label: "Reinstall" }, - { value: "skip", label: "Skip" }, + { value: "restart", label: "重启 (Restart)" }, + { value: "reinstall", label: "重新安装 (Reinstall)" }, + { value: "skip", label: "跳过 (Skip)" }, ], })) as "restart" | "reinstall" | "skip"; if (action === "restart") { await withWizardProgress( - "Gateway service", - { doneMessage: "Gateway service restarted." }, + t("onboarding.finalize.serviceInstalled"), + { doneMessage: t("onboarding.finalize.restarted") }, async (progress) => { - progress.update("Restarting Gateway service…"); + progress.update(t("onboarding.finalize.restarting")); await service.restart({ env: process.env, stdout: process.stdout, @@ -148,10 +148,10 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption ); } else if (action === "reinstall") { await withWizardProgress( - "Gateway service", - { doneMessage: "Gateway service uninstalled." }, + t("onboarding.finalize.serviceInstalled"), + { doneMessage: t("onboarding.finalize.uninstalled") }, async (progress) => { - progress.update("Uninstalling Gateway service…"); + progress.update(t("onboarding.finalize.uninstalling")); await service.uninstall({ env: process.env, stdout: process.stdout }); }, ); @@ -159,10 +159,10 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption } if (!loaded || (loaded && (await service.isLoaded({ env: process.env })) === false)) { - const progress = prompter.progress("Gateway service"); + const progress = prompter.progress(t("onboarding.finalize.serviceInstalled")); let installError: string | null = null; try { - progress.update("Preparing Gateway service…"); + progress.update(t("onboarding.finalize.preparing")); const { programArguments, workingDirectory, environment } = await buildGatewayInstallPlan({ env: process.env, port: settings.port, @@ -172,7 +172,7 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption config: nextConfig, }); - progress.update("Installing Gateway service…"); + progress.update(t("onboarding.finalize.installing")); await service.install({ env: process.env, stdout: process.stdout, @@ -184,11 +184,11 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption installError = err instanceof Error ? err.message : String(err); } finally { progress.stop( - installError ? "Gateway service install failed." : "Gateway service installed.", + installError ? t("onboarding.finalize.installFail") : t("onboarding.finalize.installSuccess"), ); } if (installError) { - await prompter.note(`Gateway service install failed: ${installError}`, "Gateway"); + await prompter.note(`${t("onboarding.finalize.installFail")}: ${installError}`, "Gateway"); await prompter.note(gatewayInstallErrorHint(), "Gateway"); } } @@ -213,11 +213,11 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption runtime.error(formatHealthCheckFailure(err)); await prompter.note( [ - "Docs:", + t("onboarding.finalize.healthDocsPrefix"), "https://docs.openclaw.ai/gateway/health", "https://docs.openclaw.ai/gateway/troubleshooting", ].join("\n"), - "Health check help", + t("onboarding.finalize.healthHelp"), ); } } @@ -232,13 +232,8 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption } await prompter.note( - [ - "Add nodes for extra features:", - "- macOS app (system + notifications)", - "- iOS app (camera/canvas)", - "- Android app (camera/canvas)", - ].join("\n"), - "Optional apps", + t("onboarding.finalize.optionalAppsList"), + t("onboarding.finalize.optionalApps"), ); const controlUiBasePath = @@ -260,8 +255,8 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption password: settings.authMode === "password" ? nextConfig.gateway?.auth?.password : "", }); const gatewayStatusLine = gatewayProbe.ok - ? "Gateway: reachable" - : `Gateway: not detected${gatewayProbe.detail ? ` (${gatewayProbe.detail})` : ""}`; + ? t("onboarding.setup.localOk") + : `${t("onboarding.setup.localFail")}${gatewayProbe.detail ? ` (${gatewayProbe.detail})` : ""}`; const bootstrapPath = path.join( resolveUserPath(options.workspaceDir), DEFAULT_BOOTSTRAP_FILENAME, @@ -274,14 +269,14 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption await prompter.note( [ `Web UI: ${links.httpUrl}`, - tokenParam ? `Web UI (with token): ${authedUrl}` : undefined, + tokenParam ? `Web UI (${t("onboarding.gatewayConfig.authToken")}): ${authedUrl}` : undefined, `Gateway WS: ${links.wsUrl}`, gatewayStatusLine, "Docs: https://docs.openclaw.ai/web/control-ui", ] .filter(Boolean) .join("\n"), - "Control UI", + t("onboarding.finalize.controlUi"), ); let controlUiOpened = false; @@ -292,32 +287,22 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption if (!opts.skipUi && gatewayProbe.ok) { if (hasBootstrap) { await prompter.note( - [ - "This is the defining action that makes your agent you.", - "Please take your time.", - "The more you tell it, the better the experience will be.", - 'We will send: "Wake up, my friend!"', - ].join("\n"), - "Start TUI (best option!)", + t("onboarding.finalize.hatchTuiNote"), + t("onboarding.finalize.hatchTui"), ); } await prompter.note( - [ - "Gateway token: shared auth for the Gateway + Control UI.", - "Stored in: ~/.openclaw/openclaw.json (gateway.auth.token) or OPENCLAW_GATEWAY_TOKEN.", - "Web UI stores a copy in this browser's localStorage (openclaw.control.settings.v1).", - `Get the tokenized link anytime: ${formatCliCommand("openclaw dashboard --no-open")}`, - ].join("\n"), - "Token", + t("onboarding.finalize.tokenNote"), + t("onboarding.gatewayConfig.authToken"), ); hatchChoice = (await prompter.select({ - message: "How do you want to hatch your bot?", + message: t("onboarding.finalize.hatchQuestion"), options: [ - { value: "tui", label: "Hatch in TUI (recommended)" }, - { value: "web", label: "Open the Web UI" }, - { value: "later", label: "Do this later" }, + { value: "tui", label: t("onboarding.finalize.hatchTui") }, + { value: "web", label: t("onboarding.finalize.hatchWeb") }, + { value: "later", label: t("onboarding.finalize.hatchLater") }, ], initialValue: "tui", })) as "tui" | "web" | "later"; @@ -336,7 +321,7 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption } if (seededInBackground) { await prompter.note( - `Web UI seeded in the background. Open later with: ${formatCliCommand( + `${t("onboarding.finalize.webUiSeeded")} ${formatCliCommand( "openclaw dashboard --no-open", )}`, "Web UI", @@ -362,37 +347,37 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption } await prompter.note( [ - `Dashboard link (with token): ${authedUrl}`, + `${t("onboarding.finalize.dashboardReady")} (${t("onboarding.gatewayConfig.authToken")}): ${authedUrl}`, controlUiOpened - ? "Opened in your browser. Keep that tab to control OpenClaw." - : "Copy/paste this URL in a browser on this machine to control OpenClaw.", + ? t("onboarding.finalize.dashboardOpened") + : t("onboarding.finalize.dashboardCopy"), controlUiOpenHint, ] .filter(Boolean) .join("\n"), - "Dashboard ready", + t("onboarding.finalize.dashboardReady"), ); } else { await prompter.note( - `When you're ready: ${formatCliCommand("openclaw dashboard --no-open")}`, - "Later", + `${t("onboarding.finalize.hatchLater")}: ${formatCliCommand("openclaw dashboard --no-open")}`, + t("onboarding.finalize.hatchLater"), ); } } else if (opts.skipUi) { - await prompter.note("Skipping Control UI/TUI prompts.", "Control UI"); + await prompter.note("Skipping Control UI/TUI prompts.", t("onboarding.finalize.controlUi")); } await prompter.note( [ - "Back up your agent workspace.", + t("onboarding.finalize.backupNote"), "Docs: https://docs.openclaw.ai/concepts/agent-workspace", ].join("\n"), - "Workspace backup", + t("onboarding.finalize.backupNote"), ); await prompter.note( "Running agents on your computer is risky — harden your setup: https://docs.openclaw.ai/security", - "Security", + t("onboarding.security.title"), ); const shouldOpenControlUi = @@ -421,15 +406,15 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption await prompter.note( [ - `Dashboard link (with token): ${authedUrl}`, + `${t("onboarding.finalize.dashboardReady")} (${t("onboarding.gatewayConfig.authToken")}): ${authedUrl}`, controlUiOpened - ? "Opened in your browser. Keep that tab to control OpenClaw." - : "Copy/paste this URL in a browser on this machine to control OpenClaw.", + ? t("onboarding.finalize.dashboardOpened") + : t("onboarding.finalize.dashboardCopy"), controlUiOpenHint, ] .filter(Boolean) .join("\n"), - "Dashboard ready", + t("onboarding.finalize.dashboardReady"), ); } @@ -439,38 +424,32 @@ export async function finalizeOnboardingWizard(options: FinalizeOnboardingOption await prompter.note( hasWebSearchKey ? [ - "Web search is enabled, so your agent can look things up online when needed.", - "", - webSearchKey - ? "API key: stored in config (tools.web.search.apiKey)." - : "API key: provided via BRAVE_API_KEY env var (Gateway environment).", - "Docs: https://docs.openclaw.ai/tools/web", - ].join("\n") + t("onboarding.finalize.webSearchEnabled"), + "", + webSearchKey + ? t("onboarding.finalize.webSearchKeyConfig") + : t("onboarding.finalize.webSearchKeyEnv"), + "Docs: https://docs.openclaw.ai/tools/web", + ].join("\n") : [ - "If you want your agent to be able to search the web, you’ll need an API key.", - "", - "OpenClaw uses Brave Search for the `web_search` tool. Without a Brave Search API key, web search won’t work.", - "", - "Set it up interactively:", - `- Run: ${formatCliCommand("openclaw configure --section web")}`, - "- Enable web_search and paste your Brave Search API key", - "", - "Alternative: set BRAVE_API_KEY in the Gateway environment (no config changes).", - "Docs: https://docs.openclaw.ai/tools/web", - ].join("\n"), - "Web search (optional)", + t("onboarding.finalize.webSearchDisabled"), + "", + `设置命令: ${formatCliCommand("openclaw configure --section web")}`, + "Docs: https://docs.openclaw.ai/tools/web", + ].join("\n"), + t("onboarding.finalize.webSearchOptional"), ); await prompter.note( - 'What now: https://openclaw.ai/showcase ("What People Are Building").', - "What now", + 'Showcase: https://openclaw.ai/showcase', + t("onboarding.finalize.whatNow"), ); await prompter.outro( controlUiOpened - ? "Onboarding complete. Dashboard opened with your token; keep that tab to control OpenClaw." + ? t("onboarding.finalize.onboardingCompleteOpened") : seededInBackground - ? "Onboarding complete. Web UI seeded in the background; open it anytime with the tokenized link above." - : "Onboarding complete. Use the tokenized dashboard link above to control OpenClaw.", + ? t("onboarding.finalize.onboardingCompleteSeeded") + : t("onboarding.finalize.onboardingComplete"), ); } diff --git a/src/wizard/onboarding.gateway-config.ts b/src/wizard/onboarding.gateway-config.ts index d7dceae24..f85e6a0b5 100644 --- a/src/wizard/onboarding.gateway-config.ts +++ b/src/wizard/onboarding.gateway-config.ts @@ -9,6 +9,7 @@ import type { WizardFlow, } from "./onboarding.types.js"; import type { WizardPrompter } from "./prompts.js"; +import { t } from "./i18n.js"; type ConfigureGatewayOptions = { flow: WizardFlow; @@ -35,29 +36,29 @@ export async function configureGatewayForOnboarding( flow === "quickstart" ? quickstartGateway.port : Number.parseInt( - String( - await prompter.text({ - message: "Gateway port", - initialValue: String(localPort), - validate: (value) => (Number.isFinite(Number(value)) ? undefined : "Invalid port"), - }), - ), - 10, - ); + String( + await prompter.text({ + message: t("onboarding.gatewayConfig.port"), + initialValue: String(localPort), + validate: (value) => (Number.isFinite(Number(value)) ? undefined : t("onboarding.gatewayConfig.invalidPort")), + }), + ), + 10, + ); let bind = ( flow === "quickstart" ? quickstartGateway.bind : ((await prompter.select({ - message: "Gateway bind", - options: [ - { value: "loopback", label: "Loopback (127.0.0.1)" }, - { value: "lan", label: "LAN (0.0.0.0)" }, - { value: "tailnet", label: "Tailnet (Tailscale IP)" }, - { value: "auto", label: "Auto (Loopback → LAN)" }, - { value: "custom", label: "Custom IP" }, - ], - })) as "loopback" | "lan" | "auto" | "custom" | "tailnet") + message: t("onboarding.gatewayConfig.bind"), + options: [ + { value: "loopback", label: t("onboarding.gateway.bindLoopback") }, + { value: "lan", label: t("onboarding.gateway.bindLan") }, + { value: "tailnet", label: t("onboarding.gateway.bindTailnet") }, + { value: "auto", label: t("onboarding.gateway.bindAuto") }, + { value: "custom", label: t("onboarding.gateway.bindCustom") }, + ], + })) as "loopback" | "lan" | "auto" | "custom" | "tailnet") ) as "loopback" | "lan" | "auto" | "custom" | "tailnet"; let customBindHost = quickstartGateway.customBindHost; @@ -65,14 +66,14 @@ export async function configureGatewayForOnboarding( const needsPrompt = flow !== "quickstart" || !customBindHost; if (needsPrompt) { const input = await prompter.text({ - message: "Custom IP address", + message: t("onboarding.gatewayConfig.customIp"), placeholder: "192.168.1.100", initialValue: customBindHost ?? "", validate: (value) => { - if (!value) return "IP address is required for custom bind mode"; + if (!value) return t("onboarding.gatewayConfig.customIpRequired"); const trimmed = value.trim(); const parts = trimmed.split("."); - if (parts.length !== 4) return "Invalid IPv4 address (e.g., 192.168.1.100)"; + if (parts.length !== 4) return t("onboarding.gatewayConfig.invalidIp"); if ( parts.every((part) => { const n = parseInt(part, 10); @@ -80,7 +81,7 @@ export async function configureGatewayForOnboarding( }) ) return undefined; - return "Invalid IPv4 address (each octet must be 0-255)"; + return t("onboarding.gatewayConfig.invalidIpOctet"); }, }); customBindHost = typeof input === "string" ? input.trim() : undefined; @@ -91,38 +92,38 @@ export async function configureGatewayForOnboarding( flow === "quickstart" ? quickstartGateway.authMode : ((await prompter.select({ - message: "Gateway auth", - options: [ - { - value: "token", - label: "Token", - hint: "Recommended default (local + remote)", - }, - { value: "password", label: "Password" }, - ], - initialValue: "token", - })) as GatewayAuthChoice) + message: t("onboarding.gatewayConfig.auth"), + options: [ + { + value: "token", + label: t("onboarding.gatewayConfig.authToken"), + hint: t("onboarding.gatewayConfig.authTokenHint"), + }, + { value: "password", label: t("onboarding.gatewayConfig.authPassword") }, + ], + initialValue: "token", + })) as GatewayAuthChoice) ) as GatewayAuthChoice; const tailscaleMode = ( flow === "quickstart" ? quickstartGateway.tailscaleMode : ((await prompter.select({ - message: "Tailscale exposure", - options: [ - { value: "off", label: "Off", hint: "No Tailscale exposure" }, - { - value: "serve", - label: "Serve", - hint: "Private HTTPS for your tailnet (devices on Tailscale)", - }, - { - value: "funnel", - label: "Funnel", - hint: "Public HTTPS via Tailscale Funnel (internet)", - }, - ], - })) as "off" | "serve" | "funnel") + message: t("onboarding.gatewayConfig.tsExposure"), + options: [ + { value: "off", label: t("onboarding.gatewayConfig.tsOff"), hint: t("onboarding.gatewayConfig.tsOffHint") }, + { + value: "serve", + label: t("onboarding.gatewayConfig.tsServe"), + hint: t("onboarding.gatewayConfig.tsServeHint"), + }, + { + value: "funnel", + label: t("onboarding.gatewayConfig.tsFunnel"), + hint: t("onboarding.gatewayConfig.tsFunnelHint"), + }, + ], + })) as "off" | "serve" | "funnel") ) as "off" | "serve" | "funnel"; // Detect Tailscale binary before proceeding with serve/funnel setup. @@ -130,14 +131,8 @@ export async function configureGatewayForOnboarding( const tailscaleBin = await findTailscaleBinary(); if (!tailscaleBin) { await prompter.note( - [ - "Tailscale binary not found in PATH or /Applications.", - "Ensure Tailscale is installed from:", - " https://tailscale.com/download/mac", - "", - "You can continue setup, but serve/funnel will fail at runtime.", - ].join("\n"), - "Tailscale Warning", + t("onboarding.gatewayConfig.tsNotFound"), + t("onboarding.gatewayConfig.tsWarningTitle"), ); } } @@ -145,14 +140,14 @@ export async function configureGatewayForOnboarding( let tailscaleResetOnExit = flow === "quickstart" ? quickstartGateway.tailscaleResetOnExit : false; if (tailscaleMode !== "off" && flow !== "quickstart") { await prompter.note( - ["Docs:", "https://docs.openclaw.ai/gateway/tailscale", "https://docs.openclaw.ai/web"].join( + [t("onboarding.finalize.healthDocsPrefix"), "https://docs.openclaw.ai/gateway/tailscale", "https://docs.openclaw.ai/web"].join( "\n", ), - "Tailscale", + t("onboarding.gateway.tailscale"), ); tailscaleResetOnExit = Boolean( await prompter.confirm({ - message: "Reset Tailscale serve/funnel on exit?", + message: t("onboarding.gatewayConfig.tsResetConfirm"), initialValue: false, }), ); @@ -162,13 +157,13 @@ export async function configureGatewayForOnboarding( // - Tailscale wants bind=loopback so we never expose a non-loopback server + tailscale serve/funnel at once. // - Funnel requires password auth. if (tailscaleMode !== "off" && bind !== "loopback") { - await prompter.note("Tailscale requires bind=loopback. Adjusting bind to loopback.", "Note"); + await prompter.note(t("onboarding.gatewayConfig.tsAdjustBind"), t("onboarding.gateway.tailscale")); bind = "loopback"; customBindHost = undefined; } if (tailscaleMode === "funnel" && authMode !== "password") { - await prompter.note("Tailscale funnel requires password auth.", "Note"); + await prompter.note(t("onboarding.gatewayConfig.tsFunnelAuth"), t("onboarding.gateway.tailscale")); authMode = "password"; } @@ -178,8 +173,8 @@ export async function configureGatewayForOnboarding( gatewayToken = quickstartGateway.token ?? randomToken(); } else { const tokenInput = await prompter.text({ - message: "Gateway token (blank to generate)", - placeholder: "Needed for multi-machine or non-loopback access", + message: t("onboarding.gatewayConfig.tokenPlaceholder"), + placeholder: t("onboarding.gatewayConfig.tokenHint"), initialValue: quickstartGateway.token ?? "", }); gatewayToken = String(tokenInput).trim() || randomToken(); @@ -191,9 +186,9 @@ export async function configureGatewayForOnboarding( flow === "quickstart" && quickstartGateway.password ? quickstartGateway.password : await prompter.text({ - message: "Gateway password", - validate: (value) => (value?.trim() ? undefined : "Required"), - }); + message: t("onboarding.gatewayConfig.passwordLabel"), + validate: (value) => (value?.trim() ? undefined : t("onboarding.gatewayConfig.passwordRequired")), + }); nextConfig = { ...nextConfig, gateway: { diff --git a/src/wizard/onboarding.ts b/src/wizard/onboarding.ts index ef2e349c6..7989b3f00 100644 --- a/src/wizard/onboarding.ts +++ b/src/wizard/onboarding.ts @@ -38,6 +38,7 @@ import { logConfigUpdated } from "../config/logging.js"; import type { RuntimeEnv } from "../runtime.js"; import { defaultRuntime } from "../runtime.js"; import { resolveUserPath } from "../utils.js"; +import { t } from "./i18n.js"; import { finalizeOnboardingWizard } from "./onboarding.finalize.js"; import { configureGatewayForOnboarding } from "./onboarding.gateway-config.js"; import type { QuickstartGatewayDefaults, WizardFlow } from "./onboarding.types.js"; @@ -50,33 +51,12 @@ async function requireRiskAcknowledgement(params: { if (params.opts.acceptRisk === true) return; await params.prompter.note( - [ - "Security warning — please read.", - "", - "OpenClaw is a hobby project and still in beta. Expect sharp edges.", - "This bot can read files and run actions if tools are enabled.", - "A bad prompt can trick it into doing unsafe things.", - "", - "If you’re not comfortable with basic security and access control, don’t run OpenClaw.", - "Ask someone experienced to help before enabling tools or exposing it to the internet.", - "", - "Recommended baseline:", - "- Pairing/allowlists + mention gating.", - "- Sandbox + least-privilege tools.", - "- Keep secrets out of the agent’s reachable filesystem.", - "- Use the strongest available model for any bot with tools or untrusted inboxes.", - "", - "Run regularly:", - "openclaw security audit --deep", - "openclaw security audit --fix", - "", - "Must read: https://docs.openclaw.ai/gateway/security", - ].join("\n"), - "Security", + t("onboarding.security.note"), + t("onboarding.security.title"), ); const ok = await params.prompter.confirm({ - message: "I understand this is powerful and inherently risky. Continue?", + message: t("onboarding.security.confirm"), initialValue: false, }); if (!ok) { @@ -90,14 +70,14 @@ export async function runOnboardingWizard( prompter: WizardPrompter, ) { printWizardHeader(runtime); - await prompter.intro("OpenClaw onboarding"); + await prompter.intro(t("onboarding.intro")); await requireRiskAcknowledgement({ opts, prompter }); const snapshot = await readConfigFileSnapshot(); let baseConfig: OpenClawConfig = snapshot.valid ? snapshot.config : {}; if (snapshot.exists && !snapshot.valid) { - await prompter.note(summarizeExistingConfig(baseConfig), "Invalid config"); + await prompter.note(summarizeExistingConfig(baseConfig), t("onboarding.config.invalid")); if (snapshot.issues.length > 0) { await prompter.note( [ @@ -105,18 +85,18 @@ export async function runOnboardingWizard( "", "Docs: https://docs.openclaw.ai/gateway/configuration", ].join("\n"), - "Config issues", + t("onboarding.config.issues"), ); } await prompter.outro( - `Config invalid. Run \`${formatCliCommand("openclaw doctor")}\` to repair it, then re-run onboarding.`, + t("onboarding.config.repair"), ); runtime.exit(1); return; } - const quickstartHint = `Configure details later via ${formatCliCommand("openclaw configure")}.`; - const manualHint = "Configure port, network, Tailscale, and auth options."; + const quickstartHint = t("onboarding.flow.quickstartHint"); + const manualHint = t("onboarding.flow.manualHint"); const explicitFlowRaw = opts.flow?.trim(); const normalizedExplicitFlow = explicitFlowRaw === "manual" ? "advanced" : explicitFlowRaw; if ( @@ -124,7 +104,7 @@ export async function runOnboardingWizard( normalizedExplicitFlow !== "quickstart" && normalizedExplicitFlow !== "advanced" ) { - runtime.error("Invalid --flow (use quickstart, manual, or advanced)."); + runtime.error(t("onboarding.flow.invalidFlow")); runtime.exit(1); return; } @@ -135,47 +115,47 @@ export async function runOnboardingWizard( let flow: WizardFlow = explicitFlow ?? ((await prompter.select({ - message: "Onboarding mode", + message: t("onboarding.flow.modeSelect"), options: [ - { value: "quickstart", label: "QuickStart", hint: quickstartHint }, - { value: "advanced", label: "Manual", hint: manualHint }, + { value: "quickstart", label: t("onboarding.flow.quickstart"), hint: quickstartHint }, + { value: "advanced", label: t("onboarding.flow.manual"), hint: manualHint }, ], initialValue: "quickstart", })) as "quickstart" | "advanced"); if (opts.mode === "remote" && flow === "quickstart") { await prompter.note( - "QuickStart only supports local gateways. Switching to Manual mode.", - "QuickStart", + t("onboarding.flow.remoteSwitch"), + t("onboarding.flow.quickstart"), ); flow = "advanced"; } if (snapshot.exists) { - await prompter.note(summarizeExistingConfig(baseConfig), "Existing config detected"); + await prompter.note(summarizeExistingConfig(baseConfig), t("onboarding.existingConfig.title")); const action = (await prompter.select({ - message: "Config handling", + message: t("onboarding.existingConfig.action"), options: [ - { value: "keep", label: "Use existing values" }, - { value: "modify", label: "Update values" }, - { value: "reset", label: "Reset" }, + { value: "keep", label: t("onboarding.existingConfig.keep") }, + { value: "modify", label: t("onboarding.existingConfig.modify") }, + { value: "reset", label: t("onboarding.existingConfig.reset") }, ], })) as "keep" | "modify" | "reset"; if (action === "reset") { const workspaceDefault = baseConfig.agents?.defaults?.workspace ?? DEFAULT_WORKSPACE; const resetScope = (await prompter.select({ - message: "Reset scope", + message: t("onboarding.existingConfig.resetScope"), options: [ - { value: "config", label: "Config only" }, + { value: "config", label: t("onboarding.existingConfig.scopeConfig") }, { value: "config+creds+sessions", - label: "Config + creds + sessions", + label: t("onboarding.existingConfig.scopeConfigCreds"), }, { value: "full", - label: "Full reset (config + creds + sessions + workspace)", + label: t("onboarding.existingConfig.scopeFull"), }, ], })) as ResetScope; @@ -197,10 +177,10 @@ export async function runOnboardingWizard( const bindRaw = baseConfig.gateway?.bind; const bind = bindRaw === "loopback" || - bindRaw === "lan" || - bindRaw === "auto" || - bindRaw === "custom" || - bindRaw === "tailnet" + bindRaw === "lan" || + bindRaw === "auto" || + bindRaw === "custom" || + bindRaw === "tailnet" ? bindRaw : "loopback"; @@ -237,41 +217,41 @@ export async function runOnboardingWizard( if (flow === "quickstart") { const formatBind = (value: "loopback" | "lan" | "auto" | "custom" | "tailnet") => { - if (value === "loopback") return "Loopback (127.0.0.1)"; - if (value === "lan") return "LAN"; - if (value === "custom") return "Custom IP"; - if (value === "tailnet") return "Tailnet (Tailscale IP)"; - return "Auto"; + if (value === "loopback") return t("onboarding.gateway.bindLoopback"); + if (value === "lan") return t("onboarding.gateway.bindLan"); + if (value === "custom") return t("onboarding.gateway.bindCustom"); + if (value === "tailnet") return t("onboarding.gateway.bindTailnet"); + return t("onboarding.gateway.bindAuto"); }; const formatAuth = (value: GatewayAuthChoice) => { - if (value === "token") return "Token (default)"; - return "Password"; + if (value === "token") return t("onboarding.gateway.authToken"); + return t("onboarding.gateway.authPassword"); }; const formatTailscale = (value: "off" | "serve" | "funnel") => { - if (value === "off") return "Off"; - if (value === "serve") return "Serve"; - return "Funnel"; + if (value === "off") return t("onboarding.gateway.tsOff"); + if (value === "serve") return t("onboarding.gateway.tsServe"); + return t("onboarding.gateway.tsFunnel"); }; const quickstartLines = quickstartGateway.hasExisting ? [ - "Keeping your current gateway settings:", - `Gateway port: ${quickstartGateway.port}`, - `Gateway bind: ${formatBind(quickstartGateway.bind)}`, - ...(quickstartGateway.bind === "custom" && quickstartGateway.customBindHost - ? [`Gateway custom IP: ${quickstartGateway.customBindHost}`] - : []), - `Gateway auth: ${formatAuth(quickstartGateway.authMode)}`, - `Tailscale exposure: ${formatTailscale(quickstartGateway.tailscaleMode)}`, - "Direct to chat channels.", - ] + t("onboarding.gateway.keepSettings"), + `${t("onboarding.gateway.port")}: ${quickstartGateway.port}`, + `${t("onboarding.gateway.bind")}: ${formatBind(quickstartGateway.bind)}`, + ...(quickstartGateway.bind === "custom" && quickstartGateway.customBindHost + ? [`${t("onboarding.gateway.bindCustom")}: ${quickstartGateway.customBindHost}`] + : []), + `${t("onboarding.gateway.auth")}: ${formatAuth(quickstartGateway.authMode)}`, + `${t("onboarding.gateway.tailscale")}: ${formatTailscale(quickstartGateway.tailscaleMode)}`, + t("onboarding.gateway.chatChannels"), + ] : [ - `Gateway port: ${DEFAULT_GATEWAY_PORT}`, - "Gateway bind: Loopback (127.0.0.1)", - "Gateway auth: Token (default)", - "Tailscale exposure: Off", - "Direct to chat channels.", - ]; - await prompter.note(quickstartLines.join("\n"), "QuickStart"); + `${t("onboarding.gateway.port")}: ${DEFAULT_GATEWAY_PORT}`, + `${t("onboarding.gateway.bind")}: ${t("onboarding.gateway.bindLoopback")}`, + `${t("onboarding.gateway.auth")}: ${t("onboarding.gateway.authToken")}`, + `${t("onboarding.gateway.tailscale")}: ${t("onboarding.gateway.tsOff")}`, + t("onboarding.gateway.chatChannels"), + ]; + await prompter.note(quickstartLines.join("\n"), t("onboarding.flow.quickstart")); } const localPort = resolveGatewayPort(baseConfig); @@ -284,9 +264,9 @@ export async function runOnboardingWizard( const remoteUrl = baseConfig.gateway?.remote?.url?.trim() ?? ""; const remoteProbe = remoteUrl ? await probeGatewayReachable({ - url: remoteUrl, - token: baseConfig.gateway?.remote?.token, - }) + url: remoteUrl, + token: baseConfig.gateway?.remote?.token, + }) : null; const mode = @@ -294,33 +274,33 @@ export async function runOnboardingWizard( (flow === "quickstart" ? "local" : ((await prompter.select({ - message: "What do you want to set up?", - options: [ - { - value: "local", - label: "Local gateway (this machine)", - hint: localProbe.ok - ? `Gateway reachable (${localUrl})` - : `No gateway detected (${localUrl})`, - }, - { - value: "remote", - label: "Remote gateway (info-only)", - hint: !remoteUrl - ? "No remote URL configured yet" - : remoteProbe?.ok - ? `Gateway reachable (${remoteUrl})` - : `Configured but unreachable (${remoteUrl})`, - }, - ], - })) as OnboardMode)); + message: t("onboarding.setup.question"), + options: [ + { + value: "local", + label: t("onboarding.setup.local"), + hint: localProbe.ok + ? `${t("onboarding.setup.localOk")} (${localUrl})` + : `${t("onboarding.setup.localFail")} (${localUrl})`, + }, + { + value: "remote", + label: t("onboarding.setup.remote"), + hint: !remoteUrl + ? t("onboarding.setup.remoteNoUrl") + : remoteProbe?.ok + ? `${t("onboarding.setup.remoteOk")} (${remoteUrl})` + : `${t("onboarding.setup.remoteFail")} (${remoteUrl})`, + }, + ], + })) as OnboardMode)); if (mode === "remote") { let nextConfig = await promptRemoteGatewayConfig(baseConfig, prompter); nextConfig = applyWizardMetadata(nextConfig, { command: "onboard", mode }); await writeConfigFile(nextConfig); logConfigUpdated(runtime); - await prompter.outro("Remote gateway configured."); + await prompter.outro(t("onboarding.setup.remoteDone")); return; } @@ -329,9 +309,9 @@ export async function runOnboardingWizard( (flow === "quickstart" ? (baseConfig.agents?.defaults?.workspace ?? DEFAULT_WORKSPACE) : await prompter.text({ - message: "Workspace directory", - initialValue: baseConfig.agents?.defaults?.workspace ?? DEFAULT_WORKSPACE, - })); + message: t("onboarding.setup.workspaceDir"), + initialValue: baseConfig.agents?.defaults?.workspace ?? DEFAULT_WORKSPACE, + })); const workspaceDir = resolveUserPath(workspaceInput.trim() || DEFAULT_WORKSPACE); @@ -403,13 +383,13 @@ export async function runOnboardingWizard( const settings = gateway.settings; if (opts.skipChannels ?? opts.skipProviders) { - await prompter.note("Skipping channel setup.", "Channels"); + await prompter.note(t("onboarding.setup.skippingChannels"), t("onboarding.gateway.chatChannels")); } else { const quickstartAllowFromChannels = flow === "quickstart" ? listChannelPlugins() - .filter((plugin) => plugin.meta.quickstartAllowFrom) - .map((plugin) => plugin.id) + .filter((plugin) => plugin.meta.quickstartAllowFrom) + .map((plugin) => plugin.id) : []; nextConfig = await setupChannels(nextConfig, runtime, prompter, { allowSignalInstall: true, @@ -427,7 +407,7 @@ export async function runOnboardingWizard( }); if (opts.skipSkills) { - await prompter.note("Skipping skills setup.", "Skills"); + await prompter.note(t("onboarding.setup.skippingSkills"), t("onboarding.setup.skills")); } else { nextConfig = await setupSkills(nextConfig, workspaceDir, runtime, prompter); } diff --git a/ui/package-lock.json b/ui/package-lock.json new file mode 100644 index 000000000..230df7e09 --- /dev/null +++ b/ui/package-lock.json @@ -0,0 +1,1686 @@ +{ + "name": "openclaw-control-ui", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "openclaw-control-ui", + "dependencies": { + "@noble/ed25519": "3.0.0", + "dompurify": "^3.3.1", + "lit": "^3.3.2", + "marked": "^17.0.1", + "vite": "7.3.1" + }, + "devDependencies": { + "@vitest/browser-playwright": "4.0.18", + "playwright": "^1.58.0", + "typescript": "^5.9.3", + "vitest": "4.0.18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@lit-labs/ssr-dom-shim": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.5.1.tgz", + "integrity": "sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA==", + "license": "BSD-3-Clause" + }, + "node_modules/@lit/reactive-element": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.2.tgz", + "integrity": "sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.5.0" + } + }, + "node_modules/@noble/ed25519": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@noble/ed25519/-/ed25519-3.0.0.tgz", + "integrity": "sha512-QyteqMNm0GLqfa5SoYbSC3+Pvykwpn95Zgth4MFVSMKBB75ELl9tX1LAVsN4c3HXOrakHsF2gL4zWDAYCcsnzg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.0.tgz", + "integrity": "sha512-tPgXB6cDTndIe1ah7u6amCI1T0SsnlOuKgg10Xh3uizJk4e5M1JGaUMk7J4ciuAUcFpbOiNhm2XIjP9ON0dUqA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.0.tgz", + "integrity": "sha512-sa4LyseLLXr1onr97StkU1Nb7fWcg6niokTwEVNOO7awaKaoRObQ54+V/hrF/BP1noMEaaAW6Fg2d/CfLiq3Mg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.0.tgz", + "integrity": "sha512-/NNIj9A7yLjKdmkx5dC2XQ9DmjIECpGpwHoGmA5E1AhU0fuICSqSWScPhN1yLCkEdkCwJIDu2xIeLPs60MNIVg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.0.tgz", + "integrity": "sha512-xoh8abqgPrPYPr7pTYipqnUi1V3em56JzE/HgDgitTqZBZ3yKCWI+7KUkceM6tNweyUKYru1UMi7FC060RyKwA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.0.tgz", + "integrity": "sha512-PCkMh7fNahWSbA0OTUQ2OpYHpjZZr0hPr8lId8twD7a7SeWrvT3xJVyza+dQwXSSq4yEQTMoXgNOfMCsn8584g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.0.tgz", + "integrity": "sha512-1j3stGx+qbhXql4OCDZhnK7b01s6rBKNybfsX+TNrEe9JNq4DLi1yGiR1xW+nL+FNVvI4D02PUnl6gJ/2y6WJA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.0.tgz", + "integrity": "sha512-eyrr5W08Ms9uM0mLcKfM/Uzx7hjhz2bcjv8P2uynfj0yU8GGPdz8iYrBPhiLOZqahoAMB8ZiolRZPbbU2MAi6Q==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.0.tgz", + "integrity": "sha512-Xds90ITXJCNyX9pDhqf85MKWUI4lqjiPAipJ8OLp8xqI2Ehk+TCVhF9rvOoN8xTbcafow3QOThkNnrM33uCFQA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.0.tgz", + "integrity": "sha512-Xws2KA4CLvZmXjy46SQaXSejuKPhwVdaNinldoYfqruZBaJHqVo6hnRa8SDo9z7PBW5x84SH64+izmldCgbezw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.0.tgz", + "integrity": "sha512-hrKXKbX5FdaRJj7lTMusmvKbhMJSGWJ+w++4KmjiDhpTgNlhYobMvKfDoIWecy4O60K6yA4SnztGuNTQF+Lplw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.0.tgz", + "integrity": "sha512-6A+nccfSDGKsPm00d3xKcrsBcbqzCTAukjwWK6rbuAnB2bHaL3r9720HBVZ/no7+FhZLz/U3GwwZZEh6tOSI8Q==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.0.tgz", + "integrity": "sha512-4P1VyYUe6XAJtQH1Hh99THxr0GKMMwIXsRNOceLrJnaHTDgk1FTcTimDgneRJPvB3LqDQxUmroBclQ1S0cIJwQ==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.0.tgz", + "integrity": "sha512-8Vv6pLuIZCMcgXre6c3nOPhE0gjz1+nZP6T+hwWjr7sVH8k0jRkH+XnfjjOTglyMBdSKBPPz54/y1gToSKwrSQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.0.tgz", + "integrity": "sha512-r1te1M0Sm2TBVD/RxBPC6RZVwNqUTwJTA7w+C/IW5v9Ssu6xmxWEi+iJQlpBhtUiT1raJ5b48pI8tBvEjEFnFA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.0.tgz", + "integrity": "sha512-say0uMU/RaPm3CDQLxUUTF2oNWL8ysvHkAjcCzV2znxBr23kFfaxocS9qJm+NdkRhF8wtdEEAJuYcLPhSPbjuQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.0.tgz", + "integrity": "sha512-/MU7/HizQGsnBREtRpcSbSV1zfkoxSTR7wLsRmBPQ8FwUj5sykrP1MyJTvsxP5KBq9SyE6kH8UQQQwa0ASeoQQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.0.tgz", + "integrity": "sha512-Q9eh+gUGILIHEaJf66aF6a414jQbDnn29zeu0eX3dHMuysnhTvsUvZTCAyZ6tJhUjnvzBKE4FtuaYxutxRZpOg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.0.tgz", + "integrity": "sha512-OR5p5yG5OKSxHReWmwvM0P+VTPMwoBS45PXTMYaskKQqybkS3Kmugq1W+YbNWArF8/s7jQScgzXUhArzEQ7x0A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.0.tgz", + "integrity": "sha512-XeatKzo4lHDsVEbm1XDHZlhYZZSQYym6dg2X/Ko0kSFgio+KXLsxwJQprnR48GvdIKDOpqWqssC3iBCjoMcMpw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.0.tgz", + "integrity": "sha512-Lu71y78F5qOfYmubYLHPcJm74GZLU6UJ4THkf/a1K7Tz2ycwC2VUbsqbJAXaR6Bx70SRdlVrt2+n5l7F0agTUw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.0.tgz", + "integrity": "sha512-v5xwKDWcu7qhAEcsUubiav7r+48Uk/ENWdr82MBZZRIm7zThSxCIVDfb3ZeRRq9yqk+oIzMdDo6fCcA5DHfMyA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.0.tgz", + "integrity": "sha512-XnaaaSMGSI6Wk8F4KK3QP7GfuuhjGchElsVerCplUuxRIzdvZ7hRBpLR0omCmw+kI2RFJB80nenhOoGXlJ5TfQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.0.tgz", + "integrity": "sha512-3K1lP+3BXY4t4VihLw5MEg6IZD3ojSYzqzBG571W3kNQe4G4CcFpSUQVgurYgib5d+YaCjeFow8QivWp8vuSvA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.0.tgz", + "integrity": "sha512-MDk610P/vJGc5L5ImE4k5s+GZT3en0KoK1MKPXCRgzmksAMk79j4h3k1IerxTNqwDLxsGxStEZVBqG0gIqZqoA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.0.tgz", + "integrity": "sha512-Zv7v6q6aV+VslnpwzqKAmrk5JdVkLUzok2208ZXGipjb+msxBr/fJPZyeEXiFgH7k62Ak0SLIfxQRZQvTuf7rQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT" + }, + "node_modules/@vitest/browser": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.0.18.tgz", + "integrity": "sha512-gVQqh7paBz3gC+ZdcCmNSWJMk70IUjDeVqi+5m5vYpEHsIwRgw3Y545jljtajhkekIpIp5Gg8oK7bctgY0E2Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/mocker": "4.0.18", + "@vitest/utils": "4.0.18", + "magic-string": "^0.30.21", + "pixelmatch": "7.1.0", + "pngjs": "^7.0.0", + "sirv": "^3.0.2", + "tinyrainbow": "^3.0.3", + "ws": "^8.18.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "4.0.18" + } + }, + "node_modules/@vitest/browser-playwright": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.0.18.tgz", + "integrity": "sha512-gfajTHVCiwpxRj1qh0Sh/5bbGLG4F/ZH/V9xvFVoFddpITfMta9YGow0W6ZpTTORv2vdJuz9TnrNSmjKvpOf4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/browser": "4.0.18", + "@vitest/mocker": "4.0.18", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "playwright": "*", + "vitest": "4.0.18" + }, + "peerDependenciesMeta": { + "playwright": { + "optional": false + } + } + }, + "node_modules/@vitest/expect": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", + "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", + "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.18", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", + "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.18", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", + "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", + "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", + "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/dompurify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz", + "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/lit": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.2.tgz", + "integrity": "sha512-NF9zbsP79l4ao2SNrH3NkfmFgN/hBYSQo90saIVI1o5GpjAdCPVstVzO1MrLOakHoEhYkrtRjPK6Ob521aoYWQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit/reactive-element": "^2.1.0", + "lit-element": "^4.2.0", + "lit-html": "^3.3.0" + } + }, + "node_modules/lit-element": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.2.tgz", + "integrity": "sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.5.0", + "@lit/reactive-element": "^2.1.0", + "lit-html": "^3.3.0" + } + }, + "node_modules/lit-html": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.2.tgz", + "integrity": "sha512-Qy9hU88zcmaxBXcc10ZpdK7cOLXvXpRoBxERdtqV9QOrfpMZZ6pSYP91LhpPtap3sFMUiL7Tw2RImbe0Al2/kw==", + "license": "BSD-3-Clause", + "dependencies": { + "@types/trusted-types": "^2.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/marked": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/marked/-/marked-17.0.1.tgz", + "integrity": "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pixelmatch": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-7.1.0.tgz", + "integrity": "sha512-1wrVzJ2STrpmONHKBy228LM1b84msXDUoAzVEl0R8Mz4Ce6EPr+IVtxm8+yvrqLYMHswREkjYFaMxnyGnaY3Ng==", + "dev": true, + "license": "ISC", + "dependencies": { + "pngjs": "^7.0.0" + }, + "bin": { + "pixelmatch": "bin/pixelmatch" + } + }, + "node_modules/playwright": { + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.0.tgz", + "integrity": "sha512-2SVA0sbPktiIY/MCOPX8e86ehA/e+tDNq+e5Y8qjKYti2Z/JG7xnronT/TXTIkKbYGWlCbuucZ6dziEgkoEjQQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.0.tgz", + "integrity": "sha512-aaoB1RWrdNi3//rOeKuMiS65UCcgOVljU46At6eFcOFPFHWtd2weHRRow6z/n+Lec0Lvu0k9ZPKJSjPugikirw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/pngjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", + "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.19.0" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.0.tgz", + "integrity": "sha512-e5lPJi/aui4TO1LpAXIRLySmwXSE8k3b9zoGfd42p67wzxog4WHjiZF3M2uheQih4DGyc25QEV4yRBbpueNiUA==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.0", + "@rollup/rollup-android-arm64": "4.57.0", + "@rollup/rollup-darwin-arm64": "4.57.0", + "@rollup/rollup-darwin-x64": "4.57.0", + "@rollup/rollup-freebsd-arm64": "4.57.0", + "@rollup/rollup-freebsd-x64": "4.57.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.0", + "@rollup/rollup-linux-arm-musleabihf": "4.57.0", + "@rollup/rollup-linux-arm64-gnu": "4.57.0", + "@rollup/rollup-linux-arm64-musl": "4.57.0", + "@rollup/rollup-linux-loong64-gnu": "4.57.0", + "@rollup/rollup-linux-loong64-musl": "4.57.0", + "@rollup/rollup-linux-ppc64-gnu": "4.57.0", + "@rollup/rollup-linux-ppc64-musl": "4.57.0", + "@rollup/rollup-linux-riscv64-gnu": "4.57.0", + "@rollup/rollup-linux-riscv64-musl": "4.57.0", + "@rollup/rollup-linux-s390x-gnu": "4.57.0", + "@rollup/rollup-linux-x64-gnu": "4.57.0", + "@rollup/rollup-linux-x64-musl": "4.57.0", + "@rollup/rollup-openbsd-x64": "4.57.0", + "@rollup/rollup-openharmony-arm64": "4.57.0", + "@rollup/rollup-win32-arm64-msvc": "4.57.0", + "@rollup/rollup-win32-ia32-msvc": "4.57.0", + "@rollup/rollup-win32-x64-gnu": "4.57.0", + "@rollup/rollup-win32-x64-msvc": "4.57.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/vitest": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", + "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.18", + "@vitest/mocker": "4.0.18", + "@vitest/pretty-format": "4.0.18", + "@vitest/runner": "4.0.18", + "@vitest/snapshot": "4.0.18", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.18", + "@vitest/browser-preview": "4.0.18", + "@vitest/browser-webdriverio": "4.0.18", + "@vitest/ui": "4.0.18", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/ui/src/ui/app-render.ts b/ui/src/ui/app-render.ts index d692693cb..188d18a41 100644 --- a/ui/src/ui/app-render.ts +++ b/ui/src/ui/app-render.ts @@ -82,6 +82,7 @@ import { import { loadCronRuns, toggleCronJob, runCronJob, removeCronJob, addCronJob } from "./controllers/cron"; import { loadDebug, callDebugMethod } from "./controllers/debug"; import { loadLogs } from "./controllers/logs"; +import { t } from "./i18n"; const AVATAR_DATA_RE = /^data:/i; const AVATAR_HTTP_RE = /^https?:\/\//i; @@ -105,7 +106,7 @@ export function renderApp(state: AppViewState) { const presenceCount = state.presenceEntries.length; const sessionsCount = state.sessionsResult?.count ?? null; const cronNext = state.cronStatus?.nextWakeAtMs ?? null; - const chatDisabledReason = state.connected ? null : "Disconnected from gateway."; + const chatDisabledReason = state.connected ? null : t("common.disconnected"); const isChat = state.tab === "chat"; const chatFocus = isChat && (state.settings.chatFocusMode || state.onboarding); const showThinking = state.onboarding ? false : state.settings.chatShowThinking; @@ -119,12 +120,12 @@ export function renderApp(state: AppViewState) { @@ -133,39 +134,43 @@ export function renderApp(state: AppViewState) { OpenClaw
-
OPENCLAW
-
Gateway Dashboard
+
${t("app.title")}
+
${t("app.subtitle")}
- Health - ${state.connected ? "OK" : "Offline"} + ${t("app.health")} + ${state.connected ? t("app.healthOk") : t("app.healthOffline")}
${renderThemeToggle(state)}