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

- 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:
iven
2026-04-05 16:12:06 +08:00
parent 7e56b40972
commit 9ee89ff67c
7 changed files with 97 additions and 21 deletions

View File

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

View File

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

View File

@@ -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]);

View File

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

View File

@@ -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 */}

View File

@@ -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. */

View File

@@ -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: [] };
} }