fix(roundtable): 会商 Step2/Step3 沙箱产物按对话创建者解析,修复历史/跨用户查看不了
圆桌会商系统线程(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) <noreply@anthropic.com>
This commit is contained in:
1 parent
551c748aa8
commit
60626c384a
3 files changed
+32
-4
No files matched your search
@@ -459,7 +459,7 @@ Operators upgrading an existing DB run the whole file (`USE <db>;` 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
|
||||
|
||||
+7
-2
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
Reference in new issue
Block a user