From 790c94d64494b176fd74f5acfec7213a5d673e9e Mon Sep 17 00:00:00 2001 From: jackwener Date: Sun, 15 Mar 2026 16:03:45 +0800 Subject: [PATCH] feat: Add Xueqiu site with 6 new commands and a Twitter bookmarks command, updating documentation. --- CLI-CREATOR.md | 6 ++- README.md | 5 +- README.zh-CN.md | 5 +- SKILL.md | 9 +++- src/clis/twitter/bookmarks.yaml | 85 +++++++++++++++++++++++++++++++++ src/clis/xueqiu/feed.yaml | 53 ++++++++++++++++++++ src/clis/xueqiu/hot-stock.yaml | 49 +++++++++++++++++++ src/clis/xueqiu/hot.yaml | 46 ++++++++++++++++++ src/clis/xueqiu/search.yaml | 53 ++++++++++++++++++++ src/clis/xueqiu/stock.yaml | 67 ++++++++++++++++++++++++++ src/clis/xueqiu/watchlist.yaml | 46 ++++++++++++++++++ 11 files changed, 418 insertions(+), 6 deletions(-) create mode 100644 src/clis/twitter/bookmarks.yaml create mode 100644 src/clis/xueqiu/feed.yaml create mode 100644 src/clis/xueqiu/hot-stock.yaml create mode 100644 src/clis/xueqiu/hot.yaml create mode 100644 src/clis/xueqiu/search.yaml create mode 100644 src/clis/xueqiu/stock.yaml create mode 100644 src/clis/xueqiu/watchlist.yaml diff --git a/CLI-CREATOR.md b/CLI-CREATOR.md index 51b9dac..7ac5ea7 100644 --- a/CLI-CREATOR.md +++ b/CLI-CREATOR.md @@ -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` 等命令提取。这将彻底消灭跨操作系统下产生的第三方二进制库依赖。 + --- ## 常见陷阱 diff --git a/README.md b/README.md index ce3d4b3..b9a4e5a 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [![npm](https://img.shields.io/npm/v/@jackwener/opencli)](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 | diff --git a/README.zh-CN.md b/README.zh-CN.md index ec5ba21..3490302 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -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` | 🔐 浏览器 | diff --git a/SKILL.md b/SKILL.md index 1075423..7e7237c 100644 --- a/SKILL.md +++ b/SKILL.md @@ -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 # 热门帖子 diff --git a/src/clis/twitter/bookmarks.yaml b/src/clis/twitter/bookmarks.yaml new file mode 100644 index 0000000..1fa59d2 --- /dev/null +++ b/src/clis/twitter/bookmarks.yaml @@ -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] diff --git a/src/clis/xueqiu/feed.yaml b/src/clis/xueqiu/feed.yaml new file mode 100644 index 0000000..3e8172a --- /dev/null +++ b/src/clis/xueqiu/feed.yaml @@ -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] diff --git a/src/clis/xueqiu/hot-stock.yaml b/src/clis/xueqiu/hot-stock.yaml new file mode 100644 index 0000000..9964c10 --- /dev/null +++ b/src/clis/xueqiu/hot-stock.yaml @@ -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] diff --git a/src/clis/xueqiu/hot.yaml b/src/clis/xueqiu/hot.yaml new file mode 100644 index 0000000..c7370ac --- /dev/null +++ b/src/clis/xueqiu/hot.yaml @@ -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] diff --git a/src/clis/xueqiu/search.yaml b/src/clis/xueqiu/search.yaml new file mode 100644 index 0000000..ed7ab76 --- /dev/null +++ b/src/clis/xueqiu/search.yaml @@ -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] diff --git a/src/clis/xueqiu/stock.yaml b/src/clis/xueqiu/stock.yaml new file mode 100644 index 0000000..45d09e4 --- /dev/null +++ b/src/clis/xueqiu/stock.yaml @@ -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] diff --git a/src/clis/xueqiu/watchlist.yaml b/src/clis/xueqiu/watchlist.yaml new file mode 100644 index 0000000..7304f73 --- /dev/null +++ b/src/clis/xueqiu/watchlist.yaml @@ -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]