@heybox/hb-sdk 0.8.0 → 0.8.1-alpha.10

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 (69) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/README.md +80 -7
  3. package/dist/cli-chunks/{build-PYCNacya.cjs → build-CtqIXxDL.cjs} +16 -10
  4. package/dist/cli-chunks/{context-m2W2XbL0.cjs → context-t57bMfbr.cjs} +17 -3
  5. package/dist/cli-chunks/{create-BdAg3WGA.cjs → create-BDOPSacf.cjs} +1 -1
  6. package/dist/cli-chunks/{dev-CyZuw7Yn.cjs → dev-VxjconYc.cjs} +628 -147
  7. package/dist/cli-chunks/{doctor-DU8rCfUF.cjs → doctor-BkfYdlnZ.cjs} +1 -1
  8. package/dist/cli-chunks/{index-MMW2ibQm.cjs → index-DDrytLTX.cjs} +87 -15
  9. package/dist/cli-chunks/{index-DATObqzK.cjs → index-lLaHzwfT.cjs} +2 -2
  10. package/dist/cli-chunks/{index.esm-BiAaAUFC.cjs → index.esm-Dy4qRhuo.cjs} +9 -8
  11. package/dist/cli-chunks/{login-B3TThMss.cjs → login-D2EX4R5_.cjs} +2 -2
  12. package/dist/cli-chunks/{project-vite-BQj8YLI4.cjs → project-vite-CFhz2THK.cjs} +1 -1
  13. package/dist/cli-chunks/{remote-DNvI7tHH.cjs → remote-CfESZxO2.cjs} +41 -28
  14. package/dist/cli-chunks/{runtime-gate-BEFp1w_s.cjs → runtime-gate-BWb8QIcX.cjs} +0 -107
  15. package/dist/cli-chunks/{runtime-permission-env-CtL8rsjB.cjs → runtime-permission-env-BtTDFqEL.cjs} +304 -23
  16. package/dist/cli-chunks/{session-DjBkjaF8.cjs → session-Dkhwjqhm.cjs} +1 -1
  17. package/dist/cli-chunks/{skill-cR_wnaw2.cjs → skill-DOveC5jt.cjs} +83 -27
  18. package/dist/cli-chunks/{version-yEn1E2Bg.cjs → version-C4nE66sX.cjs} +1 -1
  19. package/dist/cli.cjs +1 -1
  20. package/dist/devtools/browser-dev-host/assets/browser-dev-host-BM9Qo_Ta.js +101 -0
  21. package/dist/devtools/browser-dev-host/assets/desktop-app-launch-C2I333Yn.js +6 -0
  22. package/dist/devtools/browser-dev-host/assets/index-Bnb6MMTv.js +567 -0
  23. package/dist/devtools/browser-dev-host/assets/index-D-aNERAr.css +1 -0
  24. package/dist/devtools/browser-dev-host/index.html +3 -3
  25. package/dist/index.cjs.js +1970 -1567
  26. package/dist/index.esm.js +1969 -1568
  27. package/dist/miniapp-publish.cjs.js +61 -0
  28. package/dist/miniapp-publish.esm.js +59 -1
  29. package/dist/protocol.cjs.js +64 -13
  30. package/dist/protocol.esm.js +64 -13
  31. package/dist/templates/vanilla-vite-js/README.md.ejs +1 -1
  32. package/dist/templates/vanilla-vite-js/package.json.ejs +1 -0
  33. package/dist/templates/vanilla-vite-js/vite.config.js +1 -1
  34. package/dist/vite.cjs.js +1333 -18
  35. package/dist/vite.esm.js +1334 -20
  36. package/package.json +9 -11
  37. package/skill/SKILL.md +11 -3
  38. package/skill/references/api-protocol.md +8 -4
  39. package/skill/references/api-root.md +328 -210
  40. package/skill/references/cli.md +11 -3
  41. package/skill/references/examples.md +13 -1
  42. package/skill/references/recipes.md +162 -0
  43. package/skill/references/safety-boundaries.md +9 -3
  44. package/skill/skill.json +5 -5
  45. package/types/core/client.d.ts +9 -1
  46. package/types/core/sdk.d.ts +6 -0
  47. package/types/core/singleton.d.ts +6 -0
  48. package/types/devtools/device-logs.d.ts +48 -0
  49. package/types/index.d.ts +5 -1
  50. package/types/miniapp-manifest/companion-directory.d.ts +2 -0
  51. package/types/miniapp-manifest/companion-executable.d.ts +4 -0
  52. package/types/miniapp-manifest/companion-types.d.ts +38 -0
  53. package/types/miniapp-manifest/companions.d.ts +17 -0
  54. package/types/miniapp-manifest/index.d.ts +2 -0
  55. package/types/miniapp-manifest/node.d.ts +9 -0
  56. package/types/miniapp-manifest/permissions.d.ts +1 -1
  57. package/types/miniapp-manifest/schema.d.ts +5 -1
  58. package/types/miniapp-publish/index.d.ts +17 -2
  59. package/types/modules/companion/index.d.ts +57 -0
  60. package/types/modules/environment/index.d.ts +70 -0
  61. package/types/modules/network/index.d.ts +1 -4
  62. package/types/modules/network/observability.d.ts +0 -1
  63. package/types/protocol/dev-session.d.ts +8 -1
  64. package/types/protocol.d.ts +1 -1
  65. package/types/vite/index.d.ts +11 -4
  66. package/dist/devtools/browser-dev-host/assets/browser-dev-host-TzYf9L6C.js +0 -99
  67. package/dist/devtools/browser-dev-host/assets/index-C5MZZDa5.js +0 -567
  68. package/dist/devtools/browser-dev-host/assets/index-P-ra4m1y.css +0 -1
  69. package/dist/devtools/browser-dev-host/assets/workbench-state-BwV7bm4n.js +0 -5
package/dist/vite.cjs.js CHANGED
@@ -1,14 +1,17 @@
1
1
  'use strict';
2
2
 
3
3
  var node_fs = require('node:fs');
4
+ var node_crypto = require('node:crypto');
5
+ var node_os = require('node:os');
4
6
  var path = require('node:path');
7
+ var node_zlib = require('node:zlib');
5
8
  var node_async_hooks = require('node:async_hooks');
6
9
 
7
10
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
8
11
  /** 构建时替换为当前发布包的实际版本。 */
9
12
  const HB_SDK_VERSION = typeof undefined === 'string'
10
13
  ? undefined
11
- : '0.8.0';
14
+ : '0.8.1-alpha.10';
12
15
 
13
16
  /**
14
17
  * iframe 与调试台页面的开发期 console 捕获转发。
@@ -28,11 +31,306 @@ const HB_SDK_VERSION = typeof undefined === 'string'
28
31
  * 调试台页面自身(runtime 跑在这里,`[hb-sdk-runtime]` 日志不经过 iframe)另用
29
32
  * `installMiniDevConsoleSelfCapture` 接入「日志」页签(source: 'host')。
30
33
  */
31
- const MINI_DEV_IFRAME_WINDOW_NAME = '__hb_sdk_mini_dev__';
32
34
  const MINI_DEV_CONSOLE_EVENT_TYPE = 'hb-sdk:mini-dev-console';
33
35
  /** 调试台页面与 iframe bootstrap 共用的已安装标记(window/console 上的属性名)。 */
34
36
  const MINI_DEV_CONSOLE_INSTALL_FLAG = '__hb_sdk_mini_dev_console_installed__';
35
37
 
