@heybox/hb-sdk 0.8.2-alpha.1 → 0.8.2-alpha.5
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.
- package/CHANGELOG.md +39 -1
- package/README.md +5 -1
- package/dist/cli-chunks/{build-DFwk1Xza.cjs → build-D2T3ky_9.cjs} +5 -5
- package/dist/cli-chunks/{context-DPwjjbIf.cjs → context-DEgmdpSL.cjs} +326 -104
- package/dist/cli-chunks/{create-Cbk708wm.cjs → create-DKqQhrG5.cjs} +5 -1
- package/dist/cli-chunks/{dev-B1dCAh3V.cjs → dev-BEXM7cmq.cjs} +17 -9
- package/dist/cli-chunks/{doctor-KX6cHAbM.cjs → doctor-B7UixGrd.cjs} +1 -1
- package/dist/cli-chunks/{index-B5GNdZS6.cjs → index-BtwfuoWI.cjs} +15 -17
- package/dist/cli-chunks/{index-D1CfvRqX.cjs → index-C69OtRug.cjs} +2 -2
- package/dist/cli-chunks/{index.esm-BvuovqBQ.cjs → index.esm-C86KlVdK.cjs} +11 -10
- package/dist/cli-chunks/{login-CqdIa3XD.cjs → login-Bp_HMx-i.cjs} +2 -2
- package/dist/cli-chunks/{project-vite-Bw3UJTnC.cjs → project-vite-DZxipd13.cjs} +1 -1
- package/dist/cli-chunks/{remote-t0V-fuAW.cjs → remote-B_sVsbQP.cjs} +80 -60
- package/dist/cli-chunks/runtime-gate-GR9jZd37.cjs +58 -0
- package/dist/cli-chunks/{runtime-gate-BWb8QIcX.cjs → runtime-permission-env-COWketgE.cjs} +4533 -40
- package/dist/cli-chunks/{session-DFY2dygN.cjs → session-6-AApcTZ.cjs} +1 -1
- package/dist/cli-chunks/{skill-D3xHcK8w.cjs → skill-CPDLk7t7.cjs} +2 -2
- package/dist/cli-chunks/{version-DkVq4CXN.cjs → version-hHrSPRwW.cjs} +1 -1
- package/dist/cli.cjs +1 -1
- package/dist/devtools/browser-dev-host/assets/browser-dev-host-CpwQQhA1.js +101 -0
- package/dist/devtools/browser-dev-host/assets/{desktop-app-launch-8J62MOi_.js → desktop-app-launch-BdWBOIvY.js} +1 -1
- package/dist/devtools/browser-dev-host/assets/{index-CrsPiYXy.js → index-BQSEynx6.js} +3 -3
- package/dist/devtools/browser-dev-host/index.html +2 -2
- package/dist/index.cjs.js +946 -42
- package/dist/index.esm.js +946 -42
- package/dist/miniapp-publish.cjs.js +9410 -122
- package/dist/miniapp-publish.esm.js +9406 -123
- package/dist/protocol.cjs.js +805 -0
- package/dist/protocol.esm.js +805 -0
- package/dist/templates/vanilla-vite-js/assets/heybox-placeholder.png +0 -0
- package/dist/templates/vanilla-vite-js/package.json.ejs +3 -0
- package/dist/vite.cjs.js +843 -507
- package/dist/vite.esm.js +844 -508
- package/package.json +8 -7
- package/skill/SKILL.md +5 -1
- package/skill/references/api-root.md +194 -9
- package/skill/references/cli.md +3 -3
- package/skill/references/recipes.md +43 -4
- package/skill/skill.json +5 -5
- package/types/core/errors.d.ts +33 -9
- package/types/core/handshake-state.d.ts +32 -2
- package/types/core/singleton.d.ts +265 -6
- package/types/miniapp-manifest/basic-info-browser.d.ts +9 -0
- package/types/miniapp-manifest/basic-info-image-header.d.ts +6 -0
- package/types/miniapp-manifest/basic-info-node.d.ts +17 -0
- package/types/miniapp-manifest/basic-info-placeholder.d.ts +1 -0
- package/types/miniapp-manifest/basic-info.d.ts +12 -0
- package/types/miniapp-manifest/bindings.d.ts +65 -0
- package/types/miniapp-manifest/schema.d.ts +143 -20
- package/types/miniapp-publish/index.d.ts +433 -29
- package/types/modules/auth/index.d.ts +159 -5
- package/types/modules/cloud/index.d.ts +514 -40
- package/types/modules/companion/index.d.ts +1518 -17
- package/types/modules/device/index.d.ts +140 -9
- package/types/modules/environment/index.d.ts +172 -4
- package/types/modules/files/types.d.ts +1396 -21
- package/types/modules/navigation/app-pages/types.d.ts +81 -24
- package/types/modules/navigation/index.d.ts +230 -7
- package/types/modules/network/index.d.ts +297 -26
- package/types/modules/share/copy-link.d.ts +35 -5
- package/types/modules/share/index.d.ts +210 -0
- package/types/modules/share/screenshot.d.ts +23 -5
- package/types/modules/share/show-share-menu.d.ts +18 -5
- package/types/modules/share/types.d.ts +148 -23
- package/types/modules/storage/index.d.ts +139 -9
- package/types/modules/ui/index.d.ts +249 -18
- package/types/modules/user/get-info.d.ts +29 -8
- package/types/modules/user/index.d.ts +157 -0
- package/types/modules/user/revoke-authorization.d.ts +26 -2
- package/types/modules/user/steam-game-list.d.ts +96 -15
- package/types/modules/viewport/index.d.ts +158 -22
- package/types/protocol/types.d.ts +19 -1
- package/types/vite/index.d.ts +159 -1
- package/dist/cli-chunks/runtime-permission-env-BgYjqHIE.cjs +0 -3942
- package/dist/devtools/browser-dev-host/assets/browser-dev-host-DqPIIvha.js +0 -101
- /package/types/{vite → miniapp-manifest}/runtime-gate.d.ts +0 -0
package/dist/vite.cjs.js
CHANGED
|
@@ -1,400 +1,79 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var node_fs = require('node:fs');
|
|
4
3
|
var node_crypto = require('node:crypto');
|
|
5
|
-
var
|
|
4
|
+
var node_fs = require('node:fs');
|
|
5
|
+
var promises = require('node:fs/promises');
|
|
6
6
|
var path = require('node:path');
|
|
7
|
+
var sharp = require('sharp');
|
|
8
|
+
var node_os = require('node:os');
|
|
7
9
|
var node_zlib = require('node:zlib');
|
|
8
10
|
var node_async_hooks = require('node:async_hooks');
|
|
9
11
|
|
|
10
12
|
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
11
|
-
|
|
12
|
-
const HB_SDK_VERSION = typeof undefined === 'string'
|
|
13
|
-
? undefined
|
|
14
|
-
: '0.8.2-alpha.1';
|
|
13
|
+
var re = {exports: {}};
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
*
|
|
19
|
-
* 双条件门控(iframe 转发):
|
|
20
|
-
* 1. 构建期:`hb-sdk dev`(vite serve)注入 `false`;`hb-sdk build` /
|
|
21
|
-
* `vite build`(生产)永不注入或替换为 `false`,本模块被死代码消除。
|
|
22
|
-
* 2. 运行期:仅在 `window.name` 为 `__hb_sdk_mini_dev__` 的调试 iframe 中激活,
|
|
23
|
-
* 用户设备上的正式运行环境没有该标记。
|
|
24
|
-
*
|
|
25
|
-
* 无侵入 patch `console.log/debug/info/warn/error`:原样放行(DevTools 不受影响)+
|
|
26
|
-
* 通过 `window.parent.postMessage` 转发给调试台(CSP 的 connect-src 不约束 postMessage,
|
|
27
|
-
* 网络上报由调试台页面代理)。发送全程静默:失败不得回打 console,避免自触发死循环。
|
|
28
|
-
*
|
|
29
|
-
* 激活时机:由 SDK 默认单例初始化时调用(业务 `import '@heybox/hb-sdk'` 即生效);
|
|
30
|
-
* iframe 标记由调试台 `iframe.name` 与 vite dev 注入的无依赖 bootstrap 共同设置。
|
|
31
|
-
* 调试台页面自身(runtime 跑在这里,`[hb-sdk-runtime]` 日志不经过 iframe)另用
|
|
32
|
-
* `installMiniDevConsoleSelfCapture` 接入「日志」页签(source: 'host')。
|
|
33
|
-
*/
|
|
34
|
-
const MINI_DEV_CONSOLE_EVENT_TYPE = 'hb-sdk:mini-dev-console';
|
|
35
|
-
/** 调试台页面与 iframe bootstrap 共用的已安装标记(window/console 上的属性名)。 */
|
|
36
|
-
const MINI_DEV_CONSOLE_INSTALL_FLAG = '__hb_sdk_mini_dev_console_installed__';
|
|
15
|
+
var constants;
|
|
16
|
+
var hasRequiredConstants;
|
|
37
17
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
});
|
|
18
|
+
function requireConstants () {
|
|
19
|
+
if (hasRequiredConstants) return constants;
|
|
20
|
+
hasRequiredConstants = 1;
|
|
21
|
+
|
|
22
|
+
// Note: this is the semver.org version of the spec that it implements
|
|
23
|
+
// Not necessarily the package version of this code.
|
|
24
|
+
const SEMVER_SPEC_VERSION = '2.0.0';
|
|
25
|
+
|
|
26
|
+
const MAX_LENGTH = 256;
|
|
27
|
+
const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER ||
|
|
28
|
+
/* istanbul ignore next */ 9007199254740991;
|
|
29
|
+
|
|
30
|
+
// Max safe segment length for coercion.
|
|
31
|
+
const MAX_SAFE_COMPONENT_LENGTH = 16;
|
|
32
|
+
|
|
33
|
+
// Max safe length for a build identifier. The max length minus 6 characters for
|
|
34
|
+
// the shortest version with a build 0.0.0+BUILD.
|
|
35
|
+
const MAX_SAFE_BUILD_LENGTH = MAX_LENGTH - 6;
|
|
36
|
+
|
|
37
|
+
const RELEASE_TYPES = [
|
|
38
|
+
'major',
|
|
39
|
+
'premajor',
|
|
40
|
+
'minor',
|
|
41
|
+
'preminor',
|
|
42
|
+
'patch',
|
|
43
|
+
'prepatch',
|
|
44
|
+
'prerelease',
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
constants = {
|
|
48
|
+
MAX_LENGTH,
|
|
49
|
+
MAX_SAFE_COMPONENT_LENGTH,
|
|
50
|
+
MAX_SAFE_BUILD_LENGTH,
|
|
51
|
+
MAX_SAFE_INTEGER,
|
|
52
|
+
RELEASE_TYPES,
|
|
53
|
+
SEMVER_SPEC_VERSION,
|
|
54
|
+
FLAG_INCLUDE_PRERELEASE: 0b001,
|
|
55
|
+
FLAG_LOOSE: 0b010,
|
|
56
|
+
};
|
|
57
|
+
return constants;
|
|
104
58
|
}
|
|
105
59
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
334
|
-
var re = {exports: {}};
|
|
335
|
-
|
|
336
|
-
var constants;
|
|
337
|
-
var hasRequiredConstants;
|
|
338
|
-
|
|
339
|
-
function requireConstants () {
|
|
340
|
-
if (hasRequiredConstants) return constants;
|
|
341
|
-
hasRequiredConstants = 1;
|
|
342
|
-
|
|
343
|
-
// Note: this is the semver.org version of the spec that it implements
|
|
344
|
-
// Not necessarily the package version of this code.
|
|
345
|
-
const SEMVER_SPEC_VERSION = '2.0.0';
|
|
346
|
-
|
|
347
|
-
const MAX_LENGTH = 256;
|
|
348
|
-
const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER ||
|
|
349
|
-
/* istanbul ignore next */ 9007199254740991;
|
|
350
|
-
|
|
351
|
-
// Max safe segment length for coercion.
|
|
352
|
-
const MAX_SAFE_COMPONENT_LENGTH = 16;
|
|
353
|
-
|
|
354
|
-
// Max safe length for a build identifier. The max length minus 6 characters for
|
|
355
|
-
// the shortest version with a build 0.0.0+BUILD.
|
|
356
|
-
const MAX_SAFE_BUILD_LENGTH = MAX_LENGTH - 6;
|
|
357
|
-
|
|
358
|
-
const RELEASE_TYPES = [
|
|
359
|
-
'major',
|
|
360
|
-
'premajor',
|
|
361
|
-
'minor',
|
|
362
|
-
'preminor',
|
|
363
|
-
'patch',
|
|
364
|
-
'prepatch',
|
|
365
|
-
'prerelease',
|
|
366
|
-
];
|
|
367
|
-
|
|
368
|
-
constants = {
|
|
369
|
-
MAX_LENGTH,
|
|
370
|
-
MAX_SAFE_COMPONENT_LENGTH,
|
|
371
|
-
MAX_SAFE_BUILD_LENGTH,
|
|
372
|
-
MAX_SAFE_INTEGER,
|
|
373
|
-
RELEASE_TYPES,
|
|
374
|
-
SEMVER_SPEC_VERSION,
|
|
375
|
-
FLAG_INCLUDE_PRERELEASE: 0b001,
|
|
376
|
-
FLAG_LOOSE: 0b010,
|
|
377
|
-
};
|
|
378
|
-
return constants;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
var debug_1;
|
|
382
|
-
var hasRequiredDebug;
|
|
383
|
-
|
|
384
|
-
function requireDebug () {
|
|
385
|
-
if (hasRequiredDebug) return debug_1;
|
|
386
|
-
hasRequiredDebug = 1;
|
|
387
|
-
|
|
388
|
-
const debug = (
|
|
389
|
-
typeof process === 'object' &&
|
|
390
|
-
process.env &&
|
|
391
|
-
process.env.NODE_DEBUG &&
|
|
392
|
-
/\bsemver\b/i.test(process.env.NODE_DEBUG)
|
|
393
|
-
) ? (...args) => console.error('SEMVER', ...args)
|
|
394
|
-
: () => {};
|
|
395
|
-
|
|
396
|
-
debug_1 = debug;
|
|
397
|
-
return debug_1;
|
|
60
|
+
var debug_1;
|
|
61
|
+
var hasRequiredDebug;
|
|
62
|
+
|
|
63
|
+
function requireDebug () {
|
|
64
|
+
if (hasRequiredDebug) return debug_1;
|
|
65
|
+
hasRequiredDebug = 1;
|
|
66
|
+
|
|
67
|
+
const debug = (
|
|
68
|
+
typeof process === 'object' &&
|
|
69
|
+
process.env &&
|
|
70
|
+
process.env.NODE_DEBUG &&
|
|
71
|
+
/\bsemver\b/i.test(process.env.NODE_DEBUG)
|
|
72
|
+
) ? (...args) => console.error('SEMVER', ...args)
|
|
73
|
+
: () => {};
|
|
74
|
+
|
|
75
|
+
debug_1 = debug;
|
|
76
|
+
return debug_1;
|
|
398
77
|
}
|
|
399
78
|
|
|
400
79
|
var hasRequiredRe;
|
|
@@ -3012,111 +2691,635 @@ function requireSubset () {
|
|
|
3012
2691
|
subset_1 = subset;
|
|
3013
2692
|
return subset_1;
|
|
3014
2693
|
}
|
|
3015
|
-
|
|
3016
|
-
var semver;
|
|
3017
|
-
var hasRequiredSemver;
|
|
3018
|
-
|
|
3019
|
-
function requireSemver () {
|
|
3020
|
-
if (hasRequiredSemver) return semver;
|
|
3021
|
-
hasRequiredSemver = 1;
|
|
3022
|
-
|
|
3023
|
-
// just pre-load all the stuff that index.js lazily exports
|
|
3024
|
-
const internalRe = requireRe();
|
|
3025
|
-
const constants = requireConstants();
|
|
3026
|
-
const SemVer = requireSemver$1();
|
|
3027
|
-
const identifiers = requireIdentifiers();
|
|
3028
|
-
const parse = requireParse();
|
|
3029
|
-
const valid = requireValid$1();
|
|
3030
|
-
const clean = requireClean();
|
|
3031
|
-
const inc = requireInc();
|
|
3032
|
-
const diff = requireDiff();
|
|
3033
|
-
const major = requireMajor();
|
|
3034
|
-
const minor = requireMinor();
|
|
3035
|
-
const patch = requirePatch();
|
|
3036
|
-
const prerelease = requirePrerelease();
|
|
3037
|
-
const compare = requireCompare();
|
|
3038
|
-
const rcompare = requireRcompare();
|
|
3039
|
-
const compareLoose = requireCompareLoose();
|
|
3040
|
-
const compareBuild = requireCompareBuild();
|
|
3041
|
-
const sort = requireSort();
|
|
3042
|
-
const rsort = requireRsort();
|
|
3043
|
-
const gt = requireGt();
|
|
3044
|
-
const lt = requireLt();
|
|
3045
|
-
const eq = requireEq();
|
|
3046
|
-
const neq = requireNeq();
|
|
3047
|
-
const gte = requireGte();
|
|
3048
|
-
const lte = requireLte();
|
|
3049
|
-
const cmp = requireCmp();
|
|
3050
|
-
const coerce = requireCoerce();
|
|
3051
|
-
const truncate = requireTruncate();
|
|
3052
|
-
const Comparator = requireComparator();
|
|
3053
|
-
const Range = requireRange();
|
|
3054
|
-
const satisfies = requireSatisfies();
|
|
3055
|
-
const toComparators = requireToComparators();
|
|
3056
|
-
const maxSatisfying = requireMaxSatisfying();
|
|
3057
|
-
const minSatisfying = requireMinSatisfying();
|
|
3058
|
-
const minVersion = requireMinVersion();
|
|
3059
|
-
const validRange = requireValid();
|
|
3060
|
-
const outside = requireOutside();
|
|
3061
|
-
const gtr = requireGtr();
|
|
3062
|
-
const ltr = requireLtr();
|
|
3063
|
-
const intersects = requireIntersects();
|
|
3064
|
-
const simplifyRange = requireSimplify();
|
|
3065
|
-
const subset = requireSubset();
|
|
3066
|
-
semver = {
|
|
3067
|
-
parse,
|
|
3068
|
-
valid,
|
|
3069
|
-
clean,
|
|
3070
|
-
inc,
|
|
3071
|
-
diff,
|
|
3072
|
-
major,
|
|
3073
|
-
minor,
|
|
3074
|
-
patch,
|
|
3075
|
-
prerelease,
|
|
3076
|
-
compare,
|
|
3077
|
-
rcompare,
|
|
3078
|
-
compareLoose,
|
|
3079
|
-
compareBuild,
|
|
3080
|
-
sort,
|
|
3081
|
-
rsort,
|
|
3082
|
-
gt,
|
|
3083
|
-
lt,
|
|
3084
|
-
eq,
|
|
3085
|
-
neq,
|
|
3086
|
-
gte,
|
|
3087
|
-
lte,
|
|
3088
|
-
cmp,
|
|
3089
|
-
coerce,
|
|
3090
|
-
truncate,
|
|
3091
|
-
Comparator,
|
|
3092
|
-
Range,
|
|
3093
|
-
satisfies,
|
|
3094
|
-
toComparators,
|
|
3095
|
-
maxSatisfying,
|
|
3096
|
-
minSatisfying,
|
|
3097
|
-
minVersion,
|
|
3098
|
-
validRange,
|
|
3099
|
-
outside,
|
|
3100
|
-
gtr,
|
|
3101
|
-
ltr,
|
|
3102
|
-
intersects,
|
|
3103
|
-
simplifyRange,
|
|
3104
|
-
subset,
|
|
3105
|
-
SemVer,
|
|
3106
|
-
re: internalRe.re,
|
|
3107
|
-
src: internalRe.src,
|
|
3108
|
-
tokens: internalRe.t,
|
|
3109
|
-
SEMVER_SPEC_VERSION: constants.SEMVER_SPEC_VERSION,
|
|
3110
|
-
RELEASE_TYPES: constants.RELEASE_TYPES,
|
|
3111
|
-
compareIdentifiers: identifiers.compareIdentifiers,
|
|
3112
|
-
rcompareIdentifiers: identifiers.rcompareIdentifiers,
|
|
3113
|
-
};
|
|
3114
|
-
return semver;
|
|
2694
|
+
|
|
2695
|
+
var semver;
|
|
2696
|
+
var hasRequiredSemver;
|
|
2697
|
+
|
|
2698
|
+
function requireSemver () {
|
|
2699
|
+
if (hasRequiredSemver) return semver;
|
|
2700
|
+
hasRequiredSemver = 1;
|
|
2701
|
+
|
|
2702
|
+
// just pre-load all the stuff that index.js lazily exports
|
|
2703
|
+
const internalRe = requireRe();
|
|
2704
|
+
const constants = requireConstants();
|
|
2705
|
+
const SemVer = requireSemver$1();
|
|
2706
|
+
const identifiers = requireIdentifiers();
|
|
2707
|
+
const parse = requireParse();
|
|
2708
|
+
const valid = requireValid$1();
|
|
2709
|
+
const clean = requireClean();
|
|
2710
|
+
const inc = requireInc();
|
|
2711
|
+
const diff = requireDiff();
|
|
2712
|
+
const major = requireMajor();
|
|
2713
|
+
const minor = requireMinor();
|
|
2714
|
+
const patch = requirePatch();
|
|
2715
|
+
const prerelease = requirePrerelease();
|
|
2716
|
+
const compare = requireCompare();
|
|
2717
|
+
const rcompare = requireRcompare();
|
|
2718
|
+
const compareLoose = requireCompareLoose();
|
|
2719
|
+
const compareBuild = requireCompareBuild();
|
|
2720
|
+
const sort = requireSort();
|
|
2721
|
+
const rsort = requireRsort();
|
|
2722
|
+
const gt = requireGt();
|
|
2723
|
+
const lt = requireLt();
|
|
2724
|
+
const eq = requireEq();
|
|
2725
|
+
const neq = requireNeq();
|
|
2726
|
+
const gte = requireGte();
|
|
2727
|
+
const lte = requireLte();
|
|
2728
|
+
const cmp = requireCmp();
|
|
2729
|
+
const coerce = requireCoerce();
|
|
2730
|
+
const truncate = requireTruncate();
|
|
2731
|
+
const Comparator = requireComparator();
|
|
2732
|
+
const Range = requireRange();
|
|
2733
|
+
const satisfies = requireSatisfies();
|
|
2734
|
+
const toComparators = requireToComparators();
|
|
2735
|
+
const maxSatisfying = requireMaxSatisfying();
|
|
2736
|
+
const minSatisfying = requireMinSatisfying();
|
|
2737
|
+
const minVersion = requireMinVersion();
|
|
2738
|
+
const validRange = requireValid();
|
|
2739
|
+
const outside = requireOutside();
|
|
2740
|
+
const gtr = requireGtr();
|
|
2741
|
+
const ltr = requireLtr();
|
|
2742
|
+
const intersects = requireIntersects();
|
|
2743
|
+
const simplifyRange = requireSimplify();
|
|
2744
|
+
const subset = requireSubset();
|
|
2745
|
+
semver = {
|
|
2746
|
+
parse,
|
|
2747
|
+
valid,
|
|
2748
|
+
clean,
|
|
2749
|
+
inc,
|
|
2750
|
+
diff,
|
|
2751
|
+
major,
|
|
2752
|
+
minor,
|
|
2753
|
+
patch,
|
|
2754
|
+
prerelease,
|
|
2755
|
+
compare,
|
|
2756
|
+
rcompare,
|
|
2757
|
+
compareLoose,
|
|
2758
|
+
compareBuild,
|
|
2759
|
+
sort,
|
|
2760
|
+
rsort,
|
|
2761
|
+
gt,
|
|
2762
|
+
lt,
|
|
2763
|
+
eq,
|
|
2764
|
+
neq,
|
|
2765
|
+
gte,
|
|
2766
|
+
lte,
|
|
2767
|
+
cmp,
|
|
2768
|
+
coerce,
|
|
2769
|
+
truncate,
|
|
2770
|
+
Comparator,
|
|
2771
|
+
Range,
|
|
2772
|
+
satisfies,
|
|
2773
|
+
toComparators,
|
|
2774
|
+
maxSatisfying,
|
|
2775
|
+
minSatisfying,
|
|
2776
|
+
minVersion,
|
|
2777
|
+
validRange,
|
|
2778
|
+
outside,
|
|
2779
|
+
gtr,
|
|
2780
|
+
ltr,
|
|
2781
|
+
intersects,
|
|
2782
|
+
simplifyRange,
|
|
2783
|
+
subset,
|
|
2784
|
+
SemVer,
|
|
2785
|
+
re: internalRe.re,
|
|
2786
|
+
src: internalRe.src,
|
|
2787
|
+
tokens: internalRe.t,
|
|
2788
|
+
SEMVER_SPEC_VERSION: constants.SEMVER_SPEC_VERSION,
|
|
2789
|
+
RELEASE_TYPES: constants.RELEASE_TYPES,
|
|
2790
|
+
compareIdentifiers: identifiers.compareIdentifiers,
|
|
2791
|
+
rcompareIdentifiers: identifiers.rcompareIdentifiers,
|
|
2792
|
+
};
|
|
2793
|
+
return semver;
|
|
2794
|
+
}
|
|
2795
|
+
|
|
2796
|
+
var semverExports = requireSemver();
|
|
2797
|
+
|
|
2798
|
+
const BASIC_INFO_MAX_BYTES = 10 * 1024 * 1024;
|
|
2799
|
+
const BASIC_INFO_MAX_PIXELS = 40_000_000;
|
|
2800
|
+
/** 按 NFKC 和 Unicode 空白规范化名称,并校验 1–40 个字符及禁用字符。 */
|
|
2801
|
+
function normalizeBasicInfoName(raw) {
|
|
2802
|
+
if (typeof raw !== 'string')
|
|
2803
|
+
throw new Error('heybox.name 必须是字符串');
|
|
2804
|
+
// 与 Go TrimSpace 一致(JS trim 额外移除 BOM,故使用 Unicode White_Space)。
|
|
2805
|
+
const name = raw.normalize('NFKC').replace(/^\p{White_Space}+|\p{White_Space}+$/gu, '');
|
|
2806
|
+
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)) {
|
|
2807
|
+
throw new Error('名称须包含 1–40 个字符及文字或数字,不能包含控制字符或 HTML 特殊字符');
|
|
2808
|
+
}
|
|
2809
|
+
return name;
|
|
2810
|
+
}
|
|
2811
|
+
function validateBasicInfoPath(raw) {
|
|
2812
|
+
if (typeof raw !== 'string' || !raw || /[\\:%?#\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(raw))
|
|
2813
|
+
throw new Error('资料图片必须是项目内的规范相对路径');
|
|
2814
|
+
const forbidden = new Set(['', '.', '..', '.git', 'node_modules', 'dist', '.hb-sdk', '.vite', '.cache', '.tmp', 'tmp']);
|
|
2815
|
+
if (raw.split('/').some((part) => forbidden.has(part)))
|
|
2816
|
+
throw new Error('资料图片路径包含禁止的目录');
|
|
2817
|
+
if (!/\.(png|jpe?g|gif|webp)$/i.test(raw))
|
|
2818
|
+
throw new Error('资料图片仅支持 JPEG、PNG、GIF、WebP');
|
|
2819
|
+
return raw;
|
|
2820
|
+
}
|
|
2821
|
+
/** 解析项目或构建资料;仅早于 0.8.2 且三项全部缺失时返回 undefined。 */
|
|
2822
|
+
function parseMiniappBasicInfo(value, sdkVersion = '0.8.2') {
|
|
2823
|
+
sdkVersion = sdkVersion.trim();
|
|
2824
|
+
if (!semverExports.valid(sdkVersion) || sdkVersion.startsWith('v') || sdkVersion.includes('+'))
|
|
2825
|
+
throw new Error('manifest.sdkVersion 必须是合法 SemVer');
|
|
2826
|
+
const fields = ['name', 'icon', 'coverImages'];
|
|
2827
|
+
const present = fields.filter((key) => Object.prototype.hasOwnProperty.call(value, key));
|
|
2828
|
+
if (!present.length && semverExports.lt(sdkVersion, '0.8.2'))
|
|
2829
|
+
return undefined;
|
|
2830
|
+
if (present.length !== fields.length)
|
|
2831
|
+
throw new Error('请配置 heybox.name、heybox.icon、heybox.coverImages,或运行 remote bind/dev/remote deploy 迁移已发布资料');
|
|
2832
|
+
const name = normalizeBasicInfoName(value.name);
|
|
2833
|
+
const icon = validateBasicInfoPath(value.icon);
|
|
2834
|
+
if (!Array.isArray(value.coverImages) || value.coverImages.length < 1 || value.coverImages.length > 5)
|
|
2835
|
+
throw new Error('coverImages 必须包含 1–5 张图片');
|
|
2836
|
+
const coverImages = value.coverImages.map(validateBasicInfoPath);
|
|
2837
|
+
if (new Set(coverImages).size !== coverImages.length)
|
|
2838
|
+
throw new Error('封面路径不能重复');
|
|
2839
|
+
return { name, icon, coverImages };
|
|
2840
|
+
}
|
|
2841
|
+
|
|
2842
|
+
async function inspectBasicInfoImage(bytes, sourcePath, kind) {
|
|
2843
|
+
const inspected = await inspectDecodedBasicInfoImage(bytes, kind);
|
|
2844
|
+
const extensions = { jpg: ['jpg', 'jpeg'], png: ['png'], gif: ['gif'], webp: ['webp'] };
|
|
2845
|
+
const ext = sourcePath.split('.').pop().toLowerCase();
|
|
2846
|
+
if (!extensions[inspected.extension]?.includes(ext))
|
|
2847
|
+
throw new Error('资料图片格式与扩展名不一致');
|
|
2848
|
+
return inspected;
|
|
2849
|
+
}
|
|
2850
|
+
async function inspectDecodedBasicInfoImage(bytes, kind) {
|
|
2851
|
+
if (!bytes.length || bytes.length > BASIC_INFO_MAX_BYTES)
|
|
2852
|
+
throw new Error('资料图片不得为空或超过 10 MiB');
|
|
2853
|
+
const decoder = sharp(bytes, { limitInputPixels: BASIC_INFO_MAX_PIXELS, failOn: 'warning' });
|
|
2854
|
+
const metadata = await decoder.metadata();
|
|
2855
|
+
const extensions = { jpeg: 'jpg', png: 'png', gif: 'gif', webp: 'webp' };
|
|
2856
|
+
if (!metadata.format || !extensions[metadata.format])
|
|
2857
|
+
throw new Error('资料图片格式不支持');
|
|
2858
|
+
const width = metadata.width ?? 0;
|
|
2859
|
+
const height = metadata.pageHeight ?? metadata.height ?? 0;
|
|
2860
|
+
if (!width || !height || width * height > BASIC_INFO_MAX_PIXELS)
|
|
2861
|
+
throw new Error('资料图片不得超过 4000 万像素');
|
|
2862
|
+
if (kind === 'icon' && (width !== 200 || height !== 200))
|
|
2863
|
+
throw new Error('icon 必须是 200×200');
|
|
2864
|
+
// 强制解码而非只信任图片头;不将解码结果写回图片。
|
|
2865
|
+
await decoder.stats();
|
|
2866
|
+
return { sha256: node_crypto.createHash('sha256').update(bytes).digest('hex'), extension: extensions[metadata.format] };
|
|
2867
|
+
}
|
|
2868
|
+
async function readBasicInfoSource(projectRoot, relative, kind) {
|
|
2869
|
+
validateBasicInfoPath(relative);
|
|
2870
|
+
const root = await promises.realpath(projectRoot);
|
|
2871
|
+
const target = await promises.realpath(path.join(root, relative));
|
|
2872
|
+
const resolvedRelative = path.relative(root, target).split(path.sep).join('/');
|
|
2873
|
+
validateBasicInfoPath(resolvedRelative);
|
|
2874
|
+
if (resolvedRelative.startsWith('../') || path.isAbsolute(resolvedRelative))
|
|
2875
|
+
throw new Error('资料图片不能逃逸项目目录');
|
|
2876
|
+
const info = await promises.stat(target);
|
|
2877
|
+
if (!info.isFile() || info.size > BASIC_INFO_MAX_BYTES)
|
|
2878
|
+
throw new Error('资料图片必须为不超过 10 MiB 的普通文件');
|
|
2879
|
+
const file = await promises.open(target, node_fs.constants.O_RDONLY | node_fs.constants.O_NOFOLLOW);
|
|
2880
|
+
try {
|
|
2881
|
+
const actual = await file.stat();
|
|
2882
|
+
if (!actual.isFile() || actual.size > BASIC_INFO_MAX_BYTES)
|
|
2883
|
+
throw new Error('资料图片文件已变化');
|
|
2884
|
+
const bytes = await file.readFile();
|
|
2885
|
+
return { bytes, ...(await inspectBasicInfoImage(bytes, relative, kind)) };
|
|
2886
|
+
}
|
|
2887
|
+
finally {
|
|
2888
|
+
await file.close();
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
async function prepareMiniappBasicInfo(projectRoot, outputRoot, sdkVersion) {
|
|
2892
|
+
const pkg = JSON.parse(await promises.readFile(path.join(projectRoot, 'package.json'), 'utf8'));
|
|
2893
|
+
const profile = parseMiniappBasicInfo(pkg.heybox ?? {}, sdkVersion);
|
|
2894
|
+
if (!profile)
|
|
2895
|
+
return undefined;
|
|
2896
|
+
await promises.mkdir(outputRoot, { recursive: true });
|
|
2897
|
+
const output = { ...profile, coverImages: [] };
|
|
2898
|
+
const entries = [
|
|
2899
|
+
{ path: profile.icon, kind: 'icon', label: 'icon' },
|
|
2900
|
+
...profile.coverImages.map((p, i) => ({ path: p, kind: 'cover', label: `cover-${String(i + 1).padStart(2, '0')}` })),
|
|
2901
|
+
];
|
|
2902
|
+
for (const entry of entries) {
|
|
2903
|
+
const sourceReal = await promises.realpath(path.join(projectRoot, entry.path));
|
|
2904
|
+
const outputReal = await promises.realpath(outputRoot);
|
|
2905
|
+
const fromOutput = path.relative(outputReal, sourceReal);
|
|
2906
|
+
if (!fromOutput.startsWith('..') && !path.isAbsolute(fromOutput))
|
|
2907
|
+
throw new Error('资料源文件不能位于构建输出目录');
|
|
2908
|
+
const image = await readBasicInfoSource(projectRoot, entry.path, entry.kind);
|
|
2909
|
+
const relative = `_hb/basic-info/${entry.label}.${image.sha256.slice(0, 12)}.${image.extension}`;
|
|
2910
|
+
const target = path.join(outputRoot, relative);
|
|
2911
|
+
await promises.mkdir(outputRoot, { recursive: true });
|
|
2912
|
+
await ensureBasicInfoDirectory(outputRoot, '_hb/basic-info');
|
|
2913
|
+
// 全字节比较保证截断哈希冲突和 public 保留路径冲突不会静默覆盖。
|
|
2914
|
+
try {
|
|
2915
|
+
await promises.writeFile(target, image.bytes, { flag: 'wx' });
|
|
2916
|
+
}
|
|
2917
|
+
catch (error) {
|
|
2918
|
+
if (error.code !== 'EEXIST' ||
|
|
2919
|
+
!(await readBasicInfoSource(outputRoot, relative, entry.kind).then((existing) => existing.bytes.equals(image.bytes), () => false)))
|
|
2920
|
+
throw new Error('构建资料保留目录冲突,请移除未知文件');
|
|
2921
|
+
}
|
|
2922
|
+
if (entry.kind === 'icon')
|
|
2923
|
+
output.icon = relative;
|
|
2924
|
+
else
|
|
2925
|
+
output.coverImages.push(relative);
|
|
2926
|
+
}
|
|
2927
|
+
return output;
|
|
2928
|
+
}
|
|
2929
|
+
/** 逐级校验真实目录,避免保留目录或迁移目录中的符号链接将写入带到项目外。 */
|
|
2930
|
+
async function ensureBasicInfoDirectory(rootPath, relative) {
|
|
2931
|
+
const root = await promises.realpath(rootPath);
|
|
2932
|
+
let current = root;
|
|
2933
|
+
for (const part of relative.split('/')) {
|
|
2934
|
+
if (!part || part === '.' || part === '..')
|
|
2935
|
+
throw new Error('资料目录不合法');
|
|
2936
|
+
current = path.join(current, part);
|
|
2937
|
+
await promises.mkdir(current).catch((error) => {
|
|
2938
|
+
if (error.code !== 'EEXIST')
|
|
2939
|
+
throw error;
|
|
2940
|
+
});
|
|
2941
|
+
const resolved = await promises.realpath(current);
|
|
2942
|
+
const within = path.relative(root, resolved);
|
|
2943
|
+
if (within.startsWith('..') || path.isAbsolute(within))
|
|
2944
|
+
throw new Error('资料目录不能逃逸项目');
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
|
|
2948
|
+
/** 构建时替换为当前发布包的实际版本。 */
|
|
2949
|
+
const HB_SDK_VERSION = typeof undefined === 'string'
|
|
2950
|
+
? undefined
|
|
2951
|
+
: '0.8.2-alpha.5';
|
|
2952
|
+
|
|
2953
|
+
/**
|
|
2954
|
+
* iframe 与调试台页面的开发期 console 捕获转发。
|
|
2955
|
+
*
|
|
2956
|
+
* 双条件门控(iframe 转发):
|
|
2957
|
+
* 1. 构建期:`hb-sdk dev`(vite serve)注入 `false`;`hb-sdk build` /
|
|
2958
|
+
* `vite build`(生产)永不注入或替换为 `false`,本模块被死代码消除。
|
|
2959
|
+
* 2. 运行期:仅在 `window.name` 为 `__hb_sdk_mini_dev__` 的调试 iframe 中激活,
|
|
2960
|
+
* 用户设备上的正式运行环境没有该标记。
|
|
2961
|
+
*
|
|
2962
|
+
* 无侵入 patch `console.log/debug/info/warn/error`:原样放行(DevTools 不受影响)+
|
|
2963
|
+
* 通过 `window.parent.postMessage` 转发给调试台(CSP 的 connect-src 不约束 postMessage,
|
|
2964
|
+
* 网络上报由调试台页面代理)。发送全程静默:失败不得回打 console,避免自触发死循环。
|
|
2965
|
+
*
|
|
2966
|
+
* 激活时机:由 SDK 默认单例初始化时调用(业务 `import '@heybox/hb-sdk'` 即生效);
|
|
2967
|
+
* iframe 标记由调试台 `iframe.name` 与 vite dev 注入的无依赖 bootstrap 共同设置。
|
|
2968
|
+
* 调试台页面自身(runtime 跑在这里,`[hb-sdk-runtime]` 日志不经过 iframe)另用
|
|
2969
|
+
* `installMiniDevConsoleSelfCapture` 接入「日志」页签(source: 'host')。
|
|
2970
|
+
*/
|
|
2971
|
+
const MINI_DEV_CONSOLE_EVENT_TYPE = 'hb-sdk:mini-dev-console';
|
|
2972
|
+
/** 调试台页面与 iframe bootstrap 共用的已安装标记(window/console 上的属性名)。 */
|
|
2973
|
+
const MINI_DEV_CONSOLE_INSTALL_FLAG = '__hb_sdk_mini_dev_console_installed__';
|
|
2974
|
+
|
|
2975
|
+
/**
|
|
2976
|
+
* network.request 诊断日志的纯脱敏工具。
|
|
2977
|
+
*
|
|
2978
|
+
* @remarks
|
|
2979
|
+
* 放在 `core` 而非 `modules/network`:无错误类型依赖,供 SDK 错误文案、
|
|
2980
|
+
* modules observability、devtools Browser Dev Host / proxy 共用,并满足 boundary
|
|
2981
|
+
*(devtools 不得直接 import runtime capability modules)。
|
|
2982
|
+
*/
|
|
2983
|
+
/** 敏感字段名:header / query / body key 共用。 */
|
|
2984
|
+
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;
|
|
2985
|
+
/**
|
|
2986
|
+
* 脱敏请求 URL:去掉 userinfo,并将 query / fragment 中的敏感参数值替换为 `[redacted]`。
|
|
2987
|
+
*/
|
|
2988
|
+
function sanitizeNetworkRequestUrl(url) {
|
|
2989
|
+
if (!url) {
|
|
2990
|
+
return url;
|
|
2991
|
+
}
|
|
2992
|
+
try {
|
|
2993
|
+
const parsed = new URL(url);
|
|
2994
|
+
parsed.username = '';
|
|
2995
|
+
parsed.password = '';
|
|
2996
|
+
redactUrlSearchParams(parsed.searchParams);
|
|
2997
|
+
if (parsed.hash && parsed.hash.length > 1) {
|
|
2998
|
+
const hashBody = parsed.hash.slice(1);
|
|
2999
|
+
if (hashBody.includes('=')) {
|
|
3000
|
+
const hashParams = new URLSearchParams(hashBody);
|
|
3001
|
+
if (redactUrlSearchParams(hashParams)) {
|
|
3002
|
+
parsed.hash = hashParams.toString();
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
return parsed.toString();
|
|
3007
|
+
}
|
|
3008
|
+
catch {
|
|
3009
|
+
return redactSensitivePairsInRawText(url);
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
/**
|
|
3013
|
+
* 诊断 message 中的 URL 片段做脱敏,避免 error.message 二次泄漏 query secrets。
|
|
3014
|
+
*/
|
|
3015
|
+
function sanitizeDiagnosticMessage(message) {
|
|
3016
|
+
if (!message) {
|
|
3017
|
+
return message;
|
|
3018
|
+
}
|
|
3019
|
+
return message.replace(/https?:\/\/[^\s]+/gi, matched => sanitizeNetworkRequestUrl(matched.replace(/[),.;]+$/g, '')));
|
|
3020
|
+
}
|
|
3021
|
+
function isSensitiveNetworkKey(key) {
|
|
3022
|
+
return SENSITIVE_KEY_PATTERN.test(key.trim());
|
|
3023
|
+
}
|
|
3024
|
+
function redactUrlSearchParams(params) {
|
|
3025
|
+
let changed = false;
|
|
3026
|
+
for (const key of [...params.keys()]) {
|
|
3027
|
+
if (isSensitiveNetworkKey(key)) {
|
|
3028
|
+
params.set(key, '[redacted]');
|
|
3029
|
+
changed = true;
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
return changed;
|
|
3033
|
+
}
|
|
3034
|
+
function redactSensitivePairsInRawText(text) {
|
|
3035
|
+
return text.replace(/([?&#]|^|[?&])([^=&#\s]+)=([^&#\s]*)/g, (full, prefix, key, value) => {
|
|
3036
|
+
if (!isSensitiveNetworkKey(key)) {
|
|
3037
|
+
return full;
|
|
3038
|
+
}
|
|
3039
|
+
return `${prefix}${key}=[redacted]`;
|
|
3040
|
+
});
|
|
3041
|
+
}
|
|
3042
|
+
|
|
3043
|
+
const DEVICE_LOG_PATH = '/__hb_sdk__/device-logs';
|
|
3044
|
+
const registryKey = Symbol.for('@heybox/hb-sdk/device-logs');
|
|
3045
|
+
const registry = globalThis;
|
|
3046
|
+
const stores = (registry[registryKey] ??= new Map());
|
|
3047
|
+
function registerDeviceLogs(port, store) {
|
|
3048
|
+
stores.set(port, store);
|
|
3049
|
+
return () => {
|
|
3050
|
+
if (stores.get(port) === store)
|
|
3051
|
+
stores.delete(port);
|
|
3052
|
+
};
|
|
3053
|
+
}
|
|
3054
|
+
class DeviceLogStore {
|
|
3055
|
+
sandboxRequests = new Map();
|
|
3056
|
+
listSandbox(id, path) {
|
|
3057
|
+
const device = this.devices.get(id);
|
|
3058
|
+
if (!device || Date.now() - device.lastSeen > 15000)
|
|
3059
|
+
return Promise.resolve({ error: '设备已离线' });
|
|
3060
|
+
if (path !== '.' && (!path || path.length > 1024 || /[\\\\\u0000-\u001f]/.test(path) || path.split('/').some(part => !part || part === '.' || part === '..')))
|
|
3061
|
+
return Promise.resolve({ error: '无效目录路径' });
|
|
3062
|
+
if (this.sandboxRequests.size >= 64)
|
|
3063
|
+
return Promise.resolve({ error: '查询繁忙,请稍后重试' });
|
|
3064
|
+
const requestId = node_crypto.randomUUID();
|
|
3065
|
+
return new Promise(resolve => {
|
|
3066
|
+
const timer = setTimeout(() => { this.sandboxRequests.delete(requestId); resolve({ error: '设备查询超时' }); }, 10000);
|
|
3067
|
+
this.sandboxRequests.set(requestId, { id, path, requestId, resolve: value => { clearTimeout(timer); this.sandboxRequests.delete(requestId); resolve(value); } });
|
|
3068
|
+
});
|
|
3069
|
+
}
|
|
3070
|
+
uploadToken = node_crypto.randomBytes(32).toString('hex');
|
|
3071
|
+
epoch = node_crypto.randomUUID();
|
|
3072
|
+
expiresAt = Date.now() + 12 * 60 * 60 * 1000;
|
|
3073
|
+
cursor = 0;
|
|
3074
|
+
logs = [];
|
|
3075
|
+
devices = new Map();
|
|
3076
|
+
query(params) {
|
|
3077
|
+
const after = Math.max(0, Number(params.get('after')) || 0);
|
|
3078
|
+
const limit = Math.min(500, Math.max(1, Number(params.get('limit')) || 100));
|
|
3079
|
+
const selected = this.logs.filter((log) => log.cursor > after &&
|
|
3080
|
+
(!params.get('device') || log.deviceSessionId === params.get('device')) &&
|
|
3081
|
+
(!params.get('level') || log.level === params.get('level')) &&
|
|
3082
|
+
(!params.get('source') || log.source === params.get('source')) &&
|
|
3083
|
+
(!params.get('callId') || log.callId === params.get('callId')) &&
|
|
3084
|
+
(!params.get('id') || String(log.cursor) === params.get('id')) &&
|
|
3085
|
+
(!params.get('since') || log.timestamp >= Number(params.get('since'))) &&
|
|
3086
|
+
(!params.get('query') || log.message.toLowerCase().includes(params.get('query').toLowerCase())));
|
|
3087
|
+
const items = selected.slice(0, limit);
|
|
3088
|
+
return {
|
|
3089
|
+
schemaVersion: 1,
|
|
3090
|
+
epoch: this.epoch,
|
|
3091
|
+
items,
|
|
3092
|
+
nextCursor: selected.length > limit ? items.at(-1).cursor : this.cursor,
|
|
3093
|
+
hasMore: selected.length > limit,
|
|
3094
|
+
gap: after > 0 && after < (this.logs[0]?.cursor ?? 1) - 1,
|
|
3095
|
+
devices: [...this.devices.values()].map(({ id, name, platform, connectedAt, lastSeen }) => ({
|
|
3096
|
+
id,
|
|
3097
|
+
name,
|
|
3098
|
+
platform,
|
|
3099
|
+
connectedAt,
|
|
3100
|
+
lastSeen,
|
|
3101
|
+
online: Date.now() - lastSeen < 15000,
|
|
3102
|
+
})),
|
|
3103
|
+
};
|
|
3104
|
+
}
|
|
3105
|
+
async upload(req, res) {
|
|
3106
|
+
const reply = (status, value) => {
|
|
3107
|
+
res.writeHead(status, { 'content-type': 'application/json', 'cache-control': 'no-store' });
|
|
3108
|
+
res.end(JSON.stringify(value));
|
|
3109
|
+
};
|
|
3110
|
+
if (req.method !== 'POST') {
|
|
3111
|
+
reply(405, {});
|
|
3112
|
+
return;
|
|
3113
|
+
}
|
|
3114
|
+
if (Date.now() > this.expiresAt) {
|
|
3115
|
+
reply(410, { code: 'EXPIRED' });
|
|
3116
|
+
return;
|
|
3117
|
+
}
|
|
3118
|
+
let text = '';
|
|
3119
|
+
try {
|
|
3120
|
+
req.setEncoding('utf8');
|
|
3121
|
+
for await (const chunk of req) {
|
|
3122
|
+
text += chunk;
|
|
3123
|
+
if (Buffer.byteLength(text) > 65536) {
|
|
3124
|
+
reply(413, {});
|
|
3125
|
+
return;
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
3128
|
+
const body = JSON.parse(text);
|
|
3129
|
+
if (body.register === true) {
|
|
3130
|
+
if (req.headers['x-hb-log-token'] !== this.uploadToken) {
|
|
3131
|
+
reply(403, {});
|
|
3132
|
+
return;
|
|
3133
|
+
}
|
|
3134
|
+
const registrationId = typeof body.registrationId === 'string' && /^[a-f0-9]{32}$/.test(body.registrationId) ? body.registrationId : undefined;
|
|
3135
|
+
const existing = registrationId && [...this.devices.values()].find(device => device.registrationId === registrationId);
|
|
3136
|
+
if (existing) {
|
|
3137
|
+
reply(200, { id: existing.id, token: existing.token });
|
|
3138
|
+
return;
|
|
3139
|
+
}
|
|
3140
|
+
if (this.devices.size >= 64) {
|
|
3141
|
+
reply(429, {});
|
|
3142
|
+
return;
|
|
3143
|
+
}
|
|
3144
|
+
const id = node_crypto.randomUUID();
|
|
3145
|
+
const platform = ['iOS', 'Android', 'Windows', 'macOS', 'Browser'].includes(body.platform) ? body.platform : 'Browser';
|
|
3146
|
+
const device = {
|
|
3147
|
+
id,
|
|
3148
|
+
platform,
|
|
3149
|
+
registrationId,
|
|
3150
|
+
connectedAt: Date.now(),
|
|
3151
|
+
name: `${platform} · ${this.devices.size + 1}`,
|
|
3152
|
+
lastSeen: Date.now(),
|
|
3153
|
+
sequence: 0,
|
|
3154
|
+
token: node_crypto.randomBytes(32).toString('hex'),
|
|
3155
|
+
windowStart: Date.now(),
|
|
3156
|
+
count: 0,
|
|
3157
|
+
};
|
|
3158
|
+
this.devices.set(id, device);
|
|
3159
|
+
reply(200, { id, token: device.token });
|
|
3160
|
+
return;
|
|
3161
|
+
}
|
|
3162
|
+
const device = this.devices.get(body.id);
|
|
3163
|
+
if (!device || req.headers['x-hb-log-token'] !== device.token) {
|
|
3164
|
+
reply(403, {});
|
|
3165
|
+
return;
|
|
3166
|
+
}
|
|
3167
|
+
if (!Array.isArray(body.logs) || body.logs.length > 50) {
|
|
3168
|
+
reply(400, {});
|
|
3169
|
+
return;
|
|
3170
|
+
}
|
|
3171
|
+
if (Date.now() - device.windowStart > 1000) {
|
|
3172
|
+
device.windowStart = Date.now();
|
|
3173
|
+
device.count = 0;
|
|
3174
|
+
}
|
|
3175
|
+
if (device.count + body.logs.length > 200) {
|
|
3176
|
+
reply(429, {});
|
|
3177
|
+
return;
|
|
3178
|
+
}
|
|
3179
|
+
device.count += body.logs.length;
|
|
3180
|
+
device.lastSeen = Date.now();
|
|
3181
|
+
if (body.sandboxResult && typeof body.sandboxResult === 'object') {
|
|
3182
|
+
const result = body.sandboxResult;
|
|
3183
|
+
const pending = this.sandboxRequests.get(result.requestId);
|
|
3184
|
+
if (pending?.id === device.id) {
|
|
3185
|
+
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 })) : [];
|
|
3186
|
+
pending.resolve({ deviceSessionId: device.id, requestId: pending.requestId, path: pending.path, timestamp: Date.now(), entries, error: result.error ? '设备不支持或无权读取此目录' : undefined, truncated: result.truncated === true });
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
3189
|
+
for (const log of body.logs) {
|
|
3190
|
+
if (!log || typeof log !== 'object')
|
|
3191
|
+
continue;
|
|
3192
|
+
if (!Number.isSafeInteger(log.sequence) ||
|
|
3193
|
+
log.sequence <= device.sequence ||
|
|
3194
|
+
!Number.isFinite(log.timestamp) ||
|
|
3195
|
+
!['log', 'debug', 'info', 'warn', 'error'].includes(log.level) ||
|
|
3196
|
+
typeof log.message !== 'string')
|
|
3197
|
+
continue;
|
|
3198
|
+
device.sequence = log.sequence;
|
|
3199
|
+
this.logs.push({
|
|
3200
|
+
cursor: ++this.cursor,
|
|
3201
|
+
deviceSessionId: device.id,
|
|
3202
|
+
sequence: log.sequence,
|
|
3203
|
+
timestamp: log.timestamp,
|
|
3204
|
+
receivedAt: Date.now(),
|
|
3205
|
+
level: log.level,
|
|
3206
|
+
source: log.source === 'sdk' ? 'sdk' : 'app',
|
|
3207
|
+
message: sanitizeDiagnosticMessage(log.message)
|
|
3208
|
+
.replace(/(authorization|cookie|password|secret|token|pkey|api[_-]?key)(["'\s:=]+)([^\s,}]+)/gi, '$1$2[redacted]')
|
|
3209
|
+
.slice(0, 8192),
|
|
3210
|
+
truncated: log.truncated === true || log.message.length > 8192,
|
|
3211
|
+
...(typeof log.method === 'string' ? { method: log.method.slice(0, 100) } : {}),
|
|
3212
|
+
...(typeof log.callId === 'string' ? { callId: log.callId.slice(0, 100) } : {}),
|
|
3213
|
+
...(typeof log.errorCode === 'string' && /^[A-Z0-9_]{1,100}$/.test(log.errorCode) ? { errorCode: log.errorCode } : {}),
|
|
3214
|
+
...(log.outcome === 'success' || log.outcome === 'error' ? { outcome: log.outcome } : {}),
|
|
3215
|
+
...(Number.isFinite(log.durationMs) ? { durationMs: Math.max(0, log.durationMs) } : {}),
|
|
3216
|
+
});
|
|
3217
|
+
}
|
|
3218
|
+
if (this.logs.length > 5000)
|
|
3219
|
+
this.logs.splice(0, this.logs.length - 5000);
|
|
3220
|
+
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 });
|
|
3221
|
+
}
|
|
3222
|
+
catch {
|
|
3223
|
+
reply(400, { code: 'INVALID_LOG_BATCH' });
|
|
3224
|
+
}
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
/** 同源开发页面使用有界批量回传;传输失败不会递归写入 console。 */
|
|
3228
|
+
function deviceLogBootstrap(token) {
|
|
3229
|
+
return `<script>(function(){if(window.__hb_sdk_device_logs_installed__)return;
|
|
3230
|
+
window.__hb_sdk_device_logs_installed__=true;
|
|
3231
|
+
window.__hb_sdk_mini_dev_console_installed__=true;
|
|
3232
|
+
var endpoint='${DEVICE_LOG_PATH}',credential='${token}',id='',queue=[],seq=0,busy=false,dropped=0;
|
|
3233
|
+
var registrationId=Array.from(crypto.getRandomValues(new Uint8Array(16)),function(n){return n.toString(16).padStart(2,'0')}).join('');
|
|
3234
|
+
var send=window.fetch.bind(window);
|
|
3235
|
+
var sandboxResult;
|
|
3236
|
+
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();
|
|
3237
|
+
dropped++}var message=args.map(format).join(' ');
|
|
3238
|
+
message=message.replace(/https?:\\/\\/[^\\s]+/g,function(raw){try{var url=new URL(raw);
|
|
3239
|
+
url.username='';
|
|
3240
|
+
url.password='';
|
|
3241
|
+
url.search='';
|
|
3242
|
+
url.hash='';
|
|
3243
|
+
return url.href}catch(e){return '[URL]'}});
|
|
3244
|
+
message=message.replace(/(authorization|cookie|password|secret|token|pkey|api[_-]?key)([\\"'\\s:=]+)([^\\s,}]+)/gi,'$1$2[redacted]');
|
|
3245
|
+
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');
|
|
3246
|
+
Object.assign(queue[queue.length-1],event)};
|
|
3247
|
+
['log','debug','info','warn','error'].forEach(function(level){var original=console[level];
|
|
3248
|
+
console[level]=function(){add(level,Array.prototype.slice.call(arguments));
|
|
3249
|
+
return original.apply(this,arguments)}});
|
|
3250
|
+
window.addEventListener('error',function(e){add('error',[e.message])});
|
|
3251
|
+
window.addEventListener('unhandledrejection',function(e){add('error',[e.reason])});
|
|
3252
|
+
async function flush(){if(busy)return;
|
|
3253
|
+
busy=true;
|
|
3254
|
+
try{if(!id){var ua=navigator.userAgent;
|
|
3255
|
+
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';
|
|
3256
|
+
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)});
|
|
3257
|
+
if(!r.ok)return;
|
|
3258
|
+
var d=await r.json();
|
|
3259
|
+
id=d.id;
|
|
3260
|
+
credential=d.token}if(dropped){add('warn',['日志缓冲已丢弃 '+dropped+' 条']);
|
|
3261
|
+
dropped=0}var batch=queue.slice(0,50);
|
|
3262
|
+
while(batch.length>1&&new TextEncoder().encode(JSON.stringify(batch)).length>48000)batch.pop();
|
|
3263
|
+
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)});
|
|
3264
|
+
if(result.ok){var ack=await result.json();sandboxResult=undefined;
|
|
3265
|
+
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}}}
|
|
3266
|
+
queue=queue.filter(function(l){return l.sequence>ack.sequence})}}catch(e){}finally{busy=false}}setInterval(flush,1000);
|
|
3267
|
+
flush()})();
|
|
3268
|
+
</script>`;
|
|
3115
3269
|
}
|
|
3116
3270
|
|
|
3117
|
-
|
|
3118
|
-
|
|
3271
|
+
/** 单个小程序版本最多声明的关联游戏数量。 */
|
|
3119
3272
|
const MINIAPP_BIND_APP_IDS_MAX_COUNT = 20;
|
|
3273
|
+
/**
|
|
3274
|
+
* 校验并规范化小程序关联的游戏 App ID。
|
|
3275
|
+
*
|
|
3276
|
+
* @param value - 不可信声明。`undefined` 和 `[]` 均返回空绑定;其他值必须是最多 20 项的
|
|
3277
|
+
* 字符串数组,不接受 `null`、单个字符串、number 数组或类数组对象。
|
|
3278
|
+
* @param options - 可选诊断来源;`sourceLabel` 默认 `package.json#heybox.bindAppIds`,并原样
|
|
3279
|
+
* 作为所有错误消息的前缀。
|
|
3280
|
+
* @returns 新建、按 appid 数值升序排列且经 `Object.freeze` 冻结的 `readonly string[]`。
|
|
3281
|
+
* 输入数组及其顺序不会被修改;空输入也返回新的冻结空数组。
|
|
3282
|
+
* @throws `Error` `${sourceLabel} 必须是字符串数组`:`value` 不是数组。
|
|
3283
|
+
* @throws `Error` `${sourceLabel} 最多包含 20 个游戏 appid`:输入数组超过 20 项;数量检查
|
|
3284
|
+
* 先于逐项格式检查。
|
|
3285
|
+
* @throws `Error` `${sourceLabel} 包含非法游戏 appid:<value>`:任一项不是 string,或不匹配
|
|
3286
|
+
* `/^[1-9]\d*$/`。因此空串、首尾空白、`0`、前导零、正负号、小数、指数和非十进制字符
|
|
3287
|
+
* 都不会被自动修正。
|
|
3288
|
+
* @throws `Error` `${sourceLabel} 包含超出正 int32 范围的游戏 appid:<value>`:十进制值
|
|
3289
|
+
* 大于 `2147483647` 或不能成为安全整数。
|
|
3290
|
+
* @throws `Error` `${sourceLabel} 包含重复游戏 appid:<value>`:规范字符串在同一数组重复。
|
|
3291
|
+
*
|
|
3292
|
+
* @remarks
|
|
3293
|
+
* 合法范围是闭区间 `1..2147483647`,每项在输出中仍保持十进制 string。排序按数值而不是
|
|
3294
|
+
* 字典序执行,所以 `['3513350', '730', '10']` 输出为 `['10', '730', '3513350']`。
|
|
3295
|
+
* 函数先复制后排序并冻结结果,不排序或冻结调用方传入的数组。
|
|
3296
|
+
*
|
|
3297
|
+
* 这是同步、纯离线的格式规范化函数,不访问网络,也不确认 appid 是否真实存在、可绑定或
|
|
3298
|
+
* 属于当前开发者。`miniappManifest()` 用它校验 `package.json#heybox.bindAppIds` 并把非空结果
|
|
3299
|
+
* 写入 Manifest;远端 deploy 预检和提审才校验游戏可用性。缺失和空数组在普通构建中都表示
|
|
3300
|
+
* 无绑定,函数本身不处理 `remote deploy --from-version` 的历史快照继承语义。
|
|
3301
|
+
*
|
|
3302
|
+
* `bindAppIds` 是版本发布元数据,不是 Runtime 权限,也不会因调用
|
|
3303
|
+
* [navigation.openAppPage](/reference/sdk/navigation/openAppPage) 而变化。后者的 `appId` 是一次
|
|
3304
|
+
* 页面导航参数,允许 string 或 number、只要求字符串化后非空,不适用本函数的数组、int32、
|
|
3305
|
+
* 前导零、去重、20 项或排序规则。
|
|
3306
|
+
*
|
|
3307
|
+
* @example
|
|
3308
|
+
* ```ts
|
|
3309
|
+
* import { parseMiniappBindAppIds } from '@heybox/hb-sdk/miniapp-publish'
|
|
3310
|
+
*
|
|
3311
|
+
* const declared = ['3513350', '730', '10']
|
|
3312
|
+
* const appIds = parseMiniappBindAppIds(declared)
|
|
3313
|
+
*
|
|
3314
|
+
* console.log(appIds) // ['10', '730', '3513350']
|
|
3315
|
+
* console.log(declared) // ['3513350', '730', '10'],输入顺序不变
|
|
3316
|
+
* console.log(Object.isFrozen(appIds)) // true
|
|
3317
|
+
* ```
|
|
3318
|
+
*
|
|
3319
|
+
* @see [miniappManifest](/reference/symbols/vite/functions/miniappManifest)
|
|
3320
|
+
* @see [navigation.openAppPage](/reference/sdk/navigation/openAppPage)
|
|
3321
|
+
* @see [CLI 与远端发布](/guide/cli#生产构建)
|
|
3322
|
+
*/
|
|
3120
3323
|
function parseMiniappBindAppIds(value, options = {}) {
|
|
3121
3324
|
const sourceLabel = options.sourceLabel ?? 'package.json#heybox.bindAppIds';
|
|
3122
3325
|
if (value === undefined)
|
|
@@ -3145,8 +3348,25 @@ function parseMiniappBindAppIds(value, options = {}) {
|
|
|
3145
3348
|
return Object.freeze(appIds);
|
|
3146
3349
|
}
|
|
3147
3350
|
|
|
3351
|
+
/**
|
|
3352
|
+
* 所有小程序 iframe bridge envelope 共用的固定 namespace。
|
|
3353
|
+
*
|
|
3354
|
+
* @remarks 该值用于排除无关 `postMessage` 数据,不负责验证消息来源;接收方仍须校验 source 和 nonce,
|
|
3355
|
+
* 并在 origin 可以固定且协议模式要求时校验 origin。
|
|
3356
|
+
*/
|
|
3148
3357
|
const MINI_PROGRAM_MESSAGE_NAMESPACE = 'heybox:miniprogram';
|
|
3358
|
+
/**
|
|
3359
|
+
* 新消息默认发送的当前 bridge wire 版本。
|
|
3360
|
+
*
|
|
3361
|
+
* @remarks 接收 guard 为握手协商继续接受 v1 与 v2 envelope;cancel 与 operation progress 等 v2-only 消息仍必须使用 v2。
|
|
3362
|
+
*/
|
|
3149
3363
|
const MINI_PROGRAM_MESSAGE_VERSION = 2;
|
|
3364
|
+
/**
|
|
3365
|
+
* Runtime 在 iframe URL 中传递 bridge nonce 的 query 参数名。
|
|
3366
|
+
*
|
|
3367
|
+
* @remarks nonce 用于把消息关联到当前 iframe 实例,不是独立的身份认证凭据;接收方还必须校验 source,
|
|
3368
|
+
* 并在 origin 可以固定且协议模式要求时校验 origin。
|
|
3369
|
+
*/
|
|
3150
3370
|
const MINI_PROGRAM_BRIDGE_NONCE_PARAM = 'hb_mini_bridge_nonce';
|
|
3151
3371
|
/** Companion alias 的跨构建、Runtime 与 Host 规范。 */
|
|
3152
3372
|
const COMPANION_ALIAS_PATTERN = /^[a-z][a-z0-9-]{0,31}$/;
|
|
@@ -3362,6 +3582,11 @@ function isRecord$3(value) {
|
|
|
3362
3582
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
3363
3583
|
}
|
|
3364
3584
|
|
|
3585
|
+
/**
|
|
3586
|
+
* `package.json#heybox.platforms` 接受的平台值,顺序同时是 Manifest 的规范化输出顺序。
|
|
3587
|
+
*
|
|
3588
|
+
* @remarks 项目仍应只声明实际完成适配并经过验收的平台;模板默认值不是自动兼容承诺。
|
|
3589
|
+
*/
|
|
3365
3590
|
const MINIAPP_PLATFORM_VALUES = ['android', 'ios', 'ohos', 'windows', 'macos', 'linux'];
|
|
3366
3591
|
/** 窗口尺寸字段的脏值兜底上限;真正的上限是宿主侧显示器 workArea。 */
|
|
3367
3592
|
const MINIAPP_WINDOW_SIZE_MAX = 16_384;
|
|
@@ -3384,6 +3609,7 @@ function renderMiniappManifest(manifest) {
|
|
|
3384
3609
|
const output = {
|
|
3385
3610
|
version: manifest.version,
|
|
3386
3611
|
sdkVersion: manifest.sdkVersion,
|
|
3612
|
+
...(manifest.name === undefined ? {} : { name: manifest.name, icon: manifest.icon, coverImages: manifest.coverImages }),
|
|
3387
3613
|
platforms,
|
|
3388
3614
|
...(manifest.permissions === undefined ? {} : { permissions: manifest.permissions }),
|
|
3389
3615
|
...(manifest.companions === undefined ? {} : { companions: manifest.companions }),
|
|
@@ -12749,29 +12975,29 @@ function renderRuntimeGateScript() {
|
|
|
12749
12975
|
function removePlatformGateNodes(node) {
|
|
12750
12976
|
if (!('childNodes' in node))
|
|
12751
12977
|
return;
|
|
12752
|
-
node.childNodes = node.childNodes.filter(child => !('tagName' in child) || !hasAttr(child, RUNTIME_GATE_MARKER));
|
|
12978
|
+
node.childNodes = node.childNodes.filter((child) => !('tagName' in child) || !hasAttr(child, RUNTIME_GATE_MARKER));
|
|
12753
12979
|
for (const child of node.childNodes)
|
|
12754
12980
|
removePlatformGateNodes(child);
|
|
12755
12981
|
}
|
|
12756
12982
|
function readAttr(element, name) {
|
|
12757
|
-
return element.attrs.find(attr => attr.name.toLowerCase() === name)?.value;
|
|
12983
|
+
return element.attrs.find((attr) => attr.name.toLowerCase() === name)?.value;
|
|
12758
12984
|
}
|
|
12759
12985
|
function hasAttr(element, name) {
|
|
12760
12986
|
return readAttr(element, name) !== undefined;
|
|
12761
12987
|
}
|
|
12762
12988
|
function setAttr(element, name, value) {
|
|
12763
|
-
const attr = element.attrs.find(item => item.name.toLowerCase() === name);
|
|
12989
|
+
const attr = element.attrs.find((item) => item.name.toLowerCase() === name);
|
|
12764
12990
|
if (attr)
|
|
12765
12991
|
attr.value = value;
|
|
12766
12992
|
else
|
|
12767
12993
|
element.attrs.push({ name, value });
|
|
12768
12994
|
}
|
|
12769
12995
|
function removeAttr(element, name) {
|
|
12770
|
-
element.attrs = element.attrs.filter(attr => attr.name.toLowerCase() !== name);
|
|
12996
|
+
element.attrs = element.attrs.filter((attr) => attr.name.toLowerCase() !== name);
|
|
12771
12997
|
}
|
|
12772
12998
|
function findElement$1(node, tagName) {
|
|
12773
12999
|
let found;
|
|
12774
|
-
walk$1(node, element => {
|
|
13000
|
+
walk$1(node, (element) => {
|
|
12775
13001
|
if (!found && element.tagName === tagName)
|
|
12776
13002
|
found = element;
|
|
12777
13003
|
});
|
|
@@ -12790,10 +13016,7 @@ function walk$1(node, visit) {
|
|
|
12790
13016
|
const FORBIDDEN_RESOURCE_HINTS = new Set(['dns-prefetch', 'preconnect', 'prerender']);
|
|
12791
13017
|
const RELATIVE_URL_BASE = new URL('https://heybox-relative.invalid/');
|
|
12792
13018
|
const OFFICIAL_RESOURCE_ROOT_DOMAINS = ['xiaoheihe.cn', 'max-c.com', 'debugmode.cn', 'maxjia.com'];
|
|
12793
|
-
const OFFICIAL_RESOURCE_SOURCES = OFFICIAL_RESOURCE_ROOT_DOMAINS.flatMap(rootDomain => [
|
|
12794
|
-
`https://${rootDomain}`,
|
|
12795
|
-
`https://*.${rootDomain}`,
|
|
12796
|
-
]);
|
|
13019
|
+
const OFFICIAL_RESOURCE_SOURCES = OFFICIAL_RESOURCE_ROOT_DOMAINS.flatMap((rootDomain) => [`https://${rootDomain}`, `https://*.${rootDomain}`]);
|
|
12797
13020
|
const URL_ATTRIBUTES = {
|
|
12798
13021
|
audio: ['src'],
|
|
12799
13022
|
embed: ['src'],
|
|
@@ -12830,14 +13053,12 @@ function enforceMiniappHtmlPolicy(html, options = {}) {
|
|
|
12830
13053
|
throw new Error('入口 index.html 必须包含 <head>');
|
|
12831
13054
|
}
|
|
12832
13055
|
removePlatformCspMarkers(document);
|
|
12833
|
-
walk(document, element => validateElement(element));
|
|
13056
|
+
walk(document, (element) => validateElement(element));
|
|
12834
13057
|
injectMiniappRuntimeGate(document);
|
|
12835
13058
|
if (options.skipPlatformCsp) {
|
|
12836
13059
|
return serialize(document);
|
|
12837
13060
|
}
|
|
12838
|
-
const content = options.hmrWebSocketUrl
|
|
12839
|
-
? PRODUCTION_CSP.replace("connect-src 'none'", `connect-src ${options.hmrWebSocketUrl}`)
|
|
12840
|
-
: PRODUCTION_CSP;
|
|
13061
|
+
const content = options.hmrWebSocketUrl ? PRODUCTION_CSP.replace("connect-src 'none'", `connect-src ${options.hmrWebSocketUrl}`) : PRODUCTION_CSP;
|
|
12841
13062
|
head.childNodes.unshift({
|
|
12842
13063
|
nodeName: 'meta',
|
|
12843
13064
|
tagName: 'meta',
|
|
@@ -12855,7 +13076,7 @@ function enforceMiniappHtmlPolicy(html, options = {}) {
|
|
|
12855
13076
|
}
|
|
12856
13077
|
function validateElement(element) {
|
|
12857
13078
|
const tagName = element.tagName.toLowerCase();
|
|
12858
|
-
const attrs = new Map(element.attrs.map(attr => [attr.name.toLowerCase(), attr.value]));
|
|
13079
|
+
const attrs = new Map(element.attrs.map((attr) => [attr.name.toLowerCase(), attr.value]));
|
|
12859
13080
|
if (tagName === 'meta' && attrs.get('http-equiv')?.toLowerCase() === 'refresh') {
|
|
12860
13081
|
throw new Error('index.html 不允许 meta refresh');
|
|
12861
13082
|
}
|
|
@@ -12867,7 +13088,7 @@ function validateElement(element) {
|
|
|
12867
13088
|
}
|
|
12868
13089
|
if (tagName === 'link') {
|
|
12869
13090
|
const rels = (attrs.get('rel') ?? '').toLowerCase().split(/\s+/).filter(Boolean);
|
|
12870
|
-
const forbidden = rels.find(rel => FORBIDDEN_RESOURCE_HINTS.has(rel));
|
|
13091
|
+
const forbidden = rels.find((rel) => FORBIDDEN_RESOURCE_HINTS.has(rel));
|
|
12871
13092
|
if (forbidden) {
|
|
12872
13093
|
throw new Error(`index.html 不允许 rel="${forbidden}"`);
|
|
12873
13094
|
}
|
|
@@ -12884,7 +13105,10 @@ function validateElement(element) {
|
|
|
12884
13105
|
for (const attribute of URL_ATTRIBUTES[tagName] ?? []) {
|
|
12885
13106
|
const value = attrs.get(attribute);
|
|
12886
13107
|
if (attribute === 'srcset') {
|
|
12887
|
-
for (const candidate of (value ?? '')
|
|
13108
|
+
for (const candidate of (value ?? '')
|
|
13109
|
+
.split(',')
|
|
13110
|
+
.map((item) => item.trim().split(/\s+/)[0])
|
|
13111
|
+
.filter(Boolean)) {
|
|
12888
13112
|
validateResourceUrl(candidate, `<${tagName} ${attribute}>`, true, supportsOfficialResourceUrl(tagName, attribute));
|
|
12889
13113
|
}
|
|
12890
13114
|
}
|
|
@@ -12896,10 +13120,10 @@ function validateElement(element) {
|
|
|
12896
13120
|
function removePlatformCspMarkers(node) {
|
|
12897
13121
|
if (!('childNodes' in node))
|
|
12898
13122
|
return;
|
|
12899
|
-
node.childNodes = node.childNodes.filter(child => {
|
|
13123
|
+
node.childNodes = node.childNodes.filter((child) => {
|
|
12900
13124
|
if (!('tagName' in child))
|
|
12901
13125
|
return true;
|
|
12902
|
-
return !child.attrs.some(attr => attr.name.toLowerCase() === 'data-heybox-platform-csp');
|
|
13126
|
+
return !child.attrs.some((attr) => attr.name.toLowerCase() === 'data-heybox-platform-csp');
|
|
12903
13127
|
});
|
|
12904
13128
|
for (const child of node.childNodes)
|
|
12905
13129
|
removePlatformCspMarkers(child);
|
|
@@ -12935,7 +13159,7 @@ function isOfficialResourceUrl(value) {
|
|
|
12935
13159
|
const hostname = url.hostname.toLowerCase();
|
|
12936
13160
|
if (hostname.endsWith('.'))
|
|
12937
13161
|
return false;
|
|
12938
|
-
return OFFICIAL_RESOURCE_ROOT_DOMAINS.some(rootDomain => hostname === rootDomain || hostname.endsWith(`.${rootDomain}`));
|
|
13162
|
+
return OFFICIAL_RESOURCE_ROOT_DOMAINS.some((rootDomain) => hostname === rootDomain || hostname.endsWith(`.${rootDomain}`));
|
|
12939
13163
|
}
|
|
12940
13164
|
catch {
|
|
12941
13165
|
return false;
|
|
@@ -12960,7 +13184,7 @@ function isRelativeUrl(value, normalized) {
|
|
|
12960
13184
|
}
|
|
12961
13185
|
function findElement(node, tagName) {
|
|
12962
13186
|
let found;
|
|
12963
|
-
walk(node, element => {
|
|
13187
|
+
walk(node, (element) => {
|
|
12964
13188
|
if (!found && element.tagName === tagName)
|
|
12965
13189
|
found = element;
|
|
12966
13190
|
});
|
|
@@ -13025,7 +13249,19 @@ const DEV_ERROR_MESSAGE = 'Companion 开发资源暂时不可用';
|
|
|
13025
13249
|
/** 与 Companion manifest 一致的单响应上限,避免本地调试入口被大体积声明撑爆。 */
|
|
13026
13250
|
const DEV_MINI_PROGRAM_MANIFEST_MAX_BYTES = 1024 * 1024;
|
|
13027
13251
|
const DEV_COMPANION_PATH_PATTERN = /^\/__hb_sdk__\/companions\/([a-z][a-z0-9-]{0,31})\/(windows-x64|macos-arm64)\.zip$/;
|
|
13252
|
+
/**
|
|
13253
|
+
* `hb-sdk dev` 为本地 Companion 产物访问注入的进程环境变量名。
|
|
13254
|
+
*
|
|
13255
|
+
* @remarks
|
|
13256
|
+
* 该变量由 CLI 管理,不是项目配置项。应用代码不应读取、设置、记录或转发它的值。
|
|
13257
|
+
*/
|
|
13028
13258
|
const HB_SDK_COMPANION_ARTIFACT_ACCESS_TOKEN_ENV = 'HB_SDK_COMPANION_ARTIFACT_ACCESS_TOKEN';
|
|
13259
|
+
/**
|
|
13260
|
+
* `hb-sdk dev` 为本地小程序 Manifest 访问注入的进程环境变量名。
|
|
13261
|
+
*
|
|
13262
|
+
* @remarks
|
|
13263
|
+
* 该变量由 CLI 管理,不是项目配置项。应用代码不应读取、设置、记录或转发它的值。
|
|
13264
|
+
*/
|
|
13029
13265
|
const HB_SDK_MINI_PROGRAM_MANIFEST_ACCESS_TOKEN_ENV = 'HB_SDK_MINI_PROGRAM_MANIFEST_ACCESS_TOKEN';
|
|
13030
13266
|
const devSnapshotStoreKey = Symbol.for('@heybox/hb-sdk/dev-companion-snapshots');
|
|
13031
13267
|
const devSnapshotRegistry = globalThis;
|
|
@@ -13039,6 +13275,104 @@ if (!devSnapshotRegistry[devSnapshotStoreKey]) {
|
|
|
13039
13275
|
});
|
|
13040
13276
|
}
|
|
13041
13277
|
const devSnapshotStores = devSnapshotRegistry[devSnapshotStoreKey];
|
|
13278
|
+
/**
|
|
13279
|
+
* 创建工坊小程序使用的 Vite 插件。
|
|
13280
|
+
*
|
|
13281
|
+
* @returns 可直接加入 Vite `plugins` 数组的
|
|
13282
|
+
* [MiniappManifestPlugin](/reference/symbols/vite/interfaces/MiniappManifestPlugin)。插件固定名为
|
|
13283
|
+
* `heybox-miniapp-manifest`,并提供 `config`、`configResolved`、`configureServer`、`buildEnd`、
|
|
13284
|
+
* `transformIndexHtml`、`closeBundle` hooks;这些 hooks 由 Vite 调用,应用代码不应手动调用。
|
|
13285
|
+
* @throws `Error` `config` 阶段发现 Vite `base` 不是 `''` 或 `'./'`,或无法解析当前 SDK 版本时抛出。
|
|
13286
|
+
* @throws `Error` `configResolved` 阶段发现已解析插件列表中 `miniappManifest()` 数量不是一个时抛出。
|
|
13287
|
+
* @throws `Error` HTML 缺少 `<head>`,包含 `meta refresh`、`<base>`、禁止的 resource hint,或资源 URL
|
|
13288
|
+
* 不符合小程序 HTML 策略时,`transformIndexHtml` / 生产产物复检抛出。
|
|
13289
|
+
* @throws `Error` 生产构建无法读取或解析最近的 `package.json`,`version`、`heybox.platforms`、
|
|
13290
|
+
* `heybox.permissions`、`heybox.window`、`heybox.bindAppIds` 或 `heybox.companions` 声明非法时抛出。
|
|
13291
|
+
* @throws `Error` 生产 `build.outDir` 缺少根 `index.html`、递归发现额外 HTML,或 Companion 权限、
|
|
13292
|
+
* 目标平台、来源目录、归档结构、入口二进制不符合约束时抛出;更早的 Rollup/Vite 错误会原样优先重抛。
|
|
13293
|
+
*
|
|
13294
|
+
* @remarks
|
|
13295
|
+
* 该函数必须从 `@heybox/hb-sdk/vite` 导入,在 `vite.config.ts` 中无参调用且只注册一次;它不是
|
|
13296
|
+
* iframe 业务代码 API,也没有 options 对象。小程序声明统一来自从 Vite `root` 向上找到的最近一个
|
|
13297
|
+
* `package.json`,插件只读取和规范化,不会改写源文件:
|
|
13298
|
+
*
|
|
13299
|
+
* - `version`:发布版本;trim 后必须是完整 SemVer,可含 prerelease,不允许 build metadata 或模板值
|
|
13300
|
+
* `0.0.0`;
|
|
13301
|
+
* - `heybox.platforms`:项目实际承诺适配的非空、无重复平台 allowlist;输出按平台词表规范排序;
|
|
13302
|
+
* - `heybox.permissions`:严格权限对象;未知权限/字段或错误类型会失败,`enabled: false` 项不写入
|
|
13303
|
+
* Manifest。旧版本缺失声明时可能警告,新硬切版本会失败;
|
|
13304
|
+
* - `heybox.window`:可选的新 PC 开窗初值、最小尺寸和 `resizable` 声明;省略时不写该字段;
|
|
13305
|
+
* - `heybox.bindAppIds`:可选的最多 20 个正 int32 字符串 App ID;校验、去重并按数值排序,空数组
|
|
13306
|
+
* 在本地产物中等同不写;
|
|
13307
|
+
* - `heybox.companions`:可选的受管桌面程序声明;`source` 相对上述 `package.json` 所在目录解析,
|
|
13308
|
+
* 必须同时声明启用的 `companion` 权限和匹配的 Windows/macOS 目标平台。
|
|
13309
|
+
*
|
|
13310
|
+
* 产物中的 `sdkVersion` 来自当前实际安装的 `@heybox/hb-sdk`,不是项目可配置字段。Vite options、页面
|
|
13311
|
+
* 代码和普通环境变量不能覆盖上述 package 声明;插件也不会联网确认 App ID、平台批准或远端版本状态。
|
|
13312
|
+
*
|
|
13313
|
+
* Hook 行为如下:
|
|
13314
|
+
*
|
|
13315
|
+
* - `config`:省略 `base` 时补为 `'./'`,显式值只接受 `''` / `'./'`;注入 SDK 构建版本,且仅
|
|
13316
|
+
* `vite serve` 注入开发日志编译常量;
|
|
13317
|
+
* - `configResolved`:确认插件唯一性,记录真实 `root`、`build.outDir`、`serve|build` 命令和精确 HMR
|
|
13318
|
+
* WebSocket 地址;直接 `vite build` 使用该实际 `outDir`,`hb-sdk build` 则由 CLI 固定为 `dist/`;
|
|
13319
|
+
* - `configureServer`:为 `hb-sdk dev` 注册设备日志、本地小程序 Manifest 与 Companion Manifest/ZIP
|
|
13320
|
+
* 中间件;敏感路由只接受数值 loopback socket/Host、`GET|HEAD` 和 CLI 注入的独立短期令牌,快照在
|
|
13321
|
+
* 配置重载时按 digest 保留,并在服务真正关闭时清理;
|
|
13322
|
+
* - `transformIndexHtml`:校验 HTML URL/标签策略,注入 Runtime Gate;默认注入平台 CSP,serve 时
|
|
13323
|
+
* `connect-src` 只开放解析出的 HMR WebSocket,并额外注入调试日志 bootstrap;
|
|
13324
|
+
* - `buildEnd`:记住更早的 Rollup/Vite 错误,避免 `closeBundle` 的二次产物错误遮盖根因;
|
|
13325
|
+
* - `closeBundle`:serve 时直接完成;build 时复检最终 HTML,要求整个输出树只有根 `index.html`,再于
|
|
13326
|
+
* `build.outDir` 写入 `manifest.json`,并把 Companion 目录打包成
|
|
13327
|
+
* `companions/<alias>/<target>.zip`,记录 size、SHA-256、固定入口与 args。
|
|
13328
|
+
*
|
|
13329
|
+
* HTML 策略会隔离业务 script/modulepreload,直到父 Runtime Gate 以 namespace、版本、nonce、source
|
|
13330
|
+
* 以及 referrer 可确定时的 origin 完成探测;失败时显示只能在小黑盒 App 内打开的安全页。策略拒绝自动跳转、`<base>`、危险
|
|
13331
|
+
* resource hints 和不受支持的外部 URL,并按标签只允许相对 URL、明确允许的 `data:` / `blob:` 或
|
|
13332
|
+
* 小黑盒官方 HTTPS 资源。开发者自行提供的 CSP 会保留;插件管理的 CSP marker 可重复转换而不会叠加。
|
|
13333
|
+
*
|
|
13334
|
+
* 平台 CSP 默认始终保留,直接 `vite build` / `hb-sdk build` 不会因项目声明
|
|
13335
|
+
* `heybox.permissions.network.useOfficialDomain` 而跳过。只有官方 `hb-sdk dev` 或
|
|
13336
|
+
* `hb-sdk remote deploy` 将当前版本的 `network` 声明与远端批准结果求交并确认最终启用后,才会
|
|
13337
|
+
* 建立 verified 上下文供插件读取内部
|
|
13338
|
+
* `HB_SDK_RUNTIME_PERMISSION_CONTEXT` + `HB_SDK_RUNTIME_USE_OFFICIAL_DOMAIN` 组合跳过平台 CSP;单独设置
|
|
13339
|
+
* 任一值无效。0.8 兼容期对完全缺失权限声明的项目仍按迁移规则处理;`useOfficialDomain` 不参与
|
|
13340
|
+
* CSP 跳过决定。Runtime Gate、HTML 校验和 Manifest 生成始终保留。
|
|
13341
|
+
*
|
|
13342
|
+
* `HB_SDK_COMPANION_ARTIFACT_ACCESS_TOKEN`、`HB_SDK_MINI_PROGRAM_MANIFEST_ACCESS_TOKEN`、
|
|
13343
|
+
* `HB_SDK_SKIP_COMPANIONS` 以及上述 CSP 上下文均是 CLI/发布链路内部开关,不是项目配置项。它们在
|
|
13344
|
+
* 插件创建时捕获;开发服务配置重载只会从同一服务的内存快照恢复 CLI 令牌。业务代码和 Vite 配置
|
|
13345
|
+
* 不应手工设置、读取、记录或转发令牌值。正式声明必须继续写在 `package.json#heybox`,内部环境变量
|
|
13346
|
+
* 不能替代声明校验。
|
|
13347
|
+
*
|
|
13348
|
+
* `manifest.json` 是 release pipeline 元数据,不是部署后供页面 fetch 的 CDN 资源。发布链路使用
|
|
13349
|
+
* [isValidVersion](/reference/symbols/miniapp-publish/functions/isValidVersion) 复核版本、
|
|
13350
|
+
* [resolveMiniappManifestPlatforms](/reference/symbols/miniapp-publish/functions/resolveMiniappManifestPlatforms)
|
|
13351
|
+
* 解析平台硬切规则、
|
|
13352
|
+
* [parseMiniappBindAppIds](/reference/symbols/miniapp-publish/functions/parseMiniappBindAppIds) 复核游戏绑定,
|
|
13353
|
+
* 并通过 [shouldUploadDistFile](/reference/symbols/miniapp-publish/functions/shouldUploadDistFile) 将根
|
|
13354
|
+
* `manifest.json` 排除出普通 CDN 文件。Web/Companion 上传预算由
|
|
13355
|
+
* [validateUploadTotalSize](/reference/symbols/miniapp-publish/functions/validateUploadTotalSize) 等发布 helper
|
|
13356
|
+
* 独立校验,不能用插件成功代替 deploy 预检。
|
|
13357
|
+
*
|
|
13358
|
+
* @example
|
|
13359
|
+
* ```ts
|
|
13360
|
+
* import vue from '@vitejs/plugin-vue'
|
|
13361
|
+
* import { miniappManifest } from '@heybox/hb-sdk/vite'
|
|
13362
|
+
* import { defineConfig } from 'vite'
|
|
13363
|
+
*
|
|
13364
|
+
* export default defineConfig({
|
|
13365
|
+
* // base 省略时 miniappManifest() 会补为 './'
|
|
13366
|
+
* plugins: [vue(), miniappManifest()],
|
|
13367
|
+
* })
|
|
13368
|
+
* ```
|
|
13369
|
+
*
|
|
13370
|
+
* @see [MiniappManifestPlugin](/reference/symbols/vite/interfaces/MiniappManifestPlugin) 返回插件及各 hook 类型。
|
|
13371
|
+
* @see [安装](/guide/installation) Vite 接入、相对资源路径和构建命令。
|
|
13372
|
+
* @see [权限声明](/guide/permissions) `heybox.permissions` 严格 schema 与迁移规则。
|
|
13373
|
+
* @see [窗口尺寸与可缩放性](/guide/window-size) `heybox.window` 字段与 Host 消费语义。
|
|
13374
|
+
* @see [受管桌面程序](/guide/companion) Companion 声明、归档和本地联调边界。
|
|
13375
|
+
*/
|
|
13042
13376
|
function miniappManifest() {
|
|
13043
13377
|
const includeCompanions = process.env.HB_SDK_SKIP_COMPANIONS !== '1';
|
|
13044
13378
|
const deviceLogs = new DeviceLogStore();
|
|
@@ -13357,10 +13691,12 @@ function miniappManifest() {
|
|
|
13357
13691
|
throw new Error('构建产物必须包含入口 dist/index.html');
|
|
13358
13692
|
}
|
|
13359
13693
|
node_fs.writeFileSync(indexHtmlPath, enforceMiniappHtmlPolicy(node_fs.readFileSync(indexHtmlPath, 'utf8'), { skipPlatformCsp }));
|
|
13694
|
+
const basicInfo = await prepareMiniappBasicInfo(companionConfig.projectRoot, outputRoot, sdkVersion);
|
|
13360
13695
|
const manifestPath = path.join(outputRoot, 'manifest.json');
|
|
13361
13696
|
const companions = prepareMiniappCompanions(companionDeclarations, { root: companionConfig.projectRoot, outputRoot, platforms });
|
|
13362
13697
|
node_fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
|
|
13363
13698
|
node_fs.writeFileSync(manifestPath, renderMiniappManifest({
|
|
13699
|
+
...basicInfo,
|
|
13364
13700
|
version,
|
|
13365
13701
|
sdkVersion,
|
|
13366
13702
|
platforms,
|