fix(rwfx): 3q 无数据时不默认发送问题,仅「不去3q分析」才发送

问题:rwfx 进入时的 3q 预检弹框与「目的树为空自动发送第一次问题」相互独立,
导致 3q 无数据时弹框出现的同时问题已被自动发出。

修复:新增 rwfx 专用 3q 门控(rwfx-sq-gate.tsx,RwfxSqGateProvider + useRwfxSqGate),
把自动发送交给门控——
- 3q 完成/无法判定 → 立即发送;
- 3q 未完成 → 弹「前往 3qfx」框并挂起发送,仅当用户「暂不(不去3q分析)」或关闭弹框才发送;
  点「前往 3qfx」则跳转并丢弃发送。
TaskPurposePanel 自动开始改调 gateStart(onStart);手动「开始问答」/覆盖确认不受影响。
替换原独立的 RwfxSqReportGate(它弹同一个框但没门控自动发送)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
吉鑫升andClaude Opus 4.8 committed 2026-06-23 17:48:23 +08:00
1 parent d2be3b206d
commit 23d170cde1
4 files changed
+202 -83

No files matched your search

@@ -33,10 +33,8 @@ import { deleteTaskPurpose, readTaskContext } from "@/core/auth/task-purpose";
import {
fetchActionDetail,
readCopTaskDetail,
resolveTaskDeeplinkTarget,
type ActionDetail,
} from "@/core/auth/task-deeplink";
import { fetchSqReportStatus } from "@/core/auth/dashboard-input";
import { TASK_BUSINESSES, type TaskBusiness } from "@/core/auth/task-buttons";
import { useTaskThreads } from "@/core/threads/use-task-threads";
import { titleOfThread } from "@/core/threads/utils";
@@ -45,6 +43,7 @@ import { cn } from "@/lib/utils";
import AgentChatPage from "@/pages/AgentChatPage";
import { ChatRuntime } from "@/pages/WorkspaceRoutes";
import { TaskJumpBar } from "@/strategy-components/components/task-components/TaskJumpBar";
import { RwfxSqGateProvider } from "@/strategy-components/components/task-components/rwfx-sq-gate";
function useChatsBase(taskBase: string): {
taskId: string;
@@ -293,82 +292,6 @@ function TaskClearButton({ taskId }: { taskId: string }) {
);
}
/**
* rwfx 专用「3q 完成预检」闸门:进入任务分析工作区时调研判情报报告接口
* (``GET {VITE_AIPI_CONSUMER_URL}/cop/selectSqReport?taskId=``)。若该任务的报告为空
* (数组为空 / 内容全为 null,即 3q 未完成)→ 弹框提示「3q 未完成,是否进行 3qfx」,
* 并提供「前往 3qfx」按钮切换到**同一任务**的三情分析工作区(复用深链解析重绑任务上下文)。
* 报告正常 / 接口不可判定(缺地址、请求失败)时不打扰用户。见 [[dashboard-input]] / [[task-deeplink]]。
*/
function RwfxSqReportGate({ taskId }: { taskId: string }) {
const navigate = useNavigate();
const [open, setOpen] = useState(false);
const [jumping, setJumping] = useState(false);
useEffect(() => {
if (!taskId) return;
let alive = true;
void fetchSqReportStatus(taskId).then((status) => {
// 仅「明确为空(3q 未完成)」才弹框;complete / unknown 静默放行。
if (alive && status === "incomplete") setOpen(true);
});
return () => {
alive = false;
};
}, [taskId]);
const handleGo3qfx = async () => {
const detail = readCopTaskDetail();
if (!detail || !taskId) {
toast.error("缺少任务信息,无法跳转");
return;
}
setJumping(true);
try {
// 复用深链解析:会清掉当前 rwfx 任务上下文并按 3qfx 业务重新绑定(同一 taskId)。
const target = await resolveTaskDeeplinkTarget("3qfx", detail, taskId);
if (target) {
setOpen(false);
navigate(target.path, { state: target.state });
} else {
toast.error("未配置三情分析(3qfx)对应的业务智能体");
}
} catch {
toast.error("跳转失败,请稍后再试");
} finally {
setJumping(false);
}
};
return (
<Dialog open={open} onOpenChange={(o) => !jumping && setOpen(o)}>
<DialogContent>
<DialogHeader>
<DialogTitle>3q 未完成</DialogTitle>
<DialogDescription>
未查询到该任务的研判情报报告(3q 未完成),是否前往该任务的三情分析(3qfx)?
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setOpen(false)} disabled={jumping}>
暂不
</Button>
<Button
// 弹框经 Radix Portal 渲染到 body,在 `.strategy-data` 作用域之外,故不能用
// 局部的 `--color-main`(红主题侥幸有值、蓝主题失效→按钮消失)。改用全局按主题
// 定义的品牌色 token(:root/.dark/.lightblue 都有),三套主题都能正确取色。
className="bg-[var(--workspace-brand)] text-[var(--workspace-brand-foreground)] hover:opacity-90"
onClick={handleGo3qfx}
disabled={jumping}
>
{jumping ? <Loader2 className="h-4 w-4 animate-spin" /> : "前往 3qfx"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
/**
* 任务工作区布局:左列表 + 右 Outlet。
* ``lite``(3qfx/xdfx)= 精简版:左列表无跳转按钮、无底部任务操作面板。
@@ -383,7 +306,7 @@ export function TaskFxWorkspace({
lite?: boolean;
}) {
const { taskId, agentId, actionId, chatsBase } = useChatsBase(taskBase);
return (
const content = (
<div className="flex size-full min-h-0 flex-col overflow-hidden">
<div className="flex min-h-0 flex-1 overflow-hidden">
<TaskConversationsList
@@ -399,10 +322,14 @@ export function TaskFxWorkspace({
</div>
</div>
<TaskJumpBar business={business} taskId={taskId} />
{/* rwfx 专属:进入时预检 3q 是否完成,未完成则弹框引导去 3qfx。 */}
{business === "rwfx" && <RwfxSqReportGate taskId={taskId} />}
</div>
);
// rwfx 专属:3q 预检门控。进入时若 3q 未完成弹「前往 3qfx」框;并门控目的树为空时的
// 自动发送——3q 无数据时不默认发送,仅当用户点「暂不(不去 3q 分析)」才发送。
if (business === "rwfx") {
return <RwfxSqGateProvider taskId={taskId}>{content}</RwfxSqGateProvider>;
}
return content;
}
/** chats 索引:有历史对话 → 跳最新;否则 → 跳 new(rwfx 走闸门 / 3qfx/xdfx 直接带开场信息)。 */
@@ -31,6 +31,7 @@ import {
type TaskPurposeResult,
} from "@/core/auth/task-purpose";
import { fetchTaskIdFromAction } from "@/core/auth/task-deeplink";
import { useRwfxSqGate } from "@/strategy-components/components/task-components/rwfx-sq-gate";
import { desensitize } from "@/lib/desensitize";
import { cn } from "@/lib/utils";
@@ -176,6 +177,8 @@ export function TaskPurposePanel({
const [collapsed, setCollapsed] = useState(true);
// rwfx 空数据自动开始:防止重复触发。
const autoStartedRef = useRef(false);
// rwfx 3q 预检门控:3q 无数据时不默认发送,交由门控(仅「暂不」才发送)。
const { gateStart } = useRwfxSqGate();
useEffect(() => {
let cancelled = false;
@@ -185,9 +188,10 @@ export function TaskPurposePanel({
if (cancelled) return;
setResult(r);
// 查询任务目的为空数据 → 默认直接开始第一次问答(跳过手动「开始问答」)。
// 经 3q 门控:3q 完成/未知立即发送;3q 未完成则挂起,仅当用户「暂不(不去 3q 分析)」才发送。
if (autoStartWhenEmpty && !r.hasData && !autoStartedRef.current) {
autoStartedRef.current = true;
onStart();
gateStart(onStart);
}
})
.catch(() => {
@@ -0,0 +1,188 @@
/**
* rwfx(任务分析)专用「3q 预检门控」。
*
* 进入任务分析工作区时调研判情报报告接口(``fetchSqReportStatus``)判断 3q 是否完成:
* - ``incomplete``(3q 未完成 / 无数据)→ 弹「3q 未完成,是否前往 3qfx」框(暂不 / 前往 3qfx)。
* - ``complete`` / ``unknown`` → 不打扰。
*
* 关键点:rwfx「目的树为空时自动发送第一次问题」必须**经本门控**(``gateStart``),不能在 3q
* 无数据时还默认发送:
* - 3q 完成 / 无法判定 → 立即发送;
* - 3q 未完成 → 挂起,**仅当用户点「暂不」(不去 3q 分析)才发送**;点「前往 3qfx」则跳转、丢弃发送。
*
* Provider 由 ``TaskFxWorkspace``(仅 rwfx)包裹整个工作区;``TaskPurposePanel`` 经 ``useRwfxSqGate``
* 读取门控。非 rwfx / 无 Provider → ``gateStart`` 直接发送(默认值)。见 [[dashboard-input]] / [[task-deeplink]]。
*/
import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
type ReactNode,
} from "react";
import { useNavigate } from "react-router-dom";
import { Loader2 } from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { fetchSqReportStatus } from "@/core/auth/dashboard-input";
import { readCopTaskDetail, resolveTaskDeeplinkTarget } from "@/core/auth/task-deeplink";
type GateStatus = "loading" | "incomplete" | "ok";
interface RwfxSqGateValue {
/**
* rwfx 自动开始第一次问答的门控。
* - 3q 完成 / 无法判定 → 立即 ``start()``。
* - 3q 未完成 → 挂起 ``start``,弹「前往 3qfx」框;用户点「暂不」才 ``start()``,点「前往 3qfx」跳转并丢弃。
* 非 rwfx / 无 Provider → 直接 ``start()``(默认值)。
*/
gateStart: (start: () => void) => void;
}
const RwfxSqGateContext = createContext<RwfxSqGateValue>({
gateStart: (start) => start(),
});
export function useRwfxSqGate(): RwfxSqGateValue {
return useContext(RwfxSqGateContext);
}
export function RwfxSqGateProvider({
taskId,
children,
}: {
taskId: string;
children: ReactNode;
}) {
const navigate = useNavigate();
const [open, setOpen] = useState(false);
const [jumping, setJumping] = useState(false);
const statusRef = useRef<GateStatus>(taskId ? "loading" : "ok");
// 待触发的「开始问答」回调:3q 未完成时挂起,点「暂不」/关闭弹框后触发。
const pendingStartRef = useRef<(() => void) | null>(null);
// taskId 变化:重置门控状态并预检 3q。
useEffect(() => {
pendingStartRef.current = null;
setOpen(false);
if (!taskId) {
statusRef.current = "ok";
return;
}
statusRef.current = "loading";
let alive = true;
void fetchSqReportStatus(taskId).then((s) => {
if (!alive) return;
// complete / unknown 都按「放行」(不打扰);仅明确 incomplete 才门控。
const next: GateStatus = s === "incomplete" ? "incomplete" : "ok";
statusRef.current = next;
if (next === "incomplete") {
setOpen(true); // 进入即提醒(无论是否有挂起的自动发送)
} else if (pendingStartRef.current) {
// 加载期间已请求自动开始 → 放行后补发。
const start = pendingStartRef.current;
pendingStartRef.current = null;
start();
}
});
return () => {
alive = false;
};
}, [taskId]);
const gateStart = useCallback((start: () => void) => {
const status = statusRef.current;
if (status === "ok") {
start();
return;
}
// loading / incomplete:挂起。loading 解析为 ok 时补发;incomplete 时等用户「暂不」。
pendingStartRef.current = start;
if (status === "incomplete") setOpen(true);
}, []);
/** 暂不(不去 3q 分析)/ 关闭弹框 → 补发挂起的「开始问答」(无则无操作)。 */
const stayAndStart = useCallback(() => {
setOpen(false);
const start = pendingStartRef.current;
pendingStartRef.current = null;
start?.();
}, []);
const handleGo3qfx = async () => {
const detail = readCopTaskDetail();
if (!detail || !taskId) {
toast.error("缺少任务信息,无法跳转");
return;
}
setJumping(true);
try {
// 复用深链解析:清掉当前 rwfx 任务上下文并按 3qfx 业务重新绑定(同一 taskId)。
const target = await resolveTaskDeeplinkTarget("3qfx", detail, taskId);
if (target) {
pendingStartRef.current = null; // 去 3q 分析 → 丢弃自动发送
setOpen(false);
navigate(target.path, { state: target.state });
} else {
toast.error("未配置三情分析(3qfx)对应的业务智能体");
}
} catch {
toast.error("跳转失败,请稍后再试");
} finally {
setJumping(false);
}
};
const value = useMemo<RwfxSqGateValue>(() => ({ gateStart }), [gateStart]);
return (
<RwfxSqGateContext.Provider value={value}>
{children}
<Dialog
open={open}
onOpenChange={(o) => {
if (jumping) return;
if (o) setOpen(true);
else stayAndStart(); // 关闭(暂不/ESC/点外部)= 不去 3q 分析 → 发送
}}
>
<DialogContent>
<DialogHeader>
<DialogTitle>3q 未完成</DialogTitle>
<DialogDescription>
未查询到该任务的研判情报报告(3q 未完成),是否前往该任务的三情分析(3qfx)?
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={stayAndStart} disabled={jumping}>
暂不
</Button>
<Button
// 弹框经 Radix Portal 渲染到 body,在 `.strategy-data` 作用域之外,故用全局按主题
// 定义的品牌色 token(:root/.dark/.lightblue 都有),三套主题都能正确取色。
className="bg-[var(--workspace-brand)] text-[var(--workspace-brand-foreground)] hover:opacity-90"
onClick={handleGo3qfx}
disabled={jumping}
>
{jumping ? <Loader2 className="h-4 w-4 animate-spin" /> : "前往 3qfx"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</RwfxSqGateContext.Provider>
);
}