fix: 会商第二步只对可预览成品出文件卡/开沙箱,去掉干预并发送按钮

This commit is contained in:
董宏凯 committed 2026-08-30 20:02:21 +08:00
1 parent 7066798ab0
commit b92a26cdd4
3 files changed
+41 -29

No files matched your search

@@ -22,6 +22,7 @@ import {
ChainOfThoughtStep,
} from "@/components/ai-elements/chain-of-thought";
import { Button } from "@/components/ui/button";
import { languageOfStreamingArtifact } from "@/core/artifacts/streaming-write-preview";
import { cn } from "@/lib/utils";
import {
getStepDisplay,
@@ -177,13 +178,17 @@ function ToolStep({
const display = getStepDisplay(step.key, step.label, step.args);
const argNode = renderStepArgs(step.key, step.args, resolveAgentName);
const [, toolName] = step.key.split(":");
// 只让「自己生成/展示的文件」可点(write_file / str_replace / present_files),
// 与主聊天页一致。read_file 不可点 —— 否则智能体调用技能时读取的 SOUL.md、
// 执行的 .py 脚本等也会被渲染成可点击文件名,点开却没有意义。
// 只让「自己生成/展示的文件」可点,与主聊天页一致。read_file 不可点 —— 否则智能体
// 调用技能时读取的 SOUL.md、执行的 .py 脚本等也会被渲染成可点击文件名,点开却没有意义。
// write_file / str_replace 还要过与文件卡同一道门槛(`languageOfStreamingArtifact`,
// 只认 .md/.html/.mermaid 等可预览成品):席位写的过程文件(fetch.py / r1.txt)与
// 畸形路径(mnt/user-data/outputs)只作步骤展示、点了也不会打开沙箱。
// present_files 由后端校验"outputs 下真实存在",保持可点。
const stepPath = typeof step.args?.path === "string" ? step.args.path : "";
const isArtifactStep =
toolName === "write_file" ||
toolName === "str_replace" ||
toolName === "present_files";
toolName === "present_files" ||
((toolName === "write_file" || toolName === "str_replace") &&
!!languageOfStreamingArtifact(stepPath));
return (
<ChainOfThoughtStep
@@ -43,6 +43,7 @@ const SHOW_BACKGROUND_SUSPEND = true;
import { AutoGrowTextarea } from "@/components/ui/auto-grow-textarea";
import { PositionIntentInput } from "@/position-roundtable/components/PositionIntentInput";
import { useLocalSettings } from "@/core/settings";
import { languageOfStreamingArtifact } from "@/core/artifacts/streaming-write-preview";
import { cn } from "@/lib/utils";
import type { MessageMetric } from "@/components/workspace/messages/use-message-metrics";
@@ -985,14 +986,6 @@ function Step2LegacyComposer({
</span>
</button>
)}
<button
type="button"
onClick={onSend}
title="记录干预指令并立即恢复会商执行"
className="shrink-0 cursor-pointer rounded-xl bg-primary px-4 py-2.5 text-xs font-bold text-white shadow-md shadow-primary/10 transition hover:bg-[var(--brand-900)] active:scale-95"
>
干预并发送
</button>
<button
type="button"
onClick={onProceed}
@@ -1163,11 +1156,17 @@ function RoundtableSubtaskCard({
/**
* 收集气泡里所有"写了文件"的产出路径,按出现顺序去重,聚合到一行文件卡片。
*
* 三类来源都算:
* - `present_files`:总控呈现文件,args.filepaths 是数组(可一次多个);
* 三类来源:
* - `present_files`:总控呈现文件,args.filepaths 是数组(可一次多个)。后端
* present_files 已强制"必须在 /mnt/user-data/outputs 下且真实存在",与主聊天
* `chats/:threadId` 的产物面板同源,原样放行;
* - `write_file` / `str_replace`:子智能体被后端 excluded_tools 禁用了 present_files,
* 只能 write_file 写文件,单文件 path 在 args.path —— 也聚合进卡片,保证"凡是写了
* 文件就出可点的卡片",而不是只有 present_files 才出。
* 成品文件就出可点的卡片"。
*
* write_file/str_replace 的 path 需过 `languageOfStreamingArtifact`(只认 .md/.html/
* .mermaid 等可预览产物,与主聊天 `ArtifactWriteToolCall` 的流式预览门槛同一套):
* 席位在沙箱里写的过程文件(fetch.py / r1.txt 等)与无扩展名的畸形路径不再出卡。
*/
function collectPresentedFiles(
steps: Step2Dialogue["steps"],
@@ -1175,22 +1174,23 @@ function collectPresentedFiles(
if (!Array.isArray(steps) || steps.length === 0) return [];
const out: string[] = [];
const seen = new Set<string>();
const add = (fp: unknown) => {
const add = (fp: unknown, requirePreviewable = false) => {
if (typeof fp !== "string" || !fp || seen.has(fp)) return;
if (requirePreviewable && !languageOfStreamingArtifact(fp)) return;
seen.add(fp);
out.push(fp);
};
for (const step of steps) {
if (step.key === "tool_calling:present_files") {
const fps = step.args?.filepaths;
if (Array.isArray(fps)) fps.forEach(add);
if (Array.isArray(fps)) fps.forEach((fp) => add(fp));
continue;
}
if (
step.key === "tool_calling:write_file" ||
step.key === "tool_calling:str_replace"
) {
add(step.args?.path);
add(step.args?.path, true);
}
}
return out;
@@ -29,6 +29,7 @@ import {
} from "@/roundtable-planning/api/multi-agent";
import { isValidSeatDelivery, stripReasoning } from "@/roundtable-planning/lib/reasoning";
import { uploadFiles } from "@/core/uploads/api";
import { languageOfStreamingArtifact } from "@/core/artifacts/streaming-write-preview";
import { useUpdateSubtask } from "@/core/tasks/context";
/** 干预消息携带的上传文件引用(与 streamMultiAgent 的 files 字段同形)。 */
@@ -924,6 +925,9 @@ export function useStep2Orchestration(
? ((tc.args as { path?: string }).path as string)
: "";
if (!path) continue;
// 同 handleArtifactToolPhase 的门槛:跳过 .py/.txt 等过程文件与畸形路径,
// 沙箱跟随只落在该席位最近的**成品**文件上。
if (!languageOfStreamingArtifact(path)) continue;
const virtualUrl = buildWriteFileVirtualUrl(path, stub.id, tc.id);
// manual:false —— 沙箱跟随是系统行为,强制打开但**不**关闭 autoSelect(否则毒化
// 后续实时自动打开,见 openSeatVirtualArtifact 注释)。
@@ -1065,6 +1069,8 @@ export function useStep2Orchestration(
if (toolName === "write_file" || toolName === "str_replace") {
const path = step.args?.path;
if (typeof path !== "string" || !step.args) return;
// 与 MessageStepsCard 的可点判定同一道门槛:过程文件/畸形路径点了不开沙箱。
if (!languageOfStreamingArtifact(path)) return;
const { messageId, toolCallId } = upsertSeatToolCall(
seatThreadId,
toolName,
@@ -1116,6 +1122,10 @@ export function useStep2Orchestration(
}
return;
}
// 与主聊天 ArtifactWriteToolCall 的流式预览同一道门槛:只有 .md/.html/.mermaid
// 等可预览产物才自动打开沙箱。席位写的过程文件(fetch.py / r*.txt)与无扩展名的
// 畸形路径不再触发自动预览,免得右侧沙箱被无意义文件反复抢占。
if (!languageOfStreamingArtifact(path)) return;
const { messageId, toolCallId } = upsertSeatToolCall(
seatThreadId,
toolName as string,
@@ -1972,11 +1982,9 @@ export function useStep2Orchestration(
// 第 2 轮起(已有席位交付)才是「综合轮」:让后端注入各席位完整交付供总控收口。
// 首轮纯派活、无交付,传 false 保持上下文精简。
synthesisMode: safetyCounter > 1,
// 削减重复广播(2026-06-20):首轮(safetyCounter==1)保留预广播——它把任务背景播进各席位
// thread,为席位打底;第 2 轮起的续轮/综合广播是「总控收到:<续轮 prompt>」纯噪声 + N 次
// 读写巨大席位 thread 的纯浪费(还会让弱模型从历史模仿派活),故 suppress。席位的前序交付
// 另经「席位交付广播」(broadcastSeatDeliveryToOthers)送达,不受影响。
suppressPreBroadcast: safetyCounter > 1,
// 总控 prompt 含派活/编排指令,只能进入总控 thread。任何轮次都禁止广播到席位历史,
// 否则弱模型会模仿总控并声称要加载编排技能。席位上下文由具体 task 与交付广播提供。
suppressPreBroadcast: true,
// 用户随干预上传的参考文件(consume-once:仅首轮 leader 带,之后清空)。
files: takeOnce(pendingInjectFilesRef),
model: selectedModelRef.current || undefined,
@@ -2257,10 +2265,9 @@ export function useStep2Orchestration(
dAgentThreadId: ids,
agentName: coord,
newMessage: leaderTask,
// 削减重复广播(2026-06-20):首个席位(i===0)保留预广播为其打底任务背景;从第 2 个席位起
// suppress——后续席位的前序交付已由「席位交付广播」送达其 thread,总控派活 prompt 再播一遍
// 是 N 次读写巨大 thread 的纯浪费(还会让弱模型模仿派活)。
suppressPreBroadcast: i > 0,
// 链条首轮同样不能把「请总控调用 agent_orchestration」广播进席位 thread。
// 当前席位拿到的是总控生成的具体 task,后续席位再通过交付广播承接上下文。
suppressPreBroadcast: true,
// 用户随干预上传的参考文件(consume-once:仅首轮 leader 带,之后清空)。
files: takeOnce(pendingInjectFilesRef),
model: selectedModelRef.current || undefined,