feat: complete 89cat rebrand, token server, API validation, skills tabs
- 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:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
57
electron/services/port-pinger.ts
Normal file
57
electron/services/port-pinger.ts
Normal 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 };
|
||||
@@ -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(
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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...",
|
||||
|
||||
@@ -14,7 +14,13 @@
|
||||
"builtIn": "内置 ({{count}})",
|
||||
"marketplace": "市场 ({{count}})",
|
||||
"enabledList": "已开启 ({{count}})",
|
||||
"disabledList": "已禁用 ({{count}})"
|
||||
"disabledList": "已禁用 ({{count}})",
|
||||
"category": {
|
||||
"all": "全部",
|
||||
"cat89": "89插件",
|
||||
"openclaw": "OpenClaw 技能",
|
||||
"other": "其他插件"
|
||||
}
|
||||
},
|
||||
"search": "搜索技能...",
|
||||
"searchMarketplace": "搜索市场...",
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user