feat: remove OpenCode assistant mode
This commit is contained in:
@@ -364,8 +364,7 @@
|
||||
"prompt",
|
||||
"workflow",
|
||||
"dify",
|
||||
"fastgpt",
|
||||
"opencode"
|
||||
"fastgpt"
|
||||
],
|
||||
"title": "Type",
|
||||
"type": "string"
|
||||
@@ -534,8 +533,7 @@
|
||||
"prompt",
|
||||
"workflow",
|
||||
"dify",
|
||||
"fastgpt",
|
||||
"opencode"
|
||||
"fastgpt"
|
||||
],
|
||||
"title": "Type",
|
||||
"type": "string"
|
||||
|
||||
@@ -148,7 +148,7 @@ class Assistant(Base):
|
||||
|
||||
id: Mapped[str] = mapped_column(String(40), primary_key=True) # asst_xxx
|
||||
name: Mapped[str] = mapped_column(String(128))
|
||||
# prompt|workflow|dify|fastgpt|opencode;创建后可改
|
||||
# prompt|workflow|dify|fastgpt;创建后可改
|
||||
type: Mapped[str] = mapped_column(String(16), index=True, default="prompt")
|
||||
runtime_mode: Mapped[str] = mapped_column(String(16), default="pipeline")
|
||||
greeting: Mapped[str] = mapped_column(String(2048), default="")
|
||||
@@ -174,12 +174,12 @@ class Assistant(Base):
|
||||
)
|
||||
|
||||
# ---- 瘦类型专属字段(真列,稀疏:按 type 用其中几列) ----
|
||||
prompt: Mapped[str] = mapped_column(Text, default="") # prompt / opencode
|
||||
prompt: Mapped[str] = mapped_column(Text, default="") # prompt
|
||||
dynamic_variable_definitions: Mapped[dict] = mapped_column(JSON, default=dict)
|
||||
built_in_tools: Mapped[dict] = mapped_column(JSONB, default=dict)
|
||||
analysis_config: Mapped[dict] = mapped_column(JSONB, default=dict)
|
||||
api_url: Mapped[str] = mapped_column(String(512), default="") # dify / fastgpt / opencode
|
||||
api_key: Mapped[str] = mapped_column(String(512), default="") # dify / fastgpt / opencode(打码/哨兵)
|
||||
api_url: Mapped[str] = mapped_column(String(512), default="") # dify / fastgpt
|
||||
api_key: Mapped[str] = mapped_column(String(512), default="") # dify / fastgpt(打码/哨兵)
|
||||
app_id: Mapped[str] = mapped_column(String(128), default="") # fastgpt
|
||||
# workflow 专属:图(nodes/edges)。要版本化时再迁出到 assistant_workflow 表
|
||||
graph: Mapped[dict] = mapped_column(JSON, default=dict)
|
||||
|
||||
@@ -22,10 +22,6 @@ INSERT INTO assistants (
|
||||
('asst_004', 'FastGPT 售后助手', 'fastgpt', 'pipeline', '', TRUE,
|
||||
FALSE, NULL,
|
||||
NULL, '', 'https://api.fastgpt.in/api/v1/chat/completions', 'fastgpt-demo-key', 'app-fastgpt-001', '{}',
|
||||
'{"execution_mode":"sequential","opening_mode":"interruptible","entry_mode":"wait_user","actions":[],"opening_message":null}'),
|
||||
('asst_005', 'OpenCode 代码助手', 'opencode', 'pipeline', '', TRUE,
|
||||
FALSE, NULL,
|
||||
NULL, '你是一个代码助手的语音界面,用简洁口语回答工程问题。', 'http://localhost:4096', 'opencode-demo-key', '', '{}',
|
||||
'{"execution_mode":"sequential","opening_mode":"interruptible","entry_mode":"wait_user","actions":[],"opening_message":null}')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
@@ -40,10 +36,7 @@ VALUES
|
||||
('asst_003', 'Agent', 'model_014', '{}'),
|
||||
('asst_004', 'ASR', 'model_002', '{}'),
|
||||
('asst_004', 'TTS', 'model_004', '{}'),
|
||||
('asst_004', 'Agent', 'model_015', '{}'),
|
||||
('asst_005', 'ASR', 'model_002', '{}'),
|
||||
('asst_005', 'TTS', 'model_004', '{}'),
|
||||
('asst_005', 'Agent', 'model_016', '{}')
|
||||
('asst_004', 'Agent', 'model_015', '{}')
|
||||
ON CONFLICT (assistant_id, capability) DO UPDATE SET
|
||||
model_resource_id = EXCLUDED.model_resource_id,
|
||||
updated_at = now();
|
||||
|
||||
@@ -48,9 +48,6 @@ VALUES
|
||||
('model_015', 'FastGPT 售后应用', 'Agent', 'fastgpt',
|
||||
'{"apiUrl":"https://api.fastgpt.in/api/v1/chat/completions","appId":"app-fastgpt-001"}',
|
||||
'{"apiKey":"replace-me"}', FALSE, TRUE, FALSE),
|
||||
('model_016', 'OpenCode 服务', 'Agent', 'opencode',
|
||||
'{"apiUrl":"http://localhost:4096"}',
|
||||
'{"apiKey":"replace-me"}', FALSE, TRUE, FALSE),
|
||||
('model_017', 'Qwen-Audio 3.0 Realtime', 'Realtime', 'qwen-audio-realtime',
|
||||
'{"modelId":"qwen-audio-3.0-realtime-flash","apiUrl":"wss://replace-with-workspace-id.cn-beijing.maas.aliyuncs.com/api-ws/v1/realtime","voice":"longanqian","turnDetection":"server_vad","vadThreshold":0.5,"silenceDurationMs":800,"maxHistoryTurns":20}',
|
||||
'{"apiKey":"replace-me"}', FALSE, TRUE, FALSE)
|
||||
|
||||
+2
-2
@@ -74,7 +74,7 @@ class AssistantConfig(BaseModel):
|
||||
"""运行时配置:前端可见部分(name/prompt/...) + 服务端注入部分(*_api_key/*_base_url)。"""
|
||||
|
||||
name: str = "未命名助手"
|
||||
# prompt|workflow|dify|fastgpt|opencode;决定由哪种「大脑」驱动对话
|
||||
# prompt|workflow|dify|fastgpt;决定由哪种「大脑」驱动对话
|
||||
type: str = "prompt"
|
||||
greeting: str = "您好,我是 AI 视频助手,请问有什么可以帮您?"
|
||||
prompt: str = "你是一个有帮助的助手。"
|
||||
@@ -154,7 +154,7 @@ class AssistantConfig(BaseModel):
|
||||
workflow_model_resources: dict[str, RuntimeModelResource] = Field(default_factory=dict)
|
||||
workflow_knowledge_bases: dict[str, RuntimeKnowledgeBase] = Field(default_factory=dict)
|
||||
|
||||
# 外部托管类型(fastgpt/dify/opencode)的连接信息:context/KB/tools 由对方服务端接管。
|
||||
# 外部托管类型(fastgpt/dify)的连接信息:context/KB/tools 由对方服务端接管。
|
||||
dify_api_url: str = ""
|
||||
dify_api_key: str = ""
|
||||
fastgpt_api_url: str = ""
|
||||
|
||||
@@ -17,6 +17,7 @@ from fastapi import APIRouter, Depends, HTTPException
|
||||
from schemas import (
|
||||
AssistantOut,
|
||||
AssistantUpsert,
|
||||
SUPPORTED_ASSISTANT_TYPES,
|
||||
)
|
||||
from services.auth import require_admin
|
||||
from services.masking import mask, resolve_incoming_key
|
||||
@@ -607,6 +608,8 @@ async def _tool_bindings(session: AsyncSession, assistant_id: str) -> list[dict]
|
||||
|
||||
|
||||
async def _to_out(session: AsyncSession, assistant: Assistant) -> AssistantOut:
|
||||
if assistant.type not in SUPPORTED_ASSISTANT_TYPES:
|
||||
raise HTTPException(410, "该助手类型已下线")
|
||||
return AssistantOut(
|
||||
id=assistant.id,
|
||||
name=assistant.name,
|
||||
@@ -647,7 +650,11 @@ async def _to_out(session: AsyncSession, assistant: Assistant) -> AssistantOut:
|
||||
@router.get("", response_model=list[AssistantOut])
|
||||
async def list_assistants(session: AsyncSession = Depends(get_session)):
|
||||
rows = (
|
||||
await session.execute(select(Assistant).order_by(Assistant.updated_at.desc()))
|
||||
await session.execute(
|
||||
select(Assistant)
|
||||
.where(Assistant.type.in_(SUPPORTED_ASSISTANT_TYPES))
|
||||
.order_by(Assistant.updated_at.desc())
|
||||
)
|
||||
).scalars().all()
|
||||
return [await _to_out(session, assistant) for assistant in rows]
|
||||
|
||||
@@ -703,6 +710,8 @@ async def duplicate_assistant(
|
||||
source = await session.get(Assistant, assistant_id)
|
||||
if not source:
|
||||
raise HTTPException(404, "助手不存在")
|
||||
if source.type not in SUPPORTED_ASSISTANT_TYPES:
|
||||
raise HTTPException(410, "该助手类型已下线")
|
||||
assistant = Assistant(
|
||||
id=f"asst_{uuid.uuid4().hex[:12]}",
|
||||
name=f"{source.name} 副本",
|
||||
|
||||
+4
-4
@@ -9,7 +9,7 @@ from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
import re
|
||||
from typing import Annotated, Any, Literal, Union
|
||||
from typing import Annotated, Any, Literal, Union, get_args
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
||||
from pydantic.alias_generators import to_camel
|
||||
@@ -17,7 +17,8 @@ from pydantic.alias_generators import to_camel
|
||||
RuntimeMode = Literal["pipeline", "realtime"]
|
||||
VisionPromptMode = Literal["fixed", "runtime", "guided"]
|
||||
ModelType = Literal["LLM", "ASR", "TTS", "Realtime", "Embedding", "Agent"]
|
||||
AssistantType = Literal["prompt", "workflow", "dify", "fastgpt", "opencode"]
|
||||
AssistantType = Literal["prompt", "workflow", "dify", "fastgpt"]
|
||||
SUPPORTED_ASSISTANT_TYPES = get_args(AssistantType)
|
||||
TurnEndStrategy = Literal["silence", "smart_turn"]
|
||||
KnowledgeRetrievalMode = Literal["automatic", "on_demand"]
|
||||
ToolType = Literal["http", "mcp", "client"]
|
||||
@@ -34,7 +35,7 @@ PromptOpeningMode = Literal["interruptible", "playback", "confirmation"]
|
||||
PrimitiveValue = str | int | float | bool
|
||||
|
||||
# 外部应用类型:其 config.apiKey 是该助手私有密钥,读时打码 / 写时哨兵
|
||||
EXTERNAL_TYPES = {"dify", "fastgpt", "opencode"}
|
||||
EXTERNAL_TYPES = {"dify", "fastgpt"}
|
||||
|
||||
# Prompt 和 Workflow 支持 realtime;外部托管大脑只走 pipeline。
|
||||
# 与 services.brains 各 BrainSpec.supported_runtime_modes 对齐(此处独立声明,
|
||||
@@ -131,7 +132,6 @@ ALLOWED_FIELDS: dict[str, set[str]] = {
|
||||
"workflow": {"graph"},
|
||||
"dify": {"api_url", "api_key"},
|
||||
"fastgpt": {"app_id", "api_url", "api_key"},
|
||||
"opencode": {"prompt", "api_url", "api_key"},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ async def _agent_resource_for(
|
||||
assistant_id: str,
|
||||
interface_type: str,
|
||||
) -> ModelResource | None:
|
||||
if interface_type not in {"dify", "fastgpt", "opencode"}:
|
||||
if interface_type not in {"dify", "fastgpt"}:
|
||||
return None
|
||||
binding = await session.get(AssistantModelBinding, (assistant_id, "Agent"))
|
||||
resource_id = binding.model_resource_id if binding else None
|
||||
|
||||
@@ -95,12 +95,6 @@ INTERFACE_DEFINITIONS: list[dict] = [
|
||||
"capability": "Agent",
|
||||
"fields": AGENT_COMMON + [field("appId", "App ID", required=True)],
|
||||
},
|
||||
{
|
||||
"interface_type": "opencode",
|
||||
"name": "OpenCode Agent",
|
||||
"capability": "Agent",
|
||||
"fields": AGENT_COMMON,
|
||||
},
|
||||
{
|
||||
"interface_type": "stepfun-realtime",
|
||||
"name": "StepFun StepAudio Realtime",
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
"""Removed assistant types must fail cleanly without changing stored data."""
|
||||
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from fastapi import HTTPException
|
||||
from pydantic import ValidationError
|
||||
|
||||
from routes.assistants import duplicate_assistant, get_assistant
|
||||
from schemas import AssistantUpsert
|
||||
|
||||
|
||||
class AssistantTypeTests(unittest.TestCase):
|
||||
def test_removed_type_is_rejected(self):
|
||||
with self.assertRaises(ValidationError):
|
||||
AssistantUpsert(name="旧助手", type="opencode")
|
||||
|
||||
def test_remaining_types_can_be_created(self):
|
||||
for assistant_type in ("prompt", "workflow", "dify", "fastgpt"):
|
||||
with self.subTest(type=assistant_type):
|
||||
assistant = AssistantUpsert(name="助手", type=assistant_type)
|
||||
self.assertEqual(assistant.type, assistant_type)
|
||||
|
||||
|
||||
class RetiredAssistantTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_old_record_returns_gone_without_writes(self):
|
||||
for operation in (get_assistant, duplicate_assistant):
|
||||
with self.subTest(operation=operation.__name__):
|
||||
session = AsyncMock()
|
||||
session.get.return_value = SimpleNamespace(type="opencode")
|
||||
with self.assertRaises(HTTPException) as error:
|
||||
await operation("legacy_assistant", session)
|
||||
self.assertEqual(error.exception.status_code, 410)
|
||||
session.execute.assert_not_called()
|
||||
session.add.assert_not_called()
|
||||
session.commit.assert_not_called()
|
||||
session.delete.assert_not_called()
|
||||
@@ -185,7 +185,7 @@ class BrainRegistryTests(unittest.TestCase):
|
||||
|
||||
def test_unknown_brain_does_not_fallback_to_prompt(self):
|
||||
with self.assertRaisesRegex(ValueError, "尚未实现"):
|
||||
build_brain(AssistantConfig(type="opencode"))
|
||||
build_brain(AssistantConfig(type="unknown"))
|
||||
|
||||
def test_workflow_realtime_is_accepted_at_schema_boundary(self):
|
||||
assistant = AssistantUpsert(
|
||||
|
||||
@@ -31,11 +31,10 @@ icon: plug
|
||||
|
||||
## 外部 Agent
|
||||
|
||||
创建助手时选择 Dify、FastGPT 或 OpenCode:
|
||||
创建助手时选择 「智能体平台」,接入 Dify 或 FastGPT:
|
||||
|
||||
- Dify:填写服务地址和 API Key。
|
||||
- FastGPT:额外填写应用 ID。
|
||||
- OpenCode:填写服务地址、API Key 和可选提示词。
|
||||
|
||||
外部 Agent 的上下文、知识库和工具由对方服务处理。
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ icon: sliders-horizontal
|
||||
| --- | --- | --- | --- |
|
||||
| Prompt | 已实现 | 已实现,有 Provider 限制 | 提示词驱动,可绑定模型及受支持的工具。 |
|
||||
| Workflow | 已实现 | 已实现,有 Provider 限制 | 用节点编排对话、状态和动作。 |
|
||||
| Dify / FastGPT / OpenCode | 已实现 | 未实现 | 对话处理交给外部 Agent,语音输入输出仍由平台管线处理。 |
|
||||
| Dify / FastGPT | 已实现 | 未实现 | 对话处理交给外部 Agent,语音输入输出仍由平台管线处理。 |
|
||||
|
||||
创建助手时,**Pipeline** 需要分别绑定 ASR、LLM、TTS;外部 Agent 助手将 LLM 环节交给外部服务。**Realtime** 绑定一个端到端语音模型资源。模型资源按 `interface_type` 选择适配,填写一个看似兼容的 URL 不会自动生成新的运行适配。
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ icon: list-checks
|
||||
| Pipecat Pipeline:ASR → LLM/外部 Agent → TTS | 已实现 | 需要匹配的模型接口、有效凭证和实际联调。 |
|
||||
| Pipecat Pipeline 的 Silero VAD、静音结束、Local Smart Turn、打断 | 已实现 | 可调整参数与策略;模型实现目前固定。 |
|
||||
| StepFun StepAudio、Qwen Audio Realtime | 已实现 | Provider 的轮次、工具与视觉能力各不相同。 |
|
||||
| Prompt、Workflow、Dify/FastGPT/OpenCode 助手 | 已实现 | 外部 Agent 只支持 Pipeline;公开 `/v1/models` 当前不列出 OpenCode。 |
|
||||
| Prompt、Workflow、Dify/FastGPT 助手 | 已实现 | 外部 Agent 只支持 Pipeline。 |
|
||||
| OpenAI Realtime 兼容子集的 WebSocket/WebRTC | 已实现 | 是对外协议适配,不代表所有 OpenAI 字段或底层 OpenAI 模型可用。 |
|
||||
| Responses 文本接口 | 已实现 | 面向可通过该接口调用的 Pipeline 助手。 |
|
||||
| WebRTC 可选摄像头与视觉处理 | 有条件可用 | 需要助手授权视觉、客户端协商视频轨道,并受运行模式限制。 |
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
Boxes,
|
||||
Brain,
|
||||
Copy,
|
||||
MessageSquareText,
|
||||
MoreHorizontal,
|
||||
Pencil,
|
||||
Plus,
|
||||
@@ -14,7 +13,6 @@ import {
|
||||
ChevronDown,
|
||||
LayoutTemplate,
|
||||
Save,
|
||||
Terminal,
|
||||
Loader2,
|
||||
} from "lucide-react";
|
||||
|
||||
@@ -76,8 +74,6 @@ import {
|
||||
EditableTitle,
|
||||
EditorBackButton,
|
||||
ResourceSelectField,
|
||||
TextAreaField,
|
||||
ToggleRow,
|
||||
} from "@/components/assistant-editor/editor-controls";
|
||||
import type { AssistantForm } from "@/components/assistant-editor/types";
|
||||
import { PromptEditor } from "@/components/assistant-editor/prompt-editor";
|
||||
@@ -95,27 +91,13 @@ type AgentPlatformForm = {
|
||||
turnConfig: TurnConfig;
|
||||
};
|
||||
|
||||
type OpenCodeForm = {
|
||||
name: string;
|
||||
prompt: string;
|
||||
agent: string;
|
||||
model: string;
|
||||
asr: string;
|
||||
voice: string;
|
||||
enableInterrupt: boolean;
|
||||
turnConfig: TurnConfig;
|
||||
visionEnabled: boolean;
|
||||
visionModelResourceId: string;
|
||||
};
|
||||
|
||||
type AssistantType = "提示词" | "工作流" | "Dify" | "FastGPT" | "OpenCode";
|
||||
type AssistantType = "提示词" | "工作流" | "Dify" | "FastGPT";
|
||||
|
||||
const assistantTypes: AssistantType[] = [
|
||||
"提示词",
|
||||
"工作流",
|
||||
"Dify",
|
||||
"FastGPT",
|
||||
"OpenCode",
|
||||
];
|
||||
|
||||
function defaultKnowledgeRetrievalConfig(): KnowledgeRetrievalConfig {
|
||||
@@ -132,7 +114,6 @@ const typeToLabel: Record<ApiAssistantType, AssistantType> = {
|
||||
workflow: "工作流",
|
||||
dify: "Dify",
|
||||
fastgpt: "FastGPT",
|
||||
opencode: "OpenCode",
|
||||
};
|
||||
|
||||
// 后端 type → 编辑器视图(工作流暂为占位页)
|
||||
@@ -140,7 +121,6 @@ const typeToView = {
|
||||
prompt: "create",
|
||||
dify: "create-agent-platform",
|
||||
fastgpt: "create-agent-platform",
|
||||
opencode: "create-opencode",
|
||||
workflow: "workflow",
|
||||
} as const;
|
||||
|
||||
@@ -201,20 +181,6 @@ function blankAgentPlatformForm(name: string): AgentPlatformForm {
|
||||
};
|
||||
}
|
||||
|
||||
function blankOpenCodeForm(name: string): OpenCodeForm {
|
||||
return {
|
||||
name,
|
||||
prompt: "",
|
||||
agent: "",
|
||||
model: "",
|
||||
asr: "",
|
||||
voice: "",
|
||||
enableInterrupt: true,
|
||||
turnConfig: defaultTurnConfig(),
|
||||
visionEnabled: false,
|
||||
visionModelResourceId: "",
|
||||
};
|
||||
}
|
||||
function formatTimestamp(iso?: string | null): string {
|
||||
if (!iso) return "";
|
||||
const d = new Date(iso);
|
||||
@@ -251,9 +217,6 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
useState<AgentPlatformForm>(() =>
|
||||
blankAgentPlatformForm(""),
|
||||
);
|
||||
const [openCodeForm, setOpenCodeForm] = useState<OpenCodeForm>(() =>
|
||||
blankOpenCodeForm(""),
|
||||
);
|
||||
const [assistants, setAssistants] = useState<Assistant[]>([]);
|
||||
const [listLoading, setListLoading] = useState(true);
|
||||
const [listError, setListError] = useState<string | null>(null);
|
||||
@@ -366,7 +329,7 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
label: c.name,
|
||||
supportImageInput: c.supportImageInput,
|
||||
}));
|
||||
const agentOptions = (interfaceType: "dify" | "fastgpt" | "opencode") =>
|
||||
const agentOptions = (interfaceType: AgentPlatform) =>
|
||||
modelResources
|
||||
.filter(
|
||||
(resource) =>
|
||||
@@ -405,18 +368,6 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
}
|
||||
}
|
||||
|
||||
function handleOpenCodeVisionEnabledChange(enabled: boolean) {
|
||||
updateOpenCodeForm("visionEnabled", enabled);
|
||||
if (!enabled) updateOpenCodeForm("visionModelResourceId", "");
|
||||
}
|
||||
|
||||
function handleOpenCodeModelChange(value: string) {
|
||||
updateOpenCodeForm("model", value);
|
||||
if (openCodeForm.visionModelResourceId === value) {
|
||||
updateOpenCodeForm("visionModelResourceId", "");
|
||||
}
|
||||
}
|
||||
|
||||
function startCreate() {
|
||||
router.push("/assistants/new");
|
||||
}
|
||||
@@ -541,10 +492,6 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
const next = { ...agentPlatformForm };
|
||||
setAgentPlatformForm(next);
|
||||
setSavedSnapshot(JSON.stringify(next));
|
||||
} else if (view === "create-opencode") {
|
||||
const next = { ...openCodeForm };
|
||||
setOpenCodeForm(next);
|
||||
setSavedSnapshot(JSON.stringify(next));
|
||||
} else if (view === "workflow") {
|
||||
setSavedSnapshot(
|
||||
JSON.stringify({
|
||||
@@ -640,24 +587,6 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
);
|
||||
}
|
||||
|
||||
// ---- OpenCode ----
|
||||
function fillOpenCodeForm(a: Assistant): OpenCodeForm {
|
||||
const next: OpenCodeForm = {
|
||||
name: a.name,
|
||||
prompt: a.prompt,
|
||||
agent: a.modelResourceIds.Agent ?? "",
|
||||
model: a.modelResourceIds.LLM ?? "",
|
||||
asr: a.modelResourceIds.ASR ?? "",
|
||||
voice: a.modelResourceIds.TTS ?? "",
|
||||
enableInterrupt: a.enableInterrupt,
|
||||
turnConfig: normalizeTurnConfig(a.turnConfig),
|
||||
visionEnabled: a.visionEnabled,
|
||||
visionModelResourceId: a.visionModelResourceId ?? "",
|
||||
};
|
||||
setOpenCodeForm(next);
|
||||
return next;
|
||||
}
|
||||
|
||||
// 编辑模式:按路由中的 id 拉取助手,回填对应类型的表单后再切换视图
|
||||
useEffect(() => {
|
||||
if (props.mode !== "edit" || !editingId) return;
|
||||
@@ -674,8 +603,6 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
assistant.type === "dify"
|
||||
) {
|
||||
setSavedSnapshot(JSON.stringify(fillAgentPlatformForm(assistant)));
|
||||
} else if (assistant.type === "opencode") {
|
||||
setSavedSnapshot(JSON.stringify(fillOpenCodeForm(assistant)));
|
||||
} else {
|
||||
// 工作流:回填名称与图(空图回落到默认 开始→智能体→结束)
|
||||
const graph =
|
||||
@@ -770,26 +697,6 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
setWorkflowSettings,
|
||||
]);
|
||||
|
||||
function handleSaveOpenCode() {
|
||||
void save(
|
||||
baseUpsert({
|
||||
name: openCodeForm.name.trim(),
|
||||
type: "opencode",
|
||||
enableInterrupt: openCodeForm.enableInterrupt,
|
||||
turnConfig: openCodeForm.turnConfig,
|
||||
visionEnabled: openCodeForm.visionEnabled,
|
||||
visionModelResourceId: openCodeForm.visionModelResourceId || null,
|
||||
modelResourceIds: {
|
||||
...(openCodeForm.agent ? { Agent: openCodeForm.agent } : {}),
|
||||
...(openCodeForm.model ? { LLM: openCodeForm.model } : {}),
|
||||
...(openCodeForm.asr ? { ASR: openCodeForm.asr } : {}),
|
||||
...(openCodeForm.voice ? { TTS: openCodeForm.voice } : {}),
|
||||
},
|
||||
prompt: openCodeForm.prompt,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function handleSaveWorkflow() {
|
||||
void save(
|
||||
baseUpsert({
|
||||
@@ -822,8 +729,6 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
? JSON.stringify(form)
|
||||
: view === "create-agent-platform"
|
||||
? JSON.stringify(agentPlatformForm)
|
||||
: view === "create-opencode"
|
||||
? JSON.stringify(openCodeForm)
|
||||
: view === "workflow"
|
||||
? JSON.stringify({
|
||||
name: workflowName,
|
||||
@@ -926,15 +831,6 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
}));
|
||||
}
|
||||
|
||||
function updateOpenCodeForm<K extends keyof OpenCodeForm>(
|
||||
key: K,
|
||||
value: OpenCodeForm[K],
|
||||
) {
|
||||
setOpenCodeForm((prev) => ({
|
||||
...prev,
|
||||
[key]: value,
|
||||
}));
|
||||
}
|
||||
if (view === "loading") {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
@@ -1325,139 +1221,6 @@ export function AssistantPage(props: AssistantPageProps) {
|
||||
);
|
||||
}
|
||||
|
||||
if (view === "create-opencode") {
|
||||
return (
|
||||
<div className="-mt-6 flex h-full flex-col gap-4">
|
||||
<div className="flex shrink-0 items-center justify-between gap-6 border-b border-hairline pb-3 pt-1">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<EditorBackButton onClick={() => router.push("/assistants")} />
|
||||
<EditableTitle
|
||||
value={openCodeForm.name}
|
||||
onChange={(value) => updateOpenCodeForm("name", value)}
|
||||
/>
|
||||
<AssistantIdentity assistantId={editingId} />
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 gap-2">
|
||||
{saveError && (
|
||||
<span className="self-center text-xs text-destructive">
|
||||
{saveError}
|
||||
</span>
|
||||
)}
|
||||
<Button
|
||||
className="gap-2"
|
||||
disabled={
|
||||
saving ||
|
||||
!dirty ||
|
||||
!openCodeForm.name.trim() ||
|
||||
!openCodeForm.agent
|
||||
}
|
||||
onClick={() => void handleSaveOpenCode()}
|
||||
>
|
||||
{saving ? (
|
||||
<Loader2 size={16} className="animate-spin" />
|
||||
) : (
|
||||
<Save size={16} />
|
||||
)}
|
||||
保存
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex min-h-0 flex-1 gap-4">
|
||||
<div className="scrollbar-subtle min-w-0 flex-1 space-y-3 overflow-y-auto pr-1">
|
||||
<SectionCard
|
||||
icon={<Terminal size={15} />}
|
||||
title="OpenCode 服务配置"
|
||||
description="从「模型资源」中选择 OpenCode 服务资源。"
|
||||
>
|
||||
<ResourceSelectField
|
||||
label="OpenCode 服务"
|
||||
value={openCodeForm.agent}
|
||||
onChange={(value) => updateOpenCodeForm("agent", value)}
|
||||
options={agentOptions("opencode")}
|
||||
noneLabel="请选择"
|
||||
/>
|
||||
</SectionCard>
|
||||
|
||||
<SectionCard
|
||||
icon={<MessageSquareText size={15} />}
|
||||
title="提示词"
|
||||
description="描述助手的角色、能力和回答要求"
|
||||
>
|
||||
<TextAreaField
|
||||
value={openCodeForm.prompt}
|
||||
onChange={(value) => updateOpenCodeForm("prompt", value)}
|
||||
placeholder="请输入提示词,描述助手的角色、能力和回答要求"
|
||||
rows={8}
|
||||
/>
|
||||
</SectionCard>
|
||||
|
||||
<SectionCard
|
||||
icon={<Brain size={15} />}
|
||||
title="模型与语音配置"
|
||||
description="配置 OpenCode 使用的大语言模型、语音识别与语音合成资源。"
|
||||
>
|
||||
<ToggleRow
|
||||
title="视觉理解"
|
||||
hint="开启后,开始对话时会允许助手按需理解当前视频画面。视觉模型选「模型自己」时,大语言模型本身必须支持图片输入。"
|
||||
checked={openCodeForm.visionEnabled}
|
||||
onChange={handleOpenCodeVisionEnabledChange}
|
||||
/>
|
||||
{openCodeForm.visionEnabled && (
|
||||
<ResourceSelectField
|
||||
label="视觉模型"
|
||||
value={openCodeForm.visionModelResourceId}
|
||||
onChange={(value) =>
|
||||
updateOpenCodeForm("visionModelResourceId", value)
|
||||
}
|
||||
options={visionModelOptionsFor(openCodeForm.model)}
|
||||
noneLabel="模型自己"
|
||||
/>
|
||||
)}
|
||||
<ResourceSelectField
|
||||
label="大语言模型"
|
||||
value={openCodeForm.model}
|
||||
onChange={handleOpenCodeModelChange}
|
||||
options={credOptions("LLM")}
|
||||
noneLabel="无"
|
||||
/>
|
||||
<ResourceSelectField
|
||||
label="语音识别"
|
||||
value={openCodeForm.asr}
|
||||
onChange={(value) => updateOpenCodeForm("asr", value)}
|
||||
options={credOptions("ASR")}
|
||||
noneLabel="无"
|
||||
/>
|
||||
<ResourceSelectField
|
||||
label="语音合成"
|
||||
value={openCodeForm.voice}
|
||||
onChange={(value) => updateOpenCodeForm("voice", value)}
|
||||
options={credOptions("TTS")}
|
||||
noneLabel="无"
|
||||
/>
|
||||
</SectionCard>
|
||||
|
||||
<SectionCard
|
||||
icon={<Sparkles size={15} />}
|
||||
title="交互策略"
|
||||
description="设置实时视频对话时的交互体验"
|
||||
>
|
||||
<TurnConfigEditor
|
||||
enabled={openCodeForm.enableInterrupt}
|
||||
config={openCodeForm.turnConfig}
|
||||
onEnabledChange={(checked) => updateOpenCodeForm("enableInterrupt", checked)}
|
||||
onConfigChange={(config) => updateOpenCodeForm("turnConfig", config)}
|
||||
/>
|
||||
</SectionCard>
|
||||
</div>
|
||||
|
||||
<DebugDrawer assistantId={editingId} hasUnsavedChanges={dirty} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PromptEditor
|
||||
assistantId={editingId}
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
Loader2,
|
||||
MessageSquareText,
|
||||
Rocket,
|
||||
Terminal,
|
||||
Workflow,
|
||||
} from "lucide-react";
|
||||
|
||||
@@ -16,7 +15,6 @@ import {
|
||||
ListPageLayout,
|
||||
ListPageSection,
|
||||
} from "@/components/layout/list-page-layout";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
@@ -31,7 +29,7 @@ import {
|
||||
} from "@/lib/api";
|
||||
import { defaultTurnConfig } from "@/lib/turn-config";
|
||||
|
||||
type BuildMethod = "提示词" | "工作流" | "智能体平台" | "OpenCode";
|
||||
type BuildMethod = "提示词" | "工作流" | "智能体平台";
|
||||
|
||||
const typeFromBuildMethod: Record<
|
||||
Exclude<BuildMethod, "智能体平台">,
|
||||
@@ -39,7 +37,6 @@ const typeFromBuildMethod: Record<
|
||||
> = {
|
||||
提示词: "prompt",
|
||||
工作流: "workflow",
|
||||
OpenCode: "opencode",
|
||||
};
|
||||
|
||||
type AssistantTypeOption = {
|
||||
@@ -47,8 +44,6 @@ type AssistantTypeOption = {
|
||||
label: string;
|
||||
description: string;
|
||||
icon: React.ReactNode;
|
||||
/** 提示词、工作流、智能体平台已落地;OpenCode 暂时显示即将上线 */
|
||||
available: boolean;
|
||||
};
|
||||
|
||||
const assistantTypeOptions: AssistantTypeOption[] = [
|
||||
@@ -57,14 +52,12 @@ const assistantTypeOptions: AssistantTypeOption[] = [
|
||||
label: "使用提示词构建",
|
||||
description: "通过提示词、模型与语音快速搭建对话助手,适合大多数场景。",
|
||||
icon: <MessageSquareText size={20} />,
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
type: "工作流",
|
||||
label: "使用工作流构建",
|
||||
description: "用可视化编排串联多个节点,适合多步骤、带分支的复杂流程。",
|
||||
icon: <Workflow size={20} />,
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
type: "智能体平台",
|
||||
@@ -72,14 +65,6 @@ const assistantTypeOptions: AssistantTypeOption[] = [
|
||||
description:
|
||||
"接入 Dify、FastGPT 等智能体平台,复用平台中的提示词、知识库和工作流配置。",
|
||||
icon: <Boxes size={20} />,
|
||||
available: true,
|
||||
},
|
||||
{
|
||||
type: "OpenCode",
|
||||
label: "使用 OpenCode 构建",
|
||||
description: "对接 OpenCode 服务,通过提示词驱动代码助手并支持实时语音对话。",
|
||||
icon: <Terminal size={20} />,
|
||||
available: false,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -203,16 +188,6 @@ export function CreateAssistantPage() {
|
||||
title={option.label}
|
||||
description={option.description}
|
||||
onClick={() => setDraftType(option.type)}
|
||||
unselectedTrailing={
|
||||
!option.available ? (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="h-6 bg-surface-strong px-3 text-xs text-muted-foreground"
|
||||
>
|
||||
即将上线
|
||||
</Badge>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -263,8 +263,7 @@ export type AssistantType =
|
||||
| "prompt"
|
||||
| "workflow"
|
||||
| "dify"
|
||||
| "fastgpt"
|
||||
| "opencode";
|
||||
| "fastgpt";
|
||||
export type RuntimeMode = "pipeline" | "realtime";
|
||||
export type TurnConfig = {
|
||||
bargeIn: {
|
||||
|
||||
Reference in New Issue
Block a user