fix(会商第三步): 流程图入库限 rwfx-A + 修复跳过报告生成流程图出现双调度中卡片
- 入库功能仅 rwfx-A(businessCode 6BF)深链会商模式提供,其它业务深链生成流程图不再出现入库按钮/询问 - 流程图抽取串行化:auto 自动出图与用户「跳过报告直接生成流程图」手动出图竞态时,先中止上一轮、复用/移除被取代气泡,保证任意时刻只有一张流程图卡片 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
04e6d0aa00
commit
893f1e6933
19 files changed
+559
-51
No files matched your search
@@ -20,6 +20,7 @@ import {
|
||||
readCopTaskDetail,
|
||||
resolveTaskDeeplinkTarget,
|
||||
} from "@/core/auth/task-deeplink";
|
||||
import { readUserInfo } from "@/strategy-components/lib/light-app";
|
||||
|
||||
/** 「大屏绘制」独立页应用内路由(深链「上一步」默认指向它,跳转追加 ?taskId=)。 */
|
||||
export const DASHBOARD_AGENT_ROUTE = "/page/strategy/dashboard-agent";
|
||||
@@ -66,6 +67,20 @@ export type TaskButtonIdKind = "task" | "action";
|
||||
/** ``url`` 外链打开方式:``blank``=新窗口(默认)/ ``self``=当前窗口(嵌入态即当前 iframe)。 */
|
||||
export type TaskButtonOpenMode = "blank" | "self";
|
||||
|
||||
/**
|
||||
* ``url`` 外链追加的**登录参数**(与「应用管理」一致):在拼好的跳转地址后再依次
|
||||
* 追加 ``?{key}={value}``,``value`` 运行时从 ``userInfo`` 按 ``source`` 解析
|
||||
* (``other`` 时取 ``customValue`` 字面值)。复用 [[light-app-center]] 的取值来源。
|
||||
*/
|
||||
export interface TaskButtonLoginParam {
|
||||
/** 拼到 URL 的查询参数名。 */
|
||||
key: string;
|
||||
/** 取值来源:``userInfo`` 字段名(如 ``accessToken`` / ``yUserId``)或 ``other``(自填)。 */
|
||||
source: string;
|
||||
/** ``source==="other"`` 时的自定义字面值。 */
|
||||
customValue: string;
|
||||
}
|
||||
|
||||
/** 一个可配置跳转按钮。 */
|
||||
export interface TaskButton {
|
||||
id: string;
|
||||
@@ -85,10 +100,25 @@ export interface TaskButton {
|
||||
idKind: TaskButtonIdKind;
|
||||
/** ``url`` 外链打开方式:``blank``=新窗口(默认)/ ``self``=当前窗口。仅 ``url`` 类型有意义。 */
|
||||
openMode: TaskButtonOpenMode;
|
||||
/** ``url`` 外链追加的登录参数列表(仅 ``url`` 类型有意义;为空则不追加,行为同旧版)。 */
|
||||
loginParams: TaskButtonLoginParam[];
|
||||
enabled: boolean;
|
||||
sortOrder: number;
|
||||
}
|
||||
|
||||
/** 运行时解析单个登录参数的值(``other`` 取字面值,否则从 ``userInfo`` 取 ``source`` 字段)。 */
|
||||
function resolveLoginParamValue(
|
||||
p: TaskButtonLoginParam,
|
||||
userInfo: Record<string, unknown>,
|
||||
): string {
|
||||
if (p.source === "other") return p.customValue ?? "";
|
||||
if (p.source) {
|
||||
const value = userInfo[p.source];
|
||||
return value == null ? "" : String(value);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/** 内部切换可选的目标业务(``business`` 类型按钮的下拉项)。 */
|
||||
export const TASK_BUSINESS_TARGETS: { code: TaskBusiness; label: string }[] = [
|
||||
{ code: "3qfx", label: "三情分析" },
|
||||
@@ -120,6 +150,7 @@ export function defaultTaskButtons(cfg: TaskDeeplinkConfig): TaskButton[] {
|
||||
idParam: "id",
|
||||
idKind: "task",
|
||||
openMode: "blank",
|
||||
loginParams: [],
|
||||
enabled: true,
|
||||
sortOrder,
|
||||
});
|
||||
@@ -134,6 +165,7 @@ export function defaultTaskButtons(cfg: TaskDeeplinkConfig): TaskButton[] {
|
||||
idParam: "id",
|
||||
idKind: "task",
|
||||
openMode: "blank",
|
||||
loginParams: [],
|
||||
enabled: true,
|
||||
sortOrder,
|
||||
});
|
||||
@@ -149,6 +181,7 @@ export function defaultTaskButtons(cfg: TaskDeeplinkConfig): TaskButton[] {
|
||||
idParam: "id",
|
||||
idKind: "task",
|
||||
openMode: "blank",
|
||||
loginParams: [],
|
||||
enabled: true,
|
||||
sortOrder: 0,
|
||||
},
|
||||
@@ -167,6 +200,7 @@ export function defaultTaskButtons(cfg: TaskDeeplinkConfig): TaskButton[] {
|
||||
idParam: "taskId",
|
||||
idKind: "task",
|
||||
openMode: "self",
|
||||
loginParams: [],
|
||||
enabled: true,
|
||||
sortOrder: 1,
|
||||
},
|
||||
@@ -180,6 +214,7 @@ export function defaultTaskButtons(cfg: TaskDeeplinkConfig): TaskButton[] {
|
||||
idParam: "id",
|
||||
idKind: "task",
|
||||
openMode: "blank",
|
||||
loginParams: [],
|
||||
enabled: true,
|
||||
sortOrder: 2,
|
||||
},
|
||||
@@ -193,6 +228,7 @@ export function defaultTaskButtons(cfg: TaskDeeplinkConfig): TaskButton[] {
|
||||
idParam: "id",
|
||||
idKind: "task",
|
||||
openMode: "blank",
|
||||
loginParams: [],
|
||||
enabled: true,
|
||||
sortOrder: 3,
|
||||
},
|
||||
@@ -259,7 +295,17 @@ export async function runTaskButton(btn: TaskButton, ctx: RunTaskButtonCtx): Pro
|
||||
// 行动分析(xdfx)可选追加行动id;否则用任务id。行动id 缺失时回退任务id。
|
||||
const id = btn.idKind === "action" ? ctx.actionId || ctx.taskId : ctx.taskId;
|
||||
const param = (btn.idParam || "id").trim() || "id";
|
||||
const url = btn.appendTaskId && id ? appendQueryParam(base, param, id) : base;
|
||||
let url = btn.appendTaskId && id ? appendQueryParam(base, param, id) : base;
|
||||
// 追加登录参数(与应用管理一致):运行时从 userInfo 解析每项的值后拼到地址尾部。
|
||||
const loginParams = btn.loginParams ?? [];
|
||||
if (loginParams.length > 0) {
|
||||
const userInfo = readUserInfo();
|
||||
for (const lp of loginParams) {
|
||||
const key = (lp.key || "").trim();
|
||||
if (!key) continue;
|
||||
url = appendQueryParam(url, key, resolveLoginParamValue(lp, userInfo));
|
||||
}
|
||||
}
|
||||
// 应用内部路由(以单个 / 开头,如「上一步」→ 大屏绘制页):
|
||||
// self → 走 react-router 同窗导航(不刷新、保留 SPA);blank → 拼成当前源的 hash 路由新标签打开。
|
||||
if (base.startsWith("/") && !base.startsWith("//")) {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Navigate } from "react-router-dom";
|
||||
import { ArrowDown, ArrowUp, Download, MousePointerClick, Plus, Save, Trash2 } from "lucide-react";
|
||||
import { ArrowDown, ArrowUp, Download, MousePointerClick, Plus, Save, Trash2, X } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -37,7 +37,9 @@ import {
|
||||
defaultTaskButtons,
|
||||
type TaskBusiness,
|
||||
type TaskButton,
|
||||
type TaskButtonLoginParam,
|
||||
} from "@/core/auth/task-buttons";
|
||||
import { LOGIN_SOURCE_OPTIONS } from "@/strategy-components/lib/light-app";
|
||||
import { useTaskButtonActions, useTaskButtons } from "@/strategy-components/api/task-buttons";
|
||||
|
||||
let _seq = 1;
|
||||
@@ -105,12 +107,28 @@ export default function TaskButtonsPage() {
|
||||
idParam: "id",
|
||||
idKind: "task",
|
||||
openMode: "blank",
|
||||
loginParams: [],
|
||||
enabled: true,
|
||||
sortOrder: maxSort + 1,
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
// ── 登录参数(url 外链追加,与应用管理一致)增删改 ──
|
||||
const updateLoginParams = (id: string, fn: (ps: TaskButtonLoginParam[]) => TaskButtonLoginParam[]) =>
|
||||
setRows((rs) =>
|
||||
rs.map((r) => (r.id === id ? { ...r, loginParams: fn(r.loginParams ?? []) } : r)),
|
||||
);
|
||||
|
||||
const addLoginParam = (id: string) =>
|
||||
updateLoginParams(id, (ps) => [...ps, { key: "", source: "accessToken", customValue: "" }]);
|
||||
|
||||
const patchLoginParam = (id: string, idx: number, patch: Partial<TaskButtonLoginParam>) =>
|
||||
updateLoginParams(id, (ps) => ps.map((p, i) => (i === idx ? { ...p, ...patch } : p)));
|
||||
|
||||
const removeLoginParam = (id: string, idx: number) =>
|
||||
updateLoginParams(id, (ps) => ps.filter((_, i) => i !== idx));
|
||||
|
||||
const moveRow = (id: string, dir: "up" | "down") => {
|
||||
const list = [...tabRows];
|
||||
const i = list.findIndex((r) => r.id === id);
|
||||
@@ -335,6 +353,77 @@ export default function TaskButtonsPage() {
|
||||
</Select>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 登录参数(仅外链):与「应用管理」一致,运行时从 userInfo 解析后拼到地址尾部 */}
|
||||
{r.linkType === "url" && (
|
||||
<div className="border-border/60 ml-16 flex flex-col gap-2 rounded-md border border-dashed bg-muted/30 p-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-muted-foreground text-xs">
|
||||
登录参数(追加到地址,值取自登录信息)
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-7"
|
||||
onClick={() => addLoginParam(r.id)}
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5" /> 添加登录参数
|
||||
</Button>
|
||||
</div>
|
||||
{(r.loginParams ?? []).length === 0 ? (
|
||||
<span className="text-muted-foreground text-[11px]">
|
||||
暂无登录参数,跳转时不追加额外参数。
|
||||
</span>
|
||||
) : (
|
||||
(r.loginParams ?? []).map((p, pi) => (
|
||||
<div key={pi} className="flex flex-wrap items-center gap-2">
|
||||
<Input
|
||||
value={p.key}
|
||||
onChange={(e) => patchLoginParam(r.id, pi, { key: e.target.value })}
|
||||
placeholder="参数名 key"
|
||||
className="h-8 w-32"
|
||||
title="拼到地址的查询参数名"
|
||||
/>
|
||||
<span className="text-muted-foreground text-xs">=</span>
|
||||
<Select
|
||||
value={p.source}
|
||||
onValueChange={(v) => patchLoginParam(r.id, pi, { source: v })}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-36" title="取值来源">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{LOGIN_SOURCE_OPTIONS.map((o) => (
|
||||
<SelectItem key={o.value} value={o.value}>
|
||||
{o.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{p.source === "other" && (
|
||||
<Input
|
||||
value={p.customValue}
|
||||
onChange={(e) =>
|
||||
patchLoginParam(r.id, pi, { customValue: e.target.value })
|
||||
}
|
||||
placeholder="自定义值"
|
||||
className="h-8 w-40"
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-muted-foreground hover:text-destructive h-8 w-8"
|
||||
onClick={() => removeLoginParam(r.id, pi)}
|
||||
title="删除该登录参数"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
|
||||
@@ -177,13 +177,13 @@ function ToolStep({
|
||||
const display = getStepDisplay(step.key, step.label, step.args);
|
||||
const argNode = renderStepArgs(step.key, step.args, resolveAgentName);
|
||||
const [, toolName] = step.key.split(":");
|
||||
// read_file 也可点 —— 验证类「查看文件…」步骤常出现在最后一条,点击后在沙箱里
|
||||
// 查看该文件内容(走虚拟预览或 outputs HTTP)。主聊天只让 write 类可点,这里增强。
|
||||
// 只让「自己生成/展示的文件」可点(write_file / str_replace / present_files),
|
||||
// 与主聊天页一致。read_file 不可点 —— 否则智能体调用技能时读取的 SOUL.md、
|
||||
// 执行的 .py 脚本等也会被渲染成可点击文件名,点开却没有意义。
|
||||
const isArtifactStep =
|
||||
toolName === "write_file" ||
|
||||
toolName === "str_replace" ||
|
||||
toolName === "present_files" ||
|
||||
toolName === "read_file";
|
||||
toolName === "present_files";
|
||||
|
||||
return (
|
||||
<ChainOfThoughtStep
|
||||
|
||||
@@ -963,16 +963,8 @@ export function useStep2Orchestration(
|
||||
openSeatVirtualArtifact(seatThreadId, virtualUrl, false);
|
||||
return;
|
||||
}
|
||||
if (toolName === "read_file") {
|
||||
// 验证类「查看文件…」步骤:用 args.path 在沙箱预览该文件。
|
||||
// openArtifactPreview 内部 findSeatArtifactSource 优先按 path 命中流式期间
|
||||
// write_file 缓存的内容(虚拟预览);未命中再走 seat thread 的 outputs HTTP。
|
||||
const path = step.args?.path;
|
||||
if (typeof path === "string" && path) {
|
||||
openArtifactPreview(seatThreadId, path);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// read_file 不再可点(见 MessageStepsCard 的 isArtifactStep):智能体调用技能
|
||||
// 时读取的 SOUL.md / 执行的 .py 脚本等不应作为可点击文件渲染。
|
||||
if (toolName === "present_files") {
|
||||
const filepaths = step.args?.filepaths;
|
||||
if (Array.isArray(filepaths)) {
|
||||
|
||||
@@ -584,6 +584,11 @@ export function useStep3Summary(
|
||||
// 不再要求用户说出「结构化输出/流程图」等关键词。
|
||||
const flowActiveRef = useRef(false);
|
||||
const lastAutoOpenedUrlRef = useRef<string | null>(null);
|
||||
// 流程图抽取串行化:在途抽取的 aborter + 当前承接抽取/校验的气泡 id。新一轮抽取会先中止
|
||||
// 上一轮、并复用同一张「调度中」气泡——保证任何时刻只有一张流程图卡片。修复:报告刚结束时
|
||||
// run() 的「自动出图」与用户「跳过报告直接生成流程图」的手动出图竞态,各自建卡片 → 双「调度中」。
|
||||
const flowAborterRef = useRef<AbortController | null>(null);
|
||||
const activeFlowBubbleRef = useRef<number | null>(null);
|
||||
|
||||
// ── 对话气泡三段式(append → delta → finalize)+ 工具步骤(与第 2 步同款)──
|
||||
const appendStreamingDialogue = useCallback(
|
||||
@@ -646,6 +651,25 @@ export function useStep3Summary(
|
||||
setDialogues((prev) => prev.map((d) => (d.id === id ? { ...d, sender } : d)));
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 复用一条旧气泡承接新一轮(改名 + 清空内容/步骤 + 重新转圈)。流程图抽取被新一轮抢占时
|
||||
* 复用上一轮那条气泡,避免遗留一张卡死的「调度中」卡片。
|
||||
*/
|
||||
const resetDialogueForReuse = useCallback((id: number, sender: string) => {
|
||||
setDialogues((prev) =>
|
||||
prev.map((d) =>
|
||||
d.id === id
|
||||
? { ...d, sender, content: "", steps: [], streaming: true, confidence: "调度中", time: nowTime() }
|
||||
: d,
|
||||
),
|
||||
);
|
||||
}, []);
|
||||
|
||||
/** 移除一条气泡(流程图抽取抢占时清掉被取代的那张孤儿「调度中」卡片)。 */
|
||||
const removeDialogue = useCallback((id: number) => {
|
||||
setDialogues((prev) => prev.filter((d) => d.id !== id));
|
||||
}, []);
|
||||
|
||||
const appendStepIfNew = useCallback(
|
||||
(
|
||||
bubbleId: number,
|
||||
@@ -1149,19 +1173,36 @@ export function useStep3Summary(
|
||||
opts?: { instruction?: string; currentGraph?: FlowGraphData | null; bubbleId?: number },
|
||||
): Promise<void> => {
|
||||
const isEdit = !!opts?.instruction && !!opts?.currentGraph;
|
||||
// 串行化:先中止上一轮仍在途的流程图抽取(auto 自动出图与用户手动「重画/跳过报告生成流程图」
|
||||
// 可能并发),让被抢占的旧流走 abort 分支静默退出,避免两条抽取流各自建卡片 → 双「调度中」。
|
||||
if (flowAborterRef.current && flowAborterRef.current !== aborter) {
|
||||
flowAborterRef.current.abort();
|
||||
}
|
||||
flowAborterRef.current = aborter;
|
||||
setFlowPhase("extracting");
|
||||
if (!isEdit) setFlowGraphState(null); // 编辑模式保留旧图直到新图就绪(防画布闪空)
|
||||
// 两个气泡:抽取/修改 + 校验——把过程(思考 `<think>` + 文字 + flow-json 代码块)都留着展示,
|
||||
// 由 Step2Message 渲染(思考折叠块 + markdown 代码块),不折叠成一句话。
|
||||
// 传入 bubbleId(意图分类期间已建的「调度中」气泡)则复用并改名,避免空窗 + 不重复建气泡。
|
||||
// 气泡选择:①调用方指定(意图分类期间已建的「调度中」气泡)则复用并改名;②上一轮抽取气泡仍在
|
||||
// (被本轮抢占)则复用并重置(清掉旧内容、重新转圈),不另起新卡片;③否则新建。
|
||||
const extractSender = isEdit ? "结构化流程图 · 修改" : "结构化流程图 · 抽取";
|
||||
const prevFlowBubble = activeFlowBubbleRef.current;
|
||||
let extractBubbleId: number;
|
||||
if (opts?.bubbleId != null) {
|
||||
extractBubbleId = opts.bubbleId;
|
||||
relabelDialogue(extractBubbleId, extractSender);
|
||||
} else if (prevFlowBubble != null) {
|
||||
extractBubbleId = prevFlowBubble;
|
||||
resetDialogueForReuse(extractBubbleId, extractSender);
|
||||
} else {
|
||||
extractBubbleId = appendStreamingDialogue({ sender: extractSender, avatarType: "blue" });
|
||||
}
|
||||
// 抢占场景:上一轮在途流程图气泡 ≠ 本轮要用的气泡(如 ask 用意图分类气泡承接,而 auto 流程图
|
||||
// 另起过一张)→ 移除那张被取代的孤儿「调度中」卡片,保证只剩一张。
|
||||
if (prevFlowBubble != null && prevFlowBubble !== extractBubbleId) {
|
||||
removeDialogue(prevFlowBubble);
|
||||
}
|
||||
activeFlowBubbleRef.current = extractBubbleId;
|
||||
let validateBubbleId: number | null = null;
|
||||
try {
|
||||
const graph = await streamFlowExtract({
|
||||
@@ -1179,7 +1220,10 @@ export function useStep3Summary(
|
||||
instruction: opts?.instruction,
|
||||
signal: aborter.signal,
|
||||
// 模型自动容错:后端切到下一个模型重抽 → 清空抽取气泡里失败尝试的半截内容 + 留可见提示。
|
||||
// 各回调都先看本轮是否仍是「在途的那一轮」——被新一轮抢占(aborter 已 abort)后立即
|
||||
// 变 no-op,避免旧流的迟到帧污染已被新一轮复用的同一张气泡。
|
||||
onModelSwitch: (info) => {
|
||||
if (aborter.signal.aborted) return;
|
||||
setDialogues((prev) => prev.map((d) => (d.id === extractBubbleId ? { ...d, content: "" } : d)));
|
||||
setDialogueConfidence(extractBubbleId, `模型切换:${info.failedModel}→${info.nextModel}`);
|
||||
appendStepIfNew(
|
||||
@@ -1188,18 +1232,27 @@ export function useStep3Summary(
|
||||
`model_switch:${info.nextModel}`,
|
||||
);
|
||||
},
|
||||
onExtractDelta: (t) => appendToDialogue(extractBubbleId, t),
|
||||
onExtractDelta: (t) => {
|
||||
if (aborter.signal.aborted) return;
|
||||
appendToDialogue(extractBubbleId, t);
|
||||
},
|
||||
onValidating: () => {
|
||||
if (aborter.signal.aborted) return;
|
||||
setFlowPhase("validating");
|
||||
// 抽取气泡收尾(保留已流出的内容),另起校验气泡。
|
||||
// 抽取气泡收尾(保留已流出的内容),另起校验气泡(校验阶段它成为当前流程图气泡)。
|
||||
finalizeDialogue(extractBubbleId, "", "抽取完成");
|
||||
validateBubbleId = appendStreamingDialogue({ sender: "结构化流程图 · 校验", avatarType: "blue" });
|
||||
activeFlowBubbleRef.current = validateBubbleId;
|
||||
},
|
||||
onValidateDelta: (t) => {
|
||||
if (aborter.signal.aborted) return;
|
||||
if (validateBubbleId != null) appendToDialogue(validateBubbleId, t);
|
||||
},
|
||||
});
|
||||
if (aborter.signal.aborted) return;
|
||||
// 本轮成功收尾 → 释放串行化句柄(下一轮抽取会新建气泡,而非复用本轮这条已完成的)。
|
||||
if (flowAborterRef.current === aborter) flowAborterRef.current = null;
|
||||
activeFlowBubbleRef.current = null;
|
||||
setFlowGraphState(graph);
|
||||
setFlowPhase("done");
|
||||
flowActiveRef.current = true;
|
||||
@@ -1220,6 +1273,9 @@ export function useStep3Summary(
|
||||
}
|
||||
} catch (err) {
|
||||
if (aborter.signal.aborted) return;
|
||||
// 本轮失败收尾 → 同样释放串行化句柄。
|
||||
if (flowAborterRef.current === aborter) flowAborterRef.current = null;
|
||||
activeFlowBubbleRef.current = null;
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
setFlowPhase("error");
|
||||
finalizeDialogue(
|
||||
@@ -1238,6 +1294,8 @@ export function useStep3Summary(
|
||||
appendToDialogue,
|
||||
finalizeDialogue,
|
||||
relabelDialogue,
|
||||
resetDialogueForReuse,
|
||||
removeDialogue,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1283,6 +1341,9 @@ export function useStep3Summary(
|
||||
setSeatStubMessages({});
|
||||
setFlowGraphState(null);
|
||||
setFlowPhase("idle");
|
||||
// 清空对话同时复位流程图串行化句柄(旧气泡已随 setDialogues([]) 移除,不能再被复用)。
|
||||
flowAborterRef.current = null;
|
||||
activeFlowBubbleRef.current = null;
|
||||
// 整体重跑(重试 / 重新生成)= 重新分析 → 复位入库状态,旧产物的入库询问/按钮先隐藏;
|
||||
// 重跑出新流程图后 runFlowExtract 会重新询问是否入库。
|
||||
resetIngestState();
|
||||
@@ -1316,9 +1377,13 @@ export function useStep3Summary(
|
||||
// 不再在总结线程里续问吐 flow-json(那会被 coordinator_prompt 的报告锚定带偏、不稳);
|
||||
// 改走独立端点:流式抽取(实时显示) → 校验修复 → 用校验后结构化图绘制。
|
||||
// 有报告或有席位交付即触发(无报告也能据席位交付出图)。
|
||||
// `aborterRef.current === aborter` 防竞态:用户在报告刚结束时发「跳过报告直接生成流程图」,
|
||||
// ask() 会先 abort() 本轮(aborterRef 被置空/换成新 aborter)→ 这里不再自动出图,
|
||||
// 由 ask 的手动出图独占,避免 auto + manual 双卡片。
|
||||
if (
|
||||
autoFlow &&
|
||||
!aborter.signal.aborted &&
|
||||
aborterRef.current === aborter &&
|
||||
(currentMdRef.current.trim() || seatDeliveries.length > 0)
|
||||
) {
|
||||
await runFlowExtract(aborter);
|
||||
@@ -1795,6 +1860,9 @@ export function useStep3Summary(
|
||||
(snap: DraftStep3SummarySnapshot) => {
|
||||
abort();
|
||||
runningRef.current = false;
|
||||
// 恢复历史 → 复位流程图串行化句柄(避免指向已不存在的旧气泡)。
|
||||
flowAborterRef.current = null;
|
||||
activeFlowBubbleRef.current = null;
|
||||
// 恢复流程图:快照 closing 里若存了 flow-json,解析成结构化图填进 flowGraph 状态,
|
||||
// 让画布直接用它绘制(也供 Phase 3 手动编辑)。解析不出则回退到对话气泡解析。
|
||||
const restoredGraph = snap.closing ? extractFlowGraph(snap.closing) : null;
|
||||
|
||||
@@ -150,6 +150,12 @@ const BUSINESS_CODE_TO_TASK_BUSINESS: Record<string, TaskBusiness> = {
|
||||
"7BF": "xdfx",
|
||||
};
|
||||
|
||||
/**
|
||||
* 入库仅 rwfx-A(任务分析,businessCode `6BF`)这一深链会商模式才提供——该业务较特殊,
|
||||
* 其它业务(3qfx-A / xdfx-A 等)即便深链进会商生成了流程图也不出现入库按钮 / 入库询问。
|
||||
*/
|
||||
const INGEST_BUSINESS_CODE = "6BF";
|
||||
|
||||
/** 从任务页带进来的预填(经 navigation state 传递)。见会商自动化串联(模块 C)。 */
|
||||
interface RoundtablePrefill {
|
||||
/** 任务名称(注入 Step 1 首条用户消息)。 */
|
||||
@@ -379,7 +385,8 @@ export default function RoundtablePlanningPage() {
|
||||
const ingestTaskIdForCheck = prefillRef.current?.taskId ?? null;
|
||||
const ingestBusinessCode = prefillRef.current?.businessCode ?? null;
|
||||
useEffect(() => {
|
||||
if (!ingestTaskIdForCheck) {
|
||||
// 入库仅 rwfx-A(businessCode 6BF)深链会商模式提供;其它业务一律不判定、不显示入库。
|
||||
if (!ingestTaskIdForCheck || ingestBusinessCode !== INGEST_BUSINESS_CODE) {
|
||||
setIngestAvailable(false);
|
||||
setIngestSkill("");
|
||||
return;
|
||||
|
||||
@@ -16,11 +16,18 @@ import type {
|
||||
TaskButton,
|
||||
TaskButtonIdKind,
|
||||
TaskButtonLinkType,
|
||||
TaskButtonLoginParam,
|
||||
TaskButtonOpenMode,
|
||||
} from "@/core/auth/task-buttons";
|
||||
|
||||
const BASE = () => `${getBackendBaseURL()}/api/task-buttons`;
|
||||
|
||||
interface ServerLoginParam {
|
||||
key: string;
|
||||
source: string;
|
||||
custom_value: string;
|
||||
}
|
||||
|
||||
interface ServerButton {
|
||||
id: string;
|
||||
business: string;
|
||||
@@ -31,10 +38,19 @@ interface ServerButton {
|
||||
id_param: string;
|
||||
id_kind: string;
|
||||
open_mode: string;
|
||||
login_params: ServerLoginParam[];
|
||||
enabled: boolean;
|
||||
sort_order: number;
|
||||
}
|
||||
|
||||
function toLoginParam(p: ServerLoginParam): TaskButtonLoginParam {
|
||||
return {
|
||||
key: p.key ?? "",
|
||||
source: p.source || "accessToken",
|
||||
customValue: p.custom_value ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
function toButton(s: ServerButton): TaskButton {
|
||||
return {
|
||||
id: s.id,
|
||||
@@ -46,6 +62,7 @@ function toButton(s: ServerButton): TaskButton {
|
||||
idParam: s.id_param || "id",
|
||||
idKind: (s.id_kind as TaskButtonIdKind) ?? "task",
|
||||
openMode: (s.open_mode as TaskButtonOpenMode) ?? "blank",
|
||||
loginParams: (s.login_params ?? []).map(toLoginParam),
|
||||
enabled: s.enabled ?? true,
|
||||
sortOrder: s.sort_order ?? 0,
|
||||
};
|
||||
@@ -62,6 +79,11 @@ function toBody(b: TaskButton): ServerButton {
|
||||
id_param: b.idParam,
|
||||
id_kind: b.idKind,
|
||||
open_mode: b.openMode,
|
||||
login_params: (b.loginParams ?? []).map((p) => ({
|
||||
key: p.key,
|
||||
source: p.source,
|
||||
custom_value: p.customValue,
|
||||
})),
|
||||
enabled: b.enabled,
|
||||
sort_order: b.sortOrder,
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user