Ver código fonte

工具包与工具展示收尾:弹框内部滚动、删除解绑方案、只读改内置

- 查看工具弹框正文限高 64vh、内部垂直滚动
- 删除工具包时从所有方案 tool_packs_json 摘掉该 id 并驱逐实例池(AgentService.removePackBinding)
- 工具卡片/详情「只读」标记改「内置」,移除「默认装备」标记
- 新增 docs/agent-data-model.md:工具/工具包/研判方案 表关系与绑定完整性规则
cc 3 dias atrás
pai
commit
965f51cdbe

+ 3 - 4
ai-electron/frontend/src/ai/views/aiPlugin/index.vue

@@ -207,12 +207,11 @@
             >
               <div class="ai-plugin__card-head">
                 <span class="ai-plugin__mono ai-plugin__tool-name ai-plugin__ellipsis">{{ tool.toolName }}</span>
-                <span v-if="tool.readOnly" class="ai-plugin__chip">只读</span>
+                <span v-if="tool.readOnly" class="ai-plugin__chip">内置</span>
               </div>
 
               <div class="ai-plugin__card-meta">
                 <span class="ai-plugin__chip">{{ tool.groupLabel || tool.groupCode || '未分组' }}</span>
-                <span v-if="tool.defaultActive" class="ai-plugin__chip is-success">默认装备</span>
               </div>
 
               <p class="ai-plugin__card-desc">{{ tool.description || '(无说明)' }}</p>
@@ -238,8 +237,7 @@
       <div v-if="selectedTool" class="ai-plugin__detail">
         <div class="ai-plugin__detail-tags">
           <span class="ai-plugin__chip">{{ selectedTool.groupLabel || selectedTool.groupCode || '未分组' }}</span>
-          <span v-if="selectedTool.defaultActive" class="ai-plugin__chip is-success">默认装备</span>
-          <span v-if="selectedTool.readOnly" class="ai-plugin__chip">只读</span>
+          <span v-if="selectedTool.readOnly" class="ai-plugin__chip">内置</span>
         </div>
 
         <div class="ai-plugin__detail-block">
@@ -260,6 +258,7 @@
       :title="`${selectedPack?.name || '工具包'} · 工具列表`"
       :width="720"
       :footer="null"
+      :body-style="{ maxHeight: '64vh', overflowY: 'auto' }"
       destroy-on-close
     >
       <div v-if="selectedPack" class="ai-plugin__detail">

+ 4 - 1
ai-server/src/main/java/com/zsjz/ai/module/agent/controller/AgentToolPackController.java

@@ -3,6 +3,7 @@ package com.zsjz.ai.module.agent.controller;
 import cn.dev33.satoken.stp.StpUtil;
 import com.zsjz.ai.common.config.Result;
 import com.zsjz.ai.module.agent.dto.SaveToolPackDTO;
+import com.zsjz.ai.module.agent.service.AgentService;
 import com.zsjz.ai.module.agent.service.ToolPackService;
 import com.zsjz.ai.module.agent.vo.ToolPackVO;
 import jakarta.validation.Valid;
