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 05:42:47. No deletions; secrets/venv/node_modules/ caches were not touched (tracked-source-only copy).
This commit is contained in:
1 parent
49f7503978
commit
a08ec87b22
19 files changed
+7474
-7196
No files matched your search
@@ -457,6 +457,64 @@ const Dropdown: React.FC<DropdownProps> = ({ trigger, children, className = '',
|
||||
);
|
||||
};
|
||||
|
||||
// ===== AI 思考 / 执行过程面板(顶部展开 + 自动收拢) =====
|
||||
// autoExpand=true 时(本轮刚产生的助手消息)首屏展开呈现思考链路,
|
||||
// 2.6s 后若用户未手动干预则自动收拢为一行摘要,避免与回复内容冲突。
|
||||
const AiProcessBlock: React.FC<{ steps: any[]; autoExpand: boolean }> = ({ steps, autoExpand }) => {
|
||||
const [open, setOpen] = useState(autoExpand);
|
||||
const timerRef = useRef<number | null>(null);
|
||||
const touchedRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoExpand) {
|
||||
setOpen(false);
|
||||
return;
|
||||
}
|
||||
setOpen(true);
|
||||
touchedRef.current = false;
|
||||
timerRef.current = window.setTimeout(() => {
|
||||
if (!touchedRef.current) setOpen(false);
|
||||
}, 2600);
|
||||
return () => {
|
||||
if (timerRef.current) window.clearTimeout(timerRef.current);
|
||||
};
|
||||
}, [autoExpand]);
|
||||
|
||||
if (!steps || steps.length === 0) return null;
|
||||
|
||||
const toggle = () => {
|
||||
touchedRef.current = true;
|
||||
if (timerRef.current) {
|
||||
window.clearTimeout(timerRef.current);
|
||||
timerRef.current = null;
|
||||
}
|
||||
setOpen(o => !o);
|
||||
};
|
||||
|
||||
return (
|
||||
<details className="ai-process" open={open}>
|
||||
<summary className="ai-process-summary" onClick={(e) => { e.preventDefault(); toggle(); }}>
|
||||
<span className="ai-process-badge">🧠</span>
|
||||
思考 / 执行过程({steps.length} 步,{open ? '点击收起' : '点击展开'})
|
||||
</summary>
|
||||
<div className="ai-process-list">
|
||||
{steps.map((s: any, i: number) => (
|
||||
<div key={i} className={`ai-process-item ai-process-${s.type}`}>
|
||||
<span className="ai-process-ico">
|
||||
{s.type === 'thinking' ? '💡' : s.type === 'skill' ? '🛠' : '🔌'}
|
||||
</span>
|
||||
<div className="ai-process-body">
|
||||
{s.type === 'thinking' && (<><b className="ai-process-title">思考</b><p className="ai-process-text">{s.text}</p></>)}
|
||||
{s.type === 'skill' && (<><b className="ai-process-title">调用技能:{s.name}</b><p className="ai-process-text">{(s.result || '').slice(0, 400)}</p></>)}
|
||||
{s.type === 'mcp' && (<><b className="ai-process-title">MCP · {s.server}:{s.name}</b><p className="ai-process-text">{(s.result || '').slice(0, 400)}</p></>)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
);
|
||||
};
|
||||
|
||||
// ===== Main Component =====
|
||||
const ChatPage: React.FC = () => {
|
||||
const { id } = useParams<{ id?: string }>();
|
||||
@@ -470,6 +528,8 @@ const ChatPage: React.FC = () => {
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [loadingConversations, setLoadingConversations] = useState(true);
|
||||
const [loadingMessages, setLoadingMessages] = useState(false);
|
||||
// 本轮对话新产生的助手消息 id 集合,用于触发"思考过程"首屏展开
|
||||
const [freshMsgIds, setFreshMsgIds] = useState<Set<string>>(new Set());
|
||||
const [knowledgeBases, setKnowledgeBases] = useState<any[]>([]);
|
||||
const [agents, setAgents] = useState<any[]>([]);
|
||||
const [skills, setSkills] = useState<any[]>([]);
|
||||
@@ -901,6 +961,11 @@ const ChatPage: React.FC = () => {
|
||||
return [...filtered, ...newMsgs];
|
||||
});
|
||||
|
||||
// 标记本轮新产生的助手消息,触发"思考过程"首屏展开
|
||||
if (data.assistant_message) {
|
||||
setFreshMsgIds(prev => new Set(prev).add(data.assistant_message.id));
|
||||
}
|
||||
|
||||
// 媒体异步生成:启动轮询,待后端回写后自动刷新
|
||||
if (data.assistant_message?.generating) {
|
||||
startMediaPolling(selectedConversation!.id, data.assistant_message.id);
|
||||
@@ -1174,6 +1239,9 @@ const ChatPage: React.FC = () => {
|
||||
const clarify = msg.role === 'assistant' ? parseClarify(msg.content) : null;
|
||||
return (
|
||||
<div key={msg.id} className={`message-row ${msg.role}`}>
|
||||
{msg.role === 'assistant' && steps.length > 0 && (
|
||||
<AiProcessBlock steps={steps} autoExpand={freshMsgIds.has(msg.id)} />
|
||||
)}
|
||||
<div className={`message-bubble ${msg.role}`}>
|
||||
{clarify ? (
|
||||
<ClarifyCard
|
||||
@@ -1237,34 +1305,6 @@ const ChatPage: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{msg.role === 'assistant' && steps.length > 0 && (
|
||||
<details className="ai-process" open={false}>
|
||||
<summary className="ai-process-summary">
|
||||
<span className="ai-process-badge">🧠</span>
|
||||
思考 / 执行过程({steps.length} 步,点击展开)
|
||||
</summary>
|
||||
<div className="ai-process-list">
|
||||
{steps.map((s: any, i: number) => (
|
||||
<div key={i} className={`ai-process-item ai-process-${s.type}`}>
|
||||
<span className="ai-process-ico">
|
||||
{s.type === 'thinking' ? '💡' : s.type === 'skill' ? '🛠' : '🔌'}
|
||||
</span>
|
||||
<div className="ai-process-body">
|
||||
{s.type === 'thinking' && (
|
||||
<><b className="ai-process-title">思考</b><p className="ai-process-text">{s.text}</p></>
|
||||
)}
|
||||
{s.type === 'skill' && (
|
||||
<><b className="ai-process-title">调用技能:{s.name}</b><p className="ai-process-text">{(s.result || '').slice(0, 400)}</p></>
|
||||
)}
|
||||
{s.type === 'mcp' && (
|
||||
<><b className="ai-process-title">MCP · {s.server}:{s.name}</b><p className="ai-process-text">{(s.result || '').slice(0, 400)}</p></>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
)}
|
||||
<div className={`message-meta ${msg.role}`}>
|
||||
<span className="message-time">{formatTime(msg.createdAt)}</span>
|
||||
<span className="message-tokens">{msg.tokenUsed} tokens</span>
|
||||
|
||||
Reference in new issue
Block a user