@heybox/hb-sdk 0.8.2-alpha.2 → 0.8.2-alpha.6

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 (54) hide show
  1. package/CHANGELOG.md +44 -1
  2. package/README.md +10 -3
  3. package/dist/cli-chunks/{build-bVThH3Z5.cjs → build-TOcahJhW.cjs} +5 -5
  4. package/dist/cli-chunks/{context-DpKqGnjp.cjs → context-Bk0adtVQ.cjs} +326 -104
  5. package/dist/cli-chunks/{create-BWC2343y.cjs → create-BxIgQ4SF.cjs} +5 -1
  6. package/dist/cli-chunks/{dev-t14YpHHY.cjs → dev-CyhKTGGJ.cjs} +17 -9
  7. package/dist/cli-chunks/{doctor-BWkrE0Hj.cjs → doctor-ItlNn-K2.cjs} +25 -13
  8. package/dist/cli-chunks/{index-CDtcZVWv.cjs → index-CP14FXez.cjs} +16 -18
  9. package/dist/cli-chunks/{index-D3FMcOZq.cjs → index-CjWRMwuI.cjs} +2 -2
  10. package/dist/cli-chunks/{index.esm-BrJBaJWS.cjs → index.esm-deP-KEyE.cjs} +11 -10
  11. package/dist/cli-chunks/{login-DRMl7nFH.cjs → login-4B-2nqgd.cjs} +2 -2
  12. package/dist/cli-chunks/{project-vite-CXgdzIYw.cjs → project-vite-HKW4tSBX.cjs} +1 -1
  13. package/dist/cli-chunks/{remote-ibQybNiM.cjs → remote-BLdGzViG.cjs} +80 -60
  14. package/dist/cli-chunks/runtime-gate-B_fHNic6.cjs +58 -0
  15. package/dist/cli-chunks/{runtime-gate-BWb8QIcX.cjs → runtime-permission-env-CAhESVDA.cjs} +4533 -40
  16. package/dist/cli-chunks/{session-CIoBspg-.cjs → session-COr9TQ6O.cjs} +1 -1
  17. package/dist/cli-chunks/{skill-5labzuWk.cjs → skill-DiZSczbw.cjs} +2 -2
  18. package/dist/cli-chunks/{version-QiVEs76a.cjs → version-tRrcrp9F.cjs} +1 -1
  19. package/dist/cli.cjs +1 -1
  20. package/dist/devtools/browser-dev-host/assets/browser-dev-host-CF7IQARw.js +101 -0
  21. package/dist/devtools/browser-dev-host/assets/{desktop-app-launch-B4KsgItx.js → desktop-app-launch-CQmXIMyL.js} +1 -1
  22. package/dist/devtools/browser-dev-host/assets/{index-9Nb3XhtI.js → index-DdKYO0um.js} +3 -3
  23. package/dist/devtools/browser-dev-host/index.html +2 -2
  24. package/dist/index.cjs.js +1 -1
  25. package/dist/index.esm.js +1 -1
  26. package/dist/miniapp-publish.cjs.js +8782 -0
  27. package/dist/miniapp-publish.esm.js +8778 -1
  28. package/dist/templates/vanilla-vite-js/README.md.ejs +3 -0
  29. package/dist/templates/vanilla-vite-js/assets/heybox-placeholder.png +0 -0
  30. package/dist/templates/vanilla-vite-js/package.json.ejs +3 -0
  31. package/dist/vite.cjs.js +499 -346
  32. package/dist/vite.esm.js +500 -347
  33. package/package.json +8 -7
  34. package/skill/SKILL.md +22 -124
  35. package/skill/skill.json +5 -5
  36. package/types/miniapp-manifest/basic-info-browser.d.ts +9 -0
  37. package/types/miniapp-manifest/basic-info-image-header.d.ts +6 -0
  38. package/types/miniapp-manifest/basic-info-node.d.ts +17 -0
  39. package/types/miniapp-manifest/basic-info-placeholder.d.ts +1 -0
  40. package/types/miniapp-manifest/basic-info.d.ts +12 -0
  41. package/types/miniapp-manifest/schema.d.ts +7 -1
  42. package/types/miniapp-publish/index.d.ts +8 -0
  43. package/types/skill-metadata.d.ts +2 -2
  44. package/dist/cli-chunks/runtime-permission-env-DiTeD75Q.cjs +0 -4498
  45. package/dist/devtools/browser-dev-host/assets/browser-dev-host-DxzOkljJ.js +0 -101
  46. package/skill/references/api-protocol.md +0 -317
  47. package/skill/references/api-root.md +0 -1243
  48. package/skill/references/cli.md +0 -401
  49. package/skill/references/examples.md +0 -204
  50. package/skill/references/llms-index.md +0 -24
  51. package/skill/references/recipes.md +0 -854
  52. package/skill/references/safety-boundaries.md +0 -63
  53. package/skill/references/smoke-evaluation.md +0 -26
  54. /package/types/{vite → miniapp-manifest}/runtime-gate.d.ts +0 -0
package/dist/vite.esm.js CHANGED
@@ -1,333 +1,12 @@
1
- import { readFileSync, existsSync, realpathSync, lstatSync, readdirSync, readlinkSync, mkdirSync, writeFileSync, renameSync, rmSync, unwatchFile, createReadStream, watchFile, mkdtempSync, chmodSync } from 'node:fs';
2
- import { randomUUID, randomBytes, createHash, timingSafeEqual } from 'node:crypto';
3
- import { tmpdir, platform, arch } from 'node:os';
1
+ import { createHash, randomUUID, randomBytes, timingSafeEqual } from 'node:crypto';
2
+ import { constants as constants$1, readFileSync, existsSync, realpathSync, lstatSync, readdirSync, readlinkSync, mkdirSync, writeFileSync, renameSync, rmSync, unwatchFile, createReadStream, watchFile, mkdtempSync, chmodSync } from 'node:fs';
3
+ import { readFile, mkdir, realpath, writeFile, stat, open } from 'node:fs/promises';
4
4
  import path from 'node:path';
5
+ import sharp from 'sharp';
6
+ import { tmpdir, platform, arch } from 'node:os';
5
7
  import { inflateRawSync } from 'node:zlib';
6
8
  import { AsyncLocalStorage } from 'node:async_hooks';
7
9
 
