@jackwener/opencli 1.5.4 → 1.5.6

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 (256) hide show
  1. package/README.md +27 -2
  2. package/README.zh-CN.md +36 -4
  3. package/dist/browser/daemon-client.d.ts +5 -1
  4. package/dist/browser/page.d.ts +6 -0
  5. package/dist/browser/page.js +15 -0
  6. package/dist/cli-manifest.json +1284 -67
  7. package/dist/cli.js +14 -14
  8. package/dist/clis/antigravity/serve.js +2 -2
  9. package/dist/clis/band/bands.d.ts +1 -0
  10. package/dist/clis/band/bands.js +72 -0
  11. package/dist/clis/band/mentions.d.ts +1 -0
  12. package/dist/clis/band/mentions.js +127 -0
  13. package/dist/clis/band/post.d.ts +1 -0
  14. package/dist/clis/band/post.js +175 -0
  15. package/dist/clis/band/posts.d.ts +1 -0
  16. package/dist/clis/band/posts.js +94 -0
  17. package/dist/clis/doubao/detail.d.ts +1 -0
  18. package/dist/clis/doubao/detail.js +33 -0
  19. package/dist/clis/doubao/detail.test.d.ts +1 -0
  20. package/dist/clis/doubao/detail.test.js +42 -0
  21. package/dist/clis/doubao/history.d.ts +1 -0
  22. package/dist/clis/doubao/history.js +28 -0
  23. package/dist/clis/doubao/history.test.d.ts +1 -0
  24. package/dist/clis/doubao/history.test.js +37 -0
  25. package/dist/clis/doubao/meeting-summary.d.ts +1 -0
  26. package/dist/clis/doubao/meeting-summary.js +39 -0
  27. package/dist/clis/doubao/meeting-transcript.d.ts +1 -0
  28. package/dist/clis/doubao/meeting-transcript.js +36 -0
  29. package/dist/clis/doubao/utils.d.ts +27 -0
  30. package/dist/clis/doubao/utils.js +317 -0
  31. package/dist/clis/doubao/utils.test.d.ts +1 -0
  32. package/dist/clis/doubao/utils.test.js +24 -0
  33. package/dist/clis/douyin/_shared/public-api.d.ts +33 -0
  34. package/dist/clis/douyin/_shared/public-api.js +29 -0
  35. package/dist/clis/douyin/user-videos.d.ts +5 -0
  36. package/dist/clis/douyin/user-videos.js +74 -0
  37. package/dist/clis/douyin/user-videos.test.d.ts +1 -0
  38. package/dist/clis/douyin/user-videos.test.js +108 -0
  39. package/dist/clis/ones/common.d.ts +32 -0
  40. package/dist/clis/ones/common.js +144 -0
  41. package/dist/clis/ones/enrich-tasks.d.ts +5 -0
  42. package/dist/clis/ones/enrich-tasks.js +37 -0
  43. package/dist/clis/ones/login.d.ts +1 -0
  44. package/dist/clis/ones/login.js +80 -0
  45. package/dist/clis/ones/logout.d.ts +1 -0
  46. package/dist/clis/ones/logout.js +17 -0
  47. package/dist/clis/ones/me.d.ts +1 -0
  48. package/dist/clis/ones/me.js +30 -0
  49. package/dist/clis/ones/my-tasks.d.ts +1 -0
  50. package/dist/clis/ones/my-tasks.js +120 -0
  51. package/dist/clis/ones/resolve-labels.d.ts +10 -0
  52. package/dist/clis/ones/resolve-labels.js +64 -0
  53. package/dist/clis/ones/task-helpers.d.ts +29 -0
  54. package/dist/clis/ones/task-helpers.js +212 -0
  55. package/dist/clis/ones/task-helpers.test.d.ts +1 -0
  56. package/dist/clis/ones/task-helpers.test.js +12 -0
  57. package/dist/clis/ones/task.d.ts +1 -0
  58. package/dist/clis/ones/task.js +66 -0
  59. package/dist/clis/ones/tasks.d.ts +1 -0
  60. package/dist/clis/ones/tasks.js +79 -0
  61. package/dist/clis/ones/token-info.d.ts +1 -0
  62. package/dist/clis/ones/token-info.js +42 -0
  63. package/dist/clis/ones/worklog.d.ts +11 -0
  64. package/dist/clis/ones/worklog.js +267 -0
  65. package/dist/clis/ones/worklog.test.d.ts +1 -0
  66. package/dist/clis/ones/worklog.test.js +20 -0
  67. package/dist/clis/sinafinance/rolling-news.d.ts +4 -0
  68. package/dist/clis/sinafinance/rolling-news.js +40 -0
  69. package/dist/clis/sinafinance/stock.d.ts +8 -0
  70. package/dist/clis/sinafinance/stock.js +117 -0
  71. package/dist/clis/spotify/spotify.d.ts +1 -0
  72. package/dist/clis/spotify/spotify.js +316 -0
  73. package/dist/clis/spotify/utils.d.ts +21 -0
  74. package/dist/clis/spotify/utils.js +66 -0
  75. package/dist/clis/spotify/utils.test.d.ts +1 -0
  76. package/dist/clis/spotify/utils.test.js +67 -0
  77. package/dist/clis/tieba/commands.test.d.ts +4 -0
  78. package/dist/clis/tieba/commands.test.js +79 -0
  79. package/dist/clis/tieba/hot.d.ts +1 -0
  80. package/dist/clis/tieba/hot.js +48 -0
  81. package/dist/clis/tieba/posts.d.ts +1 -0
  82. package/dist/clis/tieba/posts.js +85 -0
  83. package/dist/clis/tieba/read.d.ts +1 -0
  84. package/dist/clis/tieba/read.js +140 -0
  85. package/dist/clis/tieba/search.d.ts +1 -0
  86. package/dist/clis/tieba/search.js +108 -0
  87. package/dist/clis/tieba/utils.d.ts +101 -0
  88. package/dist/clis/tieba/utils.js +240 -0
  89. package/dist/clis/tieba/utils.test.d.ts +1 -0
  90. package/dist/clis/tieba/utils.test.js +290 -0
  91. package/dist/clis/weread/book.js +100 -13
  92. package/dist/clis/weread/commands.test.js +221 -0
  93. package/dist/clis/weread/private-api-regression.test.d.ts +1 -0
  94. package/dist/{weread-private-api-regression.test.js → clis/weread/private-api-regression.test.js} +92 -30
  95. package/dist/clis/weread/search-regression.test.d.ts +1 -0
  96. package/dist/clis/weread/search-regression.test.js +407 -0
  97. package/dist/clis/weread/search.js +143 -7
  98. package/dist/clis/weread/shelf.js +13 -95
  99. package/dist/clis/weread/utils.d.ts +46 -0
  100. package/dist/clis/weread/utils.js +214 -7
  101. package/dist/clis/weread/utils.test.js +71 -1
  102. package/dist/clis/xiaohongshu/publish.d.ts +1 -1
  103. package/dist/clis/xiaohongshu/publish.js +78 -31
  104. package/dist/clis/xiaohongshu/publish.test.js +66 -1
  105. package/dist/clis/xiaohongshu/user-helpers.d.ts +1 -0
  106. package/dist/clis/xiaohongshu/user-helpers.js +2 -0
  107. package/dist/clis/xiaohongshu/user-helpers.test.js +18 -0
  108. package/dist/clis/xueqiu/comments.d.ts +118 -0
  109. package/dist/clis/xueqiu/comments.js +354 -0
  110. package/dist/clis/xueqiu/comments.test.d.ts +1 -0
  111. package/dist/clis/xueqiu/comments.test.js +696 -0
  112. package/dist/clis/youtube/transcript.js +2 -4
  113. package/dist/clis/youtube/utils.d.ts +9 -0
  114. package/dist/clis/youtube/utils.js +67 -3
  115. package/dist/clis/youtube/utils.test.d.ts +1 -0
  116. package/dist/clis/youtube/utils.test.js +37 -0
  117. package/dist/clis/youtube/video.js +16 -15
  118. package/dist/clis/zsxq/dynamics.d.ts +1 -0
  119. package/dist/clis/zsxq/dynamics.js +47 -0
  120. package/dist/clis/zsxq/groups.d.ts +1 -0
  121. package/dist/clis/zsxq/groups.js +32 -0
  122. package/dist/clis/zsxq/search.d.ts +1 -0
  123. package/dist/clis/zsxq/search.js +43 -0
  124. package/dist/clis/zsxq/search.test.d.ts +1 -0
  125. package/dist/clis/zsxq/search.test.js +24 -0
  126. package/dist/clis/zsxq/topic.d.ts +1 -0
  127. package/dist/clis/zsxq/topic.js +47 -0
  128. package/dist/clis/zsxq/topic.test.d.ts +1 -0
  129. package/dist/clis/zsxq/topic.test.js +29 -0
  130. package/dist/clis/zsxq/topics.d.ts +1 -0
  131. package/dist/clis/zsxq/topics.js +25 -0
  132. package/dist/clis/zsxq/topics.test.d.ts +1 -0
  133. package/dist/clis/zsxq/topics.test.js +24 -0
  134. package/dist/clis/zsxq/utils.d.ts +97 -0
  135. package/dist/clis/zsxq/utils.js +230 -0
  136. package/dist/commanderAdapter.js +27 -4
  137. package/dist/commanderAdapter.test.js +39 -0
  138. package/dist/daemon.js +5 -4
  139. package/dist/errors.d.ts +29 -1
  140. package/dist/errors.js +49 -11
  141. package/dist/external-clis.yaml +17 -0
  142. package/dist/external.js +3 -3
  143. package/dist/main.js +2 -1
  144. package/dist/tui.js +2 -1
  145. package/dist/types.d.ts +5 -0
  146. package/docs/.vitepress/config.mts +3 -0
  147. package/docs/adapters/browser/band.md +63 -0
  148. package/docs/adapters/browser/ones.md +59 -0
  149. package/docs/adapters/browser/sinafinance.md +56 -6
  150. package/docs/adapters/browser/spotify.md +62 -0
  151. package/docs/adapters/browser/tieba.md +45 -0
  152. package/docs/adapters/browser/xueqiu.md +5 -0
  153. package/docs/adapters/browser/zsxq.md +49 -0
  154. package/docs/adapters/index.md +5 -2
  155. package/docs/adapters-doc/ones.md +32 -0
  156. package/extension/dist/background.js +1 -2
  157. package/extension/manifest.json +1 -1
  158. package/extension/package.json +1 -1
  159. package/extension/src/background.ts +17 -1
  160. package/extension/src/cdp.ts +42 -0
  161. package/extension/src/protocol.ts +5 -1
  162. package/package.json +1 -1
  163. package/scripts/postinstall.js +16 -0
  164. package/src/browser/daemon-client.ts +5 -1
  165. package/src/browser/page.ts +16 -0
  166. package/src/cli.ts +14 -14
  167. package/src/clis/antigravity/serve.ts +2 -2
  168. package/src/clis/band/bands.ts +76 -0
  169. package/src/clis/band/mentions.ts +134 -0
  170. package/src/clis/band/post.ts +187 -0
  171. package/src/clis/band/posts.ts +106 -0
  172. package/src/clis/doubao/detail.test.ts +53 -0
  173. package/src/clis/doubao/detail.ts +41 -0
  174. package/src/clis/doubao/history.test.ts +45 -0
  175. package/src/clis/doubao/history.ts +32 -0
  176. package/src/clis/doubao/meeting-summary.ts +53 -0
  177. package/src/clis/doubao/meeting-transcript.ts +48 -0
  178. package/src/clis/doubao/utils.test.ts +45 -0
  179. package/src/clis/doubao/utils.ts +371 -0
  180. package/src/clis/douyin/_shared/public-api.ts +84 -0
  181. package/src/clis/douyin/user-videos.test.ts +122 -0
  182. package/src/clis/douyin/user-videos.ts +101 -0
  183. package/src/clis/ones/common.ts +187 -0
  184. package/src/clis/ones/enrich-tasks.ts +47 -0
  185. package/src/clis/ones/login.ts +103 -0
  186. package/src/clis/ones/logout.ts +19 -0
  187. package/src/clis/ones/me.ts +34 -0
  188. package/src/clis/ones/my-tasks.ts +148 -0
  189. package/src/clis/ones/resolve-labels.ts +80 -0
  190. package/src/clis/ones/task-helpers.test.ts +14 -0
  191. package/src/clis/ones/task-helpers.ts +214 -0
  192. package/src/clis/ones/task.ts +79 -0
  193. package/src/clis/ones/tasks.ts +92 -0
  194. package/src/clis/ones/token-info.ts +46 -0
  195. package/src/clis/ones/worklog.test.ts +24 -0
  196. package/src/clis/ones/worklog.ts +306 -0
  197. package/src/clis/sinafinance/rolling-news.ts +42 -0
  198. package/src/clis/sinafinance/stock.ts +127 -0
  199. package/src/clis/spotify/spotify.ts +328 -0
  200. package/src/clis/spotify/utils.test.ts +87 -0
  201. package/src/clis/spotify/utils.ts +92 -0
  202. package/src/clis/tieba/commands.test.ts +86 -0
  203. package/src/clis/tieba/hot.ts +52 -0
  204. package/src/clis/tieba/posts.ts +108 -0
  205. package/src/clis/tieba/read.ts +158 -0
  206. package/src/clis/tieba/search.ts +119 -0
  207. package/src/clis/tieba/utils.test.ts +322 -0
  208. package/src/clis/tieba/utils.ts +348 -0
  209. package/src/clis/weread/book.ts +116 -13
  210. package/src/clis/weread/commands.test.ts +249 -0
  211. package/src/{weread-private-api-regression.test.ts → clis/weread/private-api-regression.test.ts} +108 -30
  212. package/src/clis/weread/search-regression.test.ts +440 -0
  213. package/src/clis/weread/search.ts +189 -9
  214. package/src/clis/weread/shelf.ts +20 -122
  215. package/src/clis/weread/utils.test.ts +81 -1
  216. package/src/clis/weread/utils.ts +264 -7
  217. package/src/clis/xiaohongshu/publish.test.ts +79 -1
  218. package/src/clis/xiaohongshu/publish.ts +84 -30
  219. package/src/clis/xiaohongshu/user-helpers.test.ts +23 -0
  220. package/src/clis/xiaohongshu/user-helpers.ts +4 -0
  221. package/src/clis/xueqiu/comments.test.ts +823 -0
  222. package/src/clis/xueqiu/comments.ts +461 -0
  223. package/src/clis/youtube/transcript.ts +2 -4
  224. package/src/clis/youtube/utils.test.ts +43 -0
  225. package/src/clis/youtube/utils.ts +69 -0
  226. package/src/clis/youtube/video.ts +16 -15
  227. package/src/clis/zsxq/dynamics.ts +60 -0
  228. package/src/clis/zsxq/groups.ts +41 -0
  229. package/src/clis/zsxq/search.test.ts +29 -0
  230. package/src/clis/zsxq/search.ts +54 -0
  231. package/src/clis/zsxq/topic.test.ts +34 -0
  232. package/src/clis/zsxq/topic.ts +68 -0
  233. package/src/clis/zsxq/topics.test.ts +29 -0
  234. package/src/clis/zsxq/topics.ts +36 -0
  235. package/src/clis/zsxq/utils.ts +351 -0
  236. package/src/commanderAdapter.test.ts +47 -0
  237. package/src/commanderAdapter.ts +26 -3
  238. package/src/daemon.ts +5 -4
  239. package/src/errors.ts +71 -10
  240. package/src/external-clis.yaml +17 -0
  241. package/src/external.ts +3 -3
  242. package/src/main.ts +2 -1
  243. package/src/tui.ts +2 -1
  244. package/src/types.ts +5 -0
  245. package/tests/e2e/band-auth.test.ts +20 -0
  246. package/tests/e2e/browser-auth-helpers.ts +18 -0
  247. package/tests/e2e/browser-auth.test.ts +35 -47
  248. package/tests/e2e/browser-public.test.ts +288 -0
  249. package/tests/e2e/management.test.ts +1 -1
  250. package/tests/e2e/plugin-management.test.ts +1 -1
  251. package/vitest.config.ts +1 -0
  252. package/SKILL.md +0 -879
  253. package/dist/weread-private-api-regression.test.d.ts +0 -1
  254. package/dist/weread-search-regression.test.d.ts +0 -1
  255. package/dist/weread-search-regression.test.js +0 -39
  256. package/src/weread-search-regression.test.ts +0 -44
