@heybox/hb-sdk 0.8.0-alpha → 0.8.0-alpha.12
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 +106 -395
- package/README.md +107 -24
- package/THIRD_PARTY_NOTICES.md +1755 -0
- package/dist/cli-chunks/{build-DJWFSM1B.cjs → build-DNtjBmyy.cjs} +8 -5
- package/dist/cli-chunks/{context-DV2UK1Nz.cjs → context-CO_3KgM_.cjs} +39 -81
- package/dist/cli-chunks/{create-2HfoB48V.cjs → create-DG8JxrPU.cjs} +2 -2
- package/dist/cli-chunks/{dev-Dgt2zS9k.cjs → dev-C5U1Lo4e.cjs} +404 -577
- package/dist/cli-chunks/doctor-B50OXvrj.cjs +65 -0
- package/dist/cli-chunks/{index-D62ANeBv.cjs → index-B4nFqf-3.cjs} +50 -30
- package/dist/cli-chunks/{index-BjoSXl8C.cjs → index-B8FK1ojC.cjs} +2 -2
- package/dist/cli-chunks/{index.esm-CigcxJ2B.cjs → index.esm-DiX5WKSp.cjs} +8 -7
- package/dist/cli-chunks/{login-Cumknwdx.cjs → login-CBwmxsEK.cjs} +2 -2
- package/dist/cli-chunks/{project-vite-CcE-HMmd.cjs → project-vite-DWpmHOsV.cjs} +1 -1
- package/dist/cli-chunks/{remote-DDdP3xcE.cjs → remote-Dt-U4gDD.cjs} +57 -25
- package/dist/cli-chunks/{runtime-gate-DFjw66kF.cjs → runtime-gate-CMnGCdfq.cjs} +11 -3
- package/dist/cli-chunks/{runtime-permission-env-CjsCe5bp.cjs → runtime-permission-env-BcUsO3nG.cjs} +351 -0
- package/dist/cli-chunks/{session-BDi_AZSv.cjs → session-yy543BIB.cjs} +1 -1
- package/dist/cli-chunks/skill-B4SWt2o6.cjs +83 -0
- package/dist/cli-chunks/version-CmizPu7D.cjs +8 -0
- package/dist/cli.cjs +1 -1
- package/dist/devtools/browser-dev-host/assets/browser-dev-host-TzYf9L6C.js +99 -0
- package/dist/devtools/browser-dev-host/assets/heybox-logo-CogNENsk.svg +6 -0
- package/dist/devtools/browser-dev-host/assets/index-C5MZZDa5.js +567 -0
- package/dist/devtools/browser-dev-host/assets/index-P-ra4m1y.css +1 -0
- package/dist/devtools/browser-dev-host/assets/workbench-state-BwV7bm4n.js +5 -0
- package/dist/devtools/browser-dev-host/index.html +6 -435
- package/dist/index.cjs.js +1023 -29
- package/dist/index.esm.js +1023 -30
- package/dist/protocol.cjs.js +381 -37
- package/dist/protocol.esm.js +355 -38
- package/dist/templates/{vue3-vite-ts → vanilla-vite-js}/.gitignore.ejs +0 -1
- package/dist/templates/vanilla-vite-js/README.md.ejs +14 -0
- package/dist/templates/vanilla-vite-js/index.html.ejs +20 -0
- package/dist/templates/vanilla-vite-js/package.json.ejs +22 -0
- package/dist/templates/vanilla-vite-js/src/assets/heybox-logo.svg +8 -0
- package/dist/templates/vanilla-vite-js/src/main.js +39 -0
- package/dist/templates/vanilla-vite-js/src/styles.css +155 -0
- package/dist/templates/{vue3-vite-ts/vite.config.ts → vanilla-vite-js/vite.config.js} +1 -2
- package/dist/vite.cjs.js +281 -13
- package/dist/vite.esm.js +281 -13
- package/package.json +34 -11
- package/skill/SKILL.md +22 -20
- package/skill/references/api-protocol.md +100 -6
- package/skill/references/api-root.md +199 -28
- package/skill/references/cli.md +41 -24
- package/skill/references/examples.md +30 -1
- package/skill/references/llms-index.md +1 -1
- package/skill/references/recipes.md +139 -36
- package/skill/references/safety-boundaries.md +20 -5
- package/skill/skill.json +10 -5
- package/types/core/client.d.ts +17 -1
- package/types/core/mini-dev-console.d.ts +36 -0
- package/types/core/sdk.d.ts +3 -0
- package/types/core/singleton.d.ts +3 -0
- package/types/index.d.ts +4 -2
- package/types/miniapp-manifest/index.d.ts +1 -0
- package/types/miniapp-manifest/node.d.ts +3 -0
- package/types/miniapp-manifest/permissions.d.ts +37 -0
- package/types/miniapp-manifest/schema.d.ts +5 -0
- package/types/modules/files/index.d.ts +5 -0
- package/types/modules/files/registry.d.ts +35 -0
- package/types/modules/files/types.d.ts +159 -0
- package/types/modules/network/index.d.ts +50 -3
- package/types/modules/share/index.d.ts +1 -1
- package/types/modules/share/show-share-menu.d.ts +1 -1
- package/types/modules/share/types.d.ts +2 -4
- package/types/protocol/capabilities.d.ts +2 -2
- package/types/protocol/constants.d.ts +1 -1
- package/types/protocol/guards.d.ts +1 -1
- package/types/protocol/types.d.ts +1 -1
- package/types/protocol.d.ts +4 -3
- package/types/skill-metadata.d.ts +0 -4
- package/types/vite/index.d.ts +3 -1
- package/dist/cli-chunks/doctor-C95gIao_.cjs +0 -204
- package/dist/devtools/browser-dev-host/main.js +0 -12263
- package/dist/templates/vue3-vite-ts/README.md.ejs +0 -47
- package/dist/templates/vue3-vite-ts/index.html.ejs +0 -12
- package/dist/templates/vue3-vite-ts/package.json.ejs +0 -33
- package/dist/templates/vue3-vite-ts/src/App.vue +0 -78
- package/dist/templates/vue3-vite-ts/src/__tests__/App.spec.ts +0 -148
- package/dist/templates/vue3-vite-ts/src/auth-handoff.ts +0 -46
- package/dist/templates/vue3-vite-ts/src/main.ts +0 -5
- package/dist/templates/vue3-vite-ts/src/styles.css +0 -60
- package/dist/templates/vue3-vite-ts/src/vite-env.d.ts +0 -1
- package/dist/templates/vue3-vite-ts/tsconfig.app.json +0 -17
- package/dist/templates/vue3-vite-ts/tsconfig.json +0 -11
- package/dist/templates/vue3-vite-ts/tsconfig.node.json +0 -11
- package/dist/templates/vue3-vite-ts/vitest.config.ts +0 -10
- package/skill/scripts/check-references.mjs +0 -14
- package/skill/scripts/markdown-sections.mjs +0 -36
- package/skill/scripts/package-skill.mjs +0 -60
- package/skill/scripts/package-skill.sh +0 -6
- package/skill/scripts/skill-metadata.mjs +0 -77
- package/skill/scripts/sync-agent-skills-payload.mjs +0 -359
- package/skill/scripts/sync-references.mjs +0 -794
- package/skill/scripts/validate-skill.mjs +0 -263
package/dist/index.cjs.js
CHANGED
|
@@ -576,6 +576,22 @@ const VIEWPORT_SET_NAVIGATION_BAR_STYLE_METHOD = 'viewport.setNavigationBarStyle
|
|
|
576
576
|
const STORAGE_GET_STORAGE_METHOD = 'storage.getStorage';
|
|
577
577
|
const STORAGE_SET_STORAGE_METHOD = 'storage.setStorage';
|
|
578
578
|
const NETWORK_REQUEST_METHOD = 'network.request';
|
|
579
|
+
const NETWORK_DOWNLOAD_METHOD = 'network.download';
|
|
580
|
+
const FILES_PICK_FILES_METHOD = 'files.pickFiles';
|
|
581
|
+
const FILES_PICK_DIRECTORY_METHOD = 'files.pickDirectory';
|
|
582
|
+
const FILES_SAVE_FILE_METHOD = 'files.saveFile';
|
|
583
|
+
const FILE_CREATE_METHOD = 'file.create';
|
|
584
|
+
const FILE_REMOVE_METHOD = 'file.remove';
|
|
585
|
+
const FILE_EXISTS_METHOD = 'file.exists';
|
|
586
|
+
const FILE_READ_TEXT_METHOD = 'file.readText';
|
|
587
|
+
const FILE_WRITE_TEXT_METHOD = 'file.writeText';
|
|
588
|
+
const FILE_READ_BYTES_METHOD = 'file.readBytes';
|
|
589
|
+
const FILE_WRITE_BYTES_METHOD = 'file.writeBytes';
|
|
590
|
+
const FILE_STAT_METHOD = 'file.stat';
|
|
591
|
+
const DIRECTORY_CREATE_METHOD = 'directory.create';
|
|
592
|
+
const DIRECTORY_REMOVE_METHOD = 'directory.remove';
|
|
593
|
+
const DIRECTORY_EXISTS_METHOD = 'directory.exists';
|
|
594
|
+
const DIRECTORY_LIST_METHOD = 'directory.list';
|
|
579
595
|
const UI_SHOW_TOAST_METHOD = 'ui.showToast';
|
|
580
596
|
const UI_SHOW_LOADING_METHOD = 'ui.showLoading';
|
|
581
597
|
const UI_HIDE_LOADING_METHOD = 'ui.hideLoading';
|
|
@@ -597,20 +613,131 @@ const SDK_HANDSHAKE_METHOD = 'sdk.handshake';
|
|
|
597
613
|
const RUNTIME_LOCATION_PROBE_METHOD = 'runtime.location.probe';
|
|
598
614
|
const SDK_LOCATION_REPORT_METHOD = 'sdk.location.report';
|
|
599
615
|
const SDK_CSP_VIOLATION_METHOD = 'sdk.csp.violation';
|
|
616
|
+
const MINI_PROGRAM_OPERATION_PROGRESS_METHOD = 'operation.progress';
|
|
600
617
|
|
|
601
618
|
function isMiniProgramBridgeMessage(value) {
|
|
602
|
-
if (!value
|
|
619
|
+
if (!isRecord$1(value))
|
|
603
620
|
return false;
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
621
|
+
if (value.namespace !== MINI_PROGRAM_MESSAGE_NAMESPACE || (value.version !== 1 && value.version !== 2) || !isNonEmptyString(value.nonce)) {
|
|
622
|
+
return false;
|
|
623
|
+
}
|
|
624
|
+
if (value.type === 'handshake') {
|
|
625
|
+
return isNonEmptyString(value.id) && value.method === SDK_HANDSHAKE_METHOD && value.error === undefined && isSDKHandshakePayload(value.payload);
|
|
626
|
+
}
|
|
627
|
+
if (value.type === 'request') {
|
|
628
|
+
return isNonEmptyString(value.id) && isNonEmptyString(value.method) && value.error === undefined;
|
|
629
|
+
}
|
|
630
|
+
if (value.type === 'response') {
|
|
631
|
+
return (isNonEmptyString(value.id) &&
|
|
632
|
+
(value.method === undefined || isNonEmptyString(value.method)) &&
|
|
633
|
+
(value.error === undefined || isBridgeError(value.error)));
|
|
634
|
+
}
|
|
635
|
+
if (value.type === 'event') {
|
|
636
|
+
if (!isNonEmptyString(value.method) || value.error !== undefined)
|
|
637
|
+
return false;
|
|
638
|
+
if (value.method === MINI_PROGRAM_OPERATION_PROGRESS_METHOD) {
|
|
639
|
+
return value.version === 2 && isNonEmptyString(value.id) && isMiniProgramDownloadProgressPayload(value.payload);
|
|
640
|
+
}
|
|
641
|
+
return value.id === undefined || isNonEmptyString(value.id);
|
|
642
|
+
}
|
|
643
|
+
if (value.type === 'cancel') {
|
|
644
|
+
return (value.version === 2 && isNonEmptyString(value.id) && value.method === undefined && value.payload === undefined && value.error === undefined);
|
|
645
|
+
}
|
|
646
|
+
return false;
|
|
647
|
+
}
|
|
648
|
+
function isMiniProgramRelativePath(value) {
|
|
649
|
+
if (typeof value !== 'string' || !value || value.startsWith('/') || value.endsWith('/')) {
|
|
650
|
+
return false;
|
|
651
|
+
}
|
|
652
|
+
if (/[\\\u0000-\u001f\u007f]/.test(value))
|
|
653
|
+
return false;
|
|
654
|
+
return value.split('/').every((segment) => Boolean(segment) && segment !== '.' && segment !== '..');
|
|
655
|
+
}
|
|
656
|
+
function isMiniProgramPathRootRef(value) {
|
|
657
|
+
if (!isRecord$1(value))
|
|
658
|
+
return false;
|
|
659
|
+
if (value.kind === 'sandbox')
|
|
660
|
+
return hasOnlyKeys(value, ['kind']);
|
|
661
|
+
return value.kind === 'directory' && isNonEmptyString(value.handleId) && hasOnlyKeys(value, ['kind', 'handleId']);
|
|
662
|
+
}
|
|
663
|
+
function isMiniProgramFileRef(value) {
|
|
664
|
+
if (!isRecord$1(value) || value.kind !== 'file')
|
|
665
|
+
return false;
|
|
666
|
+
if (isNonEmptyString(value.handleId)) {
|
|
667
|
+
return hasOnlyKeys(value, ['kind', 'handleId']);
|
|
668
|
+
}
|
|
669
|
+
return (isMiniProgramPathRootRef(value.root) && isMiniProgramRelativePath(value.relativePath) && hasOnlyKeys(value, ['kind', 'root', 'relativePath']));
|
|
670
|
+
}
|
|
671
|
+
function isMiniProgramDirectoryRef(value) {
|
|
672
|
+
if (!isRecord$1(value) || value.kind !== 'directory')
|
|
673
|
+
return false;
|
|
674
|
+
if (isNonEmptyString(value.handleId)) {
|
|
675
|
+
return hasOnlyKeys(value, ['kind', 'handleId']);
|
|
676
|
+
}
|
|
677
|
+
return (isMiniProgramPathRootRef(value.root) && isMiniProgramRelativePath(value.relativePath) && hasOnlyKeys(value, ['kind', 'root', 'relativePath']));
|
|
678
|
+
}
|
|
679
|
+
function isMiniProgramByteArray(value) {
|
|
680
|
+
return value instanceof Uint8Array && value.buffer instanceof ArrayBuffer;
|
|
681
|
+
}
|
|
682
|
+
function isMiniProgramFileStat(value) {
|
|
683
|
+
return (isRecord$1(value) &&
|
|
684
|
+
value.kind === 'file' &&
|
|
685
|
+
isNonNegativeSafeInteger(value.size) &&
|
|
686
|
+
(value.modifiedAt === undefined || isNonNegativeFiniteNumber(value.modifiedAt)) &&
|
|
687
|
+
(value.createdAt === undefined || isNonNegativeFiniteNumber(value.createdAt)) &&
|
|
688
|
+
hasOnlyKeys(value, ['kind', 'size', 'modifiedAt', 'createdAt']));
|
|
689
|
+
}
|
|
690
|
+
function isMiniProgramDownloadProgressPayload(value) {
|
|
691
|
+
if (!isRecord$1(value) ||
|
|
692
|
+
!isNonNegativeSafeInteger(value.loaded) ||
|
|
693
|
+
typeof value.lengthComputable !== 'boolean' ||
|
|
694
|
+
!hasOnlyKeys(value, ['loaded', 'total', 'lengthComputable'])) {
|
|
695
|
+
return false;
|
|
696
|
+
}
|
|
697
|
+
if (!value.lengthComputable)
|
|
698
|
+
return value.total === undefined;
|
|
699
|
+
return isNonNegativeSafeInteger(value.total) && value.loaded <= value.total;
|
|
700
|
+
}
|
|
701
|
+
function isSDKHandshakePayload(value) {
|
|
702
|
+
return isRecord$1(value) && isNonEmptyString(value.href) && typeof value.userAgent === 'string' && isNonEmptyString(value.sdkVersion);
|
|
703
|
+
}
|
|
704
|
+
function isBridgeError(value) {
|
|
705
|
+
return isRecord$1(value) && isNonEmptyString(value.code) && typeof value.message === 'string';
|
|
706
|
+
}
|
|
707
|
+
function isNonNegativeSafeInteger(value) {
|
|
708
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
|
|
709
|
+
}
|
|
710
|
+
function isNonNegativeFiniteNumber(value) {
|
|
711
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0;
|
|
712
|
+
}
|
|
713
|
+
function isNonEmptyString(value) {
|
|
714
|
+
return typeof value === 'string' && value.length > 0;
|
|
715
|
+
}
|
|
716
|
+
function hasOnlyKeys(value, allowedKeys) {
|
|
717
|
+
const allowed = new Set(allowedKeys);
|
|
718
|
+
return Object.keys(value).every((key) => allowed.has(key));
|
|
719
|
+
}
|
|
720
|
+
function isRecord$1(value) {
|
|
721
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
609
722
|
}
|
|
610
723
|
|
|
611
724
|
/** 当前用户信息与登录能力支持的完整 scope 目录。 */
|
|
612
725
|
const USER_INFO_AUTHORIZATION_SCOPES = ['identity', 'profile'];
|
|
613
726
|
|
|
727
|
+
const MINI_PROGRAM_PERMISSION_KEYS = [
|
|
728
|
+
'userInfo',
|
|
729
|
+
'steamLibrary',
|
|
730
|
+
'share',
|
|
731
|
+
'storage',
|
|
732
|
+
'filesystem',
|
|
733
|
+
'clipboard',
|
|
734
|
+
'leaderboard',
|
|
735
|
+
'network',
|
|
736
|
+
];
|
|
737
|
+
new Set(MINI_PROGRAM_PERMISSION_KEYS);
|
|
738
|
+
|
|
739
|
+
new Set(MINI_PROGRAM_PERMISSION_KEYS);
|
|
740
|
+
|
|
614
741
|
/** 获取浏览器全局 window。 */
|
|
615
742
|
function getGlobalWindow() {
|
|
616
743
|
return typeof window === 'undefined' ? undefined : window;
|
|
@@ -672,7 +799,7 @@ function createMessageId() {
|
|
|
672
799
|
/** 构建时替换为当前发布包的实际版本。 */
|
|
673
800
|
const HB_SDK_VERSION = typeof undefined === 'string'
|
|
674
801
|
? undefined
|
|
675
|
-
: '0.8.0-alpha';
|
|
802
|
+
: '0.8.0-alpha.12';
|
|
676
803
|
|
|
677
804
|
const DEFAULT_TIMEOUT = 10000;
|
|
678
805
|
const HANDSHAKE_RETRY_INTERVAL = 250;
|
|
@@ -707,8 +834,7 @@ class MiniProgramBridgeClient {
|
|
|
707
834
|
constructor(options = {}) {
|
|
708
835
|
this.timeout = options.timeout || DEFAULT_TIMEOUT;
|
|
709
836
|
this.selfWindow = options.selfWindow || getGlobalWindow();
|
|
710
|
-
this.targetWindow =
|
|
711
|
-
options.targetWindow === undefined ? getParentWindow(this.selfWindow) : options.targetWindow;
|
|
837
|
+
this.targetWindow = options.targetWindow === undefined ? getParentWindow(this.selfWindow) : options.targetWindow;
|
|
712
838
|
this.targetOrigin = options.targetOrigin || readParentOrigin(this.selfWindow) || '*';
|
|
713
839
|
this.nonce = options.nonce || readBridgeNonce(this.selfWindow);
|
|
714
840
|
this.handleMessage = this.onMessage.bind(this);
|
|
@@ -741,20 +867,42 @@ class MiniProgramBridgeClient {
|
|
|
741
867
|
}
|
|
742
868
|
/** 调用父容器开放能力。 */
|
|
743
869
|
async request(method, ...args) {
|
|
870
|
+
return this.performRequest(method, args[0], true);
|
|
871
|
+
}
|
|
872
|
+
/** 发起不使用固定总时长 timer 的长操作。 */
|
|
873
|
+
async requestOperation(method, payload, options = {}) {
|
|
874
|
+
return this.performRequest(method, payload, false, options);
|
|
875
|
+
}
|
|
876
|
+
async performRequest(method, payload, useTimeout, options = {}) {
|
|
744
877
|
if (this.destroyed) {
|
|
745
878
|
throw createSDKError('SDK_DESTROYED', '小程序 SDK 已销毁');
|
|
746
879
|
}
|
|
747
880
|
if (this.runtimeUnavailable) {
|
|
748
881
|
throw this.getRuntimeUnavailableError();
|
|
749
882
|
}
|
|
750
|
-
|
|
883
|
+
if (!useTimeout && options.signal?.aborted) {
|
|
884
|
+
throw options.createAbortError?.() ?? createSDKError('REQUEST_CANCELLED', `${method} 已取消`);
|
|
885
|
+
}
|
|
886
|
+
if (!useTimeout && options.signal && !this.handshakeSettled) {
|
|
887
|
+
await waitForHandshakeOrAbort(this.waitForHandshake(), options.signal, () => options.createAbortError?.() ?? createSDKError('REQUEST_CANCELLED', `${method} 已取消`), () => {
|
|
888
|
+
const state = this.handshakeState.getState();
|
|
889
|
+
if (state.status === 'failed')
|
|
890
|
+
return state.error;
|
|
891
|
+
return this.destroyed ? createSDKError('SDK_DESTROYED', '小程序 SDK 已销毁') : undefined;
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
else {
|
|
895
|
+
await this.waitForHandshake();
|
|
896
|
+
}
|
|
751
897
|
if (this.destroyed) {
|
|
752
898
|
throw createSDKError('SDK_DESTROYED', '小程序 SDK 已销毁');
|
|
753
899
|
}
|
|
754
900
|
if (this.runtimeUnavailable) {
|
|
755
901
|
throw this.getRuntimeUnavailableError();
|
|
756
902
|
}
|
|
757
|
-
|
|
903
|
+
if (options.signal?.aborted) {
|
|
904
|
+
throw options.createAbortError?.() ?? createSDKError('REQUEST_CANCELLED', `${method} 已取消`);
|
|
905
|
+
}
|
|
758
906
|
const id = createMessageId();
|
|
759
907
|
const message = {
|
|
760
908
|
namespace: MINI_PROGRAM_MESSAGE_NAMESPACE,
|
|
@@ -766,27 +914,46 @@ class MiniProgramBridgeClient {
|
|
|
766
914
|
payload,
|
|
767
915
|
};
|
|
768
916
|
return new Promise((resolve, reject) => {
|
|
769
|
-
const
|
|
770
|
-
this.pendingRequests.delete(id);
|
|
771
|
-
reject(createSDKError('REQUEST_TIMEOUT', `${method} 调用超时`));
|
|
772
|
-
}, this.timeout);
|
|
773
|
-
this.pendingRequests.set(id, {
|
|
917
|
+
const pending = {
|
|
774
918
|
resolve: resolve,
|
|
775
919
|
reject,
|
|
776
|
-
|
|
777
|
-
|
|
920
|
+
isOperation: !useTimeout,
|
|
921
|
+
...(options.onProgress ? { onProgress: options.onProgress } : {}),
|
|
922
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
923
|
+
};
|
|
924
|
+
if (useTimeout) {
|
|
925
|
+
pending.timer = setTimeout(() => {
|
|
926
|
+
const active = this.pendingRequests.get(id);
|
|
927
|
+
if (!active) {
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
this.cleanupPending(active);
|
|
931
|
+
this.pendingRequests.delete(id);
|
|
932
|
+
reject(createSDKError('REQUEST_TIMEOUT', `${method} 调用超时`));
|
|
933
|
+
}, this.timeout);
|
|
934
|
+
}
|
|
935
|
+
this.pendingRequests.set(id, pending);
|
|
778
936
|
try {
|
|
779
937
|
this.postMessage(message);
|
|
780
938
|
}
|
|
781
939
|
catch (error) {
|
|
782
|
-
|
|
940
|
+
this.cleanupPending(pending);
|
|
783
941
|
this.pendingRequests.delete(id);
|
|
784
942
|
reject(error);
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
if (options.signal) {
|
|
946
|
+
pending.abortHandler = () => this.sendCancel(id);
|
|
947
|
+
options.signal.addEventListener('abort', pending.abortHandler, { once: true });
|
|
948
|
+
if (options.signal.aborted) {
|
|
949
|
+
pending.abortHandler();
|
|
950
|
+
}
|
|
785
951
|
}
|
|
786
952
|
});
|
|
787
953
|
}
|
|
788
954
|
/** 销毁 SDK 实例,并拒绝尚未完成的请求。 */
|
|
789
955
|
destroy() {
|
|
956
|
+
this.pendingRequests.forEach((_pending, id) => this.sendCancel(id));
|
|
790
957
|
this.destroyed = true;
|
|
791
958
|
this.selfWindow?.removeEventListener('message', this.handleMessage);
|
|
792
959
|
this.selfWindow?.removeEventListener('popstate', this.handlePopState);
|
|
@@ -839,16 +1006,12 @@ class MiniProgramBridgeClient {
|
|
|
839
1006
|
this.postHandshake();
|
|
840
1007
|
}
|
|
841
1008
|
catch (error) {
|
|
842
|
-
this.failHandshake(error instanceof HbMiniProgramSDKError
|
|
843
|
-
? error
|
|
844
|
-
: createSDKError('HANDSHAKE_FAILED', '小程序沙盒握手发送失败', error));
|
|
1009
|
+
this.failHandshake(error instanceof HbMiniProgramSDKError ? error : createSDKError('HANDSHAKE_FAILED', '小程序沙盒握手发送失败', error));
|
|
845
1010
|
}
|
|
846
1011
|
}, HANDSHAKE_RETRY_INTERVAL);
|
|
847
1012
|
}
|
|
848
1013
|
catch (error) {
|
|
849
|
-
this.failHandshake(error instanceof HbMiniProgramSDKError
|
|
850
|
-
? error
|
|
851
|
-
: createSDKError('HANDSHAKE_FAILED', '小程序沙盒握手发送失败', error));
|
|
1014
|
+
this.failHandshake(error instanceof HbMiniProgramSDKError ? error : createSDKError('HANDSHAKE_FAILED', '小程序沙盒握手发送失败', error));
|
|
852
1015
|
}
|
|
853
1016
|
}
|
|
854
1017
|
postHandshake() {
|
|
@@ -877,6 +1040,10 @@ class MiniProgramBridgeClient {
|
|
|
877
1040
|
return;
|
|
878
1041
|
}
|
|
879
1042
|
if (event.data.type === 'event') {
|
|
1043
|
+
if (event.data.id && event.data.method === MINI_PROGRAM_OPERATION_PROGRESS_METHOD) {
|
|
1044
|
+
this.handleOperationProgress(event.data);
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
880
1047
|
this.handleEvent(event.data);
|
|
881
1048
|
return;
|
|
882
1049
|
}
|
|
@@ -958,7 +1125,7 @@ class MiniProgramBridgeClient {
|
|
|
958
1125
|
if (!pending) {
|
|
959
1126
|
return;
|
|
960
1127
|
}
|
|
961
|
-
|
|
1128
|
+
this.cleanupPending(pending);
|
|
962
1129
|
this.pendingRequests.delete(message.id);
|
|
963
1130
|
if (message.error) {
|
|
964
1131
|
pending.reject(new HbMiniProgramSDKError(message.error));
|
|
@@ -966,6 +1133,52 @@ class MiniProgramBridgeClient {
|
|
|
966
1133
|
}
|
|
967
1134
|
pending.resolve(message.payload);
|
|
968
1135
|
}
|
|
1136
|
+
handleOperationProgress(message) {
|
|
1137
|
+
if (!message.id) {
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
const pending = this.pendingRequests.get(message.id);
|
|
1141
|
+
if (!pending?.onProgress) {
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
try {
|
|
1145
|
+
pending.onProgress(message.payload);
|
|
1146
|
+
}
|
|
1147
|
+
catch {
|
|
1148
|
+
// Mini-program 回调失败不能改变 Host 长操作的生命周期。
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
sendCancel(id) {
|
|
1152
|
+
const pending = this.pendingRequests.get(id);
|
|
1153
|
+
if (!pending?.isOperation || pending.cancelSent) {
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
pending.cancelSent = true;
|
|
1157
|
+
try {
|
|
1158
|
+
const message = {
|
|
1159
|
+
namespace: MINI_PROGRAM_MESSAGE_NAMESPACE,
|
|
1160
|
+
version: MINI_PROGRAM_MESSAGE_VERSION,
|
|
1161
|
+
nonce: this.nonce,
|
|
1162
|
+
type: 'cancel',
|
|
1163
|
+
id,
|
|
1164
|
+
};
|
|
1165
|
+
this.postMessage(message);
|
|
1166
|
+
}
|
|
1167
|
+
catch {
|
|
1168
|
+
// cancel 只是意图,发送失败后仍等待唯一 terminal response。
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
cleanupPending(pending) {
|
|
1172
|
+
if (pending.timer) {
|
|
1173
|
+
clearTimeout(pending.timer);
|
|
1174
|
+
}
|
|
1175
|
+
if (pending.signal && pending.abortHandler) {
|
|
1176
|
+
pending.signal.removeEventListener('abort', pending.abortHandler);
|
|
1177
|
+
}
|
|
1178
|
+
pending.onProgress = undefined;
|
|
1179
|
+
pending.abortHandler = undefined;
|
|
1180
|
+
pending.signal = undefined;
|
|
1181
|
+
}
|
|
969
1182
|
postMessage(message) {
|
|
970
1183
|
if (!this.targetWindow) {
|
|
971
1184
|
throw createSDKError('NOT_IN_IFRAME', '当前页面不在小程序沙盒 iframe 中');
|
|
@@ -1017,13 +1230,34 @@ class MiniProgramBridgeClient {
|
|
|
1017
1230
|
return this.runtimeUnavailableError;
|
|
1018
1231
|
}
|
|
1019
1232
|
rejectAllPending(error) {
|
|
1020
|
-
this.pendingRequests.forEach(pending => {
|
|
1021
|
-
|
|
1233
|
+
this.pendingRequests.forEach((pending) => {
|
|
1234
|
+
this.cleanupPending(pending);
|
|
1022
1235
|
pending.reject(error);
|
|
1023
1236
|
});
|
|
1024
1237
|
this.pendingRequests.clear();
|
|
1025
1238
|
}
|
|
1026
1239
|
}
|
|
1240
|
+
function waitForHandshakeOrAbort(handshake, signal, createAbortError, getTerminalError) {
|
|
1241
|
+
if (!signal)
|
|
1242
|
+
return handshake;
|
|
1243
|
+
if (signal.aborted)
|
|
1244
|
+
return Promise.reject(createAbortError());
|
|
1245
|
+
return new Promise((resolve, reject) => {
|
|
1246
|
+
let settled = false;
|
|
1247
|
+
const settle = (callback) => {
|
|
1248
|
+
if (settled)
|
|
1249
|
+
return;
|
|
1250
|
+
settled = true;
|
|
1251
|
+
signal.removeEventListener('abort', onAbort);
|
|
1252
|
+
callback();
|
|
1253
|
+
};
|
|
1254
|
+
const onAbort = () => settle(() => reject(getTerminalError() ?? createAbortError()));
|
|
1255
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
1256
|
+
handshake.then(() => settle(() => (signal.aborted ? reject(createAbortError()) : resolve())), (error) => settle(() => reject(error)));
|
|
1257
|
+
if (signal.aborted)
|
|
1258
|
+
onAbort();
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1027
1261
|
|
|
1028
1262
|
/** 获取短期授权码。 */
|
|
1029
1263
|
function login(requester, options) {
|
|
@@ -2265,7 +2499,7 @@ function screenshot(requester, options) {
|
|
|
2265
2499
|
}
|
|
2266
2500
|
|
|
2267
2501
|
/**
|
|
2268
|
-
*
|
|
2502
|
+
* 展示以当前小程序通用分享页为落地页的基础分享面板。
|
|
2269
2503
|
*
|
|
2270
2504
|
* @param requester - 底层 bridge 请求能力。
|
|
2271
2505
|
* @param options - 基础分享参数。
|
|
@@ -2326,6 +2560,373 @@ function createStorageModule(requester) {
|
|
|
2326
2560
|
};
|
|
2327
2561
|
}
|
|
2328
2562
|
|
|
2563
|
+
const registries = new WeakMap();
|
|
2564
|
+
const brandedFileSystemValues = new WeakSet();
|
|
2565
|
+
const CONTROL_CHARACTER_PATTERN = /[\u0000-\u001f\u007f]/;
|
|
2566
|
+
function invalidParams$1(message, data) {
|
|
2567
|
+
return createSDKError('INVALID_PARAMS', message, data);
|
|
2568
|
+
}
|
|
2569
|
+
function clonePathRootRef(ref) {
|
|
2570
|
+
return ref.kind === 'sandbox' ? Object.freeze({ kind: 'sandbox' }) : Object.freeze({ kind: 'directory', handleId: ref.handleId });
|
|
2571
|
+
}
|
|
2572
|
+
function cloneFileRef(ref) {
|
|
2573
|
+
return 'handleId' in ref
|
|
2574
|
+
? Object.freeze({ kind: 'file', handleId: ref.handleId })
|
|
2575
|
+
: Object.freeze({
|
|
2576
|
+
kind: 'file',
|
|
2577
|
+
root: clonePathRootRef(ref.root),
|
|
2578
|
+
relativePath: ref.relativePath,
|
|
2579
|
+
});
|
|
2580
|
+
}
|
|
2581
|
+
function cloneDirectoryRef(ref) {
|
|
2582
|
+
return 'handleId' in ref
|
|
2583
|
+
? Object.freeze({ kind: 'directory', handleId: ref.handleId })
|
|
2584
|
+
: Object.freeze({
|
|
2585
|
+
kind: 'directory',
|
|
2586
|
+
root: clonePathRootRef(ref.root),
|
|
2587
|
+
relativePath: ref.relativePath,
|
|
2588
|
+
});
|
|
2589
|
+
}
|
|
2590
|
+
function assertMode(mode) {
|
|
2591
|
+
if (mode !== 'read' && mode !== 'readwrite') {
|
|
2592
|
+
throw createSDKError('FILE_IO_FAILED', 'Host 返回了无效的文件访问模式');
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
function assertName(name) {
|
|
2596
|
+
if (typeof name !== 'string' || name.length === 0 || name.includes('/') || name.includes('\\') || CONTROL_CHARACTER_PATTERN.test(name)) {
|
|
2597
|
+
throw createSDKError('FILE_IO_FAILED', 'Host 返回了无效的文件名');
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
function assertFileRef(ref) {
|
|
2601
|
+
if (!isMiniProgramFileRef(ref)) {
|
|
2602
|
+
throw createSDKError('FILE_IO_FAILED', 'Host 返回了无效的 File handle');
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
function assertDirectoryRef(ref) {
|
|
2606
|
+
if (!isMiniProgramDirectoryRef(ref)) {
|
|
2607
|
+
throw createSDKError('FILE_IO_FAILED', 'Host 返回了无效的 Directory handle');
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
function defineReadonlyValue(target, key, value, enumerable = true) {
|
|
2611
|
+
Object.defineProperty(target, key, {
|
|
2612
|
+
value,
|
|
2613
|
+
enumerable,
|
|
2614
|
+
configurable: false,
|
|
2615
|
+
writable: false,
|
|
2616
|
+
});
|
|
2617
|
+
}
|
|
2618
|
+
function joinRelativePath(prefix, relativePath) {
|
|
2619
|
+
return prefix ? `${prefix}/${relativePath}` : relativePath;
|
|
2620
|
+
}
|
|
2621
|
+
function basename(relativePath) {
|
|
2622
|
+
const segments = relativePath.split('/');
|
|
2623
|
+
return segments[segments.length - 1];
|
|
2624
|
+
}
|
|
2625
|
+
function assertWritable(mode) {
|
|
2626
|
+
if (mode !== 'readwrite') {
|
|
2627
|
+
throw createSDKError('FILE_ACCESS_DENIED', '当前文件句柄不允许写入');
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
function assertSandboxRef(ref) {
|
|
2631
|
+
if ('handleId' in ref || ref.root.kind !== 'sandbox') {
|
|
2632
|
+
throw createSDKError('FILE_ACCESS_DENIED', '外部文件授权不允许删除');
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
function normalizeCreateOptions(options) {
|
|
2636
|
+
if (options !== undefined && (!options || typeof options !== 'object' || Array.isArray(options))) {
|
|
2637
|
+
throw invalidParams$1('create options 必须是对象');
|
|
2638
|
+
}
|
|
2639
|
+
if (options?.recursive !== undefined && typeof options.recursive !== 'boolean') {
|
|
2640
|
+
throw invalidParams$1('recursive 必须是 boolean');
|
|
2641
|
+
}
|
|
2642
|
+
if (options?.exclusive !== undefined && typeof options.exclusive !== 'boolean') {
|
|
2643
|
+
throw invalidParams$1('exclusive 必须是 boolean');
|
|
2644
|
+
}
|
|
2645
|
+
return {
|
|
2646
|
+
recursive: options?.recursive ?? false,
|
|
2647
|
+
exclusive: options?.exclusive ?? false,
|
|
2648
|
+
};
|
|
2649
|
+
}
|
|
2650
|
+
function normalizeListOptions(options) {
|
|
2651
|
+
if (options !== undefined && (!options || typeof options !== 'object' || Array.isArray(options))) {
|
|
2652
|
+
throw invalidParams$1('list options 必须是对象');
|
|
2653
|
+
}
|
|
2654
|
+
if (options?.limit !== undefined && (!Number.isSafeInteger(options.limit) || options.limit <= 0)) {
|
|
2655
|
+
throw invalidParams$1('limit 必须是正的 safe integer');
|
|
2656
|
+
}
|
|
2657
|
+
return options?.limit;
|
|
2658
|
+
}
|
|
2659
|
+
function normalizeRemoveOptions(options) {
|
|
2660
|
+
if (options !== undefined && (!options || typeof options !== 'object' || Array.isArray(options))) {
|
|
2661
|
+
throw invalidParams$1('remove options 必须是对象');
|
|
2662
|
+
}
|
|
2663
|
+
if (options?.recursive !== undefined && typeof options.recursive !== 'boolean') {
|
|
2664
|
+
throw invalidParams$1('recursive 必须是 boolean');
|
|
2665
|
+
}
|
|
2666
|
+
return { recursive: options?.recursive ?? false };
|
|
2667
|
+
}
|
|
2668
|
+
function cloneBytes(content) {
|
|
2669
|
+
if (!(content instanceof Uint8Array)) {
|
|
2670
|
+
throw invalidParams$1('content 必须是 Uint8Array');
|
|
2671
|
+
}
|
|
2672
|
+
if (typeof SharedArrayBuffer !== 'undefined' && content.buffer instanceof SharedArrayBuffer) {
|
|
2673
|
+
throw invalidParams$1('content 不能由 SharedArrayBuffer 承载');
|
|
2674
|
+
}
|
|
2675
|
+
return new Uint8Array(content);
|
|
2676
|
+
}
|
|
2677
|
+
function normalizeStat(stat) {
|
|
2678
|
+
if (!isMiniProgramFileStat(stat)) {
|
|
2679
|
+
throw createSDKError('FILE_IO_FAILED', 'Host 返回了无效的 FileStat');
|
|
2680
|
+
}
|
|
2681
|
+
return {
|
|
2682
|
+
kind: 'file',
|
|
2683
|
+
size: stat.size,
|
|
2684
|
+
...(stat.modifiedAt === undefined ? {} : { modifiedAt: stat.modifiedAt }),
|
|
2685
|
+
...(stat.createdAt === undefined ? {} : { createdAt: stat.createdAt }),
|
|
2686
|
+
};
|
|
2687
|
+
}
|
|
2688
|
+
/** 校验并返回平台无关的 `/` 分隔逻辑相对路径。 */
|
|
2689
|
+
function normalizeRelativePath(value) {
|
|
2690
|
+
if (!isMiniProgramRelativePath(value)) {
|
|
2691
|
+
throw invalidParams$1('relativePath 必须是规范的 `/` 分隔逻辑相对路径');
|
|
2692
|
+
}
|
|
2693
|
+
return value;
|
|
2694
|
+
}
|
|
2695
|
+
class MiniProgramFileHandleRegistry {
|
|
2696
|
+
requester;
|
|
2697
|
+
fileStates = new WeakMap();
|
|
2698
|
+
directoryStates = new WeakMap();
|
|
2699
|
+
pathRootStates = new WeakMap();
|
|
2700
|
+
sandboxRoot;
|
|
2701
|
+
constructor(requester) {
|
|
2702
|
+
this.requester = requester;
|
|
2703
|
+
}
|
|
2704
|
+
getSandbox() {
|
|
2705
|
+
if (this.sandboxRoot) {
|
|
2706
|
+
return this.sandboxRoot;
|
|
2707
|
+
}
|
|
2708
|
+
const sandbox = {};
|
|
2709
|
+
const state = {
|
|
2710
|
+
root: Object.freeze({ kind: 'sandbox' }),
|
|
2711
|
+
prefix: '',
|
|
2712
|
+
mode: 'readwrite',
|
|
2713
|
+
};
|
|
2714
|
+
this.definePathRootMethods(sandbox, state);
|
|
2715
|
+
this.pathRootStates.set(sandbox, state);
|
|
2716
|
+
brandedFileSystemValues.add(sandbox);
|
|
2717
|
+
this.sandboxRoot = Object.freeze(sandbox);
|
|
2718
|
+
return this.sandboxRoot;
|
|
2719
|
+
}
|
|
2720
|
+
createFile(descriptor) {
|
|
2721
|
+
this.assertFileDescriptor(descriptor);
|
|
2722
|
+
const ref = cloneFileRef(descriptor.ref);
|
|
2723
|
+
const state = { ref, mode: descriptor.mode };
|
|
2724
|
+
const handle = {};
|
|
2725
|
+
defineReadonlyValue(handle, 'kind', 'file');
|
|
2726
|
+
defineReadonlyValue(handle, 'name', descriptor.name);
|
|
2727
|
+
defineReadonlyValue(handle, 'mode', descriptor.mode);
|
|
2728
|
+
defineReadonlyValue(handle, 'create', async (options) => {
|
|
2729
|
+
assertWritable(state.mode);
|
|
2730
|
+
await this.requester.request(FILE_CREATE_METHOD, {
|
|
2731
|
+
target: state.ref,
|
|
2732
|
+
...normalizeCreateOptions(options),
|
|
2733
|
+
});
|
|
2734
|
+
return handle;
|
|
2735
|
+
}, false);
|
|
2736
|
+
defineReadonlyValue(handle, 'remove', async () => {
|
|
2737
|
+
assertWritable(state.mode);
|
|
2738
|
+
assertSandboxRef(state.ref);
|
|
2739
|
+
await this.requester.request(FILE_REMOVE_METHOD, { target: state.ref });
|
|
2740
|
+
}, false);
|
|
2741
|
+
defineReadonlyValue(handle, 'exists', async () => {
|
|
2742
|
+
const result = await this.requester.request(FILE_EXISTS_METHOD, { target: state.ref });
|
|
2743
|
+
if (typeof result !== 'boolean') {
|
|
2744
|
+
throw createSDKError('FILE_IO_FAILED', 'Host 返回了无效的 exists 结果');
|
|
2745
|
+
}
|
|
2746
|
+
return result;
|
|
2747
|
+
}, false);
|
|
2748
|
+
defineReadonlyValue(handle, 'readText', async () => {
|
|
2749
|
+
const result = await this.requester.request(FILE_READ_TEXT_METHOD, { target: state.ref });
|
|
2750
|
+
if (typeof result !== 'string') {
|
|
2751
|
+
throw createSDKError('FILE_IO_FAILED', 'Host 返回了无效的文本内容');
|
|
2752
|
+
}
|
|
2753
|
+
return result;
|
|
2754
|
+
}, false);
|
|
2755
|
+
defineReadonlyValue(handle, 'writeText', async (content) => {
|
|
2756
|
+
assertWritable(state.mode);
|
|
2757
|
+
if (typeof content !== 'string') {
|
|
2758
|
+
throw invalidParams$1('content 必须是 string');
|
|
2759
|
+
}
|
|
2760
|
+
await this.requester.request(FILE_WRITE_TEXT_METHOD, { target: state.ref, content });
|
|
2761
|
+
}, false);
|
|
2762
|
+
defineReadonlyValue(handle, 'readBytes', async () => {
|
|
2763
|
+
const result = await this.requester.request(FILE_READ_BYTES_METHOD, { target: state.ref });
|
|
2764
|
+
if (!isMiniProgramByteArray(result)) {
|
|
2765
|
+
throw createSDKError('FILE_IO_FAILED', 'Host 返回了无效的二进制内容');
|
|
2766
|
+
}
|
|
2767
|
+
return new Uint8Array(result);
|
|
2768
|
+
}, false);
|
|
2769
|
+
defineReadonlyValue(handle, 'writeBytes', async (content) => {
|
|
2770
|
+
assertWritable(state.mode);
|
|
2771
|
+
const snapshot = cloneBytes(content);
|
|
2772
|
+
await this.requester.request(FILE_WRITE_BYTES_METHOD, { target: state.ref, content: snapshot });
|
|
2773
|
+
}, false);
|
|
2774
|
+
defineReadonlyValue(handle, 'stat', async () => {
|
|
2775
|
+
const result = await this.requester.request(FILE_STAT_METHOD, { target: state.ref });
|
|
2776
|
+
return normalizeStat(result);
|
|
2777
|
+
}, false);
|
|
2778
|
+
this.fileStates.set(handle, state);
|
|
2779
|
+
brandedFileSystemValues.add(handle);
|
|
2780
|
+
return Object.freeze(handle);
|
|
2781
|
+
}
|
|
2782
|
+
createDirectFile(descriptor) {
|
|
2783
|
+
this.assertFileDescriptor(descriptor);
|
|
2784
|
+
if (!('handleId' in descriptor.ref)) {
|
|
2785
|
+
throw createSDKError('FILE_IO_FAILED', 'Host 返回的 File 不是精确文件授权');
|
|
2786
|
+
}
|
|
2787
|
+
return this.createFile(descriptor);
|
|
2788
|
+
}
|
|
2789
|
+
createDirectory(descriptor) {
|
|
2790
|
+
this.assertDirectoryDescriptor(descriptor);
|
|
2791
|
+
const ref = cloneDirectoryRef(descriptor.ref);
|
|
2792
|
+
const state = { ref, mode: descriptor.mode };
|
|
2793
|
+
const pathRootState = 'handleId' in ref ? { root: ref, prefix: '', mode: descriptor.mode } : { root: ref.root, prefix: ref.relativePath, mode: descriptor.mode };
|
|
2794
|
+
const handle = {};
|
|
2795
|
+
defineReadonlyValue(handle, 'kind', 'directory');
|
|
2796
|
+
defineReadonlyValue(handle, 'name', descriptor.name);
|
|
2797
|
+
defineReadonlyValue(handle, 'mode', descriptor.mode);
|
|
2798
|
+
this.definePathRootMethods(handle, pathRootState);
|
|
2799
|
+
defineReadonlyValue(handle, 'create', async (options) => {
|
|
2800
|
+
assertWritable(state.mode);
|
|
2801
|
+
await this.requester.request(DIRECTORY_CREATE_METHOD, {
|
|
2802
|
+
target: state.ref,
|
|
2803
|
+
...normalizeCreateOptions(options),
|
|
2804
|
+
});
|
|
2805
|
+
return handle;
|
|
2806
|
+
}, false);
|
|
2807
|
+
defineReadonlyValue(handle, 'remove', async (options) => {
|
|
2808
|
+
assertWritable(state.mode);
|
|
2809
|
+
assertSandboxRef(state.ref);
|
|
2810
|
+
await this.requester.request(DIRECTORY_REMOVE_METHOD, {
|
|
2811
|
+
target: state.ref,
|
|
2812
|
+
...normalizeRemoveOptions(options),
|
|
2813
|
+
});
|
|
2814
|
+
}, false);
|
|
2815
|
+
defineReadonlyValue(handle, 'exists', async () => {
|
|
2816
|
+
const result = await this.requester.request(DIRECTORY_EXISTS_METHOD, { target: state.ref });
|
|
2817
|
+
if (typeof result !== 'boolean') {
|
|
2818
|
+
throw createSDKError('FILE_IO_FAILED', 'Host 返回了无效的 exists 结果');
|
|
2819
|
+
}
|
|
2820
|
+
return result;
|
|
2821
|
+
}, false);
|
|
2822
|
+
defineReadonlyValue(handle, 'list', async (options) => {
|
|
2823
|
+
const limit = normalizeListOptions(options);
|
|
2824
|
+
const result = await this.requester.request(DIRECTORY_LIST_METHOD, {
|
|
2825
|
+
target: state.ref,
|
|
2826
|
+
...(limit === undefined ? {} : { limit }),
|
|
2827
|
+
});
|
|
2828
|
+
if (!Array.isArray(result)) {
|
|
2829
|
+
throw createSDKError('FILE_IO_FAILED', 'Host 返回了无效的目录列表');
|
|
2830
|
+
}
|
|
2831
|
+
if (result.some((entry) => entry?.mode !== state.mode)) {
|
|
2832
|
+
throw createSDKError('FILE_IO_FAILED', 'Host 返回的目录项未继承父目录 mode');
|
|
2833
|
+
}
|
|
2834
|
+
return result.map((entry) => this.createEntity(entry));
|
|
2835
|
+
}, false);
|
|
2836
|
+
this.directoryStates.set(handle, state);
|
|
2837
|
+
this.pathRootStates.set(handle, pathRootState);
|
|
2838
|
+
brandedFileSystemValues.add(handle);
|
|
2839
|
+
return Object.freeze(handle);
|
|
2840
|
+
}
|
|
2841
|
+
createDirectDirectory(descriptor) {
|
|
2842
|
+
this.assertDirectoryDescriptor(descriptor);
|
|
2843
|
+
if (!('handleId' in descriptor.ref)) {
|
|
2844
|
+
throw createSDKError('FILE_IO_FAILED', 'Host 返回的 Directory 不是精确目录授权');
|
|
2845
|
+
}
|
|
2846
|
+
return this.createDirectory(descriptor);
|
|
2847
|
+
}
|
|
2848
|
+
createEntity(descriptor) {
|
|
2849
|
+
if (descriptor?.kind === 'file') {
|
|
2850
|
+
return this.createFile(descriptor);
|
|
2851
|
+
}
|
|
2852
|
+
if (descriptor?.kind === 'directory') {
|
|
2853
|
+
return this.createDirectory(descriptor);
|
|
2854
|
+
}
|
|
2855
|
+
throw createSDKError('FILE_IO_FAILED', 'Host 返回了无效的文件实体');
|
|
2856
|
+
}
|
|
2857
|
+
requireFile(handle) {
|
|
2858
|
+
const state = handle && typeof handle === 'object' ? this.fileStates.get(handle) : undefined;
|
|
2859
|
+
if (!state) {
|
|
2860
|
+
throw invalidParams$1('to 必须是当前 SDK session 创建的 FileHandle');
|
|
2861
|
+
}
|
|
2862
|
+
return state;
|
|
2863
|
+
}
|
|
2864
|
+
requireDirectory(handle) {
|
|
2865
|
+
const state = handle && typeof handle === 'object' ? this.directoryStates.get(handle) : undefined;
|
|
2866
|
+
if (!state) {
|
|
2867
|
+
throw invalidParams$1('to 必须是当前 SDK session 创建的 DirectoryHandle');
|
|
2868
|
+
}
|
|
2869
|
+
return state;
|
|
2870
|
+
}
|
|
2871
|
+
definePathRootMethods(target, state) {
|
|
2872
|
+
defineReadonlyValue(target, 'file', (relativePath) => {
|
|
2873
|
+
const normalized = normalizeRelativePath(relativePath);
|
|
2874
|
+
const fullPath = joinRelativePath(state.prefix, normalized);
|
|
2875
|
+
return this.createFile({
|
|
2876
|
+
kind: 'file',
|
|
2877
|
+
name: basename(normalized),
|
|
2878
|
+
mode: state.mode,
|
|
2879
|
+
ref: {
|
|
2880
|
+
kind: 'file',
|
|
2881
|
+
root: state.root,
|
|
2882
|
+
relativePath: fullPath,
|
|
2883
|
+
},
|
|
2884
|
+
});
|
|
2885
|
+
}, false);
|
|
2886
|
+
defineReadonlyValue(target, 'directory', (relativePath) => {
|
|
2887
|
+
const normalized = normalizeRelativePath(relativePath);
|
|
2888
|
+
const fullPath = joinRelativePath(state.prefix, normalized);
|
|
2889
|
+
return this.createDirectory({
|
|
2890
|
+
kind: 'directory',
|
|
2891
|
+
name: basename(normalized),
|
|
2892
|
+
mode: state.mode,
|
|
2893
|
+
ref: {
|
|
2894
|
+
kind: 'directory',
|
|
2895
|
+
root: state.root,
|
|
2896
|
+
relativePath: fullPath,
|
|
2897
|
+
},
|
|
2898
|
+
});
|
|
2899
|
+
}, false);
|
|
2900
|
+
}
|
|
2901
|
+
assertFileDescriptor(descriptor) {
|
|
2902
|
+
if (!descriptor || descriptor.kind !== 'file') {
|
|
2903
|
+
throw createSDKError('FILE_IO_FAILED', 'Host 返回了无效的 File descriptor');
|
|
2904
|
+
}
|
|
2905
|
+
assertName(descriptor.name);
|
|
2906
|
+
assertMode(descriptor.mode);
|
|
2907
|
+
assertFileRef(descriptor.ref);
|
|
2908
|
+
}
|
|
2909
|
+
assertDirectoryDescriptor(descriptor) {
|
|
2910
|
+
if (!descriptor || descriptor.kind !== 'directory') {
|
|
2911
|
+
throw createSDKError('FILE_IO_FAILED', 'Host 返回了无效的 Directory descriptor');
|
|
2912
|
+
}
|
|
2913
|
+
assertName(descriptor.name);
|
|
2914
|
+
assertMode(descriptor.mode);
|
|
2915
|
+
assertDirectoryRef(descriptor.ref);
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2918
|
+
function getFileHandleRegistry(requester) {
|
|
2919
|
+
let registry = registries.get(requester);
|
|
2920
|
+
if (!registry) {
|
|
2921
|
+
registry = new MiniProgramFileHandleRegistry(requester);
|
|
2922
|
+
registries.set(requester, registry);
|
|
2923
|
+
}
|
|
2924
|
+
return registry;
|
|
2925
|
+
}
|
|
2926
|
+
function isMiniProgramFileSystemValue(value) {
|
|
2927
|
+
return !!value && typeof value === 'object' && brandedFileSystemValues.has(value);
|
|
2928
|
+
}
|
|
2929
|
+
|
|
2329
2930
|
const LOG_PREFIX = '[hb-sdk][network.request]';
|
|
2330
2931
|
/**
|
|
2331
2932
|
* 记录 `network.request` 失败,保证异常路径在开发者控制台可观测。
|
|
@@ -2527,6 +3128,194 @@ function assertSupportedPublicNetworkRequest(config) {
|
|
|
2527
3128
|
if (isUnsupportedMultipartNetworkRequest(config)) {
|
|
2528
3129
|
throw createSDKError('INVALID_PARAMS', NETWORK_REQUEST_MULTIPART_UNSUPPORTED_MESSAGE);
|
|
2529
3130
|
}
|
|
3131
|
+
if (containsUnsupportedNetworkData(config.data)) {
|
|
3132
|
+
throw createSDKError('INVALID_PARAMS', 'network.request data 不支持 FileHandle、FileSystem、Blob、FormData 或二进制数据');
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
function containsUnsupportedNetworkData(value, visiting = new WeakSet(), scanned = new WeakSet()) {
|
|
3136
|
+
if (isMiniProgramFileSystemValue(value))
|
|
3137
|
+
return true;
|
|
3138
|
+
if (typeof Blob !== 'undefined' && value instanceof Blob)
|
|
3139
|
+
return true;
|
|
3140
|
+
if (typeof FormData !== 'undefined' && value instanceof FormData)
|
|
3141
|
+
return true;
|
|
3142
|
+
if (value instanceof ArrayBuffer || ArrayBuffer.isView(value))
|
|
3143
|
+
return true;
|
|
3144
|
+
if (typeof SharedArrayBuffer !== 'undefined' && value instanceof SharedArrayBuffer)
|
|
3145
|
+
return true;
|
|
3146
|
+
if (!value || typeof value !== 'object' || value instanceof URLSearchParams)
|
|
3147
|
+
return false;
|
|
3148
|
+
if (visiting.has(value))
|
|
3149
|
+
return true;
|
|
3150
|
+
if (scanned.has(value))
|
|
3151
|
+
return false;
|
|
3152
|
+
visiting.add(value);
|
|
3153
|
+
let unsupported = false;
|
|
3154
|
+
if (Array.isArray(value))
|
|
3155
|
+
unsupported = value.some((item) => containsUnsupportedNetworkData(item, visiting, scanned));
|
|
3156
|
+
if (isPlainObject(value)) {
|
|
3157
|
+
unsupported ||= Object.values(value).some((item) => containsUnsupportedNetworkData(item, visiting, scanned));
|
|
3158
|
+
}
|
|
3159
|
+
visiting.delete(value);
|
|
3160
|
+
if (!unsupported)
|
|
3161
|
+
scanned.add(value);
|
|
3162
|
+
return unsupported;
|
|
3163
|
+
}
|
|
3164
|
+
function assertFinitePositive(value, name) {
|
|
3165
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
|
|
3166
|
+
throw createSDKError('INVALID_PARAMS', `${name} 必须是有限正数`);
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
function normalizeDownloadProgress(value) {
|
|
3170
|
+
if (!value || typeof value !== 'object') {
|
|
3171
|
+
throw createSDKError('INVALID_NETWORK_RESPONSE', 'network.download 返回了无效的进度');
|
|
3172
|
+
}
|
|
3173
|
+
if (!isMiniProgramDownloadProgressPayload(value)) {
|
|
3174
|
+
throw createSDKError('INVALID_NETWORK_RESPONSE', 'network.download 返回了无效的进度');
|
|
3175
|
+
}
|
|
3176
|
+
return {
|
|
3177
|
+
loaded: value.loaded,
|
|
3178
|
+
...(value.total === undefined ? {} : { total: value.total }),
|
|
3179
|
+
lengthComputable: value.lengthComputable,
|
|
3180
|
+
};
|
|
3181
|
+
}
|
|
3182
|
+
function normalizeDownloadPayload(options, to, isFileTarget) {
|
|
3183
|
+
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
3184
|
+
throw createSDKError('INVALID_PARAMS', 'network.download options 必须是对象');
|
|
3185
|
+
}
|
|
3186
|
+
if (typeof options.url !== 'string' || options.url.length === 0) {
|
|
3187
|
+
throw createSDKError('INVALID_PARAMS', 'url 必须是非空字符串');
|
|
3188
|
+
}
|
|
3189
|
+
if (options.params !== undefined && !isPlainObject(options.params)) {
|
|
3190
|
+
throw createSDKError('INVALID_PARAMS', 'params 必须是对象');
|
|
3191
|
+
}
|
|
3192
|
+
if (options.headers !== undefined) {
|
|
3193
|
+
if (!isPlainObject(options.headers) || Object.values(options.headers).some((value) => typeof value !== 'string')) {
|
|
3194
|
+
throw createSDKError('INVALID_PARAMS', 'headers 必须是字符串字典');
|
|
3195
|
+
}
|
|
3196
|
+
}
|
|
3197
|
+
if (options.timeout !== undefined) {
|
|
3198
|
+
assertFinitePositive(options.timeout, 'timeout');
|
|
3199
|
+
}
|
|
3200
|
+
if (options.maxBytes !== undefined && (!Number.isSafeInteger(options.maxBytes) || options.maxBytes <= 0)) {
|
|
3201
|
+
throw createSDKError('INVALID_PARAMS', 'maxBytes 必须是正的 safe integer');
|
|
3202
|
+
}
|
|
3203
|
+
if (options.withCredentials !== undefined && typeof options.withCredentials !== 'boolean') {
|
|
3204
|
+
throw createSDKError('INVALID_PARAMS', 'withCredentials 必须是 boolean');
|
|
3205
|
+
}
|
|
3206
|
+
if (options.onProgress !== undefined && typeof options.onProgress !== 'function') {
|
|
3207
|
+
throw createSDKError('INVALID_PARAMS', 'onProgress 必须是函数');
|
|
3208
|
+
}
|
|
3209
|
+
if (options.signal !== undefined &&
|
|
3210
|
+
(typeof options.signal !== 'object' ||
|
|
3211
|
+
typeof options.signal.aborted !== 'boolean' ||
|
|
3212
|
+
typeof options.signal.addEventListener !== 'function' ||
|
|
3213
|
+
typeof options.signal.removeEventListener !== 'function')) {
|
|
3214
|
+
throw createSDKError('INVALID_PARAMS', 'signal 必须是 AbortSignal');
|
|
3215
|
+
}
|
|
3216
|
+
if ('method' in options || 'data' in options || 'body' in options) {
|
|
3217
|
+
throw createSDKError('INVALID_PARAMS', 'network.download 固定使用 GET 且不接受 body');
|
|
3218
|
+
}
|
|
3219
|
+
const base = {
|
|
3220
|
+
url: options.url,
|
|
3221
|
+
to,
|
|
3222
|
+
...(options.params === undefined ? {} : { params: clonePublicValue(options.params) }),
|
|
3223
|
+
...(options.headers === undefined ? {} : { headers: clonePublicValue(options.headers) }),
|
|
3224
|
+
...(options.timeout === undefined ? {} : { timeout: options.timeout }),
|
|
3225
|
+
...(options.withCredentials === undefined ? {} : { withCredentials: options.withCredentials }),
|
|
3226
|
+
...(options.maxBytes === undefined ? {} : { maxBytes: options.maxBytes }),
|
|
3227
|
+
};
|
|
3228
|
+
if (isFileTarget) {
|
|
3229
|
+
if (options.suggestedName !== undefined || options.overwrite !== undefined) {
|
|
3230
|
+
throw createSDKError('INVALID_PARAMS', 'File target 不接受 suggestedName 或 overwrite');
|
|
3231
|
+
}
|
|
3232
|
+
return base;
|
|
3233
|
+
}
|
|
3234
|
+
if (options.overwrite !== undefined && typeof options.overwrite !== 'boolean') {
|
|
3235
|
+
throw createSDKError('INVALID_PARAMS', 'overwrite 必须是 boolean');
|
|
3236
|
+
}
|
|
3237
|
+
let suggestedName;
|
|
3238
|
+
if (options.suggestedName !== undefined) {
|
|
3239
|
+
suggestedName = normalizeRelativePath(options.suggestedName);
|
|
3240
|
+
if (suggestedName.includes('/')) {
|
|
3241
|
+
throw createSDKError('INVALID_PARAMS', 'suggestedName 必须是 basename');
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
return {
|
|
3245
|
+
...base,
|
|
3246
|
+
...(suggestedName === undefined ? {} : { suggestedName }),
|
|
3247
|
+
...(options.overwrite === undefined ? {} : { overwrite: options.overwrite }),
|
|
3248
|
+
};
|
|
3249
|
+
}
|
|
3250
|
+
function isSamePathRoot(left, right) {
|
|
3251
|
+
if (left.kind !== right.kind)
|
|
3252
|
+
return false;
|
|
3253
|
+
return left.kind === 'sandbox' || (right.kind === 'directory' && left.handleId === right.handleId);
|
|
3254
|
+
}
|
|
3255
|
+
function isSameFileRef(left, right) {
|
|
3256
|
+
if ('handleId' in left || 'handleId' in right) {
|
|
3257
|
+
return 'handleId' in left && 'handleId' in right && left.handleId === right.handleId;
|
|
3258
|
+
}
|
|
3259
|
+
return left.relativePath === right.relativePath && isSamePathRoot(left.root, right.root);
|
|
3260
|
+
}
|
|
3261
|
+
function isDirectChildFileRef(directory, file) {
|
|
3262
|
+
if ('handleId' in file)
|
|
3263
|
+
return false;
|
|
3264
|
+
if ('handleId' in directory) {
|
|
3265
|
+
return file.root.kind === 'directory' && file.root.handleId === directory.handleId && !file.relativePath.includes('/');
|
|
3266
|
+
}
|
|
3267
|
+
if (!isSamePathRoot(directory.root, file.root))
|
|
3268
|
+
return false;
|
|
3269
|
+
const prefix = `${directory.relativePath}/`;
|
|
3270
|
+
if (!file.relativePath.startsWith(prefix))
|
|
3271
|
+
return false;
|
|
3272
|
+
const childName = file.relativePath.slice(prefix.length);
|
|
3273
|
+
return childName.length > 0 && !childName.includes('/');
|
|
3274
|
+
}
|
|
3275
|
+
function invalidDownloadResult(message) {
|
|
3276
|
+
throw createSDKError('INVALID_NETWORK_RESPONSE', message);
|
|
3277
|
+
}
|
|
3278
|
+
/** 发起不使用 bridge 固定总时长 timer 的流式下载。 */
|
|
3279
|
+
async function download(requester, options) {
|
|
3280
|
+
if (options?.signal?.aborted) {
|
|
3281
|
+
throw createSDKError('DOWNLOAD_CANCELLED', '下载已取消');
|
|
3282
|
+
}
|
|
3283
|
+
const registry = getFileHandleRegistry(requester);
|
|
3284
|
+
const isFileTarget = options?.to?.kind === 'file';
|
|
3285
|
+
const target = isFileTarget ? registry.requireFile(options?.to) : registry.requireDirectory(options?.to);
|
|
3286
|
+
if (target.mode !== 'readwrite') {
|
|
3287
|
+
throw createSDKError('FILE_ACCESS_DENIED', '下载 target 必须是 readwrite');
|
|
3288
|
+
}
|
|
3289
|
+
const payload = normalizeDownloadPayload(options, target.ref, isFileTarget);
|
|
3290
|
+
const result = await requester.requestOperation(NETWORK_DOWNLOAD_METHOD, payload, {
|
|
3291
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
3292
|
+
...(options.onProgress === undefined
|
|
3293
|
+
? {}
|
|
3294
|
+
: {
|
|
3295
|
+
onProgress: (progress) => {
|
|
3296
|
+
options.onProgress?.(normalizeDownloadProgress(progress));
|
|
3297
|
+
},
|
|
3298
|
+
}),
|
|
3299
|
+
createAbortError: () => createSDKError('DOWNLOAD_CANCELLED', '下载已取消'),
|
|
3300
|
+
});
|
|
3301
|
+
const resultHandle = registry.createFile(result);
|
|
3302
|
+
const resultState = registry.requireFile(resultHandle);
|
|
3303
|
+
if (resultHandle.mode !== 'readwrite') {
|
|
3304
|
+
invalidDownloadResult('network.download 返回的 File 不是 readwrite');
|
|
3305
|
+
}
|
|
3306
|
+
if (isFileTarget) {
|
|
3307
|
+
const original = options.to;
|
|
3308
|
+
if (resultHandle.name !== original.name || !isSameFileRef(target.ref, resultState.ref)) {
|
|
3309
|
+
invalidDownloadResult('network.download 返回的 File 与 target 不一致');
|
|
3310
|
+
}
|
|
3311
|
+
return original;
|
|
3312
|
+
}
|
|
3313
|
+
if (!isDirectChildFileRef(target.ref, resultState.ref) ||
|
|
3314
|
+
'handleId' in resultState.ref ||
|
|
3315
|
+
resultHandle.name !== resultState.ref.relativePath.split('/').at(-1)) {
|
|
3316
|
+
invalidDownloadResult('network.download 返回的 File 不在 target Directory 内');
|
|
3317
|
+
}
|
|
3318
|
+
return resultHandle;
|
|
2530
3319
|
}
|
|
2531
3320
|
/**
|
|
2532
3321
|
* 创建 network 模块。
|
|
@@ -2537,9 +3326,115 @@ function assertSupportedPublicNetworkRequest(config) {
|
|
|
2537
3326
|
function createNetworkModule(requester) {
|
|
2538
3327
|
return {
|
|
2539
3328
|
request: (config) => request(requester, config),
|
|
3329
|
+
download: (options) => download(requester, options),
|
|
2540
3330
|
};
|
|
2541
3331
|
}
|
|
2542
3332
|
|
|
3333
|
+
function invalidParams(message) {
|
|
3334
|
+
return createSDKError('INVALID_PARAMS', message);
|
|
3335
|
+
}
|
|
3336
|
+
function normalizeMode(mode) {
|
|
3337
|
+
if (mode === undefined) {
|
|
3338
|
+
return undefined;
|
|
3339
|
+
}
|
|
3340
|
+
if (mode !== 'read' && mode !== 'readwrite') {
|
|
3341
|
+
throw invalidParams('mode 必须是 read 或 readwrite');
|
|
3342
|
+
}
|
|
3343
|
+
return mode;
|
|
3344
|
+
}
|
|
3345
|
+
function assertOptionsObject(value, name) {
|
|
3346
|
+
if (value !== undefined && (!value || typeof value !== 'object' || Array.isArray(value))) {
|
|
3347
|
+
throw invalidParams(`${name} 必须是对象`);
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3350
|
+
function normalizeAccept(accept) {
|
|
3351
|
+
if (accept === undefined) {
|
|
3352
|
+
return undefined;
|
|
3353
|
+
}
|
|
3354
|
+
if (!Array.isArray(accept)) {
|
|
3355
|
+
throw invalidParams('accept 必须是扩展名数组');
|
|
3356
|
+
}
|
|
3357
|
+
return accept.map((extension) => {
|
|
3358
|
+
if (typeof extension !== 'string' ||
|
|
3359
|
+
!/^\.[a-z0-9][a-z0-9._-]*$/i.test(extension)) {
|
|
3360
|
+
throw invalidParams('accept 只能包含规范扩展名,例如 .json');
|
|
3361
|
+
}
|
|
3362
|
+
return extension;
|
|
3363
|
+
});
|
|
3364
|
+
}
|
|
3365
|
+
function normalizePickFilesOptions(options) {
|
|
3366
|
+
assertOptionsObject(options, 'pickFiles options');
|
|
3367
|
+
if (options?.multiple !== undefined && typeof options.multiple !== 'boolean') {
|
|
3368
|
+
throw invalidParams('multiple 必须是 boolean');
|
|
3369
|
+
}
|
|
3370
|
+
const mode = normalizeMode(options?.mode);
|
|
3371
|
+
const accept = normalizeAccept(options?.accept);
|
|
3372
|
+
if (options === undefined) {
|
|
3373
|
+
return undefined;
|
|
3374
|
+
}
|
|
3375
|
+
return {
|
|
3376
|
+
...(mode === undefined ? {} : { mode }),
|
|
3377
|
+
...(accept === undefined ? {} : { accept }),
|
|
3378
|
+
...(options.multiple === undefined ? {} : { multiple: options.multiple }),
|
|
3379
|
+
};
|
|
3380
|
+
}
|
|
3381
|
+
function normalizePickDirectoryOptions(options) {
|
|
3382
|
+
assertOptionsObject(options, 'pickDirectory options');
|
|
3383
|
+
const mode = normalizeMode(options?.mode);
|
|
3384
|
+
return options === undefined ? undefined : mode === undefined ? {} : { mode };
|
|
3385
|
+
}
|
|
3386
|
+
function normalizeSaveFileOptions(options) {
|
|
3387
|
+
assertOptionsObject(options, 'saveFile options');
|
|
3388
|
+
if (!options || typeof options.suggestedName !== 'string') {
|
|
3389
|
+
throw invalidParams('suggestedName 必须是非空字符串');
|
|
3390
|
+
}
|
|
3391
|
+
const suggestedName = normalizeRelativePath(options.suggestedName);
|
|
3392
|
+
if (suggestedName.includes('/')) {
|
|
3393
|
+
throw invalidParams('suggestedName 必须是 basename');
|
|
3394
|
+
}
|
|
3395
|
+
if ('accept' in options) {
|
|
3396
|
+
throw invalidParams('saveFile V1 不支持 accept');
|
|
3397
|
+
}
|
|
3398
|
+
return { suggestedName };
|
|
3399
|
+
}
|
|
3400
|
+
/** 创建当前 SDK session 的 files 模块。 */
|
|
3401
|
+
function createFilesModule(requester) {
|
|
3402
|
+
const registry = getFileHandleRegistry(requester);
|
|
3403
|
+
return Object.freeze({
|
|
3404
|
+
sandbox: registry.getSandbox(),
|
|
3405
|
+
async pickFiles(options) {
|
|
3406
|
+
const normalizedOptions = normalizePickFilesOptions(options);
|
|
3407
|
+
const descriptors = await requester.request(FILES_PICK_FILES_METHOD, normalizedOptions);
|
|
3408
|
+
if (!Array.isArray(descriptors) || descriptors.length === 0 || (normalizedOptions?.multiple !== true && descriptors.length > 1)) {
|
|
3409
|
+
throw createSDKError('FILE_IO_FAILED', 'Host 返回了无效的 picker 结果');
|
|
3410
|
+
}
|
|
3411
|
+
const expectedMode = normalizedOptions?.mode ?? 'read';
|
|
3412
|
+
const handles = descriptors.map((descriptor) => registry.createDirectFile(descriptor));
|
|
3413
|
+
if (handles.some((handle) => handle.mode !== expectedMode)) {
|
|
3414
|
+
throw createSDKError('FILE_IO_FAILED', 'Host 返回的 File mode 与 picker 请求不一致');
|
|
3415
|
+
}
|
|
3416
|
+
return handles;
|
|
3417
|
+
},
|
|
3418
|
+
async pickDirectory(options) {
|
|
3419
|
+
const normalizedOptions = normalizePickDirectoryOptions(options);
|
|
3420
|
+
const descriptor = await requester.request(FILES_PICK_DIRECTORY_METHOD, normalizedOptions);
|
|
3421
|
+
const handle = registry.createDirectDirectory(descriptor);
|
|
3422
|
+
if (handle.mode !== (normalizedOptions?.mode ?? 'read')) {
|
|
3423
|
+
throw createSDKError('FILE_IO_FAILED', 'Host 返回的 Directory mode 与 picker 请求不一致');
|
|
3424
|
+
}
|
|
3425
|
+
return handle;
|
|
3426
|
+
},
|
|
3427
|
+
async saveFile(options) {
|
|
3428
|
+
const descriptor = await requester.request(FILES_SAVE_FILE_METHOD, normalizeSaveFileOptions(options));
|
|
3429
|
+
const handle = registry.createDirectFile(descriptor);
|
|
3430
|
+
if (handle.mode !== 'readwrite') {
|
|
3431
|
+
throw createSDKError('FILE_IO_FAILED', 'Host 返回的 saveFile 不是 readwrite 授权');
|
|
3432
|
+
}
|
|
3433
|
+
return handle;
|
|
3434
|
+
},
|
|
3435
|
+
});
|
|
3436
|
+
}
|
|
3437
|
+
|
|
2543
3438
|
/**
|
|
2544
3439
|
* 获取当前小程序窗口信息。
|
|
2545
3440
|
*
|
|
@@ -2786,6 +3681,8 @@ class MiniProgramSDK {
|
|
|
2786
3681
|
storage;
|
|
2787
3682
|
/** 网络请求相关开放能力。 */
|
|
2788
3683
|
network;
|
|
3684
|
+
/** 受控文件与目录能力。 */
|
|
3685
|
+
files;
|
|
2789
3686
|
/** UI 交互相关开放能力。 */
|
|
2790
3687
|
ui;
|
|
2791
3688
|
/** 设备相关开放能力。 */
|
|
@@ -2802,6 +3699,7 @@ class MiniProgramSDK {
|
|
|
2802
3699
|
this.share = createShareModule(this.client, launchHref);
|
|
2803
3700
|
this.viewport = createViewportModule(this.client);
|
|
2804
3701
|
this.storage = createStorageModule(this.client);
|
|
3702
|
+
this.files = createFilesModule(this.client);
|
|
2805
3703
|
this.network = createNetworkModule(this.client);
|
|
2806
3704
|
this.ui = createUiModule(this.client);
|
|
2807
3705
|
this.device = createDeviceModule(this.client);
|
|
@@ -2868,7 +3766,91 @@ function createMiniProgramSDK(options) {
|
|
|
2868
3766
|
return new MiniProgramSDK(options);
|
|
2869
3767
|
}
|
|
2870
3768
|
|
|
3769
|
+
/**
|
|
3770
|
+
* iframe 与调试台页面的开发期 console 捕获转发。
|
|
3771
|
+
*
|
|
3772
|
+
* 双条件门控(iframe 转发):
|
|
3773
|
+
* 1. 构建期:`hb-sdk dev`(vite serve)注入 `false`;`hb-sdk build` /
|
|
3774
|
+
* `vite build`(生产)永不注入或替换为 `false`,本模块被死代码消除。
|
|
3775
|
+
* 2. 运行期:仅在 `window.name` 为 `__hb_sdk_mini_dev__` 的调试 iframe 中激活,
|
|
3776
|
+
* 用户设备上的正式运行环境没有该标记。
|
|
3777
|
+
*
|
|
3778
|
+
* 无侵入 patch `console.log/debug/info/warn/error`:原样放行(DevTools 不受影响)+
|
|
3779
|
+
* 通过 `window.parent.postMessage` 转发给调试台(CSP 的 connect-src 不约束 postMessage,
|
|
3780
|
+
* 网络上报由调试台页面代理)。发送全程静默:失败不得回打 console,避免自触发死循环。
|
|
3781
|
+
*
|
|
3782
|
+
* 激活时机:由 SDK 默认单例初始化时调用(业务 `import '@heybox/hb-sdk'` 即生效);
|
|
3783
|
+
* iframe 标记由调试台 `iframe.name` 与 vite dev 注入的无依赖 bootstrap 共同设置。
|
|
3784
|
+
* 调试台页面自身(runtime 跑在这里,`[hb-sdk-runtime]` 日志不经过 iframe)另用
|
|
3785
|
+
* `installMiniDevConsoleSelfCapture` 接入「日志」页签(source: 'host')。
|
|
3786
|
+
*/
|
|
3787
|
+
const MINI_DEV_IFRAME_WINDOW_NAME = '__hb_sdk_mini_dev__';
|
|
3788
|
+
const MINI_DEV_CONSOLE_EVENT_TYPE = 'hb-sdk:mini-dev-console';
|
|
3789
|
+
/** 调试台页面与 iframe bootstrap 共用的已安装标记(window/console 上的属性名)。 */
|
|
3790
|
+
const MINI_DEV_CONSOLE_INSTALL_FLAG = '__hb_sdk_mini_dev_console_installed__';
|
|
3791
|
+
/** 调试台页面自捕获(source: 'host')与 iframe 转发共用的已安装标记。 */
|
|
3792
|
+
const INSTALL_FLAG = MINI_DEV_CONSOLE_INSTALL_FLAG;
|
|
3793
|
+
function isDevLoggingEnabled() {
|
|
3794
|
+
if (typeof false !== 'undefined' && false !== true) {
|
|
3795
|
+
return false;
|
|
3796
|
+
}
|
|
3797
|
+
return typeof window !== 'undefined' && window.name === MINI_DEV_IFRAME_WINDOW_NAME;
|
|
3798
|
+
}
|
|
3799
|
+
const levels = ['log', 'debug', 'info', 'warn', 'error'];
|
|
3800
|
+
function patchConsoleLevels(consoleRef, deliver, owner) {
|
|
3801
|
+
if (!owner || owner[INSTALL_FLAG] === true) {
|
|
3802
|
+
return false;
|
|
3803
|
+
}
|
|
3804
|
+
let patched = false;
|
|
3805
|
+
for (const level of levels) {
|
|
3806
|
+
const original = consoleRef[level];
|
|
3807
|
+
if (typeof original !== 'function') {
|
|
3808
|
+
continue;
|
|
3809
|
+
}
|
|
3810
|
+
const forward = function (...args) {
|
|
3811
|
+
try {
|
|
3812
|
+
deliver(level, args);
|
|
3813
|
+
}
|
|
3814
|
+
catch {
|
|
3815
|
+
/* 转发失败必须静默,禁止回打 console 造成自触发循环。 */
|
|
3816
|
+
}
|
|
3817
|
+
return original.apply(this, args);
|
|
3818
|
+
};
|
|
3819
|
+
try {
|
|
3820
|
+
consoleRef[level] = forward;
|
|
3821
|
+
patched = true;
|
|
3822
|
+
}
|
|
3823
|
+
catch {
|
|
3824
|
+
return patched;
|
|
3825
|
+
}
|
|
3826
|
+
}
|
|
3827
|
+
if (patched) {
|
|
3828
|
+
owner[INSTALL_FLAG] = true;
|
|
3829
|
+
}
|
|
3830
|
+
return patched;
|
|
3831
|
+
}
|
|
3832
|
+
function installMiniDevConsoleForwarding(consoleRef = console) {
|
|
3833
|
+
if (!isDevLoggingEnabled()) {
|
|
3834
|
+
return;
|
|
3835
|
+
}
|
|
3836
|
+
patchConsoleLevels(consoleRef, (level, args) => {
|
|
3837
|
+
const message = {
|
|
3838
|
+
type: MINI_DEV_CONSOLE_EVENT_TYPE,
|
|
3839
|
+
detail: { level, args, timestamp: Date.now() },
|
|
3840
|
+
};
|
|
3841
|
+
window.parent?.postMessage(message, '*');
|
|
3842
|
+
}, typeof window === 'undefined' ? undefined : window);
|
|
3843
|
+
}
|
|
3844
|
+
|
|
2871
3845
|
let defaultSDK = createMiniProgramSDK();
|
|
3846
|
+
// 开发期调试台 console 捕获:双条件门控(vite serve 注入 define + 调试 iframe 标记),
|
|
3847
|
+
// 生产构建被死代码消除,正式环境无副作用。失败静默,绝不影响 SDK 初始化。
|
|
3848
|
+
try {
|
|
3849
|
+
installMiniDevConsoleForwarding();
|
|
3850
|
+
}
|
|
3851
|
+
catch {
|
|
3852
|
+
/* 捕获层异常不得阻断 SDK 单例创建。 */
|
|
3853
|
+
}
|
|
2872
3854
|
function getDefaultSDK() {
|
|
2873
3855
|
if (!defaultSDK) {
|
|
2874
3856
|
defaultSDK = createMiniProgramSDK();
|
|
@@ -2983,6 +3965,16 @@ const storage = {
|
|
|
2983
3965
|
/** 默认 SDK 实例的 network 模块。 */
|
|
2984
3966
|
const network = {
|
|
2985
3967
|
request: (config) => getDefaultSDK().network.request(config),
|
|
3968
|
+
download: (options) => getDefaultSDK().network.download(options),
|
|
3969
|
+
};
|
|
3970
|
+
/** 默认 SDK 实例的 files 模块。 */
|
|
3971
|
+
const files = {
|
|
3972
|
+
get sandbox() {
|
|
3973
|
+
return getDefaultSDK().files.sandbox;
|
|
3974
|
+
},
|
|
3975
|
+
pickFiles: (options) => getDefaultSDK().files.pickFiles(options),
|
|
3976
|
+
pickDirectory: (options) => getDefaultSDK().files.pickDirectory(options),
|
|
3977
|
+
saveFile: (options) => getDefaultSDK().files.saveFile(options),
|
|
2986
3978
|
};
|
|
2987
3979
|
/** 默认 SDK 实例的 UI 模块。 */
|
|
2988
3980
|
const ui = {
|
|
@@ -3022,6 +4014,7 @@ const hbSDK = {
|
|
|
3022
4014
|
share,
|
|
3023
4015
|
viewport,
|
|
3024
4016
|
storage,
|
|
4017
|
+
files,
|
|
3025
4018
|
network,
|
|
3026
4019
|
ui,
|
|
3027
4020
|
device,
|
|
@@ -3036,6 +4029,7 @@ exports.auth = auth;
|
|
|
3036
4029
|
exports.cloud = cloud;
|
|
3037
4030
|
exports.default = hbSDK;
|
|
3038
4031
|
exports.device = device;
|
|
4032
|
+
exports.files = files;
|
|
3039
4033
|
exports.getHandshakeState = getHandshakeState;
|
|
3040
4034
|
exports.navigation = navigation;
|
|
3041
4035
|
exports.network = network;
|