feat: inject 3q context into rwfx chat

This commit is contained in:
吉鑫升 committed 2026-07-02 10:03:34 +08:00
1 parent ba0c32dcee
commit 36a3851233
7 files changed
+179 -45

No files matched your search

+18 -6
View File
@@ -233,6 +233,12 @@ export async function fetchDashboardInputJson(
*/
export type SqReportStatus = "complete" | "incomplete" | "unknown";
export interface SqReportCheckResult {
status: SqReportStatus;
/** 提取后的 3q 研判情报报告;仅 status=complete 时有值。 */
report?: unknown;
}
/**
* rwfx(任务分析)进入时的「3q 是否完成」预检:调 selectSqReport,复用 ``extractSqReport``
* 判定该任务是否已有有效研判情报。判定口径与大屏绘制取数完全一致(空/全 null → 无内容)。
@@ -245,20 +251,26 @@ export type SqReportStatus = "complete" | "incomplete" | "unknown";
* (与目的树查询/行动解析的 mock 同一套机制,见 [[task-deeplink-config]]);
* - ``test=false`` → 调真实接口 ``GET {VITE_AIPI_CONSUMER_URL}/cop/selectSqReport?taskId=``。
*/
export async function fetchSqReportStatus(taskId: string): Promise<SqReportStatus> {
if (!taskId) return "unknown";
export async function fetchSqReportCheck(taskId: string): Promise<SqReportCheckResult> {
if (!taskId) return { status: "unknown" };
const cfg = await fetchTaskDeeplinkConfig();
// test=true:外部接口不可用,直接用后端 config 的假数据判定(不打真实接口)。
if (cfg.test) {
return extractSqReport(cfg.mock.sqReport, taskId) ? "complete" : "incomplete";
const report = extractSqReport(cfg.mock.sqReport, taskId);
return report ? { status: "complete", report } : { status: "incomplete" };
}
// 真实模式:打真实接口。service 拦截器统一注入鉴权并返回整包 body;空/全 null → null。
const url = sqReportUrl();
if (!url) return "unknown";
if (!url) return { status: "unknown" };
try {
const body = await requestCrudService.get(url, { taskId });
return extractSqReport(body, taskId) ? "complete" : "incomplete";
const report = extractSqReport(body, taskId);
return report ? { status: "complete", report } : { status: "incomplete" };
} catch {
return "unknown";
return { status: "unknown" };
}
}
export async function fetchSqReportStatus(taskId: string): Promise<SqReportStatus> {
return (await fetchSqReportCheck(taskId)).status;
}
+18 -9
View File
@@ -343,15 +343,24 @@ function lastHumanFromChatModelInput(input: unknown): string {
if (Array.isArray(m)) m.forEach(push);
else if (m != null) flat.push(m);
};
push(raw);
for (let i = flat.length - 1; i >= 0; i--) {
const m = flat[i];
if (!m || typeof m !== "object") continue;
const type = (m as Record<string, unknown>).type ?? (m as Record<string, unknown>).role;
if (type === "human" || type === "user") {
const text = textFromChatModelOutput(m);
if (text) return text;
}
push(raw);
for (let i = flat.length - 1; i >= 0; i--) {
const m = flat[i];
if (!m || typeof m !== "object") continue;
const obj = m as Record<string, unknown>;
const type = obj.type ?? obj.role;
if (type === "human" || type === "user") {
const kwargs = obj.additional_kwargs;
if (
kwargs &&
typeof kwargs === "object" &&
(kwargs as Record<string, unknown>).hide_from_ui === true
) {
continue;
}
const text = textFromChatModelOutput(m);
if (text) return text;
}
}
} catch {
/* 取不到忽略 */
+20 -4
View File
@@ -62,10 +62,26 @@ import {
readActionDetail,
readCopTaskDetail,
} from "@/core/auth/task-deeplink";
import type { RwfxSqGateStartContext } from "@/strategy-components/components/task-components/rwfx-sq-gate";
/** 内置「模板数据转换器」agent 的稳定 id。仅此 agent 的对话页显示「页面模板 + 渲染预览」调试入口。 */
const TEMPLATE_BUILDER_AGENT_ID = "template-json-builder";
function rwfxSqReportRunContext(
gateContext?: RwfxSqGateStartContext,
): Record<string, unknown> | undefined {
if (gateContext?.sqReport == null) return undefined;
return {
task_external_context: {
kind: "rwfx_sq_report",
title: "三情分析(3q)研判情报报告",
usage:
"这是任务分析(rwfx)问答的隐藏业务上下文。回答时应参考这些已完成的 3q 结果,但不要向用户说明页面注入了隐藏上下文,除非用户明确询问。",
data: gateContext.sqReport,
},
};
}
export default function AgentChatPage({
embeddedAgentId,
chatBasePath,
@@ -288,7 +304,7 @@ export default function AgentChatPage({
}, [debugTemplateId, thread.messages]);
const handleSubmit = useCallback(
(message: PromptInputMessage) => {
(message: PromptInputMessage, extraContext?: Record<string, unknown>) => {
setHasStartedConversation(true);
// Remember this agent so the new-chat landing screen pre-selects it next
// time (covers both direct agent chats and the ChatPage handoff replay).
@@ -296,7 +312,7 @@ export default function AgentChatPage({
window.dispatchEvent(
new CustomEvent("workspace:sandbox-resume-autoscroll"),
);
void sendMessage(threadId, message, { agent_id });
void sendMessage(threadId, message, { ...(extraContext ?? {}), agent_id });
},
[sendMessage, threadId, agent_id],
);
@@ -319,9 +335,9 @@ export default function AgentChatPage({
// 任务闸门通过(无目的数据 / 覆盖成功):发出开场信息开始问答;无开场信息则
// 仅切到对话态(隐藏面板),让用户手动输入。
const handleGateStart = useCallback(() => {
const handleGateStart = useCallback((gateContext?: RwfxSqGateStartContext) => {
const text = taskContext?.openingText?.trim();
if (text) handleSubmit({ text, files: [] });
if (text) handleSubmit({ text, files: [] }, rwfxSqReportRunContext(gateContext));
else setHasStartedConversation(true);
}, [handleSubmit, taskContext]);
@@ -31,7 +31,10 @@ 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 {
useRwfxSqGate,
type RwfxSqGateStartContext,
} from "@/strategy-components/components/task-components/rwfx-sq-gate";
import { desensitize } from "@/lib/desensitize";
import { cn } from "@/lib/utils";
@@ -164,7 +167,7 @@ export function TaskPurposePanel({
}: {
taskId: string;
/** 闸门通过(无数据 / 覆盖成功),开始问答。 */
onStart: () => void;
onStart: (context?: RwfxSqGateStartContext) => void;
/** rwfx:查询任务目的为空数据时,自动开始第一次问答(跳过手动点「开始问答」)。 */
autoStartWhenEmpty?: boolean;
}) {
@@ -209,7 +212,7 @@ export function TaskPurposePanel({
const handleStart = () => {
if (result?.hasData) setConfirmOpen(true); // 有数据 → 先确认覆盖
else onStart(); // 无数据 → 直接开始
else gateStart(onStart); // 无数据 → 经过 3q 门控后开始
};
const handleConfirmOverwrite = async () => {
@@ -218,7 +221,7 @@ export function TaskPurposePanel({
const r = await deleteTaskPurpose(taskId);
if (r.success) {
setConfirmOpen(false);
onStart();
gateStart(onStart);
} else {
toast.error(r.message || "无法覆盖,请稍后再试");
setConfirmOpen(false);
@@ -1,7 +1,7 @@
/**
* rwfx(任务分析)专用「3q 预检门控」。
*
* 进入任务分析工作区时调研判情报报告接口(``fetchSqReportStatus``)判断 3q 是否完成:
* 进入任务分析工作区时调研判情报报告接口(``fetchSqReportCheck``)判断 3q 是否完成:
* - ``incomplete``(3q 未完成 / 无数据)→ 弹「3q 未完成,是否前往 3qfx」框(暂不 / 前往 3qfx)。
* - ``complete`` / ``unknown`` → 不打扰。
*
@@ -37,11 +37,16 @@ import {
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { fetchSqReportStatus } from "@/core/auth/dashboard-input";
import { fetchSqReportCheck } from "@/core/auth/dashboard-input";
import { readCopTaskDetail, resolveTaskDeeplinkTarget } from "@/core/auth/task-deeplink";
type GateStatus = "loading" | "incomplete" | "ok";
export interface RwfxSqGateStartContext {
/** 已完成的 3q 研判情报报告。只随 run context 传给模型,不在页面展示。 */
sqReport?: unknown;
}
interface RwfxSqGateValue {
/**
* rwfx 自动开始第一次问答的门控。
@@ -49,7 +54,7 @@ interface RwfxSqGateValue {
* - 3q 未完成 → 挂起 ``start``,弹「前往 3qfx」框;用户点「暂不」才 ``start()``,点「前往 3qfx」跳转并丢弃。
* 非 rwfx / 无 Provider → 直接 ``start()``(默认值)。
*/
gateStart: (start: () => void) => void;
gateStart: (start: (context?: RwfxSqGateStartContext) => void) => void;
}
const RwfxSqGateContext = createContext<RwfxSqGateValue>({
@@ -72,12 +77,19 @@ export function RwfxSqGateProvider({
const [jumping, setJumping] = useState(false);
const statusRef = useRef<GateStatus>(taskId ? "loading" : "ok");
const sqReportRef = useRef<unknown | null>(null);
// 待触发的「开始问答」回调:3q 未完成时挂起,点「暂不」/关闭弹框后触发。
const pendingStartRef = useRef<(() => void) | null>(null);
const pendingStartRef = useRef<((context?: RwfxSqGateStartContext) => void) | null>(null);
const buildStartContext = useCallback((): RwfxSqGateStartContext | undefined => {
const sqReport = sqReportRef.current;
return sqReport == null ? undefined : { sqReport };
}, []);
// taskId 变化:重置门控状态并预检 3q。
useEffect(() => {
pendingStartRef.current = null;
sqReportRef.current = null;
setOpen(false);
if (!taskId) {
statusRef.current = "ok";
@@ -85,43 +97,44 @@ export function RwfxSqGateProvider({
}
statusRef.current = "loading";
let alive = true;
void fetchSqReportStatus(taskId).then((s) => {
void fetchSqReportCheck(taskId).then((result) => {
if (!alive) return;
// complete / unknown 都按「放行」(不打扰);仅明确 incomplete 才门控。
const next: GateStatus = s === "incomplete" ? "incomplete" : "ok";
const next: GateStatus = result.status === "incomplete" ? "incomplete" : "ok";
statusRef.current = next;
sqReportRef.current = result.status === "complete" ? (result.report ?? null) : null;
if (next === "incomplete") {
setOpen(true); // 进入即提醒(无论是否有挂起的自动发送)
} else if (pendingStartRef.current) {
// 加载期间已请求自动开始 → 放行后补发。
const start = pendingStartRef.current;
pendingStartRef.current = null;
start();
start(buildStartContext());
}
});
return () => {
alive = false;
};
}, [taskId]);
}, [buildStartContext, taskId]);
const gateStart = useCallback((start: () => void) => {
const gateStart = useCallback((start: (context?: RwfxSqGateStartContext) => void) => {
const status = statusRef.current;
if (status === "ok") {
start();
start(buildStartContext());
return;
}
// loading / incomplete:挂起。loading 解析为 ok 时补发;incomplete 时等用户「暂不」。
pendingStartRef.current = start;
if (status === "incomplete") setOpen(true);
}, []);
}, [buildStartContext]);
/** 暂不(不去 3q 分析)/ 关闭弹框 → 补发挂起的「开始问答」(无则无操作)。 */
const stayAndStart = useCallback(() => {
setOpen(false);
const start = pendingStartRef.current;
pendingStartRef.current = null;
start?.();
}, []);
start?.(buildStartContext());
}, [buildStartContext]);
const handleGo3qfx = async () => {
const detail = readCopTaskDetail();