import { useCallback, useEffect, useState } from 'react'; import { Download, Folder, RefreshCw } from 'lucide-react'; import { Button } from '@/components/ui/button'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { cloudAgentsApi } from '@/lib/cloud-agents-api'; import type { CloudAgentOperations, CloudChannelSession, CloudChannelConversation, CloudFile, CloudPendingOperation } from '../../../shared/cloud-agents'; import { CloudApproval, cloudStatus } from './CloudChat'; import { usePendingCloudInput } from './CloudPending'; const failureText = (error: unknown) => error instanceof Error ? error.message : '微信会话暂时无法读取'; const activeRun = (status?: string) => ['pending', 'queued', 'running', 'interrupted', 'preparing'].includes(status ?? ''); type Control = CloudAgentOperations['channelConversationControl']['input']; function useVisible(active: boolean) { const [visible, setVisible] = useState(document.visibilityState !== 'hidden'); useEffect(() => { const update = () => setVisible(document.visibilityState !== 'hidden'); document.addEventListener('visibilitychange', update); return () => document.removeEventListener('visibilitychange', update); }, []); return active && visible; } export function CloudChannelConversations({ slug, channelAccountId, activeView = true }: { slug: string; channelAccountId?: string; activeView?: boolean }) { const [sessions, setSessions] = useState([]); const [selected, setSelected] = useState(''); const [offset, setOffset] = useState(null); const [revision, setRevision] = useState(0); const [error, setError] = useState(''); const [loading, setLoading] = useState(false); const [busy, setBusy] = useState(false); const visible = useVisible(activeView); useEffect(() => { if (!visible) return; let live = true; setLoading(true); void cloudAgentsApi.call('channelConversations', { slug }).then(page => { if (!live) return; const items = channelAccountId ? page.items.filter(item => item.channel_account_id === channelAccountId) : page.items; setSessions(items); setOffset(page.next_offset); setError(''); setSelected(current => items.some(item => item.session_id === current) ? current : items[0]?.session_id || ''); }).catch(error => { if (live) setError(failureText(error)); }).finally(() => { if (live) setLoading(false); }); return () => { live = false; }; }, [channelAccountId, slug, visible, revision]); const more = async () => { if (loading || offset === null) return; setLoading(true); try { const page = await cloudAgentsApi.call('channelConversations', { slug, offset }); const next = channelAccountId ? page.items.filter(item => item.channel_account_id === channelAccountId) : page.items; setSessions(items => [...items, ...next]); setOffset(page.next_offset); } catch (error) { setError(failureText(error)); } finally { setLoading(false); } }; return

微信对话

查看扫码账号的微信会话,处理确认和保存文件。新消息请从对应微信对话发送。

{error &&

{error}

} {loading && !sessions.length &&

正在读取微信会话…

} {!loading && !error && !sessions.length &&

还没有微信会话。请先在“渠道 → 微信”中连接并启用账号,然后用扫码的微信发送一条消息。

} {!!sessions.length && } {offset !== null && } {selected && { setSelected(id); setRevision(value => value + 1); }} />}
; } function ChannelSession({ sessionId, visible, refreshRevision, onBusy, onNewSession }: { sessionId: string; visible: boolean; refreshRevision: number; onBusy: (busy: boolean) => void; onNewSession: (id: string) => void; }) { const [data, setData] = useState(null); const [files, setFiles] = useState([]); const [outputs, setOutputs] = useState([]); const [path, setPath] = useState('/'); const [revision, setRevision] = useState(0); const [busy, setBusy] = useState(false); const [error, setError] = useState(''); const [fileError, setFileError] = useState(''); const [notice, setNotice] = useState(''); const [pending, setPending] = useState([]); const [recoveryReady, setRecoveryReady] = useState(false); const [intent, setIntent] = useState(null); const locked = busy || !recoveryReady || Boolean(intent) || pending.length > 0; usePendingCloudInput(locked); useEffect(() => { onBusy(locked); return () => onBusy(false); }, [locked, onBusy]); const loadRecovery = useCallback(async () => { const recovery = await cloudAgentsApi.recovery(); setPending(recovery.pending.filter(item => item.operation === 'channelConversationControl' && item.input.session_id === sessionId)); setRecoveryReady(true); }, [sessionId]); useEffect(() => { void loadRecovery().catch(error => setError(failureText(error))); }, [loadRecovery]); useEffect(() => { if (!visible) return; let live = true; let timer: ReturnType; const load = async () => { try { let next = await cloudAgentsApi.call('channelConversation', { session_id: sessionId }); while (live && next.next_offset !== null) { const more = await cloudAgentsApi.call('channelConversation', { session_id: sessionId, offset: next.next_offset }); next = { ...more, messages: [...next.messages, ...more.messages] }; } if (live) setData(next); } catch (error) { if (live) setError(failureText(error)); } finally { if (live) timer = setTimeout(() => void load(), 5000); } }; void load(); return () => { live = false; clearTimeout(timer); }; }, [sessionId, visible, revision, refreshRevision]); const runId = data?.run?.agent_run_id; const runStatus = data?.run?.status ?? data?.queued_requests?.[0]?.status; const threadId = data?.thread_id; useEffect(() => { if (!visible || !threadId) return; let live = true; setFileError(''); void cloudAgentsApi.call('channelFiles', { session_id: sessionId, path }).then(async page => { if (!live) return; setFiles(page.files); setOutputs([]); const outputDirectory = path === '/' && page.files.find(file => file.is_dir && file.name === 'outputs'); if (outputDirectory) { const outputPage = await cloudAgentsApi.call('channelFiles', { session_id: sessionId, path: outputDirectory.directory_path }); if (live) setOutputs(outputPage.files.filter(file => !file.is_dir)); } }).catch(error => { if (live) setFileError(failureText(error)); }); return () => { live = false; }; }, [sessionId, path, visible, threadId, runId, runStatus, revision, refreshRevision]); const control = async (input: Control) => { const frozen = intent ?? input; setIntent(frozen); setBusy(true); setError(''); try { const result = await cloudAgentsApi.call('channelConversationControl', frozen); setIntent(null); setPending([]); if (frozen.action === 'new-session') onNewSession(result.session_id); else setRevision(value => value + 1); } catch (error) { setError(failureText(error)); // Main retains accepted or uncertain operations. Move recovery to that // durable record so a rejected control can be dismissed before another. try { await loadRecovery(); setIntent(null); } catch { /* Keep the exact input available if the journal cannot be read. */ } throw error; } finally { setBusy(false); } }; const resolve = async (operation: CloudPendingOperation, discard = false) => { setBusy(true); setError(''); try { const response = await cloudAgentsApi.resolvePending(operation.id, discard); await loadRecovery(); const result = response.result; if (!discard && operation.input.action === 'new-session' && result && typeof result === 'object' && 'session_id' in result && typeof result.session_id === 'string') onNewSession(result.session_id); else setRevision(value => value + 1); } catch (error) { setError(failureText(error)); } finally { setBusy(false); } }; const save = async (file: CloudFile) => { setBusy(true); setFileError(''); setNotice(''); try { const result = await cloudAgentsApi.downloadChannelArtifact(sessionId, file.path); if (result.saved) setNotice('已保存 ' + file.name); } catch (error) { setFileError(failureText(error)); } finally { setBusy(false); } }; const controllable = data?.session_state === 'active' && data.grant_state === 'authorized'; const running = activeRun(runStatus) || Boolean(data?.queued_requests?.length); const renderFiles = (items: CloudFile[]) => items.map(file =>
{file.is_dir ? : <>{file.name}}
); const statusText = runStatus ? cloudStatus(runStatus) : data?.session_state === 'closed' ? '历史会话 · 可以查看消息和保存文件' : '等待微信消息'; const runError = data?.run?.error; return

任务状态:{statusText}

{runError?.message &&

任务错误:{runError.type ? runError.type + ' · ' : ''}{runError.message}

}
{controllable &&
}
{pending.map(operation =>

上次会话操作的结果尚未确认。恢复会核对同一次操作。

)} {intent && } {error &&

{error}

}
{data?.messages.filter(message => message.role !== 'system').map(message =>

{message.role === 'user' ? '你 · 微信' : '智能体'}

href && /^(https?:|mailto:)/i.test(href) ? {children} : {children} }}>{message.content}
)} {data && !data.messages.length &&

还没有消息。

}
{!recoveryReady && } {recoveryReady && controllable && runStatus === 'interrupted' && data?.run?.interrupt && runId && !pending.length && !intent && control({ ...input, session_id: sessionId, action: 'resume', run_id: runId })} />}

文件与产物

{outputs.length > 0 &&

生成的文件

{renderFiles(outputs)}
} {path !== '/' && } {renderFiles(files)} {!files.length && !fileError &&

暂时没有文件。运行结束时会自动更新。

} {fileError &&

{fileError}

}{notice &&

{notice}

}
; }