Files
ai-video-fullstack/frontend/src/lib/api.ts
Xin Wang 059ad8162e Add conversation history management and API endpoints
- Introduce new database models for conversation sessions, messages, and artifacts to support conversation history tracking.
- Implement API routes for listing conversations and retrieving detailed conversation data, enhancing user interaction with historical records.
- Add a conversation recorder service to persist conversation messages in real-time without disrupting ongoing calls.
- Update the frontend to display conversation history, including filtering and sorting options, improving user experience.
- Enhance the pipeline to integrate conversation history recording seamlessly during interactions.
2026-07-10 16:25:06 +08:00

377 lines
9.6 KiB
TypeScript

/**
* 后端 API 客户端。基址走 NEXT_PUBLIC_API_BASE,缺省指向本地后端 :8000。
*
* JSON 契约与后端 schemas.py 对齐(camelCase),所以返回体可直接喂给页面 state。
* 注意:api_key 读取时后端永远打码,写回打码占位符表示"不改 key"(写时哨兵)。
*/
import { loginPathWithReturnTo } from "@/lib/auth-redirect";
export const API_BASE =
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:8000";
export type ModelType = "LLM" | "ASR" | "TTS" | "Realtime" | "Embedding" | "Agent";
async function request<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(`${API_BASE}${path}`, {
headers: { "Content-Type": "application/json" },
credentials: "include",
...init,
});
if (
res.status === 401 &&
typeof window !== "undefined" &&
!path.startsWith("/api/auth/")
) {
window.location.href = loginPathWithReturnTo(
window.location.pathname + window.location.search,
);
}
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 type AdminUser = {
username: string;
displayName: string;
role: "super_admin";
};
export const authApi = {
login: (body: { username: string; password: string }) =>
request<AdminUser>("/api/auth/login", {
method: "POST",
body: JSON.stringify(body),
}),
logout: () =>
request<{ ok: boolean }>("/api/auth/logout", {
method: "POST",
}),
me: () => request<AdminUser>("/api/auth/me"),
};
// ---------- 接口定义驱动的模型注册表 ----------
export type InterfaceField = {
key: string;
label: string;
group: "values" | "secrets";
type: "text" | "url" | "password" | "number" | "boolean" | "select";
required: boolean;
default?: unknown;
options?: string[];
};
export type InterfaceDefinition = {
interfaceType: string;
name: string;
capability: ModelType;
fieldSchema: { fields: InterfaceField[] };
enabled: boolean;
version: number;
};
export type ModelResource = {
id: string;
name: string;
capability: ModelType;
interfaceType: string;
values: Record<string, unknown>;
secrets: Record<string, unknown>;
supportImageInput: boolean;
enabled: boolean;
isDefault: boolean;
updatedAt?: string | null;
};
export type ModelResourceUpsert = Omit<
ModelResource,
"id" | "capability" | "updatedAt"
>;
export type ModelResourceTestResult = {
ok: boolean;
latencyMs: number | null;
message: string;
detail: string;
};
export const interfaceDefinitionsApi = {
list: (capability?: ModelType) =>
request<InterfaceDefinition[]>(
`/api/interface-definitions${capability ? `?capability=${capability}` : ""}`,
),
};
export const modelResourcesApi = {
list: () => request<ModelResource[]>("/api/model-resources"),
create: (body: ModelResourceUpsert) =>
request<ModelResource>("/api/model-resources", {
method: "POST",
body: JSON.stringify(body),
}),
update: (id: string, body: ModelResourceUpsert) =>
request<ModelResource>(`/api/model-resources/${id}`, {
method: "PUT",
body: JSON.stringify(body),
}),
test: (body: ModelResourceUpsert, id?: string) =>
request<ModelResourceTestResult>(
id ? `/api/model-resources/${id}/test` : "/api/model-resources/test",
{
method: "POST",
body: JSON.stringify(body),
},
),
duplicate: (id: string) =>
request<ModelResource>(`/api/model-resources/${id}/duplicate`, {
method: "POST",
}),
remove: (id: string) =>
request<{ ok: boolean }>(`/api/model-resources/${id}`, {
method: "DELETE",
}),
};
// ---------- 助手 ----------
export type AssistantType =
| "prompt"
| "workflow"
| "dify"
| "fastgpt"
| "opencode";
export type RuntimeMode = "pipeline" | "realtime";
/** 后端 AssistantOut(宽表 STI:瘦字段平铺,workflow 用 graph)。apiKey 读时打码 */
export type Assistant = {
id: string;
name: string;
type: AssistantType;
runtimeMode: RuntimeMode;
greeting: string;
enableInterrupt: boolean;
visionEnabled: boolean;
visionModelResourceId: string | null;
modelResourceIds: Partial<Record<ModelType, string>>;
knowledgeBaseId: string | null;
toolIds: string[];
prompt: string;
apiUrl: string;
apiKey: string;
appId: string;
graph: Record<string, unknown>;
updatedAt?: string | null;
};
export type AssistantUpsert = Omit<Assistant, "id" | "updatedAt">;
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 Conversation = {
id: string;
assistantId: string | null;
assistantName: string;
channel: string;
runtimeMode: string;
status: string;
messageCount: number;
startedAt: string;
endedAt: string | null;
};
export type ConversationMessage = {
id: string;
sequence: number;
role: "user" | "assistant";
contentType: string;
content: string;
occurredAt: string;
extra: { interrupted?: boolean; turn_id?: string };
};
export type ConversationDetail = Conversation & {
messages: ConversationMessage[];
};
export type ConversationList = {
items: Conversation[];
total: number;
page: number;
pageSize: number;
};
export const conversationsApi = {
list: (params: {
page?: number;
pageSize?: number;
search?: string;
channel?: string;
status?: string;
sortOrder?: "newest" | "oldest";
} = {}) => {
const query = new URLSearchParams({
page: String(params.page ?? 1),
page_size: String(params.pageSize ?? 20),
sort_order: params.sortOrder ?? "newest",
});
if (params.search) query.set("search", params.search);
if (params.channel) query.set("channel", params.channel);
if (params.status) query.set("status", params.status);
return request<ConversationList>(`/api/conversations?${query}`);
},
get: (id: string) =>
request<ConversationDetail>(`/api/conversations/${id}`),
};
// ---------- 工具 ----------
export type ToolStatus = "active" | "archived" | "draft";
export type ToolParameter = {
name: string;
type: "string" | "number" | "integer" | "boolean" | "object" | "array";
location: "path" | "query" | "body" | "header";
description: string;
required: boolean;
};
export type EndCallToolDefinition = {
schemaVersion: number;
type: "end_call";
config: {
messageType: "none" | "custom";
customMessage: string;
captureReason: boolean;
};
};
export type HttpToolDefinition = {
schemaVersion: number;
type: "http";
config: {
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
url: string;
timeoutSeconds: number;
headers: Record<string, string>;
parameters: ToolParameter[];
body: Record<string, unknown>;
};
};
export type Tool = {
id: string;
name: string;
functionName: string;
type: "end_call" | "http";
description: string;
definition: EndCallToolDefinition | HttpToolDefinition;
secrets: Record<string, unknown>;
status: ToolStatus;
updatedAt?: string | null;
};
export type ToolUpsert = Omit<Tool, "id" | "type" | "updatedAt">;
export const toolsApi = {
list: () => request<Tool[]>("/api/tools"),
create: (body: ToolUpsert) =>
request<Tool>("/api/tools", {
method: "POST",
body: JSON.stringify(body),
}),
update: (id: string, body: ToolUpsert) =>
request<Tool>(`/api/tools/${id}`, {
method: "PUT",
body: JSON.stringify(body),
}),
remove: (id: string) =>
request<{ ok: boolean }>(`/api/tools/${id}`, { method: "DELETE" }),
};
// ---------- 知识库 ----------
export type KnowledgeBase = {
id: string;
name: string;
description: string;
embeddingModelResourceId: string | null;
status: string;
updatedAt?: string | null;
};
export const knowledgeBasesApi = {
list: () => request<KnowledgeBase[]>("/api/knowledge-bases"),
};
// ---------- 工作流节点类型目录 ----------
export type NodeFieldSpec = {
key: string;
label: string;
type: "text" | "textarea" | "switch";
required?: boolean;
};
export type NodeConstraints = {
minIncoming?: number;
maxIncoming?: number;
minOutgoing?: number;
maxOutgoing?: number;
};
export type NodeSpecDto = {
name: string;
displayName: string;
category: string;
description: string;
icon: string;
accent: string;
addable: boolean;
constraints: NodeConstraints;
fields: NodeFieldSpec[];
};
export type NodeTypesResponse = {
specVersion: string;
nodeTypes: NodeSpecDto[];
};
export const nodeTypesApi = {
list: () => request<NodeTypesResponse>("/api/node-types"),
};
export type IceServerConfig = {
urls: string | string[];
username?: string;
credential?: string;
};
export const webrtcApi = {
iceServers: () =>
request<{ iceServers: IceServerConfig[] }>("/api/webrtc/ice-servers"),
};