@jackwener/opencli 0.9.6 → 1.0.0

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 (307) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.yml +83 -0
  2. package/.github/ISSUE_TEMPLATE/config.yml +8 -0
  3. package/.github/ISSUE_TEMPLATE/feature_request.yml +42 -0
  4. package/.github/ISSUE_TEMPLATE/new_site_adapter.yml +57 -0
  5. package/.github/dependabot.yml +27 -0
  6. package/.github/pull_request_template.md +24 -0
  7. package/.github/workflows/ci.yml +14 -8
  8. package/.github/workflows/e2e-headed.yml +6 -2
  9. package/.github/workflows/pkg-pr-new.yml +2 -2
  10. package/.github/workflows/release-please.yml +25 -0
  11. package/.github/workflows/release.yml +2 -2
  12. package/.github/workflows/security.yml +36 -0
  13. package/CDP.md +1 -1
  14. package/CDP.zh-CN.md +1 -1
  15. package/CLI-ELECTRON.md +89 -36
  16. package/CLI-EXPLORER.md +4 -4
  17. package/CONTRIBUTING.md +167 -0
  18. package/README.md +113 -89
  19. package/README.zh-CN.md +114 -91
  20. package/SKILL.md +10 -8
  21. package/TESTING.md +7 -7
  22. package/dist/browser/daemon-client.d.ts +37 -0
  23. package/dist/browser/daemon-client.js +82 -0
  24. package/dist/browser/discover.d.ts +11 -34
  25. package/dist/browser/discover.js +15 -190
  26. package/dist/browser/errors.d.ts +6 -20
  27. package/dist/browser/errors.js +24 -63
  28. package/dist/browser/index.d.ts +2 -11
  29. package/dist/browser/index.js +5 -11
  30. package/dist/browser/mcp.d.ts +9 -18
  31. package/dist/browser/mcp.js +70 -284
  32. package/dist/browser/page.d.ts +28 -6
  33. package/dist/browser/page.js +210 -85
  34. package/dist/browser.test.js +4 -202
  35. package/dist/build-manifest.d.ts +26 -0
  36. package/dist/build-manifest.js +132 -60
  37. package/dist/build-manifest.test.d.ts +1 -0
  38. package/dist/build-manifest.test.js +26 -0
  39. package/dist/cli-manifest.json +1582 -29
  40. package/dist/clis/bilibili/download.d.ts +10 -0
  41. package/dist/clis/bilibili/download.js +135 -0
  42. package/dist/clis/chatwise/ask.d.ts +1 -0
  43. package/dist/clis/chatwise/ask.js +76 -0
  44. package/dist/clis/chatwise/export.d.ts +1 -0
  45. package/dist/clis/chatwise/export.js +46 -0
  46. package/dist/clis/chatwise/history.d.ts +1 -0
  47. package/dist/clis/chatwise/history.js +43 -0
  48. package/dist/clis/chatwise/model.d.ts +1 -0
  49. package/dist/clis/chatwise/model.js +81 -0
  50. package/dist/clis/chatwise/new.d.ts +1 -0
  51. package/dist/clis/chatwise/new.js +18 -0
  52. package/dist/clis/chatwise/read.d.ts +1 -0
  53. package/dist/clis/chatwise/read.js +39 -0
  54. package/dist/clis/chatwise/screenshot.d.ts +1 -0
  55. package/dist/clis/chatwise/screenshot.js +27 -0
  56. package/dist/clis/chatwise/send.d.ts +1 -0
  57. package/dist/clis/chatwise/send.js +45 -0
  58. package/dist/clis/chatwise/status.d.ts +1 -0
  59. package/dist/clis/chatwise/status.js +22 -0
  60. package/dist/clis/discord-app/channels.d.ts +1 -0
  61. package/dist/clis/discord-app/channels.js +45 -0
  62. package/dist/clis/discord-app/members.d.ts +1 -0
  63. package/dist/clis/discord-app/members.js +38 -0
  64. package/dist/clis/discord-app/read.d.ts +1 -0
  65. package/dist/clis/discord-app/read.js +45 -0
  66. package/dist/clis/discord-app/search.d.ts +1 -0
  67. package/dist/clis/discord-app/search.js +56 -0
  68. package/dist/clis/discord-app/send.d.ts +1 -0
  69. package/dist/clis/discord-app/send.js +27 -0
  70. package/dist/clis/discord-app/servers.d.ts +1 -0
  71. package/dist/clis/discord-app/servers.js +36 -0
  72. package/dist/clis/discord-app/status.d.ts +1 -0
  73. package/dist/clis/discord-app/status.js +16 -0
  74. package/dist/clis/feishu/new.d.ts +1 -0
  75. package/dist/clis/feishu/new.js +27 -0
  76. package/dist/clis/feishu/read.d.ts +1 -0
  77. package/dist/clis/feishu/read.js +40 -0
  78. package/dist/clis/feishu/search.d.ts +1 -0
  79. package/dist/clis/feishu/search.js +30 -0
  80. package/dist/clis/feishu/send.d.ts +1 -0
  81. package/dist/clis/feishu/send.js +39 -0
  82. package/dist/clis/feishu/status.d.ts +1 -0
  83. package/dist/clis/feishu/status.js +28 -0
  84. package/dist/clis/grok/ask.d.ts +1 -0
  85. package/dist/clis/grok/ask.js +82 -0
  86. package/dist/clis/grok/debug.d.ts +1 -0
  87. package/dist/clis/grok/debug.js +45 -0
  88. package/dist/clis/jimeng/generate.yaml +84 -0
  89. package/dist/clis/jimeng/history.yaml +47 -0
  90. package/dist/clis/linux-do/categories.yaml +41 -0
  91. package/dist/clis/linux-do/category.yaml +49 -0
  92. package/dist/clis/linux-do/hot.yaml +50 -0
  93. package/dist/clis/linux-do/latest.yaml +40 -0
  94. package/dist/clis/linux-do/search.yaml +45 -0
  95. package/dist/clis/linux-do/topic.yaml +38 -0
  96. package/dist/clis/neteasemusic/like.d.ts +1 -0
  97. package/dist/clis/neteasemusic/like.js +25 -0
  98. package/dist/clis/neteasemusic/lyrics.d.ts +1 -0
  99. package/dist/clis/neteasemusic/lyrics.js +47 -0
  100. package/dist/clis/neteasemusic/next.d.ts +1 -0
  101. package/dist/clis/neteasemusic/next.js +26 -0
  102. package/dist/clis/neteasemusic/play.d.ts +1 -0
  103. package/dist/clis/neteasemusic/play.js +26 -0
  104. package/dist/clis/neteasemusic/playing.d.ts +1 -0
  105. package/dist/clis/neteasemusic/playing.js +59 -0
  106. package/dist/clis/neteasemusic/playlist.d.ts +1 -0
  107. package/dist/clis/neteasemusic/playlist.js +46 -0
  108. package/dist/clis/neteasemusic/prev.d.ts +1 -0
  109. package/dist/clis/neteasemusic/prev.js +25 -0
  110. package/dist/clis/neteasemusic/search.d.ts +1 -0
  111. package/dist/clis/neteasemusic/search.js +52 -0
  112. package/dist/clis/neteasemusic/status.d.ts +1 -0
  113. package/dist/clis/neteasemusic/status.js +16 -0
  114. package/dist/clis/neteasemusic/volume.d.ts +1 -0
  115. package/dist/clis/neteasemusic/volume.js +54 -0
  116. package/dist/clis/notion/export.d.ts +1 -0
  117. package/dist/clis/notion/export.js +31 -0
  118. package/dist/clis/notion/favorites.d.ts +1 -0
  119. package/dist/clis/notion/favorites.js +84 -0
  120. package/dist/clis/notion/new.d.ts +1 -0
  121. package/dist/clis/notion/new.js +34 -0
  122. package/dist/clis/notion/read.d.ts +1 -0
  123. package/dist/clis/notion/read.js +30 -0
  124. package/dist/clis/notion/search.d.ts +1 -0
  125. package/dist/clis/notion/search.js +46 -0
  126. package/dist/clis/notion/sidebar.d.ts +1 -0
  127. package/dist/clis/notion/sidebar.js +41 -0
  128. package/dist/clis/notion/status.d.ts +1 -0
  129. package/dist/clis/notion/status.js +16 -0
  130. package/dist/clis/notion/write.d.ts +1 -0
  131. package/dist/clis/notion/write.js +40 -0
  132. package/dist/clis/twitter/download.d.ts +8 -0
  133. package/dist/clis/twitter/download.js +204 -0
  134. package/dist/clis/wechat/chats.d.ts +1 -0
  135. package/dist/clis/wechat/chats.js +28 -0
  136. package/dist/clis/wechat/contacts.d.ts +1 -0
  137. package/dist/clis/wechat/contacts.js +28 -0
  138. package/dist/clis/wechat/read.d.ts +1 -0
  139. package/dist/clis/wechat/read.js +58 -0
  140. package/dist/clis/wechat/search.d.ts +1 -0
  141. package/dist/clis/wechat/search.js +31 -0
  142. package/dist/clis/wechat/send.d.ts +1 -0
  143. package/dist/clis/wechat/send.js +42 -0
  144. package/dist/clis/wechat/status.d.ts +1 -0
  145. package/dist/clis/wechat/status.js +29 -0
  146. package/dist/clis/xiaohongshu/creator-note-detail.d.ts +10 -0
  147. package/dist/clis/xiaohongshu/creator-note-detail.js +88 -0
  148. package/dist/clis/xiaohongshu/creator-notes.d.ts +11 -0
  149. package/dist/clis/xiaohongshu/creator-notes.js +109 -0
  150. package/dist/clis/xiaohongshu/creator-profile.d.ts +10 -0
  151. package/dist/clis/xiaohongshu/creator-profile.js +54 -0
  152. package/dist/clis/xiaohongshu/creator-stats.d.ts +10 -0
  153. package/dist/clis/xiaohongshu/creator-stats.js +74 -0
  154. package/dist/clis/xiaohongshu/download.d.ts +7 -0
  155. package/dist/clis/xiaohongshu/download.js +155 -0
  156. package/dist/clis/xiaohongshu/search.js +1 -1
  157. package/dist/clis/xiaohongshu/user-helpers.d.ts +15 -0
  158. package/dist/clis/xiaohongshu/user-helpers.js +67 -0
  159. package/dist/clis/xiaohongshu/user-helpers.test.d.ts +1 -0
  160. package/dist/clis/xiaohongshu/user-helpers.test.js +81 -0
  161. package/dist/clis/xiaohongshu/user.js +46 -29
  162. package/dist/clis/zhihu/download.d.ts +11 -0
  163. package/dist/clis/zhihu/download.js +186 -0
  164. package/dist/clis/zhihu/download.test.d.ts +1 -0
  165. package/dist/clis/zhihu/download.test.js +10 -0
  166. package/dist/daemon.d.ts +13 -0
  167. package/dist/daemon.js +187 -0
  168. package/dist/doctor.d.ts +27 -61
  169. package/dist/doctor.js +70 -601
  170. package/dist/doctor.test.js +30 -170
  171. package/dist/download/index.d.ts +79 -0
  172. package/dist/download/index.js +325 -0
  173. package/dist/download/progress.d.ts +36 -0
  174. package/dist/download/progress.js +111 -0
  175. package/dist/engine.test.js +15 -0
  176. package/dist/main.js +22 -28
  177. package/dist/pipeline/executor.test.js +1 -0
  178. package/dist/pipeline/registry.js +2 -0
  179. package/dist/pipeline/steps/browser.js +2 -2
  180. package/dist/pipeline/steps/download.d.ts +34 -0
  181. package/dist/pipeline/steps/download.js +251 -0
  182. package/dist/pipeline/steps/intercept.js +1 -2
  183. package/dist/pipeline/template.js +28 -0
  184. package/dist/setup.d.ts +6 -0
  185. package/dist/setup.js +46 -160
  186. package/dist/types.d.ts +6 -0
  187. package/extension/icons/icon-128.png +0 -0
  188. package/extension/icons/icon-16.png +0 -0
  189. package/extension/icons/icon-32.png +0 -0
  190. package/extension/icons/icon-48.png +0 -0
  191. package/extension/manifest.json +31 -0
  192. package/extension/package.json +16 -0
  193. package/extension/src/background.ts +293 -0
  194. package/extension/src/cdp.ts +125 -0
  195. package/extension/src/protocol.ts +57 -0
  196. package/extension/store-assets/screenshot-1280x800.png +0 -0
  197. package/extension/tsconfig.json +15 -0
  198. package/extension/vite.config.ts +18 -0
  199. package/package.json +8 -7
  200. package/scripts/test-site.mjs +70 -0
  201. package/src/browser/daemon-client.ts +113 -0
  202. package/src/browser/discover.ts +18 -216
  203. package/src/browser/errors.ts +30 -100
  204. package/src/browser/index.ts +6 -12
  205. package/src/browser/mcp.ts +78 -278
  206. package/src/browser/page.ts +222 -88
  207. package/src/browser.test.ts +3 -210
  208. package/src/build-manifest.test.ts +28 -0
  209. package/src/build-manifest.ts +147 -57
  210. package/src/clis/bilibili/download.ts +161 -0
  211. package/src/clis/chatgpt/README.md +1 -1
  212. package/src/clis/chatgpt/README.zh-CN.md +1 -1
  213. package/src/clis/chatwise/README.md +38 -0
  214. package/src/clis/chatwise/README.zh-CN.md +38 -0
  215. package/src/clis/chatwise/ask.ts +87 -0
  216. package/src/clis/chatwise/export.ts +51 -0
  217. package/src/clis/chatwise/history.ts +47 -0
  218. package/src/clis/chatwise/model.ts +87 -0
  219. package/src/clis/chatwise/new.ts +21 -0
  220. package/src/clis/chatwise/read.ts +42 -0
  221. package/src/clis/chatwise/screenshot.ts +33 -0
  222. package/src/clis/chatwise/send.ts +50 -0
  223. package/src/clis/chatwise/status.ts +25 -0
  224. package/src/clis/discord-app/README.md +28 -0
  225. package/src/clis/discord-app/README.zh-CN.md +28 -0
  226. package/src/clis/discord-app/channels.ts +48 -0
  227. package/src/clis/discord-app/members.ts +41 -0
  228. package/src/clis/discord-app/read.ts +49 -0
  229. package/src/clis/discord-app/search.ts +64 -0
  230. package/src/clis/discord-app/send.ts +32 -0
  231. package/src/clis/discord-app/servers.ts +39 -0
  232. package/src/clis/discord-app/status.ts +18 -0
  233. package/src/clis/feishu/README.md +20 -0
  234. package/src/clis/feishu/README.zh-CN.md +20 -0
  235. package/src/clis/feishu/new.ts +32 -0
  236. package/src/clis/feishu/read.ts +48 -0
  237. package/src/clis/feishu/search.ts +35 -0
  238. package/src/clis/feishu/send.ts +46 -0
  239. package/src/clis/feishu/status.ts +34 -0
  240. package/src/clis/grok/ask.ts +90 -0
  241. package/src/clis/grok/debug.ts +49 -0
  242. package/src/clis/jimeng/generate.yaml +84 -0
  243. package/src/clis/jimeng/history.yaml +47 -0
  244. package/src/clis/linux-do/categories.yaml +41 -0
  245. package/src/clis/linux-do/category.yaml +49 -0
  246. package/src/clis/linux-do/hot.yaml +50 -0
  247. package/src/clis/linux-do/latest.yaml +40 -0
  248. package/src/clis/linux-do/search.yaml +45 -0
  249. package/src/clis/linux-do/topic.yaml +38 -0
  250. package/src/clis/neteasemusic/README.md +31 -0
  251. package/src/clis/neteasemusic/README.zh-CN.md +31 -0
  252. package/src/clis/neteasemusic/like.ts +28 -0
  253. package/src/clis/neteasemusic/lyrics.ts +53 -0
  254. package/src/clis/neteasemusic/next.ts +30 -0
  255. package/src/clis/neteasemusic/play.ts +30 -0
  256. package/src/clis/neteasemusic/playing.ts +62 -0
  257. package/src/clis/neteasemusic/playlist.ts +51 -0
  258. package/src/clis/neteasemusic/prev.ts +29 -0
  259. package/src/clis/neteasemusic/search.ts +58 -0
  260. package/src/clis/neteasemusic/status.ts +18 -0
  261. package/src/clis/neteasemusic/volume.ts +61 -0
  262. package/src/clis/notion/README.md +29 -0
  263. package/src/clis/notion/README.zh-CN.md +29 -0
  264. package/src/clis/notion/export.ts +36 -0
  265. package/src/clis/notion/favorites.ts +87 -0
  266. package/src/clis/notion/new.ts +39 -0
  267. package/src/clis/notion/read.ts +33 -0
  268. package/src/clis/notion/search.ts +54 -0
  269. package/src/clis/notion/sidebar.ts +44 -0
  270. package/src/clis/notion/status.ts +18 -0
  271. package/src/clis/notion/write.ts +45 -0
  272. package/src/clis/twitter/download.ts +227 -0
  273. package/src/clis/wechat/README.md +28 -0
  274. package/src/clis/wechat/README.zh-CN.md +28 -0
  275. package/src/clis/wechat/chats.ts +33 -0
  276. package/src/clis/wechat/contacts.ts +33 -0
  277. package/src/clis/wechat/read.ts +72 -0
  278. package/src/clis/wechat/search.ts +36 -0
  279. package/src/clis/wechat/send.ts +49 -0
  280. package/src/clis/wechat/status.ts +35 -0
  281. package/src/clis/xiaohongshu/creator-note-detail.ts +95 -0
  282. package/src/clis/xiaohongshu/creator-notes.ts +116 -0
  283. package/src/clis/xiaohongshu/creator-profile.ts +60 -0
  284. package/src/clis/xiaohongshu/creator-stats.ts +81 -0
  285. package/src/clis/xiaohongshu/download.ts +173 -0
  286. package/src/clis/xiaohongshu/search.ts +1 -1
  287. package/src/clis/xiaohongshu/user-helpers.test.ts +106 -0
  288. package/src/clis/xiaohongshu/user-helpers.ts +85 -0
  289. package/src/clis/xiaohongshu/user.ts +52 -32
  290. package/src/clis/zhihu/download.test.ts +12 -0
  291. package/src/clis/zhihu/download.ts +223 -0
  292. package/src/daemon.ts +217 -0
  293. package/src/doctor.test.ts +32 -193
  294. package/src/doctor.ts +74 -668
  295. package/src/download/index.ts +395 -0
  296. package/src/download/progress.ts +125 -0
  297. package/src/engine.test.ts +17 -0
  298. package/src/main.ts +18 -26
  299. package/src/pipeline/executor.test.ts +1 -0
  300. package/src/pipeline/registry.ts +2 -0
  301. package/src/pipeline/steps/browser.ts +2 -2
  302. package/src/pipeline/steps/download.ts +310 -0
  303. package/src/pipeline/steps/intercept.ts +1 -2
  304. package/src/pipeline/template.ts +26 -0
  305. package/src/setup.ts +47 -183
  306. package/src/types.ts +1 -0
  307. package/tests/e2e/browser-auth.test.ts +25 -0
