fix(desktop): 功能验证 6 项缺陷修复
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
- ISS-002: SkillInfoResponse 增加 source/path 字段,修复技能系统显示 0 个 - ISS-003: Sidebar 添加自动化/技能市场导航入口 + App 返回按钮 - ISS-004: SaaS fetchAvailableModels 添加 .catch() 防限流崩溃 - ISS-006: SaaSSettings/PricingPage 包裹 ErrorBoundary 防白屏 - ISS-008: listModels 加载 localStorage 自定义模型,修复仅显示 1 个模型 - configStore listSkills 映射添加 source/path 转发
This commit is contained in:
@@ -30,6 +30,14 @@ pub struct SkillInfoResponse {
|
|||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
pub triggers: Vec<String>,
|
pub triggers: Vec<String>,
|
||||||
pub category: Option<String>,
|
pub category: Option<String>,
|
||||||
|
#[serde(default = "default_source")]
|
||||||
|
pub source: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub path: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_source() -> String {
|
||||||
|
"builtin".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<zclaw_skills::SkillManifest> for SkillInfoResponse {
|
impl From<zclaw_skills::SkillManifest> for SkillInfoResponse {
|
||||||
@@ -45,6 +53,8 @@ impl From<zclaw_skills::SkillManifest> for SkillInfoResponse {
|
|||||||
enabled: manifest.enabled,
|
enabled: manifest.enabled,
|
||||||
triggers: manifest.triggers,
|
triggers: manifest.triggers,
|
||||||
category: manifest.category,
|
category: manifest.category,
|
||||||
|
source: "builtin".to_string(),
|
||||||
|
path: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -486,6 +486,16 @@ function App() {
|
|||||||
animate="animate"
|
animate="animate"
|
||||||
className="h-full overflow-y-auto"
|
className="h-full overflow-y-auto"
|
||||||
>
|
>
|
||||||
|
<div className="sticky top-0 z-10 flex items-center gap-2 px-4 py-2 bg-white/80 dark:bg-gray-900/80 backdrop-blur-sm border-b border-gray-200 dark:border-gray-800">
|
||||||
|
<button
|
||||||
|
onClick={() => handleMainViewChange('chat')}
|
||||||
|
className="p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-md transition-colors text-gray-600 dark:text-gray-400"
|
||||||
|
title="返回聊天"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m15 18-6-6 6-6"/></svg>
|
||||||
|
</button>
|
||||||
|
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">自动化</span>
|
||||||
|
</div>
|
||||||
<AutomationPanel />
|
<AutomationPanel />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
) : mainContentView === 'skills' ? (
|
) : mainContentView === 'skills' ? (
|
||||||
@@ -495,7 +505,19 @@ function App() {
|
|||||||
animate="animate"
|
animate="animate"
|
||||||
className="h-full overflow-hidden"
|
className="h-full overflow-hidden"
|
||||||
>
|
>
|
||||||
<SkillMarket />
|
<div className="flex items-center gap-2 px-4 py-2 border-b border-gray-200 dark:border-gray-800">
|
||||||
|
<button
|
||||||
|
onClick={() => handleMainViewChange('chat')}
|
||||||
|
className="p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-md transition-colors text-gray-600 dark:text-gray-400"
|
||||||
|
title="返回聊天"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m15 18-6-6 6-6"/></svg>
|
||||||
|
</button>
|
||||||
|
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">技能市场</span>
|
||||||
|
</div>
|
||||||
|
<div className="h-[calc(100%-40px)] overflow-auto">
|
||||||
|
<SkillMarket />
|
||||||
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
) : (
|
) : (
|
||||||
<ChatArea />
|
<ChatArea />
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ export function SaaSStatus({ isLoggedIn, account, saasUrl, onLogout, onLogin }:
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isLoggedIn) {
|
if (isLoggedIn) {
|
||||||
fetchAvailableModels();
|
fetchAvailableModels().catch(() => {
|
||||||
|
// Silently handle SaaS API errors — they should not crash the app
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [isLoggedIn, fetchAvailableModels]);
|
}, [isLoggedIn, fetchAvailableModels]);
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import { SecureStorage } from './SecureStorage';
|
|||||||
import { VikingPanel } from '../VikingPanel';
|
import { VikingPanel } from '../VikingPanel';
|
||||||
import { SaaSSettings } from '../SaaS/SaaSSettings';
|
import { SaaSSettings } from '../SaaS/SaaSSettings';
|
||||||
import { PricingPage } from '../SaaS/PricingPage';
|
import { PricingPage } from '../SaaS/PricingPage';
|
||||||
|
import { ErrorBoundary } from '../ui/ErrorBoundary';
|
||||||
|
|
||||||
interface SettingsLayoutProps {
|
interface SettingsLayoutProps {
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
@@ -105,8 +106,20 @@ export function SettingsLayout({ onBack }: SettingsLayoutProps) {
|
|||||||
case 'workspace': return <Workspace />;
|
case 'workspace': return <Workspace />;
|
||||||
case 'privacy': return <Privacy />;
|
case 'privacy': return <Privacy />;
|
||||||
case 'storage': return <SecureStorage />;
|
case 'storage': return <SecureStorage />;
|
||||||
case 'saas': return <SaaSSettings />;
|
case 'saas': return (
|
||||||
case 'billing': return <PricingPage />;
|
<ErrorBoundary
|
||||||
|
fallback={<div className="p-6 text-center text-gray-500">SaaS 平台加载失败,请稍后重试</div>}
|
||||||
|
>
|
||||||
|
<SaaSSettings />
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
case 'billing': return (
|
||||||
|
<ErrorBoundary
|
||||||
|
fallback={<div className="p-6 text-center text-gray-500">计费信息加载失败,请稍后重试</div>}
|
||||||
|
>
|
||||||
|
<PricingPage />
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
case 'security': return (
|
case 'security': return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import {
|
import {
|
||||||
SquarePen, MessageSquare, Bot, Search, X, Settings
|
SquarePen, MessageSquare, Bot, Search, X, Settings, Zap, Sparkles
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { ConversationList } from './ConversationList';
|
import { ConversationList } from './ConversationList';
|
||||||
import { CloneManager } from './CloneManager';
|
import { CloneManager } from './CloneManager';
|
||||||
@@ -33,11 +33,7 @@ export function Sidebar({
|
|||||||
|
|
||||||
const handleNavClick = (tab: Tab) => {
|
const handleNavClick = (tab: Tab) => {
|
||||||
setActiveTab(tab);
|
setActiveTab(tab);
|
||||||
if (tab === 'clones') {
|
onMainViewChange?.('chat');
|
||||||
onMainViewChange?.('chat');
|
|
||||||
} else {
|
|
||||||
onMainViewChange?.('chat');
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -85,6 +81,24 @@ export function Sidebar({
|
|||||||
<Bot className="w-4 h-4" />
|
<Bot className="w-4 h-4" />
|
||||||
智能体
|
智能体
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* Divider between primary nav and secondary tools */}
|
||||||
|
<div className="mx-1 my-1 border-t border-[#e8e6e1]/40 dark:border-gray-800" />
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => onMainViewChange?.('automation')}
|
||||||
|
className="w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm text-gray-600 dark:text-gray-400 hover:bg-black/5 dark:hover:bg-white/5 transition-colors"
|
||||||
|
>
|
||||||
|
<Zap className="w-4 h-4" />
|
||||||
|
自动化
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => onMainViewChange?.('skills')}
|
||||||
|
className="w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm text-gray-600 dark:text-gray-400 hover:bg-black/5 dark:hover:bg-white/5 transition-colors"
|
||||||
|
>
|
||||||
|
<Sparkles className="w-4 h-4" />
|
||||||
|
技能市场
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Divider */}
|
{/* Divider */}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ type SkillItem = {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
triggers: string[];
|
triggers: string[];
|
||||||
category?: string;
|
category?: string;
|
||||||
|
source?: string;
|
||||||
|
path?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Skill list container shared by list/refresh responses. */
|
/** Skill list container shared by list/refresh responses. */
|
||||||
|
|||||||
@@ -643,27 +643,26 @@ function createConfigClientFromKernel(client: KernelClient): ConfigStoreClient {
|
|||||||
const result = await client.listSkills();
|
const result = await client.listSkills();
|
||||||
if (result?.skills) {
|
if (result?.skills) {
|
||||||
return {
|
return {
|
||||||
skills: result.skills.map((s) => ({
|
skills: result.skills.map((s: { id: string; name: string; description?: string; version: string; capabilities?: string[]; tags?: string[]; mode: string; enabled?: boolean; triggers?: string[]; category?: string; source?: string; path?: string }) => ({
|
||||||
id: s.id,
|
id: s.id,
|
||||||
name: s.name,
|
name: s.name,
|
||||||
description: s.description || '',
|
description: s.description || '',
|
||||||
version: s.version,
|
version: s.version,
|
||||||
// Use capabilities directly
|
|
||||||
capabilities: s.capabilities || [],
|
capabilities: s.capabilities || [],
|
||||||
tags: s.tags || [],
|
tags: s.tags || [],
|
||||||
mode: s.mode,
|
mode: s.mode,
|
||||||
// Map triggers to the expected format
|
|
||||||
triggers: (s.triggers || []).map((t: string) => ({
|
triggers: (s.triggers || []).map((t: string) => ({
|
||||||
type: 'keyword',
|
type: 'keyword',
|
||||||
pattern: t,
|
pattern: t,
|
||||||
})),
|
})),
|
||||||
// Create actions from capabilities for UI display
|
|
||||||
actions: (s.capabilities || []).map((cap: string) => ({
|
actions: (s.capabilities || []).map((cap: string) => ({
|
||||||
type: cap,
|
type: cap,
|
||||||
params: undefined,
|
params: undefined,
|
||||||
})),
|
})),
|
||||||
enabled: s.enabled ?? true,
|
enabled: s.enabled ?? true,
|
||||||
category: s.category,
|
category: s.category,
|
||||||
|
source: (s.source as 'builtin' | 'extra') || 'builtin',
|
||||||
|
path: s.path || undefined,
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -753,13 +752,27 @@ function createConfigClientFromKernel(client: KernelClient): ConfigStoreClient {
|
|||||||
listModels: async () => {
|
listModels: async () => {
|
||||||
try {
|
try {
|
||||||
const status = await client.status();
|
const status = await client.status();
|
||||||
return {
|
const defaultModel = status.defaultModel ? [{
|
||||||
models: status.defaultModel ? [{
|
id: status.defaultModel as string,
|
||||||
id: status.defaultModel as string,
|
name: status.defaultModel as string,
|
||||||
name: status.defaultModel as string,
|
provider: (status.defaultProvider as string) || 'default',
|
||||||
provider: (status.defaultProvider as string) || 'default',
|
}] : [];
|
||||||
}] : [],
|
// Load custom models from localStorage
|
||||||
};
|
const customModels: Array<{ id: string; name: string; provider?: string }> = [];
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem('zclaw-custom-models');
|
||||||
|
if (raw) {
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
if (Array.isArray(parsed)) {
|
||||||
|
for (const m of parsed) {
|
||||||
|
if (m.id && m.name) {
|
||||||
|
customModels.push({ id: m.id, name: m.name, provider: m.provider || 'custom' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch { /* ignore parse errors */ }
|
||||||
|
return { models: [...defaultModel, ...customModels] };
|
||||||
} catch {
|
} catch {
|
||||||
return { models: [] };
|
return { models: [] };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user