mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 11:58:34 +08:00
feat(agent): add knowledge base binding tab to agent editor (#237)
Agents now have a per-agent primary wiki KB stored on
mate_agent.primary_kb_id. KBs remain workspace-shared — selecting one in
the agent editor only chooses the default wiki target for that agent, it
does not change the KB's ownership or visibility.
Backend
- AgentEntity: add primary_kb_id field (FieldStrategy.ALWAYS so the UI
can clear it back to "no primary")
- AgentController#update: switch body to Map<String, Object> so we can
tell "field missing" apart from "explicit null" via containsKey, then
convertValue back to AgentEntity
- WikiKnowledgeBaseService:
- new resolvePrimaryKb(agentId): prefers agent.primary_kb_id when it
points to a workspace-visible KB; falls back to legacy
kb.agent_id marker, then to most-recently-updated workspace KB
- listByAgentId now returns the full workspace set (KBs are
workspace-shared under the new model)
- update(id, name, description) no longer touches agent_id
- WikiController: new GET /knowledge-bases/bindable for the UI picker;
PUT /knowledge-bases/{id} no longer reads agentId
- WikiKnowledgeBaseEntity: add FieldStrategy.ALWAYS on embeddingModelId
and configContent so explicit nulls actually unbind/clear instead of
being silently skipped by MyBatis-Plus's NOT_NULL default
- Migrations V129 (H2 + MySQL): add primary_kb_id column + index, backfill
from legacy kb.agent_id, MySQL uses INFORMATION_SCHEMA guard +
PREPARE/EXECUTE for idempotency
- WikiKnowledgeBaseServiceTest: 13 cases, all passing
Frontend
- Agents.vue: new "Knowledge Base" tab, radio-select bindable KBs
- API: listBindableKBs() + Agent.primaryKbId typed string | number | null
- IDs handled as strings throughout (Snowflake-safe)
- i18n keys for the new tab in zh-CN and en-US
This commit is contained in:
parent
0ef735588a
commit
bd02734d61
@ -1,5 +1,6 @@
|
|||||||
package vip.mate.agent.controller;
|
package vip.mate.agent.controller;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@ -27,6 +28,7 @@ import vip.mate.workspace.core.service.WorkspaceService;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
@ -49,6 +51,7 @@ public class AgentController {
|
|||||||
private final ModelConfigService modelConfigService;
|
private final ModelConfigService modelConfigService;
|
||||||
private final ModelCapabilityService modelCapabilityService;
|
private final ModelCapabilityService modelCapabilityService;
|
||||||
private final SystemSettingService systemSettingService;
|
private final SystemSettingService systemSettingService;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
private final ExecutorService sseExecutor = Executors.newCachedThreadPool();
|
private final ExecutorService sseExecutor = Executors.newCachedThreadPool();
|
||||||
|
|
||||||
@Operation(summary = "获取Agent列表")
|
@Operation(summary = "获取Agent列表")
|
||||||
@ -144,10 +147,14 @@ public class AgentController {
|
|||||||
@Operation(summary = "更新Agent")
|
@Operation(summary = "更新Agent")
|
||||||
@PutMapping("/{id}")
|
@PutMapping("/{id}")
|
||||||
@RequireWorkspaceRole("member")
|
@RequireWorkspaceRole("member")
|
||||||
public R<AgentEntity> update(@PathVariable Long id, @RequestBody AgentEntity agent,
|
public R<AgentEntity> update(@PathVariable Long id, @RequestBody Map<String, Object> body,
|
||||||
@RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) {
|
@RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) {
|
||||||
AgentEntity existing = agentService.getAgent(id);
|
AgentEntity existing = agentService.getAgent(id);
|
||||||
verifyResourceWorkspace(existing.getWorkspaceId(), workspaceId);
|
verifyResourceWorkspace(existing.getWorkspaceId(), workspaceId);
|
||||||
|
AgentEntity agent = objectMapper.convertValue(body, AgentEntity.class);
|
||||||
|
if (!body.containsKey("primaryKbId")) {
|
||||||
|
agent.setPrimaryKbId(existing.getPrimaryKbId());
|
||||||
|
}
|
||||||
agent.setId(id);
|
agent.setId(id);
|
||||||
agent.setWorkspaceId(existing.getWorkspaceId()); // 不允许跨 workspace 迁移
|
agent.setWorkspaceId(existing.getWorkspaceId()); // 不允许跨 workspace 迁移
|
||||||
AgentEntity updated = agentService.updateAgent(agent);
|
AgentEntity updated = agentService.updateAgent(agent);
|
||||||
|
|||||||
@ -86,6 +86,15 @@ public class AgentEntity {
|
|||||||
@TableField(value = "workspace_base_path", updateStrategy = FieldStrategy.ALWAYS)
|
@TableField(value = "workspace_base_path", updateStrategy = FieldStrategy.ALWAYS)
|
||||||
private String workspaceBasePath;
|
private String workspaceBasePath;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent's primary wiki knowledge base. This is a per-agent default target
|
||||||
|
* for wiki tools; it does not affect KB visibility or ownership.
|
||||||
|
* Null means no explicit primary KB, so wiki resolution falls back to the
|
||||||
|
* workspace's most recently updated KB.
|
||||||
|
*/
|
||||||
|
@TableField(value = "primary_kb_id", updateStrategy = FieldStrategy.ALWAYS)
|
||||||
|
private Long primaryKbId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Explicit opt-out from every skill. When {@code true}, the binding service
|
* Explicit opt-out from every skill. When {@code true}, the binding service
|
||||||
* returns {@link java.util.Collections#emptySet()} from
|
* returns {@link java.util.Collections#emptySet()} from
|
||||||
|
|||||||
@ -67,6 +67,15 @@ public class WikiController {
|
|||||||
return R.ok(withLivePageCount(kbService.listByWorkspace(wsId)));
|
return R.ok(withLivePageCount(kbService.listByWorkspace(wsId)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@RequireWorkspaceRole("viewer")
|
||||||
|
@Operation(summary = "列出可绑定到指定 Agent 的知识库")
|
||||||
|
@GetMapping("/knowledge-bases/bindable")
|
||||||
|
public R<List<WikiKnowledgeBaseEntity>> listBindableKBs(
|
||||||
|
@RequestHeader(value = "X-Workspace-Id", required = false) Long workspaceId) {
|
||||||
|
long wsId = workspaceId != null ? workspaceId : 1L;
|
||||||
|
return R.ok(withLivePageCount(kbService.listByWorkspace(wsId)));
|
||||||
|
}
|
||||||
|
|
||||||
@RequireWorkspaceRole("viewer")
|
@RequireWorkspaceRole("viewer")
|
||||||
@Operation(summary = "获取知识库详情")
|
@Operation(summary = "获取知识库详情")
|
||||||
@GetMapping("/knowledge-bases/{id}")
|
@GetMapping("/knowledge-bases/{id}")
|
||||||
@ -131,8 +140,7 @@ public class WikiController {
|
|||||||
verifyKBWorkspace(id, workspaceId);
|
verifyKBWorkspace(id, workspaceId);
|
||||||
String name = (String) body.get("name");
|
String name = (String) body.get("name");
|
||||||
String description = (String) body.get("description");
|
String description = (String) body.get("description");
|
||||||
Long agentId = body.get("agentId") != null ? Long.valueOf(body.get("agentId").toString()) : null;
|
kbService.update(id, name, description);
|
||||||
kbService.update(id, name, description, agentId);
|
|
||||||
// RFC Embedding UI: 允许通过此接口绑定 / 解绑 embedding 模型
|
// RFC Embedding UI: 允许通过此接口绑定 / 解绑 embedding 模型
|
||||||
if (body.containsKey("embeddingModelId")) {
|
if (body.containsKey("embeddingModelId")) {
|
||||||
Object v = body.get("embeddingModelId");
|
Object v = body.get("embeddingModelId");
|
||||||
|
|||||||
@ -51,6 +51,7 @@ public class WikiKnowledgeBaseEntity {
|
|||||||
* NULL = 使用系统默认(mate_system_setting 的 embedding.default.model.id),
|
* NULL = 使用系统默认(mate_system_setting 的 embedding.default.model.id),
|
||||||
* 再无则取任意 enabled 的 embedding 模型,最终全无则语义搜索降级为不可用。
|
* 再无则取任意 enabled 的 embedding 模型,最终全无则语义搜索降级为不可用。
|
||||||
*/
|
*/
|
||||||
|
@TableField(updateStrategy = FieldStrategy.ALWAYS)
|
||||||
private Long embeddingModelId;
|
private Long embeddingModelId;
|
||||||
|
|
||||||
@TableField(fill = FieldFill.INSERT)
|
@TableField(fill = FieldFill.INSERT)
|
||||||
|
|||||||
@ -5,6 +5,8 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import vip.mate.agent.model.AgentEntity;
|
||||||
|
import vip.mate.agent.repository.AgentMapper;
|
||||||
import vip.mate.wiki.job.model.WikiProcessingJobEntity;
|
import vip.mate.wiki.job.model.WikiProcessingJobEntity;
|
||||||
import vip.mate.wiki.model.WikiChunkEntity;
|
import vip.mate.wiki.model.WikiChunkEntity;
|
||||||
import vip.mate.wiki.model.WikiKnowledgeBaseEntity;
|
import vip.mate.wiki.model.WikiKnowledgeBaseEntity;
|
||||||
@ -36,6 +38,7 @@ public class WikiKnowledgeBaseService {
|
|||||||
private final WikiChunkMapper chunkMapper;
|
private final WikiChunkMapper chunkMapper;
|
||||||
private final WikiPageCitationMapper citationMapper;
|
private final WikiPageCitationMapper citationMapper;
|
||||||
private final WikiProcessingJobMapper processingJobMapper;
|
private final WikiProcessingJobMapper processingJobMapper;
|
||||||
|
private final AgentMapper agentMapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RFC-051 PR-2: optional system-page scaffold (overview / log). Marked
|
* RFC-051 PR-2: optional system-page scaffold (overview / log). Marked
|
||||||
@ -102,32 +105,42 @@ public class WikiKnowledgeBaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取 Agent 可访问的知识库:Agent 专属 KB + 公共 KB(agent_id IS NULL)
|
* 获取 Agent 可访问的知识库。
|
||||||
|
* <p>
|
||||||
|
* Knowledge bases are workspace-shared. The agent's primary KB is stored
|
||||||
|
* on mate_agent.primary_kb_id and does not affect visibility.
|
||||||
*/
|
*/
|
||||||
public List<WikiKnowledgeBaseEntity> listByAgentId(Long agentId) {
|
public List<WikiKnowledgeBaseEntity> listByAgentId(Long agentId) {
|
||||||
return kbMapper.selectList(
|
AgentEntity agent = getAgentOrNull(agentId);
|
||||||
new LambdaQueryWrapper<WikiKnowledgeBaseEntity>()
|
if (agent == null || agent.getWorkspaceId() == null) {
|
||||||
.and(w -> w.eq(WikiKnowledgeBaseEntity::getAgentId, agentId)
|
return listAll();
|
||||||
.or().isNull(WikiKnowledgeBaseEntity::getAgentId))
|
}
|
||||||
.orderByDesc(WikiKnowledgeBaseEntity::getUpdateTime));
|
return listByWorkspace(agent.getWorkspaceId());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve the single knowledge base an agent's wiki tools should operate on.
|
* Resolve the single knowledge base an agent's wiki tools should operate on.
|
||||||
* <p>
|
* <p>
|
||||||
* Prefers a KB explicitly bound to the agent; a shared (agent-less) KB is
|
* Prefers mate_agent.primary_kb_id when it points to a KB in the same
|
||||||
* only used as a fallback when the agent has no bound KB of its own. This
|
* workspace. For legacy rows that predate primary_kb_id, falls back to the
|
||||||
* matters because {@link #listByAgentId} also returns shared KBs, and a
|
* old mate_wiki_knowledge_base.agent_id marker only when no primary is set.
|
||||||
* shared KB with a more recent {@code update_time} would otherwise win the
|
* Otherwise the most recently updated workspace KB wins.
|
||||||
* {@code get(0)} pick over the agent's own KB. Within each tier the most
|
|
||||||
* recently updated KB wins. Returns {@code null} when the agent can reach
|
|
||||||
* no knowledge base at all.
|
|
||||||
*/
|
*/
|
||||||
public WikiKnowledgeBaseEntity resolvePrimaryKb(Long agentId) {
|
public WikiKnowledgeBaseEntity resolvePrimaryKb(Long agentId) {
|
||||||
|
AgentEntity agent = getAgentOrNull(agentId);
|
||||||
List<WikiKnowledgeBaseEntity> kbs = listByAgentId(agentId);
|
List<WikiKnowledgeBaseEntity> kbs = listByAgentId(agentId);
|
||||||
if (kbs.isEmpty()) {
|
if (kbs.isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
Long primaryKbId = agent != null ? agent.getPrimaryKbId() : null;
|
||||||
|
if (primaryKbId != null) {
|
||||||
|
for (WikiKnowledgeBaseEntity kb : kbs) {
|
||||||
|
if (primaryKbId.equals(kb.getId()) && sameWorkspace(agent, kb)) {
|
||||||
|
return kb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return kbs.get(0);
|
||||||
|
}
|
||||||
if (agentId != null) {
|
if (agentId != null) {
|
||||||
for (WikiKnowledgeBaseEntity kb : kbs) {
|
for (WikiKnowledgeBaseEntity kb : kbs) {
|
||||||
if (agentId.equals(kb.getAgentId())) {
|
if (agentId.equals(kb.getAgentId())) {
|
||||||
@ -138,9 +151,23 @@ public class WikiKnowledgeBaseService {
|
|||||||
return kbs.get(0);
|
return kbs.get(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private AgentEntity getAgentOrNull(Long agentId) {
|
||||||
|
if (agentId == null || agentMapper == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return agentMapper.selectById(agentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean sameWorkspace(AgentEntity agent, WikiKnowledgeBaseEntity kb) {
|
||||||
|
if (agent == null || kb == null || agent.getWorkspaceId() == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return kb.getWorkspaceId() == null || agent.getWorkspaceId().equals(kb.getWorkspaceId());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve a specific knowledge base by name, restricted to the agent's
|
* Resolve a specific knowledge base by name, restricted to the agent's
|
||||||
* visibility set (agent-bound KBs + shared NULL KBs). Used by wiki tools
|
* workspace-visible KB set. Used by wiki tools
|
||||||
* that accept an optional {@code kbName} parameter so the LLM can target
|
* that accept an optional {@code kbName} parameter so the LLM can target
|
||||||
* a non-primary KB when the agent reaches more than one.
|
* a non-primary KB when the agent reaches more than one.
|
||||||
* <p>
|
* <p>
|
||||||
@ -222,14 +249,13 @@ public class WikiKnowledgeBaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public WikiKnowledgeBaseEntity update(Long id, String name, String description, Long agentId) {
|
public WikiKnowledgeBaseEntity update(Long id, String name, String description) {
|
||||||
WikiKnowledgeBaseEntity entity = kbMapper.selectById(id);
|
WikiKnowledgeBaseEntity entity = kbMapper.selectById(id);
|
||||||
if (entity == null) {
|
if (entity == null) {
|
||||||
throw new IllegalArgumentException("Knowledge base not found: " + id);
|
throw new IllegalArgumentException("Knowledge base not found: " + id);
|
||||||
}
|
}
|
||||||
if (name != null) entity.setName(name);
|
if (name != null) entity.setName(name);
|
||||||
if (description != null) entity.setDescription(description);
|
if (description != null) entity.setDescription(description);
|
||||||
if (agentId != null) entity.setAgentId(agentId);
|
|
||||||
kbMapper.updateById(entity);
|
kbMapper.updateById(entity);
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,25 @@
|
|||||||
|
-- V129: Store the per-agent primary wiki KB on mate_agent.
|
||||||
|
--
|
||||||
|
-- Knowledge bases remain workspace-shared; this field only chooses the
|
||||||
|
-- default KB for wiki tools when no kbName/kbId is specified.
|
||||||
|
ALTER TABLE mate_agent ADD COLUMN IF NOT EXISTS primary_kb_id BIGINT DEFAULT NULL;
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_agent_primary_kb ON mate_agent(primary_kb_id);
|
||||||
|
|
||||||
|
UPDATE mate_agent a
|
||||||
|
SET primary_kb_id = (
|
||||||
|
SELECT kb.id
|
||||||
|
FROM mate_wiki_knowledge_base kb
|
||||||
|
WHERE kb.agent_id = a.id
|
||||||
|
AND (kb.workspace_id IS NULL OR kb.workspace_id = a.workspace_id)
|
||||||
|
AND kb.deleted = 0
|
||||||
|
ORDER BY kb.update_time DESC
|
||||||
|
LIMIT 1
|
||||||
|
)
|
||||||
|
WHERE a.primary_kb_id IS NULL
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM mate_wiki_knowledge_base kb
|
||||||
|
WHERE kb.agent_id = a.id
|
||||||
|
AND (kb.workspace_id IS NULL OR kb.workspace_id = a.workspace_id)
|
||||||
|
AND kb.deleted = 0
|
||||||
|
);
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
-- V129: Store the per-agent primary wiki KB on mate_agent (MySQL).
|
||||||
|
--
|
||||||
|
-- Knowledge bases remain workspace-shared; this field only chooses the
|
||||||
|
-- default KB for wiki tools when no kbName/kbId is specified.
|
||||||
|
|
||||||
|
SET @col_exists := (
|
||||||
|
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'mate_agent'
|
||||||
|
AND COLUMN_NAME = 'primary_kb_id'
|
||||||
|
);
|
||||||
|
SET @stmt := IF(@col_exists = 0,
|
||||||
|
'ALTER TABLE mate_agent ADD COLUMN primary_kb_id BIGINT DEFAULT NULL',
|
||||||
|
'SELECT 1');
|
||||||
|
PREPARE s FROM @stmt; EXECUTE s; DEALLOCATE PREPARE s;
|
||||||
|
|
||||||
|
SET @idx_exists := (
|
||||||
|
SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'mate_agent'
|
||||||
|
AND INDEX_NAME = 'idx_agent_primary_kb'
|
||||||
|
);
|
||||||
|
SET @stmt := IF(@idx_exists = 0,
|
||||||
|
'CREATE INDEX idx_agent_primary_kb ON mate_agent(primary_kb_id)',
|
||||||
|
'SELECT 1');
|
||||||
|
PREPARE s FROM @stmt; EXECUTE s; DEALLOCATE PREPARE s;
|
||||||
|
|
||||||
|
UPDATE mate_agent a
|
||||||
|
SET primary_kb_id = (
|
||||||
|
SELECT kb.id
|
||||||
|
FROM mate_wiki_knowledge_base kb
|
||||||
|
WHERE kb.agent_id = a.id
|
||||||
|
AND (kb.workspace_id IS NULL OR kb.workspace_id = a.workspace_id)
|
||||||
|
AND kb.deleted = 0
|
||||||
|
ORDER BY kb.update_time DESC
|
||||||
|
LIMIT 1
|
||||||
|
)
|
||||||
|
WHERE a.primary_kb_id IS NULL
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM mate_wiki_knowledge_base kb
|
||||||
|
WHERE kb.agent_id = a.id
|
||||||
|
AND (kb.workspace_id IS NULL OR kb.workspace_id = a.workspace_id)
|
||||||
|
AND kb.deleted = 0
|
||||||
|
);
|
||||||
@ -28,9 +28,11 @@ CREATE TABLE IF NOT EXISTS mate_agent (
|
|||||||
icon VARCHAR(256),
|
icon VARCHAR(256),
|
||||||
tags VARCHAR(256),
|
tags VARCHAR(256),
|
||||||
workspace_id BIGINT NOT NULL DEFAULT 1,
|
workspace_id BIGINT NOT NULL DEFAULT 1,
|
||||||
|
primary_kb_id BIGINT DEFAULT NULL,
|
||||||
create_time DATETIME NOT NULL,
|
create_time DATETIME NOT NULL,
|
||||||
update_time DATETIME NOT NULL,
|
update_time DATETIME NOT NULL,
|
||||||
deleted INT NOT NULL DEFAULT 0
|
deleted INT NOT NULL DEFAULT 0,
|
||||||
|
INDEX idx_agent_primary_kb (primary_kb_id)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
-- 模型配置表
|
-- 模型配置表
|
||||||
|
|||||||
@ -30,10 +30,12 @@ CREATE TABLE IF NOT EXISTS mate_agent (
|
|||||||
tags VARCHAR(256),
|
tags VARCHAR(256),
|
||||||
workspace_id BIGINT NOT NULL DEFAULT 1,
|
workspace_id BIGINT NOT NULL DEFAULT 1,
|
||||||
default_thinking_level VARCHAR(32) DEFAULT NULL,
|
default_thinking_level VARCHAR(32) DEFAULT NULL,
|
||||||
|
primary_kb_id BIGINT DEFAULT NULL,
|
||||||
create_time DATETIME NOT NULL,
|
create_time DATETIME NOT NULL,
|
||||||
update_time DATETIME NOT NULL,
|
update_time DATETIME NOT NULL,
|
||||||
deleted INT NOT NULL DEFAULT 0
|
deleted INT NOT NULL DEFAULT 0
|
||||||
);
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_agent_primary_kb ON mate_agent(primary_kb_id);
|
||||||
|
|
||||||
-- 模型配置表
|
-- 模型配置表
|
||||||
CREATE TABLE IF NOT EXISTS mate_model_config (
|
CREATE TABLE IF NOT EXISTS mate_model_config (
|
||||||
|
|||||||
@ -2,6 +2,8 @@ package vip.mate.wiki.service;
|
|||||||
|
|
||||||
import org.junit.jupiter.api.DisplayName;
|
import org.junit.jupiter.api.DisplayName;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import vip.mate.agent.model.AgentEntity;
|
||||||
|
import vip.mate.agent.repository.AgentMapper;
|
||||||
import vip.mate.wiki.model.WikiKnowledgeBaseEntity;
|
import vip.mate.wiki.model.WikiKnowledgeBaseEntity;
|
||||||
import vip.mate.wiki.repository.WikiKnowledgeBaseMapper;
|
import vip.mate.wiki.repository.WikiKnowledgeBaseMapper;
|
||||||
|
|
||||||
@ -10,6 +12,7 @@ import java.util.List;
|
|||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -24,8 +27,9 @@ import static org.mockito.Mockito.when;
|
|||||||
class WikiKnowledgeBaseServiceTest {
|
class WikiKnowledgeBaseServiceTest {
|
||||||
|
|
||||||
private final WikiKnowledgeBaseMapper kbMapper = mock(WikiKnowledgeBaseMapper.class);
|
private final WikiKnowledgeBaseMapper kbMapper = mock(WikiKnowledgeBaseMapper.class);
|
||||||
|
private final AgentMapper agentMapper = mock(AgentMapper.class);
|
||||||
private final WikiKnowledgeBaseService service = new WikiKnowledgeBaseService(
|
private final WikiKnowledgeBaseService service = new WikiKnowledgeBaseService(
|
||||||
kbMapper, null, null, null, null, null);
|
kbMapper, null, null, null, null, null, agentMapper);
|
||||||
|
|
||||||
private static WikiKnowledgeBaseEntity kb(long id, Long agentId) {
|
private static WikiKnowledgeBaseEntity kb(long id, Long agentId) {
|
||||||
return kb(id, agentId, null);
|
return kb(id, agentId, null);
|
||||||
@ -39,14 +43,29 @@ class WikiKnowledgeBaseServiceTest {
|
|||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static WikiKnowledgeBaseEntity kb(long id, Long agentId, Long workspaceId, String name) {
|
||||||
|
WikiKnowledgeBaseEntity entity = kb(id, agentId, name);
|
||||||
|
entity.setWorkspaceId(workspaceId);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AgentEntity agent(long id, Long workspaceId, Long primaryKbId) {
|
||||||
|
AgentEntity entity = new AgentEntity();
|
||||||
|
entity.setId(id);
|
||||||
|
entity.setWorkspaceId(workspaceId);
|
||||||
|
entity.setPrimaryKbId(primaryKbId);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("prefers the agent's bound KB even when a shared KB was updated more recently")
|
@DisplayName("prefers the agent's bound KB even when a shared KB was updated more recently")
|
||||||
void prefersBoundKbOverNewerSharedKb() {
|
void prefersBoundKbOverNewerSharedKb() {
|
||||||
|
when(agentMapper.selectById(7L)).thenReturn(agent(7L, 1L, 100L));
|
||||||
// listByAgentId order is update_time DESC: two shared KBs precede the bound one.
|
// listByAgentId order is update_time DESC: two shared KBs precede the bound one.
|
||||||
when(kbMapper.selectList(any())).thenReturn(List.of(
|
when(kbMapper.selectList(any())).thenReturn(List.of(
|
||||||
kb(900L, null),
|
kb(900L, null, 1L, "Shared"),
|
||||||
kb(800L, null),
|
kb(800L, 8L, 1L, "Legacy Other"),
|
||||||
kb(100L, 7L)));
|
kb(100L, null, 1L, "Primary")));
|
||||||
|
|
||||||
assertThat(service.resolvePrimaryKb(7L)).isNotNull();
|
assertThat(service.resolvePrimaryKb(7L)).isNotNull();
|
||||||
assertThat(service.resolvePrimaryKb(7L).getId()).isEqualTo(100L);
|
assertThat(service.resolvePrimaryKb(7L).getId()).isEqualTo(100L);
|
||||||
@ -55,9 +74,34 @@ class WikiKnowledgeBaseServiceTest {
|
|||||||
@Test
|
@Test
|
||||||
@DisplayName("falls back to the most recent shared KB when the agent has no bound KB")
|
@DisplayName("falls back to the most recent shared KB when the agent has no bound KB")
|
||||||
void fallsBackToSharedKbWhenNoneBound() {
|
void fallsBackToSharedKbWhenNoneBound() {
|
||||||
|
when(agentMapper.selectById(7L)).thenReturn(agent(7L, 1L, null));
|
||||||
when(kbMapper.selectList(any())).thenReturn(List.of(
|
when(kbMapper.selectList(any())).thenReturn(List.of(
|
||||||
kb(900L, null),
|
kb(900L, 8L, 1L, "Most Recent"),
|
||||||
kb(800L, null)));
|
kb(800L, null, 1L, "Older")));
|
||||||
|
|
||||||
|
assertThat(service.resolvePrimaryKb(7L).getId()).isEqualTo(900L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("two agents can share the same primary KB")
|
||||||
|
void twoAgentsCanSharePrimaryKb() {
|
||||||
|
when(agentMapper.selectById(7L)).thenReturn(agent(7L, 1L, 100L));
|
||||||
|
when(agentMapper.selectById(8L)).thenReturn(agent(8L, 1L, 100L));
|
||||||
|
when(kbMapper.selectList(any())).thenReturn(List.of(
|
||||||
|
kb(100L, null, 1L, "Shared Primary"),
|
||||||
|
kb(900L, null, 1L, "Fallback")));
|
||||||
|
|
||||||
|
assertThat(service.resolvePrimaryKb(7L).getId()).isEqualTo(100L);
|
||||||
|
assertThat(service.resolvePrimaryKb(8L).getId()).isEqualTo(100L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("primary KB pointing outside the agent workspace falls back")
|
||||||
|
void primaryKbOutsideWorkspaceFallsBack() {
|
||||||
|
when(agentMapper.selectById(7L)).thenReturn(agent(7L, 1L, 200L));
|
||||||
|
when(kbMapper.selectList(any())).thenReturn(List.of(
|
||||||
|
kb(900L, null, 1L, "Workspace Fallback"),
|
||||||
|
kb(200L, null, 2L, "Wrong Workspace")));
|
||||||
|
|
||||||
assertThat(service.resolvePrimaryKb(7L).getId()).isEqualTo(900L);
|
assertThat(service.resolvePrimaryKb(7L).getId()).isEqualTo(900L);
|
||||||
}
|
}
|
||||||
@ -167,13 +211,16 @@ class WikiKnowledgeBaseServiceTest {
|
|||||||
@Test
|
@Test
|
||||||
@DisplayName("findVisibleById returns the KB only when it is in the agent's visibility set")
|
@DisplayName("findVisibleById returns the KB only when it is in the agent's visibility set")
|
||||||
void findVisibleByIdGate() {
|
void findVisibleByIdGate() {
|
||||||
|
when(agentMapper.selectById(7L)).thenReturn(agent(7L, 1L, null));
|
||||||
when(kbMapper.selectList(any())).thenReturn(List.of(
|
when(kbMapper.selectList(any())).thenReturn(List.of(
|
||||||
kb(100L, 7L, "Bound KB"),
|
kb(100L, 7L, "Bound KB"),
|
||||||
kb(900L, null, "Shared KB")));
|
kb(900L, null, "Shared KB"),
|
||||||
|
kb(800L, 8L, "Other Agent Primary KB")));
|
||||||
|
|
||||||
// Visible: returned.
|
// Visible: returned.
|
||||||
assertThat(service.findVisibleById(7L, 100L)).isNotNull();
|
assertThat(service.findVisibleById(7L, 100L)).isNotNull();
|
||||||
assertThat(service.findVisibleById(7L, 900L)).isNotNull();
|
assertThat(service.findVisibleById(7L, 900L)).isNotNull();
|
||||||
|
assertThat(service.findVisibleById(7L, 800L)).isNotNull();
|
||||||
|
|
||||||
// Not in visibility set: deliberate fail-closed gate so an LLM
|
// Not in visibility set: deliberate fail-closed gate so an LLM
|
||||||
// can't pivot to an arbitrary KB by guessing an id.
|
// can't pivot to an arbitrary KB by guessing an id.
|
||||||
|
|||||||
@ -745,11 +745,12 @@ export const cronJobApi = {
|
|||||||
export const wikiApi = {
|
export const wikiApi = {
|
||||||
// Knowledge Base
|
// Knowledge Base
|
||||||
listKBs: () => http.get('/wiki/knowledge-bases'),
|
listKBs: () => http.get('/wiki/knowledge-bases'),
|
||||||
getKB: (id: number) => http.get(`/wiki/knowledge-bases/${id}`),
|
getKB: (id: string | number) => http.get(`/wiki/knowledge-bases/${id}`),
|
||||||
listKBsByAgent: (agentId: number) => http.get(`/wiki/knowledge-bases/agent/${agentId}`),
|
listKBsByAgent: (agentId: string | number) => http.get(`/wiki/knowledge-bases/agent/${agentId}`),
|
||||||
createKB: (data: { name: string; description?: string; agentId?: number }) =>
|
listBindableKBs: () => http.get('/wiki/knowledge-bases/bindable'),
|
||||||
|
createKB: (data: { name: string; description?: string; agentId?: string | number }) =>
|
||||||
http.post('/wiki/knowledge-bases', data),
|
http.post('/wiki/knowledge-bases', data),
|
||||||
updateKB: (id: number, data: { name?: string; description?: string; agentId?: number; embeddingModelId?: string | number | null }) =>
|
updateKB: (id: string | number, data: { name?: string; description?: string; embeddingModelId?: string | number | null }) =>
|
||||||
http.put(`/wiki/knowledge-bases/${id}`, data),
|
http.put(`/wiki/knowledge-bases/${id}`, data),
|
||||||
deleteKB: (id: number) => http.delete(`/wiki/knowledge-bases/${id}`),
|
deleteKB: (id: number) => http.delete(`/wiki/knowledge-bases/${id}`),
|
||||||
getConfig: (id: number) => http.get(`/wiki/knowledge-bases/${id}/config`),
|
getConfig: (id: number) => http.get(`/wiki/knowledge-bases/${id}/config`),
|
||||||
|
|||||||
@ -1133,6 +1133,7 @@ export default {
|
|||||||
skills: 'Skills',
|
skills: 'Skills',
|
||||||
tools: 'Tools',
|
tools: 'Tools',
|
||||||
providers: 'Providers',
|
providers: 'Providers',
|
||||||
|
wiki: 'Knowledge Base',
|
||||||
context: 'Context',
|
context: 'Context',
|
||||||
},
|
},
|
||||||
columns: {
|
columns: {
|
||||||
@ -1258,6 +1259,13 @@ export default {
|
|||||||
noMatchingSkills: 'No matching skills',
|
noMatchingSkills: 'No matching skills',
|
||||||
noMatchingTools: 'No matching tools',
|
noMatchingTools: 'No matching tools',
|
||||||
noProviderPreferences: 'No preferences set — the agent uses the global fallback chain order.',
|
noProviderPreferences: 'No preferences set — the agent uses the global fallback chain order.',
|
||||||
|
wikiKicker: 'Primary Knowledge Base',
|
||||||
|
wikiTagline: 'Choose the default knowledge base this agent should prefer. Knowledge bases remain workspace-shared.',
|
||||||
|
wikiHint: 'Select this agent\'s primary knowledge base. When unset, workspace fallback applies.',
|
||||||
|
noKBs: 'No knowledge bases available',
|
||||||
|
noKB: 'No primary KB',
|
||||||
|
wikiPages: '{count} pages',
|
||||||
|
wikiLoadFailed: 'Failed to load knowledge bases',
|
||||||
contextHint: 'Manage context files (e.g. AGENT.md) that define this agent\'s behavior, knowledge, and instructions.',
|
contextHint: 'Manage context files (e.g. AGENT.md) that define this agent\'s behavior, knowledge, and instructions.',
|
||||||
goToContext: 'Edit Context Files',
|
goToContext: 'Edit Context Files',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1025,6 +1025,7 @@ export default {
|
|||||||
skills: '技能',
|
skills: '技能',
|
||||||
tools: '工具',
|
tools: '工具',
|
||||||
providers: '偏好提供商',
|
providers: '偏好提供商',
|
||||||
|
wiki: '知识库',
|
||||||
context: '上下文',
|
context: '上下文',
|
||||||
},
|
},
|
||||||
columns: {
|
columns: {
|
||||||
@ -1150,6 +1151,13 @@ export default {
|
|||||||
noMatchingSkills: '没有匹配的技能',
|
noMatchingSkills: '没有匹配的技能',
|
||||||
noMatchingTools: '没有匹配的工具',
|
noMatchingTools: '没有匹配的工具',
|
||||||
noProviderPreferences: '尚未配置偏好顺序,将按全局回退链顺序使用。',
|
noProviderPreferences: '尚未配置偏好顺序,将按全局回退链顺序使用。',
|
||||||
|
wikiKicker: '主知识库',
|
||||||
|
wikiTagline: '为智能体指定默认优先使用的知识库,所有知识库仍保持工作区共享。',
|
||||||
|
wikiHint: '选择此智能体的主知识库。未指定时会按工作区知识库回退。',
|
||||||
|
noKBs: '暂无可用知识库',
|
||||||
|
noKB: '未指定主库',
|
||||||
|
wikiPages: '{count} 页',
|
||||||
|
wikiLoadFailed: '加载知识库列表失败',
|
||||||
contextHint: '管理此智能体的上下文文件(如 AGENT.md),定义智能体的行为、知识和指令。',
|
contextHint: '管理此智能体的上下文文件(如 AGENT.md),定义智能体的行为、知识和指令。',
|
||||||
goToContext: '前往编辑上下文',
|
goToContext: '前往编辑上下文',
|
||||||
},
|
},
|
||||||
|
|||||||
@ -43,7 +43,9 @@ export interface Agent {
|
|||||||
enabled: boolean
|
enabled: boolean
|
||||||
icon?: string
|
icon?: string
|
||||||
tags?: string
|
tags?: string
|
||||||
workspaceBasePath?: string
|
workspaceBasePath?: string | null
|
||||||
|
/** Agent-level primary wiki KB. Null means use workspace fallback. */
|
||||||
|
primaryKbId?: string | number | null
|
||||||
/**
|
/**
|
||||||
* Explicit opt-out: drop every SKILL.md catalog entry from the system
|
* Explicit opt-out: drop every SKILL.md catalog entry from the system
|
||||||
* prompt and exclude skill-expanded tools. Independent of binding rows
|
* prompt and exclude skill-expanded tools. Independent of binding rows
|
||||||
|
|||||||
@ -225,6 +225,10 @@
|
|||||||
{{ t('agents.tabs.providers', 'Providers') }}
|
{{ t('agents.tabs.providers', 'Providers') }}
|
||||||
<span v-if="selectedProviderIds.length" class="tab-badge">{{ selectedProviderIds.length }}</span>
|
<span v-if="selectedProviderIds.length" class="tab-badge">{{ selectedProviderIds.length }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button v-if="editingAgent" class="modal-tab" :class="{ active: modalTab === 'wiki' }" @click="modalTab = 'wiki'">
|
||||||
|
{{ t('agents.tabs.wiki', 'Wiki') }}
|
||||||
|
<span v-if="selectedKBId" class="tab-badge">1</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Basic Tab -->
|
<!-- Basic Tab -->
|
||||||
@ -552,6 +556,43 @@
|
|||||||
>+ {{ p.name }}</button>
|
>+ {{ p.name }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Wiki / Knowledge Base Tab -->
|
||||||
|
<div v-if="modalTab === 'wiki'" class="binding-tab">
|
||||||
|
<div class="binding-intro">
|
||||||
|
<span class="binding-intro__kicker">{{ t('agents.binding.wikiKicker') }}</span>
|
||||||
|
<p class="binding-intro__tagline">{{ t('agents.binding.wikiTagline') }}</p>
|
||||||
|
</div>
|
||||||
|
<p class="binding-hint">{{ t('agents.binding.wikiHint') }}</p>
|
||||||
|
<div v-if="availableKBs.length === 0" class="binding-empty">{{ t('agents.binding.noKBs') }}</div>
|
||||||
|
<div v-else class="binding-list">
|
||||||
|
<label
|
||||||
|
class="binding-item"
|
||||||
|
:class="{ selected: selectedKBId === null }"
|
||||||
|
>
|
||||||
|
<input type="radio" name="kb-select" :checked="selectedKBId === null" class="binding-checkbox" @change="selectedKBId = null" />
|
||||||
|
<span class="binding-icon">🚫</span>
|
||||||
|
<div class="binding-info">
|
||||||
|
<span class="binding-name">{{ t('agents.binding.noKB') }}</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
v-for="kb in availableKBs"
|
||||||
|
:key="kb.id"
|
||||||
|
class="binding-item"
|
||||||
|
:class="{ selected: selectedKBId === String(kb.id) }"
|
||||||
|
>
|
||||||
|
<input type="radio" name="kb-select" :checked="selectedKBId === String(kb.id)" class="binding-checkbox" @change="selectedKBId = String(kb.id)" />
|
||||||
|
<span class="binding-icon">📚</span>
|
||||||
|
<div class="binding-info">
|
||||||
|
<span class="binding-name">{{ kb.name }}</span>
|
||||||
|
<span v-if="kb.description" class="binding-desc">{{ kb.description?.slice(0, 80) }}</span>
|
||||||
|
</div>
|
||||||
|
<!-- binding-version class reused for pageCount badge (same positioning as skill version) -->
|
||||||
|
<span v-if="kb.pageCount != null" class="binding-version">{{ t('agents.binding.wikiPages', { count: kb.pageCount }, `${kb.pageCount} pages`) }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button class="btn-secondary" @click="closeModal">{{ t('common.cancel') }}</button>
|
<button class="btn-secondary" @click="closeModal">{{ t('common.cancel') }}</button>
|
||||||
@ -570,7 +611,7 @@ import { useRoute, useRouter } from 'vue-router'
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { mcToast } from '@/composables/useMcToast'
|
import { mcToast } from '@/composables/useMcToast'
|
||||||
import { mcConfirm } from '@/components/common/useConfirm'
|
import { mcConfirm } from '@/components/common/useConfirm'
|
||||||
import { agentApi, agentBindingApi, modelApi, skillApi, toolApi, templateApi, liveApi } from '@/api/index'
|
import { agentApi, agentBindingApi, modelApi, skillApi, toolApi, templateApi, liveApi, wikiApi } from '@/api/index'
|
||||||
import type { Agent } from '@/types/index'
|
import type { Agent } from '@/types/index'
|
||||||
import SkillIcon from '@/components/common/SkillIcon.vue'
|
import SkillIcon from '@/components/common/SkillIcon.vue'
|
||||||
import SkillIconPicker from '@/components/common/SkillIconPicker.vue'
|
import SkillIconPicker from '@/components/common/SkillIconPicker.vue'
|
||||||
@ -597,7 +638,7 @@ const searchText = ref('')
|
|||||||
const activeFilter = ref('all')
|
const activeFilter = ref('all')
|
||||||
const showModal = ref(false)
|
const showModal = ref(false)
|
||||||
const editingAgent = ref<Agent | null>(null)
|
const editingAgent = ref<Agent | null>(null)
|
||||||
const modalTab = ref<'basic' | 'skills' | 'tools' | 'providers'>('basic')
|
const modalTab = ref<'basic' | 'skills' | 'tools' | 'providers' | 'wiki'>('basic')
|
||||||
/** RFC-090 §9.2 调整 B — Tool picker is an Advanced bypass; collapsed by
|
/** RFC-090 §9.2 调整 B — Tool picker is an Advanced bypass; collapsed by
|
||||||
* default but stays open as soon as the agent has any direct tool
|
* default but stays open as soon as the agent has any direct tool
|
||||||
* bindings, so existing users don't lose visibility on their picks. */
|
* bindings, so existing users don't lose visibility on their picks. */
|
||||||
@ -752,6 +793,9 @@ function onSkillToggle(skillId: number | string, event: Event) {
|
|||||||
}
|
}
|
||||||
const selectedSkillIds = ref<number[]>([])
|
const selectedSkillIds = ref<number[]>([])
|
||||||
const selectedToolNames = ref<string[]>([])
|
const selectedToolNames = ref<string[]>([])
|
||||||
|
// Agent-level primary wiki KB. KB visibility remains workspace-wide.
|
||||||
|
const availableKBs = ref<any[]>([])
|
||||||
|
const selectedKBId = ref<string | null>(null)
|
||||||
// RFC-009 PR-3: per-agent provider preference order
|
// RFC-009 PR-3: per-agent provider preference order
|
||||||
const availableProviders = ref<{ id: string; name: string }[]>([])
|
const availableProviders = ref<{ id: string; name: string }[]>([])
|
||||||
const selectedProviderIds = ref<string[]>([])
|
const selectedProviderIds = ref<string[]>([])
|
||||||
@ -789,6 +833,7 @@ const defaultForm = (): Partial<Agent> & { name: string; defaultThinkingLevel: s
|
|||||||
// Agent type declares this as `string | undefined`; using `undefined` keeps
|
// Agent type declares this as `string | undefined`; using `undefined` keeps
|
||||||
// the Partial<Agent> shape happy without widening the type to allow null.
|
// the Partial<Agent> shape happy without widening the type to allow null.
|
||||||
workspaceBasePath: undefined,
|
workspaceBasePath: undefined,
|
||||||
|
primaryKbId: null,
|
||||||
// Issue #184 — explicit opt-out flags. Default false matches the legacy
|
// Issue #184 — explicit opt-out flags. Default false matches the legacy
|
||||||
// "zero rows = inherit global default" contract for newly-created agents.
|
// "zero rows = inherit global default" contract for newly-created agents.
|
||||||
skillsDisabled: false,
|
skillsDisabled: false,
|
||||||
@ -914,6 +959,8 @@ function openBlankCreateModal() {
|
|||||||
selectedSkillIds.value = []
|
selectedSkillIds.value = []
|
||||||
selectedToolNames.value = []
|
selectedToolNames.value = []
|
||||||
selectedProviderIds.value = []
|
selectedProviderIds.value = []
|
||||||
|
availableKBs.value = []
|
||||||
|
selectedKBId.value = null
|
||||||
showModal.value = true
|
showModal.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -981,6 +1028,7 @@ async function openEditModal(agent: Agent) {
|
|||||||
enabled: agent.enabled,
|
enabled: agent.enabled,
|
||||||
defaultThinkingLevel: (agent as any).defaultThinkingLevel || null,
|
defaultThinkingLevel: (agent as any).defaultThinkingLevel || null,
|
||||||
workspaceBasePath: agent.workspaceBasePath || undefined,
|
workspaceBasePath: agent.workspaceBasePath || undefined,
|
||||||
|
primaryKbId: agent.primaryKbId != null ? String(agent.primaryKbId) : null,
|
||||||
skillsDisabled: agent.skillsDisabled === true,
|
skillsDisabled: agent.skillsDisabled === true,
|
||||||
toolsDisabled: agent.toolsDisabled === true,
|
toolsDisabled: agent.toolsDisabled === true,
|
||||||
}
|
}
|
||||||
@ -1022,7 +1070,20 @@ async function openEditModal(agent: Agent) {
|
|||||||
.filter((b: any) => b.enabled)
|
.filter((b: any) => b.enabled)
|
||||||
.map((b: any) => b.providerId)
|
.map((b: any) => b.providerId)
|
||||||
} catch {
|
} catch {
|
||||||
// Non-blocking: binding data load failure doesn't prevent editing basic info
|
mcToast.error(t('agents.messages.loadFailed'))
|
||||||
|
}
|
||||||
|
|
||||||
|
// KB request is caught separately so its error message is accurate.
|
||||||
|
try {
|
||||||
|
const kbsRes: any = await wikiApi.listBindableKBs()
|
||||||
|
const bindableKBs = (kbsRes.data || []) as any[]
|
||||||
|
availableKBs.value = bindableKBs
|
||||||
|
const primaryKbId = agent.primaryKbId != null ? String(agent.primaryKbId) : null
|
||||||
|
selectedKBId.value = primaryKbId && bindableKBs.some((kb: any) => String(kb.id) === primaryKbId)
|
||||||
|
? primaryKbId
|
||||||
|
: null
|
||||||
|
} catch {
|
||||||
|
mcToast.error(t('agents.binding.wikiLoadFailed'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1031,6 +1092,8 @@ function closeModal() {
|
|||||||
editingAgent.value = null
|
editingAgent.value = null
|
||||||
skillBindingSearch.value = ''
|
skillBindingSearch.value = ''
|
||||||
toolBindingSearch.value = ''
|
toolBindingSearch.value = ''
|
||||||
|
availableKBs.value = []
|
||||||
|
selectedKBId.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveAgent() {
|
async function saveAgent() {
|
||||||
@ -1039,7 +1102,7 @@ async function saveAgent() {
|
|||||||
// sending to the backend — the schema is unchanged, only the editor
|
// sending to the backend — the schema is unchanged, only the editor
|
||||||
// exposes the H2 sections to the user.
|
// exposes the H2 sections to the user.
|
||||||
const serialized = serializePrompt(profileForm.value)
|
const serialized = serializePrompt(profileForm.value)
|
||||||
const payload = { ...form.value, systemPrompt: serialized }
|
const payload = { ...form.value, systemPrompt: serialized, primaryKbId: selectedKBId.value }
|
||||||
|
|
||||||
let agentId: string | number
|
let agentId: string | number
|
||||||
if (editingAgent.value) {
|
if (editingAgent.value) {
|
||||||
@ -1528,6 +1591,7 @@ html.dark .seg-count.warn {
|
|||||||
.binding-info { flex: 1; display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
.binding-info { flex: 1; display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
||||||
.binding-name { font-size: 14px; font-weight: 500; color: var(--mc-text-primary); }
|
.binding-name { font-size: 14px; font-weight: 500; color: var(--mc-text-primary); }
|
||||||
.binding-desc { font-size: 12px; color: var(--mc-text-tertiary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
.binding-desc { font-size: 12px; color: var(--mc-text-tertiary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
/* reused for KB pageCount badge in wiki tab */
|
||||||
.binding-version { font-size: 11px; color: var(--mc-text-tertiary); flex-shrink: 0; }
|
.binding-version { font-size: 11px; color: var(--mc-text-tertiary); flex-shrink: 0; }
|
||||||
.binding-type-badge {
|
.binding-type-badge {
|
||||||
font-size: 10px; padding: 2px 6px; border-radius: 4px; flex-shrink: 0;
|
font-size: 10px; padding: 2px 6px; border-radius: 4px; flex-shrink: 0;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user