Author SHA1 Message Date
kai 450ecebcff v0.10: add automated Codex orchestration and deployment check 2026-04-24 13:38:22 +08:00
kai 77df1838b1 v0.10: add native Codex adapter for deep research 2026-04-24 10:48:14 +08:00
kai c93fce5078 v0.9: parallelize phase4 and add model/search playbooks 2026-04-24 10:19:48 +08:00
kai c444007f04 v0.8: Serper 集成 + H1 章节标题双行居中 + 反方证据观点化 + 术语核查命令
新增:Google 系检索(SerpAPI → Serper.dev)

- scripts/lib/serper_client.py:封装 serper.dev 的 Google Search / Scholar / News / Patents
- 专利检索用 site:patents.google.com 技巧,serper.dev 没专用 endpoint 但效果很好
- Scholar 带引用数、年份、期刊信息,便于权威信源识别
- News 支持 time_range(d/w/m/y)时效性过滤

- scripts/lib/search_client.py 扩展为多路由门面:
  - search() 通用:Exa → Tavily
  - patents() 专利:Serper(Google Patents)→ 通用搜索 + site: 兜底
  - scholar() 论文:Serper Scholar → 通用搜索兜底
  - news() 新闻:Serper News → 通用搜索兜底
- 所有 httpx 客户端 trust_env=False,绕过系统 socks5 代理(v0.6 修过的 TLS EOF)

- .opencode/skills/search-strategy/SKILL.md §三重写:按查询类型路由,明确何时用哪个 API

H1 章节标题:两行居中 + 装饰横线

- 新增 ParagraphStyle: h1-chapter-num / h1-chapter-title
- 新增 parse_chapter_title() 支持中文/阿拉伯/混合空格章号:
  "第一章" / "第 9 章" / "第6章" / "Chapter 1" 全覆盖
- 分隔符支持: em dash — / en dash – / - / : / :
- 新增 build_chapter_header():章号小字居中 + 章名大字深蓝居中 + HRFlowable 3cm 装饰线
- 只对正文章节(_title_kind == "chapter")启用;前置件(免责声明/执行摘要/术语表/目录)
  仍用单行 h1 样式

