@heybox/hb-sdk 0.7.4-alpha.4 → 0.7.4-alpha.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +19 -1
- package/dist/cli-chunks/{build-BqGFRLe-.cjs → build-DF74IHGR.cjs} +2 -2
- package/dist/cli-chunks/{context-DTr3hRTv.cjs → context-QemmshdY.cjs} +26 -1
- package/dist/cli-chunks/{create-OkiiiH4I.cjs → create-Ch9m8w8M.cjs} +1 -1
- package/dist/cli-chunks/dev-D2DwLxXW.cjs +15682 -0
- package/dist/cli-chunks/{doctor-BO0vkDny.cjs → doctor-B9xJ2j20.cjs} +1 -1
- package/dist/cli-chunks/{index-CZuvk1Rg.cjs → index-Cv36miWx.cjs} +83 -83
- package/dist/cli-chunks/{index-CyEqCbeu.cjs → index-DxQgKWRT.cjs} +31 -32
- package/dist/cli-chunks/index.esm-DZQrllrR.cjs +5212 -0
- package/dist/cli-chunks/{login-CuAGltGs.cjs → login-DEKhX1Vf.cjs} +2 -2
- package/dist/cli-chunks/{project-vite-BUY9DgYo.cjs → project-vite-D_NNB_Ib.cjs} +1 -1
- package/dist/cli-chunks/{remote-tPMcxgpi.cjs → remote-B_Bn4v81.cjs} +4 -4
- package/dist/cli-chunks/{session-BUjFX2U2.cjs → session-C2zY7XHn.cjs} +2 -1
- package/dist/cli.cjs +1 -1
- package/dist/devtools/browser-dev-host/index.html +128 -151
- package/dist/devtools/browser-dev-host/main.js +2591 -345
- package/dist/index.cjs.js +1187 -3
- package/dist/index.esm.js +1187 -3
- package/dist/protocol.cjs.js +3 -0
- package/dist/protocol.esm.js +3 -1
- package/dist/vite.cjs.js +1 -1
- package/dist/vite.esm.js +1 -1
- package/package.json +16 -8
- package/skill/SKILL.md +7 -6
- package/skill/references/api-protocol.md +6 -3
- package/skill/references/api-root.md +6 -2
- package/skill/references/cli.md +16 -20
- package/skill/references/examples.md +2 -2
- package/skill/references/recipes.md +37 -0
- package/skill/references/safety-boundaries.md +1 -1
- package/skill/references/smoke-evaluation.md +1 -1
- package/skill/scripts/sync-references.mjs +6 -6
- package/skill/skill.json +4 -4
- package/types/index.d.ts +1 -1
- package/types/modules/share/copy-link.d.ts +16 -0
- package/types/modules/share/extra.d.ts +3 -0
- package/types/modules/share/index.d.ts +8 -2
- package/types/modules/share/types.d.ts +11 -0
- package/types/protocol/capabilities.d.ts +1 -1
- package/types/protocol/dev-session.d.ts +13 -0
- package/types/protocol.d.ts +2 -2
- package/dist/cli-chunks/dev-j7W9R7ew.cjs +0 -2769
|
@@ -2,6 +2,7 @@ const AUTH_LOGIN_METHOD = 'auth.login';
|
|
|
2
2
|
const USER_GET_INFO_METHOD = 'user.getInfo';
|
|
3
3
|
const USER_REVOKE_AUTHORIZATION_METHOD = 'user.revokeAuthorization';
|
|
4
4
|
const USER_GET_STEAM_GAME_LIST_METHOD = 'user.getSteamGameList';
|
|
5
|
+
const SHARE_COPY_LINK_METHOD = 'share.copyLink';
|
|
5
6
|
const SHARE_SHOW_SHARE_MENU_METHOD = 'share.showShareMenu';
|
|
6
7
|
const SHARE_SCREENSHOT_METHOD = 'share.screenshot';
|
|
7
8
|
const VIEWPORT_GET_WINDOW_INFO_METHOD = 'viewport.getWindowInfo';
|
|
@@ -39,6 +40,7 @@ const MINI_PROGRAM_PROTOCOL_CAPABILITIES = [
|
|
|
39
40
|
permission: 'user.platformAccount.steam',
|
|
40
41
|
risk: 'high',
|
|
41
42
|
},
|
|
43
|
+
{ method: SHARE_COPY_LINK_METHOD, module: 'share', capability: SHARE_COPY_LINK_METHOD, permission: 'share.copyLink', risk: 'medium' },
|
|
42
44
|
{ method: SHARE_SHOW_SHARE_MENU_METHOD, module: 'share', capability: SHARE_SHOW_SHARE_MENU_METHOD, permission: 'share.basic', risk: 'low' },
|
|
43
45
|
{ method: SHARE_SCREENSHOT_METHOD, module: 'share', capability: SHARE_SCREENSHOT_METHOD, permission: 'share.screenshot', risk: 'medium' },
|
|
44
46
|
{
|
|
@@ -445,6 +447,7 @@ const HOST_METHOD_PRIMITIVE_REQUIREMENTS = {
|
|
|
445
447
|
[USER_GET_INFO_METHOD]: ['account.getCurrentUserId', 'account.onChange', 'network.request'],
|
|
446
448
|
[USER_REVOKE_AUTHORIZATION_METHOD]: ['account.getCurrentUserId', 'account.onChange', 'network.request'],
|
|
447
449
|
[USER_GET_STEAM_GAME_LIST_METHOD]: ['account.getCurrentUserId', 'account.onChange', 'network.request'],
|
|
450
|
+
[SHARE_COPY_LINK_METHOD]: ['presentation.setClipboard'],
|
|
448
451
|
[SHARE_SHOW_SHARE_MENU_METHOD]: ['presentation.showShareMenu'],
|
|
449
452
|
[SHARE_SCREENSHOT_METHOD]: ['presentation.getViewportMetrics', 'presentation.captureAndShare'],
|
|
450
453
|
[VIEWPORT_GET_WINDOW_INFO_METHOD]: ['presentation.getViewportMetrics', 'presentation.getNavigationBarHeight'],
|
|
@@ -701,6 +704,7 @@ const userMiniprogramPublicDetailEndpoint = {
|
|
|
701
704
|
...(input.local_dev_launch_token ? { local_dev_launch_token: input.local_dev_launch_token } : {}),
|
|
702
705
|
...(input.entry ? { entry: input.entry } : {}),
|
|
703
706
|
...(input.h_src_parent ? { h_src_parent: input.h_src_parent } : {}),
|
|
707
|
+
...(input.extra !== undefined ? { extra: input.extra } : {}),
|
|
704
708
|
};
|
|
705
709
|
},
|
|
706
710
|
/**
|
|
@@ -2791,7 +2795,7 @@ function inferBodyEncoding(headers, data, official) {
|
|
|
2791
2795
|
return 'json';
|
|
2792
2796
|
if (typeof URLSearchParams !== 'undefined' && data instanceof URLSearchParams)
|
|
2793
2797
|
return 'form';
|
|
2794
|
-
if (official && (Array.isArray(data) || isPlainRecord(data)))
|
|
2798
|
+
if (official && (Array.isArray(data) || isPlainRecord$1(data)))
|
|
2795
2799
|
return 'form';
|
|
2796
2800
|
return isPlainJsonValue(data) ? 'json' : undefined;
|
|
2797
2801
|
}
|
|
@@ -2807,7 +2811,7 @@ function isPlainJsonValue(value) {
|
|
|
2807
2811
|
typeof value === 'number' ||
|
|
2808
2812
|
(typeof value === 'object' && !ArrayBuffer.isView(value) && !(value instanceof ArrayBuffer)));
|
|
2809
2813
|
}
|
|
2810
|
-
function isPlainRecord(value) {
|
|
2814
|
+
function isPlainRecord$1(value) {
|
|
2811
2815
|
if (typeof value !== 'object' || value === null)
|
|
2812
2816
|
return false;
|
|
2813
2817
|
const prototype = Object.getPrototypeOf(value);
|
|
@@ -2880,6 +2884,1009 @@ function bridgeError$2(code, message) {
|
|
|
2880
2884
|
return { code, message };
|
|
2881
2885
|
}
|
|
2882
2886
|
|
|
2887
|
+
function utf8Count(str) {
|
|
2888
|
+
const strLength = str.length;
|
|
2889
|
+
let byteLength = 0;
|
|
2890
|
+
let pos = 0;
|
|
2891
|
+
while (pos < strLength) {
|
|
2892
|
+
let value = str.charCodeAt(pos++);
|
|
2893
|
+
if ((value & 0xffffff80) === 0) {
|
|
2894
|
+
// 1-byte
|
|
2895
|
+
byteLength++;
|
|
2896
|
+
continue;
|
|
2897
|
+
}
|
|
2898
|
+
else if ((value & 0xfffff800) === 0) {
|
|
2899
|
+
// 2-bytes
|
|
2900
|
+
byteLength += 2;
|
|
2901
|
+
}
|
|
2902
|
+
else {
|
|
2903
|
+
// handle surrogate pair
|
|
2904
|
+
if (value >= 0xd800 && value <= 0xdbff) {
|
|
2905
|
+
// high surrogate
|
|
2906
|
+
if (pos < strLength) {
|
|
2907
|
+
const extra = str.charCodeAt(pos);
|
|
2908
|
+
if ((extra & 0xfc00) === 0xdc00) {
|
|
2909
|
+
++pos;
|
|
2910
|
+
value = ((value & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000;
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2914
|
+
if ((value & 0xffff0000) === 0) {
|
|
2915
|
+
// 3-byte
|
|
2916
|
+
byteLength += 3;
|
|
2917
|
+
}
|
|
2918
|
+
else {
|
|
2919
|
+
// 4-byte
|
|
2920
|
+
byteLength += 4;
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
return byteLength;
|
|
2925
|
+
}
|
|
2926
|
+
function utf8EncodeJs(str, output, outputOffset) {
|
|
2927
|
+
const strLength = str.length;
|
|
2928
|
+
let offset = outputOffset;
|
|
2929
|
+
let pos = 0;
|
|
2930
|
+
while (pos < strLength) {
|
|
2931
|
+
let value = str.charCodeAt(pos++);
|
|
2932
|
+
if ((value & 0xffffff80) === 0) {
|
|
2933
|
+
// 1-byte
|
|
2934
|
+
output[offset++] = value;
|
|
2935
|
+
continue;
|
|
2936
|
+
}
|
|
2937
|
+
else if ((value & 0xfffff800) === 0) {
|
|
2938
|
+
// 2-bytes
|
|
2939
|
+
output[offset++] = ((value >> 6) & 0x1f) | 0xc0;
|
|
2940
|
+
}
|
|
2941
|
+
else {
|
|
2942
|
+
// handle surrogate pair
|
|
2943
|
+
if (value >= 0xd800 && value <= 0xdbff) {
|
|
2944
|
+
// high surrogate
|
|
2945
|
+
if (pos < strLength) {
|
|
2946
|
+
const extra = str.charCodeAt(pos);
|
|
2947
|
+
if ((extra & 0xfc00) === 0xdc00) {
|
|
2948
|
+
++pos;
|
|
2949
|
+
value = ((value & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000;
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2953
|
+
if ((value & 0xffff0000) === 0) {
|
|
2954
|
+
// 3-byte
|
|
2955
|
+
output[offset++] = ((value >> 12) & 0x0f) | 0xe0;
|
|
2956
|
+
output[offset++] = ((value >> 6) & 0x3f) | 0x80;
|
|
2957
|
+
}
|
|
2958
|
+
else {
|
|
2959
|
+
// 4-byte
|
|
2960
|
+
output[offset++] = ((value >> 18) & 0x07) | 0xf0;
|
|
2961
|
+
output[offset++] = ((value >> 12) & 0x3f) | 0x80;
|
|
2962
|
+
output[offset++] = ((value >> 6) & 0x3f) | 0x80;
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
output[offset++] = (value & 0x3f) | 0x80;
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
// TextEncoder and TextDecoder are standardized in whatwg encoding:
|
|
2969
|
+
// https://encoding.spec.whatwg.org/
|
|
2970
|
+
// and available in all the modern browsers:
|
|
2971
|
+
// https://caniuse.com/textencoder
|
|
2972
|
+
// They are available in Node.js since v12 LTS as well:
|
|
2973
|
+
// https://nodejs.org/api/globals.html#textencoder
|
|
2974
|
+
const sharedTextEncoder = new TextEncoder();
|
|
2975
|
+
// This threshold should be determined by benchmarking, which might vary in engines and input data.
|
|
2976
|
+
// Run `npx ts-node benchmark/encode-string.ts` for details.
|
|
2977
|
+
const TEXT_ENCODER_THRESHOLD = 50;
|
|
2978
|
+
function utf8EncodeTE(str, output, outputOffset) {
|
|
2979
|
+
sharedTextEncoder.encodeInto(str, output.subarray(outputOffset));
|
|
2980
|
+
}
|
|
2981
|
+
function utf8Encode(str, output, outputOffset) {
|
|
2982
|
+
if (str.length > TEXT_ENCODER_THRESHOLD) {
|
|
2983
|
+
utf8EncodeTE(str, output, outputOffset);
|
|
2984
|
+
}
|
|
2985
|
+
else {
|
|
2986
|
+
utf8EncodeJs(str, output, outputOffset);
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
const CHUNK_SIZE = 4096;
|
|
2990
|
+
function utf8DecodeJs(bytes, inputOffset, byteLength) {
|
|
2991
|
+
let offset = inputOffset;
|
|
2992
|
+
const end = offset + byteLength;
|
|
2993
|
+
const units = [];
|
|
2994
|
+
let result = "";
|
|
2995
|
+
while (offset < end) {
|
|
2996
|
+
const byte1 = bytes[offset++];
|
|
2997
|
+
if ((byte1 & 0x80) === 0) {
|
|
2998
|
+
// 1 byte
|
|
2999
|
+
units.push(byte1);
|
|
3000
|
+
}
|
|
3001
|
+
else if ((byte1 & 0xe0) === 0xc0) {
|
|
3002
|
+
// 2 bytes
|
|
3003
|
+
const byte2 = bytes[offset++] & 0x3f;
|
|
3004
|
+
units.push(((byte1 & 0x1f) << 6) | byte2);
|
|
3005
|
+
}
|
|
3006
|
+
else if ((byte1 & 0xf0) === 0xe0) {
|
|
3007
|
+
// 3 bytes
|
|
3008
|
+
const byte2 = bytes[offset++] & 0x3f;
|
|
3009
|
+
const byte3 = bytes[offset++] & 0x3f;
|
|
3010
|
+
units.push(((byte1 & 0x1f) << 12) | (byte2 << 6) | byte3);
|
|
3011
|
+
}
|
|
3012
|
+
else if ((byte1 & 0xf8) === 0xf0) {
|
|
3013
|
+
// 4 bytes
|
|
3014
|
+
const byte2 = bytes[offset++] & 0x3f;
|
|
3015
|
+
const byte3 = bytes[offset++] & 0x3f;
|
|
3016
|
+
const byte4 = bytes[offset++] & 0x3f;
|
|
3017
|
+
let unit = ((byte1 & 0x07) << 0x12) | (byte2 << 0x0c) | (byte3 << 0x06) | byte4;
|
|
3018
|
+
if (unit > 0xffff) {
|
|
3019
|
+
unit -= 0x10000;
|
|
3020
|
+
units.push(((unit >>> 10) & 0x3ff) | 0xd800);
|
|
3021
|
+
unit = 0xdc00 | (unit & 0x3ff);
|
|
3022
|
+
}
|
|
3023
|
+
units.push(unit);
|
|
3024
|
+
}
|
|
3025
|
+
else {
|
|
3026
|
+
units.push(byte1);
|
|
3027
|
+
}
|
|
3028
|
+
if (units.length >= CHUNK_SIZE) {
|
|
3029
|
+
result += String.fromCharCode(...units);
|
|
3030
|
+
units.length = 0;
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
if (units.length > 0) {
|
|
3034
|
+
result += String.fromCharCode(...units);
|
|
3035
|
+
}
|
|
3036
|
+
return result;
|
|
3037
|
+
}
|
|
3038
|
+
new TextDecoder();
|
|
3039
|
+
|
|
3040
|
+
/**
|
|
3041
|
+
* ExtData is used to handle Extension Types that are not registered to ExtensionCodec.
|
|
3042
|
+
*/
|
|
3043
|
+
class ExtData {
|
|
3044
|
+
type;
|
|
3045
|
+
data;
|
|
3046
|
+
constructor(type, data) {
|
|
3047
|
+
this.type = type;
|
|
3048
|
+
this.data = data;
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
3051
|
+
|
|
3052
|
+
class DecodeError extends Error {
|
|
3053
|
+
constructor(message) {
|
|
3054
|
+
super(message);
|
|
3055
|
+
// fix the prototype chain in a cross-platform way
|
|
3056
|
+
const proto = Object.create(DecodeError.prototype);
|
|
3057
|
+
Object.setPrototypeOf(this, proto);
|
|
3058
|
+
Object.defineProperty(this, "name", {
|
|
3059
|
+
configurable: true,
|
|
3060
|
+
enumerable: false,
|
|
3061
|
+
value: DecodeError.name,
|
|
3062
|
+
});
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
|
|
3066
|
+
// Integer Utility
|
|
3067
|
+
// DataView extension to handle int64 / uint64,
|
|
3068
|
+
// where the actual range is 53-bits integer (a.k.a. safe integer)
|
|
3069
|
+
function setUint64(view, offset, value) {
|
|
3070
|
+
const high = value / 4294967296;
|
|
3071
|
+
const low = value; // high bits are truncated by DataView
|
|
3072
|
+
view.setUint32(offset, high);
|
|
3073
|
+
view.setUint32(offset + 4, low);
|
|
3074
|
+
}
|
|
3075
|
+
function setInt64(view, offset, value) {
|
|
3076
|
+
const high = Math.floor(value / 4294967296);
|
|
3077
|
+
const low = value; // high bits are truncated by DataView
|
|
3078
|
+
view.setUint32(offset, high);
|
|
3079
|
+
view.setUint32(offset + 4, low);
|
|
3080
|
+
}
|
|
3081
|
+
function getInt64(view, offset) {
|
|
3082
|
+
const high = view.getInt32(offset);
|
|
3083
|
+
const low = view.getUint32(offset + 4);
|
|
3084
|
+
return high * 4294967296 + low;
|
|
3085
|
+
}
|
|
3086
|
+
|
|
3087
|
+
// https://github.com/msgpack/msgpack/blob/master/spec.md#timestamp-extension-type
|
|
3088
|
+
const EXT_TIMESTAMP = -1;
|
|
3089
|
+
const TIMESTAMP32_MAX_SEC = 0x100000000 - 1; // 32-bit unsigned int
|
|
3090
|
+
const TIMESTAMP64_MAX_SEC = 0x400000000 - 1; // 34-bit unsigned int
|
|
3091
|
+
function encodeTimeSpecToTimestamp({ sec, nsec }) {
|
|
3092
|
+
if (sec >= 0 && nsec >= 0 && sec <= TIMESTAMP64_MAX_SEC) {
|
|
3093
|
+
// Here sec >= 0 && nsec >= 0
|
|
3094
|
+
if (nsec === 0 && sec <= TIMESTAMP32_MAX_SEC) {
|
|
3095
|
+
// timestamp 32 = { sec32 (unsigned) }
|
|
3096
|
+
const rv = new Uint8Array(4);
|
|
3097
|
+
const view = new DataView(rv.buffer);
|
|
3098
|
+
view.setUint32(0, sec);
|
|
3099
|
+
return rv;
|
|
3100
|
+
}
|
|
3101
|
+
else {
|
|
3102
|
+
// timestamp 64 = { nsec30 (unsigned), sec34 (unsigned) }
|
|
3103
|
+
const secHigh = sec / 0x100000000;
|
|
3104
|
+
const secLow = sec & 0xffffffff;
|
|
3105
|
+
const rv = new Uint8Array(8);
|
|
3106
|
+
const view = new DataView(rv.buffer);
|
|
3107
|
+
// nsec30 | secHigh2
|
|
3108
|
+
view.setUint32(0, (nsec << 2) | (secHigh & 0x3));
|
|
3109
|
+
// secLow32
|
|
3110
|
+
view.setUint32(4, secLow);
|
|
3111
|
+
return rv;
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
3114
|
+
else {
|
|
3115
|
+
// timestamp 96 = { nsec32 (unsigned), sec64 (signed) }
|
|
3116
|
+
const rv = new Uint8Array(12);
|
|
3117
|
+
const view = new DataView(rv.buffer);
|
|
3118
|
+
view.setUint32(0, nsec);
|
|
3119
|
+
setInt64(view, 4, sec);
|
|
3120
|
+
return rv;
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
function encodeDateToTimeSpec(date) {
|
|
3124
|
+
const msec = date.getTime();
|
|
3125
|
+
const sec = Math.floor(msec / 1e3);
|
|
3126
|
+
const nsec = (msec - sec * 1e3) * 1e6;
|
|
3127
|
+
// Normalizes { sec, nsec } to ensure nsec is unsigned.
|
|
3128
|
+
const nsecInSec = Math.floor(nsec / 1e9);
|
|
3129
|
+
return {
|
|
3130
|
+
sec: sec + nsecInSec,
|
|
3131
|
+
nsec: nsec - nsecInSec * 1e9,
|
|
3132
|
+
};
|
|
3133
|
+
}
|
|
3134
|
+
function encodeTimestampExtension(object) {
|
|
3135
|
+
if (object instanceof Date) {
|
|
3136
|
+
const timeSpec = encodeDateToTimeSpec(object);
|
|
3137
|
+
return encodeTimeSpecToTimestamp(timeSpec);
|
|
3138
|
+
}
|
|
3139
|
+
else {
|
|
3140
|
+
return null;
|
|
3141
|
+
}
|
|
3142
|
+
}
|
|
3143
|
+
function decodeTimestampToTimeSpec(data) {
|
|
3144
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
3145
|
+
// data may be 32, 64, or 96 bits
|
|
3146
|
+
switch (data.byteLength) {
|
|
3147
|
+
case 4: {
|
|
3148
|
+
// timestamp 32 = { sec32 }
|
|
3149
|
+
const sec = view.getUint32(0);
|
|
3150
|
+
const nsec = 0;
|
|
3151
|
+
return { sec, nsec };
|
|
3152
|
+
}
|
|
3153
|
+
case 8: {
|
|
3154
|
+
// timestamp 64 = { nsec30, sec34 }
|
|
3155
|
+
const nsec30AndSecHigh2 = view.getUint32(0);
|
|
3156
|
+
const secLow32 = view.getUint32(4);
|
|
3157
|
+
const sec = (nsec30AndSecHigh2 & 0x3) * 0x100000000 + secLow32;
|
|
3158
|
+
const nsec = nsec30AndSecHigh2 >>> 2;
|
|
3159
|
+
return { sec, nsec };
|
|
3160
|
+
}
|
|
3161
|
+
case 12: {
|
|
3162
|
+
// timestamp 96 = { nsec32 (unsigned), sec64 (signed) }
|
|
3163
|
+
const sec = getInt64(view, 4);
|
|
3164
|
+
const nsec = view.getUint32(0);
|
|
3165
|
+
return { sec, nsec };
|
|
3166
|
+
}
|
|
3167
|
+
default:
|
|
3168
|
+
throw new DecodeError(`Unrecognized data size for timestamp (expected 4, 8, or 12): ${data.length}`);
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
3171
|
+
function decodeTimestampExtension(data) {
|
|
3172
|
+
const timeSpec = decodeTimestampToTimeSpec(data);
|
|
3173
|
+
return new Date(timeSpec.sec * 1e3 + timeSpec.nsec / 1e6);
|
|
3174
|
+
}
|
|
3175
|
+
const timestampExtension = {
|
|
3176
|
+
type: EXT_TIMESTAMP,
|
|
3177
|
+
encode: encodeTimestampExtension,
|
|
3178
|
+
decode: decodeTimestampExtension,
|
|
3179
|
+
};
|
|
3180
|
+
|
|
3181
|
+
// ExtensionCodec to handle MessagePack extensions
|
|
3182
|
+
class ExtensionCodec {
|
|
3183
|
+
static defaultCodec = new ExtensionCodec();
|
|
3184
|
+
// ensures ExtensionCodecType<X> matches ExtensionCodec<X>
|
|
3185
|
+
// this will make type errors a lot more clear
|
|
3186
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
3187
|
+
__brand;
|
|
3188
|
+
// built-in extensions
|
|
3189
|
+
builtInEncoders = [];
|
|
3190
|
+
builtInDecoders = [];
|
|
3191
|
+
// custom extensions
|
|
3192
|
+
encoders = [];
|
|
3193
|
+
decoders = [];
|
|
3194
|
+
constructor() {
|
|
3195
|
+
this.register(timestampExtension);
|
|
3196
|
+
}
|
|
3197
|
+
register({ type, encode, decode, }) {
|
|
3198
|
+
if (type >= 0) {
|
|
3199
|
+
// custom extensions
|
|
3200
|
+
this.encoders[type] = encode;
|
|
3201
|
+
this.decoders[type] = decode;
|
|
3202
|
+
}
|
|
3203
|
+
else {
|
|
3204
|
+
// built-in extensions
|
|
3205
|
+
const index = -1 - type;
|
|
3206
|
+
this.builtInEncoders[index] = encode;
|
|
3207
|
+
this.builtInDecoders[index] = decode;
|
|
3208
|
+
}
|
|
3209
|
+
}
|
|
3210
|
+
tryToEncode(object, context) {
|
|
3211
|
+
// built-in extensions
|
|
3212
|
+
for (let i = 0; i < this.builtInEncoders.length; i++) {
|
|
3213
|
+
const encodeExt = this.builtInEncoders[i];
|
|
3214
|
+
if (encodeExt != null) {
|
|
3215
|
+
const data = encodeExt(object, context);
|
|
3216
|
+
if (data != null) {
|
|
3217
|
+
const type = -1 - i;
|
|
3218
|
+
return new ExtData(type, data);
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
3221
|
+
}
|
|
3222
|
+
// custom extensions
|
|
3223
|
+
for (let i = 0; i < this.encoders.length; i++) {
|
|
3224
|
+
const encodeExt = this.encoders[i];
|
|
3225
|
+
if (encodeExt != null) {
|
|
3226
|
+
const data = encodeExt(object, context);
|
|
3227
|
+
if (data != null) {
|
|
3228
|
+
const type = i;
|
|
3229
|
+
return new ExtData(type, data);
|
|
3230
|
+
}
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
3233
|
+
if (object instanceof ExtData) {
|
|
3234
|
+
// to keep ExtData as is
|
|
3235
|
+
return object;
|
|
3236
|
+
}
|
|
3237
|
+
return null;
|
|
3238
|
+
}
|
|
3239
|
+
decode(data, type, context) {
|
|
3240
|
+
const decodeExt = type < 0 ? this.builtInDecoders[-1 - type] : this.decoders[type];
|
|
3241
|
+
if (decodeExt) {
|
|
3242
|
+
return decodeExt(data, type, context);
|
|
3243
|
+
}
|
|
3244
|
+
else {
|
|
3245
|
+
// decode() does not fail, returns ExtData instead.
|
|
3246
|
+
return new ExtData(type, data);
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
|
|
3251
|
+
function isArrayBufferLike(buffer) {
|
|
3252
|
+
return (buffer instanceof ArrayBuffer || (typeof SharedArrayBuffer !== "undefined" && buffer instanceof SharedArrayBuffer));
|
|
3253
|
+
}
|
|
3254
|
+
function ensureUint8Array(buffer) {
|
|
3255
|
+
if (buffer instanceof Uint8Array) {
|
|
3256
|
+
return buffer;
|
|
3257
|
+
}
|
|
3258
|
+
else if (ArrayBuffer.isView(buffer)) {
|
|
3259
|
+
return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
|
3260
|
+
}
|
|
3261
|
+
else if (isArrayBufferLike(buffer)) {
|
|
3262
|
+
return new Uint8Array(buffer);
|
|
3263
|
+
}
|
|
3264
|
+
else {
|
|
3265
|
+
// ArrayLike<number>
|
|
3266
|
+
return Uint8Array.from(buffer);
|
|
3267
|
+
}
|
|
3268
|
+
}
|
|
3269
|
+
|
|
3270
|
+
const DEFAULT_MAX_DEPTH = 100;
|
|
3271
|
+
const DEFAULT_INITIAL_BUFFER_SIZE = 2048;
|
|
3272
|
+
class Encoder {
|
|
3273
|
+
extensionCodec;
|
|
3274
|
+
context;
|
|
3275
|
+
useBigInt64;
|
|
3276
|
+
maxDepth;
|
|
3277
|
+
initialBufferSize;
|
|
3278
|
+
sortKeys;
|
|
3279
|
+
forceFloat32;
|
|
3280
|
+
ignoreUndefined;
|
|
3281
|
+
forceIntegerToFloat;
|
|
3282
|
+
pos;
|
|
3283
|
+
view;
|
|
3284
|
+
bytes;
|
|
3285
|
+
entered = false;
|
|
3286
|
+
constructor(options) {
|
|
3287
|
+
this.extensionCodec = options?.extensionCodec ?? ExtensionCodec.defaultCodec;
|
|
3288
|
+
this.context = options?.context; // needs a type assertion because EncoderOptions has no context property when ContextType is undefined
|
|
3289
|
+
this.useBigInt64 = options?.useBigInt64 ?? false;
|
|
3290
|
+
this.maxDepth = options?.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
3291
|
+
this.initialBufferSize = options?.initialBufferSize ?? DEFAULT_INITIAL_BUFFER_SIZE;
|
|
3292
|
+
this.sortKeys = options?.sortKeys ?? false;
|
|
3293
|
+
this.forceFloat32 = options?.forceFloat32 ?? false;
|
|
3294
|
+
this.ignoreUndefined = options?.ignoreUndefined ?? false;
|
|
3295
|
+
this.forceIntegerToFloat = options?.forceIntegerToFloat ?? false;
|
|
3296
|
+
this.pos = 0;
|
|
3297
|
+
this.view = new DataView(new ArrayBuffer(this.initialBufferSize));
|
|
3298
|
+
this.bytes = new Uint8Array(this.view.buffer);
|
|
3299
|
+
}
|
|
3300
|
+
clone() {
|
|
3301
|
+
// Because of slightly special argument `context`,
|
|
3302
|
+
// type assertion is needed.
|
|
3303
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
3304
|
+
return new Encoder({
|
|
3305
|
+
extensionCodec: this.extensionCodec,
|
|
3306
|
+
context: this.context,
|
|
3307
|
+
useBigInt64: this.useBigInt64,
|
|
3308
|
+
maxDepth: this.maxDepth,
|
|
3309
|
+
initialBufferSize: this.initialBufferSize,
|
|
3310
|
+
sortKeys: this.sortKeys,
|
|
3311
|
+
forceFloat32: this.forceFloat32,
|
|
3312
|
+
ignoreUndefined: this.ignoreUndefined,
|
|
3313
|
+
forceIntegerToFloat: this.forceIntegerToFloat,
|
|
3314
|
+
});
|
|
3315
|
+
}
|
|
3316
|
+
reinitializeState() {
|
|
3317
|
+
this.pos = 0;
|
|
3318
|
+
}
|
|
3319
|
+
/**
|
|
3320
|
+
* This is almost equivalent to {@link Encoder#encode}, but it returns an reference of the encoder's internal buffer and thus much faster than {@link Encoder#encode}.
|
|
3321
|
+
*
|
|
3322
|
+
* @returns Encodes the object and returns a shared reference the encoder's internal buffer.
|
|
3323
|
+
*/
|
|
3324
|
+
encodeSharedRef(object) {
|
|
3325
|
+
if (this.entered) {
|
|
3326
|
+
const instance = this.clone();
|
|
3327
|
+
return instance.encodeSharedRef(object);
|
|
3328
|
+
}
|
|
3329
|
+
try {
|
|
3330
|
+
this.entered = true;
|
|
3331
|
+
this.reinitializeState();
|
|
3332
|
+
this.doEncode(object, 1);
|
|
3333
|
+
return this.bytes.subarray(0, this.pos);
|
|
3334
|
+
}
|
|
3335
|
+
finally {
|
|
3336
|
+
this.entered = false;
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
/**
|
|
3340
|
+
* @returns Encodes the object and returns a copy of the encoder's internal buffer.
|
|
3341
|
+
*/
|
|
3342
|
+
encode(object) {
|
|
3343
|
+
if (this.entered) {
|
|
3344
|
+
const instance = this.clone();
|
|
3345
|
+
return instance.encode(object);
|
|
3346
|
+
}
|
|
3347
|
+
try {
|
|
3348
|
+
this.entered = true;
|
|
3349
|
+
this.reinitializeState();
|
|
3350
|
+
this.doEncode(object, 1);
|
|
3351
|
+
return this.bytes.slice(0, this.pos);
|
|
3352
|
+
}
|
|
3353
|
+
finally {
|
|
3354
|
+
this.entered = false;
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
doEncode(object, depth) {
|
|
3358
|
+
if (depth > this.maxDepth) {
|
|
3359
|
+
throw new Error(`Too deep objects in depth ${depth}`);
|
|
3360
|
+
}
|
|
3361
|
+
if (object == null) {
|
|
3362
|
+
this.encodeNil();
|
|
3363
|
+
}
|
|
3364
|
+
else if (typeof object === "boolean") {
|
|
3365
|
+
this.encodeBoolean(object);
|
|
3366
|
+
}
|
|
3367
|
+
else if (typeof object === "number") {
|
|
3368
|
+
if (!this.forceIntegerToFloat) {
|
|
3369
|
+
this.encodeNumber(object);
|
|
3370
|
+
}
|
|
3371
|
+
else {
|
|
3372
|
+
this.encodeNumberAsFloat(object);
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
3375
|
+
else if (typeof object === "string") {
|
|
3376
|
+
this.encodeString(object);
|
|
3377
|
+
}
|
|
3378
|
+
else if (this.useBigInt64 && typeof object === "bigint") {
|
|
3379
|
+
this.encodeBigInt64(object);
|
|
3380
|
+
}
|
|
3381
|
+
else {
|
|
3382
|
+
this.encodeObject(object, depth);
|
|
3383
|
+
}
|
|
3384
|
+
}
|
|
3385
|
+
ensureBufferSizeToWrite(sizeToWrite) {
|
|
3386
|
+
const requiredSize = this.pos + sizeToWrite;
|
|
3387
|
+
if (this.view.byteLength < requiredSize) {
|
|
3388
|
+
this.resizeBuffer(requiredSize * 2);
|
|
3389
|
+
}
|
|
3390
|
+
}
|
|
3391
|
+
resizeBuffer(newSize) {
|
|
3392
|
+
const newBuffer = new ArrayBuffer(newSize);
|
|
3393
|
+
const newBytes = new Uint8Array(newBuffer);
|
|
3394
|
+
const newView = new DataView(newBuffer);
|
|
3395
|
+
newBytes.set(this.bytes);
|
|
3396
|
+
this.view = newView;
|
|
3397
|
+
this.bytes = newBytes;
|
|
3398
|
+
}
|
|
3399
|
+
encodeNil() {
|
|
3400
|
+
this.writeU8(0xc0);
|
|
3401
|
+
}
|
|
3402
|
+
encodeBoolean(object) {
|
|
3403
|
+
if (object === false) {
|
|
3404
|
+
this.writeU8(0xc2);
|
|
3405
|
+
}
|
|
3406
|
+
else {
|
|
3407
|
+
this.writeU8(0xc3);
|
|
3408
|
+
}
|
|
3409
|
+
}
|
|
3410
|
+
encodeNumber(object) {
|
|
3411
|
+
if (!this.forceIntegerToFloat && Number.isSafeInteger(object)) {
|
|
3412
|
+
if (object >= 0) {
|
|
3413
|
+
if (object < 0x80) {
|
|
3414
|
+
// positive fixint
|
|
3415
|
+
this.writeU8(object);
|
|
3416
|
+
}
|
|
3417
|
+
else if (object < 0x100) {
|
|
3418
|
+
// uint 8
|
|
3419
|
+
this.writeU8(0xcc);
|
|
3420
|
+
this.writeU8(object);
|
|
3421
|
+
}
|
|
3422
|
+
else if (object < 0x10000) {
|
|
3423
|
+
// uint 16
|
|
3424
|
+
this.writeU8(0xcd);
|
|
3425
|
+
this.writeU16(object);
|
|
3426
|
+
}
|
|
3427
|
+
else if (object < 0x100000000) {
|
|
3428
|
+
// uint 32
|
|
3429
|
+
this.writeU8(0xce);
|
|
3430
|
+
this.writeU32(object);
|
|
3431
|
+
}
|
|
3432
|
+
else if (!this.useBigInt64) {
|
|
3433
|
+
// uint 64
|
|
3434
|
+
this.writeU8(0xcf);
|
|
3435
|
+
this.writeU64(object);
|
|
3436
|
+
}
|
|
3437
|
+
else {
|
|
3438
|
+
this.encodeNumberAsFloat(object);
|
|
3439
|
+
}
|
|
3440
|
+
}
|
|
3441
|
+
else {
|
|
3442
|
+
if (object >= -32) {
|
|
3443
|
+
// negative fixint
|
|
3444
|
+
this.writeU8(0xe0 | (object + 0x20));
|
|
3445
|
+
}
|
|
3446
|
+
else if (object >= -128) {
|
|
3447
|
+
// int 8
|
|
3448
|
+
this.writeU8(0xd0);
|
|
3449
|
+
this.writeI8(object);
|
|
3450
|
+
}
|
|
3451
|
+
else if (object >= -32768) {
|
|
3452
|
+
// int 16
|
|
3453
|
+
this.writeU8(0xd1);
|
|
3454
|
+
this.writeI16(object);
|
|
3455
|
+
}
|
|
3456
|
+
else if (object >= -2147483648) {
|
|
3457
|
+
// int 32
|
|
3458
|
+
this.writeU8(0xd2);
|
|
3459
|
+
this.writeI32(object);
|
|
3460
|
+
}
|
|
3461
|
+
else if (!this.useBigInt64) {
|
|
3462
|
+
// int 64
|
|
3463
|
+
this.writeU8(0xd3);
|
|
3464
|
+
this.writeI64(object);
|
|
3465
|
+
}
|
|
3466
|
+
else {
|
|
3467
|
+
this.encodeNumberAsFloat(object);
|
|
3468
|
+
}
|
|
3469
|
+
}
|
|
3470
|
+
}
|
|
3471
|
+
else {
|
|
3472
|
+
this.encodeNumberAsFloat(object);
|
|
3473
|
+
}
|
|
3474
|
+
}
|
|
3475
|
+
encodeNumberAsFloat(object) {
|
|
3476
|
+
if (this.forceFloat32) {
|
|
3477
|
+
// float 32
|
|
3478
|
+
this.writeU8(0xca);
|
|
3479
|
+
this.writeF32(object);
|
|
3480
|
+
}
|
|
3481
|
+
else {
|
|
3482
|
+
// float 64
|
|
3483
|
+
this.writeU8(0xcb);
|
|
3484
|
+
this.writeF64(object);
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3487
|
+
encodeBigInt64(object) {
|
|
3488
|
+
if (object >= BigInt(0)) {
|
|
3489
|
+
// uint 64
|
|
3490
|
+
this.writeU8(0xcf);
|
|
3491
|
+
this.writeBigUint64(object);
|
|
3492
|
+
}
|
|
3493
|
+
else {
|
|
3494
|
+
// int 64
|
|
3495
|
+
this.writeU8(0xd3);
|
|
3496
|
+
this.writeBigInt64(object);
|
|
3497
|
+
}
|
|
3498
|
+
}
|
|
3499
|
+
writeStringHeader(byteLength) {
|
|
3500
|
+
if (byteLength < 32) {
|
|
3501
|
+
// fixstr
|
|
3502
|
+
this.writeU8(0xa0 + byteLength);
|
|
3503
|
+
}
|
|
3504
|
+
else if (byteLength < 0x100) {
|
|
3505
|
+
// str 8
|
|
3506
|
+
this.writeU8(0xd9);
|
|
3507
|
+
this.writeU8(byteLength);
|
|
3508
|
+
}
|
|
3509
|
+
else if (byteLength < 0x10000) {
|
|
3510
|
+
// str 16
|
|
3511
|
+
this.writeU8(0xda);
|
|
3512
|
+
this.writeU16(byteLength);
|
|
3513
|
+
}
|
|
3514
|
+
else if (byteLength < 0x100000000) {
|
|
3515
|
+
// str 32
|
|
3516
|
+
this.writeU8(0xdb);
|
|
3517
|
+
this.writeU32(byteLength);
|
|
3518
|
+
}
|
|
3519
|
+
else {
|
|
3520
|
+
throw new Error(`Too long string: ${byteLength} bytes in UTF-8`);
|
|
3521
|
+
}
|
|
3522
|
+
}
|
|
3523
|
+
encodeString(object) {
|
|
3524
|
+
const maxHeaderSize = 1 + 4;
|
|
3525
|
+
const byteLength = utf8Count(object);
|
|
3526
|
+
this.ensureBufferSizeToWrite(maxHeaderSize + byteLength);
|
|
3527
|
+
this.writeStringHeader(byteLength);
|
|
3528
|
+
utf8Encode(object, this.bytes, this.pos);
|
|
3529
|
+
this.pos += byteLength;
|
|
3530
|
+
}
|
|
3531
|
+
encodeObject(object, depth) {
|
|
3532
|
+
// try to encode objects with custom codec first of non-primitives
|
|
3533
|
+
const ext = this.extensionCodec.tryToEncode(object, this.context);
|
|
3534
|
+
if (ext != null) {
|
|
3535
|
+
this.encodeExtension(ext);
|
|
3536
|
+
}
|
|
3537
|
+
else if (Array.isArray(object)) {
|
|
3538
|
+
this.encodeArray(object, depth);
|
|
3539
|
+
}
|
|
3540
|
+
else if (ArrayBuffer.isView(object)) {
|
|
3541
|
+
this.encodeBinary(object);
|
|
3542
|
+
}
|
|
3543
|
+
else if (typeof object === "object") {
|
|
3544
|
+
this.encodeMap(object, depth);
|
|
3545
|
+
}
|
|
3546
|
+
else {
|
|
3547
|
+
// symbol, function and other special object come here unless extensionCodec handles them.
|
|
3548
|
+
throw new Error(`Unrecognized object: ${Object.prototype.toString.apply(object)}`);
|
|
3549
|
+
}
|
|
3550
|
+
}
|
|
3551
|
+
encodeBinary(object) {
|
|
3552
|
+
const size = object.byteLength;
|
|
3553
|
+
if (size < 0x100) {
|
|
3554
|
+
// bin 8
|
|
3555
|
+
this.writeU8(0xc4);
|
|
3556
|
+
this.writeU8(size);
|
|
3557
|
+
}
|
|
3558
|
+
else if (size < 0x10000) {
|
|
3559
|
+
// bin 16
|
|
3560
|
+
this.writeU8(0xc5);
|
|
3561
|
+
this.writeU16(size);
|
|
3562
|
+
}
|
|
3563
|
+
else if (size < 0x100000000) {
|
|
3564
|
+
// bin 32
|
|
3565
|
+
this.writeU8(0xc6);
|
|
3566
|
+
this.writeU32(size);
|
|
3567
|
+
}
|
|
3568
|
+
else {
|
|
3569
|
+
throw new Error(`Too large binary: ${size}`);
|
|
3570
|
+
}
|
|
3571
|
+
const bytes = ensureUint8Array(object);
|
|
3572
|
+
this.writeU8a(bytes);
|
|
3573
|
+
}
|
|
3574
|
+
encodeArray(object, depth) {
|
|
3575
|
+
const size = object.length;
|
|
3576
|
+
if (size < 16) {
|
|
3577
|
+
// fixarray
|
|
3578
|
+
this.writeU8(0x90 + size);
|
|
3579
|
+
}
|
|
3580
|
+
else if (size < 0x10000) {
|
|
3581
|
+
// array 16
|
|
3582
|
+
this.writeU8(0xdc);
|
|
3583
|
+
this.writeU16(size);
|
|
3584
|
+
}
|
|
3585
|
+
else if (size < 0x100000000) {
|
|
3586
|
+
// array 32
|
|
3587
|
+
this.writeU8(0xdd);
|
|
3588
|
+
this.writeU32(size);
|
|
3589
|
+
}
|
|
3590
|
+
else {
|
|
3591
|
+
throw new Error(`Too large array: ${size}`);
|
|
3592
|
+
}
|
|
3593
|
+
for (const item of object) {
|
|
3594
|
+
this.doEncode(item, depth + 1);
|
|
3595
|
+
}
|
|
3596
|
+
}
|
|
3597
|
+
countWithoutUndefined(object, keys) {
|
|
3598
|
+
let count = 0;
|
|
3599
|
+
for (const key of keys) {
|
|
3600
|
+
if (object[key] !== undefined) {
|
|
3601
|
+
count++;
|
|
3602
|
+
}
|
|
3603
|
+
}
|
|
3604
|
+
return count;
|
|
3605
|
+
}
|
|
3606
|
+
encodeMap(object, depth) {
|
|
3607
|
+
const keys = Object.keys(object);
|
|
3608
|
+
if (this.sortKeys) {
|
|
3609
|
+
keys.sort();
|
|
3610
|
+
}
|
|
3611
|
+
const size = this.ignoreUndefined ? this.countWithoutUndefined(object, keys) : keys.length;
|
|
3612
|
+
if (size < 16) {
|
|
3613
|
+
// fixmap
|
|
3614
|
+
this.writeU8(0x80 + size);
|
|
3615
|
+
}
|
|
3616
|
+
else if (size < 0x10000) {
|
|
3617
|
+
// map 16
|
|
3618
|
+
this.writeU8(0xde);
|
|
3619
|
+
this.writeU16(size);
|
|
3620
|
+
}
|
|
3621
|
+
else if (size < 0x100000000) {
|
|
3622
|
+
// map 32
|
|
3623
|
+
this.writeU8(0xdf);
|
|
3624
|
+
this.writeU32(size);
|
|
3625
|
+
}
|
|
3626
|
+
else {
|
|
3627
|
+
throw new Error(`Too large map object: ${size}`);
|
|
3628
|
+
}
|
|
3629
|
+
for (const key of keys) {
|
|
3630
|
+
const value = object[key];
|
|
3631
|
+
if (!(this.ignoreUndefined && value === undefined)) {
|
|
3632
|
+
this.encodeString(key);
|
|
3633
|
+
this.doEncode(value, depth + 1);
|
|
3634
|
+
}
|
|
3635
|
+
}
|
|
3636
|
+
}
|
|
3637
|
+
encodeExtension(ext) {
|
|
3638
|
+
if (typeof ext.data === "function") {
|
|
3639
|
+
const data = ext.data(this.pos + 6);
|
|
3640
|
+
const size = data.length;
|
|
3641
|
+
if (size >= 0x100000000) {
|
|
3642
|
+
throw new Error(`Too large extension object: ${size}`);
|
|
3643
|
+
}
|
|
3644
|
+
this.writeU8(0xc9);
|
|
3645
|
+
this.writeU32(size);
|
|
3646
|
+
this.writeI8(ext.type);
|
|
3647
|
+
this.writeU8a(data);
|
|
3648
|
+
return;
|
|
3649
|
+
}
|
|
3650
|
+
const size = ext.data.length;
|
|
3651
|
+
if (size === 1) {
|
|
3652
|
+
// fixext 1
|
|
3653
|
+
this.writeU8(0xd4);
|
|
3654
|
+
}
|
|
3655
|
+
else if (size === 2) {
|
|
3656
|
+
// fixext 2
|
|
3657
|
+
this.writeU8(0xd5);
|
|
3658
|
+
}
|
|
3659
|
+
else if (size === 4) {
|
|
3660
|
+
// fixext 4
|
|
3661
|
+
this.writeU8(0xd6);
|
|
3662
|
+
}
|
|
3663
|
+
else if (size === 8) {
|
|
3664
|
+
// fixext 8
|
|
3665
|
+
this.writeU8(0xd7);
|
|
3666
|
+
}
|
|
3667
|
+
else if (size === 16) {
|
|
3668
|
+
// fixext 16
|
|
3669
|
+
this.writeU8(0xd8);
|
|
3670
|
+
}
|
|
3671
|
+
else if (size < 0x100) {
|
|
3672
|
+
// ext 8
|
|
3673
|
+
this.writeU8(0xc7);
|
|
3674
|
+
this.writeU8(size);
|
|
3675
|
+
}
|
|
3676
|
+
else if (size < 0x10000) {
|
|
3677
|
+
// ext 16
|
|
3678
|
+
this.writeU8(0xc8);
|
|
3679
|
+
this.writeU16(size);
|
|
3680
|
+
}
|
|
3681
|
+
else if (size < 0x100000000) {
|
|
3682
|
+
// ext 32
|
|
3683
|
+
this.writeU8(0xc9);
|
|
3684
|
+
this.writeU32(size);
|
|
3685
|
+
}
|
|
3686
|
+
else {
|
|
3687
|
+
throw new Error(`Too large extension object: ${size}`);
|
|
3688
|
+
}
|
|
3689
|
+
this.writeI8(ext.type);
|
|
3690
|
+
this.writeU8a(ext.data);
|
|
3691
|
+
}
|
|
3692
|
+
writeU8(value) {
|
|
3693
|
+
this.ensureBufferSizeToWrite(1);
|
|
3694
|
+
this.view.setUint8(this.pos, value);
|
|
3695
|
+
this.pos++;
|
|
3696
|
+
}
|
|
3697
|
+
writeU8a(values) {
|
|
3698
|
+
const size = values.length;
|
|
3699
|
+
this.ensureBufferSizeToWrite(size);
|
|
3700
|
+
this.bytes.set(values, this.pos);
|
|
3701
|
+
this.pos += size;
|
|
3702
|
+
}
|
|
3703
|
+
writeI8(value) {
|
|
3704
|
+
this.ensureBufferSizeToWrite(1);
|
|
3705
|
+
this.view.setInt8(this.pos, value);
|
|
3706
|
+
this.pos++;
|
|
3707
|
+
}
|
|
3708
|
+
writeU16(value) {
|
|
3709
|
+
this.ensureBufferSizeToWrite(2);
|
|
3710
|
+
this.view.setUint16(this.pos, value);
|
|
3711
|
+
this.pos += 2;
|
|
3712
|
+
}
|
|
3713
|
+
writeI16(value) {
|
|
3714
|
+
this.ensureBufferSizeToWrite(2);
|
|
3715
|
+
this.view.setInt16(this.pos, value);
|
|
3716
|
+
this.pos += 2;
|
|
3717
|
+
}
|
|
3718
|
+
writeU32(value) {
|
|
3719
|
+
this.ensureBufferSizeToWrite(4);
|
|
3720
|
+
this.view.setUint32(this.pos, value);
|
|
3721
|
+
this.pos += 4;
|
|
3722
|
+
}
|
|
3723
|
+
writeI32(value) {
|
|
3724
|
+
this.ensureBufferSizeToWrite(4);
|
|
3725
|
+
this.view.setInt32(this.pos, value);
|
|
3726
|
+
this.pos += 4;
|
|
3727
|
+
}
|
|
3728
|
+
writeF32(value) {
|
|
3729
|
+
this.ensureBufferSizeToWrite(4);
|
|
3730
|
+
this.view.setFloat32(this.pos, value);
|
|
3731
|
+
this.pos += 4;
|
|
3732
|
+
}
|
|
3733
|
+
writeF64(value) {
|
|
3734
|
+
this.ensureBufferSizeToWrite(8);
|
|
3735
|
+
this.view.setFloat64(this.pos, value);
|
|
3736
|
+
this.pos += 8;
|
|
3737
|
+
}
|
|
3738
|
+
writeU64(value) {
|
|
3739
|
+
this.ensureBufferSizeToWrite(8);
|
|
3740
|
+
setUint64(this.view, this.pos, value);
|
|
3741
|
+
this.pos += 8;
|
|
3742
|
+
}
|
|
3743
|
+
writeI64(value) {
|
|
3744
|
+
this.ensureBufferSizeToWrite(8);
|
|
3745
|
+
setInt64(this.view, this.pos, value);
|
|
3746
|
+
this.pos += 8;
|
|
3747
|
+
}
|
|
3748
|
+
writeBigUint64(value) {
|
|
3749
|
+
this.ensureBufferSizeToWrite(8);
|
|
3750
|
+
this.view.setBigUint64(this.pos, value);
|
|
3751
|
+
this.pos += 8;
|
|
3752
|
+
}
|
|
3753
|
+
writeBigInt64(value) {
|
|
3754
|
+
this.ensureBufferSizeToWrite(8);
|
|
3755
|
+
this.view.setBigInt64(this.pos, value);
|
|
3756
|
+
this.pos += 8;
|
|
3757
|
+
}
|
|
3758
|
+
}
|
|
3759
|
+
|
|
3760
|
+
/**
|
|
3761
|
+
* It encodes `value` in the MessagePack format and
|
|
3762
|
+
* returns a byte buffer.
|
|
3763
|
+
*
|
|
3764
|
+
* The returned buffer is a slice of a larger `ArrayBuffer`, so you have to use its `#byteOffset` and `#byteLength` in order to convert it to another typed arrays including NodeJS `Buffer`.
|
|
3765
|
+
*/
|
|
3766
|
+
function encode(value, options) {
|
|
3767
|
+
const encoder = new Encoder(options);
|
|
3768
|
+
return encoder.encodeSharedRef(value);
|
|
3769
|
+
}
|
|
3770
|
+
|
|
3771
|
+
const DEFAULT_MAX_KEY_LENGTH = 16;
|
|
3772
|
+
const DEFAULT_MAX_LENGTH_PER_KEY = 16;
|
|
3773
|
+
class CachedKeyDecoder {
|
|
3774
|
+
hit = 0;
|
|
3775
|
+
miss = 0;
|
|
3776
|
+
caches;
|
|
3777
|
+
maxKeyLength;
|
|
3778
|
+
maxLengthPerKey;
|
|
3779
|
+
constructor(maxKeyLength = DEFAULT_MAX_KEY_LENGTH, maxLengthPerKey = DEFAULT_MAX_LENGTH_PER_KEY) {
|
|
3780
|
+
this.maxKeyLength = maxKeyLength;
|
|
3781
|
+
this.maxLengthPerKey = maxLengthPerKey;
|
|
3782
|
+
// avoid `new Array(N)`, which makes a sparse array,
|
|
3783
|
+
// because a sparse array is typically slower than a non-sparse array.
|
|
3784
|
+
this.caches = [];
|
|
3785
|
+
for (let i = 0; i < this.maxKeyLength; i++) {
|
|
3786
|
+
this.caches.push([]);
|
|
3787
|
+
}
|
|
3788
|
+
}
|
|
3789
|
+
canBeCached(byteLength) {
|
|
3790
|
+
return byteLength > 0 && byteLength <= this.maxKeyLength;
|
|
3791
|
+
}
|
|
3792
|
+
find(bytes, inputOffset, byteLength) {
|
|
3793
|
+
const records = this.caches[byteLength - 1];
|
|
3794
|
+
FIND_CHUNK: for (const record of records) {
|
|
3795
|
+
const recordBytes = record.bytes;
|
|
3796
|
+
for (let j = 0; j < byteLength; j++) {
|
|
3797
|
+
if (recordBytes[j] !== bytes[inputOffset + j]) {
|
|
3798
|
+
continue FIND_CHUNK;
|
|
3799
|
+
}
|
|
3800
|
+
}
|
|
3801
|
+
return record.str;
|
|
3802
|
+
}
|
|
3803
|
+
return null;
|
|
3804
|
+
}
|
|
3805
|
+
store(bytes, value) {
|
|
3806
|
+
const records = this.caches[bytes.length - 1];
|
|
3807
|
+
const record = { bytes, str: value };
|
|
3808
|
+
if (records.length >= this.maxLengthPerKey) {
|
|
3809
|
+
// `records` are full!
|
|
3810
|
+
// Set `record` to an arbitrary position.
|
|
3811
|
+
records[(Math.random() * records.length) | 0] = record;
|
|
3812
|
+
}
|
|
3813
|
+
else {
|
|
3814
|
+
records.push(record);
|
|
3815
|
+
}
|
|
3816
|
+
}
|
|
3817
|
+
decode(bytes, inputOffset, byteLength) {
|
|
3818
|
+
const cachedValue = this.find(bytes, inputOffset, byteLength);
|
|
3819
|
+
if (cachedValue != null) {
|
|
3820
|
+
this.hit++;
|
|
3821
|
+
return cachedValue;
|
|
3822
|
+
}
|
|
3823
|
+
this.miss++;
|
|
3824
|
+
const str = utf8DecodeJs(bytes, inputOffset, byteLength);
|
|
3825
|
+
// Ensure to copy a slice of bytes because the bytes may be a NodeJS Buffer and Buffer#slice() returns a reference to its internal ArrayBuffer.
|
|
3826
|
+
const slicedCopyOfBytes = Uint8Array.prototype.slice.call(bytes, inputOffset, inputOffset + byteLength);
|
|
3827
|
+
this.store(slicedCopyOfBytes, str);
|
|
3828
|
+
return str;
|
|
3829
|
+
}
|
|
3830
|
+
}
|
|
3831
|
+
|
|
3832
|
+
const EMPTY_VIEW = new DataView(new ArrayBuffer(0));
|
|
3833
|
+
new Uint8Array(EMPTY_VIEW.buffer);
|
|
3834
|
+
try {
|
|
3835
|
+
// IE11: The spec says it should throw RangeError,
|
|
3836
|
+
// IE11: but in IE11 it throws TypeError.
|
|
3837
|
+
EMPTY_VIEW.getInt8(0);
|
|
3838
|
+
}
|
|
3839
|
+
catch (e) {
|
|
3840
|
+
if (!(e instanceof RangeError)) {
|
|
3841
|
+
throw new Error("This module is not supported in the current JavaScript engine because DataView does not throw RangeError on out-of-bounds access");
|
|
3842
|
+
}
|
|
3843
|
+
}
|
|
3844
|
+
new CachedKeyDecoder();
|
|
3845
|
+
|
|
3846
|
+
function encodeMiniProgramShareExtra(value) {
|
|
3847
|
+
assertExtraValue(value);
|
|
3848
|
+
const bytes = encode(value);
|
|
3849
|
+
if (bytes.byteLength > 128)
|
|
3850
|
+
throw new Error('share extra is too large');
|
|
3851
|
+
return btoa(String.fromCharCode(...bytes))
|
|
3852
|
+
.replaceAll('+', '-')
|
|
3853
|
+
.replaceAll('/', '_')
|
|
3854
|
+
.replaceAll('=', '');
|
|
3855
|
+
}
|
|
3856
|
+
function assertExtraValue(value, depth = 0, seen = new WeakSet()) {
|
|
3857
|
+
if (depth > 8)
|
|
3858
|
+
throw new Error('extra nesting depth exceeded');
|
|
3859
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean')
|
|
3860
|
+
return;
|
|
3861
|
+
if (typeof value === 'number') {
|
|
3862
|
+
if (Number.isFinite(value))
|
|
3863
|
+
return;
|
|
3864
|
+
throw new Error('extra number must be finite');
|
|
3865
|
+
}
|
|
3866
|
+
if (typeof value !== 'object')
|
|
3867
|
+
throw new Error('extra value must be JSON-compatible');
|
|
3868
|
+
if (seen.has(value))
|
|
3869
|
+
throw new Error('extra value must not contain cycles');
|
|
3870
|
+
seen.add(value);
|
|
3871
|
+
if (Array.isArray(value)) {
|
|
3872
|
+
if (value.length > 64)
|
|
3873
|
+
throw new Error('extra array is too large');
|
|
3874
|
+
for (const item of value)
|
|
3875
|
+
assertExtraValue(item, depth + 1, seen);
|
|
3876
|
+
seen.delete(value);
|
|
3877
|
+
return;
|
|
3878
|
+
}
|
|
3879
|
+
const prototype = Object.getPrototypeOf(value);
|
|
3880
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
3881
|
+
throw new Error('extra object must be a plain object');
|
|
3882
|
+
const values = Object.values(value);
|
|
3883
|
+
if (values.length > 64)
|
|
3884
|
+
throw new Error('extra object has too many fields');
|
|
3885
|
+
for (const item of values)
|
|
3886
|
+
assertExtraValue(item, depth + 1, seen);
|
|
3887
|
+
seen.delete(value);
|
|
3888
|
+
}
|
|
3889
|
+
|
|
2883
3890
|
function isRecord$3(value) {
|
|
2884
3891
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
2885
3892
|
}
|
|
@@ -3003,11 +4010,11 @@ function readHttpUrl(value, message) {
|
|
|
3003
4010
|
function isShareChannel(value) {
|
|
3004
4011
|
return value === 'wechatSession' || value === 'wechatTimeline' || value === 'qqFriend' || value === 'qzone' || value === 'weibo';
|
|
3005
4012
|
}
|
|
3006
|
-
function createDefaultShareUrl(options) {
|
|
4013
|
+
function createDefaultShareUrl(options, method, extra) {
|
|
3007
4014
|
const identity = options.identity.get();
|
|
3008
4015
|
const miniProgramId = identity?.miniProgramId.trim();
|
|
3009
4016
|
if (!miniProgramId)
|
|
3010
|
-
return invalidParams$2(
|
|
4017
|
+
return invalidParams$2(`${method} 缺少有效的小程序分享地址`);
|
|
3011
4018
|
try {
|
|
3012
4019
|
const currentUrl = new URL(options.environment.getCurrentHref());
|
|
3013
4020
|
const shareUrl = new URL('/tools/common_share', currentUrl);
|
|
@@ -3018,10 +4025,12 @@ function createDefaultShareUrl(options) {
|
|
|
3018
4025
|
const hSrc = currentUrl.searchParams.get('h_src');
|
|
3019
4026
|
if (hSrc?.trim())
|
|
3020
4027
|
shareUrl.searchParams.set('h_src', hSrc);
|
|
4028
|
+
if (extra !== undefined)
|
|
4029
|
+
shareUrl.searchParams.set('extra', extra);
|
|
3021
4030
|
return shareUrl.href;
|
|
3022
4031
|
}
|
|
3023
4032
|
catch {
|
|
3024
|
-
return invalidParams$2(
|
|
4033
|
+
return invalidParams$2(`${method} 缺少有效的小程序分享地址`);
|
|
3025
4034
|
}
|
|
3026
4035
|
}
|
|
3027
4036
|
function readShareMenuInput(payload, options) {
|
|
@@ -3030,15 +4039,18 @@ function readShareMenuInput(payload, options) {
|
|
|
3030
4039
|
const channel = payload.channel;
|
|
3031
4040
|
if (channel !== undefined && !isShareChannel(channel))
|
|
3032
4041
|
return invalidParams$2('share.showShareMenu channel 不合法');
|
|
4042
|
+
const extra = readShareExtra(payload.extra, 'share.showShareMenu');
|
|
3033
4043
|
let url;
|
|
3034
4044
|
if (typeof payload.url === 'string' && payload.url.trim()) {
|
|
4045
|
+
if (extra !== undefined)
|
|
4046
|
+
return invalidParams$2('share.showShareMenu url 与 extra 不能同时使用');
|
|
3035
4047
|
url = readHttpUrl(payload.url, 'share.showShareMenu url 必须是 HTTP(S) URL');
|
|
3036
4048
|
}
|
|
3037
4049
|
else if (payload.url !== undefined && payload.url !== null && payload.url !== '') {
|
|
3038
4050
|
return invalidParams$2('share.showShareMenu url 必须是 HTTP(S) URL');
|
|
3039
4051
|
}
|
|
3040
4052
|
else {
|
|
3041
|
-
url = createDefaultShareUrl(options);
|
|
4053
|
+
url = createDefaultShareUrl(options, 'share.showShareMenu', extra);
|
|
3042
4054
|
}
|
|
3043
4055
|
return {
|
|
3044
4056
|
title: readRequiredString(payload.title, 'share.showShareMenu title 必须是非空字符串'),
|
|
@@ -3048,6 +4060,16 @@ function readShareMenuInput(payload, options) {
|
|
|
3048
4060
|
...(channel === undefined ? {} : { channel }),
|
|
3049
4061
|
};
|
|
3050
4062
|
}
|
|
4063
|
+
function readShareExtra(value, method) {
|
|
4064
|
+
if (value === undefined)
|
|
4065
|
+
return undefined;
|
|
4066
|
+
try {
|
|
4067
|
+
return encodeMiniProgramShareExtra(value);
|
|
4068
|
+
}
|
|
4069
|
+
catch {
|
|
4070
|
+
return invalidParams$2(`${method} extra 必须是符合限制的 JSON-compatible 数据`);
|
|
4071
|
+
}
|
|
4072
|
+
}
|
|
3051
4073
|
async function normalizePost(result, options) {
|
|
3052
4074
|
if (!result.invalidMessage)
|
|
3053
4075
|
return result.post;
|
|
@@ -3117,6 +4139,15 @@ async function showShareMenu(payload, options) {
|
|
|
3117
4139
|
const post = await normalizePost(postResult, options);
|
|
3118
4140
|
return waitForHostOperation(() => options.presentation.showShareMenu({ ...input, post }, options.operationContext), options.operationContext.signal);
|
|
3119
4141
|
}
|
|
4142
|
+
async function copyLink(payload, options) {
|
|
4143
|
+
throwIfAborted(options.operationContext.signal);
|
|
4144
|
+
if (payload !== undefined && !isRecord$2(payload))
|
|
4145
|
+
return invalidParams$2('share.copyLink 参数必须是对象');
|
|
4146
|
+
const extra = readShareExtra(payload?.extra, 'share.copyLink');
|
|
4147
|
+
const url = createDefaultShareUrl(options, 'share.copyLink', extra);
|
|
4148
|
+
await waitForHostOperation(() => options.presentation.setClipboard({ text: url }, options.operationContext), options.operationContext.signal);
|
|
4149
|
+
return url;
|
|
4150
|
+
}
|
|
3120
4151
|
async function screenshot(payload, options) {
|
|
3121
4152
|
throwIfAborted(options.operationContext.signal);
|
|
3122
4153
|
const input = readScreenshotOptions(payload, options.presentation);
|
|
@@ -3171,6 +4202,7 @@ async function restoreScreenshotChrome(presentation, cleanupContext, operationSi
|
|
|
3171
4202
|
}
|
|
3172
4203
|
function createShareCapabilityHandlers(options) {
|
|
3173
4204
|
return {
|
|
4205
|
+
[SHARE_COPY_LINK_METHOD]: (payload, _context) => copyLink(payload, options),
|
|
3174
4206
|
[SHARE_SHOW_SHARE_MENU_METHOD]: (payload, _context) => showShareMenu(payload, options),
|
|
3175
4207
|
[SHARE_SCREENSHOT_METHOD]: (payload, _context) => screenshot(payload, options),
|
|
3176
4208
|
};
|
|
@@ -9019,12 +10051,28 @@ function isPlainObject(value) {
|
|
|
9019
10051
|
}
|
|
9020
10052
|
|
|
9021
10053
|
/** Dev Shell 读取局域网 Dev Session endpoint 的 query 参数。 */
|
|
9022
|
-
const MINI_PROGRAM_DEV_CONTEXT_QUERY_PARAM = 'dev_context_url';
|
|
9023
10054
|
/** Browser Dev Host 创建和读取 Dev Session 的固定路径。 */
|
|
9024
10055
|
const MINI_PROGRAM_DEV_CONTEXT_PATH = '/__hb_sdk__/dev-context';
|
|
9025
10056
|
|
|
9026
|
-
|
|
9027
|
-
networkRequest
|
|
10057
|
+
function parseBrowserDevPermissionState(value) {
|
|
10058
|
+
if (!isPlainRecord(value) || !hasExactKeys(value, ['networkRequest']))
|
|
10059
|
+
return undefined;
|
|
10060
|
+
const networkRequest = value.networkRequest;
|
|
10061
|
+
if (!isPlainRecord(networkRequest) || !hasExactKeys(networkRequest, ['status', 'useOfficialDomain']))
|
|
10062
|
+
return undefined;
|
|
10063
|
+
if (networkRequest.status !== 'enabled' && networkRequest.status !== 'disabled')
|
|
10064
|
+
return undefined;
|
|
10065
|
+
if (typeof networkRequest.useOfficialDomain !== 'boolean')
|
|
10066
|
+
return undefined;
|
|
10067
|
+
return {
|
|
10068
|
+
networkRequest: {
|
|
10069
|
+
status: networkRequest.status,
|
|
10070
|
+
useOfficialDomain: networkRequest.useOfficialDomain,
|
|
10071
|
+
},
|
|
10072
|
+
};
|
|
10073
|
+
}
|
|
10074
|
+
const DEFAULT_PERMISSION_STATE = {
|
|
10075
|
+
networkRequest: {
|
|
9028
10076
|
status: 'disabled',
|
|
9029
10077
|
useOfficialDomain: false,
|
|
9030
10078
|
},
|
|
@@ -9055,68 +10103,243 @@ function createBrowserDevRuntimePermissions(state) {
|
|
|
9055
10103
|
],
|
|
9056
10104
|
};
|
|
9057
10105
|
}
|
|
9058
|
-
|
|
9059
|
-
|
|
9060
|
-
|
|
9061
|
-
|
|
9062
|
-
|
|
9063
|
-
|
|
9064
|
-
|
|
9065
|
-
|
|
9066
|
-
|
|
9067
|
-
|
|
9068
|
-
|
|
9069
|
-
|
|
9070
|
-
|
|
9071
|
-
|
|
9072
|
-
|
|
9073
|
-
|
|
9074
|
-
|
|
9075
|
-
|
|
9076
|
-
|
|
10106
|
+
/** Mobile Dev Session 不允许官方域名;创建时只冻结 network.request 开关。 */
|
|
10107
|
+
function createMobileDevSessionRuntimePermissions(state) {
|
|
10108
|
+
return createBrowserDevRuntimePermissions({
|
|
10109
|
+
networkRequest: {
|
|
10110
|
+
status: state.networkRequest.status,
|
|
10111
|
+
useOfficialDomain: false,
|
|
10112
|
+
},
|
|
10113
|
+
});
|
|
10114
|
+
}
|
|
10115
|
+
function cloneDefaultPermissionState() {
|
|
10116
|
+
return {
|
|
10117
|
+
networkRequest: { ...DEFAULT_PERMISSION_STATE.networkRequest },
|
|
10118
|
+
};
|
|
10119
|
+
}
|
|
10120
|
+
function isPlainRecord(value) {
|
|
10121
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
|
|
10122
|
+
}
|
|
10123
|
+
function hasExactKeys(value, keys) {
|
|
10124
|
+
const actualKeys = Object.keys(value);
|
|
10125
|
+
return actualKeys.length === keys.length && keys.every((key) => Object.hasOwn(value, key));
|
|
10126
|
+
}
|
|
10127
|
+
|
|
10128
|
+
function createBrowserDevHostStatusDrain(options) {
|
|
10129
|
+
let activeFetch;
|
|
10130
|
+
let dirty = false;
|
|
10131
|
+
let disposed = false;
|
|
10132
|
+
let drainTask;
|
|
10133
|
+
return {
|
|
10134
|
+
invalidate(hint) {
|
|
10135
|
+
if (disposed)
|
|
10136
|
+
return Promise.resolve({ ok: false });
|
|
10137
|
+
if (options.shouldSkip?.(hint, { draining: drainTask !== undefined })) {
|
|
10138
|
+
return drainTask ?? Promise.resolve({ ok: true });
|
|
10139
|
+
}
|
|
10140
|
+
dirty = true;
|
|
10141
|
+
drainTask ?? (drainTask = drain().finally(() => {
|
|
10142
|
+
drainTask = undefined;
|
|
10143
|
+
}));
|
|
10144
|
+
return drainTask;
|
|
10145
|
+
},
|
|
10146
|
+
dispose() {
|
|
10147
|
+
if (disposed)
|
|
10148
|
+
return;
|
|
10149
|
+
disposed = true;
|
|
10150
|
+
dirty = false;
|
|
10151
|
+
activeFetch?.abort();
|
|
10152
|
+
activeFetch = undefined;
|
|
10153
|
+
},
|
|
10154
|
+
};
|
|
10155
|
+
async function drain() {
|
|
10156
|
+
let outcome = { ok: true };
|
|
10157
|
+
while (dirty && !disposed) {
|
|
10158
|
+
dirty = false;
|
|
10159
|
+
const fetchController = new AbortController();
|
|
10160
|
+
activeFetch = fetchController;
|
|
10161
|
+
let canonical;
|
|
10162
|
+
try {
|
|
10163
|
+
canonical = await options.fetch(fetchController.signal);
|
|
10164
|
+
}
|
|
10165
|
+
catch (error) {
|
|
10166
|
+
outcome = { ok: false };
|
|
10167
|
+
if (!disposed && !fetchController.signal.aborted)
|
|
10168
|
+
reportError(error);
|
|
10169
|
+
continue;
|
|
10170
|
+
}
|
|
10171
|
+
finally {
|
|
10172
|
+
if (activeFetch === fetchController)
|
|
10173
|
+
activeFetch = undefined;
|
|
10174
|
+
}
|
|
10175
|
+
if (disposed)
|
|
10176
|
+
return { ok: false };
|
|
10177
|
+
let applied;
|
|
10178
|
+
try {
|
|
10179
|
+
applied = options.apply(canonical);
|
|
10180
|
+
}
|
|
10181
|
+
catch (error) {
|
|
10182
|
+
outcome = { ok: false };
|
|
10183
|
+
if (!disposed)
|
|
10184
|
+
reportError(error);
|
|
10185
|
+
continue;
|
|
10186
|
+
}
|
|
10187
|
+
if (disposed)
|
|
10188
|
+
return { ok: false };
|
|
10189
|
+
outcome = { ok: true };
|
|
10190
|
+
if (!options.hasChanged(applied))
|
|
10191
|
+
continue;
|
|
10192
|
+
try {
|
|
10193
|
+
await options.onChanged(applied);
|
|
10194
|
+
}
|
|
10195
|
+
catch (error) {
|
|
10196
|
+
outcome = { ok: false };
|
|
10197
|
+
if (!disposed)
|
|
10198
|
+
reportError(error);
|
|
10199
|
+
}
|
|
10200
|
+
}
|
|
10201
|
+
return disposed ? { ok: false } : outcome;
|
|
9077
10202
|
}
|
|
9078
|
-
|
|
9079
|
-
|
|
9080
|
-
|
|
9081
|
-
|
|
9082
|
-
|
|
10203
|
+
function reportError(error) {
|
|
10204
|
+
try {
|
|
10205
|
+
options.onError(error);
|
|
10206
|
+
}
|
|
10207
|
+
catch {
|
|
10208
|
+
// 错误观察方不能中断状态同步或产生未处理 rejection。
|
|
9083
10209
|
}
|
|
9084
|
-
: DEFAULT_PERMISSION_STATE.networkRequest;
|
|
9085
|
-
if (localState.networkRequest.status === 'enabled' && remoteState.status !== 'enabled') {
|
|
9086
|
-
return [
|
|
9087
|
-
{
|
|
9088
|
-
code: 'NETWORK_REQUEST_DISABLED_ONLINE',
|
|
9089
|
-
severity: 'warning',
|
|
9090
|
-
message: '当前本地允许 network.request,但线上权限已关闭;上线后请求会返回 PERMISSION_DENIED。',
|
|
9091
|
-
},
|
|
9092
|
-
];
|
|
9093
10210
|
}
|
|
9094
|
-
|
|
9095
|
-
|
|
9096
|
-
|
|
9097
|
-
|
|
9098
|
-
|
|
9099
|
-
|
|
9100
|
-
|
|
9101
|
-
|
|
10211
|
+
}
|
|
10212
|
+
|
|
10213
|
+
function createBrowserDevHostAccountStatusDrain(options) {
|
|
10214
|
+
return createBrowserDevHostStatusDrain({
|
|
10215
|
+
apply: options.applyStatus,
|
|
10216
|
+
fetch: options.fetchStatus,
|
|
10217
|
+
hasChanged: (applied) => applied.changed,
|
|
10218
|
+
onChanged: (applied) => options.reconcile(applied.status),
|
|
10219
|
+
onError: options.onError,
|
|
10220
|
+
shouldSkip: (revisionHint) => revisionHint !== undefined && revisionHint <= options.getCanonicalRevision(),
|
|
10221
|
+
});
|
|
10222
|
+
}
|
|
10223
|
+
|
|
10224
|
+
function createBrowserDevHostAccountStatusPoll(options) {
|
|
10225
|
+
const minDelayMs = options.minDelayMs ?? 1000;
|
|
10226
|
+
const maxDelayMs = options.maxDelayMs ?? 15000;
|
|
10227
|
+
let delayMs = minDelayMs;
|
|
10228
|
+
let disposed = false;
|
|
10229
|
+
let timer;
|
|
10230
|
+
return {
|
|
10231
|
+
schedule,
|
|
10232
|
+
reset() {
|
|
10233
|
+
delayMs = minDelayMs;
|
|
10234
|
+
clearTimer();
|
|
10235
|
+
},
|
|
10236
|
+
dispose() {
|
|
10237
|
+
if (disposed)
|
|
10238
|
+
return;
|
|
10239
|
+
disposed = true;
|
|
10240
|
+
clearTimer();
|
|
10241
|
+
},
|
|
10242
|
+
};
|
|
10243
|
+
function schedule() {
|
|
10244
|
+
if (disposed || options.isDisposed())
|
|
10245
|
+
return;
|
|
10246
|
+
clearTimer();
|
|
10247
|
+
timer = options.timers.setTimeout(async () => {
|
|
10248
|
+
timer = undefined;
|
|
10249
|
+
let outcome;
|
|
10250
|
+
try {
|
|
10251
|
+
outcome = await options.invalidate();
|
|
10252
|
+
}
|
|
10253
|
+
catch {
|
|
10254
|
+
outcome = { ok: false };
|
|
10255
|
+
}
|
|
10256
|
+
if (disposed || options.isDisposed())
|
|
10257
|
+
return;
|
|
10258
|
+
if (outcome.ok) {
|
|
10259
|
+
delayMs = minDelayMs;
|
|
10260
|
+
options.connectEvents();
|
|
10261
|
+
return;
|
|
10262
|
+
}
|
|
10263
|
+
delayMs = Math.min(delayMs * 2, maxDelayMs);
|
|
10264
|
+
schedule();
|
|
10265
|
+
}, delayMs);
|
|
10266
|
+
}
|
|
10267
|
+
function clearTimer() {
|
|
10268
|
+
if (timer !== undefined)
|
|
10269
|
+
options.timers.clearTimeout(timer);
|
|
10270
|
+
timer = undefined;
|
|
9102
10271
|
}
|
|
9103
|
-
return [];
|
|
9104
10272
|
}
|
|
9105
|
-
|
|
10273
|
+
|
|
10274
|
+
function createBrowserDevHostPermissionStatusDrain(options) {
|
|
10275
|
+
const drain = createBrowserDevHostStatusDrain({
|
|
10276
|
+
apply: options.applyCanonical,
|
|
10277
|
+
fetch: options.fetchCanonical,
|
|
10278
|
+
hasChanged: Boolean,
|
|
10279
|
+
onChanged: options.onChanged,
|
|
10280
|
+
onError: options.onError,
|
|
10281
|
+
shouldSkip: (input, { draining }) => !draining && !input?.force && input?.hint !== undefined && options.equals(input.hint, options.getCurrent()),
|
|
10282
|
+
});
|
|
9106
10283
|
return {
|
|
9107
|
-
|
|
10284
|
+
dispose: drain.dispose,
|
|
10285
|
+
invalidate(hint, invalidateOptions) {
|
|
10286
|
+
return drain.invalidate({ force: invalidateOptions?.force, hint });
|
|
10287
|
+
},
|
|
9108
10288
|
};
|
|
9109
10289
|
}
|
|
9110
10290
|
|
|
9111
|
-
const
|
|
9112
|
-
|
|
9113
|
-
|
|
9114
|
-
|
|
9115
|
-
|
|
9116
|
-
|
|
9117
|
-
|
|
9118
|
-
|
|
9119
|
-
|
|
10291
|
+
const SCOPE_LABELS = {
|
|
10292
|
+
identity: '小程序身份',
|
|
10293
|
+
profile: '昵称和头像',
|
|
10294
|
+
};
|
|
10295
|
+
function confirmBrowserDevHostAuthorization(input) {
|
|
10296
|
+
return new Promise((resolve, reject) => {
|
|
10297
|
+
if (input.signal.aborted) {
|
|
10298
|
+
reject(input.signal.reason);
|
|
10299
|
+
return;
|
|
10300
|
+
}
|
|
10301
|
+
const root = document.createElement('div');
|
|
10302
|
+
root.className = 'hb-sdk-auth-dialog';
|
|
10303
|
+
root.setAttribute('role', 'dialog');
|
|
10304
|
+
root.setAttribute('aria-modal', 'true');
|
|
10305
|
+
root.innerHTML = `
|
|
10306
|
+
<div class="hb-sdk-auth-dialog__panel">
|
|
10307
|
+
<h2>授权请求</h2>
|
|
10308
|
+
<p class="hb-sdk-auth-dialog__app">${escapeHtml$2(input.miniProgram?.name || '当前小程序')} 申请访问:</p>
|
|
10309
|
+
<ul class="hb-sdk-auth-dialog__scopes">${input.scopes.map((scope) => `<li>${escapeHtml$2(SCOPE_LABELS[scope] || scope)}</li>`).join('')}</ul>
|
|
10310
|
+
${input.userProfile?.nickname
|
|
10311
|
+
? `<p class="hb-sdk-auth-dialog__profile">当前账号:${escapeHtml$2(input.userProfile.nickname)}</p>`
|
|
10312
|
+
: ''}
|
|
10313
|
+
<div class="hb-sdk-auth-dialog__actions">
|
|
10314
|
+
<button type="button" data-action="deny">拒绝</button>
|
|
10315
|
+
<button type="button" class="primary" data-action="allow">允许</button>
|
|
10316
|
+
</div>
|
|
10317
|
+
</div>
|
|
10318
|
+
`;
|
|
10319
|
+
const finish = (decision) => {
|
|
10320
|
+
cleanup();
|
|
10321
|
+
resolve(decision);
|
|
10322
|
+
};
|
|
10323
|
+
const onAbort = () => {
|
|
10324
|
+
cleanup();
|
|
10325
|
+
reject(input.signal.reason);
|
|
10326
|
+
};
|
|
10327
|
+
const cleanup = () => {
|
|
10328
|
+
input.signal.removeEventListener('abort', onAbort);
|
|
10329
|
+
root.remove();
|
|
10330
|
+
};
|
|
10331
|
+
root.querySelector('[data-action="allow"]')?.addEventListener('click', () => {
|
|
10332
|
+
finish({ status: 'granted', grantedScopes: [...input.scopes] });
|
|
10333
|
+
});
|
|
10334
|
+
root.querySelector('[data-action="deny"]')?.addEventListener('click', () => {
|
|
10335
|
+
finish({ status: 'cancelled' });
|
|
10336
|
+
});
|
|
10337
|
+
input.signal.addEventListener('abort', onAbort, { once: true });
|
|
10338
|
+
document.body.appendChild(root);
|
|
10339
|
+
});
|
|
10340
|
+
}
|
|
10341
|
+
function escapeHtml$2(value) {
|
|
10342
|
+
return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", ''');
|
|
9120
10343
|
}
|
|
9121
10344
|
|
|
9122
10345
|
function createBrowserDevHostLaunchConfiguration(input) {
|
|
@@ -9133,58 +10356,963 @@ function createBrowserDevHostLaunchConfiguration(input) {
|
|
|
9133
10356
|
};
|
|
9134
10357
|
}
|
|
9135
10358
|
|
|
10359
|
+
const ACTION_DIALOG_STYLE_ID = 'hb-sdk-action-dialog-style';
|
|
10360
|
+
function promptBrowserDevHostAction(input) {
|
|
10361
|
+
return new Promise((resolve, reject) => {
|
|
10362
|
+
if (input.signal?.aborted) {
|
|
10363
|
+
reject(input.signal.reason);
|
|
10364
|
+
return;
|
|
10365
|
+
}
|
|
10366
|
+
ensureActionDialogStyles();
|
|
10367
|
+
const root = document.createElement('div');
|
|
10368
|
+
root.className = 'hb-sdk-action-dialog';
|
|
10369
|
+
root.setAttribute('role', 'dialog');
|
|
10370
|
+
root.setAttribute('aria-modal', 'true');
|
|
10371
|
+
root.innerHTML = `
|
|
10372
|
+
<div class="hb-sdk-action-dialog__panel">
|
|
10373
|
+
<h2>${escapeHtml$1(input.title)}</h2>
|
|
10374
|
+
<pre class="hb-sdk-action-dialog__detail">${escapeHtml$1(input.detail)}</pre>
|
|
10375
|
+
<div class="hb-sdk-action-dialog__actions">
|
|
10376
|
+
<button type="button" class="primary" data-action="confirm">确定</button>
|
|
10377
|
+
</div>
|
|
10378
|
+
</div>
|
|
10379
|
+
`;
|
|
10380
|
+
const finish = () => {
|
|
10381
|
+
cleanup();
|
|
10382
|
+
resolve();
|
|
10383
|
+
};
|
|
10384
|
+
const onAbort = () => {
|
|
10385
|
+
cleanup();
|
|
10386
|
+
reject(input.signal?.reason);
|
|
10387
|
+
};
|
|
10388
|
+
const cleanup = () => {
|
|
10389
|
+
input.signal?.removeEventListener('abort', onAbort);
|
|
10390
|
+
root.remove();
|
|
10391
|
+
};
|
|
10392
|
+
root.querySelector('[data-action="confirm"]')?.addEventListener('click', finish);
|
|
10393
|
+
input.signal?.addEventListener('abort', onAbort, { once: true });
|
|
10394
|
+
document.body.appendChild(root);
|
|
10395
|
+
});
|
|
10396
|
+
}
|
|
10397
|
+
async function promptOpenAppPage(protocol, context) {
|
|
10398
|
+
await promptBrowserDevHostAction({
|
|
10399
|
+
title: '打开页面',
|
|
10400
|
+
detail: ['protocolType: ' + protocol.protocolType, 'path: ' + protocol.path, 'params: ' + stringifyJson(protocol.params)].join('\n'),
|
|
10401
|
+
signal: context.signal,
|
|
10402
|
+
});
|
|
10403
|
+
}
|
|
10404
|
+
async function promptShowShareMenu(options, context) {
|
|
10405
|
+
await promptBrowserDevHostAction({
|
|
10406
|
+
title: '分享',
|
|
10407
|
+
detail: [
|
|
10408
|
+
'title: ' + options.title,
|
|
10409
|
+
'description: ' + options.description,
|
|
10410
|
+
'url: ' + options.url,
|
|
10411
|
+
'channel: ' + stringifyJson(options.channel),
|
|
10412
|
+
'post: ' + stringifyJson(options.post),
|
|
10413
|
+
].join('\n'),
|
|
10414
|
+
signal: context.signal,
|
|
10415
|
+
});
|
|
10416
|
+
return undefined;
|
|
10417
|
+
}
|
|
10418
|
+
async function promptCaptureAndShare(options, context) {
|
|
10419
|
+
// 调试页只提示截图参数,不读取 canvas。
|
|
10420
|
+
await promptBrowserDevHostAction({
|
|
10421
|
+
title: '截图分享',
|
|
10422
|
+
detail: ['rect: ' + stringifyJson(options.rect), 'post: ' + stringifyJson(options.post)].join('\n'),
|
|
10423
|
+
signal: context.signal,
|
|
10424
|
+
});
|
|
10425
|
+
return undefined;
|
|
10426
|
+
}
|
|
10427
|
+
function stringifyJson(value) {
|
|
10428
|
+
return JSON.stringify(value, null, 2);
|
|
10429
|
+
}
|
|
10430
|
+
function escapeHtml$1(value) {
|
|
10431
|
+
return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", ''');
|
|
10432
|
+
}
|
|
10433
|
+
/** 样式跟 index.html 解耦,避免和其他 agent 同时改静态页冲突。 */
|
|
10434
|
+
function ensureActionDialogStyles() {
|
|
10435
|
+
if (document.getElementById(ACTION_DIALOG_STYLE_ID)) {
|
|
10436
|
+
return;
|
|
10437
|
+
}
|
|
10438
|
+
const style = document.createElement('style');
|
|
10439
|
+
style.id = ACTION_DIALOG_STYLE_ID;
|
|
10440
|
+
style.textContent = `
|
|
10441
|
+
.hb-sdk-action-dialog {
|
|
10442
|
+
position: fixed;
|
|
10443
|
+
inset: 0;
|
|
10444
|
+
z-index: 40;
|
|
10445
|
+
display: grid;
|
|
10446
|
+
place-items: center;
|
|
10447
|
+
padding: 24px;
|
|
10448
|
+
background: rgba(15, 23, 42, 0.45);
|
|
10449
|
+
}
|
|
10450
|
+
|
|
10451
|
+
.hb-sdk-action-dialog__panel {
|
|
10452
|
+
width: min(420px, 100%);
|
|
10453
|
+
border-radius: 12px;
|
|
10454
|
+
padding: 20px;
|
|
10455
|
+
background: #fff;
|
|
10456
|
+
box-shadow: 0 20px 45px rgba(15, 23, 42, 0.2);
|
|
10457
|
+
}
|
|
10458
|
+
|
|
10459
|
+
.hb-sdk-action-dialog__panel h2 {
|
|
10460
|
+
margin: 0 0 8px;
|
|
10461
|
+
font-size: 18px;
|
|
10462
|
+
}
|
|
10463
|
+
|
|
10464
|
+
.hb-sdk-action-dialog__detail {
|
|
10465
|
+
margin: 0 0 16px;
|
|
10466
|
+
max-height: 40vh;
|
|
10467
|
+
overflow: auto;
|
|
10468
|
+
color: #1f2933;
|
|
10469
|
+
font-family: inherit;
|
|
10470
|
+
font-size: 12px;
|
|
10471
|
+
line-height: 1.6;
|
|
10472
|
+
white-space: pre-wrap;
|
|
10473
|
+
overflow-wrap: anywhere;
|
|
10474
|
+
word-break: break-word;
|
|
10475
|
+
}
|
|
10476
|
+
|
|
10477
|
+
.hb-sdk-action-dialog__actions {
|
|
10478
|
+
display: flex;
|
|
10479
|
+
justify-content: flex-end;
|
|
10480
|
+
gap: 8px;
|
|
10481
|
+
}
|
|
10482
|
+
|
|
10483
|
+
.hb-sdk-action-dialog__actions button {
|
|
10484
|
+
min-height: 36px;
|
|
10485
|
+
border: 1px solid #cad5e2;
|
|
10486
|
+
border-radius: 6px;
|
|
10487
|
+
padding: 0 12px;
|
|
10488
|
+
background: #fff;
|
|
10489
|
+
cursor: pointer;
|
|
10490
|
+
}
|
|
10491
|
+
|
|
10492
|
+
.hb-sdk-action-dialog__actions .primary {
|
|
10493
|
+
border-color: #2563eb;
|
|
10494
|
+
color: #fff;
|
|
10495
|
+
background: #2563eb;
|
|
10496
|
+
}
|
|
10497
|
+
`;
|
|
10498
|
+
document.head.appendChild(style);
|
|
10499
|
+
}
|
|
10500
|
+
|
|
10501
|
+
const STYLE_ID = 'hb-sdk-feedback-style';
|
|
10502
|
+
const TOAST_DURATION_MS = 2000;
|
|
10503
|
+
const VIBRATE_DURATION_MS = {
|
|
10504
|
+
light: 20,
|
|
10505
|
+
medium: 40,
|
|
10506
|
+
heavy: 80,
|
|
10507
|
+
};
|
|
10508
|
+
const VIBRATE_FLASH_MS = 120;
|
|
10509
|
+
const FEEDBACK_STYLE = `
|
|
10510
|
+
.hb-sdk-feedback-toast-stack {
|
|
10511
|
+
position: fixed;
|
|
10512
|
+
top: 16px;
|
|
10513
|
+
left: 50%;
|
|
10514
|
+
z-index: 50;
|
|
10515
|
+
display: grid;
|
|
10516
|
+
gap: 8px;
|
|
10517
|
+
transform: translateX(-50%);
|
|
10518
|
+
pointer-events: none;
|
|
10519
|
+
}
|
|
10520
|
+
.hb-sdk-feedback-toast {
|
|
10521
|
+
min-width: 160px;
|
|
10522
|
+
max-width: min(420px, calc(100vw - 32px));
|
|
10523
|
+
border-radius: 8px;
|
|
10524
|
+
padding: 10px 14px;
|
|
10525
|
+
color: #fff;
|
|
10526
|
+
background: #334155;
|
|
10527
|
+
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.18);
|
|
10528
|
+
text-align: center;
|
|
10529
|
+
line-height: 1.5;
|
|
10530
|
+
}
|
|
10531
|
+
.hb-sdk-feedback-toast--success {
|
|
10532
|
+
background: #047857;
|
|
10533
|
+
}
|
|
10534
|
+
.hb-sdk-feedback-toast--error {
|
|
10535
|
+
background: #b91c1c;
|
|
10536
|
+
}
|
|
10537
|
+
.hb-sdk-feedback-loading {
|
|
10538
|
+
position: fixed;
|
|
10539
|
+
inset: 0;
|
|
10540
|
+
z-index: 50;
|
|
10541
|
+
display: grid;
|
|
10542
|
+
place-items: center;
|
|
10543
|
+
padding: 24px;
|
|
10544
|
+
background: rgba(15, 23, 42, 0.45);
|
|
10545
|
+
}
|
|
10546
|
+
.hb-sdk-feedback-loading__panel {
|
|
10547
|
+
border-radius: 12px;
|
|
10548
|
+
padding: 20px 28px;
|
|
10549
|
+
color: #1f2933;
|
|
10550
|
+
background: #fff;
|
|
10551
|
+
box-shadow: 0 20px 45px rgba(15, 23, 42, 0.2);
|
|
10552
|
+
font-size: 16px;
|
|
10553
|
+
}
|
|
10554
|
+
html.hb-sdk-feedback-vibrate-flash {
|
|
10555
|
+
filter: brightness(1.18);
|
|
10556
|
+
}
|
|
10557
|
+
`;
|
|
10558
|
+
/** 全局只保留一个 loading 遮罩。 */
|
|
10559
|
+
let loadingRoot;
|
|
10560
|
+
let toastStack;
|
|
10561
|
+
function showBrowserDevHostToast(options) {
|
|
10562
|
+
ensureStyles();
|
|
10563
|
+
const stack = getToastStack();
|
|
10564
|
+
const toast = document.createElement('div');
|
|
10565
|
+
toast.className = 'hb-sdk-feedback-toast';
|
|
10566
|
+
if (options.status === 'success' || options.status === 'error') {
|
|
10567
|
+
toast.classList.add(`hb-sdk-feedback-toast--${options.status}`);
|
|
10568
|
+
}
|
|
10569
|
+
toast.setAttribute('role', 'status');
|
|
10570
|
+
// textContent 避免 toast 文案 XSS
|
|
10571
|
+
toast.textContent = options.message;
|
|
10572
|
+
stack.appendChild(toast);
|
|
10573
|
+
window.setTimeout(() => {
|
|
10574
|
+
toast.remove();
|
|
10575
|
+
if (toastStack && toastStack.childElementCount === 0) {
|
|
10576
|
+
toastStack.remove();
|
|
10577
|
+
toastStack = undefined;
|
|
10578
|
+
}
|
|
10579
|
+
}, TOAST_DURATION_MS);
|
|
10580
|
+
return Promise.resolve();
|
|
10581
|
+
}
|
|
10582
|
+
function showBrowserDevHostLoading(options) {
|
|
10583
|
+
ensureStyles();
|
|
10584
|
+
const dismissible = options?.dismissible === true;
|
|
10585
|
+
if (!loadingRoot) {
|
|
10586
|
+
const root = document.createElement('div');
|
|
10587
|
+
root.className = 'hb-sdk-feedback-loading';
|
|
10588
|
+
root.setAttribute('role', 'status');
|
|
10589
|
+
root.setAttribute('aria-live', 'polite');
|
|
10590
|
+
root.innerHTML = `<div class="hb-sdk-feedback-loading__panel">加载中</div>`;
|
|
10591
|
+
root.addEventListener('click', (event) => {
|
|
10592
|
+
if (event.target !== root)
|
|
10593
|
+
return;
|
|
10594
|
+
if (root.dataset.dismissible !== 'true')
|
|
10595
|
+
return;
|
|
10596
|
+
void hideBrowserDevHostLoading();
|
|
10597
|
+
});
|
|
10598
|
+
document.body.appendChild(root);
|
|
10599
|
+
loadingRoot = root;
|
|
10600
|
+
}
|
|
10601
|
+
loadingRoot.dataset.dismissible = dismissible ? 'true' : 'false';
|
|
10602
|
+
return Promise.resolve();
|
|
10603
|
+
}
|
|
10604
|
+
function hideBrowserDevHostLoading() {
|
|
10605
|
+
loadingRoot?.remove();
|
|
10606
|
+
loadingRoot = undefined;
|
|
10607
|
+
return Promise.resolve();
|
|
10608
|
+
}
|
|
10609
|
+
async function vibrateBrowserDevHost(options) {
|
|
10610
|
+
ensureStyles();
|
|
10611
|
+
await wait(Math.max(0, options.delay));
|
|
10612
|
+
flashVibrate();
|
|
10613
|
+
const vibrate = navigator.vibrate;
|
|
10614
|
+
if (typeof vibrate !== 'function') {
|
|
10615
|
+
return;
|
|
10616
|
+
}
|
|
10617
|
+
try {
|
|
10618
|
+
// DOM lib 将 VibratePattern 收成 Iterable<number>,单次时长仍按 number 传入。
|
|
10619
|
+
vibrate.call(navigator, VIBRATE_DURATION_MS[options.intensity]);
|
|
10620
|
+
}
|
|
10621
|
+
catch {
|
|
10622
|
+
// 调试页无震动能力时保持静默
|
|
10623
|
+
}
|
|
10624
|
+
}
|
|
10625
|
+
function setBrowserDevHostScreenshotChromeHidden(hidden) {
|
|
10626
|
+
document.documentElement.dataset.screenshotChromeHidden = hidden ? 'true' : 'false';
|
|
10627
|
+
}
|
|
10628
|
+
function ensureStyles() {
|
|
10629
|
+
// 调试页 CSS 只注入一次
|
|
10630
|
+
if (document.getElementById(STYLE_ID))
|
|
10631
|
+
return;
|
|
10632
|
+
const style = document.createElement('style');
|
|
10633
|
+
style.id = STYLE_ID;
|
|
10634
|
+
style.textContent = FEEDBACK_STYLE;
|
|
10635
|
+
document.head.appendChild(style);
|
|
10636
|
+
}
|
|
10637
|
+
function getToastStack() {
|
|
10638
|
+
if (toastStack?.isConnected) {
|
|
10639
|
+
return toastStack;
|
|
10640
|
+
}
|
|
10641
|
+
const stack = document.createElement('div');
|
|
10642
|
+
stack.className = 'hb-sdk-feedback-toast-stack';
|
|
10643
|
+
document.body.appendChild(stack);
|
|
10644
|
+
toastStack = stack;
|
|
10645
|
+
return stack;
|
|
10646
|
+
}
|
|
10647
|
+
function flashVibrate() {
|
|
10648
|
+
const root = document.documentElement;
|
|
10649
|
+
root.classList.add('hb-sdk-feedback-vibrate-flash');
|
|
10650
|
+
window.setTimeout(() => {
|
|
10651
|
+
root.classList.remove('hb-sdk-feedback-vibrate-flash');
|
|
10652
|
+
}, VIBRATE_FLASH_MS);
|
|
10653
|
+
}
|
|
10654
|
+
function wait(ms) {
|
|
10655
|
+
if (ms <= 0)
|
|
10656
|
+
return Promise.resolve();
|
|
10657
|
+
return new Promise((resolve) => {
|
|
10658
|
+
window.setTimeout(resolve, ms);
|
|
10659
|
+
});
|
|
10660
|
+
}
|
|
10661
|
+
|
|
10662
|
+
function createBrowserDevHostPresentation(input) {
|
|
10663
|
+
return {
|
|
10664
|
+
close: async () => {
|
|
10665
|
+
await input.close();
|
|
10666
|
+
},
|
|
10667
|
+
reload: async () => {
|
|
10668
|
+
location.reload();
|
|
10669
|
+
},
|
|
10670
|
+
getViewportMetrics: () => ({
|
|
10671
|
+
windowWidth: window.innerWidth,
|
|
10672
|
+
windowHeight: window.innerHeight,
|
|
10673
|
+
screenWidth: window.screen.width,
|
|
10674
|
+
screenHeight: window.screen.height,
|
|
10675
|
+
pixelRatio: window.devicePixelRatio,
|
|
10676
|
+
}),
|
|
10677
|
+
getNavigationBarHeight: () => 0,
|
|
10678
|
+
setNavigationBarStyle: (style) => {
|
|
10679
|
+
document.documentElement.dataset.navigationBarStyle = style;
|
|
10680
|
+
},
|
|
10681
|
+
setClipboard: async ({ text }) => {
|
|
10682
|
+
await navigator.clipboard.writeText(text);
|
|
10683
|
+
},
|
|
10684
|
+
showToast: async ({ message, status }) => {
|
|
10685
|
+
input.pushLog('host.message', { message, status });
|
|
10686
|
+
await showBrowserDevHostToast({ message, status });
|
|
10687
|
+
},
|
|
10688
|
+
showLoading: showBrowserDevHostLoading,
|
|
10689
|
+
hideLoading: hideBrowserDevHostLoading,
|
|
10690
|
+
vibrate: vibrateBrowserDevHost,
|
|
10691
|
+
openAppPage: promptOpenAppPage,
|
|
10692
|
+
showShareMenu: promptShowShareMenu,
|
|
10693
|
+
captureAndShare: promptCaptureAndShare,
|
|
10694
|
+
setScreenshotChromeHidden: setBrowserDevHostScreenshotChromeHidden,
|
|
10695
|
+
};
|
|
10696
|
+
}
|
|
10697
|
+
|
|
10698
|
+
const OPEN_IN_APP_URL = 'https://api.xiaoheihe.cn/open_inapp/';
|
|
10699
|
+
function createMobileLaunchRedirectHttpUrl(options) {
|
|
10700
|
+
const port = typeof options.port === 'number' ? options.port : Number(options.port);
|
|
10701
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
10702
|
+
throw new TypeError('Mobile launch redirect port is invalid');
|
|
10703
|
+
}
|
|
10704
|
+
if (!/^[A-Za-z0-9_-]{22}$/.test(options.token)) {
|
|
10705
|
+
throw new TypeError('Mobile launch redirect token is invalid');
|
|
10706
|
+
}
|
|
10707
|
+
const url = new URL(`http://${options.host}:${port}/l/${options.token}`);
|
|
10708
|
+
if (url.protocol !== 'http:' ||
|
|
10709
|
+
url.hostname !== options.host ||
|
|
10710
|
+
url.port !== String(port) ||
|
|
10711
|
+
url.username ||
|
|
10712
|
+
url.password ||
|
|
10713
|
+
url.search ||
|
|
10714
|
+
url.hash) {
|
|
10715
|
+
throw new TypeError('Mobile launch redirect host is invalid');
|
|
10716
|
+
}
|
|
10717
|
+
return url.href;
|
|
10718
|
+
}
|
|
10719
|
+
function createMobileAppRedirectQrPayload(redirectUrl) {
|
|
10720
|
+
const url = new URL(redirectUrl);
|
|
10721
|
+
if (url.protocol !== 'http:' || url.username || url.password) {
|
|
10722
|
+
throw new TypeError('Mobile launch redirect URL must be an HTTP URL without credentials');
|
|
10723
|
+
}
|
|
10724
|
+
return `${OPEN_IN_APP_URL}#${createHeyboxProtocol({
|
|
10725
|
+
protocol_type: 'openWindow',
|
|
10726
|
+
full_screen: true,
|
|
10727
|
+
navigation_bar: {
|
|
10728
|
+
title: '',
|
|
10729
|
+
},
|
|
10730
|
+
webview: {
|
|
10731
|
+
url: url.href,
|
|
10732
|
+
pull: false,
|
|
10733
|
+
refresh: false,
|
|
10734
|
+
},
|
|
10735
|
+
})}`;
|
|
10736
|
+
}
|
|
10737
|
+
function createHeyboxProtocol(payload) {
|
|
10738
|
+
return `heybox://${encodeURIComponent(JSON.stringify(payload))}`;
|
|
10739
|
+
}
|
|
10740
|
+
|
|
10741
|
+
const BROWSER_DEV_HOST_CHANNEL_VERSION = 1;
|
|
10742
|
+
const BROWSER_DEV_HOST_CHANNEL_MAX_TEXT_BYTES = 64 * 1024;
|
|
10743
|
+
const BROWSER_DEV_HOST_CHANNEL_MAX_JSON_DEPTH = 64;
|
|
10744
|
+
const FIXED_BASE64URL_PATTERN = /^[A-Za-z0-9_-]{22}$/;
|
|
10745
|
+
class BrowserDevHostChannelError extends TypeError {
|
|
10746
|
+
constructor(code, message) {
|
|
10747
|
+
super(message);
|
|
10748
|
+
this.name = 'BrowserDevHostChannelError';
|
|
10749
|
+
this.code = code;
|
|
10750
|
+
}
|
|
10751
|
+
}
|
|
10752
|
+
function parseBrowserDevHostServerMessage(raw, binary = false) {
|
|
10753
|
+
const value = parseTextFrame(raw, binary);
|
|
10754
|
+
assertRecord(value, 'Server message');
|
|
10755
|
+
assertVersion(value);
|
|
10756
|
+
switch (value.type) {
|
|
10757
|
+
case 'grant':
|
|
10758
|
+
assertExactKeys(value, ['version', 'type', 'connectionEpoch', 'nextClientSequence', 'nextServerSequence', 'snapshot'], 'grant');
|
|
10759
|
+
assertFixedBase64Url(value.connectionEpoch, 'connectionEpoch');
|
|
10760
|
+
assertNonNegativeSafeInteger(value.nextClientSequence, 'nextClientSequence');
|
|
10761
|
+
assertNonNegativeSafeInteger(value.nextServerSequence, 'nextServerSequence');
|
|
10762
|
+
assertSnapshot(value.snapshot);
|
|
10763
|
+
return value;
|
|
10764
|
+
case 'event':
|
|
10765
|
+
assertExactKeys(value, ['version', 'type', 'connectionEpoch', 'sequence', 'event', 'snapshot'], 'event');
|
|
10766
|
+
assertFixedBase64Url(value.connectionEpoch, 'connectionEpoch');
|
|
10767
|
+
assertNonNegativeSafeInteger(value.sequence, 'sequence');
|
|
10768
|
+
if (value.event !== 'account.changed' && value.event !== 'status.changed') {
|
|
10769
|
+
invalidMessage('event must be account.changed or status.changed');
|
|
10770
|
+
}
|
|
10771
|
+
assertSnapshot(value.snapshot);
|
|
10772
|
+
return value;
|
|
10773
|
+
case 'heartbeat':
|
|
10774
|
+
assertExactKeys(value, ['version', 'type', 'connectionEpoch', 'sequence'], 'heartbeat');
|
|
10775
|
+
assertFixedBase64Url(value.connectionEpoch, 'connectionEpoch');
|
|
10776
|
+
assertNonNegativeSafeInteger(value.sequence, 'sequence');
|
|
10777
|
+
return value;
|
|
10778
|
+
default:
|
|
10779
|
+
return invalidMessage('Unknown server message type');
|
|
10780
|
+
}
|
|
10781
|
+
}
|
|
10782
|
+
function createBrowserDevHostChannelSequenceGuard(input) {
|
|
10783
|
+
const connectionEpoch = input.connectionEpoch;
|
|
10784
|
+
assertFixedBase64Url(connectionEpoch, 'connectionEpoch');
|
|
10785
|
+
assertNonNegativeSafeInteger(input.nextSequence, 'nextSequence');
|
|
10786
|
+
let nextSequence = input.nextSequence;
|
|
10787
|
+
return {
|
|
10788
|
+
get nextSequence() {
|
|
10789
|
+
return nextSequence;
|
|
10790
|
+
},
|
|
10791
|
+
accept(message) {
|
|
10792
|
+
assertFixedBase64Url(message.connectionEpoch, 'connectionEpoch');
|
|
10793
|
+
assertNonNegativeSafeInteger(message.sequence, 'sequence');
|
|
10794
|
+
if (message.connectionEpoch !== connectionEpoch) {
|
|
10795
|
+
throw new BrowserDevHostChannelError('INVALID_SEQUENCE', 'Connection epoch does not match');
|
|
10796
|
+
}
|
|
10797
|
+
if (nextSequence === undefined) {
|
|
10798
|
+
throw new BrowserDevHostChannelError('INVALID_SEQUENCE', 'Sequence is exhausted');
|
|
10799
|
+
}
|
|
10800
|
+
if (message.sequence !== nextSequence) {
|
|
10801
|
+
throw new BrowserDevHostChannelError('INVALID_SEQUENCE', `Expected sequence ${nextSequence}`);
|
|
10802
|
+
}
|
|
10803
|
+
nextSequence = nextSequence === Number.MAX_SAFE_INTEGER ? undefined : nextSequence + 1;
|
|
10804
|
+
},
|
|
10805
|
+
};
|
|
10806
|
+
}
|
|
10807
|
+
function parseTextFrame(raw, binary) {
|
|
10808
|
+
if (binary) {
|
|
10809
|
+
throw new BrowserDevHostChannelError('BINARY_FRAME', 'Binary frames are not supported');
|
|
10810
|
+
}
|
|
10811
|
+
if (typeof raw !== 'string') {
|
|
10812
|
+
throw new BrowserDevHostChannelError('TEXT_FRAME_REQUIRED', 'Channel frame must be text');
|
|
10813
|
+
}
|
|
10814
|
+
if (raw.length > BROWSER_DEV_HOST_CHANNEL_MAX_TEXT_BYTES) {
|
|
10815
|
+
throw new BrowserDevHostChannelError('TEXT_FRAME_TOO_LARGE', 'Channel text frame exceeds 64 KiB');
|
|
10816
|
+
}
|
|
10817
|
+
if (new TextEncoder().encode(raw).byteLength > BROWSER_DEV_HOST_CHANNEL_MAX_TEXT_BYTES) {
|
|
10818
|
+
throw new BrowserDevHostChannelError('TEXT_FRAME_TOO_LARGE', 'Channel text frame exceeds 64 KiB');
|
|
10819
|
+
}
|
|
10820
|
+
try {
|
|
10821
|
+
assertNoDuplicateJsonKeys(raw);
|
|
10822
|
+
return JSON.parse(raw);
|
|
10823
|
+
}
|
|
10824
|
+
catch (error) {
|
|
10825
|
+
if (error instanceof BrowserDevHostChannelError)
|
|
10826
|
+
throw error;
|
|
10827
|
+
throw new BrowserDevHostChannelError('INVALID_JSON', 'Channel frame must contain strict JSON');
|
|
10828
|
+
}
|
|
10829
|
+
}
|
|
10830
|
+
function assertSnapshot(value) {
|
|
10831
|
+
assertRecord(value, 'snapshot');
|
|
10832
|
+
assertExactKeys(value, ['account', 'permissions'], 'snapshot');
|
|
10833
|
+
assertRecord(value.account, 'snapshot.account');
|
|
10834
|
+
assertExactKeys(value.account, ['revision'], 'snapshot.account');
|
|
10835
|
+
assertNonNegativeSafeInteger(value.account.revision, 'snapshot.account.revision');
|
|
10836
|
+
if (!parseBrowserDevPermissionState(value.permissions))
|
|
10837
|
+
invalidMessage('snapshot.permissions is invalid');
|
|
10838
|
+
}
|
|
10839
|
+
function assertVersion(value) {
|
|
10840
|
+
if (value.version !== BROWSER_DEV_HOST_CHANNEL_VERSION)
|
|
10841
|
+
invalidMessage('version must be 1');
|
|
10842
|
+
}
|
|
10843
|
+
function assertRecord(value, field) {
|
|
10844
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype) {
|
|
10845
|
+
invalidMessage(`${field} must be a plain JSON object`);
|
|
10846
|
+
}
|
|
10847
|
+
}
|
|
10848
|
+
function assertExactKeys(value, required, field, optional = []) {
|
|
10849
|
+
const keys = Object.keys(value);
|
|
10850
|
+
if (required.some((key) => !Object.hasOwn(value, key)) || keys.some((key) => !required.includes(key) && !optional.includes(key))) {
|
|
10851
|
+
invalidMessage(`${field} fields are invalid`);
|
|
10852
|
+
}
|
|
10853
|
+
}
|
|
10854
|
+
function assertNonNegativeSafeInteger(value, field) {
|
|
10855
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {
|
|
10856
|
+
invalidMessage(`${field} must be a non-negative safe integer`);
|
|
10857
|
+
}
|
|
10858
|
+
}
|
|
10859
|
+
function assertFixedBase64Url(value, field) {
|
|
10860
|
+
if (typeof value !== 'string' || !FIXED_BASE64URL_PATTERN.test(value)) {
|
|
10861
|
+
invalidMessage(`${field} must be a 22-character base64url value`);
|
|
10862
|
+
}
|
|
10863
|
+
}
|
|
10864
|
+
function invalidMessage(message) {
|
|
10865
|
+
throw new BrowserDevHostChannelError('INVALID_MESSAGE', message);
|
|
10866
|
+
}
|
|
10867
|
+
function assertNoDuplicateJsonKeys(raw) {
|
|
10868
|
+
const scanner = new StrictJsonKeyScanner(raw);
|
|
10869
|
+
scanner.scan();
|
|
10870
|
+
}
|
|
10871
|
+
class StrictJsonKeyScanner {
|
|
10872
|
+
constructor(source) {
|
|
10873
|
+
this.source = source;
|
|
10874
|
+
this.position = 0;
|
|
10875
|
+
}
|
|
10876
|
+
scan() {
|
|
10877
|
+
this.skipWhitespace();
|
|
10878
|
+
this.scanValue();
|
|
10879
|
+
this.skipWhitespace();
|
|
10880
|
+
if (this.position !== this.source.length)
|
|
10881
|
+
throw new SyntaxError('Unexpected trailing JSON data');
|
|
10882
|
+
}
|
|
10883
|
+
scanValue(depth = 0) {
|
|
10884
|
+
const character = this.source[this.position];
|
|
10885
|
+
if (character === '{' || character === '[') {
|
|
10886
|
+
if (depth >= BROWSER_DEV_HOST_CHANNEL_MAX_JSON_DEPTH) {
|
|
10887
|
+
throw new BrowserDevHostChannelError('INVALID_JSON', 'Channel JSON exceeds maximum nesting depth');
|
|
10888
|
+
}
|
|
10889
|
+
return character === '{' ? this.scanObject(depth) : this.scanArray(depth);
|
|
10890
|
+
}
|
|
10891
|
+
if (character === '"') {
|
|
10892
|
+
this.scanString();
|
|
10893
|
+
return;
|
|
10894
|
+
}
|
|
10895
|
+
this.scanPrimitive();
|
|
10896
|
+
}
|
|
10897
|
+
scanObject(depth) {
|
|
10898
|
+
this.position += 1;
|
|
10899
|
+
this.skipWhitespace();
|
|
10900
|
+
const keys = new Set();
|
|
10901
|
+
if (this.consume('}'))
|
|
10902
|
+
return;
|
|
10903
|
+
while (true) {
|
|
10904
|
+
if (this.source[this.position] !== '"')
|
|
10905
|
+
throw new SyntaxError('JSON object key must be a string');
|
|
10906
|
+
const keySource = this.scanString();
|
|
10907
|
+
const key = JSON.parse(keySource);
|
|
10908
|
+
if (typeof key !== 'string')
|
|
10909
|
+
throw new SyntaxError('JSON object key must be a string');
|
|
10910
|
+
if (keys.has(key)) {
|
|
10911
|
+
throw new BrowserDevHostChannelError('INVALID_JSON', 'Channel JSON contains a duplicate key');
|
|
10912
|
+
}
|
|
10913
|
+
keys.add(key);
|
|
10914
|
+
this.skipWhitespace();
|
|
10915
|
+
if (!this.consume(':'))
|
|
10916
|
+
throw new SyntaxError('Missing JSON object colon');
|
|
10917
|
+
this.skipWhitespace();
|
|
10918
|
+
this.scanValue(depth + 1);
|
|
10919
|
+
this.skipWhitespace();
|
|
10920
|
+
if (this.consume('}'))
|
|
10921
|
+
return;
|
|
10922
|
+
if (!this.consume(','))
|
|
10923
|
+
throw new SyntaxError('Missing JSON object comma');
|
|
10924
|
+
this.skipWhitespace();
|
|
10925
|
+
}
|
|
10926
|
+
}
|
|
10927
|
+
scanArray(depth) {
|
|
10928
|
+
this.position += 1;
|
|
10929
|
+
this.skipWhitespace();
|
|
10930
|
+
if (this.consume(']'))
|
|
10931
|
+
return;
|
|
10932
|
+
while (true) {
|
|
10933
|
+
this.scanValue(depth + 1);
|
|
10934
|
+
this.skipWhitespace();
|
|
10935
|
+
if (this.consume(']'))
|
|
10936
|
+
return;
|
|
10937
|
+
if (!this.consume(','))
|
|
10938
|
+
throw new SyntaxError('Missing JSON array comma');
|
|
10939
|
+
this.skipWhitespace();
|
|
10940
|
+
}
|
|
10941
|
+
}
|
|
10942
|
+
scanString() {
|
|
10943
|
+
const start = this.position;
|
|
10944
|
+
this.position += 1;
|
|
10945
|
+
while (this.position < this.source.length) {
|
|
10946
|
+
const character = this.source[this.position];
|
|
10947
|
+
if (character === '"') {
|
|
10948
|
+
this.position += 1;
|
|
10949
|
+
return this.source.slice(start, this.position);
|
|
10950
|
+
}
|
|
10951
|
+
if (character === '\\') {
|
|
10952
|
+
this.position += 2;
|
|
10953
|
+
}
|
|
10954
|
+
else {
|
|
10955
|
+
this.position += 1;
|
|
10956
|
+
}
|
|
10957
|
+
}
|
|
10958
|
+
throw new SyntaxError('Unterminated JSON string');
|
|
10959
|
+
}
|
|
10960
|
+
scanPrimitive() {
|
|
10961
|
+
const start = this.position;
|
|
10962
|
+
while (this.position < this.source.length && !/[\s,\]}]/.test(this.source[this.position] ?? '')) {
|
|
10963
|
+
this.position += 1;
|
|
10964
|
+
}
|
|
10965
|
+
if (start === this.position)
|
|
10966
|
+
throw new SyntaxError('Missing JSON value');
|
|
10967
|
+
}
|
|
10968
|
+
skipWhitespace() {
|
|
10969
|
+
while (/[\t\n\r ]/.test(this.source[this.position] ?? ''))
|
|
10970
|
+
this.position += 1;
|
|
10971
|
+
}
|
|
10972
|
+
consume(character) {
|
|
10973
|
+
if (this.source[this.position] !== character)
|
|
10974
|
+
return false;
|
|
10975
|
+
this.position += 1;
|
|
10976
|
+
return true;
|
|
10977
|
+
}
|
|
10978
|
+
}
|
|
10979
|
+
|
|
10980
|
+
const RECONNECT_DELAYS_MS = [1000, 2000, 4000, 8000, 15000];
|
|
10981
|
+
function createBrowserDevHostChannelClient(options) {
|
|
10982
|
+
const updateListeners = new Set();
|
|
10983
|
+
const createWebSocket = options.webSocketFactory ?? ((url) => new WebSocket(url));
|
|
10984
|
+
const randomValues = options.randomValues ?? ((bytes) => crypto.getRandomValues(bytes));
|
|
10985
|
+
const timers = options.timers ?? {
|
|
10986
|
+
clearTimeout: (handle) => window.clearTimeout(handle),
|
|
10987
|
+
setTimeout: (callback, delay) => window.setTimeout(callback, delay),
|
|
10988
|
+
};
|
|
10989
|
+
const onlineTarget = options.onlineTarget ?? (typeof window === 'undefined' ? undefined : window);
|
|
10990
|
+
const visibilityTarget = options.visibilityTarget ?? (typeof document === 'undefined' ? undefined : document);
|
|
10991
|
+
const isOnline = options.isOnline ?? (() => typeof navigator === 'undefined' || navigator.onLine !== false);
|
|
10992
|
+
let socket;
|
|
10993
|
+
let detachSocketListeners;
|
|
10994
|
+
let snapshot;
|
|
10995
|
+
let disposed = false;
|
|
10996
|
+
let reconnectAttempt = 0;
|
|
10997
|
+
let reconnectTimer;
|
|
10998
|
+
let started = false;
|
|
10999
|
+
let lifecycleListenersAttached = false;
|
|
11000
|
+
const diagnosticsSinceGrant = new Set();
|
|
11001
|
+
const reportDiagnostic = (phase, code) => {
|
|
11002
|
+
if (!options.onDiagnostic || diagnosticsSinceGrant.has(phase))
|
|
11003
|
+
return;
|
|
11004
|
+
diagnosticsSinceGrant.add(phase);
|
|
11005
|
+
try {
|
|
11006
|
+
options.onDiagnostic({ phase, ...(code ? { code } : {}) });
|
|
11007
|
+
}
|
|
11008
|
+
catch {
|
|
11009
|
+
// 诊断观察方不能影响 Browser channel 或 HTTP/SSE fallback。
|
|
11010
|
+
}
|
|
11011
|
+
};
|
|
11012
|
+
const publishSnapshot = (nextSnapshot, update, notifyEqual = false) => {
|
|
11013
|
+
if (snapshot && nextSnapshot.account.revision < snapshot.account.revision)
|
|
11014
|
+
return;
|
|
11015
|
+
const equal = snapshot && JSON.stringify(nextSnapshot) === JSON.stringify(snapshot);
|
|
11016
|
+
snapshot = nextSnapshot;
|
|
11017
|
+
if (equal && !notifyEqual)
|
|
11018
|
+
return;
|
|
11019
|
+
for (const listener of updateListeners)
|
|
11020
|
+
listener(nextSnapshot, update);
|
|
11021
|
+
};
|
|
11022
|
+
const scheduleReconnect = () => {
|
|
11023
|
+
if (disposed || reconnectTimer !== undefined || socket)
|
|
11024
|
+
return;
|
|
11025
|
+
const delay = RECONNECT_DELAYS_MS[Math.min(reconnectAttempt, RECONNECT_DELAYS_MS.length - 1)];
|
|
11026
|
+
reconnectAttempt += 1;
|
|
11027
|
+
reconnectTimer = timers.setTimeout(() => {
|
|
11028
|
+
reconnectTimer = undefined;
|
|
11029
|
+
connect();
|
|
11030
|
+
}, delay);
|
|
11031
|
+
};
|
|
11032
|
+
const reconnectNow = () => {
|
|
11033
|
+
if (disposed || !started || socket)
|
|
11034
|
+
return;
|
|
11035
|
+
if (reconnectTimer !== undefined) {
|
|
11036
|
+
timers.clearTimeout(reconnectTimer);
|
|
11037
|
+
reconnectTimer = undefined;
|
|
11038
|
+
}
|
|
11039
|
+
connect();
|
|
11040
|
+
};
|
|
11041
|
+
const onOnline = () => {
|
|
11042
|
+
if (isOnline())
|
|
11043
|
+
reconnectNow();
|
|
11044
|
+
};
|
|
11045
|
+
const onVisibilityChange = () => {
|
|
11046
|
+
if (visibilityTarget && !visibilityTarget.hidden)
|
|
11047
|
+
reconnectNow();
|
|
11048
|
+
};
|
|
11049
|
+
const attachLifecycleListeners = () => {
|
|
11050
|
+
if (lifecycleListenersAttached)
|
|
11051
|
+
return;
|
|
11052
|
+
lifecycleListenersAttached = true;
|
|
11053
|
+
onlineTarget?.addEventListener('online', onOnline);
|
|
11054
|
+
visibilityTarget?.addEventListener('visibilitychange', onVisibilityChange);
|
|
11055
|
+
};
|
|
11056
|
+
const detachLifecycleListeners = () => {
|
|
11057
|
+
if (!lifecycleListenersAttached)
|
|
11058
|
+
return;
|
|
11059
|
+
lifecycleListenersAttached = false;
|
|
11060
|
+
onlineTarget?.removeEventListener('online', onOnline);
|
|
11061
|
+
visibilityTarget?.removeEventListener('visibilitychange', onVisibilityChange);
|
|
11062
|
+
};
|
|
11063
|
+
const connect = () => {
|
|
11064
|
+
if (disposed || socket)
|
|
11065
|
+
return;
|
|
11066
|
+
started = true;
|
|
11067
|
+
attachLifecycleListeners();
|
|
11068
|
+
if (reconnectTimer !== undefined) {
|
|
11069
|
+
timers.clearTimeout(reconnectTimer);
|
|
11070
|
+
reconnectTimer = undefined;
|
|
11071
|
+
}
|
|
11072
|
+
let nextSocket;
|
|
11073
|
+
let clientNonce;
|
|
11074
|
+
try {
|
|
11075
|
+
nextSocket = createWebSocket(options.url);
|
|
11076
|
+
}
|
|
11077
|
+
catch {
|
|
11078
|
+
reportDiagnostic('constructor', 'WEBSOCKET_CONSTRUCTION_FAILED');
|
|
11079
|
+
scheduleReconnect();
|
|
11080
|
+
return;
|
|
11081
|
+
}
|
|
11082
|
+
try {
|
|
11083
|
+
clientNonce = createClientNonce(randomValues);
|
|
11084
|
+
}
|
|
11085
|
+
catch {
|
|
11086
|
+
reportDiagnostic('entropy', 'NONCE_GENERATION_FAILED');
|
|
11087
|
+
try {
|
|
11088
|
+
nextSocket?.close(1011, 'channel initialization failed');
|
|
11089
|
+
}
|
|
11090
|
+
catch {
|
|
11091
|
+
// WebSocket 可能尚未完成初始化。
|
|
11092
|
+
}
|
|
11093
|
+
scheduleReconnect();
|
|
11094
|
+
return;
|
|
11095
|
+
}
|
|
11096
|
+
socket = nextSocket;
|
|
11097
|
+
let connectionEpoch;
|
|
11098
|
+
let nextClientSequence;
|
|
11099
|
+
let serverSequenceGuard;
|
|
11100
|
+
const detach = () => {
|
|
11101
|
+
nextSocket.removeEventListener('open', onOpen);
|
|
11102
|
+
nextSocket.removeEventListener('message', onMessage);
|
|
11103
|
+
nextSocket.removeEventListener('close', onClose);
|
|
11104
|
+
nextSocket.removeEventListener('error', onError);
|
|
11105
|
+
if (detachSocketListeners === detach)
|
|
11106
|
+
detachSocketListeners = undefined;
|
|
11107
|
+
};
|
|
11108
|
+
const fail = () => {
|
|
11109
|
+
if (socket !== nextSocket)
|
|
11110
|
+
return;
|
|
11111
|
+
socket = undefined;
|
|
11112
|
+
detach();
|
|
11113
|
+
try {
|
|
11114
|
+
nextSocket.close(1008, 'invalid channel state');
|
|
11115
|
+
}
|
|
11116
|
+
catch {
|
|
11117
|
+
// WebSocket 已终止时只需进入 fallback 重连。
|
|
11118
|
+
}
|
|
11119
|
+
scheduleReconnect();
|
|
11120
|
+
};
|
|
11121
|
+
const onOpen = () => {
|
|
11122
|
+
if (disposed || socket !== nextSocket)
|
|
11123
|
+
return;
|
|
11124
|
+
try {
|
|
11125
|
+
nextSocket.send(JSON.stringify({
|
|
11126
|
+
version: BROWSER_DEV_HOST_CHANNEL_VERSION,
|
|
11127
|
+
type: 'hello',
|
|
11128
|
+
csrfToken: options.csrfToken,
|
|
11129
|
+
clientNonce,
|
|
11130
|
+
}));
|
|
11131
|
+
}
|
|
11132
|
+
catch {
|
|
11133
|
+
reportDiagnostic('send', 'HELLO_SEND_FAILED');
|
|
11134
|
+
fail();
|
|
11135
|
+
}
|
|
11136
|
+
};
|
|
11137
|
+
const onMessage = (event) => {
|
|
11138
|
+
if (disposed || socket !== nextSocket)
|
|
11139
|
+
return;
|
|
11140
|
+
if (typeof event.data !== 'string') {
|
|
11141
|
+
reportDiagnostic('parse', 'TEXT_FRAME_REQUIRED');
|
|
11142
|
+
fail();
|
|
11143
|
+
return;
|
|
11144
|
+
}
|
|
11145
|
+
let message;
|
|
11146
|
+
try {
|
|
11147
|
+
message = parseBrowserDevHostServerMessage(event.data);
|
|
11148
|
+
}
|
|
11149
|
+
catch (error) {
|
|
11150
|
+
reportDiagnostic('parse', error instanceof BrowserDevHostChannelError ? error.code : 'INVALID_MESSAGE');
|
|
11151
|
+
fail();
|
|
11152
|
+
return;
|
|
11153
|
+
}
|
|
11154
|
+
if (message.type === 'grant') {
|
|
11155
|
+
if (serverSequenceGuard) {
|
|
11156
|
+
reportDiagnostic('sequence', 'DUPLICATE_GRANT');
|
|
11157
|
+
fail();
|
|
11158
|
+
return;
|
|
11159
|
+
}
|
|
11160
|
+
connectionEpoch = message.connectionEpoch;
|
|
11161
|
+
nextClientSequence = message.nextClientSequence;
|
|
11162
|
+
serverSequenceGuard = createBrowserDevHostChannelSequenceGuard({
|
|
11163
|
+
connectionEpoch,
|
|
11164
|
+
nextSequence: message.nextServerSequence,
|
|
11165
|
+
});
|
|
11166
|
+
reconnectAttempt = 0;
|
|
11167
|
+
diagnosticsSinceGrant.clear();
|
|
11168
|
+
publishSnapshot(message.snapshot, { type: 'grant' });
|
|
11169
|
+
return;
|
|
11170
|
+
}
|
|
11171
|
+
if (!serverSequenceGuard || !connectionEpoch || nextClientSequence === undefined) {
|
|
11172
|
+
reportDiagnostic('sequence', 'MESSAGE_BEFORE_GRANT');
|
|
11173
|
+
fail();
|
|
11174
|
+
return;
|
|
11175
|
+
}
|
|
11176
|
+
try {
|
|
11177
|
+
serverSequenceGuard.accept(message);
|
|
11178
|
+
}
|
|
11179
|
+
catch {
|
|
11180
|
+
reportDiagnostic('sequence', 'INVALID_SEQUENCE');
|
|
11181
|
+
fail();
|
|
11182
|
+
return;
|
|
11183
|
+
}
|
|
11184
|
+
if (message.type === 'heartbeat') {
|
|
11185
|
+
try {
|
|
11186
|
+
nextSocket.send(JSON.stringify({
|
|
11187
|
+
version: BROWSER_DEV_HOST_CHANNEL_VERSION,
|
|
11188
|
+
type: 'heartbeat.ack',
|
|
11189
|
+
connectionEpoch,
|
|
11190
|
+
sequence: nextClientSequence,
|
|
11191
|
+
}));
|
|
11192
|
+
}
|
|
11193
|
+
catch {
|
|
11194
|
+
reportDiagnostic('send', 'HEARTBEAT_ACK_SEND_FAILED');
|
|
11195
|
+
fail();
|
|
11196
|
+
}
|
|
11197
|
+
nextClientSequence = nextClientSequence === Number.MAX_SAFE_INTEGER ? undefined : nextClientSequence + 1;
|
|
11198
|
+
return;
|
|
11199
|
+
}
|
|
11200
|
+
publishSnapshot(message.snapshot, { type: 'event', event: message.event }, true);
|
|
11201
|
+
};
|
|
11202
|
+
const onClose = () => {
|
|
11203
|
+
if (socket !== nextSocket)
|
|
11204
|
+
return;
|
|
11205
|
+
reportDiagnostic('close', 'SOCKET_CLOSED');
|
|
11206
|
+
socket = undefined;
|
|
11207
|
+
detach();
|
|
11208
|
+
scheduleReconnect();
|
|
11209
|
+
};
|
|
11210
|
+
const onError = () => {
|
|
11211
|
+
reportDiagnostic('open', 'SOCKET_ERROR');
|
|
11212
|
+
fail();
|
|
11213
|
+
};
|
|
11214
|
+
nextSocket.addEventListener('open', onOpen);
|
|
11215
|
+
nextSocket.addEventListener('message', onMessage);
|
|
11216
|
+
nextSocket.addEventListener('close', onClose);
|
|
11217
|
+
nextSocket.addEventListener('error', onError);
|
|
11218
|
+
detachSocketListeners = detach;
|
|
11219
|
+
};
|
|
11220
|
+
return {
|
|
11221
|
+
connect,
|
|
11222
|
+
subscribeUpdates(listener) {
|
|
11223
|
+
updateListeners.add(listener);
|
|
11224
|
+
return () => updateListeners.delete(listener);
|
|
11225
|
+
},
|
|
11226
|
+
dispose() {
|
|
11227
|
+
if (disposed)
|
|
11228
|
+
return;
|
|
11229
|
+
disposed = true;
|
|
11230
|
+
started = false;
|
|
11231
|
+
detachLifecycleListeners();
|
|
11232
|
+
if (reconnectTimer !== undefined)
|
|
11233
|
+
timers.clearTimeout(reconnectTimer);
|
|
11234
|
+
reconnectTimer = undefined;
|
|
11235
|
+
const activeSocket = socket;
|
|
11236
|
+
socket = undefined;
|
|
11237
|
+
detachSocketListeners?.();
|
|
11238
|
+
activeSocket?.close(1000, 'client disposed');
|
|
11239
|
+
updateListeners.clear();
|
|
11240
|
+
},
|
|
11241
|
+
};
|
|
11242
|
+
}
|
|
11243
|
+
function encodeBase64Url(bytes) {
|
|
11244
|
+
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
|
|
11245
|
+
let result = '';
|
|
11246
|
+
for (let index = 0; index < bytes.length; index += 3) {
|
|
11247
|
+
const first = bytes[index] ?? 0;
|
|
11248
|
+
const second = bytes[index + 1];
|
|
11249
|
+
const third = bytes[index + 2];
|
|
11250
|
+
result += alphabet[first >> 2];
|
|
11251
|
+
result += alphabet[((first & 0x03) << 4) | ((second ?? 0) >> 4)];
|
|
11252
|
+
if (second !== undefined)
|
|
11253
|
+
result += alphabet[((second & 0x0f) << 2) | ((third ?? 0) >> 6)];
|
|
11254
|
+
if (third !== undefined)
|
|
11255
|
+
result += alphabet[third & 0x3f];
|
|
11256
|
+
}
|
|
11257
|
+
return result;
|
|
11258
|
+
}
|
|
11259
|
+
function createClientNonce(randomValues) {
|
|
11260
|
+
const bytes = randomValues(new Uint8Array(16));
|
|
11261
|
+
if (!(bytes instanceof Uint8Array) || bytes.byteLength !== 16)
|
|
11262
|
+
throw new TypeError('Channel nonce source must fill 16 bytes');
|
|
11263
|
+
const nonce = encodeBase64Url(bytes);
|
|
11264
|
+
if (!/^[A-Za-z0-9_-]{22}$/.test(nonce))
|
|
11265
|
+
throw new TypeError('Channel nonce source returned invalid bytes');
|
|
11266
|
+
return nonce;
|
|
11267
|
+
}
|
|
11268
|
+
|
|
9136
11269
|
const BOOTSTRAP_PATH = '/__hb_sdk_bootstrap__';
|
|
9137
11270
|
const DEV_PERMISSIONS_PATH = '/__hb_sdk__/dev-permissions';
|
|
11271
|
+
const MOBILE_LAUNCH_REDIRECT_PATH = '/__hb_sdk__/mobile-launch-redirect';
|
|
9138
11272
|
const NETWORK_PATH = '/__hb_sdk__/network';
|
|
9139
11273
|
const SESSION_STATUS_PATH = '/session/status';
|
|
9140
|
-
const SESSION_LOGIN_INTENT_PATH = '/session/login-intent';
|
|
9141
|
-
const SESSION_LOGOUT_PATH = '/session/logout';
|
|
9142
11274
|
const SESSION_REQUEST_PATH = '/session/request';
|
|
9143
11275
|
const SESSION_EVENTS_PATH = '/session/events';
|
|
9144
11276
|
const MOBILE_QR_IMAGE_SIZE = 320;
|
|
9145
11277
|
const NATIVE_LAUNCH_CONFIG_REFRESH_MS = 1000;
|
|
9146
11278
|
const SESSION_POLL_MIN_MS = 1000;
|
|
9147
11279
|
const SESSION_POLL_MAX_MS = 15000;
|
|
9148
|
-
const
|
|
11280
|
+
const pageQuery = new URL(location.href).searchParams;
|
|
11281
|
+
const miniProgramUrl = pageQuery.get('mini_url');
|
|
11282
|
+
const uiPreview = pageQuery.get('ui_preview') === '1';
|
|
9149
11283
|
const elements = {
|
|
9150
|
-
|
|
9151
|
-
|
|
9152
|
-
|
|
9153
|
-
debugPageUrl: queryElement('#debug-page-url'),
|
|
9154
|
-
devContextStatus: queryElement('#dev-context-status'),
|
|
11284
|
+
developerAvatar: queryElement('#developer-avatar'),
|
|
11285
|
+
developerHint: queryElement('#developer-hint'),
|
|
11286
|
+
developerName: queryElement('#developer-name'),
|
|
9155
11287
|
device: queryElement('#device'),
|
|
9156
11288
|
logs: queryElement('#logs'),
|
|
9157
|
-
|
|
9158
|
-
|
|
9159
|
-
|
|
9160
|
-
|
|
11289
|
+
miniProgramEmpty: queryElement('#mini-program-empty'),
|
|
11290
|
+
miniProgramIcon: queryElement('#mini-program-icon'),
|
|
11291
|
+
miniProgramId: queryElement('#mini-program-id'),
|
|
11292
|
+
miniProgramName: queryElement('#mini-program-name'),
|
|
11293
|
+
miniProgramSummary: queryElement('#mini-program-summary'),
|
|
9161
11294
|
mobileLanSelect: queryElement('#mobile-lan-select'),
|
|
9162
11295
|
mobileQrImage: queryElement('#mobile-qr-image'),
|
|
9163
11296
|
mobileQrStatus: queryElement('#mobile-qr-status'),
|
|
9164
|
-
networkPermissionSelect: queryElement('#network-permission-select'),
|
|
9165
|
-
permissionPersistenceStatus: queryElement('#permission-persistence-status'),
|
|
9166
|
-
releaseDiagnostics: queryElement('#release-diagnostics'),
|
|
9167
|
-
resetPermissionsButton: queryElement('#reset-permissions-button'),
|
|
9168
11297
|
storageSnapshot: queryElement('#storage-snapshot'),
|
|
9169
|
-
userStatus: queryElement('#user-status'),
|
|
9170
11298
|
};
|
|
9171
|
-
if (!miniProgramUrl) {
|
|
11299
|
+
if (!miniProgramUrl && !uiPreview) {
|
|
9172
11300
|
elements.device.innerHTML = '<div class="error">Missing mini_url query.</div>';
|
|
9173
11301
|
throw new Error('Missing mini_url query.');
|
|
9174
11302
|
}
|
|
9175
|
-
const launchUrl = miniProgramUrl;
|
|
11303
|
+
const launchUrl = miniProgramUrl ?? 'about:blank';
|
|
9176
11304
|
const bootstrap = await loadBootstrap();
|
|
9177
11305
|
let lanAddresses = bootstrap.lanAddresses;
|
|
9178
11306
|
let defaultLanAddressId = bootstrap.defaultLanAddressId;
|
|
9179
|
-
let
|
|
9180
|
-
let nativeAppLaunchUnavailableReason = bootstrap.nativeAppLaunchUnavailableReason;
|
|
11307
|
+
let mobileLaunchRedirect = bootstrap.mobileLaunchRedirect;
|
|
9181
11308
|
let nativeLaunchConfigSignature = readNativeLaunchConfigSignature(bootstrap);
|
|
9182
11309
|
let devContext = bootstrap.devContext;
|
|
9183
11310
|
let remotePermissionState = createBrowserDevPermissionState(devContext);
|
|
9184
11311
|
let permissionState = clonePermissionState(remotePermissionState);
|
|
9185
|
-
let
|
|
9186
|
-
let permissionActionMessage = '';
|
|
11312
|
+
let runtimePermissionState;
|
|
9187
11313
|
let disposed = false;
|
|
11314
|
+
let hostGeneration = 0;
|
|
11315
|
+
const hostAbortController = new AbortController();
|
|
9188
11316
|
let runtimeGeneration = 0;
|
|
9189
11317
|
let activeRuntime;
|
|
9190
11318
|
let activeTerminal;
|
|
@@ -9195,20 +11323,55 @@ let nativeLaunchConfigRefreshTimer;
|
|
|
9195
11323
|
let nativeLaunchConfigRefreshInFlight = false;
|
|
9196
11324
|
let mobileDevSessionCreation = Promise.resolve();
|
|
9197
11325
|
let sessionRevision = -1;
|
|
11326
|
+
let sessionAuthenticated = false;
|
|
9198
11327
|
let sessionUserId;
|
|
11328
|
+
let sessionUserNickname;
|
|
11329
|
+
let sessionUserAvatar;
|
|
9199
11330
|
let sessionEventSource;
|
|
9200
|
-
let
|
|
9201
|
-
let sessionPollDelay = SESSION_POLL_MIN_MS;
|
|
9202
|
-
let accountRevisionTask;
|
|
11331
|
+
let channelClient;
|
|
9203
11332
|
const accountListeners = new Set();
|
|
11333
|
+
const accountStatusDrain = createBrowserDevHostAccountStatusDrain({
|
|
11334
|
+
applyStatus: (status) => applySessionStatus(status, true),
|
|
11335
|
+
fetchStatus: (signal) => fetchSessionStatus(combineAbortSignals(hostAbortController.signal, signal)),
|
|
11336
|
+
getCanonicalRevision: () => sessionRevision,
|
|
11337
|
+
onError: handleSessionNavigationError,
|
|
11338
|
+
reconcile: () => reconcileAccountRevision(hostGeneration),
|
|
11339
|
+
});
|
|
11340
|
+
const accountStatusPoll = createBrowserDevHostAccountStatusPoll({
|
|
11341
|
+
connectEvents: connectSessionEvents,
|
|
11342
|
+
invalidate: () => accountStatusDrain.invalidate(),
|
|
11343
|
+
isDisposed: () => disposed,
|
|
11344
|
+
maxDelayMs: SESSION_POLL_MAX_MS,
|
|
11345
|
+
minDelayMs: SESSION_POLL_MIN_MS,
|
|
11346
|
+
timers: {
|
|
11347
|
+
clearTimeout: (handle) => window.clearTimeout(handle),
|
|
11348
|
+
setTimeout: (callback, delay) => window.setTimeout(() => void callback(), delay),
|
|
11349
|
+
},
|
|
11350
|
+
});
|
|
11351
|
+
const permissionStatusDrain = createBrowserDevHostPermissionStatusDrain({
|
|
11352
|
+
applyCanonical: (configuration) => {
|
|
11353
|
+
applyPermissionConfiguration(configuration);
|
|
11354
|
+
return !runtimePermissionState || !isSamePermissionState(permissionState, runtimePermissionState);
|
|
11355
|
+
},
|
|
11356
|
+
equals: isSamePermissionState,
|
|
11357
|
+
fetchCanonical: (signal) => fetchPermissionConfiguration(combineAbortSignals(hostAbortController.signal, signal)),
|
|
11358
|
+
getCurrent: () => permissionState,
|
|
11359
|
+
onChanged: refreshRuntimeForCanonicalPermission,
|
|
11360
|
+
onError: () => {
|
|
11361
|
+
if (!disposed)
|
|
11362
|
+
pushLog('permission.sync.failed', { code: 'CANONICAL_PERMISSION_FETCH_FAILED' });
|
|
11363
|
+
},
|
|
11364
|
+
});
|
|
9204
11365
|
await loadPersistedPermissionConfiguration();
|
|
9205
11366
|
await refreshSessionStatus(false);
|
|
9206
|
-
|
|
9207
|
-
|
|
11367
|
+
renderDeveloperAccount();
|
|
11368
|
+
renderMiniProgramSummary(resolveDisplayedMiniProgram(bootstrap));
|
|
9208
11369
|
updateStorageSnapshot();
|
|
9209
|
-
elements.miniUrl.textContent = miniProgramUrl;
|
|
9210
11370
|
await restartRuntime('initial');
|
|
9211
|
-
|
|
11371
|
+
if (!uiPreview) {
|
|
11372
|
+
connectSessionEvents();
|
|
11373
|
+
connectBrowserChannel();
|
|
11374
|
+
}
|
|
9212
11375
|
setupControls();
|
|
9213
11376
|
const mobileAppQrReady = setupMobileAppQr();
|
|
9214
11377
|
if (bootstrap.devContext.source === 'remote') {
|
|
@@ -9220,25 +11383,17 @@ if (bootstrap.devContext.source === 'remote') {
|
|
|
9220
11383
|
}
|
|
9221
11384
|
function setupControls() {
|
|
9222
11385
|
window.addEventListener('beforeunload', disposeBrowserDevHost);
|
|
9223
|
-
elements.loginButton.addEventListener('click', () => {
|
|
9224
|
-
void beginLogin().catch(handleSessionNavigationError);
|
|
9225
|
-
});
|
|
9226
|
-
queryElement('#logout-button').addEventListener('click', () => void logout());
|
|
9227
|
-
queryElement('#show-button').addEventListener('click', () => activeRuntime?.show());
|
|
9228
|
-
queryElement('#hide-button').addEventListener('click', () => activeRuntime?.hide());
|
|
9229
|
-
elements.macAppButton.addEventListener('click', () => void openMacApp());
|
|
9230
|
-
elements.copyDebugPageUrlButton.addEventListener('click', () => void copyDebugPageUrl());
|
|
9231
11386
|
elements.mobileLanSelect.addEventListener('change', () => void updateMobileQrCode());
|
|
9232
|
-
elements.networkPermissionSelect.addEventListener('change', () => {
|
|
9233
|
-
void savePermissionOverride(elements.networkPermissionSelect.value === 'enabled' ? 'enabled' : 'disabled');
|
|
9234
|
-
});
|
|
9235
|
-
elements.resetPermissionsButton.addEventListener('click', () => void resetPermissionOverride());
|
|
9236
11387
|
}
|
|
9237
11388
|
async function restartRuntime(reason) {
|
|
9238
11389
|
const generation = ++runtimeGeneration;
|
|
9239
11390
|
destroyActiveRuntime(reason);
|
|
9240
|
-
|
|
11391
|
+
runtimePermissionState = clonePermissionState(permissionState);
|
|
9241
11392
|
elements.device.replaceChildren();
|
|
11393
|
+
if (uiPreview) {
|
|
11394
|
+
elements.device.innerHTML = '<div class="placeholder">未加载小程序</div>';
|
|
11395
|
+
return;
|
|
11396
|
+
}
|
|
9242
11397
|
const iframe = document.createElement('iframe');
|
|
9243
11398
|
iframe.allow = 'clipboard-read; clipboard-write';
|
|
9244
11399
|
elements.device.appendChild(iframe);
|
|
@@ -9260,11 +11415,10 @@ async function restartRuntime(reason) {
|
|
|
9260
11415
|
resolveLaunchConfiguration: async () => (await assembly.resolveLaunch()).configuration,
|
|
9261
11416
|
dispose: assembly.dispose,
|
|
9262
11417
|
onReady: () => {
|
|
9263
|
-
elements.bridgeStatus.textContent = 'ready';
|
|
9264
11418
|
pushLog('runtime.ready', { generation });
|
|
9265
11419
|
},
|
|
9266
11420
|
onStateChange: (state) => {
|
|
9267
|
-
|
|
11421
|
+
pushLog('runtime.state', { generation, state });
|
|
9268
11422
|
},
|
|
9269
11423
|
onError: (error) => pushLog('runtime.error', redactNetworkLogValue(error)),
|
|
9270
11424
|
onSecurityViolation: (event) => pushLog('runtime.security', redactNetworkLogValue(event)),
|
|
@@ -9275,7 +11429,6 @@ async function restartRuntime(reason) {
|
|
|
9275
11429
|
catch (error) {
|
|
9276
11430
|
if (disposed || generation !== runtimeGeneration || terminal.signal.aborted)
|
|
9277
11431
|
return;
|
|
9278
|
-
elements.bridgeStatus.textContent = 'failed';
|
|
9279
11432
|
pushLog('runtime.start.failed', redactNetworkLogValue(error));
|
|
9280
11433
|
}
|
|
9281
11434
|
}
|
|
@@ -9284,13 +11437,10 @@ function createWebHostInput(signal) {
|
|
|
9284
11437
|
signal,
|
|
9285
11438
|
account: {
|
|
9286
11439
|
async getCurrentUserId() {
|
|
9287
|
-
return (await refreshSessionStatus(false)).user_id;
|
|
11440
|
+
return (await refreshSessionStatus(false, signal)).user_id;
|
|
9288
11441
|
},
|
|
9289
11442
|
async ensureAuthenticated({ signal: operationSignal }) {
|
|
9290
|
-
|
|
9291
|
-
return;
|
|
9292
|
-
await beginLogin();
|
|
9293
|
-
await waitForAbort(operationSignal);
|
|
11443
|
+
await refreshSessionStatus(false, operationSignal);
|
|
9294
11444
|
},
|
|
9295
11445
|
onChange(listener) {
|
|
9296
11446
|
accountListeners.add(listener);
|
|
@@ -9299,29 +11449,12 @@ function createWebHostInput(signal) {
|
|
|
9299
11449
|
},
|
|
9300
11450
|
authenticatedRequest: requestFromWebHost,
|
|
9301
11451
|
storage: { area: 'local', storage: localStorage },
|
|
9302
|
-
presentation: {
|
|
9303
|
-
close: async () =>
|
|
9304
|
-
|
|
9305
|
-
getViewportMetrics: () => ({
|
|
9306
|
-
windowWidth: window.innerWidth,
|
|
9307
|
-
windowHeight: window.innerHeight,
|
|
9308
|
-
screenWidth: window.screen.width,
|
|
9309
|
-
screenHeight: window.screen.height,
|
|
9310
|
-
pixelRatio: window.devicePixelRatio,
|
|
9311
|
-
}),
|
|
9312
|
-
getNavigationBarHeight: () => 0,
|
|
9313
|
-
setNavigationBarStyle: (style) => {
|
|
9314
|
-
document.documentElement.dataset.navigationBarStyle = style;
|
|
9315
|
-
},
|
|
9316
|
-
showToast: async ({ message, status }) => showTransientMessage(message, status),
|
|
9317
|
-
showLoading: async () => {
|
|
9318
|
-
elements.bridgeStatus.textContent = 'loading';
|
|
9319
|
-
},
|
|
9320
|
-
hideLoading: async () => {
|
|
9321
|
-
elements.bridgeStatus.textContent = activeRuntime?.state ?? 'idle';
|
|
11452
|
+
presentation: createBrowserDevHostPresentation({
|
|
11453
|
+
close: async () => {
|
|
11454
|
+
activeRuntime?.destroy();
|
|
9322
11455
|
},
|
|
9323
|
-
|
|
9324
|
-
},
|
|
11456
|
+
pushLog,
|
|
11457
|
+
}),
|
|
9325
11458
|
environment: {
|
|
9326
11459
|
getCurrentHref: () => location.href,
|
|
9327
11460
|
isDevRuntime: () => true,
|
|
@@ -9338,11 +11471,7 @@ function createWebHostInput(signal) {
|
|
|
9338
11471
|
hostHints: { launchKind: devContext.source === 'remote' ? 'dev-session' : 'anonymous-local-dev' },
|
|
9339
11472
|
}),
|
|
9340
11473
|
authPresentation: {
|
|
9341
|
-
|
|
9342
|
-
if (operationSignal.aborted)
|
|
9343
|
-
throw operationSignal.reason;
|
|
9344
|
-
return window.confirm(`允许小程序访问 ${scopes.join('、')}?`) ? { status: 'granted', grantedScopes: scopes } : { status: 'cancelled' };
|
|
9345
|
-
},
|
|
11474
|
+
confirmAuthorization: (request) => confirmBrowserDevHostAuthorization(request),
|
|
9346
11475
|
},
|
|
9347
11476
|
};
|
|
9348
11477
|
}
|
|
@@ -9372,44 +11501,82 @@ async function requestFromWebHost(request) {
|
|
|
9372
11501
|
signal: request.signal,
|
|
9373
11502
|
});
|
|
9374
11503
|
if (!anonymousPayload && response.status === 401) {
|
|
9375
|
-
void
|
|
11504
|
+
void accountStatusDrain.invalidate();
|
|
9376
11505
|
}
|
|
9377
11506
|
const payload = (await response.json());
|
|
9378
11507
|
if (!response.ok)
|
|
9379
11508
|
throw payload;
|
|
9380
11509
|
return payload;
|
|
9381
11510
|
}
|
|
9382
|
-
async function
|
|
9383
|
-
|
|
9384
|
-
|
|
9385
|
-
|
|
9386
|
-
|
|
9387
|
-
|
|
11511
|
+
async function refreshSessionStatus(notify, signal) {
|
|
11512
|
+
const generation = hostGeneration;
|
|
11513
|
+
const status = await fetchSessionStatus(combineAbortSignals(hostAbortController.signal, signal));
|
|
11514
|
+
if (!isActiveHostGeneration(generation))
|
|
11515
|
+
return currentSessionStatus();
|
|
11516
|
+
return applySessionStatus(status, notify).status;
|
|
11517
|
+
}
|
|
11518
|
+
async function fetchSessionStatus(signal) {
|
|
11519
|
+
const response = await fetch(SESSION_STATUS_PATH, {
|
|
11520
|
+
cache: 'no-store',
|
|
11521
|
+
credentials: 'same-origin',
|
|
11522
|
+
redirect: 'error',
|
|
11523
|
+
signal,
|
|
9388
11524
|
});
|
|
9389
|
-
|
|
9390
|
-
|
|
9391
|
-
|
|
9392
|
-
|
|
9393
|
-
|
|
9394
|
-
|
|
9395
|
-
|
|
9396
|
-
|
|
9397
|
-
const
|
|
9398
|
-
|
|
9399
|
-
|
|
9400
|
-
|
|
9401
|
-
|
|
9402
|
-
|
|
9403
|
-
async function refreshSessionStatus(notify) {
|
|
9404
|
-
const response = await fetch(SESSION_STATUS_PATH, { cache: 'no-store', credentials: 'same-origin', redirect: 'error' });
|
|
9405
|
-
const status = (await response.json());
|
|
9406
|
-
const changed = status.revision !== sessionRevision || status.user_id !== sessionUserId;
|
|
11525
|
+
return (await response.json());
|
|
11526
|
+
}
|
|
11527
|
+
function applySessionStatus(status, notify) {
|
|
11528
|
+
if (!Number.isSafeInteger(status.revision) || status.revision < sessionRevision) {
|
|
11529
|
+
return { changed: false, status: currentSessionStatus() };
|
|
11530
|
+
}
|
|
11531
|
+
const nextUserId = status.authenticated ? status.user_id : undefined;
|
|
11532
|
+
const nextNickname = status.authenticated ? readOptionalText(status.nickname) : undefined;
|
|
11533
|
+
const nextAvatar = status.authenticated ? readOptionalText(status.avatar) : undefined;
|
|
11534
|
+
const changed = status.revision !== sessionRevision ||
|
|
11535
|
+
status.authenticated !== sessionAuthenticated ||
|
|
11536
|
+
nextUserId !== sessionUserId ||
|
|
11537
|
+
nextNickname !== sessionUserNickname ||
|
|
11538
|
+
nextAvatar !== sessionUserAvatar;
|
|
9407
11539
|
sessionRevision = status.revision;
|
|
9408
|
-
|
|
9409
|
-
|
|
11540
|
+
sessionAuthenticated = status.authenticated;
|
|
11541
|
+
sessionUserId = nextUserId;
|
|
11542
|
+
sessionUserNickname = nextNickname;
|
|
11543
|
+
sessionUserAvatar = nextAvatar;
|
|
11544
|
+
renderDeveloperAccount();
|
|
9410
11545
|
if (notify && changed)
|
|
9411
11546
|
accountListeners.forEach((listener) => listener(sessionUserId));
|
|
9412
|
-
return status;
|
|
11547
|
+
return { changed, status: currentSessionStatus() };
|
|
11548
|
+
}
|
|
11549
|
+
function currentSessionStatus() {
|
|
11550
|
+
return {
|
|
11551
|
+
authenticated: sessionAuthenticated,
|
|
11552
|
+
revision: sessionRevision,
|
|
11553
|
+
...(sessionAuthenticated && sessionUserId ? { user_id: sessionUserId } : {}),
|
|
11554
|
+
};
|
|
11555
|
+
}
|
|
11556
|
+
function connectBrowserChannel() {
|
|
11557
|
+
if (!bootstrap.csrfToken || disposed)
|
|
11558
|
+
return;
|
|
11559
|
+
try {
|
|
11560
|
+
const channelUrl = new URL('/__hb_sdk__/channel', location.href);
|
|
11561
|
+
channelUrl.protocol = channelUrl.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
11562
|
+
channelClient = createBrowserDevHostChannelClient({
|
|
11563
|
+
csrfToken: bootstrap.csrfToken,
|
|
11564
|
+
onDiagnostic: (diagnostic) => pushLog('channel.diagnostic', diagnostic),
|
|
11565
|
+
url: channelUrl.toString(),
|
|
11566
|
+
});
|
|
11567
|
+
channelClient.subscribeUpdates((snapshot, update) => applyBrowserChannelSnapshot(snapshot, update));
|
|
11568
|
+
channelClient.connect();
|
|
11569
|
+
}
|
|
11570
|
+
catch {
|
|
11571
|
+
channelClient?.dispose();
|
|
11572
|
+
channelClient = undefined;
|
|
11573
|
+
}
|
|
11574
|
+
}
|
|
11575
|
+
function applyBrowserChannelSnapshot(snapshot, update) {
|
|
11576
|
+
void accountStatusDrain.invalidate(snapshot.account.revision);
|
|
11577
|
+
void permissionStatusDrain.invalidate(snapshot.permissions, {
|
|
11578
|
+
force: update.type === 'event' && update.event === 'status.changed',
|
|
11579
|
+
});
|
|
9413
11580
|
}
|
|
9414
11581
|
function connectSessionEvents() {
|
|
9415
11582
|
sessionEventSource?.close();
|
|
@@ -9418,52 +11585,20 @@ function connectSessionEvents() {
|
|
|
9418
11585
|
return;
|
|
9419
11586
|
const source = new EventSource(SESSION_EVENTS_PATH, { withCredentials: true });
|
|
9420
11587
|
sessionEventSource = source;
|
|
9421
|
-
source.addEventListener('account-invalidated', () => void
|
|
9422
|
-
source.addEventListener('account-revision', () => void
|
|
11588
|
+
source.addEventListener('account-invalidated', () => void accountStatusDrain.invalidate());
|
|
11589
|
+
source.addEventListener('account-revision', () => void accountStatusDrain.invalidate());
|
|
9423
11590
|
source.onopen = () => {
|
|
9424
|
-
|
|
9425
|
-
clearSessionPoll();
|
|
11591
|
+
accountStatusPoll.reset();
|
|
9426
11592
|
};
|
|
9427
11593
|
source.onerror = () => {
|
|
9428
11594
|
source.close();
|
|
9429
|
-
|
|
11595
|
+
accountStatusPoll.schedule();
|
|
9430
11596
|
};
|
|
9431
11597
|
}
|
|
9432
|
-
function
|
|
9433
|
-
|
|
9434
|
-
accountRevisionTask = undefined;
|
|
9435
|
-
}));
|
|
9436
|
-
return accountRevisionTask;
|
|
9437
|
-
}
|
|
9438
|
-
async function processAccountRevision() {
|
|
9439
|
-
const previousRevision = sessionRevision;
|
|
9440
|
-
const status = await refreshSessionStatus(true);
|
|
9441
|
-
if (status.revision === previousRevision)
|
|
11598
|
+
async function reconcileAccountRevision(generation) {
|
|
11599
|
+
if (!isActiveHostGeneration(generation))
|
|
9442
11600
|
return;
|
|
9443
|
-
|
|
9444
|
-
if (status.authenticated)
|
|
9445
|
-
await restartRuntime('account revision changed');
|
|
9446
|
-
else
|
|
9447
|
-
await beginLogin();
|
|
9448
|
-
}
|
|
9449
|
-
function scheduleSessionPoll() {
|
|
9450
|
-
clearSessionPoll();
|
|
9451
|
-
sessionPollTimer = window.setTimeout(async () => {
|
|
9452
|
-
try {
|
|
9453
|
-
await handleAccountRevision();
|
|
9454
|
-
sessionPollDelay = SESSION_POLL_MIN_MS;
|
|
9455
|
-
connectSessionEvents();
|
|
9456
|
-
}
|
|
9457
|
-
catch {
|
|
9458
|
-
sessionPollDelay = Math.min(sessionPollDelay * 2, SESSION_POLL_MAX_MS);
|
|
9459
|
-
scheduleSessionPoll();
|
|
9460
|
-
}
|
|
9461
|
-
}, sessionPollDelay);
|
|
9462
|
-
}
|
|
9463
|
-
function clearSessionPoll() {
|
|
9464
|
-
if (sessionPollTimer !== undefined)
|
|
9465
|
-
window.clearTimeout(sessionPollTimer);
|
|
9466
|
-
sessionPollTimer = undefined;
|
|
11601
|
+
await restartRuntime('account revision changed');
|
|
9467
11602
|
}
|
|
9468
11603
|
function destroyActiveRuntime(reason) {
|
|
9469
11604
|
activeRuntime?.destroy();
|
|
@@ -9475,76 +11610,12 @@ function destroyActiveRuntime(reason) {
|
|
|
9475
11610
|
}
|
|
9476
11611
|
async function loadPersistedPermissionConfiguration() {
|
|
9477
11612
|
try {
|
|
9478
|
-
const
|
|
9479
|
-
const configuration = readPermissionConfiguration(await response.json());
|
|
9480
|
-
if (!response.ok || !configuration)
|
|
9481
|
-
throw new Error('权限持久化配置加载失败');
|
|
11613
|
+
const configuration = await fetchPermissionConfiguration(hostAbortController.signal);
|
|
9482
11614
|
applyPermissionConfiguration(configuration);
|
|
9483
11615
|
}
|
|
9484
11616
|
catch {
|
|
9485
|
-
|
|
9486
|
-
}
|
|
9487
|
-
}
|
|
9488
|
-
async function savePermissionOverride(status) {
|
|
9489
|
-
setPermissionControlsDisabled(true);
|
|
9490
|
-
permissionState = { networkRequest: { ...permissionState.networkRequest, status } };
|
|
9491
|
-
hasPermissionOverride = true;
|
|
9492
|
-
permissionActionMessage = '正在保存本地权限覆盖...';
|
|
9493
|
-
updatePermissionControls();
|
|
9494
|
-
try {
|
|
9495
|
-
const response = await requestJson(DEV_PERMISSIONS_PATH, {
|
|
9496
|
-
method: 'PUT',
|
|
9497
|
-
body: JSON.stringify({ runtime_permissions: createBrowserDevRuntimePermissions(permissionState) }),
|
|
9498
|
-
});
|
|
9499
|
-
const configuration = readPermissionConfiguration(await response.json());
|
|
9500
|
-
if (!response.ok || !configuration)
|
|
9501
|
-
throw new Error('保存失败');
|
|
9502
|
-
applyPermissionConfiguration(configuration, configuration.persistenceError || '本地权限覆盖已保存。');
|
|
9503
|
-
await restartRuntime('permission changed');
|
|
9504
|
-
void updateMobileQrCode();
|
|
9505
|
-
}
|
|
9506
|
-
catch (error) {
|
|
9507
|
-
permissionActionMessage = `${readErrorText(error)},当前页面仍会应用该权限,但刷新或重启后会丢失。`;
|
|
9508
|
-
}
|
|
9509
|
-
finally {
|
|
9510
|
-
setPermissionControlsDisabled(false);
|
|
9511
|
-
updatePermissionControls();
|
|
9512
|
-
}
|
|
9513
|
-
}
|
|
9514
|
-
async function resetPermissionOverride() {
|
|
9515
|
-
setPermissionControlsDisabled(true);
|
|
9516
|
-
try {
|
|
9517
|
-
const response = await requestJson(DEV_PERMISSIONS_PATH, { method: 'DELETE', body: '{}' });
|
|
9518
|
-
const configuration = readPermissionConfiguration(await response.json());
|
|
9519
|
-
if (!response.ok || !configuration)
|
|
9520
|
-
throw new Error('重置失败');
|
|
9521
|
-
applyPermissionConfiguration(configuration, configuration.source === 'remote' ? '已重新读取远端权限并清除本地覆盖。' : '已清除本地权限覆盖。');
|
|
9522
|
-
await restartRuntime('permission reset');
|
|
9523
|
-
void updateMobileQrCode();
|
|
11617
|
+
/* 权限覆盖读取失败时继续使用当前内存态。 */
|
|
9524
11618
|
}
|
|
9525
|
-
catch (error) {
|
|
9526
|
-
permissionActionMessage = `重置失败:${readErrorText(error)}`;
|
|
9527
|
-
}
|
|
9528
|
-
finally {
|
|
9529
|
-
setPermissionControlsDisabled(false);
|
|
9530
|
-
updatePermissionControls();
|
|
9531
|
-
}
|
|
9532
|
-
}
|
|
9533
|
-
function updatePermissionControls() {
|
|
9534
|
-
elements.devContextStatus.textContent =
|
|
9535
|
-
devContext.source === 'remote' ? `已连接线上配置${devContext.miniProgramId ? ` · ${devContext.miniProgramId}` : ''}` : '匿名本地沙箱';
|
|
9536
|
-
elements.networkPermissionSelect.value = permissionState.networkRequest.status;
|
|
9537
|
-
const remoteLabel = readPermissionStatusLabel(remotePermissionState.networkRequest.status);
|
|
9538
|
-
const localLabel = readPermissionStatusLabel(permissionState.networkRequest.status);
|
|
9539
|
-
const sourceDescription = hasPermissionOverride
|
|
9540
|
-
? `远端基线:${remoteLabel} · 本地覆盖:${localLabel}`
|
|
9541
|
-
: `${devContext.source === 'remote' ? '当前使用远端权限' : '当前使用匿名默认权限'}:${remoteLabel}`;
|
|
9542
|
-
elements.permissionPersistenceStatus.textContent = permissionActionMessage
|
|
9543
|
-
? `${sourceDescription} · ${permissionActionMessage}`
|
|
9544
|
-
: sourceDescription;
|
|
9545
|
-
elements.releaseDiagnostics.innerHTML = createMiniProgramDevReleaseDiagnostics(devContext, permissionState)
|
|
9546
|
-
.map((diagnostic) => `<li>${escapeHtml(diagnostic.message)}</li>`)
|
|
9547
|
-
.join('');
|
|
9548
11619
|
}
|
|
9549
11620
|
function readPermissionConfiguration(value) {
|
|
9550
11621
|
if (!isRecord(value) || (value.source !== 'anonymous' && value.source !== 'remote'))
|
|
@@ -9561,15 +11632,33 @@ function readPermissionConfiguration(value) {
|
|
|
9561
11632
|
persistenceError: typeof value.persistence_error === 'string' ? value.persistence_error : undefined,
|
|
9562
11633
|
};
|
|
9563
11634
|
}
|
|
9564
|
-
function
|
|
11635
|
+
async function fetchPermissionConfiguration(signal) {
|
|
11636
|
+
const response = await fetch(DEV_PERMISSIONS_PATH, {
|
|
11637
|
+
cache: 'no-store',
|
|
11638
|
+
credentials: 'same-origin',
|
|
11639
|
+
redirect: 'error',
|
|
11640
|
+
signal,
|
|
11641
|
+
});
|
|
11642
|
+
const configuration = readPermissionConfiguration(await response.json());
|
|
11643
|
+
if (!response.ok || !configuration)
|
|
11644
|
+
throw new Error('权限持久化配置加载失败');
|
|
11645
|
+
return configuration;
|
|
11646
|
+
}
|
|
11647
|
+
function applyPermissionConfiguration(configuration) {
|
|
9565
11648
|
devContext =
|
|
9566
11649
|
configuration.source === 'remote'
|
|
9567
11650
|
? { source: 'remote', miniProgramId: configuration.miniProgramId, runtimePermissions: configuration.remotePermissions }
|
|
9568
11651
|
: { source: 'anonymous', warning: devContext.warning };
|
|
9569
11652
|
remotePermissionState = clonePermissionState(configuration.remoteState);
|
|
9570
11653
|
permissionState = clonePermissionState(configuration.overrideState ?? configuration.remoteState);
|
|
9571
|
-
|
|
9572
|
-
|
|
11654
|
+
}
|
|
11655
|
+
async function refreshRuntimeForCanonicalPermission() {
|
|
11656
|
+
if (disposed)
|
|
11657
|
+
return;
|
|
11658
|
+
await restartRuntime('permission changed');
|
|
11659
|
+
if (disposed)
|
|
11660
|
+
return;
|
|
11661
|
+
await updateMobileQrCode();
|
|
9573
11662
|
}
|
|
9574
11663
|
async function loadBootstrap() {
|
|
9575
11664
|
const response = await fetch(BOOTSTRAP_PATH, { cache: 'no-store', credentials: 'same-origin', redirect: 'error' });
|
|
@@ -9578,6 +11667,10 @@ async function loadBootstrap() {
|
|
|
9578
11667
|
const value = await response.json();
|
|
9579
11668
|
if (!isRecord(value))
|
|
9580
11669
|
throw new Error('Browser Dev Host bootstrap is invalid');
|
|
11670
|
+
const mobileLaunchRedirect = readMobileLaunchRedirectEntry(value.mobileLaunchRedirect);
|
|
11671
|
+
if (Object.hasOwn(value, 'mobileLaunchRedirect') && value.mobileLaunchRedirect !== undefined && !mobileLaunchRedirect) {
|
|
11672
|
+
throw new InvalidMobileLaunchRedirectError();
|
|
11673
|
+
}
|
|
9581
11674
|
return {
|
|
9582
11675
|
csrfToken: typeof value.csrfToken === 'string' ? value.csrfToken : undefined,
|
|
9583
11676
|
defaultLanAddressId: typeof value.defaultLanAddressId === 'string' ? value.defaultLanAddressId : undefined,
|
|
@@ -9585,6 +11678,8 @@ async function loadBootstrap() {
|
|
|
9585
11678
|
lanAddresses: Array.isArray(value.lanAddresses) ? value.lanAddresses.filter(isLanAddress) : [],
|
|
9586
11679
|
loginAvailable: value.loginAvailable === true,
|
|
9587
11680
|
macAppProtocol: typeof value.macAppProtocol === 'string' ? value.macAppProtocol : undefined,
|
|
11681
|
+
miniProgram: readMiniProgramSummary(value.miniProgram),
|
|
11682
|
+
mobileLaunchRedirect,
|
|
9588
11683
|
nativeAppLaunchUnavailableReason: typeof value.nativeAppLaunchUnavailableReason === 'string' ? value.nativeAppLaunchUnavailableReason : undefined,
|
|
9589
11684
|
};
|
|
9590
11685
|
}
|
|
@@ -9611,7 +11706,7 @@ async function setupMobileAppQr() {
|
|
|
9611
11706
|
setupMobileLanSelect();
|
|
9612
11707
|
await updateMobileQrCode();
|
|
9613
11708
|
}
|
|
9614
|
-
function setupMobileLanSelect() {
|
|
11709
|
+
function setupMobileLanSelect(preferredLanAddressId = elements.mobileLanSelect.options.length > 0 ? elements.mobileLanSelect.value : undefined) {
|
|
9615
11710
|
elements.mobileLanSelect.replaceChildren();
|
|
9616
11711
|
for (const address of lanAddresses) {
|
|
9617
11712
|
const option = document.createElement('option');
|
|
@@ -9619,8 +11714,15 @@ function setupMobileLanSelect() {
|
|
|
9619
11714
|
option.textContent = `${address.name} · ${address.address}`;
|
|
9620
11715
|
elements.mobileLanSelect.appendChild(option);
|
|
9621
11716
|
}
|
|
9622
|
-
if (
|
|
11717
|
+
if (preferredLanAddressId && lanAddresses.some(({ id }) => id === preferredLanAddressId)) {
|
|
11718
|
+
elements.mobileLanSelect.value = preferredLanAddressId;
|
|
11719
|
+
}
|
|
11720
|
+
else if (defaultLanAddressId && lanAddresses.some(({ id }) => id === defaultLanAddressId)) {
|
|
9623
11721
|
elements.mobileLanSelect.value = defaultLanAddressId;
|
|
11722
|
+
}
|
|
11723
|
+
else if (lanAddresses[0]) {
|
|
11724
|
+
elements.mobileLanSelect.value = lanAddresses[0].id;
|
|
11725
|
+
}
|
|
9624
11726
|
elements.mobileLanSelect.disabled = lanAddresses.length === 0;
|
|
9625
11727
|
}
|
|
9626
11728
|
async function updateMobileQrCode() {
|
|
@@ -9639,21 +11741,101 @@ async function updateMobileQrCode() {
|
|
|
9639
11741
|
const devSession = await createMobileDevSession(selected);
|
|
9640
11742
|
if (generation !== mobileQrGeneration || disposed)
|
|
9641
11743
|
return;
|
|
9642
|
-
const
|
|
9643
|
-
|
|
11744
|
+
const qr = await resolveDisplayedMobileQr(devSession.address, devSession.expiresAt);
|
|
11745
|
+
if (generation !== mobileQrGeneration || disposed)
|
|
11746
|
+
return;
|
|
11747
|
+
const imageUrl = await browserExports.toDataURL(qr.payload, { width: MOBILE_QR_IMAGE_SIZE, margin: 1 });
|
|
9644
11748
|
if (generation !== mobileQrGeneration || disposed)
|
|
9645
11749
|
return;
|
|
9646
11750
|
elements.mobileQrImage.src = imageUrl;
|
|
9647
|
-
elements.mobileQrStatus.textContent = `${
|
|
9648
|
-
scheduleMobileQrRefresh(
|
|
11751
|
+
elements.mobileQrStatus.textContent = `${devSession.address.name} · ${devSession.address.address}`;
|
|
11752
|
+
scheduleMobileQrRefresh(qr.expiresAt, generation);
|
|
9649
11753
|
}
|
|
9650
11754
|
catch (error) {
|
|
9651
11755
|
if (generation !== mobileQrGeneration || disposed)
|
|
9652
11756
|
return;
|
|
11757
|
+
if (error instanceof InvalidMobileLaunchRedirectError) {
|
|
11758
|
+
invalidateMobileLaunchRedirectQr();
|
|
11759
|
+
return;
|
|
11760
|
+
}
|
|
9653
11761
|
elements.mobileQrImage.removeAttribute('src');
|
|
9654
11762
|
elements.mobileQrStatus.textContent = readErrorText(error);
|
|
9655
11763
|
}
|
|
9656
11764
|
}
|
|
11765
|
+
async function resolveDisplayedMobileQr(address, sessionExpiresAt) {
|
|
11766
|
+
const payload = address.mobileAppQrPayload;
|
|
11767
|
+
if (!payload)
|
|
11768
|
+
throw new Error('Mobile 启动配置不可用');
|
|
11769
|
+
const redirect = await ensureMobileLaunchRedirect(address, payload);
|
|
11770
|
+
if (!redirect) {
|
|
11771
|
+
return { expiresAt: sessionExpiresAt, payload };
|
|
11772
|
+
}
|
|
11773
|
+
try {
|
|
11774
|
+
return {
|
|
11775
|
+
expiresAt: Math.min(sessionExpiresAt, redirect.expiresAt),
|
|
11776
|
+
payload: createMobileAppRedirectQrPayload(createMobileLaunchRedirectHttpUrl({
|
|
11777
|
+
host: address.address,
|
|
11778
|
+
port: location.port,
|
|
11779
|
+
token: redirect.token,
|
|
11780
|
+
})),
|
|
11781
|
+
};
|
|
11782
|
+
}
|
|
11783
|
+
catch {
|
|
11784
|
+
throw new InvalidMobileLaunchRedirectError();
|
|
11785
|
+
}
|
|
11786
|
+
}
|
|
11787
|
+
async function ensureMobileLaunchRedirect(address, payload) {
|
|
11788
|
+
if (mobileLaunchRedirect?.addressId === address.id)
|
|
11789
|
+
return mobileLaunchRedirect;
|
|
11790
|
+
if (!mobileLaunchRedirect)
|
|
11791
|
+
return undefined;
|
|
11792
|
+
const response = await requestJson(MOBILE_LAUNCH_REDIRECT_PATH, {
|
|
11793
|
+
method: 'POST',
|
|
11794
|
+
body: JSON.stringify({
|
|
11795
|
+
address_id: address.id,
|
|
11796
|
+
payload,
|
|
11797
|
+
previous_token: mobileLaunchRedirect.token,
|
|
11798
|
+
}),
|
|
11799
|
+
});
|
|
11800
|
+
if (!response.ok)
|
|
11801
|
+
throw new InvalidMobileLaunchRedirectError();
|
|
11802
|
+
const value = await response.json();
|
|
11803
|
+
const entry = isRecord(value)
|
|
11804
|
+
? readMobileLaunchRedirectEntry({
|
|
11805
|
+
addressId: value.address_id,
|
|
11806
|
+
expiresAt: value.expires_at,
|
|
11807
|
+
token: value.token,
|
|
11808
|
+
})
|
|
11809
|
+
: undefined;
|
|
11810
|
+
if (!entry || entry.addressId !== address.id)
|
|
11811
|
+
throw new InvalidMobileLaunchRedirectError();
|
|
11812
|
+
mobileLaunchRedirect = entry;
|
|
11813
|
+
return entry;
|
|
11814
|
+
}
|
|
11815
|
+
async function reloadNativeLaunchConfigForDevSession(selectedAddressId) {
|
|
11816
|
+
const latest = await loadBootstrap();
|
|
11817
|
+
const address = readLatestLanAddress(latest, selectedAddressId);
|
|
11818
|
+
const payload = address?.mobileAppQrPayload;
|
|
11819
|
+
if (!address || !payload)
|
|
11820
|
+
throw new Error('Mobile 启动配置不可用');
|
|
11821
|
+
applyLatestNativeLaunchConfig(latest);
|
|
11822
|
+
return { ...address, mobileAppQrPayload: payload };
|
|
11823
|
+
}
|
|
11824
|
+
function readLatestLanAddress(latest, selectedAddressId) {
|
|
11825
|
+
return (latest.lanAddresses.find(({ id }) => id === selectedAddressId) ??
|
|
11826
|
+
latest.lanAddresses.find(({ id }) => id === latest.defaultLanAddressId) ??
|
|
11827
|
+
latest.lanAddresses[0]);
|
|
11828
|
+
}
|
|
11829
|
+
function applyLatestNativeLaunchConfig(latest) {
|
|
11830
|
+
nativeLaunchConfigSignature = readNativeLaunchConfigSignature(latest);
|
|
11831
|
+
lanAddresses = latest.lanAddresses;
|
|
11832
|
+
defaultLanAddressId = latest.defaultLanAddressId;
|
|
11833
|
+
mobileLaunchRedirect = latest.mobileLaunchRedirect;
|
|
11834
|
+
renderMiniProgramSummary(resolveDisplayedMiniProgram(latest));
|
|
11835
|
+
setupMobileLanSelect();
|
|
11836
|
+
}
|
|
11837
|
+
class InvalidMobileLaunchRedirectError extends Error {
|
|
11838
|
+
}
|
|
9657
11839
|
async function createMobileDevSession(selected) {
|
|
9658
11840
|
const creation = mobileDevSessionCreation.then(() => requestMobileDevSession(selected));
|
|
9659
11841
|
mobileDevSessionCreation = creation.then(() => undefined, () => undefined);
|
|
@@ -9661,22 +11843,20 @@ async function createMobileDevSession(selected) {
|
|
|
9661
11843
|
}
|
|
9662
11844
|
async function requestMobileDevSession(selected) {
|
|
9663
11845
|
const hostUrl = new URL(selected.appUrl);
|
|
9664
|
-
const
|
|
9665
|
-
const response = await requestJson(
|
|
11846
|
+
const endpointUrl = new URL(MINI_PROGRAM_DEV_CONTEXT_PATH, location.origin);
|
|
11847
|
+
const response = await requestJson(endpointUrl.pathname, {
|
|
9666
11848
|
method: 'POST',
|
|
9667
|
-
body: JSON.stringify({
|
|
11849
|
+
body: JSON.stringify({
|
|
11850
|
+
mini_url_origin: hostUrl.origin,
|
|
11851
|
+
runtime_permissions: createMobileDevSessionRuntimePermissions(permissionState),
|
|
11852
|
+
}),
|
|
9668
11853
|
});
|
|
9669
11854
|
const value = await response.json();
|
|
9670
11855
|
if (!response.ok || !isRecord(value) || typeof value.token !== 'string' || typeof value.expires_at !== 'number') {
|
|
9671
11856
|
throw new Error('创建 Mobile Dev Session 失败');
|
|
9672
11857
|
}
|
|
9673
|
-
|
|
9674
|
-
return {
|
|
9675
|
-
}
|
|
9676
|
-
function applyDevContextToMobileQrPayload(payload, devContextUrl) {
|
|
9677
|
-
const target = new URL(payload);
|
|
9678
|
-
target.searchParams.set(MINI_PROGRAM_DEV_CONTEXT_QUERY_PARAM, devContextUrl);
|
|
9679
|
-
return target.toString();
|
|
11858
|
+
const latest = await reloadNativeLaunchConfigForDevSession(selected.id);
|
|
11859
|
+
return { address: latest, expiresAt: value.expires_at };
|
|
9680
11860
|
}
|
|
9681
11861
|
function scheduleMobileQrRefresh(expiresAt, generation) {
|
|
9682
11862
|
mobileQrRefreshTimer = window.setTimeout(() => {
|
|
@@ -9693,51 +11873,112 @@ async function refreshNativeLaunchConfig() {
|
|
|
9693
11873
|
const signature = readNativeLaunchConfigSignature(latest);
|
|
9694
11874
|
if (signature === nativeLaunchConfigSignature)
|
|
9695
11875
|
return;
|
|
9696
|
-
|
|
9697
|
-
lanAddresses = latest.lanAddresses;
|
|
9698
|
-
defaultLanAddressId = latest.defaultLanAddressId;
|
|
9699
|
-
macAppProtocol = latest.macAppProtocol;
|
|
9700
|
-
nativeAppLaunchUnavailableReason = latest.nativeAppLaunchUnavailableReason;
|
|
9701
|
-
setupMobileLanSelect();
|
|
9702
|
-
updateNativeAppLaunchAvailability();
|
|
11876
|
+
applyLatestNativeLaunchConfig(latest);
|
|
9703
11877
|
void updateMobileQrCode();
|
|
9704
11878
|
}
|
|
11879
|
+
catch (error) {
|
|
11880
|
+
if (error instanceof InvalidMobileLaunchRedirectError)
|
|
11881
|
+
invalidateMobileLaunchRedirectQr();
|
|
11882
|
+
}
|
|
9705
11883
|
finally {
|
|
9706
11884
|
nativeLaunchConfigRefreshInFlight = false;
|
|
9707
11885
|
}
|
|
9708
11886
|
}
|
|
11887
|
+
function invalidateMobileLaunchRedirectQr() {
|
|
11888
|
+
mobileQrGeneration += 1;
|
|
11889
|
+
clearMobileQrRefreshTimer();
|
|
11890
|
+
elements.mobileQrImage.removeAttribute('src');
|
|
11891
|
+
elements.mobileQrStatus.textContent = 'Mobile 短链接配置无效';
|
|
11892
|
+
}
|
|
9709
11893
|
function readNativeLaunchConfigSignature(value) {
|
|
9710
11894
|
return JSON.stringify({
|
|
9711
11895
|
defaultLanAddressId: value.defaultLanAddressId,
|
|
9712
11896
|
lanAddresses: value.lanAddresses,
|
|
9713
11897
|
macAppProtocol: value.macAppProtocol,
|
|
11898
|
+
mobileLaunchRedirect: value.mobileLaunchRedirect
|
|
11899
|
+
? {
|
|
11900
|
+
addressId: value.mobileLaunchRedirect.addressId,
|
|
11901
|
+
expiresAt: value.mobileLaunchRedirect.expiresAt,
|
|
11902
|
+
token: value.mobileLaunchRedirect.token,
|
|
11903
|
+
}
|
|
11904
|
+
: undefined,
|
|
9714
11905
|
nativeAppLaunchUnavailableReason: value.nativeAppLaunchUnavailableReason,
|
|
9715
11906
|
});
|
|
9716
11907
|
}
|
|
9717
|
-
|
|
9718
|
-
if (
|
|
9719
|
-
|
|
9720
|
-
|
|
9721
|
-
|
|
9722
|
-
|
|
9723
|
-
|
|
9724
|
-
|
|
9725
|
-
|
|
9726
|
-
|
|
9727
|
-
|
|
9728
|
-
|
|
9729
|
-
|
|
9730
|
-
|
|
9731
|
-
|
|
9732
|
-
|
|
9733
|
-
|
|
9734
|
-
|
|
9735
|
-
|
|
9736
|
-
|
|
9737
|
-
|
|
9738
|
-
|
|
9739
|
-
|
|
9740
|
-
|
|
11908
|
+
function renderDeveloperAccount() {
|
|
11909
|
+
if (!sessionAuthenticated) {
|
|
11910
|
+
elements.developerAvatar.hidden = true;
|
|
11911
|
+
elements.developerAvatar.removeAttribute('src');
|
|
11912
|
+
elements.developerName.textContent = '未登录';
|
|
11913
|
+
elements.developerHint.textContent = '请运行 hb-sdk login';
|
|
11914
|
+
return;
|
|
11915
|
+
}
|
|
11916
|
+
elements.developerName.textContent = sessionUserNickname || sessionUserId || '已登录';
|
|
11917
|
+
elements.developerHint.textContent = sessionUserId ? `heybox_id ${sessionUserId}` : '';
|
|
11918
|
+
if (sessionUserAvatar) {
|
|
11919
|
+
elements.developerAvatar.hidden = false;
|
|
11920
|
+
elements.developerAvatar.src = sessionUserAvatar;
|
|
11921
|
+
elements.developerAvatar.alt = elements.developerName.textContent;
|
|
11922
|
+
}
|
|
11923
|
+
else {
|
|
11924
|
+
elements.developerAvatar.hidden = true;
|
|
11925
|
+
elements.developerAvatar.removeAttribute('src');
|
|
11926
|
+
elements.developerAvatar.alt = '';
|
|
11927
|
+
}
|
|
11928
|
+
}
|
|
11929
|
+
function resolveDisplayedMiniProgram(source) {
|
|
11930
|
+
if (source.miniProgram?.miniProgramId)
|
|
11931
|
+
return source.miniProgram;
|
|
11932
|
+
const miniProgramId = source.devContext.miniProgramId?.trim();
|
|
11933
|
+
return miniProgramId ? { miniProgramId } : source.miniProgram ?? null;
|
|
11934
|
+
}
|
|
11935
|
+
function renderMiniProgramSummary(summary) {
|
|
11936
|
+
const bound = Boolean(summary?.miniProgramId);
|
|
11937
|
+
elements.miniProgramSummary.hidden = !bound;
|
|
11938
|
+
elements.miniProgramEmpty.hidden = bound;
|
|
11939
|
+
if (!bound || !summary) {
|
|
11940
|
+
elements.miniProgramIcon.hidden = true;
|
|
11941
|
+
elements.miniProgramIcon.removeAttribute('src');
|
|
11942
|
+
elements.miniProgramName.textContent = '';
|
|
11943
|
+
elements.miniProgramId.textContent = '';
|
|
11944
|
+
return;
|
|
11945
|
+
}
|
|
11946
|
+
elements.miniProgramName.textContent = summary.name || summary.miniProgramId;
|
|
11947
|
+
elements.miniProgramId.textContent = [summary.miniProgramId, summary.version, summary.status].filter(Boolean).join(' · ');
|
|
11948
|
+
if (summary.iconUrl) {
|
|
11949
|
+
elements.miniProgramIcon.hidden = false;
|
|
11950
|
+
elements.miniProgramIcon.src = summary.iconUrl;
|
|
11951
|
+
elements.miniProgramIcon.alt = summary.name || summary.miniProgramId;
|
|
11952
|
+
}
|
|
11953
|
+
else {
|
|
11954
|
+
elements.miniProgramIcon.hidden = true;
|
|
11955
|
+
elements.miniProgramIcon.removeAttribute('src');
|
|
11956
|
+
elements.miniProgramIcon.alt = '';
|
|
11957
|
+
}
|
|
11958
|
+
}
|
|
11959
|
+
function readMiniProgramSummary(value) {
|
|
11960
|
+
if (value === null)
|
|
11961
|
+
return null;
|
|
11962
|
+
if (!isRecord(value))
|
|
11963
|
+
return undefined;
|
|
11964
|
+
const miniProgramId = readOptionalText(value.miniProgramId) ?? readOptionalText(value.mini_program_id);
|
|
11965
|
+
if (!miniProgramId)
|
|
11966
|
+
return undefined;
|
|
11967
|
+
return {
|
|
11968
|
+
miniProgramId,
|
|
11969
|
+
...(readOptionalText(value.name) ? { name: readOptionalText(value.name) } : {}),
|
|
11970
|
+
...(readOptionalText(value.iconUrl) ?? readOptionalText(value.icon_url)
|
|
11971
|
+
? { iconUrl: readOptionalText(value.iconUrl) ?? readOptionalText(value.icon_url) }
|
|
11972
|
+
: {}),
|
|
11973
|
+
...(readOptionalText(value.status) ? { status: readOptionalText(value.status) } : {}),
|
|
11974
|
+
...(readOptionalText(value.version) ? { version: readOptionalText(value.version) } : {}),
|
|
11975
|
+
...(readOptionalText(value.developerName) ?? readOptionalText(value.developer_name)
|
|
11976
|
+
? { developerName: readOptionalText(value.developerName) ?? readOptionalText(value.developer_name) }
|
|
11977
|
+
: {}),
|
|
11978
|
+
};
|
|
11979
|
+
}
|
|
11980
|
+
function readOptionalText(value) {
|
|
11981
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
9741
11982
|
}
|
|
9742
11983
|
function updateStorageSnapshot() {
|
|
9743
11984
|
const snapshot = {};
|
|
@@ -9752,6 +11993,8 @@ function showTransientMessage(message, status) {
|
|
|
9752
11993
|
pushLog('host.message', { message, status });
|
|
9753
11994
|
}
|
|
9754
11995
|
function handleSessionNavigationError(error) {
|
|
11996
|
+
if (disposed)
|
|
11997
|
+
return;
|
|
9755
11998
|
showTransientMessage(readErrorText(error), 'error');
|
|
9756
11999
|
}
|
|
9757
12000
|
function pushLog(method, detail) {
|
|
@@ -9766,10 +12009,16 @@ function disposeBrowserDevHost() {
|
|
|
9766
12009
|
if (disposed)
|
|
9767
12010
|
return;
|
|
9768
12011
|
disposed = true;
|
|
12012
|
+
hostGeneration += 1;
|
|
12013
|
+
hostAbortController.abort(new DOMException('Browser Dev Host disposed', 'AbortError'));
|
|
12014
|
+
accountStatusDrain.dispose();
|
|
12015
|
+
permissionStatusDrain.dispose();
|
|
9769
12016
|
runtimeGeneration += 1;
|
|
9770
12017
|
destroyActiveRuntime('Browser Dev Host disposed');
|
|
9771
12018
|
sessionEventSource?.close();
|
|
9772
|
-
|
|
12019
|
+
channelClient?.dispose();
|
|
12020
|
+
channelClient = undefined;
|
|
12021
|
+
accountStatusPoll.dispose();
|
|
9773
12022
|
clearMobileQrRefreshTimer();
|
|
9774
12023
|
if (nativeLaunchConfigRefreshTimer !== undefined)
|
|
9775
12024
|
window.clearInterval(nativeLaunchConfigRefreshTimer);
|
|
@@ -9780,12 +12029,11 @@ function clearMobileQrRefreshTimer() {
|
|
|
9780
12029
|
window.clearTimeout(mobileQrRefreshTimer);
|
|
9781
12030
|
mobileQrRefreshTimer = undefined;
|
|
9782
12031
|
}
|
|
9783
|
-
function
|
|
9784
|
-
|
|
9785
|
-
|
|
9786
|
-
|
|
9787
|
-
|
|
9788
|
-
});
|
|
12032
|
+
function combineAbortSignals(primary, secondary) {
|
|
12033
|
+
return secondary ? AbortSignal.any([primary, secondary]) : primary;
|
|
12034
|
+
}
|
|
12035
|
+
function isActiveHostGeneration(generation) {
|
|
12036
|
+
return !disposed && generation === hostGeneration;
|
|
9789
12037
|
}
|
|
9790
12038
|
function readSelectedLanAddress() {
|
|
9791
12039
|
return lanAddresses.find(({ id }) => id === elements.mobileLanSelect.value);
|
|
@@ -9798,25 +12046,23 @@ function isLanAddress(value) {
|
|
|
9798
12046
|
typeof value.name === 'string' &&
|
|
9799
12047
|
(value.mobileAppQrPayload === undefined || typeof value.mobileAppQrPayload === 'string'));
|
|
9800
12048
|
}
|
|
9801
|
-
|
|
9802
|
-
if (
|
|
9803
|
-
|
|
9804
|
-
|
|
9805
|
-
|
|
9806
|
-
|
|
9807
|
-
|
|
9808
|
-
|
|
9809
|
-
|
|
9810
|
-
}
|
|
9811
|
-
|
|
9812
|
-
elements.networkPermissionSelect.disabled = disabled;
|
|
9813
|
-
elements.resetPermissionsButton.disabled = disabled;
|
|
12049
|
+
function readMobileLaunchRedirectEntry(value) {
|
|
12050
|
+
if (!isRecord(value) ||
|
|
12051
|
+
typeof value.addressId !== 'string' ||
|
|
12052
|
+
!value.addressId ||
|
|
12053
|
+
typeof value.expiresAt !== 'number' ||
|
|
12054
|
+
!Number.isFinite(value.expiresAt) ||
|
|
12055
|
+
typeof value.token !== 'string' ||
|
|
12056
|
+
!/^[A-Za-z0-9_-]{22}$/.test(value.token)) {
|
|
12057
|
+
return undefined;
|
|
12058
|
+
}
|
|
12059
|
+
return { addressId: value.addressId, expiresAt: value.expiresAt, token: value.token };
|
|
9814
12060
|
}
|
|
9815
12061
|
function clonePermissionState(state) {
|
|
9816
12062
|
return { networkRequest: { ...state.networkRequest } };
|
|
9817
12063
|
}
|
|
9818
|
-
function
|
|
9819
|
-
return status ===
|
|
12064
|
+
function isSamePermissionState(left, right) {
|
|
12065
|
+
return (left.networkRequest.status === right.networkRequest.status && left.networkRequest.useOfficialDomain === right.networkRequest.useOfficialDomain);
|
|
9820
12066
|
}
|
|
9821
12067
|
function readErrorText(error) {
|
|
9822
12068
|
return error instanceof Error && error.message ? error.message : '操作失败';
|