@stalfh233/omc-cli 0.6.12 → 0.6.14
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/coverage/m1-coverage-manifest-v1.json +2 -0
- package/dist/approval-flow.js +84 -3
- package/dist/batch-call.js +32 -2
- package/dist/bench/call-face-eval.js +2 -2
- package/dist/call.js +35 -2
- package/dist/capability-overview.js +21 -7
- package/dist/cli-contract.js +8 -2
- package/dist/cli.js +313 -72
- package/dist/describe-cache.js +8 -4
- package/dist/discovery.js +4 -0
- package/dist/environment-policy.js +20 -0
- package/dist/field-families.js +62 -4
- package/dist/form-support.js +293 -6
- package/dist/goal-routes.js +137 -22
- package/dist/identity.js +63 -11
- package/dist/lanes/app-contract.js +4 -4
- package/dist/lanes/app.js +20 -2
- package/dist/lanes/dictionary-contract.js +16 -5
- package/dist/lanes/dictionary-coverage.js +1 -0
- package/dist/lanes/dictionary.js +200 -8
- package/dist/lanes/environment-contract.js +18 -6
- package/dist/lanes/environment.js +129 -16
- package/dist/lanes/field-contract.js +3 -2
- package/dist/lanes/field.js +785 -63
- package/dist/lanes/form.js +52 -6
- package/dist/lanes/list-view-contract.js +3 -3
- package/dist/lanes/list-view.js +143 -12
- package/dist/lanes/meta.js +36 -10
- package/dist/lanes/model-contract.js +7 -6
- package/dist/lanes/model.js +154 -10
- package/dist/lanes/organization.js +67 -7
- package/dist/lanes/runtime-data-contract.js +4 -2
- package/dist/lanes/runtime-data.js +395 -41
- package/dist/lanes/workflow-contract.js +2 -2
- package/dist/lanes/workflow.js +381 -11
- package/dist/model-scaffold.js +11 -3
- package/dist/path-projection.js +181 -31
- package/dist/read-shapes.js +31 -11
- package/dist/recipes/b2-registration.js +1 -1
- package/dist/recipes/field.js +16 -3
- package/dist/recipes/form.js +4 -3
- package/dist/recipes/runtime-data.js +58 -8
- package/dist/recipes/workflow.js +86 -1
- package/dist/refusal-guidance.js +18 -6
- package/dist/rollback-spec.js +41 -2
- package/dist/rollback.js +49 -11
- package/dist/script.js +72 -12
- package/dist/skills.js +91 -3
- package/dist/tools.js +113 -1
- package/dist/wire.js +13 -1
- package/dist/write-lock.js +92 -4
- package/dist/write.js +129 -16
- package/package.json +3 -3
- package/skills/omc-business-configuration.md +1 -1
- package/skills/omc-capabilities.md +5 -4
- package/skills/omc-playbook.md +41 -0
- package/skills/omc-refusals.md +58 -6
- package/skills/omc-tool-selection.md +2 -0
- package/skills/omc-write-hazards.md +3 -2
package/dist/describe-cache.js
CHANGED
|
@@ -51,9 +51,13 @@ function writeCache(cache) {
|
|
|
51
51
|
/**
|
|
52
52
|
* Return the compact repeat-read face for `tool`, or `undefined` when this is
|
|
53
53
|
* the first read (or the contract changed). Records the new entry as a side
|
|
54
|
-
* effect so the NEXT read
|
|
54
|
+
* effect so the NEXT `--cache-ok` read can be compact.
|
|
55
|
+
*
|
|
56
|
+
* r31b F3: `--full` alone always wants the body. Pass `{ recordOnly:true }` to
|
|
57
|
+
* refresh the cache without short-circuiting; pass nothing / `{cacheOk:true}`
|
|
58
|
+
* (via CLI `--full --cache-ok`) to allow the compact face.
|
|
55
59
|
*/
|
|
56
|
-
export function repeatFullDescribe(tool, result) {
|
|
60
|
+
export function repeatFullDescribe(tool, result, options = {}) {
|
|
57
61
|
// No project session (repo tests, golden master, one-off runs) ⇒ no cache:
|
|
58
62
|
// those callers need byte-stable output, and a cache write would be litter.
|
|
59
63
|
if (!projectSessionActive())
|
|
@@ -63,7 +67,7 @@ export function repeatFullDescribe(tool, result) {
|
|
|
63
67
|
const digest = createHash("sha256").update(body).digest("hex");
|
|
64
68
|
const cache = readCache();
|
|
65
69
|
const previous = cache[tool];
|
|
66
|
-
if (previous && previous.sha256 === digest && Date.now() - previous.at < CACHE_TTL_MS) {
|
|
70
|
+
if (!options.recordOnly && previous && previous.sha256 === digest && Date.now() - previous.at < CACHE_TTL_MS) {
|
|
67
71
|
// A pruned artifact would turn the summary into a dead end; re-ship instead.
|
|
68
72
|
if (existsSync(join(artifactRoot(), `${previous.artifactId}.artifact`))) {
|
|
69
73
|
try {
|
|
@@ -75,7 +79,7 @@ export function repeatFullDescribe(tool, result) {
|
|
|
75
79
|
sha256: digest,
|
|
76
80
|
bytes,
|
|
77
81
|
fetch: `omc artifact get ${previous.artifactId} --summary(再 --keys/--jq 取需要的键)`,
|
|
78
|
-
note: `本项目的会话里已读过同一个契约(${bytes}B,内容 sha256
|
|
82
|
+
note: `本项目的会话里已读过同一个契约(${bytes}B,内容 sha256 未变),本次因 --cache-ok 不再重发全文;要全文请去掉 --cache-ok 再 --full`,
|
|
79
83
|
};
|
|
80
84
|
}
|
|
81
85
|
catch {
|
package/dist/discovery.js
CHANGED
|
@@ -485,6 +485,10 @@ export function describeTool(registry, name) {
|
|
|
485
485
|
// High-frequency read faces: declare the RESULT keys so `--jq` does not
|
|
486
486
|
// have to learn them from a pathMiss. Zero bytes when undeclared.
|
|
487
487
|
...(returnsFace(tool.name) ?? {}),
|
|
488
|
+
// r32 F1(真机):探针并发发两条 app.create,第二条回 write-lock-held。
|
|
489
|
+
// 撞上之前没有任何发现面说过"写是环境级串行"——describe 的 params 里没有 wait
|
|
490
|
+
// (它是 omc call 的 CLI 开关,不在工具 schema),capability notes 是空的。
|
|
491
|
+
...(tool.kind === "write" ? { writeLock: "写锁=环境级:同一环境的并发写,后到的直接回 write-lock-held(不排队)。可带 --wait <ms> 让 CLI 内部排队(CLI 开关,不是本工具参数);不要写 shell 重试循环" } : {}),
|
|
488
492
|
// Keep the cost salient at the decision point: agents reached for --full even
|
|
489
493
|
// when --example/--param existed (sandbox22: 8 --full calls, one 19 KB).
|
|
490
494
|
full: `omc describe ${tool.name} --full(5–20KB;先试 --example/--param)`,
|
|
@@ -23,4 +23,24 @@ export function environmentNotice(profile) {
|
|
|
23
23
|
? "当前是测试环境;已允许写入,仅应使用测试数据。"
|
|
24
24
|
: "当前是测试环境;当前设置为只读,写入会被拒绝。";
|
|
25
25
|
}
|
|
26
|
+
export function versionEvidenceOf(input) {
|
|
27
|
+
const detected = input.detectedVersion;
|
|
28
|
+
if (detected === undefined || detected.length === 0) {
|
|
29
|
+
return {
|
|
30
|
+
detectedVersion: null,
|
|
31
|
+
inAnchorFamily: null,
|
|
32
|
+
versionSource: null,
|
|
33
|
+
anchorVersion: input.anchorVersion,
|
|
34
|
+
versionFamily: input.versionFamily,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
detectedVersion: detected,
|
|
39
|
+
inAnchorFamily: detected.startsWith(`${input.versionFamily}.`),
|
|
40
|
+
versionSource: "system-config",
|
|
41
|
+
anchorVersion: input.anchorVersion,
|
|
42
|
+
versionFamily: input.versionFamily,
|
|
43
|
+
...(input.versionNotice ? { versionNotice: input.versionNotice } : {}),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
26
46
|
//# sourceMappingURL=environment-policy.js.map
|
package/dist/field-families.js
CHANGED
|
@@ -11,8 +11,11 @@ export { FIELD_FAMILY_LIST, FIELD_CONTROL_CODES };
|
|
|
11
11
|
export const propertyTypeArg = z
|
|
12
12
|
.string()
|
|
13
13
|
.min(1)
|
|
14
|
-
.describe(`field family. Accepted: ${FIELD_FAMILY_LIST}, CHILD_TABLE (
|
|
15
|
-
export const relativeCodeArg = z
|
|
14
|
+
.describe(`field family. Accepted: ${FIELD_FAMILY_LIST}, CHILD_TABLE (requires \`subFields\`). **NUMERICAL 必填 options.scale**(0..8)。WORK_SHEET=关联单选;RELEVANCE_DATA=引用关联列;MULT_WORK_SHEET=关联多选。Out of coverage: OCR/ELECTRONIC_SIGN/nested CHILD_TABLE. Selection→\`choices\`(dictionary 用 optionsSet);relation→\`relativeCode\`+\`options\`。`);
|
|
15
|
+
export const relativeCodeArg = z
|
|
16
|
+
.string()
|
|
17
|
+
.optional()
|
|
18
|
+
.describe("relation target model code;WORK_SHEET/MULT_WORK_SHEET 必填,且与 options.schemaCode 相同");
|
|
16
19
|
/**
|
|
17
20
|
* Ticket 59 (P0): the wire `name_i18n` column must be a *parseable JSON object
|
|
18
21
|
* string*, never a bare display string. The admin designer runs an unguarded
|
|
@@ -164,10 +167,13 @@ export const subFieldArg = z
|
|
|
164
167
|
*/
|
|
165
168
|
export const fieldOptionsArg = z
|
|
166
169
|
.object({
|
|
167
|
-
schemaCode: z.string().optional().describe("relation target model code
|
|
170
|
+
schemaCode: z.string().optional().describe("relation target model code(WORK_SHEET/MULT_WORK_SHEET;与 relativeCode 都填且必须相同)"),
|
|
168
171
|
queryCode: z.string().optional().describe("relation query code (usually the target model code)"),
|
|
169
172
|
displayField: z.string().optional().describe("relation display field (usually 'name')"),
|
|
170
|
-
mappings: z
|
|
173
|
+
mappings: z
|
|
174
|
+
.union([z.string(), z.array(z.object({ source: z.string(), target: z.string() }))])
|
|
175
|
+
.optional()
|
|
176
|
+
.describe('relation mappings:[{"source":"<目标字段>","target":"<本模型字段>"}] 或 JSON 串([]/""=无映射)。仅门户选取时回填;data.save/update 不触发;data.load 是快照,上游改名不跟随'),
|
|
171
177
|
searchFormula: jsonString().optional().describe("WORK_SHEET / MULT_WORK_SHEET data restriction range JSON"),
|
|
172
178
|
conditions: jsonString().optional().describe("WORK_SHEET / MULT_WORK_SHEET query conditions JSON"),
|
|
173
179
|
optionsSet: optionsSetArg.optional().describe("selection source: custom, dictionary, or businessModel"),
|
|
@@ -1205,4 +1211,56 @@ export function assertFieldOptionsAllowed(type, options, schemaCode) {
|
|
|
1205
1211
|
refuseFieldOptions("RELEVANCE_DATA referenceFieldControlType is not an integer control code", "rejected: RELEVANCE_DATA(16) referenceFieldControlType must be an integer BizFormPropertyType code");
|
|
1206
1212
|
}
|
|
1207
1213
|
}
|
|
1214
|
+
/**
|
|
1215
|
+
* CHILD_TABLE(8) columns as a compact list (issue 2026-10-25 §7).
|
|
1216
|
+
*
|
|
1217
|
+
* The stored `subSchema` is the full designer payload — system columns included,
|
|
1218
|
+
* ~37.6 KB for a 10-column child table when asked for one field by code. A caller
|
|
1219
|
+
* almost always wants the column codes (to build formData / verify the attach);
|
|
1220
|
+
* the raw payload stays available behind `full:true`. Accepts both the object and
|
|
1221
|
+
* the JSON-string wire form.
|
|
1222
|
+
*
|
|
1223
|
+
* Lives here (not in the field lane) because the form lane needs the same
|
|
1224
|
+
* child-column inventory to answer "which subtable columns is this layout
|
|
1225
|
+
* missing?" (GitLab issue #24).
|
|
1226
|
+
*/
|
|
1227
|
+
export function childColumnsOf(subSchema) {
|
|
1228
|
+
let parsed = subSchema;
|
|
1229
|
+
if (typeof subSchema === "string") {
|
|
1230
|
+
try {
|
|
1231
|
+
parsed = JSON.parse(subSchema);
|
|
1232
|
+
}
|
|
1233
|
+
catch {
|
|
1234
|
+
return [];
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
if (parsed === null || typeof parsed !== "object")
|
|
1238
|
+
return [];
|
|
1239
|
+
const properties = parsed.properties;
|
|
1240
|
+
if (!Array.isArray(properties))
|
|
1241
|
+
return [];
|
|
1242
|
+
return properties
|
|
1243
|
+
.filter((child) => child !== null && typeof child === "object")
|
|
1244
|
+
.map((child) => {
|
|
1245
|
+
const rawType = child.propertyType ?? child.type;
|
|
1246
|
+
const type = rawType === undefined || rawType === null || Number.isNaN(Number(rawType)) ? null : Number(rawType);
|
|
1247
|
+
const typeLabel = type === null ? undefined : fieldTypeLabel(type);
|
|
1248
|
+
const code = String(child.code ?? child.propertyCode ?? "");
|
|
1249
|
+
return {
|
|
1250
|
+
code,
|
|
1251
|
+
name: String(child.name ?? child.code ?? child.propertyCode ?? ""),
|
|
1252
|
+
type,
|
|
1253
|
+
...(typeLabel !== undefined ? { typeName: typeLabel } : {}),
|
|
1254
|
+
...(typeof child.relativeCode === "string" && child.relativeCode ? { relativeCode: child.relativeCode } : {}),
|
|
1255
|
+
// r15 feedback: the projection mixed the platform's system columns with
|
|
1256
|
+
// the authored ones, so "which child columns did I declare?" still needed
|
|
1257
|
+
// a filter. Keep every column but mark the system ones.
|
|
1258
|
+
// `parentId` / `sortKey` are injected by the platform into every child
|
|
1259
|
+
// schema (they are the child row's link + order), so they are system
|
|
1260
|
+
// columns here even though a MAIN model never carries them.
|
|
1261
|
+
...(SYSTEM_FIELD_CODES.has(code) || code === "parentId" || code === "sortKey" ? { system: true } : {}),
|
|
1262
|
+
};
|
|
1263
|
+
})
|
|
1264
|
+
.filter((child) => child.code.length > 0);
|
|
1265
|
+
}
|
|
1208
1266
|
//# sourceMappingURL=field-families.js.map
|
package/dist/form-support.js
CHANGED
|
@@ -2,7 +2,7 @@ import { wireCall, wireRows, WireError } from "./wire.js";
|
|
|
2
2
|
import { ENDPOINTS } from "./endpoints.js";
|
|
3
3
|
import { decodeJsonObjectColumn } from "./json-column.js";
|
|
4
4
|
import { ToolRefusalError } from "./tool-types.js";
|
|
5
|
-
import { FIELD_CONTROL_TYPES, SYSTEM_FIELD_CODES, fieldControlOptions, relationDisplayField, relationMappings, } from "./field-families.js";
|
|
5
|
+
import { FIELD_CONTROL_TYPES, childColumnsOf, SYSTEM_FIELD_CODES, fieldControlOptions, relationDisplayField, relationMappings, } from "./field-families.js";
|
|
6
6
|
import { deriveControlOptions } from "./capability-shape.js";
|
|
7
7
|
import { hardLayoutViolations, validateFormLayoutShape } from "./form-layout.js";
|
|
8
8
|
/**
|
|
@@ -39,6 +39,31 @@ export function propagateRelationOptions(attributesJson, fields) {
|
|
|
39
39
|
return JSON.stringify(attributes);
|
|
40
40
|
}
|
|
41
41
|
export const propagateRelationMappings = propagateRelationOptions;
|
|
42
|
+
/**
|
|
43
|
+
* Parse-only JavaScript check for a `<script>` element body.
|
|
44
|
+
*
|
|
45
|
+
* `new Function(source)` compiles without running it, so a SyntaxError here means
|
|
46
|
+
* the browser would fail the same way — except the browser fails at RENDER time,
|
|
47
|
+
* with the whole form script dead and no signal in any read face.
|
|
48
|
+
*
|
|
49
|
+
* Only `SyntaxError` is reported. Anything else (runtime `ReferenceError`, a
|
|
50
|
+
* `form` that is undefined at parse time) is not a shape problem and must not
|
|
51
|
+
* block a legitimate compose.
|
|
52
|
+
*/
|
|
53
|
+
function scriptSyntaxProblem(source) {
|
|
54
|
+
try {
|
|
55
|
+
// Function body context: the platform wraps customScript in
|
|
56
|
+
// `(function(form){ … })`, so top-level `return`/`await` semantics differ.
|
|
57
|
+
// Compiling as a *function body* matches how it actually runs.
|
|
58
|
+
new Function(source);
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (error instanceof SyntaxError)
|
|
63
|
+
return error.message;
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
42
67
|
/** Validate the JSON shape consumed by the Online JS HTML renderer. */
|
|
43
68
|
export function validateHtmlRender(value, requirePlatformSkeleton = false) {
|
|
44
69
|
if (value === undefined || value === null)
|
|
@@ -70,6 +95,16 @@ export function validateHtmlRender(value, requirePlatformSkeleton = false) {
|
|
|
70
95
|
violations.push({ index, problem: "tagName missing or attrs not an array" });
|
|
71
96
|
if (typeof record.id === "string")
|
|
72
97
|
ids.add(record.id);
|
|
98
|
+
// 2026-11-05(r30b F14):`<script>` 元素的正文**从来没被检查过**,于是
|
|
99
|
+
// `onlinejs.compose` 写进一段 `var x = ;` 也三连绿(写入 ok / 发布 ok /
|
|
100
|
+
// htmlRenderCompat ok)——门户一渲染就 SyntaxError,整个表单脚本全死。
|
|
101
|
+
// 这里只做**最便宜的语法检查**(`new Function` 只解析、不执行),
|
|
102
|
+
// 不引入依赖、不请求网络;运行期错误(如 form 未定义)不归它管。
|
|
103
|
+
if (record.tagName === "script" && typeof record.innerHTML === "string" && record.innerHTML.trim() !== "") {
|
|
104
|
+
const syntaxProblem = scriptSyntaxProblem(record.innerHTML);
|
|
105
|
+
if (syntaxProblem !== undefined)
|
|
106
|
+
violations.push({ index, problem: `${String(record.id ?? "script")} JavaScript 语法错误:${syntaxProblem}` });
|
|
107
|
+
}
|
|
73
108
|
});
|
|
74
109
|
if (requirePlatformSkeleton) {
|
|
75
110
|
for (const id of ["toolbar", "template", "beforeCustomScript", "customScript", "afterCustomScript"]) {
|
|
@@ -335,6 +370,135 @@ function childTableColumns(parentCode, children) {
|
|
|
335
370
|
options: fieldControlOptions(child.type, child.name, child.relativeCode, child.mappings, child.displayField ?? "name", child.fieldOptions),
|
|
336
371
|
}));
|
|
337
372
|
}
|
|
373
|
+
/**
|
|
374
|
+
* GitLab issue #24(2026-11-05,Windows 8.6.44 实测):给**已有手工布局**的模型
|
|
375
|
+
* 加子表列后,`field.publish` 成功、模型里也有这一列,但表单的 Sheet201 控件
|
|
376
|
+
* `columns` 里没有它 → 业务页面看不到。旧路径只有两条路:手工读
|
|
377
|
+
* `publishedAttributesJson`(26KB+)改 JSON 再写回,或整表 `rebuild`(丢掉所有
|
|
378
|
+
* 手工布局与控件表达式)。这里补第三条:**只往已有的 Sheet201 控件里追加缺失列**,
|
|
379
|
+
* 其余控件与顺序一律不动。
|
|
380
|
+
*
|
|
381
|
+
* 列的形状与 scaffold 共用 `childTableColumns`(`{type,width,key,parentKey,options}`),
|
|
382
|
+
* 因为平台渲染与 `bizsheet/publish` 的校验只认这一种形状(field-contract 的
|
|
383
|
+
* `childTableFormControl` 就是按它钉的)。
|
|
384
|
+
*/
|
|
385
|
+
export async function insertChildColumnsIntoLayout(session, schemaCode, attributesJson, requests) {
|
|
386
|
+
let attributes;
|
|
387
|
+
try {
|
|
388
|
+
attributes = JSON.parse(attributesJson);
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
throw new ToolRefusalError({
|
|
392
|
+
status: "refused",
|
|
393
|
+
blocker: "form-layout-invalid",
|
|
394
|
+
schemaCode,
|
|
395
|
+
message: "已有表单布局的 draftAttributesJson 不是合法 JSON 对象,无法增量插入子表列;先按 `omc call form.get` 的原文修复布局",
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
const added = [];
|
|
399
|
+
const alreadyBound = [];
|
|
400
|
+
const unknownParents = [];
|
|
401
|
+
const unknownColumns = [];
|
|
402
|
+
for (const request of requests) {
|
|
403
|
+
const control = attributes[request.parent];
|
|
404
|
+
const columns = control && Array.isArray(control.columns) ? control.columns : undefined;
|
|
405
|
+
if (!control || columns === undefined) {
|
|
406
|
+
unknownParents.push(request.parent);
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
const bound = new Set(columns.map((column) => String(column.key ?? "")).filter((key) => key.length > 0));
|
|
410
|
+
// 列的字段元数据在**父模型下的子表 schema**(子表 schemaCode = 父字段 code)。
|
|
411
|
+
const rows = wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: request.parent, isPublish: "true" } })).data);
|
|
412
|
+
for (const code of request.fieldCodes) {
|
|
413
|
+
if (bound.has(code)) {
|
|
414
|
+
alreadyBound.push({ parent: request.parent, code });
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
const row = rows.find((candidate) => String(candidate.code ?? candidate.propertyCode ?? "") === code);
|
|
418
|
+
if (!row) {
|
|
419
|
+
unknownColumns.push({ parent: request.parent, code });
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
const type = Number(row.type ?? row.propertyType);
|
|
423
|
+
const relativeCode = row.relativeCode ? String(row.relativeCode) : undefined;
|
|
424
|
+
let displayField;
|
|
425
|
+
if (relativeCode && (type === 9 || type === 11)) {
|
|
426
|
+
displayField = relationDisplayField(row.options);
|
|
427
|
+
if (!displayField) {
|
|
428
|
+
const targetRows = wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: relativeCode, isPublish: "true" } })).data);
|
|
429
|
+
const title = targetRows.find((target) => String(target.code ?? target.propertyCode) === "name" || target.name === "数据标题");
|
|
430
|
+
displayField = title ? String(title.code ?? title.propertyCode) : "name";
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
columns.push(...childTableColumns(request.parent, [{
|
|
434
|
+
code,
|
|
435
|
+
name: String(row.name ?? code),
|
|
436
|
+
type: Number.isFinite(type) ? type : 1,
|
|
437
|
+
...(relativeCode !== undefined ? { relativeCode } : {}),
|
|
438
|
+
...(row.mappings !== undefined ? { mappings: String(row.mappings) } : {}),
|
|
439
|
+
...(displayField !== undefined ? { displayField } : {}),
|
|
440
|
+
fieldOptions: decodeJsonObjectColumn(row.options, { invalid: "throw", array: "accept", object: "accept" }),
|
|
441
|
+
}]));
|
|
442
|
+
bound.add(code);
|
|
443
|
+
added.push({ parent: request.parent, code });
|
|
444
|
+
}
|
|
445
|
+
control.columns = columns;
|
|
446
|
+
}
|
|
447
|
+
return { attributesJson: JSON.stringify(attributes), added, alreadyBound, unknownParents, unknownColumns };
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* True when `provided` differs from `stored` ONLY by appended child-table
|
|
451
|
+
* columns inside existing Sheet201 controls. This is the one edit that arrives
|
|
452
|
+
* from the platform's own layout (issue #24): everything else is byte-identical,
|
|
453
|
+
* so the caller-shape gate would otherwise force a full rebuild (losing manual
|
|
454
|
+
* layout + control expressions) or hand-editing 26KB of JSON.
|
|
455
|
+
*/
|
|
456
|
+
export function attributesOnlyAddedChildColumns(provided, stored) {
|
|
457
|
+
let next;
|
|
458
|
+
let prev;
|
|
459
|
+
try {
|
|
460
|
+
next = JSON.parse(provided);
|
|
461
|
+
prev = JSON.parse(stored);
|
|
462
|
+
}
|
|
463
|
+
catch {
|
|
464
|
+
return false;
|
|
465
|
+
}
|
|
466
|
+
if (next === null || prev === null || typeof next !== "object" || typeof prev !== "object")
|
|
467
|
+
return false;
|
|
468
|
+
if (Array.isArray(next) || Array.isArray(prev))
|
|
469
|
+
return false;
|
|
470
|
+
const nextKeys = Object.keys(next).sort();
|
|
471
|
+
const prevKeys = Object.keys(prev).sort();
|
|
472
|
+
if (nextKeys.length !== prevKeys.length || nextKeys.some((key, index) => key !== prevKeys[index]))
|
|
473
|
+
return false;
|
|
474
|
+
for (const key of prevKeys) {
|
|
475
|
+
const nextControl = next[key];
|
|
476
|
+
const prevControl = prev[key];
|
|
477
|
+
if (nextControl === null || prevControl === null || typeof nextControl !== "object" || typeof prevControl !== "object") {
|
|
478
|
+
if (JSON.stringify(nextControl) !== JSON.stringify(prevControl))
|
|
479
|
+
return false;
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
const { columns: nextColumns, ...nextRest } = nextControl;
|
|
483
|
+
const { columns: prevColumns, ...prevRest } = prevControl;
|
|
484
|
+
if (JSON.stringify(nextRest) !== JSON.stringify(prevRest))
|
|
485
|
+
return false;
|
|
486
|
+
if (prevColumns === undefined) {
|
|
487
|
+
if (nextColumns !== undefined)
|
|
488
|
+
return false;
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
if (!Array.isArray(nextColumns) || !Array.isArray(prevColumns))
|
|
492
|
+
return false;
|
|
493
|
+
if (nextColumns.length < prevColumns.length)
|
|
494
|
+
return false;
|
|
495
|
+
for (let index = 0; index < prevColumns.length; index += 1) {
|
|
496
|
+
if (JSON.stringify(nextColumns[index]) !== JSON.stringify(prevColumns[index]))
|
|
497
|
+
return false;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return true;
|
|
501
|
+
}
|
|
338
502
|
/**
|
|
339
503
|
* Keep the designer's non-structural control edits across a rebuild. The
|
|
340
504
|
* freshly scaffolded control owns its label and relation target keys (they must
|
|
@@ -879,6 +1043,30 @@ export function readFormButtons(options) {
|
|
|
879
1043
|
}
|
|
880
1044
|
}
|
|
881
1045
|
export async function executeFormDraftWrite(session, args) {
|
|
1046
|
+
// 兼容别名:`fieldCodes:["items.category"]` 等价于
|
|
1047
|
+
// `childColumns:[{parent:"items",fieldCodes:["category"]}]`(issue #24)。
|
|
1048
|
+
// 点号形式是使用者最自然的写法——子表列在父模型里本来就该这样指代。
|
|
1049
|
+
if (args.childColumns === undefined) {
|
|
1050
|
+
const dotted = (args.fieldCodes ?? []).filter((code) => code.includes("."));
|
|
1051
|
+
if (dotted.length > 0) {
|
|
1052
|
+
const grouped = new Map();
|
|
1053
|
+
for (const entry of dotted) {
|
|
1054
|
+
const [parent, ...rest] = entry.split(".");
|
|
1055
|
+
const child = rest.join(".");
|
|
1056
|
+
if (!parent || !child)
|
|
1057
|
+
continue;
|
|
1058
|
+
grouped.set(parent, [...(grouped.get(parent) ?? []), child]);
|
|
1059
|
+
}
|
|
1060
|
+
if (grouped.size > 0) {
|
|
1061
|
+
args = {
|
|
1062
|
+
...args,
|
|
1063
|
+
fieldCodes: (args.fieldCodes ?? []).filter((code) => !code.includes(".")),
|
|
1064
|
+
childColumns: [...grouped.entries()].map(([parent, fieldCodes]) => ({ parent, fieldCodes })),
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
const requestedChildColumns = args.childColumns && args.childColumns.length > 0 ? args.childColumns : undefined;
|
|
882
1070
|
if (args.draftHtmlJson !== undefined) {
|
|
883
1071
|
assertHtmlRender(args.draftHtmlJson, { schemaCode: args.schemaCode, sheetCode: args.sheetCode, face: "draftHtmlJson" });
|
|
884
1072
|
}
|
|
@@ -909,18 +1097,47 @@ export async function executeFormDraftWrite(session, args) {
|
|
|
909
1097
|
// BEFORE creating anything so an unknown code never writes a form row.
|
|
910
1098
|
const wanted = args.fieldCodes;
|
|
911
1099
|
let selectionUnknown;
|
|
1100
|
+
let childColumnOwners;
|
|
912
1101
|
if (wanted && args.draftAttributesJson === undefined) {
|
|
913
1102
|
const rows = wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "true" } })).data);
|
|
914
1103
|
const publishedCodes = new Set(rows.map((row) => String(row.code ?? row.propertyCode ?? "")));
|
|
915
1104
|
selectionUnknown = wanted.filter((code) => !publishedCodes.has(code));
|
|
1105
|
+
// issue #24:子表列不在父模型发布面上,但它是真实存在的已发布字段——
|
|
1106
|
+
// 旧文案只会说「先 field.create + field.publish」,而用户明明已经做了。
|
|
1107
|
+
if (selectionUnknown.length > 0) {
|
|
1108
|
+
childColumnOwners = new Map();
|
|
1109
|
+
for (const row of rows) {
|
|
1110
|
+
if (Number(row.type ?? row.propertyType) !== 8)
|
|
1111
|
+
continue;
|
|
1112
|
+
const parent = String(row.code ?? row.propertyCode ?? "");
|
|
1113
|
+
for (const column of childColumnsOf(row.subSchema)) {
|
|
1114
|
+
if (column.system === true)
|
|
1115
|
+
continue;
|
|
1116
|
+
childColumnOwners.set(column.code, [...(childColumnOwners.get(column.code) ?? []), parent]);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
916
1120
|
}
|
|
917
1121
|
if (selectionUnknown && selectionUnknown.length > 0) {
|
|
1122
|
+
const asChildColumns = selectionUnknown
|
|
1123
|
+
.map((code) => ({ code, parents: childColumnOwners?.get(code) ?? [] }))
|
|
1124
|
+
.filter((entry) => entry.parents.length > 0);
|
|
918
1125
|
throw new ToolRefusalError({
|
|
919
1126
|
status: "refused",
|
|
920
1127
|
blocker: "form-fieldcodes-unknown",
|
|
921
1128
|
schemaCode: args.schemaCode,
|
|
922
1129
|
fieldCodes: selectionUnknown,
|
|
923
|
-
|
|
1130
|
+
...(asChildColumns.length > 0
|
|
1131
|
+
? {
|
|
1132
|
+
childColumns: asChildColumns.map((entry) => ({ code: entry.code, parent: entry.parents[0] })),
|
|
1133
|
+
nextCommand: `omc call configure_form --args '${JSON.stringify({ schemaCode: args.schemaCode, childColumns: asChildColumns.map((entry) => ({ parent: entry.parents[0], fieldCodes: [entry.code] })) })}'`,
|
|
1134
|
+
}
|
|
1135
|
+
: {}),
|
|
1136
|
+
message: asChildColumns.length === selectionUnknown.length
|
|
1137
|
+
? `${selectionUnknown.join("、")} 是**子表列**,不在模型 ${args.schemaCode} 的顶层发布面——子表列用 childColumns(或 fieldCodes 写「父字段.子列」,如 ${asChildColumns[0].parents[0]}.${asChildColumns[0].code})绑进对应 Sheet201 控件的 columns;纯 fieldCodes 只描述顶层控件`
|
|
1138
|
+
: asChildColumns.length > 0
|
|
1139
|
+
? `${selectionUnknown.join("、")} 里有子表列(${asChildColumns.map((entry) => `${entry.parents[0]}.${entry.code}`).join("、")}),子表列用 childColumns / 「父字段.子列」;其余未知 code 先 field.create + field.publish`
|
|
1140
|
+
: `fieldCodes 引用了模型 ${args.schemaCode} 发布面上不存在的字段:${selectionUnknown.join("、")};先 field.create + field.publish 这些字段,或修正 fieldCodes`,
|
|
924
1141
|
});
|
|
925
1142
|
}
|
|
926
1143
|
// The default form may not exist yet on a freshly created model
|
|
@@ -934,6 +1151,46 @@ export async function executeFormDraftWrite(session, args) {
|
|
|
934
1151
|
if (pre === null)
|
|
935
1152
|
throw new WireError("business", `form ${args.sheetCode} was created but is not readable`, 302024);
|
|
936
1153
|
}
|
|
1154
|
+
// issue #24:把子表列追加进**已有**布局(Sheet201 控件的 columns),不重建整表。
|
|
1155
|
+
// 没有已存布局时退回 scaffold(rebuild 会按 subSchema 生成全部子表列,等价)。
|
|
1156
|
+
let childColumnInsert;
|
|
1157
|
+
if (requestedChildColumns !== undefined && args.draftAttributesJson === undefined && args.rebuild !== true) {
|
|
1158
|
+
const storedAttributes = typeof pre.draftAttributesJson === "string" && pre.draftAttributesJson.trim() !== "" && pre.draftAttributesJson.trim() !== "{}"
|
|
1159
|
+
? pre.draftAttributesJson
|
|
1160
|
+
: typeof pre.publishedAttributesJson === "string" && pre.publishedAttributesJson.trim() !== "" && pre.publishedAttributesJson.trim() !== "{}"
|
|
1161
|
+
? pre.publishedAttributesJson
|
|
1162
|
+
: undefined;
|
|
1163
|
+
if (storedAttributes === undefined) {
|
|
1164
|
+
args = { ...args, rebuild: true };
|
|
1165
|
+
}
|
|
1166
|
+
else {
|
|
1167
|
+
childColumnInsert = await insertChildColumnsIntoLayout(session, args.schemaCode, storedAttributes, requestedChildColumns);
|
|
1168
|
+
if (childColumnInsert.unknownParents.length > 0 || childColumnInsert.unknownColumns.length > 0) {
|
|
1169
|
+
throw new ToolRefusalError({
|
|
1170
|
+
status: "refused",
|
|
1171
|
+
blocker: "form-child-columns-unknown",
|
|
1172
|
+
schemaCode: args.schemaCode,
|
|
1173
|
+
...(childColumnInsert.unknownParents.length > 0 ? { unknownParents: childColumnInsert.unknownParents } : {}),
|
|
1174
|
+
...(childColumnInsert.unknownColumns.length > 0 ? { unknownColumns: childColumnInsert.unknownColumns } : {}),
|
|
1175
|
+
message: (childColumnInsert.unknownParents.length > 0
|
|
1176
|
+
? `父控件 ${childColumnInsert.unknownParents.join("、")} 不在表单布局里(不是子表控件):先用 fieldCodes 绑定父字段,或核对控件 code。`
|
|
1177
|
+
: "")
|
|
1178
|
+
+ (childColumnInsert.unknownColumns.length > 0
|
|
1179
|
+
? `子表发布面没有这些列:${childColumnInsert.unknownColumns.map((entry) => `${entry.parent}.${entry.code}`).join("、")};先在子表上 field.create 再 field.publish 父模型。`
|
|
1180
|
+
: ""),
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
const storedView = typeof pre.draftViewJson === "string" ? pre.draftViewJson : typeof pre.publishedViewJson === "string" ? pre.publishedViewJson : undefined;
|
|
1184
|
+
const storedHtml = typeof pre.draftHtmlJson === "string" ? pre.draftHtmlJson : undefined;
|
|
1185
|
+
args = {
|
|
1186
|
+
...args,
|
|
1187
|
+
...(storedView !== undefined ? { draftViewJson: storedView } : {}),
|
|
1188
|
+
draftAttributesJson: childColumnInsert.attributesJson,
|
|
1189
|
+
...(storedHtml !== undefined ? { draftHtmlJson: storedHtml } : {}),
|
|
1190
|
+
amendChildColumns: true,
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
937
1194
|
//
|
|
938
1195
|
// 1.3 of issue 2026-10-23: the platform's OWN readback can carry shapes this
|
|
939
1196
|
// gate rejects (`child_column_in_rows` when a published layout lists child
|
|
@@ -948,14 +1205,38 @@ export async function executeFormDraftWrite(session, args) {
|
|
|
948
1205
|
const same = (provided, stored) => typeof provided === "string" && typeof stored === "string" && provided.trim() === stored.trim();
|
|
949
1206
|
const viewRoundTrip = same(args.draftViewJson, pre2?.publishedViewJson) || same(args.draftViewJson, pre2?.draftViewJson);
|
|
950
1207
|
const attributesRoundTrip = same(args.draftAttributesJson, pre2?.publishedAttributesJson) || same(args.draftAttributesJson, pre2?.draftAttributesJson);
|
|
951
|
-
|
|
1208
|
+
// issue #24:本次 attributes 由工具自己从已存布局**只追加了子表列**得来。
|
|
1209
|
+
// 它既不是逐字回写(所以要过形状门),也不该按调用方手写布局严判(平台
|
|
1210
|
+
// 自己的布局本来就可能带门不接受的形态)。唯一允许的差异 = 既有 Sheet201
|
|
1211
|
+
// 控件 columns 尾部追加。
|
|
1212
|
+
const amendedChildColumns = childColumnInsert !== undefined &&
|
|
1213
|
+
typeof args.draftAttributesJson === "string" &&
|
|
1214
|
+
(typeof pre2?.publishedAttributesJson === "string" && attributesOnlyAddedChildColumns(args.draftAttributesJson, pre2.publishedAttributesJson)
|
|
1215
|
+
|| typeof pre2?.draftAttributesJson === "string" && attributesOnlyAddedChildColumns(args.draftAttributesJson, pre2.draftAttributesJson));
|
|
1216
|
+
const allProvidedAreRoundTrip = (args.draftViewJson === undefined || viewRoundTrip) && (args.draftAttributesJson === undefined || attributesRoundTrip || amendedChildColumns);
|
|
952
1217
|
if (!allProvidedAreRoundTrip && args.fromReadback === true) {
|
|
1218
|
+
// GitLab issue #24 附带坑 2:`fromReadback` 的语义是「**逐字**回写未改动的
|
|
1219
|
+
// 原文」,不是「我读过原文」。所有「读 → 改一列 → 写回」都会撞在这里,
|
|
1220
|
+
// 而旧文案只说「去掉 fromReadback」,没给可直接执行的下一条命令。
|
|
1221
|
+
const onlyChildColumnsDiffers = typeof args.draftAttributesJson === "string" &&
|
|
1222
|
+
typeof pre2?.publishedAttributesJson === "string" &&
|
|
1223
|
+
attributesOnlyAddedChildColumns(args.draftAttributesJson, pre2.publishedAttributesJson);
|
|
953
1224
|
throw new ToolRefusalError({
|
|
954
1225
|
status: "refused",
|
|
955
1226
|
blocker: "form-layout-invalid",
|
|
956
1227
|
schemaCode: args.schemaCode,
|
|
957
1228
|
sheetCode: args.sheetCode,
|
|
958
|
-
|
|
1229
|
+
reason: "fromReadback 只接受**逐字**未改动的原文回写;读过之后改过一个字符就不再是回读",
|
|
1230
|
+
...(onlyChildColumnsDiffers
|
|
1231
|
+
? {
|
|
1232
|
+
detectedChange: "child-columns-appended",
|
|
1233
|
+
message: "fromReadback 只接受逐字未改动的原文(它是「回写」不是「我读过」)。本次比已存布局多出的内容**只是子表列**——加子表列不必手动改 26KB 的属性 JSON,用 configure_form 的 childColumns 增量绑定即可(不动其它控件与布局)。",
|
|
1234
|
+
nextCommand: `omc call configure_form --args '${JSON.stringify({ schemaCode: args.schemaCode, childColumns: [{ parent: "<父字段>", fieldCodes: ["<子列>"] }] })}'`,
|
|
1235
|
+
}
|
|
1236
|
+
: {
|
|
1237
|
+
message: "fromReadback 只接受逐字未改动的原文(它是「回写」不是「我读过」)。你改过布局 → 按普通写面校验:同一条命令去掉 fromReadback 即可;若仍被拒,按 validation 修正后再写。",
|
|
1238
|
+
nextCommand: `omc call form.draft --args '<同一份参数,去掉 \"fromReadback\">'`,
|
|
1239
|
+
}),
|
|
959
1240
|
validation: formLayoutViolations(args),
|
|
960
1241
|
});
|
|
961
1242
|
}
|
|
@@ -1300,8 +1581,14 @@ export async function executeFormPublishWrite(session, args) {
|
|
|
1300
1581
|
(Array.isArray(args.removeButtonCodes) && args.removeButtonCodes.length > 0) ||
|
|
1301
1582
|
(Array.isArray(args.buttonScripts) && args.buttonScripts.length > 0);
|
|
1302
1583
|
const forcePromote = args.force === true || args.rebuild === true || hasButtonDelta;
|
|
1303
|
-
|
|
1304
|
-
|
|
1584
|
+
// issue #24:旧短路只看「已发布」标记,于是「改了草稿 → form.publish」被静默跳过,
|
|
1585
|
+
// 回执还是 `skipped:"already-published" + published:true`(读起来像成功),实际
|
|
1586
|
+
// 发布面一字未变——页面继续看不到新控件。只有当**草稿与发布面逐字相同**时才跳过。
|
|
1587
|
+
const draftEqualsPublished = typeof pre.draftViewJson === "string" && pre.draftViewJson === pre.publishedViewJson &&
|
|
1588
|
+
typeof pre.draftAttributesJson === "string" && pre.draftAttributesJson === pre.publishedAttributesJson &&
|
|
1589
|
+
(pre.draftHtmlJson === undefined || pre.draftHtmlJson === pre.publishedHtmlJson);
|
|
1590
|
+
if (pre.published === true && draftEqualsPublished && !forcePromote)
|
|
1591
|
+
return { performed: { skipped: "already-published" }, readback: { published: true, draftMatchesPublished: true } };
|
|
1305
1592
|
if (!hasDraft(pre))
|
|
1306
1593
|
throw new WireError("business", "form publish requires a saved draft (draftViewJson/draftAttributesJson)", 302020);
|
|
1307
1594
|
if (pre.draftHtmlJson !== "[]")
|