@jackwener/opencli 1.7.2 → 1.7.4
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.
- package/README.md +18 -15
- package/README.zh-CN.md +31 -15
- package/cli-manifest.json +1265 -101
- package/clis/barchart/flow.js +1 -1
- package/clis/barchart/greeks.js +2 -2
- package/clis/barchart/options.js +2 -2
- package/clis/barchart/quote.js +1 -1
- package/clis/bilibili/favorite.js +18 -13
- package/clis/bilibili/feed.js +202 -48
- package/clis/binance/depth.js +3 -4
- package/clis/boss/utils.js +2 -2
- package/clis/chatgpt/image.js +97 -0
- package/clis/chatgpt/utils.js +297 -0
- package/clis/{chatgpt → chatgpt-app}/ask.js +1 -1
- package/clis/{chatgpt → chatgpt-app}/ax.js +6 -3
- package/clis/{chatgpt → chatgpt-app}/model.js +1 -1
- package/clis/{chatgpt → chatgpt-app}/new.js +1 -1
- package/clis/{chatgpt → chatgpt-app}/read.js +1 -1
- package/clis/{chatgpt → chatgpt-app}/send.js +1 -1
- package/clis/{chatgpt → chatgpt-app}/status.js +1 -1
- package/clis/discord-app/delete.js +114 -0
- package/clis/douban/search.js +1 -0
- package/clis/douban/search.test.js +11 -0
- package/clis/douban/subject.js +20 -93
- package/clis/douban/subject.test.js +11 -0
- package/clis/douban/utils.js +279 -10
- package/clis/douban/utils.test.js +296 -1
- package/clis/doubao/utils.js +319 -130
- package/clis/doubao/utils.test.js +241 -2
- package/clis/eastmoney/hot-rank.js +50 -0
- package/clis/eastmoney/hot-rank.test.js +59 -0
- package/clis/grok/image.test.ts +107 -0
- package/clis/grok/image.ts +356 -0
- package/clis/ke/chengjiao.js +77 -0
- package/clis/ke/ershoufang.js +100 -0
- package/clis/ke/utils.js +104 -0
- package/clis/ke/xiaoqu.js +77 -0
- package/clis/ke/zufang.js +94 -0
- package/clis/maimai/search-talents.js +172 -0
- package/clis/mubu/doc.js +40 -0
- package/clis/mubu/docs.js +43 -0
- package/clis/mubu/notes.js +244 -0
- package/clis/mubu/recent.js +27 -0
- package/clis/mubu/search.js +62 -0
- package/clis/mubu/utils.js +304 -0
- package/clis/reuters/search.js +1 -1
- package/clis/tdx/hot-rank.js +47 -0
- package/clis/tdx/hot-rank.test.js +59 -0
- package/clis/ths/hot-rank.js +49 -0
- package/clis/ths/hot-rank.test.js +64 -0
- package/clis/twitter/bookmarks.js +2 -1
- package/clis/uiverse/_shared.js +368 -0
- package/clis/uiverse/_shared.test.js +55 -0
- package/clis/uiverse/code.js +47 -0
- package/clis/uiverse/preview.js +71 -0
- package/clis/xiaohongshu/comments.js +20 -8
- package/clis/xiaohongshu/comments.test.js +69 -12
- package/clis/xiaohongshu/creator-note-detail.js +2 -0
- package/clis/xiaohongshu/creator-note-detail.test.js +32 -0
- package/clis/xiaohongshu/creator-notes-summary.js +4 -0
- package/clis/xiaohongshu/creator-notes-summary.test.js +39 -1
- package/clis/xiaohongshu/creator-notes.js +1 -0
- package/clis/xiaohongshu/creator-profile.js +1 -0
- package/clis/xiaohongshu/creator-stats.js +1 -0
- package/clis/xiaohongshu/download.js +18 -7
- package/clis/xiaohongshu/download.test.js +42 -0
- package/clis/xiaohongshu/navigation.test.js +34 -0
- package/clis/xiaohongshu/note-helpers.js +46 -12
- package/clis/xiaohongshu/note.js +17 -10
- package/clis/xiaohongshu/note.test.js +66 -11
- package/clis/xiaohongshu/publish.js +1 -0
- package/clis/xiaohongshu/search.js +1 -0
- package/clis/xiaohongshu/user.js +1 -0
- package/clis/xiaoyuzhou/auth.js +303 -0
- package/clis/xiaoyuzhou/auth.test.js +124 -0
- package/clis/xiaoyuzhou/download.js +49 -0
- package/clis/xiaoyuzhou/download.test.js +125 -0
- package/clis/xiaoyuzhou/transcript.js +76 -0
- package/clis/xiaoyuzhou/transcript.test.js +195 -0
- package/clis/yahoo-finance/quote.js +1 -1
- package/clis/youtube/feed.js +120 -0
- package/clis/youtube/history.js +118 -0
- package/clis/youtube/like.js +62 -0
- package/clis/youtube/playlist.js +97 -0
- package/clis/youtube/subscribe.js +71 -0
- package/clis/youtube/subscriptions.js +57 -0
- package/clis/youtube/unlike.js +62 -0
- package/clis/youtube/unsubscribe.js +71 -0
- package/clis/youtube/utils.js +122 -0
- package/clis/youtube/utils.test.js +32 -1
- package/clis/youtube/watch-later.js +76 -0
- package/dist/src/browser/base-page.d.ts +9 -0
- package/dist/src/browser/base-page.js +44 -5
- package/dist/src/browser/bridge.d.ts +2 -0
- package/dist/src/browser/bridge.js +51 -14
- package/dist/src/browser/cdp.js +11 -2
- package/dist/src/browser/daemon-client.d.ts +2 -0
- package/dist/src/browser/dom-snapshot.js +13 -1
- package/dist/src/browser/page.d.ts +4 -1
- package/dist/src/browser/page.js +48 -8
- package/dist/src/browser/page.test.js +61 -1
- package/dist/src/browser/target-errors.d.ts +23 -0
- package/dist/src/browser/target-errors.js +29 -0
- package/dist/src/browser/target-errors.test.d.ts +1 -0
- package/dist/src/browser/target-errors.test.js +61 -0
- package/dist/src/browser/target-resolver.d.ts +57 -0
- package/dist/src/browser/target-resolver.js +298 -0
- package/dist/src/browser/target-resolver.test.d.ts +1 -0
- package/dist/src/browser/target-resolver.test.js +43 -0
- package/dist/src/browser.test.js +38 -1
- package/dist/src/cli.js +45 -35
- package/dist/src/commands/daemon.d.ts +4 -2
- package/dist/src/commands/daemon.js +22 -2
- package/dist/src/commands/daemon.test.js +65 -2
- package/dist/src/daemon.js +7 -0
- package/dist/src/doctor.d.ts +2 -0
- package/dist/src/doctor.js +82 -10
- package/dist/src/doctor.test.js +28 -12
- package/dist/src/electron-apps.js +1 -1
- package/dist/src/errors.d.ts +1 -0
- package/dist/src/errors.js +13 -0
- package/dist/src/execution.js +36 -9
- package/dist/src/execution.test.js +23 -0
- package/dist/src/external-clis.yaml +2 -2
- package/dist/src/logger.d.ts +2 -2
- package/dist/src/logger.js +3 -8
- package/dist/src/output.js +1 -5
- package/dist/src/output.test.js +0 -21
- package/dist/src/pipeline/steps/transform.js +1 -1
- package/dist/src/pipeline/template.d.ts +1 -0
- package/dist/src/pipeline/template.js +11 -3
- package/dist/src/pipeline/template.test.js +3 -0
- package/dist/src/pipeline/transform.test.js +14 -0
- package/dist/src/plugin.d.ts +7 -1
- package/dist/src/plugin.js +23 -1
- package/dist/src/plugin.test.js +15 -1
- package/dist/src/registry.js +3 -4
- package/dist/src/types.d.ts +3 -1
- package/dist/src/update-check.d.ts +14 -0
- package/dist/src/update-check.js +48 -3
- package/dist/src/update-check.test.d.ts +1 -0
- package/dist/src/update-check.test.js +31 -0
- package/package.json +1 -1
- package/scripts/fetch-adapters.js +35 -8
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
import { cityUrl, gotoKe } from './utils.js';
|
|
3
|
+
|
|
4
|
+
cli({
|
|
5
|
+
site: 'ke',
|
|
6
|
+
name: 'xiaoqu',
|
|
7
|
+
description: '贝壳找房小区列表',
|
|
8
|
+
domain: 'ke.com',
|
|
9
|
+
strategy: Strategy.COOKIE,
|
|
10
|
+
browser: true,
|
|
11
|
+
args: [
|
|
12
|
+
{ name: 'city', default: 'bj', help: '城市代码,如 bj(北京), sh(上海), gz(广州), sz(深圳), zs(中山)' },
|
|
13
|
+
{ name: 'district', help: '区域拼音,如 chaoyang, haidian' },
|
|
14
|
+
{ name: 'limit', type: 'int', default: 20, help: '返回数量' },
|
|
15
|
+
],
|
|
16
|
+
columns: ['name', 'district', 'avg_price', 'year', 'on_sale'],
|
|
17
|
+
func: async (page, kwargs) => {
|
|
18
|
+
const city = kwargs.city || 'bj';
|
|
19
|
+
const limit = Number(kwargs.limit) || 20;
|
|
20
|
+
const base = cityUrl(city);
|
|
21
|
+
|
|
22
|
+
let path = '/xiaoqu/';
|
|
23
|
+
if (kwargs.district) {
|
|
24
|
+
path = `/xiaoqu/${kwargs.district}/`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
await gotoKe(page, base + path);
|
|
28
|
+
|
|
29
|
+
const items = await page.evaluate(`(async () => {
|
|
30
|
+
const selectors = [
|
|
31
|
+
'.xiaoquListItem',
|
|
32
|
+
'li.xiaoquListItem',
|
|
33
|
+
'.listContent li',
|
|
34
|
+
'ul.listContent li',
|
|
35
|
+
];
|
|
36
|
+
let cards = [];
|
|
37
|
+
for (const sel of selectors) {
|
|
38
|
+
cards = document.querySelectorAll(sel);
|
|
39
|
+
if (cards.length > 0) break;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const results = [];
|
|
43
|
+
for (const card of cards) {
|
|
44
|
+
// Name is in a.img[title] or .title a
|
|
45
|
+
const imgLink = card.querySelector('a.img[title], a[title]');
|
|
46
|
+
const titleLink = card.querySelector('.title a');
|
|
47
|
+
const nameEl = titleLink || imgLink;
|
|
48
|
+
if (!nameEl) continue;
|
|
49
|
+
|
|
50
|
+
const name = (titleLink ? titleLink.textContent : imgLink.getAttribute('title')) || '';
|
|
51
|
+
const url = nameEl.href || '';
|
|
52
|
+
|
|
53
|
+
const priceEl = card.querySelector('.totalPrice span');
|
|
54
|
+
const districtEl = card.querySelector('.positionInfo a, .district a');
|
|
55
|
+
const infoEl = card.querySelector('.positionInfo, .houseInfo, .xiaoquListItemInfo');
|
|
56
|
+
const saleEl = card.querySelector('.xiaoquListItemSellCount a, .houseInfo a');
|
|
57
|
+
|
|
58
|
+
const infoText = infoEl ? infoEl.textContent : '';
|
|
59
|
+
const yearMatch = infoText.match(/(\\d{4})年/);
|
|
60
|
+
|
|
61
|
+
const priceText = (priceEl ? priceEl.textContent : '').trim();
|
|
62
|
+
|
|
63
|
+
results.push({
|
|
64
|
+
name: name.trim(),
|
|
65
|
+
url: url,
|
|
66
|
+
district: (districtEl ? districtEl.textContent : '').trim(),
|
|
67
|
+
avg_price: priceText ? priceText + '元/平' : '暂无',
|
|
68
|
+
year: yearMatch ? yearMatch[1] : '',
|
|
69
|
+
on_sale: (saleEl ? saleEl.textContent : '').trim(),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return results;
|
|
73
|
+
})()`);
|
|
74
|
+
|
|
75
|
+
return (items || []).slice(0, limit);
|
|
76
|
+
},
|
|
77
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
import { gotoKe } from './utils.js';
|
|
3
|
+
|
|
4
|
+
cli({
|
|
5
|
+
site: 'ke',
|
|
6
|
+
name: 'zufang',
|
|
7
|
+
description: '贝壳找房租房列表',
|
|
8
|
+
domain: 'ke.com',
|
|
9
|
+
strategy: Strategy.COOKIE,
|
|
10
|
+
browser: true,
|
|
11
|
+
args: [
|
|
12
|
+
{ name: 'city', default: 'bj', help: '城市代码,如 bj(北京), sh(上海), gz(广州), sz(深圳), zs(中山)' },
|
|
13
|
+
{ name: 'district', help: '区域拼音,如 chaoyang, haidian' },
|
|
14
|
+
{ name: 'min-price', type: 'int', help: '最低月租(元)' },
|
|
15
|
+
{ name: 'max-price', type: 'int', help: '最高月租(元)' },
|
|
16
|
+
{ name: 'limit', type: 'int', default: 20, help: '返回数量' },
|
|
17
|
+
],
|
|
18
|
+
columns: ['title', 'community', 'area', 'layout', 'price', 'url'],
|
|
19
|
+
func: async (page, kwargs) => {
|
|
20
|
+
const city = kwargs.city || 'bj';
|
|
21
|
+
const limit = Number(kwargs.limit) || 20;
|
|
22
|
+
|
|
23
|
+
let path = '/zufang/';
|
|
24
|
+
if (kwargs.district) {
|
|
25
|
+
path = `/zufang/${kwargs.district}/`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const priceParts = [];
|
|
29
|
+
if (kwargs['min-price'] || kwargs['max-price']) {
|
|
30
|
+
const min = kwargs['min-price'] || '';
|
|
31
|
+
const max = kwargs['max-price'] || '';
|
|
32
|
+
priceParts.push(`rp${min}t${max}`);
|
|
33
|
+
}
|
|
34
|
+
const filters = priceParts.join('');
|
|
35
|
+
|
|
36
|
+
const baseUrl = `https://${city}.zu.ke.com`;
|
|
37
|
+
const url = baseUrl + path + (filters ? filters + '/' : '');
|
|
38
|
+
|
|
39
|
+
await gotoKe(page, url);
|
|
40
|
+
|
|
41
|
+
const items = await page.evaluate(`(async () => {
|
|
42
|
+
const allLinks = document.querySelectorAll('a.twoline');
|
|
43
|
+
const results = [];
|
|
44
|
+
for (const titleEl of allLinks) {
|
|
45
|
+
let card = titleEl.closest('div');
|
|
46
|
+
if (!card) continue;
|
|
47
|
+
while (card && card.parentElement && !card.parentElement.classList.contains('content__list')) {
|
|
48
|
+
card = card.parentElement;
|
|
49
|
+
}
|
|
50
|
+
if (!card) continue;
|
|
51
|
+
|
|
52
|
+
const title = (titleEl.textContent || '').replace(/\\s+/g, ' ').trim();
|
|
53
|
+
const href = titleEl.getAttribute('href') || '';
|
|
54
|
+
const fullUrl = href.startsWith('http') ? href : '${baseUrl}' + href;
|
|
55
|
+
|
|
56
|
+
const allPs = card.querySelectorAll('p');
|
|
57
|
+
let community = '', area = '', layout = '';
|
|
58
|
+
for (const p of allPs) {
|
|
59
|
+
if ((p.className || '').indexOf('des') === -1) continue;
|
|
60
|
+
const links = p.querySelectorAll('a[title]');
|
|
61
|
+
if (links.length > 0) {
|
|
62
|
+
community = (links[links.length - 1].getAttribute('title') || '').trim();
|
|
63
|
+
}
|
|
64
|
+
const parts = p.textContent.replace(/\\s+/g, ' ').trim().split('/');
|
|
65
|
+
for (const part of parts) {
|
|
66
|
+
const t = part.trim();
|
|
67
|
+
if (/\\u33A1|\\u5E73\\u7C73/.test(t)) area = t;
|
|
68
|
+
else if (/\\u5BA4.*\\u5385/.test(t)) layout = t;
|
|
69
|
+
}
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const emEls = card.querySelectorAll('em');
|
|
74
|
+
let priceText = '';
|
|
75
|
+
for (const em of emEls) {
|
|
76
|
+
const t = em.textContent.trim();
|
|
77
|
+
if (/^\\d+$/.test(t)) { priceText = t; break; }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
results.push({
|
|
81
|
+
title,
|
|
82
|
+
url: fullUrl,
|
|
83
|
+
community,
|
|
84
|
+
area,
|
|
85
|
+
layout,
|
|
86
|
+
price: priceText ? priceText + '\\u5143/\\u6708' : '',
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return results;
|
|
90
|
+
})()`);
|
|
91
|
+
|
|
92
|
+
return (items || []).slice(0, limit);
|
|
93
|
+
},
|
|
94
|
+
});
|
|
@@ -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
|
+
});
|
package/clis/mubu/doc.js
ADDED
|
@@ -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
|
+
});
|