From 60626c384acc2b97278c17f349de488edd392e76 Mon Sep 17 00:00:00 2001 From: fengxiangdebaozi <10845145+fengxiangdebaozi@user.noreply.gitee.com> Date: Fri, 19 Jun 2026 20:51:05 +0800 Subject: [PATCH] =?UTF-8?q?fix(roundtable):=20=E4=BC=9A=E5=95=86=20Step2/S?= =?UTF-8?q?tep3=20=E6=B2=99=E7=AE=B1=E4=BA=A7=E7=89=A9=E6=8C=89=E5=AF=B9?= =?UTF-8?q?=E8=AF=9D=E5=88=9B=E5=BB=BA=E8=80=85=E8=A7=A3=E6=9E=90=EF=BC=8C?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=8E=86=E5=8F=B2/=E8=B7=A8=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E6=9F=A5=E7=9C=8B=E4=B8=8D=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 圆桌会商系统线程(metadata.thread_type==roundtable)的 Step2 席位/总控 + Step3 报告/总结/大屏/结构化 沙箱产物(md/流程图/报告)落在创建者目录下, 之前按当前登录解析路径 + owner_check 校验 → 历史草稿重开或 taskId 会商被 其他账号打开时 403/404 查看不了。沿用任务深链对话方案(23e6685)扩展到圆桌: - thread_paths._is_creator_shared_thread:taskId 或 thread_type==roundtable 都解析到对话创建者(threads_meta.user_id),覆盖所有线程路径关口 - ThreadMetaRepository.check_access:圆桌系统线程同样跳过 owner 过滤 (artifacts 路由 owner_check 据此放行其他账号/历史查看) 线程 id 仅存在于草稿中,无关账号无法发现,与 taskId 共享模型一致。 Co-Authored-By: Claude Opus 4.8 (1M context) --- offline-backend-20260512/backend/CLAUDE.md | 2 +- .../deerflow/persistence/thread_meta/sql.py | 9 +++++-- .../harness/deerflow/runtime/thread_paths.py | 25 ++++++++++++++++++- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/offline-backend-20260512/backend/CLAUDE.md b/offline-backend-20260512/backend/CLAUDE.md index 17eb0b9b..8fcc4e42 100644 --- a/offline-backend-20260512/backend/CLAUDE.md +++ b/offline-backend-20260512/backend/CLAUDE.md @@ -459,7 +459,7 @@ Operators upgrading an existing DB run the whole file (`USE ;` then execute) - Physical: `backend/.deer-flow/users/{user_id}/threads/{thread_id}/user-data/...`, `deer-flow/skills/` - Translation: `replace_virtual_path()` / `replace_virtual_paths_in_command()` - Detection: `is_local_sandbox()` checks `sandbox_id == "local"` -- **`{user_id}` for path resolution = `resolve_path_user_id(thread_id)`, NOT the current login.** Task-deeplink conversations (rwfx/3qfx/xdfx, `threads_meta.metadata.taskId`) are shared across all users (see the `taskId` escape branches in `thread_meta/sql.py`), but generated files/uploads live under a *per-user* dir. Resolving by current login meant account B opening a shared task conversation 404'd on the md/artifacts account A generated ("切换账号后生成的文件找不到"). `deerflow/runtime/thread_paths.py` (`resolve_path_user_id` sync / `aresolve_path_user_id` async) resolves a task-shared thread to its **creator** (`threads_meta.user_id`), so every account reads/writes the same physical directory; personal threads resolve to the current login unchanged (no behavior change). Wired at every thread-path seam: write source `ThreadDataMiddleware.before_agent` (drives all sandbox writes via `thread_data` paths), `present_file_tool`, `uploads/manager.get_uploads_dir` + `uploads_middleware` + uploads router, artifact serving (`path_utils.{a,}resolve_thread_virtual_path` → artifacts/skills routers), embedded `client.get_artifact`. Positive (taskId→creator) results are cached process-wide (immutable mapping); any failure / memory-mode store / missing row falls back to the current login. **Caveat**: files a *non-creator* wrote into a shared thread before this fix stay orphaned under that writer's dir (no data loss); all new writes consolidate under the creator. Tests: `tests/test_thread_paths_owner.py`. +- **`{user_id}` for path resolution = `resolve_path_user_id(thread_id)`, NOT the current login.** Task-deeplink conversations (rwfx/3qfx/xdfx, `threads_meta.metadata.taskId`) are shared across all users (see the `taskId` escape branches in `thread_meta/sql.py`), but generated files/uploads live under a *per-user* dir. Resolving by current login meant account B opening a shared task conversation 404'd on the md/artifacts account A generated ("切换账号后生成的文件找不到"). `deerflow/runtime/thread_paths.py` (`resolve_path_user_id` sync / `aresolve_path_user_id` async) resolves a task-shared thread to its **creator** (`threads_meta.user_id`), so every account reads/writes the same physical directory; personal threads resolve to the current login unchanged (no behavior change). Wired at every thread-path seam: write source `ThreadDataMiddleware.before_agent` (drives all sandbox writes via `thread_data` paths), `present_file_tool`, `uploads/manager.get_uploads_dir` + `uploads_middleware` + uploads router, artifact serving (`path_utils.{a,}resolve_thread_virtual_path` → artifacts/skills routers), embedded `client.get_artifact`. Positive (taskId→creator) results are cached process-wide (immutable mapping); any failure / memory-mode store / missing row falls back to the current login. **Caveat**: files a *non-creator* wrote into a shared thread before this fix stay orphaned under that writer's dir (no data loss); all new writes consolidate under the creator. **Extended to 圆桌会商 system threads** (`metadata.thread_type == "roundtable"` — Step 2 seats/coordinator + Step 3 report/summary/dashboard/structure, created by `multi_agent.py` with `_ROUNDTABLE_THREAD_METADATA`): their Step2/Step3 sandbox artifacts (md/流程图/报告) must stay viewable when a历史草稿 is reopened or a taskId会商 draft is opened by another account, so `thread_paths._is_creator_shared_thread(metadata)` treats both `taskId` **and** `thread_type=="roundtable"` as creator-shared, and `ThreadMetaRepository.check_access` (the artifacts router's `owner_check`) bypasses the owner filter for both — roundtable thread ids are only surfaced inside the owner's draft, so this does not let unrelated accounts *discover* others' 会商 files. Tests: `tests/test_thread_paths_owner.py`, `tests/test_thread_paths_roundtable.py`. **Sandbox Tools** (in `packages/harness/deerflow/sandbox/tools.py`): - `bash` - Execute commands with path translation and error handling diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/persistence/thread_meta/sql.py b/offline-backend-20260512/backend/packages/harness/deerflow/persistence/thread_meta/sql.py index 780a3182..ae749047 100644 --- a/offline-backend-20260512/backend/packages/harness/deerflow/persistence/thread_meta/sql.py +++ b/offline-backend-20260512/backend/packages/harness/deerflow/persistence/thread_meta/sql.py @@ -97,8 +97,13 @@ class ThreadMetaRepository(ThreadMetaStore): row = await session.get(ThreadMetaRow, thread_id) if row is None: return not require_existing - # 任务深链对话(metadata.taskId)全局共享、不区分用户:任何登录用户都可读写/续聊。 - if (row.metadata_json or {}).get("taskId"): + md = row.metadata_json or {} + # 全局共享、不区分用户(任何登录用户都可读写/续聊): + # ① 任务深链对话(metadata.taskId); + # ② 圆桌会商系统线程(metadata.thread_type=="roundtable")——其 Step2/Step3 + # 沙箱产物(md/流程图/报告)需让历史草稿或其他账号打开同一会商时也能查看, + # 与任务深链对话一致(线程 id 仅存在于草稿中,不会被无关账号发现)。 + if md.get("taskId") or md.get("thread_type") == "roundtable": return True if row.user_id is None: return True diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/runtime/thread_paths.py b/offline-backend-20260512/backend/packages/harness/deerflow/runtime/thread_paths.py index fdbae69a..28358b5c 100644 --- a/offline-backend-20260512/backend/packages/harness/deerflow/runtime/thread_paths.py +++ b/offline-backend-20260512/backend/packages/harness/deerflow/runtime/thread_paths.py @@ -78,11 +78,34 @@ async def _fetch_task_owner(thread_id: str) -> str | None: return None metadata = row.get("metadata") or {} owner = row.get("user_id") - if metadata.get("taskId") and owner: + if owner and _is_creator_shared_thread(metadata): return str(owner) return None +def _is_creator_shared_thread(metadata: dict) -> bool: + """True iff a thread's files should resolve to its **creator** (shared bucket). + + Two kinds of threads are shared to one stable directory rather than the + current login: + + - **Task-deeplink conversations** (``metadata.taskId``) — rwfx / 3qfx / xdfx, + explicitly shared across all users; + - **Roundtable 会商 system threads** (``metadata.thread_type == "roundtable"``) + — the Step 2 seats / coordinator and Step 3 report / summary / dashboard / + structure threads. Their sandbox artifacts (md / 流程图 / 报告) live under the + creator's directory, but a历史草稿 reopened later — or a taskId会商 draft opened + by **another account** — must still be able to view them. Resolving to the + creator keeps the *file* layer consistent with the *draft-sharing* layer. + + Roundtable thread ids are random UUIDs surfaced only inside the owner's draft + (personal drafts are owner-scoped; taskId drafts are already shared), so this + does not let an unrelated account *discover* another user's 会商 files — it only + lets a caller that already holds the thread id read the creator's bucket. + """ + return bool(metadata.get("taskId")) or metadata.get("thread_type") == "roundtable" + + def _run_coro_blocking(coro): """Run an async coroutine to completion from sync code.