@open-dy/vibecoding-sdk-taro 0.1.11 → 0.1.13

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 (47) hide show
  1. package/dist/adapters/interest-card/eslint/taro.mjs +1 -0
  2. package/dist/adapters/interest-card/runtime/microapp.d.ts +1 -1
  3. package/dist/adapters/interest-card/runtime/microapp.js +1 -1
  4. package/dist/adapters/interest-card/runtime/microapp.js.map +1 -1
  5. package/dist/eslint/card-profile.mjs +96 -0
  6. package/dist/eslint/eslint-plugin-post-card.mjs +82 -0
  7. package/dist/eslint/taro.mjs +10 -0
  8. package/dist/reference/llms.txt +3 -1
  9. package/dist/reference/sdk-api/runtime/CreateMicroappClientOptions.md +3 -1
  10. package/dist/reference/sdk-api/runtime/PreparedImageForUpload.md +52 -0
  11. package/dist/reference/sdk-api/runtime/createMicroappClient.md +4 -3
  12. package/dist/reference/sdk-api/runtime/index.md +3 -1
  13. package/dist/reference/sdk-api/runtime/prepareImageForUpload.md +69 -0
  14. package/dist/reference/sdk-api.json +144 -6
  15. package/dist/reference/sdk-api.md +2 -0
  16. package/dist/runtime/client.d.ts +3 -1
  17. package/dist/runtime/client.js +20 -5
  18. package/dist/runtime/client.js.map +1 -1
  19. package/dist/runtime/default-client.d.ts +2 -2
  20. package/dist/runtime/default-client.js +2 -2
  21. package/dist/runtime/douyin-container-request.d.ts +3 -2
  22. package/dist/runtime/douyin-container-request.js +24 -15
  23. package/dist/runtime/douyin-container-request.js.map +1 -1
  24. package/dist/runtime/globals.d.ts +2 -2
  25. package/dist/runtime/image-upload-preparation.d.ts +39 -0
  26. package/dist/runtime/image-upload-preparation.js +190 -0
  27. package/dist/runtime/image-upload-preparation.js.map +1 -0
  28. package/dist/runtime/index.d.ts +1 -0
  29. package/dist/runtime/index.js +1 -0
  30. package/dist/runtime/index.js.map +1 -1
  31. package/dist/runtime/prepared-upload-task.d.ts +3 -0
  32. package/dist/runtime/prepared-upload-task.js +91 -0
  33. package/dist/runtime/prepared-upload-task.js.map +1 -0
  34. package/package.json +1 -1
  35. package/release-provenance.json +4 -4
  36. package/src/adapters/interest-card/eslint/taro.mjs +1 -0
  37. package/src/adapters/interest-card/runtime/microapp.ts +2 -0
  38. package/src/eslint/card-profile.mjs +2 -0
  39. package/src/eslint/eslint-plugin-post-card.mjs +82 -0
  40. package/src/eslint/taro.mjs +10 -0
  41. package/src/runtime/client.ts +25 -5
  42. package/src/runtime/default-client.ts +2 -2
  43. package/src/runtime/douyin-container-request.ts +26 -20
  44. package/src/runtime/globals.d.ts +2 -2
  45. package/src/runtime/image-upload-preparation.ts +225 -0
  46. package/src/runtime/index.ts +1 -0
  47. package/src/runtime/prepared-upload-task.ts +86 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prepared-upload-task.js","sourceRoot":"","sources":["../../src/runtime/prepared-upload-task.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-dy/vibecoding-sdk-taro",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "Runtime, UI, build and ESLint capabilities for Douyin Taro templates.",
5
5
  "type": "module",
