openxiangda-devkit-core 2.0.0-alpha.12 → 2.0.0-alpha.121

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 (188) hide show
  1. package/README.md +3 -8
  2. package/dist/app-spec.d.ts +228 -0
  3. package/dist/app-spec.d.ts.map +1 -0
  4. package/dist/app-spec.js +1443 -0
  5. package/dist/app-spec.js.map +1 -0
  6. package/dist/application-services.d.ts +996 -49
  7. package/dist/application-services.d.ts.map +1 -1
  8. package/dist/application-services.js +1348 -197
  9. package/dist/application-services.js.map +1 -1
  10. package/dist/application-ui-contract.d.ts +5 -0
  11. package/dist/application-ui-contract.d.ts.map +1 -0
  12. package/dist/application-ui-contract.js +149 -0
  13. package/dist/application-ui-contract.js.map +1 -0
  14. package/dist/backend-image-build.d.ts +23 -0
  15. package/dist/backend-image-build.d.ts.map +1 -0
  16. package/dist/backend-image-build.js +204 -0
  17. package/dist/backend-image-build.js.map +1 -0
  18. package/dist/command-process.d.ts +14 -0
  19. package/dist/command-process.d.ts.map +1 -0
  20. package/dist/command-process.js +51 -0
  21. package/dist/command-process.js.map +1 -0
  22. package/dist/command-registry.d.ts +123 -366
  23. package/dist/command-registry.d.ts.map +1 -1
  24. package/dist/command-registry.js +26 -266
  25. package/dist/command-registry.js.map +1 -1
  26. package/dist/compiler/ai-catalog.d.ts +4 -0
  27. package/dist/compiler/ai-catalog.d.ts.map +1 -0
  28. package/dist/compiler/ai-catalog.js +354 -0
  29. package/dist/compiler/ai-catalog.js.map +1 -0
  30. package/dist/compiler/application-model.d.ts +60 -0
  31. package/dist/compiler/application-model.d.ts.map +1 -0
  32. package/dist/compiler/application-model.js +141 -0
  33. package/dist/compiler/application-model.js.map +1 -0
  34. package/dist/compiler/authorization-transition.d.ts +7 -0
  35. package/dist/compiler/authorization-transition.d.ts.map +1 -0
  36. package/dist/compiler/authorization-transition.js +27 -0
  37. package/dist/compiler/authorization-transition.js.map +1 -0
  38. package/dist/compiler/bundle.d.ts +34 -0
  39. package/dist/compiler/bundle.d.ts.map +1 -0
  40. package/dist/compiler/bundle.js +1910 -0
  41. package/dist/compiler/bundle.js.map +1 -0
  42. package/dist/compiler/config.d.ts +882 -0
  43. package/dist/compiler/config.d.ts.map +1 -0
  44. package/dist/compiler/config.js +4118 -0
  45. package/dist/compiler/config.js.map +1 -0
  46. package/dist/compiler/data-policy-expression.d.ts +12 -0
  47. package/dist/compiler/data-policy-expression.d.ts.map +1 -0
  48. package/dist/compiler/data-policy-expression.js +43 -0
  49. package/dist/compiler/data-policy-expression.js.map +1 -0
  50. package/dist/compiler/field-codec.d.ts +14 -0
  51. package/dist/compiler/field-codec.d.ts.map +1 -0
  52. package/dist/compiler/field-codec.js +131 -0
  53. package/dist/compiler/field-codec.js.map +1 -0
  54. package/dist/compiler/field-physical-plan.d.ts +10 -0
  55. package/dist/compiler/field-physical-plan.d.ts.map +1 -0
  56. package/dist/compiler/field-physical-plan.js +66 -0
  57. package/dist/compiler/field-physical-plan.js.map +1 -0
  58. package/dist/compiler/field-policy-path.d.ts +10 -0
  59. package/dist/compiler/field-policy-path.d.ts.map +1 -0
  60. package/dist/compiler/field-policy-path.js +283 -0
  61. package/dist/compiler/field-policy-path.js.map +1 -0
  62. package/dist/compiler/field-query-plan.d.ts +13 -0
  63. package/dist/compiler/field-query-plan.d.ts.map +1 -0
  64. package/dist/compiler/field-query-plan.js +122 -0
  65. package/dist/compiler/field-query-plan.js.map +1 -0
  66. package/dist/compiler/field-surface.d.ts +15 -0
  67. package/dist/compiler/field-surface.d.ts.map +1 -0
  68. package/dist/compiler/field-surface.js +51 -0
  69. package/dist/compiler/field-surface.js.map +1 -0
  70. package/dist/compiler/index.d.ts +6 -0
  71. package/dist/compiler/index.d.ts.map +1 -0
  72. package/dist/compiler/index.js +6 -0
  73. package/dist/compiler/index.js.map +1 -0
  74. package/dist/compiler/package-compiler.d.ts +22 -0
  75. package/dist/compiler/package-compiler.d.ts.map +1 -0
  76. package/dist/compiler/package-compiler.js +257 -0
  77. package/dist/compiler/package-compiler.js.map +1 -0
  78. package/dist/compiler/permission-review.d.ts +139 -0
  79. package/dist/compiler/permission-review.d.ts.map +1 -0
  80. package/dist/compiler/permission-review.js +131 -0
  81. package/dist/compiler/permission-review.js.map +1 -0
  82. package/dist/compiler/schema-composition.d.ts +21 -0
  83. package/dist/compiler/schema-composition.d.ts.map +1 -0
  84. package/dist/compiler/schema-composition.js +193 -0
  85. package/dist/compiler/schema-composition.js.map +1 -0
  86. package/dist/config.d.ts +1 -1
  87. package/dist/config.js +1 -1
  88. package/dist/configuration-preflight.d.ts +7 -0
  89. package/dist/configuration-preflight.d.ts.map +1 -0
  90. package/dist/configuration-preflight.js +13 -0
  91. package/dist/configuration-preflight.js.map +1 -0
  92. package/dist/connected-development.d.ts +62 -0
  93. package/dist/connected-development.d.ts.map +1 -0
  94. package/dist/connected-development.js +393 -0
  95. package/dist/connected-development.js.map +1 -0
  96. package/dist/control-plane-client.d.ts +353 -123
  97. package/dist/control-plane-client.d.ts.map +1 -1
  98. package/dist/control-plane-client.js +420 -141
  99. package/dist/control-plane-client.js.map +1 -1
  100. package/dist/delivery-cache.d.ts +34 -0
  101. package/dist/delivery-cache.d.ts.map +1 -0
  102. package/dist/delivery-cache.js +186 -0
  103. package/dist/delivery-cache.js.map +1 -0
  104. package/dist/delivery-source.d.ts +17 -0
  105. package/dist/delivery-source.d.ts.map +1 -0
  106. package/dist/delivery-source.js +85 -0
  107. package/dist/delivery-source.js.map +1 -0
  108. package/dist/deployment-observer.d.ts +14 -0
  109. package/dist/deployment-observer.d.ts.map +1 -0
  110. package/dist/deployment-observer.js +68 -0
  111. package/dist/deployment-observer.js.map +1 -0
  112. package/dist/deployment.d.ts +55 -3
  113. package/dist/deployment.d.ts.map +1 -1
  114. package/dist/deployment.js +182 -21
  115. package/dist/deployment.js.map +1 -1
  116. package/dist/design-readiness.d.ts +24 -0
  117. package/dist/design-readiness.d.ts.map +1 -0
  118. package/dist/design-readiness.js +102 -0
  119. package/dist/design-readiness.js.map +1 -0
  120. package/dist/developer-errors.d.ts +11 -0
  121. package/dist/developer-errors.d.ts.map +1 -0
  122. package/dist/developer-errors.js +16 -0
  123. package/dist/developer-errors.js.map +1 -0
  124. package/dist/developer-operations.d.ts +188 -0
  125. package/dist/developer-operations.d.ts.map +1 -0
  126. package/dist/developer-operations.js +58 -0
  127. package/dist/developer-operations.js.map +1 -0
  128. package/dist/development-lifecycle.d.ts +75 -0
  129. package/dist/development-lifecycle.d.ts.map +1 -0
  130. package/dist/development-lifecycle.js +189 -0
  131. package/dist/development-lifecycle.js.map +1 -0
  132. package/dist/documentation.d.ts +111 -0
  133. package/dist/documentation.d.ts.map +1 -0
  134. package/dist/documentation.js +112 -0
  135. package/dist/documentation.js.map +1 -0
  136. package/dist/index.d.ts +16 -0
  137. package/dist/index.d.ts.map +1 -1
  138. package/dist/index.js +16 -0
  139. package/dist/index.js.map +1 -1
  140. package/dist/internal/workflow.d.ts +83 -0
  141. package/dist/internal/workflow.d.ts.map +1 -0
  142. package/dist/internal/workflow.js +419 -0
  143. package/dist/internal/workflow.js.map +1 -0
  144. package/dist/nest-injection-contract.d.ts +4 -0
  145. package/dist/nest-injection-contract.d.ts.map +1 -0
  146. package/dist/nest-injection-contract.js +116 -0
  147. package/dist/nest-injection-contract.js.map +1 -0
  148. package/dist/operation-progress.d.ts +26 -0
  149. package/dist/operation-progress.d.ts.map +1 -0
  150. package/dist/operation-progress.js +92 -0
  151. package/dist/operation-progress.js.map +1 -0
  152. package/dist/optional-backend.d.ts +14 -0
  153. package/dist/optional-backend.d.ts.map +1 -0
  154. package/dist/optional-backend.js +145 -0
  155. package/dist/optional-backend.js.map +1 -0
  156. package/dist/package-compiler.d.ts +1 -1
  157. package/dist/package-compiler.d.ts.map +1 -1
  158. package/dist/package-compiler.js +1 -1
  159. package/dist/package-compiler.js.map +1 -1
  160. package/dist/session.d.ts +149 -4
  161. package/dist/session.d.ts.map +1 -1
  162. package/dist/session.js +362 -23
  163. package/dist/session.js.map +1 -1
  164. package/dist/testing.d.ts +77 -0
  165. package/dist/testing.d.ts.map +1 -0
  166. package/dist/testing.js +213 -0
  167. package/dist/testing.js.map +1 -0
  168. package/dist/toolchain-capsule.d.ts +45 -0
  169. package/dist/toolchain-capsule.d.ts.map +1 -0
  170. package/dist/toolchain-capsule.js +165 -0
  171. package/dist/toolchain-capsule.js.map +1 -0
  172. package/dist/workspace-loader.d.ts +1 -1
  173. package/dist/workspace-loader.d.ts.map +1 -1
  174. package/dist/workspace-loader.js +35 -3
  175. package/dist/workspace-loader.js.map +1 -1
  176. package/dist/workspace-operation.d.ts +3 -0
  177. package/dist/workspace-operation.d.ts.map +1 -0
  178. package/dist/workspace-operation.js +45 -0
  179. package/dist/workspace-operation.js.map +1 -0
  180. package/dist/workspace.d.ts.map +1 -1
  181. package/dist/workspace.js +37 -1
  182. package/dist/workspace.js.map +1 -1
  183. package/package.json +16 -4
  184. package/templates/backend/Dockerfile +20 -0
  185. package/templates/backend/package.json +27 -0
  186. package/templates/backend/src/app.module.ts +23 -0
  187. package/templates/backend/src/main.ts +5 -0
  188. package/templates/backend/tsconfig.json +9 -0
@@ -1,12 +1,28 @@
1
- import { spawn, spawnSync } from "node:child_process";
1
+ import { randomUUID } from "node:crypto";
2
+ import { publishedDeliverySource, assertDeliverySourceUnchanged, DeliverySourceError } from './delivery-source.js';
3
+ import { operationStage, skippedOperationStage, updateOperationStage } from './operation-progress.js';
4
+ import { deliveryInputDigest, reusableValidation, recordValidation, recordSealedCandidate, reusableSealedCandidate } from './delivery-cache.js';
5
+ import { developmentLifecycle, summarizeLifecycle, lifecycleAtCommit, verifyBusinessAcceptance } from './development-lifecycle.js';
6
+ import { runCommandProcess } from './command-process.js';
7
+ import { compileLocalConfiguration } from './configuration-preflight.js';
8
+ import { NativeConfigurationCompilerError, NATIVE_CONFIGURATION_VALIDATOR_DIGEST } from 'openxiangda-contracts/native-compiler';
2
9
  import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync, } from "node:fs";
3
- import { dirname, join, relative } from "node:path";
4
- import { SCHEMA_VERSIONS, canonicalJson, } from "openxiangda-contracts";
5
- import { compileApplicationSources, compileAppPackage, sha256Bytes, validateAppConfig, } from "openxiangda-compiler";
6
- import { OpenXiangdaControlPlaneClient, } from "./control-plane-client.js";
7
- import { loadSession } from "./session.js";
10
+ import { dirname, join, relative, resolve } from "node:path";
11
+ import { CONFIGURATION_COMPATIBILITY_CAPABILITY, CURRENT_APPLICATION_CONTRACT, OPENXIANGDA_CONTRACT_VERSION, SCHEMA_VERSIONS, canonicalJson, sha256Digest, } from "openxiangda-contracts";
12
+ import { compileApplicationSources, compileAppPackage, requiredPlatformCapabilities, backendRuntimeRequired, renderAdminNavigationSuggestion, sha256Bytes, validateAppConfig, } from "./compiler/index.js";
13
+ import { OpenXiangdaControlPlaneClient, ControlPlaneError, } from "./control-plane-client.js";
14
+ import { normalizePlatformBaseUrl, OpenXiangdaDeveloperSession, } from "./session.js";
15
+ import { runConnectedDevelopment, selectConnectedDevelopmentEnvironment, } from "./connected-development.js";
16
+ import { backendImageBuildTarget, publishBackendImage, } from "./backend-image-build.js";
8
17
  import { OPENXIANGDA_TOOLCHAIN_VERSION } from "./version.js";
9
18
  import { git, loadWorkspace, } from "./workspace-loader.js";
