feat(scripts): 跨 owner 懒加载目录树 + 跨用户可见 workspace/public
修两个后端接口问题:
1) /api/v1/workspace-directories 返回为空,目录树结构消失
2) 同 workspace 内脚本/数据互相可见但默认排除 private
后端改动
--------
* list_scripts / list_resources / list_workspace_directories 新增
owner_user_id 可选 query 参数;缺省 = 当前请求者本人(scope 到
workspace/{me}/...),传值时 scope 到该 owner 的子树。前端根加载
默认只见自己一级,其他成员以折叠分组呈现。
* visibility 过滤统一:非 admin 请求者只返回 owner==me 或
visibility ∈ {workspace, public};admin 跳过。owner=me 含自己
的 private,owner=other 只剩其 workspace/public,排除他人 private。
* create_workspace_directory 两个分支 visibility 默认 'public'
(非 private),使跨 owner 目录树可见;响应新增 owner_user_id 字段。
* platform.list_members 鉴权从 system_admin_context 放宽为
系统管理员或该 workspace 活跃成员(让普通用户也能渲染同
workspace 成员名册,用于跨 owner 分组)。
* main.py 注册 platform 模块(随 list_members 改动补齐导入)。
* .env.example 同步 common/config.py 26 个字段。
前端改动
--------
* ScriptExplorer.memberScriptGroups 改由 members 列表播种分组,
display_name 取 members.display_name;inferredDirectories 现在按
owner_user_id 标记,统一跨 owner 目录渲染。删除脚本目录页头与
树分组标题的工作副本数量角标。
* WorkspaceTree 新增 ownerUserId 透传到 store.toggleExpanded;
仅"我"的分组 mount 时 auto-expand,他人分组默认折叠,展开才
调 loadOwnerGroup / owner-scoped loadScripts / loadChildren。
* scriptWorkspaceStore 引入 namespaced cache key
(ownerCacheKey = `${ownerUserId ?? me}:${path}`),loadedScriptPaths
/ loadedChildPaths / loadedOwnerGroups 全部按 owner 隔离;
toggleExpanded 用 loadPath === undefined 区分 group 头与真实
目录,修"他人子目录点击不触发接口"的 loadPath 前缀误判 bug。
* api.ts / AuthContext 透传 ownerUserId 给 listScripts /
listResources / listWorkspaceDirectories。
文档
----
* API.md: §3.2 创建目录 visibility 默认 public + 响应加 owner_user_id;
§3.3.1 GET directories 加 owner_user_id 参数 + 响应字段;
§3.4 GET scripts 改写为 owner 作用域 + visibility 过滤语义;
§五.1 GET data-resources 新增,同一套统一语义;
§7 intro 例外 — GET members 对系统管理员或 workspace 活跃成员开放。
* DEVELOP.md: Code layout 重写以反映 backend api/services/clients/
schemas 拆分 + schedule domain/scheduling/application/execution/
infrastructure 拆分 + common 子包(auth/storage/backends);
Configuration 系统补全 26 个 settings 字段;新增
"Owner-scoping + visibility (cross-owner browsing)" 小节;
Per-service dev 注释用 uv run 的源布局要求;Add a new DAG endpoint /
storage bucket 路径改为 backend/src/backend/api/* 与 services/*。
测试
----
* test_list_scripts_parent_path.py /
test_resources.py 补充 owner_user_id 参数化直接调用 + LIKE
前缀断言(workspace/{owner}/... 前缀)。
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -99,7 +99,12 @@
|
||||
|
||||
### 3.2 `POST /api/v1/workspace-directories`
|
||||
|
||||
创建一个**目录**。后端会落一行 `StorageObjects(object_type='directory', storage_backend='rustfs', storage_uri='inline://directory/{path}', visibility='private')`,因此空目录也能在 §3.1 树里出现并保留下来。
|
||||
创建一个**目录**。后端会落一行 `StorageObjects(object_type='directory', storage_backend='rustfs', storage_uri='inline://directory/{path}', visibility='public')`,因此空目录也能在 §3.1 树里出现并保留下来。
|
||||
|
||||
> 目录行默认 `visibility='public'`(非 `private`)。目录是结构性导航行,
|
||||
> 默认 public 使同一 workspace 内其他成员可以浏览彼此的目录结构(目录树
|
||||
> 跨 owner 可见);文件级私密仍由 §3.4 / §五 的 visibility 过滤兜底
|
||||
> —— 其他 owner 的 `private` 脚本 / 数据资源不会返回。
|
||||
|
||||
- **请求体**:
|
||||
```json
|
||||
@@ -123,7 +128,8 @@
|
||||
"storage_object_id": "01HXY...",
|
||||
"path": "scripts/etl",
|
||||
"name": "etl",
|
||||
"parent_path": "scripts"
|
||||
"parent_path": "scripts",
|
||||
"owner_user_id": "01HXX..."
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -155,10 +161,12 @@
|
||||
|
||||
- **行为**: `parent_path` 为空字符串或缺省 → 用户根目录;非空 → 该父目录的直接子目录(workspace-relative)。仅返回 `object_status='available' AND is_deleted=0` 的 `StorageObjects` 行。
|
||||
- **鉴权**: workspace 成员
|
||||
- **owner 作用域**: `owner_user_id` 缺省时 scope 为**当前请求者**本人根目录(`scoped_prefix = workspace/{me}`);传 `owner_user_id` 时 scope 为该 owner 的根目录(`scoped_prefix = workspace/{owner_user_id}`),用于跨 owner 浏览目录树(见 §3.4 visibility 模型)。该接口本身不施加 visibility 过滤——目录行默认 `visibility='public'`(见 §3.2),跨 owner 均可见。
|
||||
- **查询参数**:
|
||||
| 名 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `parent_path` | string | 否 | 父目录相对路径,空字符串或缺省表示用户根目录 |
|
||||
| `owner_user_id` | string | 否 | 目标 owner 的 user_id;缺省=请求者本人。指定后 scope 到 `workspace/{owner_user_id}/{parent_path}` |
|
||||
- **谓词(SQL 等价)**: `relative_path LIKE '<prefix>/%' AND relative_path NOT LIKE '<prefix>/%/%'`,其中 `prefix = scoped_prefix/{parent_path}`,索引走 `idx_storage_workspace_relative_path(workspace_id, relative_path(255))`。
|
||||
- **响应**:
|
||||
```json
|
||||
@@ -166,9 +174,9 @@
|
||||
"request_id": "...",
|
||||
"data": {
|
||||
"directories": [
|
||||
{"path": "scripts", "name": "scripts", "parent_path": "", "has_children": true},
|
||||
{"path": "scripts/etl", "name": "etl", "parent_path": "scripts", "has_children": false},
|
||||
{"path": "scripts/etl/daily", "name": "daily", "parent_path": "scripts/etl", "has_children": false}
|
||||
{"path": "scripts", "name": "scripts", "parent_path": "", "owner_user_id": "01HXX...", "has_children": true},
|
||||
{"path": "scripts/etl", "name": "etl", "parent_path": "scripts", "owner_user_id": "01HXX...", "has_children": false},
|
||||
{"path": "scripts/etl/daily", "name": "daily", "parent_path": "scripts/etl", "owner_user_id": "01HXX...", "has_children": false}
|
||||
]
|
||||
},
|
||||
"meta": {"directory_count": 3}
|
||||
@@ -180,15 +188,28 @@
|
||||
| `path` | string | workspace 内相对路径 |
|
||||
| `name` | string | `path` 的最后一段 |
|
||||
| `parent_path` | string | 父目录相对路径,根目录用空串 |
|
||||
| `owner_user_id` | string | 该目录行所属 owner 的 user_id(`owner_user_id` 参数缺省时=请求者本人) |
|
||||
| `has_children` | bool | 该目录下是否还有直接子目录(后端额外 `EXISTS` 查询,可为空目录为 `false`) |
|
||||
- **空结果**: 不返回 404,空目录列表即 `directories: []`。
|
||||
- **错误**: 401(未登录)/ 403(非 workspace 成员)同其他接口。
|
||||
|
||||
### 3.4 `GET /api/v1/scripts`
|
||||
|
||||
列出当前 workspace 内**全部 active 脚本**。不受 is_locked 影响(读路径不锁)。
|
||||
列出脚本,按 **owner 作用域 + visibility 过滤**返回。不受 is_locked 影响(读路径不锁)。
|
||||
|
||||
- **响应**: `data` 为 `ScriptPayload` 数组(见 §3.10)。
|
||||
- **owner 作用域**: `owner_user_id` 缺省=当前请求者本人,scope 到 `workspace/{me}/...`;传 `owner_user_id` 时 scope 到 `workspace/{owner_user_id}/...`,用于跨 owner 浏览他人脚本。
|
||||
- **visibility 过滤(统一)**: 非 admin 请求者只返回 `owner_user_id == me OR visibility IN (workspace, public)`;admin 请求者跳过过滤返回全部。
|
||||
- **owner=me(缺省)**: scope 是我的子树,行都是我的 → `owner==me` 恒成立 → **含我的 private 脚本** ✓
|
||||
- **owner=other**: scope 是他人的子树,`owner==me` 不成立 → 只剩其 `workspace/public` 脚本(排除他人的 `private`) ✓
|
||||
- 即"本人可见自己全部;他人只见其 workspace/public",私密仅在 owner==me 时可见。
|
||||
- **非递归**: 仅返回 `parent_path` 下的**直接子级**脚本(懒加载用);子目录脚本需带 `parent_path` 再次请求。
|
||||
- **查询参数**:
|
||||
| 名 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `parent_path` | string | 否 | 父目录相对路径,空串/缺省=该 owner 根目录下的一级脚本 |
|
||||
| `owner_user_id` | string | 否 | 目标 owner;缺省=请求者本人 |
|
||||
| `keyword` | string | 否 | 名称模糊匹配 |
|
||||
- **响应**: `data` 为 `ScriptPayload` 数组(见 §3.10),每条带 `owner_user_id`。
|
||||
|
||||
### 3.5 `GET /api/v1/scripts/{script_id}`
|
||||
|
||||
@@ -485,7 +506,7 @@ queued ──→ running ──┬─→ succeeded
|
||||
| `POST` | `/api/v1/data-resources/uploads` | 创建上传会话,返回 `upload_id` + `upload_path` |
|
||||
| `PUT` | `/api/v1/data-resources/uploads/{upload_id}` | 上传字节(请求体即文件内容) |
|
||||
| `POST` | `/api/v1/data-resources/uploads/{upload_id}/bind` | 绑定已完成上传为数据资源 |
|
||||
| `GET` | `/api/v1/data-resources` | 列表(workspace 范围) |
|
||||
| `GET` | `/api/v1/data-resources` | 列表(owner 作用域 + visibility 过滤) |
|
||||
| `GET` | `/api/v1/data-resources/{id}` | 详情 |
|
||||
| `POST` | `/api/v1/data-resources/{id}/download-url` | 生成 presigned GET URL |
|
||||
| `DELETE` | `/api/v1/data-resources/{id}` | 软删 |
|
||||
@@ -521,6 +542,22 @@ queued ──→ running ──┬─→ succeeded
|
||||
`content_base64` 字段(JSON 体里走),内部走同一条 `AsyncStorageBackend.put`
|
||||
路径,前端无需分两步。
|
||||
|
||||
### 五.1 `GET /api/v1/data-resources`
|
||||
|
||||
列出数据资源,按 **owner 作用域 + visibility 过滤**返回(与 §3.4 `GET /scripts` 同一套统一语义)。
|
||||
|
||||
- **owner 作用域**: `owner_user_id` 缺省=当前请求者本人,scope 到 `object_key` 前缀 `{workspace_id}/{me}/...`;传 `owner_user_id` 时 scope 到 `{workspace_id}/{owner_user_id}/...`。
|
||||
- **visibility 过滤(统一)**: 非 admin 请求者只返回 `owner_user_id == me OR visibility IN (workspace, public)`;admin 请求者跳过过滤返回全部。语义同 §3.4——owner=me 含自己的 private;owner=other 只见其 workspace/public。
|
||||
- **非递归**: 仅返回 `parent_path` 下的**直接子级**资源(懒加载用)。
|
||||
- **查询参数**:
|
||||
| 名 | 类型 | 必填 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `parent_path` | string | 否 | 父目录相对路径,空串/缺省=该 owner 根目录下的一级资源 |
|
||||
| `owner_user_id` | string | 否 | 目标 owner;缺省=请求者本人 |
|
||||
| `visibility` | string | 否 | `workspace` \| `public` \| `private`,二次过滤 |
|
||||
| `keyword` | string | 否 | 名称模糊匹配 |
|
||||
- **响应**: `data` 为 `ResourcePayload` 数组,每条带 `owner_user_id`。
|
||||
|
||||
---
|
||||
|
||||
## 六、管理后台
|
||||
@@ -581,10 +618,16 @@ Base 前缀 `/api/v1/admin`。
|
||||
## 七、系统管理 (`/api/v1/platform/...`)
|
||||
|
||||
平台级(跨 workspace)管理接口,用于管理员工、workspace 实体与 workspace 成员。
|
||||
所有端点要求调用者是**系统管理员**——其 `users.platform_role_id` 指向
|
||||
除特别注明外,所有端点要求调用者是**系统管理员**——其 `users.platform_role_id` 指向
|
||||
`role_code='admin'` 的角色行,且 `users.status == 'active'`。系统管理员判定
|
||||
通过 `GET /api/v1/auth/me` 响应中的 `data.user.is_system_admin` 字段(详见 §一)。
|
||||
|
||||
> **例外 — `GET /workspaces/{id}/members`**:该端点对**系统管理员(任意
|
||||
> workspace)**与**该 workspace 的活跃成员**(`workspace_members.is_deleted=0`
|
||||
> 且 `member_status='active'`)均开放。这是为了让普通(非 admin)用户能在
|
||||
> 脚本目录树里渲染同 workspace 其他成员的折叠分组(跨 owner 浏览,见 §3.4)。
|
||||
> 其余 members 写端点(POST/PATCH/DELETE members)仍仅限系统管理员。
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|---|---|---|
|
||||
| `GET` | `/api/v1/platform/employees` | 列全平台未软删员工;包含停用、锁定及无平台角色用户 |
|
||||
@@ -596,7 +639,7 @@ Base 前缀 `/api/v1/admin`。
|
||||
| `GET` | `/api/v1/platform/workspaces/{workspace_id}` | 单个 workspace(含已 disabled 的,用于恢复) |
|
||||
| `PATCH` | `/api/v1/platform/workspaces/{workspace_id}` | 改 workspace 字段;`status` 仅允许 `active`/`archived` |
|
||||
| `DELETE` | `/api/v1/platform/workspaces/{workspace_id}` | 软删 workspace;级联软删其成员 |
|
||||
| `GET` | `/api/v1/platform/workspaces/{workspace_id}/members` | 列成员 |
|
||||
| `GET` | `/api/v1/platform/workspaces/{workspace_id}/members` | 列成员(**系统管理员或该 workspace 活跃成员**;为跨 owner 目录树提供成员名册,见 §7 intro 例外) |
|
||||
| `POST` | `/api/v1/platform/workspaces/{workspace_id}/members` | 添加成员(返回 201) |
|
||||
| `PATCH` | `/api/v1/platform/workspaces/{workspace_id}/members/{user_id}` | 改成员 `member_status`;**不能改 role_code**(workspace 角色继承自平台角色) |
|
||||
| `DELETE` | `/api/v1/platform/workspaces/{workspace_id}/members/{user_id}` | 软删成员 |
|
||||
|
||||
Reference in New Issue
Block a user