Refactor assistant configuration and database seeding

- Update Makefile to include new database seed commands for assistants and credentials.
- Refactor assistant model to use explicit fields instead of a config dictionary, improving data integrity and clarity.
- Implement new seeding SQL script for assistants, ensuring dependencies on credentials are respected.
- Modify backend routes and frontend components to accommodate the new assistant structure, including direct field access for prompt, API URL, and keys.
- Enhance the AssistantPage component to handle the new data structure and streamline the save process for different assistant types.
This commit is contained in:
Xin Wang
2026-06-09 10:37:29 +08:00
parent 23e1cf5d42
commit 519cc0fefe
8 changed files with 197 additions and 185 deletions

View File

@@ -339,7 +339,7 @@ export function AssistantPage() {
setForm({
name: a.name,
greeting: a.greeting,
prompt: typeof a.config.prompt === "string" ? a.config.prompt : "",
prompt: a.prompt,
runtimeMode: a.runtimeMode,
realtimeModel: a.realtimeCredentialId ?? "",
model: a.llmCredentialId ?? "",
@@ -464,70 +464,32 @@ export function AssistantPage() {
}
}
// 保存提示词助手(新建 POST / 编辑 PUT)
async function handleSavePrompt() {
setSaving(true);
setSaveError(null);
const payload: AssistantUpsert = {
name: form.name.trim(),
// 平铺字段的空白 upsert,各类型按需覆盖(后端会按 type 清掉无关字段)
function baseUpsert(over: Partial<AssistantUpsert>): AssistantUpsert {
return {
name: "",
type: "prompt",
runtimeMode: form.runtimeMode,
greeting: form.greeting,
enableInterrupt: form.enableInterrupt,
llmCredentialId: form.model || null,
asrCredentialId: form.asr || null,
ttsCredentialId: form.voice || null,
realtimeCredentialId: form.realtimeModel || null,
knowledgeBaseId: form.knowledgeBase || null,
config: { prompt: form.prompt },
};
try {
if (editingId) {
await assistantsApi.update(editingId, payload);
} else {
await assistantsApi.create(payload);
}
await loadAssistants();
setView("list");
} catch (error) {
setSaveError(error instanceof Error ? error.message : "保存失败");
} finally {
setSaving(false);
}
}
// config 里取字符串字段(后端回传是 Record<string, unknown>)
const cfgStr = (a: Assistant, key: string) =>
typeof a.config[key] === "string" ? (a.config[key] as string) : "";
// ---- Dify ----
function fillDifyForm(a: Assistant) {
setDifyForm({
name: a.name,
apiUrl: cfgStr(a, "apiUrl"),
apiKey: cfgStr(a, "apiKey"), // 编辑时为打码值,不改则原样回传(后端哨兵保留旧 key)
asr: a.asrCredentialId ?? "",
voice: a.ttsCredentialId ?? "",
enableInterrupt: a.enableInterrupt,
});
}
async function handleSaveDify() {
setSaving(true);
setSaveError(null);
const payload: AssistantUpsert = {
name: difyForm.name.trim(),
type: "dify",
runtimeMode: "pipeline",
greeting: "",
enableInterrupt: difyForm.enableInterrupt,
llmCredentialId: null, // LLM 由 Dify 应用提供
asrCredentialId: difyForm.asr || null,
ttsCredentialId: difyForm.voice || null,
enableInterrupt: true,
llmCredentialId: null,
asrCredentialId: null,
ttsCredentialId: null,
realtimeCredentialId: null,
knowledgeBaseId: null,
config: { apiUrl: difyForm.apiUrl, apiKey: difyForm.apiKey },
prompt: "",
apiUrl: "",
apiKey: "",
appId: "",
graph: {},
...over,
};
}
// 统一的保存:新建 POST / 编辑 PUT
async function save(payload: AssistantUpsert) {
setSaving(true);
setSaveError(null);
try {
if (editingId) await assistantsApi.update(editingId, payload);
else await assistantsApi.create(payload);
@@ -540,49 +502,76 @@ export function AssistantPage() {
}
}
function handleSavePrompt() {
void save(
baseUpsert({
name: form.name.trim(),
type: "prompt",
runtimeMode: form.runtimeMode,
greeting: form.greeting,
enableInterrupt: form.enableInterrupt,
llmCredentialId: form.model || null,
asrCredentialId: form.asr || null,
ttsCredentialId: form.voice || null,
realtimeCredentialId: form.realtimeModel || null,
knowledgeBaseId: form.knowledgeBase || null,
prompt: form.prompt,
}),
);
}
// ---- Dify ----
function fillDifyForm(a: Assistant) {
setDifyForm({
name: a.name,
apiUrl: a.apiUrl,
apiKey: a.apiKey, // 编辑时为打码值,不改则原样回传(后端哨兵保留旧 key)
asr: a.asrCredentialId ?? "",
voice: a.ttsCredentialId ?? "",
enableInterrupt: a.enableInterrupt,
});
}
function handleSaveDify() {
void save(
baseUpsert({
name: difyForm.name.trim(),
type: "dify",
enableInterrupt: difyForm.enableInterrupt,
asrCredentialId: difyForm.asr || null,
ttsCredentialId: difyForm.voice || null,
apiUrl: difyForm.apiUrl,
apiKey: difyForm.apiKey,
}),
);
}
// ---- FastGPT ----
function fillFastGptForm(a: Assistant) {
setFastGptForm({
name: a.name,
appId: cfgStr(a, "appId"),
apiUrl: cfgStr(a, "apiUrl"),
apiKey: cfgStr(a, "apiKey"),
appId: a.appId,
apiUrl: a.apiUrl,
apiKey: a.apiKey,
asr: a.asrCredentialId ?? "",
voice: a.ttsCredentialId ?? "",
enableInterrupt: a.enableInterrupt,
});
}
async function handleSaveFastGpt() {
setSaving(true);
setSaveError(null);
const payload: AssistantUpsert = {
name: fastGptForm.name.trim(),
type: "fastgpt",
runtimeMode: "pipeline",
greeting: "",
enableInterrupt: fastGptForm.enableInterrupt,
llmCredentialId: null,
asrCredentialId: fastGptForm.asr || null,
ttsCredentialId: fastGptForm.voice || null,
realtimeCredentialId: null,
knowledgeBaseId: null,
config: {
function handleSaveFastGpt() {
void save(
baseUpsert({
name: fastGptForm.name.trim(),
type: "fastgpt",
enableInterrupt: fastGptForm.enableInterrupt,
asrCredentialId: fastGptForm.asr || null,
ttsCredentialId: fastGptForm.voice || null,
appId: fastGptForm.appId,
apiUrl: fastGptForm.apiUrl,
apiKey: fastGptForm.apiKey,
},
};
try {
if (editingId) await assistantsApi.update(editingId, payload);
else await assistantsApi.create(payload);
await loadAssistants();
setView("list");
} catch (error) {
setSaveError(error instanceof Error ? error.message : "保存失败");
} finally {
setSaving(false);
}
}),
);
}
const listItems: AssistantListItem[] = assistants.map((a) => ({

View File

@@ -82,7 +82,7 @@ export type AssistantType =
| "opencode";
export type RuntimeMode = "pipeline" | "realtime";
/** 后端 AssistantOut。config 形态随 type 变,故用宽松 record */
/** 后端 AssistantOut(宽表 STI:瘦字段平铺,workflow 用 graph)。apiKey 读时打码 */
export type Assistant = {
id: string;
name: string;
@@ -95,23 +95,15 @@ export type Assistant = {
ttsCredentialId: string | null;
realtimeCredentialId: string | null;
knowledgeBaseId: string | null;
config: Record<string, unknown>;
prompt: string;
apiUrl: string;
apiKey: string;
appId: string;
graph: Record<string, unknown>;
updatedAt?: string | null;
};
export type AssistantUpsert = {
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;
config: Record<string, unknown>;
};
export type AssistantUpsert = Omit<Assistant, "id" | "updatedAt">;
export const assistantsApi = {
list: () => request<Assistant[]>("/api/assistants"),