@heybox/hb-sdk 0.4.3 → 0.4.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.
Files changed (37) hide show
  1. package/README.md +106 -18
  2. package/dist/cli-chunks/{create-BV4h2BTs.cjs → create-DpyZCNdo.cjs} +1 -1
  3. package/dist/cli-chunks/{dev-DgX88vaK.cjs → dev-DWIpgJnn.cjs} +1 -1
  4. package/dist/cli-chunks/{doctor-BIZoQ6go.cjs → doctor-DBotVUQI.cjs} +1 -1
  5. package/dist/cli-chunks/{index-ovy_xoLn.cjs → index-BYMTp2I6.cjs} +171 -43
  6. package/dist/cli-chunks/{index-DQAFCtK1.cjs → index-DRsyeAcg.cjs} +3 -3
  7. package/dist/cli-chunks/{login-CoZhlwxt.cjs → login-DIgcT1gv.cjs} +2 -2
  8. package/dist/cli-chunks/{deploy-Bz0-WpHy.cjs → remote-DjaOc1VS.cjs} +797 -21
  9. package/dist/cli-chunks/{session-BQs0wf65.cjs → session-BAgaqpNL.cjs} +1 -1
  10. package/dist/cli.cjs +1 -1
  11. package/dist/devtools/mock-host/main.js +842 -428
  12. package/dist/index.cjs.js +98 -0
  13. package/dist/index.esm.js +96 -1
  14. package/dist/miniapp-publish.cjs.js +26 -0
  15. package/dist/miniapp-publish.esm.js +14 -1
  16. package/dist/protocol.cjs.js +70 -0
  17. package/dist/protocol.esm.js +64 -1
  18. package/dist/templates/vue3-vite-ts/README.md.ejs +1 -1
  19. package/dist/templates/vue3-vite-ts/package.json.ejs +1 -1
  20. package/package.json +1 -1
  21. package/skill/SKILL.md +23 -18
  22. package/skill/references/api-protocol.md +34 -2
  23. package/skill/references/api-root.md +79 -4
  24. package/skill/references/cli.md +86 -349
  25. package/skill/references/llms-index.md +2 -2
  26. package/skill/references/safety-boundaries.md +1 -1
  27. package/skill/scripts/sync-references.mjs +48 -4
  28. package/skill/skill.json +4 -4
  29. package/types/core/sdk.d.ts +9 -0
  30. package/types/core/singleton.d.ts +9 -0
  31. package/types/index.d.ts +7 -1
  32. package/types/miniapp-publish/index.d.ts +13 -0
  33. package/types/modules/device/index.d.ts +34 -0
  34. package/types/modules/navigation/index.d.ts +24 -0
  35. package/types/modules/ui/index.d.ts +42 -0
  36. package/types/protocol/capabilities.d.ts +81 -2
  37. package/types/protocol.d.ts +5 -2
@@ -68,6 +68,20 @@ const STORAGE_SET_STORAGE_METHOD = 'storage.setStorage';
68
68
  * 供 SDK 与父容器 runtime 共享同一 bridge method 标识。
69
69
  */
70
70
  const NETWORK_REQUEST_METHOD = 'network.request';