8
- /** 构建时替换为当前发布包的实际版本。 */
9
- const HB_SDK_VERSION = typeof undefined === 'string'
10
- ? undefined
11
- : '0.8.2-alpha.2';
12
-
13
- /**
14
- * iframe 与调试台页面的开发期 console 捕获转发。
15
- *
16
- * 双条件门控(iframe 转发):
17
- * 1. 构建期:`hb-sdk dev`(vite serve)注入 `false`;`hb-sdk build` /
18
- * `vite build`(生产)永不注入或替换为 `false`,本模块被死代码消除。
19
- * 2. 运行期:仅在 `window.name` 为 `__hb_sdk_mini_dev__` 的调试 iframe 中激活,
20
- * 用户设备上的正式运行环境没有该标记。
21
- *
22
- * 无侵入 patch `console.log/debug/info/warn/error`:原样放行(DevTools 不受影响)+
23
- * 通过 `window.parent.postMessage` 转发给调试台(CSP 的 connect-src 不约束 postMessage,
24
- * 网络上报由调试台页面代理)。发送全程静默:失败不得回打 console,避免自触发死循环。
25
- *
26
- * 激活时机:由 SDK 默认单例初始化时调用(业务 `import '@heybox/hb-sdk'` 即生效);
27
- * iframe 标记由调试台 `iframe.name` 与 vite dev 注入的无依赖 bootstrap 共同设置。
28
- * 调试台页面自身(runtime 跑在这里,`[hb-sdk-runtime]` 日志不经过 iframe)另用
29
- * `installMiniDevConsoleSelfCapture` 接入「日志」页签(source: 'host')。
30
- */
31
- const MINI_DEV_CONSOLE_EVENT_TYPE = 'hb-sdk:mini-dev-console';
32
- /** 调试台页面与 iframe bootstrap 共用的已安装标记(window/console 上的属性名)。 */
33
- const MINI_DEV_CONSOLE_INSTALL_FLAG = '__hb_sdk_mini_dev_console_installed__';
34
-
35
- /**
36
- * network.request 诊断日志的纯脱敏工具。
37
- *
38
- * @remarks
39
- * 放在 `core` 而非 `modules/network`:无错误类型依赖,供 SDK 错误文案、
40
- * modules observability、devtools Browser Dev Host / proxy 共用,并满足 boundary
41
- *(devtools 不得直接 import runtime capability modules)。
42
- */
43
- /** 敏感字段名:header / query / body key 共用。 */
44
- 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;
45
- /**
46
- * 脱敏请求 URL:去掉 userinfo,并将 query / fragment 中的敏感参数值替换为 `[redacted]`。
47
- */
48
- function sanitizeNetworkRequestUrl(url) {
49
- if (!url) {
50
- return url;
51
- }
52
- try {
53
- const parsed = new URL(url);
54
- parsed.username = '';
55
- parsed.password = '';
56
- redactUrlSearchParams(parsed.searchParams);
57
- if (parsed.hash && parsed.hash.length > 1) {
58
- const hashBody = parsed.hash.slice(1);
59
- if (hashBody.includes('=')) {
60
- const hashParams = new URLSearchParams(hashBody);
61
- if (redactUrlSearchParams(hashParams)) {
62
- parsed.hash = hashParams.toString();
63
- }
64
- }
65
- }
66
- return parsed.toString();
67
- }
68
- catch {
69
- return redactSensitivePairsInRawText(url);
70
- }
71
- }
72
- /**
73
- * 诊断 message 中的 URL 片段做脱敏,避免 error.message 二次泄漏 query secrets。
74
- */
75
- function sanitizeDiagnosticMessage(message) {
76
- if (!message) {
77
- return message;
78
- }
79
- return message.replace(/https?:\/\/[^\s]+/gi, matched => sanitizeNetworkRequestUrl(matched.replace(/[),.;]+$/g, '')));
80
- }
81
- function isSensitiveNetworkKey(key) {
82
- return SENSITIVE_KEY_PATTERN.test(key.trim());
83
- }
84
- function redactUrlSearchParams(params) {
85
- let changed = false;
86
- for (const key of [...params.keys()]) {
87
- if (isSensitiveNetworkKey(key)) {
88
- params.set(key, '[redacted]');
89
- changed = true;
90
- }
91
- }
92
- return changed;
93
- }
94
- function redactSensitivePairsInRawText(text) {
95
- return text.replace(/([?&#]|^|[?&])([^=&#\s]+)=([^&#\s]*)/g, (full, prefix, key, value) => {
96
- if (!isSensitiveNetworkKey(key)) {
97
- return full;
98
- }
99
- return `${prefix}${key}=[redacted]`;
100
- });
101
- }
102
-
103
- const DEVICE_LOG_PATH = '/__hb_sdk__/device-logs';
104
- const registryKey = Symbol.for('@heybox/hb-sdk/device-logs');
105
- const registry = globalThis;
106
- const stores = (registry[registryKey] ??= new Map());
107
- function registerDeviceLogs(port, store) {
108
- stores.set(port, store);
109
- return () => {
110
- if (stores.get(port) === store)
111
- stores.delete(port);
112
- };
113
- }
114
- class DeviceLogStore {
115
- sandboxRequests = new Map();
116
- listSandbox(id, path) {
117
- const device = this.devices.get(id);
118
- if (!device || Date.now() - device.lastSeen > 15000)
119
- return Promise.resolve({ error: '设备已离线' });
120
- if (path !== '.' && (!path || path.length > 1024 || /[\\\\\u0000-\u001f]/.test(path) || path.split('/').some(part => !part || part === '.' || part === '..')))
121
- return Promise.resolve({ error: '无效目录路径' });
122
- if (this.sandboxRequests.size >= 64)
123
- return Promise.resolve({ error: '查询繁忙,请稍后重试' });
124
- const requestId = randomUUID();
125
- return new Promise(resolve => {
126
- const timer = setTimeout(() => { this.sandboxRequests.delete(requestId); resolve({ error: '设备查询超时' }); }, 10000);
127
- this.sandboxRequests.set(requestId, { id, path, requestId, resolve: value => { clearTimeout(timer); this.sandboxRequests.delete(requestId); resolve(value); } });
128
- });
129
- }
130
- uploadToken = randomBytes(32).toString('hex');
131
- epoch = randomUUID();
132
- expiresAt = Date.now() + 12 * 60 * 60 * 1000;
133
- cursor = 0;
134
- logs = [];
135
- devices = new Map();
136
- query(params) {
137
- const after = Math.max(0, Number(params.get('after')) || 0);
138
- const limit = Math.min(500, Math.max(1, Number(params.get('limit')) || 100));
139
- const selected = this.logs.filter((log) => log.cursor > after &&
140
- (!params.get('device') || log.deviceSessionId === params.get('device')) &&
141
- (!params.get('level') || log.level === params.get('level')) &&
142
- (!params.get('source') || log.source === params.get('source')) &&
143
- (!params.get('callId') || log.callId === params.get('callId')) &&
144
- (!params.get('id') || String(log.cursor) === params.get('id')) &&
145
- (!params.get('since') || log.timestamp >= Number(params.get('since'))) &&
146
- (!params.get('query') || log.message.toLowerCase().includes(params.get('query').toLowerCase())));
147
- const items = selected.slice(0, limit);
148
- return {
149
- schemaVersion: 1,
150
- epoch: this.epoch,
151
- items,
152
- nextCursor: selected.length > limit ? items.at(-1).cursor : this.cursor,
153
- hasMore: selected.length > limit,
154
- gap: after > 0 && after < (this.logs[0]?.cursor ?? 1) - 1,
155
- devices: [...this.devices.values()].map(({ id, name, platform, connectedAt, lastSeen }) => ({
156
- id,
157
- name,
158
- platform,
159
- connectedAt,
160
- lastSeen,
161
- online: Date.now() - lastSeen < 15000,
162
- })),
163
- };
164
- }
165
- async upload(req, res) {
166
- const reply = (status, value) => {
167
- res.writeHead(status, { 'content-type': 'application/json', 'cache-control': 'no-store' });
168
- res.end(JSON.stringify(value));
169
- };
170
- if (req.method !== 'POST') {
171
- reply(405, {});
172
- return;
173
- }
174
- if (Date.now() > this.expiresAt) {
175
- reply(410, { code: 'EXPIRED' });
176
- return;
177
- }
178
- let text = '';
179
- try {
180
- req.setEncoding('utf8');
181
- for await (const chunk of req) {
182
- text += chunk;
183
- if (Buffer.byteLength(text) > 65536) {
184
- reply(413, {});
185
- return;
186
- }
187
- }
188
- const body = JSON.parse(text);
189
- if (body.register === true) {
190
- if (req.headers['x-hb-log-token'] !== this.uploadToken) {
191
- reply(403, {});
192
- return;
193
- }
194
- const registrationId = typeof body.registrationId === 'string' && /^[a-f0-9]{32}$/.test(body.registrationId) ? body.registrationId : undefined;
195
- const existing = registrationId && [...this.devices.values()].find(device => device.registrationId === registrationId);
196
- if (existing) {
197
- reply(200, { id: existing.id, token: existing.token });
198
- return;
199
- }
200
- if (this.devices.size >= 64) {
201
- reply(429, {});
202
- return;
203
- }
204
- const id = randomUUID();
205
- const platform = ['iOS', 'Android', 'Windows', 'macOS', 'Browser'].includes(body.platform) ? body.platform : 'Browser';
206
- const device = {
207
- id,
208
- platform,
209
- registrationId,
210
- connectedAt: Date.now(),
211
- name: `${platform} · ${this.devices.size + 1}`,
212
- lastSeen: Date.now(),
213
- sequence: 0,
214
- token: randomBytes(32).toString('hex'),
215
- windowStart: Date.now(),
216
- count: 0,
217
- };
218
- this.devices.set(id, device);
219
- reply(200, { id, token: device.token });
220
- return;
221
- }
222
- const device = this.devices.get(body.id);
223
- if (!device || req.headers['x-hb-log-token'] !== device.token) {
224
- reply(403, {});
225
- return;
226
- }
227
- if (!Array.isArray(body.logs) || body.logs.length > 50) {
228
- reply(400, {});
229
- return;
230
- }
231
- if (Date.now() - device.windowStart > 1000) {
232
- device.windowStart = Date.now();
233
- device.count = 0;
234
- }
235
- if (device.count + body.logs.length > 200) {
236
- reply(429, {});
237
- return;
238
- }
239
- device.count += body.logs.length;
240
- device.lastSeen = Date.now();
241
- if (body.sandboxResult && typeof body.sandboxResult === 'object') {
242
- const result = body.sandboxResult;
243
- const pending = this.sandboxRequests.get(result.requestId);
244
- if (pending?.id === device.id) {
245
- 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 })) : [];
246
- pending.resolve({ deviceSessionId: device.id, requestId: pending.requestId, path: pending.path, timestamp: Date.now(), entries, error: result.error ? '设备不支持或无权读取此目录' : undefined, truncated: result.truncated === true });
247
- }
248
- }
249
- for (const log of body.logs) {
250
- if (!log || typeof log !== 'object')
251
- continue;
252
- if (!Number.isSafeInteger(log.sequence) ||
253
- log.sequence <= device.sequence ||
254
- !Number.isFinite(log.timestamp) ||
255
- !['log', 'debug', 'info', 'warn', 'error'].includes(log.level) ||
256
- typeof log.message !== 'string')
257
- continue;
258
- device.sequence = log.sequence;
259
- this.logs.push({
260
- cursor: ++this.cursor,
261
- deviceSessionId: device.id,
262
- sequence: log.sequence,
263
- timestamp: log.timestamp,
264
- receivedAt: Date.now(),
265
- level: log.level,
266
- source: log.source === 'sdk' ? 'sdk' : 'app',
267
- message: sanitizeDiagnosticMessage(log.message)
268
- .replace(/(authorization|cookie|password|secret|token|pkey|api[_-]?key)(["'\s:=]+)([^\s,}]+)/gi, '$1$2[redacted]')
269
- .slice(0, 8192),
270
- truncated: log.truncated === true || log.message.length > 8192,
271
- ...(typeof log.method === 'string' ? { method: log.method.slice(0, 100) } : {}),
272
- ...(typeof log.callId === 'string' ? { callId: log.callId.slice(0, 100) } : {}),
273
- ...(typeof log.errorCode === 'string' && /^[A-Z0-9_]{1,100}$/.test(log.errorCode) ? { errorCode: log.errorCode } : {}),
274
- ...(log.outcome === 'success' || log.outcome === 'error' ? { outcome: log.outcome } : {}),
275
- ...(Number.isFinite(log.durationMs) ? { durationMs: Math.max(0, log.durationMs) } : {}),
276
- });
277
- }
278
- if (this.logs.length > 5000)
279
- this.logs.splice(0, this.logs.length - 5000);
280
- 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 });
281
- }
282
- catch {
283
- reply(400, { code: 'INVALID_LOG_BATCH' });
284
- }
285
- }
286
- }
287
- /** 同源开发页面使用有界批量回传;传输失败不会递归写入 console。 */
288
- function deviceLogBootstrap(token) {
289
- return `<script>(function(){if(window.__hb_sdk_device_logs_installed__)return;
290
- window.__hb_sdk_device_logs_installed__=true;
291
- window.__hb_sdk_mini_dev_console_installed__=true;
292
- var endpoint='${DEVICE_LOG_PATH}',credential='${token}',id='',queue=[],seq=0,busy=false,dropped=0;
293
- var registrationId=Array.from(crypto.getRandomValues(new Uint8Array(16)),function(n){return n.toString(16).padStart(2,'0')}).join('');
294
- var send=window.fetch.bind(window);
295
- var sandboxResult;
296
- 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();
297
- dropped++}var message=args.map(format).join(' ');
298
- message=message.replace(/https?:\\/\\/[^\\s]+/g,function(raw){try{var url=new URL(raw);
299
- url.username='';
300
- url.password='';
301
- url.search='';
302
- url.hash='';
303
- return url.href}catch(e){return '[URL]'}});
304
- message=message.replace(/(authorization|cookie|password|secret|token|pkey|api[_-]?key)([\\"'\\s:=]+)([^\\s,}]+)/gi,'$1$2[redacted]');
305
- 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');
306
- Object.assign(queue[queue.length-1],event)};
307
- ['log','debug','info','warn','error'].forEach(function(level){var original=console[level];
308
- console[level]=function(){add(level,Array.prototype.slice.call(arguments));
309
- return original.apply(this,arguments)}});
310
- window.addEventListener('error',function(e){add('error',[e.message])});
311
- window.addEventListener('unhandledrejection',function(e){add('error',[e.reason])});
312
- async function flush(){if(busy)return;
313
- busy=true;
314
- try{if(!id){var ua=navigator.userAgent;
315
- 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';
316
- 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)});
317
- if(!r.ok)return;
318
- var d=await r.json();
319
- id=d.id;
320
- credential=d.token}if(dropped){add('warn',['日志缓冲已丢弃 '+dropped+' 条']);
321
- dropped=0}var batch=queue.slice(0,50);
322
- while(batch.length>1&&new TextEncoder().encode(JSON.stringify(batch)).length>48000)batch.pop();
323
- 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)});
324
- if(result.ok){var ack=await result.json();sandboxResult=undefined;
325
- 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}}}
326
- queue=queue.filter(function(l){return l.sequence>ack.sequence})}}catch(e){}finally{busy=false}}setInterval(flush,1000);
327
- flush()})();
328
- </script>`;
329
- }
330
-
331
10
  var re = {exports: {}};
