@jackwener/opencli 1.5.6 → 1.5.8

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 (338) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +4 -2
  3. package/README.zh-CN.md +4 -1
  4. package/SKILL.md +879 -0
  5. package/dist/browser/cdp.d.ts +1 -0
  6. package/dist/browser/cdp.js +30 -27
  7. package/dist/browser/daemon-client.d.ts +7 -1
  8. package/dist/browser/daemon-client.js +3 -0
  9. package/dist/browser/dom-helpers.js +1 -0
  10. package/dist/browser/dom-helpers.test.js +14 -1
  11. package/dist/browser/mcp.js +18 -13
  12. package/dist/browser/page.js +22 -2
  13. package/dist/browser/page.test.d.ts +1 -0
  14. package/dist/browser/page.test.js +44 -0
  15. package/dist/browser/stealth.js +198 -0
  16. package/dist/browser/stealth.test.d.ts +1 -0
  17. package/dist/browser/stealth.test.js +134 -0
  18. package/dist/browser.test.js +1 -1
  19. package/dist/build-manifest.d.ts +1 -0
  20. package/dist/build-manifest.js +5 -1
  21. package/dist/build-manifest.test.js +2 -0
  22. package/dist/cli-manifest.json +544 -137
  23. package/dist/cli.js +20 -3
  24. package/dist/clis/antigravity/serve.d.ts +1 -1
  25. package/dist/clis/antigravity/serve.js +5 -8
  26. package/dist/clis/bilibili/subtitle.js +4 -0
  27. package/dist/clis/bilibili/subtitle.test.d.ts +1 -0
  28. package/dist/clis/bilibili/subtitle.test.js +48 -0
  29. package/dist/clis/chatwise/ask.js +0 -2
  30. package/dist/clis/chatwise/export.js +0 -2
  31. package/dist/clis/chatwise/history.js +0 -2
  32. package/dist/clis/chatwise/model.js +0 -2
  33. package/dist/clis/chatwise/new.js +1 -2
  34. package/dist/clis/chatwise/read.js +0 -2
  35. package/dist/clis/chatwise/screenshot.js +1 -2
  36. package/dist/clis/chatwise/send.js +0 -2
  37. package/dist/clis/chatwise/status.js +1 -2
  38. package/dist/clis/ctrip/search.d.ts +13 -0
  39. package/dist/clis/ctrip/search.js +73 -48
  40. package/dist/clis/ctrip/search.test.d.ts +1 -0
  41. package/dist/clis/ctrip/search.test.js +64 -0
  42. package/dist/clis/douyin/_shared/sts2.js +8 -2
  43. package/dist/clis/douyin/_shared/sts2.test.d.ts +1 -0
  44. package/dist/clis/douyin/_shared/sts2.test.js +27 -0
  45. package/dist/clis/douyin/activities.js +4 -2
  46. package/dist/clis/douyin/activities.test.js +34 -1
  47. package/dist/clis/douyin/collections.js +1 -1
  48. package/dist/clis/douyin/collections.test.js +24 -2
  49. package/dist/clis/douyin/draft.d.ts +8 -11
  50. package/dist/clis/douyin/draft.js +302 -185
  51. package/dist/clis/douyin/draft.test.d.ts +1 -1
  52. package/dist/clis/douyin/draft.test.js +357 -2
  53. package/dist/clis/douyin/hashtag.js +9 -2
  54. package/dist/clis/douyin/hashtag.test.js +35 -2
  55. package/dist/clis/douyin/profile.js +1 -1
  56. package/dist/clis/douyin/profile.test.js +36 -1
  57. package/dist/clis/douyin/videos.js +22 -5
  58. package/dist/clis/douyin/videos.test.js +45 -2
  59. package/dist/clis/facebook/search.test.d.ts +5 -0
  60. package/dist/clis/facebook/search.test.js +60 -0
  61. package/dist/clis/facebook/search.yaml +4 -3
  62. package/dist/clis/instagram/download.d.ts +16 -0
  63. package/dist/clis/instagram/download.js +225 -0
  64. package/dist/clis/instagram/download.test.d.ts +1 -0
  65. package/dist/clis/instagram/download.test.js +118 -0
  66. package/dist/clis/notebooklm/bind-current.d.ts +1 -0
  67. package/dist/clis/notebooklm/bind-current.js +29 -0
  68. package/dist/clis/notebooklm/bind-current.test.d.ts +1 -0
  69. package/dist/clis/notebooklm/bind-current.test.js +35 -0
  70. package/dist/clis/notebooklm/binding.test.d.ts +1 -0
  71. package/dist/clis/notebooklm/binding.test.js +44 -0
  72. package/dist/clis/notebooklm/compat.test.d.ts +3 -0
  73. package/dist/clis/notebooklm/compat.test.js +16 -0
  74. package/dist/clis/notebooklm/current.d.ts +1 -0
  75. package/dist/clis/notebooklm/current.js +28 -0
  76. package/dist/clis/notebooklm/get.d.ts +1 -0
  77. package/dist/clis/notebooklm/get.js +37 -0
  78. package/dist/clis/notebooklm/history.d.ts +1 -0
  79. package/dist/clis/notebooklm/history.js +25 -0
  80. package/dist/clis/notebooklm/history.test.d.ts +1 -0
  81. package/dist/clis/notebooklm/history.test.js +58 -0
  82. package/dist/clis/notebooklm/list.d.ts +1 -0
  83. package/dist/clis/notebooklm/list.js +35 -0
  84. package/dist/clis/notebooklm/note-list.d.ts +1 -0
  85. package/dist/clis/notebooklm/note-list.js +28 -0
  86. package/dist/clis/notebooklm/note-list.test.d.ts +1 -0
  87. package/dist/clis/notebooklm/note-list.test.js +56 -0
  88. package/dist/clis/notebooklm/notes-get.d.ts +1 -0
  89. package/dist/clis/notebooklm/notes-get.js +47 -0
  90. package/dist/clis/notebooklm/notes-get.test.d.ts +1 -0
  91. package/dist/clis/notebooklm/notes-get.test.js +72 -0
  92. package/dist/clis/notebooklm/rpc.d.ts +36 -0
  93. package/dist/clis/notebooklm/rpc.js +189 -0
  94. package/dist/clis/notebooklm/rpc.test.d.ts +1 -0
  95. package/dist/clis/notebooklm/rpc.test.js +105 -0
  96. package/dist/clis/notebooklm/shared.d.ts +87 -0
  97. package/dist/clis/notebooklm/shared.js +3 -0
  98. package/dist/clis/notebooklm/source-fulltext.d.ts +1 -0
  99. package/dist/clis/notebooklm/source-fulltext.js +44 -0
  100. package/dist/clis/notebooklm/source-fulltext.test.d.ts +1 -0
  101. package/dist/clis/notebooklm/source-fulltext.test.js +106 -0
  102. package/dist/clis/notebooklm/source-get.d.ts +1 -0
  103. package/dist/clis/notebooklm/source-get.js +40 -0
  104. package/dist/clis/notebooklm/source-get.test.d.ts +1 -0
  105. package/dist/clis/notebooklm/source-get.test.js +84 -0
  106. package/dist/clis/notebooklm/source-guide.d.ts +1 -0
  107. package/dist/clis/notebooklm/source-guide.js +44 -0
  108. package/dist/clis/notebooklm/source-guide.test.d.ts +1 -0
  109. package/dist/clis/notebooklm/source-guide.test.js +104 -0
  110. package/dist/clis/notebooklm/source-list.d.ts +1 -0
  111. package/dist/clis/notebooklm/source-list.js +30 -0
  112. package/dist/clis/notebooklm/status.d.ts +1 -0
  113. package/dist/clis/notebooklm/status.js +31 -0
  114. package/dist/clis/notebooklm/summary.d.ts +1 -0
  115. package/dist/clis/notebooklm/summary.js +30 -0
  116. package/dist/clis/notebooklm/summary.test.d.ts +1 -0
  117. package/dist/clis/notebooklm/summary.test.js +78 -0
  118. package/dist/clis/notebooklm/utils.d.ts +37 -0
  119. package/dist/clis/notebooklm/utils.js +739 -0
  120. package/dist/clis/notebooklm/utils.test.d.ts +1 -0
  121. package/dist/clis/notebooklm/utils.test.js +390 -0
  122. package/dist/clis/substack/utils.d.ts +4 -0
  123. package/dist/clis/substack/utils.js +8 -2
  124. package/dist/clis/substack/utils.test.d.ts +1 -0
  125. package/dist/clis/substack/utils.test.js +46 -0
  126. package/dist/clis/v2ex/hot.yaml +4 -1
  127. package/dist/clis/v2ex/latest.yaml +4 -1
  128. package/dist/clis/v2ex/topic.yaml +6 -1
  129. package/dist/clis/weixin/download.d.ts +9 -0
  130. package/dist/clis/weixin/download.js +76 -6
  131. package/dist/clis/weread/book.js +108 -2
  132. package/dist/clis/weread/commands.test.js +262 -152
  133. package/dist/clis/weread/utils.d.ts +10 -0
  134. package/dist/clis/weread/utils.js +27 -7
  135. package/dist/clis/xiaohongshu/comments.d.ts +3 -0
  136. package/dist/clis/xiaohongshu/comments.js +76 -17
  137. package/dist/clis/xiaohongshu/comments.test.js +70 -9
  138. package/dist/clis/xiaohongshu/download.d.ts +4 -1
  139. package/dist/clis/xiaohongshu/download.js +83 -22
  140. package/dist/clis/xiaohongshu/download.test.d.ts +1 -0
  141. package/dist/clis/xiaohongshu/download.test.js +75 -0
  142. package/dist/clis/xiaohongshu/note-helpers.d.ts +12 -0
  143. package/dist/clis/xiaohongshu/note-helpers.js +23 -0
  144. package/dist/clis/xiaohongshu/note.d.ts +7 -0
  145. package/dist/clis/xiaohongshu/note.js +76 -0
  146. package/dist/clis/xiaohongshu/note.test.d.ts +1 -0
  147. package/dist/clis/xiaohongshu/note.test.js +136 -0
  148. package/dist/clis/xiaohongshu/search.js +9 -0
  149. package/dist/clis/xiaohongshu/search.test.js +10 -4
  150. package/dist/clis/youtube/search.js +57 -17
  151. package/dist/clis/zhihu/question.js +19 -17
  152. package/dist/clis/zhihu/question.test.d.ts +1 -0
  153. package/dist/clis/zhihu/question.test.js +54 -0
  154. package/dist/commanderAdapter.js +9 -0
  155. package/dist/commanderAdapter.test.js +25 -0
  156. package/dist/commands/daemon.d.ts +9 -0
  157. package/dist/commands/daemon.js +124 -0
  158. package/dist/commands/daemon.test.d.ts +1 -0
  159. package/dist/commands/daemon.test.js +185 -0
  160. package/dist/completion.js +3 -1
  161. package/dist/constants.d.ts +2 -0
  162. package/dist/constants.js +2 -0
  163. package/dist/daemon.d.ts +1 -1
  164. package/dist/daemon.js +25 -14
  165. package/dist/daemon.test.d.ts +1 -0
  166. package/dist/daemon.test.js +65 -0
  167. package/dist/discovery.d.ts +9 -0
  168. package/dist/discovery.js +47 -2
  169. package/dist/electron-apps.d.ts +29 -0
  170. package/dist/electron-apps.js +65 -0
  171. package/dist/electron-apps.test.d.ts +1 -0
  172. package/dist/electron-apps.test.js +43 -0
  173. package/dist/engine.test.js +41 -9
  174. package/dist/execution.js +20 -16
  175. package/dist/extension-manifest-regression.test.js +1 -0
  176. package/dist/idle-manager.d.ts +19 -0
  177. package/dist/idle-manager.js +54 -0
  178. package/dist/launcher.d.ts +36 -0
  179. package/dist/launcher.js +152 -0
  180. package/dist/launcher.test.d.ts +1 -0
  181. package/dist/launcher.test.js +57 -0
  182. package/dist/main.js +3 -3
  183. package/dist/registry.d.ts +1 -0
  184. package/dist/registry.js +31 -3
  185. package/dist/registry.test.js +13 -0
  186. package/dist/runtime.d.ts +5 -3
  187. package/dist/runtime.js +12 -5
  188. package/dist/serialization.d.ts +1 -0
  189. package/dist/serialization.js +3 -0
  190. package/dist/serialization.test.js +17 -1
  191. package/dist/tui.d.ts +7 -0
  192. package/dist/tui.js +52 -0
  193. package/dist/tui.test.d.ts +1 -0
  194. package/dist/tui.test.js +19 -0
  195. package/dist/weixin-download.test.js +14 -0
  196. package/docs/.vitepress/config.mts +1 -0
  197. package/docs/adapters/browser/notebooklm.md +69 -0
  198. package/docs/adapters/browser/xiaohongshu.md +19 -10
  199. package/docs/adapters/index.md +67 -66
  200. package/docs/guide/browser-bridge.md +12 -0
  201. package/docs/guide/troubleshooting.md +9 -4
  202. package/docs/superpowers/plans/2026-03-31-daemon-lifecycle-redesign.md +857 -0
  203. package/docs/superpowers/specs/2026-03-31-daemon-lifecycle-redesign.md +208 -0
  204. package/docs/zh/guide/browser-bridge.md +12 -0
  205. package/extension/dist/background.js +250 -11
  206. package/extension/manifest.json +2 -1
  207. package/extension/src/background.test.ts +202 -2
  208. package/extension/src/background.ts +175 -10
  209. package/extension/src/cdp.test.ts +75 -0
  210. package/extension/src/cdp.ts +89 -3
  211. package/extension/src/protocol.ts +7 -5
  212. package/package.json +1 -1
  213. package/src/browser/cdp.ts +24 -17
  214. package/src/browser/daemon-client.ts +7 -1
  215. package/src/browser/dom-helpers.test.ts +15 -1
  216. package/src/browser/dom-helpers.ts +1 -0
  217. package/src/browser/mcp.ts +18 -13
  218. package/src/browser/page.test.ts +58 -0
  219. package/src/browser/page.ts +18 -2
  220. package/src/browser/stealth.test.ts +153 -0
  221. package/src/browser/stealth.ts +198 -0
  222. package/src/browser.test.ts +1 -1
  223. package/src/build-manifest.test.ts +2 -0
  224. package/src/build-manifest.ts +6 -1
  225. package/src/cli.ts +21 -3
  226. package/src/clis/antigravity/SKILL.md +3 -12
  227. package/src/clis/antigravity/serve.ts +5 -10
  228. package/src/clis/bilibili/subtitle.test.ts +60 -0
  229. package/src/clis/bilibili/subtitle.ts +4 -0
  230. package/src/clis/chatwise/ask.ts +0 -2
  231. package/src/clis/chatwise/export.ts +0 -2
  232. package/src/clis/chatwise/history.ts +0 -2
  233. package/src/clis/chatwise/model.ts +0 -2
  234. package/src/clis/chatwise/new.ts +1 -2
  235. package/src/clis/chatwise/read.ts +0 -2
  236. package/src/clis/chatwise/screenshot.ts +1 -2
  237. package/src/clis/chatwise/send.ts +0 -2
  238. package/src/clis/chatwise/status.ts +1 -2
  239. package/src/clis/ctrip/search.test.ts +73 -0
  240. package/src/clis/ctrip/search.ts +97 -47
  241. package/src/clis/douyin/_shared/sts2.test.ts +31 -0
  242. package/src/clis/douyin/_shared/sts2.ts +11 -3
  243. package/src/clis/douyin/activities.test.ts +41 -1
  244. package/src/clis/douyin/activities.ts +12 -3
  245. package/src/clis/douyin/collections.test.ts +35 -2
  246. package/src/clis/douyin/collections.ts +1 -1
  247. package/src/clis/douyin/draft.test.ts +444 -2
  248. package/src/clis/douyin/draft.ts +382 -218
  249. package/src/clis/douyin/hashtag.test.ts +42 -2
  250. package/src/clis/douyin/hashtag.ts +11 -3
  251. package/src/clis/douyin/profile.test.ts +43 -1
  252. package/src/clis/douyin/profile.ts +9 -2
  253. package/src/clis/douyin/videos.test.ts +52 -2
  254. package/src/clis/douyin/videos.ts +49 -15
  255. package/src/clis/facebook/search.test.ts +70 -0
  256. package/src/clis/facebook/search.yaml +4 -3
  257. package/src/clis/instagram/download.test.ts +159 -0
  258. package/src/clis/instagram/download.ts +286 -0
  259. package/src/clis/notebooklm/bind-current.test.ts +43 -0
  260. package/src/clis/notebooklm/bind-current.ts +36 -0
  261. package/src/clis/notebooklm/binding.test.ts +53 -0
  262. package/src/clis/notebooklm/compat.test.ts +19 -0
  263. package/src/clis/notebooklm/current.ts +38 -0
  264. package/src/clis/notebooklm/get.ts +53 -0
  265. package/src/clis/notebooklm/history.test.ts +70 -0
  266. package/src/clis/notebooklm/history.ts +36 -0
  267. package/src/clis/notebooklm/list.ts +40 -0
  268. package/src/clis/notebooklm/note-list.test.ts +64 -0
  269. package/src/clis/notebooklm/note-list.ts +42 -0
  270. package/src/clis/notebooklm/notes-get.test.ts +88 -0
  271. package/src/clis/notebooklm/notes-get.ts +67 -0
  272. package/src/clis/notebooklm/rpc.test.ts +126 -0
  273. package/src/clis/notebooklm/rpc.ts +286 -0
  274. package/src/clis/notebooklm/shared.ts +98 -0
  275. package/src/clis/notebooklm/source-fulltext.test.ts +123 -0
  276. package/src/clis/notebooklm/source-fulltext.ts +69 -0
  277. package/src/clis/notebooklm/source-get.test.ts +100 -0
  278. package/src/clis/notebooklm/source-get.ts +60 -0
  279. package/src/clis/notebooklm/source-guide.test.ts +121 -0
  280. package/src/clis/notebooklm/source-guide.ts +69 -0
  281. package/src/clis/notebooklm/source-list.ts +45 -0
  282. package/src/clis/notebooklm/status.ts +34 -0
  283. package/src/clis/notebooklm/summary.test.ts +94 -0
  284. package/src/clis/notebooklm/summary.ts +45 -0
  285. package/src/clis/notebooklm/utils.test.ts +446 -0
  286. package/src/clis/notebooklm/utils.ts +893 -0
  287. package/src/clis/substack/utils.test.ts +54 -0
  288. package/src/clis/substack/utils.ts +10 -2
  289. package/src/clis/v2ex/hot.yaml +4 -1
  290. package/src/clis/v2ex/latest.yaml +4 -1
  291. package/src/clis/v2ex/topic.yaml +6 -1
  292. package/src/clis/weixin/download.ts +95 -6
  293. package/src/clis/weread/book.ts +142 -2
  294. package/src/clis/weread/commands.test.ts +314 -154
  295. package/src/clis/weread/utils.ts +33 -4
  296. package/src/clis/xiaohongshu/comments.test.ts +85 -9
  297. package/src/clis/xiaohongshu/comments.ts +76 -17
  298. package/src/clis/xiaohongshu/download.test.ts +96 -0
  299. package/src/clis/xiaohongshu/download.ts +83 -22
  300. package/src/clis/xiaohongshu/note-helpers.ts +25 -0
  301. package/src/clis/xiaohongshu/note.test.ts +164 -0
  302. package/src/clis/xiaohongshu/note.ts +86 -0
  303. package/src/clis/xiaohongshu/search.test.ts +11 -4
  304. package/src/clis/xiaohongshu/search.ts +13 -0
  305. package/src/clis/youtube/search.ts +57 -17
  306. package/src/clis/zhihu/question.test.ts +71 -0
  307. package/src/clis/zhihu/question.ts +27 -15
  308. package/src/commanderAdapter.test.ts +30 -0
  309. package/src/commanderAdapter.ts +7 -0
  310. package/src/commands/daemon.test.ts +238 -0
  311. package/src/commands/daemon.ts +135 -0
  312. package/src/completion.ts +2 -1
  313. package/src/constants.ts +3 -0
  314. package/src/daemon.test.ts +88 -0
  315. package/src/daemon.ts +26 -14
  316. package/src/discovery.ts +52 -2
  317. package/src/electron-apps.test.ts +50 -0
  318. package/src/electron-apps.ts +89 -0
  319. package/src/engine.test.ts +45 -9
  320. package/src/execution.ts +24 -19
  321. package/src/extension-manifest-regression.test.ts +1 -0
  322. package/src/idle-manager.ts +60 -0
  323. package/src/launcher.test.ts +67 -0
  324. package/src/launcher.ts +185 -0
  325. package/src/main.ts +3 -2
  326. package/src/registry.test.ts +15 -0
  327. package/src/registry.ts +32 -3
  328. package/src/runtime.ts +13 -7
  329. package/src/serialization.test.ts +19 -1
  330. package/src/serialization.ts +2 -0
  331. package/src/tui.test.ts +23 -0
  332. package/src/tui.ts +65 -0
  333. package/src/weixin-download.test.ts +27 -0
  334. package/tests/e2e/browser-public-extended.test.ts +6 -2
  335. package/chatwise-opencli.ps1 +0 -82
  336. package/dist/clis/chatwise/shared.d.ts +0 -2
  337. package/dist/clis/chatwise/shared.js +0 -6
  338. package/src/clis/chatwise/shared.ts +0 -8
