@jackwener/opencli 1.5.5 → 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 (231) 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 +1229 -67
  7. package/dist/clis/band/bands.d.ts +1 -0
  8. package/dist/clis/band/bands.js +72 -0
  9. package/dist/clis/band/mentions.d.ts +1 -0
  10. package/dist/clis/band/mentions.js +127 -0
  11. package/dist/clis/band/post.d.ts +1 -0
  12. package/dist/clis/band/post.js +175 -0
  13. package/dist/clis/band/posts.d.ts +1 -0
  14. package/dist/clis/band/posts.js +94 -0
  15. package/dist/clis/doubao/detail.d.ts +1 -0
  16. package/dist/clis/doubao/detail.js +33 -0
  17. package/dist/clis/doubao/detail.test.d.ts +1 -0
  18. package/dist/clis/doubao/detail.test.js +42 -0
  19. package/dist/clis/doubao/history.d.ts +1 -0
  20. package/dist/clis/doubao/history.js +28 -0
  21. package/dist/clis/doubao/history.test.d.ts +1 -0
  22. package/dist/clis/doubao/history.test.js +37 -0
  23. package/dist/clis/doubao/meeting-summary.d.ts +1 -0
  24. package/dist/clis/doubao/meeting-summary.js +39 -0
  25. package/dist/clis/doubao/meeting-transcript.d.ts +1 -0
  26. package/dist/clis/doubao/meeting-transcript.js +36 -0
  27. package/dist/clis/doubao/utils.d.ts +27 -0
  28. package/dist/clis/doubao/utils.js +317 -0
  29. package/dist/clis/doubao/utils.test.d.ts +1 -0
  30. package/dist/clis/doubao/utils.test.js +24 -0
  31. package/dist/clis/douyin/_shared/public-api.d.ts +33 -0
  32. package/dist/clis/douyin/_shared/public-api.js +29 -0
  33. package/dist/clis/douyin/user-videos.d.ts +5 -0
  34. package/dist/clis/douyin/user-videos.js +74 -0
  35. package/dist/clis/douyin/user-videos.test.d.ts +1 -0
  36. package/dist/clis/douyin/user-videos.test.js +108 -0
  37. package/dist/clis/ones/common.d.ts +32 -0
  38. package/dist/clis/ones/common.js +144 -0
  39. package/dist/clis/ones/enrich-tasks.d.ts +5 -0
  40. package/dist/clis/ones/enrich-tasks.js +37 -0
  41. package/dist/clis/ones/login.d.ts +1 -0
  42. package/dist/clis/ones/login.js +80 -0
  43. package/dist/clis/ones/logout.d.ts +1 -0
  44. package/dist/clis/ones/logout.js +17 -0
  45. package/dist/clis/ones/me.d.ts +1 -0
  46. package/dist/clis/ones/me.js +30 -0
  47. package/dist/clis/ones/my-tasks.d.ts +1 -0
  48. package/dist/clis/ones/my-tasks.js +120 -0
  49. package/dist/clis/ones/resolve-labels.d.ts +10 -0
  50. package/dist/clis/ones/resolve-labels.js +64 -0
  51. package/dist/clis/ones/task-helpers.d.ts +29 -0
  52. package/dist/clis/ones/task-helpers.js +212 -0
  53. package/dist/clis/ones/task-helpers.test.d.ts +1 -0
  54. package/dist/clis/ones/task-helpers.test.js +12 -0
  55. package/dist/clis/ones/task.d.ts +1 -0
  56. package/dist/clis/ones/task.js +66 -0
  57. package/dist/clis/ones/tasks.d.ts +1 -0
  58. package/dist/clis/ones/tasks.js +79 -0
  59. package/dist/clis/ones/token-info.d.ts +1 -0
  60. package/dist/clis/ones/token-info.js +42 -0
  61. package/dist/clis/ones/worklog.d.ts +11 -0
  62. package/dist/clis/ones/worklog.js +267 -0
  63. package/dist/clis/ones/worklog.test.d.ts +1 -0
  64. package/dist/clis/ones/worklog.test.js +20 -0
  65. package/dist/clis/spotify/spotify.d.ts +1 -0
  66. package/dist/clis/spotify/spotify.js +316 -0
  67. package/dist/clis/spotify/utils.d.ts +21 -0
  68. package/dist/clis/spotify/utils.js +66 -0
  69. package/dist/clis/spotify/utils.test.d.ts +1 -0
  70. package/dist/clis/spotify/utils.test.js +67 -0
  71. package/dist/clis/tieba/commands.test.d.ts +4 -0
  72. package/dist/clis/tieba/commands.test.js +79 -0
  73. package/dist/clis/tieba/hot.d.ts +1 -0
  74. package/dist/clis/tieba/hot.js +48 -0
  75. package/dist/clis/tieba/posts.d.ts +1 -0
  76. package/dist/clis/tieba/posts.js +85 -0
  77. package/dist/clis/tieba/read.d.ts +1 -0
  78. package/dist/clis/tieba/read.js +140 -0
  79. package/dist/clis/tieba/search.d.ts +1 -0
  80. package/dist/clis/tieba/search.js +108 -0
  81. package/dist/clis/tieba/utils.d.ts +101 -0
  82. package/dist/clis/tieba/utils.js +240 -0
  83. package/dist/clis/tieba/utils.test.d.ts +1 -0
  84. package/dist/clis/tieba/utils.test.js +290 -0
  85. package/dist/clis/weread/book.js +100 -13
  86. package/dist/clis/weread/commands.test.js +221 -0
  87. package/dist/clis/weread/private-api-regression.test.d.ts +1 -0
  88. package/dist/{weread-private-api-regression.test.js → clis/weread/private-api-regression.test.js} +92 -30
  89. package/dist/clis/weread/search-regression.test.d.ts +1 -0
  90. package/dist/clis/weread/search-regression.test.js +407 -0
  91. package/dist/clis/weread/search.js +143 -7
  92. package/dist/clis/weread/shelf.js +13 -95
  93. package/dist/clis/weread/utils.d.ts +46 -0
  94. package/dist/clis/weread/utils.js +214 -7
  95. package/dist/clis/weread/utils.test.js +71 -1
  96. package/dist/clis/xiaohongshu/publish.d.ts +1 -1
  97. package/dist/clis/xiaohongshu/publish.js +78 -31
  98. package/dist/clis/xiaohongshu/publish.test.js +66 -1
  99. package/dist/clis/xiaohongshu/user-helpers.d.ts +1 -0
  100. package/dist/clis/xiaohongshu/user-helpers.js +2 -0
  101. package/dist/clis/xiaohongshu/user-helpers.test.js +18 -0
  102. package/dist/clis/xueqiu/comments.d.ts +118 -0
  103. package/dist/clis/xueqiu/comments.js +354 -0
  104. package/dist/clis/xueqiu/comments.test.d.ts +1 -0
  105. package/dist/clis/xueqiu/comments.test.js +696 -0
  106. package/dist/clis/youtube/transcript.js +2 -4
  107. package/dist/clis/youtube/utils.d.ts +9 -0
  108. package/dist/clis/youtube/utils.js +67 -3
  109. package/dist/clis/youtube/utils.test.d.ts +1 -0
  110. package/dist/clis/youtube/utils.test.js +37 -0
  111. package/dist/clis/youtube/video.js +16 -15
  112. package/dist/clis/zsxq/dynamics.d.ts +1 -0
  113. package/dist/clis/zsxq/dynamics.js +47 -0
  114. package/dist/clis/zsxq/groups.d.ts +1 -0
  115. package/dist/clis/zsxq/groups.js +32 -0
  116. package/dist/clis/zsxq/search.d.ts +1 -0
  117. package/dist/clis/zsxq/search.js +43 -0
  118. package/dist/clis/zsxq/search.test.d.ts +1 -0
  119. package/dist/clis/zsxq/search.test.js +24 -0
  120. package/dist/clis/zsxq/topic.d.ts +1 -0
  121. package/dist/clis/zsxq/topic.js +47 -0
  122. package/dist/clis/zsxq/topic.test.d.ts +1 -0
  123. package/dist/clis/zsxq/topic.test.js +29 -0
  124. package/dist/clis/zsxq/topics.d.ts +1 -0
  125. package/dist/clis/zsxq/topics.js +25 -0
  126. package/dist/clis/zsxq/topics.test.d.ts +1 -0
  127. package/dist/clis/zsxq/topics.test.js +24 -0
  128. package/dist/clis/zsxq/utils.d.ts +97 -0
  129. package/dist/clis/zsxq/utils.js +230 -0
  130. package/dist/commanderAdapter.js +1 -1
  131. package/dist/commanderAdapter.test.js +39 -0
  132. package/dist/external-clis.yaml +17 -0
  133. package/dist/types.d.ts +5 -0
  134. package/docs/.vitepress/config.mts +3 -0
  135. package/docs/adapters/browser/band.md +63 -0
  136. package/docs/adapters/browser/ones.md +59 -0
  137. package/docs/adapters/browser/spotify.md +62 -0
  138. package/docs/adapters/browser/tieba.md +45 -0
  139. package/docs/adapters/browser/xueqiu.md +5 -0
  140. package/docs/adapters/browser/zsxq.md +49 -0
  141. package/docs/adapters/index.md +5 -2
  142. package/docs/adapters-doc/ones.md +32 -0
  143. package/extension/src/background.ts +15 -0
  144. package/extension/src/cdp.ts +42 -0
  145. package/extension/src/protocol.ts +5 -1
  146. package/package.json +1 -1
  147. package/scripts/postinstall.js +16 -0
  148. package/src/browser/daemon-client.ts +5 -1
  149. package/src/browser/page.ts +16 -0
  150. package/src/clis/band/bands.ts +76 -0
  151. package/src/clis/band/mentions.ts +134 -0
  152. package/src/clis/band/post.ts +187 -0
  153. package/src/clis/band/posts.ts +106 -0
  154. package/src/clis/doubao/detail.test.ts +53 -0
  155. package/src/clis/doubao/detail.ts +41 -0
  156. package/src/clis/doubao/history.test.ts +45 -0
  157. package/src/clis/doubao/history.ts +32 -0
  158. package/src/clis/doubao/meeting-summary.ts +53 -0
  159. package/src/clis/doubao/meeting-transcript.ts +48 -0
  160. package/src/clis/doubao/utils.test.ts +45 -0
  161. package/src/clis/doubao/utils.ts +371 -0
  162. package/src/clis/douyin/_shared/public-api.ts +84 -0
  163. package/src/clis/douyin/user-videos.test.ts +122 -0
  164. package/src/clis/douyin/user-videos.ts +101 -0
  165. package/src/clis/ones/common.ts +187 -0
  166. package/src/clis/ones/enrich-tasks.ts +47 -0
  167. package/src/clis/ones/login.ts +103 -0
  168. package/src/clis/ones/logout.ts +19 -0
  169. package/src/clis/ones/me.ts +34 -0
  170. package/src/clis/ones/my-tasks.ts +148 -0
  171. package/src/clis/ones/resolve-labels.ts +80 -0
  172. package/src/clis/ones/task-helpers.test.ts +14 -0
  173. package/src/clis/ones/task-helpers.ts +214 -0
  174. package/src/clis/ones/task.ts +79 -0
  175. package/src/clis/ones/tasks.ts +92 -0
  176. package/src/clis/ones/token-info.ts +46 -0
  177. package/src/clis/ones/worklog.test.ts +24 -0
  178. package/src/clis/ones/worklog.ts +306 -0
  179. package/src/clis/spotify/spotify.ts +328 -0
  180. package/src/clis/spotify/utils.test.ts +87 -0
  181. package/src/clis/spotify/utils.ts +92 -0
  182. package/src/clis/tieba/commands.test.ts +86 -0
  183. package/src/clis/tieba/hot.ts +52 -0
  184. package/src/clis/tieba/posts.ts +108 -0
  185. package/src/clis/tieba/read.ts +158 -0
  186. package/src/clis/tieba/search.ts +119 -0
  187. package/src/clis/tieba/utils.test.ts +322 -0
  188. package/src/clis/tieba/utils.ts +348 -0
  189. package/src/clis/weread/book.ts +116 -13
  190. package/src/clis/weread/commands.test.ts +249 -0
  191. package/src/{weread-private-api-regression.test.ts → clis/weread/private-api-regression.test.ts} +108 -30
  192. package/src/clis/weread/search-regression.test.ts +440 -0
  193. package/src/clis/weread/search.ts +189 -9
  194. package/src/clis/weread/shelf.ts +20 -122
  195. package/src/clis/weread/utils.test.ts +81 -1
  196. package/src/clis/weread/utils.ts +264 -7
  197. package/src/clis/xiaohongshu/publish.test.ts +79 -1
  198. package/src/clis/xiaohongshu/publish.ts +84 -30
  199. package/src/clis/xiaohongshu/user-helpers.test.ts +23 -0
  200. package/src/clis/xiaohongshu/user-helpers.ts +4 -0
  201. package/src/clis/xueqiu/comments.test.ts +823 -0
  202. package/src/clis/xueqiu/comments.ts +461 -0
  203. package/src/clis/youtube/transcript.ts +2 -4
  204. package/src/clis/youtube/utils.test.ts +43 -0
  205. package/src/clis/youtube/utils.ts +69 -0
  206. package/src/clis/youtube/video.ts +16 -15
  207. package/src/clis/zsxq/dynamics.ts +60 -0
  208. package/src/clis/zsxq/groups.ts +41 -0
  209. package/src/clis/zsxq/search.test.ts +29 -0
  210. package/src/clis/zsxq/search.ts +54 -0
  211. package/src/clis/zsxq/topic.test.ts +34 -0
  212. package/src/clis/zsxq/topic.ts +68 -0
  213. package/src/clis/zsxq/topics.test.ts +29 -0
  214. package/src/clis/zsxq/topics.ts +36 -0
  215. package/src/clis/zsxq/utils.ts +351 -0
  216. package/src/commanderAdapter.test.ts +47 -0
  217. package/src/commanderAdapter.ts +1 -1
  218. package/src/external-clis.yaml +17 -0
  219. package/src/types.ts +5 -0
  220. package/tests/e2e/band-auth.test.ts +20 -0
  221. package/tests/e2e/browser-auth-helpers.ts +18 -0
  222. package/tests/e2e/browser-auth.test.ts +35 -47
  223. package/tests/e2e/browser-public.test.ts +288 -0
  224. package/tests/e2e/management.test.ts +1 -1
  225. package/tests/e2e/plugin-management.test.ts +1 -1
  226. package/vitest.config.ts +1 -0
  227. package/SKILL.md +0 -879
  228. package/dist/weread-private-api-regression.test.d.ts +0 -1
  229. package/dist/weread-search-regression.test.d.ts +0 -1
  230. package/dist/weread-search-regression.test.js +0 -39
  231. package/src/weread-search-regression.test.ts +0 -44
@@ -0,0 +1,39 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import { DOUBAO_DOMAIN, openMeetingPanel, getMeetingSummary, getMeetingChapters, parseDoubaoConversationId, } from './utils.js';
3
+ export const meetingSummaryCommand = cli({
4
+ site: 'doubao',
5
+ name: 'meeting-summary',
6
+ description: 'Get meeting summary and chapters from a Doubao conversation',
7
+ domain: DOUBAO_DOMAIN,
8
+ strategy: Strategy.COOKIE,
9
+ browser: true,
10
+ navigateBefore: false,
11
+ args: [
12
+ { name: 'id', required: true, positional: true, help: 'Conversation ID (numeric or full URL)' },
13
+ { name: 'chapters', required: false, help: 'Also include AI chapters', default: 'false' },
14
+ ],
15
+ columns: ['Section', 'Content'],
16
+ func: async (page, kwargs) => {
17
+ const conversationId = parseDoubaoConversationId(kwargs.id);
18
+ const includeChapters = kwargs.chapters === 'true' || kwargs.chapters === true;
19
+ const opened = await openMeetingPanel(page, conversationId);
20
+ if (!opened) {
21
+ return [{ Section: 'Error', Content: 'No meeting card found in this conversation.' }];
22
+ }
23
+ const summary = await getMeetingSummary(page);
24
+ const result = [];
25
+ if (summary) {
26
+ result.push({ Section: 'Summary', Content: summary });
27
+ }
28
+ if (includeChapters) {
29
+ const chapters = await getMeetingChapters(page);
30
+ if (chapters) {
31
+ result.push({ Section: 'Chapters', Content: chapters });
32
+ }
33
+ }
34
+ if (result.length === 0) {
35
+ return [{ Section: 'Info', Content: 'Meeting panel opened but no content found yet. Try again.' }];
36
+ }
37
+ return result;
38
+ },
39
+ });
@@ -0,0 +1 @@
1
+ export declare const meetingTranscriptCommand: import("../../registry.js").CliCommand;
@@ -0,0 +1,36 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import { DOUBAO_DOMAIN, openMeetingPanel, getMeetingTranscript, parseDoubaoConversationId, triggerTranscriptDownload, } from './utils.js';
3
+ export const meetingTranscriptCommand = cli({
4
+ site: 'doubao',
5
+ name: 'meeting-transcript',
6
+ description: 'Get or download the meeting transcript from a Doubao conversation',
7
+ domain: DOUBAO_DOMAIN,
8
+ strategy: Strategy.COOKIE,
9
+ browser: true,
10
+ navigateBefore: false,
11
+ args: [
12
+ { name: 'id', required: true, positional: true, help: 'Conversation ID (numeric or full URL)' },
13
+ { name: 'download', required: false, help: 'Trigger browser file download instead of reading text', default: 'false' },
14
+ ],
15
+ columns: ['Section', 'Content'],
16
+ func: async (page, kwargs) => {
17
+ const conversationId = parseDoubaoConversationId(kwargs.id);
18
+ const shouldDownload = kwargs.download === 'true' || kwargs.download === true;
19
+ const opened = await openMeetingPanel(page, conversationId);
20
+ if (!opened) {
21
+ return [{ Section: 'Error', Content: 'No meeting card found in this conversation.' }];
22
+ }
23
+ if (shouldDownload) {
24
+ const ok = await triggerTranscriptDownload(page);
25
+ if (!ok) {
26
+ return [{ Section: 'Error', Content: 'Failed to trigger transcript download.' }];
27
+ }
28
+ return [{ Section: 'Download', Content: 'Transcript download triggered in browser. Check your Downloads folder.' }];
29
+ }
30
+ const transcript = await getMeetingTranscript(page);
31
+ if (!transcript) {
32
+ return [{ Section: 'Info', Content: 'No transcript content found. The meeting may not have a text record.' }];
33
+ }
34
+ return [{ Section: 'Transcript', Content: transcript }];
35
+ },
36
+ });
@@ -2,6 +2,11 @@ import type { IPage } from '../../types.js';
2
2
  export declare const DOUBAO_DOMAIN = "www.doubao.com";
3
3
  export declare const DOUBAO_CHAT_URL = "https://www.doubao.com/chat";
4
4
  export declare const DOUBAO_NEW_CHAT_URL = "https://www.doubao.com/chat/new-thread/create-by-msg";
5
+ export interface DoubaoConversation {
6
+ Id: string;
7
+ Title: string;
8
+ Url: string;
9
+ }
5
10
  export interface DoubaoTurn {
6
11
  Role: 'User' | 'Assistant' | 'System';
7
12
  Text: string;
@@ -20,4 +25,26 @@ export declare function getDoubaoVisibleTurns(page: IPage): Promise<DoubaoTurn[]
20
25
  export declare function getDoubaoTranscriptLines(page: IPage): Promise<string[]>;
21
26
  export declare function sendDoubaoMessage(page: IPage, text: string): Promise<'button' | 'enter'>;
22
27
  export declare function waitForDoubaoResponse(page: IPage, beforeLines: string[], beforeTurns: DoubaoTurn[], promptText: string, timeoutSeconds: number): Promise<string>;
28
+ export declare function getDoubaoConversationList(page: IPage): Promise<DoubaoConversation[]>;
29
+ export interface DoubaoMessage {
30
+ Role: 'User' | 'Assistant' | 'System';
31
+ Text: string;
32
+ HasMeetingCard: boolean;
33
+ }
34
+ export interface DoubaoMeetingInfo {
35
+ title: string;
36
+ time: string;
37
+ }
38
+ export declare function parseDoubaoConversationId(input: string): string;
39
+ export declare function navigateToConversation(page: IPage, conversationId: string): Promise<void>;
40
+ export declare function getConversationDetail(page: IPage, conversationId: string): Promise<{
41
+ messages: DoubaoMessage[];
42
+ meeting: DoubaoMeetingInfo | null;
43
+ }>;
44
+ export declare function mergeTranscriptSnapshots(existing: string, incoming: string): string;
45
+ export declare function openMeetingPanel(page: IPage, conversationId: string): Promise<boolean>;
46
+ export declare function getMeetingSummary(page: IPage): Promise<string>;
47
+ export declare function getMeetingChapters(page: IPage): Promise<string>;
48
+ export declare function getMeetingTranscript(page: IPage): Promise<string>;
49
+ export declare function triggerTranscriptDownload(page: IPage): Promise<boolean>;
23
50
  export declare function startNewDoubaoChat(page: IPage): Promise<string>;
@@ -551,6 +551,323 @@ export async function waitForDoubaoResponse(page, beforeLines, beforeTurns, prom
551
551
  }
552
552
  return lastCandidate;
553
553
  }
554
+ function getConversationListScript() {
555
+ return `
556
+ (() => {
557
+ const sidebar = document.querySelector('[data-testid="flow_chat_sidebar"]');
558
+ if (!sidebar) return [];
559
+
560
+ const items = Array.from(
561
+ sidebar.querySelectorAll('a[data-testid="chat_list_thread_item"]')
562
+ );
563
+
564
+ return items
565
+ .map(a => {
566
+ const href = a.getAttribute('href') || '';
567
+ const match = href.match(/\\/chat\\/(\\d{10,})/);
568
+ if (!match) return null;
569
+ const id = match[1];
570
+ const textContent = (a.textContent || a.innerText || '').trim();
571
+ const title = textContent
572
+ .replace(/\\s+/g, ' ')
573
+ .substring(0, 200);
574
+ return { id, title, href };
575
+ })
576
+ .filter(Boolean);
577
+ })()
578
+ `;
579
+ }
580
+ export async function getDoubaoConversationList(page) {
581
+ await ensureDoubaoChatPage(page);
582
+ const raw = await page.evaluate(getConversationListScript());
583
+ if (!Array.isArray(raw))
584
+ return [];
585
+ return raw.map((item) => ({
586
+ Id: item.id,
587
+ Title: item.title,
588
+ Url: `${DOUBAO_CHAT_URL}/${item.id}`,
589
+ }));
590
+ }
591
+ export function parseDoubaoConversationId(input) {
592
+ const match = input.match(/(\d{10,})/);
593
+ return match ? match[1] : input;
594
+ }
595
+ function getConversationDetailScript() {
596
+ return `
597
+ (() => {
598
+ const clean = (v) => (v || '').replace(/\\u00a0/g, ' ').replace(/\\n{3,}/g, '\\n\\n').trim();
599
+
600
+ const messageList = document.querySelector('[data-testid="message-list"]');
601
+ if (!messageList) return { messages: [], meeting: null };
602
+
603
+ const meetingCard = messageList.querySelector('[data-testid="meeting-minutes-card"]');
604
+ let meeting = null;
605
+ if (meetingCard) {
606
+ const raw = clean(meetingCard.textContent || '');
607
+ const match = raw.match(/^(.+?)(?:会议时间:|\\s*$)(.*)/);
608
+ meeting = {
609
+ title: match ? match[1].trim() : raw,
610
+ time: match && match[2] ? match[2].trim() : '',
611
+ };
612
+ }
613
+
614
+ const unions = Array.from(messageList.querySelectorAll('[data-testid="union_message"]'));
615
+ const messages = unions.map(u => {
616
+ const isSend = !!u.querySelector('[data-testid="send_message"]');
617
+ const isReceive = !!u.querySelector('[data-testid="receive_message"]');
618
+ const textEl = u.querySelector('[data-testid="message_text_content"]');
619
+ const text = textEl ? clean(textEl.innerText || textEl.textContent || '') : '';
620
+ return {
621
+ role: isSend ? 'User' : isReceive ? 'Assistant' : 'System',
622
+ text,
623
+ hasMeetingCard: !!u.querySelector('[data-testid="meeting-minutes-card"]'),
624
+ };
625
+ }).filter(m => m.text);
626
+
627
+ return { messages, meeting };
628
+ })()
629
+ `;
630
+ }
631
+ export async function navigateToConversation(page, conversationId) {
632
+ const url = `${DOUBAO_CHAT_URL}/${conversationId}`;
633
+ const currentUrl = await page.evaluate('window.location.href').catch(() => '');
634
+ if (typeof currentUrl === 'string' && currentUrl.includes(`/chat/${conversationId}`)) {
635
+ await page.wait(1);
636
+ return;
637
+ }
638
+ await page.goto(url, { waitUntil: 'load', settleMs: 3000 });
639
+ await page.wait(2);
640
+ }
641
+ export async function getConversationDetail(page, conversationId) {
642
+ await navigateToConversation(page, conversationId);
643
+ const raw = await page.evaluate(getConversationDetailScript());
644
+ const messages = (raw.messages || []).map((m) => ({
645
+ Role: m.role,
646
+ Text: m.text,
647
+ HasMeetingCard: m.hasMeetingCard,
648
+ }));
649
+ return { messages, meeting: raw.meeting };
650
+ }
651
+ // ---------------------------------------------------------------------------
652
+ // Meeting minutes panel helpers
653
+ // ---------------------------------------------------------------------------
654
+ function clickMeetingCardScript() {
655
+ return `
656
+ (() => {
657
+ const card = document.querySelector('[data-testid="meeting-minutes-card"]');
658
+ if (!card) return false;
659
+ card.click();
660
+ return true;
661
+ })()
662
+ `;
663
+ }
664
+ function readMeetingSummaryScript() {
665
+ return `
666
+ (() => {
667
+ const panel = document.querySelector('[data-testid="canvas_panel_container"]');
668
+ if (!panel) return { error: 'no panel' };
669
+
670
+ const summary = panel.querySelector('[data-testid="meeting-summary-todos"]');
671
+ const summaryText = summary
672
+ ? (summary.innerText || summary.textContent || '').trim()
673
+ : '';
674
+
675
+ return { summary: summaryText };
676
+ })()
677
+ `;
678
+ }
679
+ function clickTextNotesTabScript() {
680
+ return `
681
+ (() => {
682
+ const panel = document.querySelector('[data-testid="canvas_panel_container"]');
683
+ if (!panel) return false;
684
+ const tabs = panel.querySelectorAll('[role="tab"], .semi-tabs-tab');
685
+ for (const tab of tabs) {
686
+ if ((tab.textContent || '').trim().includes('文字')) {
687
+ tab.click();
688
+ return true;
689
+ }
690
+ }
691
+ return false;
692
+ })()
693
+ `;
694
+ }
695
+ function readTextNotesScript() {
696
+ return `
697
+ (() => {
698
+ const panel = document.querySelector('[data-testid="canvas_panel_container"]');
699
+ if (!panel) return '';
700
+ const textNotes = panel.querySelector('[data-testid="meeting-text-notes"]');
701
+ if (!textNotes) return '';
702
+ return (textNotes.innerText || textNotes.textContent || '').trim();
703
+ })()
704
+ `;
705
+ }
706
+ function normalizeTranscriptLines(text) {
707
+ return text
708
+ .split('\n')
709
+ .map(line => line.trim())
710
+ .filter(Boolean);
711
+ }
712
+ function containsLineSequence(haystack, needle) {
713
+ if (needle.length === 0)
714
+ return true;
715
+ if (needle.length > haystack.length)
716
+ return false;
717
+ for (let start = 0; start <= haystack.length - needle.length; start += 1) {
718
+ let matched = true;
719
+ for (let offset = 0; offset < needle.length; offset += 1) {
720
+ if (haystack[start + offset] !== needle[offset]) {
721
+ matched = false;
722
+ break;
723
+ }
724
+ }
725
+ if (matched)
726
+ return true;
727
+ }
728
+ return false;
729
+ }
730
+ export function mergeTranscriptSnapshots(existing, incoming) {
731
+ const currentLines = normalizeTranscriptLines(existing);
732
+ const nextLines = normalizeTranscriptLines(incoming);
733
+ if (nextLines.length === 0)
734
+ return currentLines.join('\n');
735
+ if (currentLines.length === 0)
736
+ return nextLines.join('\n');
737
+ if (containsLineSequence(currentLines, nextLines))
738
+ return currentLines.join('\n');
739
+ const maxOverlap = Math.min(currentLines.length, nextLines.length);
740
+ for (let overlap = maxOverlap; overlap > 0; overlap -= 1) {
741
+ let matched = true;
742
+ for (let index = 0; index < overlap; index += 1) {
743
+ if (currentLines[currentLines.length - overlap + index] !== nextLines[index]) {
744
+ matched = false;
745
+ break;
746
+ }
747
+ }
748
+ if (matched) {
749
+ return [...currentLines, ...nextLines.slice(overlap)].join('\n');
750
+ }
751
+ }
752
+ return [...currentLines, ...nextLines].join('\n');
753
+ }
754
+ function clickChapterTabScript() {
755
+ return `
756
+ (() => {
757
+ const panel = document.querySelector('[data-testid="canvas_panel_container"]');
758
+ if (!panel) return false;
759
+ const tabs = panel.querySelectorAll('[role="tab"], .semi-tabs-tab');
760
+ for (const tab of tabs) {
761
+ if ((tab.textContent || '').trim().includes('章节')) {
762
+ tab.click();
763
+ return true;
764
+ }
765
+ }
766
+ return false;
767
+ })()
768
+ `;
769
+ }
770
+ function readChapterScript() {
771
+ return `
772
+ (() => {
773
+ const panel = document.querySelector('[data-testid="canvas_panel_container"]');
774
+ if (!panel) return '';
775
+ const chapter = panel.querySelector('[data-testid="meeting-ai-chapter"]');
776
+ if (!chapter) return '';
777
+ return (chapter.innerText || chapter.textContent || '').trim();
778
+ })()
779
+ `;
780
+ }
781
+ function triggerTranscriptDownloadScript() {
782
+ return `
783
+ (() => {
784
+ const panel = document.querySelector('[data-testid="canvas_panel_container"]');
785
+ if (!panel) return { error: 'no panel' };
786
+
787
+ const downloadIcon = panel.querySelector('[class*="DownloadMeetingAudio"] span[role="img"]');
788
+ if (!downloadIcon) return { error: 'no download icon' };
789
+
790
+ downloadIcon.click();
791
+ return { clicked: 'icon' };
792
+ })()
793
+ `;
794
+ }
795
+ function clickTranscriptDownloadBtnScript() {
796
+ return `
797
+ (() => {
798
+ const btn = document.querySelector('[data-testid="minutes-download-text-btn"]');
799
+ if (!btn) return { error: 'no download text btn' };
800
+ btn.click();
801
+ return { clicked: 'transcript' };
802
+ })()
803
+ `;
804
+ }
805
+ export async function openMeetingPanel(page, conversationId) {
806
+ await navigateToConversation(page, conversationId);
807
+ const clicked = await page.evaluate(clickMeetingCardScript());
808
+ if (!clicked)
809
+ return false;
810
+ await page.wait(2);
811
+ return true;
812
+ }
813
+ export async function getMeetingSummary(page) {
814
+ const result = await page.evaluate(readMeetingSummaryScript());
815
+ return result.summary || '';
816
+ }
817
+ export async function getMeetingChapters(page) {
818
+ await page.evaluate(clickChapterTabScript());
819
+ await page.wait(1.5);
820
+ return await page.evaluate(readChapterScript());
821
+ }
822
+ function scrollTextNotesPanelScript() {
823
+ return `
824
+ (() => {
825
+ const panel = document.querySelector('[data-testid="canvas_panel_container"]');
826
+ if (!panel) return 0;
827
+ const textNotes = panel.querySelector('[data-testid="meeting-text-notes"]');
828
+ if (!textNotes) return 0;
829
+
830
+ const scrollable = textNotes.closest('[class*="overflow"]')
831
+ || textNotes.parentElement
832
+ || textNotes;
833
+ const maxScroll = scrollable.scrollHeight - scrollable.clientHeight;
834
+ if (maxScroll > 0) {
835
+ scrollable.scrollTop = scrollable.scrollHeight;
836
+ }
837
+ return maxScroll;
838
+ })()
839
+ `;
840
+ }
841
+ export async function getMeetingTranscript(page) {
842
+ await page.evaluate(clickTextNotesTabScript());
843
+ await page.wait(2);
844
+ let merged = '';
845
+ let stableRounds = 0;
846
+ for (let i = 0; i < 10; i++) {
847
+ await page.evaluate(scrollTextNotesPanelScript());
848
+ await page.wait(1);
849
+ const snapshot = await page.evaluate(readTextNotesScript());
850
+ const nextMerged = mergeTranscriptSnapshots(merged, snapshot);
851
+ if (nextMerged === merged && snapshot.length > 0) {
852
+ stableRounds += 1;
853
+ if (stableRounds >= 2)
854
+ break;
855
+ }
856
+ else {
857
+ stableRounds = 0;
858
+ merged = nextMerged;
859
+ }
860
+ }
861
+ return merged;
862
+ }
863
+ export async function triggerTranscriptDownload(page) {
864
+ const iconResult = await page.evaluate(triggerTranscriptDownloadScript());
865
+ if (iconResult.error)
866
+ return false;
867
+ await page.wait(1);
868
+ const btnResult = await page.evaluate(clickTranscriptDownloadBtnScript());
869
+ return !btnResult.error;
870
+ }
554
871
  export async function startNewDoubaoChat(page) {
555
872
  await ensureDoubaoChatPage(page);
556
873
  const clickedLabel = await page.evaluate(clickNewChatScript());
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,24 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { mergeTranscriptSnapshots, parseDoubaoConversationId } from './utils.js';
3
+ describe('parseDoubaoConversationId', () => {
4
+ it('extracts the numeric id from a full conversation URL', () => {
5
+ expect(parseDoubaoConversationId('https://www.doubao.com/chat/1234567890123')).toBe('1234567890123');
6
+ });
7
+ it('keeps a raw id unchanged', () => {
8
+ expect(parseDoubaoConversationId('1234567890123')).toBe('1234567890123');
9
+ });
10
+ });
11
+ describe('mergeTranscriptSnapshots', () => {
12
+ it('extends the transcript when the next snapshot overlaps with the tail', () => {
13
+ const merged = mergeTranscriptSnapshots('Alice 00:00\nHello team\nBob 00:05\nHi', 'Bob 00:05\nHi\nAlice 00:10\nNext topic');
14
+ expect(merged).toBe('Alice 00:00\nHello team\nBob 00:05\nHi\nAlice 00:10\nNext topic');
15
+ });
16
+ it('does not duplicate a snapshot that is already contained in the transcript', () => {
17
+ const merged = mergeTranscriptSnapshots('Alice 00:00\nHello team\nBob 00:05\nHi', 'Bob 00:05\nHi');
18
+ expect(merged).toBe('Alice 00:00\nHello team\nBob 00:05\nHi');
19
+ });
20
+ it('keeps both windows when a virtualized panel returns adjacent chunks without full history', () => {
21
+ const merged = mergeTranscriptSnapshots('Alice 00:00\nHello team\nBob 00:05\nHi', 'Alice 00:10\nNext topic\nBob 00:15\nAction items');
22
+ expect(merged).toBe('Alice 00:00\nHello team\nBob 00:05\nHi\nAlice 00:10\nNext topic\nBob 00:15\nAction items');
23
+ });
24
+ });
@@ -0,0 +1,33 @@
1
+ import type { IPage } from '../../../types.js';
2
+ export interface DouyinComment {
3
+ text?: string;
4
+ digg_count?: number;
5
+ user?: {
6
+ nickname?: string;
7
+ };
8
+ }
9
+ export interface DouyinVideo {
10
+ aweme_id: string;
11
+ desc?: string;
12
+ video?: {
13
+ duration?: number;
14
+ play_addr?: {
15
+ url_list?: string[];
16
+ };
17
+ };
18
+ statistics?: {
19
+ digg_count?: number;
20
+ };
21
+ }
22
+ export interface DouyinVideoListResponse {
23
+ aweme_list?: DouyinVideo[];
24
+ }
25
+ export interface DouyinCommentListResponse {
26
+ comments?: DouyinComment[];
27
+ }
28
+ export declare function fetchDouyinUserVideos(page: IPage, secUid: string, count: number): Promise<DouyinVideo[]>;
29
+ export declare function fetchDouyinComments(page: IPage, awemeId: string, count: number): Promise<Array<{
30
+ text: string;
31
+ digg_count: number;
32
+ nickname: string;
33
+ }>>;
@@ -0,0 +1,29 @@
1
+ import { browserFetch } from './browser-fetch.js';
2
+ export async function fetchDouyinUserVideos(page, secUid, count) {
3
+ const params = new URLSearchParams({
4
+ sec_user_id: secUid,
5
+ max_cursor: '0',
6
+ count: String(count),
7
+ aid: '6383',
8
+ });
9
+ const data = await browserFetch(page, 'GET', `https://www.douyin.com/aweme/v1/web/aweme/post/?${params.toString()}`, {
10
+ headers: { referer: 'https://www.douyin.com/' },
11
+ });
12
+ return data.aweme_list || [];
13
+ }
14
+ export async function fetchDouyinComments(page, awemeId, count) {
15
+ const params = new URLSearchParams({
16
+ aweme_id: awemeId,
17
+ count: String(count),
18
+ cursor: '0',
19
+ aid: '6383',
20
+ });
21
+ const data = await browserFetch(page, 'GET', `https://www.douyin.com/aweme/v1/web/comment/list/?${params.toString()}`, {
22
+ headers: { referer: 'https://www.douyin.com/' },
23
+ });
24
+ return (data.comments || []).slice(0, count).map((comment) => ({
25
+ text: comment.text || '',
26
+ digg_count: comment.digg_count ?? 0,
27
+ nickname: comment.user?.nickname || '',
28
+ }));
29
+ }
@@ -0,0 +1,5 @@
1
+ export declare const MAX_USER_VIDEOS_LIMIT = 20;
2
+ export declare const USER_VIDEO_COMMENT_CONCURRENCY = 4;
3
+ export declare const DEFAULT_COMMENT_LIMIT = 10;
4
+ export declare function normalizeUserVideosLimit(limit: unknown): number;
5
+ export declare function normalizeCommentLimit(limit: unknown): number;
@@ -0,0 +1,74 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import { fetchDouyinComments, fetchDouyinUserVideos } from './_shared/public-api.js';
3
+ export const MAX_USER_VIDEOS_LIMIT = 20;
4
+ export const USER_VIDEO_COMMENT_CONCURRENCY = 4;
5
+ export const DEFAULT_COMMENT_LIMIT = 10;
6
+ export function normalizeUserVideosLimit(limit) {
7
+ const numeric = Number(limit);
8
+ if (!Number.isFinite(numeric))
9
+ return MAX_USER_VIDEOS_LIMIT;
10
+ return Math.min(MAX_USER_VIDEOS_LIMIT, Math.max(1, Math.round(numeric)));
11
+ }
12
+ export function normalizeCommentLimit(limit) {
13
+ const numeric = Number(limit);
14
+ if (!Number.isFinite(numeric))
15
+ return DEFAULT_COMMENT_LIMIT;
16
+ return Math.min(DEFAULT_COMMENT_LIMIT, Math.max(1, Math.round(numeric)));
17
+ }
18
+ async function mapInBatches(items, concurrency, mapper) {
19
+ const results = [];
20
+ for (let index = 0; index < items.length; index += concurrency) {
21
+ const chunk = items.slice(index, index + concurrency);
22
+ results.push(...(await Promise.all(chunk.map(mapper))));
23
+ }
24
+ return results;
25
+ }
26
+ async function fetchTopComments(page, awemeId, count) {
27
+ try {
28
+ return await fetchDouyinComments(page, awemeId, count);
29
+ }
30
+ catch {
31
+ return [];
32
+ }
33
+ }
34
+ cli({
35
+ site: 'douyin',
36
+ name: 'user-videos',
37
+ description: '获取指定用户的视频列表(含下载地址和热门评论)',
38
+ domain: 'www.douyin.com',
39
+ strategy: Strategy.COOKIE,
40
+ args: [
41
+ { name: 'sec_uid', type: 'string', required: true, positional: true, help: '用户 sec_uid(URL 末尾部分)' },
42
+ { name: 'limit', type: 'int', default: 20, help: '获取数量(最大 20)' },
43
+ { name: 'with_comments', type: 'bool', default: true, help: '包含热门评论(默认: true)' },
44
+ { name: 'comment_limit', type: 'int', default: 10, help: '每个视频获取多少条评论(最大 10)' },
45
+ ],
46
+ columns: ['index', 'aweme_id', 'title', 'duration', 'digg_count', 'play_url', 'top_comments'],
47
+ func: async (page, kwargs) => {
48
+ const secUid = kwargs.sec_uid;
49
+ const limit = normalizeUserVideosLimit(kwargs.limit);
50
+ const withComments = kwargs.with_comments !== false;
51
+ const commentLimit = normalizeCommentLimit(kwargs.comment_limit);
52
+ await page.goto(`https://www.douyin.com/user/${secUid}`);
53
+ await page.wait(3);
54
+ const awemeList = (await fetchDouyinUserVideos(page, secUid, limit)).slice(0, limit);
55
+ const videos = withComments
56
+ ? await mapInBatches(awemeList, USER_VIDEO_COMMENT_CONCURRENCY, async (video) => ({
57
+ ...video,
58
+ top_comments: await fetchTopComments(page, video.aweme_id, commentLimit),
59
+ }))
60
+ : awemeList.map((video) => ({ ...video, top_comments: [] }));
61
+ return videos.map((video, index) => {
62
+ const playUrl = video.video?.play_addr?.url_list?.[0] ?? '';
63
+ return {
64
+ index: index + 1,
65
+ aweme_id: video.aweme_id,
66
+ title: video.desc ?? '',
67
+ duration: Math.round((video.video?.duration ?? 0) / 1000),
68
+ digg_count: video.statistics?.digg_count ?? 0,
69
+ play_url: playUrl,
70
+ top_comments: video.top_comments ?? [],
71
+ };
72
+ });
73
+ },
74
+ });
@@ -0,0 +1 @@
1
+ export {};