6
6
  "files": [
@@ -2,12 +2,12 @@
2
2
  "schema": 1,
3
3
  "producer": "vibecoding-sdk-taro",
4
4
  "packageName": "@open-dy/vibecoding-sdk-taro",
5
- "version": "0.1.11",
5
+ "version": "0.1.13",
6
6
  "source": {
7
- "digest": "sha256-65487d3df50200d0d27332e26f04e9d3520d3313d31eae611483a3078b0f658c",
8
- "files": 159
7
+ "digest": "sha256-d7a5401f44161aed968a09decead0697bc0ef1f543626208b3ed2eb4e072df74",
8
+ "files": 164
9
9
  },
10
- "payload": "sha256-ff279eab5b08f88997926c9180ecd6a50e8d0f724cf81a76f314f477b116cc40",
10
+ "payload": "sha256-01638ed4e3da5db1f656bcfca5cddb7a1d23fc14176d366adf563406dfde8d48",
11
11
  "toolchain": {
12
12
  "node": "v22.22.3",
13
13
  "runtime": {
@@ -1,3 +1,4 @@
1
+ export { CARD_TYPE_RULES_VERSION } from '../../../eslint/taro.mjs'
1
2
  import { createMicroappTaroEslintConfig } from '../../../eslint/taro.mjs'
2
3
 
3
4
  /**
@@ -7,5 +7,7 @@
7
7
  export {
8
8
  clearDouyinLoginAuth,
9
9
  createMicroappClient,
10
+ prepareImageForUpload,
10
11
  type CreateMicroappClientOptions,
12
+ type PreparedImageForUpload,
11
13
  } from '../../../runtime/index.js'
@@ -0,0 +1,2 @@
1
+ // SDK builds embed this shared, dependency-free module beside the ESLint entry.
2
+ export { createPostCardMatcher } from '../../../vibecoding-cli/src/config/card-profile.mjs'
@@ -0,0 +1,82 @@
1
+ const TARO_SOURCES = new Set(['@tarojs/taro'])
2
+ const TAB_APIS = new Set([
3
+ 'switchTab', 'setTabBarItem', 'setTabBarStyle', 'showTabBar', 'hideTabBar',
4
+ 'setTabBarBadge', 'removeTabBarBadge', 'showTabBarRedDot', 'hideTabBarRedDot',
5
+ 'getTabBar', 'onTabItemTap', 'useTabItemTap',
6
+ ])
7
+
8
+ function key(node) {
9
+ return node?.type === 'Identifier' ? node.name
10
+ : node?.type === 'Literal' && typeof node.value === 'string' ? node.value : null
11
+ }
12
+
13
+ function platformObject(node, context, seen = new Set()) {
14
+ if (!node || seen.has(node)) return false
15
+ seen.add(node)
16
+ if (['TSAsExpression', 'TSNonNullExpression', 'ChainExpression'].includes(node.type)) {
17
+ return platformObject(node.expression, context, seen)
18
+ }
19
+ if (node.type === 'CallExpression') {
20
+ return node.callee.type === 'Identifier' && node.callee.name === 'require'
21
+ && TARO_SOURCES.has(node.arguments[0]?.value)
22
+ }
23
+ if (node.type === 'MemberExpression') {
24
+ return ['globalThis', 'window'].includes(node.object?.name)
25
+ && ['tt', 'Taro'].includes(key(node.property))
26
+ }
27
+ if (node.type !== 'Identifier') return false
28
+ let scope = context.sourceCode.getScope(node)
29
+ let variable
30
+ while (scope && !variable) { variable = scope.set.get(node.name); scope = scope.upper }
31
+ if (!variable?.defs.length) return ['tt', 'Taro'].includes(node.name)
32
+ return variable.defs.some(def => {
33
+ if (def.type === 'ImportBinding') {
34
+ return TARO_SOURCES.has(def.parent?.source?.value)
35
+ && ['ImportDefaultSpecifier', 'ImportNamespaceSpecifier'].includes(def.node.type)
36
+ }
37
+ return def.type === 'Variable' && platformObject(def.node.init, context, seen)
38
+ })
39
+ }
40
+
41
+ export default {
42
+ rules: {
43
+ 'no-platform-tabbar': {
44
+ meta: { type: 'problem', schema: [], messages: {
45
+ config: '作品卡不支持平台 tabBar(含 custom 模式)。请用普通组件绘制导航并实现页面或状态切换。',
46
+ api: '作品卡不支持 {{name}}。请使用普通页面路由或组件状态切换,不调用平台 TabBar。',
47
+ } },
48
+ create(context) {
49
+ const filename = context.filename.replaceAll('\\', '/')
50
+ const isAppConfig = /\/app\.config\.[cm]?[jt]s$/.test(filename)
51
+ const report = (node, name) => {
52
+ if (TAB_APIS.has(name)) context.report({ node, messageId: 'api', data: { name } })
53
+ }
54
+ return {
55
+ Property(node) {
56
+ if (isAppConfig && key(node.key) === 'tabBar') context.report({ node, messageId: 'config' })
57
+ if (node.parent.type === 'ObjectPattern' && platformObject(node.parent.parent?.init, context)) {
58
+ report(node, key(node.key))
59
+ }
60
+ },
61
+ ImportSpecifier(node) {
62
+ if (node.importKind !== 'type' && node.parent.importKind !== 'type'
63
+ && TARO_SOURCES.has(node.parent.source.value)) report(node, key(node.imported))
64
+ },
65
+ ExportSpecifier(node) {
66
+ if (TARO_SOURCES.has(node.parent.source?.value)) report(node, key(node.local))
67
+ },
68
+ MemberExpression(node) {
69
+ const name = key(node.property)
70
+ if (platformObject(node.object, context) || (name === 'getTabBar' && node.object.type === 'ThisExpression')) {
71
+ report(node, name)
72
+ }
73
+ },
74
+ JSXAttribute(node) {
75
+ const value = node.value?.type === 'JSXExpressionContainer' ? node.value.expression?.value : node.value?.value
76
+ if (node.name.name === 'openType' && value === 'switchTab') report(node, 'switchTab')
77
+ },
78
+ }
79
+ },
80
+ },
81
+ },
82
+ }
@@ -1,3 +1,6 @@
1
+ import postCard from './eslint-plugin-post-card.mjs';
2
+ import { createPostCardMatcher } from './card-profile.mjs';
3
+ export const CARD_TYPE_RULES_VERSION = 1;
1
4
  import eslint from '@eslint/js';
2
5
  import { fixupPluginRules } from '@eslint/compat';
3
6
  import tseslint from 'typescript-eslint';
@@ -381,6 +384,7 @@ const indexPageRestrictedSyntaxRules = [
381
384
  ];
382
385
 
383
386
  export function createMicroappTaroEslintConfig(options = {}) {
387
+ const matchesPostCard = createPostCardMatcher({ cardType: options.cardType, target: 'taro' });
384
388
  const lucideColorCheckPlugin = createLucideColorCheckPlugin(options.lucideImportSources)
385
389
  const pageRestrictedSyntaxRules = createPageRestrictedSyntaxRules(
386
390
  options.preferredUiImportBase,
@@ -581,6 +585,12 @@ export function createMicroappTaroEslintConfig(options = {}) {
581
585
  },
582
586
  );
583
587
 
588
+ defaults.push({
589
+ files: [['src/**/*.{js,jsx,ts,tsx}', matchesPostCard]],
590
+ settings: { cardType: 'post', cardTypeRulesVersion: CARD_TYPE_RULES_VERSION },
591
+ plugins: { postCard },
592
+ rules: { 'postCard/no-platform-tabbar': 'error' },
593
+ });
584
594
  const overrides = options.overrides
585
595
  if (!overrides) return defaults
586
596
  if (typeof overrides === 'function') return overrides(defaults) ?? defaults
@@ -2,6 +2,7 @@ import {
2
2
  canUseDouyinContainer,
3
3
  downloadByDouyinContainer,
4
4
  isDouyinMiniProgram,
5
+ isDouyinNativePreviewEnv,
5
6
  isDouyinPreviewEnv,
6
7
  isWebPreviewEnv,
7
8
  requestByDouyinContainer,
@@ -18,10 +19,12 @@ import {
18
19
  downloadByDouyinLogin,
19
20
  } from './douyin-login-request'
20
21
  import { mergeIframeIdentityHeaders } from '../internal/runtime/iframe-auth/index'
21
- import { appendGetQuery, createUrl } from './network-url'
22
+ import { canPrepareImageForUpload, prepareUploadFilePath } from './image-upload-preparation'
23
+ import { appendGetQuery, createUrl, getManagedUploadError } from './network-url'
24
+ import { startPreparedUploadTask } from './prepared-upload-task'
22
25
 
23
26
  /**
24
- * @desc 配置普通小程序 Network 的平台渠道。
27
+ * @desc 配置普通小程序 Network 的平台渠道和真机预览上传环境。
25
28
  * @example
26
29
  * ```ts
27
30
  * const options = { platformChannel: 'ext' as const }
@@ -34,6 +37,8 @@ import { appendGetQuery, createUrl } from './network-url'
34
37
  export interface CreateMicroappClientOptions {
35
38
  /** @desc 平台渠道:空字符串为内场,ext 为外场;默认空字符串。 */
36
39
  platformChannel?: '' | 'ext'
40
+ /** @desc 设置为 development 时,真机扫码预览的托管上传使用抖音云开发环境。 */
41
+ previewUploadEnvType?: 'development'
37
42
  }
38
43
 
39
44
  /**
@@ -115,7 +120,7 @@ export function createMicroappClient(
115
120
  return promise
116
121
  }) as typeof Taro.request
117
122
 
118
- const uploadFile = ((option: Taro.uploadFile.Option) => {
123
+ const startUpload = (option: Taro.uploadFile.Option) => {
119
124
  if (!option.url) {
120
125
  option.url = '';
121
126
  }
@@ -126,16 +131,31 @@ export function createMicroappClient(
126
131
  if (!option.name) {
127
132
  option.name = 'file';
128
133
  }
134
+ if (options.previewUploadEnvType === 'development' && isDouyinNativePreviewEnv()) {
135
+ return uploadByDouyinContainer(option, options.previewUploadEnvType)
136
+ }
129
137
  if (
130
138
  isDouyinMiniProgram() &&
131
139
  !isWebPreviewEnv() &&
132
140
  !isDouyinPreviewEnv() &&
133
- canUseDouyinContainer() &&
134
- !/^http(s)?:\/\//.test(option.url)
141
+ canUseDouyinContainer()
135
142
  ) {
136
143
  return uploadByDouyinContainer(option)
137
144
  }
138
145
  return uploadByDouyinLogin(option)
146
+ }
147
+
148
+ const uploadFile = ((option: Taro.uploadFile.Option) => {
149
+ if (getManagedUploadError(option) || !canPrepareImageForUpload(option.filePath)) {
150
+ return startUpload(option)
151
+ }
152
+
153
+ const uploadOption = { ...option }
154
+ const preparedPath = prepareUploadFilePath(uploadOption.filePath)
155
+ if (/^https?:\/\//.test(uploadOption.url ?? '')) {
156
+ return startPreparedUploadTask(uploadOption, preparedPath, prepared => taro.uploadFile(prepared))
157
+ }
158
+ return preparedPath.then(filePath => startUpload({ ...uploadOption, filePath }))
139
159
  }) as (
140
160
  option: Omit<Taro.uploadFile.Option, 'url' | 'name'> & { url?: string, name?: string },
141
161
  ) => ReturnType<typeof Taro.uploadFile>
@@ -6,9 +6,9 @@ import {
6
6
 
7
7
  /**
8
8
  * @desc 创建普通小程序的网络入口;SDK 自动绑定项目 Taro 运行时。
9
- * @params {CreateMicroappClientOptions} [options] 平台渠道配置;默认使用内场渠道。
9
+ * @params {CreateMicroappClientOptions} [options] 平台渠道和真机预览上传环境配置。默认使用内场渠道。
10
10
  * @return {ReturnType<typeof createMicroappClient>} 仅提供普通小程序的 request、uploadFile、downloadFile 和 streamSse。
11
- * @remarks 业务优先从模板的 @/network 导入 Network;HTTP(S) 绝对地址直接交给 Taro。宿主需要封闭域功能时通过 MiniappOpenCard 承载,请求与数据处理仅在卡内完成。streamSse 返回的是流句柄,Promise resolve 不代表生成完成。正常关闭仅调用一次 onComplete;流错误仅调用一次 onError(Error),随后释放任务;close() 主动取消仅调用一次可选的 onCancel,不调用 onComplete/onError。业务须在对应终态回调中清理 loading,不能把 onComplete 当通用 finally;终态后的消息与重复事件会被忽略。创建任务前的失败仍通过 Promise rejection 交付。
11
+ * @remarks 业务优先从模板的 @/network 导入 Network。uploadFile 在支持原生图片处理的抖音小程序中自动尝试压缩符合条件的本地 JPEG,各上传路由均适用。业务直接传入原始 filePath。HTTP(S) 绝对地址仍由 Taro 传输。宿主需要封闭域功能时通过 MiniappOpenCard 承载,请求与数据处理仅在卡内完成。streamSse 返回的是流句柄,Promise resolve 不代表生成完成。正常关闭仅调用一次 onComplete;流错误仅调用一次 onError(Error),随后释放任务;close() 主动取消仅调用一次可选的 onCancel,不调用 onComplete/onError。业务须在对应终态回调中清理 loading,不能把 onComplete 当通用 finally;终态后的消息与重复事件会被忽略。创建任务前的失败仍通过 Promise rejection 交付。
12
12
  * @example
13
13
  * ```ts
14
14
  * const Network = createMicroappClient({ platformChannel: 'ext' })
@@ -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'