fix(desktop): Tauri 端找碴验证 7 项修复 — 消息泄漏/UUID暴露/错误友好化
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
B15/B11: streamStore onAgentStream 添加 activeRunId 过滤,移除降级匹配, hand/workflow 消息追加前验证 runId 归属;chatStore 切换/新建对话时 先 cancelStream 终止旧流;ChatArea hand-execution-complete 事件 添加 isStreaming 守卫 B4/B5: ChatArea 模型列表过滤 embedding 模型,provider 设为 undefined 隐藏 UUID B2/B3: streamStore onError 添加 formatUserError 函数,将原始 JSON 错误转换为中文友好提示 B1: SuggestionChips onSelect 延迟调用 handleSend 自动发送建议 fix(runtime): test_util.rs with_error 添加 mut self,with_stream_chunks 移除多余 mut fix(saas): lib.rs 添加 Result/SaasError re-export
This commit is contained in:
@@ -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<StreamChunk>) -> Self {
|
||||
pub fn with_stream_chunks(self, chunks: Vec<StreamChunk>) -> Self {
|
||||
self.stream_chunks
|
||||
.lock()
|
||||
.expect("stream_chunks lock")
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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) && (
|
||||
<SuggestionChips
|
||||
suggestions={suggestions}
|
||||
onSelect={(text) => { setInput(text); textareaRef.current?.focus(); }}
|
||||
onSelect={(text) => { setInput(text); textareaRef.current?.focus(); setTimeout(() => handleSend(), 0); }}
|
||||
className="mb-3"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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<StreamState>()(
|
||||
|
||||
// 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<StreamState>()(
|
||||
}
|
||||
|
||||
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<StreamState>()(
|
||||
(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<StreamState>()(
|
||||
}
|
||||
}
|
||||
} 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<StreamState>()(
|
||||
};
|
||||
_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',
|
||||
|
||||
@@ -155,12 +155,18 @@ export const useChatStore = create<ChatState>()(
|
||||
},
|
||||
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user