71
+ /** 展示 toast 能力方法名。 */
72
+ const UI_SHOW_TOAST_METHOD = 'ui.showToast';
73
+ /** 展示全局 loading 能力方法名。 */
74
+ const UI_SHOW_LOADING_METHOD = 'ui.showLoading';
75
+ /** 隐藏全局 loading 能力方法名。 */
76
+ const UI_HIDE_LOADING_METHOD = 'ui.hideLoading';
77
+ /** 震动反馈能力方法名。 */
78
+ const DEVICE_VIBRATE_METHOD = 'device.vibrate';
79
+ /** 写入文本剪贴板能力方法名。 */
80
+ const DEVICE_SET_CLIPBOARD_METHOD = 'device.setClipboard';
81
+ /** 关闭当前小程序容器能力方法名。 */
82
+ const NAVIGATION_CLOSE_METHOD = 'navigation.close';
83
+ /** 重载当前小程序容器能力方法名。 */
84
+ const NAVIGATION_RELOAD_METHOD = 'navigation.reload';
71
85
  /**
72
86
  * 小程序开放能力目录。
73
87
  *
@@ -138,6 +152,55 @@ const MINI_PROGRAM_PROTOCOL_CAPABILITIES = [
138
152
  permission: 'network.request',
139
153
  risk: 'high',
140
154
  },
155
+ {
156
+ method: UI_SHOW_TOAST_METHOD,
157
+ module: 'ui',
158
+ capability: UI_SHOW_TOAST_METHOD,
159
+ permission: 'ui.toast',
160
+ risk: 'low',
161
+ },
162
+ {
163
+ method: UI_SHOW_LOADING_METHOD,
164
+ module: 'ui',
165
+ capability: UI_SHOW_LOADING_METHOD,
166
+ permission: 'ui.loading',
167
+ risk: 'low',
168
+ },
169
+ {
170
+ method: UI_HIDE_LOADING_METHOD,
171
+ module: 'ui',
172
+ capability: UI_HIDE_LOADING_METHOD,
173
+ permission: 'ui.loading',
174
+ risk: 'low',
175
+ },
176
+ {
177
+ method: DEVICE_VIBRATE_METHOD,
178
+ module: 'device',
179
+ capability: DEVICE_VIBRATE_METHOD,
180
+ permission: 'device.vibrate',
181
+ risk: 'low',
182
+ },
183
+ {
184
+ method: DEVICE_SET_CLIPBOARD_METHOD,
185
+ module: 'device',
186
+ capability: DEVICE_SET_CLIPBOARD_METHOD,
187
+ permission: 'device.clipboard.write',
188
+ risk: 'medium',
189
+ },
190
+ {
191
+ method: NAVIGATION_CLOSE_METHOD,
192
+ module: 'navigation',
193
+ capability: NAVIGATION_CLOSE_METHOD,
194
+ permission: 'navigation.close',
195
+ risk: 'medium',
196
+ },
197
+ {
198
+ method: NAVIGATION_RELOAD_METHOD,
199
+ module: 'navigation',
200
+ capability: NAVIGATION_RELOAD_METHOD,
201
+ permission: 'navigation.reload',
202
+ risk: 'medium',
203
+ },
141
204
  ];
142
205
 
143
206
  var browser = {};
@@ -3240,65 +3303,6 @@ async function loginAndGetMiniProgramRuntimeUserInfo(platformAdapter) {
3240
3303
  return getMiniProgramRuntimeUserInfo(platformAdapter);
3241
3304
  }
3242
3305
 
3243
- function getMiniProgramRuntimeWindowInfo(platformAdapter) {
3244
- const metrics = platformAdapter.viewport.getViewportMetrics();
3245
- const windowWidth = getViewportWidth(metrics);
3246
- const windowHeight = getViewportHeight(metrics);
3247
- const screenWidth = getPositiveNumber(metrics.screenWidth, windowWidth);
3248
- const screenHeight = getPositiveNumber(metrics.screenHeight, windowHeight);
3249
- const navigationBarHeight = getNavigationBarHeight(platformAdapter);
3250
- const safeAreaHeight = Math.max(windowHeight - navigationBarHeight, 0);
3251
- return {
3252
- pixelRatio: getPositiveNumber(metrics.pixelRatio, 1),
3253
- screenWidth,
3254
- screenHeight,
3255
- windowWidth,
3256
- windowHeight,
3257
- statusBarHeight: navigationBarHeight,
3258
- safeArea: {
3259
- left: 0,
3260
- right: windowWidth,
3261
- top: navigationBarHeight,
3262
- bottom: windowHeight,
3263
- width: windowWidth,
3264
- height: safeAreaHeight,
3265
- },
3266
- screenTop: navigationBarHeight,
3267
- };
3268
- }
3269
- function getMiniProgramRuntimeViewportRect(platformAdapter) {
3270
- const metrics = platformAdapter.viewport.getViewportMetrics();
3271
- const width = getPositiveNumber(metrics.innerWidth, getPositiveNumber(metrics.documentElementClientWidth, 0));
3272
- const height = getPositiveNumber(metrics.innerHeight, getPositiveNumber(metrics.documentElementClientHeight, 0));
3273
- return {
3274
- left: 0,
3275
- top: 0,
3276
- width,
3277
- height,
3278
- };
3279
- }
3280
- function getNavigationBarHeight(platformAdapter) {
3281
- try {
3282
- return platformAdapter.viewport.getNavigationBarHeight();
3283
- }
3284
- catch {
3285
- return 0;
3286
- }
3287
- }
3288
- function getViewportWidth(metrics) {
3289
- return getPositiveNumber(metrics.innerWidth, getPositiveNumber(metrics.documentElementClientWidth, 0));
3290
- }
3291
- function getViewportHeight(metrics) {
3292
- return getPositiveNumber(metrics.innerHeight, getPositiveNumber(metrics.documentElementClientHeight, 0));
3293
- }
3294
- function getPositiveNumber(value, fallback) {
3295
- const numberValue = Number(value);
3296
- if (!Number.isFinite(numberValue) || numberValue <= 0) {
3297
- return fallback;
3298
- }
3299
- return numberValue;
3300
- }
3301
-
3302
3306
  const DEFAULT_STORAGE_KEY_MAX_LENGTH = 128;
3303
3307
  const STORAGE_KEY_CHAR_RE = /^[A-Za-z0-9_-]+$/;
3304
3308
  function createMiniProgramRuntimeBridgeError(code, message, data) {
@@ -3384,86 +3388,292 @@ function stringifyMiniProgramRuntimeJsonData(data, options) {
3384
3388
  return value;
3385
3389
  }
3386
3390
 
3387
- /** 设置导航栏关闭按钮与状态栏图标/文字的前景样式。 */
3388
- function setMiniProgramRuntimeNavigationBarStyle(payload, platformAdapter) {
3389
- return platformAdapter.viewport.setNavigationBarStyle(readMiniProgramNavigationBarStyleOptions(payload));
3391
+ const NETWORK_REQUEST_ALLOWED_KEYS = new Set([
3392
+ 'url',
3393
+ 'method',
3394
+ 'params',
3395
+ 'data',
3396
+ 'headers',
3397
+ 'timeout',
3398
+ 'withCredentials',
3399
+ ]);
3400
+ const NETWORK_REQUEST_MAX_TIMEOUT = 30000;
3401
+ const NETWORK_REQUEST_MAX_BODY_BYTES = 2 * 1024 * 1024;
3402
+ const NETWORK_REQUEST_DENIED_HEADERS = new Set([
3403
+ 'authorization',
3404
+ 'connection',
3405
+ 'content-length',
3406
+ 'cookie',
3407
+ 'host',
3408
+ 'origin',
3409
+ 'proxy-authorization',
3410
+ 'referer',
3411
+ 'set-cookie',
3412
+ 'transfer-encoding',
3413
+ 'user-agent',
3414
+ ]);
3415
+ const NETWORK_HTTP_METHODS = new Set([
3416
+ 'GET',
3417
+ 'POST',
3418
+ 'PUT',
3419
+ 'PATCH',
3420
+ 'DELETE',
3421
+ 'HEAD',
3422
+ 'OPTIONS',
3423
+ ]);
3424
+ /** network.request:校验桥接参数并委托平台适配层完成真实 HTTP 交换。 */
3425
+ async function requestMiniProgramRuntimeNetwork(payload, platformAdapter) {
3426
+ return platformAdapter.network.request(readMiniProgramRuntimeNetworkRequest(payload));
3390
3427
  }
3391
- function readMiniProgramNavigationBarStyleOptions(payload) {
3392
- assertMiniProgramRuntimeRecord(payload, 'viewport.setNavigationBarStyle 参数必须是对象');
3393
- const foregroundStyle = payload.foregroundStyle;
3394
- if (!isNavigationBarForegroundStyle(foregroundStyle)) {
3395
- throw createMiniProgramRuntimeBridgeError('INVALID_PARAMS', 'viewport.setNavigationBarStyle foregroundStyle 必须是 light 或 dark');
3396
- }
3397
- return {
3398
- foregroundStyle,
3428
+ function readMiniProgramRuntimeNetworkRequest(payload) {
3429
+ assertMiniProgramRuntimeRecord(payload, 'network.request 参数必须是对象');
3430
+ assertNetworkAllowedKeys(payload);
3431
+ const request = {
3432
+ url: readNetworkRequestUrl(payload.url),
3399
3433
  };
3434
+ if (payload.method !== undefined) {
3435
+ request.method = readNetworkMethod(payload.method);
3436
+ }
3437
+ if (payload.params !== undefined) {
3438
+ request.params = readUnknownRecord(payload.params, 'network.request params 必须是对象');
3439
+ }
3440
+ if (payload.data !== undefined) {
3441
+ assertNetworkRequestBody(payload.data);
3442
+ request.data = payload.data;
3443
+ }
3444
+ if (payload.headers !== undefined) {
3445
+ request.headers = readNetworkRequestHeaders(payload.headers);
3446
+ }
3447
+ if (payload.timeout !== undefined) {
3448
+ request.timeout = readMiniProgramRuntimeNonNegativeNumber(payload.timeout, 'network.request timeout 必须是非负数字');
3449
+ if (request.timeout > NETWORK_REQUEST_MAX_TIMEOUT) {
3450
+ throw createMiniProgramRuntimeBridgeError('INVALID_PARAMS', `network.request timeout 不能超过 ${NETWORK_REQUEST_MAX_TIMEOUT}ms`);
3451
+ }
3452
+ }
3453
+ if (payload.withCredentials !== undefined) {
3454
+ request.withCredentials = readMiniProgramRuntimeBoolean(payload.withCredentials, 'network.request withCredentials 必须是布尔值');
3455
+ }
3456
+ return request;
3400
3457
  }
3401
- function isNavigationBarForegroundStyle(value) {
3402
- return value === 'light' || value === 'dark';
3403
- }
3404
-
3405
- /** 展示基础分享面板。 */
3406
- function showMiniProgramRuntimeShareMenu(payload, platformAdapter) {
3407
- return platformAdapter.share.showShareMenu(readMiniProgramShareMenuOptions(payload));
3408
- }
3409
- function readMiniProgramShareMenuOptions(payload) {
3410
- assertMiniProgramRuntimeRecord(payload, 'share.showShareMenu 参数必须是对象');
3411
- const title = readMiniProgramRuntimeRequiredString(payload.title, 'share.showShareMenu title 必须是非空字符串');
3412
- const desc = readMiniProgramRuntimeRequiredString(payload.desc, 'share.showShareMenu desc 必须是非空字符串');
3413
- const url = readMiniProgramRuntimeHttpUrl(payload.url, 'share.showShareMenu url 必须是 HTTP(S) URL');
3414
- const imageUrl = payload.imageUrl === undefined
3415
- ? undefined
3416
- : readMiniProgramRuntimeHttpUrl(payload.imageUrl, 'share.showShareMenu imageUrl 必须是 HTTP(S) URL');
3417
- const channel = payload.channel;
3418
- if (channel !== undefined && !isShareChannel(channel)) {
3419
- throw createMiniProgramRuntimeBridgeError('INVALID_PARAMS', 'share.showShareMenu channel 不合法');
3458
+ function assertNetworkAllowedKeys(payload) {
3459
+ const invalidKeys = Object.keys(payload).filter(key => !NETWORK_REQUEST_ALLOWED_KEYS.has(key));
3460
+ if (invalidKeys.length > 0) {
3461
+ throw createMiniProgramRuntimeBridgeError('INVALID_PARAMS', `network.request 不支持字段: ${invalidKeys.join(', ')}`);
3420
3462
  }
3421
- return {
3422
- title,
3423
- desc,
3424
- url,
3425
- imageUrl,
3426
- channel,
3427
- };
3428
3463
  }
3429
- function isShareChannel(value) {
3430
- return (value === 'wechatSession' ||
3431
- value === 'wechatTimeline' ||
3432
- value === 'qqFriend' ||
3433
- value === 'qzone' ||
3434
- value === 'weibo');
3464
+ function readNetworkRequestUrl(value) {
3465
+ const requestUrl = readMiniProgramRuntimeHttpUrl(value, 'network.request url 必须是 HTTP(S) URL');
3466
+ assertPublicNetworkRequestUrl(requestUrl);
3467
+ return requestUrl;
3435
3468
  }
3436
-
3437
- /** 截图并唤起分享。 */
3438
- function shareMiniProgramRuntimeScreenshot(payload, platformAdapter) {
3439
- const options = readMiniProgramScreenshotOptions(payload);
3440
- const rect = options.rect || getMiniProgramRuntimeViewportRect(platformAdapter);
3441
- return platformAdapter.share.shareScreenshot({
3442
- rect,
3443
- delay: options.delay,
3444
- saveToAlbum: options.saveToAlbum,
3445
- });
3469
+ function assertPublicNetworkRequestUrl(requestUrl) {
3470
+ const url = new URL(requestUrl);
3471
+ if (url.username || url.password) {
3472
+ throw createMiniProgramRuntimeBridgeError('INVALID_PARAMS', 'network.request url 不允许包含用户信息');
3473
+ }
3474
+ if (isUnsafeNetworkHost(url.hostname)) {
3475
+ throw createMiniProgramRuntimeBridgeError('INVALID_PARAMS', 'network.request url 不允许指向本机、内网或保留地址');
3476
+ }
3446
3477
  }
3447
- function readMiniProgramScreenshotOptions(payload) {
3448
- if (payload === undefined || payload === null) {
3449
- return {};
3478
+ function isUnsafeNetworkHost(hostname) {
3479
+ const normalizedHost = hostname.toLowerCase();
3480
+ const ipv6Host = normalizedHost.startsWith('[') && normalizedHost.endsWith(']')
3481
+ ? normalizedHost.slice(1, -1)
3482
+ : normalizedHost;
3483
+ if (normalizedHost === 'localhost' ||
3484
+ normalizedHost.endsWith('.localhost') ||
3485
+ normalizedHost.endsWith('.local') ||
3486
+ normalizedHost === 'metadata.google.internal') {
3487
+ return true;
3450
3488
  }
3451
- assertMiniProgramRuntimeRecord(payload, 'share.screenshot 参数必须是对象');
3452
- const options = {};
3453
- if (payload.rect !== undefined) {
3454
- options.rect = readScreenshotRect(payload.rect);
3489
+ if (isUnsafeIPv4Host(normalizedHost)) {
3490
+ return true;
3455
3491
  }
3456
- if (payload.delay !== undefined) {
3457
- options.delay = readMiniProgramRuntimeNonNegativeNumber(payload.delay, 'share.screenshot delay 必须是非负数字');
3492
+ return isUnsafeIPv6Host(ipv6Host);
3493
+ }
3494
+ function isUnsafeIPv4Host(hostname) {
3495
+ if (!/^\d{1,3}(\.\d{1,3}){3}$/.test(hostname)) {
3496
+ return false;
3458
3497
  }
3459
- if (payload.saveToAlbum !== undefined) {
3460
- options.saveToAlbum = readMiniProgramRuntimeBoolean(payload.saveToAlbum, 'share.screenshot saveToAlbum 必须是布尔值');
3498
+ const parts = hostname.split('.').map(part => Number(part));
3499
+ if (parts.some(part => !Number.isInteger(part) || part < 0 || part > 255)) {
3500
+ return true;
3461
3501
  }
3462
- return options;
3502
+ const [a, b] = parts;
3503
+ return (a === 0 ||
3504
+ a === 10 ||
3505
+ a === 127 ||
3506
+ (a === 100 && b >= 64 && b <= 127) ||
3507
+ (a === 169 && b === 254) ||
3508
+ (a === 172 && b >= 16 && b <= 31) ||
3509
+ (a === 192 && b === 168));
3463
3510
  }
3464
- function readScreenshotRect(value) {
3465
- assertMiniProgramRuntimeRecord(value, 'share.screenshot rect 必须是对象');
3466
- const left = readMiniProgramRuntimeNonNegativeNumber(value.left, 'share.screenshot rect.left 必须是非负数字');
3511
+ function isUnsafeIPv6Host(hostname) {
3512
+ return (hostname === '::' ||
3513
+ hostname === '::1' ||
3514
+ hostname.startsWith('fc') ||
3515
+ hostname.startsWith('fd') ||
3516
+ hostname.startsWith('fe80:'));
3517
+ }
3518
+ function readNetworkRequestHeaders(value) {
3519
+ const headers = readStringRecord(value, 'network.request headers 必须是 Record<string, string>');
3520
+ const deniedHeader = Object.keys(headers).find(isDeniedNetworkRequestHeader);
3521
+ if (deniedHeader) {
3522
+ throw createMiniProgramRuntimeBridgeError('INVALID_PARAMS', `network.request 不允许设置敏感请求头: ${deniedHeader}`);
3523
+ }
3524
+ return headers;
3525
+ }
3526
+ function isDeniedNetworkRequestHeader(headerName) {
3527
+ const normalizedHeaderName = headerName.trim().toLowerCase();
3528
+ return (NETWORK_REQUEST_DENIED_HEADERS.has(normalizedHeaderName) ||
3529
+ normalizedHeaderName.startsWith('x-heybox-') ||
3530
+ normalizedHeaderName.startsWith('x-xhh-'));
3531
+ }
3532
+ function assertNetworkRequestBody(data) {
3533
+ if (isUnsupportedNetworkRequestBody(data)) {
3534
+ throw createMiniProgramRuntimeBridgeError('INVALID_PARAMS', 'network.request data 不支持 File、Blob 或 FormData,请使用专用上传能力');
3535
+ }
3536
+ const bodyBytes = estimateNetworkRequestBodyBytes(data);
3537
+ if (bodyBytes > NETWORK_REQUEST_MAX_BODY_BYTES) {
3538
+ throw createMiniProgramRuntimeBridgeError('INVALID_PARAMS', `network.request data 不能超过 ${NETWORK_REQUEST_MAX_BODY_BYTES} 字节`);
3539
+ }
3540
+ }
3541
+ function isUnsupportedNetworkRequestBody(data) {
3542
+ return ((typeof Blob !== 'undefined' && data instanceof Blob) ||
3543
+ (typeof FormData !== 'undefined' && data instanceof FormData));
3544
+ }
3545
+ function estimateNetworkRequestBodyBytes(data) {
3546
+ if (typeof data === 'string') {
3547
+ return getMiniProgramRuntimeUtf8ByteLength(data);
3548
+ }
3549
+ if (data instanceof ArrayBuffer) {
3550
+ return data.byteLength;
3551
+ }
3552
+ if (ArrayBuffer.isView(data)) {
3553
+ return data.byteLength;
3554
+ }
3555
+ if (typeof URLSearchParams !== 'undefined' && data instanceof URLSearchParams) {
3556
+ return getMiniProgramRuntimeUtf8ByteLength(data.toString());
3557
+ }
3558
+ try {
3559
+ const serialized = JSON.stringify(data);
3560
+ return serialized ? getMiniProgramRuntimeUtf8ByteLength(serialized) : 0;
3561
+ }
3562
+ catch {
3563
+ throw createMiniProgramRuntimeBridgeError('INVALID_PARAMS', 'network.request data 必须可序列化');
3564
+ }
3565
+ }
3566
+ function readNetworkMethod(value) {
3567
+ const method = readMiniProgramRuntimeRequiredString(value, 'network.request method 必须是字符串').trim().toUpperCase();
3568
+ if (!NETWORK_HTTP_METHODS.has(method)) {
3569
+ throw createMiniProgramRuntimeBridgeError('INVALID_PARAMS', 'network.request method 不合法');
3570
+ }
3571
+ return method;
3572
+ }
3573
+ function readUnknownRecord(value, message) {
3574
+ assertMiniProgramRuntimeRecord(value, message);
3575
+ return { ...value };
3576
+ }
3577
+ function readStringRecord(value, message) {
3578
+ assertMiniProgramRuntimeRecord(value, message);
3579
+ return Object.entries(value).reduce((headers, [key, headerValue]) => {
3580
+ if (!key.trim() || typeof headerValue !== 'string') {
3581
+ throw createMiniProgramRuntimeBridgeError('INVALID_PARAMS', message);
3582
+ }
3583
+ headers[key] = headerValue;
3584
+ return headers;
3585
+ }, {});
3586
+ }
3587
+
3588
+ function getMiniProgramRuntimeWindowInfo(platformAdapter) {
3589
+ const metrics = platformAdapter.viewport.getViewportMetrics();
3590
+ const windowWidth = getViewportWidth(metrics);
3591
+ const windowHeight = getViewportHeight(metrics);
3592
+ const screenWidth = getPositiveNumber(metrics.screenWidth, windowWidth);
3593
+ const screenHeight = getPositiveNumber(metrics.screenHeight, windowHeight);
3594
+ const navigationBarHeight = getNavigationBarHeight(platformAdapter);
3595
+ const safeAreaHeight = Math.max(windowHeight - navigationBarHeight, 0);
3596
+ return {
3597
+ pixelRatio: getPositiveNumber(metrics.pixelRatio, 1),
3598
+ screenWidth,
3599
+ screenHeight,
3600
+ windowWidth,
3601
+ windowHeight,
3602
+ statusBarHeight: navigationBarHeight,
3603
+ safeArea: {
3604
+ left: 0,
3605
+ right: windowWidth,
3606
+ top: navigationBarHeight,
3607
+ bottom: windowHeight,
3608
+ width: windowWidth,
3609
+ height: safeAreaHeight,
3610
+ },
3611
+ screenTop: navigationBarHeight,
3612
+ };
3613
+ }
3614
+ function getMiniProgramRuntimeViewportRect(platformAdapter) {
3615
+ const metrics = platformAdapter.viewport.getViewportMetrics();
3616
+ const width = getPositiveNumber(metrics.innerWidth, getPositiveNumber(metrics.documentElementClientWidth, 0));
3617
+ const height = getPositiveNumber(metrics.innerHeight, getPositiveNumber(metrics.documentElementClientHeight, 0));
3618
+ return {
3619
+ left: 0,
3620
+ top: 0,
3621
+ width,
3622
+ height,
3623
+ };
3624
+ }
3625
+ function getNavigationBarHeight(platformAdapter) {
3626
+ try {
3627
+ return platformAdapter.viewport.getNavigationBarHeight();
3628
+ }
3629
+ catch {
3630
+ return 0;
3631
+ }
3632
+ }
3633
+ function getViewportWidth(metrics) {
3634
+ return getPositiveNumber(metrics.innerWidth, getPositiveNumber(metrics.documentElementClientWidth, 0));
3635
+ }
3636
+ function getViewportHeight(metrics) {
3637
+ return getPositiveNumber(metrics.innerHeight, getPositiveNumber(metrics.documentElementClientHeight, 0));
3638
+ }
3639
+ function getPositiveNumber(value, fallback) {
3640
+ const numberValue = Number(value);
3641
+ if (!Number.isFinite(numberValue) || numberValue <= 0) {
3642
+ return fallback;
3643
+ }
3644
+ return numberValue;
3645
+ }
3646
+
3647
+ /** 截图并唤起分享。 */
3648
+ function shareMiniProgramRuntimeScreenshot(payload, platformAdapter) {
3649
+ const options = readMiniProgramScreenshotOptions(payload);
3650
+ const rect = options.rect || getMiniProgramRuntimeViewportRect(platformAdapter);
3651
+ return platformAdapter.share.shareScreenshot({
3652
+ rect,
3653
+ delay: options.delay,
3654
+ saveToAlbum: options.saveToAlbum,
3655
+ });
3656
+ }
3657
+ function readMiniProgramScreenshotOptions(payload) {
3658
+ if (payload === undefined || payload === null) {
3659
+ return {};
3660
+ }
3661
+ assertMiniProgramRuntimeRecord(payload, 'share.screenshot 参数必须是对象');
3662
+ const options = {};
3663
+ if (payload.rect !== undefined) {
3664
+ options.rect = readScreenshotRect(payload.rect);
3665
+ }
3666
+ if (payload.delay !== undefined) {
3667
+ options.delay = readMiniProgramRuntimeNonNegativeNumber(payload.delay, 'share.screenshot delay 必须是非负数字');
3668
+ }
3669
+ if (payload.saveToAlbum !== undefined) {
3670
+ options.saveToAlbum = readMiniProgramRuntimeBoolean(payload.saveToAlbum, 'share.screenshot saveToAlbum 必须是布尔值');
3671
+ }
3672
+ return options;
3673
+ }
3674
+ function readScreenshotRect(value) {
3675
+ assertMiniProgramRuntimeRecord(value, 'share.screenshot rect 必须是对象');
3676
+ const left = readMiniProgramRuntimeNonNegativeNumber(value.left, 'share.screenshot rect.left 必须是非负数字');
3467
3677
  const top = readMiniProgramRuntimeNonNegativeNumber(value.top, 'share.screenshot rect.top 必须是非负数字');
3468
3678
  const width = readMiniProgramRuntimePositiveNumber(value.width, 'share.screenshot rect.width 必须是正数');
3469
3679
  const height = readMiniProgramRuntimePositiveNumber(value.height, 'share.screenshot rect.height 必须是正数');
@@ -3475,6 +3685,38 @@ function readScreenshotRect(value) {
3475
3685
  };
3476
3686
  }
3477
3687
 
3688
+ /** 展示基础分享面板。 */
3689
+ function showMiniProgramRuntimeShareMenu(payload, platformAdapter) {
3690
+ return platformAdapter.share.showShareMenu(readMiniProgramShareMenuOptions(payload));
3691
+ }
3692
+ function readMiniProgramShareMenuOptions(payload) {
3693
+ assertMiniProgramRuntimeRecord(payload, 'share.showShareMenu 参数必须是对象');
3694
+ const title = readMiniProgramRuntimeRequiredString(payload.title, 'share.showShareMenu title 必须是非空字符串');
3695
+ const desc = readMiniProgramRuntimeRequiredString(payload.desc, 'share.showShareMenu desc 必须是非空字符串');
3696
+ const url = readMiniProgramRuntimeHttpUrl(payload.url, 'share.showShareMenu url 必须是 HTTP(S) URL');
3697
+ const imageUrl = payload.imageUrl === undefined
3698
+ ? undefined
3699
+ : readMiniProgramRuntimeHttpUrl(payload.imageUrl, 'share.showShareMenu imageUrl 必须是 HTTP(S) URL');
3700
+ const channel = payload.channel;
3701
+ if (channel !== undefined && !isShareChannel(channel)) {
3702
+ throw createMiniProgramRuntimeBridgeError('INVALID_PARAMS', 'share.showShareMenu channel 不合法');
3703
+ }
3704
+ return {
3705
+ title,
3706
+ desc,
3707
+ url,
3708
+ imageUrl,
3709
+ channel,
3710
+ };
3711
+ }
3712
+ function isShareChannel(value) {
3713
+ return (value === 'wechatSession' ||
3714
+ value === 'wechatTimeline' ||
3715
+ value === 'qqFriend' ||
3716
+ value === 'qzone' ||
3717
+ value === 'weibo');
3718
+ }
3719
+
3478
3720
  /** 获取小程序隔离 storage。 */
3479
3721
  function getMiniProgramRuntimeStorage(payload, options, platformAdapter) {
3480
3722
  const { key } = readMiniProgramGetStoragePayload(payload, options.maxKeyLength);
@@ -3565,227 +3807,189 @@ function hashString(value) {
3565
3807
  return (hash >>> 0).toString(36);
3566
3808
  }
3567
3809
 
3568
- const NETWORK_REQUEST_ALLOWED_KEYS = new Set([
3569
- 'url',
3570
- 'method',
3571
- 'params',
3572
- 'data',
3573
- 'headers',
3574
- 'timeout',
3575
- 'withCredentials',
3576
- ]);
3577
- const NETWORK_REQUEST_MAX_TIMEOUT = 30000;
3578
- const NETWORK_REQUEST_MAX_BODY_BYTES = 2 * 1024 * 1024;
3579
- const NETWORK_REQUEST_DENIED_HEADERS = new Set([
3580
- 'authorization',
3581
- 'connection',
3582
- 'content-length',
3583
- 'cookie',
3584
- 'host',
3585
- 'origin',
3586
- 'proxy-authorization',
3587
- 'referer',
3588
- 'set-cookie',
3589
- 'transfer-encoding',
3590
- 'user-agent',
3591
- ]);
3592
- const NETWORK_HTTP_METHODS = new Set([
3593
- 'GET',
3594
- 'POST',
3595
- 'PUT',
3596
- 'PATCH',
3597
- 'DELETE',
3598
- 'HEAD',
3599
- 'OPTIONS',
3600
- ]);
3601
- /** network.request:校验桥接参数并委托平台适配层完成真实 HTTP 交换。 */
3602
- async function requestMiniProgramRuntimeNetwork(payload, platformAdapter) {
3603
- return platformAdapter.network.request(readMiniProgramRuntimeNetworkRequest(payload));
3604
- }
3605
- function readMiniProgramRuntimeNetworkRequest(payload) {
3606
- assertMiniProgramRuntimeRecord(payload, 'network.request 参数必须是对象');
3607
- assertNetworkAllowedKeys(payload);
3608
- const request = {
3609
- url: readNetworkRequestUrl(payload.url),
3610
- };
3611
- if (payload.method !== undefined) {
3612
- request.method = readNetworkMethod(payload.method);
3613
- }
3614
- if (payload.params !== undefined) {
3615
- request.params = readUnknownRecord(payload.params, 'network.request params 必须是对象');
3616
- }
3617
- if (payload.data !== undefined) {
3618
- assertNetworkRequestBody(payload.data);
3619
- request.data = payload.data;
3620
- }
3621
- if (payload.headers !== undefined) {
3622
- request.headers = readNetworkRequestHeaders(payload.headers);
3623
- }
3624
- if (payload.timeout !== undefined) {
3625
- request.timeout = readMiniProgramRuntimeNonNegativeNumber(payload.timeout, 'network.request timeout 必须是非负数字');
3626
- if (request.timeout > NETWORK_REQUEST_MAX_TIMEOUT) {
3627
- throw createMiniProgramRuntimeBridgeError('INVALID_PARAMS', `network.request timeout 不能超过 ${NETWORK_REQUEST_MAX_TIMEOUT}ms`);
3628
- }
3629
- }
3630
- if (payload.withCredentials !== undefined) {
3631
- request.withCredentials = readMiniProgramRuntimeBoolean(payload.withCredentials, 'network.request withCredentials 必须是布尔值');
3632
- }
3633
- return request;
3634
- }
3635
- function assertNetworkAllowedKeys(payload) {
3636
- const invalidKeys = Object.keys(payload).filter(key => !NETWORK_REQUEST_ALLOWED_KEYS.has(key));
3637
- if (invalidKeys.length > 0) {
3638
- throw createMiniProgramRuntimeBridgeError('INVALID_PARAMS', `network.request 不支持字段: ${invalidKeys.join(', ')}`);
3639
- }
3640
- }
3641
- function readNetworkRequestUrl(value) {
3642
- const requestUrl = readMiniProgramRuntimeHttpUrl(value, 'network.request url 必须是 HTTP(S) URL');
3643
- assertPublicNetworkRequestUrl(requestUrl);
3644
- return requestUrl;
3645
- }
3646
- function assertPublicNetworkRequestUrl(requestUrl) {
3647
- const url = new URL(requestUrl);
3648
- if (url.username || url.password) {
3649
- throw createMiniProgramRuntimeBridgeError('INVALID_PARAMS', 'network.request url 不允许包含用户信息');
3650
- }
3651
- if (isUnsafeNetworkHost(url.hostname)) {
3652
- throw createMiniProgramRuntimeBridgeError('INVALID_PARAMS', 'network.request url 不允许指向本机、内网或保留地址');
3653
- }
3654
- }
3655
- function isUnsafeNetworkHost(hostname) {
3656
- const normalizedHost = hostname.toLowerCase();
3657
- const ipv6Host = normalizedHost.startsWith('[') && normalizedHost.endsWith(']')
3658
- ? normalizedHost.slice(1, -1)
3659
- : normalizedHost;
3660
- if (normalizedHost === 'localhost' ||
3661
- normalizedHost.endsWith('.localhost') ||
3662
- normalizedHost.endsWith('.local') ||
3663
- normalizedHost === 'metadata.google.internal') {
3664
- return true;
3665
- }
3666
- if (isUnsafeIPv4Host(normalizedHost)) {
3667
- return true;
3668
- }
3669
- return isUnsafeIPv6Host(ipv6Host);
3670
- }
3671
- function isUnsafeIPv4Host(hostname) {
3672
- if (!/^\d{1,3}(\.\d{1,3}){3}$/.test(hostname)) {
3673
- return false;
3674
- }
3675
- const parts = hostname.split('.').map(part => Number(part));
3676
- if (parts.some(part => !Number.isInteger(part) || part < 0 || part > 255)) {
3677
- return true;
3678
- }
3679
- const [a, b] = parts;
3680
- return (a === 0 ||
3681
- a === 10 ||
3682
- a === 127 ||
3683
- (a === 100 && b >= 64 && b <= 127) ||
3684
- (a === 169 && b === 254) ||
3685
- (a === 172 && b >= 16 && b <= 31) ||
3686
- (a === 192 && b === 168));
3687
- }
3688
- function isUnsafeIPv6Host(hostname) {
3689
- return (hostname === '::' ||
3690
- hostname === '::1' ||
3691
- hostname.startsWith('fc') ||
3692
- hostname.startsWith('fd') ||
3693
- hostname.startsWith('fe80:'));
3694
- }
3695
- function readNetworkRequestHeaders(value) {
3696
- const headers = readStringRecord(value, 'network.request headers 必须是 Record<string, string>');
3697
- const deniedHeader = Object.keys(headers).find(isDeniedNetworkRequestHeader);
3698
- if (deniedHeader) {
3699
- throw createMiniProgramRuntimeBridgeError('INVALID_PARAMS', `network.request 不允许设置敏感请求头: ${deniedHeader}`);
3700
- }
3701
- return headers;
3702
- }
3703
- function isDeniedNetworkRequestHeader(headerName) {
3704
- const normalizedHeaderName = headerName.trim().toLowerCase();
3705
- return (NETWORK_REQUEST_DENIED_HEADERS.has(normalizedHeaderName) ||
3706
- normalizedHeaderName.startsWith('x-heybox-') ||
3707
- normalizedHeaderName.startsWith('x-xhh-'));
3708
- }
3709
- function assertNetworkRequestBody(data) {
3710
- if (isUnsupportedNetworkRequestBody(data)) {
3711
- throw createMiniProgramRuntimeBridgeError('INVALID_PARAMS', 'network.request data 不支持 File、Blob 或 FormData,请使用专用上传能力');
3712
- }
3713
- const bodyBytes = estimateNetworkRequestBodyBytes(data);
3714
- if (bodyBytes > NETWORK_REQUEST_MAX_BODY_BYTES) {
3715
- throw createMiniProgramRuntimeBridgeError('INVALID_PARAMS', `network.request data 不能超过 ${NETWORK_REQUEST_MAX_BODY_BYTES} 字节`);
3716
- }
3717
- }
3718
- function isUnsupportedNetworkRequestBody(data) {
3719
- return ((typeof Blob !== 'undefined' && data instanceof Blob) ||
3720
- (typeof FormData !== 'undefined' && data instanceof FormData));
3721
- }
3722
- function estimateNetworkRequestBodyBytes(data) {
3723
- if (typeof data === 'string') {
3724
- return getMiniProgramRuntimeUtf8ByteLength(data);
3725
- }
3726
- if (data instanceof ArrayBuffer) {
3727
- return data.byteLength;
3728
- }
3729
- if (ArrayBuffer.isView(data)) {
3730
- return data.byteLength;
3731
- }
3732
- if (typeof URLSearchParams !== 'undefined' && data instanceof URLSearchParams) {
3733
- return getMiniProgramRuntimeUtf8ByteLength(data.toString());
3734
- }
3735
- try {
3736
- const serialized = JSON.stringify(data);
3737
- return serialized ? getMiniProgramRuntimeUtf8ByteLength(serialized) : 0;
3738
- }
3739
- catch {
3740
- throw createMiniProgramRuntimeBridgeError('INVALID_PARAMS', 'network.request data 必须可序列化');
3741
- }
3742
- }
3743
- function readNetworkMethod(value) {
3744
- const method = readMiniProgramRuntimeRequiredString(value, 'network.request method 必须是字符串').trim().toUpperCase();
3745
- if (!NETWORK_HTTP_METHODS.has(method)) {
3746
- throw createMiniProgramRuntimeBridgeError('INVALID_PARAMS', 'network.request method 不合法');
3747
- }
3748
- return method;
3810
+ /** 设置导航栏关闭按钮与状态栏图标/文字的前景样式。 */
3811
+ function setMiniProgramRuntimeNavigationBarStyle(payload, platformAdapter) {
3812
+ return platformAdapter.viewport.setNavigationBarStyle(readMiniProgramNavigationBarStyleOptions(payload));
3749
3813
  }
3750
- function readUnknownRecord(value, message) {
3751
- assertMiniProgramRuntimeRecord(value, message);
3752
- return { ...value };
3814
+ function readMiniProgramNavigationBarStyleOptions(payload) {
3815
+ assertMiniProgramRuntimeRecord(payload, 'viewport.setNavigationBarStyle 参数必须是对象');
3816
+ const foregroundStyle = payload.foregroundStyle;
3817
+ if (!isNavigationBarForegroundStyle(foregroundStyle)) {
3818
+ throw createMiniProgramRuntimeBridgeError('INVALID_PARAMS', 'viewport.setNavigationBarStyle foregroundStyle 必须是 light 或 dark');
3819
+ }
3820
+ return {
3821
+ foregroundStyle,
3822
+ };
3753
3823
  }
3754
- function readStringRecord(value, message) {
3755
- assertMiniProgramRuntimeRecord(value, message);
3756
- return Object.entries(value).reduce((headers, [key, headerValue]) => {
3757
- if (!key.trim() || typeof headerValue !== 'string') {
3758
- throw createMiniProgramRuntimeBridgeError('INVALID_PARAMS', message);
3759
- }
3760
- headers[key] = headerValue;
3761
- return headers;
3762
- }, {});
3824
+ function isNavigationBarForegroundStyle(value) {
3825
+ return value === 'light' || value === 'dark';
3763
3826
  }
3764
3827
 
3765
- const MINI_PROGRAM_MOCK_RUNTIME_METHODS = MINI_PROGRAM_PROTOCOL_CAPABILITIES.map(capability => capability.method);
3828
+ const MINI_PROGRAM_MOCK_RUNTIME_METHODS = MINI_PROGRAM_PROTOCOL_CAPABILITIES.map((capability) => capability.method);
3766
3829
  const MINI_PROGRAM_MOCK_RUNTIME_METHOD_HANDLERS = {
3767
- [AUTH_LOGIN_METHOD]: runtime => runtime.login(),
3768
- [USER_GET_INFO_METHOD]: runtime => runtime.getUserInfo(),
3830
+ [AUTH_LOGIN_METHOD]: (runtime) => runtime.login(),
3831
+ [USER_GET_INFO_METHOD]: (runtime) => runtime.getUserInfo(),
3769
3832
  [SHARE_SHOW_SHARE_MENU_METHOD]: (runtime, payload) => runtime.showShareMenu(payload),
3770
3833
  [SHARE_SCREENSHOT_METHOD]: (runtime, payload) => runtime.shareScreenshot(payload),
3771
- [VIEWPORT_GET_WINDOW_INFO_METHOD]: runtime => runtime.getWindowInfo(),
3834
+ [VIEWPORT_GET_WINDOW_INFO_METHOD]: (runtime) => runtime.getWindowInfo(),
3772
3835
  [VIEWPORT_SET_NAVIGATION_BAR_STYLE_METHOD]: (runtime, payload) => runtime.setNavigationBarStyle(payload),
3773
3836
  [STORAGE_GET_STORAGE_METHOD]: (runtime, payload) => runtime.getStorage(payload),
3774
3837
  [STORAGE_SET_STORAGE_METHOD]: (runtime, payload) => runtime.setStorage(payload),
3775
3838
  [NETWORK_REQUEST_METHOD]: (runtime, payload) => runtime.requestNetwork(payload),
3839
+ [UI_SHOW_TOAST_METHOD]: (runtime, payload) => runtime.showToast(payload),
3840
+ [UI_SHOW_LOADING_METHOD]: (runtime, payload) => runtime.showLoading(payload),
3841
+ [UI_HIDE_LOADING_METHOD]: (runtime, payload) => runtime.hideLoading(payload),
3842
+ [DEVICE_VIBRATE_METHOD]: (runtime, payload) => runtime.vibrate(payload),
3843
+ [DEVICE_SET_CLIPBOARD_METHOD]: (runtime, payload) => runtime.setClipboard(payload),
3844
+ [NAVIGATION_CLOSE_METHOD]: (runtime, payload) => runtime.close(payload),
3845
+ [NAVIGATION_RELOAD_METHOD]: (runtime, payload) => runtime.reload(payload),
3776
3846
  };
3847
+ function createBrowserMockRuntimePlatformAdapter(options) {
3848
+ return {
3849
+ app: {
3850
+ getCurrentHref: options.getCurrentHref,
3851
+ },
3852
+ launch: {
3853
+ async getMiniProgramInfo() {
3854
+ return {
3855
+ pageUrl: options.getMiniProgramPageUrl(),
3856
+ };
3857
+ },
3858
+ },
3859
+ auth: {
3860
+ async getCurrentUserId() {
3861
+ return options.getCurrentUser()?.heybox_id;
3862
+ },
3863
+ async login() {
3864
+ await options.login();
3865
+ },
3866
+ },
3867
+ user: {
3868
+ async getUserBasicInfo(userId) {
3869
+ const currentUser = options.getCurrentUser();
3870
+ if (!currentUser || currentUser.heybox_id !== userId) {
3871
+ return undefined;
3872
+ }
3873
+ return {
3874
+ nickname: currentUser.nickname,
3875
+ avatar: currentUser.avatar,
3876
+ };
3877
+ },
3878
+ },
3879
+ share: {
3880
+ async showShareMenu(params) {
3881
+ return {
3882
+ ok: true,
3883
+ platform: 'hb-sdk-mock-host',
3884
+ type: 'showShareMenu',
3885
+ params,
3886
+ };
3887
+ },
3888
+ async shareScreenshot(params) {
3889
+ return {
3890
+ ok: true,
3891
+ platform: 'hb-sdk-mock-host',
3892
+ type: 'screenshot',
3893
+ params,
3894
+ };
3895
+ },
3896
+ },
3897
+ viewport: {
3898
+ setDocumentTitle(title) {
3899
+ options.setDocumentTitle?.(title);
3900
+ },
3901
+ setNavigationBarStyle(styleOptions) {
3902
+ return options.setNavigationBarStyle?.(styleOptions);
3903
+ },
3904
+ getViewportMetrics() {
3905
+ const rect = options.iframe.getBoundingClientRect();
3906
+ const width = Math.max(1, Math.round(rect.width));
3907
+ const height = Math.max(1, Math.round(rect.height));
3908
+ const currentWindow = options.iframe.ownerDocument.defaultView ?? readWindow();
3909
+ return {
3910
+ innerWidth: width,
3911
+ innerHeight: height,
3912
+ documentElementClientWidth: width,
3913
+ documentElementClientHeight: height,
3914
+ screenWidth: currentWindow?.screen.width || width,
3915
+ screenHeight: currentWindow?.screen.height || height,
3916
+ pixelRatio: currentWindow?.devicePixelRatio || 1,
3917
+ };
3918
+ },
3919
+ getNavigationBarHeight() {
3920
+ return 0;
3921
+ },
3922
+ },
3923
+ ui: {
3924
+ async showToast() {
3925
+ return undefined;
3926
+ },
3927
+ async showLoading() {
3928
+ return undefined;
3929
+ },
3930
+ async hideLoading() {
3931
+ return undefined;
3932
+ },
3933
+ },
3934
+ device: {
3935
+ async vibrate() {
3936
+ return undefined;
3937
+ },
3938
+ async setClipboard() {
3939
+ return undefined;
3940
+ },
3941
+ },
3942
+ navigation: {
3943
+ async close() {
3944
+ return undefined;
3945
+ },
3946
+ async reload() {
3947
+ return undefined;
3948
+ },
3949
+ },
3950
+ storage: {
3951
+ getStorage: options.getStorage,
3952
+ setStorage: options.setStorage,
3953
+ },
3954
+ network: {
3955
+ request: options.requestNetwork,
3956
+ },
3957
+ };
3958
+ }
3777
3959
  class MiniProgramMockRuntime {
3778
3960
  constructor(adapter, options = {}) {
3779
3961
  this.adapter = adapter;
3780
3962
  this.options = options;
3963
+ this.clipboardText = null;
3964
+ this.closed = false;
3965
+ this.loading = {
3966
+ dismissible: false,
3967
+ visible: false,
3968
+ };
3969
+ this.records = [];
3970
+ this.toasts = [];
3971
+ this.vibrates = [];
3781
3972
  }
3782
3973
  async runMethod(method, payload) {
3783
3974
  if (!isMiniProgramMockRuntimeMethod(method)) {
3784
3975
  throw createMockBridgeError('METHOD_NOT_FOUND', `未开放小程序能力: ${method}`);
3785
3976
  }
3977
+ if (this.closed && method !== NAVIGATION_CLOSE_METHOD) {
3978
+ throw createMockBridgeError('INVALID_STATE', '当前小程序容器已关闭');
3979
+ }
3786
3980
  const handler = MINI_PROGRAM_MOCK_RUNTIME_METHOD_HANDLERS[method];
3787
3981
  return handler(this, payload);
3788
3982
  }
3983
+ getState() {
3984
+ return {
3985
+ clipboardText: this.clipboardText,
3986
+ closed: this.closed,
3987
+ loading: { ...this.loading },
3988
+ records: [...this.records],
3989
+ toasts: [...this.toasts],
3990
+ vibrates: [...this.vibrates],
3991
+ };
3992
+ }
3789
3993
  async login() {
3790
3994
  const result = await loginAndGetMiniProgramRuntimeUserInfo(this.adapter);
3791
3995
  this.options.onAuthChange?.(result);
@@ -3815,6 +4019,80 @@ class MiniProgramMockRuntime {
3815
4019
  requestNetwork(payload) {
3816
4020
  return requestMiniProgramRuntimeNetwork(payload, this.adapter);
3817
4021
  }
4022
+ async showToast(payload) {
4023
+ const toast = readShowToastPayload(payload);
4024
+ this.toasts.unshift(toast);
4025
+ this.toasts.splice(10);
4026
+ this.record(UI_SHOW_TOAST_METHOD, toast);
4027
+ renderMockToast(toast);
4028
+ await this.options.onToast?.(toast);
4029
+ }
4030
+ async showLoading(payload) {
4031
+ this.loading = readShowLoadingPayload(payload);
4032
+ this.record(UI_SHOW_LOADING_METHOD, this.loading);
4033
+ renderMockLoading(this.loading);
4034
+ await this.options.onLoadingChange?.({ ...this.loading });
4035
+ }
4036
+ async hideLoading(payload) {
4037
+ assertEmptyPayload(payload, 'ui.hideLoading');
4038
+ this.loading = {
4039
+ dismissible: false,
4040
+ visible: false,
4041
+ };
4042
+ this.record(UI_HIDE_LOADING_METHOD, this.loading);
4043
+ renderMockLoading(this.loading);
4044
+ await this.options.onLoadingChange?.({ ...this.loading });
4045
+ }
4046
+ async vibrate(payload) {
4047
+ const action = readVibratePayload(payload);
4048
+ this.vibrates.unshift(action);
4049
+ this.vibrates.splice(10);
4050
+ this.record(DEVICE_VIBRATE_METHOD, action);
4051
+ if (typeof navigator !== 'undefined' && typeof navigator.vibrate === 'function') {
4052
+ navigator.vibrate(action.pattern);
4053
+ }
4054
+ await this.options.onVibrate?.(action);
4055
+ }
4056
+ async setClipboard(payload) {
4057
+ const text = readSetClipboardPayload(payload);
4058
+ this.record(DEVICE_SET_CLIPBOARD_METHOD, { text });
4059
+ try {
4060
+ if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
4061
+ await navigator.clipboard.writeText(text);
4062
+ }
4063
+ }
4064
+ catch {
4065
+ // Browser clipboard permissions are intentionally non-blocking in the mock host.
4066
+ }
4067
+ this.clipboardText = text;
4068
+ await this.options.onClipboardText?.(text);
4069
+ }
4070
+ async close(payload) {
4071
+ assertEmptyPayload(payload, 'navigation.close');
4072
+ this.record(NAVIGATION_CLOSE_METHOD, { closed: this.closed });
4073
+ if (this.closed) {
4074
+ return;
4075
+ }
4076
+ this.closed = true;
4077
+ closeMockIframe();
4078
+ await this.options.onClose?.();
4079
+ }
4080
+ async reload(payload) {
4081
+ assertEmptyPayload(payload, 'navigation.reload');
4082
+ this.record(NAVIGATION_RELOAD_METHOD, {});
4083
+ await this.options.onReload?.();
4084
+ if (!this.options.onReload) {
4085
+ reloadMockIframe();
4086
+ }
4087
+ }
4088
+ record(method, payload) {
4089
+ this.records.unshift({
4090
+ method,
4091
+ payload,
4092
+ timestamp: Date.now(),
4093
+ });
4094
+ this.records.splice(30);
4095
+ }
3818
4096
  }
3819
4097
  function isMiniProgramMockRuntimeMethod(method) {
3820
4098
  return MINI_PROGRAM_MOCK_RUNTIME_METHODS.includes(method);
@@ -3844,6 +4122,219 @@ function toMockBridgeError(error) {
3844
4122
  function isObjectLike(value) {
3845
4123
  return (typeof value === 'object' || typeof value === 'function') && value !== null;
3846
4124
  }
4125
+ function readShowToastPayload(payload) {
4126
+ if (!isPlainRecord(payload)) {
4127
+ throw createMockBridgeError('INVALID_PARAMS', 'ui.showToast options 必须是对象');
4128
+ }
4129
+ assertOnlyAllowedKeys(payload, new Set(['message', 'status']), 'ui.showToast');
4130
+ if (typeof payload.message !== 'string') {
4131
+ throw createMockBridgeError('INVALID_PARAMS', 'ui.showToast message 必须是字符串');
4132
+ }
4133
+ const message = payload.message.trim();
4134
+ if (!message) {
4135
+ throw createMockBridgeError('INVALID_PARAMS', 'ui.showToast message 不能为空');
4136
+ }
4137
+ if (message.length > 120) {
4138
+ throw createMockBridgeError('INVALID_PARAMS', 'ui.showToast message 不能超过 120 个字符');
4139
+ }
4140
+ if (payload.status !== undefined && payload.status !== 'success' && payload.status !== 'error') {
4141
+ throw createMockBridgeError('INVALID_PARAMS', 'ui.showToast status 必须是 success 或 error');
4142
+ }
4143
+ return {
4144
+ message,
4145
+ status: payload.status,
4146
+ timestamp: Date.now(),
4147
+ };
4148
+ }
4149
+ function readShowLoadingPayload(payload) {
4150
+ if (payload === undefined) {
4151
+ return {
4152
+ dismissible: false,
4153
+ visible: true,
4154
+ };
4155
+ }
4156
+ if (!isPlainRecord(payload)) {
4157
+ throw createMockBridgeError('INVALID_PARAMS', 'ui.showLoading options 必须是对象');
4158
+ }
4159
+ assertOnlyAllowedKeys(payload, new Set(['dismissible']), 'ui.showLoading');
4160
+ if (payload.dismissible !== undefined && typeof payload.dismissible !== 'boolean') {
4161
+ throw createMockBridgeError('INVALID_PARAMS', 'ui.showLoading dismissible 必须是布尔值');
4162
+ }
4163
+ return {
4164
+ dismissible: payload.dismissible === true,
4165
+ visible: true,
4166
+ };
4167
+ }
4168
+ function readVibratePayload(payload) {
4169
+ if (payload !== undefined && !isPlainRecord(payload)) {
4170
+ throw createMockBridgeError('INVALID_PARAMS', 'device.vibrate options 必须是对象');
4171
+ }
4172
+ if (payload !== undefined) {
4173
+ assertOnlyAllowedKeys(payload, new Set(['intensity', 'delay']), 'device.vibrate');
4174
+ }
4175
+ const intensity = payload?.intensity ?? 'light';
4176
+ if (intensity !== 'light' && intensity !== 'medium' && intensity !== 'heavy') {
4177
+ throw createMockBridgeError('INVALID_PARAMS', 'device.vibrate intensity 必须是 light、medium 或 heavy');
4178
+ }
4179
+ const rawDelay = payload?.delay;
4180
+ if (rawDelay !== undefined && (typeof rawDelay !== 'number' || !Number.isInteger(rawDelay) || rawDelay < 0 || rawDelay > 5000)) {
4181
+ throw createMockBridgeError('INVALID_PARAMS', 'device.vibrate delay 必须是 0..5000 的整数毫秒');
4182
+ }
4183
+ const delay = rawDelay ?? 0;
4184
+ return {
4185
+ delay,
4186
+ intensity,
4187
+ pattern: createVibratePattern(intensity),
4188
+ timestamp: Date.now(),
4189
+ };
4190
+ }
4191
+ function readSetClipboardPayload(payload) {
4192
+ if (!isPlainRecord(payload)) {
4193
+ throw createMockBridgeError('INVALID_PARAMS', 'device.setClipboard options 必须是对象');
4194
+ }
4195
+ const extraKeys = Object.keys(payload).filter((key) => key !== 'text');
4196
+ if (extraKeys.length > 0) {
4197
+ throw createMockBridgeError('INVALID_PARAMS', `device.setClipboard 不支持字段: ${extraKeys.join(', ')}`);
4198
+ }
4199
+ if (typeof payload.text !== 'string') {
4200
+ throw createMockBridgeError('INVALID_PARAMS', 'device.setClipboard text 必须是字符串');
4201
+ }
4202
+ if (!payload.text.trim()) {
4203
+ throw createMockBridgeError('INVALID_PARAMS', 'device.setClipboard text 不能为空');
4204
+ }
4205
+ if (payload.text.length > 10000) {
4206
+ throw createMockBridgeError('INVALID_PARAMS', 'device.setClipboard text 不能超过 10000 个字符');
4207
+ }
4208
+ return payload.text;
4209
+ }
4210
+ function assertEmptyPayload(payload, methodName) {
4211
+ if (payload === undefined) {
4212
+ return;
4213
+ }
4214
+ if (!isPlainRecord(payload) || Object.keys(payload).length > 0) {
4215
+ throw createMockBridgeError('INVALID_PARAMS', `${methodName} 不支持参数`);
4216
+ }
4217
+ }
4218
+ function assertOnlyAllowedKeys(payload, allowedKeys, methodName) {
4219
+ const invalidKeys = Object.keys(payload).filter((key) => !allowedKeys.has(key));
4220
+ if (invalidKeys.length > 0) {
4221
+ throw createMockBridgeError('INVALID_PARAMS', `${methodName} 不支持字段: ${invalidKeys.join(', ')}`);
4222
+ }
4223
+ }
4224
+ function createVibratePattern(intensity) {
4225
+ switch (intensity) {
4226
+ case 'heavy':
4227
+ return 40;
4228
+ case 'medium':
4229
+ return 25;
4230
+ case 'light':
4231
+ default:
4232
+ return 10;
4233
+ }
4234
+ }
4235
+ function renderMockToast(toast) {
4236
+ const document = readDocument();
4237
+ if (!document) {
4238
+ return;
4239
+ }
4240
+ const element = findOrCreateMockElement(document, 'hb-sdk-mock-runtime-toast');
4241
+ element.textContent = toast.message;
4242
+ element.dataset.status = toast.status ?? 'default';
4243
+ element.style.cssText = [
4244
+ 'position:fixed',
4245
+ 'left:50%',
4246
+ 'bottom:48px',
4247
+ 'transform:translateX(-50%)',
4248
+ 'z-index:2147483647',
4249
+ 'max-width:80%',
4250
+ 'padding:8px 12px',
4251
+ 'border-radius:6px',
4252
+ 'background:rgba(17,24,39,.92)',
4253
+ 'color:#fff',
4254
+ 'font:13px/1.4 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif',
4255
+ 'box-shadow:0 8px 24px rgba(0,0,0,.18)',
4256
+ 'pointer-events:none',
4257
+ 'white-space:pre-wrap',
4258
+ ].join(';');
4259
+ }
4260
+ function renderMockLoading(state) {
4261
+ const document = readDocument();
4262
+ if (!document) {
4263
+ return;
4264
+ }
4265
+ const element = findOrCreateMockElement(document, 'hb-sdk-mock-runtime-loading');
4266
+ element.hidden = !state.visible;
4267
+ element.dataset.dismissible = String(state.dismissible);
4268
+ element.textContent = '加载中...';
4269
+ element.style.cssText = [
4270
+ 'position:fixed',
4271
+ 'inset:0',
4272
+ 'z-index:2147483646',
4273
+ 'display:flex',
4274
+ 'align-items:center',
4275
+ 'justify-content:center',
4276
+ 'background:rgba(15,23,42,.16)',
4277
+ 'color:#fff',
4278
+ 'font:14px/1.4 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif',
4279
+ 'pointer-events:none',
4280
+ ].join(';');
4281
+ }
4282
+ function reloadMockIframe() {
4283
+ const iframe = readMockIframe();
4284
+ if (!iframe) {
4285
+ return;
4286
+ }
4287
+ setTimeout(() => {
4288
+ iframe.src = iframe.src;
4289
+ }, 0);
4290
+ }
4291
+ function closeMockIframe() {
4292
+ const iframe = readMockIframe();
4293
+ if (!iframe) {
4294
+ return;
4295
+ }
4296
+ iframe.hidden = true;
4297
+ iframe.dataset.hbSdkMockClosed = 'true';
4298
+ const document = readDocument();
4299
+ const parent = iframe.parentElement;
4300
+ if (!document || !parent) {
4301
+ return;
4302
+ }
4303
+ const element = findOrCreateMockElement(document, 'hb-sdk-mock-runtime-closed');
4304
+ element.textContent = '小程序已关闭';
4305
+ element.style.cssText = [
4306
+ 'display:flex',
4307
+ 'align-items:center',
4308
+ 'justify-content:center',
4309
+ 'min-height:240px',
4310
+ 'color:#64748b',
4311
+ 'font:14px/1.4 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif',
4312
+ ].join(';');
4313
+ parent.appendChild(element);
4314
+ }
4315
+ function readMockIframe() {
4316
+ const document = readDocument();
4317
+ return document?.querySelector('iframe') ?? null;
4318
+ }
4319
+ function findOrCreateMockElement(document, id) {
4320
+ const current = document.getElementById(id);
4321
+ if (current) {
4322
+ return current;
4323
+ }
4324
+ const element = document.createElement('div');
4325
+ element.id = id;
4326
+ document.body.appendChild(element);
4327
+ return element;
4328
+ }
4329
+ function readDocument() {
4330
+ return typeof document === 'undefined' ? null : document;
4331
+ }
4332
+ function readWindow() {
4333
+ return typeof window === 'undefined' ? undefined : window;
4334
+ }
4335
+ function isPlainRecord(value) {
4336
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
4337
+ }
3847
4338
 
3848
4339
  const MINI_PROGRAM_URL_QUERY_PARAM = 'mini_url';
3849
4340
  const MINI_PROGRAM_DEV_SHELL_URL = 'heybox-mini-dev://sandbox';
@@ -3892,8 +4383,30 @@ iframe.src = appendNonce(miniProgramUrl, nonce);
3892
4383
  iframe.allow = 'clipboard-read; clipboard-write';
3893
4384
  elements.device.appendChild(iframe);
3894
4385
  elements.miniUrl.textContent = miniProgramUrl;
3895
- const runtime = new MiniProgramMockRuntime(createBrowserMockRuntimeAdapter(), {
3896
- onAuthChange: result => postEvent('authChange', result),
4386
+ const runtime = new MiniProgramMockRuntime(createBrowserMockRuntimePlatformAdapter({
4387
+ iframe,
4388
+ getCurrentHref: () => location.href,
4389
+ getCurrentUser: () => currentUser,
4390
+ getMiniProgramPageUrl: () => miniProgramUrl,
4391
+ getStorage: ({ key }) => storage.get(key),
4392
+ async login() {
4393
+ currentUser = createUserFromForm();
4394
+ updateUserStatus();
4395
+ },
4396
+ requestNetwork,
4397
+ setDocumentTitle(title) {
4398
+ document.title = title;
4399
+ },
4400
+ setNavigationBarStyle(options) {
4401
+ navigationBarStyle = options.foregroundStyle;
4402
+ document.documentElement.dataset.navigationBarStyle = navigationBarStyle;
4403
+ },
4404
+ async setStorage({ key, value }) {
4405
+ storage.set(key, value);
4406
+ updateStorageSnapshot();
4407
+ },
4408
+ }), {
4409
+ onAuthChange: (result) => postEvent('authChange', result),
3897
4410
  });
3898
4411
  updateUserStatus();
3899
4412
  updateStorageSnapshot();
@@ -3925,98 +4438,6 @@ elements.mobileLanSelect.addEventListener('change', () => {
3925
4438
  void updateMobileQrCode();
3926
4439
  });
3927
4440
  const mobileAppQrReady = setupMobileAppQr();
3928
- function createBrowserMockRuntimeAdapter() {
3929
- return {
3930
- app: {
3931
- getCurrentHref() {
3932
- return location.href;
3933
- },
3934
- },
3935
- launch: {
3936
- async getMiniProgramInfo() {
3937
- return {
3938
- pageUrl: miniProgramUrl,
3939
- };
3940
- },
3941
- },
3942
- auth: {
3943
- async getCurrentUserId() {
3944
- return currentUser?.heybox_id;
3945
- },
3946
- async login() {
3947
- currentUser = createUserFromForm();
3948
- updateUserStatus();
3949
- },
3950
- },
3951
- user: {
3952
- async getUserBasicInfo(userId) {
3953
- if (!currentUser || currentUser.heybox_id !== userId) {
3954
- return undefined;
3955
- }
3956
- return {
3957
- nickname: currentUser.nickname,
3958
- avatar: currentUser.avatar,
3959
- };
3960
- },
3961
- },
3962
- share: {
3963
- async showShareMenu(options) {
3964
- return {
3965
- ok: true,
3966
- platform: 'hb-sdk-mock-host',
3967
- type: 'showShareMenu',
3968
- params: options,
3969
- };
3970
- },
3971
- async shareScreenshot(options) {
3972
- return {
3973
- ok: true,
3974
- platform: 'hb-sdk-mock-host',
3975
- type: 'screenshot',
3976
- params: options,
3977
- };
3978
- },
3979
- },
3980
- viewport: {
3981
- setDocumentTitle(title) {
3982
- document.title = title;
3983
- },
3984
- setNavigationBarStyle(options) {
3985
- navigationBarStyle = options.foregroundStyle;
3986
- document.documentElement.dataset.navigationBarStyle = navigationBarStyle;
3987
- },
3988
- getViewportMetrics() {
3989
- const rect = iframe.getBoundingClientRect();
3990
- const width = Math.max(1, Math.round(rect.width));
3991
- const height = Math.max(1, Math.round(rect.height));
3992
- return {
3993
- innerWidth: width,
3994
- innerHeight: height,
3995
- documentElementClientWidth: width,
3996
- documentElementClientHeight: height,
3997
- screenWidth: window.screen.width || width,
3998
- screenHeight: window.screen.height || height,
3999
- pixelRatio: window.devicePixelRatio || 1,
4000
- };
4001
- },
4002
- getNavigationBarHeight() {
4003
- return 0;
4004
- },
4005
- },
4006
- storage: {
4007
- getStorage({ key }) {
4008
- return storage.get(key);
4009
- },
4010
- async setStorage({ key, value }) {
4011
- storage.set(key, value);
4012
- updateStorageSnapshot();
4013
- },
4014
- },
4015
- network: {
4016
- request: requestNetwork,
4017
- },
4018
- };
4019
- }
4020
4441
  function handleMessage(event) {
4021
4442
  if (event.source !== iframe.contentWindow || !isBridgeMessage(event.data)) {
4022
4443
  return;
@@ -4086,9 +4507,7 @@ async function readNetworkProxyResponse(response) {
4086
4507
  catch {
4087
4508
  throw {
4088
4509
  code: response.status === 404 ? 'MOCK_NETWORK_PROXY_NOT_FOUND' : 'MOCK_NETWORK_PROXY_ERROR',
4089
- message: response.status === 404
4090
- ? 'mock network proxy 不可用,请重启 hb-sdk dev'
4091
- : text,
4510
+ message: response.status === 404 ? 'mock network proxy 不可用,请重启 hb-sdk dev' : text,
4092
4511
  };
4093
4512
  }
4094
4513
  }
@@ -4325,7 +4744,7 @@ function pushLog(method, detail) {
4325
4744
  });
4326
4745
  logs.splice(30);
4327
4746
  elements.logs.innerHTML = logs
4328
- .map(log => `<article class="log"><strong>${escapeHtml(log.method)} · ${escapeHtml(log.timestamp)}</strong><pre>${escapeHtml(JSON.stringify(log.detail, null, 2))}</pre></article>`)
4747
+ .map((log) => `<article class="log"><strong>${escapeHtml(log.method)} · ${escapeHtml(log.timestamp)}</strong><pre>${escapeHtml(JSON.stringify(log.detail, null, 2))}</pre></article>`)
4329
4748
  .join('');
4330
4749
  }
4331
4750
  function appendNonce(input, value) {
@@ -4366,10 +4785,5 @@ function isRecord(value) {
4366
4785
  return typeof value === 'object' && value !== null;
4367
4786
  }
4368
4787
  function escapeHtml(value) {
4369
- return String(value)
4370
- .replace(/&/g, '&amp;')
4371
- .replace(/</g, '&lt;')
4372
- .replace(/>/g, '&gt;')
4373
- .replace(/"/g, '&quot;')
4374
- .replace(/'/g, '&#039;');
4788
+ return String(value).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;');
4375
4789
  }