chore(align): sync Git with running container as authoritative source
One-time alignment to close drift between the running container (qihu-aiflow / image qihuaiflow:with-deps) and this Git repository. Container hotfixes previously persisted only via `docker commit` are now captured in Git so Git becomes the single source of truth. Backend: - chat_service.py: accumulated fixes incl. EigenFlux README guidance - chat_context.py / chat_media.py: URL/web-repo context injection - api/url_reader.py: NEW SSRF-safe web/README reader (Issue #725) - skills_admin.py & Skills.tsx: 14-scene capability unification - pgvector_search.py, orchestrator.py, platform_knowledge.py, market.py, admin/models.py: drift sync Frontend: - ChatPage.tsx: collapsible AI think/act block (Issue #724) - MarketPage / EvolutionDashboard / TokenDashboard styling sync Migrations: - 018_normalize_marketapp_scene.sql: fixed capability allowlist (prevents capability wipe on restart) Alignment performed 2026-07-23 16:04:25. No deletions; secrets/venv/node_modules/ caches were not touched (tracked-source-only copy).
This commit is contained in:
1 parent
d3d8d62b94
commit
4a95657030
2 files changed
+125
-2
No files matched your search
@@ -1664,3 +1664,74 @@ html.dark .ai-process {
|
|||||||
background: color-mix(in srgb, var(--color-bg-secondary, #1b1d29) 70%, var(--color-accent, #5B5FED) 8%);
|
background: color-mix(in srgb, var(--color-bg-secondary, #1b1d29) 70%, var(--color-accent, #5B5FED) 8%);
|
||||||
border-color: color-mix(in srgb, var(--color-accent, #5B5FED) 25%, transparent);
|
border-color: color-mix(in srgb, var(--color-accent, #5B5FED) 25%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== AI 回复异常状态 ===== */
|
||||||
|
.ai-error-bubble {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
background: color-mix(in srgb, var(--color-error, #e74c3c) 8%, var(--color-bg-primary, #fff));
|
||||||
|
border: 1px solid color-mix(in srgb, var(--color-error, #e74c3c) 30%, transparent);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-error-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
margin-bottom: -6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-error-text {
|
||||||
|
font-size: var(--text-sm, 13px);
|
||||||
|
color: var(--color-text-secondary, #555);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-error-text p {
|
||||||
|
margin: 0 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-error-retry {
|
||||||
|
align-self: flex-start;
|
||||||
|
padding: 5px 14px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #fff;
|
||||||
|
background: var(--color-accent, #5B5FED);
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s, transform 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-error-retry:hover {
|
||||||
|
background: var(--color-accent-hover, #4A4ECC);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-error-retry:active {
|
||||||
|
transform: scale(0.97);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 暗色主题 */
|
||||||
|
[data-theme="dark"] .ai-error-bubble,
|
||||||
|
html.dark .ai-error-bubble {
|
||||||
|
background: color-mix(in srgb, var(--color-error, #e74c3c) 12%, var(--color-bg-primary, #1a1a2e));
|
||||||
|
border-color: color-mix(in srgb, var(--color-error, #e74c3c) 20%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 发送中——超时状态 */
|
||||||
|
.ai-sending-timeout {
|
||||||
|
border-color: color-mix(in srgb, #f39c12 30%, transparent) !important;
|
||||||
|
background: color-mix(in srgb, #f39c12 5%, var(--color-bg-primary, #fff)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .ai-sending-timeout,
|
||||||
|
html.dark .ai-sending-timeout {
|
||||||
|
border-color: color-mix(in srgb, #f39c12 25%, transparent) !important;
|
||||||
|
background: color-mix(in srgb, #f39c12 8%, var(--color-bg-primary, #1a1a2e)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-cancel-btn:hover {
|
||||||
|
background: color-mix(in srgb, #e74c3c 15%, var(--color-bg-tertiary)) !important;
|
||||||
|
}
|
||||||
@@ -12,6 +12,25 @@ import mermaid from 'mermaid';
|
|||||||
|
|
||||||
mermaid.initialize({ startOnLoad: false, theme: 'default' });
|
mermaid.initialize({ startOnLoad: false, theme: 'default' });
|
||||||
|
|
||||||
|
// ===== AI 回复异常检测 =====
|
||||||
|
const AI_ERROR_PATTERNS = [
|
||||||
|
'AI模型调用失败',
|
||||||
|
'AI服务暂时不可用',
|
||||||
|
'AI模型返回了空响应',
|
||||||
|
'AI模型已处理工具调用结果',
|
||||||
|
'该模型缺少API密钥或端点配置',
|
||||||
|
'请选择一个已配置的AI模型',
|
||||||
|
'请先在后台管理-大模型库配置默认对话模型',
|
||||||
|
'⚠️ 多 Agent 编排执行失败',
|
||||||
|
'⚠️ 外部 Agent 调用失败',
|
||||||
|
'编排未产生内容',
|
||||||
|
'外部 Agent 未返回内容',
|
||||||
|
];
|
||||||
|
function isAiError(content: string): boolean {
|
||||||
|
if (!content) return false;
|
||||||
|
return AI_ERROR_PATTERNS.some(p => content.includes(p));
|
||||||
|
}
|
||||||
|
|
||||||
// ===== Enhanced Markdown Renderer =====
|
// ===== Enhanced Markdown Renderer =====
|
||||||
// 媒体(图片/视频)以受限缩略图呈现,并提供下载(格式+尺寸)选项
|
// 媒体(图片/视频)以受限缩略图呈现,并提供下载(格式+尺寸)选项
|
||||||
import { renderMarkdownToHtml, MediaInfo } from '@/utils/markdown';
|
import { renderMarkdownToHtml, MediaInfo } from '@/utils/markdown';
|
||||||
@@ -524,6 +543,8 @@ const ChatPage: React.FC = () => {
|
|||||||
const [chatModels, setChatModels] = useState<any[]>([]);
|
const [chatModels, setChatModels] = useState<any[]>([]);
|
||||||
const [inputValue, setInputValue] = useState('');
|
const [inputValue, setInputValue] = useState('');
|
||||||
const [sending, setSending] = useState(false);
|
const [sending, setSending] = useState(false);
|
||||||
|
const [sendingTimeout, setSendingTimeout] = useState(false);
|
||||||
|
const sendingTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const [conversations, setConversations] = useState<Conversation[]>([]);
|
const [conversations, setConversations] = useState<Conversation[]>([]);
|
||||||
const [messages, setMessages] = useState<Message[]>([]);
|
const [messages, setMessages] = useState<Message[]>([]);
|
||||||
const [loadingConversations, setLoadingConversations] = useState(true);
|
const [loadingConversations, setLoadingConversations] = useState(true);
|
||||||
@@ -903,6 +924,12 @@ const ChatPage: React.FC = () => {
|
|||||||
const sendText = async (content: string) => {
|
const sendText = async (content: string) => {
|
||||||
if (!content.trim() || sending || !selectedConversation) return;
|
if (!content.trim() || sending || !selectedConversation) return;
|
||||||
setSending(true);
|
setSending(true);
|
||||||
|
setSendingTimeout(false);
|
||||||
|
// 60s 超时警告
|
||||||
|
if (sendingTimerRef.current) clearTimeout(sendingTimerRef.current);
|
||||||
|
sendingTimerRef.current = setTimeout(() => {
|
||||||
|
setSendingTimeout(true);
|
||||||
|
}, 60000);
|
||||||
|
|
||||||
// Add user message optimistically
|
// Add user message optimistically
|
||||||
const tempUserMsg: Message = {
|
const tempUserMsg: Message = {
|
||||||
@@ -988,11 +1015,14 @@ const ChatPage: React.FC = () => {
|
|||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
if (sendingTimerRef.current) { clearTimeout(sendingTimerRef.current); sendingTimerRef.current = null; }
|
||||||
// Remove optimistic message on error
|
// Remove optimistic message on error
|
||||||
setMessages(prev => prev.filter(m => m.id !== tempUserMsg.id));
|
setMessages(prev => prev.filter(m => m.id !== tempUserMsg.id));
|
||||||
alert('发送失败: ' + (err.message || err.detail || '未知错误'));
|
alert('发送失败: ' + (err.message || err.detail || '未知错误'));
|
||||||
} finally {
|
} finally {
|
||||||
|
if (sendingTimerRef.current) { clearTimeout(sendingTimerRef.current); sendingTimerRef.current = null; }
|
||||||
setSending(false);
|
setSending(false);
|
||||||
|
setSendingTimeout(false);
|
||||||
inputRef.current?.focus();
|
inputRef.current?.focus();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1248,6 +1278,19 @@ const ChatPage: React.FC = () => {
|
|||||||
data={clarify}
|
data={clarify}
|
||||||
onChoose={(opt: any) => sendText(`${opt.medium}:${clarify.prompt}`)}
|
onChoose={(opt: any) => sendText(`${opt.medium}:${clarify.prompt}`)}
|
||||||
/>
|
/>
|
||||||
|
) : msg.role === 'assistant' && isAiError(msg.content) ? (
|
||||||
|
<div className="ai-error-bubble">
|
||||||
|
<div className="ai-error-icon">⚠️</div>
|
||||||
|
<div className="ai-error-text">
|
||||||
|
<MarkdownContent content={msg.content} />
|
||||||
|
</div>
|
||||||
|
<button className="ai-error-retry" onClick={() => {
|
||||||
|
setMessages(prev => prev.filter(m => m.id !== msg.id));
|
||||||
|
const msgsAfterFilter = messages.filter(m => m.id !== msg.id);
|
||||||
|
const lastUser = [...msgsAfterFilter].reverse().find(m => m.role === 'user');
|
||||||
|
if (lastUser) sendText(lastUser.content);
|
||||||
|
}}>🔄 重试</button>
|
||||||
|
</div>
|
||||||
) : msg.role === 'assistant' ? (
|
) : msg.role === 'assistant' ? (
|
||||||
<MarkdownContent content={msg.content} />
|
<MarkdownContent content={msg.content} />
|
||||||
) : (
|
) : (
|
||||||
@@ -1315,9 +1358,18 @@ const ChatPage: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
{sending && (
|
{sending && (
|
||||||
<div className="message-row assistant">
|
<div className="message-row assistant">
|
||||||
<div className="message-bubble assistant" style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
<div className={`message-bubble assistant${sendingTimeout ? ' ai-sending-timeout' : ''}`} style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
<IconSpinner />
|
<IconSpinner />
|
||||||
<span>思考中...</span>
|
<span>{sendingTimeout ? '⏳ 响应时间较长,AI 仍在处理中...' : '思考中...'}</span>
|
||||||
|
{sendingTimeout && (
|
||||||
|
<button className="ai-cancel-btn" onClick={() => {
|
||||||
|
setSending(false);
|
||||||
|
setSendingTimeout(false);
|
||||||
|
if (sendingTimerRef.current) clearTimeout(sendingTimerRef.current);
|
||||||
|
}} title="取消等待" style={{ marginLeft: 12, padding: '2px 8px', fontSize: 12, cursor: 'pointer', background: 'var(--color-bg-tertiary)', border: '1px solid var(--color-border)', borderRadius: 4, color: 'var(--color-text-secondary)' }}>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in new issue
Block a user