@stalfh233/omc-cli 0.6.16 → 0.6.18
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/batch-call.js +39 -5
- package/dist/capability-overview.js +2 -2
- package/dist/cli.js +25 -13
- package/dist/compute.js +1 -1
- package/dist/field-change.js +7 -1
- package/dist/field-families.js +102 -1
- package/dist/graph.js +142 -24
- package/dist/lanes/app.js +17 -4
- package/dist/lanes/field.js +118 -5
- package/dist/lanes/model-contract.js +1 -1
- package/dist/lanes/model.js +57 -30
- package/dist/lanes/online-js-contract.js +1 -1
- package/dist/lanes/online-js.js +41 -9
- package/dist/lanes/organization.js +9 -2
- package/dist/lanes/programming.js +67 -14
- package/dist/lanes/runtime-data.js +177 -20
- package/dist/lanes/workflow-contract.js +5 -1
- package/dist/lanes/workflow.js +104 -3
- package/dist/mcp-graph.js +45 -15
- package/dist/programming-workflow.js +16 -3
- package/dist/read-shapes.js +3 -3
- package/dist/rollback.js +54 -19
- package/dist/rule-when.js +42 -12
- package/dist/skills.js +1 -1
- package/dist/workflow-support.js +2 -1
- package/dist/write.js +6 -2
- package/package.json +3 -3
package/dist/lanes/model.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { ENDPOINTS } from "../endpoints.js";
|
|
3
|
-
import { assertFieldFamilyAllowed, choicesArg, fieldOptionsArg, nameI18nJson, propertyTypeArg, relationTargetOf, relativeCodeArg, SYSTEM_FIELD_CODES, subFieldArg, } from "../field-families.js";
|
|
3
|
+
import { assertFieldFamilyAllowed, assertSelectionChoicesAllowed, choicesArg, fieldOptionsArg, nameI18nJson, propertyTypeArg, relationTargetOf, relativeCodeArg, SYSTEM_FIELD_CODES, subFieldArg, assertRelationTargetExists, } from "../field-families.js";
|
|
4
|
+
import { modelSchemaCodes } from "./field.js";
|
|
4
5
|
import { fieldChangeWireBody, prepareFieldChange, sameFieldChange } from "../field-change.js";
|
|
5
6
|
import { executeFormDraftWrite, executeFormPublishWrite } from "../form-support.js";
|
|
6
7
|
import { mapBounded } from "../support.js";
|
|
@@ -9,7 +10,8 @@ import { writeSummary } from "../human-summary.js";
|
|
|
9
10
|
import { capabilityArg, environmentArg } from "../args.js";
|
|
10
11
|
import { governedRead, withoutReadbackCache } from "../read.js";
|
|
11
12
|
import { parseToolInput } from "../tool-types.js";
|
|
12
|
-
import { applicationResolutionOf, applicationScopeHint, applicationScopeNotice, flattenTreeRows, resolveApplicationCode
|
|
13
|
+
import { applicationResolutionOf, applicationScopeHint, applicationScopeNotice, flattenTreeRows, resolveApplicationCode } from "../support.js";
|
|
14
|
+
import { resolveValidatedApplicationScope } from "./app.js";
|
|
13
15
|
import { writeTool } from "../write.js";
|
|
14
16
|
import { registerPackageTool } from "../package-tool.js";
|
|
15
17
|
import { defineTool, ToolInputError, ToolRefusalError } from "../tool-types.js";
|
|
@@ -251,28 +253,11 @@ export function registerModelLane(tools) {
|
|
|
251
253
|
capability: capabilityArg,
|
|
252
254
|
},
|
|
253
255
|
handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
|
|
254
|
-
const scope = await
|
|
256
|
+
const scope = await resolveValidatedApplicationScope(session, args.applicationCode);
|
|
255
257
|
const applicationCode = scope.code;
|
|
256
258
|
// r29f F6:显式/配置的 applicationCode 若不在 app.list 里,旧路径会把拼写错误
|
|
257
259
|
// 伪装成「该应用没有模型」。先核对租户应用表,未命中即拒(与 discover 的
|
|
258
260
|
// applicationResolution.status 同构)。
|
|
259
|
-
const availableApps = wireRows((await wireCall(session, { path: ENDPOINTS.applicationList, query: {} })).data)
|
|
260
|
-
.map((row) => String(row.appCode ?? row.code ?? ""))
|
|
261
|
-
.filter((code) => code !== "");
|
|
262
|
-
if (availableApps.length > 0 && !availableApps.includes(applicationCode)) {
|
|
263
|
-
throw new ToolRefusalError({
|
|
264
|
-
status: "refused",
|
|
265
|
-
blocker: "app-not-found",
|
|
266
|
-
applicationCode,
|
|
267
|
-
applicationResolution: {
|
|
268
|
-
status: "unresolved",
|
|
269
|
-
source: scope.source,
|
|
270
|
-
requested: scope.requested ?? applicationCode,
|
|
271
|
-
available: availableApps.slice(0, 40),
|
|
272
|
-
},
|
|
273
|
-
message: `应用「${applicationCode}」不在租户 app.list 里(不是「该应用没有模型」)。可用:${availableApps.slice(0, 12).join("、")}${availableApps.length > 12 ? " 等" : ""};先 app.list 取正确 applicationCode。`,
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
261
|
const tree = flattenTreeRows(wireRows((await wireCall(session, { path: ENDPOINTS.applicationTree, query: { appCode: applicationCode } })).data));
|
|
277
262
|
const all = tree
|
|
278
263
|
.filter((row) => row.type === "BizModel" && typeof row.code === "string" && row.code)
|
|
@@ -361,14 +346,15 @@ export function registerModelLane(tools) {
|
|
|
361
346
|
// 节点本来就带 name,与六面同一批并发读,零额外顺序往返。
|
|
362
347
|
const treeFace = async () => {
|
|
363
348
|
try {
|
|
364
|
-
|
|
349
|
+
const resolved = await resolveModelTreeNode(session, schemaCode);
|
|
350
|
+
return { node: resolved.node, applicationCode: resolved.applicationCode };
|
|
365
351
|
}
|
|
366
352
|
catch {
|
|
367
353
|
return undefined;
|
|
368
354
|
}
|
|
369
355
|
};
|
|
370
356
|
const [fieldFace, formFace, listviewFace, ruleFace, dataruleFace, draftFace, treeRead] = await mapBounded([
|
|
371
|
-
() => face("field.list", { schemaCode, published: true, ...(all ? { all: true } : {}) }, faceErrcodes),
|
|
357
|
+
() => face("field.list", { schemaCode, published: true, ...(all ? { all: true, full: true } : {}) }, faceErrcodes),
|
|
372
358
|
() => face("form.get", { schemaCode }),
|
|
373
359
|
() => face("listview.get", { schemaCode }),
|
|
374
360
|
() => face("rule.list", { schemaCode }),
|
|
@@ -378,10 +364,21 @@ export function registerModelLane(tools) {
|
|
|
378
364
|
], 7, (task) => task());
|
|
379
365
|
const treeNode = treeRead?.node;
|
|
380
366
|
const modelName = typeof treeNode?.name === "string" && treeNode.name ? treeNode.name : typeof treeNode?.text === "string" && treeNode.text ? treeNode.text : undefined;
|
|
381
|
-
|
|
367
|
+
let fieldRows = Array.isArray(fieldFace?.fields) ? fieldFace.fields : [];
|
|
368
|
+
if (all && typeof fieldFace?.detailFile === "string" && !fieldRows.some((field) => field.options !== undefined)) {
|
|
369
|
+
try {
|
|
370
|
+
const { readFileSync } = await import("node:fs");
|
|
371
|
+
const artifact = JSON.parse(readFileSync(fieldFace.detailFile, "utf8"));
|
|
372
|
+
if (Array.isArray(artifact.fields))
|
|
373
|
+
fieldRows = artifact.fields;
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
// Keep the compact field inventory if the nested artifact is unavailable.
|
|
377
|
+
}
|
|
378
|
+
}
|
|
382
379
|
const fields = fieldRows
|
|
383
380
|
.filter((field) => all || !SYSTEM_FIELD_CODES.has(String(field.code)))
|
|
384
|
-
.map((field) => ({ code: field.code, type: field.type, ...(field.typeName !== undefined ? { typeName: field.typeName } : {}), name: field.name, ...(field.conditions ? { conditions: field.conditions } : {}) }));
|
|
381
|
+
.map((field) => ({ code: field.code, type: field.type, ...(field.typeName !== undefined ? { typeName: field.typeName } : {}), name: field.name, ...(field.conditions ? { conditions: field.conditions } : {}), ...(all && field.options !== undefined ? { options: field.options } : {}) }));
|
|
385
382
|
const form = formFace && formFace.present !== undefined
|
|
386
383
|
? { present: formFace.present === true, published: formFace.published === true, controlCount: Array.isArray(formFace.draftControlCodes) ? formFace.draftControlCodes.length : 0 }
|
|
387
384
|
: undefined;
|
|
@@ -454,6 +451,7 @@ export function registerModelLane(tools) {
|
|
|
454
451
|
return {
|
|
455
452
|
code: schemaCode,
|
|
456
453
|
schemaCode,
|
|
454
|
+
...(typeof treeRead?.applicationCode === "string" && treeRead.applicationCode ? { applicationCode: treeRead.applicationCode } : {}),
|
|
457
455
|
...(modelName !== undefined ? { name: modelName } : {}),
|
|
458
456
|
...(modelPresent !== undefined ? { modelPresent } : {}),
|
|
459
457
|
...(modelPresent === false
|
|
@@ -488,10 +486,14 @@ export function registerModelLane(tools) {
|
|
|
488
486
|
return result;
|
|
489
487
|
return {
|
|
490
488
|
schemaCode: result.schemaCode,
|
|
489
|
+
...(result.applicationCode !== undefined ? { applicationCode: result.applicationCode } : {}),
|
|
491
490
|
// r35a-F3:compact 投影必须保留 code(model.list 有 code,inspect 此前
|
|
492
491
|
// 只剩 schemaCode,`--jq '[].code'` 必 pathMiss 白跑一轮)。
|
|
493
492
|
...(result.code !== undefined ? { code: result.code } : {}),
|
|
494
493
|
fieldCodes: (result.fields ?? []).map((field) => field.code),
|
|
494
|
+
...(Array.isArray(result.fields) && result.fields.some((field) => field.options !== undefined)
|
|
495
|
+
? { fields: result.fields }
|
|
496
|
+
: {}),
|
|
495
497
|
form: result.form,
|
|
496
498
|
listview: result.listview ? { code: result.listview.code, columnCount: result.listview.columnCount } : undefined,
|
|
497
499
|
ruleCodes: (result.rules ?? []).map((rule) => rule.code),
|
|
@@ -533,15 +535,17 @@ export function registerModelLane(tools) {
|
|
|
533
535
|
// r29a F6:dry-run 必须做与实跑同样的 duplicate 探测,否则会假绿灯。
|
|
534
536
|
dryRunPayload: modelCreateDryRun,
|
|
535
537
|
execute: async (session, args) => {
|
|
536
|
-
const
|
|
538
|
+
const applicationScope = await resolveValidatedApplicationScope(session, args.applicationCode);
|
|
539
|
+
const applicationCode = applicationScope.code;
|
|
537
540
|
const existing = await resolveModelTreeNode(session, args.code, args.applicationCode);
|
|
538
541
|
if (existing.node) {
|
|
539
542
|
throw new ToolRefusalError({
|
|
540
543
|
status: "refused",
|
|
541
544
|
blocker: "model-create-duplicate",
|
|
542
545
|
schemaCode: args.code,
|
|
543
|
-
|
|
544
|
-
|
|
546
|
+
requestedApplicationCode: applicationCode,
|
|
547
|
+
conflictInApplicationCode: existing.applicationCode,
|
|
548
|
+
message: `模型 code 在租户内全局唯一,跨应用同名即冲突:模型 ${args.code} 已存在于应用 ${existing.applicationCode};create 拒绝重复 POST`,
|
|
545
549
|
});
|
|
546
550
|
}
|
|
547
551
|
// A tree-miss does NOT prove absence: an orphan mount (row exists in
|
|
@@ -1305,6 +1309,7 @@ export function registerModelLane(tools) {
|
|
|
1305
1309
|
.optional()
|
|
1306
1310
|
.describe("也可在本调用内配置默认列表视图(内部走 listview.configure);一次到「模型+字段+表单+列表」"),
|
|
1307
1311
|
applicationCode: z.string().optional(),
|
|
1312
|
+
createOnly: z.boolean().optional().describe("true=模型已存在时拒绝,不执行字段合并"),
|
|
1308
1313
|
},
|
|
1309
1314
|
// CFG-naming/CFG-02 parity: refuse an invalid code before a plan
|
|
1310
1315
|
// is minted (the executor's model.create/field.create would otherwise
|
|
@@ -1317,6 +1322,8 @@ export function registerModelLane(tools) {
|
|
|
1317
1322
|
];
|
|
1318
1323
|
if (violations.length > 0)
|
|
1319
1324
|
throw new ToolRefusalError({ status: "refused", blocker: "business-code-invalid", message: "code 未过出门前校验;按 validation 修正(长度/风格/保留字)", validation: violations });
|
|
1325
|
+
for (const entry of entries)
|
|
1326
|
+
assertSelectionChoicesAllowed(entry);
|
|
1320
1327
|
for (const entry of entries)
|
|
1321
1328
|
prepareFieldChange(fieldChangeInputOf(args.code, entry));
|
|
1322
1329
|
},
|
|
@@ -1356,6 +1363,12 @@ export function registerModelLane(tools) {
|
|
|
1356
1363
|
execute: async (session, args) => {
|
|
1357
1364
|
const results = [];
|
|
1358
1365
|
const entries = fieldEntriesOf(args);
|
|
1366
|
+
// r37 跨路接线(同扇门):field.create 单独调用已对关联字段目标写前拒
|
|
1367
|
+
// (relation-target-not-found),configure_model 的字段包不能是例外——
|
|
1368
|
+
// 否则混合包仍会落到裸 300010 把整个模型毒住。
|
|
1369
|
+
const relationCandidates = await modelSchemaCodes(session);
|
|
1370
|
+
for (const entry of entries)
|
|
1371
|
+
assertRelationTargetExists(entry, relationCandidates);
|
|
1359
1372
|
const stop = (index, tool, error) => {
|
|
1360
1373
|
const envelope = error instanceof ToolRefusalError
|
|
1361
1374
|
? Object.fromEntries(Object.entries(error.payload).filter(([key]) => key !== "status"))
|
|
@@ -1378,7 +1391,8 @@ export function registerModelLane(tools) {
|
|
|
1378
1391
|
return stop(index, tool, error);
|
|
1379
1392
|
}
|
|
1380
1393
|
};
|
|
1381
|
-
const
|
|
1394
|
+
const applicationScope = await resolveValidatedApplicationScope(session, args.applicationCode);
|
|
1395
|
+
const applicationCode = applicationScope.code;
|
|
1382
1396
|
const tree = flattenTreeRows(wireRows((await wireCall(session, { path: ENDPOINTS.applicationTree, query: { appCode: applicationCode } })).data));
|
|
1383
1397
|
const folder = args.folderId ? tree.find((row) => row.id === args.folderId) : tree.find((row) => row.type !== "BizModel" && typeof row.id === "string");
|
|
1384
1398
|
if (!folder?.id)
|
|
@@ -1389,6 +1403,10 @@ export function registerModelLane(tools) {
|
|
|
1389
1403
|
// or form step. The application tree is already in hand, so an existing
|
|
1390
1404
|
// model is detected with no extra call: skip the create and continue.
|
|
1391
1405
|
const existingModel = tree.find((row) => row.type === "BizModel" && row.code === args.code);
|
|
1406
|
+
if (existingModel && args.createOnly === true) {
|
|
1407
|
+
throw new ToolRefusalError({ status: "refused", blocker: "model-already-exists", requestedApplicationCode: applicationCode, conflictInApplicationCode: applicationCode, schemaCode: args.code, message: `模型 code 在租户内全局唯一,跨应用同名即冲突:${args.code} 已存在;createOnly=true 拒绝合并` });
|
|
1408
|
+
}
|
|
1409
|
+
const created = !existingModel;
|
|
1392
1410
|
let failed;
|
|
1393
1411
|
if (existingModel) {
|
|
1394
1412
|
results.push({ step: "model.create", skipped: "model-exists", folderId: String(folder.id), readback: { present: true } });
|
|
@@ -1405,6 +1423,7 @@ export function registerModelLane(tools) {
|
|
|
1405
1423
|
return failed;
|
|
1406
1424
|
}
|
|
1407
1425
|
const existingFields = new Map();
|
|
1426
|
+
const mergedFieldCodes = [];
|
|
1408
1427
|
for (const isPublish of ["false", "true"]) {
|
|
1409
1428
|
for (const row of wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.code, isPublish } })).data))
|
|
1410
1429
|
existingFields.set(String(row.code ?? row.propertyCode ?? ""), row);
|
|
@@ -1415,8 +1434,10 @@ export function registerModelLane(tools) {
|
|
|
1415
1434
|
failed = await run(results.length, "field.create", async () => {
|
|
1416
1435
|
const existing = existingFields.get(entry.code);
|
|
1417
1436
|
if (existing !== undefined) {
|
|
1418
|
-
if (sameFieldChange(existing, input))
|
|
1437
|
+
if (sameFieldChange(existing, input)) {
|
|
1438
|
+
mergedFieldCodes.push(entry.code);
|
|
1419
1439
|
return { step: "field.create", skipped: "field-exists", code: entry.code, readback: { present: true } };
|
|
1440
|
+
}
|
|
1420
1441
|
throw new ToolRefusalError({
|
|
1421
1442
|
status: "refused",
|
|
1422
1443
|
blocker: "field-conflict",
|
|
@@ -1430,6 +1451,7 @@ export function registerModelLane(tools) {
|
|
|
1430
1451
|
const after = wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.code, isPublish: "false" } })).data);
|
|
1431
1452
|
if (!after.some((row) => String(row.code ?? row.propertyCode) === entry.code))
|
|
1432
1453
|
throw new ToolRefusalError({ status: "refused", blocker: "field-create-readback-failed", message: `field.create 后权威草稿字段未读到 ${entry.code}` });
|
|
1454
|
+
mergedFieldCodes.push(entry.code);
|
|
1433
1455
|
return { step: "field.create", errcode: performed.errcode, code: entry.code, readback: { present: true } };
|
|
1434
1456
|
});
|
|
1435
1457
|
if (failed)
|
|
@@ -1482,6 +1504,8 @@ export function registerModelLane(tools) {
|
|
|
1482
1504
|
const rows = wireRows(published.data);
|
|
1483
1505
|
const publishedCodes = rows.map((row) => String(row.code ?? row.propertyCode ?? ""));
|
|
1484
1506
|
const readback = {
|
|
1507
|
+
created,
|
|
1508
|
+
...(created ? {} : { mergedChanges: { fields: mergedFieldCodes, nameIgnored: args.name !== String(existingModel?.name ?? args.name) } }),
|
|
1485
1509
|
publishedCodes,
|
|
1486
1510
|
...(entries.length === 1
|
|
1487
1511
|
? { publishedFieldPresent: publishedCodes.includes(entries[0].code) }
|
|
@@ -1513,8 +1537,10 @@ function fieldEntriesOf(args) {
|
|
|
1513
1537
|
if (Array.isArray(args.fields) && args.fields.length > 0) {
|
|
1514
1538
|
if (args.fieldCode !== undefined || args.fieldName !== undefined || args.propertyType !== undefined)
|
|
1515
1539
|
throw new ToolRefusalError({ status: "refused", blocker: "configure-model-fields-exclusive", message: "fields[] 与单字段参数(fieldCode/fieldName/propertyType)互斥;多字段用 fields[],单字段用旧三元组" });
|
|
1516
|
-
for (const entry of args.fields)
|
|
1540
|
+
for (const entry of args.fields) {
|
|
1541
|
+
assertSelectionChoicesAllowed(entry);
|
|
1517
1542
|
assertFieldFamilyAllowed({ ...entry });
|
|
1543
|
+
}
|
|
1518
1544
|
return args.fields.map((entry) => ({ code: entry.code, name: entry.name, propertyType: entry.propertyType, ...withLegacy(entry) }));
|
|
1519
1545
|
}
|
|
1520
1546
|
if (typeof args.fieldCode !== "string" || typeof args.fieldName !== "string" || typeof args.propertyType !== "string")
|
|
@@ -1524,6 +1550,7 @@ function fieldEntriesOf(args) {
|
|
|
1524
1550
|
message: "configure_model 需要 fields[](多字段)或 fieldCode+fieldName+propertyType(单字段)",
|
|
1525
1551
|
});
|
|
1526
1552
|
const legacy = { code: args.fieldCode, name: args.fieldName, propertyType: args.propertyType, ...withLegacy(args) };
|
|
1553
|
+
assertSelectionChoicesAllowed(legacy);
|
|
1527
1554
|
assertFieldFamilyAllowed(legacy);
|
|
1528
1555
|
return [legacy];
|
|
1529
1556
|
}
|
|
@@ -78,7 +78,7 @@ export const ONLINE_JS_LANE_CONTRACT = Object.freeze({
|
|
|
78
78
|
}),
|
|
79
79
|
"onlinejs.compose": Object.freeze({
|
|
80
80
|
scope: ONLINE_JS_SCOPE,
|
|
81
|
-
description: `**本调用 = 改 JS +
|
|
81
|
+
description: `**本调用 = 改 JS + 立即发布整张表单**(force:true 重推,不需要再 form.publish——它只提拔布局、不碰 JS)。publish:false 只写草稿;remove:true 或 source:"" 移除脚本。Compose the Online JS source as the customScript element into the form's draftHtmlJson layout array (then publish, promoting it verbatim to publishedHtmlJson). Core contract: preserve the platform template and only extend customScript.innerHTML — the six-element skeleton includes the ${PLATFORM_LIFECYCLE_WRAPPER_MARKER} lifecycle wrapper; the style, toolbar, template, beforeCustomScript, customScript, and afterCustomScript elements are kept or synthesized with platform defaults, while every layout/control element is preserved. customScript is rebuilt canonically as {tagName:'script', id:'customScript', innerHTML:<platform lifecycle wrapper containing source>, attrs:[]} because the designer requires an ARRAY attrs (e.attrs.some) and tagName, else the Online JS editor fails with t.some is not a function. Drives bizsheet/update + bizsheet/publish; publishedHtmlJson is read back verbatim and SHA-256-fingerprinted so the caller can verify byte equality. Read the EXISTING script first with onlinejs.get (decide replace vs INCREMENTAL from what is actually stored). Never overwrites the layout with a standalone script object or a partial [customScript]-only array (regression for issue 11's root cause). summary:true answers "did it land?" without any HTML/JS body (sha + byte counts + draftPublishedInSync); the default readback dedupes the composed draft and the script when they equal the published face, so the same script is never returned three times.`,
|
|
82
82
|
}),
|
|
83
83
|
}),
|
|
84
84
|
});
|
package/dist/lanes/online-js.js
CHANGED
|
@@ -77,7 +77,7 @@ export function registerOnlineJsLane(tools) {
|
|
|
77
77
|
return {
|
|
78
78
|
schemaCode: args.schemaCode,
|
|
79
79
|
sheetCode,
|
|
80
|
-
present:
|
|
80
|
+
present: draft.present || published.present,
|
|
81
81
|
sheetPublished: sheet.published === true,
|
|
82
82
|
draft: { ...draft, ...(draft.script === null ? { note: "草稿无 customScript 脚本(尚未 compose 或被清空)" } : {}) },
|
|
83
83
|
published: { ...published, ...(published.script === null ? { note: "发布面无 customScript 脚本" } : {}) },
|
|
@@ -107,13 +107,19 @@ export function registerOnlineJsLane(tools) {
|
|
|
107
107
|
// 的 sheetCode 都是 optional(省略 = schemaCode)。此前只有这里必填,
|
|
108
108
|
// dry-run 与真写都拒——同一个 lane 两套约定,调用方要白撞一轮才知道。
|
|
109
109
|
sheetCode: z.string().min(1).optional().describe("表单槽位码;省略时默认 = schemaCode(与 form.draft/form.publish 同口径)"),
|
|
110
|
-
source: z.string().
|
|
110
|
+
source: z.string().optional().describe("在线 JS 代码。默认 INCREMENTAL:并入平台 (function(form){…}) 模板的既有脚本,模板与既有注册全部保留;配 replace:true 才是整段替换。脚本不得使用 export/import;事件名必须是平台事件(onLoad/onRendered/onValidate/onPreAction/onCustomAction/onActionDone/onPrintFormDataHandle/onPrintWorkflowLogDataHandle)。与 remove:true 二选一;source 为空字符串也表示移除 customScript"),
|
|
111
111
|
replace: z.boolean().optional().describe("true = 以 source 整体替换 customScript(含完整 (function(form){…}) 脚本)。默认 false = 增量并入既有脚本,保留平台模板与既有逻辑"),
|
|
112
|
-
|
|
112
|
+
remove: z.boolean().optional().describe("true = 显式移除 customScript,使 onlinejs.get 返回 present:false;会写入并按 publish 设置发布。与 source 二选一"),
|
|
113
|
+
publish: z.boolean().optional().describe("默认 true:写草稿并推发布面;false = 只写草稿不推发布面"),
|
|
113
114
|
summary: z.boolean().optional().describe("true = 只回同步状态(sha/字节数/是否同步/渲染兼容),不回任何 HTML/JS 原文。确认「改动是否生效」用这个;要看原文用 onlinejs.get"),
|
|
114
115
|
},
|
|
115
116
|
execute: async (session, args) => {
|
|
116
117
|
const shouldPublish = args.publish !== false;
|
|
118
|
+
const removing = args.remove === true || args.source === "";
|
|
119
|
+
if (!removing && args.source === undefined)
|
|
120
|
+
throw new Error("必须提供 source,或显式传 remove:true");
|
|
121
|
+
if (args.remove === true && args.source !== undefined && args.source !== "")
|
|
122
|
+
throw new Error("remove:true 不能与非空 source 同时传入");
|
|
117
123
|
// r28e F9:和同 lane 其它工具一致——省略 sheetCode 即模型码槽位。
|
|
118
124
|
const targetSheetCode = args.sheetCode ?? args.schemaCode;
|
|
119
125
|
// 1. Read the existing draftHtmlJson.
|
|
@@ -133,10 +139,13 @@ export function registerOnlineJsLane(tools) {
|
|
|
133
139
|
// existing script and merge the caller's code into it; the platform
|
|
134
140
|
// default (an empty script) is seeded with the lifecycle scaffold.
|
|
135
141
|
const existingScript = String(existingElements.find((element) => element.id === "customScript")?.innerHTML ?? "");
|
|
136
|
-
const composedScript = composeCustomScript(existingScript, args.source, args.replace === true ? "replace" : "merge");
|
|
142
|
+
const composedScript = removing ? "" : composeCustomScript(existingScript, args.source, args.replace === true ? "replace" : "merge");
|
|
137
143
|
// 3. Preserve the platform template read-modify-write: keep the
|
|
138
144
|
// style/beforeCustomScript/afterCustomScript skeleton and every
|
|
139
145
|
// layout/control element; customScript carries the MERGED script.
|
|
146
|
+
// Keep the platform's required six-element skeleton, but an empty
|
|
147
|
+
// customScript element is semantically absent (onlinejs.get reports
|
|
148
|
+
// present:false) and remains accepted by the renderer contract.
|
|
140
149
|
const composedElements = composeLayoutElements(existingElements, composedScript);
|
|
141
150
|
assertHtmlRender(JSON.stringify(composedElements), { schemaCode: args.schemaCode, sheetCode: targetSheetCode, face: "composedDraftHtmlJson" }, true);
|
|
142
151
|
// 4. Stringify the COMPLETE element array (never partial).
|
|
@@ -176,7 +185,10 @@ export function registerOnlineJsLane(tools) {
|
|
|
176
185
|
const publishedScript = readCustomScriptSource(publishedRaw).script;
|
|
177
186
|
const customScriptSha256 = createHash("sha256").update(composedScript, "utf8").digest("hex");
|
|
178
187
|
const draftPublishedInSync = publishedScript !== null && publishedScript === composedScript;
|
|
179
|
-
const htmlRenderCompat = {
|
|
188
|
+
const htmlRenderCompat = {
|
|
189
|
+
draft: { ...validateHtmlRender(composedDraftHtml, true), note: "仅 HTML 结构不含 JS 运行" },
|
|
190
|
+
published: { ...validateHtmlRender(publishedRaw, true), note: "仅 HTML 结构不含 JS 运行" },
|
|
191
|
+
};
|
|
180
192
|
// Live 2026-10-21 (customer): this readback shipped the same ~10KB script
|
|
181
193
|
// three times (composedDraftHtmlJson + publishedHtmlJson + customScript),
|
|
182
194
|
// ~45KB for a call whose only question was "did publish land?" — the
|
|
@@ -222,13 +234,18 @@ export function registerOnlineJsLane(tools) {
|
|
|
222
234
|
customScriptMatchesPublished: draftPublishedInSync,
|
|
223
235
|
customScriptMerged: args.replace !== true,
|
|
224
236
|
htmlRenderCompat,
|
|
237
|
+
portalVerificationPlan: ["在门户打开该表单", "触发已绑定的在线 JS 事件", "检查事件产生的副作用字段或校验提示"],
|
|
225
238
|
},
|
|
226
239
|
};
|
|
227
240
|
},
|
|
228
241
|
preflight: // Refuse a source that cannot run in the script slot (export/import, unknown
|
|
229
242
|
// form.on event) BEFORE the write permission gate. This is the exact
|
|
230
243
|
// class of mistake that blanked a live form page with no harness signal.
|
|
231
|
-
(args) =>
|
|
244
|
+
(args) => {
|
|
245
|
+
if (args.remove === true || args.source === "" || args.source === undefined)
|
|
246
|
+
return;
|
|
247
|
+
lintOnlineJsSource(args.source);
|
|
248
|
+
},
|
|
232
249
|
dryRunPayload: // r23 F4:dry-run 必须走与真写相同的 compose 校验路径(含 replace/merge 的
|
|
233
250
|
// wrapper 判定),否则 dry-run 过、真写拒——探针按 dry-run 绿灯写正式调用被拒。
|
|
234
251
|
async (session, args) => {
|
|
@@ -238,12 +255,27 @@ export function registerOnlineJsLane(tools) {
|
|
|
238
255
|
const compat = validateHtmlRender(raw);
|
|
239
256
|
const elements = compat.ok ? parseLayoutElements(raw) : [];
|
|
240
257
|
const existingScript = String(elements.find((element) => element.id === "customScript")?.innerHTML ?? "");
|
|
241
|
-
const
|
|
258
|
+
const removing = args.remove === true || args.source === "";
|
|
259
|
+
if (!removing && args.source === undefined)
|
|
260
|
+
throw new Error("必须提供 source,或显式传 remove:true");
|
|
261
|
+
const composed = removing ? "" : composeCustomScript(existingScript, args.source, args.replace === true ? "replace" : "merge");
|
|
262
|
+
const composedBytes = Buffer.byteLength(composed, "utf8");
|
|
263
|
+
const previewSize = 200;
|
|
264
|
+
const composedPreview = composed.length <= previewSize * 2
|
|
265
|
+
? composed
|
|
266
|
+
: `${composed.slice(0, previewSize)}…${composed.slice(-previewSize)}`;
|
|
242
267
|
return {
|
|
243
268
|
mode: args.replace === true ? "replace" : "merge",
|
|
269
|
+
publish: args.publish !== false,
|
|
244
270
|
wouldChange: composed !== existingScript,
|
|
245
271
|
composedSha256: createHash("sha256").update(composed, "utf8").digest("hex"),
|
|
246
|
-
composedBytes
|
|
272
|
+
composedBytes,
|
|
273
|
+
composedPreview,
|
|
274
|
+
composedPreviewHeadBytes: Buffer.byteLength(composed.slice(0, previewSize), "utf8"),
|
|
275
|
+
composedPreviewTailBytes: Buffer.byteLength(composed.slice(-previewSize), "utf8"),
|
|
276
|
+
composedPreviewTotalBytes: composedBytes,
|
|
277
|
+
composedPreviewNote: "预览为头/尾各 200 个字符(短文本不截断);字节数以 UTF-8 提交串对账,未自动补换行或外壳",
|
|
278
|
+
summary: args.publish === false ? "只写草稿,不推发布面" : "写草稿并推发布面",
|
|
247
279
|
hint: "dry-run 已执行与真写一致的 compose 校验;该结果即正式调用将落库的脚本形状",
|
|
248
280
|
};
|
|
249
281
|
},
|
|
@@ -267,7 +299,7 @@ export function registerOnlineJsLane(tools) {
|
|
|
267
299
|
inverse: ({ before, args }) => {
|
|
268
300
|
const source = before?.customScript;
|
|
269
301
|
if (typeof source !== "string")
|
|
270
|
-
return { kind: "
|
|
302
|
+
return { kind: "calls", calls: [{ tool: "onlinejs.compose", args: { schemaCode: String(args.schemaCode), sheetCode: String(args.sheetCode ?? args.schemaCode), remove: true, publish: args.publish } }] };
|
|
271
303
|
return { kind: "calls", calls: [{ tool: "onlinejs.compose", args: { schemaCode: String(args.schemaCode), sheetCode: String(args.sheetCode ?? args.schemaCode), source, replace: true, publish: args.publish } }] };
|
|
272
304
|
},
|
|
273
305
|
},
|
|
@@ -531,10 +531,17 @@ export function registerOrganizationLane(tools) {
|
|
|
531
531
|
remarks: z.string().optional(),
|
|
532
532
|
corpId: z.string().min(1).optional().describe("组织 corpId,默认当前登录企业;传 \"other\" 表示非组织分组角色"),
|
|
533
533
|
},
|
|
534
|
-
|
|
534
|
+
preflight: (args) => {
|
|
535
535
|
if (!args.roleGroupCode && !args.roleGroupId) {
|
|
536
|
-
throw new
|
|
536
|
+
throw new ToolRefusalError({
|
|
537
|
+
status: "refused",
|
|
538
|
+
blocker: "role-group-required",
|
|
539
|
+
nextCommand: "omc call org.role.list --args '{}'",
|
|
540
|
+
message: "必须给 roleGroupCode(推荐)或 roleGroupId;先 org.role.list 取 groups 码表",
|
|
541
|
+
});
|
|
537
542
|
}
|
|
543
|
+
},
|
|
544
|
+
execute: async (session, args) => {
|
|
538
545
|
const { groups } = await readRoleTree(session);
|
|
539
546
|
if (groups.length === 0) {
|
|
540
547
|
throw new ToolRefusalError({ status: "refused", blocker: "wire-shape", message: "org.role.list 未解析出任何角色组;先确认组织树可读,不要盲建角色" });
|
|
@@ -7,6 +7,7 @@ import { writeTool } from "../write.js";
|
|
|
7
7
|
import { defineTool, ToolRefusalError } from "../tool-types.js";
|
|
8
8
|
import { wireCall, WireError } from "../wire.js";
|
|
9
9
|
import { PROGRAMMING_LANE_CONTRACT, PROGRAMMING_WIRE } from "./programming-contract.js";
|
|
10
|
+
import { readModelRecord } from "../model-record.js";
|
|
10
11
|
const LANE = PROGRAMMING_LANE_CONTRACT.lane;
|
|
11
12
|
const TOOLS = PROGRAMMING_LANE_CONTRACT.tools;
|
|
12
13
|
/** metadata.app.CodeType is the NUMERIC index on the wire (8.6.26 source). */
|
|
@@ -60,7 +61,8 @@ async function readSource(session, args) {
|
|
|
60
61
|
const model = asRecord(response.data);
|
|
61
62
|
if (model === null)
|
|
62
63
|
return { present: false, provisioned: false, model: null };
|
|
63
|
-
|
|
64
|
+
// get_source can return a generated template without a persisted source row.
|
|
65
|
+
return { present: typeof model.version === "number", provisioned: typeof model.version === "number", model };
|
|
64
66
|
}
|
|
65
67
|
catch (error) {
|
|
66
68
|
if (error instanceof WireError && error.kind === "business" && /PROGRAMMING_SOURCE_NONEXISTENT|源码不存在|source.*nonexistent/i.test(error.message))
|
|
@@ -102,6 +104,13 @@ function assertTemplateShape(content, code) {
|
|
|
102
104
|
});
|
|
103
105
|
}
|
|
104
106
|
}
|
|
107
|
+
function assertProgrammingName(name) {
|
|
108
|
+
if (name === undefined)
|
|
109
|
+
return;
|
|
110
|
+
if (typeof name !== "string" || !/^[A-Za-z][A-Za-z0-9_ -]{0,127}$/.test(name)) {
|
|
111
|
+
throw new ToolRefusalError({ status: "refused", blocker: "programming-name-invalid", message: "businessRuleName 只能以字母开头,并只含字母、数字、下划线、空格或连字符(最多 128 字符)。" });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
105
114
|
async function listRules(session, schemaCode) {
|
|
106
115
|
const response = await wireCall(session, { path: ENDPOINTS.businessRuleListBySchemaCode, query: { schemaCode } });
|
|
107
116
|
return rowsOf(response.data);
|
|
@@ -223,6 +232,17 @@ function sourceRollback() {
|
|
|
223
232
|
},
|
|
224
233
|
};
|
|
225
234
|
}
|
|
235
|
+
function publishRollback() {
|
|
236
|
+
return {
|
|
237
|
+
target: (args) => `programming.publish:${String(args.schemaCode ?? "")}/${String(args.businessRuleCode ?? "")}`,
|
|
238
|
+
// A published version has no authoritative read face and cannot be
|
|
239
|
+
// restored by replaying the draft. Deliberately fail before-capture so the
|
|
240
|
+
// shared journal records rollbackable:false rather than implying a draft
|
|
241
|
+
// restore would undo the runtime version.
|
|
242
|
+
read: async () => { throw new Error("published version has no rollbackable read face"); },
|
|
243
|
+
inverse: () => ({ kind: "irreversible", reason: "无图形化回退端点;删除规则才会连源码一起删" }),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
226
246
|
export function registerProgrammingLane(tools) {
|
|
227
247
|
tools.push(defineTool({
|
|
228
248
|
name: "programming.schemas",
|
|
@@ -310,12 +330,14 @@ export function registerProgrammingLane(tools) {
|
|
|
310
330
|
},
|
|
311
331
|
handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
|
|
312
332
|
const read = await readSource(session, args);
|
|
333
|
+
const model = await readModelRecord(session, args.schemaCode);
|
|
313
334
|
const content = typeof read.model?.content === "string" ? read.model.content : null;
|
|
314
335
|
return {
|
|
315
336
|
schemaCode: args.schemaCode,
|
|
316
337
|
businessRuleCode: args.businessRuleCode,
|
|
317
338
|
code: String(read.model?.code ?? args.code ?? businessRuleSourceCode(args.schemaCode, args.businessRuleCode)),
|
|
318
339
|
present: read.present,
|
|
340
|
+
modelExists: model !== null,
|
|
319
341
|
provisioned: read.provisioned,
|
|
320
342
|
className: read.model?.className ?? null,
|
|
321
343
|
codeType: read.model?.codeType ?? null,
|
|
@@ -326,13 +348,15 @@ export function registerProgrammingLane(tools) {
|
|
|
326
348
|
contentBytes: content === null ? null : Buffer.byteLength(content, "utf8"),
|
|
327
349
|
// r28j F7: --search "bizObject" 被写危害文档淹没;API 骨架入口钉在这里。
|
|
328
350
|
apiHint: 'BizObject 读写骨架:omc help omc-backend-online-coding --section "5. 典型 handler 骨架"',
|
|
329
|
-
...(
|
|
330
|
-
? {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
351
|
+
...(model === null
|
|
352
|
+
? { hint: `模型 ${args.schemaCode} 不存在;get_source 返回的只是平台模板骨架,不代表源码或模型存在。先用 model.list 核对 schemaCode。` }
|
|
353
|
+
: read.provisioned
|
|
354
|
+
? {
|
|
355
|
+
// Draft face always reports published:false after publish too —
|
|
356
|
+
// do not let callers treat this boolean as the publish verdict (F11).
|
|
357
|
+
publishedNote: "本面 published 来自 version=0 草稿行;发布是否生效请看 programming.publish 的 readback.runtimeEffective / developmentMode+enabled,不要看本字段。",
|
|
358
|
+
}
|
|
359
|
+
: { hint: "未落库草稿:content 是平台按规则类型生成的模板骨架(version:null)。改完后 programming.save 落库,programming.publish 生效。" }),
|
|
336
360
|
};
|
|
337
361
|
}, {
|
|
338
362
|
target: `programming.get:${args.schemaCode}/${args.businessRuleCode}`,
|
|
@@ -442,10 +466,20 @@ export function registerProgrammingLane(tools) {
|
|
|
442
466
|
},
|
|
443
467
|
preflight: (args) => {
|
|
444
468
|
const code = args.code ?? businessRuleSourceCode(args.schemaCode, args.businessRuleCode);
|
|
469
|
+
assertProgrammingName(args.businessRuleName);
|
|
445
470
|
assertTemplateShape(args.content, code);
|
|
446
471
|
},
|
|
447
|
-
dryRunPayload: async (
|
|
472
|
+
dryRunPayload: async (session, args) => {
|
|
448
473
|
const code = args.code ?? businessRuleSourceCode(args.schemaCode, args.businessRuleCode);
|
|
474
|
+
const rule = await ruleOf(session, args.schemaCode, args.businessRuleCode);
|
|
475
|
+
if (!rule) {
|
|
476
|
+
throw new ToolRefusalError({
|
|
477
|
+
status: "refused",
|
|
478
|
+
blocker: "programming-rule-absent",
|
|
479
|
+
message: `业务规则 ${args.schemaCode}/${args.businessRuleCode} 不存在;save 不会凭空创建规则行。先用 rule.save(或 rule.list)确认/创建规则,再保存源码。`,
|
|
480
|
+
nextCommand: `omc call rule.save --env local --args '{"schemaCode":"${args.schemaCode}","ruleCode":"${args.businessRuleCode}"}'`,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
449
483
|
return {
|
|
450
484
|
code,
|
|
451
485
|
contentSha256: sha256(args.content),
|
|
@@ -538,6 +572,8 @@ export function registerProgrammingLane(tools) {
|
|
|
538
572
|
const publishStatusRaw = readbackRule?.publishStatus;
|
|
539
573
|
return {
|
|
540
574
|
performed: { errcode: 0, code, contentSha256: sha256(content), contentBytes: Buffer.byteLength(content, "utf8"), compiled: true },
|
|
575
|
+
rollbackReceipt: { supported: false, reason: "无图形化回退端点;rollback plan 亦标 irreversible" },
|
|
576
|
+
runtimeVerification: { note: "自定义规则无调用响应体读面;验证须写副作用后用 data.load 读取" },
|
|
541
577
|
readback: {
|
|
542
578
|
present: readbackSource.present,
|
|
543
579
|
contentMatches: readbackContent === content,
|
|
@@ -554,19 +590,36 @@ export function registerProgrammingLane(tools) {
|
|
|
554
590
|
// so it cannot contradict runtimeEffective:"published".
|
|
555
591
|
...(readbackSource.model?.published === true ? { publishedFlag: true } : { publishedFlagNote: "get_source.published 读的是 version=0 草稿行,发布后仍常为 false;不是发布成败判据(published 内容无 wire 读端点)" }),
|
|
556
592
|
note: "运行时执行的是 published 最新版;published 内容无 wire 读取端点,回滚靠本地 git + 重新 publish。判发布是否生效:authoritative=developmentMode+enabled(1/CODING + true)。",
|
|
593
|
+
rollback: { supported: false, reason: "无图形化回退端点;删除规则才会连源码一起删" },
|
|
594
|
+
runtimeVerification: { note: "自定义规则无调用响应体读面;须写副作用(如字段赋值)后 data.load 验证" },
|
|
557
595
|
},
|
|
558
596
|
};
|
|
559
597
|
},
|
|
560
598
|
preflight: (args) => {
|
|
561
599
|
const code = args.code ?? businessRuleSourceCode(args.schemaCode, args.businessRuleCode);
|
|
600
|
+
assertProgrammingName(args.businessRuleName);
|
|
562
601
|
if (args.content !== undefined)
|
|
563
602
|
assertTemplateShape(args.content, code);
|
|
564
603
|
},
|
|
565
|
-
dryRunPayload: async (
|
|
566
|
-
code
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
604
|
+
dryRunPayload: async (session, args) => {
|
|
605
|
+
const code = args.code ?? businessRuleSourceCode(args.schemaCode, args.businessRuleCode);
|
|
606
|
+
const rule = await ruleOf(session, args.schemaCode, args.businessRuleCode);
|
|
607
|
+
if (!rule)
|
|
608
|
+
throw new ToolRefusalError({ status: "refused", blocker: "programming-rule-absent", message: `业务规则 ${args.schemaCode}/${args.businessRuleCode} 不存在:publish 必须携带 businessRule 整模型。` });
|
|
609
|
+
let content = args.content;
|
|
610
|
+
if (content === undefined) {
|
|
611
|
+
const read = await readSource(session, { schemaCode: args.schemaCode, businessRuleCode: args.businessRuleCode });
|
|
612
|
+
content = typeof read.model?.content === "string" ? read.model.content : undefined;
|
|
613
|
+
}
|
|
614
|
+
if (content === undefined || content.trim() === "")
|
|
615
|
+
throw new ToolRefusalError({ status: "refused", blocker: "programming-content-required", message: "没有可发布的源码:传 content,或先 programming.save 落库草稿。" });
|
|
616
|
+
assertTemplateShape(content, code);
|
|
617
|
+
const compiled = await compileSource(session, code, content);
|
|
618
|
+
if (!compiled.success)
|
|
619
|
+
throw new ToolRefusalError({ status: "refused", blocker: "programming-compile-failed", message: "编译失败,拒绝 dry-run(真写也会拒绝发布)。", errors: compiled.presented.errors ?? compiled.errors, userErrors: compiled.presented.userErrors, platformErrors: compiled.presented.platformErrors });
|
|
620
|
+
return { code, contentSha256: sha256(content), contentBytes: Buffer.byteLength(content, "utf8"), note: "dry-run 与真写执行相同的规则存在、模板形状、编译 preflight;不会写入或切 CODING。" };
|
|
621
|
+
},
|
|
622
|
+
rollback: publishRollback(),
|
|
570
623
|
});
|
|
571
624
|
}
|
|
572
625
|
export { PROGRAMMING_WIRE };
|