Merge branch 'dhk-1' into 'master'
接口地址修改 See merge request knowledge-base-bj/deerflow-server!109
This commit is contained in:
commit
c57696cf80
11 files changed
+427
-61
No files matched your search
@@ -8,10 +8,10 @@ VITE_USE_REAL_DATA=true
|
||||
VITE_BACKEND_BASE_URL=https://ch1.b.uat.4.cn/deerflow
|
||||
VITE_LANGGRAPH_BASE_URL=https://ch1.b.uat.4.cn/deerflow/api
|
||||
|
||||
# CMZS后端接口
|
||||
VITE_API_BASE_URL=https://ch1.b.uat.4.cn/zncm
|
||||
# 轻应用「新建笔记」系列接口
|
||||
VITE_NOTE_API_BASE_URL=https://your-intranet-host/note-api
|
||||
# CMZS后端接口
|
||||
VITE_API_BASE_URL=https://ch1.b.uat.4.cn/zncm
|
||||
# 佳来模型对话接口
|
||||
VITE_AIPI_CHAT_URL=https://ch1.b.uat.4.cn/gpt
|
||||
# 多轮检索接口
|
||||
@@ -26,15 +26,16 @@ VITE_AIPI_KNOWLEDGE_IFRAME_SKIN=red
|
||||
VITE_AIPI_MOPECHAT_URL=https://ch1.b.uat.4.cn/likun_b1_mope
|
||||
# 工作室/笔记本后端接口(侧边栏“工作室”列表、笔记本详情等都走这里,不是 VITE_NOTE_API_BASE_URL)
|
||||
VITE_AIPI_BACKENP_URL=https://knowledgetoolserver.b.uat.4.cn
|
||||
VITE_NOTEBOOK_API_BASE_URL=https://knowledgetoolserver.b.uat.4.cn
|
||||
# 工作室侧边栏显示开关;仅控制入口显示,展开内容仍依赖 notebook 接口成功返回数据
|
||||
VITE_STUDIO_ENABLED=true
|
||||
# 工作室/笔记本前端地址(用于 auth/callback 等跳转)
|
||||
VITE_AIPI_FRONTEND_URL=https://knowledgetoolweb.b.uat.4.cn
|
||||
VITE_AIPI_CONSUMER_URL=https://ch1.b.uat.4.cn/consumer
|
||||
|
||||
# MaxKey 鉴权(getTokenByOnlineTicket)
|
||||
VITE_AUTH_BASE_URL=https://auth.y.uat.4.cn
|
||||
VITE_AIPI_PGLINKTO_URL=https://pg.b.uat.4.cn
|
||||
VITE_AIPI_XKLINKTO_URL=https://kz.b.uat.4.cn
|
||||
# 工作室/笔记本前端地址(用于 auth/callback 等跳转)
|
||||
VITE_AIPI_FRONTEND_URL=https://knowledgetoolweb.b.uat.4.cn
|
||||
# 工作室侧边栏显示开关;仅控制入口显示,展开内容仍依赖 notebook 接口成功返回数据
|
||||
VITE_STUDIO_ENABLED=true
|
||||
|
||||
# 可选:Canvas 独立 LangGraph 地址
|
||||
# VITE_CANVAS_LANGGRAPH_BASE_URL=
|
||||
@@ -10,6 +10,8 @@ VITE_LANGGRAPH_BASE_URL=https://ch1.b.sit.4.cn/deerflow/api
|
||||
|
||||
# 轻应用「新建笔记」系列接口
|
||||
VITE_NOTE_API_BASE_URL=https://your-intranet-host/note-api
|
||||
# CMZS后端接口
|
||||
VITE_API_BASE_URL=https://ch1.b.sit.4.cn/zncm
|
||||
# 佳来模型对话接口
|
||||
VITE_AIPI_CHAT_URL=https://ch1.b.sit.4.cn/gpt
|
||||
# 多轮检索接口
|
||||
@@ -24,15 +26,16 @@ VITE_AIPI_KNOWLEDGE_IFRAME_SKIN=red
|
||||
VITE_AIPI_MOPECHAT_URL=https://ch1.b.sit.4.cn/likun_b1_mope
|
||||
# 工作室/笔记本后端接口(侧边栏“工作室”列表、笔记本详情等都走这里,不是 VITE_NOTE_API_BASE_URL)
|
||||
VITE_AIPI_BACKENP_URL=https://knowledgetoolserver.b.sit.4.cn
|
||||
VITE_NOTEBOOK_API_BASE_URL=https://knowledgetoolserver.b.sit.4.cn
|
||||
# 工作室侧边栏显示开关;仅控制入口显示,展开内容仍依赖 notebook 接口成功返回数据
|
||||
VITE_STUDIO_ENABLED=true
|
||||
# 工作室/笔记本前端地址(用于 auth/callback 等跳转)
|
||||
VITE_AIPI_FRONTEND_URL=https://knowledgetoolserver.b.sit.4.cn
|
||||
VITE_AIPI_CONSUMER_URL=https://ch1.b.sit.4.cn/consumer
|
||||
|
||||
# MaxKey 鉴权(getTokenByOnlineTicket)
|
||||
VITE_AUTH_BASE_URL=https://auth.y.sit.4.cn
|
||||
VITE_AIPI_PGLINKTO_URL=https://pg.b.sit.4.cn
|
||||
VITE_AIPI_XKLINKTO_URL=https://kz.b.sit.4.cn
|
||||
# 工作室/笔记本前端地址(用于 auth/callback 等跳转)
|
||||
VITE_AIPI_FRONTEND_URL=https://knowledgetoolserver.b.sit.4.cn
|
||||
# 工作室侧边栏显示开关;仅控制入口显示,展开内容仍依赖 notebook 接口成功返回数据
|
||||
VITE_STUDIO_ENABLED=true
|
||||
|
||||
# 可选:Canvas 独立 LangGraph 地址
|
||||
# VITE_CANVAS_LANGGRAPH_BASE_URL=
|
||||
@@ -142,10 +142,24 @@ export interface IntentSummary {
|
||||
*/
|
||||
export type IntentStreamPhase = "connecting" | "connected" | "streaming" | "tool_calling";
|
||||
|
||||
/**
|
||||
* 用户上传文件的引用(已经过 `/api/threads/{thread_id}/uploads`)。透传给后端
|
||||
* `additional_kwargs.files`,由 UploadsMiddleware 注入文件清单并解锁 read_file 等工具。
|
||||
* 字段与普通聊天的 `FileInMessage` 对齐:`path` 用上传返回的 `virtual_path`。
|
||||
*/
|
||||
export interface IntentFileRef {
|
||||
filename: string;
|
||||
size: number;
|
||||
path: string;
|
||||
status?: "uploading" | "uploaded";
|
||||
}
|
||||
|
||||
export interface StreamIntentRequest {
|
||||
threadId: string;
|
||||
message: string;
|
||||
model?: string;
|
||||
/** Step 1 用户上传的参考文件(可选)。 */
|
||||
files?: IntentFileRef[];
|
||||
signal?: AbortSignal;
|
||||
/** Fires for every AI text chunk parsed out of LangGraph messages-tuple frames. */
|
||||
onTextDelta?: (text: string, messageId: string) => void;
|
||||
@@ -318,6 +332,8 @@ export async function streamIntent(req: StreamIntentRequest): Promise<StreamInte
|
||||
thread_id: req.threadId,
|
||||
message: req.message,
|
||||
model: req.model,
|
||||
// 仅在有文件时带上,空数组后端也安全忽略。
|
||||
files: req.files && req.files.length > 0 ? req.files : undefined,
|
||||
}),
|
||||
signal: req.signal,
|
||||
});
|
||||
|
||||
@@ -227,6 +227,12 @@ export interface StreamMultiAgentRequest {
|
||||
* (平时只看广播摘要)。不进 prompt → 不调工具就不耗 token。
|
||||
*/
|
||||
peerDeliveries?: Array<{ name: string; content: string }>;
|
||||
/**
|
||||
* 用户随干预消息上传的参考文件(已经过 `/api/threads/{thread_id}/uploads`)。
|
||||
* 透传给后端 `additional_kwargs.files`,由 UploadsMiddleware 注入清单 + 解锁
|
||||
* read_file 等工具。字段 `path` 用上传返回的 `virtual_path`。
|
||||
*/
|
||||
files?: Array<{ filename: string; size: number; path: string; status?: string }>;
|
||||
signal?: AbortSignal;
|
||||
/** Fires for every non-status data frame the upstream emits. */
|
||||
onUpstreamFrame?: (frame: unknown) => void;
|
||||
@@ -447,6 +453,8 @@ export async function streamMultiAgent(
|
||||
parallel_no_file: req.parallelNoFile ?? false,
|
||||
// 仅 special 用;前序席位完整交付,后端注入 context 供 read_peer_delivery 按需取全文。
|
||||
peer_deliveries: req.peerDeliveries ?? [],
|
||||
// 用户随干预上传的参考文件;仅在有文件时带上,后端注入 additional_kwargs.files。
|
||||
files: req.files && req.files.length > 0 ? req.files : undefined,
|
||||
}),
|
||||
signal: req.signal,
|
||||
});
|
||||
|
||||
@@ -14,8 +14,10 @@
|
||||
import { useRef, useState } from "react";
|
||||
import {
|
||||
CheckCircle,
|
||||
FileText,
|
||||
Loader2,
|
||||
MessageSquare,
|
||||
Paperclip,
|
||||
Pause,
|
||||
Repeat,
|
||||
Save,
|
||||
@@ -25,6 +27,13 @@ import {
|
||||
X,
|
||||
} from "lucide-react";
|
||||
|
||||
/** 人类可读的文件大小(B / KB / MB)。 */
|
||||
function formatFileSize(bytes: number): string {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Man1Avatar, RobotAvatar } from "@/roundtable-planning/components/Avatars";
|
||||
import {
|
||||
@@ -62,7 +71,7 @@ export interface Step1PanelProps {
|
||||
setIntentClarificationSelections: React.Dispatch<
|
||||
React.SetStateAction<Map<string, IntentClarificationSelection>>
|
||||
>;
|
||||
onSendMessage: (text: string) => void;
|
||||
onSendMessage: (text: string, files?: File[]) => void;
|
||||
onStopIntent: () => void;
|
||||
onSkipQuestion: () => void;
|
||||
onFollowUp: () => void;
|
||||
@@ -101,13 +110,34 @@ export function Step1Panel(props: Step1PanelProps) {
|
||||
|
||||
// 输入框完全是本地状态 —— 提到 Page 层会导致每次打字都重渲整页。
|
||||
const [inputText, setInputText] = useState<string>("");
|
||||
// 待发送的本地附件(点发送时连同文本一起上传)。
|
||||
const [attachedFiles, setAttachedFiles] = useState<File[]>([]);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const charactersLeft = inputText.length;
|
||||
|
||||
const handlePickFiles = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const picked = Array.from(e.target.files ?? []);
|
||||
if (picked.length > 0) {
|
||||
// 去重(按 名称+大小),追加到已选列表。
|
||||
setAttachedFiles((prev) => {
|
||||
const seen = new Set(prev.map((f) => `${f.name}:${f.size}`));
|
||||
return [...prev, ...picked.filter((f) => !seen.has(`${f.name}:${f.size}`))];
|
||||
});
|
||||
}
|
||||
// 清空 input 值,允许再次选同名文件。
|
||||
e.target.value = "";
|
||||
};
|
||||
|
||||
const removeAttachedFile = (idx: number) => {
|
||||
setAttachedFiles((prev) => prev.filter((_, i) => i !== idx));
|
||||
};
|
||||
|
||||
const handleSend = () => {
|
||||
if (!inputText.trim()) return;
|
||||
onSendMessage(inputText);
|
||||
if (!inputText.trim() && attachedFiles.length === 0) return;
|
||||
onSendMessage(inputText, attachedFiles.length > 0 ? attachedFiles : undefined);
|
||||
setInputText("");
|
||||
setAttachedFiles([]);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -150,6 +180,33 @@ export function Step1Panel(props: Step1PanelProps) {
|
||||
{/* Bottom input area */}
|
||||
<div className="shrink-0 p-4 border-t border-slate-100 bg-[#fcfcfd]">
|
||||
<div className="relative border border-slate-200 focus-within:border-primary focus-within:ring-2 focus-within:ring-[var(--brand-200)]/50 transition-all rounded-xl overflow-hidden bg-white px-3 py-2.5">
|
||||
{/* 已选附件预览(点发送时随消息上传) */}
|
||||
{attachedFiles.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5 pb-2 mb-2 border-b border-slate-100">
|
||||
{attachedFiles.map((file, idx) => (
|
||||
<span
|
||||
key={`${file.name}-${file.size}-${idx}`}
|
||||
className="inline-flex items-center gap-1.5 bg-slate-50 border border-slate-200 rounded-lg pl-2 pr-1 py-1 text-[11px] text-slate-600 max-w-[200px]"
|
||||
>
|
||||
<FileText className="w-3 h-3 shrink-0 text-primary" />
|
||||
<span className="truncate font-medium" title={file.name}>
|
||||
{file.name}
|
||||
</span>
|
||||
<span className="shrink-0 text-slate-400">
|
||||
{formatFileSize(file.size)}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeAttachedFile(idx)}
|
||||
className="shrink-0 p-0.5 rounded hover:bg-slate-200 text-slate-400 hover:text-slate-600 transition"
|
||||
title="移除"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<textarea
|
||||
id="input-text-area"
|
||||
value={inputText}
|
||||
@@ -177,6 +234,21 @@ export function Step1Panel(props: Step1PanelProps) {
|
||||
>
|
||||
<Smile className="w-4 h-4" />
|
||||
</button>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={handlePickFiles}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
title="上传参考文件(智能体将读取文件内容辅助理解任务)"
|
||||
className="p-1 hover:text-primary hover:bg-slate-50 rounded transition"
|
||||
>
|
||||
<Paperclip className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<span className="text-[10px] font-mono text-slate-400">{charactersLeft}/500</span>
|
||||
@@ -422,8 +494,30 @@ function Step1Message({
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="roundtable-chat-message text-slate-700 font-medium whitespace-pre-wrap break-words">
|
||||
{msg.content}
|
||||
<div className="space-y-2">
|
||||
{msg.files && msg.files.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{msg.files.map((file, i) => (
|
||||
<span
|
||||
key={`${file.filename}-${i}`}
|
||||
className="inline-flex items-center gap-1.5 bg-white/70 border border-slate-200 rounded-lg px-2 py-1 text-[11px] text-slate-600 max-w-[200px]"
|
||||
>
|
||||
<FileText className="w-3 h-3 shrink-0 text-primary" />
|
||||
<span className="truncate font-medium" title={file.filename}>
|
||||
{file.filename}
|
||||
</span>
|
||||
<span className="shrink-0 text-slate-400">
|
||||
{formatFileSize(file.size)}
|
||||
</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{msg.content && (
|
||||
<div className="roundtable-chat-message text-slate-700 font-medium whitespace-pre-wrap break-words">
|
||||
{msg.content}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</MessageBubble>
|
||||
|
||||
@@ -23,11 +23,20 @@ import {
|
||||
HelpCircle,
|
||||
Loader2,
|
||||
Network,
|
||||
Paperclip,
|
||||
Pause,
|
||||
Play,
|
||||
Sparkles,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
|
||||
/** 人类可读的文件大小(B / KB / MB)。 */
|
||||
function formatFileSize(bytes: number): string {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -124,7 +133,7 @@ export interface Step2PanelProps {
|
||||
// ─── Handlers ─────────────────────────────────────────────────
|
||||
onPauseRoundtable: () => void;
|
||||
onResumeRoundtable: () => void;
|
||||
onHandleInjectToRoundtable: () => void;
|
||||
onHandleInjectToRoundtable: (files?: File[]) => void;
|
||||
onSubmitClarification: (answer: string) => void;
|
||||
onSavePersona: (id: number) => void;
|
||||
onProceedToStep3: () => void;
|
||||
@@ -251,11 +260,34 @@ export function Step2Panel(props: Step2PanelProps) {
|
||||
// 干预研审:记录指令并自动恢复会商,右上角按钮状态由 isRoundTableRunning 驱动
|
||||
const [pauseFlash, setPauseFlash] = useState(false);
|
||||
|
||||
// 干预消息携带的本地附件(点「干预并发送」时连同指令一起上传)。
|
||||
const [injectFiles, setInjectFiles] = useState<File[]>([]);
|
||||
const injectFileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handlePickInjectFiles = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const picked = Array.from(e.target.files ?? []);
|
||||
if (picked.length > 0) {
|
||||
setInjectFiles((prev) => {
|
||||
const seen = new Set(prev.map((f) => `${f.name}:${f.size}`));
|
||||
return [...prev, ...picked.filter((f) => !seen.has(`${f.name}:${f.size}`))];
|
||||
});
|
||||
}
|
||||
e.target.value = "";
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const removeInjectFile = useCallback((idx: number) => {
|
||||
setInjectFiles((prev) => prev.filter((_, i) => i !== idx));
|
||||
}, []);
|
||||
|
||||
const handleInject = useCallback(() => {
|
||||
onHandleInjectToRoundtable();
|
||||
onHandleInjectToRoundtable(injectFiles.length > 0 ? injectFiles : undefined);
|
||||
setInjectFiles([]);
|
||||
setPauseFlash(true);
|
||||
window.setTimeout(() => setPauseFlash(false), 1600);
|
||||
}, [onHandleInjectToRoundtable]);
|
||||
}, [onHandleInjectToRoundtable, injectFiles]);
|
||||
|
||||
const isSandboxStreaming =
|
||||
isOrchestrating ||
|
||||
@@ -530,7 +562,35 @@ export function Step2Panel(props: Step2PanelProps) {
|
||||
)}
|
||||
|
||||
{/* Console + intervention bar */}
|
||||
<div className="shrink-0 p-4 bg-slate-50 border-t border-slate-100 flex items-center gap-3">
|
||||
<div className="shrink-0 p-4 bg-slate-50 border-t border-slate-100 flex flex-col gap-2">
|
||||
{/* 已选附件预览(点「干预并发送」时随指令上传) */}
|
||||
{injectFiles.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{injectFiles.map((file, idx) => (
|
||||
<span
|
||||
key={`${file.name}-${file.size}-${idx}`}
|
||||
className="inline-flex items-center gap-1.5 bg-white border border-slate-200 rounded-lg pl-2 pr-1 py-1 text-[11px] text-slate-600 max-w-[200px]"
|
||||
>
|
||||
<FileIcon className="w-3 h-3 shrink-0 text-primary" />
|
||||
<span className="truncate font-medium" title={file.name}>
|
||||
{file.name}
|
||||
</span>
|
||||
<span className="shrink-0 text-slate-400">
|
||||
{formatFileSize(file.size)}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeInjectFile(idx)}
|
||||
className="shrink-0 p-0.5 rounded hover:bg-slate-200 text-slate-400 hover:text-slate-600 transition"
|
||||
title="移除"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
type="text"
|
||||
value={customAgentInput}
|
||||
@@ -542,6 +602,22 @@ export function Step2Panel(props: Step2PanelProps) {
|
||||
className="flex-1 bg-white border border-slate-200 rounded-xl px-3.5 py-2.5 text-xs focus:outline-none focus:border-primary focus:ring-1 focus:ring-[var(--brand-200)]/50 font-medium"
|
||||
/>
|
||||
|
||||
<input
|
||||
ref={injectFileInputRef}
|
||||
type="file"
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={handlePickInjectFiles}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => injectFileInputRef.current?.click()}
|
||||
title="上传参考文件(席位/总控将读取文件内容辅助研讨)"
|
||||
className="bg-white border border-slate-200 hover:bg-slate-50 text-slate-500 hover:text-primary p-2.5 rounded-xl transition shrink-0 cursor-pointer active:scale-95"
|
||||
>
|
||||
<Paperclip className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleInject}
|
||||
@@ -581,6 +657,7 @@ export function Step2Panel(props: Step2PanelProps) {
|
||||
<Sparkles className="w-3.5 h-3.5 text-emerald-100" />
|
||||
达成共识生成全案成果 >
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -19,9 +19,11 @@ import {
|
||||
initIntent,
|
||||
streamIntent,
|
||||
type IntentErrorDetail,
|
||||
type IntentFileRef,
|
||||
type IntentStreamPhase,
|
||||
type IntentSummary,
|
||||
} from "@/roundtable-planning/api/intent";
|
||||
import { uploadFiles } from "@/core/uploads/api";
|
||||
import {
|
||||
clarificationKey,
|
||||
formatIntentClarificationAnswer,
|
||||
@@ -42,6 +44,8 @@ export type InteractiveMessage = {
|
||||
time: string;
|
||||
streaming?: boolean;
|
||||
phase?: string;
|
||||
/** 用户气泡里附带的上传文件(仅展示用:文件名 + 大小)。 */
|
||||
files?: Array<{ filename: string; size: number }>;
|
||||
/** First clarification (back-compat); prefer `clarifications`. */
|
||||
clarification?: ClarificationPayload;
|
||||
/**
|
||||
@@ -109,8 +113,12 @@ export interface UseStep1IntentReturn {
|
||||
// ─── Handlers ────────────────────────────────────────────────────────
|
||||
/** 清空当前 Step 1 状态并重新拉取 intent thread。 */
|
||||
runStep1Init: () => void;
|
||||
/** 把 `userText` 作为用户气泡发给 intent agent,流式渲染回复。 */
|
||||
sendIntentMessage: (userText: string) => Promise<void>;
|
||||
/**
|
||||
* 把 `userText` 作为用户气泡发给 intent agent,流式渲染回复。
|
||||
* `files`:可选的本地文件,发送前先上传到 intent thread,再把引用随消息提交(后端
|
||||
* 注入 additional_kwargs.files,意图智能体即可 read_file 读取)。允许「仅文件无文本」。
|
||||
*/
|
||||
sendIntentMessage: (userText: string, files?: File[]) => Promise<void>;
|
||||
/**
|
||||
* 提交某条 agent 消息的全部澄清问题答案 —— 逐题取各自选择,拼成一条合并消息
|
||||
* 发回。至少答一题才会发送。单题时退化为发送该题答案。
|
||||
@@ -275,9 +283,10 @@ export function useStep1Intent(options: UseStep1IntentOptions): UseStep1IntentRe
|
||||
}, [interactiveMessages, intentReady]);
|
||||
|
||||
const sendIntentMessage = useCallback(
|
||||
async (userText: string) => {
|
||||
async (userText: string, files?: File[]) => {
|
||||
const trimmed = userText.trim();
|
||||
if (!trimmed) return;
|
||||
const hasFiles = !!(files && files.length > 0);
|
||||
if (!trimmed && !hasFiles) return;
|
||||
if (!intentThreadId) {
|
||||
triggerToast("⚠️ 任务理解智能体尚未就绪,请稍候");
|
||||
return;
|
||||
@@ -304,16 +313,24 @@ export function useStep1Intent(options: UseStep1IntentOptions): UseStep1IntentRe
|
||||
senderName: "用户",
|
||||
content: trimmed,
|
||||
time: userMsgTime,
|
||||
files: hasFiles
|
||||
? files!.map((f) => ({ filename: f.name, size: f.size }))
|
||||
: undefined,
|
||||
};
|
||||
setInteractiveMessages((prev) => [...prev, userMsg]);
|
||||
|
||||
const timelineDesc = trimmed
|
||||
? trimmed.length > 25
|
||||
? trimmed.substring(0, 25) + "..."
|
||||
: trimmed
|
||||
: `上传了 ${files!.length} 个文件`;
|
||||
setTimelineEntries((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: prev.length + 1,
|
||||
time: userMsgTime,
|
||||
title: "用户实时反馈",
|
||||
desc: trimmed.length > 25 ? trimmed.substring(0, 25) + "..." : trimmed,
|
||||
desc: timelineDesc,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -333,10 +350,49 @@ export function useStep1Intent(options: UseStep1IntentOptions): UseStep1IntentRe
|
||||
|
||||
setIsIntentSending(true);
|
||||
try {
|
||||
// 先把本地文件上传到 intent thread —— 与普通聊天同一上传接口/技能链路。
|
||||
// 拿到 virtual_path 后随消息提交,后端注入 additional_kwargs.files。
|
||||
let fileRefs: IntentFileRef[] | undefined;
|
||||
if (hasFiles) {
|
||||
setInteractiveMessages((prev) =>
|
||||
prev.map((m) =>
|
||||
m.id === agentMsgId ? { ...m, phase: "正在上传文件" } : m,
|
||||
),
|
||||
);
|
||||
try {
|
||||
const up = await uploadFiles(intentThreadId, files!);
|
||||
fileRefs = up.files.map((f) => ({
|
||||
filename: f.filename,
|
||||
size: f.size,
|
||||
path: f.virtual_path,
|
||||
status: "uploaded" as const,
|
||||
}));
|
||||
} catch (err) {
|
||||
setInteractiveMessages((prev) =>
|
||||
prev.map((m) =>
|
||||
m.id === agentMsgId
|
||||
? {
|
||||
...m,
|
||||
content: `(文件上传失败) ${(err as Error).message}`,
|
||||
streaming: false,
|
||||
}
|
||||
: m,
|
||||
),
|
||||
);
|
||||
triggerToast(`❌ 文件上传失败: ${(err as Error).message}`);
|
||||
if (intentAborterRef.current === aborter) {
|
||||
intentAborterRef.current = null;
|
||||
}
|
||||
setIsIntentSending(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const res = await streamIntent({
|
||||
threadId: intentThreadId,
|
||||
message: trimmed,
|
||||
model: selectedModelRef.current || undefined,
|
||||
files: fileRefs,
|
||||
signal: aborter.signal,
|
||||
onTextDelta: (delta) => {
|
||||
setInteractiveMessages((prev) =>
|
||||
|
||||
@@ -23,8 +23,20 @@ import {
|
||||
type ClarificationPayload,
|
||||
type LeaderDispatch,
|
||||
type StreamPhase,
|
||||
type StreamMultiAgentRequest,
|
||||
type ThreadIdMap,
|
||||
} from "@/roundtable-planning/api/multi-agent";
|
||||
import { uploadFiles } from "@/core/uploads/api";
|
||||
|
||||
/** 干预消息携带的上传文件引用(与 streamMultiAgent 的 files 字段同形)。 */
|
||||
type RunFileRef = NonNullable<StreamMultiAgentRequest["files"]>[number];
|
||||
|
||||
/** 读取并清空 ref(consume-once):首个 leader run 拿到文件后清空,后续轮次不再带。 */
|
||||
function takeOnce<T>(ref: { current: T | null }): T | undefined {
|
||||
const v = ref.current ?? undefined;
|
||||
ref.current = null;
|
||||
return v;
|
||||
}
|
||||
import {
|
||||
buildDagStageLeaderTask,
|
||||
pickDagSeatTask,
|
||||
@@ -305,8 +317,11 @@ export interface UseStep2OrchestrationReturn {
|
||||
recordInjectMessage: () => void;
|
||||
/** 中断所有 in-flight SSE(给新建任务 / 加载草稿用)。 */
|
||||
abortAllInFlight: () => void;
|
||||
/** 一键干预:记录指令 + 自动恢复会商 + 触发新一轮编排(替代 pause + record + resume 组合)。 */
|
||||
handleInjectToRoundtable: () => void;
|
||||
/**
|
||||
* 一键干预:记录指令 + 自动恢复会商 + 触发新一轮编排(替代 pause + record + resume 组合)。
|
||||
* `files`:可选的参考文件,发送前先上传到总控 thread,首轮 leader run 据此 read_file 读取。
|
||||
*/
|
||||
handleInjectToRoundtable: (files?: File[]) => void;
|
||||
/** 总控澄清回复 —— 把回答拼到 prompt 里再起一轮编排。 */
|
||||
submitClarification: (rawAnswer: string) => void;
|
||||
/** 预算滑块(本地演示,非编排真实输入)。 */
|
||||
@@ -464,6 +479,11 @@ export function useStep2Orchestration(
|
||||
const activeAbortersRef = useRef<Set<AbortController>>(new Set());
|
||||
/** 干预研审:恢复会商后待执行的 leader prompt。 */
|
||||
const pendingInjectPromptRef = useRef<string | null>(null);
|
||||
/**
|
||||
* 干预消息携带的上传文件引用:由下一轮编排的**首个 leader run**消费一次后清空,
|
||||
* 总控据此 read_file 读取文件再派活。consume-once 语义避免后续轮次重复带文件。
|
||||
*/
|
||||
const pendingInjectFilesRef = useRef<RunFileRef[] | null>(null);
|
||||
// selectedModel / agentConfigs / agentNameToRole / threadIds / coordinatorName
|
||||
// 等等被 runOrchestration 在循环里反复读取。把它们打到 ref 上,可以让
|
||||
// runOrchestration 的 useCallback deps 大幅收窄,避免每次模型切换、agentConfigs
|
||||
@@ -1353,6 +1373,8 @@ export function useStep2Orchestration(
|
||||
// 第 2 轮起(已有席位交付)才是「综合轮」:让后端注入各席位完整交付供总控收口。
|
||||
// 首轮纯派活、无交付,传 false 保持上下文精简。
|
||||
synthesisMode: safetyCounter > 1,
|
||||
// 用户随干预上传的参考文件(consume-once:仅首轮 leader 带,之后清空)。
|
||||
files: takeOnce(pendingInjectFilesRef),
|
||||
model: selectedModelRef.current || undefined,
|
||||
signal: leaderAborter.signal,
|
||||
onTextDelta: (text) => appendToDialogue(leaderBubbleId, text),
|
||||
@@ -1600,6 +1622,8 @@ export function useStep2Orchestration(
|
||||
dAgentThreadId: ids,
|
||||
agentName: coord,
|
||||
newMessage: leaderTask,
|
||||
// 用户随干预上传的参考文件(consume-once:仅首轮 leader 带,之后清空)。
|
||||
files: takeOnce(pendingInjectFilesRef),
|
||||
model: selectedModelRef.current || undefined,
|
||||
signal: leaderAborter.signal,
|
||||
onTextDelta: (text) => appendToDialogue(leaderBubbleId, text),
|
||||
@@ -1887,6 +1911,8 @@ export function useStep2Orchestration(
|
||||
// 抑制预广播:别把总控的派活 prompt 播进席位 thread(否则弱模型从历史里捡到派活
|
||||
// 意图、跟着模仿派活 / 绕道 bash 调内部接口)。席位任务由 pickDagSeatTask 干净下发。
|
||||
suppressPreBroadcast: true,
|
||||
// 用户随干预上传的参考文件(consume-once:仅首轮 leader 带,之后清空)。
|
||||
files: takeOnce(pendingInjectFilesRef),
|
||||
model: selectedModelRef.current || undefined,
|
||||
signal: leaderAborter.signal,
|
||||
onTextDelta: (text) => appendToDialogue(leaderBubbleId, text),
|
||||
@@ -2289,38 +2315,75 @@ export function useStep2Orchestration(
|
||||
triggerToast("干预指令已记录,会商已自动恢复执行");
|
||||
}, [appendDialogue, customAgentInput, triggerToast]);
|
||||
|
||||
const handleInjectToRoundtable = useCallback(() => {
|
||||
const userText = customAgentInput.trim();
|
||||
if (!userText) return;
|
||||
if (!threadIdsRef.current || !coordinatorNameRef.current) {
|
||||
triggerToast("⚠️ 圆桌尚未就绪,无法记录干预指令");
|
||||
return;
|
||||
}
|
||||
const handleInjectToRoundtable = useCallback(
|
||||
async (files?: File[]) => {
|
||||
const userText = customAgentInput.trim();
|
||||
const hasFiles = !!(files && files.length > 0);
|
||||
if (!userText && !hasFiles) return;
|
||||
if (!threadIdsRef.current || !coordinatorNameRef.current) {
|
||||
triggerToast("⚠️ 圆桌尚未就绪,无法记录干预指令");
|
||||
return;
|
||||
}
|
||||
|
||||
// 中止当前编排(如有)
|
||||
orchestrationCancelRef.current = true;
|
||||
abortAllInFlight();
|
||||
// 强制标记非编排中,使后续 runOrchestration 可立即启动
|
||||
isOrchestratingRef.current = false;
|
||||
setIsOrchestrating(false);
|
||||
// 先把文件上传到总控 thread(与普通聊天同一上传接口/技能链路)。拿到 virtual_path
|
||||
// 存进 consume-once ref,供下一轮首个 leader run 注入 additional_kwargs.files。
|
||||
// 上传失败则中止本次干预,不打断当前会商。
|
||||
if (hasFiles) {
|
||||
const coordThreadId = resolveSeatThreadId(
|
||||
threadIdsRef.current,
|
||||
coordinatorNameRef.current,
|
||||
);
|
||||
try {
|
||||
const up = await uploadFiles(coordThreadId, files!);
|
||||
pendingInjectFilesRef.current = up.files.map((f) => ({
|
||||
filename: f.filename,
|
||||
size: f.size,
|
||||
path: f.virtual_path,
|
||||
status: "uploaded",
|
||||
}));
|
||||
} catch (err) {
|
||||
triggerToast(`❌ 文件上传失败: ${(err as Error).message}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setCustomAgentInput("");
|
||||
appendDialogue({
|
||||
sender: "用户 (插话干预)",
|
||||
avatarType: "human",
|
||||
content: `💡 (人工指令介入) ${userText}`,
|
||||
confidence: "最高指令",
|
||||
});
|
||||
// 中止当前编排(如有)
|
||||
orchestrationCancelRef.current = true;
|
||||
abortAllInFlight();
|
||||
// 强制标记非编排中,使后续 runOrchestration 可立即启动
|
||||
isOrchestratingRef.current = false;
|
||||
setIsOrchestrating(false);
|
||||
|
||||
const prompt = `用户人工干预新指令:${userText}\n\n请优先按这条新指令,通过 agent_orchestration 把活派给相关的真实席位去获取交付;若用户点名了某个席位(如"前端实现"),必须派给清单里对应的那个 agent_name。**不要**自己代答,也**不要**在没有真正看到该席位「完成…交付内容为:…」的情况下臆造交付或直接收口。只有当用户明确要求收口、或相关席位都已真实交付时,才综合输出结论。`;
|
||||
setCustomAgentInput("");
|
||||
const fileNote = hasFiles ? `(附 ${files!.length} 个文件)` : "";
|
||||
const shownText = userText || "(上传了参考文件)";
|
||||
appendDialogue({
|
||||
sender: "用户 (插话干预)",
|
||||
avatarType: "human",
|
||||
content: `💡 (人工指令介入) ${shownText}${fileNote}`,
|
||||
confidence: "最高指令",
|
||||
});
|
||||
|
||||
// 确保处于运行状态(同步更新按钮 UI)
|
||||
setIsRoundTableRunning(true);
|
||||
triggerToast("干预指令已发送,会商继续执行");
|
||||
const promptText =
|
||||
userText || "我上传了参考文件,请先阅读文件内容,再据此推进研讨。";
|
||||
const prompt = `用户人工干预新指令:${promptText}\n\n请优先按这条新指令,通过 agent_orchestration 把活派给相关的真实席位去获取交付;若用户点名了某个席位(如"前端实现"),必须派给清单里对应的那个 agent_name。**不要**自己代答,也**不要**在没有真正看到该席位「完成…交付内容为:…」的情况下臆造交付或直接收口。只有当用户明确要求收口、或相关席位都已真实交付时,才综合输出结论。`;
|
||||
|
||||
// 直接启动新一轮编排
|
||||
void runActiveOrchestration(prompt, threadIdsRef.current, coordinatorNameRef.current);
|
||||
}, [customAgentInput, appendDialogue, abortAllInFlight, runActiveOrchestration, triggerToast]);
|
||||
// 确保处于运行状态(同步更新按钮 UI)
|
||||
setIsRoundTableRunning(true);
|
||||
triggerToast("干预指令已发送,会商继续执行");
|
||||
|
||||
// 直接启动新一轮编排
|
||||
void runActiveOrchestration(prompt, threadIdsRef.current, coordinatorNameRef.current);
|
||||
},
|
||||
[
|
||||
customAgentInput,
|
||||
appendDialogue,
|
||||
abortAllInFlight,
|
||||
runActiveOrchestration,
|
||||
resolveSeatThreadId,
|
||||
triggerToast,
|
||||
],
|
||||
);
|
||||
|
||||
const submitClarification = useCallback(
|
||||
(rawAnswer: string) => {
|
||||
|
||||
@@ -1010,7 +1010,7 @@ export default function RoundtablePlanningPage() {
|
||||
pendingIntentClarificationMsgId={step1.pendingIntentClarificationMsgId}
|
||||
intentClarificationSelections={step1.intentClarificationSelections}
|
||||
setIntentClarificationSelections={step1.setIntentClarificationSelections}
|
||||
onSendMessage={(text) => void step1.sendIntentMessage(text)}
|
||||
onSendMessage={(text, files) => void step1.sendIntentMessage(text, files)}
|
||||
onStopIntent={step1.stopIntentStream}
|
||||
onSkipQuestion={handleSkipQuestion}
|
||||
onFollowUp={handleAgentFollowUp}
|
||||
|
||||
@@ -328,9 +328,15 @@ async def stream_intent(request: Request, payload: dict = Body(...)) -> Streamin
|
||||
# config.yaml 的 models[0],与 /api/ai-writing 等其它路由一致,内网部署只配
|
||||
# 自己的本地模型即可工作。
|
||||
model_name = str(payload.get("model") or "")
|
||||
# 用户在 Step 1 上传的参考文件(已经过 /api/threads/{thread_id}/uploads)。
|
||||
# 前端传 [{filename, size, path, status}, ...],这里原样透传给 _run_payload →
|
||||
# additional_kwargs.files → UploadsMiddleware 自动注入清单 + 解锁 read_file 等工具。
|
||||
raw_files = payload.get("files")
|
||||
files = raw_files if isinstance(raw_files, list) else []
|
||||
if not thread_id:
|
||||
raise HTTPException(status_code=400, detail="thread_id is required")
|
||||
if not message:
|
||||
# 允许「仅文件、无文本」的回合:带了文件就不强制要求 message。
|
||||
if not message and not files:
|
||||
raise HTTPException(status_code=400, detail="message is required")
|
||||
|
||||
# 调用前自检:防御性二次检查——若管理员在 init 之后手动删了目录,这里也能自愈。
|
||||
@@ -381,8 +387,17 @@ async def stream_intent(request: Request, payload: dict = Body(...)) -> Streamin
|
||||
_MAX_CLARIFICATION_ROUNDS,
|
||||
)
|
||||
|
||||
# 带了上传文件时,解锁「读」类工具(read_file / ls / grep / view_image),
|
||||
# 否则 UploadsMiddleware 注入了文件清单、模型却没工具读取。仍保留对
|
||||
# write_file / str_replace / present_files / bash / agent_orchestration 的封锁,
|
||||
# 防止意图阶段越界做方案/派活。
|
||||
if files:
|
||||
read_tools = {"read_file", "ls", "grep", "view_image"}
|
||||
base_excluded_tools = [t for t in base_excluded_tools if t not in read_tools]
|
||||
|
||||
# 2. message 前缀——给模型一个明确的"本轮必须收敛"信号。
|
||||
effective_message = message
|
||||
# 仅文件、无文本时给一句兜底引导,让模型先读文件再澄清意图。
|
||||
effective_message = message or "我上传了文件,请先阅读文件内容,再据此理解并澄清我的任务意图。"
|
||||
if past_clarifications >= _MAX_CLARIFICATION_ROUNDS:
|
||||
effective_message = (
|
||||
f"[系统强制] 已澄清 {past_clarifications} 轮,触发硬上限。"
|
||||
@@ -409,6 +424,7 @@ async def stream_intent(request: Request, payload: dict = Body(...)) -> Streamin
|
||||
# not reliable on its own, hard exclusion is.
|
||||
excluded_tools=base_excluded_tools,
|
||||
skill_stop_names=[],
|
||||
files=files,
|
||||
)
|
||||
|
||||
# captured:上游 SSE 全部 data 行,用于流结束后从 values 快照取 messages。
|
||||
|
||||
@@ -638,6 +638,7 @@ def _run_payload(
|
||||
reasoning_effort: str = "medium",
|
||||
multitask_strategy: str = "reject",
|
||||
extra_context: dict[str, Any] | None = None,
|
||||
files: list[dict[str, Any]] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""构造 ``POST /api/threads/{thread_id}/runs/stream`` 的请求体。
|
||||
|
||||
@@ -650,13 +651,19 @@ def _run_payload(
|
||||
应传 ``thinking_enabled=False`` + ``reasoning_effort="low"``,省掉模型先吐
|
||||
reasoning tokens 才开口的时间,显著降低前端"调度中"首字节延迟。
|
||||
"""
|
||||
# 用户上传的文件元数据透传给 UploadsMiddleware(与普通聊天同链路):
|
||||
# before_agent 钩子读 additional_kwargs.files,生成 <uploaded_files> 清单注入消息,
|
||||
# 之后席位/总控用内置 read_file / grep / view_image 工具按需读取。空时落空 dict,行为不变。
|
||||
additional_kwargs: dict[str, Any] = {}
|
||||
if files:
|
||||
additional_kwargs["files"] = files
|
||||
payload: dict[str, Any] = {
|
||||
"input": {
|
||||
"messages": [
|
||||
{
|
||||
"type": "human",
|
||||
"content": [{"type": "text", "text": new_message}],
|
||||
"additional_kwargs": {},
|
||||
"additional_kwargs": additional_kwargs,
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1258,6 +1265,7 @@ async def _leader_run(
|
||||
seat_meta: dict[str, dict[str, Any]] | None = None,
|
||||
synthesis_mode: bool = False,
|
||||
suppress_pre_broadcast: bool = False,
|
||||
files: list[dict[str, Any]] | None = None,
|
||||
) -> AsyncIterator[str | dict[str, Any]]:
|
||||
"""总控(leader)单轮:透传上游 SSE + 末尾 yield 一个 dict 状态帧(由路由层 ``json.dumps``)。
|
||||
|
||||
@@ -1346,18 +1354,26 @@ async def _leader_run(
|
||||
# 调用方可再叠加自己的 skill_stop。后台进程内网关用同一份 policy,保证传参一致。
|
||||
policy = roundtable_run_policy("leader")
|
||||
effective_stop_names = merge_skill_stop_names(policy.skill_stop_names, skill_stop_names)
|
||||
# 带了上传文件时,给总控解锁「读」类工具(leader policy 默认禁 read_file/ls/view_image),
|
||||
# 否则文件清单注入了却无工具读取。写/派活类仍按 policy 封锁,不放开。
|
||||
leader_excluded = policy.excluded_tools
|
||||
if files:
|
||||
_read_tools = {"read_file", "ls", "grep", "view_image"}
|
||||
leader_excluded = [t for t in leader_excluded if t not in _read_tools]
|
||||
body = _run_payload(
|
||||
agent_name=agent_name,
|
||||
model_name=model_name,
|
||||
thread_id=thread_id,
|
||||
# 注入了席位清单的输入(leader_input);seat_roster 为空时等同 new_message。
|
||||
new_message=leader_input,
|
||||
excluded_tools=policy.excluded_tools,
|
||||
excluded_tools=leader_excluded,
|
||||
skill_stop_names=effective_stop_names,
|
||||
thinking_enabled=policy.thinking_enabled,
|
||||
reasoning_effort=policy.reasoning_effort,
|
||||
# 人工干预/恢复会重发 leader run 到同一总控 thread;抢占掉没结束的旧 run,避免 409。
|
||||
multitask_strategy="rollback",
|
||||
# 用户随干预上传的参考文件 → 总控可用 read_file 等工具读取后再派活。
|
||||
files=files,
|
||||
)
|
||||
t_payload_built = time.perf_counter()
|
||||
logger.info(
|
||||
@@ -1532,6 +1548,7 @@ async def _special_run(
|
||||
display_name: str = "",
|
||||
no_file: bool = False,
|
||||
peer_deliveries: list[dict[str, Any]] | None = None,
|
||||
files: list[dict[str, Any]] | None = None,
|
||||
) -> AsyncIterator[str | dict[str, Any]]:
|
||||
"""子智能体(special)单轮:执行总控下发的 task 文本,交付 content。
|
||||
|
||||
@@ -1560,12 +1577,18 @@ async def _special_run(
|
||||
else:
|
||||
role = "seat"
|
||||
policy = roundtable_run_policy(role)
|
||||
# 带了上传文件时,给席位解锁「读」类工具(view_image 默认被禁;read_file 本就可用),
|
||||
# 让席位能读取/识别用户上传的文件。写/派活类仍按 policy 封锁。
|
||||
seat_excluded = policy.excluded_tools
|
||||
if files:
|
||||
_read_tools = {"read_file", "ls", "grep", "view_image"}
|
||||
seat_excluded = [t for t in seat_excluded if t not in _read_tools]
|
||||
body = _run_payload(
|
||||
agent_name=agent_name,
|
||||
model_name=model_name,
|
||||
thread_id=thread_id,
|
||||
new_message=new_message,
|
||||
excluded_tools=policy.excluded_tools,
|
||||
excluded_tools=seat_excluded,
|
||||
skill_stop_names=merge_skill_stop_names(policy.skill_stop_names, skill_stop_names),
|
||||
thinking_enabled=policy.thinking_enabled,
|
||||
reasoning_effort=policy.reasoning_effort,
|
||||
@@ -1575,6 +1598,8 @@ async def _special_run(
|
||||
extra_context=(
|
||||
{"roundtable_peer_deliveries": peer_deliveries} if peer_deliveries else None
|
||||
),
|
||||
# 用户随干预上传的参考文件 → 席位可用 read_file 等工具读取。
|
||||
files=files,
|
||||
)
|
||||
t_payload_built = time.perf_counter()
|
||||
logger.info(
|
||||
@@ -1677,6 +1702,11 @@ async def run_multi_agent_stream(request: Request, payload: dict = Body(...)) ->
|
||||
display_name: str = str(payload.get("display_name") or "").strip()
|
||||
new_message: str = str(payload.get("new_message") or "")
|
||||
skill_stop_names: list[str] = payload.get("skill_stop_names") or []
|
||||
# 用户随干预消息上传的参考文件(已经过 /api/threads/{thread_id}/uploads)。
|
||||
# 前端传 [{filename, size, path, status}, ...],透传给 _run_payload →
|
||||
# additional_kwargs.files → UploadsMiddleware 注入清单 + 解锁 read_file 等工具。
|
||||
_files_raw = payload.get("files")
|
||||
files: list[dict[str, Any]] = _files_raw if isinstance(_files_raw, list) else []
|
||||
# 综合轮标记(仅 leader 用):前端在「派活后让总控判断继续/综合」的轮次置真,网关据此
|
||||
# 额外把各已交付席位的完整交付正文注入总控输入,供其写最终方案。缺省 False(派活决策轮
|
||||
# 只给摘要,保持上下文精简)。旧前端不传 → False,行为安全退化。
|
||||
@@ -1741,6 +1771,7 @@ async def run_multi_agent_stream(request: Request, payload: dict = Body(...)) ->
|
||||
seat_meta=seat_meta,
|
||||
synthesis_mode=synthesis_mode,
|
||||
suppress_pre_broadcast=suppress_pre_broadcast,
|
||||
files=files,
|
||||
)
|
||||
else:
|
||||
gen = _special_run(
|
||||
@@ -1748,6 +1779,7 @@ async def run_multi_agent_stream(request: Request, payload: dict = Body(...)) ->
|
||||
display_name=display_name,
|
||||
no_file=no_file,
|
||||
peer_deliveries=peer_deliveries,
|
||||
files=files,
|
||||
)
|
||||
async for chunk in gen:
|
||||
if isinstance(chunk, dict):
|
||||
|
||||
Reference in new issue
Block a user