@heybox/hb-sdk 0.7.4-alpha.5 → 0.7.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -0
- package/README.md +2 -0
- package/dist/cli-chunks/{build-C-ufarA6.cjs → build-CVPqGQY3.cjs} +2 -2
- package/dist/cli-chunks/{context-C839--TH.cjs → context-C1nTGr-R.cjs} +26 -1
- package/dist/cli-chunks/{create-CNcII_CB.cjs → create-C5qrLLvv.cjs} +1 -1
- package/dist/cli-chunks/dev-U5BlN4ei.cjs +15682 -0
- package/dist/cli-chunks/{doctor-bhfQwyYU.cjs → doctor-DxqHMTbm.cjs} +1 -1
- package/dist/cli-chunks/{index-B_qJzSFo.cjs → index-BfR6OZeU.cjs} +83 -83
- package/dist/cli-chunks/{index-CT94XzyO.cjs → index-DLrcC_ij.cjs} +31 -32
- package/dist/cli-chunks/index.esm-DbbyeU1z.cjs +5212 -0
- package/dist/cli-chunks/{login-hYrDJ3dR.cjs → login-DQkArK14.cjs} +2 -2
- package/dist/cli-chunks/{project-vite-zaw68V1G.cjs → project-vite-c13CKUUl.cjs} +1 -1
- package/dist/cli-chunks/{remote-DQZRAAHh.cjs → remote-816SyJyA.cjs} +4 -4
- package/dist/cli-chunks/{session-D692TU5N.cjs → session-ySUU0fVq.cjs} +2 -1
- package/dist/cli.cjs +1 -1
- package/dist/devtools/browser-dev-host/index.html +128 -151
- package/dist/devtools/browser-dev-host/main.js +1553 -339
- package/dist/index.cjs.js +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/vite.cjs.js +1 -1
- package/dist/vite.esm.js +1 -1
- package/package.json +14 -7
- package/skill/SKILL.md +6 -6
- package/skill/references/api-protocol.md +1 -1
- package/skill/references/api-root.md +1 -1
- package/skill/references/cli.md +16 -20
- package/skill/references/examples.md +2 -2
- package/skill/references/safety-boundaries.md +1 -1
- package/skill/references/smoke-evaluation.md +1 -1
- package/skill/scripts/sync-references.mjs +6 -6
- package/skill/skill.json +4 -4
- package/types/protocol/dev-session.d.ts +13 -0
- package/dist/cli-chunks/dev-MpESCaMm.cjs +0 -2769
|
@@ -2795,7 +2795,7 @@ function inferBodyEncoding(headers, data, official) {
|
|
|
2795
2795
|
return 'json';
|
|
2796
2796
|
if (typeof URLSearchParams !== 'undefined' && data instanceof URLSearchParams)
|
|
2797
2797
|
return 'form';
|
|
2798
|
-
if (official && (Array.isArray(data) || isPlainRecord(data)))
|
|
2798
|
+
if (official && (Array.isArray(data) || isPlainRecord$1(data)))
|
|
2799
2799
|
return 'form';
|
|
2800
2800
|
return isPlainJsonValue(data) ? 'json' : undefined;
|
|
2801
2801
|
}
|
|
@@ -2811,7 +2811,7 @@ function isPlainJsonValue(value) {
|
|
|
2811
2811
|
typeof value === 'number' ||
|
|
2812
2812
|
(typeof value === 'object' && !ArrayBuffer.isView(value) && !(value instanceof ArrayBuffer)));
|
|
2813
2813
|
}
|
|
2814
|
-
function isPlainRecord(value) {
|
|
2814
|
+
function isPlainRecord$1(value) {
|
|
2815
2815
|
if (typeof value !== 'object' || value === null)
|
|
2816
2816
|
return false;
|
|
2817
2817
|
const prototype = Object.getPrototypeOf(value);
|
|
@@ -10051,10 +10051,26 @@ function isPlainObject(value) {
|
|
|
10051
10051
|
}
|
|
10052
10052
|
|
|
10053
10053
|
/** Dev Shell 读取局域网 Dev Session endpoint 的 query 参数。 */
|
|
10054
|
-
const MINI_PROGRAM_DEV_CONTEXT_QUERY_PARAM = 'dev_context_url';
|
|
10055
10054
|
/** Browser Dev Host 创建和读取 Dev Session 的固定路径。 */
|
|
10056
10055
|
const MINI_PROGRAM_DEV_CONTEXT_PATH = '/__hb_sdk__/dev-context';
|
|
10057
10056
|
|
|
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
|
+
}
|
|
10058
10074
|
const DEFAULT_PERMISSION_STATE = {
|
|
10059
10075
|
networkRequest: {
|
|
10060
10076
|
status: 'disabled',
|
|
@@ -10087,68 +10103,243 @@ function createBrowserDevRuntimePermissions(state) {
|
|
|
10087
10103
|
],
|
|
10088
10104
|
};
|
|
10089
10105
|
}
|
|
10090
|
-
|
|
10091
|
-
|
|
10092
|
-
|
|
10093
|
-
|
|
10094
|
-
|
|
10095
|
-
|
|
10096
|
-
|
|
10097
|
-
|
|
10098
|
-
|
|
10099
|
-
|
|
10100
|
-
|
|
10101
|
-
|
|
10102
|
-
|
|
10103
|
-
|
|
10104
|
-
|
|
10105
|
-
|
|
10106
|
-
|
|
10107
|
-
|
|
10108
|
-
|
|
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;
|
|
10109
10202
|
}
|
|
10110
|
-
|
|
10111
|
-
|
|
10112
|
-
|
|
10113
|
-
|
|
10114
|
-
|
|
10203
|
+
function reportError(error) {
|
|
10204
|
+
try {
|
|
10205
|
+
options.onError(error);
|
|
10206
|
+
}
|
|
10207
|
+
catch {
|
|
10208
|
+
// 错误观察方不能中断状态同步或产生未处理 rejection。
|
|
10115
10209
|
}
|
|
10116
|
-
: DEFAULT_PERMISSION_STATE.networkRequest;
|
|
10117
|
-
if (localState.networkRequest.status === 'enabled' && remoteState.status !== 'enabled') {
|
|
10118
|
-
return [
|
|
10119
|
-
{
|
|
10120
|
-
code: 'NETWORK_REQUEST_DISABLED_ONLINE',
|
|
10121
|
-
severity: 'warning',
|
|
10122
|
-
message: '当前本地允许 network.request,但线上权限已关闭;上线后请求会返回 PERMISSION_DENIED。',
|
|
10123
|
-
},
|
|
10124
|
-
];
|
|
10125
10210
|
}
|
|
10126
|
-
|
|
10127
|
-
|
|
10128
|
-
|
|
10129
|
-
|
|
10130
|
-
|
|
10131
|
-
|
|
10132
|
-
|
|
10133
|
-
|
|
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;
|
|
10134
10271
|
}
|
|
10135
|
-
return [];
|
|
10136
10272
|
}
|
|
10137
|
-
|
|
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
|
+
});
|
|
10138
10283
|
return {
|
|
10139
|
-
|
|
10284
|
+
dispose: drain.dispose,
|
|
10285
|
+
invalidate(hint, invalidateOptions) {
|
|
10286
|
+
return drain.invalidate({ force: invalidateOptions?.force, hint });
|
|
10287
|
+
},
|
|
10140
10288
|
};
|
|
10141
10289
|
}
|
|
10142
10290
|
|
|
10143
|
-
const
|
|
10144
|
-
|
|
10145
|
-
|
|
10146
|
-
|
|
10147
|
-
|
|
10148
|
-
|
|
10149
|
-
|
|
10150
|
-
|
|
10151
|
-
|
|
10291
|
+
const SCOPE_LABELS = {
|
|
10292
|
+
identity: '小程序身份',
|
|
10293
|
+
profile: '昵称和头像',
|
|
10294
|
+
};
|
|
10295
|
+
function confirmBrowserDevHostAuthorization(input) {
|
|
10296
|
+
return new Promise((resolve, reject) => {
|
|
10297
|
+
if (input.signal.aborted) {
|
|
10298
|
+
reject(input.signal.reason);
|
|
10299
|
+
return;
|
|
10300
|
+
}
|
|
10301
|
+
const root = document.createElement('div');
|
|
10302
|
+
root.className = 'hb-sdk-auth-dialog';
|
|
10303
|
+
root.setAttribute('role', 'dialog');
|
|
10304
|
+
root.setAttribute('aria-modal', 'true');
|
|
10305
|
+
root.innerHTML = `
|
|
10306
|
+
<div class="hb-sdk-auth-dialog__panel">
|
|
10307
|
+
<h2>授权请求</h2>
|
|
10308
|
+
<p class="hb-sdk-auth-dialog__app">${escapeHtml$2(input.miniProgram?.name || '当前小程序')} 申请访问:</p>
|
|
10309
|
+
<ul class="hb-sdk-auth-dialog__scopes">${input.scopes.map((scope) => `<li>${escapeHtml$2(SCOPE_LABELS[scope] || scope)}</li>`).join('')}</ul>
|
|
10310
|
+
${input.userProfile?.nickname
|
|
10311
|
+
? `<p class="hb-sdk-auth-dialog__profile">当前账号:${escapeHtml$2(input.userProfile.nickname)}</p>`
|
|
10312
|
+
: ''}
|
|
10313
|
+
<div class="hb-sdk-auth-dialog__actions">
|
|
10314
|
+
<button type="button" data-action="deny">拒绝</button>
|
|
10315
|
+
<button type="button" class="primary" data-action="allow">允许</button>
|
|
10316
|
+
</div>
|
|
10317
|
+
</div>
|
|
10318
|
+
`;
|
|
10319
|
+
const finish = (decision) => {
|
|
10320
|
+
cleanup();
|
|
10321
|
+
resolve(decision);
|
|
10322
|
+
};
|
|
10323
|
+
const onAbort = () => {
|
|
10324
|
+
cleanup();
|
|
10325
|
+
reject(input.signal.reason);
|
|
10326
|
+
};
|
|
10327
|
+
const cleanup = () => {
|
|
10328
|
+
input.signal.removeEventListener('abort', onAbort);
|
|
10329
|
+
root.remove();
|
|
10330
|
+
};
|
|
10331
|
+
root.querySelector('[data-action="allow"]')?.addEventListener('click', () => {
|
|
10332
|
+
finish({ status: 'granted', grantedScopes: [...input.scopes] });
|
|
10333
|
+
});
|
|
10334
|
+
root.querySelector('[data-action="deny"]')?.addEventListener('click', () => {
|
|
10335
|
+
finish({ status: 'cancelled' });
|
|
10336
|
+
});
|
|
10337
|
+
input.signal.addEventListener('abort', onAbort, { once: true });
|
|
10338
|
+
document.body.appendChild(root);
|
|
10339
|
+
});
|
|
10340
|
+
}
|
|
10341
|
+
function escapeHtml$2(value) {
|
|
10342
|
+
return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", ''');
|
|
10152
10343
|
}
|
|
10153
10344
|
|
|
10154
10345
|
function createBrowserDevHostLaunchConfiguration(input) {
|
|
@@ -10165,58 +10356,963 @@ function createBrowserDevHostLaunchConfiguration(input) {
|
|
|
10165
10356
|
};
|
|
10166
10357
|
}
|
|
10167
10358
|
|
|
10359
|
+
const ACTION_DIALOG_STYLE_ID = 'hb-sdk-action-dialog-style';
|
|
10360
|
+
function promptBrowserDevHostAction(input) {
|
|
10361
|
+
return new Promise((resolve, reject) => {
|
|
10362
|
+
if (input.signal?.aborted) {
|
|
10363
|
+
reject(input.signal.reason);
|
|
10364
|
+
return;
|
|
10365
|
+
}
|
|
10366
|
+
ensureActionDialogStyles();
|
|
10367
|
+
const root = document.createElement('div');
|
|
10368
|
+
root.className = 'hb-sdk-action-dialog';
|
|
10369
|
+
root.setAttribute('role', 'dialog');
|
|
10370
|
+
root.setAttribute('aria-modal', 'true');
|
|
10371
|
+
root.innerHTML = `
|
|
10372
|
+
<div class="hb-sdk-action-dialog__panel">
|
|
10373
|
+
<h2>${escapeHtml$1(input.title)}</h2>
|
|
10374
|
+
<pre class="hb-sdk-action-dialog__detail">${escapeHtml$1(input.detail)}</pre>
|
|
10375
|
+
<div class="hb-sdk-action-dialog__actions">
|
|
10376
|
+
<button type="button" class="primary" data-action="confirm">确定</button>
|
|
10377
|
+
</div>
|
|
10378
|
+
</div>
|
|
10379
|
+
`;
|
|
10380
|
+
const finish = () => {
|
|
10381
|
+
cleanup();
|
|
10382
|
+
resolve();
|
|
10383
|
+
};
|
|
10384
|
+
const onAbort = () => {
|
|
10385
|
+
cleanup();
|
|
10386
|
+
reject(input.signal?.reason);
|
|
10387
|
+
};
|
|
10388
|
+
const cleanup = () => {
|
|
10389
|
+
input.signal?.removeEventListener('abort', onAbort);
|
|
10390
|
+
root.remove();
|
|
10391
|
+
};
|
|
10392
|
+
root.querySelector('[data-action="confirm"]')?.addEventListener('click', finish);
|
|
10393
|
+
input.signal?.addEventListener('abort', onAbort, { once: true });
|
|
10394
|
+
document.body.appendChild(root);
|
|
10395
|
+
});
|
|
10396
|
+
}
|
|
10397
|
+
async function promptOpenAppPage(protocol, context) {
|
|
10398
|
+
await promptBrowserDevHostAction({
|
|
10399
|
+
title: '打开页面',
|
|
10400
|
+
detail: ['protocolType: ' + protocol.protocolType, 'path: ' + protocol.path, 'params: ' + stringifyJson(protocol.params)].join('\n'),
|
|
10401
|
+
signal: context.signal,
|
|
10402
|
+
});
|
|
10403
|
+
}
|
|
10404
|
+
async function promptShowShareMenu(options, context) {
|
|
10405
|
+
await promptBrowserDevHostAction({
|
|
10406
|
+
title: '分享',
|
|
10407
|
+
detail: [
|
|
10408
|
+
'title: ' + options.title,
|
|
10409
|
+
'description: ' + options.description,
|
|
10410
|
+
'url: ' + options.url,
|
|
10411
|
+
'channel: ' + stringifyJson(options.channel),
|
|
10412
|
+
'post: ' + stringifyJson(options.post),
|
|
10413
|
+
].join('\n'),
|
|
10414
|
+
signal: context.signal,
|
|
10415
|
+
});
|
|
10416
|
+
return undefined;
|
|
10417
|
+
}
|
|
10418
|
+
async function promptCaptureAndShare(options, context) {
|
|
10419
|
+
// 调试页只提示截图参数,不读取 canvas。
|
|
10420
|
+
await promptBrowserDevHostAction({
|
|
10421
|
+
title: '截图分享',
|
|
10422
|
+
detail: ['rect: ' + stringifyJson(options.rect), 'post: ' + stringifyJson(options.post)].join('\n'),
|
|
10423
|
+
signal: context.signal,
|
|
10424
|
+
});
|
|
10425
|
+
return undefined;
|
|
10426
|
+
}
|
|
10427
|
+
function stringifyJson(value) {
|
|
10428
|
+
return JSON.stringify(value, null, 2);
|
|
10429
|
+
}
|
|
10430
|
+
function escapeHtml$1(value) {
|
|
10431
|
+
return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", ''');
|
|
10432
|
+
}
|
|
10433
|
+
/** 样式跟 index.html 解耦,避免和其他 agent 同时改静态页冲突。 */
|
|
10434
|
+
function ensureActionDialogStyles() {
|
|
10435
|
+
if (document.getElementById(ACTION_DIALOG_STYLE_ID)) {
|
|
10436
|
+
return;
|
|
10437
|
+
}
|
|
10438
|
+
const style = document.createElement('style');
|
|
10439
|
+
style.id = ACTION_DIALOG_STYLE_ID;
|
|
10440
|
+
style.textContent = `
|
|
10441
|
+
.hb-sdk-action-dialog {
|
|
10442
|
+
position: fixed;
|
|
10443
|
+
inset: 0;
|
|
10444
|
+
z-index: 40;
|
|
10445
|
+
display: grid;
|
|
10446
|
+
place-items: center;
|
|
10447
|
+
padding: 24px;
|
|
10448
|
+
background: rgba(15, 23, 42, 0.45);
|
|
10449
|
+
}
|
|
10450
|
+
|
|
10451
|
+
.hb-sdk-action-dialog__panel {
|
|
10452
|
+
width: min(420px, 100%);
|
|
10453
|
+
border-radius: 12px;
|
|
10454
|
+
padding: 20px;
|
|
10455
|
+
background: #fff;
|
|
10456
|
+
box-shadow: 0 20px 45px rgba(15, 23, 42, 0.2);
|
|
10457
|
+
}
|
|
10458
|
+
|
|
10459
|
+
.hb-sdk-action-dialog__panel h2 {
|
|
10460
|
+
margin: 0 0 8px;
|
|
10461
|
+
font-size: 18px;
|
|
10462
|
+
}
|
|
10463
|
+
|
|
10464
|
+
.hb-sdk-action-dialog__detail {
|
|
10465
|
+
margin: 0 0 16px;
|
|
10466
|
+
max-height: 40vh;
|
|
10467
|
+
overflow: auto;
|
|
10468
|
+
color: #1f2933;
|
|
10469
|
+
font-family: inherit;
|
|
10470
|
+
font-size: 12px;
|
|
10471
|
+
line-height: 1.6;
|
|
10472
|
+
white-space: pre-wrap;
|
|
10473
|
+
overflow-wrap: anywhere;
|
|
10474
|
+
word-break: break-word;
|
|
10475
|
+
}
|
|
10476
|
+
|
|
10477
|
+
.hb-sdk-action-dialog__actions {
|
|
10478
|
+
display: flex;
|
|
10479
|
+
justify-content: flex-end;
|
|
10480
|
+
gap: 8px;
|
|
10481
|
+
}
|
|
10482
|
+
|
|
10483
|
+
.hb-sdk-action-dialog__actions button {
|
|
10484
|
+
min-height: 36px;
|
|
10485
|
+
border: 1px solid #cad5e2;
|
|
10486
|
+
border-radius: 6px;
|
|
10487
|
+
padding: 0 12px;
|
|
10488
|
+
background: #fff;
|
|
10489
|
+
cursor: pointer;
|
|
10490
|
+
}
|
|
10491
|
+
|
|
10492
|
+
.hb-sdk-action-dialog__actions .primary {
|
|
10493
|
+
border-color: #2563eb;
|
|
10494
|
+
color: #fff;
|
|
10495
|
+
background: #2563eb;
|
|
10496
|
+
}
|
|
10497
|
+
`;
|
|
10498
|
+
document.head.appendChild(style);
|
|
10499
|
+
}
|
|
10500
|
+
|
|
10501
|
+
const STYLE_ID = 'hb-sdk-feedback-style';
|
|
10502
|
+
const TOAST_DURATION_MS = 2000;
|
|
10503
|
+
const VIBRATE_DURATION_MS = {
|
|
10504
|
+
light: 20,
|
|
10505
|
+
medium: 40,
|
|
10506
|
+
heavy: 80,
|
|
10507
|
+
};
|
|
10508
|
+
const VIBRATE_FLASH_MS = 120;
|
|
10509
|
+
const FEEDBACK_STYLE = `
|
|
10510
|
+
.hb-sdk-feedback-toast-stack {
|
|
10511
|
+
position: fixed;
|
|
10512
|
+
top: 16px;
|
|
10513
|
+
left: 50%;
|
|
10514
|
+
z-index: 50;
|
|
10515
|
+
display: grid;
|
|
10516
|
+
gap: 8px;
|
|
10517
|
+
transform: translateX(-50%);
|
|
10518
|
+
pointer-events: none;
|
|
10519
|
+
}
|
|
10520
|
+
.hb-sdk-feedback-toast {
|
|
10521
|
+
min-width: 160px;
|
|
10522
|
+
max-width: min(420px, calc(100vw - 32px));
|
|
10523
|
+
border-radius: 8px;
|
|
10524
|
+
padding: 10px 14px;
|
|
10525
|
+
color: #fff;
|
|
10526
|
+
background: #334155;
|
|
10527
|
+
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.18);
|
|
10528
|
+
text-align: center;
|
|
10529
|
+
line-height: 1.5;
|
|
10530
|
+
}
|
|
10531
|
+
.hb-sdk-feedback-toast--success {
|
|
10532
|
+
background: #047857;
|
|
10533
|
+
}
|
|
10534
|
+
.hb-sdk-feedback-toast--error {
|
|
10535
|
+
background: #b91c1c;
|
|
10536
|
+
}
|
|
10537
|
+
.hb-sdk-feedback-loading {
|
|
10538
|
+
position: fixed;
|
|
10539
|
+
inset: 0;
|
|
10540
|
+
z-index: 50;
|
|
10541
|
+
display: grid;
|
|
10542
|
+
place-items: center;
|
|
10543
|
+
padding: 24px;
|
|
10544
|
+
background: rgba(15, 23, 42, 0.45);
|
|
10545
|
+
}
|
|
10546
|
+
.hb-sdk-feedback-loading__panel {
|
|
10547
|
+
border-radius: 12px;
|
|
10548
|
+
padding: 20px 28px;
|
|
10549
|
+
color: #1f2933;
|
|
10550
|
+
background: #fff;
|
|
10551
|
+
box-shadow: 0 20px 45px rgba(15, 23, 42, 0.2);
|
|
10552
|
+
font-size: 16px;
|
|
10553
|
+
}
|
|
10554
|
+
html.hb-sdk-feedback-vibrate-flash {
|
|
10555
|
+
filter: brightness(1.18);
|
|
10556
|
+
}
|
|
10557
|
+
`;
|
|
10558
|
+
/** 全局只保留一个 loading 遮罩。 */
|
|
10559
|
+
let loadingRoot;
|
|
10560
|
+
let toastStack;
|
|
10561
|
+
function showBrowserDevHostToast(options) {
|
|
10562
|
+
ensureStyles();
|
|
10563
|
+
const stack = getToastStack();
|
|
10564
|
+
const toast = document.createElement('div');
|
|
10565
|
+
toast.className = 'hb-sdk-feedback-toast';
|
|
10566
|
+
if (options.status === 'success' || options.status === 'error') {
|
|
10567
|
+
toast.classList.add(`hb-sdk-feedback-toast--${options.status}`);
|
|
10568
|
+
}
|
|
10569
|
+
toast.setAttribute('role', 'status');
|
|
10570
|
+
// textContent 避免 toast 文案 XSS
|
|
10571
|
+
toast.textContent = options.message;
|
|
10572
|
+
stack.appendChild(toast);
|
|
10573
|
+
window.setTimeout(() => {
|
|
10574
|
+
toast.remove();
|
|
10575
|
+
if (toastStack && toastStack.childElementCount === 0) {
|
|
10576
|
+
toastStack.remove();
|
|
10577
|
+
toastStack = undefined;
|
|
10578
|
+
}
|
|
10579
|
+
}, TOAST_DURATION_MS);
|
|
10580
|
+
return Promise.resolve();
|
|
10581
|
+
}
|
|
10582
|
+
function showBrowserDevHostLoading(options) {
|
|
10583
|
+
ensureStyles();
|
|
10584
|
+
const dismissible = options?.dismissible === true;
|
|
10585
|
+
if (!loadingRoot) {
|
|
10586
|
+
const root = document.createElement('div');
|
|
10587
|
+
root.className = 'hb-sdk-feedback-loading';
|
|
10588
|
+
root.setAttribute('role', 'status');
|
|
10589
|
+
root.setAttribute('aria-live', 'polite');
|
|
10590
|
+
root.innerHTML = `<div class="hb-sdk-feedback-loading__panel">加载中</div>`;
|
|
10591
|
+
root.addEventListener('click', (event) => {
|
|
10592
|
+
if (event.target !== root)
|
|
10593
|
+
return;
|
|
10594
|
+
if (root.dataset.dismissible !== 'true')
|
|
10595
|
+
return;
|
|
10596
|
+
void hideBrowserDevHostLoading();
|
|
10597
|
+
});
|
|
10598
|
+
document.body.appendChild(root);
|
|
10599
|
+
loadingRoot = root;
|
|
10600
|
+
}
|
|
10601
|
+
loadingRoot.dataset.dismissible = dismissible ? 'true' : 'false';
|
|
10602
|
+
return Promise.resolve();
|
|
10603
|
+
}
|
|
10604
|
+
function hideBrowserDevHostLoading() {
|
|
10605
|
+
loadingRoot?.remove();
|
|
10606
|
+
loadingRoot = undefined;
|
|
10607
|
+
return Promise.resolve();
|
|
10608
|
+
}
|
|
10609
|
+
async function vibrateBrowserDevHost(options) {
|
|
10610
|
+
ensureStyles();
|
|
10611
|
+
await wait(Math.max(0, options.delay));
|
|
10612
|
+
flashVibrate();
|
|
10613
|
+
const vibrate = navigator.vibrate;
|
|
10614
|
+
if (typeof vibrate !== 'function') {
|
|
10615
|
+
return;
|
|
10616
|
+
}
|
|
10617
|
+
try {
|
|
10618
|
+
// DOM lib 将 VibratePattern 收成 Iterable<number>,单次时长仍按 number 传入。
|
|
10619
|
+
vibrate.call(navigator, VIBRATE_DURATION_MS[options.intensity]);
|
|
10620
|
+
}
|
|
10621
|
+
catch {
|
|
10622
|
+
// 调试页无震动能力时保持静默
|
|
10623
|
+
}
|
|
10624
|
+
}
|
|
10625
|
+
function setBrowserDevHostScreenshotChromeHidden(hidden) {
|
|
10626
|
+
document.documentElement.dataset.screenshotChromeHidden = hidden ? 'true' : 'false';
|
|
10627
|
+
}
|
|
10628
|
+
function ensureStyles() {
|
|
10629
|
+
// 调试页 CSS 只注入一次
|
|
10630
|
+
if (document.getElementById(STYLE_ID))
|
|
10631
|
+
return;
|
|
10632
|
+
const style = document.createElement('style');
|
|
10633
|
+
style.id = STYLE_ID;
|
|
10634
|
+
style.textContent = FEEDBACK_STYLE;
|
|
10635
|
+
document.head.appendChild(style);
|
|
10636
|
+
}
|
|
10637
|
+
function getToastStack() {
|
|
10638
|
+
if (toastStack?.isConnected) {
|
|
10639
|
+
return toastStack;
|
|
10640
|
+
}
|
|
10641
|
+
const stack = document.createElement('div');
|
|
10642
|
+
stack.className = 'hb-sdk-feedback-toast-stack';
|
|
10643
|
+
document.body.appendChild(stack);
|
|
10644
|
+
toastStack = stack;
|
|
10645
|
+
return stack;
|
|
10646
|
+
}
|
|
10647
|
+
function flashVibrate() {
|
|
10648
|
+
const root = document.documentElement;
|
|
10649
|
+
root.classList.add('hb-sdk-feedback-vibrate-flash');
|
|
10650
|
+
window.setTimeout(() => {
|
|
10651
|
+
root.classList.remove('hb-sdk-feedback-vibrate-flash');
|
|
10652
|
+
}, VIBRATE_FLASH_MS);
|
|
10653
|
+
}
|
|
10654
|
+
function wait(ms) {
|
|
10655
|
+
if (ms <= 0)
|
|
10656
|
+
return Promise.resolve();
|
|
10657
|
+
return new Promise((resolve) => {
|
|
10658
|
+
window.setTimeout(resolve, ms);
|
|
10659
|
+
});
|
|
10660
|
+
}
|
|
10661
|
+
|
|
10662
|
+
function createBrowserDevHostPresentation(input) {
|
|
10663
|
+
return {
|
|
10664
|
+
close: async () => {
|
|
10665
|
+
await input.close();
|
|
10666
|
+
},
|
|
10667
|
+
reload: async () => {
|
|
10668
|
+
location.reload();
|
|
10669
|
+
},
|
|
10670
|
+
getViewportMetrics: () => ({
|
|
10671
|
+
windowWidth: window.innerWidth,
|
|
10672
|
+
windowHeight: window.innerHeight,
|
|
10673
|
+
screenWidth: window.screen.width,
|
|
10674
|
+
screenHeight: window.screen.height,
|
|
10675
|
+
pixelRatio: window.devicePixelRatio,
|
|
10676
|
+
}),
|
|
10677
|
+
getNavigationBarHeight: () => 0,
|
|
10678
|
+
setNavigationBarStyle: (style) => {
|
|
10679
|
+
document.documentElement.dataset.navigationBarStyle = style;
|
|
10680
|
+
},
|
|
10681
|
+
setClipboard: async ({ text }) => {
|
|
10682
|
+
await navigator.clipboard.writeText(text);
|
|
10683
|
+
},
|
|
10684
|
+
showToast: async ({ message, status }) => {
|
|
10685
|
+
input.pushLog('host.message', { message, status });
|
|
10686
|
+
await showBrowserDevHostToast({ message, status });
|
|
10687
|
+
},
|
|
10688
|
+
showLoading: showBrowserDevHostLoading,
|
|
10689
|
+
hideLoading: hideBrowserDevHostLoading,
|
|
10690
|
+
vibrate: vibrateBrowserDevHost,
|
|
10691
|
+
openAppPage: promptOpenAppPage,
|
|
10692
|
+
showShareMenu: promptShowShareMenu,
|
|
10693
|
+
captureAndShare: promptCaptureAndShare,
|
|
10694
|
+
setScreenshotChromeHidden: setBrowserDevHostScreenshotChromeHidden,
|
|
10695
|
+
};
|
|
10696
|
+
}
|
|
10697
|
+
|
|
10698
|
+
const OPEN_IN_APP_URL = 'https://api.xiaoheihe.cn/open_inapp/';
|
|
10699
|
+
function createMobileLaunchRedirectHttpUrl(options) {
|
|
10700
|
+
const port = typeof options.port === 'number' ? options.port : Number(options.port);
|
|
10701
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
10702
|
+
throw new TypeError('Mobile launch redirect port is invalid');
|
|
10703
|
+
}
|
|
10704
|
+
if (!/^[A-Za-z0-9_-]{22}$/.test(options.token)) {
|
|
10705
|
+
throw new TypeError('Mobile launch redirect token is invalid');
|
|
10706
|
+
}
|
|
10707
|
+
const url = new URL(`http://${options.host}:${port}/l/${options.token}`);
|
|
10708
|
+
if (url.protocol !== 'http:' ||
|
|
10709
|
+
url.hostname !== options.host ||
|
|
10710
|
+
url.port !== String(port) ||
|
|
10711
|
+
url.username ||
|
|
10712
|
+
url.password ||
|
|
10713
|
+
url.search ||
|
|
10714
|
+
url.hash) {
|
|
10715
|
+
throw new TypeError('Mobile launch redirect host is invalid');
|
|
10716
|
+
}
|
|
10717
|
+
return url.href;
|
|
10718
|
+
}
|
|
10719
|
+
function createMobileAppRedirectQrPayload(redirectUrl) {
|
|
10720
|
+
const url = new URL(redirectUrl);
|
|
10721
|
+
if (url.protocol !== 'http:' || url.username || url.password) {
|
|
10722
|
+
throw new TypeError('Mobile launch redirect URL must be an HTTP URL without credentials');
|
|
10723
|
+
}
|
|
10724
|
+
return `${OPEN_IN_APP_URL}#${createHeyboxProtocol({
|
|
10725
|
+
protocol_type: 'openWindow',
|
|
10726
|
+
full_screen: true,
|
|
10727
|
+
navigation_bar: {
|
|
10728
|
+
title: '',
|
|
10729
|
+
},
|
|
10730
|
+
webview: {
|
|
10731
|
+
url: url.href,
|
|
10732
|
+
pull: false,
|
|
10733
|
+
refresh: false,
|
|
10734
|
+
},
|
|
10735
|
+
})}`;
|
|
10736
|
+
}
|
|
10737
|
+
function createHeyboxProtocol(payload) {
|
|
10738
|
+
return `heybox://${encodeURIComponent(JSON.stringify(payload))}`;
|
|
10739
|
+
}
|
|
10740
|
+
|
|
10741
|
+
const BROWSER_DEV_HOST_CHANNEL_VERSION = 1;
|
|
10742
|
+
const BROWSER_DEV_HOST_CHANNEL_MAX_TEXT_BYTES = 64 * 1024;
|
|
10743
|
+
const BROWSER_DEV_HOST_CHANNEL_MAX_JSON_DEPTH = 64;
|
|
10744
|
+
const FIXED_BASE64URL_PATTERN = /^[A-Za-z0-9_-]{22}$/;
|
|
10745
|
+
class BrowserDevHostChannelError extends TypeError {
|
|
10746
|
+
constructor(code, message) {
|
|
10747
|
+
super(message);
|
|
10748
|
+
this.name = 'BrowserDevHostChannelError';
|
|
10749
|
+
this.code = code;
|
|
10750
|
+
}
|
|
10751
|
+
}
|
|
10752
|
+
function parseBrowserDevHostServerMessage(raw, binary = false) {
|
|
10753
|
+
const value = parseTextFrame(raw, binary);
|
|
10754
|
+
assertRecord(value, 'Server message');
|
|
10755
|
+
assertVersion(value);
|
|
10756
|
+
switch (value.type) {
|
|
10757
|
+
case 'grant':
|
|
10758
|
+
assertExactKeys(value, ['version', 'type', 'connectionEpoch', 'nextClientSequence', 'nextServerSequence', 'snapshot'], 'grant');
|
|
10759
|
+
assertFixedBase64Url(value.connectionEpoch, 'connectionEpoch');
|
|
10760
|
+
assertNonNegativeSafeInteger(value.nextClientSequence, 'nextClientSequence');
|
|
10761
|
+
assertNonNegativeSafeInteger(value.nextServerSequence, 'nextServerSequence');
|
|
10762
|
+
assertSnapshot(value.snapshot);
|
|
10763
|
+
return value;
|
|
10764
|
+
case 'event':
|
|
10765
|
+
assertExactKeys(value, ['version', 'type', 'connectionEpoch', 'sequence', 'event', 'snapshot'], 'event');
|
|
10766
|
+
assertFixedBase64Url(value.connectionEpoch, 'connectionEpoch');
|
|
10767
|
+
assertNonNegativeSafeInteger(value.sequence, 'sequence');
|
|
10768
|
+
if (value.event !== 'account.changed' && value.event !== 'status.changed') {
|
|
10769
|
+
invalidMessage('event must be account.changed or status.changed');
|
|
10770
|
+
}
|
|
10771
|
+
assertSnapshot(value.snapshot);
|
|
10772
|
+
return value;
|
|
10773
|
+
case 'heartbeat':
|
|
10774
|
+
assertExactKeys(value, ['version', 'type', 'connectionEpoch', 'sequence'], 'heartbeat');
|
|
10775
|
+
assertFixedBase64Url(value.connectionEpoch, 'connectionEpoch');
|
|
10776
|
+
assertNonNegativeSafeInteger(value.sequence, 'sequence');
|
|
10777
|
+
return value;
|
|
10778
|
+
default:
|
|
10779
|
+
return invalidMessage('Unknown server message type');
|
|
10780
|
+
}
|
|
10781
|
+
}
|
|
10782
|
+
function createBrowserDevHostChannelSequenceGuard(input) {
|
|
10783
|
+
const connectionEpoch = input.connectionEpoch;
|
|
10784
|
+
assertFixedBase64Url(connectionEpoch, 'connectionEpoch');
|
|
10785
|
+
assertNonNegativeSafeInteger(input.nextSequence, 'nextSequence');
|
|
10786
|
+
let nextSequence = input.nextSequence;
|
|
10787
|
+
return {
|
|
10788
|
+
get nextSequence() {
|
|
10789
|
+
return nextSequence;
|
|
10790
|
+
},
|
|
10791
|
+
accept(message) {
|
|
10792
|
+
assertFixedBase64Url(message.connectionEpoch, 'connectionEpoch');
|
|
10793
|
+
assertNonNegativeSafeInteger(message.sequence, 'sequence');
|
|
10794
|
+
if (message.connectionEpoch !== connectionEpoch) {
|
|
10795
|
+
throw new BrowserDevHostChannelError('INVALID_SEQUENCE', 'Connection epoch does not match');
|
|
10796
|
+
}
|
|
10797
|
+
if (nextSequence === undefined) {
|
|
10798
|
+
throw new BrowserDevHostChannelError('INVALID_SEQUENCE', 'Sequence is exhausted');
|
|
10799
|
+
}
|
|
10800
|
+
if (message.sequence !== nextSequence) {
|
|
10801
|
+
throw new BrowserDevHostChannelError('INVALID_SEQUENCE', `Expected sequence ${nextSequence}`);
|
|
10802
|
+
}
|
|
10803
|
+
nextSequence = nextSequence === Number.MAX_SAFE_INTEGER ? undefined : nextSequence + 1;
|
|
10804
|
+
},
|
|
10805
|
+
};
|
|
10806
|
+
}
|
|
10807
|
+
function parseTextFrame(raw, binary) {
|
|
10808
|
+
if (binary) {
|
|
10809
|
+
throw new BrowserDevHostChannelError('BINARY_FRAME', 'Binary frames are not supported');
|
|
10810
|
+
}
|
|
10811
|
+
if (typeof raw !== 'string') {
|
|
10812
|
+
throw new BrowserDevHostChannelError('TEXT_FRAME_REQUIRED', 'Channel frame must be text');
|
|
10813
|
+
}
|
|
10814
|
+
if (raw.length > BROWSER_DEV_HOST_CHANNEL_MAX_TEXT_BYTES) {
|
|
10815
|
+
throw new BrowserDevHostChannelError('TEXT_FRAME_TOO_LARGE', 'Channel text frame exceeds 64 KiB');
|
|
10816
|
+
}
|
|
10817
|
+
if (new TextEncoder().encode(raw).byteLength > BROWSER_DEV_HOST_CHANNEL_MAX_TEXT_BYTES) {
|
|
10818
|
+
throw new BrowserDevHostChannelError('TEXT_FRAME_TOO_LARGE', 'Channel text frame exceeds 64 KiB');
|
|
10819
|
+
}
|
|
10820
|
+
try {
|
|
10821
|
+
assertNoDuplicateJsonKeys(raw);
|
|
10822
|
+
return JSON.parse(raw);
|
|
10823
|
+
}
|
|
10824
|
+
catch (error) {
|
|
10825
|
+
if (error instanceof BrowserDevHostChannelError)
|
|
10826
|
+
throw error;
|
|
10827
|
+
throw new BrowserDevHostChannelError('INVALID_JSON', 'Channel frame must contain strict JSON');
|
|
10828
|
+
}
|
|
10829
|
+
}
|
|
10830
|
+
function assertSnapshot(value) {
|
|
10831
|
+
assertRecord(value, 'snapshot');
|
|
10832
|
+
assertExactKeys(value, ['account', 'permissions'], 'snapshot');
|
|
10833
|
+
assertRecord(value.account, 'snapshot.account');
|
|
10834
|
+
assertExactKeys(value.account, ['revision'], 'snapshot.account');
|
|
10835
|
+
assertNonNegativeSafeInteger(value.account.revision, 'snapshot.account.revision');
|
|
10836
|
+
if (!parseBrowserDevPermissionState(value.permissions))
|
|
10837
|
+
invalidMessage('snapshot.permissions is invalid');
|
|
10838
|
+
}
|
|
10839
|
+
function assertVersion(value) {
|
|
10840
|
+
if (value.version !== BROWSER_DEV_HOST_CHANNEL_VERSION)
|
|
10841
|
+
invalidMessage('version must be 1');
|
|
10842
|
+
}
|
|
10843
|
+
function assertRecord(value, field) {
|
|
10844
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype) {
|
|
10845
|
+
invalidMessage(`${field} must be a plain JSON object`);
|
|
10846
|
+
}
|
|
10847
|
+
}
|
|
10848
|
+
function assertExactKeys(value, required, field, optional = []) {
|
|
10849
|
+
const keys = Object.keys(value);
|
|
10850
|
+
if (required.some((key) => !Object.hasOwn(value, key)) || keys.some((key) => !required.includes(key) && !optional.includes(key))) {
|
|
10851
|
+
invalidMessage(`${field} fields are invalid`);
|
|
10852
|
+
}
|
|
10853
|
+
}
|
|
10854
|
+
function assertNonNegativeSafeInteger(value, field) {
|
|
10855
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {
|
|
10856
|
+
invalidMessage(`${field} must be a non-negative safe integer`);
|
|
10857
|
+
}
|
|
10858
|
+
}
|
|
10859
|
+
function assertFixedBase64Url(value, field) {
|
|
10860
|
+
if (typeof value !== 'string' || !FIXED_BASE64URL_PATTERN.test(value)) {
|
|
10861
|
+
invalidMessage(`${field} must be a 22-character base64url value`);
|
|
10862
|
+
}
|
|
10863
|
+
}
|
|
10864
|
+
function invalidMessage(message) {
|
|
10865
|
+
throw new BrowserDevHostChannelError('INVALID_MESSAGE', message);
|
|
10866
|
+
}
|
|
10867
|
+
function assertNoDuplicateJsonKeys(raw) {
|
|
10868
|
+
const scanner = new StrictJsonKeyScanner(raw);
|
|
10869
|
+
scanner.scan();
|
|
10870
|
+
}
|
|
10871
|
+
class StrictJsonKeyScanner {
|
|
10872
|
+
constructor(source) {
|
|
10873
|
+
this.source = source;
|
|
10874
|
+
this.position = 0;
|
|
10875
|
+
}
|
|
10876
|
+
scan() {
|
|
10877
|
+
this.skipWhitespace();
|
|
10878
|
+
this.scanValue();
|
|
10879
|
+
this.skipWhitespace();
|
|
10880
|
+
if (this.position !== this.source.length)
|
|
10881
|
+
throw new SyntaxError('Unexpected trailing JSON data');
|
|
10882
|
+
}
|
|
10883
|
+
scanValue(depth = 0) {
|
|
10884
|
+
const character = this.source[this.position];
|
|
10885
|
+
if (character === '{' || character === '[') {
|
|
10886
|
+
if (depth >= BROWSER_DEV_HOST_CHANNEL_MAX_JSON_DEPTH) {
|
|
10887
|
+
throw new BrowserDevHostChannelError('INVALID_JSON', 'Channel JSON exceeds maximum nesting depth');
|
|
10888
|
+
}
|
|
10889
|
+
return character === '{' ? this.scanObject(depth) : this.scanArray(depth);
|
|
10890
|
+
}
|
|
10891
|
+
if (character === '"') {
|
|
10892
|
+
this.scanString();
|
|
10893
|
+
return;
|
|
10894
|
+
}
|
|
10895
|
+
this.scanPrimitive();
|
|
10896
|
+
}
|
|
10897
|
+
scanObject(depth) {
|
|
10898
|
+
this.position += 1;
|
|
10899
|
+
this.skipWhitespace();
|
|
10900
|
+
const keys = new Set();
|
|
10901
|
+
if (this.consume('}'))
|
|
10902
|
+
return;
|
|
10903
|
+
while (true) {
|
|
10904
|
+
if (this.source[this.position] !== '"')
|
|
10905
|
+
throw new SyntaxError('JSON object key must be a string');
|
|
10906
|
+
const keySource = this.scanString();
|
|
10907
|
+
const key = JSON.parse(keySource);
|
|
10908
|
+
if (typeof key !== 'string')
|
|
10909
|
+
throw new SyntaxError('JSON object key must be a string');
|
|
10910
|
+
if (keys.has(key)) {
|
|
10911
|
+
throw new BrowserDevHostChannelError('INVALID_JSON', 'Channel JSON contains a duplicate key');
|
|
10912
|
+
}
|
|
10913
|
+
keys.add(key);
|
|
10914
|
+
this.skipWhitespace();
|
|
10915
|
+
if (!this.consume(':'))
|
|
10916
|
+
throw new SyntaxError('Missing JSON object colon');
|
|
10917
|
+
this.skipWhitespace();
|
|
10918
|
+
this.scanValue(depth + 1);
|
|
10919
|
+
this.skipWhitespace();
|
|
10920
|
+
if (this.consume('}'))
|
|
10921
|
+
return;
|
|
10922
|
+
if (!this.consume(','))
|
|
10923
|
+
throw new SyntaxError('Missing JSON object comma');
|
|
10924
|
+
this.skipWhitespace();
|
|
10925
|
+
}
|
|
10926
|
+
}
|
|
10927
|
+
scanArray(depth) {
|
|
10928
|
+
this.position += 1;
|
|
10929
|
+
this.skipWhitespace();
|
|
10930
|
+
if (this.consume(']'))
|
|
10931
|
+
return;
|
|
10932
|
+
while (true) {
|
|
10933
|
+
this.scanValue(depth + 1);
|
|
10934
|
+
this.skipWhitespace();
|
|
10935
|
+
if (this.consume(']'))
|
|
10936
|
+
return;
|
|
10937
|
+
if (!this.consume(','))
|
|
10938
|
+
throw new SyntaxError('Missing JSON array comma');
|
|
10939
|
+
this.skipWhitespace();
|
|
10940
|
+
}
|
|
10941
|
+
}
|
|
10942
|
+
scanString() {
|
|
10943
|
+
const start = this.position;
|
|
10944
|
+
this.position += 1;
|
|
10945
|
+
while (this.position < this.source.length) {
|
|
10946
|
+
const character = this.source[this.position];
|
|
10947
|
+
if (character === '"') {
|
|
10948
|
+
this.position += 1;
|
|
10949
|
+
return this.source.slice(start, this.position);
|
|
10950
|
+
}
|
|
10951
|
+
if (character === '\\') {
|
|
10952
|
+
this.position += 2;
|
|
10953
|
+
}
|
|
10954
|
+
else {
|
|
10955
|
+
this.position += 1;
|
|
10956
|
+
}
|
|
10957
|
+
}
|
|
10958
|
+
throw new SyntaxError('Unterminated JSON string');
|
|
10959
|
+
}
|
|
10960
|
+
scanPrimitive() {
|
|
10961
|
+
const start = this.position;
|
|
10962
|
+
while (this.position < this.source.length && !/[\s,\]}]/.test(this.source[this.position] ?? '')) {
|
|
10963
|
+
this.position += 1;
|
|
10964
|
+
}
|
|
10965
|
+
if (start === this.position)
|
|
10966
|
+
throw new SyntaxError('Missing JSON value');
|
|
10967
|
+
}
|
|
10968
|
+
skipWhitespace() {
|
|
10969
|
+
while (/[\t\n\r ]/.test(this.source[this.position] ?? ''))
|
|
10970
|
+
this.position += 1;
|
|
10971
|
+
}
|
|
10972
|
+
consume(character) {
|
|
10973
|
+
if (this.source[this.position] !== character)
|
|
10974
|
+
return false;
|
|
10975
|
+
this.position += 1;
|
|
10976
|
+
return true;
|
|
10977
|
+
}
|
|
10978
|
+
}
|
|
10979
|
+
|
|
10980
|
+
const RECONNECT_DELAYS_MS = [1000, 2000, 4000, 8000, 15000];
|
|
10981
|
+
function createBrowserDevHostChannelClient(options) {
|
|
10982
|
+
const updateListeners = new Set();
|
|
10983
|
+
const createWebSocket = options.webSocketFactory ?? ((url) => new WebSocket(url));
|
|
10984
|
+
const randomValues = options.randomValues ?? ((bytes) => crypto.getRandomValues(bytes));
|
|
10985
|
+
const timers = options.timers ?? {
|
|
10986
|
+
clearTimeout: (handle) => window.clearTimeout(handle),
|
|
10987
|
+
setTimeout: (callback, delay) => window.setTimeout(callback, delay),
|
|
10988
|
+
};
|
|
10989
|
+
const onlineTarget = options.onlineTarget ?? (typeof window === 'undefined' ? undefined : window);
|
|
10990
|
+
const visibilityTarget = options.visibilityTarget ?? (typeof document === 'undefined' ? undefined : document);
|
|
10991
|
+
const isOnline = options.isOnline ?? (() => typeof navigator === 'undefined' || navigator.onLine !== false);
|
|
10992
|
+
let socket;
|
|
10993
|
+
let detachSocketListeners;
|
|
10994
|
+
let snapshot;
|
|
10995
|
+
let disposed = false;
|
|
10996
|
+
let reconnectAttempt = 0;
|
|
10997
|
+
let reconnectTimer;
|
|
10998
|
+
let started = false;
|
|
10999
|
+
let lifecycleListenersAttached = false;
|
|
11000
|
+
const diagnosticsSinceGrant = new Set();
|
|
11001
|
+
const reportDiagnostic = (phase, code) => {
|
|
11002
|
+
if (!options.onDiagnostic || diagnosticsSinceGrant.has(phase))
|
|
11003
|
+
return;
|
|
11004
|
+
diagnosticsSinceGrant.add(phase);
|
|
11005
|
+
try {
|
|
11006
|
+
options.onDiagnostic({ phase, ...(code ? { code } : {}) });
|
|
11007
|
+
}
|
|
11008
|
+
catch {
|
|
11009
|
+
// 诊断观察方不能影响 Browser channel 或 HTTP/SSE fallback。
|
|
11010
|
+
}
|
|
11011
|
+
};
|
|
11012
|
+
const publishSnapshot = (nextSnapshot, update, notifyEqual = false) => {
|
|
11013
|
+
if (snapshot && nextSnapshot.account.revision < snapshot.account.revision)
|
|
11014
|
+
return;
|
|
11015
|
+
const equal = snapshot && JSON.stringify(nextSnapshot) === JSON.stringify(snapshot);
|
|
11016
|
+
snapshot = nextSnapshot;
|
|
11017
|
+
if (equal && !notifyEqual)
|
|
11018
|
+
return;
|
|
11019
|
+
for (const listener of updateListeners)
|
|
11020
|
+
listener(nextSnapshot, update);
|
|
11021
|
+
};
|
|
11022
|
+
const scheduleReconnect = () => {
|
|
11023
|
+
if (disposed || reconnectTimer !== undefined || socket)
|
|
11024
|
+
return;
|
|
11025
|
+
const delay = RECONNECT_DELAYS_MS[Math.min(reconnectAttempt, RECONNECT_DELAYS_MS.length - 1)];
|
|
11026
|
+
reconnectAttempt += 1;
|
|
11027
|
+
reconnectTimer = timers.setTimeout(() => {
|
|
11028
|
+
reconnectTimer = undefined;
|
|
11029
|
+
connect();
|
|
11030
|
+
}, delay);
|
|
11031
|
+
};
|
|
11032
|
+
const reconnectNow = () => {
|
|
11033
|
+
if (disposed || !started || socket)
|
|
11034
|
+
return;
|
|
11035
|
+
if (reconnectTimer !== undefined) {
|
|
11036
|
+
timers.clearTimeout(reconnectTimer);
|
|
11037
|
+
reconnectTimer = undefined;
|
|
11038
|
+
}
|
|
11039
|
+
connect();
|
|
11040
|
+
};
|
|
11041
|
+
const onOnline = () => {
|
|
11042
|
+
if (isOnline())
|
|
11043
|
+
reconnectNow();
|
|
11044
|
+
};
|
|
11045
|
+
const onVisibilityChange = () => {
|
|
11046
|
+
if (visibilityTarget && !visibilityTarget.hidden)
|
|
11047
|
+
reconnectNow();
|
|
11048
|
+
};
|
|
11049
|
+
const attachLifecycleListeners = () => {
|
|
11050
|
+
if (lifecycleListenersAttached)
|
|
11051
|
+
return;
|
|
11052
|
+
lifecycleListenersAttached = true;
|
|
11053
|
+
onlineTarget?.addEventListener('online', onOnline);
|
|
11054
|
+
visibilityTarget?.addEventListener('visibilitychange', onVisibilityChange);
|
|
11055
|
+
};
|
|
11056
|
+
const detachLifecycleListeners = () => {
|
|
11057
|
+
if (!lifecycleListenersAttached)
|
|
11058
|
+
return;
|
|
11059
|
+
lifecycleListenersAttached = false;
|
|
11060
|
+
onlineTarget?.removeEventListener('online', onOnline);
|
|
11061
|
+
visibilityTarget?.removeEventListener('visibilitychange', onVisibilityChange);
|
|
11062
|
+
};
|
|
11063
|
+
const connect = () => {
|
|
11064
|
+
if (disposed || socket)
|
|
11065
|
+
return;
|
|
11066
|
+
started = true;
|
|
11067
|
+
attachLifecycleListeners();
|
|
11068
|
+
if (reconnectTimer !== undefined) {
|
|
11069
|
+
timers.clearTimeout(reconnectTimer);
|
|
11070
|
+
reconnectTimer = undefined;
|
|
11071
|
+
}
|
|
11072
|
+
let nextSocket;
|
|
11073
|
+
let clientNonce;
|
|
11074
|
+
try {
|
|
11075
|
+
nextSocket = createWebSocket(options.url);
|
|
11076
|
+
}
|
|
11077
|
+
catch {
|
|
11078
|
+
reportDiagnostic('constructor', 'WEBSOCKET_CONSTRUCTION_FAILED');
|
|
11079
|
+
scheduleReconnect();
|
|
11080
|
+
return;
|
|
11081
|
+
}
|
|
11082
|
+
try {
|
|
11083
|
+
clientNonce = createClientNonce(randomValues);
|
|
11084
|
+
}
|
|
11085
|
+
catch {
|
|
11086
|
+
reportDiagnostic('entropy', 'NONCE_GENERATION_FAILED');
|
|
11087
|
+
try {
|
|
11088
|
+
nextSocket?.close(1011, 'channel initialization failed');
|
|
11089
|
+
}
|
|
11090
|
+
catch {
|
|
11091
|
+
// WebSocket 可能尚未完成初始化。
|
|
11092
|
+
}
|
|
11093
|
+
scheduleReconnect();
|
|
11094
|
+
return;
|
|
11095
|
+
}
|
|
11096
|
+
socket = nextSocket;
|
|
11097
|
+
let connectionEpoch;
|
|
11098
|
+
let nextClientSequence;
|
|
11099
|
+
let serverSequenceGuard;
|
|
11100
|
+
const detach = () => {
|
|
11101
|
+
nextSocket.removeEventListener('open', onOpen);
|
|
11102
|
+
nextSocket.removeEventListener('message', onMessage);
|
|
11103
|
+
nextSocket.removeEventListener('close', onClose);
|
|
11104
|
+
nextSocket.removeEventListener('error', onError);
|
|
11105
|
+
if (detachSocketListeners === detach)
|
|
11106
|
+
detachSocketListeners = undefined;
|
|
11107
|
+
};
|
|
11108
|
+
const fail = () => {
|
|
11109
|
+
if (socket !== nextSocket)
|
|
11110
|
+
return;
|
|
11111
|
+
socket = undefined;
|
|
11112
|
+
detach();
|
|
11113
|
+
try {
|
|
11114
|
+
nextSocket.close(1008, 'invalid channel state');
|
|
11115
|
+
}
|
|
11116
|
+
catch {
|
|
11117
|
+
// WebSocket 已终止时只需进入 fallback 重连。
|
|
11118
|
+
}
|
|
11119
|
+
scheduleReconnect();
|
|
11120
|
+
};
|
|
11121
|
+
const onOpen = () => {
|
|
11122
|
+
if (disposed || socket !== nextSocket)
|
|
11123
|
+
return;
|
|
11124
|
+
try {
|
|
11125
|
+
nextSocket.send(JSON.stringify({
|
|
11126
|
+
version: BROWSER_DEV_HOST_CHANNEL_VERSION,
|
|
11127
|
+
type: 'hello',
|
|
11128
|
+
csrfToken: options.csrfToken,
|
|
11129
|
+
clientNonce,
|
|
11130
|
+
}));
|
|
11131
|
+
}
|
|
11132
|
+
catch {
|
|
11133
|
+
reportDiagnostic('send', 'HELLO_SEND_FAILED');
|
|
11134
|
+
fail();
|
|
11135
|
+
}
|
|
11136
|
+
};
|
|
11137
|
+
const onMessage = (event) => {
|
|
11138
|
+
if (disposed || socket !== nextSocket)
|
|
11139
|
+
return;
|
|
11140
|
+
if (typeof event.data !== 'string') {
|
|
11141
|
+
reportDiagnostic('parse', 'TEXT_FRAME_REQUIRED');
|
|
11142
|
+
fail();
|
|
11143
|
+
return;
|
|
11144
|
+
}
|
|
11145
|
+
let message;
|
|
11146
|
+
try {
|
|
11147
|
+
message = parseBrowserDevHostServerMessage(event.data);
|
|
11148
|
+
}
|
|
11149
|
+
catch (error) {
|
|
11150
|
+
reportDiagnostic('parse', error instanceof BrowserDevHostChannelError ? error.code : 'INVALID_MESSAGE');
|
|
11151
|
+
fail();
|
|
11152
|
+
return;
|
|
11153
|
+
}
|
|
11154
|
+
if (message.type === 'grant') {
|
|
11155
|
+
if (serverSequenceGuard) {
|
|
11156
|
+
reportDiagnostic('sequence', 'DUPLICATE_GRANT');
|
|
11157
|
+
fail();
|
|
11158
|
+
return;
|
|
11159
|
+
}
|
|
11160
|
+
connectionEpoch = message.connectionEpoch;
|
|
11161
|
+
nextClientSequence = message.nextClientSequence;
|
|
11162
|
+
serverSequenceGuard = createBrowserDevHostChannelSequenceGuard({
|
|
11163
|
+
connectionEpoch,
|
|
11164
|
+
nextSequence: message.nextServerSequence,
|
|
11165
|
+
});
|
|
11166
|
+
reconnectAttempt = 0;
|
|
11167
|
+
diagnosticsSinceGrant.clear();
|
|
11168
|
+
publishSnapshot(message.snapshot, { type: 'grant' });
|
|
11169
|
+
return;
|
|
11170
|
+
}
|
|
11171
|
+
if (!serverSequenceGuard || !connectionEpoch || nextClientSequence === undefined) {
|
|
11172
|
+
reportDiagnostic('sequence', 'MESSAGE_BEFORE_GRANT');
|
|
11173
|
+
fail();
|
|
11174
|
+
return;
|
|
11175
|
+
}
|
|
11176
|
+
try {
|
|
11177
|
+
serverSequenceGuard.accept(message);
|
|
11178
|
+
}
|
|
11179
|
+
catch {
|
|
11180
|
+
reportDiagnostic('sequence', 'INVALID_SEQUENCE');
|
|
11181
|
+
fail();
|
|
11182
|
+
return;
|
|
11183
|
+
}
|
|
11184
|
+
if (message.type === 'heartbeat') {
|
|
11185
|
+
try {
|
|
11186
|
+
nextSocket.send(JSON.stringify({
|
|
11187
|
+
version: BROWSER_DEV_HOST_CHANNEL_VERSION,
|
|
11188
|
+
type: 'heartbeat.ack',
|
|
11189
|
+
connectionEpoch,
|
|
11190
|
+
sequence: nextClientSequence,
|
|
11191
|
+
}));
|
|
11192
|
+
}
|
|
11193
|
+
catch {
|
|
11194
|
+
reportDiagnostic('send', 'HEARTBEAT_ACK_SEND_FAILED');
|
|
11195
|
+
fail();
|
|
11196
|
+
}
|
|
11197
|
+
nextClientSequence = nextClientSequence === Number.MAX_SAFE_INTEGER ? undefined : nextClientSequence + 1;
|
|
11198
|
+
return;
|
|
11199
|
+
}
|
|
11200
|
+
publishSnapshot(message.snapshot, { type: 'event', event: message.event }, true);
|
|
11201
|
+
};
|
|
11202
|
+
const onClose = () => {
|
|
11203
|
+
if (socket !== nextSocket)
|
|
11204
|
+
return;
|
|
11205
|
+
reportDiagnostic('close', 'SOCKET_CLOSED');
|
|
11206
|
+
socket = undefined;
|
|
11207
|
+
detach();
|
|
11208
|
+
scheduleReconnect();
|
|
11209
|
+
};
|
|
11210
|
+
const onError = () => {
|
|
11211
|
+
reportDiagnostic('open', 'SOCKET_ERROR');
|
|
11212
|
+
fail();
|
|
11213
|
+
};
|
|
11214
|
+
nextSocket.addEventListener('open', onOpen);
|
|
11215
|
+
nextSocket.addEventListener('message', onMessage);
|
|
11216
|
+
nextSocket.addEventListener('close', onClose);
|
|
11217
|
+
nextSocket.addEventListener('error', onError);
|
|
11218
|
+
detachSocketListeners = detach;
|
|
11219
|
+
};
|
|
11220
|
+
return {
|
|
11221
|
+
connect,
|
|
11222
|
+
subscribeUpdates(listener) {
|
|
11223
|
+
updateListeners.add(listener);
|
|
11224
|
+
return () => updateListeners.delete(listener);
|
|
11225
|
+
},
|
|
11226
|
+
dispose() {
|
|
11227
|
+
if (disposed)
|
|
11228
|
+
return;
|
|
11229
|
+
disposed = true;
|
|
11230
|
+
started = false;
|
|
11231
|
+
detachLifecycleListeners();
|
|
11232
|
+
if (reconnectTimer !== undefined)
|
|
11233
|
+
timers.clearTimeout(reconnectTimer);
|
|
11234
|
+
reconnectTimer = undefined;
|
|
11235
|
+
const activeSocket = socket;
|
|
11236
|
+
socket = undefined;
|
|
11237
|
+
detachSocketListeners?.();
|
|
11238
|
+
activeSocket?.close(1000, 'client disposed');
|
|
11239
|
+
updateListeners.clear();
|
|
11240
|
+
},
|
|
11241
|
+
};
|
|
11242
|
+
}
|
|
11243
|
+
function encodeBase64Url(bytes) {
|
|
11244
|
+
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
|
|
11245
|
+
let result = '';
|
|
11246
|
+
for (let index = 0; index < bytes.length; index += 3) {
|
|
11247
|
+
const first = bytes[index] ?? 0;
|
|
11248
|
+
const second = bytes[index + 1];
|
|
11249
|
+
const third = bytes[index + 2];
|
|
11250
|
+
result += alphabet[first >> 2];
|
|
11251
|
+
result += alphabet[((first & 0x03) << 4) | ((second ?? 0) >> 4)];
|
|
11252
|
+
if (second !== undefined)
|
|
11253
|
+
result += alphabet[((second & 0x0f) << 2) | ((third ?? 0) >> 6)];
|
|
11254
|
+
if (third !== undefined)
|
|
11255
|
+
result += alphabet[third & 0x3f];
|
|
11256
|
+
}
|
|
11257
|
+
return result;
|
|
11258
|
+
}
|
|
11259
|
+
function createClientNonce(randomValues) {
|
|
11260
|
+
const bytes = randomValues(new Uint8Array(16));
|
|
11261
|
+
if (!(bytes instanceof Uint8Array) || bytes.byteLength !== 16)
|
|
11262
|
+
throw new TypeError('Channel nonce source must fill 16 bytes');
|
|
11263
|
+
const nonce = encodeBase64Url(bytes);
|
|
11264
|
+
if (!/^[A-Za-z0-9_-]{22}$/.test(nonce))
|
|
11265
|
+
throw new TypeError('Channel nonce source returned invalid bytes');
|
|
11266
|
+
return nonce;
|
|
11267
|
+
}
|
|
11268
|
+
|
|
10168
11269
|
const BOOTSTRAP_PATH = '/__hb_sdk_bootstrap__';
|
|
10169
11270
|
const DEV_PERMISSIONS_PATH = '/__hb_sdk__/dev-permissions';
|
|
11271
|
+
const MOBILE_LAUNCH_REDIRECT_PATH = '/__hb_sdk__/mobile-launch-redirect';
|
|
10170
11272
|
const NETWORK_PATH = '/__hb_sdk__/network';
|
|
10171
11273
|
const SESSION_STATUS_PATH = '/session/status';
|
|
10172
|
-
const SESSION_LOGIN_INTENT_PATH = '/session/login-intent';
|
|
10173
|
-
const SESSION_LOGOUT_PATH = '/session/logout';
|
|
10174
11274
|
const SESSION_REQUEST_PATH = '/session/request';
|
|
10175
11275
|
const SESSION_EVENTS_PATH = '/session/events';
|
|
10176
11276
|
const MOBILE_QR_IMAGE_SIZE = 320;
|
|
10177
11277
|
const NATIVE_LAUNCH_CONFIG_REFRESH_MS = 1000;
|
|
10178
11278
|
const SESSION_POLL_MIN_MS = 1000;
|
|
10179
11279
|
const SESSION_POLL_MAX_MS = 15000;
|
|
10180
|
-
const
|
|
11280
|
+
const pageQuery = new URL(location.href).searchParams;
|
|
11281
|
+
const miniProgramUrl = pageQuery.get('mini_url');
|
|
11282
|
+
const uiPreview = pageQuery.get('ui_preview') === '1';
|
|
10181
11283
|
const elements = {
|
|
10182
|
-
|
|
10183
|
-
|
|
10184
|
-
|
|
10185
|
-
debugPageUrl: queryElement('#debug-page-url'),
|
|
10186
|
-
devContextStatus: queryElement('#dev-context-status'),
|
|
11284
|
+
developerAvatar: queryElement('#developer-avatar'),
|
|
11285
|
+
developerHint: queryElement('#developer-hint'),
|
|
11286
|
+
developerName: queryElement('#developer-name'),
|
|
10187
11287
|
device: queryElement('#device'),
|
|
10188
11288
|
logs: queryElement('#logs'),
|
|
10189
|
-
|
|
10190
|
-
|
|
10191
|
-
|
|
10192
|
-
|
|
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'),
|
|
10193
11294
|
mobileLanSelect: queryElement('#mobile-lan-select'),
|
|
10194
11295
|
mobileQrImage: queryElement('#mobile-qr-image'),
|
|
10195
11296
|
mobileQrStatus: queryElement('#mobile-qr-status'),
|
|
10196
|
-
networkPermissionSelect: queryElement('#network-permission-select'),
|
|
10197
|
-
permissionPersistenceStatus: queryElement('#permission-persistence-status'),
|
|
10198
|
-
releaseDiagnostics: queryElement('#release-diagnostics'),
|
|
10199
|
-
resetPermissionsButton: queryElement('#reset-permissions-button'),
|
|
10200
11297
|
storageSnapshot: queryElement('#storage-snapshot'),
|
|
10201
|
-
userStatus: queryElement('#user-status'),
|
|
10202
11298
|
};
|
|
10203
|
-
if (!miniProgramUrl) {
|
|
11299
|
+
if (!miniProgramUrl && !uiPreview) {
|
|
10204
11300
|
elements.device.innerHTML = '<div class="error">Missing mini_url query.</div>';
|
|
10205
11301
|
throw new Error('Missing mini_url query.');
|
|
10206
11302
|
}
|
|
10207
|
-
const launchUrl = miniProgramUrl;
|
|
11303
|
+
const launchUrl = miniProgramUrl ?? 'about:blank';
|
|
10208
11304
|
const bootstrap = await loadBootstrap();
|
|
10209
11305
|
let lanAddresses = bootstrap.lanAddresses;
|
|
10210
11306
|
let defaultLanAddressId = bootstrap.defaultLanAddressId;
|
|
10211
|
-
let
|
|
10212
|
-
let nativeAppLaunchUnavailableReason = bootstrap.nativeAppLaunchUnavailableReason;
|
|
11307
|
+
let mobileLaunchRedirect = bootstrap.mobileLaunchRedirect;
|
|
10213
11308
|
let nativeLaunchConfigSignature = readNativeLaunchConfigSignature(bootstrap);
|
|
10214
11309
|
let devContext = bootstrap.devContext;
|
|
10215
11310
|
let remotePermissionState = createBrowserDevPermissionState(devContext);
|
|
10216
11311
|
let permissionState = clonePermissionState(remotePermissionState);
|
|
10217
|
-
let
|
|
10218
|
-
let permissionActionMessage = '';
|
|
11312
|
+
let runtimePermissionState;
|
|
10219
11313
|
let disposed = false;
|
|
11314
|
+
let hostGeneration = 0;
|
|
11315
|
+
const hostAbortController = new AbortController();
|
|
10220
11316
|
let runtimeGeneration = 0;
|
|
10221
11317
|
let activeRuntime;
|
|
10222
11318
|
let activeTerminal;
|
|
@@ -10227,20 +11323,55 @@ let nativeLaunchConfigRefreshTimer;
|
|
|
10227
11323
|
let nativeLaunchConfigRefreshInFlight = false;
|
|
10228
11324
|
let mobileDevSessionCreation = Promise.resolve();
|
|
10229
11325
|
let sessionRevision = -1;
|
|
11326
|
+
let sessionAuthenticated = false;
|
|
10230
11327
|
let sessionUserId;
|
|
11328
|
+
let sessionUserNickname;
|
|
11329
|
+
let sessionUserAvatar;
|
|
10231
11330
|
let sessionEventSource;
|
|
10232
|
-
let
|
|
10233
|
-
let sessionPollDelay = SESSION_POLL_MIN_MS;
|
|
10234
|
-
let accountRevisionTask;
|
|
11331
|
+
let channelClient;
|
|
10235
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
|
+
});
|
|
10236
11365
|
await loadPersistedPermissionConfiguration();
|
|
10237
11366
|
await refreshSessionStatus(false);
|
|
10238
|
-
|
|
10239
|
-
|
|
11367
|
+
renderDeveloperAccount();
|
|
11368
|
+
renderMiniProgramSummary(resolveDisplayedMiniProgram(bootstrap));
|
|
10240
11369
|
updateStorageSnapshot();
|
|
10241
|
-
elements.miniUrl.textContent = miniProgramUrl;
|
|
10242
11370
|
await restartRuntime('initial');
|
|
10243
|
-
|
|
11371
|
+
if (!uiPreview) {
|
|
11372
|
+
connectSessionEvents();
|
|
11373
|
+
connectBrowserChannel();
|
|
11374
|
+
}
|
|
10244
11375
|
setupControls();
|
|
10245
11376
|
const mobileAppQrReady = setupMobileAppQr();
|
|
10246
11377
|
if (bootstrap.devContext.source === 'remote') {
|
|
@@ -10252,25 +11383,17 @@ if (bootstrap.devContext.source === 'remote') {
|
|
|
10252
11383
|
}
|
|
10253
11384
|
function setupControls() {
|
|
10254
11385
|
window.addEventListener('beforeunload', disposeBrowserDevHost);
|
|
10255
|
-
elements.loginButton.addEventListener('click', () => {
|
|
10256
|
-
void beginLogin().catch(handleSessionNavigationError);
|
|
10257
|
-
});
|
|
10258
|
-
queryElement('#logout-button').addEventListener('click', () => void logout());
|
|
10259
|
-
queryElement('#show-button').addEventListener('click', () => activeRuntime?.show());
|
|
10260
|
-
queryElement('#hide-button').addEventListener('click', () => activeRuntime?.hide());
|
|
10261
|
-
elements.macAppButton.addEventListener('click', () => void openMacApp());
|
|
10262
|
-
elements.copyDebugPageUrlButton.addEventListener('click', () => void copyDebugPageUrl());
|
|
10263
11386
|
elements.mobileLanSelect.addEventListener('change', () => void updateMobileQrCode());
|
|
10264
|
-
elements.networkPermissionSelect.addEventListener('change', () => {
|
|
10265
|
-
void savePermissionOverride(elements.networkPermissionSelect.value === 'enabled' ? 'enabled' : 'disabled');
|
|
10266
|
-
});
|
|
10267
|
-
elements.resetPermissionsButton.addEventListener('click', () => void resetPermissionOverride());
|
|
10268
11387
|
}
|
|
10269
11388
|
async function restartRuntime(reason) {
|
|
10270
11389
|
const generation = ++runtimeGeneration;
|
|
10271
11390
|
destroyActiveRuntime(reason);
|
|
10272
|
-
|
|
11391
|
+
runtimePermissionState = clonePermissionState(permissionState);
|
|
10273
11392
|
elements.device.replaceChildren();
|
|
11393
|
+
if (uiPreview) {
|
|
11394
|
+
elements.device.innerHTML = '<div class="placeholder">未加载小程序</div>';
|
|
11395
|
+
return;
|
|
11396
|
+
}
|
|
10274
11397
|
const iframe = document.createElement('iframe');
|
|
10275
11398
|
iframe.allow = 'clipboard-read; clipboard-write';
|
|
10276
11399
|
elements.device.appendChild(iframe);
|
|
@@ -10292,11 +11415,10 @@ async function restartRuntime(reason) {
|
|
|
10292
11415
|
resolveLaunchConfiguration: async () => (await assembly.resolveLaunch()).configuration,
|
|
10293
11416
|
dispose: assembly.dispose,
|
|
10294
11417
|
onReady: () => {
|
|
10295
|
-
elements.bridgeStatus.textContent = 'ready';
|
|
10296
11418
|
pushLog('runtime.ready', { generation });
|
|
10297
11419
|
},
|
|
10298
11420
|
onStateChange: (state) => {
|
|
10299
|
-
|
|
11421
|
+
pushLog('runtime.state', { generation, state });
|
|
10300
11422
|
},
|
|
10301
11423
|
onError: (error) => pushLog('runtime.error', redactNetworkLogValue(error)),
|
|
10302
11424
|
onSecurityViolation: (event) => pushLog('runtime.security', redactNetworkLogValue(event)),
|
|
@@ -10307,7 +11429,6 @@ async function restartRuntime(reason) {
|
|
|
10307
11429
|
catch (error) {
|
|
10308
11430
|
if (disposed || generation !== runtimeGeneration || terminal.signal.aborted)
|
|
10309
11431
|
return;
|
|
10310
|
-
elements.bridgeStatus.textContent = 'failed';
|
|
10311
11432
|
pushLog('runtime.start.failed', redactNetworkLogValue(error));
|
|
10312
11433
|
}
|
|
10313
11434
|
}
|
|
@@ -10316,13 +11437,10 @@ function createWebHostInput(signal) {
|
|
|
10316
11437
|
signal,
|
|
10317
11438
|
account: {
|
|
10318
11439
|
async getCurrentUserId() {
|
|
10319
|
-
return (await refreshSessionStatus(false)).user_id;
|
|
11440
|
+
return (await refreshSessionStatus(false, signal)).user_id;
|
|
10320
11441
|
},
|
|
10321
11442
|
async ensureAuthenticated({ signal: operationSignal }) {
|
|
10322
|
-
|
|
10323
|
-
return;
|
|
10324
|
-
await beginLogin();
|
|
10325
|
-
await waitForAbort(operationSignal);
|
|
11443
|
+
await refreshSessionStatus(false, operationSignal);
|
|
10326
11444
|
},
|
|
10327
11445
|
onChange(listener) {
|
|
10328
11446
|
accountListeners.add(listener);
|
|
@@ -10331,29 +11449,12 @@ function createWebHostInput(signal) {
|
|
|
10331
11449
|
},
|
|
10332
11450
|
authenticatedRequest: requestFromWebHost,
|
|
10333
11451
|
storage: { area: 'local', storage: localStorage },
|
|
10334
|
-
presentation: {
|
|
10335
|
-
close: async () =>
|
|
10336
|
-
|
|
10337
|
-
getViewportMetrics: () => ({
|
|
10338
|
-
windowWidth: window.innerWidth,
|
|
10339
|
-
windowHeight: window.innerHeight,
|
|
10340
|
-
screenWidth: window.screen.width,
|
|
10341
|
-
screenHeight: window.screen.height,
|
|
10342
|
-
pixelRatio: window.devicePixelRatio,
|
|
10343
|
-
}),
|
|
10344
|
-
getNavigationBarHeight: () => 0,
|
|
10345
|
-
setNavigationBarStyle: (style) => {
|
|
10346
|
-
document.documentElement.dataset.navigationBarStyle = style;
|
|
10347
|
-
},
|
|
10348
|
-
showToast: async ({ message, status }) => showTransientMessage(message, status),
|
|
10349
|
-
showLoading: async () => {
|
|
10350
|
-
elements.bridgeStatus.textContent = 'loading';
|
|
10351
|
-
},
|
|
10352
|
-
hideLoading: async () => {
|
|
10353
|
-
elements.bridgeStatus.textContent = activeRuntime?.state ?? 'idle';
|
|
11452
|
+
presentation: createBrowserDevHostPresentation({
|
|
11453
|
+
close: async () => {
|
|
11454
|
+
activeRuntime?.destroy();
|
|
10354
11455
|
},
|
|
10355
|
-
|
|
10356
|
-
},
|
|
11456
|
+
pushLog,
|
|
11457
|
+
}),
|
|
10357
11458
|
environment: {
|
|
10358
11459
|
getCurrentHref: () => location.href,
|
|
10359
11460
|
isDevRuntime: () => true,
|
|
@@ -10370,11 +11471,7 @@ function createWebHostInput(signal) {
|
|
|
10370
11471
|
hostHints: { launchKind: devContext.source === 'remote' ? 'dev-session' : 'anonymous-local-dev' },
|
|
10371
11472
|
}),
|
|
10372
11473
|
authPresentation: {
|
|
10373
|
-
|
|
10374
|
-
if (operationSignal.aborted)
|
|
10375
|
-
throw operationSignal.reason;
|
|
10376
|
-
return window.confirm(`允许小程序访问 ${scopes.join('、')}?`) ? { status: 'granted', grantedScopes: scopes } : { status: 'cancelled' };
|
|
10377
|
-
},
|
|
11474
|
+
confirmAuthorization: (request) => confirmBrowserDevHostAuthorization(request),
|
|
10378
11475
|
},
|
|
10379
11476
|
};
|
|
10380
11477
|
}
|
|
@@ -10404,44 +11501,82 @@ async function requestFromWebHost(request) {
|
|
|
10404
11501
|
signal: request.signal,
|
|
10405
11502
|
});
|
|
10406
11503
|
if (!anonymousPayload && response.status === 401) {
|
|
10407
|
-
void
|
|
11504
|
+
void accountStatusDrain.invalidate();
|
|
10408
11505
|
}
|
|
10409
11506
|
const payload = (await response.json());
|
|
10410
11507
|
if (!response.ok)
|
|
10411
11508
|
throw payload;
|
|
10412
11509
|
return payload;
|
|
10413
11510
|
}
|
|
10414
|
-
async function
|
|
10415
|
-
|
|
10416
|
-
|
|
10417
|
-
|
|
10418
|
-
|
|
10419
|
-
|
|
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,
|
|
10420
11524
|
});
|
|
10421
|
-
|
|
10422
|
-
|
|
10423
|
-
|
|
10424
|
-
|
|
10425
|
-
|
|
10426
|
-
|
|
10427
|
-
|
|
10428
|
-
|
|
10429
|
-
const
|
|
10430
|
-
|
|
10431
|
-
|
|
10432
|
-
|
|
10433
|
-
|
|
10434
|
-
|
|
10435
|
-
async function refreshSessionStatus(notify) {
|
|
10436
|
-
const response = await fetch(SESSION_STATUS_PATH, { cache: 'no-store', credentials: 'same-origin', redirect: 'error' });
|
|
10437
|
-
const status = (await response.json());
|
|
10438
|
-
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;
|
|
10439
11539
|
sessionRevision = status.revision;
|
|
10440
|
-
|
|
10441
|
-
|
|
11540
|
+
sessionAuthenticated = status.authenticated;
|
|
11541
|
+
sessionUserId = nextUserId;
|
|
11542
|
+
sessionUserNickname = nextNickname;
|
|
11543
|
+
sessionUserAvatar = nextAvatar;
|
|
11544
|
+
renderDeveloperAccount();
|
|
10442
11545
|
if (notify && changed)
|
|
10443
11546
|
accountListeners.forEach((listener) => listener(sessionUserId));
|
|
10444
|
-
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
|
+
});
|
|
10445
11580
|
}
|
|
10446
11581
|
function connectSessionEvents() {
|
|
10447
11582
|
sessionEventSource?.close();
|
|
@@ -10450,52 +11585,20 @@ function connectSessionEvents() {
|
|
|
10450
11585
|
return;
|
|
10451
11586
|
const source = new EventSource(SESSION_EVENTS_PATH, { withCredentials: true });
|
|
10452
11587
|
sessionEventSource = source;
|
|
10453
|
-
source.addEventListener('account-invalidated', () => void
|
|
10454
|
-
source.addEventListener('account-revision', () => void
|
|
11588
|
+
source.addEventListener('account-invalidated', () => void accountStatusDrain.invalidate());
|
|
11589
|
+
source.addEventListener('account-revision', () => void accountStatusDrain.invalidate());
|
|
10455
11590
|
source.onopen = () => {
|
|
10456
|
-
|
|
10457
|
-
clearSessionPoll();
|
|
11591
|
+
accountStatusPoll.reset();
|
|
10458
11592
|
};
|
|
10459
11593
|
source.onerror = () => {
|
|
10460
11594
|
source.close();
|
|
10461
|
-
|
|
11595
|
+
accountStatusPoll.schedule();
|
|
10462
11596
|
};
|
|
10463
11597
|
}
|
|
10464
|
-
function
|
|
10465
|
-
|
|
10466
|
-
accountRevisionTask = undefined;
|
|
10467
|
-
}));
|
|
10468
|
-
return accountRevisionTask;
|
|
10469
|
-
}
|
|
10470
|
-
async function processAccountRevision() {
|
|
10471
|
-
const previousRevision = sessionRevision;
|
|
10472
|
-
const status = await refreshSessionStatus(true);
|
|
10473
|
-
if (status.revision === previousRevision)
|
|
11598
|
+
async function reconcileAccountRevision(generation) {
|
|
11599
|
+
if (!isActiveHostGeneration(generation))
|
|
10474
11600
|
return;
|
|
10475
|
-
|
|
10476
|
-
if (status.authenticated)
|
|
10477
|
-
await restartRuntime('account revision changed');
|
|
10478
|
-
else
|
|
10479
|
-
await beginLogin();
|
|
10480
|
-
}
|
|
10481
|
-
function scheduleSessionPoll() {
|
|
10482
|
-
clearSessionPoll();
|
|
10483
|
-
sessionPollTimer = window.setTimeout(async () => {
|
|
10484
|
-
try {
|
|
10485
|
-
await handleAccountRevision();
|
|
10486
|
-
sessionPollDelay = SESSION_POLL_MIN_MS;
|
|
10487
|
-
connectSessionEvents();
|
|
10488
|
-
}
|
|
10489
|
-
catch {
|
|
10490
|
-
sessionPollDelay = Math.min(sessionPollDelay * 2, SESSION_POLL_MAX_MS);
|
|
10491
|
-
scheduleSessionPoll();
|
|
10492
|
-
}
|
|
10493
|
-
}, sessionPollDelay);
|
|
10494
|
-
}
|
|
10495
|
-
function clearSessionPoll() {
|
|
10496
|
-
if (sessionPollTimer !== undefined)
|
|
10497
|
-
window.clearTimeout(sessionPollTimer);
|
|
10498
|
-
sessionPollTimer = undefined;
|
|
11601
|
+
await restartRuntime('account revision changed');
|
|
10499
11602
|
}
|
|
10500
11603
|
function destroyActiveRuntime(reason) {
|
|
10501
11604
|
activeRuntime?.destroy();
|
|
@@ -10507,76 +11610,12 @@ function destroyActiveRuntime(reason) {
|
|
|
10507
11610
|
}
|
|
10508
11611
|
async function loadPersistedPermissionConfiguration() {
|
|
10509
11612
|
try {
|
|
10510
|
-
const
|
|
10511
|
-
const configuration = readPermissionConfiguration(await response.json());
|
|
10512
|
-
if (!response.ok || !configuration)
|
|
10513
|
-
throw new Error('权限持久化配置加载失败');
|
|
11613
|
+
const configuration = await fetchPermissionConfiguration(hostAbortController.signal);
|
|
10514
11614
|
applyPermissionConfiguration(configuration);
|
|
10515
11615
|
}
|
|
10516
11616
|
catch {
|
|
10517
|
-
|
|
10518
|
-
}
|
|
10519
|
-
}
|
|
10520
|
-
async function savePermissionOverride(status) {
|
|
10521
|
-
setPermissionControlsDisabled(true);
|
|
10522
|
-
permissionState = { networkRequest: { ...permissionState.networkRequest, status } };
|
|
10523
|
-
hasPermissionOverride = true;
|
|
10524
|
-
permissionActionMessage = '正在保存本地权限覆盖...';
|
|
10525
|
-
updatePermissionControls();
|
|
10526
|
-
try {
|
|
10527
|
-
const response = await requestJson(DEV_PERMISSIONS_PATH, {
|
|
10528
|
-
method: 'PUT',
|
|
10529
|
-
body: JSON.stringify({ runtime_permissions: createBrowserDevRuntimePermissions(permissionState) }),
|
|
10530
|
-
});
|
|
10531
|
-
const configuration = readPermissionConfiguration(await response.json());
|
|
10532
|
-
if (!response.ok || !configuration)
|
|
10533
|
-
throw new Error('保存失败');
|
|
10534
|
-
applyPermissionConfiguration(configuration, configuration.persistenceError || '本地权限覆盖已保存。');
|
|
10535
|
-
await restartRuntime('permission changed');
|
|
10536
|
-
void updateMobileQrCode();
|
|
10537
|
-
}
|
|
10538
|
-
catch (error) {
|
|
10539
|
-
permissionActionMessage = `${readErrorText(error)},当前页面仍会应用该权限,但刷新或重启后会丢失。`;
|
|
10540
|
-
}
|
|
10541
|
-
finally {
|
|
10542
|
-
setPermissionControlsDisabled(false);
|
|
10543
|
-
updatePermissionControls();
|
|
10544
|
-
}
|
|
10545
|
-
}
|
|
10546
|
-
async function resetPermissionOverride() {
|
|
10547
|
-
setPermissionControlsDisabled(true);
|
|
10548
|
-
try {
|
|
10549
|
-
const response = await requestJson(DEV_PERMISSIONS_PATH, { method: 'DELETE', body: '{}' });
|
|
10550
|
-
const configuration = readPermissionConfiguration(await response.json());
|
|
10551
|
-
if (!response.ok || !configuration)
|
|
10552
|
-
throw new Error('重置失败');
|
|
10553
|
-
applyPermissionConfiguration(configuration, configuration.source === 'remote' ? '已重新读取远端权限并清除本地覆盖。' : '已清除本地权限覆盖。');
|
|
10554
|
-
await restartRuntime('permission reset');
|
|
10555
|
-
void updateMobileQrCode();
|
|
11617
|
+
/* 权限覆盖读取失败时继续使用当前内存态。 */
|
|
10556
11618
|
}
|
|
10557
|
-
catch (error) {
|
|
10558
|
-
permissionActionMessage = `重置失败:${readErrorText(error)}`;
|
|
10559
|
-
}
|
|
10560
|
-
finally {
|
|
10561
|
-
setPermissionControlsDisabled(false);
|
|
10562
|
-
updatePermissionControls();
|
|
10563
|
-
}
|
|
10564
|
-
}
|
|
10565
|
-
function updatePermissionControls() {
|
|
10566
|
-
elements.devContextStatus.textContent =
|
|
10567
|
-
devContext.source === 'remote' ? `已连接线上配置${devContext.miniProgramId ? ` · ${devContext.miniProgramId}` : ''}` : '匿名本地沙箱';
|
|
10568
|
-
elements.networkPermissionSelect.value = permissionState.networkRequest.status;
|
|
10569
|
-
const remoteLabel = readPermissionStatusLabel(remotePermissionState.networkRequest.status);
|
|
10570
|
-
const localLabel = readPermissionStatusLabel(permissionState.networkRequest.status);
|
|
10571
|
-
const sourceDescription = hasPermissionOverride
|
|
10572
|
-
? `远端基线:${remoteLabel} · 本地覆盖:${localLabel}`
|
|
10573
|
-
: `${devContext.source === 'remote' ? '当前使用远端权限' : '当前使用匿名默认权限'}:${remoteLabel}`;
|
|
10574
|
-
elements.permissionPersistenceStatus.textContent = permissionActionMessage
|
|
10575
|
-
? `${sourceDescription} · ${permissionActionMessage}`
|
|
10576
|
-
: sourceDescription;
|
|
10577
|
-
elements.releaseDiagnostics.innerHTML = createMiniProgramDevReleaseDiagnostics(devContext, permissionState)
|
|
10578
|
-
.map((diagnostic) => `<li>${escapeHtml(diagnostic.message)}</li>`)
|
|
10579
|
-
.join('');
|
|
10580
11619
|
}
|
|
10581
11620
|
function readPermissionConfiguration(value) {
|
|
10582
11621
|
if (!isRecord(value) || (value.source !== 'anonymous' && value.source !== 'remote'))
|
|
@@ -10593,15 +11632,33 @@ function readPermissionConfiguration(value) {
|
|
|
10593
11632
|
persistenceError: typeof value.persistence_error === 'string' ? value.persistence_error : undefined,
|
|
10594
11633
|
};
|
|
10595
11634
|
}
|
|
10596
|
-
function
|
|
11635
|
+
async function fetchPermissionConfiguration(signal) {
|
|
11636
|
+
const response = await fetch(DEV_PERMISSIONS_PATH, {
|
|
11637
|
+
cache: 'no-store',
|
|
11638
|
+
credentials: 'same-origin',
|
|
11639
|
+
redirect: 'error',
|
|
11640
|
+
signal,
|
|
11641
|
+
});
|
|
11642
|
+
const configuration = readPermissionConfiguration(await response.json());
|
|
11643
|
+
if (!response.ok || !configuration)
|
|
11644
|
+
throw new Error('权限持久化配置加载失败');
|
|
11645
|
+
return configuration;
|
|
11646
|
+
}
|
|
11647
|
+
function applyPermissionConfiguration(configuration) {
|
|
10597
11648
|
devContext =
|
|
10598
11649
|
configuration.source === 'remote'
|
|
10599
11650
|
? { source: 'remote', miniProgramId: configuration.miniProgramId, runtimePermissions: configuration.remotePermissions }
|
|
10600
11651
|
: { source: 'anonymous', warning: devContext.warning };
|
|
10601
11652
|
remotePermissionState = clonePermissionState(configuration.remoteState);
|
|
10602
11653
|
permissionState = clonePermissionState(configuration.overrideState ?? configuration.remoteState);
|
|
10603
|
-
|
|
10604
|
-
|
|
11654
|
+
}
|
|
11655
|
+
async function refreshRuntimeForCanonicalPermission() {
|
|
11656
|
+
if (disposed)
|
|
11657
|
+
return;
|
|
11658
|
+
await restartRuntime('permission changed');
|
|
11659
|
+
if (disposed)
|
|
11660
|
+
return;
|
|
11661
|
+
await updateMobileQrCode();
|
|
10605
11662
|
}
|
|
10606
11663
|
async function loadBootstrap() {
|
|
10607
11664
|
const response = await fetch(BOOTSTRAP_PATH, { cache: 'no-store', credentials: 'same-origin', redirect: 'error' });
|
|
@@ -10610,6 +11667,10 @@ async function loadBootstrap() {
|
|
|
10610
11667
|
const value = await response.json();
|
|
10611
11668
|
if (!isRecord(value))
|
|
10612
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
|
+
}
|
|
10613
11674
|
return {
|
|
10614
11675
|
csrfToken: typeof value.csrfToken === 'string' ? value.csrfToken : undefined,
|
|
10615
11676
|
defaultLanAddressId: typeof value.defaultLanAddressId === 'string' ? value.defaultLanAddressId : undefined,
|
|
@@ -10617,6 +11678,8 @@ async function loadBootstrap() {
|
|
|
10617
11678
|
lanAddresses: Array.isArray(value.lanAddresses) ? value.lanAddresses.filter(isLanAddress) : [],
|
|
10618
11679
|
loginAvailable: value.loginAvailable === true,
|
|
10619
11680
|
macAppProtocol: typeof value.macAppProtocol === 'string' ? value.macAppProtocol : undefined,
|
|
11681
|
+
miniProgram: readMiniProgramSummary(value.miniProgram),
|
|
11682
|
+
mobileLaunchRedirect,
|
|
10620
11683
|
nativeAppLaunchUnavailableReason: typeof value.nativeAppLaunchUnavailableReason === 'string' ? value.nativeAppLaunchUnavailableReason : undefined,
|
|
10621
11684
|
};
|
|
10622
11685
|
}
|
|
@@ -10643,7 +11706,7 @@ async function setupMobileAppQr() {
|
|
|
10643
11706
|
setupMobileLanSelect();
|
|
10644
11707
|
await updateMobileQrCode();
|
|
10645
11708
|
}
|
|
10646
|
-
function setupMobileLanSelect() {
|
|
11709
|
+
function setupMobileLanSelect(preferredLanAddressId = elements.mobileLanSelect.options.length > 0 ? elements.mobileLanSelect.value : undefined) {
|
|
10647
11710
|
elements.mobileLanSelect.replaceChildren();
|
|
10648
11711
|
for (const address of lanAddresses) {
|
|
10649
11712
|
const option = document.createElement('option');
|
|
@@ -10651,8 +11714,15 @@ function setupMobileLanSelect() {
|
|
|
10651
11714
|
option.textContent = `${address.name} · ${address.address}`;
|
|
10652
11715
|
elements.mobileLanSelect.appendChild(option);
|
|
10653
11716
|
}
|
|
10654
|
-
if (
|
|
11717
|
+
if (preferredLanAddressId && lanAddresses.some(({ id }) => id === preferredLanAddressId)) {
|
|
11718
|
+
elements.mobileLanSelect.value = preferredLanAddressId;
|
|
11719
|
+
}
|
|
11720
|
+
else if (defaultLanAddressId && lanAddresses.some(({ id }) => id === defaultLanAddressId)) {
|
|
10655
11721
|
elements.mobileLanSelect.value = defaultLanAddressId;
|
|
11722
|
+
}
|
|
11723
|
+
else if (lanAddresses[0]) {
|
|
11724
|
+
elements.mobileLanSelect.value = lanAddresses[0].id;
|
|
11725
|
+
}
|
|
10656
11726
|
elements.mobileLanSelect.disabled = lanAddresses.length === 0;
|
|
10657
11727
|
}
|
|
10658
11728
|
async function updateMobileQrCode() {
|
|
@@ -10671,21 +11741,101 @@ async function updateMobileQrCode() {
|
|
|
10671
11741
|
const devSession = await createMobileDevSession(selected);
|
|
10672
11742
|
if (generation !== mobileQrGeneration || disposed)
|
|
10673
11743
|
return;
|
|
10674
|
-
const
|
|
10675
|
-
|
|
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 });
|
|
10676
11748
|
if (generation !== mobileQrGeneration || disposed)
|
|
10677
11749
|
return;
|
|
10678
11750
|
elements.mobileQrImage.src = imageUrl;
|
|
10679
|
-
elements.mobileQrStatus.textContent = `${
|
|
10680
|
-
scheduleMobileQrRefresh(
|
|
11751
|
+
elements.mobileQrStatus.textContent = `${devSession.address.name} · ${devSession.address.address}`;
|
|
11752
|
+
scheduleMobileQrRefresh(qr.expiresAt, generation);
|
|
10681
11753
|
}
|
|
10682
11754
|
catch (error) {
|
|
10683
11755
|
if (generation !== mobileQrGeneration || disposed)
|
|
10684
11756
|
return;
|
|
11757
|
+
if (error instanceof InvalidMobileLaunchRedirectError) {
|
|
11758
|
+
invalidateMobileLaunchRedirectQr();
|
|
11759
|
+
return;
|
|
11760
|
+
}
|
|
10685
11761
|
elements.mobileQrImage.removeAttribute('src');
|
|
10686
11762
|
elements.mobileQrStatus.textContent = readErrorText(error);
|
|
10687
11763
|
}
|
|
10688
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
|
+
}
|
|
10689
11839
|
async function createMobileDevSession(selected) {
|
|
10690
11840
|
const creation = mobileDevSessionCreation.then(() => requestMobileDevSession(selected));
|
|
10691
11841
|
mobileDevSessionCreation = creation.then(() => undefined, () => undefined);
|
|
@@ -10693,22 +11843,20 @@ async function createMobileDevSession(selected) {
|
|
|
10693
11843
|
}
|
|
10694
11844
|
async function requestMobileDevSession(selected) {
|
|
10695
11845
|
const hostUrl = new URL(selected.appUrl);
|
|
10696
|
-
const
|
|
10697
|
-
const response = await requestJson(
|
|
11846
|
+
const endpointUrl = new URL(MINI_PROGRAM_DEV_CONTEXT_PATH, location.origin);
|
|
11847
|
+
const response = await requestJson(endpointUrl.pathname, {
|
|
10698
11848
|
method: 'POST',
|
|
10699
|
-
body: JSON.stringify({
|
|
11849
|
+
body: JSON.stringify({
|
|
11850
|
+
mini_url_origin: hostUrl.origin,
|
|
11851
|
+
runtime_permissions: createMobileDevSessionRuntimePermissions(permissionState),
|
|
11852
|
+
}),
|
|
10700
11853
|
});
|
|
10701
11854
|
const value = await response.json();
|
|
10702
11855
|
if (!response.ok || !isRecord(value) || typeof value.token !== 'string' || typeof value.expires_at !== 'number') {
|
|
10703
11856
|
throw new Error('创建 Mobile Dev Session 失败');
|
|
10704
11857
|
}
|
|
10705
|
-
|
|
10706
|
-
return {
|
|
10707
|
-
}
|
|
10708
|
-
function applyDevContextToMobileQrPayload(payload, devContextUrl) {
|
|
10709
|
-
const target = new URL(payload);
|
|
10710
|
-
target.searchParams.set(MINI_PROGRAM_DEV_CONTEXT_QUERY_PARAM, devContextUrl);
|
|
10711
|
-
return target.toString();
|
|
11858
|
+
const latest = await reloadNativeLaunchConfigForDevSession(selected.id);
|
|
11859
|
+
return { address: latest, expiresAt: value.expires_at };
|
|
10712
11860
|
}
|
|
10713
11861
|
function scheduleMobileQrRefresh(expiresAt, generation) {
|
|
10714
11862
|
mobileQrRefreshTimer = window.setTimeout(() => {
|
|
@@ -10725,51 +11873,112 @@ async function refreshNativeLaunchConfig() {
|
|
|
10725
11873
|
const signature = readNativeLaunchConfigSignature(latest);
|
|
10726
11874
|
if (signature === nativeLaunchConfigSignature)
|
|
10727
11875
|
return;
|
|
10728
|
-
|
|
10729
|
-
lanAddresses = latest.lanAddresses;
|
|
10730
|
-
defaultLanAddressId = latest.defaultLanAddressId;
|
|
10731
|
-
macAppProtocol = latest.macAppProtocol;
|
|
10732
|
-
nativeAppLaunchUnavailableReason = latest.nativeAppLaunchUnavailableReason;
|
|
10733
|
-
setupMobileLanSelect();
|
|
10734
|
-
updateNativeAppLaunchAvailability();
|
|
11876
|
+
applyLatestNativeLaunchConfig(latest);
|
|
10735
11877
|
void updateMobileQrCode();
|
|
10736
11878
|
}
|
|
11879
|
+
catch (error) {
|
|
11880
|
+
if (error instanceof InvalidMobileLaunchRedirectError)
|
|
11881
|
+
invalidateMobileLaunchRedirectQr();
|
|
11882
|
+
}
|
|
10737
11883
|
finally {
|
|
10738
11884
|
nativeLaunchConfigRefreshInFlight = false;
|
|
10739
11885
|
}
|
|
10740
11886
|
}
|
|
11887
|
+
function invalidateMobileLaunchRedirectQr() {
|
|
11888
|
+
mobileQrGeneration += 1;
|
|
11889
|
+
clearMobileQrRefreshTimer();
|
|
11890
|
+
elements.mobileQrImage.removeAttribute('src');
|
|
11891
|
+
elements.mobileQrStatus.textContent = 'Mobile 短链接配置无效';
|
|
11892
|
+
}
|
|
10741
11893
|
function readNativeLaunchConfigSignature(value) {
|
|
10742
11894
|
return JSON.stringify({
|
|
10743
11895
|
defaultLanAddressId: value.defaultLanAddressId,
|
|
10744
11896
|
lanAddresses: value.lanAddresses,
|
|
10745
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,
|
|
10746
11905
|
nativeAppLaunchUnavailableReason: value.nativeAppLaunchUnavailableReason,
|
|
10747
11906
|
});
|
|
10748
11907
|
}
|
|
10749
|
-
|
|
10750
|
-
if (
|
|
10751
|
-
|
|
10752
|
-
|
|
10753
|
-
|
|
10754
|
-
|
|
10755
|
-
|
|
10756
|
-
|
|
10757
|
-
|
|
10758
|
-
|
|
10759
|
-
|
|
10760
|
-
|
|
10761
|
-
|
|
10762
|
-
|
|
10763
|
-
|
|
10764
|
-
|
|
10765
|
-
|
|
10766
|
-
|
|
10767
|
-
|
|
10768
|
-
|
|
10769
|
-
|
|
10770
|
-
|
|
10771
|
-
|
|
10772
|
-
|
|
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;
|
|
10773
11982
|
}
|
|
10774
11983
|
function updateStorageSnapshot() {
|
|
10775
11984
|
const snapshot = {};
|
|
@@ -10784,6 +11993,8 @@ function showTransientMessage(message, status) {
|
|
|
10784
11993
|
pushLog('host.message', { message, status });
|
|
10785
11994
|
}
|
|
10786
11995
|
function handleSessionNavigationError(error) {
|
|
11996
|
+
if (disposed)
|
|
11997
|
+
return;
|
|
10787
11998
|
showTransientMessage(readErrorText(error), 'error');
|
|
10788
11999
|
}
|
|
10789
12000
|
function pushLog(method, detail) {
|
|
@@ -10798,10 +12009,16 @@ function disposeBrowserDevHost() {
|
|
|
10798
12009
|
if (disposed)
|
|
10799
12010
|
return;
|
|
10800
12011
|
disposed = true;
|
|
12012
|
+
hostGeneration += 1;
|
|
12013
|
+
hostAbortController.abort(new DOMException('Browser Dev Host disposed', 'AbortError'));
|
|
12014
|
+
accountStatusDrain.dispose();
|
|
12015
|
+
permissionStatusDrain.dispose();
|
|
10801
12016
|
runtimeGeneration += 1;
|
|
10802
12017
|
destroyActiveRuntime('Browser Dev Host disposed');
|
|
10803
12018
|
sessionEventSource?.close();
|
|
10804
|
-
|
|
12019
|
+
channelClient?.dispose();
|
|
12020
|
+
channelClient = undefined;
|
|
12021
|
+
accountStatusPoll.dispose();
|
|
10805
12022
|
clearMobileQrRefreshTimer();
|
|
10806
12023
|
if (nativeLaunchConfigRefreshTimer !== undefined)
|
|
10807
12024
|
window.clearInterval(nativeLaunchConfigRefreshTimer);
|
|
@@ -10812,12 +12029,11 @@ function clearMobileQrRefreshTimer() {
|
|
|
10812
12029
|
window.clearTimeout(mobileQrRefreshTimer);
|
|
10813
12030
|
mobileQrRefreshTimer = undefined;
|
|
10814
12031
|
}
|
|
10815
|
-
function
|
|
10816
|
-
|
|
10817
|
-
|
|
10818
|
-
|
|
10819
|
-
|
|
10820
|
-
});
|
|
12032
|
+
function combineAbortSignals(primary, secondary) {
|
|
12033
|
+
return secondary ? AbortSignal.any([primary, secondary]) : primary;
|
|
12034
|
+
}
|
|
12035
|
+
function isActiveHostGeneration(generation) {
|
|
12036
|
+
return !disposed && generation === hostGeneration;
|
|
10821
12037
|
}
|
|
10822
12038
|
function readSelectedLanAddress() {
|
|
10823
12039
|
return lanAddresses.find(({ id }) => id === elements.mobileLanSelect.value);
|
|
@@ -10830,25 +12046,23 @@ function isLanAddress(value) {
|
|
|
10830
12046
|
typeof value.name === 'string' &&
|
|
10831
12047
|
(value.mobileAppQrPayload === undefined || typeof value.mobileAppQrPayload === 'string'));
|
|
10832
12048
|
}
|
|
10833
|
-
|
|
10834
|
-
if (
|
|
10835
|
-
|
|
10836
|
-
|
|
10837
|
-
|
|
10838
|
-
|
|
10839
|
-
|
|
10840
|
-
|
|
10841
|
-
|
|
10842
|
-
}
|
|
10843
|
-
|
|
10844
|
-
elements.networkPermissionSelect.disabled = disabled;
|
|
10845
|
-
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 };
|
|
10846
12060
|
}
|
|
10847
12061
|
function clonePermissionState(state) {
|
|
10848
12062
|
return { networkRequest: { ...state.networkRequest } };
|
|
10849
12063
|
}
|
|
10850
|
-
function
|
|
10851
|
-
return status ===
|
|
12064
|
+
function isSamePermissionState(left, right) {
|
|
12065
|
+
return (left.networkRequest.status === right.networkRequest.status && left.networkRequest.useOfficialDomain === right.networkRequest.useOfficialDomain);
|
|
10852
12066
|
}
|
|
10853
12067
|
function readErrorText(error) {
|
|
10854
12068
|
return error instanceof Error && error.message ? error.message : '操作失败';
|