2026-06-08 22:39:45 +08:00
|
|
|
/**
|
|
|
|
|
* 后端 API 客户端。基址走 NEXT_PUBLIC_API_BASE,缺省指向本地后端 :8000。
|
|
|
|
|
*
|
|
|
|
|
* JSON 契约与后端 schemas.py 对齐(camelCase),所以返回体可直接喂给页面 state。
|
|
|
|
|
* 注意:api_key 读取时后端永远打码,写回打码占位符表示"不改 key"(写时哨兵)。
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
const API_BASE =
|
|
|
|
|
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8000";
|
|
|
|
|
|
|
|
|
|
export type ModelType = "LLM" | "ASR" | "TTS" | "Realtime" | "Embedding";
|
|
|
|
|
export type InterfaceType = "openai" | "xfyun" | "dashscope" | "gemini";
|
|
|
|
|
|
|
|
|
|
/** 列表/详情返回(api_key 已打码) */
|
|
|
|
|
export type Credential = {
|
|
|
|
|
id: string;
|
|
|
|
|
name: string;
|
|
|
|
|
modelId: string;
|
|
|
|
|
type: ModelType;
|
|
|
|
|
interfaceType: InterfaceType;
|
|
|
|
|
apiUrl: string;
|
|
|
|
|
apiKey: string;
|
2026-06-09 14:42:25 +08:00
|
|
|
voice: string;
|
|
|
|
|
speed: number;
|
|
|
|
|
language: string;
|
2026-06-08 22:39:45 +08:00
|
|
|
isDefault: boolean;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** 创建/更新入参。apiKey 留空或打码值 → 后端保留旧 key */
|
|
|
|
|
export type CredentialUpsert = {
|
|
|
|
|
name: string;
|
|
|
|
|
modelId: string;
|
|
|
|
|
type: ModelType;
|
|
|
|
|
interfaceType: InterfaceType;
|
|
|
|
|
apiUrl: string;
|
|
|
|
|
apiKey: string;
|
|
|
|
|
isDefault: boolean;
|
|
|
|
|
};
|
|
|
|
|
|
2026-06-09 14:42:25 +08:00
|
|
|
export type CredentialTestRequest = Pick<
|
|
|
|
|
CredentialUpsert,
|
|
|
|
|
| "modelId"
|
|
|
|
|
| "type"
|
|
|
|
|
| "interfaceType"
|
|
|
|
|
| "apiUrl"
|
|
|
|
|
| "apiKey"
|
|
|
|
|
| "voice"
|
|
|
|
|
| "speed"
|
|
|
|
|
| "language"
|
|
|
|
|
>;
|
|
|
|
|
|
|
|
|
|
export type CredentialTestResult = {
|
|
|
|
|
ok: boolean;
|
|
|
|
|
latencyMs: number | null;
|
|
|
|
|
message: string;
|
|
|
|
|
detail: string;
|
|
|
|
|
};
|
|
|
|
|
|
2026-06-08 22:39:45 +08:00
|
|
|
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
|
|
|
|
const res = await fetch(`${API_BASE}${path}`, {
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
|
|
|
|
...init,
|
|
|
|
|
});
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
let detail = `请求失败 (${res.status})`;
|
|
|
|
|
try {
|
|
|
|
|
const body = (await res.json()) as { detail?: string };
|
|
|
|
|
if (body?.detail) detail = body.detail;
|
|
|
|
|
} catch {
|
|
|
|
|
// 响应体不是 JSON,沿用默认错误信息
|
|
|
|
|
}
|
|
|
|
|
throw new Error(detail);
|
|
|
|
|
}
|
|
|
|
|
// 204 / 空响应保护
|
|
|
|
|
const text = await res.text();
|
|
|
|
|
return (text ? JSON.parse(text) : undefined) as T;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const credentialsApi = {
|
|
|
|
|
list: () => request<Credential[]>("/api/credentials"),
|
|
|
|
|
create: (body: CredentialUpsert) =>
|
|
|
|
|
request<Credential>("/api/credentials", {
|
|
|
|
|
method: "POST",
|
|
|
|
|
body: JSON.stringify(body),
|
|
|
|
|
}),
|
|
|
|
|
update: (id: string, body: CredentialUpsert) =>
|
|
|
|
|
request<Credential>(`/api/credentials/${id}`, {
|
|
|
|
|
method: "PUT",
|
|
|
|
|
body: JSON.stringify(body),
|
|
|
|
|
}),
|
2026-06-09 14:42:25 +08:00
|
|
|
test: (body: CredentialTestRequest, id?: string) =>
|
|
|
|
|
request<CredentialTestResult>(
|
|
|
|
|
id ? `/api/credentials/${id}/test` : "/api/credentials/test",
|
|
|
|
|
{
|
|
|
|
|
method: "POST",
|
|
|
|
|
body: JSON.stringify(body),
|
|
|
|
|
},
|
|
|
|
|
),
|
2026-06-09 09:48:43 +08:00
|
|
|
// 服务端整行复制(含真 key,密钥不经浏览器)
|
|
|
|
|
duplicate: (id: string) =>
|
|
|
|
|
request<Credential>(`/api/credentials/${id}/duplicate`, { method: "POST" }),
|
2026-06-08 22:39:45 +08:00
|
|
|
remove: (id: string) =>
|
|
|
|
|
request<{ ok: boolean }>(`/api/credentials/${id}`, { method: "DELETE" }),
|
|
|
|
|
};
|
2026-06-09 09:48:43 +08:00
|
|
|
|
|
|
|
|
// ---------- 助手 ----------
|
|
|
|
|
export type AssistantType =
|
|
|
|
|
| "prompt"
|
|
|
|
|
| "workflow"
|
|
|
|
|
| "dify"
|
|
|
|
|
| "fastgpt"
|
|
|
|
|
| "opencode";
|
|
|
|
|
export type RuntimeMode = "pipeline" | "realtime";
|
|
|
|
|
|
2026-06-09 10:37:29 +08:00
|
|
|
/** 后端 AssistantOut(宽表 STI:瘦字段平铺,workflow 用 graph)。apiKey 读时打码 */
|
2026-06-09 09:48:43 +08:00
|
|
|
export type Assistant = {
|
|
|
|
|
id: string;
|
|
|
|
|
name: string;
|
|
|
|
|
type: AssistantType;
|
|
|
|
|
runtimeMode: RuntimeMode;
|
|
|
|
|
greeting: string;
|
|
|
|
|
enableInterrupt: boolean;
|
|
|
|
|
llmCredentialId: string | null;
|
|
|
|
|
asrCredentialId: string | null;
|
|
|
|
|
ttsCredentialId: string | null;
|
|
|
|
|
realtimeCredentialId: string | null;
|
|
|
|
|
knowledgeBaseId: string | null;
|
2026-06-09 10:37:29 +08:00
|
|
|
prompt: string;
|
|
|
|
|
apiUrl: string;
|
|
|
|
|
apiKey: string;
|
|
|
|
|
appId: string;
|
|
|
|
|
graph: Record<string, unknown>;
|
2026-06-09 09:48:43 +08:00
|
|
|
updatedAt?: string | null;
|
|
|
|
|
};
|
|
|
|
|
|
2026-06-09 10:37:29 +08:00
|
|
|
export type AssistantUpsert = Omit<Assistant, "id" | "updatedAt">;
|
2026-06-09 09:48:43 +08:00
|
|
|
|
|
|
|
|
export const assistantsApi = {
|
|
|
|
|
list: () => request<Assistant[]>("/api/assistants"),
|
|
|
|
|
get: (id: string) => request<Assistant>(`/api/assistants/${id}`),
|
|
|
|
|
create: (body: AssistantUpsert) =>
|
|
|
|
|
request<Assistant>("/api/assistants", {
|
|
|
|
|
method: "POST",
|
|
|
|
|
body: JSON.stringify(body),
|
|
|
|
|
}),
|
|
|
|
|
update: (id: string, body: AssistantUpsert) =>
|
|
|
|
|
request<Assistant>(`/api/assistants/${id}`, {
|
|
|
|
|
method: "PUT",
|
|
|
|
|
body: JSON.stringify(body),
|
|
|
|
|
}),
|
|
|
|
|
// 服务端整行复制(含真 key,密钥不经浏览器)
|
|
|
|
|
duplicate: (id: string) =>
|
|
|
|
|
request<Assistant>(`/api/assistants/${id}/duplicate`, { method: "POST" }),
|
|
|
|
|
remove: (id: string) =>
|
|
|
|
|
request<{ ok: boolean }>(`/api/assistants/${id}`, { method: "DELETE" }),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// ---------- 知识库 ----------
|
|
|
|
|
export type KnowledgeBase = {
|
|
|
|
|
id: string;
|
|
|
|
|
name: string;
|
|
|
|
|
description: string;
|
|
|
|
|
embeddingCredentialId: string | null;
|
|
|
|
|
status: string;
|
|
|
|
|
updatedAt?: string | null;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const knowledgeBasesApi = {
|
|
|
|
|
list: () => request<KnowledgeBase[]>("/api/knowledge-bases"),
|
|
|
|
|
};
|