@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/lanes/field.js
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { ENDPOINTS } from "../endpoints.js";
|
|
3
3
|
import { fieldChangeWireBody, fieldUpdateWireBody, prepareFieldChange, prepareFieldUpdate } from "../field-change.js";
|
|
4
|
-
import { aggregationSelfSchemaMessage, aggregationTargetsOwnSchema, choicesArg, FIELD_PROPERTY_TYPES, fieldOptionsArg, fieldTypeLabel, numericalScaleWarning, propertyTypeArg, relativeCodeArg, stripAggregationPrefix, subFieldArg, SYSTEM_FIELD_CODES, } from "../field-families.js";
|
|
4
|
+
import { aggregationSelfSchemaMessage, aggregationTargetsOwnSchema, choicesArg, childColumnsOf, FIELD_PROPERTY_TYPES, fieldOptionsArg, fieldTypeLabel, numericalScaleWarning, propertyTypeArg, relativeCodeArg, stripAggregationPrefix, subFieldArg, SYSTEM_FIELD_CODES, } from "../field-families.js";
|
|
5
5
|
import { capabilityArg, environmentArg } from "../args.js";
|
|
6
6
|
import { governedRead } from "../read.js";
|
|
7
7
|
import { writeTool } from "../write.js";
|
|
8
|
-
import { decodeJsonColumn, decodeJsonObjectColumn, JSON_COLUMN_PASSTHROUGH } from "../json-column.js";
|
|
8
|
+
import { decodeJsonColumn, decodeJsonObjectColumn, JSON_COLUMN_NULL_COALESCE, JSON_COLUMN_PASSTHROUGH } from "../json-column.js";
|
|
9
9
|
import { syncFormConditionDataRules } from "./datarule.js";
|
|
10
10
|
import { FORM_CONDITION_OPTION_KEYS } from "./datarule-protocol.js";
|
|
11
11
|
import { defineTool, ToolRefusalError } from "../tool-types.js";
|
|
12
12
|
import { wireCall, wireRows, WireError } from "../wire.js";
|
|
13
|
-
import { executeFormDraftWrite, executeFormPublishWrite } from "../form-support.js";
|
|
13
|
+
import { executeFormDraftWrite, executeFormPublishWrite, readFormOrNull } from "../form-support.js";
|
|
14
14
|
import { FIELD_LANE_CONTRACT } from "./field-contract.js";
|
|
15
15
|
import { mapBounded } from "../support.js";
|
|
16
|
+
import { extractBizObjectPage } from "./runtime-data.js";
|
|
16
17
|
const FIELD_LANE = FIELD_LANE_CONTRACT.lane;
|
|
17
18
|
const FIELD_TOOLS = FIELD_LANE_CONTRACT.tools;
|
|
18
19
|
async function readFieldRollback(session, args) {
|
|
@@ -45,54 +46,6 @@ function fieldRollbackSpec(toolName) {
|
|
|
45
46
|
function parseFieldOptionsForReadback(options) {
|
|
46
47
|
return decodeJsonColumn(options, JSON_COLUMN_PASSTHROUGH);
|
|
47
48
|
}
|
|
48
|
-
/**
|
|
49
|
-
* CHILD_TABLE(8) columns as a compact list (issue 2026-10-25 §7).
|
|
50
|
-
*
|
|
51
|
-
* The stored `subSchema` is the full designer payload — system columns included,
|
|
52
|
-
* ~37.6 KB for a 10-column child table when asked for one field by code. A caller
|
|
53
|
-
* almost always wants the column codes (to build formData / verify the attach);
|
|
54
|
-
* the raw payload stays available behind `full:true`. Accepts both the object and
|
|
55
|
-
* the JSON-string wire form.
|
|
56
|
-
*/
|
|
57
|
-
function childColumnsOf(subSchema) {
|
|
58
|
-
let parsed = subSchema;
|
|
59
|
-
if (typeof subSchema === "string") {
|
|
60
|
-
try {
|
|
61
|
-
parsed = JSON.parse(subSchema);
|
|
62
|
-
}
|
|
63
|
-
catch {
|
|
64
|
-
return [];
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
if (parsed === null || typeof parsed !== "object")
|
|
68
|
-
return [];
|
|
69
|
-
const properties = parsed.properties;
|
|
70
|
-
if (!Array.isArray(properties))
|
|
71
|
-
return [];
|
|
72
|
-
return properties
|
|
73
|
-
.filter((child) => child !== null && typeof child === "object")
|
|
74
|
-
.map((child) => {
|
|
75
|
-
const rawType = child.propertyType ?? child.type;
|
|
76
|
-
const type = rawType === undefined || rawType === null || Number.isNaN(Number(rawType)) ? null : Number(rawType);
|
|
77
|
-
const typeLabel = type === null ? undefined : fieldTypeLabel(type);
|
|
78
|
-
const code = String(child.code ?? child.propertyCode ?? "");
|
|
79
|
-
return {
|
|
80
|
-
code,
|
|
81
|
-
name: String(child.name ?? child.code ?? child.propertyCode ?? ""),
|
|
82
|
-
type,
|
|
83
|
-
...(typeLabel !== undefined ? { typeName: typeLabel } : {}),
|
|
84
|
-
...(typeof child.relativeCode === "string" && child.relativeCode ? { relativeCode: child.relativeCode } : {}),
|
|
85
|
-
// r15 feedback: the projection mixed the platform's system columns with
|
|
86
|
-
// the authored ones, so "which child columns did I declare?" still needed
|
|
87
|
-
// a filter. Keep every column but mark the system ones.
|
|
88
|
-
// `parentId` / `sortKey` are injected by the platform into every child
|
|
89
|
-
// schema (they are the child row's link + order), so they are system
|
|
90
|
-
// columns here even though a MAIN model never carries them.
|
|
91
|
-
...(SYSTEM_FIELD_CODES.has(code) || code === "parentId" || code === "sortKey" ? { system: true } : {}),
|
|
92
|
-
};
|
|
93
|
-
})
|
|
94
|
-
.filter((child) => child.code.length > 0);
|
|
95
|
-
}
|
|
96
49
|
/**
|
|
97
50
|
* `name_i18n` is a JSON locale map. When every locale value equals the display
|
|
98
51
|
* `name` there is no translation to convey, so echoing the map on every field of
|
|
@@ -244,7 +197,7 @@ export function registerFieldLane(tools) {
|
|
|
244
197
|
lane: FIELD_LANE,
|
|
245
198
|
scope: FIELD_TOOLS["field.list"].scope,
|
|
246
199
|
description: FIELD_TOOLS["field.list"].description,
|
|
247
|
-
inputSchema: { environment: environmentArg, schemaCode: z.string(), published: z.boolean().default(false), code: z.string().optional(), fields: z.array(z.string()).optional(), full: z.boolean().optional().describe("行内联完整 options/subSchema;默认清单面只有 code/name/type/typeName(+conditions 信号/shapeWarning),验证 options 用 code/fields 选择器或 full:true"), all: z.boolean().optional().describe("默认只返业务字段(过滤 SYSTEM_FIELD_CODES 的 13 个平台字段:id/name/sequenceNo/createdTime/creater/sequenceStatus/…);true 连系统字段一起返"), capability: capabilityArg },
|
|
200
|
+
inputSchema: { environment: environmentArg, schemaCode: z.string(), published: z.boolean().default(false), code: z.string().optional(), fields: z.array(z.string()).optional(), full: z.boolean().optional().describe("行内联完整 options/subSchema;默认清单面只有 code/name/type/typeName(+conditions 信号/shapeWarning),验证 options 用 code/fields 选择器或 full:true;平台读面带回 propertyEmpty 时一并输出(不回则省略,不塞 false)"), all: z.boolean().optional().describe("默认只返业务字段(过滤 SYSTEM_FIELD_CODES 的 13 个平台字段:id/name/sequenceNo/createdTime/creater/sequenceStatus/…);true 连系统字段一起返"), capability: capabilityArg },
|
|
248
201
|
handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
|
|
249
202
|
const rows = wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: args.published ? "true" : "false" } })).data);
|
|
250
203
|
const mapped = rows.map((row) => {
|
|
@@ -277,6 +230,11 @@ export function registerFieldLane(tools) {
|
|
|
277
230
|
: {}),
|
|
278
231
|
...(row.relativeCode ? { relativeCode: row.relativeCode } : {}),
|
|
279
232
|
...(typeof row.relativePropertyCode === "string" && row.relativePropertyCode ? { relativePropertyCode: row.relativePropertyCode } : {}),
|
|
233
|
+
// r32:propertyEmpty 只在平台读面**确实**带回该值时才输出——缺键不给
|
|
234
|
+
// 每个字段塞 false(假读回 + 噪音)。真机 8.6.26 的 bizproperty/list
|
|
235
|
+
// 不带这个键,于是不会出现;write-only 的确认走 field.update 的
|
|
236
|
+
// readback.committed。
|
|
237
|
+
...(Object.prototype.hasOwnProperty.call(row, "propertyEmpty") ? { propertyEmpty: row.propertyEmpty } : {}),
|
|
280
238
|
...(options !== undefined ? { options } : {}),
|
|
281
239
|
...(scaleWarning !== undefined ? { shapeWarning: scaleWarning } : {}),
|
|
282
240
|
};
|
|
@@ -474,6 +432,7 @@ export function registerFieldLane(tools) {
|
|
|
474
432
|
return {
|
|
475
433
|
performed: { errcode: performed.errcode },
|
|
476
434
|
readback: {
|
|
435
|
+
face: "draft",
|
|
477
436
|
draftFieldPresent: rows.some((row) => (row.code ?? row.propertyCode) === args.code),
|
|
478
437
|
...(textLike
|
|
479
438
|
? {
|
|
@@ -500,7 +459,11 @@ export function registerFieldLane(tools) {
|
|
|
500
459
|
choices: choicesArg,
|
|
501
460
|
relativeCode: relativeCodeArg,
|
|
502
461
|
subFields: subFieldArg,
|
|
503
|
-
propertyEmpty: z.boolean().optional().describe("覆盖数据模型层的不可为空声明;传 false
|
|
462
|
+
propertyEmpty: z.boolean().optional().describe("覆盖数据模型层的不可为空声明;传 false 可把历史误建的字段改回可为空(必填/只读应在表单与流程节点配置)。readback.committed 列出本次实际提交到线缆的键,readback.propertyEmpty 只在草稿面回读到该值时才出现(r32 真机:bizproperty/list 行不带 propertyEmpty,读不到即不编造,用 committed 确认本次提交)"),
|
|
463
|
+
syncDictionary: z
|
|
464
|
+
.boolean()
|
|
465
|
+
.optional()
|
|
466
|
+
.describe("true 且字段是字典-backed:服务端重读 dictionary 启用记录并重填 options.optionsSet.dictionary.useDictionariesData(字典改动不会被动传播到字段快照)"),
|
|
504
467
|
cascadeRepublish: z
|
|
505
468
|
.boolean()
|
|
506
469
|
.optional()
|
|
@@ -513,6 +476,7 @@ export function registerFieldLane(tools) {
|
|
|
513
476
|
return runFieldUpdateCascade(session, args, outcome);
|
|
514
477
|
},
|
|
515
478
|
preflight: (args) => assertNameI18nArg(args.name_i18n),
|
|
479
|
+
dryRunPayload: fieldUpdateDryRun,
|
|
516
480
|
rollback: fieldRollbackSpec("field.update"),
|
|
517
481
|
});
|
|
518
482
|
writeTool({
|
|
@@ -536,6 +500,9 @@ export function registerFieldLane(tools) {
|
|
|
536
500
|
description: FIELD_TOOLS["field.delete"].description,
|
|
537
501
|
inputSchema: { schemaCode: z.string().min(1), code: z.string().min(1) },
|
|
538
502
|
execute: async (session, args) => {
|
|
503
|
+
// 真写回执只给摘要(省查询成本);全量引用/行数扫描留给 --dry-run。
|
|
504
|
+
// impact 放进 readback:governedWrite 只透传 performed/readback/steps。
|
|
505
|
+
const impact = await collectFieldDeleteImpact(session, args, { full: false });
|
|
539
506
|
const performed = await wireCall(session, {
|
|
540
507
|
path: ENDPOINTS.fieldForceDelete,
|
|
541
508
|
method: "POST",
|
|
@@ -544,11 +511,452 @@ export function registerFieldLane(tools) {
|
|
|
544
511
|
const draft = await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "false" } });
|
|
545
512
|
const published = await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "true" } });
|
|
546
513
|
const gone = (value) => !wireRows(value).some((row) => (row.code ?? row.propertyCode) === args.code);
|
|
547
|
-
return {
|
|
514
|
+
return {
|
|
515
|
+
performed: { errcode: performed.errcode },
|
|
516
|
+
readback: { absentDraft: gone(draft.data), absentPublished: gone(published.data), impact },
|
|
517
|
+
};
|
|
518
|
+
},
|
|
519
|
+
dryRunPayload: async (session, args) => {
|
|
520
|
+
const impact = await collectFieldDeleteImpact(session, args, { full: true });
|
|
521
|
+
return { wouldChange: true, impact, schemaCode: args.schemaCode, code: args.code };
|
|
548
522
|
},
|
|
549
523
|
rollback: fieldRollbackSpec("field.delete"),
|
|
550
524
|
});
|
|
551
525
|
}
|
|
526
|
+
/** Normalize options JSON (string or object) into a plain object for comparison/merge. */
|
|
527
|
+
function optionsObjectOf(raw) {
|
|
528
|
+
if (raw === null || raw === undefined)
|
|
529
|
+
return {};
|
|
530
|
+
if (typeof raw === "string") {
|
|
531
|
+
const trimmed = raw.trim();
|
|
532
|
+
if (!trimmed)
|
|
533
|
+
return {};
|
|
534
|
+
try {
|
|
535
|
+
const parsed = JSON.parse(trimmed);
|
|
536
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
537
|
+
}
|
|
538
|
+
catch {
|
|
539
|
+
return {};
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
if (typeof raw === "object" && !Array.isArray(raw))
|
|
543
|
+
return raw;
|
|
544
|
+
return {};
|
|
545
|
+
}
|
|
546
|
+
/** Stable fingerprint for draft↔published options equality (r29e F10/F11). */
|
|
547
|
+
function optionsFingerprint(raw) {
|
|
548
|
+
const options = optionsObjectOf(raw);
|
|
549
|
+
// 去掉平台自动垫的空值键(如 dictionaryData:""),避免 SHORT_TEXT 因
|
|
550
|
+
// `{dictionaryData:""}` vs `{}` 假差异而拒绝 already-published 短路(ticket 81)。
|
|
551
|
+
// 字典/关联真正有内容的键仍参与比对。
|
|
552
|
+
const meaningful = {};
|
|
553
|
+
for (const [key, value] of Object.entries(options)) {
|
|
554
|
+
if (value === "" || value === null || value === undefined)
|
|
555
|
+
continue;
|
|
556
|
+
if (Array.isArray(value) && value.length === 0)
|
|
557
|
+
continue;
|
|
558
|
+
if (typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0)
|
|
559
|
+
continue;
|
|
560
|
+
meaningful[key] = value;
|
|
561
|
+
}
|
|
562
|
+
if (Object.keys(meaningful).length === 0)
|
|
563
|
+
return "";
|
|
564
|
+
try {
|
|
565
|
+
return JSON.stringify(meaningful);
|
|
566
|
+
}
|
|
567
|
+
catch {
|
|
568
|
+
return String(raw ?? "");
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
function dictionaryBindingOf(options) {
|
|
572
|
+
const optionsSet = options.optionsSet;
|
|
573
|
+
if (!optionsSet || optionsSet.optionsType !== "dictionary")
|
|
574
|
+
return undefined;
|
|
575
|
+
const dictionary = (optionsSet.dictionary ?? {});
|
|
576
|
+
const use = Array.isArray(dictionary.useDictionariesData)
|
|
577
|
+
? dictionary.useDictionariesData.map((row) => ({
|
|
578
|
+
code: String(row.code ?? ""),
|
|
579
|
+
...(row.name !== undefined ? { name: String(row.name) } : {}),
|
|
580
|
+
...(row.id !== undefined ? { id: String(row.id) } : {}),
|
|
581
|
+
}))
|
|
582
|
+
: [];
|
|
583
|
+
return {
|
|
584
|
+
...(typeof dictionary.checkedDictionary === "string" ? { checkedDictionary: dictionary.checkedDictionary } : {}),
|
|
585
|
+
...(typeof dictionary.checkedDictionaryCode === "string" ? { checkedDictionaryCode: dictionary.checkedDictionaryCode } : {}),
|
|
586
|
+
useDictionariesData: use,
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
async function loadEnabledDictionaryRecords(session, binding) {
|
|
590
|
+
const dicId = binding.checkedDictionary;
|
|
591
|
+
const code = binding.checkedDictionaryCode;
|
|
592
|
+
let id = dicId ?? "";
|
|
593
|
+
if (!id && code) {
|
|
594
|
+
const header = (await wireCall(session, { path: ENDPOINTS.dictionaryGetByCode, query: { code } })).data;
|
|
595
|
+
id = String(header?.id ?? "");
|
|
596
|
+
}
|
|
597
|
+
if (!id)
|
|
598
|
+
return [];
|
|
599
|
+
const data = await wireCall(session, { path: ENDPOINTS.dictionaryEnabledRecords, query: { dicId: id } });
|
|
600
|
+
const rows = Array.isArray(data.data) ? data.data : [];
|
|
601
|
+
return rows
|
|
602
|
+
.map((row) => ({ id: String(row.id ?? ""), code: String(row.code ?? ""), name: String(row.name ?? row.code ?? "") }))
|
|
603
|
+
.filter((row) => row.code.length > 0)
|
|
604
|
+
.sort((left, right) => left.code.localeCompare(right.code));
|
|
605
|
+
}
|
|
606
|
+
/** Keys that change the stored data shape (not just display labels). */
|
|
607
|
+
const DATA_SHAPE_OPTION_KEYS = new Set(["scale", "propertyType", "propertyEmpty", "choices"]);
|
|
608
|
+
function fieldValuesEqual(left, right) {
|
|
609
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
610
|
+
}
|
|
611
|
+
function choiceCodesOf(options) {
|
|
612
|
+
const set = options.optionsSet;
|
|
613
|
+
if (!set || typeof set !== "object" || Array.isArray(set))
|
|
614
|
+
return undefined;
|
|
615
|
+
const custom = set.custom;
|
|
616
|
+
if (!Array.isArray(custom))
|
|
617
|
+
return undefined;
|
|
618
|
+
return custom
|
|
619
|
+
.map((entry) => {
|
|
620
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
621
|
+
return "";
|
|
622
|
+
return String(entry.code ?? entry.value ?? "");
|
|
623
|
+
})
|
|
624
|
+
.filter((code) => code.length > 0);
|
|
625
|
+
}
|
|
626
|
+
async function loadFieldRowForUpdate(session, schemaCode, code) {
|
|
627
|
+
const listed = await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode, isPublish: "false" } });
|
|
628
|
+
let rows = wireRows(listed.data);
|
|
629
|
+
if (!rows.some((row) => (row.code ?? row.propertyCode) === code)) {
|
|
630
|
+
const published = await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode, isPublish: "true" } });
|
|
631
|
+
rows = wireRows(published.data);
|
|
632
|
+
}
|
|
633
|
+
const record = rows.find((row) => (row.code ?? row.propertyCode) === code);
|
|
634
|
+
if (!record)
|
|
635
|
+
throw new WireError("business", `field ${code} not found on ${schemaCode}`, 301001);
|
|
636
|
+
return record;
|
|
637
|
+
}
|
|
638
|
+
async function countModelDataRows(session, schemaCode) {
|
|
639
|
+
try {
|
|
640
|
+
const result = await wireCall(session, {
|
|
641
|
+
path: ENDPOINTS.runtimeQueryList,
|
|
642
|
+
method: "POST",
|
|
643
|
+
body: { schemaCode, queryCode: schemaCode, filters: [], mobile: false, page: 0, size: 1, queryVersion: 1, showTotal: true },
|
|
644
|
+
});
|
|
645
|
+
return { dataRows: extractBizObjectPage(result.data).total };
|
|
646
|
+
}
|
|
647
|
+
catch (error) {
|
|
648
|
+
return {
|
|
649
|
+
dataRows: "unknown",
|
|
650
|
+
note: `拿不到行数:${error instanceof Error ? error.message : String(error)}`,
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Session-aware dry-run for field.update: structured from→to changes (only real
|
|
656
|
+
* diffs), plus an honest dataImpact note when storage-shape keys move.
|
|
657
|
+
*/
|
|
658
|
+
async function fieldUpdateDryRun(session, args) {
|
|
659
|
+
const record = await loadFieldRowForUpdate(session, args.schemaCode, args.code);
|
|
660
|
+
if (args.options !== undefined) {
|
|
661
|
+
const publishedRows = await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "true" } })
|
|
662
|
+
.then((response) => wireRows(response.data))
|
|
663
|
+
.catch(() => []);
|
|
664
|
+
assertNoSelfSchemaAggregation({ schemaCode: args.schemaCode, propertyType: record.type ?? record.propertyType, options: args.options }, publishedRows);
|
|
665
|
+
await assertNoChildSchemaAggregation(session, args.schemaCode, record.type ?? record.propertyType, args.options);
|
|
666
|
+
}
|
|
667
|
+
const targetVersion = session.detectedVersion();
|
|
668
|
+
const update = {
|
|
669
|
+
schemaCode: args.schemaCode,
|
|
670
|
+
code: args.code,
|
|
671
|
+
base: record,
|
|
672
|
+
...(targetVersion ? { targetVersion } : {}),
|
|
673
|
+
...(args.name !== undefined ? { name: args.name } : {}),
|
|
674
|
+
...(args.name_i18n !== undefined ? { name_i18n: args.name_i18n } : {}),
|
|
675
|
+
...(args.options !== undefined ? { options: args.options } : {}),
|
|
676
|
+
...(args.choices !== undefined ? { choices: args.choices } : {}),
|
|
677
|
+
...(args.relativeCode !== undefined ? { relativeCode: args.relativeCode } : {}),
|
|
678
|
+
...(args.subFields !== undefined ? { subFields: args.subFields } : {}),
|
|
679
|
+
...(args.propertyEmpty !== undefined ? { propertyEmpty: args.propertyEmpty } : {}),
|
|
680
|
+
};
|
|
681
|
+
const prepared = prepareFieldUpdate(update);
|
|
682
|
+
const currentOptions = decodeJsonObjectColumn(record.options, { invalid: "throw", array: "accept", object: "accept" }) ?? {};
|
|
683
|
+
const changes = [];
|
|
684
|
+
const pushChange = (field, from, to) => {
|
|
685
|
+
if (fieldValuesEqual(from, to))
|
|
686
|
+
return;
|
|
687
|
+
changes.push({ field, from, to });
|
|
688
|
+
};
|
|
689
|
+
if (args.name !== undefined)
|
|
690
|
+
pushChange("name", record.name ?? null, prepared.name ?? args.name);
|
|
691
|
+
if (args.name_i18n !== undefined) {
|
|
692
|
+
const fromI18n = typeof record.name_i18n === "string" ? record.name_i18n : record.name_i18n ?? null;
|
|
693
|
+
pushChange("name_i18n", fromI18n, prepared.nameI18n ?? args.name_i18n);
|
|
694
|
+
}
|
|
695
|
+
if (args.propertyEmpty !== undefined)
|
|
696
|
+
pushChange("propertyEmpty", record.propertyEmpty ?? null, args.propertyEmpty);
|
|
697
|
+
if (args.relativeCode !== undefined)
|
|
698
|
+
pushChange("relativeCode", record.relativeCode ?? null, args.relativeCode);
|
|
699
|
+
if (args.options !== undefined || args.choices !== undefined) {
|
|
700
|
+
const optionKeys = new Set([...Object.keys(currentOptions), ...Object.keys(prepared.options)]);
|
|
701
|
+
for (const key of [...optionKeys].sort()) {
|
|
702
|
+
if (key === "optionsSet" && args.choices !== undefined)
|
|
703
|
+
continue;
|
|
704
|
+
pushChange(`options.${key}`, currentOptions[key] ?? null, prepared.options[key] ?? null);
|
|
705
|
+
}
|
|
706
|
+
if (args.choices !== undefined) {
|
|
707
|
+
const fromChoices = choiceCodesOf(currentOptions) ?? [];
|
|
708
|
+
pushChange("choices", fromChoices, [...args.choices]);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
const shapeTouched = changes.some((change) => {
|
|
712
|
+
if (change.field === "propertyEmpty" || change.field === "choices")
|
|
713
|
+
return true;
|
|
714
|
+
if (change.field === "options.scale" || change.field === "options.propertyType" || change.field === "options.propertyEmpty")
|
|
715
|
+
return true;
|
|
716
|
+
if (change.field.startsWith("options.") && DATA_SHAPE_OPTION_KEYS.has(change.field.slice("options.".length)))
|
|
717
|
+
return true;
|
|
718
|
+
return false;
|
|
719
|
+
});
|
|
720
|
+
let dataImpact;
|
|
721
|
+
if (shapeTouched) {
|
|
722
|
+
const counted = await countModelDataRows(session, args.schemaCode);
|
|
723
|
+
const shapeFields = changes
|
|
724
|
+
.filter((change) => change.field === "propertyEmpty" || change.field === "choices" || change.field === "options.scale" || change.field === "options.propertyType" || change.field === "options.propertyEmpty" || (change.field.startsWith("options.") && DATA_SHAPE_OPTION_KEYS.has(change.field.slice("options.".length))))
|
|
725
|
+
.map((change) => change.field);
|
|
726
|
+
dataImpact = {
|
|
727
|
+
...(counted.dataRows === "unknown"
|
|
728
|
+
? { dataRows: "unknown", note: counted.note ?? "拿不到行数:data.list total 不可用" }
|
|
729
|
+
: {
|
|
730
|
+
dataRows: counted.dataRows,
|
|
731
|
+
note: `触及数据形状键(${shapeFields.join("、")});现有 ${counted.dataRows} 条数据不会被本工具自动重写,请自行评估显示/校验影响`,
|
|
732
|
+
}),
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
return {
|
|
736
|
+
wouldChange: changes.length > 0,
|
|
737
|
+
changes,
|
|
738
|
+
...(dataImpact !== undefined ? { dataImpact } : {}),
|
|
739
|
+
schemaCode: args.schemaCode,
|
|
740
|
+
code: args.code,
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
function formControlCodes(face) {
|
|
744
|
+
if (typeof face !== "string")
|
|
745
|
+
return [];
|
|
746
|
+
try {
|
|
747
|
+
const parsed = JSON.parse(face);
|
|
748
|
+
return Object.keys(parsed);
|
|
749
|
+
}
|
|
750
|
+
catch {
|
|
751
|
+
return [];
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
function scriptMentionsField(script, code) {
|
|
755
|
+
if (!script)
|
|
756
|
+
return false;
|
|
757
|
+
const escaped = code.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
758
|
+
return new RegExp(`(?:^|[^A-Za-z0-9_])${escaped}(?:[^A-Za-z0-9_]|$)`).test(script);
|
|
759
|
+
}
|
|
760
|
+
async function scanFieldRuleReferences(session, schemaCode, code) {
|
|
761
|
+
const references = [];
|
|
762
|
+
let rows = [];
|
|
763
|
+
try {
|
|
764
|
+
rows = wireRows((await wireCall(session, { path: ENDPOINTS.businessRuleListBySchemaCode, query: { schemaCode } })).data);
|
|
765
|
+
}
|
|
766
|
+
catch (error) {
|
|
767
|
+
return { references, unknown: `业务规则列表不可用:${error instanceof Error ? error.message : String(error)}` };
|
|
768
|
+
}
|
|
769
|
+
for (const row of rows) {
|
|
770
|
+
const ruleCode = String(row.code ?? "");
|
|
771
|
+
const ruleId = row.id;
|
|
772
|
+
if (typeof ruleId !== "string" || ruleId === "")
|
|
773
|
+
continue;
|
|
774
|
+
let record = null;
|
|
775
|
+
try {
|
|
776
|
+
record = (await wireCall(session, { path: ENDPOINTS.businessRuleGet, query: { id: ruleId } })).data;
|
|
777
|
+
}
|
|
778
|
+
catch {
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
if (!record)
|
|
782
|
+
continue;
|
|
783
|
+
const nodes = decodeJsonColumn(record.node, JSON_COLUMN_NULL_COALESCE);
|
|
784
|
+
if (!Array.isArray(nodes))
|
|
785
|
+
continue;
|
|
786
|
+
for (const rawNode of nodes) {
|
|
787
|
+
const nodeCode = String(rawNode.nodeCode ?? "");
|
|
788
|
+
const nodeType = String(rawNode.nodeType ?? "");
|
|
789
|
+
const mapping = rawNode.methodMapping;
|
|
790
|
+
let via;
|
|
791
|
+
if (["CREATE", "UPDATE", "DELETE"].includes(nodeType) && Array.isArray(rawNode.dataActions)
|
|
792
|
+
&& rawNode.dataActions.some((action) => String(action.targetDataItem ?? "") === code)) {
|
|
793
|
+
via = "dataActions.targetDataItem";
|
|
794
|
+
}
|
|
795
|
+
if (!via && mapping) {
|
|
796
|
+
for (const [key, label] of [["inputMappings", "inputMappings.bizCode"], ["outputMappings", "outputMappings.bizCode"]]) {
|
|
797
|
+
if (Array.isArray(mapping[key]) && mapping[key].some((entry) => String(entry.bizCode ?? "") === code && (entry.codeType === undefined || entry.codeType === 1))) {
|
|
798
|
+
via = label;
|
|
799
|
+
break;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
if (!via)
|
|
804
|
+
continue;
|
|
805
|
+
references.push({
|
|
806
|
+
kind: "businessRule",
|
|
807
|
+
code: ruleCode || nodeCode,
|
|
808
|
+
name: String(record.name ?? (ruleCode || nodeCode)),
|
|
809
|
+
via,
|
|
810
|
+
schemaCode,
|
|
811
|
+
});
|
|
812
|
+
break;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
return { references };
|
|
816
|
+
}
|
|
817
|
+
async function scanFieldFormListviewOnlineJs(session, schemaCode, code) {
|
|
818
|
+
const references = [];
|
|
819
|
+
const dependentUpdates = {};
|
|
820
|
+
// form.get 面:控件码清单
|
|
821
|
+
try {
|
|
822
|
+
const sheet = (await wireCall(session, { path: ENDPOINTS.sheetGet, query: { schemaCode, sheetCode: schemaCode } })).data;
|
|
823
|
+
if (!sheet) {
|
|
824
|
+
dependentUpdates.form = { present: false, note: "默认表单不存在(sheetGet 空)" };
|
|
825
|
+
}
|
|
826
|
+
else {
|
|
827
|
+
const draftControls = formControlCodes(sheet.draftAttributesJson);
|
|
828
|
+
const publishedControls = formControlCodes(sheet.publishedAttributesJson);
|
|
829
|
+
const inDraft = draftControls.includes(code);
|
|
830
|
+
const inPublished = publishedControls.includes(code);
|
|
831
|
+
dependentUpdates.form = {
|
|
832
|
+
present: true,
|
|
833
|
+
inDraft,
|
|
834
|
+
inPublished,
|
|
835
|
+
note: inDraft || inPublished
|
|
836
|
+
? "删除字段后表单控件会残留;需 form.draft(rebuild)/form.publish 清理"
|
|
837
|
+
: "默认表单控件码未引用该字段",
|
|
838
|
+
};
|
|
839
|
+
if (inDraft || inPublished) {
|
|
840
|
+
references.push({
|
|
841
|
+
kind: "form",
|
|
842
|
+
code: schemaCode,
|
|
843
|
+
name: String(sheet.name ?? schemaCode),
|
|
844
|
+
via: inDraft && inPublished ? "draft+published controls" : inDraft ? "draft controls" : "published controls",
|
|
845
|
+
schemaCode,
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
// online JS:扫 customScript 原文是否提及字段 code
|
|
849
|
+
const scripts = [];
|
|
850
|
+
for (const face of [sheet.draftHtmlJson, sheet.publishedHtmlJson]) {
|
|
851
|
+
if (typeof face !== "string" || !face.trim())
|
|
852
|
+
continue;
|
|
853
|
+
try {
|
|
854
|
+
const parsed = JSON.parse(face);
|
|
855
|
+
if (!Array.isArray(parsed))
|
|
856
|
+
continue;
|
|
857
|
+
const custom = parsed.find((element) => element?.id === "customScript");
|
|
858
|
+
if (typeof custom?.innerHTML === "string" && custom.innerHTML.trim())
|
|
859
|
+
scripts.push(custom.innerHTML);
|
|
860
|
+
}
|
|
861
|
+
catch {
|
|
862
|
+
// ignore malformed html face
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
const mentioned = scripts.some((script) => scriptMentionsField(script, code));
|
|
866
|
+
dependentUpdates.onlineJs = {
|
|
867
|
+
scanned: true,
|
|
868
|
+
mentioned,
|
|
869
|
+
note: mentioned
|
|
870
|
+
? "在线 JS 原文仍包含该字段 code;删除后脚本引用会静默残留,需 onlinejs.compose 清理"
|
|
871
|
+
: "在线 JS 原文未匹配到该字段 code(按词边界扫描,不含间接引用)",
|
|
872
|
+
};
|
|
873
|
+
if (mentioned) {
|
|
874
|
+
references.push({
|
|
875
|
+
kind: "onlineJs",
|
|
876
|
+
code: schemaCode,
|
|
877
|
+
name: String(sheet.name ?? schemaCode),
|
|
878
|
+
via: "customScript.innerHTML",
|
|
879
|
+
schemaCode,
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
catch (error) {
|
|
885
|
+
dependentUpdates.form = { unknown: true, note: `form.get 不可用:${error instanceof Error ? error.message : String(error)}` };
|
|
886
|
+
dependentUpdates.onlineJs = { unknown: true, note: `onlinejs 扫描跳过:form 面读取失败` };
|
|
887
|
+
}
|
|
888
|
+
// listview.get 面:列/条件
|
|
889
|
+
try {
|
|
890
|
+
const got = await wireCall(session, { path: ENDPOINTS.listQueryGet, query: { schemaCode, code: schemaCode, clientType: "PC" } });
|
|
891
|
+
const model = (got.data ?? null);
|
|
892
|
+
if (!model || !model.id) {
|
|
893
|
+
dependentUpdates.listview = { present: false, note: "默认列表视图不存在" };
|
|
894
|
+
}
|
|
895
|
+
else {
|
|
896
|
+
const columns = Array.isArray(model.queryColumns) ? model.queryColumns : [];
|
|
897
|
+
const conditions = Array.isArray(model.queryConditions) ? model.queryConditions : [];
|
|
898
|
+
const inColumns = columns.some((column) => String(column.propertyCode ?? "") === code);
|
|
899
|
+
const inConditions = conditions.some((condition) => String(condition.propertyCode ?? "") === code);
|
|
900
|
+
dependentUpdates.listview = {
|
|
901
|
+
present: true,
|
|
902
|
+
code: String(model.code ?? schemaCode),
|
|
903
|
+
inColumns,
|
|
904
|
+
inConditions,
|
|
905
|
+
note: inColumns || inConditions
|
|
906
|
+
? "删除字段后列表列/条件会残留;需 listview.configure 清理"
|
|
907
|
+
: "默认列表视图列/条件未引用该字段",
|
|
908
|
+
};
|
|
909
|
+
if (inColumns || inConditions) {
|
|
910
|
+
references.push({
|
|
911
|
+
kind: "listview",
|
|
912
|
+
code: String(model.code ?? schemaCode),
|
|
913
|
+
name: String(model.name ?? model.code ?? schemaCode),
|
|
914
|
+
via: inColumns && inConditions ? "columns+conditions" : inColumns ? "columns" : "conditions",
|
|
915
|
+
schemaCode,
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
catch (error) {
|
|
921
|
+
dependentUpdates.listview = { unknown: true, note: `listview.get 不可用:${error instanceof Error ? error.message : String(error)}` };
|
|
922
|
+
}
|
|
923
|
+
return { references, dependentUpdates };
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* field.delete 影响面:dry-run 全量(行数 + 规则 + 表单/视图/在线JS),
|
|
927
|
+
* 真写回执给摘要(不扫规则图,只扫表单/视图/在线JS + 行数)。
|
|
928
|
+
*/
|
|
929
|
+
async function collectFieldDeleteImpact(session, args, options) {
|
|
930
|
+
const counted = await countModelDataRows(session, args.schemaCode);
|
|
931
|
+
const references = [];
|
|
932
|
+
const unknowns = [];
|
|
933
|
+
if (options.full) {
|
|
934
|
+
const rules = await scanFieldRuleReferences(session, args.schemaCode, args.code);
|
|
935
|
+
references.push(...rules.references);
|
|
936
|
+
if (rules.unknown)
|
|
937
|
+
unknowns.push(rules.unknown);
|
|
938
|
+
}
|
|
939
|
+
else {
|
|
940
|
+
unknowns.push("真写回执未扫业务规则引用(成本高);完整引用面请先 --dry-run");
|
|
941
|
+
}
|
|
942
|
+
const surfaces = await scanFieldFormListviewOnlineJs(session, args.schemaCode, args.code);
|
|
943
|
+
references.push(...surfaces.references);
|
|
944
|
+
for (const key of ["form", "listview", "onlineJs"]) {
|
|
945
|
+
const face = surfaces.dependentUpdates[key];
|
|
946
|
+
if (face?.unknown === true && typeof face.note === "string")
|
|
947
|
+
unknowns.push(face.note);
|
|
948
|
+
}
|
|
949
|
+
return {
|
|
950
|
+
dataRows: counted.dataRows,
|
|
951
|
+
...(counted.note ? { dataRowsNote: counted.note } : counted.dataRows === "unknown" ? { dataRowsNote: "拿不到行数:data.list total 不可用" } : {}),
|
|
952
|
+
references,
|
|
953
|
+
dependentUpdates: surfaces.dependentUpdates,
|
|
954
|
+
...(unknowns.length > 0 ? { unknown: unknowns } : {}),
|
|
955
|
+
note: options.full
|
|
956
|
+
? "完整影响面(规则+表单+视图+在线JS+行数)。删除不会自动清理依赖面残留。"
|
|
957
|
+
: "摘要影响面(表单+视图+在线JS+行数;规则请看 dry-run)。删除不会自动清理依赖面残留。",
|
|
958
|
+
};
|
|
959
|
+
}
|
|
552
960
|
/** The proven field.update write body — shared by the write tool and the cascade path. */
|
|
553
961
|
async function runFieldUpdate(session, args) {
|
|
554
962
|
const listed = await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "false" } });
|
|
@@ -560,7 +968,33 @@ async function runFieldUpdate(session, args) {
|
|
|
560
968
|
const record = rows.find((row) => (row.code ?? row.propertyCode) === args.code);
|
|
561
969
|
if (!record)
|
|
562
970
|
throw new WireError("business", `field ${args.code} not found on ${args.schemaCode}`, 301001);
|
|
563
|
-
|
|
971
|
+
let effectiveOptions = args.options;
|
|
972
|
+
let dictionarySynced;
|
|
973
|
+
if (args.syncDictionary === true) {
|
|
974
|
+
const baseOptions = optionsObjectOf(args.options ?? record.options);
|
|
975
|
+
const binding = dictionaryBindingOf(baseOptions);
|
|
976
|
+
if (!binding?.checkedDictionary && !binding?.checkedDictionaryCode) {
|
|
977
|
+
throw new ToolRefusalError({
|
|
978
|
+
status: "refused",
|
|
979
|
+
blocker: "field-not-dictionary-backed",
|
|
980
|
+
schemaCode: args.schemaCode,
|
|
981
|
+
code: args.code,
|
|
982
|
+
message: `field.update syncDictionary:true 要求字段 options.optionsSet.optionsType=dictionary(含 checkedDictionary/checkedDictionaryCode);${args.schemaCode}.${args.code} 不是字典-backed`,
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
const enabled = await loadEnabledDictionaryRecords(session, binding);
|
|
986
|
+
const optionsSet = { ...baseOptions.optionsSet };
|
|
987
|
+
const dictionary = { ...(optionsSet.dictionary ?? {}) };
|
|
988
|
+
dictionary.useDictionariesData = enabled;
|
|
989
|
+
optionsSet.dictionary = dictionary;
|
|
990
|
+
optionsSet.optionsType = "dictionary";
|
|
991
|
+
effectiveOptions = { ...baseOptions, optionsSet };
|
|
992
|
+
dictionarySynced = {
|
|
993
|
+
from: binding.useDictionariesData.map((row) => row.code).filter(Boolean),
|
|
994
|
+
to: enabled.map((row) => row.code),
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
if (effectiveOptions !== undefined) {
|
|
564
998
|
// Same platform cycle as field.create (r17). On UPDATE the caller may also
|
|
565
999
|
// be rewriting a previously-fine AGGREGATION, so resolve the child tables
|
|
566
1000
|
// from the published face the same way; a read failure must not silently
|
|
@@ -568,8 +1002,8 @@ async function runFieldUpdate(session, args) {
|
|
|
568
1002
|
const publishedRows = await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "true" } })
|
|
569
1003
|
.then((response) => wireRows(response.data))
|
|
570
1004
|
.catch(() => []);
|
|
571
|
-
assertNoSelfSchemaAggregation({ schemaCode: args.schemaCode, propertyType: record.type ?? record.propertyType, options:
|
|
572
|
-
await assertNoChildSchemaAggregation(session, args.schemaCode, record.type ?? record.propertyType,
|
|
1005
|
+
assertNoSelfSchemaAggregation({ schemaCode: args.schemaCode, propertyType: record.type ?? record.propertyType, options: effectiveOptions }, publishedRows);
|
|
1006
|
+
await assertNoChildSchemaAggregation(session, args.schemaCode, record.type ?? record.propertyType, effectiveOptions);
|
|
573
1007
|
}
|
|
574
1008
|
const targetVersion = session.detectedVersion();
|
|
575
1009
|
const update = {
|
|
@@ -579,7 +1013,7 @@ async function runFieldUpdate(session, args) {
|
|
|
579
1013
|
...(targetVersion ? { targetVersion } : {}),
|
|
580
1014
|
...(args.name !== undefined ? { name: args.name } : {}),
|
|
581
1015
|
...(args.name_i18n !== undefined ? { name_i18n: args.name_i18n } : {}),
|
|
582
|
-
...(
|
|
1016
|
+
...(effectiveOptions !== undefined ? { options: effectiveOptions } : {}),
|
|
583
1017
|
...(args.choices !== undefined ? { choices: args.choices } : {}),
|
|
584
1018
|
...(args.relativeCode !== undefined ? { relativeCode: args.relativeCode } : {}),
|
|
585
1019
|
...(args.subFields !== undefined ? { subFields: args.subFields } : {}),
|
|
@@ -597,7 +1031,7 @@ async function runFieldUpdate(session, args) {
|
|
|
597
1031
|
// failure never hides the field write (it is surfaced as formConditionSync.error).
|
|
598
1032
|
let formConditionSync;
|
|
599
1033
|
let formConditionSyncError;
|
|
600
|
-
if (
|
|
1034
|
+
if (effectiveOptions !== undefined && !prepared.isChildTable) {
|
|
601
1035
|
const previous = decodeJsonObjectColumn(record.options, { invalid: "throw", array: "accept", object: "accept" }) ?? {};
|
|
602
1036
|
try {
|
|
603
1037
|
const sync = await syncFormConditionDataRules(session, args.schemaCode, args.code, previous, prepared.options);
|
|
@@ -613,17 +1047,42 @@ async function runFieldUpdate(session, args) {
|
|
|
613
1047
|
// confirm the write landed — the readback only named the field. Field
|
|
614
1048
|
// options are the form-interaction contract (ADR 0005), so read them back.
|
|
615
1049
|
const storedOptions = after !== undefined ? parseFieldOptionsForReadback(after.options) : undefined;
|
|
1050
|
+
// r32(真机复现:field.update propertyEmpty:true 回 ok/DELIVERED,field.list --full
|
|
1051
|
+
// 与 model.inspect 都看不到该值):平台字段行读面(bizproperty/list 与
|
|
1052
|
+
// bizproperty/get)都不回 propertyEmpty,这是**只写属性**。不编造读回值:
|
|
1053
|
+
// - `propertyEmpty` 只在草稿行确实带该键时才输出(不与 null 比较);否则省略;
|
|
1054
|
+
// - `committed` 列出本次线缆体里实际提交的键,调用方据此确认「提交了什么」。
|
|
1055
|
+
const submittedPropertyEmpty = args.propertyEmpty;
|
|
1056
|
+
const hasReadBackPropertyEmpty = after !== undefined && Object.prototype.hasOwnProperty.call(after, "propertyEmpty");
|
|
1057
|
+
const committedKeys = [
|
|
1058
|
+
...(args.name !== undefined ? ["name"] : []),
|
|
1059
|
+
...(args.name_i18n !== undefined ? ["name_i18n"] : []),
|
|
1060
|
+
...(effectiveOptions !== undefined || args.choices !== undefined ? ["options"] : []),
|
|
1061
|
+
...(args.relativeCode !== undefined ? ["relativeCode"] : []),
|
|
1062
|
+
...(args.propertyEmpty !== undefined ? ["propertyEmpty"] : []),
|
|
1063
|
+
];
|
|
1064
|
+
const unverifiable = submittedPropertyEmpty !== undefined && !hasReadBackPropertyEmpty ? ["propertyEmpty"] : [];
|
|
616
1065
|
return {
|
|
617
1066
|
performed: { errcode: performed.errcode },
|
|
618
1067
|
...(prepared.conditionCompiled !== undefined && prepared.conditionCompiled.length > 0
|
|
619
1068
|
? { note: { conditionCompiled: prepared.conditionCompiled, hint: "符号式/裸值条件已就地编译为平台中文规范形状;单选字段按选项 code 比较仍需 specifyOption;此配置是门户交互层,data.submit 不拦截" } }
|
|
620
1069
|
: {}),
|
|
621
1070
|
readback: {
|
|
1071
|
+
face: "draft",
|
|
622
1072
|
fieldPresent: after !== undefined,
|
|
623
1073
|
...(after?.name !== undefined ? { name: after.name } : {}),
|
|
624
1074
|
...(typeof after?.name_i18n === "string" ? { name_i18n: after.name_i18n } : {}),
|
|
625
1075
|
...(after?.type !== undefined || after?.propertyType !== undefined ? { type: after?.type ?? after?.propertyType } : {}),
|
|
1076
|
+
...(hasReadBackPropertyEmpty ? { propertyEmpty: after?.propertyEmpty } : {}),
|
|
1077
|
+
committed: committedKeys,
|
|
1078
|
+
...(unverifiable.length > 0
|
|
1079
|
+
? {
|
|
1080
|
+
unverifiable,
|
|
1081
|
+
unverifiableNote: "propertyEmpty 是只写属性:平台字段读面(bizproperty/list|get)不回该键,无法读回核对。本回执只证明它已随本次请求提交。",
|
|
1082
|
+
}
|
|
1083
|
+
: {}),
|
|
626
1084
|
...(storedOptions !== undefined && storedOptions !== null ? { options: storedOptions } : {}),
|
|
1085
|
+
...(dictionarySynced !== undefined ? { dictionarySynced } : {}),
|
|
627
1086
|
...(formConditionSync !== undefined ? { formConditionSync } : {}),
|
|
628
1087
|
...(formConditionSyncError !== undefined ? { formConditionSync: { error: formConditionSyncError } } : {}),
|
|
629
1088
|
},
|
|
@@ -638,6 +1097,194 @@ async function runFieldUpdate(session, args) {
|
|
|
638
1097
|
* cascaded republish exists precisely because the PUBLISHED face carries the
|
|
639
1098
|
* stale shape while the field is present there.
|
|
640
1099
|
*/
|
|
1100
|
+
/**
|
|
1101
|
+
* GitLab issue #24(2026-11-05,Windows 8.6.44 实测):`field.publish` 用**子表**
|
|
1102
|
+
* 的 schemaCode 调用时平台回 300004「数据模型不存在!」——子表不是顶层 biz schema,
|
|
1103
|
+
* 发布是父模型级的。旧回执只给 `omc help omc-refusals`,用户只能自己猜父模型。
|
|
1104
|
+
* 这里在失败路径上反查一次(读面,最多 60 个模型、并发 4),把父模型直接点名。
|
|
1105
|
+
*/
|
|
1106
|
+
async function findChildTableParent(session, childCode) {
|
|
1107
|
+
const candidates = wireRows((await wireCall(session, { path: ENDPOINTS.modelList, query: {} })).data)
|
|
1108
|
+
.filter((row) => row.type === "BizModel" && typeof row.code === "string" && String(row.code).length > 0)
|
|
1109
|
+
.slice(0, 60);
|
|
1110
|
+
const queue = [...candidates];
|
|
1111
|
+
let found;
|
|
1112
|
+
const workers = Array.from({ length: 4 }, async () => {
|
|
1113
|
+
for (;;) {
|
|
1114
|
+
if (found !== undefined)
|
|
1115
|
+
return;
|
|
1116
|
+
const row = queue.shift();
|
|
1117
|
+
if (row === undefined)
|
|
1118
|
+
return;
|
|
1119
|
+
const parent = String(row.code);
|
|
1120
|
+
try {
|
|
1121
|
+
const fields = wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: parent, isPublish: "true" } })).data);
|
|
1122
|
+
if (fields.some((field) => Number(field.type ?? field.propertyType) === FIELD_PROPERTY_TYPES.CHILD_TABLE && String(field.code ?? field.propertyCode ?? "") === childCode)) {
|
|
1123
|
+
found = { parent };
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
catch {
|
|
1128
|
+
// A model whose field face is unreadable must not abort the search.
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
});
|
|
1132
|
+
await Promise.all(workers);
|
|
1133
|
+
return found;
|
|
1134
|
+
}
|
|
1135
|
+
/**
|
|
1136
|
+
* GitLab issue #24 的**主诉**:`field.publish` 成功只证明「字段可用」,
|
|
1137
|
+
* 业务页面还要表单布局里有这个控件。顶层字段看 attributes 的顶层键;子表列看
|
|
1138
|
+
* 父 Sheet201 控件的 `columns`(child keys 永远不进 viewJson)。
|
|
1139
|
+
*
|
|
1140
|
+
* 只在**确实缺**的时候占字节;同步时回一个短字符串。
|
|
1141
|
+
*/
|
|
1142
|
+
async function dictionaryStaleAdvisory(session, publishedRows) {
|
|
1143
|
+
const stale = [];
|
|
1144
|
+
const cache = new Map();
|
|
1145
|
+
for (const row of publishedRows) {
|
|
1146
|
+
const code = String(row.code ?? row.propertyCode ?? "");
|
|
1147
|
+
if (!code || SYSTEM_FIELD_CODES.has(code))
|
|
1148
|
+
continue;
|
|
1149
|
+
const binding = dictionaryBindingOf(optionsObjectOf(row.options));
|
|
1150
|
+
if (!binding)
|
|
1151
|
+
continue;
|
|
1152
|
+
const cacheKey = `${binding.checkedDictionary ?? ""}|${binding.checkedDictionaryCode ?? ""}`;
|
|
1153
|
+
let enabledCodes = cache.get(cacheKey);
|
|
1154
|
+
if (enabledCodes === undefined) {
|
|
1155
|
+
try {
|
|
1156
|
+
enabledCodes = (await loadEnabledDictionaryRecords(session, binding)).map((entry) => entry.code);
|
|
1157
|
+
}
|
|
1158
|
+
catch {
|
|
1159
|
+
continue;
|
|
1160
|
+
}
|
|
1161
|
+
cache.set(cacheKey, enabledCodes);
|
|
1162
|
+
}
|
|
1163
|
+
const fieldCodes = binding.useDictionariesData.map((entry) => entry.code).filter(Boolean);
|
|
1164
|
+
const enabledSet = new Set(enabledCodes);
|
|
1165
|
+
const fieldSet = new Set(fieldCodes);
|
|
1166
|
+
const missingInField = enabledCodes.filter((entry) => !fieldSet.has(entry));
|
|
1167
|
+
const extraInField = fieldCodes.filter((entry) => !enabledSet.has(entry));
|
|
1168
|
+
if (missingInField.length > 0 || extraInField.length > 0) {
|
|
1169
|
+
stale.push({ code, missingInField, extraInField });
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
return stale.length > 0 ? stale : undefined;
|
|
1173
|
+
}
|
|
1174
|
+
function relationMappingsEmptyWarnings(publishedRows) {
|
|
1175
|
+
const empty = publishedRows
|
|
1176
|
+
.filter((row) => {
|
|
1177
|
+
const type = Number(row.type ?? row.propertyType);
|
|
1178
|
+
if (type !== FIELD_PROPERTY_TYPES.WORK_SHEET)
|
|
1179
|
+
return false;
|
|
1180
|
+
const options = optionsObjectOf(row.options);
|
|
1181
|
+
const mappings = options.mappings;
|
|
1182
|
+
if (mappings === undefined || mappings === null)
|
|
1183
|
+
return true;
|
|
1184
|
+
if (typeof mappings === "string")
|
|
1185
|
+
return mappings.trim() === "" || mappings.trim() === "[]";
|
|
1186
|
+
if (Array.isArray(mappings))
|
|
1187
|
+
return mappings.length === 0;
|
|
1188
|
+
return false;
|
|
1189
|
+
})
|
|
1190
|
+
.map((row) => String(row.code ?? row.propertyCode ?? ""))
|
|
1191
|
+
.filter((code) => code.length > 0);
|
|
1192
|
+
if (empty.length === 0)
|
|
1193
|
+
return undefined;
|
|
1194
|
+
return [{ code: "relation-mappings-empty", fields: empty, hint: "选中时不回填副本列;配 options.mappings 后再 field.publish" }];
|
|
1195
|
+
}
|
|
1196
|
+
async function formBindingAdvisory(session, schemaCode, publishedRows) {
|
|
1197
|
+
const dictionaryStale = await dictionaryStaleAdvisory(session, publishedRows);
|
|
1198
|
+
const warnings = relationMappingsEmptyWarnings(publishedRows);
|
|
1199
|
+
let form = null;
|
|
1200
|
+
try {
|
|
1201
|
+
form = await readFormOrNull(session, schemaCode, schemaCode);
|
|
1202
|
+
}
|
|
1203
|
+
catch {
|
|
1204
|
+
return undefined;
|
|
1205
|
+
}
|
|
1206
|
+
const extras = {
|
|
1207
|
+
...(dictionaryStale !== undefined ? { dictionaryStale } : {}),
|
|
1208
|
+
...(warnings !== undefined ? { warnings } : {}),
|
|
1209
|
+
};
|
|
1210
|
+
if (form === null) {
|
|
1211
|
+
return {
|
|
1212
|
+
formPresent: false,
|
|
1213
|
+
note: `模型没有表单,业务页面看不到任何字段;建表单:omc call configure_form --args '${JSON.stringify({ schemaCode })}'`,
|
|
1214
|
+
...extras,
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
const attributesRaw = typeof form.publishedAttributesJson === "string" && form.publishedAttributesJson.trim() !== "" && form.publishedAttributesJson.trim() !== "{}"
|
|
1218
|
+
? form.publishedAttributesJson
|
|
1219
|
+
: typeof form.draftAttributesJson === "string" && form.draftAttributesJson.trim() !== "" && form.draftAttributesJson.trim() !== "{}"
|
|
1220
|
+
? form.draftAttributesJson
|
|
1221
|
+
: undefined;
|
|
1222
|
+
if (attributesRaw === undefined) {
|
|
1223
|
+
return {
|
|
1224
|
+
formPresent: true,
|
|
1225
|
+
note: `表单存在但布局为空,业务页面看不到字段;重建:omc call configure_form --args '${JSON.stringify({ schemaCode, rebuild: true })}'`,
|
|
1226
|
+
...extras,
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
let attributes;
|
|
1230
|
+
try {
|
|
1231
|
+
attributes = JSON.parse(attributesRaw);
|
|
1232
|
+
}
|
|
1233
|
+
catch {
|
|
1234
|
+
return { formPresent: true, note: "表单布局不是合法 JSON,未做绑定核对(先 form.get 读原文修复)", ...extras };
|
|
1235
|
+
}
|
|
1236
|
+
const business = publishedRows.filter((row) => {
|
|
1237
|
+
const code = String(row.code ?? row.propertyCode ?? "");
|
|
1238
|
+
return code.length > 0 && !SYSTEM_FIELD_CODES.has(code) && FIELD_PROPERTY_TYPES[fieldTypeName(row)] !== undefined;
|
|
1239
|
+
});
|
|
1240
|
+
const missingTopLevel = business
|
|
1241
|
+
.filter((row) => fieldTypeName(row) !== "CHILD_TABLE" && attributes[String(row.code ?? row.propertyCode)] === undefined)
|
|
1242
|
+
.map((row) => String(row.code ?? row.propertyCode));
|
|
1243
|
+
const missingChildColumns = [];
|
|
1244
|
+
for (const row of business) {
|
|
1245
|
+
if (fieldTypeName(row) !== "CHILD_TABLE")
|
|
1246
|
+
continue;
|
|
1247
|
+
const parent = String(row.code ?? row.propertyCode);
|
|
1248
|
+
const columns = attributes[parent]?.columns;
|
|
1249
|
+
const bound = new Set(Array.isArray(columns) ? columns.map((column) => String(column.key ?? "")) : []);
|
|
1250
|
+
const missing = childColumnsOf(row.subSchema).filter((column) => column.system !== true && !bound.has(column.code)).map((column) => column.code);
|
|
1251
|
+
if (missing.length > 0)
|
|
1252
|
+
missingChildColumns.push({ parent, fieldCodes: missing });
|
|
1253
|
+
}
|
|
1254
|
+
if (missingTopLevel.length === 0 && missingChildColumns.length === 0) {
|
|
1255
|
+
if (dictionaryStale === undefined && warnings === undefined)
|
|
1256
|
+
return "in-sync";
|
|
1257
|
+
return { formPresent: true, layout: "in-sync", ...extras };
|
|
1258
|
+
}
|
|
1259
|
+
const fix = missingChildColumns.length > 0
|
|
1260
|
+
? { schemaCode, ...(missingTopLevel.length > 0 ? { fieldCodes: missingTopLevel } : {}), childColumns: missingChildColumns }
|
|
1261
|
+
: { schemaCode, fieldCodes: missingTopLevel, rebuild: true };
|
|
1262
|
+
return {
|
|
1263
|
+
formPresent: true,
|
|
1264
|
+
...(missingTopLevel.length > 0 ? { notInFormLayout: missingTopLevel } : {}),
|
|
1265
|
+
...(missingChildColumns.length > 0 ? { childColumnsNotInForm: missingChildColumns } : {}),
|
|
1266
|
+
note: "字段已发布但**表单布局里没有**,业务页面看不到。子表列用 childColumns 增量绑进 Sheet201(不动其它控件);顶层缺字段用 rebuild:true(会重建布局)",
|
|
1267
|
+
nextCommand: `omc call configure_form --args '${JSON.stringify(fix)}'`,
|
|
1268
|
+
...extras,
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
/** PropertyType label for a raw field row (accepts {type} or {propertyType}). */
|
|
1272
|
+
function fieldTypeName(row) {
|
|
1273
|
+
const raw = row.typeName ?? fieldTypeLabel(row.type ?? row.propertyType);
|
|
1274
|
+
return String(raw ?? "");
|
|
1275
|
+
}
|
|
1276
|
+
/**
|
|
1277
|
+
* r32: 一句话说清「哪些 code 没到位、下一步怎么核对」。不存在的 code 指向
|
|
1278
|
+
* field.list(拼错/尚未创建),仍留在草稿面的 code 指向平台发布异常。
|
|
1279
|
+
*/
|
|
1280
|
+
function signPartialDelivery(missing, nonexistent, stillDraft) {
|
|
1281
|
+
const parts = [];
|
|
1282
|
+
if (nonexistent.length > 0)
|
|
1283
|
+
parts.push(`${nonexistent.join("、")} 在模型里不存在(草稿面也没有):先 field.list 核对 code(omc call field.list --args '{"schemaCode":"<schemaCode>"}'),或先 field.create 建字段——本次发布不会建字段`);
|
|
1284
|
+
if (stillDraft.length > 0)
|
|
1285
|
+
parts.push(`${stillDraft.join("、")} 模型里有但发布后仍不在发布面:按发布失败排查(查看平台日志后重试本工具,模型级发布可重复执行)`);
|
|
1286
|
+
return `部分交付(unpublishedRequested: ${missing.join("、")}):已发布的字段确实已发布,但这些请求的 code 不在发布面。${parts.join(";")}`;
|
|
1287
|
+
}
|
|
641
1288
|
export async function executeFieldPublish(session, args) {
|
|
642
1289
|
// bizproperty/publish is MODEL-SCOPED: one call publishes every draft
|
|
643
1290
|
// field of the schema. `code`/`codes` only select which fields to verify
|
|
@@ -682,9 +1329,35 @@ export async function executeFieldPublish(session, args) {
|
|
|
682
1329
|
const mismatches = checked.filter((entry) => entry.kind === "mismatch");
|
|
683
1330
|
return { mode: "detail", verified: true, checked: bounded.length, ok: mismatches.length === 0, ...(mismatches.length > 0 ? { mismatches } : {}) };
|
|
684
1331
|
};
|
|
1332
|
+
// r32: 请求里不存在的 code 不能靠「发布面缺它」推断——发布面缺也可能是这次
|
|
1333
|
+
// 发布才补上的。先记录 DRAFT 面(= 模型里真实存在的字段)的 code 集合,
|
|
1334
|
+
// 用来把「不存在」与「未发布」分开报。
|
|
1335
|
+
const draftCodesBefore = new Set(draftSnapshot.keys());
|
|
685
1336
|
const preRead = await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "true" } });
|
|
686
|
-
const
|
|
687
|
-
|
|
1337
|
+
const prePublishedRows = wireRows(preRead.data);
|
|
1338
|
+
const prePublished = prePublishedRows.map(codeOf).filter((code) => code.length > 0);
|
|
1339
|
+
// r29e F10/F11:旧短路只看「code 在发布面存在」,改了 options 仍报
|
|
1340
|
+
// already-published + verification.ok(假绿)。再比草稿/发布 options 指纹;
|
|
1341
|
+
// 不一致就真发布。name/type 仍由 verifyFieldDetails 核对。
|
|
1342
|
+
const draftOptionsByCode = new Map(wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "false" } })).data)
|
|
1343
|
+
.map((row) => [codeOf(row), optionsFingerprint(row.options)])
|
|
1344
|
+
.filter(([code]) => code.length > 0));
|
|
1345
|
+
const publishedOptionsByCode = new Map(prePublishedRows
|
|
1346
|
+
.map((row) => [codeOf(row), optionsFingerprint(row.options)])
|
|
1347
|
+
.filter(([code]) => code.length > 0));
|
|
1348
|
+
const optionsMatchRequested = requestedCodes.length > 0 &&
|
|
1349
|
+
requestedCodes.every((code) => {
|
|
1350
|
+
if (!prePublished.includes(code))
|
|
1351
|
+
return false;
|
|
1352
|
+
// 模型级 publish 后草稿面可能被清空(ticket 81 幂等重试场景)——草稿行不在
|
|
1353
|
+
// 时没有「内容不一致」可证,按已发布存在即可短路。
|
|
1354
|
+
if (!draftOptionsByCode.has(code))
|
|
1355
|
+
return true;
|
|
1356
|
+
const draftFp = draftOptionsByCode.get(code) ?? "";
|
|
1357
|
+
const publishedFp = publishedOptionsByCode.get(code) ?? "";
|
|
1358
|
+
return draftFp === publishedFp;
|
|
1359
|
+
});
|
|
1360
|
+
if (args.force !== true && requestedCodes.length > 0 && requestedCodes.every((code) => prePublished.includes(code)) && optionsMatchRequested) {
|
|
688
1361
|
return {
|
|
689
1362
|
performed: { skipped: "already-published", scope: "model" },
|
|
690
1363
|
readback: {
|
|
@@ -693,6 +1366,7 @@ export async function executeFieldPublish(session, args) {
|
|
|
693
1366
|
...(args.codes ? { publishedFieldsPresent: Object.fromEntries(args.codes.map((code) => [code, true])) } : {}),
|
|
694
1367
|
allRequestedPresent: true,
|
|
695
1368
|
verification: await verifyFieldDetails(requestedCodes),
|
|
1369
|
+
formBinding: await formBindingAdvisory(session, args.schemaCode, prePublishedRows),
|
|
696
1370
|
},
|
|
697
1371
|
};
|
|
698
1372
|
}
|
|
@@ -718,6 +1392,22 @@ export async function executeFieldPublish(session, args) {
|
|
|
718
1392
|
break;
|
|
719
1393
|
}
|
|
720
1394
|
catch (error) {
|
|
1395
|
+
// GitLab issue #24 附带坑 1:用**子表** schemaCode 发布 → 平台 300004
|
|
1396
|
+
// 「数据模型不存在!」。子表不是顶层 biz schema,发布是父模型级。旧回执只
|
|
1397
|
+
// 把裸 wire 错误冒泡 + 指向 omc help omc-refusals,用户只能自己猜父模型。
|
|
1398
|
+
if (error instanceof WireError && error.kind === "business" && error.errcode === 300004) {
|
|
1399
|
+
const parent = await findChildTableParent(session, args.schemaCode).catch(() => undefined);
|
|
1400
|
+
throw new ToolRefusalError({
|
|
1401
|
+
status: "refused",
|
|
1402
|
+
blocker: parent ? "field-publish-child-schema" : "wire-business",
|
|
1403
|
+
errcode: 300004,
|
|
1404
|
+
schemaCode: args.schemaCode,
|
|
1405
|
+
...(parent ? { childOf: parent.parent, nextCommand: `omc call field.publish --args '${JSON.stringify({ schemaCode: parent.parent })}'` } : {}),
|
|
1406
|
+
message: parent
|
|
1407
|
+
? `${args.schemaCode} 是子表(属于父模型 ${parent.parent}),不是顶层数据模型——平台按顶层 schema 发布,所以回 300004「数据模型不存在!」。改发父模型:omc call field.publish --args '${JSON.stringify({ schemaCode: parent.parent })}'(模型级一次发布全部草稿字段,子表列随父模型一起发布)`
|
|
1408
|
+
: `${args.schemaCode} 不在顶层数据模型清单里(300004 数据模型不存在)。若它是子表,请用**父模型**的 schemaCode 发布;\`omc call model.list\` 可核对顶层模型 code`,
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
721
1411
|
const transientColumnSync = error instanceof WireError && error.kind === "business" && error.errcode === 50000 && error.message.includes("不存在列");
|
|
722
1412
|
if (!(transientColumnSync && attempt < 3))
|
|
723
1413
|
throw error;
|
|
@@ -729,8 +1419,22 @@ export async function executeFieldPublish(session, args) {
|
|
|
729
1419
|
const publishedCodes = wireRows(readback.data).map(codeOf).filter((code) => code.length > 0);
|
|
730
1420
|
const requested = [...(args.code ? [args.code] : []), ...(args.codes ?? [])];
|
|
731
1421
|
const present = requested.filter((code) => publishedCodes.includes(code));
|
|
1422
|
+
// r32(真机复现 0.6.13):`codes:["g1","notExist"]` 回 ok + DELIVERED +
|
|
1423
|
+
// allRequestedPresent:false + verification.ok:true —— 三个成功标记与「有请求的
|
|
1424
|
+
// 字段不在发布面」同真。不在发布面的 code 分两类,报法不同:
|
|
1425
|
+
// nonexistentCodes —— 草稿面都没有(模型里根本不存在,多半是拼错);
|
|
1426
|
+
// stillDraftCodes —— 模型里有、发布后仍不在发布面(真异常)。
|
|
1427
|
+
// 两者都让这次交付只算**部分成功**:已发布的字段是真发布了,所以不整单
|
|
1428
|
+
// 判失败;但既有的 verified/ok、allRequestedPresent、verification.ok 都不能
|
|
1429
|
+
// 再报 true(派生的 readback.present=false 会让共享写门把 deliveryState 降为
|
|
1430
|
+
// 既有枚举值 WRITE_UNVERIFIED——刻意不新增枚举外的值)。
|
|
1431
|
+
const missingRequested = requested.filter((code) => !present.includes(code));
|
|
1432
|
+
const nonexistentCodes = missingRequested.filter((code) => !draftCodesBefore.has(code));
|
|
1433
|
+
const stillDraftCodes = missingRequested.filter((code) => draftCodesBefore.has(code));
|
|
1434
|
+
const allRequestedPresent = missingRequested.length === 0;
|
|
732
1435
|
const targets = (requested.length > 0 ? requested : [...draftSnapshot.keys()]).filter((code) => publishedCodes.includes(code));
|
|
733
|
-
const
|
|
1436
|
+
const verificationRun = await verifyFieldDetails(targets);
|
|
1437
|
+
const verification = allRequestedPresent ? verificationRun : { ...verificationRun, ok: false };
|
|
734
1438
|
const mismatches = verification.mismatches;
|
|
735
1439
|
if (mismatches !== undefined)
|
|
736
1440
|
throw new ToolRefusalError({
|
|
@@ -746,8 +1450,26 @@ export async function executeFieldPublish(session, args) {
|
|
|
746
1450
|
publishedCodes,
|
|
747
1451
|
...(args.code ? { publishedFieldPresent: publishedCodes.includes(args.code) } : {}),
|
|
748
1452
|
...(args.codes ? { publishedFieldsPresent: Object.fromEntries(args.codes.map((code) => [code, publishedCodes.includes(code)])) } : {}),
|
|
749
|
-
...(requested.length > 0
|
|
1453
|
+
...(requested.length > 0
|
|
1454
|
+
? {
|
|
1455
|
+
allRequestedPresent,
|
|
1456
|
+
verified: allRequestedPresent,
|
|
1457
|
+
// 共享写门(write.ts deliveryStateFor)读 readback.present:部分成功不
|
|
1458
|
+
// 能盖 DELIVERED。全部就位时不写这个键(只增语义,不改变成功回执)。
|
|
1459
|
+
...(allRequestedPresent ? {} : { present: false }),
|
|
1460
|
+
...(allRequestedPresent
|
|
1461
|
+
? {}
|
|
1462
|
+
: {
|
|
1463
|
+
unpublishedRequested: missingRequested,
|
|
1464
|
+
...(nonexistentCodes.length > 0 ? { nonexistentCodes } : {}),
|
|
1465
|
+
...(stillDraftCodes.length > 0 ? { stillDraftCodes } : {}),
|
|
1466
|
+
note: signPartialDelivery(missingRequested, nonexistentCodes, stillDraftCodes),
|
|
1467
|
+
}),
|
|
1468
|
+
}
|
|
1469
|
+
: {}),
|
|
750
1470
|
verification,
|
|
1471
|
+
// issue #24 主诉:publishedCodes 只证明「字段可用」,不等于「页面可见」。
|
|
1472
|
+
formBinding: await formBindingAdvisory(session, args.schemaCode, wireRows(readback.data)),
|
|
751
1473
|
},
|
|
752
1474
|
};
|
|
753
1475
|
}
|