@jackwener/opencli 1.7.22 → 1.8.1

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 (346) hide show
  1. package/README.md +35 -194
  2. package/README.zh-CN.md +42 -260
  3. package/cli-manifest.json +8160 -4392
  4. package/clis/12306/me.js +73 -0
  5. package/clis/12306/orders.js +96 -0
  6. package/clis/12306/passengers.js +90 -0
  7. package/clis/12306/price.js +166 -0
  8. package/clis/12306/stations.js +66 -0
  9. package/clis/12306/train.js +91 -0
  10. package/clis/12306/trains.js +119 -0
  11. package/clis/12306/utils.js +272 -0
  12. package/clis/12306/utils.test.js +331 -0
  13. package/clis/36kr/article.js +6 -3
  14. package/clis/36kr/article.test.js +46 -0
  15. package/clis/_atlassian/shared.js +577 -0
  16. package/clis/_atlassian/shared.test.js +170 -0
  17. package/clis/apple-podcasts/commands.test.js +20 -0
  18. package/clis/apple-podcasts/search.js +2 -2
  19. package/clis/barchart/greeks.js +144 -56
  20. package/clis/barchart/greeks.test.js +138 -0
  21. package/clis/bilibili/comment.js +125 -0
  22. package/clis/bilibili/comment.test.js +153 -0
  23. package/clis/bilibili/comments.js +116 -21
  24. package/clis/bilibili/comments.test.js +77 -18
  25. package/clis/bilibili/subtitle.js +76 -31
  26. package/clis/bilibili/subtitle.test.js +156 -9
  27. package/clis/bilibili/summary.js +167 -0
  28. package/clis/bilibili/summary.test.js +210 -0
  29. package/clis/bilibili/utils.js +63 -5
  30. package/clis/bilibili/utils.test.js +45 -1
  31. package/clis/booking/booking.test.js +356 -0
  32. package/clis/booking/search.js +351 -0
  33. package/clis/chatgpt/envelope.test.js +108 -0
  34. package/clis/chatgpt/image.js +2 -2
  35. package/clis/chatgpt/image.test.js +6 -0
  36. package/clis/chatgpt/utils.js +148 -41
  37. package/clis/chatgpt/utils.test.js +92 -2
  38. package/clis/chess/analyze.js +35 -0
  39. package/clis/chess/analyze.test.js +79 -0
  40. package/clis/chess/game.js +114 -0
  41. package/clis/chess/game.test.js +178 -0
  42. package/clis/chess/games.js +67 -0
  43. package/clis/chess/games.test.js +164 -0
  44. package/clis/chess/stats.js +32 -0
  45. package/clis/chess/stats.test.js +79 -0
  46. package/clis/chess/utils.js +170 -0
  47. package/clis/chess/utils.test.js +230 -0
  48. package/clis/confluence/commands.test.js +195 -0
  49. package/clis/confluence/create.js +39 -0
  50. package/clis/confluence/page.js +23 -0
  51. package/clis/confluence/search.js +34 -0
  52. package/clis/confluence/shared.js +173 -0
  53. package/clis/confluence/update.js +38 -0
  54. package/clis/douyin/_shared/browser-fetch.js +44 -20
  55. package/clis/douyin/_shared/browser-fetch.test.js +22 -1
  56. package/clis/douyin/_shared/evaluate-result.js +16 -0
  57. package/clis/douyin/_shared/tos-upload.js +105 -69
  58. package/clis/douyin/_shared/vod-upload.js +212 -0
  59. package/clis/douyin/_shared/vod-upload.test.js +38 -0
  60. package/clis/douyin/delete.js +137 -4
  61. package/clis/douyin/delete.test.js +90 -1
  62. package/clis/douyin/hashtag.js +84 -23
  63. package/clis/douyin/hashtag.test.js +113 -0
  64. package/clis/douyin/publish-upload-id.test.js +170 -0
  65. package/clis/douyin/publish.js +88 -42
  66. package/clis/douyin/user-videos.js +9 -2
  67. package/clis/douyin/user-videos.test.js +43 -0
  68. package/clis/flomo/memos.js +228 -0
  69. package/clis/flomo/memos.test.js +144 -0
  70. package/clis/geogebra/add-circle.js +46 -0
  71. package/clis/geogebra/add-line.js +35 -0
  72. package/clis/geogebra/add-point.js +27 -0
  73. package/clis/geogebra/add-polygon.js +25 -0
  74. package/clis/geogebra/eval.js +35 -0
  75. package/clis/geogebra/geogebra.test.js +175 -0
  76. package/clis/geogebra/hexagon.js +62 -0
  77. package/clis/geogebra/info.js +72 -0
  78. package/clis/geogebra/list.js +35 -0
  79. package/clis/geogebra/triangle.js +60 -0
  80. package/clis/geogebra/utils.js +271 -0
  81. package/clis/gitee/search.js +2 -2
  82. package/clis/gitee/search.test.js +65 -0
  83. package/clis/jike/post.js +27 -17
  84. package/clis/jike/read.test.js +86 -0
  85. package/clis/jike/topic.js +32 -19
  86. package/clis/jike/user.js +33 -20
  87. package/clis/jira/attachments.js +28 -0
  88. package/clis/jira/commands.test.js +287 -0
  89. package/clis/jira/comments.js +28 -0
  90. package/clis/jira/issue.js +28 -0
  91. package/clis/jira/links.js +28 -0
  92. package/clis/jira/search.js +47 -0
  93. package/clis/jira/shared.js +256 -0
  94. package/clis/lesswrong/comments.js +1 -1
  95. package/clis/lesswrong/curated.js +1 -1
  96. package/clis/lesswrong/frontpage.js +1 -1
  97. package/clis/lesswrong/frontpage.test.js +37 -0
  98. package/clis/lesswrong/new.js +1 -1
  99. package/clis/lesswrong/read.js +1 -1
  100. package/clis/lesswrong/sequences.js +1 -1
  101. package/clis/lesswrong/shortform.js +1 -1
  102. package/clis/lesswrong/tag.js +1 -1
  103. package/clis/lesswrong/top-month.js +1 -1
  104. package/clis/lesswrong/top-week.js +1 -1
  105. package/clis/lesswrong/top-year.js +1 -1
  106. package/clis/lesswrong/top.js +1 -1
  107. package/clis/linkedin/connect.js +401 -0
  108. package/clis/linkedin/connect.test.js +213 -0
  109. package/clis/linkedin/inbox.js +234 -0
  110. package/clis/linkedin/inbox.test.js +152 -0
  111. package/clis/linkedin/job-detail.js +167 -0
  112. package/clis/linkedin/job-detail.test.js +38 -0
  113. package/clis/linkedin/jobs-preferences.js +113 -0
  114. package/clis/linkedin/jobs-preferences.test.js +43 -0
  115. package/clis/linkedin/people-search.js +262 -0
  116. package/clis/linkedin/people-search.test.js +216 -0
  117. package/clis/linkedin/post-analytics.js +74 -0
  118. package/clis/linkedin/post-analytics.test.js +40 -0
  119. package/clis/linkedin/posts-core.js +241 -0
  120. package/clis/linkedin/posts.js +22 -0
  121. package/clis/linkedin/posts.test.js +40 -0
  122. package/clis/linkedin/profile-analytics.js +104 -0
  123. package/clis/linkedin/profile-analytics.test.js +67 -0
  124. package/clis/linkedin/profile-experience.js +671 -0
  125. package/clis/linkedin/profile-experience.test.js +152 -0
  126. package/clis/linkedin/profile-projects.js +311 -0
  127. package/clis/linkedin/profile-projects.test.js +111 -0
  128. package/clis/linkedin/profile-read.js +148 -0
  129. package/clis/linkedin/profile-read.test.js +77 -0
  130. package/clis/linkedin/safe-send.js +357 -0
  131. package/clis/linkedin/safe-send.test.js +204 -0
  132. package/clis/linkedin/salesnav-inbox.js +210 -0
  133. package/clis/linkedin/salesnav-inbox.test.js +113 -0
  134. package/clis/linkedin/salesnav-message.js +360 -0
  135. package/clis/linkedin/salesnav-message.test.js +172 -0
  136. package/clis/linkedin/salesnav-search.js +186 -0
  137. package/clis/linkedin/salesnav-search.test.js +76 -0
  138. package/clis/linkedin/salesnav-thread.js +212 -0
  139. package/clis/linkedin/salesnav-thread.test.js +79 -0
  140. package/clis/linkedin/sent-invitations.js +92 -0
  141. package/clis/linkedin/sent-invitations.test.js +62 -0
  142. package/clis/linkedin/services-read.js +213 -0
  143. package/clis/linkedin/services-read.test.js +105 -0
  144. package/clis/linkedin/shared.js +124 -0
  145. package/clis/linkedin/thread-snapshot.js +214 -0
  146. package/clis/linkedin/thread-snapshot.test.js +89 -0
  147. package/clis/linkedin/timeline.js +14 -7
  148. package/clis/linkedin-learning/course.js +138 -0
  149. package/clis/linkedin-learning/course.test.js +114 -0
  150. package/clis/linkedin-learning/search.js +155 -0
  151. package/clis/linkedin-learning/search.test.js +144 -0
  152. package/clis/linkedin-learning/trending.js +133 -0
  153. package/clis/linkedin-learning/trending.test.js +123 -0
  154. package/clis/notebooklm/add-source.js +269 -0
  155. package/clis/notebooklm/add-source.test.js +97 -0
  156. package/clis/notebooklm/create.js +76 -0
  157. package/clis/notebooklm/create.test.js +58 -0
  158. package/clis/notebooklm/generate-audio.js +91 -0
  159. package/clis/notebooklm/generate-audio.test.js +63 -0
  160. package/clis/notebooklm/generate-slides.js +106 -0
  161. package/clis/notebooklm/generate-slides.test.js +75 -0
  162. package/clis/notebooklm/open.test.js +10 -10
  163. package/clis/notebooklm/rpc.js +20 -6
  164. package/clis/notebooklm/rpc.test.js +27 -1
  165. package/clis/notebooklm/utils.js +100 -24
  166. package/clis/notebooklm/utils.test.js +60 -1
  167. package/clis/notebooklm/write-note.js +103 -0
  168. package/clis/notebooklm/write-note.test.js +70 -0
  169. package/clis/pixiv/detail.js +41 -34
  170. package/clis/pixiv/detail.test.js +93 -0
  171. package/clis/pixiv/user.js +36 -31
  172. package/clis/pixiv/user.test.js +100 -0
  173. package/clis/pixiv/utils.js +56 -7
  174. package/clis/powerchina/search.js +3 -3
  175. package/clis/powerchina/search.test.js +27 -1
  176. package/clis/reddit/extract-media.test.js +149 -0
  177. package/clis/reddit/frontpage.js +47 -9
  178. package/clis/reddit/frontpage.test.js +34 -0
  179. package/clis/reddit/home.js +31 -1
  180. package/clis/reddit/home.test.js +46 -3
  181. package/clis/reddit/hot.js +32 -1
  182. package/clis/reddit/hot.test.js +15 -1
  183. package/clis/reddit/popular.js +39 -1
  184. package/clis/reddit/popular.test.js +26 -0
  185. package/clis/reddit/saved.js +1 -1
  186. package/clis/reddit/search.js +38 -1
  187. package/clis/reddit/search.test.js +26 -0
  188. package/clis/reddit/subreddit.js +52 -7
  189. package/clis/reddit/subreddit.test.js +31 -0
  190. package/clis/reddit/subscribed.js +165 -0
  191. package/clis/reddit/subscribed.test.js +168 -0
  192. package/clis/reddit/upvoted.js +1 -1
  193. package/clis/suno/commands.test.js +188 -0
  194. package/clis/suno/download.js +140 -0
  195. package/clis/suno/download.test.js +151 -0
  196. package/clis/suno/generate.js +231 -0
  197. package/clis/suno/generate.test.js +252 -0
  198. package/clis/suno/list.js +79 -0
  199. package/clis/suno/status.js +63 -0
  200. package/clis/suno/utils.js +549 -0
  201. package/clis/suno/utils.test.js +329 -0
  202. package/clis/twitter/device-follow.js +193 -0
  203. package/clis/twitter/device-follow.test.js +287 -0
  204. package/clis/twitter/download.js +443 -73
  205. package/clis/twitter/download.test.js +457 -0
  206. package/clis/twitter/followers.js +6 -2
  207. package/clis/twitter/followers.test.js +19 -1
  208. package/clis/twitter/following.js +14 -5
  209. package/clis/twitter/following.test.js +29 -0
  210. package/clis/twitter/likes.js +12 -4
  211. package/clis/twitter/likes.test.js +26 -1
  212. package/clis/twitter/list-add.js +1 -1
  213. package/clis/twitter/list-create.js +155 -0
  214. package/clis/twitter/list-create.test.js +169 -0
  215. package/clis/twitter/list-remove.js +13 -6
  216. package/clis/twitter/list-remove.test.js +74 -0
  217. package/clis/twitter/list-tweets.js +6 -2
  218. package/clis/twitter/list-tweets.test.js +41 -1
  219. package/clis/twitter/lists.js +31 -4
  220. package/clis/twitter/lists.test.js +152 -16
  221. package/clis/twitter/notifications.js +4 -4
  222. package/clis/twitter/post.js +62 -4
  223. package/clis/twitter/post.test.js +35 -3
  224. package/clis/twitter/profile.js +81 -28
  225. package/clis/twitter/profile.test.js +113 -2
  226. package/clis/twitter/quote.js +9 -4
  227. package/clis/twitter/reply.js +13 -10
  228. package/clis/twitter/reply.test.js +41 -0
  229. package/clis/twitter/search.js +7 -3
  230. package/clis/twitter/search.test.js +41 -0
  231. package/clis/twitter/shared.js +155 -0
  232. package/clis/twitter/shared.test.js +465 -1
  233. package/clis/twitter/thread.js +10 -2
  234. package/clis/twitter/thread.test.js +58 -0
  235. package/clis/twitter/timeline.js +6 -2
  236. package/clis/twitter/timeline.test.js +2 -0
  237. package/clis/twitter/tweets.js +3 -2
  238. package/clis/twitter/tweets.test.js +1 -1
  239. package/clis/twitter/utils.js +53 -16
  240. package/clis/upwork/detail.js +132 -0
  241. package/clis/upwork/feed.js +109 -0
  242. package/clis/upwork/search.js +115 -0
  243. package/clis/upwork/upwork.test.js +566 -0
  244. package/clis/upwork/utils.js +323 -0
  245. package/clis/weibo/delete.js +172 -0
  246. package/clis/weibo/delete.test.js +94 -0
  247. package/clis/weibo/publish.js +37 -14
  248. package/clis/weibo/publish.test.js +14 -5
  249. package/clis/weibo/user-posts.js +234 -0
  250. package/clis/weibo/user-posts.test.js +92 -0
  251. package/clis/weread/book-search.js +438 -0
  252. package/clis/weread/book-search.test.js +242 -0
  253. package/clis/weread/search-regression.test.js +98 -11
  254. package/clis/weread/search.js +32 -9
  255. package/clis/weread-official/book.js +135 -0
  256. package/clis/weread-official/commands.test.js +385 -0
  257. package/clis/weread-official/discover.js +107 -0
  258. package/clis/weread-official/list-apis.js +95 -0
  259. package/clis/weread-official/notes.js +171 -0
  260. package/clis/weread-official/readdata.js +158 -0
  261. package/clis/weread-official/review.js +93 -0
  262. package/clis/weread-official/search.js +106 -0
  263. package/clis/weread-official/shelf.js +97 -0
  264. package/clis/weread-official/utils.js +293 -0
  265. package/clis/weread-official/utils.test.js +242 -0
  266. package/clis/wikipedia/trending.js +7 -3
  267. package/clis/wikipedia/trending.test.js +57 -0
  268. package/clis/xianyu/chat.js +24 -109
  269. package/clis/xianyu/chat.test.js +5 -0
  270. package/clis/xianyu/im.js +322 -0
  271. package/clis/xianyu/im.test.js +253 -0
  272. package/clis/xianyu/inbox.js +96 -0
  273. package/clis/xianyu/messages.js +91 -0
  274. package/clis/xianyu/reply.js +82 -0
  275. package/clis/xiaohongshu/creator-note-detail.js +166 -28
  276. package/clis/xiaohongshu/creator-note-detail.test.js +196 -36
  277. package/clis/xiaohongshu/creator-notes-summary.js +2 -1
  278. package/clis/xiaohongshu/creator-notes-summary.test.js +7 -0
  279. package/clis/xiaohongshu/creator-notes.js +252 -2
  280. package/clis/xiaohongshu/creator-notes.test.js +90 -1
  281. package/clis/xiaohongshu/creator-stats.js +2 -1
  282. package/clis/xiaohongshu/creator-stats.test.js +24 -0
  283. package/clis/xiaohongshu/delete-note.js +260 -0
  284. package/clis/xiaohongshu/delete-note.test.js +172 -0
  285. package/clis/xiaohongshu/download.js +97 -39
  286. package/clis/xiaohongshu/download.test.js +201 -0
  287. package/clis/xiaohongshu/publish.js +48 -8
  288. package/clis/xiaohongshu/publish.test.js +65 -10
  289. package/clis/xiaohongshu/user-helpers.test.js +41 -0
  290. package/clis/xiaohongshu/user.js +27 -4
  291. package/clis/xiaoyuzhou/download.js +1 -1
  292. package/clis/xiaoyuzhou/transcript.js +1 -1
  293. package/clis/youdao/note.js +258 -0
  294. package/clis/youdao/note.test.js +99 -0
  295. package/clis/youtube/transcript.js +397 -24
  296. package/clis/youtube/transcript.test.js +196 -6
  297. package/clis/zhihu/answer-comments.js +280 -0
  298. package/clis/zhihu/answer-comments.test.js +287 -0
  299. package/clis/zhihu/answer-detail.js +2 -19
  300. package/clis/zhihu/answer-detail.test.js +8 -0
  301. package/clis/zhihu/collection.js +17 -16
  302. package/clis/zhihu/collection.test.js +50 -3
  303. package/clis/zhihu/download.js +1 -1
  304. package/clis/zhihu/question.js +42 -17
  305. package/clis/zhihu/question.test.js +113 -11
  306. package/clis/zhihu/search.js +195 -43
  307. package/clis/zhihu/search.test.js +198 -0
  308. package/clis/zhihu/text.js +29 -0
  309. package/clis/zhihu/text.test.js +24 -0
  310. package/dist/src/browser/errors.js +4 -2
  311. package/dist/src/browser/errors.test.js +6 -0
  312. package/dist/src/browser/network-cache.js +13 -1
  313. package/dist/src/browser/network-cache.test.js +17 -0
  314. package/dist/src/browser/page.js +30 -4
  315. package/dist/src/browser/page.test.js +42 -0
  316. package/dist/src/browser/utils.d.ts +1 -1
  317. package/dist/src/cli-argv-preprocess.d.ts +26 -0
  318. package/dist/src/cli-argv-preprocess.js +138 -0
  319. package/dist/src/cli-argv-preprocess.test.js +79 -0
  320. package/dist/src/convention-audit.js +15 -8
  321. package/dist/src/convention-audit.test.js +21 -0
  322. package/dist/src/download/index.js +13 -1
  323. package/dist/src/download/index.test.js +23 -1
  324. package/dist/src/download/media-download.js +15 -2
  325. package/dist/src/download/media-download.test.d.ts +1 -0
  326. package/dist/src/download/media-download.test.js +112 -0
  327. package/dist/src/download/progress.js +2 -2
  328. package/dist/src/download/progress.test.js +12 -1
  329. package/dist/src/electron-apps.js +1 -1
  330. package/dist/src/electron-apps.test.js +7 -2
  331. package/dist/src/errors.d.ts +17 -0
  332. package/dist/src/errors.js +22 -0
  333. package/dist/src/external-clis.yaml +8 -0
  334. package/dist/src/main.js +14 -2
  335. package/dist/src/output.js +11 -1
  336. package/dist/src/output.test.js +6 -0
  337. package/dist/src/registry.js +1 -0
  338. package/dist/src/registry.test.js +11 -0
  339. package/dist/src/utils.d.ts +43 -0
  340. package/dist/src/utils.js +97 -0
  341. package/dist/src/utils.test.d.ts +1 -0
  342. package/dist/src/utils.test.js +155 -0
  343. package/package.json +8 -2
  344. package/scripts/silent-column-drop-baseline.json +0 -52
  345. package/scripts/typed-error-lint-baseline.json +28 -380
  346. package/clis/slock/_utils.js +0 -12
