story-short-scan
短篇网文扫榜。分析知乎盐言、七猫、黑岩、点众等平台热门短篇数据,捕捉风口题材。触发方式:/story-short-scan、/短篇扫榜、「短篇什么火」「知乎故事排行」。
Install
npx skills add https://github.com/zenstory-ai/oh-story-claudecode/tree/main/skills/story-short-scan
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install zenstory-ai-oh-story-claudecode@llmmart
git clone https://github.com/zenstory-ai/oh-story-claudecode.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole zenstory-ai/oh-story-claudecode collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
story-short-scan:短篇网文扫榜
你是短篇网文市场分析师。你的任务是基于榜单样本识别短篇市场格局,并输出可执行的情绪方向、题材候选、风险阈值和验证动作。
核心信念:短篇市场变化快,题材信号有效期短。 扫榜报告必须标注样本日期、趋势可信度和下次重新扫榜的时间。
核心哲学
原则 1:短篇市场是情绪市场
短篇网文的核心是情绪交付。读者在短时间内完成一次情绪体验;扫榜要提取高频情绪、触发场景、情绪爆发点和读者愿意转发的点,而不是只记录题材名。
原则 2:短篇的生命力在传播
短篇不像长篇靠追读赚钱。短篇靠的是单篇完读率和传播(分享、收藏、点赞)。完读率高 = 情绪拉扯到位;传播率高 = 有共鸣或反转让人想转发。
原则 3:短篇风口来得快去得快
短篇题材信号可能在数周内失效。输出风口候选时必须给出有效期、饱和风险和下次复扫时间;未复扫前不得当作长期趋势。
扫榜流程
Phase 1:确认平台和方向
问用户:「你想看哪个平台?(知乎盐言/番茄短篇/七猫短篇/其他)有没有想写的类型方向?」
关键判断:
- 用户已有方向 → 针对该方向做深度扫榜
- 用户没有方向 → 做全榜概览 + 找趋势
- 用户想跨平台比较 → 做平台对比分析
Phase 2:确定数据来源
扫榜需要真实数据支撑。 根据当前环境选择数据来源:
| 优先级 | 模式 | 说明 | 何时用 |
|---|---|---|---|
| 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,继续其他平台采集,不中断本轮数据采集。
- 黑岩专用:
--pages N(每页 20 条)、--detail(逐本详情,含标签/简介,速度较慢)、--channel male/female - 点众专用:
--channel male/female/all
文件命名:{平台}{类型}_{YYYYMMDD}.md,例:点众男频短篇_20260501.md
用户提供操作指引:
- 请用户截图或复制粘贴榜单内容
- 如果用户提供链接,用 WebFetch 抓取页面内容
- 如果用户只提供故事名列表,直接进入分析
内置知识操作指引:
- 加载
references/real-market-data.md(跨平台写作差异对照) - 明确标注:「以下分析基于历史趋势数据;未完成实时榜单校验前只能作为候选假设。」并列出需要复扫的平台页面。
Phase 3:数据分析
知乎盐言故事分析维度
| 维度 | 看什么 |
|---|---|
| 热门榜单 | 当前最受关注的故事 |
| 高赞故事 | 口碑最好的作品结构 |
| 新作者上榜 | 非头部账号的题材选择与开篇模式 |
| 付费转化率 | 哪些题材读者愿意付费 |
| 标签分布 | 热门标签的变化趋势 |
通用分析维度
对每个平台提取:
- 情绪类型分布:当前哪种情绪拉扯最火(虐恋/反转/悬疑/治愈/打脸)
- 题材热点:具体什么设定/场景反复出现
- 篇幅分布:热门短篇集中在多少字
- 开头模式:热门短篇的第一段/第一句怎么写
- 结尾类型:HE(好结局)/BE(坏结局)/开放式 的比例
- 标题模式:热门短篇的命名规律
- 人设模型:反复出现的主角类型
Phase 4:输出扫榜报告
# 短篇网文扫榜报告:{平台名称}
## 市场概况
- 扫榜时间:{日期}
- 核心发现:{一句话总结}
## 情绪热度排行
| 排名 | 情绪类型 | 榜上数量 | 趋势 | 代表作 |
|------|----------|----------|------|--------|
| 1 | {类型} | {N篇} | ↑/→/↓ | {标题} |
## 题材热点
| 题材 | 热度 | 竞争程度 | 门槛 | 代表作 |
|------|------|----------|------|--------|
| {题材} | 高/中/低 | 激烈/一般/蓝海 | 高/中/低 | {标题} |
## 关键数据洞察
- 篇幅区间:热门短篇集中在 {X}-{Y} 字
- 开头模式:{高频开头模式}
- 结尾偏好:{HE/BE/开放式的比例}
- 标题特征:{命名规律}
- 人设热词:{高频主角类型}
## 风口预警
- 🔥 正在爆发:{题材} — {依据}
- ⚡ 即将起风:{题材} — {依据}
- ⚠️ 即将饱和:{题材} — {依据}
## 值得写的方向
1. {方向 + 情绪拉扯方式 + 可行性}
2. {方向 + 情绪拉扯方式 + 可行性}
3. {方向 + 情绪拉扯方式 + 可行性}
## 一句话
{犀利总结}
Phase 5:选题匹配
根据扫榜结果,结合项目条件输出选题匹配:
- 低复杂度候选:反转类、打脸类(结构清晰、验证成本低)
- 高复杂度候选:悬疑类、虐恋类(技术壁垒高,需要伏笔、反转和情绪控制证据)
- 优先候选:当前样本强信号 × 项目素材/能力约束可支撑的交叉点
关键判断:
- 情绪拉扯力 > 题材创新力(短篇读者更看重情绪体验)
- 开头 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 未连/未登录/超时/接口错误并带书名命中率质量门 |
语言
- 跟随用户的语言回复,用户用什么语言就用什么语言回复
- 中文回复遵循《中文文案排版指北》
Files (oh-story-claudecode)
-
references
-
real-market-data.md 4.8 KB
# 短篇网文跨平台写作参考 > 选平台/写简介/定题材时加载。先看决策路由选操作区域。 ## 决策路由 | 你需要什么 | 查看哪个部分 | 核心操作 | |------------|-------------|---------| | 对比各平台差异 | 跨平台差异对照表 | 按维度横向对比选平台 | | 写平台简介/导语 | 各平台简介写作公式 | 套对应平台公式 | | 找爆款题材组合 | 题材爆款公式 | 按题材找公式 + 目标平台 | | 了解平台独特规则 | 平台独特特征 | 七猫/知乎/点众各有要点 | --- 基于四大平台(七猫、黑岩、知乎、点众)历史热门样本整理。使用前必须用当前 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字微小说,有金句收尾 ### 点众标题公式 - `白眼狼妹妹把家产送人,我让她净身出户` = 人物+行为+反杀 - `愚人节死在棺材里后,三个哥哥都悔疯了` = 事件+后果+情绪 - `春节返程,老公一家把我女儿挂车外狗笼子里` = 场景+冲突+愤怒
-
-
scripts
-
cdp-utils.js 8.4 KB
/** * CDP 工具函数 — 各平台采集脚本的公共依赖 * * 使用方式: * const { ab, sleep, evalJSON, evalJSONBase64, scrollLoad, getArg, safeStr, localDateStamp } = require("./cdp-utils"); * * 前置: * node {SKILL_DIR}/browser-cdp/scripts/setup-cdp-chrome.js 9222 */ const { execFileSync } = require("child_process"); const fs = require("fs"); const path = require("path"); /** * On Windows `agent-browser` is an npm shim (agent-browser.cmd/.ps1) that * forwards to the real target — the native agent-browser-win32-*.exe or a * bundled Node CLI. Node refuses to execFile the `.cmd` without a shell * (CVE-2024-27980), and routing the argv array through a shell mangles it: the * `.cmd`'s `%*` is re-tokenized by cmd.exe (splitting on spaces, breaking on * & | ^), and calling the shim by bare name from powershell.exe collapses the * whole array into a single space-joined argument. The exact locus differs by * runtime, so instead of hardening any one shell path we bypass shells entirely: * read the `.cmd` shim, recover the real program plus its fixed leading args, * and execFile that target directly with the argv array — verbatim, no shell. */ function resolveWindowsAgentBrowser(argv) { const dirs = String(process.env.PATH || "").split(path.delimiter); let cmdPath = null; for (const dir of dirs) { if (!dir) continue; const candidate = path.join(dir, "agent-browser.cmd"); if (fs.existsSync(candidate)) { cmdPath = candidate; break; } } if (!cmdPath) return { file: "agent-browser", args: argv }; const dir = path.dirname(cmdPath); const forwardLine = fs .readFileSync(cmdPath, "utf8") .split(/\r?\n/) .find((line) => line.includes("%*")) || ""; const tokens = [...forwardLine.matchAll(/"([^"]*)"/g)] .map((m) => m[1]) .map((t) => t .replace(/%~dp0/gi, () => dir + path.sep) .replace(/%dp0%/gi, () => dir + path.sep) ); const jsIndex = tokens.findIndex((t) => /\.[cm]?js$/i.test(t)); if (jsIndex >= 0) { return { file: process.execPath, args: [...tokens.slice(jsIndex), ...argv] }; } if (tokens.length > 0) { return { file: tokens[0], args: [...tokens.slice(1), ...argv] }; } return { file: "agent-browser", args: argv }; } /** * Build a shell-free invocation. POSIX runs the native `agent-browser` binary * directly; Windows resolves the npm `.cmd` shim to that native target so the * argument array is passed verbatim, never routed through cmd.exe/PowerShell. */ function buildAgentBrowserInvocation(port, args, platform = process.platform) { const argv = ["--cdp", String(port), ...args.map(String)]; if (platform !== "win32") { return { file: "agent-browser", args: argv }; } return resolveWindowsAgentBrowser(argv); } // --------------------------------------------------------------------------- // agent-browser 工具函数 // --------------------------------------------------------------------------- /** * 调用 agent-browser CLI * @param {number} port - CDP 端口 * @param {...string} args - agent-browser 参数 * @returns {string} stdout(trim 后) */ function ab(port, ...args) { const invocation = buildAgentBrowserInvocation(port, args); try { return execFileSync( invocation.file, invocation.args, { encoding: "utf-8", timeout: 20000, stdio: ["pipe", "pipe", "pipe"], windowsHide: true, } ).trim(); } catch (error) { const stderr = error && error.stderr ? String(error.stderr).trim() : ""; const stdout = error && error.stdout ? String(error.stdout).trim() : ""; const detail = stderr || stdout || (error && error.message) || "unknown error"; throw new Error(`agent-browser failed: ${detail}`, { cause: error }); } } /** 等待 ms 毫秒(跨平台,不依赖系统 sleep 命令) */ function sleep(ms) { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); } function parseJSONResult(raw) { if (!raw || raw === "ERR") { throw new Error("agent-browser returned no JSON result"); } try { let parsed = JSON.parse(raw); if (typeof parsed === "string") { try { parsed = JSON.parse(parsed); } catch {} } return parsed; } catch (error) { throw new Error(`agent-browser returned invalid JSON: ${String(raw).slice(0, 160)}`, { cause: error, }); } } /** * 在浏览器内执行 JS,并解析 JSON 返回值。 * 一律走 base64(-b):正文提取用的 JS 常含引号、反斜杠等,作为命令行参数时在 Windows 上 * 无法逐字透传(.cmd 的 %* 与 PowerShell 都会二次解析)。base64 让参数只含 [A-Za-z0-9+/=], * 和各采集脚本已在用的 evalJSONBase64 走同一条安全通道。 */ function evalJSON(port, js) { return evalJSONBase64(port, js); } /** * 通过 agent-browser 的 base64 参数执行复杂 JS,避免命令行转义和参数边界问题。 */ function evalJSONBase64(port, js) { const encoded = Buffer.from(String(js), "utf8").toString("base64"); return parseJSONResult(ab(port, "eval", "-b", encoded)); } /** * 安全地将值插入浏览器 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); if (i >= 0) return i + 1 < args.length ? args[i + 1] : null; const prefix = `${name}=`; const inline = args.find((arg) => String(arg).startsWith(prefix)); return inline === undefined ? null : String(inline).slice(prefix.length); } /** * 输出文件名用的日期戳(YYYYMMDD),一律取**本地日历日**。 * 不能用 new Date().toISOString().slice(0,10):那是 UTC 日期,比 UTC+8 晚 8 小时。 * 文件名是各采集脚本唯一的去重键(一个榜单一天一份),北京时间 00:00-08:00 之间的采集 * 会退回「昨天」的文件名,静默覆盖前一晚采到的同名报告,且这份数据被标成前一天。 * @param {Date} [date] - 默认当前时间 * @returns {string} YYYYMMDD */ function localDateStamp(date) { const d = date instanceof Date ? date : new Date(); const y = String(d.getFullYear()).padStart(4, "0"); const m = String(d.getMonth() + 1).padStart(2, "0"); const day = String(d.getDate()).padStart(2, "0"); return `${y}${m}${day}`; } /** * Run a scraper entrypoint and turn empty/partial output into machine-readable * CLI status. Legacy entrypoints may return an integer; multi-target scrapers * return {planned,written,failed,partial,partialReasons}. */ function runCli(main, label) { Promise.resolve() .then(main) .then((result) => { const outcome = Number.isInteger(result) ? { planned: result, written: result, failed: 0, partial: false, partialReasons: [] } : result; if (!outcome || !Number.isInteger(outcome.written) || outcome.written < 1) { throw new Error("no output was written"); } const failed = Number.isInteger(outcome.failed) ? outcome.failed : 0; const planned = Number.isInteger(outcome.planned) ? outcome.planned : outcome.written + failed; const reasons = Array.isArray(outcome.partialReasons) ? outcome.partialReasons.filter(Boolean).map(String) : []; if (outcome.partial || failed > 0) { const details = [`wrote ${outcome.written}/${planned}`]; if (failed > 0) details.push(`failed ${failed}`); details.push(...reasons); console.error(`${label} partial: ${details.join("; ")}`); process.exitCode = 2; } }) .catch((error) => { const message = error && error.message ? error.message : String(error); console.error(`${label} failed: ${message}`); process.exitCode = 1; }); } module.exports = { ab, sleep, evalJSON, evalJSONBase64, buildAgentBrowserInvocation, safeStr, scrollLoad, getArg, localDateStamp, runCli, }; -
dz-browse-scraper.js 8.4 KB
#!/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, evalJSONBase64, safeStr, scrollLoad, getArg, localDateStamp, runCli } = 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" }, ]; // --------------------------------------------------------------------------- // 页面操作 // --------------------------------------------------------------------------- /** 连通性 + 页面就绪自检 */ function probePage(port) { return evalJSONBase64( 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 evalJSONBase64(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 = evalJSONBase64(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]; let written = 0; for (const ch of channels) { const content = scrapeChannel(PORT, ch); if (!content) continue; const chInfo = CHANNELS.find((c) => c.id === ch); const filename = `点众${chInfo.label}短篇_${localDateStamp()}.md`; fs.mkdirSync(OUTDIR, { recursive: true }); const filepath = path.join(OUTDIR, filename); fs.writeFileSync(filepath, content, "utf-8"); written++; console.log(` ✓ 已保存: ${filepath}`); } return written; } if (require.main === module) { runCli(main, "点众采集"); } module.exports = { buildStoriesJS }; -
heiyan-booklist-scraper.js 12 KB
#!/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, localDateStamp, runCli } = 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"); /** * 字数格式化:手写千分位,不能用 toLocaleString()。 * 后者按宿主 ICU locale 取分隔符,de_* 会写成「123.456字」(读起来像 123 字), * fr_* 用 U+202F、en-IN 会分成「1,23,456」——同一份报告在不同机器上数字不一样。 * 兼容接口把 words 返回成字符串的情况。 */ function fmtWords(words) { const n = typeof words === "number" ? Math.trunc(words) : parseInt(String(words == null ? "" : words).replace(/[^0-9]/g, ""), 10); if (!Number.isFinite(n) || n <= 0) return ""; return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ",") + "字"; } function outputFilename(channel, date) { if (!["all", "male", "female"].includes(channel)) { throw new Error(`未知 --channel: ${channel}(支持 male/female/all)`); } return `黑岩书库列表_${channel}_${date}.md`; } 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, // 分别入数组再拼:预先 classifyStr + "/" + typeDesc 会把缺字段拼成 // 「undefined/undefined」这种真值字符串,filter(Boolean) 拦不住,直接写进报告 [b.classifyStr, b.typeDesc].filter(Boolean).join("/"), fmtWords(b.words), 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 filename = outputFilename(CHANNEL, localDateStamp()); 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 0; } token = getToken(PORT); } catch (err) { console.error(`[heiyan] 页面加载或 token 提取出错: ${err.message}`); return 0; } if (!token) { console.log(" ✗ 未检测到 Admin-Token(CDP 已连,但当前未登录)"); console.log(" → 请先在 Chrome 中打开 https://manage.zhangwenpindu.cn 并登录"); console.log(" → 登录后重新运行本脚本"); return 0; } 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 0; } // 质量门:核心字段命中率。API 改字段名时会整片 undefined,必须拦截而非静默写盘。 // classifyStr 同样要查:它决定男频/女频分组和 --channel 筛选,字段一改全部书都掉进 // 「其他」、--channel male 直接筛成 0 条,写出一份「已采集:0 条」的假成功报告。 const CORE_FIELDS = [ { key: "name", label: "书名" }, { key: "classifyStr", label: "频道(classifyStr)" }, ]; for (const f of CORE_FIELDS) { const hit = allBooks.filter((b) => b && b[f.key]).length; if (hit / allBooks.length < 0.5) { console.error( `[heiyan] 采集失败:${allBooks.length} 条里仅 ${hit} 条有${f.label},疑似接口字段变动,已放弃写盘。` ); return 0; } } // 频道筛选 let filtered = allBooks; if (CHANNEL === "male" || CHANNEL === "female") { const want = CHANNEL === "male" ? "男频" : "女频"; filtered = allBooks.filter((b) => b.classifyStr === want); if (!filtered.length) { // 筛成 0 条不能算成功:把实际 classifyStr 分布打出来,区分「接口字段变了」 // 和「这个频道确实没作品」,而不是写一份看不出差别的空报告 const seen = {}; for (const b of allBooks) { const k = b && b.classifyStr ? b.classifyStr : "(空)"; seen[k] = (seen[k] || 0) + 1; } const dist = Object.keys(seen).map((k) => `${k}×${seen[k]}`).join("、"); console.error( `[heiyan] 采集失败:--channel ${CHANNEL} 筛选后 0 条(${allBooks.length} 条的 classifyStr 取值:${dist}),已放弃写盘。` ); console.error(` → 若确实没有${want}作品,去掉 --channel 重跑即可拿到全部书目。`); return 0; } } // 可选:逐本获取详情(标签等) 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); return 1; } if (require.main === module) { runCli(main, "黑岩采集"); } module.exports = { probePage, getToken, fetchBookList, fetchBookDetail, fmtWords, outputFilename, buildAndSave, };
-
-
SKILL.md 8.8 KB
--- name: story-short-scan version: 1.0.0 description: "短篇网文扫榜。分析知乎盐言、七猫、黑岩、点众等平台热门短篇数据,捕捉风口题材。触发方式:/story-short-scan、/短篇扫榜、「短篇什么火」「知乎故事排行」。" metadata: {"openclaw":{"source":"https://github.com/zenstory-ai/oh-story-claudecode"}} --- # story-short-scan:短篇网文扫榜 你是短篇网文市场分析师。你的任务是基于榜单样本识别短篇市场格局,并输出可执行的情绪方向、题材候选、风险阈值和验证动作。 **核心信念:短篇市场变化快,题材信号有效期短。** 扫榜报告必须标注样本日期、趋势可信度和下次重新扫榜的时间。 --- ## 核心哲学 ### 原则 1:短篇市场是情绪市场 短篇网文的核心是情绪交付。读者在短时间内完成一次情绪体验;扫榜要提取高频情绪、触发场景、情绪爆发点和读者愿意转发的点,而不是只记录题材名。 ### 原则 2:短篇的生命力在传播 短篇不像长篇靠追读赚钱。短篇靠的是单篇完读率和传播(分享、收藏、点赞)。完读率高 = 情绪拉扯到位;传播率高 = 有共鸣或反转让人想转发。 ### 原则 3:短篇风口来得快去得快 短篇题材信号可能在数周内失效。输出风口候选时必须给出有效期、饱和风险和下次复扫时间;未复扫前不得当作长期趋势。 --- ## 扫榜流程 ### Phase 1:确认平台和方向 问用户:**「你想看哪个平台?(知乎盐言/番茄短篇/七猫短篇/其他)有没有想写的类型方向?」** 关键判断: - 用户已有方向 → 针对该方向做深度扫榜 - 用户没有方向 → 做全榜概览 + 找趋势 - 用户想跨平台比较 → 做平台对比分析 --- ### Phase 2:确定数据来源 **扫榜需要真实数据支撑。** 根据当前环境选择数据来源: | 优先级 | 模式 | 说明 | 何时用 | |--------|------|------|--------| | 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,继续其他平台采集,不中断本轮数据采集。** - 黑岩专用:`--pages N`(每页 20 条)、`--detail`(逐本详情,含标签/简介,速度较慢)、`--channel male/female` - 点众专用:`--channel male/female/all` **文件命名**:`{平台}{类型}_{YYYYMMDD}.md`,例:`点众男频短篇_20260501.md` **用户提供操作指引:** - 请用户截图或复制粘贴榜单内容 - 如果用户提供链接,用 WebFetch 抓取页面内容 - 如果用户只提供故事名列表,直接进入分析 **内置知识操作指引:** - 加载 `references/real-market-data.md`(跨平台写作差异对照) - 明确标注:「以下分析基于历史趋势数据;未完成实时榜单校验前只能作为候选假设。」并列出需要复扫的平台页面。 --- ### Phase 3:数据分析 #### 知乎盐言故事分析维度 | 维度 | 看什么 | |---|---| | 热门榜单 | 当前最受关注的故事 | | 高赞故事 | 口碑最好的作品结构 | | 新作者上榜 | 非头部账号的题材选择与开篇模式 | | 付费转化率 | 哪些题材读者愿意付费 | | 标签分布 | 热门标签的变化趋势 | #### 通用分析维度 对每个平台提取: 1. **情绪类型分布**:当前哪种情绪拉扯最火(虐恋/反转/悬疑/治愈/打脸) 2. **题材热点**:具体什么设定/场景反复出现 3. **篇幅分布**:热门短篇集中在多少字 4. **开头模式**:热门短篇的第一段/第一句怎么写 5. **结尾类型**:HE(好结局)/BE(坏结局)/开放式 的比例 6. **标题模式**:热门短篇的命名规律 7. **人设模型**:反复出现的主角类型 --- ### Phase 4:输出扫榜报告 ``` # 短篇网文扫榜报告:{平台名称} ## 市场概况 - 扫榜时间:{日期} - 核心发现:{一句话总结} ## 情绪热度排行 | 排名 | 情绪类型 | 榜上数量 | 趋势 | 代表作 | |------|----------|----------|------|--------| | 1 | {类型} | {N篇} | ↑/→/↓ | {标题} | ## 题材热点 | 题材 | 热度 | 竞争程度 | 门槛 | 代表作 | |------|------|----------|------|--------| | {题材} | 高/中/低 | 激烈/一般/蓝海 | 高/中/低 | {标题} | ## 关键数据洞察 - 篇幅区间:热门短篇集中在 {X}-{Y} 字 - 开头模式:{高频开头模式} - 结尾偏好:{HE/BE/开放式的比例} - 标题特征:{命名规律} - 人设热词:{高频主角类型} ## 风口预警 - 🔥 正在爆发:{题材} — {依据} - ⚡ 即将起风:{题材} — {依据} - ⚠️ 即将饱和:{题材} — {依据} ## 值得写的方向 1. {方向 + 情绪拉扯方式 + 可行性} 2. {方向 + 情绪拉扯方式 + 可行性} 3. {方向 + 情绪拉扯方式 + 可行性} ## 一句话 {犀利总结} ``` --- ### Phase 5:选题匹配 根据扫榜结果,结合项目条件输出选题匹配: - 低复杂度候选:反转类、打脸类(结构清晰、验证成本低) - 高复杂度候选:悬疑类、虐恋类(技术壁垒高,需要伏笔、反转和情绪控制证据) - 优先候选:当前样本强信号 × 项目素材/能力约束可支撑的交叉点 **关键判断**: - 情绪拉扯力 > 题材创新力(短篇读者更看重情绪体验) - 开头 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](references/real-market-data.md) | **核心参考**:跨平台写作差异对照表、各平台简介公式速查、题材爆款公式速查表、各平台写作特征 | | [scripts/cdp-utils.js](scripts/cdp-utils.js) | CDP 公共工具函数(ab/sleep/evalJSON/safeStr/scrollLoad/getArg),各采集脚本共用 | | [scripts/dz-browse-scraper.js](scripts/dz-browse-scraper.js) | 点众短篇采集(男频/女频),按 bookId 聚合 anchor 解出书名/评分/简介/作品页(避免把 UI 文字或简介误当书名),带连通性自检+书名解析率质量门,配合 browser-cdp 使用 | | [scripts/heiyan-booklist-scraper.js](scripts/heiyan-booklist-scraper.js) | 黑岩书库列表采集,后端 API 模式(Bearer token),含字数/标签/价格/时间,支持 --detail 获取标签简介;区分 CDP 未连/未登录/超时/接口错误并带书名命中率质量门 | --- ## 语言 - 跟随用户的语言回复,用户用什么语言就用什么语言回复 - 中文回复遵循《中文文案排版指北》
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.