299 lines
7.3 KiB
TypeScript
299 lines
7.3 KiB
TypeScript
import {
|
||
apiRequest,
|
||
ApiRequestError,
|
||
createUuid,
|
||
type ApiErrorEnvelope,
|
||
} from "./_shared";
|
||
|
||
export type ScriptType = "python" | "notebook";
|
||
export type Visibility = "private" | "workspace" | "public";
|
||
|
||
export type ScriptItem = {
|
||
script_id: string;
|
||
workspace_id: string;
|
||
current_object_id: string;
|
||
owner_user_id: string;
|
||
owner_display_name: string | null;
|
||
script_name: string;
|
||
script_type: ScriptType;
|
||
visibility: Visibility;
|
||
status: string;
|
||
is_locked: boolean;
|
||
relative_path: string;
|
||
jupyter_path: string;
|
||
content_hash: string;
|
||
size_bytes: number;
|
||
created_at: string;
|
||
updated_at: string;
|
||
};
|
||
|
||
export async function listScripts(
|
||
workspaceId: string,
|
||
parentPath: string = "",
|
||
ownerUserId?: string,
|
||
): Promise<ScriptItem[]> {
|
||
// Default (no ownerUserId) scopes to the requester's own subtree; passing
|
||
// ownerUserId scopes to that owner's subtree (workspace/public only — the
|
||
// backend excludes their private) so the tree can lazily fetch another
|
||
// member's content when their group is expanded.
|
||
const parameters = new URLSearchParams();
|
||
if (parentPath) parameters.set("parent_path", parentPath);
|
||
if (ownerUserId) parameters.set("owner_user_id", ownerUserId);
|
||
const query = parameters.toString();
|
||
return apiRequest<ScriptItem[]>(
|
||
`/api/v1/scripts${query ? `?${query}` : ""}`,
|
||
{},
|
||
workspaceId,
|
||
);
|
||
}
|
||
|
||
export async function countScripts(
|
||
workspaceId: string,
|
||
): Promise<number> {
|
||
// Backend route /api/v1/scripts/count must be declared BEFORE the
|
||
// /scripts/{script_id} route on the server side. Returns
|
||
// { data: { total: number } } — the dashboard's single source of
|
||
// truth for "total active scripts in workspace", independent of the
|
||
// lazy-loaded scripts[] in the workspace store.
|
||
const envelope = await apiRequest<{ total: number }>(
|
||
"/api/v1/scripts/count",
|
||
{},
|
||
workspaceId,
|
||
);
|
||
return envelope.total;
|
||
}
|
||
|
||
function initialContent(scriptType: ScriptType): string {
|
||
if (scriptType === "python") {
|
||
return [
|
||
'"""模型实验开发平台构建脚本。"""',
|
||
"",
|
||
"",
|
||
"def main() -> None:",
|
||
' print("Hello, Model Platform!")',
|
||
"",
|
||
"",
|
||
'if __name__ == "__main__":',
|
||
" main()",
|
||
"",
|
||
].join("\n");
|
||
}
|
||
|
||
return JSON.stringify(
|
||
{
|
||
cells: [
|
||
{
|
||
id: "intro",
|
||
cell_type: "code",
|
||
execution_count: null,
|
||
metadata: {},
|
||
outputs: [],
|
||
source: ["print('Hello, Model Platform!')\n"],
|
||
},
|
||
],
|
||
metadata: {
|
||
kernelspec: {
|
||
display_name: "Python 3",
|
||
language: "python",
|
||
name: "python3",
|
||
},
|
||
language_info: {
|
||
name: "python",
|
||
version: "3.12",
|
||
},
|
||
},
|
||
nbformat: 4,
|
||
nbformat_minor: 5,
|
||
},
|
||
null,
|
||
2,
|
||
);
|
||
}
|
||
|
||
export async function createScript(
|
||
workspaceId: string,
|
||
input: {
|
||
name: string;
|
||
scriptType: ScriptType;
|
||
visibility: Visibility;
|
||
parentPath?: string | null;
|
||
},
|
||
): Promise<ScriptItem> {
|
||
return apiRequest<ScriptItem>(
|
||
"/api/v1/scripts",
|
||
{
|
||
method: "POST",
|
||
body: JSON.stringify({
|
||
script_name: input.name.trim(),
|
||
script_type: input.scriptType,
|
||
visibility: input.visibility,
|
||
content: initialContent(input.scriptType),
|
||
parent_path: input.parentPath,
|
||
}),
|
||
},
|
||
workspaceId,
|
||
);
|
||
}
|
||
|
||
export async function uploadScript(
|
||
workspaceId: string,
|
||
file: File,
|
||
parentPath = "",
|
||
visibility: Visibility = "workspace",
|
||
): Promise<ScriptItem> {
|
||
const parameters = new URLSearchParams({
|
||
file_name: file.name,
|
||
parent_path: parentPath,
|
||
visibility,
|
||
});
|
||
return apiRequest<ScriptItem>(
|
||
`/api/v1/scripts/upload?${parameters.toString()}`,
|
||
{
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/octet-stream" },
|
||
body: file,
|
||
},
|
||
workspaceId,
|
||
);
|
||
}
|
||
|
||
export async function setScriptLock(
|
||
workspaceId: string,
|
||
scriptId: string,
|
||
isLocked: boolean,
|
||
): Promise<ScriptItem> {
|
||
return apiRequest<ScriptItem>(
|
||
`/api/v1/scripts/${encodeURIComponent(scriptId)}/lock`,
|
||
{
|
||
method: "PATCH",
|
||
body: JSON.stringify({ is_locked: isLocked }),
|
||
},
|
||
workspaceId,
|
||
);
|
||
}
|
||
|
||
export async function updateScript(
|
||
workspaceId: string,
|
||
scriptId: string,
|
||
input: { content: string },
|
||
): Promise<ScriptItem> {
|
||
return apiRequest<ScriptItem>(
|
||
`/api/v1/scripts/${scriptId}`,
|
||
{ method: "PUT", body: JSON.stringify(input) },
|
||
workspaceId,
|
||
);
|
||
}
|
||
|
||
export async function getScriptContent(
|
||
workspaceId: string,
|
||
scriptId: string,
|
||
): Promise<{
|
||
script_id: string;
|
||
script_type: ScriptType;
|
||
content: string | object;
|
||
format: string;
|
||
}> {
|
||
const response = await fetch(
|
||
`/api/v1/scripts/${scriptId}/content?workspace_id=${encodeURIComponent(workspaceId)}`,
|
||
{
|
||
credentials: "same-origin",
|
||
headers: {
|
||
"X-Request-ID": createUuid().replaceAll("-", ""),
|
||
},
|
||
},
|
||
);
|
||
|
||
if (response.status === 401 && typeof window !== "undefined") {
|
||
const here = window.location.pathname;
|
||
if (here !== "/login") {
|
||
window.location.assign("/login");
|
||
}
|
||
throw new ApiRequestError("未登录或登录已过期", 401);
|
||
}
|
||
|
||
const payload = await response.json();
|
||
if (!response.ok) {
|
||
const error = payload as ApiErrorEnvelope;
|
||
const detailMessage = typeof error.detail === "string"
|
||
? error.detail
|
||
: error.detail?.message;
|
||
throw new ApiRequestError(
|
||
detailMessage ?? `请求失败(HTTP ${response.status})`,
|
||
response.status,
|
||
);
|
||
}
|
||
|
||
const data = (payload as { data: { script_id: string; script_type: ScriptType; content: string | object; format: string } }).data;
|
||
if (!data || !data.script_type) {
|
||
throw new ApiRequestError("响应数据格式错误", response.status);
|
||
}
|
||
return data;
|
||
}
|
||
|
||
export async function deleteScript(
|
||
workspaceId: string,
|
||
scriptId: string,
|
||
): Promise<{ script_id: string; status: string; versions_preserved: boolean }> {
|
||
return apiRequest(
|
||
`/api/v1/scripts/${scriptId}`,
|
||
{ method: "DELETE" },
|
||
workspaceId,
|
||
);
|
||
}
|
||
|
||
export type StableVersion = {
|
||
versions_id: string;
|
||
workspace_id: string;
|
||
script_id: string;
|
||
source_object_id: string;
|
||
artifact_object_id: string;
|
||
version_no: number;
|
||
version_label: string;
|
||
source_path: string;
|
||
artifact_path: string;
|
||
content_hash: string;
|
||
file_size_bytes: number;
|
||
visibility: Visibility;
|
||
release_note: string | null;
|
||
created_by: string;
|
||
created_at: string;
|
||
};
|
||
|
||
export type LatestVersion = {
|
||
versions_id: string;
|
||
version_label: string;
|
||
};
|
||
|
||
export async function getLatestScriptVersion(
|
||
workspaceId: string,
|
||
scriptId: string,
|
||
): Promise<LatestVersion | null> {
|
||
return apiRequest<LatestVersion | null>(
|
||
`/api/v1/scripts/${scriptId}/latest-version`,
|
||
{},
|
||
workspaceId,
|
||
);
|
||
}
|
||
|
||
export async function publishScriptVersion(
|
||
workspaceId: string,
|
||
input: {
|
||
script: ScriptItem;
|
||
releaseNote: string;
|
||
visibility: Visibility;
|
||
},
|
||
): Promise<StableVersion> {
|
||
return apiRequest<StableVersion>(
|
||
`/api/v1/scripts/${input.script.script_id}/versions`,
|
||
{
|
||
method: "POST",
|
||
body: JSON.stringify({
|
||
source_object_id: input.script.current_object_id,
|
||
release_note: input.releaseNote.trim() || null,
|
||
visibility: input.visibility,
|
||
}),
|
||
},
|
||
workspaceId,
|
||
);
|
||
}
|