@open-dy/vibecoding-sdk-taro 0.1.12 → 0.1.14
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/dist/adapters/interest-card/runtime/microapp.d.ts +1 -1
- package/dist/adapters/interest-card/runtime/microapp.js +1 -1
- package/dist/adapters/interest-card/runtime/microapp.js.map +1 -1
- package/dist/reference/llms.txt +3 -1
- package/dist/reference/sdk-api/runtime/CreateMicroappClientOptions.md +3 -1
- package/dist/reference/sdk-api/runtime/PreparedImageForUpload.md +52 -0
- package/dist/reference/sdk-api/runtime/createMicroappClient.md +4 -3
- package/dist/reference/sdk-api/runtime/index.md +3 -1
- package/dist/reference/sdk-api/runtime/prepareImageForUpload.md +69 -0
- package/dist/reference/sdk-api.json +144 -6
- package/dist/reference/sdk-api.md +2 -0
- package/dist/runtime/client.d.ts +3 -1
- package/dist/runtime/client.js +20 -5
- package/dist/runtime/client.js.map +1 -1
- package/dist/runtime/default-client.d.ts +2 -2
- package/dist/runtime/default-client.js +2 -2
- package/dist/runtime/douyin-container-request.d.ts +3 -2
- package/dist/runtime/douyin-container-request.js +24 -15
- package/dist/runtime/douyin-container-request.js.map +1 -1
- package/dist/runtime/globals.d.ts +2 -2
- package/dist/runtime/image-upload-preparation.d.ts +39 -0
- package/dist/runtime/image-upload-preparation.js +190 -0
- package/dist/runtime/image-upload-preparation.js.map +1 -0
- package/dist/runtime/index.d.ts +1 -0
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/prepared-upload-task.d.ts +3 -0
- package/dist/runtime/prepared-upload-task.js +91 -0
- package/dist/runtime/prepared-upload-task.js.map +1 -0
- package/package.json +1 -1
- package/release-provenance.json +4 -4
- package/src/adapters/interest-card/runtime/microapp.ts +2 -0
- package/src/runtime/client.ts +25 -5
- package/src/runtime/default-client.ts +2 -2
- package/src/runtime/douyin-container-request.ts +26 -20
- package/src/runtime/globals.d.ts +2 -2
- package/src/runtime/image-upload-preparation.ts +225 -0
- package/src/runtime/index.ts +1 -0
- package/src/runtime/prepared-upload-task.ts +86 -0
|
@@ -6,7 +6,8 @@ const ttRuntime: DouyinMiniProgramRuntime | undefined =
|
|
|
6
6
|
// @ts-ignore
|
|
7
7
|
typeof tt !== "undefined" ? tt : undefined;
|
|
8
8
|
|
|
9
|
-
let douyinCloud:
|
|
9
|
+
let douyinCloud: DouyinMiniProgramCloudInstance | null | undefined;
|
|
10
|
+
let devPreviewUploadCloud: DouyinMiniProgramCloudInstance | null | undefined;
|
|
10
11
|
|
|
11
12
|
export const isDouyinMiniProgram = (): boolean =>
|
|
12
13
|
Taro.getEnv() === Taro.ENV_TYPE.TT;
|
|
@@ -23,6 +24,12 @@ export const isDouyinPreviewEnv = (): boolean => {
|
|
|
23
24
|
return getDouyinMicroappEnvType() === "preview";
|
|
24
25
|
};
|
|
25
26
|
|
|
27
|
+
export const isDouyinNativePreviewEnv = (): boolean => {
|
|
28
|
+
if (!isDouyinMiniProgram() || !isDouyinPreviewEnv()) return false;
|
|
29
|
+
const platform = ttRuntime?.getSystemInfoSync?.()?.platform;
|
|
30
|
+
return platform !== "devtools" && platform !== "web-devtools";
|
|
31
|
+
};
|
|
32
|
+
|
|
26
33
|
export const isDouyinTestEnv = (): boolean => {
|
|
27
34
|
return getDouyinMicroappEnvType() === "development";
|
|
28
35
|
};
|
|
@@ -92,23 +99,18 @@ const parseContainerResponseBody = (
|
|
|
92
99
|
return body;
|
|
93
100
|
};
|
|
94
101
|
|
|
95
|
-
|
|
96
|
-
if (
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const envID = getDouyinCloudEnvID();
|
|
101
|
-
if (!ttRuntime?.createCloud || !envID) {
|
|
102
|
-
douyinCloud = null;
|
|
103
|
-
return douyinCloud;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const serviceID = getDouyinCloudServiceID();
|
|
107
|
-
douyinCloud = ttRuntime.createCloud({
|
|
102
|
+
const createDouyinCloud = (envID: string, serviceID: string): DouyinMiniProgramCloudInstance | null => {
|
|
103
|
+
if (!ttRuntime?.createCloud || !envID) return null;
|
|
104
|
+
return ttRuntime.createCloud({
|
|
108
105
|
envID,
|
|
109
106
|
...(serviceID ? { serviceID } : {}),
|
|
110
107
|
});
|
|
108
|
+
};
|
|
111
109
|
|
|
110
|
+
export const getDouyinCloud = (): DouyinMiniProgramCloudInstance | null => {
|
|
111
|
+
if (douyinCloud === undefined) {
|
|
112
|
+
douyinCloud = createDouyinCloud(getDouyinCloudEnvID(), getDouyinCloudServiceID());
|
|
113
|
+
}
|
|
112
114
|
return douyinCloud;
|
|
113
115
|
};
|
|
114
116
|
|
|
@@ -257,21 +259,25 @@ export const requestSSEByDouyinContainer = (
|
|
|
257
259
|
return requestPromise;
|
|
258
260
|
};
|
|
259
261
|
|
|
260
|
-
export const uploadByDouyinContainer = (option: Taro.uploadFile.Option) => {
|
|
262
|
+
export const uploadByDouyinContainer = (option: Taro.uploadFile.Option, envType?: "development") => {
|
|
261
263
|
const error = getManagedUploadError({ fileName: option.fileName });
|
|
262
264
|
if (error) return rejectManagedUpload(option, error);
|
|
263
|
-
const
|
|
265
|
+
const useDevPreviewCloud = envType === "development";
|
|
266
|
+
const cloud = useDevPreviewCloud
|
|
267
|
+
? (devPreviewUploadCloud ??= createDouyinCloud(DOUYIN_CLOUD_DEV_ENV_ID, DOUYIN_CLOUD_SERVICE_ID))
|
|
268
|
+
: getDouyinCloud();
|
|
264
269
|
const uploadFile = cloud?.uploadFile;
|
|
265
270
|
if (!uploadFile) {
|
|
266
|
-
const envID = getDouyinCloudEnvID();
|
|
271
|
+
const envID = useDevPreviewCloud ? DOUYIN_CLOUD_DEV_ENV_ID : getDouyinCloudEnvID();
|
|
272
|
+
const envIDName = useDevPreviewCloud ? "douyin_cloud_dev_env_id" : getDouyinCloudEnvIDName();
|
|
267
273
|
const error = {
|
|
268
274
|
errMsg: envID
|
|
269
275
|
? "request:fail douyin cloud uploadFile is unavailable"
|
|
270
|
-
: `request:fail ${
|
|
276
|
+
: `request:fail ${envIDName} is required for createCloud`,
|
|
271
277
|
};
|
|
272
278
|
option.fail?.(error as never);
|
|
273
279
|
option.complete?.(error as never);
|
|
274
|
-
return Promise.reject(error)
|
|
280
|
+
return Promise.reject(error);
|
|
275
281
|
}
|
|
276
282
|
|
|
277
283
|
const requestPromise = (async () => {
|
|
@@ -307,7 +313,7 @@ export const uploadByDouyinContainer = (option: Taro.uploadFile.Option) => {
|
|
|
307
313
|
);
|
|
308
314
|
})();
|
|
309
315
|
|
|
310
|
-
return requestPromise
|
|
316
|
+
return requestPromise;
|
|
311
317
|
};
|
|
312
318
|
|
|
313
319
|
export const downloadByDouyinContainer = (option: Taro.downloadFile.Option) => {
|
package/src/runtime/globals.d.ts
CHANGED
|
@@ -42,7 +42,7 @@ type DouyinCallContainerFailResult = {
|
|
|
42
42
|
errMsg?: string;
|
|
43
43
|
};
|
|
44
44
|
|
|
45
|
-
type
|
|
45
|
+
type DouyinMiniProgramCloudInstance = {
|
|
46
46
|
callContainer?: <T = unknown>(options: {
|
|
47
47
|
path: string;
|
|
48
48
|
id?: string;
|
|
@@ -165,7 +165,7 @@ type DouyinMiniProgramRuntime = {
|
|
|
165
165
|
createCloud?: (options: {
|
|
166
166
|
envID: string;
|
|
167
167
|
serviceID?: string;
|
|
168
|
-
}) =>
|
|
168
|
+
}) => DouyinMiniProgramCloudInstance;
|
|
169
169
|
login?: (options: {
|
|
170
170
|
success?: (result: { code?: string; errMsg?: string }) => void;
|
|
171
171
|
fail?: (result: { errMsg?: string }) => void;
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { isDouyinMiniProgram } from './douyin-container-request'
|
|
2
|
+
|
|
3
|
+
const MIB = 1024 * 1024
|
|
4
|
+
const TARGET_BYTES = 2 * MIB
|
|
5
|
+
const LARGE_IMAGE_BYTES = 10 * MIB
|
|
6
|
+
const MAX_SOURCE_BYTES = 32 * MIB
|
|
7
|
+
const MAX_SOURCE_PIXELS = 24_000_000
|
|
8
|
+
const MAX_LARGE_SOURCE_PIXELS = 40_000_000
|
|
9
|
+
const PREPARATION_BUDGET_MS = 1500
|
|
10
|
+
const MAX_OUTPUT_RATIO = 0.9
|
|
11
|
+
const PREPARED_PATH_TTL_MS = 5000
|
|
12
|
+
|
|
13
|
+
type NativeCallbacks<T> = {
|
|
14
|
+
success: (result: T) => void
|
|
15
|
+
fail: (error: unknown) => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type NativeImageInfo = {
|
|
19
|
+
type?: string
|
|
20
|
+
width?: number
|
|
21
|
+
height?: number
|
|
22
|
+
orientation?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type NativeFileStat = { size?: number, lastModifiedTime?: number }
|
|
26
|
+
type NativeFileSystem = {
|
|
27
|
+
stat?: (options: NativeCallbacks<{ stats?: NativeFileStat }> & { path: string }) => void
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
declare const tt: {
|
|
31
|
+
getFileSystemManager?: () => NativeFileSystem
|
|
32
|
+
getImageInfo?: (options: NativeCallbacks<NativeImageInfo> & { src: string }) => void
|
|
33
|
+
compressImage?: (options: NativeCallbacks<{ tempFilePath?: string }> & { src: string, quality: number }) => void
|
|
34
|
+
} | undefined
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @desc 图片上传应使用的本地路径,以及可取得的压缩诊断信息。
|
|
38
|
+
* @example
|
|
39
|
+
* ```ts
|
|
40
|
+
* const { filePath, reason } = await prepareImageForUpload(tempFilePath)
|
|
41
|
+
* ```
|
|
42
|
+
* @ai-intent 读取静默压缩选出的路径和诊断信息。
|
|
43
|
+
* @ai-when 既有调用方需要读取显式预处理的结果时。
|
|
44
|
+
* @ai-avoid 不要把压缩诊断用作上传错误或用户提示。
|
|
45
|
+
* @ai-example const { filePath, reason } = await prepareImageForUpload(tempFilePath)
|
|
46
|
+
*/
|
|
47
|
+
export interface PreparedImageForUpload {
|
|
48
|
+
/** @desc 最终上传使用的本地文件路径。 */
|
|
49
|
+
filePath: string
|
|
50
|
+
/** @desc 原文件实际大小,单位字节。未取得时为 undefined。 */
|
|
51
|
+
originalBytes?: number
|
|
52
|
+
/** @desc 最终文件实际大小,单位字节。未取得时为 undefined。 */
|
|
53
|
+
bytes?: number
|
|
54
|
+
/** @desc 压缩采用或使用原文件的原因。 */
|
|
55
|
+
reason: 'compressed' | 'below-target' | 'unsupported' | 'not-jpeg' | 'input-budget'
|
|
56
|
+
| 'busy' | 'invalid-image' | 'no-benefit' | 'timeout' | 'failed'
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let preparationInProgress = false
|
|
60
|
+
type PreparedFile = { filePath: string, bytes: number, lastModifiedTime?: number }
|
|
61
|
+
let lastManuallyPrepared: (PreparedFile & { expiresAt: number }) | undefined
|
|
62
|
+
|
|
63
|
+
function callNative<T>(invoke: (callbacks: NativeCallbacks<T>) => void): Promise<T> {
|
|
64
|
+
return new Promise((success, fail) => invoke({ success, fail }))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isJpeg(type: string | undefined): boolean {
|
|
68
|
+
return /^jpe?g$/i.test(type ?? '')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function canPrepareImageForUpload(filePath: unknown): filePath is string {
|
|
72
|
+
// 只检查宿主本地文件,避免 getImageInfo 下载远程路径。
|
|
73
|
+
if (typeof filePath !== 'string' || !/^ttfile:\/\/(?:temp|user)\//.test(filePath)
|
|
74
|
+
|| typeof tt === 'undefined') return false
|
|
75
|
+
try {
|
|
76
|
+
return isDouyinMiniProgram()
|
|
77
|
+
&& typeof tt.getImageInfo === 'function' && typeof tt.compressImage === 'function'
|
|
78
|
+
&& typeof tt.getFileSystemManager === 'function'
|
|
79
|
+
} catch {
|
|
80
|
+
return false
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @desc 显式预处理本地 JPEG,返回上传路径和诊断信息。
|
|
86
|
+
* @param {string} filePath 选图或拍摄获得的本地文件路径。
|
|
87
|
+
* @returns {Promise<PreparedImageForUpload>} 上传路径及诊断信息,降级时返回原路径。
|
|
88
|
+
* @remarks Network.uploadFile 会自动预处理符合条件的 JPEG。此方法兼容已有显式调用。压缩结果由宿主临时保存,能力缺失或超时时沿用原路径或已验证的候选。
|
|
89
|
+
* @example
|
|
90
|
+
* ```ts
|
|
91
|
+
* const prepared = await prepareImageForUpload(tempFilePath)
|
|
92
|
+
* console.log(prepared.bytes, prepared.reason)
|
|
93
|
+
* ```
|
|
94
|
+
* @ai-intent 兼容已有的本地 JPEG 显式预处理。
|
|
95
|
+
* @ai-when 维护已使用显式预处理或需要其诊断信息的调用方时。
|
|
96
|
+
* @ai-avoid 不要为使用新版 Network.uploadFile 的新业务再接一层手动预处理。
|
|
97
|
+
* @ai-example const prepared = await prepareImageForUpload(tempFilePath)
|
|
98
|
+
*/
|
|
99
|
+
export function prepareImageForUpload(filePath: string): Promise<PreparedImageForUpload> {
|
|
100
|
+
return prepareImage(filePath, false)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function prepareUploadFilePath(filePath: string): Promise<string> {
|
|
104
|
+
return (await prepareImage(filePath, true)).filePath
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function prepareImage(filePath: string, forUpload: boolean): Promise<PreparedImageForUpload> {
|
|
108
|
+
let originalBytes: number | undefined
|
|
109
|
+
let best: PreparedFile | undefined
|
|
110
|
+
const result = (reason: PreparedImageForUpload['reason']): PreparedImageForUpload =>
|
|
111
|
+
best ? { filePath: best.filePath, bytes: best.bytes, originalBytes, reason: 'compressed' }
|
|
112
|
+
: { filePath, originalBytes, bytes: originalBytes, reason }
|
|
113
|
+
|
|
114
|
+
if (!canPrepareImageForUpload(filePath) || !tt) return result('unsupported')
|
|
115
|
+
if (preparationInProgress) return result('busy')
|
|
116
|
+
|
|
117
|
+
const deadline = Date.now() + PREPARATION_BUDGET_MS
|
|
118
|
+
const expired = () => Date.now() >= deadline
|
|
119
|
+
const runtime = tt
|
|
120
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
121
|
+
preparationInProgress = true
|
|
122
|
+
|
|
123
|
+
const prepare = async () => {
|
|
124
|
+
try {
|
|
125
|
+
const fs = runtime.getFileSystemManager?.()
|
|
126
|
+
if (!fs?.stat || !runtime.getImageInfo || !runtime.compressImage) return result('unsupported')
|
|
127
|
+
const stat = fs.stat.bind(fs)
|
|
128
|
+
const getInfo = runtime.getImageInfo.bind(runtime)
|
|
129
|
+
const compress = runtime.compressImage.bind(runtime)
|
|
130
|
+
const readStat = async (path: string) => {
|
|
131
|
+
const { stats } = await callNative<{ stats?: NativeFileStat }>(callbacks => stat({ ...callbacks, path }))
|
|
132
|
+
const size = stats?.size
|
|
133
|
+
return size && Number.isSafeInteger(size) && size > 0
|
|
134
|
+
? { bytes: size, lastModifiedTime: stats?.lastModifiedTime }
|
|
135
|
+
: undefined
|
|
136
|
+
}
|
|
137
|
+
const readInfo = (src: string) =>
|
|
138
|
+
callNative<NativeImageInfo>(callbacks => getInfo({ ...callbacks, src }))
|
|
139
|
+
|
|
140
|
+
if (expired()) return result('timeout')
|
|
141
|
+
const original = await readStat(filePath)
|
|
142
|
+
originalBytes = original?.bytes
|
|
143
|
+
if (expired()) return result('timeout')
|
|
144
|
+
if (originalBytes === undefined) return result('invalid-image')
|
|
145
|
+
if (forUpload && lastManuallyPrepared?.filePath === filePath) {
|
|
146
|
+
const prepared = lastManuallyPrepared
|
|
147
|
+
lastManuallyPrepared = undefined
|
|
148
|
+
if (Date.now() < prepared.expiresAt && originalBytes === prepared.bytes
|
|
149
|
+
&& original?.lastModifiedTime === prepared.lastModifiedTime) return result('compressed')
|
|
150
|
+
}
|
|
151
|
+
if (originalBytes <= TARGET_BYTES) return result('below-target')
|
|
152
|
+
if (originalBytes > MAX_SOURCE_BYTES) return result('input-budget')
|
|
153
|
+
|
|
154
|
+
const source = await readInfo(filePath)
|
|
155
|
+
if (expired()) return result('timeout')
|
|
156
|
+
if (!isJpeg(source.type)) return result(source.type ? 'not-jpeg' : 'invalid-image')
|
|
157
|
+
const { width, height, orientation } = source
|
|
158
|
+
if (!width || !height || !Number.isSafeInteger(width) || !Number.isSafeInteger(height)
|
|
159
|
+
|| width < 0 || height < 0) return result('invalid-image')
|
|
160
|
+
const isLargeImage = originalBytes > LARGE_IMAGE_BYTES
|
|
161
|
+
const maxSourcePixels = isLargeImage ? MAX_LARGE_SOURCE_PIXELS : MAX_SOURCE_PIXELS
|
|
162
|
+
if (width * height > maxSourcePixels) return result('input-budget')
|
|
163
|
+
// 大图允许尝试常见的 EXIF 旋转,候选必须保留同样的宽高和方向才能上传。
|
|
164
|
+
const hasKnownRotation = orientation === 'left' || orientation === 'right' || orientation === 'down'
|
|
165
|
+
if (orientation !== 'up' && !(isLargeImage && hasKnownRotation)) return result('unsupported')
|
|
166
|
+
|
|
167
|
+
const qualities = isLargeImage ? [65, 50] : [85, 75]
|
|
168
|
+
let completedAttempt = false
|
|
169
|
+
for (const quality of qualities) {
|
|
170
|
+
if (expired()) return result('timeout')
|
|
171
|
+
// 宿主可能复用临时路径。下一次编码返回不同路径前,上一候选不能用于上传。
|
|
172
|
+
const previousBest = best
|
|
173
|
+
if (previousBest) completedAttempt = false
|
|
174
|
+
best = undefined
|
|
175
|
+
// 宿主可能覆盖此前显式预处理的临时文件,开始编码前使记录失效。
|
|
176
|
+
lastManuallyPrepared = undefined
|
|
177
|
+
try {
|
|
178
|
+
const { tempFilePath } = await callNative<{ tempFilePath?: string }>(callbacks =>
|
|
179
|
+
compress({ ...callbacks, src: filePath, quality }),
|
|
180
|
+
)
|
|
181
|
+
// 宿主临时目录只读。超时晚到的结果不会上传,文件由宿主回收。
|
|
182
|
+
if (expired()) return result('timeout')
|
|
183
|
+
if (!tempFilePath || tempFilePath === filePath || !/^ttfile:\/\/temp\//.test(tempFilePath)) continue
|
|
184
|
+
if (previousBest && tempFilePath !== previousBest.filePath) best = previousBest
|
|
185
|
+
const output = await readStat(tempFilePath)
|
|
186
|
+
if (expired()) return result('timeout')
|
|
187
|
+
if (!output) continue
|
|
188
|
+
const { bytes } = output
|
|
189
|
+
if (bytes > originalBytes * MAX_OUTPUT_RATIO || (best && bytes >= best.bytes)) {
|
|
190
|
+
completedAttempt = true
|
|
191
|
+
continue
|
|
192
|
+
}
|
|
193
|
+
const candidate = await readInfo(tempFilePath)
|
|
194
|
+
if (expired()) return result('timeout')
|
|
195
|
+
if (!isJpeg(candidate.type) || candidate.width !== width || candidate.height !== height
|
|
196
|
+
|| candidate.orientation !== orientation) continue
|
|
197
|
+
completedAttempt = true
|
|
198
|
+
best = { filePath: tempFilePath, ...output }
|
|
199
|
+
if (bytes <= TARGET_BYTES) break
|
|
200
|
+
} catch {
|
|
201
|
+
// 宿主单次处理失败时,仍可用剩余预算或已验证的候选。
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return result(expired() ? 'timeout' : completedAttempt ? 'no-benefit' : 'failed')
|
|
205
|
+
} catch {
|
|
206
|
+
return result(expired() ? 'timeout' : 'failed')
|
|
207
|
+
} finally {
|
|
208
|
+
preparationInProgress = false
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const timeout = new Promise<PreparedImageForUpload>(resolve => {
|
|
213
|
+
timer = setTimeout(() => resolve(result('timeout')), PREPARATION_BUDGET_MS)
|
|
214
|
+
})
|
|
215
|
+
// 等待预算只影响调用方。宿主仍在处理时继续占用唯一名额。
|
|
216
|
+
try {
|
|
217
|
+
const prepared = await Promise.race([prepare(), timeout])
|
|
218
|
+
if (!forUpload && best) {
|
|
219
|
+
lastManuallyPrepared = { ...best, expiresAt: Date.now() + PREPARED_PATH_TTL_MS }
|
|
220
|
+
}
|
|
221
|
+
return prepared
|
|
222
|
+
} finally {
|
|
223
|
+
clearTimeout(timer)
|
|
224
|
+
}
|
|
225
|
+
}
|
package/src/runtime/index.ts
CHANGED
|
@@ -2,3 +2,4 @@ export { createMicroappClient } from './default-client'
|
|
|
2
2
|
export { type CreateMicroappClientOptions } from './client'
|
|
3
3
|
export { clearDouyinLoginAuth } from './douyin-login-request'
|
|
4
4
|
export { syncIframeAuth } from './default-iframe-auth'
|
|
5
|
+
export { prepareImageForUpload, type PreparedImageForUpload } from './image-upload-preparation'
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
type UploadTask = ReturnType<typeof Taro.uploadFile>
|
|
2
|
+
type ProgressCallback = Parameters<UploadTask['onProgressUpdate']>[0]
|
|
3
|
+
type HeadersCallback = Parameters<UploadTask['onHeadersReceived']>[0]
|
|
4
|
+
|
|
5
|
+
export function startPreparedUploadTask(
|
|
6
|
+
option: Taro.uploadFile.Option,
|
|
7
|
+
preparedPath: Promise<string>,
|
|
8
|
+
start: (option: Taro.uploadFile.Option) => UploadTask,
|
|
9
|
+
): UploadTask {
|
|
10
|
+
let nativeTask: UploadTask | undefined
|
|
11
|
+
let abortBeforeStart: (() => void) | undefined
|
|
12
|
+
const progressListeners = new Set<ProgressCallback>()
|
|
13
|
+
const headersListeners = new Set<HeadersCallback>()
|
|
14
|
+
|
|
15
|
+
const onProgressUpdate = (callback: ProgressCallback) => {
|
|
16
|
+
if (nativeTask?.onProgressUpdate) nativeTask.onProgressUpdate(callback)
|
|
17
|
+
else if (nativeTask) nativeTask.progress?.(callback)
|
|
18
|
+
else if (abortBeforeStart) progressListeners.add(callback)
|
|
19
|
+
}
|
|
20
|
+
const onHeadersReceived = (callback: HeadersCallback) => {
|
|
21
|
+
if (nativeTask?.onHeadersReceived) nativeTask.onHeadersReceived(callback)
|
|
22
|
+
else if (nativeTask) nativeTask.headersReceive?.(callback)
|
|
23
|
+
else if (abortBeforeStart) headersListeners.add(callback)
|
|
24
|
+
}
|
|
25
|
+
const clearListeners = () => {
|
|
26
|
+
progressListeners.clear()
|
|
27
|
+
headersListeners.clear()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const promise = new Promise<Taro.uploadFile.SuccessCallbackResult>((resolve, reject) => {
|
|
31
|
+
abortBeforeStart = () => {
|
|
32
|
+
abortBeforeStart = undefined
|
|
33
|
+
clearListeners()
|
|
34
|
+
const error = { errMsg: 'uploadFile:fail upload file abort', errNo: 121905 }
|
|
35
|
+
reject(error)
|
|
36
|
+
try {
|
|
37
|
+
option.fail?.(error)
|
|
38
|
+
} finally {
|
|
39
|
+
option.complete?.(error)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
void preparedPath.catch(() => option.filePath).then(filePath => {
|
|
44
|
+
if (!abortBeforeStart) return
|
|
45
|
+
abortBeforeStart = undefined
|
|
46
|
+
try {
|
|
47
|
+
nativeTask = start({ ...option, filePath })
|
|
48
|
+
resolve(nativeTask)
|
|
49
|
+
progressListeners.forEach(onProgressUpdate)
|
|
50
|
+
headersListeners.forEach(onHeadersReceived)
|
|
51
|
+
} finally {
|
|
52
|
+
clearListeners()
|
|
53
|
+
}
|
|
54
|
+
}).catch(reject)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
const task: UploadTask = Object.assign(promise, {
|
|
58
|
+
abort(callback?: () => void) {
|
|
59
|
+
if (nativeTask || abortBeforeStart) callback?.()
|
|
60
|
+
if (nativeTask) nativeTask.abort?.()
|
|
61
|
+
else abortBeforeStart?.()
|
|
62
|
+
return task
|
|
63
|
+
},
|
|
64
|
+
onProgressUpdate,
|
|
65
|
+
offProgressUpdate(callback: ProgressCallback) {
|
|
66
|
+
if (callback) progressListeners.delete(callback)
|
|
67
|
+
else progressListeners.clear()
|
|
68
|
+
nativeTask?.offProgressUpdate?.(callback)
|
|
69
|
+
},
|
|
70
|
+
onHeadersReceived,
|
|
71
|
+
offHeadersReceived(callback: HeadersCallback) {
|
|
72
|
+
if (callback) headersListeners.delete(callback)
|
|
73
|
+
else headersListeners.clear()
|
|
74
|
+
nativeTask?.offHeadersReceived?.(callback)
|
|
75
|
+
},
|
|
76
|
+
progress(callback: ProgressCallback) {
|
|
77
|
+
onProgressUpdate(callback)
|
|
78
|
+
return task
|
|
79
|
+
},
|
|
80
|
+
headersReceive(callback: HeadersCallback) {
|
|
81
|
+
onHeadersReceived(callback)
|
|
82
|
+
return task
|
|
83
|
+
},
|
|
84
|
+
})
|
|
85
|
+
return task
|
|
86
|
+
}
|