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:
18
Makefile
18
Makefile
@@ -7,7 +7,7 @@
|
||||
PSQL = docker compose exec -T postgres psql -U postgres -d postgres
|
||||
|
||||
.DEFAULT_GOAL := help
|
||||
.PHONY: help up down restart logs api-logs db db-list db-seed db-clear db-reset
|
||||
.PHONY: help up down restart logs api-logs db db-list db-seed db-seed-credentials db-seed-assistants db-clear db-reset
|
||||
|
||||
help: ## 列出所有可用目标
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) \
|
||||
@@ -33,13 +33,19 @@ api-logs: ## 只看后端日志
|
||||
db: ## 进入交互式 psql
|
||||
docker compose exec postgres psql -U postgres -d postgres
|
||||
|
||||
db-list: ## 列出模型凭证(key 明文,仅本地调试用)
|
||||
db-list: ## 列出凭证与助手(key 明文,仅本地调试用)
|
||||
@$(PSQL) -c "SELECT id, name, type, interface_type, is_default FROM provider_credentials ORDER BY id;"
|
||||
@$(PSQL) -c "SELECT id, name, type FROM assistants ORDER BY id;"
|
||||
|
||||
db-seed: ## 灌入 12 条模型凭证种子(幂等)
|
||||
db-seed-credentials: ## 灌入 12 条模型凭证种子(幂等)
|
||||
$(PSQL) < backend/db/seed_credentials.sql
|
||||
|
||||
db-clear: ## 清空模型凭证表
|
||||
$(PSQL) -c "TRUNCATE provider_credentials;"
|
||||
db-seed-assistants: ## 灌入 知识库 + 助手 种子(幂等;依赖凭证已就绪)
|
||||
$(PSQL) < backend/db/seed_assistants.sql
|
||||
|
||||
db-reset: db-clear db-seed ## 清空后重新灌种子
|
||||
db-seed: db-seed-credentials db-seed-assistants ## 全量灌种子(凭证→知识库→助手,幂等,可重复执行)
|
||||
|
||||
db-clear: ## 清空 助手/知识库/凭证 三表(按依赖顺序)
|
||||
$(PSQL) -c "TRUNCATE assistants, knowledge_bases, provider_credentials CASCADE;"
|
||||
|
||||
db-reset: db-clear db-seed ## 清空后重新灌全部种子
|
||||
|
||||
@@ -95,8 +95,13 @@ class Assistant(Base):
|
||||
String(40), ForeignKey("knowledge_bases.id", ondelete="RESTRICT"), nullable=True
|
||||
)
|
||||
|
||||
# 类型专属字段(形态各异):prompt / graph / dify|fastgpt|opencode 端点+key(打码)
|
||||
config: Mapped[dict] = mapped_column(JSON, default=dict)
|
||||
# ---- 瘦类型专属字段(真列,稀疏:按 type 用其中几列) ----
|
||||
prompt: Mapped[str] = mapped_column(String(8192), default="") # prompt / opencode
|
||||
api_url: Mapped[str] = mapped_column(String(512), default="") # dify / fastgpt / opencode
|
||||
api_key: Mapped[str] = mapped_column(String(512), default="") # dify / fastgpt / opencode(打码/哨兵,同凭证)
|
||||
app_id: Mapped[str] = mapped_column(String(128), default="") # fastgpt
|
||||
# workflow 专属:图(nodes/edges)。要版本化时再迁出到 assistant_workflow 表
|
||||
graph: Mapped[dict] = mapped_column(JSON, default=dict)
|
||||
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
|
||||
51
backend/db/seed_assistants.sql
Normal file
51
backend/db/seed_assistants.sql
Normal file
@@ -0,0 +1,51 @@
|
||||
-- 知识库 + 助手种子数据(依赖 seed_credentials.sql 先灌入 model_xxx 凭证)。
|
||||
--
|
||||
-- 用法(从仓库根目录):
|
||||
-- docker compose exec -T postgres psql -U postgres -d postgres < backend/db/seed_assistants.sql
|
||||
-- 或:make db-seed-assistants(凭证已就绪时);make db-seed 会按顺序全灌。
|
||||
--
|
||||
-- 说明:
|
||||
-- * id 固定(kb_001 / asst_001..005)+ ON CONFLICT 幂等,可重复执行。
|
||||
-- * 引用 seed_credentials 的 model_001(LLM)/003(ASR)/005(TTS)/010(Embedding)。
|
||||
-- * 宽表 STI:瘦类型用真列(prompt/api_url/api_key/app_id),workflow 用 graph 列。
|
||||
-- * api_key 在库里明文(读取走 API 才打码),这里填示例占位。
|
||||
|
||||
-- 知识库(引用 Embedding 凭证 model_010)
|
||||
INSERT INTO knowledge_bases (id, name, description, embedding_credential_id, status)
|
||||
VALUES
|
||||
('kb_001', '政务政策知识库', '政策解读 / 办事指南示例库', 'model_010', 'active')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 助手(一种类型一条)
|
||||
INSERT INTO assistants (
|
||||
id, name, type, runtime_mode, greeting, enable_interrupt,
|
||||
llm_credential_id, asr_credential_id, tts_credential_id,
|
||||
realtime_credential_id, knowledge_base_id,
|
||||
prompt, api_url, api_key, app_id, graph
|
||||
) VALUES
|
||||
-- 提示词:llm/asr/tts + 知识库,prompt 真列
|
||||
('asst_001', '政务咨询助手', 'prompt', 'pipeline', '您好,我是政务助手,请问有什么可以帮您?', TRUE,
|
||||
'model_001', 'model_003', 'model_005', NULL, 'kb_001',
|
||||
'你是一名专业的政务咨询助手,回答准确、简洁,不编造政策内容。', '', '', '', '{}'),
|
||||
|
||||
-- 工作流:asr/tts + graph 列(最小图)
|
||||
('asst_002', '热线工单助手', 'workflow', 'pipeline', '', TRUE,
|
||||
NULL, 'model_003', 'model_005', NULL, NULL,
|
||||
'', '', '', '',
|
||||
'{"nodes":[{"id":"1","type":"startCall","position":{"x":0,"y":0},"data":{"name":"开场","prompt":"你好,请问需要办理什么业务?"}}],"edges":[]}'),
|
||||
|
||||
-- Dify:asr/tts + api_url/api_key
|
||||
('asst_003', 'Dify 客服助手', 'dify', 'pipeline', '', TRUE,
|
||||
NULL, 'model_003', 'model_005', NULL, NULL,
|
||||
'', 'https://api.dify.ai/v1', 'app-dify-demo-key', '', '{}'),
|
||||
|
||||
-- FastGPT:asr/tts + app_id/api_url/api_key
|
||||
('asst_004', 'FastGPT 售后助手', 'fastgpt', 'pipeline', '', TRUE,
|
||||
NULL, 'model_003', 'model_005', NULL, NULL,
|
||||
'', 'https://api.fastgpt.in/api/v1/chat/completions', 'fastgpt-demo-key', 'app-fastgpt-001', '{}'),
|
||||
|
||||
-- OpenCode:asr/tts + prompt/api_url/api_key
|
||||
('asst_005', 'OpenCode 代码助手', 'opencode', 'pipeline', '', TRUE,
|
||||
NULL, 'model_003', 'model_005', NULL, NULL,
|
||||
'你是一个代码助手的语音界面,用简洁口语回答工程问题。', 'http://localhost:4096', 'opencode-demo-key', '', '{}')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
@@ -1,7 +1,7 @@
|
||||
"""助手 CRUD。前端「助手列表 / 创建 / 编辑」对接这里。
|
||||
|
||||
模型/KB 以 FK 引用注册表;外部类型(dify/fastgpt/opencode)的 config.apiKey 是私有密钥,
|
||||
读时打码、写时哨兵(复用 services/masking)。
|
||||
模型/KB 以 FK 引用注册表;瘦类型字段直接是真列。外部类型(dify/fastgpt/opencode)的
|
||||
api_key 是私有密钥,读时打码、写时哨兵(列级,复用 services/masking,与凭证表一致)。
|
||||
"""
|
||||
|
||||
import uuid
|
||||
@@ -9,7 +9,7 @@ import uuid
|
||||
from db.models import Assistant
|
||||
from db.session import get_session
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from schemas import EXTERNAL_TYPES, AssistantOut, AssistantUpsert
|
||||
from schemas import AssistantOut, AssistantUpsert
|
||||
from services.masking import mask, resolve_incoming_key
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -17,25 +17,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
router = APIRouter(prefix="/api/assistants", tags=["assistants"])
|
||||
|
||||
|
||||
def _mask_config(type_: str, config: dict) -> dict:
|
||||
"""读取返回前:外部类型的 apiKey 打码,其余原样。"""
|
||||
if type_ in EXTERNAL_TYPES and config.get("apiKey"):
|
||||
return {**config, "apiKey": mask(config["apiKey"])}
|
||||
return config
|
||||
|
||||
|
||||
def _merge_config(type_: str, incoming: dict, stored: dict) -> dict:
|
||||
"""写入时:外部类型若回传打码占位符/空 apiKey → 保留旧 key。"""
|
||||
if type_ in EXTERNAL_TYPES and "apiKey" in incoming:
|
||||
incoming = {
|
||||
**incoming,
|
||||
"apiKey": resolve_incoming_key(
|
||||
incoming.get("apiKey"), stored.get("apiKey", "")
|
||||
),
|
||||
}
|
||||
return incoming
|
||||
|
||||
|
||||
def _to_out(a: Assistant) -> AssistantOut:
|
||||
return AssistantOut(
|
||||
id=a.id,
|
||||
@@ -49,7 +30,11 @@ def _to_out(a: Assistant) -> AssistantOut:
|
||||
tts_credential_id=a.tts_credential_id,
|
||||
realtime_credential_id=a.realtime_credential_id,
|
||||
knowledge_base_id=a.knowledge_base_id,
|
||||
config=_mask_config(a.type, a.config or {}),
|
||||
prompt=a.prompt,
|
||||
api_url=a.api_url,
|
||||
api_key=mask(a.api_key), # 仅外部类型有值;空串 mask 仍是空串
|
||||
app_id=a.app_id,
|
||||
graph=a.graph or {},
|
||||
updated_at=a.updated_at.isoformat() if a.updated_at else None,
|
||||
)
|
||||
|
||||
@@ -87,7 +72,7 @@ async def get_assistant(
|
||||
async def duplicate_assistant(
|
||||
assistant_id: str, session: AsyncSession = Depends(get_session)
|
||||
):
|
||||
"""服务端整行复制:含真实 config(真 key),DB→DB,密钥不经过浏览器,副本可直接用。"""
|
||||
"""服务端整行复制:含真实 api_key,DB→DB,密钥不经过浏览器,副本可直接用。"""
|
||||
src = await session.get(Assistant, assistant_id)
|
||||
if not src:
|
||||
raise HTTPException(404, "助手不存在")
|
||||
@@ -103,7 +88,11 @@ async def duplicate_assistant(
|
||||
tts_credential_id=src.tts_credential_id,
|
||||
realtime_credential_id=src.realtime_credential_id,
|
||||
knowledge_base_id=src.knowledge_base_id,
|
||||
config=dict(src.config or {}), # 浅拷贝,避免与源行共享同一 dict
|
||||
prompt=src.prompt,
|
||||
api_url=src.api_url,
|
||||
api_key=src.api_key, # 真 key,DB→DB
|
||||
app_id=src.app_id,
|
||||
graph=dict(src.graph or {}), # 浅拷贝,避免与源行共享同一 dict
|
||||
)
|
||||
session.add(a)
|
||||
await session.commit()
|
||||
@@ -121,8 +110,8 @@ async def update_assistant(
|
||||
if not a:
|
||||
raise HTTPException(404, "助手不存在")
|
||||
data = body.model_dump()
|
||||
# 外部类型 apiKey 写时哨兵:打码占位符 → 保留旧 key(在改 a.config 前用旧值)
|
||||
data["config"] = _merge_config(body.type, data["config"], a.config or {})
|
||||
# 写时哨兵(列级):回传打码/空 api_key → 保留旧 key
|
||||
data["api_key"] = resolve_incoming_key(data["api_key"], a.api_key)
|
||||
for k, v in data.items():
|
||||
setattr(a, k, v)
|
||||
await session.commit()
|
||||
|
||||
@@ -31,43 +31,17 @@ class CamelModel(BaseModel):
|
||||
)
|
||||
|
||||
|
||||
# ---------- 各类型的 config 形态(JSON 内嵌,按 type 校验) ----------
|
||||
class PromptConfig(CamelModel):
|
||||
prompt: str = ""
|
||||
realtime_model: str = ""
|
||||
|
||||
|
||||
class WorkflowConfig(CamelModel):
|
||||
graph: dict[str, Any] = {} # {nodes, edges, viewport};节点 data 可带 *CredentialId 覆盖
|
||||
|
||||
|
||||
class DifyConfig(CamelModel):
|
||||
api_url: str = ""
|
||||
api_key: str = "" # 写时:占位符/空 → 保留旧
|
||||
|
||||
|
||||
class FastgptConfig(CamelModel):
|
||||
app_id: str = ""
|
||||
api_url: str = ""
|
||||
api_key: str = ""
|
||||
|
||||
|
||||
class OpencodeConfig(CamelModel):
|
||||
prompt: str = ""
|
||||
api_url: str = ""
|
||||
api_key: str = ""
|
||||
|
||||
|
||||
CONFIG_BY_TYPE: dict[str, type[CamelModel]] = {
|
||||
"prompt": PromptConfig,
|
||||
"workflow": WorkflowConfig,
|
||||
"dify": DifyConfig,
|
||||
"fastgpt": FastgptConfig,
|
||||
"opencode": OpencodeConfig,
|
||||
# 各 type 允许的瘦字段(其余字段写入时清零,防止跨类型脏数据)
|
||||
ALLOWED_FIELDS: dict[str, set[str]] = {
|
||||
"prompt": {"prompt"},
|
||||
"workflow": {"graph"},
|
||||
"dify": {"api_url", "api_key"},
|
||||
"fastgpt": {"app_id", "api_url", "api_key"},
|
||||
"opencode": {"prompt", "api_url", "api_key"},
|
||||
}
|
||||
|
||||
|
||||
# ---------- 助手(单表,无版本化;type 可变) ----------
|
||||
# ---------- 助手(单表 STI:瘦类型真列 + workflow 图 JSON 列) ----------
|
||||
class AssistantUpsert(CamelModel):
|
||||
name: str
|
||||
type: AssistantType = "prompt"
|
||||
@@ -82,14 +56,22 @@ class AssistantUpsert(CamelModel):
|
||||
realtime_credential_id: str | None = None
|
||||
knowledge_base_id: str | None = None
|
||||
|
||||
# 类型专属字段;校验后归一为 camelCase 存库
|
||||
config: dict[str, Any] = {}
|
||||
# 瘦类型专属(真列);按 type 取用,无关字段写入时清零
|
||||
prompt: str = ""
|
||||
api_url: str = ""
|
||||
api_key: str = "" # 写时:占位符/空 → 保留旧(哨兵)
|
||||
app_id: str = ""
|
||||
# workflow 专属:图
|
||||
graph: dict[str, Any] = {}
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _normalize_config(self):
|
||||
model = CONFIG_BY_TYPE[self.type]
|
||||
# 按当前 type 校验并裁掉无关字段,统一回 camelCase 落库
|
||||
self.config = model.model_validate(self.config).model_dump(by_alias=True)
|
||||
def _strip_irrelevant_fields(self):
|
||||
allowed = ALLOWED_FIELDS[self.type]
|
||||
for field in ("prompt", "api_url", "api_key", "app_id"):
|
||||
if field not in allowed:
|
||||
setattr(self, field, "")
|
||||
if "graph" not in allowed:
|
||||
self.graph = {}
|
||||
return self
|
||||
|
||||
|
||||
|
||||
@@ -48,20 +48,18 @@ async def resolve_runtime_config(
|
||||
tts = await _resolve(session, assistant.tts_credential_id, "TTS")
|
||||
realtime = await _resolve(session, assistant.realtime_credential_id, "Realtime")
|
||||
|
||||
cfg = assistant.config or {}
|
||||
|
||||
return AssistantConfig(
|
||||
name=assistant.name,
|
||||
greeting=assistant.greeting,
|
||||
# 提示词/工作流类型把 prompt 放 config;外部类型由其平台编排,这里给个兜底
|
||||
prompt=cfg.get("prompt") or "你是一个有帮助的助手。",
|
||||
# prompt 现在是真列;外部类型由其平台编排,这里给个兜底
|
||||
prompt=assistant.prompt or "你是一个有帮助的助手。",
|
||||
runtimeMode=assistant.runtime_mode, # type: ignore[arg-type]
|
||||
enableInterrupt=assistant.enable_interrupt,
|
||||
# 模型/音色:凭证的模型ID优先
|
||||
model=(llm.model_id if llm else ""),
|
||||
asr=(stt.model_id if stt else ""),
|
||||
voice=cfg.get("voice", ""), # 音色不再是独立列,若 config 带则用,否则 .env 兜底
|
||||
realtimeModel=(realtime.model_id if realtime else cfg.get("realtimeModel", "")),
|
||||
voice="", # 音色无独立列,留空 → service_factory 回退 .env TTS_VOICE
|
||||
realtimeModel=(realtime.model_id if realtime else ""),
|
||||
# 运行时连接信息(真 key + url):凭证优先,否则 .env 兜底
|
||||
llm_api_key=(llm.api_key if llm else config.LLM_API_KEY),
|
||||
llm_base_url=(llm.api_url if llm else config.LLM_BASE_URL),
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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"),
|
||||
|
||||
Reference in New Issue
Block a user