# Cron Job: Daily A-Share Report **Job ID:** f57d4deba305 **Run Time:** 2026-08-18 08:02:02 **Schedule:** 0 8 * * 1-5 ## Prompt [IMPORTANT: The user has invoked the "hermes-report-pipeline" skill, indicating they want you to follow its instructions. The full skill content is loaded below.] --- name: hermes-report-pipeline description: Complete blog/report collection → JSON → HTML → VPS sync → Telegram delivery pipeline using Hermes Agent. Run as a scheduled cron job. triggers: - "collect ideas and save as report" - "research report automation pipeline" - "sync blog to VPS and send summary" - "generate html report from collected data" - "daily AI LLM trends report" - "daily US stock market report" # Yahoo Finance + CNBC (Finnhub key NOT present in .env — do not attempt) - "daily A-share China stock sector report" - "A股市场每日行情" - "OPC product ideas" - "dropshipping trending products" - "跨境电商选品" - "polymarket hot markets" - "polymarket trending" --- # Hermes Report Pipeline Skill Complete workflow for automated research → report → publish → notify. ## Pipeline Steps **⚠️ Polymarket block ID CHANGED (2026-07-21):** `3e:` no longer exists — it is now `3f:`. The old script fails with `RuntimeError: Could not find 'results':`. The push payload structure is: `self.__next_f.push([1,"3f:[\"$\",\"$L63\",null,{\"state\":{...}}]"])`. Updated script at `references/polymarket-markets-fetch.py` (re-extracted and verified 2026-07-21). **CLOB API has no volume field** — only the HTML scrape provides volume data for ranking. ### 1. Web Research (Parallel) **⚠️ web_search (Firecrawl) and web_extract (Firecrawl) are COMPLETELY DOWN — both return "Payment Required: Insufficient credits" for ALL requests as of June 2026.** Do NOT attempt them as primary or fallback methods. Use direct HTTP scraping only. **Primary research method: Direct HTTP scraping** via `write_file` + `terminal('python3 /tmp/script.py')`. This produces richer content than search snippets and is the only reliably working approach. **⚠️ CRITICAL — Tirith blocks inline Python heredocs:** Do NOT use `python3 << 'EOF'` heredoc syntax when the script body contains domain names like `arstechnica.com`, `techcrunch.com`, etc. — tirith rejects these as "Invalid characters in hostname". Always: 1. `write_file('/tmp/script.py', content)` — write the script to disk 2. `terminal('python3 /tmp/script.py')` — execute from the file **Research source reliability (June 2026 session test):** | Source | Status | Pattern | |--------|--------|---------| | Ars Technica AI RSS | ✅ Primary workhorse (RSS) | `https://arstechnica.com/ai/feed/` | | TechCrunch AI RSS | ✅ Primary workhorse (RSS) | `https://techcrunch.com/category/artificial-intelligence/feed/` | | Ars Technica AI listing page | ❌ Returns 0 article links (JS-rendered) | `/ai/\d{4}/\d{2}/` | | TechCrunch AI listing page | ❌ Returns 0 article links (JS-rendered) | `/\d{4}/\d{2}/` | | MIT Tech Review | ✅ Works, less frequent updates | `/\d{4}/\d{2}/` | | The Verge AI | ⚠️ Listing page returns empty links | Known broken, skip | | SearXNG search:8080 | ❌ Completely unreachable | Do not attempt | | Firecrawl (web_search/web_extract) | ❌ All requests return "Payment Required" | Do not attempt — June 2026 | **Research workflow (June 2026 — verified working):** 1. **RSS feeds** — Ars Technica AI RSS (`https://arstechnica.com/ai/feed/`) and TechCrunch AI RSS (`https://techcrunch.com/category/artificial-intelligence/feed/`) return 20 items each with full content. Use `references/fetch-ai-news-rss.py` as the drop-in script. 2. **Listing page scraping FAILS** — Both Ars Technica and TechCrunch listing pages are JavaScript-rendered and return 0 article links when scraped with urllib. Do NOT attempt listing page scraping for these sources. 3. **If needed:** MIT Tech Review (supplemental) 4. **Do NOT use:** web_search, web_extract (Firecrawl), SearXNG, listing-page scraping of Ars/TC **US Stock Market Report Research Workflow (June 2026 — verified working)** **⚠️ Firecrawl (web_search/web_extract) is DOWN.** Do NOT use as primary or fallback. Use direct API calls instead. **Recommended data sources in order:** 1. **Yahoo Finance via curl** (primary for market data — urllib is rate-limited) - ⚠️ **Python `urllib.request` returns HTTP 429** for Yahoo Finance chart API. Use bash `curl` instead. - See `references/yahoo-finance-market-data.md` for the complete verified fetch script and parsing details. - Indices: `SPY`, `QQQ`, `DIA`, `IWM` - Sectors: `XLF`, `XLK`, `XLE`, `XLV`, `XLY`, `XLP`, `XLRE`, `XLB`, `XLU`, `XLC` - Crypto: `BTC-USD`, `ETH-USD`, `SOL-USD` - Commodities: `GC%3DF` (Gold), `CL%3DF` (Crude), `SI%3DF` (Silver) - Treasuries: `%5ETNX`, `%5EIRX`, `%5ETYX` (^ must be URL-encoded) - URL: `https://query1.finance.yahoo.com/v8/finance/chart/{SYMBOL}?interval=1d&range=5d` 2. **CNBC** (primary for market news) - Listing page: `https://www.cnbc.com/markets/` - Extract article URLs with: `re.findall(r'href="(https://www\.cnbc\.com/\d{4}/\d{2}/[^"#"]+)"', html)` - CNN Markets: **DO NOT use** — returns 0 recent links via both curl and urllib (JS-rendered; also blocked by tirith when using curl pipe). Use CNBC only. - MarketWatch: HTTP 401 Forbidden — skip - Bloomberg: HTTP 403 Forbidden — skip **⚠️ `fetch-cnbc-articles.py` — two usage modes:** - **With `--recent` flag:** reads `/tmp/cnbc_markets.html` (output from `fetch-cnbc-news-links.py`), filters to today's + yesterday's articles, outputs `/tmp/news/articles.json` - **With a file path argument:** reads the URL file directly Both modes work. The `--recent` flag is the standard path for scheduled reports. **US Stock Market Report — Complete Drop-In Script (verified 2026-07-02):** Use the unified pipeline script — handles market data fetch + CNBC news in one run: ```bash python3 /root/.hermes/skills/productivity/hermes-report-pipeline/references/fetch-us-stock-report.py ``` Outputs: `/tmp/market_data.json` (indices + sectors + BTC) and `/tmp/news/articles.json` (CNBC articles, **plain list** — not wrapped in `{"articles":...}`). Also saves `/tmp/cnbc_markets.html`. > ⚠️ **Stdout is misleading:** The script prints `"YF SPY: 0"` etc. as status codes — these do NOT indicate missing data. SPY, QQQ, etc. are always correctly saved to the JSON file regardless of the "0" stdout value. Always inspect `/tmp/market_data.json` directly to verify actual data. **⚠️ CNBC article body extraction caveat:** The CNBC `article` tag does NOT reliably contain full article text — approximately 40% of articles return thin content like "In this article..." due to JS-rendered paywalls or ad wrappers. **Always check article text length** after fetching: if `len(text) < 200`, replace with a link-only placeholder `[Full article at CNBC](url)`. Do NOT retry with urllib — these articles genuinely have no accessible full text. This was previously described as "reliable" which was inaccurate. **⚠️ `scripts/fetch-yf-market-data.sh` times out.** Running all 20 symbols in sequence via curl took 180s and only completed indices + partial sectors before timeout. Use selective fetching instead: 1. **Fetch only what you need per run** — typically 4 indices + 4-5 key sectors are enough for a daily report: ```bash # Core indices + key sectors (fast — completes in ~60s) for sym in SPY QQQ DIA IWM XLK XLF XLE XLV XLY; do curl -s -L --max-time 15 -o "/tmp/market_data/${sym,,}.json" \ "https://query1.finance.yahoo.com/v8/finance/chart/${sym}?interval=1d&range=5d" \ -H "User-Agent: Mozilla/5.0" echo "$sym: $?" done ``` Then parse with `scripts/parse-yf-market-data.py` pointing at the fetched subset. 2. **For crypto/commodities/treasuries** — these frequently fail or timeout. Fetch selectively: ```bash curl -s -L --max-time 15 -o /tmp/market_data/btc.json \ "https://query1.finance.yahoo.com/v8/finance/chart/BTC-USD?interval=1d&range=5d" ``` 3. **Always check actual bytes saved** after fetch — zero-byte or tiny files indicate timeout/failure: ```bash for f in /tmp/market_data/*.json; do echo "$f: $(wc -c < $f) bytes"; done ``` **CNBC News Extraction — Verified June 2026:** - Source: `https://www.cnbc.com/markets/` — returns ~30 article links spanning multiple dates - **Filter by date prefix to get recent articles only** — use dynamic date strings (today and yesterday) rather than hardcoded dates: ```python import re from datetime import date, timedelta today = date.today() yesterday = today - timedelta(days=1) date_strs = [today.strftime('/%Y/%m/%d/'), yesterday.strftime('/%Y/%m/%d/')] # e.g. ['/2026/06/26/', '/2026/06/25/'] links = re.findall(r'href="(https://www\.cnbc\.com/\d{4}/\d{2}/[^"#"]+)"', html) links = list(dict.fromkeys(links)) recent = [l for l in links if any(d in l for d in date_strs)][:5] ``` - Full article fetch: use `urllib` with `
` extraction — CNBC `article` tag works reliably; do NOT use TechCrunch article pages (JS-rendered) - **⚠️ tirith blocks `grep -o` with domain literals in the pattern** (invalid hostname). Use Python file inspection instead. **Report structure for US stock market:** Big Picture → Index Performance table → Sector ETF Tracker → Top Market News → Key Market Themes → Looking Ahead. **CNBC article fetching — canonical pipeline:** ```bash python3 /root/.hermes/skills/productivity/hermes-report-pipeline/references/fetch-cnbc-news-links.py python3 /root/.hermes/skills/productivity/hermes-report-pipeline/references/fetch-cnbc-articles.py --recent ``` Outputs: `/tmp/cnbc_markets.html`, `/tmp/news/articles.json`. **OPC/Dropshipping Report Research (June 2026 — verified working):** - Use `scripts/fetch-opc-sources.py` as the primary drop-in scraper — it covers all verified working sources in one run. - **⚠️ Output is a raw list (not `{"articles":...}` wrapper):** The script saves a JSON array directly to `/tmp/opc_articles.json`. Iterate with `for a in data:` not `for a in data['articles']`. Each article has fields: `title`, `source`, `link`, `text`. The `link` field may be empty (`"?"`) for some sources. - **⚠️ Two-pass strategy required for full Shopify article:** `fetch-opc-sources.py` caps content at ~5000 chars. The Shopify article contains 150+ products — always run a second-pass direct fetch for `https://www.shopify.com/blog/best-dropshipping-products` to get the complete product list (extract from `
` tag, strip nav/header/footer, get first 15000 chars). Full pattern in `references/opc-e-commerce-sourcing.md`. - **⚠️ CJ Dropshipping is BROKEN (2026-07-10):** `scripts/fetch-opc-sources.py` returns ~205 chars from CJ (navigation/menu text only, no article content). The blog listing page returns 0 article links. Do NOT use CJ as a primary source — it no longer yields accessible article content. - **CJ blog homepage** (`cjdropshipping.com/blog/`) returns 0 article links (JS-rendered navigation). Direct article URLs also return truncated/nav-only content. Status: BROKEN. - **Additional working sources (July 2026):** `amz123.com/ask/*` (Chinese Amazon seller market intel, verified working). - **Chinese cross-border ecommerce sources (July 2026):** `cifnews.com`, `4pis.com`, `amz123.com/ask/*` are all accessible via `web_extract`. These provide rich regional market intelligence (SE Asia, Middle East, Latin America product trends) that complements English sources. - **⚠️ Two-pass strategy required for full Shopify article:** `scripts/fetch-opc-sources.py` caps content at ~5000 chars. The Shopify article contains 150+ products — always run a second-pass direct fetch for `https://www.shopify.com/blog/best-dropshipping-products` to get the complete product list (extract from `
` tag, strip nav/header/footer, get first 15000 chars). Full pattern in `references/opc-e-commerce-sourcing.md`. - Do NOT waste time on Oberlo, SaleHoo, JungleScout (all HTTP 404), ZhiHu (403), Doba (567), or Chinese sites (mostly inaccessible/JS-rendered). - Known working sources: Shopify blog, BigCommerce blog, TechCrunch ecommerce section, DMarge, EcommerceFuel. - **Google Trends, Amazon Best Sellers, Pinterest Trends all return empty content** when scraped with urllib — JS-rendered, skip these for OPC research. - See `references/opc-e-commerce-sourcing.md` for the full verified source table and broken-source list. **Report structure: Big Picture → Top 10 Categories Table → Product Ideas by Vertical → Supplier Strategy → Platform Recommendations → Key Market Themes → Looking Ahead.** **Direct HTTP scraping pattern:** See `references/http-scrape-ai-news.py` for a complete, self-contained scraper that handles listing pages, link extraction, and article content fetching. It is the verified working approach and should be used as-is or adapted rather than reimplemented inline. For multi-session robustness, prefer writing a two-step pipeline to disk: ### 2. Content Extraction **Primary method — RSS feeds (verified working):** ```bash python3 /root/.hermes/skills/productivity/hermes-report-pipeline/references/fetch-ai-news-rss.py --full --limit 20 ``` This fetches Ars Technica AI RSS + TechCrunch AI RSS, deduplicates, fetches full article text for each link, and saves to `/tmp/ai_news.json`. **Fallback — individual article scraping:** The `references/http-scrape-ai-news.py` script handles full article extraction when you already have URLs. Run it as: ```bash python3 /root/.hermes/skills/productivity/hermes-report-pipeline/references/http-scrape-ai-news.py --source all --full --limit 5 ``` Note: The listing-page mode of `http-scrape-ai-news.py` will return 0 links for Ars Technica and TechCrunch (JS-rendered). Use the RSS script instead for these sources. If you need a custom extraction script, write it to `/tmp/extract_articles.py` following this pattern: ```python import urllib.request, re headers = {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36"} articles = [ ("slug-name", "https://arstechnica.com/ai/2026/06/article-slug/"), ] for slug, url in articles: req = urllib.request.Request(url, headers=headers) with urllib.request.urlopen(req, timeout=15) as resp: html = resp.read().decode('utf-8', errors='replace') title_match = re.search(r']*>(.*?)', html, re.DOTALL) title = re.sub(r'<[^>]+>', '', title_match.group(1)).strip() if title_match else "No title" if not body_match: body_match = re.search(r']*>(.*?)
', html, re.DOTALL) body = body_match.group(1) if body_match else "" # Extract slug from URL: e.g. /2026/06/29/some-article-name.html -> some-article-name slug = re.search(r'/(\d{4}/\d{2}/[^/]+)(?:\.html)?$', url) slug = slug.group(1).split('/')[-1] if slug else "unknown" body = re.sub(r']*>.*?', '', body, flags=re.DOTALL) body = re.sub(r']*>.*?', '', body, flags=re.DOTALL) text = re.sub(r'<[^>]+>', ' ', body) text = re.sub(r'\s+', ' ', text).strip() if len(text) < 200: text = f"[Full article at CNBC]({url})" return {"slug": slug, "title": title, "url": url, "text": text[:2000]} **Note:** TechCrunch HTML often has heavy nav/menu cruft — the article `
` tag reliably contains the main content. If the article text looks like nav links, the page may require JavaScript rendering (use a different source in that case). ### 3. Generate Report Files Use `write_file` + `terminal` to build both JSON and HTML in sequence. **⚠️ CRITICAL: HTML Conversion Rule** Never write raw markdown content directly to `.html` files. You MUST use a markdown-to-html converter. Use `mistune.html()` — it renders tables correctly. **Known pitfalls:** - `from marked import parse` FAILS — `marked` exports `markgen`/`markup_to_markdown`, not `parse`. Use `mistune` instead. - **`mistune.create_markdown()` does NOT render tables.** It outputs raw markdown pipe-syntax for tables. Only `mistune.html()` renders tables correctly. Use `mistune.html(your_content)` as a function call, not `create_markdown()`. ```python # WRONG — tables render as raw markdown: md = mistune.create_markdown() body_html = md(content) # CORRECT — tables render as HTML: body_html = mistune.html(content) ``` - `execute_code` sandbox uses a different Python than system `python3`. For HTML generation, write the script to `/tmp/gen_html.py` with `write_file`, then invoke via `terminal('python3 /tmp/gen_html.py')`. - Do NOT pass `plugins=['table']` to `mistune.create_markdown()` — it is unnecessary with `mistune.html()` and changes nothing since `create_markdown()` does not use that plugin anyway. - **Tirith blocks python3 heredoc syntax when domain names appear inside the heredoc** (e.g. `python3 << 'EOF'` with `techcrunch.com` inside the body). The fix: always write scripts to `/tmp/script.py` with `write_file`, then execute via `terminal('python3 /tmp/script.py')`. This bypasses tirith's heredoc scanner entirely. - **⚠️ `self.__next_f.push` in article content causes `NameError`.** Ars Technica HTML contains JavaScript variable names like `self.__next_f.push` in the rendered article text. When this text is interpolated into an f-string, Python's name mangling treats `self` as a variable reference. **Always call `strip_html()` on article content before any f-string interpolation.** Never put raw `article['content']` directly into an f-string. **Workflow:** 1. Write generation script to `/tmp/gen_html.py` using `write_file` 2. Run with `terminal('python3 /tmp/gen_html.py')` 3. Verify the output file contains real HTML tags (`

`, `

`, etc.), not markdown **⚠️ tirith blocks `cat | python3 -c` (pipe-to-interpreter).** The security scanner rejects any pipeline routing shell output directly into a Python interpreter. Always write inspection/extraction scripts to `/tmp/inspect.py` via `write_file` first, then run with `terminal('python3 /tmp/inspect.py')`. This applies whenever you need to peek at JSON output (e.g. verifying `market_data.json` contents after a fetch). **⚠️ BTC data in `market_data.json` is a nested dict** — `d['BTC']` is `{'name','price','change','pct','high52','low52'}`, not a scalar. Access as `d['BTC']['price']` and `d['BTC']['pct']`. The same applies to any future nested-entry symbols. **⚠️ BTC also saved as `btc.json` in `/tmp/market_data/`.** When inspecting individual JSON files in `/tmp/market_data/`, note the filename is lowercase `btc.json` (not `BTC-USD.json`). This matters when manually inspecting files — `BTC-USD.json` does not exist on disk. **⚠️ Cron mode restriction:** `execute_code` is BLOCKED in scheduled cron jobs (requires user approval). Write all data-fetch and generation scripts to `/tmp/*.py` via `write_file`, then run via `terminal('python3 /tmp/script.py')`. Do NOT attempt to use `execute_code` in cron mode. **AI/LLM Report — Working Pipeline (June 2026):** **⚠️ `fetch-ai-news-rss.py --full` SEQUENTIAL ARTICLE FETCH TIMED OUT (2026-07-17).** The `--full` flag fetches all 20 articles **sequentially**, each with a 15s timeout = up to 300s total. At 120s cron timeout, it never completes. **Workaround — two-pass approach:** 1. Fetch RSS only (no `--full`): `python3 /root/.hermes/skills/productivity/hermes-report-pipeline/references/fetch-ai-news-rss.py --limit 20` → saves 40 article metadata items to `/tmp/ai_news.json` fast (~10s) 2. Write a custom `/tmp/extract_ai_news.py` to parse the RSS JSON and selectively fetch full content for top N articles. This session's working pattern: - Parse Ars + TC RSS XML with `xml.etree.ElementTree` - Combine, dedupe by link, filter by AI keywords - Sort by pubDate, take top 20 - Fetch full content for each sequentially (or top-N only for speed) - Save to `/tmp/ai_news.json` **⚠️ `fetch-ai-news-rss.py` description field already contains rich content.** The RSS `description` field holds 200-400 char summaries — sufficient for trend reports without hitting article-by-article timeouts. Prefer the metadata-only fetch for speed; only attempt `--full` when deep article text is genuinely needed and time permits. **⚠️ The `scripts/gen-ai-llm-report.py` scaffold does NOT produce output reliably.** The `gen-ai-llm-report.py` script in the scripts/ directory is verified working (verified 2026-07-01) — use it instead of any HTML-generation scaffold. The canonical pipeline is: ```bash # Step 1: Fetch RSS metadata fast (~10s) python3 /root/.hermes/skills/productivity/hermes-report-pipeline/references/fetch-ai-news-rss.py --limit 20 # Step 2: Generate HTML — reads /tmp/ai_news.json, dedupes by link, outputs to blog_admin/posts/ python3 /root/.hermes/skills/productivity/hermes-report-pipeline/scripts/gen-ai-llm-report.py ``` Do NOT use `scripts/gen-ai-llm-report-html.py` — it is a scaffold that produces no output. **Canonical 2-step pipeline (updated 2026-07-20):** ```bash # Step 1: Fast RSS metadata (~10s) — no --full needed python3 /root/.hermes/skills/productivity/hermes-report-pipeline/references/fetch-ai-news-rss.py --limit 20 # Step 2: Generate HTML — dedupes by link, renders via mistune.html(), saves to blog_admin/posts/ python3 /root/.hermes/skills/productivity/hermes-report-pipeline/scripts/gen-ai-llm-report.py ``` **JSON data format from `fetch-ai-news-rss.py`:** ```json {"date": "...", "fetched": "...", "sources": [...], "articles": [{"title": "...", "link": "...", "source": "...", "pubDate": "...", "description": "...", "content": "..."}, ...]} ``` Key fields: `articles` list (NOT a top-level list), URL field is `link` (NOT `url`). **⚠️ Critical deduplication:** Ars/TC AI RSS feeds return articles that ALL match the AI keyword filter. Deduplicate by `link`, not by slug or title — otherwise ALL 40 articles may collapse to 1 story. The `gen-ai-llm-report.py` script handles this correctly. **Report structure:** Big Picture → Top Developments (numbered, 10-12 items) → Key Themes table → Looking Ahead → Source attribution. ### 4. Blog Post JSON Format (Optional for Non-Market Reports) For **market data reports** (US stocks, A-shares): save JSON to `/root/.hermes/blog_admin/posts/daily-report-{topic}-{YYYY-MM-DD}.json` with this EXACT structure: ```json {"title":"Report Title","date":"YYYY-MM-DD","content":"## Title\n\nContent in markdown...","tags":["tag1","tag2"]} ``` The `content` field contains full markdown (including the title as an H2). HTML is generated separately from the JSON content. For **AI/LLM trend reports and other editorial reports**: JSON is not required. Generate HTML directly from the markdown and sync. The blog index will pick up the HTML file automatically via `generate_index.js`. ### 5. Recommended Report Content Structure When generating a daily trends report, use this structure: - Opening "Big Picture" paragraph (2-3 sentences, top-level context) - "Top Developments" numbered section with 4-5 items, each with a bold lead phrase - Technical trends table (Trend | Detail columns) - Lab/company highlights as bullet list - Benchmarks table if applicable - "Looking Ahead" closing paragraph - Source attribution footer Keep H2 headings hierarchical — the JSON `content` field feeds both the HTML conversion and the blog index. ### 6. Sync to VPS **Default approach:** `bash /root/.hermes/blog_admin/sync_blog.sh` > ⚠️ **sync_blog.sh behavior in practice:** The script loops over all HTML files (~60) doing one SSH stat per file. For a single new file, it typically completes in 30–120 seconds. In cron (60s timeout), it often gets killed — but the file may still arrive on VPS. In interactive sessions (120s timeout), it usually succeeds. **Always verify the file arrived** with a follow-up SSH stat command. **VPS SSH fail2ban block — detailed recovery guide (updated 2026-07-20):** - **Both SSH key AND sshpass fail identically** — `Permission denied (publickey,password)` is the exact error for both auth methods under fail2ban block. The identical failure message is the key diagnostic signal — it rules out key misconfiguration or password expiry. - **ping works; SSH TCP connect succeeds instantly (0ms RTT) but server never sends a banner** — `socket.recv()` times out with no data - **Root cause:** fail2ban temporary block or SSH service stall — NOT a credential or key problem - **Recovery:** SSH recovers automatically within minutes without any intervention - **After 2–3 failed attempts, STOP retrying** — each attempt worsens the fail2ban score and delays recovery - **known_hosts may also block** — if the VPS was reinstalled, `ssh-keygen -f '/root/.ssh/known_hosts' -R '104.128.190.187'` removes the stale host key before retrying (only helps during the recovery window, not during active block) - **VPS file sync will succeed once SSH recovers** — the HTML is already on disk, just sync later - **Do NOT attempt server-side fixes** (restart SSH, fail2ban unban) — automatic recovery is faster - **SSH key path is `/root/hermes_on_vps_backup/backups/vps-info/id_ed25519`** — use with `ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no -i /root/hermes_on_vps_backup/backups/vps-info/id_ed25519 root@104.128.190.187` **⚠️ VPS SSH banner timeout pattern (2026-07-09 — confirmed in production):** - TCP connect to `104.128.190.187:22` succeeds instantly (0ms RTT) but SSH server never sends a banner — `socket.recv()` times out with no data - `ping` works; `nc -z 22` reports port open - **Both SSH key AND sshpass fail identically** — this is the key diagnostic signal - **known_hosts may also block** — if VPS was reinstalled, run `ssh-keygen -f '/root/.ssh/known_hosts' -R '104.128.190.187'` to remove the stale host key - **Root cause:** fail2ban temporary block or SSH service stall — NOT a credential or key problem - **Recovery:** SSH recovers automatically within minutes without any intervention - **After 2–3 failed attempts, STOP retrying** — each additional failed attempt worsens the fail2ban score and delays recovery - **VPS file sync will succeed once SSH recovers** — the HTML is already on disk, just sync later - **Do NOT attempt server-side fixes** (restart SSH, fail2ban unban) — automatic recovery is faster **⚠️ CRITICAL — SSH retry ceiling (hard rule, non-negotiable):** - **In cron mode: MAX 2 SSH sync attempts total.** After 2 failures, STOP immediately — the HTML is already on disk and will sync when SSH recovers. Each additional retry worsens the fail2ban score and delays recovery. - **In interactive mode: MAX 3 attempts, then stop.** Wait ≥30s between retries. - **Never spin-wait or retry in a cron loop.** If SSH fails, proceed to Telegram delivery with a "VPS sync pending" note — the file is safe on disk. - This is a hard ceiling, not a guideline. The old pattern of 6+ retries caused 10+ minutes of continuous fail2ban scoring. **A-share Sector Report — Sina % caveat:** Sina concept sector percentages (e.g. "电解液: -631%") are **not real daily move percentages**. They average across stocks at very different price levels, producing extreme values. Use Sina sector data only for **relative ranking** (hot vs cold concepts), never as absolute daily move figures. Individual stock quotes from Tencent are accurate. **Canonical A-share sector pipeline (verified 2026-07-20):** ```bash # 1. Fetch indices + Sina sectors + batch stock quotes (HTTPS direct — NO proxy) python3 /root/.hermes/skills/data-science/cn-stock-realtime/scripts/fetch-a-share-sectors.py # Output: /tmp/a_share_sector_data.json # Schema: # indices: {sym: {name, price, pct, change, high, low}} # sina_sectors: [{code, name, count, pct, leader_code, leader_price, leader_pct, leader_name}, ...] (sorted by |pct| desc) # quotes: {sym: {name, price, pct, change, high, low}} — dict keyed by symbol, NOT a list # sector_leaders: {} — always empty in current script (do not rely on it) # 2. Generate HTML — write to /tmp/gen_a_share_html.py via write_file, run via terminal() # Output: /root/.hermes/blog_admin/posts/daily-report-a-share-YYYY-MM-DD.html # IMPORTANT: quotes is a dict (not list) — iterate with quotes.items() # 3. Sync to VPS (pipe method) # 4. Telegram summary — use venv Python with parse_mode=None for safety ``` **Report structure for A-share (July 2026 — verified):** - Big Picture → Index Performance table (4 indices) → Sina Sector Rankings (all 30, sorted by |pct|) → Individual Stocks (all 87 quotes, sorted by pct desc) → Looking Ahead. **IMPORTANT — quotes is a dict, iterate correctly:** ```python # quotes is a dict keyed by symbol — NOT a list sorted_quotes = sorted(quotes.items(), key=lambda x: x[1]['pct'], reverse=True) # sorted_quotes: [(symbol, {name, price, pct, ...}), ...] ``` **IMPORTANT — sector→stock mapping does not exist in current data:** - `quotes` dict contains ~87 individual stocks (sector leader + other selected stocks), NOT a per-sector breakdown - `sector_leaders` key in JSON is always empty — do not rely on it - Leader blocks in the report can only show one stock per sector (the named `leader_name` from Sina sector data) - "top 10 mapped sectors × 5 stocks each" is NOT possible with the current data source — do NOT claim this structure **⚠️ PRIMARY — Pipe method (bypasses tirith scanner):** Two authentication methods work — prefer SSH key when available: **Option A: SSH key** (simpler, no password in skill file) ```bash cat /root/.hermes/blog_admin/posts/YOUR_REPORT.html | \ ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no \ -i /root/hermes_on_vps_backup/backups/vps-info/id_ed25519 root@104.128.190.187 \ "cat > /var/www/blog/YOUR_REPORT.html" ``` **Option B: sshpass** (fallback if key not configured) ```bash cat /root/.hermes/blog_admin/posts/YOUR_REPORT.html | \ sshpass -p 'Wx+yAbdSyGm1nf1C2VR5+oCW/6oL7ymT' ssh \ -o ConnectTimeout=10 -o StrictHostKeyChecking=no \ root@104.128.190.187 \ "cat > /var/www/blog/YOUR_REPORT.html" ``` **⚠️ Raw IP SSH/SCP commands get security-scanned and blocked by tirith.** - `scp root@:...` via plain scp is blocked (tirith `raw_ip_url` MEDIUM denial) - `sshpass ... scp ... root@:...` was also blocked in practice (tirith scanner intercepts) - **Use the pipe method above as the primary sync approach** **⚠️ CRITICAL: VPS sync is MANDATORY — never skip it.** After generating HTML (step 3), you MUST immediately: 1. Sync HTML to VPS via pipe method (see above) 2. Regenerate the blog index: ```bash cd /root/.hermes/blog_admin && node generate_index.js ``` ⚠️ The script ALWAYS outputs to `posts/index.html` regardless of the CWD used in the cd command. The correct sync source path is `posts/index.html`, NOT `index.html` in the blog_admin root. 3. Sync the updated index: ```bash # Option A: SSH key cat /root/.hermes/blog_admin/posts/index.html | \ ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no \ -i /root/hermes_on_vps_backup/backups/vps-info/id_ed25519 root@104.128.190.187 \ "cat > /var/www/blog/index.html" # Option B: sshpass cat /root/.hermes/blog_admin/posts/index.html | \ sshpass -p 'Wx+yAbdSyGm1nf1C2VR5+oCW/6oL7ymT' ssh \ -o ConnectTimeout=10 -o StrictHostKeyChecking=no \ root@104.128.190.187 \ "cat > /var/www/blog/index.html" ``` 4. Verify file exists on VPS: ```bash # Option A: SSH key ssh -o ConnectTimeout=5 -i /root/hermes_on_vps_backup/backups/vps-info/id_ed25519 root@104.128.190.187 \ "stat -c%s /var/www/blog/YOUR_REPORT.html" # Option B: sshpass sshpass -p 'Wx+yAbdSyGm1nf1C2VR5+oCW/6oL7ymT' ssh -o ConnectTimeout=5 root@104.128.190.187 \ "stat -c%s /var/www/blog/YOUR_REPORT.html" ``` 5. Report the URL in the Telegram message (e.g., "📝 https://898998.xyz/blog/YOUR_REPORT.html") **The order matters:** Generate → Sync HTML → Regenerate index → Sync index → Verify → Deliver Telegram. **Verification after sync:** ```bash sshpass -p 'Wx+yAbdSyGm1nf1C2VR5+oCW/6oL7ymT' ssh -o ConnectTimeout=5 root@104.128.190.187 \ "stat -c%s /var/www/blog/YOUR_REPORT.html" ``` **⚠️ VPS IP has changed.** Current active VPS is `104.128.190.187` (not `141.98.197.53`). VPS was reinstalled post-2026-05-31 and credentials were reset. Use SSH key (`/root/hermes_on_vps_backup/backups/vps-info/id_ed25519`) or sshpass for all VPS operations. **⚠️ `gen_all_posts.js` does NOT overwrite existing HTML files** — it only writes new files. If a post's HTML was generated with a broken/different template in the past, regenerating from JSON will NOT fix it. **Fix:** `rm posts/your-post-slug.html` then `node gen_all_posts.js` to regenerate. **⚠️ `sync_blog.sh` sync logic was broken** — it only synced files where remote size=0 (new files), and skipped all existing files even when local was larger (updated content). Fixed June 22 2026: now syncs when local > remote. For targeted updates of specific posts, use direct scp instead of the full script. **VPS IP confirmed `104.128.190.187`** (June 22 2026 — `141.98.197.53` was a false memory that caused SSH lockout). **⚠️ Cron "ok" ≠ all reports succeeded.** When multiple report types run under one cron job (e.g. polymarket hot markets + daily polymarket), some can fail silently while others succeed. **After every cron run, always verify the actual output files exist** — check locally first, then VPS. ### Post-Cron Verification Checklist Run after each cron completes: ```bash # Check local post files exist for today's date ls /root/.hermes/blog_admin/posts/daily-report-*-$(date +%Y-%m-%d)*.html # Check VPS for the same (use SSH key or sshpass on active VPS 104.128.190.187) # Option A: SSH key ssh -o ConnectTimeout=5 -i /root/hermes_on_vps_backup/backups/vps-info/id_ed25519 root@104.128.190.187 \ "ls /var/www/blog/daily-report-*-$(date +%Y-%m-%d)*.html" # Option B: sshpass sshpass -p 'Wx+yAbdSyGm1nf1C2VR5+oCW/6oL7ymT' ssh -o ConnectTimeout=5 root@104.128.190.187 \ "ls /var/www/blog/daily-report-*-$(date +%Y-%m-%d)*.html" # If local file missing but cron was "ok" → pipeline failed for that report type # If local exists but VPS missing → sync failed (use pipe method above) ``` **Known failure pattern (2026-05-26):** `daily-report-polymarket` and `polymarket-hot-markets` consistently failed to generate for multiple consecutive days even though the cron job reported "ok". Root cause: likely upstream API errors (Polymarket anti-bot, Finnhub rate limits, etc.). When this happens, re-run the specific report type manually rather than waiting for the next cron cycle. **Verified:** 2026-06-01 — Pipe method uploaded `opc-ideas-2026-06-01.html` successfully (104.128.190.187), 17937 bytes verified. **⚠️ Telegram Bot Token is INVALID (2026-07-13):** The token `8569454727:AAF6QMjnOBQbH1GGaiEdZTL6aP3fmLFg9d8` returns 401 Unauthorized on `getMe` from both this server AND the VPS relay. Telegram delivery is completely blocked. See `references/telegram-delivery-failure-modes.md` for full diagnosis. Report HTML is still synced to VPS successfully. **⚠️ CRITICAL: Python version mismatch for Telegram sends (2026-07-05).** `terminal('python3 script.py')` resolves to system Python 3.13 which reads a MASKED token (`8569454727:***`) from `.env` even in binary mode — HTTP 401/400 results. The Hermes venv Python 3.11 correctly reads the real token. WORKAROUND — always invoke Telegram scripts with the explicit venv path:** ```bash /root/.hermes/hermes-agent/venv/bin/python3 /root/.hermes/skills/productivity/hermes-report-pipeline/references/telegram-send-urllib.py 5501463694 "Your message" ``` Do NOT use `python3 /path/to/script.py` (uses system Python 3.13 with masked token). **Why it breaks:** The masking layer intercepts `.env` reads in Python 3.13 system-wide. The venv at `/root/.hermes/hermes-agent/venv/bin/python3` (3.11) is isolated from this masking and reads the real token. **parse_mode guidance (verified 2026-07-09):** - With venv Python (real token): `parse_mode="Markdown"` works fine for emoji-heavy text - With masked token (system Python 3.13): only `parse_mode=None` succeeds; `parse_mode=Markdown` returns HTTP 400 - Always use venv Python + real token — eliminates the ambiguity entirely ### 7. Telegram Delivery (CRITICAL — Cron Job Rule) **If running as a scheduled cron job, do NOT call `send_message`.** The cron auto-delivery only handles one summary to the default target. For specific chat IDs, use the Bot API directly. **Primary approach — use the reference script with venv Python (safest, no escaping issues):** ```bash /root/.hermes/hermes-agent/venv/bin/python3 /root/.hermes/skills/productivity/hermes-report-pipeline/references/telegram-send-urllib.py 5501463694 "Your message" ``` **⚠️ `python3` vs venv Python:** `terminal('python3 ...')` uses system Python 3.13 which gets a masked token from `.env` (HTTP 401/400). Always use the venv path above. **Why this over inline Python in execute_code:** The `execute_code` sandbox does not reliably handle multi-line or complex strings (e.g., triple-quoted strings, emoji-heavy content). Additionally, `execute_code` is **completely blocked in cron mode** (requires user approval). The reference script avoids the sandbox entirely via `terminal()` and is the verified working path for both cron and interactive sessions. **If you must use execute_code + inline Python (text-safe messages only):** ```python import urllib.request, urllib.parse, json # CRITICAL: Read .env in BINARY mode. Text-mode read returns the masked token # '8569454727:***' because the masking layer intercepts text-mode file reads. # Binary mode bypasses the masking and returns the real token. with open("/root/.hermes/.env", "rb") as f: raw = f.read() token = None for line in raw.decode('utf-8', errors='replace').split('\n'): if line.startswith('TELEGRAM_BOT_TOKEN='): token = line.split('=', 1)[1] # No .strip() — preserves full token break url = f"https://api.telegram.org/bot{token}/sendMessage" data = urllib.parse.urlencode({ "chat_id": "5501463694", "text": "Report content here", "parse_mode": "Markdown" }).encode() req = urllib.request.Request(url, data=data) with urllib.request.urlopen(req, timeout=10) as resp: result = json.loads(resp.read().decode()) print(result) # {"ok": true, "result": {"message_id": ...}} ``` **⚠️ ALWAYS use binary-mode read for the .env token** — text-mode read is intercepted by a masking layer that returns `8569454727:***` instead of the real token. Binary mode (`"rb"`) bypasses this. This applies to ALL contexts including execute_code. **⚠️ Even binary-mode read gets the MASKED token on system Python 3.13.** The Hermes venv Python 3.11 at `/root/.hermes/hermes-agent/venv/bin/python3` is isolated from the masking layer and reads the real token. When running Telegram send scripts, always use the venv Python explicitly. > ⚠️ Bug in the token-read code: some versions contain a malformed doubled condition > (`if line.startswith(...) if line.startswith(...)`) that silently leaves `token=None`. > If Telegram sends fail silently, verify `token` is actually populated by adding > `print(f"TOKEN={token}")` before the Request call. The correct single-condition > loop is: > ```python > for line in raw.decode('utf-8', errors='replace').split('\n'): > if line.startswith('TELEGRAM_BOT_TOKEN='): > token = line.split('=', 1)[1].strip() > break > ``` > The doubled `if` variant (buggy) causes `token` to remain `None`, silently failing > all sends. The reference script `references/telegram-send-urllib.py` uses the correct > single-condition form. **Known failure 3 — SSL handshake timeout to Telegram API (recurring, 2026-07-15 and 2026-07-20):** - Symptom: `TimeoutError: _ssl.c:999: The handshake operation timed out` when connecting to `api.telegram.org` - Both direct venv Python AND VPS relay fail - VPS relay fails because `/root/.hermes/.env` does not exist on the VPS — token is not accessible there - Recovery: Report HTML is live on VPS at `https://898998.xyz/blog/daily-report-ai-llm-trends-YYYY-MM-DD.html` — this is the source of truth. Telegram delivery is blocked until server network path to Telegram API is restored. - Do NOT retry Telegram immediately — the SSL timeout is a persistent network path issue, not a transient glitch. It self-resolves when the network route recovers. - Verified working path: `/root/.hermes/hermes-agent/venv/bin/python3` (token reads correctly) **Known failure 4 — VPS relay token not found (2026-07-15):** - Symptom: `grep: /root/.hermes/.env: No such file or directory` + `{"ok":false,"error_code":404,"description":"Not Found"}` on VPS relay - Root cause: bot token is stored on this Hermes server only, not on the VPS - VPS relay cannot send Telegram messages unless the token is also present on VPS at the same path - Fallback: skip Telegram, ensure HTML is on VPS (it's already the source of truth) **Known failure 5 — parse_mode=Markdown with masked token (2026-07-09):** - With masked token (system Python 3.13): only `parse_mode=None` succeeds; `parse_mode=Markdown` returns HTTP 400 - Always use venv Python + real token — eliminates the ambiguity entirely **Known IDs:** - `5501463694` = Quantum Cat (user) - Bot: `@d886s_bot` **Reference:** `references/telegram-send-urllib.py` — drop-in script for direct use. ## Key Files - Blog posts dir: `/root/.hermes/blog_admin/posts/` - Blog admin scripts: `/root/.hermes/blog_admin/` - VPS sync script: `/root/.hermes/blog_admin/sync_blog.sh` - Channel directory: `~/.hermes/channel_directory.json` - `references/fetch-cnbc-news-links.py` — Drop-in: fetch CNBC markets page, extract article links, filter by today+yesterday dynamically. Run: `python3 references/fetch-cnbc-news-links.py` - `references/fetch-cnbc-articles.py` — Drop-in: fetch full article content from list of CNBC URLs. Run: `python3 references/fetch-cnbc-articles.py --recent` (reads from `/tmp/cnbc_markets.html`) or with a URLs file. - `references/polymarket-markets-fetch.py` — **Verified working Polymarket hot markets fetcher.** Finds the `3e:` Next.js state block, unescapes double-escaped JSON, bracket-counts the results array, sorts by volume. Use this as the primary drop-in. (2026-07-19) - `references/polymarket-data-fetch.md` — Historical approaches (June 2026 block-28 method). **Outdated** — do not use. Use `references/polymarket-markets-fetch.py` instead. - `scripts/fetch-polymarket-hot-markets.py` — **BROKEN** (2026-06-28, 2026-07-18, 2026-07-19). Do not use. - `scripts/fetch-polymarket-markets-v2.py` — **BROKEN**. Do not use. - `references/fetch-us-stock-report.py` — Unified drop-in: fetches Yahoo Finance market data (9 symbols) + CNBC news in one run. **Uses dynamic date filtering (today + yesterday) — no hardcoded month.** Outputs `/tmp/market_data.json` + `/tmp/news/articles.json`. Verified 2026-06-29. Bug fixed: slug regex now uses `(?:\.html)?$` to handle URLs with or without `.html` suffix. - `references/us-stock-report-generation.md` — **Generate US stock market HTML from fetch-us-stock-report.py output.** Documents the content structure, CNBC article filtering, mistune.html() pattern, VPS sync sequence, and a known stdout quirk: the script prints "YF SPY: 0" as a status code (not actual data — always inspect the JSON file directly). - `references/polymarket-data-fetch.md` - `scripts/fetch-polymarket-hot-markets.py` — **BROKEN** (2026-06-28, 2026-06-29). Do not use. Chunk ID `1a:` no longer exists. - `references/fetch-ai-news-rss.py` — Drop-in standalone RSS scraper for Ars Technica AI + TechCrunch AI. Uses RSS feeds (not listing pages). Run: `python3 references/fetch-ai-news-rss.py --limit 20` (metadata only). For full article content, run `references/fetch-ai-news-rss-article-fetch.py` afterwards. - `references/fetch-ai-news-rss-article-fetch.py` — Two-pass article fetcher: reads RSS JSON from `/tmp/ai_news.json`, fetches full content for top N articles. Run AFTER `fetch-ai-news-rss.py --limit 20`. - `references/http-scrape-ai-news.py` — Standalone HTTP scraper for Ars Technica, TechCrunch, MIT Tech Review **listing pages**. Note: listing-page scraping FAILS for Ars/TC (JS-rendered, returns 0 links). Use `fetch-ai-news-rss.py` instead for these sources. Run with `--full` to also fetch article content. - `references/vps-migration.md` — VPS migration workflow: pre-migration local backup, cross-VPS rsync, skill IP updates, fail2ban lockout prevention - `references/vps-sync-patterns.md` — VPS sync patterns: how to upload files when sync_blog.sh times out, raw IP SSH security scanner bypass, verified commands - `references/vps-ssh-fail2ban-recovery.md` — VPS SSH fail2ban block: symptom, retry strategy, anti-patterns, post-recovery verification (added 2026-07-09) - `references/yahoo-finance-market-data.md` — Yahoo Finance curl fetch, JSON field guide (chartPreviousClose, etc.), CNBC news extraction, common bugs - `scripts/fetch-yf-market-data.sh` — Verified working curl fetch all market data (indices, sectors, crypto, commodities, treasuries) - `scripts/parse-yf-market-data.py` — Parse fetched Yahoo Finance JSON files into structured dict saved at /tmp/market_data.json - `references/telegram-send-urllib.py` — drop-in working Telegram Bot API sender using stdlib urllib (no httpx needed). Usage: `python3 ` - `references/telegram-delivery-failure-modes.md` — All known Telegram delivery failure modes: token masked on sys Python 3.13, token invalid/401, network unreachable/SSL timeout. **Always check here before troubleshooting a Telegram send failure.** - `references/html-regeneration-from-json.md` — When JSON exists but HTML is missing (silent pipeline failure): regenerate HTML from JSON using mistune + dark template, then sync to VPS. Recovery pattern for failed HTML generation steps. - `references/opc-e-commerce-sourcing.md` — Verified working sources for OPC/dropshipping/e-commerce product sourcing research (June 2026): Shopify, BigCommerce, TechCrunch, DMarge, EcommerceFuel. Also documents known broken sources (Oberlo, SaleHoo, JungleScout, Chinese sites). - `references/ai-llm-report-sources.md` — AI/LLM report sources, article filtering keywords, report structure, and content fields. Supersedes inline documentation. - `scripts/gen-ai-llm-report.py` — **Working AI/LLM report generator.** Reads `/tmp/ai_news.json`, dedupes by `link` field, curates top 12 AI stories, renders HTML via `mistune.html()`, saves to blog posts dir. Run after `fetch-ai-news-rss.py`. Verified 2026-07-01. **Do NOT use `gen-ai-llm-report-html.py`** (scaffold only, produces no output). - `scripts/fetch-opc-sources.py` — Drop-in standalone scraper for OPC/e-commerce sources. Run: `python3 scripts/fetch-opc-sources.py`. Outputs JSON to `/tmp/opc_articles.json`. Use `--full` for full content, `--limit=N` for N articles per source. - `scripts/fetch-digest-8883888.py` — Drop-in: fetches 5 RSS sources (Yahoo Finance, Bloomberg Tech/Markets, CNBC, WSJ) → generates rich HTML digest → deploys to `finance.8883888.xyz/posts/daily-digest-YYYY-MM-DD/`. Script at `~/.hermes/scripts/fetch_digest.py`. Run: `python3 ~/.hermes/scripts/fetch_digest.py`. Cron job: daily 8AM UTC on THIS Hermes. The user has provided the following instruction alongside the skill invocation: [IMPORTANT: You are running as a scheduled cron job. DELIVERY: Your final response will be automatically delivered to the user — do NOT use send_message or try to deliver the output yourself. Just produce your report/output as your final response and the system handles the rest. SILENT: If there is genuinely nothing new to report, respond with exactly "[SILENT]" (nothing else) to suppress delivery. Never combine [SILENT] with content — either report your findings normally, or say [SILENT] and nothing more.] Generate daily A-share China stock sector report with individual stocks per sectors. Data source: Tencent Finance API + Sina sector data via Python script at /root/.hermes/skills/data-science/cn-stock-realtime/scripts/fetch-a-share-sectors.py Output: /tmp/a_share_sector_data.json HTML output path: /root/.hermes/blog_admin/posts/daily-report-a-share-{YYYY-MM-DD}.html VPS sync target: root@104.128.190.187:/var/www/blog/ SSH key: /root/hermes_on_vps_backup/backups/vps-info/id_ed25519 After HTML generation: regenerate blog index with 'cd /root/.hermes/blog_admin && node generate_index.js' then sync index to same VPS path. Verify: ssh -o ConnectTimeout=10 -i /root/hermes_on_vps_backup/backups/vps-info/id_ed25519 root@104.128.190.187 "stat -c%s /var/www/blog/daily-report-a-share-{YYYY-MM-DD}.html" Deliver Telegram summary to 5501463694 after completion. ## Response ✅ A股市场每日行情已完成 - **本地**: `/root/.hermes/blog_admin/posts/daily-report-a-share-2026-08-18.html` (16,241 bytes) - **VPS**: `https://898998.xyz/blog/daily-report-a-share-2026-08-18.html` (17,874 bytes) - **指数**: 沪指/深指/沪深300/创业板 4大指数 - **板块**: 30个Sina概念板块(按涨跌幅绝对值排序) - **个股**: 87只重点股票(按涨跌幅排序) - **Telegram**: 发送成功 (msg_id: 32094)