@@ -0,0 +1,34 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+ import { atlassianRequest, parseLimit, queryString, requireNonEmptyRows, requireString } from '../_atlassian/shared.js';
3
+ import { confluenceConfig, confluenceResults, normalizeSearchResult, withSpaceCql } from './shared.js';
4
+
5
+ cli({
6
+ site: 'confluence',
7
+ name: 'search',
8
+ access: 'read',
9
+ description: 'Search Confluence content with CQL',
10
+ domain: 'atlassian.net',
11
+ strategy: Strategy.PUBLIC,
12
+ browser: false,
13
+ args: [
14
+ { name: 'cql', positional: true, required: true, help: 'CQL query, e.g. "type = page and title ~ \\"RCA\\""' },
15
+ { name: 'space', type: 'string', help: 'Limit search to a Confluence space key' },
16
+ { name: 'limit', type: 'int', default: 20, help: 'Max results to return (1-100)' },
17
+ ],
18
+ columns: ['id', 'title', 'type', 'spaceKey', 'status', 'lastModified', 'url'],
19
+ func: async (args) => {
20
+ const config = confluenceConfig();
21
+ const cql = withSpaceCql(requireString(args.cql, 'CQL'), args.space);
22
+ const limit = parseLimit(args.limit, 20, 100, 'confluence limit');
23
+ // CQL search is still exposed through Confluence REST v1 for Cloud;
24
+ // page CRUD uses v2 where available.
25
+ const path = `/rest/api/search${queryString({ cql, limit })}`;
26
+ const data = await atlassianRequest(config, path, { label: 'confluence search' });
27
+ const results = confluenceResults(data, 'confluence search');
28
+ return requireNonEmptyRows(
29
+ results.map((result) => normalizeSearchResult(result, config)),
30
+ 'confluence search',
31
+ `No Confluence content matched "${cql}".`,
32
+ );
33
+ },
34
+ });
@@ -0,0 +1,173 @@
1
+ import {
2
+ atlassianRequest,
3
+ getConfluenceConfig,
4
+ htmlToMarkdown,
5
+ markdownToConfluenceStorage,
6
+ queryString,
7
+ requirePayloadArray,
8
+ requirePayloadObject,
9
+ requirePayloadString,
10
+ readUtf8File,
11
+ requireString,
12
+ } from '../_atlassian/shared.js';
13
+ import { CommandExecutionError } from '@jackwener/opencli/errors';
14
+
15
+ export function confluenceConfig() {
16
+ return getConfluenceConfig();
17
+ }
18
+
19
+ function confluenceUrl(config, link) {
20
+ if (!link) return '';
21
+ if (/^https?:\/\//i.test(link)) return link;
22
+ return `${config.baseUrl}${link.startsWith('/') ? link : `/${link}`}`;
23
+ }
24
+
25
+ function pageStorageBody(page) {
26
+ return page?.body?.storage?.value
27
+ ?? page?.body?.view?.value
28
+ ?? '';
29
+ }
30
+
31
+ export function normalizeConfluencePage(page, config) {
32
+ const row = requirePayloadObject(page, 'confluence page');
33
+ const id = requirePayloadString(row.id, 'page id', 'confluence page');
34
+ const title = requirePayloadString(row.title, 'title', 'confluence page');
35
+ const storage = pageStorageBody(row);
36
+ const version = row.version?.number != null ? Number(row.version.number) : undefined;
37
+ const links = row._links && typeof row._links === 'object' && !Array.isArray(row._links) ? row._links : {};
38
+ const webui = links.webui ?? links.tinyui ?? '';
39
+ return {
40
+ id,
41
+ title,
42
+ status: String(row.status ?? ''),
43
+ spaceId: row.spaceId != null ? String(row.spaceId) : undefined,
44
+ spaceKey: row.space?.key ? String(row.space.key) : undefined,
45
+ parentId: row.parentId != null ? String(row.parentId) : undefined,
46
+ version,
47
+ createdAt: row.createdAt ? String(row.createdAt) : undefined,
48
+ updatedAt: row.version?.createdAt ?? row.version?.when ?? undefined,
49
+ url: confluenceUrl(config, webui),
50
+ body: {
51
+ storage,
52
+ markdown: htmlToMarkdown(storage),
53
+ },
54
+ };
55
+ }
56
+
57
+ export async function getPage(config, pageId) {
58
+ if (config.deployment === 'cloud') {
59
+ const page = await atlassianRequest(config, `/api/v2/pages/${encodeURIComponent(pageId)}${queryString({ 'body-format': 'storage' })}`, {
60
+ label: `confluence page ${pageId}`,
61
+ });
62
+ return requirePayloadObject(page, `confluence page ${pageId}`);
63
+ }
64
+ const page = await atlassianRequest(config, `/rest/api/content/${encodeURIComponent(pageId)}${queryString({ expand: 'body.storage,version,space,ancestors' })}`, {
65
+ label: `confluence page ${pageId}`,
66
+ });
67
+ return requirePayloadObject(page, `confluence page ${pageId}`);
68
+ }
69
+
70
+ export async function readPageBodyFile(args) {
71
+ const text = await readUtf8File(args.file);
72
+ if (args.representation === 'storage') return text;
73
+ return markdownToConfluenceStorage(text);
74
+ }
75
+
76
+ export function createPagePayload(config, args, storage) {
77
+ const title = requireString(args.title, 'Confluence page title');
78
+ const space = requireString(args.space, 'Confluence space');
79
+ if (config.deployment === 'cloud') {
80
+ return {
81
+ spaceId: space,
82
+ status: 'current',
83
+ title,
84
+ ...(args.parent ? { parentId: String(args.parent) } : {}),
85
+ body: { representation: 'storage', value: storage },
86
+ };
87
+ }
88
+ return {
89
+ type: 'page',
90
+ status: 'current',
91
+ title,
92
+ space: { key: space },
93
+ ...(args.parent ? { ancestors: [{ id: String(args.parent) }] } : {}),
94
+ body: { storage: { representation: 'storage', value: storage } },
95
+ };
96
+ }
97
+
98
+ export function updatePagePayload(config, current, args, storage) {
99
+ const page = requirePayloadObject(current, 'confluence current page');
100
+ const id = requirePayloadString(page.id, 'page id', 'confluence current page');
101
+ const title = args.title ? requireString(args.title, 'Confluence page title') : requirePayloadString(page.title, 'title', 'confluence current page');
102
+ const currentVersion = Number(page.version?.number);
103
+ if (!Number.isSafeInteger(currentVersion) || currentVersion < 1) {
104
+ throw new CommandExecutionError('confluence update could not determine the current page version.');
105
+ }
106
+ const nextVersion = currentVersion + 1;
107
+ if (config.deployment === 'cloud') {
108
+ return {
109
+ id,
110
+ status: 'current',
111
+ title,
112
+ body: { representation: 'storage', value: storage },
113
+ version: {
114
+ number: nextVersion,
115
+ ...(args['version-message'] ? { message: String(args['version-message']) } : {}),
116
+ },
117
+ };
118
+ }
119
+ return {
120
+ id,
121
+ type: 'page',
122
+ status: 'current',
123
+ title,
124
+ body: { storage: { representation: 'storage', value: storage } },
125
+ version: {
126
+ number: nextVersion,
127
+ ...(args['version-message'] ? { message: String(args['version-message']) } : {}),
128
+ },
129
+ };
130
+ }
131
+
132
+ export function normalizeSearchResult(result, config) {
133
+ const row = requirePayloadObject(result, 'confluence search result');
134
+ const content = row.content ?? row;
135
+ const contentObject = requirePayloadObject(content, 'confluence search result content');
136
+ const id = requirePayloadString(contentObject.id, 'content id', 'confluence search result');
137
+ const title = requirePayloadString(row.title ?? contentObject.title, 'title', 'confluence search result');
138
+ const space = row.space ?? contentObject.space ?? {};
139
+ return {
140
+ id,
141
+ title,
142
+ type: String(contentObject.type ?? row.entityType ?? ''),
143
+ spaceKey: String(space?.key ?? ''),
144
+ status: String(contentObject.status ?? ''),
145
+ lastModified: String(row.lastModified ?? contentObject.version?.when ?? contentObject.version?.createdAt ?? ''),
146
+ url: confluenceUrl(config, row.url ?? contentObject._links?.webui ?? ''),
147
+ excerpt: row.excerpt ? htmlToMarkdown(row.excerpt) : '',
148
+ };
149
+ }
150
+
151
+ export function confluenceResults(data, label) {
152
+ const payload = requirePayloadObject(data, label);
153
+ return requirePayloadArray(payload.results, label);
154
+ }
155
+
156
+ export function withSpaceCql(cql, space) {
157
+ const q = String(cql ?? '').trim();
158
+ const s = String(space ?? '').trim();
159
+ if (!s) return q;
160
+ const escaped = s.replace(/"/g, '\\"');
161
+ if (!q) return `space = "${escaped}"`;
162
+ return `space = "${escaped}" and (${q})`;
163
+ }
164
+
165
+ export const __test__ = {
166
+ createPagePayload,
167
+ getPage,
168
+ normalizeConfluencePage,
169
+ normalizeSearchResult,
170
+ readPageBodyFile,
171
+ updatePagePayload,
172
+ withSpaceCql,
173
+ };
@@ -0,0 +1,38 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+ import { atlassianRequest, requireExecute, requirePayloadObject, requireString } from '../_atlassian/shared.js';
3
+ import { confluenceConfig, getPage, normalizeConfluencePage, readPageBodyFile, updatePagePayload } from './shared.js';
4
+
5
+ cli({
6
+ site: 'confluence',
7
+ name: 'update',
8
+ access: 'write',
9
+ description: 'Update a Confluence page body from Markdown or storage XHTML',
10
+ domain: 'atlassian.net',
11
+ strategy: Strategy.PUBLIC,
12
+ browser: false,
13
+ args: [
14
+ { name: 'id', positional: true, required: true, help: 'Confluence page id' },
15
+ { name: 'file', type: 'string', required: true, help: 'Markdown file path' },
16
+ { name: 'title', type: 'string', help: 'Optional replacement title; defaults to current title' },
17
+ { name: 'version-message', type: 'string', help: 'Confluence version message' },
18
+ { name: 'representation', type: 'string', default: 'markdown', choices: ['markdown', 'storage'], help: 'Input file format' },
19
+ { name: 'execute', type: 'boolean', help: 'Actually update the remote page' },
20
+ ],
21
+ columns: ['status', 'id', 'title', 'spaceId', 'spaceKey', 'version', 'url'],
22
+ func: async (args) => {
23
+ requireExecute(args, 'confluence update');
24
+ const config = confluenceConfig();
25
+ const id = requireString(args.id, 'Confluence page id');
26
+ const current = await getPage(config, id);
27
+ const storage = await readPageBodyFile(args);
28
+ const payload = updatePagePayload(config, current, args, storage);
29
+ const path = config.deployment === 'cloud' ? `/api/v2/pages/${encodeURIComponent(id)}` : `/rest/api/content/${encodeURIComponent(id)}`;
30
+ const page = requirePayloadObject(await atlassianRequest(config, path, {
31
+ method: 'PUT',
32
+ body: payload,
33
+ label: `confluence update ${id}`,
34
+ }), `confluence update ${id}`);
35
+ const normalized = normalizeConfluencePage(page, config);
36
+ return [{ ...normalized, pageStatus: normalized.status, status: 'updated' }];
37
+ },
38
+ });
@@ -1,4 +1,11 @@
1
- import { CommandExecutionError } from '@jackwener/opencli/errors';
1
+ import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
2
+ import { unwrapEvaluateResult } from './evaluate-result.js';
3
+
4
+ function isAuthLikeError(code, message) {
5
+ const text = String(message ?? '');
6
+ return code === 401 || code === 403 || /login|cookie|auth|captcha|verify|forbidden|permission|登录|登陆|权限|验证|验证码/i.test(text);
7
+ }
8
+
2
9
  /**
3
10
  * Execute a fetch() call inside the Chrome browser context via page.evaluate.
4
11
  * This ensures a_bogus signing and cookies are handled automatically by the browser.
@@ -6,36 +13,53 @@ import { CommandExecutionError } from '@jackwener/opencli/errors';
6
13
  export async function browserFetch(page, method, url, options = {}) {
7
14
  const js = `
8
15
  (async () => {
9
- const res = await fetch(${JSON.stringify(url)}, {
10
- method: ${JSON.stringify(method)},
11
- credentials: 'include',
12
- headers: {
13
- 'Content-Type': 'application/json',
14
- ...${JSON.stringify(options.headers ?? {})}
15
- },
16
- ${options.body ? `body: JSON.stringify(${JSON.stringify(options.body)}),` : ''}
17
- });
18
- const text = await res.text();
19
- if (!text) return null;
20
- return JSON.parse(text);
16
+ const controller = new AbortController();
17
+ const timer = setTimeout(() => controller.abort(), ${Number(options.timeoutMs ?? 30000)});
18
+ try {
19
+ const res = await fetch(${JSON.stringify(url)}, {
20
+ method: ${JSON.stringify(method)},
21
+ credentials: 'include',
22
+ signal: controller.signal,
23
+ headers: {
24
+ 'Content-Type': 'application/json',
25
+ ...${JSON.stringify(options.headers ?? {})}
26
+ },
27
+ ${options.body ? `body: JSON.stringify(${JSON.stringify(options.body)}),` : ''}
28
+ });
29
+ const text = await res.text();
30
+ try {
31
+ return JSON.parse(text);
32
+ } catch (error) {
33
+ return { status_code: res.ok ? -2 : res.status, status_msg: \`JSON parse failed: \${text.slice(0, 500) || String(error && error.message || error)}\` };
34
+ }
35
+ } catch (error) {
36
+ return { status_code: -1, status_msg: String(error && error.message || error) };
37
+ } finally {
38
+ clearTimeout(timer);
39
+ }
21
40
  })()
22
41
  `;
23
42
  let result;
24
43
  try {
25
- result = await page.evaluate(js);
44
+ result = unwrapEvaluateResult(await page.evaluate(js));
26
45
  }
27
46
  catch (error) {
28
- const message = error instanceof Error ? error.message : String(error);
29
- throw new CommandExecutionError(`Douyin API request failed: ${message}`);
47
+ throw new CommandExecutionError(`Douyin API request failed (${method} ${url}): ${error instanceof Error ? error.message : String(error)}`);
48
+ }
49
+ if (result == null) {
50
+ throw new CommandExecutionError(`Empty response from Douyin API (${method} ${url})`);
30
51
  }
31
- if (result === null || result === undefined) {
32
- throw new CommandExecutionError('Empty response from Douyin API');
52
+ if (Array.isArray(result) || typeof result !== 'object') {
53
+ throw new CommandExecutionError(`Malformed response from Douyin API (${method} ${url})`);
33
54
  }
34
55
  if (result && typeof result === 'object' && 'status_code' in result) {
35
56
  const code = result.status_code;
36
57
  if (code !== 0) {
37
- const msg = result.status_msg ?? 'unknown error';
38
- throw new CommandExecutionError(`Douyin API error ${code}: ${msg}`);
58
+ const msg = result.status_msg ?? result.message ?? 'unknown error';
59
+ if (isAuthLikeError(code, msg)) {
60
+ throw new AuthRequiredError('creator.douyin.com', `Douyin API auth/permission error ${code} at ${method} ${url}: ${msg}`);
61
+ }
62
+ throw new CommandExecutionError(`Douyin API error ${code} at ${method} ${url}: ${msg}`);
39
63
  }
40
64
  }
41
65
  return result;
@@ -1,4 +1,5 @@
1
1
  import { describe, expect, it, vi } from 'vitest';
2
+ import { AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors';
2
3
  import { browserFetch } from './browser-fetch.js';
3
4
  function makePage(result) {
4
5
  return {
@@ -18,10 +19,20 @@ describe('browserFetch', () => {
18
19
  const result = await browserFetch(page, 'GET', 'https://creator.douyin.com/api/test');
19
20
  expect(result).toEqual({ status_code: 0, data: { ak: 'KEY' } });
20
21
  });
22
+ it('unwraps Browser Bridge {session,data} envelopes', async () => {
23
+ const page = makePage({ session: 'site:douyin:test', data: { status_code: 0, data: { ok: true } } });
24
+ await expect(browserFetch(page, 'GET', 'https://creator.douyin.com/api/test'))
25
+ .resolves.toEqual({ status_code: 0, data: { ok: true } });
26
+ });
21
27
  it('throws when status_code is non-zero', async () => {
22
28
  const page = makePage({ status_code: 8, message: 'fail' });
23
29
  await expect(browserFetch(page, 'GET', 'https://creator.douyin.com/api/test')).rejects.toThrow('Douyin API error 8');
24
30
  });
31
+ it('maps auth-like API errors to AuthRequiredError', async () => {
32
+ const page = makePage({ status_code: 401, status_msg: 'login required' });
33
+ await expect(browserFetch(page, 'GET', 'https://creator.douyin.com/api/test'))
34
+ .rejects.toBeInstanceOf(AuthRequiredError);
35
+ });
25
36
  it('returns result even when no status_code field', async () => {
26
37
  const page = makePage({ some_field: 'value' });
27
38
  const result = await browserFetch(page, 'GET', 'https://creator.douyin.com/api/test');
@@ -35,9 +46,19 @@ describe('browserFetch', () => {
35
46
  const page = makePage(undefined);
36
47
  await expect(browserFetch(page, 'GET', 'https://creator.douyin.com/api/test')).rejects.toThrow('Empty response from Douyin API');
37
48
  });
49
+ it('throws typed on malformed primitive response body', async () => {
50
+ const page = makePage('not-json-object');
51
+ await expect(browserFetch(page, 'GET', 'https://creator.douyin.com/api/test'))
52
+ .rejects.toBeInstanceOf(CommandExecutionError);
53
+ });
54
+ it('throws typed when browser fetch returns a non-JSON body', async () => {
55
+ const page = makePage({ status_code: -2, status_msg: 'JSON parse failed: <html>not-json</html>' });
56
+ await expect(browserFetch(page, 'GET', 'https://creator.douyin.com/api/test'))
57
+ .rejects.toThrow('Douyin API error -2');
58
+ });
38
59
  it('wraps browser-side fetch or JSON parse failures', async () => {
39
60
  const page = makePage(null);
40
61
  page.evaluate.mockRejectedValueOnce(new SyntaxError('Unexpected token < in JSON'));
41
- await expect(browserFetch(page, 'GET', 'https://creator.douyin.com/api/test')).rejects.toThrow('Douyin API request failed: Unexpected token < in JSON');
62
+ await expect(browserFetch(page, 'GET', 'https://creator.douyin.com/api/test')).rejects.toThrow('Douyin API request failed (GET https://creator.douyin.com/api/test): Unexpected token < in JSON');
42
63
  });
43
64
  });
@@ -0,0 +1,16 @@
1
+ import { CommandExecutionError } from '@jackwener/opencli/errors';
2
+
3
+ export function unwrapEvaluateResult(payload) {
4
+ if (payload && !Array.isArray(payload) && typeof payload === 'object' && 'session' in payload && 'data' in payload) {
5
+ return payload.data;
6
+ }
7
+ return payload;
8
+ }
9
+
10
+ export function requireObjectEvaluateResult(payload, context) {
11
+ const result = unwrapEvaluateResult(payload);
12
+ if (!result || Array.isArray(result) || typeof result !== 'object') {
13
+ throw new CommandExecutionError(`${context}: malformed evaluate payload`);
14
+ }
15
+ return result;
16
+ }
@@ -56,6 +56,31 @@ function sha256Hex(data) {
56
56
  }
57
57
  return hash.digest('hex');
58
58
  }
59
+ const CRC32_TABLE = new Uint32Array(256).map((_, index) => {
60
+ let value = index;
61
+ for (let bit = 0; bit < 8; bit += 1) {
62
+ value = (value & 1) ? (0xEDB88320 ^ (value >>> 1)) : (value >>> 1);
63
+ }
64
+ return value >>> 0;
65
+ });
66
+ function crc32Hex(data) {
67
+ let crc = 0xffffffff;
68
+ for (const byte of data) {
69
+ crc = CRC32_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8);
70
+ }
71
+ return ((crc ^ 0xffffffff) >>> 0).toString(16).padStart(8, '0');
72
+ }
73
+ function gatewayBaseUrl(tosUrl) {
74
+ const parsedUrl = new URL(tosUrl);
75
+ return `https://${parsedUrl.host}/upload/v1${parsedUrl.pathname}`;
76
+ }
77
+ function gatewayHeaders(auth, uploadHeader, userId = '') {
78
+ return {
79
+ Authorization: auth,
80
+ 'X-Storage-U': encodeURIComponent(userId),
81
+ ...(uploadHeader ?? {}),
82
+ };
83
+ }
59
84
  function extractRegionFromHost(host) {
60
85
  // e.g. "tos-cn-i-alisg.volces.com" → "cn-i-alisg"
61
86
  // e.g. "tos-cn-beijing.ivolces.com" → "cn-beijing"
@@ -129,6 +154,7 @@ async function tosRequest(opts) {
129
154
  method,
130
155
  headers,
131
156
  body: fetchBody,
157
+ signal: AbortSignal.timeout(60000),
132
158
  });
133
159
  const responseBody = await res.text();
134
160
  const responseHeaders = {};
@@ -140,86 +166,95 @@ async function tosRequest(opts) {
140
166
  function nowDatetime() {
141
167
  return new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d+Z$/, 'Z');
142
168
  }
169
+ function extractUploadId(body) {
170
+ const xmlMatch = body.match(/<UploadId>([^<]+)<\/UploadId>/i);
171
+ if (xmlMatch) return xmlMatch[1];
172
+ try {
173
+ const json = JSON.parse(body);
174
+ return json?.payload?.uploadID
175
+ || json?.payload?.uploadId
176
+ || json?.payload?.UploadID
177
+ || json?.payload?.UploadId
178
+ || json?.data?.uploadid
179
+ || json?.data?.uploadID
180
+ || json?.data?.uploadId
181
+ || json?.data?.UploadID
182
+ || json?.data?.UploadId
183
+ || json?.UploadID
184
+ || json?.UploadId
185
+ || json?.uploadID
186
+ || json?.uploadId
187
+ || null;
188
+ }
189
+ catch {
190
+ return null;
191
+ }
192
+ }
143
193
  // ── Phase 1: Init multipart upload ───────────────────────────────────────────
144
- async function initMultipartUpload(tosUrl, auth, credentials) {
145
- const initUrl = `${tosUrl}?uploads`;
146
- const datetime = nowDatetime();
147
- // Use the pre-computed auth for INIT, as it comes from ApplyVideoUpload
148
- const headers = {
149
- Authorization: auth,
150
- 'x-amz-date': datetime,
151
- 'x-amz-security-token': credentials.session_token,
152
- 'content-type': 'application/octet-stream',
153
- };
154
- const res = await tosRequest({ method: 'POST', url: initUrl, headers });
194
+ async function initMultipartUpload(tosUrl, auth, uploadHeader, userId) {
195
+ const initUrl = `${gatewayBaseUrl(tosUrl)}?uploadmode=part&phase=init`;
196
+ const res = await tosRequest({
197
+ method: 'POST',
198
+ url: initUrl,
199
+ headers: gatewayHeaders(auth, uploadHeader, userId),
200
+ });
155
201
  if (res.status !== 200) {
156
- throw new CommandExecutionError(`TOS init multipart upload failed with status ${res.status}: ${res.body}`, 'Check that TOS credentials are valid and not expired.');
202
+ throw new CommandExecutionError(`TOS init multipart upload failed with status ${res.status}: ${res.body}`, 'Check that TOS upload authorization is valid and not expired.');
157
203
  }
158
- // Parse UploadId from XML: <UploadId>...</UploadId>
159
- const match = res.body.match(/<UploadId>([^<]+)<\/UploadId>/);
160
- if (!match) {
204
+ const uploadId = extractUploadId(res.body);
205
+ if (!uploadId) {
161
206
  throw new CommandExecutionError(`TOS init response missing UploadId: ${res.body}`);
162
207
  }
163
- return match[1];
208
+ return uploadId;
164
209
  }
165
210
  // ── Phase 2: Upload a single part ────────────────────────────────────────────
166
- async function uploadPart(tosUrl, partNumber, uploadId, data, credentials, region) {
167
- const parsedUrl = new URL(tosUrl);
168
- parsedUrl.searchParams.set('partNumber', String(partNumber));
169
- parsedUrl.searchParams.set('uploadId', uploadId);
170
- const url = parsedUrl.toString();
171
- const datetime = nowDatetime();
172
- const headers = computeAws4Headers({
173
- method: 'PUT',
174
- url,
175
- headers: { 'content-type': 'application/octet-stream' },
176
- body: data,
177
- credentials,
178
- service: 'tos',
179
- region,
180
- datetime,
181
- });
182
- const res = await tosRequest({ method: 'PUT', url, headers, body: data });
183
- if (res.status !== 200) {
184
- throw new CommandExecutionError(`TOS upload part ${partNumber} failed with status ${res.status}: ${res.body}`, 'Check that STS2 credentials are valid and not expired.');
211
+ async function uploadPart(tosUrl, partNumber, uploadId, data, auth, uploadHeader, userId) {
212
+ const crc32 = crc32Hex(data);
213
+ const url = `${gatewayBaseUrl(tosUrl)}?uploadid=${encodeURIComponent(uploadId)}&part_number=${partNumber}&phase=transfer`;
214
+ const headers = {
215
+ ...gatewayHeaders(auth, uploadHeader, userId),
216
+ 'Content-CRC32': crc32,
217
+ 'Content-Type': 'application/octet-stream',
218
+ 'X-Use-Init-Upload-Optimize': '1',
219
+ 'X-Use-Large-Local-Cache': '1',
220
+ };
221
+ const res = await tosRequest({ method: 'POST', url, headers, body: data });
222
+ let parsed;
223
+ try {
224
+ parsed = JSON.parse(res.body);
185
225
  }
186
- const etag = res.headers['etag'];
187
- if (!etag) {
188
- throw new CommandExecutionError(`TOS upload part ${partNumber} response missing ETag header`);
226
+ catch {
227
+ parsed = null;
189
228
  }
190
- return etag;
229
+ if (res.status !== 200 || parsed?.code !== 2000) {
230
+ throw new CommandExecutionError(`TOS upload part ${partNumber} failed with status ${res.status}: ${res.body}`, 'Check that TOS upload authorization is valid and not expired.');
231
+ }
232
+ return parsed?.data?.crc32 || crc32;
191
233
  }
192
234
  // ── Phase 3: Complete multipart upload ───────────────────────────────────────
193
- async function completeMultipartUpload(tosUrl, uploadId, parts, credentials, region) {
194
- const parsedUrl = new URL(tosUrl);
195
- parsedUrl.searchParams.set('uploadId', uploadId);
196
- const url = parsedUrl.toString();
197
- const xmlBody = '<CompleteMultipartUpload>' +
198
- parts
199
- .sort((a, b) => a.partNumber - b.partNumber)
200
- .map(p => `<Part><PartNumber>${p.partNumber}</PartNumber><ETag>${p.etag}</ETag></Part>`)
201
- .join('') +
202
- '</CompleteMultipartUpload>';
203
- const datetime = nowDatetime();
204
- const headers = computeAws4Headers({
205
- method: 'POST',
206
- url,
207
- headers: { 'content-type': 'application/xml' },
208
- body: xmlBody,
209
- credentials,
210
- service: 'tos',
211
- region,
212
- datetime,
213
- });
235
+ async function completeMultipartUpload(tosUrl, uploadId, parts, auth, uploadHeader, userId) {
236
+ const url = `${gatewayBaseUrl(tosUrl)}?uploadmode=part&phase=finish&uploadid=${encodeURIComponent(uploadId)}`;
237
+ const body = parts
238
+ .sort((a, b) => a.partNumber - b.partNumber)
239
+ .map(p => `${p.partNumber}:${p.crc32}`)
240
+ .join(',');
214
241
  const res = await tosRequest({
215
242
  method: 'POST',
216
243
  url,
217
- headers,
218
- body: xmlBody,
244
+ headers: gatewayHeaders(auth, uploadHeader, userId),
245
+ body,
219
246
  });
220
- if (res.status !== 200) {
247
+ let parsed;
248
+ try {
249
+ parsed = JSON.parse(res.body);
250
+ }
251
+ catch {
252
+ parsed = null;
253
+ }
254
+ if (res.status !== 200 || parsed?.code !== 2000) {
221
255
  throw new CommandExecutionError(`TOS complete multipart upload failed with status ${res.status}: ${res.body}`, 'Check that all parts were uploaded successfully.');
222
256
  }
257
+ return parsed?.data?.key || null;
223
258
  }
224
259
  let _readSyncOverride = null;
225
260
  /** @internal — for testing only */
@@ -237,7 +272,7 @@ export async function tosUpload(options) {
237
272
  if (fileSize === 0) {
238
273
  throw new CommandExecutionError(`Video file is empty: ${filePath}`);
239
274
  }
240
- const { tos_upload_url: tosUrl, auth } = uploadInfo;
275
+ const { tos_upload_url: tosUrl, auth, upload_header: uploadHeader, user_id: userId } = uploadInfo;
241
276
  const parsedTosUrl = new URL(tosUrl);
242
277
  const region = extractRegionFromHost(parsedTosUrl.host);
243
278
  const resumePath = getResumeFilePath(filePath);
@@ -251,7 +286,7 @@ export async function tosUpload(options) {
251
286
  }
252
287
  else {
253
288
  // Start fresh
254
- uploadId = await initMultipartUpload(tosUrl, auth, credentials);
289
+ uploadId = await initMultipartUpload(tosUrl, auth, uploadHeader, userId);
255
290
  completedParts = [];
256
291
  saveResumeState(resumePath, { uploadId, fileSize, parts: completedParts });
257
292
  }
@@ -277,8 +312,8 @@ export async function tosUpload(options) {
277
312
  if (bytesRead !== chunkSize) {
278
313
  throw new CommandExecutionError(`Short read on part ${partNumber}: expected ${chunkSize} bytes, got ${bytesRead}`);
279
314
  }
280
- const etag = await uploadPart(tosUrl, partNumber, uploadId, buffer, credentials, region);
281
- completedParts.push({ partNumber, etag });
315
+ const crc32 = await uploadPart(tosUrl, partNumber, uploadId, buffer, auth, uploadHeader, userId);
316
+ completedParts.push({ partNumber, crc32 });
282
317
  saveResumeState(resumePath, { uploadId, fileSize, parts: completedParts });
283
318
  uploadedBytes = Math.min(offset + chunkSize, fileSize);
284
319
  if (onProgress)
@@ -288,8 +323,9 @@ export async function tosUpload(options) {
288
323
  finally {
289
324
  fs.closeSync(fd);
290
325
  }
291
- await completeMultipartUpload(tosUrl, uploadId, completedParts, credentials, region);
326
+ const completedKey = await completeMultipartUpload(tosUrl, uploadId, completedParts, auth, uploadHeader, userId);
292
327
  deleteResumeState(resumePath);
328
+ return completedKey;
293
329
  }
294
330
  // ── Internal exports for testing ─────────────────────────────────────────────
295
- export { PART_SIZE, RESUME_DIR, extractRegionFromHost, getResumeFilePath, loadResumeState, saveResumeState, deleteResumeState, computeAws4Headers, };
331
+ export { PART_SIZE, RESUME_DIR, extractRegionFromHost, getResumeFilePath, loadResumeState, saveResumeState, deleteResumeState, computeAws4Headers, extractUploadId, crc32Hex, gatewayBaseUrl, gatewayHeaders, };