第二次生成结果后台挂起
This commit is contained in:
1 parent
19399482e1
commit
90bced3ed2
12 files changed
+465
-45
No files matched your search
@@ -31,6 +31,8 @@ export interface DraftStep1Snapshot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface DraftStep2Snapshot {
|
export interface DraftStep2Snapshot {
|
||||||
|
activeRunId?: string | null;
|
||||||
|
runs?: DraftStep2RunSnapshot[];
|
||||||
/**
|
/**
|
||||||
* The seats actually selected for this session. Restored on load so the
|
* The seats actually selected for this session. Restored on load so the
|
||||||
* Step 2/3 「参与角色」panel shows the chosen agents instead of the default 6.
|
* Step 2/3 「参与角色」panel shows the chosen agents instead of the default 6.
|
||||||
@@ -57,6 +59,27 @@ export interface DraftStep2Snapshot {
|
|||||||
chain?: { id: string; title: string } | null;
|
chain?: { id: string; title: string } | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DraftStep2RunSnapshot {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
createdAt: string;
|
||||||
|
source: "initial" | "recommend" | "chain" | "manual";
|
||||||
|
selectedAgents?: Array<{ agent_id: string; name: string }>;
|
||||||
|
threadIds: ThreadIdMap | null;
|
||||||
|
coordinatorName: string | null;
|
||||||
|
step2RoundtableDialogues: unknown[];
|
||||||
|
lastLeaderContent: string;
|
||||||
|
hasConsensus: boolean;
|
||||||
|
consensusPercentage: number;
|
||||||
|
budgetLimit: number;
|
||||||
|
seatStubMessages?: Record<string, unknown[]>;
|
||||||
|
orchestrationMode?: "recommend" | "chain";
|
||||||
|
chain?: { id: string; title: string } | null;
|
||||||
|
step3?: DraftStep3Snapshot | null;
|
||||||
|
jobId?: string | null;
|
||||||
|
jobStatus?: JobStatus | null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Step 3「结果绘制」snapshot — the generated visualization report, stored inline
|
* Step 3「结果绘制」snapshot — the generated visualization report, stored inline
|
||||||
* so a reloaded draft can show it without depending on the ephemeral report
|
* so a reloaded draft can show it without depending on the ephemeral report
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ function mapJob(raw: Record<string, unknown>): RoundtableJob {
|
|||||||
chain: (raw.chain as RoundtableJob["chain"]) ?? null,
|
chain: (raw.chain as RoundtableJob["chain"]) ?? null,
|
||||||
error: (raw.error as string | null) ?? null,
|
error: (raw.error as string | null) ?? null,
|
||||||
dialogues: Array.isArray(raw.dialogues) ? (raw.dialogues as JobDialogue[]) : [],
|
dialogues: Array.isArray(raw.dialogues) ? (raw.dialogues as JobDialogue[]) : [],
|
||||||
|
step3: (raw.step3 as RoundtableJob["step3"]) ?? null,
|
||||||
createdAt: (raw.createdAt as string | null) ?? null,
|
createdAt: (raw.createdAt as string | null) ?? null,
|
||||||
updatedAt: (raw.updatedAt as string | null) ?? null,
|
updatedAt: (raw.updatedAt as string | null) ?? null,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
import type { Step1Snapshot } from "@/roundtable-planning/hooks/useStep1Intent";
|
import type { Step1Snapshot } from "@/roundtable-planning/hooks/useStep1Intent";
|
||||||
import type { Step2Snapshot } from "@/roundtable-planning/hooks/useStep2Orchestration";
|
|
||||||
import {
|
import {
|
||||||
createDraft,
|
createDraft,
|
||||||
deleteDraft,
|
deleteDraft,
|
||||||
@@ -27,6 +26,7 @@ import {
|
|||||||
getDraft,
|
getDraft,
|
||||||
listDrafts,
|
listDrafts,
|
||||||
updateDraft,
|
updateDraft,
|
||||||
|
type DraftStep2Snapshot,
|
||||||
type DraftStep3Snapshot,
|
type DraftStep3Snapshot,
|
||||||
} from "@/roundtable-planning/api/drafts";
|
} from "@/roundtable-planning/api/drafts";
|
||||||
import type { JobStatus } from "@/roundtable-planning/lib/job-types";
|
import type { JobStatus } from "@/roundtable-planning/lib/job-types";
|
||||||
@@ -51,13 +51,13 @@ export interface UseDraftPersistenceOptions {
|
|||||||
/** 读 Step 1 当前快照(由 useStep1Intent 提供的 getter)。 */
|
/** 读 Step 1 当前快照(由 useStep1Intent 提供的 getter)。 */
|
||||||
getStep1Snapshot: () => Step1Snapshot;
|
getStep1Snapshot: () => Step1Snapshot;
|
||||||
/** 读 Step 2 当前快照(由 useStep2Orchestration 提供的 getter)。 */
|
/** 读 Step 2 当前快照(由 useStep2Orchestration 提供的 getter)。 */
|
||||||
getStep2Snapshot: () => Step2Snapshot;
|
getStep2Snapshot: () => DraftStep2Snapshot;
|
||||||
/** 读 Step 3「结果绘制」当前快照(无则 null)。 */
|
/** 读 Step 3「结果绘制」当前快照(无则 null)。 */
|
||||||
getStep3Snapshot?: () => DraftStep3Snapshot | null;
|
getStep3Snapshot?: () => DraftStep3Snapshot | null;
|
||||||
/** 加载草稿时把 Step 1 状态打回去。 */
|
/** 加载草稿时把 Step 1 状态打回去。 */
|
||||||
hydrateStep1: (snapshot: Step1Snapshot) => void;
|
hydrateStep1: (snapshot: Step1Snapshot) => void;
|
||||||
/** 加载草稿时把 Step 2 状态打回去。 */
|
/** 加载草稿时把 Step 2 状态打回去。 */
|
||||||
hydrateStep2: (snapshot: Step2Snapshot) => void;
|
hydrateStep2: (snapshot: DraftStep2Snapshot, step3: DraftStep3Snapshot | null) => void;
|
||||||
/** 加载草稿时把 Step 3 结果打回去(无则传 null 清空)。 */
|
/** 加载草稿时把 Step 3 结果打回去(无则传 null 清空)。 */
|
||||||
hydrateStep3?: (snapshot: DraftStep3Snapshot | null) => void;
|
hydrateStep3?: (snapshot: DraftStep3Snapshot | null) => void;
|
||||||
/** 加载草稿前先中断所有 Step 1 / Step 2 in-flight。 */
|
/** 加载草稿前先中断所有 Step 1 / Step 2 in-flight。 */
|
||||||
@@ -187,7 +187,7 @@ export function useDraftPersistence(
|
|||||||
}, [refreshList]);
|
}, [refreshList]);
|
||||||
|
|
||||||
/** 当前会话是否有值得保存的内容。 */
|
/** 当前会话是否有值得保存的内容。 */
|
||||||
const hasSavableContent = useCallback((step1: Step1Snapshot, step2: Step2Snapshot): boolean => {
|
const hasSavableContent = useCallback((step1: Step1Snapshot, step2: DraftStep2Snapshot): boolean => {
|
||||||
const hasUserMessage = step1.interactiveMessages.some((m) => m.sender === "user");
|
const hasUserMessage = step1.interactiveMessages.some((m) => m.sender === "user");
|
||||||
return hasUserMessage || step2.step2RoundtableDialogues.length > 0;
|
return hasUserMessage || step2.step2RoundtableDialogues.length > 0;
|
||||||
}, []);
|
}, []);
|
||||||
@@ -350,7 +350,12 @@ export function useDraftPersistence(
|
|||||||
suppressStep2AutoSaveRef.current = true;
|
suppressStep2AutoSaveRef.current = true;
|
||||||
|
|
||||||
if (draft.step1) hydrateStep1(draft.step1 as Step1Snapshot);
|
if (draft.step1) hydrateStep1(draft.step1 as Step1Snapshot);
|
||||||
if (draft.step2) hydrateStep2(draft.step2 as Step2Snapshot);
|
if (draft.step2) {
|
||||||
|
hydrateStep2(
|
||||||
|
draft.step2 as DraftStep2Snapshot,
|
||||||
|
(draft.step3 as DraftStep3Snapshot | null) ?? null,
|
||||||
|
);
|
||||||
|
}
|
||||||
// 恢复 Step 3 报告(无则传 null 清掉上一条草稿残留),进入「结果绘制」即可看回。
|
// 恢复 Step 3 报告(无则传 null 清掉上一条草稿残留),进入「结果绘制」即可看回。
|
||||||
hydrateStep3?.((draft.step3 as DraftStep3Snapshot | null) ?? null);
|
hydrateStep3?.((draft.step3 as DraftStep3Snapshot | null) ?? null);
|
||||||
setStep2InitGate(draft.furthestStep >= 2);
|
setStep2InitGate(draft.furthestStep >= 2);
|
||||||
|
|||||||
@@ -336,6 +336,7 @@ export function useStep2Orchestration(
|
|||||||
const [selectedAgents, setSelectedAgents] = useState<SelectedAgent[]>(
|
const [selectedAgents, setSelectedAgents] = useState<SelectedAgent[]>(
|
||||||
DEFAULT_SELECTED_AGENTS,
|
DEFAULT_SELECTED_AGENTS,
|
||||||
);
|
);
|
||||||
|
const [initNonce, setInitNonce] = useState(0);
|
||||||
const [threadIds, setThreadIds] = useState<ThreadIdMap | null>(null);
|
const [threadIds, setThreadIds] = useState<ThreadIdMap | null>(null);
|
||||||
const [coordinatorName, setCoordinatorName] = useState<string | null>(null);
|
const [coordinatorName, setCoordinatorName] = useState<string | null>(null);
|
||||||
const [isInitializing, setIsInitializing] = useState<boolean>(false);
|
const [isInitializing, setIsInitializing] = useState<boolean>(false);
|
||||||
@@ -1538,7 +1539,7 @@ export function useStep2Orchestration(
|
|||||||
// 只关心 currentStep 切换 —— buildInitialIntent / runOrchestration / 等
|
// 只关心 currentStep 切换 —— buildInitialIntent / runOrchestration / 等
|
||||||
// 都是 useCallback / ref 包装,变化不应触发重新 init。
|
// 都是 useCallback / ref 包装,变化不应触发重新 init。
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [currentStep]);
|
}, [currentStep, initNonce]);
|
||||||
|
|
||||||
// 挂起 / 恢复:与上面的 init effect 独立。`isRoundTableRunning` toggle 时:
|
// 挂起 / 恢复:与上面的 init effect 独立。`isRoundTableRunning` toggle 时:
|
||||||
// - false → stopOrchestration
|
// - false → stopOrchestration
|
||||||
@@ -1738,6 +1739,7 @@ export function useStep2Orchestration(
|
|||||||
|
|
||||||
const resetInitGate = useCallback(() => {
|
const resetInitGate = useCallback(() => {
|
||||||
step2InitRef.current = false;
|
step2InitRef.current = false;
|
||||||
|
setInitNonce((n) => n + 1);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const hydrateFromDraft = useCallback(
|
const hydrateFromDraft = useCallback(
|
||||||
|
|||||||
@@ -71,6 +71,8 @@ export interface RoundtableJob {
|
|||||||
error: string | null;
|
error: string | null;
|
||||||
/** 研讨对话记录(leader 说明 + 各席位交付),跑完 / 进行中都可看真实内容。 */
|
/** 研讨对话记录(leader 说明 + 各席位交付),跑完 / 进行中都可看真实内容。 */
|
||||||
dialogues: JobDialogue[];
|
dialogues: JobDialogue[];
|
||||||
|
/** Step3 报告快照(done 时有):{ html, generatedAt, model, summary }。 */
|
||||||
|
step3: { html: string; generatedAt?: string; model?: string; summary?: string } | null;
|
||||||
createdAt: string | null;
|
createdAt: string | null;
|
||||||
updatedAt: string | null;
|
updatedAt: string | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,12 +60,19 @@ import { DispatchChainJobModal } from "@/roundtable-planning/components/Dispatch
|
|||||||
import { Step1Panel } from "@/roundtable-planning/components/Step1Panel";
|
import { Step1Panel } from "@/roundtable-planning/components/Step1Panel";
|
||||||
import { Step2Panel } from "@/roundtable-planning/components/Step2Panel";
|
import { Step2Panel } from "@/roundtable-planning/components/Step2Panel";
|
||||||
import { Step3Panel } from "@/roundtable-planning/components/Step3Panel";
|
import { Step3Panel } from "@/roundtable-planning/components/Step3Panel";
|
||||||
import type { DraftStep3Snapshot } from "@/roundtable-planning/api/drafts";
|
import type {
|
||||||
|
DraftStep2RunSnapshot,
|
||||||
|
DraftStep2Snapshot,
|
||||||
|
DraftStep3Snapshot,
|
||||||
|
} from "@/roundtable-planning/api/drafts";
|
||||||
import {
|
import {
|
||||||
useStep1Intent,
|
useStep1Intent,
|
||||||
type InteractiveMessage,
|
type InteractiveMessage,
|
||||||
} from "@/roundtable-planning/hooks/useStep1Intent";
|
} from "@/roundtable-planning/hooks/useStep1Intent";
|
||||||
import { useStep2Orchestration } from "@/roundtable-planning/hooks/useStep2Orchestration";
|
import {
|
||||||
|
useStep2Orchestration,
|
||||||
|
type Step2Snapshot,
|
||||||
|
} from "@/roundtable-planning/hooks/useStep2Orchestration";
|
||||||
import { useDraftPersistence } from "@/roundtable-planning/hooks/useDraftPersistence";
|
import { useDraftPersistence } from "@/roundtable-planning/hooks/useDraftPersistence";
|
||||||
import {
|
import {
|
||||||
BUILTIN_AGENT_META,
|
BUILTIN_AGENT_META,
|
||||||
@@ -101,11 +108,15 @@ export default function RoundtablePlanningPage() {
|
|||||||
// chain 模式来源链条(仅展示/持久化用)。
|
// chain 模式来源链条(仅展示/持久化用)。
|
||||||
const [chainMeta, setChainMeta] = useState<{ id: string; title: string } | null>(null);
|
const [chainMeta, setChainMeta] = useState<{ id: string; title: string } | null>(null);
|
||||||
const [step2SidebarExpanded, setStep2SidebarExpanded] = useState(true);
|
const [step2SidebarExpanded, setStep2SidebarExpanded] = useState(true);
|
||||||
|
const [step2Runs, setStep2Runs] = useState<DraftStep2RunSnapshot[]>([]);
|
||||||
|
const [activeStep2RunId, setActiveStep2RunId] = useState<string | null>(null);
|
||||||
|
|
||||||
// Step 3「结果绘制」结果 —— 持久化进草稿 step3。绘制完成由 onReportSaved 写入;
|
// Step 3「结果绘制」结果 —— 持久化进草稿 step3。绘制完成由 onReportSaved 写入;
|
||||||
// 加载草稿时 hydrateStep3 恢复;新建任务清空。step3SavedTick 递增触发一次草稿自动保存。
|
// 加载草稿时 hydrateStep3 恢复;新建任务清空。step3SavedTick 递增触发一次草稿自动保存。
|
||||||
const [step3Report, setStep3Report] = useState<DraftStep3Snapshot | null>(null);
|
const [step3Report, setStep3Report] = useState<DraftStep3Snapshot | null>(null);
|
||||||
const [step3SavedTick, setStep3SavedTick] = useState<number>(0);
|
const [step3SavedTick, setStep3SavedTick] = useState<number>(0);
|
||||||
|
const step2RunsRef = useRef<DraftStep2RunSnapshot[]>([]);
|
||||||
|
const activeStep2RunIdRef = useRef<string | null>(null);
|
||||||
|
|
||||||
// 角色面板(LeftSidebar)。原页面把它当成两步共用的展示数据,这里继续放在
|
// 角色面板(LeftSidebar)。原页面把它当成两步共用的展示数据,这里继续放在
|
||||||
// 主页 —— Step 1 / Step 2 / Step 3 都会读 / 写它,任何 hook 单独持有都不合适。
|
// 主页 —— Step 1 / Step 2 / Step 3 都会读 / 写它,任何 hook 单独持有都不合适。
|
||||||
@@ -207,6 +218,173 @@ export default function RoundtablePlanningPage() {
|
|||||||
[step2.step2RoundtableDialogues],
|
[step2.step2RoundtableDialogues],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
step2RunsRef.current = step2Runs;
|
||||||
|
}, [step2Runs]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
activeStep2RunIdRef.current = activeStep2RunId;
|
||||||
|
}, [activeStep2RunId]);
|
||||||
|
|
||||||
|
const buildCurrentStep2Snapshot = useCallback(
|
||||||
|
(): DraftStep2Snapshot => ({
|
||||||
|
selectedAgents: step2.selectedAgents,
|
||||||
|
threadIds: step2.threadIds,
|
||||||
|
coordinatorName: step2.coordinatorName,
|
||||||
|
step2RoundtableDialogues: step2.step2RoundtableDialogues,
|
||||||
|
lastLeaderContent: step2.lastLeaderContent,
|
||||||
|
hasConsensus: step2.hasConsensus,
|
||||||
|
consensusPercentage: step2.consensusPercentage,
|
||||||
|
budgetLimit: step2.budgetLimit,
|
||||||
|
seatStubMessages: step2.seatStubMessages,
|
||||||
|
orchestrationMode,
|
||||||
|
chain: chainMeta,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
chainMeta,
|
||||||
|
orchestrationMode,
|
||||||
|
step2.budgetLimit,
|
||||||
|
step2.consensusPercentage,
|
||||||
|
step2.coordinatorName,
|
||||||
|
step2.hasConsensus,
|
||||||
|
step2.lastLeaderContent,
|
||||||
|
step2.seatStubMessages,
|
||||||
|
step2.selectedAgents,
|
||||||
|
step2.step2RoundtableDialogues,
|
||||||
|
step2.threadIds,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const snapshotToRun = useCallback(
|
||||||
|
(
|
||||||
|
snapshot: DraftStep2Snapshot,
|
||||||
|
fallback: Partial<Pick<DraftStep2RunSnapshot, "id" | "title" | "createdAt" | "source">> = {},
|
||||||
|
): DraftStep2RunSnapshot => ({
|
||||||
|
id: fallback.id ?? `step2-run-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||||
|
title: fallback.title ?? "第一次生成结果",
|
||||||
|
createdAt: fallback.createdAt ?? new Date().toISOString(),
|
||||||
|
source: fallback.source ?? snapshot.orchestrationMode ?? "initial",
|
||||||
|
selectedAgents: snapshot.selectedAgents,
|
||||||
|
threadIds: snapshot.threadIds,
|
||||||
|
coordinatorName: snapshot.coordinatorName,
|
||||||
|
step2RoundtableDialogues: snapshot.step2RoundtableDialogues ?? [],
|
||||||
|
lastLeaderContent: snapshot.lastLeaderContent ?? "",
|
||||||
|
hasConsensus: Boolean(snapshot.hasConsensus),
|
||||||
|
consensusPercentage: snapshot.consensusPercentage ?? 0,
|
||||||
|
budgetLimit: snapshot.budgetLimit ?? 0,
|
||||||
|
seatStubMessages: snapshot.seatStubMessages ?? {},
|
||||||
|
orchestrationMode: snapshot.orchestrationMode,
|
||||||
|
chain: snapshot.chain ?? null,
|
||||||
|
step3: step3Report,
|
||||||
|
}),
|
||||||
|
[step3Report],
|
||||||
|
);
|
||||||
|
|
||||||
|
const mergeActiveRun = useCallback(
|
||||||
|
(
|
||||||
|
runs: DraftStep2RunSnapshot[],
|
||||||
|
activeRunId: string | null,
|
||||||
|
snapshot: DraftStep2Snapshot,
|
||||||
|
report: DraftStep3Snapshot | null,
|
||||||
|
): DraftStep2RunSnapshot[] => {
|
||||||
|
if (!activeRunId) return runs;
|
||||||
|
const idx = runs.findIndex((run) => run.id === activeRunId);
|
||||||
|
if (idx === -1) return runs;
|
||||||
|
const current = runs[idx];
|
||||||
|
const next = snapshotToRun(snapshot, {
|
||||||
|
id: current.id,
|
||||||
|
title: current.title,
|
||||||
|
createdAt: current.createdAt,
|
||||||
|
source: current.source,
|
||||||
|
});
|
||||||
|
next.step3 = report;
|
||||||
|
return runs.map((run, i) => (i === idx ? next : run));
|
||||||
|
},
|
||||||
|
[snapshotToRun],
|
||||||
|
);
|
||||||
|
|
||||||
|
const getStep2SnapshotWithRuns = useCallback((): DraftStep2Snapshot => {
|
||||||
|
const snapshot = buildCurrentStep2Snapshot();
|
||||||
|
const runs = mergeActiveRun(
|
||||||
|
step2RunsRef.current,
|
||||||
|
activeStep2RunIdRef.current,
|
||||||
|
snapshot,
|
||||||
|
step3Report,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
...snapshot,
|
||||||
|
runs,
|
||||||
|
activeRunId: activeStep2RunIdRef.current,
|
||||||
|
};
|
||||||
|
}, [buildCurrentStep2Snapshot, mergeActiveRun, step3Report]);
|
||||||
|
|
||||||
|
const normalizeStep2Runs = useCallback(
|
||||||
|
(
|
||||||
|
snapshot: DraftStep2Snapshot,
|
||||||
|
fallbackStep3: DraftStep3Snapshot | null,
|
||||||
|
): { runs: DraftStep2RunSnapshot[]; activeRunId: string | null } => {
|
||||||
|
if (snapshot.runs?.length) {
|
||||||
|
const activeRunId = snapshot.activeRunId ?? snapshot.runs[0]?.id ?? null;
|
||||||
|
const runs = snapshot.runs.map((run) =>
|
||||||
|
run.id === activeRunId && !run.step3 && fallbackStep3
|
||||||
|
? { ...run, step3: fallbackStep3 }
|
||||||
|
: run,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
runs,
|
||||||
|
activeRunId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasLegacyContent =
|
||||||
|
(snapshot.step2RoundtableDialogues?.length ?? 0) > 0 ||
|
||||||
|
Boolean(snapshot.threadIds) ||
|
||||||
|
Boolean(snapshot.lastLeaderContent);
|
||||||
|
|
||||||
|
if (!hasLegacyContent) return { runs: [], activeRunId: null };
|
||||||
|
|
||||||
|
const legacy = snapshotToRun(snapshot, {
|
||||||
|
id: "legacy-run",
|
||||||
|
title: "第一次生成结果",
|
||||||
|
source: snapshot.orchestrationMode ?? "initial",
|
||||||
|
});
|
||||||
|
legacy.step3 = fallbackStep3;
|
||||||
|
return { runs: [legacy], activeRunId: legacy.id };
|
||||||
|
},
|
||||||
|
[snapshotToRun],
|
||||||
|
);
|
||||||
|
|
||||||
|
const hydrateStep2Run = useCallback(
|
||||||
|
(run: DraftStep2RunSnapshot) => {
|
||||||
|
setOrchestrationMode(run.orchestrationMode ?? "recommend");
|
||||||
|
setChainMeta(run.chain ?? null);
|
||||||
|
setStep3Report(run.step3 ?? null);
|
||||||
|
step2.hydrateFromDraft(run as unknown as Step2Snapshot);
|
||||||
|
},
|
||||||
|
[step2],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSelectStep2Run = useCallback(
|
||||||
|
(runId: string) => {
|
||||||
|
if (runId === activeStep2RunIdRef.current) return;
|
||||||
|
step2.stopOrchestration();
|
||||||
|
const currentSnapshot = buildCurrentStep2Snapshot();
|
||||||
|
const syncedRuns = mergeActiveRun(
|
||||||
|
step2RunsRef.current,
|
||||||
|
activeStep2RunIdRef.current,
|
||||||
|
currentSnapshot,
|
||||||
|
step3Report,
|
||||||
|
);
|
||||||
|
const target = syncedRuns.find((run) => run.id === runId);
|
||||||
|
if (!target) return;
|
||||||
|
setStep2Runs(syncedRuns);
|
||||||
|
setActiveStep2RunId(runId);
|
||||||
|
hydrateStep2Run(target);
|
||||||
|
setCurrentStep(2);
|
||||||
|
},
|
||||||
|
[buildCurrentStep2Snapshot, hydrateStep2Run, mergeActiveRun, step2, step3Report],
|
||||||
|
);
|
||||||
|
|
||||||
// 草稿 hook —— 同时依赖 step1 + step2 的 snapshot / hydrate。
|
// 草稿 hook —— 同时依赖 step1 + step2 的 snapshot / hydrate。
|
||||||
const drafts = useDraftPersistence({
|
const drafts = useDraftPersistence({
|
||||||
currentStep,
|
currentStep,
|
||||||
@@ -223,26 +401,23 @@ export default function RoundtablePlanningPage() {
|
|||||||
interactiveMessages: step1.interactiveMessages,
|
interactiveMessages: step1.interactiveMessages,
|
||||||
timelineEntries: step1.timelineEntries,
|
timelineEntries: step1.timelineEntries,
|
||||||
}),
|
}),
|
||||||
getStep2Snapshot: () => ({
|
getStep2Snapshot: getStep2SnapshotWithRuns,
|
||||||
selectedAgents: step2.selectedAgents,
|
|
||||||
threadIds: step2.threadIds,
|
|
||||||
coordinatorName: step2.coordinatorName,
|
|
||||||
step2RoundtableDialogues: step2.step2RoundtableDialogues,
|
|
||||||
lastLeaderContent: step2.lastLeaderContent,
|
|
||||||
hasConsensus: step2.hasConsensus,
|
|
||||||
consensusPercentage: step2.consensusPercentage,
|
|
||||||
budgetLimit: step2.budgetLimit,
|
|
||||||
seatStubMessages: step2.seatStubMessages,
|
|
||||||
orchestrationMode,
|
|
||||||
chain: chainMeta,
|
|
||||||
}),
|
|
||||||
hydrateStep1: step1.hydrateFromDraft,
|
hydrateStep1: step1.hydrateFromDraft,
|
||||||
// 包一层:先把页面持有的编排模式 / 链条恢复出来(老草稿无字段 → recommend),
|
// 包一层:先把页面持有的编排模式 / 链条恢复出来(老草稿无字段 → recommend),
|
||||||
// 再交给 step2 hook 恢复其余 Step 2 状态。
|
// 再交给 step2 hook 恢复其余 Step 2 状态。
|
||||||
hydrateStep2: (snapshot) => {
|
hydrateStep2: (snapshot, savedStep3) => {
|
||||||
|
const normalized = normalizeStep2Runs(snapshot, savedStep3);
|
||||||
|
setStep2Runs(normalized.runs);
|
||||||
|
setActiveStep2RunId(normalized.activeRunId);
|
||||||
|
const active =
|
||||||
|
normalized.runs.find((run) => run.id === normalized.activeRunId) ?? null;
|
||||||
|
if (active) {
|
||||||
|
hydrateStep2Run(active);
|
||||||
|
return;
|
||||||
|
}
|
||||||
setOrchestrationMode(snapshot.orchestrationMode ?? "recommend");
|
setOrchestrationMode(snapshot.orchestrationMode ?? "recommend");
|
||||||
setChainMeta(snapshot.chain ?? null);
|
setChainMeta(snapshot.chain ?? null);
|
||||||
step2.hydrateFromDraft(snapshot);
|
step2.hydrateFromDraft(snapshot as unknown as Step2Snapshot);
|
||||||
},
|
},
|
||||||
cancelAllStreams: () => {
|
cancelAllStreams: () => {
|
||||||
step2.abortAllInFlight();
|
step2.abortAllInFlight();
|
||||||
@@ -309,6 +484,38 @@ export default function RoundtablePlanningPage() {
|
|||||||
// 内置 agent 走 BUILTIN_AGENT_META;非内置用候选池传过来的 name。
|
// 内置 agent 走 BUILTIN_AGENT_META;非内置用候选池传过来的 name。
|
||||||
name: BUILTIN_AGENT_META[agent_id]?.displayName ?? name ?? agent_id,
|
name: BUILTIN_AGENT_META[agent_id]?.displayName ?? name ?? agent_id,
|
||||||
}));
|
}));
|
||||||
|
step2.stopOrchestration();
|
||||||
|
const currentSnapshot = buildCurrentStep2Snapshot();
|
||||||
|
const syncedRuns = mergeActiveRun(
|
||||||
|
step2RunsRef.current,
|
||||||
|
activeStep2RunIdRef.current,
|
||||||
|
currentSnapshot,
|
||||||
|
step3Report,
|
||||||
|
);
|
||||||
|
const nextRunNumber = syncedRuns.length + 1;
|
||||||
|
const newRunId = `step2-run-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||||
|
const newRun: DraftStep2RunSnapshot = {
|
||||||
|
id: newRunId,
|
||||||
|
title: `第 ${nextRunNumber} 次生成结果`,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
source: result.mode,
|
||||||
|
selectedAgents: normalized,
|
||||||
|
threadIds: null,
|
||||||
|
coordinatorName: null,
|
||||||
|
step2RoundtableDialogues: [],
|
||||||
|
lastLeaderContent: "",
|
||||||
|
hasConsensus: false,
|
||||||
|
consensusPercentage: 0,
|
||||||
|
budgetLimit: step2.budgetLimit,
|
||||||
|
seatStubMessages: {},
|
||||||
|
orchestrationMode: result.mode,
|
||||||
|
chain: result.mode === "chain" ? result.chain ?? null : null,
|
||||||
|
step3: null,
|
||||||
|
};
|
||||||
|
setStep2Runs([...syncedRuns, newRun]);
|
||||||
|
setActiveStep2RunId(newRunId);
|
||||||
|
setStep3Report(null);
|
||||||
|
step2.resetStep2State();
|
||||||
step2.setSelectedAgents(normalized);
|
step2.setSelectedAgents(normalized);
|
||||||
step2.resetInitGate();
|
step2.resetInitGate();
|
||||||
// result.mode 决定 Step 2 编排:recommend = 总控自由派活;chain = 按链条顺序派活。
|
// result.mode 决定 Step 2 编排:recommend = 总控自由派活;chain = 按链条顺序派活。
|
||||||
@@ -322,7 +529,7 @@ export default function RoundtablePlanningPage() {
|
|||||||
: "成功确认!已切换至第二阶段圆桌研讨系统",
|
: "成功确认!已切换至第二阶段圆桌研讨系统",
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[step2, triggerToast],
|
[buildCurrentStep2Snapshot, mergeActiveRun, step2, step3Report, triggerToast],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 把页面重置回「第一步全新任务」——新建任务 + 后台挂起后都复用。
|
// 把页面重置回「第一步全新任务」——新建任务 + 后台挂起后都复用。
|
||||||
@@ -331,6 +538,8 @@ export default function RoundtablePlanningPage() {
|
|||||||
const resetToStep1 = useCallback(() => {
|
const resetToStep1 = useCallback(() => {
|
||||||
step2.stopOrchestration();
|
step2.stopOrchestration();
|
||||||
step2.resetStep2State();
|
step2.resetStep2State();
|
||||||
|
setStep2Runs([]);
|
||||||
|
setActiveStep2RunId(null);
|
||||||
setOrchestrationMode("recommend");
|
setOrchestrationMode("recommend");
|
||||||
setChainMeta(null);
|
setChainMeta(null);
|
||||||
setStep3Report(null);
|
setStep3Report(null);
|
||||||
@@ -603,6 +812,9 @@ export default function RoundtablePlanningPage() {
|
|||||||
intentReady={step1.intentReady}
|
intentReady={step1.intentReady}
|
||||||
sidebarExpanded={step2SidebarExpanded}
|
sidebarExpanded={step2SidebarExpanded}
|
||||||
setSidebarExpanded={setStep2SidebarExpanded}
|
setSidebarExpanded={setStep2SidebarExpanded}
|
||||||
|
step2Runs={step2Runs}
|
||||||
|
activeStep2RunId={activeStep2RunId}
|
||||||
|
onSelectStep2Run={handleSelectStep2Run}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* MIDDLE VIEWPORT */}
|
{/* MIDDLE VIEWPORT */}
|
||||||
@@ -700,6 +912,11 @@ export default function RoundtablePlanningPage() {
|
|||||||
savedReport={step3Report}
|
savedReport={step3Report}
|
||||||
onReportSaved={(snap) => {
|
onReportSaved={(snap) => {
|
||||||
setStep3Report(snap);
|
setStep3Report(snap);
|
||||||
|
setStep2Runs((prev) =>
|
||||||
|
prev.map((run) =>
|
||||||
|
run.id === activeStep2RunIdRef.current ? { ...run, step3: snap } : run,
|
||||||
|
),
|
||||||
|
);
|
||||||
setStep3SavedTick((t) => t + 1);
|
setStep3SavedTick((t) => t + 1);
|
||||||
}}
|
}}
|
||||||
onReset={handleStartNewTask}
|
onReset={handleStartNewTask}
|
||||||
@@ -1027,6 +1244,9 @@ interface LeftSidebarProps {
|
|||||||
intentReady: ReturnType<typeof useStep1Intent>["intentReady"];
|
intentReady: ReturnType<typeof useStep1Intent>["intentReady"];
|
||||||
sidebarExpanded: boolean;
|
sidebarExpanded: boolean;
|
||||||
setSidebarExpanded: React.Dispatch<React.SetStateAction<boolean>>;
|
setSidebarExpanded: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
step2Runs: DraftStep2RunSnapshot[];
|
||||||
|
activeStep2RunId: string | null;
|
||||||
|
onSelectStep2Run: (runId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Step2SandboxSidebarSync({
|
function Step2SandboxSidebarSync({
|
||||||
@@ -1063,6 +1283,9 @@ function LeftSidebar({
|
|||||||
intentReady,
|
intentReady,
|
||||||
sidebarExpanded,
|
sidebarExpanded,
|
||||||
setSidebarExpanded,
|
setSidebarExpanded,
|
||||||
|
step2Runs,
|
||||||
|
activeStep2RunId,
|
||||||
|
onSelectStep2Run,
|
||||||
}: LeftSidebarProps) {
|
}: LeftSidebarProps) {
|
||||||
const isStep2 = currentStep === 2;
|
const isStep2 = currentStep === 2;
|
||||||
|
|
||||||
@@ -1116,6 +1339,11 @@ function LeftSidebar({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<HumanParticipationCard />
|
<HumanParticipationCard />
|
||||||
|
<Step2RunSwitcher
|
||||||
|
runs={step2Runs}
|
||||||
|
activeRunId={activeStep2RunId}
|
||||||
|
onSelect={onSelectStep2Run}
|
||||||
|
/>
|
||||||
<TaskOverviewCard currentStep={currentStep} />
|
<TaskOverviewCard currentStep={currentStep} />
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@@ -1170,6 +1398,94 @@ function LeftSidebar({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Step2RunSwitcher({
|
||||||
|
runs,
|
||||||
|
activeRunId,
|
||||||
|
onSelect,
|
||||||
|
}: {
|
||||||
|
runs: DraftStep2RunSnapshot[];
|
||||||
|
activeRunId: string | null;
|
||||||
|
onSelect: (runId: string) => void;
|
||||||
|
}) {
|
||||||
|
if (runs.length === 0) return null;
|
||||||
|
|
||||||
|
const formatTime = (value: string) => {
|
||||||
|
const date = new Date(value);
|
||||||
|
if (Number.isNaN(date.getTime())) return "";
|
||||||
|
return date.toLocaleString("zh-CN", {
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
hour12: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const sourceLabel = (run: DraftStep2RunSnapshot) => {
|
||||||
|
if (run.source === "chain") return "业务链条";
|
||||||
|
if (run.source === "recommend") return "智能体推荐";
|
||||||
|
return "历史结果";
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusLabel = (run: DraftStep2RunSnapshot) => {
|
||||||
|
if (run.step3) return "已生成报告";
|
||||||
|
if (run.hasConsensus) return "已达成共识";
|
||||||
|
if ((run.step2RoundtableDialogues?.length ?? 0) > 0) return "研讨记录";
|
||||||
|
return "待开始";
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white border border-slate-200/50 rounded-2xl p-4 shadow-[0_2px_8px_rgba(0,0,0,0.01)]">
|
||||||
|
<div className="flex items-center gap-2 border-b border-slate-100 pb-2.5 mb-3">
|
||||||
|
<ListOrdered className="w-4 h-4 text-primary" strokeWidth={2.5} />
|
||||||
|
<h2 className="text-xs font-bold text-slate-900 uppercase tracking-widest">生成结果</h2>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{runs.map((run) => {
|
||||||
|
const active = run.id === activeRunId;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={run.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => onSelect(run.id)}
|
||||||
|
className={cn(
|
||||||
|
"w-full rounded-xl border p-2.5 text-left transition-colors",
|
||||||
|
active
|
||||||
|
? "border-primary/40 bg-primary/5 text-slate-900"
|
||||||
|
: "border-slate-100 bg-slate-50/60 text-slate-600 hover:border-primary/20 hover:bg-white",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<span className="truncate text-xs font-extrabold">{run.title}</span>
|
||||||
|
{active ? (
|
||||||
|
<CheckCircle className="h-3.5 w-3.5 shrink-0 text-primary" />
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div className="mt-1.5 flex items-center justify-between gap-2 text-[10px] font-semibold">
|
||||||
|
<span className="truncate text-slate-400">{sourceLabel(run)}</span>
|
||||||
|
<span className="shrink-0 text-slate-400">{formatTime(run.createdAt)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 flex items-center gap-1.5">
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"h-1.5 w-1.5 rounded-full",
|
||||||
|
run.step3
|
||||||
|
? "bg-emerald-500"
|
||||||
|
: run.hasConsensus
|
||||||
|
? "bg-primary"
|
||||||
|
: "bg-slate-300",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span className="text-[10px] font-bold text-slate-500">{statusLabel(run)}</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function TaskOverviewCard({ currentStep }: { currentStep: number }) {
|
function TaskOverviewCard({ currentStep }: { currentStep: number }) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-white border border-slate-200/50 rounded-2xl p-4 shadow-[0_2px_8px_rgba(0,0,0,0.01)]">
|
<div className="bg-white border border-slate-200/50 rounded-2xl p-4 shadow-[0_2px_8px_rgba(0,0,0,0.01)]">
|
||||||
|
|||||||
@@ -149,12 +149,26 @@ async def _run_agent_turn(
|
|||||||
|
|
||||||
|
|
||||||
def _read_report_html(app, thread_id: str, user_id: str) -> str | None:
|
def _read_report_html(app, thread_id: str, user_id: str) -> str | None:
|
||||||
"""从 report thread 的 outputs/方案总览.html 读出报告 HTML。"""
|
"""从 report thread 的 outputs 目录读出报告 HTML。
|
||||||
|
|
||||||
|
优先 ``方案总览.html``;找不到就扫目录里**任意 .html**(取体积最大的那个),
|
||||||
|
容忍报告 agent 用了别的文件名。
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
outputs_dir = get_paths().sandbox_outputs_dir(thread_id, user_id=user_id)
|
outputs_dir = get_paths().sandbox_outputs_dir(thread_id, user_id=user_id)
|
||||||
path = outputs_dir / _REPORT_FILE
|
preferred = outputs_dir / _REPORT_FILE
|
||||||
if path.exists():
|
if preferred.exists():
|
||||||
return path.read_text(encoding="utf-8")
|
return preferred.read_text(encoding="utf-8")
|
||||||
|
if outputs_dir.exists():
|
||||||
|
htmls = sorted(
|
||||||
|
outputs_dir.glob("*.html"),
|
||||||
|
key=lambda p: p.stat().st_size,
|
||||||
|
reverse=True,
|
||||||
|
)
|
||||||
|
if htmls:
|
||||||
|
logger.info("report html: 方案总览.html not found, using %s", htmls[0].name)
|
||||||
|
return htmls[0].read_text(encoding="utf-8")
|
||||||
|
logger.warning("report html: no .html found in %s", outputs_dir)
|
||||||
except Exception: # noqa: BLE001
|
except Exception: # noqa: BLE001
|
||||||
logger.warning("read report html failed for thread %s", thread_id, exc_info=True)
|
logger.warning("read report html failed for thread %s", thread_id, exc_info=True)
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -106,23 +106,26 @@ class RoundtableJobExecutor:
|
|||||||
mode=params.mode,
|
mode=params.mode,
|
||||||
intent_text=params.intent_text,
|
intent_text=params.intent_text,
|
||||||
)
|
)
|
||||||
# 完成且有报告:写回关联草稿的 step3 + 推进 furthest_step=3,
|
# 完成且有报告:
|
||||||
# 这样用户从历史「查看报告」加载草稿即可看到最终方案(沿用既有 hydrateStep3 路径)。
|
# 1) 把 step3 挂到 job 上 —— 前端「多运行」模型从单个 job 即可拿到完整结果
|
||||||
if (
|
# (对话 dialogues + 报告 step3)组装一个 run;
|
||||||
result.status == "done"
|
# 2) 同时写回关联草稿的顶层 step3 + 推进 furthest_step=3,兼容既有
|
||||||
and result.step3
|
# 「查看报告 → 加载草稿 → Step3」路径。
|
||||||
and params.draft_id
|
if result.status == "done" and result.step3:
|
||||||
and self._draft_store is not None
|
|
||||||
):
|
|
||||||
try:
|
try:
|
||||||
await self._draft_store.update_draft(
|
await self._store.update_progress(params.job_id, step3=result.step3)
|
||||||
params.draft_id,
|
except Exception: # noqa: BLE001
|
||||||
params.user_id,
|
logger.warning("persist step3 to job %s failed", params.job_id, exc_info=True)
|
||||||
step3=result.step3,
|
if params.draft_id and self._draft_store is not None:
|
||||||
furthest_step=3,
|
try:
|
||||||
)
|
await self._draft_store.update_draft(
|
||||||
except Exception: # noqa: BLE001 — 写报告失败不影响作业本身已 done
|
params.draft_id,
|
||||||
logger.warning("persist step3 to draft %s failed", params.draft_id, exc_info=True)
|
params.user_id,
|
||||||
|
step3=result.step3,
|
||||||
|
furthest_step=3,
|
||||||
|
)
|
||||||
|
except Exception: # noqa: BLE001 — 写报告失败不影响作业本身已 done
|
||||||
|
logger.warning("persist step3 to draft %s failed", params.draft_id, exc_info=True)
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
# 干净停止即可,**不在被取消的任务里写 DB**:取消态下二次 await 会再抛
|
# 干净停止即可,**不在被取消的任务里写 DB**:取消态下二次 await 会再抛
|
||||||
# CancelledError、并可能打断进行中的 sqlite 连接。``cancelled`` 终态由
|
# CancelledError、并可能打断进行中的 sqlite 连接。``cancelled`` 终态由
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ class RoundtableJobResponse(BaseModel):
|
|||||||
chain: dict[str, Any] | None = None
|
chain: dict[str, Any] | None = None
|
||||||
error: str | None = None
|
error: str | None = None
|
||||||
dialogues: list[dict[str, Any]] = Field(default_factory=list)
|
dialogues: list[dict[str, Any]] = Field(default_factory=list)
|
||||||
|
step3: dict[str, Any] | None = None
|
||||||
createdAt: datetime | str | None = None
|
createdAt: datetime | str | None = None
|
||||||
updatedAt: datetime | str | None = None
|
updatedAt: datetime | str | None = None
|
||||||
|
|
||||||
@@ -149,6 +150,7 @@ def _to_response(row: dict[str, Any]) -> RoundtableJobResponse:
|
|||||||
chain=row.get("chain"),
|
chain=row.get("chain"),
|
||||||
error=row.get("error"),
|
error=row.get("error"),
|
||||||
dialogues=row.get("dialogues") or [],
|
dialogues=row.get("dialogues") or [],
|
||||||
|
step3=row.get("step3"),
|
||||||
createdAt=row.get("created_at"),
|
createdAt=row.get("created_at"),
|
||||||
updatedAt=row.get("updated_at"),
|
updatedAt=row.get("updated_at"),
|
||||||
)
|
)
|
||||||
|
|||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
"""Add step3 column to roundtable_jobs.
|
||||||
|
|
||||||
|
把作业的 Step3 报告({html, generatedAt, model, summary})直接挂在 job 上,使前端
|
||||||
|
「多运行」模型(draft.step2.runs[])能从单个 job 读到完整结果(对话 + 报告)组装一个 run,
|
||||||
|
而不必依赖草稿顶层的单一 step3。LONGTEXT(HTML 可超 64 KB)。
|
||||||
|
|
||||||
|
Revision ID: 20260605_02
|
||||||
|
Revises: 20260605_01
|
||||||
|
Create Date: 2026-06-05
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
from sqlalchemy import inspect
|
||||||
|
from sqlalchemy.dialects.mysql import LONGTEXT
|
||||||
|
|
||||||
|
|
||||||
|
revision = "20260605_02"
|
||||||
|
down_revision = "20260605_01"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def _has_table(table_name: str) -> bool:
|
||||||
|
inspector = inspect(op.get_bind())
|
||||||
|
return table_name in inspector.get_table_names()
|
||||||
|
|
||||||
|
|
||||||
|
def _has_column(table_name: str, column_name: str) -> bool:
|
||||||
|
if not _has_table(table_name):
|
||||||
|
return False
|
||||||
|
inspector = inspect(op.get_bind())
|
||||||
|
return any(col["name"] == column_name for col in inspector.get_columns(table_name))
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
if _has_table("roundtable_jobs") and not _has_column("roundtable_jobs", "step3"):
|
||||||
|
col_type = LONGTEXT() if op.get_bind().dialect.name == "mysql" else sa.Text()
|
||||||
|
op.add_column("roundtable_jobs", sa.Column("step3", col_type, nullable=True))
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
if _has_column("roundtable_jobs", "step3"):
|
||||||
|
op.drop_column("roundtable_jobs", "step3")
|
||||||
+3
@@ -58,6 +58,9 @@ class RoundtableJobRow(Base):
|
|||||||
# 研讨对话记录:JSON array of { id, role('leader'|'seat'), agentId, name, content, cycle }。
|
# 研讨对话记录:JSON array of { id, role('leader'|'seat'), agentId, name, content, cycle }。
|
||||||
# 让用户跑完 / 进行中都能看到真实研讨内容(leader 说明 + 各席位交付),而非只有流程图。
|
# 让用户跑完 / 进行中都能看到真实研讨内容(leader 说明 + 各席位交付),而非只有流程图。
|
||||||
dialogues: Mapped[str | None] = mapped_column(PortableLongText, nullable=True)
|
dialogues: Mapped[str | None] = mapped_column(PortableLongText, nullable=True)
|
||||||
|
# Step3 报告快照:JSON object { html, generatedAt, model, summary }。done 时写入,
|
||||||
|
# 使前端从单个 job 即可拿到完整结果(对话 + 报告)。
|
||||||
|
step3: Mapped[str | None] = mapped_column(PortableLongText, nullable=True)
|
||||||
# Terminal error detail when status=error.
|
# Terminal error detail when status=error.
|
||||||
error: Mapped[str | None] = mapped_column(Text, nullable=True)
|
error: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||||
# 时间列必须用 BeijingDateTime(项目规范,与 ai_writing_sessions / roundtable_drafts 对齐)。
|
# 时间列必须用 BeijingDateTime(项目规范,与 ai_writing_sessions / roundtable_drafts 对齐)。
|
||||||
|
|||||||
+4
-1
@@ -55,7 +55,7 @@ class RoundtableJobRepository:
|
|||||||
for k in ("created_at", "updated_at"):
|
for k in ("created_at", "updated_at"):
|
||||||
if isinstance(d.get(k), datetime):
|
if isinstance(d.get(k), datetime):
|
||||||
d[k] = d[k].isoformat()
|
d[k] = d[k].isoformat()
|
||||||
for k in ("dispatch_chain", "thread_ids", "chain", "dialogues"):
|
for k in ("dispatch_chain", "thread_ids", "chain", "dialogues", "step3"):
|
||||||
d[k] = _loads(d.get(k))
|
d[k] = _loads(d.get(k))
|
||||||
return d
|
return d
|
||||||
|
|
||||||
@@ -119,6 +119,7 @@ class RoundtableJobRepository:
|
|||||||
thread_ids=_UNSET,
|
thread_ids=_UNSET,
|
||||||
coordinator_name=_UNSET,
|
coordinator_name=_UNSET,
|
||||||
dialogues=_UNSET,
|
dialogues=_UNSET,
|
||||||
|
step3=_UNSET,
|
||||||
error=_UNSET,
|
error=_UNSET,
|
||||||
) -> dict[str, Any] | None:
|
) -> dict[str, Any] | None:
|
||||||
resolved_user_id = resolve_user_id(user_id, method_name="RoundtableJobRepository.update_progress")
|
resolved_user_id = resolve_user_id(user_id, method_name="RoundtableJobRepository.update_progress")
|
||||||
@@ -148,6 +149,8 @@ class RoundtableJobRepository:
|
|||||||
row.coordinator_name = coordinator_name
|
row.coordinator_name = coordinator_name
|
||||||
if dialogues is not _UNSET:
|
if dialogues is not _UNSET:
|
||||||
row.dialogues = _dumps(dialogues)
|
row.dialogues = _dumps(dialogues)
|
||||||
|
if step3 is not _UNSET:
|
||||||
|
row.step3 = _dumps(step3)
|
||||||
if error is not _UNSET:
|
if error is not _UNSET:
|
||||||
row.error = error
|
row.error = error
|
||||||
row.updated_at = datetime.now(UTC)
|
row.updated_at = datetime.now(UTC)
|
||||||
|
|||||||
Reference in new issue
Block a user