diff --git a/my-daily/QUICKSTART.md b/my-daily/QUICKSTART.md index 537545d..0b18565 100644 --- a/my-daily/QUICKSTART.md +++ b/my-daily/QUICKSTART.md @@ -27,17 +27,17 @@ python start.py ## 常用命令 -| 命令 | 说明 | -|------|------| -| `python start.py` | 一键启动:管线 + Web 归档 + 日志 | -| `python main.py check` | 校验 LLM 接口是否可用 | -| `python main.py test-fetch` | 测试抓取(仅拉取前 5 个源,验证流程) | +| 命令 | 说明 | +| --------------------------------------------------- | --------------------- | +| `python start.py` | 一键启动:管线 + Web 归档 + 日志 | +| `python main.py check` | 校验 LLM 接口是否可用 | +| `python main.py test-fetch` | 测试抓取(仅拉取前 5 个源,验证流程) | | `python main.py test-fetch --max 10 --lookback 300` | 测试抓取(前 10 个源,回溯 5 小时) | -| `python main.py fetch` | 单次完整抓取 → 评分 → 缓存 | -| `python main.py fetch --lookback 120` | 单次抓取(回溯 2 小时) | -| `python main.py push` | 单次推送(早报 / 默认模式) | -| `python main.py rss` | 仅打印 RSS Digest,不推送 | -| `python main.py loop` | 长跑模式(持续运行) | +| `python main.py fetch` | 单次完整抓取 → 评分 → 缓存 | +| `python main.py fetch --lookback 120` | 单次抓取(回溯 2 小时) | +| `python main.py push` | 单次推送(早报 / 默认模式) | +| `python main.py rss` | 仅打印 RSS Digest,不推送 | +| `python main.py loop` | 长跑模式(持续运行) | ## 首次运行建议 @@ -72,23 +72,74 @@ git push ## Docker 部署 -```bash -# 1. 准备配置 -mkdir -p docker/prompts docker/data docker/logs -cp .env docker/.env -cp config.json docker/config.json -cp -r prompts/* docker/prompts/ +### 打包镜像 -# 2. 构建并启动 -docker compose up -d --build +```powershell +# 在项目根目录执行(需先启动 Docker Desktop) +cd my-daily + +# 1. 构建镜像 +docker build -t my-daily-military-digest:latest . + +# 2. 导出为 tar 文件(用于上传到 NAS 等离线环境) +docker save -o military-digest.tar my-daily-military-digest:latest +``` + +### 本地测试运行 + +```powershell +# 1. 准备 docker 目录(首次) +mkdir -p docker\prompts docker\data docker\logs +cp .env docker\.env +cp config.json docker\config.json +cp prompts\*.md docker\prompts\ + +# 2. 启动容器 +docker compose up -d # 3. 查看日志 docker compose logs -f + +# 4. 停止容器 +docker compose down ``` +### NAS 部署 + +```bash +# 1. 上传 military-digest.tar 到 NAS,加载镜像 +docker load -i /path/to/military-digest.tar + +# 2. 准备好目录结构 +# /volume2/webdav/code/my-daily/ +# ├── docker/ +# │ ├── .env +# │ ├── config.json +# │ ├── prompts/ +# │ ├── data/ +# │ └── logs/ +# ├── resources/rss_feeds.opml (可选,未挂载则使用镜像内置) +# └── docker-compose.nas.yml + +# 3. 启动 +cd /volume2/webdav/code/my-daily +docker compose -f docker-compose.nas.yml up -d + +# 4. 查看日志 +docker compose -f docker-compose.nas.yml logs -f +``` + +> **推送类型**:系统有两种推送 — +> - 🚨 **即时推送**:Fetch 循环中检测到热点簇时自动推送(军事科技快讯) +> - 📰 **定时推送**:每天 08:00 推送(热点速览 + 每日精选 + 网摘图片) + ## 配置要点 -- **抓取间隔**:`.env` 中 `FETCH_INTERVAL_MINUTES=30`(每 30 分钟) -- **推送时间**:`.env` 中 `PUSH_CRON="0 8 * * *"`(每天 8:00) -- **API 切换**:修改 `.env` 中 `OPENAI_API_BASE` 和 `OPENAI_MODEL` -- 更多参数见 [CONFIG.md](./CONFIG.md) \ No newline at end of file +* **抓取间隔**:`.env` 中 `FETCH_INTERVAL_MINUTES=30`(每 30 分钟) + +* **推送时间**:`.env` 中 `PUSH_CRON="0 8 * * *"`(每天 8:00) + +* **API 切换**:修改 `.env` 中 `OPENAI_API_BASE` 和 `OPENAI_MODEL` + +* 更多参数见 [CONFIG.md](./CONFIG.md) + diff --git a/my-daily/docker/prompts/category_overview.md b/my-daily/docker/prompts/category_overview.md new file mode 100644 index 0000000..af74fd8 --- /dev/null +++ b/my-daily/docker/prompts/category_overview.md @@ -0,0 +1,16 @@ +# 军事科技分类总体介绍 + +你是军事科技领域的资深分析师。请根据以下分类的文章列表,为该分类撰写一段50字左右的总体情况介绍。 + +## 分类名称 +{category_name} + +## 文章列表 +{article_list} + +## 输出要求 +- 一句话概括该分类今日的核心动态 +- 字数严格控制在45-55字 +- 使用专业、客观、简洁的军事术语 +- 不要列举具体文章标题 +- 只输出介绍文字,不要前缀或后缀 diff --git a/my-daily/docker/prompts/classify_angle.md b/my-daily/docker/prompts/classify_angle.md new file mode 100644 index 0000000..724ab47 --- /dev/null +++ b/my-daily/docker/prompts/classify_angle.md @@ -0,0 +1,57 @@ +你是一名军事科技新闻编辑。请判断以下文章的报道角度。 + +## 角度分类(七选一 + 空值) + +必须从以下 8 个选项中选择 **恰好 1 个**: + +| 角度 | 适用场景 | 正确示例 | ⚠️ 不适用(反例) | +|------|---------|---------|-------------------| +| `政策发布` | 官方政策/法规/条例/命令/预算的**正式发布或生效** | 《国防白皮书》发布;某国批准新军费预算;出口管制新规 | 非官方渠道的解读/分析;内部工作会议;产品发布/广告 | +| `技术突破` | 新型装备/武器/技术的**实质进展**(首飞/列装/测试/验证) | 新一代战斗机首飞;新型雷达通过验收;高超音速导弹试射成功 | 常规装备维护升级;历史装备回顾;技术路线对比分析 | +| `情报信号` | 基于**开源情报(OSINT)**的发现:卫星图像识别/电子信号截获/部队部署热力图/新基地建设/异常调动 | 卫星图发现南海新雷达阵地;AIS数据显示航母编队异常动向;商业卫星揭示朝鲜导弹发射场扩建 | 官方已宣布的部署/演习;媒体报道的常规驻防;历史考古 | +| `战略分析` | 对局势/博弈/地缘的**深度研判**,文中必须有"分析/认为/意味着/判断/预测"等主观论断 | 台海局势走向研判;俄乌冲突对军贸格局影响 | 单纯事实报道;产品推广;采访/会见/任命(注意:无主观论断的纯事实归入其他角度) | +| `舆论反应` | 官方**回应/表态/谴责/抗议/澄清** | 外交部谴责某国军售;国防部回应南海传闻 | 网民个人观点;历史事件追认/纪念 | +| `冲突事件` | 正在发生或刚发生的**具体军事行动/冲突/演习/部署** | 军舰穿越台湾海峡;边境交火;联合军演启动 | 历史回顾;战略层面演习意义分析;政策文件威慑措辞 | +| `人物动态` | 以**人物或机构活动**为核心:任免、访晤、会议、纪念、文化活动、基层事迹 | 国防部长访问某国;军营开放日;烈士纪念仪式 | 以政策/装备/事件为核心、人物仅为信息源的报道 | +| `""`(空) | 明确**不属于军事/科技/时政**领域:广告、促销、纯生活内容、娱乐八卦 | 商品促销广告、周边挂件上新、纯情感散文 | — | + +## 决策顺序(从上到下,命中即止) + +``` +0. 是否明确不属于军事/科技/时政(广告/促销/纯娱乐)?→ 返回空字符串 "" +1. 文中是否有"谴责/驳斥/抗议/回应/声明"等官方表态?→ 舆论反应 +2. 是否在报道某场具体的战斗/冲突/演习/部署过程?→ 冲突事件 +3. 信息源是否为卫星图像/AIS/电子信号等OSINT数据?→ 情报信号 +4. 是否在报道某项技术/装备的首次突破/列装/测试/验证?→ 技术突破 +5. 是否在宣布某个政策/法规/条例/命令/预算的正式出台?→ 政策发布 +6. 文章核心是人物(任免/访问/活动)还是机构内部事务?→ 人物动态 +7. 文中是否有明确的"分析/认为/判断/预测/意味着"等主观论断?→ 战略分析 +8. 以上均不符合 → 根据内容归入最接近的角度(避免都进战略分析) +``` + +## 强制规则 + +1. 只看标题和正文前 500 字做判断 +2. **情报信号**必须基于开源情报(OSINT)数据源(卫星图/AIS/电子信号/热力图等),普通新闻配图不算 +3. **政策发布 vs 技术突破**判别标准:信息源头是官方公告 → 政策发布;是测试/列装/验证的实际进展 → 技术突破 +4. **战略分析**必须在文中找到至少 1 处明确的"分析/判断/预测/认为/意味着"等主观论断,否则不选 +5. 一条输出对应一条输入,链接丢失的输出项将被丢弃 + +## 输出格式 + +严格 JSON: + +```json +{{ + "items": [ + {{"link": "原文链接", "angle": "技术突破"}}, + {{"link": "原文链接2", "angle": "情报信号"}} + ] +}} +``` + +## 输入数据 + +```json +{entries_json} +``` diff --git a/my-daily/docker/prompts/cluster_insight.md b/my-daily/docker/prompts/cluster_insight.md new file mode 100644 index 0000000..b3d80df --- /dev/null +++ b/my-daily/docker/prompts/cluster_insight.md @@ -0,0 +1,73 @@ +你是一个资深军事新闻主编兼情报分析师。请对以下热点话题簇进行整合分析,同时输出精炼摘要和结构化洞察。 + +## 输入数据 + +以下是一个 JSON 对象,包含该话题簇的所有文章: + +```json +{cluster_json} +``` + +## Part 1: 摘要整合(新闻编辑视角) + +基于多源报道,生成一条 50-100 字的精炼摘要: + +- 只陈述核心事实,不做趋势判断 +- 不使用形容词(如"重大""深远""重要") +- 合并重复信息,提取最关键的 who/what/when/where +- 摘要应体现该簇最核心的军事/科技/时政价值点 + +## Part 2: 结构化洞察(情报分析师视角) + +### Metadata(10 字标题 + 20 字导语 + 3-5 要点) + +生成以下结构化信息: +- title: 10 字以内的标题(事实陈述,无修饰) +- lead: 20 字以内的导语(核心事实) +- highlights: 3-5 个关键要点(bullet points,纯事实) + +约束:只陈述事实,不用形容词。 + +### 洞察正文 + +基于多源报道,识别: +1. **事件脉络**:关键时间节点和进展。如果该话题是持续多日的事件,事件脉络段第一行必须写上 `[持续跟踪]`(单独一行),随后再写: + - 前情提要:**优先使用下方 `previous_summary` 字段**(上次推送的洞察摘要),用 1-2 句概括此前已知的关键进展。仅当 `previous_summary` 为空或不存在时,才从素材文章的早期报道中提取前情。禁止写"无显著前情" + - 最新突破:今天的最新进展(具体事实) +2. 多角度分析: + - **技术维度**:装备性能、技术水平、技术创新点及其在战术/战役层面的应用价值 + - **战略维度**:地缘政治影响、军力平衡变化、联盟关系调整 + - **信息战/认知战维度**:各方如何通过信息释放塑造战场叙事、舆论引导策略、叙事冲突 +3. 影响与展望:短期影响、中长期趋势、值得关注信号 + +约束: +- 禁止空泛评论(如"意义重大""影响深远") +- 每个观点必须有素材支撑 +- 使用具体名称(装备型号/政策名称/组织名称),不用"某些""部分"等模糊指代 +- 装备型号必须完整(如"F-35A"不能简写为"F35"),兵力/规模要有具体数字 + +## 输出格式 + +必须返回纯 JSON 对象: + +```json +{ + "summary": "50-100字精炼摘要,纯事实陈述", + + "metadata": { + "title": "10字以内的标题", + "lead": "20字以内的导语", + "highlights": ["要点1", "要点2", "要点3"] + }, + + "body": "#### **事件脉络**\n[持续跟踪]\n- 前情提要:...\n- 最新突破:...\n\n#### **多角度分析**\n- **技术维度**:...\n- **战略维度**:...\n- **信息战/认知战维度**:...\n\n#### **影响与展望**\n短期:...\n中期:...\n值得关注:..." + +注意:如果该话题是单日事件(所有报道在同一天),事件脉络段**不加** `[持续跟踪]` 标记。 +} +``` + +要求: +1. summary 必须 50-100 字(中文字符计数) +2. metadata.highlights 至少 2 条,至多 5 条 +3. body 必须包含三个段落(事件脉络/多角度分析/影响与展望) +4. 只返回 JSON 对象,不要添加额外说明 diff --git a/my-daily/docker/prompts/cluster_summary.md b/my-daily/docker/prompts/cluster_summary.md new file mode 100644 index 0000000..4952342 --- /dev/null +++ b/my-daily/docker/prompts/cluster_summary.md @@ -0,0 +1,20 @@ +你是一个军事新闻编辑。请将以下多源报道合并为一条 50-100 字的中文精炼摘要。 + +## 规则 + +1. 只陈述核心事实(who/what/when/where),不做趋势判断 +2. 不使用形容词(如"重大""深远""重要""显著") +3. 不使用模糊指代(如"某些""相关""有关"),必须用具体名称 +4. 合并重复信息,提取最关键的进展 +5. 如果同一事件有多个角度,用 1-2 句话概括主要维度 +6. 装备型号必须完整(如"歼-35A"),地点精确到城市/基地级别 + +## 输入 + +```json +{cluster_json} +``` + +## 输出 + +只返回 50-100 字的中文摘要文本,不要 JSON 包裹,不要额外说明。 diff --git a/my-daily/docker/prompts/digest.md b/my-daily/docker/prompts/digest.md new file mode 100644 index 0000000..b8d189b --- /dev/null +++ b/my-daily/docker/prompts/digest.md @@ -0,0 +1,29 @@ +你是一位顶级的军事科技观察家与日报主编。你的任务是将今天杂乱无章、来源各异的信息碎片,熔炼、重组成一篇结构清晰、主次分明、洞察深刻的《军事科技每日精选》。 + +## 核心排版与整合规则: +1. **新旧剥离与进展追踪**:对比今天的新信息与历史上下文。如果今天的信息是历史事件的延续,请将标题标记为`[持续跟踪]`,并在正文中清晰划分"前情提要"与"最新突破"。 +2. **精准的事件级融合**:把讨论同一具体事件的多方报道合并为一条新闻,提炼全貌。不要把两个毫不相干的事件强行合并。 +3. **精选原则**:只挑选真正有价值的4-10个核心事件。宁缺勿滥。绝对剔除:KOL个人动态、企业公关软文、纯情绪发泄、未经验证的小道消息。 +4. **结构化呈现**:每个事件采用"一句核心亮点 + 展开的无序列表"结构。 +5. **客观专业**:用简练的中文,像分析师一样指出事件的行业意义。保留所有相关的原文链接。 +6. **避免风格趋同**:每天的前言导读必须从今日素材本身的具体事实出发,严禁评价性措辞/总结性套话/宏大叙事框架。 +7. **正文不要写开头引言**:正文请直接从`### 1️⃣ ...`开始。 + +## 输出格式(严格按以下结构输出,先frontmatter后正文): +frontmatter字段要求: +- `title`: 提炼1-3个核心看点制作短标题,8-30字,核心事实陈述语气。 +- `lead`: 60-100字前言导读,只陈述今日具体发生的事实。 +- `highlights`: 2-3条最值得关注的事件清单,每条15-30字,纯事实陈述。 + +正文要求:直接从`### 1️⃣ ...`开始,不要前言段。 + +## 待处理的碎片化信息(共{count}条): +```json +{entries} +``` + +### 近期已推送内容(仅供查重,严禁模仿): +{recent_push_context} + +### 近几天已处理过的碎片化信息(供洞察参考): +{context} diff --git a/my-daily/docker/prompts/extract_entities.md b/my-daily/docker/prompts/extract_entities.md new file mode 100644 index 0000000..df8ffb3 --- /dev/null +++ b/my-daily/docker/prompts/extract_entities.md @@ -0,0 +1,49 @@ +你是一名军事科技情报分析师。请从以下文章中提取关键实体。 + +## 实体类型定义 + +提取以下 5 类实体,每类最多 5 个: + +| 类型 | 说明 | 军事/科技示例 | 政策/福利/机构示例 | +|------|------|-------------|------------------| +| `person` | 人物/角色 | 国防部长、马斯克、普京 | 退役军人、军属、烈士 | +| `org` | 组织/机构/军种 | 北约、美国海军、中国航天科工 | 军队幼儿园、退役军人事务部、军委后勤保障部 | +| `equipment` | 装备/武器系统/技术 | 歼-35A、东风-21D、星链 | — | +| `event` | 事件/行动/演习/政策 | 环太平洋军演、台海巡航 | 军娃优待政策、教育优待、住房保障、征兵条例修订 | +| `location` | 地域/战区/海域 | 南海、波斯湾、关岛 | — | + +## 提取原则 + +1. 只提取文中明确出现的实体,不推测 +2. 实体名称尽量具体("军队幼儿园"优于"幼儿园","退役军人事务部"优于"事务部") +3. 无该类实体时返回空数组 `[]` +4. 同一实体多次出现只取一次 +5. 排除过于宽泛的概念("武器""军队""国家""政府"),但**带限定词的宽泛概念可以接受**("军队幼儿园""某国政府"可以,"军队""政府"不行) +6. 政策内容也应提取——发布机构归入 `org`,政策内容归入 `event` + +## 输出格式 + +严格输出 JSON,与输入一一对应: + +```json +{{ + "items": [ + {{ + "link": "原文链接", + "entities": {{ + "person": ["实体1", "实体2"], + "org": ["实体1"], + "equipment": [], + "event": ["实体1"], + "location": ["实体1", "实体2"] + }} + }} + ] +}} +``` + +## 输入数据 + +```json +{entries_json} +``` diff --git a/my-daily/docker/prompts/immediate_push.md b/my-daily/docker/prompts/immediate_push.md new file mode 100644 index 0000000..0911342 --- /dev/null +++ b/my-daily/docker/prompts/immediate_push.md @@ -0,0 +1,19 @@ +你是一位顶级的军事科技观察家与专业的军事新闻记者。你需要将最新发生的高分重磅消息整理成即时推送快讯。 + +## 任务要求: +1. **严格查重与阻断机制**:仔细对比原始数据与【过去已推送历史】。如果事件已经推送过且无重大新进展,请直接抛弃。如果所有输入的事件都被判定为重复,你必须且只能输出[NO_NEW_CONTENT]。 +2. **专业聚合**:如果输入数据中有多条讨论同一个事件,请将它们融合为一条快讯。 +3. **专业视角**:用2-3条无序列表项陈述核心事实(What);列表之后另起一段给出1-2句为什么值得关注(Why it matters)的洞察分析,应涉及对地区安全架构/军力平衡/技术格局的影响。 +4. **语气风格**:客观、准确、克制。多用短句,重点内容加粗。避免空洞的行业宏大叙事和未来预测。 +5. **军事专业性**:装备型号必须写完整(如"F-35A"不简写"F35"、"阿利·伯克级驱逐舰"不简写"伯克级"),兵力/射程/吨位等使用具体数字。 + +## 输出格式: +{output_format} + +## 过去N天已推送事件清单(仅供查重,严禁模仿): +{recent_push_context} + +## 本次需要推送的原始数据: +```json +{entries} +``` diff --git a/my-daily/docker/prompts/precise_heat.md b/my-daily/docker/prompts/precise_heat.md new file mode 100644 index 0000000..45e714d --- /dev/null +++ b/my-daily/docker/prompts/precise_heat.md @@ -0,0 +1,60 @@ +你是一个资深的军事新闻主编。请对以下热点话题簇进行精评分(0-100)。 + +## 评分维度 + +请从以下四个维度综合打分: + +1. **军事价值(40分)**:是否涉及装备列装/技术突破/实战行动/战略部署/军事时政 + - 装备首次曝光/列装/实战验证 = 35-40 + - 技术验证/测试进展/军控谈判/重大制裁 = 25-35 + - 演习/训练/常规动态/外交斡旋 = 15-25 + - 非军事主题 = 0-15 + - 注意:芯片出口管制、AI军事化政策、太空军备竞赛等交叉领域属于军事科技/军事时政,不应归入"非军事" + +2. **时效性(25分)**:是否为最新进展或首次报道 + - 24h内的突发/首次报道 = 20-25 + - 近期进展的跟进报道 = 15-20 + - 回顾性/综述性内容 = 5-15 + +3. **信息密度(20分)**:是否包含具体数据/型号/地点/时间 + - 含具体型号+数量+地点+时间 = 15-20 + - 含部分具体信息 = 10-15 + - 泛泛而谈/观点评论 = 5-10 + - 加分项:含卫星图像/电子信号数据/OSINT分析/部署热力图等情报级信息 +3 + +4. **传播广度(15分)**:多家媒体、多个角度报道的热度 + - 5+源或新华社等国家级通讯社通稿 × 3+角度 = 12-15 + - 3-4源 或 2角度 = 8-12 + - 单一来源 = 3-8 + - 注意:中文军事新闻中,新华社/央视/国防部发布的一手通稿视为等效多源,不因"单源"而扣分 + +## 输入数据 + +以下是一个 JSON 数组,包含热点话题簇的摘要信息: + +```json +{clusters_json} +``` + +## 输出要求 + +必须返回纯 JSON 对象,格式如下: + +```json +{ + "items": [ + { + "cluster_id": 0, + "precise_score": 92, + "brief": "歼-35A首次公开确认列装,官方首发,多源多角度报道" + } + ] +} +``` + +要求: +1. items 数组长度必须与输入相同 +2. cluster_id 必须与输入一一对应 +3. precise_score 为 0-100 整数 +4. brief 为 25 字以内的评分依据简述 +5. 只返回 JSON 对象,不要添加额外说明 diff --git a/my-daily/docker/prompts/score_batch.md b/my-daily/docker/prompts/score_batch.md new file mode 100644 index 0000000..2ab0791 --- /dev/null +++ b/my-daily/docker/prompts/score_batch.md @@ -0,0 +1,50 @@ +你是一个专业且严苛的军事科技新闻主编。请对抓取到的碎片化信息进行过滤、评分和信息提取。 + +## 任务与评分标准 +请根据以下标准为每条信息打分(0-100)。 + +**核心约束(先判这三条,再进入分档)**: +1. 90+ **必须同时满足**:(a) 主题与时政/军事/科技强相关;(b) 来源为当事方官方账号或权威防务专业媒体(Defense News / The War Zone / Janes / 新华社等,非低质KOL、非纯营销号);(c) 属于首发或第一时间跟进 +2. 非时政/军事/科技主题(纯娱乐八卦、生活琐事、纯体育等)无论多重大、即使来自官方,**上限79分** +3. 时政/军事/科技新闻若来源是低质KOL(纯转述无增量信息、个人自媒体、无专业背景的营销号),**上限89分**。有深度分析、独家信息或专业背景(退役军官/OSINT分析师/行业研究员)的KOL不在此限。 + +**分档**: +- 【90-100分】军事/科技领域 + 官方或权威防务专业媒体首发 + 里程碑级装备/政策/事件。军事时政重磅事件(军控条约签署、重大制裁、香格里拉级别对话会)也在此档。 +- 【80-89分】重要军事科技进展、深度技术分析、军事时政重要进展;或重磅新闻但通过普通媒体转述。 +- 【70-79分】实用装备分析、技术教程、行业报告;**非目标领域**的重磅新闻;低质KOL转述的军事内容。 +- 【60-69分】二手信息、一般性新闻、小道消息。 +- 【<60分】低价值内容:纯情绪宣泄、无营养评价、广告。 + +## 输出要求 +必须返回纯JSON对象,顶层包含`items`数组字段,数组中每个对象包含: +- `link`: 原文链接(必须保留原样) +- `score`: 整数评分,JSON数字类型 +- `tags`: 字符串数组,数量1-3个,每个标签2-16个字符。必须是新闻中具体的关键词(装备型号/政策名称/技术特性/关键人物/地缘事件),禁止空泛分类标签。 + - ✅ 好示例:`["歼-35A","舰载战斗机","隐身性能"]`、`["AI芯片出口限制","Rubin架构"]` + - ❌ 坏示例:`["军事","新闻","热点"]` +- `summary`: 一句话客观摘要(提取核心事实,50字内) + +## 输出格式(严格只输出JSON对象): +{ + "items": [ + { + "link": "https://example.com/article1", + "score": 95, + "tags": ["歼-35A", "舰载战斗机", "隐身性能"], + "summary": "空军官方确认歼-35A已列装首批作战部队。" + } + ] +} + +## 重要提示 +1. items数组长度必须与输入相同 +2. link字段必须与输入一一对应 +3. 只返回JSON对象,不要添加任何额外说明文字 + +## 输入数据 +以下是一个JSON数组,包含抓取到的内容: +每个对象包含:link, title, source, published, content + +```json +{entries_json} +``` diff --git a/my-daily/docker/prompts/webzine.md b/my-daily/docker/prompts/webzine.md new file mode 100644 index 0000000..b0f373a --- /dev/null +++ b/my-daily/docker/prompts/webzine.md @@ -0,0 +1,55 @@ +# 军事科技网摘生成 + +你是一名资深军事编辑,请你严格按照《参考消息》官方新闻报道格式、标题风格,对我提供的新闻内容进行改写,严格遵守以下所有规则: + +## 文章信息 +- 标题:{title} +- 来源:{source} +- 发布时间:{published} +- AI摘要:{summary} +- 原文内容:{content} + +## 标题要求 +- 简洁、客观、中性、信息密度高 +- 结构:主体 + 事件 + 核心态势 +- 不抒情、不夸张、不用网络用语 + +## 价值点要求 +- 价值点严格 35~50 字(含标点) +- 句式结构:事件 - 影响/后果 - 值得关注 +- 凝练、客观、不发散 + +## 正文要求 +- 开头第一句必须加:据 XXX 报道 +- 如果新闻来源是微信公众号,开头第一句必须加:据微信公众号 XXX +- 正文风格:客观、平实、严谨、书面化,类似外电编译稿 +- 只保留核心信息(时间、地点、人物、事件、内容、前景/影响),全文聚焦文章最核心的亮点,其他次要内容简要提及或不介绍 +- 正文中可适当使用编号概述,如(1)(2)(3)…,不超过4个,每个编号后的冒号前为2~4字短语,如(1)具体行动:...(2)实现目标:...等 +- 装备型号必须完整准确(如"F-35A"不简写为"F35"),兵力规模使用具体数字 +- 全文一段到底,不分段 +- 字数强制要求:正文字数严格控制在 250~350 字(含标点),不许超、不许少 + +## 输出格式 + +严格输出 JSON,包含六个字段: + +```json +{ + "title": "网摘标题", + "value": "价值点", + "body": "正文", + "source": "新华社", + "original_title": "原文原标题", + "published": "2026-06-02T10:00:00" +} +``` + +- source: 来源名称 +- original_title: 未经改写的原标题 +- published: 发布时间 + +只输出 JSON,不要任何前缀或后缀文字。 + +## 注意事项 +- 标题与价值点、正文中的"核心亮点"必须一致——标题突出什么,价值点和正文就从什么角度切入组织 +- 不遗漏新闻中值得关注的意外事件或副作用(如事故、失误),以客观笔法简要带过 diff --git a/my-daily/main.py b/my-daily/main.py index 6e55544..e0b2683 100644 --- a/my-daily/main.py +++ b/my-daily/main.py @@ -35,6 +35,7 @@ from src.sections.webzine.section import generate_webzine_batch from src.storage import ( get_cluster_file, get_digest_file, + get_fetch_file, save_push_file, ) diff --git a/my-daily/src/llm.py b/my-daily/src/llm.py index 6caecaa..0491fa6 100644 --- a/my-daily/src/llm.py +++ b/my-daily/src/llm.py @@ -129,6 +129,7 @@ def _try_fix_truncated_json(text: str) -> str: def _parse_score_response(response: str) -> List[Dict]: text = response.strip() + # 移除 markdown 代码块标记 if text.startswith("```json"): text = text[7:] elif text.startswith("```"): @@ -138,24 +139,39 @@ def _parse_score_response(response: str) -> List[Dict]: text = text.strip() parsed = None + # 尝试 1: 直接解析 try: parsed = json.loads(text) except json.JSONDecodeError: - for pattern in (r"\{.*\}", r"\[.*\]"): + pass + + # 尝试 2: 修复截断 + 修复常见 JSON 语法错误 + if parsed is None: + fixed = _try_fix_truncated_json(text) + try: + parsed = json.loads(fixed) + except json.JSONDecodeError: + pass + + # 尝试 3: 正则提取整个 JSON 对象 + if parsed is None: + for pattern in (r"\{[^{}]*\"items\"\s*:\s*\[.*?\]\s*\}", r"\{.*\}", r"\[.*\]"): match = re.search(pattern, text, re.DOTALL) if match: try: parsed = json.loads(match.group()) break except json.JSONDecodeError: - continue + # 尝试修复后再解析 + fixed_match = _try_fix_truncated_json(match.group()) + try: + parsed = json.loads(fixed_match) + break + except json.JSONDecodeError: + continue if parsed is None: - fixed = _try_fix_truncated_json(text) - try: - parsed = json.loads(fixed) - except json.JSONDecodeError: - raise ValueError(f"无法从响应中解析JSON: {response[:200]}...") + raise ValueError(f"无法从响应中解析JSON: {response[:300]}...") if isinstance(parsed, list): return parsed @@ -166,7 +182,7 @@ def _parse_score_response(response: str) -> List[Dict]: list_values = [v for v in parsed.values() if isinstance(v, list)] if len(list_values) == 1: return list_values[0] - raise ValueError(f"无法从响应中提取评分数组: {response[:200]}...") + raise ValueError(f"无法从响应中提取评分数组: {response[:300]}...") def _split_entries_for_batch(entries: List[Dict], max_prompt_chars: int = 10000) -> List[List[Dict]]: @@ -227,16 +243,29 @@ def _reconcile_batch_results(entries: List[Dict], results: List[Dict], batch_ind return matched_results, errors -async def _score_single_batch(entries: List[Dict], config: Dict, batch_index: int = 0) -> Tuple[List[Dict], List[str]]: +async def _score_single_batch(entries: List[Dict], config: Dict, batch_index: int = 0, + retry: bool = True) -> Tuple[List[Dict], List[str]]: prompt_path = config.get("prompts", {}).get("score_batch", "prompts/score_batch.md") prompt = _build_batch_prompt(entries, prompt_path) - try: - response = await call_llm(prompt, config, response_format={"type": "json_object"}) + + async def _try_score(): + response = await call_llm(prompt, config, max_tokens=40960) results = _parse_score_response(response) if not isinstance(results, list): raise ValueError(f"LLM返回的不是数组: {type(results)}") return _reconcile_batch_results(entries, results, batch_index) + + try: + return await _try_score() except Exception as e: + if retry: + print(f" 🔄 批次{batch_index + 1} 首次失败,重试中... ({e})") + try: + return await _try_score() + except Exception as retry_e: + error_message = f"批次{batch_index + 1} 评分失败(重试后): {retry_e}" + print(f" ⚠️ {error_message}") + return [], [error_message] error_message = f"批次{batch_index + 1} 评分失败: {e}" print(f" ⚠️ {error_message}") return [], [error_message]