import { apiFetch } from "@/core/api/fetch-client"; import { getBackendBaseURL } from "@/core/config"; import { createClientId } from "@/lib/client-id"; import { EMPTY_TASK_DRAFT, INITIAL_INTENT_TURNS, STRATEGY_ANGLES, TAIWAN_STRAIT_DEMO_EVALUATIONS, TAIWAN_STRAIT_DEMO_OUTPUTS, TAIWAN_STRAIT_DEMO_SYNTHESIS, TAIWAN_STRAIT_DEMO_TASK, TAIWAN_STRAIT_DEMO_TURNS, } from "./constants"; import type { AgentOutputState, IntentTurn, StrategyEvaluation, StrategyStage, StrategyTaskDraft, } from "./types"; export const STRATEGY_PROJECT_ID_PREFIX = "strategy-selection-"; const SNAPSHOT_KIND = "strategy-selection-project"; const DEMO_JUDGE_CONTENT = `### 评判结论 六个角度均与任务目标相关,其中治理统筹、安全风险和外交沟通直接作用于“减少误判与阻断升级”,应优先纳入;法律规则是高压情境下的必要边界。经济韧性和社会民生策略建议与主策略合并实施,避免形成两套割裂的连续性机制。 **组合提醒:** 对外沟通节奏、市场稳定措施和风险等级调整必须使用同一事实基线;任何高风险政策动作均需设置冷静期、法律复核与退出条件。`; export interface StrategyProjectSnapshot { version: 1; stage: StrategyStage; taskDraft: StrategyTaskDraft; turns: IntentTurn[]; outputs: Record; judgeStatus: AgentOutputState["status"]; judgeContent: string; evaluations: StrategyEvaluation[]; selectedStrategyIds: string[]; synthesis: string; synthesisStatus: AgentOutputState["status"]; isDemo: boolean; workflowStale: boolean; } export interface StrategyProjectSummary { id: string; title: string; brief: string; createdAt: string | null; updatedAt: string | null; stage: StrategyStage; questionCount: number; angleCount: number; completedStrategyCount: number; hasEvaluation: boolean; hasSolution: boolean; isDemo: boolean; workflowStale: boolean; } export interface StrategyProject extends StrategyProjectSummary { snapshot: StrategyProjectSnapshot; } interface ParallelTaskResponse { id: string; title?: string; brief?: string | null; agents?: Array<{ agent_id: string; name?: string; avatarType?: string | null }>; messages?: Array> | null; created_at?: string | null; updated_at?: string | null; } interface ParallelTaskListResponse { tasks?: ParallelTaskResponse[]; } interface SnapshotEnvelope extends Record { kind: typeof SNAPSHOT_KIND; version: 1; snapshot: StrategyProjectSnapshot; } const cloneTask = (task: StrategyTaskDraft): StrategyTaskDraft => ({ ...task, selectedAngleIds: [...task.selectedAngleIds], }); const cloneTurns = (turns: IntentTurn[]): IntentTurn[] => turns.map((turn) => ({ ...turn, streaming: false })); const cloneOutputs = ( outputs: Record, ): Record => Object.fromEntries( Object.entries(outputs).map(([id, output]) => [ id, { ...output, steps: [...(output.steps ?? [])] }, ]), ); const cloneEvaluations = (evaluations: StrategyEvaluation[]) => evaluations.map((item) => ({ ...item, strengths: [...item.strengths], risks: [...item.risks], })); export function createEmptyStrategySnapshot(): StrategyProjectSnapshot { return { version: 1, stage: "intent", taskDraft: cloneTask(EMPTY_TASK_DRAFT), turns: cloneTurns(INITIAL_INTENT_TURNS), outputs: {}, judgeStatus: "idle", judgeContent: "", evaluations: [], selectedStrategyIds: [], synthesis: "", synthesisStatus: "idle", isDemo: false, workflowStale: false, }; } export function createDemoStrategySnapshot(): StrategyProjectSnapshot { return { version: 1, stage: "workspace", taskDraft: cloneTask(TAIWAN_STRAIT_DEMO_TASK), turns: cloneTurns(TAIWAN_STRAIT_DEMO_TURNS), outputs: cloneOutputs(TAIWAN_STRAIT_DEMO_OUTPUTS), judgeStatus: "done", judgeContent: DEMO_JUDGE_CONTENT, evaluations: cloneEvaluations(TAIWAN_STRAIT_DEMO_EVALUATIONS), selectedStrategyIds: TAIWAN_STRAIT_DEMO_EVALUATIONS.filter( (item) => item.score >= 88, ).map((item) => item.agentId), synthesis: TAIWAN_STRAIT_DEMO_SYNTHESIS, synthesisStatus: "done", isDemo: true, workflowStale: false, }; } function isObject(value: unknown): value is Record { return Boolean(value) && typeof value === "object" && !Array.isArray(value); } function readSnapshot(task: ParallelTaskResponse): StrategyProjectSnapshot | null { const envelope = task.messages?.find( (message) => message.kind === SNAPSHOT_KIND && message.version === 1, ); if (!isObject(envelope) || !isObject(envelope.snapshot)) return null; const snapshot = envelope.snapshot as unknown as StrategyProjectSnapshot; if ( snapshot.version !== 1 || (snapshot.stage !== "intent" && snapshot.stage !== "workspace") || !isObject(snapshot.taskDraft) || !Array.isArray(snapshot.taskDraft.selectedAngleIds) || !Array.isArray(snapshot.turns) || !isObject(snapshot.outputs) || !Array.isArray(snapshot.evaluations) || !Array.isArray(snapshot.selectedStrategyIds) ) { return null; } return { ...snapshot, workflowStale: snapshot.workflowStale === true, taskDraft: cloneTask(snapshot.taskDraft), turns: cloneTurns(snapshot.turns), outputs: cloneOutputs(snapshot.outputs), evaluations: cloneEvaluations(snapshot.evaluations), selectedStrategyIds: [...snapshot.selectedStrategyIds], }; } function toProject(task: ParallelTaskResponse): StrategyProject | null { const snapshot = readSnapshot(task); if (!snapshot) return null; const completedStrategyCount = Object.values(snapshot.outputs).filter( (output) => output.status === "done" && output.content.trim(), ).length; return { id: task.id, title: snapshot.taskDraft.name.trim() || task.title?.trim() || "未命名策略项目", brief: snapshot.taskDraft.detail.trim() || task.brief?.trim() || "尚未补充项目说明", createdAt: task.created_at ?? null, updatedAt: task.updated_at ?? null, stage: snapshot.stage, questionCount: snapshot.turns.filter((turn) => turn.role === "user").length, angleCount: snapshot.taskDraft.selectedAngleIds.length, completedStrategyCount, hasEvaluation: !snapshot.workflowStale && snapshot.judgeStatus === "done" && Boolean(snapshot.judgeContent.trim()), hasSolution: !snapshot.workflowStale && snapshot.synthesisStatus === "done" && Boolean(snapshot.synthesis.trim()), isDemo: snapshot.isDemo, workflowStale: snapshot.workflowStale, snapshot, }; } async function readError(response: Response): Promise { const body = (await response.json().catch(() => null)) as | { detail?: string } | null; return body?.detail || `请求失败(HTTP ${response.status})`; } const endpoint = (suffix = "") => `${getBackendBaseURL()}/api/parallel-agent-tasks${suffix}`; async function getParallelTask(id: string): Promise { const response = await apiFetch(endpoint(`/${encodeURIComponent(id)}`)); if (!response.ok) throw new Error(await readError(response)); return (await response.json()) as ParallelTaskResponse; } export async function listStrategyProjects(): Promise { const response = await apiFetch(endpoint()); if (!response.ok) throw new Error(await readError(response)); const body = (await response.json()) as ParallelTaskListResponse; const candidates = (body.tasks ?? []).filter((task) => task.id.startsWith(STRATEGY_PROJECT_ID_PREFIX), ); const records = await Promise.allSettled( candidates.map((task) => getParallelTask(task.id)), ); return records .flatMap((result) => { if (result.status !== "fulfilled") return []; const project = toProject(result.value); return project ? [project] : []; }) .sort((a, b) => String(b.updatedAt ?? "").localeCompare(String(a.updatedAt ?? "")), ); } export async function getStrategyProject(id: string): Promise { if (!id.startsWith(STRATEGY_PROJECT_ID_PREFIX)) { throw new Error("这不是一个策略优选项目"); } const task = await getParallelTask(id); const project = toProject(task); if (!project) throw new Error("项目数据不完整,无法恢复策略工作台"); return project; } function projectAgents(snapshot: StrategyProjectSnapshot) { const selected = new Set(snapshot.taskDraft.selectedAngleIds); const angleAgents = STRATEGY_ANGLES.filter((angle) => selected.has(angle.id)).map( (angle) => ({ agent_id: angle.agentId, name: angle.name }), ); return [ { agent_id: "strategy-selection-intent", name: "意图理解智能体" }, ...angleAgents, { agent_id: "strategy-selection-judge", name: "策略评判智能体" }, { agent_id: "strategy-selection-solution", name: "方案整理智能体" }, ]; } function snapshotEnvelope(snapshot: StrategyProjectSnapshot): SnapshotEnvelope { return { kind: SNAPSHOT_KIND, version: 1, snapshot, }; } export async function createStrategyProject( snapshot: StrategyProjectSnapshot = createEmptyStrategySnapshot(), ): Promise { const id = `${STRATEGY_PROJECT_ID_PREFIX}${createClientId()}`; const response = await apiFetch(endpoint(), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id, title: snapshot.taskDraft.name.trim() || "未命名策略项目", brief: snapshot.taskDraft.detail.trim() || null, agents: projectAgents(snapshot), messages: [snapshotEnvelope(snapshot)], }), }); if (!response.ok) throw new Error(await readError(response)); const project = toProject((await response.json()) as ParallelTaskResponse); if (!project) throw new Error("项目已创建,但返回数据无法识别"); return project; } export async function saveStrategyProject( id: string, snapshot: StrategyProjectSnapshot, options: { keepalive?: boolean; signal?: AbortSignal; } = {}, ): Promise { const response = await apiFetch(endpoint(`/${encodeURIComponent(id)}`), { method: "PUT", headers: { "Content-Type": "application/json" }, keepalive: options.keepalive, signal: options.signal, body: JSON.stringify({ title: snapshot.taskDraft.name.trim() || "未命名策略项目", brief: snapshot.taskDraft.detail.trim() || null, agents: projectAgents(snapshot), messages: [snapshotEnvelope(snapshot)], }), }); if (!response.ok) throw new Error(await readError(response)); const project = toProject((await response.json()) as ParallelTaskResponse); if (!project) throw new Error("项目已保存,但返回数据无法识别"); return project; } export async function deleteStrategyProject(id: string): Promise { const response = await apiFetch(endpoint(`/${encodeURIComponent(id)}`), { method: "DELETE", }); if (!response.ok && response.status !== 404) { throw new Error(await readError(response)); } }