@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.
Files changed (39) hide show
  1. package/dist/adapters/interest-card/runtime/microapp.d.ts +1 -1
  2. package/dist/adapters/interest-card/runtime/microapp.js +1 -1
  3. package/dist/adapters/interest-card/runtime/microapp.js.map +1 -1
  4. package/dist/reference/llms.txt +3 -1
  5. package/dist/reference/sdk-api/runtime/CreateMicroappClientOptions.md +3 -1
  6. package/dist/reference/sdk-api/runtime/PreparedImageForUpload.md +52 -0
  7. package/dist/reference/sdk-api/runtime/createMicroappClient.md +4 -3
  8. package/dist/reference/sdk-api/runtime/index.md +3 -1
  9. package/dist/reference/sdk-api/runtime/prepareImageForUpload.md +69 -0
  10. package/dist/reference/sdk-api.json +144 -6
  11. package/dist/reference/sdk-api.md +2 -0
  12. package/dist/runtime/client.d.ts +3 -1
  13. package/dist/runtime/client.js +20 -5
  14. package/dist/runtime/client.js.map +1 -1
  15. package/dist/runtime/default-client.d.ts +2 -2
  16. package/dist/runtime/default-client.js +2 -2
  17. package/dist/runtime/douyin-container-request.d.ts +3 -2
  18. package/dist/runtime/douyin-container-request.js +24 -15
  19. package/dist/runtime/douyin-container-request.js.map +1 -1
  20. package/dist/runtime/globals.d.ts +2 -2
  21. package/dist/runtime/image-upload-preparation.d.ts +39 -0
  22. package/dist/runtime/image-upload-preparation.js +190 -0
  23. package/dist/runtime/image-upload-preparation.js.map +1 -0
  24. package/dist/runtime/index.d.ts +1 -0
  25. package/dist/runtime/index.js +1 -0
  26. package/dist/runtime/index.js.map +1 -1
  27. package/dist/runtime/prepared-upload-task.d.ts +3 -0
  28. package/dist/runtime/prepared-upload-task.js +91 -0
  29. package/dist/runtime/prepared-upload-task.js.map +1 -0
  30. package/package.json +1 -1
  31. package/release-provenance.json +4 -4
  32. package/src/adapters/interest-card/runtime/microapp.ts +2 -0
  33. package/src/runtime/client.ts +25 -5
  34. package/src/runtime/default-client.ts +2 -2
  35. package/src/runtime/douyin-container-request.ts +26 -20
  36. package/src/runtime/globals.d.ts +2 -2
  37. package/src/runtime/image-upload-preparation.ts +225 -0
  38. package/src/runtime/index.ts +1 -0
  39. 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: DouyinCloudInstance | null | undefined;
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
- export const getDouyinCloud = (): DouyinCloudInstance | null => {
96
- if (douyinCloud !== undefined) {
97
- return douyinCloud;
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 cloud = getDouyinCloud();
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 ${getDouyinCloudEnvIDName()} is required for createCloud`,
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) as ReturnType<typeof Taro.request>;
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 as ReturnType<typeof Taro.request>;
316
+ return requestPromise;
311
317
  };
312
318
 
313
319
  export const downloadByDouyinContainer = (option: Taro.downloadFile.Option) => {
@@ -42,7 +42,7 @@ type DouyinCallContainerFailResult = {
42
42
  errMsg?: string;
43
43
  };
44
44
 
45
- type DouyinCloudInstance = {
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
- }) => DouyinCloudInstance;
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
+ }
@@ -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
+ }