
Story Short Scan
- 9.5k installs
- 4.7k repo stars
- Updated July 27, 2026
- worldwonderer/oh-story-claudecode
story-short-scan is an agent skill that scans short web fiction platform rankings to identify emotion-driven genre trends with expiry and validation guidance.
About
The story-short-scan skill analyzes short web fiction market rankings on platforms like Zhihu salt-style stories -Fanqie short, Qimao, and Heiyan to surface actionable emotion directions and genre candidates. Core philosophy treats short fiction as an emotion market where readers complete a feeling in one sitting, emphasizing completion rate and shareability over long-form chase metrics. Short trend signals expire quickly, so reports must label sample dates, confidence, saturation risk, and next rescan time. Phase 1 confirms platform and genre direction for focused or overview scans. Data sourcing prioritizes browser-cdp page scraping, then user-provided screenshots, then built-in knowledge trends. Outputs include emotion frequency, trigger scenes, burst points, and viral hooks rather than genre names alone. Cross-platform comparison mode contrasts platform-specific success patterns. Use when users invoke short rank scanning, ask what short genres are hot, or request Zhihu story ranking analysis.
- Emotion-market framing: completion rate and shareability over chase metrics.
- Trend expiry labels with rescan timing and saturation risk warnings.
- browser-cdp scraping priority for platform rank data collection.
- Extracts emotion triggers and viral hooks not just genre labels.
- Cross-platform comparison for Zhihu, Fanqie, Qimao short fiction ranks.
Story Short Scan by the numbers
- 9,495 all-time installs (skills.sh)
- +761 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #89 of 1,340 Generative Media skills by installs in the Skillselion catalog
- Security screen: HIGH risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
story-short-scan capabilities & compatibility
- Capabilities
- short fiction platform rank scraping · emotion and viral hook extraction · trend expiry and rescan scheduling · cross platform short market comparison
- Use cases
- research · copywriting
What story-short-scan says it does
短篇市场是情绪市场
分析知乎盐言、七猫、黑岩、点众等平台热门短篇数据
npx skills add https://github.com/worldwonderer/oh-story-claudecode --skill story-short-scanAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 9.5k |
|---|---|
| repo stars | ★ 4.7k |
| Security audit | 0 / 3 scanners passed |
| Last updated | July 27, 2026 |
| Repository | worldwonderer/oh-story-claudecode ↗ |
What short-fiction emotions and genres are trending on Zhihu, Fanqie, or Qimao right now?
Scan short-fiction platform rank lists to identify emotion-driven genre trends with expiry dates and validation actions.
Who is it for?
Developers and writers researching short web fiction market trends before drafting new pieces.
Skip if: Skip for long-novel rank analysis; use story-long-scan instead.
When should I use this skill?
User invokes /story-short-scan, asks what short genres are hot, or requests Zhihu story rank analysis.
What you get
Rank scan report with emotion directions, genre candidates, trend confidence, and rescan schedule.
Files
story-short-scan:短篇网文扫榜
你是短篇网文市场分析师。你的任务是基于榜单样本识别短篇市场格局,并输出可执行的情绪方向、题材候选、风险阈值和验证动作。
核心信念:短篇市场变化快,题材信号有效期短。 扫榜报告必须标注样本日期、趋势可信度和下次重新扫榜的时间。
---
核心哲学
原则 1:短篇市场是情绪市场
短篇网文的核心是情绪交付。读者在短时间内完成一次情绪体验;扫榜要提取高频情绪、触发场景、情绪爆发点和读者愿意转发的点,而不是只记录题材名。
原则 2:短篇的生命力在传播
短篇不像长篇靠追读赚钱。短篇靠的是单篇完读率和传播(分享、收藏、点赞)。完读率高 = 情绪拉扯到位;传播率高 = 有共鸣或反转让人想转发。
原则 3:短篇风口来得快去得快
短篇题材信号可能在数周内失效。输出风口候选时必须给出有效期、饱和风险和下次复扫时间;未复扫前不得当作长期趋势。
---
扫榜流程
Phase 1:确认平台和方向
问用户:「你想看哪个平台?(知乎盐言/番茄短篇/七猫短篇/其他)有没有想写的类型方向?」
关键判断:
- 用户已有方向 → 针对该方向做深度扫榜
- 用户没有方向 → 做全榜概览 + 找趋势
- 用户想跨平台比较 → 做平台对比分析
---
Phase 1.5:确定数据来源
扫榜需要真实数据支撑。 根据当前环境选择数据来源:
| 优先级 | 模式 | 说明 | 何时用 |
|---|---|---|---|
| 1 | browser-cdp 采集 | 直接抓取平台页面,产出结构化文件 | 有 Chrome 环境时(优先) |
| 2 | 用户提供 | 用户粘贴榜单截图/文字/链接 | 用户已有数据时 |
| 3 | 内置知识 | 基于知识库中的趋势数据和方法论做分析 | 无法联网、用户无数据时 |
browser-cdp 采集模式
使用 /browser-cdp 启动 Chrome,直接抓取平台页面的结构化数据。
点众采集目标:
| 页面 | URL | 核心字段 |
|---|---|---|
| 男频短篇 | ishugui.com/browse | 书名·作者·标签·状态·字数·评分·最新章节 |
| 女频短篇 | ishugui.com/browse/on3 | 书名·作者·标签·状态·字数·评分·最新章节 |
黑岩采集目标:
| 页面 | URL | 核心字段 |
|---|---|---|
| 书库列表 | manage.zhangwenpindu.cn/books/booklist | 书名·作者·字数·分类·类型·价格·创建/更新时间·标签(详情模式) |
黑岩需要登录! 必须先在 Chrome 中手动登录 manage.zhangwenpindu.cn,脚本才能从 Cookie 中提取 Bearer token 调用后端 API。未登录会报错提示。黑岩采集失败时标记为 SKIP,继续其他平台采集,不中断整个 Phase 1。- 黑岩专用:
--pages N(每页 20 条)、--detail(逐本详情,含标签/简介,速度较慢)、--channel male/female - 点众专用:
--channel male/female/all
文件命名:{平台}{类型}_{YYYYMMDD}.md,例:点众男频短篇_20260501.md
用户提供操作指引:
- 请用户截图或复制粘贴榜单内容
- 如果用户提供链接,用 WebFetch 抓取页面内容
- 如果用户只提供故事名列表,直接进入分析
内置知识操作指引:
- 加载
references/real-market-data.md(跨平台写作差异对照) - 明确标注:「以下分析基于历史趋势数据;未完成实时榜单校验前只能作为候选假设。」并列出需要复扫的平台页面。
浏览器操控(高级模式):
- 如果可用 agent-browser CLI,通过 CDP 连接 Chrome 获取平台数据
- 示例:
agent-browser --cdp 9222 open "https://www.ishugui.com/browse" - 可复用用户已登录的 Chrome session,获取完整榜单数据
- 适用于需要登录才能看到的数据(知乎个人中心、番茄书架等)
---
Phase 2:数据分析
知乎盐言故事分析维度
| 维度 | 看什么 |
|---|---|
| 热门榜单 | 当前最受关注的故事 |
| 高赞故事 | 口碑最好的作品结构 |
| 新作者上榜 | 非头部账号的题材选择与开篇模式 |
| 付费转化率 | 哪些题材读者愿意付费 |
| 标签分布 | 热门标签的变化趋势 |
通用分析维度
对每个平台提取:
1. 情绪类型分布:当前哪种情绪拉扯最火(虐恋/反转/悬疑/治愈/打脸) 2. 题材热点:具体什么设定/场景反复出现 3. 篇幅分布:热门短篇集中在多少字 4. 开头模式:热门短篇的第一段/第一句怎么写 5. 结尾类型:HE(好结局)/BE(坏结局)/开放式 的比例 6. 标题模式:热门短篇的命名规律 7. 人设模型:反复出现的主角类型
---
Phase 3:输出扫榜报告
# 短篇网文扫榜报告:{平台名称}
## 市场概况
- 扫榜时间:{日期}
- 核心发现:{一句话总结}
## 情绪热度排行
| 排名 | 情绪类型 | 榜上数量 | 趋势 | 代表作 |
|------|----------|----------|------|--------|
| 1 | {类型} | {N篇} | ↑/→/↓ | {标题} |
## 题材热点
| 题材 | 热度 | 竞争程度 | 门槛 | 代表作 |
|------|------|----------|------|--------|
| {题材} | 高/中/低 | 激烈/一般/蓝海 | 高/中/低 | {标题} |
## 关键数据洞察
- 篇幅区间:热门短篇集中在 {X}-{Y} 字
- 开头模式:{高频开头模式}
- 结尾偏好:{HE/BE/开放式的比例}
- 标题特征:{命名规律}
- 人设热词:{高频主角类型}
## 风口预警
- 🔥 正在爆发:{题材} — {依据}
- ⚡ 即将起风:{题材} — {依据}
- ⚠️ 即将饱和:{题材} — {依据}
## 值得写的方向
1. {方向 + 情绪拉扯方式 + 可行性}
2. {方向 + 情绪拉扯方式 + 可行性}
3. {方向 + 情绪拉扯方式 + 可行性}
## 一句话
{犀利总结}---
Phase 4:选题匹配
根据扫榜结果,结合项目条件输出选题匹配:
- 低复杂度候选:反转类、打脸类(结构清晰、验证成本低)
- 高复杂度候选:悬疑类、虐恋类(技术壁垒高,需要伏笔、反转和情绪控制证据)
- 优先候选:当前样本强信号 × 项目素材/能力约束可支撑的交叉点
关键判断:
- 情绪拉扯力 > 题材创新力(短篇读者更看重情绪体验)
- 开头 3 句话是留存高风险区,必须建立冲突、身份差或情绪钩子
- 反转是短篇常见传播引擎;若不使用反转,必须用强共鸣、强话题或强余韵补足传播风险
---
平台特性速查
| 平台 | 调性 | 核心指标 | 主力读者 | 适合类型 | 短篇主力字数 |
|---|---|---|---|---|---|
| 知乎盐言故事 | 精品短篇,情绪深度 | 付费转化、收藏 | 20-35 都市人群 | 虐恋、反转、悬疑、现实 | 5千-1.5万字 |
| 七猫短篇 | 下沉市场,女频为主 | 完读率 | 女性为主(80%+) | 总裁/现实/宅斗/年代/悬疑 | 1-2万字(7-19章) |
| 黑岩短篇 | 极端情绪,快节奏 | 完读率、付费 | 混合 | 虐恋、复仇、身份反转 | 8千-4万字 |
| 点众短篇 | 精品快节奏 | 完读率 | 混合 | 家庭复仇、假千金、弹幕流 | 1-2万字(5-10章) |
---
流程衔接
流水线: 短篇 位置: 扫榜(第 1/3 步)
| 时机 | 跳转到 | 命令 |
|---|---|---|
| 找到方向 | story-short-analyze | /story-short-analyze |
| 直接开写 | story-short-write | /story-short-write |
| 更适合长篇 | story-long-scan | /story-long-scan |
---
参考资料
按需加载以下文件:
| 文件 | 何时加载 |
|---|---|
| references/real-market-data.md | 核心参考:跨平台写作差异对照表、各平台简介公式速查、题材爆款公式速查表、各平台写作特征 |
| scripts/cdp-utils.js | CDP 公共工具函数(ab/sleep/evalJSON/safeStr/scrollLoad/getArg),各采集脚本共用 |
| scripts/dz-browse-scraper.js | 点众短篇采集(男频/女频),按 bookId 聚合 anchor 解出书名/评分/简介/作品页(避免把 UI 文字或简介误当书名),带连通性自检+书名解析率质量门,配合 browser-cdp 使用 |
| scripts/heiyan-booklist-scraper.js | 黑岩书库列表采集,后端 API 模式(Bearer token),含字数/标签/价格/时间,支持 --detail 获取标签简介;区分 CDP 未连/未登录/超时/接口错误并带书名命中率质量门 |
---
语言
- 跟随用户的语言回复,用户用什么语言就用什么语言回复
- 中文回复遵循《中文文案排版指北》
短篇网文跨平台写作参考
选平台/写简介/定题材时加载。先看决策路由选操作区域。
决策路由
| 你需要什么 | 查看哪个部分 | 核心操作 |
|---|---|---|
| 对比各平台差异 | 跨平台差异对照表 | 按维度横向对比选平台 |
| 写平台简介/导语 | 各平台简介写作公式 | 套对应平台公式 |
| 找爆款题材组合 | 题材爆款公式 | 按题材找公式 + 目标平台 |
| 了解平台独特规则 | 平台独特特征 | 七猫/知乎/点众各有要点 |
---
基于四大平台(七猫、黑岩、知乎、点众)历史热门样本整理。使用前必须用当前 scan 结果校验;未校验时只作为候选规则,不作为当前市场事实。
---
跨平台短篇写作差异对照表
| 维度 | 七猫 | 黑岩 | 点众 | 知乎 |
|---|---|---|---|---|
| 主力字数 | 1-2万字 | 8千-4万字 | 1-2万字(10章x1000字) | 5千-1.5万字(中位1.2万) |
| 章节数 | 7-19 | 4-14段(或1章4万字) | 5-10(标准10章) | 6-10 |
| 人称 | 第一人称95%+ | 第一人称85%(女)/50%(男) | 第一人称95%+ | 混合(古言用第三人称) |
| 章节字数 | 1000-1500 | 2000-5000 | 800-1500 | 800-1500 |
| 对话占比 | 20-38%(均值27.7%) | 30-50% | 9%平均(功能化) | 20-40%(金句多) |
| 段落结构 | 1-3句/段 | 1-3句/段 | 1-2句/段(16-37字) | 1-3句/段(30-60字) |
| 导语/简介 | 必须(100-200字) | 必须(100-363字) | 必须(150-300字) | 简介即作品(200-500字) |
| 开头模式 | 重生回溯/冲突前置 | 极端冲击/道德踩踏 | 标题即剧情/极端事件 | 一句话炸弹/金句开场 |
| 结尾模式 | HE为主(50%+新恋情) | HE/BE各半 | HE为主(悔恨/打脸) | 多样化(开放式多) |
| 独特元素 | 重生、穿书、心声流 | 灵魂视角、虐恋极致 | 弹幕流、读心、系统流 | 专业细节、脑洞、BL |
| 反转手法 | 态度反转(重生后180度转变) | 道德反转+恐怖反转 | 身份反转+死亡反转 | 信息差+情感深层揭露 |
| 爽点节奏 | 虐30%->觉醒15%->爽35%->治愈20% | 女:300-500字小反转/男:1000-2000字大爆发 | 每3章小转折/第10章大爆发 | 层层揭露/慢节奏但后劲大 |
| 情绪密度 | 中-高 | 最高(女频3.23次/千字) | 极高(短句占比33-66%) | 中(克制但有力) |
| 对话引号 | 「」或"" | 「」或"" | ""为主 | 「」为主 |
| 核心定位 | 重生幻想 | 极致刺激 | 情绪快餐 | 情感精品 |
---
各平台简介写作公式
| 平台 | 简介公式 | 字数 | 核心特征 |
|---|---|---|---|
| 七猫总裁 | 正常生活->伤害事件->最残忍对话->女主反转->悬念钩子 | 133-322字 | 70%含对话,对话选最伤人那句 |
| 黑岩女频 | 递进式控诉/心声揭穿/绝症倒计时/重生对比/公开羞辱+底牌 | 103-363字 | 核心是"这一次我..." |
| 黑岩男频 | 极简对白+信息炸弹/前世被害+重生/职场压榨+超自然 | 109-401字 | 第一段直接上最炸裂的场景或对白 |
| 点众 | 场景设定+信任陷阱+致命真相+反击暗示 | 150-300字 | 简介浓缩前1/3精华 |
| 知乎 | 人物关系+核心冲突+2-3转折+金句/留白 | 200-500字 | 简介即独立微小说,有金句收尾 |
---
题材爆款公式
| 题材 | 爆款公式 | 核心情绪 | 目标平台 |
|---|---|---|---|
| 总裁豪门 | 契约婚姻+白月光+觉醒离开 | 虐->爽 | 七猫/黑岩 |
| 现实生活 | 家庭不公+重生/觉醒+反杀 | 愤怒->解气 | 七猫 |
| 宫闱宅斗 | 被害+隐忍布局+反攻称帝 | 压抑->极致释放 | 七猫 |
| 年代重生 | 替罪冤屈+重生+选不同路 | 憋屈->痛快 | 七猫 |
| 现代悬疑 | 超自然设定+犯罪+反转 | 紧张->震撼 | 七猫/知乎 |
| 玄幻仙侠 | 重生+反常行为+对手自乱 | 爽->更爽 | 七猫 |
| 灵魂虐文 | 极致虐待+灵魂旁观+悔恨 | 愤怒->意难平 | 点众/黑岩 |
| 职场逆袭 | 重生/穿越+投资+前夫后悔 | 憋屈->极爽 | 点众 |
| 架空历史 | 性别反转/身份错位+离开+后悔 | 憋屈->释然 | 七猫 |
| 家庭复仇 | 出轨/背叛+收集证据+当众打脸 | 愤怒->痛快 | 七猫/黑岩 |
---
平台独特特征
七猫
- 第一人称95%+,女性主角80%+
- 每章1000-1500字,7-10章为主力区间
- 几乎都有明确导语(前言概括全文核心冲突)
知乎
- 专业细节丰富(医疗有医学知识,法律有法律细节)
- 社会议题导向(婆媳、重男轻女、医疗纠纷)
- 情绪更克制但后劲更大,对话更真实
- 简介即作品:200-500字微小说,有金句收尾
点众标题公式
白眼狼妹妹把家产送人,我让她净身出户= 人物+行为+反杀愚人节死在棺材里后,三个哥哥都悔疯了= 事件+后果+情绪春节返程,老公一家把我女儿挂车外狗笼子里= 场景+冲突+愤怒
/**
* CDP 工具函数 — 各平台采集脚本的公共依赖
*
* 使用方式:
* const { ab, sleep, evalJSON, scrollLoad, getArg, safeStr } = require("./cdp-utils");
*
* 前置:
* node {SKILL_DIR}/browser-cdp/scripts/setup-cdp-chrome.js 9222
*/
const { execSync } = require("child_process");
// ---------------------------------------------------------------------------
// agent-browser 工具函数
// ---------------------------------------------------------------------------
/**
* 调用 agent-browser CLI
* @param {number} port - CDP 端口
* @param {...string} args - agent-browser 参数
* @returns {string} stdout(trim 后)
*/
function ab(port, ...args) {
const cmd = args.map((a) => `"${a.replace(/"/g, '\\"')}"`).join(" ");
try {
return execSync(`agent-browser --cdp ${port} ${cmd}`, {
encoding: "utf-8",
timeout: 20000,
stdio: ["pipe", "pipe", "pipe"],
}).trim();
} catch (e) {
return e.stdout?.trim() || "";
}
}
/** 等待 ms 毫秒(跨平台,不依赖系统 sleep 命令) */
function sleep(ms) {
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
}
/** 在浏览器内执行 JS 并解析 JSON 返回值 */
function evalJSON(port, js) {
const raw = ab(port, "eval", js);
if (!raw || raw === "ERR") return null;
try {
let parsed = JSON.parse(raw);
if (typeof parsed === "string") {
try { parsed = JSON.parse(parsed); } catch {}
}
return parsed;
} catch {
return null;
}
}
/**
* 安全地将值插入浏览器 eval 字符串。
* 使用 JSON.stringify 确保值不会因特殊字符(引号、反斜杠等)破坏 eval 字符串。
* @param {*} val - 要插入的值
* @returns {string} JSON 字符串表示(含引号)
*/
function safeStr(val) {
return JSON.stringify(String(val));
}
/**
* 滚动页面加载更多内容
* @param {number} port - CDP 端口
* @param {number} times - 滚动次数
* @param {number} [interval=1000] - 每次滚动间隔(ms)
*/
function scrollLoad(port, times, interval = 1000) {
for (let i = 0; i < times; i++) {
ab(port, "eval", "window.scrollBy(0, window.innerHeight)");
sleep(interval);
}
}
/** 解析 --xxx 参数 */
function getArg(args, name) {
const i = args.indexOf(name);
return i >= 0 && i + 1 < args.length ? args[i + 1] : null;
}
module.exports = { ab, sleep, evalJSON, safeStr, scrollLoad, getArg };
#!/usr/bin/env node
/**
* 点众阅读短篇采集脚本
*
* 配合 browser-cdp skill 使用。先启动 Chrome CDP 环境,再运行本脚本。
* 采集策略:以 /book/{id} 链接为骨架,按 bookId 聚合每本书的多个 anchor
* (封面/书名+评分/简介各一个),从中解出书名、评分、简介、作品页,再从卡片
* 容器文本里解出 作者·标签·状态·字数 与最新章节。避免纯 innerText 行序解析把
* UI 文字或简介误当书名。
* 输出 Markdown 格式。
*
* 用法:
* node dz-browse-scraper.js --channel male # 男频
* node dz-browse-scraper.js --channel female # 女频
* node dz-browse-scraper.js --channel all # 全部
*
* 前置:
* node {SKILL_DIR}/browser-cdp/scripts/setup-cdp-chrome.js 9222
*/
const fs = require("fs");
const path = require("path");
const { ab, sleep, safeStr, scrollLoad, getArg } = require("./cdp-utils");
const CHANNELS = [
{ id: "male", label: "男频", tab: "男频", url: "https://www.ishugui.com/browse" },
{ id: "female", label: "女频", tab: "女频", url: "https://www.ishugui.com/browse/on3" },
];
// ---------------------------------------------------------------------------
// eval 封装:统一走 base64,规避复杂 JS(正则/引号/反斜杠)的 shell 转义问题
// ---------------------------------------------------------------------------
function evalJSON(port, js) {
const b64 = Buffer.from(String(js), "utf-8").toString("base64");
const raw = ab(port, "eval", "-b", b64);
if (!raw || raw === "ERR") return null;
try {
let parsed = JSON.parse(raw);
if (typeof parsed === "string") {
try { parsed = JSON.parse(parsed); } catch {}
}
return parsed;
} catch {
return null;
}
}
// ---------------------------------------------------------------------------
// 页面操作
// ---------------------------------------------------------------------------
/** 连通性 + 页面就绪自检 */
function probePage(port) {
return evalJSON(
port,
"JSON.stringify({host:location.host,len:(document.body&&document.body.innerText||'').length})"
);
}
/** 点击指定文本的 tab */
function clickTab(port, text) {
const js =
"JSON.stringify((function(){" +
"var all=document.querySelectorAll('div,span,a,button,li');" +
"var el=Array.from(all).find(function(e){return (e.textContent||'').trim()===" + safeStr(text) + "});" +
"if(el){el.click();return true}return false" +
"})())";
return evalJSON(port, js);
}
/**
* 以 /book/{id} 链接为骨架聚合每本书的字段。
* 书名取自“书名+评分”anchor(去掉尾部 X.X分),简介取自最长 anchor,
* 作者/标签/状态/字数从卡片容器文本里正则提取。
*/
function buildStoriesJS() {
return `JSON.stringify((function(){
var anchors=Array.from(document.querySelectorAll('a')).filter(function(a){
return /\\/book\\/[0-9]+/.test(a.getAttribute('href')||'');
});
var byId={};var order=[];
anchors.forEach(function(a){
var m=(a.getAttribute('href')||'').match(/\\/book\\/([0-9]+)/);if(!m)return;
var id=m[1];var txt=(a.innerText||a.textContent||'').replace(/\\s+/g,' ').trim();
if(!byId[id]){byId[id]={id:id,texts:[],node:a};order.push(id);}
if(txt)byId[id].texts.push(txt);
});
var out=[];
order.forEach(function(id){
var g=byId[id];
var title='',score='';
for(var i=0;i<g.texts.length;i++){
var tm=g.texts[i].match(/^(.+?)\\s*([0-9]+(?:\\.[0-9]+)?)分$/);
if(tm){title=tm[1].trim();score=tm[2]+'分';break;}
}
if(!title){
var cand=g.texts.filter(Boolean).slice().sort(function(a,b){return a.length-b.length;});
title=cand.length?cand[0]:'';
}
// 简介:最长的、不是“书名+评分”的 anchor 文本
var desc='';
g.texts.forEach(function(t){ if(/分$/.test(t))return; if(t.length>desc.length)desc=t; });
// 卡片容器:从任一 anchor 向上找到含“字”的祖先
var el=g.node;
for(var j=0;j<6;j++){ if(el.parentElement){el=el.parentElement; if((el.innerText||'').indexOf('字')>-1)break;} }
var card=(el.innerText||'').replace(/\\s+/g,' ');
var tail=(desc&&card.indexOf(desc)>-1)?card.slice(card.indexOf(desc)+desc.length):card;
var meta=tail.match(/([^·]{1,20}?)\\s*·\\s*([^·]{1,20}?)\\s*·\\s*(完结|完本|连载)\\s*·\\s*([0-9]+)\\s*字/);
var author=meta?meta[1].trim():'';
var tag=meta?meta[2].trim():'';
var status=meta?meta[3]:'';
var words=meta?meta[4]+'字':'';
var um=card.match(/最新章节[::\\s]*([^·]{1,40})/);
var update=um?um[1].trim():'';
out.push({rank:out.length+1,bookId:id,title:title,score:score,author:author,tag:tag,status:status,words:words,update:update,desc:desc.slice(0,200),url:'https://www.ishugui.com/book/'+id});
});
return out;
})())`;
}
function extractStories(port) {
const list = evalJSON(port, buildStoriesJS());
return Array.isArray(list) ? list : [];
}
// ---------------------------------------------------------------------------
// 主流程
// ---------------------------------------------------------------------------
const args = process.argv.slice(2);
const PORT = parseInt(getArg(args, "--port") || "9222", 10);
const OUTDIR = getArg(args, "--outdir") || ".";
const CHANNEL = getArg(args, "--channel") || "male";
function scrapeChannel(port, channelId) {
const ch = CHANNELS.find((c) => c.id === channelId);
if (!ch) return null;
console.log(`\n→ 采集 点众${ch.label}短篇...`);
let stories;
try {
ab(port, "open", ch.url);
sleep(3000);
// 连通性自检:CDP 未起/被重定向时给可操作报错,而非静默产空
const probe = probePage(port);
if (!probe) {
console.error(
` ✗ CDP 无响应。请确认已用 browser-cdp 启动 Chrome(端口 ${port}),且 agent-browser 可用。`
);
return null;
}
if (probe.host && probe.host.indexOf("ishugui") === -1) {
console.error(` ✗ 当前页面非点众(host=${probe.host}),可能被重定向,已跳过。`);
return null;
}
// 切换频道(female 已有独立 URL,tab 失败不致命)
try {
if (clickTab(port, ch.tab)) {
console.log(` ✓ 切换到${ch.tab}`);
sleep(2000);
}
} catch (tabErr) {
console.error(`[dz] ${ch.label} tab切换出错,继续采集: ${tabErr.message}`);
}
scrollLoad(port, 8);
sleep(1000);
stories = extractStories(port);
} catch (err) {
console.error(`[dz] ${ch.label} 页面加载或提取出错: ${err.message}`);
return null;
}
if (!stories.length) {
console.error(
`[dz] 采集失败:未解析到书目(页面结构可能变动或未加载)。请人工打开 ${ch.url} 确认页面正常。`
);
return null;
}
// 质量门:书名命中率是点众采集成败的核心信号
const titled = stories.filter((s) => s.title).length;
const ratio = titled / stories.length;
const quality = ratio < 0.5 ? "[书名解析异常]" : "[OK]";
console.log(` ✓ 提取 ${stories.length} 条(书名 ${titled}/${stories.length})`);
if (ratio < 0.5) {
console.error(` ⚠ 书名解析率偏低(${titled}/${stories.length}),结果质量已标注。`);
}
const now = new Date().toISOString();
const lines = [
`# 点众 · ${ch.label}短篇`,
"",
`- 来源:${ch.url}`,
`- 抓取时间:${now}`,
`- 条目数:${stories.length}`,
`- 书名解析:${titled} / ${stories.length}`,
`- 数据质量:${quality}`,
"",
"---",
"",
];
stories.forEach((s, i) => {
try {
lines.push(`### #${i + 1} ${s.title || "(书名待解析)"}`);
const meta = [s.author, s.tag, s.status, s.words, s.score].filter(Boolean).join(" · ");
if (meta) lines.push(`*${meta}*`);
if (s.update) lines.push(`**最新:** ${s.update}`);
if (s.url) lines.push(`[作品页](${s.url})`);
if (s.desc) {
lines.push("");
lines.push(`> ${s.desc.substring(0, 150)}${s.desc.length > 150 ? "..." : ""}`);
}
lines.push("", "---", "");
} catch (storyErr) {
console.error(`[dz] ${ch.label} 第${i + 1}条处理出错: ${storyErr.message}`);
lines.push("", "---", "");
}
});
return lines.join("\n");
}
function main() {
const channels = CHANNEL === "all" ? CHANNELS.map((c) => c.id) : [CHANNEL];
for (const ch of channels) {
const content = scrapeChannel(PORT, ch);
if (!content) continue;
const chInfo = CHANNELS.find((c) => c.id === ch);
const date = new Date().toISOString().slice(0, 10).replace(/-/g, "");
const filename = `点众${chInfo.label}短篇_${date}.md`;
fs.mkdirSync(OUTDIR, { recursive: true });
const filepath = path.join(OUTDIR, filename);
fs.writeFileSync(filepath, content, "utf-8");
console.log(` ✓ 已保存: ${filepath}`);
}
}
if (require.main === module) {
try {
main();
} catch (e) {
console.error(`点众采集失败: ${e && e.message ? e.message : e}`);
process.exit(1);
}
}
module.exports = { buildStoriesJS };
#!/usr/bin/env node
/**
* 黑岩短篇书库列表采集脚本
*
* ⚠️ 前置条件(必须):
* 1. 启动 Chrome CDP 环境
* 2. 在 Chrome 中手动登录 https://manage.zhangwenpindu.cn
* 登录后会生成 Admin-Token cookie,脚本需要它调用后端 API
* 未登录 → 脚本报错「未检测到 Admin-Token」
*
* 采集策略:从 Cookie 提取 Bearer token,调用 ms.zhangwenpindu.cn 后端 API
* 获取结构化 JSON 数据(书名、作者、字数、分类、标签等)。
*
* 用法:
* node heiyan-booklist-scraper.js --pages 5 # 采集前5页(每页20条)
* node heiyan-booklist-scraper.js --pages 3 --channel male # 仅男频
* node heiyan-booklist-scraper.js --pages 2 --detail # 含逐本详情(标签等)
*/
const fs = require("fs");
const path = require("path");
const { ab, sleep, evalJSON, safeStr, getArg } = require("./cdp-utils");
const BOOKLIST_URL = "https://manage.zhangwenpindu.cn/books/booklist";
const API_BASE = "https://ms.zhangwenpindu.cn";
const PAGE_SIZE = 20;
// ---------------------------------------------------------------------------
// API 调用
// ---------------------------------------------------------------------------
/** 连通性自检:区分 CDP 未连 vs 已连但未登录 */
function probePage(port) {
return evalJSON(port, "JSON.stringify({host:location.host})");
}
/** 从 Cookie 中提取 Admin-Token */
function getToken(port) {
const js =
"JSON.stringify((()=>{" +
"var m=document.cookie.match(/Admin-Token=([^;]+)/);" +
"return m?m[1]:''" +
"})())";
return evalJSON(port, js) || "";
}
/** 调用后端 API 获取书籍列表 */
function fetchBookList(port, token, pageNum) {
const t = safeStr(token);
const js =
"fetch(" + safeStr(API_BASE + "/manage/book/list") + "," +
"{method:'POST'," +
"headers:{" +
"'Content-Type':'application/x-www-form-urlencoded'," +
"'Authorization':'Bearer '+" + t +
"}," +
"body:new URLSearchParams({pageNum:" + safeStr(pageNum) + ",pageSize:" + safeStr(PAGE_SIZE) + ",language:'zh_TW'})" +
"}).then(function(r){return r.json()})";
return evalJSON(port, js);
}
/** 调用后端 API 获取书籍详情(标签等) */
function fetchBookDetail(port, token, bookId) {
const t = safeStr(token);
const js =
"fetch(" + safeStr(API_BASE + "/manage/book/" + encodeURIComponent(bookId)) + "," +
"{headers:{'Authorization':'Bearer '+" + t + "}}" +
").then(function(r){return r.json()})";
return evalJSON(port, js);
}
// ---------------------------------------------------------------------------
// 主流程
// ---------------------------------------------------------------------------
const args = process.argv.slice(2);
const PORT = parseInt(getArg(args, "--port") || "9222", 10);
const OUTDIR = getArg(args, "--outdir") || ".";
const PAGES = parseInt(getArg(args, "--pages") || "5", 10);
const CHANNEL = getArg(args, "--channel") || "all";
const DETAIL = args.includes("--detail");
function buildAndSave(allBooks, total, filtered, filepath) {
const now = new Date().toISOString();
const maleBooks = filtered.filter((b) => b.classifyStr === "男频");
const femaleBooks = filtered.filter((b) => b.classifyStr === "女频");
const otherBooks = filtered.filter(
(b) => b.classifyStr !== "男频" && b.classifyStr !== "女频"
);
const groups = [
{ label: "男频", books: maleBooks },
{ label: "女频", books: femaleBooks },
];
if (otherBooks.length) {
groups.push({ label: "其他", books: otherBooks });
}
const lines = [
`# 黑岩 · 书库列表`,
"",
`- 来源:${BOOKLIST_URL}`,
`- 抓取时间:${now}`,
`- 总条目:${total}`,
`- 已采集:${filtered.length} 条(${PAGES} 页)`,
DETAIL ? "- 含详情(标签、简介)" : "- 列表模式(加 --detail 获取标签和简介)",
"",
"---",
"",
];
for (const g of groups) {
if (!g.books.length) continue;
lines.push(`## ${g.label}短篇 — ${g.books.length} 本`, "");
for (let i = 0; i < g.books.length; i++) {
try {
const b = g.books[i];
lines.push(`### #${i + 1} ${b.name}`);
const meta = [
b.userName,
b.classifyStr + "/" + b.typeDesc,
b.words ? b.words.toLocaleString() + "字" : "",
b.price ? b.price + "钻" : "",
b.open ? "公开" : "未公开",
].filter(Boolean).join(" · ");
if (meta) lines.push(`*${meta}*`);
if (b.createTime) lines.push(`**创建:** ${b.createTime}`);
if (b.updateTime) lines.push(`**更新:** ${b.updateTime}`);
if (b.tags && b.tags.length) {
lines.push(`**标签:** ${b.tags.join("、")}`);
}
if (b.description) {
lines.push("");
lines.push(`> ${b.description.substring(0, 200)}${b.description.length > 200 ? "..." : ""}`);
}
lines.push("");
} catch (bookErr) {
console.error(`[heiyan] ${g.label} 第${i + 1}条处理出错: ${bookErr.message}`);
lines.push("");
}
}
lines.push("---", "");
}
fs.mkdirSync(OUTDIR, { recursive: true });
fs.writeFileSync(filepath, lines.join("\n"), "utf-8");
console.log(` ✓ 已保存: ${filepath}`);
}
function main() {
console.log("\n→ 采集 黑岩书库列表(API 模式)...");
console.log(` 计划采集: ${PAGES} 页(每页 ${PAGE_SIZE} 条)`);
const date = new Date().toISOString().slice(0, 10).replace(/-/g, "");
const filename = `黑岩书库列表_${date}.md`;
const filepath = path.join(OUTDIR, filename);
// 先导航到管理后台获取 token
let token;
try {
ab(PORT, "open", BOOKLIST_URL);
sleep(3000);
// 连通性自检:把"CDP 没起来"和"没登录"分开,避免误导用户去登录
const probe = probePage(PORT);
if (!probe) {
console.error(
` ✗ CDP 无响应。请确认已用 browser-cdp 启动 Chrome(端口 ${PORT}),且 agent-browser 可用。`
);
return;
}
token = getToken(PORT);
} catch (err) {
console.error(`[heiyan] 页面加载或 token 提取出错: ${err.message}`);
return;
}
if (!token) {
console.log(" ✗ 未检测到 Admin-Token(CDP 已连,但当前未登录)");
console.log(" → 请先在 Chrome 中打开 https://manage.zhangwenpindu.cn 并登录");
console.log(" → 登录后重新运行本脚本");
return;
}
console.log(" ✓ 获取到认证 token");
// 分页采集
const allBooks = [];
let total = 0;
for (let p = 1; p <= PAGES; p++) {
try {
sleep(800);
const resp = fetchBookList(PORT, token, p);
// 区分失败:接口无响应(超时/CDP) / 401 未授权
if (!resp) {
console.error(` ✗ 第${p}页接口无响应(请求超时或 CDP 中断),已停止。`);
break;
}
if (resp.code === 401) {
console.log(` ⚠ 第${p}页认证失败(401),请重新登录后重试。`);
break;
}
const rows = resp?.data?.rows;
if (!rows || !rows.length) {
// 仅在无数据时才用 code 区分"服务端错误"与"正常到底",避免把
// 携带非常规成功 code 的有效响应误判为错误(成功带 rows 一律放行)
if (resp.code != null && resp.code !== 0 && resp.code !== 200) {
console.error(` ✗ 第${p}页接口返回错误 code=${resp.code} ${resp.msg || ""},已停止。`);
} else {
console.log(` 第${p}页无数据,停止`);
}
break;
}
if (p === 1) {
total = parseInt(resp.data.total) || 0;
console.log(` 总条目: ${total}`);
}
allBooks.push(...rows);
console.log(` 第${p}页: ${rows.length} 条 (累计 ${allBooks.length})`);
} catch (pageErr) {
console.error(`[heiyan] 第${p}页采集出错,跳过: ${pageErr.message}`);
if (allBooks.length > 0) {
console.log(` 已采集 ${allBooks.length} 条,继续处理已有数据`);
}
break;
}
}
if (!allBooks.length) {
console.error("[heiyan] 采集失败:未取到任何书目。多为登录态过期或接口变动,请重新登录后重试。");
return;
}
// 质量门:书名命中率。API 改字段名时会整片 undefined,必须拦截而非静默写盘
const named = allBooks.filter((b) => b && b.name).length;
if (named / allBooks.length < 0.5) {
console.error(
`[heiyan] 采集失败:${allBooks.length} 条里仅 ${named} 条有书名,疑似接口字段变动,已放弃写盘。`
);
return;
}
// 频道筛选
let filtered = allBooks;
if (CHANNEL === "male") {
filtered = allBooks.filter((b) => b.classifyStr === "男频");
} else if (CHANNEL === "female") {
filtered = allBooks.filter((b) => b.classifyStr === "女频");
}
// 可选:逐本获取详情(标签等)
if (DETAIL && filtered.length) {
console.log(` 获取 ${filtered.length} 本详情...`);
for (let i = 0; i < filtered.length; i++) {
try {
sleep(500);
const detail = fetchBookDetail(PORT, token, filtered[i].id);
if (detail?.data) {
filtered[i].tags = detail.data.tags || [];
filtered[i].description = detail.data.description || "";
filtered[i].chapterCount = detail.data.chapterCount || 0;
}
if ((i + 1) % 10 === 0) {
console.log(` 已获取 ${i + 1}/${filtered.length}`);
}
} catch (detailErr) {
console.error(`[heiyan] 第${i + 1}本详情获取出错,跳过: ${detailErr.message}`);
}
}
console.log(" ✓ 详情获取完成");
}
buildAndSave(allBooks, total, filtered, filepath);
}
try {
main();
} catch (e) {
console.error(`黑岩采集失败: ${e && e.message ? e.message : e}`);
process.exit(1);
}
Related skills
FAQ
Is Story Short Scan safe to install?
skills.sh reports 0 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.