@jackwener/opencli 1.7.1 → 1.7.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 (122) hide show
  1. package/README.md +5 -2
  2. package/README.zh-CN.md +6 -3
  3. package/cli-manifest.json +1085 -73
  4. package/clis/barchart/flow.js +1 -1
  5. package/clis/barchart/greeks.js +2 -2
  6. package/clis/barchart/options.js +2 -2
  7. package/clis/barchart/quote.js +1 -1
  8. package/clis/bilibili/feed.js +202 -48
  9. package/clis/binance/asks.js +21 -0
  10. package/clis/binance/commands.test.js +70 -0
  11. package/clis/binance/depth.js +21 -0
  12. package/clis/binance/gainers.js +22 -0
  13. package/clis/binance/klines.js +21 -0
  14. package/clis/binance/losers.js +22 -0
  15. package/clis/binance/pairs.js +21 -0
  16. package/clis/binance/price.js +18 -0
  17. package/clis/binance/prices.js +19 -0
  18. package/clis/binance/ticker.js +21 -0
  19. package/clis/binance/top.js +21 -0
  20. package/clis/binance/trades.js +20 -0
  21. package/clis/boss/utils.js +2 -1
  22. package/clis/chatgpt/image.js +97 -0
  23. package/clis/chatgpt/utils.js +297 -0
  24. package/clis/{chatgpt → chatgpt-app}/ask.js +1 -1
  25. package/clis/{chatgpt → chatgpt-app}/model.js +1 -1
  26. package/clis/{chatgpt → chatgpt-app}/new.js +1 -1
  27. package/clis/{chatgpt → chatgpt-app}/read.js +1 -1
  28. package/clis/{chatgpt → chatgpt-app}/send.js +1 -1
  29. package/clis/{chatgpt → chatgpt-app}/status.js +1 -1
  30. package/clis/discord-app/delete.js +114 -0
  31. package/clis/douban/utils.js +29 -2
  32. package/clis/douban/utils.test.js +121 -1
  33. package/clis/ke/chengjiao.js +77 -0
  34. package/clis/ke/ershoufang.js +100 -0
  35. package/clis/ke/utils.js +104 -0
  36. package/clis/ke/xiaoqu.js +77 -0
  37. package/clis/ke/zufang.js +94 -0
  38. package/clis/maimai/search-talents.js +172 -0
  39. package/clis/mubu/doc.js +40 -0
  40. package/clis/mubu/docs.js +43 -0
  41. package/clis/mubu/notes.js +244 -0
  42. package/clis/mubu/recent.js +27 -0
  43. package/clis/mubu/search.js +62 -0
  44. package/clis/mubu/utils.js +304 -0
  45. package/clis/reuters/search.js +1 -1
  46. package/clis/twitter/lists-parser.js +77 -0
  47. package/clis/twitter/lists.d.ts +5 -0
  48. package/clis/twitter/lists.js +62 -0
  49. package/clis/twitter/lists.test.js +50 -0
  50. package/clis/weibo/feed.js +18 -5
  51. package/clis/xiaohongshu/comments.js +18 -6
  52. package/clis/xiaohongshu/comments.test.js +36 -0
  53. package/clis/xiaohongshu/creator-note-detail.js +2 -0
  54. package/clis/xiaohongshu/creator-note-detail.test.js +32 -0
  55. package/clis/xiaohongshu/creator-notes-summary.js +4 -0
  56. package/clis/xiaohongshu/creator-notes-summary.test.js +39 -1
  57. package/clis/xiaohongshu/creator-notes.js +1 -0
  58. package/clis/xiaohongshu/creator-profile.js +1 -0
  59. package/clis/xiaohongshu/creator-stats.js +1 -0
  60. package/clis/xiaohongshu/download.js +12 -0
  61. package/clis/xiaohongshu/download.test.js +30 -0
  62. package/clis/xiaohongshu/navigation.test.js +34 -0
  63. package/clis/xiaohongshu/note.js +14 -5
  64. package/clis/xiaohongshu/note.test.js +28 -0
  65. package/clis/xiaohongshu/publish.js +1 -0
  66. package/clis/xiaohongshu/search.js +1 -0
  67. package/clis/xiaohongshu/user.js +1 -0
  68. package/clis/yahoo-finance/quote.js +1 -1
  69. package/clis/zsxq/topic.js +5 -3
  70. package/clis/zsxq/topic.test.js +4 -3
  71. package/clis/zsxq/utils.js +1 -1
  72. package/dist/src/browser/base-page.d.ts +9 -0
  73. package/dist/src/browser/base-page.js +19 -0
  74. package/dist/src/browser/cdp.js +10 -2
  75. package/dist/src/browser/daemon-client.d.ts +1 -0
  76. package/dist/src/cli.js +112 -2
  77. package/dist/src/daemon.js +5 -0
  78. package/dist/src/discovery.d.ts +5 -2
  79. package/dist/src/discovery.js +7 -35
  80. package/dist/src/doctor.d.ts +1 -0
  81. package/dist/src/doctor.js +51 -2
  82. package/dist/src/electron-apps.js +1 -1
  83. package/dist/src/engine.test.js +29 -1
  84. package/dist/src/errors.d.ts +1 -0
  85. package/dist/src/errors.js +13 -0
  86. package/dist/src/execution.js +36 -9
  87. package/dist/src/execution.test.js +23 -0
  88. package/dist/src/logger.d.ts +2 -2
  89. package/dist/src/logger.js +4 -9
  90. package/dist/src/main.js +6 -5
  91. package/dist/src/registry.js +3 -4
  92. package/dist/src/types.d.ts +2 -0
  93. package/dist/src/update-check.d.ts +14 -0
  94. package/dist/src/update-check.js +48 -3
  95. package/dist/src/update-check.test.js +31 -0
  96. package/package.json +3 -3
  97. package/scripts/fetch-adapters.js +92 -34
  98. package/dist/src/clis/binance/asks.js +0 -20
  99. package/dist/src/clis/binance/commands.test.d.ts +0 -3
  100. package/dist/src/clis/binance/commands.test.js +0 -58
  101. package/dist/src/clis/binance/depth.d.ts +0 -1
  102. package/dist/src/clis/binance/depth.js +0 -20
  103. package/dist/src/clis/binance/gainers.d.ts +0 -1
  104. package/dist/src/clis/binance/gainers.js +0 -21
  105. package/dist/src/clis/binance/klines.d.ts +0 -1
  106. package/dist/src/clis/binance/klines.js +0 -20
  107. package/dist/src/clis/binance/losers.d.ts +0 -1
  108. package/dist/src/clis/binance/losers.js +0 -21
  109. package/dist/src/clis/binance/pairs.d.ts +0 -1
  110. package/dist/src/clis/binance/pairs.js +0 -20
  111. package/dist/src/clis/binance/price.d.ts +0 -1
  112. package/dist/src/clis/binance/price.js +0 -17
  113. package/dist/src/clis/binance/prices.d.ts +0 -1
  114. package/dist/src/clis/binance/prices.js +0 -18
  115. package/dist/src/clis/binance/ticker.d.ts +0 -1
  116. package/dist/src/clis/binance/ticker.js +0 -20
  117. package/dist/src/clis/binance/top.d.ts +0 -1
  118. package/dist/src/clis/binance/top.js +0 -20
  119. package/dist/src/clis/binance/trades.d.ts +0 -1
  120. package/dist/src/clis/binance/trades.js +0 -19
  121. /package/clis/{chatgpt → chatgpt-app}/ax.js +0 -0
  122. /package/dist/src/{clis/binance/asks.d.ts → update-check.test.d.ts} +0 -0
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Maimai talent search - Browser cookie API.
3
+ * Reuses Chrome login session to search for candidates on maimai.cn
4
+ */
5
+ import { cli, Strategy } from '@jackwener/opencli/registry';
6
+
7
+ cli({
8
+ site: 'maimai',
9
+ name: 'search-talents',
10
+ description: 'Search for candidates on Maimai with multi-dimensional filters',
11
+ domain: 'maimai.cn',
12
+ strategy: Strategy.COOKIE,
13
+ browser: true,
14
+ args: [
15
+ { name: 'query', positional: true, required: true, help: 'Search keyword (e.g., "Java", "产品经理")' },
16
+ { name: 'page', type: 'int', default: 0, help: 'Page number (0-based)' },
17
+ { name: 'size', type: 'int', default: 20, help: 'Results per page' },
18
+ { name: 'positions', help: 'Positions (e.g., "运营", "Java 开发工程师")' },
19
+ { name: 'companies', help: 'Companies, comma-separated (e.g., "百度", "字节跳动,阿里巴巴")' },
20
+ { name: 'schools', help: 'Schools, comma-separated (e.g., "北京大学", "清华大学,复旦大学")' },
21
+ { name: 'provinces', help: 'Provinces (e.g., "北京", "上海")' },
22
+ { name: 'cities', help: 'Cities (e.g., "北京市", "上海市")' },
23
+ { name: 'worktimes', help: 'Work years: 1=1-3y, 2=3-5y, 3=5-10y, 4=10+y' },
24
+ { name: 'degrees', help: 'Education: 1=大专,2=本科,3=硕士,4=博士,5=MBA' },
25
+ { name: 'professions', help: 'Industries: 01=互联网,02=金融,03=电子,04=通信' },
26
+ { name: 'is_211', type: 'int', help: '211 university: 0=any, 1=211' },
27
+ { name: 'is_985', type: 'int', help: '985 university: 0=any, 1=985' },
28
+ { name: 'sortby', type: 'int', default: 0, help: 'Sort: 0=relevance, 1=activity, 2=work_years, 3=education' },
29
+ { name: 'is_direct_chat', type: 'int', default: 0, help: 'Direct chat: 0=any, 1=available' },
30
+ ],
31
+ columns: ['name', 'job_title', 'company', 'historical_companies', 'location', 'work_year', 'school', 'degree', 'active_status', 'age', 'tags', 'mutual_friends'],
32
+ func: async (page, kwargs) => {
33
+ const {
34
+ query,
35
+ page: pageNum = 0,
36
+ size = 20,
37
+ positions = '',
38
+ companies = '',
39
+ schools = '',
40
+ provinces = '',
41
+ cities = '',
42
+ worktimes = '',
43
+ degrees = '',
44
+ professions = '',
45
+ is_211 = 0,
46
+ is_985 = 0,
47
+ sortby = 0,
48
+ is_direct_chat = 0,
49
+ } = kwargs;
50
+
51
+ // Navigate to the search page
52
+ await page.goto('https://maimai.cn/ent/talents/discover/search_v2', { waitUntil: 'networkidle' });
53
+ await page.waitForTimeout(5000);
54
+
55
+ // Generate random session IDs
56
+ const sessionid = 'b92d0fb5-f3fd-1f4b-fcdc-' + Math.random().toString(16).slice(2, 14);
57
+ const deletesessionid = 'ae907d75-315c-8db7-2cc7-' + Math.random().toString(16).slice(2, 14);
58
+
59
+ const requestBody = {
60
+ search: {
61
+ page: pageNum,
62
+ size: size,
63
+ sessionid: sessionid,
64
+ deletesessionid: deletesessionid,
65
+ worktimes: worktimes,
66
+ degrees: degrees,
67
+ professions: professions,
68
+ schools: schools,
69
+ positions: positions,
70
+ companyscope: 0,
71
+ sortby: sortby,
72
+ is_direct_chat: is_direct_chat,
73
+ query: query,
74
+ cities: cities,
75
+ provinces: provinces,
76
+ is_211: is_211,
77
+ is_985: is_985,
78
+ allcompanies: companies,
79
+ },
80
+ };
81
+
82
+ // Execute the search API call in browser context
83
+ const data = await page.evaluate(async (body) => {
84
+ // Get CSRF token from cookie or meta tag
85
+ let csrftoken = document.cookie.split('; ')
86
+ .find(row => row.startsWith('csrftoken='))
87
+ ?.split('=')[1] || '';
88
+
89
+ if (!csrftoken) {
90
+ const meta = document.querySelector('meta[name="csrf-token"]');
91
+ if (meta) csrftoken = meta.getAttribute('content') || '';
92
+ }
93
+
94
+ const res = await fetch('https://maimai.cn/api/ent/discover/search?channel=www&data_version=3.0&version=1.0.0', {
95
+ method: 'POST',
96
+ headers: {
97
+ 'accept': '*/*',
98
+ 'content-type': 'text/plain;charset=UTF-8',
99
+ 'origin': 'https://maimai.cn',
100
+ 'referer': 'https://maimai.cn/ent/talents/discover/search_v2',
101
+ 'x-csrf-token': csrftoken,
102
+ },
103
+ credentials: 'include',
104
+ body: JSON.stringify(body),
105
+ });
106
+
107
+ const result = await res.json();
108
+
109
+ // Check login status
110
+ if (res.status === 401 || res.status === 403 || result.error_code === 20002) {
111
+ throw new Error('需要登录!请先在浏览器中访问 maimai.cn 并登录');
112
+ }
113
+
114
+ if (result.code !== 200 && result.code !== 0) {
115
+ throw new Error(result.message || result.error || 'API 请求失败');
116
+ }
117
+
118
+ return result;
119
+ }, requestBody);
120
+
121
+ // Extract talent list from response
122
+ const talentList = data.data?.list || data.data?.talent_list || data.list || data.talent_list || [];
123
+
124
+ if (!talentList || talentList.length === 0) {
125
+ return [{ error: '未找到匹配的候选人', query: query }];
126
+ }
127
+
128
+ // Map to output format
129
+ return talentList.map(item => {
130
+ // Extract school info (first one)
131
+ const schoolInfo = item.edu && item.edu.length > 0 ? item.edu[0] : {};
132
+
133
+ // Work years: use work_time field directly (e.g., "11 年", "10 年")
134
+ const workYear = item.work_time || item.worktime || '';
135
+
136
+ // Extract all companies from work experience (deduplicated, excluding current company)
137
+ const currentCompany = item.company || '';
138
+ const historicalCompanies = (item.exp || [])
139
+ .map(e => e.company)
140
+ .filter(c => c && c.trim() !== '' && c !== currentCompany)
141
+ .filter((c, i, arr) => arr.indexOf(c) === i)
142
+ .join(' / ');
143
+
144
+ // Extract tags/skills from tag_list array
145
+ const tags = (item.tag_list || item.tags || [])
146
+ .filter(t => t && t.trim() !== '')
147
+ .join(', ');
148
+
149
+ // Extract mutual friends count and list
150
+ const mutualFriendsCount = item.friends_cnt || item.common_friends_count || 0;
151
+ const mutualFriendsList = (item.friends || item.common_friends || [])
152
+ .map(f => f.name || f.user_name || f)
153
+ .slice(0, 3)
154
+ .join(', ');
155
+
156
+ return {
157
+ name: item.name || '',
158
+ job_title: item.position || item.job_title || '',
159
+ company: currentCompany,
160
+ historical_companies: historicalCompanies,
161
+ location: (item.province || '') + (item.city ? '·' + item.city : ''),
162
+ work_year: workYear,
163
+ school: schoolInfo.school || schoolInfo.hover?.name || '',
164
+ degree: schoolInfo.sdegree || schoolInfo.hover?.school_level || '',
165
+ active_status: item.active_state_v2 || item.active_state_v1 || item.active_state || '',
166
+ age: item.age || '',
167
+ tags: tags,
168
+ mutual_friends: mutualFriendsCount > 0 ? `${mutualFriendsCount}人${mutualFriendsList ? ' (' + mutualFriendsList + ')' : ''}` : '',
169
+ };
170
+ });
171
+ },
172
+ });
@@ -0,0 +1,40 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+ import { ArgumentError } from '@jackwener/opencli/errors';
3
+ import { mubuPost, nodesToMarkdown, nodesToText } from './utils.js';
4
+
5
+ cli({
6
+ site: 'mubu',
7
+ name: 'doc',
8
+ description: '读取幕布文档内容(默认输出 Markdown,可用 --output text 输出纯文本)',
9
+ domain: 'mubu.com',
10
+ strategy: Strategy.COOKIE,
11
+ defaultFormat: 'plain',
12
+ args: [
13
+ { name: 'id', positional: true, required: true, help: '文档 ID' },
14
+ { name: 'output', default: 'md', help: '输出格式:md(默认,缩进列表 Markdown,适合导入 Obsidian)或 text(纯文本,适合终端阅读)' },
15
+ ],
16
+ columns: ['content'],
17
+ func: async (page, kwargs) => {
18
+ const docId = kwargs.id;
19
+ const format = kwargs.output;
20
+ if (format !== 'md' && format !== 'text') {
21
+ throw new ArgumentError(`--output 只接受 md 或 text,收到:${format}`);
22
+ }
23
+
24
+ await page.goto('https://mubu.com/app');
25
+
26
+ const data = await mubuPost(page, '/document/edit/get', { docId });
27
+
28
+ let nodes = [];
29
+ try {
30
+ const def = JSON.parse(data.definition);
31
+ nodes = def.nodes ?? [];
32
+ } catch {
33
+ return [{ content: data.name }];
34
+ }
35
+
36
+ const output = format === 'md' ? nodesToMarkdown(nodes) : nodesToText(nodes);
37
+
38
+ return [{ content: output }];
39
+ },
40
+ });
@@ -0,0 +1,43 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+ import { formatDate, mubuPost } from './utils.js';
3
+
4
+ cli({
5
+ site: 'mubu',
6
+ name: 'docs',
7
+ description: '列出幕布文档(默认根目录,--starred 查看快速访问列表)',
8
+ domain: 'mubu.com',
9
+ strategy: Strategy.COOKIE,
10
+ args: [
11
+ { name: 'folder', default: '0', help: '文件夹 ID(默认根目录 0)' },
12
+ { name: 'starred', type: 'bool', default: false, help: '只显示快速访问的文档和文件夹' },
13
+ { name: 'limit', type: 'int', default: 50, help: '最多显示条数' },
14
+ ],
15
+ columns: ['type', 'id', 'name', 'updated', 'stared'],
16
+ func: async (page, kwargs) => {
17
+ const folderId = kwargs.folder;
18
+ const starred = kwargs.starred;
19
+ const limit = kwargs.limit;
20
+
21
+ await page.goto('https://mubu.com/app');
22
+ const body = starred ? { source: 'star' } : { folderId };
23
+ const data = await mubuPost(page, '/list/get', body);
24
+
25
+ const folders = (data.folders ?? []).map((f) => ({
26
+ type: '📁',
27
+ id: f.id,
28
+ name: f.name,
29
+ updated: formatDate(f.updateTime),
30
+ stared: f.stared ? '★' : '',
31
+ }));
32
+
33
+ const docs = (data.documents ?? []).map((doc) => ({
34
+ type: '📄',
35
+ id: doc.id,
36
+ name: doc.name,
37
+ updated: formatDate(doc.updateTime),
38
+ stared: doc.stared ? '★' : '',
39
+ }));
40
+
41
+ return [...folders, ...docs].slice(0, limit);
42
+ },
43
+ });
@@ -0,0 +1,244 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+ import { ArgumentError } from '@jackwener/opencli/errors';
3
+ import { mubuPost, nodesToMarkdown, nodesToText, htmlToText } from './utils.js';
4
+
5
+ // ── 日期工具 ──────────────────────────────────────────────
6
+
7
+ function localToday() {
8
+ const d = new Date();
9
+ return { year: d.getFullYear(), month: d.getMonth() + 1, day: d.getDate() };
10
+ }
11
+
12
+ function lastDayOfMonth(year, month) {
13
+ return new Date(year, month, 0).getDate();
14
+ }
15
+
16
+ function validateYear(year, label = '年份') {
17
+ if (!Number.isInteger(year) || year < 1) {
18
+ throw new ArgumentError(`${label} 非法:${year},应为正整数`);
19
+ }
20
+ }
21
+
22
+ function validateMonth(month) {
23
+ if (!Number.isInteger(month) || month < 1 || month > 12) {
24
+ throw new ArgumentError(`月份非法:${month},应为 1-12`);
25
+ }
26
+ }
27
+
28
+ function validateDay(year, month, day) {
29
+ const maxDay = lastDayOfMonth(year, month);
30
+ if (!Number.isInteger(day) || day < 1 || day > maxDay) {
31
+ throw new ArgumentError(`日期非法:${year}-${month}-${day}(${year} 年 ${month} 月共 ${maxDay} 天)`);
32
+ }
33
+ }
34
+
35
+ function parseDate(s) {
36
+ const parts = s.split('-').map(Number);
37
+ if (parts.length !== 3 || parts.some(isNaN)) {
38
+ throw new ArgumentError(`日期格式错误:${s},应为 YYYY-MM-DD`);
39
+ }
40
+ const [year, month, day] = parts;
41
+ validateYear(year);
42
+ validateMonth(month);
43
+ validateDay(year, month, day);
44
+ return { year, month, day };
45
+ }
46
+
47
+ function parseMonth(s) {
48
+ const parts = s.split('-').map(Number);
49
+ if (parts.length !== 2 || parts.some(isNaN)) {
50
+ throw new ArgumentError(`月份格式错误:${s},应为 YYYY-MM`);
51
+ }
52
+ const [year, month] = parts;
53
+ validateYear(year);
54
+ validateMonth(month);
55
+ return { year, month };
56
+ }
57
+
58
+ function dateToKey(d) {
59
+ return `${d.year}-${String(d.month).padStart(2, '0')}-${String(d.day).padStart(2, '0')}`;
60
+ }
61
+
62
+ /** 将各种时间参数统一解析为 {start, end} */
63
+ function resolveRange(kwargs) {
64
+ const dateStr = kwargs.date;
65
+ const monthStr = kwargs.month;
66
+ const yearArg = kwargs.year;
67
+ const fromStr = kwargs.from;
68
+ const toStr = kwargs.to;
69
+
70
+ // --from / --to 优先级最高
71
+ if (fromStr || toStr) {
72
+ if (!fromStr) throw new ArgumentError('使用 --to 时必须同时指定 --from');
73
+ const start = parseDate(fromStr);
74
+ const end = toStr ? parseDate(toStr) : localToday();
75
+ if (dateToKey(start) > dateToKey(end)) throw new ArgumentError('--from 不能晚于 --to');
76
+ return { start, end };
77
+ }
78
+
79
+ if (yearArg !== undefined && yearArg !== null) {
80
+ validateYear(yearArg, '--year');
81
+ return {
82
+ start: { year: yearArg, month: 1, day: 1 },
83
+ end: { year: yearArg, month: 12, day: 31 },
84
+ };
85
+ }
86
+
87
+ if (monthStr) {
88
+ const { year, month } = parseMonth(monthStr);
89
+ return {
90
+ start: { year, month, day: 1 },
91
+ end: { year, month, day: lastDayOfMonth(year, month) },
92
+ };
93
+ }
94
+
95
+ if (dateStr) {
96
+ const d = parseDate(dateStr);
97
+ return { start: d, end: d };
98
+ }
99
+
100
+ // 默认:今天
101
+ const today = localToday();
102
+ return { start: today, end: today };
103
+ }
104
+
105
+ // ── API 工具 ──────────────────────────────────────────────
106
+
107
+ async function getYearDocId(page, year) {
108
+ const raw = await page.evaluate(`localStorage.getItem('daily_notes_doc_list')`);
109
+ if (!raw) return null;
110
+ const list = JSON.parse(raw);
111
+ return list.find((d) => d.name === `${year}年`)?.id ?? null;
112
+ }
113
+
114
+ async function getYearNodes(page, docId) {
115
+ const data = await mubuPost(page, '/document/edit/get', { docId });
116
+ const def = JSON.parse(data.definition);
117
+ return def.nodes ?? [];
118
+ }
119
+
120
+ /** 加载某年的所有 day 节点,返回带 dateKey 的列表 */
121
+ async function loadYearEntries(page, year) {
122
+ const docId = await getYearDocId(page, year);
123
+ if (!docId) return [];
124
+
125
+ const yearNodes = await getYearNodes(page, docId);
126
+ const entries = [];
127
+
128
+ for (const monthNode of yearNodes) {
129
+ const monthNum = parseInt(htmlToText(monthNode.text), 10);
130
+ if (!monthNode.children?.length) continue;
131
+
132
+ for (const dayNode of monthNode.children) {
133
+ const plain = htmlToText(dayNode.text).replace(/\s+/g, ' ').trim();
134
+ const compact = plain.replace(/\s/g, '');
135
+ const match = compact.match(/^(\d+)月(\d+)日/);
136
+ if (!match) continue;
137
+ const m = parseInt(match[1], 10);
138
+ const d = parseInt(match[2], 10);
139
+ if (m !== monthNum) continue;
140
+
141
+ const dateKey = dateToKey({ year, month: m, day: d });
142
+ entries.push({ dateKey, label: plain, node: dayNode });
143
+ }
144
+ }
145
+
146
+ return entries;
147
+ }
148
+
149
+ /** 收集 [start, end] 范围内涉及的所有年份 */
150
+ function yearsInRange(start, end) {
151
+ const years = [];
152
+ for (let y = start.year; y <= end.year; y++) years.push(y);
153
+ return years;
154
+ }
155
+
156
+ // ── 命令 ──────────────────────────────────────────────────
157
+
158
+ cli({
159
+ site: 'mubu',
160
+ name: 'notes',
161
+ description: '读取幕布速记(默认今天)。支持 --date/--month/--year/--from/--to 指定时间范围,--list 为概览模式(日期+条数)。',
162
+ domain: 'mubu.com',
163
+ strategy: Strategy.COOKIE,
164
+ args: [
165
+ {
166
+ name: 'list',
167
+ type: 'bool',
168
+ default: false,
169
+ help: '概览模式:只输出日期和条数,不含速记内容。可与任意时间范围参数组合。',
170
+ },
171
+ {
172
+ name: 'date',
173
+ help: '单日,格式 YYYY-MM-DD。不指定时间范围则默认今天(系统本地时间)。',
174
+ },
175
+ {
176
+ name: 'month',
177
+ help: '整月,格式 YYYY-MM。',
178
+ },
179
+ {
180
+ name: 'year',
181
+ type: 'int',
182
+ help: '整年,格式 YYYY(整数)。',
183
+ },
184
+ {
185
+ name: 'from',
186
+ help: '范围起始日,格式 YYYY-MM-DD。须与 --to 同时使用。',
187
+ },
188
+ {
189
+ name: 'to',
190
+ help: '范围截止日,格式 YYYY-MM-DD。须与 --from 同时使用。',
191
+ },
192
+ {
193
+ name: 'output',
194
+ default: 'md',
195
+ help: '输出格式:md(默认,Markdown)或 text(纯文本)',
196
+ },
197
+ ],
198
+ columns: ['date', 'content'],
199
+ func: async (page, kwargs) => {
200
+ const isList = kwargs.list;
201
+ const format = kwargs.output;
202
+ if (format !== 'md' && format !== 'text') {
203
+ throw new ArgumentError(`--output 只接受 md 或 text,收到:${format}`);
204
+ }
205
+
206
+ await page.goto('https://mubu.com/app');
207
+
208
+ const { start, end } = resolveRange(kwargs);
209
+ const startKey = dateToKey(start);
210
+ const endKey = dateToKey(end);
211
+
212
+ // 并行加载所有涉及年份的 day 节点,按范围过滤
213
+ const yearResults = await Promise.all(
214
+ yearsInRange(start, end).map((year) => loadYearEntries(page, year)),
215
+ );
216
+ const allEntries = yearResults
217
+ .flat()
218
+ .filter((e) => e.dateKey >= startKey && e.dateKey <= endKey);
219
+
220
+ if (allEntries.length === 0) {
221
+ const label = startKey === endKey ? startKey : `${startKey} ~ ${endKey}`;
222
+ return [{ date: label, content: '该时间段暂无速记' }];
223
+ }
224
+
225
+ // 概览模式
226
+ if (isList) {
227
+ return allEntries.map((e) => ({
228
+ date: e.label,
229
+ content: `${e.node.children?.length ?? 0} 条记录`,
230
+ }));
231
+ }
232
+
233
+ // 内容模式
234
+ const render = (children) =>
235
+ format === 'text' ? nodesToText(children) : nodesToMarkdown(children);
236
+
237
+ return allEntries
238
+ .filter((e) => e.node.children?.length)
239
+ .map((e) => ({
240
+ date: e.label,
241
+ content: render(e.node.children ?? []) || '(空)',
242
+ }));
243
+ },
244
+ });
@@ -0,0 +1,27 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+ import { formatDate, mubuPost } from './utils.js';
3
+
4
+ cli({
5
+ site: 'mubu',
6
+ name: 'recent',
7
+ description: '最近编辑的幕布文档',
8
+ domain: 'mubu.com',
9
+ strategy: Strategy.COOKIE,
10
+ args: [
11
+ { name: 'limit', type: 'int', default: 20, help: '最多显示条数' },
12
+ ],
13
+ columns: ['id', 'name', 'updated'],
14
+ func: async (page, kwargs) => {
15
+ const limit = kwargs.limit;
16
+
17
+ await page.goto('https://mubu.com/app');
18
+
19
+ const data = await mubuPost(page, '/list/get', { folderId: 'recent' });
20
+
21
+ return (data.documents ?? []).slice(0, limit).map((doc) => ({
22
+ id: doc.id,
23
+ name: doc.name,
24
+ updated: formatDate(doc.updateTime),
25
+ }));
26
+ },
27
+ });
@@ -0,0 +1,62 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+ import { mubuPost, htmlToText } from './utils.js';
3
+
4
+ cli({
5
+ site: 'mubu',
6
+ name: 'search',
7
+ description: '全局搜索幕布文档和文件夹(标题+内容,服务端全量匹配)。结果含 type/id/name/path/hits/snippet 字段。',
8
+ domain: 'mubu.com',
9
+ strategy: Strategy.COOKIE,
10
+ args: [
11
+ { name: 'query', positional: true, required: true, help: '搜索关键词' },
12
+ { name: 'limit', type: 'int', default: 100, help: '最多显示条数(默认 100,结果被截断时用 --limit N 调大)' },
13
+ ],
14
+ columns: ['type', 'id', 'name', 'path', 'hits', 'snippet'],
15
+ func: async (page, kwargs) => {
16
+ const query = kwargs.query;
17
+ const limit = kwargs.limit ?? 100;
18
+
19
+ await page.goto('https://mubu.com/app');
20
+
21
+ const data = await mubuPost(page, '/list/search', { keywords: query });
22
+
23
+ const formatPath = (paths) => paths.map((p) => p.name).join(' > ');
24
+
25
+ const folders = (data.folders ?? []).map((f) => ({
26
+ type: 'folder',
27
+ id: f.id,
28
+ name: f.name,
29
+ path: formatPath(f.paths),
30
+ hits: '',
31
+ snippet: '',
32
+ }));
33
+
34
+ const docs = (data.documents ?? []).map((d) => ({
35
+ type: 'doc',
36
+ id: d.id,
37
+ name: d.name,
38
+ path: formatPath(d.paths),
39
+ hits: d.total > 0 ? String(d.total) : '',
40
+ snippet: d.nodes
41
+ .map((n) => htmlToText(n.text))
42
+ .filter(Boolean)
43
+ .join(' | '),
44
+ }));
45
+
46
+ const all = [...folders, ...docs];
47
+ const result = all.slice(0, limit);
48
+
49
+ if (all.length > limit) {
50
+ result.push({
51
+ type: '...',
52
+ id: '',
53
+ name: `还有 ${all.length - limit} 条未显示,用 --limit ${all.length} 查看全部`,
54
+ path: '',
55
+ hits: '',
56
+ snippet: '',
57
+ });
58
+ }
59
+
60
+ return result;
61
+ },
62
+ });