|
|
@@ -1,14 +1,13 @@
|
|
|
<template>
|
|
|
<!--
|
|
|
- 研判方案新建/编辑弹框:内容版式取自 docs/designs/新建方案页-设计稿(基本信息 + 工具组/技能卡片多选),
|
|
|
- 默认全屏打开(defaultFullscreen,右上角可还原为窗口弹框)。BasicModal 右上角自带
|
|
|
- 「还原/全屏 + 关闭」,ModalWrapper 按视口限高并在内部滚动 —— 内容再多也不会撑出屏幕。
|
|
|
+ 研判方案新建/编辑弹框:内容版式取自 docs/designs/新建方案页-设计稿(基本信息 + 工具包/技能卡片多选),
|
|
|
+ 默认按窗口弹框打开(不再 defaultFullscreen,右上角可随时全屏)。BasicModal 右上角自带
|
|
|
+ 「全屏/还原 + 关闭」,bodyStyle 按视口限高并在内部滚动 —— 内容再多也不会撑出屏幕。
|
|
|
-->
|
|
|
<BasicModal
|
|
|
:open="open"
|
|
|
:title="isEdit ? '编辑研判方案' : '新建研判方案'"
|
|
|
:width="1100"
|
|
|
- default-fullscreen
|
|
|
wrap-class-name="ae-agent-modal"
|
|
|
:confirm-loading="submitting"
|
|
|
:body-style="bodyStyle"
|
|
|
@@ -18,7 +17,7 @@
|
|
|
@cancel="close"
|
|
|
>
|
|
|
<div class="ae-form">
|
|
|
- <p class="ae-form__sub">组装提示词、工具组与技能,定义一个可参与研判的智能体。</p>
|
|
|
+ <p class="ae-form__sub">组装提示词、工具包与技能,定义一个可参与研判的智能体。</p>
|
|
|
|
|
|
<a-form ref="formRef" class="ae-form__form" :model="form" :rules="rules" layout="vertical">
|
|
|
<!-- 基本信息(左标签 + 控件行) -->
|
|
|
@@ -63,47 +62,7 @@
|
|
|
</div>
|
|
|
</section>
|
|
|
|
|
|
- <!-- 可用工具组(卡片多选:不勾 = 不限制) -->
|
|
|
- <section class="ae-form__panel">
|
|
|
- <div class="ae-form__sec-head">
|
|
|
- <span class="ae-form__sec-title">可用工具组</span>
|
|
|
- <span class="ae-form__count">{{ pickedGroups.length ? `已选 ${pickedGroups.length}` : '不限制' }}</span>
|
|
|
- <span class="ae-form__sp"></span>
|
|
|
- </div>
|
|
|
- <div class="ae-form__hint"
|
|
|
- >不勾选 =
|
|
|
- 不限制(全部业务组可用,默认装备人员画像组);勾选的工具组与工具包共同构成业务工具白名单,未选的工具对模型不可见。</div
|
|
|
- >
|
|
|
- <a-spin :spinning="toolsLoading">
|
|
|
- <div v-if="toolGroups.length" class="ae-form__grid">
|
|
|
- <div
|
|
|
- v-for="g in toolGroups"
|
|
|
- :key="g.group"
|
|
|
- class="ae-form__card"
|
|
|
- :class="{ 'is-on': checkedGroups[g.group] }"
|
|
|
- role="checkbox"
|
|
|
- :aria-checked="!!checkedGroups[g.group]"
|
|
|
- @click="checkedGroups[g.group] = !checkedGroups[g.group]"
|
|
|
- >
|
|
|
- <span class="ae-form__ck"><Icon icon="mdi:check" :size="11" /></span>
|
|
|
- <div class="ae-form__card-name">{{ g.label }}</div>
|
|
|
- <div class="ae-form__card-desc">{{ g.description }}</div>
|
|
|
- <div class="ae-form__chips"
|
|
|
- ><span>{{ g.count }} 个工具</span></div
|
|
|
- >
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div v-else class="ae-form__hint">暂无可用工具组</div>
|
|
|
- </a-spin>
|
|
|
- <div v-if="pickedGroups.length" class="ae-form__selbar">
|
|
|
- <span class="ae-form__sel-label">已选 {{ pickedGroups.length }} 组:</span>
|
|
|
- <span v-for="g in pickedGroups" :key="g" class="ae-form__sel-chip">{{ toolGroupLabel(g) }}</span>
|
|
|
- <span class="ae-form__sp"></span>
|
|
|
- <button type="button" class="ae-form__clear" @click="clearGroups">清空</button>
|
|
|
- </div>
|
|
|
- </section>
|
|
|
-
|
|
|
- <!-- 可用工具包(用户自定义工具组合;与工具组并集构成业务工具白名单) -->
|
|
|
+ <!-- 可用工具包(卡片多选:不勾 = 不限制);内置 6 个包与业务工具组同构,旧方案的工具组勾选打开时自动映射进来 -->
|
|
|
<section class="ae-form__panel">
|
|
|
<div class="ae-form__sec-head">
|
|
|
<span class="ae-form__sec-title">可用工具包</span>
|
|
|
@@ -111,8 +70,8 @@
|
|
|
<span class="ae-form__sp"></span>
|
|
|
</div>
|
|
|
<div class="ae-form__hint"
|
|
|
- >工具包在「插件与技能 → 工具包」维护;勾选的工具组与工具包共同构成业务工具白名单(只启用其中的工具),全不勾
|
|
|
- = 不限制。</div
|
|
|
+ >不勾选 =
|
|
|
+ 不限制(全部业务工具可用);勾选的工具包构成业务工具白名单,未选的工具对模型不可见。工具包在「插件与技能 → 工具包」维护。</div
|
|
|
>
|
|
|
<a-spin :spinning="packsLoading">
|
|
|
<div v-if="packs.length" class="ae-form__grid">
|
|
|
@@ -229,17 +188,20 @@
|
|
|
|
|
|
<script lang="ts" setup>
|
|
|
/**
|
|
|
- * 研判方案新建/编辑弹框:内容版式取自新建方案页设计稿(纯页面版试过被否,回弹框 + 默认全屏)。
|
|
|
+ * 研判方案新建/编辑弹框:内容版式取自新建方案页设计稿(纯页面版试过被否,回弹框,默认窗口态打开、可手动全屏)。
|
|
|
*
|
|
|
* 逻辑与历史弹框版完全一致:
|
|
|
- * - 工具/技能/工具包的勾选语义:**不勾 = 不限制**(tools_allow_json/skills_allow_json/tool_packs_json 落 null 走默认口径),
|
|
|
- * 勾选若干 = 只启用所选(工具组与工具包并集构成业务工具白名单,未选的对模型不可见);
|
|
|
- * - 候选数据:工具组从 capabilityApi.listTools() 按 groupCode 聚合(与「插件与技能」页同一数据源),
|
|
|
- * 技能取当前账号已安装的技能(listSkills),repo 来源的只读技能不参与勾选;
|
|
|
+ * - 工具/技能/工具包的勾选语义:**不勾 = 不限制**(skills_allow_json/tool_packs_json 落 null 走默认口径),
|
|
|
+ * 勾选若干 = 只启用所选(工具包构成业务工具白名单,未选的对模型不可见);
|
|
|
+ * - 「工具组就是工具包」:内置 6 个工具包与 6 个业务工具组同名同工具(AgentToolRegistry 逐个相等),
|
|
|
+ * 页面只保留「可用工具包」一区。旧方案的 tools_allow_json(工具组编码)打开时按组名映射成对应
|
|
|
+ * 内置包勾选、保存写 tool_packs_json;映射不到的组(包清单没加载出来等)留在 legacyGroups 回落写回,
|
|
|
+ * 不丢原有限制;
|
|
|
+ * - 技能取当前账号已安装的技能(listSkills),repo 来源的只读技能不参与勾选;
|
|
|
* - 技能编辑器:修改直接写当前用户的技能目录(新建技能在「插件与技能」页进行),与研判方案是否保存无关;
|
|
|
* - 校验与保存走 a-form rules + agentApi.createExpert/updateExpert。
|
|
|
*
|
|
|
- * 新增的纯前端能力(不影响保存语义):技能名称过滤、分组已选摘要条与「清空」。
|
|
|
+ * 新增的纯前端能力(不影响保存语义):技能名称过滤、已选摘要条与「清空」。
|
|
|
*/
|
|
|
import { computed, reactive, ref, watch } from 'vue';
|
|
|
import { Button, Form, FormItem, Input, Spin, Textarea } from 'ant-design-vue';
|
|
|
@@ -250,7 +212,7 @@
|
|
|
import * as capabilityApi from '../api/capabilityApi';
|
|
|
import * as toolPackApi from '../api/toolPackApi';
|
|
|
import type { AiExpert, SaveExpertParams, SaveSkillParams, Skill, ToolPack } from '../api/types';
|
|
|
- import { TOOL_GROUP_LABELS, TOOL_GROUP_KEYS, toolGroupLabel } from '../utils/constants';
|
|
|
+ import { toolGroupLabel } from '../utils/constants';
|
|
|
|
|
|
const AButton = Button;
|
|
|
const AForm = Form;
|
|
|
@@ -282,7 +244,6 @@
|
|
|
maxHeight: 'calc(100vh - 220px)',
|
|
|
overflowY: 'auto' as const,
|
|
|
}));
|
|
|
- const toolsLoading = ref(false);
|
|
|
const skillsLoading = ref(false);
|
|
|
const formRef = ref();
|
|
|
|
|
|
@@ -297,21 +258,17 @@
|
|
|
sysPrompt: [{ required: true, message: '请输入系统提示词', trigger: 'blur' }],
|
|
|
};
|
|
|
|
|
|
- interface ToolGroupOption {
|
|
|
- group: string;
|
|
|
- label: string;
|
|
|
- description: string;
|
|
|
- count: number;
|
|
|
- }
|
|
|
-
|
|
|
- const toolGroups = ref<ToolGroupOption[]>([]);
|
|
|
const skills = ref<Skill[]>([]);
|
|
|
const packs = ref<ToolPack[]>([]);
|
|
|
const packsLoading = ref(false);
|
|
|
/** 勾选态(key → checked);未出现的 key 视为不勾 */
|
|
|
- const checkedGroups = reactive<Record<string, boolean>>({});
|
|
|
const checkedSkills = reactive<Record<string, boolean>>({});
|
|
|
const checkedPacks = reactive<Record<string, boolean>>({});
|
|
|
+ /**
|
|
|
+ * 打开时旧方案的工具组编码(tools_allow_json):按组名映射到同名内置工具包后清空;
|
|
|
+ * 映射不到的留在这里,保存时回落写回 toolGroups,原有限制不丢。
|
|
|
+ */
|
|
|
+ const legacyGroups = ref<string[]>([]);
|
|
|
|
|
|
/** 技能名称过滤(纯前端展示,不参与保存语义) */
|
|
|
const skillKeyword = ref('');
|
|
|
@@ -328,49 +285,43 @@
|
|
|
});
|
|
|
|
|
|
function resetChecks() {
|
|
|
- Object.keys(checkedGroups).forEach((k) => delete checkedGroups[k]);
|
|
|
Object.keys(checkedPacks).forEach((k) => delete checkedPacks[k]);
|
|
|
Object.keys(checkedSkills).forEach((k) => delete checkedSkills[k]);
|
|
|
// 不勾 = 不限制(与后端 null 口径一致)
|
|
|
- const groups = props.record?.toolGroups || [];
|
|
|
- groups.forEach((g) => (checkedGroups[g] = true));
|
|
|
const names = props.record?.skillNames || [];
|
|
|
names.forEach((n) => (checkedSkills[n] = true));
|
|
|
const packIds = props.record?.toolPackIds || [];
|
|
|
packIds.forEach((id) => (checkedPacks[id] = true));
|
|
|
+ // 旧工具组:先挂起,工具包清单加载回来后映射成同名内置包勾选
|
|
|
+ legacyGroups.value = [...(props.record?.toolGroups || [])];
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 旧工具组 → 同名内置工具包;映射到的从 legacyGroups 摘掉,映射不到的留着回落写回 */
|
|
|
+ function mapLegacyGroups() {
|
|
|
+ if (!legacyGroups.value.length) return;
|
|
|
+ const rest: string[] = [];
|
|
|
+ for (const g of legacyGroups.value) {
|
|
|
+ const hit = packs.value.find((p) => p.name === toolGroupLabel(g));
|
|
|
+ if (hit) checkedPacks[hit.id] = true;
|
|
|
+ else rest.push(g);
|
|
|
+ }
|
|
|
+ legacyGroups.value = rest;
|
|
|
}
|
|
|
|
|
|
- /** 从工具清单聚合业务组(保持 TOOL_GROUP_KEYS 顺序,缺失的组用静态标签兜底) */
|
|
|
async function loadOptions() {
|
|
|
- toolsLoading.value = true;
|
|
|
skillsLoading.value = true;
|
|
|
packsLoading.value = true;
|
|
|
try {
|
|
|
- const [tools, skillList, packList] = await Promise.all([
|
|
|
- capabilityApi.listTools().catch(() => [] as any[]),
|
|
|
+ const [skillList, packList] = await Promise.all([
|
|
|
capabilityApi.listSkills().catch(() => [] as Skill[]),
|
|
|
toolPackApi.listToolPacks().catch(() => [] as ToolPack[]),
|
|
|
]);
|
|
|
- const countBy = new Map<string, { count: number; desc: string }>();
|
|
|
- for (const t of tools || []) {
|
|
|
- const key = String(t.groupCode || '');
|
|
|
- if (!TOOL_GROUP_KEYS.includes(key)) continue;
|
|
|
- const cur = countBy.get(key) || { count: 0, desc: String(t.groupDescription || '') };
|
|
|
- cur.count += 1;
|
|
|
- if (!cur.desc && t.groupDescription) cur.desc = String(t.groupDescription);
|
|
|
- countBy.set(key, cur);
|
|
|
- }
|
|
|
- toolGroups.value = TOOL_GROUP_KEYS.map((group) => ({
|
|
|
- group,
|
|
|
- label: toolGroupLabel(group),
|
|
|
- description: countBy.get(group)?.desc || TOOL_GROUP_LABELS[group] || '',
|
|
|
- count: countBy.get(group)?.count || 0,
|
|
|
- }));
|
|
|
// 白名单只对用户技能目录生效:repo 来源是只读技能,不参与勾选
|
|
|
skills.value = (skillList || []).filter((s) => s.source === 'user');
|
|
|
packs.value = packList || [];
|
|
|
+ // 工具包清单就位后,把旧工具组映射成同名内置包勾选
|
|
|
+ mapLegacyGroups();
|
|
|
} finally {
|
|
|
- toolsLoading.value = false;
|
|
|
skillsLoading.value = false;
|
|
|
packsLoading.value = false;
|
|
|
}
|
|
|
@@ -392,7 +343,6 @@
|
|
|
},
|
|
|
);
|
|
|
|
|
|
- const pickedGroups = computed(() => TOOL_GROUP_KEYS.filter((g) => checkedGroups[g]));
|
|
|
const pickedPackIds = computed(() => packs.value.map((p) => p.id).filter((id) => checkedPacks[id]));
|
|
|
|
|
|
/** 摘要条 chip 显示工具包名(id → 名) */
|
|
|
@@ -406,10 +356,6 @@
|
|
|
const pickedSkills = computed(() => skills.value.map((s) => s.name).filter((n) => checkedSkills[n]));
|
|
|
|
|
|
/** 摘要条「清空」:取消该区全部勾选(回到不限制),与逐个取消完全等价 */
|
|
|
- function clearGroups() {
|
|
|
- pickedGroups.value.forEach((g) => delete checkedGroups[g]);
|
|
|
- }
|
|
|
-
|
|
|
function clearSkills() {
|
|
|
pickedSkills.value.forEach((n) => delete checkedSkills[n]);
|
|
|
}
|
|
|
@@ -504,7 +450,7 @@
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- /** 只重载技能清单(不动工具组);autoCheckName:重载后补勾该技能 */
|
|
|
+ /** 只重载技能清单(不动工具包勾选);autoCheckName:重载后补勾该技能 */
|
|
|
async function reloadSkills(autoCheckName?: string) {
|
|
|
skillsLoading.value = true;
|
|
|
try {
|
|
|
@@ -530,7 +476,8 @@
|
|
|
name: form.name.trim(),
|
|
|
description: form.description?.trim() || '',
|
|
|
sysPrompt: form.sysPrompt.trim(),
|
|
|
- toolGroups: pickedGroups.value,
|
|
|
+ // 映射成功的旧工具组已并入 toolPackIds;这里只剩映射不到的,回落写回不丢限制
|
|
|
+ toolGroups: legacyGroups.value,
|
|
|
skillNames: pickedSkills.value,
|
|
|
toolPackIds: pickedPackIds.value,
|
|
|
};
|