@heybox/hb-sdk 0.1.3 → 0.2.0-alpha.1
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/README.md +149 -345
- package/bin/hb-sdk.cjs +3 -0
- package/dist/cli.cjs +10117 -0
- package/dist/devtools/mock-host/index.html +252 -0
- package/dist/devtools/mock-host/main.js +975 -0
- package/dist/index.cjs.js +474 -85
- package/dist/index.esm.js +465 -71
- package/dist/protocol.cjs.js +163 -0
- package/dist/protocol.esm.js +148 -0
- package/dist/templates/vue3-vite-ts/.gitignore.ejs +5 -0
- package/dist/templates/vue3-vite-ts/README.md.ejs +42 -0
- package/dist/templates/vue3-vite-ts/index.html.ejs +12 -0
- package/dist/templates/vue3-vite-ts/package.json.ejs +28 -0
- package/dist/templates/vue3-vite-ts/src/App.vue +63 -0
- package/dist/templates/vue3-vite-ts/src/__tests__/App.spec.ts +67 -0
- package/dist/templates/vue3-vite-ts/src/main.ts +5 -0
- package/dist/templates/vue3-vite-ts/src/styles.css +60 -0
- package/dist/templates/vue3-vite-ts/src/vite-env.d.ts +1 -0
- package/dist/templates/vue3-vite-ts/tsconfig.app.json +17 -0
- package/dist/templates/vue3-vite-ts/tsconfig.json +11 -0
- package/dist/templates/vue3-vite-ts/tsconfig.node.json +11 -0
- package/dist/templates/vue3-vite-ts/vite.config.ts +6 -0
- package/dist/templates/vue3-vite-ts/vitest.config.ts +10 -0
- package/package.json +30 -5
- package/skill/SKILL.md +95 -0
- package/skill/references/api-protocol.md +135 -0
- package/skill/references/api-root.md +346 -0
- package/skill/references/cli.md +360 -0
- package/skill/references/examples.md +107 -0
- package/skill/references/llms-index.md +44 -0
- package/skill/references/recipes.md +374 -0
- package/skill/references/safety-boundaries.md +28 -0
- package/skill/references/smoke-evaluation.md +24 -0
- package/skill/scripts/check-references.mjs +14 -0
- package/skill/scripts/package-skill.mjs +60 -0
- package/skill/scripts/package-skill.sh +6 -0
- package/skill/scripts/skill-metadata.mjs +74 -0
- package/skill/scripts/sync-references.mjs +541 -0
- package/skill/scripts/validate-skill.mjs +233 -0
- package/skill/skill.json +11 -0
- package/types/core/client.d.ts +23 -3
- package/types/core/errors.d.ts +45 -2
- package/types/core/sdk.d.ts +78 -10
- package/types/core/singleton.d.ts +33 -7
- package/types/core/utils.d.ts +2 -0
- package/types/index.d.ts +14 -6
- package/types/modules/auth/index.d.ts +35 -0
- package/types/modules/network/index.d.ts +120 -0
- package/types/modules/share/index.d.ts +9 -5
- package/types/modules/share/screenshot.d.ts +9 -3
- package/types/modules/share/show-share-menu.d.ts +9 -3
- package/types/modules/share/types.d.ts +24 -4
- package/types/modules/storage/index.d.ts +56 -0
- package/types/modules/user/get-info.d.ts +6 -2
- package/types/modules/user/index.d.ts +8 -10
- package/types/modules/user/types.d.ts +1 -0
- package/types/modules/viewport/index.d.ts +71 -0
- package/types/protocol/capabilities.d.ts +180 -0
- package/types/protocol/guards.d.ts +6 -1
- package/types/protocol/types.d.ts +19 -4
- package/types/protocol.d.ts +13 -0
- package/types/modules/system/get-storage.d.ts +0 -15
- package/types/modules/system/get-window-info.d.ts +0 -16
- package/types/modules/system/index.d.ts +0 -23
- package/types/modules/system/set-storage.d.ts +0 -12
- package/types/modules/system/types.d.ts +0 -34
- package/types/modules/user/login.d.ts +0 -18
|
@@ -0,0 +1,975 @@
|
|
|
1
|
+
/** 小程序沙盒通信命名空间,父容器与 iframe 内 SDK 必须保持一致。 */
|
|
2
|
+
const MINI_PROGRAM_MESSAGE_NAMESPACE = 'heybox:miniprogram';
|
|
3
|
+
/** 小程序沙盒通信协议版本。 */
|
|
4
|
+
const MINI_PROGRAM_MESSAGE_VERSION = 1;
|
|
5
|
+
/** 父容器注入到小程序 URL 上的通信 nonce 参数名。 */
|
|
6
|
+
const MINI_PROGRAM_BRIDGE_NONCE_PARAM = 'hb_mini_bridge_nonce';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 登录授权能力方法名。
|
|
10
|
+
*
|
|
11
|
+
* @remarks
|
|
12
|
+
* 供 SDK 与父容器 runtime 共享同一 bridge method 标识。
|
|
13
|
+
*/
|
|
14
|
+
const AUTH_LOGIN_METHOD = 'auth.login';
|
|
15
|
+
/**
|
|
16
|
+
* 用户信息能力方法名。
|
|
17
|
+
*
|
|
18
|
+
* @remarks
|
|
19
|
+
* 供 SDK 与父容器 runtime 共享同一 bridge method 标识。
|
|
20
|
+
*/
|
|
21
|
+
const USER_GET_INFO_METHOD = 'user.getInfo';
|
|
22
|
+
/**
|
|
23
|
+
* 展示分享面板能力方法名。
|
|
24
|
+
*
|
|
25
|
+
* @remarks
|
|
26
|
+
* 供 SDK 与父容器 runtime 共享同一 bridge method 标识。
|
|
27
|
+
*/
|
|
28
|
+
const SHARE_SHOW_SHARE_MENU_METHOD = 'share.showShareMenu';
|
|
29
|
+
/**
|
|
30
|
+
* 截图分享能力方法名。
|
|
31
|
+
*
|
|
32
|
+
* @remarks
|
|
33
|
+
* 供 SDK 与父容器 runtime 共享同一 bridge method 标识。
|
|
34
|
+
*/
|
|
35
|
+
const SHARE_SCREENSHOT_METHOD = 'share.screenshot';
|
|
36
|
+
/**
|
|
37
|
+
* 视口窗口信息能力方法名。
|
|
38
|
+
*
|
|
39
|
+
* @remarks
|
|
40
|
+
* 供 SDK 与父容器 runtime 共享同一 bridge method 标识。
|
|
41
|
+
*/
|
|
42
|
+
const VIEWPORT_GET_WINDOW_INFO_METHOD = 'viewport.getWindowInfo';
|
|
43
|
+
/**
|
|
44
|
+
* 读取小程序隔离 storage 能力方法名。
|
|
45
|
+
*
|
|
46
|
+
* @remarks
|
|
47
|
+
* 供 SDK 与父容器 runtime 共享同一 bridge method 标识。
|
|
48
|
+
*/
|
|
49
|
+
const STORAGE_GET_STORAGE_METHOD = 'storage.getStorage';
|
|
50
|
+
/**
|
|
51
|
+
* 写入小程序隔离 storage 能力方法名。
|
|
52
|
+
*
|
|
53
|
+
* @remarks
|
|
54
|
+
* 供 SDK 与父容器 runtime 共享同一 bridge method 标识。
|
|
55
|
+
*/
|
|
56
|
+
const STORAGE_SET_STORAGE_METHOD = 'storage.setStorage';
|
|
57
|
+
/**
|
|
58
|
+
* 发起网络请求能力方法名。
|
|
59
|
+
*
|
|
60
|
+
* @remarks
|
|
61
|
+
* 供 SDK 与父容器 runtime 共享同一 bridge method 标识。
|
|
62
|
+
*/
|
|
63
|
+
const NETWORK_REQUEST_METHOD = 'network.request';
|
|
64
|
+
/**
|
|
65
|
+
* 小程序开放能力目录。
|
|
66
|
+
*
|
|
67
|
+
* @remarks
|
|
68
|
+
* 新增 bridge method 时应先更新这里,再分别补齐 SDK 模块、runtime handler 与测试。
|
|
69
|
+
*/
|
|
70
|
+
const MINI_PROGRAM_PROTOCOL_CAPABILITIES = [
|
|
71
|
+
{
|
|
72
|
+
method: AUTH_LOGIN_METHOD,
|
|
73
|
+
module: 'auth',
|
|
74
|
+
capability: AUTH_LOGIN_METHOD,
|
|
75
|
+
permission: 'auth.login',
|
|
76
|
+
risk: 'medium',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
method: USER_GET_INFO_METHOD,
|
|
80
|
+
module: 'user',
|
|
81
|
+
capability: USER_GET_INFO_METHOD,
|
|
82
|
+
permission: 'user.getInfo',
|
|
83
|
+
risk: 'medium',
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
method: SHARE_SHOW_SHARE_MENU_METHOD,
|
|
87
|
+
module: 'share',
|
|
88
|
+
capability: SHARE_SHOW_SHARE_MENU_METHOD,
|
|
89
|
+
permission: 'share.basic',
|
|
90
|
+
risk: 'low',
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
method: SHARE_SCREENSHOT_METHOD,
|
|
94
|
+
module: 'share',
|
|
95
|
+
capability: SHARE_SCREENSHOT_METHOD,
|
|
96
|
+
permission: 'share.screenshot',
|
|
97
|
+
risk: 'medium',
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
method: VIEWPORT_GET_WINDOW_INFO_METHOD,
|
|
101
|
+
module: 'viewport',
|
|
102
|
+
capability: VIEWPORT_GET_WINDOW_INFO_METHOD,
|
|
103
|
+
permission: 'viewport.windowInfo',
|
|
104
|
+
risk: 'low',
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
method: STORAGE_GET_STORAGE_METHOD,
|
|
108
|
+
module: 'storage',
|
|
109
|
+
capability: STORAGE_GET_STORAGE_METHOD,
|
|
110
|
+
permission: 'storage.read',
|
|
111
|
+
risk: 'low',
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
method: STORAGE_SET_STORAGE_METHOD,
|
|
115
|
+
module: 'storage',
|
|
116
|
+
capability: STORAGE_SET_STORAGE_METHOD,
|
|
117
|
+
permission: 'storage.write',
|
|
118
|
+
risk: 'medium',
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
method: NETWORK_REQUEST_METHOD,
|
|
122
|
+
module: 'network',
|
|
123
|
+
capability: NETWORK_REQUEST_METHOD,
|
|
124
|
+
permission: 'network.request',
|
|
125
|
+
risk: 'high',
|
|
126
|
+
},
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
const MINI_PROGRAM_URL_QUERY_PARAM = 'mini_url';
|
|
130
|
+
const MINI_PROGRAM_DEV_QUERY_PARAM = 'hb_mini_dev';
|
|
131
|
+
const MINI_PROGRAM_DETAIL_URL = 'https://www.xiaoheihe.cn/tools/user_miniprogram/detail';
|
|
132
|
+
function createMacAppProtocol(appUrl) {
|
|
133
|
+
const detailUrl = new URL(MINI_PROGRAM_DETAIL_URL);
|
|
134
|
+
detailUrl.searchParams.set(MINI_PROGRAM_URL_QUERY_PARAM, appUrl);
|
|
135
|
+
detailUrl.searchParams.set(MINI_PROGRAM_DEV_QUERY_PARAM, '1');
|
|
136
|
+
const protocolPayload = {
|
|
137
|
+
protocol_type: 'openWindow',
|
|
138
|
+
full_screen: true,
|
|
139
|
+
mini_program: '1',
|
|
140
|
+
webview: {
|
|
141
|
+
url: detailUrl.toString(),
|
|
142
|
+
pull: false,
|
|
143
|
+
refresh: false,
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
return `heybox://${encodeURIComponent(JSON.stringify(protocolPayload))}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const DEFAULT_STORAGE_KEY_MAX_LENGTH = 128;
|
|
150
|
+
const STORAGE_KEY_CHAR_RE = /^[A-Za-z0-9_-]+$/;
|
|
151
|
+
const NETWORK_REQUEST_ALLOWED_KEYS = new Set([
|
|
152
|
+
'url',
|
|
153
|
+
'method',
|
|
154
|
+
'params',
|
|
155
|
+
'data',
|
|
156
|
+
'headers',
|
|
157
|
+
'timeout',
|
|
158
|
+
'withCredentials',
|
|
159
|
+
]);
|
|
160
|
+
const NETWORK_HTTP_METHODS = new Set([
|
|
161
|
+
'GET',
|
|
162
|
+
'POST',
|
|
163
|
+
'PUT',
|
|
164
|
+
'PATCH',
|
|
165
|
+
'DELETE',
|
|
166
|
+
'HEAD',
|
|
167
|
+
'OPTIONS',
|
|
168
|
+
]);
|
|
169
|
+
function createMiniProgramRuntimeBridgeError(code, message, data) {
|
|
170
|
+
return {
|
|
171
|
+
code,
|
|
172
|
+
message,
|
|
173
|
+
}
|
|
174
|
+
;
|
|
175
|
+
}
|
|
176
|
+
function isMiniProgramRuntimeRecord(value) {
|
|
177
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
178
|
+
}
|
|
179
|
+
function assertMiniProgramRuntimeRecord(value, message) {
|
|
180
|
+
if (!isMiniProgramRuntimeRecord(value)) {
|
|
181
|
+
throw createMiniProgramRuntimeBridgeError('INVALID_PARAMS', message);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function readMiniProgramRuntimeRequiredString(value, message) {
|
|
185
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
186
|
+
throw createMiniProgramRuntimeBridgeError('INVALID_PARAMS', message);
|
|
187
|
+
}
|
|
188
|
+
return value;
|
|
189
|
+
}
|
|
190
|
+
function readMiniProgramRuntimeBoolean(value, message) {
|
|
191
|
+
if (typeof value !== 'boolean') {
|
|
192
|
+
throw createMiniProgramRuntimeBridgeError('INVALID_PARAMS', message);
|
|
193
|
+
}
|
|
194
|
+
return value;
|
|
195
|
+
}
|
|
196
|
+
function readMiniProgramRuntimeHttpUrl(value, message) {
|
|
197
|
+
const urlValue = readMiniProgramRuntimeRequiredString(value, message);
|
|
198
|
+
try {
|
|
199
|
+
const url = new URL(urlValue);
|
|
200
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
201
|
+
throw new Error('invalid protocol');
|
|
202
|
+
}
|
|
203
|
+
return urlValue;
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
throw createMiniProgramRuntimeBridgeError('INVALID_PARAMS', message);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function readMiniProgramRuntimeNonNegativeNumber(value, message) {
|
|
210
|
+
const numberValue = Number(value);
|
|
211
|
+
if (!Number.isFinite(numberValue) || numberValue < 0) {
|
|
212
|
+
throw createMiniProgramRuntimeBridgeError('INVALID_PARAMS', message);
|
|
213
|
+
}
|
|
214
|
+
return numberValue;
|
|
215
|
+
}
|
|
216
|
+
function readMiniProgramRuntimePositiveNumber(value, message) {
|
|
217
|
+
const numberValue = Number(value);
|
|
218
|
+
if (!Number.isFinite(numberValue) || numberValue <= 0) {
|
|
219
|
+
throw createMiniProgramRuntimeBridgeError('INVALID_PARAMS', message);
|
|
220
|
+
}
|
|
221
|
+
return numberValue;
|
|
222
|
+
}
|
|
223
|
+
function readMiniProgramRuntimeStorageKey(value, methodName, maxKeyLength = DEFAULT_STORAGE_KEY_MAX_LENGTH) {
|
|
224
|
+
if (typeof value !== 'string') {
|
|
225
|
+
throw createMiniProgramRuntimeBridgeError('INVALID_PARAMS', `${methodName} key 必须是字符串`);
|
|
226
|
+
}
|
|
227
|
+
if (!value || value.length > maxKeyLength || !STORAGE_KEY_CHAR_RE.test(value)) {
|
|
228
|
+
throw createMiniProgramRuntimeBridgeError('INVALID_PARAMS', `storage key 仅支持 1-${maxKeyLength} 位字母、数字、下划线和连字符`);
|
|
229
|
+
}
|
|
230
|
+
return value;
|
|
231
|
+
}
|
|
232
|
+
function getMiniProgramRuntimeUtf8ByteLength(value) {
|
|
233
|
+
return new TextEncoder().encode(value).length;
|
|
234
|
+
}
|
|
235
|
+
function stringifyMiniProgramRuntimeJsonData(data, options) {
|
|
236
|
+
let value;
|
|
237
|
+
try {
|
|
238
|
+
value = JSON.stringify(data);
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
throw createMiniProgramRuntimeBridgeError('INVALID_PARAMS', options.invalidMessage);
|
|
242
|
+
}
|
|
243
|
+
if (typeof value !== 'string') {
|
|
244
|
+
throw createMiniProgramRuntimeBridgeError('INVALID_PARAMS', options.invalidMessage);
|
|
245
|
+
}
|
|
246
|
+
if (options.maxValueBytes !== undefined && getMiniProgramRuntimeUtf8ByteLength(value) > options.maxValueBytes) {
|
|
247
|
+
throw createMiniProgramRuntimeBridgeError('STORAGE_QUOTA_EXCEEDED', `storage.setStorage data 超出 ${options.maxValueBytes} 字节限制`);
|
|
248
|
+
}
|
|
249
|
+
return value;
|
|
250
|
+
}
|
|
251
|
+
async function getMiniProgramRuntimeUserInfo(platformAdapter) {
|
|
252
|
+
const userId = await resolveCurrentUserId(platformAdapter);
|
|
253
|
+
if (!userId) {
|
|
254
|
+
return {
|
|
255
|
+
isLogin: false,
|
|
256
|
+
userInfo: null,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
const result = await platformAdapter.user.getUserBasicInfo(userId);
|
|
260
|
+
return {
|
|
261
|
+
isLogin: true,
|
|
262
|
+
userInfo: {
|
|
263
|
+
heybox_id: userId,
|
|
264
|
+
nickname: result?.nickname || '',
|
|
265
|
+
avatar: result?.avatar || '',
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
async function loginAndGetMiniProgramRuntimeUserInfo(platformAdapter) {
|
|
270
|
+
await platformAdapter.auth.login();
|
|
271
|
+
return getMiniProgramRuntimeUserInfo(platformAdapter);
|
|
272
|
+
}
|
|
273
|
+
function getMiniProgramRuntimeWindowInfo(platformAdapter) {
|
|
274
|
+
const metrics = platformAdapter.viewport.getViewportMetrics();
|
|
275
|
+
const windowWidth = getViewportWidth(metrics);
|
|
276
|
+
const windowHeight = getViewportHeight(metrics);
|
|
277
|
+
const screenWidth = getPositiveNumber(metrics.screenWidth, windowWidth);
|
|
278
|
+
const screenHeight = getPositiveNumber(metrics.screenHeight, windowHeight);
|
|
279
|
+
const navigationBarHeight = getNavigationBarHeight(platformAdapter);
|
|
280
|
+
const safeAreaHeight = Math.max(windowHeight - navigationBarHeight, 0);
|
|
281
|
+
return {
|
|
282
|
+
pixelRatio: getPositiveNumber(metrics.pixelRatio, 1),
|
|
283
|
+
screenWidth,
|
|
284
|
+
screenHeight,
|
|
285
|
+
windowWidth,
|
|
286
|
+
windowHeight,
|
|
287
|
+
statusBarHeight: navigationBarHeight,
|
|
288
|
+
safeArea: {
|
|
289
|
+
left: 0,
|
|
290
|
+
right: windowWidth,
|
|
291
|
+
top: navigationBarHeight,
|
|
292
|
+
bottom: windowHeight,
|
|
293
|
+
width: windowWidth,
|
|
294
|
+
height: safeAreaHeight,
|
|
295
|
+
},
|
|
296
|
+
screenTop: navigationBarHeight,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
function showMiniProgramRuntimeShareMenu(payload, platformAdapter) {
|
|
300
|
+
return platformAdapter.share.showShareMenu(readMiniProgramRuntimeShareMenuOptions(payload));
|
|
301
|
+
}
|
|
302
|
+
function shareMiniProgramRuntimeScreenshot(payload, platformAdapter) {
|
|
303
|
+
const options = readMiniProgramRuntimeScreenshotOptions(payload);
|
|
304
|
+
const rect = options.rect || getViewportRect(platformAdapter);
|
|
305
|
+
return platformAdapter.share.shareScreenshot({
|
|
306
|
+
rect,
|
|
307
|
+
delay: options.delay,
|
|
308
|
+
saveToAlbum: options.saveToAlbum,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
function getMiniProgramRuntimeStorage(payload, options, platformAdapter) {
|
|
312
|
+
const { key } = readMiniProgramRuntimeGetStoragePayload(payload, options.maxKeyLength);
|
|
313
|
+
const storageValue = platformAdapter.storage.getStorage({
|
|
314
|
+
key: createMiniProgramRuntimeStorageKey(key, options.scope || getMiniProgramRuntimeStorageScope(undefined, platformAdapter), options.maxKeyLength),
|
|
315
|
+
});
|
|
316
|
+
return {
|
|
317
|
+
data: parseStorageData(storageValue),
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
async function setMiniProgramRuntimeStorage(payload, options, platformAdapter) {
|
|
321
|
+
const { key, data } = readMiniProgramRuntimeSetStoragePayload(payload, options.maxKeyLength);
|
|
322
|
+
await platformAdapter.storage.setStorage({
|
|
323
|
+
key: createMiniProgramRuntimeStorageKey(key, options.scope || getMiniProgramRuntimeStorageScope(undefined, platformAdapter), options.maxKeyLength),
|
|
324
|
+
value: stringifyMiniProgramRuntimeJsonData(data, {
|
|
325
|
+
invalidMessage: 'storage.setStorage data 必须可 JSON 序列化',
|
|
326
|
+
maxValueBytes: options.maxValueBytes,
|
|
327
|
+
}),
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
function getMiniProgramRuntimeStorageScope(href, platformAdapter) {
|
|
331
|
+
const currentHref = platformAdapter.app.getCurrentHref();
|
|
332
|
+
try {
|
|
333
|
+
const url = new URL(currentHref);
|
|
334
|
+
const miniProgramId = url.searchParams.get('mini_program_id')?.trim();
|
|
335
|
+
if (miniProgramId) {
|
|
336
|
+
return `id_${toStorageScopeToken(miniProgramId)}`;
|
|
337
|
+
}
|
|
338
|
+
const miniUrl = url.searchParams.get('mini_url')?.trim();
|
|
339
|
+
if (miniUrl) {
|
|
340
|
+
return `url_${hashString(miniUrl)}`;
|
|
341
|
+
}
|
|
342
|
+
return `url_${hashString(`${url.origin}${url.pathname}`)}`;
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
return `url_${hashString(currentHref)}`;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
function createMiniProgramRuntimeStorageKey(key, storageScope, maxKeyLength) {
|
|
349
|
+
readMiniProgramRuntimeStorageKey(key, 'storage.setStorage', maxKeyLength);
|
|
350
|
+
return `hb_miniprogram_${storageScope}_${key}`;
|
|
351
|
+
}
|
|
352
|
+
async function requestMiniProgramRuntimeNetwork(payload, platformAdapter) {
|
|
353
|
+
return platformAdapter.network.request(readMiniProgramRuntimeNetworkRequest(payload));
|
|
354
|
+
}
|
|
355
|
+
function readMiniProgramRuntimeNetworkRequest(payload) {
|
|
356
|
+
assertMiniProgramRuntimeRecord(payload, 'network.request 参数必须是对象');
|
|
357
|
+
assertNetworkAllowedKeys(payload);
|
|
358
|
+
const request = {
|
|
359
|
+
url: readMiniProgramRuntimeHttpUrl(payload.url, 'network.request url 必须是 HTTP(S) URL'),
|
|
360
|
+
};
|
|
361
|
+
if (payload.method !== undefined) {
|
|
362
|
+
request.method = readNetworkMethod(payload.method);
|
|
363
|
+
}
|
|
364
|
+
if (payload.params !== undefined) {
|
|
365
|
+
request.params = readUnknownRecord(payload.params, 'network.request params 必须是对象');
|
|
366
|
+
}
|
|
367
|
+
if (payload.data !== undefined) {
|
|
368
|
+
request.data = payload.data;
|
|
369
|
+
}
|
|
370
|
+
if (payload.headers !== undefined) {
|
|
371
|
+
request.headers = readStringRecord(payload.headers, 'network.request headers 必须是 Record<string, string>');
|
|
372
|
+
}
|
|
373
|
+
if (payload.timeout !== undefined) {
|
|
374
|
+
request.timeout = readMiniProgramRuntimeNonNegativeNumber(payload.timeout, 'network.request timeout 必须是非负数字');
|
|
375
|
+
}
|
|
376
|
+
if (payload.withCredentials !== undefined) {
|
|
377
|
+
request.withCredentials = readMiniProgramRuntimeBoolean(payload.withCredentials, 'network.request withCredentials 必须是布尔值');
|
|
378
|
+
}
|
|
379
|
+
return request;
|
|
380
|
+
}
|
|
381
|
+
function readMiniProgramRuntimeShareMenuOptions(payload) {
|
|
382
|
+
assertMiniProgramRuntimeRecord(payload, 'share.showShareMenu 参数必须是对象');
|
|
383
|
+
const title = readMiniProgramRuntimeRequiredString(payload.title, 'share.showShareMenu title 必须是非空字符串');
|
|
384
|
+
const desc = readMiniProgramRuntimeRequiredString(payload.desc, 'share.showShareMenu desc 必须是非空字符串');
|
|
385
|
+
const url = readMiniProgramRuntimeHttpUrl(payload.url, 'share.showShareMenu url 必须是 HTTP(S) URL');
|
|
386
|
+
const imageUrl = payload.imageUrl === undefined
|
|
387
|
+
? undefined
|
|
388
|
+
: readMiniProgramRuntimeHttpUrl(payload.imageUrl, 'share.showShareMenu imageUrl 必须是 HTTP(S) URL');
|
|
389
|
+
const channel = payload.channel;
|
|
390
|
+
if (channel !== undefined && !isShareChannel(channel)) {
|
|
391
|
+
throw createMiniProgramRuntimeBridgeError('INVALID_PARAMS', 'share.showShareMenu channel 不合法');
|
|
392
|
+
}
|
|
393
|
+
return {
|
|
394
|
+
title,
|
|
395
|
+
desc,
|
|
396
|
+
url,
|
|
397
|
+
imageUrl,
|
|
398
|
+
channel,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
function readMiniProgramRuntimeScreenshotOptions(payload) {
|
|
402
|
+
if (payload === undefined || payload === null) {
|
|
403
|
+
return {};
|
|
404
|
+
}
|
|
405
|
+
assertMiniProgramRuntimeRecord(payload, 'share.screenshot 参数必须是对象');
|
|
406
|
+
const options = {};
|
|
407
|
+
if (payload.rect !== undefined) {
|
|
408
|
+
options.rect = readScreenshotRect(payload.rect);
|
|
409
|
+
}
|
|
410
|
+
if (payload.delay !== undefined) {
|
|
411
|
+
options.delay = readMiniProgramRuntimeNonNegativeNumber(payload.delay, 'share.screenshot delay 必须是非负数字');
|
|
412
|
+
}
|
|
413
|
+
if (payload.saveToAlbum !== undefined) {
|
|
414
|
+
options.saveToAlbum = readMiniProgramRuntimeBoolean(payload.saveToAlbum, 'share.screenshot saveToAlbum 必须是布尔值');
|
|
415
|
+
}
|
|
416
|
+
return options;
|
|
417
|
+
}
|
|
418
|
+
function readScreenshotRect(value) {
|
|
419
|
+
assertMiniProgramRuntimeRecord(value, 'share.screenshot rect 必须是对象');
|
|
420
|
+
const left = readMiniProgramRuntimeNonNegativeNumber(value.left, 'share.screenshot rect.left 必须是非负数字');
|
|
421
|
+
const top = readMiniProgramRuntimeNonNegativeNumber(value.top, 'share.screenshot rect.top 必须是非负数字');
|
|
422
|
+
const width = readMiniProgramRuntimePositiveNumber(value.width, 'share.screenshot rect.width 必须是正数');
|
|
423
|
+
const height = readMiniProgramRuntimePositiveNumber(value.height, 'share.screenshot rect.height 必须是正数');
|
|
424
|
+
return {
|
|
425
|
+
left,
|
|
426
|
+
top,
|
|
427
|
+
width,
|
|
428
|
+
height,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
function readMiniProgramRuntimeGetStoragePayload(payload, maxKeyLength) {
|
|
432
|
+
if (!isMiniProgramRuntimeRecord(payload)) {
|
|
433
|
+
throw createMiniProgramRuntimeBridgeError('INVALID_PARAMS', 'storage.getStorage 参数必须是对象');
|
|
434
|
+
}
|
|
435
|
+
const key = readMiniProgramRuntimeStorageKey(payload.key, 'storage.getStorage', maxKeyLength);
|
|
436
|
+
return {
|
|
437
|
+
key,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
function readMiniProgramRuntimeSetStoragePayload(payload, maxKeyLength) {
|
|
441
|
+
if (!isMiniProgramRuntimeRecord(payload)) {
|
|
442
|
+
throw createMiniProgramRuntimeBridgeError('INVALID_PARAMS', 'storage.setStorage 参数必须是对象');
|
|
443
|
+
}
|
|
444
|
+
const key = readMiniProgramRuntimeStorageKey(payload.key, 'storage.setStorage', maxKeyLength);
|
|
445
|
+
return {
|
|
446
|
+
key,
|
|
447
|
+
data: payload.data,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
function assertNetworkAllowedKeys(payload) {
|
|
451
|
+
const invalidKeys = Object.keys(payload).filter(key => !NETWORK_REQUEST_ALLOWED_KEYS.has(key));
|
|
452
|
+
if (invalidKeys.length > 0) {
|
|
453
|
+
throw createMiniProgramRuntimeBridgeError('INVALID_PARAMS', `network.request 不支持字段: ${invalidKeys.join(', ')}`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
function readNetworkMethod(value) {
|
|
457
|
+
const method = readMiniProgramRuntimeRequiredString(value, 'network.request method 必须是字符串').trim().toUpperCase();
|
|
458
|
+
if (!NETWORK_HTTP_METHODS.has(method)) {
|
|
459
|
+
throw createMiniProgramRuntimeBridgeError('INVALID_PARAMS', 'network.request method 不合法');
|
|
460
|
+
}
|
|
461
|
+
return method;
|
|
462
|
+
}
|
|
463
|
+
function readUnknownRecord(value, message) {
|
|
464
|
+
assertMiniProgramRuntimeRecord(value, message);
|
|
465
|
+
return { ...value };
|
|
466
|
+
}
|
|
467
|
+
function readStringRecord(value, message) {
|
|
468
|
+
assertMiniProgramRuntimeRecord(value, message);
|
|
469
|
+
return Object.entries(value).reduce((headers, [key, headerValue]) => {
|
|
470
|
+
if (!key.trim() || typeof headerValue !== 'string') {
|
|
471
|
+
throw createMiniProgramRuntimeBridgeError('INVALID_PARAMS', message);
|
|
472
|
+
}
|
|
473
|
+
headers[key] = headerValue;
|
|
474
|
+
return headers;
|
|
475
|
+
}, {});
|
|
476
|
+
}
|
|
477
|
+
function resolveCurrentUserId(platformAdapter) {
|
|
478
|
+
return platformAdapter.auth.getCurrentUserId()
|
|
479
|
+
.then(userId => (userId ? String(userId) : ''))
|
|
480
|
+
.catch(() => '');
|
|
481
|
+
}
|
|
482
|
+
function getNavigationBarHeight(platformAdapter) {
|
|
483
|
+
try {
|
|
484
|
+
return platformAdapter.viewport.getNavigationBarHeight();
|
|
485
|
+
}
|
|
486
|
+
catch {
|
|
487
|
+
return 0;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
function getViewportRect(platformAdapter) {
|
|
491
|
+
const metrics = platformAdapter.viewport.getViewportMetrics();
|
|
492
|
+
const width = getPositiveNumber(metrics.innerWidth, getPositiveNumber(metrics.documentElementClientWidth, 0));
|
|
493
|
+
const height = getPositiveNumber(metrics.innerHeight, getPositiveNumber(metrics.documentElementClientHeight, 0));
|
|
494
|
+
return {
|
|
495
|
+
left: 0,
|
|
496
|
+
top: 0,
|
|
497
|
+
width,
|
|
498
|
+
height,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
function getViewportWidth(metrics) {
|
|
502
|
+
return getPositiveNumber(metrics.innerWidth, getPositiveNumber(metrics.documentElementClientWidth, 0));
|
|
503
|
+
}
|
|
504
|
+
function getViewportHeight(metrics) {
|
|
505
|
+
return getPositiveNumber(metrics.innerHeight, getPositiveNumber(metrics.documentElementClientHeight, 0));
|
|
506
|
+
}
|
|
507
|
+
function getPositiveNumber(value, fallback) {
|
|
508
|
+
const numberValue = Number(value);
|
|
509
|
+
if (!Number.isFinite(numberValue) || numberValue <= 0) {
|
|
510
|
+
return fallback;
|
|
511
|
+
}
|
|
512
|
+
return numberValue;
|
|
513
|
+
}
|
|
514
|
+
function parseStorageData(value) {
|
|
515
|
+
if (value === undefined || value === null || value === '') {
|
|
516
|
+
return undefined;
|
|
517
|
+
}
|
|
518
|
+
try {
|
|
519
|
+
return JSON.parse(value);
|
|
520
|
+
}
|
|
521
|
+
catch {
|
|
522
|
+
return value;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
function toStorageScopeToken(value) {
|
|
526
|
+
if (/^[A-Za-z0-9_-]{1,64}$/.test(value)) {
|
|
527
|
+
return value;
|
|
528
|
+
}
|
|
529
|
+
return hashString(value);
|
|
530
|
+
}
|
|
531
|
+
function hashString(value) {
|
|
532
|
+
let hash = 2166136261;
|
|
533
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
534
|
+
hash ^= value.charCodeAt(index);
|
|
535
|
+
hash = Math.imul(hash, 16777619);
|
|
536
|
+
}
|
|
537
|
+
return (hash >>> 0).toString(36);
|
|
538
|
+
}
|
|
539
|
+
function isShareChannel(value) {
|
|
540
|
+
return (value === 'wechatSession' ||
|
|
541
|
+
value === 'wechatTimeline' ||
|
|
542
|
+
value === 'qqFriend' ||
|
|
543
|
+
value === 'qzone' ||
|
|
544
|
+
value === 'weibo');
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const MINI_PROGRAM_MOCK_RUNTIME_METHODS = MINI_PROGRAM_PROTOCOL_CAPABILITIES.map(capability => capability.method);
|
|
548
|
+
const MINI_PROGRAM_MOCK_RUNTIME_METHOD_HANDLERS = {
|
|
549
|
+
[AUTH_LOGIN_METHOD]: runtime => runtime.login(),
|
|
550
|
+
[USER_GET_INFO_METHOD]: runtime => runtime.getUserInfo(),
|
|
551
|
+
[SHARE_SHOW_SHARE_MENU_METHOD]: (runtime, payload) => runtime.showShareMenu(payload),
|
|
552
|
+
[SHARE_SCREENSHOT_METHOD]: (runtime, payload) => runtime.shareScreenshot(payload),
|
|
553
|
+
[VIEWPORT_GET_WINDOW_INFO_METHOD]: runtime => runtime.getWindowInfo(),
|
|
554
|
+
[STORAGE_GET_STORAGE_METHOD]: (runtime, payload) => runtime.getStorage(payload),
|
|
555
|
+
[STORAGE_SET_STORAGE_METHOD]: (runtime, payload) => runtime.setStorage(payload),
|
|
556
|
+
[NETWORK_REQUEST_METHOD]: (runtime, payload) => runtime.requestNetwork(payload),
|
|
557
|
+
};
|
|
558
|
+
class MiniProgramMockRuntime {
|
|
559
|
+
adapter;
|
|
560
|
+
options;
|
|
561
|
+
constructor(adapter, options = {}) {
|
|
562
|
+
this.adapter = adapter;
|
|
563
|
+
this.options = options;
|
|
564
|
+
}
|
|
565
|
+
async runMethod(method, payload) {
|
|
566
|
+
if (!isMiniProgramMockRuntimeMethod(method)) {
|
|
567
|
+
throw createMockBridgeError('METHOD_NOT_FOUND', `未开放小程序能力: ${method}`);
|
|
568
|
+
}
|
|
569
|
+
const handler = MINI_PROGRAM_MOCK_RUNTIME_METHOD_HANDLERS[method];
|
|
570
|
+
return handler(this, payload);
|
|
571
|
+
}
|
|
572
|
+
async login() {
|
|
573
|
+
const result = await loginAndGetMiniProgramRuntimeUserInfo(this.adapter);
|
|
574
|
+
this.options.onAuthChange?.(result);
|
|
575
|
+
return result;
|
|
576
|
+
}
|
|
577
|
+
async getUserInfo() {
|
|
578
|
+
return getMiniProgramRuntimeUserInfo(this.adapter);
|
|
579
|
+
}
|
|
580
|
+
showShareMenu(payload) {
|
|
581
|
+
return showMiniProgramRuntimeShareMenu(payload, this.adapter);
|
|
582
|
+
}
|
|
583
|
+
shareScreenshot(payload) {
|
|
584
|
+
return shareMiniProgramRuntimeScreenshot(payload, this.adapter);
|
|
585
|
+
}
|
|
586
|
+
getWindowInfo() {
|
|
587
|
+
return getMiniProgramRuntimeWindowInfo(this.adapter);
|
|
588
|
+
}
|
|
589
|
+
getStorage(payload) {
|
|
590
|
+
return getMiniProgramRuntimeStorage(payload, this.options.storage || {}, this.adapter);
|
|
591
|
+
}
|
|
592
|
+
async setStorage(payload) {
|
|
593
|
+
await setMiniProgramRuntimeStorage(payload, this.options.storage || {}, this.adapter);
|
|
594
|
+
}
|
|
595
|
+
requestNetwork(payload) {
|
|
596
|
+
return requestMiniProgramRuntimeNetwork(payload, this.adapter);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
function isMiniProgramMockRuntimeMethod(method) {
|
|
600
|
+
return MINI_PROGRAM_MOCK_RUNTIME_METHODS.includes(method);
|
|
601
|
+
}
|
|
602
|
+
function createMockBridgeError(code, message, data) {
|
|
603
|
+
return data === undefined
|
|
604
|
+
? {
|
|
605
|
+
code,
|
|
606
|
+
message,
|
|
607
|
+
}
|
|
608
|
+
: {
|
|
609
|
+
code,
|
|
610
|
+
message,
|
|
611
|
+
data,
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
function toMockBridgeError(error) {
|
|
615
|
+
if (isObjectLike(error) && typeof error.code === 'string' && typeof error.message === 'string') {
|
|
616
|
+
return {
|
|
617
|
+
code: error.code,
|
|
618
|
+
message: error.message,
|
|
619
|
+
...(Object.prototype.hasOwnProperty.call(error, 'data') ? { data: error.data } : {}),
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
return createMockBridgeError('MOCK_RUNTIME_ERROR', error instanceof Error ? error.message : String(error));
|
|
623
|
+
}
|
|
624
|
+
function isObjectLike(value) {
|
|
625
|
+
return (typeof value === 'object' || typeof value === 'function') && value !== null;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const NETWORK_PROXY_PATH = '/__hb_sdk_mock_network__';
|
|
629
|
+
const params = new URL(location.href).searchParams;
|
|
630
|
+
const miniUrl = params.get('mini_url');
|
|
631
|
+
const nonce = createNonce();
|
|
632
|
+
const storage = new Map();
|
|
633
|
+
const logs = [];
|
|
634
|
+
let handshaken = false;
|
|
635
|
+
let currentUser = {
|
|
636
|
+
heybox_id: 'debug_user',
|
|
637
|
+
nickname: 'PC Debug User',
|
|
638
|
+
avatar: '',
|
|
639
|
+
};
|
|
640
|
+
const elements = {
|
|
641
|
+
bridgeStatus: queryElement('#bridge-status'),
|
|
642
|
+
device: queryElement('#device'),
|
|
643
|
+
logs: queryElement('#logs'),
|
|
644
|
+
macAppButton: queryElement('#mac-app-button'),
|
|
645
|
+
macAppStatus: queryElement('#mac-app-status'),
|
|
646
|
+
miniUrl: queryElement('#mini-url'),
|
|
647
|
+
nicknameInput: queryElement('#nickname-input'),
|
|
648
|
+
storageSnapshot: queryElement('#storage-snapshot'),
|
|
649
|
+
userStatus: queryElement('#user-status'),
|
|
650
|
+
};
|
|
651
|
+
if (!miniUrl) {
|
|
652
|
+
elements.device.innerHTML = '<div class="error">Missing mini_url query.</div>';
|
|
653
|
+
throw new Error('Missing mini_url query.');
|
|
654
|
+
}
|
|
655
|
+
const miniProgramUrl = miniUrl;
|
|
656
|
+
const iframe = document.createElement('iframe');
|
|
657
|
+
iframe.src = appendNonce(miniProgramUrl, nonce);
|
|
658
|
+
iframe.allow = 'clipboard-read; clipboard-write';
|
|
659
|
+
elements.device.appendChild(iframe);
|
|
660
|
+
elements.miniUrl.textContent = miniProgramUrl;
|
|
661
|
+
const runtime = new MiniProgramMockRuntime(createBrowserMockRuntimeAdapter(), {
|
|
662
|
+
onAuthChange: result => postEvent('authChange', result),
|
|
663
|
+
});
|
|
664
|
+
updateUserStatus();
|
|
665
|
+
updateStorageSnapshot();
|
|
666
|
+
window.addEventListener('message', handleMessage);
|
|
667
|
+
window.addEventListener('beforeunload', () => {
|
|
668
|
+
postEvent('unload', { timestamp: Date.now() });
|
|
669
|
+
});
|
|
670
|
+
queryElement('#login-button').addEventListener('click', () => {
|
|
671
|
+
void runtime.login();
|
|
672
|
+
});
|
|
673
|
+
queryElement('#logout-button').addEventListener('click', () => {
|
|
674
|
+
currentUser = null;
|
|
675
|
+
updateUserStatus();
|
|
676
|
+
postEvent('authChange', createUserInfoResult());
|
|
677
|
+
});
|
|
678
|
+
queryElement('#show-button').addEventListener('click', () => {
|
|
679
|
+
postEvent('show', { timestamp: Date.now(), source: 'mock-host' });
|
|
680
|
+
});
|
|
681
|
+
queryElement('#hide-button').addEventListener('click', () => {
|
|
682
|
+
postEvent('hide', { timestamp: Date.now(), source: 'mock-host' });
|
|
683
|
+
});
|
|
684
|
+
elements.macAppButton.addEventListener('click', () => {
|
|
685
|
+
elements.macAppStatus.textContent = '已尝试唤起 Mac 版 APP';
|
|
686
|
+
window.location.href = createMacAppProtocol(miniProgramUrl);
|
|
687
|
+
});
|
|
688
|
+
function createBrowserMockRuntimeAdapter() {
|
|
689
|
+
return {
|
|
690
|
+
app: {
|
|
691
|
+
getCurrentHref() {
|
|
692
|
+
return location.href;
|
|
693
|
+
},
|
|
694
|
+
},
|
|
695
|
+
launch: {
|
|
696
|
+
async getMiniProgramInfo() {
|
|
697
|
+
return {
|
|
698
|
+
pageUrl: miniProgramUrl,
|
|
699
|
+
};
|
|
700
|
+
},
|
|
701
|
+
},
|
|
702
|
+
auth: {
|
|
703
|
+
async getCurrentUserId() {
|
|
704
|
+
return currentUser?.heybox_id;
|
|
705
|
+
},
|
|
706
|
+
async login() {
|
|
707
|
+
currentUser = createUserFromForm();
|
|
708
|
+
updateUserStatus();
|
|
709
|
+
},
|
|
710
|
+
},
|
|
711
|
+
user: {
|
|
712
|
+
async getUserBasicInfo(userId) {
|
|
713
|
+
if (!currentUser || currentUser.heybox_id !== userId) {
|
|
714
|
+
return undefined;
|
|
715
|
+
}
|
|
716
|
+
return {
|
|
717
|
+
nickname: currentUser.nickname,
|
|
718
|
+
avatar: currentUser.avatar,
|
|
719
|
+
};
|
|
720
|
+
},
|
|
721
|
+
},
|
|
722
|
+
share: {
|
|
723
|
+
async showShareMenu(options) {
|
|
724
|
+
return {
|
|
725
|
+
ok: true,
|
|
726
|
+
platform: 'hb-sdk-mock-host',
|
|
727
|
+
type: 'showShareMenu',
|
|
728
|
+
params: options,
|
|
729
|
+
};
|
|
730
|
+
},
|
|
731
|
+
async shareScreenshot(options) {
|
|
732
|
+
return {
|
|
733
|
+
ok: true,
|
|
734
|
+
platform: 'hb-sdk-mock-host',
|
|
735
|
+
type: 'screenshot',
|
|
736
|
+
params: options,
|
|
737
|
+
};
|
|
738
|
+
},
|
|
739
|
+
},
|
|
740
|
+
viewport: {
|
|
741
|
+
setDocumentTitle(title) {
|
|
742
|
+
document.title = title;
|
|
743
|
+
},
|
|
744
|
+
getViewportMetrics() {
|
|
745
|
+
const rect = iframe.getBoundingClientRect();
|
|
746
|
+
const width = Math.max(1, Math.round(rect.width));
|
|
747
|
+
const height = Math.max(1, Math.round(rect.height));
|
|
748
|
+
return {
|
|
749
|
+
innerWidth: width,
|
|
750
|
+
innerHeight: height,
|
|
751
|
+
documentElementClientWidth: width,
|
|
752
|
+
documentElementClientHeight: height,
|
|
753
|
+
screenWidth: window.screen.width || width,
|
|
754
|
+
screenHeight: window.screen.height || height,
|
|
755
|
+
pixelRatio: window.devicePixelRatio || 1,
|
|
756
|
+
};
|
|
757
|
+
},
|
|
758
|
+
getNavigationBarHeight() {
|
|
759
|
+
return 0;
|
|
760
|
+
},
|
|
761
|
+
},
|
|
762
|
+
storage: {
|
|
763
|
+
getStorage({ key }) {
|
|
764
|
+
return storage.get(key);
|
|
765
|
+
},
|
|
766
|
+
async setStorage({ key, value }) {
|
|
767
|
+
storage.set(key, value);
|
|
768
|
+
updateStorageSnapshot();
|
|
769
|
+
},
|
|
770
|
+
},
|
|
771
|
+
network: {
|
|
772
|
+
request: requestNetwork,
|
|
773
|
+
},
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
function handleMessage(event) {
|
|
777
|
+
if (event.source !== iframe.contentWindow || !isBridgeMessage(event.data)) {
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
const message = event.data;
|
|
781
|
+
if (message.type === 'handshake') {
|
|
782
|
+
handleHandshake();
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
if (message.type === 'request') {
|
|
786
|
+
void handleRequest(message);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
function handleHandshake() {
|
|
790
|
+
const firstHandshake = !handshaken;
|
|
791
|
+
handshaken = true;
|
|
792
|
+
elements.bridgeStatus.textContent = 'ready';
|
|
793
|
+
if (firstHandshake) {
|
|
794
|
+
postEvent('launch', {
|
|
795
|
+
timestamp: Date.now(),
|
|
796
|
+
url: iframe.src,
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
postEvent('ready', {
|
|
800
|
+
timestamp: Date.now(),
|
|
801
|
+
});
|
|
802
|
+
pushLog('sdk.handshake', { firstHandshake });
|
|
803
|
+
}
|
|
804
|
+
async function handleRequest(message) {
|
|
805
|
+
if (!message.id || !message.method) {
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
try {
|
|
809
|
+
const payload = await runtime.runMethod(message.method, message.payload);
|
|
810
|
+
postResponse(message, payload);
|
|
811
|
+
pushLog(message.method, { payload: message.payload, result: payload });
|
|
812
|
+
}
|
|
813
|
+
catch (error) {
|
|
814
|
+
const bridgeError = toMockBridgeError(error);
|
|
815
|
+
postResponse(message, undefined, bridgeError);
|
|
816
|
+
postEvent('error', bridgeError);
|
|
817
|
+
pushLog(message.method, { payload: message.payload, error: bridgeError });
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
async function requestNetwork(config) {
|
|
821
|
+
const response = await fetch(NETWORK_PROXY_PATH, {
|
|
822
|
+
method: 'POST',
|
|
823
|
+
headers: {
|
|
824
|
+
'content-type': 'application/json',
|
|
825
|
+
},
|
|
826
|
+
body: JSON.stringify(createNetworkProxyPayload(config)),
|
|
827
|
+
});
|
|
828
|
+
const payload = await readNetworkProxyResponse(response);
|
|
829
|
+
if (!response.ok) {
|
|
830
|
+
throw payload;
|
|
831
|
+
}
|
|
832
|
+
return payload;
|
|
833
|
+
}
|
|
834
|
+
async function readNetworkProxyResponse(response) {
|
|
835
|
+
const text = await response.text();
|
|
836
|
+
if (!text.trim()) {
|
|
837
|
+
return undefined;
|
|
838
|
+
}
|
|
839
|
+
try {
|
|
840
|
+
return JSON.parse(text);
|
|
841
|
+
}
|
|
842
|
+
catch {
|
|
843
|
+
throw {
|
|
844
|
+
code: response.status === 404 ? 'MOCK_NETWORK_PROXY_NOT_FOUND' : 'MOCK_NETWORK_PROXY_ERROR',
|
|
845
|
+
message: response.status === 404
|
|
846
|
+
? 'mock network proxy 不可用,请重启 hb-sdk dev'
|
|
847
|
+
: text,
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
function createNetworkProxyPayload(config) {
|
|
852
|
+
const url = new URL(config.url, iframe.src);
|
|
853
|
+
if (config.params && typeof config.params === 'object') {
|
|
854
|
+
Object.entries(config.params).forEach(([key, value]) => {
|
|
855
|
+
if (value !== undefined && value !== null) {
|
|
856
|
+
url.searchParams.set(key, String(value));
|
|
857
|
+
}
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
return {
|
|
861
|
+
url: url.toString(),
|
|
862
|
+
method: config.method,
|
|
863
|
+
data: config.data,
|
|
864
|
+
headers: config.headers,
|
|
865
|
+
timeout: config.timeout,
|
|
866
|
+
withCredentials: config.withCredentials,
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
function postEvent(method, payload) {
|
|
870
|
+
postMessageToMiniProgram({
|
|
871
|
+
namespace: MINI_PROGRAM_MESSAGE_NAMESPACE,
|
|
872
|
+
version: MINI_PROGRAM_MESSAGE_VERSION,
|
|
873
|
+
nonce,
|
|
874
|
+
type: 'event',
|
|
875
|
+
method,
|
|
876
|
+
payload,
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
function postResponse(request, payload, error) {
|
|
880
|
+
postMessageToMiniProgram({
|
|
881
|
+
namespace: MINI_PROGRAM_MESSAGE_NAMESPACE,
|
|
882
|
+
version: MINI_PROGRAM_MESSAGE_VERSION,
|
|
883
|
+
nonce,
|
|
884
|
+
type: 'response',
|
|
885
|
+
id: request.id,
|
|
886
|
+
method: request.method,
|
|
887
|
+
payload,
|
|
888
|
+
error,
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
function postMessageToMiniProgram(message) {
|
|
892
|
+
iframe.contentWindow?.postMessage(message, readTargetOrigin());
|
|
893
|
+
}
|
|
894
|
+
function isBridgeMessage(message) {
|
|
895
|
+
return Boolean(message &&
|
|
896
|
+
typeof message === 'object' &&
|
|
897
|
+
'namespace' in message &&
|
|
898
|
+
'version' in message &&
|
|
899
|
+
'nonce' in message &&
|
|
900
|
+
message.namespace === MINI_PROGRAM_MESSAGE_NAMESPACE &&
|
|
901
|
+
message.version === MINI_PROGRAM_MESSAGE_VERSION &&
|
|
902
|
+
message.nonce === nonce);
|
|
903
|
+
}
|
|
904
|
+
function createUserInfoResult() {
|
|
905
|
+
return {
|
|
906
|
+
isLogin: Boolean(currentUser),
|
|
907
|
+
userInfo: currentUser ? { ...currentUser } : null,
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
function createUserFromForm() {
|
|
911
|
+
return {
|
|
912
|
+
heybox_id: 'debug_user',
|
|
913
|
+
nickname: elements.nicknameInput.value.trim() || 'PC Debug User',
|
|
914
|
+
avatar: '',
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
function updateUserStatus() {
|
|
918
|
+
elements.userStatus.textContent = currentUser ? currentUser.nickname : '未登录';
|
|
919
|
+
}
|
|
920
|
+
function updateStorageSnapshot() {
|
|
921
|
+
elements.storageSnapshot.textContent = JSON.stringify(Object.fromEntries(Array.from(storage.entries()).map(([key, value]) => [key, safeJsonParse(value)])), null, 2);
|
|
922
|
+
}
|
|
923
|
+
function pushLog(method, detail) {
|
|
924
|
+
logs.unshift({
|
|
925
|
+
method,
|
|
926
|
+
detail,
|
|
927
|
+
timestamp: new Date().toLocaleTimeString(),
|
|
928
|
+
});
|
|
929
|
+
logs.splice(30);
|
|
930
|
+
elements.logs.innerHTML = logs
|
|
931
|
+
.map(log => `<article class="log"><strong>${escapeHtml(log.method)} · ${escapeHtml(log.timestamp)}</strong><pre>${escapeHtml(JSON.stringify(log.detail, null, 2))}</pre></article>`)
|
|
932
|
+
.join('');
|
|
933
|
+
}
|
|
934
|
+
function appendNonce(input, value) {
|
|
935
|
+
const url = new URL(input, location.href);
|
|
936
|
+
url.searchParams.set(MINI_PROGRAM_BRIDGE_NONCE_PARAM, value);
|
|
937
|
+
return url.toString();
|
|
938
|
+
}
|
|
939
|
+
function readTargetOrigin() {
|
|
940
|
+
try {
|
|
941
|
+
return new URL(iframe.src).origin;
|
|
942
|
+
}
|
|
943
|
+
catch {
|
|
944
|
+
return '*';
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
function createNonce() {
|
|
948
|
+
if (globalThis.crypto?.randomUUID) {
|
|
949
|
+
return globalThis.crypto.randomUUID();
|
|
950
|
+
}
|
|
951
|
+
return `mock_${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
|
952
|
+
}
|
|
953
|
+
function queryElement(selector) {
|
|
954
|
+
const element = document.querySelector(selector);
|
|
955
|
+
if (!element) {
|
|
956
|
+
throw new Error(`Missing mock host element: ${selector}`);
|
|
957
|
+
}
|
|
958
|
+
return element;
|
|
959
|
+
}
|
|
960
|
+
function safeJsonParse(value) {
|
|
961
|
+
try {
|
|
962
|
+
return JSON.parse(value);
|
|
963
|
+
}
|
|
964
|
+
catch {
|
|
965
|
+
return value;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
function escapeHtml(value) {
|
|
969
|
+
return String(value)
|
|
970
|
+
.replace(/&/g, '&')
|
|
971
|
+
.replace(/</g, '<')
|
|
972
|
+
.replace(/>/g, '>')
|
|
973
|
+
.replace(/"/g, '"')
|
|
974
|
+
.replace(/'/g, ''');
|
|
975
|
+
}
|