From 7d32b41e90eaa098b471f6fe825327ee4bfc5cbe Mon Sep 17 00:00:00 2001 From: fengxiangdebaozi <10845145+fengxiangdebaozi@user.noreply.gitee.com> Date: Mon, 22 Jun 2026 10:46:57 +0800 Subject: [PATCH 1/7] =?UTF-8?q?=E5=8A=A0=E8=BD=BD=E6=A1=86=E6=A0=B7?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/RoundtablePlanningPage.tsx | 59 ++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx b/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx index 4b9cf882..bb7b0733 100644 --- a/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx +++ b/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx @@ -296,6 +296,10 @@ export default function RoundtablePlanningPage() { const [taskRestoreResolved, setTaskRestoreResolved] = useState( !prefillRef.current?.taskId, ); + // 深链/预填自动会话:进入页面后要先建意图线程、(带 taskId 时)回显历史草稿,这段时间 + // 页面只有一句欢迎语、问答还没真正开始,线程创建慢时像卡住。下面这个标志用来在超时后 + // 强制撤掉「启动加载遮罩」,避免万一初始化卡死把界面永久锁住。 + const [bootLoadingExpired, setBootLoadingExpired] = useState(false); const { models: availableModels, isLoading: modelsLoading } = useModels(); @@ -1143,6 +1147,35 @@ export default function RoundtablePlanningPage() { }; }, []); + // ── 启动加载遮罩(深链/预填自动会话)────────────────────────────── + // 是否是「自动会话」:深链 -A、taskId、businessCode/chainId 任一存在即自动进入问答, + // 用户没有手动操作,进入页面到问答之间必须给出明确加载反馈。 + const isAutoSession = Boolean( + prefillRef.current?.autoStart || + prefillRef.current?.taskId || + prefillRef.current?.businessCode || + prefillRef.current?.chainId, + ); + // 「问答已开始」:自动发出的首条用户消息已入列 / 正在流式 / 已得到意图结果,任一为真即认为 + // 已经动起来,可以撤掉遮罩交给 Step1Panel 自身的流式占位。 + const conversationStarted = + step1.isIntentSending || + step1.intentReady != null || + step1.interactiveMessages.some((m) => m.sender === "user"); + // 遮罩展示条件:自动会话 + 仍在 Step1 + 问答还没开始 + 没有初始化报错 + 没到安全超时。 + const showBootLoading = + isAutoSession && + currentStep === 1 && + !conversationStarted && + !step1.intentInitError && + !bootLoadingExpired; + // 安全超时:无论如何最多遮 30s,避免初始化卡死锁住界面(用户仍可手动操作底层页面)。 + useEffect(() => { + if (!showBootLoading) return; + const timer = window.setTimeout(() => setBootLoadingExpired(true), 30_000); + return () => window.clearTimeout(timer); + }, [showBootLoading]); + /** 组装无界回传载荷:总控共识 + 各智能体最终交付(剔除总控席位)。 */ const buildWujieConsensusPayload = useCallback( () => ({ @@ -1780,10 +1813,34 @@ export default function RoundtablePlanningPage() { return (
+ {/* 启动加载遮罩:深链/预填自动会话从进入页面到问答真正开始(建意图线程 / 回显历史草稿) + 期间全屏覆盖,避免线程创建慢时页面像卡住。问答一旦开始或超时即撤掉。 */} + {showBootLoading && ( +
+
+
+

+ 正在准备多智能体会商 +

+

正在初始化任务理解智能体,请稍候…

+
+ + + + + +
+
+ )} + Date: Mon, 22 Jun 2026 11:02:51 +0800 Subject: [PATCH 2/7] =?UTF-8?q?feat(layout):=20=E7=AE=80=E6=B4=81=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=E5=8F=B3=E4=B8=8A=E8=A7=92=E5=8A=A0=E6=8D=A2=E8=82=A4?= =?UTF-8?q?=20+=20=E7=95=8C=E9=9D=A2=E6=A8=A1=E5=BC=8F=E4=B8=8B=E6=8B=89?= =?UTF-8?q?=E5=88=87=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 抽出共享 appearance-controls(ThemeSwitcher 按 tone 适配 banner/content 配色,新增图标+下拉的 LayoutModeSwitcher);Header 复用换肤组件,简洁模式 ChatPage 顶栏右上角补上换肤与界面模式切换,与全量模式保持一致。 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../workspace/appearance-controls.tsx | 178 ++++++++++++++++++ frontend-web/src/pages/ChatPage.tsx | 8 + .../src/strategy-components/Header.tsx | 96 +--------- 3 files changed, 191 insertions(+), 91 deletions(-) create mode 100644 frontend-web/src/components/workspace/appearance-controls.tsx diff --git a/frontend-web/src/components/workspace/appearance-controls.tsx b/frontend-web/src/components/workspace/appearance-controls.tsx new file mode 100644 index 00000000..8decbe91 --- /dev/null +++ b/frontend-web/src/components/workspace/appearance-controls.tsx @@ -0,0 +1,178 @@ +"use client"; + +import { Check, LayoutIcon, PanelLeftIcon, Shirt } from "lucide-react"; +import { useEffect, useRef, useState } from "react"; + +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { useTheme, type ThemeType } from "@/contexts/ThemeContext"; +import { useLayoutMode } from "@/core/system-settings/hooks"; +import { cn } from "@/lib/utils"; + +/** + * 换肤 / 界面模式 顶栏控件。深色顶栏(Header)与简洁模式页面级顶栏(ChatPage 等)共用, + * 通过 `tone` 适配不同底色: + * - "banner-dark" / "banner-light":Header 落在主题 banner 上(白字 / 浅色 hover)。 + * - "content":简洁模式聊天页浅底顶栏,走 foreground / muted-foreground。 + */ +export type AppearanceControlTone = "banner-dark" | "banner-light" | "content"; + +// 换肤色板:红色=红白主题(light),蓝色=浅蓝主题(lightblue),深蓝色=深色主题(dark) +export const THEME_SWATCHES: { id: ThemeType; label: string; color: string }[] = [ + { id: "light", label: "红色", color: "#cf1322" }, + { id: "lightblue", label: "蓝色", color: "#0062d9" }, + { id: "dark", label: "深蓝色", color: "#002466" }, +]; + +const triggerToneClass = (tone: AppearanceControlTone) => + tone === "banner-dark" + ? "text-slate-400 hover:bg-slate-800/50 hover:text-slate-400" + : tone === "banner-light" + ? "text-white hover:bg-[var(--brand-950)] hover:text-white active:bg-[#8a0618]" + : "text-muted-foreground hover:bg-muted hover:text-foreground"; + +export function ThemeSwitcher({ tone }: { tone: AppearanceControlTone }) { + const { theme, setTheme } = useTheme(); + const [open, setOpen] = useState(false); + const closeTimer = useRef | null>(null); + + const cancelClose = () => { + if (closeTimer.current) { + clearTimeout(closeTimer.current); + closeTimer.current = null; + } + }; + const scheduleClose = () => { + cancelClose(); + closeTimer.current = setTimeout(() => setOpen(false), 120); + }; + + useEffect(() => () => cancelClose(), []); + + return ( +
{ + cancelClose(); + setOpen(true); + }} + onMouseLeave={scheduleClose} + > + + {open ? ( +
+
+
+ {THEME_SWATCHES.map((swatch) => { + const active = theme === swatch.id; + return ( +
+
+ ) : null} +
+ ); +} + +const LAYOUT_OPTIONS = [ + { + id: "full" as const, + label: "全量模式", + description: "展示侧边栏与顶栏导航", + icon: PanelLeftIcon, + }, + { + id: "compact" as const, + label: "简洁模式", + description: "隐藏侧边栏与顶栏,获得更大工作区", + icon: LayoutIcon, + }, +]; + +/** + * 界面模式(全量 / 简洁)下拉切换。简洁模式下全局顶栏被隐藏,故把外观设置里的 + * 「界面模式」切换也搬到页面级顶栏右上角,图标 + 下拉,与换肤并排。 + */ +export function LayoutModeSwitcher({ tone }: { tone: AppearanceControlTone }) { + const [mode, setLayoutMode] = useLayoutMode(); + // 未显式设置过时简洁模式才会渲染本控件(全量模式有全局顶栏),故当前态按 compact 兜底。 + const current = mode === "full" ? "full" : "compact"; + const TriggerIcon = current === "full" ? PanelLeftIcon : LayoutIcon; + + return ( + + + + + + 界面模式 + + {LAYOUT_OPTIONS.map((option) => { + const Icon = option.icon; + const active = current === option.id; + return ( + setLayoutMode(option.id)} + className="gap-2" + > + +
+ {option.label} + + {option.description} + +
+ {active && } +
+ ); + })} +
+
+ ); +} diff --git a/frontend-web/src/pages/ChatPage.tsx b/frontend-web/src/pages/ChatPage.tsx index b1c24df3..325a8739 100644 --- a/frontend-web/src/pages/ChatPage.tsx +++ b/frontend-web/src/pages/ChatPage.tsx @@ -26,6 +26,10 @@ import { ThreadContext } from "@/components/workspace/messages/context"; import { QuestionNavigator } from "@/components/workspace/question-navigator"; import { ThreadTitle } from "@/components/workspace/thread-title"; import { TodoList } from "@/components/workspace/todo-list"; +import { + LayoutModeSwitcher, + ThemeSwitcher, +} from "@/components/workspace/appearance-controls"; import { TokenUsageIndicator } from "@/components/workspace/token-usage-indicator"; import { UserIdentityBadge } from "@/components/workspace/user-identity-badge"; import { Welcome } from "@/components/workspace/welcome"; @@ -484,8 +488,12 @@ export default function ChatPage() { {/* 不带全局顶栏(紧凑模式)时,名称+岗位回落到聊天页顶栏右侧, 与全量模式 Header 右上角保持一致。全量模式已有顶栏则不重复展示。 */} + {/* 简洁模式无全局顶栏:把换肤 + 界面模式切换搬到页面级顶栏右上角, + 与全量模式 Header 右上角保持一致(content 配色贴浅底顶栏)。 */} {!compactChrome && ( <> + +
cn( @@ -32,93 +33,6 @@ const iconButtonClass = (isDark: boolean) => const LEFT_MENU_IDS = ["qa-center", "task-management", "light-app-center"]; const RIGHT_MENU_IDS = ["comprehensive-management", "resource-management"]; -// 换肤色板:红色=红白主题(light),蓝色=浅蓝主题(lightblue),深蓝色=深色主题(dark) -const THEME_SWATCHES: { id: ThemeType; label: string; color: string }[] = [ - { id: "light", label: "红色", color: "#cf1322" }, - { id: "lightblue", label: "蓝色", color: "#0062d9" }, - { id: "dark", label: "深蓝色", color: "#002466" }, -]; - -function ThemeSwitcher({ isDark }: { isDark: boolean }) { - const { theme, setTheme } = useTheme(); - const [open, setOpen] = useState(false); - const closeTimer = useRef | null>(null); - - const cancelClose = () => { - if (closeTimer.current) { - clearTimeout(closeTimer.current); - closeTimer.current = null; - } - }; - const scheduleClose = () => { - cancelClose(); - closeTimer.current = setTimeout(() => setOpen(false), 120); - }; - - useEffect(() => () => cancelClose(), []); - - // 切换按钮:悬浮只改背景,不改图标颜色(显式锁定 hover 文字色等于常态色) - const triggerClass = cn( - "size-8 shrink-0 rounded-sm", - isDark - ? "text-slate-400 hover:bg-slate-800/50 hover:text-slate-400" - : "text-white hover:bg-[var(--brand-950)] hover:text-white active:bg-[#8a0618]", - ); - - return ( -
{ - cancelClose(); - setOpen(true); - }} - onMouseLeave={scheduleClose} - > - - {open ? ( -
-
-
- {THEME_SWATCHES.map((swatch) => { - const active = theme === swatch.id; - return ( -
-
- ) : null} -
- ); -} - type HeaderProps = { collapsed?: boolean; }; @@ -210,7 +124,7 @@ const Header = ({ collapsed }: HeaderProps) => { // 右侧操作区(换肤/通知/用户)—— 深浅两套布局共用 const actionCluster = ( <> - +
-

会商诊断日志

+

会商报错日志

- 多智能体会商全流程报错与关键事件(含后台挂起卡死、超时、产物缺失等) + 多智能体会商全流程报错日志(席位失败 / 空交付 / 超时 / 模型容错 / 产物缺失 / 崩溃等;运行进度类 info 不再收集)

- {/* 下:时间(左下) + 步骤(右下) */} -
- + {/* 下:时间(左下) + 业务链(右下,无链条显示「自由模式」) */} +
+ {formatTime(rec.updatedAt)} - - 第 {rec.furthestStep} 步 + + {rec.chainTitle || "自由模式"}
diff --git a/offline-backend-20260512/backend/CLAUDE.md b/offline-backend-20260512/backend/CLAUDE.md index 22eca306..68f8f72f 100644 --- a/offline-backend-20260512/backend/CLAUDE.md +++ b/offline-backend-20260512/backend/CLAUDE.md @@ -245,9 +245,9 @@ FastAPI application on port 8001 with health check at `GET /health`. Set `GATEWA | **Runs** (`/api/runs`) | `POST /stream` - stateless run + SSE; `POST /wait` - stateless run + block; `GET /{rid}/messages` - paginated messages by run_id `{data, has_more}` (cursor: `after_seq`/`before_seq`); `GET /{rid}/feedback` - list feedback by run_id | | **Thread Shares** (`/api/thread-shares`) | Per-conversation sharing with two modes (`thread_shares.mode`). **`import`**: `POST /` `{thread_id, mode}` - create (or reuse) a revocable share code; `GET /{code}/preview` + `POST /{code}/import` - copy that conversation into the caller's account (fresh `thread_id` via `app/gateway/thread_copy.py` + new `threads_meta` row + `user-data` copy; dedup on `metadata.shared_origin`). **`view`**: a public, read-only static page ? see Public Shares below. Both modes: `GET /` - list my codes; `DELETE /{code}` - revoke. | | **Public Shares** (`/api/public/shares`) | **Unauthenticated** (the `/api/public/` prefix is whitelisted in `auth_middleware._PUBLIC_PATH_PREFIXES`). `GET /{code}` - read-only conversation snapshot (title + serialized messages + artifacts) for a `view`-mode share; messages are read straight from the latest checkpoint. `GET /{code}/files/{path}` - serves a file referenced by that conversation (images, Markdown, text, PDF, ?) so the shared page's links are openable; path resolution is confined to the owner's thread directory. Active content (`text/html`, `application/xhtml+xml`, `image/svg+xml`) is forced to download (`_public_share_disposition`); everything else is served inline. Missing/revoked/non-`view` codes all return 404. | -| **Roundtable Drafts** (`/api/roundtable-drafts`) | **Strictly per-user** persistence for the roundtable-planning page (replaces the old browser `localStorage`). `GET /` - list drafts (lightweight meta); `POST /` - create; `GET/PUT/DELETE /{id}` - full read / partial update / delete (all owner-scoped). A draft = one full session (Step 1 intent + Step 2 roundtable), step snapshots stored as JSON in `PortableLongText`. Recommendation history bound to a draft: `GET /{id}/recommendations` - list newest-first; `POST /{id}/recommendations` - append one run (objective/status/rationale/picks/candidates). taskId 深链会商记录 are **not** here — they live in the separate `/api/roundtable-task-drafts` store (see below) so personal history never mixes with task records. Backed by `deerflow.persistence.roundtable_drafts` (tables `roundtable_drafts`, `roundtable_recommend_history`); store wired in `deps.py` as `app.state.roundtable_draft_store`. Tests: `tests/test_roundtable_task_drafts.py`. | -| **Roundtable Task Drafts** (`/api/roundtable-task-drafts`) | **Task-scoped, unpartitioned** store for taskId-deeplink roundtable 聊天记录 (无界嵌入抽屉). Keyed **only by `task_id`** (one task → many records), **never partitioned by user** — any authenticated caller may read/write/delete a task's records (`created_by` is audit-only). Physically separate from `roundtable_drafts` so the two never mix. `GET /by-task/{task_id}` - all drafts for a task, newest-first (history dropdown), enriched with the latest background-job status per draft via `RoundtableJobRepository.status_map_by_drafts`; `GET /by-task/{task_id}/latest` - newest draft (回显最新一条); `POST /` - create (`task_id` required); `GET/PUT/DELETE /{id}` - full read / partial update / delete (all user-agnostic). Backed by `deerflow.persistence.roundtable_task_drafts` (table `roundtable_task_drafts`, auto-created by `create_all`; migration `20260618_01`); store wired in `deps.py` as `app.state.roundtable_task_draft_store`. Existing `task_id`-bearing `roundtable_drafts` rows are migrated over by `scripts/migrate_roundtable_task_drafts.py`. Tests: `tests/test_roundtable_task_drafts.py`, `tests/test_roundtable_drafts_by_task.py`. | -| **Roundtable Diagnostics** (`/api/roundtable-diagnostics`) | 会商全流程诊断日志 (报错 + 关键事件). One row per event in `roundtable_diagnostics`: `{scope (background = 后台挂起 / foreground = 前台服务端观测 / client = 前端上报), stage (step1_intent/step2_init/step2_leader/step2_seat/step2_recommend/step2_orchestration/step3_summary/step3_report/step3_dashboard/step3_structure/upstream/job), level (info/warning/error), event, message, detail (JSON), job_id/draft_id/task_id/user_id/agent_id/agent_name/cycle, created_at}`. **Reads are admin-only**: `GET /` - filter (scope/stage/level/event/job_id/draft_id/task_id/user_id/q/since/until; `scope` accepts a comma-separated set so 前端「前台调用」= `foreground,client`) + paginate (newest-first), each item enriched with `userName` (user_id → 邮箱 `@`-prefix via `get_local_provider().get_user`, best-effort batched cache); `GET /facets` - distinct values for filter dropdowns; `POST /admin/cleanup` - retention purge. **`POST /report` is NOT admin-gated** — any authenticated user's frontend posts the errors *the user actually hit* (HTTP 502 before any SSE frame / network drop / SSE error frame) so they're collected too; stored as `scope="client"`. Backed by `deerflow.persistence.roundtable_diagnostics` (table `roundtable_diagnostics`, auto-created by `create_all`; migration `20260619_01`); store wired in `deps.py` as `app.state.roundtable_diagnostic_store` + a process-level default store (`register_default_store`) so the harness-side engine/gateway can write across the app/harness boundary. **Writers** (all best-effort — diagnostics never break a run): background path uses `DiagnosticsRecorder` (bound to job ctx) threaded through `RoundtableJobExecutor` → `JobProgressWriter.diag(...)` (engine transitions/step3 failures/empty-delivery) + `InProcessRoundtableGateway` (per-turn timeout/report·summary·dashboard truncation+missing); foreground path uses `app/gateway/roundtable_diag.py::record_foreground_diag` in `multi_agent.py`/`intent.py`/`recommend.py` error frames. **Bug-fix shipped alongside**: `InProcessRoundtableGateway._run_agent_turn` guards each agent run with an **inactivity (idle) timeout** (`_await_run_with_idle_guard`, `ROUNDTABLE_SEAT_IDLE_TIMEOUT_SECONDS` default 1200s = 20min, tuned high for unstable intranet models) — **not** a total-duration cap, so a slow-but-alive run that keeps streaming tokens runs as long as it needs; only a run that emits **zero StreamBridge events for `idle_timeout` seconds straight** (a true freeze: 内网模型连接池挂死 / sqlite 锁在挂载卷上挂死) is cancelled + raises (顶层把 job 标 error,而非永久冻结) + records a `seat_turn_timeout` diagnostic. Activity is observed via `app.state.stream_bridge` (the worker publishes every chunk there). Optional absolute cap `ROUNDTABLE_SEAT_MAX_TOTAL_SECONDS` (default 0 = off). Frontend: admin page `pages/RoundtableDiagnosticsPage.tsx` (route `/page/strategy/admin/roundtable-diagnostics`), opened from the **会商页右上角设置按钮** (admin only — `handleSettingsClick` in `roundtable-planning/pages/RoundtablePlanningPage.tsx`); admin read client `strategy-components/api/roundtable-diagnostics.ts`. **Client error reporter** `roundtable-planning/api/diagnostics-report.ts` (`reportRoundtableError` fire-and-forget + `keepalive`, `setRoundtableErrorContext` for draft/task attribution set by the page, `stageForRoundtableAgent`) is called at every client-observable failure: `multi-agent.ts` (`streamMultiAgent` — covers Step2 leader/seat AND Step3 report/summary/dashboard/structure — reports HTTP error + SSE `{status:"error"}` frame + worker `event:error` frame [模型调用没成功] + fetch-reject [网络不可达] + mid-stream drop [连接重置/超时], deduped via a `reported` flag; `initMultiAgent` HTTP/error-frame/incomplete/mid-stream), `intent.ts` (Step1 HTTP+error-frame), `recommend.ts` (Step2 recommend HTTP+error-frame), `roundtable-jobs.ts` (job api/stream). The admin page's first filter is a **后台挂起 / 前台调用** toggle (frontend = `foreground,client`); the stage dropdown options adapt to it (background → step2+step3+job; frontend → step1+step2+step3) and each row shows the reporting user's name + 👤. Tests: `tests/test_roundtable_diagnostics.py`, `tests/test_roundtable_inprocess_timeout.py`. **Seat skill-recognition (弱模型补偿)**: a seat = one custom agent run whose configured skills (`agent config.yaml → skills`) are injected **only** via the system prompt's `` block (model must `read_file` the SKILL.md itself) — weak models (deepseek-chat 等) ignore it and never use their skills. `app/gateway/roundtable_seat_skills.py::append_seat_skill_directive(agent_id, task, *, chain_enabled=False)` appends an explicit Chinese directive (each configured-and-enabled skill's name + description + SKILL.md container path + "先 read_file 再按其流程取信息/执行") to the **seat's per-turn task (human turn)** — far stronger pull on weak models than the system prompt. Wired into **both** seat paths: foreground `multi_agent._special_run` (only `role in {seat, seat_parallel}`; Step3 singletons report/summary/dashboard/ingest excluded) + background `InProcessRoundtableGateway.run_seat`. No-op (returns task unchanged) when the seat has no configured skills. **2026-06-20: default-off + two opt-in switches (OR semantics)** — was global-on, now injects only when **either** the seat agent's own switch (`AgentConfig.seat_skill_directive`, edited in the agent edit dialog `agent-card.tsx` + `agent-detail-panel.tsx`) **or** the **per-seat** switch on that agent **within the selected business chain** is on. The per-seat chain switch lives **in the chain's `seats` JSON** (`ChainSeatSchema.seat_skill_directive`, no DB column — rides the existing JSON like the per-seat `model` override), edited via a small ✨ icon → dropdown (Switch + description) on each agent card in `BusinessChainEditorPage`. It is threaded **per-seat** to each seat run: foreground `multi_agent` per-call field `seat_skill_directive` (orchestration looks it up per agentId from a `{agentId: bool}` session map) → `_special_run` → `chain_enabled`; background as the job field `seat_skill_directives: dict[str,bool]` (StartJobRequest → StartParams → deps factory → `InProcessRoundtableGateway._seat_skill_directives`) → `run_seat` looks up `agent_id` → `chain_enabled`. `ROUNDTABLE_SEAT_SKILL_DIRECTIVE` is only a **master kill-switch** (default allow; set `0/false` to hard-disable regardless of the switches — never force-enables). Tests: `tests/test_roundtable_seat_skills.py`, `tests/test_agents_backend.py`. **2026-06-20 Step2 提速三件套**:(1) **每席位推理深度覆盖** —— 业务链条 `seats` JSON 里逐席位可配 `seat_mode`(flash/thinking/pro/ultra,覆盖链条级 `seatMode`;编辑器席位卡右侧 ⏱Gauge 图标下拉,null=跟随链条默认)。前台经 `useStep2Orchestration` 三处 dispatch 的 `seatModeToRequestFields(seatModes[id] ?? selectedMode)` 透传;后台经 job `seat_modes: dict[str,str]` → `StartParams` → `InProcessRoundtableGateway._seat_modes`,`run_seat` 用 `_SEAT_MODE_PARAMS`(与前端 `seatModeToRunParams` 逐字对齐)解析覆盖作业级 seat_*(未命中→跟随作业级→policy 默认)。无 DB 列(ride `seats` JSON,但 `ChainSeatSchema.seat_mode` 须显式声明否则 Pydantic 剥掉)。Tests: `tests/test_roundtable_seat_modes.py`。(2) **recommend 批内并行** —— 后端 `engine._run_recommend` 由 for-await 串行改 `asyncio.gather`(对齐前端 2026-06-13 已并行;总控一轮派多席位=判定相互独立→并发;批内用派活前 deliveries 快照、批后统一并入,语义不变)。(3) **研讨前并行『取数』两段式**(业务链条 `gather_first` 列,逐条 opt-in,migration `20260620_02`,editor「研讨前并行取数」开关)—— `gather_first=True` 时 `run_orchestration` 在分发前先 `engine._run_gather_phase`(全席位 `asyncio.gather` 跑 `_GATHER_TASK`「只取数不研讨」),产出 `gathered: dict` 经 `_seat_message(gathered=...)` 作为「已收集资料」块注入 recommend/chain/dag 各研讨轮席位任务(与「前序交付」块分开),把最慢的技能延迟前置并行、给(尤其 chain 串行的)研讨段提速;研讨阶段不限制再调技能。前台对称实现:`useStep2Orchestration.runGatherPhase` 在 `runActiveOrchestration` 分发前跑一次(`gatherPhaseDoneRef` 闸门,resetInitGate 复位),产出存 `gatheredDeliveriesRef` 经 `peerDeliveries`/初始 `priorDeliveries` 注入研讨席位。链条级 `gatherFirst`(camel)↔`gather_first`(snake) 经 `api/chains.ts` 映射;job 经 `StartJobRequest.gather_first`→`StartParams`→`run_orchestration(gather_first=…)`。Tests: `tests/test_roundtable_gather_phase.py`。(4) **削减重复广播**(前端 only,多智能体 foreground 路径)—— `suppress_pre_broadcast`(仅 leader:抑制「总控本轮输入→各席位 thread」预广播 + 「总控给X派活」→兄弟席位的 post 广播,二者一个 flag)此前只在 DAG 派活轮开;现扩到 recommend(`safetyCounter > 1`,即第 2 轮起的续轮/综合)+ chain 逐席位(`i > 0`,第 2 个席位起)+ chain 收口轮(恒 true)。**保留每种模式的首轮预广播**(它为席位打底任务背景),只 suppress 重复的续轮广播(N 次读写巨大席位 thread 的纯浪费 + 弱模型模仿派活);席位的前序交付另经「席位交付广播」`broadcastSeatDeliveryToOthers` 送达,不受影响。后台引擎路径无此广播(`_seat_message` 直接拼上下文),故 #4 仅前端。**2026-06-20 深链接业务链抽取规范**:深链接会商(taskId+goPath,businessCode 如 rwfx→6BF)下,结构化抽取(flow-json)与方案总结报告都按所选业务链**完整层级**强制校正。结构化抽取实为 `roundtable-structure` 智能体(`flow_extract.py` 的 `_system_prompt()=load_agent_soul(STRUCTURE_AGENT_ID)`,抽取→校验两段式 `model.astream`);其 SOUL 全局一份,旧实现把某业务层级写死进 SOUL→所有业务都按「目的→行为体」抽(bug)。修法:**单份领域知识、两角色用法**的 `deerflow.agents.roundtable_orchestrator.extraction_spec`(放 harness 层,app+harness 都可 import;`build_structure_directive`/`build_summary_directive` 按 businessCode 取;rwfx=6BF 写实「任务→目的→行为体→预期行为→驱动因素→叙事,预期行为↔驱动因素 1:1、余 1:N、label 不含步骤/阶段名、优先报告回退席位」,其余业务通用规范=强约束完整性+命名+取数且明令不套固定模板;business_code 空→""零副作用)。注入:(1) 结构化抽取 `flow_extract._hierarchy_hint`+`_validate_messages` 用 `build_structure_directive` 权威覆盖默认层级(前端 `streamFlowExtract` 传 `business_code`,源 `ingestBusinessCode`);(2) 方案总结报告 `build_summary_directive`——前台 `multi_agent._special_run`(role==summary 且 business_code) 追加、后台 `engine.build_summary_prompt(business_code=…)`(StartParams/StartJobRequest 透传)。**仅深链接生效**。种子 `roundtable-structure/SOUL.md` 层级顺序改 spec-driven、连接示例去 rwfx 写死(存量 live SOUL 不被 seed 覆盖,但注入规范权威可纠正)。Tests: `tests/test_roundtable_extraction_spec.py`。 | +| **Roundtable Drafts** (`/api/roundtable-drafts`) | **Strictly per-user** persistence for the roundtable-planning page (replaces the old browser `localStorage`). `GET /` - list drafts (lightweight meta — incl. `chain_title`, the business-chain name used this session, cheaply regex-extracted from the in-memory `step2` blob in `_draft_to_meta`/`_extract_chain_title`; null = no chain / 自由模式; surfaced on the 第一步「分析记录」history card + sidebar list instead of the old「第 X 步」); `POST /` - create; `GET/PUT/DELETE /{id}` - full read / partial update / delete (all owner-scoped). A draft = one full session (Step 1 intent + Step 2 roundtable), step snapshots stored as JSON in `PortableLongText`. Recommendation history bound to a draft: `GET /{id}/recommendations` - list newest-first; `POST /{id}/recommendations` - append one run (objective/status/rationale/picks/candidates). taskId 深链会商记录 are **not** here — they live in the separate `/api/roundtable-task-drafts` store (see below) so personal history never mixes with task records. Backed by `deerflow.persistence.roundtable_drafts` (tables `roundtable_drafts`, `roundtable_recommend_history`); store wired in `deps.py` as `app.state.roundtable_draft_store`. Tests: `tests/test_roundtable_task_drafts.py`. | +| **Roundtable Task Drafts** (`/api/roundtable-task-drafts`) | **Task-scoped, unpartitioned** store for taskId-deeplink roundtable 聊天记录 (无界嵌入抽屉). Keyed **only by `task_id`** (one task → many records), **never partitioned by user** — any authenticated caller may read/write/delete a task's records (`created_by` is audit-only). Physically separate from `roundtable_drafts` so the two never mix. `GET /by-task/{task_id}` - all drafts for a task, newest-first (history dropdown), enriched with the latest background-job status per draft via `RoundtableJobRepository.status_map_by_drafts` (each meta also carries `chain_title`, same `_extract_chain_title` regex as the personal store); `GET /by-task/{task_id}/latest` - newest draft (回显最新一条); `POST /` - create (`task_id` required); `GET/PUT/DELETE /{id}` - full read / partial update / delete (all user-agnostic). Backed by `deerflow.persistence.roundtable_task_drafts` (table `roundtable_task_drafts`, auto-created by `create_all`; migration `20260618_01`); store wired in `deps.py` as `app.state.roundtable_task_draft_store`. Existing `task_id`-bearing `roundtable_drafts` rows are migrated over by `scripts/migrate_roundtable_task_drafts.py`. Tests: `tests/test_roundtable_task_drafts.py`, `tests/test_roundtable_drafts_by_task.py`. | +| **Roundtable Diagnostics** (`/api/roundtable-diagnostics`) | 会商全流程诊断日志 (报错 + 关键事件). One row per event in `roundtable_diagnostics`: `{scope (background = 后台挂起 / foreground = 前台服务端观测 / client = 前端上报), stage (step1_intent/step2_init/step2_leader/step2_seat/step2_recommend/step2_orchestration/step3_summary/step3_report/step3_dashboard/step3_structure/upstream/job), level (warning/error — **`info` 级运行日志不再收集**:`record_diagnostic` 在写库前丢弃 `level=="info"`,只留报错;begin/consensus/gather/job_started 等纯进度事件即使仍调 `.diag(level="info")` 也成 no-op,故诊断页 = 报错日志页), event, message, detail (JSON), job_id/draft_id/task_id/user_id/agent_id/agent_name/cycle, created_at}`. **Reads are admin-only**: `GET /` - filter (scope/stage/level/event/job_id/draft_id/task_id/user_id/q/since/until; `scope` accepts a comma-separated set so 前端「前台调用」= `foreground,client`) + paginate (newest-first), each item enriched with `userName` (user_id → 邮箱 `@`-prefix via `get_local_provider().get_user`, best-effort batched cache); `GET /facets` - distinct values for filter dropdowns; `POST /admin/cleanup` - retention purge. **`POST /report` is NOT admin-gated** — any authenticated user's frontend posts the errors *the user actually hit* (HTTP 502 before any SSE frame / network drop / SSE error frame) so they're collected too; stored as `scope="client"`. Backed by `deerflow.persistence.roundtable_diagnostics` (table `roundtable_diagnostics`, auto-created by `create_all`; migration `20260619_01`); store wired in `deps.py` as `app.state.roundtable_diagnostic_store` + a process-level default store (`register_default_store`) so the harness-side engine/gateway can write across the app/harness boundary. **Writers** (all best-effort — diagnostics never break a run): background path uses `DiagnosticsRecorder` (bound to job ctx) threaded through `RoundtableJobExecutor` → `JobProgressWriter.diag(...)` (engine transitions/step3 failures/empty-delivery) + `InProcessRoundtableGateway` (per-turn timeout/report·summary·dashboard truncation+missing); foreground path uses `app/gateway/roundtable_diag.py::record_foreground_diag` in `multi_agent.py`/`intent.py`/`recommend.py` error frames. **Bug-fix shipped alongside**: `InProcessRoundtableGateway._run_agent_turn` guards each agent run with an **inactivity (idle) timeout** (`_await_run_with_idle_guard`, `ROUNDTABLE_SEAT_IDLE_TIMEOUT_SECONDS` default 1200s = 20min, tuned high for unstable intranet models) — **not** a total-duration cap, so a slow-but-alive run that keeps streaming tokens runs as long as it needs; only a run that emits **zero StreamBridge events for `idle_timeout` seconds straight** (a true freeze: 内网模型连接池挂死 / sqlite 锁在挂载卷上挂死) is cancelled + raises (顶层把 job 标 error,而非永久冻结) + records a `seat_turn_timeout` diagnostic. Activity is observed via `app.state.stream_bridge` (the worker publishes every chunk there). Optional absolute cap `ROUNDTABLE_SEAT_MAX_TOTAL_SECONDS` (default 0 = off). Frontend: admin page `pages/RoundtableDiagnosticsPage.tsx` (route `/page/strategy/admin/roundtable-diagnostics`), opened from the **会商页右上角设置按钮** (admin only — `handleSettingsClick` in `roundtable-planning/pages/RoundtablePlanningPage.tsx`); admin read client `strategy-components/api/roundtable-diagnostics.ts`. **Client error reporter** `roundtable-planning/api/diagnostics-report.ts` (`reportRoundtableError` fire-and-forget + `keepalive`, `setRoundtableErrorContext` for draft/task attribution set by the page, `stageForRoundtableAgent`) is called at every client-observable failure: `multi-agent.ts` (`streamMultiAgent` — covers Step2 leader/seat AND Step3 report/summary/dashboard/structure — reports HTTP error + SSE `{status:"error"}` frame + worker `event:error` frame [模型调用没成功] + fetch-reject [网络不可达] + mid-stream drop [连接重置/超时], deduped via a `reported` flag; `initMultiAgent` HTTP/error-frame/incomplete/mid-stream), `intent.ts` (Step1 HTTP+error-frame), `recommend.ts` (Step2 recommend HTTP+error-frame), `roundtable-jobs.ts` (job api/stream). The admin page's first filter is a **后台挂起 / 前台调用** toggle (frontend = `foreground,client`); the stage dropdown options adapt to it (background → step2+step3+job; frontend → step1+step2+step3) and each row shows the reporting user's name + 👤. Tests: `tests/test_roundtable_diagnostics.py`, `tests/test_roundtable_inprocess_timeout.py`. **Seat skill-recognition (弱模型补偿)**: a seat = one custom agent run whose configured skills (`agent config.yaml → skills`) are injected **only** via the system prompt's `` block (model must `read_file` the SKILL.md itself) — weak models (deepseek-chat 等) ignore it and never use their skills. `app/gateway/roundtable_seat_skills.py::append_seat_skill_directive(agent_id, task, *, chain_enabled=False)` appends an explicit Chinese directive (each configured-and-enabled skill's name + description + SKILL.md container path + "先 read_file 再按其流程取信息/执行") to the **seat's per-turn task (human turn)** — far stronger pull on weak models than the system prompt. Wired into **both** seat paths: foreground `multi_agent._special_run` (only `role in {seat, seat_parallel}`; Step3 singletons report/summary/dashboard/ingest excluded) + background `InProcessRoundtableGateway.run_seat`. No-op (returns task unchanged) when the seat has no configured skills. **2026-06-20: default-off + two opt-in switches (OR semantics)** — was global-on, now injects only when **either** the seat agent's own switch (`AgentConfig.seat_skill_directive`, edited in the agent edit dialog `agent-card.tsx` + `agent-detail-panel.tsx`) **or** the **per-seat** switch on that agent **within the selected business chain** is on. The per-seat chain switch lives **in the chain's `seats` JSON** (`ChainSeatSchema.seat_skill_directive`, no DB column — rides the existing JSON like the per-seat `model` override), edited via a small ✨ icon → dropdown (Switch + description) on each agent card in `BusinessChainEditorPage`. It is threaded **per-seat** to each seat run: foreground `multi_agent` per-call field `seat_skill_directive` (orchestration looks it up per agentId from a `{agentId: bool}` session map) → `_special_run` → `chain_enabled`; background as the job field `seat_skill_directives: dict[str,bool]` (StartJobRequest → StartParams → deps factory → `InProcessRoundtableGateway._seat_skill_directives`) → `run_seat` looks up `agent_id` → `chain_enabled`. `ROUNDTABLE_SEAT_SKILL_DIRECTIVE` is only a **master kill-switch** (default allow; set `0/false` to hard-disable regardless of the switches — never force-enables). Tests: `tests/test_roundtable_seat_skills.py`, `tests/test_agents_backend.py`. **2026-06-20 Step2 提速三件套**:(1) **每席位推理深度覆盖** —— 业务链条 `seats` JSON 里逐席位可配 `seat_mode`(flash/thinking/pro/ultra,覆盖链条级 `seatMode`;编辑器席位卡右侧 ⏱Gauge 图标下拉,null=跟随链条默认)。前台经 `useStep2Orchestration` 三处 dispatch 的 `seatModeToRequestFields(seatModes[id] ?? selectedMode)` 透传;后台经 job `seat_modes: dict[str,str]` → `StartParams` → `InProcessRoundtableGateway._seat_modes`,`run_seat` 用 `_SEAT_MODE_PARAMS`(与前端 `seatModeToRunParams` 逐字对齐)解析覆盖作业级 seat_*(未命中→跟随作业级→policy 默认)。无 DB 列(ride `seats` JSON,但 `ChainSeatSchema.seat_mode` 须显式声明否则 Pydantic 剥掉)。Tests: `tests/test_roundtable_seat_modes.py`。(2) **recommend 批内并行** —— 后端 `engine._run_recommend` 由 for-await 串行改 `asyncio.gather`(对齐前端 2026-06-13 已并行;总控一轮派多席位=判定相互独立→并发;批内用派活前 deliveries 快照、批后统一并入,语义不变)。(3) **研讨前并行『取数』两段式**(业务链条 `gather_first` 列,逐条 opt-in,migration `20260620_02`,editor「研讨前并行取数」开关)—— `gather_first=True` 时 `run_orchestration` 在分发前先 `engine._run_gather_phase`(全席位 `asyncio.gather` 跑 `_GATHER_TASK`「只取数不研讨」),产出 `gathered: dict` 经 `_seat_message(gathered=...)` 作为「已收集资料」块注入 recommend/chain/dag 各研讨轮席位任务(与「前序交付」块分开),把最慢的技能延迟前置并行、给(尤其 chain 串行的)研讨段提速;研讨阶段不限制再调技能。前台对称实现:`useStep2Orchestration.runGatherPhase` 在 `runActiveOrchestration` 分发前跑一次(`gatherPhaseDoneRef` 闸门,resetInitGate 复位),产出存 `gatheredDeliveriesRef` 经 `peerDeliveries`/初始 `priorDeliveries` 注入研讨席位。链条级 `gatherFirst`(camel)↔`gather_first`(snake) 经 `api/chains.ts` 映射;job 经 `StartJobRequest.gather_first`→`StartParams`→`run_orchestration(gather_first=…)`。Tests: `tests/test_roundtable_gather_phase.py`。(4) **削减重复广播**(前端 only,多智能体 foreground 路径)—— `suppress_pre_broadcast`(仅 leader:抑制「总控本轮输入→各席位 thread」预广播 + 「总控给X派活」→兄弟席位的 post 广播,二者一个 flag)此前只在 DAG 派活轮开;现扩到 recommend(`safetyCounter > 1`,即第 2 轮起的续轮/综合)+ chain 逐席位(`i > 0`,第 2 个席位起)+ chain 收口轮(恒 true)。**保留每种模式的首轮预广播**(它为席位打底任务背景),只 suppress 重复的续轮广播(N 次读写巨大席位 thread 的纯浪费 + 弱模型模仿派活);席位的前序交付另经「席位交付广播」`broadcastSeatDeliveryToOthers` 送达,不受影响。后台引擎路径无此广播(`_seat_message` 直接拼上下文),故 #4 仅前端。**2026-06-20 深链接业务链抽取规范**:深链接会商(taskId+goPath,businessCode 如 rwfx→6BF)下,结构化抽取(flow-json)与方案总结报告都按所选业务链**完整层级**强制校正。结构化抽取实为 `roundtable-structure` 智能体(`flow_extract.py` 的 `_system_prompt()=load_agent_soul(STRUCTURE_AGENT_ID)`,抽取→校验两段式 `model.astream`);其 SOUL 全局一份,旧实现把某业务层级写死进 SOUL→所有业务都按「目的→行为体」抽(bug)。修法:**单份领域知识、两角色用法**的 `deerflow.agents.roundtable_orchestrator.extraction_spec`(放 harness 层,app+harness 都可 import;`build_structure_directive`/`build_summary_directive` 按 businessCode 取;rwfx=6BF 写实「任务→目的→行为体→预期行为→驱动因素→叙事,预期行为↔驱动因素 1:1、余 1:N、label 不含步骤/阶段名、优先报告回退席位」,其余业务通用规范=强约束完整性+命名+取数且明令不套固定模板;business_code 空→""零副作用)。注入:(1) 结构化抽取 `flow_extract._hierarchy_hint`+`_validate_messages` 用 `build_structure_directive` 权威覆盖默认层级(前端 `streamFlowExtract` 传 `business_code`,源 `ingestBusinessCode`);(2) 方案总结报告 `build_summary_directive`——前台 `multi_agent._special_run`(role==summary 且 business_code) 追加、后台 `engine.build_summary_prompt(business_code=…)`(StartParams/StartJobRequest 透传)。**仅深链接生效**。种子 `roundtable-structure/SOUL.md` 层级顺序改 spec-driven、连接示例去 rwfx 写死(存量 live SOUL 不被 seed 覆盖,但注入规范权威可纠正)。Tests: `tests/test_roundtable_extraction_spec.py`。 | | **Roundtable Draft Shares** (`/api/roundtable-draft-shares`) | Per-draft sharing for the roundtable-planning page, mirroring Thread Shares but the shared resource is a `roundtable_drafts` row. Two modes (`roundtable_draft_shares.mode`). **`import`**: `POST /` `{draft_id, mode}` - create (or reuse) a revocable code; `GET /{code}/preview` + `POST /{code}/import` - copy that draft (step1/2/3) into the caller's account under a deterministic per-(code,recipient) id (`imp_` ? re-import is a no-op). **`view`**: a public, read-only result page ? see Public Roundtable Shares. Both: `GET /` - list my codes; `DELETE /{code}` - revoke. Backed by `deerflow.persistence.roundtable_draft_shares` (table `roundtable_draft_shares`, migration `20260606_01`); store wired in `deps.py` as `app.state.roundtable_draft_share_store`. Tests: `tests/test_roundtable_draft_shares.py`. | | **Public Roundtable Shares** (`/api/public/roundtable-shares`) | **Unauthenticated** (`/api/public/` whitelisted). `GET /{code}` - read-only snapshot for a `view`-mode draft share: returns the Step 3??????report HTML (`step3.html`, a self-contained document) + title/summary/`has_report`. The frontend renders it in a sandboxed `iframe` (no `allow-scripts`) at `#/roundtable-share/`. Missing/revoked/non-`view` codes all return 404. | | **Scheduled Tasks** (`/api/scheduled-tasks`) | Per-user cron-like agent tasks. `GET/POST /` - list/create owned tasks; `PATCH/DELETE /{id}` - update/delete (owner); `POST /{id}/{pause,resume,trigger}`; `GET /{id}/runs`, `GET /runs/{rid}`, run-file preview/edit/download; `GET /runs/{rid}/messages` - agent messages for the run's underlying agent run (used to inspect a stuck/running run; authorized via `_load_run`); `GET /runs/{rid}/html` - generated HTML document for an `html_page` run; `DELETE /runs/{rid}` - owner-scoped, with an admin fallback so admins can delete any user's run; publish/subscribe under `/{id}/{publish,unpublish,subscribe}` and `GET /published`. **Admin (cross-user):** `GET /admin/all` - every task; `PATCH/DELETE /admin/{id}` - edit/delete any task; `POST /admin/{id}/trigger`; `GET /admin/{id}/runs`. Admin routes require `system_role == "admin"`. | diff --git a/offline-backend-20260512/backend/app/gateway/routers/roundtable_drafts.py b/offline-backend-20260512/backend/app/gateway/routers/roundtable_drafts.py index 9ef5ccd5..75574acf 100644 --- a/offline-backend-20260512/backend/app/gateway/routers/roundtable_drafts.py +++ b/offline-backend-20260512/backend/app/gateway/routers/roundtable_drafts.py @@ -47,6 +47,8 @@ class DraftMetaResponse(BaseModel): task_id: str | None = None title: str = "" furthest_step: int = 1 + # 本次会商所用业务链名(历史卡片展示);无链条(自由/recommend 模式)时为 None。 + chain_title: str | None = None created_at: datetime | str | None = None updated_at: datetime | str | None = None # 最近一次后台作业的状态(Phase 6 历史下拉「进行中」loading 用);无作业时为 None。 diff --git a/offline-backend-20260512/backend/app/gateway/routers/roundtable_task_drafts.py b/offline-backend-20260512/backend/app/gateway/routers/roundtable_task_drafts.py index b0696cdd..1bea7a18 100644 --- a/offline-backend-20260512/backend/app/gateway/routers/roundtable_task_drafts.py +++ b/offline-backend-20260512/backend/app/gateway/routers/roundtable_task_drafts.py @@ -42,6 +42,8 @@ class TaskDraftMetaResponse(BaseModel): task_id: str title: str = "" furthest_step: int = 1 + # 本次会商所用业务链名(历史卡片展示);无链条(自由/recommend 模式)时为 None。 + chain_title: str | None = None created_at: datetime | str | None = None updated_at: datetime | str | None = None # Latest background-job status for this draft (history-dropdown loading badge). diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/persistence/roundtable_diagnostics/__init__.py b/offline-backend-20260512/backend/packages/harness/deerflow/persistence/roundtable_diagnostics/__init__.py index 2afba98c..99204114 100644 --- a/offline-backend-20260512/backend/packages/harness/deerflow/persistence/roundtable_diagnostics/__init__.py +++ b/offline-backend-20260512/backend/packages/harness/deerflow/persistence/roundtable_diagnostics/__init__.py @@ -41,10 +41,17 @@ def get_default_store() -> RoundtableDiagnosticRepository | None: async def record_diagnostic(store: RoundtableDiagnosticRepository | None = None, **fields: Any) -> None: - """Best-effort 写一条诊断事件。``store`` 缺省用进程级默认仓储。任何异常静默吞掉。""" + """Best-effort 写一条诊断事件。``store`` 缺省用进程级默认仓储。任何异常静默吞掉。 + + 会商诊断**只收集报错日志**:``info`` 级是纯运行进度(begin/consensus/gather/job_started + 等「运行日志」),对排障没价值、还会淹没真正的报错,直接不落库;只保留 ``warning`` / + ``error``(席位失败、空交付、超时、模型容错、产物缺失、崩溃等)。 + """ repo = store or _default_store if repo is None: return + if str(fields.get("level") or "").strip().lower() == "info": + return try: await repo.record(**fields) except Exception: # noqa: BLE001 — 诊断日志写失败绝不打断会商 diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/persistence/roundtable_drafts/sql.py b/offline-backend-20260512/backend/packages/harness/deerflow/persistence/roundtable_drafts/sql.py index aeb74b88..2ca3ddcb 100644 --- a/offline-backend-20260512/backend/packages/harness/deerflow/persistence/roundtable_drafts/sql.py +++ b/offline-backend-20260512/backend/packages/harness/deerflow/persistence/roundtable_drafts/sql.py @@ -11,6 +11,7 @@ All write/read paths are scoped by ``user_id`` for ownership isolation. from __future__ import annotations import json +import re from datetime import UTC, datetime from typing import Any @@ -24,6 +25,29 @@ from deerflow.persistence.roundtable_drafts.model import ( _UNSET = object() +# 业务链名抽取:在已加载进内存的 step2 JSON 串里直接找 `"chain":{...,"title":"X"}`, +# 不整体 json.loads(step2 是 LONGTEXT 大块),只做一次廉价正则扫描。`"chain":null` +# (recommend/自由模式)不匹配 → 返回 None。`[^{}]*?` 限定在 chain 对象内({id,title} +# 无嵌套花括号)。 +_CHAIN_TITLE_RE = re.compile(r'"chain"\s*:\s*\{[^{}]*?"title"\s*:\s*"((?:\\.|[^"\\])*)"') + + +def _extract_chain_title(step2_raw: Any) -> str | None: + """从 step2 原始 JSON 串里抽取所用业务链名(无链条 → None,前端显示「自由模式」)。""" + if not isinstance(step2_raw, str) or not step2_raw: + return None + m = _CHAIN_TITLE_RE.search(step2_raw) + if not m: + return None + title = m.group(1) + try: + # 还原 JSON 字符串转义(\" \\ \n \uXXXX 等)。 + title = json.loads(f'"{title}"') + except Exception: + pass + title = title.strip() + return title or None + def _loads(value: Any) -> Any: if not isinstance(value, str): @@ -56,6 +80,8 @@ class RoundtableDraftRepository: "task_id": row.task_id, "title": row.title, "furthest_step": row.furthest_step, + # 所用业务链名(历史卡片展示用);无链条 → None → 前端显示「自由模式」。 + "chain_title": _extract_chain_title(row.step2), "created_at": row.created_at.isoformat() if isinstance(row.created_at, datetime) else row.created_at, "updated_at": row.updated_at.isoformat() if isinstance(row.updated_at, datetime) else row.updated_at, } diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/persistence/roundtable_task_drafts/sql.py b/offline-backend-20260512/backend/packages/harness/deerflow/persistence/roundtable_task_drafts/sql.py index c415f62d..fad0ef0a 100644 --- a/offline-backend-20260512/backend/packages/harness/deerflow/persistence/roundtable_task_drafts/sql.py +++ b/offline-backend-20260512/backend/packages/harness/deerflow/persistence/roundtable_task_drafts/sql.py @@ -15,6 +15,7 @@ in ``PortableLongText`` and round-tripped with ``json.dumps`` / ``json.loads``. from __future__ import annotations import json +import re from datetime import UTC, datetime from typing import Any @@ -25,6 +26,27 @@ from deerflow.persistence.roundtable_task_drafts.model import RoundtableTaskDraf _UNSET = object() +# 业务链名抽取:在已加载进内存的 step2 JSON 串里直接找 `"chain":{...,"title":"X"}`, +# 不整体 json.loads(step2 是 LONGTEXT 大块)。`"chain":null`(自由模式)不匹配 → None。 +# 与 roundtable_drafts/sql.py 保持一致。 +_CHAIN_TITLE_RE = re.compile(r'"chain"\s*:\s*\{[^{}]*?"title"\s*:\s*"((?:\\.|[^"\\])*)"') + + +def _extract_chain_title(step2_raw: Any) -> str | None: + """从 step2 原始 JSON 串里抽取所用业务链名(无链条 → None,前端显示「自由模式」)。""" + if not isinstance(step2_raw, str) or not step2_raw: + return None + m = _CHAIN_TITLE_RE.search(step2_raw) + if not m: + return None + title = m.group(1) + try: + title = json.loads(f'"{title}"') + except Exception: + pass + title = title.strip() + return title or None + def _loads(value: Any) -> Any: if not isinstance(value, str): @@ -55,6 +77,8 @@ class RoundtableTaskDraftRepository: "task_id": row.task_id, "title": row.title, "furthest_step": row.furthest_step, + # 所用业务链名(历史卡片展示用);无链条 → None → 前端显示「自由模式」。 + "chain_title": _extract_chain_title(row.step2), "created_at": row.created_at.isoformat() if isinstance(row.created_at, datetime) else row.created_at, "updated_at": row.updated_at.isoformat() if isinstance(row.updated_at, datetime) else row.updated_at, } From 23743ee1930a329a5e46d3338fe57650ca32f113 Mon Sep 17 00:00:00 2001 From: fengxiangdebaozi <10845145+fengxiangdebaozi@user.noreply.gitee.com> Date: Mon, 22 Jun 2026 15:52:35 +0800 Subject: [PATCH 5/7] =?UTF-8?q?chore:=20=E7=B2=BE=E7=AE=80=20AI=20?= =?UTF-8?q?=E5=86=99=E4=BD=9C=E7=A4=BA=E4=BE=8B=E6=96=87=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ai-writing 表单/检索/欢迎页的占位与示例改为通用文案。 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/open-canvas/components/ai-writing/AIWritingForm.tsx | 2 +- .../src/open-canvas/components/ai-writing/MaterialsToolbar.tsx | 2 +- .../src/open-canvas/components/ai-writing/WritingWelcome.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend-web/src/open-canvas/components/ai-writing/AIWritingForm.tsx b/frontend-web/src/open-canvas/components/ai-writing/AIWritingForm.tsx index aa6a7d60..c89efb9f 100644 --- a/frontend-web/src/open-canvas/components/ai-writing/AIWritingForm.tsx +++ b/frontend-web/src/open-canvas/components/ai-writing/AIWritingForm.tsx @@ -192,7 +192,7 @@ export function AIWritingForm({