Claude Skill

story-short-scan

短篇网文扫榜。分析知乎盐言、七猫、黑岩、点众等平台热门短篇数据,捕捉风口题材。触发方式:/story-short-scan、/短篇扫榜、「短篇什么火」「知乎故事排行」。

LLM Mart · 0 points · 18 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download zenstory-ai-oh-story-claudecode-skills_story-short-scan-d1f8858.zip · 20 KB
Part of zenstory-ai/oh-story-claudecode — 26 skills

Install

skills CLI npx skills add https://github.com/zenstory-ai/oh-story-claudecode/tree/main/skills/story-short-scan
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install zenstory-ai-oh-story-claudecode@llmmart
Git 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:数据分析

知乎盐言故事分析维度

维度 看什么
热门榜单 当前最受关注的故事
高赞故事 口碑最好的作品结构
新作者上榜 非头部账号的题材选择与开篇模式
付费转化率 哪些题材读者愿意付费
标签分布 热门标签的变化趋势

通用分析维度

对每个平台提取:

  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 核心参考:跨平台写作差异对照表、各平台简介公式速查、题材爆款公式速查表、各平台写作特征
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.

No comments yet.

Reviews (0)

No reviews yet.

Related