@heybox/hb-sdk 0.6.8-alpha.2 → 0.6.9-alpha.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 (38) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +2 -0
  3. package/dist/cli-chunks/{build-CglqyB9Z.cjs → build-9hbhrRbU.cjs} +29 -25
  4. package/dist/cli-chunks/{context-CP7W_8aR.cjs → context-7rmakzS_.cjs} +122 -23
  5. package/dist/cli-chunks/{create-DldsTIVt.cjs → create-BkX4rtRP.cjs} +1 -1
  6. package/dist/cli-chunks/{dev-BG0icySa.cjs → dev-DiZGT4FQ.cjs} +142 -108
  7. package/dist/cli-chunks/{doctor-Cd6Xq0m-.cjs → doctor-BcpqXVRh.cjs} +1 -1
  8. package/dist/cli-chunks/{index-jXyfZKy2.cjs → index-6HfcwZ_r.cjs} +3 -3
  9. package/dist/cli-chunks/{index-paN77avR.cjs → index-DofTxdoX.cjs} +14 -14
  10. package/dist/cli-chunks/{login-DlD9n_AF.cjs → login-DUiejZcD.cjs} +2 -2
  11. package/dist/cli-chunks/{project-vite-vHezGsg7.cjs → project-vite-CfD8Bp1K.cjs} +1 -1
  12. package/dist/cli-chunks/{remote-O5_nRdZN.cjs → remote-A4-PKgOn.cjs} +462 -190
  13. package/dist/cli-chunks/{runtime-gate-DfMJQGH9.cjs → runtime-gate-BWlU-R4h.cjs} +3 -3
  14. package/dist/cli-chunks/{index-v4-6fbXX.cjs → runtime-permission-env-DKrhgVM3.cjs} +276 -49
  15. package/dist/cli-chunks/{session-CiQplems.cjs → session-D5u3XzHN.cjs} +1 -1
  16. package/dist/cli.cjs +1 -1
  17. package/dist/devtools/mock-host/main.js +609 -6
  18. package/dist/index.cjs.js +403 -7
  19. package/dist/index.esm.js +403 -7
  20. package/dist/miniapp-publish.cjs.js +205 -0
  21. package/dist/miniapp-publish.esm.js +201 -1
  22. package/dist/vite.cjs.js +17 -2
  23. package/dist/vite.esm.js +17 -2
  24. package/package.json +2 -2
  25. package/skill/SKILL.md +7 -6
  26. package/skill/references/api-root.md +7 -2
  27. package/skill/references/cli.md +12 -0
  28. package/skill/references/examples.md +17 -2
  29. package/skill/references/safety-boundaries.md +2 -0
  30. package/skill/scripts/sync-references.mjs +17 -2
  31. package/skill/skill.json +4 -4
  32. package/types/core/network-sanitize.d.ts +31 -0
  33. package/types/miniapp-publish/index.d.ts +77 -0
  34. package/types/modules/network/index.d.ts +2 -0
  35. package/types/modules/network/observability.d.ts +39 -0
  36. package/types/modules/network/request-shape.d.ts +44 -0
  37. package/types/vite/html-policy.d.ts +1 -0
  38. package/types/vite/runtime-permission-env.d.ts +9 -0
@@ -1,15 +1,16 @@
1
1
  'use strict';
2
2
 
3
3
  var promises = require('node:readline/promises');