19
+ import { validateApplicationUiContract } from './application-ui-contract.js';
20
+ import { initializeOptionalBackend } from './optional-backend.js';
21
+ import { buildPermissionReview } from './compiler/permission-review.js';
22
+ import { validateNestInjectionContract } from "./nest-injection-contract.js";
23
+ import { collectWorkspaceToolchainDependencies, defaultDevkitCoreToolchainCapsule, ToolchainCapsuleScanLimitError, toolchainCapsuleDiagnostic, } from "./toolchain-capsule.js";
24
+ import { advisoryAppSpecDiagnostics, closeAppSpecChange, createAppSpecCapability, createAppSpecChange, initializeAppSpec, inspectAppSpec, summarizeAppSpecContext, } from "./app-spec.js";
25
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
10
26
  export class OpenXiangdaApplicationServices {
11
27
  options;
12
28
  constructor(options = {}) {
@@ -14,33 +30,248 @@ export class OpenXiangdaApplicationServices {
14
30
  }
15
31
  async workspaceContext(root) {
16
32
  const workspace = await this.workspace(root);
17
- return this.ok("workspace.context", workspace.context.workspace, workspace.context);
33
+ const lifecycle = developmentLifecycle(workspace.root, this.appSpecContractIndex(workspace, true));
34
+ return this.ok("workspace.context", workspace.context.workspace, { ...workspace.context, development: {
35
+ ...summarizeLifecycle(lifecycle),
36
+ application: lifecycle.context.application ? { path: lifecycle.context.application.path, content: lifecycle.context.application.content } : null,
37
+ records: [
38
+ ...lifecycle.context.index.capabilities, ...lifecycle.context.index.activeChanges,
39
+ ...lifecycle.context.index.decisions, ...lifecycle.context.index.designs, ...lifecycle.context.index.history,
40
+ ].map(({ id, title, status, path, kind }) => ({ id, title, status, path, kind })),
41
+ } });
42
+ }
43
+ async appSpecInit(root) {
44
+ const workspace = await this.workspace(root);
45
+ const data = initializeAppSpec({
46
+ root: workspace.root,
47
+ appCode: workspace.config.app.code,
48
+ appName: workspace.config.app.name,
49
+ });
50
+ return this.ok("spec.init", workspace.context.workspace, data, [
51
+ {
52
+ code: "spec.context",
53
+ label: "读取当前 AppSpec 上下文",
54
+ command: "openxiangda spec context --json",
55
+ },
56
+ ]);
57
+ }
58
+ async appSpecAddCapability(input) {
59
+ const workspace = await this.workspace(input.root);
60
+ const data = createAppSpecCapability({
61
+ root: workspace.root,
62
+ appCode: workspace.config.app.code,
63
+ appName: workspace.config.app.name,
64
+ id: input.id,
65
+ title: input.title,
66
+ ...(input.resources ? { resources: input.resources } : {}),
67
+ ...(input.actions ? { actions: input.actions } : {}),
68
+ });
69
+ return this.ok("spec.add-capability", workspace.context.workspace, data);
70
+ }
71
+ async appSpecNew(input) {
72
+ const workspace = await this.workspace(input.root);
73
+ const data = createAppSpecChange({
74
+ root: workspace.root,
75
+ appCode: workspace.config.app.code,
76
+ appName: workspace.config.app.name,
77
+ id: input.id,
78
+ title: input.title,
79
+ ...(input.risk ? { risk: input.risk } : {}),
80
+ ...(input.summary ? { summary: input.summary } : {}),
81
+ ...(input.capabilities ? { capabilities: input.capabilities } : {}),
82
+ ...(input.requirements ? { requirements: input.requirements } : {}),
83
+ ...(input.resources ? { resources: input.resources } : {}),
84
+ ...(input.actions ? { actions: input.actions } : {}),
85
+ });
86
+ return this.ok("spec.new", workspace.context.workspace, data, [
87
+ {
88
+ code: "spec.context",
89
+ label: "读取本次变更相关上下文",
90
+ command: `openxiangda spec context ${input.id} --json`,
91
+ },
92
+ ]);
93
+ }
94
+ async appSpecContext(root, selector, historyOffset = 0) {
95
+ const workspace = await this.workspace(root);
96
+ const context = inspectAppSpec(workspace.root, this.appSpecContractIndex(workspace, true), selector, historyOffset);
97
+ return this.result("spec.context", workspace.context.workspace, { ...context, lifecycle: summarizeLifecycle(developmentLifecycle(workspace.root, this.appSpecContractIndex(workspace, true), undefined, selector)) }, advisoryAppSpecDiagnostics(context.diagnostics));
98
+ }
99
+ async appSpecCheck(root) {
100
+ const workspace = await this.workspace(root);
101
+ const context = inspectAppSpec(workspace.root, this.appSpecContractIndex(workspace, true));
102
+ const lifecycle = developmentLifecycle(workspace.root, this.appSpecContractIndex(workspace, true));
103
+ return this.result("spec.check", workspace.context.workspace, { ...context, lifecycle: summarizeLifecycle(lifecycle) }, lifecycle.diagnostics, [
104
+ {
105
+ code: "check",
106
+ label: "继续运行应用检查",
107
+ command: "openxiangda check",
108
+ },
109
+ ]);
110
+ }
111
+ async appSpecVerify(root, deploymentId, evidencePath) {
112
+ const workspace = await this.workspace(root);
113
+ const run = await (await this.client(workspace.root)).deployment(workspace.config.app.code, deploymentId);
114
+ const lifecycle = developmentLifecycle(workspace.root, this.appSpecContractIndex(workspace, true));
115
+ const verification = verifyBusinessAcceptance(workspace.root, run, lifecycle, evidencePath);
116
+ return this.result('spec.verify', workspace.context.workspace, verification, verification.diagnostics);
117
+ }
118
+ async appSpecClose(input) {
119
+ const workspace = await this.workspace(input.root);
120
+ const outcome = closeAppSpecChange({
121
+ root: workspace.root,
122
+ id: input.id,
123
+ contract: this.appSpecContractIndex(workspace, true),
124
+ ...(input.summary ? { summary: input.summary } : {}),
125
+ ...(input.currentSpec ? { currentSpec: input.currentSpec } : {}),
126
+ });
127
+ const { diagnostics, ...data } = outcome;
128
+ return this.result("spec.close", workspace.context.workspace, data, diagnostics, data.converged
129
+ ? []
130
+ : [
131
+ {
132
+ code: "spec.context",
133
+ label: "检查当前规格与变更差异",
134
+ command: `openxiangda spec context ${input.id} --json`,
135
+ },
136
+ ]);
137
+ }
138
+ async accept(input) {
139
+ const workspace = await this.workspace(input.root);
140
+ const diagnostics = this.toolchainDiagnostics(workspace);
141
+ if (diagnostics.length > 0) {
142
+ return this.result("accept", workspace.context.workspace, undefined, diagnostics);
143
+ }
144
+ const planPath = resolve(workspace.root, String(input.planPath || ""));
145
+ if (!existsSync(planPath)) {
146
+ throw new Error(`OPENXIANGDA_ACCEPTANCE_PLAN_NOT_FOUND: ${planPath}`);
147
+ }
148
+ let plan;
149
+ try {
150
+ plan = JSON.parse(readFileSync(planPath, "utf8"));
151
+ }
152
+ catch {
153
+ throw new Error(`OPENXIANGDA_ACCEPTANCE_PLAN_INVALID_JSON: ${planPath}`);
154
+ }
155
+ if (plan?.schemaVersion !==
156
+ "openxiangda.preproduction-acceptance-plan/v2" ||
157
+ plan.environmentKey !== "preproduction" ||
158
+ !Array.isArray(plan.actors)) {
159
+ throw new Error("OPENXIANGDA_ACCEPTANCE_PLAN_INVALID: 计划必须是 preproduction v2 身份矩阵");
160
+ }
161
+ const client = await this.client(workspace.root);
162
+ const prepared = await client.preparePreproductionAcceptanceIdentities(workspace.config.app.code, plan);
163
+ return this.ok("accept", workspace.context.workspace, {
164
+ manual: true,
165
+ releaseGate: false,
166
+ planPath,
167
+ ...prepared,
168
+ });
18
169
  }
19
170
  async contractDescribe(root) {
20
171
  const workspace = await this.workspace(root);
21
- const sources = compileApplicationSources(workspace.config);
172
+ const sources = compileApplicationSources(workspace.config, this.toolchainVersion);
173
+ const adminNavigationSuggestion = renderAdminNavigationSuggestion(workspace.config);
22
174
  return this.ok("contract.describe", workspace.context.workspace, {
23
175
  configDigest: sources.config.digest,
24
176
  contractDigest: sources.contracts.digest,
177
+ aiCatalogDigest: sources.aiCatalog.digest,
25
178
  contract: sources.contracts.value,
179
+ aiCatalog: sources.aiCatalog.value,
180
+ permissionReview: buildPermissionReview(sources.config.value, sources.contracts.value),
181
+ adminNavigationAuthoring: {
182
+ schemaVersion: "openxiangda.admin-navigation-authoring/v1",
183
+ owner: "application",
184
+ target: "frontend.admin.navigation",
185
+ configured: sources.config.value.frontend.admin.navigation.length > 0,
186
+ applyMode: "copy-once",
187
+ automaticRuntimeDiscovery: false,
188
+ suggestion: adminNavigationSuggestion,
189
+ },
26
190
  counts: {
27
- backendSecrets: sources.config.value.backendSecrets.length,
28
- dataResources: sources.config.value.dataResources.length,
29
- eventSubscriptions: sources.config.value.eventSubscriptions.length,
30
- timerSubscriptions: sources.config.value.timerSubscriptions?.length || 0,
31
- workflowDefinitions: sources.config.value.workflowDefinitions.length,
32
- workflowBindings: sources.config.value.workflowBindings.length,
33
- workflowProviders: sources.config.value.workflowProviders?.length || 0,
191
+ backendSecrets: sources.config.value.backend.secrets.length,
192
+ backendOperations: sources.config.value.backend.operations.length,
193
+ frontendRoutes: sources.config.value.frontend.routes.length,
194
+ anonymousPublicPolicies: sources.config.value.frontend.publicAccess?.policies.length || 0,
195
+ dataResources: sources.config.value.data.resources.length,
196
+ aiCapabilities: sources.aiCatalog.value.capabilities.length,
197
+ eventSubscriptions: sources.config.value.events.subscriptions.length,
198
+ timerSubscriptions: sources.config.value.events.timers.length,
199
+ workflowDefinitions: sources.config.value.workflows.definitions.length,
200
+ workflowBindings: sources.config.value.workflows.bindings.length,
201
+ workflowProviders: sources.config.value.workflows.providers.length,
34
202
  },
35
203
  });
36
204
  }
205
+ async contractContext(root, input = {}) {
206
+ const result = await this.contractDescribe(root);
207
+ const data = result.data;
208
+ const groups = {
209
+ resource: data.contract.resources,
210
+ operation: data.contract.operations,
211
+ workflow: data.contract.workflows,
212
+ route: data.contract.routes,
213
+ capability: data.aiCatalog.capabilities,
214
+ };
215
+ const entries = Object.entries(groups).flatMap(([kind, values]) => values.map(value => ({
216
+ selector: `${kind}:${String(value.code || value.resourceCode || value.id)}`,
217
+ kind, code: String(value.code || value.resourceCode || value.id),
218
+ title: String(value.name || value.label || value.title || value.code || value.resourceCode || value.id),
219
+ value,
220
+ })));
221
+ const selector = input.selector || 'index';
222
+ const offset = input.offset ?? 0;
223
+ const limit = input.limit ?? 30;
224
+ if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > 100) {
225
+ throw new Error('CONTRACT_SELECTION_INVALID: offset 必须为非负整数,limit 必须在 1 到 100 之间');
226
+ }
227
+ let selection;
228
+ if (selector === 'index')
229
+ selection = {
230
+ total: entries.length, offset, limit, nextOffset: offset + limit < entries.length ? offset + limit : null,
231
+ sections: ['navigation', 'permissions', 'all'],
232
+ entries: entries.slice(offset, offset + limit).map(({ value: _, ...entry }) => entry),
233
+ };
234
+ else if (selector === 'navigation')
235
+ selection = { adminNavigationAuthoring: data.adminNavigationAuthoring };
236
+ else if (selector === 'permissions')
237
+ selection = { permissionReview: data.permissionReview };
238
+ else if (selector === 'all')
239
+ selection = data;
240
+ else {
241
+ const entry = entries.find(item => item.selector === selector);
242
+ if (!entry)
243
+ throw new Error(`CONTRACT_SELECTION_NOT_FOUND: 未知契约选择器 ${selector},先读取 index`);
244
+ selection = entry.value;
245
+ }
246
+ if (Buffer.byteLength(JSON.stringify(selection)) > 256 * 1024)
247
+ throw new Error('CONTRACT_SELECTION_TOO_LARGE: 选择结果超过 256 KiB,请按模型或能力读取');
248
+ return { ...result, data: { configDigest: data.configDigest, contractDigest: data.contractDigest, aiCatalogDigest: data.aiCatalogDigest, counts: data.counts, selector, selection } };
249
+ }
37
250
  async platformCapabilities(root) {
38
251
  const workspace = await this.workspace(root);
39
252
  const capabilities = await (await this.client(workspace.root)).capabilities();
40
253
  return this.ok("platform.capabilities", workspace.context.workspace, capabilities);
41
254
  }
255
+ async administrationContext(root, environmentKey = "preproduction") {
256
+ const workspace = await this.workspace(root);
257
+ const environment = this.deploymentEnvironment(environmentKey);
258
+ const client = await this.client(workspace.root);
259
+ return this.ok("admin.context", workspace.context.workspace, await client.applicationAdministrationContext(workspace.config.app.code, environment));
260
+ }
261
+ async workflowNodeConfigurations(root, workflowCode, environmentKey = "preproduction") {
262
+ const workspace = await this.workspace(root);
263
+ const environment = this.deploymentEnvironment(environmentKey);
264
+ if (!workflowCode || workflowCode.length > 128)
265
+ throw new Error("WORKFLOW_CODE_REQUIRED");
266
+ const client = await this.client(workspace.root);
267
+ return this.ok("admin.workflow", workspace.context.workspace, await client.workflowNodeConfigurations(workspace.config.app.code, workflowCode, environment));
268
+ }
42
269
  async provisionApplication(root) {
43
270
  const workspace = await this.workspace(root);
271
+ const capsuleDiagnostics = this.toolchainDiagnostics(workspace);
272
+ if (capsuleDiagnostics.length > 0) {
273
+ return this.result("app.provision", workspace.context.workspace, undefined, capsuleDiagnostics);
274
+ }
44
275
  const application = await (await this.client(workspace.root)).provisionApplication({
45
276
  appCode: workspace.config.app.code,
46
277
  name: workspace.config.app.name,
@@ -56,33 +287,34 @@ export class OpenXiangdaApplicationServices {
56
287
  }
57
288
  async runtimeOAuthCredentialStatus(root, environmentKey) {
58
289
  const workspace = await this.workspace(root);
59
- const data = await (await this.client(workspace.root)).runtimeOAuthCredentialStatus(workspace.config.app.code, environmentKey);
290
+ const environment = this.deploymentEnvironment(environmentKey);
291
+ const data = await (await this.client(workspace.root)).runtimeOAuthCredentialStatus(workspace.config.app.code, environment);
60
292
  return this.ok("oauth.runtime.status", workspace.context.workspace, data);
61
293
  }
62
294
  async rotateRuntimeOAuthCredential(root, input) {
63
295
  const workspace = await this.workspace(root);
64
296
  const client = await this.client(workspace.root);
65
- const status = await client.runtimeOAuthCredentialStatus(workspace.config.app.code, input.environmentKey);
297
+ const environment = this.deploymentEnvironment(input.environmentKey);
298
+ const status = await client.runtimeOAuthCredentialStatus(workspace.config.app.code, environment);
66
299
  if (!status.configured || !status.client) {
67
300
  throw new Error("OAUTH2_RUNTIME_CLIENT_NOT_PROVISIONED");
68
301
  }
69
- const rotation = await client.stageRuntimeOAuthCredentialRotation(workspace.config.app.code, input.environmentKey, {
302
+ const rotation = await client.stageRuntimeOAuthCredentialRotation(workspace.config.app.code, environment, {
70
303
  expectedCredentialVersion: status.client.credentialVersion,
71
304
  ...(input.gracePeriodSeconds === undefined
72
305
  ? {}
73
306
  : { gracePeriodSeconds: input.gracePeriodSeconds }),
74
307
  idempotencyKey: input.idempotencyKey,
75
308
  });
76
- const head = await client.environmentHead(workspace.config.app.code, input.environmentKey);
309
+ const head = await client.environmentHead(workspace.config.app.code, environment);
77
310
  if (!head.activeAppVersionId) {
78
311
  throw new Error("OAUTH2_RUNTIME_ROTATION_ACTIVE_VERSION_REQUIRED");
79
312
  }
80
- const deployment = await client.promote({
313
+ const deployment = await client.redeploy({
81
314
  appCode: workspace.config.app.code,
82
315
  appVersionId: head.activeAppVersionId,
83
- ...(head.environmentId
84
- ? { environmentId: head.environmentId }
85
- : { environmentKind: head.environmentKind }),
316
+ ...(head.environmentId ? { environmentId: head.environmentId } : {}),
317
+ environmentKind: head.environmentKind,
86
318
  idempotencyKey: `oauth-runtime-rotation:${input.idempotencyKey}`,
87
319
  requestId: input.idempotencyKey,
88
320
  });
@@ -97,12 +329,22 @@ export class OpenXiangdaApplicationServices {
97
329
  }
98
330
  async createOAuthClient(root, input) {
99
331
  const workspace = await this.workspace(root);
100
- const data = await (await this.client(workspace.root)).createOAuthClient(workspace.config.app.code, input);
332
+ const environmentKey = this.deploymentEnvironment(input.environmentKey);
333
+ const data = await (await this.client(workspace.root)).createOAuthClient(workspace.config.app.code, {
334
+ ...input,
335
+ environmentKey,
336
+ });
101
337
  return this.ok("oauth.client.create", workspace.context.workspace, data);
102
338
  }
103
339
  async updateOAuthClient(root, clientId, input) {
104
340
  const workspace = await this.workspace(root);
105
- const data = await (await this.client(workspace.root)).updateOAuthClient(workspace.config.app.code, clientId, input);
341
+ const environmentKey = input.environmentKey
342
+ ? this.deploymentEnvironment(input.environmentKey)
343
+ : undefined;
344
+ const data = await (await this.client(workspace.root)).updateOAuthClient(workspace.config.app.code, clientId, {
345
+ ...input,
346
+ ...(environmentKey ? { environmentKey } : {}),
347
+ });
106
348
  return this.ok("oauth.client.update", workspace.context.workspace, data);
107
349
  }
108
350
  async rotateOAuthClientSecret(root, clientId, gracePeriodSeconds = 600) {
@@ -117,39 +359,149 @@ export class OpenXiangdaApplicationServices {
117
359
  }
118
360
  async oauthAuditEvents(root, input = {}) {
119
361
  const workspace = await this.workspace(root);
120
- const data = await (await this.client(workspace.root)).oauthAuditEvents(workspace.config.app.code, input);
362
+ const environmentKey = input.environmentKey
363
+ ? this.deploymentEnvironment(input.environmentKey)
364
+ : undefined;
365
+ const data = await (await this.client(workspace.root)).oauthAuditEvents(workspace.config.app.code, {
366
+ ...input,
367
+ ...(environmentKey ? { environmentKey } : {}),
368
+ });
121
369
  return this.ok("oauth.audit.list", workspace.context.workspace, data);
122
370
  }
123
371
  async applicationSecrets(root, environmentKey) {
124
372
  const workspace = await this.workspace(root);
125
- const data = await (await this.client(workspace.root)).applicationSecrets(workspace.config.app.code, environmentKey);
373
+ const environment = this.deploymentEnvironment(environmentKey);
374
+ const data = await (await this.client(workspace.root)).applicationSecrets(workspace.config.app.code, environment);
126
375
  return this.ok("secret.list", workspace.context.workspace, data);
127
376
  }
128
377
  async createApplicationSecret(root, environmentKey, input) {
129
378
  const workspace = await this.workspace(root);
130
- const data = await (await this.client(workspace.root)).createApplicationSecret(workspace.config.app.code, environmentKey, input);
379
+ const environment = this.deploymentEnvironment(environmentKey);
380
+ const data = await (await this.client(workspace.root)).createApplicationSecret(workspace.config.app.code, environment, input);
131
381
  return this.ok("secret.create", workspace.context.workspace, data);
132
382
  }
133
383
  async updateApplicationSecret(root, environmentKey, name, input) {
134
384
  const workspace = await this.workspace(root);
135
- const data = await (await this.client(workspace.root)).updateApplicationSecret(workspace.config.app.code, environmentKey, name, input);
385
+ const environment = this.deploymentEnvironment(environmentKey);
386
+ const data = await (await this.client(workspace.root)).updateApplicationSecret(workspace.config.app.code, environment, name, input);
136
387
  return this.ok("secret.update", workspace.context.workspace, data);
137
388
  }
138
389
  async rotateApplicationSecret(root, environmentKey, name, input) {
139
390
  const workspace = await this.workspace(root);
140
- const data = await (await this.client(workspace.root)).rotateApplicationSecret(workspace.config.app.code, environmentKey, name, input);
391
+ const environment = this.deploymentEnvironment(environmentKey);
392
+ const data = await (await this.client(workspace.root)).rotateApplicationSecret(workspace.config.app.code, environment, name, input);
141
393
  return this.ok("secret.rotate", workspace.context.workspace, data);
142
394
  }
143
395
  async deleteApplicationSecret(root, environmentKey, name, input) {
144
396
  const workspace = await this.workspace(root);
145
- const data = await (await this.client(workspace.root)).deleteApplicationSecret(workspace.config.app.code, environmentKey, name, input);
397
+ const environment = this.deploymentEnvironment(environmentKey);
398
+ const data = await (await this.client(workspace.root)).deleteApplicationSecret(workspace.config.app.code, environment, name, input);
146
399
  return this.ok("secret.delete", workspace.context.workspace, data);
147
400
  }
148
401
  async applicationSecretAuditEvents(root, environmentKey, name, limit = 100) {
149
402
  const workspace = await this.workspace(root);
150
- const data = await (await this.client(workspace.root)).applicationSecretAuditEvents(workspace.config.app.code, environmentKey, name, limit);
403
+ const environment = this.deploymentEnvironment(environmentKey);
404
+ const data = await (await this.client(workspace.root)).applicationSecretAuditEvents(workspace.config.app.code, environment, name, limit);
151
405
  return this.ok("secret.audit.list", workspace.context.workspace, data);
152
406
  }
407
+ async nativeRoleMemberships(root, environmentKey, input = {}) {
408
+ const workspace = await this.workspace(root);
409
+ const environment = this.deploymentEnvironment(environmentKey);
410
+ const client = await this.client(workspace.root);
411
+ const data = await client.nativeRoleMemberships(workspace.config.app.code, { ...input, environmentKey: environment });
412
+ return this.ok("authz.membership.list", workspace.context.workspace, data);
413
+ }
414
+ async createNativeRoleMembership(root, input) {
415
+ const workspace = await this.workspace(root);
416
+ const environment = this.deploymentEnvironment(input.environmentKey);
417
+ const client = await this.client(workspace.root);
418
+ const data = await client.createNativeRoleMembership(workspace.config.app.code, { ...input, environmentKey: environment });
419
+ return this.ok("authz.membership.create", workspace.context.workspace, data);
420
+ }
421
+ async updateNativeRoleMembership(root, membershipId, input) {
422
+ const workspace = await this.workspace(root);
423
+ const environment = this.deploymentEnvironment(input.environmentKey);
424
+ const client = await this.client(workspace.root);
425
+ const data = await client.updateNativeRoleMembership(workspace.config.app.code, membershipId, { ...input, environmentKey: environment });
426
+ return this.ok("authz.membership.update", workspace.context.workspace, data);
427
+ }
428
+ async revokeNativeRoleMembership(root, membershipId, input) {
429
+ const workspace = await this.workspace(root);
430
+ const environment = this.deploymentEnvironment(input.environmentKey);
431
+ const client = await this.client(workspace.root);
432
+ const data = await client.revokeNativeRoleMembership(workspace.config.app.code, membershipId, { ...input, environmentKey: environment });
433
+ return this.ok("authz.membership.revoke", workspace.context.workspace, data);
434
+ }
435
+ async nativeSuperAdmins(root, environmentKey, input = {}) {
436
+ const workspace = await this.workspace(root);
437
+ const environment = this.deploymentEnvironment(environmentKey);
438
+ const client = await this.client(workspace.root);
439
+ const data = await client.nativeSuperAdmins(workspace.config.app.code, { ...input, environmentKey: environment });
440
+ return this.ok("authz.super-admin.list", workspace.context.workspace, data);
441
+ }
442
+ async grantNativeSuperAdmin(root, environmentKey, input) {
443
+ const workspace = await this.workspace(root);
444
+ const environment = this.deploymentEnvironment(environmentKey);
445
+ const client = await this.client(workspace.root);
446
+ const data = await client.grantNativeSuperAdmin(workspace.config.app.code, { ...input, environmentKey: environment });
447
+ return this.ok("authz.super-admin.grant", workspace.context.workspace, data);
448
+ }
449
+ async revokeNativeSuperAdmin(root, environmentKey, grantId, input) {
450
+ const workspace = await this.workspace(root);
451
+ const environment = this.deploymentEnvironment(environmentKey);
452
+ const client = await this.client(workspace.root);
453
+ const data = await client.revokeNativeSuperAdmin(workspace.config.app.code, grantId, { ...input, environmentKey: environment });
454
+ return this.ok("authz.super-admin.revoke", workspace.context.workspace, data);
455
+ }
456
+ async nativeRelationshipGrants(root, environmentKey, input = {}) {
457
+ const workspace = await this.workspace(root);
458
+ const environment = this.deploymentEnvironment(environmentKey);
459
+ const client = await this.client(workspace.root);
460
+ const data = await client.nativeRelationshipGrants(workspace.config.app.code, { ...input, environmentKey: environment });
461
+ return this.ok("authz.relationship.list", workspace.context.workspace, data);
462
+ }
463
+ async createNativeRelationshipGrant(root, input) {
464
+ const workspace = await this.workspace(root);
465
+ const environment = this.deploymentEnvironment(input.environmentKey);
466
+ const client = await this.client(workspace.root);
467
+ const data = await client.createNativeRelationshipGrant(workspace.config.app.code, { ...input, environmentKey: environment });
468
+ return this.ok("authz.relationship.create", workspace.context.workspace, data);
469
+ }
470
+ async updateNativeRelationshipGrant(root, grantId, input) {
471
+ const workspace = await this.workspace(root);
472
+ const environment = this.deploymentEnvironment(input.environmentKey);
473
+ const client = await this.client(workspace.root);
474
+ const data = await client.updateNativeRelationshipGrant(workspace.config.app.code, grantId, { ...input, environmentKey: environment });
475
+ return this.ok("authz.relationship.update", workspace.context.workspace, data);
476
+ }
477
+ async revokeNativeRelationshipGrant(root, grantId, input) {
478
+ const workspace = await this.workspace(root);
479
+ const environment = this.deploymentEnvironment(input.environmentKey);
480
+ const client = await this.client(workspace.root);
481
+ const data = await client.revokeNativeRelationshipGrant(workspace.config.app.code, grantId, { ...input, environmentKey: environment });
482
+ return this.ok("authz.relationship.revoke", workspace.context.workspace, data);
483
+ }
484
+ async nativeAuthorizationProjectionHealth(root, environmentKey) {
485
+ const workspace = await this.workspace(root);
486
+ const environment = this.deploymentEnvironment(environmentKey);
487
+ const data = await (await this.client(workspace.root)).nativeAuthorizationProjectionHealth(workspace.config.app.code, environment);
488
+ return this.ok("authz.projection.health", workspace.context.workspace, data);
489
+ }
490
+ async rebuildNativeAuthorizationProjections(root, input) {
491
+ const workspace = await this.workspace(root);
492
+ const environment = this.deploymentEnvironment(input.environmentKey);
493
+ const data = await (await this.client(workspace.root)).rebuildNativeAuthorizationProjections(workspace.config.app.code, {
494
+ ...input,
495
+ environmentKey: environment,
496
+ });
497
+ return this.ok("authz.projection.rebuild", workspace.context.workspace, data);
498
+ }
499
+ async recoverNativeAuthorizationProjection(root, jobId, input) {
500
+ const workspace = await this.workspace(root);
501
+ const environment = this.deploymentEnvironment(input.environmentKey);
502
+ const data = await (await this.client(workspace.root)).recoverNativeAuthorizationProjection(workspace.config.app.code, jobId, { ...input, environmentKey: environment });
503
+ return this.ok("authz.projection.recover", workspace.context.workspace, data);
504
+ }
153
505
  async eventSubscriptions(root) {
154
506
  const workspace = await this.workspace(root);
155
507
  const data = await (await this.client(workspace.root)).eventSubscriptions(workspace.config.app.code);
@@ -160,11 +512,6 @@ export class OpenXiangdaApplicationServices {
160
512
  const data = await (await this.client(workspace.root)).setEventSubscriptionStatus(workspace.config.app.code, subscriptionId, input);
161
513
  return this.ok("event.subscription.status", workspace.context.workspace, data);
162
514
  }
163
- async rotateEventSubscriptionSecret(root, subscriptionId, input) {
164
- const workspace = await this.workspace(root);
165
- const data = await (await this.client(workspace.root)).rotateEventSubscriptionSecret(workspace.config.app.code, subscriptionId, input);
166
- return this.ok("event.subscription.rotate", workspace.context.workspace, data);
167
- }
168
515
  async eventDeliveries(root, limit = 100) {
169
516
  const workspace = await this.workspace(root);
170
517
  const data = await (await this.client(workspace.root)).eventDeliveries(workspace.config.app.code, limit);
@@ -187,36 +534,46 @@ export class OpenXiangdaApplicationServices {
187
534
  }
188
535
  async workflowAssigneeProviders(root, environmentKey) {
189
536
  const workspace = await this.workspace(root);
190
- const data = await (await this.client(workspace.root)).workflowAssigneeProviders(workspace.config.app.code, environmentKey);
537
+ const environment = this.deploymentEnvironment(environmentKey);
538
+ const data = await (await this.client(workspace.root)).workflowAssigneeProviders(workspace.config.app.code, environment);
191
539
  return this.ok("workflow.provider.list", workspace.context.workspace, data);
192
540
  }
193
541
  async rotateWorkflowAssigneeProviderSecret(root, providerCode, input) {
194
542
  const workspace = await this.workspace(root);
195
- const data = await (await this.client(workspace.root)).rotateWorkflowAssigneeProviderSecret(workspace.config.app.code, providerCode, input);
543
+ const environmentKey = this.deploymentEnvironment(input.environmentKey);
544
+ const data = await (await this.client(workspace.root)).rotateWorkflowAssigneeProviderSecret(workspace.config.app.code, providerCode, { ...input, environmentKey });
196
545
  return this.ok("workflow.provider.rotate", workspace.context.workspace, data);
197
546
  }
198
547
  async linkApplication(root, input) {
199
548
  const workspace = await this.workspace(root);
200
549
  const path = join(workspace.root, ".openxiangda", "link.json");
550
+ const environments = (input.environments || []).map((environment) => ({
551
+ ...environment,
552
+ kind: this.deploymentEnvironment(environment.kind),
553
+ }));
201
554
  mkdirSync(dirname(path), { recursive: true });
202
555
  const value = {
203
556
  schemaVersion: 2,
204
557
  appCode: workspace.config.app.code,
205
- baseUrl: input.baseUrl.replace(/\/+$/, ""),
206
- environments: input.environments || [],
558
+ baseUrl: normalizePlatformBaseUrl(input.baseUrl),
559
+ environments,
207
560
  };
208
561
  writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
209
562
  return this.ok("app.link", workspace.context.workspace, { path, ...value });
210
563
  }
211
564
  async generate(input = {}) {
212
565
  const workspace = await this.workspace(input.root);
213
- const sources = compileApplicationSources(workspace.config);
566
+ const capsuleDiagnostics = this.toolchainDiagnostics(workspace);
567
+ if (capsuleDiagnostics.length > 0) {
568
+ return this.result("generate", workspace.context.workspace, undefined, capsuleDiagnostics);
569
+ }
570
+ const sources = compileApplicationSources(workspace.config, this.toolchainVersion);
214
571
  const output = join(workspace.root, "packages/contracts/src/generated.ts");
215
572
  const current = existsSync(output) ? readFileSync(output, "utf8") : "";
216
573
  const changed = current !== sources.contracts.typescript;
217
574
  const diagnostics = [];
218
575
  if (input.check && changed) {
219
- diagnostics.push(this.diagnostic("GENERATED_CONTRACTS_OUTDATED", "生成契约与应用声明不一致", relative(workspace.root, output), "运行 openxiangda generate 并提交生成结果"));
576
+ diagnostics.push(this.diagnostic("GENERATED_CONTRACTS_OUTDATED", "生成契约与应用声明不一致", relative(workspace.root, output), "运行 openxiangda check 并提交生成结果"));
220
577
  }
221
578
  else if (changed) {
222
579
  mkdirSync(dirname(output), { recursive: true });
@@ -226,42 +583,136 @@ export class OpenXiangdaApplicationServices {
226
583
  output,
227
584
  changed,
228
585
  contractDigest: sources.contracts.digest,
586
+ aiCatalogDigest: sources.aiCatalog.digest,
229
587
  }, diagnostics, changed && input.check
230
588
  ? [
231
589
  {
232
590
  code: "generate",
233
591
  label: "更新生成契约",
234
- command: "openxiangda generate",
592
+ command: "openxiangda check",
235
593
  },
236
594
  ]
237
595
  : []);
238
596
  }
239
- async check(root) {
597
+ async check(root, environmentKey) {
240
598
  const workspace = await this.workspace(root);
599
+ const appSpec = inspectAppSpec(workspace.root, this.appSpecContractIndex(workspace, false));
600
+ const lifecycle = developmentLifecycle(workspace.root, this.appSpecContractIndex(workspace, false));
241
601
  const diagnostics = [
242
602
  ...validateAppConfig(workspace.config),
243
603
  ...this.versionTrainDiagnostics(workspace),
604
+ ...validateNestInjectionContract(workspace.root),
605
+ ...validateApplicationUiContract(workspace.root, workspace.config.frontend.root),
606
+ ...advisoryAppSpecDiagnostics(appSpec.diagnostics),
607
+ ...advisoryAppSpecDiagnostics(lifecycle.diagnostics),
244
608
  ];
245
- const generated = await this.generate({
246
- root: workspace.root,
247
- check: true,
248
- });
609
+ if (diagnostics.some(item => item.severity === "error")) {
610
+ const sealedArtifact = this.writeCheckSealedArtifactStatus(workspace, false);
611
+ return this.result("check", workspace.context.workspace, {
612
+ generated: undefined,
613
+ compatibility: null,
614
+ stages: [],
615
+ sealedArtifact,
616
+ appSpec: { ...summarizeAppSpecContext(appSpec), lifecycle: summarizeLifecycle(lifecycle) },
617
+ }, diagnostics);
618
+ }
619
+ let compatibility = null;
620
+ try {
621
+ const local = await operationStage('local-configuration', '本地完整配置校验', async () => compileLocalConfiguration(workspace.config, this.toolchainVersion));
622
+ if (environmentKey) {
623
+ compatibility = await operationStage('target-preflight', '目标平台只读预检', () => this.validateTargetConfigurationCompatibility(workspace, environmentKey, local));
624
+ }
625
+ }
626
+ catch (error) {
627
+ diagnostics.push(this.configurationCompatibilityDiagnostic(error));
628
+ const sealedArtifact = this.writeCheckSealedArtifactStatus(workspace, false);
629
+ return this.result("check", workspace.context.workspace, {
630
+ generated: undefined,
631
+ compatibility: null,
632
+ stages: [],
633
+ sealedArtifact,
634
+ appSpec: { ...summarizeAppSpecContext(appSpec), lifecycle: summarizeLifecycle(lifecycle) },
635
+ }, diagnostics);
636
+ }
637
+ const generated = await operationStage('generate', '生成应用契约', () => this.generate({ root: workspace.root }));
249
638
  diagnostics.push(...generated.diagnostics);
250
- const command = this.runPackageScript(workspace.root, "check");
251
- if (!command.ok) {
252
- diagnostics.push(this.diagnostic("WORKSPACE_CHECK_FAILED", "工作区 check 脚本失败", "package.json#scripts.check", "修复类型、契约或静态检查错误后重试", { output: command.output }));
639
+ if (!generated.ok) {
640
+ const sealedArtifact = this.writeCheckSealedArtifactStatus(workspace, false);
641
+ return this.result("check", workspace.context.workspace, {
642
+ generated: generated.data,
643
+ compatibility: null,
644
+ stages: [],
645
+ sealedArtifact,
646
+ appSpec: { ...summarizeAppSpecContext(appSpec), lifecycle: summarizeLifecycle(lifecycle) },
647
+ }, diagnostics, generated.nextActions);
648
+ }
649
+ const backendDiagnostics = this.initializeBackend(workspace);
650
+ if (backendDiagnostics.length) {
651
+ return this.result("check", workspace.context.workspace, undefined, backendDiagnostics);
253
652
  }
254
- return this.result("check", workspace.context.workspace, { stages: [{ name: "workspace", ...command }] }, diagnostics);
653
+ const stages = [];
654
+ const cache = this.deliveryCacheContext(workspace);
655
+ const inputDigest = deliveryInputDigest(cache);
656
+ const reusable = reusableValidation(cache, inputDigest);
657
+ for (const name of ["check", "test", "build"]) {
658
+ if (reusable) {
659
+ await operationStage(name, `${this.packageScriptLabel(name)}(复用已验证结果)`, async () => {
660
+ updateOperationStage(name, `${this.packageScriptLabel(name)}(复用已验证结果)`, { reused: true, inputDigest, outputDigest: reusable.outputDigest });
661
+ });
662
+ stages.push({ name, state: 'passed', ok: true, exitCode: 0, output: '输入与构建输出摘要未变,复用已通过的本地检查', reused: true });
663
+ continue;
664
+ }
665
+ if (stages.some(stage => stage.state === 'failed')) {
666
+ stages.push({ name, state: 'skipped', ok: false, exitCode: null, output: '前置阶段失败,未执行' });
667
+ skippedOperationStage(name, this.packageScriptLabel(name));
668
+ continue;
669
+ }
670
+ const result = await this.runPackageScript(workspace.root, name);
671
+ stages.push({ name, ...result, state: result.ok ? 'passed' : 'failed' });
672
+ }
673
+ for (const stage of stages.filter(item => item.state === 'failed')) {
674
+ const code = `WORKSPACE_${stage.name.toUpperCase()}_FAILED`;
675
+ diagnostics.push(this.diagnostic(code, `工作区 ${stage.name} 脚本失败`, `package.json#scripts.${stage.name}`, "修复失败后重新运行 openxiangda check", { output: stage.output }));
676
+ }
677
+ if (!reusable && stages.every(stage => stage.state === 'passed')) {
678
+ try {
679
+ recordValidation(cache, inputDigest);
680
+ }
681
+ catch (error) {
682
+ diagnostics.push(this.diagnostic('OPENXIANGDA_VALIDATION_INPUT_CHANGED', error.message, 'workspace', '等待源码修改结束后重新运行 openxiangda check'));
683
+ }
684
+ }
685
+ const passed = !diagnostics.some(item => item.severity === "error");
686
+ const sealedArtifact = this.writeCheckSealedArtifactStatus(workspace, passed);
687
+ return this.result("check", workspace.context.workspace, {
688
+ generated: generated.data,
689
+ compatibility,
690
+ stages,
691
+ sealedArtifact,
692
+ appSpec: { ...summarizeAppSpecContext(appSpec), lifecycle: summarizeLifecycle(lifecycle) },
693
+ }, diagnostics, passed
694
+ ? [
695
+ {
696
+ code: "deploy",
697
+ label: "密封并部署到预发",
698
+ command: "openxiangda deploy",
699
+ },
700
+ ]
701
+ : []);
255
702
  }
256
703
  async test(root) {
257
704
  const workspace = await this.workspace(root);
258
- const command = this.runPackageScript(workspace.root, "test");
259
- const diagnostics = command.ok
705
+ const diagnostics = this.toolchainDiagnostics(workspace);
706
+ if (diagnostics.length > 0) {
707
+ return this.result("test", workspace.context.workspace, undefined, diagnostics);
708
+ }
709
+ const command = await this.runPackageScript(workspace.root, "test");
710
+ const commandDiagnostics = command.ok
260
711
  ? []
261
712
  : [
262
713
  this.diagnostic("WORKSPACE_TEST_FAILED", "工作区测试失败", "package.json#scripts.test", "修复失败测试后重试", { output: command.output }),
263
714
  ];
264
- return this.result("test", workspace.context.workspace, command, diagnostics);
715
+ return this.result("test", workspace.context.workspace, command, commandDiagnostics);
265
716
  }
266
717
  async build(input = {}) {
267
718
  const sealed = await this.buildSealed(input);
@@ -271,37 +722,65 @@ export class OpenXiangdaApplicationServices {
271
722
  ...sealed,
272
723
  data: {
273
724
  package: sealed.data.package,
725
+ sealedArtifact: sealed.data.sealedArtifact,
274
726
  ...(sealed.data.outputDirectory
275
727
  ? { outputDirectory: sealed.data.outputDirectory }
276
728
  : {}),
277
729
  },
278
730
  };
279
731
  }
732
+ async buildPreview(root) {
733
+ const checked = await this.check(root);
734
+ return {
735
+ ...checked,
736
+ operation: "build.preview",
737
+ data: {
738
+ sealed: false,
739
+ packageDigest: null,
740
+ check: checked.data,
741
+ },
742
+ };
743
+ }
280
744
  async buildSealed(input = {}) {
281
745
  const workspace = await this.workspace(input.root);
282
- const diagnostics = [];
283
- await this.generate({ root: workspace.root });
746
+ const diagnostics = this.toolchainDiagnostics(workspace);
747
+ if (diagnostics.length > 0) {
748
+ return this.result("build", workspace.context.workspace, undefined, diagnostics);
749
+ }
750
+ const generated = await this.generate({ root: workspace.root });
751
+ if (!generated.ok) {
752
+ return this.result("build", workspace.context.workspace, undefined, generated.diagnostics, generated.nextActions);
753
+ }
284
754
  if (!input.skipWorkspaceBuild) {
285
- const command = this.runPackageScript(workspace.root, "build");
755
+ const backendDiagnostics = this.initializeBackend(workspace);
756
+ if (backendDiagnostics.length) {
757
+ return this.result("build", workspace.context.workspace, undefined, backendDiagnostics);
758
+ }
759
+ const command = await this.runPackageScript(workspace.root, "build");
286
760
  if (!command.ok) {
287
761
  diagnostics.push(this.diagnostic("WORKSPACE_BUILD_FAILED", "前后端生产构建失败", "package.json#scripts.build", "修复构建错误后重试", { output: command.output }));
288
762
  return this.result("build", workspace.context.workspace, undefined, diagnostics);
289
763
  }
290
764
  }
291
- const backendImage = String(input.backendImage || process.env.OPENXIANGDA_BACKEND_IMAGE || "").trim();
292
- if (!backendImage) {
293
- diagnostics.push(this.diagnostic("BACKEND_IMAGE_REQUIRED", "当前应用包含 NestJS 后端,密封 AppPackage 时必须提供不可变 OCI image", "backend.image", "通过 --backend-image <registry/repository@sha256:digest> 或 OPENXIANGDA_BACKEND_IMAGE 提供"));
765
+ const hasBackend = backendRuntimeRequired(workspace.config);
766
+ const backendImage = String(input.backendImage || "").trim();
767
+ if (hasBackend && !backendImage) {
768
+ diagnostics.push(this.diagnostic("BACKEND_IMAGE_REQUIRED", "当前应用包含 NestJS 后端,密封 AppPackage 时必须提供不可变 OCI image", "backend.image", "运行 openxiangda deploy 由工具自动构建并推送官方后端镜像"));
294
769
  return this.result("build", workspace.context.workspace, undefined, diagnostics);
295
770
  }
296
- if (!/@sha256:[a-f0-9]{64}$/i.test(backendImage)) {
297
- diagnostics.push(this.diagnostic("BACKEND_IMAGE_NOT_IMMUTABLE", "后端镜像必须使用不可变 sha256 digest,不能使用可变 tag", "backend.image", "将镜像引用改为 <registry/repository@sha256:64位摘要>"));
771
+ if (hasBackend && !/@sha256:[a-f0-9]{64}$/i.test(backendImage)) {
772
+ diagnostics.push(this.diagnostic("BACKEND_IMAGE_NOT_IMMUTABLE", "后端镜像必须使用不可变 sha256 digest,不能使用可变 tag", "backend.image", "运行 openxiangda deploy 重新构建并取得不可变镜像摘要"));
298
773
  return this.result("build", workspace.context.workspace, undefined, diagnostics);
299
774
  }
300
- const sources = compileApplicationSources(workspace.config);
775
+ const sources = compileApplicationSources(workspace.config, this.toolchainVersion);
301
776
  const frontendContent = this.directoryBundle(join(workspace.root, workspace.config.frontend.root, "dist"));
302
777
  const frontendDigest = sha256Bytes(frontendContent);
303
- const backendContent = canonicalJson({ image: backendImage });
304
- const backendDigest = sha256Bytes(backendContent);
778
+ const backendContent = hasBackend
779
+ ? canonicalJson({ image: backendImage })
780
+ : undefined;
781
+ const backendDigest = backendContent
782
+ ? sha256Bytes(backendContent)
783
+ : undefined;
305
784
  const artifacts = [
306
785
  {
307
786
  kind: "frontend",
@@ -310,16 +789,19 @@ export class OpenXiangdaApplicationServices {
310
789
  size: Buffer.byteLength(frontendContent),
311
790
  entrypoint: "index.html",
312
791
  },
313
- {
314
- kind: "backend",
315
- digest: backendDigest,
316
- mediaType: "application/vnd.oci.image.manifest.v1+json",
317
- size: Buffer.byteLength(backendContent),
318
- metadata: {
319
- imageDigest: backendImage,
320
- port: 3000,
321
- },
322
- },
792
+ ...(backendDigest && backendContent
793
+ ? [
794
+ {
795
+ kind: "backend",
796
+ digest: backendDigest,
797
+ mediaType: "application/vnd.oci.image.manifest.v1+json",
798
+ size: Buffer.byteLength(backendContent),
799
+ metadata: {
800
+ imageDigest: backendImage,
801
+ },
802
+ },
803
+ ]
804
+ : []),
323
805
  sources.config.artifact,
324
806
  sources.contracts.artifact,
325
807
  ];
@@ -339,22 +821,33 @@ export class OpenXiangdaApplicationServices {
339
821
  artifacts,
340
822
  manifests: {
341
823
  frontend: frontendDigest,
342
- backend: backendDigest,
824
+ ...(backendDigest ? { backend: backendDigest } : {}),
343
825
  config: sources.config.digest,
344
826
  dataContract: sources.contracts.digest,
345
827
  },
346
- minimumPlatformVersion: "2.0.0-alpha.1",
828
+ minimumPlatformVersion: "2.0.0-alpha.5",
347
829
  metadata: {
348
- runtimeProfile: workspace.config.backend.runMode || "shared",
830
+ backend: hasBackend
831
+ ? {
832
+ present: true,
833
+ isolation: workspace.config.backend.isolation || "shared",
834
+ resourceProfile: workspace.config.backend.resourceProfile || "light",
835
+ }
836
+ : { present: false },
837
+ aiCatalogDigest: sources.aiCatalog.digest,
838
+ aiCatalog: sources.aiCatalog.value,
349
839
  },
350
840
  });
351
841
  const artifactContent = {
352
842
  [frontendDigest]: frontendContent,
353
- [backendDigest]: backendContent,
354
843
  [sources.config.digest]: sources.config.content,
355
844
  [sources.contracts.digest]: sources.contracts.content,
356
845
  };
846
+ if (backendDigest && backendContent) {
847
+ artifactContent[backendDigest] = backendContent;
848
+ }
357
849
  let outputDirectory;
850
+ const sealedArtifact = this.sealedArtifactStatus(workspace, compiled.digest, input.write !== false);
358
851
  if (input.write !== false) {
359
852
  outputDirectory = join(workspace.root, ".openxiangda", "build");
360
853
  mkdirSync(join(outputDirectory, "artifacts"), { recursive: true });
@@ -362,49 +855,125 @@ export class OpenXiangdaApplicationServices {
362
855
  for (const [digest, content] of Object.entries(artifactContent)) {
363
856
  writeFileSync(join(outputDirectory, "artifacts", digest), content, "utf8");
364
857
  }
858
+ this.writeSealedArtifactStatus(workspace.root, sealedArtifact);
365
859
  }
366
860
  return this.result("build", workspace.context.workspace, {
367
861
  package: compiled,
368
862
  artifactContent,
863
+ sealedArtifact,
369
864
  ...(outputDirectory ? { outputDirectory } : {}),
370
865
  }, diagnostics, [
371
866
  {
372
867
  code: "deploy",
373
868
  label: "部署到预发",
374
- command: "openxiangda deploy preproduction",
869
+ command: "openxiangda deploy",
375
870
  },
376
871
  ]);
377
872
  }
378
873
  async deploymentPlan(input) {
379
- const built = await this.build({ ...input, write: false });
380
- if (!built.ok || !built.data)
381
- return built;
382
- return {
383
- ...built,
384
- operation: "deployment.plan",
385
- data: {
386
- environment: input.environment,
387
- packageDigest: built.data.package.digest,
388
- appCode: built.data.package.manifest.appCode,
389
- artifacts: built.data.package.manifest.artifacts,
390
- requiredCapabilities: built.data.package.manifest.compatibility.requiredCapabilities,
391
- },
392
- };
874
+ const workspace = await this.workspace(input.root);
875
+ const diagnostics = this.toolchainDiagnostics(workspace);
876
+ if (diagnostics.length > 0) {
877
+ return this.result("deployment.plan", workspace.context.workspace, undefined, diagnostics);
878
+ }
879
+ const source = await operationStage('source', '核对远端主线与源码', () => publishedDeliverySource(workspace.root));
880
+ const lifecycle = developmentLifecycle(workspace.root, this.appSpecContractIndex(workspace, true));
881
+ if (!lifecycle.readyForTest)
882
+ return this.result('deployment.plan', workspace.context.workspace, { lifecycle: summarizeLifecycle(lifecycle) }, lifecycle.diagnostics);
883
+ const sources = compileApplicationSources(workspace.config, this.toolchainVersion);
884
+ return this.ok("deployment.plan", workspace.context.workspace, {
885
+ environment: input.environment,
886
+ appCode: workspace.config.app.code,
887
+ sealed: false,
888
+ packageDigest: null,
889
+ buildOwner: "openxiangda deploy",
890
+ source,
891
+ frontendRoot: workspace.config.frontend.root,
892
+ backendRoot: workspace.config.backend.root,
893
+ configDigest: sources.config.digest,
894
+ contractDigest: sources.contracts.digest,
895
+ });
393
896
  }
394
897
  async deploy(input) {
395
898
  const workspace = await this.workspace(input.root);
396
- if (input.environment === "production" &&
397
- workspace.context.workspace.dirty) {
899
+ const capsuleDiagnostics = this.toolchainDiagnostics(workspace);
900
+ if (capsuleDiagnostics.length > 0) {
901
+ return this.result("deploy", workspace.context.workspace, undefined, capsuleDiagnostics);
902
+ }
903
+ if (String(input.environment) !== "preproduction") {
398
904
  return this.result("deploy", workspace.context.workspace, undefined, [
399
- this.diagnostic("PRODUCTION_SOURCE_DIRTY", "生产部署只接受 clean Git commit", "git", "提交并推送全部变更后重新构建同一 AppVersion"),
905
+ this.diagnostic("DEPLOY_PREPRODUCTION_ONLY", "Native 2.0 只允许直接部署到预发环境", String(input.environment), "使用 openxiangda deploy;生产环境必须显式复用成功的测试 DeploymentRun"),
400
906
  ]);
401
907
  }
402
- const built = await this.buildSealed({ ...input, root: workspace.root });
908
+ const source = await operationStage('source', '核对远端主线与源码', () => publishedDeliverySource(workspace.root));
909
+ const client = await this.client(workspace.root);
910
+ const capabilities = await client.capabilities();
911
+ const sources = compileApplicationSources(workspace.config, this.toolchainVersion);
912
+ const { assertApplicationContractCompatible, assertRequiredCapabilitiesAvailable, } = await import("./deployment.js");
913
+ assertApplicationContractCompatible(capabilities, {
914
+ appPackageSchemaVersion: SCHEMA_VERSIONS.appPackage,
915
+ configurationBundleSchemaVersion: sources.config.value.schemaVersion,
916
+ contractBundleSchemaVersion: sources.contracts.value.schemaVersion,
917
+ compilerContractVersion: sources.config.value.compilerContractVersion,
918
+ });
919
+ assertRequiredCapabilitiesAvailable(capabilities, requiredPlatformCapabilities(workspace.config));
920
+ const hasBackend = backendRuntimeRequired(workspace.config);
921
+ const generated = await this.generate({ root: workspace.root, check: true });
922
+ if (!generated.ok)
923
+ return { ...generated, operation: 'deploy' };
924
+ const lifecycle = await operationStage('development-records', '核对需求、架构与测试验收计划', async () => {
925
+ const lifecycle = developmentLifecycle(workspace.root, this.appSpecContractIndex(workspace, true));
926
+ return { ...lifecycle, ok: lifecycle.readyForTest };
927
+ });
928
+ if (!lifecycle.readyForTest)
929
+ return this.result('deploy', workspace.context.workspace, { lifecycle: summarizeLifecycle(lifecycle) }, lifecycle.diagnostics);
930
+ const checked = await this.check(workspace.root, input.environment);
931
+ if (!checked.ok)
932
+ return { ...checked, operation: "deploy" };
933
+ await assertDeliverySourceUnchanged(workspace.root, source);
934
+ const cache = this.deliveryCacheContext(workspace);
935
+ const preparedInputDigest = deliveryInputDigest(cache);
936
+ const reused = await operationStage('candidate', '核对可复用的不可变制品', async () => {
937
+ const candidate = await reusableSealedCandidate(cache, source, input.backendImage);
938
+ updateOperationStage('candidate', candidate ? '复用已验证的不可变制品' : '准备构建新制品', { reused: !!candidate, ...(candidate ? { packageDigest: candidate.package.digest } : {}) });
939
+ return candidate;
940
+ });
941
+ const backendImage = !reused && hasBackend
942
+ ? input.backendImage
943
+ ? input.backendImage
944
+ : (await operationStage('backend-image', '构建并推送后端镜像', () => publishBackendImage({
945
+ root: workspace.root,
946
+ backendRoot: workspace.config.backend.root,
947
+ target: backendImageBuildTarget(capabilities, workspace.config.app.code),
948
+ ...(workspace.context.workspace.revision
949
+ ? { sourceRevision: workspace.context.workspace.revision }
950
+ : {}),
951
+ }))).reference
952
+ : undefined;
953
+ await assertDeliverySourceUnchanged(workspace.root, source);
954
+ const built = reused ? this.ok('build', workspace.context.workspace, {
955
+ ...reused, sealedArtifact: this.sealedArtifactStatus(workspace, reused.package.digest, true),
956
+ }) : await operationStage('seal', '校验并冻结不可变制品', () => this.buildSealed({
957
+ ...input,
958
+ root: workspace.root,
959
+ ...(backendImage ? { backendImage } : {}),
960
+ skipWorkspaceBuild: true,
961
+ }));
403
962
  if (!built.ok || !built.data)
404
963
  return built;
405
- const client = await this.client(workspace.root);
964
+ if (preparedInputDigest && preparedInputDigest !== deliveryInputDigest(cache)) {
965
+ throw new DeliverySourceError('OPENXIANGDA_VALIDATION_INPUT_CHANGED', '密封期间源码、依赖或构建环境发生变化,已停止上传', '等待修改结束后重新运行 openxiangda deploy');
966
+ }
967
+ if (!reused)
968
+ recordSealedCandidate(cache, built.data.package);
969
+ const retained = await publishedDeliverySource(workspace.root, source.commit);
970
+ if (retained.repository !== source.repository) {
971
+ throw new DeliverySourceError('DELIVERY_REPOSITORY_CHANGED', '发布准备期间绑定远端发生变化', '恢复并核对原来源仓库,再发布同一候选');
972
+ }
973
+ await assertDeliverySourceUnchanged(workspace.root, source);
406
974
  const deployment = await (await import("./deployment.js")).submitAppPackage({
407
975
  client,
976
+ capabilities,
408
977
  compiledPackage: built.data.package,
409
978
  artifactContent: built.data.artifactContent,
410
979
  environmentKind: input.environment,
@@ -424,23 +993,56 @@ export class OpenXiangdaApplicationServices {
424
993
  async deploymentStatus(root, deploymentId) {
425
994
  const workspace = await this.workspace(root);
426
995
  const client = await this.client(workspace.root);
427
- const data = deploymentId
428
- ? await client.deployment(workspace.config.app.code, deploymentId)
429
- : await client.deployments(workspace.config.app.code, 20);
430
- return this.ok("status", workspace.context.workspace, data);
996
+ let data;
997
+ if (deploymentId) {
998
+ data = await client.deployment(workspace.config.app.code, deploymentId);
999
+ }
1000
+ else {
1001
+ const latest = (await client.deployments(workspace.config.app.code, 1))
1002
+ .items[0];
1003
+ data = latest
1004
+ ? await client.deployment(workspace.config.app.code, latest.id)
1005
+ : undefined;
1006
+ }
1007
+ if (!data) {
1008
+ return this.result("status", workspace.context.workspace, undefined, [
1009
+ this.diagnostic("DEPLOYMENT_NOT_FOUND", "当前应用还没有部署记录", "deploymentId", "运行 openxiangda deploy 创建测试部署"),
1010
+ ], [
1011
+ { code: "deploy", label: "部署测试环境", command: "openxiangda deploy" },
1012
+ ]);
1013
+ }
1014
+ return this.ok("status", workspace.context.workspace, data, this.deploymentRecoveryActions(data));
1015
+ }
1016
+ async environmentStatus(root) {
1017
+ const workspace = await this.workspace(root);
1018
+ const data = await (await this.client(workspace.root)).applicationEnvironments(workspace.config.app.code);
1019
+ return this.ok("environment.status", workspace.context.workspace, data);
1020
+ }
1021
+ async startEnvironment(root, environment, options = {}) {
1022
+ return await this.changeEnvironmentRuntimeState(root, environment, "start", options);
1023
+ }
1024
+ async stopEnvironment(root, environment, options = {}) {
1025
+ return await this.changeEnvironmentRuntimeState(root, environment, "stop", options);
431
1026
  }
432
1027
  async deploymentLogs(root, deploymentId) {
433
1028
  const status = await this.deploymentStatus(root, deploymentId);
1029
+ if (!status.ok || !status.data)
1030
+ return { ...status, operation: "logs" };
434
1031
  const run = status.data;
435
1032
  return {
436
1033
  ...status,
437
1034
  operation: "logs",
438
1035
  data: {
439
- deploymentId,
1036
+ deploymentId: run.id,
440
1037
  status: run.status,
441
1038
  stage: run.stage,
442
1039
  checkpoints: run.checkpoints,
443
1040
  failure: run.failure,
1041
+ rootFailure: run.rootFailure,
1042
+ latestFailure: run.latestFailure,
1043
+ candidate: run.candidate,
1044
+ recovery: run.recovery,
1045
+ attempts: run.attempts,
444
1046
  traceId: run.result?.traceId,
445
1047
  },
446
1048
  };
@@ -448,115 +1050,507 @@ export class OpenXiangdaApplicationServices {
448
1050
  async retry(root, deploymentId) {
449
1051
  const workspace = await this.workspace(root);
450
1052
  const deployment = await (await this.client(workspace.root)).retryDeployment(workspace.config.app.code, deploymentId);
451
- return this.ok("retry", workspace.context.workspace, deployment);
1053
+ return this.ok("retry", workspace.context.workspace, deployment, [
1054
+ {
1055
+ code: "status",
1056
+ label: "查看重试状态",
1057
+ command: `openxiangda status ${deployment.id}`,
1058
+ },
1059
+ ]);
452
1060
  }
453
1061
  async cancel(root, deploymentId) {
454
1062
  const workspace = await this.workspace(root);
455
1063
  const deployment = await (await this.client(workspace.root)).cancelDeployment(workspace.config.app.code, deploymentId);
456
- return this.ok("cancel", workspace.context.workspace, deployment);
1064
+ return this.ok("cancel", workspace.context.workspace, deployment, [
1065
+ {
1066
+ code: "status",
1067
+ label: "查看取消结果",
1068
+ command: `openxiangda status ${deployment.id}`,
1069
+ },
1070
+ ]);
457
1071
  }
458
- async promote(root, deploymentId, environment) {
1072
+ deploymentRecoveryActions(run) {
1073
+ const recovery = run.recovery;
1074
+ return [
1075
+ ...(recovery.nextCommand
1076
+ ? [
1077
+ {
1078
+ code: recovery.retryable ? "retry" : "recover",
1079
+ label: recovery.retryable
1080
+ ? "显式重试失败部署"
1081
+ : "执行平台指定恢复",
1082
+ command: recovery.nextCommand,
1083
+ },
1084
+ ]
1085
+ : []),
1086
+ {
1087
+ code: "logs",
1088
+ label: "查看部署尝试账本",
1089
+ command: `openxiangda logs ${run.id}`,
1090
+ },
1091
+ ...(recovery.cancelAllowed
1092
+ ? [
1093
+ {
1094
+ code: "cancel",
1095
+ label: "取消未激活部署",
1096
+ command: `openxiangda cancel ${run.id}`,
1097
+ },
1098
+ ]
1099
+ : []),
1100
+ ];
1101
+ }
1102
+ async productionDeploymentPlan(root, deploymentId) {
459
1103
  const workspace = await this.workspace(root);
460
1104
  const client = await this.client(workspace.root);
461
1105
  const source = await client.deployment(workspace.config.app.code, deploymentId);
1106
+ const appVersionId = String(source.result?.applicationVersionId || '');
1107
+ if (!appVersionId)
1108
+ throw new Error('DEPLOYMENT_APP_VERSION_NOT_AVAILABLE: 测试运行尚无可晋级版本');
1109
+ const diagnostics = await this.productionPromotionDiagnostics(workspace, client, source, appVersionId);
1110
+ return this.result('deployment.plan', workspace.context.workspace, {
1111
+ environment: 'production', sourceDeploymentId: deploymentId, appVersionId,
1112
+ packageDigest: source.packageDigest, sealed: true, submitted: false,
1113
+ rebuild: false, businessAcceptance: '需要核对该测试版本的业务验收记录',
1114
+ }, diagnostics);
1115
+ }
1116
+ async deployProduction(root, deploymentId) {
1117
+ const workspace = await this.workspace(root);
1118
+ const capsuleDiagnostics = this.toolchainDiagnostics(workspace);
1119
+ if (capsuleDiagnostics.length > 0) {
1120
+ return this.result("deploy", workspace.context.workspace, undefined, capsuleDiagnostics);
1121
+ }
1122
+ const environment = "production";
1123
+ const client = await this.client(workspace.root);
1124
+ const source = await client.deployment(workspace.config.app.code, deploymentId);
462
1125
  const appVersionId = String(source.result?.applicationVersionId || "");
463
1126
  if (!appVersionId)
464
1127
  throw new Error("DEPLOYMENT_APP_VERSION_NOT_AVAILABLE");
465
- const deployment = await client.promote({
1128
+ const diagnostics = await this.productionPromotionDiagnostics(workspace, client, source, appVersionId);
1129
+ if (diagnostics.length > 0) {
1130
+ return this.result("deploy", workspace.context.workspace, undefined, diagnostics);
1131
+ }
1132
+ const deployment = await operationStage('submit', '提交生产晋级', () => client.promote({
466
1133
  appCode: workspace.config.app.code,
467
1134
  appVersionId,
468
1135
  environmentKind: environment,
469
1136
  idempotencyKey: `promote:${source.packageDigest}:${environment}`,
470
- });
471
- return this.ok("promote", workspace.context.workspace, deployment);
1137
+ }));
1138
+ return this.ok("deploy", workspace.context.workspace, deployment, [
1139
+ { code: "status", label: "查看生产部署状态", command: `openxiangda status ${deployment.id}` },
1140
+ ]);
1141
+ }
1142
+ async productionPromotionDiagnostics(workspace, client, source, appVersionId) {
1143
+ const diagnostics = [];
1144
+ if (source.status !== "succeeded" ||
1145
+ source.environment.kind !== "preproduction") {
1146
+ diagnostics.push(this.diagnostic("PRODUCTION_PREPRODUCTION_RUN_REQUIRED", "生产晋级来源必须是成功的预发 DeploymentRun", "deploymentId", "等待预发部署成功,并使用该 DeploymentRun ID 晋级", { status: source.status, environment: source.environment.kind }));
1147
+ return diagnostics;
1148
+ }
1149
+ let version;
1150
+ try {
1151
+ const capabilities = await client.capabilities();
1152
+ const { assertApplicationContractCompatible, assertProductionPromotionPreflight } = await import('./deployment.js');
1153
+ assertApplicationContractCompatible(capabilities, CURRENT_APPLICATION_CONTRACT);
1154
+ version = await operationStage('promotion-preflight', '按原测试制品预检生产环境', () => client.productionPromotionPreflight(workspace.config.app.code, source.id));
1155
+ assertProductionPromotionPreflight(version, {
1156
+ appCode: workspace.config.app.code, sourceDeploymentId: source.id, appVersionId, packageDigest: source.packageDigest,
1157
+ });
1158
+ const testedLifecycle = lifecycleAtCommit(workspace.root, version.source.commit, this.appSpecContractIndex(workspace, true));
1159
+ if (!testedLifecycle.changeId || !testedLifecycle.acceptanceIds.length) {
1160
+ diagnostics.push(this.diagnostic('APPSPEC_TESTED_PLAN_REQUIRED', '指定测试版本缺少可追溯的需求与验收计划', 'appspec', '补齐真实记录并发布新的测试候选,再执行验收和生产晋级'));
1161
+ }
1162
+ // 原测试提交中的设计基线也必须有效,当前主线的补写不能补签旧版本。
1163
+ diagnostics.push(...testedLifecycle.design.diagnostics);
1164
+ const acceptance = await operationStage('business-acceptance', '核对原测试版本的业务验收报告', async () => verifyBusinessAcceptance(workspace.root, source, testedLifecycle));
1165
+ diagnostics.push(...acceptance.diagnostics);
1166
+ }
1167
+ catch (error) {
1168
+ return [this.configurationCompatibilityDiagnostic(error)];
1169
+ }
1170
+ if (workspace.context.workspace.dirty || version.source.dirty) {
1171
+ diagnostics.push(this.diagnostic("PRODUCTION_SOURCE_DIRTY", "生产晋级只接受来源干净且当前工作区无未提交变更的 AppVersion", "git", "提交并推送当前改动,或在干净主线目录中晋级该测试版本"));
1172
+ }
1173
+ const repository = git(workspace.root, ["config", "--get", "remote.origin.url"]);
1174
+ const currentRepository = this.repositoryIdentity(repository);
1175
+ const sourceRepository = this.repositoryIdentity(version.source.repository);
1176
+ if (!currentRepository ||
1177
+ !sourceRepository ||
1178
+ currentRepository !== sourceRepository) {
1179
+ diagnostics.push(this.diagnostic("PRODUCTION_SOURCE_REPOSITORY_MISMATCH", "当前工作区仓库与 AppVersion 来源仓库不一致", "git.remote.origin.url", "在构建该 AppVersion 的同一仓库中执行晋级", { current: repository || null, source: version.source.repository }));
1180
+ }
1181
+ if (diagnostics.length)
1182
+ return diagnostics;
1183
+ try {
1184
+ await operationStage('promotion-source', '核对测试制品的主线来源', () => publishedDeliverySource(workspace.root, version.source.commit));
1185
+ }
1186
+ catch (error) {
1187
+ if (!(error instanceof DeliverySourceError))
1188
+ throw error;
1189
+ diagnostics.push(this.diagnostic(error.code, error.message, 'git', String(error.data.remediation), error.data));
1190
+ }
1191
+ return diagnostics;
472
1192
  }
473
- async rollback(root, environment, appVersionId) {
1193
+ async rollback(root, environment, appVersionId, options = {}) {
474
1194
  const workspace = await this.workspace(root);
1195
+ const targetEnvironment = this.deploymentEnvironment(environment);
1196
+ const requestedOperationId = options.operationId?.trim();
1197
+ if (requestedOperationId && !UUID_PATTERN.test(requestedOperationId)) {
1198
+ throw new Error("OPENXIANGDA_ROLLBACK_OPERATION_ID_INVALID: --operation-id 必须是 UUID");
1199
+ }
1200
+ const operationId = requestedOperationId || randomUUID();
475
1201
  const deployment = await (await this.client(workspace.root)).rollback({
476
1202
  appCode: workspace.config.app.code,
477
1203
  appVersionId,
478
- environmentKind: environment,
479
- idempotencyKey: `rollback:${appVersionId}:${environment}`,
1204
+ environmentKind: targetEnvironment,
1205
+ idempotencyKey: `rollback:${appVersionId}:${targetEnvironment}:${operationId}`,
1206
+ requestId: operationId,
480
1207
  });
481
1208
  return this.ok("rollback", workspace.context.workspace, deployment);
482
1209
  }
483
- async doctor(root) {
484
- const workspace = await this.workspace(root);
485
- const commands = ["git", "node", "pnpm", "docker"].map((command) => {
486
- const result = spawnSync(command, ["--version"], { encoding: "utf8" });
487
- return {
488
- command,
489
- ok: result.status === 0,
490
- version: String(result.stdout || result.stderr || "").trim(),
491
- };
492
- });
493
- let platform;
494
- try {
495
- platform = (await (await this.client(workspace.root)).capabilities());
1210
+ deploymentEnvironment(value) {
1211
+ const normalized = String(value || "").trim();
1212
+ if (normalized === "preproduction" || normalized === "production") {
1213
+ return normalized;
496
1214
  }
497
- catch (error) {
498
- platform = { ok: false, message: error.message };
1215
+ throw new Error(`OPENXIANGDA_REMOTE_ENVIRONMENT_INVALID: ${normalized || "<empty>"}`);
1216
+ }
1217
+ async changeEnvironmentRuntimeState(root, environment, action, options) {
1218
+ const workspace = await this.workspace(root);
1219
+ const environmentKey = this.deploymentEnvironment(environment);
1220
+ const client = await this.client(workspace.root);
1221
+ const environments = await client.applicationEnvironments(workspace.config.app.code);
1222
+ const current = environments.items.find(item => item.environmentKey === environmentKey);
1223
+ if (!current) {
1224
+ throw new Error(`OPENXIANGDA_ENVIRONMENT_NOT_PROVISIONED: ${environmentKey}`);
499
1225
  }
500
- const diagnostics = commands
501
- .filter((item) => !item.ok && item.command !== "docker")
502
- .map((item) => this.diagnostic("TOOLCHAIN_COMMAND_MISSING", `缺少必需命令: ${item.command}`, item.command));
503
- return this.result("doctor", workspace.context.workspace, { commands, platform }, diagnostics);
1226
+ const input = {
1227
+ idempotencyKey: options.idempotencyKey ||
1228
+ `environment:${action}:${environmentKey}:revision:${current.revision}`,
1229
+ ...(options.requestId ? { requestId: options.requestId } : {}),
1230
+ };
1231
+ const deployment = action === "start"
1232
+ ? await client.startEnvironment(workspace.config.app.code, environmentKey, input)
1233
+ : await client.stopEnvironment(workspace.config.app.code, environmentKey, input);
1234
+ return this.ok(`environment.${action}`, workspace.context.workspace, deployment, [
1235
+ {
1236
+ code: "status",
1237
+ label: "查看环境状态",
1238
+ command: "openxiangda status",
1239
+ },
1240
+ ]);
504
1241
  }
505
- async dev(root) {
1242
+ async dev(root, input = {}) {
506
1243
  const workspace = await this.workspace(root);
507
- return await new Promise((resolve) => {
508
- const child = spawn("pnpm", ["run", "dev"], {
509
- cwd: workspace.root,
510
- stdio: "inherit",
1244
+ const capsuleDiagnostics = this.toolchainDiagnostics(workspace);
1245
+ if (capsuleDiagnostics.length > 0) {
1246
+ return this.result("dev", workspace.context.workspace, undefined, capsuleDiagnostics);
1247
+ }
1248
+ const injectionDiagnostics = validateNestInjectionContract(workspace.root);
1249
+ if (injectionDiagnostics.length) {
1250
+ return this.result("dev", workspace.context.workspace, undefined, injectionDiagnostics);
1251
+ }
1252
+ const generated = await this.generate({
1253
+ root: workspace.root,
1254
+ });
1255
+ if (!generated.ok) {
1256
+ return this.result("dev", workspace.context.workspace, undefined, generated.diagnostics, generated.nextActions);
1257
+ }
1258
+ const backendDiagnostics = this.initializeBackend(workspace);
1259
+ if (backendDiagnostics.length) {
1260
+ return this.result("dev", workspace.context.workspace, undefined, backendDiagnostics);
1261
+ }
1262
+ try {
1263
+ const linkPath = join(workspace.root, ".openxiangda", "link.json");
1264
+ if (!existsSync(linkPath)) {
1265
+ throw new Error("OPENXIANGDA_CONNECTED_LINK_REQUIRED: 先运行 openxiangda create <directory> --base-url <platform>");
1266
+ }
1267
+ let link;
1268
+ try {
1269
+ link = JSON.parse(readFileSync(linkPath, "utf8"));
1270
+ }
1271
+ catch {
1272
+ throw new Error("OPENXIANGDA_CONNECTED_LINK_INVALID");
1273
+ }
1274
+ if (link.schemaVersion !== 2 || link.appCode !== workspace.config.app.code || !link.baseUrl) {
1275
+ throw new Error("OPENXIANGDA_CONNECTED_LINK_INVALID");
1276
+ }
1277
+ const developerSession = await OpenXiangdaDeveloperSession.load();
1278
+ if (!developerSession) {
1279
+ throw new Error("OPENXIANGDA_CONNECTED_LOGIN_REQUIRED: 先运行 openxiangda login");
1280
+ }
1281
+ developerSession.assertPlatform(link.baseUrl);
1282
+ await developerSession.whoami();
1283
+ const client = new OpenXiangdaControlPlaneClient({
1284
+ baseUrl: developerSession.baseUrl,
1285
+ tokenProvider: developerSession,
511
1286
  });
512
- child.on("exit", (code) => resolve(this.result("dev", workspace.context.workspace, { exitCode: code ?? 1 }, code === 0
1287
+ const environments = await client.applicationEnvironments(workspace.config.app.code);
1288
+ const environment = selectConnectedDevelopmentEnvironment(environments.items);
1289
+ if (!environment) {
1290
+ throw new Error("OPENXIANGDA_CONNECTED_ACTIVE_ENVIRONMENT_REQUIRED: 应用需要已发布的 test(preproduction) 或 production 环境");
1291
+ }
1292
+ const sources = compileApplicationSources(workspace.config, this.toolchainVersion);
1293
+ const remoteSession = {
1294
+ create: async () => {
1295
+ const value = await client.createConnectedDevelopmentSession(workspace.config.app.code, {
1296
+ environmentKey: environment.environmentKey,
1297
+ manifestDigest: sources.config.digest,
1298
+ configuration: sources.config.value,
1299
+ });
1300
+ return {
1301
+ id: value.sessionId,
1302
+ token: value.sessionToken,
1303
+ expiresAt: value.expiresAt,
1304
+ mode: value.mode,
1305
+ manifestOverlay: value.manifestOverlay,
1306
+ manifestDigest: value.manifestDigest,
1307
+ };
1308
+ },
1309
+ current: async (token) => {
1310
+ const value = await client.connectedDevelopmentSession(workspace.config.app.code, token);
1311
+ return {
1312
+ id: value.sessionId,
1313
+ expiresAt: value.expiresAt,
1314
+ mode: value.mode,
1315
+ manifestOverlay: value.manifestOverlay,
1316
+ manifestDigest: value.manifestDigest,
1317
+ };
1318
+ },
1319
+ refresh: async (token) => {
1320
+ const value = await client.refreshConnectedDevelopmentSession(workspace.config.app.code, token);
1321
+ return {
1322
+ id: value.sessionId,
1323
+ expiresAt: value.expiresAt,
1324
+ mode: value.mode,
1325
+ manifestOverlay: value.manifestOverlay,
1326
+ manifestDigest: value.manifestDigest,
1327
+ };
1328
+ },
1329
+ revoke: async (token) => {
1330
+ await client.revokeConnectedDevelopmentSession(workspace.config.app.code, token);
1331
+ },
1332
+ };
1333
+ input.onStatus?.("connected dev 已启用增量配置:已有资源上的可空新字段无需先部署 test");
1334
+ const connected = await runConnectedDevelopment({
1335
+ root: workspace.root,
1336
+ appCode: workspace.config.app.code,
1337
+ platformBaseUrl: developerSession.baseUrl,
1338
+ environment,
1339
+ developerSession,
1340
+ remoteSession,
1341
+ ...(input.noOpen === undefined ? {} : { noOpen: input.noOpen }),
1342
+ ...(input.webPort === undefined ? {} : { webPort: input.webPort }),
1343
+ ...(input.onStatus ? { onStatus: input.onStatus } : {}),
1344
+ });
1345
+ const diagnostics = connected.exitCode === 0
513
1346
  ? []
514
1347
  : [
515
- this.diagnostic("DEV_PROCESS_FAILED", "开发进程异常退出", "dev"),
516
- ])));
517
- });
1348
+ this.diagnostic("DEV_PROCESS_FAILED", "连接式开发进程异常退出", workspace.root, "查看 Web 或 Nest 进程的终端输出后重试"),
1349
+ ];
1350
+ return this.result("dev", workspace.context.workspace, connected, diagnostics);
1351
+ }
1352
+ catch (error) {
1353
+ const failure = this.connectedDevelopmentFailure(error, workspace.root);
1354
+ return this.result("dev", workspace.context.workspace, undefined, [
1355
+ this.diagnostic(failure.code, failure.message, failure.path, failure.remediation),
1356
+ ], [{ code: failure.actionCode, label: failure.actionLabel, command: failure.command }]);
1357
+ }
518
1358
  }
519
1359
  async workspace(root) {
520
1360
  return await loadWorkspace(root, this.toolchainVersion);
521
1361
  }
1362
+ appSpecContractIndex(workspace, includeDigests) {
1363
+ const source = includeDigests
1364
+ ? compileApplicationSources(workspace.config, this.toolchainVersion)
1365
+ : undefined;
1366
+ return {
1367
+ appCode: workspace.config.app.code,
1368
+ resourceCodes: (workspace.config.data?.resources || []).map(resource => resource.code),
1369
+ actionCodes: (workspace.config.backend?.operations || []).map(operation => operation.code),
1370
+ ...(source
1371
+ ? {
1372
+ configDigest: source.config.digest,
1373
+ contractDigest: source.contracts.digest,
1374
+ aiCatalogDigest: source.aiCatalog.digest,
1375
+ }
1376
+ : {}),
1377
+ };
1378
+ }
1379
+ async validateTargetConfigurationCompatibility(workspace, environmentKey, local) {
1380
+ const client = await this.client(workspace.root);
1381
+ const capabilities = await client.capabilities();
1382
+ const { sources } = local;
1383
+ const required = {
1384
+ ...CURRENT_APPLICATION_CONTRACT,
1385
+ configurationBundleSchemaVersion: sources.config.value.schemaVersion,
1386
+ contractBundleSchemaVersion: sources.contracts.value.schemaVersion,
1387
+ compilerContractVersion: sources.config.value.compilerContractVersion,
1388
+ };
1389
+ const { assertApplicationContractCompatible, assertConfigurationValidationResult, assertRequiredCapabilitiesAvailable, } = await import("./deployment.js");
1390
+ assertApplicationContractCompatible(capabilities, required);
1391
+ assertRequiredCapabilitiesAvailable(capabilities, requiredPlatformCapabilities(workspace.config));
1392
+ const result = await client.validateConfigurationCompatibility(workspace.config.app.code, capabilities, {
1393
+ schemaVersion: SCHEMA_VERSIONS.configurationValidationRequest,
1394
+ environmentKey,
1395
+ clientContractVersion: OPENXIANGDA_CONTRACT_VERSION,
1396
+ required,
1397
+ configuration: {
1398
+ schemaVersion: sources.config.value.schemaVersion,
1399
+ digest: sources.config.digest,
1400
+ canonical: sources.config.content,
1401
+ },
1402
+ contract: {
1403
+ schemaVersion: sources.contracts.value.schemaVersion,
1404
+ digest: sources.contracts.digest,
1405
+ canonical: sources.contracts.content,
1406
+ },
1407
+ });
1408
+ assertConfigurationValidationResult(capabilities, required, result, {
1409
+ configurationDigest: sources.config.digest,
1410
+ contractDigest: sources.contracts.digest,
1411
+ }, requiredPlatformCapabilities(workspace.config));
1412
+ if (result.projectionDigest !== local.compiled.aggregateDigest) {
1413
+ throw new ControlPlaneError(409, 'OPENXIANGDA_CONFIGURATION_PROJECTION_MISMATCH', '相同校验规则与输入生成了不同投影,已停止构建,请检查平台配套版本', {
1414
+ pointer: '/projectionDigest', expectedProjectionDigest: local.compiled.aggregateDigest,
1415
+ actualProjectionDigest: result.projectionDigest, validatorDigest: NATIVE_CONFIGURATION_VALIDATOR_DIGEST,
1416
+ });
1417
+ }
1418
+ return result;
1419
+ }
1420
+ configurationCompatibilityDiagnostic(error) {
1421
+ if (error instanceof NativeConfigurationCompilerError) {
1422
+ return this.diagnostic(error.code, '应用声明未通过本地完整配置校验', error.pointer, '按错误指针修正应用声明,运行 openxiangda check 复核', {
1423
+ pointer: error.pointer, identifiers: error.identifiers, validatorDigest: NATIVE_CONFIGURATION_VALIDATOR_DIGEST,
1424
+ validationOwner: 'local-shared-compiler',
1425
+ });
1426
+ }
1427
+ const data = error instanceof ControlPlaneError &&
1428
+ error.data &&
1429
+ typeof error.data === "object" &&
1430
+ !Array.isArray(error.data)
1431
+ ? error.data
1432
+ : {};
1433
+ const required = { ...CURRENT_APPLICATION_CONTRACT };
1434
+ const pointer = String(data.pointer ||
1435
+ (error instanceof ControlPlaneError ? error.remote?.path : "") ||
1436
+ "/");
1437
+ const details = {
1438
+ pointer,
1439
+ clientContractVersion: OPENXIANGDA_CONTRACT_VERSION,
1440
+ clientSchemaVersions: {
1441
+ appPackage: required.appPackageSchemaVersion,
1442
+ configuration: required.configurationBundleSchemaVersion,
1443
+ contract: required.contractBundleSchemaVersion,
1444
+ compiler: required.compilerContractVersion,
1445
+ },
1446
+ platformVersion: "unknown",
1447
+ platformCapability: {
1448
+ code: CONFIGURATION_COMPATIBILITY_CAPABILITY,
1449
+ version: "unknown",
1450
+ status: "unavailable",
1451
+ },
1452
+ required,
1453
+ supported: [],
1454
+ ...data,
1455
+ };
1456
+ const diagnostic = this.diagnostic(String(error?.code ||
1457
+ "OPENXIANGDA_CONFIGURATION_COMPATIBILITY_FAILED"), error instanceof Error
1458
+ ? error.message
1459
+ : "目标平台配置兼容性预检失败", pointer, error instanceof ControlPlaneError
1460
+ ? error.remote?.remediation ||
1461
+ (typeof data.remediation === "string"
1462
+ ? data.remediation
1463
+ : "升级目标平台或修正应用声明后重试 openxiangda check")
1464
+ : "检查目标平台兼容性后重试 openxiangda check", details);
1465
+ return {
1466
+ ...diagnostic,
1467
+ retryable: error instanceof ControlPlaneError
1468
+ ? error.remote?.retryable === true || error.status >= 500
1469
+ : false,
1470
+ };
1471
+ }
522
1472
  async client(root) {
523
1473
  if (this.options.client)
524
1474
  return this.options.client;
525
1475
  if (this.options.clientOptions) {
526
1476
  return new OpenXiangdaControlPlaneClient(this.options.clientOptions);
527
1477
  }
528
- const session = await loadSession();
1478
+ const session = await OpenXiangdaDeveloperSession.load();
529
1479
  if (!session)
530
1480
  throw new Error("OPENXIANGDA_AUTH_REQUIRED");
531
- let baseUrl = session.baseUrl;
532
- if (root && session.savedAt !== "environment") {
1481
+ if (root) {
533
1482
  const linkPath = join(root, ".openxiangda", "link.json");
534
1483
  if (existsSync(linkPath)) {
535
1484
  const link = JSON.parse(readFileSync(linkPath, "utf8"));
536
1485
  if (link.baseUrl)
537
- baseUrl = String(link.baseUrl).replace(/\/+$/, "");
1486
+ session.assertPlatform(String(link.baseUrl));
538
1487
  }
539
1488
  }
540
1489
  return new OpenXiangdaControlPlaneClient({
541
- baseUrl,
542
- token: session.token,
1490
+ baseUrl: session.baseUrl,
1491
+ tokenProvider: session,
543
1492
  });
544
1493
  }
545
1494
  get toolchainVersion() {
546
1495
  return this.options.toolchainVersion || OPENXIANGDA_TOOLCHAIN_VERSION;
547
1496
  }
548
- runPackageScript(root, script) {
549
- const result = spawnSync("pnpm", ["run", script], {
550
- cwd: root,
551
- encoding: "utf8",
552
- env: process.env,
553
- });
1497
+ deliveryCacheContext(workspace) {
554
1498
  return {
555
- ok: result.status === 0,
556
- exitCode: result.status ?? 1,
557
- output: `${result.stdout || ""}${result.stderr || ""}`.trim(),
1499
+ root: workspace.root, frontendRoot: workspace.config.frontend.root,
1500
+ ...(backendRuntimeRequired(workspace.config) ? { backendRoot: workspace.config.backend.root } : {}),
1501
+ toolchain: { version: this.toolchainVersion, capsule: this.toolchainCapsule, validatorDigest: NATIVE_CONFIGURATION_VALIDATOR_DIGEST },
558
1502
  };
559
1503
  }
1504
+ get toolchainCapsule() {
1505
+ return this.options.toolchainCapsule || defaultDevkitCoreToolchainCapsule();
1506
+ }
1507
+ initializeBackend(workspace) {
1508
+ try {
1509
+ initializeOptionalBackend(workspace.root, workspace.config, {
1510
+ ...(this.options.toolchainCapsule?.packages.openxiangda
1511
+ ? { sdkVersion: this.options.toolchainCapsule.packages.openxiangda }
1512
+ : {}),
1513
+ });
1514
+ return [];
1515
+ }
1516
+ catch (error) {
1517
+ return [{
1518
+ ...this.diagnostic(String(error.code || "OPENXIANGDA_BACKEND_INITIALIZATION_FAILED"), error.message, workspace.config.backend.root, "修复初始化或依赖安装问题后重新运行 openxiangda check"),
1519
+ retryable: error.retryable === true,
1520
+ }];
1521
+ }
1522
+ }
1523
+ packageScriptLabel(script) {
1524
+ return { check: '类型与规范检查', test: '应用测试', build: '前后端构建' }[script] || script;
1525
+ }
1526
+ async runPackageScript(root, script) {
1527
+ return operationStage(script, this.packageScriptLabel(script), async () => {
1528
+ const result = await runCommandProcess('pnpm', ['run', script], { cwd: root });
1529
+ return { ok: result.ok, exitCode: result.status ?? 1, output: result.output.trim(), outputTruncated: result.outputTruncated };
1530
+ });
1531
+ }
1532
+ repositoryIdentity(value) {
1533
+ const normalized = String(value || "").trim();
1534
+ if (!normalized)
1535
+ return "";
1536
+ if (normalized.startsWith("/") || normalized.startsWith("file://")) {
1537
+ return normalized.replace(/^file:\/\//, "").replace(/\.git\/?$/, "");
1538
+ }
1539
+ const scp = normalized.match(/^(?:[^@]+@)?([^:]+):(.+)$/);
1540
+ if (scp && !normalized.includes("://")) {
1541
+ return `${scp[1]}/${scp[2]}`.toLowerCase().replace(/\.git\/?$/, "");
1542
+ }
1543
+ try {
1544
+ const url = new URL(normalized);
1545
+ return `${url.hostname}${url.pathname}`
1546
+ .toLowerCase()
1547
+ .replace(/\.git\/?$/, "")
1548
+ .replace(/\/$/, "");
1549
+ }
1550
+ catch {
1551
+ return normalized.toLowerCase().replace(/\.git\/?$/, "");
1552
+ }
1553
+ }
560
1554
  directoryBundle(root) {
561
1555
  if (!existsSync(root) || !statSync(root).isDirectory()) {
562
1556
  throw Object.assign(new Error(`前端构建目录不存在: ${root}`), {
@@ -582,47 +1576,128 @@ export class OpenXiangdaApplicationServices {
582
1576
  visit(root);
583
1577
  return canonicalJson({ schemaVersion: 2, files });
584
1578
  }
585
- versionTrainDiagnostics(workspace) {
586
- const versions = new Map();
587
- const packageFiles = [];
588
- const visit = (directory) => {
589
- for (const entry of readdirSync(directory, { withFileTypes: true })) {
590
- if (["node_modules", "dist", "coverage", ".git", ".openxiangda"].includes(entry.name)) {
591
- continue;
592
- }
593
- const path = join(directory, entry.name);
594
- if (entry.isDirectory())
595
- visit(path);
596
- else if (entry.isFile() && entry.name === "package.json")
597
- packageFiles.push(path);
598
- }
1579
+ writeCheckSealedArtifactStatus(workspace, passed) {
1580
+ const previousArtifact = this.previousSealedArtifact(workspace);
1581
+ const status = {
1582
+ schemaVersion: "openxiangda.sealed-artifact-status/v2",
1583
+ state: "check-did-not-seal",
1584
+ sealed: false,
1585
+ usableForDeploy: false,
1586
+ refreshedBy: "check",
1587
+ persisted: true,
1588
+ statusPath: ".openxiangda/build/seal-status.json",
1589
+ manifestPath: ".openxiangda/build/app-package.json",
1590
+ workspaceRevision: workspace.context.workspace.revision || null,
1591
+ workspaceDirty: workspace.context.workspace.dirty === true,
1592
+ packageDigest: null,
1593
+ ...(previousArtifact ? { previousArtifact } : {}),
1594
+ reasonCode: "OPENXIANGDA_CHECK_DOES_NOT_SEAL",
1595
+ nextCommand: passed ? "openxiangda deploy" : "openxiangda check",
599
1596
  };
600
- visit(workspace.root);
601
- for (const packageFile of packageFiles) {
602
- const packageJson = JSON.parse(readFileSync(packageFile, "utf8"));
603
- for (const section of [
604
- "dependencies",
605
- "devDependencies",
606
- "peerDependencies",
607
- ]) {
608
- const dependencies = packageJson[section];
609
- if (!dependencies || typeof dependencies !== "object")
610
- continue;
611
- for (const [name, version] of Object.entries(dependencies)) {
612
- if (!name.startsWith("openxiangda-"))
613
- continue;
614
- const normalized = String(version).replace(/^workspace:/, "");
615
- const locations = versions.get(normalized) || [];
616
- locations.push(relative(workspace.root, packageFile).replaceAll("\\", "/"));
617
- versions.set(normalized, locations);
618
- }
1597
+ this.writeSealedArtifactStatus(workspace.root, status);
1598
+ return status;
1599
+ }
1600
+ sealedArtifactStatus(workspace, packageDigest, persisted) {
1601
+ return {
1602
+ schemaVersion: "openxiangda.sealed-artifact-status/v2",
1603
+ state: "sealed",
1604
+ sealed: true,
1605
+ usableForDeploy: persisted,
1606
+ refreshedBy: "build",
1607
+ persisted,
1608
+ statusPath: ".openxiangda/build/seal-status.json",
1609
+ manifestPath: ".openxiangda/build/app-package.json",
1610
+ workspaceRevision: workspace.context.workspace.revision || null,
1611
+ workspaceDirty: workspace.context.workspace.dirty === true,
1612
+ packageDigest,
1613
+ nextCommand: "openxiangda deploy",
1614
+ };
1615
+ }
1616
+ previousSealedArtifact(workspace) {
1617
+ const manifestPath = join(workspace.root, ".openxiangda", "build", "app-package.json");
1618
+ if (!existsSync(manifestPath) || !statSync(manifestPath).isFile()) {
1619
+ return undefined;
1620
+ }
1621
+ if (statSync(manifestPath).size > 5 * 1024 * 1024) {
1622
+ return {
1623
+ packageDigest: null,
1624
+ sourceRevision: null,
1625
+ sourceDirty: null,
1626
+ matchesWorkspaceSource: false,
1627
+ };
1628
+ }
1629
+ try {
1630
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
1631
+ const sourceRevision = typeof manifest.source?.commit === "string"
1632
+ ? manifest.source.commit
1633
+ : null;
1634
+ const sourceDirty = typeof manifest.source?.dirty === "boolean"
1635
+ ? manifest.source.dirty
1636
+ : null;
1637
+ const workspaceRevision = workspace.context.workspace.revision || null;
1638
+ const workspaceDirty = workspace.context.workspace.dirty === true;
1639
+ return {
1640
+ packageDigest: sha256Digest(manifest),
1641
+ sourceRevision,
1642
+ sourceDirty,
1643
+ matchesWorkspaceSource: sourceRevision !== null &&
1644
+ sourceRevision === workspaceRevision &&
1645
+ sourceDirty === workspaceDirty,
1646
+ };
1647
+ }
1648
+ catch {
1649
+ return {
1650
+ packageDigest: null,
1651
+ sourceRevision: null,
1652
+ sourceDirty: null,
1653
+ matchesWorkspaceSource: false,
1654
+ };
1655
+ }
1656
+ }
1657
+ writeSealedArtifactStatus(root, status) {
1658
+ const statusPath = join(root, status.statusPath);
1659
+ mkdirSync(dirname(statusPath), { recursive: true });
1660
+ writeFileSync(statusPath, `${JSON.stringify(status, null, 2)}\n`, "utf8");
1661
+ }
1662
+ versionTrainDiagnostics(workspace) {
1663
+ let dependencies;
1664
+ try {
1665
+ dependencies = collectWorkspaceToolchainDependencies(workspace.root);
1666
+ }
1667
+ catch (error) {
1668
+ if (error instanceof ToolchainCapsuleScanLimitError) {
1669
+ return [
1670
+ this.diagnostic(error.code, "工作区 package.json 扫描超过安全资源上限", "package.json", "减少工作区中纳入检查的 package.json 数量或文件大小后重试", { message: error.message }),
1671
+ ];
619
1672
  }
1673
+ throw error;
620
1674
  }
621
- if (versions.size <= 1)
622
- return [];
623
- return [
624
- this.diagnostic("TOOLCHAIN_VERSION_TRAIN_MISMATCH", `openxiangda-* 依赖不属于同一固定版本列车: ${[...versions.keys()].join(", ")}`, "package.json", "将全部工作区包的 openxiangda-* 依赖升级到同一版本", { versions: Object.fromEntries(versions) }),
625
- ];
1675
+ const parsed = dependencies.map(dependency => {
1676
+ const version = dependency.specifier.replace(/^workspace:/, "");
1677
+ const match = version.match(/^(\d+)\.(\d+)\.(\d+)(?:-[0-9A-Za-z]+(?:\.[0-9A-Za-z]+)*)?(?:\+[0-9A-Za-z]+(?:\.[0-9A-Za-z]+)*)?$/);
1678
+ return {
1679
+ ...dependency,
1680
+ version,
1681
+ major: match ? Number(match[1]) : null,
1682
+ exact: Boolean(match),
1683
+ };
1684
+ });
1685
+ const unpinned = parsed.filter(dependency => !dependency.exact);
1686
+ const incompatible = parsed.filter(dependency => dependency.exact && dependency.major !== 2);
1687
+ const diagnostics = [];
1688
+ if (unpinned.length > 0) {
1689
+ diagnostics.push(this.diagnostic("TOOLCHAIN_DEPENDENCY_NOT_PINNED", "openxiangda-* 依赖必须精确锁定,不能使用范围、标签或未定版本的 workspace 协议", "package.json", "把每个 openxiangda-* 依赖改为已验证的精确 2.x 版本,并提交 lockfile", { dependencies: unpinned }));
1690
+ }
1691
+ if (incompatible.length > 0) {
1692
+ diagnostics.push(this.diagnostic("TOOLCHAIN_RELEASE_TRAIN_INCOMPATIBLE", "应用包含不属于 OpenXiangda 2.x 发布列车的依赖", "package.json", "使用兼容的 OpenXiangda 2.x 精确版本;包版本可以独立递增,不要求字符串相同", { dependencies: incompatible }));
1693
+ }
1694
+ const capsuleDiagnostic = toolchainCapsuleDiagnostic(dependencies, this.toolchainCapsule);
1695
+ if (capsuleDiagnostic)
1696
+ diagnostics.push(capsuleDiagnostic);
1697
+ return diagnostics;
1698
+ }
1699
+ toolchainDiagnostics(workspace) {
1700
+ return this.versionTrainDiagnostics(workspace);
626
1701
  }
627
1702
  diagnostic(code, message, path, remediation, details) {
628
1703
  return {
@@ -636,6 +1711,82 @@ export class OpenXiangdaApplicationServices {
636
1711
  ...(details ? { details } : {}),
637
1712
  };
638
1713
  }
1714
+ connectedDevelopmentFailure(error, workspaceRoot) {
1715
+ const rawCode = String(error?.code ||
1716
+ (error instanceof Error ? error.message.split(":", 1)[0] : "") ||
1717
+ "OPENXIANGDA_CONNECTED_DEV_FAILED").trim();
1718
+ const known = {
1719
+ OPENXIANGDA_CONNECTED_LINK_REQUIRED: {
1720
+ message: "当前工作区尚未绑定 OpenXiangda 平台",
1721
+ remediation: "运行 create 幂等完成工作区绑定和平台初始化",
1722
+ actionCode: "create",
1723
+ actionLabel: "初始化应用",
1724
+ command: "openxiangda create <directory> --base-url <platform>",
1725
+ path: ".openxiangda/link.json",
1726
+ },
1727
+ OPENXIANGDA_CONNECTED_LINK_INVALID: {
1728
+ message: "当前工作区的平台绑定无效或属于另一个应用",
1729
+ remediation: "运行 create 幂等校验并重新生成当前应用绑定",
1730
+ actionCode: "create",
1731
+ actionLabel: "重新初始化应用",
1732
+ command: "openxiangda create <directory> --base-url <platform>",
1733
+ path: ".openxiangda/link.json",
1734
+ },
1735
+ OPENXIANGDA_CONNECTED_LOGIN_REQUIRED: {
1736
+ message: "尚未登录 OpenXiangda 平台",
1737
+ remediation: "登录工作区绑定的平台后重试",
1738
+ actionCode: "login",
1739
+ actionLabel: "登录平台",
1740
+ command: "openxiangda login --base-url <platform>",
1741
+ },
1742
+ OPENXIANGDA_AUTH_REQUIRED: {
1743
+ message: "OpenXiangda 登录已失效",
1744
+ remediation: "重新登录工作区绑定的平台后重试",
1745
+ actionCode: "login",
1746
+ actionLabel: "重新登录",
1747
+ command: "openxiangda login --base-url <platform>",
1748
+ },
1749
+ OPENXIANGDA_PLATFORM_SESSION_MISMATCH: {
1750
+ message: "工作区绑定平台与当前登录平台不一致",
1751
+ remediation: "切换登录平台,或明确重新绑定当前工作区",
1752
+ actionCode: "login",
1753
+ actionLabel: "切换登录平台",
1754
+ command: "openxiangda login --base-url <linked-platform>",
1755
+ path: ".openxiangda/link.json",
1756
+ },
1757
+ OPENXIANGDA_CONNECTED_ACTIVE_ENVIRONMENT_REQUIRED: {
1758
+ message: "应用没有可用于 connected dev 的已发布环境",
1759
+ remediation: "先发布 test(preproduction);只有 production 时也可以直接连接正式数据",
1760
+ actionCode: "deploy.test",
1761
+ actionLabel: "部署 test",
1762
+ command: "openxiangda deploy",
1763
+ },
1764
+ };
1765
+ const dataRemediation = error
1766
+ ?.data?.remediation;
1767
+ const remoteRemediation = error instanceof ControlPlaneError
1768
+ ? error.remote?.remediation ||
1769
+ (typeof dataRemediation === "string" ? dataRemediation : undefined)
1770
+ : undefined;
1771
+ const selected = known[rawCode] || {
1772
+ message: "连接式开发预检失败",
1773
+ remediation: remoteRemediation || (error instanceof Error && error.message
1774
+ ? error.message.replace(/Bearer\s+\S+/gi, "Bearer [redacted]")
1775
+ : "检查平台登录、应用权限和 Dev Session 状态后重试"),
1776
+ actionCode: "dev.retry",
1777
+ actionLabel: "重试 connected dev",
1778
+ command: "openxiangda dev",
1779
+ };
1780
+ return {
1781
+ code: rawCode,
1782
+ message: selected.message,
1783
+ remediation: selected.remediation,
1784
+ actionCode: selected.actionCode,
1785
+ actionLabel: selected.actionLabel,
1786
+ command: selected.command,
1787
+ path: selected.path || workspaceRoot,
1788
+ };
1789
+ }
639
1790
  ok(operation, workspace, data, nextActions = []) {
640
1791
  return {
641
1792
  ok: true,
@@ -652,7 +1803,7 @@ export class OpenXiangdaApplicationServices {
652
1803
  operation,
653
1804
  workspace,
654
1805
  ...(data === undefined ? {} : { data }),
655
- diagnostics,
1806
+ diagnostics: [...diagnostics].sort((left, right) => ({ error: 0, warning: 1, info: 2 })[left.severity] - ({ error: 0, warning: 1, info: 2 })[right.severity]),
656
1807
  nextActions,
657
1808
  };
658
1809
  }