mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 03:55:09 +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}):
|
* {@link #getBoundSkillIds}):
|
||||||
*
|
*
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>{@code null} — no binding rows. Caller treats this as "no
|
* <li>{@code null} — {@code wiki_disabled=false} AND no binding rows.
|
||||||
* agent-level restriction; inherit every KB in the agent's
|
* Caller treats this as "no agent-level restriction; inherit every
|
||||||
* workspace" (the default wiki-tool behavior).</li>
|
* KB in the agent's workspace" (the default wiki-tool behavior).</li>
|
||||||
* <li>{@code Set.of()} — rows exist but none are {@code enabled=true}.
|
* <li>{@code Set.of()} — either {@code wiki_disabled=true}, or binding
|
||||||
* Caller treats this as "explicitly scoped to zero KBs".</li>
|
* 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>
|
* <li>non-empty set — the explicit allowlist.</li>
|
||||||
* </ul>
|
* </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
|
@Override
|
||||||
public Set<Long> getBoundKbIds(Long agentId) {
|
public Set<Long> getBoundKbIds(Long agentId) {
|
||||||
|
if (isWikiDisabled(agentId)) {
|
||||||
|
return Set.of();
|
||||||
|
}
|
||||||
List<AgentWikiKbBinding> bindings = listKbBindings(agentId);
|
List<AgentWikiKbBinding> bindings = listKbBindings(agentId);
|
||||||
if (bindings.isEmpty()) {
|
if (bindings.isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
@ -1023,6 +1033,12 @@ public class AgentBindingService implements AgentBindingResolver {
|
|||||||
for (Long kbId : distinct) {
|
for (Long kbId : distinct) {
|
||||||
requireKbInAgentWorkspace(agentId, kbId);
|
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(
|
kbBindingMapper.delete(
|
||||||
new LambdaQueryWrapper<AgentWikiKbBinding>()
|
new LambdaQueryWrapper<AgentWikiKbBinding>()
|
||||||
.eq(AgentWikiKbBinding::getAgentId, agentId));
|
.eq(AgentWikiKbBinding::getAgentId, agentId));
|
||||||
@ -1118,4 +1134,28 @@ public class AgentBindingService implements AgentBindingResolver {
|
|||||||
update.setToolsDisabled(false);
|
update.setToolsDisabled(false);
|
||||||
agentMapper.updateById(update);
|
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")
|
@TableField(value = "tools_disabled")
|
||||||
private Boolean toolsDisabled;
|
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)
|
@TableField(fill = FieldFill.INSERT)
|
||||||
private LocalDateTime createTime;
|
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).',
|
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.',
|
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).',
|
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',
|
wikiSetPrimary: 'Set default',
|
||||||
wikiPrimary: 'Default',
|
wikiPrimary: 'Default',
|
||||||
noKBs: 'No knowledge bases available',
|
noKBs: 'No knowledge bases available',
|
||||||
|
|||||||
@ -1234,6 +1234,10 @@ export default {
|
|||||||
wikiHint: '勾选此智能体允许访问的知识库;可将其中一个设为默认(未指定 kbId/kbName 时优先使用)。',
|
wikiHint: '勾选此智能体允许访问的知识库;可将其中一个设为默认(未指定 kbId/kbName 时优先使用)。',
|
||||||
wikiScopeAll: '未勾选任何知识库:此智能体可访问当前工作区内的全部知识库。',
|
wikiScopeAll: '未勾选任何知识库:此智能体可访问当前工作区内的全部知识库。',
|
||||||
wikiScopeLimited: '此智能体仅能访问已勾选的 {count} 个知识库。',
|
wikiScopeLimited: '此智能体仅能访问已勾选的 {count} 个知识库。',
|
||||||
|
wikiScopeNone: '已开启「不使用任何知识库」:此智能体将无法访问任何知识库,所有 wiki 工具会返回空结果。',
|
||||||
|
disableAllWiki: '此智能体不使用任何知识库',
|
||||||
|
disableAllWikiHint: '开启后保存会清空该智能体的知识库绑定,并标记为「显式无知识库」:wiki_read_page / wiki_search_pages 等工具返回 "no knowledge base",webchat 的 /wiki/pages 端点返回空清单。关闭后,未勾选任何知识库仍按「可访问工作区内全部知识库」处理。',
|
||||||
|
disableAllWikiBadge: '已禁用',
|
||||||
wikiSetPrimary: '设为默认',
|
wikiSetPrimary: '设为默认',
|
||||||
wikiPrimary: '默认',
|
wikiPrimary: '默认',
|
||||||
noKBs: '暂无可用知识库',
|
noKBs: '暂无可用知识库',
|
||||||
|
|||||||
@ -59,6 +59,13 @@ export interface Agent {
|
|||||||
* delegation primitives still pass through. Defaults to `false`.
|
* delegation primitives still pass through. Defaults to `false`.
|
||||||
*/
|
*/
|
||||||
toolsDisabled?: boolean
|
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
|
createTime?: string
|
||||||
updateTime?: string
|
updateTime?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@ -256,7 +256,8 @@
|
|||||||
</button>
|
</button>
|
||||||
<button v-if="editingAgent" class="modal-tab" :class="{ active: modalTab === 'wiki' }" @click="modalTab = 'wiki'">
|
<button v-if="editingAgent" class="modal-tab" :class="{ active: modalTab === 'wiki' }" @click="modalTab = 'wiki'">
|
||||||
{{ t('agents.tabs.wiki', '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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -622,24 +623,46 @@
|
|||||||
<span class="binding-intro__kicker">{{ t('agents.binding.wikiKicker') }}</span>
|
<span class="binding-intro__kicker">{{ t('agents.binding.wikiKicker') }}</span>
|
||||||
<p class="binding-intro__tagline">{{ t('agents.binding.wikiTagline') }}</p>
|
<p class="binding-intro__tagline">{{ t('agents.binding.wikiTagline') }}</p>
|
||||||
</div>
|
</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>
|
<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-if="availableKBs.length === 0" class="binding-empty">{{ t('agents.binding.noKBs') }}</div>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<p class="binding-hint" :class="{ 'binding-hint--warn': selectedKbIds.length > 0 }">
|
<p class="binding-hint" :class="{ 'binding-hint--warn': !form.wikiDisabled && selectedKbIds.length > 0 }">
|
||||||
{{ selectedKbIds.length === 0 ? t('agents.binding.wikiScopeAll') : t('agents.binding.wikiScopeLimited', { count: selectedKbIds.length }) }}
|
<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>
|
</p>
|
||||||
<div class="binding-list">
|
<div class="binding-list" :class="{ 'binding-list--disabled': form.wikiDisabled }">
|
||||||
<label
|
<label
|
||||||
v-for="kb in availableKBs"
|
v-for="kb in availableKBs"
|
||||||
:key="kb.id"
|
:key="kb.id"
|
||||||
class="binding-item"
|
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
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="binding-checkbox"
|
class="binding-checkbox"
|
||||||
:checked="isKbInScope(kb.id)"
|
:checked="!form.wikiDisabled && isKbInScope(kb.id)"
|
||||||
@change="toggleKbScope(kb.id)"
|
@change="toggleKbScope(kb.id)"
|
||||||
|
:disabled="form.wikiDisabled"
|
||||||
/>
|
/>
|
||||||
<span class="binding-icon">📚</span>
|
<span class="binding-icon">📚</span>
|
||||||
<div class="binding-info">
|
<div class="binding-info">
|
||||||
@ -650,7 +673,8 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="kb-primary-toggle"
|
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')"
|
:title="t('agents.binding.wikiSetPrimary')"
|
||||||
@click.prevent.stop="setPrimaryKb(kb.id)"
|
@click.prevent.stop="setPrimaryKb(kb.id)"
|
||||||
>{{ selectedKBId === String(kb.id) ? t('agents.binding.wikiPrimary') : t('agents.binding.wikiSetPrimary') }}</button>
|
>{{ 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.
|
// "zero rows = inherit global default" contract for newly-created agents.
|
||||||
skillsDisabled: false,
|
skillsDisabled: false,
|
||||||
toolsDisabled: 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())
|
const form = ref(defaultForm())
|
||||||
@ -1259,6 +1287,7 @@ async function openEditModal(agent: Agent) {
|
|||||||
primaryKbId: agent.primaryKbId != null ? String(agent.primaryKbId) : null,
|
primaryKbId: agent.primaryKbId != null ? String(agent.primaryKbId) : null,
|
||||||
skillsDisabled: agent.skillsDisabled === true,
|
skillsDisabled: agent.skillsDisabled === true,
|
||||||
toolsDisabled: agent.toolsDisabled === true,
|
toolsDisabled: agent.toolsDisabled === true,
|
||||||
|
wikiDisabled: (agent as any).wikiDisabled === true,
|
||||||
}
|
}
|
||||||
tagInput.value = ''
|
tagInput.value = ''
|
||||||
recentlyRemovedTag.value = null
|
recentlyRemovedTag.value = null
|
||||||
@ -1377,9 +1406,12 @@ async function saveAgent() {
|
|||||||
await agentBindingApi.setSkills(agentId, skillIdsToSave)
|
await agentBindingApi.setSkills(agentId, skillIdsToSave)
|
||||||
await agentBindingApi.setTools(agentId, toolNamesToSave)
|
await agentBindingApi.setTools(agentId, toolNamesToSave)
|
||||||
await agentBindingApi.setProviderPreferences(agentId, selectedProviderIds.value)
|
await agentBindingApi.setProviderPreferences(agentId, selectedProviderIds.value)
|
||||||
// KB access scope. Empty = unrestricted (workspace-wide). Sent as
|
// KB access scope. Issue #304: when wiki_disabled is on the agent
|
||||||
// strings per the Snowflake-precision contract.
|
// sees zero KBs regardless of the binding list, so clear the save
|
||||||
await agentBindingApi.setKbs(agentId, selectedKbIds.value)
|
// 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) {
|
} catch (bindingError: any) {
|
||||||
mcToast.error(bindingError?.message || t('agents.messages.saveFailed'))
|
mcToast.error(bindingError?.message || t('agents.messages.saveFailed'))
|
||||||
// Pull the authoritative server state back into the editing form so
|
// Pull the authoritative server state back into the editing form so
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user