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.
|
/// 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 {
|
self.push_response(CompletionResponse {
|
||||||
content: vec![],
|
content: vec![],
|
||||||
model: "mock-model".to_string(),
|
model: "mock-model".to_string(),
|
||||||
@@ -97,7 +97,7 @@ impl MockLlmDriver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Queue stream chunks for a streaming call.
|
/// 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
|
self.stream_chunks
|
||||||
.lock()
|
.lock()
|
||||||
.expect("stream_chunks lock")
|
.expect("stream_chunks lock")
|
||||||
|
|||||||
@@ -28,3 +28,5 @@ pub mod telemetry;
|
|||||||
pub mod billing;
|
pub mod billing;
|
||||||
pub mod industry;
|
pub mod industry;
|
||||||
pub mod knowledge;
|
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 models = useMemo(() => {
|
||||||
const failed = failedModelIds.current;
|
const failed = failedModelIds.current;
|
||||||
if (isLoggedIn && saasModels.length > 0) {
|
if (isLoggedIn && saasModels.length > 0) {
|
||||||
return saasModels.map(m => ({
|
return saasModels
|
||||||
id: m.alias || m.id,
|
.filter(m => {
|
||||||
name: m.alias || m.id,
|
const name = (m.alias || m.id).toLowerCase();
|
||||||
provider: m.provider_id,
|
return !name.includes('embedding');
|
||||||
available: !failed.has(m.alias || m.id),
|
})
|
||||||
}));
|
.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) {
|
if (configModels.length > 0) {
|
||||||
return configModels;
|
return configModels;
|
||||||
@@ -210,6 +215,8 @@ export function ChatArea({ compact, onOpenDetail }: { compact?: boolean; onOpenD
|
|||||||
'hand-execution-complete',
|
'hand-execution-complete',
|
||||||
(event) => {
|
(event) => {
|
||||||
const { handId, success, error } = event.payload;
|
const { handId, success, error } = event.payload;
|
||||||
|
const streaming = useChatStore.getState().isStreaming;
|
||||||
|
if (!streaming) return;
|
||||||
useChatStore.getState().addMessage({
|
useChatStore.getState().addMessage({
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
role: 'hand',
|
role: 'hand',
|
||||||
@@ -502,7 +509,7 @@ export function ChatArea({ compact, onOpenDetail }: { compact?: boolean; onOpenD
|
|||||||
{!isStreaming && suggestions.length > 0 && !messages.some(m => m.error) && (
|
{!isStreaming && suggestions.length > 0 && !messages.some(m => m.error) && (
|
||||||
<SuggestionChips
|
<SuggestionChips
|
||||||
suggestions={suggestions}
|
suggestions={suggestions}
|
||||||
onSelect={(text) => { setInput(text); textareaRef.current?.focus(); }}
|
onSelect={(text) => { setInput(text); textareaRef.current?.focus(); setTimeout(() => handleSend(), 0); }}
|
||||||
className="mb-3"
|
className="mb-3"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -37,6 +37,39 @@ import { useArtifactStore } from './artifactStore';
|
|||||||
|
|
||||||
const log = createLogger('StreamStore');
|
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
|
// 401 Auth Error Recovery
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -556,7 +589,8 @@ export const useStreamStore = create<StreamState>()(
|
|||||||
|
|
||||||
// Attempt 401 auth recovery (token refresh + kernel reconnect)
|
// Attempt 401 auth recovery (token refresh + kernel reconnect)
|
||||||
const recoveryMsg = await tryRecoverFromAuthError(error);
|
const recoveryMsg = await tryRecoverFromAuthError(error);
|
||||||
const displayError = recoveryMsg || error;
|
const rawError = recoveryMsg || error;
|
||||||
|
const displayError = formatUserError(rawError);
|
||||||
|
|
||||||
_chat?.updateMessages(msgs =>
|
_chat?.updateMessages(msgs =>
|
||||||
msgs.map(m =>
|
msgs.map(m =>
|
||||||
@@ -699,6 +733,9 @@ export const useStreamStore = create<StreamState>()(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const unsubscribe = client.onAgentStream((delta: AgentStreamDelta) => {
|
const unsubscribe = client.onAgentStream((delta: AgentStreamDelta) => {
|
||||||
|
const activeRunId = get().activeRunId;
|
||||||
|
if (activeRunId && delta.runId && delta.runId !== activeRunId) return;
|
||||||
|
|
||||||
const msgs = _chat?.getMessages() || [];
|
const msgs = _chat?.getMessages() || [];
|
||||||
|
|
||||||
const streamingMsg = [...msgs]
|
const streamingMsg = [...msgs]
|
||||||
@@ -710,10 +747,7 @@ export const useStreamStore = create<StreamState>()(
|
|||||||
(delta.runId && m.runId === delta.runId)
|
(delta.runId && m.runId === delta.runId)
|
||||||
|| (!delta.runId && m.runId === null)
|
|| (!delta.runId && m.runId === null)
|
||||||
)
|
)
|
||||||
))
|
));
|
||||||
|| [...msgs]
|
|
||||||
.reverse()
|
|
||||||
.find(m => m.role === 'assistant' && m.streaming);
|
|
||||||
|
|
||||||
if (!streamingMsg) return;
|
if (!streamingMsg) return;
|
||||||
|
|
||||||
@@ -804,6 +838,8 @@ export const useStreamStore = create<StreamState>()(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (delta.stream === 'hand') {
|
} else if (delta.stream === 'hand') {
|
||||||
|
const runId = get().activeRunId;
|
||||||
|
if (!runId || (delta.runId && delta.runId !== runId)) return;
|
||||||
const handMsg: StreamMsg = {
|
const handMsg: StreamMsg = {
|
||||||
id: `hand_${Date.now()}_${generateRandomString(4)}`,
|
id: `hand_${Date.now()}_${generateRandomString(4)}`,
|
||||||
role: 'hand',
|
role: 'hand',
|
||||||
@@ -818,6 +854,8 @@ export const useStreamStore = create<StreamState>()(
|
|||||||
};
|
};
|
||||||
_chat?.updateMessages(ms => [...ms, handMsg]);
|
_chat?.updateMessages(ms => [...ms, handMsg]);
|
||||||
} else if (delta.stream === 'workflow') {
|
} else if (delta.stream === 'workflow') {
|
||||||
|
const runId = get().activeRunId;
|
||||||
|
if (!runId || (delta.runId && delta.runId !== runId)) return;
|
||||||
const workflowMsg: StreamMsg = {
|
const workflowMsg: StreamMsg = {
|
||||||
id: `workflow_${Date.now()}_${generateRandomString(4)}`,
|
id: `workflow_${Date.now()}_${generateRandomString(4)}`,
|
||||||
role: 'workflow',
|
role: 'workflow',
|
||||||
|
|||||||
@@ -155,12 +155,18 @@ export const useChatStore = create<ChatState>()(
|
|||||||
},
|
},
|
||||||
|
|
||||||
newConversation: () => {
|
newConversation: () => {
|
||||||
|
if (get().isStreaming) {
|
||||||
|
useStreamStore.getState().cancelStream();
|
||||||
|
}
|
||||||
const messages = get().messages;
|
const messages = get().messages;
|
||||||
useConversationStore.getState().newConversation(messages);
|
useConversationStore.getState().newConversation(messages);
|
||||||
set({ messages: [], isStreaming: false });
|
set({ messages: [], isStreaming: false });
|
||||||
},
|
},
|
||||||
|
|
||||||
switchConversation: (id: string) => {
|
switchConversation: (id: string) => {
|
||||||
|
if (get().isStreaming) {
|
||||||
|
useStreamStore.getState().cancelStream();
|
||||||
|
}
|
||||||
const messages = get().messages;
|
const messages = get().messages;
|
||||||
const result = useConversationStore.getState().switchConversation(id, messages);
|
const result = useConversationStore.getState().switchConversation(id, messages);
|
||||||
if (result) {
|
if (result) {
|
||||||
|
|||||||
Reference in New Issue
Block a user