@cloudbase/manager-node 5.7.1-beta.0 → 5.8.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/lib/cloudApp/index.js +12 -3
  2. package/lib/deploy/DatabaseDeployer.js +238 -0
  3. package/lib/deploy/DeployOrchestrator.js +645 -0
  4. package/lib/deploy/FunctionDeployer.js +611 -0
  5. package/lib/deploy/GatewayDeployer.js +612 -0
  6. package/lib/deploy/StateStore.js +327 -0
  7. package/lib/deploy/StaticDeployer.js +236 -0
  8. package/lib/deploy/domain.js +41 -0
  9. package/lib/deploy/framework.js +230 -0
  10. package/lib/deploy/function/artifact.js +207 -0
  11. package/lib/deploy/function/builders/cloud.js +654 -0
  12. package/lib/deploy/function/cam-preflight.js +157 -0
  13. package/lib/deploy/function/cloud-build-service.js +191 -0
  14. package/lib/deploy/function/config-guard.js +595 -0
  15. package/lib/deploy/function/docker-preflight.js +87 -0
  16. package/lib/deploy/function/image-preflight.js +164 -0
  17. package/lib/deploy/function/local-builder.js +129 -0
  18. package/lib/deploy/function/planner.js +278 -0
  19. package/lib/deploy/function/preflight.js +329 -0
  20. package/lib/deploy/types.js +103 -0
  21. package/lib/env/index.js +0 -9
  22. package/lib/environment.js +11 -0
  23. package/lib/function/index.js +1 -1
  24. package/lib/hosting/index.js +4 -1
  25. package/lib/index.js +31 -0
  26. package/lib/projectValidator/index.js +452 -0
  27. package/lib/projectValidator/types.js +2 -0
  28. package/lib/storage/index.js +6 -7
  29. package/lib/utils/index.js +62 -5
  30. package/package.json +2 -1
  31. package/types/cloudApp/index.d.ts +4 -0
  32. package/types/cloudApp/types.d.ts +87 -6
  33. package/types/deploy/DatabaseDeployer.d.ts +70 -0
  34. package/types/deploy/DeployOrchestrator.d.ts +170 -0
  35. package/types/deploy/FunctionDeployer.d.ts +159 -0
  36. package/types/deploy/GatewayDeployer.d.ts +194 -0
  37. package/types/deploy/StateStore.d.ts +170 -0
  38. package/types/deploy/StaticDeployer.d.ts +97 -0
  39. package/types/deploy/domain.d.ts +17 -0
  40. package/types/deploy/framework.d.ts +63 -0
  41. package/types/deploy/function/artifact.d.ts +40 -0
  42. package/types/deploy/function/builders/cloud.d.ts +110 -0
  43. package/types/deploy/function/cam-preflight.d.ts +51 -0
  44. package/types/deploy/function/cloud-build-service.d.ts +10 -0
  45. package/types/deploy/function/config-guard.d.ts +41 -0
  46. package/types/deploy/function/docker-preflight.d.ts +17 -0
  47. package/types/deploy/function/image-preflight.d.ts +21 -0
  48. package/types/deploy/function/local-builder.d.ts +26 -0
  49. package/types/deploy/function/planner.d.ts +15 -0
  50. package/types/deploy/function/preflight.d.ts +9 -0
  51. package/types/deploy/types.d.ts +429 -0
  52. package/types/env/index.d.ts +1 -8
  53. package/types/env/type.d.ts +6 -97
  54. package/types/environment.d.ts +7 -0
  55. package/types/function/types.d.ts +15 -0
  56. package/types/hosting/index.d.ts +6 -0
  57. package/types/index.d.ts +18 -0
  58. package/types/interfaces/function.interface.d.ts +1 -1
  59. package/types/projectValidator/index.d.ts +38 -0
  60. package/types/projectValidator/types.d.ts +26 -0
  61. package/types/storage/index.d.ts +6 -0
  62. package/types/utils/index.d.ts +13 -0
