feat: complete 89cat rebrand, token server, API validation, skills tabs
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

- Phase 1: rename app identifiers (package/electron-builder/main process)
- Phase 4: cat89 provider + port-pinger for api2.is89.com:898902-898909
- Phase 5: skills page category tabs (89 plugins/OpenClaw/other)
- Phase 7: /api/cat89/validate-key endpoint with auto-ping

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
zyh
2026-06-04 10:54:47 +08:00
parent 48e65882d6
commit b7349c6ebf
12 changed files with 157 additions and 25 deletions

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

@@ -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

@@ -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

@@ -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

@@ -42,13 +42,10 @@ function ApiKeyCard() {
setErrorMsg('');
try {
const res = await hostApiFetch<{ valid: boolean; message?: string }>(
'/api/provider-accounts/validate',
'/api/cat89/validate-key',
{
method: 'POST',
body: JSON.stringify({
vendorId: 'cat89',
apiKey: trimmed,
}),
body: JSON.stringify({ apiKey: trimmed }),
},
);
if (res.valid) {

View File

@@ -356,7 +356,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;
@@ -423,6 +427,7 @@ export function Skills() {
}
}, [t]);
const [categoryFilter, setCategoryFilter] = useState<'all' | 'cat89' | 'openclaw' | 'other'>('all');
const [skillsDirPath, setSkillsDirPath] = useState('~/.openclaw/skills');
useEffect(() => {
@@ -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">