Merge branch 'dhk-1' of http://192.168.0.200/knowledge-base-bj/deerflow-server into dhk-1
This commit is contained in:
commit
96947d7636
8 files changed
+294
-73
No files matched your search
@@ -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;
|
||||
}
|
||||
@@ -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 {
|
||||
/* 取不到忽略 */
|
||||
|
||||
@@ -62,9 +62,60 @@ 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";
|
||||
const RWFX_SQ_REPORT_CONTEXT_MAX_CHARS = 12000;
|
||||
const RWFX_SQ_REPORT_CONTEXT_TAIL_CHARS = 2000;
|
||||
|
||||
function stringifyForHiddenContext(value: unknown): string {
|
||||
if (typeof value === "string") return value;
|
||||
try {
|
||||
const text = JSON.stringify(value, null, 2);
|
||||
return typeof text === "string" ? text : "";
|
||||
} catch {
|
||||
return String(value ?? "");
|
||||
}
|
||||
}
|
||||
|
||||
function truncateMiddle(text: string, maxChars: number): string {
|
||||
if (text.length <= maxChars) return text;
|
||||
const marker = "\n\n...[内容过长,已截断中间部分]...\n\n";
|
||||
const tailChars = Math.min(RWFX_SQ_REPORT_CONTEXT_TAIL_CHARS, Math.floor(maxChars / 4));
|
||||
const headChars = Math.max(0, maxChars - marker.length - tailChars);
|
||||
return `${text.slice(0, headChars)}${marker}${text.slice(-tailChars)}`;
|
||||
}
|
||||
|
||||
function compactSqReportForContext(report: unknown): unknown {
|
||||
if (report == null) return null;
|
||||
const serialized = stringifyForHiddenContext(report);
|
||||
if (serialized.length <= RWFX_SQ_REPORT_CONTEXT_MAX_CHARS) return report;
|
||||
return {
|
||||
truncated: true,
|
||||
originalChars: serialized.length,
|
||||
maxChars: RWFX_SQ_REPORT_CONTEXT_MAX_CHARS,
|
||||
content: truncateMiddle(serialized, RWFX_SQ_REPORT_CONTEXT_MAX_CHARS),
|
||||
};
|
||||
}
|
||||
|
||||
function rwfxSqReportRunContext(
|
||||
gateContext?: RwfxSqGateStartContext,
|
||||
): Record<string, unknown> | undefined {
|
||||
if (!gateContext?.sqReportStatus) return undefined;
|
||||
return {
|
||||
task_external_context: {
|
||||
kind: "rwfx_sq_report",
|
||||
status: gateContext.sqReportStatus,
|
||||
title: "三情分析(3q)研判情报报告",
|
||||
usage:
|
||||
gateContext.sqReportStatus === "complete"
|
||||
? "这是任务分析(rwfx)问答的隐藏业务上下文。回答时应参考这些已完成的 3q 结果;如 data.truncated=true,说明内容已截断,只能基于可见片段分析。不要向用户说明页面注入了隐藏上下文,除非用户明确询问。"
|
||||
: "3q 研判情报报告当前未提供有效结果。回答时不要编造 3q 结果,可仅基于任务信息继续分析。",
|
||||
data: compactSqReportForContext(gateContext.sqReport),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default function AgentChatPage({
|
||||
embeddedAgentId,
|
||||
@@ -288,7 +339,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 +347,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 +370,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]);
|
||||
|
||||
|
||||
@@ -2729,7 +2729,8 @@ function LeftSidebar({
|
||||
ref={dzRef}
|
||||
id="left-sidebar"
|
||||
className={cn(
|
||||
"shrink-0 flex flex-col gap-3 overflow-y-auto pr-1 transition-[width] duration-200 ease-out",
|
||||
"min-h-0 shrink-0 flex flex-col gap-3 pr-1 transition-[width] duration-200 ease-out",
|
||||
isStep2 ? "overflow-y-auto" : "overflow-hidden",
|
||||
isStep2 && !sidebarExpanded ? "w-[56px]" : "w-[280px]",
|
||||
)}
|
||||
>
|
||||
@@ -2911,6 +2912,7 @@ function RoundtableEntryRail({
|
||||
businessCode?: string | null;
|
||||
}) {
|
||||
const [chains, setChains] = useState<BusinessChain[]>([]);
|
||||
const [chainSectionExpanded, setChainSectionExpanded] = useState(true);
|
||||
const [expandedChainId, setExpandedChainId] = useState<string | null>(null);
|
||||
const { drafts: records, refresh } = useRoundtableDrafts(taskId);
|
||||
// 分析记录「按业务链筛选」—— 选项从已加载草稿的 chainTitle 去重得到,
|
||||
@@ -2961,7 +2963,7 @@ function RoundtableEntryRail({
|
||||
let cancelled = false;
|
||||
void listChains("public")
|
||||
.then((all) => {
|
||||
if (!cancelled) setChains(all.slice(0, 3));
|
||||
if (!cancelled) setChains(all);
|
||||
})
|
||||
.catch(() => {
|
||||
/* 业务链非关键,静默忽略 */
|
||||
@@ -2985,7 +2987,7 @@ function RoundtableEntryRail({
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex min-h-0 flex-1 flex-col gap-3">
|
||||
{/* 深链接(taskId)模式:左上角展示「当前任务」(rwfx/3qfx)或「当前行动」(xdfx),替代业务链选择。 */}
|
||||
{deeplink ? (
|
||||
isXdfx ? (
|
||||
@@ -3028,33 +3030,60 @@ function RoundtableEntryRail({
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
/* 业务链(公共,前 3 个) */
|
||||
/* 业务链(公共列表,内部滚动) */
|
||||
<div className="bg-white border border-[#ECE7E4] rounded-2xl p-4 shadow-[0_4px_20px_rgba(0,0,0,0.02)] transition-shadow duration-300 hover:shadow-[0_4px_24px_rgba(45,37,34,0.04)]">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="flex items-center justify-center w-5 h-5 rounded-md bg-primary text-primary-foreground">
|
||||
<Layers className="w-3 h-3" strokeWidth={2.5} />
|
||||
</span>
|
||||
<h2 className="text-sm font-semibold tracking-wide text-slate-900">业务链</h2>
|
||||
<span className="ml-auto flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-neutral-100 text-sm font-bold tabular-nums leading-none text-neutral-500">
|
||||
{chains.length}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setChainSectionExpanded((v) => !v)}
|
||||
title={chainSectionExpanded ? "收起业务链" : "展开业务链"}
|
||||
aria-expanded={chainSectionExpanded}
|
||||
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full border border-[#E6C9C4] bg-white text-neutral-400 transition-colors hover:border-primary/40 hover:text-primary"
|
||||
>
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
"h-3.5 w-3.5 transition-transform duration-300 ease-out",
|
||||
chainSectionExpanded && "rotate-180",
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
{chains.length === 0 ? (
|
||||
<p className="text-xs text-slate-400 py-1.5">暂无公共业务链</p>
|
||||
) : (
|
||||
<div className="flex flex-col gap-2.5">
|
||||
{chains.map((chain) => {
|
||||
const selected = selectedChainId === chain.id;
|
||||
const graphOpen = expandedChainId === chain.id;
|
||||
const stageGroups = getChainStageGroups(chain);
|
||||
let seatColorIndex = 0;
|
||||
return (
|
||||
<div
|
||||
key={chain.id}
|
||||
className={cn(
|
||||
"rounded-xl border transition-all duration-200",
|
||||
selected
|
||||
? "border-primary/25 bg-primary/5 ring-1 ring-primary/20 shadow-sm"
|
||||
: "border-[#ECE7E4] bg-neutral-50/50 hover:border-neutral-300 hover:bg-neutral-50",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className="grid overflow-hidden transition-all duration-300 ease-out"
|
||||
style={{
|
||||
gridTemplateRows: chainSectionExpanded ? "1fr" : "0fr",
|
||||
marginTop: chainSectionExpanded ? 12 : 0,
|
||||
opacity: chainSectionExpanded ? 1 : 0,
|
||||
}}
|
||||
>
|
||||
<div className="min-h-0 overflow-hidden">
|
||||
{chains.length === 0 ? (
|
||||
<p className="text-xs text-slate-400 py-1.5">暂无公共业务链</p>
|
||||
) : (
|
||||
<div className="max-h-[292px] overflow-y-auto pr-1 -mr-1 custom-scrollbar">
|
||||
<div className="flex flex-col gap-2.5 py-0.5">
|
||||
{chains.map((chain) => {
|
||||
const selected = selectedChainId === chain.id;
|
||||
const graphOpen = expandedChainId === chain.id;
|
||||
const stageGroups = getChainStageGroups(chain);
|
||||
let seatColorIndex = 0;
|
||||
return (
|
||||
<div
|
||||
key={chain.id}
|
||||
className={cn(
|
||||
"min-h-[88px] rounded-xl border transition-all duration-200",
|
||||
selected
|
||||
? "border-primary/25 bg-primary/5 ring-1 ring-primary/20 shadow-sm"
|
||||
: "border-[#ECE7E4] bg-neutral-50/50 hover:border-neutral-300 hover:bg-neutral-50",
|
||||
)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onSelectChain(chain)}
|
||||
@@ -3203,11 +3232,14 @@ function RoundtableEntryRail({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -3342,7 +3374,7 @@ function RoundtableEntryRail({
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+7
-4
@@ -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,18 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { fetchSqReportStatus } from "@/core/auth/dashboard-input";
|
||||
import { fetchSqReportCheck, type SqReportStatus } from "@/core/auth/dashboard-input";
|
||||
import { readCopTaskDetail, resolveTaskDeeplinkTarget } from "@/core/auth/task-deeplink";
|
||||
|
||||
type GateStatus = "loading" | "incomplete" | "ok";
|
||||
|
||||
export interface RwfxSqGateStartContext {
|
||||
/** 3q 研判情报报告状态。即使未完成/未知也会随 run context 传出,便于后端判断与本地验证。 */
|
||||
sqReportStatus: SqReportStatus;
|
||||
/** 已完成的 3q 研判情报报告。只随 run context 传给模型,不在页面展示。 */
|
||||
sqReport?: unknown;
|
||||
}
|
||||
|
||||
interface RwfxSqGateValue {
|
||||
/**
|
||||
* rwfx 自动开始第一次问答的门控。
|
||||
@@ -49,7 +56,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,56 +79,70 @@ export function RwfxSqGateProvider({
|
||||
const [jumping, setJumping] = useState(false);
|
||||
|
||||
const statusRef = useRef<GateStatus>(taskId ? "loading" : "ok");
|
||||
const sqReportStatusRef = useRef<SqReportStatus | null>(taskId ? null : "unknown");
|
||||
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 sqReportStatus = sqReportStatusRef.current;
|
||||
if (!sqReportStatus) return undefined;
|
||||
const sqReport = sqReportRef.current;
|
||||
return sqReport == null ? { sqReportStatus } : { sqReportStatus, sqReport };
|
||||
}, []);
|
||||
|
||||
// taskId 变化:重置门控状态并预检 3q。
|
||||
useEffect(() => {
|
||||
pendingStartRef.current = null;
|
||||
sqReportStatusRef.current = null;
|
||||
sqReportRef.current = null;
|
||||
setOpen(false);
|
||||
if (!taskId) {
|
||||
statusRef.current = "ok";
|
||||
sqReportStatusRef.current = "unknown";
|
||||
return;
|
||||
}
|
||||
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;
|
||||
sqReportStatusRef.current = result.status;
|
||||
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();
|
||||
|
||||
+14
-9
@@ -12,9 +12,10 @@ from deerflow.agents.middlewares.prompt_prefix_middleware import PromptPrefixMid
|
||||
from deerflow.agents.middlewares.rag_citation_middleware import RagCitationMiddleware
|
||||
from deerflow.agents.middlewares.setup_agent_approval_middleware import SetupAgentApprovalMiddleware
|
||||
from deerflow.agents.middlewares.setup_writing_approval_middleware import SetupWritingApprovalMiddleware
|
||||
from deerflow.agents.middlewares.subagent_limit_middleware import SubagentLimitMiddleware
|
||||
from deerflow.agents.middlewares.summarization_middleware import BeforeSummarizationHook, DeerFlowSummarizationMiddleware
|
||||
from deerflow.agents.middlewares.title_middleware import TitleMiddleware
|
||||
from deerflow.agents.middlewares.subagent_limit_middleware import SubagentLimitMiddleware
|
||||
from deerflow.agents.middlewares.summarization_middleware import BeforeSummarizationHook, DeerFlowSummarizationMiddleware
|
||||
from deerflow.agents.middlewares.task_external_context_middleware import TaskExternalContextMiddleware
|
||||
from deerflow.agents.middlewares.title_middleware import TitleMiddleware
|
||||
from deerflow.agents.middlewares.todo_middleware import TodoMiddleware
|
||||
from deerflow.agents.middlewares.token_usage_middleware import TokenUsageMiddleware
|
||||
from deerflow.agents.middlewares.tool_error_handling_middleware import build_lead_runtime_middlewares
|
||||
@@ -326,12 +327,16 @@ def _build_middlewares(
|
||||
# to the global ``knowledge.rag_enabled`` config. No-op when knowledge is
|
||||
# disabled. Skipped in bootstrap/writing-setup/ephemeral like MemoryMiddleware.
|
||||
knowledge_cfg = getattr(resolved_app_config, "knowledge", None)
|
||||
if not is_bootstrap and not is_writing_setup and not is_ephemeral and knowledge_cfg is not None and knowledge_cfg.enabled:
|
||||
from deerflow.agents.middlewares.knowledge_rag_middleware import KnowledgeRagMiddleware
|
||||
|
||||
middlewares.append(KnowledgeRagMiddleware(limit=knowledge_cfg.rag_limit, default_enabled=knowledge_cfg.rag_enabled))
|
||||
|
||||
# Add ViewImageMiddleware only for the NATIVE-vision path (main model
|
||||
if not is_bootstrap and not is_writing_setup and not is_ephemeral and knowledge_cfg is not None and knowledge_cfg.enabled:
|
||||
from deerflow.agents.middlewares.knowledge_rag_middleware import KnowledgeRagMiddleware
|
||||
|
||||
middlewares.append(KnowledgeRagMiddleware(limit=knowledge_cfg.rag_limit, default_enabled=knowledge_cfg.rag_enabled))
|
||||
|
||||
# TaskExternalContextMiddleware — inject hidden per-run business context
|
||||
# (for example rwfx's completed 3q report) into the model request only.
|
||||
middlewares.append(TaskExternalContextMiddleware())
|
||||
|
||||
# Add ViewImageMiddleware only for the NATIVE-vision path (main model
|
||||
# ingests raw image data). In delegated mode the view_image tool already
|
||||
# returns a text description and stores nothing in `viewed_images`, so the
|
||||
# middleware would otherwise inject a spurious "No images have been viewed."
|
||||
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
"""Inject task-specific external context into the model request.
|
||||
|
||||
The web task workspace can pass business data through LangGraph run context
|
||||
without showing it in the chat transcript. This middleware converts that
|
||||
context into a transient message right before model invocation, so it is
|
||||
available to the agent but never persisted as user-visible conversation state.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any, override
|
||||
|
||||
from langchain.agents import AgentState
|
||||
from langchain.agents.middleware import AgentMiddleware
|
||||
from langchain_core.messages import HumanMessage
|
||||
|
||||
_CONTEXT_KEY = "task_external_context"
|
||||
_TAG = "task_external_context"
|
||||
_MAX_CONTEXT_CHARS = 18000
|
||||
_TAIL_CONTEXT_CHARS = 2500
|
||||
|
||||
|
||||
def _truncate_middle(text: str, max_chars: int = _MAX_CONTEXT_CHARS) -> str:
|
||||
if len(text) <= max_chars:
|
||||
return text
|
||||
marker = "\n\n...[隐藏任务上下文过长,已截断中间部分]...\n\n"
|
||||
tail_chars = min(_TAIL_CONTEXT_CHARS, max_chars // 4)
|
||||
head_chars = max(0, max_chars - len(marker) - tail_chars)
|
||||
return f"{text[:head_chars]}{marker}{text[-tail_chars:]}"
|
||||
|
||||
|
||||
def _render_context(value: Any) -> str:
|
||||
if value is None:
|
||||
return ""
|
||||
if isinstance(value, str):
|
||||
return value.strip()
|
||||
try:
|
||||
text = json.dumps(value, ensure_ascii=False, indent=2, default=str).strip()
|
||||
except Exception:
|
||||
text = str(value).strip()
|
||||
return _truncate_middle(text)
|
||||
|
||||
|
||||
def _task_context_from_request(request: Any) -> Any:
|
||||
runtime = getattr(request, "runtime", None)
|
||||
context = getattr(runtime, "context", None)
|
||||
if isinstance(context, dict):
|
||||
return context.get(_CONTEXT_KEY)
|
||||
return None
|
||||
|
||||
|
||||
def _build_context_message(value: Any) -> HumanMessage | None:
|
||||
text = _render_context(value)
|
||||
if not text:
|
||||
return None
|
||||
content = (
|
||||
f"<{_TAG}>\n"
|
||||
"[系统提示:以下是业务系统随本次运行提供的隐藏任务上下文,不是用户的新输入。"
|
||||
"回答当前任务时应作为背景资料参考;不要主动说明这些内容来自隐藏上下文,"
|
||||
"也不要在回答中暴露本标签本身。]\n\n"
|
||||
f"{text}\n"
|
||||
f"</{_TAG}>"
|
||||
)
|
||||
return HumanMessage(
|
||||
content=content,
|
||||
additional_kwargs={"hide_from_ui": True, _CONTEXT_KEY: True},
|
||||
)
|
||||
|
||||
|
||||
class TaskExternalContextMiddleware(AgentMiddleware[AgentState]):
|
||||
"""Append hidden task context as a transient model-call message."""
|
||||
|
||||
state_schema = AgentState
|
||||
|
||||
@override
|
||||
async def awrap_model_call(self, request: Any, handler: Any) -> Any:
|
||||
msg = _build_context_message(_task_context_from_request(request))
|
||||
if msg is not None:
|
||||
request = request.override(messages=[*request.messages, msg])
|
||||
return await handler(request)
|
||||
|
||||
@override
|
||||
def wrap_model_call(self, request: Any, handler: Any) -> Any:
|
||||
msg = _build_context_message(_task_context_from_request(request))
|
||||
if msg is not None:
|
||||
request = request.override(messages=[*request.messages, msg])
|
||||
return handler(request)
|
||||
Reference in new issue
Block a user