fix(ui): 5 项 E2E 测试 Bug 修复 — Agent 502 / 错误持久化 / 模型标记 / 侧面板 / 记忆页
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
- BUG-01: createFromTemplate 在 saas-relay 模式下 try-catch 跳过本地 Kernel - BUG-02: upsertActiveConversation 持久化前剥离 error/streaming/optimistic 字段 - BUG-04: ModelSelector 添加 available 标记,ChatArea 追踪失败模型 ID - BUG-05: VikingPanel 移除 status?.available 门控,不可用时 disabled + 重连按钮 - BUG-06: 侧面板 tooltip 改为"查看产物文件",空状态增加图标和说明
This commit is contained in:
@@ -72,13 +72,27 @@ export function ChatArea({ compact, onOpenDetail }: { compact?: boolean; onOpenD
|
|||||||
const saasModels = useSaaSStore((s) => s.availableModels);
|
const saasModels = useSaaSStore((s) => s.availableModels);
|
||||||
const isLoggedIn = useSaaSStore((s) => s.isLoggedIn);
|
const isLoggedIn = useSaaSStore((s) => s.isLoggedIn);
|
||||||
|
|
||||||
|
// Track models that failed with API key errors in this session
|
||||||
|
const failedModelIds = useRef<Set<string>>(new Set());
|
||||||
|
|
||||||
|
// Scan messages for API key errors to populate failedModelIds
|
||||||
|
useEffect(() => {
|
||||||
|
for (const msg of messages) {
|
||||||
|
if (msg.error && (msg.error.includes('没有可用的 API Key') || msg.error.includes('Key Pool'))) {
|
||||||
|
failedModelIds.current.add(currentModel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [messages, currentModel]);
|
||||||
|
|
||||||
// Merge models: SaaS available models take priority when logged in
|
// Merge models: SaaS available models take priority when logged in
|
||||||
const models = useMemo(() => {
|
const models = useMemo(() => {
|
||||||
|
const failed = failedModelIds.current;
|
||||||
if (isLoggedIn && saasModels.length > 0) {
|
if (isLoggedIn && saasModels.length > 0) {
|
||||||
return saasModels.map(m => ({
|
return saasModels.map(m => ({
|
||||||
id: m.alias || m.id,
|
id: m.alias || m.id,
|
||||||
name: m.alias || m.id,
|
name: m.alias || m.id,
|
||||||
provider: m.provider_id,
|
provider: m.provider_id,
|
||||||
|
available: !failed.has(m.alias || m.id),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
if (configModels.length > 0) {
|
if (configModels.length > 0) {
|
||||||
|
|||||||
@@ -196,68 +196,89 @@ export function VikingPanel() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Storage Info */}
|
{/* Storage Info */}
|
||||||
{status?.available && (
|
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-4 mb-6 shadow-sm">
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-4 mb-6 shadow-sm">
|
<div className="flex items-center gap-3 mb-3">
|
||||||
<div className="flex items-center gap-3 mb-3">
|
<div className={`w-10 h-10 rounded-xl flex items-center justify-center ${status?.available ? 'bg-gradient-to-br from-blue-500 to-indigo-500' : 'bg-gray-300 dark:bg-gray-600'}`}>
|
||||||
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-blue-500 to-indigo-500 flex items-center justify-center">
|
<Database className="w-4 h-4 text-white" />
|
||||||
<Database className="w-4 h-4 text-white" />
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium text-gray-900 dark:text-white">
|
||||||
|
本地存储
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
<div className="text-sm font-medium text-gray-900 dark:text-white">
|
{status?.available
|
||||||
本地存储
|
? `${status.version || 'Native'} · ${status.dataDir || '默认路径'}`
|
||||||
</div>
|
: '存储未连接'}
|
||||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
{status.version || 'Native'} · {status.dataDir || '默认路径'}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-4 text-xs">
|
{!status?.available && (
|
||||||
<div className="flex items-center gap-1.5 text-gray-600 dark:text-gray-300">
|
<button
|
||||||
<CheckCircle className="w-3.5 h-3.5 text-green-500" />
|
onClick={loadStatus}
|
||||||
<span>SQLite + FTS5</span>
|
disabled={isLoading}
|
||||||
</div>
|
className="ml-auto text-xs text-amber-600 dark:text-amber-400 hover:text-amber-700 dark:hover:text-amber-300 flex items-center gap-1 disabled:opacity-50"
|
||||||
<div className="flex items-center gap-1.5 text-gray-600 dark:text-gray-300">
|
>
|
||||||
<CheckCircle className="w-3.5 h-3.5 text-green-500" />
|
<RefreshCw className={`w-3 h-3 ${isLoading ? 'animate-spin' : ''}`} /> 重新连接
|
||||||
<span>TF-IDF 语义评分</span>
|
</button>
|
||||||
</div>
|
)}
|
||||||
{memoryCount !== null && (
|
|
||||||
<div className="flex items-center gap-1.5 text-gray-600 dark:text-gray-300">
|
|
||||||
<CheckCircle className="w-3.5 h-3.5 text-green-500" />
|
|
||||||
<span>{memoryCount} 条记忆</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="flex gap-4 text-xs">
|
||||||
|
<div className="flex items-center gap-1.5 text-gray-600 dark:text-gray-300">
|
||||||
|
{status?.available ? (
|
||||||
|
<CheckCircle className="w-3.5 h-3.5 text-green-500" />
|
||||||
|
) : (
|
||||||
|
<AlertCircle className="w-3.5 h-3.5 text-amber-500" />
|
||||||
|
)}
|
||||||
|
<span>SQLite + FTS5</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5 text-gray-600 dark:text-gray-300">
|
||||||
|
{status?.available ? (
|
||||||
|
<CheckCircle className="w-3.5 h-3.5 text-green-500" />
|
||||||
|
) : (
|
||||||
|
<AlertCircle className="w-3.5 h-3.5 text-amber-500" />
|
||||||
|
)}
|
||||||
|
<span>TF-IDF 语义评分</span>
|
||||||
|
</div>
|
||||||
|
{memoryCount !== null && (
|
||||||
|
<div className="flex items-center gap-1.5 text-gray-600 dark:text-gray-300">
|
||||||
|
<CheckCircle className="w-3.5 h-3.5 text-green-500" />
|
||||||
|
<span>{memoryCount} 条记忆</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Search Box */}
|
{/* Search Box */}
|
||||||
{status?.available && (
|
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-4 mb-6 shadow-sm">
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-4 mb-6 shadow-sm">
|
<h3 className="text-sm font-medium text-gray-900 dark:text-white mb-3">语义搜索</h3>
|
||||||
<h3 className="text-sm font-medium text-gray-900 dark:text-white mb-3">语义搜索</h3>
|
{!status?.available && (
|
||||||
<div className="flex gap-2">
|
<p className="text-xs text-amber-600 dark:text-amber-400 mb-2 flex items-center gap-1">
|
||||||
<input
|
<AlertCircle className="w-3 h-3" /> 存储未连接,搜索功能不可用
|
||||||
type="text"
|
</p>
|
||||||
value={searchQuery}
|
)}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
<div className="flex gap-2">
|
||||||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
<input
|
||||||
placeholder="输入自然语言查询..."
|
type="text"
|
||||||
className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
value={searchQuery}
|
||||||
/>
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
<button
|
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||||
onClick={handleSearch}
|
placeholder="输入自然语言查询..."
|
||||||
disabled={isSearching || !searchQuery.trim()}
|
disabled={!status?.available}
|
||||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 flex items-center gap-2 text-sm"
|
className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
/>
|
||||||
{isSearching ? (
|
<button
|
||||||
<RefreshCw className="w-4 h-4 animate-spin" />
|
onClick={handleSearch}
|
||||||
) : (
|
disabled={isSearching || !searchQuery.trim() || !status?.available}
|
||||||
<Search className="w-4 h-4" />
|
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 flex items-center gap-2 text-sm"
|
||||||
)}
|
>
|
||||||
搜索
|
{isSearching ? (
|
||||||
</button>
|
<RefreshCw className="w-4 h-4 animate-spin" />
|
||||||
</div>
|
) : (
|
||||||
|
<Search className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
搜索
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
{/* Search Results */}
|
{/* Search Results */}
|
||||||
{searchResults.length > 0 && (
|
{searchResults.length > 0 && (
|
||||||
@@ -385,59 +406,64 @@ export function VikingPanel() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Summary Generation */}
|
{/* Summary Generation */}
|
||||||
{status?.available && (
|
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-4 mb-6 shadow-sm">
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-4 mb-6 shadow-sm">
|
<h3 className="text-sm font-medium text-gray-900 dark:text-white mb-3">智能摘要</h3>
|
||||||
<h3 className="text-sm font-medium text-gray-900 dark:text-white mb-3">智能摘要</h3>
|
<p className="text-xs text-gray-500 dark:text-gray-400 mb-3">
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-3">
|
存储资源并自动通过 LLM 生成 L0/L1 多级摘要(需配置摘要驱动)
|
||||||
存储资源并自动通过 LLM 生成 L0/L1 多级摘要(需配置摘要驱动)
|
</p>
|
||||||
|
{!status?.available && (
|
||||||
|
<p className="text-xs text-amber-600 dark:text-amber-400 mb-2 flex items-center gap-1">
|
||||||
|
<AlertCircle className="w-3 h-3" /> 存储未连接,摘要功能不可用
|
||||||
</p>
|
</p>
|
||||||
<div className="space-y-2">
|
)}
|
||||||
<input
|
<div className="space-y-2">
|
||||||
type="text"
|
<input
|
||||||
value={summaryUri}
|
type="text"
|
||||||
onChange={(e) => setSummaryUri(e.target.value)}
|
value={summaryUri}
|
||||||
placeholder="资源 URI (如: notes/project-plan)"
|
onChange={(e) => setSummaryUri(e.target.value)}
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
placeholder="资源 URI (如: notes/project-plan)"
|
||||||
/>
|
disabled={!status?.available}
|
||||||
<textarea
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
value={summaryContent}
|
/>
|
||||||
onChange={(e) => setSummaryContent(e.target.value)}
|
<textarea
|
||||||
placeholder="资源内容..."
|
value={summaryContent}
|
||||||
rows={3}
|
onChange={(e) => setSummaryContent(e.target.value)}
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
|
placeholder="资源内容..."
|
||||||
/>
|
rows={3}
|
||||||
<button
|
disabled={!status?.available}
|
||||||
onClick={async () => {
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
if (!summaryUri.trim() || !summaryContent.trim()) return;
|
/>
|
||||||
setIsGeneratingSummary(true);
|
<button
|
||||||
setMessage(null);
|
onClick={async () => {
|
||||||
try {
|
if (!summaryUri.trim() || !summaryContent.trim()) return;
|
||||||
await storeWithSummaries(summaryUri, summaryContent);
|
setIsGeneratingSummary(true);
|
||||||
setMessage({ type: 'success', text: `摘要生成完成: ${summaryUri}` });
|
setMessage(null);
|
||||||
setSummaryUri('');
|
try {
|
||||||
setSummaryContent('');
|
await storeWithSummaries(summaryUri, summaryContent);
|
||||||
} catch (error) {
|
setMessage({ type: 'success', text: `摘要生成完成: ${summaryUri}` });
|
||||||
setMessage({
|
setSummaryUri('');
|
||||||
type: 'error',
|
setSummaryContent('');
|
||||||
text: `摘要生成失败: ${error instanceof Error ? error.message : '未知错误'}`,
|
} catch (error) {
|
||||||
});
|
setMessage({
|
||||||
} finally {
|
type: 'error',
|
||||||
setIsGeneratingSummary(false);
|
text: `摘要生成失败: ${error instanceof Error ? error.message : '未知错误'}`,
|
||||||
}
|
});
|
||||||
}}
|
} finally {
|
||||||
disabled={isGeneratingSummary || !summaryUri.trim() || !summaryContent.trim()}
|
setIsGeneratingSummary(false);
|
||||||
className="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 disabled:opacity-50 flex items-center gap-2 text-sm"
|
}
|
||||||
>
|
}}
|
||||||
{isGeneratingSummary ? (
|
disabled={isGeneratingSummary || !summaryUri.trim() || !summaryContent.trim() || !status?.available}
|
||||||
<RefreshCw className="w-4 h-4 animate-spin" />
|
className="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 disabled:opacity-50 flex items-center gap-2 text-sm"
|
||||||
) : (
|
>
|
||||||
<Sparkles className="w-4 h-4" />
|
{isGeneratingSummary ? (
|
||||||
)}
|
<RefreshCw className="w-4 h-4 animate-spin" />
|
||||||
生成摘要并存储
|
) : (
|
||||||
</button>
|
<Sparkles className="w-4 h-4" />
|
||||||
</div>
|
)}
|
||||||
|
生成摘要并存储
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
{/* Info Section */}
|
{/* Info Section */}
|
||||||
<div className="mt-6 p-4 bg-gray-50 dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-gray-700">
|
<div className="mt-6 p-4 bg-gray-50 dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||||
|
|||||||
@@ -87,10 +87,14 @@ export function ArtifactPanel({
|
|||||||
<div className={`h-full flex flex-col ${className}`}>
|
<div className={`h-full flex flex-col ${className}`}>
|
||||||
<div className="p-4 flex-1 overflow-y-auto custom-scrollbar">
|
<div className="p-4 flex-1 overflow-y-auto custom-scrollbar">
|
||||||
{artifacts.length === 0 ? (
|
{artifacts.length === 0 ? (
|
||||||
<div className="flex flex-col items-center justify-center h-full text-gray-400 dark:text-gray-500">
|
<div className="flex flex-col items-center justify-center h-full text-gray-400 dark:text-gray-500 gap-3">
|
||||||
<FileText className="w-8 h-8 mb-2 opacity-50" />
|
<div className="w-14 h-14 rounded-2xl bg-gray-100 dark:bg-gray-800 flex items-center justify-center">
|
||||||
<p className="text-sm">暂无产物文件</p>
|
<FileText className="w-7 h-7 opacity-40" />
|
||||||
<p className="text-xs mt-1">Agent 生成文件后将在此显示</p>
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-sm font-medium">暂无产物文件</p>
|
||||||
|
<p className="text-xs mt-1.5 text-gray-400 dark:text-gray-500">Agent 生成代码、文档等文件后<br />将在此处显示,可实时预览</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useRef, useEffect } from 'react';
|
import { useState, useRef, useEffect } from 'react';
|
||||||
import { ChevronDown, Check } from 'lucide-react';
|
import { ChevronDown, Check, AlertTriangle } from 'lucide-react';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -15,6 +15,8 @@ interface ModelOption {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
provider?: string;
|
provider?: string;
|
||||||
|
/** If false, this model has no API key configured or previously failed */
|
||||||
|
available?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ModelSelectorProps {
|
interface ModelSelectorProps {
|
||||||
@@ -99,7 +101,9 @@ export function ModelSelector({
|
|||||||
{/* Model list */}
|
{/* Model list */}
|
||||||
<div className="max-h-48 overflow-y-auto py-1" role="listbox">
|
<div className="max-h-48 overflow-y-auto py-1" role="listbox">
|
||||||
{filteredModels.length > 0 ? (
|
{filteredModels.length > 0 ? (
|
||||||
filteredModels.map(model => (
|
filteredModels.map(model => {
|
||||||
|
const unavailable = model.available === false;
|
||||||
|
return (
|
||||||
<button
|
<button
|
||||||
key={model.id}
|
key={model.id}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -113,21 +117,31 @@ export function ModelSelector({
|
|||||||
w-full text-left px-3 py-2 text-xs flex items-center justify-between gap-2 transition-colors
|
w-full text-left px-3 py-2 text-xs flex items-center justify-between gap-2 transition-colors
|
||||||
${model.id === currentModel
|
${model.id === currentModel
|
||||||
? 'text-orange-600 dark:text-orange-400 bg-orange-50 dark:bg-orange-900/20'
|
? 'text-orange-600 dark:text-orange-400 bg-orange-50 dark:bg-orange-900/20'
|
||||||
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700'
|
: unavailable
|
||||||
|
? 'text-gray-400 dark:text-gray-500 hover:bg-gray-50 dark:hover:bg-gray-700'
|
||||||
|
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700'
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col min-w-0">
|
<div className="flex flex-col min-w-0">
|
||||||
<span className="truncate font-medium">{model.name}</span>
|
<span className="truncate font-medium">{model.name}</span>
|
||||||
{model.provider && (
|
<div className="flex items-center gap-1">
|
||||||
<span className="text-[10px] text-gray-400 dark:text-gray-500">{model.provider}</span>
|
{model.provider && (
|
||||||
)}
|
<span className="text-[10px] text-gray-400 dark:text-gray-500">{model.provider}</span>
|
||||||
|
)}
|
||||||
|
{unavailable && (
|
||||||
|
<span className="text-[10px] text-amber-500 flex items-center gap-0.5">
|
||||||
|
<AlertTriangle className="w-2.5 h-2.5" />未配置
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{model.id === currentModel && (
|
{model.id === currentModel && (
|
||||||
<Check className="w-3.5 h-3.5 flex-shrink-0" />
|
<Check className="w-3.5 h-3.5 flex-shrink-0" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
))
|
);
|
||||||
|
})
|
||||||
) : (
|
) : (
|
||||||
<div className="px-3 py-2 text-xs text-gray-400">无匹配模型</div>
|
<div className="px-3 py-2 text-xs text-gray-400">无匹配模型</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ export function PanelToggleButton({
|
|||||||
<button
|
<button
|
||||||
onClick={onToggle}
|
onClick={onToggle}
|
||||||
className="p-1.5 rounded-md text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
className="p-1.5 rounded-md text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
||||||
title={panelOpen ? '关闭侧面板' : '打开侧面板'}
|
title={panelOpen ? '关闭侧面板' : '查看产物文件'}
|
||||||
>
|
>
|
||||||
{panelOpen
|
{panelOpen
|
||||||
? <PanelRightClose className="w-4 h-4" />
|
? <PanelRightClose className="w-4 h-4" />
|
||||||
|
|||||||
@@ -233,16 +233,30 @@ export const useAgentStore = create<AgentStore>((set, get) => ({
|
|||||||
?? 'default';
|
?? 'default';
|
||||||
|
|
||||||
// Step 2: Create clone with merged data from backend
|
// Step 2: Create clone with merged data from backend
|
||||||
const result = await client.createClone({
|
// In saas-relay mode the local Kernel may not be running,
|
||||||
name: config.name,
|
// so wrap createClone in a try-catch and skip gracefully.
|
||||||
emoji: config.emoji,
|
let cloneId: string | undefined;
|
||||||
personality: config.personality,
|
let freshClone: Clone | undefined;
|
||||||
scenarios: template.scenarios,
|
try {
|
||||||
communicationStyle: config.communication_style,
|
const result = await client.createClone({
|
||||||
model: resolvedModel,
|
name: config.name,
|
||||||
});
|
emoji: config.emoji,
|
||||||
|
personality: config.personality,
|
||||||
const cloneId = result?.clone?.id;
|
scenarios: template.scenarios,
|
||||||
|
communicationStyle: config.communication_style,
|
||||||
|
model: resolvedModel,
|
||||||
|
});
|
||||||
|
cloneId = result?.clone?.id;
|
||||||
|
} catch (cloneErr) {
|
||||||
|
log.warn('[AgentStore] createClone failed (likely saas-relay mode without local kernel):', cloneErr);
|
||||||
|
// In SaaS relay mode, the agent was already created server-side in Step 1.
|
||||||
|
// Just refresh the clone list from the server.
|
||||||
|
await get().loadClones();
|
||||||
|
freshClone = get().clones.find(c => c.name === config.name);
|
||||||
|
if (freshClone) {
|
||||||
|
cloneId = freshClone.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (cloneId) {
|
if (cloneId) {
|
||||||
// Persist SOUL.md via identity system
|
// Persist SOUL.md via identity system
|
||||||
@@ -286,7 +300,15 @@ export const useAgentStore = create<AgentStore>((set, get) => ({
|
|||||||
await get().loadClones();
|
await get().loadClones();
|
||||||
|
|
||||||
// Return a fresh clone from the store (immutable — no in-place mutation)
|
// Return a fresh clone from the store (immutable — no in-place mutation)
|
||||||
const freshClone = get().clones.find((c) => c.id === cloneId);
|
const storedClone = get().clones.find((c) => c.id === cloneId);
|
||||||
|
if (storedClone) {
|
||||||
|
return {
|
||||||
|
...storedClone,
|
||||||
|
...(config.welcome_message ? { welcomeMessage: config.welcome_message } : {}),
|
||||||
|
...(config.quick_commands?.length ? { quickCommands: config.quick_commands } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Fallback: if clone was found by name earlier in the catch path
|
||||||
if (freshClone) {
|
if (freshClone) {
|
||||||
return {
|
return {
|
||||||
...freshClone,
|
...freshClone,
|
||||||
@@ -294,7 +316,7 @@ export const useAgentStore = create<AgentStore>((set, get) => ({
|
|||||||
...(config.quick_commands?.length ? { quickCommands: config.quick_commands } : {}),
|
...(config.quick_commands?.length ? { quickCommands: config.quick_commands } : {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return result?.clone as Clone | undefined;
|
return undefined;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
set({ error: String(error) });
|
set({ error: String(error) });
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
@@ -326,8 +326,16 @@ export const useConversationStore = create<ConversationState>()(
|
|||||||
upsertActiveConversation: (currentMessages: ChatMessage[]) => {
|
upsertActiveConversation: (currentMessages: ChatMessage[]) => {
|
||||||
const state = get();
|
const state = get();
|
||||||
const currentId = state.currentConversationId || null;
|
const currentId = state.currentConversationId || null;
|
||||||
|
// Strip transient fields (error, streaming, optimistic) before persistence
|
||||||
|
// so old errors don't permanently show "重试" buttons on reload
|
||||||
|
const cleanMessages = currentMessages.map(m => ({
|
||||||
|
...m,
|
||||||
|
error: undefined,
|
||||||
|
streaming: undefined,
|
||||||
|
optimistic: undefined,
|
||||||
|
}));
|
||||||
const conversations = upsertActiveConversation(
|
const conversations = upsertActiveConversation(
|
||||||
[...state.conversations], currentMessages, state.sessionKey,
|
[...state.conversations], cleanMessages, state.sessionKey,
|
||||||
state.currentConversationId, state.currentAgent,
|
state.currentConversationId, state.currentAgent,
|
||||||
);
|
);
|
||||||
// If this was a new conversation (no prior currentConversationId),
|
// If this was a new conversation (no prior currentConversationId),
|
||||||
|
|||||||
Reference in New Issue
Block a user