@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.
Files changed (43) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +19 -1
  3. package/dist/cli-chunks/{build-BqGFRLe-.cjs → build-DF74IHGR.cjs} +2 -2
  4. package/dist/cli-chunks/{context-DTr3hRTv.cjs → context-QemmshdY.cjs} +26 -1
  5. package/dist/cli-chunks/{create-OkiiiH4I.cjs → create-Ch9m8w8M.cjs} +1 -1
  6. package/dist/cli-chunks/dev-D2DwLxXW.cjs +15682 -0
  7. package/dist/cli-chunks/{doctor-BO0vkDny.cjs → doctor-B9xJ2j20.cjs} +1 -1
  8. package/dist/cli-chunks/{index-CZuvk1Rg.cjs → index-Cv36miWx.cjs} +83 -83
  9. package/dist/cli-chunks/{index-CyEqCbeu.cjs → index-DxQgKWRT.cjs} +31 -32
  10. package/dist/cli-chunks/index.esm-DZQrllrR.cjs +5212 -0
  11. package/dist/cli-chunks/{login-CuAGltGs.cjs → login-DEKhX1Vf.cjs} +2 -2
  12. package/dist/cli-chunks/{project-vite-BUY9DgYo.cjs → project-vite-D_NNB_Ib.cjs} +1 -1
  13. package/dist/cli-chunks/{remote-tPMcxgpi.cjs → remote-B_Bn4v81.cjs} +4 -4
  14. package/dist/cli-chunks/{session-BUjFX2U2.cjs → session-C2zY7XHn.cjs} +2 -1
  15. package/dist/cli.cjs +1 -1
  16. package/dist/devtools/browser-dev-host/index.html +128 -151
  17. package/dist/devtools/browser-dev-host/main.js +2591 -345
  18. package/dist/index.cjs.js +1187 -3
  19. package/dist/index.esm.js +1187 -3
  20. package/dist/protocol.cjs.js +3 -0
  21. package/dist/protocol.esm.js +3 -1
  22. package/dist/vite.cjs.js +1 -1
  23. package/dist/vite.esm.js +1 -1
  24. package/package.json +16 -8
  25. package/skill/SKILL.md +7 -6
  26. package/skill/references/api-protocol.md +6 -3
  27. package/skill/references/api-root.md +6 -2
  28. package/skill/references/cli.md +16 -20
  29. package/skill/references/examples.md +2 -2
  30. package/skill/references/recipes.md +37 -0
  31. package/skill/references/safety-boundaries.md +1 -1
  32. package/skill/references/smoke-evaluation.md +1 -1
  33. package/skill/scripts/sync-references.mjs +6 -6
  34. package/skill/skill.json +4 -4
  35. package/types/index.d.ts +1 -1
  36. package/types/modules/share/copy-link.d.ts +16 -0
  37. package/types/modules/share/extra.d.ts +3 -0
  38. package/types/modules/share/index.d.ts +8 -2
  39. package/types/modules/share/types.d.ts +11 -0
  40. package/types/protocol/capabilities.d.ts +1 -1
  41. package/types/protocol/dev-session.d.ts +13 -0
  42. package/types/protocol.d.ts +2 -2
  43. 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('share.showShareMenu url 必须是 HTTP(S) URL');
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('share.showShareMenu url 必须是 HTTP(S) URL');
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
- const DEFAULT_PERMISSION_STATE = {
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
- function createMiniProgramDevReleaseDiagnostics(context, localState) {
9059
- if (context.source === 'anonymous') {
9060
- return [
9061
- {
9062
- code: 'REMOTE_CONTEXT_UNAVAILABLE',
9063
- severity: 'warning',
9064
- message: `未获取到线上权限配置,本地模拟不代表上线状态${context.warning ? `:${context.warning}` : ''}`,
9065
- },
9066
- ];
9067
- }
9068
- const parsedRemote = parseMiniProgramRuntimePermissions(context.runtimePermissions);
9069
- if (!parsedRemote.valid) {
9070
- return [
9071
- {
9072
- code: 'REMOTE_PERMISSIONS_INVALID',
9073
- severity: 'warning',
9074
- message: '线上权限配置不可用,本地模拟不代表上线状态。',
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
- const remoteNetworkRequest = parsedRemote.permissions['network.request'];
9079
- const remoteState = remoteNetworkRequest
9080
- ? {
9081
- status: remoteNetworkRequest.status,
9082
- useOfficialDomain: remoteNetworkRequest.config.useOfficialDomain === true,
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
- if (localState.networkRequest.status === 'enabled' && localState.networkRequest.useOfficialDomain && !remoteState.useOfficialDomain) {
9095
- return [
9096
- {
9097
- code: 'OFFICIAL_DOMAIN_DISABLED_ONLINE',
9098
- severity: 'warning',
9099
- message: '当前本地允许访问小黑盒官方域名,但线上权限未开放;上线后相关请求会返回 PERMISSION_DENIED。',
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
- function cloneDefaultPermissionState() {
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
- networkRequest: { ...DEFAULT_PERMISSION_STATE.networkRequest },
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 LOGIN_UNAVAILABLE_MESSAGE = '外部 PKCE 登录服务尚未配置';
9112
- function applyBrowserDevLoginAvailability(options) {
9113
- options.loginButton.disabled = !options.available;
9114
- options.loginButton.title = options.available ? '' : LOGIN_UNAVAILABLE_MESSAGE;
9115
- options.userStatus.textContent = options.available
9116
- ? options.userId
9117
- ? `已登录 · ${options.userId}`
9118
- : '未登录'
9119
- : `登录不可用:${LOGIN_UNAVAILABLE_MESSAGE}`;
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('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;').replaceAll("'", '&#039;');
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('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;').replaceAll("'", '&#039;');
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 miniProgramUrl = new URL(location.href).searchParams.get('mini_url');
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
- bridgeStatus: queryElement('#bridge-status'),
9151
- copyDebugPageUrlButton: queryElement('#copy-debug-page-url-button'),
9152
- debugPageCopyStatus: queryElement('#debug-page-copy-status'),
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
- loginButton: queryElement('#login-button'),
9158
- macAppButton: queryElement('#mac-app-button'),
9159
- macAppStatus: queryElement('#mac-app-status'),
9160
- miniUrl: queryElement('#mini-url'),
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 macAppProtocol = bootstrap.macAppProtocol;
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 hasPermissionOverride = false;
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 sessionPollTimer;
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
- updatePermissionControls();
9207
- updateUserStatus();
11367
+ renderDeveloperAccount();
11368
+ renderMiniProgramSummary(resolveDisplayedMiniProgram(bootstrap));
9208
11369
  updateStorageSnapshot();
9209
- elements.miniUrl.textContent = miniProgramUrl;
9210
11370
  await restartRuntime('initial');
9211
- connectSessionEvents();
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
- elements.bridgeStatus.textContent = 'starting';
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
- elements.bridgeStatus.textContent = state;
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
- if ((await refreshSessionStatus(false)).authenticated)
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 () => activeRuntime?.destroy(),
9304
- reload: async () => location.reload(),
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
- setClipboard: async ({ text }) => navigator.clipboard.writeText(text),
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
- async confirmAuthorization({ scopes, signal: operationSignal }) {
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 handleAccountRevision().catch(handleSessionNavigationError);
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 beginLogin() {
9383
- if ((await refreshSessionStatus(false)).authenticated)
9384
- return;
9385
- const response = await requestJson(SESSION_LOGIN_INTENT_PATH, {
9386
- method: 'POST',
9387
- body: JSON.stringify({ return_path: `${location.pathname}${location.search}` }),
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
- const payload = (await response.json());
9390
- if (!response.ok || typeof payload.authorization_url !== 'string') {
9391
- const unavailable = payload.code === 'LOGIN_SERVICE_UNAVAILABLE';
9392
- throw new Error(unavailable ? 'Browser Dev Host 登录服务尚未配置' : '登录启动失败');
9393
- }
9394
- location.assign(payload.authorization_url);
9395
- }
9396
- async function logout() {
9397
- const response = await requestJson(SESSION_LOGOUT_PATH, { method: 'POST', body: '{}' });
9398
- if (!response.ok)
9399
- showTransientMessage('退出登录失败', 'error');
9400
- else
9401
- await handleAccountRevision();
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
- sessionUserId = status.authenticated ? status.user_id : undefined;
9409
- updateUserStatus();
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 handleAccountRevision().catch(handleSessionNavigationError));
9422
- source.addEventListener('account-revision', () => void handleAccountRevision().catch(handleSessionNavigationError));
11588
+ source.addEventListener('account-invalidated', () => void accountStatusDrain.invalidate());
11589
+ source.addEventListener('account-revision', () => void accountStatusDrain.invalidate());
9423
11590
  source.onopen = () => {
9424
- sessionPollDelay = SESSION_POLL_MIN_MS;
9425
- clearSessionPoll();
11591
+ accountStatusPoll.reset();
9426
11592
  };
9427
11593
  source.onerror = () => {
9428
11594
  source.close();
9429
- scheduleSessionPoll();
11595
+ accountStatusPoll.schedule();
9430
11596
  };
9431
11597
  }
9432
- function handleAccountRevision() {
9433
- accountRevisionTask ?? (accountRevisionTask = processAccountRevision().finally(() => {
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
- destroyActiveRuntime('account revision changed');
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 response = await fetch(DEV_PERMISSIONS_PATH, { cache: 'no-store', credentials: 'same-origin', redirect: 'error' });
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
- permissionActionMessage = '权限持久化暂不可用,刷新或重启后可能丢失本页修改。';
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 applyPermissionConfiguration(configuration, message) {
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
- hasPermissionOverride = Boolean(configuration.overrideState);
9572
- permissionActionMessage = message ?? configuration.persistenceError ?? '';
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 (defaultLanAddressId && lanAddresses.some(({ id }) => id === defaultLanAddressId))
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 payload = applyDevContextToMobileQrPayload(selected.mobileAppQrPayload, devSession.url);
9643
- const imageUrl = await browserExports.toDataURL(payload, { width: MOBILE_QR_IMAGE_SIZE, margin: 1 });
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 = `${selected.name} · ${selected.address}`;
9648
- scheduleMobileQrRefresh(devSession.expiresAt, generation);
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 devContextUrl = new URL(MINI_PROGRAM_DEV_CONTEXT_PATH, location.origin);
9665
- const response = await requestJson(devContextUrl.pathname, {
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({ mini_url_origin: hostUrl.origin, runtime_permissions: createBrowserDevRuntimePermissions(permissionState) }),
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
- devContextUrl.pathname = `${MINI_PROGRAM_DEV_CONTEXT_PATH}/${value.token}`;
9674
- return { url: devContextUrl.toString(), expiresAt: value.expires_at };
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
- nativeLaunchConfigSignature = signature;
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
- async function openMacApp() {
9718
- if (macAppProtocol)
9719
- location.href = macAppProtocol;
9720
- showMacAppLaunchHint();
9721
- }
9722
- function showMacAppLaunchHint() {
9723
- elements.macAppStatus.hidden = false;
9724
- elements.debugPageUrl.textContent = location.href;
9725
- }
9726
- async function copyDebugPageUrl() {
9727
- await copyText(location.href);
9728
- elements.debugPageCopyStatus.textContent = '已复制';
9729
- }
9730
- function updateNativeAppLaunchAvailability() {
9731
- elements.macAppButton.disabled = !macAppProtocol;
9732
- elements.macAppButton.title = nativeAppLaunchUnavailableReason ?? '';
9733
- }
9734
- function updateUserStatus() {
9735
- applyBrowserDevLoginAvailability({
9736
- available: bootstrap.loginAvailable,
9737
- loginButton: elements.loginButton,
9738
- userId: sessionUserId,
9739
- userStatus: elements.userStatus,
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
- clearSessionPoll();
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 waitForAbort(signal) {
9784
- if (signal.aborted)
9785
- return Promise.reject(signal.reason);
9786
- return new Promise((_resolve, reject) => {
9787
- signal.addEventListener('abort', () => reject(signal.reason), { once: true });
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
- async function copyText(value) {
9802
- if (navigator.clipboard?.writeText)
9803
- return navigator.clipboard.writeText(value);
9804
- const textarea = document.createElement('textarea');
9805
- textarea.value = value;
9806
- document.body.appendChild(textarea);
9807
- textarea.select();
9808
- document.execCommand('copy');
9809
- textarea.remove();
9810
- }
9811
- function setPermissionControlsDisabled(disabled) {
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 readPermissionStatusLabel(status) {
9819
- return status === 'enabled' ? '允许' : '拒绝';
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 : '操作失败';