@@ -0,0 +1,45 @@
1
+ site: linux-do
2
+ name: search
3
+ description: 搜索 linux.do
4
+ domain: linux.do
5
+ browser: true
6
+
7
+ args:
8
+ keyword:
9
+ type: str
10
+ required: true
11
+ description: Search keyword
12
+ limit:
13
+ type: int
14
+ default: 20
15
+ description: Number of results
16
+
17
+ pipeline:
18
+ - navigate: https://linux.do
19
+
20
+ - evaluate: |
21
+ (async () => {
22
+ const keyword = ${{ args.keyword | json }};
23
+ const res = await fetch('/search.json?q=' + encodeURIComponent(keyword), { credentials: 'include' });
24
+ if (!res.ok) throw new Error('HTTP ' + res.status + ' - 请先登录 linux.do');
25
+ let data;
26
+ try { data = await res.json(); } catch { throw new Error('响应不是有效 JSON - 请先登录 linux.do'); }
27
+ const topics = data?.topics || [];
28
+ return topics.slice(0, ${{ args.limit }}).map(t => ({
29
+ title: t.title,
30
+ views: t.views,
31
+ likes: t.like_count,
32
+ replies: (t.posts_count || 1) - 1,
33
+ }));
34
+ })()
35
+
36
+ - map:
37
+ rank: ${{ index + 1 }}
38
+ title: ${{ item.title }}
39
+ views: ${{ item.views }}
40
+ likes: ${{ item.likes }}
41
+ replies: ${{ item.replies }}
42
+
43
+ - limit: ${{ args.limit }}
44
+
45
+ columns: [rank, title, views, likes, replies]
@@ -0,0 +1,38 @@
1
+ site: linux-do
2
+ name: topic
3
+ description: linux.do 帖子详情和回复(首页)
4
+ domain: linux.do
5
+ browser: true
6
+
7
+ args:
8
+ id:
9
+ type: int
10
+ required: true
11
+ description: Topic ID
12
+
13
+ pipeline:
14
+ - navigate: https://linux.do
15
+
16
+ - evaluate: |
17
+ (async () => {
18
+ const res = await fetch('/t/${{ args.id }}.json', { credentials: 'include' });
19
+ if (!res.ok) throw new Error('HTTP ' + res.status + ' - 请先登录 linux.do');
20
+ let data;
21
+ try { data = await res.json(); } catch { throw new Error('响应不是有效 JSON - 请先登录 linux.do'); }
22
+ const strip = (html) => (html || '').replace(/<br\s*\/?>/gi, ' ').replace(/<\/(p|div|li|blockquote|h[1-6])>/gi, ' ').replace(/<[^>]+>/g, '').replace(/&nbsp;/g, ' ').replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&#(?:(\d+)|x([0-9a-fA-F]+));/g, (_, dec, hex) => { try { return String.fromCodePoint(dec !== undefined ? Number(dec) : parseInt(hex, 16)); } catch { return ''; } }).replace(/\s+/g, ' ').trim();
23
+ const posts = data?.post_stream?.posts || [];
24
+ return posts.map(p => ({
25
+ author: p.username,
26
+ content: strip(p.cooked).slice(0, 200),
27
+ likes: p.like_count,
28
+ created_at: p.created_at,
29
+ }));
30
+ })()
31
+
32
+ - map:
33
+ author: ${{ item.author }}
34
+ content: ${{ item.content }}
35
+ likes: ${{ item.likes }}
36
+ created_at: ${{ item.created_at }}
37
+
38
+ columns: [author, content, likes, created_at]
@@ -0,0 +1,31 @@
1
+ # NeteaseMusic Desktop Adapter (网易云音乐)
2
+
3
+ Control **NeteaseMusic** (网易云音乐) from the terminal via Chrome DevTools Protocol (CDP). The app uses Chromium Embedded Framework (CEF).
4
+
5
+ ## Prerequisites
6
+
7
+ Launch with remote debugging port:
8
+ ```bash
9
+ /Applications/NeteaseMusic.app/Contents/MacOS/NeteaseMusic --remote-debugging-port=9234
10
+ ```
11
+
12
+ ## Setup
13
+
14
+ ```bash
15
+ export OPENCLI_CDP_ENDPOINT="http://127.0.0.1:9234"
16
+ ```
17
+
18
+ ## Commands
19
+
20
+ | Command | Description |
21
+ |---------|-------------|
22
+ | `neteasemusic status` | Check CDP connection |
23
+ | `neteasemusic playing` | Current song info (title, artist, album) |
24
+ | `neteasemusic play` | Play / Pause toggle |
25
+ | `neteasemusic next` | Skip to next song |
26
+ | `neteasemusic prev` | Go to previous song |
27
+ | `neteasemusic search "query"` | Search songs, artists |
28
+ | `neteasemusic playlist` | Show current playback queue |
29
+ | `neteasemusic like` | Like / unlike current song |
30
+ | `neteasemusic lyrics` | Get lyrics of current song |
31
+ | `neteasemusic volume [0-100]` | Get or set volume |
@@ -0,0 +1,31 @@
1
+ # 网易云音乐桌面端适配器
2
+
3
+ 通过 Chrome DevTools Protocol (CDP) 在终端中控制 **网易云音乐**。该应用基于 Chromium Embedded Framework (CEF)。
4
+
5
+ ## 前置条件
6
+
7
+ 通过远程调试端口启动:
8
+ ```bash
9
+ /Applications/NeteaseMusic.app/Contents/MacOS/NeteaseMusic --remote-debugging-port=9234
10
+ ```
11
+
12
+ ## 配置
13
+
14
+ ```bash
15
+ export OPENCLI_CDP_ENDPOINT="http://127.0.0.1:9234"
16
+ ```
17
+
18
+ ## 命令
19
+
20
+ | 命令 | 说明 |
21
+ |------|------|
22
+ | `neteasemusic status` | 检查 CDP 连接 |
23
+ | `neteasemusic playing` | 当前播放歌曲信息 |
24
+ | `neteasemusic play` | 播放 / 暂停切换 |
25
+ | `neteasemusic next` | 下一首 |
26
+ | `neteasemusic prev` | 上一首 |
27
+ | `neteasemusic search "关键词"` | 搜索歌曲 |
28
+ | `neteasemusic playlist` | 显示当前播放列表 |
29
+ | `neteasemusic like` | 喜欢 / 取消喜欢 |
30
+ | `neteasemusic lyrics` | 获取当前歌词 |
31
+ | `neteasemusic volume [0-100]` | 获取或设置音量 |
@@ -0,0 +1,28 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import type { IPage } from '../../types.js';
3
+
4
+ export const likeCommand = cli({
5
+ site: 'neteasemusic',
6
+ name: 'like',
7
+ description: 'Like/unlike the currently playing song',
8
+ domain: 'localhost',
9
+ strategy: Strategy.UI,
10
+ browser: true,
11
+ args: [],
12
+ columns: ['Status'],
13
+ func: async (page: IPage) => {
14
+ const result = await page.evaluate(`
15
+ (function() {
16
+ // The like/heart button in the player bar
17
+ const btn = document.querySelector('.m-playbar .icn-love, .m-playbar [class*="like"], .m-player [class*="love"], [data-action="like"]');
18
+ if (!btn) return 'Like button not found';
19
+
20
+ const wasLiked = btn.classList.contains('loved') || btn.classList.contains('active') || btn.getAttribute('data-liked') === 'true';
21
+ btn.click();
22
+ return wasLiked ? 'Unliked' : 'Liked';
23
+ })()
24
+ `);
25
+
26
+ return [{ Status: result }];
27
+ },
28
+ });
@@ -0,0 +1,53 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import type { IPage } from '../../types.js';
3
+
4
+ export const lyricsCommand = cli({
5
+ site: 'neteasemusic',
6
+ name: 'lyrics',
7
+ description: 'Get the lyrics of the currently playing song',
8
+ domain: 'localhost',
9
+ strategy: Strategy.UI,
10
+ browser: true,
11
+ args: [],
12
+ columns: ['Line'],
13
+ func: async (page: IPage) => {
14
+ // Try to open lyrics panel if not visible
15
+ await page.evaluate(`
16
+ (function() {
17
+ const btn = document.querySelector('.m-playbar .icn-lyric, [class*="lyric-btn"], [data-action="lyric"]');
18
+ if (btn) btn.click();
19
+ })()
20
+ `);
21
+
22
+ await page.wait(1);
23
+
24
+ const lyrics = await page.evaluate(`
25
+ (function() {
26
+ // Look for lyrics container
27
+ const selectors = [
28
+ '.m-lyric p, .m-lyric [class*="line"]',
29
+ '[class*="lyric-content"] p',
30
+ '.listlyric li',
31
+ '[class*="lyric"] [class*="line"]',
32
+ '.j-lyric p',
33
+ ];
34
+
35
+ for (const sel of selectors) {
36
+ const nodes = document.querySelectorAll(sel);
37
+ if (nodes.length > 0) {
38
+ return Array.from(nodes).map(n => (n.textContent || '').trim()).filter(l => l.length > 0);
39
+ }
40
+ }
41
+
42
+ // Fallback: try the body text for any lyrics-like content
43
+ return [];
44
+ })()
45
+ `);
46
+
47
+ if (lyrics.length === 0) {
48
+ return [{ Line: 'No lyrics found. Try opening the lyrics panel first.' }];
49
+ }
50
+
51
+ return lyrics.map((line: string) => ({ Line: line }));
52
+ },
53
+ });
@@ -0,0 +1,30 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import type { IPage } from '../../types.js';
3
+
4
+ export const nextCommand = cli({
5
+ site: 'neteasemusic',
6
+ name: 'next',
7
+ description: 'Skip to the next song',
8
+ domain: 'localhost',
9
+ strategy: Strategy.UI,
10
+ browser: true,
11
+ args: [],
12
+ columns: ['Status'],
13
+ func: async (page: IPage) => {
14
+ const clicked = await page.evaluate(`
15
+ (function() {
16
+ const btn = document.querySelector('.m-playbar .btnfwd, .m-playbar [class*="next"], .m-player .btn-next, [data-action="next"]');
17
+ if (btn) { btn.click(); return true; }
18
+ return false;
19
+ })()
20
+ `);
21
+
22
+ if (!clicked) {
23
+ // Fallback: Ctrl+Right is common next-track shortcut
24
+ await page.pressKey('Control+ArrowRight');
25
+ }
26
+
27
+ await page.wait(1);
28
+ return [{ Status: 'Skipped to next song' }];
29
+ },
30
+ });
@@ -0,0 +1,30 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import type { IPage } from '../../types.js';
3
+
4
+ export const playCommand = cli({
5
+ site: 'neteasemusic',
6
+ name: 'play',
7
+ description: 'Toggle play/pause for the current song',
8
+ domain: 'localhost',
9
+ strategy: Strategy.UI,
10
+ browser: true,
11
+ args: [],
12
+ columns: ['Status'],
13
+ func: async (page: IPage) => {
14
+ // Click the play/pause button or use Space key
15
+ const clicked = await page.evaluate(`
16
+ (function() {
17
+ const btn = document.querySelector('.m-playbar .btnp, .m-playbar [class*="play"], .m-player .btn-play, [data-action="play"]');
18
+ if (btn) { btn.click(); return true; }
19
+ return false;
20
+ })()
21
+ `);
22
+
23
+ if (!clicked) {
24
+ // Fallback: use Space key which is the universal play/pause shortcut
25
+ await page.pressKey('Space');
26
+ }
27
+
28
+ return [{ Status: 'Play/Pause toggled' }];
29
+ },
30
+ });
@@ -0,0 +1,62 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import type { IPage } from '../../types.js';
3
+
4
+ export const playingCommand = cli({
5
+ site: 'neteasemusic',
6
+ name: 'playing',
7
+ description: 'Get the currently playing song info',
8
+ domain: 'localhost',
9
+ strategy: Strategy.UI,
10
+ browser: true,
11
+ args: [],
12
+ columns: ['Title', 'Artist', 'Album', 'Duration', 'Progress'],
13
+ func: async (page: IPage) => {
14
+ const info = await page.evaluate(`
15
+ (function() {
16
+ // NeteaseMusic player bar is at the bottom
17
+ const selectors = {
18
+ title: '.m-playbar .j-song .name, .m-playbar .song .name, [class*="playing"] .name, .m-player .name',
19
+ artist: '.m-playbar .j-song .by, .m-playbar .song .by, [class*="playing"] .artist, .m-player .by',
20
+ album: '.m-playbar .j-song .album, [class*="playing"] .album',
21
+ time: '.m-playbar .j-dur, .m-playbar .time, .m-player .time',
22
+ progress: '.m-playbar .barbg .rng, .m-playbar [role="progressbar"], .m-player [class*="progress"]',
23
+ };
24
+
25
+ function getText(sel) {
26
+ for (const s of sel.split(',')) {
27
+ const el = document.querySelector(s.trim());
28
+ if (el) return (el.textContent || el.innerText || '').trim();
29
+ }
30
+ return '';
31
+ }
32
+
33
+ const title = getText(selectors.title);
34
+ const artist = getText(selectors.artist);
35
+ const album = getText(selectors.album);
36
+ const time = getText(selectors.time);
37
+
38
+ // Try to get playback progress from the progress bar width
39
+ let progress = '';
40
+ const bar = document.querySelector('.m-playbar .barbg .rng, [class*="progress"] [class*="played"]');
41
+ if (bar) {
42
+ const style = bar.getAttribute('style') || '';
43
+ const match = style.match(/width:\\s*(\\d+\\.?\\d*)%/);
44
+ if (match) progress = match[1] + '%';
45
+ }
46
+
47
+ if (!title) {
48
+ // Fallback: try document title which often contains "songName - NeteaseMusic"
49
+ const docTitle = document.title;
50
+ if (docTitle && !docTitle.includes('NeteaseMusic')) {
51
+ return { Title: docTitle, Artist: '', Album: '', Duration: '', Progress: '' };
52
+ }
53
+ return { Title: 'No song playing', Artist: '—', Album: '—', Duration: '—', Progress: '—' };
54
+ }
55
+
56
+ return { Title: title, Artist: artist, Album: album, Duration: time, Progress: progress };
57
+ })()
58
+ `);
59
+
60
+ return [info];
61
+ },
62
+ });
@@ -0,0 +1,51 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import type { IPage } from '../../types.js';
3
+
4
+ export const playlistCommand = cli({
5
+ site: 'neteasemusic',
6
+ name: 'playlist',
7
+ description: 'Show the current playback queue / playlist',
8
+ domain: 'localhost',
9
+ strategy: Strategy.UI,
10
+ browser: true,
11
+ args: [],
12
+ columns: ['Index', 'Title', 'Artist'],
13
+ func: async (page: IPage) => {
14
+ // Open the playlist panel (usually a button at the bottom bar)
15
+ await page.evaluate(`
16
+ (function() {
17
+ const btn = document.querySelector('.m-playbar .icn-list, .m-playbar [class*="playlist"], [data-action="playlist"], .m-playbar .btnlist');
18
+ if (btn) btn.click();
19
+ })()
20
+ `);
21
+
22
+ await page.wait(1);
23
+
24
+ const items = await page.evaluate(`
25
+ (function() {
26
+ const results = [];
27
+ // Playlist panel items
28
+ const rows = document.querySelectorAll('.m-playlist li, [class*="playlist-panel"] li, .listlyric li, .j-playlist li');
29
+
30
+ rows.forEach((row, i) => {
31
+ const nameEl = row.querySelector('.name, [class*="name"], a, span:first-child');
32
+ const artistEl = row.querySelector('.by, [class*="artist"], .ar');
33
+
34
+ const title = nameEl ? (nameEl.getAttribute('title') || nameEl.textContent || '').trim() : (row.textContent || '').trim();
35
+ const artist = artistEl ? (artistEl.textContent || '').trim() : '';
36
+
37
+ if (title && title.length > 0) {
38
+ results.push({ Index: i + 1, Title: title.substring(0, 80), Artist: artist });
39
+ }
40
+ });
41
+
42
+ return results;
43
+ })()
44
+ `);
45
+
46
+ if (items.length === 0) {
47
+ return [{ Index: 0, Title: 'Playlist is empty or panel not open', Artist: '—' }];
48
+ }
49
+ return items;
50
+ },
51
+ });
@@ -0,0 +1,29 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import type { IPage } from '../../types.js';
3
+
4
+ export const prevCommand = cli({
5
+ site: 'neteasemusic',
6
+ name: 'prev',
7
+ description: 'Go back to the previous song',
8
+ domain: 'localhost',
9
+ strategy: Strategy.UI,
10
+ browser: true,
11
+ args: [],
12
+ columns: ['Status'],
13
+ func: async (page: IPage) => {
14
+ const clicked = await page.evaluate(`
15
+ (function() {
16
+ const btn = document.querySelector('.m-playbar .btnbak, .m-playbar [class*="prev"], .m-player .btn-prev, [data-action="prev"]');
17
+ if (btn) { btn.click(); return true; }
18
+ return false;
19
+ })()
20
+ `);
21
+
22
+ if (!clicked) {
23
+ await page.pressKey('Control+ArrowLeft');
24
+ }
25
+
26
+ await page.wait(1);
27
+ return [{ Status: 'Went to previous song' }];
28
+ },
29
+ });
@@ -0,0 +1,58 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import type { IPage } from '../../types.js';
3
+
4
+ export const searchCommand = cli({
5
+ site: 'neteasemusic',
6
+ name: 'search',
7
+ description: 'Search for songs, artists, albums, or playlists',
8
+ domain: 'localhost',
9
+ strategy: Strategy.UI,
10
+ browser: true,
11
+ args: [{ name: 'query', required: true, positional: true, help: 'Search query' }],
12
+ columns: ['Index', 'Title', 'Artist'],
13
+ func: async (page: IPage, kwargs: any) => {
14
+ const query = kwargs.query as string;
15
+
16
+ // Focus and fill the search box
17
+ await page.evaluate(`
18
+ (function(q) {
19
+ const input = document.querySelector('.m-search input, #srch, [class*="search"] input, input[type="search"]');
20
+ if (!input) throw new Error('Search input not found');
21
+ input.focus();
22
+ const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
23
+ setter.call(input, q);
24
+ input.dispatchEvent(new Event('input', { bubbles: true }));
25
+ })(${JSON.stringify(query)})
26
+ `);
27
+
28
+ await page.pressKey('Enter');
29
+ await page.wait(2);
30
+
31
+ // Scrape results
32
+ const results = await page.evaluate(`
33
+ (function() {
34
+ const items = [];
35
+ // Song list items in search results
36
+ const rows = document.querySelectorAll('.srchsongst li, .m-table tbody tr, [class*="songlist"] [class*="item"], table tbody tr');
37
+
38
+ rows.forEach((row, i) => {
39
+ if (i >= 20) return;
40
+ const nameEl = row.querySelector('.sn, .name a, [class*="songName"], td:nth-child(2) a, b[title]');
41
+ const artistEl = row.querySelector('.ar, .artist, [class*="artist"], td:nth-child(4) a, td:nth-child(3) a');
42
+
43
+ const title = nameEl ? (nameEl.getAttribute('title') || nameEl.textContent || '').trim() : '';
44
+ const artist = artistEl ? (artistEl.getAttribute('title') || artistEl.textContent || '').trim() : '';
45
+
46
+ if (title) items.push({ Index: i + 1, Title: title, Artist: artist });
47
+ });
48
+
49
+ return items;
50
+ })()
51
+ `);
52
+
53
+ if (results.length === 0) {
54
+ return [{ Index: 0, Title: `No results for "${query}"`, Artist: '—' }];
55
+ }
56
+ return results;
57
+ },
58
+ });
@@ -0,0 +1,18 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import type { IPage } from '../../types.js';
3
+
4
+ export const statusCommand = cli({
5
+ site: 'neteasemusic',
6
+ name: 'status',
7
+ description: 'Check CDP connection to NeteaseMusic Desktop',
8
+ domain: 'localhost',
9
+ strategy: Strategy.UI,
10
+ browser: true,
11
+ args: [],
12
+ columns: ['Status', 'Url', 'Title'],
13
+ func: async (page: IPage) => {
14
+ const url = await page.evaluate('window.location.href');
15
+ const title = await page.evaluate('document.title');
16
+ return [{ Status: 'Connected', Url: url, Title: title }];
17
+ },
18
+ });
@@ -0,0 +1,61 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import type { IPage } from '../../types.js';
3
+
4
+ export const volumeCommand = cli({
5
+ site: 'neteasemusic',
6
+ name: 'volume',
7
+ description: 'Get or set the volume level (0-100)',
8
+ domain: 'localhost',
9
+ strategy: Strategy.UI,
10
+ browser: true,
11
+ args: [
12
+ { name: 'level', required: false, positional: true, help: 'Volume level 0-100 (omit to read current)' },
13
+ ],
14
+ columns: ['Status', 'Volume'],
15
+ func: async (page: IPage, kwargs: any) => {
16
+ const level = kwargs.level as string | undefined;
17
+
18
+ if (!level) {
19
+ // Read current volume
20
+ const vol = await page.evaluate(`
21
+ (function() {
22
+ const bar = document.querySelector('.m-playbar .vol .barbg .rng, [class*="volume"] [class*="progress"], [class*="volume"] [class*="played"]');
23
+ if (bar) {
24
+ const style = bar.getAttribute('style') || '';
25
+ const match = style.match(/width:\\s*(\\d+\\.?\\d*)%/);
26
+ if (match) return match[1];
27
+ }
28
+
29
+ const vol = document.querySelector('.m-playbar .j-vol, [class*="volume-value"]');
30
+ if (vol) return vol.textContent.trim();
31
+
32
+ return 'Unknown';
33
+ })()
34
+ `);
35
+
36
+ return [{ Status: 'Current', Volume: vol + '%' }];
37
+ }
38
+
39
+ // Set volume by clicking on the volume bar at the right position
40
+ const targetVol = Math.max(0, Math.min(100, parseInt(level, 10)));
41
+
42
+ await page.evaluate(`
43
+ (function(target) {
44
+ const bar = document.querySelector('.m-playbar .vol .barbg, [class*="volume-bar"], [class*="volume"] [class*="track"]');
45
+ if (!bar) return;
46
+
47
+ const rect = bar.getBoundingClientRect();
48
+ const x = rect.left + (rect.width * target / 100);
49
+ const y = rect.top + rect.height / 2;
50
+
51
+ bar.dispatchEvent(new MouseEvent('click', {
52
+ clientX: x,
53
+ clientY: y,
54
+ bubbles: true,
55
+ }));
56
+ })(${targetVol})
57
+ `);
58
+
59
+ return [{ Status: 'Set', Volume: targetVol + '%' }];
60
+ },
61
+ });
@@ -0,0 +1,29 @@
1
+ # Notion Desktop Adapter
2
+
3
+ Control the **Notion Desktop App** from the terminal via Chrome DevTools Protocol (CDP).
4
+
5
+ ## Prerequisites
6
+
7
+ Launch with remote debugging port:
8
+ ```bash
9
+ /Applications/Notion.app/Contents/MacOS/Notion --remote-debugging-port=9230
10
+ ```
11
+
12
+ ## Setup
13
+
14
+ ```bash
15
+ export OPENCLI_CDP_ENDPOINT="http://127.0.0.1:9230"
16
+ ```
17
+
18
+ ## Commands
19
+
20
+ | Command | Description |
21
+ |---------|-------------|
22
+ | `notion status` | Check CDP connection |
23
+ | `notion search "query"` | Quick Find search (Cmd+P) |
24
+ | `notion read` | Read the current page content |
25
+ | `notion new "title"` | Create a new page (Cmd+N) |
26
+ | `notion write "text"` | Append text to the current page |
27
+ | `notion sidebar` | List pages from the sidebar |
28
+ | `notion favorites` | List pages from the Favorites section |
29
+ | `notion export` | Export page as Markdown |
@@ -0,0 +1,29 @@
1
+ # Notion 桌面端适配器
2
+
3
+ 通过 Chrome DevTools Protocol (CDP) 在终端中控制 **Notion 桌面应用**。
4
+
5
+ ## 前置条件
6
+
7
+ 通过远程调试端口启动:
8
+ ```bash
9
+ /Applications/Notion.app/Contents/MacOS/Notion --remote-debugging-port=9230
10
+ ```
11
+
12
+ ## 配置
13
+
14
+ ```bash
15
+ export OPENCLI_CDP_ENDPOINT="http://127.0.0.1:9230"
16
+ ```
17
+
18
+ ## 命令
19
+
20
+ | 命令 | 说明 |
21
+ |------|------|
22
+ | `notion status` | 检查 CDP 连接 |
23
+ | `notion search "关键词"` | 快速搜索(Cmd+P) |
24
+ | `notion read` | 读取当前页面内容 |
25
+ | `notion new "标题"` | 新建页面(Cmd+N) |
26
+ | `notion write "文本"` | 在当前页面追加文字 |
27
+ | `notion sidebar` | 列出侧边栏页面列表 |
28
+ | `notion favorites` | 列出收藏夹页面列表 |
29
+ | `notion export` | 导出页面为 Markdown |
@@ -0,0 +1,36 @@
1
+ import * as fs from 'node:fs';
2
+ import { cli, Strategy } from '../../registry.js';
3
+ import type { IPage } from '../../types.js';
4
+
5
+ export const exportCommand = cli({
6
+ site: 'notion',
7
+ name: 'export',
8
+ description: 'Export the current Notion page as Markdown',
9
+ domain: 'localhost',
10
+ strategy: Strategy.UI,
11
+ browser: true,
12
+ args: [
13
+ { name: 'output', required: false, positional: true, help: 'Output file (default: /tmp/notion-export.md)' },
14
+ ],
15
+ columns: ['Status', 'File'],
16
+ func: async (page: IPage, kwargs: any) => {
17
+ const outputPath = (kwargs.output as string) || '/tmp/notion-export.md';
18
+
19
+ const result = await page.evaluate(`
20
+ (function() {
21
+ const titleEl = document.querySelector('[data-block-id] [placeholder="Untitled"], h1.notion-title, [class*="title"]');
22
+ const title = titleEl ? (titleEl.textContent || '').trim() : document.title;
23
+
24
+ const frame = document.querySelector('.notion-page-content, [class*="page-content"], main');
25
+ const content = frame ? (frame.innerText || '').trim() : document.body.innerText;
26
+
27
+ return { title, content };
28
+ })()
29
+ `);
30
+
31
+ const md = `# ${result.title}\n\n${result.content}`;
32
+ fs.writeFileSync(outputPath, md);
33
+
34
+ return [{ Status: 'Success', File: outputPath }];
35
+ },
36
+ });