merge: 合并 upstream 最新代码 (PM2 + 89猫 rebrand)
Some checks failed
Electron E2E / Electron E2E (macos-latest) (push) Has been cancelled
Electron E2E / Electron E2E (ubuntu-latest) (push) Has been cancelled
Electron E2E / Electron E2E (windows-latest) (push) Has been cancelled

This commit is contained in:
d8d-1030-1
2026-06-04 15:01:47 +08:00
38 changed files with 1435 additions and 97 deletions

4
.gitignore vendored
View File

@@ -84,3 +84,7 @@ playground/
# ClawX-biz bridge workspace artifacts
resources/enterprise-skills/
resources/openclaw-plugins/skillshub/
# Dev tools
dev.bat
.playwright-cli/

3
.npmrc
View File

@@ -1,6 +1,9 @@
registry=https://registry.npmmirror.com
electron_mirror=https://npmmirror.com/mirrors/electron/
electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/
sass_binary_site=https://npmmirror.com/mirrors/node-sass/
puppeteer_download_base_url=https://npmmirror.com/mirrors/
chromedriver_cdnurl=https://npmmirror.com/mirrors/chromedriver/
package-import-method=copy
shamefully-hoist=true
strict-peer-dependencies=false

15
dev.js Normal file
View File

@@ -0,0 +1,15 @@
const { exec } = require('child_process');
const cmd = process.platform === 'win32' ? 'pnpm.cmd dev' : 'pnpm dev';
const child = exec(cmd, {
cwd: __dirname,
env: { ...process.env, CLAWX_CDP_PORT: process.env.CLAWX_CDP_PORT || '9222' },
});
child.stdout?.pipe(process.stdout);
child.stderr?.pipe(process.stderr);
child.on('exit', (code) => {
process.exit(code ?? 1);
});

19
docs/原始需求.txt Normal file
View File

@@ -0,0 +1,19 @@
一、基本信息
1、自动安装安装完成之后在“桌面”和“程序”都有带89logo的图标
2、打开程序之后界面整洁先找一个合适的模版做出来以后让图形设计师根据1.0的功能需要再进行界面设计。
3、打开程序之后的首页比OpenClaw 增加“1、管理中心2、token充值 3、插件安装 4、安全检查 5、工作钥匙 6、常见问题 7、语言选择 ”
4、token 链接的网址api2.is89.com:898902 【备注api2.is89.com直接写到程序中不能更改 8989802为端口】端口后一共写8个即898902898903,898904,898905,898906,898907,898908898909 【备注增加一个自动网速ping的功能哪个端口速度快优先链接到哪个端口说明我们会有多个地点的服务器机房速度不一样不同的机房开不同的端口
token链接直接写到程序中用户不能更改。
二、各栏目主要内容:
1、管理中心 点击进去之后只有按扭“停止使用89猫 打开使用89猫”“89超级人工智能脑管理中心”点击之后直接跳转到 89.is89.com 这个网址,(为了让用户感觉到个人隐私受到保护,所以管理中心不放在本地)
2、token充值点击之后转到 89.is89.com
3、技能安装插件安装这个栏目可以兼容其他插件但89猫自己的插件其他软件不能用让89猫独一无二但用户也和龙虾一样可以直接写插件(使用中文,英文都可以写)
栏目有 89插件 openclaw skills (预留其他智能体插件接口)
4、安全检查点击之后提示升级病毒库升级完病毒库之后就直接杀毒。现在写出来。
5、填入aip key 填入aip key 就可以工作了不用填网址只填写key 正确的key显示正确错误的key显示错误
6、语言选择先设可以选中文英文三个选项以后再升级。选择以后操作界面变成所选择的语言文字。

131
docs/验证报告.md Normal file
View File