@@ -0,0 +1,645 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.DeployOrchestrator = void 0;
7
+ const path_1 = __importDefault(require("path"));
8
+ const error_1 = require("../error");
9
+ const DatabaseDeployer_1 = require("./DatabaseDeployer");
10
+ const FunctionDeployer_1 = require("./FunctionDeployer");
11
+ const StaticDeployer_1 = require("./StaticDeployer");
12
+ const GatewayDeployer_1 = require("./GatewayDeployer");
13
+ const StateStore_1 = require("./StateStore");
14
+ const framework_1 = require("./framework");
15
+ const parallel_1 = require("../utils/parallel");
16
+ /** 部署顺序(按依赖:database 最先,函数依赖新 Schema;gateway 最后,依赖函数/hosting) */
17
+ const DEPLOY_ORDER = ['database', 'functions', 'app', 'hosting', 'gateway'];
18
+ /**
19
+ * 声明式部署编排器
20
+ * 顺序:database → functions → app → hosting → gateway
21
+ * 支持 dry-run / only / skip / 幂等重试(database 失败/冲突会中断后续步骤)
22
+ */
23
+ class DeployOrchestrator {
24
+ constructor(environment) {
25
+ this.environment = environment;
26
+ this.databaseDeployer = new DatabaseDeployer_1.DatabaseDeployer(environment);
27
+ this.functionDeployer = new FunctionDeployer_1.FunctionDeployer(environment);
28
+ this.staticDeployer = new StaticDeployer_1.StaticDeployer(environment);
29
+ this.gatewayDeployer = new GatewayDeployer_1.GatewayDeployer(environment);
30
+ }
31
+ /**
32
+ * 计算部署计划(dry-run 输出)
33
+ */
34
+ async deployPlan(options) {
35
+ return this.buildPlan(options.config, options);
36
+ }
37
+ /**
38
+ * 执行部署
39
+ *
40
+ * 编排模型:plan 按「连续相同资源类型」切成 segments,segment 之间严格按
41
+ * DEPLOY_ORDER 串行;segment 内部按 concurrency 并行执行。失败中断策略:
42
+ * - 默认 fail-fast:任一资源失败即中断后续(return)
43
+ * - continueOnError:非 database 资源失败仍继续,最后汇总失败数
44
+ * - database 失败始终强制中断(无论是否 continueOnError)
45
+ */
46
+ async deploy(options) {
47
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
48
+ const { config, envId, dryRun, cwd = process.cwd(), log, concurrency = 1, continueOnError = false } = options;
49
+ // 1. 轻量 inline 守卫(validateProject 未接入时的兜底)
50
+ this.guardConfig(config);
51
+ const plan = await this.buildPlan(config, options);
52
+ if (dryRun) {
53
+ (_a = log === null || log === void 0 ? void 0 : log.info) === null || _a === void 0 ? void 0 : _a.call(log, 'dry-run 模式,仅输出部署计划');
54
+ return { plan, results: [] };
55
+ }
56
+ const results = [];
57
+ // 按连续相同 type 切段:段间串行(依赖顺序),段内并行
58
+ const segments = this.segmentPlan(plan);
59
+ // gateway 段强制串行:executeStep 的 gateway 分支部署 config.gateway.routes 全部路由
60
+ // (逐路由 plan item 仅用于展示 diff),并发执行会对同一批路由发起重复全量部署(竞态 create/modify 路由、bindCustomDomain)
61
+ const globalLimit = Math.max(1, Math.min(Number(concurrency) || 1, 20));
62
+ try {
63
+ for (const segment of segments) {
64
+ // 段内并行度:gateway 段固定 1(全量部署语义);其余段用全局 concurrency
65
+ const limit = ((_b = segment[0]) === null || _b === void 0 ? void 0 : _b.type) === 'gateway' ? 1 : globalLimit;
66
+ // 段内预判定(顺序执行,保留交互/冲突中断语义)
67
+ const toExecute = [];
68
+ let segmentFailed = false;
69
+ let segmentDbFailed = false;
70
+ for (const item of segment) {
71
+ // skip:产物/配置未变更,静默跳过(不执行、不询问)
72
+ if (item.status === 'skip') {
73
+ (_c = log === null || log === void 0 ? void 0 : log.info) === null || _c === void 0 ? void 0 : _c.call(log, `[${item.type}] ${item.name}: ${item.action}`);
74
+ results.push({ type: item.type, name: item.name, ok: true, reason: 'skip' });
75
+ continue;
76
+ }
77
+ (_d = log === null || log === void 0 ? void 0 : log.info) === null || _d === void 0 ? void 0 : _d.call(log, `[${item.type}] ${item.name}: ${item.action}`);
78
+ // conflict:数据库迁移冲突 → 中断整个部署(后续资源可能依赖新 Schema)
79
+ if (item.status === 'conflict') {
80
+ const message = '检测到冲突,已中断部署';
81
+ (_e = log === null || log === void 0 ? void 0 : log.error) === null || _e === void 0 ? void 0 : _e.call(log, `[${item.type}] ${item.name} ${message}`);
82
+ results.push({ type: item.type, name: item.name, ok: false, error: message });
83
+ if (item.type === 'database') {
84
+ return { plan, results };
85
+ }
86
+ segmentFailed = true;
87
+ continue;
88
+ }
89
+ // 覆盖确认:仅函数已存在(update)时触发,对齐 tcb fn deploy 的 FunctionConflictChecker 语义
90
+ // yes=true 直接放行;注入 confirmUpdate 按回调决定;两者都无 → 保守跳过(no-confirm)
91
+ if (item.type === 'functions' && item.status === 'update') {
92
+ let confirmed = false;
93
+ if (options.yes) {
94
+ confirmed = true;
95
+ }
96
+ else if (options.confirmUpdate) {
97
+ confirmed = await options.confirmUpdate(item);
98
+ }
99
+ if (!confirmed) {
100
+ const reason = options.confirmUpdate
101
+ ? 'skipped-by-user'
102
+ : 'no-confirm';
103
+ (_f = log === null || log === void 0 ? void 0 : log.warn) === null || _f === void 0 ? void 0 : _f.call(log, `[${item.type}] ${item.name} 跳过覆盖更新(${reason})`);
104
+ results.push({ type: item.type, name: item.name, ok: false, reason });
105
+ continue;
106
+ }
107
+ }
108
+ toExecute.push(item);
109
+ }
110
+ if (toExecute.length === 0) {
111
+ // 段内无实际执行项:若本段已因 conflict 标记失败且 fail-fast,则中断
112
+ if (segmentFailed && !continueOnError) {
113
+ return { plan, results };
114
+ }
115
+ continue;
116
+ }
117
+ // 段内并行执行
118
+ const controller = new parallel_1.AsyncTaskParallelController(limit);
119
+ const tasks = toExecute.map((item) => async () => {
120
+ var _a, _b, _c, _d, _e;
121
+ try {
122
+ const stepResult = await this.executeStep({ item, config, envId, cwd });
123
+ results.push({ type: item.type, name: item.name, ok: true, url: stepResult });
124
+ }
125
+ catch (e) {
126
+ const message = (e === null || e === void 0 ? void 0 : e.message) || String(e);
127
+ const requestId = (e === null || e === void 0 ? void 0 : e.requestId) ||
128
+ (e === null || e === void 0 ? void 0 : e.RequestId) ||
129
+ ((_a = e === null || e === void 0 ? void 0 : e.original) === null || _a === void 0 ? void 0 : _a.requestId) ||
130
+ ((_b = e === null || e === void 0 ? void 0 : e.original) === null || _b === void 0 ? void 0 : _b.RequestId) ||
131
+ ((_c = e === null || e === void 0 ? void 0 : e.response) === null || _c === void 0 ? void 0 : _c.requestId) ||
132
+ ((_d = e === null || e === void 0 ? void 0 : e.response) === null || _d === void 0 ? void 0 : _d.RequestId);
133
+ (_e = log === null || log === void 0 ? void 0 : log.error) === null || _e === void 0 ? void 0 : _e.call(log, `[${item.type}] ${item.name} 部署失败: ${message}`);
134
+ results.push(Object.assign({ type: item.type, name: item.name, ok: false, error: message }, (requestId ? { requestId } : {})));
135
+ segmentFailed = true;
136
+ if (item.type === 'database') {
137
+ segmentDbFailed = true;
138
+ }
139
+ }
140
+ });
141
+ controller.loadTasks(tasks);
142
+ await controller.run();
143
+ // 段内失败中断决策
144
+ if (segmentFailed) {
145
+ if (segmentDbFailed) {
146
+ (_g = log === null || log === void 0 ? void 0 : log.error) === null || _g === void 0 ? void 0 : _g.call(log, '数据库迁移失败,中断部署');
147
+ return { plan, results };
148
+ }
149
+ if (!continueOnError) {
150
+ (_h = log === null || log === void 0 ? void 0 : log.error) === null || _h === void 0 ? void 0 : _h.call(log, '存在资源部署失败,中断部署');
151
+ return { plan, results };
152
+ }
153
+ }
154
+ }
155
+ }
156
+ finally {
157
+ // 无论部署全部成功还是 fail-fast 中断,都将已成功资源的状态快照落盘
158
+ // (失败资源保留旧快照,下次 diff 仍会检测到差异);快照写入失败不影响部署结果
159
+ if (!dryRun) {
160
+ try {
161
+ await this.persistState({ config, plan, results, cwd });
162
+ }
163
+ catch (e) {
164
+ (_j = log === null || log === void 0 ? void 0 : log.warn) === null || _j === void 0 ? void 0 : _j.call(log, `状态快照写入失败:${(e === null || e === void 0 ? void 0 : e.message) || String(e)}`);
165
+ }
166
+ }
167
+ }
168
+ return { plan, results };
169
+ }
170
+ /**
171
+ * 将扁平 plan 按「连续相同资源类型」切成有序 segments
172
+ *
173
+ * segment 之间保留 DEPLOY_ORDER 依赖顺序(严格串行);segment 内部为同类型
174
+ * 多实例,可并行执行。例如 [db, fnA, fnB, hostingX, hostingY, gateway]
175
+ * → [[db], [fnA, fnB], [hostingX, hostingY], [gateway]]
176
+ */
177
+ segmentPlan(plan) {
178
+ const segments = [];
179
+ let current = [];
180
+ let currentType;
181
+ for (const item of plan) {
182
+ if (currentType === undefined || item.type === currentType) {
183
+ current.push(item);
184
+ currentType = item.type;
185
+ }
186
+ else {
187
+ segments.push(current);
188
+ current = [item];
189
+ currentType = item.type;
190
+ }
191
+ }
192
+ if (current.length > 0) {
193
+ segments.push(current);
194
+ }
195
+ return segments;
196
+ }
197
+ /**
198
+ * 部署成功后写回本地 state 快照
199
+ *
200
+ * - hosting:成功部署/未变更(skip)的项重算产物指纹;失败项保留旧快照
201
+ * - app:成功部署写 buildAppConfig 快照;skip 复用旧快照
202
+ * - gateway:保留旧快照(本轮不做路由级实时快照)
203
+ */
204
+ async persistState(options) {
205
+ var _a, _b, _c, _d, _e, _f;
206
+ const { config, plan, results, cwd } = options;
207
+ const stateStore = new StateStore_1.StateStore({ cwd });
208
+ const prev = stateStore.read();
209
+ // hosting
210
+ const hostingEntries = [];
211
+ for (const h of config.hosting || []) {
212
+ const name = h.name || 'default';
213
+ const planItem = plan.find(p => p.type === 'hosting' && p.name === name);
214
+ const result = results.find(r => r.type === 'hosting' && r.name === name);
215
+ if (!result || !result.ok) {
216
+ // 未执行(fail-fast 中断,results 中无对应条目)或部署失败:
217
+ // 保留旧快照(下次 diff 仍会检测到差异)
218
+ const prevEntry = (_b = (_a = prev === null || prev === void 0 ? void 0 : prev.resources) === null || _a === void 0 ? void 0 : _a.hosting) === null || _b === void 0 ? void 0 : _b.find(s => s.name === name);
219
+ if (prevEntry) {
220
+ hostingEntries.push(prevEntry);
221
+ }
222
+ continue;
223
+ }
224
+ if ((planItem === null || planItem === void 0 ? void 0 : planItem.status) === 'skip') {
225
+ const prevEntry = (_d = (_c = prev === null || prev === void 0 ? void 0 : prev.resources) === null || _c === void 0 ? void 0 : _c.hosting) === null || _d === void 0 ? void 0 : _d.find(s => s.name === name);
226
+ if (prevEntry) {
227
+ hostingEntries.push(prevEntry);
228
+ continue;
229
+ }
230
+ }
231
+ // 成功部署:重算当前指纹(含 size,供下次 diff 做 size 预判)
232
+ // 注意:deployHosting 可能执行本地构建,产物已更新,必须独立重算而非复用 plan 指纹
233
+ const root = path_1.default.resolve(cwd, h.root || '.');
234
+ // outputDir 与 StaticDeployer.deployHosting 完全复用同一解析逻辑
235
+ const outputDir = (0, framework_1.resolveHostingOutputDir)(h, root);
236
+ try {
237
+ const files = await stateStore.computeHostingFingerprints(outputDir, h.ignore || []);
238
+ hostingEntries.push({ name, deployPath: h.deployPath, files });
239
+ }
240
+ catch (_g) {
241
+ // 目录缺失 / 文件数超限:不写入(下次 diff 走云端兜底或全量部署)
242
+ }
243
+ }
244
+ // app
245
+ let appEntry;
246
+ if (config.app) {
247
+ const appResult = results.find(r => r.type === 'app');
248
+ if (appResult === null || appResult === void 0 ? void 0 : appResult.ok) {
249
+ appEntry = stateStore.buildAppConfig(config.app);
250
+ }
251
+ else {
252
+ appEntry = (_e = prev === null || prev === void 0 ? void 0 : prev.resources) === null || _e === void 0 ? void 0 : _e.app;
253
+ }
254
+ }
255
+ const resources = {};
256
+ if (hostingEntries.length) {
257
+ resources.hosting = hostingEntries;
258
+ }
259
+ if (appEntry) {
260
+ resources.app = appEntry;
261
+ }
262
+ if ((_f = prev === null || prev === void 0 ? void 0 : prev.resources) === null || _f === void 0 ? void 0 : _f.gateway) {
263
+ resources.gateway = prev.resources.gateway;
264
+ }
265
+ if (Object.keys(resources).length) {
266
+ stateStore.write({ resources });
267
+ }
268
+ }
269
+ /**
270
+ * 构建完整部署计划(按依赖顺序 + only/skip 过滤)
271
+ *
272
+ * 通过查询云端状态判定每个资源的动作:
273
+ * - database:先按迁移文件做 preview,再聚合成单条资源级计划项(pending→create,存在 conflict→conflict,全部 applied→不出现在 plan)
274
+ * - functions:ListFunctions 判断是否已存在 → create / update(不做本地 hash / 字段 diff)
275
+ * - app:describeAppInfo 判断是否已存在 → create / update;本地 state 配置一致 → skip
276
+ * - hosting:本地 state 指纹 diff → 一致 skip(不重新上传)/ 有差异 deploy(带文件级明细)
277
+ * - gateway:planRoutes 对比云端路由 → create / update / skip(含变更字段明细)
278
+ * 云端查询失败时降级为保守判定(全 create),不影响 dry-run 展示。
279
+ */
280
+ async buildPlan(config, options) {
281
+ const { only, skip, refresh } = options;
282
+ const cwd = options.cwd || process.cwd();
283
+ const envId = options.envId;
284
+ const plan = [];
285
+ const isEnabled = (type) => {
286
+ if ((only === null || only === void 0 ? void 0 : only.length) && !only.includes(type))
287
+ return false;
288
+ if (skip === null || skip === void 0 ? void 0 : skip.includes(type))
289
+ return false;
290
+ return true;
291
+ };
292
+ // 本地 state 快照(diff 基准):--refresh 时忽略本地 skip 判定
293
+ const stateStore = new StateStore_1.StateStore({ cwd });
294
+ const state = refresh ? null : stateStore.read();
295
+ const planCtx = { config, envId, stateStore, state, refresh, cwd };
296
+ for (const type of DEPLOY_ORDER) {
297
+ if (!isEnabled(type))
298
+ continue;
299
+ const typePlans = await this.buildPlanForType(type, planCtx);
300
+ plan.push(...typePlans);
301
+ }
302
+ return plan;
303
+ }
304
+ /**
305
+ * 按资源类型构建计划(拆分自 buildPlan,控制圈复杂度)
306
+ */
307
+ async buildPlanForType(type, ctx) {
308
+ const { config, envId, stateStore, state, refresh, cwd } = ctx;
309
+ if (type === 'database') {
310
+ return this.buildDatabasePlan(config, cwd, envId);
311
+ }
312
+ if (type === 'functions') {
313
+ const existingFunctions = await this.listExistingFunctionNames();
314
+ return (config.functions || []).map((fn) => {
315
+ const exists = existingFunctions.has(fn.name);
316
+ const details = [];
317
+ const runtime = fn.runtime || 'Nodejs20.19';
318
+ const memorySize = fn.memorySize || 256;
319
+ details.push({ label: '运行时', value: `${runtime}, ${memorySize}MB` });
320
+ if (fn.type === 'HTTP') {
321
+ const strategy = fn.buildStrategy || 'zip';
322
+ details.push({ label: '构建', value: strategy });
323
+ if (fn.public) {
324
+ details.push({ label: '访问', value: 'public: true(自动配置匿名访问)' });
325
+ }
326
+ if (fn.gatewayPath) {
327
+ details.push({ label: '网关路径', value: fn.gatewayPath });
328
+ }
329
+ }
330
+ else {
331
+ const install = fn.installDependency === false ? '本地安装依赖打包' : '云端安装依赖';
332
+ details.push({ label: '依赖', value: install });
333
+ }
334
+ return {
335
+ type,
336
+ name: fn.name,
337
+ status: exists ? 'update' : 'create',
338
+ action: exists
339
+ ? `覆盖更新已存在函数 ${fn.name}`
340
+ : `新建函数 ${fn.name}`,
341
+ details
342
+ };
343
+ });
344
+ }
345
+ if (type === 'app') {
346
+ return this.buildAppPlan({ config, envId, stateStore, state, refresh });
347
+ }
348
+ if (type === 'hosting') {
349
+ return this.buildHostingPlans(config, ctx);
350
+ }
351
+ return this.buildGatewayPlans(config, envId);
352
+ }
353
+ /**
354
+ * database 计划:preview 聚合成单条资源级计划项
355
+ */
356
+ async buildDatabasePlan(config, cwd, envId) {
357
+ if (!config.database)
358
+ return [];
359
+ const dbConfig = config.database;
360
+ const migrationPlans = await this.databaseDeployer.plan(dbConfig, cwd, envId);
361
+ const pending = migrationPlans.filter(item => item.status === 'pending');
362
+ const conflicts = migrationPlans.filter(item => item.status === 'conflict');
363
+ // database 是资源级步骤:无论有多少 migration,plan 中最多只保留一条 database 记录
364
+ const details = migrationPlans.map(item => ({
365
+ label: `${item.version}_${item.name || 'unknown'}`,
366
+ value: item.status === 'pending'
367
+ ? '待应用'
368
+ : item.status === 'conflict'
369
+ ? `冲突:${item.reason || 'checksum mismatch'}`
370
+ : '已应用,跳过'
371
+ }));
372
+ if (conflicts.length > 0) {
373
+ return [
374
+ {
375
+ type: 'database',
376
+ name: 'postgresql',
377
+ status: 'conflict',
378
+ action: `数据库迁移存在 ${conflicts.length} 个冲突,执行时将中断部署`,
379
+ changes: conflicts.map(item => ({
380
+ field: `conflict.${item.version}_${item.name || 'unknown'}`,
381
+ to: item.reason || 'checksum mismatch'
382
+ })),
383
+ details
384
+ }
385
+ ];
386
+ }
387
+ if (pending.length > 0) {
388
+ return [
389
+ {
390
+ type: 'database',
391
+ name: 'postgresql',
392
+ status: 'create',
393
+ action: `将执行 ${pending.length} 条数据库迁移`,
394
+ changes: pending.map(item => ({
395
+ field: 'migration',
396
+ to: `${item.version}_${item.name}`
397
+ })),
398
+ details
399
+ }
400
+ ];
401
+ }
402
+ // 全部 applied 时不产生 database 计划项
403
+ return [];
404
+ }
405
+ /**
406
+ * app 计划:本地 state 配置一致 → skip;否则按云端存在性 → create / update
407
+ */
408
+ async buildAppPlan(options) {
409
+ var _a;
410
+ const { config, envId, stateStore, state, refresh } = options;
411
+ if (!config.app)
412
+ return [];
413
+ const name = config.app.serviceName || 'app';
414
+ // 本地 state 配置一致 → skip(真增量,不重复构建版本)
415
+ const appEntry = stateStore.buildAppConfig(config.app);
416
+ const stateApp = (_a = state === null || state === void 0 ? void 0 : state.resources) === null || _a === void 0 ? void 0 : _a.app;
417
+ const appUnchanged = !refresh &&
418
+ stateApp !== undefined &&
419
+ appEntry !== undefined &&
420
+ this.deepEqual(stateApp, appEntry);
421
+ if (appUnchanged) {
422
+ return [
423
+ {
424
+ type: 'app',
425
+ name,
426
+ status: 'skip',
427
+ action: `云应用 ${name} 配置未变更,跳过`
428
+ }
429
+ ];
430
+ }
431
+ const appExists = await this.appExists(name);
432
+ const changes = this.appConfigChanges(stateApp, appEntry);
433
+ return [
434
+ Object.assign({ type: 'app', name, status: appExists ? 'update' : 'create', action: appExists
435
+ ? `覆盖更新云应用 ${name}`
436
+ : `新建云应用 ${name}` }, (changes.length ? { changes } : {}))
437
+ ];
438
+ }
439
+ /**
440
+ * hosting 计划:本地 state 指纹 diff → 一致 skip / 有差异 deploy(带文件级明细)
441
+ */
442
+ async buildHostingPlans(config, ctx) {
443
+ var _a, _b;
444
+ const { stateStore, state, refresh, cwd } = ctx;
445
+ const plans = [];
446
+ for (const h of config.hosting || []) {
447
+ const name = h.name || 'default';
448
+ const root = path_1.default.resolve(cwd, h.root || '.');
449
+ const buildCommand = (0, framework_1.resolveBuildCommand)(h, root);
450
+ // outputDir 与 StaticDeployer.deployHosting 完全复用同一解析逻辑
451
+ const outputDir = (0, framework_1.resolveHostingOutputDir)(h, root);
452
+ const stateHosting = (_b = (_a = state === null || state === void 0 ? void 0 : state.resources) === null || _a === void 0 ? void 0 : _a.hosting) === null || _b === void 0 ? void 0 : _b.find(s => s.name === name);
453
+ // 懒加载 diff:size 预判 + 按需 hash(目录缺失 / 文件数超限 → 保守部署)
454
+ let fileDiff = null;
455
+ try {
456
+ fileDiff = await stateStore.diffHostingFiles(outputDir, stateHosting === null || stateHosting === void 0 ? void 0 : stateHosting.files, {
457
+ ignore: h.ignore || []
458
+ });
459
+ }
460
+ catch (_c) {
461
+ fileDiff = null;
462
+ }
463
+ const unchanged = !refresh && fileDiff !== null && fileDiff.unchanged;
464
+ if (unchanged) {
465
+ plans.push({
466
+ type: 'hosting',
467
+ name,
468
+ status: 'skip',
469
+ action: `静态托管 ${name} 产物未变更,跳过`
470
+ });
471
+ continue;
472
+ }
473
+ const hasBuild = !!h.buildCommand ||
474
+ (!!h.framework && h.framework !== 'static' && h.framework !== 'custom');
475
+ const details = [
476
+ { label: '框架', value: h.framework || 'static' },
477
+ {
478
+ label: '构建',
479
+ value: hasBuild
480
+ ? `本地构建(${buildCommand || 'npm run build'})→ 上传 ${outputDir}`
481
+ : '无构建,直接上传'
482
+ },
483
+ { label: '部署路径', value: h.deployPath || '/' }
484
+ ];
485
+ if (fileDiff === null || fileDiff === void 0 ? void 0 : fileDiff.overLimit) {
486
+ details.push({ label: '指纹', value: '文件数超上限,降级为全量部署' });
487
+ }
488
+ plans.push(Object.assign({ type: 'hosting', name, status: 'deploy', action: hasBuild
489
+ ? `本地构建后直传静态托管 ${name}`
490
+ : `直传覆盖静态托管 ${name}`, details }, (fileDiff && !fileDiff.overLimit
491
+ ? {
492
+ fileDiff: {
493
+ added: fileDiff.added,
494
+ modified: fileDiff.modified,
495
+ deleted: fileDiff.deleted,
496
+ totalChanged: fileDiff.totalChanged
497
+ }
498
+ }
499
+ : {})));
500
+ }
501
+ return plans;
502
+ }
503
+ /**
504
+ * gateway 计划:planRoutes 对比云端路由 → create / update / skip(含变更字段明细)
505
+ */
506
+ async buildGatewayPlans(config, envId) {
507
+ var _a;
508
+ const routes = ((_a = config.gateway) === null || _a === void 0 ? void 0 : _a.routes) || [];
509
+ if (routes.length === 0)
510
+ return [];
511
+ const routePlans = await this.gatewayDeployer.planRoutes({
512
+ routes,
513
+ envId,
514
+ hostings: config.hosting || []
515
+ });
516
+ const routeConfigMap = new Map(routes.map(r => [r.path, r]));
517
+ return routePlans.map(rp => {
518
+ const routeCfg = routeConfigMap.get(rp.path);
519
+ const details = [];
520
+ if (routeCfg === null || routeCfg === void 0 ? void 0 : routeCfg.target) {
521
+ details.push({ label: '目标', value: routeCfg.target });
522
+ }
523
+ if (routeCfg === null || routeCfg === void 0 ? void 0 : routeCfg.domain) {
524
+ details.push({ label: '域名', value: routeCfg.domain });
525
+ }
526
+ return Object.assign({ type: 'gateway', name: rp.path, status: rp.status, action: rp.status === 'create'
527
+ ? `新建路由 ${rp.path}`
528
+ : rp.status === 'update'
529
+ ? `更新路由 ${rp.path}`
530
+ : `路由 ${rp.path} 配置一致,跳过`, changes: rp.changes }, (details.length ? { details } : {}));
531
+ });
532
+ }
533
+ /**
534
+ * 查询环境中已存在的函数名集合(查询失败返回空集,降级为全新建判定)
535
+ */
536
+ async listExistingFunctionNames() {
537
+ try {
538
+ const list = await this.environment.getFunctionService().listFunctions(100);
539
+ // listFunctions 返回字段名为 FunctionName(SCF ListFunctions API),兜底兼容 Name/name
540
+ return new Set((list || []).map((f) => f.FunctionName || f.Name || f.name));
541
+ }
542
+ catch (_a) {
543
+ return new Set();
544
+ }
545
+ }
546
+ /**
547
+ * 判断云应用是否已存在(查询失败返回 false,降级为新建判定)
548
+ */
549
+ async appExists(name) {
550
+ try {
551
+ await this.environment.getCloudAppService().describeAppInfo({
552
+ deployType: 'static-hosting',
553
+ serviceName: name
554
+ });
555
+ return true;
556
+ }
557
+ catch (_a) {
558
+ return false;
559
+ }
560
+ }
561
+ /**
562
+ * 执行单个计划步骤
563
+ */
564
+ async executeStep(options) {
565
+ const { item, config, envId, cwd } = options;
566
+ switch (item.type) {
567
+ case 'database': {
568
+ const dbConfig = config.database;
569
+ const result = await this.databaseDeployer.deploy(dbConfig, cwd, envId);
570
+ return result.taskId ? `task:${result.taskId}` : undefined;
571
+ }
572
+ case 'functions': {
573
+ const fn = (config.functions || []).find((f) => f.name === item.name);
574
+ if (!fn)
575
+ return undefined;
576
+ // 函数代码目录解析(对齐 CLI FunctionPathResolver 的 dir 语义):
577
+ // - 显式配置 dir → path.resolve(cwd, dir)(独立于 functionRoot,不拼接)
578
+ // - 未配置 dir → path.join(functionRootPath, name) = {cwd}/{functionRoot}/{name}
579
+ const functionRoot = config.functionRoot || 'functions';
580
+ const functionRootPath = path_1.default.resolve(cwd, functionRoot);
581
+ const functionPath = fn.dir
582
+ ? path_1.default.resolve(cwd, fn.dir)
583
+ : path_1.default.join(functionRootPath, fn.name);
584
+ // 声明式部署幂等语义:函数已存在则覆盖更新(force=true),支持重跑续传
585
+ const result = await this.functionDeployer.deploy({
586
+ func: fn,
587
+ functionRootPath,
588
+ functionPath,
589
+ force: true
590
+ });
591
+ return result.url;
592
+ }
593
+ case 'app': {
594
+ const result = await this.staticDeployer.deployApp({ config: config.app, cwd });
595
+ return result.url;
596
+ }
597
+ case 'hosting': {
598
+ const h = (config.hosting || []).find((x) => (x.name || 'default') === item.name);
599
+ const result = await this.staticDeployer.deployHosting({ config: h, cwd });
600
+ return result.url;
601
+ }
602
+ case 'gateway': {
603
+ const result = await this.gatewayDeployer.deploy({
604
+ routes: config.gateway.routes,
605
+ envId,
606
+ // 传入 hosting 配置:hosting 路由自动生成 PathRewrite.Prefix = deployPath
607
+ hostings: config.hosting || []
608
+ });
609
+ return result.domain;
610
+ }
611
+ }
612
+ }
613
+ /** 深比较(JSON 快照语义):仅用于 app 配置快照等简单对象 */
614
+ deepEqual(a, b) {
615
+ return JSON.stringify(a) === JSON.stringify(b);
616
+ }
617
+ /** app 配置差异明细(from → to) */
618
+ appConfigChanges(prev, curr) {
619
+ const changes = [];
620
+ const keys = new Set([
621
+ ...Object.keys(prev || {}),
622
+ ...Object.keys(curr || {})
623
+ ]);
624
+ for (const key of keys) {
625
+ const from = prev === null || prev === void 0 ? void 0 : prev[key];
626
+ const to = curr === null || curr === void 0 ? void 0 : curr[key];
627
+ if (from !== to) {
628
+ changes.push({ field: String(key), from, to });
629
+ }
630
+ }
631
+ return changes;
632
+ }
633
+ /**
634
+ * 轻量 inline 守卫:配置合法 + 目录存在(validateProject 接线点)
635
+ */
636
+ guardConfig(config) {
637
+ if (!config || typeof config !== 'object') {
638
+ throw new error_1.CloudBaseError('无效的配置:config 必须为对象');
639
+ }
640
+ if (!config.envId) {
641
+ throw new error_1.CloudBaseError('无效的配置:缺少 envId');
642
+ }
643
+ }
644
+ }
645
+ exports.DeployOrchestrator = DeployOrchestrator;