feat: Add Xueqiu site with 6 new commands and a Twitter bookmarks command, updating documentation.
This commit is contained in:
1 parent
aef110a8f8
commit
790c94d644
11 files changed
+418
-6
No files matched your search
+5
-1
@@ -55,7 +55,7 @@ opencli bilibili hot -v # 查看已有命令的 pipeline 每步数据流
|
||||
|
||||
在开始死磕复杂的抓包拦截之前,按照以下优先级进行尝试:
|
||||
|
||||
1. **后缀爆破法 (`.json`)**: 像 Reddit 这样复杂的网站,只要在其 URL 后加上 `.json`(例如 `/r/all.json`),就能在带 Cookie 的情况下直接利用 `fetch` 拿到极其干净的 REST 数据(Tier 2 Cookie 策略极速秒杀)。
|
||||
1. **后缀爆破法 (`.json`)**: 像 Reddit 这样复杂的网站,只要在其 URL 后加上 `.json`(例如 `/r/all.json`),就能在带 Cookie 的情况下直接利用 `fetch` 拿到极其干净的 REST 数据(Tier 2 Cookie 策略极速秒杀)。另外如功能完备的**雪球 (xueqiu)** 也可以走这种纯 API 的方式极简获取,成为你构建简单 YAML 的黄金标杆。
|
||||
2. **全局状态查找法 (`__INITIAL_STATE__`)**: 许多服务端渲染 (SSR) 的网站(如小红书、Bilibili)会将首页或详情页的完整数据挂载到全局 window 对象上。与其去拦截网络请求,不如直接 `page.evaluate('() => window.__INITIAL_STATE__')` 获取整个数据树。
|
||||
3. **框架探测与 Store Action 截断**: 如果站点使用 Vue + Pinia,可以使用 `tap` 步骤调用 action,让前端框架代替你完成复杂的鉴权签名封装。
|
||||
4. **底层 XHR/Fetch 拦截**: 最后手段,当上述都不行时,使用 TypeScript 适配器进行无侵入式的请求抓取。
|
||||
@@ -533,6 +533,10 @@ git commit -m "feat(mysite): add hot and search adapters"
|
||||
git push
|
||||
```
|
||||
|
||||
## 设计哲学: Zero-Dependency jq
|
||||
|
||||
> 💡 **架构理念升级**: OpenCLI 的原生机制本质上内建了一个 **Zero-Dependency jq 数据处理流**。使用时不需要依赖系统命令级别的 `jq` 包,而是将所有的解析拍平动作放在 `evaluate` 块内的原生 JavaScript 里,再由外层 YAML 通过 `select`、`map` 等命令提取。这将彻底消灭跨操作系统下产生的第三方二进制库依赖。
|
||||
|
||||
---
|
||||
|
||||
## 常见陷阱
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
[](https://www.npmjs.com/package/@jackwener/opencli)
|
||||
|
||||
A CLI tool that turns **any website** into a command-line interface. **28+ commands** across **16 sites** — bilibili, zhihu, xiaohongshu, twitter, reddit, github, v2ex, hackernews, bbc, weibo, boss, yahoo-finance, reuters, smzdm, ctrip, youtube — powered by browser session reuse and AI-native discovery.
|
||||
A CLI tool that turns **any website** into a command-line interface. **35+ commands** across **17 sites** — bilibili, zhihu, xiaohongshu, twitter, reddit, xueqiu, github, v2ex, hackernews, bbc, weibo, boss, yahoo-finance, reuters, smzdm, ctrip, youtube — powered by browser session reuse and AI-native discovery.
|
||||
|
||||
## ✨ Highlights
|
||||
|
||||
@@ -85,7 +85,8 @@ Public API commands (`hackernews`, `github search`, `v2ex`) need no browser at a
|
||||
| **bilibili** | `hot` `search` `me` `favorite` `history` `feed` `user-videos` | 🔐 Browser |
|
||||
| **zhihu** | `hot` `search` `question` | 🔐 Browser |
|
||||
| **xiaohongshu** | `search` `notifications` `feed` | 🔐 Browser |
|
||||
| **twitter** | `trending` | 🔐 Browser |
|
||||
| **xueqiu** | `feed` `hot-stock` `hot` `search` `stock` `watchlist` | 🔐 Browser |
|
||||
| **twitter** | `trending` `bookmarks` | 🔐 Browser |
|
||||
| **reddit** | `hot` | 🔐 Browser |
|
||||
| **weibo** | `hot` | 🔐 Browser |
|
||||
| **boss** | `search` | 🔐 Browser |
|
||||
|
||||
+3
-2
@@ -11,7 +11,7 @@ OpenCLI 通过 Chrome 浏览器 + [Playwright MCP Bridge](https://github.com/nic
|
||||
|
||||
## ✨ 亮点
|
||||
|
||||
- 🌐 **28+ 命令,16 个站点** — B站、知乎、小红书、Twitter、Reddit、GitHub、V2EX、Hacker News、BBC、微博、BOSS直聘、Yahoo Finance、路透社、什么值得买、携程、YouTube
|
||||
- 🌐 **35+ 命令,17 个站点** — B站、知乎、小红书、Twitter、Reddit、雪球(xueqiu)、GitHub、V2EX、Hacker News、BBC、微博、BOSS直聘、Yahoo Finance、路透社、什么值得买、携程、YouTube
|
||||
- 🔐 **零风控** — 复用 Chrome 登录态,无需存储任何凭证
|
||||
- 🤖 **AI 原生** — `explore` 自动发现 API,`synthesize` 生成适配器,`cascade` 探测认证策略
|
||||
- 🚀 **动态加载引擎** — 只需将 `.ts` 或 `.yaml` 适配器放入 `clis/` 文件夹即可自动注册生效
|
||||
@@ -86,7 +86,8 @@ npm install -g @jackwener/opencli@latest
|
||||
| **bilibili** | `hot` `search` `me` `favorite` `history` `feed` `user-videos` | 🔐 浏览器 |
|
||||
| **zhihu** | `hot` `search` `question` | 🔐 浏览器 |
|
||||
| **xiaohongshu** | `search` `notifications` `feed` | 🔐 浏览器 |
|
||||
| **twitter** | `trending` | 🔐 浏览器 |
|
||||
| **xueqiu** | `feed` `hot-stock` `hot` `search` `stock` `watchlist` | 🔐 浏览器 |
|
||||
| **twitter** | `trending` `bookmarks` | 🔐 浏览器 |
|
||||
| **reddit** | `hot` | 🔐 浏览器 |
|
||||
| **weibo** | `hot` | 🔐 浏览器 |
|
||||
| **boss** | `search` | 🔐 浏览器 |
|
||||
|
||||
@@ -3,7 +3,7 @@ name: opencli
|
||||
description: "OpenCLI — Make any website your CLI. Zero risk, AI-powered, reuse Chrome login."
|
||||
version: 0.1.0
|
||||
author: jackwener
|
||||
tags: [cli, browser, web, mcp, playwright, bilibili, zhihu, twitter, github, v2ex, hackernews, reddit, xiaohongshu, AI, agent]
|
||||
tags: [cli, browser, web, mcp, playwright, bilibili, zhihu, twitter, github, v2ex, hackernews, reddit, xiaohongshu, xueqiu, AI, agent]
|
||||
---
|
||||
|
||||
# OpenCLI
|
||||
@@ -60,12 +60,19 @@ opencli xiaohongshu search --keyword "美食" # 搜索笔记
|
||||
opencli xiaohongshu notifications # 通知(mentions/likes/connections)
|
||||
opencli xiaohongshu feed --limit 10 # 推荐 Feed
|
||||
|
||||
# 雪球 Xueqiu (browser)
|
||||
opencli xueqiu hot-stock --limit 10 # 雪球热门股票榜
|
||||
opencli xueqiu stock --symbol SH600519 # 查看股票实时行情
|
||||
opencli xueqiu watchlist # 获取自选股/持仓列表
|
||||
opencli xueqiu feed # 我的关注 timeline
|
||||
|
||||
# GitHub (trending=browser, search=public)
|
||||
opencli github trending --limit 10 # GitHub Trending
|
||||
opencli github search --keyword "cli" # 搜索仓库
|
||||
|
||||
# Twitter/X (browser)
|
||||
opencli twitter trending --limit 10 # 热门话题
|
||||
opencli twitter bookmarks --limit 20 # 获取收藏的书签推文
|
||||
|
||||
# Reddit (browser)
|
||||
opencli reddit hot --limit 10 # 热门帖子
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
site: twitter
|
||||
name: bookmarks
|
||||
description: 获取 Twitter 书签列表
|
||||
domain: x.com
|
||||
browser: true
|
||||
|
||||
args:
|
||||
limit:
|
||||
type: int
|
||||
default: 20
|
||||
description: Number of bookmarks to return (default 20)
|
||||
|
||||
pipeline:
|
||||
- navigate: https://x.com/i/bookmarks
|
||||
- wait: 2
|
||||
- evaluate: |
|
||||
(async () => {
|
||||
const ct0 = document.cookie.split(';').map(c=>c.trim()).find(c=>c.startsWith('ct0='))?.split('=')[1];
|
||||
if (!ct0) throw new Error('No ct0 cookie. Hint: Not logged into x.com.');
|
||||
const bearer = decodeURIComponent('AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA');
|
||||
const _h = {'Authorization':'Bearer '+bearer, 'X-Csrf-Token':ct0, 'X-Twitter-Auth-Type':'OAuth2Session', 'X-Twitter-Active-User':'yes'};
|
||||
|
||||
const count = Math.min(${{ args.limit }}, 100);
|
||||
const variables = JSON.stringify({count, includePromotedContent: false});
|
||||
const features = JSON.stringify({
|
||||
rweb_video_screen_enabled: false, profile_label_improvements_pcf_label_in_post_enabled: true,
|
||||
responsive_web_profile_redirect_enabled: false, rweb_tipjar_consumption_enabled: false,
|
||||
verified_phone_label_enabled: false, creator_subscriptions_tweet_preview_api_enabled: true,
|
||||
responsive_web_graphql_timeline_navigation_enabled: true,
|
||||
responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
|
||||
premium_content_api_read_enabled: false, communities_web_enable_tweet_community_results_fetch: true,
|
||||
c9s_tweet_anatomy_moderator_badge_enabled: true,
|
||||
articles_preview_enabled: true, responsive_web_edit_tweet_api_enabled: true,
|
||||
graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
|
||||
view_counts_everywhere_api_enabled: true, longform_notetweets_consumption_enabled: true,
|
||||
responsive_web_twitter_article_tweet_consumption_enabled: true,
|
||||
tweet_awards_web_tipping_enabled: false,
|
||||
content_disclosure_indicator_enabled: true, content_disclosure_ai_generated_indicator_enabled: true,
|
||||
freedom_of_speech_not_reach_fetch_enabled: true, standardized_nudges_misinfo: true,
|
||||
tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true,
|
||||
longform_notetweets_rich_text_read_enabled: true, longform_notetweets_inline_media_enabled: false,
|
||||
responsive_web_enhance_cards_enabled: false
|
||||
});
|
||||
const url = '/i/api/graphql/Fy0QMy4q_aZCpkO0PnyLYw/Bookmarks?variables=' + encodeURIComponent(variables) + '&features=' + encodeURIComponent(features);
|
||||
const resp = await fetch(url, {headers: _h, credentials: 'include'});
|
||||
if (!resp.ok) throw new Error('HTTP ' + resp.status + '. Hint: queryId may have changed.');
|
||||
const d = await resp.json();
|
||||
|
||||
const instructions = d.data?.bookmark_timeline_v2?.timeline?.instructions || d.data?.bookmark_timeline?.timeline?.instructions || [];
|
||||
let tweets = [], seen = new Set();
|
||||
for (const inst of instructions) {
|
||||
for (const entry of (inst.entries || [])) {
|
||||
const r = entry.content?.itemContent?.tweet_results?.result;
|
||||
if (!r) continue;
|
||||
const tw = r.tweet || r;
|
||||
const l = tw.legacy || {};
|
||||
if (!tw.rest_id || seen.has(tw.rest_id)) continue;
|
||||
seen.add(tw.rest_id);
|
||||
const u = tw.core?.user_results?.result;
|
||||
const nt = tw.note_tweet?.note_tweet_results?.result?.text;
|
||||
const screenName = u?.legacy?.screen_name || u?.core?.screen_name;
|
||||
tweets.push({
|
||||
id: tw.rest_id,
|
||||
author: screenName,
|
||||
name: u?.legacy?.name || u?.core?.name,
|
||||
url: 'https://x.com/' + (screenName || '_') + '/status/' + tw.rest_id,
|
||||
text: nt || l.full_text || '',
|
||||
likes: l.favorite_count,
|
||||
retweets: l.retweet_count,
|
||||
created_at: l.created_at
|
||||
});
|
||||
}
|
||||
}
|
||||
return tweets;
|
||||
})()
|
||||
|
||||
- map:
|
||||
author: ${{ item.author }}
|
||||
text: ${{ item.text }}
|
||||
likes: ${{ item.likes }}
|
||||
url: ${{ item.url }}
|
||||
|
||||
- limit: ${{ args.limit }}
|
||||
|
||||
columns: [author, text, likes, url]
|
||||
@@ -0,0 +1,53 @@
|
||||
site: xueqiu
|
||||
name: feed
|
||||
description: 获取雪球首页时间线(关注用户的动态)
|
||||
domain: xueqiu.com
|
||||
browser: true
|
||||
|
||||
args:
|
||||
page:
|
||||
type: int
|
||||
default: 1
|
||||
description: 页码,默认 1
|
||||
limit:
|
||||
type: int
|
||||
default: 20
|
||||
description: 每页数量,默认 20
|
||||
|
||||
pipeline:
|
||||
- navigate: https://xueqiu.com
|
||||
- evaluate: |
|
||||
(async () => {
|
||||
const page = ${{ args.page }};
|
||||
const count = ${{ args.limit }};
|
||||
const resp = await fetch(`https://xueqiu.com/v4/statuses/home_timeline.json?page=${page}&count=${count}`, {credentials: 'include'});
|
||||
if (!resp.ok) throw new Error('HTTP ' + resp.status + ' Hint: Not logged in?');
|
||||
const d = await resp.json();
|
||||
|
||||
const strip = (html) => (html || '').replace(/<[^>]+>/g, '').replace(/ /g, ' ').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').trim();
|
||||
const list = d.home_timeline || d.list || [];
|
||||
return list.map(item => {
|
||||
const user = item.user || {};
|
||||
return {
|
||||
id: item.id,
|
||||
text: strip(item.description).substring(0, 200),
|
||||
url: 'https://xueqiu.com/' + user.id + '/' + item.id,
|
||||
author: user.screen_name,
|
||||
likes: item.fav_count,
|
||||
retweets: item.retweet_count,
|
||||
replies: item.reply_count,
|
||||
created_at: item.created_at ? new Date(item.created_at).toISOString() : null
|
||||
};
|
||||
});
|
||||
})()
|
||||
|
||||
- map:
|
||||
author: ${{ item.author }}
|
||||
text: ${{ item.text }}
|
||||
likes: ${{ item.likes }}
|
||||
replies: ${{ item.replies }}
|
||||
url: ${{ item.url }}
|
||||
|
||||
- limit: ${{ args.limit }}
|
||||
|
||||
columns: [author, text, likes, replies, url]
|
||||
@@ -0,0 +1,49 @@
|
||||
site: xueqiu
|
||||
name: hot-stock
|
||||
description: 获取雪球热门股票榜
|
||||
domain: xueqiu.com
|
||||
browser: true
|
||||
|
||||
args:
|
||||
limit:
|
||||
type: int
|
||||
default: 20
|
||||
description: 返回数量,默认 20,最大 50
|
||||
type:
|
||||
type: str
|
||||
default: "10"
|
||||
description: 榜单类型 10=人气榜(默认) 12=关注榜
|
||||
|
||||
pipeline:
|
||||
- navigate: https://xueqiu.com
|
||||
- evaluate: |
|
||||
(async () => {
|
||||
const count = ${{ args.limit }};
|
||||
const type = ${{ args.type | json }};
|
||||
const resp = await fetch(`https://stock.xueqiu.com/v5/stock/hot_stock/list.json?size=${count}&type=${type}`, {credentials: 'include'});
|
||||
if (!resp.ok) throw new Error('HTTP ' + resp.status + ' Hint: Not logged in?');
|
||||
const d = await resp.json();
|
||||
if (!d.data || !d.data.items) throw new Error('获取失败');
|
||||
return d.data.items.map((s, i) => ({
|
||||
rank: i + 1,
|
||||
symbol: s.symbol,
|
||||
name: s.name,
|
||||
price: s.current,
|
||||
changePercent: s.percent != null ? s.percent.toFixed(2) + '%' : null,
|
||||
heat: s.value,
|
||||
rank_change: s.rank_change,
|
||||
url: 'https://xueqiu.com/S/' + s.symbol
|
||||
}));
|
||||
})()
|
||||
|
||||
- map:
|
||||
rank: ${{ item.rank }}
|
||||
symbol: ${{ item.symbol }}
|
||||
name: ${{ item.name }}
|
||||
price: ${{ item.price }}
|
||||
changePercent: ${{ item.changePercent }}
|
||||
heat: ${{ item.heat }}
|
||||
|
||||
- limit: ${{ args.limit }}
|
||||
|
||||
columns: [rank, symbol, name, price, changePercent, heat]
|
||||
@@ -0,0 +1,46 @@
|
||||
site: xueqiu
|
||||
name: hot
|
||||
description: 获取雪球热门动态
|
||||
domain: xueqiu.com
|
||||
browser: true
|
||||
|
||||
args:
|
||||
limit:
|
||||
type: int
|
||||
default: 20
|
||||
description: 返回数量,默认 20,最大 50
|
||||
|
||||
pipeline:
|
||||
- navigate: https://xueqiu.com
|
||||
- evaluate: |
|
||||
(async () => {
|
||||
const resp = await fetch('https://xueqiu.com/statuses/hot/listV3.json?source=hot&page=1', {credentials: 'include'});
|
||||
if (!resp.ok) throw new Error('HTTP ' + resp.status + ' Hint: Not logged in?');
|
||||
const d = await resp.json();
|
||||
const list = d.list || [];
|
||||
|
||||
const strip = (html) => (html || '').replace(/<[^>]+>/g, '').replace(/ /g, ' ').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').trim();
|
||||
return list.map((item, i) => {
|
||||
const user = item.user || {};
|
||||
return {
|
||||
rank: i + 1,
|
||||
text: strip(item.description).substring(0, 200),
|
||||
url: 'https://xueqiu.com/' + user.id + '/' + item.id,
|
||||
author: user.screen_name,
|
||||
likes: item.fav_count,
|
||||
retweets: item.retweet_count,
|
||||
replies: item.reply_count
|
||||
};
|
||||
});
|
||||
})()
|
||||
|
||||
- map:
|
||||
rank: ${{ item.rank }}
|
||||
author: ${{ item.author }}
|
||||
text: ${{ item.text }}
|
||||
likes: ${{ item.likes }}
|
||||
url: ${{ item.url }}
|
||||
|
||||
- limit: ${{ args.limit }}
|
||||
|
||||
columns: [rank, author, text, likes, url]
|
||||
@@ -0,0 +1,53 @@
|
||||
site: xueqiu
|
||||
name: search
|
||||
description: 搜索雪球股票(代码或名称)
|
||||
domain: xueqiu.com
|
||||
browser: true
|
||||
|
||||
args:
|
||||
query:
|
||||
type: str
|
||||
description: 搜索关键词,如 茅台、AAPL、腾讯
|
||||
limit:
|
||||
type: int
|
||||
default: 10
|
||||
description: 返回数量,默认 10
|
||||
|
||||
pipeline:
|
||||
- navigate: https://xueqiu.com
|
||||
- evaluate: |
|
||||
(async () => {
|
||||
const query = ${{ args.query | json }};
|
||||
const count = ${{ args.limit }};
|
||||
if (!query) throw new Error('Missing argument: query');
|
||||
const resp = await fetch(`https://xueqiu.com/stock/search.json?code=${encodeURIComponent(query)}&size=${count}`, {credentials: 'include'});
|
||||
if (!resp.ok) throw new Error('HTTP ' + resp.status + ' Hint: Not logged in?');
|
||||
const d = await resp.json();
|
||||
return (d.stocks || []).map(s => {
|
||||
let symbol = '';
|
||||
if (s.exchange === 'SH' || s.exchange === 'SZ' || s.exchange === 'BJ') {
|
||||
symbol = s.code.startsWith(s.exchange) ? s.code : s.exchange + s.code;
|
||||
} else {
|
||||
symbol = s.code;
|
||||
}
|
||||
return {
|
||||
symbol: symbol,
|
||||
name: s.name,
|
||||
exchange: s.exchange,
|
||||
price: s.current,
|
||||
changePercent: s.percentage != null ? s.percentage.toFixed(2) + '%' : null,
|
||||
url: 'https://xueqiu.com/S/' + symbol
|
||||
};
|
||||
});
|
||||
})()
|
||||
|
||||
- map:
|
||||
symbol: ${{ item.symbol }}
|
||||
name: ${{ item.name }}
|
||||
exchange: ${{ item.exchange }}
|
||||
price: ${{ item.price }}
|
||||
changePercent: ${{ item.changePercent }}
|
||||
|
||||
- limit: ${{ args.limit }}
|
||||
|
||||
columns: [symbol, name, exchange, price, changePercent]
|
||||
@@ -0,0 +1,67 @@
|
||||
site: xueqiu
|
||||
name: stock
|
||||
description: 获取雪球股票实时行情
|
||||
domain: xueqiu.com
|
||||
browser: true
|
||||
|
||||
args:
|
||||
symbol:
|
||||
type: str
|
||||
description: 股票代码,如 SH600519、SZ000858、AAPL、00700
|
||||
|
||||
pipeline:
|
||||
- navigate: https://xueqiu.com
|
||||
- evaluate: |
|
||||
(async () => {
|
||||
const symbol = (${{ args.symbol | json }} || '').toUpperCase();
|
||||
if (!symbol) throw new Error('Missing argument: symbol');
|
||||
const resp = await fetch(`https://stock.xueqiu.com/v5/stock/batch/quote.json?symbol=${encodeURIComponent(symbol)}`, {credentials: 'include'});
|
||||
if (!resp.ok) throw new Error('HTTP ' + resp.status + ' Hint: Not logged in?');
|
||||
const d = await resp.json();
|
||||
if (!d.data || !d.data.items || d.data.items.length === 0) throw new Error('未找到股票: ' + symbol);
|
||||
|
||||
function fmtAmount(v) {
|
||||
if (v == null) return null;
|
||||
if (Math.abs(v) >= 1e12) return (v / 1e12).toFixed(2) + '万亿';
|
||||
if (Math.abs(v) >= 1e8) return (v / 1e8).toFixed(2) + '亿';
|
||||
if (Math.abs(v) >= 1e4) return (v / 1e4).toFixed(2) + '万';
|
||||
return v.toString();
|
||||
}
|
||||
|
||||
const item = d.data.items[0];
|
||||
const q = item.quote || {};
|
||||
const m = item.market || {};
|
||||
|
||||
return [{
|
||||
name: q.name,
|
||||
symbol: q.symbol,
|
||||
exchange: q.exchange,
|
||||
currency: q.currency,
|
||||
price: q.current,
|
||||
change: q.chg,
|
||||
changePercent: q.percent != null ? q.percent.toFixed(2) + '%' : null,
|
||||
open: q.open,
|
||||
high: q.high,
|
||||
low: q.low,
|
||||
prevClose: q.last_close,
|
||||
amplitude: q.amplitude != null ? q.amplitude.toFixed(2) + '%' : null,
|
||||
volume: q.volume,
|
||||
amount: fmtAmount(q.amount),
|
||||
turnover_rate: q.turnover_rate != null ? q.turnover_rate.toFixed(2) + '%' : null,
|
||||
marketCap: fmtAmount(q.market_capital),
|
||||
floatMarketCap: fmtAmount(q.float_market_capital),
|
||||
ytdPercent: q.current_year_percent != null ? q.current_year_percent.toFixed(2) + '%' : null,
|
||||
market_status: m.status || null,
|
||||
time: q.timestamp ? new Date(q.timestamp).toISOString() : null,
|
||||
url: 'https://xueqiu.com/S/' + q.symbol
|
||||
}];
|
||||
})()
|
||||
|
||||
- map:
|
||||
name: ${{ item.name }}
|
||||
symbol: ${{ item.symbol }}
|
||||
price: ${{ item.price }}
|
||||
changePercent: ${{ item.changePercent }}
|
||||
marketCap: ${{ item.marketCap }}
|
||||
|
||||
columns: [name, symbol, price, changePercent, marketCap]
|
||||
@@ -0,0 +1,46 @@
|
||||
site: xueqiu
|
||||
name: watchlist
|
||||
description: 获取雪球自选股列表
|
||||
domain: xueqiu.com
|
||||
browser: true
|
||||
|
||||
args:
|
||||
category:
|
||||
type: str # using str to prevent parsing issues like 01
|
||||
default: "1"
|
||||
description: "分类:1=自选(默认) 2=持仓 3=关注"
|
||||
limit:
|
||||
type: int
|
||||
default: 100
|
||||
description: 默认 100
|
||||
|
||||
pipeline:
|
||||
- navigate: https://xueqiu.com
|
||||
- evaluate: |
|
||||
(async () => {
|
||||
const category = parseInt(${{ args.category | json }}) || 1;
|
||||
const resp = await fetch(`https://stock.xueqiu.com/v5/stock/portfolio/stock/list.json?size=100&category=${category}&pid=-1`, {credentials: 'include'});
|
||||
if (!resp.ok) throw new Error('HTTP ' + resp.status + ' Hint: Not logged in?');
|
||||
const d = await resp.json();
|
||||
if (!d.data || !d.data.stocks) throw new Error('获取失败,可能未登录');
|
||||
|
||||
return d.data.stocks.map(s => ({
|
||||
symbol: s.symbol,
|
||||
name: s.name,
|
||||
price: s.current,
|
||||
change: s.chg,
|
||||
changePercent: s.percent != null ? s.percent.toFixed(2) + '%' : null,
|
||||
volume: s.volume,
|
||||
url: 'https://xueqiu.com/S/' + s.symbol
|
||||
}));
|
||||
})()
|
||||
|
||||
- map:
|
||||
symbol: ${{ item.symbol }}
|
||||
name: ${{ item.name }}
|
||||
price: ${{ item.price }}
|
||||
changePercent: ${{ item.changePercent }}
|
||||
|
||||
- limit: ${{ args.limit }}
|
||||
|
||||
columns: [symbol, name, price, changePercent]
|
||||
Reference in new issue
Block a user