@@ -1,7 +1,16 @@
1
- import { describe, expect, it } from 'vitest';
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ const { browserFetchMock } = vi.hoisted(() => ({
3
+ browserFetchMock: vi.fn(),
4
+ }));
5
+ vi.mock('./_shared/browser-fetch.js', () => ({
6
+ browserFetch: browserFetchMock,
7
+ }));
2
8
  import { getRegistry } from '../../registry.js';
3
9
  import './collections.js';
4
- describe('douyin collections registration', () => {
10
+ describe('douyin collections', () => {
11
+ beforeEach(() => {
12
+ browserFetchMock.mockReset();
13
+ });
5
14
  it('registers the collections command', () => {
6
15
  const registry = getRegistry();
7
16
  const cmd = [...registry.values()].find(c => c.site === 'douyin' && c.name === 'collections');
@@ -20,4 +29,17 @@ describe('douyin collections registration', () => {
20
29
  const cmd = [...registry.values()].find(c => c.site === 'douyin' && c.name === 'collections');
21
30
  expect(cmd?.strategy).toBe('cookie');
22
31
  });
32
+ it('uses the current mix list request shape', async () => {
33
+ const registry = getRegistry();
34
+ const command = [...registry.values()].find((cmd) => cmd.site === 'douyin' && cmd.name === 'collections');
35
+ expect(command?.func).toBeDefined();
36
+ if (!command?.func)
37
+ throw new Error('douyin collections command not registered');
38
+ browserFetchMock.mockResolvedValueOnce({
39
+ mix_list: [],
40
+ });
41
+ const rows = await command.func({}, { limit: 12 });
42
+ expect(browserFetchMock).toHaveBeenCalledWith({}, 'GET', 'https://creator.douyin.com/web/api/mix/list/?status=0,1,2,3,6&count=12&cursor=0&should_query_new_mix=1&device_platform=web&aid=1128');
43
+ expect(rows).toEqual([]);
44
+ });
23
45
  });
@@ -1,14 +1,11 @@
1
1
  /**
2
- * Douyin draft — 6-phase pipeline for saving video as draft.
2
+ * Douyin draft — upload through the official creator page and save as draft.
3
3
  *
4
- * Phases:
5
- * 1. STS2 credentials
6
- * 2. Apply TOS upload URL
7
- * 3. TOS multipart upload
8
- * 4. Cover upload (optional, via ImageX)
9
- * 5. Enable video
10
- * 6. Poll transcode
11
- * 7. (skipped — no safety check for drafts)
12
- * 8. create_v2 with is_draft: 1
4
+ * The previous API pipeline relied on an old pre-upload endpoint that no longer
5
+ * matches creator center's live upload flow. This command now drives the
6
+ * official upload page directly so it stays aligned with the site.
13
7
  */
14
- export {};
8
+ /**
9
+ * Read the local quick-check panel text that reflects cover validation state.
10
+ */
11
+ export declare function buildCoverCheckPanelTextJs(): string;
@@ -1,64 +1,299 @@
1
1
  /**
2
- * Douyin draft — 6-phase pipeline for saving video as draft.
2
+ * Douyin draft — upload through the official creator page and save as draft.
3
3
  *
4
- * Phases:
5
- * 1. STS2 credentials
6
- * 2. Apply TOS upload URL
7
- * 3. TOS multipart upload
8
- * 4. Cover upload (optional, via ImageX)
9
- * 5. Enable video
10
- * 6. Poll transcode
11
- * 7. (skipped — no safety check for drafts)
12
- * 8. create_v2 with is_draft: 1
4
+ * The previous API pipeline relied on an old pre-upload endpoint that no longer
5
+ * matches creator center's live upload flow. This command now drives the
6
+ * official upload page directly so it stays aligned with the site.
13
7
  */
14
8
  import * as fs from 'node:fs';
15
9
  import * as path from 'node:path';
16
10
  import { cli, Strategy } from '../../registry.js';
17
11
  import { ArgumentError, CommandExecutionError } from '../../errors.js';
18
- import { getSts2Credentials } from './_shared/sts2.js';
19
- import { tosUpload } from './_shared/tos-upload.js';
20
- import { imagexUpload } from './_shared/imagex-upload.js';
21
- import { pollTranscode } from './_shared/transcode.js';
22
- import { browserFetch } from './_shared/browser-fetch.js';
23
- import { generateCreationId } from './_shared/creation-id.js';
24
- import { parseTextExtra, extractHashtagNames } from './_shared/text-extra.js';
25
- const VISIBILITY_MAP = {
26
- public: 0,
27
- friends: 1,
28
- private: 2,
12
+ const VISIBILITY_LABELS = {
13
+ public: '公开',
14
+ friends: '好友可见',
15
+ private: '仅自己可见',
29
16
  };
30
- const IMAGEX_BASE = 'https://imagex.bytedanceapi.com';
31
- const IMAGEX_SERVICE_ID = '1147';
32
- const DEVICE_PARAMS = 'aid=1128&cookie_enabled=true&screen_width=1512&screen_height=982&browser_language=zh-CN&browser_platform=MacIntel&browser_name=Mozilla&browser_online=true&timezone_name=Asia%2FTokyo&support_h265=1';
33
- const DEFAULT_COVER_TOOLS_INFO = JSON.stringify({
34
- video_cover_source: 2,
35
- cover_timestamp: 0,
36
- recommend_timestamp: 0,
37
- is_cover_edit: 0,
38
- is_cover_template: 0,
39
- cover_template_id: '',
40
- is_text_template: 0,
41
- text_template_id: '',
42
- text_template_content: '',
43
- is_text: 0,
44
- text_num: 0,
45
- text_content: '',
46
- is_use_sticker: 0,
47
- sticker_id: '',
48
- is_use_filter: 0,
49
- filter_id: '',
50
- is_cover_modify: 0,
51
- to_status: 0,
52
- cover_type: 0,
53
- initial_cover_uri: '',
54
- cut_coordinate: '',
55
- });
17
+ const DRAFT_UPLOAD_URL = 'https://creator.douyin.com/creator-micro/content/upload';
18
+ const COMPOSER_WAIT_ATTEMPTS = 120;
19
+ const COVER_INPUT_WAIT_ATTEMPTS = 20;
20
+ const COVER_READY_WAIT_ATTEMPTS = 20;
21
+ /**
22
+ * Best-effort dismissal for coach marks and upload tips that can block clicks.
23
+ */
24
+ async function dismissKnownModals(page) {
25
+ await page.evaluate(`() => {
26
+ const targets = ['我知道了', '知道了', '关闭'];
27
+ for (const text of targets) {
28
+ const btn = Array.from(document.querySelectorAll('button,[role="button"]'))
29
+ .find((el) => (el.textContent || '').trim() === text);
30
+ if (btn instanceof HTMLElement) btn.click();
31
+ }
32
+ }`);
33
+ }
34
+ /**
35
+ * Wait until Douyin finishes uploading and lands on the post-video composer.
36
+ */
37
+ async function waitForDraftComposer(page) {
38
+ let lastState = {
39
+ href: '',
40
+ ready: false,
41
+ bodyText: '',
42
+ };
43
+ for (let attempt = 0; attempt < COMPOSER_WAIT_ATTEMPTS; attempt += 1) {
44
+ lastState = (await page.evaluate(`() => ({
45
+ href: location.href,
46
+ ready: !!Array.from(document.querySelectorAll('input')).find(
47
+ (el) => (el.placeholder || '').includes('填写作品标题')
48
+ ) && !!Array.from(document.querySelectorAll('button')).find(
49
+ (el) => (el.textContent || '').includes('暂存离开')
50
+ ),
51
+ bodyText: document.body?.innerText || ''
52
+ })`));
53
+ if (lastState.ready)
54
+ return;
55
+ await page.wait({ time: 0.5 });
56
+ }
57
+ throw new CommandExecutionError('等待抖音草稿编辑页超时', `当前页面: ${lastState.href || 'unknown'}`);
58
+ }
59
+ /**
60
+ * Fill title, caption and visibility controls on the live composer page.
61
+ */
62
+ async function fillDraftComposer(page, options) {
63
+ const titleOk = (await page.evaluate(`() => {
64
+ const titleInput = Array.from(document.querySelectorAll('input')).find(
65
+ (el) => (el.placeholder || '').includes('填写作品标题')
66
+ );
67
+ if (!(titleInput instanceof HTMLInputElement)) return false;
68
+ const propKey = Object.keys(titleInput).find((key) => key.startsWith('__reactProps$'));
69
+ const props = propKey ? titleInput[propKey] : null;
70
+ if (props?.onChange) {
71
+ props.onChange({
72
+ target: { value: ${JSON.stringify(options.title)} },
73
+ currentTarget: { value: ${JSON.stringify(options.title)} },
74
+ });
75
+ } else {
76
+ titleInput.focus();
77
+ titleInput.value = ${JSON.stringify(options.title)};
78
+ titleInput.dispatchEvent(new Event('input', { bubbles: true }));
79
+ titleInput.dispatchEvent(new Event('change', { bubbles: true }));
80
+ }
81
+ if (props?.onBlur) {
82
+ props.onBlur({
83
+ target: titleInput,
84
+ currentTarget: titleInput,
85
+ relatedTarget: null,
86
+ });
87
+ } else {
88
+ titleInput.dispatchEvent(new Event('blur', { bubbles: true }));
89
+ }
90
+ return true;
91
+ }`));
92
+ if (!titleOk) {
93
+ throw new CommandExecutionError('填写抖音草稿表单失败: title-input-missing');
94
+ }
95
+ if (options.caption) {
96
+ const captionOk = (await page.evaluate(`() => {
97
+ const editor = document.querySelector('[contenteditable="true"]');
98
+ if (!(editor instanceof HTMLElement)) return false;
99
+ editor.focus();
100
+ editor.textContent = '';
101
+ document.execCommand('selectAll', false);
102
+ document.execCommand('insertText', false, ${JSON.stringify(options.caption)});
103
+ editor.dispatchEvent(new Event('input', { bubbles: true }));
104
+ return true;
105
+ }`));
106
+ if (!captionOk) {
107
+ throw new CommandExecutionError('填写抖音草稿表单失败: caption-editor-missing');
108
+ }
109
+ }
110
+ const visibilityOk = (await page.evaluate(`() => {
111
+ const visibility = Array.from(document.querySelectorAll('label')).find(
112
+ (el) => (el.textContent || '').includes(${JSON.stringify(options.visibilityLabel)})
113
+ );
114
+ if (!(visibility instanceof HTMLElement)) return false;
115
+ visibility.click();
116
+ return true;
117
+ }`));
118
+ if (!visibilityOk) {
119
+ throw new CommandExecutionError('填写抖音草稿表单失败: visibility-missing');
120
+ }
121
+ }
122
+ /**
123
+ * Switch the composer into custom-cover mode and expose the cover input with a
124
+ * stable selector for CDP file injection.
125
+ */
126
+ async function prepareCustomCoverInput(page) {
127
+ let lastReason = 'cover-input-missing';
128
+ const baselineCount = (await page.evaluate(`() => Array.from(document.querySelectorAll('input[type="file"]')).length`));
129
+ for (let attempt = 0; attempt < COVER_INPUT_WAIT_ATTEMPTS; attempt += 1) {
130
+ const result = (await page.evaluate(`() => {
131
+ const coverLabel = Array.from(document.querySelectorAll('label')).find(
132
+ (el) => (el.textContent || '').includes('上传新封面')
133
+ );
134
+ if (coverLabel instanceof HTMLElement) {
135
+ coverLabel.click();
136
+ }
137
+
138
+ const inputs = Array.from(document.querySelectorAll('input[type="file"]'));
139
+ const target = inputs
140
+ .slice(${JSON.stringify(baselineCount)})
141
+ .find((el) => el instanceof HTMLInputElement && !el.disabled);
142
+ if (!(target instanceof HTMLInputElement)) {
143
+ return { ok: false, reason: 'cover-input-pending' };
144
+ }
145
+
146
+ document
147
+ .querySelectorAll('[data-opencli-cover-input="1"]')
148
+ .forEach((el) => el.removeAttribute('data-opencli-cover-input'));
149
+ target.setAttribute('data-opencli-cover-input', '1');
150
+ return { ok: true, selector: '[data-opencli-cover-input="1"]' };
151
+ }`));
152
+ if (result?.ok && result.selector) {
153
+ return result.selector;
154
+ }
155
+ lastReason = result?.reason || lastReason;
156
+ await page.wait({ time: 0.5 });
157
+ }
158
+ throw new CommandExecutionError(`准备抖音自定义封面输入框失败: ${lastReason}`);
159
+ }
160
+ /**
161
+ * Read the local quick-check panel text that reflects cover validation state.
162
+ */
163
+ export function buildCoverCheckPanelTextJs() {
164
+ return `() => {
165
+ const normalize = (value) => (value || '').replace(/\\s+/g, ' ').trim();
166
+ const stateTexts = ['检测', '检测中', '封面检测中', '重新检测', '横/竖双封面缺失'];
167
+ const marker = Array.from(document.querySelectorAll('div,span,p,button')).find(
168
+ (el) => normalize(el.textContent) === '快速检测'
169
+ );
170
+ let root = marker?.parentElement || null;
171
+ while (root && root !== document.body) {
172
+ const descendants = Array.from(root.querySelectorAll('div,span,p,button'))
173
+ .map((el) => normalize(el.textContent));
174
+ const hasMarkerText = descendants.includes('快速检测');
175
+ const hasStateText = descendants.some((text) => stateTexts.includes(text));
176
+ if (hasMarkerText && hasStateText) {
177
+ return normalize(root.textContent).slice(0, 400);
178
+ }
179
+ root = root.parentElement;
180
+ }
181
+ return '';
182
+ }`;
183
+ }
184
+ async function getCoverCheckPanelText(page) {
185
+ return (await page.evaluate(buildCoverCheckPanelTextJs())) || '';
186
+ }
187
+ /**
188
+ * Wait for Douyin's cover-detection pipeline to expose a post-upload signal.
189
+ * In the live creator page, custom cover upload first shows `封面检测中`, then
190
+ * lands on a ready state such as `重新检测` or the warning copy for missing
191
+ * horizontal/vertical covers.
192
+ */
193
+ async function waitForCoverReady(page) {
194
+ let lastPanelText = '';
195
+ let sawBusy = false;
196
+ for (let attempt = 0; attempt < COVER_READY_WAIT_ATTEMPTS; attempt += 1) {
197
+ const panelText = await getCoverCheckPanelText(page);
198
+ const busy = panelText.includes('检测中');
199
+ const ready = (panelText.includes('重新检测')
200
+ || panelText.includes('横/竖双封面缺失'));
201
+ if (busy) {
202
+ sawBusy = true;
203
+ }
204
+ if (sawBusy && ready && !busy) {
205
+ return;
206
+ }
207
+ lastPanelText = panelText;
208
+ await page.wait({ time: 0.5 });
209
+ }
210
+ throw new CommandExecutionError('等待抖音封面处理完成超时', lastPanelText || 'unknown');
211
+ }
212
+ /**
213
+ * Click the draft button on the composer page and extract the current creation id.
214
+ */
215
+ async function clickSaveDraft(page) {
216
+ const result = (await page.evaluate(`() => {
217
+ const extractCreationId = () => {
218
+ const titleInput = Array.from(document.querySelectorAll('input')).find(
219
+ (el) => (el.placeholder || '').includes('填写作品标题')
220
+ );
221
+ if (!(titleInput instanceof HTMLInputElement)) return '';
222
+
223
+ const fiberKey = Object.keys(titleInput).find((key) => key.startsWith('__reactFiber$'));
224
+ let fiber = fiberKey ? titleInput[fiberKey] : null;
225
+ while (fiber) {
226
+ const props = fiber.memoizedProps;
227
+ if (typeof props?.creation_id === 'string' && props.creation_id) {
228
+ return props.creation_id;
229
+ }
230
+ fiber = fiber.return;
231
+ }
232
+ return '';
233
+ };
234
+
235
+ const btn = Array.from(document.querySelectorAll('button')).find(
236
+ (el) => (el.textContent || '').includes('暂存离开')
237
+ );
238
+ if (!(btn instanceof HTMLButtonElement)) {
239
+ return { ok: false, reason: 'draft-button-missing' };
240
+ }
241
+ const creationId = extractCreationId();
242
+ const propKey = Object.keys(btn).find((key) => key.startsWith('__reactProps$'));
243
+ const props = propKey ? btn[propKey] : null;
244
+ if (props?.onClick) {
245
+ props.onClick({
246
+ preventDefault() {},
247
+ stopPropagation() {},
248
+ nativeEvent: null,
249
+ target: btn,
250
+ currentTarget: btn,
251
+ });
252
+ } else {
253
+ btn.click();
254
+ }
255
+ return {
256
+ ok: true,
257
+ text: (btn.textContent || '').trim(),
258
+ creationId,
259
+ };
260
+ }`));
261
+ if (!result?.ok) {
262
+ throw new CommandExecutionError(`点击草稿按钮失败: ${result?.reason || 'unknown'}`);
263
+ }
264
+ if (!result.creationId) {
265
+ throw new CommandExecutionError('点击草稿按钮失败: creation-id-missing');
266
+ }
267
+ return {
268
+ text: result.text || '暂存离开',
269
+ creationId: result.creationId,
270
+ };
271
+ }
272
+ /**
273
+ * Wait until creator center shows the resumable-draft prompt after saving.
274
+ */
275
+ async function waitForDraftResult(page, creationId) {
276
+ let lastState = { href: '', bodyText: '' };
277
+ for (let attempt = 0; attempt < 20; attempt += 1) {
278
+ lastState = (await page.evaluate(`() => ({
279
+ href: location.href,
280
+ bodyText: document.body?.innerText || ''
281
+ })`));
282
+ if (lastState.href.includes('/creator-micro/content/upload')
283
+ && /继续编辑/.test(lastState.bodyText)) {
284
+ return creationId;
285
+ }
286
+ await page.wait({ time: 1 });
287
+ }
288
+ throw new CommandExecutionError('未检测到抖音草稿恢复提示', `当前页面: ${lastState.href || 'unknown'}`);
289
+ }
56
290
  cli({
57
291
  site: 'douyin',
58
292
  name: 'draft',
59
293
  description: '上传视频并保存为草稿',
60
294
  domain: 'creator.douyin.com',
61
295
  strategy: Strategy.COOKIE,
296
+ navigateBefore: false,
62
297
  args: [
63
298
  { name: 'video', required: true, positional: true, help: '视频文件路径' },
64
299
  { name: 'title', required: true, help: '视频标题(≤30字)' },
@@ -66,9 +301,8 @@ cli({
66
301
  { name: 'cover', default: '', help: '封面图片路径' },
67
302
  { name: 'visibility', default: 'public', choices: ['public', 'friends', 'private'] },
68
303
  ],
69
- columns: ['status', 'aweme_id'],
304
+ columns: ['status', 'draft_id'],
70
305
  func: async (page, kwargs) => {
71
- // ── Fail-fast validation ────────────────────────────────────────────
72
306
  const videoPath = path.resolve(kwargs.video);
73
307
  if (!fs.existsSync(videoPath)) {
74
308
  throw new ArgumentError(`视频文件不存在: ${videoPath}`);
@@ -77,7 +311,6 @@ cli({
77
311
  if (!['.mp4', '.mov', '.avi', '.webm'].includes(ext)) {
78
312
  throw new ArgumentError(`不支持的视频格式: ${ext}(支持 mp4/mov/avi/webm)`);
79
313
  }
80
- const fileSize = fs.statSync(videoPath).size;
81
314
  const title = kwargs.title;
82
315
  if (title.length > 30) {
83
316
  throw new ArgumentError('标题不能超过 30 字');
@@ -86,151 +319,35 @@ cli({
86
319
  if (caption.length > 1000) {
87
320
  throw new ArgumentError('正文不能超过 1000 字');
88
321
  }
89
- const visibilityType = VISIBILITY_MAP[kwargs.visibility] ?? 0;
90
322
  const coverPath = kwargs.cover;
91
323
  if (coverPath) {
92
324
  if (!fs.existsSync(path.resolve(coverPath))) {
93
325
  throw new ArgumentError(`封面文件不存在: ${path.resolve(coverPath)}`);
94
326
  }
95
327
  }
96
- // ── Phase 1: STS2 credentials ───────────────────────────────────────
97
- const credentials = await getSts2Credentials(page);
98
- // ── Phase 2: Apply TOS upload URL ───────────────────────────────────
99
- const vodUrl = `https://vod.bytedanceapi.com/?Action=ApplyVideoUpload&ServiceId=1128&Version=2021-01-01&FileType=video&FileSize=${fileSize}`;
100
- const vodJs = `fetch(${JSON.stringify(vodUrl)}, { credentials: 'include' }).then(r => r.json())`;
101
- const vodRes = (await page.evaluate(vodJs));
102
- const { VideoId: videoId, UploadHosts, StoreInfos } = vodRes.Result.UploadAddress;
103
- const tosUrl = `https://${UploadHosts[0]}/${StoreInfos[0].StoreUri}`;
104
- const tosUploadInfo = {
105
- tos_upload_url: tosUrl,
106
- auth: StoreInfos[0].Auth,
107
- video_id: videoId,
108
- };
109
- // ── Phase 3: TOS upload ─────────────────────────────────────────────
110
- await tosUpload({
111
- filePath: videoPath,
112
- uploadInfo: tosUploadInfo,
113
- credentials,
114
- onProgress: (uploaded, total) => {
115
- const pct = Math.round((uploaded / total) * 100);
116
- process.stderr.write(`\r 上传进度: ${pct}%`);
117
- },
118
- });
119
- process.stderr.write('\n');
120
- // ── Phase 4: Cover upload (optional) ────────────────────────────────
121
- let coverUri = '';
122
- let coverWidth = 720;
123
- let coverHeight = 1280;
124
- if (kwargs.cover) {
125
- const resolvedCoverPath = path.resolve(kwargs.cover);
126
- // 4A: Apply ImageX upload
127
- const applyUrl = `${IMAGEX_BASE}/?Action=ApplyImageUpload&ServiceId=${IMAGEX_SERVICE_ID}&Version=2018-08-01&UploadNum=1`;
128
- const applyJs = `fetch(${JSON.stringify(applyUrl)}, { credentials: 'include' }).then(r => r.json())`;
129
- const applyRes = (await page.evaluate(applyJs));
130
- const { StoreInfos: imgStoreInfos } = applyRes.Result.UploadAddress;
131
- const imgUploadUrl = `https://${imgStoreInfos[0].UploadHost}/${imgStoreInfos[0].StoreUri}`;
132
- // 4B: Upload image
133
- const coverStoreUri = await imagexUpload(resolvedCoverPath, {
134
- upload_url: imgUploadUrl,
135
- store_uri: imgStoreInfos[0].StoreUri,
136
- });
137
- // 4C: Commit ImageX upload
138
- const commitUrl = `${IMAGEX_BASE}/?Action=CommitImageUpload&ServiceId=${IMAGEX_SERVICE_ID}&Version=2018-08-01`;
139
- const commitBody = JSON.stringify({ SuccessObjKeys: [coverStoreUri] });
140
- const commitJs = `
141
- fetch(${JSON.stringify(commitUrl)}, {
142
- method: 'POST',
143
- credentials: 'include',
144
- headers: { 'Content-Type': 'application/json' },
145
- body: ${JSON.stringify(commitBody)}
146
- }).then(r => r.json())
147
- `;
148
- await page.evaluate(commitJs);
149
- coverUri = coverStoreUri;
150
- }
151
- // ── Phase 5: Enable video ───────────────────────────────────────────
152
- const enableUrl = `https://creator.douyin.com/web/api/media/video/enable/?video_id=${videoId}&aid=1128`;
153
- await browserFetch(page, 'GET', enableUrl);
154
- // ── Phase 6: Poll transcode ─────────────────────────────────────────
155
- const transResult = await pollTranscode(page, videoId);
156
- coverWidth = transResult.width;
157
- coverHeight = transResult.height;
158
- if (!coverUri) {
159
- coverUri = transResult.poster_uri;
328
+ if (!page.setFileInput) {
329
+ throw new CommandExecutionError('当前浏览器适配器不支持文件注入', '请使用 Browser Bridge 或支持 setFileInput 的浏览器模式');
160
330
  }
161
- // ── Phase 7: SKIP (no safety check for drafts) ──────────────────────
162
- // ── Phase 8: create_v2 with is_draft: 1 ────────────────────────────
163
- const hashtagNames = extractHashtagNames(caption);
164
- const hashtags = [];
165
- let searchFrom = 0;
166
- for (const name of hashtagNames) {
167
- const idx = caption.indexOf(`#${name}`, searchFrom);
168
- if (idx === -1)
169
- continue;
170
- hashtags.push({ name, id: 0, start: idx, end: idx + name.length + 1 });
171
- searchFrom = idx + name.length + 1;
172
- }
173
- const textExtraArr = parseTextExtra(caption, hashtags);
174
- const publishBody = {
175
- item: {
176
- common: {
177
- text: caption,
178
- caption: '',
179
- item_title: title,
180
- activity: '[]',
181
- text_extra: JSON.stringify(textExtraArr),
182
- challenges: '[]',
183
- mentions: '[]',
184
- hashtag_source: '',
185
- hot_sentence: '',
186
- interaction_stickers: '[]',
187
- visibility_type: visibilityType,
188
- download: 0,
189
- is_draft: 1,
190
- creation_id: generateCreationId(),
191
- media_type: 4,
192
- video_id: videoId,
193
- music_source: 0,
194
- music_id: null,
195
- },
196
- cover: {
197
- poster: coverUri,
198
- custom_cover_image_height: coverHeight,
199
- custom_cover_image_width: coverWidth,
200
- poster_delay: 0,
201
- cover_tools_info: DEFAULT_COVER_TOOLS_INFO,
202
- cover_tools_extend_info: '{}',
203
- },
204
- mix: {},
205
- chapter: {
206
- chapter: JSON.stringify({
207
- chapter_abstract: '',
208
- chapter_details: [],
209
- chapter_type: 0,
210
- }),
211
- },
212
- anchor: {},
213
- sync: {
214
- should_sync: false,
215
- sync_to_toutiao: 0,
216
- },
217
- open_platform: {},
218
- assistant: { is_preview: 0, is_post_assistant: 1 },
219
- declare: { user_declare_info: '{}' },
220
- },
221
- };
222
- const publishUrl = `https://creator.douyin.com/web/api/media/aweme/create_v2/?read_aid=2906&${DEVICE_PARAMS}`;
223
- const publishRes = (await browserFetch(page, 'POST', publishUrl, {
224
- body: publishBody,
225
- }));
226
- const awemeId = publishRes.aweme_id;
227
- if (!awemeId) {
228
- throw new CommandExecutionError(`草稿保存成功但未返回 aweme_id: ${JSON.stringify(publishRes)}`);
331
+ const visibilityLabel = VISIBILITY_LABELS[kwargs.visibility] ?? VISIBILITY_LABELS.public;
332
+ await page.goto(DRAFT_UPLOAD_URL);
333
+ await page.wait({ selector: 'input[type="file"]', timeout: 20 });
334
+ await dismissKnownModals(page);
335
+ await page.setFileInput([videoPath], 'input[type="file"]');
336
+ await waitForDraftComposer(page);
337
+ await dismissKnownModals(page);
338
+ if (coverPath) {
339
+ const coverSelector = await prepareCustomCoverInput(page);
340
+ await page.setFileInput([path.resolve(coverPath)], coverSelector);
341
+ await waitForCoverReady(page);
229
342
  }
343
+ await fillDraftComposer(page, { title, caption, visibilityLabel });
344
+ await page.wait({ time: 1 });
345
+ const saveResult = await clickSaveDraft(page);
346
+ const draftId = await waitForDraftResult(page, saveResult.creationId);
230
347
  return [
231
348
  {
232
- status: '✅ 草稿保存成功!',
233
- aweme_id: awemeId,
349
+ status: '✅ 草稿已保存,可在创作中心继续编辑',
350
+ draft_id: draftId,
234
351
  },
235
352
  ];
236
353
  },
@@ -1 +1 @@
1
- import './draft.js';
1
+ export {};