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

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:
iven
2026-04-21 20:29:47 +08:00
parent 79e7cd3446
commit b2908791f6
5 changed files with 67 additions and 14 deletions

View File

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

View File

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

View File

@@ -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"
/>
)}

View File

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

View File

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