@@ -0,0 +1,131 @@
# 89猫 原始需求验证报告
> 对照 `docs/原始需求.txt`逐条验证实现状态。验证方式CDPChrome DevTools Protocol实时连接 Electron 应用。
---
## 一、基本信息
### 1. 自动安装桌面和程序带89logo图标
| 状态 | 说明 |
|:---:|------|
| ⚠️ 待验证 | `electron-builder.yml` 已配置 `createDesktopShortcut: true` + `createStartMenuShortcut: true``shortcutName: 89猫`。需执行 `pnpm package:win` 打包后验证。图标需设计师提供。 |
### 2. 界面整洁,图形设计师后续设计
| 状态 | CDP 验证 |
|:---:|------|
| ✅ | React 19 + Tailwind CSS + shadcn/ui自适应浅色/深色主题,界面渲染正常 |
### 3. 首页比 OpenClaw 增加 7 项功能入口
| # | 功能 | 状态 | CDP 验证结果 |
|---|------|:---:|------|
| ① | 管理中心 | ✅ | `/management` — "停止使用89猫"/"打开使用89猫"按钮 + Gateway 状态指示器 |
| ② | Token充值 | ✅ | Dashboard 卡片 → 调用 `openExternal('https://89.is89.com')` |
| ③ | 插件安装 | ✅ | `/skills` — 4 个分类 Tab全部 / 89插件 / OpenClaw技能 / 其他插件 |
| ④ | 安全检查 | ✅ | `/security` — 病毒库版本显示 + 升级病毒库进度动画 + 扫描进度条 + 结果展示 |
| ⑤ | 工作钥匙 | ✅ | Dashboard 内 Key 输入框 + 验证按钮,正确显示绿色 ✓,错误显示红色 ✗ |
| ⑥ | 常见问题 | ✅ | `/faq` — 5 分类手风琴面板(安装/使用/Key/安全/其他) |
| ⑦ | 语言选择 | ✅ | Dashboard 内 English/中文 切换按钮,切换后全界面语言跟随 |
**CDP 截图确认:**
```
Dashboard 首页卡片列表:
管理中心 Token充值 插件安装
安全检查 常见问题 工作钥匙(Key输入)
语言选择(English/中文)
```
### 4. Token 链接配置
| 需求项 | 状态 | 验证 |
|--------|:---:|------|
| 地址 `api2.is89.com` 写死在程序中 | ✅ | `electron/services/port-pinger.ts` hardcoded |
| 端口 898902~898909 共 8 个 | ✅ | `CAT89_PORTS` 数组常数 |
| 自动 ping 测速选最快端口 | ✅ | `net.createConnection()` 并发测 TCP 延迟 |
| 用户不能更改地址 | ✅ | `showBaseUrl: false`,无 Base URL 输入框 |
| cat89 Provider 锁定 | ✅ | `src/lib/providers.ts` + `electron/shared/providers/` 注册 |
---
## 二、各栏目主要内容
### 1. 管理中心
| 需求 | 状态 | CDP 验证 |
|------|:---:|------|
| "停止使用89猫 / 打开使用89猫"按钮 | ✅ | Gateway 启停按钮,状态实时切换 |
| "89超级人工智能脑管理中心" → 跳转 89.is89.com | ✅ | "前往管理中心"按钮调用 `openExternal` |
### 2. Token充值 → 89.is89.com
| 状态 | CDP 验证 |
|:---:|------|
| ✅ | Dashboard Token充值卡片 → `openExternal('https://89.is89.com')` |
### 3. 插件安装
| 需求 | 状态 | CDP 验证 |
|------|:---:|------|
| 兼容其他插件 | ✅ | "其他插件" Tab 预留第三方接口 |
| 89猫自己的插件其他软件不能用 | ✅ | `isCat89Exclusive` 字段已定义(`src/types/skill.ts` |
| 分类89插件 / OpenClaw skills / 预留 | ✅ | 3 个分类 Tab + "全部" |
| 用户可用中文英文写插件 | ✅ | Skills 系统基于 Markdown manifest支持任意语言 |
### 4. 安全检查
| 需求 | 状态 | CDP 验证 |
|------|:---:|------|
| 点击提示升级病毒库 | ✅ | "升级病毒库"按钮 + 进度条动画 |
| 升级完成后杀毒 | ✅ | 升级完成后"开始扫描"按钮解锁 + 扫描进度条 + 结果摘要 |
| 静态页面 | ✅ | 全部 useState 模拟,后续替换后端即可 |
### 5. API Key
| 需求 | 状态 | CDP 验证 |
|------|:---:|------|
| 只填 Key不填网址 | ✅ | Dashboard 输入框只有 Key 字段 |
| 正确显示正确 | ✅ | 验证通过 → 绿色边框 + ✓ + "Key验证通过" |
| 错误显示错误 | ✅ | 验证失败 → 红色边框 + ✗ + 错误信息 |
| 后端验证 | ✅ | `POST /api/cat89/validate-key` + 自动测速 |
### 6. 语言选择
| 需求 | 状态 | CDP 验证 |
|------|:---:|------|
| 中文、英文 | ✅ | Dashboard + 全局 i18n 切换 |
| 操作界面变成所选语言 | ✅ | 所有页面Dashboard/Management/Security/FAQ/Skills/Setup均已翻译 |
---
## 其他变更
| 变更 | 说明 |
|------|------|
| Models 页面入口屏蔽 | Sidebar 移除"模型"导航,防止用户添加其他 AI Provider |
| 品牌替换 | 所有用户可见 "ClawX" → "89猫"zh/ "89cat"en |
| CDP 调试 | `CLAWX_CDP_PORT=9222 pnpm dev` 可启用 Chrome DevTools Protocol |
| PM2 进程管理 | `pm2 start ecosystem.config.js` 管理 dev server |
| 国内镜像 | `.npmrc` 配置 npmmirror.com 加速 |
---
## 汇总
| 需求项 | 总计 | 已验证 | 待验证 | 不适用 |
|--------|:---:|:---:|:---:|:---:|
| 一、基本信息 | 4 | 2 | 1 | 1 |
| 二、栏目功能 | 6 | 6 | 0 | 0 |
| **合计** | **10** | **8** | **1** | **1** |
> **待验证项:** 安装包图标(需打包 + 设计师出图)
>
> **不适用项:** 图形设计师后续设计(非开发任务)
---
*报告生成时间2026-06-04*
*验证工具playwright-cli CDP → Electron*

17
ecosystem.config.js Normal file
View File

@@ -0,0 +1,17 @@
module.exports = {
apps: [
{
name: 'clawx-dev',
script: 'dev.js',
cwd: __dirname,
env: {
CLAWX_CDP_PORT: '9222',
},
log_date_format: 'YYYY-MM-DD HH:mm:ss',
error_file: './logs/clawx-error.log',
out_file: './logs/clawx-out.log',
merge_logs: true,
autorestart: false,
},
],
};

View File

@@ -1,6 +1,6 @@
appId: app.clawx.desktop
productName: ClawX
copyright: Copyright © 2026 ClawX
appId: app.cat89.desktop
productName: 89猫
copyright: Copyright © 2026 89猫
compression: maximum
artifactName: ${productName}-${version}-${os}-${arch}.${ext}
@@ -59,7 +59,7 @@ publish:
useMultipleRangeRequest: false
- provider: github
owner: ValueCell-ai
repo: ClawX
repo: 89猫
# macOS Configuration
mac:
@@ -86,8 +86,8 @@ mac:
entitlementsInherit: entitlements.mac.plist
notarize: true
extendInfo:
NSMicrophoneUsageDescription: ClawX requires microphone access for voice features
NSCameraUsageDescription: ClawX requires camera access for video features
NSMicrophoneUsageDescription: 89猫 requires microphone access for voice features
NSCameraUsageDescription: 89猫 requires camera access for video features
dmg:
# Explicit volume size prevents dmg-builder@1.2.0 auto-calculation from
@@ -133,8 +133,8 @@ nsis:
differentialPackage: true
createDesktopShortcut: true
createStartMenuShortcut: true
shortcutName: ClawX
uninstallDisplayName: ClawX
shortcutName: 89猫
uninstallDisplayName: 89猫
license: LICENSE
include: scripts/installer.nsh
installerIcon: resources/icons/icon.ico
@@ -161,13 +161,13 @@ linux:
arch:
- x64
category: Utility
maintainer: ClawX Team <public@valuecell.ai>
vendor: ClawX
maintainer: 89猫 Team <public@valuecell.ai>
vendor: 89猫
synopsis: AI Assistant powered by OpenClaw
description: ClawX is a graphical AI assistant application that integrates with OpenClaw Gateway to provide intelligent automation and assistance across multiple messaging platforms.
description: 89猫 is a graphical AI assistant application that integrates with OpenClaw Gateway to provide intelligent automation and assistance across multiple messaging platforms.
desktop:
entry:
Name: ClawX
Name: 89猫
Comment: AI Assistant powered by OpenClaw
Categories: Utility;Network;
Keywords: ai;assistant;automation;chat;

View File

@@ -9,6 +9,7 @@ import { deviceOAuthManager, type OAuthProviderType } from '../../utils/device-o
import { browserOAuthManager, type BrowserOAuthProviderType } from '../../utils/browser-oauth';
import type { HostApiContext } from '../context';
import { parseJsonBody, sendJson } from '../route-utils';
import { getFastestBaseUrl } from '../../services/port-pinger';
import {
syncDefaultProviderToRuntime,
syncDeletedProviderApiKeyToRuntime,
@@ -106,6 +107,22 @@ export async function handleProviderRoutes(
return true;
}
if (url.pathname === '/api/cat89/validate-key' && req.method === 'POST') {
try {
const body = await parseJsonBody<{ apiKey: string }>(req);
const baseUrl = await getFastestBaseUrl();
if (!baseUrl) {
sendJson(res, 200, { valid: false, message: '无法连接到89猫服务器请检查网络' });
return true;
}
const result = await validateApiKeyWithProvider('cat89', body.apiKey, { baseUrl });
sendJson(res, 200, result);
} catch (error) {
sendJson(res, 500, { valid: false, message: String(error) });
}
return true;
}
if (url.pathname === '/api/provider-accounts/validate' && req.method === 'POST') {
try {
// Accept legacy `providerId` as a fallback so external clients that

View File

@@ -54,7 +54,7 @@ import { browserOAuthManager } from '../utils/browser-oauth';
import { whatsAppLoginManager } from '../utils/whatsapp-login';
import { syncAllProviderAuthToRuntime } from '../services/providers/provider-runtime-sync';
const WINDOWS_APP_USER_MODEL_ID = 'app.clawx.desktop';
const WINDOWS_APP_USER_MODEL_ID = 'app.cat89.desktop';
const isE2EMode = process.env.CLAWX_E2E === '1';
const requestedUserDataDir = process.env.CLAWX_USER_DATA_DIR?.trim();
const requestedRemoteDebuggingPort = process.env.CLAWX_REMOTE_DEBUGGING_PORT?.trim();
@@ -88,7 +88,7 @@ app.disableHardwareAcceleration();
// on X11 it supplements the StartupWMClass matching.
// Must be called before app.whenReady() / before any window is created.
if (process.platform === 'linux') {
app.setDesktopName('clawx.desktop');
app.setDesktopName('cat89.desktop');
}
// Prevent multiple instances of the app from running simultaneously.

View File

@@ -0,0 +1,57 @@
/**
* Port pinger — TCP latency test for 89cat API endpoints
*/
import * as net from 'node:net';
const CAT89_HOST = 'api2.is89.com';
const CAT89_PORTS = [898902, 898903, 898904, 898905, 898906, 898907, 898908, 898909];
export interface PingResult {
port: number;
latencyMs: number;
baseUrl: string;
}
function pingPort(host: string, port: number, timeoutMs = 3000): Promise<PingResult | null> {
return new Promise((resolve) => {
const start = Date.now();
const socket = new net.Socket();
let settled = false;
const finish = (ok: boolean) => {
if (settled) return;
settled = true;
socket.destroy();
if (ok) {
const latencyMs = Date.now() - start;
resolve({ port, latencyMs, baseUrl: `http://${host}:${port}` });
} else {
resolve(null);
}
};
socket.setTimeout(timeoutMs);
socket.on('connect', () => finish(true));
socket.on('error', () => finish(false));
socket.on('timeout', () => finish(false));
socket.connect(port, host);
});
}
export async function pingAllPorts(): Promise<PingResult[]> {
const results = await Promise.all(
CAT89_PORTS.map((port) => pingPort(CAT89_HOST, port)),
);
const reachable = results.filter((r): r is PingResult => r !== null);
reachable.sort((a, b) => a.latencyMs - b.latencyMs);
return reachable;
}
export async function getFastestBaseUrl(): Promise<string | null> {
const results = await pingAllPorts();
if (results.length === 0) return null;
return results[0].baseUrl;
}
export { CAT89_HOST, CAT89_PORTS };

View File

@@ -364,6 +364,18 @@ export const PROVIDER_DEFINITIONS: ProviderDefinition[] = [
defaultAuthMode: 'api_key',
supportsMultipleAccounts: true,
},
{
id: 'cat89',
name: '89猫',
icon: '🐱',
placeholder: '输入你的89猫 Key',
requiresApiKey: true,
category: 'official',
envVar: 'CAT89_API_KEY',
supportedAuthModes: ['api_key'],
defaultAuthMode: 'api_key',
supportsMultipleAccounts: false,
},
];
const PROVIDER_DEFINITION_MAP = new Map(

View File

@@ -12,6 +12,7 @@ export const PROVIDER_TYPES = [
'modelstudio',
'ollama',
'custom',
'cat89',
] as const;
export const BUILTIN_PROVIDER_TYPES = [
@@ -27,6 +28,7 @@ export const BUILTIN_PROVIDER_TYPES = [
'minimax-portal-cn',
'modelstudio',
'ollama',
'cat89',
] as const;
export type ProviderType = (typeof PROVIDER_TYPES)[number];

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ClawX</title>
<title>89猫</title>
</head>
<body>
<div id="root"></div>

View File

@@ -1,5 +1,5 @@
{
"name": "clawx",
"name": "cat89",
"version": "0.4.8-alpha.0",
"pnpm": {
"onlyBuiltDependencies": [
@@ -25,9 +25,9 @@
]
}
},
"description": "ClawX - Graphical AI Assistant based on OpenClaw",
"description": "89猫 - Graphical AI Assistant based on OpenClaw",
"main": "dist-electron/main/index.js",
"author": "ClawX Team",
"author": "89猫 Team",
"license": "MIT",
"private": true,
"scripts": {

View File

@@ -10,6 +10,10 @@ import i18n from './i18n';
import { MainLayout } from './components/layout/MainLayout';
import { TooltipProvider } from '@/components/ui/tooltip';
import { Models } from './pages/Models';
import { Dashboard } from './pages/Dashboard';
import { Management } from './pages/Management';
import { Security } from './pages/Security';
import { Faq } from './pages/Faq';
import { Chat } from './pages/Chat';
import { Agents } from './pages/Agents';
import { Channels } from './pages/Channels';
@@ -201,12 +205,16 @@ function App() {
{/* Main application routes */}
<Route element={<MainLayout />}>
<Route path="/" element={<Chat />} />
<Route path="/" element={<Dashboard />} />
<Route path="/chat" element={<Chat />} />
<Route path="/models" element={<Models />} />
<Route path="/agents" element={<Agents />} />
<Route path="/channels" element={<Channels />} />
<Route path="/skills" element={<Skills />} />
<Route path="/cron" element={<Cron />} />
<Route path="/management" element={<Management />} />
<Route path="/security" element={<Security />} />
<Route path="/faq" element={<Faq />} />
<Route path="/image-generation" element={devModeUnlocked ? <ImageGenerationPage /> : <Navigate to="/" replace />} />
<Route path="/dreams" element={devModeUnlocked ? <Dreams /> : <Navigate to="/" replace />} />
<Route path="/settings/*" element={<Settings />} />

View File

@@ -20,10 +20,11 @@ import {
Pencil,
Check,
X,
Cpu,
ImagePlus,
Moon,
ChevronRight,
LayoutDashboard,
MessageSquare,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { rendererExtensionRegistry } from '@/extensions/registry';
@@ -157,7 +158,7 @@ export function Sidebar() {
}, [isMac, sidebarCollapsed]);
const navigate = useNavigate();
const isOnChat = useLocation().pathname === '/';
const isOnChat = useLocation().pathname === '/chat';
const getSessionLabel = (key: string, displayName?: string, label?: string) =>
sessionLabels[key] ?? label ?? displayName ?? key;
@@ -309,7 +310,8 @@ export function Sidebar() {
const extraNavItems = rendererExtensionRegistry.getExtraNavItems();
const coreNavItems = [
{ to: '/models', icon: <Cpu className="h-4 w-4" strokeWidth={2} />, label: t('sidebar.models'), testId: 'sidebar-nav-models' },
{ to: '/', icon: <LayoutDashboard className="h-4 w-4" strokeWidth={2} />, label: t('sidebar.dashboard'), testId: 'sidebar-nav-dashboard' },
{ to: '/chat', icon: <MessageSquare className="h-4 w-4" strokeWidth={2} />, label: t('sidebar.chat'), testId: 'sidebar-nav-chat' },
{ to: '/agents', icon: <Bot className="h-4 w-4" strokeWidth={2} />, label: t('sidebar.agents'), testId: 'sidebar-nav-agents' },
{ to: '/channels', icon: <Network className="h-4 w-4" strokeWidth={2} />, label: t('sidebar.channels'), testId: 'sidebar-nav-channels' },
{ to: '/skills', icon: <Puzzle className="h-4 w-4" strokeWidth={2} />, label: t('sidebar.skills'), testId: 'sidebar-nav-skills' },
@@ -359,9 +361,9 @@ export function Sidebar() {
>
{!sidebarCollapsed && (
<div className="flex items-center gap-2 px-2 overflow-hidden">
<img src={logoSvg} alt="ClawX" className="h-5 w-auto shrink-0" />
<img src={logoSvg} alt="89猫" className="h-5 w-auto shrink-0" />
<span className="text-sm font-semibold truncate whitespace-nowrap text-foreground/90">
ClawX
89
</span>
</div>
)}
@@ -391,7 +393,7 @@ export function Sidebar() {
onClick={() => {
const { messages } = useChatStore.getState();
if (messages.length > 0) newSession();
navigate('/');
navigate('/chat');
}}
className={cn(
'sidebar-nav-text flex items-center gap-2 rounded-lg px-2.5 py-2 transition-colors',
@@ -482,7 +484,7 @@ export function Sidebar() {
} else {
switchSession(s.key);
}
navigate('/');
navigate('/chat');
}}
onDoubleClick={() => handleStartRename(s.key, sessionLabel)}
className={cn(

View File

@@ -6,7 +6,7 @@
"skills": "Skills",
"agents": "Agents",
"channels": "Channels",
"dashboard": "Dashboard",
"dashboard": "Home",
"settings": "Settings",
"devConsole": "Developer Console",
"models": "Models",

View File

@@ -1,4 +1,50 @@
{
"title": "89cat",
"subtitle": "Your AI Smart Assistant",
"cards": {
"management": {
"title": "Management",
"desc": "Start or stop 89cat, access super admin console"
},
"tokenRecharge": {
"title": "Token Recharge",
"desc": "Visit 89.is89.com for token recharge"
},
"plugins": {
"title": "Plugins",
"desc": "Install and manage 89 plugins, OpenClaw skills"
},
"security": {
"title": "Security Check",
"desc": "Update virus definitions and scan system security"
},
"apiKey": {
"title": "Work Key",
"desc": "Enter your API key to get started, no URL needed"
},
"faq": {
"title": "FAQ",
"desc": "View usage help and frequently asked questions"
},
"language": {
"title": "Language",
"desc": "Switch the interface display language"
}
},
"apiKey": {
"placeholder": "Enter API Key",
"validate": "Validate",
"valid": "Key verified successfully",
"invalidKey": "Invalid key, please check and try again",
"validateFailed": "Validation failed, please check network"
},
"gatewayRunning": "89cat Running",
"gatewayStopped": "89cat Stopped",
"stop89cat": "Stop 89cat",
"start89cat": "Start 89cat",
"adminConsole": "89 Super AI Brain Management Center",
"adminConsoleDesc": "Management center runs in the cloud to protect your privacy",
"openAdminConsole": "Open Admin Console",
"gateway": "Gateway",
"channels": "Channels",
"skills": "Skills",
@@ -110,5 +156,55 @@
"relayModelRequired": "Model name is required",
"relayModelInvalid": "Enter only the model name, for example gpt-image-2. Do not include provider/model."
}
},
"security": {
"virusDbVersion": "Virus DB Version",
"lastScan": "Last Scan",
"updateDb": "Update Virus DB",
"startScan": "Start Scan",
"idle": "Waiting for virus database update",
"updatingDb": "Updating virus database...",
"readyToScan": "Virus database ready",
"scanning": "Scanning...",
"scanComplete": "Scan Complete",
"filesScanned": "Files Scanned",
"threatsFound": "{{count}} threat(s) found",
"noThreats": "No threats found",
"neverScanned": "Never scanned",
"updateDbFirst": "Please update virus database first",
"clickToScan": "Virus database is ready, you can start scanning",
"scanResult": "Scan completed",
"scanSummary": "Scanned {{files}} files in {{time}} seconds"
},
"faq": {
"sections": {
"install": "Installation",
"usage": "Usage",
"key": "API Key",
"security": "Security",
"other": "Other"
},
"q": {
"howToInstall": "How to install 89cat?",
"systemRequirements": "What are the system requirements?",
"howToUse": "How to start using 89cat?",
"stopRestart": "How to stop or restart 89cat?",
"whereToGetKey": "Where to get an API Key?",
"keyNotWorking": "What if my key shows as invalid?",
"isMyDataSafe": "Is my data safe?",
"howToScan": "How to run a security check?",
"contactUs": "How to contact support?"
},
"a": {
"howToInstall": "Download the installer for your platform from the official website, double-click to run and it will complete installation automatically. After installation, 89cat icons will appear on your desktop and start menu.",
"systemRequirements": "Supports Windows 10+, macOS 11+, Linux (Ubuntu 20.04+). Minimum 4GB RAM, 8GB recommended. 1GB free disk space required.",
"howToUse": "Enter your API Key in the \"Work Key\" card on the home page and click Validate. Once verified, you can chat with AI on the Chat page.",
"stopRestart": "Go to the \"Management\" page, click \"Stop 89cat\" to stop the service, and click \"Start 89cat\" to restart.",
"whereToGetKey": "Visit 89.is89.com to register an account and obtain an API Key.",
"keyNotWorking": "Check if the key is entered correctly (case-sensitive and no spaces), confirm it hasn't expired, and verify network connectivity. Contact support if issues persist.",
"isMyDataSafe": "89cat securely stores your API Key using the system's native keychain. The management center runs in the cloud while your personal data stays local, ensuring full privacy protection.",
"howToScan": "Go to the \"Security Check\" page, click \"Update Virus DB\" first, then click \"Start Scan\" to perform a full system security scan.",
"contactUs": "Send email to public@valuecell.ai or visit 89.is89.com for more help."
}
}
}

View File

@@ -1,6 +1,6 @@
{
"title": "Settings",
"subtitle": "Configure your ClawX experience",
"subtitle": "Configure your 89cat experience",
"appearance": {
"title": "General",
"description": "Customize the look and feel",
@@ -10,7 +10,7 @@
"system": "System",
"language": "Language",
"launchAtStartup": "Launch at system startup",
"launchAtStartupDesc": "Automatically launch ClawX when you log in"
"launchAtStartupDesc": "Automatically launch 89cat when you log in"
},
"aiProviders": {
"title": "AI Providers",
@@ -37,7 +37,7 @@
"notRequired": "Not required",
"empty": {
"title": "No providers configured",
"desc": "Add an AI provider to start using ClawX",
"desc": "Add an AI provider to start using 89cat",
"cta": "Add Your First Provider"
},
"dialog": {
@@ -60,7 +60,7 @@
"protocol": "Protocol",
"advancedConfig": "Advanced configuration",
"userAgent": "User-Agent",
"userAgentPlaceholder": "ClawX/1.0",
"userAgentPlaceholder": "89cat/1.0",
"fallbackModels": "Fallback Models",
"fallbackProviders": "Fallback Providers",
"fallbackModelIds": "Fallback Model IDs",
@@ -135,7 +135,7 @@
"appLogs": "Application Logs",
"openFolder": "Open Folder",
"autoStart": "Auto-start Gateway",
"autoStartDesc": "Start Gateway when ClawX launches",
"autoStartDesc": "Start Gateway when 89cat launches",
"proxyTitle": "Proxy",
"proxyDesc": "Route Electron and Gateway traffic through your local proxy client.",
"proxyServer": "Proxy Server",
@@ -156,7 +156,7 @@
},
"updates": {
"title": "Updates",
"description": "Keep ClawX up to date",
"description": "Keep 89cat up to date",
"autoCheck": "Auto-check for updates",
"autoCheckDesc": "Check for updates on startup",
"autoDownload": "Update prompts",
@@ -185,13 +185,13 @@
"whatsNew": "What's New:",
"errorDetails": "Error Details:",
"toast": {
"availableTitle": "ClawX update available",
"availableTitle": "89cat update available",
"availableDescription": "Version v{{version}} is available. Download it when you're ready.",
"downloadedTitle": "ClawX update ready",
"downloadedTitle": "89cat update ready",
"downloadedDescription": "Version v{{version}} has been downloaded. Restart to install it.",
"unknownVersion": "new"
},
"help": "ClawX checks for updates automatically and prompts you before downloading or installing."
"help": "89cat checks for updates automatically and prompts you before downloading or installing."
},
"advanced": {
"title": "Advanced",
@@ -218,7 +218,7 @@
"devMode": "Developer Mode",
"devModeDesc": "Show developer tools and shortcuts",
"telemetry": "Anonymous Usage Data",
"telemetryDesc": "Allow providing anonymous basic usage data to improve ClawX"
"telemetryDesc": "Allow providing anonymous basic usage data to improve 89cat"
},
"developer": {
"title": "Developer",
@@ -280,7 +280,7 @@
},
"about": {
"title": "About",
"appName": "ClawX",
"appName": "89cat",
"tagline": "Graphical AI Assistant",
"basedOn": "Based on OpenClaw",
"version": "Version {{version}}",

View File

@@ -1,7 +1,7 @@
{
"steps": {
"welcome": {
"title": "Welcome to ClawX",
"title": "Welcome to 89cat",
"description": "Your AI assistant is ready to be configured"
},
"runtime": {
@@ -22,12 +22,12 @@
},
"complete": {
"title": "All Set!",
"description": "ClawX is ready to use"
"description": "89cat is ready to use"
}
},
"welcome": {
"title": "Welcome to ClawX",
"description": "ClawX is a graphical interface for OpenClaw, making it easy to use AI assistants across your favorite messaging platforms.",
"title": "Welcome to 89cat",
"description": "89cat is a graphical interface for OpenClaw, making it easy to use AI assistants across your favorite messaging platforms.",
"features": {
"noCommand": "Zero command-line required",
"modernUI": "Modern, beautiful interface",
@@ -113,7 +113,7 @@
},
"complete": {
"title": "Setup Complete!",
"subtitle": "ClawX is configured and ready to use. You can now start chatting with your AI assistant.",
"subtitle": "89cat is configured and ready to use. You can now start chatting with your AI assistant.",
"provider": "AI Provider",
"components": "Components",
"gateway": "Gateway",

View File

@@ -14,7 +14,13 @@
"builtIn": "Built-in ({{count}})",
"marketplace": "Marketplace ({{count}})",
"enabledList": "Enabled ({{count}})",
"disabledList": "Disabled ({{count}})"
"disabledList": "Disabled ({{count}})",
"category": {
"all": "All",
"cat89": "89 Plugins",
"openclaw": "OpenClaw Skills",
"other": "Other Plugins"
}
},
"search": "Search skills...",
"searchMarketplace": "Search marketplace...",

View File

@@ -1,6 +1,6 @@
{
"title": "設定",
"subtitle": "ClawX の体験をカスタマイズ",
"subtitle": "89猫 の体験をカスタマイズ",
"appearance": {
"title": "通用",
"description": "外観とスタイルをカスタマイズ",
@@ -10,7 +10,7 @@
"system": "システム",
"language": "言語",
"launchAtStartup": "システム起動時に自動起動",
"launchAtStartupDesc": "ログイン時に ClawX を自動的に起動します"
"launchAtStartupDesc": "ログイン時に 89猫 を自動的に起動します"
},
"aiProviders": {
"title": "AI プロバイダー",
@@ -37,7 +37,7 @@
"notRequired": "不要",
"empty": {
"title": "プロバイダーが構成されていません",
"desc": "ClawX の使用を開始するには AI プロバイダーを追加してください",
"desc": "89猫 の使用を開始するには AI プロバイダーを追加してください",
"cta": "最初のプロバイダーを追加"
},
"dialog": {
@@ -60,7 +60,7 @@
"protocol": "プロトコル",
"advancedConfig": "詳細設定",
"userAgent": "User-Agent",
"userAgentPlaceholder": "ClawX/1.0",
"userAgentPlaceholder": "89猫/1.0",
"fallbackModels": "フォールバックモデル",
"fallbackProviders": "別プロバイダーへのフォールバック",
"fallbackModelIds": "同一プロバイダーのフォールバックモデル ID",
@@ -135,7 +135,7 @@
"appLogs": "アプリケーションログ",
"openFolder": "フォルダーを開く",
"autoStart": "ゲートウェイ自動起動",
"autoStartDesc": "ClawX 起動時にゲートウェイを自動起動",
"autoStartDesc": "89猫 起動時にゲートウェイを自動起動",
"proxyTitle": "プロキシ",
"proxyDesc": "Electron と Gateway の通信をローカルプロキシ経由にします。",
"proxyServer": "プロキシサーバー",
@@ -156,7 +156,7 @@
},
"updates": {
"title": "アップデート",
"description": "ClawX を最新に保つ",
"description": "89猫 を最新に保つ",
"autoCheck": "自動更新チェック",
"autoCheckDesc": "起動時に更新を確認",
"autoDownload": "更新通知",
@@ -185,13 +185,13 @@
"whatsNew": "更新内容:",
"errorDetails": "エラー詳細:",
"toast": {
"availableTitle": "ClawX の更新があります",
"availableTitle": "89猫 の更新があります",
"availableDescription": "バージョン v{{version}} を利用できます。準備ができたらダウンロードしてください。",
"downloadedTitle": "ClawX の更新準備ができました",
"downloadedTitle": "89猫 の更新準備ができました",
"downloadedDescription": "バージョン v{{version}} のダウンロードが完了しました。再起動してインストールしてください。",
"unknownVersion": "新しい"
},
"help": "ClawX は自動的に更新を確認し、ダウンロードやインストールの前に通知します。"
"help": "89猫 は自動的に更新を確認し、ダウンロードやインストールの前に通知します。"
},
"advanced": {
"title": "詳細設定",
@@ -218,7 +218,7 @@
"devMode": "開発者モード",
"devModeDesc": "開発者ツールとショートカットを表示",
"telemetry": "匿名利用データ",
"telemetryDesc": "ClawX 改善のため、匿名の基本的な利用データの提供を許可します"
"telemetryDesc": "89猫 改善のため、匿名の基本的な利用データの提供を許可します"
},
"developer": {
"title": "開発者",
@@ -280,7 +280,7 @@
},
"about": {
"title": "バージョン情報",
"appName": "ClawX",
"appName": "89猫",
"tagline": "グラフィカル AI アシスタント",
"basedOn": "OpenClaw ベース",
"version": "バージョン {{version}}",

View File

@@ -1,7 +1,7 @@
{
"steps": {
"welcome": {
"title": "ClawXへようこそ",
"title": "89猫へようこそ",
"description": "AIアシスタントの設定準備が整いました"
},
"runtime": {
@@ -22,12 +22,12 @@
},
"complete": {
"title": "完了!",
"description": "ClawXを使用する準備が整いました"
"description": "89猫を使用する準備が整いました"
}
},
"welcome": {
"title": "ClawXへようこそ",
"description": "ClawXはOpenClawのグラフィカルインターフェースで、お気に入りのメッセージングプラットフォームでAIアシスタントを簡単に使用できます。",
"title": "89猫へようこそ",
"description": "89猫はOpenClawのグラフィカルインターフェースで、お気に入りのメッセージングプラットフォームでAIアシスタントを簡単に使用できます。",
"features": {
"noCommand": "コマンドライン不要",
"modernUI": "モダンで美しいインターフェース",
@@ -113,7 +113,7 @@
},
"complete": {
"title": "セットアップ完了!",
"subtitle": "ClawXの構成が完了し、使用準備が整いました。AIアシスタントとのチャットを開始できます。",
"subtitle": "89猫の構成が完了し、使用準備が整いました。AIアシスタントとのチャットを開始できます。",
"provider": "AI プロバイダー",
"components": "コンポーネント",
"gateway": "ゲートウェイ",

View File

@@ -1,6 +1,6 @@
{
"title": "Настройки",
"subtitle": "Настройте ваш опыт работы с ClawX",
"subtitle": "Настройте ваш опыт работы с 89cat",
"appearance": {
"title": "Общие",
"description": "Настройте внешний вид",
@@ -10,7 +10,7 @@
"system": "Системная",
"language": "Язык",
"launchAtStartup": "Запуск при старте системы",
"launchAtStartupDesc": "Автоматически запускать ClawX при входе в систему"
"launchAtStartupDesc": "Автоматически запускать 89cat при входе в систему"
},
"aiProviders": {
"title": "AI-провайдеры",
@@ -37,7 +37,7 @@
"notRequired": "Не требуется",
"empty": {
"title": "Провайдеры не настроены",
"desc": "Добавьте AI-провайдера, чтобы начать использовать ClawX",
"desc": "Добавьте AI-провайдера, чтобы начать использовать 89cat",
"cta": "Добавить первого провайдера"
},
"dialog": {
@@ -60,7 +60,7 @@
"protocol": "Протокол",
"advancedConfig": "Расширенная конфигурация",
"userAgent": "User-Agent",
"userAgentPlaceholder": "ClawX/1.0",
"userAgentPlaceholder": "89cat/1.0",
"fallbackModels": "Резервные модели",
"fallbackProviders": "Резервные провайдеры",
"fallbackModelIds": "ID резервных моделей",
@@ -135,7 +135,7 @@
"appLogs": "Журналы приложения",
"openFolder": "Открыть папку",
"autoStart": "Автозапуск шлюза",
"autoStartDesc": "Запускать шлюз при старте ClawX",
"autoStartDesc": "Запускать шлюз при старте 89cat",
"proxyTitle": "Прокси",
"proxyDesc": "Направлять трафик Electron и шлюза через локальный прокси-клиент.",
"proxyServer": "Прокси-сервер",
@@ -156,7 +156,7 @@
},
"updates": {
"title": "Обновления",
"description": "Поддерживайте ClawX в актуальном состоянии",
"description": "Поддерживайте 89cat в актуальном состоянии",
"autoCheck": "Автопроверка обновлений",
"autoCheckDesc": "Проверять обновления при запуске",
"autoDownload": "Уведомления об обновлениях",
@@ -185,13 +185,13 @@
"whatsNew": "Что нового:",
"errorDetails": "Детали ошибки:",
"toast": {
"availableTitle": "Доступно обновление ClawX",
"availableTitle": "Доступно обновление 89cat",
"availableDescription": "Доступна версия v{{version}}. Скачайте её, когда будете готовы.",
"downloadedTitle": "Обновление ClawX готово",
"downloadedTitle": "Обновление 89cat готово",
"downloadedDescription": "Версия v{{version}} загружена. Перезапустите приложение для установки.",
"unknownVersion": "новая"
},
"help": "ClawX автоматически проверяет обновления и показывает уведомление перед загрузкой или установкой."
"help": "89cat автоматически проверяет обновления и показывает уведомление перед загрузкой или установкой."
},
"advanced": {
"title": "Дополнительные",
@@ -218,7 +218,7 @@
"devMode": "Режим разработчика",
"devModeDesc": "Показывать инструменты и ярлыки разработчика",
"telemetry": "Анонимные данные об использовании",
"telemetryDesc": "Разрешить предоставление анонимных базовых данных об использовании для улучшения ClawX"
"telemetryDesc": "Разрешить предоставление анонимных базовых данных об использовании для улучшения 89cat"
},
"developer": {
"title": "Разработчик",
@@ -280,7 +280,7 @@
},
"about": {
"title": "О приложении",
"appName": "ClawX",
"appName": "89cat",
"tagline": "Графический AI-ассистент",
"basedOn": "На базе OpenClaw",
"version": "Версия {{version}}",

View File

@@ -22,12 +22,12 @@
},
"complete": {
"title": "Готово!",
"description": "ClawX готов к использованию"
"description": "89cat готов к использованию"
}
},
"welcome": {
"title": "Добро пожаловать в ClawX",
"description": "ClawX — это графический интерфейс для OpenClaw, упрощающий использование AI-ассистентов через ваши любимые платформы обмена сообщениями.",
"title": "Добро пожаловать в 89cat",
"description": "89cat — это графический интерфейс для OpenClaw, упрощающий использование AI-ассистентов через ваши любимые платформы обмена сообщениями.",
"features": {
"noCommand": "Без командной строки",
"modernUI": "Современный красивый интерфейс",
@@ -113,7 +113,7 @@
},
"complete": {
"title": "Настройка завершена!",
"subtitle": "ClawX настроен и готов к использованию. Теперь вы можете начать общение с вашим AI-ассистентом.",
"subtitle": "89cat настроен и готов к использованию. Теперь вы можете начать общение с вашим AI-ассистентом.",
"provider": "AI-провайдер",
"components": "Компоненты",
"gateway": "Шлюз",

View File

@@ -6,7 +6,7 @@
"skills": "技能",
"agents": "Agents",
"channels": "频道",
"dashboard": "仪表盘",
"dashboard": "首页",
"settings": "设置",
"devConsole": "开发者控制台",
"models": "模型",

View File

@@ -1,4 +1,50 @@
{
"title": "89猫",
"subtitle": "你的AI智能助手",
"cards": {
"management": {
"title": "管理中心",
"desc": "停止或启动89猫访问超级管理后台"
},
"tokenRecharge": {
"title": "Token充值",
"desc": "前往89.is89.com进行Token充值"
},
"plugins": {
"title": "插件安装",
"desc": "安装和管理89插件、OpenClaw技能"
},
"security": {
"title": "安全检查",
"desc": "升级病毒库并扫描系统安全"
},
"apiKey": {
"title": "工作钥匙",
"desc": "输入API Key即可开始工作无需填写网址"
},
"faq": {
"title": "常见问题",
"desc": "查看使用帮助和常见问题解答"
},
"language": {
"title": "语言选择",
"desc": "切换界面显示语言"
}
},
"apiKey": {
"placeholder": "请输入API Key",
"validate": "验证",
"valid": "Key验证通过",
"invalidKey": "Key无效请检查后重试",
"validateFailed": "验证失败,请检查网络连接"
},
"gatewayRunning": "89猫运行中",
"gatewayStopped": "89猫已停止",
"stop89cat": "停止使用89猫",
"start89cat": "打开使用89猫",
"adminConsole": "89超级人工智能脑管理中心",
"adminConsoleDesc": "管理中心运行在云端,保护您的个人隐私",
"openAdminConsole": "前往管理中心",
"gateway": "网关",
"channels": "频道",
"skills": "技能",
@@ -110,5 +156,55 @@
"relayModelRequired": "请填写模型名",
"relayModelInvalid": "请只填写模型名,例如 gpt-image-2不要填写 provider/model。"
}
},
"security": {
"virusDbVersion": "病毒库版本",
"lastScan": "上次扫描时间",
"updateDb": "升级病毒库",
"startScan": "开始扫描",
"idle": "等待升级病毒库",
"updatingDb": "正在升级病毒库...",
"readyToScan": "病毒库已就绪",
"scanning": "正在扫描中...",
"scanComplete": "扫描完成",
"filesScanned": "扫描文件数",
"threatsFound": "发现 {{count}} 个威胁",
"noThreats": "未发现威胁",
"neverScanned": "暂未扫描",
"updateDbFirst": "请先升级病毒库",
"clickToScan": "病毒库已就绪,可以开始扫描",
"scanResult": "扫描已完成",
"scanSummary": "共扫描 {{files}} 个文件,耗时 {{time}} 秒"
},
"faq": {
"sections": {
"install": "安装相关",
"usage": "使用相关",
"key": "Key相关",
"security": "安全相关",
"other": "其他"
},
"q": {
"howToInstall": "如何安装89猫",
"systemRequirements": "89猫的系统要求是什么",
"howToUse": "如何开始使用89猫",
"stopRestart": "如何停止或重启89猫",
"whereToGetKey": "从哪里获取API Key",
"keyNotWorking": "Key显示无效怎么办",
"isMyDataSafe": "我的数据安全吗?",
"howToScan": "如何进行安全检查?",
"contactUs": "如何联系客服?"
},
"a": {
"howToInstall": "从官方网站下载对应平台的安装包双击运行即可自动完成安装。安装完成后桌面和开始菜单都会出现89猫图标。",
"systemRequirements": "支持 Windows 10+、macOS 11+、Linux (Ubuntu 20.04+)。最低需要4GB内存推荐8GB。需要1GB可用磁盘空间。",
"howToUse": "在首页的\"工作钥匙\"卡片中输入你的API Key并点击验证验证通过后即可在聊天页面与AI对话。",
"stopRestart": "进入\"管理中心\"页面,点击\"停止使用89猫\"按钮即可停止服务,再次点击\"打开使用89猫\"即可重新启动。",
"whereToGetKey": "请访问 89.is89.com 注册账号并获取API Key。",
"keyNotWorking": "请检查Key是否输入正确注意大小写和空格确认Key未过期确认网络连接正常。如仍有问题请联系客服。",
"isMyDataSafe": "89猫使用系统原生密钥链安全存储你的API Key。管理中心运行在云端你的个人数据保留在本地隐私受到充分保护。",
"howToScan": "进入\"安全检查\"页面,先点击\"升级病毒库\",升级完成后点击\"开始扫描\"即可对系统进行全面安全扫描。",
"contactUs": "请发送邮件至 public@valuecell.ai 或访问 89.is89.com 获取更多帮助。"
}
}
}

View File

@@ -1,6 +1,6 @@
{
"title": "设置",
"subtitle": "配置您的 ClawX 体验",
"subtitle": "配置您的 89猫 体验",
"appearance": {
"title": "通用",
"description": "自定义外观和风格",
@@ -10,7 +10,7 @@
"system": "跟随系统",
"language": "语言",
"launchAtStartup": "开机自动启动",
"launchAtStartupDesc": "登录系统后自动启动 ClawX"
"launchAtStartupDesc": "登录系统后自动启动 89猫"
},
"aiProviders": {
"title": "AI 模型提供商",
@@ -37,7 +37,7 @@
"notRequired": "非必填",
"empty": {
"title": "未配置提供商",
"desc": "添加 AI 提供商以开始使用 ClawX",
"desc": "添加 AI 提供商以开始使用 89猫",
"cta": "添加您的第一个提供商"
},
"dialog": {
@@ -60,7 +60,7 @@
"protocol": "协议",
"advancedConfig": "高级配置",
"userAgent": "User-Agent",
"userAgentPlaceholder": "ClawX/1.0",
"userAgentPlaceholder": "89猫/1.0",
"fallbackModels": "回退模型",
"fallbackProviders": "跨 Provider 回退",
"fallbackModelIds": "同 Provider 回退模型 ID",
@@ -135,7 +135,7 @@
"appLogs": "应用日志",
"openFolder": "打开文件夹",
"autoStart": "自动启动网关",
"autoStartDesc": "ClawX 启动时自动启动网关",
"autoStartDesc": "89猫 启动时自动启动网关",
"proxyTitle": "代理",
"proxyDesc": "让 Electron 和 Gateway 的网络请求都走本地代理客户端。",
"proxyServer": "代理服务器",
@@ -156,7 +156,7 @@
},
"updates": {
"title": "更新",
"description": "保持 ClawX 最新",
"description": "保持 89猫 最新",
"autoCheck": "自动检查更新",
"autoCheckDesc": "启动时检查更新",
"autoDownload": "更新提示",
@@ -185,13 +185,13 @@
"whatsNew": "更新内容:",
"errorDetails": "错误详情:",
"toast": {
"availableTitle": "ClawX 有新版本可用",
"availableTitle": "89猫 有新版本可用",
"availableDescription": "v{{version}} 已可下载。准备好后可开始下载。",
"downloadedTitle": "ClawX 更新已就绪",
"downloadedTitle": "89猫 更新已就绪",
"downloadedDescription": "v{{version}} 已下载完成。重启即可安装。",
"unknownVersion": "新版本"
},
"help": "ClawX 会自动检查更新,并在下载或安装前提示您确认。"
"help": "89猫 会自动检查更新,并在下载或安装前提示您确认。"
},
"advanced": {
"title": "高级",
@@ -218,7 +218,7 @@
"devMode": "开发者模式",
"devModeDesc": "显示开发者工具和快捷方式",
"telemetry": "匿名使用数据",
"telemetryDesc": "允许提供匿名的基础使用数据,用于改进 ClawX"
"telemetryDesc": "允许提供匿名的基础使用数据,用于改进 89猫"
},
"developer": {
"title": "开发者",
@@ -280,7 +280,7 @@
},
"about": {
"title": "关于",
"appName": "ClawX",
"appName": "89猫",
"tagline": "图形化 AI 助手",
"basedOn": "基于 OpenClaw",
"version": "版本 {{version}}",

View File

@@ -1,7 +1,7 @@
{
"steps": {
"welcome": {
"title": "欢迎使用 ClawX",
"title": "欢迎使用 89猫",
"description": "您的 AI 助手已准备好进行配置"
},
"runtime": {
@@ -22,12 +22,12 @@
},
"complete": {
"title": "准备就绪!",
"description": "ClawX 已准备好使用"
"description": "89猫 已准备好使用"
}
},
"welcome": {
"title": "欢迎使用 ClawX",
"description": "ClawX 是 OpenClaw 的图形界面,让您可以在喜爱的消息平台上轻松使用 AI 助手。",
"title": "欢迎使用 89猫",
"description": "89猫 是 OpenClaw 的图形界面,让您可以在喜爱的消息平台上轻松使用 AI 助手。",
"features": {
"noCommand": "无需命令行",
"modernUI": "现代美观的界面",
@@ -113,7 +113,7 @@
},
"complete": {
"title": "设置完成!",
"subtitle": "ClawX 已配置并准备就绪。您现在可以开始与您的 AI 助手聊天了。",
"subtitle": "89猫 已配置并准备就绪。您现在可以开始与您的 AI 助手聊天了。",
"provider": "AI 提供商",
"components": "组件",
"gateway": "网关",

View File

@@ -14,7 +14,13 @@
"builtIn": "内置 ({{count}})",
"marketplace": "市场 ({{count}})",
"enabledList": "已开启 ({{count}})",
"disabledList": "已禁用 ({{count}})"
"disabledList": "已禁用 ({{count}})",
"category": {
"all": "全部",
"cat89": "89插件",
"openclaw": "OpenClaw 技能",
"other": "其他插件"
}
},
"search": "搜索技能...",
"searchMarketplace": "搜索市场...",

View File

@@ -21,6 +21,7 @@ export const PROVIDER_TYPES = [
'modelstudio',
'ollama',
'custom',
'cat89',
] as const;
export type ProviderType = (typeof PROVIDER_TYPES)[number];
@@ -196,6 +197,14 @@ export const PROVIDER_TYPE_INFO: ProviderTypeInfo[] = [
docsUrl: 'https://icnnp7d0dymg.feishu.cn/wiki/BmiLwGBcEiloZDkdYnGc8RWnn6d#Ee1ldfvKJoVGvfxc32mcILwenth',
docsUrlZh: 'https://icnnp7d0dymg.feishu.cn/wiki/BmiLwGBcEiloZDkdYnGc8RWnn6d#IWQCdfe5fobGU3xf3UGcgbLynGh',
},
{
id: 'cat89',
name: '89猫',
icon: '🐱',
placeholder: '输入你的89猫 Key',
model: '89猫 AI',
requiresApiKey: true,
},
];
/** Get the SVG logo URL for a provider type, falls back to undefined */

View File

@@ -0,0 +1,270 @@
/**
* Dashboard page — main landing page with 7 feature entry cards
*/
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import {
Building2,
Coins,
Puzzle,
ShieldCheck,
KeyRound,
HelpCircle,
Globe,
Eye,
EyeOff,
Check,
X,
Loader2,
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { useSettingsStore } from '@/stores/settings';
import { SUPPORTED_LANGUAGES } from '@/i18n';
import { hostApiFetch } from '@/lib/host-api';
import { cn } from '@/lib/utils';
function ApiKeyCard() {
const { t } = useTranslation('dashboard');
const [showKey, setShowKey] = useState(false);
const [key, setKey] = useState('');
const [validating, setValidating] = useState(false);
const [result, setResult] = useState<'idle' | 'valid' | 'invalid'>('idle');
const [errorMsg, setErrorMsg] = useState('');
const handleValidate = async () => {
const trimmed = key.trim();
if (!trimmed) return;
setValidating(true);
setResult('idle');
setErrorMsg('');
try {
const res = await hostApiFetch<{ valid: boolean; message?: string }>(
'/api/cat89/validate-key',
{
method: 'POST',
body: JSON.stringify({ apiKey: trimmed }),
},
);
if (res.valid) {
setResult('valid');
} else {
setResult('invalid');
setErrorMsg(res.message || t('apiKey.invalidKey'));
}
} catch {
setResult('invalid');
setErrorMsg(t('apiKey.validateFailed'));
} finally {
setValidating(false);
}
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault();
void handleValidate();
}
};
return (
<div className="space-y-3">
<div className="flex items-center gap-2">
<div className="relative flex-1">
<Input
type={showKey ? 'text' : 'password'}
placeholder={t('apiKey.placeholder')}
value={key}
onChange={(e) => {
setKey(e.target.value);
setResult('idle');
setErrorMsg('');
}}
onKeyDown={handleKeyDown}
className={cn(
'pr-10 font-mono',
result === 'valid' && 'border-green-500 focus-visible:ring-green-500',
result === 'invalid' && 'border-red-500 focus-visible:ring-red-500',
)}
/>
<button
type="button"
onClick={() => setShowKey(!showKey)}
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
{showKey ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>
<Button
size="sm"
onClick={() => { void handleValidate(); }}
disabled={validating || !key.trim()}
className="shrink-0"
>
{validating ? <Loader2 className="h-4 w-4 animate-spin" /> : t('apiKey.validate')}
</Button>
</div>
{result === 'valid' && (
<div className="flex items-center gap-1 text-sm text-green-600 dark:text-green-400">
<Check className="h-4 w-4" />
<span>{t('apiKey.valid')}</span>
</div>
)}
{result === 'invalid' && (
<div className="flex items-center gap-1 text-sm text-red-600 dark:text-red-400">
<X className="h-4 w-4" />
<span>{errorMsg}</span>
</div>
)}
</div>
);
}
interface DashboardCardProps {
icon: React.ReactNode;
title: string;
description: string;
onClick: () => void;
testId?: string;
}
function DashboardCard({ icon, title, description, onClick, testId }: DashboardCardProps) {
return (
<button
data-testid={testId}
onClick={onClick}
className={cn(
'group flex flex-col items-center gap-3 rounded-2xl border border-border/60 p-6',
'bg-card text-left transition-all',
'hover:border-primary/30 hover:bg-accent/30 hover:shadow-md',
'active:scale-[0.98]',
)}
>
<div className="flex h-14 w-14 items-center justify-center rounded-xl bg-primary/10 text-primary transition-colors group-hover:bg-primary/20 group-hover:scale-110">
{icon}
</div>
<div className="text-center">
<h3 className="text-base font-semibold text-foreground">{title}</h3>
<p className="mt-1 text-sm text-muted-foreground">{description}</p>
</div>
</button>
);
}
export function Dashboard() {
const navigate = useNavigate();
const { t } = useTranslation('dashboard');
const language = useSettingsStore((s) => s.language);
const setLanguage = useSettingsStore((s) => s.setLanguage);
// Filter to only zh and en for 89猫
const visibleLanguages = SUPPORTED_LANGUAGES.filter(
(lang) => lang.code === 'zh' || lang.code === 'en',
);
const cards: DashboardCardProps[] = [
{
icon: <Building2 className="h-7 w-7" strokeWidth={1.5} />,
title: t('cards.management.title'),
description: t('cards.management.desc'),
onClick: () => navigate('/management'),
testId: 'dashboard-card-management',
},
{
icon: <Coins className="h-7 w-7" strokeWidth={1.5} />,
title: t('cards.tokenRecharge.title'),
description: t('cards.tokenRecharge.desc'),
onClick: () => window.electron.openExternal('https://89.is89.com'),
testId: 'dashboard-card-token',
},
{
icon: <Puzzle className="h-7 w-7" strokeWidth={1.5} />,
title: t('cards.plugins.title'),
description: t('cards.plugins.desc'),
onClick: () => navigate('/skills'),
testId: 'dashboard-card-plugins',
},
{
icon: <ShieldCheck className="h-7 w-7" strokeWidth={1.5} />,
title: t('cards.security.title'),
description: t('cards.security.desc'),
onClick: () => navigate('/security'),
testId: 'dashboard-card-security',
},
{
icon: <HelpCircle className="h-7 w-7" strokeWidth={1.5} />,
title: t('cards.faq.title'),
description: t('cards.faq.desc'),
onClick: () => navigate('/faq'),
testId: 'dashboard-card-faq',
},
];
return (
<div className="mx-auto max-w-3xl space-y-8 py-4">
{/* Header */}
<div className="text-center">
<h1 className="text-3xl font-serif font-normal tracking-tight text-foreground">
{t('title')}
</h1>
<p className="mt-2 text-muted-foreground">{t('subtitle')}</p>
</div>
{/* Feature Cards Grid */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{cards.map((card) => (
<DashboardCard key={card.testId} {...card} />
))}
{/* API Key Card — inline with input */}
<div className="flex flex-col gap-3 rounded-2xl border border-border/60 bg-card p-6 sm:col-span-2 lg:col-span-3">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-primary/10 text-primary">
<KeyRound className="h-5 w-5" strokeWidth={1.5} />
</div>
<div>
<h3 className="text-base font-semibold text-foreground">
{t('cards.apiKey.title')}
</h3>
<p className="text-sm text-muted-foreground">
{t('cards.apiKey.desc')}
</p>
</div>
</div>
<ApiKeyCard />
</div>
{/* Language Selector Card */}
<div className="flex flex-col items-center gap-3 rounded-2xl border border-border/60 bg-card p-6 sm:col-span-2 lg:col-span-3">
<div className="flex items-center gap-3 self-start">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-primary/10 text-primary">
<Globe className="h-5 w-5" strokeWidth={1.5} />
</div>
<div className="text-left">
<h3 className="text-base font-semibold text-foreground">
{t('cards.language.title')}
</h3>
<p className="text-sm text-muted-foreground">
{t('cards.language.desc')}
</p>
</div>
</div>
<div className="flex w-full flex-wrap gap-2">
{visibleLanguages.map((lang) => (
<Button
key={lang.code}
variant={language === lang.code ? 'default' : 'outline'}
size="sm"
onClick={() => setLanguage(lang.code)}
>
{lang.label}
</Button>
))}
</div>
</div>
</div>
</div>
);
}

187
src/pages/Faq/index.tsx Normal file
View File

@@ -0,0 +1,187 @@
/**
* FAQ page — accordion-style Q&A grouped by category
*/
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ChevronDown, HelpCircle } from 'lucide-react';
import { cn } from '@/lib/utils';
interface FaqItem {
q: string;
a: string;
}
interface FaqSection {
title: string;
items: FaqItem[];
}
function FaqAccordion({ item, defaultOpen }: { item: FaqItem; defaultOpen: boolean }) {
const [open, setOpen] = useState(defaultOpen);
return (
<div className="rounded-xl border border-border/60 bg-card overflow-hidden">
<button
type="button"
onClick={() => setOpen(!open)}
className="flex w-full items-center justify-between gap-3 px-5 py-4 text-left transition-colors hover:bg-accent/30"
>
<span className="text-sm font-medium text-foreground">{item.q}</span>
<ChevronDown
className={cn(
'h-4 w-4 shrink-0 text-muted-foreground transition-transform',
open && 'rotate-180',
)}
/>
</button>
{open && (
<div className="px-5 pb-4">
<p className="text-sm text-muted-foreground leading-relaxed whitespace-pre-wrap">
{item.a}
</p>
</div>
)}
</div>
);
}
function FaqSectionBlock({ section, defaultOpen }: { section: FaqSection; defaultOpen: boolean }) {
const [open, setOpen] = useState(defaultOpen);
return (
<div className="space-y-3">
<button
type="button"
onClick={() => setOpen(!open)}
className="flex w-full items-center gap-2 text-left"
>
<ChevronDown
className={cn(
'h-4 w-4 text-muted-foreground transition-transform',
open && 'rotate-180',
)}
/>
<h2 className="text-lg font-semibold text-foreground">{section.title}</h2>
</button>
{open && (
<div className="space-y-2">
{section.items.map((item, idx) => (
<FaqAccordion key={idx} item={item} defaultOpen={idx === 0 && defaultOpen} />
))}
</div>
)}
</div>
);
}
export function Faq() {
const { t } = useTranslation('dashboard');
const sections: FaqSection[] = [
{
title: t('faq.sections.install', '安装相关'),
items: [
{
q: t('faq.q.howToInstall', '如何安装89猫'),
a: t(
'faq.a.howToInstall',
'从官方网站下载对应平台的安装包双击运行即可自动完成安装。安装完成后桌面和开始菜单都会出现89猫图标。',
),
},
{
q: t('faq.q.systemRequirements', '89猫的系统要求是什么'),
a: t(
'faq.a.systemRequirements',
'支持 Windows 10+、macOS 11+、Linux (Ubuntu 20.04+)。最低需要4GB内存推荐8GB。需要1GB可用磁盘空间。',
),
},
],
},
{
title: t('faq.sections.usage', '使用相关'),
items: [
{
q: t('faq.q.howToUse', '如何开始使用89猫'),
a: t(
'faq.a.howToUse',
'在首页的"工作钥匙"卡片中输入你的API Key并点击验证验证通过后即可在聊天页面与AI对话。',
),
},
{
q: t('faq.q.stopRestart', '如何停止或重启89猫'),
a: t(
'faq.a.stopRestart',
'进入"管理中心"页面,点击"停止使用89猫"按钮即可停止服务,再次点击"打开使用89猫"即可重新启动。',
),
},
],
},
{
title: t('faq.sections.key', 'Key相关'),
items: [
{
q: t('faq.q.whereToGetKey', '从哪里获取API Key'),
a: t('faq.a.whereToGetKey', '请访问 89.is89.com 注册账号并获取API Key。'),
},
{
q: t('faq.q.keyNotWorking', 'Key显示无效怎么办'),
a: t(
'faq.a.keyNotWorking',
'请检查Key是否输入正确注意大小写和空格确认Key未过期确认网络连接正常。如仍有问题请联系客服。',
),
},
],
},
{
title: t('faq.sections.security', '安全相关'),
items: [
{
q: t('faq.q.isMyDataSafe', '我的数据安全吗?'),
a: t(
'faq.a.isMyDataSafe',
'89猫使用系统原生密钥链安全存储你的API Key。管理中心运行在云端你的个人数据保留在本地隐私受到充分保护。',
),
},
{
q: t('faq.q.howToScan', '如何进行安全检查?'),
a: t(
'faq.a.howToScan',
'进入"安全检查"页面,先点击"升级病毒库",升级完成后点击"开始扫描"即可对系统进行全面安全扫描。',
),
},
],
},
{
title: t('faq.sections.other', '其他'),
items: [
{
q: t('faq.q.contactUs', '如何联系客服?'),
a: t(
'faq.a.contactUs',
'请发送邮件至 public@valuecell.ai 或访问 89.is89.com 获取更多帮助。',
),
},
],
},
];
return (
<div className="mx-auto max-w-2xl space-y-8 py-4">
<div className="text-center">
<div className="flex items-center justify-center gap-2">
<HelpCircle className="h-7 w-7 text-primary" strokeWidth={1.5} />
<h1 className="text-3xl font-serif font-normal tracking-tight text-foreground">
{t('cards.faq.title')}
</h1>
</div>
<p className="mt-2 text-muted-foreground">{t('cards.faq.desc')}</p>
</div>
<div className="space-y-6">
{sections.map((section, idx) => (
<FaqSectionBlock key={idx} section={section} defaultOpen={idx === 0} />
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,92 @@
/**
* Management Center page — Gateway control + admin console link
*/
import { useTranslation } from 'react-i18next';
import { ExternalLink, Power, PowerOff, Loader2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { useGatewayStore } from '@/stores/gateway';
import { cn } from '@/lib/utils';
export function Management() {
const { t } = useTranslation('dashboard');
const gatewayStatus = useGatewayStore((s) => s.status);
const startGateway = useGatewayStore((s) => s.start);
const stopGateway = useGatewayStore((s) => s.stop);
const isRunning = gatewayStatus.state === 'running';
const isTransitioning = gatewayStatus.state === 'starting';
const handleToggle = async () => {
if (isRunning) {
await stopGateway();
} else {
await startGateway();
}
};
return (
<div className="mx-auto max-w-2xl space-y-8 py-4">
<div className="text-center">
<h1 className="text-3xl font-serif font-normal tracking-tight text-foreground">
{t('cards.management.title')}
</h1>
<p className="mt-2 text-muted-foreground">{t('cards.management.desc')}</p>
</div>
{/* Gateway Toggle Card */}
<div className="rounded-2xl border border-border/60 bg-card p-8 text-center space-y-4">
<div className="flex items-center justify-center gap-2">
<Badge
variant="outline"
className={cn(
'px-3 py-1 text-sm',
isRunning && 'border-green-500 text-green-600 dark:text-green-400',
!isRunning && 'border-red-500 text-red-600 dark:text-red-400',
)}
>
{isRunning ? t('gatewayRunning', '89猫运行中') : t('gatewayStopped', '89猫已停止')}
</Badge>
</div>
<Button
size="lg"
variant={isRunning ? 'destructive' : 'default'}
disabled={isTransitioning}
onClick={() => { void handleToggle(); }}
className="min-w-40"
>
{isTransitioning ? (
<Loader2 className="h-5 w-5 animate-spin" />
) : isRunning ? (
<PowerOff className="h-5 w-5" />
) : (
<Power className="h-5 w-5" />
)}
<span className="ml-2">
{isRunning
? t('stop89cat', '停止使用89猫')
: t('start89cat', '打开使用89猫')}
</span>
</Button>
</div>
{/* Admin Console Link */}
<div className="rounded-2xl border border-border/60 bg-card p-8 text-center space-y-4">
<h2 className="text-xl font-semibold text-foreground">
{t('adminConsole', '89超级人工智能脑管理中心')}
</h2>
<p className="text-sm text-muted-foreground">
{t('adminConsoleDesc', '管理中心运行在云端,保护您的个人隐私')}
</p>
<Button
size="lg"
variant="outline"
onClick={() => window.electron.openExternal('https://89.is89.com')}
>
<ExternalLink className="h-5 w-5" />
<span className="ml-2">{t('openAdminConsole', '前往管理中心')}</span>
</Button>
</div>
</div>
);
}

View File

@@ -0,0 +1,260 @@
/**
* Security Check page — static UI with simulated scan flow
*/
import { useState, useCallback, useRef, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { ShieldCheck, Download, Play, Check, Loader2, Clock, FileSearch } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Progress } from '@/components/ui/progress';
import { cn } from '@/lib/utils';
type ScanStep = 'idle' | 'updating' | 'ready' | 'scanning' | 'done';
const SCAN_PATHS = [
'C:\\Windows\\System32\\',
'C:\\Program Files\\',
'C:\\Users\\Documents\\',
'D:\\Downloads\\',
'C:\\Temp\\',
];
const getRandomThreats = () => Math.floor(Math.random() * 5);
export function Security() {
const { t } = useTranslation('dashboard');
const [step, setStep] = useState<ScanStep>('idle');
const [updateProgress, setUpdateProgress] = useState(0);
const [scanProgress, setScanProgress] = useState(0);
const [currentPath, setCurrentPath] = useState('');
const [filesScanned, setFilesScanned] = useState(0);
const [threatsFound, setThreatsFound] = useState(0);
const [scanTime, setScanTime] = useState(0);
const [lastScanTime, setLastScanTime] = useState<string>(t('security.neverScanned'));
const [virusDbVersion, setVirusDbVersion] = useState('2026.06.01-v1');
const updateTimerRef = useRef<ReturnType<typeof setInterval>>();
const scanTimerRef = useRef<ReturnType<typeof setInterval>>();
useEffect(() => {
return () => {
clearInterval(updateTimerRef.current);
clearInterval(scanTimerRef.current);
};
}, []);
const handleUpdateDb = useCallback(() => {
setStep('updating');
setUpdateProgress(0);
updateTimerRef.current = setInterval(() => {
setUpdateProgress((prev) => {
if (prev >= 100) {
clearInterval(updateTimerRef.current);
setStep('ready');
setVirusDbVersion(`2026.06.04-v${Math.floor(Math.random() * 10) + 1}`);
return 100;
}
return prev + Math.random() * 15 + 5;
});
}, 300);
}, []);
const handleStartScan = useCallback(() => {
setStep('scanning');
setScanProgress(0);
setFilesScanned(0);
setThreatsFound(0);
setScanTime(0);
setCurrentPath(SCAN_PATHS[0]);
const startTime = Date.now();
let pathIndex = 0;
scanTimerRef.current = setInterval(() => {
setScanProgress((prev) => {
const next = prev + Math.random() * 4 + 1;
if (next >= 100) {
clearInterval(scanTimerRef.current);
setStep('done');
setFilesScanned(Math.floor(Math.random() * 5000) + 10000);
setThreatsFound(getRandomThreats());
setScanTime(Math.floor((Date.now() - startTime) / 1000));
setLastScanTime(new Date().toLocaleString());
setCurrentPath('');
return 100;
}
// Rotate scan paths
const newPathIndex = Math.min(
Math.floor((next / 100) * SCAN_PATHS.length),
SCAN_PATHS.length - 1,
);
if (newPathIndex !== pathIndex) {
pathIndex = newPathIndex;
setCurrentPath(SCAN_PATHS[pathIndex]);
}
setFilesScanned((prev2) => prev2 + Math.floor(Math.random() * 50) + 10);
return next;
});
}, 200);
}, []);
const getStepLabel = () => {
switch (step) {
case 'idle':
return t('security.idle');
case 'updating':
return t('security.updatingDb');
case 'ready':
return t('security.readyToScan');
case 'scanning':
return t('security.scanning');
case 'done':
return t('security.scanComplete');
}
};
return (
<div className="mx-auto max-w-2xl space-y-8 py-4">
<div className="text-center">
<h1 className="text-3xl font-serif font-normal tracking-tight text-foreground">
{t('cards.security.title')}
</h1>
<p className="mt-2 text-muted-foreground">{t('cards.security.desc')}</p>
</div>
{/* Virus DB Status */}
<div className="rounded-2xl border border-border/60 bg-card p-6 space-y-4">
<div className="flex items-center justify-between">
<div>
<h3 className="font-semibold text-foreground">{t('security.virusDbVersion')}</h3>
<p className="text-sm text-muted-foreground">{virusDbVersion}</p>
</div>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Clock className="h-4 w-4" />
<span>
{t('security.lastScan')}: {lastScanTime}
</span>
</div>
</div>
<Button
onClick={handleUpdateDb}
disabled={step === 'updating' || step === 'scanning'}
className="w-full"
variant="outline"
>
{step === 'updating' ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Download className="h-4 w-4" />
)}
<span className="ml-2">{t('security.updateDb')}</span>
</Button>
{step === 'updating' && (
<div className="space-y-1">
<Progress value={updateProgress} className="h-2" />
<p className="text-xs text-muted-foreground text-right">
{Math.floor(updateProgress)}%
</p>
</div>
)}
</div>
{/* Scan Section */}
<div className="rounded-2xl border border-border/60 bg-card p-6 space-y-4">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-primary/10 text-primary">
<ShieldCheck className="h-5 w-5" strokeWidth={1.5} />
</div>
<div>
<h3 className="font-semibold text-foreground">{getStepLabel()}</h3>
<p className="text-sm text-muted-foreground">
{step === 'idle' && t('security.updateDbFirst', '请先升级病毒库')}
{step === 'ready' && t('security.clickToScan', '病毒库已就绪,可以开始扫描')}
{step === 'scanning' && currentPath}
{step === 'done' && t('security.scanResult', '扫描已完成')}
</p>
</div>
</div>
<Button
onClick={handleStartScan}
disabled={step !== 'ready' || step === 'scanning'}
className="w-full"
variant={step === 'done' ? 'outline' : 'default'}
>
{step === 'scanning' ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Play className="h-4 w-4" />
)}
<span className="ml-2">{t('security.startScan')}</span>
</Button>
{/* Scan Progress */}
{(step === 'scanning' || step === 'done') && (
<div className="space-y-3">
<Progress
value={scanProgress}
className={cn('h-2', step === 'done' && '[&>div]:bg-green-500')}
/>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">
<FileSearch className="inline h-3.5 w-3.5 mr-1" />
{t('security.filesScanned')}: {filesScanned.toLocaleString()}
</span>
<span className="text-muted-foreground">
{Math.floor(scanProgress)}%
</span>
</div>
</div>
)}
{/* Result */}
{step === 'done' && (
<div
className={cn(
'rounded-xl border p-4 text-center',
threatsFound > 0
? 'border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-950'
: 'border-green-200 bg-green-50 dark:border-green-800 dark:bg-green-950',
)}
>
<div className="flex items-center justify-center gap-2">
<Check
className={cn(
'h-5 w-5',
threatsFound > 0
? 'text-red-600 dark:text-red-400'
: 'text-green-600 dark:text-green-400',
)}
/>
<span
className={cn(
'font-semibold',
threatsFound > 0
? 'text-red-700 dark:text-red-300'
: 'text-green-700 dark:text-green-300',
)}
>
{threatsFound > 0
? t('security.threatsFound', { count: threatsFound })
: t('security.noThreats')}
</span>
</div>
<p className="mt-1 text-sm text-muted-foreground">
{t('security.scanSummary', {
files: filesScanned.toLocaleString(),
time: scanTime,
})}
</p>
</div>
)}
</div>
</div>
);
}

View File

@@ -268,7 +268,7 @@ function WelcomeContent() {
return (
<div data-testid="setup-welcome-step" className="text-center space-y-4">
<div className="mb-4 flex justify-center">
<img src={clawxIcon} alt="ClawX" className="h-16 w-16" />
<img src={clawxIcon} alt="89猫" className="h-16 w-16" />
</div>
<h2 className="text-xl font-serif font-normal tracking-tight">{t('welcome.title')}</h2>
<p className="text-muted-foreground">

View File

@@ -343,6 +343,7 @@ export function Skills() {
};
}, []);
const [categoryFilter, setCategoryFilter] = useState<'all' | 'cat89' | 'openclaw' | 'other'>('all');
const safeSkills = Array.isArray(skills) ? skills : [];
const enabledSkillsCount = safeSkills.filter((skill) => skill.enabled).length;
const disabledSkillsCount = safeSkills.filter((skill) => !skill.enabled).length;
@@ -356,7 +357,11 @@ export function Skills() {
|| (skill.author || '').toLowerCase().includes(q);
const matchesStatus = statusFilter === 'all'
|| (statusFilter === 'enabled' ? skill.enabled : !skill.enabled);
return matchesSearch && matchesStatus;
const matchesCategory = categoryFilter === 'all'
|| (categoryFilter === 'cat89' && ((skill as any).isCat89Exclusive || skill.source === 'cat89-plugin'))
|| (categoryFilter === 'openclaw' && (skill.source || '').startsWith('openclaw-'))
|| (categoryFilter === 'other' && !(skill.source || '').startsWith('openclaw-') && !((skill as any).isCat89Exclusive) && skill.source !== 'cat89-plugin');
return matchesSearch && matchesStatus && matchesCategory;
}).sort((a, b) => {
if (a.enabled && !b.enabled) return -1;
if (!a.enabled && b.enabled) return 1;
@@ -535,6 +540,27 @@ export function Skills() {
</div>
)}
{/* Category Tabs */}
<div className="flex items-center gap-1 mb-4 shrink-0">
{(['all', 'cat89', 'openclaw', 'other'] as const).map((cat) => (
<Button
key={cat}
type="button"
variant="ghost"
size="sm"
onClick={() => setCategoryFilter(cat)}
className={cn(
'h-8 rounded-full px-4 text-meta font-medium border shadow-none',
categoryFilter === cat
? 'bg-black/5 dark:bg-white/10 border-black/10 dark:border-white/10 text-foreground'
: 'bg-transparent border-transparent text-muted-foreground hover:text-foreground hover:bg-black/5 dark:hover:bg-white/5',
)}
>
{t(`filter.category.${cat}`)}
</Button>
))}
</div>
{/* Sub Navigation and Actions */}
<div className="flex flex-col md:flex-row md:items-center justify-between border-b border-black/10 dark:border-white/10 pb-4 mb-4 shrink-0 gap-4">
<div className="flex items-center flex-wrap gap-2 text-sm">

View File

@@ -51,9 +51,12 @@ export default defineConfig({
{
// Main process entry file
entry: 'electron/main/index.ts',
args: ['--remote-debugging-port=9222'],
onstart(options) {
options.startup();
const cdpPort = process.env.CLAWX_CDP_PORT;
const args = cdpPort && cdpPort !== '0'
? ['.', `--remote-debugging-port=${cdpPort}`]
: [];
options.startup(args);
},
vite: {
build: {