mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
feat(agent): add wiki_disabled opt-out flag for knowledge bases
Issue #304. Operators who want an agent with NO knowledge base had no way to express it: leaving the KB picker empty fell through to "inherit workspace-wide" (every KB visible), so the agent ended up ingesting every KB's context. This adds the same opt-out toggle that skills_disabled (V126) / tools_disabled already provide. Backend: - V154 migration (h2 + mysql + kingbase): mate_agent.wiki_disabled BOOLEAN/TINYINT/SMALLINT NOT NULL DEFAULT FALSE. Legacy agents stay bit-identical. - AgentEntity.wikiDisabled: Boolean field, @TableField("wiki_disabled"). - AgentBindingService.getBoundKbIds: short-circuit at the top — wiki_disabled=true returns Set.of() regardless of binding rows. Mirrors the precedence contract of getBoundSkillIds vs skills_disabled. - AgentBindingService.setKbBindings: a non-empty save auto-clears a stale wiki_disabled flag (same contract as setSkillBindings / setToolBindings on their respective flags). Empty saves leave the flag untouched — the UI toggle owns the bit, not the binding writer. - AgentBindingServiceWikiDisabledTest: 5 cases covering all three return states + the stale-flag auto-clear + empty-save no-op. Frontend: - Agents.vue KB picker: add the "此智能体不使用任何知识库" / "This agent uses no knowledge bases" toggle, mirroring the skills / tools picker layout. Tab badge shows "Off" when the toggle is on. - types/index.ts: add Agent.wikiDisabled?: boolean. - Save logic: when wikiDisabled is on, send an empty KB list (the setKbs contract then leaves the flag alone server-side, exactly as setSkills / setTools behave for their opt-out flags). - i18n (zh + en): new strings for toggle label, hint, badge, and the scope description shown when the toggle is on. Stacked on top of #382 (which introduced AgentBindingResolver .getBoundKbIds). No agent-runtime changes — wiki tools already degrade cleanly when getBoundKbIds returns Set.of().
This commit is contained in:
parent
0ab11f8922
commit
22a212a2e6
@ -980,16 +980,26 @@ public class AgentBindingService implements AgentBindingResolver {
|
||||
* {@link #getBoundSkillIds}):
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@code null} — no binding rows. Caller treats this as "no
|
||||
* agent-level restriction; inherit every KB in the agent's
|
||||
* workspace" (the default wiki-tool behavior).</li>
|
||||
* <li>{@code Set.of()} — rows exist but none are {@code enabled=true}.
|
||||
* Caller treats this as "explicitly scoped to zero KBs".</li>
|
||||
* <li>{@code null} — {@code wiki_disabled=false} AND no binding rows.
|
||||
* Caller treats this as "no agent-level restriction; inherit every
|
||||
* KB in the agent's workspace" (the default wiki-tool behavior).</li>
|
||||
* <li>{@code Set.of()} — either {@code wiki_disabled=true}, or binding
|
||||
* rows exist but none are {@code enabled=true}. Caller treats this
|
||||
* as "explicitly scoped to zero KBs" — wiki tools degrade with
|
||||
* their standard "no knowledge base" message.</li>
|
||||
* <li>non-empty set — the explicit allowlist.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>The {@code wiki_disabled} flag takes precedence over row count, so
|
||||
* a stale (flag + leftover rows) combination still surfaces as "no KBs".
|
||||
* Mirrors how {@code skills_disabled} interacts with
|
||||
* {@link #getBoundSkillIds}.
|
||||
*/
|
||||
@Override
|
||||
public Set<Long> getBoundKbIds(Long agentId) {
|
||||
if (isWikiDisabled(agentId)) {
|
||||
return Set.of();
|
||||
}
|
||||
List<AgentWikiKbBinding> bindings = listKbBindings(agentId);
|
||||
if (bindings.isEmpty()) {
|
||||
return null;
|
||||
@ -1023,6 +1033,12 @@ public class AgentBindingService implements AgentBindingResolver {
|
||||
for (Long kbId : distinct) {
|
||||
requireKbInAgentWorkspace(agentId, kbId);
|
||||
}
|
||||
// Auto-clear wiki_disabled on a non-empty save — same contract as
|
||||
// setSkillBindings: a concrete KB commitment contradicts an opt-out
|
||||
// flag, so the data layer must never hold both states at once.
|
||||
if (!distinct.isEmpty()) {
|
||||
clearWikiDisabledFlag(agentId);
|
||||
}
|
||||
kbBindingMapper.delete(
|
||||
new LambdaQueryWrapper<AgentWikiKbBinding>()
|
||||
.eq(AgentWikiKbBinding::getAgentId, agentId));
|
||||
@ -1118,4 +1134,28 @@ public class AgentBindingService implements AgentBindingResolver {
|
||||
update.setToolsDisabled(false);
|
||||
agentMapper.updateById(update);
|
||||
}
|
||||
|
||||
/** Mirror of {@link #isSkillsDisabled} for the wiki/knowledge-base opt-out toggle. */
|
||||
private boolean isWikiDisabled(Long agentId) {
|
||||
if (agentId == null) return false;
|
||||
AgentEntity agent = agentMapper.selectById(agentId);
|
||||
return agent != null && Boolean.TRUE.equals(agent.getWikiDisabled());
|
||||
}
|
||||
|
||||
/**
|
||||
* Mirror of {@link #clearSkillsDisabledFlag} for the wiki toggle. Used as
|
||||
* an auto-clear step in {@link #setKbBindings} so a concrete KB commitment
|
||||
* always wins over a stale opt-out flag.
|
||||
*/
|
||||
private void clearWikiDisabledFlag(Long agentId) {
|
||||
if (agentId == null) return;
|
||||
AgentEntity agent = agentMapper.selectById(agentId);
|
||||
if (agent == null || !Boolean.TRUE.equals(agent.getWikiDisabled())) {
|
||||
return;
|
||||
}
|
||||
AgentEntity update = new AgentEntity();
|
||||
update.setId(agentId);
|
||||
update.setWikiDisabled(false);
|
||||
agentMapper.updateById(update);
|
||||
}
|
||||
}
|
||||
|
||||
@ -131,6 +131,22 @@ public class AgentEntity {
|
||||
@TableField(value = "tools_disabled")
|
||||
private Boolean toolsDisabled;
|
||||
|
||||
/**
|
||||
* Explicit opt-out from every knowledge base. When {@code true},
|
||||
* {@code AgentBindingService.getBoundKbIds} returns
|
||||
* {@link java.util.Collections#emptySet()} and the wiki tools degrade with
|
||||
* their standard "no knowledge base" message; the webchat
|
||||
* {@code /wiki/pages} picker endpoint returns an empty list. Without this
|
||||
* flag, leaving the KB picker empty means "inherit workspace-wide" — every
|
||||
* KB visible — which is the right default but leaves no way to express
|
||||
* "this agent intentionally uses no KB" (issue #304).
|
||||
*
|
||||
* <p>Same defaulting / auto-clear / update strategy contract as
|
||||
* {@link #skillsDisabled}.
|
||||
*/
|
||||
@TableField(value = "wiki_disabled")
|
||||
private Boolean wikiDisabled;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
-- V154: Wiki/knowledge-base opt-out flag on mate_agent.
|
||||
--
|
||||
-- Mirrors skills_disabled (V126) / tools_disabled. Without this column an
|
||||
-- operator who wants an agent with NO knowledge base has no way to express
|
||||
-- that intent: leaving the KB picker empty means "inherit workspace-wide"
|
||||
-- (every KB visible), so the agent ends up ingesting every KB's context.
|
||||
-- issue #304.
|
||||
--
|
||||
-- Defaults to FALSE so legacy agents stay bit-identical.
|
||||
ALTER TABLE mate_agent ADD COLUMN IF NOT EXISTS wiki_disabled BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
@ -0,0 +1,3 @@
|
||||
-- V154: Wiki/knowledge-base opt-out flag on mate_agent (issue #304).
|
||||
-- Mirrors skills_disabled / tools_disabled. Defaults to FALSE.
|
||||
ALTER TABLE mate_agent ADD COLUMN IF NOT EXISTS wiki_disabled SMALLINT NOT NULL DEFAULT 0;
|
||||
@ -0,0 +1,3 @@
|
||||
-- V154: Wiki/knowledge-base opt-out flag on mate_agent (issue #304).
|
||||
-- Mirrors skills_disabled / tools_disabled. Defaults to FALSE.
|
||||
ALTER TABLE mate_agent ADD COLUMN IF NOT EXISTS wiki_disabled TINYINT(1) NOT NULL DEFAULT 0;
|
||||
@ -0,0 +1,158 @@
|
||||
package vip.mate.agent.binding;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import vip.mate.MateClawApplication;
|
||||
import vip.mate.agent.binding.service.AgentBindingService;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Issue #304 coverage for the {@code wiki_disabled} opt-out flag on
|
||||
* {@code mate_agent}. Mirrors the contract proven for {@code skills_disabled}
|
||||
* / {@code tools_disabled} in {@link AgentBindingServiceTest}: the flag flips
|
||||
* the "no binding rows" semantic from "inherit workspace-wide" to "explicitly
|
||||
* scoped to zero KBs", and a non-empty {@code setKbBindings} save auto-clears
|
||||
* a stale flag.
|
||||
*/
|
||||
@SpringBootTest(
|
||||
classes = MateClawApplication.class,
|
||||
webEnvironment = SpringBootTest.WebEnvironment.NONE
|
||||
)
|
||||
@TestPropertySource(properties = {
|
||||
"spring.datasource.url=jdbc:h2:mem:binding_wiki_${random.uuid};MODE=MySQL;DATABASE_TO_LOWER=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE;DB_CLOSE_DELAY=-1",
|
||||
"spring.ai.dashscope.api-key=test-key",
|
||||
"spring.main.web-application-type=none"
|
||||
})
|
||||
class AgentBindingServiceWikiDisabledTest {
|
||||
|
||||
private static final long AGENT_ID = 9_500_011L;
|
||||
private static final long KB_ID_A = 9_500_101L;
|
||||
private static final long KB_ID_B = 9_500_102L;
|
||||
|
||||
@Autowired private AgentBindingService bindingService;
|
||||
@Autowired private JdbcTemplate jdbc;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
jdbc.update("DELETE FROM mate_agent_wiki_kb WHERE agent_id = ?", AGENT_ID);
|
||||
jdbc.update("DELETE FROM mate_agent WHERE id = ?", AGENT_ID);
|
||||
jdbc.update("DELETE FROM mate_wiki_knowledge_base WHERE id IN (?, ?)", KB_ID_A, KB_ID_B);
|
||||
|
||||
jdbc.update(
|
||||
"MERGE INTO mate_agent (id, name, agent_type, system_prompt, max_iterations, enabled, " +
|
||||
"workspace_id, skills_disabled, tools_disabled, wiki_disabled, " +
|
||||
"create_time, update_time, deleted) " +
|
||||
"KEY(id) VALUES (?, 'wiki-disabled-agent', 'react', '', 10, TRUE, 1, " +
|
||||
"FALSE, FALSE, FALSE, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 0)",
|
||||
AGENT_ID);
|
||||
// Two KBs in workspace 1 so we can prove "no rows → inherit" surfaces them
|
||||
// and "disabled → Set.of()" hides them, without relying on fixture data.
|
||||
for (long kbId : new long[]{KB_ID_A, KB_ID_B}) {
|
||||
jdbc.update("MERGE INTO mate_wiki_knowledge_base (id, name, description, status, " +
|
||||
"page_count, raw_count, workspace_id, create_time, update_time, deleted) " +
|
||||
"KEY(id) VALUES (?, ?, ?, 'active', 0, 0, 1, " +
|
||||
"CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 0)",
|
||||
kbId, "kb-" + kbId, "desc-" + kbId);
|
||||
}
|
||||
}
|
||||
|
||||
private void setWikiDisabledFlag(boolean value) {
|
||||
jdbc.update("UPDATE mate_agent SET wiki_disabled = ? WHERE id = ?", value, AGENT_ID);
|
||||
}
|
||||
|
||||
private boolean readWikiDisabledFlag() {
|
||||
Boolean v = jdbc.queryForObject(
|
||||
"SELECT wiki_disabled FROM mate_agent WHERE id = ?",
|
||||
Boolean.class, AGENT_ID);
|
||||
return Boolean.TRUE.equals(v);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("issue #304: wiki_disabled=false + no binding rows → null (inherit workspace-wide)")
|
||||
void noRowsReturnsNullWhenNotDisabled() {
|
||||
// Pre-V154 behavior preserved: an agent with no KB rows and no opt-out
|
||||
// flag inherits every KB in the workspace. The webchat picker and wiki
|
||||
// tools rely on null meaning "no restriction" to fall through to
|
||||
// workspace-wide retrieval.
|
||||
assertThat(bindingService.getBoundKbIds(AGENT_ID)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("issue #304: wiki_disabled=true → Set.of() (NOT null) even with binding rows")
|
||||
void disabledFlagReturnsEmptyEvenWithRows() {
|
||||
// Seed a binding row so we can prove the flag wins over row count.
|
||||
// Stale (flag + leftover rows) is exactly the contradiction the
|
||||
// auto-clear in setKbBindings is designed to prevent; this test pins
|
||||
// that getBoundKbIds stays defensive when state drifts.
|
||||
jdbc.update("MERGE INTO mate_agent_wiki_kb (id, agent_id, kb_id, enabled, " +
|
||||
"create_time, update_time, deleted) " +
|
||||
"KEY(id) VALUES (?, ?, ?, TRUE, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 0)",
|
||||
9_500_201L, AGENT_ID, KB_ID_A);
|
||||
setWikiDisabledFlag(true);
|
||||
|
||||
Set<Long> result = bindingService.getBoundKbIds(AGENT_ID);
|
||||
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("issue #304: wiki_disabled=false + binding rows → the explicit allowlist")
|
||||
void bindingRowsReturnAllowlistWhenNotDisabled() {
|
||||
// Standard three-state contract: non-empty bindings + flag off returns
|
||||
// the enabled KB ids, not null.
|
||||
jdbc.update("MERGE INTO mate_agent_wiki_kb (id, agent_id, kb_id, enabled, " +
|
||||
"create_time, update_time, deleted) " +
|
||||
"KEY(id) VALUES (?, ?, ?, TRUE, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 0)",
|
||||
9_500_202L, AGENT_ID, KB_ID_A);
|
||||
jdbc.update("MERGE INTO mate_agent_wiki_kb (id, agent_id, kb_id, enabled, " +
|
||||
"create_time, update_time, deleted) " +
|
||||
"KEY(id) VALUES (?, ?, ?, TRUE, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 0)",
|
||||
9_500_203L, AGENT_ID, KB_ID_B);
|
||||
|
||||
Set<Long> result = bindingService.getBoundKbIds(AGENT_ID);
|
||||
|
||||
assertThat(result).containsExactlyInAnyOrder(KB_ID_A, KB_ID_B);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("issue #304: setKbBindings non-empty save auto-clears a stale wiki_disabled flag")
|
||||
void setKbBindingsClearsStaleFlag() {
|
||||
// Set up the contradiction: flag on, then operator saves a real
|
||||
// binding. setKbBindings must clear the flag — same contract as
|
||||
// setSkillBindings / setToolBindings on the skills_disabled and
|
||||
// tools_disabled flags.
|
||||
setWikiDisabledFlag(true);
|
||||
assertThat(readWikiDisabledFlag()).isTrue();
|
||||
|
||||
bindingService.setKbBindings(AGENT_ID, java.util.List.of(KB_ID_A));
|
||||
|
||||
assertThat(readWikiDisabledFlag())
|
||||
.as("a concrete KB commitment must clear wiki_disabled so the data layer never holds both states at once")
|
||||
.isFalse();
|
||||
// And the flag clear surfaces in getBoundKbIds — the new binding is
|
||||
// honored, not silently masked by a stale opt-out.
|
||||
assertThat(bindingService.getBoundKbIds(AGENT_ID)).containsExactly(KB_ID_A);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("issue #304: setKbBindings empty save leaves the flag untouched (UI toggle owns the bit)")
|
||||
void setKbBindingsEmptySaveLeavesFlagUntouched() {
|
||||
// Empty / null save is ambiguous: "uncheck everything" vs "I never
|
||||
// had any". The UI opt-out toggle owns the bit, not the binding
|
||||
// writer. Mirrors setSkillBindings empty-save semantics so the four-
|
||||
// state matrix stays consistent across skill/tool/wiki pickers.
|
||||
setWikiDisabledFlag(true);
|
||||
bindingService.setKbBindings(AGENT_ID, java.util.List.of());
|
||||
assertThat(readWikiDisabledFlag()).isTrue();
|
||||
assertThat(bindingService.getBoundKbIds(AGENT_ID)).isEmpty();
|
||||
}
|
||||
}
|
||||
@ -1343,6 +1343,10 @@ export default {
|
||||
wikiHint: 'Tick the knowledge bases this agent may access; mark one as the default (used by wiki tools when no kbId/kbName is given).',
|
||||
wikiScopeAll: 'No knowledge base selected: this agent can reach every KB in the current workspace.',
|
||||
wikiScopeLimited: 'This agent can only reach the {count} selected knowledge base(s).',
|
||||
wikiScopeNone: '"No knowledge bases" is on: this agent cannot reach any KB, and every wiki tool returns "no knowledge base".',
|
||||
disableAllWiki: 'This agent uses no knowledge bases',
|
||||
disableAllWikiHint: 'Saving with this on clears the agent\'s KB bindings and marks it as "explicitly no KBs": wiki_read_page / wiki_search_pages / etc. return "no knowledge base", and the webchat /wiki/pages endpoint returns an empty list. When off, picking nothing still falls back to "can reach every KB in the workspace".',
|
||||
disableAllWikiBadge: 'Off',
|
||||
wikiSetPrimary: 'Set default',
|
||||
wikiPrimary: 'Default',
|
||||
noKBs: 'No knowledge bases available',
|
||||
|
||||
@ -1234,6 +1234,10 @@ export default {
|
||||
wikiHint: '勾选此智能体允许访问的知识库;可将其中一个设为默认(未指定 kbId/kbName 时优先使用)。',
|
||||
wikiScopeAll: '未勾选任何知识库:此智能体可访问当前工作区内的全部知识库。',
|
||||
wikiScopeLimited: '此智能体仅能访问已勾选的 {count} 个知识库。',
|
||||
wikiScopeNone: '已开启「不使用任何知识库」:此智能体将无法访问任何知识库,所有 wiki 工具会返回空结果。',
|
||||
disableAllWiki: '此智能体不使用任何知识库',
|
||||
disableAllWikiHint: '开启后保存会清空该智能体的知识库绑定,并标记为「显式无知识库」:wiki_read_page / wiki_search_pages 等工具返回 "no knowledge base",webchat 的 /wiki/pages 端点返回空清单。关闭后,未勾选任何知识库仍按「可访问工作区内全部知识库」处理。',
|
||||
disableAllWikiBadge: '已禁用',
|
||||
wikiSetPrimary: '设为默认',
|
||||
wikiPrimary: '默认',
|
||||
noKBs: '暂无可用知识库',
|
||||
|
||||
@ -59,6 +59,13 @@ export interface Agent {
|
||||
* delegation primitives still pass through. Defaults to `false`.
|
||||
*/
|
||||
toolsDisabled?: boolean
|
||||
/**
|
||||
* Explicit opt-out: this agent sees zero knowledge bases regardless of
|
||||
* leftover `mate_agent_wiki_kb` rows. Wiki tools degrade with their
|
||||
* standard "no knowledge base" message; the webchat `/wiki/pages` picker
|
||||
* returns an empty list. Defaults to `false`. Issue #304.
|
||||
*/
|
||||
wikiDisabled?: boolean
|
||||
createTime?: string
|
||||
updateTime?: string
|
||||
}
|
||||
|
||||
@ -256,7 +256,8 @@
|
||||
</button>
|
||||
<button v-if="editingAgent" class="modal-tab" :class="{ active: modalTab === 'wiki' }" @click="modalTab = 'wiki'">
|
||||
{{ t('agents.tabs.wiki', 'Wiki') }}
|
||||
<span v-if="selectedKbIds.length" class="tab-badge">{{ selectedKbIds.length }}</span>
|
||||
<span v-if="form.wikiDisabled" class="tab-badge tab-badge--off">{{ t('agents.binding.disableAllWikiBadge') }}</span>
|
||||
<span v-else-if="selectedKbIds.length" class="tab-badge">{{ selectedKbIds.length }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -622,24 +623,46 @@
|
||||
<span class="binding-intro__kicker">{{ t('agents.binding.wikiKicker') }}</span>
|
||||
<p class="binding-intro__tagline">{{ t('agents.binding.wikiTagline') }}</p>
|
||||
</div>
|
||||
<!-- Issue #304: explicit "no KBs" toggle. Empty selection alone
|
||||
falls through to "inherit workspace-wide" (every KB visible,
|
||||
context bloat for agents that don't need a KB), so an operator
|
||||
who wants zero KBs needs this dedicated bit — mirrors the
|
||||
skills_disabled / tools_disabled pattern. -->
|
||||
<div class="binding-disable-row">
|
||||
<label class="binding-disable-label">
|
||||
<input type="checkbox" v-model="form.wikiDisabled" class="binding-disable-checkbox" />
|
||||
<span class="binding-disable-text">
|
||||
<strong>{{ t('agents.binding.disableAllWiki') }}</strong>
|
||||
<span class="binding-disable-hint">{{ t('agents.binding.disableAllWikiHint') }}</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<p class="binding-hint">{{ t('agents.binding.wikiHint') }}</p>
|
||||
<div v-if="availableKBs.length === 0" class="binding-empty">{{ t('agents.binding.noKBs') }}</div>
|
||||
<template v-else>
|
||||
<p class="binding-hint" :class="{ 'binding-hint--warn': selectedKbIds.length > 0 }">
|
||||
{{ selectedKbIds.length === 0 ? t('agents.binding.wikiScopeAll') : t('agents.binding.wikiScopeLimited', { count: selectedKbIds.length }) }}
|
||||
<p class="binding-hint" :class="{ 'binding-hint--warn': !form.wikiDisabled && selectedKbIds.length > 0 }">
|
||||
<template v-if="form.wikiDisabled">{{ t('agents.binding.wikiScopeNone') }}</template>
|
||||
<template v-else>{{ selectedKbIds.length === 0 ? t('agents.binding.wikiScopeAll') : t('agents.binding.wikiScopeLimited', { count: selectedKbIds.length }) }}</template>
|
||||
</p>
|
||||
<div class="binding-list">
|
||||
<div class="binding-list" :class="{ 'binding-list--disabled': form.wikiDisabled }">
|
||||
<label
|
||||
v-for="kb in availableKBs"
|
||||
:key="kb.id"
|
||||
class="binding-item"
|
||||
:class="{ selected: isKbInScope(kb.id) }"
|
||||
:class="{
|
||||
selected: !form.wikiDisabled && isKbInScope(kb.id),
|
||||
'binding-item--inert': form.wikiDisabled,
|
||||
}"
|
||||
>
|
||||
<!-- Manual :checked (not v-model) so flipping the toggle
|
||||
back on restores the previous picks in one click. Same
|
||||
inert-checkbox trick the skills picker uses (issue #184). -->
|
||||
<input
|
||||
type="checkbox"
|
||||
class="binding-checkbox"
|
||||
:checked="isKbInScope(kb.id)"
|
||||
:checked="!form.wikiDisabled && isKbInScope(kb.id)"
|
||||
@change="toggleKbScope(kb.id)"
|
||||
:disabled="form.wikiDisabled"
|
||||
/>
|
||||
<span class="binding-icon">📚</span>
|
||||
<div class="binding-info">
|
||||
@ -650,7 +673,8 @@
|
||||
<button
|
||||
type="button"
|
||||
class="kb-primary-toggle"
|
||||
:class="{ 'kb-primary-toggle--active': selectedKBId === String(kb.id) }"
|
||||
:class="{ 'kb-primary-toggle--active': !form.wikiDisabled && selectedKBId === String(kb.id) }"
|
||||
:disabled="form.wikiDisabled"
|
||||
:title="t('agents.binding.wikiSetPrimary')"
|
||||
@click.prevent.stop="setPrimaryKb(kb.id)"
|
||||
>{{ selectedKBId === String(kb.id) ? t('agents.binding.wikiPrimary') : t('agents.binding.wikiSetPrimary') }}</button>
|
||||
@ -945,6 +969,10 @@ const defaultForm = (): Partial<Agent> & { name: string; defaultThinkingLevel: s
|
||||
// "zero rows = inherit global default" contract for newly-created agents.
|
||||
skillsDisabled: false,
|
||||
toolsDisabled: false,
|
||||
// Issue #304 — same contract for the wiki/knowledge-base picker. Empty
|
||||
// selection alone would fall through to "inherit workspace-wide", so an
|
||||
// operator who wants zero KBs in the context needs this dedicated bit.
|
||||
wikiDisabled: false,
|
||||
})
|
||||
|
||||
const form = ref(defaultForm())
|
||||
@ -1259,6 +1287,7 @@ async function openEditModal(agent: Agent) {
|
||||
primaryKbId: agent.primaryKbId != null ? String(agent.primaryKbId) : null,
|
||||
skillsDisabled: agent.skillsDisabled === true,
|
||||
toolsDisabled: agent.toolsDisabled === true,
|
||||
wikiDisabled: (agent as any).wikiDisabled === true,
|
||||
}
|
||||
tagInput.value = ''
|
||||
recentlyRemovedTag.value = null
|
||||
@ -1377,9 +1406,12 @@ async function saveAgent() {
|
||||
await agentBindingApi.setSkills(agentId, skillIdsToSave)
|
||||
await agentBindingApi.setTools(agentId, toolNamesToSave)
|
||||
await agentBindingApi.setProviderPreferences(agentId, selectedProviderIds.value)
|
||||
// KB access scope. Empty = unrestricted (workspace-wide). Sent as
|
||||
// strings per the Snowflake-precision contract.
|
||||
await agentBindingApi.setKbs(agentId, selectedKbIds.value)
|
||||
// KB access scope. Issue #304: when wiki_disabled is on the agent
|
||||
// sees zero KBs regardless of the binding list, so clear the save
|
||||
// payload — same pattern as skills/tools above. Empty save leaves
|
||||
// the wiki_disabled flag untouched (the toggle owns the bit).
|
||||
const kbIdsToSave = form.value.wikiDisabled ? [] : selectedKbIds.value
|
||||
await agentBindingApi.setKbs(agentId, kbIdsToSave)
|
||||
} catch (bindingError: any) {
|
||||
mcToast.error(bindingError?.message || t('agents.messages.saveFailed'))
|
||||
// Pull the authoritative server state back into the editing form so
|
||||
|
||||
Loading…
Reference in New Issue
Block a user