4
- var index = require('./index-paN77avR.cjs');
5
- var session = require('./session-CiQplems.cjs');
4
+ var index = require('./index-DofTxdoX.cjs');
5
+ var session = require('./session-D5u3XzHN.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 index$1 = require('./index-v4-6fbXX.cjs');
11
- var runtimeGate = require('./runtime-gate-DfMJQGH9.cjs');
12
- var context = require('./context-CP7W_8aR.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-7rmakzS_.cjs');
13
14
  require('node:module');
14
15
  require('path');
15
16
  require('os');
@@ -20,9 +21,9 @@ 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
+ require('node:async_hooks');
26
27
 
27
28
  async function getCDNUploadInfo(options, runtime = {}) {
28
29
  const body = new URLSearchParams();
@@ -59,6 +60,63 @@ async function postHeyboxApi(pathWithQuery, body, session$1, runtime) {
59
60
  return result;
60
61
  }
61
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
+
62
120
  const DEFAULT_CONCURRENCY = 4;
63
121
  async function runUpload(options, runtime = {}) {
64
122
  const logger = runtime.logger ?? index.createCliLogger();
@@ -67,9 +125,15 @@ async function runUpload(options, runtime = {}) {
67
125
  const getCDNUploadInfo$1 = runtime.getCDNUploadInfo ?? getCDNUploadInfo;
68
126
  const getCDNUploadToken$1 = runtime.getCDNUploadToken ?? getCDNUploadToken;
69
127
  const postCDNUploadCallback$1 = runtime.postCDNUploadCallback ?? postCDNUploadCallback;
128
+ const recordUserMiniprogramUploadResult$1 = runtime.recordUserMiniprogramUploadResult ?? recordUserMiniprogramUploadResult;
70
129
  const createReadStream = runtime.createReadStream ?? fs.createReadStream;
71
- const orderedFiles = [...options.files].sort((a, b) => a.relativePath.localeCompare(b.relativePath));
72
- const batches = chunkUploadFiles(orderedFiles, index$1.MINIAPP_UPLOAD_BATCH_SIZE);
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);
136
+ const batches = chunkUploadFiles(orderedFiles, runtimePermissionEnv.MINIAPP_UPLOAD_BATCH_SIZE);
73
137
  await logger.task(`正在上传 ${orderedFiles.length} 个文件`, async (taskContext) => {
74
138
  logger.debug(`上传并发数: ${concurrency}`);
75
139
  logger.debug(`上传批次数: ${batches.length}`);
@@ -78,25 +142,37 @@ async function runUpload(options, runtime = {}) {
78
142
  const batch = batches[batchIndex];
79
143
  logger.debug(`正在处理上传批次 ${batchIndex + 1}/${batches.length},文件数: ${batch.length}`);
80
144
  logger.debug('正在获取 CDN 上传信息');
81
- const fileInfos = createUploadFileInfos(batch, options);
82
- const uploadInfo = await getCDNUploadInfo$1({ session: options.session, scope: index$1.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 });
83
152
  logger.debug(`CDN bucket: ${uploadInfo.bucket}`);
84
153
  logger.debug(`CDN region: ${uploadInfo.region}`);
85
- validateUploadInfoKeys(batch, uploadInfo.keys, options);
154
+ validateUploadInfoKeys(batch, uploadInfo.keys);
86
155
  const indexedFiles = batch.map((file, index) => ({ file, key: uploadInfo.keys[index] }));
87
- const batchKeys = indexedFiles.map(({ key }) => key);
88
- logger.debug('正在获取 CDN 上传凭证');
89
- const uploadToken = await getCDNUploadToken$1({
90
- session: options.session,
91
- bucket: uploadInfo.bucket,
92
- keys: batchKeys,
93
- mimetypes: indexedFiles.map(({ file }) => file.mimeType),
94
- isMultipartUpload: 0,
95
- }, { baseUrl: runtime.baseUrl, fetchImpl });
96
- 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 = [];
97
172
  const queue = indexedFiles.slice();
173
+ let firstUploadError;
98
174
  const workers = Array.from({ length: Math.min(concurrency, indexedFiles.length) }, async () => {
99
- while (queue.length > 0) {
175
+ while (queue.length > 0 && !firstUploadError) {
100
176
  const uploadTask = queue.shift();
101
177
  if (!uploadTask) {
102
178
  return;
@@ -110,6 +186,7 @@ async function runUpload(options, runtime = {}) {
110
186
  ContentLength: uploadTask.file.size,
111
187
  ContentType: uploadTask.file.mimeType,
112
188
  });
189
+ successfulKeys.push(uploadTask.key);
113
190
  completed += 1;
114
191
  taskContext.update(`正在上传 ${completed}/${orderedFiles.length} 个文件`);
115
192
  logger.debug(`[${String(completed).padStart(2)}/${orderedFiles.length}] ok ${uploadTask.file.relativePath} (${formatSize(uploadTask.file.size)})`);
@@ -118,13 +195,25 @@ async function runUpload(options, runtime = {}) {
118
195
  completed += 1;
119
196
  const message = readErrorMessage(error);
120
197
  logger.debug(`[${String(completed).padStart(2)}/${orderedFiles.length}] error ${uploadTask.file.relativePath} -> ${message}`);
121
- 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));
122
199
  }
123
200
  }
124
201
  });
125
202
  await Promise.all(workers);
126
- logger.debug('正在确认 CDN 上传结果');
127
- 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
+ }
128
217
  }
129
218
  }, { successText: `已上传 ${orderedFiles.length} 个文件` });
130
219
  }
