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.
- package/README.md +3 -8
- package/dist/app-spec.d.ts +228 -0
- package/dist/app-spec.d.ts.map +1 -0
- package/dist/app-spec.js +1443 -0
- package/dist/app-spec.js.map +1 -0
- package/dist/application-services.d.ts +996 -49
- package/dist/application-services.d.ts.map +1 -1
- package/dist/application-services.js +1348 -197
- package/dist/application-services.js.map +1 -1
- package/dist/application-ui-contract.d.ts +5 -0
- package/dist/application-ui-contract.d.ts.map +1 -0
- package/dist/application-ui-contract.js +149 -0
- package/dist/application-ui-contract.js.map +1 -0
- package/dist/backend-image-build.d.ts +23 -0
- package/dist/backend-image-build.d.ts.map +1 -0
- package/dist/backend-image-build.js +204 -0
- package/dist/backend-image-build.js.map +1 -0
- package/dist/command-process.d.ts +14 -0
- package/dist/command-process.d.ts.map +1 -0
- package/dist/command-process.js +51 -0
- package/dist/command-process.js.map +1 -0
- package/dist/command-registry.d.ts +123 -366
- package/dist/command-registry.d.ts.map +1 -1
- package/dist/command-registry.js +26 -266
- package/dist/command-registry.js.map +1 -1
- package/dist/compiler/ai-catalog.d.ts +4 -0
- package/dist/compiler/ai-catalog.d.ts.map +1 -0
- package/dist/compiler/ai-catalog.js +354 -0
- package/dist/compiler/ai-catalog.js.map +1 -0
- package/dist/compiler/application-model.d.ts +60 -0
- package/dist/compiler/application-model.d.ts.map +1 -0
- package/dist/compiler/application-model.js +141 -0
- package/dist/compiler/application-model.js.map +1 -0
- package/dist/compiler/authorization-transition.d.ts +7 -0
- package/dist/compiler/authorization-transition.d.ts.map +1 -0
- package/dist/compiler/authorization-transition.js +27 -0
- package/dist/compiler/authorization-transition.js.map +1 -0
- package/dist/compiler/bundle.d.ts +34 -0
- package/dist/compiler/bundle.d.ts.map +1 -0
- package/dist/compiler/bundle.js +1910 -0
- package/dist/compiler/bundle.js.map +1 -0
- package/dist/compiler/config.d.ts +882 -0
- package/dist/compiler/config.d.ts.map +1 -0
- package/dist/compiler/config.js +4118 -0
- package/dist/compiler/config.js.map +1 -0
- package/dist/compiler/data-policy-expression.d.ts +12 -0
- package/dist/compiler/data-policy-expression.d.ts.map +1 -0
- package/dist/compiler/data-policy-expression.js +43 -0
- package/dist/compiler/data-policy-expression.js.map +1 -0
- package/dist/compiler/field-codec.d.ts +14 -0
- package/dist/compiler/field-codec.d.ts.map +1 -0
- package/dist/compiler/field-codec.js +131 -0
- package/dist/compiler/field-codec.js.map +1 -0
- package/dist/compiler/field-physical-plan.d.ts +10 -0
- package/dist/compiler/field-physical-plan.d.ts.map +1 -0
- package/dist/compiler/field-physical-plan.js +66 -0
- package/dist/compiler/field-physical-plan.js.map +1 -0
- package/dist/compiler/field-policy-path.d.ts +10 -0
- package/dist/compiler/field-policy-path.d.ts.map +1 -0
- package/dist/compiler/field-policy-path.js +283 -0
- package/dist/compiler/field-policy-path.js.map +1 -0
- package/dist/compiler/field-query-plan.d.ts +13 -0
- package/dist/compiler/field-query-plan.d.ts.map +1 -0
- package/dist/compiler/field-query-plan.js +122 -0
- package/dist/compiler/field-query-plan.js.map +1 -0
- package/dist/compiler/field-surface.d.ts +15 -0
- package/dist/compiler/field-surface.d.ts.map +1 -0
- package/dist/compiler/field-surface.js +51 -0
- package/dist/compiler/field-surface.js.map +1 -0
- package/dist/compiler/index.d.ts +6 -0
- package/dist/compiler/index.d.ts.map +1 -0
- package/dist/compiler/index.js +6 -0
- package/dist/compiler/index.js.map +1 -0
- package/dist/compiler/package-compiler.d.ts +22 -0
- package/dist/compiler/package-compiler.d.ts.map +1 -0
- package/dist/compiler/package-compiler.js +257 -0
- package/dist/compiler/package-compiler.js.map +1 -0
- package/dist/compiler/permission-review.d.ts +139 -0
- package/dist/compiler/permission-review.d.ts.map +1 -0
- package/dist/compiler/permission-review.js +131 -0
- package/dist/compiler/permission-review.js.map +1 -0
- package/dist/compiler/schema-composition.d.ts +21 -0
- package/dist/compiler/schema-composition.d.ts.map +1 -0
- package/dist/compiler/schema-composition.js +193 -0
- package/dist/compiler/schema-composition.js.map +1 -0
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/configuration-preflight.d.ts +7 -0
- package/dist/configuration-preflight.d.ts.map +1 -0
- package/dist/configuration-preflight.js +13 -0
- package/dist/configuration-preflight.js.map +1 -0
- package/dist/connected-development.d.ts +62 -0
- package/dist/connected-development.d.ts.map +1 -0
- package/dist/connected-development.js +393 -0
- package/dist/connected-development.js.map +1 -0
- package/dist/control-plane-client.d.ts +353 -123
- package/dist/control-plane-client.d.ts.map +1 -1
- package/dist/control-plane-client.js +420 -141
- package/dist/control-plane-client.js.map +1 -1
- package/dist/delivery-cache.d.ts +34 -0
- package/dist/delivery-cache.d.ts.map +1 -0
- package/dist/delivery-cache.js +186 -0
- package/dist/delivery-cache.js.map +1 -0
- package/dist/delivery-source.d.ts +17 -0
- package/dist/delivery-source.d.ts.map +1 -0
- package/dist/delivery-source.js +85 -0
- package/dist/delivery-source.js.map +1 -0
- package/dist/deployment-observer.d.ts +14 -0
- package/dist/deployment-observer.d.ts.map +1 -0
- package/dist/deployment-observer.js +68 -0
- package/dist/deployment-observer.js.map +1 -0
- package/dist/deployment.d.ts +55 -3
- package/dist/deployment.d.ts.map +1 -1
- package/dist/deployment.js +182 -21
- package/dist/deployment.js.map +1 -1
- package/dist/design-readiness.d.ts +24 -0
- package/dist/design-readiness.d.ts.map +1 -0
- package/dist/design-readiness.js +102 -0
- package/dist/design-readiness.js.map +1 -0
- package/dist/developer-errors.d.ts +11 -0
- package/dist/developer-errors.d.ts.map +1 -0
- package/dist/developer-errors.js +16 -0
- package/dist/developer-errors.js.map +1 -0
- package/dist/developer-operations.d.ts +188 -0
- package/dist/developer-operations.d.ts.map +1 -0
- package/dist/developer-operations.js +58 -0
- package/dist/developer-operations.js.map +1 -0
- package/dist/development-lifecycle.d.ts +75 -0
- package/dist/development-lifecycle.d.ts.map +1 -0
- package/dist/development-lifecycle.js +189 -0
- package/dist/development-lifecycle.js.map +1 -0
- package/dist/documentation.d.ts +111 -0
- package/dist/documentation.d.ts.map +1 -0
- package/dist/documentation.js +112 -0
- package/dist/documentation.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/workflow.d.ts +83 -0
- package/dist/internal/workflow.d.ts.map +1 -0
- package/dist/internal/workflow.js +419 -0
- package/dist/internal/workflow.js.map +1 -0
- package/dist/nest-injection-contract.d.ts +4 -0
- package/dist/nest-injection-contract.d.ts.map +1 -0
- package/dist/nest-injection-contract.js +116 -0
- package/dist/nest-injection-contract.js.map +1 -0
- package/dist/operation-progress.d.ts +26 -0
- package/dist/operation-progress.d.ts.map +1 -0
- package/dist/operation-progress.js +92 -0
- package/dist/operation-progress.js.map +1 -0
- package/dist/optional-backend.d.ts +14 -0
- package/dist/optional-backend.d.ts.map +1 -0
- package/dist/optional-backend.js +145 -0
- package/dist/optional-backend.js.map +1 -0
- package/dist/package-compiler.d.ts +1 -1
- package/dist/package-compiler.d.ts.map +1 -1
- package/dist/package-compiler.js +1 -1
- package/dist/package-compiler.js.map +1 -1
- package/dist/session.d.ts +149 -4
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +362 -23
- package/dist/session.js.map +1 -1
- package/dist/testing.d.ts +77 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +213 -0
- package/dist/testing.js.map +1 -0
- package/dist/toolchain-capsule.d.ts +45 -0
- package/dist/toolchain-capsule.d.ts.map +1 -0
- package/dist/toolchain-capsule.js +165 -0
- package/dist/toolchain-capsule.js.map +1 -0
- package/dist/workspace-loader.d.ts +1 -1
- package/dist/workspace-loader.d.ts.map +1 -1
- package/dist/workspace-loader.js +35 -3
- package/dist/workspace-loader.js.map +1 -1
- package/dist/workspace-operation.d.ts +3 -0
- package/dist/workspace-operation.d.ts.map +1 -0
- package/dist/workspace-operation.js +45 -0
- package/dist/workspace-operation.js.map +1 -0
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +37 -1
- package/dist/workspace.js.map +1 -1
- package/package.json +16 -4
- package/templates/backend/Dockerfile +20 -0
- package/templates/backend/package.json +27 -0
- package/templates/backend/src/app.module.ts +23 -0
- package/templates/backend/src/main.ts +5 -0
- package/templates/backend/tsconfig.json +9 -0
|
@@ -1,12 +1,28 @@
|
|
|
1
|
-
import {
|
|
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 "
|
|
6
|
-
import { OpenXiangdaControlPlaneClient, } from "./control-plane-client.js";
|
|
7
|
-
import {
|
|
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
|
-
|
|
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.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
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.
|
|
313
|
+
const deployment = await client.redeploy({
|
|
81
314
|
appCode: workspace.config.app.code,
|
|
82
315
|
appVersionId: head.activeAppVersionId,
|
|
83
|
-
...(head.environmentId
|
|
84
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
206
|
-
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
|
|
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
|
|
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
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
check
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
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
|
|
259
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
292
|
-
|
|
293
|
-
|
|
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", "
|
|
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 =
|
|
304
|
-
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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.
|
|
828
|
+
minimumPlatformVersion: "2.0.0-alpha.5",
|
|
347
829
|
metadata: {
|
|
348
|
-
|
|
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
|
|
869
|
+
command: "openxiangda deploy",
|
|
375
870
|
},
|
|
376
871
|
]);
|
|
377
872
|
}
|
|
378
873
|
async deploymentPlan(input) {
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
-
|
|
397
|
-
|
|
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("
|
|
905
|
+
this.diagnostic("DEPLOY_PREPRODUCTION_ONLY", "Native 2.0 只允许直接部署到预发环境", String(input.environment), "使用 openxiangda deploy;生产环境必须显式复用成功的测试 DeploymentRun"),
|
|
400
906
|
]);
|
|
401
907
|
}
|
|
402
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
|
|
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
|
|
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("
|
|
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:
|
|
479
|
-
idempotencyKey: `rollback:${appVersionId}:${
|
|
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
|
-
|
|
484
|
-
const
|
|
485
|
-
|
|
486
|
-
|
|
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
|
-
|
|
498
|
-
|
|
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
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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
|
-
|
|
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", "
|
|
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
|
|
1478
|
+
const session = await OpenXiangdaDeveloperSession.load();
|
|
529
1479
|
if (!session)
|
|
530
1480
|
throw new Error("OPENXIANGDA_AUTH_REQUIRED");
|
|
531
|
-
|
|
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
|
-
|
|
1486
|
+
session.assertPlatform(String(link.baseUrl));
|
|
538
1487
|
}
|
|
539
1488
|
}
|
|
540
1489
|
return new OpenXiangdaControlPlaneClient({
|
|
541
|
-
baseUrl,
|
|
542
|
-
|
|
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
|
-
|
|
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
|
-
|
|
556
|
-
|
|
557
|
-
|
|
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
|
-
|
|
586
|
-
const
|
|
587
|
-
const
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
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
|
}
|