@heybox/hb-sdk 0.6.8 → 0.6.9

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 (26) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/cli-chunks/{build-D4crgajb.cjs → build-BtGxhrcV.cjs} +4 -4
  3. package/dist/cli-chunks/{context-fRWWSXDj.cjs → context-DX1KG-8A.cjs} +3 -3
  4. package/dist/cli-chunks/{create-BU_EFgDz.cjs → create-DAD0OF1r.cjs} +1 -1
  5. package/dist/cli-chunks/{dev-BrkChQ7v.cjs → dev-Dh4HY0I8.cjs} +6 -6
  6. package/dist/cli-chunks/{doctor-DZXVO4rL.cjs → doctor-BfKdiD3H.cjs} +1 -1
  7. package/dist/cli-chunks/{index-DxH41nsb.cjs → index-DB6Q8HaC.cjs} +2 -2
  8. package/dist/cli-chunks/{index-CSjJrnkO.cjs → index-tCURDEx8.cjs} +14 -14
  9. package/dist/cli-chunks/{login-DLVozxrs.cjs → login-D60EJLgs.cjs} +2 -2
  10. package/dist/cli-chunks/{project-vite-CwmMfCk3.cjs → project-vite-DAUaP0_3.cjs} +1 -1
  11. package/dist/cli-chunks/{remote-CfpkkC9I.cjs → remote-nQDniAUY.cjs} +456 -202
  12. package/dist/cli-chunks/{runtime-gate-CgN_v4Te.cjs → runtime-gate-BWlU-R4h.cjs} +1 -1
  13. package/dist/cli-chunks/{runtime-permission-env-B0jK9Rt1.cjs → runtime-permission-env-DKrhgVM3.cjs} +205 -49
  14. package/dist/cli-chunks/{session-DB7ARwm4.cjs → session-D6jOKXeu.cjs} +1 -1
  15. package/dist/cli.cjs +1 -1
  16. package/dist/index.cjs.js +1 -1
  17. package/dist/index.esm.js +1 -1
  18. package/dist/miniapp-publish.cjs.js +205 -0
  19. package/dist/miniapp-publish.esm.js +201 -1
  20. package/dist/vite.cjs.js +1 -1
  21. package/dist/vite.esm.js +1 -1
  22. package/package.json +2 -2
  23. package/skill/references/api-root.md +1 -1
  24. package/skill/references/cli.md +8 -0
  25. package/skill/skill.json +4 -4
  26. package/types/miniapp-publish/index.d.ts +77 -0
@@ -1,15 +1,16 @@
1
1
  'use strict';
2
2
 
3
3
  var promises = require('node:readline/promises');
4
- var index = require('./index-CSjJrnkO.cjs');
5
- var session = require('./session-DB7ARwm4.cjs');
4
+ var index = require('./index-tCURDEx8.cjs');
5
+ var session = require('./session-D6jOKXeu.cjs');
6
6
  var childProcess = require('node:child_process');
7
7
  var fs = require('node:fs');
8
8
  var fs$1 = require('node:fs/promises');
9
9
  var path = require('node:path');
10
- var runtimePermissionEnv = require('./runtime-permission-env-B0jK9Rt1.cjs');
11
- var runtimeGate = require('./runtime-gate-CgN_v4Te.cjs');
12
- var context = require('./context-fRWWSXDj.cjs');
10
+ var runtimePermissionEnv = require('./runtime-permission-env-DKrhgVM3.cjs');
11
+ var runtimeGate = require('./runtime-gate-BWlU-R4h.cjs');
12
+ var node_crypto = require('node:crypto');
13
+ var context = require('./context-DX1KG-8A.cjs');
13
14
  require('node:module');
14
15
  require('path');
15
16
  require('os');
@@ -20,7 +21,6 @@ require('events');
20
21
  require('stream');
21
22
  require('buffer');
22
23
  require('util');
23
- require('node:crypto');
24
24
  require('fs');
25
25
  require('constants');
26
26
  require('node:async_hooks');
@@ -60,6 +60,63 @@ async function postHeyboxApi(pathWithQuery, body, session$1, runtime) {
60
60
  return result;
61
61
  }
62
62
 
63
+ const CREATE_USER_MINIPROGRAM_UPLOAD_SESSION_API_PATH = '/mall/developer/user_miniprogram/version/upload_session/create';
64
+ const RECORD_USER_MINIPROGRAM_UPLOAD_RESULT_API_PATH = '/mall/developer/user_miniprogram/version/upload_session/record';
65
+ async function precheckUserMiniprogramVersion(options, runtime = {}) {
66
+ return postUserMiniprogramVersionForm(runtimePermissionEnv.PRECHECK_USER_MINIPROGRAM_VERSION_API_PATH, options, {
67
+ // 展示资料由服务端沿用 last approved 或注入默认名/默认图;CLI 不传 name/icon/cover。
68
+ ...(options.version ? { version: options.version } : {}),
69
+ ...(options.source_version ? { source_version: options.source_version } : {}),
70
+ release_note: options.releaseNote,
71
+ auto_publish: String(options.autoPublish),
72
+ }, runtime);
73
+ }
74
+ async function submitUserMiniprogramAudit(options, runtime = {}) {
75
+ return postUserMiniprogramVersionForm(runtimePermissionEnv.SUBMIT_USER_MINIPROGRAM_AUDIT_API_PATH, options, {
76
+ // 展示资料由服务端沿用 last approved 或注入默认名/默认图;CLI 不传 name/icon/cover。
77
+ ...(options.manifest ? { manifest: JSON.stringify(options.manifest) } : {}),
78
+ ...(options.source_version ? { source_version: options.source_version } : {}),
79
+ ...(options.sessionId ? { session_id: options.sessionId } : {}),
80
+ release_note: options.releaseNote,
81
+ auto_publish: String(options.autoPublish),
82
+ }, runtime);
83
+ }
84
+ async function createUserMiniprogramUploadSession(options, runtime = {}) {
85
+ return postUserMiniprogramVersionForm(CREATE_USER_MINIPROGRAM_UPLOAD_SESSION_API_PATH, options, {
86
+ version: options.version,
87
+ artifact_signature: options.artifactSignature,
88
+ relative_paths: options.relativePaths.join(','),
89
+ file_sizes: options.fileSizes.map(size => String(size)).join(','),
90
+ }, runtime);
91
+ }
92
+ async function recordUserMiniprogramUploadResult(options, runtime = {}) {
93
+ await postUserMiniprogramVersionForm(RECORD_USER_MINIPROGRAM_UPLOAD_RESULT_API_PATH, options, {
94
+ version: options.version,
95
+ session_id: options.sessionId,
96
+ keys: options.keys.join(','),
97
+ }, runtime);
98
+ }
99
+ async function postUserMiniprogramVersionForm(path, options, fields, runtime) {
100
+ const fetchImpl = runtime.fetchImpl ?? fetch;
101
+ const context = session.createHeyboxOpenPlatformRequestContext(options.session, path, {
102
+ contentType: 'application/x-www-form-urlencoded',
103
+ });
104
+ const body = new URLSearchParams();
105
+ body.set('mini_program_id', options.miniProgramId);
106
+ for (const [key, value] of Object.entries(fields)) {
107
+ body.set(key, value);
108
+ }
109
+ const response = await fetchImpl(session.createHeyboxApiUrl(runtime.baseUrl ?? context.baseUrl, path, context.platformParams), {
110
+ method: 'POST',
111
+ headers: context.headers,
112
+ body: body.toString(),
113
+ });
114
+ const result = await session.readHeyboxApiEnvelope(response, {
115
+ pathWithQuery: path,
116
+ });
117
+ return result ?? {};
118
+ }
119
+
63
120
  const DEFAULT_CONCURRENCY = 4;
