openxiangda 1.0.162 → 1.0.164

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 (33) hide show
  1. package/README.md +6 -2
  2. package/lib/change-baseline.js +88 -2
  3. package/lib/cli.js +565 -73
  4. package/lib/js-code-build.js +120 -8
  5. package/lib/publish-lease.js +13 -1
  6. package/lib/release-mainline.js +545 -0
  7. package/lib/runtime-lineage.js +19 -12
  8. package/lib/sdd.js +137 -0
  9. package/lib/workspace-bootstrap.js +36 -7
  10. package/lib/workspace-init.js +12 -3
  11. package/lib/worktree-owner.js +13 -0
  12. package/openxiangda-skills/SKILL.md +9 -3
  13. package/openxiangda-skills/references/automation-v3.md +1 -1
  14. package/openxiangda-skills/references/resource-manifest-cheatsheet.md +1 -1
  15. package/openxiangda-skills/references/workflow-v3.md +1 -1
  16. package/openxiangda-skills/skills/openxiangda-core/SKILL.md +8 -2
  17. package/openxiangda-skills/skills/openxiangda-workflow-automation/SKILL.md +2 -2
  18. package/package.json +3 -1
  19. package/templates/openxiangda-react-spa/.cursor/rules/openxiangda-resources.mdc +2 -1
  20. package/templates/openxiangda-react-spa/.cursor/rules/openxiangda.mdc +2 -2
  21. package/templates/openxiangda-react-spa/.qoder/rules/openxiangda-resources.md +2 -1
  22. package/templates/openxiangda-react-spa/.qoder/rules/openxiangda.md +3 -3
  23. package/templates/openxiangda-react-spa/AGENTS.md +8 -6
  24. package/templates/openxiangda-react-spa/scripts/build-js-code.mjs +172 -20
  25. package/templates/openxiangda-react-spa/scripts/deploy.mjs +19 -39
  26. package/templates/openxiangda-react-spa/scripts/guard-publish.mjs +4 -5
  27. package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda-resources.mdc +3 -1
  28. package/templates/sy-lowcode-app-workspace/.cursor/rules/openxiangda.mdc +1 -1
  29. package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda-resources.md +3 -1
  30. package/templates/sy-lowcode-app-workspace/.qoder/rules/openxiangda.md +1 -1
  31. package/templates/sy-lowcode-app-workspace/AGENTS.md +4 -4
  32. package/templates/sy-lowcode-app-workspace/scripts/build-js-code.mjs +172 -20
  33. package/templates/sy-lowcode-app-workspace/scripts/guard-publish.mjs +4 -5
