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.
Files changed (62) hide show
  1. package/README.md +39 -13
  2. package/lib/change-baseline.js +1077 -0
  3. package/lib/cli.js +12002 -6723
  4. package/lib/config.js +272 -7
  5. package/lib/http.js +2 -0
  6. package/lib/js-code-build.js +61 -0
  7. package/lib/publish-context.js +98 -0
  8. package/lib/publish-lease-heartbeat.js +182 -0
  9. package/lib/publish-lease.js +190 -0
  10. package/lib/release-telemetry.js +182 -0
  11. package/lib/runtime-lineage.js +297 -0
  12. package/lib/sdd.js +567 -35
  13. package/lib/source-dependencies.js +742 -0
  14. package/lib/workspace-init.js +6 -1
  15. package/lib/worktree-owner.js +219 -0
  16. package/openxiangda-skills/SKILL.md +103 -228
  17. package/openxiangda-skills/references/architecture-design.md +2 -1
  18. package/openxiangda-skills/references/openxiangda-api.md +27 -0
  19. package/openxiangda-skills/references/pages/page-sdk.md +11 -1
  20. package/openxiangda-skills/references/resource-manifest-cheatsheet.md +12 -2
  21. package/openxiangda-skills/references/workspace-state.md +2 -0
  22. package/openxiangda-skills/skills/openxiangda-core/SKILL.md +77 -243
  23. package/openxiangda-skills/skills/openxiangda-form/SKILL.md +8 -0
  24. package/openxiangda-skills/skills/openxiangda-page/SKILL.md +18 -0
  25. package/package.json +20 -1
  26. package/packages/sdk/dist/{ProcessPreview-DMkzccq4.d.mts → ProcessPreview-Dux73lY_.d.mts} +5 -0
  27. package/packages/sdk/dist/{ProcessPreview-DMkzccq4.d.ts → ProcessPreview-Dux73lY_.d.ts} +5 -0
  28. package/packages/sdk/dist/components/index.cjs +29 -1
  29. package/packages/sdk/dist/components/index.cjs.map +1 -1
  30. package/packages/sdk/dist/components/index.d.mts +4 -4
  31. package/packages/sdk/dist/components/index.d.ts +4 -4
  32. package/packages/sdk/dist/components/index.mjs +29 -1
  33. package/packages/sdk/dist/components/index.mjs.map +1 -1
  34. package/packages/sdk/dist/{dataManagementApi-C9O-Bb0j.d.ts → dataManagementApi-BNLH2o7f.d.ts} +2 -1
  35. package/packages/sdk/dist/{dataManagementApi-gpZkgRDM.d.mts → dataManagementApi-BOcIx4U-.d.mts} +2 -1
  36. package/packages/sdk/dist/runtime/index.cjs +150 -22
  37. package/packages/sdk/dist/runtime/index.cjs.map +1 -1
  38. package/packages/sdk/dist/runtime/index.d.mts +3 -3
  39. package/packages/sdk/dist/runtime/index.d.ts +3 -3
  40. package/packages/sdk/dist/runtime/index.mjs +150 -22
  41. package/packages/sdk/dist/runtime/index.mjs.map +1 -1
  42. package/packages/sdk/dist/runtime/react.cjs +121 -21
  43. package/packages/sdk/dist/runtime/react.cjs.map +1 -1
  44. package/packages/sdk/dist/runtime/react.d.mts +15 -2
  45. package/packages/sdk/dist/runtime/react.d.ts +15 -2
  46. package/packages/sdk/dist/runtime/react.mjs +121 -21
  47. package/packages/sdk/dist/runtime/react.mjs.map +1 -1
  48. package/packages/sdk/src/build-source/scripts/register.mjs +54 -23
  49. package/packages/sdk/src/build-source/scripts/sync-schema.mjs +59 -19
  50. package/packages/sdk/src/build-source/scripts/utils/form-api.mjs +242 -2
  51. package/templates/openxiangda-react-spa/.cursor/rules/openxiangda-resources.mdc +7 -3
  52. package/templates/openxiangda-react-spa/.cursor/rules/openxiangda.mdc +11 -4
  53. package/templates/openxiangda-react-spa/.qoder/rules/openxiangda-resources.md +7 -3
  54. package/templates/openxiangda-react-spa/.qoder/rules/openxiangda.md +10 -4
  55. package/templates/openxiangda-react-spa/AGENTS.md +25 -12
  56. package/templates/openxiangda-react-spa/scripts/build-js-code.mjs +344 -60
  57. package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda-resources.mdc +5 -1
  58. package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda.mdc +8 -2
  59. package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda-resources.md +8 -4
  60. package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda.md +8 -2
  61. package/templates/sy-lowcode-app-workspace/AGENTS.md +14 -4
  62. 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
- const response = await fetch(getFormPublishUrl(appType, formUuid), {
156
- method: "POST",
157
- headers: {
158
- "Content-Type": "application/json",
159
- ...(isOpenXiangdaMode(config)
160
- ? { Authorization: `Bearer ${await getAccessToken()}` }
161
- : { "x-acs-dingtalk-access-token": await getAccessToken() }),
162
- },
163
- body: JSON.stringify(
164
- isOpenXiangdaMode(config)
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
- : payload,
174
- ),
175
- });
176
- const body = await response.json().catch(() => null);
177
- if (!response.ok || body?.code !== 200) {
178
- console.error(
179
- ` ✗ 表单 ${formName}: HTTP ${response.status} ${body?.message || response.statusText}`,
180
- );
181
- failed += 1;
182
- continue;
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
- ? { Authorization: `Bearer ${await getAccessToken()}` }
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
- ...(isOpenXiangdaMode(config)
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
- isOpenXiangdaMode(config)
191
- ? {
192
- schema: apiPayload.schema,
193
- packages: apiPayload.packages,
194
- formType: apiPayload.formType,
195
- }
196
- : {
197
- userId: config.userId,
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(apiPayload, config, accessToken);
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 data = body?.data?.form || body?.data || body;
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
- - High-risk changes must start with `openxiangda sdd context --json`, use an approved change, complete `coverage.json`, `release.json`, `evidence.md`, and `tasks.md`, then run `openxiangda sdd verify <change> --changed` before release.
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.