38
+ /**
39
+ * network.request 诊断日志的纯脱敏工具。
40
+ *
41
+ * @remarks
42
+ * 放在 `core` 而非 `modules/network`:无错误类型依赖,供 SDK 错误文案、
43
+ * modules observability、devtools Browser Dev Host / proxy 共用,并满足 boundary
44
+ *(devtools 不得直接 import runtime capability modules)。
45
+ */
46
+ /** 敏感字段名:header / query / body key 共用。 */
47
+ const SENSITIVE_KEY_PATTERN = /(authorization|cookie|password|passwd|secret|token|pkey|private[_-]?key|access[_-]?key|client[_-]?secret|api[_-]?key|apikey|session|jwt|refresh[_-]?token|csrf)/i;
48
+ /**
49
+ * 脱敏请求 URL:去掉 userinfo,并将 query / fragment 中的敏感参数值替换为 `[redacted]`。
50
+ */
51
+ function sanitizeNetworkRequestUrl(url) {
52
+ if (!url) {
53
+ return url;
54
+ }
55
+ try {
56
+ const parsed = new URL(url);
57
+ parsed.username = '';
58
+ parsed.password = '';
59
+ redactUrlSearchParams(parsed.searchParams);
60
+ if (parsed.hash && parsed.hash.length > 1) {
61
+ const hashBody = parsed.hash.slice(1);
62
+ if (hashBody.includes('=')) {
63
+ const hashParams = new URLSearchParams(hashBody);
64
+ if (redactUrlSearchParams(hashParams)) {
65
+ parsed.hash = hashParams.toString();
66
+ }
67
+ }
68
+ }
69
+ return parsed.toString();
70
+ }
71
+ catch {
72
+ return redactSensitivePairsInRawText(url);
73
+ }
74
+ }
75
+ /**
76
+ * 诊断 message 中的 URL 片段做脱敏,避免 error.message 二次泄漏 query secrets。
77
+ */
78
+ function sanitizeDiagnosticMessage(message) {
79
+ if (!message) {
80
+ return message;
81
+ }
82
+ return message.replace(/https?:\/\/[^\s]+/gi, matched => sanitizeNetworkRequestUrl(matched.replace(/[),.;]+$/g, '')));
83
+ }
84
+ function isSensitiveNetworkKey(key) {
85
+ return SENSITIVE_KEY_PATTERN.test(key.trim());
86
+ }
87
+ function redactUrlSearchParams(params) {
88
+ let changed = false;
89
+ for (const key of [...params.keys()]) {
90
+ if (isSensitiveNetworkKey(key)) {
91
+ params.set(key, '[redacted]');
92
+ changed = true;
93
+ }
94
+ }
95
+ return changed;
96
+ }
97
+ function redactSensitivePairsInRawText(text) {
98
+ return text.replace(/([?&#]|^|[?&])([^=&#\s]+)=([^&#\s]*)/g, (full, prefix, key, value) => {
99
+ if (!isSensitiveNetworkKey(key)) {
100
+ return full;
101
+ }
102
+ return `${prefix}${key}=[redacted]`;
103
+ });
104
+ }
105
+
106
+ const DEVICE_LOG_PATH = '/__hb_sdk__/device-logs';
107
+ const registryKey = Symbol.for('@heybox/hb-sdk/device-logs');
108
+ const registry = globalThis;
109
+ const stores = (registry[registryKey] ??= new Map());
110
+ function registerDeviceLogs(port, store) {
111
+ stores.set(port, store);
112
+ return () => {
113
+ if (stores.get(port) === store)
114
+ stores.delete(port);
115
+ };
116
+ }
117
+ class DeviceLogStore {
118
+ sandboxRequests = new Map();
119
+ listSandbox(id, path) {
120
+ const device = this.devices.get(id);
121
+ if (!device || Date.now() - device.lastSeen > 15000)
122
+ return Promise.resolve({ error: '设备已离线' });
123
+ if (path !== '.' && (!path || path.length > 1024 || /[\\\\\u0000-\u001f]/.test(path) || path.split('/').some(part => !part || part === '.' || part === '..')))
124
+ return Promise.resolve({ error: '无效目录路径' });
125
+ if (this.sandboxRequests.size >= 64)
126
+ return Promise.resolve({ error: '查询繁忙,请稍后重试' });
127
+ const requestId = node_crypto.randomUUID();
128
+ return new Promise(resolve => {
129
+ const timer = setTimeout(() => { this.sandboxRequests.delete(requestId); resolve({ error: '设备查询超时' }); }, 10000);
130
+ this.sandboxRequests.set(requestId, { id, path, requestId, resolve: value => { clearTimeout(timer); this.sandboxRequests.delete(requestId); resolve(value); } });
131
+ });
132
+ }
133
+ uploadToken = node_crypto.randomBytes(32).toString('hex');
134
+ epoch = node_crypto.randomUUID();
135
+ expiresAt = Date.now() + 12 * 60 * 60 * 1000;
136
+ cursor = 0;
137
+ logs = [];
138
+ devices = new Map();
139
+ query(params) {
140
+ const after = Math.max(0, Number(params.get('after')) || 0);
141
+ const limit = Math.min(500, Math.max(1, Number(params.get('limit')) || 100));
142
+ const selected = this.logs.filter((log) => log.cursor > after &&
143
+ (!params.get('device') || log.deviceSessionId === params.get('device')) &&
144
+ (!params.get('level') || log.level === params.get('level')) &&
145
+ (!params.get('source') || log.source === params.get('source')) &&
146
+ (!params.get('callId') || log.callId === params.get('callId')) &&
147
+ (!params.get('id') || String(log.cursor) === params.get('id')) &&
148
+ (!params.get('since') || log.timestamp >= Number(params.get('since'))) &&
149
+ (!params.get('query') || log.message.toLowerCase().includes(params.get('query').toLowerCase())));
150
+ const items = selected.slice(0, limit);
151
+ return {
152
+ schemaVersion: 1,
153
+ epoch: this.epoch,
154
+ items,
155
+ nextCursor: selected.length > limit ? items.at(-1).cursor : this.cursor,
156
+ hasMore: selected.length > limit,
157
+ gap: after > 0 && after < (this.logs[0]?.cursor ?? 1) - 1,
158
+ devices: [...this.devices.values()].map(({ id, name, platform, connectedAt, lastSeen }) => ({
159
+ id,
160
+ name,
161
+ platform,
162
+ connectedAt,
163
+ lastSeen,
164
+ online: Date.now() - lastSeen < 15000,
165
+ })),
166
+ };
167
+ }
168
+ async upload(req, res) {
169
+ const reply = (status, value) => {
170
+ res.writeHead(status, { 'content-type': 'application/json', 'cache-control': 'no-store' });
171
+ res.end(JSON.stringify(value));
172
+ };
173
+ if (req.method !== 'POST') {
174
+ reply(405, {});
175
+ return;
176
+ }
177
+ if (Date.now() > this.expiresAt) {
178
+ reply(410, { code: 'EXPIRED' });
179
+ return;
180
+ }
181
+ let text = '';
182
+ try {
183
+ req.setEncoding('utf8');
184
+ for await (const chunk of req) {
185
+ text += chunk;
186
+ if (Buffer.byteLength(text) > 65536) {
187
+ reply(413, {});
188
+ return;
189
+ }
190
+ }
191
+ const body = JSON.parse(text);
192
+ if (body.register === true) {
193
+ if (req.headers['x-hb-log-token'] !== this.uploadToken) {
194
+ reply(403, {});
195
+ return;
196
+ }
197
+ const registrationId = typeof body.registrationId === 'string' && /^[a-f0-9]{32}$/.test(body.registrationId) ? body.registrationId : undefined;
198
+ const existing = registrationId && [...this.devices.values()].find(device => device.registrationId === registrationId);
199
+ if (existing) {
200
+ reply(200, { id: existing.id, token: existing.token });
201
+ return;
202
+ }
203
+ if (this.devices.size >= 64) {
204
+ reply(429, {});
205
+ return;
206
+ }
207
+ const id = node_crypto.randomUUID();
208
+ const platform = ['iOS', 'Android', 'Windows', 'macOS', 'Browser'].includes(body.platform) ? body.platform : 'Browser';
209
+ const device = {
210
+ id,
211
+ platform,
212
+ registrationId,
213
+ connectedAt: Date.now(),
214
+ name: `${platform} · ${this.devices.size + 1}`,
215
+ lastSeen: Date.now(),
216
+ sequence: 0,
217
+ token: node_crypto.randomBytes(32).toString('hex'),
218
+ windowStart: Date.now(),
219
+ count: 0,
220
+ };
221
+ this.devices.set(id, device);
222
+ reply(200, { id, token: device.token });
223
+ return;
224
+ }
225
+ const device = this.devices.get(body.id);
226
+ if (!device || req.headers['x-hb-log-token'] !== device.token) {
227
+ reply(403, {});
228
+ return;
229
+ }
230
+ if (!Array.isArray(body.logs) || body.logs.length > 50) {
231
+ reply(400, {});
232
+ return;
233
+ }
234
+ if (Date.now() - device.windowStart > 1000) {
235
+ device.windowStart = Date.now();
236
+ device.count = 0;
237
+ }
238
+ if (device.count + body.logs.length > 200) {
239
+ reply(429, {});
240
+ return;
241
+ }
242
+ device.count += body.logs.length;
243
+ device.lastSeen = Date.now();
244
+ if (body.sandboxResult && typeof body.sandboxResult === 'object') {
245
+ const result = body.sandboxResult;
246
+ const pending = this.sandboxRequests.get(result.requestId);
247
+ if (pending?.id === device.id) {
248
+ const entries = Array.isArray(result.entries) ? result.entries.slice(0, 200).filter((entry) => entry && ['file', 'directory'].includes(entry.kind) && typeof entry.name === 'string' && entry.name.length <= 255 && !/[\\\\/\u0000-\u001f]/.test(entry.name) && !['.', '..'].includes(entry.name)).map((entry) => ({ name: entry.name, kind: entry.kind })) : [];
249
+ pending.resolve({ deviceSessionId: device.id, requestId: pending.requestId, path: pending.path, timestamp: Date.now(), entries, error: result.error ? '设备不支持或无权读取此目录' : undefined, truncated: result.truncated === true });
250
+ }
251
+ }
252
+ for (const log of body.logs) {
253
+ if (!log || typeof log !== 'object')
254
+ continue;
255
+ if (!Number.isSafeInteger(log.sequence) ||
256
+ log.sequence <= device.sequence ||
257
+ !Number.isFinite(log.timestamp) ||
258
+ !['log', 'debug', 'info', 'warn', 'error'].includes(log.level) ||
259
+ typeof log.message !== 'string')
260
+ continue;
261
+ device.sequence = log.sequence;
262
+ this.logs.push({
263
+ cursor: ++this.cursor,
264
+ deviceSessionId: device.id,
265
+ sequence: log.sequence,
266
+ timestamp: log.timestamp,
267
+ receivedAt: Date.now(),
268
+ level: log.level,
269
+ source: log.source === 'sdk' ? 'sdk' : 'app',
270
+ message: sanitizeDiagnosticMessage(log.message)
271
+ .replace(/(authorization|cookie|password|secret|token|pkey|api[_-]?key)(["'\s:=]+)([^\s,}]+)/gi, '$1$2[redacted]')
272
+ .slice(0, 8192),
273
+ truncated: log.truncated === true || log.message.length > 8192,
274
+ ...(typeof log.method === 'string' ? { method: log.method.slice(0, 100) } : {}),
275
+ ...(typeof log.callId === 'string' ? { callId: log.callId.slice(0, 100) } : {}),
276
+ ...(typeof log.errorCode === 'string' && /^[A-Z0-9_]{1,100}$/.test(log.errorCode) ? { errorCode: log.errorCode } : {}),
277
+ ...(log.outcome === 'success' || log.outcome === 'error' ? { outcome: log.outcome } : {}),
278
+ ...(Number.isFinite(log.durationMs) ? { durationMs: Math.max(0, log.durationMs) } : {}),
279
+ });
280
+ }
281
+ if (this.logs.length > 5000)
282
+ this.logs.splice(0, this.logs.length - 5000);
283
+ reply(200, { sequence: device.sequence, sandboxRequest: [...this.sandboxRequests.values()].find(request => request.id === device.id) ? (() => { const request = [...this.sandboxRequests.values()].find(request => request.id === device.id); return { requestId: request.requestId, path: request.path }; })() : undefined });
284
+ }
285
+ catch {
286
+ reply(400, { code: 'INVALID_LOG_BATCH' });
287
+ }
288
+ }
289
+ }
290
+ /** 同源开发页面使用有界批量回传;传输失败不会递归写入 console。 */
291
+ function deviceLogBootstrap(token) {
292
+ return `<script>(function(){if(window.__hb_sdk_device_logs_installed__)return;
293
+ window.__hb_sdk_device_logs_installed__=true;
294
+ window.__hb_sdk_mini_dev_console_installed__=true;
295
+ var endpoint='${DEVICE_LOG_PATH}',credential='${token}',id='',queue=[],seq=0,busy=false,dropped=0;
296
+ var registrationId=Array.from(crypto.getRandomValues(new Uint8Array(16)),function(n){return n.toString(16).padStart(2,'0')}).join('');
297
+ var send=window.fetch.bind(window);
298
+ var sandboxResult;
299
+ function format(v){try{return typeof v==='string'?v:v instanceof Error?v.stack||v.message:JSON.stringify(v,function(k,value){return /authorization|cookie|password|secret|token|pkey|api.?key/i.test(k)?'[redacted]':value})}catch(e){return '[无法序列化]'}}function add(level,args,source){if(queue.length>=500){queue.shift();
300
+ dropped++}var message=args.map(format).join(' ');
301
+ message=message.replace(/https?:\\/\\/[^\\s]+/g,function(raw){try{var url=new URL(raw);
302
+ url.username='';
303
+ url.password='';
304
+ url.search='';
305
+ url.hash='';
306
+ return url.href}catch(e){return '[URL]'}});
307
+ message=message.replace(/(authorization|cookie|password|secret|token|pkey|api[_-]?key)([\\"'\\s:=]+)([^\\s,}]+)/gi,'$1$2[redacted]');
308
+ queue.push({truncated:message.length>8192,sequence:++seq,timestamp:Date.now(),level:level,source:source||'app',message:message.slice(0,8192)})}window.__hbSdkDeviceLog=function(event){add(event.outcome==='error'?'error':'info',[event.method+' · '+event.outcome+' · '+event.durationMs+' ms'],'sdk');
309
+ Object.assign(queue[queue.length-1],event)};
310
+ ['log','debug','info','warn','error'].forEach(function(level){var original=console[level];
311
+ console[level]=function(){add(level,Array.prototype.slice.call(arguments));
312
+ return original.apply(this,arguments)}});
313
+ window.addEventListener('error',function(e){add('error',[e.message])});
314
+ window.addEventListener('unhandledrejection',function(e){add('error',[e.reason])});
315
+ async function flush(){if(busy)return;
316
+ busy=true;
317
+ try{if(!id){var ua=navigator.userAgent;
318
+ var platform=window.name==='__hb_sdk_mini_dev__'?'Browser':/Android/i.test(ua)?'Android':/iPhone|iPad/i.test(ua)?'iOS':/Windows/i.test(ua)?'Windows':/Mac/i.test(ua)?'macOS':'Browser';
319
+ var r=await send(endpoint,{method:'POST',headers:{'content-type':'application/json','x-hb-log-token':credential},body:JSON.stringify({register:true,platform:platform,registrationId:registrationId}),signal:AbortSignal.timeout(5000)});
320
+ if(!r.ok)return;
321
+ var d=await r.json();
322
+ id=d.id;
323
+ credential=d.token}if(dropped){add('warn',['日志缓冲已丢弃 '+dropped+' 条']);
324
+ dropped=0}var batch=queue.slice(0,50);
325
+ while(batch.length>1&&new TextEncoder().encode(JSON.stringify(batch)).length>48000)batch.pop();
326
+ var result=await send(endpoint,{method:'POST',headers:{'content-type':'application/json','x-hb-log-token':credential},body:JSON.stringify({id:id,logs:batch,sandboxResult:sandboxResult}),signal:AbortSignal.timeout(5000)});
327
+ if(result.ok){var ack=await result.json();sandboxResult=undefined;
328
+ if(ack.sandboxRequest){try{var entries=await window.__hbSdkSandboxList(ack.sandboxRequest.path);sandboxResult={requestId:ack.sandboxRequest.requestId,entries:entries.slice(0,200),truncated:entries.length>200}}catch(e){sandboxResult={requestId:ack.sandboxRequest.requestId,error:true}}}
329
+ queue=queue.filter(function(l){return l.sequence>ack.sequence})}}catch(e){}finally{busy=false}}setInterval(flush,1000);
330
+ flush()})();
331
+ </script>`;
332
+ }
333
+
36
334
  var re = {exports: {}};
37
335
 
38
336
  var constants;
@@ -2821,6 +3119,8 @@ var semverExports = requireSemver();
2821
3119
  const MINI_PROGRAM_MESSAGE_NAMESPACE = 'heybox:miniprogram';
2822
3120
  const MINI_PROGRAM_MESSAGE_VERSION = 2;
2823
3121
  const MINI_PROGRAM_BRIDGE_NONCE_PARAM = 'hb_mini_bridge_nonce';
3122
+ /** Companion alias 的跨构建、Runtime 与 Host 规范。 */
3123
+ const COMPANION_ALIAS_PATTERN = /^[a-z][a-z0-9-]{0,31}$/;
2824
3124
 
2825
3125
  const MINI_PROGRAM_PERMISSION_KEYS = [
2826
3126
  'userInfo',
@@ -2831,6 +3131,7 @@ const MINI_PROGRAM_PERMISSION_KEYS = [
2831
3131
  'clipboard',
2832
3132
  'leaderboard',
2833
3133
  'network',
3134
+ 'companion',
2834
3135
  ];
2835
3136
  const MINI_PROGRAM_PERMISSION_CATALOG = [
2836
3137
  {
@@ -2943,6 +3244,29 @@ const MINI_PROGRAM_PERMISSION_CATALOG = [
2943
3244
  methods: ['network.request', 'network.download'],
2944
3245
  conditions: ['声明后仍需平台批准,并继续执行网络安全策略。'],
2945
3246
  },
3247
+ {
3248
+ key: 'companion',
3249
+ name: '桌面辅助应用',
3250
+ description: '下载、准备并运行随当前小程序版本发布的受管桌面程序。',
3251
+ access: 'platform-approval',
3252
+ risk: 'high',
3253
+ config: {},
3254
+ methods: [
3255
+ 'companion.getInfo',
3256
+ 'companion.getPreparationStatus',
3257
+ 'companion.prepare',
3258
+ 'companion.cancelPreparation',
3259
+ 'companion.getActiveSession',
3260
+ 'companion.launch',
3261
+ 'companion.session.write',
3262
+ 'companion.session.closeStdin',
3263
+ 'companion.session.terminate',
3264
+ 'companion.session.ackOutput',
3265
+ 'companion.openAttachment',
3266
+ 'companion.showNotification',
3267
+ ],
3268
+ conditions: ['声明后仍需平台批准,并继续执行可信用户手势、Host 能力和当前版本 Manifest 校验。'],
3269
+ },
2946
3270
  ];
2947
3271
  new Set(MINI_PROGRAM_PERMISSION_KEYS);
2948
3272
 
@@ -2957,7 +3281,7 @@ function parseMiniappPermissions(value, options) {
2957
3281
  }
2958
3282
  return { declared: false };
2959
3283
  }
2960
- if (!isRecord$1(value)) {
3284
+ if (!isRecord$2(value)) {
2961
3285
  throw new Error(`${sourceLabel} 必须是 JSON 对象`);
2962
3286
  }
2963
3287
  const knownKeys = new Set(MINIAPP_PERMISSION_KEYS);
@@ -2971,7 +3295,7 @@ function parseMiniappPermissions(value, options) {
2971
3295
  continue;
2972
3296
  const declaration = value[key];
2973
3297
  const itemLabel = `${sourceLabel}.${key}`;
2974
- if (!isRecord$1(declaration))
3298
+ if (!isRecord$2(declaration))
2975
3299
  throw new Error(`${itemLabel} 必须是 JSON 对象`);
2976
3300
  const definition = MINI_PROGRAM_PERMISSION_CATALOG.find((item) => item.key === key);
2977
3301
  if (!definition)
@@ -3005,7 +3329,7 @@ function requiresExplicitPermissions(sdkVersion) {
3005
3329
  function getMissingPermissionsWarning(sourceLabel = 'package.json#heybox.permissions') {
3006
3330
  return `${sourceLabel} 尚未声明。当前按 0.8 兼容行为构建;0.9.0 起将拒绝构建,请尽快补充权限声明(无权限需求时配置 {})。`;
3007
3331
  }
3008
- function isRecord$1(value) {
3332
+ function isRecord$2(value) {
3009
3333
  return typeof value === 'object' && value !== null && !Array.isArray(value);
3010
3334
  }
3011
3335
 
@@ -3027,6 +3351,7 @@ function renderMiniappManifest(manifest) {
3027
3351
  sdkVersion: manifest.sdkVersion,
3028
3352
  platforms,
3029
3353
  ...(manifest.permissions === undefined ? {} : { permissions: manifest.permissions }),
3354
+ ...(manifest.companions === undefined ? {} : { companions: manifest.companions }),
3030
3355
  };
3031
3356
  return `${JSON.stringify(output, null, 2)}\n`;
3032
3357
  }
@@ -3085,14 +3410,30 @@ function findNearestPackageJsonPath(startDir) {
3085
3410
  function readMiniappVersionFromPackageJson(root) {
3086
3411
  return readMiniappPackageJson(root, true).version;
3087
3412
  }
3413
+ /** Companion source 始终相对声明所在 package.json,而非 Vite 的 HTML root。 */
3414
+ function readMiniappCompanionsFromPackageJson(root) {
3415
+ const { packageJson, packageJsonPath } = readMiniappPackageJson(root, false);
3416
+ const heybox = readMiniappHeyboxConfig(packageJson);
3417
+ const declarations = heybox?.companions;
3418
+ if (declarations !== undefined && (!isRecord$1(declarations) || Object.keys(declarations).length === 0)) {
3419
+ throw new Error('package.json#heybox.companions 必须是非空对象');
3420
+ }
3421
+ return {
3422
+ declarations: declarations,
3423
+ projectRoot: path.dirname(packageJsonPath),
3424
+ packageJsonPath,
3425
+ };
3426
+ }
3427
+ function readMiniappPlatformsFromPackageJson(root) {
3428
+ const { packageJson } = readMiniappPackageJson(root, false);
3429
+ const heybox = readMiniappHeyboxConfig(packageJson);
3430
+ return validateMiniappPlatforms(heybox?.platforms, 'package.json#heybox.platforms');
3431
+ }
3088
3432
  function readMiniappPermissionsFromPackageJson(root, sdkVersion) {
3089
3433
  const { packageJson } = readMiniappPackageJson(root, false);
3090
- const heybox = packageJson.heybox;
3091
- if (heybox !== undefined && !isRecord(heybox)) {
3092
- throw new Error('@heybox/hb-sdk 提示:package.json#heybox 必须是 JSON 对象');
3093
- }
3094
- const present = isRecord(heybox) && Object.prototype.hasOwnProperty.call(heybox, 'permissions');
3095
- return parseMiniappPermissions(isRecord(heybox) ? heybox.permissions : undefined, {
3434
+ const heybox = readMiniappHeyboxConfig(packageJson);
3435
+ const present = heybox !== undefined && Object.prototype.hasOwnProperty.call(heybox, 'permissions');
3436
+ return parseMiniappPermissions(heybox?.permissions, {
3096
3437
  present,
3097
3438
  sdkVersion,
3098
3439
  });
@@ -3116,9 +3457,17 @@ function readMiniappPackageJson(root, requireVersion) {
3116
3457
  if (requireVersion && (typeof packageJson.version !== 'string' || packageJson.version.trim() === '')) {
3117
3458
  throw new Error('@heybox/hb-sdk 提示:package.json.version 必须是非空字符串');
3118
3459
  }
3119
- return { packageJson, version: typeof packageJson.version === 'string' ? packageJson.version.trim() : '' };
3460
+ return { packageJson, packageJsonPath, version: typeof packageJson.version === 'string' ? packageJson.version.trim() : '' };
3120
3461
  }
3121
- function isRecord(value) {
3462
+ function readMiniappHeyboxConfig(packageJson) {
3463
+ if (packageJson.heybox === undefined)
3464
+ return undefined;
3465
+ if (!isRecord$1(packageJson.heybox)) {
3466
+ throw new Error('@heybox/hb-sdk 提示:package.json#heybox 必须是 JSON 对象');
3467
+ }
3468
+ return packageJson.heybox;
3469
+ }
3470
+ function isRecord$1(value) {
3122
3471
  return typeof value === 'object' && value !== null && !Array.isArray(value);
3123
3472
  }
3124
3473
  function formatReason(error) {
@@ -3128,6 +3477,646 @@ function formatReason(error) {
3128
3477
  return String(error);
3129
3478
  }
3130
3479
 
3480
+ /** 检查原生入口的文件格式与目标架构;文件后缀和执行位不能代替此门禁。 */
3481
+ function assertCompanionExecutable(bytes, target) {
3482
+ const fail = () => { throw new Error('Companion 入口必须是目标架构的原生可执行文件,禁止脚本或损坏的二进制'); };
3483
+ if (target === 'windows-x64') {
3484
+ if (bytes.length < 64 || bytes.readUInt16LE(0) !== 0x5a4d)
3485
+ fail();
3486
+ const pe = bytes.readUInt32LE(60);
3487
+ if (pe < 64 || pe > bytes.length - 24 || bytes.readUInt32LE(pe) !== 0x00004550
3488
+ || bytes.readUInt16LE(pe + 4) !== 0x8664)
3489
+ fail();
3490
+ const optionalSize = bytes.readUInt16LE(pe + 20);
3491
+ const characteristics = bytes.readUInt16LE(pe + 22);
3492
+ if (optionalSize < 112 || pe + 24 + optionalSize > bytes.length
3493
+ || bytes.readUInt16LE(pe + 24) !== 0x20b || !(characteristics & 2) || (characteristics & 0x2000))
3494
+ fail();
3495
+ return;
3496
+ }
3497
+ const thin = (slice) => {
3498
+ if (slice.length < 32)
3499
+ return false;
3500
+ const magic = slice.readUInt32LE(0);
3501
+ if (magic !== 0xfeedfacf && magic !== 0xcffaedfe)
3502
+ return false;
3503
+ const read = (offset) => magic === 0xfeedfacf ? slice.readUInt32LE(offset) : slice.readUInt32BE(offset);
3504
+ return read(4) === 0x0100000c && read(12) === 2 && read(20) <= slice.length - 32;
3505
+ };
3506
+ if (thin(bytes))
3507
+ return;
3508
+ if (bytes.length < 8)
3509
+ fail();
3510
+ const magic = bytes.readUInt32BE(0);
3511
+ if (![0xcafebabe, 0xbebafeca, 0xcafebabf, 0xbfbafeca].includes(magic))
3512
+ fail();
3513
+ const little = magic === 0xbebafeca || magic === 0xbfbafeca;
3514
+ const wide = magic === 0xcafebabf || magic === 0xbfbafeca;
3515
+ const read = (offset) => little ? bytes.readUInt32LE(offset) : bytes.readUInt32BE(offset);
3516
+ const count = read(4);
3517
+ const stride = wide ? 32 : 20;
3518
+ if (!count || count > 64 || 8 + count * stride > bytes.length)
3519
+ fail();
3520
+ let arm64 = false;
3521
+ const ranges = [];
3522
+ for (let i = 0; i < count; i++) {
3523
+ const record = 8 + i * stride;
3524
+ const integer = (offset) => {
3525
+ if (!wide)
3526
+ return read(offset);
3527
+ const value = little ? bytes.readBigUInt64LE(offset) : bytes.readBigUInt64BE(offset);
3528
+ return value <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(value) : -1;
3529
+ };
3530
+ const offset = integer(record + 8);
3531
+ const size = integer(record + (wide ? 16 : 12));
3532
+ if (offset < 8 + count * stride || size < 1 || offset > bytes.length - size
3533
+ || ranges.some(([start, end]) => offset < end && offset + size > start))
3534
+ fail();
3535
+ ranges.push([offset, offset + size]);
3536
+ if (read(record) === 0x0100000c) {
3537
+ if (!thin(bytes.subarray(offset, offset + size)))
3538
+ fail();
3539
+ arm64 = true;
3540
+ }
3541
+ }
3542
+ if (!arm64)
3543
+ fail();
3544
+ }
3545
+ /** 只解析 bundle 主字典的 CFBundleExecutable,兼容 XML 与 binary plist。 */
3546
+ function companionBundleExecutable(plist) {
3547
+ let name;
3548
+ if (plist.subarray(0, 8).toString() === 'bplist00') {
3549
+ if (plist.length < 40)
3550
+ throw new Error('Companion Info.plist 损坏');
3551
+ const trailer = plist.length - 32;
3552
+ const offsetSize = plist[trailer + 6];
3553
+ const refSize = plist[trailer + 7];
3554
+ const integer = (offset, size) => {
3555
+ if (size < 1 || size > 8 || offset < 0 || offset + size > plist.length)
3556
+ throw new Error('Companion Info.plist 损坏');
3557
+ let value = 0;
3558
+ for (let i = 0; i < size; i++)
3559
+ value = value * 256 + plist[offset + i];
3560
+ if (!Number.isSafeInteger(value))
3561
+ throw new Error('Companion Info.plist 过大');
3562
+ return value;
3563
+ };
3564
+ const u64 = (offset) => {
3565
+ const value = plist.readBigUInt64BE(offset);
3566
+ if (value > BigInt(Number.MAX_SAFE_INTEGER))
3567
+ throw new Error('Companion Info.plist 过大');
3568
+ return Number(value);
3569
+ };
3570
+ const count = u64(trailer + 8), top = u64(trailer + 16), table = u64(trailer + 24);
3571
+ if (count > 10000 || top >= count || table < 8 || table + count * offsetSize > trailer)
3572
+ throw new Error('Companion Info.plist 损坏');
3573
+ const object = (ref) => {
3574
+ if (ref >= count)
3575
+ throw new Error('Companion Info.plist 损坏');
3576
+ const offset = integer(table + ref * offsetSize, offsetSize);
3577
+ if (offset < 8 || offset >= table)
3578
+ throw new Error('Companion Info.plist 损坏');
3579
+ const type = plist[offset] >> 4;
3580
+ let length = plist[offset] & 15, data = offset + 1;
3581
+ if (length === 15) {
3582
+ const marker = plist[data++];
3583
+ if ((marker >> 4) !== 1)
3584
+ throw new Error('Companion Info.plist 损坏');
3585
+ const size = 2 ** (marker & 15);
3586
+ length = integer(data, size);
3587
+ data += size;
3588
+ }
3589
+ if (length > 10000 || data + length * (type === 13 ? refSize * 2 : type === 6 ? 2 : 1) > table)
3590
+ throw new Error('Companion Info.plist 损坏');
3591
+ return { type, length, data };
3592
+ };
3593
+ const string = (ref) => {
3594
+ const item = object(ref);
3595
+ if (item.type === 5)
3596
+ return plist.subarray(item.data, item.data + item.length).toString('ascii');
3597
+ if (item.type === 6)
3598
+ return Buffer.from(plist.subarray(item.data, item.data + item.length * 2)).swap16().toString('utf16le');
3599
+ return undefined;
3600
+ };
3601
+ const root = object(top);
3602
+ if (root.type !== 13)
3603
+ throw new Error('Companion Info.plist 必须是字典');
3604
+ for (let i = 0; i < root.length; i++) {
3605
+ if (string(integer(root.data + i * refSize, refSize)) === 'CFBundleExecutable') {
3606
+ if (name !== undefined)
3607
+ throw new Error('Companion Info.plist 含重复入口');
3608
+ name = string(integer(root.data + (root.length + i) * refSize, refSize));
3609
+ }
3610
+ }
3611
+ }
3612
+ else {
3613
+ const xml = plist.toString('utf8').replace(/<!--[\s\S]*?-->/g, '');
3614
+ const matches = [...xml.matchAll(/<key>\s*CFBundleExecutable\s*<\/key>\s*<string>([^<]*)<\/string>/g)];
3615
+ if (matches.length === 1)
3616
+ name = matches[0][1];
3617
+ }
3618
+ if (typeof name !== 'string' || !/^[^/\\\u0000-\u001f\u007f&]{1,255}$/.test(name) || name === '.' || name === '..')
3619
+ throw new Error('Companion Info.plist 缺少安全的 CFBundleExecutable');
3620
+ return name;
3621
+ }
3622
+
3623
+ const MAX_BYTES = 2 * 1024 * 1024 * 1024;
3624
+ const MAX_ENTRIES = 10_000;
3625
+ /** 双次读取校验输入稳定性;保留 .app 安全链接以避免破坏签名。 */
3626
+ function packCompanionDirectory(root, label) {
3627
+ root = node_fs.realpathSync(root);
3628
+ const first = snapshot(root, label);
3629
+ let second;
3630
+ try {
3631
+ second = snapshot(root, label);
3632
+ }
3633
+ catch (cause) {
3634
+ throw new Error(`${label}.source 产物读取期间发生变化,请等待编译完成后重试`, { cause });
3635
+ }
3636
+ if (first.fingerprint !== second.fingerprint)
3637
+ throw new Error(`${label}.source 产物读取期间发生变化,请等待编译完成后重试`);
3638
+ return zip(first.entries);
3639
+ }
3640
+ function snapshot(root, label) {
3641
+ const entries = [];
3642
+ const hash = node_crypto.createHash('sha256');
3643
+ let total = 0;
3644
+ const inside = (candidate) => {
3645
+ const relative = path.relative(root, candidate);
3646
+ return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
3647
+ };
3648
+ const checked = new Set();
3649
+ function checkCycles(diskPath, active) {
3650
+ if (active.size > 128)
3651
+ throw new Error(`${label}.source 目录层级过深`);
3652
+ const real = node_fs.realpathSync(diskPath);
3653
+ if (!inside(real))
3654
+ throw new Error(`${label}.source 符号链接逃逸产物目录`);
3655
+ if (active.has(real))
3656
+ throw new Error(`${label}.source 包含循环符号链接`);
3657
+ if (checked.has(real))
3658
+ return;
3659
+ if (checked.size >= MAX_ENTRIES)
3660
+ throw new Error(`${label}.source 文件数超限`);
3661
+ if (node_fs.lstatSync(real).isDirectory()) {
3662
+ const next = new Set(active).add(real);
3663
+ for (const child of node_fs.readdirSync(real).sort())
3664
+ checkCycles(path.join(real, child), next);
3665
+ }
3666
+ checked.add(real);
3667
+ }
3668
+ checkCycles(root, new Set());
3669
+ function visit(diskPath, name, ancestors) {
3670
+ const stat = node_fs.lstatSync(diskPath);
3671
+ hash.update(JSON.stringify([name, stat.dev, stat.ino, stat.size, stat.mtimeMs, stat.ctimeMs, stat.mode]));
3672
+ if (stat.isSymbolicLink()) {
3673
+ const link = node_fs.readlinkSync(diskPath);
3674
+ const parts = name.split('/');
3675
+ const bundleIndex = parts.findIndex(segment => segment.endsWith('.app'));
3676
+ if (bundleIndex < 0 || !link || /[\u0000-\u001f\u007f]/.test(link) || path.isAbsolute(link) || link.includes('\\') || /^[A-Za-z]:/.test(link)) {
3677
+ throw new Error(`${label}.source 仅允许 .app 内部的相对符号链接`);
3678
+ }
3679
+ const resolvedLink = node_fs.realpathSync(diskPath);
3680
+ const bundle = path.join(root, ...parts.slice(0, bundleIndex + 1));
3681
+ const lexicalLink = path.relative(bundle, path.resolve(path.dirname(diskPath), link));
3682
+ const relativeLink = path.relative(bundle, resolvedLink);
3683
+ if ([lexicalLink, relativeLink].some(relative => relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)))
3684
+ throw new Error(`${label}.source 符号链接逃逸产物目录`);
3685
+ if (ancestors.has(resolvedLink))
3686
+ throw new Error(`${label}.source 包含循环符号链接`);
3687
+ const targetStat = node_fs.lstatSync(resolvedLink);
3688
+ if (!targetStat.isDirectory() && !targetStat.isFile())
3689
+ throw new Error(`${label}.source 不允许特殊文件`);
3690
+ if (entries.length >= MAX_ENTRIES)
3691
+ throw new Error(`${label}.source 文件数超限`);
3692
+ hash.update(link);
3693
+ entries.push({ name, bytes: Buffer.from(link), directory: false, executable: false, symlink: true });
3694
+ return;
3695
+ }
3696
+ const real = node_fs.realpathSync(diskPath);
3697
+ if (!inside(real))
3698
+ throw new Error(`${label}.source 符号链接逃逸产物目录`);
3699
+ if (ancestors.has(real))
3700
+ throw new Error(`${label}.source 包含循环符号链接`);
3701
+ const resolved = node_fs.lstatSync(real);
3702
+ if (!resolved.isDirectory() && !resolved.isFile())
3703
+ throw new Error(`${label}.source 不允许特殊文件`);
3704
+ if (name) {
3705
+ if (entries.length >= MAX_ENTRIES)
3706
+ throw new Error(`${label}.source 文件数超限`);
3707
+ if (resolved.isFile() && (total += resolved.size) > MAX_BYTES)
3708
+ throw new Error(`${label}.source 文件大小预算超限`);
3709
+ const bytes = resolved.isFile() ? node_fs.readFileSync(real) : Buffer.alloc(0);
3710
+ hash.update(bytes);
3711
+ entries.push({ name, bytes, directory: resolved.isDirectory(), executable: (resolved.mode & 0o111) !== 0 });
3712
+ }
3713
+ if (resolved.isDirectory()) {
3714
+ const next = new Set(ancestors).add(real);
3715
+ for (const child of node_fs.readdirSync(real).sort())
3716
+ visit(path.join(real, child), name ? `${name}/${child}` : child, next);
3717
+ }
3718
+ }
3719
+ visit(root, '', new Set());
3720
+ return { entries, fingerprint: hash.digest('hex') };
3721
+ }
3722
+ const CRC_TABLE = Array.from({ length: 256 }, (_, value) => {
3723
+ let crc = value;
3724
+ for (let bit = 0; bit < 8; bit++)
3725
+ crc = (crc >>> 1) ^ ((crc & 1) ? 0xedb88320 : 0);
3726
+ return crc >>> 0;
3727
+ });
3728
+ function crc32(bytes) {
3729
+ let crc = 0xffffffff;
3730
+ for (const byte of bytes) {
3731
+ crc = (crc >>> 8) ^ CRC_TABLE[(crc ^ byte) & 0xff];
3732
+ }
3733
+ return (crc ^ 0xffffffff) >>> 0;
3734
+ }
3735
+ /** 使用 ZIP store 避免不同压缩器版本改变摘要,也避免高压缩率资源触发解压预算门禁。 */
3736
+ function zip(entries) {
3737
+ const locals = [];
3738
+ const records = [];
3739
+ let offset = 0;
3740
+ for (const entry of entries) {
3741
+ const name = Buffer.from(entry.name + (entry.directory ? '/' : ''));
3742
+ if (name.length > 65535)
3743
+ throw new Error('Companion 产物路径过长');
3744
+ const crc = crc32(entry.bytes);
3745
+ const local = Buffer.alloc(30 + name.length);
3746
+ local.writeUInt32LE(0x04034b50);
3747
+ local.writeUInt16LE(20, 4);
3748
+ local.writeUInt16LE(0x800, 6);
3749
+ local.writeUInt16LE(33, 12);
3750
+ local.writeUInt32LE(crc, 14);
3751
+ local.writeUInt32LE(entry.bytes.length, 18);
3752
+ local.writeUInt32LE(entry.bytes.length, 22);
3753
+ local.writeUInt16LE(name.length, 26);
3754
+ name.copy(local, 30);
3755
+ const record = Buffer.alloc(46 + name.length);
3756
+ record.writeUInt32LE(0x02014b50);
3757
+ record.writeUInt16LE(0x314, 4);
3758
+ record.writeUInt16LE(20, 6);
3759
+ record.writeUInt16LE(0x800, 8);
3760
+ record.writeUInt16LE(33, 14);
3761
+ record.writeUInt32LE(crc, 16);
3762
+ record.writeUInt32LE(entry.bytes.length, 20);
3763
+ record.writeUInt32LE(entry.bytes.length, 24);
3764
+ record.writeUInt16LE(name.length, 28);
3765
+ const mode = entry.symlink ? 0o120777 : entry.directory ? 0o040755 : entry.executable ? 0o100755 : 0o100644;
3766
+ record.writeUInt32LE((mode * 65536) >>> 0, 38);
3767
+ record.writeUInt32LE(offset, 42);
3768
+ name.copy(record, 46);
3769
+ locals.push(local, entry.bytes);
3770
+ records.push(record);
3771
+ offset += local.length + entry.bytes.length;
3772
+ }
3773
+ const central = Buffer.concat(records);
3774
+ const end = Buffer.alloc(22);
3775
+ end.writeUInt32LE(0x06054b50);
3776
+ end.writeUInt16LE(entries.length, 8);
3777
+ end.writeUInt16LE(entries.length, 10);
3778
+ end.writeUInt32LE(central.length, 12);
3779
+ end.writeUInt32LE(offset, 16);
3780
+ return Buffer.concat([...locals, central, end]);
3781
+ }
3782
+
3783
+ const MINIAPP_COMPANION_TARGETS = ['windows-x64', 'macos-arm64'];
3784
+ const MINIAPP_COMPANION_CAPABILITIES = ['stdio', 'attachments', 'notifications'];
3785
+
3786
+ const MAX_ENTRY_COUNT = 10_000;
3787
+ const MAX_UNCOMPRESSED_BYTES = 2 * 1024 * 1024 * 1024;
3788
+ const MAX_COMPRESSION_RATIO = 200;
3789
+ const MAX_DISPLAY_NAME_CODE_POINTS = 120;
3790
+ const MAX_ARG_COUNT = 64;
3791
+ const MAX_ARG_BYTES = 1024;
3792
+ const MAX_ARGS_TOTAL_BYTES = 16_384;
3793
+ function prepareMiniappCompanions(declarations, options) {
3794
+ if (declarations === undefined)
3795
+ return undefined;
3796
+ if (!isRecord(declarations) || Object.keys(declarations).length === 0) {
3797
+ throw new Error('package.json#heybox.companions 必须是非空对象');
3798
+ }
3799
+ const output = {};
3800
+ for (const alias of Object.keys(declarations).sort()) {
3801
+ if (!COMPANION_ALIAS_PATTERN.test(alias))
3802
+ throw new Error(`Companion alias 不合法:${alias}`);
3803
+ const definition = declarations[alias];
3804
+ assertExactKeys(definition, ['displayName', 'capabilities', 'targets'], `companions.${alias}`);
3805
+ if (typeof definition.displayName !== 'string' || !definition.displayName.trim()) {
3806
+ throw new Error(`companions.${alias}.displayName 必须是非空字符串`);
3807
+ }
3808
+ if (Array.from(definition.displayName.trim()).length > MAX_DISPLAY_NAME_CODE_POINTS) {
3809
+ throw new Error(`companions.${alias}.displayName 最多允许 ${MAX_DISPLAY_NAME_CODE_POINTS} 个 Unicode 字符`);
3810
+ }
3811
+ const capabilities = validateCapabilities(definition.capabilities, alias);
3812
+ if (!isRecord(definition.targets) || Object.keys(definition.targets).length === 0) {
3813
+ throw new Error(`companions.${alias}.targets 必须是非空对象`);
3814
+ }
3815
+ const targets = {};
3816
+ for (const targetName of Object.keys(definition.targets).sort()) {
3817
+ if (!MINIAPP_COMPANION_TARGETS.includes(targetName)) {
3818
+ throw new Error(`companions.${alias}.targets 包含不支持的平台:${targetName}`);
3819
+ }
3820
+ const platform = targetName.startsWith('windows-') ? 'windows' : 'macos';
3821
+ if (options.platforms && !options.platforms.includes(platform)) {
3822
+ throw new Error(`companions.${alias}.targets.${targetName} 未包含在 package.json#heybox.platforms 中`);
3823
+ }
3824
+ const target = definition.targets[targetName];
3825
+ if (!target)
3826
+ continue;
3827
+ if (options.targets && !options.targets.includes(targetName))
3828
+ continue;
3829
+ targets[targetName] = prepareTarget(alias, targetName, target, options);
3830
+ }
3831
+ if (!options.targets || Object.keys(targets).length)
3832
+ output[alias] = { displayName: definition.displayName.trim(), capabilities, targets };
3833
+ }
3834
+ return options.targets && Object.keys(output).length === 0 ? undefined : output;
3835
+ }
3836
+ function prepareTarget(alias, targetName, target, options) {
3837
+ const label = `companions.${alias}.targets.${targetName}`;
3838
+ assertExactKeys(target, ['source', 'entrypoint', 'elevation', 'args'], label);
3839
+ if (typeof target.source !== 'string' || !target.source.trim())
3840
+ throw new Error(`${label}.source 必须是非空字符串`);
3841
+ if (!isRecord(target.entrypoint))
3842
+ throw new Error(`${label}.entrypoint 必须是对象`);
3843
+ assertExactKeys(target.entrypoint, ['kind', 'path'], `${label}.entrypoint`);
3844
+ const entrypointPath = validateArchivePath(target.entrypoint.path, `${label}.entrypoint.path`);
3845
+ validateEntrypoint(targetName, target.entrypoint.kind, entrypointPath, label);
3846
+ const elevation = target.elevation ?? 'none';
3847
+ if (elevation !== 'none')
3848
+ throw new Error(`${label}.elevation V1 仅支持 none,暂不支持管理员权限启动`);
3849
+ const args = validateArgs(target.args, label);
3850
+ const projectRoot = node_fs.realpathSync(options.root);
3851
+ const outputRoot = resolveRealPathIfPresent(options.outputRoot);
3852
+ const sourcePath = path.resolve(projectRoot, target.source);
3853
+ if (!isPathInside(projectRoot, sourcePath))
3854
+ throw new Error(`${label}.source 必须位于项目目录内`);
3855
+ if (isPathInside(outputRoot, sourcePath, true))
3856
+ throw new Error(`${label}.source 不能位于构建输出目录内`);
3857
+ const sourceStat = node_fs.lstatSync(sourcePath);
3858
+ if (sourceStat.isFile())
3859
+ throw new Error(`${label}.source 必须是产物目录;请将旧 ZIP 解压到 companion 目录并修改 source,SDK 会自动打包`);
3860
+ if (sourceStat.isSymbolicLink() || !sourceStat.isDirectory())
3861
+ throw new Error(`${label}.source 必须是普通产物目录`);
3862
+ const realSourcePath = node_fs.realpathSync(sourcePath);
3863
+ if (!isPathInside(projectRoot, realSourcePath))
3864
+ throw new Error(`${label}.source 不能通过符号链接逃逸项目目录`);
3865
+ if (isPathInside(outputRoot, realSourcePath, true))
3866
+ throw new Error(`${label}.source 不能位于构建输出目录内`);
3867
+ if (isPathInside(realSourcePath, outputRoot, true))
3868
+ throw new Error(`${label}.source 不能包含构建输出目录`);
3869
+ const archive = packCompanionDirectory(realSourcePath, label);
3870
+ const entries = inspectZipArchive(archive, label);
3871
+ assertEntrypointExists(entries, target.entrypoint.kind, entrypointPath, label);
3872
+ const readEntry = (name, visited = new Set()) => {
3873
+ if (visited.has(name))
3874
+ throw new Error(`${label} 包含循环符号链接`);
3875
+ visited.add(name);
3876
+ const segments = name.split('/');
3877
+ for (let end = 1; end <= segments.length; end++) {
3878
+ const prefix = segments.slice(0, end).join('/');
3879
+ const link = entries.find(item => item.name === prefix && item.symlink);
3880
+ if (link) {
3881
+ const target = archive.subarray(link.dataStart, link.localEnd).toString('utf8');
3882
+ const resolved = path.posix.join(path.posix.dirname(prefix), target, ...segments.slice(end));
3883
+ validateArchivePath(resolved, `${label} 符号链接目标`);
3884
+ return readEntry(resolved, visited);
3885
+ }
3886
+ }
3887
+ const entry = entries.find((item) => item.name === name && !item.directory);
3888
+ if (!entry)
3889
+ throw new Error(`${label} 的入口文件在 ZIP 中不存在`);
3890
+ const compressed = archive.subarray(entry.dataStart, entry.localEnd);
3891
+ const bytes = entry.compressionMethod === 0 ? compressed
3892
+ : entry.compressionMethod === 8 ? node_zlib.inflateRawSync(compressed, { maxOutputLength: Math.max(1, entry.uncompressedSize) })
3893
+ : undefined;
3894
+ if (!bytes || bytes.length !== entry.uncompressedSize)
3895
+ throw new Error(`${label} 的入口 ZIP 数据损坏或压缩格式不支持`);
3896
+ return bytes;
3897
+ };
3898
+ const executablePath = target.entrypoint.kind === 'executable' ? entrypointPath
3899
+ : `${entrypointPath}/Contents/MacOS/${companionBundleExecutable(readEntry(`${entrypointPath}/Contents/Info.plist`))}`;
3900
+ assertCompanionExecutable(readEntry(executablePath), targetName);
3901
+ const relativePath = `companions/${alias}/${targetName}.zip`;
3902
+ const destination = path.join(options.outputRoot, ...relativePath.split('/'));
3903
+ if (options.copyArtifacts !== false) {
3904
+ node_fs.mkdirSync(path.dirname(destination), { recursive: true });
3905
+ const temporaryDestination = path.join(path.dirname(destination), `.${path.basename(destination)}.${node_crypto.randomUUID()}.tmp`);
3906
+ try {
3907
+ node_fs.writeFileSync(temporaryDestination, archive, { flag: 'wx' });
3908
+ node_fs.renameSync(temporaryDestination, destination);
3909
+ }
3910
+ finally {
3911
+ node_fs.rmSync(temporaryDestination, { force: true });
3912
+ }
3913
+ }
3914
+ const size = archive.length;
3915
+ options.onPreparedArtifact?.({ archive, path: relativePath, size, sourcePath: realSourcePath });
3916
+ return {
3917
+ path: relativePath,
3918
+ size,
3919
+ sha256: node_crypto.createHash('sha256').update(archive).digest('hex'),
3920
+ entrypoint: { kind: target.entrypoint.kind, path: entrypointPath },
3921
+ elevation,
3922
+ args,
3923
+ };
3924
+ }
3925
+ function inspectZipArchive(archive, label) {
3926
+ const eocd = findEndOfCentralDirectory(archive);
3927
+ const entryCount = archive.readUInt16LE(eocd + 10);
3928
+ if (entryCount !== archive.readUInt16LE(eocd + 8))
3929
+ throw new Error(`${label}.source ZIP 条目数量不一致`);
3930
+ const centralSize = archive.readUInt32LE(eocd + 12);
3931
+ const centralOffset = archive.readUInt32LE(eocd + 16);
3932
+ if (entryCount === 0 || entryCount > MAX_ENTRY_COUNT)
3933
+ throw new Error(`${label}.source ZIP 文件数不合法`);
3934
+ if (centralOffset + centralSize > eocd)
3935
+ throw new Error(`${label}.source ZIP 中央目录损坏`);
3936
+ const names = new Set();
3937
+ const foldedNames = new Set();
3938
+ const entries = [];
3939
+ let cursor = centralOffset;
3940
+ let totalUncompressed = 0;
3941
+ for (let index = 0; index < entryCount; index += 1) {
3942
+ if (cursor + 46 > archive.length || archive.readUInt32LE(cursor) !== 0x02014b50)
3943
+ throw new Error(`${label}.source ZIP 中央目录损坏`);
3944
+ const flags = archive.readUInt16LE(cursor + 8);
3945
+ const compressionMethod = archive.readUInt16LE(cursor + 10);
3946
+ const crc32 = archive.readUInt32LE(cursor + 16);
3947
+ const compressedSize = archive.readUInt32LE(cursor + 20);
3948
+ const uncompressedSize = archive.readUInt32LE(cursor + 24);
3949
+ const nameLength = archive.readUInt16LE(cursor + 28);
3950
+ const extraLength = archive.readUInt16LE(cursor + 30);
3951
+ const commentLength = archive.readUInt16LE(cursor + 32);
3952
+ const externalAttributes = archive.readUInt32LE(cursor + 38);
3953
+ const localOffset = archive.readUInt32LE(cursor + 42);
3954
+ const end = cursor + 46 + nameLength + extraLength + commentLength;
3955
+ if (end > archive.length || (flags & 1) !== 0)
3956
+ throw new Error(`${label}.source ZIP 包含加密或损坏的条目`);
3957
+ if (compressedSize === 0xffffffff || uncompressedSize === 0xffffffff)
3958
+ throw new Error(`${label}.source ZIP 暂不支持 ZIP64`);
3959
+ const centralName = archive.subarray(cursor + 46, cursor + 46 + nameLength);
3960
+ const name = centralName.toString('utf8');
3961
+ if ((flags & 0x800) === 0 && /[^\x00-\x7f]/.test(name))
3962
+ throw new Error(`${label}.source ZIP 非 ASCII 路径必须声明 UTF-8`);
3963
+ const normalized = validateArchivePath(name.replace(/\/$/, ''), `${label}.source ZIP 条目`);
3964
+ const unixMode = externalAttributes >>> 16;
3965
+ const fileType = unixMode & 0o170000;
3966
+ if (fileType && fileType !== 0o100000 && fileType !== 0o040000 && fileType !== 0o120000)
3967
+ throw new Error(`${label}.source ZIP 不允许特殊文件:${normalized}`);
3968
+ const folded = normalized.toLocaleLowerCase('en-US');
3969
+ if (names.has(normalized) || foldedNames.has(folded))
3970
+ throw new Error(`${label}.source ZIP 包含重复或大小写冲突条目:${normalized}`);
3971
+ names.add(normalized);
3972
+ foldedNames.add(folded);
3973
+ totalUncompressed += uncompressedSize;
3974
+ if (totalUncompressed > MAX_UNCOMPRESSED_BYTES ||
3975
+ (compressedSize === 0 ? uncompressedSize > 0 : uncompressedSize / compressedSize > MAX_COMPRESSION_RATIO)) {
3976
+ throw new Error(`${label}.source ZIP 解压预算超限`);
3977
+ }
3978
+ const local = inspectLocalEntry(archive, {
3979
+ centralOffset,
3980
+ compressedSize,
3981
+ compressionMethod,
3982
+ crc32,
3983
+ flags,
3984
+ localOffset,
3985
+ name: centralName,
3986
+ uncompressedSize,
3987
+ }, label);
3988
+ entries.push({ name: normalized, directory: name.endsWith('/'), symlink: fileType === 0o120000, ...local });
3989
+ cursor = end;
3990
+ }
3991
+ if (cursor !== centralOffset + centralSize)
3992
+ throw new Error(`${label}.source ZIP 中央目录长度不一致`);
3993
+ const orderedLocalEntries = [...entries].sort((left, right) => left.localStart - right.localStart);
3994
+ for (let index = 1; index < orderedLocalEntries.length; index += 1) {
3995
+ if (orderedLocalEntries[index].localStart < orderedLocalEntries[index - 1].localEnd) {
3996
+ throw new Error(`${label}.source ZIP local entry 范围重叠`);
3997
+ }
3998
+ }
3999
+ return entries;
4000
+ }
4001
+ function inspectLocalEntry(archive, expected, label) {
4002
+ const start = expected.localOffset;
4003
+ if (start + 30 > expected.centralOffset || archive.readUInt32LE(start) !== 0x04034b50) {
4004
+ throw new Error(`${label}.source ZIP local header 损坏`);
4005
+ }
4006
+ const flags = archive.readUInt16LE(start + 6);
4007
+ const compressionMethod = archive.readUInt16LE(start + 8);
4008
+ const crc32 = archive.readUInt32LE(start + 14);
4009
+ const compressedSize = archive.readUInt32LE(start + 18);
4010
+ const uncompressedSize = archive.readUInt32LE(start + 22);
4011
+ const nameLength = archive.readUInt16LE(start + 26);
4012
+ const extraLength = archive.readUInt16LE(start + 28);
4013
+ const nameStart = start + 30;
4014
+ const dataStart = nameStart + nameLength + extraLength;
4015
+ const dataEnd = dataStart + expected.compressedSize;
4016
+ if (dataEnd > expected.centralOffset || !archive.subarray(nameStart, nameStart + nameLength).equals(expected.name)) {
4017
+ throw new Error(`${label}.source ZIP central/local 路径或数据范围不一致`);
4018
+ }
4019
+ if (flags !== expected.flags || compressionMethod !== expected.compressionMethod) {
4020
+ throw new Error(`${label}.source ZIP central/local metadata 不一致`);
4021
+ }
4022
+ if ((flags & 0x08) === 0 &&
4023
+ (crc32 !== expected.crc32 || compressedSize !== expected.compressedSize || uncompressedSize !== expected.uncompressedSize)) {
4024
+ throw new Error(`${label}.source ZIP central/local 大小或 CRC 不一致`);
4025
+ }
4026
+ return { localStart: start, localEnd: dataEnd, dataStart, compressionMethod, uncompressedSize: expected.uncompressedSize };
4027
+ }
4028
+ function findEndOfCentralDirectory(archive) {
4029
+ const minimum = Math.max(0, archive.length - 65_557);
4030
+ for (let offset = archive.length - 22; offset >= minimum; offset -= 1) {
4031
+ if (archive.readUInt32LE(offset) === 0x06054b50) {
4032
+ if (archive.readUInt16LE(offset + 4) !== 0 || archive.readUInt16LE(offset + 6) !== 0)
4033
+ throw new Error('Companion ZIP 不支持多磁盘归档');
4034
+ return offset;
4035
+ }
4036
+ }
4037
+ throw new Error('Companion source 不是有效 ZIP');
4038
+ }
4039
+ function validateArchivePath(value, label) {
4040
+ if (typeof value !== 'string' || !value || value.includes('\\') || value.startsWith('/') || /^[A-Za-z]:/.test(value))
4041
+ throw new Error(`${label} 必须是 ZIP 内相对 POSIX 路径`);
4042
+ const segments = value.split('/');
4043
+ if (segments.some((segment) => !segment || segment === '.' || segment === '..') || /[\u0000-\u001f\u007f]/.test(value))
4044
+ throw new Error(`${label} 包含不安全路径`);
4045
+ return value;
4046
+ }
4047
+ function validateEntrypoint(target, kind, entrypointPath, label) {
4048
+ if (kind !== 'executable' && kind !== 'app-bundle')
4049
+ throw new Error(`${label}.entrypoint.kind 不合法`);
4050
+ if (target === 'windows-x64' && (kind !== 'executable' || !entrypointPath.toLowerCase().endsWith('.exe')))
4051
+ throw new Error(`${label} 必须声明 .exe executable`);
4052
+ if (target === 'macos-arm64' && kind === 'app-bundle' && !entrypointPath.endsWith('.app'))
4053
+ throw new Error(`${label} 的 app-bundle 必须指向 .app`);
4054
+ }
4055
+ function assertEntrypointExists(entries, kind, entrypointPath, label) {
4056
+ const exists = kind === 'app-bundle'
4057
+ ? entries.some((entry) => entry.name === entrypointPath || entry.name.startsWith(`${entrypointPath}/`))
4058
+ : entries.some((entry) => entry.name === entrypointPath && !entry.directory);
4059
+ if (!exists)
4060
+ throw new Error(`${label}.entrypoint 在 ZIP 中不存在:${entrypointPath}`);
4061
+ }
4062
+ function validateCapabilities(value, alias) {
4063
+ if (value === undefined)
4064
+ return [];
4065
+ if (!Array.isArray(value))
4066
+ throw new Error(`companions.${alias}.capabilities 必须是数组`);
4067
+ const result = [];
4068
+ for (const capability of value) {
4069
+ if (!MINIAPP_COMPANION_CAPABILITIES.includes(capability))
4070
+ throw new Error(`companions.${alias}.capabilities 包含未知能力:${capability}`);
4071
+ if (result.includes(capability))
4072
+ throw new Error(`companions.${alias}.capabilities 包含重复能力:${capability}`);
4073
+ result.push(capability);
4074
+ }
4075
+ return result.sort((left, right) => MINIAPP_COMPANION_CAPABILITIES.indexOf(left) - MINIAPP_COMPANION_CAPABILITIES.indexOf(right));
4076
+ }
4077
+ function validateArgs(value, label) {
4078
+ if (value === undefined)
4079
+ return [];
4080
+ if (!Array.isArray(value) || value.length > MAX_ARG_COUNT)
4081
+ throw new Error(`${label}.args 不合法`);
4082
+ let totalBytes = 0;
4083
+ for (const item of value) {
4084
+ if (typeof item !== 'string' || /[\u0000\r\n]/.test(item))
4085
+ throw new Error(`${label}.args 不合法`);
4086
+ const bytes = Buffer.byteLength(item, 'utf8');
4087
+ if (bytes > MAX_ARG_BYTES)
4088
+ throw new Error(`${label}.args 单项最多允许 ${MAX_ARG_BYTES} UTF-8 bytes`);
4089
+ totalBytes += bytes;
4090
+ }
4091
+ if (totalBytes > MAX_ARGS_TOTAL_BYTES)
4092
+ throw new Error(`${label}.args 总计最多允许 ${MAX_ARGS_TOTAL_BYTES} UTF-8 bytes`);
4093
+ return [...value];
4094
+ }
4095
+ function resolveRealPathIfPresent(value) {
4096
+ try {
4097
+ return node_fs.realpathSync(value);
4098
+ }
4099
+ catch {
4100
+ const absolute = path.resolve(value);
4101
+ const parent = path.dirname(absolute);
4102
+ return parent === absolute ? absolute : path.join(resolveRealPathIfPresent(parent), path.basename(absolute));
4103
+ }
4104
+ }
4105
+ function isPathInside(root, candidate, allowEqual = false) {
4106
+ const relative = path.relative(root, candidate);
4107
+ return ((allowEqual && relative === '') || (relative !== '' && relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative)));
4108
+ }
4109
+ function assertExactKeys(value, allowed, label) {
4110
+ if (!isRecord(value))
4111
+ throw new Error(`${label} 必须是对象`);
4112
+ const unknown = Object.keys(value).find((key) => !allowed.includes(key));
4113
+ if (unknown)
4114
+ throw new Error(`${label} 包含未知字段:${unknown}`);
4115
+ }
4116
+ function isRecord(value) {
4117
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
4118
+ }
4119
+
3131
4120
  const UNDEFINED_CODE_POINTS = new Set([
3132
4121
  65534, 65535, 131070, 131071, 196606, 196607, 262142, 262143, 327678, 327679, 393214,
3133
4122
  393215, 458750, 458751, 524286, 524287, 589822, 589823, 655358, 655359, 720894,
@@ -11892,7 +12881,7 @@ function shouldSkipMiniappPlatformCspFromEnv(env = process.env) {
11892
12881
  * 检测同款 INSTALL_FLAG,不会重复 patch。
11893
12882
  */
11894
12883
  function injectMiniDevConsoleBootstrap(html) {
11895
- const bootstrap = `<script>(function(){var LEVELS=['log','debug','info','warn','error'];var FLAG='${MINI_DEV_CONSOLE_INSTALL_FLAG}';var TYPE='${MINI_DEV_CONSOLE_EVENT_TYPE}';if(window.name!=='${MINI_DEV_IFRAME_WINDOW_NAME}')window.name='${MINI_DEV_IFRAME_WINDOW_NAME}';if(window[FLAG])return;var patched=false;for(var i=0;i<LEVELS.length;i++)(function(level){var original=console[level];if(typeof original!=='function')return;console[level]=function(){try{parent.postMessage({type:TYPE,detail:{level:level,args:Array.prototype.slice.call(arguments),timestamp:Date.now()}},'*')}catch(e){}return original.apply(this,arguments)};patched=true})(LEVELS[i]);if(patched)window[FLAG]=true})();</script>`;
12884
+ const bootstrap = `<script>(function(){var LEVELS=['log','debug','info','warn','error'];var FLAG='${MINI_DEV_CONSOLE_INSTALL_FLAG}';var TYPE='${MINI_DEV_CONSOLE_EVENT_TYPE}';if(window[FLAG])return;var patched=false;for(var i=0;i<LEVELS.length;i++)(function(level){var original=console[level];if(typeof original!=='function')return;console[level]=function(){try{parent.postMessage({type:TYPE,detail:{level:level,args:Array.prototype.slice.call(arguments),timestamp:Date.now()}},'*')}catch(e){}return original.apply(this,arguments)};patched=true})(LEVELS[i]);if(patched)window[FLAG]=true})();</script>`;
11896
12885
  const headIndex = html.indexOf('<head>');
11897
12886
  if (headIndex < 0) {
11898
12887
  return html;
@@ -11903,9 +12892,29 @@ function injectMiniDevConsoleBootstrap(html) {
11903
12892
  const sdkVersionPlaceholder = ['__HB_SDK', 'VERSION__'].join('_');
11904
12893
  const sdkVersionBuildConstant = ['__HB_SDK_BUILD', 'VERSION__'].join('_');
11905
12894
  const miniDevLoggingConstant = ['__HB_SDK_DEV', 'LOGGING__'].join('_');
11906
- function miniappManifest(options) {
11907
- const platforms = validateMiniappPlatforms(options?.platforms, 'miniappManifest().platforms');
12895
+ const DEV_MANIFEST_PATH = '/__hb_sdk__/companion/manifest.json';
12896
+ const DEV_TOKEN_HEADER = 'x-hb-sdk-companion-dev-token';
12897
+ const DEV_DIGEST_HEADER = 'x-hb-sdk-companion-manifest-sha256';
12898
+ const DEV_ERROR_MESSAGE = 'Companion 开发资源暂时不可用';
12899
+ const DEV_COMPANION_PATH_PATTERN = /^\/__hb_sdk__\/companions\/([a-z][a-z0-9-]{0,31})\/(windows-x64|macos-arm64)\.zip$/;
12900
+ const HB_SDK_COMPANION_ARTIFACT_ACCESS_TOKEN_ENV = 'HB_SDK_COMPANION_ARTIFACT_ACCESS_TOKEN';
12901
+ const devSnapshotStoreKey = Symbol.for('@heybox/hb-sdk/dev-companion-snapshots');
12902
+ const devSnapshotRegistry = globalThis;
12903
+ if (!devSnapshotRegistry[devSnapshotStoreKey]) {
12904
+ devSnapshotRegistry[devSnapshotStoreKey] = new Map();
12905
+ process.once('exit', () => {
12906
+ for (const store of devSnapshotRegistry[devSnapshotStoreKey].values()) {
12907
+ for (const snapshot of store.snapshots.values())
12908
+ node_fs.rmSync(snapshot.root, { force: true, recursive: true });
12909
+ }
12910
+ });
12911
+ }
12912
+ const devSnapshotStores = devSnapshotRegistry[devSnapshotStoreKey];
12913
+ function miniappManifest() {
12914
+ const includeCompanions = process.env.HB_SDK_SKIP_COMPANIONS !== '1';
12915
+ const deviceLogs = new DeviceLogStore();
11908
12916
  const skipPlatformCsp = shouldSkipMiniappPlatformCspFromEnv();
12917
+ let companionArtifactAccessToken = parseCompanionArtifactAccessToken(process.env[HB_SDK_COMPANION_ARTIFACT_ACCESS_TOKEN_ENV]);
11909
12918
  let root = process.cwd();
11910
12919
  let outDir = 'dist';
11911
12920
  let command = 'build';
@@ -11940,6 +12949,211 @@ function miniappManifest(options) {
11940
12949
  command = resolved.command;
11941
12950
  hmrWebSocketUrl = resolveHmrWebSocketUrl(resolved);
11942
12951
  },
12952
+ configureServer(server) {
12953
+ let unregisterLogs;
12954
+ const registerLogs = () => {
12955
+ const address = server.httpServer?.address();
12956
+ if (address && typeof address !== 'string')
12957
+ unregisterLogs = registerDeviceLogs(address.port, deviceLogs);
12958
+ };
12959
+ server.httpServer?.once('listening', registerLogs);
12960
+ server.httpServer?.once('close', () => unregisterLogs?.());
12961
+ if (server.httpServer?.listening)
12962
+ registerLogs();
12963
+ let store = devSnapshotStores.get(root);
12964
+ companionArtifactAccessToken ??= store?.token;
12965
+ if (!store || store.token !== companionArtifactAccessToken) {
12966
+ store = { token: companionArtifactAccessToken ?? '', snapshots: new Map() };
12967
+ devSnapshotStores.set(root, store);
12968
+ }
12969
+ const snapshots = store.snapshots;
12970
+ if (server.restart) {
12971
+ const restart = server.restart.bind(server);
12972
+ server.restart = async (forceOptimize) => {
12973
+ store.restarting = true;
12974
+ try {
12975
+ await restart(forceOptimize);
12976
+ }
12977
+ finally {
12978
+ store.restarting = false;
12979
+ }
12980
+ };
12981
+ }
12982
+ let dirty = true;
12983
+ let latestDigest;
12984
+ let changedAt = 0;
12985
+ let sourceRoots = [];
12986
+ const watchPaths = new Set();
12987
+ const invalidate = () => {
12988
+ dirty = true;
12989
+ changedAt = Date.now();
12990
+ // 目录自身的变化发现新增/删除子项;文件监听发现原地编译与资源覆盖。
12991
+ updateWatchPaths(sourceRoots);
12992
+ };
12993
+ const updateWatchPaths = (files) => {
12994
+ sourceRoots = files;
12995
+ const next = new Set(files.flatMap(collectCompanionWatchPaths));
12996
+ for (const file of watchPaths)
12997
+ if (!next.has(file)) {
12998
+ node_fs.unwatchFile(file, invalidate);
12999
+ watchPaths.delete(file);
13000
+ }
13001
+ for (const file of next)
13002
+ if (!watchPaths.has(file)) {
13003
+ node_fs.watchFile(file, { interval: 500, persistent: false }, invalidate);
13004
+ watchPaths.add(file);
13005
+ }
13006
+ };
13007
+ updateWatchPaths([path.join(root, 'package.json')]);
13008
+ server.httpServer?.once('close', () => {
13009
+ for (const file of watchPaths)
13010
+ node_fs.unwatchFile(file, invalidate);
13011
+ if (store.restarting)
13012
+ return;
13013
+ for (const snapshot of snapshots.values())
13014
+ node_fs.rmSync(snapshot.root, { force: true, recursive: true });
13015
+ if (devSnapshotStores.get(root) === store)
13016
+ devSnapshotStores.delete(root);
13017
+ });
13018
+ const getSnapshot = (requestedDigest) => {
13019
+ if (requestedDigest)
13020
+ return snapshots.get(requestedDigest);
13021
+ if (!dirty && latestDigest)
13022
+ return snapshots.get(latestDigest);
13023
+ if (Date.now() - changedAt < 200)
13024
+ throw new Error('Companion 产物准备中');
13025
+ const snapshotRoot = node_fs.mkdtempSync(path.join(node_os.tmpdir(), 'hb-sdk-companion-snapshot-'));
13026
+ node_fs.chmodSync(snapshotRoot, 0o700);
13027
+ try {
13028
+ const config = readMiniappCompanionsFromPackageJson(root);
13029
+ const sources = Object.values(config.declarations ?? {}).flatMap(definition => definition && typeof definition === 'object' && definition.targets && typeof definition.targets === 'object'
13030
+ ? Object.entries(definition.targets).flatMap(([name, target]) => currentCompanionTargets().includes(name) && target && typeof target.source === 'string'
13031
+ ? [path.resolve(config.projectRoot, target.source)] : []) : [])
13032
+ .filter(source => source.startsWith(`${config.projectRoot}${path.sep}`));
13033
+ updateWatchPaths([config.packageJsonPath, ...sources]);
13034
+ const snapshot = prepareDevSnapshot(root, outDir, readMiniappPlatformsFromPackageJson(root), config.declarations, snapshotRoot, config.projectRoot);
13035
+ const digest = node_crypto.createHash('sha256').update(snapshot.manifestBytes).digest('hex');
13036
+ if (snapshots.has(digest))
13037
+ node_fs.rmSync(snapshotRoot, { force: true, recursive: true });
13038
+ else {
13039
+ const totalBytes = [...snapshots.values(), snapshot].reduce((total, item) => total + [...item.artifacts.values()].reduce((size, artifact) => size + artifact.size, 0), 0);
13040
+ if (snapshots.size >= 32 || totalBytes > 1024 * 1024 * 1024) {
13041
+ throw new Error('Companion 快照存储已达到上限,请结束旧调试会话后重启 dev');
13042
+ }
13043
+ snapshots.set(digest, { ...snapshot, root: snapshotRoot });
13044
+ }
13045
+ latestDigest = digest;
13046
+ dirty = false;
13047
+ return snapshots.get(digest);
13048
+ }
13049
+ catch (error) {
13050
+ node_fs.rmSync(snapshotRoot, { force: true, recursive: true });
13051
+ throw error;
13052
+ }
13053
+ };
13054
+ server.middlewares.use(async (request, response, next) => {
13055
+ const pathname = parseRequestPathname(request.url);
13056
+ if (pathname === DEVICE_LOG_PATH) {
13057
+ const origin = request.headers.origin;
13058
+ let allowed = false;
13059
+ try {
13060
+ allowed = Boolean(origin && new URL(origin).host === request.headers.host);
13061
+ }
13062
+ catch { /* 拒绝畸形 Origin。 */ }
13063
+ if (!allowed) {
13064
+ response.writeHead(403).end();
13065
+ return;
13066
+ }
13067
+ await deviceLogs.upload(request, response);
13068
+ return;
13069
+ }
13070
+ const companionMatch = pathname?.match(DEV_COMPANION_PATH_PATTERN);
13071
+ if (pathname !== DEV_MANIFEST_PATH && !companionMatch) {
13072
+ next();
13073
+ return;
13074
+ }
13075
+ if (request.method !== 'GET' && request.method !== 'HEAD') {
13076
+ response.statusCode = 405;
13077
+ response.setHeader('Allow', 'GET, HEAD');
13078
+ response.end();
13079
+ return;
13080
+ }
13081
+ if (!companionArtifactAccessToken ||
13082
+ !isNumericLoopbackRequest(request) ||
13083
+ !isNumericLoopbackHost(request.headers.host) ||
13084
+ !hasValidCompanionArtifactToken(request, companionArtifactAccessToken)) {
13085
+ response.statusCode = 403;
13086
+ response.end();
13087
+ return;
13088
+ }
13089
+ try {
13090
+ const requestedDigest = request.headers[DEV_DIGEST_HEADER];
13091
+ if (requestedDigest !== undefined && (typeof requestedDigest !== 'string' || !/^[a-f0-9]{64}$/.test(requestedDigest))) {
13092
+ response.statusCode = 400;
13093
+ response.end();
13094
+ return;
13095
+ }
13096
+ const snapshot = getSnapshot(requestedDigest);
13097
+ if (!snapshot) {
13098
+ response.statusCode = 404;
13099
+ response.end();
13100
+ return;
13101
+ }
13102
+ response.setHeader('Cache-Control', 'no-store');
13103
+ response.setHeader('X-Content-Type-Options', 'nosniff');
13104
+ if (pathname === DEV_MANIFEST_PATH) {
13105
+ response.statusCode = 200;
13106
+ response.setHeader('Content-Type', 'application/json; charset=utf-8');
13107
+ response.setHeader('Content-Length', snapshot.manifestBytes.length);
13108
+ response.end(request.method === 'HEAD' ? undefined : snapshot.manifestBytes);
13109
+ return;
13110
+ }
13111
+ const [, alias, target] = companionMatch;
13112
+ const artifact = snapshot.artifacts.get(`companions/${alias}/${target}.zip`);
13113
+ if (!artifact || `/__hb_sdk__/${artifact.path}` !== pathname) {
13114
+ response.statusCode = 404;
13115
+ response.end();
13116
+ return;
13117
+ }
13118
+ response.statusCode = 200;
13119
+ response.setHeader('Content-Type', 'application/zip');
13120
+ response.setHeader('Content-Length', artifact.size);
13121
+ if (request.method === 'HEAD') {
13122
+ response.end();
13123
+ return;
13124
+ }
13125
+ const stream = node_fs.createReadStream(artifact.sourcePath);
13126
+ const stopStreaming = () => stream.destroy();
13127
+ request.once('aborted', stopStreaming);
13128
+ response.once('close', stopStreaming);
13129
+ stream.once('close', () => {
13130
+ request.off('aborted', stopStreaming);
13131
+ response.off('close', stopStreaming);
13132
+ });
13133
+ stream.on('error', () => {
13134
+ if (response.headersSent) {
13135
+ response.destroy();
13136
+ return;
13137
+ }
13138
+ response.statusCode = 500;
13139
+ response.removeHeader('Content-Length');
13140
+ response.setHeader('Content-Type', 'text/plain; charset=utf-8');
13141
+ response.end(DEV_ERROR_MESSAGE);
13142
+ });
13143
+ stream.pipe(response);
13144
+ }
13145
+ catch (error) {
13146
+ response.statusCode = 500;
13147
+ const message = error instanceof Error ? error.message : '';
13148
+ response.setHeader('X-Hb-Sdk-Companion-Error', /ENOENT/.test(message) ? 'artifact-missing'
13149
+ : /快照存储/.test(message) ? 'snapshot-limit'
13150
+ : /产物准备中/.test(message) ? 'artifact-updating'
13151
+ : /package.json|JSON|permissions/.test(message) ? 'invalid-manifest' : 'invalid-artifact');
13152
+ response.setHeader('Content-Type', 'text/plain; charset=utf-8');
13153
+ response.end(DEV_ERROR_MESSAGE);
13154
+ }
13155
+ });
13156
+ },
11943
13157
  buildEnd(error) {
11944
13158
  if (error) {
11945
13159
  priorBuildError = priorBuildError ?? toError(error);
@@ -11950,8 +13164,14 @@ function miniappManifest(options) {
11950
13164
  ...(command === 'serve' ? { hmrWebSocketUrl } : {}),
11951
13165
  skipPlatformCsp,
11952
13166
  });
11953
- return command === 'serve' ? injectMiniDevConsoleBootstrap(enforced) : enforced;
11954
- }, async closeBundle() {
13167
+ return command === 'serve'
13168
+ ? injectMiniDevConsoleBootstrap(enforced.replace('<head>', `<head>${deviceLogBootstrap(deviceLogs.uploadToken)}`))
13169
+ : enforced;
13170
+ },
13171
+ async closeBundle() {
13172
+ // Vite dev 的关闭/配置重载同样会触发此钩子,不执行发布打包。
13173
+ if (command === 'serve')
13174
+ return;
11955
13175
  // Rollup 在 buildStart/transform 失败后仍会调用 closeBundle。
11956
13176
  // 若此时再抛「缺 index.html」等二次错误,会掩盖真实失败原因。
11957
13177
  if (priorBuildError) {
@@ -11959,9 +13179,15 @@ function miniappManifest(options) {
11959
13179
  }
11960
13180
  const version = validateMiniappPackageVersionForBuild(readMiniappVersionFromPackageJson(root));
11961
13181
  const sdkVersion = resolveSdkVersion();
13182
+ const platforms = readMiniappPlatformsFromPackageJson(root);
11962
13183
  const parsedPermissions = readMiniappPermissionsFromPackageJson(root, sdkVersion);
13184
+ const companionConfig = readMiniappCompanionsFromPackageJson(root);
13185
+ const companionDeclarations = includeCompanions ? companionConfig.declarations : undefined;
11963
13186
  if (!parsedPermissions.declared)
11964
13187
  this.warn(getMissingPermissionsWarning());
13188
+ if (companionDeclarations && !parsedPermissions.permissions?.companion) {
13189
+ throw new Error('配置 companions 时必须声明 package.json#heybox.permissions.companion.enabled=true');
13190
+ }
11965
13191
  const outputRoot = path.resolve(root, outDir);
11966
13192
  const htmlFiles = collectHtmlFiles(outputRoot);
11967
13193
  const unexpectedHtmlFiles = htmlFiles.filter((file) => path.relative(outputRoot, file) !== 'index.html');
@@ -11976,16 +13202,104 @@ function miniappManifest(options) {
11976
13202
  }
11977
13203
  node_fs.writeFileSync(indexHtmlPath, enforceMiniappHtmlPolicy(node_fs.readFileSync(indexHtmlPath, 'utf8'), { skipPlatformCsp }));
11978
13204
  const manifestPath = path.join(outputRoot, 'manifest.json');
13205
+ const companions = prepareMiniappCompanions(companionDeclarations, { root: companionConfig.projectRoot, outputRoot, platforms });
11979
13206
  node_fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
11980
13207
  node_fs.writeFileSync(manifestPath, renderMiniappManifest({
11981
13208
  version,
11982
13209
  sdkVersion,
11983
13210
  platforms,
11984
13211
  ...(parsedPermissions.permissions === undefined ? {} : { permissions: parsedPermissions.permissions }),
13212
+ ...(companions === undefined ? {} : { companions }),
11985
13213
  }));
11986
13214
  },
11987
13215
  };
11988
13216
  }
13217
+ function prepareDevSnapshot(root, outDir, platforms, declarations, snapshotRoot, projectRoot = root) {
13218
+ const version = validateMiniappPackageVersionForBuild(readMiniappVersionFromPackageJson(root));
13219
+ const sdkVersion = resolveSdkVersion();
13220
+ const permissions = readMiniappPermissionsFromPackageJson(root, sdkVersion).permissions;
13221
+ if (declarations && !permissions?.companion) {
13222
+ throw new Error('配置 companions 时必须声明 package.json#heybox.permissions.companion.enabled=true');
13223
+ }
13224
+ const artifacts = new Map();
13225
+ const companions = prepareMiniappCompanions(declarations, {
13226
+ root: projectRoot,
13227
+ outputRoot: path.resolve(root, outDir),
13228
+ platforms,
13229
+ targets: currentCompanionTargets(),
13230
+ copyArtifacts: false,
13231
+ onPreparedArtifact: ({ archive, path: artifactPath, size }) => {
13232
+ const sourcePath = path.join(snapshotRoot, ...artifactPath.split('/'));
13233
+ node_fs.mkdirSync(path.dirname(sourcePath), { mode: 0o700, recursive: true });
13234
+ node_fs.writeFileSync(sourcePath, archive, { flag: 'wx', mode: 0o600 });
13235
+ artifacts.set(artifactPath, { path: artifactPath, size, sourcePath });
13236
+ },
13237
+ });
13238
+ const manifest = {
13239
+ version,
13240
+ sdkVersion,
13241
+ platforms,
13242
+ ...(permissions === undefined ? {} : { permissions }),
13243
+ ...(companions === undefined ? {} : { companions }),
13244
+ };
13245
+ return { artifacts, manifestBytes: Buffer.from(renderMiniappManifest(manifest)) };
13246
+ }
13247
+ function currentCompanionTargets() {
13248
+ if (node_os.platform() === 'win32' && node_os.arch() === 'x64')
13249
+ return ['windows-x64'];
13250
+ if (node_os.platform() === 'darwin' && node_os.arch() === 'arm64')
13251
+ return ['macos-arm64'];
13252
+ return [];
13253
+ }
13254
+ /** 不跟随链接扫描;合法 .app 链接的最终目标仍在此树内,由普通路径负责监听。 */
13255
+ function collectCompanionWatchPaths(root) {
13256
+ const paths = [root];
13257
+ try {
13258
+ if (node_fs.lstatSync(root).isDirectory()) {
13259
+ for (const name of node_fs.readdirSync(root))
13260
+ paths.push(...collectCompanionWatchPaths(path.join(root, name)));
13261
+ }
13262
+ }
13263
+ catch {
13264
+ // 缺失目录也保留监听,允许第一次编译或原子替换后自动恢复。
13265
+ }
13266
+ return paths;
13267
+ }
13268
+ function parseRequestPathname(value) {
13269
+ if (!value)
13270
+ return undefined;
13271
+ try {
13272
+ const url = new URL(value, 'http://127.0.0.1');
13273
+ return url.search || url.hash ? undefined : url.pathname;
13274
+ }
13275
+ catch {
13276
+ return undefined;
13277
+ }
13278
+ }
13279
+ function isNumericLoopbackRequest(request) {
13280
+ const remoteAddress = request.socket.remoteAddress;
13281
+ return remoteAddress === '127.0.0.1' || remoteAddress === '::1' || remoteAddress === '::ffff:127.0.0.1';
13282
+ }
13283
+ function isNumericLoopbackHost(host) {
13284
+ if (!host)
13285
+ return false;
13286
+ const match = host.match(/^(?:127\.0\.0\.1|\[::1\]):(\d{1,5})$/);
13287
+ if (!match)
13288
+ return false;
13289
+ const port = Number(match[1]);
13290
+ return port >= 1 && port <= 65_535;
13291
+ }
13292
+ function parseCompanionArtifactAccessToken(value) {
13293
+ return value && /^[A-Za-z0-9_-]{43}$/.test(value) ? value : undefined;
13294
+ }
13295
+ function hasValidCompanionArtifactToken(request, expected) {
13296
+ const value = request.headers[DEV_TOKEN_HEADER];
13297
+ const candidate = Buffer.alloc(43);
13298
+ if (typeof value === 'string')
13299
+ Buffer.from(value).copy(candidate, 0, 0, candidate.length);
13300
+ const matches = node_crypto.timingSafeEqual(candidate, Buffer.from(expected));
13301
+ return matches && typeof value === 'string' && /^[A-Za-z0-9_-]{43}$/.test(value);
13302
+ }
11989
13303
  function assertMiniappViteBase(base) {
11990
13304
  if (base !== undefined && base !== '' && base !== './') {
11991
13305
  throw new Error(`工坊小程序 Vite base 仅支持空字符串或 "./",当前值:${base}`);
@@ -12028,5 +13342,6 @@ function resolveHmrWebSocketUrl(resolved) {
12028
13342
  return `${protocol}://${host}${port ? `:${port}` : ''}`;
12029
13343
  }
12030
13344
 
13345
+ exports.HB_SDK_COMPANION_ARTIFACT_ACCESS_TOKEN_ENV = HB_SDK_COMPANION_ARTIFACT_ACCESS_TOKEN_ENV;
12031
13346
  exports.MINIAPP_PLATFORM_VALUES = MINIAPP_PLATFORM_VALUES;
12032
13347
  exports.miniappManifest = miniappManifest;