反方证据段规范化(用户反馈 v0.7 问题 #5)

- skill:evidence-table 新增 §"正文中反方证据段落的写作规范":
  - 禁止机械标题"反驳证据" / "Counter-Evidence" / "反方观点"
  - 必须观点化,包含具体判断(如"另一种声音:管线虚胖还是真实进展?")
  - 用 H2 或 H3,禁止加粗段冒充标题
  - 给出段落结构模板(1-2 句过渡 → 列表型反方论点 → 整合判断)
- dr-analyst.md Hard Rules #3 改为引用该规范

术语表事实核查前置(新 command /dr-glossary)

- 新增 .opencode/commands/dr-glossary.md,支持 --from phase1|phase2|phase4 三个时机
- Phase 1 末 / Phase 2 初:从 framework.md 抽取专有名词种子表,在 dr-analyst 起草前
  预先核查公司名/产品名/技术名拼写,避免编造错误(Mabwell → Maywavee 这类)
- Phase 4:维持当前用法,对 glossary.json 全量核查

实测:dual-target-rnai-pipeline-2026 重生 PDF 55 页,所有 10 章标题双行居中正确渲染
(第一章/第二章/... 第十章 / 第 6 章 / 第 9 章 多种形式都识别)。
2026-04-22 17:04:05 +08:00
kai 09f681beb5 v0.7.2: 前置件排版重构 + emoji 禁令 + 引文核查
用户反馈 7 个 bug 修复:

1. 禁止 LLM 使用 emoji(全链路)
   - scripts/prompts/translate_system.txt 增加规则 12
   - scripts/prompts/polish_system.txt 增加规则 7
   - .opencode/agents/dr-analyst.md Hard Rules 增加第 10 条(同时把 prompt 自身的  改为 MUST / MUST NOT)
   - .opencode/agents/dr-editor-in-chief.md 禁止事项加入 emoji 条款
   - .opencode/skills/output-hygiene/SKILL.md 新增 §J emoji 强制禁用

2. 术语表位置错误(应在目录之后)
   重构 build_body 为两阶段:
   (a) 扫描所有前置件(第一个正文 H1 前的所有 H1/H2),按 title_kind 分组收集
   (b) 按固定顺序渲染:免责声明 → 执行摘要 → 目录 → 术语表 → 正文 → 参考文献
   无论 Markdown 原文顺序如何,排版都一致。

3. 执行摘要/术语表提升为一级标题 + 分页空页 bug
   统一所有独立章节(disclaimer/executive_summary/toc/glossary/references)用 h1 样式,
   章节前 PageBreak;但第一个独立章节不 PageBreak(封面后已换页,避免空白)。
   去掉 build_toc 内部末尾 PageBreak(原双 PageBreak 夹出空白页)。

4. 参考文献分页
   已作为独立章节自动分页。

5. 附录章节自动删除
   _title_kind 识别 "appendix" / "version_history" / "abstract" 全部跳过。
   正文中若写了这些章节,模板直接丢弃。

6. 信源完整性核查
   新增 scripts/check_citations.py:
   - 孤立引用(正文有 sources 无)检测
   - 孤岛信源(sources 有正文无)检测
   - emoji 扫描
   - 实测发现项目中 61 条孤立引用(dr-analyst 编造的占位符)+ 5 条孤岛信源

7. git commit message 中文转义 bug
   之前 commit 用 shell 双引号 + 反斜杠导致 \uXXXX 字面保留。
   本 commit 用 heredoc 保证中文以 UTF-8 直接写入。
   已 push 的历史不改,之后都用本 commit 的写法。

PDF 验证结果:55 页,0 空白页。
章节起始页:封面(1) - 免责声明(2) - 执行摘要(3) - 目录(5) - 术语表(7) -
第一章(12) - 第十章(48) - 参考文献(52)。
2026-04-22 16:31:01 +08:00
72 changed files with 5246 additions and 244 deletions
+166
View File
@@ -0,0 +1,166 @@
---
name: citation-manager
description: 引用管理规范。规定 [src_xxx] 编号体系、sources.jsonl 字段标准、跨章节去重规则、参考文献列表生成格式(GB/T 7714)。
---
# 引用管理规范
## 一、src_id 编号规则
### 全局唯一编号
`src_id` 在整个项目内**全局唯一**,格式:`src_XXX`3 位数字,不足补零)。
- 由 dr-analyst 在首次引用时分配
- 按发现顺序递增:`src_001`, `src_002`, ...
- 跨章节引用同一来源时,使用**相同 ID**
### 分配流程
1. dr-analyst 发现一条新信源
2. 读取 `projects/<slug>/phase2/sources.jsonl`,找当前最大 ID
3. 分配下一个 ID(如当前最大为 `src_023`,下一个为 `src_024`
4. 写入 sources.jsonl
### 草稿中的引用格式
行内引用:`数据或观点 [src_042]`
多来源:`数据或观点 [src_042][src_058]`
---
## 二、sources.jsonl 字段标准
每行一个 JSON 对象(JSONL 格式):
```jsonl
{
"id": "src_001",
"tier": 1,
"score": 8.5,
"type": "journal",
"title": "论文标题",
"authors": ["Zhang S", "Li M"],
"year": 2024,
"venue": "Nature Medicine",
"impact_factor": 58.7,
"url": "https://doi.org/10.1038/...",
"doi": "10.1038/...",
"accessed_at": "2026-04-20",
"abstract": "2-3句摘要",
"key_data": {
"sample_size": 1200,
"primary_endpoint": "OS 改善 23%"
},
"used_in": ["ch02", "ch05.sec3"],
"conflict_of_interest": null,
"notes": "RCT 主要终点数据在 Table 2"
}
```
### type 字段枚举值
| 值 | 含义 |
|---|---|
| `journal` | 期刊论文(含综述) |
| `trial` | 临床试验(ClinicalTrials.gov 注册信息) |
| `regulatory` | 监管机构公告/审批文件 |
| `patent` | 专利文件 |
| `report` | 咨询/行业报告 |
| `disclosure` | 上市公司披露(年报/招股书/SEC)|
| `preprint` | 预印本(bioRxiv/medRxiv|
| `news` | 专业媒体报道(Tier 3 用) |
### 必填字段
`id`, `tier`, `score`, `type`, `title`, `year`, `url`(或 `doi`
---
## 三、去重规则
dr-pm 在 Phase 2 结束时执行去重:
```python
# 伪代码
seen_urls = {}
seen_dois = {}
unique_sources = []
for source in all_sources:
key = source.get("doi") or source.get("url")
if key not in seen_urls:
seen_urls[key] = True
unique_sources.append(source)
else:
# 合并 used_in 字段
existing = seen_urls[key]
existing["used_in"] = list(set(existing["used_in"] + source["used_in"]))
```
去重后,草稿文件里的 [src_xxx] 标注**不需要更改**,因为 ID 是全局分配的。
---
## 四、参考文献列表生成(GB/T 7714-2015
dr-reporter 从 sources.jsonl 生成参考文献列表时,按以下格式:
### 期刊论文
```
[src_001] ZHANG S, LI M. 论文标题[J]. Nature Medicine, 2024, 30(5): 1234-1245. DOI: 10.1038/...
```
### 报告/白皮书
```
[src_042] McKinsey & Company. 报告标题[R]. McKinsey Global Institute, 2024.
```
### 监管文件
```
[src_018] FDA. NDA 申请审批公告[EB/OL]. (2024-03-15)[2026-04-20]. https://www.fda.gov/...
```
### 临床试验
```
[src_055] ClinicalTrials.gov. 试验名称 (NCT12345678)[EB/OL]. (2023-01-01)[2026-04-20]. https://clinicaltrials.gov/...
```
### 专利
```
[src_067] 发明人. 专利名称[P]. 专利号, 申请日.
```
### 排序规则
参考文献按在正文中**首次出现的顺序**排列,即 [src_001] 在最前,以此类推。
---
## 五、引用完整性检查(dr-chief-editor 用)
审校时检查:
1. 正文中所有 [src_xxx] 都在 sources.jsonl 里有对应记录
2. sources.jsonl 里所有 ID 在正文中都有引用(无孤立信源)
3. 所有 Tier 1 信源的 URL 或 DOI 格式正确
检查脚本(可用 bash 执行):
```bash
# 提取正文中的所有 src_id
grep -oE 'src_[0-9]+' projects/<slug>/phase4/final.md | sort -u > /tmp/cited.txt
# 提取 sources.jsonl 中的所有 id
python3 -c "
import json
ids = []
with open('projects/<slug>/phase2/sources.jsonl') as f:
for line in f:
d = json.loads(line)
ids.append(d['id'])
print('\n'.join(sorted(ids)))
" > /tmp/registered.txt
# 找差集
diff /tmp/cited.txt /tmp/registered.txt
```
+267
View File
@@ -0,0 +1,267 @@
---
name: en-zh-translation
description: 生物医药英译中规范。专有名词双语对照策略、术语表管理、断句重构原则、数字与标点转换、去翻译腔。dr-translator 必读。
---
# 英译中规范(生物医药专业报告)
## 你的身份
你是生物医药专业报告的翻译编辑,不是机器翻译。目标:译文读起来**像母语中文写作者的原创**,不是"翻译腔"。
---
## 一、整体原则
### 1. 忠实于内容,不忠实于句式
英文长句拆成中文短句。英文习惯的 "X, which is Y, was Z" 嵌套结构,中文应断为两到三句。
**反例(翻译腔)**
> 这种药物,它是 GLP-1 受体激动剂家族中的一员,被证明在降低 HbA1c 方面是有效的。
**改写**
> 这种药物属于 GLP-1 受体激动剂家族,能有效降低 HbA1c。
### 2. 不保留英文的被动语态
英文被动 → 中文主动(或不提主语)。
- "The drug was approved by FDA" → "FDA 批准了该药物" 或 "该药物获 FDA 批准"
- "It has been shown that..." → 直接说"研究显示..."或"数据显示..."
### 3. 去掉冗余连词
英文的 Furthermore / Moreover / Additionally / In addition / Furthermore 在中文大多可以删,让段落自然承接。
---
## 二、专有名词双语对照策略
### 首次出现原则
专有名词**首次出现**时用"中文(英文)"格式,之后只用中文或英文单独一致使用:
- 首次:`糖苷水解酶 101 家族(GH101 family`
- 之后:`GH101 家族``糖苷水解酶 101 家族`(选一种风格用到底)
### 行业惯例优先
以下术语在行业中**中英混用是标准做法**,不要强制翻译:
- 直接保留英文:FDA, EMA, NMPA, FEV1, BEC, sST2, CAT, SGRQ, BLA, NDA, IND, ICH, CMC, CDMO, CDMO, GH101, IgG, mAb, ADC, BsAb, GLP-1, PD-1, CAR-T, HbA1c, IL-4, IL-5, IL-13, IL-33, ST2, Th2, ILC2s
- 中英并列的术语:抗体偶联药物(ADC)、单克隆抗体(mAb)、慢性阻塞性肺疾病(COPD)
### 动词/形容词类翻译
这类不保留英文:
| 英文 | 中文 |
|---|---|
| significant | 显著(需跟数据)|
| substantial | 大幅 |
| demonstrate | 表明 / 显示 |
| exhibit | 表现出 |
| propose | 提出 |
| investigate | 研究 |
| establish | 建立 / 确立 |
| confirm | 证实 |
| underscore | (直接删,见 humanizer-cn |
| highlight | (直接删或改为"指出" |
---
## 三、数字与单位转换
### 数字表达
- 英文 "12,000" → 中文 "12,000"(保留千分位逗号)
- 英文 "1.2 billion USD" → 中文 "12 亿美元"
- 英文 "3.5 million patients" → 中文 "350 万名患者"
- 英文 "23%" → 中文 "23%"
### 量词
数字后面中文要加量词:
| 英文 | 中文 |
|---|---|
| 12 studies | 12 项研究 |
| 3 companies | 3 家公司 |
| 5 patents | 5 项专利 |
| 20 patients | 20 名患者 |
| 1.2 billion | 12 亿 |
| 3 years | 3 年 |
| 6 months | 6 个月 |
### 时间格式
- "2024" → "2024 年"
- "2020-2025" → "2020-2025 年"(用半角连字符)
- "Q4 2025" → "2025 年第 4 季度" 或 "2025 Q4"
- "March 2024" → "2024 年 3 月"
### 百分比
- "30%" → "30%"
- "up to 60%" → "最高 60%"
- "approximately 40%" → "约 40%"
---
## 四、标点转换
### 必须转为中文标点的情况
中文段落中,句内标点必须是**中文全角标点**:
| 英文 | 中文 |
|---|---|
| `,` | `` |
| `.` | `。` |
| `;` | `` |
| `:` | `` |
| `?` | `` |
| `!` | `` |
| `"..."` | `"..."` |
| `'...'` | `'...'` |
| `(...)` | `...` |
| `—` | `——`(中文全角破折号是两个连一起)|
| `...` | `……`(中文省略号六个点)|
### 保留英文标点的情况
- 英文术语、代码、URL 内部的标点
- 行内引用的英文原文
- 数据单位前后:`3.2 mg/kg`(数字和单位之间用英文斜杠)
---
## 五、句子结构重构
### 英文长句→中文短句
**原文**
> The observation that sST2 levels, which have been validated as a prognostic biomarker in heart failure and included in ACC/AHA guidelines, are elevated in COPD patients during acute exacerbations suggests a potential cross-disease mechanism.
**烂翻译**
> sST2 水平(它已被验证为心力衰竭的预后生物标志物并被纳入 ACC/AHA 指南)在 COPD 患者急性加重期间升高的观察结果,提示了潜在的跨疾病机制。
**好翻译**
> sST2 已是心力衰竭领域的预后生物标志物,纳入 ACC/AHA 指南多年。COPD 患者在急性加重期间 sST2 水平升高,提示这两类疾病可能共享同一条炎症通路。
### 修饰语位置
英文习惯把修饰语放后面(of / which / that 从句),中文要前置或拆句。
- "the company that pioneered O-glycosidase commercialization in 1990" → "1990 年率先实现 O-糖苷酶商业化的那家公司"(前置)
- 或拆句:"NEB 公司在 1990 年率先将 O-糖苷酶商业化"
---
## 六、段落重构
### 英文段落首句原则
英文学术/商业写作的段落一般首句即论点(Topic sentence),后面是论据。翻译时保留这一结构,不要打乱。
### 段落长度调整
- 英文习惯长段(150-200 词 / ≈ 250-330 字)
- 中文如果一段超过 400 字,读起来累。超过 400 字应考虑拆段。
- 拆段原则:按子论点拆,不要机械按句数拆
---
## 七、翻译后校对清单
译完后自己过三遍:
### 第 1 遍:准确性
- 所有数字、日期、百分比、来源 ID `[src_xxx]` 完全一致?
- 所有专有名词首次出现有中英对照?
- 没有错译、漏译?
### 第 2 遍:流畅性
- 有没有"的"字过多?(`X 的 Y 的 Z 的 W` 这种链式改为"X 下的 Y 中的 Z"
- 有没有翻译腔?("...的话"、"...的话说"、"对于...来说"、"在...方面"
- 句子长度是否有节奏变化?全长句或全短句都不行
- 读一遍念出来,是否自然?
### 第 3 遍:humanizer-cn 禁用词
加载 `skill:humanizer-cn`,扫一遍中文禁用词清单:
- 跃迁 / 赋能 / 落地 / 格局 / 生态 / 痛点 / 风口 / 闭环 / 抓手 / 颠覆 / 引领 / 重塑 / 赛道 / 范式 / 底层逻辑 / 本质上 / 从根本上
- AI 套话:随着、不断、深入、值得注意、不难发现、具有重要意义、综上所述、由此可见
---
## 八、特殊情况处理
### 原文是咨询报告风格
保留咨询报告的语感:观点前置、数字支撑、对比结构。不要为了"接地气"而口语化。
### 原文包含表格
表格翻译时:
- 表头翻译
- 单元格数字保留原格式
- 专有名词保留英文(节省宽度)
- 表格标题用 `表 X-Y<内容>(数据来源:[src_xxx]`
### 原文包含图表
图表标题和说明翻译;图表本身(如果是 matplotlib 生成的 PNG)可能需要 dr-reporter 用中文字体重新生成。
### 原文包含 SCQA 式段落(如 Executive Summary
SCQA 结构保留(因为这是麦肯锡式叙事),但不要标注 "S/C/Q/A"字样,按 `mckinsey-method skill` 要求写成融合式。
---
## 九、术语表维护
dr-translator 在翻译过程中维护项目级术语表:
```
projects/<slug>/phase4/glossary.json
{
"GH101 family": "糖苷水解酶 101 家族",
"endoglycosidase": "内切糖苷酶",
"O-glycosylation": "O-糖基化",
"Core 1": "核心 1 型",
...
}
```
这个术语表会作为 final report 里的"术语表(Glossary"章节,双语对照呈现。
---
## 十、输出要求
### 文件命名
- 英文稿:`projects/<slug>/phase4/final_en.md`
- 中文稿:`projects/<slug>/phase4/final_zh.md`dr-translator 产出)
- 中文稿润色后:仍然 `final_zh.md`dr-polisher 原地修改)
- PDF 主稿:`final.pdf`(中文)
- PDF 英文参考版:`final_en.pdf`
- DOCX 主稿:`final.docx`(中文)
### 交付汇报
翻译完成后向调用者返回:
```
翻译完成
英文源:projects/<slug>/phase4/final_en.md (X words)
中文译:projects/<slug>/phase4/final_zh.md (X 字)
膨胀率:X%(预期 1.4 倍)
术语表:projects/<slug>/phase4/glossary.json (X 条)
翻译质检:humanizer-cn 禁用词 X 项(已清理)
下一步:等待 dr-polisher 做最终润色
```
+157
View File
@@ -0,0 +1,157 @@
---
name: evidence-table
description: 证据矩阵规范。规定每条核心结论必须有对应的证据记录,格式、字段、置信度分级和文件结构。dr-analyst 撰写初稿时使用,dr-verifier 追加反方证据时使用,dr-chief-editor 审校时作为核验基准。
---
# 证据矩阵规范
## 核心原则
**每条结论必须可追溯**。报告中每一个有 [src_xxx] 标注的观点,都必须在对应章节的 evidence 文件中有一行记录。
---
## 证据矩阵文件格式
文件路径:`projects/<slug>/phase2/evidence/chXX-evidence.md`
### 文件结构
```markdown
# 第 X 章 <标题> — 证据矩阵
生成时间:<datetime>
研究员:dr-analyst
字数统计:<N> 字 / 配额 <N> 字
---
## 核心结论证据表
| 结论 ID | 观点摘要(≤30字) | 支持证据 1 | 支持证据 2 | 置信度 | 备注 |
|---|---|---|---|---|---|
| C01 | <观点> | [src_001] <标题> Tier1 | [src_002] <标题> Tier2 | 高 | |
| C02 | <观点> | [src_003] <标题> Tier2 | **[待验证]** 仅 1 个来源 | 中 | 需补充 |
| C03 | <观点> | [src_004] <标题> Tier1 | [src_005] <标题> Tier1 | 高 | |
---
## 置信度说明
- **高**2 个以上独立 Tier 1-2 信源支持,无重大反方证据
- **中**:只有 1 个 Tier 1-2 信源,或有轻微反方证据
- **低**:仅 Tier 3 信源,或有实质性反方证据
- **[待验证]**:找不到第 2 个独立信源,在正文明确标注
---
## 信源详情
<!-- 每条 [src_xxx] 的完整信息 -->
**[src_001]**
- 标题:
- 作者/机构:
- 年份:
- URL/DOI
- Tier1
- 评分:8.5
- 摘要(2-3句):
**[src_002]**
...
---
## 反方证据(dr-verifier 填写)
<!-- dr-verifier 完成后追加以下内容 -->
### 验证摘要
- 核验结论数:X
- 发现反方证据:X 条
- 补足待验证:X 条
- 重大挑战:X 条
### 反方证据详情
#### 针对结论 C01<观点摘要>
- 反方证据:<内容>
- 来源:[src_xxx] | Tier X
- 处理建议:保留并注明争议 / 修改措辞 / 删除
<!-- 如有重大挑战 -->
CRITICAL: <说明>
```
---
## 正文中反方证据段落的写作规范(v0.8 新)
### 标题必须观点化,不能叫 "反驳证据 / Counter-Evidence"
**问题诊断**v0.7 发现每章末尾 dr-analyst 会机械地写 `## 反驳证据`,标题重复而空洞,读者看了没有信息增益。
**新规则**:正文反方证据段落的标题必须:
1. **用二级 H2 或三级 H3 标题**(统一层级,禁止用加粗段冒充标题)
2. **包含具体判断**,不要用"反驳证据" / "反方证据" / "Counter-Evidence" 这种模板化命名
3. 至少要回答:**"对前述论点的哪一方面提出了什么挑战?"**
### 可接受的命名示例
| ✗ 不推荐 | ✓ 推荐 |
|---|---|
| 反驳证据 | 另一种声音:管线虚胖还是真实进展? |
| Counter-Evidence | 需要补充判断的副作用:汇聚偶联收率可能被高估 |
| 反方观点 | 反例:Codexis ECO 并非所有情境都优于 SPOS |
| Counter Arguments | 值得警惕的数据:临床前到 IND 的衰减率 |
### 段落结构模板(推荐)
```markdown
## <观点化标题>
虽然上文论证了 <核心观点>,但以下证据提示需要**有限度地**接受这一判断:
1. **<反方论点 1>**<具体数据或案例> [src_xxx]。影响评估:<说明>
2. **<反方论点 2>**<具体数据或案例> [src_xxx]。影响评估:<说明>
综合而言,核心结论仍成立,但需在 <某个具体维度> 上留出缓冲。
```
### 禁止的写法
- 单独用 **加粗段** 冒充反方证据标题(`**反方证据:** ...`
- 反方证据后不做整合判断,只是堆数据
- 在每个小节末尾都加反方证据(只在章末加一次即可;若小节级别有重大挑战,写在小节正文里即可)
---
## 置信度分级标准
| 置信度 | 条件 | 正文处理方式 |
|---|---|---|
| 高 | ≥2 个独立 Tier 1-2 信源,无 CRITICAL 反方 | 直接陈述 |
| 中 | 1 个 Tier 1-2 信源,或有轻微反方 | 陈述 + "但部分研究认为..." |
| 低 | 仅 Tier 3,或有实质反方 | 必须加 "[待验证]" 标注 |
| [待验证] | 无法找到第 2 个独立来源 | 正文明确写 "该观点仅有 1 个来源支持,待验证" |
---
## 结论 ID 命名规则
- `C01`-`C99`:正向核心结论
- `F01`-`F09`:事实性陈述(不需要观点判断)
- `T01`-`T09`:趋势判断(通常需要时间序列数据支撑)
dr-analyst 在撰写草稿时,给每个有 [src_xxx] 的观点分配一个 ID,在草稿和 evidence 文件里保持一致。
---
## 硬性规则
1. 草稿中每个 [src_xxx] 必须在 evidence 文件里有对应行
2. 草稿中标注 `[待验证]` 的观点必须在 evidence 表里有对应行(置信度列写"低/待验证"
3. dr-verifier 只能在"反方证据"段落追加,不能修改"核心结论证据表"
4. CRITICAL 标注的问题,dr-chief-editor 审校时必须明确处理(不能忽略)
+356
View File
@@ -0,0 +1,356 @@
---
name: humanizer-cn
description: 去除 AI 味的中英双语写作规则。基于 Wikipedia Signs of AI Writing + blader/humanizer + 生物医药中文报告特化经验。dr-polisher 必读;dr-editor-in-chief 和 dr-translator 也应遵循。
---
# Humanizer(中英双语去 AI 味规范)
## 你的任务
拿到文稿后,**按以下流程处理**
1. **扫描**全文,识别所有 AI 模式(下述 28 条)
2. **重写**问题段落,保留原意,换掉 AI-ism
3. **注入人味**:不只是删除坏模式,要加入真正的个性
4. **做最后一遍自问**:问自己"这段里还有什么显得是 AI 写的?"逐条修正
---
## 核心原则
> **避免 AI 模式只是一半工作。没有灵魂的干净文字和 slop 一样明显。好的写作背后有真人。**
---
## 英文规则(Phase 2/3 工作语言)
### 1. 过度渲染"意义/遗产/更宏大趋势"
**禁用词**stands/serves as, is a testament/reminder, pivotal/crucial/key moment, underscores importance, reflects broader, symbolizing enduring, contributing to, setting the stage for, marking a shift, key turning point, evolving landscape
**反例**
> The Institute was established in 1989, marking a pivotal moment in the evolution of regional statistics and reflecting broader decentralization trends.
**改写**
> The Institute was established in 1989 to collect regional statistics independently from the national office.
### 2. 空泛归因 / Weasel Words
**禁用词**Industry reports suggest, Observers have cited, Experts argue, Some critics argue, several sources, It has been noted
**反例**
> Experts believe this river plays a crucial role in the regional ecosystem.
**改写**
> A 2019 survey by the Chinese Academy of Sciences found that this river supports 12 endemic fish species.
### 3. -ing 短语堆砌(伪深度)
**禁用模式**highlighting/underscoring/emphasizing/ensuring/reflecting/contributing to/cultivating/fostering/encompassing/showcasing
**反例**
> The architect chose blue, green, and gold, symbolizing Texas bluebonnets, reflecting the community's deep connection to the land.
**改写**
> The architect chose blue, green, and gold to reference local bluebonnets and the Gulf coast.
### 4. 高频 AI 词汇
**禁用**Actually, additionally, align with, crucial, delve, emphasizing, enduring, enhance, fostering, garner, highlight (v), interplay, intricate, key (adj), landscape (abstract), pivotal, showcase, tapestry, testament, underscore, valuable, vibrant
### 5. 规避 "is/are"Copula Avoidance
**禁用模式**X serves as Y / X stands as Y / X marks Y / X represents Y / X boasts / X features / X offers
**反例**
> Gallery 825 serves as LAAA's exhibition space and boasts over 3,000 square feet.
**改写**
> Gallery 825 is LAAA's exhibition space, with 3,000 square feet.
### 6. 负向平行(Negative Parallelism
**禁用**It's not just X, it's Y / Not only X but Y / more than just
### 7. 三段式(Rule of Three
不为凑数拼三项。"The event features keynote sessions, panel discussions, and networking opportunities" → "The event includes talks and panels with informal networking between sessions."
### 8. 同义词循环(Elegant Variation
避免在连续句子里对同一概念使用"the protagonist / the main character / the central figure / the hero"。一个叫法用到底即可。
### 9. 伪范围(False Ranges
**禁用**from X to Y 当 X 和 Y 不在同一量纲上。
"Our journey has taken us from the Big Bang to the cosmic web, from star formation to dark matter" → "The book covers the Big Bang, star formation, and dark matter."
### 10. 被动/无主语片段
"No configuration file needed" → "You do not need a configuration file."
"The results are preserved automatically" → "The system preserves the results automatically."
### 11. 破折号过度使用
长破折号每章不超过 3 处。绝大多数情况下可以用逗号、句号或括号改写。
### 12. 内联粗体列表(Inline-header bullets
**反例**
- **Speed:** Code generation is significantly faster.
- **Quality:** Output quality has been enhanced.
- **Adoption:** Usage continues to grow.
**改写为段落**
> The tool speeds up code generation substantially and improves output through better training. Adoption has grown alongside these improvements.
### 13. 标题 Title Case
**禁用**## Strategic Negotiations And Global Partnerships
**改为**## Strategic negotiations and global partnerships
### 14. Emoji 装饰
禁止在标题、段落开头用 emoji(🚀 💡 ✅)。
### 15. Curly Quotes
"这种引号" 改为普通 "straight quotes"。
### 16. Chatbot 残留
**禁用**I hope this helps, Of course!, Certainly!, You're absolutely right!, Would you like..., Let me know, Here is a..., Great question!
### 17. 知识截止声明
**禁用**as of [date], up to my last training update, While specific details are limited, based on available information
### 18. 谄媚语气
**禁用**Great question! You're absolutely right! That's an excellent point.
### 19. Filler 短语
| 禁用 | 改为 |
|---|---|
| In order to achieve this goal | To achieve this |
| Due to the fact that | Because |
| At this point in time | Now |
| In the event that | If |
| Has the ability to | Can |
| It is important to note that | (直接删) |
### 20. 过度 Hedging
"It could potentially possibly be argued that..." → "The policy may..."
### 21. 通用正向总结
**禁用**The future looks bright. Exciting times lie ahead. A major step in the right direction.
**改为**:具体说清楚下一步做什么。
### 22. 连字符成对词过多
AI 对常用词对几乎 100% 加连字符(third-party, cross-functional, data-driven, decision-making, well-known, high-quality, real-time, long-term, end-to-end)。人类用得不那么整齐。选择性去掉连字符。
### 23. 权威套话
**禁用**The real question is, at its core, in reality, what really matters, fundamentally, the deeper issue, the heart of the matter
这些都是 AI 假装切穿表象直达真相的套路,删掉后真相还是在那里。
### 24. Signposting 预告
**禁用**Let's dive in, let's explore, let's break this down, here's what you need to know, without further ado
直接进入内容,不要预告。
### 25. 标题后重复一遍标题内容
```
## Performance
Speed matters.
When users hit a slow page, they leave.
```
删掉 "Speed matters"。
### 26. 过度 boldface
不要在每一句里加粗 **这个****那个**。一段里粗体超过 2 处,说明你没认真思考该强调什么。
---
## 中文规则(Phase 4 翻译后适用)
中文有一套自己的 AI 味。以下是核心禁用清单。
### CN-1:AI 高频词(见到即替换)
| 禁用词 | 替换策略 |
|---|---|
| 跃迁 / 跃升 / 跃上 | 改为"升至 / 提升到" |
| 赋能 | 改为"帮助 / 支持 / 推动" |
| 落地 | 改为"实施 / 推行 / 执行" |
| 格局 | 具体说清楚是"竞争格局"还是"市场格局" |
| 生态 / 生态圈 | 少用,具体说"上下游企业" |
| 深度 / 深度的 | 少用,改为具体描述 |
| 痛点 | 改为"问题 / 困难" |
| 风口 | 直接说"市场机会" |
| 闭环 / 打通闭环 | 改为"完成了完整流程" |
| 抓手 | 删掉,直接说动作 |
| 颠覆 / 颠覆性 | 少用,过度戏剧化 |
| 引领 / 引领性 | 改为具体的"率先做了..." |
| 重塑 / 重构 | 改为"改变 / 改组" |
| 赛道 | 改为"细分领域 / 市场" |
| 范式 | 改为"方式 / 模式" |
| 底层逻辑 | 改为"根本原因 / 核心机制" |
| 本质上 / 从根本上 | 直接删除,讲具体内容 |
### CN-2AI 套话(整句清除)
**禁用**
- "随着 X 的不断发展"
- "在 X 背景下 / 在这一背景下"
- "值得注意的是"
- "不难发现"
- "显而易见"
- "毫无疑问"
- "具有重要意义"
- "发挥了重要作用"
- "起到了 X 的作用"
- "综上所述"
- "由此可见"
- "总的来说"
- "据报道"
- "有研究表明 / 有专家认为"(除非说出具体是哪项研究/哪位专家)
### CN-3:规避"是"的冗余句式(AI 极高频)
| 禁用 | 改为 |
|---|---|
| X 标志着 Y | X 是 Y / 从此 Y 成立 |
| X 代表着 Y | X 是 Y |
| X 构成 Y | X 是 Y |
| X 成为 Y | 直接用 "X 是 Y" |
| X 跃升为 Y | X 升到 Y |
| X 彰显着 Y | X 说明 Y |
| X 体现了 Y | X 说明 Y / X 显示 Y |
### CN-4:三段式堆砌(Rule of Three
中文 AI 特别爱拼三段:
- "短期、中期、长期"
- "需求侧、供给侧、政策侧"
- "技术、资本、市场"
- "硬实力、软实力、巧实力"
不为凑数拼三段。如果只有两个要点就说两个,四个就说四个,节奏自然一些。
### CN-5:空洞形容词(必须跟数据)
- 巨大 → "250 亿美元"
- 快速 → "CAGR 23%"
- 显著 → "降低 40%p<0.001"
- 强劲 → "增速高于同行 8 个百分点"
- 显著领先 → "领先第二名 30% 市场份额"
无数据形容词直接删。
### CN-6:破折号规范
- 中文用全角破折号 `——`(两个连接起来)
- 每章不超过 3 处
- 多数情况可改为逗号、括号、句号
### CN-7:负向平行
**禁用**
- "不仅...更..."
- "不仅仅是...,更是..."
- "不是...而是..."
- "不只是...,还是..."
偶用可以,成段出现必改。
### CN-8:过度书面腔 / 翻译腔
- "使...得以..." → "让 X 能 Y"
- "通过...方式 / 通过...的方式" → "用 X / 以 X"
- "对于 X 而言" → "X 的..."
- "在 X 方面" → 具体说哪方面
- "就 X 来说" → 删
### CN-9:内联粗体小标题(每段开头都加粗)
**反例**
- **技术层面**xxx
- **商业层面**xxx
- **风险层面**xxx
改为叙述段落。保留加粗的前提是:真的是关键强调,而不是装饰。
### CN-10:章节标题 / 小节标题禁止用符号开头
禁止:🚀 / ✅ / 💡 / ⚠️ 等 emoji 开头。如果是正式提示标志,在正文中用文字表述。
---
## 注入人味(Add Soul
干净但没灵魂的文字和 AI slop 一样明显。合格的报告要有:
### 有立场
不要只报告事实——对事实有判断。"定价策略存在风险"比"定价策略有利有弊"好。
### 节奏变化
短句。再长一些的句子慢慢把意思讲清楚。混着用。
### 承认不确定
真人会说"我不确定",AI 不会。允许出现"这个趋势能否持续还需观察"、"目前数据尚不足以做判断"这种诚实表达。
### 具体而非抽象
- "用户体验有待改善" → "首次打开页面要 8 秒,用户很可能就离开了"
- "存在风险" → "最大风险是 NEB 可能用专利诉讼拖延 18-24 个月"
### 有节制的第一人称
研究报告一般不用"我",但是"我们认为 / 本报告认为"比"一般认为"好。
---
## 最后的自审流程
写完一段后,问自己三个问题:
1. **这句话是真人会说的吗?** 读出来听听看是否拗口做作
2. **能更具体吗?** 任何抽象词都问能否换成数据/案例
3. **删掉这句会失去什么?** 如果没损失,删
---
## Process Summary(给调用 agent 的操作清单)
拿到一份待润色的稿子后:
1. **扫描并标记**:全文搜索上述禁用词和模式
2. **逐段重写**:按规则替换
3. **检查节奏**:段落长短、句子长短是否有变化
4. **删减冗余**:空话 / 套话 / 无数据形容词
5. **核对格式**:没有 emoji、粗体不过度、标题 sentence case、破折号节制
6. **So What 检验**:每段都得有价值
7. **最后一遍自问**:这段还有哪里像 AI?针对性再改一轮
---
## 参考
- Wikipedia: Signs of AI writing (WikiProject AI Cleanup)
- blader/humanizer SKILL v2.5.1
- 9MW1911 综合战略报告(中文风格参考)
+172
View File
@@ -0,0 +1,172 @@
---
name: length-budget
description: 报告字数预算分配与执行校验。支持 4 种字数模式(自动/简明/详细/深度),英中互换规则(英文词数 ≈ 中文字数 × 0.7),章节配额算法与补写策略。Phase 1 用于设定预算,Phase 2/4 用于自检。
---
# 字数预算系统
## 一、四种字数模式(Phase 1 访谈时选择)
用户在 /dr-init 访谈时选定其中一种,写入 manifest.word_budget_mode
| 模式 | 中文字数目标 | 英文词数目标 | 章节数 | 适用场景 |
|---|---|---|---|---|
| `auto` | 按类型默认 | 按类型默认 | 8-12 | 不明确时默认 |
| `concise` | 8,000-12,000 | 5,600-8,400 | 6-8 | 高管一阅即过 |
| `detailed` | 20,000-35,000 | 14,000-24,500 | 10-12 | 标准专业报告 |
| `deep` | 50,000-80,000 | 35,000-56,000 | 12-15 | 深度研究/行业专著 |
**auto 模式的默认字数**(按报告类型):
| 报告类型 | 中文字数 | 英文词数 | 章节数 |
|---|---|---|---|
| 综述 | 12,000 | 8,400 | 8-10 |
| 研究 | 35,000 | 24,500 | 10-12 |
| 投资 | 22,000 | 15,400 | 10-12 |
| 管理/工艺 | 18,000 | 12,600 | 9-11 |
**字数只是参考,以把问题讲清楚为第一优先**。过少则内容单薄,过多则注水。目标区间±20% 内都可接受。
---
## 二、英文-中文字数换算
**核心换算率**:1 个英文词 ≈ 1.4-1.5 个中文字
Phase 2 产出英文,Phase 4 翻译为中文后字数会自然膨胀约 40%。预算时:
- Phase 2 英文稿:按英文词数目标 × 1.0 写
- Phase 4 中文稿:英文词数 × 1.4 应落在中文字数目标的 ±15% 内
自动换算工具:
```python
def en_words_to_zh_chars(en_words: int) -> int:
return int(en_words * 1.4)
def zh_chars_to_en_words(zh_chars: int) -> int:
return int(zh_chars / 1.4)
```
---
## 三、章节配额分配(dr-plan 在 Phase 1 用)
### 三层优先级
```
P0(核心章,2-3 章):每章 = 总字数 × 13-16%
P1(主干章,3-5 章):每章 = 总字数 × 8-11%
P2(辅助章,2-3 章):每章 = 总字数 × 5-7%
引言 / 结论章:各 = 总字数 × 6-8%
```
### 约束
- 章节字数差距 ≤ ±30%(防止头重脚轻)
- 结论章 ≥ 总字数 × 10%
- 每节 ≥ 600 字(英文 ≥ 420 词),不足则合并节
### 示例:30,000 字研究报告分配
| 章 | 定位 | 中文配额 | 英文配额 |
|---|---|---|---|
| 第 1 章 引言 | intro | 1,800 | 1,260 |
| 第 2 章 核心论点 | P0 | 4,500 | 3,150 |
| 第 3 章 机制剖析 | P0 | 4,500 | 3,150 |
| 第 4 章 临床证据 | P1 | 3,300 | 2,310 |
| 第 5 章 竞争格局 | P1 | 3,000 | 2,100 |
| 第 6 章 产业链 | P1 | 2,700 | 1,890 |
| 第 7 章 政策监管 | P1 | 2,400 | 1,680 |
| 第 8 章 风险 | P2 | 1,800 | 1,260 |
| 第 9 章 国际对比 | P2 | 1,500 | 1,050 |
| 第 10 章 趋势判断 | P1 | 2,100 | 1,470 |
| 第 11 章 结论与建议 | conclusion | 2,400 | 1,680 |
| **合计** | | **30,000** | **21,000** |
---
## 四、Phase 2 执行校验
dr-analyst 每章交稿前自检,dr-pm 每批校验。
### 英文字数统计
```bash
# 英文词数
wc -w projects/<slug>/phase2/drafts/chXX.md
```
### 中文字数统计(未来用)
```python
def count_chinese_words(text: str) -> int:
import re
cn = sum(1 for c in text if '\u4e00' <= c <= '\u9fff')
text_no_cn = re.sub(r'[\u4e00-\u9fff]', ' ', text)
en = len(re.findall(r"[A-Za-z]+(?:[-'][A-Za-z]+)*", text_no_cn))
return cn + en
```
### 校验逻辑
```
1. 读 chXX.md 统计英文词数
2. 对照 framework 中该章配额
3. 判断:
实际/配额 < 0.7 → 不合格,继续挖掘证据补写
0.7 ≤ 比值 < 0.85 → 警告,建议补足
0.85 ≤ 比值 ≤ 1.3 → 合格
比值 > 1.3 → 超纲,考虑精简或拆分
```
---
## 五、字数不足的正确补救路径
**绝对禁止为凑数注水**"在此背景下"、"随着...的不断发展"等空话)。字数不足的应对顺序:
1. **检查证据覆盖**:该章观点是否有 ≥2 独立 Tier 1-2 信源?若无 → 检索补证据
2. **展开数据细节**:把表格里的数字展开成文字分析(趋势、拐点、对比)
3. **增加案例**:用 1-2 个具体公司/产品案例佐证抽象观点
4. **补反方证据**:把反方证据段落写详细(500-800 字)
5. **延伸推论**:对核心判断做"若成立则..."和"若不成立则..."分支
6. **国际对比**:若原文只讲中国,加一段国际对比
---
## 六、manifest 字段规范
```json
{
"word_budget_mode": "detailed",
"target_words_zh": 30000,
"target_words_en": 21000,
"min_words_zh": 24000,
"chapter_quotas_en": [
{"index": 1, "title": "...", "en_words": 1260, "priority": "intro"},
{"index": 2, "title": "...", "en_words": 3150, "priority": "P0"}
]
}
```
---
## 七、Phase 4 翻译后中文字数校验
翻译完成后:
1. 读 final_zh.md,按中英混排规则统计字数
2. 对照 manifest.target_words_zh
3. 落在 ±15% 区间内 → 合格
4. 不合格 → dr-polisher 在润色时适度扩展/压缩
---
## 八、硬规则
1. ✅ auto 模式按类型默认,其他模式按用户选择
2. ✅ 章节字数差距 ≤ ±30%
3. ✅ 结论章 ≥ 全文 10%
4. ✅ Phase 2 英文稿每章自检,不足 70% 配额必须补
5. ✅ Phase 4 中文稿整体字数校验在 ±15% 内
6. ❌ 禁止为凑数注水(AI 套话、重复表述)
7. ❌ 禁止对字数"打折"交稿
+177
View File
@@ -0,0 +1,177 @@
---
name: mckinsey-method
description: 顶级咨询公司(麦肯锡/BCG/德勤)研究报告写作方法论。金字塔原理、MECE、观点型标题、证据-数据-案例三要素、So What 自检。SCQA 仅用于 Executive Summary 和各章引入段,严禁每节机械套用。所有写作类 agentdr-analyst/dr-editor-in-chief/dr-polisher)必读。
---
# 顶级咨询报告写作方法论(精炼版)
## 核心定位
本文档是报告写作的**唯一风格准则**。读过 9MW1911 综合战略报告吗?那是目标风格。读起来像咨询公司资深合伙人在跟董事会讲话,不像 AI 生成的综述。
---
## 一、金字塔原理(Pyramid Principle
**结论先行,论据支撑,论据之间 MECE。**
```
章标题(= 一句判断)
章首 2-3 段:直接给出本章结论 + 核心逻辑
节 2.1= 支撑论点 1
节首 1 段:本节论点 + 核心证据
数据/事实/案例(带 [src_xxx]
So What:这意味着什么
节 2.2= 支撑论点 2
...
```
**章标题即判断**(强制):
- ✗ "第 2 章 市场现状"
- ✓ "第 2 章 中国 GLP-1 市场 2025 年已跨越 10 亿美元门槛,增速仍在加速"
**节标题即子判断**(强制):
- ✗ "2.1 竞争格局"
- ✓ "2.1 双寡头格局将在 3 年内被国产厂商打破"
---
## 二、SCQA 的正确使用(关键纠正)
**SCQA 不是每章每节都套的格式,是整份报告的开篇叙事工具。**
### 允许使用的位置(仅以下三处)
1. **Executive Summary 开头**(最重要):用 SCQA 引出报告核心命题
2. **第 1 章引言**:用 SCQA 建立整份报告的分析框架
3. **决策性关键章引入段**(可选):当某章提出重大判断或政策建议时
### 禁止使用的位置
- ✗ 每一节的开头都写 SCQA
- ✗ 任何位置显式标注 "**Situation(背景)**" "**Complication(张力)**" 等字样
- ✗ 三级小节还套用 SCQA
### 正确的 SCQA(隐式、融合式)
> 生物药 pipeline 的爆炸式扩张与 CMC 合规要求的层层抬高,正将 O-糖苷酶从一个依赖唾液酸预处理的专业试剂,推向覆盖 ADC 开发、双特异性抗体表征的工作流必选组分。然而,30 年来主导市场的 NEB 经典产品,对唾液酸化底物无活性这一根本局限始终未被突破。这就引出了一个核心问题:当下一代工程酶在 2019-2024 年集中商业化、市场标准正在重构时,后来者的进入窗口是否真实存在?本报告认为,窗口存在,但持续时间不超过 36 个月。
(这段里有 S/C/Q/A 四个要素,但没有任何显式标注,读起来像一段自然的论述。)
### 错误的 SCQA(显式标注式)
> **Situation(背景)**:生物药 pipeline 扩张...
> **Complication(张力)**NEB 经典产品局限...
> **Question(问题)**:窗口是否存在?
> **Answer(答案)**:本章认为...
(这种写法是典型的 AI 套路,在最终报告里绝对禁止出现。)
---
## 三、MECE(章节划分铁律)
**Mutually Exclusive, Collectively Exhaustive — 互斥且穷尽。**
自检:
- 如果读者读完第 3 章再读第 5 章,会不会觉得"刚才好像说过这个"?有 → 合并或重划
- 对照分析框架列出所有应覆盖的维度,有遗漏 → 补章节
常用框架:
- 市场:需求侧 / 供给侧 / 竞争格局 / 监管环境
- 技术:原理 / 验证 / 产业化 / 壁垒
- 投资:市场空间 / 竞争壁垒 / 财务模型 / 风险
---
## 四、观点 + 证据的写作单元
每个段落的标准结构:
```
[论点句] 具体判断,1 句话。
[证据 1] 根据 <具体来源><数据> [src_xxx]。
[证据 2] 进一步,<案例/对比> [src_xxx]。
[So What] 这意味着 <对上层论点的意义>。
```
### 数据引用规范
| 类型 | 写法示例 |
|---|---|
| 市场规模 | "120 亿美元(2024 年)[src_042]" |
| 增长率 | "CAGR 23%2023-2030[src_018]" |
| 临床数据 | "OS 改善 23%95% CI: 13.8-16.6p<0.001N=1,200[src_007]" |
| 成功率 | "FDA 获批率 41%N=127 项 NDA2020-2024[src_033]" |
### 禁止写法
- "市场规模巨大" → 必须写具体数字
- "研究表明" → 必须写是哪项研究(来源 ID)
- "近年来" → 必须写具体年份
- "有专家认为" → 必须写哪位专家(或删去,用数据代替)
---
## 五、So What 自检(每段都做)
每写完一段,问自己:"所以呢?这对读者有什么意义?"
- 答案是"只是客观描述" → **要么删,要么补 So What 句**
- So What 句通常放段尾,1-2 句,明确点出这段内容对上层论点的贡献
示例:
- "这意味着,先发厂商在 2026 年之前建立的渠道优势将难以被后来者复制。"
- "因此,判断一个 GLP-1 管线的商业价值,给药频率比疗效终点更关键。"
---
## 六、报告整体结构(11 件套)
参考 9MW1911 综合战略报告的组织方式:
1. **封面**:主标题 + 副标题 + 保密标识 + 编制日期
2. **Executive Summary / 执行摘要**(≈800 字):SCQA 开篇 + 核心结论 4 条 + 关键行动优先级
3. **Abstract / 摘要**(500-600 字):独立一页,叙事式,面向广义读者
4. **Glossary / 术语表**(双语对照)
5. **Table of Contents / 目录**
6. **正文章节**8-15 章)
7. **结论与建议章**
8. **附录**(2-5 个,如 CMC、临床对比、合并症分析)
9. **参考文献**[src_xxx] 编号格式)
10. **免责声明**
11. **版本信息**
---
## 七、章节写作 Checklist(每章完成后自检)
- [ ] 章标题是观点型判断,不是"概述/现状/背景"
- [ ] 章首 2-3 段给出结论和逻辑
- [ ] 各节互斥且穷尽(MECE
- [ ] 每节标题也是观点型判断
- [ ] 每个数字/事实后接 [src_xxx]
- [ ] 每段末尾有 So What 句
- [ ] 没有显式标注 "S/C/Q/A"
- [ ] 没有"巨大/快速/显著/强劲"等无数据形容词
- [ ] 没有"本章定位/字数配额/研究员"等调度元数据
- [ ] 没有占位符 [待验证](除非确实找不到第二个信源)
---
## 八、严禁出现的写作坏习惯(humanizer 已列,此处精简)
**AI 套话**:随着、不断、深入、值得注意、不难发现、显而易见、具有重要意义、发挥重要作用、显著、巨大、快速发展、在此背景下、综上所述、由此可见、总的来说
**规避 "是" 的冗余结构**(AI 高频):标志着 / 代表着 / 构成 / 成为 / 跃升为 → 直接用 "是"
**三段式堆砌**:不要为凑数硬拼"需求侧 / 供给侧 / 政策侧"、"短期 / 中期 / 长期"
**破折号过用**:长破折号每章不超过 3 处
**负向平行**:不要"不仅...更..."、"不是...而是..."(偶用可,成段出现不可)
完整规则见 `skill:humanizer-cn`
+266
View File
@@ -0,0 +1,266 @@
---
name: output-hygiene
description: 报告输出卫生检查。禁止词清单(调度元数据、占位符残留、待验证标注)、格式异常检测、参考文献完整性校验。dr-polisher 在润色最后一步必跑;dr-reporter 出稿前复查一次。
---
# 输出卫生清单(Output Hygiene Checklist
## 目的
拦截"调度元数据"和"中间产物残留"进入最终报告。9MW1911 那份报告之所以干净,是因为过滤掉了一切过程性内容,只留最终成品。
---
## 一、禁止出现在最终报告正文中的字样(元数据黑名单)
以下字符串在 final.md / final_zh.md / final_en.md 里**一旦出现即为缺陷**dr-polisher 必须清除:
### A. 调度元数据
- `章节定位`
- `字数配额`
- `研究员:dr-analyst`
- `研究员:dr-searcher`
- `生成时间:2026-`(Year-MM 生成日期只在版本信息页出现一次)
- `P0 核心章` / `P1` / `P2`(这些是内部分级,不给读者看)
- `dr-plan` / `dr-pm` / `dr-analyst` / `dr-verifier` / `dr-chief-editor` / `dr-editor-in-chief` / `dr-polisher` / `dr-reporter` / `dr-translator`
- `Phase 1` / `Phase 2` / `Phase 3` / `Phase 4`(除非在"方法论说明"附录讨论研究流程时)
### B. 占位符残留
- `[由 dr-reporter 自动生成]`
- `[待填]` / `[TBD]` / `[TODO]`
- `<slug>` / `<topic>` / `<N>` / `<X>` 等模板占位符
- `{{ ... }}` / `${...}` 变量语法残留
### C. 中间产物引用
- `参考信源:[src_101] [src_120] (详见 sources.jsonl ch02 条目)`
- `详见 phase2/evidence/chXX-evidence.md`
- `详见 sources.jsonl`
- `本章信源索引:...(详见 ...)`
- `⚠️ 待验证` / `⚠️ [待验证]`(这是过程性标注;如必须保留某个"存疑观点"的提示,应改为正式语言如"该数据仅有 X 个来源支持,建议人工核实")
### D. 研究思路泄漏
- `研究思路:`
- `核心研究问题:`
- `初步假设:`
- `预期信源:`
- `预期篇幅:`
这些是 framework.md 里给 dr-analyst 看的规划信息,不能出现在读者版。
### E. Agent 交付汇报语
- `产出:` / `完成后返回:`
- `任务:` / `硬性要求:`
- `必读 skill`
- `章节小结:` (改为自然段落收尾)
---
## 二、格式异常检测
### F. SCQA 显式标注(已禁止的机械模式)
以下组合**不应在最终报告中成对出现**(用 grep 扫):
- `**Situation(背景)**` + `**Complication(张力)**`
- `**S(背景)**` + `**C(挑战)**` + `**Q(问题)**` + `**A(答案)**`
- `Answer-First` 显式标注
- `**核心结论(Answer-First**`
SCQA 要写得隐式融合(见 mckinsey-method skill)。
### G. 三级以上嵌套标题乱用
正文正式章节标题不要超过 3 级:
- `# 第 X 章` (报告级)
- `## X.Y 节` (章内节)
- `### X.Y.Z 小节` (节内小节)
禁止 `####` `#####` `######`。如果需要 4 级以上,重新组织结构。
### H. 引用格式不统一
所有引用统一 `[src_XXX]` 格式(3 位数字)。禁止混用:
- `[src_1]`(没补零)
- `[source_001]`(变形)
- `(src_001)`(圆括号)
- `[ref_1]` / `[r1]`(其他简写)
### I. 中英文标点混用
中文正文里的标点应是**中文标点**:
- `` 不是 `,`
- `。` 不是 `.`
- `` 不是 `;`
- `` 不是 `:`
- `"..."` 不是 `"..."`(除了直接引用英文)
- `...` 不是 `(...)`
例外:行内英文术语、代码、URL、数据单位前后保持英文标点合理。
### J. Emoji(强制禁用)
**正文与表格中严禁使用任何 emoji / 彩色符号**
禁用清单(但不限于):
`✅ ❌ ✔ ✖ 🔶 🔷 ⭐ 🟢 🔴 🟡 🟠 ⚠️ ⚠ 💡 📌 🔑 📊 📈 📉 🔥 ✨ 🎯 🎉 ➔ ➜`
**原因**:PDF 使用的思源字体子集不包含这些字符的 glyph,渲染为空白方框(□)。
**替代写法**
- 表格标记"有/无":用 `✓` `×`(思源字体支持)或中文字 `是` / `否`
- 强调状态:用 `◆` `●` 等几何符号(字体支持)
- 警示:用 `注:` `警告:` `※` 等文字前缀
- 重点:用 **粗体** 或引用块,不用 emoji
扫描命令:
```bash
python3 -c "
import re
txt = open('final_zh_polished.md').read()
pat = re.compile(r'[\u2700-\u27BF]|[\U0001F300-\U0001F9FF]|[\u2B00-\u2BFF]')
hits = [(i, m.group()) for i, m in enumerate(pat.finditer(txt))]
print(f'emoji 命中:{len(hits)} 处')
for i, c in hits[:10]:
print(f' 位置 {i}: {c!r} (U+{ord(c):04X})')
"
```
---
## 三、参考文献完整性校验(最关键)
dr-reporter 出稿前**必须**执行:
```bash
# 1. 从 final.md 提取所有引用的 src_id
grep -oE '\[src_[0-9]+\]' projects/<slug>/phase4/final.md | sort -u > /tmp/cited.txt
# 2. 从 citations.md / sources.jsonl 提取所有已登记的 src_id
grep -oE 'src_[0-9]+' projects/<slug>/phase4/citations.md | sort -u > /tmp/registered.txt
# 或从 sources.jsonl
python3 -c "
import json
with open('projects/<slug>/phase2/sources.jsonl') as f:
for line in f:
d = json.loads(line)
print(d['id'])
" | sort -u > /tmp/registered.txt
# 3. 差集:cited 里有但 registered 里没有 → 严重错误
comm -23 /tmp/cited.txt <(sed 's/[][]//g' /tmp/registered.txt) > /tmp/missing.txt
# 4. 反向差集:registered 有但从未被 cited → 孤立信源,可剔除
comm -13 /tmp/cited.txt <(sed 's/[][]//g' /tmp/registered.txt) > /tmp/orphan.txt
```
### 处理规则
- 有 missing 信源(引用了但无记录)→ **致命错误**dr-reporter 拒绝出稿,抛回上游排查
- 有 orphan 信源(有记录但未被引用)→ 警告,从 citations.md 剔除
- final.md 里的"参考文献"段落**必须包含完整的编号清单**,不能是 `[由 dr-reporter 自动生成]` 之类的占位符
- 如果 final.md 的参考文献段落是占位符 → 读 citations.md 内容回填
---
## 四、标题规范
### 章标题
- 观点型判断句,不是"概述/现状/背景"
- 长度 15-40 字(中)/ 10-25 词(英)
- 不以动词开头(如"分析/探讨/研究"),改为判断句
**反例**
- 第 2 章 分析中国 GLP-1 市场的现状
- 第 3 章 探讨 NEB 产品的竞争优势
**正例**
- 第 2 章 中国 GLP-1 市场 2025 年已跨越 10 亿美元门槛
- 第 3 章 NEB 的 30 年专利丛林将在 2028 年后开始瓦解
### 节标题
- 同样要求观点型
- 长度 10-25 字 / 8-15 词
- 禁止 `2.1 背景 / 2.2 现状 / 2.3 趋势` 这种模板化结构
---
## 五、图表与数据卫生
### 表格
- 表头第一行要有单位(金额 USD / 百分比 % / 年份等)
- 所有数据有来源标注(行内 [src_xxx] 或表脚注)
- 避免超过 10 列宽表(PDF 会被截断)
### 图表标题
格式:`图 X-Y<内容描述>(数据来源:[src_xxx]`
### 数字规范
- 阿拉伯数字 + 中文量词:`12 项研究` / `3.2 亿元`
- 大数字三位分节:`12,000` 而非 `12000`
- 百分比带 `%`,不写"百分之十二"
- 时间范围用连字符:`2020-2025 年` 不是 `2020 至 2025 年`
---
## 六、自动化检查脚本(dr-polisher / dr-reporter 必跑)
```python
# hygiene_check.py
import re, sys
BLACKLIST_ZH = [
"章节定位", "字数配额", "研究员:dr-",
"P0 核心章", "P1 主干章", "P2 辅助章",
"Phase 1", "Phase 2", "Phase 3", "Phase 4",
"dr-plan", "dr-pm", "dr-analyst", "dr-verifier",
"dr-chief-editor", "dr-editor-in-chief", "dr-polisher",
"dr-reporter", "dr-translator",
"[由 dr-reporter 自动生成]", "[待填]", "[TBD]", "[TODO]",
"详见 phase2/", "详见 sources.jsonl",
"本章信源索引", "⚠️ 待验证", "⚠️ [待验证]",
"**Situation(背景)**", "**Complication(张力)**",
"**Question(问题)**", "**Answer(答案)**",
"**S(背景)**", "**C(挑战)**",
"Answer-First", "核心结论(Answer-First",
"研究思路:", "核心研究问题:", "初步假设:",
"预期信源:", "预期篇幅:",
"硬性要求:", "必读 skill", "产出:",
]
path = sys.argv[1]
text = open(path, encoding='utf-8').read()
issues = []
for pattern in BLACKLIST_ZH:
if pattern in text:
count = text.count(pattern)
issues.append(f" × '{pattern}' 出现 {count}")
if issues:
print(f"{path} 存在 {len(issues)} 项卫生问题:")
for i in issues:
print(i)
sys.exit(1)
else:
print(f"{path} 输出卫生检查通过")
sys.exit(0)
```
---
## 七、硬规则
1. ✅ dr-polisher 润色的最后一步跑 hygiene_check
2. ✅ dr-reporter 出稿前再跑一次 hygiene_check + 参考文献完整性校验
3. ✅ 任何禁止词残留都必须修正,不能"放过一马"
4. ✅ 参考文献段落必须包含完整编号清单,不允许占位符
5. ❌ 禁止把"⚠️ 待验证"这种过程标注留到读者版
6. ❌ 禁止三级以上嵌套标题
+303
View File
@@ -0,0 +1,303 @@
---
name: pdf-reportlab
description: 用 ReportLab 生成专业中文 PDF 研究报告。集中样式管理、紧凑分页(widows/orphans/keepWithNext)、颜色层次、封面保密标识、页眉页脚简洁化。基于 9MW1911 综合战略报告的编排参考。
---
# ReportLab 中文 PDF 模板使用指南(v0.5)
## 一、为什么是 ReportLab
- **完全可控**:每个字号、行距、颜色由代码说了算,不被引擎意外改动
- **中文字体一次搞定**`pdfmetrics.registerFont` 注册后全局可用,子集嵌入 PDF,无授权问题
- **分页规则精确**:支持 widows/orphans、keepWithNext、splitByRow 等避免孤行/寡行
- **图表嵌入高质量**matplotlib 300 DPI PNG,速度快
- **样式集中在 StyleSheet**:避免字号不一
---
## 二、模板入口
脚本:`.opencode/templates/report-template.py`
调用:
```bash
uv run python .opencode/templates/report-template.py \
--input projects/<slug>/phase4/final.md \
--manifest projects/<slug>/manifest.json \
--output projects/<slug>/phase4/final.pdf \
--fonts-dir .opencode/templates/fonts
```
首次使用前:
```bash
bash .opencode/templates/fonts/download-fonts.sh
```
---
## 三、字体注册(7 个字重)
```
SrcSerif-Regular 思源宋体 Regular 正文
SrcSerif-Bold 思源宋体 Bold 正文粗体
SrcSans-Light 思源黑体 Light 页眉页脚
SrcSans-Medium 思源黑体 Medium 三级标题/图表
SrcSans-Bold 思源黑体 Bold 一二级标题
SrcSans-Heavy 思源黑体 Heavy 封面大标题
Kai 霞鹜文楷 Regular 摘要/引文
```
---
## 四、样式表(StyleSheet,集中管理)
| 样式名 | 字体 | 字号 | 行高 | 颜色 | 备注 |
|---|---|---|---|---|---|
| `body` | SrcSerif | 10.5 | 18 | `#1a1a1a` | 正文,首行缩进 21pt |
| `body-bold` | SrcSerif-Bold | 10.5 | 18 | `#1a1a1a` | 行内加粗 |
| `h1` | SrcSans-Bold | 18 | 28 | `#1e3a8a` | 章标题,前强制分页 |
| `h2` | SrcSans-Bold | 14 | 22 | `#2c5282` | 节标题 |
| `h3` | SrcSans-Medium | 12 | 18 | `#374151` | 小节标题 |
| `quote` | Kai | 10.5 | 18 | `#4b5563` | 引文 |
| `caption` | SrcSans-Medium | 9 | 13 | `#6b7280` | 图表标题 |
| `footnote` | SrcSerif | 9 | 13 | `#374151` | 脚注/参考文献 |
| `header-footer` | SrcSans-Light | 8 | 12 | `#9ca3af` | 页眉页脚 |
| `cover-title` | SrcSans-Heavy | 28 | 40 | `#0f172a` | 封面主标题 |
| `cover-subtitle` | SrcSans-Medium | 15 | 24 | `#475569` | 封面副标题 |
| `cover-confidential` | SrcSans-Bold | 11 | 16 | `#dc2626` | 封面保密标识(红色)|
| `cover-meta` | SrcSerif | 11 | 18 | `#334155` | 封面元信息 |
| `summary` | SrcSerif | 11 | 20 | `#1a1a1a` | 执行摘要 |
---
## 五、分页规则(关键升级点)
### 章(h1
- `pageBreakBefore=1`(每章新起一页)
- `keepWithNext=1`(标题和下一段不分离)
### 节(h2
- **禁止单独触发分页**
- `keepWithNext=1`(标题紧跟内容)
### 小节(h3
- 同 h2`keepWithNext=1`
### 段落
- `widows=2, orphans=2`(避免寡行/孤行)
- 每段之间 `spaceBefore=6, spaceAfter=6`
### 表格
- `TableStyle` 中启用 `splitByRow=True`(长表格按行分页)
- `repeatRows=1`(表头在分页后重复)
- 单元格 padding 统一 4-6pt
### 摘要/术语表/目录/参考文献
- 每个独占起始页(用 `PageBreak`
---
## 六、封面模板(参考 9MW1911)
封面独立一页,无页眉页脚,布局:
```
(上 30% 空白)
主标题(cover-title,黑色,居中)
副标题(cover-subtitle,深灰,居中)
(中部 40% 空白)
[机密 | 仅供 XX 内部决策使用] cover-confidential,红色,居中)
(下部 20%
类型:研究类
作者:Deep Research 系统 / <公司名>
编制日期:YYYY 年 M 月
版本:v1.0
```
字段来源:
- `manifest.report_title` / `report_subtitle`
- `manifest.confidentiality` (如 "机密 | 仅供迈威生物内部决策使用")
- `manifest.author` / `manifest.date` / `manifest.version`
---
## 七、页眉页脚(简洁化)
### 页眉
左边:报告简称(从 `manifest.report_title` 取前 15 字)
右边:章节名(动态,从当前 h1 内容取)
底线:`#e5e7eb` 浅灰分隔线
### 页脚
居中:页码(格式 `— X —`
字号:8pt`#9ca3af` 浅灰
### 特殊页
- 封面:无页眉页脚
- 免责声明:无页眉,仅页脚
- 其他(摘要/术语表/目录/正文/附录/参考文献):有页眉页脚
---
## 八、报告结构(11 件套)
ReportLab 按以下顺序组装:
1. **封面**cover PageTemplate
2. **免责声明**normal PageTemplate,单页)
3. **Executive Summary / 执行摘要**summary PageTemplate1-2 页)
4. **Abstract / 摘要**normal,单页)
5. **Glossary / 术语表**normal
6. **Table of Contents / 目录**TOC,自动生成)
7. **正文各章**(normal,每章 h1 强制分页)
8. **结论与建议**(正文的一部分)
9. **附录**normal
10. **参考文献**bibliography PageTemplatefootnote 样式)
11. **版本信息**normal,单页)
---
## 九、Markdown 支持范围
| Markdown | ReportLab 渲染 |
|---|---|
| `# 第 X 章 ...` | h1(新起一页,深蓝色)|
| `## X.Y ...` | h2(节,蓝色,不分页)|
| `### X.Y.Z ...` | h3(小节,深灰)|
| `**粗体**` | inline `<b>` |
| `*斜体*` | inline `<i>` |
| `` `代码` `` | 等宽字体 |
| `> 引文` | quote 样式(楷体,浅底色)|
| `- 项` / `1. 项` | 项目符号列表 |
| 表格 `\| \| \|` | Table,自动列宽 + splitByRow |
| `![caption](path)` | 图片 + caption 样式 |
| `[src_001]` | 上标引用 |
| `---` | 分页符 |
**不支持**:HTML 标签、数学公式、代码块高亮。
---
## 十、图表嵌入
不用 ReportLab 原生绘图,全部预渲染为 PNG:
```python
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
font_path = '.opencode/templates/fonts/SourceHanSansSC-Medium.otf'
fm.fontManager.addfont(font_path)
plt.rcParams['font.family'] = 'Source Han Sans SC'
plt.rcParams['axes.unicode_minus'] = False
fig, ax = plt.subplots(figsize=(6, 4), dpi=300)
# ...
plt.savefig('projects/<slug>/phase4/figures/fig_01_XXX.png', dpi=300, bbox_inches='tight')
```
Markdown 中引用:`![图 1-12020-2025 GLP-1 市场规模(数据来源:[src_042]](figures/fig_01_XXX.png)`
---
## 十一、manifest 必需字段
```json
{
"slug": "...",
"report_title": "自研 O-糖苷酶立项可行性研究报告",
"report_subtitle": "对标 NEB 与 Merck 经典产品的技术路径、IP 壁垒与差异化战略",
"confidentiality": "机密 | 仅供内部决策使用",
"author": "Deep Research 系统",
"date": "2026-04-21",
"version": "1.0",
"type": "研究类",
"disclaimer": "本报告基于公开信息与 AI 辅助研究生成,仅供参考..."
}
```
---
## 十二、常见坑与对策
| 坑 | 对策 |
|---|---|
| 中文字号不一 | 集中 StyleSheet,不在 Paragraph 里 inline 改 fontSize |
| 行距太挤 | 行高 = 字号 × 1.5~1.7 |
| 换行断错 | `wordWrap='CJK'` 必设 |
| 表格被截断 | `splitByRow=True, repeatRows=1` |
| 标题孤行(页末一个标题后直接新页)| `keepWithNext=1` |
| 段落寡行(末段只剩一行在下页)| `widows=2, orphans=2` |
| 图片变形 | 先 matplotlib 出 300 DPI PNG,再 `Image(path, width=..., kind="proportional")` |
| 生成慢 | matplotlib 图表预渲染,不要在 PDF 生成阶段现算 |
| 参考文献丢失 | dr-reporter 出稿前检查:读 citations.md,确认内容已写入 final.md |
---
## 十三、dr-reporter 调用流程
```
1. 环境检查
- 字体 ≥6 个 OTF 文件
- final.md 存在
- manifest.json 有必需字段
2. 回填参考文献(关键步骤,修复 v0.4 的 bug)
- 读 citations.md 内容
- 在 final.md 中找到"## 参考文献"段落
- 如果段落内容是占位符(如 "[由 dr-reporter 自动生成]" 或为空)
→ 替换为 citations.md 的完整内容
- 写回 final.md
3. 图表检查
- 扫描 final.md 中所有 ![...](path) 图片引用
- 验证每个 path 在 figures/ 目录下存在
- 缺失图片 → 警告并继续,但汇报中注明
4. 生成 PDF
uv run python .opencode/templates/report-template.py \
--input projects/<slug>/phase4/final.md \
--manifest projects/<slug>/manifest.json \
--output projects/<slug>/phase4/final.pdf \
--fonts-dir .opencode/templates/fonts
5. 生成 DOCX(可选)
pandoc projects/<slug>/phase4/final.md \
--from markdown --to docx \
--output projects/<slug>/phase4/final.docx \
--toc --toc-depth=3
6. 验证
- PDF 文件大小 > 500KB(太小说明字体没嵌)
- PDF 页数合理(按字数估算)
- "参考文献"章节存在且非空
7. 汇报
```
---
## 十四、输出卫生检查(必跑)
生成 PDF 前:
```bash
# 加载 skill:output-hygiene
python3 .opencode/templates/hygiene_check.py projects/<slug>/phase4/final.md
```
禁止词残留 → 抛回 dr-polisher 再润色一轮。
+246
View File
@@ -0,0 +1,246 @@
---
name: search-strategy
description: 生物医药深度研究的统一检索策略。规定信源优先级金字塔、检索轮次、关键词策略、API 调用顺序,以及何时切换到专业信源。所有做信息收集的 agentdr-searcher/dr-analyst/dr-verifier/dr-plan)必须加载此技能。
---
# 检索策略总纲(Deep Research 黄金法则)
## 一、信源优先级金字塔
**永远从上至下尝试,不要从 Tier 4 开始**
```
Tier 1 ╲ 最高权重
(一手) ╲
╱──────────────╲
Tier 2 ╲ 标准权重
╱ (权威二手) ╲
╱──────────────────╲
Tier 3 ╲ 辅助权重
(预印本/会议/券商) ╲
─────────────────────────
Tier 4 仅做发现入口
(通用搜索 + Wiki)
─────────────────────────
【黑名单】禁用
```
### Tier 1(优先使用,加权 1.2x
- **论文**PubMed、Cochrane、顶刊(NEJM/Lancet/Nature/Science/Cell/JAMA/NatMed
- **监管**FDA/EMA/NMPA/PMDA 官网、openFDA
- **临床试验**ClinicalTrials.gov、ChiCTR、EU CTR
- **专利**USPTO、EPO、CNIPA、Google Patents、PatentsView
- **披露**SEC10-K/10-Q/S-1)、港交所、沪深交易所年报
### Tier 2(可用,标准权重)
- **咨询**McKinsey/BCG/Deloitte/IQVIA/Evaluate Pharma/Frost & Sullivan/沙利文
- **综述**:系统综述、Cochrane Review、Meta 分析
- **协会**:PhRMA、BIO、中国医药工业协会、中国医药创新促进会
- **专业媒体**BioSpace、Endpoints News、FiercePharma、STAT、医药魔方、Insight 数据库
### Tier 3(辅助,需 Tier 1-2 支撑)
- **预印本**bioRxiv、medRxiv、SSRN(必须标注"未同行评审")
- **券商**:中金/中信/高盛/摩根士丹利生物医药(注意利益冲突)
- **会议**AACR/ASCO/ASH/JPM Healthcare Conference 摘要
### Tier 4(仅做入口)
- Tavily / Brave / Exa 返回的普通网页 → **只用来发现 Tier 1-2 URL**,不做结论佐证
- Wikipedia → **只做术语理解**,结论不得引用
### 黑名单(禁用为证据)
- 百家号、头条号、大部分公众号自媒体
- 未署名行业博客
- Retraction Watch 标记的撤稿论文
- "据业内人士透露"类无来源文章
- >5 年的综述(机制研究可放宽)
---
## 二、检索 4 轮法则
对任何一个 section/chapter**必须至少 4 轮检索**
### 第 1 轮:Tier 1 直命中
- 先用**精确查询**去 PubMed / ClinicalTrials / openFDA 打
- 关键词用 MeSH Term(医学主题词)+ 布尔逻辑
- 例:`(GLP-1[MeSH] OR "glucagon-like peptide-1") AND (obesity[MeSH]) AND ("2023"[PDAT]:"2026"[PDAT])`
### 第 2 轮:Tier 2 综述扫描
- 去 McKinsey Insights / BCG / Deloitte 官网搜 industry-overview
- Evaluate Pharma / IQVIA 白皮书(通常需注册)
- 目标:获取市场规模、竞争格局、趋势判断
### 第 3 轮:反方/证伪检索
- 主动搜索与初步结论相反的关键词
- 例:研究"GLP-1 成为减重首选"→ 反方要搜 "GLP-1 limitations" "semaglutide side effects" "discontinuation rate"
- 至少 3-5 条反方证据
### 第 4 轮:Tavily/Brave/Exa 补漏
- 仅用于发现前 3 轮遗漏的 URL
- 发现后**必须**回溯到原始 Tier 1-2 来源(论文 DOI、监管公告原文)
- 不得直接引用搜索返回的二次报道
---
## 三、API 调用顺序(技术栈,v0.8 更新)
**按"查询类型"路由到最合适的 API**,而不是一律走通用搜索。
```
┌──────────────────────────────────────────────────────────────┐
│ 查询类型 首选 备选 │
├──────────────────────────────────────────────────────────────┤
│ 专利 Serper Patents Google Patents 爬页 │
│ (site:patents.google.com) │
│ │
│ 学术论文 Serper Scholar PubMed E-utils │
│ (带引用数/年份/期刊) (原始数据库) │
│ │
│ 新闻 / 行业动态 Serper News Tavily │
│ (时效性敏感) │
│ │
│ 临床试验 ClinicalTrials.gov ChiCTR │
│ (原始 API) │
│ │
│ 监管公告 openFDA / FDA 搜索 EMA / NMPA 官网 │
│ │
│ 公司/机构识别 Exa Tavily │
│ (LinkedIn/官网召回强) │
│ │
│ 通用网页 Exa Tavily / Brave │
└──────────────────────────────────────────────────────────────┘
```
### Serpergoogle.serper.dev)使用模板
**专利检索**
```python
from scripts.lib.search_client import SearchClient
with SearchClient() as c:
hits = c.patents("dual-target siRNA GalNAc", num_results=10)
```
**学术论文**
```python
hits = c.scholar("dual-target RNAi 2024", num_results=10, year_low=2023)
# hits[i].snippet 里包含引用数和期刊信息
```
**新闻(时效性)**
```python
hits = c.news("Arrowhead ARO-DIMER-PA clinical trial", time_range="w") # 最近一周
```
### Tavily MCP 调用模板(通用网页 - Phase 1 初扫)
```
工具名:tavily_search
参数:
query: "<关键词>"
search_depth: "advanced"
max_results: 10
include_domains: ["pubmed.ncbi.nlm.nih.gov", "nejm.org", "lancet.com"]
exclude_domains: ["baijiahao.baidu.com", "toutiao.com"]
time_range: "year"
```
### 何时用哪个
- **专利相关问题**"谁拥有技术 X"、"FTO 自由度分析")→ `c.patents()`**永远先走 Google Patents**
- **需要引用数排序的论文**("找影响力最大的 N 篇")→ `c.scholar()`
- **近期新闻**"2025 年 Q4 交易"、"上月 IND 批准")→ `c.news(..., time_range="m")`
- **快速概念验证 / 术语理解** → `c.search()`(通用)
### PubMed E-utilsbash 调用示例)
```bash
# 1. esearch 拿 PMID 列表
curl -s "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi?db=pubmed&term=GLP-1+obesity&retmax=20&api_key=$NCBI_API_KEY&retmode=json"
# 2. efetch 拿摘要
curl -s "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=pubmed&id=PMID1,PMID2&rettype=abstract&retmode=xml&api_key=$NCBI_API_KEY"
```
### ClinicalTrials.gov v2 API
```bash
curl -s "https://clinicaltrials.gov/api/v2/studies?query.term=semaglutide&pageSize=20&format=json"
```
### openFDA
```bash
curl -s "https://api.fda.gov/drug/event.json?search=patient.drug.medicinalproduct:semaglutide&count=patient.reaction.reactionmeddrapt.exact&limit=20"
```
---
## 四、关键词策略
### 中英双语必备
- 任何生物医药主题**必须同时用中英文检索**
- 中文关键词去找:中国监管(NMPA)、A股研报、医药魔方、Insight
- 英文关键词去找:PubMed、FDA、顶刊、欧美研报
### MeSH Term 优先于自由词
- PubMed 查询必须用 MeSH Term(医学主题词表)
- MeSH Term 能自动扩展同义词,召回更全
- 例:`obesity[MeSH]` 会自动包含 `adiposity`, `body weight, excess`
### 布尔逻辑 + 时间过滤
- 默认加 `("2023"[PDAT]:"2026"[PDAT])` 时间窗
- 机制研究可放宽到 10 年,新药/市场/政策必须近 3 年
### 反方关键词清单(备忘)
- `limitations` / `adverse events` / `side effects` / `discontinuation`
- `failed trial` / `FDA rejection` / `withdrawn`
- `conflict of interest` / `industry-funded`
- `retraction` / `replication failure`
---
## 五、每条信源的提取字段(标准化)
任何信源进 `sources.jsonl` 必须有以下字段:
```jsonl
{
"id": "src_001",
"tier": 1,
"score": 9.2,
"type": "journal" | "trial" | "regulatory" | "patent" | "report" | "news",
"url": "https://...",
"doi": "10.xxxx/...",
"title": "...",
"authors": ["...", "..."],
"year": 2025,
"venue": "NEJM",
"impact_factor": 176.0,
"accessed_at": "2026-04-20",
"abstract": "...",
"key_data": {
"market_size_2025": "12.3B USD",
"cagr": "23%"
},
"used_in": ["ch01", "ch03.sec2"],
"notes": "关键图表见 Fig 2"
}
```
---
## 六、失败兜底
- 某个 API 限流/超时:**等 5s 重试 3 次**,仍失败则跳过并在日志标注
- 某个信源 404:在 sources.jsonl 标 `"dead_link": true`,不删除(审计用)
- 关键数据查不到:**不要编造**,在正文写 "该数据暂未找到公开来源(截至 2026-04)"
---
## 七、硬规则总结
1. ✅ 每 section 至少 4 轮检索
2. ✅ 中英双语必查
3. ✅ PubMed 用 MeSH Term
4. ✅ 反方关键词必查
5. ✅ Tier 4 结果只做发现,不做佐证
6. ✅ 所有信源写入 sources.jsonl 并评分
7. ❌ 不得引用 Wikipedia 做结论
8. ❌ 不得编造数据、URL、DOI
9. ❌ 不得使用黑名单信源
+188
View File
@@ -0,0 +1,188 @@
---
name: source-quality
description: 信源质量评分系统(0-10 分制)与黑名单机制。规定每个信源入库前的评估维度、硬性淘汰规则、利益冲突检测。所有收集信源的 agent 都必须用此技能给每条信源打分后写入 sources.jsonl。
---
# 信源质量评分(0-10 分制)
## 一、评分维度(满分 10 分)
| 维度 | 满分 | 判断标准 |
|---|---|---|
| 权威性 | 3 | 期刊 IF、机构排名 |
| 时效性 | 2 | 发表时间 vs 主题 |
| 一手性 | 2 | 一手数据 > 综述 > 二次解读 |
| 可验证性 | 2 | 有 DOI/URL/原始数据 |
| 无利益冲突 | 1 | 厂商自发降权 |
### 维度 1:权威性(0-3 分)
| 分值 | 情形 |
|---|---|
| 3.0 | IF ≥ 30NEJM/Lancet/Nature/Science/Cell/JAMA)、FDA/EMA/NMPA 官方、SEC 披露 |
| 2.5 | IF 10-30NatMed/NatBiotech/BMJ/AnnOncol 等)、顶级咨询(MKS/BCG/Deloitte |
| 2.0 | IF 5-10JCO/CircRes/AJRCCM)、IQVIA/EvaluatePharma、系统综述 |
| 1.5 | IF 3-5、券商研报、行业协会白皮书 |
| 1.0 | IF 1-3、专业媒体(BioSpace/Endpoints News |
| 0.5 | 预印本(bioRxiv/medRxiv)、会议摘要 |
| 0 | 自媒体、百家号、未署名博客 |
### 维度 2:时效性(0-2 分)
| 主题类型 | 满分年限 | 每老 1 年扣分 |
|---|---|---|
| 市场 / 监管 / 临床 | 3 年内 | -0.5 |
| 作用机制 / 基础研究 | 10 年内 | -0.2 |
| 政策法规 | **以最新版本为准** | 过时版本 0 分 |
| 历史追溯(有意为之) | 不限 | 不扣 |
### 维度 3:一手性(0-2 分)
| 分值 | 情形 |
|---|---|
| 2.0 | 一手数据(原始 RCT 论文、监管公告、年报原文、专利原文) |
| 1.5 | 系统综述 / Meta 分析 |
| 1.0 | 叙述性综述 / Review |
| 0.5 | 二次解读(新闻报道、券商改写) |
| 0 | 三次传播以上("据报道"/"业内人士") |
### 维度 4:可验证性(0-2 分)
| 分值 | 情形 |
|---|---|
| 2.0 | 有 DOI + 原始数据可下载(如 ClinicalTrials 的 CSR 附件) |
| 1.5 | 有 DOI 或稳定 URL,全文可访问 |
| 1.0 | URL 稳定但需付费墙 |
| 0.5 | 仅有 URL,无唯一标识符 |
| 0 | URL 失效 / 404 / 无法验证 |
### 维度 5:利益冲突(0-1 分)
| 分值 | 情形 |
|---|---|
| 1.0 | 独立研究(学术机构、政府)、无资助声明冲突 |
| 0.5 | 有 industry funding 但已声明且方法独立 |
| 0 | 厂商自发报告 / 直接商业软文 |
| **-1**(惩罚) | 声明冲突但方法可疑、或对比实验明显偏向资助方 |
---
## 二、综合评分硬规则
| 评分 | 可用性 |
|---|---|
| 8.0+ | 可作为核心论据,单独支撑结论 |
| 6.0-7.9 | 可用,但结论需 ≥2 个独立信源 |
| 4.0-5.9 | 仅作为参考,**不得作为唯一支撑** |
| < 4.0 | **禁止**用于结论佐证,只能入发现库 |
---
## 三、黑名单(直接拒绝入库)
以下信源**无论评分多少都禁用**
### 1. 明确劣质信源
- 百家号(baijiahao.baidu.com
- 头条号(toutiao.com 非原创栏目)
- 知乎回答(除非作者本人为业内专家且有实名背书)
- 小红书、抖音笔记
- 未署名作者的 wordpress / medium 博客
### 2. 被撤稿论文
- 查询 Retraction Watch 数据库(https://retractionwatch.com/
- Crossref API 检查论文状态:`https://api.crossref.org/works/<DOI>`
### 3. 明显软文/PR 稿
识别特征(命中任意 2 条即拒):
- 标题含 "重磅发布" "首创" "引领" 等夸张词
- 通篇无具体数据,只有 CEO/专家口头引述
- 发布渠道是企业官网的"新闻中心"且无交叉第三方验证
- 仅讲优势不讲局限
### 4. 时效过期
- 综述 > 5 年(机制研究可放宽)
- 政策/监管 > 1 年(以最新版本为准)
- 市场数据 > 2 年
### 5. 维基百科
- **仅可作术语理解入口**
- 结论永不引用
- 如从 Wiki 发现了参考文献,**回溯到原始来源**再引用
---
## 四、利益冲突检测要点
### 常见利益冲突场景
- 药企赞助的 RCT 对自家产品评价极高 → 查对比剂、盲法、样本量
- 咨询公司报告引用自家客户数据 → 查 acknowledgment 段
- 行业协会报告涉及会员企业 → 查资助方名单
- 券商研报 + 该券商是相关公司的保荐人 → 查 IPO/承销记录
### 操作方法
每条信源入库前检查:
1. 作者/机构是否与被评估的公司/产品有商业关联?
2. 资助声明(funding statement)里提到什么?
3. 利益披露(disclosure)是否完整?
**发现强利益冲突**:评分 ≤ 3(等同废弃);写入 `sources.jsonl``"conflict_of_interest": "..."` 字段。
---
## 五、评分执行流程(伪代码)
```
for each candidate_source in search_results:
# 1. 黑名单快筛
if is_blacklisted(candidate_source):
log("BLACKLIST: " + source.url); continue
# 2. 撤稿检查
if has_doi(source) and is_retracted(source.doi):
log("RETRACTED: " + source.doi); continue
# 3. 评分
score = 0
score += authority_score(source) # 0-3
score += recency_score(source, topic) # 0-2
score += primacy_score(source) # 0-2
score += verifiability_score(source) # 0-2
score += coi_score(source) # 0-1 or -1
# 4. Tier 加权
if source.tier == 1: score *= 1.2
# 5. 入库
if score >= 4.0:
append_to_sources_jsonl(source, score)
else:
log("LOW SCORE (" + score + "): " + source.url)
```
---
## 六、输出字段(写入 sources.jsonl
```jsonl
{
"id": "src_042",
"score": 8.6,
"tier": 1,
"authority": 3.0,
"recency": 2.0,
"primacy": 2.0,
"verifiability": 2.0,
"coi": 1.0,
"conflict_of_interest": null,
"blacklist_checked": true,
"retraction_checked": true,
"notes": "NEJM 2025 原文,RCT 独立研究"
}
```
---
## 七、审计留痕
所有被**拒绝**的信源,也要写入 `projects/<slug>/phase2/rejected-sources.jsonl`(注明原因)。这是事后复盘的关键,不要静默丢弃。
+19
View File
@@ -0,0 +1,19 @@
name = "dr-analyst"
description = "Chapter deep-research agent that writes English chapter drafts and evidence matrices."
model = "gpt-5.4"
model_reasoning_effort = "high"
sandbox_mode = "workspace-write"
developer_instructions = """
You are dr-analyst.
Work in English. Own exactly one assigned chapter.
Load skills: search-strategy, source-quality, length-budget, evidence-table, mckinsey-method, humanizer-cn.
Write:
- projects/<slug>/phase2/drafts/chXX.md
- projects/<slug>/phase2/evidence/chXX-evidence.md
- append structured sources to projects/<slug>/phase2/sources.jsonl
Every claim and numerical fact needs [src_xxx].
Every conclusion needs at least two independent Tier 1-2 sources, or must be explicitly marked as under-verified.
End each chapter with a concrete counter-evidence or limitation section title, not a generic 'Counter-Evidence' label.
Do not include scheduling metadata, quota notes, agent names, or SCQA labels in the chapter body.
"""
nickname_candidates = ["Analyst A", "Analyst B", "Analyst C", "Analyst D"]
+15
View File
@@ -0,0 +1,15 @@
name = "dr-chief-editor"
description = "Phase 3 read-only editorial reviewer for whole-report logic, evidence, MECE, and quality."
model = "gpt-5.4"
model_reasoning_effort = "xhigh"
sandbox_mode = "read-only"
developer_instructions = """
You are dr-chief-editor.
You are Phase 3 only and read-only except when explicitly asked by the parent to produce phase3/critique.md.
Review all English drafts, evidence files, sources.jsonl, framework.md, and manifest.json.
Assess central thesis coherence, logic, MECE, evidence sufficiency, counter-evidence handling, word count, point-of-view strength, and AI-pattern risks.
Do not rewrite drafts.
Do not enter Phase 4.
Do not spawn subagents.
"""
nickname_candidates = ["Chief Editor"]
+15
View File
@@ -0,0 +1,15 @@
name = "dr-editor-in-chief"
description = "Phase 4 lead editor for English final assembly and deterministic script orchestration."
model = "gpt-5.4"
model_reasoning_effort = "xhigh"
sandbox_mode = "workspace-write"
developer_instructions = """
You are dr-editor-in-chief.
Own Phase 4 creative assembly only:
- Merge phase2 drafts into phase4/final_en.md.
- Write Executive Summary, Abstract, Glossary, transitions, and final structure.
- Remove scheduling metadata and output-hygiene violations.
Do not translate the whole report yourself. Use scripts/dr.py finalize or the underlying Python scripts for translate, glossary, apply_glossary, polish, and build_report.
Keep citations intact.
"""
nickname_candidates = ["Editor in Chief"]
+18
View File
@@ -0,0 +1,18 @@
name = "dr-plan"
description = "Deep Research framework planner for Phase 1 interview, initial scan synthesis, and bilingual research framework."
model = "gpt-5.4"
model_reasoning_effort = "high"
sandbox_mode = "workspace-write"
developer_instructions = """
You are dr-plan for the biomedical Deep Research system.
Work in Chinese with the user, but write framework research thinking in English.
Follow AGENTS.md and load the relevant skills: search-strategy, source-quality, length-budget, mckinsey-method, humanizer-cn.
Your responsibilities are /dr-init and /dr-frame equivalents:
- Interview the user before framework generation.
- Propose formal report titles.
- Spawn dr-searcher subagents in parallel when asked to perform initial scans.
- Generate phase1/framework.md with bilingual chapter titles, English research thinking, word quotas, central thesis, and alternative frameworks.
Do not perform Phase 2 chapter deep research yourself.
Do not enter Phase 4.
"""
nickname_candidates = ["Planner Alpha", "Planner Beta", "Planner Gamma"]
+18
View File
@@ -0,0 +1,18 @@
name = "dr-pm"
description = "Deep Research project manager for Phase 2 batching, analyst/verifier orchestration, and project status."
model = "gpt-5.4"
model_reasoning_effort = "high"
sandbox_mode = "workspace-write"
developer_instructions = """
You are dr-pm for the biomedical Deep Research system.
Use English for Phase 2 working outputs.
Follow AGENTS.md and load skills: search-strategy, source-quality, length-budget, evidence-table, mckinsey-method.
Your responsibilities:
- Read manifest.json and phase1/framework.md.
- Plan Phase 2 batches, keeping 3 chapters or fewer per batch unless a chapter is large.
- Spawn dr-analyst subagents in parallel for chapter drafts.
- Spawn dr-verifier subagents after analyst completion for counter-evidence.
- Maintain manifest progress summaries and avoid carrying detailed batch chatter forward.
- Never write final reports directly.
"""
nickname_candidates = ["PM Alpha", "PM Beta", "PM Gamma"]
+14
View File
@@ -0,0 +1,14 @@
name = "dr-reporter"
description = "Report production agent for PDF/DOCX rendering and final output checks."
model = "gpt-5.4-mini"
model_reasoning_effort = "medium"
sandbox_mode = "workspace-write"
developer_instructions = """
You are dr-reporter.
You are an execution-focused report producer.
Use scripts/build_report.py and .opencode/templates/report-template.py to render PDF/DOCX from final_zh_polished.md or the specified Markdown.
Run output hygiene checks and citation checks where available.
Do not change research conclusions.
Do not invent missing references.
"""
nickname_candidates = ["Reporter"]
+14
View File
@@ -0,0 +1,14 @@
name = "dr-searcher"
description = "Lightweight source discovery agent for initial scans and targeted source finding."
model = "gpt-5.4-mini"
model_reasoning_effort = "medium"
sandbox_mode = "read-only"
developer_instructions = """
You are dr-searcher.
Your job is source discovery only. Do not write project files unless explicitly instructed by the parent.
Load skills: search-strategy and source-quality.
Search English and Chinese keywords, prioritize Tier 1-2 sources, include counter-evidence search terms, and return concise Markdown with URLs/DOIs and source-quality scores.
Do not use Wikipedia as evidence.
Do not fabricate URLs, DOIs, trial IDs, patents, or source ids.
"""
nickname_candidates = ["Searcher A", "Searcher B", "Searcher C", "Searcher D"]
+15
View File
@@ -0,0 +1,15 @@
name = "dr-verifier"
description = "Independent counter-evidence and fact-checking agent for completed chapters."
model = "gpt-5.4"
model_reasoning_effort = "high"
sandbox_mode = "workspace-write"
developer_instructions = """
You are dr-verifier.
Act as an independent devil's advocate. Do not protect the analyst's conclusion.
Read the assigned draft and evidence file, verify numbers, search for counter-evidence, and append a verification section to the evidence file.
Use read-then-rewrite for evidence files. Do not edit chapter drafts.
Flag CRITICAL issues when counter-evidence could overturn a chapter's core claim.
Use Chinese and English searches for China-market claims.
Do not fabricate counter-evidence or sources.
"""
nickname_candidates = ["Verifier A", "Verifier B", "Verifier C"]
+25
View File
@@ -0,0 +1,25 @@
# Codex Command: dr-finalize
You are dr-editor-in-chief. The user requested `/dr-finalize $ARGUMENTS`.
Goal: run Phase 4 in Codex native mode.
Steps:
1. Resolve `$ARGUMENTS` as project slug.
2. Validate Phase 2 is complete and Phase 3 is approved, unless the user explicitly confirms skipping.
3. Assemble `phase4/final_en.md` from drafts and write Executive Summary, Abstract, Glossary, TOC placeholder, References placeholder, and version history.
4. Run deterministic pipeline:
```bash
uv run python scripts/dr.py finalize <slug> --translate-workers 4 --glossary-workers 4 --polish-workers 4
```
5. If network/API errors occur, rerun with lower workers:
```bash
uv run python scripts/dr.py finalize <slug> --translate-workers 1 --glossary-workers 3 --polish-workers 1
```
6. Report output files, word counts, glossary issues, and any citation warnings.
Do not translate or polish the full report manually in one LLM response.
+17
View File
@@ -0,0 +1,17 @@
# Codex Command: dr-frame
You are dr-plan. The user requested `/dr-frame $ARGUMENTS`.
Goal: generate Phase 1 bilingual framework for the target project.
Steps:
1. Resolve `$ARGUMENTS` as project slug; if empty, use the most recently modified project.
2. Read `manifest.json` and validate Phase 1 interview is complete.
3. Load skills: search-strategy, source-quality, length-budget, mckinsey-method, humanizer-cn.
4. Spawn 3-4 `dr-searcher` subagents in parallel for MECE keyword groups. Wait for all results.
5. Synthesize `phase1/initial-scan.md`.
6. Write `phase1/framework.md` with bilingual chapter titles, English research thinking, word quotas, central thesis, risks, and alternatives.
7. Update manifest Phase 1 fields.
8. Stop and ask the user to approve the framework before Phase 2.
Do not do Phase 2 research in this command.
+26
View File
@@ -0,0 +1,26 @@
# Codex Command: dr-glossary
You are dr-pm. The user requested `/dr-glossary $ARGUMENTS`.
Goal: run terminology verification in Codex native mode.
Default command:
```bash
uv run python scripts/dr.py glossary <slug> --workers 4
```
If network or SSL errors occur, rerun with:
```bash
uv run python scripts/dr.py glossary <slug> --workers 3
```
After completion, summarize:
- terms checked
- high/medium/low confidence counts
- spelling errors
- wrong Chinese names
- recommended manual review items
Do not auto-apply glossary corrections unless the user asks or the pipeline is in Phase 4 finalize.
+14
View File
@@ -0,0 +1,14 @@
# Codex Command: dr-init
You are dr-plan. The user requested `/dr-init $ARGUMENTS`.
Goal: initialize a new biomedical Deep Research project without using OpenCode.
Follow AGENTS.md, then:
1. Interview the user with the 8 required questions from AGENTS.md and the existing OpenCode workflow.
2. Propose 3 formal report title/subtitle candidates.
3. After the user chooses, create `projects/<slug>/manifest.json` and the phase directories.
4. Write the interview transcript to `projects/<slug>/phase1/interview.md`.
5. Stop after initialization. Do not run `/dr-frame`.
Use Codex custom agent `dr-plan` if spawning is needed, but this command can usually run in the main thread.
+18
View File
@@ -0,0 +1,18 @@
# Codex Command: dr-research
You are dr-pm. The user requested `/dr-research $ARGUMENTS`.
Goal: run Phase 2 deep research using Codex custom subagents.
Steps:
1. Resolve `$ARGUMENTS` as project slug; if empty, use the most recently modified project.
2. Validate `phase1.approved == true` and framework exists.
3. Parse chapter quotas and section research thinking from `phase1/framework.md`.
4. Plan batches: large chapters alone; otherwise no more than 3 chapters per batch.
5. For each batch, spawn `dr-analyst` subagents in parallel, one per chapter.
6. After analyst outputs are written, spawn `dr-verifier` for each completed chapter.
7. Update manifest progress and summarize each batch in compact status fields.
8. Deduplicate `phase2/sources.jsonl`.
9. Report totals and stop before Phase 3.
Do not write the final report.
+14
View File
@@ -0,0 +1,14 @@
# Codex Command: dr-review
You are dr-chief-editor. The user requested `/dr-review $ARGUMENTS`.
Goal: perform Phase 3 whole-report editorial review.
Steps:
1. Resolve `$ARGUMENTS` as project slug; if empty, use the most recently modified project.
2. Validate `phase2.status == "completed"`.
3. Read framework, all drafts, all evidence files, sources.jsonl, and manifest.
4. Evaluate central thesis coherence, logic, MECE, evidence sufficiency, counter-evidence handling, word count, point-of-view strength, and AI-pattern risk.
5. Write `projects/<slug>/phase3/critique.md`.
6. Report rating A/B/C/D and must-fix items.
7. Stop and wait for user decision. Do not enter Phase 4.
+77
View File
@@ -0,0 +1,77 @@
# Codex Command: dr-run
You are the Deep Research PM main thread for `/dr-run $ARGUMENTS`.
This command is the Codex equivalent of letting `dr-pm` own orchestration. Do not ask the user to run each phase manually. Inspect the project state, continue from the correct phase, spawn the required Codex custom agents, wait for their results, and only pause at the defined human decision gates.
## Resolve Target
1. Treat `$ARGUMENTS` as either a project slug/path or a new research topic.
2. If it matches an existing project, load `projects/<slug>/manifest.json` and continue from its current status.
3. If it is a new topic, run Phase 1 initialization and create the project structure before planning.
4. If `$ARGUMENTS` is empty, use the most recently modified project and confirm the inferred slug in your first status update.
## Global Rules
- Follow `AGENTS.md`, all relevant `.agents/skills/*/SKILL.md`, and the project `manifest.json`.
- Keep `projects/**` research artifacts out of system commits unless the user explicitly asks to commit research output.
- Use Codex subagents deliberately: spawn them when parallel work materially advances the phase, wait for results, and consolidate before moving on.
- Do not simulate subagent work in the main thread when the task calls for independent review, chapter research, or counter-evidence.
- Respect the required pause gates:
- Stop after Phase 1 framework is ready and ask the user to approve or revise it.
- Stop after Phase 3 critique is ready and ask whether to fix, rerun Phase 2, or restart.
- Ask for approval before expensive broad web searches, long-running external API work, or irreversible file operations.
## Phase 1: Framework Planning
Run this phase when there is no approved `phase1/framework.md`.
1. Spawn `dr-plan` to interview the user if the topic is underspecified.
2. Spawn `dr-searcher` workers in parallel for initial source discovery across scientific, regulatory, clinical, commercial, and patent angles.
3. Have `dr-plan` synthesize a MECE framework with chapter-level word budgets and source strategy.
4. Write `phase1/interview.md`, `phase1/initial-scan.md`, and `phase1/framework.md`.
5. Update `manifest.json` and stop for user framework approval.
## Phase 2: Deep Research
Run this phase when `phase1.approved == true` and Phase 2 is incomplete.
1. Act as `dr-pm`: parse `phase1/framework.md`, create chapter batches, and keep the main thread focused on orchestration.
2. Spawn `dr-analyst` subagents in parallel, one per chapter or chapter group depending on size.
3. Require each analyst to write English chapter drafts plus evidence matrices.
4. After analyst outputs are present, spawn independent `dr-verifier` subagents for counter-evidence and source-quality checks.
5. Reconcile verifier findings into the relevant evidence files and chapter TODOs.
6. Deduplicate and normalize `phase2/sources.jsonl`.
7. Update `manifest.json` and continue to Phase 3 unless the evidence base is materially inadequate.
## Phase 3: Chief Editor Review
Run this phase when Phase 2 is complete and Phase 3 is incomplete.
1. Spawn `dr-chief-editor` as a read-only reviewer.
2. Have it assess MECE structure, evidence sufficiency, unsupported claims, source quality, chapter balance, and executive-level narrative.
3. Write `phase3/critique.md`.
4. Stop for user decision: targeted fix, rerun Phase 2 for weak chapters, or restart framework.
## Phase 4: Finalization
Run this phase only after the user approves Phase 3.
1. Spawn `dr-editor-in-chief` to merge English drafts into `phase4/final_en.md`, Executive Summary, Abstract, and glossary seed.
2. Run the deterministic pipeline with:
```bash
uv run python scripts/dr.py finalize <slug>
```
3. If needed, spawn `dr-reporter` for final PDF/DOCX validation and citation backfill checks.
4. Report final artifact paths and remaining risks.
## Status Discipline
Give concise progress updates after each phase or batch. Always say:
- current phase
- agents spawned and why
- files produced or changed
- whether the workflow is continuing or waiting for user decision
+9
View File
@@ -0,0 +1,9 @@
# Codex Command: dr-status
Run the platform-neutral status command:
```bash
uv run python scripts/dr.py status $ARGUMENTS
```
Then summarize the result for the user and suggest the next appropriate phase.
+67
View File
@@ -0,0 +1,67 @@
model = "gpt-5.4"
model_reasoning_effort = "high"
sandbox_mode = "workspace-write"
approval_policy = "never"
project_doc_max_bytes = 65536
web_search = "live"
[features]
multi_agent = true
[agents]
max_threads = 6
max_depth = 1
[profiles.deep-research]
model = "gpt-5.4"
model_reasoning_effort = "high"
sandbox_mode = "workspace-write"
approval_policy = "never"
project_doc_max_bytes = 65536
web_search = "live"
[profiles.deep-research-fast]
model = "gpt-5.4-mini"
model_reasoning_effort = "medium"
sandbox_mode = "workspace-write"
approval_policy = "never"
project_doc_max_bytes = 65536
web_search = "live"
[profiles.deep-research-review]
model = "gpt-5.4"
model_reasoning_effort = "xhigh"
sandbox_mode = "workspace-write"
approval_policy = "never"
project_doc_max_bytes = 65536
web_search = "live"
[mcp_servers.openaiDeveloperDocs]
url = "https://developers.openai.com/mcp"
[mcp_servers.openaiDeveloperDocs.tools.fetch_openai_doc]
approval_mode = "approve"
[mcp_servers.tavily]
command = "npx"
args = ["-y", "tavily-mcp@latest"]
env_vars = ["TAVILY_API_KEY"]
enabled = true
required = false
[mcp_servers.brave_search]
command = "npx"
args = ["-y", "@modelcontextprotocol/server-brave-search"]
env_vars = ["BRAVE_API_KEY"]
enabled = true
required = false
[mcp_servers.exa]
command = "npx"
args = ["-y", "exa-mcp-server"]
env_vars = ["EXA_API_KEY"]
enabled = true
required = false
[sandbox_workspace_write]
network_access = true
+10 -9
View File
@@ -170,12 +170,13 @@ Follow `skill:humanizer-cn` §1-26 strictly:
## Hard Rules ## Hard Rules
1. Every claim has `[src_xxx]` citation 1. MUST: Every claim has `[src_xxx]` citation
2. Every numerical fact has a source 2. MUST: Every numerical fact has a source
3. Counter-evidence section is mandatory (not optional) 3. MUST: Counter-evidence paragraph is mandatory at chapter end. Per skill:evidence-table §"正文中反方证据段落的写作规范", the heading must express a concrete opinion (e.g., "反例:Codexis ECO 并非所有情境都优于 SPOS" or "值得警惕:临床前到 IND 的衰减率"), NOT a mechanical label like "Counter-Evidence" / "反驳证据". Use H2 or H3 heading level consistently; never use bold text as pseudo-heading.
4. Word count ≥85% of quota, or continue searching 4. MUST: Word count ≥85% of quota, or continue searching
5. No scheduling metadata in body text (no "P0 core", "quota: X", "researcher: dr-analyst") 5. MUST: No scheduling metadata in body text (no "P0 core", "quota: X", "researcher: dr-analyst")
6. No SCQA labels (not even implicitly suggested by structure) 6. MUST: No SCQA labels (not even implicitly suggested by structure)
7. ❌ Never fabricate data, URLs, DOIs 7. MUST NOT: Fabricate data, URLs, DOIs
8. ❌ Never use Chinese words for claims (English working language) 8. MUST NOT: Use Chinese words for claims (English working language)
9. ❌ Never delegate to other agents 9. MUST NOT: Delegate to other agents
10. MUST NOT: **Use emoji anywhere in the draft** (no ✅ ❌ 🔶 🔷 ⭐ 🟢 🔴 ⚠️ 💡 📌 🔑 📊 etc.). The PDF font has no glyphs for colored emoji; they render as empty boxes. Use plain text equivalents (e.g., "✓", "×", "注:", "警告:", or descriptive words like "advantages / limitations / example").
+6 -5
View File
@@ -299,8 +299,9 @@ Phase 4 成稿完成
## 禁止事项 ## 禁止事项
- 改写 dr-analyst 已完成的章节正文 - 改写 dr-analyst 已完成的章节正文
- 给每章开头强加 "**Situation**:" "**Complication**:" 等标注 - 给每章开头强加 "**Situation**:" "**Complication**:" 等标注
- 在正文里保留"章节定位 / P0 核心章 / 字数配额 / 研究员" - 在正文里保留"章节定位 / P0 核心章 / 字数配额 / 研究员"
- 参考文献用占位符了事,要确保 dr-reporter 把它填满 - 参考文献用占位符了事,要确保 dr-reporter 把它填满
- 中途调用 dr-chief-editor(它只管 Phase 3 - 中途调用 dr-chief-editor(它只管 Phase 3
- **在正文中使用 emoji**(✅ ❌ 🔶 🔷 ⭐ 🟢 🔴 ⚠️ 💡 📌 🔑 📊 等彩色符号)。PDF 字体无法渲染,会变成方框。用文字或简单符号(✓ × 注: 警告:)代替。
+4 -2
View File
@@ -62,13 +62,15 @@ uv run python scripts/build_glossary.py <slug> --workers 4
```bash ```bash
# 先预览 # 先预览
uv run python scripts/apply_glossary.py <slug> --dry-run uv run python scripts/apply_glossary.py <slug> --input phase4/final_zh.md --dry-run
# 确认无误后应用 # 确认无误后应用
uv run python scripts/apply_glossary.py <slug> uv run python scripts/apply_glossary.py <slug> --input phase4/final_zh.md
``` ```
这会把 glossary 中发现的拼写错误 / 错译直接替换进 `final_zh.md` 这会把 glossary 中发现的拼写错误 / 错译直接替换进 `final_zh.md`
如润色后仍需二次复核,可手动对 `final_zh_polished.md` 再运行一次:
`uv run python scripts/apply_glossary.py <slug> --input phase4/final_zh_polished.md --dry-run`
## Step 6: 润色(Python 脚本) ## Step 6: 润色(Python 脚本)
+60
View File
@@ -0,0 +1,60 @@
---
description: 术语表事实核查。用法 /dr-glossary [slug] [--from phase1|phase2|phase4]。用 Haiku + Serper/Exa 核查每个术语的英文全称和中文译名,识别拼写错误与误译。可在 Phase 2 前或 Phase 4 中运行。
agent: dr-pm
---
你是 dr-pm(项目经理),用户执行了 `/dr-glossary $ARGUMENTS`
## 何时运行
术语核查可以在三个时机运行,意义不同:
- **Phase 1 末 / Phase 2 初**:基于 framework.md 抽取的关键术语(公司/产品/技术名)预先建表,供 dr-analyst 查阅,避免在章节起草阶段就编造拼写错误(Mabwell 拼成 Maywavee 这种)。
- **Phase 2 中**:对 dr-analyst 产出的 ch01-chN.md 里出现的所有专有名词做核查。
- **Phase 4**:对 translate.py 累积的 glossary.json 做核查(当前默认时机)。
## 执行步骤
### Step 1: 定位项目 + 解析模式
```bash
slug="$ARGUMENTS"
# 默认 slug 从最近修改时间选;默认阶段 phase4
```
### Step 2: 准备术语种子列表
根据阶段选择输入:
- `--from phase1`:从 `projects/<slug>/phase1/framework.md` 抽取所有 H2/H3 标题 + 表格中的英文机构名、技术名、药物名。保存到 `projects/<slug>/phase2/terms.txt`(一行一个)。
- `--from phase2`:从 `projects/<slug>/phase2/drafts/*.md` 抽取所有高频英文专有名词(首字母大写、不在常见词词典)。
- `--from phase4`(默认):直接用 `projects/<slug>/phase4/glossary.json`
### Step 3: 调用 build_glossary.py
```bash
uv run python scripts/build_glossary.py <slug> --workers 4 \
--input <输入 glossary 或 terms.txt>
```
观察输出,关注:
- `[✓]` high confidence(自动可信)
- `[~]` medium(人工复核)
- `[?]` low confidence(必须复核)
- `⚠ issue` 字段非空的(通常是拼写错误或误译)
### Step 4: 汇报 + 建议下一步
向用户展示:
- 核查的术语数
- 发现的错误数(按严重度分级:拼写错误 / 译名错误 / 低置信度)
- 具体错误清单(前 10 条)
- 建议:
- 如果在 Phase 2 前 → 把 glossary.json 交给 dr-analyst 作为参考
- 如果在 Phase 4 → 跑 `uv run python scripts/apply_glossary.py <slug>` 把修正回塗到正文
## 注意事项
- build_glossary 需要网络访问,Serper 和 Exa 都需要代理。如遇 SSL EOF,降 `--workers 3`
- Haiku 对极专业或极新兴术语(如 "muRNA", "SBS linker")的判断可能不准,低置信度的条目必须人工复核。
- 别对通用缩写(PDE/ASGPR/LNP)作自动替换——apply_glossary 已有黑名单,但核查结果在 glossary.json 里仍会体现。
+43 -1
View File
@@ -81,11 +81,53 @@ description: 证据矩阵规范。规定每条核心结论必须有对应的证
- 处理建议:保留并注明争议 / 修改措辞 / 删除 - 处理建议:保留并注明争议 / 修改措辞 / 删除
<!-- 如有重大挑战 --> <!-- 如有重大挑战 -->
🚨 CRITICAL<说明> CRITICAL: <说明>
``` ```
--- ---
## 正文中反方证据段落的写作规范(v0.8 新)
### 标题必须观点化,不能叫 "反驳证据 / Counter-Evidence"
**问题诊断**v0.7 发现每章末尾 dr-analyst 会机械地写 `## 反驳证据`,标题重复而空洞,读者看了没有信息增益。
**新规则**:正文反方证据段落的标题必须:
1. **用二级 H2 或三级 H3 标题**(统一层级,禁止用加粗段冒充标题)
2. **包含具体判断**,不要用"反驳证据" / "反方证据" / "Counter-Evidence" 这种模板化命名
3. 至少要回答:**"对前述论点的哪一方面提出了什么挑战?"**
### 可接受的命名示例
| ✗ 不推荐 | ✓ 推荐 |
|---|---|
| 反驳证据 | 另一种声音:管线虚胖还是真实进展? |
| Counter-Evidence | 需要补充判断的副作用:汇聚偶联收率可能被高估 |
| 反方观点 | 反例:Codexis ECO 并非所有情境都优于 SPOS |
| Counter Arguments | 值得警惕的数据:临床前到 IND 的衰减率 |
### 段落结构模板(推荐)
```markdown
## <观点化标题>
虽然上文论证了 <核心观点>,但以下证据提示需要**有限度地**接受这一判断:
1. **<反方论点 1>**<具体数据或案例> [src_xxx]。影响评估:<说明>
2. **<反方论点 2>**<具体数据或案例> [src_xxx]。影响评估:<说明>
综合而言,核心结论仍成立,但需在 <某个具体维度> 上留出缓冲。
```
### 禁止的写法
- 单独用 **加粗段** 冒充反方证据标题(`**反方证据:** ...`
- 反方证据后不做整合判断,只是堆数据
- 在每个小节末尾都加反方证据(只在章末加一次即可;若小节级别有重大挑战,写在小节正文里即可)
---
## 置信度分级标准 ## 置信度分级标准
| 置信度 | 条件 | 正文处理方式 | | 置信度 | 条件 | 正文处理方式 |
+28
View File
@@ -101,6 +101,34 @@ SCQA 要写得隐式融合(见 mckinsey-method skill)。
例外:行内英文术语、代码、URL、数据单位前后保持英文标点合理。 例外:行内英文术语、代码、URL、数据单位前后保持英文标点合理。
### J. Emoji(强制禁用)
**正文与表格中严禁使用任何 emoji / 彩色符号**
禁用清单(但不限于):
`✅ ❌ ✔ ✖ 🔶 🔷 ⭐ 🟢 🔴 🟡 🟠 ⚠️ ⚠ 💡 📌 🔑 📊 📈 📉 🔥 ✨ 🎯 🎉 ➔ ➜`
**原因**:PDF 使用的思源字体子集不包含这些字符的 glyph,渲染为空白方框(□)。
**替代写法**
- 表格标记"有/无":用 `✓` `×`(思源字体支持)或中文字 `是` / `否`
- 强调状态:用 `◆` `●` 等几何符号(字体支持)
- 警示:用 `注:` `警告:` `※` 等文字前缀
- 重点:用 **粗体** 或引用块,不用 emoji
扫描命令:
```bash
python3 -c "
import re
txt = open('final_zh_polished.md').read()
pat = re.compile(r'[\u2700-\u27BF]|[\U0001F300-\U0001F9FF]|[\u2B00-\u2BFF]')
hits = [(i, m.group()) for i, m in enumerate(pat.finditer(txt))]
print(f'emoji 命中:{len(hits)} 处')
for i, c in hits[:10]:
print(f' 位置 {i}: {c!r} (U+{ord(c):04X})')
"
```
--- ---
## 三、参考文献完整性校验(最关键) ## 三、参考文献完整性校验(最关键)
+55 -20
View File
@@ -82,39 +82,74 @@ description: 生物医药深度研究的统一检索策略。规定信源优先
--- ---
## 三、API 调用顺序(技术栈) ## 三、API 调用顺序(技术栈v0.8 更新
**按"查询类型"路由到最合适的 API**,而不是一律走通用搜索。
``` ```
┌─────────────────────────────────────────────┐ ┌──────────────────────────────────────────────────────────────
Phase 1 初扫(dr-searcher 用) 查询类型 首选 备选
│ ├── tavily (MCP) — 快速宽扫 │ ├──────────────────────────────────────────────────────────────┤
├── brave (MCP) — 交叉验证 专利 Serper Patents Google Patents 爬页
└── exa (MCP) — neural search (site:patents.google.com)
│ │ │ │
Phase 2 深研(dr-analyst 用) 学术论文 Serper Scholar PubMed E-utils
├── pubmed esearch/efetch (bash+curl) (带引用数/年份/期刊) (原始数据库)
│ ├── clinicaltrials.gov API (bash+curl) │
│ ├── openfda API (bash+curl) │
│ ├── patentsview / google patents (bash) │
│ └── + Phase 1 的 3 个 MCP 继续用 │
│ │ │ │
Phase 2 反验(dr-verifier 用) 新闻 / 行业动态 Serper News Tavily
└── 与 analyst 相同但查反向关键词 (时效性敏感)
└─────────────────────────────────────────────┘ │ │
│ 临床试验 ClinicalTrials.gov ChiCTR │
│ (原始 API) │
│ │
│ 监管公告 openFDA / FDA 搜索 EMA / NMPA 官网 │
│ │
│ 公司/机构识别 Exa Tavily │
│ (LinkedIn/官网召回强) │
│ │
│ 通用网页 Exa Tavily / Brave │
└──────────────────────────────────────────────────────────────┘
``` ```
### Tavily MCP 调用模板 ### Serpergoogle.serper.dev)使用模板
**专利检索**
```python
from scripts.lib.search_client import SearchClient
with SearchClient() as c:
hits = c.patents("dual-target siRNA GalNAc", num_results=10)
```
**学术论文**
```python
hits = c.scholar("dual-target RNAi 2024", num_results=10, year_low=2023)
# hits[i].snippet 里包含引用数和期刊信息
```
**新闻(时效性)**
```python
hits = c.news("Arrowhead ARO-DIMER-PA clinical trial", time_range="w") # 最近一周
```
### Tavily MCP 调用模板(通用网页 - Phase 1 初扫)
``` ```
工具名:tavily_search 工具名:tavily_search
参数: 参数:
query: "<关键词>" query: "<关键词>"
search_depth: "advanced" # 默认 basic,深度研究用 advanced search_depth: "advanced"
max_results: 10 max_results: 10
include_domains: ["pubmed.ncbi.nlm.nih.gov", "nejm.org", "lancet.com"] # 锁 Tier 1 include_domains: ["pubmed.ncbi.nlm.nih.gov", "nejm.org", "lancet.com"]
exclude_domains: ["baijiahao.baidu.com", "toutiao.com"] # 排黑名单 exclude_domains: ["baijiahao.baidu.com", "toutiao.com"]
time_range: "year" # 或 "month" time_range: "year"
``` ```
### 何时用哪个
- **专利相关问题**"谁拥有技术 X"、"FTO 自由度分析")→ `c.patents()`**永远先走 Google Patents**
- **需要引用数排序的论文**("找影响力最大的 N 篇")→ `c.scholar()`
- **近期新闻**"2025 年 Q4 交易"、"上月 IND 批准")→ `c.news(..., time_range="m")`
- **快速概念验证 / 术语理解** → `c.search()`(通用)
### PubMed E-utilsbash 调用示例) ### PubMed E-utilsbash 调用示例)
```bash ```bash
# 1. esearch 拿 PMID 列表 # 1. esearch 拿 PMID 列表
+258 -99
View File
@@ -49,6 +49,7 @@ try:
Table, Table,
TableStyle, TableStyle,
) )
from reportlab.platypus.flowables import HRFlowable
except ImportError: except ImportError:
print("ERROR: missing reportlab. Run: uv sync", file=sys.stderr) print("ERROR: missing reportlab. Run: uv sync", file=sys.stderr)
sys.exit(1) sys.exit(1)
@@ -159,7 +160,7 @@ def build_styles() -> StyleSheet1:
allowOrphans=0, allowOrphans=0,
)) ))
# H1 (chapter) - page break before, deep blue # H1 (chapter) - page break before, deep blue (used for 前置件标题如"免责声明")
ss.add(ParagraphStyle( ss.add(ParagraphStyle(
name="h1", name="h1",
fontName="SrcSans-Bold", fontName="SrcSans-Bold",
@@ -173,6 +174,35 @@ def build_styles() -> StyleSheet1:
wordWrap="CJK", wordWrap="CJK",
)) ))
# H1 章号(正文章节第一行):小字号居中,浅色
ss.add(ParagraphStyle(
name="h1-chapter-num",
fontName="SrcSans-Medium",
fontSize=13,
leading=20,
alignment=TA_CENTER,
spaceBefore=18,
spaceAfter=6,
textColor=colors.HexColor("#6b7280"),
letterSpacing=3, # 章号加字距,视觉更稳
keepWithNext=1,
wordWrap="CJK",
))
# H1 章名(正文章节第二行):大字号居中加粗深蓝
ss.add(ParagraphStyle(
name="h1-chapter-title",
fontName="SrcSans-Bold",
fontSize=20,
leading=32,
alignment=TA_CENTER,
spaceBefore=0,
spaceAfter=12,
textColor=colors.HexColor("#1e3a8a"),
keepWithNext=1,
wordWrap="CJK",
))
# H2 (section) - blue, no page break, keep with next # H2 (section) - blue, no page break, keep with next
ss.add(ParagraphStyle( ss.add(ParagraphStyle(
name="h2", name="h2",
@@ -612,6 +642,62 @@ def md_inline_to_rl(text: str, *, add_cjk_space: bool = True) -> str:
return text return text
# H1 正文章节标题解析:拆成(章号, 章名)
# 支持:
# 第一章 — 为何... → ("第一章", "为何...")
# 第 9 章:四大监管向量... → ("第 9 章", "四大监管向量...")
# 第6章 — 固定化... → ("第6章", "固定化...")
# 第十章 — 制造体系... → ("第十章", "制造体系...")
# Chapter 1: Why the Second... → ("Chapter 1", "Why the Second...")
# 分隔符:—(em dash / –(en dash / - / : / / 空白多于一处
_CHAPTER_HEAD_RE = re.compile(
r"^\s*"
r"(?P<num>(?:第\s*[一二三四五六七八九十百零〇两廿卅\d]+\s*章)|(?:Chapter\s+\d+))"
r"\s*[—–\-:]\s*"
r"(?P<title>.+?)\s*$",
re.IGNORECASE,
)
def parse_chapter_title(raw: str) -> tuple[str, str] | None:
"""解析章节标题。命中返回 (章号, 章名),否则 None。"""
m = _CHAPTER_HEAD_RE.match(raw.strip())
if not m:
return None
num = m.group("num").strip()
title = m.group("title").strip()
if not title:
return None
# 规范化章号空白:"第 6 章" 保留"第 6 章""第6章"保留"第6章"
num = re.sub(r"\s+", " ", num)
return num, title
def build_chapter_header(raw_title: str, styles: StyleSheet1) -> list:
"""生成正文章节标题:两行居中 + 装饰横线。
解析失败时 fallback 到普通 h1 样式。
"""
parsed = parse_chapter_title(raw_title)
if parsed is None:
return [Paragraph(md_inline_to_rl(raw_title), styles["h1"])]
num, title = parsed
return [
Paragraph(md_inline_to_rl(num), styles["h1-chapter-num"]),
Paragraph(md_inline_to_rl(title), styles["h1-chapter-title"]),
# 装饰横线:居中、宽度约 3cm(视觉重量跟两行标题平衡)
HRFlowable(
width=3 * cm,
thickness=1.2,
color=colors.HexColor("#1e3a8a"),
spaceBefore=2,
spaceAfter=18,
hAlign="CENTER",
),
]
# ============================================================ # ============================================================
# Document builders # Document builders
# ============================================================ # ============================================================
@@ -1106,109 +1192,182 @@ def build_body(
break break
i = first_section_idx # 从第一个 section 开始处理 i = first_section_idx # 从第一个 section 开始处理
def _title_kind(title_raw: str) -> str:
"""识别标题的"语义类别"。无论原文 H1 或 H2,统一归类。
返回:
'abstract' — 摘要(将被跳过)
'appendix' — 附录(将被跳过)
'version_history' — 版本历史(将被跳过)
'toc' — 目录(自动生成)
'references' — 参考文献(自动生成)
'glossary' — 术语表(独立章节)
'disclaimer' — 免责声明(独立章节)
'executive_summary' — 执行摘要(独立章节)
'chapter' — 正文章节(默认)
"""
t = title_raw.strip().lower()
# 跳过类
if t in ("摘要", "abstract"):
return "abstract"
if t.startswith("附录") or t.startswith("appendix"):
return "appendix"
if t in ("版本历史", "version history", "版本"):
return "version_history"
# 自动生成类
if t in ("目录", "table of contents"):
return "toc"
if t in ("参考文献", "references", "bibliography"):
return "references"
# 识别类(带 PageBreak 独立成章)
if t in ("术语表", "glossary"):
return "glossary"
if t in ("免责声明", "disclaimer"):
return "disclaimer"
if t in ("执行摘要", "executive summary", "管理层摘要"):
return "executive_summary"
return "chapter"
def _consume_until_next_section(start: int) -> int:
"""从 start 开始收集内容(非 H1/H2 的 block),返回下一个 H1/H2 的索引。"""
j = start
while j < n and blocks[j].kind not in ("h1", "h2"):
_render_generic_block(blocks[j], story, base_dir, styles, in_summary=in_summary)
j += 1
return j
def _skip_until_next_section(start: int) -> int:
"""从 start 开始跳过内容,返回下一个 H1/H2 的索引。"""
j = start
while j < n and blocks[j].kind not in ("h1", "h2"):
j += 1
return j
# 收集前置件(在第一个"正文 H1 章节"之前的所有内容)按 title_kind 分组。
# 然后按固定顺序输出:免责声明 → 执行摘要 → 目录 → 术语表 → 正文 → 参考文献。
# 这样无论 Markdown 里写的顺序如何,最终排版都一致(目录在术语表之前)。
first_h1_idx = n
for k in range(i, n):
if blocks[k].kind == "h1" and _title_kind(blocks[k].content) == "chapter":
first_h1_idx = k
break
# 收集"前置件段":从 i 到 first_h1_idx
front_sections: dict[str, list[Block]] = {}
def _collect_section(start: int, until: int) -> tuple[str, list[Block], int]:
"""从 start 处的 H1/H2 开始,收集这一 section 直到下一个 H1/H2(或 until)。
返回 (kind, blocks 列表, 下一个 section 的起始索引)。
"""
head = blocks[start]
kind = _title_kind(head.content)
sec = [head]
k = start + 1
while k < until and blocks[k].kind not in ("h1", "h2"):
sec.append(blocks[k])
k += 1
return kind, sec, k
# 在前置件区域内遍历
k = i
while k < first_h1_idx:
b = blocks[k]
if b.kind not in ("h1", "h2"):
k += 1
continue
kind, sec, next_k = _collect_section(k, first_h1_idx)
if kind in ("abstract", "appendix", "version_history"):
pass # 丢弃
elif kind in front_sections:
# 重复出现:保留第一份
pass
else:
front_sections[kind] = sec
k = next_k
# 前置件输出顺序(固定)
front_order = [
"disclaimer", # 免责声明
"executive_summary", # 执行摘要
"toc", # 目录
"glossary", # 术语表
]
def _render_head_section(kind: str, sec: list[Block]) -> None:
"""渲染一个前置件 section。sec[0] 是标题,其余是正文。"""
nonlocal in_summary
# 独立章节前加 PageBreak(但第一个除外,避免封面后空白页)
if len(story) > 0:
story.append(PageBreak())
in_summary = (kind == "executive_summary")
head = sec[0]
# TOC 和 references 调用专门的生成器
if kind == "toc":
story.extend(build_toc(blocks, styles))
return
if kind == "references":
story.extend(build_references(blocks, sources_path, styles))
return
# 其它前置件:H1 样式渲染标题 + 内容
story.append(Paragraph(md_inline_to_rl(head.content), styles["h1"]))
for sub in sec[1:]:
# 跳过占位符段
if sub.kind == "p" and (
_TOC_PLACEHOLDER_RE.search(sub.content)
or _REF_PLACEHOLDER_RE.search(sub.content)
):
continue
_render_generic_block(sub, story, base_dir, styles, in_summary=in_summary)
for kind in front_order:
if kind in front_sections:
_render_head_section(kind, front_sections[kind])
in_summary = False
# 现在输出正文(从 first_h1_idx 开始)
i = first_h1_idx
while i < n: while i < n:
block = blocks[i] block = blocks[i]
# --- H1 处理(非首个;本循环内 first_section_idx 之后的 H1 都是真正的章节 H1)--- if block.kind not in ("h1", "h2"):
if block.kind == "h1":
content = block.content
title_low = content.strip().lower()
# 跳过整章:Abstract(保留 Executive Summary
if title_low in ("摘要", "abstract"):
j = i + 1
while j < n and blocks[j].kind != "h1":
j += 1
i = j
continue
# 所有非被替换的 H1 都 PageBreak
story.append(PageBreak())
if any(k in content for k in ("执行摘要", "Executive Summary", "管理层摘要")):
in_summary = True
else:
in_summary = False
# 目录:已经 PageBreak 了,build_toc 内部再 PageBreak(独立页)
if any(s in title_low for s in ("目录", "table of contents")):
story.extend(build_toc(blocks, styles))
# 跳过紧随其后的占位段
j = i + 1
while j < n and blocks[j].kind == "p" and _TOC_PLACEHOLDER_RE.search(blocks[j].content):
j += 1
i = j
continue
# 术语表:H1 本身已 PageBreak,末尾靠下一个 H1 自然换页
if any(s in title_low for s in ("术语表", "glossary")):
story.append(Paragraph(md_inline_to_rl(content), styles["h1"]))
i += 1
while i < n and blocks[i].kind != "h1":
sub = blocks[i]
_render_generic_block(sub, story, base_dir, styles, in_summary=False)
i += 1
continue
if any(s in title_low for s in ("参考文献", "references")):
story.extend(build_references(blocks, sources_path, styles))
j = i + 1
while j < n and blocks[j].kind == "p" and _REF_PLACEHOLDER_RE.search(blocks[j].content):
j += 1
i = j
continue
story.append(Paragraph(md_inline_to_rl(content), styles["h1"]))
i += 1
continue
# --- H2 同样检测占位符 ---
if block.kind == "h2":
title_low = block.content.strip().lower()
# 跳过整段:Abstract(与 Executive Summary 重复,根据用户偏好保留 Executive Summary
if title_low in ("摘要", "abstract"):
# 跳到下一个 H1/H2
j = i + 1
while j < n and blocks[j].kind not in ("h1", "h2"):
j += 1
i = j
continue
# 目录:前后 PageBreak(独立成页)
if any(s in title_low for s in ("目录", "table of contents")):
story.append(PageBreak())
story.extend(build_toc(blocks, styles))
j = i + 1
while j < n and blocks[j].kind == "p" and _TOC_PLACEHOLDER_RE.search(blocks[j].content):
j += 1
i = j
continue
# 术语表:前 PageBreak,后靠下一个 H1/H2 自然换页
if any(s in title_low for s in ("术语表", "glossary")):
story.append(PageBreak())
story.append(Paragraph(md_inline_to_rl(block.content), styles["h1"]))
i += 1
while i < n and blocks[i].kind not in ("h1", "h2"):
sub = blocks[i]
_render_generic_block(sub, story, base_dir, styles, in_summary=False)
i += 1
continue
if any(s in title_low for s in ("参考文献", "references")):
story.extend(build_references(blocks, sources_path, styles))
j = i + 1
while j < n and blocks[j].kind == "p" and _REF_PLACEHOLDER_RE.search(blocks[j].content):
j += 1
i = j
continue
story.append(Paragraph(md_inline_to_rl(block.content), styles["h2"]))
i += 1
continue
# 非 H1/H2 的 block:统一走 _render_generic_block
_render_generic_block(block, story, base_dir, styles, in_summary=in_summary) _render_generic_block(block, story, base_dir, styles, in_summary=in_summary)
i += 1 i += 1
continue
kind = _title_kind(block.content)
# 跳过类
if kind in ("abstract", "appendix", "version_history"):
i = _skip_until_next_section(i + 1)
continue
# 参考文献:自动生成
if kind == "references":
story.append(PageBreak())
story.extend(build_references(blocks, sources_path, styles))
j = i + 1
while j < n and blocks[j].kind == "p" and _REF_PLACEHOLDER_RE.search(blocks[j].content):
j += 1
i = j
continue
# 目录/术语表不应在正文中出现(已经作为前置件处理)
# 如果原文里在正文中又写了一遍目录/术语表,则跳过
if kind in ("toc", "glossary", "disclaimer", "executive_summary"):
i = _skip_until_next_section(i + 1)
continue
# H1 正文章节(chapter):PageBreak + 章号/章名双行居中 + 装饰线
if block.kind == "h1":
story.append(PageBreak())
story.extend(build_chapter_header(block.content, styles))
else:
# H2 正文小节:h2 样式(不分页)
story.append(Paragraph(md_inline_to_rl(block.content), styles["h2"]))
i += 1
return story return story
+56 -2
View File
@@ -1,8 +1,8 @@
# Deep Research 系统方案(OpenCode 实现) # Deep Research 系统方案(OpenCode 实现)
> 本文件是整套方案的**单一真实源**,中断后续接时从此文件恢复上下文。 > 本文件是整套方案的**单一真实源**,中断后续接时从此文件恢复上下文。
> 最后更新:2026-04-20 > 最后更新:2026-04-24
> 实施阶段:路径 2 — 最小可用先行(MVP > 实施阶段:v0.10 — Codex native adapter(独立于 OpenCode)建设中
--- ---
@@ -492,3 +492,57 @@ OpenCode 的坑:如果只是在主会话里装样子地写"让 X agent 做"
- 反驳证据段标题规范化(建议从"反方证据/Counter-Evidence"改为观点化标题如"另一种声音") - 反驳证据段标题规范化(建议从"反方证据/Counter-Evidence"改为观点化标题如"另一种声音")
- build_glossary 默认放到 Phase 2 阶段运行,在源头拦截错误 - build_glossary 默认放到 Phase 2 阶段运行,在源头拦截错误
- 提示 dr-analyst 加强对公司名/机构名的搜索验证流程 - 提示 dr-analyst 加强对公司名/机构名的搜索验证流程
- 2026-04-24 v0.9**Phase 4 并发提速 + 模型/搜索攻略本 + Codex 兼容**
**目标**:在不破坏 OpenCode 主流程的前提下,把 v0.6 Python 化 Phase 4 进一步提速,并补齐跨平台使用说明。OpenCode 仍是主适配器;Codex 第一阶段只复用 `AGENTS.md` 与 Python 脚本,不复刻 OpenCode subagent。
**Phase 4 并发化**
- `scripts/translate.py` 新增 `--workers`,默认 4;设为 1 时回退串行。
- 翻译阶段改为"稳定术语表快照 + 并发 chunk 翻译 + 事后统一合并 glossary patch",避免多线程同时写 `glossary.json`
- `scripts/polish.py` 新增 `--workers`,默认 4;润色块彼此独立,按完成顺序写 chunk,最终按原始 order 合并。
- `scripts/lib/zenmux_client.py` 增加日志与 usage 聚合锁,避免并发 JSONL 日志交错或 token 统计竞争。
**流程修正**
- 修正 `apply_glossary.py` 默认输入,从 `phase4/final_zh_polished.md` 改为 `phase4/final_zh.md`
- `/dr-finalize` 明确默认顺序:`translate.py → build_glossary.py → apply_glossary.py --input phase4/final_zh.md → polish.py → build_report.py`
- 保留二次修正选项:润色后可手动对 `final_zh_polished.md` 再跑一次 `apply_glossary.py --input phase4/final_zh_polished.md --dry-run`
**模型与搜索攻略本**
- 新增 `docs/model-playbook.md`:定义 premium / balanced / budget / cn-heavy / verifier 五套模型策略。
- 新增 `docs/search-playbook.md`:说明 Tavily / Exa / Brave / Serper / PubMed / ClinicalTrials / FDA/EMA/NMPA / Patents 的使用边界。
- 新增 `configs/model_profiles.yaml``configs/search_profiles.yaml`,作为跨平台、人类和 agent 共用的策略配置参考;当前不强制重构 `.opencode/agents` 自动读取。
**Codex 兼容**
- 新增 `docs/codex-usage.md`,说明 Codex 下如何遵循 `AGENTS.md`、运行 Phase 4 Python 流水线、检查 git staging,避免误提交 `projects/**` 研究产物。
- Codex v1 定位为"审阅/规划/修补/执行脚本";确定性编排继续放在 Python 脚本,OpenCode subagent 调度暂不移植。
**Git 管理要求**
- 本轮迭代应在独立分支推送到 Gitea。
- 提交范围仅限系统文件和文档:`README.md``PLAN.md``scripts/**``docs/**``configs/**`、必要的 `.opencode/commands/**`
- 不提交 `projects/**`、生成的 PDF/DOCX/TXT、一次性研究产物或本地临时脚本。
- 2026-04-24 v0.10**Codex native adapter(独立复刻版)**
**目标**:把 Codex 从"辅助 OpenCode 跑脚本"升级为并列 adapter。OpenCode 继续使用 `.opencode/**`Codex 使用 `.codex/config.toml``.codex/agents/*.toml``.codex/commands/*.md``.agents/skills/**` 和共享 `scripts/**`
**已落地的共享层**
- 新增 `scripts/dr.py` 平台无关 CLI:支持 `status``prompt``glossary``finalize`
- 新增 `scripts/install_codex_adapter.py`:从 `codex_adapter_templates/codex/**` 安装 `.codex/**`,并把 `.opencode/skills/**` 复制到 `.agents/skills/**`
- 新增 `scripts/deploy_check.py`:新环境部署自检;必要时用 `--repair --force` 从模板重建 `.codex/**` 并同步 `.agents/skills/**`
- 新增 `codex_adapter_templates/codex/**`:包含 Codex 项目配置、8 个 custom agents 和命令模板;`dr-run` 是主入口,用 Codex 主线程承担 PM 调度,阶段命令只作为调试和人工接管入口。
- `configs/model_profiles.yaml` 新增 `codex_native` profile,使用 OpenAI 原生 `gpt-5.4` / `gpt-5.4-mini` 角色映射。
- `docs/codex-usage.md` 重写为 Codex native adapter 使用说明。
**设计约定**
- Codex 默认走 OpenAI 原生模型,不依赖 ZenMux provider。
- Codex 不会因 custom agent 文件存在而自动启动 subagent;`dr-run` prompt 必须明确要求主线程 spawn / wait / consolidate。
- Phase 1-3 由 `dr-run` 主线程调度 Codex custom agents 执行;Phase 4 由 `scripts/dr.py finalize` 调确定性 Python 流水线。
- `.opencode/**` 不改不删,避免破坏 OpenCode 已可用流程。
- `.opencode/skills` 将复制到 `.agents/skills`,而非软链接,以保证 Git 与跨机器可移植。
**安装方式**
- 在本机运行 `uv run python scripts/install_codex_adapter.py --force`
- 安装后运行 `/debug-config` 确认 `.codex/config.toml` 被 Codex 加载。
- 自动化研究默认权限:`sandbox_mode = "workspace-write"``approval_policy = "never"``web_search = "live"``sandbox_workspace_write.network_access = true`
- Tavily / Brave / Exa MCP server 在模板中默认 `enabled = true``required = false`;确认本机 key、npm 与网络可用可直接使用,某个服务异常时再单独关闭。
+70 -12
View File
@@ -2,8 +2,8 @@
> 生物医药行业的 AI 驱动深度研究流水线。基于 OpenCode 多 agent 协作,以麦肯锡/德勤式方法论产出专业级研究报告(PDF + DOCX)。 > 生物医药行业的 AI 驱动深度研究流水线。基于 OpenCode 多 agent 协作,以麦肯锡/德勤式方法论产出专业级研究报告(PDF + DOCX)。
**当前状态**MVP(路径 2 — 最小可用先行),仅实现 Phase 1 能力 **当前状态**v0.10 迭代中。OpenCode 全流程可用(Phase 1-4),Phase 4 已切换为 Python 脚本化流水线;Codex native adapter 正在建设为独立于 OpenCode 的并列入口
详见 `PLAN.md` 了解完整方案与迭代路径。 详见 `PLAN.md` 了解完整方案、版本记录与迭代路径。
--- ---
@@ -108,22 +108,23 @@ source scripts/activate.sh
--- ---
## MVP 可用命令 ## 可用命令
| 命令 | 功能 | 状态 | | 命令 | 功能 | 状态 |
|---|---|---| |---|---|---|
| `/dr-init <主题>` | 初始化新研究,启动访谈 | ✅ MVP | | `/dr-init <主题>` | 初始化新研究,启动访谈 | ✅ 可用 |
| `/dr-frame [slug]` | Phase 1:生成 8-15 章研究框架 | ✅ MVP | | `/dr-frame [slug]` | Phase 1:生成 8-15 章双语研究框架 | ✅ 可用 |
| `/dr-research` | Phase 2:深度研究(并行) | ⏳ 下一阶段 | | `/dr-research [slug]` | Phase 2并行深度研究 | ✅ 可用 |
| `/dr-review` | Phase 3:总编审校 | ⏳ 下一阶段 | | `/dr-review [slug]` | Phase 3:总编审校 | ✅ 可用 |
| `/dr-finalize` | Phase 4成稿 PDF+DOCX | ⏳ 下一阶段 | | `/dr-finalize [slug]` | Phase 4英文合稿 → 中文翻译/术语核查/润色 → PDF+DOCX | ✅ 可用 |
| `/dr-status` | 查看进度 | ⏳ 下一阶段 | | `/dr-glossary [slug]` | 术语表事实核查 | ✅ 可用 |
| `/dr-status [slug]` | 查看进度 | ✅ 可用 |
### 典型 MVP 流程 ### 典型流程
``` ```
1. /dr-init GLP-1 减重药物市场 1. /dr-init GLP-1 减重药物市场
→ dr-plan 向你提 6-8 个访谈问题(研究类型、受众、时间范围等) → dr-plan 向你提 8 个访谈问题(研究类型、受众、时间范围等)
→ 你回答后,生成 projects/glp1-obesity-market-2026/manifest.json → 你回答后,生成 projects/glp1-obesity-market-2026/manifest.json
2. /dr-frame 2. /dr-frame
@@ -135,9 +136,66 @@ source scripts/activate.sh
3. 你审核框架,或提修改意见,或直接确认 3. 你审核框架,或提修改意见,或直接确认
→ 确认后,manifest.phase1.approved = true → 确认后,manifest.phase1.approved = true
4. (后续)/dr-research 触发 Phase 2 深研 — 目前未实现 4. /dr-research
→ dr-pm 分批并行调度 dr-analyst 深研
→ dr-verifier 做反方验证
→ 产出 phase2/drafts、evidence、sources.jsonl
5. /dr-review
→ dr-chief-editor 通读审校,产出 phase3/critique.md
6. /dr-finalize
→ dr-editor-in-chief 合并英文终稿
→ Python 脚本执行 translate → glossary → apply_glossary → polish → build_report
→ 产出 final_zh_polished.md、PDF、DOCX
``` ```
### Phase 4 Python 流水线
Phase 4 已不再依赖单个 LLM agent 一次性翻译整篇报告,而是由 Python 控制切块、并发、重试与断点续传:
```bash
uv run python scripts/translate.py <slug> --workers 4
uv run python scripts/build_glossary.py <slug> --workers 4
uv run python scripts/apply_glossary.py <slug> --input phase4/final_zh.md --dry-run
uv run python scripts/apply_glossary.py <slug> --input phase4/final_zh.md
uv run python scripts/polish.py <slug> --workers 4
uv run python scripts/build_report.py <slug>
```
网络不稳或 API 限流时,把 `--workers` 降到 `3``1` 即可断点续跑。
### 多平台兼容
- OpenCode:主适配器,使用 `.opencode/agents``.opencode/commands`
- Codexnative adapter,使用 `.codex/config.toml``.codex/agents``.codex/commands``.agents/skills``scripts/dr.py` 独立运行;主入口是 `dr-run`,由 Codex 主线程承担 PM 调度并主动 spawn subagents,详见 `docs/codex-usage.md`
- Gemini CLI / Claude Code:暂不做强适配,后续以同一套脚本与配置为基础扩展。
安装 Codex adapter
```bash
uv run python scripts/install_codex_adapter.py --force
```
Codex adapter 默认面向自动化研究:workspace 可写、命令不逐次审批、实时 web search 与脚本网络访问开启;Tavily / Brave / Exa MCP 会默认启用但不设为必需服务。
部署到新环境后自检:
```bash
uv run python scripts/deploy_check.py
uv run python scripts/deploy_check.py --repair --force
```
运行 Codex 总调度:
```bash
codex exec "$(uv run python scripts/dr.py prompt dr-run <slug-or-topic>)"
```
模型与搜索 API 选择见:
- `docs/model-playbook.md`
- `docs/search-playbook.md`
--- ---
## 项目结构 ## 项目结构
@@ -0,0 +1,19 @@
name = "dr-analyst"
description = "Chapter deep-research agent that writes English chapter drafts and evidence matrices."
model = "gpt-5.4"
model_reasoning_effort = "high"
sandbox_mode = "workspace-write"
developer_instructions = """
You are dr-analyst.
Work in English. Own exactly one assigned chapter.
Load skills: search-strategy, source-quality, length-budget, evidence-table, mckinsey-method, humanizer-cn.
Write:
- projects/<slug>/phase2/drafts/chXX.md
- projects/<slug>/phase2/evidence/chXX-evidence.md
- append structured sources to projects/<slug>/phase2/sources.jsonl
Every claim and numerical fact needs [src_xxx].
Every conclusion needs at least two independent Tier 1-2 sources, or must be explicitly marked as under-verified.
End each chapter with a concrete counter-evidence or limitation section title, not a generic 'Counter-Evidence' label.
Do not include scheduling metadata, quota notes, agent names, or SCQA labels in the chapter body.
"""
nickname_candidates = ["Analyst A", "Analyst B", "Analyst C", "Analyst D"]
@@ -0,0 +1,15 @@
name = "dr-chief-editor"
description = "Phase 3 read-only editorial reviewer for whole-report logic, evidence, MECE, and quality."
model = "gpt-5.4"
model_reasoning_effort = "xhigh"
sandbox_mode = "read-only"
developer_instructions = """
You are dr-chief-editor.
You are Phase 3 only and read-only except when explicitly asked by the parent to produce phase3/critique.md.
Review all English drafts, evidence files, sources.jsonl, framework.md, and manifest.json.
Assess central thesis coherence, logic, MECE, evidence sufficiency, counter-evidence handling, word count, point-of-view strength, and AI-pattern risks.
Do not rewrite drafts.
Do not enter Phase 4.
Do not spawn subagents.
"""
nickname_candidates = ["Chief Editor"]
@@ -0,0 +1,15 @@
name = "dr-editor-in-chief"
description = "Phase 4 lead editor for English final assembly and deterministic script orchestration."
model = "gpt-5.4"
model_reasoning_effort = "xhigh"
sandbox_mode = "workspace-write"
developer_instructions = """
You are dr-editor-in-chief.
Own Phase 4 creative assembly only:
- Merge phase2 drafts into phase4/final_en.md.
- Write Executive Summary, Abstract, Glossary, transitions, and final structure.
- Remove scheduling metadata and output-hygiene violations.
Do not translate the whole report yourself. Use scripts/dr.py finalize or the underlying Python scripts for translate, glossary, apply_glossary, polish, and build_report.
Keep citations intact.
"""
nickname_candidates = ["Editor in Chief"]
@@ -0,0 +1,18 @@
name = "dr-plan"
description = "Deep Research framework planner for Phase 1 interview, initial scan synthesis, and bilingual research framework."
model = "gpt-5.4"
model_reasoning_effort = "high"
sandbox_mode = "workspace-write"
developer_instructions = """
You are dr-plan for the biomedical Deep Research system.
Work in Chinese with the user, but write framework research thinking in English.
Follow AGENTS.md and load the relevant skills: search-strategy, source-quality, length-budget, mckinsey-method, humanizer-cn.
Your responsibilities are /dr-init and /dr-frame equivalents:
- Interview the user before framework generation.
- Propose formal report titles.
- Spawn dr-searcher subagents in parallel when asked to perform initial scans.
- Generate phase1/framework.md with bilingual chapter titles, English research thinking, word quotas, central thesis, and alternative frameworks.
Do not perform Phase 2 chapter deep research yourself.
Do not enter Phase 4.
"""
nickname_candidates = ["Planner Alpha", "Planner Beta", "Planner Gamma"]
@@ -0,0 +1,18 @@
name = "dr-pm"
description = "Deep Research project manager for Phase 2 batching, analyst/verifier orchestration, and project status."
model = "gpt-5.4"
model_reasoning_effort = "high"
sandbox_mode = "workspace-write"
developer_instructions = """
You are dr-pm for the biomedical Deep Research system.
Use English for Phase 2 working outputs.
Follow AGENTS.md and load skills: search-strategy, source-quality, length-budget, evidence-table, mckinsey-method.
Your responsibilities:
- Read manifest.json and phase1/framework.md.
- Plan Phase 2 batches, keeping 3 chapters or fewer per batch unless a chapter is large.
- Spawn dr-analyst subagents in parallel for chapter drafts.
- Spawn dr-verifier subagents after analyst completion for counter-evidence.
- Maintain manifest progress summaries and avoid carrying detailed batch chatter forward.
- Never write final reports directly.
"""
nickname_candidates = ["PM Alpha", "PM Beta", "PM Gamma"]
@@ -0,0 +1,14 @@
name = "dr-reporter"
description = "Report production agent for PDF/DOCX rendering and final output checks."
model = "gpt-5.4-mini"
model_reasoning_effort = "medium"
sandbox_mode = "workspace-write"
developer_instructions = """
You are dr-reporter.
You are an execution-focused report producer.
Use scripts/build_report.py and .opencode/templates/report-template.py to render PDF/DOCX from final_zh_polished.md or the specified Markdown.
Run output hygiene checks and citation checks where available.
Do not change research conclusions.
Do not invent missing references.
"""
nickname_candidates = ["Reporter"]
@@ -0,0 +1,14 @@
name = "dr-searcher"
description = "Lightweight source discovery agent for initial scans and targeted source finding."
model = "gpt-5.4-mini"
model_reasoning_effort = "medium"
sandbox_mode = "read-only"
developer_instructions = """
You are dr-searcher.
Your job is source discovery only. Do not write project files unless explicitly instructed by the parent.
Load skills: search-strategy and source-quality.
Search English and Chinese keywords, prioritize Tier 1-2 sources, include counter-evidence search terms, and return concise Markdown with URLs/DOIs and source-quality scores.
Do not use Wikipedia as evidence.
Do not fabricate URLs, DOIs, trial IDs, patents, or source ids.
"""
nickname_candidates = ["Searcher A", "Searcher B", "Searcher C", "Searcher D"]
@@ -0,0 +1,15 @@
name = "dr-verifier"
description = "Independent counter-evidence and fact-checking agent for completed chapters."
model = "gpt-5.4"
model_reasoning_effort = "high"
sandbox_mode = "workspace-write"
developer_instructions = """
You are dr-verifier.
Act as an independent devil's advocate. Do not protect the analyst's conclusion.
Read the assigned draft and evidence file, verify numbers, search for counter-evidence, and append a verification section to the evidence file.
Use read-then-rewrite for evidence files. Do not edit chapter drafts.
Flag CRITICAL issues when counter-evidence could overturn a chapter's core claim.
Use Chinese and English searches for China-market claims.
Do not fabricate counter-evidence or sources.
"""
nickname_candidates = ["Verifier A", "Verifier B", "Verifier C"]
@@ -0,0 +1,25 @@
# Codex Command: dr-finalize
You are dr-editor-in-chief. The user requested `/dr-finalize $ARGUMENTS`.
Goal: run Phase 4 in Codex native mode.
Steps:
1. Resolve `$ARGUMENTS` as project slug.
2. Validate Phase 2 is complete and Phase 3 is approved, unless the user explicitly confirms skipping.
3. Assemble `phase4/final_en.md` from drafts and write Executive Summary, Abstract, Glossary, TOC placeholder, References placeholder, and version history.
4. Run deterministic pipeline:
```bash
uv run python scripts/dr.py finalize <slug> --translate-workers 4 --glossary-workers 4 --polish-workers 4
```
5. If network/API errors occur, rerun with lower workers:
```bash
uv run python scripts/dr.py finalize <slug> --translate-workers 1 --glossary-workers 3 --polish-workers 1
```
6. Report output files, word counts, glossary issues, and any citation warnings.
Do not translate or polish the full report manually in one LLM response.
@@ -0,0 +1,17 @@
# Codex Command: dr-frame
You are dr-plan. The user requested `/dr-frame $ARGUMENTS`.
Goal: generate Phase 1 bilingual framework for the target project.
Steps:
1. Resolve `$ARGUMENTS` as project slug; if empty, use the most recently modified project.
2. Read `manifest.json` and validate Phase 1 interview is complete.
3. Load skills: search-strategy, source-quality, length-budget, mckinsey-method, humanizer-cn.
4. Spawn 3-4 `dr-searcher` subagents in parallel for MECE keyword groups. Wait for all results.
5. Synthesize `phase1/initial-scan.md`.
6. Write `phase1/framework.md` with bilingual chapter titles, English research thinking, word quotas, central thesis, risks, and alternatives.
7. Update manifest Phase 1 fields.
8. Stop and ask the user to approve the framework before Phase 2.
Do not do Phase 2 research in this command.
@@ -0,0 +1,26 @@
# Codex Command: dr-glossary
You are dr-pm. The user requested `/dr-glossary $ARGUMENTS`.
Goal: run terminology verification in Codex native mode.
Default command:
```bash
uv run python scripts/dr.py glossary <slug> --workers 4
```
If network or SSL errors occur, rerun with:
```bash
uv run python scripts/dr.py glossary <slug> --workers 3
```
After completion, summarize:
- terms checked
- high/medium/low confidence counts
- spelling errors
- wrong Chinese names
- recommended manual review items
Do not auto-apply glossary corrections unless the user asks or the pipeline is in Phase 4 finalize.
@@ -0,0 +1,14 @@
# Codex Command: dr-init
You are dr-plan. The user requested `/dr-init $ARGUMENTS`.
Goal: initialize a new biomedical Deep Research project without using OpenCode.
Follow AGENTS.md, then:
1. Interview the user with the 8 required questions from AGENTS.md and the existing OpenCode workflow.
2. Propose 3 formal report title/subtitle candidates.
3. After the user chooses, create `projects/<slug>/manifest.json` and the phase directories.
4. Write the interview transcript to `projects/<slug>/phase1/interview.md`.
5. Stop after initialization. Do not run `/dr-frame`.
Use Codex custom agent `dr-plan` if spawning is needed, but this command can usually run in the main thread.
@@ -0,0 +1,18 @@
# Codex Command: dr-research
You are dr-pm. The user requested `/dr-research $ARGUMENTS`.
Goal: run Phase 2 deep research using Codex custom subagents.
Steps:
1. Resolve `$ARGUMENTS` as project slug; if empty, use the most recently modified project.
2. Validate `phase1.approved == true` and framework exists.
3. Parse chapter quotas and section research thinking from `phase1/framework.md`.
4. Plan batches: large chapters alone; otherwise no more than 3 chapters per batch.
5. For each batch, spawn `dr-analyst` subagents in parallel, one per chapter.
6. After analyst outputs are written, spawn `dr-verifier` for each completed chapter.
7. Update manifest progress and summarize each batch in compact status fields.
8. Deduplicate `phase2/sources.jsonl`.
9. Report totals and stop before Phase 3.
Do not write the final report.
@@ -0,0 +1,14 @@
# Codex Command: dr-review
You are dr-chief-editor. The user requested `/dr-review $ARGUMENTS`.
Goal: perform Phase 3 whole-report editorial review.
Steps:
1. Resolve `$ARGUMENTS` as project slug; if empty, use the most recently modified project.
2. Validate `phase2.status == "completed"`.
3. Read framework, all drafts, all evidence files, sources.jsonl, and manifest.
4. Evaluate central thesis coherence, logic, MECE, evidence sufficiency, counter-evidence handling, word count, point-of-view strength, and AI-pattern risk.
5. Write `projects/<slug>/phase3/critique.md`.
6. Report rating A/B/C/D and must-fix items.
7. Stop and wait for user decision. Do not enter Phase 4.
@@ -0,0 +1,77 @@
# Codex Command: dr-run
You are the Deep Research PM main thread for `/dr-run $ARGUMENTS`.
This command is the Codex equivalent of letting `dr-pm` own orchestration. Do not ask the user to run each phase manually. Inspect the project state, continue from the correct phase, spawn the required Codex custom agents, wait for their results, and only pause at the defined human decision gates.
## Resolve Target
1. Treat `$ARGUMENTS` as either a project slug/path or a new research topic.
2. If it matches an existing project, load `projects/<slug>/manifest.json` and continue from its current status.
3. If it is a new topic, run Phase 1 initialization and create the project structure before planning.
4. If `$ARGUMENTS` is empty, use the most recently modified project and confirm the inferred slug in your first status update.
## Global Rules
- Follow `AGENTS.md`, all relevant `.agents/skills/*/SKILL.md`, and the project `manifest.json`.
- Keep `projects/**` research artifacts out of system commits unless the user explicitly asks to commit research output.
- Use Codex subagents deliberately: spawn them when parallel work materially advances the phase, wait for results, and consolidate before moving on.
- Do not simulate subagent work in the main thread when the task calls for independent review, chapter research, or counter-evidence.
- Respect the required pause gates:
- Stop after Phase 1 framework is ready and ask the user to approve or revise it.
- Stop after Phase 3 critique is ready and ask whether to fix, rerun Phase 2, or restart.
- Ask for approval before expensive broad web searches, long-running external API work, or irreversible file operations.
## Phase 1: Framework Planning
Run this phase when there is no approved `phase1/framework.md`.
1. Spawn `dr-plan` to interview the user if the topic is underspecified.
2. Spawn `dr-searcher` workers in parallel for initial source discovery across scientific, regulatory, clinical, commercial, and patent angles.
3. Have `dr-plan` synthesize a MECE framework with chapter-level word budgets and source strategy.
4. Write `phase1/interview.md`, `phase1/initial-scan.md`, and `phase1/framework.md`.
5. Update `manifest.json` and stop for user framework approval.
## Phase 2: Deep Research
Run this phase when `phase1.approved == true` and Phase 2 is incomplete.
1. Act as `dr-pm`: parse `phase1/framework.md`, create chapter batches, and keep the main thread focused on orchestration.
2. Spawn `dr-analyst` subagents in parallel, one per chapter or chapter group depending on size.
3. Require each analyst to write English chapter drafts plus evidence matrices.
4. After analyst outputs are present, spawn independent `dr-verifier` subagents for counter-evidence and source-quality checks.
5. Reconcile verifier findings into the relevant evidence files and chapter TODOs.
6. Deduplicate and normalize `phase2/sources.jsonl`.
7. Update `manifest.json` and continue to Phase 3 unless the evidence base is materially inadequate.
## Phase 3: Chief Editor Review
Run this phase when Phase 2 is complete and Phase 3 is incomplete.
1. Spawn `dr-chief-editor` as a read-only reviewer.
2. Have it assess MECE structure, evidence sufficiency, unsupported claims, source quality, chapter balance, and executive-level narrative.
3. Write `phase3/critique.md`.
4. Stop for user decision: targeted fix, rerun Phase 2 for weak chapters, or restart framework.
## Phase 4: Finalization
Run this phase only after the user approves Phase 3.
1. Spawn `dr-editor-in-chief` to merge English drafts into `phase4/final_en.md`, Executive Summary, Abstract, and glossary seed.
2. Run the deterministic pipeline with:
```bash
uv run python scripts/dr.py finalize <slug>
```
3. If needed, spawn `dr-reporter` for final PDF/DOCX validation and citation backfill checks.
4. Report final artifact paths and remaining risks.
## Status Discipline
Give concise progress updates after each phase or batch. Always say:
- current phase
- agents spawned and why
- files produced or changed
- whether the workflow is continuing or waiting for user decision
@@ -0,0 +1,9 @@
# Codex Command: dr-status
Run the platform-neutral status command:
```bash
uv run python scripts/dr.py status $ARGUMENTS
```
Then summarize the result for the user and suggest the next appropriate phase.
+67
View File
@@ -0,0 +1,67 @@
model = "gpt-5.4"
model_reasoning_effort = "high"
sandbox_mode = "workspace-write"
approval_policy = "never"
project_doc_max_bytes = 65536
web_search = "live"
[features]
multi_agent = true
[agents]
max_threads = 6
max_depth = 1
[profiles.deep-research]
model = "gpt-5.4"
model_reasoning_effort = "high"
sandbox_mode = "workspace-write"
approval_policy = "never"
project_doc_max_bytes = 65536
web_search = "live"
[profiles.deep-research-fast]
model = "gpt-5.4-mini"
model_reasoning_effort = "medium"
sandbox_mode = "workspace-write"
approval_policy = "never"
project_doc_max_bytes = 65536
web_search = "live"
[profiles.deep-research-review]
model = "gpt-5.4"
model_reasoning_effort = "xhigh"
sandbox_mode = "workspace-write"
approval_policy = "never"
project_doc_max_bytes = 65536
web_search = "live"
[mcp_servers.openaiDeveloperDocs]
url = "https://developers.openai.com/mcp"
[mcp_servers.openaiDeveloperDocs.tools.fetch_openai_doc]
approval_mode = "approve"
[mcp_servers.tavily]
command = "npx"
args = ["-y", "tavily-mcp@latest"]
env_vars = ["TAVILY_API_KEY"]
enabled = true
required = false
[mcp_servers.brave_search]
command = "npx"
args = ["-y", "@modelcontextprotocol/server-brave-search"]
env_vars = ["BRAVE_API_KEY"]
enabled = true
required = false
[mcp_servers.exa]
command = "npx"
args = ["-y", "exa-mcp-server"]
env_vars = ["EXA_API_KEY"]
enabled = true
required = false
[sandbox_workspace_write]
network_access = true
+80
View File
@@ -0,0 +1,80 @@
profiles:
premium:
description: "Highest quality for formal deliverables."
roles:
dr_plan: "zenmux-anthropic/claude-opus-4-7"
dr_pm: "zenmux-anthropic/claude-sonnet-4-6"
dr_analyst: "zenmux-anthropic/claude-sonnet-4-6"
dr_chief_editor: "zenmux/google/gemini-3.1-pro-preview"
dr_editor_in_chief: "zenmux-anthropic/claude-opus-4-7"
dr_verifier: "zenmux/openai/gpt-5.4"
translate: "anthropic/claude-sonnet-4.6"
polish: "anthropic/claude-sonnet-4.6"
glossary: "anthropic/claude-haiku-4.5"
balanced:
description: "Default profile for most reports."
roles:
dr_plan: "zenmux-anthropic/claude-opus-4-7"
dr_pm: "zenmux-anthropic/claude-sonnet-4-6"
dr_analyst: "zenmux-anthropic/claude-sonnet-4-6"
dr_chief_editor: "zenmux/google/gemini-3.1-pro-preview"
dr_editor_in_chief: "zenmux-anthropic/claude-opus-4-7"
dr_verifier: "zenmux/qwen/qwen3.6-plus"
translate: "anthropic/claude-sonnet-4.6"
polish: "anthropic/claude-sonnet-4.6"
glossary: "anthropic/claude-haiku-4.5"
budget:
description: "Lower-cost exploratory profile; requires stronger review."
roles:
dr_plan: "zenmux/qwen/qwen3.6-plus"
dr_pm: "zenmux/qwen/qwen3.6-plus"
dr_analyst: "zenmux/deepseek/deepseek-v3.2"
dr_chief_editor: "zenmux/google/gemini-2.5-pro"
dr_editor_in_chief: "zenmux-anthropic/claude-sonnet-4-6"
dr_verifier: "zenmux/minimax/minimax-m2.7"
translate: "anthropic/claude-haiku-4.5"
polish: "anthropic/claude-haiku-4.5"
glossary: "anthropic/claude-haiku-4.5"
cn_heavy:
description: "China-market-heavy research with more Chinese-source verification."
roles:
dr_plan: "zenmux-anthropic/claude-opus-4-7"
dr_pm: "zenmux-anthropic/claude-sonnet-4-6"
dr_analyst: "zenmux-anthropic/claude-sonnet-4-6"
dr_chief_editor: "zenmux/google/gemini-3.1-pro-preview"
dr_editor_in_chief: "zenmux-anthropic/claude-opus-4-7"
dr_verifier: "zenmux/qwen/qwen3.6-plus"
translate: "anthropic/claude-sonnet-4.6"
polish: "anthropic/claude-sonnet-4.6"
glossary: "anthropic/claude-haiku-4.5"
verifier:
description: "Cross-model verification profile; avoid Claude for verifier."
roles:
dr_verifier_primary: "zenmux/openai/gpt-5.4"
dr_verifier_backup_a: "zenmux/qwen/qwen3.6-plus"
dr_verifier_backup_b: "zenmux/minimax/minimax-m2.7"
dr_verifier_backup_c: "zenmux/moonshotai/kimi-k2.5"
codex_native:
description: "OpenAI-native profile for the Codex adapter; independent of OpenCode and ZenMux."
roles:
dr_plan: "gpt-5.4"
dr_pm: "gpt-5.4"
dr_searcher: "gpt-5.4-mini"
dr_analyst: "gpt-5.4"
dr_verifier: "gpt-5.4"
dr_chief_editor: "gpt-5.4"
dr_editor_in_chief: "gpt-5.4"
dr_reporter: "gpt-5.4-mini"
translate: "anthropic/claude-sonnet-4.6"
polish: "anthropic/claude-sonnet-4.6"
glossary: "anthropic/claude-haiku-4.5"
reasoning:
default: "high"
fast: "medium"
review: "xhigh"
defaults:
profile: "balanced"
translate_workers: 4
polish_workers: 4
glossary_workers: 4
+60
View File
@@ -0,0 +1,60 @@
profiles:
biomed_literature:
description: "Mechanism, clinical, systematic review, and biomedical evidence."
order:
- "pubmed_ncbi"
- "clinicaltrials"
- "fda_ema_nmpa"
- "serper_scholar"
- "tavily_exa_gap_fill"
patent_heavy:
description: "IP landscape, freedom-to-operate, and process-route research."
order:
- "google_patents_via_serper"
- "uspto_epo_cnipa"
- "company_disclosures"
- "exa_tavily_family_discovery"
china_market:
description: "China regulatory, company, supply-chain, and market research."
order:
- "nmpa_cde"
- "exchange_disclosures"
- "serper_brave_chinese"
- "exa_company_pages"
- "tavily_gap_fill"
investment:
description: "Public-company, market-size, and transaction-oriented research."
order:
- "sec_exchange_filings"
- "consulting_and_database_reports"
- "company_announcements"
- "serper_news"
- "tavily_gap_fill"
apis:
tavily:
best_for: ["initial_scan", "gap_fill", "llm_friendly_snippets"]
evidence_role: "discovery_only_unless_original_source"
exa:
best_for: ["company_pages", "terminology_check", "long_tail_professional_pages"]
evidence_role: "discovery_or_secondary"
brave:
best_for: ["cross_check", "counter_evidence", "mixed_language_search"]
evidence_role: "discovery_only_unless_original_source"
serper:
best_for: ["google_scholar", "google_patents", "news"]
evidence_role: "discovery_or_secondary"
pubmed_ncbi:
best_for: ["tier1_literature", "systematic_reviews", "meta_analysis"]
evidence_role: "tier1_or_tier2"
clinicaltrials:
best_for: ["trial_registration", "pipeline_status", "endpoint_design"]
evidence_role: "tier1"
regulatory:
best_for: ["approval_status", "label", "safety", "review_documents"]
evidence_role: "tier1"
defaults:
glossary_workers: 4
unstable_network_workers: 3
trace_back_to_tier_1_2: true
+176
View File
@@ -0,0 +1,176 @@
# Codex Native Adapter
> v0.10 起,Codex 不再只是 OpenCode 的辅助执行环境,而是 Deep Research 的并列 adapter。共享核心是 `AGENTS.md``scripts/``configs/``.agents/skills`OpenCode 使用 `.opencode/**`Codex 使用 `.codex/**`
## Architecture
| Layer | Shared | OpenCode | Codex |
|---|---:|---:|---:|
| 方法论 | `AGENTS.md` | ✅ | ✅ |
| Skills | `.agents/skills` | 继续保留 `.opencode/skills` | ✅ |
| Agent 定义 | 否 | `.opencode/agents/*.md` | `.codex/agents/*.toml` |
| 命令入口 | 部分共享脚本 | `.opencode/commands/*.md` | `.codex/commands/*.md` + `scripts/dr.py` |
| Phase 4 确定性流水线 | `scripts/*.py` | ✅ | ✅ |
Codex 官方行为要点:
- 项目级配置放在 `.codex/config.toml`,项目被 trust 后才会加载。
- Codex 会从项目根向当前目录读取 `AGENTS.md`
- repo skills 放在 `.agents/skills/*/SKILL.md`
- custom agents 放在 `.codex/agents/*.toml`
- subagents 只有在主线程明确要求时才会启动。
## Setup
```bash
cd /Users/tankai/Documents/Projects/deep_research
source scripts/activate.sh
```
首次使用 Codex adapter 前确认:
```bash
uv run python scripts/install_codex_adapter.py
find .codex -maxdepth 3 -type f | sort
find .agents/skills -maxdepth 2 -name SKILL.md | sort
uv run python scripts/dr.py status <slug>
```
新机器部署后可以先跑自检:
```bash
uv run python scripts/deploy_check.py
```
如果隐藏目录缺失或 skills 没同步:
```bash
uv run python scripts/deploy_check.py --repair --force
```
默认自动化权限:
- `sandbox_mode = "workspace-write"`:允许写入当前研究 workspace。
- `approval_policy = "never"`:命令执行不逐次弹窗。
- `web_search = "live"`:默认使用实时网络检索。
- `[sandbox_workspace_write].network_access = true`:脚本和 MCP server 默认可访问网络。
- Tavily / Brave / Exa MCP 默认启用,且 `required = false`,某个搜索服务临时不可用时不阻塞 Codex 主流程。
## Codex Commands
Codex custom command templates 位于 `.codex/commands/`。在 CLI 中可以用 `scripts/dr.py prompt` 展开:
```bash
uv run python scripts/dr.py prompt dr-run dual-target-rnai-pipeline-2026
codex exec "$(uv run python scripts/dr.py prompt dr-run dual-target-rnai-pipeline-2026)"
```
推荐入口是 `dr-run`:让 Codex 主线程进入 PM 模式,读取 manifest,判断当前应该继续哪个 phase,并在 Phase 2 主动调度 `dr-analyst` / `dr-verifier` subagents。用户不需要逐个执行每个 phase;只有 Phase 1 框架确认和 Phase 3 审校决策这类人类暂停点需要停下来。
```bash
codex exec "$(uv run python scripts/dr.py prompt dr-run <slug-or-topic>)"
```
分阶段命令保留为调试和人工接管入口:
```bash
codex exec "$(uv run python scripts/dr.py prompt dr-init '<topic>')"
codex exec "$(uv run python scripts/dr.py prompt dr-frame <slug>)"
codex exec "$(uv run python scripts/dr.py prompt dr-research <slug>)"
codex exec "$(uv run python scripts/dr.py prompt dr-review <slug>)"
uv run python scripts/dr.py finalize <slug>
```
Phase 4 推荐走确定性 CLI,而不是让单个 agent 翻译整篇:
```bash
uv run python scripts/dr.py finalize <slug> \
--translate-workers 4 \
--glossary-workers 4 \
--polish-workers 4
```
网络不稳时:
```bash
uv run python scripts/dr.py finalize <slug> \
--translate-workers 1 \
--glossary-workers 3 \
--polish-workers 1
```
## Subagent Usage
Codex 的平台限制是:subagents 不会仅因为 `.codex/agents/*.toml` 存在就自动启动,必须由当前主线程明确要求。`dr-run` 已把这个要求写进 PM promptPhase 1 会调度 `dr-plan` / `dr-searcher`Phase 2 会调度 `dr-analyst` / `dr-verifier`Phase 3 会调度 `dr-chief-editor`
```text
Spawn dr-searcher agents in parallel for four keyword groups, wait for all results, then synthesize phase1/initial-scan.md.
```
推荐映射:
- `dr-plan`:访谈、框架、初扫综合。
- `dr-pm`Phase 2 批次规划与调度。
- `dr-searcher`:轻量检索。
- `dr-analyst`:章节英文深研。
- `dr-verifier`:反方验证,必须独立于 analyst。
- `dr-chief-editor`Phase 3 只读审校。
- `dr-editor-in-chief`Phase 4 合稿与脚本调度。
- `dr-reporter`:出稿执行与格式验证。
## Git Hygiene
本仓库常有大量 `projects/**` 研究产物处于修改状态。Codex adapter 提交时只 stage 系统文件:
```bash
git add .codex .agents/skills scripts/dr.py docs configs README.md PLAN.md
git diff --staged --name-only
```
提交前确认 staged 列表不包含:
- `projects/**`
- 已生成 PDF/DOCX/TXT
- 临时检查脚本或一次性研究产物
## Installing Hidden Directories
如果 Codex 桌面沙盒禁止 agent 写入 `.codex``.agents/skills`,请在本机直接运行:
```bash
uv run python scripts/install_codex_adapter.py --force
```
安装来源:
- `codex_adapter_templates/codex/**``.codex/**`
- `.opencode/skills/**``.agents/skills/**`
安装后,在 Codex 中运行 `/debug-config`,确认 project `.codex/config.toml` 已加载。
## Config Troubleshooting
如果 `.codex/config.toml` 生效后启动报错,先按下面顺序排查:
1. 确认当前 project 已被 Codex trust。未 trust 时,Codex 会跳过项目级 `.codex/**`,此时 `--profile deep-research` 会报 profile 不存在。
2. Tavily / Brave / Exa MCP 默认启用但不是 required。若某个 server 启动异常,先确认对应环境变量存在,再临时把该 server 改成 `enabled = false`
3. 如果要完全离线排障,先把第三方 MCP 全部关掉,只保留 OpenAI Docs MCP 和内置 web search。
4. 如果仍然报错,临时保留最小配置确认 Codex 主体能启动:
```toml
model = "gpt-5.4"
model_reasoning_effort = "high"
sandbox_mode = "workspace-write"
approval_policy = "never"
project_doc_max_bytes = 65536
web_search = "live"
[agents]
max_threads = 6
max_depth = 1
[sandbox_workspace_write]
network_access = true
```
这个最小配置只启用模型、沙盒、项目说明、web search 与 subagent 上限;确认能启动后,再逐个恢复 profiles 和 MCP server。
+80
View File
@@ -0,0 +1,80 @@
# Model Playbook
> v0.9 起,本文件作为模型选择攻略本。`.opencode/opencode.json` 仍是 OpenCode 的模型白名单,`configs/model_profiles.yaml` 是跨平台策略参考。
## Profiles
### premium
用于高价值、长上下文、强叙事任务。成本最高,但最稳。
- dr-planClaude Opus 4.7cache
- dr-editor-in-chiefClaude Opus 4.7cache
- dr-pm / dr-analystClaude Sonnet 4.6cache
- dr-chief-editorGemini 3.1 Pro Preview
- dr-verifierGPT-5.4
- translate / polishClaude Sonnet 4.6
适用:投资报告、管理层决策报告、需要交付 PDF/DOCX 的正式项目。
### balanced
默认推荐。保留关键环节的强模型,把机械任务交给更便宜模型。
- dr-planClaude Opus 4.7cache
- dr-editor-in-chiefClaude Opus 4.7cache
- dr-pm / dr-analystClaude Sonnet 4.6cache
- dr-searcherClaude Haiku 4.5
- dr-verifierQwen3.6 Plus 或 GPT-5.4
- translate / polishClaude Sonnet 4.6`--workers 4`
适用:大多数 10,000-35,000 中文字报告。
### budget
用于探索性研究或内部草稿。牺牲部分风格和深度,换取成本控制。
- dr-planClaude Sonnet 4.6 或 Qwen3.6 Plus
- dr-editor-in-chiefClaude Sonnet 4.6
- dr-analystQwen3.6 Plus / DeepSeek V3.2
- dr-verifierMiniMax M2.7 / Kimi K2.5
- translate / polishQwen3.6 Plus 或 Claude Haiku 4.5(需要人工抽检)
适用:快速扫盘、预研、内部初稿。不建议直接用于最终交付。
### cn-heavy
用于中国市场、NMPA、企业工商/中文媒体密集场景。
- dr-analystClaude Sonnet 4.6 + Qwen3.6 Plus 交叉
- dr-verifierQwen3.6 Plus
- dr-searcherHaiku + Serper/Brave 中文关键词
- translate / polishClaude Sonnet 4.6
适用:中国药企、国产替代、供应链、政策环境、中文专有名词较多的研究。
### verifier
用于交叉模型验证,避免同模型同偏差。
- 首选:GPT-5.4
- 备用:Qwen3.6 Plus
- 低成本:MiniMax M2.7 / Kimi K2.5
规则:dr-verifier 不使用 Claude 家族,除非用户明确接受同源偏差风险。
## Switching Rules
- 高层结构、最终合稿、Executive Summary:优先 Opus。
- 章节深研:Sonnet 是默认;预算敏感时用 Qwen/DeepSeek,但必须加强 verifier。
- 事实核查与术语核查:优先 Haiku + 搜索 API;低置信度条目人工复核。
- 翻译与润色:默认 Sonnet;并发提速优先调 `--workers`,不要盲目换便宜模型。
- 限流时:先降并发,再换模型。
## Operational Defaults
```bash
uv run python scripts/translate.py <slug> --workers 4 --model anthropic/claude-sonnet-4.6
uv run python scripts/polish.py <slug> --workers 4 --model anthropic/claude-sonnet-4.6
uv run python scripts/build_glossary.py <slug> --workers 4 --model anthropic/claude-haiku-4.5
```
+78
View File
@@ -0,0 +1,78 @@
# Search API Playbook
> v0.9 起,本文件作为搜索 API 选择攻略本。搜索返回本身多为发现入口,结论支撑仍以 AGENTS.md 的 Tier 1-2 信源为准。
## Search Sources
### Tavily
- 优点:LLM 友好,摘要质量稳定,适合快速发现方向。
- 用法:初扫、普通网页、报告线索、交叉补漏。
- 风险:不能把普通网页当结论支撑,必须追溯原始来源。
### Exa
- 优点:neural search,对官网、公司页、长尾专业内容召回好。
- 用法:术语核查、公司/产品名纠错、专业网页发现。
- 风险:macOS 代理环境容易 TLS EOF,项目内 `SearchClient` 已使用 `trust_env=False` 绕开系统代理。
### Brave
- 优点:独立搜索引擎,适合与 Tavily/Exa 交叉验证。
- 用法:Phase 1 初扫、反方证据、中文/英文混合搜索。
- 风险:结果质量波动,需要人工筛 Tier。
### Serper
- 优点:Google Search / Scholar / News 代理,免费额度较高。
- 用法:Google Scholar、Google Patents、新闻时效检索。
- 风险:专利是 `site:patents.google.com` 技巧,不等同官方专利库。
### PubMed / NCBI
- 优点:生物医药论文的一手入口。
- 用法:机制、临床、系统综述、meta 分析。
- 风险:无 API key 限流较低;摘要不足以替代全文判断。
### ClinicalTrials.gov / ChiCTR
- 优点:临床试验注册的一手来源。
- 用法:管线、适应症、试验阶段、终点设计、入组状态。
- 风险:注册信息不等于结果;需要结合论文、公司披露、监管文件。
### openFDA / FDA / EMA / NMPA
- 优点:监管公告与标签信息,Tier 1。
- 用法:批准状态、安全性、适应症、审评文件。
- 风险:不同监管地区口径不同,必须注明地区与日期。
### Patents
- 优点:IP 与工艺路线研究的核心证据。
- 用法:Google Patents、USPTO、EPO、CNIPA。
- 风险:专利文本难读,权利要求和实施例要分开判断。
## Recommended Profiles
### biomed_literature
PubMed / NCBI → ClinicalTrials → FDA/EMA/NMPA → Serper Scholar → Tavily/Exa 补漏。
### patent_heavy
Google Patents/Serper → USPTO/EPO/CNIPA → 公司年报/招股书 → Tavily/Exa 补同族专利线索。
### china_market
NMPA/CDE → 港交所/上交所/深交所披露 → 中文专业数据库/媒体 → Brave/Serper 中文搜索。
### investment
SEC/交易所披露 → Evaluate/IQVIA/咨询报告 → 公司公告 → 新闻仅作时效入口。
## Failure Handling
- 大量 SSL/TLS 错误:先把 workers 降到 3,再重跑。
- API 限流:保留缓存结果,断点续跑,不要强制 `--force`
- 搜索返回普通网页:只做线索,继续追原始论文、监管、专利或公司披露。
- 中英文译名冲突:写入 glossary,标 medium/low confidence,交人工复核。
+2 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""根据 glossary 的核查结果,在 final_zh_polished.md 上做精确的文本替换。 """根据 glossary 的核查结果,在 final_zh.md 上做精确的文本替换。
原理 原理
- build_glossary.py 会给每条术语标 `zh`正确中文 `issue`有发现问题 - build_glossary.py 会给每条术语标 `zh`正确中文 `issue`有发现问题
@@ -178,7 +178,7 @@ def main() -> int:
parser = argparse.ArgumentParser(description="用 glossary 修正正文术语") parser = argparse.ArgumentParser(description="用 glossary 修正正文术语")
parser.add_argument("project", help="项目 slug 或路径") parser.add_argument("project", help="项目 slug 或路径")
parser.add_argument( parser.add_argument(
"--input", default="phase4/final_zh_polished.md", "--input", default="phase4/final_zh.md",
help="待修正的 Markdown", help="待修正的 Markdown",
) )
parser.add_argument( parser.add_argument(
+2 -2
View File
@@ -22,7 +22,7 @@
用法 用法
uv run python scripts/build_glossary.py <project_slug> uv run python scripts/build_glossary.py <project_slug>
uv run python scripts/build_glossary.py <project_slug> --workers 6 uv run python scripts/build_glossary.py <project_slug> --workers 4
uv run python scripts/build_glossary.py <project_slug> --only "Mabwell,Maywavee" uv run python scripts/build_glossary.py <project_slug> --only "Mabwell,Maywavee"
uv run python scripts/build_glossary.py <project_slug> --force uv run python scripts/build_glossary.py <project_slug> --force
@@ -184,7 +184,7 @@ def main() -> int:
) )
parser.add_argument("--model", default=DEFAULT_MODEL, help="LLM 模型") parser.add_argument("--model", default=DEFAULT_MODEL, help="LLM 模型")
parser.add_argument( parser.add_argument(
"--workers", type=int, default=6, help="并发度(默认 6Exa 限速 5 QPS" "--workers", type=int, default=4, help="并发度(默认 4;网络不稳时建议降到 3"
) )
parser.add_argument( parser.add_argument(
"--force", action="store_true", "--force", action="store_true",
+146
View File
@@ -0,0 +1,146 @@
#!/usr/bin/env python3
"""引文完整性核查。
检查 final_zh_polished.md或其它正文中的 [src_xxx] 引用与 sources.jsonl 是否一致
- 孤立引用正文有但 sources.jsonl 需要 dr-analyst 补信源或删这处引用
- 孤岛信源sources.jsonl 有但正文无 polish 或润色误删了上下文 dr-analyst 收集了
但没用上
- emoji 扫描正文里不该有 emoji
- 编号格式检查 src_xxx 是否符合规范
用法
uv run python scripts/check_citations.py <project_slug>
uv run python scripts/check_citations.py <project_slug> --md phase4/final_zh.md
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from pathlib import Path
EMOJI_RE = re.compile(
r"[\U0001F000-\U0001FFFF]" # Supplementary Plane emoji
r"|[\u2700-\u27BF]" # Dingbats (✅ ❌)
r"|[\u2600-\u26FF]" # Misc symbols (⭐ ⚠ ☀)
r"|[\u2B00-\u2BFF]" # Misc symbols and arrows
)
# 允许的符号(字体支持)
ALLOWED_SYMBOLS = {
"", "×", "", "", "", "", "", "",
"", "", "", "",
}
SRC_ID_RE = re.compile(r"\[(src_[A-Za-z0-9_\-]+(?:\s*,\s*src_[A-Za-z0-9_\-]+)*)\]")
def resolve_project(arg: str) -> Path:
p = Path(arg)
if p.is_dir():
return p
cand = Path.cwd() / "projects" / arg
if cand.is_dir():
return cand
raise SystemExit(f"project not found: {arg}")
def main() -> int:
parser = argparse.ArgumentParser(description="引文完整性核查")
parser.add_argument("project", help="项目 slug 或路径")
parser.add_argument("--md", default="phase4/final_zh_polished.md")
parser.add_argument("--sources", default="phase2/sources.jsonl")
args = parser.parse_args()
project = resolve_project(args.project)
md_path = project / args.md
src_path = project / args.sources
if not md_path.exists():
raise SystemExit(f"找不到正文:{md_path}")
text = md_path.read_text(encoding="utf-8")
# 1. 收集正文引用(保序去重)
cited: list[str] = []
cited_set: set[str] = set()
for m in SRC_ID_RE.finditer(text):
for sid in m.group(1).split(","):
sid = sid.strip()
if sid and sid not in cited_set:
cited_set.add(sid)
cited.append(sid)
# 2. 收集 sources.jsonl 中的 ID
sources: dict[str, dict] = {}
if src_path.exists():
for line in src_path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line:
continue
try:
r = json.loads(line)
if sid := r.get("id"):
sources[sid] = r
except Exception:
continue
orphan_cites = [s for s in cited if s not in sources]
island_sources = [s for s in sources if s not in cited_set]
matched = [s for s in cited if s in sources]
print(f"=== 引文统计 ===")
print(f" 正文引用(独立 ID):{len(cited_set)}")
print(f" sources.jsonl 收录:{len(sources)}")
print(f" 匹配:{len(matched)}")
print(f" 孤立引用(正文有 sources 无):{len(orphan_cites)}")
print(f" 孤岛信源(sources 有正文无):{len(island_sources)}")
if orphan_cites:
print(f"\n=== 孤立引用(前 20 条)===")
for s in orphan_cites[:20]:
print(f" {s}")
if len(orphan_cites) > 20:
print(f" ...还有 {len(orphan_cites) - 20}")
print(f"\n 处理建议:")
print(f" (a) 如果是 dr-analyst 编造的占位符 → 在正文中删除该引用")
print(f" (b) 如果是信源未收录 → 补到 sources.jsonl")
if island_sources:
print(f"\n=== 孤岛信源(前 20 条)===")
for s in island_sources[:20]:
print(f" {s}{sources[s].get('title', '')[:80]}")
if len(island_sources) > 20:
print(f" ...还有 {len(island_sources) - 20}")
print(f"\n 处理建议:")
print(f" (a) 如果是 polish 阶段误删了使用该信源的段落 → 检查 polish diff")
print(f" (b) 如果是收集多余信源 → 可以保留(build_references 会自动忽略)")
# 3. Emoji 扫描
emoji_hits = []
for m in EMOJI_RE.finditer(text):
c = m.group()
if c not in ALLOWED_SYMBOLS:
line = text[:m.start()].count("\n") + 1
emoji_hits.append((line, c))
if emoji_hits:
print(f"\n=== ⚠ 发现 {len(emoji_hits)} 个 emoji(不允许出现在正文)===")
seen = {}
for line, c in emoji_hits:
seen.setdefault(c, []).append(line)
for c, lines in seen.items():
print(f" U+{ord(c):04X} {c!r}{lines[:5]} 行 等 {len(lines)}")
print(f" 建议用 python3 替换:sed -i '' 's/{list(seen.keys())[0]}//g' {md_path}")
# 返回码:有问题返回非零便于 CI 使用
if orphan_cites or emoji_hits:
return 1
return 0
if __name__ == "__main__":
sys.exit(main())
+183
View File
@@ -0,0 +1,183 @@
#!/usr/bin/env python3
"""Check and repair a Deep Research deployment checkout."""
from __future__ import annotations
import argparse
import os
import shutil
import subprocess
import sys
from pathlib import Path
try:
import tomllib
except ModuleNotFoundError: # pragma: no cover - Python < 3.11 fallback.
tomllib = None # type: ignore[assignment]
REPO_ROOT = Path(__file__).resolve().parent.parent
CODEX_TEMPLATE = REPO_ROOT / "codex_adapter_templates" / "codex"
CODEX_ROOT = REPO_ROOT / ".codex"
OPENCODE_ROOT = REPO_ROOT / ".opencode"
AGENTS_SKILLS = REPO_ROOT / ".agents" / "skills"
OPENCODE_SKILLS = OPENCODE_ROOT / "skills"
REQUIRED_PATHS = [
"AGENTS.md",
"README.md",
"PLAN.md",
".opencode/opencode.json",
".codex/config.toml",
".codex/agents/dr-pm.toml",
".codex/commands/dr-run.md",
".agents/skills/search-strategy/SKILL.md",
"scripts/dr.py",
"scripts/install_codex_adapter.py",
]
REQUIRED_ENV_KEYS = [
"ZENMUX_API_KEY",
"TAVILY_API_KEY",
"BRAVE_API_KEY",
"EXA_API_KEY",
]
def rel(path: Path) -> str:
return str(path.relative_to(REPO_ROOT))
def copy_tree_contents(src: Path, dst: Path, *, force: bool) -> list[Path]:
written: list[Path] = []
if not src.exists():
raise RuntimeError(f"source not found: {rel(src)}")
dst.mkdir(parents=True, exist_ok=True)
for item in src.rglob("*"):
target = dst / item.relative_to(src)
if item.is_dir():
target.mkdir(parents=True, exist_ok=True)
continue
if target.exists() and not force:
continue
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(item, target)
written.append(target)
return written
def parse_env(path: Path) -> dict[str, str]:
values: dict[str, str] = {}
if not path.exists():
return values
for raw in path.read_text(encoding="utf-8").splitlines():
line = raw.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
values[key.strip()] = value.strip().strip('"').strip("'")
return values
def git_tracked(paths: list[str]) -> set[str]:
proc = subprocess.run(
["git", "ls-files", *paths],
cwd=REPO_ROOT,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
check=False,
)
return set(proc.stdout.splitlines())
def check_toml(path: Path, issues: list[str]) -> None:
if not path.exists():
return
if tomllib is None:
issues.append("Python tomllib unavailable; skip TOML parse checks")
return
try:
tomllib.loads(path.read_text(encoding="utf-8"))
except Exception as exc: # noqa: BLE001 - deployment diagnostics.
issues.append(f"{rel(path)} TOML parse failed: {exc}")
def check_deployment() -> int:
issues: list[str] = []
warnings: list[str] = []
for item in REQUIRED_PATHS:
if not (REPO_ROOT / item).exists():
issues.append(f"missing required path: {item}")
tracked = git_tracked([".codex", ".opencode", ".agents/skills"])
for item in REQUIRED_PATHS:
if item.startswith((".codex/", ".opencode/", ".agents/")) and item not in tracked:
warnings.append(f"not tracked by git: {item}")
skill_files = sorted(AGENTS_SKILLS.glob("*/SKILL.md"))
if len(skill_files) < 10:
issues.append(f"expected at least 10 Codex skills, found {len(skill_files)}")
check_toml(CODEX_ROOT / "config.toml", issues)
for path in sorted((CODEX_ROOT / "agents").glob("*.toml")):
check_toml(path, issues)
env_values = {key: os.environ.get(key, "") for key in REQUIRED_ENV_KEYS}
env_values.update({k: v for k, v in parse_env(REPO_ROOT / "secrets.env").items() if not env_values.get(k)})
missing_env = [key for key in REQUIRED_ENV_KEYS if not env_values.get(key)]
if missing_env:
warnings.append("missing optional/required API keys for full automation: " + ", ".join(missing_env))
print("Deep Research deployment check")
print(f" repo: {REPO_ROOT}")
print(f" codex files tracked: {sum(1 for p in tracked if p.startswith('.codex/'))}")
print(f" opencode files tracked: {sum(1 for p in tracked if p.startswith('.opencode/'))}")
print(f" codex skills: {len(skill_files)}")
if warnings:
print("\nWarnings:")
for item in warnings:
print(f" - {item}")
if issues:
print("\nIssues:")
for item in issues:
print(f" - {item}")
return 1
print("\nDeployment check passed.")
return 0
def repair(force: bool) -> int:
try:
codex_written = copy_tree_contents(CODEX_TEMPLATE, CODEX_ROOT, force=force)
skills_written = copy_tree_contents(OPENCODE_SKILLS, AGENTS_SKILLS, force=force)
except PermissionError as exc:
print(f"repair failed: permission denied: {exc}", file=sys.stderr)
return 1
except RuntimeError as exc:
print(f"repair failed: {exc}", file=sys.stderr)
return 1
print("Repair completed.")
print(f" .codex files written: {len(codex_written)}")
print(f" .agents skills written: {len(skills_written)}")
return check_deployment()
def main() -> int:
parser = argparse.ArgumentParser(description="Check or repair Deep Research deployment files")
parser.add_argument("--repair", action="store_true", help="copy Codex templates and skills into hidden dirs")
parser.add_argument("--force", action="store_true", help="overwrite existing files during --repair")
args = parser.parse_args()
if args.repair:
return repair(force=args.force)
return check_deployment()
if __name__ == "__main__":
raise SystemExit(main())
+238
View File
@@ -0,0 +1,238 @@
#!/usr/bin/env python3
"""Platform-neutral Deep Research CLI for Codex and other adapters.
This CLI intentionally keeps deterministic orchestration in Python while
allowing Codex/OpenCode/Gemini/Claude Code to provide the agentic layer.
"""
from __future__ import annotations
import argparse
import json
import re
import subprocess
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
PROJECTS_DIR = REPO_ROOT / "projects"
CODEX_COMMANDS_DIR = REPO_ROOT / ".codex" / "commands"
CODEX_COMMAND_TEMPLATES_DIR = REPO_ROOT / "codex_adapter_templates" / "codex" / "commands"
def resolve_project(project: str | None) -> Path:
if project:
p = Path(project)
if p.is_dir():
return p.resolve()
cand = PROJECTS_DIR / project
if cand.is_dir():
return cand.resolve()
raise SystemExit(f"project not found: {project}")
manifests = sorted(
PROJECTS_DIR.glob("*/manifest.json"),
key=lambda p: p.stat().st_mtime,
reverse=True,
)
if not manifests:
raise SystemExit("no projects found")
return manifests[0].parent.resolve()
def load_manifest(project_root: Path) -> dict:
path = project_root / "manifest.json"
if not path.exists():
raise SystemExit(f"manifest not found: {path}")
return json.loads(path.read_text(encoding="utf-8"))
def count_words(text: str) -> int:
return len(re.findall(r"[A-Za-z]+(?:[-'][A-Za-z]+)*", text))
def count_chinese_chars(text: str) -> int:
return sum(1 for c in text if "\u4e00" <= c <= "\u9fff")
def file_state(path: Path) -> str:
return "yes" if path.exists() else "no"
def run_cmd(cmd: list[str], *, dry_run: bool) -> int:
printable = " ".join(cmd)
print(f"$ {printable}")
if dry_run:
return 0
return subprocess.run(cmd, cwd=REPO_ROOT, check=False).returncode
def cmd_status(args: argparse.Namespace) -> int:
project_root = resolve_project(args.project)
manifest = load_manifest(project_root)
slug = project_root.name
drafts = sorted((project_root / "phase2" / "drafts").glob("ch*.md"))
evidence = sorted((project_root / "phase2" / "evidence").glob("ch*-evidence.md"))
sources = project_root / "phase2" / "sources.jsonl"
final_en = project_root / "phase4" / "final_en.md"
final_zh = project_root / "phase4" / "final_zh.md"
final_zh_polished = project_root / "phase4" / "final_zh_polished.md"
glossary = project_root / "phase4" / "glossary.json"
en_words = count_words(final_en.read_text(encoding="utf-8")) if final_en.exists() else 0
zh_chars = count_chinese_chars(final_zh_polished.read_text(encoding="utf-8")) if final_zh_polished.exists() else 0
source_count = 0
if sources.exists():
source_count = sum(1 for line in sources.read_text(encoding="utf-8").splitlines() if line.strip())
print(f"Project: {manifest.get('topic', slug)}")
print(f"Slug: {slug}")
print(f"Title: {manifest.get('report_title', '(unset)')}")
print(f"Type: {manifest.get('type', '(unset)')}")
print()
print("Phases:")
for phase in ("phase1", "phase2", "phase3", "phase4"):
p = manifest.get(phase, {})
print(f" {phase}: {p.get('status', 'pending')} approved={p.get('approved', False)}")
print()
print("Artifacts:")
print(f" framework: {file_state(project_root / 'phase1' / 'framework.md')}")
print(f" drafts: {len(drafts)}")
print(f" evidence files: {len(evidence)}")
print(f" sources: {source_count}")
print(f" final_en.md: {file_state(final_en)} ({en_words:,} words)")
print(f" final_zh.md: {file_state(final_zh)}")
print(f" final_zh_polished.md: {file_state(final_zh_polished)} ({zh_chars:,} Chinese chars)")
print(f" glossary.json: {file_state(glossary)}")
return 0
def cmd_prompt(args: argparse.Namespace) -> int:
name = args.command
if not name.startswith("dr-"):
name = f"dr-{name}"
path = CODEX_COMMANDS_DIR / f"{name}.md"
if not path.exists():
fallback = CODEX_COMMAND_TEMPLATES_DIR / f"{name}.md"
if fallback.exists():
path = fallback
else:
raise SystemExit(f"Codex command template not found: {path}")
text = path.read_text(encoding="utf-8")
if args.argument:
text = text.replace("$ARGUMENTS", args.argument)
else:
text = text.replace("$ARGUMENTS", "")
print(text)
return 0
def cmd_glossary(args: argparse.Namespace) -> int:
project_root = resolve_project(args.project)
cmd = [
sys.executable,
str(REPO_ROOT / "scripts" / "build_glossary.py"),
str(project_root),
"--workers",
str(args.workers),
]
if args.force:
cmd.append("--force")
if args.only:
cmd += ["--only", args.only]
if args.input:
cmd += ["--input", args.input]
if args.output:
cmd += ["--output", args.output]
return run_cmd(cmd, dry_run=args.dry_run)
def cmd_finalize(args: argparse.Namespace) -> int:
project_root = resolve_project(args.project)
steps = [
[
sys.executable,
str(REPO_ROOT / "scripts" / "translate.py"),
str(project_root),
"--workers",
str(args.translate_workers),
],
[
sys.executable,
str(REPO_ROOT / "scripts" / "build_glossary.py"),
str(project_root),
"--workers",
str(args.glossary_workers),
],
[
sys.executable,
str(REPO_ROOT / "scripts" / "apply_glossary.py"),
str(project_root),
"--input",
"phase4/final_zh.md",
],
[
sys.executable,
str(REPO_ROOT / "scripts" / "polish.py"),
str(project_root),
"--workers",
str(args.polish_workers),
],
[
sys.executable,
str(REPO_ROOT / "scripts" / "build_report.py"),
str(project_root),
],
]
for step in steps:
rc = run_cmd(step, dry_run=args.dry_run)
if rc != 0:
return rc
return 0
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Deep Research platform-neutral CLI")
sub = parser.add_subparsers(dest="cmd", required=True)
status = sub.add_parser("status", help="Show project status")
status.add_argument("project", nargs="?", help="Project slug or path")
status.set_defaults(func=cmd_status)
prompt = sub.add_parser("prompt", help="Print a Codex command prompt template")
prompt.add_argument("command", help="Command name, e.g. dr-frame or frame")
prompt.add_argument("argument", nargs="?", help="Replacement for $ARGUMENTS")
prompt.set_defaults(func=cmd_prompt)
glossary = sub.add_parser("glossary", help="Run glossary verification")
glossary.add_argument("project", help="Project slug or path")
glossary.add_argument("--workers", type=int, default=4)
glossary.add_argument("--force", action="store_true")
glossary.add_argument("--only")
glossary.add_argument("--input")
glossary.add_argument("--output")
glossary.add_argument("--dry-run", action="store_true")
glossary.set_defaults(func=cmd_glossary)
finalize = sub.add_parser("finalize", help="Run Phase 4 deterministic pipeline")
finalize.add_argument("project", help="Project slug or path")
finalize.add_argument("--translate-workers", type=int, default=4)
finalize.add_argument("--glossary-workers", type=int, default=4)
finalize.add_argument("--polish-workers", type=int, default=4)
finalize.add_argument("--dry-run", action="store_true")
finalize.set_defaults(func=cmd_finalize)
return parser
def main() -> int:
parser = build_parser()
args = parser.parse_args()
return args.func(args)
if __name__ == "__main__":
raise SystemExit(main())
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env python3
"""Install the Codex native adapter files into hidden project directories.
The Codex desktop sandbox may block agent-created writes into `.codex` and
`.agents/skills`. Run this script locally from the repository root when that
happens.
"""
from __future__ import annotations
import argparse
import shutil
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
TEMPLATE_ROOT = REPO_ROOT / "codex_adapter_templates" / "codex"
CODEX_ROOT = REPO_ROOT / ".codex"
AGENTS_SKILLS = REPO_ROOT / ".agents" / "skills"
OPENCODE_SKILLS = REPO_ROOT / ".opencode" / "skills"
def copy_tree_contents(src: Path, dst: Path, *, force: bool) -> list[Path]:
written: list[Path] = []
if not src.exists():
raise SystemExit(f"template source not found: {src}")
dst.mkdir(parents=True, exist_ok=True)
for item in src.rglob("*"):
rel = item.relative_to(src)
target = dst / rel
if item.is_dir():
target.mkdir(parents=True, exist_ok=True)
continue
if target.exists() and not force:
continue
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(item, target)
written.append(target)
return written
def main() -> int:
parser = argparse.ArgumentParser(description="Install Codex native adapter")
parser.add_argument("--force", action="store_true", help="overwrite existing adapter files")
parser.add_argument("--skip-skills", action="store_true", help="do not copy .opencode/skills to .agents/skills")
args = parser.parse_args()
codex_written = copy_tree_contents(TEMPLATE_ROOT, CODEX_ROOT, force=args.force)
skills_written: list[Path] = []
if not args.skip_skills:
skills_written = copy_tree_contents(OPENCODE_SKILLS, AGENTS_SKILLS, force=args.force)
print("Codex adapter installed.")
print(f" .codex files written: {len(codex_written)}")
print(f" .agents skills files written: {len(skills_written)}")
if codex_written:
for path in codex_written:
print(f" {path.relative_to(REPO_ROOT)}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+98 -13
View File
@@ -115,11 +115,20 @@ class TavilyClient:
class SearchClient: class SearchClient:
"""统一搜索门面:先用 Exa,失败/配额问题降级 Tavily。""" """统一搜索门面,支持多路由:
- `search(query)`通用网页搜索优先 Exa 降级 Tavily
- `patents(query)`专利检索 SerperGoogle Patents失败则通用搜索补刀
- `scholar(query)`学术论文 Serper Scholar失败则通用搜索补刀
- `news(query)`新闻检索 Serper News失败则通用搜索补刀
所有客户端都延迟导入 serper_client避免没装 SERPAPI_KEY import
"""
def __init__(self) -> None: def __init__(self) -> None:
self._exa: ExaClient | None = None self._exa: ExaClient | None = None
self._tavily: TavilyClient | None = None self._tavily: TavilyClient | None = None
self._serper = None # 惰性实例化
try: try:
self._exa = ExaClient() self._exa = ExaClient()
except SearchError: except SearchError:
@@ -133,11 +142,26 @@ class SearchClient:
"neither EXA_API_KEY nor TAVILY_API_KEY available" "neither EXA_API_KEY nor TAVILY_API_KEY available"
) )
def _get_serper(self):
"""惰性创建 SerperClient。没 key 时返回 None。"""
if self._serper is False:
return None
if self._serper is None:
try:
from scripts.lib.serper_client import SerperClient
self._serper = SerperClient()
except Exception:
self._serper = False
return None
return self._serper
def close(self) -> None: def close(self) -> None:
if self._exa: if self._exa:
self._exa.close() self._exa.close()
if self._tavily: if self._tavily:
self._tavily.close() self._tavily.close()
if self._serper and self._serper is not False:
self._serper.close()
def __enter__(self) -> "SearchClient": def __enter__(self) -> "SearchClient":
return self return self
@@ -146,17 +170,11 @@ class SearchClient:
self.close() self.close()
def search(self, query: str, *, num_results: int = 5) -> list[SearchHit]: def search(self, query: str, *, num_results: int = 5) -> list[SearchHit]:
# 优先 Exa """通用网页搜索。Exa 首选,Tavily 备选。"""
if self._exa: if self._exa:
try: try:
return self._exa.search(query, num_results=num_results) return self._exa.search(query, num_results=num_results)
except SearchError as e: except SearchError:
msg = str(e).lower()
if "exceed" in msg or "quota" in msg or "429" in msg or "402" in msg:
# 降级
pass
else:
# 其它错误继续往下试
pass pass
if self._tavily: if self._tavily:
try: try:
@@ -165,13 +183,80 @@ class SearchClient:
pass pass
return [] return []
def patents(self, query: str, *, num_results: int = 10) -> list[SearchHit]:
"""专利检索:Serper 走 Google Patents 最准。降级到通用搜索 + site 限定。"""
serper = self._get_serper()
if serper:
try:
hits = serper.patents(query, num_results=num_results)
return [SearchHit(h.title, h.url, h.snippet) for h in hits]
except Exception:
pass
# 降级:通用搜索加 site 限定
return self.search(f"site:patents.google.com {query}", num_results=num_results)
def scholar(
self,
query: str,
*,
num_results: int = 10,
year_low: int | None = None,
) -> list[SearchHit]:
"""学术论文:Serper Scholar 带引用数。降级到通用搜索。"""
serper = self._get_serper()
if serper:
try:
hits = serper.scholar(query, num_results=num_results, year_low=year_low)
return [
SearchHit(
title=h.title,
url=h.url,
snippet=f"{h.snippet} | {h.source} | 引用 {h.cited_by}" if h.cited_by else h.snippet,
)
for h in hits
]
except Exception:
pass
return self.search(query, num_results=num_results)
def news(
self,
query: str,
*,
num_results: int = 10,
time_range: str | None = None,
) -> list[SearchHit]:
"""新闻检索:Serper News。降级到通用搜索。"""
serper = self._get_serper()
if serper:
try:
hits = serper.news(query, num_results=num_results, time_range=time_range)
return [
SearchHit(
title=h.title,
url=h.url,
snippet=f"{h.snippet} | {h.source} | {h.date}" if h.date else h.snippet,
)
for h in hits
]
except Exception:
pass
return self.search(query, num_results=num_results)
if __name__ == "__main__": if __name__ == "__main__":
from scripts.lib.zenmux_client import load_secrets from scripts.lib.zenmux_client import load_secrets
load_secrets() load_secrets()
with SearchClient() as c: with SearchClient() as c:
hits = c.search("Mabwell 迈威生物 biopharmaceutical", num_results=3) print("--- 通用: Mabwell 迈威生物 ---")
for i, h in enumerate(hits, 1): for h in c.search("Mabwell 迈威生物 biopharmaceutical", num_results=3):
print(f"[{i}] {h.title[:80]}") print(f" {h.title[:80]}")
print(f" {h.url}")
print("\n--- 专利: dual-target siRNA ---")
for h in c.patents("dual-target siRNA GalNAc", num_results=3):
print(f" {h.title[:80]}")
print(f" {h.url}")
print("\n--- Scholar: dual-target RNAi 2024 ---")
for h in c.scholar("dual-target RNAi drug", num_results=3, year_low=2023):
print(f" {h.title[:80]}")
print(f" {h.url}") print(f" {h.url}")
print(f" {h.snippet[:160]}")
+190
View File
@@ -0,0 +1,190 @@
"""Serper.dev 客户端(Google Search API 代理)。
为什么用 Serper
- 2500 次免费额度远超 SerpAPI 100/
- 支持 Google SearchScholarNewsImagesMaps
- Google Patents 无专用 endpoint但可用 `site:patents.google.com` 技巧
- 价格比 SerpAPI 便宜 3-5×
用途
- 专利检索通用 search + `site:patents.google.com`
- 学术论文/scholar endpoint
- 新闻/news endpoint时效性敏感的行业动态
httpx 客户端使用 trust_env=False 绕过系统 socks 代理macOS Clash 会导致 TLS EOF
"""
from __future__ import annotations
import os
from dataclasses import dataclass
from typing import Any, Literal
import httpx
SERPER_BASE = "https://google.serper.dev"
@dataclass
class SerperHit:
title: str
url: str
snippet: str
source: str = "" # 论文出处 / 新闻媒体
date: str = "" # 发表日期(如 scholar / news 返回的话)
cited_by: int = 0 # 学术论文的引用数(仅 scholar)
class SerperError(RuntimeError):
pass
class SerperClient:
def __init__(self, api_key: str | None = None, timeout: float = 30.0) -> None:
self.api_key = api_key or os.environ.get("SERPAPI_KEY") or os.environ.get("SERPER_API_KEY")
if not self.api_key:
raise SerperError("SERPAPI_KEY / SERPER_API_KEY not set")
self._client = httpx.Client(trust_env=False, timeout=timeout)
def close(self) -> None:
self._client.close()
def __enter__(self) -> "SerperClient":
return self
def __exit__(self, *_args: Any) -> None:
self.close()
def _post(self, path: str, body: dict) -> dict:
try:
r = self._client.post(
f"{SERPER_BASE}{path}",
json=body,
headers={
"X-API-KEY": self.api_key,
"Content-Type": "application/json",
},
)
except httpx.RequestError as e:
raise SerperError(f"network error: {e}")
if r.status_code != 200:
raise SerperError(f"HTTP {r.status_code}: {r.text[:300]}")
try:
return r.json()
except Exception as e:
raise SerperError(f"invalid JSON: {e}")
def search(
self,
query: str,
*,
num_results: int = 10,
gl: str = "us",
hl: str = "en",
) -> list[SerperHit]:
"""通用 Google 搜索。支持 site: / filetype: / 引号短语等 Google 高级语法。"""
data = self._post("/search", {
"q": query,
"num": num_results,
"gl": gl,
"hl": hl,
})
hits: list[SerperHit] = []
for item in (data.get("organic") or [])[:num_results]:
hits.append(SerperHit(
title=(item.get("title") or "")[:200],
url=item.get("link") or "",
snippet=(item.get("snippet") or "")[:600],
date=item.get("date") or "",
))
return hits
def scholar(
self,
query: str,
*,
num_results: int = 10,
year_low: int | None = None,
year_high: int | None = None,
) -> list[SerperHit]:
"""Google Scholar 搜索——学术论文首选。
返回带引用数发表年份等元数据权威信源识别更准确
"""
body: dict[str, Any] = {"q": query, "num": num_results}
if year_low is not None:
body["tbs"] = f"cdr:1,cd_min:{year_low}" + (f",cd_max:{year_high}" if year_high else "")
data = self._post("/scholar", body)
hits: list[SerperHit] = []
for item in (data.get("organic") or [])[:num_results]:
hits.append(SerperHit(
title=(item.get("title") or "")[:200],
url=item.get("link") or "",
snippet=(item.get("snippet") or "")[:600],
source=(item.get("publicationInfo") or "")[:200],
year=item.get("year") or "",
cited_by=item.get("citedBy") or 0,
) if False else SerperHit(
title=(item.get("title") or "")[:200],
url=item.get("link") or "",
snippet=(item.get("snippet") or "")[:600],
source=(item.get("publicationInfo") or "")[:200],
date=str(item.get("year") or ""),
cited_by=item.get("citedBy") or 0,
))
return hits
def patents(
self,
query: str,
*,
num_results: int = 10,
) -> list[SerperHit]:
"""Google Patents 检索——用 site: 技巧走通用搜索。
serper.dev 没有专门的 patents endpoint `site:patents.google.com` 效果很好
"""
combined = f"site:patents.google.com {query}"
return self.search(combined, num_results=num_results)
def news(
self,
query: str,
*,
num_results: int = 10,
time_range: Literal["d", "w", "m", "y"] | None = None,
) -> list[SerperHit]:
"""Google News 搜索——时效敏感行业动态。
time_range: d=24h, w=7d, m=30d, y=1y
"""
body: dict[str, Any] = {"q": query, "num": num_results}
if time_range:
body["tbs"] = f"qdr:{time_range}"
data = self._post("/news", body)
hits: list[SerperHit] = []
for item in (data.get("news") or [])[:num_results]:
hits.append(SerperHit(
title=(item.get("title") or "")[:200],
url=item.get("link") or "",
snippet=(item.get("snippet") or "")[:600],
source=(item.get("source") or "")[:200],
date=item.get("date") or "",
))
return hits
if __name__ == "__main__":
from scripts.lib.zenmux_client import load_secrets
load_secrets()
with SerperClient() as c:
print("=== Patents: dual-target siRNA ===")
for h in c.patents("dual-target siRNA GalNAc conjugate", num_results=3):
print(f" {h.title[:70]}")
print(f" {h.url}")
print("\n=== Scholar: dual-target RNAi ===")
for h in c.scholar("dual-target RNAi drug 2024", num_results=3):
print(f" {h.title[:70]} [引用 {h.cited_by}] ({h.date})")
print(f" {h.url}")
print(f" 源: {h.source[:80]}")
+5
View File
@@ -15,6 +15,7 @@ from __future__ import annotations
import json import json
import os import os
import sys import sys
import threading
import time import time
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
@@ -111,6 +112,8 @@ class ZenMuxClient:
self.log_file = log_file self.log_file = log_file
self.usage = UsageStats() self.usage = UsageStats()
self._client = httpx.Client(timeout=timeout) self._client = httpx.Client(timeout=timeout)
self._log_lock = threading.Lock()
self._usage_lock = threading.Lock()
def close(self) -> None: def close(self) -> None:
self._client.close() self._client.close()
@@ -125,6 +128,7 @@ class ZenMuxClient:
if not self.log_file: if not self.log_file:
return return
self.log_file.parent.mkdir(parents=True, exist_ok=True) self.log_file.parent.mkdir(parents=True, exist_ok=True)
with self._log_lock:
with self.log_file.open("a", encoding="utf-8") as f: with self.log_file.open("a", encoding="utf-8") as f:
f.write(json.dumps(payload, ensure_ascii=False) + "\n") f.write(json.dumps(payload, ensure_ascii=False) + "\n")
@@ -191,6 +195,7 @@ class ZenMuxClient:
except Exception as e: except Exception as e:
raise ZenMuxError(f"invalid JSON from zenmux: {e}; body={resp.text[:500]}") raise ZenMuxError(f"invalid JSON from zenmux: {e}; body={resp.text[:500]}")
usage = data.get("usage", {}) or {} usage = data.get("usage", {}) or {}
with self._usage_lock:
self.usage.add(model, usage) self.usage.add(model, usage)
content = "" content = ""
choices = data.get("choices") or [] choices = data.get("choices") or []
+52 -13
View File
@@ -19,6 +19,7 @@
from __future__ import annotations from __future__ import annotations
import argparse import argparse
import concurrent.futures
import json import json
import sys import sys
import time import time
@@ -141,6 +142,12 @@ def main() -> int:
default=None, default=None,
help="最多润色前 N 个未缓存的块(调试用)", help="最多润色前 N 个未缓存的块(调试用)",
) )
parser.add_argument(
"--workers",
type=int,
default=4,
help="并发润色 worker 数(默认 4;设为 1 回退串行)",
)
args = parser.parse_args() args = parser.parse_args()
load_secrets() load_secrets()
@@ -167,27 +174,34 @@ def main() -> int:
total_cn = sum(count_chinese_chars(b.content) for b in blocks) total_cn = sum(count_chinese_chars(b.content) for b in blocks)
print(f"Source: {src_path.relative_to(project_root)}") print(f"Source: {src_path.relative_to(project_root)}")
print(f"Blocks: {len(blocks)} | total Chinese chars: {total_cn:,}") print(f"Blocks: {len(blocks)} | total Chinese chars: {total_cn:,}")
print(f"Model: {args.model} | temperature: {args.temperature}") workers = max(1, args.workers)
print(f"Model: {args.model} | temperature: {args.temperature} | workers: {workers}")
print() print()
start = time.time() start = time.time()
translated_this_run = 0 todo: list[MarkdownBlock] = []
cached_count = 0
notes_records: list[dict] = [] notes_records: list[dict] = []
with ZenMuxClient(log_file=log_file) as client:
for b in blocks: for b in blocks:
chunk_path = chunks_dir / f"{b.order:03d}-{b.anchor}.md" chunk_path = chunks_dir / f"{b.order:03d}-{b.anchor}.md"
if only_orders is not None and b.order not in only_orders: if only_orders is not None and b.order not in only_orders:
continue continue
if chunk_path.exists() and not args.force: if chunk_path.exists() and not args.force:
cached_count += 1
print(f" [ok ] #{b.order:03d} {b.short_title} (cached)") print(f" [ok ] #{b.order:03d} {b.short_title} (cached)")
continue continue
if args.limit is not None and translated_this_run >= args.limit: if args.limit is not None and len(todo) >= args.limit:
continue continue
todo.append(b)
label = f"#{b.order:03d} L{b.level} {count_chinese_chars(b.content):>4}{b.short_title}" if todo:
print(f" [... ] {label} ", end="", flush=True) print(f"To polish this run: {len(todo)} blocks | cached: {cached_count}")
print()
with ZenMuxClient(log_file=log_file) as client:
def run_one(b: MarkdownBlock) -> tuple[MarkdownBlock, str, str, float]:
chunk_path = chunks_dir / f"{b.order:03d}-{b.anchor}.md"
t0 = time.time() t0 = time.time()
try:
polished, notes = polish_block( polished, notes = polish_block(
client, client,
b, b,
@@ -195,22 +209,47 @@ def main() -> int:
system_prompt=system_prompt, system_prompt=system_prompt,
temperature=args.temperature, temperature=args.temperature,
) )
chunk_path.write_text(polished + "\n", encoding="utf-8")
return b, polished, notes, time.time() - t0
if workers == 1:
for b in todo:
label = f"#{b.order:03d} L{b.level} {count_chinese_chars(b.content):>4}{b.short_title}"
print(f" [... ] {label} ", end="", flush=True)
try:
block, polished, notes, elapsed = run_one(b)
except (ZenMuxError, RuntimeError) as e: except (ZenMuxError, RuntimeError) as e:
print(f"\n [FAIL] {label}\n {e}") print(f"\n [FAIL] {label}\n {e}")
continue continue
elapsed = time.time() - t0
chunk_path.write_text(polished + "\n", encoding="utf-8")
cn = count_chinese_chars(polished) cn = count_chinese_chars(polished)
before_cn = count_chinese_chars(b.content) before_cn = count_chinese_chars(block.content)
delta = cn - before_cn delta = cn - before_cn
sign = "+" if delta >= 0 else "" sign = "+" if delta >= 0 else ""
translated_this_run += 1
if notes: if notes:
notes_records.append( notes_records.append(
{"order": b.order, "anchor": b.anchor, "title": b.short_title, "notes": notes} {"order": block.order, "anchor": block.anchor, "title": block.short_title, "notes": notes}
) )
print(f"\r [done] {label}{cn}字 ({sign}{delta}, {elapsed:4.1f}s)") print(f"\r [done] {label}{cn}字 ({sign}{delta}, {elapsed:4.1f}s)")
else:
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as pool:
futures = {pool.submit(run_one, b): b for b in todo}
for fut in concurrent.futures.as_completed(futures):
b = futures[fut]
label = f"#{b.order:03d} L{b.level} {count_chinese_chars(b.content):>4}{b.short_title}"
try:
block, polished, notes, elapsed = fut.result()
except (ZenMuxError, RuntimeError) as e:
print(f" [FAIL] {label}\n {e}")
continue
cn = count_chinese_chars(polished)
before_cn = count_chinese_chars(block.content)
delta = cn - before_cn
sign = "+" if delta >= 0 else ""
if notes:
notes_records.append(
{"order": block.order, "anchor": block.anchor, "title": block.short_title, "notes": notes}
)
print(f" [done] {label}{cn}字 ({sign}{delta}, {elapsed:4.1f}s)")
# 汇总 # 汇总
merged: list[str] = [] merged: list[str] = []
+1
View File
@@ -8,6 +8,7 @@
4. **保留段落数量**:不要合并或拆分段落。每段原文对应一段输出。 4. **保留段落数量**:不要合并或拆分段落。每段原文对应一段输出。
5. **专有名词首次出现保持"中文(English)"格式**;如果译文里这个术语已经这样标了就别改,也不要删掉。 5. **专有名词首次出现保持"中文(English)"格式**;如果译文里这个术语已经这样标了就别改,也不要删掉。
6. **不改变论点、结论、数据、案例**。只改语言表达。 6. **不改变论点、结论、数据、案例**。只改语言表达。
7. **严禁使用 emoji**(✅ ❌ 🔶 🔷 ⭐ 🟢 🔴 ⚠️ 💡 📌 🔑 📊 等彩色符号)。如果原文里有 emoji,替换为字体支持的符号(✓ × ◆ ● ★ * 注 等)或直接删除。这些 emoji 在 PDF 里渲染为方框。
## 要去掉的"AI 味/翻译腔"表征 ## 要去掉的"AI 味/翻译腔"表征
+1
View File
@@ -13,6 +13,7 @@ You are a senior English-to-Chinese biomedical translator and editor. You do NOT
9. Do NOT collapse or merge consecutive paragraphs — preserve paragraph breaks. 9. Do NOT collapse or merge consecutive paragraphs — preserve paragraph breaks.
10. Output Chinese-style punctuation inside Chinese text: `,。;:?!""()`. Keep English punctuation inside parenthetical English phrases. 10. Output Chinese-style punctuation inside Chinese text: `,。;:?!""()`. Keep English punctuation inside parenthetical English phrases.
11. Do NOT add separator lines (`---`) or blank lines that weren't in the source. If the source ends with `---`, keep it; if it doesn't, don't add one. 11. Do NOT add separator lines (`---`) or blank lines that weren't in the source. If the source ends with `---`, keep it; if it doesn't, don't add one.
12. **NEVER use emoji** (✅ ❌ 🔶 🔷 ⭐ 🟢 🔴 ⚠️ 💡 📌 🔑 📊 etc.). If the source contains emoji, replace with plain text or punctuation equivalents (✓ × ◆ ● ★ * 注 等). These do not render in the PDF (font has no glyphs).
## Style rules (aim for native-Chinese feel) ## Style rules (aim for native-Chinese feel)
+71 -22
View File
@@ -6,12 +6,12 @@
# 或: # 或:
uv run python scripts/translate.py projects/dual-target-rnai-pipeline-2026 uv run python scripts/translate.py projects/dual-target-rnai-pipeline-2026
断点续传每块翻译完立即写入 `phase4/zh_chunks/<anchor>.md` 术语表 patch 断点续传每块翻译完立即写入 `phase4/zh_chunks/<anchor>.md`术语表 patch 在本轮结束后统一合并
重跑时已存在的块直接跳过只译缺的 重跑时已存在的块直接跳过只译缺的
设计要点 设计要点
1. 切块按 H2 粒度单块一般 <600 英文词单次 API 调用远低于 Sonnet output token 上限 1. 切块按 H2 粒度单块一般 <600 英文词单次 API 调用远低于 Sonnet output token 上限
2. 术语表累积式更新每块调用传入当前已知术语译完回写 patch保证全文一致 2. 并发翻译使用稳定术语表快照译完后统一合并 glossary patch避免多线程写冲突
3. 失败不会污染最终产物块级文件独立可重跑汇总步骤独立 3. 失败不会污染最终产物块级文件独立可重跑汇总步骤独立
4. 日志完整每次 API 调用写 `phase4/logs/translate.jsonl` 4. 日志完整每次 API 调用写 `phase4/logs/translate.jsonl`
""" """
@@ -19,6 +19,7 @@
from __future__ import annotations from __future__ import annotations
import argparse import argparse
import concurrent.futures
import json import json
import sys import sys
import time import time
@@ -29,7 +30,6 @@ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from scripts.lib.markdown_chunker import ( from scripts.lib.markdown_chunker import (
MarkdownBlock, MarkdownBlock,
count_chinese_chars, count_chinese_chars,
merge_blocks,
split_by_headers, split_by_headers,
) )
from scripts.lib.zenmux_client import ZenMuxClient, ZenMuxError, load_secrets from scripts.lib.zenmux_client import ZenMuxClient, ZenMuxError, load_secrets
@@ -112,12 +112,19 @@ def save_glossary(path: Path, glossary: dict[str, str]) -> None:
) )
def _glossary_value(value) -> str:
if isinstance(value, dict):
return str(value.get("zh") or value.get("中文") or "")
return str(value)
def build_user_prompt(block: MarkdownBlock, glossary: dict[str, str]) -> str: def build_user_prompt(block: MarkdownBlock, glossary: dict[str, str]) -> str:
glossary_hint = ( glossary_lines = (
"\n".join(f"{en} || {zh}" for en, zh in sorted(glossary.items())) f"{en} || {_glossary_value(zh)}"
if glossary for en, zh in sorted(glossary.items())
else "(none yet)" if _glossary_value(zh)
) )
glossary_hint = "\n".join(glossary_lines) or "(none yet)"
level_hint = ( level_hint = (
f"H{block.level}" if block.level >= 1 else "frontmatter (no heading)" f"H{block.level}" if block.level >= 1 else "frontmatter (no heading)"
) )
@@ -195,6 +202,12 @@ def main() -> int:
default=None, default=None,
help="最多翻译前 N 个未缓存的块(调试用)", help="最多翻译前 N 个未缓存的块(调试用)",
) )
parser.add_argument(
"--workers",
type=int,
default=4,
help="并发翻译 worker 数(默认 4;设为 1 回退串行)",
)
args = parser.parse_args() args = parser.parse_args()
load_secrets() load_secrets()
@@ -223,26 +236,34 @@ def main() -> int:
print(f"Source: {src_path.relative_to(project_root)}") print(f"Source: {src_path.relative_to(project_root)}")
print(f"Blocks: {len(blocks)} | total English words: {total_en_words:,}") print(f"Blocks: {len(blocks)} | total English words: {total_en_words:,}")
print(f"Glossary loaded: {len(glossary)} terms") print(f"Glossary loaded: {len(glossary)} terms")
print(f"Model: {args.model} | temperature: {args.temperature}") workers = max(1, args.workers)
print(f"Model: {args.model} | temperature: {args.temperature} | workers: {workers}")
print() print()
start = time.time() start = time.time()
translated_this_run = 0 todo: list[MarkdownBlock] = []
with ZenMuxClient(log_file=log_file) as client: cached_count = 0
for b in blocks: for b in blocks:
chunk_path = chunks_dir / f"{b.order:03d}-{b.anchor}.md" chunk_path = chunks_dir / f"{b.order:03d}-{b.anchor}.md"
if only_orders is not None and b.order not in only_orders: if only_orders is not None and b.order not in only_orders:
continue continue
if chunk_path.exists() and not args.force: if chunk_path.exists() and not args.force:
cached_count += 1
print(f" [ok ] #{b.order:03d} {b.short_title} (cached)") print(f" [ok ] #{b.order:03d} {b.short_title} (cached)")
continue continue
if args.limit is not None and translated_this_run >= args.limit: if args.limit is not None and len(todo) >= args.limit:
continue continue
todo.append(b)
label = f"#{b.order:03d} L{b.level} {b.word_count:>4}w {b.short_title}" if todo:
print(f" [... ] {label} ", end="", flush=True) print(f"To translate this run: {len(todo)} blocks | cached: {cached_count}")
print()
successful_patches: list[dict[str, str]] = []
with ZenMuxClient(log_file=log_file) as client:
def run_one(b: MarkdownBlock) -> tuple[MarkdownBlock, str, dict[str, str], float]:
chunk_path = chunks_dir / f"{b.order:03d}-{b.anchor}.md"
t0 = time.time() t0 = time.time()
try:
translation, patch = translate_block( translation, patch = translate_block(
client, client,
b, b,
@@ -251,19 +272,47 @@ def main() -> int:
system_prompt=system_prompt, system_prompt=system_prompt,
temperature=args.temperature, temperature=args.temperature,
) )
chunk_path.write_text(translation + "\n", encoding="utf-8")
return b, translation, patch, time.time() - t0
if workers == 1:
for b in todo:
label = f"#{b.order:03d} L{b.level} {b.word_count:>4}w {b.short_title}"
print(f" [... ] {label} ", end="", flush=True)
try:
_block, translation, patch, elapsed = run_one(b)
except (ZenMuxError, RuntimeError) as e: except (ZenMuxError, RuntimeError) as e:
print(f"\n [FAIL] {label}\n {e}") print(f"\n [FAIL] {label}\n {e}")
continue continue
elapsed = time.time() - t0 successful_patches.append(patch)
chunk_path.write_text(translation + "\n", encoding="utf-8")
if patch:
for k, v in patch.items():
glossary.setdefault(k, v)
save_glossary(glossary_path, glossary)
cn = count_chinese_chars(translation) cn = count_chinese_chars(translation)
translated_this_run += 1
print(f"\r [done] {label}{cn:>4}字 ({elapsed:4.1f}s, +{len(patch)} terms)") print(f"\r [done] {label}{cn:>4}字 ({elapsed:4.1f}s, +{len(patch)} terms)")
else:
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as pool:
futures = {pool.submit(run_one, b): b for b in todo}
for fut in concurrent.futures.as_completed(futures):
b = futures[fut]
label = f"#{b.order:03d} L{b.level} {b.word_count:>4}w {b.short_title}"
try:
_block, translation, patch, elapsed = fut.result()
except (ZenMuxError, RuntimeError) as e:
print(f" [FAIL] {label}\n {e}")
continue
successful_patches.append(patch)
cn = count_chinese_chars(translation)
print(f" [done] {label}{cn:>4}字 ({elapsed:4.1f}s, +{len(patch)} terms)")
glossary_conflicts: list[tuple[str, str, str]] = []
for patch in successful_patches:
for k, v in patch.items():
if k not in glossary:
glossary[k] = v
elif _glossary_value(glossary[k]) != v:
glossary_conflicts.append((k, _glossary_value(glossary[k]), v))
if successful_patches:
save_glossary(glossary_path, glossary)
if glossary_conflicts:
print(f" [WARN] glossary patch 冲突 {len(glossary_conflicts)} 条,保留既有译法")
# 汇总:按 order 拼接所有 chunk # 汇总:按 order 拼接所有 chunk
merged: list[str] = [] merged: list[str] = []