@@ -38,6 +39,7 @@ import java.util.List;
 public class AgentToolPackController {
 
     private final ToolPackService toolPackService;
+    private final AgentService agentService;
 
     /** 工具包列表(仅当前用户的) */
     @GetMapping
@@ -57,10 +59,11 @@ public class AgentToolPackController {
         return Result.succeed(toolPackService.updatePack(currentUserId(), id, dto));
     }
 
-    /** 删除工具包(非本人拒绝;引用它的方案下次装配时自动忽略该包) */
+    /** 删除工具包(内置/非本人拒绝);同时从引用它的方案绑定里摘掉该 id 并驱逐实例池 */
     @DeleteMapping("/{id}")
     public Result<Void> deletePack(@PathVariable Long id) {
         toolPackService.deletePack(currentUserId(), id);
+        agentService.removePackBinding(id);
         return Result.succeed();
     }
 

+ 23 - 0
ai-server/src/main/java/com/zsjz/ai/module/agent/service/AgentService.java

@@ -405,6 +405,29 @@ public class AgentService extends ServiceImpl<AgentMapper, AgentEntity> {
         log.info("删除专家: rowId={}, name={}, userId={}", rowId, entity.getName(), userId);
     }
 
+    /**
+     * 工具包被删除后:从所有方案的绑定(tool_packs_json)里摘掉该 id,并驱逐受影响的实例池。
+     *
+     * <p>表关系口径:方案 ↔ 工具包是多对多(JSON id 数组松引用),包删除时由本方法做**引用清理**,
+     * 不留悬空绑定;即便漏调,buildAgent 的 resolveToolNames 也会跳过已删除的包(双保险)。
+     */
+    public void removePackBinding(Long packId) {
+        String idStr = String.valueOf(packId);
+        for (AgentEntity agent : lambdaQuery().isNotNull(AgentEntity::getToolPacksJson).list()) {
+            List<String> ids = readStringList(agent.getToolPacksJson());
+            if (ids == null || !ids.contains(idStr)) {
+                continue;
+            }
+            List<String> rest = ids.stream().filter(v -> !idStr.equals(v)).toList();
+            AgentEntity upd = new AgentEntity();
+            upd.setRowId(agent.getRowId());
+            upd.setToolPacksJson(writeStringList(rest));
+            updateById(upd);
+            evictAgentPool(agent.getRowId());
+            log.info("工具包解绑方案: packId={}, agentRowId={}", packId, agent.getRowId());
+        }
+    }
+
     /**
      * 驱逐某智能体的全部池化实例。池 key = w{ws}-a{rowId}-m{model}-u{user},
      * 配置(提示词/工具组/技能)变更后必须调用,否则旧实例继续生效。

+ 85 - 0
docs/agent-data-model.md

@@ -0,0 +1,85 @@
+# Agent 数据模型:工具 / 工具包 / 研判方案 的表关系
+
+> 适用范围:`ai-server` 的 `module/agent` 域 + `sql/agent_tool_pack.sql`(含内置种子)。
+> 结论先行:**工具 ↔ 工具包 ↔ 研判方案 是两级多对多**,都用 JSON 数组做松引用,
+> 完整性由「保存时校验 + 删除时清理 + 装配时跳过」三道防线保证(见文末规则表)。
+
+## 关系图
+
+```mermaid
+erDiagram
+    agent_tool ||--o{ agent_tool_pack_item : "tool_names[] 按名引用"
+    agent_tool_pack ||--o{ agent_tool_pack_item : "包内工具集合"
+    agent_tool_pack ||--o{ agent_pack_bind : "被方案勾选"
+    agent ||--o{ agent_pack_bind : "方案勾选的包"
+
+    agent_tool {
+        bigint id PK "雪花 ID"
+        varchar tool_name UK "工具名(@Tool name)"
+        varchar group_code "call/trans/track/graph/person/file/base"
+        varchar status "ACTIVE | DELETED"
+    }
+    agent_tool_pack {
+        bigint id PK "雪花 ID(内置包固定 1~6)"
+        bigint user_id "创建者;内置包 = 1"
+        varchar name "包名(user_id 内唯一)"
+        text tool_names "JSON 数组:tool_name 列表"
+        smallint is_builtin "1=内置(全局可见、不可改删)"
+    }
+    agent {
+        bigint row_id PK "identity 自增"
+        bigint user_id "创建者;内置方案 = 1"
+        smallint is_builtin "1=内置(不可改删)"
+        text tools_allow_json "JSON 数组:工具组编码白名单"
+        text tool_packs_json "JSON 数组:工具包 id 白名单"
+        text skills_allow_json "JSON 数组:技能名白名单"
+    }
+```
+
+> 图中 `agent_tool_pack_item` / `agent_pack_bind` 是**逻辑关系**(多对多的语义实体),
+> 物理上分别落在 `agent_tool_pack.tool_names` 与 `agent.tool_packs_json` 两个 JSON 数组列里,
+> 不建物理中间表:数据量级为个位数包 × 十位数工具,JSON 松引用 + 服务层校验更贴合本域。
+
+## 多对多怎么成立
+
+| 关系 | 基数 | 存储 | 说明 |
+|---|---|---|---|
+| 工具 ↔ 工具包 | **一个工具可出现在多个包**;一个包含多个工具 | `agent_tool_pack.tool_names[]`(工具名数组) | 同一个 `tool_name` 可以出现在任意多个包的数组里,互不影响 |
+| 工具包 ↔ 研判方案 | **一个包可被多个方案勾选**;一个方案可勾多个包 | `agent.tool_packs_json[]`(包 id 数组) | 同一个包 id 可以出现在任意多个方案的数组里;改包内容对所有引用它的方案**即时生效**(下次装配) |
+| 工具 ↔ 研判方案 | 传递多对多(经包)+ 直接的**工具组**白名单 | `agent.tools_allow_json[]`(组编码) | 方案最终可见的业务工具 = 所选工具组的全部工具 ∪ 所选工具包的工具;全不勾 = 不限制 |
+
+## ID 约定
+
+- `agent.row_id`:PostgreSQL identity 自增(存量默认方案 = 1「数刃」);
+- `agent_tool.id` / `agent_tool_pack.id`:应用侧雪花 ID(MyBatis-Plus `ASSIGN_ID`);
+  **内置工具包固定占用 1~6**(雪花 ID 远大于此,不会碰撞),供脚本直接引用;
+- 前端传输一律**字符串 ID**(避免 JS 精度丢失);JSON 数组里也是字符串(`["1","2"]`)。
+
+## 完整性规则(三道防线)
+
+| 时机 | 校验/清理 | 实现位置 |
+|---|---|---|
+| **保存工具包** | 工具名逐个对照 `AgentToolRegistry.registeredToolNames()`(业务工具;base 基础工具不进包),非法 400 | `ToolPackService.validateToolNames` |
+| **保存研判方案** | 引用的包必须存在且「内置或属于当前用户」,否则 400/404 | `ToolPackService.requireBindable`(由 `AgentService.validateExpertConfig` 调用) |
+| **删除工具包** | 内置拒绝;同时从所有方案的 `tool_packs_json` 摘掉该 id,并**驱逐受影响的 agent 实例池** | `AgentToolPackController.deletePack` → `AgentService.removePackBinding` |
+| **装配对话(buildAgent)** | 解析包 id:坏 id / 已删除的包跳过(双保险);组白名单 ∪ 包内工具 = 工具级白名单,白名单外业务工具 `removeTool` 真实注销 | `ToolPackService.resolveToolNames` + `AgentToolRegistry.registerBusinessToolsForWhitelist` |
+| **改方案配置** | 保存后驱逐该方案的池化实例(否则旧工具面继续生效) | `AgentService.updateExpert` → `evictAgentPool` |
+
+## 删除语义速查
+
+| 删除对象 | 对工具包 | 对研判方案 | 对对话实例 |
+|---|---|---|---|
+| 工具(代码下线) | 包里残留该名字 → 装配时自然无此工具(惰性失效),不报错 | — | — |
+| 工具包 | — | `tool_packs_json` 被摘掉该 id(不留悬空引用) | 受影响方案实例池被驱逐 |
+| 研判方案 | 不影响(包独立存在) | — | 该方案池清空;已建会话再对话报「智能体不存在」 |
+
+## 内置 vs 自建
+
+| | 内置(`is_builtin=1`,`user_id=1`) | 自建(`is_builtin=0`) |
+|---|---|---|
+| 工具包 | 全局可见、不可编辑/删除(服务端双重拒绝);id 固定 1~6 | 仅创建者可见/改删 |
+| 研判方案 | 全局可见、不可编辑/删除(如默认方案「数刃」) | 仅创建者可见/改删 |
+| 绑定 | 任何方案可勾选内置包 | 只能勾选自己的包(保存时校验) |
+
+默认方案(`agent.row_id=1`)绑定全部 6 个内置工具包(`sql/agent_tool_pack.sql` 维护;
+内置方案 UI 不可编辑,其绑定关系只能由该脚本写入)。