@stalfh233/omc-cli 0.6.7 → 0.6.11
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/README.md +2 -1
- package/dist/approval-flow.js +5 -3
- package/dist/bizservice-config.js +26 -10
- package/dist/call.js +47 -0
- package/dist/capability-overview.js +47 -24
- package/dist/cli-contract.js +73 -26
- package/dist/cli-output.js +41 -2
- package/dist/cli-stdout.js +74 -0
- package/dist/cli.js +135 -52
- package/dist/command-transport.js +8 -1
- package/dist/config-store.js +17 -2
- package/dist/discovery.js +62 -3
- package/dist/field-families.js +35 -6
- package/dist/goal-routes.js +118 -11
- package/dist/human-summary.js +11 -0
- package/dist/lanes/app-contract.js +12 -5
- package/dist/lanes/app.js +221 -134
- package/dist/lanes/apply-changes.js +2 -1
- package/dist/lanes/b2-registration-contract.js +3 -3
- package/dist/lanes/b2-registration.js +890 -693
- package/dist/lanes/businessrule.js +75 -51
- package/dist/lanes/custom-page.js +166 -115
- package/dist/lanes/datarule-contract.js +2 -2
- package/dist/lanes/datarule.js +238 -154
- package/dist/lanes/dictionary.js +253 -110
- package/dist/lanes/environment.js +57 -9
- package/dist/lanes/field.js +160 -107
- package/dist/lanes/folder-contract.js +7 -3
- package/dist/lanes/folder.js +249 -118
- package/dist/lanes/form-contract.js +1 -1
- package/dist/lanes/form.js +188 -64
- package/dist/lanes/list-view.js +405 -369
- package/dist/lanes/meta.js +31 -6
- package/dist/lanes/model-contract.js +1 -1
- package/dist/lanes/model.js +610 -512
- package/dist/lanes/online-js-contract.js +1 -1
- package/dist/lanes/online-js.js +160 -146
- package/dist/lanes/organization.js +905 -640
- package/dist/lanes/programming.js +274 -136
- package/dist/lanes/report-contract.js +2 -2
- package/dist/lanes/report.js +354 -198
- package/dist/lanes/rule-lifecycle.js +294 -140
- package/dist/lanes/runtime-data-contract.js +4 -4
- package/dist/lanes/runtime-data.js +869 -745
- package/dist/lanes/workflow-contract.js +3 -3
- package/dist/lanes/workflow.js +732 -605
- package/dist/model-scaffold.js +26 -5
- package/dist/path-projection.js +196 -14
- package/dist/plan.js +25 -0
- package/dist/programming-workflow.js +55 -3
- package/dist/read-shapes.js +146 -0
- package/dist/read.js +40 -9
- package/dist/recipes/businessrule.js +74 -24
- package/dist/recipes/dictionary.js +7 -4
- package/dist/recipes/form.js +46 -0
- package/dist/recipes/runtime-data.js +3 -2
- package/dist/refusal-guidance.js +29 -5
- package/dist/rule-support.js +14 -1
- package/dist/rule-when.js +2 -2
- package/dist/tool-types.js +9 -3
- package/dist/version-family-gaps.js +101 -0
- package/dist/write.js +2 -1
- package/package.json +6 -4
- package/skills/omc-glossary.md +13 -0
- package/skills/omc-refusals.md +13 -5
- package/skills/omc-tool-selection.md +2 -1
- package/skills/omc-write-hazards.md +5 -2
package/dist/lanes/dictionary.js
CHANGED
|
@@ -86,26 +86,89 @@ function normalizeRecord(record, dictionaryId, parentId) {
|
|
|
86
86
|
sortKey: Number.isInteger(record.sortKey) ? record.sortKey : 0,
|
|
87
87
|
};
|
|
88
88
|
}
|
|
89
|
-
|
|
89
|
+
/**
|
|
90
|
+
* r28a F1:默认读面只留身份/决策字段。`name_i18n` 在中文环境里与 `name`
|
|
91
|
+
* 冗余(二次序列化 JSON 串),每条多 ~60B;真翻译用 `full:true` / artifact。
|
|
92
|
+
*/
|
|
93
|
+
function dictionaryView(value, options = {}) {
|
|
90
94
|
const row = (value && typeof value === "object" ? value : {});
|
|
91
|
-
|
|
95
|
+
const view = {
|
|
92
96
|
id: row.id,
|
|
93
97
|
code: row.code,
|
|
94
98
|
name: row.name,
|
|
95
|
-
name_i18n: row.name_i18n,
|
|
96
99
|
dictionaryType: row.dictionaryType,
|
|
97
100
|
status: row.status,
|
|
98
101
|
sortKey: row.sortKey,
|
|
99
102
|
classificationId: row.classificationId,
|
|
100
103
|
classificationName: row.classificationName,
|
|
101
104
|
};
|
|
105
|
+
if (options.full)
|
|
106
|
+
view.name_i18n = row.name_i18n;
|
|
107
|
+
else if (!redundantNameI18n(row.name_i18n, row.name))
|
|
108
|
+
view.name_i18n = row.name_i18n;
|
|
109
|
+
return view;
|
|
102
110
|
}
|
|
103
|
-
|
|
111
|
+
/**
|
|
112
|
+
* r28a F3/F6:记录瘦投影——绑定字段只要 id/code/name;审计列与冗余
|
|
113
|
+
* name_i18n 默认丢掉。`full:true` 保留原生行。
|
|
114
|
+
*/
|
|
115
|
+
function recordView(value, options = {}) {
|
|
116
|
+
const row = (value && typeof value === "object" ? value : {});
|
|
117
|
+
if (options.full)
|
|
118
|
+
return { ...row };
|
|
119
|
+
const view = {
|
|
120
|
+
id: row.id,
|
|
121
|
+
code: row.code,
|
|
122
|
+
name: row.name,
|
|
123
|
+
status: row.status,
|
|
124
|
+
sortKey: row.sortKey,
|
|
125
|
+
parentId: row.parentId ?? null,
|
|
126
|
+
};
|
|
127
|
+
if (!redundantNameI18n(row.name_i18n, row.name) && row.name_i18n !== undefined)
|
|
128
|
+
view.name_i18n = row.name_i18n;
|
|
129
|
+
if (Array.isArray(row.children) && row.children.length > 0) {
|
|
130
|
+
view.children = row.children.map((child) => recordView(child, options));
|
|
131
|
+
}
|
|
132
|
+
return view;
|
|
133
|
+
}
|
|
134
|
+
function redundantNameI18n(nameI18n, name) {
|
|
135
|
+
if (typeof name !== "string")
|
|
136
|
+
return false;
|
|
137
|
+
let parsed = nameI18n;
|
|
138
|
+
if (typeof nameI18n === "string") {
|
|
139
|
+
try {
|
|
140
|
+
parsed = JSON.parse(nameI18n);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
147
|
+
return false;
|
|
148
|
+
const values = Object.values(parsed);
|
|
149
|
+
return values.length > 0 && values.every((value) => value === name);
|
|
150
|
+
}
|
|
151
|
+
/** Stable order for readbacks: sortKey asc, then code. Platform order is not submission order (r28a F3). */
|
|
152
|
+
function sortRecordsBySortKey(rows) {
|
|
153
|
+
return [...rows]
|
|
154
|
+
.map((row) => (row && typeof row === "object" ? row : {}))
|
|
155
|
+
.sort((left, right) => {
|
|
156
|
+
const leftKey = Number(left.sortKey ?? 0);
|
|
157
|
+
const rightKey = Number(right.sortKey ?? 0);
|
|
158
|
+
if (leftKey !== rightKey)
|
|
159
|
+
return leftKey - rightKey;
|
|
160
|
+
return String(left.code ?? "").localeCompare(String(right.code ?? ""));
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
const RECORDS_ORDER_NOTE = "records 顺序不保证与提交一致;顺序请用 sortKey(回读已按 sortKey 升序,同值再按 code)";
|
|
164
|
+
function recordsPage(value, options = {}) {
|
|
104
165
|
const page = (value && typeof value === "object" ? value : {});
|
|
166
|
+
const raw = Array.isArray(page.content) ? page.content : [];
|
|
167
|
+
const ordered = options.sort === false ? raw : sortRecordsBySortKey(raw);
|
|
105
168
|
return {
|
|
106
|
-
totalElements: page.totalElements ??
|
|
169
|
+
totalElements: page.totalElements ?? ordered.length,
|
|
107
170
|
totalPages: page.totalPages ?? 0,
|
|
108
|
-
content:
|
|
171
|
+
content: ordered.map((row) => recordView(row, options)),
|
|
109
172
|
};
|
|
110
173
|
}
|
|
111
174
|
function requireExactlyOne(id, code) {
|
|
@@ -120,11 +183,20 @@ export function registerDictionaryLane(tools) {
|
|
|
120
183
|
lane: LANE,
|
|
121
184
|
scope: TOOLS["dictionary.list"].scope,
|
|
122
185
|
description: TOOLS["dictionary.list"].description,
|
|
123
|
-
inputSchema: { environment: environmentArg, capability: capabilityArg, classificationId: z.string().optional(), searchWord: z.string().optional(), page: z.number().int().min(1).default(1), size: z.number().int().min(1).max(200).default(20), sortColumn: z.string().optional(), sortDirection: z.enum(["asc", "desc"]).optional() },
|
|
186
|
+
inputSchema: { environment: environmentArg, capability: capabilityArg, classificationId: z.string().optional(), searchWord: z.string().optional(), page: z.number().int().min(1).default(1), size: z.number().int().min(1).max(200).default(20), sortColumn: z.string().optional(), sortDirection: z.enum(["asc", "desc"]).optional(), full: z.boolean().default(false) },
|
|
124
187
|
handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
|
|
125
188
|
const data = await wireCall(session, { path: ENDPOINTS.dictionarySearch, query: { classificationId: args.classificationId ?? "", searchWord: args.searchWord ?? "", page: String(args.page), size: String(args.size), sortColumn: args.sortColumn ?? "", sortDirection: args.sortDirection ?? "desc" } });
|
|
126
|
-
|
|
127
|
-
|
|
189
|
+
// r28a F1/F2:分页 envelope 保留;顶层再给 total,避免 --jq totalElements 路径迷失。
|
|
190
|
+
// list 的 content 是字典头(不是 records),用 dictionaryView 瘦投影。
|
|
191
|
+
const page = (data.data && typeof data.data === "object" ? data.data : {});
|
|
192
|
+
const raw = Array.isArray(page.content) ? page.content : [];
|
|
193
|
+
const dictionaries = {
|
|
194
|
+
totalElements: page.totalElements ?? raw.length,
|
|
195
|
+
totalPages: page.totalPages ?? 0,
|
|
196
|
+
content: raw.map((row) => dictionaryView(row, { full: args.full })),
|
|
197
|
+
};
|
|
198
|
+
return { dictionaries, total: Number(dictionaries.totalElements ?? 0) };
|
|
199
|
+
}, { full: args.full }),
|
|
128
200
|
}));
|
|
129
201
|
tools.push(defineTool({
|
|
130
202
|
name: "dictionary.type",
|
|
@@ -133,11 +205,12 @@ export function registerDictionaryLane(tools) {
|
|
|
133
205
|
lane: LANE,
|
|
134
206
|
scope: TOOLS["dictionary.type"].scope,
|
|
135
207
|
description: TOOLS["dictionary.type"].description,
|
|
136
|
-
inputSchema: { environment: environmentArg, capability: capabilityArg, type: dictionaryType },
|
|
208
|
+
inputSchema: { environment: environmentArg, capability: capabilityArg, type: dictionaryType, full: z.boolean().default(false) },
|
|
137
209
|
handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
|
|
138
210
|
const data = await wireCall(session, { path: ENDPOINTS.dictionaryGetByType, query: { type: String(args.type) } });
|
|
139
|
-
|
|
140
|
-
|
|
211
|
+
const dictionaries = Array.isArray(data.data) ? data.data.map((row) => dictionaryView(row, { full: args.full })) : [];
|
|
212
|
+
return { dictionaryType: args.type, dictionaries, total: dictionaries.length };
|
|
213
|
+
}, { full: args.full }),
|
|
141
214
|
}));
|
|
142
215
|
tools.push(defineTool({
|
|
143
216
|
name: "dictionary.names",
|
|
@@ -146,11 +219,19 @@ export function registerDictionaryLane(tools) {
|
|
|
146
219
|
lane: LANE,
|
|
147
220
|
scope: TOOLS["dictionary.names"].scope,
|
|
148
221
|
description: TOOLS["dictionary.names"].description,
|
|
149
|
-
inputSchema: { environment: environmentArg, capability: capabilityArg },
|
|
222
|
+
inputSchema: { environment: environmentArg, capability: capabilityArg, full: z.boolean().default(false) },
|
|
150
223
|
handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
|
|
224
|
+
// r28a F4/F5:平台 getDictionaryIdAndNames 的覆盖可能小于 searchDictionary
|
|
225
|
+
// (真机 7 vs 18)。CLI 如实回传平台数组并显式给 total + coverageNote;
|
|
226
|
+
// 全量/防冲突请用 dictionary.list(可分页)。
|
|
151
227
|
const data = await wireCall(session, { path: ENDPOINTS.dictionaryIdAndNames, query: {} });
|
|
152
|
-
|
|
153
|
-
|
|
228
|
+
const dictionaries = Array.isArray(data.data) ? data.data.map((row) => dictionaryView(row, { full: args.full })) : [];
|
|
229
|
+
return {
|
|
230
|
+
dictionaries,
|
|
231
|
+
total: dictionaries.length,
|
|
232
|
+
coverageNote: "dictionary.names 透传平台 getDictionaryIdAndNames;其覆盖可能小于 dictionary.list(searchDictionary)。全量清单/防 code 冲突请用 dictionary.list(--jq 'dictionaries.totalElements,dictionaries.content[].code' 或看顶层 total)。",
|
|
233
|
+
};
|
|
234
|
+
}, { full: args.full }),
|
|
154
235
|
}));
|
|
155
236
|
tools.push(defineTool({
|
|
156
237
|
name: "dictionary.get",
|
|
@@ -159,12 +240,12 @@ export function registerDictionaryLane(tools) {
|
|
|
159
240
|
lane: LANE,
|
|
160
241
|
scope: TOOLS["dictionary.get"].scope,
|
|
161
242
|
description: TOOLS["dictionary.get"].description,
|
|
162
|
-
inputSchema: { environment: environmentArg, capability: capabilityArg, id: z.string().optional(), code: z.string().optional() },
|
|
243
|
+
inputSchema: { environment: environmentArg, capability: capabilityArg, id: z.string().optional(), code: z.string().optional(), full: z.boolean().default(false) },
|
|
163
244
|
handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
|
|
164
245
|
requireExactlyOne(args.id, args.code);
|
|
165
246
|
const data = await wireCall(session, { path: args.id ? ENDPOINTS.dictionaryGet : ENDPOINTS.dictionaryGetByCode, query: args.id ? { id: args.id } : { code: args.code } });
|
|
166
|
-
return { dictionary: dictionaryView(data.data) };
|
|
167
|
-
}),
|
|
247
|
+
return { dictionary: dictionaryView(data.data, { full: args.full }) };
|
|
248
|
+
}, { full: args.full }),
|
|
168
249
|
}));
|
|
169
250
|
tools.push(defineTool({
|
|
170
251
|
name: "dictionary.records",
|
|
@@ -173,12 +254,14 @@ export function registerDictionaryLane(tools) {
|
|
|
173
254
|
lane: LANE,
|
|
174
255
|
scope: TOOLS["dictionary.records"].scope,
|
|
175
256
|
description: TOOLS["dictionary.records"].description,
|
|
176
|
-
inputSchema: { environment: environmentArg, capability: capabilityArg, id: z.string().optional(), code: z.string().optional(), parentId: z.string().optional(), searchWord: z.string().optional(), enabledOnly: z.boolean().default(false), page: z.number().int().min(1).default(1), size: z.number().int().min(1).max(200).default(200) },
|
|
257
|
+
inputSchema: { environment: environmentArg, capability: capabilityArg, id: z.string().optional(), code: z.string().optional(), parentId: z.string().optional(), searchWord: z.string().optional(), enabledOnly: z.boolean().default(false), page: z.number().int().min(1).default(1), size: z.number().int().min(1).max(200).default(200), full: z.boolean().default(false) },
|
|
177
258
|
handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
|
|
178
259
|
requireExactlyOne(args.id, args.code);
|
|
260
|
+
const projectRows = (rows) => sortRecordsBySortKey(rows).map((row) => recordView(row, { full: args.full }));
|
|
179
261
|
if (args.parentId) {
|
|
180
262
|
const data = await wireCall(session, { path: ENDPOINTS.dictionaryParentRecords, query: { parentId: args.parentId } });
|
|
181
|
-
|
|
263
|
+
const records = Array.isArray(data.data) ? projectRows(data.data) : [];
|
|
264
|
+
return { parentId: args.parentId, records, total: records.length, orderNote: RECORDS_ORDER_NOTE };
|
|
182
265
|
}
|
|
183
266
|
if (args.enabledOnly) {
|
|
184
267
|
const dictionary = args.id ? { id: args.id } : (await wireCall(session, { path: ENDPOINTS.dictionaryGetByCode, query: { code: args.code } })).data;
|
|
@@ -186,57 +269,99 @@ export function registerDictionaryLane(tools) {
|
|
|
186
269
|
if (!id)
|
|
187
270
|
throw new WireError("business", "dictionary code not found", 301001);
|
|
188
271
|
const data = await wireCall(session, { path: ENDPOINTS.dictionaryEnabledRecords, query: { dicId: id } });
|
|
189
|
-
|
|
272
|
+
const records = Array.isArray(data.data) ? projectRows(data.data) : [];
|
|
273
|
+
return { dictionaryId: id, enabledOnly: true, records, total: records.length, orderNote: RECORDS_ORDER_NOTE };
|
|
190
274
|
}
|
|
191
275
|
const path = args.code ? ENDPOINTS.dictionaryRecordsByCode : ENDPOINTS.dictionaryRecords;
|
|
192
276
|
const query = args.code
|
|
193
277
|
? { code: args.code, searchWord: args.searchWord ?? "", page: String(args.page), size: String(args.size) }
|
|
194
278
|
: { id: args.id, searchWord: args.searchWord ?? "", page: String(args.page), size: String(args.size) };
|
|
195
279
|
const data = await wireCall(session, { path, query });
|
|
196
|
-
|
|
197
|
-
|
|
280
|
+
const page = recordsPage(data.data, { full: args.full });
|
|
281
|
+
return { records: page, total: Number(page.totalElements ?? 0), orderNote: RECORDS_ORDER_NOTE };
|
|
282
|
+
}, { full: args.full }),
|
|
198
283
|
}));
|
|
199
|
-
writeTool(
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
//
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
284
|
+
writeTool({
|
|
285
|
+
tools,
|
|
286
|
+
name: "dictionary.save",
|
|
287
|
+
lane: LANE,
|
|
288
|
+
scope: TOOLS["dictionary.save"].scope,
|
|
289
|
+
description: TOOLS["dictionary.save"].description,
|
|
290
|
+
inputSchema: {
|
|
291
|
+
id: z.string().optional(), code: z.string().min(1), name: z.string().min(1), name_i18n: nameI18n, dictionaryType, classificationId: z.string().optional(), sortKey: z.number().int().optional(), status: z.boolean().optional(), records: recordsArg,
|
|
292
|
+
},
|
|
293
|
+
execute: async (session, args) => {
|
|
294
|
+
const existing = (await wireCall(session, { path: ENDPOINTS.dictionaryGetByCode, query: { code: args.code } })).data;
|
|
295
|
+
if (!args.id && existing && typeof existing === "object" && Object.keys(existing).length > 0)
|
|
296
|
+
throw new ToolRefusalError({ status: "refused", blocker: "dictionary-create-duplicate", code: args.code, message: `字典 ${args.code} 已存在;更新请传 id` });
|
|
297
|
+
const submittedRecords = args.records;
|
|
298
|
+
const body = {
|
|
299
|
+
...(args.id ? { id: args.id } : {}), code: args.code, name: args.name, name_i18n: JSON.stringify(args.name_i18n ?? { "zh-CN": args.name, en: args.name }), dictionaryType: args.dictionaryType,
|
|
300
|
+
classificationId: args.classificationId ?? "", sortKey: args.sortKey ?? 0, status: args.status ?? true,
|
|
301
|
+
records: (submittedRecords ?? []).map((record) => normalizeRecord(record)),
|
|
302
|
+
};
|
|
303
|
+
const performed = await wireCall(session, { path: ENDPOINTS.dictionarySave, method: "POST", body });
|
|
304
|
+
const after = (await wireCall(session, { path: ENDPOINTS.dictionaryGetByCode, query: { code: args.code } })).data;
|
|
305
|
+
// The create/update response only carries the header; the records the caller
|
|
306
|
+
// just sent are invisible unless they make another call (dogfood R3). Read
|
|
307
|
+
// them back here so "the header saved" cannot be mistaken for "the records
|
|
308
|
+
// were verified".
|
|
309
|
+
let recordsReadback = { totalElements: 0, totalPages: 0, content: [] };
|
|
310
|
+
try {
|
|
311
|
+
const stored = (await wireCall(session, { path: ENDPOINTS.dictionaryEnabledRecordsByCode, query: { code: args.code } })).data;
|
|
312
|
+
const all = (await wireCall(session, { path: ENDPOINTS.dictionaryRecordsByCode, query: { code: args.code, searchWord: "", page: "1", size: "200" } })).data;
|
|
313
|
+
// r28a F3:瘦投影 + 按 sortKey 排序;顺序不可依赖提交顺序。
|
|
314
|
+
recordsReadback = { ...recordsPage(all), enabledCount: Array.isArray(stored) ? stored.length : 0 };
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
// best-effort: the save itself is already authoritative; a record-read
|
|
318
|
+
// failure must not turn a landed write into a failure.
|
|
319
|
+
}
|
|
320
|
+
const readback = {
|
|
321
|
+
present: Boolean(after && Object.keys(after).length > 0),
|
|
322
|
+
dictionary: dictionaryView(after),
|
|
323
|
+
records: recordsReadback,
|
|
324
|
+
orderNote: RECORDS_ORDER_NOTE,
|
|
325
|
+
};
|
|
326
|
+
// r28a F13:records:[] 是增量 upsert 空操作,不会清空既有记录;顶层 ok 会
|
|
327
|
+
// 伪装成「清空成功」。只要调用方显式传了 [],就标 intentUnmet(不依赖
|
|
328
|
+
// 读回条数——部分 fake/版本族可能把 [] 当全量替换,平台真机不会)。
|
|
329
|
+
if (Array.isArray(submittedRecords) && submittedRecords.length === 0) {
|
|
330
|
+
readback.intentUnmet = true;
|
|
331
|
+
readback.intentUnmetReason = "records:[] 不会删除既有记录(平台无字典/记录删除端点,save 是增量 upsert);请用 dictionary.status 逐条停用。";
|
|
332
|
+
}
|
|
333
|
+
return { performed: { errcode: performed.errcode }, readback };
|
|
334
|
+
},
|
|
335
|
+
rollback: dictionaryRollbackSpec("dictionary.save"),
|
|
234
336
|
});
|
|
235
|
-
writeTool(
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
337
|
+
writeTool({
|
|
338
|
+
tools,
|
|
339
|
+
name: "dictionary.status",
|
|
340
|
+
lane: LANE,
|
|
341
|
+
scope: TOOLS["dictionary.status"].scope,
|
|
342
|
+
description: TOOLS["dictionary.status"].description,
|
|
343
|
+
inputSchema: {
|
|
344
|
+
target: z.enum(["dictionary", "record"]), id: z.string().min(1), status: z.boolean(),
|
|
345
|
+
},
|
|
346
|
+
execute: async (session, args) => {
|
|
347
|
+
const path = args.target === "dictionary" ? ENDPOINTS.dictionaryUpdateStatus : ENDPOINTS.dictionaryRecordStatus;
|
|
348
|
+
const performed = await wireCall(session, { path, method: "POST", body: { id: args.id, status: args.status } });
|
|
349
|
+
return { performed: { errcode: performed.errcode }, readback: { id: args.id, status: args.status } };
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
writeTool({
|
|
353
|
+
tools,
|
|
354
|
+
name: "dictionary.usage",
|
|
355
|
+
lane: LANE,
|
|
356
|
+
scope: TOOLS["dictionary.usage"].scope,
|
|
357
|
+
description: TOOLS["dictionary.usage"].description,
|
|
358
|
+
inputSchema: {
|
|
359
|
+
ids: z.array(z.string().min(1)).min(1).max(1000),
|
|
360
|
+
},
|
|
361
|
+
execute: async (session, args) => {
|
|
362
|
+
const performed = await wireCall(session, { path: ENDPOINTS.dictionaryRecordUsed, method: "POST", body: { id: args.ids.join(";") } });
|
|
363
|
+
return { performed: { errcode: performed.errcode, ids: args.ids }, readback: { ids: args.ids, initialUsed: true } };
|
|
364
|
+
},
|
|
240
365
|
});
|
|
241
366
|
tools.push(defineTool({
|
|
242
367
|
name: "dictionary.parent",
|
|
@@ -264,53 +389,71 @@ export function registerDictionaryLane(tools) {
|
|
|
264
389
|
return { detail: args.detail, classes: Array.isArray(data.data) ? data.data : [] };
|
|
265
390
|
}),
|
|
266
391
|
}));
|
|
267
|
-
writeTool(
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}, async (session, args) => {
|
|
281
|
-
// The platform's deleteDictionaryClass(Batch) answers errcode 0 for ANY id —
|
|
282
|
-
// a dictionary id, a typo, anything — while deleting nothing. Writing
|
|
283
|
-
// `absent: true` unconditionally turned that into a silent false success
|
|
284
|
-
// (2026-10-30 probe: passing a dictionary id reported success and left the
|
|
285
|
-
// object in place). Verify the inputs are class ids BEFORE the write and
|
|
286
|
-
// derive absence from a post-delete read.
|
|
287
|
-
const classIds = async () => {
|
|
392
|
+
writeTool({
|
|
393
|
+
tools,
|
|
394
|
+
name: "dictionary.class.save",
|
|
395
|
+
lane: LANE,
|
|
396
|
+
scope: TOOLS["dictionary.class.save"].scope,
|
|
397
|
+
description: TOOLS["dictionary.class.save"].description,
|
|
398
|
+
inputSchema: {
|
|
399
|
+
id: z.string().optional(), name: z.string().min(1), dictionaryIds: z.array(z.string()).optional(), sortKey: z.number().int().optional(),
|
|
400
|
+
},
|
|
401
|
+
execute: async (session, args) => {
|
|
402
|
+
const performed = await wireCall(session, { path: ENDPOINTS.dictionaryClassSave, method: "POST", body: { ...(args.id ? { id: args.id } : {}), name: args.name, dictionaryIds: args.dictionaryIds ?? [], sortKey: args.sortKey ?? 0 } });
|
|
403
|
+
// Read back the created/updated class so the caller (and the audit trail)
|
|
404
|
+
// gets its id, not just the name it sent.
|
|
288
405
|
const listed = (await wireCall(session, { path: ENDPOINTS.dictionaryClasses, query: {} })).data;
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
406
|
+
const classes = Array.isArray(listed) ? listed : [];
|
|
407
|
+
const saved = classes.find((entry) => entry.name === args.name && (args.id === undefined || entry.id === args.id)) ?? classes.find((entry) => entry.name === args.name);
|
|
408
|
+
return { performed: { errcode: performed.errcode }, readback: { present: saved !== undefined, id: saved?.id, name: args.name, dictionaryIds: args.dictionaryIds ?? [] } };
|
|
409
|
+
},
|
|
410
|
+
rollback: dictionaryRollbackSpec("dictionary.class.save"),
|
|
411
|
+
});
|
|
412
|
+
writeTool({
|
|
413
|
+
tools,
|
|
414
|
+
name: "dictionary.class.delete",
|
|
415
|
+
lane: LANE,
|
|
416
|
+
scope: TOOLS["dictionary.class.delete"].scope,
|
|
417
|
+
description: TOOLS["dictionary.class.delete"].description,
|
|
418
|
+
inputSchema: {
|
|
419
|
+
ids: z.array(z.string().min(1)).min(1),
|
|
420
|
+
},
|
|
421
|
+
execute: async (session, args) => {
|
|
422
|
+
// The platform's deleteDictionaryClass(Batch) answers errcode 0 for ANY id —
|
|
423
|
+
// a dictionary id, a typo, anything — while deleting nothing. Writing
|
|
424
|
+
// `absent: true` unconditionally turned that into a silent false success
|
|
425
|
+
// (2026-10-30 probe: passing a dictionary id reported success and left the
|
|
426
|
+
// object in place). Verify the inputs are class ids BEFORE the write and
|
|
427
|
+
// derive absence from a post-delete read.
|
|
428
|
+
const classIds = async () => {
|
|
429
|
+
const listed = (await wireCall(session, { path: ENDPOINTS.dictionaryClasses, query: {} })).data;
|
|
430
|
+
return new Set((Array.isArray(listed) ? listed : [])
|
|
431
|
+
.map((entry) => (typeof entry.id === "string" ? entry.id : ""))
|
|
432
|
+
.filter((id) => id !== ""));
|
|
433
|
+
};
|
|
434
|
+
const before = await classIds();
|
|
435
|
+
const unmatched = args.ids.filter((id) => !before.has(id));
|
|
436
|
+
if (unmatched.length > 0) {
|
|
437
|
+
throw new ToolRefusalError({
|
|
438
|
+
status: "refused",
|
|
439
|
+
blocker: "dictionary-class-absent",
|
|
440
|
+
ids: args.ids,
|
|
441
|
+
unmatched,
|
|
442
|
+
candidates: [...before].slice(0, 20),
|
|
443
|
+
message: `${unmatched.join("、")} 不是字典分类 id(删前分类列表里没有它)。平台 deleteDictionaryClass 对任意 id 都回 errcode 0 却不删任何行——传字典 id 会假成功。现有分类候选:${[...before].slice(0, 20).join("、") || "(当前没有分类)"}。另:字典本体与字典项平台无删除端点,只能 dictionary.status 停用。`,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
const path = args.ids.length === 1 ? ENDPOINTS.dictionaryClassDelete : ENDPOINTS.dictionaryClassDeleteBatch;
|
|
447
|
+
const body = args.ids.length === 1 ? { id: args.ids[0] } : { ids: args.ids };
|
|
448
|
+
const performed = await wireCall(session, { path, method: "POST", body });
|
|
449
|
+
const after = await classIds();
|
|
450
|
+
const stillPresent = args.ids.filter((id) => after.has(id));
|
|
451
|
+
return {
|
|
452
|
+
performed: { errcode: performed.errcode, ids: args.ids },
|
|
453
|
+
readback: { absent: stillPresent.length === 0, ids: args.ids, ...(stillPresent.length > 0 ? { stillPresent } : {}) },
|
|
454
|
+
};
|
|
455
|
+
},
|
|
456
|
+
rollback: dictionaryRollbackSpec("dictionary.class.delete"),
|
|
457
|
+
});
|
|
315
458
|
}
|
|
316
459
|
//# sourceMappingURL=dictionary.js.map
|
|
@@ -31,8 +31,19 @@ export function registerEnvironmentLane(tools) {
|
|
|
31
31
|
// 2–3 次 wire(r31–r37 每会话重新盘点的纯浪费)。refresh:true 强制重查。
|
|
32
32
|
if (args.refresh !== true && args.applicationCode === undefined && args.modelCode === undefined && args.full !== true) {
|
|
33
33
|
const cached = readProjectCard(ctx.artifactsDir, String(args.environment ?? session.environmentName ?? ""));
|
|
34
|
-
if (cached !== undefined)
|
|
35
|
-
|
|
34
|
+
if (cached !== undefined) {
|
|
35
|
+
// r28b F16(真机 2026-11-05,本轮复现):实时读回给 `modelsCount`,
|
|
36
|
+
// 缓存命中给 `modelCount`——同一个键换条路径就换名字,脚本 `r["modelsCount"]`
|
|
37
|
+
// 直接 KeyError,基线对比也说不清是模型变了还是口径变了。
|
|
38
|
+
// 缓存是加速,不是第二套契约:键名与实时读对齐,旧键保留不破既有调用方。
|
|
39
|
+
const { modelCount, ...rest } = cached;
|
|
40
|
+
return {
|
|
41
|
+
...rest,
|
|
42
|
+
...(typeof modelCount === "number" ? { modelsCount: modelCount, modelCount } : {}),
|
|
43
|
+
status: "ok",
|
|
44
|
+
fromCache: true,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
36
47
|
}
|
|
37
48
|
// List every application the tenant exposes — the configured
|
|
38
49
|
// applicationCode is a default, never the only application. Resolve
|
|
@@ -146,6 +157,11 @@ export function registerEnvironmentLane(tools) {
|
|
|
146
157
|
writeProjectCard(ctx.artifactsDir, {
|
|
147
158
|
environment: envName,
|
|
148
159
|
applicationCode: selected,
|
|
160
|
+
// r28k F2:缓存命中必须与实时读**同形**,否则同工具同参数第二次
|
|
161
|
+
// 调用的 result 键集就变了(applications 消失 → --jq 直接 pathMiss)。
|
|
162
|
+
applications: apps,
|
|
163
|
+
groups,
|
|
164
|
+
applicationGroups: { status: groupStatus.status },
|
|
149
165
|
...(applicationResolution !== undefined ? { applicationResolution: { code: String(selected ?? applicationResolution.requested ?? ""), source: String(applicationResolution.source ?? "") } } : {}),
|
|
150
166
|
models: discovered.models.slice(0, 300).map((row) => ({ code: String(row.schemaCode), name: String(row.name ?? "") })),
|
|
151
167
|
modelCount: discovered.models.length,
|
|
@@ -153,18 +169,46 @@ export function registerEnvironmentLane(tools) {
|
|
|
153
169
|
});
|
|
154
170
|
}
|
|
155
171
|
}
|
|
156
|
-
return
|
|
172
|
+
return {
|
|
173
|
+
...discovered,
|
|
174
|
+
// r28k F3:裸 hex 不解自用。指纹是 (env, origin, application, enterprise)
|
|
175
|
+
// 的规范 sha256——绑定写计划 / intent plan 漂移门 / draft 写前复核 /
|
|
176
|
+
// 读回缓存键;它不是审批令牌。
|
|
177
|
+
fingerprintHint: FINGERPRINT_HINT,
|
|
178
|
+
};
|
|
157
179
|
}, {
|
|
158
180
|
full: args.full === true,
|
|
159
181
|
target: `discover_environment:${args.applicationCode ?? ""}:${args.modelCode ?? ""}`,
|
|
160
182
|
compact: (result) => {
|
|
183
|
+
// r28k F1(真机 2026-11-05):旧 compact 把 applications/foldersAndModels/models
|
|
184
|
+
// 全删掉,再由 read.ts 的占位补回 `[]`——与「确实没有应用」同形,只能靠
|
|
185
|
+
// truncated:true 旁证。应用通常个位数,内联完全值得;大表才标 *Omitted。
|
|
161
186
|
const compact = { ...result };
|
|
162
|
-
|
|
187
|
+
const apps = compact.applications;
|
|
188
|
+
if (Array.isArray(apps)) {
|
|
189
|
+
compact.applicationsCount = apps.length;
|
|
190
|
+
// 保留完整 applications(个位数);不删 key,避免占位 [] 覆盖。
|
|
191
|
+
}
|
|
192
|
+
const groups = compact.groups;
|
|
193
|
+
if (Array.isArray(groups)) {
|
|
194
|
+
compact.groupsCount = groups.length;
|
|
195
|
+
// groups 通常更小,一并内联。
|
|
196
|
+
}
|
|
197
|
+
for (const key of ["foldersAndModels", "models"]) {
|
|
163
198
|
const value = compact[key];
|
|
164
|
-
if (Array.isArray(value))
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
199
|
+
if (!Array.isArray(value))
|
|
200
|
+
continue;
|
|
201
|
+
compact[`${key}Count`] = value.length;
|
|
202
|
+
// 保留 key 但置 null + *Omitted,形状上区别于真实空数组 [];
|
|
203
|
+
// key 仍在 payload 里,read.ts 不会再补空占位。
|
|
204
|
+
compact[key] = null;
|
|
205
|
+
compact[`${key}Omitted`] = true;
|
|
206
|
+
}
|
|
207
|
+
if (compact.foldersAndModelsOmitted === true || compact.modelsOmitted === true) {
|
|
208
|
+
compact.tablesHint = "foldersAndModels/models 已外置(*Omitted:true,不是空表);用 detailArtifactId 的 `omc artifact get <id> --jq <key>` 或同次 call 加 --jq/--extract 取回。applications/groups 已内联。";
|
|
209
|
+
}
|
|
210
|
+
if (typeof compact.environmentFingerprint === "string" && compact.fingerprintHint === undefined) {
|
|
211
|
+
compact.fingerprintHint = FINGERPRINT_HINT;
|
|
168
212
|
}
|
|
169
213
|
return compact;
|
|
170
214
|
},
|
|
@@ -176,12 +220,16 @@ export function registerEnvironmentLane(tools) {
|
|
|
176
220
|
kind: "read",
|
|
177
221
|
lane: ENVIRONMENT_LANE,
|
|
178
222
|
scope: ENVIRONMENT_TOOLS["environment.fingerprint"].scope,
|
|
179
|
-
|
|
223
|
+
// r28k F3:用途放首句,避免 summary 在第一个英文句号处截断。
|
|
224
|
+
description: `${FINGERPRINT_HINT} ${ENVIRONMENT_TOOLS["environment.fingerprint"].description}`,
|
|
180
225
|
inputSchema: { environment: environmentArg, capability: capabilityArg },
|
|
181
226
|
handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => ({
|
|
182
227
|
environment: session.environmentName,
|
|
183
228
|
fingerprint: session.environmentFingerprint(),
|
|
229
|
+
fingerprintHint: FINGERPRINT_HINT,
|
|
184
230
|
})),
|
|
185
231
|
}));
|
|
186
232
|
}
|
|
233
|
+
/** r28k F3 — 指纹用途一句话(回执 + describe 共用,避免两处漂移)。 */
|
|
234
|
+
const FINGERPRINT_HINT = "用途:写前/写后漂移检测(intent plan、draft 复核)、写计划与审计 artifact 绑定、读回缓存键,随 env/origin/application/enterprise 变,不是审批令牌。";
|
|
187
235
|
//# sourceMappingURL=environment.js.map
|