@@ -4,6 +4,12 @@ export const DOUBAO_DOMAIN = 'www.doubao.com';
4
4
  export const DOUBAO_CHAT_URL = 'https://www.doubao.com/chat';
5
5
  export const DOUBAO_NEW_CHAT_URL = 'https://www.doubao.com/chat/new-thread/create-by-msg';
6
6
 
7
+ export interface DoubaoConversation {
8
+ Id: string;
9
+ Title: string;
10
+ Url: string;
11
+ }
12
+
7
13
  export interface DoubaoTurn {
8
14
  Role: 'User' | 'Assistant' | 'System';
9
15
  Text: string;
@@ -605,6 +611,371 @@ export async function waitForDoubaoResponse(
605
611
  return lastCandidate;
606
612
  }
607
613
 
614
+ function getConversationListScript(): string {
615
+ return `
616
+ (() => {
617
+ const sidebar = document.querySelector('[data-testid="flow_chat_sidebar"]');
618
+ if (!sidebar) return [];
619
+
620
+ const items = Array.from(
621
+ sidebar.querySelectorAll('a[data-testid="chat_list_thread_item"]')
622
+ );
623
+
624
+ return items
625
+ .map(a => {
626
+ const href = a.getAttribute('href') || '';
627
+ const match = href.match(/\\/chat\\/(\\d{10,})/);
628
+ if (!match) return null;
629
+ const id = match[1];
630
+ const textContent = (a.textContent || a.innerText || '').trim();
631
+ const title = textContent
632
+ .replace(/\\s+/g, ' ')
633
+ .substring(0, 200);
634
+ return { id, title, href };
635
+ })
636
+ .filter(Boolean);
637
+ })()
638
+ `;
639
+ }
640
+
641
+ export async function getDoubaoConversationList(page: IPage): Promise<DoubaoConversation[]> {
642
+ await ensureDoubaoChatPage(page);
643
+ const raw = await page.evaluate(getConversationListScript()) as
644
+ Array<{ id: string; title: string; href: string }>;
645
+
646
+ if (!Array.isArray(raw)) return [];
647
+
648
+ return raw.map((item) => ({
649
+ Id: item.id,
650
+ Title: item.title,
651
+ Url: `${DOUBAO_CHAT_URL}/${item.id}`,
652
+ }));
653
+ }
654
+
655
+ // ---------------------------------------------------------------------------
656
+ // Conversation detail helpers
657
+ // ---------------------------------------------------------------------------
658
+
659
+ export interface DoubaoMessage {
660
+ Role: 'User' | 'Assistant' | 'System';
661
+ Text: string;
662
+ HasMeetingCard: boolean;
663
+ }
664
+
665
+ export interface DoubaoMeetingInfo {
666
+ title: string;
667
+ time: string;
668
+ }
669
+
670
+ export function parseDoubaoConversationId(input: string): string {
671
+ const match = input.match(/(\d{10,})/);
672
+ return match ? match[1] : input;
673
+ }
674
+
675
+ function getConversationDetailScript(): string {
676
+ return `
677
+ (() => {
678
+ const clean = (v) => (v || '').replace(/\\u00a0/g, ' ').replace(/\\n{3,}/g, '\\n\\n').trim();
679
+
680
+ const messageList = document.querySelector('[data-testid="message-list"]');
681
+ if (!messageList) return { messages: [], meeting: null };
682
+
683
+ const meetingCard = messageList.querySelector('[data-testid="meeting-minutes-card"]');
684
+ let meeting = null;
685
+ if (meetingCard) {
686
+ const raw = clean(meetingCard.textContent || '');
687
+ const match = raw.match(/^(.+?)(?:会议时间:|\\s*$)(.*)/);
688
+ meeting = {
689
+ title: match ? match[1].trim() : raw,
690
+ time: match && match[2] ? match[2].trim() : '',
691
+ };
692
+ }
693
+
694
+ const unions = Array.from(messageList.querySelectorAll('[data-testid="union_message"]'));
695
+ const messages = unions.map(u => {
696
+ const isSend = !!u.querySelector('[data-testid="send_message"]');
697
+ const isReceive = !!u.querySelector('[data-testid="receive_message"]');
698
+ const textEl = u.querySelector('[data-testid="message_text_content"]');
699
+ const text = textEl ? clean(textEl.innerText || textEl.textContent || '') : '';
700
+ return {
701
+ role: isSend ? 'User' : isReceive ? 'Assistant' : 'System',
702
+ text,
703
+ hasMeetingCard: !!u.querySelector('[data-testid="meeting-minutes-card"]'),
704
+ };
705
+ }).filter(m => m.text);
706
+
707
+ return { messages, meeting };
708
+ })()
709
+ `;
710
+ }
711
+
712
+ export async function navigateToConversation(page: IPage, conversationId: string): Promise<void> {
713
+ const url = `${DOUBAO_CHAT_URL}/${conversationId}`;
714
+ const currentUrl = await page.evaluate('window.location.href').catch(() => '');
715
+ if (typeof currentUrl === 'string' && currentUrl.includes(`/chat/${conversationId}`)) {
716
+ await page.wait(1);
717
+ return;
718
+ }
719
+ await page.goto(url, { waitUntil: 'load', settleMs: 3000 });
720
+ await page.wait(2);
721
+ }
722
+
723
+ export async function getConversationDetail(
724
+ page: IPage,
725
+ conversationId: string,
726
+ ): Promise<{ messages: DoubaoMessage[]; meeting: DoubaoMeetingInfo | null }> {
727
+ await navigateToConversation(page, conversationId);
728
+ const raw = await page.evaluate(getConversationDetailScript()) as {
729
+ messages: Array<{ role: string; text: string; hasMeetingCard: boolean }>;
730
+ meeting: { title: string; time: string } | null;
731
+ };
732
+
733
+ const messages: DoubaoMessage[] = (raw.messages || []).map((m) => ({
734
+ Role: m.role as 'User' | 'Assistant' | 'System',
735
+ Text: m.text,
736
+ HasMeetingCard: m.hasMeetingCard,
737
+ }));
738
+
739
+ return { messages, meeting: raw.meeting };
740
+ }
741
+
742
+ // ---------------------------------------------------------------------------
743
+ // Meeting minutes panel helpers
744
+ // ---------------------------------------------------------------------------
745
+
746
+ function clickMeetingCardScript(): string {
747
+ return `
748
+ (() => {
749
+ const card = document.querySelector('[data-testid="meeting-minutes-card"]');
750
+ if (!card) return false;
751
+ card.click();
752
+ return true;
753
+ })()
754
+ `;
755
+ }
756
+
757
+ function readMeetingSummaryScript(): string {
758
+ return `
759
+ (() => {
760
+ const panel = document.querySelector('[data-testid="canvas_panel_container"]');
761
+ if (!panel) return { error: 'no panel' };
762
+
763
+ const summary = panel.querySelector('[data-testid="meeting-summary-todos"]');
764
+ const summaryText = summary
765
+ ? (summary.innerText || summary.textContent || '').trim()
766
+ : '';
767
+
768
+ return { summary: summaryText };
769
+ })()
770
+ `;
771
+ }
772
+
773
+ function clickTextNotesTabScript(): string {
774
+ return `
775
+ (() => {
776
+ const panel = document.querySelector('[data-testid="canvas_panel_container"]');
777
+ if (!panel) return false;
778
+ const tabs = panel.querySelectorAll('[role="tab"], .semi-tabs-tab');
779
+ for (const tab of tabs) {
780
+ if ((tab.textContent || '').trim().includes('文字')) {
781
+ tab.click();
782
+ return true;
783
+ }
784
+ }
785
+ return false;
786
+ })()
787
+ `;
788
+ }
789
+
790
+ function readTextNotesScript(): string {
791
+ return `
792
+ (() => {
793
+ const panel = document.querySelector('[data-testid="canvas_panel_container"]');
794
+ if (!panel) return '';
795
+ const textNotes = panel.querySelector('[data-testid="meeting-text-notes"]');
796
+ if (!textNotes) return '';
797
+ return (textNotes.innerText || textNotes.textContent || '').trim();
798
+ })()
799
+ `;
800
+ }
801
+
802
+ function normalizeTranscriptLines(text: string): string[] {
803
+ return text
804
+ .split('\n')
805
+ .map(line => line.trim())
806
+ .filter(Boolean);
807
+ }
808
+
809
+ function containsLineSequence(haystack: string[], needle: string[]): boolean {
810
+ if (needle.length === 0) return true;
811
+ if (needle.length > haystack.length) return false;
812
+
813
+ for (let start = 0; start <= haystack.length - needle.length; start += 1) {
814
+ let matched = true;
815
+ for (let offset = 0; offset < needle.length; offset += 1) {
816
+ if (haystack[start + offset] !== needle[offset]) {
817
+ matched = false;
818
+ break;
819
+ }
820
+ }
821
+ if (matched) return true;
822
+ }
823
+
824
+ return false;
825
+ }
826
+
827
+ export function mergeTranscriptSnapshots(existing: string, incoming: string): string {
828
+ const currentLines = normalizeTranscriptLines(existing);
829
+ const nextLines = normalizeTranscriptLines(incoming);
830
+
831
+ if (nextLines.length === 0) return currentLines.join('\n');
832
+ if (currentLines.length === 0) return nextLines.join('\n');
833
+ if (containsLineSequence(currentLines, nextLines)) return currentLines.join('\n');
834
+
835
+ const maxOverlap = Math.min(currentLines.length, nextLines.length);
836
+ for (let overlap = maxOverlap; overlap > 0; overlap -= 1) {
837
+ let matched = true;
838
+ for (let index = 0; index < overlap; index += 1) {
839
+ if (currentLines[currentLines.length - overlap + index] !== nextLines[index]) {
840
+ matched = false;
841
+ break;
842
+ }
843
+ }
844
+ if (matched) {
845
+ return [...currentLines, ...nextLines.slice(overlap)].join('\n');
846
+ }
847
+ }
848
+
849
+ return [...currentLines, ...nextLines].join('\n');
850
+ }
851
+
852
+ function clickChapterTabScript(): string {
853
+ return `
854
+ (() => {
855
+ const panel = document.querySelector('[data-testid="canvas_panel_container"]');
856
+ if (!panel) return false;
857
+ const tabs = panel.querySelectorAll('[role="tab"], .semi-tabs-tab');
858
+ for (const tab of tabs) {
859
+ if ((tab.textContent || '').trim().includes('章节')) {
860
+ tab.click();
861
+ return true;
862
+ }
863
+ }
864
+ return false;
865
+ })()
866
+ `;
867
+ }
868
+
869
+ function readChapterScript(): string {
870
+ return `
871
+ (() => {
872
+ const panel = document.querySelector('[data-testid="canvas_panel_container"]');
873
+ if (!panel) return '';
874
+ const chapter = panel.querySelector('[data-testid="meeting-ai-chapter"]');
875
+ if (!chapter) return '';
876
+ return (chapter.innerText || chapter.textContent || '').trim();
877
+ })()
878
+ `;
879
+ }
880
+
881
+ function triggerTranscriptDownloadScript(): string {
882
+ return `
883
+ (() => {
884
+ const panel = document.querySelector('[data-testid="canvas_panel_container"]');
885
+ if (!panel) return { error: 'no panel' };
886
+
887
+ const downloadIcon = panel.querySelector('[class*="DownloadMeetingAudio"] span[role="img"]');
888
+ if (!downloadIcon) return { error: 'no download icon' };
889
+
890
+ downloadIcon.click();
891
+ return { clicked: 'icon' };
892
+ })()
893
+ `;
894
+ }
895
+
896
+ function clickTranscriptDownloadBtnScript(): string {
897
+ return `
898
+ (() => {
899
+ const btn = document.querySelector('[data-testid="minutes-download-text-btn"]');
900
+ if (!btn) return { error: 'no download text btn' };
901
+ btn.click();
902
+ return { clicked: 'transcript' };
903
+ })()
904
+ `;
905
+ }
906
+
907
+ export async function openMeetingPanel(page: IPage, conversationId: string): Promise<boolean> {
908
+ await navigateToConversation(page, conversationId);
909
+ const clicked = await page.evaluate(clickMeetingCardScript()) as boolean;
910
+ if (!clicked) return false;
911
+ await page.wait(2);
912
+ return true;
913
+ }
914
+
915
+ export async function getMeetingSummary(page: IPage): Promise<string> {
916
+ const result = await page.evaluate(readMeetingSummaryScript()) as { summary?: string; error?: string };
917
+ return result.summary || '';
918
+ }
919
+
920
+ export async function getMeetingChapters(page: IPage): Promise<string> {
921
+ await page.evaluate(clickChapterTabScript());
922
+ await page.wait(1.5);
923
+ return await page.evaluate(readChapterScript()) as string;
924
+ }
925
+
926
+ function scrollTextNotesPanelScript(): string {
927
+ return `
928
+ (() => {
929
+ const panel = document.querySelector('[data-testid="canvas_panel_container"]');
930
+ if (!panel) return 0;
931
+ const textNotes = panel.querySelector('[data-testid="meeting-text-notes"]');
932
+ if (!textNotes) return 0;
933
+
934
+ const scrollable = textNotes.closest('[class*="overflow"]')
935
+ || textNotes.parentElement
936
+ || textNotes;
937
+ const maxScroll = scrollable.scrollHeight - scrollable.clientHeight;
938
+ if (maxScroll > 0) {
939
+ scrollable.scrollTop = scrollable.scrollHeight;
940
+ }
941
+ return maxScroll;
942
+ })()
943
+ `;
944
+ }
945
+
946
+ export async function getMeetingTranscript(page: IPage): Promise<string> {
947
+ await page.evaluate(clickTextNotesTabScript());
948
+ await page.wait(2);
949
+
950
+ let merged = '';
951
+ let stableRounds = 0;
952
+ for (let i = 0; i < 10; i++) {
953
+ await page.evaluate(scrollTextNotesPanelScript());
954
+ await page.wait(1);
955
+ const snapshot = await page.evaluate(readTextNotesScript()) as string;
956
+ const nextMerged = mergeTranscriptSnapshots(merged, snapshot);
957
+
958
+ if (nextMerged === merged && snapshot.length > 0) {
959
+ stableRounds += 1;
960
+ if (stableRounds >= 2) break;
961
+ } else {
962
+ stableRounds = 0;
963
+ merged = nextMerged;
964
+ }
965
+ }
966
+
967
+ return merged;
968
+ }
969
+
970
+ export async function triggerTranscriptDownload(page: IPage): Promise<boolean> {
971
+ const iconResult = await page.evaluate(triggerTranscriptDownloadScript()) as { clicked?: string; error?: string };
972
+ if (iconResult.error) return false;
973
+ await page.wait(1);
974
+
975
+ const btnResult = await page.evaluate(clickTranscriptDownloadBtnScript()) as { clicked?: string; error?: string };
976
+ return !btnResult.error;
977
+ }
978
+
608
979
  export async function startNewDoubaoChat(page: IPage): Promise<string> {
609
980
  await ensureDoubaoChatPage(page);
610
981
  const clickedLabel = await page.evaluate(clickNewChatScript()) as string;
@@ -0,0 +1,84 @@
1
+ import type { IPage } from '../../../types.js';
2
+ import { browserFetch } from './browser-fetch.js';
3
+
4
+ export interface DouyinComment {
5
+ text?: string;
6
+ digg_count?: number;
7
+ user?: {
8
+ nickname?: string;
9
+ };
10
+ }
11
+
12
+ export interface DouyinVideo {
13
+ aweme_id: string;
14
+ desc?: string;
15
+ video?: {
16
+ duration?: number;
17
+ play_addr?: {
18
+ url_list?: string[];
19
+ };
20
+ };
21
+ statistics?: {
22
+ digg_count?: number;
23
+ };
24
+ }
25
+
26
+ export interface DouyinVideoListResponse {
27
+ aweme_list?: DouyinVideo[];
28
+ }
29
+
30
+ export interface DouyinCommentListResponse {
31
+ comments?: DouyinComment[];
32
+ }
33
+
34
+ export async function fetchDouyinUserVideos(
35
+ page: IPage,
36
+ secUid: string,
37
+ count: number,
38
+ ): Promise<DouyinVideo[]> {
39
+ const params = new URLSearchParams({
40
+ sec_user_id: secUid,
41
+ max_cursor: '0',
42
+ count: String(count),
43
+ aid: '6383',
44
+ });
45
+
46
+ const data = await browserFetch(
47
+ page,
48
+ 'GET',
49
+ `https://www.douyin.com/aweme/v1/web/aweme/post/?${params.toString()}`,
50
+ {
51
+ headers: { referer: 'https://www.douyin.com/' },
52
+ },
53
+ ) as DouyinVideoListResponse;
54
+
55
+ return data.aweme_list || [];
56
+ }
57
+
58
+ export async function fetchDouyinComments(
59
+ page: IPage,
60
+ awemeId: string,
61
+ count: number,
62
+ ): Promise<Array<{ text: string; digg_count: number; nickname: string }>> {
63
+ const params = new URLSearchParams({
64
+ aweme_id: awemeId,
65
+ count: String(count),
66
+ cursor: '0',
67
+ aid: '6383',
68
+ });
69
+
70
+ const data = await browserFetch(
71
+ page,
72
+ 'GET',
73
+ `https://www.douyin.com/aweme/v1/web/comment/list/?${params.toString()}`,
74
+ {
75
+ headers: { referer: 'https://www.douyin.com/' },
76
+ },
77
+ ) as DouyinCommentListResponse;
78
+
79
+ return (data.comments || []).slice(0, count).map((comment) => ({
80
+ text: comment.text || '',
81
+ digg_count: comment.digg_count ?? 0,
82
+ nickname: comment.user?.nickname || '',
83
+ }));
84
+ }
@@ -0,0 +1,122 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ const { fetchDouyinUserVideosMock, fetchDouyinCommentsMock } = vi.hoisted(() => ({
4
+ fetchDouyinUserVideosMock: vi.fn(),
5
+ fetchDouyinCommentsMock: vi.fn(),
6
+ }));
7
+
8
+ vi.mock('./_shared/public-api.js', () => ({
9
+ fetchDouyinUserVideos: fetchDouyinUserVideosMock,
10
+ fetchDouyinComments: fetchDouyinCommentsMock,
11
+ }));
12
+
13
+ import { getRegistry } from '../../registry.js';
14
+ import { DEFAULT_COMMENT_LIMIT, MAX_USER_VIDEOS_LIMIT, normalizeCommentLimit, normalizeUserVideosLimit } from './user-videos.js';
15
+
16
+ describe('douyin user-videos', () => {
17
+ beforeEach(() => {
18
+ fetchDouyinUserVideosMock.mockReset();
19
+ fetchDouyinCommentsMock.mockReset();
20
+ });
21
+
22
+ it('registers the command', () => {
23
+ const registry = getRegistry();
24
+ const values = [...registry.values()];
25
+ const command = values.find((cmd) => cmd.site === 'douyin' && cmd.name === 'user-videos');
26
+ expect(command).toBeDefined();
27
+ });
28
+
29
+ it('clamps limit to a safe maximum', () => {
30
+ expect(normalizeUserVideosLimit(100)).toBe(MAX_USER_VIDEOS_LIMIT);
31
+ expect(normalizeUserVideosLimit(0)).toBe(1);
32
+ expect(normalizeCommentLimit(99)).toBe(DEFAULT_COMMENT_LIMIT);
33
+ });
34
+
35
+ it('uses shared public-api helpers and applies clamped limits', async () => {
36
+ const registry = getRegistry();
37
+ const command = [...registry.values()].find((cmd) => cmd.site === 'douyin' && cmd.name === 'user-videos');
38
+ expect(command?.func).toBeDefined();
39
+ if (!command?.func) throw new Error('douyin user-videos command not registered');
40
+
41
+ fetchDouyinUserVideosMock.mockResolvedValueOnce([
42
+ {
43
+ aweme_id: '1',
44
+ desc: 'test video',
45
+ video: { duration: 1234, play_addr: { url_list: ['https://example.com/video.mp4'] } },
46
+ statistics: { digg_count: 9 },
47
+ },
48
+ ]);
49
+ fetchDouyinCommentsMock.mockResolvedValueOnce([
50
+ { text: 'nice', digg_count: 3, nickname: 'alice' },
51
+ ]);
52
+
53
+ const page = {
54
+ goto: vi.fn().mockResolvedValue(undefined),
55
+ wait: vi.fn().mockResolvedValue(undefined),
56
+ };
57
+
58
+ const rows = await command.func(page as any, {
59
+ sec_uid: 'MS4w-test',
60
+ limit: 100,
61
+ comment_limit: 99,
62
+ with_comments: true,
63
+ });
64
+
65
+ expect(fetchDouyinUserVideosMock).toHaveBeenCalledWith(page, 'MS4w-test', MAX_USER_VIDEOS_LIMIT);
66
+ expect(fetchDouyinCommentsMock).toHaveBeenCalledWith(page, '1', DEFAULT_COMMENT_LIMIT);
67
+ expect(rows).toEqual([
68
+ {
69
+ index: 1,
70
+ aweme_id: '1',
71
+ title: 'test video',
72
+ duration: 1,
73
+ digg_count: 9,
74
+ play_url: 'https://example.com/video.mp4',
75
+ top_comments: [
76
+ { text: 'nice', digg_count: 3, nickname: 'alice' },
77
+ ],
78
+ },
79
+ ]);
80
+ });
81
+
82
+ it('skips comment enrichment when with_comments is false', async () => {
83
+ const registry = getRegistry();
84
+ const command = [...registry.values()].find((cmd) => cmd.site === 'douyin' && cmd.name === 'user-videos');
85
+ expect(command?.func).toBeDefined();
86
+ if (!command?.func) throw new Error('douyin user-videos command not registered');
87
+
88
+ fetchDouyinUserVideosMock.mockResolvedValueOnce([
89
+ {
90
+ aweme_id: '2',
91
+ desc: 'plain video',
92
+ video: { duration: 2000, play_addr: { url_list: ['https://example.com/plain.mp4'] } },
93
+ statistics: { digg_count: 1 },
94
+ },
95
+ ]);
96
+
97
+ const page = {
98
+ goto: vi.fn().mockResolvedValue(undefined),
99
+ wait: vi.fn().mockResolvedValue(undefined),
100
+ };
101
+
102
+ const rows = await command.func(page as any, {
103
+ sec_uid: 'MS4w-test',
104
+ limit: 3,
105
+ with_comments: false,
106
+ comment_limit: 5,
107
+ });
108
+
109
+ expect(fetchDouyinCommentsMock).not.toHaveBeenCalled();
110
+ expect(rows).toEqual([
111
+ {
112
+ index: 1,
113
+ aweme_id: '2',
114
+ title: 'plain video',
115
+ duration: 2,
116
+ digg_count: 1,
117
+ play_url: 'https://example.com/plain.mp4',
118
+ top_comments: [],
119
+ },
120
+ ]);
121
+ });
122
+ });
@@ -0,0 +1,101 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import type { IPage } from '../../types.js';
3
+ import { fetchDouyinComments, fetchDouyinUserVideos, type DouyinVideo } from './_shared/public-api.js';
4
+
5
+ export const MAX_USER_VIDEOS_LIMIT = 20;
6
+ export const USER_VIDEO_COMMENT_CONCURRENCY = 4;
7
+ export const DEFAULT_COMMENT_LIMIT = 10;
8
+
9
+ type EnrichedDouyinVideo = DouyinVideo & {
10
+ top_comments?: Array<{
11
+ text: string;
12
+ digg_count: number;
13
+ nickname: string;
14
+ }>;
15
+ };
16
+
17
+ export function normalizeUserVideosLimit(limit: unknown): number {
18
+ const numeric = Number(limit);
19
+ if (!Number.isFinite(numeric)) return MAX_USER_VIDEOS_LIMIT;
20
+ return Math.min(MAX_USER_VIDEOS_LIMIT, Math.max(1, Math.round(numeric)));
21
+ }
22
+
23
+ export function normalizeCommentLimit(limit: unknown): number {
24
+ const numeric = Number(limit);
25
+ if (!Number.isFinite(numeric)) return DEFAULT_COMMENT_LIMIT;
26
+ return Math.min(DEFAULT_COMMENT_LIMIT, Math.max(1, Math.round(numeric)));
27
+ }
28
+
29
+ async function mapInBatches<T, R>(
30
+ items: T[],
31
+ concurrency: number,
32
+ mapper: (item: T) => Promise<R>,
33
+ ): Promise<R[]> {
34
+ const results: R[] = [];
35
+ for (let index = 0; index < items.length; index += concurrency) {
36
+ const chunk = items.slice(index, index + concurrency);
37
+ results.push(...(await Promise.all(chunk.map(mapper))));
38
+ }
39
+ return results;
40
+ }
41
+
42
+ async function fetchTopComments(
43
+ page: IPage,
44
+ awemeId: string,
45
+ count: number,
46
+ ): Promise<Array<{ text: string; digg_count: number; nickname: string }>> {
47
+ try {
48
+ return await fetchDouyinComments(page, awemeId, count);
49
+ } catch {
50
+ return [];
51
+ }
52
+ }
53
+
54
+ cli({
55
+ site: 'douyin',
56
+ name: 'user-videos',
57
+ description: '获取指定用户的视频列表(含下载地址和热门评论)',
58
+ domain: 'www.douyin.com',
59
+ strategy: Strategy.COOKIE,
60
+ args: [
61
+ { name: 'sec_uid', type: 'string', required: true, positional: true, help: '用户 sec_uid(URL 末尾部分)' },
62
+ { name: 'limit', type: 'int', default: 20, help: '获取数量(最大 20)' },
63
+ { name: 'with_comments', type: 'bool', default: true, help: '包含热门评论(默认: true)' },
64
+ { name: 'comment_limit', type: 'int', default: 10, help: '每个视频获取多少条评论(最大 10)' },
65
+ ],
66
+ columns: ['index', 'aweme_id', 'title', 'duration', 'digg_count', 'play_url', 'top_comments'],
67
+ func: async (page: IPage, kwargs) => {
68
+ const secUid = kwargs.sec_uid as string;
69
+ const limit = normalizeUserVideosLimit(kwargs.limit);
70
+ const withComments = kwargs.with_comments !== false;
71
+ const commentLimit = normalizeCommentLimit(kwargs.comment_limit);
72
+
73
+ await page.goto(`https://www.douyin.com/user/${secUid}`);
74
+ await page.wait(3);
75
+
76
+ const awemeList = (await fetchDouyinUserVideos(page, secUid, limit)).slice(0, limit);
77
+ const videos: EnrichedDouyinVideo[] = withComments
78
+ ? await mapInBatches(
79
+ awemeList,
80
+ USER_VIDEO_COMMENT_CONCURRENCY,
81
+ async (video) => ({
82
+ ...video,
83
+ top_comments: await fetchTopComments(page, video.aweme_id, commentLimit),
84
+ }),
85
+ )
86
+ : awemeList.map((video) => ({ ...video, top_comments: [] }));
87
+
88
+ return videos.map((video, index) => {
89
+ const playUrl = video.video?.play_addr?.url_list?.[0] ?? '';
90
+ return {
91
+ index: index + 1,
92
+ aweme_id: video.aweme_id,
93
+ title: video.desc ?? '',
94
+ duration: Math.round((video.video?.duration ?? 0) / 1000),
95
+ digg_count: video.statistics?.digg_count ?? 0,
96
+ play_url: playUrl,
97
+ top_comments: video.top_comments ?? [],
98
+ };
99
+ });
100
+ },
101
+ });