@@ -135,36 +224,27 @@ function chunkUploadFiles(files, batchSize) {
135
224
  }
136
225
  return batches;
137
226
  }
138
- function createUploadFileInfos(files, options) {
227
+ function createUploadFileInfos(files) {
139
228
  return files.map((file) => ({
140
229
  name: file.relativePath.split('/').pop() ?? file.relativePath,
141
230
  mimetype: file.mimeType,
142
231
  fsize: file.size,
143
- path: index$1.getMiniappUploadKey({
144
- miniProgramId: options.miniProgramId,
145
- version: options.version,
146
- relativePath: file.relativePath,
147
- }),
232
+ path: file.key,
148
233
  }));
149
234
  }
150
- function validateUploadInfoKeys(files, keys, options) {
235
+ function validateUploadInfoKeys(files, keys) {
151
236
  if (keys.length !== files.length) {
152
237
  throw new index.CliError(`CDN 上传接口返回 key 数量异常:期望 ${files.length},实际 ${keys.length}`);
153
238
  }
154
- for (let index$2 = 0; index$2 < files.length; index$2 += 1) {
155
- const expectedKey = index$1.getMiniappUploadKey({
156
- miniProgramId: options.miniProgramId,
157
- version: options.version,
158
- relativePath: files[index$2].relativePath,
159
- });
160
- const actualKey = keys[index$2];
239
+ for (let index$1 = 0; index$1 < files.length; index$1 += 1) {
240
+ const expectedKey = files[index$1].key;
241
+ const actualKey = keys[index$1];
161
242
  if (actualKey !== expectedKey) {
162
- throw new index.CliError(`CDN 上传接口返回 key 异常:${files[index$2].relativePath}`, [`CDN 上传接口返回 key 异常:${files[index$2].relativePath}`, `expected: ${expectedKey}`, `actual: ${actualKey}`].join('\n'));
243
+ throw new index.CliError(`CDN 上传接口返回 key 异常:${files[index$1].relativePath}`, [`CDN 上传接口返回 key 异常:${files[index$1].relativePath}`, `expected: ${expectedKey}`, `actual: ${actualKey}`].join('\n'));
163
244
  }
164
245
  }
165
246
  }
166
- async function createDefaultCosClient(uploadToken) {
167
- const COS = await loadCosConstructor();
247
+ function createDefaultCosClient(uploadToken, COS) {
168
248
  const cos = new COS({
169
249
  getAuthorization(_input, callback) {
170
250
  callback({
@@ -191,7 +271,7 @@ async function createDefaultCosClient(uploadToken) {
191
271
  };
192
272
  }
193
273
  async function loadCosConstructor() {
194
- const cosModule = await Promise.resolve().then(function () { return require('./index-jXyfZKy2.cjs'); }).then(function (n) { return n.index; });
274
+ const cosModule = await Promise.resolve().then(function () { return require('./index-6HfcwZ_r.cjs'); }).then(function (n) { return n.index; });
195
275
  return cosModule.default;
196
276
  }
197
277
  function formatSize(bytes) {
@@ -234,43 +314,162 @@ function isRecord(value) {
234
314
  return Object.prototype.toString.call(value) === '[object Object]';
235
315
  }
236
316
 
237
- async function precheckUserMiniprogramVersion(options, runtime = {}) {
238
- return postUserMiniprogramVersionForm(index$1.PRECHECK_USER_MINIPROGRAM_VERSION_API_PATH, options, {
239
- // 展示资料由服务端沿用 last approved 或注入默认名/默认图;CLI 不传 name/icon/cover。
240
- ...(options.version ? { version: options.version } : {}),
241
- ...(options.source_version ? { source_version: options.source_version } : {}),
242
- release_note: options.releaseNote,
243
- auto_publish: String(options.autoPublish),
244
- }, 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');
245
335
  }
246
- async function submitUserMiniprogramAudit(options, runtime = {}) {
247
- return postUserMiniprogramVersionForm(index$1.SUBMIT_USER_MINIPROGRAM_AUDIT_API_PATH, options, {
248
- // 展示资料由服务端沿用 last approved 或注入默认名/默认图;CLI 不传 name/icon/cover。
249
- ...(options.manifest ? { manifest: JSON.stringify(options.manifest) } : {}),
250
- ...(options.source_version ? { source_version: options.source_version } : {}),
251
- release_note: options.releaseNote,
252
- auto_publish: String(options.autoPublish),
253
- }, 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
+ }
254
362
  }
255
- async function postUserMiniprogramVersionForm(path, options, fields, runtime) {
256
- const fetchImpl = runtime.fetchImpl ?? fetch;
257
- const context = session.createHeyboxOpenPlatformRequestContext(options.session, path, {
258
- contentType: 'application/x-www-form-urlencoded',
259
- });
260
- const body = new URLSearchParams();
261
- body.set('mini_program_id', options.miniProgramId);
262
- for (const [key, value] of Object.entries(fields)) {
263
- 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
+ }
264
393
  }
265
- const response = await fetchImpl(session.createHeyboxApiUrl(runtime.baseUrl ?? context.baseUrl, path, context.platformParams), {
266
- method: 'POST',
267
- headers: context.headers,
268
- body: body.toString(),
269
- });
270
- const result = await session.readHeyboxApiEnvelope(response, {
271
- pathWithQuery: path,
272
- });
273
- return result ?? {};
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;
453
+ }
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;
274
473
  }
275
474
 
276
475
  const SUPPORTED_PACKAGE_MANAGERS = [
@@ -312,110 +511,175 @@ async function runDeployCommand(options, runtime = {}) {
312
511
  const releaseNote = await resolveDeployReleaseNote(options, runtime);
313
512
  const session$1 = await logger.task('正在校验 Heybox 登录态', () => (runtime.requireAuthSession ?? session.requireHeyboxAuthSession)({ loginBaseUrl }), { successText: 'Heybox 登录态有效' });
314
513
  logger.debug(`Heybox API: ${apiBaseUrl}`);
315
- let precheckVersion;
316
- let parsedManifest;
317
- const historyArtifact = fromVersion ? { source_version: fromVersion } : {};
318
- if (fromVersion) {
319
- const precheck = await runVersionPrecheck({
320
- session: session$1,
321
- miniProgramId,
322
- ...historyArtifact,
323
- releaseNote,
324
- autoPublish,
325
- }, runtime, apiBaseUrl, projectRoot, logger);
326
- const sourceSdkVersion = typeof precheck.sdk_version === 'string' ? precheck.sdk_version.trim() : '';
327
- if (!sourceSdkVersion) {
328
- throw new Error('历史产物缺少 sdkVersion,无法复用,请重新构建并上传新版本');
329
- }
330
- precheckVersion = precheck.version || precheck.version_normalized || fromVersion;
331
- }
332
- else {
333
- if (typeof packageJson.scripts?.build !== 'string') {
334
- throw new Error('package.json scripts.build 未定义,请添加 build 脚本');
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({
520
+ session: session$1,
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;
335
531
  }
336
- precheckVersion = validateVersionForDeploy(packageJson.version, 'package.json.version');
337
- await runVersionPrecheck({
338
- session: session$1,
339
- miniProgramId,
340
- version: precheckVersion,
341
- releaseNote,
342
- autoPublish,
343
- }, runtime, apiBaseUrl, projectRoot, logger);
344
- const pm = detectPackageManager(projectRoot);
345
- const platform = runtime.platform ?? process.platform;
346
- const buildMode = validateBuildMode(env.HB_SDK_ENV?.trim(), platform);
347
- logger.info(`开始构建: ${pm} ${createBuildArgs(pm, buildMode).join(' ')}`);
348
- await runBuildScript(pm, projectRoot, runtime.spawn ?? childProcess.spawn, {
349
- env,
350
- mode: buildMode,
351
- outputMode: options.buildOutputMode ?? 'inherit',
352
- platform,
353
- stderr: runtime.stderr,
354
- });
355
- logger.success('构建完成');
356
- }
357
- let submitAuditResult;
358
- let version = precheckVersion;
359
- let manifest;
360
- if (!fromVersion) {
361
- const distDir = path.join(projectRoot, 'dist');
362
- parsedManifest ??= await readDeployManifest(projectRoot, logger);
363
- ({ manifest, version } = parsedManifest);
364
- if (version !== precheckVersion) {
365
- 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('构建完成');
366
573
  }
367
- const uploadFiles = await logger.task('正在校验 dist 上传文件', async () => {
368
- const allFiles = await walkDistFiles(distDir);
369
- const blocked = allFiles.find((entry) => index$1.relativePathContainsNodeModulesSegment(entry.relativePath));
370
- if (blocked) {
371
- 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`);
372
584
  }
373
- const files = allFiles.filter((entry) => index$1.shouldUploadDistFile(entry.relativePath));
374
- const pathError = index$1.validateUploadPaths(files, { miniProgramId, version, maxLength: index$1.ACTIVITY_UPLOAD_KEY_MAX_LENGTH });
375
- if (pathError) {
376
- 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}`);
377
628
  }
378
- const sizeError = index$1.validateUploadTotalSize(files);
379
- if (sizeError) {
380
- throw new Error(sizeError);
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
+ }
646
+ }
647
+ catch (error) {
648
+ throw translateHeyboxDeployError(error, { projectRoot, stage: 'upload', version });
381
649
  }
382
- logger.debug(`待上传文件数: ${files.length}`);
383
- return files;
384
- }, { successText: 'dist 上传文件校验完成' });
650
+ }
385
651
  try {
386
- 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: '审核提交完成' });
387
659
  }
388
660
  catch (error) {
389
- throw translateHeyboxDeployError(error, { projectRoot, stage: 'upload', version });
661
+ throw translateHeyboxDeployError(error, { projectRoot, stage: 'submitAudit', version });
390
662
  }
391
- }
392
- try {
393
- const submitOptions = fromVersion
394
- ? { session: session$1, miniProgramId, ...historyArtifact, releaseNote, autoPublish }
395
- : { session: session$1, miniProgramId, manifest, releaseNote, autoPublish };
396
- submitAuditResult = await logger.task('正在提交审核', () => (runtime.submitUserMiniprogramAudit ?? submitUserMiniprogramAudit)(submitOptions, {
397
- baseUrl: apiBaseUrl,
398
- fetchImpl: runtime.fetchImpl,
399
- }), { successText: '审核提交完成' });
400
- }
401
- catch (error) {
402
- throw translateHeyboxDeployError(error, { projectRoot, stage: 'submitAudit', version });
403
- }
404
- printDeploySuccess({
405
- autoPublish,
406
- logger,
407
- miniProgramId,
408
- mode: options.successOutputMode ?? 'legacy',
409
- protocolString: submitAuditResult.protocol_string,
410
- version,
411
- });
412
- return {
413
- autoPublish,
414
- changed: true,
415
- miniProgramId,
416
- ...(submitAuditResult.protocol_string ? { protocolString: submitAuditResult.protocol_string } : {}),
417
- 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
+ };
418
678
  };
679
+ if (fromVersion) {
680
+ return deploy();
681
+ }
682
+ return (runtime.withDeployLock ?? withDeployLock)({ apiBaseUrl, miniProgramId, version: uploadVersion }, deploy);
419
683
  }
420
684
  function printDeploySuccess(options) {
421
685
  options.logger.success(`提交审核成功:${options.miniProgramId} ${options.version}`);
@@ -539,9 +803,7 @@ async function runBuildScript(pm, cwd, spawnImpl, options) {
539
803
  const pipeStdoutToStderr = options.outputMode === 'stderr';
540
804
  const buildArgs = createBuildArgs(pm, options.mode);
541
805
  const isWindows = options.platform === 'win32';
542
- const [executable, args] = isWindows
543
- ? createWindowsBuildCommand(pm, buildArgs)
544
- : [pm, buildArgs];
806
+ const [executable, args] = isWindows ? createWindowsBuildCommand(pm, buildArgs) : [pm, buildArgs];
545
807
  const child = spawnImpl(executable, args, {
546
808
  cwd,
547
809
  env: { ...process.env, ...options.env },
@@ -602,7 +864,7 @@ async function walkDistFiles(distDir) {
602
864
  }
603
865
  const fileStat = await fs$1.stat(absPath);
604
866
  results.push({
605
- relativePath: index$1.normalizeRelativePath(relPath),
867
+ relativePath: relPath,
606
868
  absolutePath: absPath,
607
869
  size: fileStat.size,
608
870
  mimeType: inferMimeType(entry.name),
@@ -613,6 +875,31 @@ async function walkDistFiles(distDir) {
613
875
  function inferMimeType(fileName) {
614
876
  return MIME_BY_EXT[path.extname(fileName).toLowerCase()] || 'application/octet-stream';
615
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
+ }
616
903
  function translateHeyboxDeployError(error, options) {
617
904
  const message = error instanceof Error && error.message ? error.message : String(error);
618
905
  if (isVersionLifecycleConflictMessage(message) && options.stage !== 'upload') {
@@ -627,18 +914,6 @@ function translateHeyboxDeployError(error, options) {
627
914
  `原始错误:${readVerboseDeployErrorMessage(error)}`,
628
915
  ].join('\n'));
629
916
  }
630
- if (isDuplicateUploadErrorMessage(message) && options.stage === 'upload') {
631
- const packageJsonPath = path.join(options.projectRoot, 'package.json');
632
- const nextVersion = suggestNextPatchVersion(options.version);
633
- return new index.CliError([
634
- `当前小程序版本 ${options.version} 的上传文件已存在,不能覆盖同版本发布产物。`,
635
- `请升级 ${packageJsonPath} 中的 version${nextVersion ? `(例如 ${nextVersion})` : ''},重新 build 后再 deploy。`,
636
- ].join('\n'), [
637
- `当前小程序版本 ${options.version} 的上传文件已存在,不能覆盖同版本发布产物。`,
638
- `请升级 ${packageJsonPath} 中的 version${nextVersion ? `(例如 ${nextVersion})` : ''},重新 build 后再 deploy。`,
639
- `原始错误:${readVerboseDeployErrorMessage(error)}`,
640
- ].join('\n'));
641
- }
642
917
  if (isAuthExpiredErrorMessage(message)) {
643
918
  if (options.stage === 'upload') {
644
919
  return new index.CliError([
@@ -654,9 +929,6 @@ function translateHeyboxDeployError(error, options) {
654
929
  }
655
930
  return error instanceof Error ? error : new Error(message);
656
931
  }
657
- function isDuplicateUploadErrorMessage(message) {
658
- return /重名|duplicate|already exists|exists/i.test(message);
659
- }
660
932
  function isVersionLifecycleConflictMessage(message) {
661
933
  return /AlreadyExists\(6\)|不能复用|已进入线上生命周期|正在审核|活跃候选版本|同版本/i.test(message);
662
934
  }