openxiangda 1.0.159 → 1.0.161
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -13
- package/lib/change-baseline.js +1077 -0
- package/lib/cli.js +12002 -6723
- package/lib/config.js +272 -7
- package/lib/http.js +2 -0
- package/lib/js-code-build.js +61 -0
- package/lib/publish-context.js +98 -0
- package/lib/publish-lease-heartbeat.js +182 -0
- package/lib/publish-lease.js +190 -0
- package/lib/release-telemetry.js +182 -0
- package/lib/runtime-lineage.js +297 -0
- package/lib/sdd.js +567 -35
- package/lib/source-dependencies.js +742 -0
- package/lib/workspace-init.js +6 -1
- package/lib/worktree-owner.js +219 -0
- package/openxiangda-skills/SKILL.md +103 -228
- package/openxiangda-skills/references/architecture-design.md +2 -1
- package/openxiangda-skills/references/openxiangda-api.md +27 -0
- package/openxiangda-skills/references/pages/page-sdk.md +11 -1
- package/openxiangda-skills/references/resource-manifest-cheatsheet.md +12 -2
- package/openxiangda-skills/references/workspace-state.md +2 -0
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +77 -243
- package/openxiangda-skills/skills/openxiangda-form/SKILL.md +8 -0
- package/openxiangda-skills/skills/openxiangda-page/SKILL.md +18 -0
- package/package.json +20 -1
- package/packages/sdk/dist/{ProcessPreview-DMkzccq4.d.mts → ProcessPreview-Dux73lY_.d.mts} +5 -0
- package/packages/sdk/dist/{ProcessPreview-DMkzccq4.d.ts → ProcessPreview-Dux73lY_.d.ts} +5 -0
- package/packages/sdk/dist/components/index.cjs +29 -1
- package/packages/sdk/dist/components/index.cjs.map +1 -1
- package/packages/sdk/dist/components/index.d.mts +4 -4
- package/packages/sdk/dist/components/index.d.ts +4 -4
- package/packages/sdk/dist/components/index.mjs +29 -1
- package/packages/sdk/dist/components/index.mjs.map +1 -1
- package/packages/sdk/dist/{dataManagementApi-C9O-Bb0j.d.ts → dataManagementApi-BNLH2o7f.d.ts} +2 -1
- package/packages/sdk/dist/{dataManagementApi-gpZkgRDM.d.mts → dataManagementApi-BOcIx4U-.d.mts} +2 -1
- package/packages/sdk/dist/runtime/index.cjs +150 -22
- package/packages/sdk/dist/runtime/index.cjs.map +1 -1
- package/packages/sdk/dist/runtime/index.d.mts +3 -3
- package/packages/sdk/dist/runtime/index.d.ts +3 -3
- package/packages/sdk/dist/runtime/index.mjs +150 -22
- package/packages/sdk/dist/runtime/index.mjs.map +1 -1
- package/packages/sdk/dist/runtime/react.cjs +121 -21
- package/packages/sdk/dist/runtime/react.cjs.map +1 -1
- package/packages/sdk/dist/runtime/react.d.mts +15 -2
- package/packages/sdk/dist/runtime/react.d.ts +15 -2
- package/packages/sdk/dist/runtime/react.mjs +121 -21
- package/packages/sdk/dist/runtime/react.mjs.map +1 -1
- package/packages/sdk/src/build-source/scripts/register.mjs +54 -23
- package/packages/sdk/src/build-source/scripts/sync-schema.mjs +59 -19
- package/packages/sdk/src/build-source/scripts/utils/form-api.mjs +242 -2
- package/templates/openxiangda-react-spa/.cursor/rules/openxiangda-resources.mdc +7 -3
- package/templates/openxiangda-react-spa/.cursor/rules/openxiangda.mdc +11 -4
- package/templates/openxiangda-react-spa/.qoder/rules/openxiangda-resources.md +7 -3
- package/templates/openxiangda-react-spa/.qoder/rules/openxiangda.md +10 -4
- package/templates/openxiangda-react-spa/AGENTS.md +25 -12
- package/templates/openxiangda-react-spa/scripts/build-js-code.mjs +344 -60
- package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda-resources.mdc +5 -1
- package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda.mdc +8 -2
- package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda-resources.md +8 -4
- package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda.md +8 -2
- package/templates/sy-lowcode-app-workspace/AGENTS.md +14 -4
- package/templates/sy-lowcode-app-workspace/scripts/build-js-code.mjs +344 -60
|
@@ -20,7 +20,10 @@ import {
|
|
|
20
20
|
ensureSchemaFormUuid,
|
|
21
21
|
getOpenApiAccessToken,
|
|
22
22
|
getOpenApiForm,
|
|
23
|
+
getOpenXiangdaPublishContextHeaders,
|
|
23
24
|
isOpenXiangdaMode,
|
|
25
|
+
markWorkspaceFormBundlePublished,
|
|
26
|
+
mutateOpenXiangdaFormConfig,
|
|
24
27
|
} from "./utils/form-api.mjs";
|
|
25
28
|
import { assertFormReadyForBundle } from "./utils/schema-transform.mjs";
|
|
26
29
|
|
|
@@ -88,6 +91,7 @@ async function registerForms() {
|
|
|
88
91
|
|
|
89
92
|
let formUuid;
|
|
90
93
|
let appType;
|
|
94
|
+
let releaseExpectation = null;
|
|
91
95
|
try {
|
|
92
96
|
const ensured = await ensureSchemaFormUuid({
|
|
93
97
|
config,
|
|
@@ -99,6 +103,7 @@ async function registerForms() {
|
|
|
99
103
|
});
|
|
100
104
|
formUuid = ensured.formUuid;
|
|
101
105
|
appType = ensured.appType || config.appType;
|
|
106
|
+
releaseExpectation = ensured.releaseExpectation;
|
|
102
107
|
if (ensured.created || ensured.dryRunCreated) created += 1;
|
|
103
108
|
} catch (error) {
|
|
104
109
|
console.error(` ✗ 表单 ${formName}: ${error.message}`);
|
|
@@ -152,34 +157,57 @@ async function registerForms() {
|
|
|
152
157
|
continue;
|
|
153
158
|
}
|
|
154
159
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
160
|
+
let body;
|
|
161
|
+
if (isOpenXiangdaMode(config)) {
|
|
162
|
+
try {
|
|
163
|
+
const mutation = await mutateOpenXiangdaFormConfig(
|
|
164
|
+
config,
|
|
165
|
+
await getAccessToken(),
|
|
166
|
+
{
|
|
167
|
+
appType,
|
|
168
|
+
formUuid,
|
|
169
|
+
endpoint: "publish",
|
|
170
|
+
operation: "bundle",
|
|
171
|
+
expected: releaseExpectation,
|
|
172
|
+
payload: {
|
|
166
173
|
bundleUrl: payload.bundleUrl,
|
|
167
174
|
cssUrl: payload.cssUrl,
|
|
168
175
|
cssIsolation: payload.cssIsolation,
|
|
169
176
|
version: payload.version,
|
|
170
177
|
runtimeMode: payload.runtimeMode,
|
|
171
178
|
runtime: payload.runtime,
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
);
|
|
182
|
+
body = mutation.response;
|
|
183
|
+
markWorkspaceFormBundlePublished(config, formName, formUuid, {
|
|
184
|
+
revision: mutation.data?.revision,
|
|
185
|
+
etag: mutation.data?.etag,
|
|
186
|
+
activeFormReleaseHead: mutation.data?.activeFormReleaseHead,
|
|
187
|
+
version: payload.version,
|
|
188
|
+
});
|
|
189
|
+
} catch (error) {
|
|
190
|
+
console.error(` ✗ 表单 ${formName}: ${error.message}`);
|
|
191
|
+
failed += 1;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
} else {
|
|
195
|
+
const response = await fetch(getFormPublishUrl(appType, formUuid), {
|
|
196
|
+
method: "POST",
|
|
197
|
+
headers: {
|
|
198
|
+
"Content-Type": "application/json",
|
|
199
|
+
"x-acs-dingtalk-access-token": await getAccessToken(),
|
|
200
|
+
},
|
|
201
|
+
body: JSON.stringify(payload),
|
|
202
|
+
});
|
|
203
|
+
body = await response.json().catch(() => null);
|
|
204
|
+
if (!response.ok || body?.code !== 200) {
|
|
205
|
+
console.error(
|
|
206
|
+
` ✗ 表单 ${formName}: HTTP ${response.status} ${body?.message || response.statusText}`,
|
|
207
|
+
);
|
|
208
|
+
failed += 1;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
183
211
|
}
|
|
184
212
|
console.log(` ✓ 表单 ${formName} (${formUuid})`);
|
|
185
213
|
succeeded += 1;
|
|
@@ -242,7 +270,10 @@ async function registerPages() {
|
|
|
242
270
|
headers: {
|
|
243
271
|
"Content-Type": "application/json",
|
|
244
272
|
...(isOpenXiangdaMode(config)
|
|
245
|
-
? {
|
|
273
|
+
? {
|
|
274
|
+
Authorization: `Bearer ${await getAccessToken()}`,
|
|
275
|
+
...getOpenXiangdaPublishContextHeaders(),
|
|
276
|
+
}
|
|
246
277
|
: { "x-acs-dingtalk-access-token": await getAccessToken() }),
|
|
247
278
|
},
|
|
248
279
|
body: JSON.stringify(payload),
|
|
@@ -21,8 +21,10 @@ import { loadConfig, getApiBaseUrl, rootDir } from "./utils/load-config.mjs";
|
|
|
21
21
|
import {
|
|
22
22
|
ensureSchemaFormUuid,
|
|
23
23
|
getOpenApiAccessToken,
|
|
24
|
+
getOpenApiForm,
|
|
24
25
|
isOpenXiangdaMode,
|
|
25
26
|
markWorkspaceFormSchemaSynced,
|
|
27
|
+
mutateOpenXiangdaFormConfig,
|
|
26
28
|
} from "./utils/form-api.mjs";
|
|
27
29
|
import {
|
|
28
30
|
assertSchemaSyncResult,
|
|
@@ -169,7 +171,7 @@ async function loadSchema(schemaPath) {
|
|
|
169
171
|
|
|
170
172
|
// ---------- API 发送 ----------
|
|
171
173
|
|
|
172
|
-
async function sendToApi(apiPayload, config, accessToken) {
|
|
174
|
+
async function sendToApi(apiPayload, config, accessToken, expected) {
|
|
173
175
|
const apiBase = getApiBaseUrl(config);
|
|
174
176
|
const targetAppType = apiPayload.appType || config.appType;
|
|
175
177
|
const url = isOpenXiangdaMode(config)
|
|
@@ -178,29 +180,57 @@ async function sendToApi(apiPayload, config, accessToken) {
|
|
|
178
180
|
|
|
179
181
|
console.log(` 发送请求到: ${url}`);
|
|
180
182
|
|
|
183
|
+
if (isOpenXiangdaMode(config)) {
|
|
184
|
+
const mutation = await mutateOpenXiangdaFormConfig(config, accessToken, {
|
|
185
|
+
appType: targetAppType,
|
|
186
|
+
formUuid: apiPayload.formUuid,
|
|
187
|
+
endpoint: "schema",
|
|
188
|
+
operation: "schema",
|
|
189
|
+
expected,
|
|
190
|
+
payload: {
|
|
191
|
+
schema: apiPayload.schema,
|
|
192
|
+
packages: apiPayload.packages,
|
|
193
|
+
formType: apiPayload.formType,
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
const formMeta = await getOpenApiForm(config, accessToken, {
|
|
197
|
+
appType: targetAppType,
|
|
198
|
+
formUuid: apiPayload.formUuid,
|
|
199
|
+
});
|
|
200
|
+
if (Number(formMeta?.revision) !== Number(mutation.data?.revision)) {
|
|
201
|
+
const error = new Error(
|
|
202
|
+
`FORM_REVISION_CONFLICT: schema 激活返回 r${mutation.data?.revision},校验读取为 r${formMeta?.revision};未继续发布`,
|
|
203
|
+
);
|
|
204
|
+
error.code = "FORM_REVISION_CONFLICT";
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
207
|
+
const verified = {
|
|
208
|
+
...mutation.response,
|
|
209
|
+
data: {
|
|
210
|
+
...mutation.data,
|
|
211
|
+
tableName: formMeta?.tableName,
|
|
212
|
+
formFields: formMeta?.formFields,
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
assertSchemaSyncResult(verified, apiPayload.fieldCount);
|
|
216
|
+
return verified;
|
|
217
|
+
}
|
|
218
|
+
|
|
181
219
|
const response = await fetch(url, {
|
|
182
220
|
method: "POST",
|
|
183
221
|
headers: {
|
|
184
222
|
"Content-Type": "application/json",
|
|
185
|
-
|
|
186
|
-
? { Authorization: `Bearer ${accessToken}` }
|
|
187
|
-
: { "x-acs-dingtalk-access-token": accessToken }),
|
|
223
|
+
"x-acs-dingtalk-access-token": accessToken,
|
|
188
224
|
},
|
|
189
225
|
body: JSON.stringify(
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
:
|
|
197
|
-
|
|
198
|
-
appType: targetAppType,
|
|
199
|
-
formUuid: apiPayload.formUuid,
|
|
200
|
-
schema: apiPayload.schema,
|
|
201
|
-
packages: apiPayload.packages,
|
|
202
|
-
formType: apiPayload.formType,
|
|
203
|
-
},
|
|
226
|
+
{
|
|
227
|
+
userId: config.userId,
|
|
228
|
+
appType: targetAppType,
|
|
229
|
+
formUuid: apiPayload.formUuid,
|
|
230
|
+
schema: apiPayload.schema,
|
|
231
|
+
packages: apiPayload.packages,
|
|
232
|
+
formType: apiPayload.formType,
|
|
233
|
+
},
|
|
204
234
|
),
|
|
205
235
|
});
|
|
206
236
|
|
|
@@ -261,6 +291,7 @@ async function main() {
|
|
|
261
291
|
continue;
|
|
262
292
|
}
|
|
263
293
|
|
|
294
|
+
let releaseExpectation = null;
|
|
264
295
|
try {
|
|
265
296
|
if (!args.dryRun && !accessToken) {
|
|
266
297
|
accessToken = await getOpenApiAccessToken(config);
|
|
@@ -275,6 +306,7 @@ async function main() {
|
|
|
275
306
|
});
|
|
276
307
|
schema.formMeta.formUuid = ensured.formUuid;
|
|
277
308
|
schema.formMeta.appType = ensured.appType || schema.formMeta.appType;
|
|
309
|
+
releaseExpectation = ensured.releaseExpectation;
|
|
278
310
|
if (ensured.created || ensured.dryRunCreated) {
|
|
279
311
|
createdForms.add(form.name);
|
|
280
312
|
}
|
|
@@ -305,12 +337,20 @@ async function main() {
|
|
|
305
337
|
results.push({ name: form.name, success: true, dryRun: true });
|
|
306
338
|
} else {
|
|
307
339
|
try {
|
|
308
|
-
const response = await sendToApi(
|
|
340
|
+
const response = await sendToApi(
|
|
341
|
+
apiPayload,
|
|
342
|
+
config,
|
|
343
|
+
accessToken,
|
|
344
|
+
releaseExpectation,
|
|
345
|
+
);
|
|
309
346
|
markWorkspaceFormSchemaSynced(config, form.name, apiPayload.formUuid, {
|
|
310
347
|
title: schema.formMeta?.title || form.name,
|
|
311
348
|
formType: apiPayload.formType || schema.formMeta?.formType || "receipt",
|
|
312
349
|
fieldCount: apiPayload.fieldCount,
|
|
313
350
|
tableName: response?.data?.tableName,
|
|
351
|
+
revision: response?.data?.revision,
|
|
352
|
+
etag: response?.data?.etag,
|
|
353
|
+
activeFormReleaseHead: response?.data?.activeFormReleaseHead,
|
|
314
354
|
});
|
|
315
355
|
console.log(` ✅ 同步成功,tableName=${response.data.tableName}`);
|
|
316
356
|
results.push({ name: form.name, success: true, response });
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import crypto from "node:crypto";
|
|
3
4
|
import { getApiBaseUrl, rootDir } from "./load-config.mjs";
|
|
4
5
|
|
|
5
6
|
const PROJECT_STATE_FILE = ".openxiangda/state.json";
|
|
@@ -124,6 +125,44 @@ export function markWorkspaceFormSchemaSynced(config, formName, formUuid, extra
|
|
|
124
125
|
});
|
|
125
126
|
}
|
|
126
127
|
|
|
128
|
+
export function markWorkspaceFormBundlePublished(
|
|
129
|
+
config,
|
|
130
|
+
formName,
|
|
131
|
+
formUuid,
|
|
132
|
+
extra = {},
|
|
133
|
+
) {
|
|
134
|
+
return saveWorkspaceFormBinding(config, formName, formUuid, {
|
|
135
|
+
...extra,
|
|
136
|
+
bundlePublishedAt: new Date().toISOString(),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function normalizeFormReleaseExpectation(value) {
|
|
141
|
+
const revision = Number(value?.revision);
|
|
142
|
+
if (!Number.isSafeInteger(revision) || revision < 1) return null;
|
|
143
|
+
const head = value?.activeFormReleaseHead || value?.expectedParent || {};
|
|
144
|
+
return {
|
|
145
|
+
revision,
|
|
146
|
+
etag: String(value?.etag || "").trim() || undefined,
|
|
147
|
+
activeFormReleaseHead: {
|
|
148
|
+
releaseId: head?.releaseId || null,
|
|
149
|
+
releaseHash: head?.releaseHash || null,
|
|
150
|
+
revision: Number(head?.revision ?? revision),
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function formReleaseStateFields(value) {
|
|
156
|
+
const expectation = normalizeFormReleaseExpectation(value);
|
|
157
|
+
return expectation
|
|
158
|
+
? {
|
|
159
|
+
revision: expectation.revision,
|
|
160
|
+
...(expectation.etag ? { etag: expectation.etag } : {}),
|
|
161
|
+
activeFormReleaseHead: expectation.activeFormReleaseHead,
|
|
162
|
+
}
|
|
163
|
+
: {};
|
|
164
|
+
}
|
|
165
|
+
|
|
127
166
|
function readExportedStringConstant(filePath, exportName) {
|
|
128
167
|
if (!filePath || !fs.existsSync(filePath)) return "";
|
|
129
168
|
const content = fs.readFileSync(filePath, "utf-8");
|
|
@@ -344,10 +383,25 @@ export function isOpenXiangdaMode(config) {
|
|
|
344
383
|
return Boolean(config.openXiangdaAccessToken);
|
|
345
384
|
}
|
|
346
385
|
|
|
386
|
+
export function getOpenXiangdaPublishContextHeaders(env = process.env) {
|
|
387
|
+
const values = [
|
|
388
|
+
["x-openxiangda-publish-lease-id", env.OPENXIANGDA_PUBLISH_LEASE_ID],
|
|
389
|
+
["x-openxiangda-change-baseline-id", env.OPENXIANGDA_CHANGE_BASELINE_ID],
|
|
390
|
+
["x-openxiangda-change-id", env.OPENXIANGDA_CHANGE_ID],
|
|
391
|
+
["x-openxiangda-client-session-id", env.OPENXIANGDA_CLIENT_SESSION_ID],
|
|
392
|
+
];
|
|
393
|
+
return Object.fromEntries(
|
|
394
|
+
values
|
|
395
|
+
.map(([name, value]) => [name, String(value || "").trim()])
|
|
396
|
+
.filter(([, value]) => Boolean(value)),
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
347
400
|
function getAuthHeaders(config, accessToken) {
|
|
348
401
|
if (isOpenXiangdaMode(config)) {
|
|
349
402
|
return {
|
|
350
403
|
Authorization: `Bearer ${accessToken || config.openXiangdaAccessToken}`,
|
|
404
|
+
...getOpenXiangdaPublishContextHeaders(),
|
|
351
405
|
};
|
|
352
406
|
}
|
|
353
407
|
return {
|
|
@@ -355,6 +409,175 @@ function getAuthHeaders(config, accessToken) {
|
|
|
355
409
|
};
|
|
356
410
|
}
|
|
357
411
|
|
|
412
|
+
function canonicalize(value) {
|
|
413
|
+
if (Array.isArray(value)) return value.map((item) => canonicalize(item));
|
|
414
|
+
if (!value || typeof value !== "object") return value;
|
|
415
|
+
return Object.fromEntries(
|
|
416
|
+
Object.keys(value)
|
|
417
|
+
.sort()
|
|
418
|
+
.filter((key) => value[key] !== undefined)
|
|
419
|
+
.map((key) => [key, canonicalize(value[key])]),
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function canonicalJsonSha256(value) {
|
|
424
|
+
return crypto
|
|
425
|
+
.createHash("sha256")
|
|
426
|
+
.update(JSON.stringify(canonicalize(value)))
|
|
427
|
+
.digest("hex");
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function normalizeFrozenFormSnapshot(value, label = "Form snapshot") {
|
|
431
|
+
const expectation = normalizeFormReleaseExpectation(value);
|
|
432
|
+
if (!expectation?.etag) {
|
|
433
|
+
const error = new Error(
|
|
434
|
+
`FORM_REVISION_CONFLICT: ${label} 未返回有效 revision/etag,拒绝配置写入`,
|
|
435
|
+
);
|
|
436
|
+
error.code = "FORM_REVISION_CONFLICT";
|
|
437
|
+
throw error;
|
|
438
|
+
}
|
|
439
|
+
if (expectation.activeFormReleaseHead.revision !== expectation.revision) {
|
|
440
|
+
const error = new Error(
|
|
441
|
+
`FORM_REVISION_CONFLICT: ${label} 的 head revision 与 Form revision 不一致`,
|
|
442
|
+
);
|
|
443
|
+
error.code = "FORM_REVISION_CONFLICT";
|
|
444
|
+
throw error;
|
|
445
|
+
}
|
|
446
|
+
return {
|
|
447
|
+
...value,
|
|
448
|
+
...expectation,
|
|
449
|
+
expectedParent: expectation.activeFormReleaseHead,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function assertExpectedFormSnapshot(frozen, expected) {
|
|
454
|
+
const normalizedExpected = normalizeFormReleaseExpectation(expected);
|
|
455
|
+
if (!normalizedExpected) return;
|
|
456
|
+
const headMatches =
|
|
457
|
+
canonicalJsonSha256(normalizedExpected.activeFormReleaseHead) ===
|
|
458
|
+
canonicalJsonSha256(frozen.expectedParent);
|
|
459
|
+
const etagMatches =
|
|
460
|
+
!normalizedExpected.etag || normalizedExpected.etag === frozen.etag;
|
|
461
|
+
if (
|
|
462
|
+
normalizedExpected.revision !== frozen.revision ||
|
|
463
|
+
!headMatches ||
|
|
464
|
+
!etagMatches
|
|
465
|
+
) {
|
|
466
|
+
const error = new Error(
|
|
467
|
+
`FORM_REVISION_CONFLICT: 工作区冻结 revision=r${normalizedExpected.revision},平台当前 revision=r${frozen.revision};未执行写入,请先合并并重新规划`,
|
|
468
|
+
);
|
|
469
|
+
error.code = "FORM_REVISION_CONFLICT";
|
|
470
|
+
throw error;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
async function readJsonResponse(response) {
|
|
475
|
+
return await response.json().catch(() => null);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
export async function getOpenXiangdaFormSnapshot(
|
|
479
|
+
config,
|
|
480
|
+
accessToken,
|
|
481
|
+
{ appType, formUuid },
|
|
482
|
+
) {
|
|
483
|
+
if (!isOpenXiangdaMode(config)) return null;
|
|
484
|
+
const targetAppType = appType || config.appType;
|
|
485
|
+
const apiBase = getApiBaseUrl(config);
|
|
486
|
+
const response = await fetch(
|
|
487
|
+
`${apiBase}/openxiangda-api/v1/apps/${encodeURIComponent(targetAppType)}/forms/${encodeURIComponent(formUuid)}/snapshot`,
|
|
488
|
+
{
|
|
489
|
+
method: "GET",
|
|
490
|
+
headers: {
|
|
491
|
+
"Content-Type": "application/json",
|
|
492
|
+
...getAuthHeaders(config, accessToken),
|
|
493
|
+
},
|
|
494
|
+
},
|
|
495
|
+
);
|
|
496
|
+
const body = await readJsonResponse(response);
|
|
497
|
+
if (!response.ok || body?.code !== 200) {
|
|
498
|
+
const message = body?.message || response.statusText || "unknown error";
|
|
499
|
+
const error = new Error(
|
|
500
|
+
`查询 Form snapshot 失败: HTTP ${response.status} ${message}`,
|
|
501
|
+
);
|
|
502
|
+
error.code = body?.errorCode || "FORM_REVISION_CONFLICT";
|
|
503
|
+
throw error;
|
|
504
|
+
}
|
|
505
|
+
return normalizeFrozenFormSnapshot(body.data);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
export async function mutateOpenXiangdaFormConfig(
|
|
509
|
+
config,
|
|
510
|
+
accessToken,
|
|
511
|
+
{
|
|
512
|
+
appType,
|
|
513
|
+
formUuid,
|
|
514
|
+
endpoint,
|
|
515
|
+
operation,
|
|
516
|
+
payload,
|
|
517
|
+
expected,
|
|
518
|
+
artifactId,
|
|
519
|
+
},
|
|
520
|
+
) {
|
|
521
|
+
if (!isOpenXiangdaMode(config)) {
|
|
522
|
+
throw new Error("mutateOpenXiangdaFormConfig 仅用于 OpenXiangda 模式");
|
|
523
|
+
}
|
|
524
|
+
const targetAppType = appType || config.appType;
|
|
525
|
+
const frozen = await getOpenXiangdaFormSnapshot(config, accessToken, {
|
|
526
|
+
appType: targetAppType,
|
|
527
|
+
formUuid,
|
|
528
|
+
});
|
|
529
|
+
assertExpectedFormSnapshot(frozen, expected);
|
|
530
|
+
const expectedParent = frozen.expectedParent;
|
|
531
|
+
const mutationPayload = payload || {};
|
|
532
|
+
const stableArtifactId =
|
|
533
|
+
artifactId ||
|
|
534
|
+
`form:${String(operation || endpoint || "config")
|
|
535
|
+
.replace(/[^A-Za-z0-9_.-]+/g, "-")
|
|
536
|
+
.slice(0, 48)}:${canonicalJsonSha256({
|
|
537
|
+
appType: targetAppType,
|
|
538
|
+
formUuid,
|
|
539
|
+
operation: operation || endpoint,
|
|
540
|
+
expectedParent,
|
|
541
|
+
payload: mutationPayload,
|
|
542
|
+
}).slice(0, 32)}`;
|
|
543
|
+
const apiBase = getApiBaseUrl(config);
|
|
544
|
+
const url = `${apiBase}/openxiangda-api/v1/apps/${encodeURIComponent(targetAppType)}/forms/${encodeURIComponent(formUuid)}/${String(endpoint || "").replace(/^\/+/, "")}`;
|
|
545
|
+
const response = await fetch(url, {
|
|
546
|
+
method: "POST",
|
|
547
|
+
headers: {
|
|
548
|
+
"Content-Type": "application/json",
|
|
549
|
+
...getAuthHeaders(config, accessToken),
|
|
550
|
+
"If-Match": frozen.etag,
|
|
551
|
+
},
|
|
552
|
+
body: JSON.stringify({
|
|
553
|
+
...mutationPayload,
|
|
554
|
+
artifactId: stableArtifactId,
|
|
555
|
+
expectedRevision: frozen.revision,
|
|
556
|
+
expectedParent,
|
|
557
|
+
}),
|
|
558
|
+
});
|
|
559
|
+
const body = await readJsonResponse(response);
|
|
560
|
+
if (!response.ok || body?.code !== 200) {
|
|
561
|
+
const message = body?.message || response.statusText || "unknown error";
|
|
562
|
+
const error = new Error(
|
|
563
|
+
`${body?.errorCode || "FORM_CONFIG_WRITE_FAILED"}: HTTP ${response.status} ${message}`,
|
|
564
|
+
);
|
|
565
|
+
error.code = body?.errorCode || "FORM_CONFIG_WRITE_FAILED";
|
|
566
|
+
error.status = response.status;
|
|
567
|
+
error.data = body?.data;
|
|
568
|
+
throw error;
|
|
569
|
+
}
|
|
570
|
+
const resultExpectation = normalizeFormReleaseExpectation(body.data);
|
|
571
|
+
if (!resultExpectation) {
|
|
572
|
+
const error = new Error(
|
|
573
|
+
"FORM_REVISION_CONFLICT: Form 配置写响应未返回 revision/head",
|
|
574
|
+
);
|
|
575
|
+
error.code = "FORM_REVISION_CONFLICT";
|
|
576
|
+
throw error;
|
|
577
|
+
}
|
|
578
|
+
return { response: body, data: body.data, frozen, url };
|
|
579
|
+
}
|
|
580
|
+
|
|
358
581
|
export async function getOpenApiAccessToken(config) {
|
|
359
582
|
assertRequiredOpenApiConfig(config);
|
|
360
583
|
if (isOpenXiangdaMode(config)) {
|
|
@@ -414,6 +637,7 @@ export async function createOpenApiForm(config, accessToken, formOptions) {
|
|
|
414
637
|
payload.createMenu = formOptions.createMenu !== false;
|
|
415
638
|
payload.menuParentId = config.menu?.parentId || "";
|
|
416
639
|
payload.menuIcon = config.menu?.icon || "";
|
|
640
|
+
payload.expectedRevision = 0;
|
|
417
641
|
} else {
|
|
418
642
|
payload.userId = config.userId;
|
|
419
643
|
payload.appType = targetAppType;
|
|
@@ -428,6 +652,7 @@ export async function createOpenApiForm(config, accessToken, formOptions) {
|
|
|
428
652
|
headers: {
|
|
429
653
|
"Content-Type": "application/json",
|
|
430
654
|
...getAuthHeaders(config, accessToken),
|
|
655
|
+
...(isOpenXiangdaMode(config) ? { "If-Match": "0" } : {}),
|
|
431
656
|
},
|
|
432
657
|
body: JSON.stringify(payload),
|
|
433
658
|
});
|
|
@@ -444,7 +669,8 @@ export async function createOpenApiForm(config, accessToken, formOptions) {
|
|
|
444
669
|
throw new Error(`创建表单失败: HTTP ${res.status} ${message}`);
|
|
445
670
|
}
|
|
446
671
|
|
|
447
|
-
const
|
|
672
|
+
const responseData = body?.data || body;
|
|
673
|
+
const data = responseData?.form || responseData;
|
|
448
674
|
const formUuid = data?.formUuid;
|
|
449
675
|
if (!formUuid) {
|
|
450
676
|
throw new Error("创建表单成功但响应中没有 formUuid");
|
|
@@ -453,6 +679,10 @@ export async function createOpenApiForm(config, accessToken, formOptions) {
|
|
|
453
679
|
return {
|
|
454
680
|
...data,
|
|
455
681
|
formUuid,
|
|
682
|
+
revision: data?.revision ?? responseData?.revision,
|
|
683
|
+
etag: data?.etag ?? responseData?.etag,
|
|
684
|
+
activeFormReleaseHead:
|
|
685
|
+
responseData?.activeFormReleaseHead || data?.activeFormReleaseHead,
|
|
456
686
|
};
|
|
457
687
|
}
|
|
458
688
|
|
|
@@ -643,6 +873,9 @@ export async function ensureSchemaFormUuid({
|
|
|
643
873
|
const meta = readSchemaMeta(schemaPath);
|
|
644
874
|
const targetAppType = resolveTargetAppType(config, meta);
|
|
645
875
|
const workspaceBinding = readWorkspaceFormBinding(config, formName);
|
|
876
|
+
const workspaceReleaseExpectation = normalizeFormReleaseExpectation(
|
|
877
|
+
workspaceBinding?.form,
|
|
878
|
+
);
|
|
646
879
|
const effectiveMeta = workspaceBinding?.form?.formUuid
|
|
647
880
|
? {
|
|
648
881
|
...meta,
|
|
@@ -658,6 +891,7 @@ export async function ensureSchemaFormUuid({
|
|
|
658
891
|
|
|
659
892
|
if (effectiveMeta.formUuid && !staleOpenXiangdaBinding) {
|
|
660
893
|
let created = false;
|
|
894
|
+
let createdForm = null;
|
|
661
895
|
if (!dryRun) {
|
|
662
896
|
const token = accessToken || (await getOpenApiAccessToken(config));
|
|
663
897
|
const existingForm = await getOpenApiForm(config, token, {
|
|
@@ -666,7 +900,7 @@ export async function ensureSchemaFormUuid({
|
|
|
666
900
|
});
|
|
667
901
|
|
|
668
902
|
if (!existingForm) {
|
|
669
|
-
await createOpenApiForm(config, token, {
|
|
903
|
+
createdForm = await createOpenApiForm(config, token, {
|
|
670
904
|
name: effectiveMeta.title || formName,
|
|
671
905
|
appType: targetAppType,
|
|
672
906
|
formType: effectiveMeta.formType || "receipt",
|
|
@@ -699,6 +933,7 @@ export async function ensureSchemaFormUuid({
|
|
|
699
933
|
|
|
700
934
|
saveWorkspaceFormBinding(config, formName, effectiveMeta.formUuid, {
|
|
701
935
|
title: effectiveMeta.title || formName,
|
|
936
|
+
...formReleaseStateFields(createdForm),
|
|
702
937
|
});
|
|
703
938
|
}
|
|
704
939
|
|
|
@@ -707,6 +942,9 @@ export async function ensureSchemaFormUuid({
|
|
|
707
942
|
appType: targetAppType,
|
|
708
943
|
created,
|
|
709
944
|
dryRunCreated: false,
|
|
945
|
+
releaseExpectation:
|
|
946
|
+
normalizeFormReleaseExpectation(createdForm) ||
|
|
947
|
+
workspaceReleaseExpectation,
|
|
710
948
|
};
|
|
711
949
|
}
|
|
712
950
|
|
|
@@ -745,6 +983,7 @@ export async function ensureSchemaFormUuid({
|
|
|
745
983
|
if (isOpenXiangdaMode(config)) {
|
|
746
984
|
saveWorkspaceFormBinding(config, formName, createdForm.formUuid, {
|
|
747
985
|
title: createOptions.name,
|
|
986
|
+
...formReleaseStateFields(createdForm),
|
|
748
987
|
});
|
|
749
988
|
} else {
|
|
750
989
|
syncCreatedFormMeta(schemaPath, meta, targetAppType, createdForm.formUuid);
|
|
@@ -775,5 +1014,6 @@ export async function ensureSchemaFormUuid({
|
|
|
775
1014
|
created: true,
|
|
776
1015
|
menuCreated: menuResult.created,
|
|
777
1016
|
dryRunCreated: false,
|
|
1017
|
+
releaseExpectation: normalizeFormReleaseExpectation(createdForm),
|
|
778
1018
|
};
|
|
779
1019
|
}
|
|
@@ -11,9 +11,10 @@ You are editing engineering-managed resource manifests under `src/resources/`. L
|
|
|
11
11
|
## Commands
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
openxiangda resource validate --profile <name>
|
|
15
|
-
openxiangda resource plan --profile <name>
|
|
16
|
-
openxiangda resource publish --profile <name>
|
|
14
|
+
openxiangda resource validate <type> --only <codes> --profile <name>
|
|
15
|
+
openxiangda resource plan <type> --only <codes> --profile <name>
|
|
16
|
+
openxiangda resource publish <type> --only <codes> --profile <name>
|
|
17
|
+
# one target: replace --only <codes> with --code <code>
|
|
17
18
|
openxiangda resource typegen --profile <name>
|
|
18
19
|
```
|
|
19
20
|
|
|
@@ -23,3 +24,6 @@ openxiangda resource typegen --profile <name>
|
|
|
23
24
|
- Guest access to forms, dataViews, functions, and connectors requires explicit policy `grants`.
|
|
24
25
|
- Connector secrets and third-party credentials belong in the platform backend, never in manifests or page source.
|
|
25
26
|
- Formal changes should keep Git as the source of truth: edit manifests, validate, plan, then publish.
|
|
27
|
+
- `resource plan` and publish dry-runs are GET/HEAD-only. On `READ_ONLY_AUTH_REQUIRED`, run `openxiangda auth refresh --profile <name>` or log in again before retrying; never refresh inside the plan.
|
|
28
|
+
- Source-triggered Function/Automation publishing patches only source fields on the server and preserves online bindings/contracts/metadata/state. Whole-manifest replacement requires `--replace-manifest --reason "..."`.
|
|
29
|
+
- Formal promotion runs `release begin --change` (freeze Git + remote baseline, preflight the complete set) through `release end`. Reconcile `SOURCE_BASE_DIVERGED` / `RESOURCE_FIELD_CONFLICT`; never retry an old full payload with a fresh revision.
|
|
@@ -11,10 +11,10 @@ This is an OpenXiangda React SPA workspace. See [AGENTS.md](mdc:AGENTS.md) for f
|
|
|
11
11
|
|
|
12
12
|
| User says | Command |
|
|
13
13
|
|---|---|
|
|
14
|
-
| publish resources | `openxiangda resource publish --profile <name>` |
|
|
15
|
-
| deploy frontend | `openxiangda runtime deploy --profile <name
|
|
14
|
+
| publish resources | `openxiangda resource publish <type> --only <codes> --profile <name> --change <change>` |
|
|
15
|
+
| deploy frontend | `openxiangda runtime deploy --profile <name> --no-activate` |
|
|
16
16
|
| full deploy | `pnpm deploy -- --profile <name>` |
|
|
17
|
-
| diff / plan | `openxiangda resource plan --profile <name>` |
|
|
17
|
+
| diff / plan | `openxiangda resource plan <type> --only <codes> --profile <name>` |
|
|
18
18
|
| diagnose | `openxiangda doctor --profile <name> --json` |
|
|
19
19
|
| external backend Open API | `openxiangda-open-api` + `openxiangda open-api spec describe ...` |
|
|
20
20
|
|
|
@@ -22,10 +22,16 @@ This is an OpenXiangda React SPA workspace. See [AGENTS.md](mdc:AGENTS.md) for f
|
|
|
22
22
|
|
|
23
23
|
- Writes and deploys must pass `--profile <name>` explicitly.
|
|
24
24
|
- Architecture-class work is plan-gated: run `openxiangda design gates --topic <code> --json` and wait for confirmation.
|
|
25
|
-
-
|
|
25
|
+
- Give every task an isolated worktree/branch and one explicit change. `sdd context` claims the current worktree for `CODEX_THREAD_ID`; on `WORKTREE_OWNED_BY_ANOTHER_TASK`, switch worktrees. Keep the same change for plan/check/verify/publish/archive.
|
|
26
|
+
- L0 read-only/docs/tests need no SDD; L1 narrow reversible fixes record exact scope without a redundant second confirmation; schema, business Functions, Automation/Workflow, permissions, auth/public access, data writes, and runtime/config are L2/L3 full-SDD work. Live evidence/archive are post-release stages.
|
|
27
|
+
- Very small copy/style/binding changes may use `openxiangda sdd quick <change> ...`; quick mode records exact low-risk scope without a redundant proposal/approval loop when the user already requested it.
|
|
28
|
+
- Use staged verification: `--stage implementation` while developing, `--stage prepublish` before promotion, and `--stage postpublish` after activation.
|
|
29
|
+
- Before platform writes, run `openxiangda release begin --change <change> --profile <name>` to freeze Git + remote field heads and preflight the complete target set; finish or abandon with `openxiangda release end --profile <name>`.
|
|
26
30
|
- Account/role/permission/RBAC/organization-account/query-param authorization work must run `openxiangda design gates --topic permissions --json`, choose `managed-platform-account`, `existing-platform-user-assignment`, `static-role-permission`, or `query-param-context`, and write the permission matrix.
|
|
27
31
|
- Roles that create roles, assign members, grant API permissions, maintain permission groups, or manage organization accounts must declare `apiPermissionCodes`, such as `app:role:manage`, `app:page-permission-group:manage`, `app:form-permission-group:manage`, and `app:organization:manage`.
|
|
28
32
|
- `src/resources/**` is the resource source of truth; use `validate -> plan -> publish`.
|
|
33
|
+
- Select logical resource codes with `--only`, or one code with `--code`; type-wide/app-wide release requires an approved dependency closure.
|
|
34
|
+
- Source-triggered Function/Automation publishing uses server-side source-field PATCH and preserves online bindings, contracts, metadata, trigger/view configuration, and enabled/published state. Whole-manifest replacement requires `--replace-manifest --reason "..."`. On `SOURCE_BASE_DIVERGED` or `RESOURCE_FIELD_CONFLICT`, reconcile, rebuild, and re-plan.
|
|
29
35
|
- React routes live in `src/app/router.tsx`; frontend artifacts are deployed with `openxiangda runtime deploy`.
|
|
30
36
|
- Use `AttachmentField` / `ImageField` for form-context previews, and `AttachmentPreviewList` / `ImagePreviewGrid` / `useFilePreview` for standalone custom pages. Do not fake form context, import internal preview implementations, or maintain local previewable-extension lists.
|
|
31
37
|
- Backend permissions, public-access grants, and App Function role/scope checks are authoritative.
|
|
@@ -36,6 +42,7 @@ This is an OpenXiangda React SPA workspace. See [AGENTS.md](mdc:AGENTS.md) for f
|
|
|
36
42
|
|
|
37
43
|
- Do not run `lowcode-workspace publish-all`, `pnpm publish:all`, or `pnpm openxiangda:publish` directly.
|
|
38
44
|
- Do not deploy without `--profile`.
|
|
45
|
+
- Do not hot-patch a temporary release copy or activate a Runtime built from an unmerged task branch.
|
|
39
46
|
- Do not use legacy `?publicAccess=guest`, `isRenderNav`, or workbench page parameters for new React SPA routes.
|
|
40
47
|
- Do not rely on frontend-only button hiding, query-param checks, hardcoded roles, mock permissions, fake account IDs, or `PermissionBoundary` alone for real authorization.
|
|
41
48
|
- Do not assign a business admin role without the role-setting API permissions needed for role/member management.
|
|
@@ -11,9 +11,10 @@ You are editing engineering-managed resource manifests under `src/resources/`. L
|
|
|
11
11
|
## Commands
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
openxiangda resource validate --profile <name>
|
|
15
|
-
openxiangda resource plan --profile <name>
|
|
16
|
-
openxiangda resource publish --profile <name>
|
|
14
|
+
openxiangda resource validate <type> --only <codes> --profile <name>
|
|
15
|
+
openxiangda resource plan <type> --only <codes> --profile <name>
|
|
16
|
+
openxiangda resource publish <type> --only <codes> --profile <name>
|
|
17
|
+
# one target: replace --only <codes> with --code <code>
|
|
17
18
|
openxiangda resource typegen --profile <name>
|
|
18
19
|
```
|
|
19
20
|
|
|
@@ -23,3 +24,6 @@ openxiangda resource typegen --profile <name>
|
|
|
23
24
|
- Guest access to forms, dataViews, functions, and connectors requires explicit policy `grants`.
|
|
24
25
|
- Connector secrets and third-party credentials belong in the platform backend, never in manifests or page source.
|
|
25
26
|
- Formal changes should keep Git as the source of truth: edit manifests, validate, plan, then publish.
|
|
27
|
+
- `resource plan` and publish dry-runs are GET/HEAD-only. On `READ_ONLY_AUTH_REQUIRED`, run `openxiangda auth refresh --profile <name>` or log in again before retrying; never refresh inside the plan.
|
|
28
|
+
- Source-triggered Function/Automation publishing patches only source fields on the server and preserves online bindings/contracts/metadata/state. Whole-manifest replacement requires `--replace-manifest --reason "..."`.
|
|
29
|
+
- Formal promotion runs `release begin --change` (freeze Git + remote baseline, preflight the complete set) through `release end`. Reconcile `SOURCE_BASE_DIVERGED` / `RESOURCE_FIELD_CONFLICT`; never retry an old full payload with a fresh revision.
|