diff --git a/crates/zclaw-runtime/src/test_util.rs b/crates/zclaw-runtime/src/test_util.rs index 7f01330..25e6647 100644 --- a/crates/zclaw-runtime/src/test_util.rs +++ b/crates/zclaw-runtime/src/test_util.rs @@ -79,7 +79,7 @@ impl MockLlmDriver { } /// Queue an error response. - pub fn with_error(self, _error: &str) -> Self { + pub fn with_error(mut self, _error: &str) -> Self { self.push_response(CompletionResponse { content: vec![], model: "mock-model".to_string(), @@ -97,7 +97,7 @@ impl MockLlmDriver { } /// Queue stream chunks for a streaming call. - pub fn with_stream_chunks(mut self, chunks: Vec) -> Self { + pub fn with_stream_chunks(self, chunks: Vec) -> Self { self.stream_chunks .lock() .expect("stream_chunks lock") diff --git a/crates/zclaw-saas/src/lib.rs b/crates/zclaw-saas/src/lib.rs index d01b4da..528416e 100644 --- a/crates/zclaw-saas/src/lib.rs +++ b/crates/zclaw-saas/src/lib.rs @@ -28,3 +28,5 @@ pub mod telemetry; pub mod billing; pub mod industry; pub mod knowledge; + +pub use error::{SaasError, SaasError as Error, SaasResult as Result}; diff --git a/desktop/src/components/ChatArea.tsx b/desktop/src/components/ChatArea.tsx index 80caafa..8aeb296 100644 --- a/desktop/src/components/ChatArea.tsx +++ b/desktop/src/components/ChatArea.tsx @@ -88,12 +88,17 @@ export function ChatArea({ compact, onOpenDetail }: { compact?: boolean; onOpenD const models = useMemo(() => { const failed = failedModelIds.current; if (isLoggedIn && saasModels.length > 0) { - return saasModels.map(m => ({ - id: m.alias || m.id, - name: m.alias || m.id, - provider: m.provider_id, - available: !failed.has(m.alias || m.id), - })); + return saasModels + .filter(m => { + const name = (m.alias || m.id).toLowerCase(); + return !name.includes('embedding'); + }) + .map(m => ({ + id: m.alias || m.id, + name: m.alias || m.id, + provider: undefined, + available: !failed.has(m.alias || m.id), + })); } if (configModels.length > 0) { return configModels; @@ -210,6 +215,8 @@ export function ChatArea({ compact, onOpenDetail }: { compact?: boolean; onOpenD 'hand-execution-complete', (event) => { const { handId, success, error } = event.payload; + const streaming = useChatStore.getState().isStreaming; + if (!streaming) return; useChatStore.getState().addMessage({ id: crypto.randomUUID(), role: 'hand', @@ -502,7 +509,7 @@ export function ChatArea({ compact, onOpenDetail }: { compact?: boolean; onOpenD {!isStreaming && suggestions.length > 0 && !messages.some(m => m.error) && ( { setInput(text); textareaRef.current?.focus(); }} + onSelect={(text) => { setInput(text); textareaRef.current?.focus(); setTimeout(() => handleSend(), 0); }} className="mb-3" /> )} diff --git a/desktop/src/store/chat/streamStore.ts b/desktop/src/store/chat/streamStore.ts index c0aebfc..9eb178a 100644 --- a/desktop/src/store/chat/streamStore.ts +++ b/desktop/src/store/chat/streamStore.ts @@ -37,6 +37,39 @@ import { useArtifactStore } from './artifactStore'; const log = createLogger('StreamStore'); +// --------------------------------------------------------------------------- +// Error formatting — convert raw LLM/API errors to user-friendly messages +// --------------------------------------------------------------------------- + +function formatUserError(raw: string): string { + if (raw.includes('API Key') || raw.includes('没有可用的 API Key')) { + return '模型服务暂时不可用,请稍后重试'; + } + if (raw.includes('404') || raw.includes('NOT_FOUND')) { + return '模型服务未找到,请检查模型配置'; + } + if (raw.includes('429') || raw.includes('rate_limit') || raw.includes('Too Many Requests')) { + return '请求过于频繁,请稍后重试'; + } + if (raw.includes('401') || raw.includes('Unauthorized')) { + return '认证已过期,请重新登录'; + } + if (raw.includes('402') || raw.includes('quota') || raw.includes('配额')) { + return '使用配额已用尽,请升级订阅或联系管理员'; + } + if (raw.includes('timeout') || raw.includes('超时') || raw.includes('Timeout')) { + return '请求超时,请稍后重试'; + } + if (raw.includes('502') || raw.includes('Bad Gateway')) { + return '服务网关异常,请稍后重试'; + } + if (raw.includes('503') || raw.includes('Service Unavailable')) { + return '服务暂时不可用,请稍后重试'; + } + // Strip raw JSON from remaining errors + return raw.replace(/\{[^}]*\}/g, '').replace(/\s+/g, ' ').trim().substring(0, 80) || '请求失败,请稍后重试'; +} + // --------------------------------------------------------------------------- // 401 Auth Error Recovery // --------------------------------------------------------------------------- @@ -556,7 +589,8 @@ export const useStreamStore = create()( // Attempt 401 auth recovery (token refresh + kernel reconnect) const recoveryMsg = await tryRecoverFromAuthError(error); - const displayError = recoveryMsg || error; + const rawError = recoveryMsg || error; + const displayError = formatUserError(rawError); _chat?.updateMessages(msgs => msgs.map(m => @@ -699,6 +733,9 @@ export const useStreamStore = create()( } const unsubscribe = client.onAgentStream((delta: AgentStreamDelta) => { + const activeRunId = get().activeRunId; + if (activeRunId && delta.runId && delta.runId !== activeRunId) return; + const msgs = _chat?.getMessages() || []; const streamingMsg = [...msgs] @@ -710,10 +747,7 @@ export const useStreamStore = create()( (delta.runId && m.runId === delta.runId) || (!delta.runId && m.runId === null) ) - )) - || [...msgs] - .reverse() - .find(m => m.role === 'assistant' && m.streaming); + )); if (!streamingMsg) return; @@ -804,6 +838,8 @@ export const useStreamStore = create()( } } } else if (delta.stream === 'hand') { + const runId = get().activeRunId; + if (!runId || (delta.runId && delta.runId !== runId)) return; const handMsg: StreamMsg = { id: `hand_${Date.now()}_${generateRandomString(4)}`, role: 'hand', @@ -818,6 +854,8 @@ export const useStreamStore = create()( }; _chat?.updateMessages(ms => [...ms, handMsg]); } else if (delta.stream === 'workflow') { + const runId = get().activeRunId; + if (!runId || (delta.runId && delta.runId !== runId)) return; const workflowMsg: StreamMsg = { id: `workflow_${Date.now()}_${generateRandomString(4)}`, role: 'workflow', diff --git a/desktop/src/store/chatStore.ts b/desktop/src/store/chatStore.ts index 8a7c10c..fba6047 100644 --- a/desktop/src/store/chatStore.ts +++ b/desktop/src/store/chatStore.ts @@ -155,12 +155,18 @@ export const useChatStore = create()( }, newConversation: () => { + if (get().isStreaming) { + useStreamStore.getState().cancelStream(); + } const messages = get().messages; useConversationStore.getState().newConversation(messages); set({ messages: [], isStreaming: false }); }, switchConversation: (id: string) => { + if (get().isStreaming) { + useStreamStore.getState().cancelStream(); + } const messages = get().messages; const result = useConversationStore.getState().switchConversation(id, messages); if (result) {