@stalfh233/omc-cli 0.4.17 → 0.4.19
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/dist/bizservice-config.js +8 -3
- package/dist/cli.js +18 -0
- package/dist/closest-match.js +50 -0
- package/dist/field-families.js +5 -42
- package/dist/goal-routes.js +25 -2
- package/dist/lanes/b2-registration.js +25 -13
- package/dist/lanes/field.js +13 -10
- package/dist/lanes/model.js +13 -6
- package/dist/script.js +65 -0
- package/dist/support.js +22 -0
- package/package.json +2 -2
- package/skills/omc-start-here.md +1 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ToolRefusalError } from "./tool-types.js";
|
|
2
|
+
import { closestMatch } from "./closest-match.js";
|
|
2
3
|
/**
|
|
3
4
|
* Ticket 92 — business-integration write-face codec + preflight (pure functions,
|
|
4
5
|
* zero DSH/wire dependencies). Reused by service.register (92), service.update
|
|
@@ -159,9 +160,13 @@ function commonlistValue(serviceConfig, code) {
|
|
|
159
160
|
export function assertBizServiceAdapterWhitelisted(adapterCode) {
|
|
160
161
|
if (typeof adapterCode !== "string" || !adapterCode)
|
|
161
162
|
refuse(BIZSERVICE_CONFIG_BLOCKERS.adapterNotWhitelisted, "adapterCode 是必需的(结构化写面)", { adapterCode });
|
|
162
|
-
if (!BIZSERVICE_WRITE_ADAPTERS.includes(adapterCode))
|
|
163
|
-
|
|
164
|
-
|
|
163
|
+
if (!BIZSERVICE_WRITE_ADAPTERS.includes(adapterCode)) {
|
|
164
|
+
// F6: an off-by-one adapter guess gets a did-you-mean (same helper as the
|
|
165
|
+
// field families) before the prose.
|
|
166
|
+
const closest = closestMatch(adapterCode, BIZSERVICE_WRITE_ADAPTERS);
|
|
167
|
+
refuse(BIZSERVICE_CONFIG_BLOCKERS.adapterNotWhitelisted, `adapterCode ${adapterCode} 不在写面支持范围内;本写面仅支持 ${BIZSERVICE_WRITE_ADAPTERS.join("/")}。${closest !== undefined ? `你要找的可能是 ${closest}。` : ""}` +
|
|
168
|
+
`**自定义 Java 方法体的常规做法是:在扩展模块写一个接口,然后用 RESTFUL 适配器注册一个服务指向它**(service.register,adapterCode:RESTFUL,methodConfig.url 指向扩展端点);不要用 INTERNAL/custom_protocol 适配器(那是平台内部/自定义协议机制,不是二开接口的常规登记方式,也不是本工具的支持面)`, { adapterCode, supported: BIZSERVICE_WRITE_ADAPTERS, ...(closest !== undefined ? { closestMatch: closest } : {}) });
|
|
169
|
+
}
|
|
165
170
|
return adapterCode;
|
|
166
171
|
}
|
|
167
172
|
/** The AES boundary is not bypassable: a raw configJson string is refused before any wire call. */
|
package/dist/cli.js
CHANGED
|
@@ -1375,6 +1375,24 @@ async function main() {
|
|
|
1375
1375
|
const rest = [tool, ...(options.env ? ["--env", options.env] : []), ...(options.environment ? ["--environment", options.environment] : []), ...(options.args !== undefined ? ["--args", options.args] : []), ...(options.argsFile !== undefined ? ["--args-file", options.argsFile] : []), ...(options.maxOutputBytes !== undefined ? ["--max-output-bytes", options.maxOutputBytes] : []), ...(options.requestId ? ["--request-id", options.requestId] : []), ...(options.timeoutMs ? ["--timeout-ms", options.timeoutMs] : []), ...(options.wait !== undefined ? ["--wait", options.wait] : []), ...(options.dryRun ? ["--dry-run"] : [])];
|
|
1376
1376
|
commandExitCode = await cmdCall(new ConfigStore(defaultPaths()), rest);
|
|
1377
1377
|
});
|
|
1378
|
+
command.command("script [file]").option("--env <environment>", "行内无 env 时的默认环境").option("--continue", "某行失败后继续执行剩余行(默认停在首个失败)").description("NDJSON 多调脚本:每行 {\"tool\":\"<名称>\",\"args\":{...},\"env\":\"<环境>\",\"dryRun\":true}(# 注释/空行忽略;file 为 - 读 stdin);一个进程一次会话跑完全部,逐行输出 {index,tool,exitCode,result}").action(async (file, options) => {
|
|
1379
|
+
const { runScript, parseScript, readScriptInput } = await import("./script.js");
|
|
1380
|
+
let lines;
|
|
1381
|
+
try {
|
|
1382
|
+
lines = parseScript(readScriptInput(file ?? "-"));
|
|
1383
|
+
}
|
|
1384
|
+
catch (error) {
|
|
1385
|
+
console.error(String(error instanceof Error ? error.message : error));
|
|
1386
|
+
commandExitCode = CLI_EXIT_CODES.invalidInput;
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
if (lines.length === 0) {
|
|
1390
|
+
console.error("脚本为空:每行一个 {\"tool\":\"<名称>\",\"args\":{...}} 调用");
|
|
1391
|
+
commandExitCode = CLI_EXIT_CODES.invalidInput;
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
commandExitCode = await runScript(lines, { defaultEnv: options.env, continueOnError: options.continue === true });
|
|
1395
|
+
});
|
|
1378
1396
|
command.command("artifact").command("get <id>").option("--jq <path>").option("--path-only").option("--summary").option("--keys [a,b]").option("--max-output-bytes <bytes>").action(async (id, options) => {
|
|
1379
1397
|
commandExitCode = await withTransport(options, request, (transport) => cmdArtifactGet(id, transport, { ...(options.jq ? { jq: options.jq } : {}), pathOnly: options.pathOnly === true, summary: options.summary === true || options.keys === true, ...(typeof options.keys === "string" ? { keys: options.keys } : {}) }));
|
|
1380
1398
|
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* F6 (r10-friction): the shared did-you-mean closest-match helper. Born from
|
|
3
|
+
* the live-measured field-family case (NUMBER → correct name NUMERICAL cost a
|
|
4
|
+
* full extra round); the same off-by-one guesses hit every enum-like gate
|
|
5
|
+
* (adapter codes, …). Case/underscore-insensitive, three acceptance shapes:
|
|
6
|
+
* prefix containment, small edit distance, shared-prefix + proportional
|
|
7
|
+
* distance. Ambiguous short inputs (TEXT, BOOL) intentionally match nothing —
|
|
8
|
+
* a misleading suggestion is worse than the plain list.
|
|
9
|
+
*/
|
|
10
|
+
export function closestMatch(input, candidates) {
|
|
11
|
+
const wanted = input.toUpperCase().replace(/[\s_-]/g, "");
|
|
12
|
+
if (wanted.length < 2)
|
|
13
|
+
return undefined;
|
|
14
|
+
let best;
|
|
15
|
+
for (const name of candidates) {
|
|
16
|
+
const candidate = name.toUpperCase().replace(/[\s_-]/g, "");
|
|
17
|
+
// 1. prefix containment (STAFF→STAFF_SELECTOR, DATETIME→DATE)
|
|
18
|
+
const contained = (candidate.startsWith(wanted) || wanted.startsWith(candidate)) && Math.min(wanted.length, candidate.length) >= 4;
|
|
19
|
+
// 2. small edit distance (NUMERIC→NUMERICAL, SHORTTEXT→SHORT_TEXT)
|
|
20
|
+
const distance = editDistance(wanted, candidate);
|
|
21
|
+
const nearMiss = distance <= 2;
|
|
22
|
+
// 3. shared prefix + proportional distance (NUMBER→NUMERICAL, the live-measured case)
|
|
23
|
+
const sharedPrefix = commonPrefixLength(wanted, candidate);
|
|
24
|
+
const proportional = sharedPrefix >= 3 && wanted.length >= 5 && distance / Math.max(wanted.length, candidate.length) <= 0.58;
|
|
25
|
+
const accepted = contained || nearMiss || proportional;
|
|
26
|
+
if (accepted && (!best || distance < best.distance))
|
|
27
|
+
best = { name, distance };
|
|
28
|
+
}
|
|
29
|
+
return best?.name;
|
|
30
|
+
}
|
|
31
|
+
function commonPrefixLength(a, b) {
|
|
32
|
+
let index = 0;
|
|
33
|
+
while (index < a.length && index < b.length && a[index] === b[index])
|
|
34
|
+
index++;
|
|
35
|
+
return index;
|
|
36
|
+
}
|
|
37
|
+
function editDistance(a, b) {
|
|
38
|
+
const row = Array.from({ length: b.length + 1 }, (_, index) => index);
|
|
39
|
+
for (let i = 1; i <= a.length; i++) {
|
|
40
|
+
let previous = row[0];
|
|
41
|
+
row[0] = i;
|
|
42
|
+
for (let j = 1; j <= b.length; j++) {
|
|
43
|
+
const temp = row[j];
|
|
44
|
+
row[j] = Math.min(row[j] + 1, row[j - 1] + 1, previous + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
45
|
+
previous = temp;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return row[b.length];
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=closest-match.js.map
|
package/dist/field-families.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { closestMatch } from "./closest-match.js";
|
|
2
3
|
import { ToolRefusalError } from "./tool-types.js";
|
|
3
4
|
import { decodeJsonObjectColumn } from "./json-column.js";
|
|
4
5
|
import { FIELD_CONTROL_CODES, FIELD_DATE_DEFAULT_OPTIONS, FIELD_DATE_FORMATS, FIELD_FAMILY_LIST } from "./lanes/field-contract.js";
|
|
@@ -266,47 +267,9 @@ export function fieldTypeHint(type) {
|
|
|
266
267
|
return `「${type}」不是族名;你要找的可能是 ${closest}`;
|
|
267
268
|
return undefined;
|
|
268
269
|
}
|
|
269
|
-
/** Case/underscore-insensitive closest match over the family list. */
|
|
270
|
+
/** Case/underscore-insensitive closest match over the family list (shared helper, see closest-match.ts). */
|
|
270
271
|
function closestFamilyName(type) {
|
|
271
|
-
|
|
272
|
-
if (wanted.length < 2)
|
|
273
|
-
return undefined;
|
|
274
|
-
let best;
|
|
275
|
-
for (const name of Object.keys(FIELD_PROPERTY_TYPES)) {
|
|
276
|
-
const candidate = name.toUpperCase().replace(/[\s_-]/g, "");
|
|
277
|
-
// Three acceptance shapes, from exact-ish to fuzzy:
|
|
278
|
-
// 1. prefix containment (STAFF→STAFF_SELECTOR, DATETIME→DATE)
|
|
279
|
-
const contained = (candidate.startsWith(wanted) || wanted.startsWith(candidate)) && Math.min(wanted.length, candidate.length) >= 4;
|
|
280
|
-
// 2. small edit distance (NUMERIC→NUMERICAL, SHORTTEXT→SHORT_TEXT)
|
|
281
|
-
const distance = editDistance(wanted, candidate);
|
|
282
|
-
const nearMiss = distance <= 2;
|
|
283
|
-
// 3. shared prefix + proportional distance (NUMBER→NUMERICAL, the live-measured case)
|
|
284
|
-
const sharedPrefix = commonPrefixLength(wanted, candidate);
|
|
285
|
-
const proportional = sharedPrefix >= 3 && wanted.length >= 5 && distance / Math.max(wanted.length, candidate.length) <= 0.58;
|
|
286
|
-
const accepted = contained || nearMiss || proportional;
|
|
287
|
-
if (accepted && (!best || distance < best.distance))
|
|
288
|
-
best = { name, distance };
|
|
289
|
-
}
|
|
290
|
-
return best?.name;
|
|
291
|
-
}
|
|
292
|
-
function commonPrefixLength(a, b) {
|
|
293
|
-
let index = 0;
|
|
294
|
-
while (index < a.length && index < b.length && a[index] === b[index])
|
|
295
|
-
index++;
|
|
296
|
-
return index;
|
|
297
|
-
}
|
|
298
|
-
function editDistance(a, b) {
|
|
299
|
-
const row = Array.from({ length: b.length + 1 }, (_, index) => index);
|
|
300
|
-
for (let i = 1; i <= a.length; i++) {
|
|
301
|
-
let previous = row[0];
|
|
302
|
-
row[0] = i;
|
|
303
|
-
for (let j = 1; j <= b.length; j++) {
|
|
304
|
-
const temp = row[j];
|
|
305
|
-
row[j] = Math.min(row[j] + 1, row[j - 1] + 1, previous + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
306
|
-
previous = temp;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
return row[b.length];
|
|
272
|
+
return closestMatch(type, Object.keys(FIELD_PROPERTY_TYPES));
|
|
310
273
|
}
|
|
311
274
|
/** wire propertyType (int) -> form controlType (type-matrix.md §3.1). */
|
|
312
275
|
export const FIELD_CONTROL_TYPES = { ...CAPABILITY_CONTROL_TYPES };
|
|
@@ -829,7 +792,7 @@ function choiceOptionsError(type, options) {
|
|
|
829
792
|
// No optionsSet: refuse the shapes that LOOK like choices but the platform ignores.
|
|
830
793
|
for (const key of ["items", "labels", "custom", "dictionary", "businessModel", "options", "dictionaryData"]) {
|
|
831
794
|
if (options[key] !== undefined)
|
|
832
|
-
return `选中控件(单选/复选/下拉)的选项必须放进 optionsSet
|
|
795
|
+
return `选中控件(单选/复选/下拉)的选项必须放进 optionsSet;裸的 options.${key} 平台不消费,门户会渲染成空控件。最小可抄样本 ${JSON.stringify({ options: { optionsSet: { optionsType: "custom", custom: [{ code: "opt1", value: "1" }, { code: "opt2", value: "2" }] } } })},或 field.create 直接传 choices:["opt1","opt2"]`;
|
|
833
796
|
}
|
|
834
797
|
return null;
|
|
835
798
|
}
|
|
@@ -853,7 +816,7 @@ export function assertFieldOptionsAllowed(type, options, schemaCode) {
|
|
|
853
816
|
refuseFieldOptions("FORMULA options carried the legacy top-level resultType/expression keys, which the platform does not read", "rejected: FORMULA(100) options must use formulaConfig (nested JSON string) + persistent:true; top-level resultType/expression are not platform keys");
|
|
854
817
|
const formulaConfig = options?.formulaConfig;
|
|
855
818
|
if (!isNonEmptyString(formulaConfig))
|
|
856
|
-
refuseFieldOptions("FORMULA options are missing the required formulaConfig JSON string", "rejected: FORMULA(100) requires options.formulaConfig (a nested JSON string: resultType/formulaType/expression)
|
|
819
|
+
refuseFieldOptions("FORMULA options are missing the required formulaConfig JSON string", "rejected: FORMULA(100) requires options.formulaConfig (a nested JSON string: resultType/formulaType/expression)。最小可抄样本 " + JSON.stringify({ options: { formulaConfig: JSON.stringify({ formulaType: "EXPRESSION", expression: "{amount}*2", resultType: 0 }), persistent: true } }) + "(resultType 0=SHORT_TEXT/2=NUMERICAL;expression 引用模型字段)");
|
|
857
820
|
const config = parseOptionsObject(formulaConfig);
|
|
858
821
|
if (!config)
|
|
859
822
|
refuseFieldOptions("FORMULA formulaConfig is not a parseable JSON object string", "rejected: FORMULA(100) options.formulaConfig must be a JSON object string");
|
package/dist/goal-routes.js
CHANGED
|
@@ -231,13 +231,36 @@ function batchReadCommands(pointers) {
|
|
|
231
231
|
byTopic.set(pointer.topic, [...(byTopic.get(pointer.topic) ?? []), pointer.section]);
|
|
232
232
|
return [...byTopic.entries()].map(([topic, sections]) => `omc help ${topic} ${sections.map((section) => `--section "${section}"`).join(" ")}`);
|
|
233
233
|
}
|
|
234
|
-
/**
|
|
234
|
+
/**
|
|
235
|
+
* The route's methodology-read surface: pointers (+ per-section bytes) +
|
|
236
|
+
* ready-to-run batched commands + hint.
|
|
237
|
+
*
|
|
238
|
+
* F9 (r10-friction): the most common task flow was `next` followed
|
|
239
|
+
* IMMEDIATELY by fetching the first readSection — one extra process +
|
|
240
|
+
* round-trip every task. The first section now rides inline when it fits the
|
|
241
|
+
* inline budget (~2 KB); larger sections stay pointer-only so `next` never
|
|
242
|
+
* balloons.
|
|
243
|
+
*/
|
|
244
|
+
const FIRST_SECTION_INLINE_BUDGET_BYTES = 2560;
|
|
235
245
|
function routeReads(readSections) {
|
|
236
246
|
// Per-section UTF-8 bytes ride each pointer (one field, not a parallel array)
|
|
237
247
|
// so a caller can budget which sections to fetch — dogfood R1 found the
|
|
238
248
|
// batched readCommands could exceed a window with no way to see sizes.
|
|
239
249
|
const budgeted = readSections.map((pointer) => ({ ...pointer, bytes: sectionBytes(pointer) }));
|
|
240
|
-
|
|
250
|
+
// Inline the FIRST pointer that fits the budget (order = relevance); an
|
|
251
|
+
// oversized head section falls through to the next fitting one rather than
|
|
252
|
+
// dropping the inline content entirely.
|
|
253
|
+
let firstSection;
|
|
254
|
+
for (const pointer of budgeted.slice(0, 3)) {
|
|
255
|
+
if (pointer.bytes > 0 && pointer.bytes <= FIRST_SECTION_INLINE_BUDGET_BYTES) {
|
|
256
|
+
const resolved = readSkillSection(pointer.topic, pointer.section);
|
|
257
|
+
if (resolved) {
|
|
258
|
+
firstSection = { topic: pointer.topic, section: pointer.section, bytes: pointer.bytes, content: resolved.section };
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return { readSections: budgeted, readCommands: batchReadCommands(readSections), readHint: READ_HINT, ...(firstSection !== undefined ? { firstSection } : {}) };
|
|
241
264
|
}
|
|
242
265
|
/** UTF-8 byte length of one methodology section (best-effort; 0 when unreadable). */
|
|
243
266
|
function sectionBytes(pointer) {
|
|
@@ -158,9 +158,15 @@ export function registerB2Lane(tools) {
|
|
|
158
158
|
description: B2_TOOLS["service.get"].description,
|
|
159
159
|
inputSchema: { environment: environmentArg, serviceCode: z.string(), capability: capabilityArg },
|
|
160
160
|
handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
|
|
161
|
+
// F5: the service row and its method list are independent reads keyed
|
|
162
|
+
// by the same serviceCode — fire both at once. The absent path
|
|
163
|
+
// short-circuits AFTER the pair settles (the method read's result is
|
|
164
|
+
// simply discarded; absence is a fact either way).
|
|
165
|
+
const serviceRead = wireCall(session, { path: ENDPOINTS.bizServiceGetByCode, query: { serviceCode: args.serviceCode } });
|
|
166
|
+
const methodRead = wireCall(session, { path: ENDPOINTS.bizServiceMethodList, query: { serviceCode: args.serviceCode, filterFlag: "false" } }).catch((error) => error);
|
|
161
167
|
let service;
|
|
162
168
|
try {
|
|
163
|
-
service = await
|
|
169
|
+
service = await serviceRead;
|
|
164
170
|
}
|
|
165
171
|
catch (error) {
|
|
166
172
|
// A deleted service answers the wire-business error 500001; reading a
|
|
@@ -173,19 +179,25 @@ export function registerB2Lane(tools) {
|
|
|
173
179
|
const serviceRow = service.data;
|
|
174
180
|
let methods = null;
|
|
175
181
|
let methodsError = null;
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
name: row.name,
|
|
180
|
-
serviceCode: row.serviceCode ?? args.serviceCode,
|
|
181
|
-
configJson: maskServiceConfigSecrets(normalizeMethodConfigReadback(adapterCodeOf(serviceRow), decodeJsonColumn(row.configJson))),
|
|
182
|
-
inputParameters: normalizeParameterFaceReadback(adapterCodeOf(serviceRow), decodeJsonColumn(row.inputParametersJson)),
|
|
183
|
-
outputParameters: normalizeParameterFaceReadback(adapterCodeOf(serviceRow), decodeJsonColumn(row.outputParametersJson)),
|
|
184
|
-
referenced: row.referenced === true,
|
|
185
|
-
}));
|
|
182
|
+
const methodOutcome = await methodRead;
|
|
183
|
+
if (methodOutcome instanceof Error) {
|
|
184
|
+
methodsError = methodOutcome.message;
|
|
186
185
|
}
|
|
187
|
-
|
|
188
|
-
|
|
186
|
+
else {
|
|
187
|
+
try {
|
|
188
|
+
methods = wireRows(methodOutcome.data).map((row) => ({
|
|
189
|
+
code: row.code,
|
|
190
|
+
name: row.name,
|
|
191
|
+
serviceCode: row.serviceCode ?? args.serviceCode,
|
|
192
|
+
configJson: maskServiceConfigSecrets(normalizeMethodConfigReadback(adapterCodeOf(serviceRow), decodeJsonColumn(row.configJson))),
|
|
193
|
+
inputParameters: normalizeParameterFaceReadback(adapterCodeOf(serviceRow), decodeJsonColumn(row.inputParametersJson)),
|
|
194
|
+
outputParameters: normalizeParameterFaceReadback(adapterCodeOf(serviceRow), decodeJsonColumn(row.outputParametersJson)),
|
|
195
|
+
referenced: row.referenced === true,
|
|
196
|
+
}));
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
methodsError = error instanceof Error ? error.message : String(error);
|
|
200
|
+
}
|
|
189
201
|
}
|
|
190
202
|
return {
|
|
191
203
|
serviceCode: args.serviceCode,
|
package/dist/lanes/field.js
CHANGED
|
@@ -12,6 +12,7 @@ import { defineTool, ToolRefusalError } from "../tool-types.js";
|
|
|
12
12
|
import { wireCall, wireRows, WireError } from "../wire.js";
|
|
13
13
|
import { executeFormDraftWrite, executeFormPublishWrite } from "../form-support.js";
|
|
14
14
|
import { FIELD_LANE_CONTRACT } from "./field-contract.js";
|
|
15
|
+
import { mapBounded } from "../support.js";
|
|
15
16
|
const FIELD_LANE = FIELD_LANE_CONTRACT.lane;
|
|
16
17
|
const FIELD_TOOLS = FIELD_LANE_CONTRACT.tools;
|
|
17
18
|
/** Parse a raw options JSON string for readback; policy: non-strings and unparseable strings pass through verbatim. */
|
|
@@ -304,24 +305,26 @@ export async function executeFieldPublish(session, args) {
|
|
|
304
305
|
.map((row) => [codeOf(row), { name: String(row.name ?? ""), propertyType: Number(row.type ?? row.propertyType) }]));
|
|
305
306
|
const verifyFieldDetails = async (targets) => {
|
|
306
307
|
const bounded = targets.slice(0, 25);
|
|
307
|
-
const
|
|
308
|
-
for (const code of bounded) {
|
|
308
|
+
const checked = await mapBounded(bounded, 5, async (code) => {
|
|
309
309
|
const expected = draftSnapshot.get(code);
|
|
310
310
|
if (!expected)
|
|
311
|
-
|
|
312
|
-
let detail;
|
|
311
|
+
return { kind: "skip" };
|
|
313
312
|
try {
|
|
314
|
-
detail = (await wireCall(session, { path: ENDPOINTS.fieldGet, query: { schemaCode: args.schemaCode, propertyCode: code } })).data;
|
|
313
|
+
const detail = (await wireCall(session, { path: ENDPOINTS.fieldGet, query: { schemaCode: args.schemaCode, propertyCode: code } })).data;
|
|
314
|
+
const actual = { name: String(detail.name ?? ""), propertyType: Number(detail.propertyType ?? detail.type) };
|
|
315
|
+
if (actual.name !== expected.name || actual.propertyType !== expected.propertyType)
|
|
316
|
+
return { kind: "mismatch", code, expected, actual };
|
|
317
|
+
return { kind: "ok" };
|
|
315
318
|
}
|
|
316
319
|
catch {
|
|
317
320
|
// The detail endpoint is optional across builds: report the
|
|
318
321
|
// verification as unavailable rather than failing the publish.
|
|
319
|
-
return {
|
|
322
|
+
return { kind: "unavailable" };
|
|
320
323
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
324
|
+
});
|
|
325
|
+
if (checked.some((entry) => entry.kind === "unavailable"))
|
|
326
|
+
return { mode: "unavailable", verified: false, note: "bizproperty/get 不可用,字段详情未核对(仅列表面存在性)" };
|
|
327
|
+
const mismatches = checked.filter((entry) => entry.kind === "mismatch");
|
|
325
328
|
return { mode: "detail", verified: true, checked: bounded.length, ok: mismatches.length === 0, ...(mismatches.length > 0 ? { mismatches } : {}) };
|
|
326
329
|
};
|
|
327
330
|
const preRead = await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "true" } });
|
package/dist/lanes/model.js
CHANGED
|
@@ -3,6 +3,7 @@ import { ENDPOINTS } from "../endpoints.js";
|
|
|
3
3
|
import { assertFieldFamilyAllowed, choicesArg, fieldOptionsArg, nameI18nJson, propertyTypeArg, relationTargetOf, relativeCodeArg, SYSTEM_FIELD_CODES, subFieldArg, } from "../field-families.js";
|
|
4
4
|
import { fieldChangeWireBody, prepareFieldChange, sameFieldChange } from "../field-change.js";
|
|
5
5
|
import { executeFormDraftWrite, executeFormPublishWrite } from "../form-support.js";
|
|
6
|
+
import { mapBounded } from "../support.js";
|
|
6
7
|
import { validateBusinessCode } from "../code-rules.js";
|
|
7
8
|
import { writeSummary } from "../human-summary.js";
|
|
8
9
|
import { capabilityArg, environmentArg } from "../args.js";
|
|
@@ -188,16 +189,25 @@ export function registerModelLane(tools) {
|
|
|
188
189
|
}
|
|
189
190
|
};
|
|
190
191
|
const inspectOne = async (schemaCode) => {
|
|
191
|
-
|
|
192
|
+
// F5: the six read faces are INDEPENDENT wire reads; fan them out in
|
|
193
|
+
// parallel (bounded) instead of paying six sequential round-trips. Only
|
|
194
|
+
// workflow.get_published stays chained (it needs workflowCode from the
|
|
195
|
+
// draft face). Output ordering is unchanged (results land by index).
|
|
196
|
+
const [fieldFace, formFace, listviewFace, ruleFace, dataruleFace, draftFace] = await mapBounded([
|
|
197
|
+
() => face("field.list", { schemaCode, published: true, ...(all ? { all: true } : {}) }),
|
|
198
|
+
() => face("form.get", { schemaCode }),
|
|
199
|
+
() => face("listview.get", { schemaCode }),
|
|
200
|
+
() => face("rule.list", { schemaCode }),
|
|
201
|
+
() => face("datarule.list", { schemaCode }),
|
|
202
|
+
() => face("workflow.get_draft", { schemaCode }),
|
|
203
|
+
], 6, (task) => task());
|
|
192
204
|
const fieldRows = Array.isArray(fieldFace?.fields) ? fieldFace.fields : [];
|
|
193
205
|
const fields = fieldRows
|
|
194
206
|
.filter((field) => all || !SYSTEM_FIELD_CODES.has(String(field.code)))
|
|
195
207
|
.map((field) => ({ code: field.code, type: field.type, ...(field.typeName !== undefined ? { typeName: field.typeName } : {}), name: field.name, ...(field.conditions ? { conditions: field.conditions } : {}) }));
|
|
196
|
-
const formFace = await face("form.get", { schemaCode });
|
|
197
208
|
const form = formFace && formFace.present !== undefined
|
|
198
209
|
? { present: formFace.present === true, published: formFace.published === true, controlCount: Array.isArray(formFace.draftControlCodes) ? formFace.draftControlCodes.length : 0 }
|
|
199
210
|
: undefined;
|
|
200
|
-
const listviewFace = await face("listview.get", { schemaCode });
|
|
201
211
|
const listviewColumns = Array.isArray(listviewFace?.columns) ? listviewFace.columns : [];
|
|
202
212
|
// Round 13: the default view is platform-generated and lists the system
|
|
203
213
|
// columns (createdTime/creater/name/sequenceNo/…). In a design-time survey
|
|
@@ -208,14 +218,12 @@ export function registerModelLane(tools) {
|
|
|
208
218
|
const listview = listviewFace && listviewFace.columns
|
|
209
219
|
? { code: listviewFace.code ?? schemaCode, columnCount: visibleColumns.length, columns: visibleColumns.map((column) => ({ propertyCode: column.propertyCode, propertyType: column.propertyType, visible: column.visible })) }
|
|
210
220
|
: undefined;
|
|
211
|
-
const ruleFace = await face("rule.list", { schemaCode });
|
|
212
221
|
const ruleRows = Array.isArray(ruleFace?.rules) ? ruleFace.rules : [];
|
|
213
222
|
// All rules are kept: the platform pre-creates trigger rows (Create/Update/
|
|
214
223
|
// …) but user logic is AUTHORED INTO them, so `defaultRule` cannot be used
|
|
215
224
|
// to hide "system" rules without dropping authored ones. `defaultRule` is
|
|
216
225
|
// reported per rule so a caller can decide.
|
|
217
226
|
const rules = ruleRows.map((rule) => ({ code: rule.code, name: rule.name, enabled: rule.enabled, status: rule.status, ruleVersion: rule.ruleVersion, defaultRule: rule.defaultRule }));
|
|
218
|
-
const dataruleFace = await face("datarule.list", { schemaCode });
|
|
219
227
|
const dataruleRows = Array.isArray(dataruleFace?.rules) ? dataruleFace.rules : [];
|
|
220
228
|
const datarules = dataruleRows.map((rule) => ({ propertyCode: rule.propertyCode, propertyCodes: rule.propertyCodes, dataRuleType: rule.dataRuleType, name: rule.name, checkType: rule.checkType }));
|
|
221
229
|
// Workflow summary: use the PUBLISHED face for the `published` flag. The
|
|
@@ -224,7 +232,6 @@ export function registerModelLane(tools) {
|
|
|
224
232
|
// `published` from get_draft reported published:false forever (live round
|
|
225
233
|
// 10: an agent's flow read as published:false while get_published was
|
|
226
234
|
// templateType:PUBLISH). Activities come from whichever face exists.
|
|
227
|
-
const draftFace = await face("workflow.get_draft", { schemaCode });
|
|
228
235
|
const workflowCode = draftFace?.workflowSummary?.workflowCode ?? draftFace?.workflowCode;
|
|
229
236
|
const publishedFace = workflowCode ? await face("workflow.get_published", { schemaCode }) : undefined;
|
|
230
237
|
const draftSummary = draftFace?.workflowSummary;
|
package/dist/script.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { ConfigStore, defaultPaths } from "./config-store.js";
|
|
4
|
+
import { SessionManager } from "./session-manager.js";
|
|
5
|
+
import { makeToolContext } from "./tools.js";
|
|
6
|
+
import { runToolCall } from "./call.js";
|
|
7
|
+
export function parseScript(text) {
|
|
8
|
+
const lines = [];
|
|
9
|
+
const raw = text.split("\n");
|
|
10
|
+
for (const [index, untrimmed] of raw.entries()) {
|
|
11
|
+
const line = untrimmed.trim();
|
|
12
|
+
if (line.length === 0 || line.startsWith("#"))
|
|
13
|
+
continue;
|
|
14
|
+
let parsed;
|
|
15
|
+
try {
|
|
16
|
+
parsed = JSON.parse(line);
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
throw new Error(`第 ${index + 1} 行不是合法 JSON:${error instanceof Error ? error.message : String(error)}`);
|
|
20
|
+
}
|
|
21
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed) || typeof parsed.tool !== "string") {
|
|
22
|
+
throw new Error(`第 ${index + 1} 行必须是 {"tool":"<名称>","args":{...},"env":"<环境>","dryRun":true} 形状(tool 必填)`);
|
|
23
|
+
}
|
|
24
|
+
const entry = parsed;
|
|
25
|
+
if (entry.args !== undefined && (entry.args === null || typeof entry.args !== "object" || Array.isArray(entry.args)))
|
|
26
|
+
throw new Error(`第 ${index + 1} 行 args 必须是 JSON 对象`);
|
|
27
|
+
lines.push({
|
|
28
|
+
tool: entry.tool,
|
|
29
|
+
...(typeof entry.args === "object" && entry.args !== null ? { args: entry.args } : {}),
|
|
30
|
+
...(typeof entry.env === "string" ? { env: entry.env } : {}),
|
|
31
|
+
...(entry.dryRun === true ? { dryRun: true } : {}),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return lines;
|
|
35
|
+
}
|
|
36
|
+
export async function runScript(lines, options = {}) {
|
|
37
|
+
const print = options.print ?? ((line) => console.log(line));
|
|
38
|
+
const context = options.context ?? makeToolContext({ sessions: new SessionManager(new ConfigStore(defaultPaths())) });
|
|
39
|
+
let failed = false;
|
|
40
|
+
for (const [index, line] of lines.entries()) {
|
|
41
|
+
const { exitCode, result } = await runToolCall(line.tool, {
|
|
42
|
+
args: line.args,
|
|
43
|
+
environment: line.env ?? options.defaultEnv,
|
|
44
|
+
dryRun: line.dryRun === true,
|
|
45
|
+
requestId: randomUUID(),
|
|
46
|
+
context,
|
|
47
|
+
print: () => undefined,
|
|
48
|
+
});
|
|
49
|
+
print(JSON.stringify({ index, tool: line.tool, exitCode, result }));
|
|
50
|
+
if (exitCode !== 0) {
|
|
51
|
+
failed = true;
|
|
52
|
+
if (options.continueOnError !== true) {
|
|
53
|
+
print(JSON.stringify({ index, tool: line.tool, stopped: true, note: `第 ${index + 1} 行失败,后续 ${lines.length - index - 1} 行未执行;加 --continue 跑完全部` }));
|
|
54
|
+
return exitCode;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return failed ? 1 : 0;
|
|
59
|
+
}
|
|
60
|
+
export function readScriptInput(path) {
|
|
61
|
+
if (path === "-")
|
|
62
|
+
return readFileSync(0, "utf8");
|
|
63
|
+
return readFileSync(path, "utf8");
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=script.js.map
|
package/dist/support.js
CHANGED
|
@@ -95,4 +95,26 @@ export function flattenTreeRows(rows) {
|
|
|
95
95
|
visit(row);
|
|
96
96
|
return flat;
|
|
97
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* F5 (r10-friction): order-preserving map with BOUNDED concurrency. The read
|
|
100
|
+
* lanes fan out independent wire faces (model.inspect's six faces, field
|
|
101
|
+
* publish verification's detail reads) that used to pay full per-call
|
|
102
|
+
* latency sequentially. The bound keeps the fan-out polite to the small
|
|
103
|
+
* environments (ticket 79 documented DB contention under 30 parallel
|
|
104
|
+
* publishes; reads are lighter but still bounded).
|
|
105
|
+
*/
|
|
106
|
+
export async function mapBounded(items, limit, worker) {
|
|
107
|
+
const results = new Array(items.length);
|
|
108
|
+
let next = 0;
|
|
109
|
+
const runners = Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, async () => {
|
|
110
|
+
while (true) {
|
|
111
|
+
const index = next++;
|
|
112
|
+
if (index >= items.length)
|
|
113
|
+
return;
|
|
114
|
+
results[index] = await worker(items[index], index);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
await Promise.all(runners);
|
|
118
|
+
return results;
|
|
119
|
+
}
|
|
98
120
|
//# sourceMappingURL=support.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stalfh233/omc-cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.19",
|
|
4
4
|
"description": "CloudPivot 8.6 configuration capability CLI — governed, fail-closed, coverage-manifest bounded.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"commander": "14.0.3",
|
|
54
|
-
"omc-core": "0.4.
|
|
54
|
+
"omc-core": "0.4.19",
|
|
55
55
|
"proper-lockfile": "4.1.2",
|
|
56
56
|
"write-file-atomic": "6.0.0",
|
|
57
57
|
"zod": "^3.23.8",
|
package/skills/omc-start-here.md
CHANGED
|
@@ -37,6 +37,7 @@ Run in a **clean project directory** (not the OMC dev repo). Start from the goal
|
|
|
37
37
|
omc next "<目标>" # goal → ordered route (offline)
|
|
38
38
|
omc doctor # environment name and readiness
|
|
39
39
|
omc call discover_environment --args '{}' # 单环境项目(.omc-env 在位)可省 --env;多环境才显式传
|
|
40
|
+
omc script - --env <env> <<< '{"tool":"model.list"}' # 多调链:NDJSON 每行一调,一进程一次会话跑完
|
|
40
41
|
omc capability <selected-id> # only the selected capability
|
|
41
42
|
omc describe <tool> --example <recipe-id> # recipe first; default describe otherwise
|
|
42
43
|
omc help <topic> --section "<heading>" # one methodology section, not all
|