332
11
 
333
12
  var constants;
@@ -3111,7 +2790,480 @@ function requireSemver () {
3111
2790
  return semver;
3112
2791
  }
3113
2792
 
3114
- var semverExports = requireSemver();
2793
+ var semverExports = requireSemver();
2794
+
2795
+ const BASIC_INFO_MAX_BYTES = 10 * 1024 * 1024;
2796
+ const BASIC_INFO_MAX_PIXELS = 40_000_000;
2797
+ /** 按 NFKC 和 Unicode 空白规范化名称,并校验 1–40 个字符及禁用字符。 */
2798
+ function normalizeBasicInfoName(raw) {
2799
+ if (typeof raw !== 'string')
2800
+ throw new Error('heybox.name 必须是字符串');
2801
+ // 与 Go TrimSpace 一致(JS trim 额外移除 BOM,故使用 Unicode White_Space)。
2802
+ const name = raw.normalize('NFKC').replace(/^\p{White_Space}+|\p{White_Space}+$/gu, '');
2803
+ if (Array.from(name).length < 1 || Array.from(name).length > 40 || /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}<>&"']/u.test(name) || !/[\p{L}\p{N}]/u.test(name)) {
2804
+ throw new Error('名称须包含 1–40 个字符及文字或数字,不能包含控制字符或 HTML 特殊字符');
2805
+ }
2806
+ return name;
2807
+ }
2808
+ function validateBasicInfoPath(raw) {
2809
+ if (typeof raw !== 'string' || !raw || /[\\:%?#\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(raw))
2810
+ throw new Error('资料图片必须是项目内的规范相对路径');
2811
+ const forbidden = new Set(['', '.', '..', '.git', 'node_modules', 'dist', '.hb-sdk', '.vite', '.cache', '.tmp', 'tmp']);
2812
+ if (raw.split('/').some((part) => forbidden.has(part)))
2813
+ throw new Error('资料图片路径包含禁止的目录');
2814
+ if (!/\.(png|jpe?g|gif|webp)$/i.test(raw))
2815
+ throw new Error('资料图片仅支持 JPEG、PNG、GIF、WebP');
2816
+ return raw;
2817
+ }
2818
+ /** 解析项目或构建资料;仅早于 0.8.2 且三项全部缺失时返回 undefined。 */
2819
+ function parseMiniappBasicInfo(value, sdkVersion = '0.8.2') {
2820
+ sdkVersion = sdkVersion.trim();
2821
+ if (!semverExports.valid(sdkVersion) || sdkVersion.startsWith('v') || sdkVersion.includes('+'))
2822
+ throw new Error('manifest.sdkVersion 必须是合法 SemVer');
2823
+ const fields = ['name', 'icon', 'coverImages'];
2824
+ const present = fields.filter((key) => Object.prototype.hasOwnProperty.call(value, key));
2825
+ if (!present.length && semverExports.lt(sdkVersion, '0.8.2'))
2826
+ return undefined;
2827
+ if (present.length !== fields.length)
2828
+ throw new Error('请配置 heybox.name、heybox.icon、heybox.coverImages,或运行 remote bind/dev/remote deploy 迁移已发布资料');
2829
+ const name = normalizeBasicInfoName(value.name);
2830
+ const icon = validateBasicInfoPath(value.icon);
2831
+ if (!Array.isArray(value.coverImages) || value.coverImages.length < 1 || value.coverImages.length > 5)
2832
+ throw new Error('coverImages 必须包含 1–5 张图片');
2833
+ const coverImages = value.coverImages.map(validateBasicInfoPath);
2834
+ if (new Set(coverImages).size !== coverImages.length)
2835
+ throw new Error('封面路径不能重复');
2836
+ return { name, icon, coverImages };
2837
+ }
2838
+
2839
+ async function inspectBasicInfoImage(bytes, sourcePath, kind) {
2840
+ const inspected = await inspectDecodedBasicInfoImage(bytes, kind);
2841
+ const extensions = { jpg: ['jpg', 'jpeg'], png: ['png'], gif: ['gif'], webp: ['webp'] };
2842
+ const ext = sourcePath.split('.').pop().toLowerCase();
2843
+ if (!extensions[inspected.extension]?.includes(ext))
2844
+ throw new Error('资料图片格式与扩展名不一致');
2845
+ return inspected;
2846
+ }
2847
+ async function inspectDecodedBasicInfoImage(bytes, kind) {
2848
+ if (!bytes.length || bytes.length > BASIC_INFO_MAX_BYTES)
2849
+ throw new Error('资料图片不得为空或超过 10 MiB');
2850
+ const decoder = sharp(bytes, { limitInputPixels: BASIC_INFO_MAX_PIXELS, failOn: 'warning' });
2851
+ const metadata = await decoder.metadata();
2852
+ const extensions = { jpeg: 'jpg', png: 'png', gif: 'gif', webp: 'webp' };
2853
+ if (!metadata.format || !extensions[metadata.format])
2854
+ throw new Error('资料图片格式不支持');
2855
+ const width = metadata.width ?? 0;
2856
+ const height = metadata.pageHeight ?? metadata.height ?? 0;
2857
+ if (!width || !height || width * height > BASIC_INFO_MAX_PIXELS)
2858
+ throw new Error('资料图片不得超过 4000 万像素');
2859
+ if (kind === 'icon' && (width !== 200 || height !== 200))
2860
+ throw new Error('icon 必须是 200×200');
2861
+ // 强制解码而非只信任图片头;不将解码结果写回图片。
2862
+ await decoder.stats();
2863
+ return { sha256: createHash('sha256').update(bytes).digest('hex'), extension: extensions[metadata.format] };
2864
+ }
2865
+ async function readBasicInfoSource(projectRoot, relative, kind) {
2866
+ validateBasicInfoPath(relative);
2867
+ const root = await realpath(projectRoot);
2868
+ const target = await realpath(path.join(root, relative));
2869
+ const resolvedRelative = path.relative(root, target).split(path.sep).join('/');
2870
+ validateBasicInfoPath(resolvedRelative);
2871
+ if (resolvedRelative.startsWith('../') || path.isAbsolute(resolvedRelative))
2872
+ throw new Error('资料图片不能逃逸项目目录');
2873
+ const info = await stat(target);
2874
+ if (!info.isFile() || info.size > BASIC_INFO_MAX_BYTES)
2875
+ throw new Error('资料图片必须为不超过 10 MiB 的普通文件');
2876
+ const file = await open(target, constants$1.O_RDONLY | constants$1.O_NOFOLLOW);
2877
+ try {
2878
+ const actual = await file.stat();
2879
+ if (!actual.isFile() || actual.size > BASIC_INFO_MAX_BYTES)
2880
+ throw new Error('资料图片文件已变化');
2881
+ const bytes = await file.readFile();
2882
+ return { bytes, ...(await inspectBasicInfoImage(bytes, relative, kind)) };
2883
+ }
2884
+ finally {
2885
+ await file.close();
2886
+ }
2887
+ }
2888
+ async function prepareMiniappBasicInfo(projectRoot, outputRoot, sdkVersion) {
2889
+ const pkg = JSON.parse(await readFile(path.join(projectRoot, 'package.json'), 'utf8'));
2890
+ const profile = parseMiniappBasicInfo(pkg.heybox ?? {}, sdkVersion);
2891
+ if (!profile)
2892
+ return undefined;
2893
+ await mkdir(outputRoot, { recursive: true });
2894
+ const output = { ...profile, coverImages: [] };
2895
+ const entries = [
2896
+ { path: profile.icon, kind: 'icon', label: 'icon' },
2897
+ ...profile.coverImages.map((p, i) => ({ path: p, kind: 'cover', label: `cover-${String(i + 1).padStart(2, '0')}` })),
2898
+ ];
2899
+ for (const entry of entries) {
2900
+ const sourceReal = await realpath(path.join(projectRoot, entry.path));
2901
+ const outputReal = await realpath(outputRoot);
2902
+ const fromOutput = path.relative(outputReal, sourceReal);
2903
+ if (!fromOutput.startsWith('..') && !path.isAbsolute(fromOutput))
2904
+ throw new Error('资料源文件不能位于构建输出目录');
2905
+ const image = await readBasicInfoSource(projectRoot, entry.path, entry.kind);
2906
+ const relative = `_hb/basic-info/${entry.label}.${image.sha256.slice(0, 12)}.${image.extension}`;
2907
+ const target = path.join(outputRoot, relative);
2908
+ await mkdir(outputRoot, { recursive: true });
2909
+ await ensureBasicInfoDirectory(outputRoot, '_hb/basic-info');
2910
+ // 全字节比较保证截断哈希冲突和 public 保留路径冲突不会静默覆盖。
2911
+ try {
2912
+ await writeFile(target, image.bytes, { flag: 'wx' });
2913
+ }
2914
+ catch (error) {
2915
+ if (error.code !== 'EEXIST' ||
2916
+ !(await readBasicInfoSource(outputRoot, relative, entry.kind).then((existing) => existing.bytes.equals(image.bytes), () => false)))
2917
+ throw new Error('构建资料保留目录冲突,请移除未知文件');
2918
+ }
2919
+ if (entry.kind === 'icon')
2920
+ output.icon = relative;
2921
+ else
2922
+ output.coverImages.push(relative);
2923
+ }
2924
+ return output;
2925
+ }
2926
+ /** 逐级校验真实目录,避免保留目录或迁移目录中的符号链接将写入带到项目外。 */
2927
+ async function ensureBasicInfoDirectory(rootPath, relative) {
2928
+ const root = await realpath(rootPath);
2929
+ let current = root;
2930
+ for (const part of relative.split('/')) {
2931
+ if (!part || part === '.' || part === '..')
2932
+ throw new Error('资料目录不合法');
2933
+ current = path.join(current, part);
2934
+ await mkdir(current).catch((error) => {
2935
+ if (error.code !== 'EEXIST')
2936
+ throw error;
2937
+ });
2938
+ const resolved = await realpath(current);
2939
+ const within = path.relative(root, resolved);
2940
+ if (within.startsWith('..') || path.isAbsolute(within))
2941
+ throw new Error('资料目录不能逃逸项目');
2942
+ }
2943
+ }
2944
+
2945
+ /** 构建时替换为当前发布包的实际版本。 */
2946
+ const HB_SDK_VERSION = typeof undefined === 'string'
2947
+ ? undefined
2948
+ : '0.8.2-alpha.6';
2949
+
2950
+ /**
2951
+ * iframe 与调试台页面的开发期 console 捕获转发。
2952
+ *
2953
+ * 双条件门控(iframe 转发):
2954
+ * 1. 构建期:`hb-sdk dev`(vite serve)注入 `false`;`hb-sdk build` /
2955
+ * `vite build`(生产)永不注入或替换为 `false`,本模块被死代码消除。
2956
+ * 2. 运行期:仅在 `window.name` 为 `__hb_sdk_mini_dev__` 的调试 iframe 中激活,
2957
+ * 用户设备上的正式运行环境没有该标记。
2958
+ *
2959
+ * 无侵入 patch `console.log/debug/info/warn/error`:原样放行(DevTools 不受影响)+
2960
+ * 通过 `window.parent.postMessage` 转发给调试台(CSP 的 connect-src 不约束 postMessage,
2961
+ * 网络上报由调试台页面代理)。发送全程静默:失败不得回打 console,避免自触发死循环。
2962
+ *
2963
+ * 激活时机:由 SDK 默认单例初始化时调用(业务 `import '@heybox/hb-sdk'` 即生效);
2964
+ * iframe 标记由调试台 `iframe.name` 与 vite dev 注入的无依赖 bootstrap 共同设置。
2965
+ * 调试台页面自身(runtime 跑在这里,`[hb-sdk-runtime]` 日志不经过 iframe)另用
2966
+ * `installMiniDevConsoleSelfCapture` 接入「日志」页签(source: 'host')。
2967
+ */
2968
+ const MINI_DEV_CONSOLE_EVENT_TYPE = 'hb-sdk:mini-dev-console';
2969
+ /** 调试台页面与 iframe bootstrap 共用的已安装标记(window/console 上的属性名)。 */
2970
+ const MINI_DEV_CONSOLE_INSTALL_FLAG = '__hb_sdk_mini_dev_console_installed__';
2971
+
2972
+ /**
2973
+ * network.request 诊断日志的纯脱敏工具。
2974
+ *
2975
+ * @remarks
2976
+ * 放在 `core` 而非 `modules/network`:无错误类型依赖,供 SDK 错误文案、
2977
+ * modules observability、devtools Browser Dev Host / proxy 共用,并满足 boundary
2978
+ *(devtools 不得直接 import runtime capability modules)。
2979
+ */
2980
+ /** 敏感字段名:header / query / body key 共用。 */
2981
+ 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;
2982
+ /**
2983
+ * 脱敏请求 URL:去掉 userinfo,并将 query / fragment 中的敏感参数值替换为 `[redacted]`。
2984
+ */
2985
+ function sanitizeNetworkRequestUrl(url) {
2986
+ if (!url) {
2987
+ return url;
2988
+ }
2989
+ try {
2990
+ const parsed = new URL(url);
2991
+ parsed.username = '';
2992
+ parsed.password = '';
2993
+ redactUrlSearchParams(parsed.searchParams);
2994
+ if (parsed.hash && parsed.hash.length > 1) {
2995
+ const hashBody = parsed.hash.slice(1);
2996
+ if (hashBody.includes('=')) {
2997
+ const hashParams = new URLSearchParams(hashBody);
2998
+ if (redactUrlSearchParams(hashParams)) {
2999
+ parsed.hash = hashParams.toString();
3000
+ }
3001
+ }
3002
+ }
3003
+ return parsed.toString();
3004
+ }
3005
+ catch {
3006
+ return redactSensitivePairsInRawText(url);
3007
+ }
3008
+ }
3009
+ /**
3010
+ * 诊断 message 中的 URL 片段做脱敏,避免 error.message 二次泄漏 query secrets。
3011
+ */
3012
+ function sanitizeDiagnosticMessage(message) {
3013
+ if (!message) {
3014
+ return message;
3015
+ }
3016
+ return message.replace(/https?:\/\/[^\s]+/gi, matched => sanitizeNetworkRequestUrl(matched.replace(/[),.;]+$/g, '')));
3017
+ }
3018
+ function isSensitiveNetworkKey(key) {
3019
+ return SENSITIVE_KEY_PATTERN.test(key.trim());
3020
+ }
3021
+ function redactUrlSearchParams(params) {
3022
+ let changed = false;
3023
+ for (const key of [...params.keys()]) {
3024
+ if (isSensitiveNetworkKey(key)) {
3025
+ params.set(key, '[redacted]');
3026
+ changed = true;
3027
+ }
3028
+ }
3029
+ return changed;
3030
+ }
3031
+ function redactSensitivePairsInRawText(text) {
3032
+ return text.replace(/([?&#]|^|[?&])([^=&#\s]+)=([^&#\s]*)/g, (full, prefix, key, value) => {
3033
+ if (!isSensitiveNetworkKey(key)) {
3034
+ return full;
3035
+ }
3036
+ return `${prefix}${key}=[redacted]`;
3037
+ });
3038
+ }
3039
+
3040
+ const DEVICE_LOG_PATH = '/__hb_sdk__/device-logs';
3041
+ const registryKey = Symbol.for('@heybox/hb-sdk/device-logs');
3042
+ const registry = globalThis;
3043
+ const stores = (registry[registryKey] ??= new Map());
3044
+ function registerDeviceLogs(port, store) {
3045
+ stores.set(port, store);
3046
+ return () => {
3047
+ if (stores.get(port) === store)
3048
+ stores.delete(port);
3049
+ };
3050
+ }
3051
+ class DeviceLogStore {
3052
+ sandboxRequests = new Map();
3053
+ listSandbox(id, path) {
3054
+ const device = this.devices.get(id);
3055
+ if (!device || Date.now() - device.lastSeen > 15000)
3056
+ return Promise.resolve({ error: '设备已离线' });
3057
+ if (path !== '.' && (!path || path.length > 1024 || /[\\\\\u0000-\u001f]/.test(path) || path.split('/').some(part => !part || part === '.' || part === '..')))
3058
+ return Promise.resolve({ error: '无效目录路径' });
3059
+ if (this.sandboxRequests.size >= 64)
3060
+ return Promise.resolve({ error: '查询繁忙,请稍后重试' });
3061
+ const requestId = randomUUID();
3062
+ return new Promise(resolve => {
3063
+ const timer = setTimeout(() => { this.sandboxRequests.delete(requestId); resolve({ error: '设备查询超时' }); }, 10000);
3064
+ this.sandboxRequests.set(requestId, { id, path, requestId, resolve: value => { clearTimeout(timer); this.sandboxRequests.delete(requestId); resolve(value); } });
3065
+ });
3066
+ }
3067
+ uploadToken = randomBytes(32).toString('hex');
3068
+ epoch = randomUUID();
3069
+ expiresAt = Date.now() + 12 * 60 * 60 * 1000;
3070
+ cursor = 0;
3071
+ logs = [];
3072
+ devices = new Map();
3073
+ query(params) {
3074
+ const after = Math.max(0, Number(params.get('after')) || 0);
3075
+ const limit = Math.min(500, Math.max(1, Number(params.get('limit')) || 100));
3076
+ const selected = this.logs.filter((log) => log.cursor > after &&
3077
+ (!params.get('device') || log.deviceSessionId === params.get('device')) &&
3078
+ (!params.get('level') || log.level === params.get('level')) &&
3079
+ (!params.get('source') || log.source === params.get('source')) &&
3080
+ (!params.get('callId') || log.callId === params.get('callId')) &&
3081
+ (!params.get('id') || String(log.cursor) === params.get('id')) &&
3082
+ (!params.get('since') || log.timestamp >= Number(params.get('since'))) &&
3083
+ (!params.get('query') || log.message.toLowerCase().includes(params.get('query').toLowerCase())));
3084
+ const items = selected.slice(0, limit);
3085
+ return {
3086
+ schemaVersion: 1,
3087
+ epoch: this.epoch,
3088
+ items,
3089
+ nextCursor: selected.length > limit ? items.at(-1).cursor : this.cursor,
3090
+ hasMore: selected.length > limit,
3091
+ gap: after > 0 && after < (this.logs[0]?.cursor ?? 1) - 1,
3092
+ devices: [...this.devices.values()].map(({ id, name, platform, connectedAt, lastSeen }) => ({
3093
+ id,
3094
+ name,
3095
+ platform,
3096
+ connectedAt,
3097
+ lastSeen,
3098
+ online: Date.now() - lastSeen < 15000,
3099
+ })),
3100
+ };
3101
+ }
3102
+ async upload(req, res) {
3103
+ const reply = (status, value) => {
3104
+ res.writeHead(status, { 'content-type': 'application/json', 'cache-control': 'no-store' });
3105
+ res.end(JSON.stringify(value));
3106
+ };
3107
+ if (req.method !== 'POST') {
3108
+ reply(405, {});
3109
+ return;
3110
+ }
3111
+ if (Date.now() > this.expiresAt) {
3112
+ reply(410, { code: 'EXPIRED' });
3113
+ return;
3114
+ }
3115
+ let text = '';
3116
+ try {
3117
+ req.setEncoding('utf8');
3118
+ for await (const chunk of req) {
3119
+ text += chunk;
3120
+ if (Buffer.byteLength(text) > 65536) {
3121
+ reply(413, {});
3122
+ return;
3123
+ }
3124
+ }
3125
+ const body = JSON.parse(text);
3126
+ if (body.register === true) {
3127
+ if (req.headers['x-hb-log-token'] !== this.uploadToken) {
3128
+ reply(403, {});
3129
+ return;
3130
+ }
3131
+ const registrationId = typeof body.registrationId === 'string' && /^[a-f0-9]{32}$/.test(body.registrationId) ? body.registrationId : undefined;
3132
+ const existing = registrationId && [...this.devices.values()].find(device => device.registrationId === registrationId);
3133
+ if (existing) {
3134
+ reply(200, { id: existing.id, token: existing.token });
3135
+ return;
3136
+ }
3137
+ if (this.devices.size >= 64) {
3138
+ reply(429, {});
3139
+ return;
3140
+ }
3141
+ const id = randomUUID();
3142
+ const platform = ['iOS', 'Android', 'Windows', 'macOS', 'Browser'].includes(body.platform) ? body.platform : 'Browser';
3143
+ const device = {
3144
+ id,
3145
+ platform,
3146
+ registrationId,
3147
+ connectedAt: Date.now(),
3148
+ name: `${platform} · ${this.devices.size + 1}`,
3149
+ lastSeen: Date.now(),
3150
+ sequence: 0,
3151
+ token: randomBytes(32).toString('hex'),
3152
+ windowStart: Date.now(),
3153
+ count: 0,
3154
+ };
3155
+ this.devices.set(id, device);
3156
+ reply(200, { id, token: device.token });
3157
+ return;
3158
+ }
3159
+ const device = this.devices.get(body.id);
3160
+ if (!device || req.headers['x-hb-log-token'] !== device.token) {
3161
+ reply(403, {});
3162
+ return;
3163
+ }
3164
+ if (!Array.isArray(body.logs) || body.logs.length > 50) {
3165
+ reply(400, {});
3166
+ return;
3167
+ }
3168
+ if (Date.now() - device.windowStart > 1000) {
3169
+ device.windowStart = Date.now();
3170
+ device.count = 0;
3171
+ }
3172
+ if (device.count + body.logs.length > 200) {
3173
+ reply(429, {});
3174
+ return;
3175
+ }
3176
+ device.count += body.logs.length;
3177
+ device.lastSeen = Date.now();
3178
+ if (body.sandboxResult && typeof body.sandboxResult === 'object') {
3179
+ const result = body.sandboxResult;
3180
+ const pending = this.sandboxRequests.get(result.requestId);
3181
+ if (pending?.id === device.id) {
3182
+ 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 })) : [];
3183
+ pending.resolve({ deviceSessionId: device.id, requestId: pending.requestId, path: pending.path, timestamp: Date.now(), entries, error: result.error ? '设备不支持或无权读取此目录' : undefined, truncated: result.truncated === true });
3184
+ }
3185
+ }
3186
+ for (const log of body.logs) {
3187
+ if (!log || typeof log !== 'object')
3188
+ continue;
3189
+ if (!Number.isSafeInteger(log.sequence) ||
3190
+ log.sequence <= device.sequence ||
3191
+ !Number.isFinite(log.timestamp) ||
3192
+ !['log', 'debug', 'info', 'warn', 'error'].includes(log.level) ||
3193
+ typeof log.message !== 'string')
3194
+ continue;
3195
+ device.sequence = log.sequence;
3196
+ this.logs.push({
3197
+ cursor: ++this.cursor,
3198
+ deviceSessionId: device.id,
3199
+ sequence: log.sequence,
3200
+ timestamp: log.timestamp,
3201
+ receivedAt: Date.now(),
3202
+ level: log.level,
3203
+ source: log.source === 'sdk' ? 'sdk' : 'app',
3204
+ message: sanitizeDiagnosticMessage(log.message)
3205
+ .replace(/(authorization|cookie|password|secret|token|pkey|api[_-]?key)(["'\s:=]+)([^\s,}]+)/gi, '$1$2[redacted]')
3206
+ .slice(0, 8192),
3207
+ truncated: log.truncated === true || log.message.length > 8192,
3208
+ ...(typeof log.method === 'string' ? { method: log.method.slice(0, 100) } : {}),
3209
+ ...(typeof log.callId === 'string' ? { callId: log.callId.slice(0, 100) } : {}),
3210
+ ...(typeof log.errorCode === 'string' && /^[A-Z0-9_]{1,100}$/.test(log.errorCode) ? { errorCode: log.errorCode } : {}),
3211
+ ...(log.outcome === 'success' || log.outcome === 'error' ? { outcome: log.outcome } : {}),
3212
+ ...(Number.isFinite(log.durationMs) ? { durationMs: Math.max(0, log.durationMs) } : {}),
3213
+ });
3214
+ }
3215
+ if (this.logs.length > 5000)
3216
+ this.logs.splice(0, this.logs.length - 5000);
3217
+ 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 });
3218
+ }
3219
+ catch {
3220
+ reply(400, { code: 'INVALID_LOG_BATCH' });
3221
+ }
3222
+ }
3223
+ }
3224
+ /** 同源开发页面使用有界批量回传;传输失败不会递归写入 console。 */
3225
+ function deviceLogBootstrap(token) {
3226
+ return `<script>(function(){if(window.__hb_sdk_device_logs_installed__)return;
3227
+ window.__hb_sdk_device_logs_installed__=true;
3228
+ window.__hb_sdk_mini_dev_console_installed__=true;
3229
+ var endpoint='${DEVICE_LOG_PATH}',credential='${token}',id='',queue=[],seq=0,busy=false,dropped=0;
3230
+ var registrationId=Array.from(crypto.getRandomValues(new Uint8Array(16)),function(n){return n.toString(16).padStart(2,'0')}).join('');
3231
+ var send=window.fetch.bind(window);
3232
+ var sandboxResult;
3233
+ 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();
3234
+ dropped++}var message=args.map(format).join(' ');
3235
+ message=message.replace(/https?:\\/\\/[^\\s]+/g,function(raw){try{var url=new URL(raw);
3236
+ url.username='';
3237
+ url.password='';
3238
+ url.search='';
3239
+ url.hash='';
3240
+ return url.href}catch(e){return '[URL]'}});
3241
+ message=message.replace(/(authorization|cookie|password|secret|token|pkey|api[_-]?key)([\\"'\\s:=]+)([^\\s,}]+)/gi,'$1$2[redacted]');
3242
+ 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');
3243
+ Object.assign(queue[queue.length-1],event)};
3244
+ ['log','debug','info','warn','error'].forEach(function(level){var original=console[level];
3245
+ console[level]=function(){add(level,Array.prototype.slice.call(arguments));
3246
+ return original.apply(this,arguments)}});
3247
+ window.addEventListener('error',function(e){add('error',[e.message])});
3248
+ window.addEventListener('unhandledrejection',function(e){add('error',[e.reason])});
3249
+ async function flush(){if(busy)return;
3250
+ busy=true;
3251
+ try{if(!id){var ua=navigator.userAgent;
3252
+ 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';
3253
+ 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)});
3254
+ if(!r.ok)return;
3255
+ var d=await r.json();
3256
+ id=d.id;
3257
+ credential=d.token}if(dropped){add('warn',['日志缓冲已丢弃 '+dropped+' 条']);
3258
+ dropped=0}var batch=queue.slice(0,50);
3259
+ while(batch.length>1&&new TextEncoder().encode(JSON.stringify(batch)).length>48000)batch.pop();
3260
+ 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)});
3261
+ if(result.ok){var ack=await result.json();sandboxResult=undefined;
3262
+ 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}}}
3263
+ queue=queue.filter(function(l){return l.sequence>ack.sequence})}}catch(e){}finally{busy=false}}setInterval(flush,1000);
3264
+ flush()})();
3265
+ </script>`;
3266
+ }
3115
3267
 
