diff --git a/frontend-web/src/roundtable-planning/components/Step3Panel.tsx b/frontend-web/src/roundtable-planning/components/Step3Panel.tsx
index d3e94e98..293fe2a1 100644
--- a/frontend-web/src/roundtable-planning/components/Step3Panel.tsx
+++ b/frontend-web/src/roundtable-planning/components/Step3Panel.tsx
@@ -16,6 +16,7 @@ import type { ModelOption } from "@/roundtable-planning/components/Step3ReportMo
import type { Step2Dialogue } from "@/roundtable-planning/hooks/useStep2Orchestration";
import type { ReportIntent } from "@/roundtable-planning/hooks/useStep3Report";
import type { ThreadIdMap } from "@/roundtable-planning/api/multi-agent";
+import { stripReasoning } from "@/roundtable-planning/lib/reasoning";
export interface Step3PanelProps {
threadIds: ThreadIdMap | null;
@@ -76,7 +77,9 @@ export function Step3Panel(props: Step3PanelProps) {
.map((d) => ({
sender: String(d.sender ?? ""),
avatarType: String(d.avatarType ?? "blue"),
- content: String(d.content ?? ""),
+ // 交付内容只保留模型生成的主体输出,剥离 `...` 推理过程
+ // —— 既用于渲染各席位交付卡片,也会喂给总结/报告模型,避免推理噪声污染。
+ content: stripReasoning(String(d.content ?? "")),
}));
const seatNameMap = Object.fromEntries(
diff --git a/frontend-web/src/roundtable-planning/lib/reasoning.test.ts b/frontend-web/src/roundtable-planning/lib/reasoning.test.ts
new file mode 100644
index 00000000..454865f1
--- /dev/null
+++ b/frontend-web/src/roundtable-planning/lib/reasoning.test.ts
@@ -0,0 +1,61 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import { splitInlineReasoning, stripReasoning } from "./reasoning";
+
+test("空输入返回空正文与 null 推理", () => {
+ assert.deepEqual(splitInlineReasoning(""), { content: "", reasoning: null });
+});
+
+test("无 think 标签时原样返回正文", () => {
+ assert.deepEqual(splitInlineReasoning("纯正文,无推理"), {
+ content: "纯正文,无推理",
+ reasoning: null,
+ });
+});
+
+test("形态 1:完整 … 提取推理,保留正文", () => {
+ const { content, reasoning } = splitInlineReasoning("先想一下这是答案");
+ assert.equal(content, "这是答案");
+ assert.equal(reasoning, "先想一下");
+});
+
+test("形态 2:多段 think 合并推理", () => {
+ const { content, reasoning } = splitInlineReasoning(
+ "第一段正文A第二段正文B",
+ );
+ assert.equal(content, "正文A正文B");
+ assert.equal(reasoning, "第一段\n\n第二段");
+});
+
+test("形态 3:流式未闭合 尾段算推理", () => {
+ const { content, reasoning } = splitInlineReasoning("正文还在想…");
+ assert.equal(content, "正文");
+ assert.equal(reasoning, "还在想…");
+});
+
+test("形态 4:模板注入式只吐闭合 ,前导内容算推理", () => {
+ const { content, reasoning } = splitInlineReasoning(
+ "我需要分析一下需求这是最终方案",
+ );
+ assert.equal(content, "这是最终方案");
+ assert.equal(reasoning, "我需要分析一下需求");
+});
+
+test("形态 4 流式:尚未吐出 时全部当正文(后续闭合再归位)", () => {
+ // 未出现任何标签 → 无从区分,先按正文渲染。
+ const { content, reasoning } = splitInlineReasoning("正在推理中,还没收到闭合标签");
+ assert.equal(content, "正在推理中,还没收到闭合标签");
+ assert.equal(reasoning, null);
+});
+
+test("安全兜底:正文里绝不残留任何 think 标签字面量", () => {
+ const { content } = splitInlineReasoning("答案前答案中");
+ assert.ok(!content.includes(""));
+ assert.ok(!content.includes(""));
+});
+
+test("stripReasoning 只返回正文", () => {
+ assert.equal(stripReasoning("推理交付正文"), "交付正文");
+ assert.equal(stripReasoning("前导推理交付正文"), "交付正文");
+});
diff --git a/frontend-web/src/roundtable-planning/lib/reasoning.ts b/frontend-web/src/roundtable-planning/lib/reasoning.ts
index 2567e51d..ecfbe5c1 100644
--- a/frontend-web/src/roundtable-planning/lib/reasoning.ts
+++ b/frontend-web/src/roundtable-planning/lib/reasoning.ts
@@ -1,15 +1,18 @@
/**
* 把模型流式文本里的 `...` 块拆出来,作为「思考过程」单独展示。
*
- * 兼容三种形态:
+ * 兼容四种形态:
* 1) **完整** `foobar` —— 提取 `foo` 作 reasoning,正文为 `bar`
* 2) **多段** `axby` —— reasoning 合并为 `a\n\nb`,正文为 `xy`
* 3) **流式未闭合** `barpartial...` —— reasoning 为 `partial...`(尾段),正文为 `bar`
+ * 4) **模板注入式(只吐闭合)** `foobar` —— 开标签 `` 被 chat template
+ * 注入到 prompt 里、模型只生成闭合标签(DeepSeek-R1 / QwQ 等推理模型的典型行为)。
+ * 此时首个 `` 之前没有配对的 ``,把它之前的内容整体算 reasoning,正文为 `bar`。
*
* 实现思路与主聊天 `src/core/messages/utils.ts::splitInlineReasoning` 对齐,但
- * 这里**额外处理了未闭合的 ``** —— 流式渲染时如果模型还没吐出 ``,
- * 主聊天那份实现会把 `` 字面量留在正文里,我们用 lastIndexOf 兜底,把尾段
- * 也算作 reasoning。
+ * 这里**额外处理了未闭合的 ``(形态 3)和只吐闭合的 ``(形态 4)** ——
+ * 主聊天靠后端 `additional_kwargs.reasoning_content` 单列推理,而圆桌多智能体流式只取
+ * `content` 文本,推理标签会内联在正文里,故这里需要把这两种内联形态都兜住。
*
* Inline `foo` 在文本中间出现(无尾闭合)被认为是流式未闭合的开头:
* 假设之后会闭合,把它之后所有内容算 reasoning。这与 DeepSeek-R1 / QwQ 等
@@ -18,6 +21,9 @@
const THINK_TAG_RE = /\s*([\s\S]*?)\s*<\/think>/g;
const OPEN_TAG = "";
+const CLOSE_TAG = "";
+// 兜底:正文里绝不应残留任何 think 标签字面量,清掉任何孤立的 `` / ``。
+const ANY_THINK_TAG_RE = /<\/?think>/gi;
export interface SplitReasoningResult {
/** 正文(去掉所有 `...` 块以及尾段未闭合 `` 之后内容)。 */
@@ -30,14 +36,27 @@ export function splitInlineReasoning(text: string): SplitReasoningResult {
if (!text) return { content: "", reasoning: null };
const reasoningParts: string[] = [];
- // 先处理所有完整 `...` 闭合块。
- const closed = text.replace(THINK_TAG_RE, (_, reasoning: string) => {
+
+ // 形态 4(模板注入式,只吐闭合):首个 `` 出现在任何 `` 之前(或根本
+ // 没有 ``),说明开标签被 chat template 注入、模型只生成了闭合标签。把第一个
+ // `` 之前的内容整体算作 reasoning,正文从闭合标签之后继续解析。
+ let working = text;
+ const firstClose = working.indexOf(CLOSE_TAG);
+ const firstOpen = working.indexOf(OPEN_TAG);
+ if (firstClose >= 0 && (firstOpen < 0 || firstClose < firstOpen)) {
+ const lead = working.slice(0, firstClose).trim();
+ if (lead) reasoningParts.push(lead);
+ working = working.slice(firstClose + CLOSE_TAG.length);
+ }
+
+ // 形态 1/2:处理所有完整 `...` 闭合块。
+ const closed = working.replace(THINK_TAG_RE, (_, reasoning: string) => {
const normalized = reasoning.trim();
if (normalized) reasoningParts.push(normalized);
return "";
});
- // 处理尾段未闭合 `` —— 流式情况。
+ // 形态 3:尾段未闭合 `` —— 流式情况。
// 如果 closed 里还残留 `` 字面量,说明这一段还没收到 ``,
// 把它之后的所有内容视为 reasoning-in-progress,正文截到 `` 之前。
let finalContent = closed;
@@ -48,8 +67,16 @@ export function splitInlineReasoning(text: string): SplitReasoningResult {
finalContent = closed.slice(0, openIdx);
}
+ // 安全兜底:无论上面是否完全配对,正文里都不该再出现 think 标签字面量。
+ finalContent = finalContent.replace(ANY_THINK_TAG_RE, "");
+
return {
content: finalContent.trim(),
reasoning: reasoningParts.length > 0 ? reasoningParts.join("\n\n").trim() : null,
};
}
+
+/** 只要正文、丢弃推理过程的便捷封装(用于「交付内容」类只需主体输出的场景)。 */
+export function stripReasoning(text: string): string {
+ return splitInlineReasoning(text).content;
+}
diff --git a/frontend-web/src/roundtable-planning/lib/roundtable-deliverables.test.ts b/frontend-web/src/roundtable-planning/lib/roundtable-deliverables.test.ts
index b8beb631..da613cc5 100644
--- a/frontend-web/src/roundtable-planning/lib/roundtable-deliverables.test.ts
+++ b/frontend-web/src/roundtable-planning/lib/roundtable-deliverables.test.ts
@@ -79,3 +79,15 @@ test("coordinatorName 为 null 时不剔除任何席位", () => {
const result = collectRoundtableDeliverables(dialogues, null);
assert.equal(result.length, 2);
});
+
+test("交付正文剥离 … 推理过程", () => {
+ const dialogues = [
+ bubble({ sender: "方案设计", content: "盘算一下方案正文" }),
+ bubble({ sender: "风险审查", content: "前导推理风险正文" }),
+ ];
+ const result = collectRoundtableDeliverables(dialogues, null);
+ assert.deepEqual(result, [
+ { agentName: "方案设计", content: "方案正文", threadId: null },
+ { agentName: "风险审查", content: "风险正文", threadId: null },
+ ]);
+});
diff --git a/frontend-web/src/roundtable-planning/lib/roundtable-deliverables.ts b/frontend-web/src/roundtable-planning/lib/roundtable-deliverables.ts
index 5219e7ec..8878fae9 100644
--- a/frontend-web/src/roundtable-planning/lib/roundtable-deliverables.ts
+++ b/frontend-web/src/roundtable-planning/lib/roundtable-deliverables.ts
@@ -1,4 +1,5 @@
import type { Step2Dialogue } from "../hooks/useStep2Orchestration";
+import { stripReasoning } from "./reasoning";
/**
* 回传父页面(无界)的单个智能体交付。
@@ -29,7 +30,8 @@ export function collectRoundtableDeliverables(
}
return Array.from(lastBySeat.values()).map((d) => ({
agentName: d.sender,
- content: d.content,
+ // 回传宿主的交付正文剥离 `...` 推理过程,只留主体输出。
+ content: stripReasoning(d.content),
threadId: d.agentThreadId ?? null,
}));
}
diff --git a/offline-backend-20260512/backend/app/gateway/roundtable_inprocess_gateway.py b/offline-backend-20260512/backend/app/gateway/roundtable_inprocess_gateway.py
index a9502170..b8cd96d6 100644
--- a/offline-backend-20260512/backend/app/gateway/roundtable_inprocess_gateway.py
+++ b/offline-backend-20260512/backend/app/gateway/roundtable_inprocess_gateway.py
@@ -3,7 +3,7 @@
实现 ``RoundtableGateway`` 协议(定义在 harness 的 roundtable_orchestrator.types),
但**进程内**跑真模型:复用 ``services.start_run``(scheduled task 同款)驱动一轮 agent,
跑完从 checkpointer 读最终 messages,用 ``multi_agent`` 的纯解析函数解析派活 / 澄清。
-不走 HTTP、不需要 Request(用 ``SimpleNamespace(app=app)`` 当虚拟 request,
+不走 HTTP、不需要 Request(用 ``SimpleNamespace(app=app, state=...)`` 当虚拟 request,
``set_current_user`` 由执行器设好)。
本网关位于 app 层(依赖 app.state + multi_agent helpers),实现 harness 定义的协议——
@@ -192,7 +192,9 @@ async def _run_agent_turn(
on_disconnect="continue",
multitask_strategy="reject",
)
- request = SimpleNamespace(app=app)
+ # 虚拟 request:带上空 ``state``(auth 中间件在真请求上会写 state.user / state.auth;
+ # 进程内没有这层,下游凡是读 request.state.X 的代码必须能拿到 None 而不是 AttributeError)。
+ request = SimpleNamespace(app=app, state=SimpleNamespace(user=None, auth=None))
record = await start_run(body, thread_id, request) # type: ignore[arg-type]
if getattr(record, "task", None) is not None:
await record.task # 阻塞到这轮 agent 跑完,checkpoint 已落盘
diff --git a/offline-backend-20260512/backend/app/gateway/services.py b/offline-backend-20260512/backend/app/gateway/services.py
index 359dba8b..12c8781b 100644
--- a/offline-backend-20260512/backend/app/gateway/services.py
+++ b/offline-backend-20260512/backend/app/gateway/services.py
@@ -493,7 +493,11 @@ async def _enforce_agent_access(body: Any, request: Request) -> None:
user_id = get_effective_user_id()
# Admins see every agent in the admin console listing (?admin=true), so
# they may also start a run against any of them.
- is_admin = getattr(getattr(request.state, "user", None), "system_role", None) == "admin"
+ # ``request`` may be an in-process virtual request (SimpleNamespace) from
+ # the roundtable job gateway / scheduled-task runtime — those carry no
+ # ``state`` attribute, so the whole chain must be getattr-guarded
+ # (a bare ``request.state`` raises AttributeError and kills the run).
+ is_admin = getattr(getattr(getattr(request, "state", None), "user", None), "system_role", None) == "admin"
for agent_id in agent_ids:
try:
normalized = validate_agent_id(agent_id)
diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/runtime/scheduler/service.py b/offline-backend-20260512/backend/packages/harness/deerflow/runtime/scheduler/service.py
index aef85a5c..fdb6fc59 100644
--- a/offline-backend-20260512/backend/packages/harness/deerflow/runtime/scheduler/service.py
+++ b/offline-backend-20260512/backend/packages/harness/deerflow/runtime/scheduler/service.py
@@ -1408,7 +1408,9 @@ class ScheduledTaskService:
token = set_current_user(_RuntimeUser(id=task["user_id"]))
try:
- request = SimpleNamespace(app=self.app)
+ # 虚拟 request:带空 state(真请求由 auth 中间件写 state.user/state.auth;
+ # 进程内调用没有这层,下游读 request.state.X 必须拿到 None 而非 AttributeError)。
+ request = SimpleNamespace(app=self.app, state=SimpleNamespace(user=None, auth=None))
execution_context = dict(task.get("execution_context") or {})
stored_agent_ref = task.get("execution_agent_name")
if stored_agent_ref: