@jackwener/opencli 1.3.1 → 1.3.3

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 (241) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/README.md +48 -9
  3. package/README.zh-CN.md +48 -9
  4. package/SKILL.md +317 -6
  5. package/TESTING.md +4 -4
  6. package/dist/browser/cdp.js +10 -1
  7. package/dist/browser/daemon-client.js +2 -1
  8. package/dist/browser/discover.js +2 -1
  9. package/dist/browser/errors.d.ts +2 -1
  10. package/dist/browser/errors.js +10 -10
  11. package/dist/browser/index.d.ts +1 -0
  12. package/dist/browser/index.js +1 -0
  13. package/dist/browser/page.js +12 -0
  14. package/dist/browser/stealth.d.ts +18 -0
  15. package/dist/browser/stealth.js +140 -0
  16. package/dist/browser.test.js +47 -1
  17. package/dist/build-manifest.js +1 -3
  18. package/dist/cli-manifest.json +2573 -989
  19. package/dist/cli.js +42 -2
  20. package/dist/clis/bilibili/download.js +20 -65
  21. package/dist/clis/bilibili/utils.js +2 -1
  22. package/dist/clis/chaoxing/assignments.js +2 -1
  23. package/dist/clis/doubao/ask.d.ts +1 -0
  24. package/dist/clis/doubao/ask.js +35 -0
  25. package/dist/clis/doubao/common.d.ts +23 -0
  26. package/dist/clis/doubao/common.js +564 -0
  27. package/dist/clis/doubao/new.d.ts +1 -0
  28. package/dist/clis/doubao/new.js +20 -0
  29. package/dist/clis/doubao/read.d.ts +1 -0
  30. package/dist/clis/doubao/read.js +19 -0
  31. package/dist/clis/doubao/send.d.ts +1 -0
  32. package/dist/clis/doubao/send.js +22 -0
  33. package/dist/clis/doubao/status.d.ts +1 -0
  34. package/dist/clis/doubao/status.js +24 -0
  35. package/dist/clis/doubao-app/ask.d.ts +1 -0
  36. package/dist/clis/doubao-app/ask.js +53 -0
  37. package/dist/clis/doubao-app/common.d.ts +37 -0
  38. package/dist/clis/doubao-app/common.js +110 -0
  39. package/dist/clis/doubao-app/dump.d.ts +1 -0
  40. package/dist/clis/doubao-app/dump.js +24 -0
  41. package/dist/clis/doubao-app/new.d.ts +1 -0
  42. package/dist/clis/doubao-app/new.js +20 -0
  43. package/dist/clis/doubao-app/read.d.ts +1 -0
  44. package/dist/clis/doubao-app/read.js +18 -0
  45. package/dist/clis/doubao-app/screenshot.d.ts +1 -0
  46. package/dist/clis/doubao-app/screenshot.js +18 -0
  47. package/dist/clis/doubao-app/send.d.ts +1 -0
  48. package/dist/clis/doubao-app/send.js +27 -0
  49. package/dist/clis/doubao-app/status.d.ts +1 -0
  50. package/dist/clis/doubao-app/status.js +16 -0
  51. package/dist/clis/hackernews/ask.yaml +38 -0
  52. package/dist/clis/hackernews/best.yaml +38 -0
  53. package/dist/clis/hackernews/jobs.yaml +36 -0
  54. package/dist/clis/hackernews/new.yaml +38 -0
  55. package/dist/clis/hackernews/search.yaml +44 -0
  56. package/dist/clis/hackernews/show.yaml +38 -0
  57. package/dist/clis/hackernews/top.yaml +3 -1
  58. package/dist/clis/hackernews/user.yaml +25 -0
  59. package/dist/clis/twitter/download.js +13 -97
  60. package/dist/clis/twitter/thread.js +2 -1
  61. package/dist/clis/v2ex/member.yaml +29 -0
  62. package/dist/clis/v2ex/node.yaml +34 -0
  63. package/dist/clis/v2ex/nodes.yaml +31 -0
  64. package/dist/clis/v2ex/replies.yaml +32 -0
  65. package/dist/clis/v2ex/user.yaml +34 -0
  66. package/dist/clis/weibo/search.d.ts +1 -0
  67. package/dist/clis/weibo/search.js +73 -0
  68. package/dist/clis/weixin/download.d.ts +12 -0
  69. package/dist/clis/weixin/download.js +183 -0
  70. package/dist/clis/xiaohongshu/download.js +12 -60
  71. package/dist/clis/xiaohongshu/publish.d.ts +18 -0
  72. package/dist/clis/xiaohongshu/publish.js +352 -0
  73. package/dist/clis/xiaohongshu/search.js +47 -15
  74. package/dist/clis/xiaohongshu/search.test.d.ts +1 -0
  75. package/dist/clis/xiaohongshu/search.test.js +114 -0
  76. package/dist/clis/yollomi/background.d.ts +4 -0
  77. package/dist/clis/yollomi/background.js +45 -0
  78. package/dist/clis/yollomi/edit.d.ts +5 -0
  79. package/dist/clis/yollomi/edit.js +56 -0
  80. package/dist/clis/yollomi/face-swap.d.ts +5 -0
  81. package/dist/clis/yollomi/face-swap.js +43 -0
  82. package/dist/clis/yollomi/generate.d.ts +9 -0
  83. package/dist/clis/yollomi/generate.js +100 -0
  84. package/dist/clis/yollomi/models.d.ts +1 -0
  85. package/dist/clis/yollomi/models.js +33 -0
  86. package/dist/clis/yollomi/object-remover.d.ts +4 -0
  87. package/dist/clis/yollomi/object-remover.js +42 -0
  88. package/dist/clis/yollomi/remove-bg.d.ts +4 -0
  89. package/dist/clis/yollomi/remove-bg.js +38 -0
  90. package/dist/clis/yollomi/restore.d.ts +4 -0
  91. package/dist/clis/yollomi/restore.js +38 -0
  92. package/dist/clis/yollomi/try-on.d.ts +4 -0
  93. package/dist/clis/yollomi/try-on.js +46 -0
  94. package/dist/clis/yollomi/upload.d.ts +7 -0
  95. package/dist/clis/yollomi/upload.js +71 -0
  96. package/dist/clis/yollomi/upscale.d.ts +4 -0
  97. package/dist/clis/yollomi/upscale.js +53 -0
  98. package/dist/clis/yollomi/utils.d.ts +45 -0
  99. package/dist/clis/yollomi/utils.js +180 -0
  100. package/dist/clis/yollomi/video.d.ts +5 -0
  101. package/dist/clis/yollomi/video.js +56 -0
  102. package/dist/clis/zhihu/download.d.ts +1 -5
  103. package/dist/clis/zhihu/download.js +20 -126
  104. package/dist/clis/zhihu/download.test.js +7 -5
  105. package/dist/clis/zhihu/question.js +2 -1
  106. package/dist/commanderAdapter.js +4 -6
  107. package/dist/constants.d.ts +2 -0
  108. package/dist/constants.js +2 -0
  109. package/dist/daemon.js +7 -3
  110. package/dist/discovery.js +10 -10
  111. package/dist/doctor.js +2 -1
  112. package/dist/download/article-download.d.ts +59 -0
  113. package/dist/download/article-download.js +178 -0
  114. package/dist/download/media-download.d.ts +49 -0
  115. package/dist/download/media-download.js +112 -0
  116. package/dist/errors.d.ts +23 -2
  117. package/dist/errors.js +58 -2
  118. package/dist/errors.test.d.ts +1 -0
  119. package/dist/errors.test.js +59 -0
  120. package/dist/execution.js +9 -10
  121. package/dist/explore.js +4 -2
  122. package/dist/external.d.ts +15 -0
  123. package/dist/external.js +48 -2
  124. package/dist/external.test.d.ts +1 -0
  125. package/dist/external.test.js +64 -0
  126. package/dist/main.js +10 -0
  127. package/dist/plugin.d.ts +4 -0
  128. package/dist/plugin.js +45 -23
  129. package/dist/plugin.test.js +6 -1
  130. package/dist/record.d.ts +47 -0
  131. package/dist/record.js +545 -0
  132. package/dist/registry.d.ts +7 -2
  133. package/dist/registry.js +2 -6
  134. package/dist/runtime.d.ts +3 -1
  135. package/dist/runtime.js +10 -3
  136. package/dist/validate.js +1 -3
  137. package/docs/.vitepress/config.mts +1 -0
  138. package/docs/adapters/browser/douban.md +18 -8
  139. package/docs/adapters/browser/doubao.md +35 -0
  140. package/docs/adapters/browser/hackernews.md +20 -4
  141. package/docs/adapters/browser/tiktok.md +1 -1
  142. package/docs/adapters/browser/v2ex.md +31 -10
  143. package/docs/adapters/browser/weibo.md +4 -0
  144. package/docs/adapters/browser/weixin.md +33 -0
  145. package/docs/adapters/browser/wikipedia.md +0 -9
  146. package/docs/adapters/browser/xiaohongshu.md +8 -6
  147. package/docs/adapters/browser/yollomi.md +69 -0
  148. package/docs/adapters/desktop/antigravity.md +0 -3
  149. package/docs/adapters/desktop/doubao-app.md +35 -0
  150. package/docs/adapters/index.md +19 -8
  151. package/docs/advanced/download.md +4 -0
  152. package/package.json +3 -1
  153. package/src/browser/cdp.ts +9 -1
  154. package/src/browser/daemon-client.ts +4 -3
  155. package/src/browser/discover.ts +2 -1
  156. package/src/browser/errors.ts +18 -11
  157. package/src/browser/index.ts +1 -0
  158. package/src/browser/page.ts +11 -0
  159. package/src/browser/stealth.ts +142 -0
  160. package/src/browser.test.ts +51 -1
  161. package/src/build-manifest.ts +1 -3
  162. package/src/cli.ts +45 -2
  163. package/src/clis/bilibili/download.ts +25 -83
  164. package/src/clis/bilibili/utils.ts +2 -1
  165. package/src/clis/chaoxing/assignments.ts +2 -1
  166. package/src/clis/doubao/ask.ts +40 -0
  167. package/src/clis/doubao/common.ts +619 -0
  168. package/src/clis/doubao/new.ts +22 -0
  169. package/src/clis/doubao/read.ts +20 -0
  170. package/src/clis/doubao/send.ts +25 -0
  171. package/src/clis/doubao/status.ts +27 -0
  172. package/src/clis/doubao-app/ask.ts +60 -0
  173. package/src/clis/doubao-app/common.ts +116 -0
  174. package/src/clis/doubao-app/dump.ts +28 -0
  175. package/src/clis/doubao-app/new.ts +21 -0
  176. package/src/clis/doubao-app/read.ts +21 -0
  177. package/src/clis/doubao-app/screenshot.ts +19 -0
  178. package/src/clis/doubao-app/send.ts +30 -0
  179. package/src/clis/doubao-app/status.ts +17 -0
  180. package/src/clis/hackernews/ask.yaml +38 -0
  181. package/src/clis/hackernews/best.yaml +38 -0
  182. package/src/clis/hackernews/jobs.yaml +36 -0
  183. package/src/clis/hackernews/new.yaml +38 -0
  184. package/src/clis/hackernews/search.yaml +44 -0
  185. package/src/clis/hackernews/show.yaml +38 -0
  186. package/src/clis/hackernews/top.yaml +3 -1
  187. package/src/clis/hackernews/user.yaml +25 -0
  188. package/src/clis/twitter/download.ts +13 -111
  189. package/src/clis/twitter/thread.ts +2 -1
  190. package/src/clis/v2ex/member.yaml +29 -0
  191. package/src/clis/v2ex/node.yaml +34 -0
  192. package/src/clis/v2ex/nodes.yaml +31 -0
  193. package/src/clis/v2ex/replies.yaml +32 -0
  194. package/src/clis/v2ex/user.yaml +34 -0
  195. package/src/clis/weibo/search.ts +78 -0
  196. package/src/clis/weixin/download.ts +199 -0
  197. package/src/clis/xiaohongshu/download.ts +12 -71
  198. package/src/clis/xiaohongshu/publish.ts +392 -0
  199. package/src/clis/xiaohongshu/search.test.ts +134 -0
  200. package/src/clis/xiaohongshu/search.ts +49 -15
  201. package/src/clis/yollomi/background.ts +48 -0
  202. package/src/clis/yollomi/edit.ts +58 -0
  203. package/src/clis/yollomi/face-swap.ts +45 -0
  204. package/src/clis/yollomi/generate.ts +95 -0
  205. package/src/clis/yollomi/models.ts +38 -0
  206. package/src/clis/yollomi/object-remover.ts +44 -0
  207. package/src/clis/yollomi/remove-bg.ts +40 -0
  208. package/src/clis/yollomi/restore.ts +40 -0
  209. package/src/clis/yollomi/try-on.ts +48 -0
  210. package/src/clis/yollomi/upload.ts +78 -0
  211. package/src/clis/yollomi/upscale.ts +49 -0
  212. package/src/clis/yollomi/utils.ts +202 -0
  213. package/src/clis/yollomi/video.ts +61 -0
  214. package/src/clis/zhihu/download.test.ts +7 -5
  215. package/src/clis/zhihu/download.ts +23 -158
  216. package/src/clis/zhihu/question.ts +2 -1
  217. package/src/commanderAdapter.ts +4 -7
  218. package/src/constants.ts +3 -0
  219. package/src/daemon.ts +7 -3
  220. package/src/discovery.ts +26 -26
  221. package/src/doctor.ts +2 -1
  222. package/src/download/article-download.ts +272 -0
  223. package/src/download/media-download.ts +178 -0
  224. package/src/errors.test.ts +79 -0
  225. package/src/errors.ts +92 -2
  226. package/src/execution.ts +14 -10
  227. package/src/explore.ts +4 -2
  228. package/src/external.test.ts +88 -0
  229. package/src/external.ts +56 -2
  230. package/src/generate.ts +2 -1
  231. package/src/main.ts +10 -0
  232. package/src/plugin.test.ts +7 -1
  233. package/src/plugin.ts +49 -25
  234. package/src/record.ts +617 -0
  235. package/src/registry.ts +9 -5
  236. package/src/runtime.ts +16 -4
  237. package/src/validate.ts +1 -3
  238. package/tests/e2e/browser-auth.test.ts +10 -1
  239. package/tests/e2e/browser-public.test.ts +13 -8
  240. package/tests/e2e/public-commands.test.ts +209 -21
  241. package/tests/smoke/api-health.test.ts +65 -6
@@ -15,6 +15,7 @@ import type { BrowserCookie, IPage, ScreenshotOptions, SnapshotOptions, WaitOpti
15
15
  import { sendCommand } from './daemon-client.js';
16
16
  import { wrapForEval } from './utils.js';
17
17
  import { generateSnapshotJs, scrollToRefJs, getFormStateJs } from './dom-snapshot.js';
18
+ import { generateStealthJs } from './stealth.js';
18
19
  import {
19
20
  clickJs,
20
21
  typeTextJs,
@@ -54,6 +55,16 @@ export class Page implements IPage {
54
55
  if (result?.tabId) {
55
56
  this._tabId = result.tabId;
56
57
  }
58
+ // Inject stealth anti-detection patches (guard flag prevents double-injection).
59
+ try {
60
+ await sendCommand('exec', {
61
+ code: generateStealthJs(),
62
+ ...this._workspaceOpt(),
63
+ ...this._tabOpt(),
64
+ });
65
+ } catch {
66
+ // Non-fatal: stealth is best-effort
67
+ }
57
68
  // Smart settle: use DOM stability detection instead of fixed sleep.
58
69
  // settleMs is now a timeout cap (default 1000ms), not a fixed wait.
59
70
  if (options?.waitUntil !== 'none') {
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Stealth anti-detection module.
3
+ *
4
+ * Generates JS code that patches browser globals to hide automation
5
+ * fingerprints (e.g. navigator.webdriver, missing chrome object, empty
6
+ * plugin list). Injected before page scripts run so that websites cannot
7
+ * detect CDP / extension-based control.
8
+ *
9
+ * Inspired by puppeteer-extra-plugin-stealth.
10
+ */
11
+
12
+ /** Guard flag set on `window` to prevent double-injection. */
13
+ export const STEALTH_GUARD = '__opencli_stealth_applied';
14
+
15
+ /**
16
+ * Return a self-contained JS string that, when evaluated in a page context,
17
+ * applies all stealth patches. Safe to call multiple times — the guard flag
18
+ * ensures patches are applied only once.
19
+ */
20
+ export function generateStealthJs(): string {
21
+ return `
22
+ (() => {
23
+ // Guard: skip if already applied
24
+ if (window.${STEALTH_GUARD}) return 'skipped';
25
+ // Use defineProperty so the guard flag is non-enumerable (not a detection vector).
26
+ Object.defineProperty(window, '${STEALTH_GUARD}', { value: true, configurable: true });
27
+
28
+ // 1. navigator.webdriver → undefined
29
+ // Most common check; Playwright/Puppeteer/CDP set this to true.
30
+ try {
31
+ Object.defineProperty(navigator, 'webdriver', {
32
+ get: () => undefined,
33
+ configurable: true,
34
+ });
35
+ } catch {}
36
+
37
+ // 2. window.chrome stub
38
+ // Real Chrome exposes window.chrome with runtime, loadTimes, csi.
39
+ // Headless/automated Chrome may not have it.
40
+ try {
41
+ if (!window.chrome) {
42
+ window.chrome = {
43
+ runtime: {
44
+ onConnect: { addListener: () => {}, removeListener: () => {} },
45
+ onMessage: { addListener: () => {}, removeListener: () => {} },
46
+ },
47
+ loadTimes: () => ({}),
48
+ csi: () => ({}),
49
+ };
50
+ }
51
+ } catch {}
52
+
53
+ // 3. navigator.plugins — fake population only if empty
54
+ // Real user browser already has plugins; only patch in automated/headless
55
+ // contexts where the list is empty (overwriting real plugins with fakes
56
+ // would be counterproductive and detectable).
57
+ try {
58
+ if (!navigator.plugins || navigator.plugins.length === 0) {
59
+ const fakePlugins = [
60
+ { name: 'PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
61
+ { name: 'Chrome PDF Viewer', filename: 'internal-pdf-viewer', description: '' },
62
+ { name: 'Chromium PDF Viewer', filename: 'internal-pdf-viewer', description: '' },
63
+ { name: 'Microsoft Edge PDF Viewer', filename: 'internal-pdf-viewer', description: '' },
64
+ { name: 'WebKit built-in PDF', filename: 'internal-pdf-viewer', description: '' },
65
+ ];
66
+ fakePlugins.item = (i) => fakePlugins[i] || null;
67
+ fakePlugins.namedItem = (n) => fakePlugins.find(p => p.name === n) || null;
68
+ fakePlugins.refresh = () => {};
69
+ Object.defineProperty(navigator, 'plugins', {
70
+ get: () => fakePlugins,
71
+ configurable: true,
72
+ });
73
+ }
74
+ } catch {}
75
+
76
+ // 4. navigator.languages — guarantee non-empty
77
+ // Some automated contexts return undefined or empty array.
78
+ try {
79
+ if (!navigator.languages || navigator.languages.length === 0) {
80
+ Object.defineProperty(navigator, 'languages', {
81
+ get: () => ['en-US', 'en'],
82
+ configurable: true,
83
+ });
84
+ }
85
+ } catch {}
86
+
87
+ // 5. Permissions.query — normalize notification permission
88
+ // Headless Chrome throws on Permissions.query({ name: 'notifications' }).
89
+ try {
90
+ const origQuery = window.Permissions?.prototype?.query;
91
+ if (origQuery) {
92
+ window.Permissions.prototype.query = function (parameters) {
93
+ if (parameters?.name === 'notifications') {
94
+ return Promise.resolve({ state: Notification.permission, onchange: null });
95
+ }
96
+ return origQuery.call(this, parameters);
97
+ };
98
+ }
99
+ } catch {}
100
+
101
+ // 6. Clean automation artifacts
102
+ // Remove properties left by Playwright, Puppeteer, or CDP injection.
103
+ try {
104
+ delete window.__playwright;
105
+ delete window.__puppeteer;
106
+ // ChromeDriver injects cdc_ prefixed globals; the suffix varies by version,
107
+ // so scan window for any matching property rather than hardcoding names.
108
+ for (const prop of Object.getOwnPropertyNames(window)) {
109
+ if (prop.startsWith('cdc_') || prop.startsWith('__cdc_')) {
110
+ try { delete window[prop]; } catch {}
111
+ }
112
+ }
113
+ } catch {}
114
+
115
+ // 7. CDP stack trace cleanup
116
+ // Runtime.evaluate injects scripts whose source URLs appear in Error
117
+ // stack traces (e.g. __puppeteer_evaluation_script__, pptr:, debugger://).
118
+ // Websites detect automation by doing: new Error().stack and inspecting it.
119
+ // We override the stack property getter on Error.prototype to filter them.
120
+ // Note: Error.prepareStackTrace is V8/Node-only and not available in
121
+ // browser page context, so we use a property descriptor approach instead.
122
+ try {
123
+ const _origDescriptor = Object.getOwnPropertyDescriptor(Error.prototype, 'stack');
124
+ const _cdpPatterns = ['puppeteer_evaluation_script', 'pptr:', 'debugger://', '__opencli'];
125
+ if (_origDescriptor && _origDescriptor.get) {
126
+ Object.defineProperty(Error.prototype, 'stack', {
127
+ get: function () {
128
+ const raw = _origDescriptor.get.call(this);
129
+ if (typeof raw !== 'string') return raw;
130
+ return raw.split('\\n').filter(line =>
131
+ !_cdpPatterns.some(p => line.includes(p))
132
+ ).join('\\n');
133
+ },
134
+ configurable: true,
135
+ });
136
+ }
137
+ } catch {}
138
+
139
+ return 'applied';
140
+ })()
141
+ `;
142
+ }
@@ -1,5 +1,6 @@
1
1
  import { afterEach, describe, it, expect, vi } from 'vitest';
2
- import { BrowserBridge, __test__ } from './browser/index.js';
2
+ import { BrowserBridge, __test__, generateStealthJs } from './browser/index.js';
3
+ import { STEALTH_GUARD } from './browser/stealth.js';
3
4
  import * as daemonClient from './browser/daemon-client.js';
4
5
 
5
6
  describe('browser helpers', () => {
@@ -133,3 +134,52 @@ describe('BrowserBridge state', () => {
133
134
  await expect(mcp.connect()).rejects.toThrow('Browser Extension is not connected');
134
135
  });
135
136
  });
137
+
138
+ describe('stealth anti-detection', () => {
139
+ it('generates non-empty JS string', () => {
140
+ const js = generateStealthJs();
141
+ expect(typeof js).toBe('string');
142
+ expect(js.length).toBeGreaterThan(100);
143
+ });
144
+
145
+ it('contains all 7 anti-detection patches', () => {
146
+ const js = generateStealthJs();
147
+ // 1. webdriver
148
+ expect(js).toContain('navigator');
149
+ expect(js).toContain('webdriver');
150
+ // 2. chrome stub
151
+ expect(js).toContain('window.chrome');
152
+ // 3. plugins
153
+ expect(js).toContain('plugins');
154
+ expect(js).toContain('PDF Viewer');
155
+ // 4. languages
156
+ expect(js).toContain('languages');
157
+ // 5. permissions
158
+ expect(js).toContain('Permissions');
159
+ expect(js).toContain('notifications');
160
+ // 6. automation artifacts (dynamic cdc_ scan)
161
+ expect(js).toContain('__playwright');
162
+ expect(js).toContain('__puppeteer');
163
+ expect(js).toContain('getOwnPropertyNames');
164
+ expect(js).toContain('cdc_');
165
+ // 7. CDP stack trace cleanup
166
+ expect(js).toContain('Error.prototype');
167
+ expect(js).toContain('puppeteer_evaluation_script');
168
+ expect(js).toContain('getOwnPropertyDescriptor');
169
+ });
170
+
171
+ it('includes guard flag to prevent double-injection', () => {
172
+ const js = generateStealthJs();
173
+ expect(js).toContain(STEALTH_GUARD);
174
+ // Guard should check early and return 'skipped'
175
+ expect(js).toContain("return 'skipped'");
176
+ // Normal path returns 'applied'
177
+ expect(js).toContain("return 'applied'");
178
+ });
179
+
180
+ it('generates syntactically valid JS', () => {
181
+ const js = generateStealthJs();
182
+ // Should not throw when parsed
183
+ expect(() => new Function(js)).not.toThrow();
184
+ });
185
+ });
@@ -13,6 +13,7 @@ import * as fs from 'node:fs';
13
13
  import * as path from 'node:path';
14
14
  import { fileURLToPath, pathToFileURL } from 'node:url';
15
15
  import yaml from 'js-yaml';
16
+ import { getErrorMessage } from './errors.js';
16
17
 
17
18
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
19
  const CLIS_DIR = path.resolve(__dirname, 'clis');
@@ -73,9 +74,6 @@ function isRecord(value: unknown): value is Record<string, unknown> {
73
74
  return typeof value === 'object' && value !== null && !Array.isArray(value);
74
75
  }
75
76
 
76
- function getErrorMessage(error: unknown): string {
77
- return error instanceof Error ? error.message : String(error);
78
- }
79
77
 
80
78
  function extractBalancedBlock(
81
79
  source: string,
package/src/cli.ts CHANGED
@@ -183,6 +183,30 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
183
183
  process.exitCode = r.ok ? 0 : 1;
184
184
  });
185
185
 
186
+ // ── Built-in: record ─────────────────────────────────────────────────────
187
+
188
+ program
189
+ .command('record')
190
+ .description('Record API calls from a live browser session → generate YAML candidates')
191
+ .argument('<url>', 'URL to open and record')
192
+ .option('--site <name>', 'Site name (inferred from URL if omitted)')
193
+ .option('--out <dir>', 'Output directory for candidates')
194
+ .option('--poll <ms>', 'Poll interval in milliseconds', '2000')
195
+ .option('--timeout <ms>', 'Auto-stop after N milliseconds (default: 60000)', '60000')
196
+ .action(async (url, opts) => {
197
+ const { recordSession, renderRecordSummary } = await import('./record.js');
198
+ const result = await recordSession({
199
+ BrowserFactory: getBrowserFactory(),
200
+ url,
201
+ site: opts.site,
202
+ outDir: opts.out,
203
+ pollMs: parseInt(opts.poll, 10),
204
+ timeoutMs: parseInt(opts.timeout, 10),
205
+ });
206
+ console.log(renderRecordSummary(result));
207
+ process.exitCode = result.candidateCount > 0 ? 0 : 1;
208
+ });
209
+
186
210
  program
187
211
  .command('cascade')
188
212
  .description('Strategy cascade: find simplest working strategy')
@@ -233,10 +257,11 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
233
257
  .argument('<source>', 'Plugin source (e.g. github:user/repo)')
234
258
  .action(async (source: string) => {
235
259
  const { installPlugin } = await import('./plugin.js');
260
+ const { discoverPlugins } = await import('./discovery.js');
236
261
  try {
237
262
  const name = installPlugin(source);
238
- console.log(chalk.green(`✅ Plugin "${name}" installed successfully.`));
239
- console.log(chalk.dim(` Restart opencli to use the new commands.`));
263
+ await discoverPlugins();
264
+ console.log(chalk.green(`✅ Plugin "${name}" installed successfully. Commands are ready to use.`));
240
265
  } catch (err: any) {
241
266
  console.error(chalk.red(`Error: ${err.message}`));
242
267
  process.exitCode = 1;
@@ -258,6 +283,24 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
258
283
  }
259
284
  });
260
285
 
286
+ pluginCmd
287
+ .command('update')
288
+ .description('Update a plugin to the latest version')
289
+ .argument('<name>', 'Plugin name')
290
+ .action(async (name: string) => {
291
+ const { updatePlugin } = await import('./plugin.js');
292
+ const { discoverPlugins } = await import('./discovery.js');
293
+ try {
294
+ updatePlugin(name);
295
+ await discoverPlugins();
296
+ console.log(chalk.green(`✅ Plugin "${name}" updated successfully.`));
297
+ } catch (err: any) {
298
+ console.error(chalk.red(`Error: ${err.message}`));
299
+ process.exitCode = 1;
300
+ }
301
+ });
302
+
303
+
261
304
  pluginCmd
262
305
  .command('list')
263
306
  .description('List installed plugins')
@@ -8,18 +8,9 @@
8
8
  * - yt-dlp must be installed: pip install yt-dlp
9
9
  */
10
10
 
11
- import * as fs from 'node:fs';
12
- import * as path from 'node:path';
13
11
  import { cli, Strategy } from '../../registry.js';
14
- import {
15
- ytdlpDownload,
16
- checkYtdlp,
17
- sanitizeFilename,
18
- getTempDir,
19
- exportCookiesToNetscape,
20
- formatCookieHeader,
21
- } from '../../download/index.js';
22
- import { DownloadProgressTracker, formatBytes } from '../../download/progress.js';
12
+ import { checkYtdlp, sanitizeFilename } from '../../download/index.js';
13
+ import { downloadMedia } from '../../download/media-download.js';
23
14
 
24
15
  cli({
25
16
  site: 'bilibili',
@@ -63,21 +54,8 @@ cli({
63
54
 
64
55
  const title = sanitizeFilename(data?.title || 'video');
65
56
 
66
- // Extract cookies for authenticated downloads
67
- const cookies = await page.getCookies({ domain: 'bilibili.com' });
68
- const cookieString = formatCookieHeader(cookies);
69
-
70
- // Create output directory
71
- fs.mkdirSync(output, { recursive: true });
72
-
73
- // Export cookies to Netscape format for yt-dlp
74
- let cookiesFile: string | undefined;
75
- if (cookies.length > 0) {
76
- const tempDir = getTempDir();
77
- fs.mkdirSync(tempDir, { recursive: true });
78
- cookiesFile = path.join(tempDir, `bilibili_cookies_${Date.now()}.txt`);
79
- exportCookiesToNetscape(cookies, cookiesFile);
80
- }
57
+ // Extract cookies for yt-dlp
58
+ const browserCookies = await page.getCookies({ domain: 'bilibili.com' });
81
59
 
82
60
  // Build yt-dlp format string based on quality
83
61
  let format = 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best';
@@ -89,62 +67,26 @@ cli({
89
67
  format = 'bestvideo[height<=480][ext=mp4]+bestaudio[ext=m4a]/best[height<=480]';
90
68
  }
91
69
 
92
- const destPath = path.join(output, `${bvid}_${title}.mp4`);
93
-
94
- const tracker = new DownloadProgressTracker(1, true);
95
- const progressBar = tracker.onFileStart(`${bvid}.mp4`, 0);
96
-
97
- try {
98
- const result = await ytdlpDownload(
99
- `https://www.bilibili.com/video/${bvid}`,
100
- destPath,
101
- {
102
- cookiesFile,
103
- format,
104
- extraArgs: [
105
- '--merge-output-format', 'mp4',
106
- '--embed-thumbnail',
107
- ],
108
- onProgress: (percent) => {
109
- if (progressBar) progressBar.update(percent, 100);
110
- },
111
- },
112
- );
113
-
114
- if (progressBar) {
115
- progressBar.complete(result.success, result.success ? formatBytes(result.size) : undefined);
116
- }
117
-
118
- tracker.onFileComplete(result.success);
119
- tracker.finish();
120
-
121
- // Cleanup cookies file
122
- if (cookiesFile && fs.existsSync(cookiesFile)) {
123
- fs.unlinkSync(cookiesFile);
124
- }
125
-
126
- return [{
127
- bvid,
128
- title: data?.title || 'video',
129
- status: result.success ? 'success' : 'failed',
130
- size: result.success ? formatBytes(result.size) : (result.error || 'unknown error'),
131
- }];
132
- } catch (err: any) {
133
- if (progressBar) progressBar.fail(err.message);
134
- tracker.onFileComplete(false);
135
- tracker.finish();
136
-
137
- // Cleanup cookies file
138
- if (cookiesFile && fs.existsSync(cookiesFile)) {
139
- fs.unlinkSync(cookiesFile);
140
- }
141
-
142
- return [{
143
- bvid,
144
- title: data?.title || 'video',
145
- status: 'failed',
146
- size: err.message,
147
- }];
148
- }
70
+ const videoUrl = `https://www.bilibili.com/video/${bvid}`;
71
+ const filename = `${bvid}_${title}.mp4`;
72
+
73
+ const results = await downloadMedia(
74
+ [{ type: 'video-ytdlp', url: videoUrl, filename }],
75
+ {
76
+ output,
77
+ browserCookies,
78
+ filenamePrefix: bvid,
79
+ ytdlpExtraArgs: ['-f', format, '--merge-output-format', 'mp4', '--embed-thumbnail'],
80
+ },
81
+ );
82
+
83
+ // Map results to bilibili-specific columns
84
+ const r = results[0] || { status: 'failed', size: '-' };
85
+ return [{
86
+ bvid,
87
+ title: data?.title || 'video',
88
+ status: r.status,
89
+ size: r.size,
90
+ }];
149
91
  },
150
92
  });
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  import type { IPage } from '../../types.js';
6
+ import { AuthRequiredError } from '../../errors.js';
6
7
 
7
8
  const MIXIN_KEY_ENC_TAB = [
8
9
  46,47,18,2,53,8,23,32,15,50,10,31,58,3,45,35,27,43,5,49,
@@ -98,7 +99,7 @@ export async function fetchJson(page: IPage, url: string): Promise<any> {
98
99
  export async function getSelfUid(page: IPage): Promise<string> {
99
100
  const nav = await getNavData(page);
100
101
  const mid = nav?.data?.mid;
101
- if (!mid) throw new Error('Not logged in to Bilibili');
102
+ if (!mid) throw new AuthRequiredError('bilibili.com');
102
103
  return String(mid);
103
104
  }
104
105
 
@@ -1,4 +1,5 @@
1
1
  import { cli, Strategy } from '../../registry.js';
2
+ import { AuthRequiredError } from '../../errors.js';
2
3
  import {
3
4
  getCourses, initSession, enterCourse, getTabIframeUrl,
4
5
  parseAssignmentsFromDom, sleep,
@@ -33,7 +34,7 @@ cli({
33
34
 
34
35
  // 2. Get courses
35
36
  const courses = await getCourses(page);
36
- if (!courses.length) throw new Error('未获取到课程列表,请确认已登录学习通');
37
+ if (!courses.length) throw new AuthRequiredError('mooc2-ans.chaoxing.com', '未获取到课程列表');
37
38
 
38
39
  const filtered = courseFilter
39
40
  ? courses.filter(c => c.title.includes(courseFilter))
@@ -0,0 +1,40 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import type { IPage } from '../../types.js';
3
+ import { DOUBAO_DOMAIN, getDoubaoTranscriptLines, getDoubaoVisibleTurns, sendDoubaoMessage, waitForDoubaoResponse } from './common.js';
4
+
5
+ export const askCommand = cli({
6
+ site: 'doubao',
7
+ name: 'ask',
8
+ description: 'Send a prompt and wait for the Doubao response',
9
+ domain: DOUBAO_DOMAIN,
10
+ strategy: Strategy.COOKIE,
11
+ browser: true,
12
+ navigateBefore: false,
13
+ timeoutSeconds: 180,
14
+ args: [
15
+ { name: 'text', required: true, positional: true, help: 'Prompt to send' },
16
+ { name: 'timeout', required: false, help: 'Max seconds to wait (default: 60)', default: '60' },
17
+ ],
18
+ columns: ['Role', 'Text'],
19
+ func: async (page: IPage, kwargs: any) => {
20
+ const text = kwargs.text as string;
21
+ const timeout = parseInt(kwargs.timeout as string, 10) || 60;
22
+ const beforeTurns = await getDoubaoVisibleTurns(page);
23
+ const beforeLines = await getDoubaoTranscriptLines(page);
24
+
25
+ await sendDoubaoMessage(page, text);
26
+ const response = await waitForDoubaoResponse(page, beforeLines, beforeTurns, text, timeout);
27
+
28
+ if (!response) {
29
+ return [
30
+ { Role: 'User', Text: text },
31
+ { Role: 'System', Text: `No response within ${timeout}s. Doubao may still be generating.` },
32
+ ];
33
+ }
34
+
35
+ return [
36
+ { Role: 'User', Text: text },
37
+ { Role: 'Assistant', Text: response },
38
+ ];
39
+ },
40
+ });