mes-mcp 0.2.3 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +5 -0
- package/README.md +2 -0
- package/dist/config.js +10 -0
- package/dist/discovery/action-search.js +0 -0
- package/dist/discovery/action-search.service.js +35 -0
- package/dist/discovery/embedding-provider.js +55 -0
- package/dist/index.js +3 -1
- package/dist/prompts/register-prompts.js +4 -2
- package/dist/resources/register-resources.js +59 -0
- package/dist/tools/register-tools.js +65 -12
- package/package.json +1 -1
package/.env.example
CHANGED
|
@@ -3,3 +3,8 @@ MES_API_PREFIX=/api
|
|
|
3
3
|
MES_AGENT_TOKEN=
|
|
4
4
|
MES_TIMEOUT_MS=30000
|
|
5
5
|
MES_REGISTER_DYNAMIC_TOOLS=false
|
|
6
|
+
# mes_action.list 动作检索模式:
|
|
7
|
+
# lexical(默认,推荐)= CJK 词元 BM25 + 双语领域词扩展,离线、零额外依赖、确定性;
|
|
8
|
+
# hybrid / semantic = 额外融合本地语义向量,需手动安装可选依赖 @huggingface/transformers
|
|
9
|
+
# (含原生 onnxruntime,部分受限环境装不上,届时自动退化为 lexical)。
|
|
10
|
+
MES_ACTION_SEARCH=lexical
|
package/README.md
CHANGED
|
@@ -75,6 +75,8 @@ npm run dev
|
|
|
75
75
|
写入类动作通过 `confirm_required` 或 `commit` 执行时需要幂等键。调用方没有显式传入时,`mes-mcp` 会为当前工具调用生成一条集成幂等键;需要跨重试严格复用同一幂等键的批处理场景,仍建议调用方显式传入 `idempotencyKey`。
|
|
76
76
|
|
|
77
77
|
- `mes_api.read`:按当前 MES 账号权限读取普通业务接口,例如 `/sale-order/list`、`/material/detail`。后端只放行只读类接口,写入动作必须使用动态业务动作;读取调用会在 MES AI 调用日志里记录路径、请求摘要和结果摘要。
|
|
78
|
+
- `mes://agent/context`:MES Agent 通用操作边界、工具顺序和安全规则。
|
|
79
|
+
- `mes://flows/sale-delivery`:销售出库上下文资源,只描述读取顺序和阻断处理;可出库状态、库存和质量判断仍以后端接口返回为准。
|
|
78
80
|
|
|
79
81
|
写入类工具支持 `executionMode`:
|
|
80
82
|
|
package/dist/config.js
CHANGED
|
@@ -24,6 +24,15 @@ function optionalBooleanEnv(name, fallback) {
|
|
|
24
24
|
return false;
|
|
25
25
|
throw new Error(`${name} must be a boolean`);
|
|
26
26
|
}
|
|
27
|
+
function actionSearchModeEnv(name, fallback) {
|
|
28
|
+
const value = process.env[name]?.trim().toLowerCase();
|
|
29
|
+
if (!value)
|
|
30
|
+
return fallback;
|
|
31
|
+
if (value === "lexical" || value === "hybrid" || value === "semantic") {
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
throw new Error(`${name} must be one of lexical | hybrid | semantic`);
|
|
35
|
+
}
|
|
27
36
|
function decodeBase64UrlJson(segment) {
|
|
28
37
|
try {
|
|
29
38
|
const normalized = segment.replace(/-/g, "+").replace(/_/g, "/");
|
|
@@ -86,6 +95,7 @@ export function loadConfig() {
|
|
|
86
95
|
mesAgentToken,
|
|
87
96
|
timeoutMs,
|
|
88
97
|
registerDynamicTools: optionalBooleanEnv("MES_REGISTER_DYNAMIC_TOOLS", false),
|
|
98
|
+
actionSearchMode: actionSearchModeEnv("MES_ACTION_SEARCH", "lexical"),
|
|
89
99
|
tokenInfo,
|
|
90
100
|
expectedUsername,
|
|
91
101
|
expectedAgentClientId,
|
|
Binary file
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { ActionSearchIndex, } from "./action-search.js";
|
|
2
|
+
import { createEmbeddingProvider } from "./embedding-provider.js";
|
|
3
|
+
/**
|
|
4
|
+
* 包装「全量动作缓存 + 相关性索引」。索引在首次检索时懒构建并缓存。
|
|
5
|
+
*
|
|
6
|
+
* `client.listBusinessActions()` 已按当前 AI 凭证 + 账号 RBAC 过滤过(后端动作目录过滤逻辑),
|
|
7
|
+
* 所以在这份缓存上检索天然遵守权限,不会暴露当前账号无权的动作。
|
|
8
|
+
*/
|
|
9
|
+
export class ActionSearchService {
|
|
10
|
+
client;
|
|
11
|
+
mode;
|
|
12
|
+
indexPromise;
|
|
13
|
+
constructor(client, mode) {
|
|
14
|
+
this.client = client;
|
|
15
|
+
this.mode = mode;
|
|
16
|
+
}
|
|
17
|
+
async buildIndex() {
|
|
18
|
+
const [actions, provider] = await Promise.all([
|
|
19
|
+
this.client.listBusinessActions(),
|
|
20
|
+
createEmbeddingProvider(this.mode),
|
|
21
|
+
]);
|
|
22
|
+
return new ActionSearchIndex(actions, provider);
|
|
23
|
+
}
|
|
24
|
+
async search(query, options) {
|
|
25
|
+
if (!this.indexPromise) {
|
|
26
|
+
this.indexPromise = this.buildIndex().catch((error) => {
|
|
27
|
+
this.indexPromise = undefined;
|
|
28
|
+
throw error;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
const index = await this.indexPromise;
|
|
32
|
+
const results = await index.search(query, options);
|
|
33
|
+
return { total: index.size, results };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 本地语义向量 provider。
|
|
3
|
+
*
|
|
4
|
+
* 使用 `@huggingface/transformers` 的 all-MiniLM-L6-v2(384 维),完全本地运行、离线、
|
|
5
|
+
* 业务数据不出机器。该依赖在 package.json 中声明为 optionalDependencies:
|
|
6
|
+
* - 装上了 + 模式为 hybrid/semantic → 启用语义融合;
|
|
7
|
+
* - 没装(受限运行时)或模式为 lexical → 返回 null,检索退化为纯 CJK 词法 BM25(仍然可用)。
|
|
8
|
+
*
|
|
9
|
+
* 这里的「装不上就退化为词法」是检索排序能力的降级(并在 stderr 明示),
|
|
10
|
+
* 不是用兜底数据掩盖业务错误——词法检索本身是完整可用的检索实现。
|
|
11
|
+
*/
|
|
12
|
+
// 多语言模型,覆盖中文业务意图(MES 动作标题/描述以中文为主)。
|
|
13
|
+
const MODEL_ID = "Xenova/paraphrase-multilingual-MiniLM-L12-v2";
|
|
14
|
+
// 用变量 specifier 引入可选依赖,避免 tsc 在未安装时报「找不到模块」。
|
|
15
|
+
const TRANSFORMERS_MODULE = "@huggingface/transformers";
|
|
16
|
+
async function importTransformers() {
|
|
17
|
+
return (await import(TRANSFORMERS_MODULE));
|
|
18
|
+
}
|
|
19
|
+
class LocalEmbeddingProvider {
|
|
20
|
+
pipelinePromise = null;
|
|
21
|
+
async getPipeline() {
|
|
22
|
+
if (!this.pipelinePromise) {
|
|
23
|
+
this.pipelinePromise = (async () => {
|
|
24
|
+
const mod = await importTransformers();
|
|
25
|
+
return mod.pipeline("feature-extraction", MODEL_ID);
|
|
26
|
+
})();
|
|
27
|
+
}
|
|
28
|
+
return this.pipelinePromise;
|
|
29
|
+
}
|
|
30
|
+
async embed(texts) {
|
|
31
|
+
if (texts.length === 0)
|
|
32
|
+
return [];
|
|
33
|
+
const extractor = await this.getPipeline();
|
|
34
|
+
const output = await extractor(texts, { pooling: "mean", normalize: true });
|
|
35
|
+
return output.tolist();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* 按检索模式创建 provider:
|
|
40
|
+
* - lexical → 始终返回 null(纯词法)。
|
|
41
|
+
* - hybrid/semantic → 探测可选向量库是否可用;可用则返回本地 provider,否则 null + 警告。
|
|
42
|
+
*/
|
|
43
|
+
export async function createEmbeddingProvider(mode) {
|
|
44
|
+
if (mode === "lexical")
|
|
45
|
+
return null;
|
|
46
|
+
try {
|
|
47
|
+
// 探测可选依赖是否安装;未安装时 import 抛错,退化为词法。
|
|
48
|
+
await importTransformers();
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
console.error(`[mes-mcp] MES_ACTION_SEARCH=${mode} 需要可选依赖 @huggingface/transformers,未检测到,已退化为词法检索(lexical)。如需语义检索,请在 mes-mcp 安装该依赖后重启。`);
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
return new LocalEmbeddingProvider();
|
|
55
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -27,6 +27,7 @@ function logStartupDiagnostics(config) {
|
|
|
27
27
|
`[mes-mcp] endpoint=${config.mesBaseUrl}${config.mesApiPrefix}`,
|
|
28
28
|
`[mes-mcp] token username=${config.tokenInfo.username ?? "<unknown>"} apiKeyId=${maskId(config.tokenInfo.apiKeyId)} agentClientId=${config.tokenInfo.agentClientId ?? "<unknown>"} expiresAt=${formatUnixSeconds(config.tokenInfo.expiresAt)}`,
|
|
29
29
|
`[mes-mcp] dynamicTools=${config.registerDynamicTools ? "enabled" : "disabled"} (${config.registerDynamicTools ? "mes.<actionCode> tools will be registered" : "use mes_action.list/detail/execute; set MES_REGISTER_DYNAMIC_TOOLS=true only when the client needs expanded tools"})`,
|
|
30
|
+
`[mes-mcp] actionSearch=${config.actionSearchMode} (mes_action.list 按相关性检索动作目录;hybrid/semantic 需安装可选依赖 @huggingface/transformers,否则自动退化为 lexical)`,
|
|
30
31
|
];
|
|
31
32
|
if (config.expectedUsername) {
|
|
32
33
|
lines.push(`[mes-mcp] expected username=${config.expectedUsername}`);
|
|
@@ -45,10 +46,11 @@ async function main() {
|
|
|
45
46
|
const client = new MesAgentApiClient(config);
|
|
46
47
|
const server = new McpServer({
|
|
47
48
|
name: "mes-mcp",
|
|
48
|
-
version: "0.
|
|
49
|
+
version: "0.3.0",
|
|
49
50
|
});
|
|
50
51
|
await registerAllAgentTools(server, client, {
|
|
51
52
|
registerDynamicTools: config.registerDynamicTools,
|
|
53
|
+
actionSearchMode: config.actionSearchMode,
|
|
52
54
|
});
|
|
53
55
|
registerMesResources(server, client);
|
|
54
56
|
registerMesPrompts(server);
|
|
@@ -15,10 +15,12 @@ export function registerMesPrompts(server) {
|
|
|
15
15
|
text: [
|
|
16
16
|
"你是 MES 业务助手,必须按当前账号权限和工具返回结果执行。",
|
|
17
17
|
"不要猜测客户、物料、BOM、工艺路线、仓库等关键主数据;缺失时先查询或要求用户确认。",
|
|
18
|
-
"
|
|
18
|
+
"动作目录有数百个动作。发现动作时用 mes_action.list,把你当前要做的事用一句自然语言意图传给 keyword(例如「销售单转生产计划」「采购入库」「报工」),它返回按相关性排序的 top-K 动作;再用 mes_action.detail 查看字段,最后用 mes_action.execute 执行。不要不带意图地翻页或猜路径,也不要依赖工具列表里必须存在 mes.<动作编码>。",
|
|
19
19
|
"mes_action.execute 返回的是执行信封,真实业务返回在 data 字段;confirm_required 产生的 invocationId 必须由同一公司/租户下有权限的账号审批。",
|
|
20
20
|
"mes_api.read 只用于普通业务只读接口,pageSize 最大 100;如果返回 _mcpWarnings 或提示需要动态动作,请按提示调整。",
|
|
21
|
-
"
|
|
21
|
+
"销售到出库的流程也走动态动作:先查客户和物料,再创建销售单、处理审批、确认销售单、转生产计划、排产或生成工单、报工、质检、生产入库/库存确认、发货出库。",
|
|
22
|
+
"涉及销售出库时先读取 `mes://flows/sale-delivery`。不要从工单状态或销售订单状态自行推断可出库;必须先查询后端销售订单行、剩余可出库数量、目标仓库存证据和相关入库事实。有 blockers 时停止写入并向用户说明原因。",
|
|
23
|
+
"库存盘点差异优先使用 stock.stock_count.create/update/start/submit/approve 盘点单流程;stock.batch_adjust 只适合非批次物料的简化批量差异调整,批次追踪物料请使用 stock.adjust 并指定 batchNo。",
|
|
22
24
|
"写入类动作默认先使用 confirm_required 或 preview;preview 不写入,confirm_required 生成待确认记录,commit 只有凭证允许时才真实提交。",
|
|
23
25
|
context ? `当前上下文:${context}` : "",
|
|
24
26
|
]
|
|
@@ -57,6 +57,20 @@ export function registerMesResources(server, client) {
|
|
|
57
57
|
"- MES 分页 `pageSize` 最大为 100;MCP 会裁剪超限值,并通过 `_mcpWarnings` 返回提示。",
|
|
58
58
|
"- 如果只读路径被提示需要动态业务动作,请改用 `mes_action.list/detail/execute` 查找并调用对应 actionCode。",
|
|
59
59
|
"",
|
|
60
|
+
"## 库存盘点和调整",
|
|
61
|
+
"",
|
|
62
|
+
"- 正式库存盘点差异优先走 `stock.stock_count.create` -> `stock.stock_count.update` -> `stock.stock_count.start` -> `stock.stock_count.submit` -> `stock.stock_count.approve`。",
|
|
63
|
+
"- `stock.out` 只用于无来源直接出库,类型限定为 `out_scrap` 或 `out_other`;销售发货不要绕过销售出库单。",
|
|
64
|
+
"- `stock.batch_adjust` 只适合非批次物料的简化批量差异调整;批次追踪物料请使用 `stock.adjust` 并传 `batchNo`。",
|
|
65
|
+
"",
|
|
66
|
+
"## 销售出库防误操作",
|
|
67
|
+
"",
|
|
68
|
+
"- 销售出库必须以后端返回的销售订单、订单行、库存和动作详情为准,不能从工单状态自行推断可出库。",
|
|
69
|
+
"- 用户说“已完工 / finished”时,先读取 `work_order.list` 的动作详情或接口 schema 确认合法状态枚举;不要把自然语言状态原样传给接口。",
|
|
70
|
+
"- 工单完工只代表生产执行状态,不代表成品已经确认入库;出库前必须查询后端库存或后端候选/预检接口。",
|
|
71
|
+
"- 没有明确的销售订单行、客户、出库仓库、剩余可出库数量和后端库存证据时,不要创建或确认销售出库单。",
|
|
72
|
+
"- 如果库存不足、缺少生产入库单、缺少客户或状态不允许,停止写入动作并向用户说明阻断原因和下一步查询建议。",
|
|
73
|
+
"",
|
|
60
74
|
"## 防误连",
|
|
61
75
|
"",
|
|
62
76
|
"- MCP 启动时会在 stderr 打印 MES 地址、Token 用户名、apiKeyId、agentClientId 和动态工具状态。",
|
|
@@ -71,4 +85,49 @@ export function registerMesResources(server, client) {
|
|
|
71
85
|
},
|
|
72
86
|
],
|
|
73
87
|
}));
|
|
88
|
+
server.registerResource("mes_sale_delivery_flow", "mes://flows/sale-delivery", {
|
|
89
|
+
title: "MES Sales Delivery Flow",
|
|
90
|
+
description: "销售出库 AI 执行上下文:说明读取顺序、后端事实来源和阻断处理,避免 AI 从工单或销售单状态自行推断可出库。",
|
|
91
|
+
mimeType: "text/markdown",
|
|
92
|
+
}, async (uri) => ({
|
|
93
|
+
contents: [
|
|
94
|
+
{
|
|
95
|
+
uri: uri.href,
|
|
96
|
+
mimeType: "text/markdown",
|
|
97
|
+
text: [
|
|
98
|
+
"# MES Sales Delivery Flow",
|
|
99
|
+
"",
|
|
100
|
+
"本资源只描述 AI 应如何读取后端事实,不承载 MES 业务规则。状态、权限、库存、质量、可执行性和审计均以后端接口返回为准。",
|
|
101
|
+
"",
|
|
102
|
+
"## 核心原则",
|
|
103
|
+
"",
|
|
104
|
+
"- 不要把“工单已完工”当成“可销售出库”。工单、生产入库单、库存和销售出库单是不同业务事实。",
|
|
105
|
+
"- 不要在 MCP 端维护可出库状态白名单、库存扣减规则或质量规则。",
|
|
106
|
+
"- 写入前必须用后端只读接口或动作详情取得当前事实;事实不完整时停止写入并说明缺口。",
|
|
107
|
+
"",
|
|
108
|
+
"## 推荐读取顺序",
|
|
109
|
+
"",
|
|
110
|
+
"1. 用 `mes_action.list` 搜索“销售出库 / 创建出库单 / 确认出库单”,再用 `mes_action.detail` 读取 `sale_delivery.create` 和 `sale_delivery.confirm` 的最新 schema。",
|
|
111
|
+
"2. 用 `mes_api.read` 查询 `/sale-order/list` 或 `/sale-order/detail`,确认销售订单、客户、订单行和后端返回的剩余可出库信息。",
|
|
112
|
+
"3. 对每个订单行,用 `mes_api.read` 查询 `/stock/list`,按物料和候选出库仓库读取后端库存行;没有库存证据时不要继续确认出库。",
|
|
113
|
+
"4. 如果用户从工单切入,先查询 `/work-order/list` 或 `/work-order/detail` 只确认工单事实,再查询 `/production-receipt/list`、`/production-receipt/output-buffer/list` 和 `/stock/list` 确认是否已经形成可用库存。",
|
|
114
|
+
"5. 只有后端事实齐全且没有阻断时,才用 `mes_action.execute` 执行 `sale_delivery.create`;确认出库使用 `sale_delivery.confirm`,最终库存扣减仍由后端复验。",
|
|
115
|
+
"",
|
|
116
|
+
"## 阻断时的回答",
|
|
117
|
+
"",
|
|
118
|
+
"- 库存不足:不要创建或确认出库单;说明目标仓库库存不足,并建议查询生产入库单、生产输出暂存、调拨或采购入库。",
|
|
119
|
+
"- 没有销售订单行:不要用工单直接拼出库单;要求用户指定销售订单或先查询待出库销售订单。",
|
|
120
|
+
"- 状态或字段不合法:引用后端错误或 schema 合法枚举,改用后端合法值重新查询。",
|
|
121
|
+
"- 缺客户、仓库、批次、库位等关键事实:先查询或要求用户确认,不要伪造默认值。",
|
|
122
|
+
"",
|
|
123
|
+
"## 写入模式",
|
|
124
|
+
"",
|
|
125
|
+
"- 优先使用 `preview` 或 `confirm_required`。",
|
|
126
|
+
"- `preview` 只校验工具调用形态,不代表业务已成功写入。",
|
|
127
|
+
"- `confirm_required` 生成待确认调用,最终是否执行仍由同公司有权限账号确认。",
|
|
128
|
+
"- `commit` 只有当前凭证允许且用户明确要求时才使用。",
|
|
129
|
+
].join("\n"),
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
}));
|
|
74
133
|
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { ActionSearchService } from "../discovery/action-search.service.js";
|
|
3
4
|
const MAX_MES_PAGE_SIZE = 100;
|
|
5
|
+
const DEFAULT_ACTION_LIST_TOPK = 20;
|
|
6
|
+
const MAX_ACTION_LIST_TOPK = 50;
|
|
4
7
|
const ExecutionModeSchema = z
|
|
5
8
|
.enum(["preview", "confirm_required", "commit"])
|
|
6
9
|
.optional();
|
|
@@ -174,15 +177,39 @@ function actionDescription(action) {
|
|
|
174
177
|
.filter(Boolean)
|
|
175
178
|
.join("\n");
|
|
176
179
|
}
|
|
177
|
-
function
|
|
180
|
+
function listActionView(action, relevance) {
|
|
181
|
+
return {
|
|
182
|
+
actionCode: action.actionCode,
|
|
183
|
+
title: action.title,
|
|
184
|
+
description: action.description,
|
|
185
|
+
module: action.module,
|
|
186
|
+
resource: action.resource,
|
|
187
|
+
operation: action.operation,
|
|
188
|
+
path: action.path,
|
|
189
|
+
method: action.method,
|
|
190
|
+
isWrite: action.isWrite,
|
|
191
|
+
permissions: action.permissions,
|
|
192
|
+
riskLevel: action.riskLevel,
|
|
193
|
+
relevance: Number(relevance.toFixed(4)),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function registerGenericActionTools(server, client, searchService) {
|
|
178
197
|
server.registerTool("mes_action.list", {
|
|
179
|
-
title: "
|
|
180
|
-
description: "
|
|
198
|
+
title: "Search MES business actions",
|
|
199
|
+
description: "按相关性检索当前 AI 凭证和账号权限允许调用的 MES 页面业务动作。动作目录有数百个,请把要做的事用一句自然语言意图放进 keyword(例如「销售单转生产计划」「采购入库」「报工」),返回的是按相关性排序的 top-K 动作(不是分页字母序)。可选 module/resource/operation/write 做精确过滤。拿到 actionCode 后用 mes_action.detail 看字段、mes_action.execute 执行。",
|
|
181
200
|
inputSchema: z
|
|
182
201
|
.object({
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
202
|
+
keyword: z
|
|
203
|
+
.string()
|
|
204
|
+
.optional()
|
|
205
|
+
.describe("要做的事的自然语言意图,用于相关性检索"),
|
|
206
|
+
topK: z
|
|
207
|
+
.number()
|
|
208
|
+
.int()
|
|
209
|
+
.positive()
|
|
210
|
+
.max(MAX_ACTION_LIST_TOPK)
|
|
211
|
+
.optional()
|
|
212
|
+
.describe("返回的最相关动作数量,默认 20,最大 50"),
|
|
186
213
|
module: z.string().optional(),
|
|
187
214
|
resource: z.string().optional(),
|
|
188
215
|
operation: z.string().optional(),
|
|
@@ -198,9 +225,8 @@ function registerGenericActionTools(server, client) {
|
|
|
198
225
|
return runTool({ toolName: "mes_action.list" }, async () => {
|
|
199
226
|
const parsed = z
|
|
200
227
|
.object({
|
|
201
|
-
page: z.number().int().positive().optional(),
|
|
202
|
-
pageSize: z.number().int().positive().optional(),
|
|
203
228
|
keyword: z.string().optional(),
|
|
229
|
+
topK: z.number().int().positive().optional(),
|
|
204
230
|
module: z.string().optional(),
|
|
205
231
|
resource: z.string().optional(),
|
|
206
232
|
operation: z.string().optional(),
|
|
@@ -209,9 +235,35 @@ function registerGenericActionTools(server, client) {
|
|
|
209
235
|
.passthrough()
|
|
210
236
|
.parse(args ?? {});
|
|
211
237
|
const warnings = [];
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
238
|
+
let topK = parsed.topK ?? DEFAULT_ACTION_LIST_TOPK;
|
|
239
|
+
if (topK > MAX_ACTION_LIST_TOPK) {
|
|
240
|
+
warnings.push({
|
|
241
|
+
code: "topK_clamped",
|
|
242
|
+
message: `topK 最大为 ${MAX_ACTION_LIST_TOPK},已从 ${topK} 调整为 ${MAX_ACTION_LIST_TOPK}。`,
|
|
243
|
+
});
|
|
244
|
+
topK = MAX_ACTION_LIST_TOPK;
|
|
245
|
+
}
|
|
246
|
+
const keyword = parsed.keyword?.trim() ?? "";
|
|
247
|
+
const { total, results } = await searchService.search(keyword, {
|
|
248
|
+
module: parsed.module,
|
|
249
|
+
resource: parsed.resource,
|
|
250
|
+
operation: parsed.operation,
|
|
251
|
+
write: parsed.write,
|
|
252
|
+
topK,
|
|
253
|
+
});
|
|
254
|
+
if (keyword && results.length === 0) {
|
|
255
|
+
warnings.push({
|
|
256
|
+
code: "no_match",
|
|
257
|
+
message: "没有命中相关动作。换一个更贴近业务的意图词(如把「转计划」换成「销售单转生产计划」),或用 module/operation 过滤。",
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
return formatToolResult({
|
|
261
|
+
query: keyword || null,
|
|
262
|
+
ranked: Boolean(keyword),
|
|
263
|
+
total,
|
|
264
|
+
returned: results.length,
|
|
265
|
+
list: results.map((r) => listActionView(r.action, r.score)),
|
|
266
|
+
}, warnings);
|
|
215
267
|
});
|
|
216
268
|
});
|
|
217
269
|
server.registerTool("mes_action.detail", {
|
|
@@ -312,8 +364,9 @@ async function registerDynamicBusinessActionTools(server, client) {
|
|
|
312
364
|
}
|
|
313
365
|
}
|
|
314
366
|
export async function registerAllAgentTools(server, client, options = {}) {
|
|
367
|
+
const searchService = new ActionSearchService(client, options.actionSearchMode ?? "lexical");
|
|
315
368
|
registerAgentTools(server, client);
|
|
316
|
-
registerGenericActionTools(server, client);
|
|
369
|
+
registerGenericActionTools(server, client, searchService);
|
|
317
370
|
if (!options.registerDynamicTools)
|
|
318
371
|
return;
|
|
319
372
|
try {
|