feat: 输入框内的模型仅作临时覆盖

This commit is contained in:
Wenjie Zhang 2026-06-05 21:02:13 +08:00
parent 83656a8eb5
commit 8582b94f68
11 changed files with 404 additions and 67 deletions

View File

@ -58,8 +58,11 @@
- [x] 将 Qwen-Image 修改为 Skill
- [x] 现在输入区域对于不同 mention 的渲染的 ICON 和 human-message 里面的渲染的 ICON;
- [x] 节点的颜色按照 label 的分类来
- [ ] 添加推荐 Skill 的列表,点击可以直接安装,实际上是替代了填入搜索的名称,并触发拉取,这个似乎只需要在前端修改就可以了
- [x] 添加推荐 Skill 的列表,点击可以直接安装,实际上是替代了填入搜索的名称,并触发拉取,这个似乎只需要在前端修改就可以了
- [ ] 智能体调用的时候模型支持单独配置
- [ ] 添加通用任务的子智能体
- [ ] 添加深度研究智能体
# bug
- [ ] 知识库、知识图谱、评估基准的空状态是不一样的,需要统一
- [x] 知识库、知识图谱、评估基准的空状态是不一样的,需要统一

View File

@ -13,6 +13,7 @@ from sqlalchemy import select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession
from yuxi.agents.buildin import agent_manager
from yuxi.models.providers.cache import model_cache
from yuxi.repositories.agent_repository import AgentRepository
from yuxi.repositories.agent_run_repository import TERMINAL_RUN_STATUSES, AgentRunRepository
from yuxi.repositories.conversation_repository import ConversationRepository
@ -34,6 +35,30 @@ SSE_MAX_CONNECTION_MINUTES = int(os.getenv("RUN_SSE_MAX_CONNECTION_MINUTES", "30
SSE_POLL_INTERVAL_SECONDS = float(os.getenv("RUN_SSE_POLL_INTERVAL_SECONDS", "1.0"))
def _validate_model_spec(model_spec: str | None) -> str | None:
"""校验对话级模型覆盖:未提供则返回 None非法模型直接 422不静默回退。"""
if not model_spec:
return None
info = model_cache.get_model_info(model_spec)
if not info or info.model_type != "chat":
raise HTTPException(status_code=422, detail=f"未找到可用聊天模型: '{model_spec}'")
return model_spec
def _resolve_effective_model_spec(model_spec: str | None, agent_item, agent_backend) -> str | None:
"""解析本次 chat run 实际使用的模型:显式覆盖优先,否则快照智能体当前配置。"""
resolved_model_spec = _validate_model_spec(model_spec)
if resolved_model_spec:
return resolved_model_spec
context = agent_backend.context_schema()
config_json = getattr(agent_item, "config_json", None) or {}
config_context = config_json.get("context") if isinstance(config_json, dict) else {}
if isinstance(config_context, dict):
context.update_from_dict(config_context)
return getattr(context, "model", None)
def _build_run_response(run) -> dict:
return {
"run_id": run.id,
@ -65,6 +90,7 @@ async def create_agent_run_view(
image_content: str | None,
current_uid: str,
db: AsyncSession,
model_spec: str | None = None,
resume: object | None = None,
parent_run_id: str | None = None,
resume_request_id: str | None = None,
@ -91,13 +117,18 @@ async def create_agent_run_view(
agent_item = await agent_repo.get_visible_by_slug(slug=agent_id, user=current_user)
if not agent_item:
raise HTTPException(status_code=404, detail="智能体不存在")
if not agent_manager.get_agent(agent_item.backend_id):
agent_backend = agent_manager.get_agent(agent_item.backend_id)
if not agent_backend:
raise HTTPException(status_code=404, detail=f"智能体后端 {agent_item.backend_id} 不存在")
run_type = "resume" if resume is not None else "chat"
request_id = str(resume_request_id or (meta or {}).get("request_id") or uuid.uuid4())
config = {"thread_id": thread_id, "agent_id": agent_id}
run_repo = AgentRunRepository(db)
# chat快照本次实际模型resume沿用被恢复运行的原始模型保证单次运行模型一致。
resolved_model_spec = (
_resolve_effective_model_spec(model_spec, agent_item, agent_backend) if run_type == "chat" else None
)
if run_type == "resume":
if not parent_run_id:
raise HTTPException(status_code=422, detail="parent_run_id 不能为空")
@ -106,6 +137,7 @@ async def create_agent_run_view(
raise HTTPException(status_code=404, detail="被恢复的运行任务不存在")
if parent_run.status != "interrupted":
raise HTTPException(status_code=409, detail="只有 interrupted run 可以恢复")
resolved_model_spec = (parent_run.input_payload or {}).get("model_spec")
if resume_request_id:
existing_resume = await run_repo.get_resume_run(parent_run_id, resume_request_id)
if existing_resume and existing_resume.uid == str(current_uid):
@ -125,6 +157,7 @@ async def create_agent_run_view(
"run_type": run_type,
"config": config or {},
"image_content": image_content,
"model_spec": resolved_model_spec,
"agent_id": agent_id,
"backend_id": agent_item.backend_id,
"thread_id": thread_id,
@ -155,6 +188,7 @@ async def create_agent_run_view(
"parent_run_id": parent_run_id,
"resume": resume,
"attachments": [],
"model_spec": resolved_model_spec,
}
if run_type == "resume":
input_metadata["source"] = "ask_user_question_resume"

View File

@ -166,6 +166,13 @@ def _json_safe(value: Any) -> Any:
return str(value)
def _apply_model_override(input_context: dict, meta: dict | None) -> None:
"""对话级模型覆盖meta.model_spec 优先于智能体配置的 model。值已在创建 run 时校验。"""
model_spec = (meta or {}).get("model_spec")
if model_spec:
input_context["model"] = model_spec
def _stream_message_key(metadata: dict | None, namespace: list[str], thread_id: str | None) -> tuple[str, str]:
if not isinstance(metadata, dict):
return thread_id or "", "/".join(namespace)
@ -755,6 +762,7 @@ async def agent_chat(
run_id=meta.get("run_id"),
request_id=meta.get("request_id"),
)
_apply_model_override(input_context, meta)
langfuse_run = _build_langfuse_run_context(
current_user=current_user,
thread_id=thread_id,
@ -961,6 +969,7 @@ async def stream_agent_chat(
run_id=meta.get("run_id"),
request_id=meta.get("request_id"),
)
_apply_model_override(input_context, meta)
langfuse_run = _build_langfuse_run_context(
current_user=current_user,
thread_id=thread_id,
@ -1247,6 +1256,7 @@ async def stream_agent_resume(
run_id=meta.get("run_id"),
request_id=meta.get("request_id"),
)
_apply_model_override(input_context, meta)
context = agent.context_schema()
context.update(input_context)
langfuse_run = _build_langfuse_run_context(

View File

@ -312,6 +312,7 @@ async def process_agent_run(ctx, run_id: str):
"uid": user.uid,
"has_image": bool(image_content),
"attachment_file_ids": payload.get("attachment_file_ids") or [],
"model_spec": payload.get("model_spec"),
}
await mark_run_running(run_id)

View File

@ -57,6 +57,7 @@ class AgentRunCreate(BaseModel):
thread_id: str = Field(..., description="会话线程 ID")
meta: dict = Field(default_factory=dict, description="可选,请求追踪信息,例如 request_id")
image_content: str | None = Field(None, description="可选base64 图片内容")
model_spec: str | None = Field(None, description="可选,对话级模型覆盖,优先级高于智能体配置")
resume: Any | None = Field(None, description="可选,恢复 interrupted run 的输入")
parent_run_id: str | None = Field(None, description="可选,被恢复的 run ID")
resume_request_id: str | None = Field(None, description="可选resume 幂等键")
@ -258,6 +259,7 @@ async def create_agent_run(
thread_id=payload.thread_id,
meta=dict(payload.meta or {}),
image_content=payload.image_content,
model_spec=payload.model_spec,
current_uid=str(current_user.uid),
db=db,
resume=payload.resume,

View File

@ -8,6 +8,20 @@ import pytest
import yuxi.services.agent_run_service as agent_run_service
class _FakeContext:
def __init__(self):
self.model = "agent-default-model"
def update_from_dict(self, data: dict):
for key, value in data.items():
if hasattr(self, key):
setattr(self, key, value)
class _FakeBackend:
context_schema = _FakeContext
@pytest.mark.asyncio
async def test_stream_agent_run_events_emits_error_on_db_error(monkeypatch: pytest.MonkeyPatch):
@asynccontextmanager
@ -140,6 +154,7 @@ async def test_create_agent_run_persists_input_before_enqueue(monkeypatch: pytes
raise AssertionError("rollback should not be called")
db = FakeDB()
captured = {}
created_run = SimpleNamespace(
id="",
thread_id="thread-1",
@ -159,6 +174,7 @@ async def test_create_agent_run_persists_input_before_enqueue(monkeypatch: pytes
async def create_run(self, **kwargs):
assert kwargs["request_id"] == "req-1"
assert kwargs["conversation_id"] == 1
captured["input_payload"] = kwargs["input_payload"]
created_run.id = kwargs["run_id"]
return created_run
@ -194,7 +210,7 @@ async def test_create_agent_run_persists_input_before_enqueue(monkeypatch: pytes
async def fake_get_arq_pool():
return Queue()
monkeypatch.setattr(agent_run_service.agent_manager, "get_agent", lambda backend_id: object())
monkeypatch.setattr(agent_run_service.agent_manager, "get_agent", lambda backend_id: _FakeBackend())
monkeypatch.setattr(agent_run_service, "AgentRepository", AgentRepo)
monkeypatch.setattr(agent_run_service, "ConversationRepository", ConvRepo)
monkeypatch.setattr(agent_run_service, "AgentRunRepository", RunRepo)
@ -215,6 +231,8 @@ async def test_create_agent_run_persists_input_before_enqueue(monkeypatch: pytes
assert result["request_id"] == "req-1"
assert db.added[0].run_id == created_run.id
assert db.added[0].request_id == "req-1"
assert captured["input_payload"]["model_spec"] == "agent-default-model"
assert db.added[0].extra_metadata["model_spec"] == "agent-default-model"
@pytest.mark.asyncio
@ -265,7 +283,7 @@ async def test_create_resume_run_marks_input_message_source(monkeypatch: pytest.
async def get_run_for_user(self, run_id: str, uid: str):
assert run_id == "parent-run"
assert uid == "user-1"
return SimpleNamespace(id=run_id, thread_id="thread-1", status="interrupted")
return SimpleNamespace(id=run_id, thread_id="thread-1", status="interrupted", input_payload={})
async def get_resume_run(self, parent_run_id: str, resume_request_id: str):
assert parent_run_id == "parent-run"
@ -314,7 +332,7 @@ async def test_create_resume_run_marks_input_message_source(monkeypatch: pytest.
async def fake_get_arq_pool():
return Queue()
monkeypatch.setattr(agent_run_service.agent_manager, "get_agent", lambda backend_id: object())
monkeypatch.setattr(agent_run_service.agent_manager, "get_agent", lambda backend_id: _FakeBackend())
monkeypatch.setattr(agent_run_service, "AgentRepository", AgentRepo)
monkeypatch.setattr(agent_run_service, "ConversationRepository", ConvRepo)
monkeypatch.setattr(agent_run_service, "AgentRunRepository", RunRepo)
@ -336,3 +354,247 @@ async def test_create_resume_run_marks_input_message_source(monkeypatch: pytest.
assert result["run_id"] == created_run.id
assert db.added[0].message_type == "resume"
assert db.added[0].extra_metadata["source"] == "ask_user_question_resume"
# ==================== 对话级模型覆盖 model_spec ====================
def test_validate_model_spec_returns_none_when_empty():
assert agent_run_service._validate_model_spec(None) is None
assert agent_run_service._validate_model_spec("") is None
def test_validate_model_spec_rejects_unknown_model(monkeypatch: pytest.MonkeyPatch):
monkeypatch.setattr(agent_run_service.model_cache, "get_model_info", lambda spec: None)
with pytest.raises(agent_run_service.HTTPException) as exc:
agent_run_service._validate_model_spec("nope")
assert exc.value.status_code == 422
def test_validate_model_spec_rejects_non_chat_model(monkeypatch: pytest.MonkeyPatch):
monkeypatch.setattr(
agent_run_service.model_cache,
"get_model_info",
lambda spec: SimpleNamespace(model_type="embedding"),
)
with pytest.raises(agent_run_service.HTTPException) as exc:
agent_run_service._validate_model_spec("embed-1")
assert exc.value.status_code == 422
def test_validate_model_spec_accepts_chat_model(monkeypatch: pytest.MonkeyPatch):
monkeypatch.setattr(
agent_run_service.model_cache,
"get_model_info",
lambda spec: SimpleNamespace(model_type="chat"),
)
assert agent_run_service._validate_model_spec("gpt-x") == "gpt-x"
def _patch_common_run_repos(
monkeypatch: pytest.MonkeyPatch,
run_repo_cls,
*,
agent_config_json: dict | None = None,
):
class FakeResult:
def scalar_one_or_none(self):
return SimpleNamespace(uid="user-1", role="user")
class FakeDB:
def __init__(self):
self.added = []
self.committed = False
async def execute(self, stmt):
del stmt
return FakeResult()
def add(self, item):
self.added.append(item)
async def flush(self):
for item in self.added:
if getattr(item, "id", None) is None:
item.id = 10
async def commit(self):
self.committed = True
async def rollback(self):
raise AssertionError("rollback should not be called")
class ConvRepo:
def __init__(self, db_session):
del db_session
async def get_conversation_by_thread_id(self, thread_id: str):
del thread_id
return SimpleNamespace(id=1, uid="user-1", status="active", agent_id="default")
class AgentRepo:
def __init__(self, db_session):
del db_session
async def get_visible_by_slug(self, slug: str, user):
del user
return SimpleNamespace(
slug=slug,
backend_id="ChatbotAgent",
config_json=agent_config_json or {"context": {}},
)
class Queue:
async def enqueue_job(self, job_name: str, run_id: str, _job_id: str):
del job_name, run_id, _job_id
async def fake_get_arq_pool():
return Queue()
monkeypatch.setattr(agent_run_service.agent_manager, "get_agent", lambda backend_id: _FakeBackend())
monkeypatch.setattr(agent_run_service, "AgentRepository", AgentRepo)
monkeypatch.setattr(agent_run_service, "ConversationRepository", ConvRepo)
monkeypatch.setattr(agent_run_service, "AgentRunRepository", run_repo_cls)
monkeypatch.setattr(agent_run_service, "get_arq_pool", fake_get_arq_pool)
return FakeDB()
@pytest.mark.asyncio
async def test_create_chat_run_persists_validated_model_spec(monkeypatch: pytest.MonkeyPatch):
monkeypatch.setattr(
agent_run_service.model_cache,
"get_model_info",
lambda spec: SimpleNamespace(model_type="chat"),
)
captured = {}
created_run = SimpleNamespace(id="", thread_id="thread-1", status="pending", request_id="req-1", uid="user-1")
class RunRepo:
def __init__(self, db_session):
del db_session
async def get_run_by_request_id(self, request_id: str):
del request_id
return None
async def create_run(self, **kwargs):
captured["input_payload"] = kwargs["input_payload"]
created_run.id = kwargs["run_id"]
return created_run
async def set_input_message(self, run_id: str, message_id: int):
del run_id, message_id
return created_run
db = _patch_common_run_repos(monkeypatch, RunRepo)
await agent_run_service.create_agent_run_view(
query="hello",
agent_id="default",
thread_id="thread-1",
meta={"request_id": "req-1"},
image_content=None,
current_uid="user-1",
db=db,
model_spec="claude-x",
)
assert captured["input_payload"]["model_spec"] == "claude-x"
@pytest.mark.asyncio
async def test_create_chat_run_snapshots_agent_configured_model_spec(monkeypatch: pytest.MonkeyPatch):
captured = {}
created_run = SimpleNamespace(id="", thread_id="thread-1", status="pending", request_id="req-1", uid="user-1")
class RunRepo:
def __init__(self, db_session):
del db_session
async def get_run_by_request_id(self, request_id: str):
del request_id
return None
async def create_run(self, **kwargs):
captured["input_payload"] = kwargs["input_payload"]
created_run.id = kwargs["run_id"]
return created_run
async def set_input_message(self, run_id: str, message_id: int):
del run_id, message_id
return created_run
db = _patch_common_run_repos(
monkeypatch,
RunRepo,
agent_config_json={"context": {"model": "agent-config-model"}},
)
await agent_run_service.create_agent_run_view(
query="hello",
agent_id="default",
thread_id="thread-1",
meta={"request_id": "req-1"},
image_content=None,
current_uid="user-1",
db=db,
model_spec=None,
)
assert captured["input_payload"]["model_spec"] == "agent-config-model"
assert db.added[0].extra_metadata["model_spec"] == "agent-config-model"
@pytest.mark.asyncio
async def test_create_resume_run_inherits_parent_model_spec(monkeypatch: pytest.MonkeyPatch):
# 即使 resume 入参传了别的模型,也必须沿用父运行的模型
captured = {}
created_run = SimpleNamespace(id="", thread_id="thread-1", status="pending", request_id="resume-req", uid="user-1")
class RunRepo:
def __init__(self, db_session):
del db_session
async def get_run_for_user(self, run_id: str, uid: str):
del uid
return SimpleNamespace(
id=run_id,
thread_id="thread-1",
status="interrupted",
input_payload={"model_spec": "parent-model"},
)
async def get_resume_run(self, parent_run_id: str, resume_request_id: str):
del parent_run_id, resume_request_id
return None
async def get_run_by_request_id(self, request_id: str):
del request_id
return None
async def create_run(self, **kwargs):
captured["input_payload"] = kwargs["input_payload"]
created_run.id = kwargs["run_id"]
return created_run
async def set_input_message(self, run_id: str, message_id: int):
del run_id, message_id
return created_run
db = _patch_common_run_repos(monkeypatch, RunRepo)
await agent_run_service.create_agent_run_view(
query=None,
agent_id="default",
thread_id="thread-1",
meta={"request_id": "resume-req"},
image_content=None,
current_uid="user-1",
db=db,
model_spec="ignored-model",
resume={"language": "python"},
parent_run_id="parent-run",
resume_request_id="resume-req",
)
assert captured["input_payload"]["model_spec"] == "parent-model"

View File

@ -0,0 +1,16 @@
from __future__ import annotations
from yuxi.services import chat_service as svc
def test_apply_model_override_sets_model_from_meta():
input_context = {"model": "agent-default"}
svc._apply_model_override(input_context, {"model_spec": "user-pick"})
assert input_context["model"] == "user-pick"
def test_apply_model_override_noop_without_model_spec():
input_context = {"model": "agent-default"}
svc._apply_model_override(input_context, {"request_id": "r1"})
svc._apply_model_override(input_context, None)
assert input_context["model"] == "agent-default"

View File

@ -72,6 +72,7 @@
- 工作区文件上传支持多选:`/workspace/upload` 与 Viewer 工作区上传统一使用 `files` 多文件字段,一次最多上传 50 个文件,批量上传失败时清理本次已写入文件。
- 聊天附件新增 MinIO tmp 临时上传、可选 PDF/图片解析、确认后加入线程附件的流程;前端改为弹窗内上传、解析与确认。
- 标准化 Agent run/SSE 执行链路run 创建时持久化输入消息并提交后入队worker 统一写入 Redis Stream envelopeSSE 输出 `event/data/id`、心跳注释、`Last-Event-ID` 回放和终止 `end` 事件;前端强制使用 run API 并支持 ask_user_question 中断后以 resume run 恢复;事件 envelope 构造收敛到统一 helper前端优先使用 envelope 一级 `thread_id` 路由。
- 新增对话级模型覆盖:发起 run 时可携带 `model_spec` 作为运行时模型覆盖,优先级高于智能体配置,无需智能体写权限即可在对话中切换模型;模型校验在创建 run 时完成(非法直接 422不静默回退并随 `agent_runs.input_payload` 持久化留痕resume 沿用被恢复运行的原始模型;前端对话输入区接入模型选择器,按线程记忆选择、默认回退到智能体配置模型,未显式切换时下发 `null` 由后端使用智能体配置模型。输入消息 `extra_metadata` 记录所用 `model_spec`,重开历史会话时按最近一条用户消息还原模型选择。
- 收敛后端模块边界:文档解析从 `plugins.parser` 移动到 `knowledge.parser`,内容审查从 `plugins.guard` 移动到 `services.guard`
- 收敛文件服务边界文件预览判断抽为独立服务Viewer 文件系统的 workspace 分支复用用户 workspace 服务,线程运行时上下文解析从泛化 `filesystem_service` 拆出为 agent runtime helper。
- 升级 DeepAgents 到 0.6.7 并适配新版文件系统协议SubAgentMiddleware 改为显式 subagent specSkills prompt 补齐新版占位符sandbox/skills backend 复用新版 `ReadResult`、`GlobResult`、`GrepResult` 等协议类型,文件权限在 backend 层明确区分 skills、uploads、outputs 与 workspace保留最小 `CustomCompositeBackend` 以避免非 route glob 误扫其他 routeAgent 上下文压缩改为复用 DeepAgents SummarizationMiddleware历史摘要与大工具结果统一 offload 到 outputs。

View File

@ -105,6 +105,7 @@ export const agentApi = {
thread_id: data.thread_id,
meta: data.meta || {},
image_content: data.image_content || null,
model_spec: data.model_spec || null,
resume: data.resume ?? null,
parent_run_id: data.parent_run_id || null,
resume_request_id: data.resume_request_id || null

View File

@ -133,6 +133,15 @@
@remove-attachment="handleAttachmentRemove"
>
<template #actions-left-extra>
<div class="input-model-selector">
<ModelSelectorComponent
:model_spec="currentModelSpec"
size="nano"
display-name="mini"
placeholder="选择模型"
@select-model="handleModelSelect"
/>
</div>
<slot name="input-actions-left" :has-active-thread="!!currentChatId"></slot>
</template>
<template #actions-right-extra>
@ -373,6 +382,7 @@ import {
SyncOutlined
} from '@ant-design/icons-vue'
import AgentInputArea from '@/components/AgentInputArea.vue'
import ModelSelectorComponent from '@/components/ModelSelectorComponent.vue'
import AgentMessageComponent from '@/components/AgentMessageComponent.vue'
import RefsComponent from '@/components/RefsComponent.vue'
import ToolCallsGroupComponent from '@/components/ToolCallsGroupComponent.vue'
@ -676,6 +686,26 @@ const currentAgent = computed(() => {
})
const currentChatId = computed(() => currentThreadId.value)
// ==================== ====================
// 线退
const DRAFT_MODEL_KEY = '__draft__'
const selectedModelByThread = reactive({})
const agentDefaultModel = computed(
() =>
agentConfig.value?.model ||
currentAgent.value?.config_json?.context?.model ||
configStore.config?.default_model ||
''
)
const currentModelSpec = computed(
() => selectedModelByThread[currentChatId.value || DRAFT_MODEL_KEY] || agentDefaultModel.value
)
const handleModelSelect = (spec) => {
if (typeof spec === 'string' && spec) {
selectedModelByThread[currentChatId.value || DRAFT_MODEL_KEY] = spec
}
}
const currentThreadAgentName = computed(() => {
const threadAgentId = currentThread.value?.agent_id
if (threadAgentId && agents.value?.length) {
@ -1432,13 +1462,29 @@ const fetchThreadMessages = async ({ agentId, threadId, delay = 0 }) => {
try {
const response = await agentApi.getAgentHistory(threadId)
threadMessages.value[threadId] = response.history || []
const history = response.history || []
threadMessages.value[threadId] = history
restoreThreadModelSelection(threadId, history)
} catch (error) {
handleChatError(error, 'load')
throw error
}
}
// model_spec
const restoreThreadModelSelection = (threadId, history) => {
if (selectedModelByThread[threadId]) return
for (let i = history.length - 1; i >= 0; i -= 1) {
const msg = history[i]
if (msg?.type !== 'human') continue
const modelSpec = msg?.extra_metadata?.model_spec
if (modelSpec) {
selectedModelByThread[threadId] = modelSpec
return
}
}
}
const fetchThreadFiles = async (threadId) => {
if (!threadId) return
try {
@ -1700,7 +1746,17 @@ const handleSendMessage = async ({ image } = {}) => {
message.error('创建对话失败,请重试')
return
}
// 线稿线
const draftModelSpec = selectedModelByThread[DRAFT_MODEL_KEY]
if (draftModelSpec) {
if (!selectedModelByThread[threadId]) {
selectedModelByThread[threadId] = draftModelSpec
}
delete selectedModelByThread[DRAFT_MODEL_KEY]
}
}
// null使
const modelSpec = selectedModelByThread[threadId] || null
userInput.value = ''
@ -1761,7 +1817,8 @@ const handleSendMessage = async ({ image } = {}) => {
request_id: requestId,
attachment_file_ids: pendingAttachmentFileIds
},
image_content: imageContent
image_content: imageContent,
model_spec: modelSpec
})
const runId = runResp?.run_id
if (!runId) {
@ -2511,6 +2568,14 @@ watch(currentChatId, (threadId, oldThreadId) => {
}
}
.input-model-selector {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 0;
max-width: min(168px, calc(100vw - 160px));
}
&.start-screen {
position: absolute;
top: 45%;

View File

@ -6,21 +6,8 @@
<AgentChatComponent
ref="chatComponentRef"
:single-mode="false"
:send-disabled="isSavingInputModel"
@thread-change="handleThreadChange"
>
<template #input-actions-left>
<div v-if="showInputModelSelector" class="input-model-selector">
<ModelSelectorComponent
:model_spec="currentInputModelSpec"
:disabled="isInputModelSelectorDisabled"
size="nano"
display-name="mini"
@select-model="handleInputModelChange"
/>
</div>
</template>
<template #input-actions-right="{ hasActiveThread }">
<a-dropdown
v-if="selectedAgentId"
@ -154,7 +141,6 @@ import { message } from 'ant-design-vue'
import { Settings2, Ellipsis, ChevronDown, Check, FolderKanban } from 'lucide-vue-next'
import { useRoute, useRouter } from 'vue-router'
import AgentChatComponent from '@/components/AgentChatComponent.vue'
import ModelSelectorComponent from '@/components/ModelSelectorComponent.vue'
import FeedbackModalComponent from '@/components/dashboard/FeedbackModalComponent.vue'
import { useUserStore } from '@/stores/user'
import { isBuiltinAgent, useAgentStore } from '@/stores/agent'
@ -178,11 +164,9 @@ const route = useRoute()
const router = useRouter()
// agentStore
const { agents, selectedAgentId, selectedAgent, agentConfig, configurableItems, isLoadingConfig } =
storeToRefs(agentStore)
const { agents, selectedAgentId, isLoadingConfig } = storeToRefs(agentStore)
const syncingRouteThread = ref(false)
const isSavingInputModel = ref(false)
const getRouteThreadId = () => {
const value = route.params.thread_id
@ -257,40 +241,6 @@ const currentAgentLabel = computed(() => {
return currentAgentOption.value?.label || '智能体'
})
const inputModelKey = computed(() => {
if (configurableItems.value?.model?.kind === 'llm') return 'model'
return (
Object.entries(configurableItems.value || {}).find(([, item]) => item?.kind === 'llm')?.[0] ||
''
)
})
const currentInputModelSpec = computed(() => {
const key = inputModelKey.value
return key ? agentConfig.value?.[key] || '' : ''
})
const showInputModelSelector = computed(() => Boolean(selectedAgentId.value && inputModelKey.value))
const isInputModelSelectorDisabled = computed(
() => isLoadingConfig.value || isSavingInputModel.value || !selectedAgent.value?.can_manage
)
const handleInputModelChange = async (spec) => {
const key = inputModelKey.value
if (!key || typeof spec !== 'string' || !spec || spec === currentInputModelSpec.value) return
if (isInputModelSelectorDisabled.value) return
const previousSpec = currentInputModelSpec.value
isSavingInputModel.value = true
try {
agentStore.updateAgentConfig({ [key]: spec })
await agentStore.saveAgentConfig()
} catch {
agentStore.updateAgentConfig({ [key]: previousSpec })
} finally {
isSavingInputModel.value = false
}
}
const agentDropdownOpen = ref(false)
@ -421,14 +371,6 @@ const handleFeedback = () => {
overflow: hidden;
}
.input-model-selector {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 0;
max-width: min(168px, calc(100vw - 160px));
}
.config-dropdown-trigger {
display: inline-flex;
align-items: center;