64
121
  async function runUpload(options, runtime = {}) {
65
122
  const logger = runtime.logger ?? index.createCliLogger();
@@ -68,8 +125,14 @@ async function runUpload(options, runtime = {}) {
68
125
  const getCDNUploadInfo$1 = runtime.getCDNUploadInfo ?? getCDNUploadInfo;
69
126
  const getCDNUploadToken$1 = runtime.getCDNUploadToken ?? getCDNUploadToken;
70
127
  const postCDNUploadCallback$1 = runtime.postCDNUploadCallback ?? postCDNUploadCallback;
128
+ const recordUserMiniprogramUploadResult$1 = runtime.recordUserMiniprogramUploadResult ?? recordUserMiniprogramUploadResult;
71
129
  const createReadStream = runtime.createReadStream ?? fs.createReadStream;
72
- const orderedFiles = [...options.files].sort((a, b) => a.relativePath.localeCompare(b.relativePath));
130
+ let createCosClient = runtime.createCosClient;
131
+ if (!createCosClient) {
132
+ const COS = await loadCosConstructor();
133
+ createCosClient = ({ uploadToken }) => createDefaultCosClient(uploadToken, COS);
134
+ }
135
+ const orderedFiles = runtimePermissionEnv.prepareMiniappArtifactFiles(options.files);
73
136
  const batches = chunkUploadFiles(orderedFiles, runtimePermissionEnv.MINIAPP_UPLOAD_BATCH_SIZE);
74
137
  await logger.task(`正在上传 ${orderedFiles.length} 个文件`, async (taskContext) => {
75
138
  logger.debug(`上传并发数: ${concurrency}`);
@@ -79,25 +142,37 @@ async function runUpload(options, runtime = {}) {
79
142
  const batch = batches[batchIndex];
80
143
  logger.debug(`正在处理上传批次 ${batchIndex + 1}/${batches.length},文件数: ${batch.length}`);
81
144
  logger.debug('正在获取 CDN 上传信息');
82
- const fileInfos = createUploadFileInfos(batch, options);
83
- const uploadInfo = await getCDNUploadInfo$1({ session: options.session, scope: runtimePermissionEnv.MINIAPP_UPLOAD_SCOPE, fileInfos, needCache: false }, { baseUrl: runtime.baseUrl, fetchImpl });
145
+ const fileInfos = createUploadFileInfos(batch);
146
+ const uploadInfo = await getCDNUploadInfo$1({
147
+ session: options.session,
148
+ scope: runtimePermissionEnv.MINIAPP_UPLOAD_SCOPE,
149
+ fileInfos,
150
+ needCache: false,
151
+ }, { baseUrl: runtime.baseUrl, fetchImpl });
84
152
  logger.debug(`CDN bucket: ${uploadInfo.bucket}`);
85
153
  logger.debug(`CDN region: ${uploadInfo.region}`);
86
- validateUploadInfoKeys(batch, uploadInfo.keys, options);
154
+ validateUploadInfoKeys(batch, uploadInfo.keys);
87
155
  const indexedFiles = batch.map((file, index) => ({ file, key: uploadInfo.keys[index] }));
88
- const batchKeys = indexedFiles.map(({ key }) => key);
89
- logger.debug('正在获取 CDN 上传凭证');
90
- const uploadToken = await getCDNUploadToken$1({
91
- session: options.session,
92
- bucket: uploadInfo.bucket,
93
- keys: batchKeys,
94
- mimetypes: indexedFiles.map(({ file }) => file.mimeType),
95
- isMultipartUpload: 0,
96
- }, { baseUrl: runtime.baseUrl, fetchImpl });
97
- const cos = runtime.createCosClient ? runtime.createCosClient({ session: options.session, uploadToken }) : await createDefaultCosClient(uploadToken);
156
+ logger.debug('正在获取本批 CDN 上传凭证');
157
+ let uploadToken;
158
+ try {
159
+ uploadToken = await getCDNUploadToken$1({
160
+ session: options.session,
161
+ bucket: uploadInfo.bucket,
162
+ keys: indexedFiles.map(item => item.key),
163
+ mimetypes: indexedFiles.map(item => item.file.mimeType),
164
+ isMultipartUpload: 0,
165
+ }, { baseUrl: runtime.baseUrl, fetchImpl });
166
+ }
167
+ catch (error) {
168
+ throw new index.CliError(`获取 CDN 上传凭证失败:${readErrorMessage(error)}`);
169
+ }
170
+ const cos = createCosClient({ session: options.session, uploadToken });
171
+ const successfulKeys = [];
98
172
  const queue = indexedFiles.slice();
173
+ let firstUploadError;
99
174
  const workers = Array.from({ length: Math.min(concurrency, indexedFiles.length) }, async () => {
100
- while (queue.length > 0) {
175
+ while (queue.length > 0 && !firstUploadError) {
101
176
  const uploadTask = queue.shift();
102
177
  if (!uploadTask) {
103
178
  return;
@@ -111,6 +186,7 @@ async function runUpload(options, runtime = {}) {
111
186
  ContentLength: uploadTask.file.size,
112
187
  ContentType: uploadTask.file.mimeType,
113
188
  });
189
+ successfulKeys.push(uploadTask.key);
114
190
  completed += 1;
115
191
  taskContext.update(`正在上传 ${completed}/${orderedFiles.length} 个文件`);
116
192
  logger.debug(`[${String(completed).padStart(2)}/${orderedFiles.length}] ok ${uploadTask.file.relativePath} (${formatSize(uploadTask.file.size)})`);
@@ -119,13 +195,25 @@ async function runUpload(options, runtime = {}) {
119
195
  completed += 1;
120
196
  const message = readErrorMessage(error);
121
197
  logger.debug(`[${String(completed).padStart(2)}/${orderedFiles.length}] error ${uploadTask.file.relativePath} -> ${message}`);
122
- throw new index.CliError(`上传文件失败:${uploadTask.file.relativePath} -> ${message}`, readVerboseUploadError(uploadTask.file.relativePath, error));
198
+ firstUploadError ??= new index.CliError(`上传文件失败:${uploadTask.file.relativePath} -> ${message}`, readVerboseUploadError(uploadTask.file.relativePath, error));
123
199
  }
124
200
  }
125
201
  });
126
202
  await Promise.all(workers);
127
- logger.debug('正在确认 CDN 上传结果');
128
- await postCDNUploadCallback$1({ session: options.session, keys: batchKeys, isFinished: true }, { baseUrl: runtime.baseUrl, fetchImpl });
203
+ if (successfulKeys.length > 0) {
204
+ logger.debug('正在确认 CDN 上传结果');
205
+ await postCDNUploadCallback$1({ session: options.session, keys: successfulKeys, isFinished: true }, { baseUrl: runtime.baseUrl, fetchImpl });
206
+ await recordUserMiniprogramUploadResult$1({
207
+ session: options.session,
208
+ miniProgramId: options.miniProgramId,
209
+ version: options.version,
210
+ sessionId: options.uploadSessionId,
211
+ keys: successfulKeys,
212
+ }, { baseUrl: runtime.baseUrl, fetchImpl });
213
+ }
214
+ if (firstUploadError) {
215
+ throw firstUploadError;
216
+ }
129
217
  }
130
218
  }, { successText: `已上传 ${orderedFiles.length} 个文件` });
131
219
  }
@@ -136,36 +224,27 @@ function chunkUploadFiles(files, batchSize) {
136
224
  }
137
225
  return batches;
138
226
  }
139
- function createUploadFileInfos(files, options) {
227
+ function createUploadFileInfos(files) {
140
228
  return files.map((file) => ({
141
229
  name: file.relativePath.split('/').pop() ?? file.relativePath,
142
230
  mimetype: file.mimeType,
143
231
  fsize: file.size,
144
- path: runtimePermissionEnv.getMiniappUploadKey({
145
- miniProgramId: options.miniProgramId,
146
- version: options.version,
147
- relativePath: file.relativePath,
148
- }),
232
+ path: file.key,
149
233
  }));
150
234
  }
151
- function validateUploadInfoKeys(files, keys, options) {
235
+ function validateUploadInfoKeys(files, keys) {
152
236
  if (keys.length !== files.length) {
153
237
  throw new index.CliError(`CDN 上传接口返回 key 数量异常:期望 ${files.length},实际 ${keys.length}`);
154
238
  }
155
239
  for (let index$1 = 0; index$1 < files.length; index$1 += 1) {
156
- const expectedKey = runtimePermissionEnv.getMiniappUploadKey({
157
- miniProgramId: options.miniProgramId,
158
- version: options.version,
159
- relativePath: files[index$1].relativePath,
160
- });
240
+ const expectedKey = files[index$1].key;
161
241
  const actualKey = keys[index$1];
162
242
  if (actualKey !== expectedKey) {
163
243
  throw new index.CliError(`CDN 上传接口返回 key 异常:${files[index$1].relativePath}`, [`CDN 上传接口返回 key 异常:${files[index$1].relativePath}`, `expected: ${expectedKey}`, `actual: ${actualKey}`].join('\n'));
164
244
  }
165
245
  }
166
246
  }
167
- async function createDefaultCosClient(uploadToken) {
168
- const COS = await loadCosConstructor();
247
+ function createDefaultCosClient(uploadToken, COS) {
169
248
  const cos = new COS({
170
249
  getAuthorization(_input, callback) {
171
250
  callback({
@@ -192,7 +271,7 @@ async function createDefaultCosClient(uploadToken) {
192
271
  };
193
272
  }
194
273
  async function loadCosConstructor() {
195
- const cosModule = await Promise.resolve().then(function () { return require('./index-DxH41nsb.cjs'); }).then(function (n) { return n.index; });
274
+ const cosModule = await Promise.resolve().then(function () { return require('./index-DB6Q8HaC.cjs'); }).then(function (n) { return n.index; });
196
275
  return cosModule.default;
197
276
  }
198
277
  function formatSize(bytes) {
@@ -235,43 +314,162 @@ function isRecord(value) {
235
314
  return Object.prototype.toString.call(value) === '[object Object]';
236
315
  }
237
316
 
238
- async function precheckUserMiniprogramVersion(options, runtime = {}) {
239
- return postUserMiniprogramVersionForm(runtimePermissionEnv.PRECHECK_USER_MINIPROGRAM_VERSION_API_PATH, options, {
240
- // 展示资料由服务端沿用 last approved 或注入默认名/默认图;CLI 不传 name/icon/cover。
241
- ...(options.version ? { version: options.version } : {}),
242
- ...(options.source_version ? { source_version: options.source_version } : {}),
243
- release_note: options.releaseNote,
244
- auto_publish: String(options.autoPublish),
245
- }, runtime);
317
+ async function computeMiniappArtifactSignature(files, runtime = {}) {
318
+ const preparedFiles = runtimePermissionEnv.prepareMiniappArtifactFiles(files);
319
+ const hash = node_crypto.createHash('sha256');
320
+ const createReadStream = runtime.createReadStream ?? fs.createReadStream;
321
+ hash.update(runtimePermissionEnv.createMiniappArtifactSignatureHeader(preparedFiles.length));
322
+ for (const file of preparedFiles) {
323
+ hash.update(runtimePermissionEnv.createMiniappArtifactFileHeader(file));
324
+ let bytesRead = 0;
325
+ for await (const chunk of createReadStream(file.absolutePath)) {
326
+ const bytes = typeof chunk === 'string' ? Buffer.from(chunk) : chunk;
327
+ bytesRead += bytes.byteLength;
328
+ hash.update(bytes);
329
+ }
330
+ if (bytesRead !== file.size) {
331
+ throw new Error(`构建产物在签名期间发生变化:${file.relativePath}`);
332
+ }
333
+ }
334
+ return hash.digest('hex');
246
335
  }
247
- async function submitUserMiniprogramAudit(options, runtime = {}) {
248
- return postUserMiniprogramVersionForm(runtimePermissionEnv.SUBMIT_USER_MINIPROGRAM_AUDIT_API_PATH, options, {
249
- // 展示资料由服务端沿用 last approved 或注入默认名/默认图;CLI 不传 name/icon/cover。
250
- ...(options.manifest ? { manifest: JSON.stringify(options.manifest) } : {}),
251
- ...(options.source_version ? { source_version: options.source_version } : {}),
252
- release_note: options.releaseNote,
253
- auto_publish: String(options.autoPublish),
254
- }, runtime);
336
+
337
+ const DEPLOY_LOCKED_MESSAGE = '当前设备已有同版本产物正在上传,请等待完成后重试';
338
+ const DEPLOY_LOCK_DIRECTORY_NAME = 'deploy-locks';
339
+ const DEPLOY_LOCK_OWNER_FILE = 'owner.json';
340
+ const INCOMPLETE_LOCK_GRACE_MS = 30_000;
341
+ const defaultPaths = index.envPaths('hb-sdk', { suffix: '' });
342
+ class DeployLockConflictError extends Error {
343
+ }
344
+ async function withDeployLock(scope, operation, options = {}) {
345
+ let lock;
346
+ try {
347
+ lock = await acquireDeployLock(scope, options);
348
+ }
349
+ catch (error) {
350
+ if (error instanceof DeployLockConflictError) {
351
+ throw error;
352
+ }
353
+ // 本地文件锁只减少重复操作,文件系统异常不阻断服务端上传流程。
354
+ return operation();
355
+ }
356
+ try {
357
+ return await operation();
358
+ }
359
+ finally {
360
+ await lock.release().catch(() => undefined);
361
+ }
255
362
  }
256
- async function postUserMiniprogramVersionForm(path, options, fields, runtime) {
257
- const fetchImpl = runtime.fetchImpl ?? fetch;
258
- const context = session.createHeyboxOpenPlatformRequestContext(options.session, path, {
259
- contentType: 'application/x-www-form-urlencoded',
260
- });
261
- const body = new URLSearchParams();
262
- body.set('mini_program_id', options.miniProgramId);
263
- for (const [key, value] of Object.entries(fields)) {
264
- body.set(key, value);
363
+ async function acquireDeployLock(scope, options = {}) {
364
+ const lockDirectory = getDeployLockDirectory(scope, options.cacheRoot);
365
+ const ownerFile = path.join(lockDirectory, DEPLOY_LOCK_OWNER_FILE);
366
+ const owner = {
367
+ version: 1,
368
+ pid: options.pid ?? process.pid,
369
+ token: options.token ?? node_crypto.randomBytes(16).toString('hex'),
370
+ };
371
+ await fs$1.mkdir(path.dirname(lockDirectory), { recursive: true, mode: 0o700 });
372
+ for (let attempt = 0; attempt < 2; attempt += 1) {
373
+ let created = false;
374
+ try {
375
+ await fs$1.mkdir(lockDirectory, { mode: 0o700 });
376
+ created = true;
377
+ await fs$1.writeFile(ownerFile, `${JSON.stringify(owner)}\n`, { encoding: 'utf8', mode: 0o600 });
378
+ return createDeployLock(lockDirectory, ownerFile, owner.token);
379
+ }
380
+ catch (error) {
381
+ if (created) {
382
+ await fs$1.rm(lockDirectory, { force: true, recursive: true });
383
+ throw error;
384
+ }
385
+ if (!isNodeError(error) || error.code !== 'EEXIST') {
386
+ throw error;
387
+ }
388
+ if (!(await canReclaimDeployLock(lockDirectory, ownerFile, options))) {
389
+ throw new DeployLockConflictError(DEPLOY_LOCKED_MESSAGE);
390
+ }
391
+ await reclaimDeployLock(lockDirectory, owner.token);
392
+ }
393
+ }
394
+ throw new DeployLockConflictError(DEPLOY_LOCKED_MESSAGE);
395
+ }
396
+ function getDeployLockDirectory(scope, cacheRoot = defaultPaths.cache) {
397
+ const scopeHash = node_crypto.createHash('sha256').update(`${scope.apiBaseUrl}\0${scope.miniProgramId}\0${scope.version}`).digest('hex');
398
+ return path.join(cacheRoot, DEPLOY_LOCK_DIRECTORY_NAME, scopeHash);
399
+ }
400
+ function createDeployLock(lockDirectory, ownerFile, token) {
401
+ let released = false;
402
+ return {
403
+ async release() {
404
+ if (released)
405
+ return;
406
+ released = true;
407
+ await removeOwnedLock(lockDirectory, ownerFile, token);
408
+ },
409
+ };
410
+ }
411
+ async function canReclaimDeployLock(lockDirectory, ownerFile, options) {
412
+ const owner = await readDeployLockOwner(ownerFile);
413
+ if (owner) {
414
+ return !(options.isProcessRunning ?? isProcessRunning)(owner.pid);
415
+ }
416
+ try {
417
+ const lockStat = await fs$1.stat(lockDirectory);
418
+ return (options.now ?? Date.now)() - lockStat.mtimeMs >= INCOMPLETE_LOCK_GRACE_MS;
419
+ }
420
+ catch (error) {
421
+ return isNodeError(error) && error.code === 'ENOENT';
422
+ }
423
+ }
424
+ async function removeOwnedLock(lockDirectory, ownerFile, token) {
425
+ const owner = await readDeployLockOwner(ownerFile);
426
+ if (owner?.token === token) {
427
+ await fs$1.rm(lockDirectory, { force: true, recursive: true });
428
+ }
429
+ }
430
+ async function reclaimDeployLock(lockDirectory, token) {
431
+ const reclaimDirectory = `${lockDirectory}.reclaim-${node_crypto.createHash('sha256').update(token).digest('hex')}`;
432
+ try {
433
+ await fs$1.rename(lockDirectory, reclaimDirectory);
434
+ }
435
+ catch (error) {
436
+ if (isNodeError(error) && error.code === 'ENOENT')
437
+ return;
438
+ throw error;
439
+ }
440
+ await fs$1.rm(reclaimDirectory, { force: true, recursive: true });
441
+ }
442
+ async function readDeployLockOwner(ownerFile) {
443
+ try {
444
+ const value = JSON.parse(await fs$1.readFile(ownerFile, 'utf8'));
445
+ return isDeployLockOwner(value) ? value : null;
446
+ }
447
+ catch (error) {
448
+ if (isNodeError(error) && error.code === 'ENOENT')
449
+ return null;
450
+ if (error instanceof SyntaxError)
451
+ return null;
452
+ throw error;
265
453
  }
266
- const response = await fetchImpl(session.createHeyboxApiUrl(runtime.baseUrl ?? context.baseUrl, path, context.platformParams), {
267
- method: 'POST',
268
- headers: context.headers,
269
- body: body.toString(),
270
- });
271
- const result = await session.readHeyboxApiEnvelope(response, {
272
- pathWithQuery: path,
273
- });
274
- return result ?? {};
454
+ }
455
+ function isDeployLockOwner(value) {
456
+ return (typeof value === 'object' &&
457
+ value !== null &&
458
+ value.version === 1 &&
459
+ Number.isInteger(value.pid) &&
460
+ typeof value.token === 'string');
461
+ }
462
+ function isProcessRunning(pid) {
463
+ try {
464
+ process.kill(pid, 0);
465
+ return true;
466
+ }
467
+ catch (error) {
468
+ return !isNodeError(error) || error.code !== 'ESRCH';
469
+ }
470
+ }
471
+ function isNodeError(error) {
472
+ return error instanceof Error && 'code' in error;
275
473
  }
276
474
 
277
475
  const SUPPORTED_PACKAGE_MANAGERS = [
@@ -313,127 +511,175 @@ async function runDeployCommand(options, runtime = {}) {
313
511
  const releaseNote = await resolveDeployReleaseNote(options, runtime);
314
512
  const session$1 = await logger.task('正在校验 Heybox 登录态', () => (runtime.requireAuthSession ?? session.requireHeyboxAuthSession)({ loginBaseUrl }), { successText: 'Heybox 登录态有效' });
315
513
  logger.debug(`Heybox API: ${apiBaseUrl}`);
316
- let precheckVersion;
317
- let parsedManifest;
318
- const historyArtifact = fromVersion ? { source_version: fromVersion } : {};
319
- if (fromVersion) {
320
- const precheck = await runVersionPrecheck({
321
- session: session$1,
322
- miniProgramId,
323
- ...historyArtifact,
324
- releaseNote,
325
- autoPublish,
326
- }, runtime, apiBaseUrl, projectRoot, logger);
327
- const sourceSdkVersion = typeof precheck.sdk_version === 'string' ? precheck.sdk_version.trim() : '';
328
- if (!sourceSdkVersion) {
329
- throw new Error('历史产物缺少 sdkVersion,无法复用,请重新构建并上传新版本');
330
- }
331
- precheckVersion = precheck.version || precheck.version_normalized || fromVersion;
332
- }
333
- else {
334
- if (typeof packageJson.scripts?.build !== 'string') {
335
- throw new Error('package.json scripts.build 未定义,请添加 build 脚本');
336
- }
337
- precheckVersion = validateVersionForDeploy(packageJson.version, 'package.json.version');
338
- await runVersionPrecheck({
339
- session: session$1,
340
- miniProgramId,
341
- version: precheckVersion,
342
- releaseNote,
343
- autoPublish,
344
- }, runtime, apiBaseUrl, projectRoot, logger);
345
- let skipPlatformCsp = false;
346
- try {
347
- const { detail } = await logger.task('正在读取远端小程序权限', () => (runtime.getBoundMiniProgram ?? context.getBoundMiniProgram)({
348
- allowUnsafeApiBaseUrl: options.allowUnsafeApiBaseUrl,
349
- apiBaseUrl,
350
- cwd: projectRoot,
351
- env,
352
- fetchImpl: runtime.fetchImpl,
353
- loginBaseUrl,
514
+ const uploadVersion = fromVersion ? '' : validateVersionForDeploy(packageJson.version, 'package.json.version');
515
+ const deploy = async () => {
516
+ let precheckVersion;
517
+ const historyArtifact = fromVersion ? { source_version: fromVersion } : {};
518
+ if (fromVersion) {
519
+ const precheck = await runVersionPrecheck({
354
520
  session: session$1,
355
- }), { successText: '已读取远端小程序权限' });
356
- skipPlatformCsp = context.shouldSkipMiniappPlatformCsp(detail.runtime_permissions);
357
- }
358
- catch (error) {
359
- logger.warn('未读取到远端 Runtime 权限,构建将继续注入平台 CSP。');
360
- logger.debug(`远端 Runtime 权限读取失败详情:${index.readRedactedErrorMessage(error, { verbose: true })}`);
521
+ miniProgramId,
522
+ ...historyArtifact,
523
+ releaseNote,
524
+ autoPublish,
525
+ }, runtime, apiBaseUrl, projectRoot, logger);
526
+ const sourceSdkVersion = typeof precheck.sdk_version === 'string' ? precheck.sdk_version.trim() : '';
527
+ if (!sourceSdkVersion) {
528
+ throw new Error('历史产物缺少 sdkVersion,无法复用,请重新构建并上传新版本');
529
+ }
530
+ precheckVersion = precheck.version || precheck.version_normalized || fromVersion;
361
531
  }
362
- const pm = detectPackageManager(projectRoot);
363
- const platform = runtime.platform ?? process.platform;
364
- const buildMode = validateBuildMode(env.HB_SDK_ENV?.trim(), platform);
365
- logger.info(`开始构建: ${pm} ${createBuildArgs(pm, buildMode).join(' ')}`);
366
- await runBuildScript(pm, projectRoot, runtime.spawn ?? childProcess.spawn, {
367
- env: runtimePermissionEnv.createMiniappPlatformCspEnv(env, skipPlatformCsp),
368
- mode: buildMode,
369
- outputMode: options.buildOutputMode ?? 'inherit',
370
- platform,
371
- stderr: runtime.stderr,
372
- });
373
- logger.success('构建完成');
374
- }
375
- let submitAuditResult;
376
- let version = precheckVersion;
377
- let manifest;
378
- if (!fromVersion) {
379
- const distDir = path.join(projectRoot, 'dist');
380
- parsedManifest ??= await readDeployManifest(projectRoot, logger);
381
- ({ manifest, version } = parsedManifest);
382
- if (version !== precheckVersion) {
383
- throw new Error(`dist/manifest.json.version (${version}) 与预检版本 (${precheckVersion}) 不一致,请重新 build 后再 deploy`);
532
+ else {
533
+ if (typeof packageJson.scripts?.build !== 'string') {
534
+ throw new Error('package.json scripts.build 未定义,请添加 build 脚本');
535
+ }
536
+ precheckVersion = uploadVersion;
537
+ await runVersionPrecheck({
538
+ session: session$1,
539
+ miniProgramId,
540
+ version: precheckVersion,
541
+ releaseNote,
542
+ autoPublish,
543
+ }, runtime, apiBaseUrl, projectRoot, logger);
544
+ let skipPlatformCsp = false;
545
+ try {
546
+ const { detail } = await logger.task('正在读取远端小程序权限', () => (runtime.getBoundMiniProgram ?? context.getBoundMiniProgram)({
547
+ allowUnsafeApiBaseUrl: options.allowUnsafeApiBaseUrl,
548
+ apiBaseUrl,
549
+ cwd: projectRoot,
550
+ env,
551
+ fetchImpl: runtime.fetchImpl,
552
+ loginBaseUrl,
553
+ session: session$1,
554
+ }), { successText: '已读取远端小程序权限' });
555
+ skipPlatformCsp = context.shouldSkipMiniappPlatformCsp(detail.runtime_permissions);
556
+ }
557
+ catch (error) {
558
+ logger.warn('未读取到远端 Runtime 权限,构建将继续注入平台 CSP。');
559
+ logger.debug(`远端 Runtime 权限读取失败详情:${index.readRedactedErrorMessage(error, { verbose: true })}`);
560
+ }
561
+ const pm = detectPackageManager(projectRoot);
562
+ const platform = runtime.platform ?? process.platform;
563
+ const buildMode = validateBuildMode(env.HB_SDK_ENV?.trim(), platform);
564
+ logger.info(`开始构建: ${pm} ${createBuildArgs(pm, buildMode).join(' ')}`);
565
+ await runBuildScript(pm, projectRoot, runtime.spawn ?? childProcess.spawn, {
566
+ env: runtimePermissionEnv.createMiniappPlatformCspEnv(env, skipPlatformCsp),
567
+ mode: buildMode,
568
+ outputMode: options.buildOutputMode ?? 'inherit',
569
+ platform,
570
+ stderr: runtime.stderr,
571
+ });
572
+ logger.success('构建完成');
384
573
  }
385
- const uploadFiles = await logger.task('正在校验 dist 上传文件', async () => {
386
- const allFiles = await walkDistFiles(distDir);
387
- const blocked = allFiles.find((entry) => runtimePermissionEnv.relativePathContainsNodeModulesSegment(entry.relativePath));
388
- if (blocked) {
389
- throw new Error(`dist 目录含 node_modules:${blocked.relativePath}`);
574
+ let submitAuditResult;
575
+ let version = precheckVersion;
576
+ let manifest;
577
+ let uploadSessionId = '';
578
+ if (!fromVersion) {
579
+ const distDir = path.join(projectRoot, 'dist');
580
+ const parsedManifest = await readDeployManifest(projectRoot, logger);
581
+ ({ manifest, version } = parsedManifest);
582
+ if (version !== precheckVersion) {
583
+ throw new Error(`dist/manifest.json.version (${version}) 与预检版本 (${precheckVersion}) 不一致,请重新 build 后再 deploy`);
390
584
  }
391
- const files = allFiles.filter((entry) => runtimePermissionEnv.shouldUploadDistFile(entry.relativePath));
392
- const pathError = runtimePermissionEnv.validateUploadPaths(files, { miniProgramId, version, maxLength: runtimePermissionEnv.ACTIVITY_UPLOAD_KEY_MAX_LENGTH });
393
- if (pathError) {
394
- throw new Error(pathError);
585
+ const uploadFiles = await logger.task('正在校验 dist 上传文件', async () => {
586
+ const allFiles = await walkDistFiles(distDir);
587
+ const blocked = allFiles.find((entry) => runtimePermissionEnv.relativePathContainsNodeModulesSegment(entry.relativePath));
588
+ if (blocked) {
589
+ throw new Error(`dist 目录含 node_modules:${blocked.relativePath}`);
590
+ }
591
+ const files = runtimePermissionEnv.prepareMiniappArtifactFiles(allFiles.filter((entry) => runtimePermissionEnv.shouldUploadDistFile(entry.relativePath)));
592
+ const pathError = runtimePermissionEnv.validateMiniappUploadPathLengths(files, { miniProgramId });
593
+ if (pathError) {
594
+ throw new Error(pathError);
595
+ }
596
+ const sizeError = runtimePermissionEnv.validateUploadTotalSize(files);
597
+ if (sizeError) {
598
+ throw new Error(sizeError);
599
+ }
600
+ logger.debug(`待上传文件数: ${files.length}`);
601
+ return files;
602
+ }, { successText: 'dist 上传文件校验完成' });
603
+ // deploy 期间 dist 视为稳定构建输出;为避免持有大量文件描述符,签名和上传不会共享打开的文件句柄。
604
+ const artifactSignature = await logger.task('正在计算构建产物签名', () => (runtime.computeMiniappArtifactSignature ?? computeMiniappArtifactSignature)(uploadFiles), { successText: '构建产物签名计算完成' });
605
+ const createdUploadSession = await logger.task('正在创建或恢复上传会话', async () => {
606
+ const result = await (runtime.createUserMiniprogramUploadSession ?? createUserMiniprogramUploadSession)({
607
+ session: session$1,
608
+ miniProgramId,
609
+ version,
610
+ artifactSignature,
611
+ relativePaths: uploadFiles.map((file) => file.relativePath),
612
+ fileSizes: uploadFiles.map((file) => file.size),
613
+ }, { baseUrl: apiBaseUrl, fetchImpl: runtime.fetchImpl });
614
+ if (typeof result.session_id !== 'string' || !result.session_id.trim()) {
615
+ throw new Error('创建上传会话失败:服务端未返回 session_id');
616
+ }
617
+ const resolved = runtimePermissionEnv.resolveMiniappUploadSessionFiles(uploadFiles, Array.isArray(result.files) ? result.files : [], Array.isArray(result.uploaded_keys) ? result.uploaded_keys : []);
618
+ return {
619
+ sessionId: result.session_id.trim(),
620
+ files: resolved.files,
621
+ missingFiles: resolved.missingFiles,
622
+ };
623
+ }, { successText: '上传会话已就绪' });
624
+ uploadSessionId = createdUploadSession.sessionId;
625
+ const uploadedCount = createdUploadSession.files.length - createdUploadSession.missingFiles.length;
626
+ if (uploadedCount > 0) {
627
+ logger.debug(`复用已上传文件: ${uploadedCount}/${createdUploadSession.files.length}`);
628
+ }
629
+ try {
630
+ if (createdUploadSession.missingFiles.length > 0) {
631
+ await assertDeployUploadFilesStillMatchSignature({
632
+ computeMiniappArtifactSignature: runtime.computeMiniappArtifactSignature ?? computeMiniappArtifactSignature,
633
+ distDir,
634
+ expectedFiles: uploadFiles,
635
+ expectedSignature: artifactSignature,
636
+ miniProgramId,
637
+ });
638
+ await (runtime.runUpload ?? runUpload)({
639
+ session: session$1,
640
+ files: createdUploadSession.missingFiles.map(({ file, key }) => ({ ...file, key })),
641
+ miniProgramId,
642
+ version,
643
+ uploadSessionId,
644
+ }, { baseUrl: apiBaseUrl, fetchImpl: runtime.fetchImpl, logger });
645
+ }
395
646
  }
396
- const sizeError = runtimePermissionEnv.validateUploadTotalSize(files);
397
- if (sizeError) {
398
- throw new Error(sizeError);
647
+ catch (error) {
648
+ throw translateHeyboxDeployError(error, { projectRoot, stage: 'upload', version });
399
649
  }
400
- logger.debug(`待上传文件数: ${files.length}`);
401
- return files;
402
- }, { successText: 'dist 上传文件校验完成' });
650
+ }
403
651
  try {
404
- await (runtime.runUpload ?? runUpload)({ session: session$1, miniProgramId, version, files: uploadFiles }, { baseUrl: apiBaseUrl, fetchImpl: runtime.fetchImpl, logger });
652
+ const submitOptions = fromVersion
653
+ ? { session: session$1, miniProgramId, ...historyArtifact, releaseNote, autoPublish }
654
+ : { session: session$1, miniProgramId, manifest, sessionId: uploadSessionId, releaseNote, autoPublish };
655
+ submitAuditResult = await logger.task('正在提交审核', () => (runtime.submitUserMiniprogramAudit ?? submitUserMiniprogramAudit)(submitOptions, {
656
+ baseUrl: apiBaseUrl,
657
+ fetchImpl: runtime.fetchImpl,
658
+ }), { successText: '审核提交完成' });
405
659
  }
406
660
  catch (error) {
407
- throw translateHeyboxDeployError(error, { projectRoot, stage: 'upload', version });
661
+ throw translateHeyboxDeployError(error, { projectRoot, stage: 'submitAudit', version });
408
662
  }
409
- }
410
- try {
411
- const submitOptions = fromVersion
412
- ? { session: session$1, miniProgramId, ...historyArtifact, releaseNote, autoPublish }
413
- : { session: session$1, miniProgramId, manifest, releaseNote, autoPublish };
414
- submitAuditResult = await logger.task('正在提交审核', () => (runtime.submitUserMiniprogramAudit ?? submitUserMiniprogramAudit)(submitOptions, {
415
- baseUrl: apiBaseUrl,
416
- fetchImpl: runtime.fetchImpl,
417
- }), { successText: '审核提交完成' });
418
- }
419
- catch (error) {
420
- throw translateHeyboxDeployError(error, { projectRoot, stage: 'submitAudit', version });
421
- }
422
- printDeploySuccess({
423
- autoPublish,
424
- logger,
425
- miniProgramId,
426
- mode: options.successOutputMode ?? 'legacy',
427
- protocolString: submitAuditResult.protocol_string,
428
- version,
429
- });
430
- return {
431
- autoPublish,
432
- changed: true,
433
- miniProgramId,
434
- ...(submitAuditResult.protocol_string ? { protocolString: submitAuditResult.protocol_string } : {}),
435
- version,
663
+ printDeploySuccess({
664
+ autoPublish,
665
+ logger,
666
+ miniProgramId,
667
+ mode: options.successOutputMode ?? 'legacy',
668
+ protocolString: submitAuditResult.protocol_string,
669
+ version,
670
+ });
671
+ return {
672
+ autoPublish,
673
+ changed: true,
674
+ miniProgramId,
675
+ ...(submitAuditResult.protocol_string ? { protocolString: submitAuditResult.protocol_string } : {}),
676
+ version,
677
+ };
436
678
  };
679
+ if (fromVersion) {
680
+ return deploy();
681
+ }
682
+ return (runtime.withDeployLock ?? withDeployLock)({ apiBaseUrl, miniProgramId, version: uploadVersion }, deploy);
437
683
  }
438
684
  function printDeploySuccess(options) {
439
685
  options.logger.success(`提交审核成功:${options.miniProgramId} ${options.version}`);
@@ -557,9 +803,7 @@ async function runBuildScript(pm, cwd, spawnImpl, options) {
557
803
  const pipeStdoutToStderr = options.outputMode === 'stderr';
558
804
  const buildArgs = createBuildArgs(pm, options.mode);
559
805
  const isWindows = options.platform === 'win32';
560
- const [executable, args] = isWindows
561
- ? createWindowsBuildCommand(pm, buildArgs)
562
- : [pm, buildArgs];
806
+ const [executable, args] = isWindows ? createWindowsBuildCommand(pm, buildArgs) : [pm, buildArgs];
563
807
  const child = spawnImpl(executable, args, {
564
808
  cwd,
565
809
  env: { ...process.env, ...options.env },
@@ -620,7 +864,7 @@ async function walkDistFiles(distDir) {
620
864
  }
621
865
  const fileStat = await fs$1.stat(absPath);
622
866
  results.push({
623
- relativePath: runtimePermissionEnv.normalizeRelativePath(relPath),
867
+ relativePath: relPath,
624
868
  absolutePath: absPath,
625
869
  size: fileStat.size,
626
870
  mimeType: inferMimeType(entry.name),
@@ -631,6 +875,31 @@ async function walkDistFiles(distDir) {
631
875
  function inferMimeType(fileName) {
632
876
  return MIME_BY_EXT[path.extname(fileName).toLowerCase()] || 'application/octet-stream';
633
877
  }
878
+ async function assertDeployUploadFilesStillMatchSignature(options) {
879
+ const currentFiles = runtimePermissionEnv.prepareMiniappArtifactFiles((await walkDistFiles(options.distDir)).filter((entry) => runtimePermissionEnv.shouldUploadDistFile(entry.relativePath)));
880
+ const currentByPath = new Map(currentFiles.map(file => [file.relativePath, file]));
881
+ if (currentFiles.length !== options.expectedFiles.length) {
882
+ throw new Error('dist 构建产物在签名后发生变化,请重新执行 deploy');
883
+ }
884
+ for (const expected of options.expectedFiles) {
885
+ const current = currentByPath.get(expected.relativePath);
886
+ if (!current || current.size !== expected.size) {
887
+ throw new Error(`dist 构建产物在签名后发生变化:${expected.relativePath}`);
888
+ }
889
+ }
890
+ const pathError = runtimePermissionEnv.validateMiniappUploadPathLengths(currentFiles, { miniProgramId: options.miniProgramId });
891
+ if (pathError) {
892
+ throw new Error(pathError);
893
+ }
894
+ const sizeError = runtimePermissionEnv.validateUploadTotalSize(currentFiles);
895
+ if (sizeError) {
896
+ throw new Error(sizeError);
897
+ }
898
+ const currentSignature = await options.computeMiniappArtifactSignature(currentFiles);
899
+ if (currentSignature !== options.expectedSignature) {
900
+ throw new Error('dist 构建产物在签名后发生变化,请重新 build 后再 deploy');
901
+ }
902
+ }
634
903
  function translateHeyboxDeployError(error, options) {
635
904
  const message = error instanceof Error && error.message ? error.message : String(error);
636
905
  if (isVersionLifecycleConflictMessage(message) && options.stage !== 'upload') {
@@ -645,18 +914,6 @@ function translateHeyboxDeployError(error, options) {
645
914
  `原始错误:${readVerboseDeployErrorMessage(error)}`,
646
915
  ].join('\n'));
647
916
  }
648
- if (isDuplicateUploadErrorMessage(message) && options.stage === 'upload') {
649
- const packageJsonPath = path.join(options.projectRoot, 'package.json');
650
- const nextVersion = suggestNextPatchVersion(options.version);
651
- return new index.CliError([
652
- `当前小程序版本 ${options.version} 的上传文件已存在,不能覆盖同版本发布产物。`,
653
- `请升级 ${packageJsonPath} 中的 version${nextVersion ? `(例如 ${nextVersion})` : ''},重新 build 后再 deploy。`,
654
- ].join('\n'), [
655
- `当前小程序版本 ${options.version} 的上传文件已存在,不能覆盖同版本发布产物。`,
656
- `请升级 ${packageJsonPath} 中的 version${nextVersion ? `(例如 ${nextVersion})` : ''},重新 build 后再 deploy。`,
657
- `原始错误:${readVerboseDeployErrorMessage(error)}`,
658
- ].join('\n'));
659
- }
660
917
  if (isAuthExpiredErrorMessage(message)) {
661
918
  if (options.stage === 'upload') {
662
919
  return new index.CliError([
@@ -672,9 +929,6 @@ function translateHeyboxDeployError(error, options) {
672
929
  }
673
930
  return error instanceof Error ? error : new Error(message);
674
931
  }
675
- function isDuplicateUploadErrorMessage(message) {
676
- return /重名|duplicate|already exists|exists/i.test(message);
677
- }
678
932
  function isVersionLifecycleConflictMessage(message) {
679
933
  return /AlreadyExists\(6\)|不能复用|已进入线上生命周期|正在审核|活跃候选版本|同版本/i.test(message);
680
934
  }