package/lib/cli.js CHANGED
@@ -24,6 +24,7 @@ const {
24
24
  buildGitBaseSourceArtifacts,
25
25
  canonicalJsonSha256,
26
26
  clearChangeBaseline,
27
+ clearChangeBaselineForReconciliation,
27
28
  getStoredChangeBaseline,
28
29
  isChangeBaselineOwnedByCurrentSession,
29
30
  readGitSourceBase,
@@ -52,9 +53,16 @@ const {
52
53
  normalizePublishContext,
53
54
  } = require('./publish-context');
54
55
  const {
56
+ assertRuntimeSourceClean,
55
57
  prepareRuntimeDeployLineage,
56
58
  resolveRuntimeRollbackBypass,
57
59
  } = require('./runtime-lineage');
60
+ const {
61
+ assertReleaseSourceIntegrated,
62
+ assertReleaseSourceRevisionStable,
63
+ inspectReleaseSourceIntegration,
64
+ prepareReleaseSourceRevision,
65
+ } = require('./release-mainline');
58
66
  const {
59
67
  analyzeManifestSourceDependencies,
60
68
  getImpactedResourceTargets,
@@ -101,6 +109,7 @@ const {
101
109
  getSddChangeScope,
102
110
  initSddWorkspace,
103
111
  proposeSddChange,
112
+ recordSddReleaseSourceRevision,
104
113
  syncSddChange,
105
114
  verifySddChange,
106
115
  } = require('./sdd');
@@ -202,7 +211,7 @@ Usage:
202
211
  openxiangda open-api credential list|get|create|update|rotate-secret [id] [--profile name] [--show-secret] [--yes] [--json]
203
212
  openxiangda design gates|template|review [--topic code] [--json]
204
213
  openxiangda sdd init|migrate|propose|quick|approve|status|context|verify|sync|archive [change] [--change id] [--json]
205
- openxiangda release begin|status|renew|end [--change id] [--profile name] [--json]
214
+ openxiangda release begin|status|integration-status|renew|end [--change id] [--profile name] [--json]
206
215
  openxiangda release backend-head|backend-list|backend-detail|backend-diff|backend-rollback|backend-abort|backend-retry [releaseId] [--profile name] [--json]
207
216
  openxiangda release app-capture|app-head|app-list|app-detail|app-diff|app-prepare|app-verify|app-activate|app-finalize|app-rollback|app-abort [releaseId] [--staged-resources-json <JSON|file>] [--activate-staged-children] [--profile name] [--json]
208
217
  openxiangda env [--profile name]
@@ -267,12 +276,14 @@ Usage:
267
276
 
268
277
  OpenXiangda 应用开发使用普通用户登录 token,不需要 AK/SK;外部后端集成使用 open-api skill 与 AK/SK。
269
278
  所有直接配置写入必须携带 --change <id>,或先执行 release begin --change <id>;dry-run、schema-plan、validate、query/invoke 等只读/数据面命令不会获取发布租约。
279
+ 正式发布会冻结 clean committed HEAD;上线后必须将该 SHA merge/fast-forward 并 push 到权威远端主分支,integration-status 通过后 normal release end 才会清理会话。
270
280
  表单页、流程表单页和代码页的主链路是 sy-lowcode-app-workspace + openxiangda workspace publish。
271
281
  新 React SPA 公开访问使用 public-access 命令和 src/resources/public-access;settings public-access 仅用于旧表单公开设置兼容/修复。
272
282
  JS_CODE V2 使用 trusted_node;AI 源码必须写在 src/js-code-nodes/<scriptCode>/index.ts,代码自动化源码写在 src/automations/<resourceCode>/index.ts。definition-json 中的 sourceFile.localPath 会在 validate/create/publish 时先 TS 校验、再构建并上传为快照。`);
273
283
  }
274
284
 
275
285
  const SUBCOMMAND_BOOLEAN_FLAGS = new Set([
286
+ '--check',
276
287
  '--all',
277
288
  '--activate',
278
289
  '--dry-run',
@@ -631,6 +642,7 @@ async function sdd(args) {
631
642
  ' - 在 Codex 中 propose/quick/context 会按 CODEX_THREAD_ID 占用当前 worktree;其他任务必须切换到独立 worktree。',
632
643
  ' - quick 仅用于 L1 精确小改动;Function quick 还必须提供完整 --risk-json,不能用 riskTier 标签绕过。',
633
644
  ' - verify stage 可选 implementation、prepublish、postpublish、archive;不传保持旧版全量校验。',
645
+ ' - 新 release 会把实际 publish HEAD 写入 release.json;archive(包括 --force)要求该 SHA 已进入权威远端主分支。',
634
646
  ].join('\n'));
635
647
  return;
636
648
  }
@@ -837,13 +849,14 @@ async function release(args) {
837
849
  const { flags, positional } = parseArgs(rest);
838
850
  if (wantsSubcommandHelp(subcommand, flags)) {
839
851
  print([
840
- '用法: openxiangda release begin|status|renew|end [--change id] [--profile name] [--json]',
852
+ '用法: openxiangda release begin|status|integration-status|renew|end [--change id] [--profile name] [--json]',
841
853
  ' openxiangda release backend-head|backend-list|backend-detail|backend-diff|backend-rollback|backend-abort|backend-retry [releaseId] [--profile name] [--json]',
842
854
  ' openxiangda release app-capture|app-head|app-list|app-detail|app-diff|app-prepare|app-verify|app-activate|app-finalize|app-rollback|app-abort [releaseId] [--staged-resources-json <JSON|file>] [--activate-staged-children] [--profile name] [--json]',
843
855
  '常用流程:',
844
856
  ' openxiangda release begin --change <id> --profile <name>',
845
857
  ' openxiangda resource publish function --only <code> --change <id> --profile <name>',
846
858
  ' openxiangda runtime deploy --change <id> --profile <name>',
859
+ ' openxiangda release integration-status --profile <name>',
847
860
  ' openxiangda release end --profile <name>',
848
861
  ' openxiangda release backend-head --profile <name>',
849
862
  ' openxiangda release backend-diff <fromReleaseId> <toReleaseId> --profile <name>',
@@ -854,6 +867,11 @@ async function release(args) {
854
867
  ' - 带 --change 的 resource/runtime 写命令在没有本地 lease 时会自动 begin,并在后续命令复用。',
855
868
  ' - 子 Runtime/Page/Backend/Form Release 默认先 stage;app-finalize 用 --staged-resources-json 覆盖 capture 中的 changed children,并在同一事务中原子激活。',
856
869
  ' - 默认 TTL 为 1800 秒;长异步发布每 30 秒检查并在到期前自动续租,续租失败会以 PUBLISH_LEASE_LOST 阻断后续写入。',
870
+ ' - begin 冻结 clean committed HEAD,并要求它包含权威远端主分支;发布期间 HEAD/源码不可变化。',
871
+ ' - 发布后先把冻结提交以保留 SHA 的 merge/fast-forward 合并并 push 到主分支;end 会在线验证后才清理会话。',
872
+ ' - integration-status --check 可作为 CI 门禁;尚未回合时返回非零。',
873
+ ' - 主线目标只能来自权威 Git remote 的实时默认 main/master;参数和环境变量不能改写。',
874
+ ' - 租约过期也会保留待回合证据;完成 merge/push 后仍可执行 end 清理。',
857
875
  ' - 同步本地构建会先续到平台最大 TTL,构建后再次校验;流程结束后显式 end。',
858
876
  ].join('\n'));
859
877
  return;
@@ -887,18 +905,26 @@ async function release(args) {
887
905
 
888
906
  if (subcommand === 'status') {
889
907
  const local = getStoredPublishLease(target);
908
+ const baseline = getStoredChangeBaseline(target, {
909
+ access: 'reconciliation-read',
910
+ });
890
911
  const clientSessionId =
891
912
  (local && isPublishLeaseOwnedByCurrentSession(local)
892
913
  ? local.clientSessionId
893
- : null) || createPublishClientSessionId();
894
- const remote = await requestWithAuth(
914
+ : null) ||
915
+ baseline?.clientSessionId ||
916
+ createPublishClientSessionId();
917
+ const remote = await fetchPublishLeaseStatus(
895
918
  config,
896
- target.profileName,
897
- `${publishLeaseApiPath(target, '/status')}?clientSessionId=${encodeURIComponent(clientSessionId)}`
919
+ target,
920
+ clientSessionId
898
921
  );
899
922
  const result = {
900
923
  remote,
901
924
  local,
925
+ releaseSourceRevision: baseline?.releaseSourceRevision || null,
926
+ integration: null,
927
+ integrationCheckRequired: Boolean(baseline?.releaseSourceRevision),
902
928
  localOwnedByCurrentSession: local
903
929
  ? isPublishLeaseOwnedByCurrentSession(local)
904
930
  : null,
@@ -908,6 +934,28 @@ async function release(args) {
908
934
  return;
909
935
  }
910
936
 
937
+ if (subcommand === 'integration-status') {
938
+ const baseline = getStoredChangeBaseline(target, {
939
+ access: 'reconciliation-read',
940
+ });
941
+ const sourceRevision = baseline?.releaseSourceRevision;
942
+ if (!sourceRevision) {
943
+ fail(
944
+ 'RELEASE_SOURCE_LINEAGE_REQUIRED: 当前 release 没有冻结的 releaseSourceRevision,不能证明发布提交已回到权威主分支'
945
+ );
946
+ }
947
+ const result = inspectReleaseSourceIntegration(
948
+ sourceRevision,
949
+ { cwd: process.cwd() }
950
+ );
951
+ if (flags.check && !result.integrated) {
952
+ assertReleaseSourceIntegrated(sourceRevision, { cwd: process.cwd() });
953
+ }
954
+ if (flags.json) return writeJson(result);
955
+ printReleaseIntegrationStatus(result);
956
+ return;
957
+ }
958
+
911
959
  if (subcommand === 'begin' || subcommand === 'acquire') {
912
960
  const changeId = positional[0] || readStringFlag(flags, 'change');
913
961
  if (!changeId) {
@@ -922,8 +970,13 @@ async function release(args) {
922
970
  `本工作区已有 change ${existing.changeId || '-'} 的发布租约,请先执行 openxiangda release end`
923
971
  );
924
972
  }
973
+ const pendingBaseline = getStoredChangeBaseline(target);
925
974
  const clientSessionId =
926
975
  existing?.clientSessionId ||
976
+ (pendingBaseline?.changeId === changeId &&
977
+ isChangeBaselineOwnedByCurrentSession(pendingBaseline)
978
+ ? pendingBaseline.clientSessionId
979
+ : null) ||
927
980
  readStringFlag(flags, 'client-session-id') ||
928
981
  createPublishClientSessionId();
929
982
  const baseline = await ensureChangeBaseline(
@@ -933,21 +986,29 @@ async function release(args) {
933
986
  flags,
934
987
  clientSessionId
935
988
  );
936
- const lease = existing
937
- ? await renewPublishLease(config, target, existing, flags)
938
- : await acquirePublishLease(config, target, changeId, {
939
- ...flags,
940
- 'client-session-id': clientSessionId,
941
- });
989
+ let lease;
990
+ try {
991
+ lease = existing
992
+ ? await renewPublishLease(config, target, existing, flags)
993
+ : await acquirePublishLease(config, target, changeId, {
994
+ ...flags,
995
+ 'client-session-id': clientSessionId,
996
+ });
997
+ } catch (error) {
998
+ clearRejectedPendingBaseline(target, baseline, error);
999
+ throw error;
1000
+ }
1001
+ const finalizedBaseline = finalizeReleaseBaseline(target, baseline);
942
1002
  const result = {
943
1003
  ...lease,
944
- baselineId: baseline?.baselineId,
945
- baselineHeadDigest: baseline?.headDigest,
946
- sourceBase: baseline?.sourceBase,
1004
+ baselineId: finalizedBaseline?.baselineId,
1005
+ baselineHeadDigest: finalizedBaseline?.headDigest,
1006
+ sourceBase: finalizedBaseline?.sourceBase,
1007
+ releaseSourceRevision: finalizedBaseline?.releaseSourceRevision,
947
1008
  };
948
1009
  if (flags.json) return writeJson(result);
949
1010
  print(
950
- `应用发布租约与变更基线已获取: ${target.appType} change=${lease.changeId || changeId} baseline=${baseline?.baselineId || '-'} expiresAt=${lease.expiresAt}`
1011
+ `应用发布租约与变更基线已获取: ${target.appType} change=${lease.changeId || changeId} baseline=${finalizedBaseline?.baselineId || '-'} publishSource=${String(finalizedBaseline?.releaseSourceRevision?.baseCommit || '-').slice(0, 12)} main=${finalizedBaseline?.releaseSourceRevision?.remoteName ? `${finalizedBaseline.releaseSourceRevision.remoteName}/` : ''}${finalizedBaseline?.releaseSourceRevision?.mainBranch || '-'} expiresAt=${lease.expiresAt}`
951
1012
  );
952
1013
  return;
953
1014
  }
@@ -961,6 +1022,10 @@ async function release(args) {
961
1022
  });
962
1023
  const leaseId = leaseIdFromArgs || stored?.leaseId;
963
1024
  if (!leaseId) fail('当前工作区没有发布租约,请先执行 openxiangda release begin --change <id>');
1025
+ const baseline = getStoredChangeBaseline(target);
1026
+ assertReleaseSourceRevisionStable(baseline?.releaseSourceRevision, {
1027
+ cwd: process.cwd(),
1028
+ });
964
1029
  const lease = await renewPublishLease(
965
1030
  config,
966
1031
  target,
@@ -974,6 +1039,40 @@ async function release(args) {
974
1039
 
975
1040
  if (subcommand === 'end' || subcommand === 'release') {
976
1041
  const stored = getStoredPublishLease(target);
1042
+ const baseline = getStoredChangeBaseline(target, {
1043
+ access: 'reconciliation-read',
1044
+ });
1045
+ if (
1046
+ leaseIdFromArgs &&
1047
+ stored?.leaseId &&
1048
+ leaseIdFromArgs !== stored.leaseId
1049
+ ) {
1050
+ fail(
1051
+ `RELEASE_LEASE_ID_MISMATCH: 显式 leaseId ${leaseIdFromArgs} 与本地冻结 lease ${stored.leaseId} 不一致;未清理任何发布证据`
1052
+ );
1053
+ }
1054
+ const reconciliationOnly = Boolean(
1055
+ (baseline && !isChangeBaselineOwnedByCurrentSession(baseline)) ||
1056
+ (stored && !isPublishLeaseOwnedByCurrentSession(stored))
1057
+ );
1058
+ if (reconciliationOnly) {
1059
+ if (!baseline?.releaseSourceRevision) {
1060
+ fail(
1061
+ 'RELEASE_SOURCE_LINEAGE_REQUIRED: 外来 release 缺少冻结 source lineage,禁止接管或清理'
1062
+ );
1063
+ }
1064
+ const result = await reconcileInactiveRelease({
1065
+ config,
1066
+ target,
1067
+ stored,
1068
+ baseline,
1069
+ });
1070
+ if (flags.json) return writeJson(result);
1071
+ print(
1072
+ `过期 release 已完成只读回合清理: ${target.appType} source=${String(result.integration.sourceCommit || '').slice(0, 12)}`
1073
+ );
1074
+ return;
1075
+ }
977
1076
  if (!leaseIdFromArgs) assertLocalPublishLeaseOwnership(stored);
978
1077
  assertOrClaimWorktreeOwner({
979
1078
  cwd: process.cwd(),
@@ -981,37 +1080,147 @@ async function release(args) {
981
1080
  });
982
1081
  const leaseId = leaseIdFromArgs || stored?.leaseId;
983
1082
  if (!leaseId) {
984
- const result = { active: false, appType: target.appType, local: false };
985
- clearChangeBaseline(target);
1083
+ const integration = baseline?.releaseSourceRevision
1084
+ ? assertReleaseSourceIntegrated(baseline.releaseSourceRevision, {
1085
+ cwd: process.cwd(),
1086
+ })
1087
+ : null;
1088
+ if (baseline && !integration) {
1089
+ fail(
1090
+ 'RELEASE_SOURCE_LINEAGE_REQUIRED: 本地仍有发布基线,但缺少可验证的 releaseSourceRevision;禁止无证据清理'
1091
+ );
1092
+ }
1093
+ const result = {
1094
+ active: false,
1095
+ appType: target.appType,
1096
+ local: false,
1097
+ integration,
1098
+ };
1099
+ if (baseline) clearChangeBaseline(target, baseline.baselineId);
986
1100
  releaseWorktreeOwner({ cwd: process.cwd() });
987
1101
  if (flags.json) return writeJson(result);
988
1102
  print(`当前工作区没有活动发布租约: ${target.appType}`);
989
1103
  return;
990
1104
  }
1105
+ const integration = assertReleaseSourceIntegrated(
1106
+ baseline?.releaseSourceRevision,
1107
+ { cwd: process.cwd() }
1108
+ );
991
1109
  try {
992
- const result = await requestWithAuth(
1110
+ const remoteResult = await requestWithAuth(
993
1111
  config,
994
1112
  target.profileName,
995
1113
  publishLeaseApiPath(target, `/${encodeURIComponent(leaseId)}/release`),
996
- { method: 'POST' }
1114
+ {
1115
+ method: 'POST',
1116
+ body: {
1117
+ completion: 'mainline-integrated',
1118
+ sourceCommit: integration.sourceCommit,
1119
+ mainBranch: integration.mainBranch,
1120
+ mainTipCommit: integration.mainTipCommit,
1121
+ },
1122
+ }
997
1123
  );
998
1124
  clearPublishLease(target, leaseId);
999
1125
  clearChangeBaseline(target);
1000
1126
  releaseWorktreeOwner({ cwd: process.cwd() });
1127
+ const result = {
1128
+ ...(remoteResult && typeof remoteResult === 'object' ? remoteResult : {}),
1129
+ integration,
1130
+ };
1001
1131
  if (flags.json) return writeJson(result);
1002
- print(`应用发布租约已释放: ${target.appType}`);
1132
+ print(
1133
+ `应用发布租约已释放,发布提交已进入 ${integration.remoteName}/${integration.mainBranch}: ${target.appType}`
1134
+ );
1003
1135
  return;
1004
1136
  } catch (error) {
1005
- if (['PUBLISH_LEASE_EXPIRED', 'PUBLISH_LEASE_INVALID'].includes(error?.code)) {
1137
+ if (
1138
+ stored?.leaseId === leaseId &&
1139
+ ['PUBLISH_LEASE_EXPIRED', 'PUBLISH_LEASE_INVALID'].includes(error?.code)
1140
+ ) {
1006
1141
  clearPublishLease(target, leaseId);
1007
- clearChangeBaseline(target);
1142
+ if (baseline) clearChangeBaseline(target, baseline.baselineId || baseline.id);
1008
1143
  releaseWorktreeOwner({ cwd: process.cwd() });
1009
1144
  }
1010
1145
  throw error;
1011
1146
  }
1012
1147
  }
1013
1148
 
1014
- fail('用法: openxiangda release begin|status|renew|end|backend-*|app-* [--change id] [--profile name]');
1149
+ fail('用法: openxiangda release begin|status|integration-status|renew|end|backend-*|app-* [--change id] [--profile name]');
1150
+ }
1151
+
1152
+ async function fetchPublishLeaseStatus(config, target, clientSessionId) {
1153
+ return await requestWithAuth(
1154
+ config,
1155
+ target.profileName,
1156
+ `${publishLeaseApiPath(target, '/status')}?clientSessionId=${encodeURIComponent(clientSessionId || createPublishClientSessionId())}`
1157
+ );
1158
+ }
1159
+
1160
+ async function reconcileInactiveRelease({ config, target, stored, baseline }) {
1161
+ const integration = assertReleaseSourceIntegrated(
1162
+ baseline.releaseSourceRevision,
1163
+ { cwd: process.cwd() }
1164
+ );
1165
+ const remote = await fetchPublishLeaseStatus(
1166
+ config,
1167
+ target,
1168
+ baseline.clientSessionId
1169
+ );
1170
+ if (remote?.active !== false) {
1171
+ const error = new Error(
1172
+ 'RELEASE_RECONCILIATION_REMOTE_ACTIVE: 原任务的远端 publish lease 仍为 active;新任务只能查看回合状态,不能续租、写入、释放或覆盖'
1173
+ );
1174
+ error.code = 'RELEASE_RECONCILIATION_REMOTE_ACTIVE';
1175
+ throw error;
1176
+ }
1177
+
1178
+ const expectedOwnerThreadId = String(
1179
+ baseline.workspace?.threadId || stored?.workspace?.threadId || ''
1180
+ ).trim();
1181
+ const ownerStatus = getWorktreeOwnerStatus({ cwd: process.cwd() });
1182
+ if (
1183
+ ownerStatus.owner &&
1184
+ (!expectedOwnerThreadId || ownerStatus.owner.threadId !== expectedOwnerThreadId)
1185
+ ) {
1186
+ fail(
1187
+ 'RELEASE_RECONCILIATION_OWNER_CHANGED: worktree owner 已不再对应冻结 release;保留发布证据且禁止清理新任务 owner'
1188
+ );
1189
+ }
1190
+ const evidenceCleared = clearChangeBaselineForReconciliation(
1191
+ target,
1192
+ {
1193
+ ...baseline,
1194
+ leaseId: stored?.leaseId || null,
1195
+ }
1196
+ );
1197
+ if (!evidenceCleared) {
1198
+ fail(
1199
+ 'RELEASE_RECONCILIATION_STATE_CHANGED: 本地 lease/baseline 与冻结证据不再精确一致;未清理任何发布证据'
1200
+ );
1201
+ }
1202
+ releaseWorktreeOwner({
1203
+ cwd: process.cwd(),
1204
+ force: true,
1205
+ expectedThreadId: expectedOwnerThreadId || undefined,
1206
+ expectedChangeId: baseline.changeId || undefined,
1207
+ reason: [
1208
+ 'inactive release reconciliation',
1209
+ `baseline=${baseline.baselineId || baseline.id}`,
1210
+ `change=${baseline.changeId}`,
1211
+ `source=${baseline.releaseSourceRevision.baseCommit}`,
1212
+ 'remoteLease=inactive',
1213
+ 'mainline=integrated',
1214
+ ].join(' '),
1215
+ });
1216
+ return {
1217
+ active: false,
1218
+ appType: target.appType,
1219
+ local: false,
1220
+ reconciliationOnly: true,
1221
+ remote,
1222
+ integration,
1223
+ };
1015
1224
  }
1016
1225
 
1017
1226
  function backendReleaseApiPath(target, suffix = '') {
@@ -1730,22 +1939,38 @@ async function ensureChangeBaseline(
1730
1939
  clientSessionId ||
1731
1940
  readStringFlag(flags, 'client-session-id') ||
1732
1941
  createPublishClientSessionId();
1733
- const sourceBase = resolveChangeSourceBase(changeId, flags);
1734
1942
  const stored = getStoredChangeBaseline(target);
1735
1943
  if (stored && !isChangeBaselineOwnedByCurrentSession(stored)) {
1736
1944
  fail(
1737
1945
  `当前工作区的变更基线由另一个任务持有 (${stored.workspace?.threadId || stored.clientSessionId || 'another-session'})`
1738
1946
  );
1739
1947
  }
1740
- if (
1741
- stored?.baselineId &&
1742
- stored.changeId === changeId &&
1743
- stored.clientSessionId === sessionId &&
1744
- stored.sourceBase?.baseCommit === sourceBase.baseCommit &&
1745
- stored.sourceBase?.treeHash === sourceBase.treeHash
1746
- ) {
1948
+ if (stored) {
1949
+ if (!stored.releaseSourceRevision) {
1950
+ fail(
1951
+ 'RELEASE_SOURCE_LINEAGE_REQUIRED: 已存在的发布基线缺少 releaseSourceRevision,禁止覆盖;请先人工核对线上状态'
1952
+ );
1953
+ }
1954
+ if (stored.changeId !== changeId) {
1955
+ fail(
1956
+ `RELEASE_RECONCILIATION_REQUIRED: change ${stored.changeId || '-'} 的发布提交仍待回合主分支,不能被 ${changeId} 覆盖;请先 merge/push 并执行 release end`
1957
+ );
1958
+ }
1959
+ if (stored.clientSessionId && stored.clientSessionId !== sessionId) {
1960
+ fail(
1961
+ `RELEASE_RECONCILIATION_REQUIRED: change ${changeId} 必须复用已冻结的 clientSessionId,禁止创建新会话覆盖发布义务`
1962
+ );
1963
+ }
1964
+ assertReleaseSourceRevisionStable(stored.releaseSourceRevision, {
1965
+ cwd: process.cwd(),
1966
+ });
1747
1967
  return stored;
1748
1968
  }
1969
+ const activeLease = getUsableStoredPublishLease(target);
1970
+ const sourceBase = resolveChangeSourceBase(changeId, flags);
1971
+ const releaseSourceRevision = prepareReleaseSourceRevision({
1972
+ cwd: process.cwd(),
1973
+ });
1749
1974
 
1750
1975
  const data = await requestWithAuth(
1751
1976
  config,
@@ -1764,12 +1989,52 @@ async function ensureChangeBaseline(
1764
1989
  },
1765
1990
  }
1766
1991
  );
1767
- return saveChangeBaseline(target, {
1992
+ const baseline = saveChangeBaseline(target, {
1768
1993
  ...data,
1769
1994
  changeId,
1770
1995
  clientSessionId: sessionId,
1771
1996
  sourceBase,
1997
+ releaseSourceRevision,
1998
+ lifecycleState: activeLease ? 'active' : 'pending-lease',
1772
1999
  });
2000
+ Object.defineProperty(baseline, '__createdForAcquire', {
2001
+ value: true,
2002
+ configurable: true,
2003
+ enumerable: false,
2004
+ });
2005
+ return baseline;
2006
+ }
2007
+
2008
+ function finalizeReleaseBaseline(target, baseline) {
2009
+ if (!baseline?.releaseSourceRevision) {
2010
+ fail('RELEASE_SOURCE_LINEAGE_REQUIRED: 无法激活缺少 releaseSourceRevision 的发布基线');
2011
+ }
2012
+ const finalized = saveChangeBaseline(target, {
2013
+ ...baseline,
2014
+ lifecycleState: 'active',
2015
+ });
2016
+ try {
2017
+ recordSddReleaseSourceRevision({
2018
+ cwd: process.cwd(),
2019
+ changeId: finalized.changeId,
2020
+ releaseSourceRevision: finalized.releaseSourceRevision,
2021
+ });
2022
+ } catch (error) {
2023
+ if (!/change 不存在|SDD 未初始化|ENOENT/i.test(String(error?.message || ''))) {
2024
+ throw error;
2025
+ }
2026
+ }
2027
+ return finalized;
2028
+ }
2029
+
2030
+ function clearRejectedPendingBaseline(target, baseline, error) {
2031
+ if (
2032
+ baseline?.__createdForAcquire &&
2033
+ baseline.lifecycleState === 'pending-lease' &&
2034
+ error?.code === 'PUBLISH_LEASE_HELD'
2035
+ ) {
2036
+ clearChangeBaseline(target, baseline.baselineId);
2037
+ }
1773
2038
  }
1774
2039
 
1775
2040
  async function acquirePublishLease(config, target, changeId, flags = {}) {
@@ -1819,7 +2084,14 @@ async function acquirePublishLease(config, target, changeId, flags = {}) {
1819
2084
  },
1820
2085
  }
1821
2086
  );
1822
- return savePublishLease(target, { ...data, clientSessionId });
2087
+ return savePublishLease(target, {
2088
+ ...data,
2089
+ clientSessionId,
2090
+ workspace: buildReleaseLeaseWorkspace(
2091
+ baseline.releaseSourceRevision,
2092
+ target
2093
+ ),
2094
+ });
1823
2095
  }
1824
2096
 
1825
2097
  async function renewPublishLease(config, target, lease, flags = {}) {
@@ -1859,13 +2131,14 @@ async function resolvePublishLeaseForWrite(config, target, flags = {}) {
1859
2131
  getStoredChangeBaseline(target)?.clientSessionId ||
1860
2132
  createPublishClientSessionId();
1861
2133
  if (changeId) {
1862
- await ensureChangeBaseline(
2134
+ const baseline = await ensureChangeBaseline(
1863
2135
  config,
1864
2136
  target,
1865
2137
  changeId,
1866
2138
  flags,
1867
2139
  clientSessionId
1868
2140
  );
2141
+ finalizeReleaseBaseline(target, baseline);
1869
2142
  }
1870
2143
  const explicitLease = {
1871
2144
  leaseId: explicitLeaseId,
@@ -1886,22 +2159,33 @@ async function resolvePublishLeaseForWrite(config, target, flags = {}) {
1886
2159
  let lease = getUsableStoredPublishLease(target);
1887
2160
  if (stored && !lease) {
1888
2161
  clearPublishLease(target, stored.leaseId);
1889
- clearChangeBaseline(target);
1890
2162
  }
1891
2163
  if (!lease && changeId) {
2164
+ const pendingBaseline = getStoredChangeBaseline(target);
1892
2165
  const clientSessionId =
1893
- readStringFlag(flags, 'client-session-id') || createPublishClientSessionId();
1894
- await ensureChangeBaseline(
2166
+ readStringFlag(flags, 'client-session-id') ||
2167
+ (pendingBaseline?.changeId === changeId &&
2168
+ isChangeBaselineOwnedByCurrentSession(pendingBaseline)
2169
+ ? pendingBaseline.clientSessionId
2170
+ : null) ||
2171
+ createPublishClientSessionId();
2172
+ const baseline = await ensureChangeBaseline(
1895
2173
  config,
1896
2174
  target,
1897
2175
  changeId,
1898
2176
  flags,
1899
2177
  clientSessionId
1900
2178
  );
1901
- lease = await acquirePublishLease(config, target, changeId, {
1902
- ...flags,
1903
- 'client-session-id': clientSessionId,
1904
- });
2179
+ try {
2180
+ lease = await acquirePublishLease(config, target, changeId, {
2181
+ ...flags,
2182
+ 'client-session-id': clientSessionId,
2183
+ });
2184
+ } catch (error) {
2185
+ clearRejectedPendingBaseline(target, baseline, error);
2186
+ throw error;
2187
+ }
2188
+ finalizeReleaseBaseline(target, baseline);
1905
2189
  }
1906
2190
  if (!lease) return null;
1907
2191
  if (changeId && lease.changeId !== changeId) {
@@ -1910,14 +2194,19 @@ async function resolvePublishLeaseForWrite(config, target, flags = {}) {
1910
2194
  );
1911
2195
  }
1912
2196
  if (changeId) {
1913
- await ensureChangeBaseline(
2197
+ const baseline = await ensureChangeBaseline(
1914
2198
  config,
1915
2199
  target,
1916
2200
  changeId,
1917
2201
  flags,
1918
2202
  lease.clientSessionId
1919
2203
  );
2204
+ finalizeReleaseBaseline(target, baseline);
1920
2205
  }
2206
+ const baseline = getStoredChangeBaseline(target);
2207
+ assertReleaseSourceRevisionStable(baseline?.releaseSourceRevision, {
2208
+ cwd: process.cwd(),
2209
+ });
1921
2210
  activatePublishRequestContext(target, lease);
1922
2211
  const heartbeat = await activatePublishLeaseHeartbeat(
1923
2212
  config,
@@ -2122,6 +2411,33 @@ function publishLeaseApiPath(target, suffix) {
2122
2411
  return `/openxiangda-api/v1/apps/${encodeURIComponent(target.appType)}/publish-lease${suffix}`;
2123
2412
  }
2124
2413
 
2414
+ function buildReleaseLeaseWorkspace(sourceRevision, target) {
2415
+ if (!sourceRevision || typeof sourceRevision !== 'object') return undefined;
2416
+ return {
2417
+ cwd: path.resolve(target?.cwd || process.cwd()),
2418
+ branch: sourceRevision.branch || '',
2419
+ commit: sourceRevision.baseCommit || '',
2420
+ treeHash: sourceRevision.treeHash || '',
2421
+ worktreeRoot: sourceRevision.worktreeRoot || '',
2422
+ remoteName: sourceRevision.remoteName || null,
2423
+ remoteUrlHash: sourceRevision.remoteUrlHash || null,
2424
+ mainBranch: sourceRevision.mainBranch || null,
2425
+ mainTipCommit: sourceRevision.mainTipCommit || null,
2426
+ };
2427
+ }
2428
+
2429
+ function printReleaseIntegrationStatus(result) {
2430
+ print(
2431
+ [
2432
+ `发布提交: ${result.sourceCommit || '-'}`,
2433
+ `来源分支: ${result.sourceBranch || '-'}`,
2434
+ `权威主分支: ${result.remoteName ? `${result.remoteName}/` : ''}${result.mainBranch}`,
2435
+ `主分支提交: ${result.mainTipCommit}`,
2436
+ `已回合并推送: ${result.integrated ? 'yes' : 'no'}`,
2437
+ ].join('\n')
2438
+ );
2439
+ }
2440
+
2125
2441
  function normalizePublishLeaseTtl(value) {
2126
2442
  if (value === undefined || value === null || value === '') {
2127
2443
  return DEFAULT_CLI_PUBLISH_LEASE_TTL_SECONDS;
@@ -2141,6 +2457,9 @@ function printPublishLeaseStatus(result) {
2141
2457
  `应用发布租约: ${remote.appType || local?.appType || '-'}`,
2142
2458
  `remote: ${remote.active ? `active holder=${remote.holder} expiresAt=${remote.expiresAt}` : 'inactive'}`,
2143
2459
  `local: ${local ? `leaseId=${local.leaseId} change=${local.changeId || '-'} expiresAt=${local.expiresAt}` : 'none'}`,
2460
+ result.releaseSourceRevision
2461
+ ? `pending mainline: ${String(result.releaseSourceRevision.baseCommit || '').slice(0, 12)} -> ${result.releaseSourceRevision.remoteName}/${result.releaseSourceRevision.mainBranch}(运行 release integration-status)`
2462
+ : 'pending mainline: none',
2144
2463
  ].join('\n')
2145
2464
  );
2146
2465
  }
@@ -3653,9 +3972,10 @@ Usage:
3653
3972
  openxiangda workspace publish --profile <name> [--changed [--since ref]|--form code|--page code|--only list] [--dry-run] [--force] [--resources|--skip-resources] [--prune] [--sdd-bypass --reason text]
3654
3973
 
3655
3974
  React SPA:
3656
- openxiangda workspace publish --profile <name> --form <formCode> # sync form schema only
3657
- openxiangda resource publish --profile <name>
3658
- openxiangda runtime deploy --profile <name>
3975
+ openxiangda workspace publish --change <change> --profile <name> --form <formCode> # sync form schema only
3976
+ openxiangda resource publish <type> --only <codes> --change <change> --profile <name>
3977
+ openxiangda runtime deploy --no-activate --change <change> --profile <name>
3978
+ # After activation, merge/push the frozen SHA, then release integration-status + release end.
3659
3979
 
3660
3980
  Options:
3661
3981
  --runtime legacy|react-spa Workspace template/runtime mode.
@@ -6931,7 +7251,7 @@ async function resource(args) {
6931
7251
  ' - publish --dry-run 等价于发布前计划,不会写平台资源。',
6932
7252
  ' - 在线 publish 必须用 --only/--code 精确选择;真正全量发布必须显式 --all --reason,SDD bypass 不能绕过此范围门。',
6933
7253
  ' - 源码依赖触发的 Function/Automation 更新默认只替换 source snapshot,保留线上 bindings/schema/metadata;显式替换 manifest 必须加 --replace-manifest --reason。',
6934
- ' - --only/--code 在读取 manifest 后立即按逻辑资源 code 过滤;也支持 function:foo 这类带类型 selector。',
7254
+ ' - --only/--code 会在读取/分析 manifest 与构建源码前按逻辑资源 code 早期收窄;也支持 function:foo 这类带类型 selector。',
6935
7255
  ' - type 可选: workflow, notification, route, menu, role, permission, data-view, function, connector 等。',
6936
7256
  ' - plan/publish 会在 stderr 输出阶段进度,超过 15 秒持续心跳;JSON 结果包含 timings,不污染 stdout。',
6937
7257
  ' - plan/publish --dry-run 严格只允许 GET/HEAD;READ_ONLY_AUTH_REQUIRED 时先执行 auth refresh 或重新登录。',
@@ -7199,7 +7519,7 @@ async function runtime(args) {
7199
7519
  '用法: openxiangda runtime deploy|releases|activate [--profile name] [--json]',
7200
7520
  '常用流程:',
7201
7521
  ' npm run build',
7202
- ' openxiangda runtime deploy --profile <name> --json',
7522
+ ' openxiangda runtime deploy --no-activate --change <change> --profile <name> --json',
7203
7523
  ' openxiangda runtime releases --profile <name>',
7204
7524
  '说明:',
7205
7525
  ' - deploy 上传当前工作区 dist/ 并激活 React SPA runtime release。',
@@ -7209,6 +7529,7 @@ async function runtime(args) {
7209
7529
  ' - runtime 只发布前端产物;表单、流程、通知等资源仍走 resource publish。',
7210
7530
  ' - deploy/activate 会读取当前 Runtime head 并携带 CAS preconditions;head 已变化时平台返回 409,避免静默覆盖。',
7211
7531
  ' - 所有 deploy(含 --no-activate)先获取应用 publish lease;在任何构建/上传前要求 Git clean committed HEAD,并校验当前 HEAD 继承线上 Runtime 源码血缘。',
7532
+ ' - release begin 还会冻结实际 publish HEAD 并要求它包含当前权威远端主分支;上线后 merge/push 该 SHA,通过 release integration-status 后再 release end。',
7212
7533
  ' - Runtime 同步构建前把 lease 续到平台最大 TTL;构建/上传后每次写入仍会重新校验,失租不会继续激活。',
7213
7534
  ' - build 不能安全前移到 lease 之前:直传模式的 assetBase 由受保护的 upload-plan 返回,不能用 gateway 路径静默替换。',
7214
7535
  ' - deploy 会输出 lease/head/preflight/build/upload/activate 阶段进度,JSON 结果包含 timings。',
@@ -7304,6 +7625,10 @@ async function runtime(args) {
7304
7625
  );
7305
7626
  const telemetry = createCliReleaseTelemetry('runtime deploy');
7306
7627
  try {
7628
+ // Preserve Runtime's dedicated dirty-source contract before the generic
7629
+ // frozen-release revision check runs inside lease resolution. The release
7630
+ // freeze and clean-HEAD gates still run immediately afterwards.
7631
+ assertRuntimeSourceClean(process.cwd());
7307
7632
  // Keep build after lease acquisition. In direct-storage modes the build
7308
7633
  // embeds the provider-specific assetBaseUrl returned by the protected
7309
7634
  // upload plan; substituting the gateway URL would change chunk/publicPath
@@ -8155,7 +8480,7 @@ async function commands(args) {
8155
8480
  'open-api credential list|get|create|update|rotate-secret [id]',
8156
8481
  'design gates|template|review [--topic code]',
8157
8482
  'sdd init|migrate|propose|approve|status|context|verify|sync|archive',
8158
- 'release begin|status|renew|end|backend-head|backend-list|backend-detail|backend-diff|backend-rollback|backend-abort|backend-retry|app-capture|app-head|app-list|app-detail|app-diff|app-prepare|app-verify|app-activate|app-finalize|app-rollback|app-abort',
8483
+ 'release begin|status|integration-status|renew|end|backend-head|backend-list|backend-detail|backend-diff|backend-rollback|backend-abort|backend-retry|app-capture|app-head|app-list|app-detail|app-diff|app-prepare|app-verify|app-activate|app-finalize|app-rollback|app-abort',
8159
8484
  'env',
8160
8485
  'workspace init|bind|plan|check|publish [--app-name] [--changed|--since|--form|--page|--only|--dry-run|--force|--resources|--skip-resources|--prune]',
8161
8486
  'app list|create|snapshot',
@@ -8190,6 +8515,7 @@ async function commands(args) {
8190
8515
  'settings public-access is legacy form public-access compatibility/repair and should not be used for new React SPA apps.',
8191
8516
  'Direct live mutation commands should use --dry-run first and --write-manifest when the repository should remain source of truth.',
8192
8517
  'OpenXiangda native SDD uses openspec/ records; high-risk publishes require an approved change with coverage.json/release.json verification unless --sdd-bypass --reason is used.',
8518
+ 'A formal release freezes the clean publish HEAD separately from the change base; after activation merge/fast-forward and push that exact SHA to the authoritative default branch, verify release integration-status, then run release end.',
8193
8519
  ],
8194
8520
  };
8195
8521
  if (flags.json) return writeJson(manifest);
@@ -9539,6 +9865,12 @@ function resourceCodeSelectorMatches(selector, resourceKey, item) {
9539
9865
  return getResourceItemCode(item) === selector.code;
9540
9866
  }
9541
9867
 
9868
+ function resourceCodeSelectorAppliesToKey(selector, resourceKey) {
9869
+ if (selector.key) return selector.key === resourceKey;
9870
+ if (selector.keys) return selector.keys.includes(resourceKey);
9871
+ return true;
9872
+ }
9873
+
9542
9874
  function normalizeResourceTypeAlias(value) {
9543
9875
  return String(value || '')
9544
9876
  .trim()
@@ -9569,21 +9901,40 @@ function loadWorkspaceResources(options = {}) {
9569
9901
  }
9570
9902
  : {}),
9571
9903
  };
9904
+ if (codeFilters) {
9905
+ Object.defineProperty(manifest, '__resourceCodeSelectors', {
9906
+ value: codeFilters,
9907
+ configurable: true,
9908
+ enumerable: false,
9909
+ writable: false,
9910
+ });
9911
+ }
9572
9912
  for (const spec of RESOURCE_SPECS) {
9573
- const items = !typeFilters || typeFilters.has(spec.key)
9574
- ? readResourceItems(baseDir, spec)
9575
- : [];
9576
- manifest[spec.key] = codeFilters
9577
- ? items.filter(item => {
9578
- let matched = false;
9579
- for (let index = 0; index < codeFilters.length; index += 1) {
9580
- if (!resourceCodeSelectorMatches(codeFilters[index], spec.key, item)) continue;
9581
- matchedCodeFilters.add(index);
9582
- matched = true;
9583
- }
9584
- return matched;
9585
- })
9586
- : items;
9913
+ if (typeFilters && !typeFilters.has(spec.key)) {
9914
+ manifest[spec.key] = [];
9915
+ continue;
9916
+ }
9917
+ if (!codeFilters) {
9918
+ manifest[spec.key] = readResourceItems(baseDir, spec);
9919
+ continue;
9920
+ }
9921
+
9922
+ const selectorEntries = codeFilters
9923
+ .map((selector, index) => ({ selector, index }))
9924
+ .filter(({ selector }) => resourceCodeSelectorAppliesToKey(selector, spec.key));
9925
+ if (selectorEntries.length === 0) {
9926
+ manifest[spec.key] = [];
9927
+ continue;
9928
+ }
9929
+ manifest[spec.key] = readResourceItems(baseDir, spec, {
9930
+ selectorEntries,
9931
+ matchedCodeFilters,
9932
+ // Composite menu/notification files may expose several logical codes
9933
+ // whose children cannot be located from the filename alone. They still
9934
+ // filter before validation/source analysis; exact Function/Automation
9935
+ // manifests use the fail-safe early file path below.
9936
+ selectorFileScope: !['menus', 'notifications'].includes(spec.key),
9937
+ });
9587
9938
  }
9588
9939
  if (codeFilters) {
9589
9940
  manifest.unmatchedResourceCodeFilters = codeFilters
@@ -9593,19 +9944,63 @@ function loadWorkspaceResources(options = {}) {
9593
9944
  return manifest;
9594
9945
  }
9595
9946
 
9596
- function readResourceItems(baseDir, spec) {
9947
+ function readResourceItems(baseDir, spec, options = {}) {
9597
9948
  const items = [];
9949
+ const selectorEntries = options.selectorEntries || null;
9950
+ const matchedSelectorIndexes = new Set();
9951
+ const visitedFiles = new Set();
9952
+ const appendFile = filePath => {
9953
+ if (visitedFiles.has(filePath)) return;
9954
+ visitedFiles.add(filePath);
9955
+ const fileItems = readResourceItemsFromFile(filePath, spec);
9956
+ if (!selectorEntries) {
9957
+ items.push(...fileItems);
9958
+ return;
9959
+ }
9960
+ for (const item of fileItems) {
9961
+ let matched = false;
9962
+ for (const { selector, index } of selectorEntries) {
9963
+ if (!resourceCodeSelectorMatches(selector, spec.key, item)) continue;
9964
+ options.matchedCodeFilters?.add(index);
9965
+ matchedSelectorIndexes.add(index);
9966
+ matched = true;
9967
+ }
9968
+ if (matched) items.push(item);
9969
+ }
9970
+ };
9598
9971
  for (const relativeFile of spec.topFiles || []) {
9599
9972
  const filePath = path.join(baseDir, relativeFile);
9600
9973
  if (fs.existsSync(filePath)) {
9601
- items.push(...readResourceItemsFromFile(filePath, spec));
9974
+ appendFile(filePath);
9602
9975
  }
9603
9976
  }
9604
9977
 
9605
9978
  const dirPath = path.join(baseDir, spec.dir);
9606
9979
  if (fs.existsSync(dirPath)) {
9607
- for (const filePath of listResourceJsonFiles(dirPath, spec)) {
9608
- items.push(...readResourceItemsFromFile(filePath, spec));
9980
+ if (selectorEntries && options.selectorFileScope !== false) {
9981
+ const remaining = () => selectorEntries.filter(
9982
+ ({ index }) => !matchedSelectorIndexes.has(index)
9983
+ );
9984
+ for (const filePath of listSelectedResourceJsonFiles(
9985
+ dirPath,
9986
+ spec,
9987
+ remaining().map(({ selector }) => selector.code)
9988
+ )) {
9989
+ appendFile(filePath);
9990
+ }
9991
+ if (remaining().length > 0) {
9992
+ // A direct path is only a hint. The file may declare another explicit
9993
+ // code, while the requested logical resource lives in an arbitrarily
9994
+ // named legacy bundle. Fall back only when no selected item actually
9995
+ // matched, preserving the standard-layout fast path.
9996
+ for (const filePath of listResourceJsonFiles(dirPath, spec)) {
9997
+ appendFile(filePath);
9998
+ }
9999
+ }
10000
+ } else {
10001
+ for (const filePath of listResourceJsonFiles(dirPath, spec)) {
10002
+ appendFile(filePath);
10003
+ }
9609
10004
  }
9610
10005
  }
9611
10006
  return items;
@@ -9754,6 +10149,36 @@ function listResourceJsonFiles(dirPath, spec) {
9754
10149
  );
9755
10150
  }
9756
10151
 
10152
+ function listSelectedResourceJsonFiles(dirPath, spec, selectedCodes = []) {
10153
+ const codeSet = new Set(selectedCodes.map(code => String(code || '').trim()).filter(Boolean));
10154
+ if (codeSet.size === 0) return [];
10155
+
10156
+ const directCandidates = new Set();
10157
+ for (const code of codeSet) {
10158
+ if (
10159
+ code === '.' ||
10160
+ code === '..' ||
10161
+ code.includes('/') ||
10162
+ code.includes('\\')
10163
+ ) {
10164
+ continue;
10165
+ }
10166
+ const directFile = path.join(dirPath, `${code}.json`);
10167
+ if (fs.existsSync(directFile) && fs.statSync(directFile).isFile()) {
10168
+ directCandidates.add(directFile);
10169
+ }
10170
+ const directDir = path.join(dirPath, code);
10171
+ if (fs.existsSync(directDir) && fs.statSync(directDir).isDirectory()) {
10172
+ for (const filePath of listJsonFiles(directDir)) {
10173
+ if (shouldReadResourceJsonFile(filePath, dirPath, spec)) {
10174
+ directCandidates.add(filePath);
10175
+ }
10176
+ }
10177
+ }
10178
+ }
10179
+ return Array.from(directCandidates).sort();
10180
+ }
10181
+
9757
10182
  function shouldReadResourceJsonFile(filePath, dirPath, spec) {
9758
10183
  if (spec.key !== 'workflows') return true;
9759
10184
  const relativeParts = path.relative(dirPath, filePath).split(path.sep);
@@ -10987,7 +11412,7 @@ async function publishResourcesForWorkspace(config, profileName, options = {}) {
10987
11412
 
10988
11413
  async function buildResourcePlan(config, target, manifest) {
10989
11414
  return await withReadOnlyHttpGuard('resource plan', async () => {
10990
- prepareManifestJsCodeBundlesForPlan(manifest);
11415
+ await prepareManifestJsCodeBundlesForPlan(manifest);
10991
11416
  const existing = await fetchExistingResourceMaps(config, target, manifest);
10992
11417
  const actions = [];
10993
11418
  addPlanActions(actions, 'role', manifest.roles, existing.roles, roleEquals);
@@ -11024,7 +11449,7 @@ async function buildResourcePlan(config, target, manifest) {
11024
11449
  });
11025
11450
  }
11026
11451
 
11027
- function prepareManifestJsCodeBundlesForPlan(manifest) {
11452
+ async function prepareManifestJsCodeBundlesForPlan(manifest) {
11028
11453
  const targetsByWorkspace = new Map();
11029
11454
  const seenObjects = new Set();
11030
11455
 
@@ -11079,21 +11504,45 @@ function prepareManifestJsCodeBundlesForPlan(manifest) {
11079
11504
  };
11080
11505
 
11081
11506
  for (const spec of RESOURCE_SPECS) {
11082
- visit(manifest[spec.key] || [], manifest.baseDir);
11507
+ const selectors = manifest.__resourceCodeSelectors || null;
11508
+ const items = selectors
11509
+ ? (manifest[spec.key] || []).filter(item =>
11510
+ selectors.some(selector =>
11511
+ resourceCodeSelectorMatches(selector, spec.key, item)
11512
+ )
11513
+ )
11514
+ : manifest[spec.key] || [];
11515
+ visit(items, manifest.baseDir);
11083
11516
  }
11084
11517
 
11085
11518
  for (const [workspaceRoot, sourceMap] of targetsByWorkspace.entries()) {
11086
11519
  const sourceInfos = Array.from(sourceMap.values());
11087
11520
  if (sourceInfos.length === 0) continue;
11088
- const result = runWorkspaceJsCodeBuildBatch(
11521
+ let result = runWorkspaceJsCodeBuildBatch(
11089
11522
  workspaceRoot,
11090
11523
  sourceInfos.map(item => ({
11091
11524
  sourceKind: item.sourceKind,
11092
11525
  scriptCode: item.scriptCode,
11093
11526
  }))
11094
11527
  );
11095
- if (result.stdout) process.stderr.write(result.stdout);
11096
- if (result.stderr) process.stderr.write(result.stderr);
11528
+ if (result.status !== 0) {
11529
+ const legacyFallback = result.openxiangdaBuildMode === 'workspace-script'
11530
+ ? await tryBuildFunctionSourcesWithLegacyWorkspaceBuilder(
11531
+ workspaceRoot,
11532
+ sourceInfos
11533
+ )
11534
+ : { ok: false, result: null };
11535
+ if (legacyFallback.ok) {
11536
+ result = { status: 0, stdout: '', stderr: '' };
11537
+ } else {
11538
+ const failure = legacyFallback.result || result;
11539
+ if (failure.stdout) process.stderr.write(failure.stdout);
11540
+ if (failure.stderr) process.stderr.write(failure.stderr);
11541
+ }
11542
+ } else {
11543
+ if (result.stdout) process.stderr.write(result.stdout);
11544
+ if (result.stderr) process.stderr.write(result.stderr);
11545
+ }
11097
11546
  if (result.status !== 0) {
11098
11547
  fail(
11099
11548
  `JS_CODE batch bundle构建失败,无法生成资源计划: ${sourceInfos
@@ -11112,6 +11561,48 @@ function prepareManifestJsCodeBundlesForPlan(manifest) {
11112
11561
  }
11113
11562
  }
11114
11563
 
11564
+ async function tryBuildFunctionSourcesWithLegacyWorkspaceBuilder(
11565
+ workspaceRoot,
11566
+ sourceInfos
11567
+ ) {
11568
+ if (
11569
+ sourceInfos.length === 0 ||
11570
+ sourceInfos.some(item => item.sourceKind !== 'functions')
11571
+ ) {
11572
+ return { ok: false, result: null };
11573
+ }
11574
+
11575
+ let usedLegacyBuilder = false;
11576
+ for (const sourceInfo of sourceInfos) {
11577
+ const result = runWorkspaceJsCodeBuild(
11578
+ workspaceRoot,
11579
+ sourceInfo.scriptCode,
11580
+ sourceInfo.sourceKind
11581
+ );
11582
+ if (result.status === 0) {
11583
+ usedLegacyBuilder = true;
11584
+ if (result.stdout) process.stderr.write(result.stdout);
11585
+ if (result.stderr) process.stderr.write(result.stderr);
11586
+ continue;
11587
+ }
11588
+ if (!isUnsupportedFunctionsBuildScript(result)) {
11589
+ return { ok: false, result };
11590
+ }
11591
+ warn(
11592
+ `当前工作区 scripts/build-js-code.mjs 不支持 App Function source=functions,已使用 openxiangda 内置构建器兼容构建 ${sourceInfo.scriptCode}`
11593
+ );
11594
+ await buildFunctionSourceWithBundledEsbuild(
11595
+ sourceInfo.entryPath,
11596
+ workspaceRoot,
11597
+ sourceInfo.scriptCode
11598
+ );
11599
+ }
11600
+ if (usedLegacyBuilder) {
11601
+ warn('当前工作区 build-js-code 不支持批量 --scripts,已兼容使用逐 Function 构建');
11602
+ }
11603
+ return { ok: true, result: null };
11604
+ }
11605
+
11115
11606
  function buildResourcePlanActionIndex(actions = []) {
11116
11607
  const index = new Map();
11117
11608
  for (const action of actions || []) {
@@ -17034,6 +17525,7 @@ function getJsCodeSourceInfoForPlan(localPath) {
17034
17525
  workspaceRoot,
17035
17526
  sourceKind,
17036
17527
  scriptCode,
17528
+ entryPath: localPath,
17037
17529
  bundlePath: path.join(workspaceRoot, 'dist', sourceKind, scriptCode, 'index.cjs'),
17038
17530
  };
17039
17531
  }