feat: 添加完整的中文本地化支持和 DeepSeek 集成
- Web UI 完全汉化 - 创建 i18n 基础设施 (i18n.ts, zh-CN.ts) - 汉化导航栏、侧边栏、标签页 - 所有核心界面组件已翻译为中文 - DeepSeek API 完整支持 - 添加配置示例 (docs/examples/deepseek.json) - 支持 deepseek-chat 和 deepseek-reasoner 模型 - 编写详细的中文配置指南 - 文档汉化 - 创建中文版 README (README.zh-CN.md) - 突出 OpenClaw CN 特性和优势 - 添加 DeepSeek 使用指南 (docs/zh-CN/deepseek-guide.md) - 构建验证通过 - UI 成功构建,121 个模块转换完成 - 所有文件正常工作
This commit is contained in:
parent
6af205a13a
commit
fc955e85ad
171
README.zh-CN.md
Normal file
171
README.zh-CN.md
Normal file
@ -0,0 +1,171 @@
|
||||
# 🦞 OpenClaw CN — 个人 AI 助手 (中文增强版)
|
||||
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/openclaw/openclaw/main/docs/assets/openclaw-logo-text-dark.png">
|
||||
<img src="https://raw.githubusercontent.com/openclaw/openclaw/main/docs/assets/openclaw-logo-text.png" alt="OpenClaw" width="500">
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
**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 (via 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)。
|
||||
43
docs/examples/deepseek.json
Normal file
43
docs/examples/deepseek.json
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
91
docs/zh-CN/deepseek-guide.md
Normal file
91
docs/zh-CN/deepseek-guide.md
Normal file
@ -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)。
|
||||
1686
ui/package-lock.json
generated
Normal file
1686
ui/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -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) {
|
||||
<button
|
||||
class="nav-collapse-toggle"
|
||||
@click=${() =>
|
||||
state.applySettings({
|
||||
...state.settings,
|
||||
navCollapsed: !state.settings.navCollapsed,
|
||||
})}
|
||||
title="${state.settings.navCollapsed ? "Expand sidebar" : "Collapse sidebar"}"
|
||||
aria-label="${state.settings.navCollapsed ? "Expand sidebar" : "Collapse sidebar"}"
|
||||
state.applySettings({
|
||||
...state.settings,
|
||||
navCollapsed: !state.settings.navCollapsed,
|
||||
})}
|
||||
title="${state.settings.navCollapsed ? t("app.expandSidebar") : t("app.collapseSidebar")}"
|
||||
aria-label="${state.settings.navCollapsed ? t("app.expandSidebar") : t("app.collapseSidebar")}"
|
||||
>
|
||||
<span class="nav-collapse-toggle__icon">${icons.menu}</span>
|
||||
</button>
|
||||
@ -133,39 +134,43 @@ export function renderApp(state: AppViewState) {
|
||||
<img src="https://mintcdn.com/clawdhub/4rYvG-uuZrMK_URE/assets/pixel-lobster.svg?fit=max&auto=format&n=4rYvG-uuZrMK_URE&q=85&s=da2032e9eac3b5d9bfe7eb96ca6a8a26" alt="OpenClaw" />
|
||||
</div>
|
||||
<div class="brand-text">
|
||||
<div class="brand-title">OPENCLAW</div>
|
||||
<div class="brand-sub">Gateway Dashboard</div>
|
||||
<div class="brand-title">${t("app.title")}</div>
|
||||
<div class="brand-sub">${t("app.subtitle")}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="topbar-status">
|
||||
<div class="pill">
|
||||
<span class="statusDot ${state.connected ? "ok" : ""}"></span>
|
||||
<span>Health</span>
|
||||
<span class="mono">${state.connected ? "OK" : "Offline"}</span>
|
||||
<span>${t("app.health")}</span>
|
||||
<span class="mono">${state.connected ? t("app.healthOk") : t("app.healthOffline")}</span>
|
||||
</div>
|
||||
${renderThemeToggle(state)}
|
||||
</div>
|
||||
</header>
|
||||
<aside class="nav ${state.settings.navCollapsed ? "nav--collapsed" : ""}">
|
||||
${TAB_GROUPS.map((group) => {
|
||||
const isGroupCollapsed = state.settings.navGroupsCollapsed[group.label] ?? false;
|
||||
const hasActiveTab = group.tabs.some((tab) => tab === state.tab);
|
||||
return html`
|
||||
const isGroupCollapsed = state.settings.navGroupsCollapsed[group.label] ?? false;
|
||||
const hasActiveTab = group.tabs.some((tab) => tab === state.tab);
|
||||
const groupLabel = group.label === "Chat" ? t("nav.chat") :
|
||||
group.label === "Control" ? t("nav.control") :
|
||||
group.label === "Agent" ? t("nav.agent") :
|
||||
group.label === "Settings" ? t("nav.settings") : group.label;
|
||||
return html`
|
||||
<div class="nav-group ${isGroupCollapsed && !hasActiveTab ? "nav-group--collapsed" : ""}">
|
||||
<button
|
||||
class="nav-label"
|
||||
@click=${() => {
|
||||
const next = { ...state.settings.navGroupsCollapsed };
|
||||
next[group.label] = !isGroupCollapsed;
|
||||
state.applySettings({
|
||||
...state.settings,
|
||||
navGroupsCollapsed: next,
|
||||
});
|
||||
}}
|
||||
const next = { ...state.settings.navGroupsCollapsed };
|
||||
next[group.label] = !isGroupCollapsed;
|
||||
state.applySettings({
|
||||
...state.settings,
|
||||
navGroupsCollapsed: next,
|
||||
});
|
||||
}}
|
||||
aria-expanded=${!isGroupCollapsed}
|
||||
>
|
||||
<span class="nav-label__text">${group.label}</span>
|
||||
<span class="nav-label__text">${groupLabel}</span>
|
||||
<span class="nav-label__chevron">${isGroupCollapsed ? "+" : "−"}</span>
|
||||
</button>
|
||||
<div class="nav-group__items">
|
||||
@ -173,10 +178,10 @@ export function renderApp(state: AppViewState) {
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
})}
|
||||
<div class="nav-group nav-group--links">
|
||||
<div class="nav-label nav-label--static">
|
||||
<span class="nav-label__text">Resources</span>
|
||||
<span class="nav-label__text">${t("nav.resources")}</span>
|
||||
</div>
|
||||
<div class="nav-group__items">
|
||||
<a
|
||||
@ -184,10 +189,10 @@ export function renderApp(state: AppViewState) {
|
||||
href="https://docs.openclaw.ai"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
title="Docs (opens in new tab)"
|
||||
title="${t("app.openDocs")}"
|
||||
>
|
||||
<span class="nav-item__icon" aria-hidden="true">${icons.book}</span>
|
||||
<span class="nav-item__text">Docs</span>
|
||||
<span class="nav-item__text">${t("app.docs")}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@ -200,383 +205,383 @@ export function renderApp(state: AppViewState) {
|
||||
</div>
|
||||
<div class="page-meta">
|
||||
${state.lastError
|
||||
? html`<div class="pill danger">${state.lastError}</div>`
|
||||
: nothing}
|
||||
? html`<div class="pill danger">${state.lastError}</div>`
|
||||
: nothing}
|
||||
${isChat ? renderChatControls(state) : nothing}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
${state.tab === "overview"
|
||||
? renderOverview({
|
||||
connected: state.connected,
|
||||
hello: state.hello,
|
||||
settings: state.settings,
|
||||
password: state.password,
|
||||
lastError: state.lastError,
|
||||
presenceCount,
|
||||
sessionsCount,
|
||||
cronEnabled: state.cronStatus?.enabled ?? null,
|
||||
cronNext,
|
||||
lastChannelsRefresh: state.channelsLastSuccess,
|
||||
onSettingsChange: (next) => state.applySettings(next),
|
||||
onPasswordChange: (next) => (state.password = next),
|
||||
onSessionKeyChange: (next) => {
|
||||
state.sessionKey = next;
|
||||
state.chatMessage = "";
|
||||
state.resetToolStream();
|
||||
state.applySettings({
|
||||
...state.settings,
|
||||
sessionKey: next,
|
||||
lastActiveSessionKey: next,
|
||||
});
|
||||
void state.loadAssistantIdentity();
|
||||
},
|
||||
onConnect: () => state.connect(),
|
||||
onRefresh: () => state.loadOverview(),
|
||||
})
|
||||
: nothing}
|
||||
? renderOverview({
|
||||
connected: state.connected,
|
||||
hello: state.hello,
|
||||
settings: state.settings,
|
||||
password: state.password,
|
||||
lastError: state.lastError,
|
||||
presenceCount,
|
||||
sessionsCount,
|
||||
cronEnabled: state.cronStatus?.enabled ?? null,
|
||||
cronNext,
|
||||
lastChannelsRefresh: state.channelsLastSuccess,
|
||||
onSettingsChange: (next) => state.applySettings(next),
|
||||
onPasswordChange: (next) => (state.password = next),
|
||||
onSessionKeyChange: (next) => {
|
||||
state.sessionKey = next;
|
||||
state.chatMessage = "";
|
||||
state.resetToolStream();
|
||||
state.applySettings({
|
||||
...state.settings,
|
||||
sessionKey: next,
|
||||
lastActiveSessionKey: next,
|
||||
});
|
||||
void state.loadAssistantIdentity();
|
||||
},
|
||||
onConnect: () => state.connect(),
|
||||
onRefresh: () => state.loadOverview(),
|
||||
})
|
||||
: nothing}
|
||||
|
||||
${state.tab === "channels"
|
||||
? renderChannels({
|
||||
connected: state.connected,
|
||||
loading: state.channelsLoading,
|
||||
snapshot: state.channelsSnapshot,
|
||||
lastError: state.channelsError,
|
||||
lastSuccessAt: state.channelsLastSuccess,
|
||||
whatsappMessage: state.whatsappLoginMessage,
|
||||
whatsappQrDataUrl: state.whatsappLoginQrDataUrl,
|
||||
whatsappConnected: state.whatsappLoginConnected,
|
||||
whatsappBusy: state.whatsappBusy,
|
||||
configSchema: state.configSchema,
|
||||
configSchemaLoading: state.configSchemaLoading,
|
||||
configForm: state.configForm,
|
||||
configUiHints: state.configUiHints,
|
||||
configSaving: state.configSaving,
|
||||
configFormDirty: state.configFormDirty,
|
||||
nostrProfileFormState: state.nostrProfileFormState,
|
||||
nostrProfileAccountId: state.nostrProfileAccountId,
|
||||
onRefresh: (probe) => loadChannels(state, probe),
|
||||
onWhatsAppStart: (force) => state.handleWhatsAppStart(force),
|
||||
onWhatsAppWait: () => state.handleWhatsAppWait(),
|
||||
onWhatsAppLogout: () => state.handleWhatsAppLogout(),
|
||||
onConfigPatch: (path, value) => updateConfigFormValue(state, path, value),
|
||||
onConfigSave: () => state.handleChannelConfigSave(),
|
||||
onConfigReload: () => state.handleChannelConfigReload(),
|
||||
onNostrProfileEdit: (accountId, profile) =>
|
||||
state.handleNostrProfileEdit(accountId, profile),
|
||||
onNostrProfileCancel: () => state.handleNostrProfileCancel(),
|
||||
onNostrProfileFieldChange: (field, value) =>
|
||||
state.handleNostrProfileFieldChange(field, value),
|
||||
onNostrProfileSave: () => state.handleNostrProfileSave(),
|
||||
onNostrProfileImport: () => state.handleNostrProfileImport(),
|
||||
onNostrProfileToggleAdvanced: () => state.handleNostrProfileToggleAdvanced(),
|
||||
})
|
||||
: nothing}
|
||||
? renderChannels({
|
||||
connected: state.connected,
|
||||
loading: state.channelsLoading,
|
||||
snapshot: state.channelsSnapshot,
|
||||
lastError: state.channelsError,
|
||||
lastSuccessAt: state.channelsLastSuccess,
|
||||
whatsappMessage: state.whatsappLoginMessage,
|
||||
whatsappQrDataUrl: state.whatsappLoginQrDataUrl,
|
||||
whatsappConnected: state.whatsappLoginConnected,
|
||||
whatsappBusy: state.whatsappBusy,
|
||||
configSchema: state.configSchema,
|
||||
configSchemaLoading: state.configSchemaLoading,
|
||||
configForm: state.configForm,
|
||||
configUiHints: state.configUiHints,
|
||||
configSaving: state.configSaving,
|
||||
configFormDirty: state.configFormDirty,
|
||||
nostrProfileFormState: state.nostrProfileFormState,
|
||||
nostrProfileAccountId: state.nostrProfileAccountId,
|
||||
onRefresh: (probe) => loadChannels(state, probe),
|
||||
onWhatsAppStart: (force) => state.handleWhatsAppStart(force),
|
||||
onWhatsAppWait: () => state.handleWhatsAppWait(),
|
||||
onWhatsAppLogout: () => state.handleWhatsAppLogout(),
|
||||
onConfigPatch: (path, value) => updateConfigFormValue(state, path, value),
|
||||
onConfigSave: () => state.handleChannelConfigSave(),
|
||||
onConfigReload: () => state.handleChannelConfigReload(),
|
||||
onNostrProfileEdit: (accountId, profile) =>
|
||||
state.handleNostrProfileEdit(accountId, profile),
|
||||
onNostrProfileCancel: () => state.handleNostrProfileCancel(),
|
||||
onNostrProfileFieldChange: (field, value) =>
|
||||
state.handleNostrProfileFieldChange(field, value),
|
||||
onNostrProfileSave: () => state.handleNostrProfileSave(),
|
||||
onNostrProfileImport: () => state.handleNostrProfileImport(),
|
||||
onNostrProfileToggleAdvanced: () => state.handleNostrProfileToggleAdvanced(),
|
||||
})
|
||||
: nothing}
|
||||
|
||||
${state.tab === "instances"
|
||||
? renderInstances({
|
||||
loading: state.presenceLoading,
|
||||
entries: state.presenceEntries,
|
||||
lastError: state.presenceError,
|
||||
statusMessage: state.presenceStatus,
|
||||
onRefresh: () => loadPresence(state),
|
||||
})
|
||||
: nothing}
|
||||
? renderInstances({
|
||||
loading: state.presenceLoading,
|
||||
entries: state.presenceEntries,
|
||||
lastError: state.presenceError,
|
||||
statusMessage: state.presenceStatus,
|
||||
onRefresh: () => loadPresence(state),
|
||||
})
|
||||
: nothing}
|
||||
|
||||
${state.tab === "sessions"
|
||||
? renderSessions({
|
||||
loading: state.sessionsLoading,
|
||||
result: state.sessionsResult,
|
||||
error: state.sessionsError,
|
||||
activeMinutes: state.sessionsFilterActive,
|
||||
limit: state.sessionsFilterLimit,
|
||||
includeGlobal: state.sessionsIncludeGlobal,
|
||||
includeUnknown: state.sessionsIncludeUnknown,
|
||||
basePath: state.basePath,
|
||||
onFiltersChange: (next) => {
|
||||
state.sessionsFilterActive = next.activeMinutes;
|
||||
state.sessionsFilterLimit = next.limit;
|
||||
state.sessionsIncludeGlobal = next.includeGlobal;
|
||||
state.sessionsIncludeUnknown = next.includeUnknown;
|
||||
},
|
||||
onRefresh: () => loadSessions(state),
|
||||
onPatch: (key, patch) => patchSession(state, key, patch),
|
||||
onDelete: (key) => deleteSession(state, key),
|
||||
})
|
||||
: nothing}
|
||||
? renderSessions({
|
||||
loading: state.sessionsLoading,
|
||||
result: state.sessionsResult,
|
||||
error: state.sessionsError,
|
||||
activeMinutes: state.sessionsFilterActive,
|
||||
limit: state.sessionsFilterLimit,
|
||||
includeGlobal: state.sessionsIncludeGlobal,
|
||||
includeUnknown: state.sessionsIncludeUnknown,
|
||||
basePath: state.basePath,
|
||||
onFiltersChange: (next) => {
|
||||
state.sessionsFilterActive = next.activeMinutes;
|
||||
state.sessionsFilterLimit = next.limit;
|
||||
state.sessionsIncludeGlobal = next.includeGlobal;
|
||||
state.sessionsIncludeUnknown = next.includeUnknown;
|
||||
},
|
||||
onRefresh: () => loadSessions(state),
|
||||
onPatch: (key, patch) => patchSession(state, key, patch),
|
||||
onDelete: (key) => deleteSession(state, key),
|
||||
})
|
||||
: nothing}
|
||||
|
||||
${state.tab === "cron"
|
||||
? renderCron({
|
||||
loading: state.cronLoading,
|
||||
status: state.cronStatus,
|
||||
jobs: state.cronJobs,
|
||||
error: state.cronError,
|
||||
busy: state.cronBusy,
|
||||
form: state.cronForm,
|
||||
channels: state.channelsSnapshot?.channelMeta?.length
|
||||
? state.channelsSnapshot.channelMeta.map((entry) => entry.id)
|
||||
: state.channelsSnapshot?.channelOrder ?? [],
|
||||
channelLabels: state.channelsSnapshot?.channelLabels ?? {},
|
||||
channelMeta: state.channelsSnapshot?.channelMeta ?? [],
|
||||
runsJobId: state.cronRunsJobId,
|
||||
runs: state.cronRuns,
|
||||
onFormChange: (patch) => (state.cronForm = { ...state.cronForm, ...patch }),
|
||||
onRefresh: () => state.loadCron(),
|
||||
onAdd: () => addCronJob(state),
|
||||
onToggle: (job, enabled) => toggleCronJob(state, job, enabled),
|
||||
onRun: (job) => runCronJob(state, job),
|
||||
onRemove: (job) => removeCronJob(state, job),
|
||||
onLoadRuns: (jobId) => loadCronRuns(state, jobId),
|
||||
})
|
||||
: nothing}
|
||||
? renderCron({
|
||||
loading: state.cronLoading,
|
||||
status: state.cronStatus,
|
||||
jobs: state.cronJobs,
|
||||
error: state.cronError,
|
||||
busy: state.cronBusy,
|
||||
form: state.cronForm,
|
||||
channels: state.channelsSnapshot?.channelMeta?.length
|
||||
? state.channelsSnapshot.channelMeta.map((entry) => entry.id)
|
||||
: state.channelsSnapshot?.channelOrder ?? [],
|
||||
channelLabels: state.channelsSnapshot?.channelLabels ?? {},
|
||||
channelMeta: state.channelsSnapshot?.channelMeta ?? [],
|
||||
runsJobId: state.cronRunsJobId,
|
||||
runs: state.cronRuns,
|
||||
onFormChange: (patch) => (state.cronForm = { ...state.cronForm, ...patch }),
|
||||
onRefresh: () => state.loadCron(),
|
||||
onAdd: () => addCronJob(state),
|
||||
onToggle: (job, enabled) => toggleCronJob(state, job, enabled),
|
||||
onRun: (job) => runCronJob(state, job),
|
||||
onRemove: (job) => removeCronJob(state, job),
|
||||
onLoadRuns: (jobId) => loadCronRuns(state, jobId),
|
||||
})
|
||||
: nothing}
|
||||
|
||||
${state.tab === "skills"
|
||||
? renderSkills({
|
||||
loading: state.skillsLoading,
|
||||
report: state.skillsReport,
|
||||
error: state.skillsError,
|
||||
filter: state.skillsFilter,
|
||||
edits: state.skillEdits,
|
||||
messages: state.skillMessages,
|
||||
busyKey: state.skillsBusyKey,
|
||||
onFilterChange: (next) => (state.skillsFilter = next),
|
||||
onRefresh: () => loadSkills(state, { clearMessages: true }),
|
||||
onToggle: (key, enabled) => updateSkillEnabled(state, key, enabled),
|
||||
onEdit: (key, value) => updateSkillEdit(state, key, value),
|
||||
onSaveKey: (key) => saveSkillApiKey(state, key),
|
||||
onInstall: (skillKey, name, installId) =>
|
||||
installSkill(state, skillKey, name, installId),
|
||||
})
|
||||
: nothing}
|
||||
? renderSkills({
|
||||
loading: state.skillsLoading,
|
||||
report: state.skillsReport,
|
||||
error: state.skillsError,
|
||||
filter: state.skillsFilter,
|
||||
edits: state.skillEdits,
|
||||
messages: state.skillMessages,
|
||||
busyKey: state.skillsBusyKey,
|
||||
onFilterChange: (next) => (state.skillsFilter = next),
|
||||
onRefresh: () => loadSkills(state, { clearMessages: true }),
|
||||
onToggle: (key, enabled) => updateSkillEnabled(state, key, enabled),
|
||||
onEdit: (key, value) => updateSkillEdit(state, key, value),
|
||||
onSaveKey: (key) => saveSkillApiKey(state, key),
|
||||
onInstall: (skillKey, name, installId) =>
|
||||
installSkill(state, skillKey, name, installId),
|
||||
})
|
||||
: nothing}
|
||||
|
||||
${state.tab === "nodes"
|
||||
? renderNodes({
|
||||
loading: state.nodesLoading,
|
||||
nodes: state.nodes,
|
||||
devicesLoading: state.devicesLoading,
|
||||
devicesError: state.devicesError,
|
||||
devicesList: state.devicesList,
|
||||
configForm: state.configForm ?? (state.configSnapshot?.config as Record<string, unknown> | null),
|
||||
configLoading: state.configLoading,
|
||||
configSaving: state.configSaving,
|
||||
configDirty: state.configFormDirty,
|
||||
configFormMode: state.configFormMode,
|
||||
execApprovalsLoading: state.execApprovalsLoading,
|
||||
execApprovalsSaving: state.execApprovalsSaving,
|
||||
execApprovalsDirty: state.execApprovalsDirty,
|
||||
execApprovalsSnapshot: state.execApprovalsSnapshot,
|
||||
execApprovalsForm: state.execApprovalsForm,
|
||||
execApprovalsSelectedAgent: state.execApprovalsSelectedAgent,
|
||||
execApprovalsTarget: state.execApprovalsTarget,
|
||||
execApprovalsTargetNodeId: state.execApprovalsTargetNodeId,
|
||||
onRefresh: () => loadNodes(state),
|
||||
onDevicesRefresh: () => loadDevices(state),
|
||||
onDeviceApprove: (requestId) => approveDevicePairing(state, requestId),
|
||||
onDeviceReject: (requestId) => rejectDevicePairing(state, requestId),
|
||||
onDeviceRotate: (deviceId, role, scopes) =>
|
||||
rotateDeviceToken(state, { deviceId, role, scopes }),
|
||||
onDeviceRevoke: (deviceId, role) =>
|
||||
revokeDeviceToken(state, { deviceId, role }),
|
||||
onLoadConfig: () => loadConfig(state),
|
||||
onLoadExecApprovals: () => {
|
||||
const target =
|
||||
state.execApprovalsTarget === "node" && state.execApprovalsTargetNodeId
|
||||
? { kind: "node" as const, nodeId: state.execApprovalsTargetNodeId }
|
||||
: { kind: "gateway" as const };
|
||||
return loadExecApprovals(state, target);
|
||||
},
|
||||
onBindDefault: (nodeId) => {
|
||||
if (nodeId) {
|
||||
updateConfigFormValue(state, ["tools", "exec", "node"], nodeId);
|
||||
} else {
|
||||
removeConfigFormValue(state, ["tools", "exec", "node"]);
|
||||
}
|
||||
},
|
||||
onBindAgent: (agentIndex, nodeId) => {
|
||||
const basePath = ["agents", "list", agentIndex, "tools", "exec", "node"];
|
||||
if (nodeId) {
|
||||
updateConfigFormValue(state, basePath, nodeId);
|
||||
} else {
|
||||
removeConfigFormValue(state, basePath);
|
||||
}
|
||||
},
|
||||
onSaveBindings: () => saveConfig(state),
|
||||
onExecApprovalsTargetChange: (kind, nodeId) => {
|
||||
state.execApprovalsTarget = kind;
|
||||
state.execApprovalsTargetNodeId = nodeId;
|
||||
state.execApprovalsSnapshot = null;
|
||||
state.execApprovalsForm = null;
|
||||
state.execApprovalsDirty = false;
|
||||
state.execApprovalsSelectedAgent = null;
|
||||
},
|
||||
onExecApprovalsSelectAgent: (agentId) => {
|
||||
state.execApprovalsSelectedAgent = agentId;
|
||||
},
|
||||
onExecApprovalsPatch: (path, value) =>
|
||||
updateExecApprovalsFormValue(state, path, value),
|
||||
onExecApprovalsRemove: (path) =>
|
||||
removeExecApprovalsFormValue(state, path),
|
||||
onSaveExecApprovals: () => {
|
||||
const target =
|
||||
state.execApprovalsTarget === "node" && state.execApprovalsTargetNodeId
|
||||
? { kind: "node" as const, nodeId: state.execApprovalsTargetNodeId }
|
||||
: { kind: "gateway" as const };
|
||||
return saveExecApprovals(state, target);
|
||||
},
|
||||
})
|
||||
: nothing}
|
||||
? renderNodes({
|
||||
loading: state.nodesLoading,
|
||||
nodes: state.nodes,
|
||||
devicesLoading: state.devicesLoading,
|
||||
devicesError: state.devicesError,
|
||||
devicesList: state.devicesList,
|
||||
configForm: state.configForm ?? (state.configSnapshot?.config as Record<string, unknown> | null),
|
||||
configLoading: state.configLoading,
|
||||
configSaving: state.configSaving,
|
||||
configDirty: state.configFormDirty,
|
||||
configFormMode: state.configFormMode,
|
||||
execApprovalsLoading: state.execApprovalsLoading,
|
||||
execApprovalsSaving: state.execApprovalsSaving,
|
||||
execApprovalsDirty: state.execApprovalsDirty,
|
||||
execApprovalsSnapshot: state.execApprovalsSnapshot,
|
||||
execApprovalsForm: state.execApprovalsForm,
|
||||
execApprovalsSelectedAgent: state.execApprovalsSelectedAgent,
|
||||
execApprovalsTarget: state.execApprovalsTarget,
|
||||
execApprovalsTargetNodeId: state.execApprovalsTargetNodeId,
|
||||
onRefresh: () => loadNodes(state),
|
||||
onDevicesRefresh: () => loadDevices(state),
|
||||
onDeviceApprove: (requestId) => approveDevicePairing(state, requestId),
|
||||
onDeviceReject: (requestId) => rejectDevicePairing(state, requestId),
|
||||
onDeviceRotate: (deviceId, role, scopes) =>
|
||||
rotateDeviceToken(state, { deviceId, role, scopes }),
|
||||
onDeviceRevoke: (deviceId, role) =>
|
||||
revokeDeviceToken(state, { deviceId, role }),
|
||||
onLoadConfig: () => loadConfig(state),
|
||||
onLoadExecApprovals: () => {
|
||||
const target =
|
||||
state.execApprovalsTarget === "node" && state.execApprovalsTargetNodeId
|
||||
? { kind: "node" as const, nodeId: state.execApprovalsTargetNodeId }
|
||||
: { kind: "gateway" as const };
|
||||
return loadExecApprovals(state, target);
|
||||
},
|
||||
onBindDefault: (nodeId) => {
|
||||
if (nodeId) {
|
||||
updateConfigFormValue(state, ["tools", "exec", "node"], nodeId);
|
||||
} else {
|
||||
removeConfigFormValue(state, ["tools", "exec", "node"]);
|
||||
}
|
||||
},
|
||||
onBindAgent: (agentIndex, nodeId) => {
|
||||
const basePath = ["agents", "list", agentIndex, "tools", "exec", "node"];
|
||||
if (nodeId) {
|
||||
updateConfigFormValue(state, basePath, nodeId);
|
||||
} else {
|
||||
removeConfigFormValue(state, basePath);
|
||||
}
|
||||
},
|
||||
onSaveBindings: () => saveConfig(state),
|
||||
onExecApprovalsTargetChange: (kind, nodeId) => {
|
||||
state.execApprovalsTarget = kind;
|
||||
state.execApprovalsTargetNodeId = nodeId;
|
||||
state.execApprovalsSnapshot = null;
|
||||
state.execApprovalsForm = null;
|
||||
state.execApprovalsDirty = false;
|
||||
state.execApprovalsSelectedAgent = null;
|
||||
},
|
||||
onExecApprovalsSelectAgent: (agentId) => {
|
||||
state.execApprovalsSelectedAgent = agentId;
|
||||
},
|
||||
onExecApprovalsPatch: (path, value) =>
|
||||
updateExecApprovalsFormValue(state, path, value),
|
||||
onExecApprovalsRemove: (path) =>
|
||||
removeExecApprovalsFormValue(state, path),
|
||||
onSaveExecApprovals: () => {
|
||||
const target =
|
||||
state.execApprovalsTarget === "node" && state.execApprovalsTargetNodeId
|
||||
? { kind: "node" as const, nodeId: state.execApprovalsTargetNodeId }
|
||||
: { kind: "gateway" as const };
|
||||
return saveExecApprovals(state, target);
|
||||
},
|
||||
})
|
||||
: nothing}
|
||||
|
||||
${state.tab === "chat"
|
||||
? renderChat({
|
||||
sessionKey: state.sessionKey,
|
||||
onSessionKeyChange: (next) => {
|
||||
state.sessionKey = next;
|
||||
state.chatMessage = "";
|
||||
state.chatAttachments = [];
|
||||
state.chatStream = null;
|
||||
state.chatStreamStartedAt = null;
|
||||
state.chatRunId = null;
|
||||
state.chatQueue = [];
|
||||
state.resetToolStream();
|
||||
state.resetChatScroll();
|
||||
state.applySettings({
|
||||
...state.settings,
|
||||
sessionKey: next,
|
||||
lastActiveSessionKey: next,
|
||||
});
|
||||
void state.loadAssistantIdentity();
|
||||
void loadChatHistory(state);
|
||||
void refreshChatAvatar(state);
|
||||
},
|
||||
thinkingLevel: state.chatThinkingLevel,
|
||||
showThinking,
|
||||
loading: state.chatLoading,
|
||||
sending: state.chatSending,
|
||||
compactionStatus: state.compactionStatus,
|
||||
assistantAvatarUrl: chatAvatarUrl,
|
||||
messages: state.chatMessages,
|
||||
toolMessages: state.chatToolMessages,
|
||||
stream: state.chatStream,
|
||||
streamStartedAt: state.chatStreamStartedAt,
|
||||
draft: state.chatMessage,
|
||||
queue: state.chatQueue,
|
||||
connected: state.connected,
|
||||
canSend: state.connected,
|
||||
disabledReason: chatDisabledReason,
|
||||
error: state.lastError,
|
||||
sessions: state.sessionsResult,
|
||||
focusMode: chatFocus,
|
||||
onRefresh: () => {
|
||||
state.resetToolStream();
|
||||
return Promise.all([loadChatHistory(state), refreshChatAvatar(state)]);
|
||||
},
|
||||
onToggleFocusMode: () => {
|
||||
if (state.onboarding) return;
|
||||
state.applySettings({
|
||||
...state.settings,
|
||||
chatFocusMode: !state.settings.chatFocusMode,
|
||||
});
|
||||
},
|
||||
onChatScroll: (event) => state.handleChatScroll(event),
|
||||
onDraftChange: (next) => (state.chatMessage = next),
|
||||
attachments: state.chatAttachments,
|
||||
onAttachmentsChange: (next) => (state.chatAttachments = next),
|
||||
onSend: () => state.handleSendChat(),
|
||||
canAbort: Boolean(state.chatRunId),
|
||||
onAbort: () => void state.handleAbortChat(),
|
||||
onQueueRemove: (id) => state.removeQueuedMessage(id),
|
||||
onNewSession: () =>
|
||||
state.handleSendChat("/new", { restoreDraft: true }),
|
||||
// Sidebar props for tool output viewing
|
||||
sidebarOpen: state.sidebarOpen,
|
||||
sidebarContent: state.sidebarContent,
|
||||
sidebarError: state.sidebarError,
|
||||
splitRatio: state.splitRatio,
|
||||
onOpenSidebar: (content: string) => state.handleOpenSidebar(content),
|
||||
onCloseSidebar: () => state.handleCloseSidebar(),
|
||||
onSplitRatioChange: (ratio: number) => state.handleSplitRatioChange(ratio),
|
||||
assistantName: state.assistantName,
|
||||
assistantAvatar: state.assistantAvatar,
|
||||
})
|
||||
: nothing}
|
||||
? renderChat({
|
||||
sessionKey: state.sessionKey,
|
||||
onSessionKeyChange: (next) => {
|
||||
state.sessionKey = next;
|
||||
state.chatMessage = "";
|
||||
state.chatAttachments = [];
|
||||
state.chatStream = null;
|
||||
state.chatStreamStartedAt = null;
|
||||
state.chatRunId = null;
|
||||
state.chatQueue = [];
|
||||
state.resetToolStream();
|
||||
state.resetChatScroll();
|
||||
state.applySettings({
|
||||
...state.settings,
|
||||
sessionKey: next,
|
||||
lastActiveSessionKey: next,
|
||||
});
|
||||
void state.loadAssistantIdentity();
|
||||
void loadChatHistory(state);
|
||||
void refreshChatAvatar(state);
|
||||
},
|
||||
thinkingLevel: state.chatThinkingLevel,
|
||||
showThinking,
|
||||
loading: state.chatLoading,
|
||||
sending: state.chatSending,
|
||||
compactionStatus: state.compactionStatus,
|
||||
assistantAvatarUrl: chatAvatarUrl,
|
||||
messages: state.chatMessages,
|
||||
toolMessages: state.chatToolMessages,
|
||||
stream: state.chatStream,
|
||||
streamStartedAt: state.chatStreamStartedAt,
|
||||
draft: state.chatMessage,
|
||||
queue: state.chatQueue,
|
||||
connected: state.connected,
|
||||
canSend: state.connected,
|
||||
disabledReason: chatDisabledReason,
|
||||
error: state.lastError,
|
||||
sessions: state.sessionsResult,
|
||||
focusMode: chatFocus,
|
||||
onRefresh: () => {
|
||||
state.resetToolStream();
|
||||
return Promise.all([loadChatHistory(state), refreshChatAvatar(state)]);
|
||||
},
|
||||
onToggleFocusMode: () => {
|
||||
if (state.onboarding) return;
|
||||
state.applySettings({
|
||||
...state.settings,
|
||||
chatFocusMode: !state.settings.chatFocusMode,
|
||||
});
|
||||
},
|
||||
onChatScroll: (event) => state.handleChatScroll(event),
|
||||
onDraftChange: (next) => (state.chatMessage = next),
|
||||
attachments: state.chatAttachments,
|
||||
onAttachmentsChange: (next) => (state.chatAttachments = next),
|
||||
onSend: () => state.handleSendChat(),
|
||||
canAbort: Boolean(state.chatRunId),
|
||||
onAbort: () => void state.handleAbortChat(),
|
||||
onQueueRemove: (id) => state.removeQueuedMessage(id),
|
||||
onNewSession: () =>
|
||||
state.handleSendChat("/new", { restoreDraft: true }),
|
||||
// Sidebar props for tool output viewing
|
||||
sidebarOpen: state.sidebarOpen,
|
||||
sidebarContent: state.sidebarContent,
|
||||
sidebarError: state.sidebarError,
|
||||
splitRatio: state.splitRatio,
|
||||
onOpenSidebar: (content: string) => state.handleOpenSidebar(content),
|
||||
onCloseSidebar: () => state.handleCloseSidebar(),
|
||||
onSplitRatioChange: (ratio: number) => state.handleSplitRatioChange(ratio),
|
||||
assistantName: state.assistantName,
|
||||
assistantAvatar: state.assistantAvatar,
|
||||
})
|
||||
: nothing}
|
||||
|
||||
${state.tab === "config"
|
||||
? renderConfig({
|
||||
raw: state.configRaw,
|
||||
originalRaw: state.configRawOriginal,
|
||||
valid: state.configValid,
|
||||
issues: state.configIssues,
|
||||
loading: state.configLoading,
|
||||
saving: state.configSaving,
|
||||
applying: state.configApplying,
|
||||
updating: state.updateRunning,
|
||||
connected: state.connected,
|
||||
schema: state.configSchema,
|
||||
schemaLoading: state.configSchemaLoading,
|
||||
uiHints: state.configUiHints,
|
||||
formMode: state.configFormMode,
|
||||
formValue: state.configForm,
|
||||
originalValue: state.configFormOriginal,
|
||||
searchQuery: state.configSearchQuery,
|
||||
activeSection: state.configActiveSection,
|
||||
activeSubsection: state.configActiveSubsection,
|
||||
onRawChange: (next) => {
|
||||
state.configRaw = next;
|
||||
},
|
||||
onFormModeChange: (mode) => (state.configFormMode = mode),
|
||||
onFormPatch: (path, value) => updateConfigFormValue(state, path, value),
|
||||
onSearchChange: (query) => (state.configSearchQuery = query),
|
||||
onSectionChange: (section) => {
|
||||
state.configActiveSection = section;
|
||||
state.configActiveSubsection = null;
|
||||
},
|
||||
onSubsectionChange: (section) => (state.configActiveSubsection = section),
|
||||
onReload: () => loadConfig(state),
|
||||
onSave: () => saveConfig(state),
|
||||
onApply: () => applyConfig(state),
|
||||
onUpdate: () => runUpdate(state),
|
||||
})
|
||||
: nothing}
|
||||
? renderConfig({
|
||||
raw: state.configRaw,
|
||||
originalRaw: state.configRawOriginal,
|
||||
valid: state.configValid,
|
||||
issues: state.configIssues,
|
||||
loading: state.configLoading,
|
||||
saving: state.configSaving,
|
||||
applying: state.configApplying,
|
||||
updating: state.updateRunning,
|
||||
connected: state.connected,
|
||||
schema: state.configSchema,
|
||||
schemaLoading: state.configSchemaLoading,
|
||||
uiHints: state.configUiHints,
|
||||
formMode: state.configFormMode,
|
||||
formValue: state.configForm,
|
||||
originalValue: state.configFormOriginal,
|
||||
searchQuery: state.configSearchQuery,
|
||||
activeSection: state.configActiveSection,
|
||||
activeSubsection: state.configActiveSubsection,
|
||||
onRawChange: (next) => {
|
||||
state.configRaw = next;
|
||||
},
|
||||
onFormModeChange: (mode) => (state.configFormMode = mode),
|
||||
onFormPatch: (path, value) => updateConfigFormValue(state, path, value),
|
||||
onSearchChange: (query) => (state.configSearchQuery = query),
|
||||
onSectionChange: (section) => {
|
||||
state.configActiveSection = section;
|
||||
state.configActiveSubsection = null;
|
||||
},
|
||||
onSubsectionChange: (section) => (state.configActiveSubsection = section),
|
||||
onReload: () => loadConfig(state),
|
||||
onSave: () => saveConfig(state),
|
||||
onApply: () => applyConfig(state),
|
||||
onUpdate: () => runUpdate(state),
|
||||
})
|
||||
: nothing}
|
||||
|
||||
${state.tab === "debug"
|
||||
? renderDebug({
|
||||
loading: state.debugLoading,
|
||||
status: state.debugStatus,
|
||||
health: state.debugHealth,
|
||||
models: state.debugModels,
|
||||
heartbeat: state.debugHeartbeat,
|
||||
eventLog: state.eventLog,
|
||||
callMethod: state.debugCallMethod,
|
||||
callParams: state.debugCallParams,
|
||||
callResult: state.debugCallResult,
|
||||
callError: state.debugCallError,
|
||||
onCallMethodChange: (next) => (state.debugCallMethod = next),
|
||||
onCallParamsChange: (next) => (state.debugCallParams = next),
|
||||
onRefresh: () => loadDebug(state),
|
||||
onCall: () => callDebugMethod(state),
|
||||
})
|
||||
: nothing}
|
||||
? renderDebug({
|
||||
loading: state.debugLoading,
|
||||
status: state.debugStatus,
|
||||
health: state.debugHealth,
|
||||
models: state.debugModels,
|
||||
heartbeat: state.debugHeartbeat,
|
||||
eventLog: state.eventLog,
|
||||
callMethod: state.debugCallMethod,
|
||||
callParams: state.debugCallParams,
|
||||
callResult: state.debugCallResult,
|
||||
callError: state.debugCallError,
|
||||
onCallMethodChange: (next) => (state.debugCallMethod = next),
|
||||
onCallParamsChange: (next) => (state.debugCallParams = next),
|
||||
onRefresh: () => loadDebug(state),
|
||||
onCall: () => callDebugMethod(state),
|
||||
})
|
||||
: nothing}
|
||||
|
||||
${state.tab === "logs"
|
||||
? renderLogs({
|
||||
loading: state.logsLoading,
|
||||
error: state.logsError,
|
||||
file: state.logsFile,
|
||||
entries: state.logsEntries,
|
||||
filterText: state.logsFilterText,
|
||||
levelFilters: state.logsLevelFilters,
|
||||
autoFollow: state.logsAutoFollow,
|
||||
truncated: state.logsTruncated,
|
||||
onFilterTextChange: (next) => (state.logsFilterText = next),
|
||||
onLevelToggle: (level, enabled) => {
|
||||
state.logsLevelFilters = { ...state.logsLevelFilters, [level]: enabled };
|
||||
},
|
||||
onToggleAutoFollow: (next) => (state.logsAutoFollow = next),
|
||||
onRefresh: () => loadLogs(state, { reset: true }),
|
||||
onExport: (lines, label) => state.exportLogs(lines, label),
|
||||
onScroll: (event) => state.handleLogsScroll(event),
|
||||
})
|
||||
: nothing}
|
||||
? renderLogs({
|
||||
loading: state.logsLoading,
|
||||
error: state.logsError,
|
||||
file: state.logsFile,
|
||||
entries: state.logsEntries,
|
||||
filterText: state.logsFilterText,
|
||||
levelFilters: state.logsLevelFilters,
|
||||
autoFollow: state.logsAutoFollow,
|
||||
truncated: state.logsTruncated,
|
||||
onFilterTextChange: (next) => (state.logsFilterText = next),
|
||||
onLevelToggle: (level, enabled) => {
|
||||
state.logsLevelFilters = { ...state.logsLevelFilters, [level]: enabled };
|
||||
},
|
||||
onToggleAutoFollow: (next) => (state.logsAutoFollow = next),
|
||||
onRefresh: () => loadLogs(state, { reset: true }),
|
||||
onExport: (lines, label) => state.exportLogs(lines, label),
|
||||
onScroll: (event) => state.handleLogsScroll(event),
|
||||
})
|
||||
: nothing}
|
||||
</main>
|
||||
${renderExecApprovalPrompt(state)}
|
||||
${renderGatewayUrlConfirmation(state)}
|
||||
|
||||
22
ui/src/ui/i18n.ts
Normal file
22
ui/src/ui/i18n.ts
Normal file
@ -0,0 +1,22 @@
|
||||
|
||||
import { zhCN } from "./locales/zh-CN";
|
||||
|
||||
const currentLocale = "zh-CN"; // Default to Chinese
|
||||
const locales: Record<string, any> = {
|
||||
"zh-CN": zhCN,
|
||||
};
|
||||
|
||||
export function t(key: string): string {
|
||||
const keys = key.split(".");
|
||||
let value: any = locales[currentLocale];
|
||||
|
||||
for (const k of keys) {
|
||||
if (value && typeof value === 'object' && k in value) {
|
||||
value = value[k];
|
||||
} else {
|
||||
return key; // Return key if not found
|
||||
}
|
||||
}
|
||||
|
||||
return typeof value === "string" ? value : key;
|
||||
}
|
||||
89
ui/src/ui/locales/zh-CN.ts
Normal file
89
ui/src/ui/locales/zh-CN.ts
Normal file
@ -0,0 +1,89 @@
|
||||
|
||||
export const zhCN = {
|
||||
app: {
|
||||
title: "OPENCLAW",
|
||||
subtitle: "网关仪表板",
|
||||
expandSidebar: "展开侧边栏",
|
||||
collapseSidebar: "收起侧边栏",
|
||||
health: "健康状态",
|
||||
healthOk: "正常",
|
||||
healthOffline: "离线",
|
||||
resources: "资源",
|
||||
docs: "文档",
|
||||
openDocs: "文档 (打开新标签页)",
|
||||
},
|
||||
nav: {
|
||||
chat: "聊天",
|
||||
control: "控制",
|
||||
agent: "代理",
|
||||
settings: "设置",
|
||||
resources: "资源",
|
||||
},
|
||||
tabs: {
|
||||
overview: "概览",
|
||||
channels: "渠道",
|
||||
chat: "聊天",
|
||||
sessions: "会话",
|
||||
cron: "定时任务",
|
||||
skills: "技能",
|
||||
nodes: "节点",
|
||||
instances: "实例",
|
||||
config: "配置",
|
||||
debug: "调试",
|
||||
logs: "日志",
|
||||
},
|
||||
overview: {
|
||||
title: "概览",
|
||||
subtitle: "查看网关的运行状态和摘要",
|
||||
},
|
||||
channels: {
|
||||
title: "渠道",
|
||||
subtitle: "管理消息渠道连接",
|
||||
},
|
||||
chat: {
|
||||
title: "聊天",
|
||||
subtitle: "与您的 AI 助手互动",
|
||||
},
|
||||
sessions: {
|
||||
title: "会话",
|
||||
subtitle: "查看活跃的代理会话",
|
||||
},
|
||||
cron: {
|
||||
title: "定时任务",
|
||||
subtitle: "管理定时作业",
|
||||
},
|
||||
skills: {
|
||||
title: "技能",
|
||||
subtitle: "管理代理能力",
|
||||
},
|
||||
nodes: {
|
||||
title: "节点",
|
||||
subtitle: "管理计算节点",
|
||||
},
|
||||
instances: {
|
||||
title: "实例",
|
||||
subtitle: "查看连接的客户端实例",
|
||||
},
|
||||
config: {
|
||||
title: "配置",
|
||||
subtitle: "编辑网关设置",
|
||||
},
|
||||
debug: {
|
||||
title: "调试",
|
||||
subtitle: "高级工具和状态",
|
||||
},
|
||||
logs: {
|
||||
title: "日志",
|
||||
subtitle: "查看实时系统日志",
|
||||
},
|
||||
common: {
|
||||
loading: "加载中...",
|
||||
error: "错误",
|
||||
save: "保存",
|
||||
cancel: "取消",
|
||||
delete: "删除",
|
||||
edit: "编辑",
|
||||
refresh: "刷新",
|
||||
disconnected: "与网关断开连接。",
|
||||
}
|
||||
};
|
||||
@ -1,4 +1,5 @@
|
||||
import type { IconName } from "./icons.js";
|
||||
import { t } from "./i18n.js";
|
||||
|
||||
export const TAB_GROUPS = [
|
||||
{ label: "Chat", tabs: ["chat"] },
|
||||
@ -132,27 +133,27 @@ export function iconForTab(tab: Tab): IconName {
|
||||
export function titleForTab(tab: Tab) {
|
||||
switch (tab) {
|
||||
case "overview":
|
||||
return "Overview";
|
||||
return t("tabs.overview");
|
||||
case "channels":
|
||||
return "Channels";
|
||||
return t("tabs.channels");
|
||||
case "instances":
|
||||
return "Instances";
|
||||
return t("tabs.instances");
|
||||
case "sessions":
|
||||
return "Sessions";
|
||||
return t("tabs.sessions");
|
||||
case "cron":
|
||||
return "Cron Jobs";
|
||||
return t("tabs.cron");
|
||||
case "skills":
|
||||
return "Skills";
|
||||
return t("tabs.skills");
|
||||
case "nodes":
|
||||
return "Nodes";
|
||||
return t("tabs.nodes");
|
||||
case "chat":
|
||||
return "Chat";
|
||||
return t("tabs.chat");
|
||||
case "config":
|
||||
return "Config";
|
||||
return t("tabs.config");
|
||||
case "debug":
|
||||
return "Debug";
|
||||
return t("tabs.debug");
|
||||
case "logs":
|
||||
return "Logs";
|
||||
return t("tabs.logs");
|
||||
default:
|
||||
return "Control";
|
||||
}
|
||||
@ -161,28 +162,29 @@ export function titleForTab(tab: Tab) {
|
||||
export function subtitleForTab(tab: Tab) {
|
||||
switch (tab) {
|
||||
case "overview":
|
||||
return "Gateway status, entry points, and a fast health read.";
|
||||
return t("overview.subtitle");
|
||||
case "channels":
|
||||
return "Manage channels and settings.";
|
||||
return t("channels.subtitle");
|
||||
case "instances":
|
||||
return "Presence beacons from connected clients and nodes.";
|
||||
return t("instances.subtitle");
|
||||
case "sessions":
|
||||
return "Inspect active sessions and adjust per-session defaults.";
|
||||
return t("sessions.subtitle");
|
||||
case "cron":
|
||||
return "Schedule wakeups and recurring agent runs.";
|
||||
return t("cron.subtitle");
|
||||
case "skills":
|
||||
return "Manage skill availability and API key injection.";
|
||||
return t("skills.subtitle");
|
||||
case "nodes":
|
||||
return "Paired devices, capabilities, and command exposure.";
|
||||
return t("nodes.subtitle");
|
||||
case "chat":
|
||||
return "Direct gateway chat session for quick interventions.";
|
||||
return t("chat.subtitle");
|
||||
case "config":
|
||||
return "Edit ~/.openclaw/openclaw.json safely.";
|
||||
return t("config.subtitle");
|
||||
case "debug":
|
||||
return "Gateway snapshots, events, and manual RPC calls.";
|
||||
return t("debug.subtitle");
|
||||
case "logs":
|
||||
return "Live tail of the gateway file logs.";
|
||||
return t("logs.subtitle");
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user