# stockselect **Repository Path**: PaoManX/stockselect ## Basic Information - **Project Name**: stockselect - **Description**: No description available - **Primary Language**: Python - **License**: AGPL-3.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-09-24 - **Last Updated**: 2026-10-10 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # stockselect —— 财联社 VIP 免费文章抓取 + 多 AI 结构化分析 一套把「财联社 VIP 栏目免费试读文章」抓下来,再用**网页 AI(豆包 / 通义 …)**逐条分析、 抽取结构化结论(要点 + 股票公司)的本地工具集。 全程通过 **Chrome CDP(远程调试协议)** 驱动你本机已登录的网页 AI,不依赖任何 AI 厂商 API Key,也不走逆向接口。 --- ## 一、整体数据流 ``` 财联社网页 │ (cls_vip_scraper.py,Playwright 拦截接口) ▼ result/cls_vip_free_articles_YYYYMMDD.json │ └──► data/vip_articles.db (sqlite) ← 文章权威源 │ (ai_analyze.py,一阶:把文章发给 AI 解读) ▼ (analysis 表 ← 分析权威源) ai_analysis.json (含 ai_reply + structured JSON) │ (历史 json 可用 migrate_analysis_to_db.py 回填进 db) │ (ai_extract.py,二阶:把一阶回复再发 AI 切要点/拆股票) ▼ ai_extracted.json (brief_points + stock_company,双向关联) 页面生成:web/build_workspace.py 只读 db(articles + analysis 表)+ ai_analysis_*.json 兜底 → workspace.json → web/build_page.py → index_YYYYMMDD.html ``` 每一「阶」都通过 `ai_controllers/` 里**对应 AI 的控制器**完成发送 / 复制 / 风控判定, 脚本本体只负责编排与解析。 --- ## 二、目录约定 ``` stockselect/ ├── cls_vip_scraper.py # ① 抓取:财联社免费试读 -> result/ JSON + sqlite ├── ai_analyze.py # ② 一阶分析:文章 -> 多 AI -> ai_analysis.json ├── ai_extract.py # ③ 二阶提取:一阶回复 -> ai_extracted.json ├── migrate_analysis_to_db.py # 一次性迁移:历史 ai_analysis_*.json -> db 的 analysis 表 ├── ai_config.json # 仅存放「多账号 CDP 端点编排」(不含 URL/选择器/提示词) ├── ai_controllers/ # 每个网页 AI 一个控制器(脚本即配置) │ ├── __init__.py # get_controller(name, overrides) / get_controller_by_url(url) │ ├── base.py # BaseAIController 抽象基类(通用发送/复制/风控逻辑) │ ├── engine.py # 通用编排:多账号 RR + 账号级封禁隔离 + 续跑 │ ├── doubao_controller.py# 豆包实现(已实测可用) │ ├── deepseek_controller.py # DeepSeek 实现(已实测可用;新版网页无复制按钮) │ ├── qwen_controller.py # 通义实现(已实测可用;回答侧需按 assistant 读容器) │ ├── probe_dom.py # DOM 探针:给新 AI 反推选择器,不靠猜 │ └── probe_model.py # 下拉探针:列出可选「模型 / 思考档位」并试切一个 ├── prompts/ # 提示词外置(多行易编辑,零依赖) │ ├── doubao.md # 豆包一阶提示词 │ ├── deepseek.md # DeepSeek 一阶提示词 │ ├── qwen.md # 通义一阶提示词(可各站不同) │ └── extract.md # 二阶提取指令(与 AI 无关,任务级) ├── result/ # 抓取结果 JSON(git 忽略;文件名带日期+自增 id 防覆盖) ├── logs/ # 运行日志(带时间戳,git 忽略) ├── data/ # vip_articles.db(sqlite,权威数据源,**已提交**)+ .gitkeep ├── web/ # 本地「财联社解读」单文件页面 │ ├── index_YYYYMMDD.html # 单文件成品:HTML + CSS + JS + 数据 全部内联,双击即开 │ ├── template.html # 页面骨架(含 __STYLE__ / __DATA__ / __APP__ 占位) │ ├── app.js # 财联社解读渲染 / 筛选 / 自选 / 主题逻辑 │ ├── styles.css # 样式(对标百汇行情 workspace 设计令牌) │ ├── build_workspace.py # 聚合 db 权威文章源 + AI 分析产物 → workspace.json(默认最近 30 天,--days 0 全量) │ ├── build_page.py # 把 template + css + js + workspace.json 合成 index_YYYYMMDD.html │ ├── workspace.json # 页面数据真源(生成物,git 忽略;替换它即刷新) │ └── data.js # workspace.json 的 JS 包装(file:// 回退用,git 忽略) └── LICENSE ``` > **为什么 ai_config.json 里只剩账号?** > 早期把 url / 选择器 / 提示词都塞进 JSON,结果「加一个 AI」要改配置还要改引擎。 > 现在改用「脚本即配置」:**每个 AI 一个 `_controller.py` 子类**,加 AI = 加一个文件, > 引擎零改动。 --- ## 三、依赖 ```bash pip install playwright # 脚本统一使用 Playwright 自带的 Chromium: playwright install chromium # WSL/Linux 若用自带 Chromium 还需: playwright install-deps ``` --- ## 四、前置:启动本机 Chrome(CDP) 网页 AI 需要「已登录」的浏览器。先手动为每个账号启动一个 Chrome 实例 (每台用独立 `--user-data-dir`,互不干扰): ```bash # 账号 1 "C:\Program Files\Google\Chrome\Application\chrome.exe" ^ --remote-debugging-port=9888 ^ --user-data-dir="D:\ChromeData\doubao1" # 账号 2(如有多账号稀释风控) "C:\Program Files\Google\Chrome\Application\chrome.exe" ^ --remote-debugging-port=9889 ^ --user-data-dir="D:\ChromeData\doubao2" ``` 打开后手动登录对应网页 AI(豆包 / 通义 …),保持窗口开着。 > ⚠️ **安全约束**:CDP **无鉴权**,谁连上就能以你已登录身份完全控制该 Chrome。 > 因此 `ai_config.json` 里的 `cdp_url` **只允许填本机回环 `127.0.0.1`**(如 `http://127.0.0.1:9888`)。 > 远程机器请先用 **SSH 隧道**把端口映射到本地再填 `127.0.0.1`,切勿直接填局域网 / 公网 IP 的裸端口。 > > **`cdp_url` 只填端点、没有 `--port` 参数**,两种写法都认: > - `http://127.0.0.1:9888`(最省事,playwright 会自动去取 ws 端点) > - `ws://127.0.0.1:9888/devtools/browser/`(完整 ws 端点,从 > `http://127.0.0.1:9888/json/version` 的 `webSocketDebuggerUrl` 字段直接抄) > > 想换 Chrome 实例 / 换通道,只改 `ai_config.json` 这一处即可,命令行不用带任何端口参数。 --- ## 五、用法 ### ① 抓取(cls_vip_scraper.py) ```bash python cls_vip_scraper.py --days 7 # 最近 7 天,结果落 result/ python cls_vip_scraper.py --start 2026-08-23 --end 2026-09-22 python cls_vip_scraper.py --cookie cookies.json # 带登录态,免费试读更完整 python cls_vip_scraper.py --explore # 只打印首屏接口样例,便于调试 ``` 主要参数: | 参数 | 说明 | |---|---| | `--days` / `--start` / `--end` | 时间窗口(默认最近 30 天);`--start`/`--end` 支持 `YYYY-MM-DD` 或 `YYYY-MM-DD HH:MM[:SS]`(精确到秒) | | `--cookie` | 浏览器导出的 cookies.json(可选,提升完整度) | | `--result-dir` / `--output` | 结果目录 / 基名;最终文件名自动追加 `YYYYMMDD`,同日重复加 `_n`,**绝不覆盖** | | `--log-dir` | 日志目录(默认 `logs/`,带时间戳) | | `--db` / `--no-db` | sqlite 路径(默认 `data/vip_articles.db`);默认开启,已存在按 id 跳过不覆盖 | | `--no-headless` | 用有头模式(默认 headless) | ### ② 一阶分析(ai_analyze.py) ```bash python ai_analyze.py --site doubao --num 3 # 取前 3 条,用豆包 python ai_analyze.py --site qwen --num 3 # 换通义 + 它的专用提示词 python ai_analyze.py --url https://www.doubao.com/chat/ # 按网址自动选站点 python ai_analyze.py --num 0 --output out.json # 跑全部 python ai_analyze.py --resume # 断点续跑 python ai_analyze.py --ask "请重点提示风险" # 在提示词后追加一句约束 ``` 主要参数: | 参数 | 说明 | |---|---| | `--site` / `--url` | 选控制器;缺省默认 `doubao` | | `--articles` | 输入 JSON(默认 `cls_vip_free_articles.json`) | | `--num` | 取前 N 条(默认 3;`0` 跑全部;配合 `--resume` 默认跑剩余) | | `--output` | 输出(默认 `ai_analysis.json`) | | ~~`--port`~~ | **已移除**:CDP 端点不再用端口参数表达,一律写在 `ai_config.json` 的 `accounts[].cdp_url`(写完整 URL,`http://` / `ws://` 皆可);站点未配 accounts 时用内置默认端点 `http://127.0.0.1:9888` | | `--account` | 指定只用某账号(否则对所有账号 round-robin) | | `--ask` | 可选追加约束 | | `--max-retry` / `--no-retry-nudge` | 单条解析失败重试次数(默认 3);重试是否加「请只输出纯 JSON」引导 | | `--resume` / `--force` | 续跑 / 强制从头 | | `--delay` / `--max-fail` | 每条间隔秒数(默认 1.0,礼貌限速);连续失败达此数自停(默认 5) | | `--block-keywords` / `--no-stop-on-block` | 自定义风控关键词;命中是否停整批(默认停) | ### ③ 二阶提取(ai_extract.py) ```bash python ai_extract.py # 默认读 ai_analysis.json -> ai_extracted.json python ai_extract.py --site qwen --num 3 python ai_extract.py --ask "只保留主板公司" # 可选追加约束 ``` 输入 `ai_analysis.json`(一阶,含 `ai_reply`),输出 `ai_extracted.json` (`brief_points` 按要点切开的 brief + `stock_company` 股票公司列表,两者双向关联)。 参数与 `ai_analyze.py` 基本一致(`--input` / `--output` / `--site` / `--num` …)。 --- ## 六、加一个新 AI(脚本即配置) 以接入「Kimi」为例,**只需新建一个文件,零改引擎**: 0. **先探真实 DOM,别靠猜**(占位选择器是通义那次踩过的坑): ```bash # 启动并登录专属 Chrome(独立 user-data-dir,端口不要撞车) # chrome.exe --remote-debugging-port=9888 --user-data-dir="D:\ChromeKimi" https://www.kimi.com # 探针固定连默认端点 http://127.0.0.1:9888(无 --port 参数,换实例请改 chrome 启动端口) python ai_controllers/probe_dom.py --url https://www.kimi.com \ --send "hi" --deep --out logs/probe_kimi.json ``` `--send` 会先发一条消息、等回答结束再 dump(**空聊天页没有回答和复制按钮,探不到东西**); `--deep` 额外列出页面全部按钮(含 svg 图标类)和消息行,专治「没有 aria-label 的纯图标按钮」。 从输出里挑出输入框、回答容器、复制按钮三条真实选择器。 若该站还能切模型,用 `probe_model.py` 把可选模型一个个列出来: ```bash python ai_controllers/probe_model.py --url https://chat.qwen.ai/ python ai_controllers/probe_model.py --pick "Qwen3.8-Max" # 顺便试切一次 # 通义这类还有「思考档位」下拉的,用 --target thinking: python ai_controllers/probe_model.py --target thinking --pick "Fast" ``` 1. 新建 `ai_controllers/kimi_controller.py`: ```python from .base import BaseAIController class KimiController(BaseAIController): name = "kimi" chat_url = "https://kimi.moonshot.cn/" editor_selector = "div[contenteditable='true']" # 用开发者工具核对 copy_button_selector = "button.copy-btn" # 用开发者工具核对 user_bubble_selector = "" # 区分用户/AI 气泡(可选) footer_markers = ["相关推荐"] # 回复尾部要截断的噪音 prompt_file = "prompts/kimi.md" # 该站专属提示词 ``` 2. 新建 `prompts/kimi.md`,写该站的一阶提示词。 3. 在 `ai_config.json` 的 `sites` 下登记账号(可选,不登记则退回内置默认端点): ```json "kimi": {"accounts": [{"name": "k1", "cdp_url": "http://127.0.0.1:9888"}]} ``` `cdp_url` 填完整端点就行,命令行没有 `--port` 了——换实例 / 换通道只改这里。 4. 直接 `python ai_analyze.py --site kimi` 即可。 > 站点名(`sites` 键 / 控制器模块名 / 子类 `name`)三者必须一致,引擎靠它动态加载。 > 抽象基类已内置发送 / 复制 / 等回复 / 风控判定逻辑,子类**通常只需声明属性**。 ### 选模型:控制器的 `model_name` 若某个 AI 的网页上能切换模型(如通义 `chat.qwen.ai`),在控制器里声明就行: ```python model_name = "Qwen3.8-Max" # 留空 "" = 不干涉,沿用网页默认模型 ``` 匹配规则:**完整名优先,退化为子串匹配**,所以填 `Qwen3.8-Max` 或 `3.8-Max` 都能选中; 填了下拉里没有的名字时不会崩,会打印可选清单并沿用当前模型。 实测下拉里通义有 3 个模型:`Qwen3.7-Plus`(默认)/ `Qwen3.8-Max` / `Qwen3.8-Omni-Flash`。 > **注意**:模型选中是「本页有效」的——`ai_analyze.py` 每条都会重新 `goto` 首页, > 模型会被重置为默认,所以**每条发送前都会重切一次**(多花约 5 秒)。别指望切一次全局生效。 ### 选思考档位:控制器的 `thinking` 通义的回答速度主要由「思考档位」决定(跟模型是两回事)。控制器里声明: ```python thinking = "Fast" # 可选 Auto / Thinking / Fast;留空 "" = 不干涉 ``` | 档位 | 行为 | 实测速度 | |---|---|---| | `Auto` | 按模型自己的策略决定(默认) | 中 | | `Thinking` | 深度思考,质量最好 | 约 3 分钟/条 | | `Fast` | 极速 | 比 Thinking 快一个量级,适合批量 | 同样是「完整名优先 + 子串兜底」匹配;填了下拉里没有的值不会崩,会提示可选清单。 > **注意**:与模型**相反**,思考档位是「跨页面保持」的(新开 tab / 重新 goto 都还在), > 所以只在档位不符时才切,不会每条白点一次。 ### 命令行临时覆盖 不想回头改 `.py` 时,这两个值都能从命令行临时改(只影响本次运行,优先于控制器变量): ```bash python ai_analyze.py --site qwen --thinking Fast python ai_analyze.py --site qwen --model Qwen3.8-Max ``` 运行结果会写进输出文件的 `meta.model_name` / `meta.thinking`,便于事后核对是哪档跑出来的。 ### 两种取回复的模式 | 属性 | 适用站点 | 说明 | |---|---|---| | `copy_button_selector` + `use_copy_button=True`(默认) | 豆包等 | 点「复制」按钮 → 读剪贴板,最可靠 | | `reply_container_selector` + `use_copy_button=False` | DeepSeek / 通义等 | 直接读回答容器的文本 | 第二种适合:**① 网页压根没有复制按钮**(DeepSeek 新版回答下方不再渲染操作栏,hover 也不注入); **② 侧栏/导航噪音太大**,全页 `innerText` 根本没法用。典型是通义 `chat.qwen.ai`——左侧历史会话列表 有几十条,不加区分地读 `body.innerText` 抓到的全是历史标题,所以必须指名回答容器 `div.qwen-chat-message.qwen-chat-message-assistant`。 **注意**:这类站点不要把 `copy_button_selector` 留空就算了——基类对空选择器已做防护 (`count_ai_copy_buttons` 返回 0、`click_ai_copy` 返回 `NO_BTN`),所以留空是安全的。 ### 头部噪音:`head_markers` 有些模型会在回答正文前渲染一行状态(通义思考模式是 `Thinking completed`)。用 `head_markers` 声明即可整行丢弃: ```python head_markers = ["Thinking completed"] ``` 判定规则是**整行相等**(不是子串包含),所以不会误伤正文里恰好含该词的行。尾部噪音用 `footer_markers` 按「首次出现位置截断」。 --- ## 七、数据库(data/vip_articles.db) 表 `articles`(按 `id` 主键去重,已存在则跳过,不覆盖更新): | 字段 | 说明 | |---|---| | `id` | 文章唯一 id(主键) | | `column_id` / `column_name` | 栏目 id / 名称(如 20015 盘中宝) | | `title` / `brief` | 标题 / 摘要 | | `ctime` / `time` | 原始时间戳 / 格式化时间 | | `paid` | 是否付费(抓取时已过滤付费,库里几乎全 0) | | `reading_num` / `unlock_num` | 阅读数 / 解锁数 | | `url` / `detail_url` | 跳转 / 详情链接 | | `related_stock` / `related_market` | 相关公司原始 `[{market,count}]` / 板块→数量聚合(JSON) | | `related_company_count` | 涉及上市公司总数 | | `fetched_at` | 抓取时间 | 表 `analysis`(AI 解读结果,与文章并列落库;主键 `article_id + site`): | 字段 | 说明 | |---|---| | `article_id` | 关联 `articles.id` | | `site` | 分析站点(doubao / deepseek / qwen) | | `parse_ok` | 结构化解析是否成功(1/0) | | `structured` | 解读正文(JSON 字符串),即前端 `ai_title` / `brief_list` / `companies` 等的来源 | | `analyzed_at` | 分析时间 | **入库状态**:`data/vip_articles.db` 是**唯一需要提交的数据文件**(`.gitignore` 里用白名单 `!data/vip_articles.db` 放行),因为 `articles` + `analysis` 两表是 `index_YYYYMMDD.html` 的唯一数据源—— 不入库的话,clone 下来的仓库只有代码、看不到任何数据。其余 `data/*.db` 仍默认忽略。 文件约 0.5 MB / 当前 138 篇文章,随抓取增长;建议每次数据有实质性新增时顺手提交一次 (`git add data/vip_articles.db && git commit -m "chore(data): 同步 db ..."`)。 > 只要 db 在、且跑过 `build_workspace.py` + `build_page.py`,**即使 `result/` 和 > `ai_analysis_*.json` 全空**(例如刚 clone 的仓库)也能重建出完整的 `index_YYYYMMDD.html`—— > 新克隆后想刷新页面,跑这两条命令即可,不需要重跑抓取或分析。 --- ## 八、续跑 / 风控 / 多账号说明 - **多账号 round-robin**:单次运行内对所有账号轮询,稀释单账号请求密度,降低被风控概率。 - **账号级封禁隔离**:某账号命中风控关键词(或连接断开)→ 临时移出本轮轮询,其余账号继续; 仅当全部账号都不可用才停整批。 - **断点续跑**:中途 Ctrl-C / 被封 / 断网,用 `--resume` 读取已有 output,跳过已成功条目,只跑剩余。 - **连续失败自停**:连续失败达 `--max-fail`(默认 5)自动暂停,避免空转。 --- ## 九、本地「财联社解读」单文件页面(web/) 把 `data/vip_articles.db`(权威文章源)和各 `ai_analysis_*.json` 聚合成一个**单 HTML 文件**(`web/index_YYYYMMDD.html`), 样式、脚本、数据全部内联,**双击就能打开**,不需要起服务、不需要任何依赖。 ```bash # 1. 生成数据(文章读 data/vip_articles.db 权威源,result/*.json 只补漏;分析读 ai_analysis_*.json) python web/build_workspace.py # 2. 合成单文件 index_YYYYMMDD.html python web/build_page.py # 3. 双击 web/index_YYYYMMDD.html 即可打开(file:// 也能正常显示) ``` 更新数据时,重新跑抓取 / 分析,再执行一次上面两条命令即可。 **文章来源(重要)**:`data/vip_articles.db` 是按 id 去重、只增不删的 sqlite 库,页面**恒等于 db 全量**, 与 `result/*.json` 是否清理完全解耦——增量轮询(看门狗)只往 db 里追加,历史数据永不丢失。 `result/cls_vip_free_articles_*.json` 降级为兜底:只补充 db 里缺失的 id(如某次 `--no-db` 跑出的漏网文章)。 `--no-db` 可强制走纯 result-json 的旧行为。 **页面体积控制(窗口)**:db 会随增量轮询无限增长,但页面默认只内联**最近 30 天**的文章 (`--days 30`,可改;`--days 0` 为全量历史)。另有 `--max-articles N` 条数上限双保险(超限保留最新 N 条)。 窗口只控制「进页面」的范围,**db 老数据永不删除**,被滑出的文章不参与页面渲染与公司汇总, 需要回看时 `--days 0` 重建即可。当前窗口信息记录在 `workspace.json` 的 `meta.window` 里。 功能: - **财联社解读**:信源卡片流;含栏目标签、AI 来源标签(豆包 / DeepSeek / 通义)、原文链接、 AI 解读要点、关联个股 chips、一键加入自选。 - **侧栏统计**:信源 / 已解读 / 个股 / 栏目 / 自选数。 - **筛选器**:时间范围(全部 / 近 24 小时 / 近 7 日)、栏目下拉、关键词搜索(标题 / 个股 / 代码)、只看已解读。 - **主题切换**:日间 / 夜间 / 跟随系统。 - **自选持久化**:个股 chip 上的「自选」按钮会把公司加入侧栏自选计数,数据存在浏览器 `localStorage`, 刷新后仍在;「一键加入自选」可整篇文章关联公司批量加入。 > `web/workspace.json`、`web/data.js` 是**生成物**,已写入 `.gitignore` 不提交; > `web/index_YYYYMMDD.html` 是由 `build_page.py` 生成的**单文件成品**,会随源码一起提交,方便直接双击查看。 > 仓库里保留 `template.html` / `styles.css` / `app.js` / `build_page.py` / `build_workspace.py` 作为源码。 --- ## 十、注意事项 - 豆包 / DeepSeek / 通义三个控制器均已实测可用。通义选择器(2026-09 实测): 输入框 `textarea.message-input-textarea`、回答容器 `div.qwen-chat-message.qwen-chat-message-assistant`、 用户侧 `div.qwen-chat-message.qwen-chat-message-user`,全是语义类名(无 hash),比豆包稳。 - **通义速度主要由思考档位决定**(见上文 `thinking`):默认 `Auto` 下模型先渲染一行 `Thinking completed` 再输出正文,实测单条约 3 分钟;切 `Fast` 后快一个量级(约 40 秒/条)。跑大批建议 `--thinking Fast`。 - 通义快档偶发「首次抓到半截 JSON」:`ai_analyze.py` 会先等 5 秒重抓一次,能救回一部分, 救不回的会自动重新提问一次(`--max-retry` 控制次数),最终结果不受影响。 - DeepSeek 页面**没有复制按钮**,`copy_button_selector` 故意留空、走 `reply_container_selector` 直读文本。 其回答容器类名里的 hash(如 `ds-markdown` 前的 `_27c9245`)会随发版变化,但 `ds-` 前缀和 `ds-assistant-message-main-content` 这类语义名是稳的,**升级 DeepSeek 后若抓不到回复,优先重跑探针核对这两处**。 - 抓取脚本统一用 Playwright 自带的 Chromium(headless 模式用临时配置,不干扰你正在用的普通 Chrome 窗口), 需先 `playwright install chromium`。 - CDP 无鉴权,务必只绑 `127.0.0.1`;远程机器先 SSH 隧道映射。 --- ## 十一、轮询看门狗(cls_watchdog.py) 把「每隔几分钟探一次 cls 是否有新 VIP → 有就自动抓 + 分析 + 重新生成单文件页面」做成常驻脚本。 纯标准库 + 调用上面三个脚本,**不改任何现有脚本语义**。 ```bash python cls_watchdog.py # 常驻,间隔 10 分钟,站点 doubao python cls_watchdog.py --interval 5 # 每 5 分钟一轮(也可 30 或任意分钟) python cls_watchdog.py --site qwen # 换 AI 站点(CDP 端点仍是 ai_config.json 里的) python cls_watchdog.py --once # 只跑一轮(供 Windows 计划任务 schtasks 调用) python cls_watchdog.py --no-ai # 只抓取 + 刷新页面,跳过 AI 分析 ``` 主要参数: | 参数 | 说明 | |---|---| | `--interval` | 轮询间隔(分钟,默认 10;可配 5/30/任意) | | `--lookback` | 首次 / 无历史状态时的回溯窗口(分钟,默认 60) | | `--site` | AI 站点(默认 doubao);CDP 端点无参数,看门狗探测 `http://127.0.0.1:9888` | | `--max-analyze-per-run` | 每轮最多分析条数(默认 20,积压多时自动分多轮补完) | | `--once` | 只跑一轮就退(对接 Windows 计划任务) | | `--no-ai` | 完全跳过 AI 分析,只抓 + 刷新页面 | 工作机制: - **增量判定**:每轮用 `cls_vip_scraper` 短窗抓取 `[上次扫描, 现在]` → 写 `data/vip_articles.db`(按 id 去重); 另从 db 反查「尚未被 AI 成功分析」的文章,保证只补分析新增 / 漏分析的,不重跑全量。 - **AI 分析的硬约束**:`ai_analyze` 依赖你**开着的已登录 Chrome(CDP)**,无法无人值守。 看门狗每轮先探测 `http://127.0.0.1:/json/version`:连得上才跑分析,连不上就跳过、 新文章先以「未解读」显示,等你开 Chrome 后下一轮自动补齐。任何一步失败都只记日志,不阻断下一轮。 - **刷新页面**:只要有「新增文章」或「本轮做了分析」,就重跑 `build_workspace.py` + `build_page.py`。 `build_workspace` 默认只取**最近 30 天**进页面(`--days 0` 可改全量),因此 index_YYYYMMDD.html 体积不随 db 无限增长。 - **断点不漏**:扫描时刻持久化到 `.watchdog_state.json`,看门狗重启也不会漏掉两次运行之间的窗口。 - **运行环境**:看门狗用 `sys.executable` 调子脚本,因此只要在**装了 Playwright 且 `playwright install chromium` 过** 的 Python 环境里跑它即可(就是平时跑 `cls_vip_scraper.py` 的那个 venv)。自动分析累积在 `ai_analysis_auto.json`, 会被 `build_workspace` 自动扫描合并。 > ⚠️ 自动分析是「尽力而为」:AI 那步要你人在、Chrome 登录态在线;长期不开 Chrome,文章会一直堆在「未解读」, > 直到你打开 Chrome 后看门狗在后续轮次补齐。