feat: add unified strategy project workflow

This commit is contained in:
Administrator committed 2026-09-09 15:40:29 +08:00
1 parent 67d12929fc
commit a26fd32eba
28 files changed
+3382 -191

No files matched your search

@@ -0,0 +1,336 @@
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<string, AgentOutputState>;
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<Record<string, unknown>> | null;
created_at?: string | null;
updated_at?: string | null;
}
interface ParallelTaskListResponse {
tasks?: ParallelTaskResponse[];
}
interface SnapshotEnvelope extends Record<string, unknown> {
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<string, AgentOutputState>,
): Record<string, AgentOutputState> =>
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<string, unknown> {
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<string> {
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<ParallelTaskResponse> {
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<StrategyProjectSummary[]> {
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<StrategyProject> {
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<StrategyProject> {
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<StrategyProject> {
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<void> {
const response = await apiFetch(endpoint(`/${encodeURIComponent(id)}`), {
method: "DELETE",
});
if (!response.ok && response.status !== 404) {
throw new Error(await readError(response));
}
}