3116
3268
  /** 单个小程序版本最多声明的关联游戏数量。 */
3117
3269
  const MINIAPP_BIND_APP_IDS_MAX_COUNT = 20;
@@ -3454,6 +3606,7 @@ function renderMiniappManifest(manifest) {
3454
3606
  const output = {
3455
3607
  version: manifest.version,
3456
3608
  sdkVersion: manifest.sdkVersion,
3609
+ ...(manifest.name === undefined ? {} : { name: manifest.name, icon: manifest.icon, coverImages: manifest.coverImages }),
3457
3610
  platforms,
3458
3611
  ...(manifest.permissions === undefined ? {} : { permissions: manifest.permissions }),
3459
3612
  ...(manifest.companions === undefined ? {} : { companions: manifest.companions }),
@@ -12819,29 +12972,29 @@ function renderRuntimeGateScript() {
12819
12972
  function removePlatformGateNodes(node) {
12820
12973
  if (!('childNodes' in node))
12821
12974
  return;
12822
- node.childNodes = node.childNodes.filter(child => !('tagName' in child) || !hasAttr(child, RUNTIME_GATE_MARKER));
12975
+ node.childNodes = node.childNodes.filter((child) => !('tagName' in child) || !hasAttr(child, RUNTIME_GATE_MARKER));
12823
12976
  for (const child of node.childNodes)
12824
12977
  removePlatformGateNodes(child);
12825
12978
  }
12826
12979
  function readAttr(element, name) {
12827
- return element.attrs.find(attr => attr.name.toLowerCase() === name)?.value;
12980
+ return element.attrs.find((attr) => attr.name.toLowerCase() === name)?.value;
12828
12981
  }
12829
12982
  function hasAttr(element, name) {
12830
12983
  return readAttr(element, name) !== undefined;
12831
12984
  }
12832
12985
  function setAttr(element, name, value) {
12833
- const attr = element.attrs.find(item => item.name.toLowerCase() === name);
12986
+ const attr = element.attrs.find((item) => item.name.toLowerCase() === name);
12834
12987
  if (attr)
12835
12988
  attr.value = value;
12836
12989
  else
12837
12990
  element.attrs.push({ name, value });
12838
12991
  }
12839
12992
  function removeAttr(element, name) {
12840
- element.attrs = element.attrs.filter(attr => attr.name.toLowerCase() !== name);
12993
+ element.attrs = element.attrs.filter((attr) => attr.name.toLowerCase() !== name);
12841
12994
  }
12842
12995
  function findElement$1(node, tagName) {
12843
12996
  let found;
12844
- walk$1(node, element => {
12997
+ walk$1(node, (element) => {
12845
12998
  if (!found && element.tagName === tagName)
12846
12999
  found = element;
12847
13000
  });
@@ -12860,10 +13013,7 @@ function walk$1(node, visit) {
12860
13013
  const FORBIDDEN_RESOURCE_HINTS = new Set(['dns-prefetch', 'preconnect', 'prerender']);
12861
13014
  const RELATIVE_URL_BASE = new URL('https://heybox-relative.invalid/');
12862
13015
  const OFFICIAL_RESOURCE_ROOT_DOMAINS = ['xiaoheihe.cn', 'max-c.com', 'debugmode.cn', 'maxjia.com'];
12863
- const OFFICIAL_RESOURCE_SOURCES = OFFICIAL_RESOURCE_ROOT_DOMAINS.flatMap(rootDomain => [
12864
- `https://${rootDomain}`,
12865
- `https://*.${rootDomain}`,
12866
- ]);
13016
+ const OFFICIAL_RESOURCE_SOURCES = OFFICIAL_RESOURCE_ROOT_DOMAINS.flatMap((rootDomain) => [`https://${rootDomain}`, `https://*.${rootDomain}`]);
12867
13017
  const URL_ATTRIBUTES = {
12868
13018
  audio: ['src'],
12869
13019
  embed: ['src'],
@@ -12900,14 +13050,12 @@ function enforceMiniappHtmlPolicy(html, options = {}) {
12900
13050
  throw new Error('入口 index.html 必须包含 <head>');
12901
13051
  }
12902
13052
  removePlatformCspMarkers(document);
12903
- walk(document, element => validateElement(element));
13053
+ walk(document, (element) => validateElement(element));
12904
13054
  injectMiniappRuntimeGate(document);
12905
13055
  if (options.skipPlatformCsp) {
12906
13056
  return serialize(document);
12907
13057
  }
12908
- const content = options.hmrWebSocketUrl
12909
- ? PRODUCTION_CSP.replace("connect-src 'none'", `connect-src ${options.hmrWebSocketUrl}`)
12910
- : PRODUCTION_CSP;
13058
+ const content = options.hmrWebSocketUrl ? PRODUCTION_CSP.replace("connect-src 'none'", `connect-src ${options.hmrWebSocketUrl}`) : PRODUCTION_CSP;
12911
13059
  head.childNodes.unshift({
12912
13060
  nodeName: 'meta',
12913
13061
  tagName: 'meta',
@@ -12925,7 +13073,7 @@ function enforceMiniappHtmlPolicy(html, options = {}) {
12925
13073
  }
12926
13074
  function validateElement(element) {
12927
13075
  const tagName = element.tagName.toLowerCase();
12928
- const attrs = new Map(element.attrs.map(attr => [attr.name.toLowerCase(), attr.value]));
13076
+ const attrs = new Map(element.attrs.map((attr) => [attr.name.toLowerCase(), attr.value]));
12929
13077
  if (tagName === 'meta' && attrs.get('http-equiv')?.toLowerCase() === 'refresh') {
12930
13078
  throw new Error('index.html 不允许 meta refresh');
12931
13079
  }
@@ -12937,7 +13085,7 @@ function validateElement(element) {
12937
13085
  }
12938
13086
  if (tagName === 'link') {
12939
13087
  const rels = (attrs.get('rel') ?? '').toLowerCase().split(/\s+/).filter(Boolean);
12940
- const forbidden = rels.find(rel => FORBIDDEN_RESOURCE_HINTS.has(rel));
13088
+ const forbidden = rels.find((rel) => FORBIDDEN_RESOURCE_HINTS.has(rel));
12941
13089
  if (forbidden) {
12942
13090
  throw new Error(`index.html 不允许 rel="${forbidden}"`);
12943
13091
  }
@@ -12954,7 +13102,10 @@ function validateElement(element) {
12954
13102
  for (const attribute of URL_ATTRIBUTES[tagName] ?? []) {
12955
13103
  const value = attrs.get(attribute);
12956
13104
  if (attribute === 'srcset') {
12957
- for (const candidate of (value ?? '').split(',').map(item => item.trim().split(/\s+/)[0]).filter(Boolean)) {
13105
+ for (const candidate of (value ?? '')
13106
+ .split(',')
13107
+ .map((item) => item.trim().split(/\s+/)[0])
13108
+ .filter(Boolean)) {
12958
13109
  validateResourceUrl(candidate, `<${tagName} ${attribute}>`, true, supportsOfficialResourceUrl(tagName, attribute));
12959
13110
  }
12960
13111
  }
@@ -12966,10 +13117,10 @@ function validateElement(element) {
12966
13117
  function removePlatformCspMarkers(node) {
12967
13118
  if (!('childNodes' in node))
12968
13119
  return;
12969
- node.childNodes = node.childNodes.filter(child => {
13120
+ node.childNodes = node.childNodes.filter((child) => {
12970
13121
  if (!('tagName' in child))
12971
13122
  return true;
12972
- return !child.attrs.some(attr => attr.name.toLowerCase() === 'data-heybox-platform-csp');
13123
+ return !child.attrs.some((attr) => attr.name.toLowerCase() === 'data-heybox-platform-csp');
12973
13124
  });
12974
13125
  for (const child of node.childNodes)
12975
13126
  removePlatformCspMarkers(child);
@@ -13005,7 +13156,7 @@ function isOfficialResourceUrl(value) {
13005
13156
  const hostname = url.hostname.toLowerCase();
13006
13157
  if (hostname.endsWith('.'))
13007
13158
  return false;
13008
- return OFFICIAL_RESOURCE_ROOT_DOMAINS.some(rootDomain => hostname === rootDomain || hostname.endsWith(`.${rootDomain}`));
13159
+ return OFFICIAL_RESOURCE_ROOT_DOMAINS.some((rootDomain) => hostname === rootDomain || hostname.endsWith(`.${rootDomain}`));
13009
13160
  }
13010
13161
  catch {
13011
13162
  return false;
@@ -13030,7 +13181,7 @@ function isRelativeUrl(value, normalized) {
13030
13181
  }
13031
13182
  function findElement(node, tagName) {
13032
13183
  let found;
13033
- walk(node, element => {
13184
+ walk(node, (element) => {
13034
13185
  if (!found && element.tagName === tagName)
13035
13186
  found = element;
13036
13187
  });
@@ -13537,10 +13688,12 @@ function miniappManifest() {
13537
13688
  throw new Error('构建产物必须包含入口 dist/index.html');
13538
13689
  }
13539
13690
  writeFileSync(indexHtmlPath, enforceMiniappHtmlPolicy(readFileSync(indexHtmlPath, 'utf8'), { skipPlatformCsp }));
13691
+ const basicInfo = await prepareMiniappBasicInfo(companionConfig.projectRoot, outputRoot, sdkVersion);
13540
13692
  const manifestPath = path.join(outputRoot, 'manifest.json');
13541
13693
  const companions = prepareMiniappCompanions(companionDeclarations, { root: companionConfig.projectRoot, outputRoot, platforms });
13542
13694
  mkdirSync(path.dirname(manifestPath), { recursive: true });
13543
13695
  writeFileSync(manifestPath, renderMiniappManifest({
13696
+ ...basicInfo,
13544
13697
  version,
13545
13698
  sdkVersion,
13546
13699
  platforms,