From 94a1aef7c7fc053a2589143bc2cc49dcb1183baa Mon Sep 17 00:00:00 2001 From: "tao.chen" <93983997+taochen-ct@users.noreply.github.com> Date: Tue, 4 Aug 2026 14:24:55 +0800 Subject: [PATCH 1/3] fix: script not found --- backend/src/backend/scripts.py | 50 ++++++++++++++----- frontend/app/context/AuthContext.tsx | 1 - .../features/platform/ModelPlatformApp.tsx | 23 --------- frontend/app/services/api.ts | 12 ----- 4 files changed, 37 insertions(+), 49 deletions(-) diff --git a/backend/src/backend/scripts.py b/backend/src/backend/scripts.py index b322d23..84a36b6 100644 --- a/backend/src/backend/scripts.py +++ b/backend/src/backend/scripts.py @@ -21,6 +21,7 @@ from fastapi import ( from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession +from common.config import settings from common.db.models import ( Scripts, StorageObjects, @@ -356,19 +357,40 @@ async def create_script_record( detail=exc.detail, ) from exc - # Synthesise a storage_data-shaped dict from the Jupyter response - # so the existing script_payload + response shape keep working. - # The storage_object_id is a fresh ULID — there is no real - # StorageObject row for this file; downstream list/get operations - # that JOIN StorageObjects will skip jupyter-only scripts. + # Build a real StorageObjects row so the file participates in + # workspace-tree / list / get queries that JOIN this table. The + # bytes live in the Jupyter mount; rclone replicates them to + # RustFS asynchronously. We mark the row "available" because the + # file is queryable as a workspace file from the user's POV; the + # storage_uri points at where the replicated bytes will land. object_id = new_ulid() - storage_data = { - "storage_object_id": object_id, - "relative_path": jupyter_name, - "object_key": f"{workspace_id}/{jupyter_name}", - "content_hash": content_hash, - "size_bytes": size_bytes, - } + object_key = f"{workspace_id}/{jupyter_name}" + bucket_name = settings.rustfs_workspace_bucket + relative_path = user_relative_path(context, jupyter_name) + mime_type = mimetypes.guess_type(jupyter_name)[0] + storage_object = StorageObjects( + storage_object_id=object_id, + workspace_id=context.workspace.workspace_id, + owner_user_id=context.user.user_id, + object_type="file", + usage_type="working_copy", + storage_backend="rustfs", + bucket_name=bucket_name, + object_key=object_key, + object_key_hash=hashlib.sha256(object_key.encode("utf-8")).digest(), + storage_uri=f"s3://{bucket_name}/{object_key}", + file_name=name, + file_extension=PurePosixPath(jupyter_name).suffix.lower() or None, + mime_type=mime_type, + size_bytes=size_bytes, + content_hash=content_hash, + visibility=visibility, + is_immutable=0, + object_status="available", + created_by=context.user.user_id, + relative_path=relative_path, + path_hash=hashlib.sha256(relative_path.encode("utf-8")).digest(), + ) script = Scripts( script_id=script_id, workspace_id=context.workspace.workspace_id, @@ -379,10 +401,12 @@ async def create_script_record( visibility=visibility, status="active", ) + session.add(storage_object) session.add(script) await session.flush() + await session.refresh(storage_object) await session.refresh(script) - return script, storage_data + return script, storage_object @router.post("/api/v1/scripts", status_code=status.HTTP_201_CREATED) diff --git a/frontend/app/context/AuthContext.tsx b/frontend/app/context/AuthContext.tsx index aeda940..81b45c6 100644 --- a/frontend/app/context/AuthContext.tsx +++ b/frontend/app/context/AuthContext.tsx @@ -222,7 +222,6 @@ export function useApi(): WorkspaceBoundApi { rawApi.createJupyterAccessTicket(workspaceId, session), getLatestScriptVersion: (scriptId) => rawApi.getLatestScriptVersion(workspaceId, scriptId), - listScriptVersions: (scriptId) => rawApi.listScriptVersions(workspaceId, scriptId), publishScriptVersion: (input) => rawApi.publishScriptVersion(workspaceId, input), listSchedules: () => rawApi.listSchedules(workspaceId), getSchedule: (scheduleId) => rawApi.getSchedule(workspaceId, scheduleId), diff --git a/frontend/app/features/platform/ModelPlatformApp.tsx b/frontend/app/features/platform/ModelPlatformApp.tsx index 947e8c2..a677676 100644 --- a/frontend/app/features/platform/ModelPlatformApp.tsx +++ b/frontend/app/features/platform/ModelPlatformApp.tsx @@ -162,8 +162,6 @@ function AuthenticatedModelPlatformApp() { scriptId: string; message: string; } | null>(null); - const [versions, setVersions] = useState([]); - const [versionsLoading, setVersionsLoading] = useState(false); const [latestVersion, setLatestVersion] = useState( null, ); @@ -248,12 +246,10 @@ function AuthenticatedModelPlatformApp() { useEffect(() => { if (!selectedId) { - setVersions([]); setLatestVersion(null); return; } let ignore = false; - setVersionsLoading(true); setLatestVersionLoading(true); void api.getLatestScriptVersion(selectedId) .then((item) => { @@ -273,21 +269,6 @@ function AuthenticatedModelPlatformApp() { .finally(() => { if (!ignore) setLatestVersionLoading(false); }); - void api.listScriptVersions(selectedId) - .then((items) => { - if (!ignore) setVersions(items); - }) - .catch((error) => { - if (!ignore) { - setToast({ - tone: "error", - message: error instanceof Error ? error.message : "版本列表加载失败", - }); - } - }) - .finally(() => { - if (!ignore) setVersionsLoading(false); - }); return () => { ignore = true; }; @@ -595,10 +576,6 @@ function AuthenticatedModelPlatformApp() { releaseNote, visibility: publishVisibility, }); - setVersions((items) => [ - version, - ...items.filter((item) => item.versions_id !== version.versions_id), - ]); setPublishTarget(null); setPublishedVersion(version); setToast({ diff --git a/frontend/app/services/api.ts b/frontend/app/services/api.ts index 9920693..86f0e50 100644 --- a/frontend/app/services/api.ts +++ b/frontend/app/services/api.ts @@ -574,17 +574,6 @@ export async function getLatestScriptVersion( ); } -export async function listScriptVersions( - workspaceId: string, - scriptId: string, -): Promise { - return apiRequest( - `/api/v1/scripts/${scriptId}/versions`, - {}, - workspaceId, - ); -} - export async function publishScriptVersion( workspaceId: string, input: { @@ -1119,7 +1108,6 @@ export type WorkspaceBoundApi = { session: ActiveEditSession, ) => Promise; getLatestScriptVersion: (scriptId: string) => Promise; - listScriptVersions: (scriptId: string) => Promise; publishScriptVersion: ( input: Parameters[1], ) => Promise; From eace1109ac9d7168d747cd942c2f91ccd2db0467 Mon Sep 17 00:00:00 2001 From: xiaozhu <2395895331@qq.com> Date: Tue, 4 Aug 2026 14:49:10 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E9=83=A8=E5=88=86=E6=9B=B4=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/app/features/admin/AdminPages.tsx | 209 ++++++++++++++---- .../features/platform/ModelPlatformApp.tsx | 11 +- frontend/app/styles/admin.css | 4 +- frontend/app/styles/platform.css | 36 ++- 4 files changed, 212 insertions(+), 48 deletions(-) diff --git a/frontend/app/features/admin/AdminPages.tsx b/frontend/app/features/admin/AdminPages.tsx index cc75cd6..5fe192a 100644 --- a/frontend/app/features/admin/AdminPages.tsx +++ b/frontend/app/features/admin/AdminPages.tsx @@ -8,6 +8,7 @@ import { useApi, useAuth } from "../../context/AuthContext"; import Icon from "../../components/Icon"; import "../../styles/admin.css"; import "../../styles/dashboard.css"; +import "../../styles/platform.css"; type Notice = { @@ -41,13 +42,13 @@ export function DashboardPage({
{scriptCount}工作副本
2Workspace
-
4平台员工
+
4平台用户
{online ? "正常" : "检查中"}平台状态
- +
@@ -105,6 +106,11 @@ const EMPTY_FORM = { status: "active" as "active" | "disabled" | "locked", }; +const EMPTY_PROJECT_FORM = { + project_name: "", + description: "", +}; + export function SystemAdminPage({ onNotify, onConnectionChange, @@ -112,6 +118,7 @@ export function SystemAdminPage({ onNotify: (notice: Notice) => void; onConnectionChange: (online: boolean) => void; }) { + const [activeTab, setActiveTab] = useState<"users" | "projects">("users"); const api = useApi(); const { user, currentWorkspace } = useAuth(); const [employees, setEmployees] = useState([]); @@ -120,6 +127,8 @@ export function SystemAdminPage({ const [editing, setEditing] = useState(null); const [dialogOpen, setDialogOpen] = useState(false); const [form, setForm] = useState(EMPTY_FORM); + const [projectDialogOpen, setProjectDialogOpen] = useState(false); + const [projectForm, setProjectForm] = useState(EMPTY_PROJECT_FORM); const canManage = user?.role_code === "admin"; const load = async (): Promise => { @@ -131,7 +140,7 @@ export function SystemAdminPage({ onConnectionChange(false); onNotify({ tone: "error", - message: error instanceof Error ? error.message : "员工列表加载失败", + message: error instanceof Error ? error.message : "用户列表加载失败", }); } finally { setLoading(false); @@ -160,7 +169,7 @@ export function SystemAdminPage({ setDialogOpen(true); }; - const submit = async (event: FormEvent): Promise => { + const submit = async (event: React.FormEvent): Promise => { event.preventDefault(); if (!form.display_name.trim() || (!editing && !form.username.trim())) return; setSaving(true); @@ -185,11 +194,11 @@ export function SystemAdminPage({ setEmployees((current) => [...current, created]); } setDialogOpen(false); - onNotify({ tone: "success", message: editing ? "员工信息已更新" : "员工已添加" }); + onNotify({ tone: "success", message: editing ? "用户信息已更新" : "用户已添加" }); } catch (error) { onNotify({ tone: "error", - message: error instanceof ApiRequestError ? error.message : "保存员工失败", + message: error instanceof ApiRequestError ? error.message : "保存用户失败", }); } finally { setSaving(false); @@ -197,62 +206,178 @@ export function SystemAdminPage({ }; const remove = async (employee: Employee): Promise => { - if (!window.confirm(`确定从当前 Workspace 删除员工“${employee.display_name}”吗?`)) return; + if (!window.confirm(`确定从当前 Workspace 删除用户"${employee.display_name}"吗?`)) return; try { await api.deleteEmployee(employee.user_id); setEmployees((current) => current.filter((item) => item.user_id !== employee.user_id)); - onNotify({ tone: "success", message: "员工已删除" }); + onNotify({ tone: "success", message: "用户已删除" }); } catch (error) { onNotify({ tone: "error", - message: error instanceof Error ? error.message : "删除员工失败", + message: error instanceof Error ? error.message : "删除用户失败", }); } }; + const openCreateProject = (): void => { + setProjectForm(EMPTY_PROJECT_FORM); + setProjectDialogOpen(true); + }; + + const submitProject = async (event: React.FormEvent): Promise => { + event.preventDefault(); + if (!projectForm.project_name.trim()) return; + setSaving(true); + try { + // TODO: 调用实际的项目创建 API + // await api.createProject({ ... }) + setProjectDialogOpen(false); + onNotify({ tone: "success", message: "项目已创建" }); + } catch (error) { + onNotify({ + tone: "error", + message: error instanceof ApiRequestError ? error.message : "创建项目失败", + }); + } finally { + setSaving(false); + } + }; + return (
-
+ {/*
系统管理 -

员工管理

-

{currentWorkspace?.workspace_name ?? "(未选择 Workspace)"} · {employees.length} 名员工

+

{activeTab === "users" ? "用户管理" : "项目管理"}

+

+ {currentWorkspace?.workspace_name ?? "(未选择 Workspace)"} + {activeTab === "users" ? ` · ${employees.length} 名用户` : ""} +

- + )} +
*/} + +
+ + -
- {!canManage &&
当前为开发人员,只能查看员工列表。
} -
-
员工账号角色状态操作
- {loading ?

正在加载员工…

: employees.map((employee) => { - const isProtectedAdmin = employee.role_code === "admin"; - return ( -
- {employee.display_name.slice(0, 1)}{employee.display_name}{employee.email ?? "未设置邮箱"} - {employee.username} - {employee.role_name} - {employee.status === "active" ? "正常" : employee.status === "disabled" ? "已停用" : "已锁定"} - - - - -
- ); - })}
- {dialogOpen && ( + {activeTab === "users" && ( +
+
+ + +
+ +
+ )} + + {activeTab === "projects" && ( +
+
+ + +
+ +
+ )} + + {activeTab === "users" ? ( + <> + {!canManage &&
当前为开发人员,只能查看用户列表。
} +
+
用户账号角色状态操作
+ {loading ?

正在加载用户…

: employees.map((employee) => { + const isProtectedAdmin = employee.role_code === "admin"; + return ( +
+ {employee.display_name.slice(0, 1)}{employee.display_name}{employee.email ?? "未设置邮箱"} + {employee.username} + {employee.role_name} + {employee.status === "active" ? "正常" : employee.status === "disabled" ? "已停用" : "已锁定"} + + + + +
+ ); + })} +
+ + {dialogOpen && ( +
+
+
EMPLOYEE

{editing ? "编辑用户" : "添加用户"}

+
void submit(event)}> + + + + + {editing && } +
+
+
+
+ )} + + ) : ( +
+ +

项目管理

+

项目管理功能开发中…

+
+ )} + + {projectDialogOpen && (
-
EMPLOYEE

{editing ? "编辑员工" : "添加员工"}

-
void submit(event)}> - - - - - {editing && } -
+
PROJECT

新建项目

+ void submitProject(event)}> + +