feat(frontend): align skills bootstrap flow to titan contract

This commit is contained in:
肖应宇 2026-04-07 13:29:13 +08:00
parent c8536c7da3
commit 021f6e3b3c
5 changed files with 38 additions and 91 deletions

View File

@ -1,34 +0,0 @@
import assert from "node:assert/strict";
import test from "node:test";
const { normalizeBootstrapRemoteSkillRequest } = await import(
new URL("./normalize-bootstrap.ts", import.meta.url).href
);
void test("keeps content_ids as primary contract", () => {
const normalized = normalizeBootstrapRemoteSkillRequest({
thread_id: "t1",
content_ids: [11, 22],
});
assert.deepEqual(normalized.content_ids, [11, 22]);
});
void test("maps legacy content_id to content_ids for compatibility", () => {
const normalized = normalizeBootstrapRemoteSkillRequest({
thread_id: "t1",
content_id: 7,
});
assert.deepEqual(normalized.content_ids, [7]);
});
void test("throws when neither content_ids nor content_id is provided", () => {
assert.throws(
() =>
normalizeBootstrapRemoteSkillRequest({
thread_id: "t1",
}),
/content_ids is required/,
);
});

View File

@ -1,9 +1,6 @@
import { getBackendBaseURL } from "@/core/config"; import { getBackendBaseURL } from "@/core/config";
import { import type { Skill } from "./type";
normalizeBootstrapRemoteSkillRequest,
} from "./normalize-bootstrap";
import type { Skill } from "./types";
export async function loadSkills() { export async function loadSkills() {
const skills = await fetch(`${getBackendBaseURL()}/api/skills`); const skills = await fetch(`${getBackendBaseURL()}/api/skills`);
@ -38,6 +35,7 @@ export interface InstallSkillResponse {
message: string; message: string;
} }
// [移植自 main 分支 ef9a071] 添加 skill yaml 解析和远程 skill 初始化 API
export interface MaterializeSkillYamlRequest { export interface MaterializeSkillYamlRequest {
thread_id: string; thread_id: string;
path: string; path: string;
@ -55,9 +53,7 @@ export interface MaterializeSkillYamlResponse {
export interface BootstrapRemoteSkillRequest { export interface BootstrapRemoteSkillRequest {
thread_id: string; thread_id: string;
content_ids?: number[]; content_id: number;
// Legacy input, kept for minimal compatibility at the API boundary.
content_id?: number;
language_type?: number; language_type?: number;
target_dir?: string; target_dir?: string;
clear_target?: boolean; clear_target?: boolean;
@ -98,9 +94,14 @@ export async function installSkill(
return response.json(); return response.json();
} }
// [移植自 main 分支 ef9a071] 解析 skill.yaml 文件并创建目录结构
export async function materializeSkillYaml( export async function materializeSkillYaml(
request: MaterializeSkillYamlRequest, request: MaterializeSkillYamlRequest,
): Promise<MaterializeSkillYamlResponse> { ): Promise<MaterializeSkillYamlResponse> {
console.log("[skills/api] ========== materializeSkillYaml START ==========");
console.log("[skills/api] request:", JSON.stringify(request, null, 2));
console.log("[skills/api] API URL:", `${getBackendBaseURL()}/api/skills/materialize-yaml`);
const response = await fetch( const response = await fetch(
`${getBackendBaseURL()}/api/skills/materialize-yaml`, `${getBackendBaseURL()}/api/skills/materialize-yaml`,
{ {
@ -112,20 +113,35 @@ export async function materializeSkillYaml(
}, },
); );
console.log("[skills/api] response status:", response.status, response.statusText);
if (!response.ok) { if (!response.ok) {
const errorData = await response.json().catch(() => ({})); const errorData = await response.json().catch(() => ({}));
const errorMessage = const errorMessage =
errorData.detail ?? `HTTP ${response.status}: ${response.statusText}`; errorData.detail ?? `HTTP ${response.status}: ${response.statusText}`;
console.error("[skills/api] materializeSkillYaml FAILED:", errorMessage);
console.error("[skills/api] error data:", errorData);
throw new Error(errorMessage); throw new Error(errorMessage);
} }
return response.json(); const result = await response.json();
console.log("[skills/api] materializeSkillYaml SUCCESS:", result);
console.log("[skills/api] ========== materializeSkillYaml END ==========");
return result;
} }
// [移植自 main 分支 ef9a071] 从远程平台获取 skill 并初始化
export async function bootstrapRemoteSkill( export async function bootstrapRemoteSkill(
request: BootstrapRemoteSkillRequest, request: BootstrapRemoteSkillRequest,
): Promise<BootstrapRemoteSkillResponse> { ): Promise<BootstrapRemoteSkillResponse> {
const normalizedRequest = normalizeBootstrapRemoteSkillRequest(request); console.log("[skills/api] ========== bootstrapRemoteSkill START ==========");
console.log("[skills/api] request:", JSON.stringify(request, null, 2));
console.log("[skills/api] thread_id:", request.thread_id);
console.log("[skills/api] content_id:", request.content_id);
console.log("[skills/api] language_type:", request.language_type);
console.log("[skills/api] target_dir:", request.target_dir);
console.log("[skills/api] API URL:", `${getBackendBaseURL()}/api/skills/bootstrap-remote`);
const response = await fetch( const response = await fetch(
`${getBackendBaseURL()}/api/skills/bootstrap-remote`, `${getBackendBaseURL()}/api/skills/bootstrap-remote`,
{ {
@ -133,16 +149,26 @@ export async function bootstrapRemoteSkill(
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify(normalizedRequest), body: JSON.stringify(request),
}, },
); );
console.log("[skills/api] response status:", response.status, response.statusText);
if (!response.ok) { if (!response.ok) {
const errorData = await response.json().catch(() => ({})); const errorData = await response.json().catch(() => ({}));
const errorMessage = const errorMessage =
errorData.detail ?? `HTTP ${response.status}: ${response.statusText}`; errorData.detail ?? `HTTP ${response.status}: ${response.statusText}`;
console.error("[skills/api] bootstrapRemoteSkill FAILED:", errorMessage);
console.error("[skills/api] error data:", errorData);
throw new Error(errorMessage); throw new Error(errorMessage);
} }
return response.json(); const result = await response.json();
console.log("[skills/api] bootstrapRemoteSkill SUCCESS:", result);
console.log("[skills/api] created_directories:", result.created_directories);
console.log("[skills/api] created_files:", result.created_files);
console.log("[skills/api] sandbox_id:", result.sandbox_id);
console.log("[skills/api] ========== bootstrapRemoteSkill END ==========");
return result;
} }

View File

@ -1,44 +0,0 @@
export interface BootstrapRemoteSkillRequestLike {
thread_id: string;
content_ids?: number[];
content_id?: number;
language_type?: number;
target_dir?: string;
clear_target?: boolean;
}
export interface NormalizedBootstrapRemoteSkillRequest
extends Omit<BootstrapRemoteSkillRequestLike, "content_id" | "content_ids"> {
content_ids: number[];
}
export function normalizeBootstrapRemoteSkillRequest(
request: BootstrapRemoteSkillRequestLike,
): NormalizedBootstrapRemoteSkillRequest {
const normalizedContentIds = Array.isArray(request.content_ids)
? request.content_ids
.map((id) => Number(id))
.filter((id) => Number.isFinite(id) && id > 0)
: [];
const legacyContentId =
request.content_id != null && Number.isFinite(Number(request.content_id))
? Number(request.content_id)
: undefined;
const contentIds =
normalizedContentIds.length > 0
? normalizedContentIds
: legacyContentId != null
? [legacyContentId]
: [];
if (contentIds.length === 0) {
throw new Error("content_ids is required.");
}
return {
...request,
content_ids: contentIds,
};
}

View File

@ -1 +0,0 @@
export type { Skill } from "./type";

View File

@ -79,7 +79,7 @@ export function useSelectedSkillListener({
try { try {
const result = await bootstrapRemoteSkill({ const result = await bootstrapRemoteSkill({
thread_id: threadId, thread_id: threadId,
content_ids: [Number(id)], content_id: Number(id),
language_type: languageType, language_type: languageType,
target_dir: "/mnt/user-data/uploads/skill", target_dir: "/mnt/user-data/uploads/skill",
clear_target: true, clear_target: true,