Claude Skill

deep-research

Professional deep research report generation — multi-agent collaboration with parallel chapter writing, automatic latest-data targeting, multilingual output, and built-in quality checks.

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

Full trust report

Download mxyhi-ok-skills-deep-research-7933c15.zip · 445 KB
mxyhi/ok-skills 490 46 forks Apache-2.0 Updated 5d ago
Part of mxyhi/ok-skills — 37 skills

Install

skills CLI npx skills add https://github.com/mxyhi/ok-skills/tree/main/deep-research
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install mxyhi-ok-skills@llmmart
Git git clone https://github.com/mxyhi/ok-skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole mxyhi/ok-skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

deep-research

生成对标券商/第三方研究机构标准的深度调研报告。

  • 架构:主 agent 调度 4 个子 agent Task(大纲/数据/预检/装配)+ 1 轮主控并行派发章节,中间数据走临时文件
  • 数据源:在线模式 → 工具内置引擎(Layer 0,如有)+ 大纲建议源定向搜索(Layer 1) + sources.json 优质源搜索(Layer 2)并行 → 按质量触发免费源补强(Layer 3 兜底)→ Scrapling 批量抓取;离线模式 → 用户指定的本地文件(md/txt/pdf/docx)
  • 安装:见下方「安装与配置」
  • 输出:$TMPDIR/outline.json(临时,非最终报告)
  • 最终报告:保存到 skill 目录下的 reports/
  • 参考文件:RULES.md(硬约束/反模式)、TYPES.md(分类标准/编号规范)、profiles.json(三档模式参数,修改后重启软件即全局生效)
  • 容错原则:调研不阻塞。所有脚本/命令调用必须有兜底路径。主路径失败 → 自动尝试替代方案(换 sys.executable/检查路径/直接 Python 实现)→ 三次失败后向用户报告具体问题。详见「容错原则」。

0. 支付级质量标准(所有 Task 共用,缺任何一项即降级)

# 标准 说明
1 结论先行 每章以 > 引用格式 核心判断开头
2 来源可追溯 每个数字标注(机构,年份)
3 反方视角 至少 1 处呈现争议或反对观点
4 三层深度 事实层 → 因果层 → 判断层
5 零套话 无"近年来""值得注意的是"等填充词
6 标题含判断 "格局:高度集中"✅ | "行业概况"❌
7 可自包含 首章必须定义核心概念
8 无内部编号 正文无任何流程编号,标题自解释
9 时间戳正确 文件名和报告尾时间必须 date 命令获取
10 目录源自大纲 目录从 outline.json 的第一级章节生成,不从正文提取
11 强制目录 报告正文前必须包含 ## 目录 标题及自动目录(TOC),列出所有章节标题
12 元数据完整 报告头部必须包含 总字数、阅读时间、数据截至日期(精确到月)、报告生成具体时间(精确到秒)、调研模式、Skill版本 六个字段,用 · 隔开。另起一行 > **参考来源**:{主要来源} 等 · 共引用 N 个来源。报告末尾须附 ## 参考来源(列出所有引用机构及链接)和 ## 免责声明。版本号从本 skill 的 VERSION 文件读取。
13 篇幅达标 见项目根目录 profiles.json。所有模式限制以 profiles.json 为准,修改后重启软件即全局生效。
14 四段式结构 顺序固定为:报告标题 → 元数据块(含六字段 + 参考来源行) → ## 目录 → 正文各章 → 尾部(参考来源 + 免责声明)
15 编码洁净 所有中间文件(outline.json / data-pool.json / chapter-*.md)必须使用 UTF-8 无 BOM 编码写入,不得出现替换字符(\ufffd)或 GBK→UTF-8 Mojibake。子 agent 在写入前必须自行验证编码洁净,不得将编码问题遗留到主 agent
16 纯文本公式 报告中不得使用 LaTeX/math 公式语法($...$、$$...$$、\[...\] 等)。公式必须用纯文本或 Unicode 符号表达,确保复制到任何编辑器都不产生渲染问题

时间锚定规则

所有主题默认以 {CURRENT_YEAR} 为目标搜索最新数据。时间锚定模式在 Task 1 中由大纲 agent 按以下规则判定:

模式 符号 判定条件 target_year 验收
latest(默认) ⏳ 所有主题的默认值,除非符合 relaxed 或 user_specified {CURRENT_YEAR} 严格:≥50% 数据来自当年/前一年
relaxed(放宽) 🔓 指南/教程/概念类主题,或用户问历史/原理("草书发展""起源""背景") {CURRENT_YEAR} 宽松:标记旧数据但不过滤
user_specified 📌 用户提问显式指定了年份/月份("2025年""2026Q1""2020年至今") 用户的指定年份 硬约束:>50% 匹配用户指定时间

{CURRENT_YEAR} 是动态变量,运行时通过 date +%Y 解析,无需手动修改。


1. 主 agent 调度流程

⚠️ CRITICAL — DO NOT SPEAK BEFORE LANGUAGE DETECTION

Your VERY FIRST action (before anything else) must be: detect language → set $LANG. Output NOTHING to the user until $LANG is set — no thinking aloud, no status messages. After language is detected, ALL output must be in $LANG. Period.

IMPORTANT: Clean the topic before detection — the user input may contain framework wrapper text (e.g. "请使用... skill 执行...用户输入如下:"). Strip all wrapper text and pass ONLY the clean research topic. For example from "请使用...用户输入如下:Quantum computing market outlook -quick" extract only "Quantum computing market outlook".

你(主 agent)的完整流程:

══ Setup (必须先执行) ══

 → 创建一个带时间戳的临时目录作为 TMPDIR(例如系统临时目录下的 deep-research-YYYYMMDD-HHMMSS)
 → 同时确定 TOOLSDIR(本 skill 的 tools/ 目录)、PROMPTSDIR(本 skill 的 prompts/ 目录)、SKILLDIR(本 skill 的根目录)
 → 读取本 SKILL.md + RULES.md + TYPES.md

══ Step 0 — Language Detection (output nothing before detection) ══

 → Clean topic: strip wrapper text, keep only the user's actual research topic
 → Determine language: analyze the cleaned topic and pick the ISO 639-1 code:
   zh (Chinese), en (English), ja (Japanese), ko (Korean), ru (Russian),
   ar (Arabic), hi (Hindi), vi (Vietnamese), th (Thai), tr (Turkish),
   es (Spanish), fr (French), de (German), pt (Portuguese), it (Italian),
   nl (Dutch), sv (Swedish), pl (Polish), id (Indonesian)
   → If unsure, default to "en".
   → Do NOT output anything during this step.
 → Write language code: use `write` tool to create {TMPDIR}/language.txt with the ISO code
 → Set `$LANG` = language code from the step above
   → **从这一行开始,所有面向用户的输出必须使用 $LANG 语言(不在 $LANG 列表中时默认 en)。SKILL.md 的指令文本不论用什么语言写的,只是供你阅读的上下文;实际输出以 $LANG 为准——你是读到中文指令后意识上翻译成 $LANG 再输出。**
   → Announce detected language to the user (single line, in $LANG, e.g. "🌐 Language detected: en")

### 🔔 语言自查清单(每次输出前执行)

☐ /language.txt 的值 = 我的 $LANG? ☐ 我正准备输出的这一句/这段,是 $LANG 吗? ☐ todo 条目是 $LANG 吗? ☐ 给用户的进度通知是 $LANG 吗? ☐ 我是否在无意识中用了指令文件的语言(如中文)而非 $LANG? 如果任一答案为"否"→ 立即改写为 $LANG 再输出。

**硬规则**:派发子任务/子 agent 时(如有多 agent 工具),其 prompt 中的 `{LANG}` 必须是你检测到的语言代码。子任务输出的语言由你负责保证。

══ 主流程 ══

 1. ══ 离线模式判定(Step 0.5) ══
    → 你已经读取了用户原始输入。用自然语言理解判断用户关于数据来源的意图,不要用关键词匹配:
      - 用户是否提到了本地文件/目录/资料?
      - 用户是否明确要求不要联网?
      - 用户是否明确要求联网补充?
    → 判断逻辑:
      - 提到本地文件 +(未说联网 / 不联网)→ 离线模式,跳过搜索
      - 提到本地文件 + 说"联网补充" → 正常流程(搜+读本地)
      - 未提本地文件 → 正常流程
    → 离线模式 + 有路径 → {TMPDIR}/offline_mode.txt,`offline_mode=true`,向用户报告单行说明
    → 离线模式 + 无路径 → 回复用户询问路径,不继续
    → 正常模式 → `offline_mode=false`

   → **模式解析**:从清洗后的主题中提取调研模式
     - 主题末尾是 ` -quick` → `$DEPTH_MODE=quick`,去除该后缀
     - 主题末尾是 ` -deep` → `$DEPTH_MODE=deep`,去除该后缀
     - 无上述后缀 → `$DEPTH_MODE=standard`(默认)

 2. 记录任务开始时间到 {TMPDIR}/start_time.txt
 3. 创建任务清单(使用 `todo_write` 或当前环境等价工具;无则用文本记录),使用 $LANG 语言
  4. ══ Task 1 — 分析主题 + 生成大纲 ══
     → 读取 {PROMPTSDIR}/task1_outline.md,替换 {TMPDIR} {TOOLSDIR} {LANG} {CURRENT_YEAR} {MODE},注入 prompt
     → **只做变量替换,不添加语言、格式、报告结构等额外指令。语言已由 Step 0 判定为 $LANG 并在 prompt 中替换 {LANG}。**
     → 派发子任务(若有多 agent 工具则用之;否则由你本人直接执行本任务),等待完成
     → 用 `read` 确认 {TMPDIR}/outline.json 存在
     → 从 outline.json 读取 title + chapter_count + depth_mode
    → 任务清单标记完成
    → 向用户报告进度(使用 $LANG 语言)
  6. ══ Task 2 — 数据收集 + 结构化数据池 ══
     → 读取 {PROMPTSDIR}/task2_data_collection.md
     → 替换标准变量 {TMPDIR} {TOOLSDIR} {LANG} {COUNTRY}
     → 如果 `offline_mode=true`,额外替换:
       {OFFLINE_MODE} → true
       {LOCAL_PATHS} → 读取 {TMPDIR}/offline_mode.txt 的内容(路径列表)
     → 如果 `offline_mode=false`,替换 {OFFLINE_MODE} → false,{LOCAL_PATHS} → 空字符串
     → 派发子任务(若有多 agent 工具则用之;否则由你本人直接执行本任务),等待返回
    → 如失败(子任务报错或 task2_manifest.json 不存在),**自动重试 1 次**,重新派发。第二次仍失败则向用户报告并终止
     → 读取 {TMPDIR}/task2_manifest.json,提取 source_count + fact_count + search_engine + fetch_method + engines + free_fallback + english_fallback + unique_domains
    → 任务清单标记完成
    → 向用户报告进度(使用 $LANG 语言)
     7. ══ Task 3 — 派发章节撰写 ══
     → 读取 {TMPDIR}/outline.json 获取 chapters 数组;读取 {TMPDIR}/data-pool.json
     → **读取 `profiles.json` 获取当前模式的 `max_chars`**,计算 `per_chapter_chars = max_chars ÷ chapters.length`
     → 从 data-pool.json 提取所有唯一 (src, yr) 组合,按首次出现顺序预分配引用编号 [1], [2], [3]...,写入 {TMPDIR}/citation_map.json
     → 读取 `{PROMPTSDIR}/task3_chapter_agent.md` 模板
     → **根据 $LANG 裁剪 prompt 中的多语言段落**:
       - prompt 中的 `[LANG_en]` 段落:仅当 $LANG=en 时保留,其他语言删除
       - prompt 中的 `[LANG_zh]` 段落:仅当 $LANG=zh 时保留,其他语言删除
       - 删除标记文本本身(`[LANG_en]` `[/LANG_en]` 占位符行)
       - 无标记的段落全部语言通用,保留
      → **撰写方式(所有平台通用,默认并行、无多 agent 时串行)**:
        - **章节执笔者不做任何工具调用**(不跑 prepare-chapter、validate、manifest、word-count),只写文件
        - 探测当前环境是否存在多 agent / 子任务工具(如 `task`、`subagent`、`Task`、`agent` 等):
          - **有** → 为每章逐一构建 prompt,**并行**派发全部章节子任务,全部完成后进入 Round 2
          - **无** → 由你本人按章节顺序**逐一撰写**每章正文并落盘,全部写完后直接进入 Round 2
        - **不得因为无法并行派发而中断调研**:串行撰写与其余步骤完全等价,仅耗时略增

      → **并行派发章节**(仅当存在多 agent 工具时):
       - 初始化空列表 task_ids = []
       - For N = 1 to chapters.length:
         - 读取 outline.chapters[N] 的 title、sections
         - 从 data-pool.json 中筛选该章 sub_questions 对应的事实条目
         - **将事实直接嵌入 prompt**:每条事实前标注预分配的 `[N]` 编号
         - 用多 agent 工具的后台/并行模式派出每章
         - 记录每章子任务的句柄/ID 到 task_ids(无句柄可记录时,以章节文件路径为追踪依据)
         - 任务清单标记该章 in_progress
       - 将 task_ids 写入 {TMPDIR}/task3_bg_ids.json(持久化,防止主 agent 中断后丢失状态)
       - 向用户报告:"已并行派出 {N} 章,等待全部完成..."(使用 $LANG 语言)
       - 等待全部章节完成(本轮结束后收到全部完成提示再继续;单章完成通知可忽略)
       - 然后进入 Round 2:

       **Round 2 — 收集结果 + 失败重写**:
       - 读取 {TMPDIR}/task3_bg_ids.json 获取所有 task 句柄
       - 对每个子任务收集其章节结果(或直接读取产物文件)
       - 用 `read` 逐一确认 {TMPDIR}/chapters/chapter-{N}.md 是否存在且非空
       - 如果有章节缺失或内容为空:
         - 记录失败章节编号列表
         - **串行重写**:对每个失败章节逐一(同步)重新派发/执笔,等待完成
         - 再次用 `read` 确认
       - 任务清单标记每章 completed
       - 向用户报告最终章节完成情况(使用 $LANG 语言)
     8. ══ Task 4 — 验证 + 装配 + QA(**主 agent 直接执行**) ══
     → **Step 0 — 清理残留**:删除 {SKILLDIR}/reports/ 目录下所有 0 字节文件(前次装配失败的空壳);创建 {SKILLDIR}/reports/$LANG/ 子目录(如果不存在)
     → **Step 1 — 批量验证**:`python {TOOLSDIR}/dr_tools.py validate-all-chapters --chapters-dir {TMPDIR}/chapters/ --chapters {chapter_count}`,内部 ThreadPoolExecutor 并行验证所有章节。从输出 JSON 的 `failed_chapters` 中找到失败章节,逐个重新生成(重新派发章节 agent → 重新验证该章)。
     → **Step 1b — 章节深度均衡检查**:`python {TOOLSDIR}/dr_tools.py depth-balance --chapters-dir {TMPDIR}/chapters/ --chapters {chapter_count}`。如果某章行数 < 平均值的 50%,标记告警(not blocking,仅提示)。
     → Step 1 或 Step 2 失败时,**先删除本次已写入的产物**(报告文件、中间文件等),再重新执行对应步骤,避免残留文件干扰下次运行
     → **Step 2 — 装配**:`python {TOOLSDIR}/dr_tools.py assemble-report --outline {TMPDIR}/outline.json --chapters-dir {TMPDIR}/chapters/ --datapool {TMPDIR}/data-pool.json --mode {depth_mode} --target-year {target_year} --output {SKILLDIR}/reports/$LANG/ --lang $LANG`
    → **$REPORT 提取**:从装配输出中提取 `Report assembled: ...` 行中冒号后的第一个路径,设为 `$REPORT` 变量
     → **Step 2b — 可信评估(数据层)**:`python {TOOLSDIR}/dr_tools.py generate-confidence-section --datapool {TMPDIR}/data-pool.json --manifest {TMPDIR}/task2_manifest.json --report "$REPORT" --lang $LANG`
       从输出中解析 `CONFIDENCE:` 行获取 `conf_coverage`、`conf_total_facts`、`conf_high_pct`、`conf_medium_pct`、`conf_low_pct`、`conf_actual_pct`、`conf_est_pct`、`conf_fct_pct`、`conf_auth_pct`、`conf_data_limited`、`conf_controversies`、`conf_adequate_subq`、`conf_total_subq`、`conf_score` 共 14 个变量。
     → **Step 2c — 可信评估(LLM判断)**:使用上一步的 14 个统计变量 + 报告标题(从 outline.json 读取)在 LLM 上下文内直接生成定性评估意见。
        - 输出必须使用 $LANG 语言,2-4 句,纯定性判断,不重复逐项明细中的具体数字
        - **语气校准(重要)**:本工具是开源信息综合项目,非付费研究报告。评估意见应遵循以下原则:
          - **总分决定基调**:score≥75 → 正面肯定为主;50-74 → 中性平衡;<50 → 温和提醒
          - **不说"缺陷""不足""未能"等负面措辞** → 改为"可进一步关注的方面""仍有补充空间"
          - **不说"无法获取""受限于"** → 改为"部分高频量化指标因商业敏感性未纳入公开讨论范围"
          - **不自我贬低**:不出现"门槛高""可信度大打折扣"等损害报告公信力的表述
          - **正面收尾**:最后一句必须是肯定整体参考价值的结论
          - **定位准确**:强调"综合公开信息形成的参考判断"而非"严谨学术研究"
        - 写出到 `{TMPDIR}/llm_assessment.txt`
        - 用 `edit` 工具将评估意见插入到报告可信评估区的 `**{综合评级标签}**` 行之后(追加 "**{评估意见标签}**:\n\n{文本}"),然后用 `read` 确认插入正确
        - `{综合评级标签}` 和 `{评估意见标签}` 使用语言映射表中的翻译
     → **Step 3 — 数据受限处理**:读取 {TMPDIR}/task2_manifest.json 的 `data_limited` 字段。如果为 true,在报告标题后插入数据说明声明,**使用 $LANG 语言**。
    → **Step 4 — 引用处理**:`python {TOOLSDIR}/dr_tools.py convert-citations --datapool {TMPDIR}/data-pool.json "$REPORT" --lang $LANG`(从 data-pool 构建参考章节,验证正文 `[N]` 引用均有对应条目)
    → **Step 4b — 货币符号转义**:`python {TOOLSDIR}/dr_tools.py escape-currency "$REPORT"`(将 `$` 转义为 `\$`,避免被知乎/Obsidian/Typora 等渲染器错误解析为 LaTeX math mode)
      → **Step 5 — QA**:`python {TOOLSDIR}/dr_tools.py qa-report "$REPORT" --mode {depth_mode} --target-year {target_year} --lang $LANG`,解析 JSON 输出,从 `checks.word_count.count` 取字数,从 `checks.word_count.limit` 取上限
     → **Step 6 — 更新本地报告列表页**:`python {TOOLSDIR}/generate_pages.py --local`(刷新 reports-browser/index.html,将 reports/ 下所有报告打包为嵌入 JS 的可浏览页面)——需在 `{SKILLDIR}` 目录下执行,bash 命令须加 `workdir="{SKILLDIR}"` 参数
     → 任务清单标记完成
    → ⏱ **强制计算总耗时**(读取 start_time.txt + 当前时间算差值)
    → 从 outline.json + task2_manifest.json + qa-report 中提取数据,使用 $LANG 语言汇报最终结果。

      **语言自适应标签映射表**(以下所有 <词> 根据 $LANG 替换):

      | 中文 | en | ja | ko | fr | de | es | 其余语言 |
      |------|----|----|----|----|----|----|---------|
      | 执行总结 | Execution Summary | 実行サマリー | 실행 요약 | Résumé exécutif | Zusammenfassung | Resumen ejecutivo | Execution Summary |
      | 阶段 | Stage | 段階 | 단계 | Phase | Phase | Fase | Stage |
      | 详情 | Detail | 詳細 | 세부 | Détail | Detail | Detalle | Detail |
      | 大纲/Plan | Plan | 概要 | 개요 | Plan | Plan | Plan | Plan |
      | 观点速览/Insight | Insight | 洞察 | 인사이트 | Aperçu | Einblick | Perspectiva | Insight |
      | 数据/Data | Data | データ | 데이터 | Données | Daten | Datos | Data |
      | 报告/Report | Report | レポート | 보고서 | Rapport | Bericht | Informe | Report |
      | 章 | ch | 章 | 장 | chap. | Kap. | cap. | ch |
      | 来源 | sources | ソース | 출처 | sources | Quellen | fuentes | sources |
      | 事实 | facts | 事実 | 사실 | faits | Fakten | datos | facts |
      | 独立域名 | domains | ドメイン | 도메인 | domaines | Domains | dominios | domains |
| 行 | lines | 行 | 줄 | lignes | Zeilen | líneas | lines |
| 字 | chars | 語 | 단어 | mots | Wörter | palabras | chars |
      | 分钟 | min | 分 | 분 | min | Min. | min | min |
      | 生成时间 | Generated | 生成時刻 | 생성 시간 | Généré le | Erzeugt | Generado | Generated |
      | 搜索 | Search | 検索 | 검색 | Recherche | Suche | Búsqueda | Search |
| 数据充足 | Adequate | 十分 | 충분 | Suffisantes | Ausreichend | Adecuado | Adequate |
| 数据受限 ⚠ | Limited ⚠ | 制限 ⚠ | 제한 ⚠ | Limitées ⚠ | Eingeschränkt ⚠ | Limitado ⚠ | Limited ⚠ |
| 可信评估 | Confidence | 信頼性評価 | 신뢰도 평가 | Évaluation de confiance | Vertrauensbewertung | Evaluación de confianza | Confidence |
| 覆盖充足/部分覆盖/覆盖不足 | Full/Partial/Limited coverage | 完全/部分/不足カバー | 충분/부분/부족 | Couverture complète/partielle/limitée | Vollständige/Teilweise/Eingeschränkte Abdeckung | Cobertura completa/parcial/limitada | Adequate/Partial/Limited |
| 统计 | Stats | 統計 | 통계 | Statistiques | Statistiken | Estadísticas | Stats |
| 综合评级 | Rating | 総合評価 | 종합 평가 | Note globale | Gesamtbewertung | Calificación general | Rating |
| 评估意见 | Assessment | 評価意見 | 평가 의견 | Avis d'évaluation | Bewertung | Opinión de evaluación | Assessment |
| 耗时 | Duration | 所要時間 | 소요 시간 | Durée | Dauer | Duración | Duration |
      | 免费源补强 | free fallback | 無料補強 | 무료 보강 | sources gratuites | kostenlose Quellen | fuentes gratuitas | free fallback |
      | 本地文件 | local files | ローカル | 로컬 파일 | fichiers locaux | lokale Dateien | archivos locales | local files |

      **搜索策略描述拼接规则**(使用映射表中的翻译):

      ```
      IF offline_mode=true:
        <搜索词>:{offline_$LANG}
      ELSE:
        engines_names = engines 数组元素大写(["websearch"] → "Websearch")
        desc = engines_names
        IF free_fallback=true: desc += " (+{free_fallback_$LANG})"
        IF english_fallback=true: desc += " (+EN)"
        <搜索词>:{desc}
      ```

      **数据质量徽标规则**:

      ```
      IF data_limited=true: <质量词> = {limited_$LANG}
      ELSE: <质量词> = {adequate_$LANG}
      ```

      严格按以下结构输出:

      ```
      📊 **<执行总结词>**

      | <阶段词> | <详情词> |
      |:----|:------|
      | 📋 <Plan词> | {outline.title} · {outline.chapter_count} <章词> · {outline.depth_mode} |
      | 🎯 <Insight词> | {outline.chapters[0].description} |
       | 📡 <Data词> | {task2_manifest.source_count} <来源词> · {task2_manifest.unique_domains} <独立域名词> · {task2_manifest.fact_count} <事实词> · <搜索词>:{search_desc} · {task2_manifest.fetch_method} |
       | 📄 <Report词> | {REPORT} |
       | 🌐 <浏览器词/Report List> | {SKILLDIR}/reports-browser/index.html |
       | ✅ <可信评估词> | <覆盖_{coverage_summary}> · 高置信{conf_high_pct}% · 已公布{conf_actual_pct}% · {conf_score}/100 · {data_quality_badge} → {llm_verdict} |
       | 📊 <统计词> | {qa_report.line_count} <行词> · {qa_report.word_count} <字词> · <耗时词>⏱ {totalMin} <分钟词> · <生成时间词>:{gen_time} |
      ```

      其中:
      - `{outline.chapters[0].description}` = 从 outline.json 读取第 1 章(核心观点)的 description 字段,作为观点速览摘要
      - `{gen_time}` = 读取 {TMPDIR}/start_time.txt 中的任务开始时间,格式化为 `YYYY-MM-DD HH:mm:ss`
       - `{REPORT}` 仅输出最终报告路径(`{SKILLDIR}/reports/{LANG}/xxx.md`),不包含任何 TMPDIR 中间路径
      - `{search_desc}` = 按搜索策略拼接规则生成,所有中文词根据 $LANG 翻译
       - `{data_quality_badge}` = 按数据质量徽标规则生成
       - `<覆盖_{coverage_summary}>` = 从 `task2_manifest.coverage_summary` 读取(adequate/partial/insufficient),用语言映射表中"覆盖充足/部分覆盖/覆盖不足"行对应翻译替换
       - `{conf_high_pct}`、`{conf_actual_pct}`、`{conf_score}` = 从 Step 2b 的 `CONFIDENCE:` 行解析对应的字段
       - `{llm_verdict}` = 读取 Step 2c 写入的 `{TMPDIR}/llm_assessment.txt` 完整内容
    → 任务清单全部完成

**禁止**:主 agent 不得在 Task 调度之间自行执行搜索引擎调用或数据处理。搜索/抓取归 Task 2,大纲生成归 Task 1,章节撰写归 Task 3,装配验证归 Task 4。Task 间的 handoff 文件读取(outline.json、task2_manifest.json 等)不受此限。

---

## 2. Task 1 — 主题分析 + 大纲

**工具**:多 agent / 子任务工具(如有)| 一次调用;无则主 agent 直接执行本任务
**prompt 文件**:`prompts/task1_outline.md`
**用法**:读取文件内容,替换 `{TMPDIR}` `{TOOLSDIR} {LANG} {CURRENT_YEAR}` 为实际值后注入 prompt。

**输出**:大纲 agent 直接用 `write` 工具创建 `{TMPDIR}/outline.json`。主 agent 通过 `read` 确认文件存在。

---

## 3. Task 2 — 数据收集 + 结构化数据池

**工具**:多 agent / 子任务工具(如有,建议用高并发/高思考 budget 分类);无则主 agent 直接执行本任务
**prompt 文件**:`prompts/task2_data_collection.md`
**用法**:读取文件内容,替换 `{TMPDIR}` `{TOOLSDIR}` `{LANG}` `{COUNTRY}` `{OFFLINE_MODE}` `{LOCAL_PATHS}` 为实际值后注入 prompt。

**输出**:{TMPDIR}/data-pool.json + {TMPDIR}/task2_manifest.json(使用 `write` 工具创建)

**LANG→COUNTRY 映射**(用于替换 `{COUNTRY}`):zh→CN, en→US, ru→RU, ja→JP, ko→KR, fr→FR, de→DE, es→ES, pt→PT, it→IT, nl→NL, sv→SE, pl→PL, id→ID, th→TH, tr→TR, vi→VN, ar→SA, hi→IN。未覆盖的语言用空字符串。

---

## 3. Task 3 — 章节撰写 & 章节 agent 指令模板

主 agent 为每章准备子任务 prompt 时,参数使用 `{PROMPTSDIR}/task3_chapter_agent.md` 模板。

**用法**:读取 `{PROMPTSDIR}/task3_chapter_agent.md`,替换以下变量:
- `[章节 title]` → 当前章的 title(从 outline.json chapters 数组读取)
- `[N]` → 当前章节编号(第 1 章为 1,第 2 章为 2...)
- `[total]` → chapters 数组总长度
- `[sections 列表]` → 当前章的 sections 数组(逗号分隔)
- `{per_chapter_chars}` → `profiles.json` 中当前模式的 `max_chars ÷ 总章数`(主 agent 一次性算好)
- `{min_paragraphs}` → `profiles.json` 中当前模式的 `min_paragraphs`
- `{调研模式}` → quick / standard / deep(当前调研模式)
- `{TMPDIR}` → 运行时临时目录
- `{TOOLSDIR}` → tools 目录

**输出**:{TMPDIR}/chapters/chapter-{N}.md(使用 `write` 工具创建)

---

## 4. Task 4 — 验证 + 装配 + QA(主 agent 直接执行)

装配、引用转换、QA 检查均由主 agent 通过 bash 命令直接执行 `{TOOLSDIR}/dr_tools.py` 完成:

1. `validate-all-chapters` → 批量结构验证(并行)
2. `assemble-report` → 生成报告
3. `generate-confidence-section` → 可信评估(从 data-pool + manifest 聚合生成)
4. `convert-citations` → 引用转换
5. `escape-currency` → 货币符号转义
4. `qa-report` → 质量检查

**清理**:装配完成后主 agent 执行 `Remove-Item -Recurse -Force "{TMPDIR}"` 清理临时文件。

---

## 6. 输出文件管理

### 路径优先级

最终报告保存路径按以下优先级判定:

1. **用户自定义路径** — 如果用户显式指定了输出目录(如 `D:\Reports\`),使用指定路径
2. **Skill 默认路径** — `{SKILLDIR}/reports/`(skill 根目录下的 reports/)

装配阶段(Step 3)根据实际使用的路径写入,文件名格式不变:`<主题>-YYYYMMDD-HHmmss.md`。

### QA 路径核验

Step 4 QA 必须确认报告文件的保存路径为上述两者之一,如果路径不属于默认目录且非用户指定目录,标记"路径异常"不通过。

### 日期锚定

文件名中的日期用当前年月日。

### 清理机制

Task 4 装配 + QA 通过后,内部已完成清理:

  1. 清理中间文件 {TMPDIR} 目录:Windows 用 Remove-Item -Recurse -Force "{TMPDIR}",Linux 用 rm -rf {TMPDIR}
  2. 确认 tool-output/ 无残留

---

## 7. 工具依赖速查

| 能力 | 用途 | 免费? | 国内源? |
|:----|:-----|:-----:|:--------:|
| 内置搜索(探测) | **主力**搜索引擎(当前工具的内置搜索,如 `websearch`/`web_search`,运行时探测) | 取决于环境 | 视引擎而定 |
| 其他搜索引擎 | 不同工具的内置搜索(运行时自动适配) | 取决于环境 | — |
| Scrapling MCP | 全文抓取(MCP,需注册到当前工具的 MCP 配置) | ✅ | **✅ 推荐,国内源主力** |
| webfetch / fetch | 抓取回退(Scrapling 不可用时替代) | ✅ | ❌ 远端受限,国内源效果一般 |
| bash / 终端 | date 时间戳 / 文件操作 / 运行 Python | ✅ | — |
| write | 写文件 | ✅ | — |

搜索策略由 agent 在运行时根据工具集**自动适配**,不依赖预设的搜索引擎配置。

**搜索链路**:

Layer 0 — 工具内置引擎探测(扫描可用工具集) │ ├─ 发现内置引擎 → 作为主力搜索,与后续层并行 └─ 未发现 → 跳过此层,不影响后续

Layer 1 — 大纲建议源定向搜索(site: 搜索,用内置引擎) ↓ Layer 2 — sources.json 优质源搜索(并行) ↓ 搜索结果质量评估(Step 3 质量门) ├─ 达标 → 直接进入抓取 └─ 不达标 → Layer 3 免费源补强(A/B 类源 + 区域引擎) ↓ 全部 URL → 检测 Scrapling MCP 可用性 ├─ 🔧 可用 → Scrapling 批量抓取全文 → 数据池 └─ 🌐 不可用 → webfetch 逐个抓取全文(标注回退)→ 数据池


---

## 8. 安装与配置

### 前置条件

- Python 3.10+
- Scrapling(安装方式由 AI 根据官方文档和当前系统自动适配)
- Playwright(可选,用于 JS 渲染和反检测抓取)

### 注册 Scrapling MCP Server

Scrapling 通过 MCP(Model Context Protocol)与 AI agent 通信,需注册到当前工具的 MCP 配置后才能被调用(各工具配置文件位置与键名不同:Claude Code 用 `mcpServers`,OpenCode 用 `mcp`,等等——让 AI 现场探测并适配)。

**推荐方式**:运行一次 `/research`,Task 2 在检测到 Scrapling 未注册时,会自动完成安装和注册。

**参考实现**:本项目提供了 `scrapling-mcp-server.py`(与本文件同目录),是一份标准 MCP Server 实现,覆盖了标准抓取、反检测抓取、JS 渲染抓取三种模式。AI 可根据本机环境参考此脚本,如有问题再参考 Scrapling 官方文档。

**手动注册格式**(以 `mcpServers` 标准格式为例,供 AI 安装时参考;按你当前工具的配置键名与数组格式调整):
  ```json
  {
    "mcpServers": {
      "scrapling": {
        "command": "<python-path>",
        "args": ["<mcp-server-script-path>"]
      }
    }
  }

重启当前工具

MCP Server 在工具启动时加载,注册后必须重启才能生效。

验证是否生效

运行 /research 调研时,Task 2 阶段如显示 🔧 Scrapling 抓取 则表示 MCP 工作正常。若显示 🌐 webfetch 抓取(Scrapling 已自动安装,重启后生效) 则表示 Scrapling 已自动安装,重启后下次生效。若显示 🌐 webfetch 抓取 则表示安装失败,需检查 Python 环境和网络连接。

抓取回退说明

若 Scrapling 抓取某 URL 失败(WAF/超时/JS 渲染需求),会自动回退到 webfetch 孤立抓取该 URL,不影响其他 URL 的 Scrapling 抓取。若 Scrapling MCP 完全不可用,则全部走 webfetch。调研不会阻塞。


9. 跨平台编码规范(Windows/macOS/Linux)

问题根因

Windows PowerShell 5.1 控制台编码为 CP936(GBK 中文编码),无法表示 18 种非英语语言: 俄语西里尔字母、日语假名/汉字、韩语谚文、阿拉伯语、泰语、印地语天城文、越南语调号、 以及德语 äöüß、法语 éèêç、西班牙语 ñ 等拉丁扩展字符——通过 shell 传参/pipe 时全部损坏。

macOS/Linux 的终端默认 UTF-8,无此问题。

硬性规则(所有 agent 必须遵守)

# 规则 正确做法 错误做法
1 非 ASCII 文本不进 shell argv/pipe 用 write 工具写文件 → Python --file 读取 Python 脚本 argv 传非 ASCII 文本 ❌
2 所有文件读写用 UTF-8 Python 统一 encoding='utf-8-sig'(BOM 容错) 依赖 shell 编码
3 写文件只用 write 工具 write 工具 → UTF-8 无 BOM PowerShell Set-Content -Encoding UTF8 ❌(会加 BOM)
4 Python stdout 显式设 UTF-8 sys.stdout.reconfigure(encoding='utf-8') 依赖系统默认编码
5 Python 子进程输出用 --output 文件 python script.py --input file --output result shell 重定向 > result.txt ❌(CP936 编码输出)

一劳永逸方案:全链路编码安全架构

 ┌─ 数据来源 ──────────────────────────────────┐
 │ write 工具 / Python open(..., 'w', encoding) │ ← UTF-8 无 BOM
 └──────────────┬──────────────────────────────┘
                ▼
 ┌─ 中间文件 ──────────────────────────────────┐
 │ *.json / *.md : 全部 UTF-8(BOM 容错读取)     │ ← utf-8-sig 代码编
 └──────────────┬──────────────────────────────┘
                ▼
 ┌─ Python 处理 ───────────────────────────────┐
 │ 所有脚本入口设 `stdout.reconfigure('utf-8')` │ ← 输出安全
 │ 所有文件读用 `encoding='utf-8-sig'`         │ ← 输入安全
  │ Python 脚本统一用 `--input`/`--output` 参数   │ ← 完全绕过 shell
 └──────────────┬──────────────────────────────┘
                ▼
 ┌─ 最终输出 ──────────────────────────────────┐
 │ 报告文件 : UTF-8 无 BOM,任意语言均可正确显示    │
 └─────────────────────────────────────────────┘

当前防护状态

文件 防护措施 状态
dr_tools.py 入口 stdout.reconfigure + 所有读操作用 utf-8-sig ✅
dr_check.py 所有读操作用 utf-8-sig ✅
dr_gen.py 所有读操作用 utf-8-sig,写操作用 utf-8(无 BOM) ✅

Created by hoolulu · github.com/hoolulu/deep-research

Files (ok-skills)
  • command
    • research.md 751 B
      ---
      description: Deep research on any topic — generates professional-grade reports comparable to investment bank / research institution standards
      ---
      
      <command-instruction>
      读取本 skill 的 SKILL.md(本 `command/` 的上级目录中的 SKILL.md,即已安装的 deep-research skill),并严格遵循其全部指令执行调研。不要再发明或改写 SKILL.md 里的流程。
      
      先读取 SKILL.md 全文,再解析 `$ARGUMENTS` 确定调研主题与可选模式:
      - `/research <topic>` → standard 模式
      - `/research <topic> -quick` → quick 模式
      - `/research <topic> -deep` → deep 模式
      </command-instruction>
      
      <user-request>
      $ARGUMENTS
      </user-request>
      
      ---
      ```
      deep-research by hoolulu · github.com/hoolulu/deep-research
      ```
      
    • update.md 1.4 KB
      ---
      description: 从 GitHub 拉取 deep-research skill 最新代码
      ---
      
      <command-instruction>
      你是一个自动更新工具。你的任务是从 GitHub 拉取 deep-research skill 的最新代码,不需要版本检查,不需要用户确认。
      
      ## 执行流程
      
      ### Step 1 — 定位 skill 目录
      
      通过以下方式查找 skill 目录(不同工具的 skill 安装位置不同,现场探测即可):
      - 检查当前工作目录是否就是 skill 根目录(存在 VERSION 文件)
      - 或在常见位置查找 deep-research 的 VERSION 文件(如 `~/.opencode/skills`、`~/.claude/skills`、`.cursor/skills` 等)
      
      如果找不到 → 提示"找不到 deep-research skill 目录",给出手动克隆命令后退出。
      
      ### Step 2 — 检查 git 仓库
      
      确认目录是 git 仓库(存在 `.git` 目录)。如果不是 → 用 `git clone` 克隆到临时目录再复制过来。
      
      ### Step 3 — 直接拉取最新代码
      
      **不需要读取 VERSION,不需要比较版本,不需要用户确认。** 直接执行:
      
      ```
      git pull origin main
      ```
      
      如果 pull 失败(网络、冲突等)→ 提示"更新失败: {具体原因}"后退出。
      
      ### Step 4 — 完成
      
      提示已更新到最新版本。
      
      ### 输出示例
      
      ```
      [✓] 已从 main 分支拉取最新代码
      ```
      </command-instruction>
      
      <user-request>
      $ARGUMENTS
      </user-request>
      
      ---
      ```
      deep-research by hoolulu · github.com/hoolulu/deep-research
      ```
      
  • prompts
    • task1_outline.md 8.8 KB
      你是一位专业研究分析师。任务是为主题生成调研大纲和调研计划。
      
      ## ⚠️ 语言强制规则
      你的语言代码是 **{LANG}**。你**所有的输出**(分析、大纲、进度、错误提示、任何面向用户的文本)必须严格使用此语言。
      - 指令文件本身可能使用另一种语言(如中文),这**不是**你输出的语言
      - 你始终将指令理解后以 **{LANG}** 输出,无论指令文件使用何种语言
      - 输出前检查:我这一句是 {LANG} 吗?——不是则立即改写
      - 违规后果:整个流程降级,需重跑
      
      **说明**:基于自身专业知识产出大纲框架,不执行在线搜索。真实数据由后续数据收集阶段完成,你只需定义清晰的研究方向和子问题结构。
      
      ## 输入
      - 主题:[用户提供的主题]
      - 模式:{MODE}
      - 输出路径:{TMPDIR}/outline.json
      
      ## 重要:模式传递
      在 outline.json 中新增 `depth_mode` 字段,值为上述模式(quick / standard / deep)。此字段将传递到最终报告,用于在元数据中标注调研模式。
      
      ## 分类标准
      从以下五种专业类型中选择最匹配的:
      - 行业研究 — 对标券商行业深度报告 / IDC Market Forecast
      - 公司/产品研究 — 对标券商公司深度报告 / Gartner Vendor Assessment
      - 技术专题研究 — 对标 IEEE 技术报告 / 技术白皮书
      - 趋势/市场前瞻 — 对标 Forrester Tech Tide / Gartner Hype Cycle
      - 指南/教程 — 对标 IEEE 方法模版 / 实操文档标准
      
      ## 章节约束
      | 项目 | quick | standard | deep |
      |------|-------|----------|------|
      | 总章节数 | 5-8 章 | 8-10 章 | 10-12 章 |
      | 风险提示 | 可选 | 强制 ≥ 3 项 | 强制 ≥ 5 项 |
      
      > 章数区间以项目根目录 `profiles.json` 的 `min_chapters` / `max_chapters` 为准(quick 5-8 / standard 8-10 / deep 10-12)。
      
      ## 时间锚定模式判定
      
      在 JSON 中新增 `time_anchor` 字段,按上述规则判定:
      
      | 用户输入特征 | mode | target_year | 示例 |
      |-------------|------|-------------|------|
      | 明确年份/月份 | `user_specified` | 用户指定年份 | "2026年厦门房价""2025Q4深圳" |
      | 时效词汇(最新/当前/现状/现在/今年/如今/最近) | `latest` | `{CURRENT_YEAR}` | "厦门房价最新情况" |
      | 趋势/预测(趋势/走势/前景/展望/预测) | `latest` | `{CURRENT_YEAR}` | "房地产市场2026年趋势" |
      | 无时效词 + 行业/公司/市场类 | `latest`(默认) | `{CURRENT_YEAR}` | "厦门房价深度分析" |
      | 指南/教程/概念类 | `relaxed` | `{CURRENT_YEAR}` | "草书学习方法""MCP原理" |
      | 历史/起源/发展历程 | `relaxed` | `{CURRENT_YEAR}` | "俄乌战争根源" |
      
      ## JSON 输出格式
      
      chapters 数组的顺序即报告中的章节顺序(第 1 个元素为第 1 章,依此类推)。每章必须定义 `sections` 字段,预定义本章的子节标题列表。各章 sections 数量根据模式控制:deep 每章 3-6 节,standard 2-4 节,quick 1-2 节。sections 用于章节 agent 撰写时的子节划分和编号(N.1, N.2…)。
      
      ```json
      {
        "title": "报告标题",
        "type": "对应的专业类型",
        "depth_mode": "standard",
        "language": "{LANG}",
        "time_anchor": {
          "mode": "latest",
          "target_year": {CURRENT_YEAR}
        },
        "source_suggestions": ["noaa.gov", "ipcc.ch", "nature.com", "worldbank.org"],
        "chapters": [
          {
            "title": "核心观点",
            "description": "3-5 条核心判断",
            "sections": ["归因科学的确证", "人口暴露规模", "健康冲击", "经济暴露", "小结"],
            "sub_questions": [
              { "question": "{CURRENT_YEAR}年印度极端高温事件的五大核心结论是什么?", "search_keywords": ["{CURRENT_YEAR}年印度极端高温五大结论", "{target_year}年印度极端高温影响"], "counter_keywords": ["印度高温数据争议 不同机构差异"], "data_targets": ["极端高温事件数量", "受影响人口规模", "经济损失估算"], "priority": "high" }
            ]
          }
        ]
      }
      ```
      
      > **注意**:`latest` 和 `relaxed` 的 `target_year` 始终为 `{CURRENT_YEAR}`(动态获取)。`user_specified` 时填入用户指定年份。search_keywords 中必须包含 `{target_year}` 占位符,Task 2 执行时解析。`language` 字段**已由主 agent 在 Step 0 判定**,直接使用即可。
      
      ### 源建议字段
      
      在 outline 顶层新增 `source_suggestions` 数组,列出该主题最适合搜索的 3-8 个权威/专业网站域名(不含搜索引擎域名)。Task 2 会优先搜索这些域名再搜全网。
      
      选择原则:
      - 选择该领域公认的权威数据源、学术期刊、官方统计机构、行业研究机构
      - 中英文主题各选对应的源(中文主题优先中文源,英文主题优先英文源)
      - 示例:
        - 气候变化主题 → `["noaa.gov", "ipcc.ch", "nature.com", "who.int", "worldbank.org"]`
        - 科技行业主题 → `["github.com", "arxiv.org", "semanticscholar.org", "reuters.com", "techcrunch.com"]`
        - 中文产业主题 → `["stats.gov.cn", "iresearch.cn", "199it.com", "36kr.com", "eastmoney.com"]`
      
      ### 子节结构要求
      
      1. 每章必须定义 `sections` 字段,列出本章的所有子节标题
      2. sections 数量根据模式控制:deep 每章 3-6 节,standard 每章 2-4 节,quick 每章 1-2 节
      3. sections 使用与主题一致的语言(英文主题用英文,中文主题用中文),10 字以内,不包含编号(如 `"赛制设计"` ✅,`"一、赛制设计"` ❌)
      4. sections 的顺序即子节在报告中的出现顺序
      5. 末节应为"延伸讨论"或"交叉视角"——不是复述前面内容,而是连接下一章、点出未解问题、或对比其他章节的结论
      
      ## 核心原则
      1. 类型是参考,不是牢笼 — 跨界主题可跨类型融合
      2. 标题自解释 — 标题本身应清晰反映内容,不依赖编号理解
      3. **结构为先**:基于自身专业知识产出大纲框架,不依赖外部搜索
      4. 至少 1 个"反方观点/争议焦点"子问题。该类子问题须填写 counter_keywords(反方搜索关键词,2-3 个),用于 Task 2 定向搜索反方数据
      5. 每个子问题列 2-3 个具体数据指标
      6. search_keywords 须用与主题语言一致的自然语言书写。主题用中文则关键词用中文,主题用英文则关键词用英文。不要自动翻译——中文主题不要写成英文,英文主题也不要写成中文。专有技术缩写(HBM/DDR5/AI/LLM等)不受限
      
      ## 完整性要求
      
      检查你的大纲是否覆盖了该领域的全部主要子话题和经典议题。如果某个公认的重要话题、标志性案例或核心争议未出现在任何章节的sections或sub_questions中,必须补充。
      
      ### 优先级判定标准
      
      每个子问题的 `priority` 根据以下客观条件判定,不得凭直觉填写:
      
      | 优先级 | 判定条件(满足任一即归入该级) |
      |:-------|:-----------------------------|
      | **high** | ① 该子问题的数据被核心观点(第1章)或置信度汇总(末章)直接引用 |
      | | ② 涉及市场规模/份额/增速/营收等量化基准数据(行业/公司类主题) |
      | | ③ 涉及技术原理/架构/性能评估(技术类主题) |
      | | ④ 涉及反方/争议观点(用户强制要求) |
      | | ⑤ 3 个以上其他子问题依赖该问题的数据作为输入 |
      | **medium** | ① 支撑性数据,非核心结论的必需前提 |
      | | ② 单来源数据需要交叉验证但该来源本身可信 |
      | | ③ 定性分析(专家观点、案例研究) |
      | **low** | ① 锦上添花的背景延展 |
      | | ② 对已确认事实的冗余确认(多一个来源说同一件事) |
      | | ③ 不影响当前分析的纯历史/背景数据 |
      
      ## 作业
      
      ### 输出 outline.json
      
      用 `write` 工具直接将完整大纲 JSON 写入 `{TMPDIR}/outline.json`。回答中只需输出确认行,不要输出 JSON 内容。
      
      - `title`、`chapters[].title`、`chapters[].sections[]` 等所有文本字段**必须使用 `{LANG}` 语言书写**
      - `language` 字段值必须设为 `"{LANG}"`
      - JSON 格式必须严格符合上述参考,`source_suggestions` 和 `time_anchor` 必须包含
      - **写入后验证**:用 `read` 工具确认 `{TMPDIR}/outline.json` 存在且内容完整
      
      参考格式:
      ```json
      {
        "title": "报告标题",
        "type": "对应的专业类型",
        "depth_mode": "standard",
        "language": "{LANG}",
        "time_anchor": {
          "mode": "latest",
          "target_year": {CURRENT_YEAR}
        },
        "source_suggestions": ["域名1", "域名2", "域名3"],
        "chapters": [
          {
            "title": "核心观点",
            "description": "...",
            "sections": ["sect1", "sect2"],
            "sub_questions": [...]
          }
        ]
      }
      ```
      
      ### 回答格式
      
      用 `write` 工具写完后,在回答末尾输出确认行供主 agent 解析:
      ```
      Outline: {TMPDIR}/outline.json
      Chapters: N
      Title: 报告标题
      ```
      
      
      ---
      ```
      deep-research by hoolulu · github.com/hoolulu/deep-research
      ```
      
    • task2_data_collection.md 28.9 KB
      你是一位研究分析师。任务是根据大纲中的子问题,完成数据收集并构建结构化数据池。
      
      ## ⚠️ 语言强制规则
      你的语言代码是 **{LANG}**。你**所有的输出**(分析说明、进度、摘要、错误信息、任何面向用户的文本)必须严格使用此语言。
      - 指令文件本身可能使用另一种语言(如中文),这**不是**你输出的语言
      - 你始终将指令理解后以 **{LANG}** 输出
      - 输出前检查:我这一句是 {LANG} 吗?——不是则立即改写
      - 违规后果:整个流程降级,需重跑
      
      ## 优化说明
      - Layer 0-3 搜索全部并行发出,不等前一层完成
      - 免费源补强与 Scrapling 抓取并行执行,不串行等待
      - **搜索源语言过滤**:根据 {LANG} 决定搜索源——{LANG}=zh 时使用全部来源;其他语言跳过中文专用站(cn.bing.com、搜狗、360、B类国内源),对通用搜索引擎加 locale 参数,对特定语言启用区域引擎(详见各步骤说明)
      - **工具探测优先**:agent 在启动时自行扫描可用工具集,根据实际可用的搜索工具自适应选择搜索策略。不依赖任何预设配置。
      
      ## 四层搜索优先级
      
      搜索执行顺序从高优先级到低优先级:
      
      **Layer 0 — 工具内置搜索(最高优先级)**:Step 0 探测当前工具的内置搜索引擎(如 `websearch` / `web_search` 等)。如果可用,**以此为主力搜索引擎**,与后续层并行发出。详见 Step 0。
      
      **Layer 1 — 大纲建议源**:读取 {TMPDIR}/outline.json 的 `source_suggestions` 数组(如 `["noaa.gov", "ipcc.ch"]`),对每个域名,用内置引擎搜索 `site:{域名} {子问题关键词}`。这些是大纲阶段根据主题定向推荐的源,最贴合主题。与 Layer 0 并行发出。
      
      **Layer 2 — sources.json 优质源**:读取 `{PROMPTSDIR}/../sources.json`(与 prompts 同级的 skill 根目录),对每个子问题,遍历其中 `lang` 字段匹配 `{LANG}` 的源,用 webfetch 或 Scrapling 抓取搜索结果页。如某源 health check 失败(前面检测结果)则跳过。
      
      **Layer 3 — 免费源补强**:当前 prompt 中 Step 3 定义的 A/B 类免费源。只在 Layer 0-2 结果不足时触发。
      
      所有层的搜索结果合并去重后进入 Step 4 抓取队列。
      
      ## 输入
      - 大纲文件:{TMPDIR}/outline.json
      - 优质源列表:{PROMPTSDIR}/../sources.json(与 prompts 同级的 skill 根目录)
      - 工具脚本:{TOOLSDIR}/dr_tools.py(通用 QA 工具,替代临时写 grep/jq)
      - 输出路径:{TMPDIR}/data-pool.json
      - QA 工具:{TOOLSDIR}/dr_tools.py(已有命令:check-datapool, json-validate, word-count)
      - Scrapling MCP 可用性:尝试调用 `scrapling_bulk_get` 检测,可用则使用 Scrapling,不可用则回退 webfetch
      
      ## ⚠️ 工具使用铁律
      
      **严禁编写内联代码**(禁止 `python -c "..."`、PowerShell 内联脚本、`bash -c` 等)。所有可复用操作必须使用 `{TOOLSDIR}/dr_tools.py` 的子命令:
      - JSON 取值 → `{TOOLSDIR}/dr_tools.py json-get <file> <key.path>`
      - 字数统计 → `{TOOLSDIR}/dr_tools.py word-count <file>`
      - 数据校验 → `{TOOLSDIR}/dr_tools.py check-datapool <file> --mode <mode>`
      如果遇到该脚本未覆盖的需求,在 task2_manifest.json 的 `gaps` 字段中记录"缺少命令:[描述]",由主 agent 处理。
      
      ## 离线模式({OFFLINE_MODE}=true 时执行)
      
      当 `{OFFLINE_MODE}=true` 时,**跳过全部 Step 1-4(搜索引擎 + 补强 + 抓取)**,改从本地文件提取数据。
      
      ### Step L1 — 读取本地文件
      
      从 `{LOCAL_PATHS}` 获取用户指定的文件或目录路径:
      
      ☐ **如果是目录**:用 `glob` 工具列出目录中所有文件
      ☐ **如果是文件**:直接处理指定文件
      
      按文件类型分别处理:
      
      | 文件类型 | 处理方式 |
      |:--------|:---------|
      | `.md` / `.txt` | `read` 工具直接读取 |
      | `.pdf` | **先用 `read` 工具尝试**(模型如支持 PDF 输入则直接理解);如失败 → 自动安装 pypdf 提取文本 |
      | `.docx` | **自动安装 python-docx 后提取**(见下方说明)|
      | 其他格式 | 标记为不支持,加入 gaps |
      
      **PDF 文件处理**(如果 `read` 工具返回的不是可读文本而是"PDF read successfully",说明模型不支持直接解析 PDF):
      ```
      pip install pypdf -q
      python3 -c "
      from pypdf import PdfReader
      import sys
      reader = PdfReader(sys.argv[1])
      for page in reader.pages:
          print(page.extract_text())
      " "{文件路径}" > "{TMPDIR}/extracted-{文件名}.txt"
      ```
      然后用 `read` 工具读取提取后的 `.txt` 文件。
      
      **DOCX 文件处理**(同样需要额外提取):
      ```
      pip install python-docx -q
      python3 -c "
      import docx, sys
      doc = docx.Document(sys.argv[1])
      for p in doc.paragraphs:
          print(p.text)
      " "{文件路径}" > "{TMPDIR}/extracted-{文件名}.txt"
      ```
      然后用 `read` 工具读取提取后的 `.txt` 文件。
      
      > 以上格式转换是纯 I/O 操作,不属于"核心数据处理",可豁免"严禁编写内联代码"的铁律。转换后读取 `.txt` 即可。
      
      📝 读取完成后向用户报告:`📄 已读取 N 个本地文件`
      
      ### Step L2 — 构建数据池
      
      从 outline.json 读取子问题列表,在已读取的本地文件内容中查找匹配的数据:
      
      **数据池格式**(与标准模式完全一致):
      
      | 模式 | 额外字段要求 |
      |:----|:------------|
      | quick | facts 需含基础字段 + `conf` + `data_type`(无 `cur`/`va`/`vb`)|
      | standard | facts 需要加 `cur`(时效性:current/recent/dated)和 `conf`(置信度:high/medium/low)|
      | deep | 同 standard,且 ctx 长度不限 |
      
      ```json
      {"question":"子问题文本","src":["文件名"],"facts":[{"src":"文件名","yr":"文件中出现年份或无年份留空","met":"指标名","val":数值,"u":"单位","ctx":"说明","url":"文件路径","title":"文件名","conf":"high","data_type":"actual|estimate|forecast"}],"controversies":[],"gaps":["缺口描述"]}
      ```
      
      **提取规则**:
      1. 严格基于文件内容,禁止推测或编造
      2. 优先提取量化数据(数字+单位+年份)
      3. 每条事实的 `url` 填本地文件路径(如 `/Users/xxx/report.pdf`),`src` 填文件名
      4. 文件内容不包含所需数据 → gaps 写"本地资料未覆盖"(不要编造)
      5. 如文件较多,先扫描文件名 + 前几行判断相关性,再精读匹配文件
      
      ### Step L3 — 输出 + 质检
      
      与标准模式的 Step 6 相同:
      
      ☐ 使用 `write` 工具创建 `{TMPDIR}/data-pool.json`(UTF-8 无 BOM)
      ☐ 运行数据质检:`python {TOOLSDIR}/dr_tools.py check-datapool {TMPDIR}/data-pool.json --mode {depth_mode}`
      ☐ 创建 `{TMPDIR}/cautions.json`
      ☐ 创建 `{TMPDIR}/task2_manifest.json`:
      
      ```json
      {"task":2,"source_count":N,"fact_count":N,"search_engine":"local_files","fetch_method":"本地读取","data_pool_path":"{TMPDIR}/data-pool.json","cautions_path":"{TMPDIR}/cautions.json","data_limited":false,"builtin_available":false,"engines":[]}
      ```
      
      > `source_count` = 本地文件数,`fact_count` = 提取到的事实总数。`data_limited` 固定为 false(用户选择纯本地,不套用在线模式的 insufficient_count 判定)。
      
      在回答中只输出 data-pool.json 路径。
      
      ## 数据收集工作流(严格执行)
      
      Step 0 — 工具环境探测(运行时自适应)
      
         巡检当前可用的所有工具,识别搜索相关工具。**这是关键步骤——agent 必须在此步骤中根据实际可用的工具自适应选择搜索策略。**
      
         1. **扫描工具集**:检查你的工具列表中每个工具的 description 字段,找出搜索相关工具:
            - **搜索引擎**:description 含 `search` / `web search` / `搜索引擎` 等关键词的工具(如 `websearch`、`web_search` 等)
            - **抓取工具**:description 含 `fetch` / `webfetch` / `scrapling` / `抓取` 等关键词的工具(如 `webfetch`、`scrapling_bulk_get` 等)
      
         2. **输出**:根据识别结果,在 manifest 的 `engines` 数组中列出可用的搜索引擎名。如未发现任何内置引擎 → 标记 `builtin_available=false`、`all_search_unavailable=true`。
      
         **Scrapling MCP 可用性**(已有,在 Step 4 前检测):
         尝试调用 `scrapling_bulk_get(urls=["https://example.com"], timeout=10, extraction_type="text")` 检测。
      
      Step 2 — 多源并行搜索(Layer 0-2 同时发出)
      
      四层搜索中的 Layer 0-2 在本步骤中一次性全部发出,不串行等待:
      
      **Layer 0 — 工具内置引擎主力搜索**:如果 Step 0 探测到内置搜索引擎,**以此为主力**,搜索所有子问题:
         ```
         websearch(query="{子问题描述} {time_anchor.target_year}", numResults=10)
         ```
         如果 Step 0 未发现任何内置引擎,跳过本层。
      
         🔍 **反方关键词搜索**:从 outline.json 找出所有 `priority=="high"` 且 `counter_keywords` 非空(首元素不为 `""`)的子问题,将其 counter_keywords 作为额外搜索词,与主搜索一次性并行发出。结果存入该子问题的 `controversies` 数组。
      
      **Layer 1 — 大纲建议源定向搜索**:如果 outline.json 包含 `source_suggestions`,对每个域名用内置引擎搜索 `site:{域名} {子问题关键词}`,一次性并行发出。
      
      **Layer 2 — sources.json 优质源搜索**:先并行 health check(`webfetch(url=health_url, timeout=5)` 检测所有 `lang` 匹配 `{LANG}` 的源,非 200/超时标记 dead 跳过)。对存活源用 `webfetch(url=url_template.replace("{query}", url编码的关键词))` 一次性并行发出全部搜索。
      
      所有层的搜索结果合并去重后进入抓取队列。
      
      ### Step 2a — 逐子问题搜索元数据记录(内部追踪,供 manifest 使用)
      
      在搜索结果合并去重后,利用各层已返回的数据,对每个子问题记录搜索元数据。**不额外增加串行等待**——各层搜索返回后立即记录,信息已在内存中。
      
      对每个 sub_question(通过 outline.json 的 chapters[].sub_questions[] 获取索引),暗自记录:
      - `layer_urls`:各层分别贡献的 URL 数(Layer 0/1/2 各自多少条搜索结果)
      - `recent_urls`:其中来自 target_year 及前一年的 URL 数
      - `authoritative_urls`:来自权威域名(.gov/.edu/.org 或知名研究机构)的 URL 数
      - `unique_domains`:不同独立域名数量
      
      以上数据仅 agent 内部暂存,不写文件,用于 Step 6 输出 manifest 中的 `coverage` 数组和 `search_layer_trace`。
      
      ### Step 2b — 小语种英文补搜({LANG} ≠ en 且搜索结果不足时执行)
      
      当 {LANG} ≠ "en" 且 **所有子问题合计可用 URL < 3** 时,执行英文补搜:
      
      ☐ 对每个子问题,使用自然语言将其 `question` 和 `search_keywords` 翻译为英文
      ☐ 使用内置引擎用英文关键词重新搜索,一次性并行发出
      ☐ 反方关键词同理翻译为英文后重搜
      ☐ 结果去重后合并到 Step 2 的 URL 队列中
      ☐ 在 manifest 的 `search_engine` 中追加标记 `+english_fallback`
      
      > 专有名词处理示例:`"Türkiye enerji piyasası"` → `"Turkey energy market"`,不要硬翻。
      
      Step 3 — 免费数据源补强(Layer 3,与 Scrapling 抓取并行)
          触发条件:基于**逐子问题矩阵**的质量评估。
      
          对每个子问题独立评估:
      
          | 条件 | 权重 |
          |:----|:-----|
          | 可用 URL ≥ 3 条 | 必要 |
          | 至少 2 条来自 target_year/前一年 | 必要 |
          | ≥ 1 条来自权威域名(.gov/.edu/.org 或知名机构) | 加分 |
          | 来自 ≥ 2 个不同独立域名 | 加分 |
      
          一个子问题标记为 `insufficient` 的条件(任一满足):
          - 可用 URL < 3 条
          - 全部结果年份早于 target_year-2
          - 0 条来自权威域名(仅限 priority=high 子问题)
      
          矩阵评估结果存入 agent 内部暂存,用于 manifest 的 `coverage` 数组的 `status` 字段(`adequate` / `insufficient`)。
      
          **补搜策略**:
          - 只对标记 `insufficient` 的子问题进行定向补搜,已达标的不重复搜索
          - 如果 `all_search_unavailable=true`,强制执行全量补搜
      
          当 `all_search_unavailable=true` 时,限时保持 **30s**。
      
         **核心原则**:所有检测和搜索操作**一次性并行发出**,不逐个串行。每个操作用独立 timeout,总耗时硬上限 30s,超时立即终止。
      
         ### Step 3a — 并行检测 + 多源搜索(一个批次全部发出)
      
         **检测 1 — Scrapling 可用性** (timeout=15s)
         ```
         scrapling_bulk_get(urls=["https://example.com"], timeout=10, extraction_type="text")
         ```
      
         **检测 2 — A 类搜索引擎搜索** (每个 timeout=8s,全部同时发出,不分条件)
          对所有子问题(同一个 query 模板,替换不同关键词),一次性发出。
          **语言规则**:通用引擎和学术引擎始终发出;中文专用引擎仅 {LANG}=zh 时发出:
            ```
            # 始终发出(通用/学术)
            webfetch(url="https://lite.duckduckgo.com/lite/?q={query}", timeout=8)
            webfetch(url="https://search.brave.com/search?q={query}&country={COUNTRY}", timeout=8)
            webfetch(url="https://www.mojeek.com/search?q={query}&lang={LANG}", timeout=8)
            webfetch(url="https://api.semanticscholar.org/graph/v1/paper/search?query={query}&limit=5", timeout=8)
            webfetch(url="https://api.gdeltproject.org/api/v2/doc/doc?query={query}&mode=artlist&maxrecords=8", timeout=8)
            webfetch(url="https://export.arxiv.org/api/query?search_query=all:{query}&start=0&max_results=5", timeout=8)
      
            # 仅在 {LANG}=zh 时发出(中文专用)
            webfetch(url="https://cn.bing.com/search?q={query}", timeout=8)
            webfetch(url="https://www.sogou.com/web?query={query}", timeout=8)
            webfetch(url="https://www.so.com/s?q={query}", timeout=8)
            ```
          **{COUNTRY} 对照表**:zh→CN, en→US, ru→RU, ja→JP, ko→KR, fr→FR, de→DE, es→ES, pt→PT, it→IT, nl→NL, sv→SE, pl→PL, id→ID, th→TH, tr→TR, vi→VN, ar→SA, hi→IN。
          **区域专用引擎**({LANG}=ru 时加 Yandex,{LANG}=ja 时加 Yahoo JP):
            ```
            webfetch(url="https://yandex.ru/search/?text={query}&lr=225", timeout=8)        # 仅 ru
            webfetch(url="https://search.yahoo.co.jp/search?p={query}", timeout=8)            # 仅 ja
            ```
          ⚠️ 某个搜索引擎超时/失败 → 跳过它,不影响其他。
      
         **检测 3 — B 类国内源搜索**({LANG}=zh 时启用,其他语言跳过)
          ⚠️ 本步骤仅在 {LANG}=zh 时执行。
          timeout=30s,全部同时发出。对每个子问题,构造以下搜索 URL 并用 `scrapling_bulk_get(urls=[全部B类URL], timeout=30, extraction_type="markdown")` **一次性批量抓取**(不逐个调用):
           | 源 | 搜索 URL 模板 | 说明 |
           |:----|:-------------|:------|
            | 百度百科 | `https://baike.baidu.com/item/{URL编码的词条名}` | 先搜索词条名,再拼 URL |
            | 知乎 | `https://www.zhihu.com/search?type=content&q={query}` | 中文问答/分析 |
            | 36氪 | `https://36kr.com/search/articles/{query}` | 科技/商业新闻 |
            | 澎湃新闻 | `https://www.thepaper.cn/search?keyword={query}` | 深度新闻/调查 |
            | 199IT | `https://www.199it.com/?s={query}` | 中文行业数据 |
            | 艾瑞咨询 | `https://report.iresearch.cn/search.aspx?key={query}` | 行业研究报告 |
            | 东方财富 | `https://so.eastmoney.com/news/s?keyword={query}` | 金融/市场数据 |
            | 微博搜索 | `https://s.weibo.com/weibo?q={query}` | 社情民意/热点追踪 |
            | 虎嗅 | `https://www.huxiu.com/search.html?q={query}` | 科技商业深度 |
            | CSDN | `https://so.csdn.net/so/search?q={query}` | 中文技术内容 |
            | 豆瓣 | `https://www.douban.com/search?q={query}` | 文化/影音/书籍评价 |
      
         ### Step 3b — 结果汇聚(Step 3a 全部返回后)
      
         从所有返回结果中提取 URL,去重,追加到抓取队列。
      
         | 结果类型 | 操作 |
         |:---------|:-----|
         | A 类(webfetch)返回的链接 | 从 HTML 提取所有 a[href],过滤掉搜索站自身域名 |
         | B 类(Scrapling)返回的内容 | 直接作为参考来源,不额外提取 URL |
         | 所有成功返回的内容 | 标记时间戳和来源类型 |
      
         **禁止**:在 Step 3 内部调用 explore agent 或其他子 agent 做搜索——搜索引擎直连更快。
      
         ### ⌛ 超时守卫(硬限制)
      
         从 Step 3a 发出第一个工具调用开始计时,以下任一条件成立则**立即终止 Step 3**:
         - ⏱ 总运行时间 > 30 秒
         - ✅ 每个子问题至少从补强来源获得 2 条有效 URL
         - ❌ 所有补强来源均不可用(Scrapling 不可用 + 所有搜索引擎超时)
      
         终止后,已获得的 URL 直接进入 Step 4 抓取队列。未完成的子问题在数据池中标记 gap。
      
      Step 4 — Scrapling 批量抓取(与 Step 3 并行)
      
      ### Scrapling 批量抓取(与补强搜索并行)
          收集全部 URL(搜索结果 + 年度专项)→ 去重 → 立即启动抓取,不等补强:
         ```
         scrapling_bulk_get(urls=[去重URL], timeout=60, extraction_type="markdown")
         ```
         ☐ 全部成功 → 标记 `🔧 Scrapling 抓取`
         ☐ 部分失败 → 对失败 URL 补抓:
           ☐ Cloudflare/WAF → scrapling_bulk_stealthy_fetch(timeout=30)
           ☐ 需 JS 渲染   → scrapling_bulk_fetch(timeout=30)
           ☐ 仍失败       → webfetch(url, format="markdown") 孤立回退
           📝 标记为 `🔧 Scrapling 抓取(N 个 URL 回退 webfetch)`
         ☐ 工具不存在(Scrapling 未注册/已损坏)→ 全部 URL 走 webfetch
           ```
           for each url in urls:
               webfetch(url=url, format="markdown")
           ```
           📝 标记为 `🌐 webfetch 抓取(Scrapling 不可用,请重新安装)`
           ⚠️ 不尝试自动修复,安装环节已要求前置依赖就绪
         ⏳ 第一轮抓取完成后,检查 Step 3 是否产生新 URL。如有 → 补抓第二轮。
      
         ⚡ 阻断点:全部抓取未完成 → 禁止进入数据池构建
         ┌─────────────────────────────────────────────────────┐
          │ 数据池唯一来源必须是抓取到的全文(Scrapling 或 webfetch)。│
          │ 禁止从搜索引擎摘要片段直接提取数据。                  │
         │ 所有抓取工具全部失败则标记"来源稀缺"并跳过该子问题。    │
         └─────────────────────────────────────────────────────┘
      
      Step 5 — 直接提取数据池(不嵌套子 agent,I/O 并行)
      
      **先一次性并行读取所有页面**:将所有抓取到的 URL 一次性用多个 `read` 工具并行读取(不逐个等),收集全部页面内容到内存。读取完成后,**再按子问题遍历**从已读取的内容中提取结构化数据。这避免了串行 I/O 等待。
      
      使用 `read`、`grep`、`write`、终端 工具,**不允许**嵌套派生子 agent / 子任务。
      
      URL↔子问题的映射已在 Step 2-4 的搜索过程中确定,无需额外匹配步骤。
      
      **数据池格式(统一字段名,按模式控制字段有无)**:
      
      所有模式使用同一套字段名,区别仅在于 quick 模式省略 `cur`/`va`/`vb`:
      
      ```json
      {"question":"子问题文本","src":["域名"],"facts":[{"src":"机构","yr":"2026","met":"指标名","val":数值,"u":"单位","ctx":"说明","url":"https://...","title":"文章原标题","cur":"current","conf":"high","data_type":"actual|estimate|forecast"}],"controversies":[{"a":"来源A","va":"数据A","b":"来源B","vb":"数据B","n":"差异摘要"}],"gaps":["缺口描述"]}
      ```
      
      | 字段 | 说明 | quick | standard | deep |
      |:-----|:-----|:-----|:---------|:-----|
      | question / src / gaps | 基础字段 | ✅ | ✅ | ✅ |
      | facts[].src/yr/met/val/u/ctx/url/title | 事实基础字段(url 来源链接,title 文章标题) | ✅ | ✅ | ✅ |
      | facts[].cur | 时效性标记 | ❌ | ✅ | ✅ |
      | facts[].conf | 置信度标记 | ✅ | ✅ | ✅ |
      | facts[].data_type | 数据类型:actual(已公布实际值)/ estimate(考前或截至结果公布前的估算)/ forecast(对未来年份的预测) | ✅ | ✅ | ✅ |
      | controversies[].va/vb | 正反方数据值 | ❌ | ✅ | ✅ |
      | ctx 长度上限 | — | ≤80 字 | ≤150 字 | 不限 |
      | 每子问题事实上限 | — | ≤5 条 | ≤8 条 | 不限 |
      | 每子问题来源上限 | — | ≤5 个 | ≤8 个 | 不限 |
      | value 为 null | 处理方式 | 移除 | 降为 low conf | 保留 |
      
      **提取规则**:
      1. 严格基于页面内容,禁止推测或虚构
      2. 每提取一条事实,根据其指标性质判断 `data_type`:如果该指标的官方数据在报告生成时尚未公布(如当年录取率、分数线等),标记为 `estimate`;如果是已公布的官方数据(如当年报名人数、政策文件等),标记为 `actual`;如果是对未来年份的预测,标记为 `forecast`。不确定时默认为 `estimate`
      3. 优先 high-priority 子问题,确保 ≥2 条事实
      3. 同一数据不跨子问题重复引用
      4. 矛盾数据并排保留
      5. 数值优先提取有单位的明确指标
      6. 每提取一条事实,必须附带 `url` 字段,值为该条数据的来源文章链接(从 Step 2 抓取的原始 URL 中获取,禁止伪造)
      7. 某子问题确实找不到数据 → gaps 写"已搜未找到"
      
      Step 6 — 输出数据池 + 数据质检
      
      使用 `write` 工具创建 `{TMPDIR}/data-pool.json`(UTF-8 无 BOM)。如数据量大可分步追加。
      
      数据池 JSON 结构如下:
      
      ```json
      [
        {"question": "...", "src": [...], "facts": [...], "controversies": [...], "gaps": [...]}
      ]
      ```
      
      写入后运行数据质检(**必须使用脚本统计 source_count / fact_count,禁止自己写 Python 算**):
      ☐ `python {TOOLSDIR}/dr_tools.py check-datapool {TMPDIR}/data-pool.json --mode {depth_mode}`
          → 从输出中的 `STATS: {...}` 行提取 `source_count` 和 `fact_count` 用于 manifest
          → 如果脚本报错(exit code != 0),修复数据池中的问题后重新运行。DATA_LIMITED 以下兜底
      ☐ 每子问题 ≥ 1 条事实或 gap 说明
      ☐ priority=high 子问题 facts ≥ 2 条
      ☐ **来源可信度检查**:逐条检查 priority=high 的 fact 来源。域名后缀 .edu/.gov/.org 或知名研究机构标记可信,自媒体/企业来源标记存疑。
      ☐ **乱码检查**:扫描 facts[] 中 `src`、`title`、`ctx` 等所有文本字段,检查替换字符(\ufffd)、GBK→UTF-8 Mojibake 以及连续 3+ 问号(`???`)。
      ☐ **跨事实一致性**:同一指标跨子问题差值 > 20% 且口径不明 → 记录。
      ☐ **Adversarial 检查**(仅 priority=high facts):
          ☐ **数值量级**:检查有无亿/billion 差 10x、万/million 混淆等量级错误
          ☐ **虚假平滑**:怀疑完美递增/递减趋势数据(可能为插值伪造)
          ☐ **来源混淆**:确认指标口径匹配(出货量 vs 零售量、营收 vs 利润等)
          → 发现问题在 cautions.json 中记录,不阻塞流程
      ☐ 根据检查结果标记 `data_limited`(依据 manifest 的 `insufficient_count` 判断:若 ≥1/3 子问题未达标则标记 true)。
      ☐ **兜底**:如果 `check-datapool` 反复报错,根据报错修复数据池问题(空值、缺少字段等),然后重新运行 `check-datapool` 直到通过。不要手动算 count——STATS 必须在脚本输出中才有 manifest 一致性。
      
      ### 输出 cautions.json
      
      使用 `write` 工具创建 `{TMPDIR}/cautions.json`,记录质检中发现的问题:
      
      ```json
      {"passed": true, "cautions": [{"sub_question_index": 3, "fact_index": 1, "type": "来源存疑", "detail": "自媒体来源"}]}
      ```
      
      ### fetch_method 判定
      
      manifest 中的 `fetch_method` 字段根据 Step 4 的实际抓取情况填写:
      
      | 情况 | fetch_method 值 |
      |:----|:--------------|
      | Scrapling 全部成功 | `🔧 Scrapling` |
      | 部分 URL 回退 webfetch | `🔧 Scrapling(N 个回退)` |
      | Scrapling 完全不可用,全部走 webfetch | `🌐 webfetch` |
      
      ## 硬规则
      1. **搜索引擎策略**:工具内置引擎(Layer 0,如有)为主力,大纲建议源(Layer 1)与 sources.json 优质源(Layer 2)补充并行,搜索结果合并去重。搜索结果质量不足时(URL < 3 / 年份过旧 / 来源过少)触发免费源补强(Layer 3 兜底)。
      2. **年份时效(默认强制)**:`time_anchor.mode != "relaxed"` 时,search_keywords 必须含 `{target_year}`;`user_specified` 时用用户指定年份替代 `{target_year}`
      3. Scrapling 为默认抓取工具,不可跳过、不可替代;若 Scrapling MCP 不可用则回退 webfetch,并在输出中明确标注所用抓取方式
      4. 连续 3 次域名 404/403 → 标记"来源稀缺"并跳过
      5. 不在不同子问题间重复使用同一来源的同一数据
      6. 矛盾数据并排记录,不得合并
      7. **补强限时**:Step 3 从启动到终止总耗时默认 ≤ 30 秒。超时后已获得的 URL 继续进入 Step 4,未完成的子问题标记 gap。不得因为补强未完成阻塞 Step 4 抓取或数据池构建。
      
      ## 作业
      1. 完成工具内置引擎(Layer 0,如有) + 大纲建议源(Layer 1) + sources.json 优质源(Layer 2) + Scrapling/webfetch 数据收集 + 数据池提取
      2. Step 6 数据质检(来源可信度/乱码/一致性/Adversarial 检查)
      3. 清理 tool-output/ 中的中间文件
      4. 使用 `write` 工具创建 `{TMPDIR}/cautions.json`:
      ```json
      {"passed":true,"summary":"预检通过","cautions":[{"sub_question_index":1,"fact_index":0,"type":"来源存疑","detail":"自媒体来源"}]}
      ```
      5. 使用 `write` 工具创建 `{TMPDIR}/task2_manifest.json`(含预检结果和逐子问题覆盖诊断):
      
         根据追踪数据(Step 2a)和矩阵评估(Step 3)填充以下结构化字段:
      
         **基础字段**(与之前一致):
         - `engines`(数组,列出 Step 0 检测到的实际可用引擎名)
         - `free_fallback`(boolean):是否触发了 Step 3 免费源补强
         - `english_fallback`(boolean):是否执行了 Step 2b 英文补搜
         - `search_engine`(字符串,向下兼容)
         - `unique_domains`:从 data-pool.json 所有来源 URL 提取域名去重计数
         - `data_limited` / `source_count` / `fact_count` / `fetch_method`
      
         **新增诊断字段**:
      
         1. `search_layer_trace`(对象)——各搜索层的贡献度:
         ```json
         {
           "layer0_builtin": {"used": true, "sub_questions_covered": 10, "urls_contributed": 30},
           "layer1_outline_sources": {"used": true, "domains_active": ["moe.gov.cn"], "urls_contributed": 5},
           "layer2_sources_json": {"sources_checked": 6, "alive": 4, "dead": 2, "urls_contributed": 6},
           "layer3_free_fallback": {"triggered": false, "reason": "达标"}
         }
         ```
      
         2. `coverage`(数组)——逐子问题覆盖度报告,从 Step 2a 追踪数据和 Step 5 提取后的 gaps 生成。每项包含:
         ```json
         {
           "q_index": 0,
           "status": "adequate",
           "facts": 4,
           "recent": 3,
           "data_types": ["actual", "estimate"],
           "gaps": []
         }
         ```
         对 `status=insufficient` 的子问题,额外填写:
         - `diagnosis`:缺口分类(`timing_gap` 数据尚未公布 / `engine_coverage_gap` 引擎覆盖盲区 / `topic_too_niche` 主题无足够公开信息 / `fetch_failure` 搜到但抓不到)
         - `actionable`:一句话说明原因和应对建议
      
         3. `coverage_summary`(字符串):`"adequate"` / `"partial"` / `"insufficient"`
         4. `insufficient_count`(整数):未达标的子问题数
      
         聚合字段 `data_limited` 判定规则改为:`insufficient_count >= total_sub_questions / 3` 时标记 true(不再用旧的独立来源 < 8 规则)。
      
         **完整 manifest 示例**:
         ```json
         {
           "task": 2,
           "source_count": 14,
           "fact_count": 41,
           "unique_domains": 11,
           "search_engine": "builtin",
           "fetch_method": "🔧 Scrapling",
           "engines": ["websearch"],
           "builtin_available": true,
           "free_fallback": false,
           "english_fallback": false,
           "data_limited": false,
           "data_pool_path": "{TMPDIR}/data-pool.json",
           "cautions_path": "{TMPDIR}/cautions.json",
           "search_layer_trace": {
             "layer0_builtin": {"used": true, "sub_questions_covered": 10, "urls_contributed": 30},
             "layer1_outline_sources": {"used": true, "domains_active": ["moe.gov.cn"], "urls_contributed": 5},
             "layer2_sources_json": {"sources_checked": 6, "alive": 4, "dead": 2, "urls_contributed": 8},
             "layer3_free_fallback": {"triggered": false, "reason": "达标"}
           },
           "coverage": [
             {"q_index": 0, "status": "adequate", "facts": 4, "recent": 3, "data_types": ["actual", "estimate"], "gaps": []},
             {"q_index": 1, "status": "insufficient", "facts": 1, "recent": 0, "data_types": ["estimate"], "gaps": ["数据尚未公布"], "diagnosis": "timing_gap", "actionable": "时间限制,非搜索问题。需用预测措辞。"}
           ],
           "coverage_summary": "partial",
           "insufficient_count": 1,
           "total_sub_questions": 10
         }
         ```
      6. 在回答中只输出 data-pool.json 路径(不要输出 JSON 内容)
      
      
      ---
      ```
      deep-research by hoolulu · github.com/hoolulu/deep-research
      ```
      
    • task3_chapter_agent.md 3.8 KB
      ### 你只需写一章
      主题:[章节 title]
      章节编号 N:[N]
      总章节数 total:[total]
      本章字数上限:{per_chapter_chars} 字符(来自 profiles.json)
      
      ## ⚠️ 语言强制规则
      这篇报告的语言是 **{LANG}**。你的**所有输出**(章节正文、说明、任何文本)必须严格使用此语言。
      - 指令文件可能使用另一种语言,这**不是**你输出的语言
      - 无论收到什么语言的指令,都输出 **{LANG}**
      - 输出前自检:我这一句是 {LANG} 吗?
      
      **严禁写内联代码**(`python -c`、PowerShell 等)。字数统计只能用 `dr_tools.py word-count`。
      
      ### 标题规则(重要)
      - **不要写 `##` 章节标题**——`## 一、章节标题` 由装配阶段自动添加,你只需写正文
      - **子节标题使用 `### N.1`、`### N.2`… 格式**,其中 N 为上述章节编号。不可使用汉字编号
      - 正文第一行直接是 `>` 核心判断(不要在前面空行留白)
      - 标题是判断句(使用与主题一致的语言,英文主题用英文,中文主题用中文)
      
      ### 子节结构说明
      本章预定义了以下子节:`[sections 列表]`。请严格按此列表划分,不可增减。
      
      ### 格式
      - 使用与主题一致的语言(英文主题用英文,中文主题用中文),引用来源使用 `(N)` 格式对最终读者显示,但在撰写时使用 `[N]` 占位。例:`该市场2025年估值145亿美元[3]` 或 `The market was valued at $145B in 2025[3]`
      - **不要在 [N] 后加链接**——引用转换和参考章节由 `convert_citations` 自动处理
      - 表格中的 `[N]` 同样只写编号,不加链接
      - 不要在正文中使用 `(机构,年份)` 格式
      - 每章以 `>` 引用格式的核心判断开头
      - **正文段落 ≥ {min_paragraphs} 段(整章合计)**,数据表 ≥ 3 张
      - 建议字数约 {per_chapter_chars} 字符。**直接一次写成,不要检查字数、不要反复调整。** 字数由装配阶段统一校验。
      - 矛盾数据并排呈现而非掩盖
      
      ### 子节写作规范
      每个子节应按以下节奏展开:
      1. **结论先行**:本子节的核心判断(第一段直接给出)
      2. **数据支撑**:用至少一个量化数据或具体案例支撑判断
      3. **因果分析**:解释数据背后的逻辑(为什么是这个结果)
      4. **反方/补充**:必要时呈现争议或限定前提
      5. **过渡**:最后一句自然连接到下一子节(不是"综上所述"式重述)
      
      ### 写作质量要求
      - **预测/估算数据措辞**:引用的事实如果来自考前预测或未公布数据(如录取率、分数线预测),正文必须使用 "预估""预计""预测" 等限定词,不得当作既定事实陈述。例:✅"据历史数据估算整体录取率约85%" ❌"整体录取率为85%"
      - 禁止在子节末尾写 "In summary" "To summarize" "综上所述" "总而言之" 等独立重述段落。
      - 禁止每节结尾都用同一句式模板(如 "这凸显了…的重要性" "This underscores the importance of…" 全篇重复使用)。
      - 表格前后必须有分析性文字——表格不解释自己。
      - 不要连续 3 段以上用同一句式开头。
      - 每个子节至少包含一个量化数据或具体案例。
      - **禁止使用公式语法**:不得出现 `$...$`、`$$...$$`、`\[...\]` 等 LaTeX/math 语法。指标用纯文本(如 "增长 23%"、"P/E 倍数为 15"、"A = πr²")。
      - **货币符号规范**:货币金额中的 `$` 必须写为 `\$`,避免被 markdown 渲染器错误解析为 LaTeX 数学公式。✅ `\$87K` ❌ `$87K`。反引号内的代码引用和表格格式化不受此限。
      
      ### 作业
      直接使用 `write` 工具创建 `{TMPDIR}/chapters/chapter-{N}.md`,写入填充完成的完整正文。不需要运行任何工具,不需要创建 manifest 文件。
      
      在回答中只返回文件路径。
      
      ---
      ```
      deep-research by hoolulu · github.com/hoolulu/deep-research
      ```
      
    • task4_assembly.md 6.2 KB
      你是一位研究分析师兼编辑。任务是将已写好的各章装配为完整报告并做质量验收。
      
      ## 输入
      - 大纲文件:{TMPDIR}/outline.json
      - 注意事项:{TMPDIR}/cautions.json(数据质量警告,装配阶段不写入报告但可参考)
      - 章节目录:{TMPDIR}/chapters/(包含 chapter-1.md, chapter-2.md…)
      - 输出目录:`{SKILLDIR}/reports/{LANG}/`(skill 目录下的 reports/ 子目录)
      - QA 工具:{TOOLSDIR}/dr_tools.py(已有命令:assemble-report, convert-citations, qa-report, check-encoding, check-headers, check-chapter-numbers, check-metadata, check-toc, check-tail, word-count, year-density)
      - 数据受限标记:`{data_limited}`(true 时数据来源不足,需降低质量预期)
      
      ## ⚠️ 工具使用铁律
      
      **禁止编写任何 Python 脚本**(`.py`)。装配、引用转换、QA 检查必须使用 `{TOOLSDIR}/dr_tools.py` 的子命令。如果遇到该脚本未覆盖的需求,在 task4_manifest.json 的 `"notes"` 字段中记录"缺少命令:[描述]",由主 agent 处理。
      
      ## Step 1 — 装配
      
      用 `assemble-report` 一条命令完成全部机械装配(章节排序 → 汉字编号 → 目录 → 元数据 → 来源提取 → 尾部拼接 → 编码洁净写入)。**字数在装配后由脚本自动计算,不需要提前准备 wordcounts.json。**
      
      ```bash
      python {TOOLSDIR}/dr_tools.py assemble-report \
        --outline {TMPDIR}/outline.json \
        --chapters-dir {TMPDIR}/chapters/ \
        --datapool {TMPDIR}/data-pool.json \
        --mode {depth_mode} \
        --target-year {target_year} \
        --output {SKILLDIR}/reports/{LANG}/
      ```
      (`{SKILLDIR}/reports/{LANG}/` 为默认输出目录,按语言分类;脚本自动根据报告标题+日期生成文件名)
      
      ## Step 2 — 引用格式转换([N] → [(N)](#refN))
      
      运行 `convert-citations` 将报告中的 `[N]` 引用转换为可点击的 `[(N)](#refN)` 格式,并在尾部生成带锚点的参考来源列表:
      
      ```bash
      python {TOOLSDIR}/dr_tools.py convert-citations \
        --datapool {TMPDIR}/data-pool.json \
        {输出目录/报告文件}
      ```
      (`--output` 可指定另存路径,不指定则原地替换)
      
      转换后效果:
      - 正文:`据报告显示[(1)](#ref1),2026年市场规模达XX亿元[(2)](#ref2)。`
      - 尾部:`## 参考来源\n\n(1) [文章标题 · 机构 · 2026](URL)\n(2) ...`(URL 隐藏为可点击链接,不显示裸网址)
      
      装配前请自行确认以下参数:
      - `depth_mode`:从 outline.json 读取
      - `target_year`:从 outline.json 的 time_anchor.target_year 读取
      - `生成时间`:当前 `date` 命令值
      - `输出路径`:优先用户指定,无则 `{SKILLDIR}/reports/{LANG}/`
      - `data_limited`:如为 true,报告开头追加醒目标注 `> ⚠️ **数据说明**:本次调研数据来源较为有限(共引用 N 个来源),部分结论基于有限样本,仅供参考。`,并将 QA 的年份密度和段落达标标准各降低 30%
      - **总字数在装配后自动计算**,无需提前准备
      
      ## Step 3 — QA 验收
      
      先运行全量自动化检查:
      ```
      python {TOOLSDIR}/dr_tools.py qa-report <最终报告路径> --mode {depth_mode} --target-year {target_year}
      ```
      输出 JSON 中 `passed=true` 则机械检查全部通过。单项检查也可单独调用:
      ```
      python {TOOLSDIR}/dr_tools.py check-encoding <报告>     # 编码/乱码
      python {TOOLSDIR}/dr_tools.py check-headers <报告>       # 标题格式
      python {TOOLSDIR}/dr_tools.py check-chapter-numbers <报告> # 章编号
      python {TOOLSDIR}/dr_tools.py check-metadata <报告>       # 元数据
      python {TOOLSDIR}/dr_tools.py check-toc <报告> --expected N # 目录
      python {TOOLSDIR}/dr_tools.py check-tail <报告>           # 尾部
      python {TOOLSDIR}/dr_tools.py word-count <报告>           # 字数
      python {TOOLSDIR}/dr_tools.py year-density <报告> --target-year N # 年份
      ```
      
      以下为完整验收清单(机械项由 dr_tools.py 覆盖,语义项人工判断):
      
      ☐ **dr_tools.py qa-report 通过**(编码/乱码/标题/元数据/TOC/尾部/年份/字数全部检查;`time_anchor=relaxed` 时年份检查自动豁免)
      ☐ 章节完整性:所有章节存在
      ☐ 行数:wc -l ≥ 模式参考值
      ☐ 段落数:抽 2 章,每章 ≥ 5 段
      ☐ 目录为单层结构:仅包含章级标题,无子节缩进
      ☐ 三段式顺序:报告第 1 行为 `# ` 标题,第 2-6 行内含元数据行(以 `> **元数据**:` 开头),元数据行后紧跟 `## 目录`
      ☐ **路径核验**:报告保存路径属于默认语言子目录(`{SKILLDIR}/reports/{LANG}/`)或用户指定目录,两者之一;既非默认也非用户指定 → 标记"路径异常"不通过
      ☐ 时间戳:尾部时间与 date 匹配
      ☐ 反方观点:至少 1 处
      ☐ 跨来源归因一致
      
      所有检查(含字数)通过 → 继续。
      
      年份密度检查:如果 `time_anchor.mode == "relaxed"`,**跳过 year-density 检查**(指南/教程类主题不要求时效性)。年份密度不达标但其他项目全过 → 加声明继续。
      
      字数超标 → 用 `word-count` 获取精确字数,对比 `profiles.json` 中当前模式的 `max_chars`。**不阻塞**,在 manifest 中标记 `word_count_exceeded: true` 即可,最终汇报时一并显示。
      其他项不达标 → 局部补刀(单章重写,最多 1 次)。
      
      ## Step 4 — 生成本地报告列表页
      
      QA 通过后,运行以下命令更新本地报告浏览器页面(将 reports/ 下所有报告打包为 JS 数据,生成可离线浏览的列表页):
      
      ```bash
      python {TOOLSDIR}/generate_pages.py --local  # 脚本已用 __file__ 锚定,不再依赖 CWD
      ```
      
      ## Step 5 — 清理
      
      ☐ 清理中间文件:`rm -rf {TMPDIR}/chapters/ {TMPDIR}/task*_manifest.json {TMPDIR}/outline.json {TMPDIR}/data-pool.json {TMPDIR}/start_time.txt {TMPDIR}/cautions.json`(Unix)或 `Remove-Item -Recurse -Force`(Windows)
      ☐ 确认 tool-output/ 中无残留临时文件
      
      ## 作业
      
      完成装配和 QA。
      
      ### 输出 task4_manifest.json
      
      使用 `write` 工具创建 `{TMPDIR}/task4_manifest.json`,写入以下 JSON:
      
      ```json
      {
        "task": 4,
        "report_path": "最终报告路径",
        "line_count": 696,
        "chapter_count": 10,
        "word_count": 17600,
        "qa_passed": true
      }
      ```
      
      在回答中只输出报告路径。
      
      
      ---
      ```
      deep-research by hoolulu · github.com/hoolulu/deep-research
      ```
      
  • reports-browser
    • favicon.svg 241 B · in bundle
    • html-docx.min.js 406.3 KB
      !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.htmlDocx=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(_dereq_,module,exports){
      /*!
       * The buffer module from node.js, for the browser.
       *
       * @author   Feross Aboukhadijeh <feross@feross.org> <http://feross.org>
       * @license  MIT
       */
      
      var base64 = _dereq_('base64-js')
      var ieee754 = _dereq_('ieee754')
      var isArray = _dereq_('is-array')
      
      exports.Buffer = Buffer
      exports.SlowBuffer = Buffer
      exports.INSPECT_MAX_BYTES = 50
      Buffer.poolSize = 8192 // not used by this implementation
      
      var kMaxLength = 0x3fffffff
      
      /**
       * If `Buffer.TYPED_ARRAY_SUPPORT`:
       *   === true    Use Uint8Array implementation (fastest)
       *   === false   Use Object implementation (most compatible, even IE6)
       *
       * Browsers that support typed arrays are IE 10+, Firefox 4+, Chrome 7+, Safari 5.1+,
       * Opera 11.6+, iOS 4.2+.
       *
       * Note:
       *
       * - Implementation must support adding new properties to `Uint8Array` instances.
       *   Firefox 4-29 lacked support, fixed in Firefox 30+.
       *   See: https://bugzilla.mozilla.org/show_bug.cgi?id=695438.
       *
       *  - Chrome 9-10 is missing the `TypedArray.prototype.subarray` function.
       *
       *  - IE10 has a broken `TypedArray.prototype.subarray` function which returns arrays of
       *    incorrect length in some situations.
       *
       * We detect these buggy browsers and set `Buffer.TYPED_ARRAY_SUPPORT` to `false` so they will
       * get the Object implementation, which is slower but will work correctly.
       */
      Buffer.TYPED_ARRAY_SUPPORT = (function () {
        try {
          var buf = new ArrayBuffer(0)
          var arr = new Uint8Array(buf)
          arr.foo = function () { return 42 }
          return 42 === arr.foo() && // typed array instances can be augmented
              typeof arr.subarray === 'function' && // chrome 9-10 lack `subarray`
              new Uint8Array(1).subarray(1, 1).byteLength === 0 // ie10 has broken `subarray`
        } catch (e) {
          return false
        }
      })()
      
      /**
       * Class: Buffer
       * =============
       *
       * The Buffer constructor returns instances of `Uint8Array` that are augmented
       * with function properties for all the node `Buffer` API functions. We use
       * `Uint8Array` so that square bracket notation works as expected -- it returns
       * a single octet.
       *
       * By augmenting the instances, we can avoid modifying the `Uint8Array`
       * prototype.
       */
      function Buffer (subject, encoding, noZero) {
        if (!(this instanceof Buffer))
          return new Buffer(subject, encoding, noZero)
      
        var type = typeof subject
      
        // Find the length
        var length
        if (type === 'number')
          length = subject > 0 ? subject >>> 0 : 0
        else if (type === 'string') {
          if (encoding === 'base64')
            subject = base64clean(subject)
          length = Buffer.byteLength(subject, encoding)
        } else if (type === 'object' && subject !== null) { // assume object is array-like
          if (subject.type === 'Buffer' && isArray(subject.data))
            subject = subject.data
          length = +subject.length > 0 ? Math.floor(+subject.length) : 0
        } else
          throw new TypeError('must start with number, buffer, array or string')
      
        if (this.length > kMaxLength)
          throw new RangeError('Attempt to allocate Buffer larger than maximum ' +
            'size: 0x' + kMaxLength.toString(16) + ' bytes')
      
        var buf
        if (Buffer.TYPED_ARRAY_SUPPORT) {
          // Preferred: Return an augmented `Uint8Array` instance for best performance
          buf = Buffer._augment(new Uint8Array(length))
        } else {
          // Fallback: Return THIS instance of Buffer (created by `new`)
          buf = this
          buf.length = length
          buf._isBuffer = true
        }
      
        var i
        if (Buffer.TYPED_ARRAY_SUPPORT && typeof subject.byteLength === 'number') {
          // Speed optimization -- use set if we're copying from a typed array
          buf._set(subject)
        } else if (isArrayish(subject)) {
          // Treat array-ish objects as a byte array
          if (Buffer.isBuffer(subject)) {
            for (i = 0; i < length; i++)
              buf[i] = subject.readUInt8(i)
          } else {
            for (i = 0; i < length; i++)
              buf[i] = ((subject[i] % 256) + 256) % 256
          }
        } else if (type === 'string') {
          buf.write(subject, 0, encoding)
        } else if (type === 'number' && !Buffer.TYPED_ARRAY_SUPPORT && !noZero) {
          for (i = 0; i < length; i++) {
            buf[i] = 0
          }
        }
      
        return buf
      }
      
      Buffer.isBuffer = function (b) {
        return !!(b != null && b._isBuffer)
      }
      
      Buffer.compare = function (a, b) {
        if (!Buffer.isBuffer(a) || !Buffer.isBuffer(b))
          throw new TypeError('Arguments must be Buffers')
      
        var x = a.length
        var y = b.length
        for (var i = 0, len = Math.min(x, y); i < len && a[i] === b[i]; i++) {}
        if (i !== len) {
          x = a[i]
          y = b[i]
        }
        if (x < y) return -1
        if (y < x) return 1
        return 0
      }
      
      Buffer.isEncoding = function (encoding) {
        switch (String(encoding).toLowerCase()) {
          case 'hex':
          case 'utf8':
          case 'utf-8':
          case 'ascii':
          case 'binary':
          case 'base64':
          case 'raw':
          case 'ucs2':
          case 'ucs-2':
          case 'utf16le':
          case 'utf-16le':
            return true
          default:
            return false
        }
      }
      
      Buffer.concat = function (list, totalLength) {
        if (!isArray(list)) throw new TypeError('Usage: Buffer.concat(list[, length])')
      
        if (list.length === 0) {
          return new Buffer(0)
        } else if (list.length === 1) {
          return list[0]
        }
      
        var i
        if (totalLength === undefined) {
          totalLength = 0
          for (i = 0; i < list.length; i++) {
            totalLength += list[i].length
          }
        }
      
        var buf = new Buffer(totalLength)
        var pos = 0
        for (i = 0; i < list.length; i++) {
          var item = list[i]
          item.copy(buf, pos)
          pos += item.length
        }
        return buf
      }
      
      Buffer.byteLength = function (str, encoding) {
        var ret
        str = str + ''
        switch (encoding || 'utf8') {
          case 'ascii':
          case 'binary':
          case 'raw':
            ret = str.length
            break
          case 'ucs2':
          case 'ucs-2':
          case 'utf16le':
          case 'utf-16le':
            ret = str.length * 2
            break
          case 'hex':
            ret = str.length >>> 1
            break
          case 'utf8':
          case 'utf-8':
            ret = utf8ToBytes(str).length
            break
          case 'base64':
            ret = base64ToBytes(str).length
            break
          default:
            ret = str.length
        }
        return ret
      }
      
      // pre-set for values that may exist in the future
      Buffer.prototype.length = undefined
      Buffer.prototype.parent = undefined
      
      // toString(encoding, start=0, end=buffer.length)
      Buffer.prototype.toString = function (encoding, start, end) {
        var loweredCase = false
      
        start = start >>> 0
        end = end === undefined || end === Infinity ? this.length : end >>> 0
      
        if (!encoding) encoding = 'utf8'
        if (start < 0) start = 0
        if (end > this.length) end = this.length
        if (end <= start) return ''
      
        while (true) {
          switch (encoding) {
            case 'hex':
              return hexSlice(this, start, end)
      
            case 'utf8':
            case 'utf-8':
              return utf8Slice(this, start, end)
      
            case 'ascii':
              return asciiSlice(this, start, end)
      
            case 'binary':
              return binarySlice(this, start, end)
      
            case 'base64':
              return base64Slice(this, start, end)
      
            case 'ucs2':
            case 'ucs-2':
            case 'utf16le':
            case 'utf-16le':
              return utf16leSlice(this, start, end)
      
            default:
              if (loweredCase)
                throw new TypeError('Unknown encoding: ' + encoding)
              encoding = (encoding + '').toLowerCase()
              loweredCase = true
          }
        }
      }
      
      Buffer.prototype.equals = function (b) {
        if(!Buffer.isBuffer(b)) throw new TypeError('Argument must be a Buffer')
        return Buffer.compare(this, b) === 0
      }
      
      Buffer.prototype.inspect = function () {
        var str = ''
        var max = exports.INSPECT_MAX_BYTES
        if (this.length > 0) {
          str = this.toString('hex', 0, max).match(/.{2}/g).join(' ')
          if (this.length > max)
            str += ' ... '
        }
        return '<Buffer ' + str + '>'
      }
      
      Buffer.prototype.compare = function (b) {
        if (!Buffer.isBuffer(b)) throw new TypeError('Argument must be a Buffer')
        return Buffer.compare(this, b)
      }
      
      // `get` will be removed in Node 0.13+
      Buffer.prototype.get = function (offset) {
        console.log('.get() is deprecated. Access using array indexes instead.')
        return this.readUInt8(offset)
      }
      
      // `set` will be removed in Node 0.13+
      Buffer.prototype.set = function (v, offset) {
        console.log('.set() is deprecated. Access using array indexes instead.')
        return this.writeUInt8(v, offset)
      }
      
      function hexWrite (buf, string, offset, length) {
        offset = Number(offset) || 0
        var remaining = buf.length - offset
        if (!length) {
          length = remaining
        } else {
          length = Number(length)
          if (length > remaining) {
            length = remaining
          }
        }
      
        // must be an even number of digits
        var strLen = string.length
        if (strLen % 2 !== 0) throw new Error('Invalid hex string')
      
        if (length > strLen / 2) {
          length = strLen / 2
        }
        for (var i = 0; i < length; i++) {
          var byte = parseInt(string.substr(i * 2, 2), 16)
          if (isNaN(byte)) throw new Error('Invalid hex string')
          buf[offset + i] = byte
        }
        return i
      }
      
      function utf8Write (buf, string, offset, length) {
        var charsWritten = blitBuffer(utf8ToBytes(string), buf, offset, length)
        return charsWritten
      }
      
      function asciiWrite (buf, string, offset, length) {
        var charsWritten = blitBuffer(asciiToBytes(string), buf, offset, length)
        return charsWritten
      }
      
      function binaryWrite (buf, string, offset, length) {
        return asciiWrite(buf, string, offset, length)
      }
      
      function base64Write (buf, string, offset, length) {
        var charsWritten = blitBuffer(base64ToBytes(string), buf, offset, length)
        return charsWritten
      }
      
      function utf16leWrite (buf, string, offset, length) {
        var charsWritten = blitBuffer(utf16leToBytes(string), buf, offset, length, 2)
        return charsWritten
      }
      
      Buffer.prototype.write = function (string, offset, length, encoding) {
        // Support both (string, offset, length, encoding)
        // and the legacy (string, encoding, offset, length)
        if (isFinite(offset)) {
          if (!isFinite(length)) {
            encoding = length
            length = undefined
          }
        } else {  // legacy
          var swap = encoding
          encoding = offset
          offset = length
          length = swap
        }
      
        offset = Number(offset) || 0
        var remaining = this.length - offset
        if (!length) {
          length = remaining
        } else {
          length = Number(length)
          if (length > remaining) {
            length = remaining
          }
        }
        encoding = String(encoding || 'utf8').toLowerCase()
      
        var ret
        switch (encoding) {
          case 'hex':
            ret = hexWrite(this, string, offset, length)
            break
          case 'utf8':
          case 'utf-8':
            ret = utf8Write(this, string, offset, length)
            break
          case 'ascii':
            ret = asciiWrite(this, string, offset, length)
            break
          case 'binary':
            ret = binaryWrite(this, string, offset, length)
            break
          case 'base64':
            ret = base64Write(this, string, offset, length)
            break
          case 'ucs2':
          case 'ucs-2':
          case 'utf16le':
          case 'utf-16le':
            ret = utf16leWrite(this, string, offset, length)
            break
          default:
            throw new TypeError('Unknown encoding: ' + encoding)
        }
        return ret
      }
      
      Buffer.prototype.toJSON = function () {
        return {
          type: 'Buffer',
          data: Array.prototype.slice.call(this._arr || this, 0)
        }
      }
      
      function base64Slice (buf, start, end) {
        if (start === 0 && end === buf.length) {
          return base64.fromByteArray(buf)
        } else {
          return base64.fromByteArray(buf.slice(start, end))
        }
      }
      
      function utf8Slice (buf, start, end) {
        var res = ''
        var tmp = ''
        end = Math.min(buf.length, end)
      
        for (var i = start; i < end; i++) {
          if (buf[i] <= 0x7F) {
            res += decodeUtf8Char(tmp) + String.fromCharCode(buf[i])
            tmp = ''
          } else {
            tmp += '%' + buf[i].toString(16)
          }
        }
      
        return res + decodeUtf8Char(tmp)
      }
      
      function asciiSlice (buf, start, end) {
        var ret = ''
        end = Math.min(buf.length, end)
      
        for (var i = start; i < end; i++) {
          ret += String.fromCharCode(buf[i])
        }
        return ret
      }
      
      function binarySlice (buf, start, end) {
        return asciiSlice(buf, start, end)
      }
      
      function hexSlice (buf, start, end) {
        var len = buf.length
      
        if (!start || start < 0) start = 0
        if (!end || end < 0 || end > len) end = len
      
        var out = ''
        for (var i = start; i < end; i++) {
          out += toHex(buf[i])
        }
        return out
      }
      
      function utf16leSlice (buf, start, end) {
        var bytes = buf.slice(start, end)
        var res = ''
        for (var i = 0; i < bytes.length; i += 2) {
          res += String.fromCharCode(bytes[i] + bytes[i + 1] * 256)
        }
        return res
      }
      
      Buffer.prototype.slice = function (start, end) {
        var len = this.length
        start = ~~start
        end = end === undefined ? len : ~~end
      
        if (start < 0) {
          start += len;
          if (start < 0)
            start = 0
        } else if (start > len) {
          start = len
        }
      
        if (end < 0) {
          end += len
          if (end < 0)
            end = 0
        } else if (end > len) {
          end = len
        }
      
        if (end < start)
          end = start
      
        if (Buffer.TYPED_ARRAY_SUPPORT) {
          return Buffer._augment(this.subarray(start, end))
        } else {
          var sliceLen = end - start
          var newBuf = new Buffer(sliceLen, undefined, true)
          for (var i = 0; i < sliceLen; i++) {
            newBuf[i] = this[i + start]
          }
          return newBuf
        }
      }
      
      /*
       * Need to make sure that buffer isn't trying to write out of bounds.
       */
      function checkOffset (offset, ext, length) {
        if ((offset % 1) !== 0 || offset < 0)
          throw new RangeError('offset is not uint')
        if (offset + ext > length)
          throw new RangeError('Trying to access beyond buffer length')
      }
      
      Buffer.prototype.readUInt8 = function (offset, noAssert) {
        if (!noAssert)
          checkOffset(offset, 1, this.length)
        return this[offset]
      }
      
      Buffer.prototype.readUInt16LE = function (offset, noAssert) {
        if (!noAssert)
          checkOffset(offset, 2, this.length)
        return this[offset] | (this[offset + 1] << 8)
      }
      
      Buffer.prototype.readUInt16BE = function (offset, noAssert) {
        if (!noAssert)
          checkOffset(offset, 2, this.length)
        return (this[offset] << 8) | this[offset + 1]
      }
      
      Buffer.prototype.readUInt32LE = function (offset, noAssert) {
        if (!noAssert)
          checkOffset(offset, 4, this.length)
      
        return ((this[offset]) |
            (this[offset + 1] << 8) |
            (this[offset + 2] << 16)) +
            (this[offset + 3] * 0x1000000)
      }
      
      Buffer.prototype.readUInt32BE = function (offset, noAssert) {
        if (!noAssert)
          checkOffset(offset, 4, this.length)
      
        return (this[offset] * 0x1000000) +
            ((this[offset + 1] << 16) |
            (this[offset + 2] << 8) |
            this[offset + 3])
      }
      
      Buffer.prototype.readInt8 = function (offset, noAssert) {
        if (!noAssert)
          checkOffset(offset, 1, this.length)
        if (!(this[offset] & 0x80))
          return (this[offset])
        return ((0xff - this[offset] + 1) * -1)
      }
      
      Buffer.prototype.readInt16LE = function (offset, noAssert) {
        if (!noAssert)
          checkOffset(offset, 2, this.length)
        var val = this[offset] | (this[offset + 1] << 8)
        return (val & 0x8000) ? val | 0xFFFF0000 : val
      }
      
      Buffer.prototype.readInt16BE = function (offset, noAssert) {
        if (!noAssert)
          checkOffset(offset, 2, this.length)
        var val = this[offset + 1] | (this[offset] << 8)
        return (val & 0x8000) ? val | 0xFFFF0000 : val
      }
      
      Buffer.prototype.readInt32LE = function (offset, noAssert) {
        if (!noAssert)
          checkOffset(offset, 4, this.length)
      
        return (this[offset]) |
            (this[offset + 1] << 8) |
            (this[offset + 2] << 16) |
            (this[offset + 3] << 24)
      }
      
      Buffer.prototype.readInt32BE = function (offset, noAssert) {
        if (!noAssert)
          checkOffset(offset, 4, this.length)
      
        return (this[offset] << 24) |
            (this[offset + 1] << 16) |
            (this[offset + 2] << 8) |
            (this[offset + 3])
      }
      
      Buffer.prototype.readFloatLE = function (offset, noAssert) {
        if (!noAssert)
          checkOffset(offset, 4, this.length)
        return ieee754.read(this, offset, true, 23, 4)
      }
      
      Buffer.prototype.readFloatBE = function (offset, noAssert) {
        if (!noAssert)
          checkOffset(offset, 4, this.length)
        return ieee754.read(this, offset, false, 23, 4)
      }
      
      Buffer.prototype.readDoubleLE = function (offset, noAssert) {
        if (!noAssert)
          checkOffset(offset, 8, this.length)
        return ieee754.read(this, offset, true, 52, 8)
      }
      
      Buffer.prototype.readDoubleBE = function (offset, noAssert) {
        if (!noAssert)
          checkOffset(offset, 8, this.length)
        return ieee754.read(this, offset, false, 52, 8)
      }
      
      function checkInt (buf, value, offset, ext, max, min) {
        if (!Buffer.isBuffer(buf)) throw new TypeError('buffer must be a Buffer instance')
        if (value > max || value < min) throw new TypeError('value is out of bounds')
        if (offset + ext > buf.length) throw new TypeError('index out of range')
      }
      
      Buffer.prototype.writeUInt8 = function (value, offset, noAssert) {
        value = +value
        offset = offset >>> 0
        if (!noAssert)
          checkInt(this, value, offset, 1, 0xff, 0)
        if (!Buffer.TYPED_ARRAY_SUPPORT) value = Math.floor(value)
        this[offset] = value
        return offset + 1
      }
      
      function objectWriteUInt16 (buf, value, offset, littleEndian) {
        if (value < 0) value = 0xffff + value + 1
        for (var i = 0, j = Math.min(buf.length - offset, 2); i < j; i++) {
          buf[offset + i] = (value & (0xff << (8 * (littleEndian ? i : 1 - i)))) >>>
            (littleEndian ? i : 1 - i) * 8
        }
      }
      
      Buffer.prototype.writeUInt16LE = function (value, offset, noAssert) {
        value = +value
        offset = offset >>> 0
        if (!noAssert)
          checkInt(this, value, offset, 2, 0xffff, 0)
        if (Buffer.TYPED_ARRAY_SUPPORT) {
          this[offset] = value
          this[offset + 1] = (value >>> 8)
        } else objectWriteUInt16(this, value, offset, true)
        return offset + 2
      }
      
      Buffer.prototype.writeUInt16BE = function (value, offset, noAssert) {
        value = +value
        offset = offset >>> 0
        if (!noAssert)
          checkInt(this, value, offset, 2, 0xffff, 0)
        if (Buffer.TYPED_ARRAY_SUPPORT) {
          this[offset] = (value >>> 8)
          this[offset + 1] = value
        } else objectWriteUInt16(this, value, offset, false)
        return offset + 2
      }
      
      function objectWriteUInt32 (buf, value, offset, littleEndian) {
        if (value < 0) value = 0xffffffff + value + 1
        for (var i = 0, j = Math.min(buf.length - offset, 4); i < j; i++) {
          buf[offset + i] = (value >>> (littleEndian ? i : 3 - i) * 8) & 0xff
        }
      }
      
      Buffer.prototype.writeUInt32LE = function (value, offset, noAssert) {
        value = +value
        offset = offset >>> 0
        if (!noAssert)
          checkInt(this, value, offset, 4, 0xffffffff, 0)
        if (Buffer.TYPED_ARRAY_SUPPORT) {
          this[offset + 3] = (value >>> 24)
          this[offset + 2] = (value >>> 16)
          this[offset + 1] = (value >>> 8)
          this[offset] = value
        } else objectWriteUInt32(this, value, offset, true)
        return offset + 4
      }
      
      Buffer.prototype.writeUInt32BE = function (value, offset, noAssert) {
        value = +value
        offset = offset >>> 0
        if (!noAssert)
          checkInt(this, value, offset, 4, 0xffffffff, 0)
        if (Buffer.TYPED_ARRAY_SUPPORT) {
          this[offset] = (value >>> 24)
          this[offset + 1] = (value >>> 16)
          this[offset + 2] = (value >>> 8)
          this[offset + 3] = value
        } else objectWriteUInt32(this, value, offset, false)
        return offset + 4
      }
      
      Buffer.prototype.writeInt8 = function (value, offset, noAssert) {
        value = +value
        offset = offset >>> 0
        if (!noAssert)
          checkInt(this, value, offset, 1, 0x7f, -0x80)
        if (!Buffer.TYPED_ARRAY_SUPPORT) value = Math.floor(value)
        if (value < 0) value = 0xff + value + 1
        this[offset] = value
        return offset + 1
      }
      
      Buffer.prototype.writeInt16LE = function (value, offset, noAssert) {
        value = +value
        offset = offset >>> 0
        if (!noAssert)
          checkInt(this, value, offset, 2, 0x7fff, -0x8000)
        if (Buffer.TYPED_ARRAY_SUPPORT) {
          this[offset] = value
          this[offset + 1] = (value >>> 8)
        } else objectWriteUInt16(this, value, offset, true)
        return offset + 2
      }
      
      Buffer.prototype.writeInt16BE = function (value, offset, noAssert) {
        value = +value
        offset = offset >>> 0
        if (!noAssert)
          checkInt(this, value, offset, 2, 0x7fff, -0x8000)
        if (Buffer.TYPED_ARRAY_SUPPORT) {
          this[offset] = (value >>> 8)
          this[offset + 1] = value
        } else objectWriteUInt16(this, value, offset, false)
        return offset + 2
      }
      
      Buffer.prototype.writeInt32LE = function (value, offset, noAssert) {
        value = +value
        offset = offset >>> 0
        if (!noAssert)
          checkInt(this, value, offset, 4, 0x7fffffff, -0x80000000)
        if (Buffer.TYPED_ARRAY_SUPPORT) {
          this[offset] = value
          this[offset + 1] = (value >>> 8)
          this[offset + 2] = (value >>> 16)
          this[offset + 3] = (value >>> 24)
        } else objectWriteUInt32(this, value, offset, true)
        return offset + 4
      }
      
      Buffer.prototype.writeInt32BE = function (value, offset, noAssert) {
        value = +value
        offset = offset >>> 0
        if (!noAssert)
          checkInt(this, value, offset, 4, 0x7fffffff, -0x80000000)
        if (value < 0) value = 0xffffffff + value + 1
        if (Buffer.TYPED_ARRAY_SUPPORT) {
          this[offset] = (value >>> 24)
          this[offset + 1] = (value >>> 16)
          this[offset + 2] = (value >>> 8)
          this[offset + 3] = value
        } else objectWriteUInt32(this, value, offset, false)
        return offset + 4
      }
      
      function checkIEEE754 (buf, value, offset, ext, max, min) {
        if (value > max || value < min) throw new TypeError('value is out of bounds')
        if (offset + ext > buf.length) throw new TypeError('index out of range')
      }
      
      function writeFloat (buf, value, offset, littleEndian, noAssert) {
        if (!noAssert)
          checkIEEE754(buf, value, offset, 4, 3.4028234663852886e+38, -3.4028234663852886e+38)
        ieee754.write(buf, value, offset, littleEndian, 23, 4)
        return offset + 4
      }
      
      Buffer.prototype.writeFloatLE = function (value, offset, noAssert) {
        return writeFloat(this, value, offset, true, noAssert)
      }
      
      Buffer.prototype.writeFloatBE = function (value, offset, noAssert) {
        return writeFloat(this, value, offset, false, noAssert)
      }
      
      function writeDouble (buf, value, offset, littleEndian, noAssert) {
        if (!noAssert)
          checkIEEE754(buf, value, offset, 8, 1.7976931348623157E+308, -1.7976931348623157E+308)
        ieee754.write(buf, value, offset, littleEndian, 52, 8)
        return offset + 8
      }
      
      Buffer.prototype.writeDoubleLE = function (value, offset, noAssert) {
        return writeDouble(this, value, offset, true, noAssert)
      }
      
      Buffer.prototype.writeDoubleBE = function (value, offset, noAssert) {
        return writeDouble(this, value, offset, false, noAssert)
      }
      
      // copy(targetBuffer, targetStart=0, sourceStart=0, sourceEnd=buffer.length)
      Buffer.prototype.copy = function (target, target_start, start, end) {
        var source = this
      
        if (!start) start = 0
        if (!end && end !== 0) end = this.length
        if (!target_start) target_start = 0
      
        // Copy 0 bytes; we're done
        if (end === start) return
        if (target.length === 0 || source.length === 0) return
      
        // Fatal error conditions
        if (end < start) throw new TypeError('sourceEnd < sourceStart')
        if (target_start < 0 || target_start >= target.length)
          throw new TypeError('targetStart out of bounds')
        if (start < 0 || start >= source.length) throw new TypeError('sourceStart out of bounds')
        if (end < 0 || end > source.length) throw new TypeError('sourceEnd out of bounds')
      
        // Are we oob?
        if (end > this.length)
          end = this.length
        if (target.length - target_start < end - start)
          end = target.length - target_start + start
      
        var len = end - start
      
        if (len < 1000 || !Buffer.TYPED_ARRAY_SUPPORT) {
          for (var i = 0; i < len; i++) {
            target[i + target_start] = this[i + start]
          }
        } else {
          target._set(this.subarray(start, start + len), target_start)
        }
      }
      
      // fill(value, start=0, end=buffer.length)
      Buffer.prototype.fill = function (value, start, end) {
        if (!value) value = 0
        if (!start) start = 0
        if (!end) end = this.length
      
        if (end < start) throw new TypeError('end < start')
      
        // Fill 0 bytes; we're done
        if (end === start) return
        if (this.length === 0) return
      
        if (start < 0 || start >= this.length) throw new TypeError('start out of bounds')
        if (end < 0 || end > this.length) throw new TypeError('end out of bounds')
      
        var i
        if (typeof value === 'number') {
          for (i = start; i < end; i++) {
            this[i] = value
          }
        } else {
          var bytes = utf8ToBytes(value.toString())
          var len = bytes.length
          for (i = start; i < end; i++) {
            this[i] = bytes[i % len]
          }
        }
      
        return this
      }
      
      /**
       * Creates a new `ArrayBuffer` with the *copied* memory of the buffer instance.
       * Added in Node 0.12. Only available in browsers that support ArrayBuffer.
       */
      Buffer.prototype.toArrayBuffer = function () {
        if (typeof Uint8Array !== 'undefined') {
          if (Buffer.TYPED_ARRAY_SUPPORT) {
            return (new Buffer(this)).buffer
          } else {
            var buf = new Uint8Array(this.length)
            for (var i = 0, len = buf.length; i < len; i += 1) {
              buf[i] = this[i]
            }
            return buf.buffer
          }
        } else {
          throw new TypeError('Buffer.toArrayBuffer not supported in this browser')
        }
      }
      
      // HELPER FUNCTIONS
      // ================
      
      var BP = Buffer.prototype
      
      /**
       * Augment a Uint8Array *instance* (not the Uint8Array class!) with Buffer methods
       */
      Buffer._augment = function (arr) {
        arr.constructor = Buffer
        arr._isBuffer = true
      
        // save reference to original Uint8Array get/set methods before overwriting
        arr._get = arr.get
        arr._set = arr.set
      
        // deprecated, will be removed in node 0.13+
        arr.get = BP.get
        arr.set = BP.set
      
        arr.write = BP.write
        arr.toString = BP.toString
        arr.toLocaleString = BP.toString
        arr.toJSON = BP.toJSON
        arr.equals = BP.equals
        arr.compare = BP.compare
        arr.copy = BP.copy
        arr.slice = BP.slice
        arr.readUInt8 = BP.readUInt8
        arr.readUInt16LE = BP.readUInt16LE
        arr.readUInt16BE = BP.readUInt16BE
        arr.readUInt32LE = BP.readUInt32LE
        arr.readUInt32BE = BP.readUInt32BE
        arr.readInt8 = BP.readInt8
        arr.readInt16LE = BP.readInt16LE
        arr.readInt16BE = BP.readInt16BE
        arr.readInt32LE = BP.readInt32LE
        arr.readInt32BE = BP.readInt32BE
        arr.readFloatLE = BP.readFloatLE
        arr.readFloatBE = BP.readFloatBE
        arr.readDoubleLE = BP.readDoubleLE
        arr.readDoubleBE = BP.readDoubleBE
        arr.writeUInt8 = BP.writeUInt8
        arr.writeUInt16LE = BP.writeUInt16LE
        arr.writeUInt16BE = BP.writeUInt16BE
        arr.writeUInt32LE = BP.writeUInt32LE
        arr.writeUInt32BE = BP.writeUInt32BE
        arr.writeInt8 = BP.writeInt8
        arr.writeInt16LE = BP.writeInt16LE
        arr.writeInt16BE = BP.writeInt16BE
        arr.writeInt32LE = BP.writeInt32LE
        arr.writeInt32BE = BP.writeInt32BE
        arr.writeFloatLE = BP.writeFloatLE
        arr.writeFloatBE = BP.writeFloatBE
        arr.writeDoubleLE = BP.writeDoubleLE
        arr.writeDoubleBE = BP.writeDoubleBE
        arr.fill = BP.fill
        arr.inspect = BP.inspect
        arr.toArrayBuffer = BP.toArrayBuffer
      
        return arr
      }
      
      var INVALID_BASE64_RE = /[^+\/0-9A-z]/g
      
      function base64clean (str) {
        // Node strips out invalid characters like \n and \t from the string, base64-js does not
        str = stringtrim(str).replace(INVALID_BASE64_RE, '')
        // Node allows for non-padded base64 strings (missing trailing ===), base64-js does not
        while (str.length % 4 !== 0) {
          str = str + '='
        }
        return str
      }
      
      function stringtrim (str) {
        if (str.trim) return str.trim()
        return str.replace(/^\s+|\s+$/g, '')
      }
      
      function isArrayish (subject) {
        return isArray(subject) || Buffer.isBuffer(subject) ||
            subject && typeof subject === 'object' &&
            typeof subject.length === 'number'
      }
      
      function toHex (n) {
        if (n < 16) return '0' + n.toString(16)
        return n.toString(16)
      }
      
      function utf8ToBytes (str) {
        var byteArray = []
        for (var i = 0; i < str.length; i++) {
          var b = str.charCodeAt(i)
          if (b <= 0x7F) {
            byteArray.push(b)
          } else {
            var start = i
            if (b >= 0xD800 && b <= 0xDFFF) i++
            var h = encodeURIComponent(str.slice(start, i+1)).substr(1).split('%')
            for (var j = 0; j < h.length; j++) {
              byteArray.push(parseInt(h[j], 16))
            }
          }
        }
        return byteArray
      }
      
      function asciiToBytes (str) {
        var byteArray = []
        for (var i = 0; i < str.length; i++) {
          // Node's code seems to be doing this and not & 0x7F..
          byteArray.push(str.charCodeAt(i) & 0xFF)
        }
        return byteArray
      }
      
      function utf16leToBytes (str) {
        var c, hi, lo
        var byteArray = []
        for (var i = 0; i < str.length; i++) {
          c = str.charCodeAt(i)
          hi = c >> 8
          lo = c % 256
          byteArray.push(lo)
          byteArray.push(hi)
        }
      
        return byteArray
      }
      
      function base64ToBytes (str) {
        return base64.toByteArray(str)
      }
      
      function blitBuffer (src, dst, offset, length, unitSize) {
        if (unitSize) length -= length % unitSize;
        for (var i = 0; i < length; i++) {
          if ((i + offset >= dst.length) || (i >= src.length))
            break
          dst[i + offset] = src[i]
        }
        return i
      }
      
      function decodeUtf8Char (str) {
        try {
          return decodeURIComponent(str)
        } catch (err) {
          return String.fromCharCode(0xFFFD) // UTF 8 invalid char
        }
      }
      
      },{"base64-js":2,"ieee754":3,"is-array":4}],2:[function(_dereq_,module,exports){
      var lookup = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
      
      ;(function (exports) {
      	'use strict';
      
        var Arr = (typeof Uint8Array !== 'undefined')
          ? Uint8Array
          : Array
      
      	var PLUS   = '+'.charCodeAt(0)
      	var SLASH  = '/'.charCodeAt(0)
      	var NUMBER = '0'.charCodeAt(0)
      	var LOWER  = 'a'.charCodeAt(0)
      	var UPPER  = 'A'.charCodeAt(0)
      
      	function decode (elt) {
      		var code = elt.charCodeAt(0)
      		if (code === PLUS)
      			return 62 // '+'
      		if (code === SLASH)
      			return 63 // '/'
      		if (code < NUMBER)
      			return -1 //no match
      		if (code < NUMBER + 10)
      			return code - NUMBER + 26 + 26
      		if (code < UPPER + 26)
      			return code - UPPER
      		if (code < LOWER + 26)
      			return code - LOWER + 26
      	}
      
      	function b64ToByteArray (b64) {
      		var i, j, l, tmp, placeHolders, arr
      
      		if (b64.length % 4 > 0) {
      			throw new Error('Invalid string. Length must be a multiple of 4')
      		}
      
      		// the number of equal signs (place holders)
      		// if there are two placeholders, than the two characters before it
      		// represent one byte
      		// if there is only one, then the three characters before it represent 2 bytes
      		// this is just a cheap hack to not do indexOf twice
      		var len = b64.length
      		placeHolders = '=' === b64.charAt(len - 2) ? 2 : '=' === b64.charAt(len - 1) ? 1 : 0
      
      		// base64 is 4/3 + up to two characters of the original data
      		arr = new Arr(b64.length * 3 / 4 - placeHolders)
      
      		// if there are placeholders, only get up to the last complete 4 chars
      		l = placeHolders > 0 ? b64.length - 4 : b64.length
      
      		var L = 0
      
      		function push (v) {
      			arr[L++] = v
      		}
      
      		for (i = 0, j = 0; i < l; i += 4, j += 3) {
      			tmp = (decode(b64.charAt(i)) << 18) | (decode(b64.charAt(i + 1)) << 12) | (decode(b64.charAt(i + 2)) << 6) | decode(b64.charAt(i + 3))
      			push((tmp & 0xFF0000) >> 16)
      			push((tmp & 0xFF00) >> 8)
      			push(tmp & 0xFF)
      		}
      
      		if (placeHolders === 2) {
      			tmp = (decode(b64.charAt(i)) << 2) | (decode(b64.charAt(i + 1)) >> 4)
      			push(tmp & 0xFF)
      		} else if (placeHolders === 1) {
      			tmp = (decode(b64.charAt(i)) << 10) | (decode(b64.charAt(i + 1)) << 4) | (decode(b64.charAt(i + 2)) >> 2)
      			push((tmp >> 8) & 0xFF)
      			push(tmp & 0xFF)
      		}
      
      		return arr
      	}
      
      	function uint8ToBase64 (uint8) {
      		var i,
      			extraBytes = uint8.length % 3, // if we have 1 byte left, pad 2 bytes
      			output = "",
      			temp, length
      
      		function encode (num) {
      			return lookup.charAt(num)
      		}
      
      		function tripletToBase64 (num) {
      			return encode(num >> 18 & 0x3F) + encode(num >> 12 & 0x3F) + encode(num >> 6 & 0x3F) + encode(num & 0x3F)
      		}
      
      		// go through the array every three bytes, we'll deal with trailing stuff later
      		for (i = 0, length = uint8.length - extraBytes; i < length; i += 3) {
      			temp = (uint8[i] << 16) + (uint8[i + 1] << 8) + (uint8[i + 2])
      			output += tripletToBase64(temp)
      		}
      
      		// pad the end with zeros, but make sure to not forget the extra bytes
      		switch (extraBytes) {
      			case 1:
      				temp = uint8[uint8.length - 1]
      				output += encode(temp >> 2)
      				output += encode((temp << 4) & 0x3F)
      				output += '=='
      				break
      			case 2:
      				temp = (uint8[uint8.length - 2] << 8) + (uint8[uint8.length - 1])
      				output += encode(temp >> 10)
      				output += encode((temp >> 4) & 0x3F)
      				output += encode((temp << 2) & 0x3F)
      				output += '='
      				break
      		}
      
      		return output
      	}
      
      	exports.toByteArray = b64ToByteArray
      	exports.fromByteArray = uint8ToBase64
      }(typeof exports === 'undefined' ? (this.base64js = {}) : exports))
      
      },{}],3:[function(_dereq_,module,exports){
      exports.read = function (buffer, offset, isLE, mLen, nBytes) {
        var e, m
        var eLen = nBytes * 8 - mLen - 1
        var eMax = (1 << eLen) - 1
        var eBias = eMax >> 1
        var nBits = -7
        var i = isLE ? (nBytes - 1) : 0
        var d = isLE ? -1 : 1
        var s = buffer[offset + i]
      
        i += d
      
        e = s & ((1 << (-nBits)) - 1)
        s >>= (-nBits)
        nBits += eLen
        for (; nBits > 0; e = e * 256 + buffer[offset + i], i += d, nBits -= 8) {}
      
        m = e & ((1 << (-nBits)) - 1)
        e >>= (-nBits)
        nBits += mLen
        for (; nBits > 0; m = m * 256 + buffer[offset + i], i += d, nBits -= 8) {}
      
        if (e === 0) {
          e = 1 - eBias
        } else if (e === eMax) {
          return m ? NaN : ((s ? -1 : 1) * Infinity)
        } else {
          m = m + Math.pow(2, mLen)
          e = e - eBias
        }
        return (s ? -1 : 1) * m * Math.pow(2, e - mLen)
      }
      
      exports.write = function (buffer, value, offset, isLE, mLen, nBytes) {
        var e, m, c
        var eLen = nBytes * 8 - mLen - 1
        var eMax = (1 << eLen) - 1
        var eBias = eMax >> 1
        var rt = (mLen === 23 ? Math.pow(2, -24) - Math.pow(2, -77) : 0)
        var i = isLE ? 0 : (nBytes - 1)
        var d = isLE ? 1 : -1
        var s = value < 0 || (value === 0 && 1 / value < 0) ? 1 : 0
      
        value = Math.abs(value)
      
        if (isNaN(value) || value === Infinity) {
          m = isNaN(value) ? 1 : 0
          e = eMax
        } else {
          e = Math.floor(Math.log(value) / Math.LN2)
          if (value * (c = Math.pow(2, -e)) < 1) {
            e--
            c *= 2
          }
          if (e + eBias >= 1) {
            value += rt / c
          } else {
            value += rt * Math.pow(2, 1 - eBias)
          }
          if (value * c >= 2) {
            e++
            c /= 2
          }
      
          if (e + eBias >= eMax) {
            m = 0
            e = eMax
          } else if (e + eBias >= 1) {
            m = (value * c - 1) * Math.pow(2, mLen)
            e = e + eBias
          } else {
            m = value * Math.pow(2, eBias - 1) * Math.pow(2, mLen)
            e = 0
          }
        }
      
        for (; mLen >= 8; buffer[offset + i] = m & 0xff, i += d, m /= 256, mLen -= 8) {}
      
        e = (e << mLen) | m
        eLen += mLen
        for (; eLen > 0; buffer[offset + i] = e & 0xff, i += d, e /= 256, eLen -= 8) {}
      
        buffer[offset + i - d] |= s * 128
      }
      
      },{}],4:[function(_dereq_,module,exports){
      
      /**
       * isArray
       */
      
      var isArray = Array.isArray;
      
      /**
       * toString
       */
      
      var str = Object.prototype.toString;
      
      /**
       * Whether or not the given `val`
       * is an array.
       *
       * example:
       *
       *        isArray([]);
       *        // > true
       *        isArray(arguments);
       *        // > false
       *        isArray('');
       *        // > false
       *
       * @param {mixed} val
       * @return {bool}
       */
      
      module.exports = isArray || function (val) {
        return !! val && '[object Array]' == str.call(val);
      };
      
      },{}],5:[function(_dereq_,module,exports){
      'use strict';
      var DataReader = _dereq_('./dataReader');
      
      function ArrayReader(data) {
          if (data) {
              this.data = data;
              this.length = this.data.length;
              this.index = 0;
              this.zero = 0;
      
              for(var i = 0; i < this.data.length; i++) {
                  data[i] = data[i] & 0xFF;
              }
          }
      }
      ArrayReader.prototype = new DataReader();
      /**
       * @see DataReader.byteAt
       */
      ArrayReader.prototype.byteAt = function(i) {
          return this.data[this.zero + i];
      };
      /**
       * @see DataReader.lastIndexOfSignature
       */
      ArrayReader.prototype.lastIndexOfSignature = function(sig) {
          var sig0 = sig.charCodeAt(0),
              sig1 = sig.charCodeAt(1),
              sig2 = sig.charCodeAt(2),
              sig3 = sig.charCodeAt(3);
          for (var i = this.length - 4; i >= 0; --i) {
              if (this.data[i] === sig0 && this.data[i + 1] === sig1 && this.data[i + 2] === sig2 && this.data[i + 3] === sig3) {
                  return i - this.zero;
              }
          }
      
          return -1;
      };
      /**
       * @see DataReader.readData
       */
      ArrayReader.prototype.readData = function(size) {
          this.checkOffset(size);
          if(size === 0) {
              return [];
          }
          var result = this.data.slice(this.zero + this.index, this.zero + this.index + size);
          this.index += size;
          return result;
      };
      module.exports = ArrayReader;
      
      },{"./dataReader":10}],6:[function(_dereq_,module,exports){
      'use strict';
      // private property
      var _keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
      
      
      // public method for encoding
      exports.encode = function(input, utf8) {
          var output = "";
          var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
          var i = 0;
      
          while (i < input.length) {
      
              chr1 = input.charCodeAt(i++);
              chr2 = input.charCodeAt(i++);
              chr3 = input.charCodeAt(i++);
      
              enc1 = chr1 >> 2;
              enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
              enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
              enc4 = chr3 & 63;
      
              if (isNaN(chr2)) {
                  enc3 = enc4 = 64;
              }
              else if (isNaN(chr3)) {
                  enc4 = 64;
              }
      
              output = output + _keyStr.charAt(enc1) + _keyStr.charAt(enc2) + _keyStr.charAt(enc3) + _keyStr.charAt(enc4);
      
          }
      
          return output;
      };
      
      // public method for decoding
      exports.decode = function(input, utf8) {
          var output = "";
          var chr1, chr2, chr3;
          var enc1, enc2, enc3, enc4;
          var i = 0;
      
          input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
      
          while (i < input.length) {
      
              enc1 = _keyStr.indexOf(input.charAt(i++));
              enc2 = _keyStr.indexOf(input.charAt(i++));
              enc3 = _keyStr.indexOf(input.charAt(i++));
              enc4 = _keyStr.indexOf(input.charAt(i++));
      
              chr1 = (enc1 << 2) | (enc2 >> 4);
              chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
              chr3 = ((enc3 & 3) << 6) | enc4;
      
              output = output + String.fromCharCode(chr1);
      
              if (enc3 != 64) {
                  output = output + String.fromCharCode(chr2);
              }
              if (enc4 != 64) {
                  output = output + String.fromCharCode(chr3);
              }
      
          }
      
          return output;
      
      };
      
      },{}],7:[function(_dereq_,module,exports){
      'use strict';
      function CompressedObject() {
          this.compressedSize = 0;
          this.uncompressedSize = 0;
          this.crc32 = 0;
          this.compressionMethod = null;
          this.compressedContent = null;
      }
      
      CompressedObject.prototype = {
          /**
           * Return the decompressed content in an unspecified format.
           * The format will depend on the decompressor.
           * @return {Object} the decompressed content.
           */
          getContent: function() {
              return null; // see implementation
          },
          /**
           * Return the compressed content in an unspecified format.
           * The format will depend on the compressed conten source.
           * @return {Object} the compressed content.
           */
          getCompressedContent: function() {
              return null; // see implementation
          }
      };
      module.exports = CompressedObject;
      
      },{}],8:[function(_dereq_,module,exports){
      'use strict';
      exports.STORE = {
          magic: "\x00\x00",
          compress: function(content, compressionOptions) {
              return content; // no compression
          },
          uncompress: function(content) {
              return content; // no compression
          },
          compressInputType: null,
          uncompressInputType: null
      };
      exports.DEFLATE = _dereq_('./flate');
      
      },{"./flate":13}],9:[function(_dereq_,module,exports){
      'use strict';
      
      var utils = _dereq_('./utils');
      
      var table = [
          0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA,
          0x076DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3,
          0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988,
          0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91,
          0x1DB71064, 0x6AB020F2, 0xF3B97148, 0x84BE41DE,
          0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7,
          0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC,
          0x14015C4F, 0x63066CD9, 0xFA0F3D63, 0x8D080DF5,
          0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172,
          0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B,
          0x35B5A8FA, 0x42B2986C, 0xDBBBC9D6, 0xACBCF940,
          0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59,
          0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116,
          0x21B4F4B5, 0x56B3C423, 0xCFBA9599, 0xB8BDA50F,
          0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924,
          0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D,
          0x76DC4190, 0x01DB7106, 0x98D220BC, 0xEFD5102A,
          0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433,
          0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818,
          0x7F6A0DBB, 0x086D3D2D, 0x91646C97, 0xE6635C01,
          0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E,
          0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457,
          0x65B0D9C6, 0x12B7E950, 0x8BBEB8EA, 0xFCB9887C,
          0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65,
          0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2,
          0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB,
          0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0,
          0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9,
          0x5005713C, 0x270241AA, 0xBE0B1010, 0xC90C2086,
          0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F,
          0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4,
          0x59B33D17, 0x2EB40D81, 0xB7BD5C3B, 0xC0BA6CAD,
          0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A,
          0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683,
          0xE3630B12, 0x94643B84, 0x0D6D6A3E, 0x7A6A5AA8,
          0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1,
          0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE,
          0xF762575D, 0x806567CB, 0x196C3671, 0x6E6B06E7,
          0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC,
          0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5,
          0xD6D6A3E8, 0xA1D1937E, 0x38D8C2C4, 0x4FDFF252,
          0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B,
          0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60,
          0xDF60EFC3, 0xA867DF55, 0x316E8EEF, 0x4669BE79,
          0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236,
          0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F,
          0xC5BA3BBE, 0xB2BD0B28, 0x2BB45A92, 0x5CB36A04,
          0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D,
          0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A,
          0x9C0906A9, 0xEB0E363F, 0x72076785, 0x05005713,
          0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38,
          0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21,
          0x86D3D2D4, 0xF1D4E242, 0x68DDB3F8, 0x1FDA836E,
          0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777,
          0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C,
          0x8F659EFF, 0xF862AE69, 0x616BFFD3, 0x166CCF45,
          0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2,
          0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB,
          0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, 0x37D83BF0,
          0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9,
          0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6,
          0xBAD03605, 0xCDD70693, 0x54DE5729, 0x23D967BF,
          0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94,
          0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D
      ];
      
      /**
       *
       *  Javascript crc32
       *  http://www.webtoolkit.info/
       *
       */
      module.exports = function crc32(input, crc) {
          if (typeof input === "undefined" || !input.length) {
              return 0;
          }
      
          var isArray = utils.getTypeOf(input) !== "string";
      
          if (typeof(crc) == "undefined") {
              crc = 0;
          }
          var x = 0;
          var y = 0;
          var b = 0;
      
          crc = crc ^ (-1);
          for (var i = 0, iTop = input.length; i < iTop; i++) {
              b = isArray ? input[i] : input.charCodeAt(i);
              y = (crc ^ b) & 0xFF;
              x = table[y];
              crc = (crc >>> 8) ^ x;
          }
      
          return crc ^ (-1);
      };
      // vim: set shiftwidth=4 softtabstop=4:
      
      },{"./utils":26}],10:[function(_dereq_,module,exports){
      'use strict';
      var utils = _dereq_('./utils');
      
      function DataReader(data) {
          this.data = null; // type : see implementation
          this.length = 0;
          this.index = 0;
          this.zero = 0;
      }
      DataReader.prototype = {
          /**
           * Check that the offset will not go too far.
           * @param {string} offset the additional offset to check.
           * @throws {Error} an Error if the offset is out of bounds.
           */
          checkOffset: function(offset) {
              this.checkIndex(this.index + offset);
          },
          /**
           * Check that the specifed index will not be too far.
           * @param {string} newIndex the index to check.
           * @throws {Error} an Error if the index is out of bounds.
           */
          checkIndex: function(newIndex) {
              if (this.length < this.zero + newIndex || newIndex < 0) {
                  throw new Error("End of data reached (data length = " + this.length + ", asked index = " + (newIndex) + "). Corrupted zip ?");
              }
          },
          /**
           * Change the index.
           * @param {number} newIndex The new index.
           * @throws {Error} if the new index is out of the data.
           */
          setIndex: function(newIndex) {
              this.checkIndex(newIndex);
              this.index = newIndex;
          },
          /**
           * Skip the next n bytes.
           * @param {number} n the number of bytes to skip.
           * @throws {Error} if the new index is out of the data.
           */
          skip: function(n) {
              this.setIndex(this.index + n);
          },
          /**
           * Get the byte at the specified index.
           * @param {number} i the index to use.
           * @return {number} a byte.
           */
          byteAt: function(i) {
              // see implementations
          },
          /**
           * Get the next number with a given byte size.
           * @param {number} size the number of bytes to read.
           * @return {number} the corresponding number.
           */
          readInt: function(size) {
              var result = 0,
                  i;
              this.checkOffset(size);
              for (i = this.index + size - 1; i >= this.index; i--) {
                  result = (result << 8) + this.byteAt(i);
              }
              this.index += size;
              return result;
          },
          /**
           * Get the next string with a given byte size.
           * @param {number} size the number of bytes to read.
           * @return {string} the corresponding string.
           */
          readString: function(size) {
              return utils.transformTo("string", this.readData(size));
          },
          /**
           * Get raw data without conversion, <size> bytes.
           * @param {number} size the number of bytes to read.
           * @return {Object} the raw data, implementation specific.
           */
          readData: function(size) {
              // see implementations
          },
          /**
           * Find the last occurence of a zip signature (4 bytes).
           * @param {string} sig the signature to find.
           * @return {number} the index of the last occurence, -1 if not found.
           */
          lastIndexOfSignature: function(sig) {
              // see implementations
          },
          /**
           * Get the next date.
           * @return {Date} the date.
           */
          readDate: function() {
              var dostime = this.readInt(4);
              return new Date(
              ((dostime >> 25) & 0x7f) + 1980, // year
              ((dostime >> 21) & 0x0f) - 1, // month
              (dostime >> 16) & 0x1f, // day
              (dostime >> 11) & 0x1f, // hour
              (dostime >> 5) & 0x3f, // minute
              (dostime & 0x1f) << 1); // second
          }
      };
      module.exports = DataReader;
      
      },{"./utils":26}],11:[function(_dereq_,module,exports){
      'use strict';
      exports.base64 = false;
      exports.binary = false;
      exports.dir = false;
      exports.createFolders = false;
      exports.date = null;
      exports.compression = null;
      exports.compressionOptions = null;
      exports.comment = null;
      exports.unixPermissions = null;
      exports.dosPermissions = null;
      
      },{}],12:[function(_dereq_,module,exports){
      'use strict';
      var utils = _dereq_('./utils');
      
      /**
       * @deprecated
       * This function will be removed in a future version without replacement.
       */
      exports.string2binary = function(str) {
          return utils.string2binary(str);
      };
      
      /**
       * @deprecated
       * This function will be removed in a future version without replacement.
       */
      exports.string2Uint8Array = function(str) {
          return utils.transformTo("uint8array", str);
      };
      
      /**
       * @deprecated
       * This function will be removed in a future version without replacement.
       */
      exports.uint8Array2String = function(array) {
          return utils.transformTo("string", array);
      };
      
      /**
       * @deprecated
       * This function will be removed in a future version without replacement.
       */
      exports.string2Blob = function(str) {
          var buffer = utils.transformTo("arraybuffer", str);
          return utils.arrayBuffer2Blob(buffer);
      };
      
      /**
       * @deprecated
       * This function will be removed in a future version without replacement.
       */
      exports.arrayBuffer2Blob = function(buffer) {
          return utils.arrayBuffer2Blob(buffer);
      };
      
      /**
       * @deprecated
       * This function will be removed in a future version without replacement.
       */
      exports.transformTo = function(outputType, input) {
          return utils.transformTo(outputType, input);
      };
      
      /**
       * @deprecated
       * This function will be removed in a future version without replacement.
       */
      exports.getTypeOf = function(input) {
          return utils.getTypeOf(input);
      };
      
      /**
       * @deprecated
       * This function will be removed in a future version without replacement.
       */
      exports.checkSupport = function(type) {
          return utils.checkSupport(type);
      };
      
      /**
       * @deprecated
       * This value will be removed in a future version without replacement.
       */
      exports.MAX_VALUE_16BITS = utils.MAX_VALUE_16BITS;
      
      /**
       * @deprecated
       * This value will be removed in a future version without replacement.
       */
      exports.MAX_VALUE_32BITS = utils.MAX_VALUE_32BITS;
      
      
      /**
       * @deprecated
       * This function will be removed in a future version without replacement.
       */
      exports.pretty = function(str) {
          return utils.pretty(str);
      };
      
      /**
       * @deprecated
       * This function will be removed in a future version without replacement.
       */
      exports.findCompression = function(compressionMethod) {
          return utils.findCompression(compressionMethod);
      };
      
      /**
       * @deprecated
       * This function will be removed in a future version without replacement.
       */
      exports.isRegExp = function (object) {
          return utils.isRegExp(object);
      };
      
      
      },{"./utils":26}],13:[function(_dereq_,module,exports){
      'use strict';
      var USE_TYPEDARRAY = (typeof Uint8Array !== 'undefined') && (typeof Uint16Array !== 'undefined') && (typeof Uint32Array !== 'undefined');
      
      var pako = _dereq_("pako");
      exports.uncompressInputType = USE_TYPEDARRAY ? "uint8array" : "array";
      exports.compressInputType = USE_TYPEDARRAY ? "uint8array" : "array";
      
      exports.magic = "\x08\x00";
      exports.compress = function(input, compressionOptions) {
          return pako.deflateRaw(input, {
              level : compressionOptions.level || -1 // default compression
          });
      };
      exports.uncompress =  function(input) {
          return pako.inflateRaw(input);
      };
      
      },{"pako":29}],14:[function(_dereq_,module,exports){
      'use strict';
      
      var base64 = _dereq_('./base64');
      
      /**
      Usage:
         zip = new JSZip();
         zip.file("hello.txt", "Hello, World!").file("tempfile", "nothing");
         zip.folder("images").file("smile.gif", base64Data, {base64: true});
         zip.file("Xmas.txt", "Ho ho ho !", {date : new Date("December 25, 2007 00:00:01")});
         zip.remove("tempfile");
      
         base64zip = zip.generate();
      
      **/
      
      /**
       * Representation a of zip file in js
       * @constructor
       * @param {String=|ArrayBuffer=|Uint8Array=} data the data to load, if any (optional).
       * @param {Object=} options the options for creating this objects (optional).
       */
      function JSZip(data, options) {
          // if this constructor is used without `new`, it adds `new` before itself:
          if(!(this instanceof JSZip)) return new JSZip(data, options);
      
          // object containing the files :
          // {
          //   "folder/" : {...},
          //   "folder/data.txt" : {...}
          // }
          this.files = {};
      
          this.comment = null;
      
          // Where we are in the hierarchy
          this.root = "";
          if (data) {
              this.load(data, options);
          }
          this.clone = function() {
              var newObj = new JSZip();
              for (var i in this) {
                  if (typeof this[i] !== "function") {
                      newObj[i] = this[i];
                  }
              }
              return newObj;
          };
      }
      JSZip.prototype = _dereq_('./object');
      JSZip.prototype.load = _dereq_('./load');
      JSZip.support = _dereq_('./support');
      JSZip.defaults = _dereq_('./defaults');
      
      /**
       * @deprecated
       * This namespace will be removed in a future version without replacement.
       */
      JSZip.utils = _dereq_('./deprecatedPublicUtils');
      
      JSZip.base64 = {
          /**
           * @deprecated
           * This method will be removed in a future version without replacement.
           */
          encode : function(input) {
              return base64.encode(input);
          },
          /**
           * @deprecated
           * This method will be removed in a future version without replacement.
           */
          decode : function(input) {
              return base64.decode(input);
          }
      };
      JSZip.compressions = _dereq_('./compressions');
      module.exports = JSZip;
      
      },{"./base64":6,"./compressions":8,"./defaults":11,"./deprecatedPublicUtils":12,"./load":15,"./object":18,"./support":22}],15:[function(_dereq_,module,exports){
      'use strict';
      var base64 = _dereq_('./base64');
      var utf8 = _dereq_('./utf8');
      var utils = _dereq_('./utils');
      var ZipEntries = _dereq_('./zipEntries');
      module.exports = function(data, options) {
          var files, zipEntries, i, input;
          options = utils.extend(options || {}, {
              base64: false,
              checkCRC32: false,
              optimizedBinaryString : false,
              createFolders: false,
              decodeFileName: utf8.utf8decode
          });
          if (options.base64) {
              data = base64.decode(data);
          }
      
          zipEntries = new ZipEntries(data, options);
          files = zipEntries.files;
          for (i = 0; i < files.length; i++) {
              input = files[i];
              this.file(input.fileNameStr, input.decompressed, {
                  binary: true,
                  optimizedBinaryString: true,
                  date: input.date,
                  dir: input.dir,
                  comment : input.fileCommentStr.length ? input.fileCommentStr : null,
                  unixPermissions : input.unixPermissions,
                  dosPermissions : input.dosPermissions,
                  createFolders: options.createFolders
              });
          }
          if (zipEntries.zipComment.length) {
              this.comment = zipEntries.zipComment;
          }
      
          return this;
      };
      
      },{"./base64":6,"./utf8":25,"./utils":26,"./zipEntries":27}],16:[function(_dereq_,module,exports){
      (function (Buffer){
      'use strict';
      module.exports = function(data, encoding){
          return new Buffer(data, encoding);
      };
      module.exports.test = function(b){
          return Buffer.isBuffer(b);
      };
      
      }).call(this,_dereq_("buffer").Buffer)
      },{"buffer":1}],17:[function(_dereq_,module,exports){
      'use strict';
      var Uint8ArrayReader = _dereq_('./uint8ArrayReader');
      
      function NodeBufferReader(data) {
          this.data = data;
          this.length = this.data.length;
          this.index = 0;
          this.zero = 0;
      }
      NodeBufferReader.prototype = new Uint8ArrayReader();
      
      /**
       * @see DataReader.readData
       */
      NodeBufferReader.prototype.readData = function(size) {
          this.checkOffset(size);
          var result = this.data.slice(this.zero + this.index, this.zero + this.index + size);
          this.index += size;
          return result;
      };
      module.exports = NodeBufferReader;
      
      },{"./uint8ArrayReader":23}],18:[function(_dereq_,module,exports){
      'use strict';
      var support = _dereq_('./support');
      var utils = _dereq_('./utils');
      var crc32 = _dereq_('./crc32');
      var signature = _dereq_('./signature');
      var defaults = _dereq_('./defaults');
      var base64 = _dereq_('./base64');
      var compressions = _dereq_('./compressions');
      var CompressedObject = _dereq_('./compressedObject');
      var nodeBuffer = _dereq_('./nodeBuffer');
      var utf8 = _dereq_('./utf8');
      var StringWriter = _dereq_('./stringWriter');
      var Uint8ArrayWriter = _dereq_('./uint8ArrayWriter');
      
      /**
       * Returns the raw data of a ZipObject, decompress the content if necessary.
       * @param {ZipObject} file the file to use.
       * @return {String|ArrayBuffer|Uint8Array|Buffer} the data.
       */
      var getRawData = function(file) {
          if (file._data instanceof CompressedObject) {
              file._data = file._data.getContent();
              file.options.binary = true;
              file.options.base64 = false;
      
              if (utils.getTypeOf(file._data) === "uint8array") {
                  var copy = file._data;
                  // when reading an arraybuffer, the CompressedObject mechanism will keep it and subarray() a Uint8Array.
                  // if we request a file in the same format, we might get the same Uint8Array or its ArrayBuffer (the original zip file).
                  file._data = new Uint8Array(copy.length);
                  // with an empty Uint8Array, Opera fails with a "Offset larger than array size"
                  if (copy.length !== 0) {
                      file._data.set(copy, 0);
                  }
              }
          }
          return file._data;
      };
      
      /**
       * Returns the data of a ZipObject in a binary form. If the content is an unicode string, encode it.
       * @param {ZipObject} file the file to use.
       * @return {String|ArrayBuffer|Uint8Array|Buffer} the data.
       */
      var getBinaryData = function(file) {
          var result = getRawData(file),
              type = utils.getTypeOf(result);
          if (type === "string") {
              if (!file.options.binary) {
                  // unicode text !
                  // unicode string => binary string is a painful process, check if we can avoid it.
                  if (support.nodebuffer) {
                      return nodeBuffer(result, "utf-8");
                  }
              }
              return file.asBinary();
          }
          return result;
      };
      
      /**
       * Transform this._data into a string.
       * @param {function} filter a function String -> String, applied if not null on the result.
       * @return {String} the string representing this._data.
       */
      var dataToString = function(asUTF8) {
          var result = getRawData(this);
          if (result === null || typeof result === "undefined") {
              return "";
          }
          // if the data is a base64 string, we decode it before checking the encoding !
          if (this.options.base64) {
              result = base64.decode(result);
          }
          if (asUTF8 && this.options.binary) {
              // JSZip.prototype.utf8decode supports arrays as input
              // skip to array => string step, utf8decode will do it.
              result = out.utf8decode(result);
          }
          else {
              // no utf8 transformation, do the array => string step.
              result = utils.transformTo("string", result);
          }
      
          if (!asUTF8 && !this.options.binary) {
              result = utils.transformTo("string", out.utf8encode(result));
          }
          return result;
      };
      /**
       * A simple object representing a file in the zip file.
       * @constructor
       * @param {string} name the name of the file
       * @param {String|ArrayBuffer|Uint8Array|Buffer} data the data
       * @param {Object} options the options of the file
       */
      var ZipObject = function(name, data, options) {
          this.name = name;
          this.dir = options.dir;
          this.date = options.date;
          this.comment = options.comment;
          this.unixPermissions = options.unixPermissions;
          this.dosPermissions = options.dosPermissions;
      
          this._data = data;
          this.options = options;
      
          /*
           * This object contains initial values for dir and date.
           * With them, we can check if the user changed the deprecated metadata in
           * `ZipObject#options` or not.
           */
          this._initialMetadata = {
            dir : options.dir,
            date : options.date
          };
      };
      
      ZipObject.prototype = {
          /**
           * Return the content as UTF8 string.
           * @return {string} the UTF8 string.
           */
          asText: function() {
              return dataToString.call(this, true);
          },
          /**
           * Returns the binary content.
           * @return {string} the content as binary.
           */
          asBinary: function() {
              return dataToString.call(this, false);
          },
          /**
           * Returns the content as a nodejs Buffer.
           * @return {Buffer} the content as a Buffer.
           */
          asNodeBuffer: function() {
              var result = getBinaryData(this);
              return utils.transformTo("nodebuffer", result);
          },
          /**
           * Returns the content as an Uint8Array.
    • html2pdf.bundle.min.js 884.7 KB
      /*! For license information please see html2pdf.bundle.min.js.LICENSE.txt */
      !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define("html2pdf",[],t):"object"==typeof exports?exports.html2pdf=t():e.html2pdf=t()}(self,(function(){return function(){var e,t,r={"./node_modules/@babel/runtime-corejs3/core-js-stable/array/from.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/array/from.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/array/is-array.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/array/is-array.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/date/now.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/date/now.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/instance/bind.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/instance/bind.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/instance/concat.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/instance/concat.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/instance/every.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/instance/every.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/instance/fill.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/instance/fill.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/instance/filter.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/instance/filter.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/instance/for-each.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/instance/for-each.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/instance/includes.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/instance/includes.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/instance/index-of.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/instance/index-of.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/instance/map.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/instance/map.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/instance/reduce.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/instance/reduce.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/instance/reverse.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/instance/reverse.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/instance/slice.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/instance/slice.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/instance/some.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/instance/some.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/instance/starts-with.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/instance/starts-with.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/instance/trim.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/instance/trim.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/instance/values.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/instance/values.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/map.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/map/index.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/object/define-properties.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/object/define-properties.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/object/define-property.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/object/define-property.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/object/get-own-property-descriptor.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/object/get-own-property-descriptor.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/object/get-own-property-descriptors.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/object/get-own-property-descriptors.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/object/get-own-property-symbols.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/object/get-own-property-symbols.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/object/keys.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/object/keys.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/parse-float.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/parse-float.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/parse-int.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/parse-int.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/promise.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/promise/index.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/reflect/apply.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/reflect/apply.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/reflect/construct.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/reflect/construct.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/reflect/delete-property.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/reflect/delete-property.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/reflect/get-prototype-of.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/reflect/get-prototype-of.js")},"./node_modules/@babel/runtime-corejs3/core-js-stable/symbol.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/stable/symbol/index.js")},"./node_modules/@babel/runtime-corejs3/core-js/array/from.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/features/array/from.js")},"./node_modules/@babel/runtime-corejs3/core-js/array/is-array.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/features/array/is-array.js")},"./node_modules/@babel/runtime-corejs3/core-js/get-iterator-method.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/features/get-iterator-method.js")},"./node_modules/@babel/runtime-corejs3/core-js/get-iterator.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/features/get-iterator.js")},"./node_modules/@babel/runtime-corejs3/core-js/instance/slice.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/features/instance/slice.js")},"./node_modules/@babel/runtime-corejs3/core-js/object/create.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/features/object/create.js")},"./node_modules/@babel/runtime-corejs3/core-js/object/define-property.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/features/object/define-property.js")},"./node_modules/@babel/runtime-corejs3/core-js/object/get-own-property-descriptor.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/features/object/get-own-property-descriptor.js")},"./node_modules/@babel/runtime-corejs3/core-js/object/get-prototype-of.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/features/object/get-prototype-of.js")},"./node_modules/@babel/runtime-corejs3/core-js/object/set-prototype-of.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/features/object/set-prototype-of.js")},"./node_modules/@babel/runtime-corejs3/core-js/promise.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/features/promise/index.js")},"./node_modules/@babel/runtime-corejs3/core-js/reflect/get.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/features/reflect/get.js")},"./node_modules/@babel/runtime-corejs3/core-js/symbol.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/features/symbol/index.js")},"./node_modules/@babel/runtime-corejs3/core-js/symbol/iterator.js":function(e,t,r){e.exports=r("./node_modules/core-js-pure/features/symbol/iterator.js")},"./node_modules/@babel/runtime-corejs3/helpers/esm/arrayLikeToArray.js":function(e,t,r){"use strict";function n(e,t){(null==t||t>e.length)&&(t=e.length);for(var r=0,n=new Array(t);r<t;r++)n[r]=e[r];return n}r.r(t),r.d(t,{default:function(){return n}})},"./node_modules/@babel/runtime-corejs3/helpers/esm/arrayWithHoles.js":function(e,t,r){"use strict";r.r(t),r.d(t,{default:function(){return o}});var n=r("./node_modules/@babel/runtime-corejs3/core-js/array/is-array.js");function o(e){if(n(e))return e}},"./node_modules/@babel/runtime-corejs3/helpers/esm/arrayWithoutHoles.js":function(e,t,r){"use strict";r.r(t),r.d(t,{default:function(){return s}});var n=r("./node_modules/@babel/runtime-corejs3/core-js/array/is-array.js"),o=r("./node_modules/@babel/runtime-corejs3/helpers/esm/arrayLikeToArray.js");function s(e){if(n(e))return(0,o.default)(e)}},"./node_modules/@babel/runtime-corejs3/helpers/esm/assertThisInitialized.js":function(e,t,r){"use strict";function n(e){if(void 0===e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return e}r.r(t),r.d(t,{default:function(){return n}})},"./node_modules/@babel/runtime-corejs3/helpers/esm/asyncToGenerator.js":function(e,t,r){"use strict";r.r(t),r.d(t,{default:function(){return s}});var n=r("./node_modules/@babel/runtime-corejs3/core-js/promise.js");function o(e,t,r,o,s,i,a){try{var A=e[i](a),u=A.value}catch(e){return void r(e)}A.done?t(u):n.resolve(u).then(o,s)}function s(e){return function(){var t=this,r=arguments;return new n((function(n,s){var i=e.apply(t,r);function a(e){o(i,n,s,a,A,"next",e)}function A(e){o(i,n,s,a,A,"throw",e)}a(void 0)}))}}},"./node_modules/@babel/runtime-corejs3/helpers/esm/classCallCheck.js":function(e,t,r){"use strict";function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}r.r(t),r.d(t,{default:function(){return n}})},"./node_modules/@babel/runtime-corejs3/helpers/esm/createClass.js":function(e,t,r){"use strict";r.r(t),r.d(t,{default:function(){return s}});var n=r("./node_modules/@babel/runtime-corejs3/core-js/object/define-property.js");function o(e,t){for(var r=0;r<t.length;r++){var o=t[r];o.enumerable=o.enumerable||!1,o.configurable=!0,"value"in o&&(o.writable=!0),n(e,o.key,o)}}function s(e,t,r){return t&&o(e.prototype,t),r&&o(e,r),e}},"./node_modules/@babel/runtime-corejs3/helpers/esm/defineProperty.js":function(e,t,r){"use strict";r.r(t),r.d(t,{default:function(){return o}});var n=r("./node_modules/@babel/runtime-corejs3/core-js/object/define-property.js");function o(e,t,r){return t in e?n(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}},"./node_modules/@babel/runtime-corejs3/helpers/esm/get.js":function(e,t,r){"use strict";r.r(t),r.d(t,{default:function(){return i}});var n=r("./node_modules/@babel/runtime-corejs3/core-js/reflect/get.js"),o=r("./node_modules/@babel/runtime-corejs3/core-js/object/get-own-property-descriptor.js"),s=r("./node_modules/@babel/runtime-corejs3/helpers/esm/superPropBase.js");function i(e,t,r){return(i="undefined"!=typeof Reflect&&n?n:function(e,t,r){var n=(0,s.default)(e,t);if(n){var i=o(n,t);return i.get?i.get.call(r):i.value}})(e,t,r||e)}},"./node_modules/@babel/runtime-corejs3/helpers/esm/getPrototypeOf.js":function(e,t,r){"use strict";r.r(t),r.d(t,{default:function(){return s}});var n=r("./node_modules/@babel/runtime-corejs3/core-js/object/set-prototype-of.js"),o=r("./node_modules/@babel/runtime-corejs3/core-js/object/get-prototype-of.js");function s(e){return(s=n?o:function(e){return e.__proto__||o(e)})(e)}},"./node_modules/@babel/runtime-corejs3/helpers/esm/inherits.js":function(e,t,r){"use strict";r.r(t),r.d(t,{default:function(){return s}});var n=r("./node_modules/@babel/runtime-corejs3/core-js/object/create.js"),o=r("./node_modules/@babel/runtime-corejs3/helpers/esm/setPrototypeOf.js");function s(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function");e.prototype=n(t&&t.prototype,{constructor:{value:e,writable:!0,configurable:!0}}),t&&(0,o.default)(e,t)}},"./node_modules/@babel/runtime-corejs3/helpers/esm/iterableToArray.js":function(e,t,r){"use strict";r.r(t),r.d(t,{default:function(){return i}});var n=r("./node_modules/@babel/runtime-corejs3/core-js/symbol.js"),o=r("./node_modules/@babel/runtime-corejs3/core-js/get-iterator-method.js"),s=r("./node_modules/@babel/runtime-corejs3/core-js/array/from.js");function i(e){if(void 0!==n&&null!=o(e)||null!=e["@@iterator"])return s(e)}},"./node_modules/@babel/runtime-corejs3/helpers/esm/iterableToArrayLimit.js":function(e,t,r){"use strict";r.r(t),r.d(t,{default:function(){return s}});var n=r("./node_modules/@babel/runtime-corejs3/core-js/symbol.js"),o=r("./node_modules/@babel/runtime-corejs3/core-js/get-iterator-method.js");function s(e,t){var r=null==e?null:void 0!==n&&o(e)||e["@@iterator"];if(null!=r){var s,i,a=[],A=!0,u=!1;try{for(r=r.call(e);!(A=(s=r.next()).done)&&(a.push(s.value),!t||a.length!==t);A=!0);}catch(e){u=!0,i=e}finally{try{A||null==r.return||r.return()}finally{if(u)throw i}}return a}}},"./node_modules/@babel/runtime-corejs3/helpers/esm/nonIterableRest.js":function(e,t,r){"use strict";function n(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}r.r(t),r.d(t,{default:function(){return n}})},"./node_modules/@babel/runtime-corejs3/helpers/esm/nonIterableSpread.js":function(e,t,r){"use strict";function n(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}r.r(t),r.d(t,{default:function(){return n}})},"./node_modules/@babel/runtime-corejs3/helpers/esm/possibleConstructorReturn.js":function(e,t,r){"use strict";r.r(t),r.d(t,{default:function(){return s}});var n=r("./node_modules/@babel/runtime-corejs3/helpers/esm/typeof.js"),o=r("./node_modules/@babel/runtime-corejs3/helpers/esm/assertThisInitialized.js");function s(e,t){return!t||"object"!==(0,n.default)(t)&&"function"!=typeof t?(0,o.default)(e):t}},"./node_modules/@babel/runtime-corejs3/helpers/esm/setPrototypeOf.js":function(e,t,r){"use strict";r.r(t),r.d(t,{default:function(){return o}});var n=r("./node_modules/@babel/runtime-corejs3/core-js/object/set-prototype-of.js");function o(e,t){return(o=n||function(e,t){return e.__proto__=t,e})(e,t)}},"./node_modules/@babel/runtime-corejs3/helpers/esm/slicedToArray.js":function(e,t,r){"use strict";r.r(t),r.d(t,{default:function(){return a}});var n=r("./node_modules/@babel/runtime-corejs3/helpers/esm/arrayWithHoles.js"),o=r("./node_modules/@babel/runtime-corejs3/helpers/esm/iterableToArrayLimit.js"),s=r("./node_modules/@babel/runtime-corejs3/helpers/esm/unsupportedIterableToArray.js"),i=r("./node_modules/@babel/runtime-corejs3/helpers/esm/nonIterableRest.js");function a(e,t){return(0,n.default)(e)||(0,o.default)(e,t)||(0,s.default)(e,t)||(0,i.default)()}},"./node_modules/@babel/runtime-corejs3/helpers/esm/superPropBase.js":function(e,t,r){"use strict";r.r(t),r.d(t,{default:function(){return o}});var n=r("./node_modules/@babel/runtime-corejs3/helpers/esm/getPrototypeOf.js");function o(e,t){for(;!Object.prototype.hasOwnProperty.call(e,t)&&null!==(e=(0,n.default)(e)););return e}},"./node_modules/@babel/runtime-corejs3/helpers/esm/toConsumableArray.js":function(e,t,r){"use strict";r.r(t),r.d(t,{default:function(){return a}});var n=r("./node_modules/@babel/runtime-corejs3/helpers/esm/arrayWithoutHoles.js"),o=r("./node_modules/@babel/runtime-corejs3/helpers/esm/iterableToArray.js"),s=r("./node_modules/@babel/runtime-corejs3/helpers/esm/unsupportedIterableToArray.js"),i=r("./node_modules/@babel/runtime-corejs3/helpers/esm/nonIterableSpread.js");function a(e){return(0,n.default)(e)||(0,o.default)(e)||(0,s.default)(e)||(0,i.default)()}},"./node_modules/@babel/runtime-corejs3/helpers/esm/typeof.js":function(e,t,r){"use strict";r.r(t),r.d(t,{default:function(){return s}});var n=r("./node_modules/@babel/runtime-corejs3/core-js/symbol.js"),o=r("./node_modules/@babel/runtime-corejs3/core-js/symbol/iterator.js");function s(e){return(s="function"==typeof n&&"symbol"==typeof o?function(e){return typeof e}:function(e){return e&&"function"==typeof n&&e.constructor===n&&e!==n.prototype?"symbol":typeof e})(e)}},"./node_modules/@babel/runtime-corejs3/helpers/esm/unsupportedIterableToArray.js":function(e,t,r){"use strict";r.r(t),r.d(t,{default:function(){return i}});var n=r("./node_modules/@babel/runtime-corejs3/core-js/instance/slice.js"),o=r("./node_modules/@babel/runtime-corejs3/core-js/array/from.js"),s=r("./node_modules/@babel/runtime-corejs3/helpers/esm/arrayLikeToArray.js");function i(e,t){var r;if(e){if("string"==typeof e)return(0,s.default)(e,t);var i=n(r=Object.prototype.toString.call(e)).call(r,8,-1);return"Object"===i&&e.constructor&&(i=e.constructor.name),"Map"===i||"Set"===i?o(e):"Arguments"===i||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(i)?(0,s.default)(e,t):void 0}}},"./node_modules/@babel/runtime-corejs3/regenerator/index.js":function(e,t,r){e.exports=r("./node_modules/regenerator-runtime/runtime.js")},"./src/plugin/hyperlinks.js":function(e,t,r){"use strict";r.r(t),r("./node_modules/core-js/modules/web.dom-collections.for-each.js"),r("./node_modules/core-js/modules/es.string.link.js");var n=r("./src/worker.js"),o=r("./src/utils.js"),s=[],i={toContainer:n.default.prototype.toContainer,toPdf:n.default.prototype.toPdf};n.default.prototype.toContainer=function(){return i.toContainer.call(this).then((function(){if(this.opt.enableLinks){var e=this.prop.container,t=e.querySelectorAll("a"),r=(0,o.unitConvert)(e.getBoundingClientRect(),this.prop.pageSize.k);s=[],Array.prototype.forEach.call(t,(function(e){for(var t=e.getClientRects(),n=0;n<t.length;n++){var i=(0,o.unitConvert)(t[n],this.prop.pageSize.k);i.left-=r.left,i.top-=r.top;var a=Math.floor(i.top/this.prop.pageSize.inner.height)+1,A=this.opt.margin[0]+i.top%this.prop.pageSize.inner.height,u=this.opt.margin[1]+i.left;s.push({page:a,top:A,left:u,clientRect:i,link:e})}}),this)}}))},n.default.prototype.toPdf=function(){return i.toPdf.call(this).then((function(){if(this.opt.enableLinks){s.forEach((function(e){this.prop.pdf.setPage(e.page),this.prop.pdf.link(e.left,e.top,e.clientRect.width,e.clientRect.height,{url:e.link.href})}),this);var e=this.prop.pdf.internal.getNumberOfPages();this.prop.pdf.setPage(e)}}))}},"./src/plugin/jspdf-plugin.js":function(e,t,r){"use strict";r.r(t),r("./node_modules/core-js/modules/es.symbol.js"),r("./node_modules/core-js/modules/es.symbol.description.js"),r("./node_modules/core-js/modules/es.object.to-string.js"),r("./node_modules/core-js/modules/es.symbol.iterator.js"),r("./node_modules/core-js/modules/es.array.iterator.js"),r("./node_modules/core-js/modules/es.string.iterator.js"),r("./node_modules/core-js/modules/web.dom-collections.iterator.js");var n=r("./node_modules/jspdf/dist/jspdf.es.min.js");function o(e){return(o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}n.jsPDF.getPageSize=function(e,t,r){if("object"===o(e)){var n=e;e=n.orientation,t=n.unit||t,r=n.format||r}t=t||"mm",r=r||"a4",e=(""+(e||"P")).toLowerCase();var s=(""+r).toLowerCase(),i={a0:[2383.94,3370.39],a1:[1683.78,2383.94],a2:[1190.55,1683.78],a3:[841.89,1190.55],a4:[595.28,841.89],a5:[419.53,595.28],a6:[297.64,419.53],a7:[209.76,297.64],a8:[147.4,209.76],a9:[104.88,147.4],a10:[73.7,104.88],b0:[2834.65,4008.19],b1:[2004.09,2834.65],b2:[1417.32,2004.09],b3:[1000.63,1417.32],b4:[708.66,1000.63],b5:[498.9,708.66],b6:[354.33,498.9],b7:[249.45,354.33],b8:[175.75,249.45],b9:[124.72,175.75],b10:[87.87,124.72],c0:[2599.37,3676.54],c1:[1836.85,2599.37],c2:[1298.27,1836.85],c3:[918.43,1298.27],c4:[649.13,918.43],c5:[459.21,649.13],c6:[323.15,459.21],c7:[229.61,323.15],c8:[161.57,229.61],c9:[113.39,161.57],c10:[79.37,113.39],dl:[311.81,623.62],letter:[612,792],"government-letter":[576,756],legal:[612,1008],"junior-legal":[576,360],ledger:[1224,792],tabloid:[792,1224],"credit-card":[153,243]};switch(t){case"pt":var a=1;break;case"mm":a=72/25.4;break;case"cm":a=72/2.54;break;case"in":a=72;break;case"px":a=.75;break;case"pc":case"em":a=12;break;case"ex":a=6;break;default:throw"Invalid unit: "+t}if(i.hasOwnProperty(s))var A=i[s][1]/a,u=i[s][0]/a;else try{A=r[1],u=r[0]}catch(e){throw new Error("Invalid format: "+r)}if("p"===e||"portrait"===e){if(e="p",u>A){var c=u;u=A,A=c}}else{if("l"!==e&&"landscape"!==e)throw"Invalid orientation: "+e;e="l",A>u&&(c=u,u=A,A=c)}return{width:u,height:A,unit:t,k:a}},t.default=n.jsPDF},"./src/plugin/pagebreaks.js":function(e,t,r){"use strict";r.r(t),r("./node_modules/core-js/modules/es.array.concat.js"),r("./node_modules/core-js/modules/es.array.slice.js"),r("./node_modules/core-js/modules/es.array.join.js"),r("./node_modules/core-js/modules/web.dom-collections.for-each.js"),r("./node_modules/core-js/modules/es.object.keys.js");var n=r("./src/worker.js"),o=r("./src/utils.js"),s={toContainer:n.default.prototype.toContainer};n.default.template.opt.pagebreak={mode:["css","legacy"],before:[],after:[],avoid:[]},n.default.prototype.toContainer=function(){return s.toContainer.call(this).then((function(){var e=this.prop.container,t=this.prop.pageSize.inner.px.height,r=[].concat(this.opt.pagebreak.mode),n={avoidAll:-1!==r.indexOf("avoid-all"),css:-1!==r.indexOf("css"),legacy:-1!==r.indexOf("legacy")},s={},i=this;["before","after","avoid"].forEach((function(t){var r=n.avoidAll&&"avoid"===t;s[t]=r?[]:[].concat(i.opt.pagebreak[t]||[]),s[t].length>0&&(s[t]=Array.prototype.slice.call(e.querySelectorAll(s[t].join(", "))))}));var a=e.querySelectorAll(".html2pdf__page-break");a=Array.prototype.slice.call(a);var A=e.querySelectorAll("*");Array.prototype.forEach.call(A,(function(e){var r={before:!1,after:n.legacy&&-1!==a.indexOf(e),avoid:n.avoidAll};if(n.css){var i=window.getComputedStyle(e),A=["always","page","left","right"];r={before:r.before||-1!==A.indexOf(i.breakBefore||i.pageBreakBefore),after:r.after||-1!==A.indexOf(i.breakAfter||i.pageBreakAfter),avoid:r.avoid||-1!==["avoid","avoid-page"].indexOf(i.breakInside||i.pageBreakInside)}}Object.keys(r).forEach((function(t){r[t]=r[t]||-1!==s[t].indexOf(e)}));var u=e.getBoundingClientRect();if(r.avoid&&!r.before){var c=Math.floor(u.top/t),l=Math.floor(u.bottom/t),d=Math.abs(u.bottom-u.top)/t;l!==c&&d<=1&&(r.before=!0)}if(r.before){var f=(0,o.createElement)("div",{style:{display:"block",height:t-u.top%t+"px"}});e.parentNode.insertBefore(f,e)}r.after&&(f=(0,o.createElement)("div",{style:{display:"block",height:t-u.bottom%t+"px"}}),e.parentNode.insertBefore(f,e.nextSibling))}))}))}},"./src/utils.js":function(e,t,r){"use strict";function n(e){return(n="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}r.r(t),r.d(t,{objType:function(){return o},createElement:function(){return s},cloneNode:function(){return i},unitConvert:function(){return a},toPx:function(){return A}}),r("./node_modules/core-js/modules/es.number.constructor.js"),r("./node_modules/core-js/modules/es.symbol.js"),r("./node_modules/core-js/modules/es.symbol.description.js"),r("./node_modules/core-js/modules/es.object.to-string.js"),r("./node_modules/core-js/modules/es.symbol.iterator.js"),r("./node_modules/core-js/modules/es.array.iterator.js"),r("./node_modules/core-js/modules/es.string.iterator.js"),r("./node_modules/core-js/modules/web.dom-collections.iterator.js");var o=function(e){var t=n(e);return"undefined"===t?"undefined":"string"===t||e instanceof String?"string":"number"===t||e instanceof Number?"number":"function"===t||e instanceof Function?"function":e&&e.constructor===Array?"array":e&&1===e.nodeType?"element":"object"===t?"object":"unknown"},s=function(e,t){var r=document.createElement(e);if(t.className&&(r.className=t.className),t.innerHTML){r.innerHTML=t.innerHTML;for(var n=r.getElementsByTagName("script"),o=n.length;o-- >0;null)n[o].parentNode.removeChild(n[o])}for(var s in t.style)r.style[s]=t.style[s];return r},i=function e(t,r){for(var n=3===t.nodeType?document.createTextNode(t.nodeValue):t.cloneNode(!1),o=t.firstChild;o;o=o.nextSibling)!0!==r&&1===o.nodeType&&"SCRIPT"===o.nodeName||n.appendChild(e(o,r));return 1===t.nodeType&&("CANVAS"===t.nodeName?(n.width=t.width,n.height=t.height,n.getContext("2d").drawImage(t,0,0)):"TEXTAREA"!==t.nodeName&&"SELECT"!==t.nodeName||(n.value=t.value),n.addEventListener("load",(function(){n.scrollTop=t.scrollTop,n.scrollLeft=t.scrollLeft}),!0)),n},a=function(e,t){if("number"===o(e))return 72*e/96/t;var r={};for(var n in e)r[n]=72*e[n]/96/t;return r},A=function(e,t){return Math.floor(e*t/72*96)}},"./src/worker.js":function(e,t,r){"use strict";r.r(t),r("./node_modules/core-js/modules/es.object.assign.js"),r("./node_modules/core-js/modules/es.array.map.js"),r("./node_modules/core-js/modules/es.object.keys.js"),r("./node_modules/core-js/modules/es.array.concat.js"),r("./node_modules/core-js/modules/es.object.to-string.js"),r("./node_modules/core-js/modules/es.regexp.to-string.js"),r("./node_modules/core-js/modules/es.function.name.js"),r("./node_modules/core-js/modules/web.dom-collections.for-each.js");var n=r("./node_modules/jspdf/dist/jspdf.es.min.js"),o=r("./node_modules/html2canvas/dist/html2canvas.js"),s=r("./src/utils.js"),i=r("./node_modules/es6-promise/dist/es6-promise.js"),a=r.n(i)().Promise,A=function e(t){var r=Object.assign(e.convert(a.resolve()),JSON.parse(JSON.stringify(e.template))),n=e.convert(a.resolve(),r);return(n=n.setProgress(1,e,1,[e])).set(t)};(A.prototype=Object.create(a.prototype)).constructor=A,A.convert=function(e,t){return e.__proto__=t||A.prototype,e},A.template={prop:{src:null,container:null,overlay:null,canvas:null,img:null,pdf:null,pageSize:null},progress:{val:0,state:null,n:0,stack:[]},opt:{filename:"file.pdf",margin:[0,0,0,0],image:{type:"jpeg",quality:.95},enableLinks:!0,html2canvas:{},jsPDF:{}}},A.prototype.from=function(e,t){return this.then((function(){switch(t=t||function(e){switch((0,s.objType)(e)){case"string":return"string";case"element":return"canvas"===e.nodeName.toLowerCase?"canvas":"element";default:return"unknown"}}(e)){case"string":return this.set({src:(0,s.createElement)("div",{innerHTML:e})});case"element":return this.set({src:e});case"canvas":return this.set({canvas:e});case"img":return this.set({img:e});default:return this.error("Unknown source type.")}}))},A.prototype.to=function(e){switch(e){case"container":return this.toContainer();case"canvas":return this.toCanvas();case"img":return this.toImg();case"pdf":return this.toPdf();default:return this.error("Invalid target.")}},A.prototype.toContainer=function(){return this.thenList([function(){return this.prop.src||this.error("Cannot duplicate - no source HTML.")},function(){return this.prop.pageSize||this.setPageSize()}]).then((function(){var e={position:"fixed",overflow:"hidden",zIndex:1e3,left:0,right:0,bottom:0,top:0,backgroundColor:"rgba(0,0,0,0.8)"},t={position:"absolute",width:this.prop.pageSize.inner.width+this.prop.pageSize.unit,left:0,right:0,top:0,height:"auto",margin:"auto",backgroundColor:"white"};e.opacity=0;var r=(0,s.cloneNode)(this.prop.src,this.opt.html2canvas.javascriptEnabled);this.prop.overlay=(0,s.createElement)("div",{className:"html2pdf__overlay",style:e}),this.prop.container=(0,s.createElement)("div",{className:"html2pdf__container",style:t}),this.prop.container.appendChild(r),this.prop.overlay.appendChild(this.prop.container),document.body.appendChild(this.prop.overlay)}))},A.prototype.toCanvas=function(){var e=[function(){return document.body.contains(this.prop.container)||this.toContainer()}];return this.thenList(e).then((function(){var e=Object.assign({},this.opt.html2canvas);return delete e.onrendered,o(this.prop.container,e)})).then((function(e){(this.opt.html2canvas.onrendered||function(){})(e),this.prop.canvas=e,document.body.removeChild(this.prop.overlay)}))},A.prototype.toImg=function(){return this.thenList([function(){return this.prop.canvas||this.toCanvas()}]).then((function(){var e=this.prop.canvas.toDataURL("image/"+this.opt.image.type,this.opt.image.quality);this.prop.img=document.createElement("img"),this.prop.img.src=e}))},A.prototype.toPdf=function(){return this.thenList([function(){return this.prop.canvas||this.toCanvas()}]).then((function(){var e=this.prop.canvas,t=this.opt,r=e.height,o=Math.floor(e.width*this.prop.pageSize.inner.ratio),s=Math.ceil(r/o),i=this.prop.pageSize.inner.height,a=document.createElement("canvas"),A=a.getContext("2d");a.width=e.width,a.height=o,this.prop.pdf=this.prop.pdf||new n.jsPDF(t.jsPDF);for(var u=0;u<s;u++){u===s-1&&r%o!=0&&(a.height=r%o,i=a.height*this.prop.pageSize.inner.width/a.width);var c=a.width,l=a.height;A.fillStyle="white",A.fillRect(0,0,c,l),A.drawImage(e,0,u*o,c,l,0,0,c,l),u&&this.prop.pdf.addPage();var d=a.toDataURL("image/"+t.image.type,t.image.quality);this.prop.pdf.addImage(d,t.image.type,t.margin[1],t.margin[0],this.prop.pageSize.inner.width,i)}}))},A.prototype.output=function(e,t,r){return"img"===(r=r||"pdf").toLowerCase()||"image"===r.toLowerCase()?this.outputImg(e,t):this.outputPdf(e,t)},A.prototype.outputPdf=function(e,t){return this.thenList([function(){return this.prop.pdf||this.toPdf()}]).then((function(){return this.prop.pdf.output(e,t)}))},A.prototype.outputImg=function(e,t){return this.thenList([function(){return this.prop.img||this.toImg()}]).then((function(){switch(e){case void 0:case"img":return this.prop.img;case"datauristring":case"dataurlstring":return this.prop.img.src;case"datauri":case"dataurl":return document.location.href=this.prop.img.src;default:throw'Image output type "'+e+'" is not supported.'}}))},A.prototype.save=function(e){return this.thenList([function(){return this.prop.pdf||this.toPdf()}]).set(e?{filename:e}:null).then((function(){this.prop.pdf.save(this.opt.filename)}))},A.prototype.set=function(e){if("object"!==(0,s.objType)(e))return this;var t=Object.keys(e||{}).map((function(t){switch(t){case"margin":return this.setMargin.bind(this,e.margin);case"jsPDF":return function(){return this.opt.jsPDF=e.jsPDF,this.setPageSize()};case"pageSize":return this.setPageSize.bind(this,e.pageSize);default:return t in A.template.prop?function(){this.prop[t]=e[t]}:function(){this.opt[t]=e[t]}}}),this);return this.then((function(){return this.thenList(t)}))},A.prototype.get=function(e,t){return this.then((function(){var r=e in A.template.prop?this.prop[e]:this.opt[e];return t?t(r):r}))},A.prototype.setMargin=function(e){return this.then((function(){switch((0,s.objType)(e)){case"number":e=[e,e,e,e];case"array":if(2===e.length&&(e=[e[0],e[1],e[0],e[1]]),4===e.length)break;default:return this.error("Invalid margin array.")}this.opt.margin=e})).then(this.setPageSize)},A.prototype.setPageSize=function(e){return this.then((function(){(e=e||n.jsPDF.getPageSize(this.opt.jsPDF)).hasOwnProperty("inner")||(e.inner={width:e.width-this.opt.margin[1]-this.opt.margin[3],height:e.height-this.opt.margin[0]-this.opt.margin[2]},e.inner.px={width:(0,s.toPx)(e.inner.width,e.k),height:(0,s.toPx)(e.inner.height,e.k)},e.inner.ratio=e.inner.height/e.inner.width),this.prop.pageSize=e}))},A.prototype.setProgress=function(e,t,r,n){return null!=e&&(this.progress.val=e),null!=t&&(this.progress.state=t),null!=r&&(this.progress.n=r),null!=n&&(this.progress.stack=n),this.progress.ratio=this.progress.val/this.progress.state,this},A.prototype.updateProgress=function(e,t,r,n){return this.setProgress(e?this.progress.val+e:null,t||null,r?this.progress.n+r:null,n?this.progress.stack.concat(n):null)},A.prototype.then=function(e,t){var r=this;return this.thenCore(e,t,(function(e,t){return r.updateProgress(null,null,1,[e]),a.prototype.then.call(this,(function(t){return r.updateProgress(null,e),t})).then(e,t).then((function(e){return r.updateProgress(1),e}))}))},A.prototype.thenCore=function(e,t,r){r=r||a.prototype.then;var n=this;e&&(e=e.bind(n)),t&&(t=t.bind(n));var o=-1!==a.toString().indexOf("[native code]")&&"Promise"===a.name?n:A.convert(Object.assign({},n),a.prototype),s=r.call(o,e,t);return A.convert(s,n.__proto__)},A.prototype.thenExternal=function(e,t){return a.prototype.then.call(this,e,t)},A.prototype.thenList=function(e){var t=this;return e.forEach((function(e){t=t.thenCore(e)})),t},A.prototype.catch=function(e){e&&(e=e.bind(this));var t=a.prototype.catch.call(this,e);return A.convert(t,this)},A.prototype.catchExternal=function(e){return a.prototype.catch.call(this,e)},A.prototype.error=function(e){return this.then((function(){throw new Error(e)}))},A.prototype.using=A.prototype.set,A.prototype.saveAs=A.prototype.save,A.prototype.export=A.prototype.output,A.prototype.run=A.prototype.then,t.default=A},"./node_modules/canvg/lib/index.es.js":function(e,t,r){"use strict";r.r(t),r.d(t,{AElement:function(){return pr},AnimateColorElement:function(){return ar},AnimateElement:function(){return ir},AnimateTransformElement:function(){return Ar},BoundingBox:function(){return Ot},CB1:function(){return at},CB2:function(){return At},CB3:function(){return ut},CB4:function(){return ct},Canvg:function(){return qr},CircleElement:function(){return Gt},ClipPathElement:function(){return Sr},DefsElement:function(){return er},DescElement:function(){return kr},Document:function(){return Kr},Element:function(){return St},EllipseElement:function(){return Xt},FeColorMatrixElement:function(){return Qr},FeCompositeElement:function(){return Ir},FeDropShadowElement:function(){return Tr},FeGaussianBlurElement:function(){return Pr},FeMorphologyElement:function(){return Hr},FilterElement:function(){return Lr},Font:function(){return Pt},FontElement:function(){return cr},FontFaceElement:function(){return lr},GElement:function(){return tr},GlyphElement:function(){return fr},GradientElement:function(){return rr},ImageElement:function(){return wr},LineElement:function(){return Wt},LinearGradientElement:function(){return nr},MarkerElement:function(){return $t},MaskElement:function(){return Fr},Matrix:function(){return Et},MissingGlyphElement:function(){return dr},Mouse:function(){return gt},PSEUDO_ZERO:function(){return nt},Parser:function(){return jt},PathElement:function(){return zt},PathParser:function(){return Kt},PatternElement:function(){return Zt},Point:function(){return mt},PolygonElement:function(){return Yt},PolylineElement:function(){return Jt},Property:function(){return ht},QB1:function(){return lt},QB2:function(){return dt},QB3:function(){return ft},RadialGradientElement:function(){return or},RectElement:function(){return qt},RenderedElement:function(){return kt},Rotate:function(){return Ct},SVGElement:function(){return Vt},SVGFontLoader:function(){return Br},Scale:function(){return xt},Screen:function(){return wt},Skew:function(){return Nt},SkewX:function(){return Qt},SkewY:function(){return Ft},StopElement:function(){return sr},StyleElement:function(){return jr},SymbolElement:function(){return br},TRefElement:function(){return hr},TSpanElement:function(){return Mt},TextElement:function(){return Rt},TextPathElement:function(){return vr},TitleElement:function(){return Or},Transform:function(){return Ut},Translate:function(){return _t},UnknownElement:function(){return Lt},UseElement:function(){return _r},ViewPort:function(){return pt},compressSpaces:function(){return Re},getSelectorSpecificity:function(){return rt},normalizeAttributeName:function(){return Ve},normalizeColor:function(){return Ge},parseExternalUrl:function(){return qe},presets:function(){return Gr},toNumbers:function(){return Ke},trimLeft:function(){return Me},trimRight:function(){return De},vectorMagnitude:function(){return ot},vectorsAngle:function(){return it},vectorsRatio:function(){return st}});var n=r("./node_modules/@babel/runtime-corejs3/core-js-stable/instance/starts-with.js"),o=r.n(n),s=r("./node_modules/@babel/runtime-corejs3/core-js-stable/parse-float.js"),i=r.n(s),a=r("./node_modules/@babel/runtime-corejs3/core-js-stable/instance/map.js"),A=r.n(a),u=r("./node_modules/@babel/runtime-corejs3/helpers/esm/slicedToArray.js"),c=r("./node_modules/@babel/runtime-corejs3/core-js-stable/object/define-property.js"),l=r.n(c),d=r("./node_modules/@babel/runtime-corejs3/core-js-stable/object/define-properties.js"),f=r.n(d),h=r("./node_modules/@babel/runtime-corejs3/core-js-stable/object/get-own-property-descriptors.js"),p=r.n(h),m=r("./node_modules/@babel/runtime-corejs3/core-js-stable/instance/for-each.js"),g=r.n(m),y=r("./node_modules/@babel/runtime-corejs3/core-js-stable/object/get-own-property-descriptor.js"),v=r.n(y),w=r("./node_modules/@babel/runtime-corejs3/core-js-stable/instance/filter.js"),b=r.n(w),B=r("./node_modules/@babel/runtime-corejs3/core-js-stable/object/get-own-property-symbols.js"),j=r.n(B),_=r("./node_modules/@babel/runtime-corejs3/core-js-stable/object/keys.js"),C=r.n(_),x=r("./node_modules/@babel/runtime-corejs3/regenerator/index.js"),E=r.n(x),N=r("./node_modules/@babel/runtime-corejs3/helpers/esm/asyncToGenerator.js"),Q=r("./node_modules/@babel/runtime-corejs3/helpers/esm/defineProperty.js"),F=r("./node_modules/@babel/runtime-corejs3/helpers/esm/classCallCheck.js"),U=r("./node_modules/@babel/runtime-corejs3/helpers/esm/createClass.js"),S=r("./node_modules/@babel/runtime-corejs3/core-js-stable/instance/concat.js"),L=r.n(S),T=r("./node_modules/@babel/runtime-corejs3/core-js-stable/instance/reduce.js"),H=r.n(T),I=r("./node_modules/@babel/runtime-corejs3/core-js-stable/date/now.js"),P=r.n(I),O=r("./node_modules/@babel/runtime-corejs3/core-js-stable/instance/every.js"),k=r.n(O),R=r("./node_modules/@babel/runtime-corejs3/core-js-stable/promise.js"),M=r.n(R),D=r("./node_modules/@babel/runtime-corejs3/core-js-stable/instance/bind.js"),K=r.n(D),z=r("./node_modules/raf/index.js"),V=r.n(z),q=r("./node_modules/@babel/runtime-corejs3/core-js-stable/instance/trim.js"),G=r.n(q),X=r("./node_modules/rgbcolor/index.js"),W=r.n(X),J=r("./node_modules/@babel/runtime-corejs3/core-js-stable/reflect/construct.js"),Y=r.n(J),Z=r("./node_modules/@babel/runtime-corejs3/helpers/esm/inherits.js"),$=r("./node_modules/@babel/runtime-corejs3/helpers/esm/possibleConstructorReturn.js"),ee=r("./node_modules/@babel/runtime-corejs3/helpers/esm/getPrototypeOf.js"),te=r("./node_modules/@babel/runtime-corejs3/helpers/esm/toConsumableArray.js"),re=r("./node_modules/@babel/runtime-corejs3/core-js-stable/instance/some.js"),ne=r.n(re),oe=r("./node_modules/@babel/runtime-corejs3/core-js-stable/instance/includes.js"),se=r.n(oe),ie=r("./node_modules/@babel/runtime-corejs3/core-js-stable/array/from.js"),ae=r.n(ie),Ae=r("./node_modules/@babel/runtime-corejs3/core-js-stable/instance/reverse.js"),ue=r.n(Ae),ce=r("./node_modules/@babel/runtime-corejs3/core-js-stable/instance/index-of.js"),le=r.n(ce),de=r("./node_modules/@babel/runtime-corejs3/helpers/esm/get.js"),fe=r("./node_modules/@babel/runtime-corejs3/core-js-stable/instance/fill.js"),he=r.n(fe),pe=r("./node_modules/svg-pathdata/lib/SVGPathData.module.js"),me=r("./node_modules/@babel/runtime-corejs3/core-js-stable/reflect/delete-property.js"),ge=r.n(me),ye=r("./node_modules/@babel/runtime-corejs3/helpers/esm/assertThisInitialized.js"),ve=r("./node_modules/@babel/runtime-corejs3/core-js-stable/instance/values.js"),we=r.n(ve),be=r("./node_modules/@babel/runtime-corejs3/core-js-stable/parse-int.js"),Be=r.n(be),je=r("./node_modules/@babel/runtime-corejs3/core-js/get-iterator.js"),_e=r.n(je),Ce=r("./node_modules/@babel/runtime-corejs3/core-js-stable/array/is-array.js"),xe=r.n(Ce),Ee=r("./node_modules/@babel/runtime-corejs3/core-js/get-iterator-method.js"),Ne=r.n(Ee),Qe=r("./node_modules/@babel/runtime-corejs3/core-js-stable/symbol.js"),Fe=r.n(Qe),Ue=r("./node_modules/@babel/runtime-corejs3/core-js-stable/instance/slice.js"),Se=r.n(Ue),Le=r("./node_modules/@babel/runtime-corejs3/core-js-stable/map.js"),Te=r.n(Le),He=r("./node_modules/@babel/runtime-corejs3/core-js-stable/reflect/apply.js"),Ie=r.n(He),Pe=r("./node_modules/@babel/runtime-corejs3/core-js-stable/reflect/get-prototype-of.js"),Oe=r.n(Pe),ke=r("./node_modules/stackblur-canvas/dist/stackblur-es.js");function Re(e){return e.replace(/(?!\u3000)\s+/gm," ")}function Me(e){return e.replace(/^[\n \t]+/,"")}function De(e){return e.replace(/[\n \t]+$/,"")}function Ke(e){var t=(e||"").match(/-?(\d+(?:\.\d*(?:[eE][+-]?\d+)?)?|\.\d+)(?=\D|$)/gm)||[];return A()(t).call(t,i())}var ze=/^[A-Z-]+$/;function Ve(e){return ze.test(e)?e.toLowerCase():e}function qe(e){var t=e.match(/url\(('([^']+)'|"([^"]+)"|([^'"\)]+))\)/)||[];return t[2]||t[3]||t[4]}function Ge(e){if(!o()(e).call(e,"rgb"))return e;var t=3;return e.replace(/\d+(\.\d+)?/g,(function(e,r){return t--&&r?String(Math.round(i()(e))):e}))}var Xe=/(\[[^\]]+\])/g,We=/(#[^\s\+>~\.\[:]+)/g,Je=/(\.[^\s\+>~\.\[:]+)/g,Ye=/(::[^\s\+>~\.\[:]+|:first-line|:first-letter|:before|:after)/gi,Ze=/(:[\w-]+\([^\)]*\))/gi,$e=/(:[^\s\+>~\.\[:]+)/g,et=/([^\s\+>~\.\[:]+)/g;function tt(e,t){var r=e.match(t);return r?[e.replace(t," "),r.length]:[e,0]}function rt(e){var t=[0,0,0],r=e.replace(/:not\(([^\)]*)\)/g,"     $1 ").replace(/{[\s\S]*/gm," "),n=0,o=tt(r,Xe),s=(0,u.default)(o,2);r=s[0],n=s[1],t[1]+=n;var i=tt(r,We),a=(0,u.default)(i,2);r=a[0],n=a[1],t[0]+=n;var A=tt(r,Je),c=(0,u.default)(A,2);r=c[0],n=c[1],t[1]+=n;var l=tt(r,Ye),d=(0,u.default)(l,2);r=d[0],n=d[1],t[2]+=n;var f=tt(r,Ze),h=(0,u.default)(f,2);r=h[0],n=h[1],t[1]+=n;var p=tt(r,$e),m=(0,u.default)(p,2);r=m[0],n=m[1],t[1]+=n;var g=tt(r=r.replace(/[\*\s\+>~]/g," ").replace(/[#\.]/g," "),et),y=(0,u.default)(g,2);return r=y[0],n=y[1],t[2]+=n,t.join("")}var nt=1e-8;function ot(e){return Math.sqrt(Math.pow(e[0],2)+Math.pow(e[1],2))}function st(e,t){return(e[0]*t[0]+e[1]*t[1])/(ot(e)*ot(t))}function it(e,t){return(e[0]*t[1]<e[1]*t[0]?-1:1)*Math.acos(st(e,t))}function at(e){return e*e*e}function At(e){return 3*e*e*(1-e)}function ut(e){return 3*e*(1-e)*(1-e)}function ct(e){return(1-e)*(1-e)*(1-e)}function lt(e){return e*e}function dt(e){return 2*e*(1-e)}function ft(e){return(1-e)*(1-e)}var ht=function(){function e(t,r,n){(0,F.default)(this,e),this.document=t,this.name=r,this.value=n,this.isNormalizedColor=!1}return(0,U.default)(e,[{key:"split",value:function(){var t,r,n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:" ",o=this.document,s=this.name;return A()(t=G()(r=Re(this.getString())).call(r).split(n)).call(t,(function(t){return new e(o,s,t)}))}},{key:"hasValue",value:function(e){var t=this.value;return null!==t&&""!==t&&(e||0!==t)&&void 0!==t}},{key:"isString",value:function(e){var t=this.value,r="string"==typeof t;return r&&e?e.test(t):r}},{key:"isUrlDefinition",value:function(){return this.isString(/^url\(/)}},{key:"isPixels",value:function(){if(!this.hasValue())return!1;var e=this.getString();switch(!0){case/px$/.test(e):case/^[0-9]+$/.test(e):return!0;default:return!1}}},{key:"setValue",value:function(e){return this.value=e,this}},{key:"getValue",value:function(e){return void 0===e||this.hasValue()?this.value:e}},{key:"getNumber",value:function(e){if(!this.hasValue())return void 0===e?0:i()(e);var t=this.value,r=i()(t);return this.isString(/%$/)&&(r/=100),r}},{key:"getString",value:function(e){return void 0===e||this.hasValue()?void 0===this.value?"":String(this.value):String(e)}},{key:"getColor",value:function(e){var t=this.getString(e);return this.isNormalizedColor||(this.isNormalizedColor=!0,t=Ge(t),this.value=t),t}},{key:"getDpi",value:function(){return 96}},{key:"getRem",value:function(){return this.document.rootEmSize}},{key:"getEm",value:function(){return this.document.emSize}},{key:"getUnits",value:function(){return this.getString().replace(/[0-9\.\-]/g,"")}},{key:"getPixels",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]&&arguments[1];if(!this.hasValue())return 0;var r="boolean"==typeof e?[void 0,e]:[e],n=(0,u.default)(r,2),o=n[0],s=n[1],i=this.document.screen.viewPort;switch(!0){case this.isString(/vmin$/):return this.getNumber()/100*Math.min(i.computeSize("x"),i.computeSize("y"));case this.isString(/vmax$/):return this.getNumber()/100*Math.max(i.computeSize("x"),i.computeSize("y"));case this.isString(/vw$/):return this.getNumber()/100*i.computeSize("x");case this.isString(/vh$/):return this.getNumber()/100*i.computeSize("y");case this.isString(/rem$/):return this.getNumber()*this.getRem();case this.isString(/em$/):return this.getNumber()*this.getEm();case this.isString(/ex$/):return this.getNumber()*this.getEm()/2;case this.isString(/px$/):return this.getNumber();case this.isString(/pt$/):return this.getNumber()*this.getDpi()*(1/72);case this.isString(/pc$/):return 15*this.getNumber();case this.isString(/cm$/):return this.getNumber()*this.getDpi()/2.54;case this.isString(/mm$/):return this.getNumber()*this.getDpi()/25.4;case this.isString(/in$/):return this.getNumber()*this.getDpi();case this.isString(/%$/)&&s:return this.getNumber()*this.getEm();case this.isString(/%$/):return this.getNumber()*i.computeSize(o);default:var a=this.getNumber();return t&&a<1?a*i.computeSize(o):a}}},{key:"getMilliseconds",value:function(){return this.hasValue()?this.isString(/ms$/)?this.getNumber():1e3*this.getNumber():0}},{key:"getRadians",value:function(){if(!this.hasValue())return 0;switch(!0){case this.isString(/deg$/):return this.getNumber()*(Math.PI/180);case this.isString(/grad$/):return this.getNumber()*(Math.PI/200);case this.isString(/rad$/):return this.getNumber();default:return this.getNumber()*(Math.PI/180)}}},{key:"getDefinition",value:function(){var e=this.getString(),t=e.match(/#([^\)'"]+)/);return t&&(t=t[1]),t||(t=e),this.document.definitions[t]}},{key:"getFillStyleDefinition",value:function(e,t){var r=this.getDefinition();if(!r)return null;if("function"==typeof r.createGradient)return r.createGradient(this.document.ctx,e,t);if("function"==typeof r.createPattern){if(r.getHrefAttribute().hasValue()){var n=r.getAttribute("patternTransform");r=r.getHrefAttribute().getDefinition(),n.hasValue()&&r.getAttribute("patternTransform",!0).setValue(n.value)}return r.createPattern(this.document.ctx,e,t)}return null}},{key:"getTextBaseline",value:function(){return this.hasValue()?e.textBaselineMapping[this.getString()]:null}},{key:"addOpacity",value:function(t){for(var r=this.getColor(),n=r.length,o=0,s=0;s<n&&(","===r[s]&&o++,3!==o);s++);if(t.hasValue()&&this.isString()&&3!==o){var i=new(W())(r);i.ok&&(i.alpha=t.getNumber(),r=i.toRGBA())}return new e(this.document,this.name,r)}}],[{key:"empty",value:function(t){return new e(t,"EMPTY","")}}]),e}();ht.textBaselineMapping={baseline:"alphabetic","before-edge":"top","text-before-edge":"top",middle:"middle",central:"middle","after-edge":"bottom","text-after-edge":"bottom",ideographic:"ideographic",alphabetic:"alphabetic",hanging:"hanging",mathematical:"alphabetic"};var pt=function(){function e(){(0,F.default)(this,e),this.viewPorts=[]}return(0,U.default)(e,[{key:"clear",value:function(){this.viewPorts=[]}},{key:"setCurrent",value:function(e,t){this.viewPorts.push({width:e,height:t})}},{key:"removeCurrent",value:function(){this.viewPorts.pop()}},{key:"getCurrent",value:function(){var e=this.viewPorts;return e[e.length-1]}},{key:"computeSize",value:function(e){return"number"==typeof e?e:"x"===e?this.width:"y"===e?this.height:Math.sqrt(Math.pow(this.width,2)+Math.pow(this.height,2))/Math.sqrt(2)}},{key:"width",get:function(){return this.getCurrent().width}},{key:"height",get:function(){return this.getCurrent().height}}]),e}(),mt=function(){function e(t,r){(0,F.default)(this,e),this.x=t,this.y=r}return(0,U.default)(e,[{key:"angleTo",value:function(e){return Math.atan2(e.y-this.y,e.x-this.x)}},{key:"applyTransform",value:function(e){var t=this.x,r=this.y,n=t*e[0]+r*e[2]+e[4],o=t*e[1]+r*e[3]+e[5];this.x=n,this.y=o}}],[{key:"parse",value:function(t){var r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0,n=Ke(t),o=(0,u.default)(n,2),s=o[0],i=void 0===s?r:s,a=o[1],A=void 0===a?r:a;return new e(i,A)}},{key:"parseScale",value:function(t){var r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:1,n=Ke(t),o=(0,u.default)(n,2),s=o[0],i=void 0===s?r:s,a=o[1],A=void 0===a?i:a;return new e(i,A)}},{key:"parsePath",value:function(t){for(var r=Ke(t),n=r.length,o=[],s=0;s<n;s+=2)o.push(new e(r[s],r[s+1]));return o}}]),e}(),gt=function(){function e(t){var r,n;(0,F.default)(this,e),this.screen=t,this.working=!1,this.events=[],this.eventElements=[],this.onClick=K()(r=this.onClick).call(r,this),this.onMouseMove=K()(n=this.onMouseMove).call(n,this)}return(0,U.default)(e,[{key:"isWorking",value:function(){return this.working}},{key:"start",value:function(){if(!this.working){var e=this.screen,t=this.onClick,r=this.onMouseMove,n=e.ctx.canvas;n.onclick=t,n.onmousemove=r,this.working=!0}}},{key:"stop",value:function(){if(this.working){var e=this.screen.ctx.canvas;this.working=!1,e.onclick=null,e.onmousemove=null}}},{key:"hasEvents",value:function(){return this.working&&this.events.length>0}},{key:"runEvents",value:function(){if(this.working){var e=this.screen,t=this.events,r=this.eventElements,n=e.ctx.canvas.style;n&&(n.cursor=""),g()(t).call(t,(function(e,t){for(var n=e.run,o=r[t];o;)n(o),o=o.parent})),this.events=[],this.eventElements=[]}}},{key:"checkPath",value:function(e,t){if(this.working&&t){var r=this.events,n=this.eventElements;g()(r).call(r,(function(r,o){var s=r.x,i=r.y;!n[o]&&t.isPointInPath&&t.isPointInPath(s,i)&&(n[o]=e)}))}}},{key:"checkBoundingBox",value:function(e,t){if(this.working&&t){var r=this.events,n=this.eventElements;g()(r).call(r,(function(r,o){var s=r.x,i=r.y;!n[o]&&t.isPointInBox(s,i)&&(n[o]=e)}))}}},{key:"mapXY",value:function(e,t){for(var r=this.screen,n=r.window,o=r.ctx,s=new mt(e,t),i=o.canvas;i;)s.x-=i.offsetLeft,s.y-=i.offsetTop,i=i.offsetParent;return n.scrollX&&(s.x+=n.scrollX),n.scrollY&&(s.y+=n.scrollY),s}},{key:"onClick",value:function(e){var t=this.mapXY((e||event).clientX,(e||event).clientY),r=t.x,n=t.y;this.events.push({type:"onclick",x:r,y:n,run:function(e){e.onClick&&e.onClick()}})}},{key:"onMouseMove",value:function(e){var t=this.mapXY((e||event).clientX,(e||event).clientY),r=t.x,n=t.y;this.events.push({type:"onmousemove",x:r,y:n,run:function(e){e.onMouseMove&&e.onMouseMove()}})}}]),e}(),yt="undefined"!=typeof window?window:null,vt="undefined"!=typeof fetch?K()(fetch).call(fetch,void 0):null,wt=function(){function e(t){var r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=r.fetch,o=void 0===n?vt:n,s=r.window,i=void 0===s?yt:s;(0,F.default)(this,e),this.ctx=t,this.FRAMERATE=30,this.MAX_VIRTUAL_PIXELS=3e4,this.CLIENT_WIDTH=800,this.CLIENT_HEIGHT=600,this.viewPort=new pt,this.mouse=new gt(this),this.animations=[],this.waits=[],this.frameDuration=0,this.isReadyLock=!1,this.isFirstRender=!0,this.intervalId=null,this.window=i,this.fetch=o}return(0,U.default)(e,[{key:"wait",value:function(e){this.waits.push(e)}},{key:"ready",value:function(){return this.readyPromise?this.readyPromise:M().resolve()}},{key:"isReady",value:function(){var e;if(this.isReadyLock)return!0;var t=k()(e=this.waits).call(e,(function(e){return e()}));return t&&(this.waits=[],this.resolveReady&&this.resolveReady()),this.isReadyLock=t,t}},{key:"setDefaults",value:function(e){e.strokeStyle="rgba(0,0,0,0)",e.lineCap="butt",e.lineJoin="miter",e.miterLimit=4}},{key:"setViewBox",value:function(e){var t=e.document,r=e.ctx,n=e.aspectRatio,o=e.width,s=e.desiredWidth,i=e.height,a=e.desiredHeight,A=e.minX,c=void 0===A?0:A,l=e.minY,d=void 0===l?0:l,f=e.refX,h=e.refY,p=e.clip,m=void 0!==p&&p,g=e.clipX,y=void 0===g?0:g,v=e.clipY,w=void 0===v?0:v,b=Re(n).replace(/^defer\s/,"").split(" "),B=(0,u.default)(b,2),j=B[0]||"xMidYMid",_=B[1]||"meet",C=o/s,x=i/a,E=Math.min(C,x),N=Math.max(C,x),Q=s,F=a;"meet"===_&&(Q*=E,F*=E),"slice"===_&&(Q*=N,F*=N);var U=new ht(t,"refX",f),S=new ht(t,"refY",h),L=U.hasValue()&&S.hasValue();if(L&&r.translate(-E*U.getPixels("x"),-E*S.getPixels("y")),m){var T=E*y,H=E*w;r.beginPath(),r.moveTo(T,H),r.lineTo(o,H),r.lineTo(o,i),r.lineTo(T,i),r.closePath(),r.clip()}if(!L){var I="meet"===_&&E===x,P="slice"===_&&N===x,O="meet"===_&&E===C,k="slice"===_&&N===C;/^xMid/.test(j)&&(I||P)&&r.translate(o/2-Q/2,0),/YMid$/.test(j)&&(O||k)&&r.translate(0,i/2-F/2),/^xMax/.test(j)&&(I||P)&&r.translate(o-Q,0),/YMax$/.test(j)&&(O||k)&&r.translate(0,i-F)}switch(!0){case"none"===j:r.scale(C,x);break;case"meet"===_:r.scale(E,E);break;case"slice"===_:r.scale(N,N)}r.translate(-c,-d)}},{key:"start",value:function(e){var t=this,r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=r.enableRedraw,o=void 0!==n&&n,s=r.ignoreMouse,i=void 0!==s&&s,a=r.ignoreAnimation,A=void 0!==a&&a,u=r.ignoreDimensions,c=void 0!==u&&u,l=r.ignoreClear,d=void 0!==l&&l,f=r.forceRedraw,h=r.scaleWidth,p=r.scaleHeight,m=r.offsetX,g=r.offsetY,y=this.FRAMERATE,v=this.mouse,w=1e3/y;if(this.frameDuration=w,this.readyPromise=new(M())((function(e){t.resolveReady=e})),this.isReady()&&this.render(e,c,d,h,p,m,g),o){var b=P()(),B=b,j=0,_=function r(){b=P()(),(j=b-B)>=w&&(B=b-j%w,t.shouldUpdate(A,f)&&(t.render(e,c,d,h,p,m,g),v.runEvents())),t.intervalId=V()(r)};i||v.start(),this.intervalId=V()(_)}}},{key:"stop",value:function(){this.intervalId&&(V().cancel(this.intervalId),this.intervalId=null),this.mouse.stop()}},{key:"shouldUpdate",value:function(e,t){if(!e){var r,n=this.frameDuration;if(H()(r=this.animations).call(r,(function(e,t){return t.update(n)||e}),!1))return!0}return!("function"!=typeof t||!t())||!(this.isReadyLock||!this.isReady())||!!this.mouse.hasEvents()}},{key:"render",value:function(e,t,r,n,o,s,i){var a=this.CLIENT_WIDTH,A=this.CLIENT_HEIGHT,u=this.viewPort,c=this.ctx,l=this.isFirstRender,d=c.canvas;u.clear(),d.width&&d.height?u.setCurrent(d.width,d.height):u.setCurrent(a,A);var f=e.getStyle("width"),h=e.getStyle("height");!t&&(l||"number"!=typeof n&&"number"!=typeof o)&&(f.hasValue()&&(d.width=f.getPixels("x"),d.style&&(d.style.width="".concat(d.width,"px"))),h.hasValue()&&(d.height=h.getPixels("y"),d.style&&(d.style.height="".concat(d.height,"px"))));var p=d.clientWidth||d.width,m=d.clientHeight||d.height;if(t&&f.hasValue()&&h.hasValue()&&(p=f.getPixels("x"),m=h.getPixels("y")),u.setCurrent(p,m),"number"==typeof s&&e.getAttribute("x",!0).setValue(s),"number"==typeof i&&e.getAttribute("y",!0).setValue(i),"number"==typeof n||"number"==typeof o){var g,y,v=Ke(e.getAttribute("viewBox").getString()),w=0,b=0;if("number"==typeof n){var B=e.getStyle("width");B.hasValue()?w=B.getPixels("x")/n:isNaN(v[2])||(w=v[2]/n)}if("number"==typeof o){var j=e.getStyle("height");j.hasValue()?b=j.getPixels("y")/o:isNaN(v[3])||(b=v[3]/o)}w||(w=b),b||(b=w),e.getAttribute("width",!0).setValue(n),e.getAttribute("height",!0).setValue(o);var _=e.getStyle("transform",!0,!0);_.setValue(L()(g=L()(y="".concat(_.getString()," scale(")).call(y,1/w,", ")).call(g,1/b,")"))}r||c.clearRect(0,0,p,m),e.render(c),l&&(this.isFirstRender=!1)}}]),e}();wt.defaultWindow=yt,wt.defaultFetch=vt;var bt=wt.defaultFetch,Bt="undefined"!=typeof DOMParser?DOMParser:null,jt=function(){function e(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},r=t.fetch,n=void 0===r?bt:r,o=t.DOMParser,s=void 0===o?Bt:o;(0,F.default)(this,e),this.fetch=n,this.DOMParser=s}var t,r;return(0,U.default)(e,[{key:"parse",value:(r=(0,N.default)(E().mark((function e(t){return E().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:if(!/^</.test(t)){e.next=2;break}return e.abrupt("return",this.parseFromString(t));case 2:return e.abrupt("return",this.load(t));case 3:case"end":return e.stop()}}),e,this)}))),function(e){return r.apply(this,arguments)})},{key:"parseFromString",value:function(e){var t=new this.DOMParser;try{return this.checkDocument(t.parseFromString(e,"image/svg+xml"))}catch(r){return this.checkDocument(t.parseFromString(e,"text/xml"))}}},{key:"checkDocument",value:function(e){var t=e.getElementsByTagName("parsererror")[0];if(t)throw new Error(t.textContent);return e}},{key:"load",value:(t=(0,N.default)(E().mark((function e(t){var r,n;return E().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,this.fetch(t);case 2:return r=e.sent,e.next=5,r.text();case 5:return n=e.sent,e.abrupt("return",this.parseFromString(n));case 7:case"end":return e.stop()}}),e,this)}))),function(e){return t.apply(this,arguments)})}]),e}(),_t=function(){function e(t,r){(0,F.default)(this,e),this.type="translate",this.point=null,this.point=mt.parse(r)}return(0,U.default)(e,[{key:"apply",value:function(e){var t=this.point,r=t.x,n=t.y;e.translate(r||0,n||0)}},{key:"unapply",value:function(e){var t=this.point,r=t.x,n=t.y;e.translate(-1*r||0,-1*n||0)}},{key:"applyToPoint",value:function(e){var t=this.point,r=t.x,n=t.y;e.applyTransform([1,0,0,1,r||0,n||0])}}]),e}(),Ct=function(){function e(t,r,n){(0,F.default)(this,e),this.type="rotate",this.angle=null,this.originX=null,this.originY=null,this.cx=0,this.cy=0;var o=Ke(r);this.angle=new ht(t,"angle",o[0]),this.originX=n[0],this.originY=n[1],this.cx=o[1]||0,this.cy=o[2]||0}return(0,U.default)(e,[{key:"apply",value:function(e){var t=this.cx,r=this.cy,n=this.originX,o=this.originY,s=this.angle,i=t+n.getPixels("x"),a=r+o.getPixels("y");e.translate(i,a),e.rotate(s.getRadians()),e.translate(-i,-a)}},{key:"unapply",value:function(e){var t=this.cx,r=this.cy,n=this.angle;e.translate(t,r),e.rotate(-1*n.getRadians()),e.translate(-t,-r)}},{key:"applyToPoint",value:function(e){var t=this.cx,r=this.cy,n=this.angle.getRadians();e.applyTransform([1,0,0,1,t||0,r||0]),e.applyTransform([Math.cos(n),Math.sin(n),-Math.sin(n),Math.cos(n),0,0]),e.applyTransform([1,0,0,1,-t||0,-r||0])}}]),e}(),xt=function(){function e(t,r){(0,F.default)(this,e),this.type="scale",this.scale=null;var n=mt.parseScale(r);0!==n.x&&0!==n.y||(n.x=nt,n.y=nt),this.scale=n}return(0,U.default)(e,[{key:"apply",value:function(e){var t=this.scale,r=t.x,n=t.y;e.scale(r,n||r)}},{key:"unapply",value:function(e){var t=this.scale,r=t.x,n=t.y;e.scale(1/r,1/n||r)}},{key:"applyToPoint",value:function(e){var t=this.scale,r=t.x,n=t.y;e.applyTransform([r||0,0,0,n||0,0,0])}}]),e}(),Et=function(){function e(t,r){(0,F.default)(this,e),this.type="matrix",this.matrix=[],this.matrix=Ke(r)}return(0,U.default)(e,[{key:"apply",value:function(e){var t=this.matrix;e.transform(t[0],t[1],t[2],t[3],t[4],t[5])}},{key:"unapply",value:function(e){var t=this.matrix,r=t[0],n=t[2],o=t[4],s=t[1],i=t[3],a=t[5],A=1/(r*(1*i-0*a)-n*(1*s-0*a)+o*(0*s-0*i));e.transform(A*(1*i-0*a),A*(0*a-1*s),A*(0*o-1*n),A*(1*r-0*o),A*(n*a-o*i),A*(o*s-r*a))}},{key:"applyToPoint",value:function(e){e.applyTransform(this.matrix)}}]),e}();var Nt=function(e){(0,Z.default)(o,e);var t,r,n=(t=o,r=function(){if("undefined"==typeof Reflect||!Y())return!1;if(Y().sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Y()(Date,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,n=(0,ee.default)(t);if(r){var o=(0,ee.default)(this).constructor;e=Y()(n,arguments,o)}else e=n.apply(this,arguments);return(0,$.default)(this,e)});function o(e,t){var r;return(0,F.default)(this,o),(r=n.call(this,e,t)).type="skew",r.angle=null,r.angle=new ht(e,"angle",t),r}return o}(Et);var Qt=function(e){(0,Z.default)(o,e);var t,r,n=(t=o,r=function(){if("undefined"==typeof Reflect||!Y())return!1;if(Y().sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Y()(Date,[],(function(){}))),!0}catch(e){return!1}}(),function(){var e,n=(0,ee.default)(t);if(r){var o=(0,ee.default)(this).constructor;e=Y()(n,arguments,o)}else e=n.apply(this,arguments);return(0,$.default)(this,e)});function o(e,t){var r;return(0,F.default)(this,o),(r=n.call(this,e,t)).type="skewX",r.matrix=[1,0,Math.tan(r.angle.getRadians()),1,0,0],r}return o}(Nt);var Ft=function(e){(0,Z.default)(o,e);var t,r,n=(t=o,r=function(){if("undefined"==ty
    • index.html 19.1 KB · in bundle
    • marked.min.js 51.6 KB
      /**
       * marked v5.1.2 - a markdown parser
       * Copyright (c) 2011-2023, Christopher Jeffrey. (MIT Licensed)
       * https://github.com/markedjs/marked
       */
      !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).marked={})}(this,function(r){"use strict";function i(e,t){for(var u=0;u<t.length;u++){var n=t[u];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,function(e){e=function(e,t){if("object"!=typeof e||null===e)return e;var u=e[Symbol.toPrimitive];if(void 0===u)return("string"===t?String:Number)(e);u=u.call(e,t||"default");if("object"!=typeof u)return u;throw new TypeError("@@toPrimitive must return a primitive value.")}(e,"string");return"symbol"==typeof e?e:String(e)}(n.key),n)}}function g(){return(g=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var u,n=arguments[t];for(u in n)Object.prototype.hasOwnProperty.call(n,u)&&(e[u]=n[u])}return e}).apply(this,arguments)}function s(e,t){(null==t||t>e.length)&&(t=e.length);for(var u=0,n=new Array(t);u<t;u++)n[u]=e[u];return n}function c(e,t){var u,n="undefined"!=typeof Symbol&&e[Symbol.iterator]||e["@@iterator"];if(n)return(n=n.call(e)).next.bind(n);if(Array.isArray(e)||(n=function(e,t){var u;if(e)return"string"==typeof e?s(e,t):"Map"===(u="Object"===(u=Object.prototype.toString.call(e).slice(8,-1))&&e.constructor?e.constructor.name:u)||"Set"===u?Array.from(e):"Arguments"===u||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(u)?s(e,t):void 0}(e))||t&&e&&"number"==typeof e.length)return n&&(e=n),u=0,function(){return u>=e.length?{done:!0}:{done:!1,value:e[u++]}};throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var t=0;function e(e){return"__private_"+t+++"_"+e}function F(e,t){if(Object.prototype.hasOwnProperty.call(e,t))return e;throw new TypeError("attempted to use private field on non-instance")}function u(){return{async:!1,baseUrl:null,breaks:!1,extensions:null,gfm:!0,headerIds:!0,headerPrefix:"",highlight:null,hooks:null,langPrefix:"language-",mangle:!0,pedantic:!1,renderer:null,sanitize:!1,sanitizer:null,silent:!1,smartypants:!1,tokenizer:null,walkTokens:null,xhtml:!1}}function n(e){r.defaults=e}r.defaults=u();function a(e){return j[e]}var o=/[&<>"']/,P=new RegExp(o.source,"g"),l=/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,O=new RegExp(l.source,"g"),j={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"};function D(e,t){if(t){if(o.test(e))return e.replace(P,a)}else if(l.test(e))return e.replace(O,a);return e}var Z=/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/gi;function x(e){return e.replace(Z,function(e,t){return"colon"===(t=t.toLowerCase())?":":"#"===t.charAt(0)?"x"===t.charAt(1)?String.fromCharCode(parseInt(t.substring(2),16)):String.fromCharCode(+t.substring(1)):""})}var q=/(^|[^\[])\^/g;function p(u,e){u="string"==typeof u?u:u.source,e=e||"";var n={replace:function(e,t){return t=(t=t.source||t).replace(q,"$1"),u=u.replace(e,t),n},getRegex:function(){return new RegExp(u,e)}};return n}var L=/[^\w:]/g,U=/^$|^[a-z][a-z0-9+.-]*:|^[?#]/i;function h(e,t,u){if(e){try{n=decodeURIComponent(x(u)).replace(L,"").toLowerCase()}catch(e){return null}if(0===n.indexOf("javascript:")||0===n.indexOf("vbscript:")||0===n.indexOf("data:"))return null}var n;t&&!U.test(u)&&(e=u,f[" "+(n=t)]||(Q.test(n)?f[" "+n]=n+"/":f[" "+n]=A(n,"/",!0)),t=-1===(n=f[" "+n]).indexOf(":"),u="//"===e.substring(0,2)?t?e:n.replace(M,"$1")+e:"/"===e.charAt(0)?t?e:n.replace(H,"$1")+e:n+e);try{u=encodeURI(u).replace(/%25/g,"%")}catch(e){return null}return u}var f={},Q=/^[^:]+:\/*[^/]*$/,M=/^([^:]+:)[\s\S]*$/,H=/^([^:]+:\/*[^/]*)[\s\S]*$/;var d={exec:function(){}};function k(e,t){var u=e.replace(/\|/g,function(e,t,u){for(var n=!1,r=t;0<=--r&&"\\"===u[r];)n=!n;return n?"|":" |"}).split(/ \|/),n=0;if(u[0].trim()||u.shift(),0<u.length&&!u[u.length-1].trim()&&u.pop(),u.length>t)u.splice(t);else for(;u.length<t;)u.push("");for(;n<u.length;n++)u[n]=u[n].trim().replace(/\\\|/g,"|");return u}function A(e,t,u){var n=e.length;if(0===n)return"";for(var r=0;r<n;){var i=e.charAt(n-r-1);if((i!==t||u)&&(i===t||!u))break;r++}return e.slice(0,n-r)}function C(e,t,u,n){var r=t.href,t=t.title?D(t.title):null,i=e[1].replace(/\\([\[\]])/g,"$1");return"!"!==e[0].charAt(0)?(n.state.inLink=!0,e={type:"link",raw:u,href:r,title:t,text:i,tokens:n.inlineTokens(i)},n.state.inLink=!1,e):{type:"image",raw:u,href:r,title:t,text:D(i)}}var E=function(){function e(e){this.options=e||r.defaults}var t=e.prototype;return t.space=function(e){e=this.rules.block.newline.exec(e);if(e&&0<e[0].length)return{type:"space",raw:e[0]}},t.code=function(e){var t,e=this.rules.block.code.exec(e);if(e)return t=e[0].replace(/^ {1,4}/gm,""),{type:"code",raw:e[0],codeBlockStyle:"indented",text:this.options.pedantic?t:A(t,"\n")}},t.fences=function(e){var t,u,n,r,e=this.rules.block.fences.exec(e);if(e)return t=e[0],u=t,n=e[3]||"",u=null===(u=t.match(/^(\s+)(?:```)/))?n:(r=u[1],n.split("\n").map(function(e){var t=e.match(/^\s+/);return null!==t&&t[0].length>=r.length?e.slice(r.length):e}).join("\n")),{type:"code",raw:t,lang:e[2]&&e[2].trim().replace(this.rules.inline._escapes,"$1"),text:u}},t.heading=function(e){var t,u,e=this.rules.block.heading.exec(e);if(e)return t=e[2].trim(),/#$/.test(t)&&(u=A(t,"#"),!this.options.pedantic&&u&&!/ $/.test(u)||(t=u.trim())),{type:"heading",raw:e[0],depth:e[1].length,text:t,tokens:this.lexer.inline(t)}},t.hr=function(e){e=this.rules.block.hr.exec(e);if(e)return{type:"hr",raw:e[0]}},t.blockquote=function(e){var t,u,n,e=this.rules.block.blockquote.exec(e);if(e)return t=e[0].replace(/^ *>[ \t]?/gm,""),u=this.lexer.state.top,this.lexer.state.top=!0,n=this.lexer.blockTokens(t),this.lexer.state.top=u,{type:"blockquote",raw:e[0],tokens:n,text:t}},t.list=function(e){var t=this.rules.block.list.exec(e);if(t){var u,n,r,i,s,a,o,l,D,c,p,h=1<(g=t[1].trim()).length,f={type:"list",raw:"",ordered:h,start:h?+g.slice(0,-1):"",loose:!1,items:[]},g=h?"\\d{1,9}\\"+g.slice(-1):"\\"+g;this.options.pedantic&&(g=h?g:"[*+-]");for(var F=new RegExp("^( {0,3}"+g+")((?:[\t ][^\\n]*)?(?:\\n|$))");e&&(p=!1,t=F.exec(e))&&!this.rules.block.hr.test(e);){if(u=t[0],e=e.substring(u.length),o=t[2].split("\n",1)[0].replace(/^\t+/,function(e){return" ".repeat(3*e.length)}),l=e.split("\n",1)[0],this.options.pedantic?(i=2,c=o.trimLeft()):(i=t[2].search(/[^ ]/),c=o.slice(i=4<i?1:i),i+=t[1].length),s=!1,!o&&/^ *$/.test(l)&&(u+=l+"\n",e=e.substring(l.length+1),p=!0),!p)for(var d=new RegExp("^ {0,"+Math.min(3,i-1)+"}(?:[*+-]|\\d{1,9}[.)])((?:[ \t][^\\n]*)?(?:\\n|$))"),k=new RegExp("^ {0,"+Math.min(3,i-1)+"}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)"),A=new RegExp("^ {0,"+Math.min(3,i-1)+"}(?:```|~~~)"),C=new RegExp("^ {0,"+Math.min(3,i-1)+"}#");e&&(l=D=e.split("\n",1)[0],this.options.pedantic&&(l=l.replace(/^ {1,4}(?=( {4})*[^ ])/g,"  ")),!A.test(l))&&!C.test(l)&&!d.test(l)&&!k.test(e);){if(l.search(/[^ ]/)>=i||!l.trim())c+="\n"+l.slice(i);else{if(s)break;if(4<=o.search(/[^ ]/))break;if(A.test(o))break;if(C.test(o))break;if(k.test(o))break;c+="\n"+l}s||l.trim()||(s=!0),u+=D+"\n",e=e.substring(D.length+1),o=l.slice(i)}f.loose||(a?f.loose=!0:/\n *\n *$/.test(u)&&(a=!0)),this.options.gfm&&(n=/^\[[ xX]\] /.exec(c))&&(r="[ ] "!==n[0],c=c.replace(/^\[[ xX]\] +/,"")),f.items.push({type:"list_item",raw:u,task:!!n,checked:r,loose:!1,text:c}),f.raw+=u}f.items[f.items.length-1].raw=u.trimRight(),f.items[f.items.length-1].text=c.trimRight(),f.raw=f.raw.trimRight();for(var E,x=f.items.length,m=0;m<x;m++)this.lexer.state.top=!1,f.items[m].tokens=this.lexer.blockTokens(f.items[m].text,[]),f.loose||(E=0<(E=f.items[m].tokens.filter(function(e){return"space"===e.type})).length&&E.some(function(e){return/\n.*\n/.test(e.raw)}),f.loose=E);if(f.loose)for(m=0;m<x;m++)f.items[m].loose=!0;return f}},t.html=function(e){var t,e=this.rules.block.html.exec(e);if(e)return t={type:"html",block:!0,raw:e[0],pre:!this.options.sanitizer&&("pre"===e[1]||"script"===e[1]||"style"===e[1]),text:e[0]},this.options.sanitize&&(e=this.options.sanitizer?this.options.sanitizer(e[0]):D(e[0]),t.type="paragraph",t.text=e,t.tokens=this.lexer.inline(e)),t},t.def=function(e){var t,u,n,e=this.rules.block.def.exec(e);if(e)return t=e[1].toLowerCase().replace(/\s+/g," "),u=e[2]?e[2].replace(/^<(.*)>$/,"$1").replace(this.rules.inline._escapes,"$1"):"",n=e[3]&&e[3].substring(1,e[3].length-1).replace(this.rules.inline._escapes,"$1"),{type:"def",tag:t,raw:e[0],href:u,title:n}},t.table=function(e){e=this.rules.block.table.exec(e);if(e){var t={type:"table",header:k(e[1]).map(function(e){return{text:e}}),align:e[2].replace(/^ *|\| *$/g,"").split(/ *\| */),rows:e[3]&&e[3].trim()?e[3].replace(/\n[ \t]*$/,"").split("\n"):[]};if(t.header.length===t.align.length){t.raw=e[0];for(var u,n,r,i=t.align.length,s=0;s<i;s++)/^ *-+: *$/.test(t.align[s])?t.align[s]="right":/^ *:-+: *$/.test(t.align[s])?t.align[s]="center":/^ *:-+ *$/.test(t.align[s])?t.align[s]="left":t.align[s]=null;for(i=t.rows.length,s=0;s<i;s++)t.rows[s]=k(t.rows[s],t.header.length).map(function(e){return{text:e}});for(i=t.header.length,u=0;u<i;u++)t.header[u].tokens=this.lexer.inline(t.header[u].text);for(i=t.rows.length,u=0;u<i;u++)for(r=t.rows[u],n=0;n<r.length;n++)r[n].tokens=this.lexer.inline(r[n].text);return t}}},t.lheading=function(e){e=this.rules.block.lheading.exec(e);if(e)return{type:"heading",raw:e[0],depth:"="===e[2].charAt(0)?1:2,text:e[1],tokens:this.lexer.inline(e[1])}},t.paragraph=function(e){var t,e=this.rules.block.paragraph.exec(e);if(e)return t="\n"===e[1].charAt(e[1].length-1)?e[1].slice(0,-1):e[1],{type:"paragraph",raw:e[0],text:t,tokens:this.lexer.inline(t)}},t.text=function(e){e=this.rules.block.text.exec(e);if(e)return{type:"text",raw:e[0],text:e[0],tokens:this.lexer.inline(e[0])}},t.escape=function(e){e=this.rules.inline.escape.exec(e);if(e)return{type:"escape",raw:e[0],text:D(e[1])}},t.tag=function(e){e=this.rules.inline.tag.exec(e);if(e)return!this.lexer.state.inLink&&/^<a /i.test(e[0])?this.lexer.state.inLink=!0:this.lexer.state.inLink&&/^<\/a>/i.test(e[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&/^<(pre|code|kbd|script)(\s|>)/i.test(e[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&/^<\/(pre|code|kbd|script)(\s|>)/i.test(e[0])&&(this.lexer.state.inRawBlock=!1),{type:this.options.sanitize?"text":"html",raw:e[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,block:!1,text:this.options.sanitize?this.options.sanitizer?this.options.sanitizer(e[0]):D(e[0]):e[0]}},t.link=function(e){e=this.rules.inline.link.exec(e);if(e){var t=e[2].trim();if(!this.options.pedantic&&/^</.test(t)){if(!/>$/.test(t))return;var u=A(t.slice(0,-1),"\\");if((t.length-u.length)%2==0)return}else{u=function(e,t){if(-1!==e.indexOf(t[1]))for(var u=e.length,n=0,r=0;r<u;r++)if("\\"===e[r])r++;else if(e[r]===t[0])n++;else if(e[r]===t[1]&&--n<0)return r;return-1}(e[2],"()");-1<u&&(r=(0===e[0].indexOf("!")?5:4)+e[1].length+u,e[2]=e[2].substring(0,u),e[0]=e[0].substring(0,r).trim(),e[3]="")}var n,u=e[2],r="";return this.options.pedantic?(n=/^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(u))&&(u=n[1],r=n[3]):r=e[3]?e[3].slice(1,-1):"",u=u.trim(),C(e,{href:(u=/^</.test(u)?this.options.pedantic&&!/>$/.test(t)?u.slice(1):u.slice(1,-1):u)&&u.replace(this.rules.inline._escapes,"$1"),title:r&&r.replace(this.rules.inline._escapes,"$1")},e[0],this.lexer)}},t.reflink=function(e,t){var u;if(u=(u=this.rules.inline.reflink.exec(e))||this.rules.inline.nolink.exec(e))return(e=t[(e=(u[2]||u[1]).replace(/\s+/g," ")).toLowerCase()])?C(u,e,u[0],this.lexer):{type:"text",raw:t=u[0].charAt(0),text:t}},t.emStrong=function(e,t,u){void 0===u&&(u="");var n=this.rules.inline.emStrong.lDelim.exec(e);if(n&&((!n[3]||!u.match(/(?:[0-9A-Za-z\xAA\xB2\xB3\xB5\xB9\xBA\xBC-\xBE\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0560-\u0588\u05D0-\u05EA\u05EF-\u05F2\u0620-\u064A\u0660-\u0669\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07C0-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u0860-\u086A\u0870-\u0887\u0889-\u088E\u08A0-\u08C9\u0904-\u0939\u093D\u0950\u0958-\u0961\u0966-\u096F\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09E6-\u09F1\u09F4-\u09F9\u09FC\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A66-\u0A6F\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AE6-\u0AEF\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B66-\u0B6F\u0B71-\u0B77\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0BE6-\u0BF2\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C5D\u0C60\u0C61\u0C66-\u0C6F\u0C78-\u0C7E\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDD\u0CDE\u0CE0\u0CE1\u0CE6-\u0CEF\u0CF1\u0CF2\u0D04-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D58-\u0D61\u0D66-\u0D78\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DE6-\u0DEF\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E50-\u0E59\u0E81\u0E82\u0E84\u0E86-\u0E8A\u0E8C-\u0EA3\u0EA5\u0EA7-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0ED0-\u0ED9\u0EDC-\u0EDF\u0F00\u0F20-\u0F33\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F-\u1049\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u1090-\u1099\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1369-\u137C\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u1711\u171F-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u17E0-\u17E9\u17F0-\u17F9\u1810-\u1819\u1820-\u1878\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1946-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u19D0-\u19DA\u1A00-\u1A16\u1A20-\u1A54\u1A80-\u1A89\u1A90-\u1A99\u1AA7\u1B05-\u1B33\u1B45-\u1B4C\u1B50-\u1B59\u1B83-\u1BA0\u1BAE-\u1BE5\u1C00-\u1C23\u1C40-\u1C49\u1C4D-\u1C7D\u1C80-\u1C88\u1C90-\u1CBA\u1CBD-\u1CBF\u1CE9-\u1CEC\u1CEE-\u1CF3\u1CF5\u1CF6\u1CFA\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2070\u2071\u2074-\u2079\u207F-\u2089\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2150-\u2189\u2460-\u249B\u24EA-\u24FF\u2776-\u2793\u2C00-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2CFD\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312F\u3131-\u318E\u3192-\u3195\u31A0-\u31BF\u31F0-\u31FF\u3220-\u3229\u3248-\u324F\u3251-\u325F\u3280-\u3289\u32B1-\u32BF\u3400-\u4DBF\u4E00-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6EF\uA717-\uA71F\uA722-\uA788\uA78B-\uA7CA\uA7D0\uA7D1\uA7D3\uA7D5-\uA7D9\uA7F2-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA830-\uA835\uA840-\uA873\uA882-\uA8B3\uA8D0-\uA8D9\uA8F2-\uA8F7\uA8FB\uA8FD\uA8FE\uA900-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF-\uA9D9\uA9E0-\uA9E4\uA9E6-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA50-\uAA59\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB69\uAB70-\uABE2\uABF0-\uABF9\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD07-\uDD33\uDD40-\uDD78\uDD8A\uDD8B\uDE80-\uDE9C\uDEA0-\uDED0\uDEE1-\uDEFB\uDF00-\uDF23\uDF2D-\uDF4A\uDF50-\uDF75\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5]|\uD801[\uDC00-\uDC9D\uDCA0-\uDCA9\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDD70-\uDD7A\uDD7C-\uDD8A\uDD8C-\uDD92\uDD94\uDD95\uDD97-\uDDA1\uDDA3-\uDDB1\uDDB3-\uDDB9\uDDBB\uDDBC\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67\uDF80-\uDF85\uDF87-\uDFB0\uDFB2-\uDFBA]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC58-\uDC76\uDC79-\uDC9E\uDCA7-\uDCAF\uDCE0-\uDCF2\uDCF4\uDCF5\uDCFB-\uDD1B\uDD20-\uDD39\uDD80-\uDDB7\uDDBC-\uDDCF\uDDD2-\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE35\uDE40-\uDE48\uDE60-\uDE7E\uDE80-\uDE9F\uDEC0-\uDEC7\uDEC9-\uDEE4\uDEEB-\uDEEF\uDF00-\uDF35\uDF40-\uDF55\uDF58-\uDF72\uDF78-\uDF91\uDFA9-\uDFAF]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2\uDCFA-\uDD23\uDD30-\uDD39\uDE60-\uDE7E\uDE80-\uDEA9\uDEB0\uDEB1\uDF00-\uDF27\uDF30-\uDF45\uDF51-\uDF54\uDF70-\uDF81\uDFB0-\uDFCB\uDFE0-\uDFF6]|\uD804[\uDC03-\uDC37\uDC52-\uDC6F\uDC71\uDC72\uDC75\uDC83-\uDCAF\uDCD0-\uDCE8\uDCF0-\uDCF9\uDD03-\uDD26\uDD36-\uDD3F\uDD44\uDD47\uDD50-\uDD72\uDD76\uDD83-\uDDB2\uDDC1-\uDDC4\uDDD0-\uDDDA\uDDDC\uDDE1-\uDDF4\uDE00-\uDE11\uDE13-\uDE2B\uDE3F\uDE40\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEDE\uDEF0-\uDEF9\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D\uDF50\uDF5D-\uDF61]|\uD805[\uDC00-\uDC34\uDC47-\uDC4A\uDC50-\uDC59\uDC5F-\uDC61\uDC80-\uDCAF\uDCC4\uDCC5\uDCC7\uDCD0-\uDCD9\uDD80-\uDDAE\uDDD8-\uDDDB\uDE00-\uDE2F\uDE44\uDE50-\uDE59\uDE80-\uDEAA\uDEB8\uDEC0-\uDEC9\uDF00-\uDF1A\uDF30-\uDF3B\uDF40-\uDF46]|\uD806[\uDC00-\uDC2B\uDCA0-\uDCF2\uDCFF-\uDD06\uDD09\uDD0C-\uDD13\uDD15\uDD16\uDD18-\uDD2F\uDD3F\uDD41\uDD50-\uDD59\uDDA0-\uDDA7\uDDAA-\uDDD0\uDDE1\uDDE3\uDE00\uDE0B-\uDE32\uDE3A\uDE50\uDE5C-\uDE89\uDE9D\uDEB0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC2E\uDC40\uDC50-\uDC6C\uDC72-\uDC8F\uDD00-\uDD06\uDD08\uDD09\uDD0B-\uDD30\uDD46\uDD50-\uDD59\uDD60-\uDD65\uDD67\uDD68\uDD6A-\uDD89\uDD98\uDDA0-\uDDA9\uDEE0-\uDEF2\uDF02\uDF04-\uDF10\uDF12-\uDF33\uDF50-\uDF59\uDFB0\uDFC0-\uDFD4]|\uD808[\uDC00-\uDF99]|\uD809[\uDC00-\uDC6E\uDC80-\uDD43]|\uD80B[\uDF90-\uDFF0]|[\uD80C\uD81C-\uD820\uD822\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879\uD880-\uD883\uD885-\uD887][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2F\uDC41-\uDC46]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDE60-\uDE69\uDE70-\uDEBE\uDEC0-\uDEC9\uDED0-\uDEED\uDF00-\uDF2F\uDF40-\uDF43\uDF50-\uDF59\uDF5B-\uDF61\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDE40-\uDE96\uDF00-\uDF4A\uDF50\uDF93-\uDF9F\uDFE0\uDFE1\uDFE3]|\uD821[\uDC00-\uDFF7]|\uD823[\uDC00-\uDCD5\uDD00-\uDD08]|\uD82B[\uDFF0-\uDFF3\uDFF5-\uDFFB\uDFFD\uDFFE]|\uD82C[\uDC00-\uDD22\uDD32\uDD50-\uDD52\uDD55\uDD64-\uDD67\uDD70-\uDEFB]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99]|\uD834[\uDEC0-\uDED3\uDEE0-\uDEF3\uDF60-\uDF78]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB\uDFCE-\uDFFF]|\uD837[\uDF00-\uDF1E\uDF25-\uDF2A]|\uD838[\uDC30-\uDC6D\uDD00-\uDD2C\uDD37-\uDD3D\uDD40-\uDD49\uDD4E\uDE90-\uDEAD\uDEC0-\uDEEB\uDEF0-\uDEF9]|\uD839[\uDCD0-\uDCEB\uDCF0-\uDCF9\uDFE0-\uDFE6\uDFE8-\uDFEB\uDFED\uDFEE\uDFF0-\uDFFE]|\uD83A[\uDC00-\uDCC4\uDCC7-\uDCCF\uDD00-\uDD43\uDD4B\uDD50-\uDD59]|\uD83B[\uDC71-\uDCAB\uDCAD-\uDCAF\uDCB1-\uDCB4\uDD01-\uDD2D\uDD2F-\uDD3D\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD83C[\uDD00-\uDD0C]|\uD83E[\uDFF0-\uDFF9]|\uD869[\uDC00-\uDEDF\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF39\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|\uD87E[\uDC00-\uDE1D]|\uD884[\uDC00-\uDF4A\uDF50-\uDFFF]|\uD888[\uDC00-\uDFAF])/))&&(!(n[1]||n[2]||"")||!u||this.rules.inline.punctuation.exec(u)))){var r=n[0].length-1,i=r,s=0,a="*"===n[0][0]?this.rules.inline.emStrong.rDelimAst:this.rules.inline.emStrong.rDelimUnd;for(a.lastIndex=0,t=t.slice(-1*e.length+r);null!=(n=a.exec(t));){var o,l=n[1]||n[2]||n[3]||n[4]||n[5]||n[6];if(l)if(l=l.length,n[3]||n[4])i+=l;else if((n[5]||n[6])&&r%3&&!((r+l)%3))s+=l;else if(!(0<(i-=l)))return l=Math.min(l,l+i+s),o=e.slice(0,r+n.index+l+1),Math.min(r,l)%2?(l=o.slice(1,-1),{type:"em",raw:o,text:l,tokens:this.lexer.inlineTokens(l)}):(l=o.slice(2,-2),{type:"strong",raw:o,text:l,tokens:this.lexer.inlineTokens(l)})}}},t.codespan=function(e){var t,u,n,e=this.rules.inline.code.exec(e);if(e)return n=e[2].replace(/\n/g," "),t=/[^ ]/.test(n),u=/^ /.test(n)&&/ $/.test(n),n=D(n=t&&u?n.substring(1,n.length-1):n,!0),{type:"codespan",raw:e[0],text:n}},t.br=function(e){e=this.rules.inline.br.exec(e);if(e)return{type:"br",raw:e[0]}},t.del=function(e){e=this.rules.inline.del.exec(e);if(e)return{type:"del",raw:e[0],text:e[2],tokens:this.lexer.inlineTokens(e[2])}},t.autolink=function(e,t){var u,e=this.rules.inline.autolink.exec(e);if(e)return t="@"===e[2]?"mailto:"+(u=D(this.options.mangle?t(e[1]):e[1])):u=D(e[1]),{type:"link",raw:e[0],text:u,href:t,tokens:[{type:"text",raw:u,text:u}]}},t.url=function(e,t){var u,n,r,i;if(u=this.rules.inline.url.exec(e)){if("@"===u[2])r="mailto:"+(n=D(this.options.mangle?t(u[0]):u[0]));else{for(;i=u[0],u[0]=this.rules.inline._backpedal.exec(u[0])[0],i!==u[0];);n=D(u[0]),r="www."===u[1]?"http://"+u[0]:u[0]}return{type:"link",raw:u[0],text:n,href:r,tokens:[{type:"text",raw:n,text:n}]}}},t.inlineText=function(e,t){e=this.rules.inline.text.exec(e);if(e)return t=this.lexer.state.inRawBlock?this.options.sanitize?this.options.sanitizer?this.options.sanitizer(e[0]):D(e[0]):e[0]:D(this.options.smartypants?t(e[0]):e[0]),{type:"text",raw:e[0],text:t}},e}(),m={newline:/^(?: *(?:\n|$))+/,code:/^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/,fences:/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,hr:/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,heading:/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,blockquote:/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/,list:/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/,html:"^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:</\\1>[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|<![A-Z][\\s\\S]*?(?:>\\n*|$)|<!\\[CDATA\\[[\\s\\S]*?(?:\\]\\]>\\n*|$)|</?(tag)(?: +|\\n|/?>)[\\s\\S]*?(?:(?:\\n *)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)|</(?!script|pre|style|textarea)[a-z][\\w-]*\\s*>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$))",def:/^ {0,3}\[(label)\]: *(?:\n *)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n *)?| *\n *)(title))? *(?:\n+|$)/,table:d,lheading:/^((?:(?!^bull ).|\n(?!\n|bull ))+?)\n {0,3}(=+|-+) *(?:\n+|$)/,_paragraph:/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,text:/^[^\n]+/,_label:/(?!\s*\])(?:\\.|[^\[\]\\])+/,_title:/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/},b=(m.def=p(m.def).replace("label",m._label).replace("title",m._title).getRegex(),m.bullet=/(?:[*+-]|\d{1,9}[.)])/,m.listItemStart=p(/^( *)(bull) */).replace("bull",m.bullet).getRegex(),m.list=p(m.list).replace(/bull/g,m.bullet).replace("hr","\\n+(?=\\1?(?:(?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$))").replace("def","\\n+(?="+m.def.source+")").getRegex(),m._tag="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",m._comment=/<!--(?!-?>)[\s\S]*?(?:-->|$)/,m.html=p(m.html,"i").replace("comment",m._comment).replace("tag",m._tag).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),m.lheading=p(m.lheading).replace(/bull/g,m.bullet).getRegex(),m.paragraph=p(m._paragraph).replace("hr",m.hr).replace("heading"," {0,3}#{1,6} ").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html","</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag",m._tag).getRegex(),m.blockquote=p(m.blockquote).replace("paragraph",m.paragraph).getRegex(),m.normal=g({},m),m.gfm=g({},m.normal,{table:"^ *([^\\n ].*\\|.*)\\n {0,3}(?:\\| *)?(:?-+:? *(?:\\| *:?-+:? *)*)(?:\\| *)?(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)"}),m.gfm.table=p(m.gfm.table).replace("hr",m.hr).replace("heading"," {0,3}#{1,6} ").replace("blockquote"," {0,3}>").replace("code"," {4}[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html","</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag",m._tag).getRegex(),m.gfm.paragraph=p(m._paragraph).replace("hr",m.hr).replace("heading"," {0,3}#{1,6} ").replace("|lheading","").replace("table",m.gfm.table).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html","</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag",m._tag).getRegex(),m.pedantic=g({},m.normal,{html:p("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+?</\\1> *(?:\\n{2,}|\\s*$)|<tag(?:\"[^\"]*\"|'[^']*'|\\s[^'\"/>\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",m._comment).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *<?([^\s>]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:d,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:p(m.normal._paragraph).replace("hr",m.hr).replace("heading"," *#{1,6} *[^\n]").replace("lheading",m.lheading).replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").getRegex()}),{escape:/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,autolink:/^<(scheme:[^\s\x00-\x1f<>]*|email)>/,url:d,tag:"^comment|^</[a-zA-Z][\\w:-]*\\s*>|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^<![a-zA-Z]+\\s[\\s\\S]*?>|^<!\\[CDATA\\[[\\s\\S]*?\\]\\]>",link:/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/,reflink:/^!?\[(label)\]\[(ref)\]/,nolink:/^!?\[(ref)\](?:\[\])?/,reflinkSearch:"reflink|nolink(?!\\()",emStrong:{lDelim:/^(?:\*+(?:((?!\*)[punct])|[^\s*]))|^_+(?:((?!_)[punct])|([^\s_]))/,rDelimAst:/^[^_*]*?__[^_*]*?\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\*)[punct](\*+)(?=[\s]|$)|[^punct\s](\*+)(?!\*)(?=[punct\s]|$)|(?!\*)[punct\s](\*+)(?=[^punct\s])|[\s](\*+)(?!\*)(?=[punct])|(?!\*)[punct](\*+)(?!\*)(?=[punct])|[^punct\s](\*+)(?=[^punct\s])/,rDelimUnd:/^[^_*]*?\*\*[^_*]*?_[^_*]*?(?=\*\*)|[^_]+(?=[^_])|(?!_)[punct](_+)(?=[\s]|$)|[^punct\s](_+)(?!_)(?=[punct\s]|$)|(?!_)[punct\s](_+)(?=[^punct\s])|[\s](_+)(?!_)(?=[punct])|(?!_)[punct](_+)(?!_)(?=[punct])/},code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,br:/^( {2,}|\\)\n(?!\s*$)/,del:d,text:/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\<!\[`*_]|\b_|$)|[^ ](?= {2,}\n)))/,punctuation:/^((?![*_])[\spunctuation])/});function N(e){return e.replace(/---/g,"—").replace(/--/g,"–").replace(/(^|[-\u2014/(\[{"\s])'/g,"$1‘").replace(/'/g,"’").replace(/(^|[-\u2014/(\[{\u2018\s])"/g,"$1“").replace(/"/g,"”").replace(/\.{3}/g,"…")}function w(e){for(var t,u="",n=e.length,r=0;r<n;r++)t=e.charCodeAt(r),u+="&#"+(t=.5<Math.random()?"x"+t.toString(16):t)+";";return u}b._punctuation="\\p{P}$+<=>`^|~",b.punctuation=p(b.punctuation,"u").replace(/punctuation/g,b._punctuation).getRegex(),b.blockSkip=/\[[^[\]]*?\]\([^\(\)]*?\)|`[^`]*?`|<[^<>]*?>/g,b.anyPunctuation=/\\[punct]/g,b._escapes=/\\([punct])/g,b._comment=p(m._comment).replace("(?:--\x3e|$)","--\x3e").getRegex(),b.emStrong.lDelim=p(b.emStrong.lDelim,"u").replace(/punct/g,b._punctuation).getRegex(),b.emStrong.rDelimAst=p(b.emStrong.rDelimAst,"gu").replace(/punct/g,b._punctuation).getRegex(),b.emStrong.rDelimUnd=p(b.emStrong.rDelimUnd,"gu").replace(/punct/g,b._punctuation).getRegex(),b.anyPunctuation=p(b.anyPunctuation,"gu").replace(/punct/g,b._punctuation).getRegex(),b._escapes=p(b._escapes,"gu").replace(/punct/g,b._punctuation).getRegex(),b._scheme=/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/,b._email=/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/,b.autolink=p(b.autolink).replace("scheme",b._scheme).replace("email",b._email).getRegex(),b._attribute=/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/,b.tag=p(b.tag).replace("comment",b._comment).replace("attribute",b._attribute).getRegex(),b._label=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,b._href=/<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/,b._title=/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/,b.link=p(b.link).replace("label",b._label).replace("href",b._href).replace("title",b._title).getRegex(),b.reflink=p(b.reflink).replace("label",b._label).replace("ref",m._label).getRegex(),b.nolink=p(b.nolink).replace("ref",m._label).getRegex(),b.reflinkSearch=p(b.reflinkSearch,"g").replace("reflink",b.reflink).replace("nolink",b.nolink).getRegex(),b.normal=g({},b),b.pedantic=g({},b.normal,{strong:{start:/^__|\*\*/,middle:/^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,endAst:/\*\*(?!\*)/g,endUnd:/__(?!_)/g},em:{start:/^_|\*/,middle:/^()\*(?=\S)([\s\S]*?\S)\*(?!\*)|^_(?=\S)([\s\S]*?\S)_(?!_)/,endAst:/\*(?!\*)/g,endUnd:/_(?!_)/g},link:p(/^!?\[(label)\]\((.*?)\)/).replace("label",b._label).getRegex(),reflink:p(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",b._label).getRegex()}),b.gfm=g({},b.normal,{escape:p(b.escape).replace("])","~|])").getRegex(),_extended_email:/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/,url:/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/,_backpedal:/(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/,del:/^(~~?)(?=[^\s~])([\s\S]*?[^\s~])\1(?=[^~]|$)/,text:/^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\<!\[`*~_]|\b_|https?:\/\/|ftp:\/\/|www\.|$)|[^ ](?= {2,}\n)|[^a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-](?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)))/}),b.gfm.url=p(b.gfm.url,"i").replace("email",b.gfm._extended_email).getRegex(),b.breaks=g({},b.gfm,{br:p(b.br).replace("{2,}","*").getRegex(),text:p(b.gfm.text).replace("\\b_","\\b_| {2,}\\n").replace(/\{2,\}/g,"*").getRegex()});var B=function(){function u(e){this.tokens=[],this.tokens.links=Object.create(null),this.options=e||r.defaults,this.options.tokenizer=this.options.tokenizer||new E,this.tokenizer=this.options.tokenizer,this.tokenizer.options=this.options,(this.tokenizer.lexer=this).inlineQueue=[],this.state={inLink:!1,inRawBlock:!1,top:!0};e={block:m.normal,inline:b.normal};this.options.pedantic?(e.block=m.pedantic,e.inline=b.pedantic):this.options.gfm&&(e.block=m.gfm,this.options.breaks?e.inline=b.breaks:e.inline=b.gfm),this.tokenizer.rules=e}u.lex=function(e,t){return new u(t).lex(e)},u.lexInline=function(e,t){return new u(t).inlineTokens(e)};var e,t,n=u.prototype;return n.lex=function(e){var t;for(e=e.replace(/\r\n|\r/g,"\n"),this.blockTokens(e,this.tokens);t=this.inlineQueue.shift();)this.inlineTokens(t.src,t.tokens);return this.tokens},n.blockTokens=function(r,i){var s,a,o,l,D=this;for(void 0===i&&(i=[]),r=this.options.pedantic?r.replace(/\t/g,"    ").replace(/^ +$/gm,""):r.replace(/^( *)(\t+)/gm,function(e,t,u){return t+"    ".repeat(u.length)});r;){var e=function(){if(D.options.extensions&&D.options.extensions.block&&D.options.extensions.block.some(function(e){return!!(s=e.call({lexer:D},r,i))&&(r=r.substring(s.raw.length),i.push(s),!0)}))return"continue";if(s=D.tokenizer.space(r))return r=r.substring(s.raw.length),1===s.raw.length&&0<i.length?i[i.length-1].raw+="\n":i.push(s),"continue";if(s=D.tokenizer.code(r))return r=r.substring(s.raw.length),!(a=i[i.length-1])||"paragraph"!==a.type&&"text"!==a.type?i.push(s):(a.raw+="\n"+s.raw,a.text+="\n"+s.text,D.inlineQueue[D.inlineQueue.length-1].src=a.text),"continue";if(s=D.tokenizer.fences(r))return r=r.substring(s.raw.length),i.push(s),"continue";if(s=D.tokenizer.heading(r))return r=r.substring(s.raw.length),i.push(s),"continue";if(s=D.tokenizer.hr(r))return r=r.substring(s.raw.length),i.push(s),"continue";if(s=D.tokenizer.blockquote(r))return r=r.substring(s.raw.length),i.push(s),"continue";if(s=D.tokenizer.list(r))return r=r.substring(s.raw.length),i.push(s),"continue";if(s=D.tokenizer.html(r))return r=r.substring(s.raw.length),i.push(s),"continue";if(s=D.tokenizer.def(r))return r=r.substring(s.raw.length),!(a=i[i.length-1])||"paragraph"!==a.type&&"text"!==a.type?D.tokens.links[s.tag]||(D.tokens.links[s.tag]={href:s.href,title:s.title}):(a.raw+="\n"+s.raw,a.text+="\n"+s.raw,D.inlineQueue[D.inlineQueue.length-1].src=a.text),"continue";if(s=D.tokenizer.table(r))return r=r.substring(s.raw.length),i.push(s),"continue";if(s=D.tokenizer.lheading(r))return r=r.substring(s.raw.length),i.push(s),"continue";var t,u,n;if(o=r,D.options.extensions&&D.options.extensions.startBlock&&(t=1/0,u=r.slice(1),D.options.extensions.startBlock.forEach(function(e){"number"==typeof(n=e.call({lexer:this},u))&&0<=n&&(t=Math.min(t,n))}),t<1/0)&&0<=t&&(o=r.substring(0,t+1)),D.state.top&&(s=D.tokenizer.paragraph(o)))return a=i[i.length-1],l&&"paragraph"===a.type?(a.raw+="\n"+s.raw,a.text+="\n"+s.text,D.inlineQueue.pop(),D.inlineQueue[D.inlineQueue.length-1].src=a.text):i.push(s),l=o.length!==r.length,r=r.substring(s.raw.length),"continue";if(s=D.tokenizer.text(r))return r=r.substring(s.raw.length),(a=i[i.length-1])&&"text"===a.type?(a.raw+="\n"+s.raw,a.text+="\n"+s.text,D.inlineQueue.pop(),D.inlineQueue[D.inlineQueue.length-1].src=a.text):i.push(s),"continue";if(r){var e="Infinite loop on byte: "+r.charCodeAt(0);if(D.options.silent)return console.error(e),"break";throw new Error(e)}}();if("continue"!==e&&"break"===e)break}return this.state.top=!0,i},n.inline=function(e,t){return this.inlineQueue.push({src:e,tokens:t=void 0===t?[]:t}),t},n.inlineTokens=function(r,i){var s,a,o,e,l,D,c=this,p=(void 0===i&&(i=[]),r);if(this.tokens.links){var t=Object.keys(this.tokens.links);if(0<t.length)for(;null!=(e=this.tokenizer.rules.inline.reflinkSearch.exec(p));)t.includes(e[0].slice(e[0].lastIndexOf("[")+1,-1))&&(p=p.slice(0,e.index)+"["+"a".repeat(e[0].length-2)+"]"+p.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;null!=(e=this.tokenizer.rules.inline.blockSkip.exec(p));)p=p.slice(0,e.index)+"["+"a".repeat(e[0].length-2)+"]"+p.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);for(;null!=(e=this.tokenizer.rules.inline.anyPunctuation.exec(p));)p=p.slice(0,e.index)+"++"+p.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);for(;r;){var u=function(){if(l||(D=""),l=!1,c.options.extensions&&c.options.extensions.inline&&c.options.extensions.inline.some(function(e){return!!(s=e.call({lexer:c},r,i))&&(r=r.substring(s.raw.length),i.push(s),!0)}))return"continue";if(s=c.tokenizer.escape(r))return r=r.substring(s.raw.length),i.push(s),"continue";if(s=c.tokenizer.tag(r))return r=r.substring(s.raw.length),(a=i[i.length-1])&&"text"===s.type&&"text"===a.type?(a.raw+=s.raw,a.text+=s.text):i.push(s),"continue";if(s=c.tokenizer.link(r))return r=r.substring(s.raw.length),i.push(s),"continue";if(s=c.tokenizer.reflink(r,c.tokens.links))return r=r.substring(s.raw.length),(a=i[i.length-1])&&"text"===s.type&&"text"===a.type?(a.raw+=s.raw,a.text+=s.text):i.push(s),"continue";if(s=c.tokenizer.emStrong(r,p,D))return r=r.substring(s.raw.length),i.push(s),"continue";if(s=c.tokenizer.codespan(r))return r=r.substring(s.raw.length),i.push(s),"continue";if(s=c.tokenizer.br(r))return r=r.substring(s.raw.length),i.push(s),"continue";if(s=c.tokenizer.del(r))return r=r.substring(s.raw.length),i.push(s),"continue";if(s=c.tokenizer.autolink(r,w))return r=r.substring(s.raw.length),i.push(s),"continue";if(!c.state.inLink&&(s=c.tokenizer.url(r,w)))return r=r.substring(s.raw.length),i.push(s),"continue";var t,u,n;if(o=r,c.options.extensions&&c.options.extensions.startInline&&(t=1/0,u=r.slice(1),c.options.extensions.startInline.forEach(function(e){"number"==typeof(n=e.call({lexer:this},u))&&0<=n&&(t=Math.min(t,n))}),t<1/0)&&0<=t&&(o=r.substring(0,t+1)),s=c.tokenizer.inlineText(o,N))return r=r.substring(s.raw.length),"_"!==s.raw.slice(-1)&&(D=s.raw.slice(-1)),l=!0,(a=i[i.length-1])&&"text"===a.type?(a.raw+=s.raw,a.text+=s.text):i.push(s),"continue";if(r){var e="Infinite loop on byte: "+r.charCodeAt(0);if(c.options.silent)return console.error(e),"break";throw new Error(e)}}();if("continue"!==u&&"break"===u)break}return i},n=u,t=[{key:"rules",get:function(){return{block:m,inline:b}}}],(e=null)&&i(n.prototype,e),t&&i(n,t),Object.defineProperty(n,"prototype",{writable:!1}),u}(),y=function(){function e(e){this.options=e||r.defaults}var t=e.prototype;return t.code=function(e,t,u){var n,t=(t||"").match(/\S*/)[0];return this.options.highlight&&null!=(n=this.options.highlight(e,t))&&n!==e&&(u=!0,e=n),e=e.replace(/\n$/,"")+"\n",t?'<pre><code class="'+this.options.langPrefix+D(t)+'">'+(u?e:D(e,!0))+"</code></pre>\n":"<pre><code>"+(u?e:D(e,!0))+"</code></pre>\n"},t.blockquote=function(e){return"<blockquote>\n"+e+"</blockquote>\n"},t.html=function(e,t){return e},t.heading=function(e,t,u,n){return this.options.headerIds?"<h"+t+' id="'+(this.options.headerPrefix+n.slug(u))+'">'+e+"</h"+t+">\n":"<h"+t+">"+e+"</h"+t+">\n"},t.hr=function(){return this.options.xhtml?"<hr/>\n":"<hr>\n"},t.list=function(e,t,u){var n=t?"ol":"ul";return"<"+n+(t&&1!==u?' start="'+u+'"':"")+">\n"+e+"</"+n+">\n"},t.listitem=function(e){return"<li>"+e+"</li>\n"},t.checkbox=function(e){return"<input "+(e?'checked="" ':"")+'disabled="" type="checkbox"'+(this.options.xhtml?" /":"")+"> "},t.paragraph=function(e){return"<p>"+e+"</p>\n"},t.table=function(e,t){return"<table>\n<thead>\n"+e+"</thead>\n"+(t=t&&"<tbody>"+t+"</tbody>")+"</table>\n"},t.tablerow=function(e){return"<tr>\n"+e+"</tr>\n"},t.tablecell=function(e,t){var u=t.header?"th":"td";return(t.align?"<"+u+' align="'+t.align+'">':"<"+u+">")+e+"</"+u+">\n"},t.strong=function(e){return"<strong>"+e+"</strong>"},t.em=function(e){return"<em>"+e+"</em>"},t.codespan=function(e){return"<code>"+e+"</code>"},t.br=function(){return this.options.xhtml?"<br/>":"<br>"},t.del=function(e){return"<del>"+e+"</del>"},t.link=function(e,t,u){return null===(e=h(this.options.sanitize,this.options.baseUrl,e))?u:(e='<a href="'+e+'"',t&&(e+=' title="'+t+'"'),e+">"+u+"</a>")},t.image=function(e,t,u){return null===(e=h(this.options.sanitize,this.options.baseUrl,e))?u:(e='<img src="'+e+'" alt="'+u+'"',t&&(e+=' title="'+t+'"'),e+(this.options.xhtml?"/>":">"))},t.text=function(e){return e},e}(),v=function(){function e(){}var t=e.prototype;return t.strong=function(e){return e},t.em=function(e){return e},t.codespan=function(e){return e},t.del=function(e){return e},t.html=function(e){return e},t.text=function(e){return e},t.link=function(e,t,u){return""+u},t.image=function(e,t,u){return""+u},t.br=function(){return""},e}(),_=function(){function e(){this.seen={}}var t=e.prototype;return t.serialize=function(e){return e.toLowerCase().trim().replace(/<[!\/a-z].*?>/gi,"").replace(/[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,./:;<=>?@[\]^`{|}~]/g,"").replace(/\s/g,"-")},t.getNextSafeSlug=function(e,t){var u=e,n=0;if(this.seen.hasOwnProperty(u))for(n=this.seen[e];u=e+"-"+ ++n,this.seen.hasOwnProperty(u););return t||(this.seen[e]=n,this.seen[u]=0),u},t.slug=function(e,t){void 0===t&&(t={});e=this.serialize(e);return this.getNextSafeSlug(e,t.dryrun)},e}(),z=function(){function u(e){this.options=e||r.defaults,this.options.renderer=this.options.renderer||new y,this.renderer=this.options.renderer,this.renderer.options=this.options,this.textRenderer=new v,this.slugger=new _}u.parse=function(e,t){return new u(t).parse(e)},u.parseInline=function(e,t){return new u(t).parseInline(e)};var e=u.prototype;return e.parse=function(e,t){void 0===t&&(t=!0);for(var u,n,r,i,s,a,o,l,D,c,p,h,f,g,F,d,k="",A=e.length,C=0;C<A;C++)if(l=e[C],this.options.extensions&&this.options.extensions.renderers&&this.options.extensions.renderers[l.type]&&(!1!==(d=this.options.extensions.renderers[l.type].call({parser:this},l))||!["space","hr","heading","code","table","blockquote","list","html","paragraph","text"].includes(l.type)))k+=d||"";else switch(l.type){case"space":continue;case"hr":k+=this.renderer.hr();continue;case"heading":k+=this.renderer.heading(this.parseInline(l.tokens),l.depth,x(this.parseInline(l.tokens,this.textRenderer)),this.slugger);continue;case"code":k+=this.renderer.code(l.text,l.lang,l.escaped);continue;case"table":for(a=D="",r=l.header.length,u=0;u<r;u++)a+=this.renderer.tablecell(this.parseInline(l.header[u].tokens),{header:!0,align:l.align[u]});for(D+=this.renderer.tablerow(a),o="",r=l.rows.length,u=0;u<r;u++){for(a="",i=(s=l.rows[u]).length,n=0;n<i;n++)a+=this.renderer.tablecell(this.parseInline(s[n].tokens),{header:!1,align:l.align[n]});o+=this.renderer.tablerow(a)}k+=this.renderer.table(D,o);continue;case"blockquote":o=this.parse(l.tokens),k+=this.renderer.blockquote(o);continue;case"list":for(D=l.ordered,E=l.start,c=l.loose,r=l.items.length,o="",u=0;u<r;u++)f=(h=l.items[u]).checked,g=h.task,p="",h.task&&(F=this.renderer.checkbox(f),c?0<h.tokens.length&&"paragraph"===h.tokens[0].type?(h.tokens[0].text=F+" "+h.tokens[0].text,h.tokens[0].tokens&&0<h.tokens[0].tokens.length&&"text"===h.tokens[0].tokens[0].type&&(h.tokens[0].tokens[0].text=F+" "+h.tokens[0].tokens[0].text)):h.tokens.unshift({type:"text",text:F}):p+=F),p+=this.parse(h.tokens,c),o+=this.renderer.listitem(p,g,f);k+=this.renderer.list(o,D,E);continue;case"html":k+=this.renderer.html(l.text,l.block);continue;case"paragraph":k+=this.renderer.paragraph(this.parseInline(l.tokens));continue;case"text":for(o=l.tokens?this.parseInline(l.tokens):l.text;C+1<A&&"text"===e[C+1].type;)o+="\n"+((l=e[++C]).tokens?this.parseInline(l.tokens):l.text);k+=t?this.renderer.paragraph(o):o;continue;default:var E='Token with "'+l.type+'" type was not found.';if(this.options.silent)return void console.error(E);throw new Error(E)}return k},e.parseInline=function(e,t){t=t||this.renderer;for(var u,n,r="",i=e.length,s=0;s<i;s++)if(u=e[s],this.options.extensions&&this.options.extensions.renderers&&this.options.extensions.renderers[u.type]&&(!1!==(n=this.options.extensions.renderers[u.type].call({parser:this},u))||!["escape","html","link","image","strong","em","codespan","br","del","text"].includes(u.type)))r+=n||"";else switch(u.type){case"escape":r+=t.text(u.text);break;case"html":r+=t.html(u.text);break;case"link":r+=t.link(u.href,u.title,this.parseInline(u.tokens,t));break;case"image":r+=t.image(u.href,u.title,u.text);break;case"strong":r+=t.strong(this.parseInline(u.tokens,t));break;case"em":r+=t.em(this.parseInline(u.tokens,t));break;case"codespan":r+=t.codespan(u.text);break;case"br":r+=t.br();break;case"del":r+=t.del(this.parseInline(u.tokens,t));break;case"text":r+=t.text(u.text);break;default:var a='Token with "'+u.type+'" type was not found.';if(this.options.silent)return void console.error(a);throw new Error(a)}return r},u}(),$=function(){function e(e){this.options=e||r.defaults}var t=e.prototype;return t.preprocess=function(e){return e},t.postprocess=function(e){return e},e}(),S=($.passThroughHooks=new Set(["preprocess","postprocess"]),e("parseMarkdown")),T=e("onError"),d=function(){function e(){Object.defineProperty(this,T,{value:G}),Object.defineProperty(this,S,{value:X}),this.defaults=u(),this.options=this.setOptions,this.parse=F(this,S)[S](B.lex,z.parse),this.parseInline=F(this,S)[S](B.lexInline,z.parseInline),this.Parser=z,this.parser=z.parse,this.Renderer=y,this.TextRenderer=v,this.Lexer=B,this.lexer=B.lex,this.Tokenizer=E,this.Slugger=_,this.Hooks=$,this.use.apply(this,arguments)}var t=e.prototype;return t.walkTokens=function(e,a){for(var o,l=this,D=[],t=c(e);!(o=t()).done;)!function(){var t=o.value;switch(D=D.concat(a.call(l,t)),t.type){case"table":for(var e=c(t.header);!(u=e()).done;){var u=u.value;D=D.concat(l.walkTokens(u.tokens,a))}for(var n,r=c(t.rows);!(n=r()).done;)for(var i=c(n.value);!(s=i()).done;){var s=s.value;D=D.concat(l.walkTokens(s.tokens,a))}break;case"list":D=D.concat(l.walkTokens(t.items,a));break;default:l.defaults.extensions&&l.defaults.extensions.childTokens&&l.defaults.extensions.childTokens[t.type]?l.defaults.extensions.childTokens[t.type].forEach(function(e){D=D.concat(l.walkTokens(t[e],a))}):t.tokens&&(D=D.concat(l.walkTokens(t.tokens,a)))}}();return D},t.use=function(){for(var D=this,c=this.defaults.extensions||{renderers:{},childTokens:{}},e=arguments.length,t=new Array(e),u=0;u<e;u++)t[u]=arguments[u];return t.forEach(function(s){var u,e=g({},s);if(e.async=D.defaults.async||e.async||!1,s.extensions&&(s.extensions.forEach(function(r){if(!r.name)throw new Error("extension name required");var i;if(r.renderer&&(i=c.renderers[r.name],c.renderers[r.name]=i?function(){for(var e=arguments.length,t=new Array(e),u=0;u<e;u++)t[u]=arguments[u];var n=r.renderer.apply(this,t);return n=!1===n?i.apply(this,t):n}:r.renderer),r.tokenizer){if(!r.level||"block"!==r.level&&"inline"!==r.level)throw new Error("extension level must be 'block' or 'inline'");c[r.level]?c[r.level].unshift(r.tokenizer):c[r.level]=[r.tokenizer],r.start&&("block"===r.level?c.startBlock?c.startBlock.push(r.start):c.startBlock=[r.start]:"inline"===r.level&&(c.startInline?c.startInline.push(r.start):c.startInline=[r.start]))}r.childTokens&&(c.childTokens[r.name]=r.childTokens)}),e.extensions=c),s.renderer){var t,a=D.defaults.renderer||new y(D.defaults);for(t in s.renderer)!function(r){var i=a[r];a[r]=function(){for(var e=arguments.length,t=new Array(e),u=0;u<e;u++)t[u]=arguments[u];var n=s.renderer[r].apply(a,t);return n=!1===n?i.apply(a,t):n}}(t);e.renderer=a}if(s.tokenizer){var n,o=D.defaults.tokenizer||new E(D.defaults);for(n in s.tokenizer)!function(r){var i=o[r];o[r]=function(){for(var e=arguments.length,t=new Array(e),u=0;u<e;u++)t[u]=arguments[u];var n=s.tokenizer[r].apply(o,t);return n=!1===n?i.apply(o,t):n}}(n);e.tokenizer=o}if(s.hooks){var r,l=D.defaults.hooks||new $;for(r in s.hooks)!function(r){var i=l[r];$.passThroughHooks.has(r)?l[r]=function(e){return D.defaults.async?Promise.resolve(s.hooks[r].call(l,e)).then(function(e){return i.call(l,e)}):(e=s.hooks[r].call(l,e),i.call(l,e))}:l[r]=function(){for(var e=arguments.length,t=new Array(e),u=0;u<e;u++)t[u]=arguments[u];var n=s.hooks[r].apply(l,t);return n=!1===n?i.apply(l,t):n}}(r);e.hooks=l}s.walkTokens&&(u=D.defaults.walkTokens,e.walkTokens=function(e){var t=[];return t.push(s.walkTokens.call(this,e)),t=u?t.concat(u.call(this,e)):t}),D.defaults=g({},D.defaults,e)}),this},t.setOptions=function(e){return this.defaults=g({},this.defaults,e),this},e}();function X(p,h){var f=this;return function(e,u,n){"function"==typeof u&&(n=u,u=null);var t,r=g({},u),i=(u=g({},f.defaults,r),F(f,T)[T](u.silent,u.async,n));if(null==e)return i(new Error("marked(): input parameter is undefined or null"));if("string"!=typeof e)return i(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(e)+", string expected"));if(r=n,(t=u)&&!t.silent&&(r&&console.warn("marked(): callback is deprecated since version 5.0.0, should not be used and will be removed in the future. Read more here: https://marked.js.org/using_pro#async"),(t.sanitize||t.sanitizer)&&console.warn("marked(): sanitize and sanitizer parameters are deprecated since version 0.7.0, should not be used and will be removed in the future. Read more here: https://marked.js.org/#/USING_ADVANCED.md#options"),!t.highlight&&"language-"===t.langPrefix||console.warn("marked(): highlight and langPrefix parameters are deprecated since version 5.0.0, should not be used and will be removed in the future. Instead use https://www.npmjs.com/package/marked-highlight."),t.mangle&&console.warn("marked(): mangle parameter is enabled by default, but is deprecated since version 5.0.0, and will be removed in the future. To clear this warning, install https://www.npmjs.com/package/marked-mangle, or disable by setting `{mangle: false}`."),t.baseUrl&&console.warn("marked(): baseUrl parameter is deprecated since version 5.0.0, should not be used and will be removed in the future. Instead use https://www.npmjs.com/package/marked-base-url."),t.smartypants&&console.warn("marked(): smartypants parameter is deprecated since version 5.0.0, should not be used and will be removed in the future. Instead use https://www.npmjs.com/package/marked-smartypants."),t.xhtml&&console.warn("marked(): xhtml parameter is deprecated since version 5.0.0, should not be used and will be removed in the future. Instead use https://www.npmjs.com/package/marked-xhtml."),t.headerIds||t.headerPrefix)&&console.warn("marked(): headerIds and headerPrefix parameters enabled by default, but are deprecated since version 5.0.0, and will be removed in the future. To clear this warning, install  https://www.npmjs.com/package/marked-gfm-heading-id, or disable by setting `{headerIds: false}`."),u.hooks&&(u.hooks.options=u),n){var s,a=u.highlight;try{u.hooks&&(e=u.hooks.preprocess(e)),s=p(e,u)}catch(e){return i(e)}var o,l=function(t){var e;if(!t)try{u.walkTokens&&f.walkTokens(s,u.walkTokens),e=h(s,u),u.hooks&&(e=u.hooks.postprocess(e))}catch(e){t=e}return u.highlight=a,t?i(t):n(null,e)};return!a||a.length<3?l():(delete u.highlight,s.length?(o=0,f.walkTokens(s,function(u){"code"===u.type&&(o++,setTimeout(function(){a(u.text,u.lang,function(e,t){if(e)return l(e);null!=t&&t!==u.text&&(u.text=t,u.escaped=!0),0===--o&&l()})},0))}),void(0===o&&l())):l())}if(u.async)return Promise.resolve(u.hooks?u.hooks.preprocess(e):e).then(function(e){return p(e,u)}).then(function(e){return u.walkTokens?Promise.all(f.walkTokens(e,u.walkTokens)).then(function(){return e}):e}).then(function(e){return h(e,u)}).then(function(e){return u.hooks?u.hooks.postprocess(e):e}).catch(i);try{u.hooks&&(e=u.hooks.preprocess(e));var D=p(e,u),c=(u.walkTokens&&f.walkTokens(D,u.walkTokens),h(D,u));return c=u.hooks?u.hooks.postprocess(c):c}catch(e){return i(e)}}}function G(u,n,r){return function(e){var t;if(e.message+="\nPlease report this to https://github.com/markedjs/marked.",u)return t="<p>An error occurred:</p><pre>"+D(e.message+"",!0)+"</pre>",n?Promise.resolve(t):r?void r(null,t):t;if(n)return Promise.reject(e);if(!r)throw e;r(e)}}var R=new d(r.defaults);function I(e,t,u){return R.parse(e,t,u)}I.options=I.setOptions=function(e){return R.setOptions(e),n(I.defaults=R.defaults),I},I.getDefaults=u,I.defaults=r.defaults,I.use=function(){return R.use.apply(R,arguments),n(I.defaults=R.defaults),I},I.walkTokens=function(e,t){return R.walkTokens(e,t)},I.parseInline=R.parseInline,I.Parser=z,I.parser=z.parse,I.Renderer=y,I.TextRenderer=v,I.Lexer=B,I.lexer=B.lex,I.Tokenizer=E,I.Slugger=_,I.Hooks=$;var V=(I.parse=I).options,J=I.setOptions,K=I.use,W=I.walkTokens,Y=I.parseInline,ee=I,te=z.parse,ue=B.lex;r.Hooks=$,r.Lexer=B,r.Marked=d,r.Parser=z,r.Renderer=y,r.Slugger=_,r.TextRenderer=v,r.Tokenizer=E,r.getDefaults=u,r.lexer=ue,r.marked=I,r.options=V,r.parse=ee,r.parseInline=Y,r.parser=te,r.setOptions=J,r.use=K,r.walkTokens=W});
    • reports.json 2 B
      []
  • tools
    • dr_check.py 21.2 KB
      #!/usr/bin/env python3
      import json
      import os
      import re
      
      from lang_config import get_lang_config
      
      
      # ── Profile loader ────────────────────────────────────────────────────────
      
      _PROFILES_CACHE = None
      
      
      def load_profile(mode: str) -> dict:
          global _PROFILES_CACHE
          if _PROFILES_CACHE is None:
              base = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
              path = os.path.join(base, 'profiles.json')
              with open(path, 'r', encoding='utf-8-sig') as f:
                  _PROFILES_CACHE = json.load(f)
          return _PROFILES_CACHE.get(mode, _PROFILES_CACHE.get('quick', {}))
      
      
      # ── Mojibake & Encoding ──────────────────────────────────────────────────
      
      MOJIBAKE_PATTERNS = [
          '\ufffd',
          '涓枃', '绯荤粺', '鍦ㄧ嚎',
          'ç³»', 'å·²',
      ]
      
      
      def check_encoding(filepath: str) -> dict:
          issues = []
          with open(filepath, 'rb') as f:
              raw = f.read()
          if raw[:3] == b'\xef\xbb\xbf':
              issues.append("BOM detected at byte 0-2: EF BB BF")
              return {"passed": False, "issues": issues}
          try:
              text = raw.decode('utf-8')
          except UnicodeDecodeError as e:
              return {"passed": False, "issues": [f"Invalid UTF-8: {e}"]}
          for i, ch in enumerate(text):
              if ch == '\ufffd':
                  line = text[:i].count('\n') + 1
                  issues.append(f"Replacement character U+FFFD at line {line}")
                  break
          for pattern in MOJIBAKE_PATTERNS[1:]:
              if pattern in text:
                  lines = [i + 1 for i, line in enumerate(text.split('\n')) if pattern in line]
                  issues.append(f"Mojibake pattern '{pattern}' at lines {lines[:3]}")
          qmark_lines = [i + 1 for i, line in enumerate(text.split('\n')) if re.search(r'\?{3,}', line)]
          if qmark_lines:
              issues.append(f"CP936 question-mark corruption at lines {qmark_lines[:5]}")
          return {"passed": len(issues) == 0, "issues": issues}
      
      
      # ── Word Count ────────────────────────────────────────────────────────────
      
      def _clean_text(filepath: str) -> str:
          with open(filepath, 'r', encoding='utf-8-sig') as f:
              text = f.read()
          text = re.sub(r'^#{1,6}\s+', '', text, flags=re.MULTILINE)
          text = re.sub(r'^>\s?', '', text, flags=re.MULTILINE)
          text = re.sub(r'^[-*+]\s+', '', text, flags=re.MULTILINE)
          text = re.sub(r'^\d+\.\s+', '', text, flags=re.MULTILINE)
          text = text.replace('|', '')
          text = text.replace('`', '')
          text = re.sub(r'\*\*(.+?)\*\*', r'\1', text)
          text = re.sub(r'__(.+?)__', r'\1', text)
          text = re.sub(r'\*(.+?)\*', r'\1', text)
          text = re.sub(r'_(.+?)_', r'\1', text)
          text = re.sub(r'\[(.+?)\]\(.+?\)', r'\1', text)
          text = re.sub(r'!\[(.+?)\]\(.+?\)', r'\1', text)
          return text
      
      
      def word_count(filepath: str) -> int:
          cleaned = re.sub(r'\s+', '', _clean_text(filepath))
          return len(cleaned)
      
      
      # ── JSON Validation ───────────────────────────────────────────────────────
      
      
      def json_validate(filepath: str) -> dict:
          try:
              with open(filepath, 'r', encoding='utf-8-sig') as f:
                  json.load(f)
              return {"passed": True, "issues": []}
          except (json.JSONDecodeError, UnicodeDecodeError) as e:
              return {"passed": False, "issues": [str(e)]}
      
      
      # ── Header Format Checks ──────────────────────────────────────────────────
      
      
      def check_headers(filepath: str, lang: str = "zh") -> dict:
          issues = []
          with open(filepath, 'r', encoding='utf-8-sig') as f:
              lines = f.readlines()
          cfg = get_lang_config(lang)
          for i, line in enumerate(lines):
              stripped = line.rstrip('\n\r')
              if cfg['check']['ch_has_number_check']:
                  # non-zh: ## headers SHOULD start with number
                  if re.match(r'^## (?!Table of Contents|目录|目次|목차|Índice|Table des matières|Inhaltsverzeichnis|Indice|Inhoudsopgave|Innehållsförteckning|Содержание|المحتويات|विषय सूची|Mục lục|Daftar Isi|สารบัญ|İçindekiler|Spis treści|References|参考来源|参照元|Referencias|Réf|Quellen|Fonti|Bronnen|Källor|Источники|المصادر|स्रोत|Nguồn|Kaynaklar|Źródła|Disclaimer|免责|Descargo|Avertissement|Haftungsausschluss|Isenção|Dichiarazione|Vrijwaring|Ansvarsfriskrivning|Отказ|إخلاء|अस्वीकरण|Tuyên bố|Penyangkalan|ข้อจำกัด|Sorumluluk|Zrzeczenie)', stripped) and not re.match(r'^## \d+\.', stripped):
                      issues.append(f"Line {i + 1}: ## header should start with number: '{stripped[:60]}'")
              else:
                  # zh: ## headers should NOT contain arabic numeral
                  if re.match(r'^## .*[0-9]\.', stripped):
                      issues.append(f"Line {i + 1}: ## header should not contain number: '{stripped[:60]}'")
              if re.match(r'^### [一二三四五六七八九十]', stripped):
                  issues.append(f"Line {i + 1}: ### header uses Chinese numeral: '{stripped[:60]}'")
          return {"passed": len(issues) == 0, "issues": issues}
      
      
      # ── Chapter Number Compliance ─────────────────────────────────────────────
      
      
      def check_chapter_numbers(filepath: str, lang: str = "zh") -> dict:
          with open(filepath, 'r', encoding='utf-8-sig') as f:
              lines = f.readlines()
          cfg = get_lang_config(lang)
          pattern = re.compile(cfg['check']['ch_number_pattern'])
          hits = [i + 1 for i, line in enumerate(lines) if pattern.match(line.rstrip('\n\r'))]
          return {"passed": len(hits) >= 1, "chapter_lines": hits, "count": len(hits)}
      
      
      # ── Metadata Check ────────────────────────────────────────────────────────
      
      
      def check_metadata(filepath: str, lang: str = "zh") -> dict:
          with open(filepath, 'r', encoding='utf-8-sig') as f:
              content = f.read()
          cfg = get_lang_config(lang)
          issues = []
          meta_match = re.search(cfg['check']['metadata_pattern'], content)
          if not meta_match:
              return {"passed": False, "issues": [f"Metadata line '{cfg['metadata_label']}' not found"]}
          meta_line = content[meta_match.end():].split('\n')[0]
          for field in cfg['metadata_fields']:
              if field not in meta_line:
                  issues.append(f"Metadata field '{field}' missing")
          if not re.search(cfg['check']['references_pattern'], content):
              issues.append(f"Reference source line '{cfg['references_label']}' not found")
          return {"passed": len(issues) == 0, "issues": issues}
      
      
      # ── TOC Check ─────────────────────────────────────────────────────────────
      
      
      def check_toc(filepath: str, expected: int = None, lang: str = "zh") -> dict:
          with open(filepath, 'r', encoding='utf-8-sig') as f:
              lines = f.readlines()
          issues = []
          cfg = get_lang_config(lang)
          toc_heading = cfg['toc_heading']
          toc_headings = [i for i, line in enumerate(lines) if line.strip() == toc_heading]
          if len(toc_headings) == 0:
              return {"passed": False, "issues": [f"'{toc_heading}' heading not found"], "count": 0}
          elif len(toc_headings) > 1:
              issues.append(f"'{toc_heading}' appears {len(toc_headings)} times (should be 1)")
          toc_start = toc_headings[0]
          toc_entries = 0
          for line in lines[toc_start + 1:]:
              stripped = line.strip()
              if stripped.startswith('## '):
                  break
              if stripped.startswith('- ['):
                  toc_entries += 1
          if expected is not None and toc_entries != expected:
              issues.append(f"TOC has {toc_entries} entries, expected {expected}")
          return {"passed": len(issues) == 0, "issues": issues, "count": toc_entries}
      
      
      # ── Tail Check ────────────────────────────────────────────────────────────
      
      
      def check_tail(filepath: str, lang: str = "zh") -> dict:
          with open(filepath, 'r', encoding='utf-8-sig') as f:
              content = f.read()
          cfg = get_lang_config(lang)
          issues = []
          refs_title = cfg['check']['tail_refs']
          disc_title = cfg['check']['tail_disclaimer']
          conf_title = cfg['check'].get('tail_confidence')
      
          accepted_refs = [refs_title, "## 数据来源"]
          for title in accepted_refs:
              if title in content:
                  break
          else:
              issues.append(f"Tail section '{refs_title}' not found")
          if disc_title not in content:
              issues.append(f"'{disc_title}' not found")
          if conf_title:
              if conf_title not in content:
                  issues.append(f"Confidence section '{conf_title}' not found")
              else:
                  # Verify data type row exists within confidence section
                  start = content.index(conf_title) + len(conf_title)
                  after = content[start:]
                  next_heading = after.find('\n## ')
                  block = after[:next_heading] if next_heading != -1 else after
                  has_data_type_pct = bool(re.search(r'\w+\s*\d+[条]?\(\d+%\)', block))
                  if not has_data_type_pct:
                      issues.append(f"Confidence section missing data-type row (e.g. '**数据类型**' / '**Data Type**')")
          return {"passed": len(issues) == 0, "issues": issues}
      
      
      # ── Year Density ──────────────────────────────────────────────────────────
      
      
      def year_density(filepath: str, target_year: int) -> dict:
          with open(filepath, 'r', encoding='utf-8-sig') as f:
              content = f.read()
          years = re.findall(r'(?:19|20|21)\d{2}', content)
          if not years:
              return {"passed": False, "issues": ["No year data found"], "density": 0, "total": 0}
          total = len(years)
          target_count = sum(1 for y in years if int(y) in (target_year, target_year - 1))
          density = target_count / total
          return {
              "passed": density >= 0.5,
              "density": round(density, 3),
              "target_count": target_count,
              "total": total,
              "issues": [] if density >= 0.5 else [f"Year density {density:.1%} < 50% (target={target_year})"],
          }
      
      
      # ── Data Pool Check ───────────────────────────────────────────────────────
      
      
      def check_datapool(filepath: str, mode: str) -> dict:
          with open(filepath, 'r', encoding='utf-8-sig') as f:
              try:
                  data = json.load(f)
              except json.JSONDecodeError as e:
                  return {"passed": False, "issues": [f"Invalid JSON: {e}"]}
          issues = []
          required_fields = {'question': str, 'src': list, 'facts': list}
          records = data if isinstance(data, list) else [data]
          for i, rec in enumerate(records):
              for field, ftype in required_fields.items():
                  if field not in rec:
                      issues.append(f"Record {i}: missing field '{field}'")
                  elif not isinstance(rec[field], ftype):
                      issues.append(f"Record {i}: '{field}' should be {ftype.__name__}")
              facts = rec.get('facts') or []
              if len(facts) < 1:
                  issues.append(f"Record {i}: no facts")
              priority = rec.get('priority', 'medium')
              if priority == 'high' and len(facts) < 2:
                  issues.append(f"Record {i}: priority=high but only {len(facts)} fact(s)")
              fact_required = ['src', 'yr', 'met', 'val', 'u', 'ctx']
              for j, fact in enumerate(facts):
                  for field in fact_required:
                      if field not in fact:
                          issues.append(f"Record {i} fact {j}: missing '{field}'")
                  url_val = fact.get('url')
                  if not url_val or not str(url_val).strip():
                      issues.append(f"Record {i} fact {j}: 'url' is empty or null")
                  if mode == 'quick':
                      if 'cur' in fact:
                          issues.append(f"Record {i} fact {j}: quick mode should not have 'cur'")
                  else:
                      if 'cur' not in fact:
                          issues.append(f"Record {i} fact {j}: {mode} mode should have 'cur'")
                      if 'conf' not in fact:
                          issues.append(f"Record {i} fact {j}: {mode} mode should have 'conf'")
          for i, rec in enumerate(records):
              for j, fact in enumerate(rec.get('facts') or []):
                  for field in ('src', 'title', 'ctx'):
                      val = str(fact.get(field, ''))
                      if re.search(r'\?{3,}', val) or '\ufffd' in val:
                          issues.append(f"Record {i} fact {j}: mojibake in '{field}'")
          all_srcs = set()
          total_facts = 0
          for rec in records:
              for s in rec.get('src', []):
                  all_srcs.add(s)
              total_facts += len(rec.get('facts', []))
          return {
              "passed": len(issues) == 0,
              "issues": issues,
              "record_count": len(records),
              "source_count": len(all_srcs),
              "fact_count": total_facts,
          }
      
      
      # ── Chapter Validation (single-command for sub-agents) ─────────────────
      
      
      def validate_chapter(filepath: str, expected_sections: int = 0) -> dict:
          results = {}
          enc = check_encoding(filepath)
          results['encoding'] = enc['passed']
          hdr = check_headers(filepath)
          results['headers'] = hdr['passed']
          try:
              wc = word_count(filepath)
          except Exception:
              wc = 0
          results['word_count'] = wc
          with open(filepath, 'r', encoding='utf-8-sig') as f:
              lines = [l.strip() for l in f.readlines() if l.strip()]
          check_lines = lines[1:] if lines and lines[0].startswith('#') else lines
          has_bq = len(check_lines) > 0 and check_lines[0].startswith('>')
          results['has_blockquote'] = has_bq
          para_count = 0
          in_table = False
          for line in lines:
              if line.startswith('|') and line.endswith('|'):
                  in_table = True
                  continue
              if in_table and not (line.startswith('|') and line.endswith('|')):
                  in_table = False
              if in_table:
                  continue
              if line.startswith('#'):
                  continue
              if line.startswith('>'):
                  continue
              if line.startswith('|---'):
                  continue
              if line.startswith('|'):
                  continue
              para_count += 1
          results['paragraphs'] = para_count
          table_count = sum(1 for l in lines if l.startswith('|---'))
          results['tables'] = table_count
          section_headers = [l.lstrip('#').strip() for l in lines if l.startswith('###')]
          results['sections'] = section_headers
          results['section_count'] = len(section_headers)
          results['sections_ok'] = expected_sections == 0 or len(section_headers) == expected_sections
          checks = [results['encoding'], results['headers'], results['has_blockquote'],
                    results['sections_ok']]
          results['passed'] = all(checks)
          return results
      
      
      # ── Batch Chapter Validation (Parallel) ───────────────────────────────────
      
      
      def validate_all_chapters(chapters_dir: str, chapter_count: int, expected_sections: int = 0) -> dict:
          from concurrent.futures import ThreadPoolExecutor, as_completed
          results = {}
          with ThreadPoolExecutor(max_workers=min(chapter_count, 8)) as ex:
              futures = {}
              for i in range(1, chapter_count + 1):
                  path = os.path.join(chapters_dir, f"chapter-{i}.md")
                  if not os.path.exists(path):
                      results[i] = {"passed": False, "error": f"chapter-{i}.md not found"}
                      continue
                  futures[ex.submit(validate_chapter, path, expected_sections)] = i
              for fut in as_completed(futures):
                  chapter_num = futures[fut]
                  try:
                      result = fut.result()
                      results[chapter_num] = result
                  except Exception as e:
                      results[chapter_num] = {"passed": False, "error": str(e)}
          sorted_results = {k: results[k] for k in sorted(results.keys())}
          failed_chapters = {str(num): r for num, r in sorted_results.items()
                             if not r.get('passed', False)}
          return {
              "passed": len(failed_chapters) == 0,
              "total": chapter_count,
              "passed_count": chapter_count - len(failed_chapters),
              "failed_count": len(failed_chapters),
              "results": sorted_results,
              "failed_chapters": failed_chapters,
          }
      
      
      # ── Full QA Report ────────────────────────────────────────────────────────
      
      
      def _run_checks_concurrent(filepath: str, target_year: int, lang: str = "zh") -> dict:
          from concurrent.futures import ThreadPoolExecutor, as_completed
      
          def _wc(p):
              wc = word_count(p)
              return {'word_count': wc}
      
          def _toc(p, expected):
              return {'toc': check_toc(p, expected=expected, lang=lang)}
      
          results = {}
          with ThreadPoolExecutor(max_workers=6) as ex:
              futures = {
                  ex.submit(check_encoding, filepath): 'encoding',
                  ex.submit(_wc, filepath): 'word_count_raw',
                  ex.submit(check_headers, filepath, lang): 'headers',
                  ex.submit(check_chapter_numbers, filepath, lang): 'chapter_numbers',
                  ex.submit(check_metadata, filepath, lang): 'metadata',
                  ex.submit(check_tail, filepath, lang): 'tail',
                  ex.submit(year_density, filepath, target_year): 'year_density',
              }
              for fut in as_completed(futures):
                  key = futures[fut]
                  try:
                      result = fut.result()
                      if key == 'word_count_raw':
                          results['word_count_raw'] = result['word_count']
                      else:
                          results[key] = result
                  except Exception as e:
                      results[key] = {"passed": False, "issues": [f"Check error: {e}"]}
          expected = results.get('chapter_numbers', {}).get('count', 0)
          results['toc'] = check_toc(filepath, expected=expected, lang=lang)
          return results
      
      
      def qa_report(filepath: str, mode: str, target_year: int, lang: str = "zh") -> dict:
          raw = _run_checks_concurrent(filepath, target_year, lang)
          results = {}
          results['encoding'] = raw.get('encoding', {"passed": False, "issues": ["missing"]})
          results['headers'] = raw.get('headers', {"passed": False, "issues": ["missing"]})
          results['chapter_numbers'] = raw.get('chapter_numbers', {"passed": False, "issues": ["missing"]})
          results['metadata'] = raw.get('metadata', {"passed": False, "issues": ["missing"]})
          results['toc'] = raw.get('toc', {"passed": False, "issues": ["missing"]})
          results['tail'] = raw.get('tail', {"passed": False, "issues": ["missing"]})
          results['year_density'] = raw.get('year_density', {"passed": False, "issues": ["missing"]})
          wc = raw.get('word_count_raw', 0)
          prof = load_profile(mode)
          limit = prof.get('max_chars', 3000)
          results['word_count'] = {"passed": True, "count": wc, "limit": limit, "exceeded": wc > limit,
                                    "issues": [] if wc <= limit else [f"{wc} > {limit} limit (informational)"]}
          all_passed = all(r.get('passed', False) for r in results.values())
          failures = {name: r.get('issues', []) for name, r in results.items()
                      if not r.get('passed', False) and r.get('issues')}
          return {
              "passed": all_passed,
              "file": filepath,
              "mode": mode,
              "target_year": target_year,
              "checks": results,
              "failures": failures,
          }
      
      
      # ── Chapter Depth Balance ────────────────────────────────────────────────
      
      
      def check_depth_balance(chapters_dir: str, chapter_count: int,
                              threshold: float = 0.5) -> dict:
          """Check if any chapter is significantly shorter than average.
          threshold=0.5 means flag if a chapter < 50% of average line count.
          Language-agnostic — pure line count comparison."""
          issues = []
          lengths = []
          for i in range(1, chapter_count + 1):
              path = os.path.join(chapters_dir, f"chapter-{i}.md")
              try:
                  with open(path, 'r', encoding='utf-8-sig') as f:
                      lines = f.readlines()
                  lengths.append((i, len(lines)))
              except FileNotFoundError:
                  lengths.append((i, 0))
                  issues.append(f"Chapter {i}: file not found")
      
          if not lengths:
              return {"passed": False, "issues": ["No chapter files found"]}
      
          avg = sum(n for _, n in lengths) / len(lengths)
          thin = [(i, n) for i, n in lengths if n < avg * threshold and n > 0]
          for i, n in thin:
              issues.append(
                  f"Chapter {i}: {n} lines ({n/avg:.0%} of avg {avg:.0f} lines) "
                  f"— below {threshold:.0%} threshold"
              )
      
          return {"passed": len(issues) == 0, "issues": issues,
                  "chapters": [{"num": i, "lines": n} for i, n in lengths],
                  "average": round(avg, 1)}
      
    • dr_gen.py 36.8 KB
      #!/usr/bin/env python3
      import json
      import locale
      import os
      import re
      import sys
      
      from dr_check import check_encoding, load_profile
      from lang_config import get_lang_config, CHINESE_NUMERALS, LANG_CONFIG
      
      
      # ── Source Extraction ─────────────────────────────────────────────────────
      
      SOURCE_PATTERN = re.compile(r'[((]([^))]+?)[,,]\s*(\d{4})[))]')
      
      
      def extract_sources(filepath: str) -> dict:
          with open(filepath, 'r', encoding='utf-8-sig') as f:
              content = f.read()
          matches = SOURCE_PATTERN.findall(content)
          seen = set()
          unique = []
          for inst, year in matches:
              key = inst.strip()
              if key and key not in seen:
                  seen.add(key)
                  unique.append(key)
          return {
              "source_count": len(unique),
              "sources": unique,
              "sources_joined": "、".join(sorted(unique)),
              "total_mentions": len(matches),
          }
      
      
      # ── TOC Generation ─────────────────────────────────────────────────────────
      
      
      def _github_anchor(text: str) -> str:
          """Generate a GitHub-compatible heading anchor.
      
          Mirrors cmark-gfm + utf8proc: keep only Unicode letters (L), digits (N),
          spaces (Z), underscore (_), and hyphen (-). Drop all punctuation and symbols.
          """
          import unicodedata
          text = text.lower()
          result = []
          for ch in text:
              cat = unicodedata.category(ch)
              if cat.startswith(('L', 'N')) or ch in (' ', '_', '-'):
                  result.append(ch)
          text = ''.join(result)
          text = re.sub(r'\s+', '-', text)
          text = re.sub(r'-+', '-', text)
          text = text.strip('-')
          return text
      
      
      def generate_toc(outline_path: str) -> dict:
          with open(outline_path, 'r', encoding='utf-8-sig') as f:
              outline = json.load(f)
          chapters = outline.get('chapters', [])
          lang = outline.get('language', 'zh')
          cfg = get_lang_config(lang)
          lines = []
          for i, ch in enumerate(chapters):
              prefix = cfg['toc_prefix'](i + 1)
              title = ch.get('title', '')
              label = f"{prefix} {title}" if lang != 'zh' else f"{prefix}{title}"
              anchor = _github_anchor(label)
              lines.append(f"- [{label}](#{anchor})")
          return {
              "chapter_count": len(chapters),
              "toc_lines": lines,
              "toc_text": '\n'.join(lines),
          }
      
      
      # ── Metadata Block Generation ─────────────────────────────────────────────
      
      
      def generate_metadata(word_count: int, reading_time: int, data_until: str,
                             generate_time: str, depth_mode: str,
                             source_count: int, top_sources: list,
                             skill_version: str = "",
                             lang: str = "zh") -> dict:
          cfg = get_lang_config(lang)
          fields = cfg['metadata_fields']
          s = cfg['sep']
          fs = cfg['field_sep']
          version_str = f"{s}{fields[5]}{fs}{skill_version}" if skill_version else ""
          line1 = (
              f"> {cfg['metadata_label']}"
              f"{fields[0]}{fs}{word_count}"
              f"{s}{fields[1]}{fs}{reading_time} {cfg['minute_unit']}"
              f"{s}{fields[2]}{fs}{data_until}"
              f"{s}{fields[3]}{fs}{generate_time}"
              f"{s}{fields[4]}{fs}{depth_mode}{version_str}"
          )
          sorted_sources = sorted(top_sources)[:8]
          src_fmt = cfg['refs_count_format']
          count_text = src_fmt(source_count) if callable(src_fmt) else src_fmt.format(count=source_count)
          if lang == 'zh':
              source_str = "、".join(sorted_sources)
              line2 = f"> {cfg['references_label']}{source_str} 等 · {count_text}"
          else:
              source_str = ", ".join(sorted_sources)
              line2 = f"> {cfg['references_label']}{source_str} et al. · {count_text}"
          return {
              "metadata_line": line1,
              "source_line": line2,
              "full_block": line1 + '\n>\n' + line2,
          }
      
      
      # ── Chapter Mapping ───────────────────────────────────────────────────────
      
      
      def map_chapters(outline_path: str) -> dict:
          with open(outline_path, 'r', encoding='utf-8-sig') as f:
              outline = json.load(f)
          chapters = outline.get('chapters', [])
          mapping = {}
          for i, ch in enumerate(chapters):
              sqs = ch.get('sub_questions', [])
              chapter_num = i + 1
              mapping[chapter_num] = {
                  "title": ch.get('title', ''),
                  "sections": ch.get('sections', []),
                  "sub_questions": [
                      {"question": sq.get('question', ''), "priority": sq.get('priority', 'medium')}
                      for sq in sqs
                  ],
                  "sub_question_count": len(sqs),
              }
          return {"chapter_count": len(chapters), "mapping": mapping, "chapter_numbers": list(mapping.keys())}
      
      
      # ── Hyperlinked Reference List ────────────────────────────────────────────
      
      
      def generate_refs(datapool_path: str, numbered: bool = False, lang: str = "zh") -> dict:
          data = _read_json_handle_bom(datapool_path)
          records = data if isinstance(data, list) else [data]
          cfg = get_lang_config(lang)
          seen_keys = set()
          entries = []
          for rec in records:
              for fact in rec.get('facts') or []:
                  url = (fact.get('url') or '').strip()
                  inst = fact.get('src', '').strip()
                  yr = fact.get('yr', '')
                  title = fact.get('title', '').strip()
                  if not url or not inst:
                      continue
                  key = (inst, yr, url)
                  if key not in seen_keys:
                      seen_keys.add(key)
                      entries.append((inst, yr, title or inst, url))
          entries.sort(key=lambda x: (x[0].lower(), x[2]))
      
          src_fmt = cfg['refs_count_format']
          count_text = src_fmt(len(entries)) if callable(src_fmt) else src_fmt.format(count=len(entries))
          lines = [f"{cfg['refs_prefix']}\n", f"{count_text}\n"]
          if numbered:
              for i, (inst, yr, title, url) in enumerate(entries, 1):
                  label = f"{title} · {inst}" + (f" · {yr}" if yr else "")
                  lines.append(f"({i}) [{label}]({url})")
              ref_text = '\n'.join(lines[:2]) + '\n\n' + '\n\n'.join(lines[2:])
          else:
              for inst, yr, title, url in entries:
                  label = f"{title} · {inst}" + (f" · {yr}" if yr else "")
                  lines.append(f"- [{label}]({url})")
              ref_text = '\n'.join(lines)
          return {"source_count": len(entries), "ref_lines": lines, "ref_text": ref_text}
      
      
      # ── Convert Citations to Numeric Index ────────────────────────────────────
      
      CITATION_RE = re.compile(r'[((]([^))]+?)[,,]\s*(\d{4})[))]')
      
      _UTF8_SIG = 'utf-8-sig'
      
      
      def _read_json_handle_bom(path: str):
          with open(path, 'r', encoding=_UTF8_SIG) as f:
              return json.load(f)
      
      
      def convert_citations(report_path: str, datapool_path: str, output_path: str = None, lang: str = "zh") -> dict:
          with open(report_path, 'r', encoding='utf-8-sig') as f:
              content = f.read()
      
          cfg = get_lang_config(lang)
      
          # Check: report should NOT contain (机构,年份) patterns
          legacy_cites = re.findall(r'[((][^))]+?[,,]\s*\d{4}[))]', content)
          if legacy_cites:
              issues = [f"Found {len(legacy_cites)} legacy (机构,年份) citations — chapter agents must use (N) format"]
              return {"passed": False, "issues": issues, "changes": 0}
      
          # Load data-pool and build ref_map from first-appearance order
          data = _read_json_handle_bom(datapool_path)
          records = data if isinstance(data, list) else [data]
      
          seen = set()
          ordered_refs = []
          pool_map = {}
          for rec in records:
              for fact in rec.get('facts') or []:
                  inst = fact.get('src', '').strip()
                  yr = fact.get('yr', '')
                  url = fact.get('url', '').strip()
                  title = fact.get('title', '').strip()
                  if not inst or not yr:
                      continue
                  key = (inst, yr, url)
                  if key not in seen:
                      seen.add(key)
                      ordered_refs.append(key)
                  if url:
                      pool_map[key] = (title or inst, url)
      
          ref_map = {pair: i + 1 for i, pair in enumerate(ordered_refs)}
      
          # Scan body for [N] patterns
          split_marker = cfg['refs_prefix']
          body = content.split(split_marker)[0] if split_marker in content else content
          body_refs = set(re.findall(r'(?<!!)\[(\d+)\](?!\()', body))
      
          issues = []
          for num in sorted(body_refs, key=int):
              num_i = int(num)
              if num_i < 1 or num_i > len(ref_map):
                  issues.append(f"Body references ({num}) which has no data-pool entry")
      
          # Build reference section
          entry_lines = []
          for key, num in sorted(ref_map.items(), key=lambda x: x[1]):
              inst, yr, url = key
              title, _ = pool_map.get(key, (inst, ''))
              label = title if title == inst else f"{title} · {inst}"
              label = label + (f" · {yr}" if yr else "")
              anchor = f'<a id="ref{num}"></a>'
              if url:
                  entry_lines.append(f'{anchor}({num}) [{label}]({url})')
              else:
                  entry_lines.append(f'{anchor}({num}) {label}')
      
          ref_text = f'\n\n{cfg["refs_prefix"]}\n\n\n' + '\n\n'.join(entry_lines)
      
          # Insert/replace refs section
          old_section = re.search(rf'{cfg["refs_prefix"]}.*?(?=\n## |\Z)', content, re.DOTALL)
          if old_section:
              new_content = content[:old_section.start()] + ref_text + content[old_section.end():]
          else:
              new_content = content + ref_text
      
          # Validate: every [N] in body has matching ref anchor
          ref_anchors = set(re.findall(r'<a id="ref(\d+)"></a>', new_content))
          missing_in_refs = body_refs - ref_anchors
          orphan_anchors = ref_anchors - body_refs
          if missing_in_refs:
              issues.append(
                  f"Citations without matching reference: [{', '.join(sorted(missing_in_refs, key=int))}]")
      
          # Convert [N] → [(N)](#refN)
          BODY_CITE_RE = re.compile(r'(?<!!)\[(\d+)\](?!\()')
          new_content = BODY_CITE_RE.sub(r'[(\1)](#ref\1)', new_content)
      
          # Clean up structural headings from other languages
          foreign_headings = set()
          for code, lcfg in LANG_CONFIG.items():
              if code == lang:
                  continue
              foreign_headings.add(lcfg['refs_prefix'])
              foreign_headings.add(lcfg['disclaimer_title'])
              foreign_headings.add(lcfg['toc_heading'])
          for heading in foreign_headings:
              pattern = re.compile(
                  rf'^{re.escape(heading)}\s*$.*?(?=\n## |\Z)',
                  re.MULTILINE | re.DOTALL
              )
              new_content = pattern.sub('', new_content)
      
          # Write output
          output = output_path or report_path
          tmp = output + '.tmp'
          with open(tmp, 'w', encoding='utf-8', newline='\n') as f:
              f.write(new_content)
          os.replace(tmp, output)
      
          return {
              "passed": len(issues) == 0,
              "changes": len(ref_map),
              "output": output,
              "issues": issues,
              "citation_count": len(body_refs),
              "ref_count": len(ref_anchors),
          }
      
      
      # ── Encoding-safe stdin reader (cross-platform) ─────────────────────────────
      
      
      def _read_stdin() -> str:
          raw = sys.stdin.buffer.read()
          if not raw:
              return ''
          try:
              return raw.decode('utf-8')
          except UnicodeDecodeError:
              pass
          try:
              return raw.decode(locale.getpreferredencoding())
          except (UnicodeDecodeError, LookupError):
              pass
          return raw.decode('utf-8', errors='replace')
      
      
      # ── JSON Write (atomic, encoding-safe) ─────────────────────────────────────
      
      
      def write_json(filepath: str) -> dict:
          raw = _read_stdin()
          if not raw.strip():
              return {"passed": False, "issues": ["Empty input from stdin"]}
          try:
              data = json.loads(raw)
          except json.JSONDecodeError as e:
              return {"passed": False, "issues": [f"JSON parse error: {e}"]}
          tmp = filepath + '.tmp'
          try:
              with open(tmp, 'w', encoding='utf-8', newline='\n') as f:
                  json.dump(data, f, ensure_ascii=False, indent=2)
                  f.write('\n')
              os.replace(tmp, filepath)
          except (OSError, IOError) as e:
              return {"passed": False, "issues": [f"Write failed: {e}"]}
          return {"passed": True, "issues": [], "size": os.path.getsize(filepath)}
      
      
      # ── Markdown Write (encoding-safe, Mojibake-free) ─────────────────────────
      
      
      def write_md(filepath: str) -> dict:
          raw = _read_stdin()
          if not raw.strip():
              return {"passed": False, "issues": ["Empty input from stdin"]}
          tmp = filepath + '.tmp'
          try:
              with open(tmp, 'w', encoding='utf-8', newline='\n') as f:
                  f.write(raw)
              os.replace(tmp, filepath)
          except (OSError, IOError) as e:
              return {"passed": False, "issues": [f"Write failed: {e}"]}
          enc_check = check_encoding(filepath)
          if not enc_check['passed']:
              return {"passed": False, "issues": [f"Write succeeded but Mojibake detected: {enc_check['issues']}"]}
          return {"passed": True, "issues": [], "size": os.path.getsize(filepath)}
      
      
      # ── Prepare Chapter Skeleton ──────────────────────────────────────────────
      
      
      def prepare_chapter(outline_path: str, datapool_path: str,
                          chapter_num: int, total_chapters: int, mode: str) -> dict:
          with open(outline_path, 'r', encoding='utf-8-sig') as f:
              outline = json.load(f)
          with open(datapool_path, 'r', encoding='utf-8-sig') as f:
              data = json.load(f)
      
          chapters = outline.get('chapters', [])
          lang = outline.get('language', 'zh')
          cfg = get_lang_config(lang)
      
          if chapter_num < 1 or chapter_num > len(chapters):
              return {"passed": False, "issues": [f"Chapter {chapter_num} out of range (1-{len(chapters)})"]}
      
          ch = chapters[chapter_num - 1]
          title = ch.get('title', '')
          sections = ch.get('sections', [])
          sub_questions = ch.get('sub_questions', [])
      
          prof = load_profile(mode)
          total_limit = prof.get('max_chars', 3000)
          per_chapter_target = total_limit // max(total_chapters, 1)
      
          pool_records = data if isinstance(data, list) else [data]
          sq_questions = [sq.get('question', '') for sq in sub_questions]
      
          relevant_facts = []
          for rec in pool_records:
              rec_q = rec.get('question', '')
              match_score = sum(1 for sq in sq_questions if any(
                  kw.lower() in rec_q.lower() for kw in sq.split() if len(kw) > 2
              ))
              if match_score > 0:
                  facts = rec.get('facts') or []
                  for fact in facts:
                      relevant_facts.append({
                          "source": fact.get('src', ''),
                          "year": fact.get('yr', ''),
                          "metric": fact.get('met', ''),
                          "value": fact.get('val'),
                          "unit": fact.get('u', ''),
                          "context": fact.get('ctx', ''),
                      })
      
          relevant_facts.sort(key=lambda x: x.get('year', ''), reverse=True)
      
          lines = []
          prefix_str = cfg['chapter_heading'](chapter_num, title)
          lines.append(f"# {title}")
          lines.append(f"\n> " + ("Core judgment for this chapter." if lang != 'zh' else "本章核心判断。") + "\n")
          per_section_est = per_chapter_target // max(len(sections), 1)
          char_note = "chars" if lang != 'zh' else "字"
          lines.append(f"> **{'Word target' if lang != 'zh' else '字数参考'}**:{'chapter target' if lang != 'zh' else '本章目标'} ≈ {per_chapter_target} {char_note}({'per section' if lang != 'zh' else '每节'} ~{per_section_est} {char_note})| sections: {len(sections)} | {'pre-matched facts' if lang != 'zh' else '预匹配事实'}: {len(relevant_facts)}\n")
      
          for idx, section in enumerate(sections):
              sec_num = idx + 1
              lines.append(f"### {chapter_num}.{sec_num} {section}\n")
              section_facts = [f for f in relevant_facts if section in f.get('context', '') or sec_num <= 2]
              if not section_facts:
                  section_facts = relevant_facts[:2] if relevant_facts else []
                  if section_facts:
                      relevant_facts = relevant_facts[2:]
              for fact in section_facts[:3]:
                  val_str = f"{fact['value']}{fact['unit']}" if fact['value'] is not None else fact['context']
                  lines.append(f"- {fact['metric']}: {val_str}({fact['source']},{fact['year']})")
              lines.append("")
      
          skeleton = '\n'.join(lines)
          return {
              "passed": True,
              "skeleton": skeleton,
              "chapter_title": title,
              "fact_count": len(relevant_facts),
              "estimated_words": per_chapter_target,
          }
      
      
      # ── Assemble Final Report ─────────────────────────────────────────────────
      
      
      def assemble_report(outline_path: str, chapters_dir: str,
                          datapool_path: str,
                          mode: str, target_year: int,
                          wordcount_path: str = None,
                          output_path: str = None) -> dict:
          from dr_check import word_count as wc_func
          import datetime
          issues = []
      
          try:
              with open(outline_path, 'r', encoding='utf-8-sig') as f:
                  outline = json.load(f)
          except Exception as e:
              return {"passed": False, "issues": [f"Failed to read outline: {e}"]}
      
          title = outline.get('title', '报告')
          lang = outline.get('language', 'zh')
          cfg = get_lang_config(lang)
          now = datetime.datetime.now()
      
          # Sanitize title for filesystem: replace Windows-invalid chars with '-'
          # Invalid on Windows: < > : " / \ | ? *
          safe_title = re.sub(r'[<>:"/\\|?*]', '-', title)
          # Also trim trailing dots/spaces (Windows issue)
          safe_title = safe_title.rstrip('. ')
      
          if not output_path:
              output_path = f"reports/{safe_title}-{now.strftime('%Y%m%d-%H%M%S')}.md"
          elif os.path.isdir(output_path) or not output_path.endswith('.md'):
              base = os.path.join(output_path, f"{safe_title}-{now.strftime('%Y%m%d-%H%M%S')}.md")
              output_path = base
          chapters = outline.get('chapters', [])
          depth_mode = outline.get('depth_mode', mode)
      
          chapter_files = []
          for i in range(1, len(chapters) + 1):
              path = os.path.join(chapters_dir, f"chapter-{i}.md")
              if os.path.exists(path):
                  chapter_files.append((i, path))
              else:
                  issues.append(f"Missing chapter file: chapter-{i}.md")
      
          toc_result = generate_toc(outline_path)
          toc_text = toc_result['toc_text']
      
          chapter_texts = []
          for num, fpath in sorted(chapter_files):
              with open(fpath, 'r', encoding='utf-8-sig') as f:
                  content = f.read().strip()
              content = re.sub(r'^#{1,2} .+?\n+', '', content, count=1)
              heading = cfg['chapter_heading'](num, chapters[num - 1].get('title', ''))
              chapter_texts.append(f'{heading}\n\n{content}')
      
          data_until = f"{target_year}" if lang != 'zh' else f"{target_year}年"
          generate_time = now.strftime("%Y-%m-%d %H:%M:%S")
      
          try:
              refs = generate_refs(datapool_path, lang=lang)
              total_sources = refs['source_count']
              ref_text = refs['ref_text']
          except Exception as e:
              total_sources = 0
              ref_text = f"{cfg['refs_prefix']}\n\n{'No source data' if lang != 'zh' else '无来源数据'}\n"
              issues.append(f"Source extraction failed: {e}")
      
          try:
              with open(datapool_path, 'r', encoding='utf-8-sig') as f:
                  dp_data = json.load(f)
              records = dp_data if isinstance(dp_data, list) else [dp_data]
              source_freq = {}
              for rec in records:
                  for fact in rec.get('facts') or []:
                      src = fact.get('src', '')
                      if src:
                          source_freq[src] = source_freq.get(src, 0) + 1
              top_sources = sorted(source_freq, key=source_freq.get, reverse=True)[:8]
          except Exception:
              top_sources = []
      
          script_dir = os.path.dirname(os.path.abspath(__file__))
          version_path = os.path.join(script_dir, '..', 'VERSION')
          try:
              with open(version_path, 'r', encoding='utf-8-sig') as f:
                  version = f.read().strip()
          except Exception:
              version = ""
      
          temp_meta = generate_metadata(
              word_count=0, reading_time=1,
              data_until=data_until, generate_time=generate_time,
              depth_mode=depth_mode, source_count=total_sources,
              top_sources=top_sources, skill_version=version, lang=lang,
          )
          toc_heading = cfg['toc_heading']
          temp_parts = [
              f"# {title}\n",
              f"{temp_meta['full_block']}\n",
              toc_heading, "\n", toc_text, "\n",
          ]
          temp_parts.append('\n\n'.join(chapter_texts))
          temp_parts.append("\n\n---\n\n")
          temp_parts.append(ref_text)
          temp_parts.append(f"\n\n{cfg['disclaimer_title']}\n\n{cfg['disclaimer_text']}\n")
          temp_parts.append(f"\n{cfg['report_generated'].format(time=generate_time)}\n")
          full_report = '\n'.join(temp_parts)
      
          total_wc = wc_func(output_path) if os.path.exists(output_path) else 0
          if total_wc == 0:
              total_wc = len(re.sub(r'\s+', '', full_report))
          reading_time = max(1, round(total_wc / 800))
      
          meta = generate_metadata(
              word_count=total_wc, reading_time=reading_time,
              data_until=data_until, generate_time=generate_time,
              depth_mode=depth_mode, source_count=total_sources,
              top_sources=top_sources, skill_version=version, lang=lang,
          )
          report_parts = [
              f"# {title}\n",
              f"{meta['full_block']}\n",
              toc_heading, "\n", toc_text, "\n",
          ]
          report_parts.append('\n\n'.join(chapter_texts))
          report_parts.append("\n\n---\n\n")
          report_parts.append(ref_text)
          report_parts.append(f"\n\n{cfg['disclaimer_title']}\n\n{cfg['disclaimer_text']}\n")
          report_parts.append(f"\n{cfg['report_generated'].format(time=generate_time)}\n")
          promo = cfg.get('promo_line') or get_lang_config('en').get('promo_line', '')
          report_parts.append(f"{promo}\n")
          full_report = '\n'.join(report_parts)
      
          os.makedirs(os.path.dirname(output_path), exist_ok=True)
      
          # Auto-dedup: delete older report files with same title in the same directory
          out_dir = os.path.dirname(output_path)
          pattern = re.compile(r'^' + re.escape(safe_title) + r'-\d{8}-\d{6}\.md$')
          for fname in os.listdir(out_dir):
              if pattern.match(fname) and fname != os.path.basename(output_path):
                  old_path = os.path.join(out_dir, fname)
                  try:
                      os.remove(old_path)
                  except OSError:
                      pass
      
          tmp = output_path + '.tmp'
          try:
              with open(tmp, 'w', encoding='utf-8', newline='\n') as f:
                  f.write(full_report)
              os.replace(tmp, output_path)
          except Exception as e:
              return {"passed": False, "issues": [f"Write failed: {e}"]}
      
          enc_check = check_encoding(output_path)
          if not enc_check['passed']:
              issues.append(f"Encoding issue in assembled report: {enc_check['issues']}")
      
          line_count = full_report.count('\n') + 1
      
          return {
              "passed": len(issues) == 0,
              "output_path": output_path,
              "line_count": line_count,
              "chapter_count": len(chapter_files),
              "word_count": total_wc,
              "issues": issues,
          }
      
      
      # ── Confidence Assessment Section ────────────────────────────────────────
      
      AUTHORITY_KEYWORDS = [
          'gov', 'edu', 'org', 'university', 'institute', 'academy',
          '统计局', '科学院', '教育部', '发改委', '央行', '人民银行',
          'world bank', 'imf', 'oecd', 'who', 'united nations',
          'national bureau', 'ministry', 'commission', '研究中心', '研究院',
          '基金会', '基金', 'association',
          # Academic / journal signals
          'arxiv', 'doi.org', 'pnas', 'nature.com', 'science.org',
          'springer', 'iopscience', 'ieee', 'nasa', 'esa',
          'un.org', 'who.int', 'worldbank',
      ]
      
      MEDIA_KEYWORDS = [
          'news', '媒体', '新闻', '报道', 'blog', '自媒体', '公众号',
          'times', 'post', 'herald', 'tribune', '记者', '日报', '晚报',
          '晨报', 'weekly', 'magazine', '广播', '电视台',
      ]
      
      
      def _classify_source(src: str) -> str:
          """Classify a source as 'authoritative', 'media', or 'industry'."""
          s = src.lower().strip()
          if not s:
              return 'industry'
          for kw in AUTHORITY_KEYWORDS:
              if kw in s:
                  return 'authoritative'
          for kw in MEDIA_KEYWORDS:
              if kw in s:
                  return 'media'
          return 'industry'
      
      
      def _source_text(fact: dict) -> str:
          parts = [fact.get('src', ''), fact.get('url', ''), fact.get('title', '')]
          return ' '.join(str(p) for p in parts if p).lower()
      
      
      def _infer_confidence(fact: dict) -> str:
          """Return high/medium/low; use explicit conf when present."""
          conf = (fact.get('conf') or '').strip().lower()
          if conf in ('high', 'medium', 'low'):
              return conf
          src_class = _classify_source(_source_text(fact))
          if src_class == 'authoritative':
              return 'high'
          if src_class == 'media':
              return 'medium'
          # arxiv / doi / peer-reviewed hints in URL or title
          text = _source_text(fact)
          if any(k in text for k in ('arxiv', 'doi.org', 'nature.com', 'science.org',
                                     'iopscience', 'springer', 'ieee', 'nasa', 'esa')):
              return 'high'
          return 'medium'
      
      
      def _infer_data_type(fact: dict) -> str:
          """Return actual/estimate/forecast; use explicit data_type when present."""
          dtype = (fact.get('data_type') or '').strip().lower()
          if dtype in ('actual', 'estimate', 'forecast'):
              return dtype
          text = ' '.join(str(fact.get(k, '')) for k in ('ctx', 'met', 'title')).lower()
          if any(k in text for k in ('预测', 'forecast', 'projected', 'projection', '预计到', '有望达到')):
              return 'forecast'
          if any(k in text for k in ('估算', '预估', '预计', 'estimate', '约', '大约', '左右')):
              return 'estimate'
          return 'actual'
      
      
      
      
      
      def generate_confidence_section(datapool_path: str, manifest_path: str,
                                      lang: str = 'zh') -> dict:
          """Generate the Confidence Assessment markdown section from data-pool + manifest.
      
          Returns dict with 'passed' (bool), 'section' (str, markdown), 'issues' (list).
          """
          issues = []
          try:
              with open(datapool_path, 'r', encoding='utf-8-sig') as f:
                  pool = json.load(f)
          except Exception as e:
              return {'passed': False, 'section': '',
                      'issues': [f"Failed to read data-pool: {e}"]}
      
          try:
              with open(manifest_path, 'r', encoding='utf-8-sig') as f:
                  manifest = json.load(f)
          except Exception as e:
              return {'passed': False, 'section': '',
                      'issues': [f"Failed to read manifest: {e}"]}
      
          records = pool if isinstance(pool, list) else [pool]
      
          # ── Aggregate stats ──
          total_facts = 0
          conf_counts = {'high': 0, 'medium': 0, 'low': 0}
          type_counts = {'actual': 0, 'estimate': 0, 'forecast': 0}
          src_class_counts = {'authoritative': 0, 'industry': 0, 'media': 0}
          controversies_total = 0
          explicit_conf = 0
          explicit_dtype = 0
      
          for rec in records:
              for fact in rec.get('facts') or []:
                  total_facts += 1
                  if (fact.get('conf') or '').strip().lower() in conf_counts:
                      explicit_conf += 1
                  if (fact.get('data_type') or '').strip().lower() in type_counts:
                      explicit_dtype += 1
                  c = _infer_confidence(fact)
                  conf_counts[c] += 1
                  t = _infer_data_type(fact)
                  type_counts[t] += 1
                  src_class_counts[_classify_source(_source_text(fact))] += 1
              controversies_total += len(rec.get('controversies') or [])
      
          quick_mode = total_facts > 0 and explicit_conf == 0
      
          # ── Coverage from manifest ──
          coverage_list = manifest.get('coverage') or []
          adequate = sum(1 for c in coverage_list if c.get('status') == 'adequate')
          insufficient = sum(1 for c in coverage_list if c.get('status') == 'insufficient')
          total_subq = len(coverage_list)
          data_limited = manifest.get('data_limited', False)
          unique_domains = manifest.get('unique_domains', 0)
      
          # ── Build lines ──
          cfg = get_lang_config(lang)
          heading = cfg.get('confidence_heading', '## Confidence Assessment')
          lines = [heading, '']
      
          # ── 1. Source type (粗分) ──
          auth = src_class_counts['authoritative']
          ind = src_class_counts['industry']
          med = src_class_counts['media']
          academic_src_pct = round(auth / total_facts * 100) if total_facts > 0 else 0
          label_src = '来源类型(粗分)' if lang == 'zh' else 'Source Type'
          label_auth = '学术/机构' if lang == 'zh' else 'Academic'
          label_ind = '行业' if lang == 'zh' else 'Industry'
          label_med = '媒体' if lang == 'zh' else 'Media'
          lines.append(f"**{label_src}**:"
                       f"{label_auth} {auth} 条 · "
                       f"{label_ind} {ind} 条 · "
                       f"{label_med} {med} 条")
          lines.append('')
      
          # ── 2. Data type (MANDATORY — never omitted) ──
          act = type_counts['actual']
          est = type_counts['estimate']
          fct = type_counts['forecast']
          if total_facts > 0:
              act_pct = round(act / total_facts * 100)
              est_pct = round(est / total_facts * 100)
              fct_pct = round(fct / total_facts * 100)
          else:
              act_pct = est_pct = fct_pct = 0
          label_actual = '已公布' if lang == 'zh' else 'Actual'
          label_est = '估算' if lang == 'zh' else 'Estimate'
          label_fcst = '预测' if lang == 'zh' else 'Forecast'
          lines.append(f"**{'数据类型' if lang == 'zh' else 'Data Type'}**:"
                       f"{label_actual} {act}条({act_pct}%) · "
                       f"{label_est} {est}条({est_pct}%) · "
                       f"{label_fcst} {fct}条({fct_pct}%)")
          lines.append('')
      
          # ── 3. Confidence distribution ──
          high = conf_counts['high']
          medium = conf_counts['medium']
          low = conf_counts['low']
          if total_facts > 0:
              high_pct = round(high / total_facts * 100)
          else:
              high_pct = 0
          label_high = '高置信' if lang == 'zh' else 'High'
          label_med = '中等' if lang == 'zh' else 'Medium'
          label_low = '低置信' if lang == 'zh' else 'Low'
          lines.append(f"**{'置信度分布' if lang == 'zh' else 'Confidence Distribution'}**:"
                       f"{label_high} {high}条({high_pct}%) · "
                       f"{label_med} {medium}条 · "
                       f"{label_low} {low}条")
          lines.append('')
      
          # ── 4. Coverage ──
          if total_subq > 0:
              label_adequate = '充足' if lang == 'zh' else 'Adequate'
              label_insuff = '不足' if lang == 'zh' else 'Insufficient'
              lines.append(f"**{'覆盖度' if lang == 'zh' else 'Coverage'}**:"
                           f"{total_subq}{'个子问题中 ' if lang == 'zh' else ' sub-questions — '}"
                           f"{adequate} {label_adequate}、{insufficient} {label_insuff}")
              lines.append('')
      
          # ── 5. Data limitation ──
          if data_limited:
              label_limited = '有(部分子问题数据覆盖不足,结论需谨慎参考)' if lang == 'zh' else 'Yes (some sub-questions have insufficient coverage — conclusions should be treated with caution)'
          else:
              label_limited = '无' if lang == 'zh' else 'None'
          lines.append(f"**{'数据限制' if lang == 'zh' else 'Data Limitations'}**:{label_limited}")
          lines.append('')
      
          # ── 6. Controversies (conditional) ──
          if controversies_total > 0:
              label_cont = '存在' if lang == 'zh' else ''
              label_cont2 = '处跨源数据差异,已在正文对应章节并排呈现' if lang == 'zh' else ' instances of cross-source data discrepancies, presented side-by-side in relevant chapters'
              lines.append(f"**{'跨源矛盾' if lang == 'zh' else 'Cross-source Discrepancies'}**:"
                           f"{label_cont}{controversies_total}{label_cont2}")
              lines.append('')
      
          # ── 7. Score & rating ──
          if total_facts > 0:
              score = (high / total_facts) * 40 + (act / total_facts) * 30
              if total_subq > 0:
                  score += (adequate / total_subq) * 20
              if not data_limited:
                  score += 10
              score = round(score)
      
              if score >= 75:
                  label_verdict = '结论可靠' if lang == 'zh' else 'Reliable'
              elif score >= 50:
                  label_verdict = '基本可信' if lang == 'zh' else 'Moderate'
              else:
                  label_verdict = '谨慎参考' if lang == 'zh' else 'Caution'
      
              rating_label = '综合评级' if lang == 'zh' else 'Rating'
              lines.append(f"**{rating_label}**:{label_verdict}({score}/100)")
              lines.append('')
      
          else:
              score = 0
              label_verdict = '无数据' if lang == 'zh' else 'No data'
      
          # ── Quick-mode note (last, conditional) ──
          if quick_mode:
              note = ('注:部分事实未显式标注 conf/data_type,以上置信度与数据类型由来源与上下文自动推断'
                      if lang == 'zh'
                      else 'Note: some facts lack explicit conf/data_type fields; confidence and data types above are inferred from sources and context.')
              lines.append(note)
              lines.append('')
      
          section = '\n'.join(lines)
      
          # ── Machine-readable summary (language-agnostic, numbers only) ──
          coverage_summary = manifest.get('coverage_summary', 'unknown')
          adequate_subq = adequate
          total_subq_count = total_subq
          summary = {
              'coverage': coverage_summary,
              'total_facts': total_facts,
              'high_pct': high_pct if total_facts > 0 else 0,
              'medium_pct': round(medium / total_facts * 100) if total_facts > 0 else 0,
              'low_pct': round(low / total_facts * 100) if total_facts > 0 else 0,
              'actual_pct': act_pct if total_facts > 0 else 0,
              'est_pct': est_pct if total_facts > 0 else 0,
              'fct_pct': fct_pct if total_facts > 0 else 0,
              'auth_pct': academic_src_pct,
              'data_limited': data_limited,
              'controversies': controversies_total,
              'adequate_subq': adequate_subq,
              'total_subq': total_subq_count,
              'verdict_label': label_verdict if total_facts > 0 else ('无数据' if lang == 'zh' else 'No data'),
              'score': score,
          }
          return {'passed': True, 'section': section, 'issues': issues, 'summary': summary}
      
      
      def escape_currency(report_path: str) -> dict:
          """Escape unescaped dollar signs to prevent LaTeX math mode rendering.
      
          Platforms like Zhihu, Typora, Obsidian and GitHub with MathJax interpret
          $ as inline math delimiter. This function escapes standalone $ used as
          currency prefix, while skipping protected contexts.
          """
          with open(report_path, 'r', encoding='utf-8-sig') as f:
              content = f.read()
      
          # Repair stale promo placeholders from pre-fix escape_currency runs
          content = re.sub(
              r'\[deep-research\]\(__URL_\d+__\)',
              '[deep-research](https://github.com/hoolulu/deep-research)',
              content,
          )
      
          changes = 0
          protected = {}
          counter = [0]
      
          def _protect(pattern, template='__DOLLAR_SAFE_{}__', flags=0):
              nonlocal content
              def replacer(m):
                  counter[0] += 1
                  key = template.format(counter[0])
                  protected[key] = m.group(0)
                  return key
              content = re.sub(pattern, replacer, content, flags=flags)
      
          # Protect contexts where $ should NOT be escaped
          _protect(r'```.*?```', '__CODE_BLOCK_{}__', re.DOTALL)  # fenced code blocks
          _protect(r'`[^`]+`', '__INLINE_CODE_{}__')             # inline code
          _protect(r'\[([^\[\]]*)\]\(([^)]*)\)', '__MD_LINK_{}__')  # markdown links (before URLs)
          _protect(r'https?://[^\s\)]+', '__URL_{}__')            # bare URLs
          _protect(r'<[^>]+>', '__HTML_TAG_{}__')                 # HTML tags (anchors etc.)
          _protect(r'^\|[-:]+\|', '__TABLE_SEP_{}__', re.MULTILINE)  # table separators
      
          # Strategy: escape ALL $ that are followed by a digit (currency pattern)
          # while skipping those already escaped (\$)
          def _escape_dollar(m):
              nonlocal changes
              changes += 1
              return m.group(1) + '\\' + m.group(2)
      
          # Match $ that is NOT preceded by backslash, and IS followed by a digit
          content = re.sub(r'(^|[^\\])(\$)(?=\d)', _escape_dollar, content)
      
          # Restore protected segments in reverse insertion order so markdown links
          # restored after URL placeholders still get their URLs expanded.
          for key, val in reversed(list(protected.items())):
              content = content.replace(key, val)
      
          issues = []
          if re.search(r'\]\(__URL_\d+__\)', content):
              issues.append('Unresolved __URL_N__ placeholders in markdown links')
      
          # Write back
          tmp = report_path + '.tmp'
          with open(tmp, 'w', encoding='utf-8', newline='\n') as f:
              f.write(content)
          os.replace(tmp, report_path)
      
          return {'passed': len(issues) == 0, 'issues': issues, 'changes': changes}
      
    • dr_tools.py 17.2 KB
      #!/usr/bin/env python3
      import argparse
      import json
      import sys
      
      from dr_check import (
          check_encoding, word_count, json_validate,
          check_headers, check_chapter_numbers, check_metadata,
          check_toc, check_tail, year_density, check_datapool,
          validate_chapter, validate_all_chapters, qa_report,
          check_depth_balance,
      )
      from dr_gen import (
          extract_sources, generate_toc, generate_metadata,
          generate_refs, map_chapters,
          write_json, write_md,
          prepare_chapter, assemble_report, convert_citations,
          generate_confidence_section, escape_currency,
      )
      from lang_config import get_lang_config
      
      
      def _exit(result: dict):
          passed = result.get('passed', False)
          issues = result.get('issues', [])
          stats = {k: result[k] for k in ('record_count', 'source_count', 'fact_count') if k in result}
          if stats:
              import json
              print(f"STATS: {json.dumps(stats)}")
          print("PASS" if passed else "FAIL")
          if not passed:
              for issue in issues:
                  print(f"  - {issue}")
          sys.exit(0 if passed else 1)
      
      
      def main():
          for stream in (sys.stdout, sys.stderr):
              enc = getattr(stream, 'encoding', None)
              if enc and enc.upper() not in ('UTF-8', 'UTF8'):
                  try:
                      stream.reconfigure(encoding='utf-8', errors='replace')
                  except Exception:
                      pass
      
          parser = argparse.ArgumentParser(description='Deep Research Tools v3.0 — QA & utilities')
          sub = parser.add_subparsers(dest='command', required=True)
      
          # Shared --lang for applicable commands
          LANG_ARG = {'flags': ['--lang'], 'default': 'zh',
                      'help': 'Report language code (zh/en/ja/ko/es/fr/de/pt/it/nl/ru/ar/hi/vi/id/th/tr/pl/sv)'}
      
          # ── Check subcommands ──
          p = sub.add_parser('check-encoding', help='Check UTF-8, BOM, Mojibake')
          p.add_argument('file')
          p = sub.add_parser('word-count', help='Count all chars (excl whitespace/markdown)')
          p.add_argument('file')
          p = sub.add_parser('json-validate', help='Validate JSON')
          p.add_argument('file')
          p = sub.add_parser('json-get', help='Read a value from JSON by dot-separated key path')
          p.add_argument('file')
          p.add_argument('key_path', help='e.g. "0.src" or "0.facts.0.src"')
          p = sub.add_parser('check-headers', help='Check ##/### header format')
          p.add_argument('file')
          p.add_argument('--lang', default='zh', help='Language code')
          p = sub.add_parser('check-chapter-numbers', help='Check chapter number format')
          p.add_argument('file')
          p.add_argument('--lang', default='zh', help='Language code')
          p = sub.add_parser('check-metadata', help='Check metadata line completeness')
          p.add_argument('file')
          p.add_argument('--lang', default='zh', help='Language code')
          p = sub.add_parser('check-toc', help='Check TOC existence and count')
          p.add_argument('file')
          p.add_argument('--expected', type=int, default=None)
          p.add_argument('--lang', default='zh', help='Language code')
          p = sub.add_parser('check-tail', help='Check tail sections (refs + disclaimer)')
          p.add_argument('file')
          p.add_argument('--lang', default='zh', help='Language code')
          p = sub.add_parser('year-density', help='Calculate year density')
          p.add_argument('file')
          p.add_argument('--target-year', type=int, required=True)
          p = sub.add_parser('check-datapool', help='Validate data-pool.json structure')
          p.add_argument('file')
          p.add_argument('--mode', choices=['quick', 'standard', 'deep'], required=True)
          p = sub.add_parser('validate-chapter', help='Single-command: all chapter checks at once')
          p.add_argument('file')
          p.add_argument('--expected-sections', type=int, default=0)
          p = sub.add_parser('validate-all-chapters', help='Parallel batch chapter validation')
          p.add_argument('--chapters-dir', required=True)
          p.add_argument('--chapters', type=int, required=True)
          p.add_argument('--expected-sections', type=int, default=0)
          p = sub.add_parser('depth-balance', help='Check chapter depth balance')
          p.add_argument('--chapters-dir', required=True)
          p.add_argument('--chapters', type=int, required=True)
          p.add_argument('--threshold', type=float, default=0.5, help='Min ratio vs average (default: 0.5)')
          p = sub.add_parser('qa-report', help='Full report quality check')
          p.add_argument('file')
          p.add_argument('--mode', choices=['quick', 'standard', 'deep'], required=True)
          p.add_argument('--target-year', type=int, required=True)
          p.add_argument('--lang', default='zh', help='Language code')
      
          # ── Generate subcommands ──
          p = sub.add_parser('extract-sources', help='Extract unique (来源,年份) from report')
          p.add_argument('file')
          p.add_argument('--format', choices=['text', 'json'], default='text')
          p = sub.add_parser('generate-toc', help='Generate TOC from outline.json')
          p.add_argument('outline')
          p = sub.add_parser('generate-metadata', help='Generate metadata block')
          p.add_argument('--word-count', type=int, required=True)
          p.add_argument('--reading-time', type=int, required=True)
          p.add_argument('--data-until', required=True)
          p.add_argument('--generate-time', required=True)
          p.add_argument('--mode', required=True, choices=['quick', 'standard', 'deep'])
          p.add_argument('--source-count', type=int, required=True)
          p.add_argument('--top-sources', nargs='*', default=[])
          p.add_argument('--version', default='', help='Skill version')
          p.add_argument('--lang', default='zh', help='Language code')
          p = sub.add_parser('map-chapters', help='Map chapters to sub_questions')
          p.add_argument('outline')
          p = sub.add_parser('generate-refs', help='Generate source list with titles')
          p.add_argument('datapool')
          p.add_argument('--numbered', action='store_true', help='Output as (N) numbered list')
          p.add_argument('--lang', default='zh', help='Language code')
      
          # convert-citations
          p = sub.add_parser('convert-citations', help='Convert [N] → [(N)](#refN) clickable citations')
          p.add_argument('report', help='Path to assembled report')
          p.add_argument('--datapool', required=True, help='Path to data-pool.json')
          p.add_argument('--output', default=None, help='Output path (default: in-place)')
          p.add_argument('--lang', default='zh', help='Language code')
      
          # ── Write subcommands ──
          p = sub.add_parser('write-json', help='Read JSON from stdin, write UTF-8 no BOM')
          p.add_argument('filepath')
          p = sub.add_parser('write-md', help='Read markdown from stdin, write UTF-8 no BOM')
          p.add_argument('filepath')
      
          # ── Confidence section ──
          p = sub.add_parser('generate-confidence-section',
                             help='Generate and insert confidence assessment into report')
          p.add_argument('--datapool', required=True)
          p.add_argument('--manifest', required=True)
          p.add_argument('--report', required=True, help='Path to assembled report (in-place update)')
          p.add_argument('--lang', default='zh', help='Language code')
      
          # ── Currency escaping ──
          p = sub.add_parser('escape-currency', help='Escape dollar signs to prevent LaTeX math mode rendering')
          p.add_argument('report', help='Path to report markdown file')
      
          # ── Chapter skeleton + Report assembly ──
          p = sub.add_parser('prepare-chapter', help='Generate chapter skeleton with pre-matched facts')
          p.add_argument('--outline', required=True)
          p.add_argument('--datapool', required=True)
          p.add_argument('--chapter', type=int, required=True)
          p.add_argument('--total', type=int, default=1)
          p.add_argument('--mode', choices=['quick', 'standard', 'deep'], default='standard')
          p = sub.add_parser('assemble-report', help='Assemble final report from chapters + metadata')
          p.add_argument('--outline', required=True)
          p.add_argument('--chapters-dir', required=True)
          p.add_argument('--wordcount', default=None, help='Deprecated')
          p.add_argument('--datapool', required=True)
          p.add_argument('--mode', choices=['quick', 'standard', 'deep'], required=True)
          p.add_argument('--target-year', type=int, required=True)
          p.add_argument('--output', default=None,
                         help='Output file path. If omitted, auto-generates from title+date')
          p.add_argument('--lang', default='zh', help='Language code')
      
          args = parser.parse_args()
      
          # ── Dispatch: checks ──
          if args.command == 'check-encoding':
              _exit(check_encoding(args.file))
          elif args.command == 'word-count':
              print(word_count(args.file))
              sys.exit(0)
          elif args.command == 'json-validate':
              _exit(json_validate(args.file))
          elif args.command == 'json-get':
              import json as _json
              with open(args.file, 'r', encoding='utf-8-sig') as _f:
                  _data = _json.load(_f)
              _val = _data
              for _key in args.key_path.split('.'):
                  if isinstance(_val, list):
                      _val = _val[int(_key)]
                  else:
                      _val = _val[_key]
              print(_json.dumps(_val, ensure_ascii=False, indent=2) if not isinstance(_val, (str, int, float, bool)) else _val)
              sys.exit(0)
          elif args.command == 'check-headers':
              _exit(check_headers(args.file, lang=args.lang))
          elif args.command == 'check-chapter-numbers':
              _exit(check_chapter_numbers(args.file, lang=args.lang))
          elif args.command == 'check-metadata':
              _exit(check_metadata(args.file, lang=args.lang))
          elif args.command == 'check-toc':
              _exit(check_toc(args.file, expected=args.expected, lang=args.lang))
          elif args.command == 'check-tail':
              _exit(check_tail(args.file, lang=args.lang))
          elif args.command == 'year-density':
              _exit(year_density(args.file, target_year=args.target_year))
          elif args.command == 'check-datapool':
              _exit(check_datapool(args.file, mode=args.mode))
          elif args.command == 'validate-chapter':
              result = validate_chapter(args.file, expected_sections=args.expected_sections)
              print(json.dumps(result, ensure_ascii=False, indent=2))
              sys.exit(0)
          elif args.command == 'validate-all-chapters':
              result = validate_all_chapters(args.chapters_dir, args.chapters, args.expected_sections)
              print(json.dumps(result, ensure_ascii=False, indent=2))
              sys.exit(0 if result['passed'] else 1)
          elif args.command == 'depth-balance':
              result = check_depth_balance(args.chapters_dir, args.chapters, args.threshold)
              print(json.dumps(result, ensure_ascii=False, indent=2))
              sys.exit(0 if result['passed'] else 1)
          elif args.command == 'qa-report':
              result = qa_report(args.file, mode=args.mode, target_year=args.target_year, lang=args.lang)
              print(json.dumps(result, ensure_ascii=False, indent=2))
              sys.exit(0 if result['passed'] else 1)
      
          # ── Dispatch: generate ──
          elif args.command == 'extract-sources':
              result = extract_sources(args.file)
              if args.format == 'json':
                  print(json.dumps(result, ensure_ascii=False, indent=2))
              else:
                  for s in sorted(result['sources']):
                      print(s)
              sys.exit(0)
          elif args.command == 'generate-toc':
              print(generate_toc(args.outline)['toc_text'])
              sys.exit(0)
          elif args.command == 'generate-metadata':
              version = args.version
              if not version:
                  import os
                  vpath = os.path.join(os.path.dirname(__file__), '..', 'VERSION')
                  try:
                      with open(vpath) as f:
                          version = f.read().strip()
                  except Exception:
                      pass
              print(generate_metadata(
                  word_count=args.word_count, reading_time=args.reading_time,
                  data_until=args.data_until, generate_time=args.generate_time,
                  depth_mode=args.mode, source_count=args.source_count,
                  top_sources=args.top_sources, skill_version=version,
                  lang=args.lang)['full_block'])
              sys.exit(0)
          elif args.command == 'map-chapters':
              print(json.dumps(map_chapters(args.outline), ensure_ascii=False, indent=2))
              sys.exit(0)
          elif args.command == 'generate-refs':
              result = generate_refs(args.datapool, numbered=args.numbered, lang=args.lang)
              print(result['ref_text'])
              sys.exit(0)
      
          elif args.command == 'convert-citations':
              result = convert_citations(args.report, args.datapool, args.output, lang=args.lang)
              _exit(result)
      
          #     ── Dispatch: write ──
          elif args.command == 'write-json':
              _exit(write_json(args.filepath))
          elif args.command == 'write-md':
              _exit(write_md(args.filepath))
      
          # ── Dispatch: confidence section ──
          elif args.command == 'generate-confidence-section':
              result = generate_confidence_section(
                  datapool_path=args.datapool, manifest_path=args.manifest, lang=args.lang)
              if not result['passed']:
                  _exit(result)
              section = result['section']
      
              report_path = args.report
              try:
                  with open(report_path, 'r', encoding='utf-8-sig') as f:
                      content = f.read()
              except Exception as e:
                  _exit({'passed': False, 'issues': [f"Failed to read report: {e}"]})
      
              cfg = get_lang_config(args.lang)
              conf_heading = cfg.get('confidence_heading', '## Confidence Assessment')
              refs_marker = cfg['refs_prefix']
      
              # Skip if confidence section already exists
              if conf_heading in content:
                  print(f"Confidence section already present, skipping: {report_path}")
                  sys.exit(0)
      
              # Insert confidence section before the references section
              pos = content.find(f'\n{refs_marker}')
              if pos == -1:
                  pos = content.find(refs_marker)
              if pos == -1:
                  _exit({'passed': False,
                         'issues': [f"References marker '{refs_marker}' not found in report"]})
      
              new_content = content[:pos] + '\n\n' + section + '\n\n---\n\n' + content[pos:]
      
              tmp = report_path + '.tmp'
              try:
                  with open(tmp, 'w', encoding='utf-8', newline='\n') as f:
                      f.write(new_content)
                  import os
                  os.replace(tmp, report_path)
              except Exception as e:
                  _exit({'passed': False, 'issues': [f"Write failed: {e}"]})
      
              enc_check = __import__('dr_check', fromlist=['check_encoding']).check_encoding(report_path)
              if not enc_check['passed']:
                  _exit(enc_check)
      
              print(f"Confidence section inserted: {report_path}")
      
              sumd = result.get('summary', {})
              print(f"CONFIDENCE: coverage={sumd.get('coverage','unknown')}"
                    f"|total_facts={sumd.get('total_facts',0)}"
                    f"|high_pct={sumd.get('high_pct',0)}"
                    f"|medium_pct={sumd.get('medium_pct',0)}"
                    f"|low_pct={sumd.get('low_pct',0)}"
                    f"|actual_pct={sumd.get('actual_pct',0)}"
                    f"|est_pct={sumd.get('est_pct',0)}"
                    f"|fct_pct={sumd.get('fct_pct',0)}"
                    f"|auth_pct={sumd.get('auth_pct',0)}"
                    f"|data_limited={str(sumd.get('data_limited',False)).lower()}"
                    f"|controversies={sumd.get('controversies',0)}"
                    f"|adequate_subq={sumd.get('adequate_subq',0)}"
                    f"|total_subq={sumd.get('total_subq',0)}"
                    f"|score={sumd.get('score',0)}"
                    f"|verdict={sumd.get('verdict_label','unknown')}")
              sys.exit(0)
      
          # ── Dispatch: escape-currency ──
          elif args.command == 'escape-currency':
              result = escape_currency(args.report)
              print(f"Escaped {result['changes']} dollar signs in: {args.report}")
              _exit(result)
      
          # ── Dispatch: skeleton + assembly ──
          elif args.command == 'prepare-chapter':
              result = prepare_chapter(
                  outline_path=args.outline, datapool_path=args.datapool,
                  chapter_num=args.chapter, total_chapters=args.total, mode=args.mode)
              if result['passed']:
                  print(result['skeleton'])
              _exit(result)
          elif args.command == 'assemble-report':
              result = assemble_report(
                  outline_path=args.outline, chapters_dir=args.chapters_dir,
                  datapool_path=args.datapool,
                  mode=args.mode, target_year=args.target_year,
                  wordcount_path=args.wordcount,
                  output_path=args.output)
              if result['passed']:
                  print(f"Report assembled: {result['output_path']} ({result['line_count']} lines, {result['chapter_count']} chapters, {result['word_count']} chars)")
              _exit(result)
      
      
      if __name__ == '__main__':
          try:
              main()
          except KeyboardInterrupt:
              print("\nInterrupted by user.", file=sys.stderr)
              sys.exit(130)
          except Exception as e:
              print(f"UNEXPECTED ERROR: {e}", file=sys.stderr)
              print(f"Type: {type(e).__name__}", file=sys.stderr)
              import traceback
              traceback.print_exc(file=sys.stderr)
              print("\n---", file=sys.stderr)
              print("Fallback: This script crashed. The LLM can:", file=sys.stderr)
              print("  1. Check Python version: python --version", file=sys.stderr)
              print("  2. Check file existence: os.path.exists(path)", file=sys.stderr)
              print("  3. Run with traceback: PYTHONTRACEMALLOC=1", file=sys.stderr)
              print("  4. Use sys.executable to find the correct Python path", file=sys.stderr)
              sys.exit(1)
      
    • generate_pages.py 28.4 KB
      #!/usr/bin/env python3
      """Scan reports/ and generate gh-pages index (index.html + reports.json).
      
      Local mode:  python tools/generate_pages.py
      API mode:    python tools/generate_pages.py --api
                     (fetches files via GitHub API, no git checkout needed)
      """
      import os, json, re, sys, base64, urllib.request, pathlib
      from datetime import datetime, timezone
      
      # 锚定脚本所在 skill 根目录,使 reports/ 解析不依赖 CWD
      _SKILL_DIR = pathlib.Path(__file__).resolve().parent.parent
      
      REPORTS_DIR = str(_SKILL_DIR / 'reports')
      OUTPUT_DIR = str(_SKILL_DIR / 'gh-pages')
      LOCAL_OUTPUT_DIR = str(_SKILL_DIR / 'reports-browser')
      
      LANG_NAMES = {
          'zh': '中文', 'en': 'English', 'ja': '日本語', 'ko': '한국어',
          'es': 'Español', 'fr': 'Français', 'de': 'Deutsch', 'pt': 'Português',
          'it': 'Italiano', 'nl': 'Nederlands', 'sv': 'Svenska', 'ru': 'Русский',
          'ar': 'العربية', 'hi': 'हिन्दी', 'vi': 'Tiếng Việt', 'id': 'Bahasa Indonesia',
          'th': 'ไทย', 'tr': 'Türkçe', 'pl': 'Polski',
      }
      MODE_LABELS = {'quick': 'Quick', 'standard': 'Standard', 'deep': 'Deep'}
      API_BASE = 'https://api.github.com'
      
      
      def _api_get(path):
          """GitHub API GET request. Uses GITHUB_TOKEN env var."""
          token = os.environ.get('GITHUB_TOKEN', '')
          repo = os.environ.get('GITHUB_REPOSITORY', 'hoolulu/deep-research')
          url = f'{API_BASE}/repos/{repo}/{path}'
          req = urllib.request.Request(url)
          req.add_header('Accept', 'application/vnd.github.v3+json')
          if token:
              req.add_header('Authorization', f'Bearer {token}')
          with urllib.request.urlopen(req) as resp:
              return json.loads(resp.read().decode())
      
      
      def parse_content(content: str, path: str) -> dict:
          """Parse a report's markdown content into metadata dict."""
          t = re.search(r'^# (.+)', content)
          title = t.group(1).strip() if t else os.path.basename(path)
          # Convert absolute path to relative (reports/LANG/filename.md)
          full_path = pathlib.Path(path).resolve()
          try:
              rel = str(full_path.relative_to(_SKILL_DIR)).replace('\\', '/')
          except ValueError:
              rel = full_path.as_posix()
          lang = rel.split('/')[1] if rel.startswith('reports/') else 'en'
          d = re.search(r'(\d{4})(\d{2})(\d{2})', path)
          date = f'{d.group(1)}-{d.group(2)}-{d.group(3)}' if d else ''
          mm = re.search(r'> \*\*(?:元数据|Metadata|メタデータ|메타데이터|Metadatos|Métadonnées|Metadaten|Metadados|Metadati)\*\*[::]\s*(.+)', content)
          mode, wc = 'standard', 0
          if mm:
              mt = mm.group(1)
              m = re.search(r'(?:调研模式|Mode|mode|Modo|モード|모드|Modus|Режим|الوضع|मोड|Chế độ)\s*[::]\s*(\w+)', mt)
              if m:
                  mode = m.group(1).lower()
              for p in [
                  r'(?:字数|总字数|文字数|글자|عدد الكلمات|शब्द|Số từ|จำนวนคำ)\s*[::]\s*([\d,]+)',
                  r'(?:Word Count|Wortanzahl|Contagem|Recuento|Kelime|Jumlah kata)\s*[::]?\s*([\d,]+)',
              ]:
                  m = re.search(p, mt, re.IGNORECASE)
                  if m:
                      wc = int(m.group(1).replace(',', ''))
                      break
          if wc == 0:
              wc = len(re.sub(r'\s+', '', content))
          src = 0
          sm = re.search(
              r'(?:共引用|Total|Totalt|Totale|総計|合计|总共|합계|إجمالي|कुल|Tổng|Total de)\s+(\d+)\s*'
              r'(?:个来源|sources|件の出典|개 출처|fuentes|fontes|Bronnen|källor|источников|'
              r'مصدرًا|स्रोत|nguồn|sumber|แหล่งที่มา|kaynak|źródeł|fonti)', content)
          if sm:
              src = int(sm.group(1))
          else:
              a = re.findall(r'<a id="ref(\d+)"></a>', content)
              src = max(int(x) for x in a) if a else 0
          return {'title': title, 'path': rel, 'lang': lang,
                  'lang_name': LANG_NAMES.get(lang, lang),
                  'mode': mode, 'word_count': wc, 'date': date, 'sources': src}
      
      
      def fmt(n):
          if n >= 10000:
              return f'{round(n/10000,1)}w' if n % 10000 else f'{n//10000}w'
          if n >= 1000:
              return f'{round(n/1000,1)}k' if n % 1000 else f'{n//1000}k'
          return str(n)
      
      
      def gen_html(reports, local=False, out_dir=None):
          out_dir = out_dir or 'gh-pages'
          now = datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')
          repo = os.environ.get('GITHUB_REPOSITORY', 'hoolulu/deep-research')
          base = f'https://github.com/{repo}/blob/main'
      
          lang_counts = {}
          for r in reports:
              lang_counts[r['lang']] = lang_counts.get(r['lang'], 0) + 1
          lang_order = sorted(lang_counts.items(), key=lambda x: (0 if x[0] == 'zh' else 1 if x[0] == 'en' else 2, -x[1]))
      
          chips = ''
          for code, cnt in lang_order:
              name = LANG_NAMES.get(code, code)
              chips += f'<button class="lc" data-lc="{code}" onclick="fc(this)">{name} <span class="lcn">{cnt}</span></button>'
      
          seen = set()
          lo = ''.join(f'<option value="{c}">{n}</option>' for c, n in LANG_NAMES.items() if (c in seen or seen.add(c) is None) and any(r['lang'] == c for r in reports))
          mo = ''.join(f'<option value="{m}">{MODE_LABELS.get(m,m.title())}</option>' for m in sorted(set(r['mode'] for r in reports)))
          # Embed report content for local preview (avoids file:// fetch restrictions)
          report_data = ''
          preview_css = ''
          preview_html = ''
          preview_js = ''
          if local:
              contents = {}
              for i, r in enumerate(reports):
                  if i >= int(os.environ.get('EMBED_MAX', '999')):
                      break
                  path = _SKILL_DIR / r['path']  # r['path'] 已是相对 skill 根的路径
                  try:
                      with open(path, encoding='utf-8') as f:
                          contents[str(i)] = f.read()
                  except:
                      pass
              rd_obj = json.dumps(contents, ensure_ascii=False)
              rf_obj = json.dumps({str(k):r["path"].split("/")[-1] for k,r in enumerate(reports) if str(k) in contents}, ensure_ascii=False)
              report_data = f'var RD={{}},RF={{}};'
              report_data += 'try{'
              report_data += f'RD={rd_obj};RF={rf_obj}'
              report_data += '}catch(e){}'
              preview_css = '.pv-overlay{display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.5);z-index:100;overflow:hidden}.pv-container{max-width:900px;margin:0 auto;height:100%;display:flex;flex-direction:column;pointer-events:none;position:relative}.pv-container>*{pointer-events:auto}.pv-header{display:flex;align-items:center;gap:8px;padding:8px 16px;background:var(--bg);border-bottom:1px solid var(--border);border-radius:8px 8px 0 0;flex-shrink:0;position:relative}.pv-header h2{flex:1;font-size:15px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pv-btn{cursor:pointer;padding:4px 10px;border:1px solid var(--border);border-radius:var(--radius);font-size:14px;background:var(--bg);font-family:inherit;color:var(--fg);white-space:nowrap;flex-shrink:0}.pv-btn:hover{background:var(--canvas)}.pv-body{flex:1;overflow-y:auto;padding:20px;line-height:1.7;font-size:15px;overflow-wrap:break-word;background:var(--bg);border-radius:0 0 8px 8px}.pv-body h1{font-size:24px;margin:0 0 16px;padding-bottom:8px;border-bottom:1px solid var(--border)}.pv-body h2{font-size:20px;margin:24px 0 12px}.pv-body h3{font-size:17px;margin:20px 0 8px}.pv-body table{border-collapse:collapse;margin:12px 0;font-size:14px;width:100%}.pv-body td,.pv-body th{border:1px solid var(--border);padding:6px 10px;text-align:left}.pv-body th{background:var(--canvas);font-weight:600}.pv-body code{background:var(--canvas);padding:2px 5px;border-radius:3px;font-size:13px;font-family:var(--fm)}.pv-body pre code{display:block;padding:12px;overflow-x:auto;line-height:1.4}.pv-body blockquote{border-left:4px solid var(--accent);margin:12px 0;padding:4px 16px;color:var(--muted);background:var(--canvas)}.pv-body ul,.pv-body ol{list-style:none;padding-left:0}.pv-body a{color:var(--accent)}.pv-toc-panel{display:none;position:absolute;top:100%;right:0;width:280px;max-height:60vh;overflow-y:auto;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius);box-shadow:0 8px 24px rgba(0,0,0,.15);z-index:2;padding:8px 0;font-size:13px}.pv-toc-panel a{display:block;padding:4px 16px;color:var(--fg);text-decoration:none}.pv-toc-panel a:hover{background:var(--canvas)}.pv-toc-panel .pv-toc-l2{padding-left:32px;font-size:12px}.pv-toc-panel .pv-toc-l3{padding-left:48px;font-size:11px}.pv-toc-panel a:first-child{margin-top:0}#pv-top{position:absolute;bottom:24px;right:24px;z-index:101;width:40px;height:40px;border:none;background:#07C160;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 8px rgba(7,193,96,.3);color:#fff;border-radius:10px;opacity:.9;transition:all .2s}#pv-top:hover{opacity:1;box-shadow:0 4px 12px rgba(7,193,96,.4);transform:translateY(-2px)}#pv-top svg{width:18px;height:18px;fill:currentColor}.pv-toc-panel{display:none;position:absolute;top:100%;right:0;width:280px;max-height:60vh;overflow-y:auto;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius);box-shadow:0 8px 24px rgba(0,0,0,.15);z-index:2;padding:8px 0;font-size:14px}.pv-toc-panel a{display:block;padding:6px 12px;color:var(--fg);text-decoration:none;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pv-toc-panel a:hover{background:var(--canvas);color:var(--accent)}.pv-toc-l2{padding-left:24px!important;font-size:13px;color:var(--muted)!important}.pv-toc-l3{padding-left:36px!important;font-size:12px;color:var(--muted)!important}.pv-export{position:relative;display:inline-block}.pv-export-menu{display:none;position:absolute;top:100%;right:0;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius);box-shadow:0 4px 12px rgba(0,0,0,.1);z-index:2;min-width:100px;white-space:nowrap;overflow:hidden}.pv-export-menu div{padding:8px 16px;cursor:pointer;font-size:13px;color:var(--fg)}.pv-export-menu div:hover{background:var(--canvas);color:var(--accent)}'
              preview_html = '<div id="pv" class="pv-overlay" onclick="if(event.target===this)pc()"><div class="pv-container"><div class="pv-header"><h2 id="pv-title"></h2><span class="pv-export" id="pv-export-wrap"><button class="pv-btn" onclick="te()">\u5bfc\u51fa \u25be</button><div class="pv-export-menu" id="pv-export-menu" style="display:none"><div onclick="ex(\'pdf\')">PDF</div><div onclick="ex(\'docx\')">DOCX</div></div></span><button class="pv-btn" id="pv-toc-btn" onclick="tt()">TOC/\u76ee\u5f55</button><div class="pv-toc-panel" id="pv-toc-panel"></div><button class="pv-btn" onclick="pc()">&#10005;</button></div><div class="pv-body" id="pv-body"></div><button id="pv-top" onclick="document.getElementById(\'pv-body\').scrollTop=0"><svg viewBox="0 0 24 24"><path d="M12 4l-8 8h5v8h6v-8h5z"/></svg></button></div></div>'
              preview_js = '''var P=document.getElementById('pv');var PB=document.getElementById('pv-body');var PTPanel=document.getElementById('pv-toc-panel');var PRid=null;function qs(t){return t.replace(/[^\w\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af-]+/g,'-').replace(/^-|-$/g,'').toLowerCase()}function qs2(t){return t.replace(/[^\w\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/g,'').toLowerCase()}PB.addEventListener('click',function(e){var a=e.target.closest('a[href^="#"]');if(a){e.preventDefault();var id=decodeURIComponent(a.getAttribute('href').slice(1));var el=document.getElementById(id);if(!el)PB.querySelectorAll('h1,h2,h3').forEach(function(h){var s1=qs(h.textContent),s2=qs2(h.textContent);if(s1===id||s2===id||s2===id.replace(/-/g,''))el=h});if(el){var t=el.getBoundingClientRect().top+PB.scrollTop-60;PB.scrollTo({top:t,behavior:'smooth'})}}});function po(r){var t=r.querySelector('a').textContent;var k=r.dataset.rid;var m=RD[k];if(!m)return;PRid=k;document.getElementById('pv-title').textContent=t;function render(){PB.innerHTML=marked.parse(m);gtoc();P.style.display='block';document.body.style.overflow='hidden';PB.scrollTop=0;PTPanel.style.display='none';var el=document.getElementById('pv-export-menu');if(el)el.style.display='none'}if(window.marked)render();else{var s=document.createElement('script');s.src='marked.min.js';s.onload=render;document.head.appendChild(s)}}function pc(){P.style.display='none';document.body.style.overflow=''}function tt(){PTPanel.style.display=PTPanel.style.display==='block'?'none':'block'}function gtoc(){PTPanel.innerHTML='';PB.querySelectorAll('h1,h2,h3').forEach(function(h,i){if(!h.id)h.id='toc-'+i;var a=document.createElement('a');a.textContent=h.textContent;a.setAttribute('data-tid',h.id);a.addEventListener('click',function(e){e.preventDefault();var el=document.getElementById(this.getAttribute('data-tid'));if(el){var t=el.getBoundingClientRect().top+PB.scrollTop-60;PB.scrollTo({top:t,behavior:'smooth'})}PTPanel.style.display='none'});var l=h.tagName.toLowerCase();if(l!='h1')a.className=l=='h2'?'pv-toc-l2':'pv-toc-l3';PTPanel.appendChild(a)})}document.getElementById('b').addEventListener('click',function(e){var a=e.target.closest('a');if(!a)return;var r=a.closest('tr');if(r&&r.dataset.rid!==void 0){e.preventDefault();po(r)}});function te(){var m=document.getElementById('pv-export-menu');m.style.display=m.style.display==='block'?'none':'block'};function ex(f){document.getElementById('pv-export-menu').style.display='none';if(f==='pdf')epdf();else if(f==='docx')edocx()};function epdf(){var e=document.getElementById('pv-body');var t=document.getElementById('pv-title').textContent.replace(/[<>:"\/\|?*]/g,'_')||'report';var o={margin:[0.5,0.5,0.5,0.5],filename:t+'.pdf',image:{type:'jpeg',quality:.95},html2canvas:{scale:2},jsPDF:{unit:'in',format:'a4',orientation:'portrait'},pagebreak:{mode:['avoid-all','css','legacy']}};if(typeof html2pdf==='undefined'){var s=document.createElement('script');s.src='html2pdf.bundle.min.js';s.onload=function(){html2pdf().from(e).set(o).save()};document.head.appendChild(s)}else{html2pdf().from(e).set(o).save()}};function edocx(){var c=document.getElementById('pv-body').innerHTML;var t=document.getElementById('pv-title').textContent.replace(/[<>:"\/\|?*]/g,'_')||'report';var ss='body{font-family:\"宋体\",SimSun,\"Noto Sans CJK SC\",serif;line-height:1.7;padding:20px;max-width:900px;color:#1f2328;font-size:12pt}h1{font-size:18pt;margin:0 0 12px;padding-bottom:6px;border-bottom:1px solid #d0d7de;font-family:\"黑体\",SimHei,\"Noto Sans CJK SC\",sans-serif}h2{font-size:15pt;margin:18px 0 10px;font-family:\"黑体\",SimHei,\"Noto Sans CJK SC\",sans-serif}h3{font-size:13pt;margin:14px 0 6px;font-family:\"黑体\",SimHei,\"Noto Sans CJK SC\",sans-serif}table{border-collapse:collapse;margin:10px 0;font-size:11pt;width:100%}td,th{border:1px solid #d0d7de;padding:4px 8px}th{background:#f6f8fa;font-weight:600}code{background:#f6f8fa;padding:1px 4px;border-radius:2px;font-size:11pt}pre code{display:block;padding:10px}blockquote{border-left:4px solid #0969da;margin:10px 0;padding:3px 14px;color:#656d76}';var html='<!DOCTYPE html><html><head><meta charset=\"utf-8\"><title>'+t+'</title><style>'+ss+'</style></head><body>'+c+'</body></html>';if(typeof htmlDocx==='undefined'){var sc=document.createElement('script');sc.src='html-docx.min.js';sc.onload=function(){edocx2(html,t)};document.head.appendChild(sc)}else{edocx2(html,t)}};function edocx2(html,t){var b=htmlDocx.asBlob(html,{margins:{top:1440,right:1440,bottom:1440,left:1440}});var u=URL.createObjectURL(b);var a=document.createElement('a');a.href=u;a.download=t+'.docx';document.body.appendChild(a);a.click();setTimeout(function(){document.body.removeChild(a);URL.revokeObjectURL(u)},1e3)}'''
      
          rows = ''
          for i, r in enumerate(reports, 1):
              mc = r['mode'] if r['mode'] in MODE_LABELS else 'standard'
              idx = i - 1
              url = f'../{r["path"]}' if local else f'{base}/{r["path"]}'
              rows += f'<tr data-lang="{r["lang"]}" data-mode="{r["mode"]}" data-rid="{idx}"><td class="n">{i}</td><td class="tc"><a href="{url}" target="_blank">{r["title"]}</a></td><td>{r["lang_name"]}</td><td><span class="mb mb-{mc}">{MODE_LABELS.get(r["mode"],r["mode"].title())}</span></td><td class="m">{fmt(r["word_count"])}</td><td class="m">{r["sources"]}</td><td class="m">{r["date"]}</td></tr>'
      
          h = f'''<!DOCTYPE html>
      <html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>{'Your Reports' if local else 'Deep Research Reports'}</title><link rel="icon" type="image/svg+xml" href="favicon.svg"><style>
      :root{{--fg:#1f2328;--bg:#fff;--canvas:#f6f8fa;--border:#d0d7de;--bm:#d8dee4;--accent:#0969da;--green:#1a7f37;--orange:#9a6700;--muted:#656d76;--radius:6px;--f:-apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans",Helvetica,Arial,sans-serif;--fm:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace}}
      *{{margin:0;padding:0;box-sizing:border-box}}body{{font-family:var(--f);color:var(--fg);background:var(--bg);line-height:1.5}}.w{{max-width:1100px;margin:0 auto;padding:24px 16px}}.hd{{display:flex;align-items:center;gap:12px;margin-bottom:4px}}.hd h1{{font-size:24px;font-weight:600;letter-spacing:-.02em}}.hd h1 a{{color:inherit;text-decoration:none}}.hd h1 a:hover{{color:var(--accent)}}.st{{color:var(--muted);font-size:14px;margin-bottom:20px}}.tb{{display:flex;gap:8px;margin-bottom:16px;flex-wrap:wrap;align-items:center}}.sw{{position:relative;flex:1;min-width:200px}}.sw svg{{position:absolute;left:10px;top:50%;transform:translateY(-50%);pointer-events:none;color:var(--muted)}}.sw input{{width:100%;padding:6px 12px 6px 34px;font-size:14px;border:1px solid var(--border);border-radius:var(--radius);outline:none;font-family:inherit}}.sw input:focus{{border-color:var(--accent);box-shadow:0 0 0 3px rgba(9,105,218,.15)}}.fs{{padding:6px 32px 6px 12px;font-size:14px;border:1px solid var(--border);border-radius:var(--radius);cursor:pointer;outline:none;font-family:inherit;appearance:none;background:var(--bg);background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16'%3E%3Cpath fill='%23656d76' d='M4.43 6.43l3.4 3.4a.25.25 0 00.35 0l3.4-3.4A.25.25 0 0011.4 6H4.6a.25.25 0 00-.18.43z'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 8px center}}
      .fs:focus{{border-color:var(--accent);box-shadow:0 0 0 3px rgba(9,105,218,.15)}}.bg{{font-size:12px;white-space:nowrap;color:var(--muted)}}
      .tw{{border:1px solid var(--border);border-radius:var(--radius);overflow:hidden}}table{{width:100%;border-collapse:collapse;font-size:14px}}thead{{background:var(--canvas)}}
      th{{padding:8px 12px;text-align:left;font-weight:600;border-bottom:1px solid var(--bm);white-space:nowrap;cursor:pointer;user-select:none}}th:hover{{color:var(--accent)}}th .si{{display:inline-block;width:16px;margin-left:4px;opacity:.3;vertical-align:middle}}th.s .si{{opacity:1;color:var(--accent)}}
      td{{padding:8px 12px;border-bottom:1px solid var(--bm);vertical-align:middle}}tbody tr{{transition:background .1s}}tbody tr:hover{{background:#f3f4f6}}tr:last-child td{{border-bottom:none}}
      .n{{color:var(--muted);font-size:12px;width:30px;text-align:right}}.tc a{{color:var(--accent);text-decoration:none;font-weight:500}}.tc a:hover{{text-decoration:underline}}.m{{font-family:var(--fm);font-size:12px;color:var(--muted);white-space:nowrap}}
      .mb{{display:inline-block;padding:2px 7px;font-size:11px;font-weight:600;border-radius:20px;line-height:1.4;white-space:nowrap}}.mb-quick{{color:var(--green);background:#dafbe1}}.mb-standard{{color:var(--accent);background:#ddf4ff}}.mb-deep{{color:var(--orange);background:#fff8c5}}
      .lc{{display:inline-flex;align-items:center;gap:5px;padding:4px 12px;margin:0 4px 8px 0;font-size:13px;font-weight:500;border:1px solid var(--border);border-radius:20px;background:var(--bg);color:var(--fg);cursor:pointer;font-family:inherit;transition:all .15s;white-space:nowrap}}.lc:hover{{border-color:var(--accent);color:var(--accent);background:#f3f8ff}}.lc.s{{border-color:var(--accent);color:#fff;background:var(--accent)}}.lcn{{display:inline-flex;align-items:center;justify-content:center;min-width:20px;height:20px;padding:0 5px;font-size:11px;font-weight:600;border-radius:10px;background:var(--canvas);color:var(--muted)}}
      .pm{{font-size:12px;color:var(--muted);font-weight:400}}.pm a{{color:var(--accent);text-decoration:none}}
      .hidden{{display:none!important}}.ft{{margin-top:16px;font-size:12px;color:var(--muted);display:flex;justify-content:space-between;flex-wrap:wrap;gap:8px}}.ft a{{color:var(--accent);text-decoration:none}}.ft a:hover{{text-decoration:underline}}
      @media(max-width:700px){{.w{{padding:16px 8px}}.lcw{{margin-bottom:8px}}.tb{{flex-direction:column}}.sw{{min-width:100%}}thead{{display:none}}tbody tr{{display:block;padding:12px;border-bottom:1px solid var(--bm)}}td{{display:block;padding:3px 0;border:none}}td.n{{display:none}}td.tc{{font-size:15px;margin-bottom:4px}}td::before{{content:attr(data-l);font-size:11px;color:var(--muted);display:inline-block;width:60px;font-weight:500}}}}
      .pg{{display:flex;align-items:center;justify-content:space-between;padding:8px 0;font-size:13px;color:var(--muted);flex-wrap:wrap;gap:8px}}.pg-b{{display:flex;align-items:center;gap:6px}}.pg-s select{{padding:2px 6px;font-size:12px;border:1px solid var(--border);border-radius:var(--radius);font-family:inherit;cursor:pointer}}.pg button{{padding:2px 10px;border:1px solid var(--border);border-radius:var(--radius);background:var(--bg);cursor:pointer;font-size:13px;font-family:inherit;color:var(--fg)}}.pg button:disabled{{opacity:.4;cursor:default}}.pg button:not(:disabled):hover{{background:var(--canvas)}}
      {preview_css}</style></head><body>
      {preview_html}
      <div class="w">
      <div class="hd"><svg width="36" height="36" viewBox="0 0 36 36"><rect width="36" height="36" rx="8" fill="#0969da"/><text x="18" y="25" text-anchor="middle" font-size="20" font-weight="bold" fill="white" font-family="sans-serif">DR</text></svg><h1><a href=".">{'Your Reports' if local else 'Deep Research Reports'}</a></h1></div>
      <p class="st">{'Browse your locally generated research reports' if local else 'Browse all generated reports'} — filter by language, depth, or keywords<br><span class="pm">{'Generated by' if local else 'Powered by'} <a href="https://github.com/hoolulu/deep-research" target="_blank">deep-research</a> &mdash; One command. Ten minutes. Deep professional reports.</span></p>
      <div class="lcw">{chips}</div>
      <div class="tb">
      <div class="sw"><svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"><path d="M10.68 11.74a6 6 0 1 1 1.06-1.06l3.04 3.04a.75.75 0 1 1-1.06 1.06l-3.04-3.04z" fill="currentColor"/><circle cx="7" cy="7" r="5.25" stroke="currentColor" stroke-width="1.5" fill="none"/></svg><input type="text" id="q" placeholder="Search reports&hellip;" oninput="f()" autocomplete="off"></div>
      <select class="fs" id="l" onchange="f()"><option value="">All languages</option>{lo}</select>
      <select class="fs" id="m" onchange="f()"><option value="">All depths</option>{mo}</select>
      <span class="bg" id="c">{len(reports)} reports</span>
      </div>
      <div class="tw"><table><thead><tr><th style="width:30px">#</th><th onclick="s(0)" class="s">Report <span class="si">&#8595;</span></th><th onclick="s(1)">Language <span class="si">&#8597;</span></th><th onclick="s(2)">Depth <span class="si">&#8597;</span></th><th onclick="s(3)">Words <span class="si">&#8597;</span></th><th onclick="s(4)">Sources <span class="si">&#8597;</span></th><th onclick="s(5)">Date <span class="si">&#8597;</span></th></tr></thead><tbody id="b">{rows}</tbody></table></div>
      <div class="pg"><span class="pg-b" id="pg-info"></span><span class="pg-s">Show: <select id="pg-ps" onchange="pg()"><option value="10">10</option><option value="20" selected>20</option><option value="30">30</option><option value="0">All</option></select></span><span class="pg-b"><button onclick="pp(-1)" id="pg-p" disabled>&#9664;</button><span id="pg-n">1</span><button onclick="pp(1)" id="pg-nn">&#9654;</button></span></div>
      <div class="ft"><span>Last updated: {now}</span><span><a href="https://github.com/hoolulu/deep-research" target="_blank">deep-research</a> &mdash; Open source</span></div>
      </div>
      <script>
      {report_data}
      var D=[],CP=1,PS=20;
      (function(){{var r=document.getElementById('b');for(var i=0;i<r.children.length;i++)D.push(r.children[i]);}})();
      function f(){{document.querySelectorAll('.lc').forEach(function(x){{x.classList.remove('s')}});var q=document.getElementById('q').value.toLowerCase(),l=document.getElementById('l').value,m=document.getElementById('m').value,c=0;for(var i=0;i<D.length;i++){{var x=D[i];var ok=(!l||x.dataset.lang==l)&&(!m||x.dataset.mode==m)&&(!q||x.querySelector('a').textContent.toLowerCase().indexOf(q)>=0);x.dataset.f=ok?'1':'0';if(ok)c++;}}document.getElementById('c').textContent=c+' reports';CP=1;pg();}}
      function fc(b){{var l=document.getElementById('l');var all=document.querySelectorAll('.lc');all.forEach(function(x){{x.classList.toggle('s',x===b)}});l.value=b.dataset.lc;f();}}
      var sd={{}};
      function s(k){{var h=event.currentTarget;sd[k]=sd[k]==='asc'?'desc':'asc';document.querySelectorAll('thead th').forEach(function(t){{t.classList.remove('s')}});h.classList.add('s');var d=sd[k];h.querySelector('.si').innerHTML=d==='asc'?'&#8593;':'&#8595;';D.sort(function(a,b){{var va,vb,ca=a.children[k+1],cb=b.children[k+1];if(k===0){{va=a.querySelector('a').textContent;vb=b.querySelector('a').textContent;}}else if(k===3){{va=p(ca.textContent);vb=p(cb.textContent);}}else if(k===4){{va=parseInt(ca.textContent)||0;vb=parseInt(cb.textContent)||0;}}else{{va=ca.textContent;vb=cb.textContent;}}var c=typeof va==='number'?va-vb:String(va).localeCompare(String(vb));return d==='asc'?c:-c;}});var tb=document.getElementById('b');for(var i=0;i<D.length;i++)tb.appendChild(D[i]);CP=1;pg();}}
      function p(s){{var m=s.match(/([\\d.]+)([wk]?)/);if(!m)return 0;var n=parseFloat(m[1]);if(m[2]==='w')return n*10000;if(m[2]==='k')return n*1000;return n||0;}}
      function pg(){{PS=parseInt(document.getElementById('pg-ps').value)||D.length;var t=D.filter(function(x){{return x.dataset.f=='1'}});var tp=PS?Math.ceil(t.length/PS):1;if(CP>tp)CP=tp;if(CP<1)CP=1;for(var i=0;i<D.length;i++){{var x=D[i];var ok=x.dataset.f=='1';if(ok&&PS){{var ti=D.slice(0,i+1).filter(function(y){{return y.dataset.f=='1'}}).length-1;var pi=Math.floor(ti/PS)+1;ok=(pi==CP)}}x.classList.toggle('hidden',!ok)}}document.getElementById('pg-p').disabled=CP<=1;document.getElementById('pg-nn').disabled=CP>=tp;document.getElementById('pg-n').textContent=tp?'Page '+CP+' of '+tp:'No results';var ps=document.getElementById('pg-ps');document.getElementById('pg-info').textContent=t.length+' reports'+(PS&&PS<D.length?' (showing '+Math.min(PS,t.length-CP*PS+PS)+')':'')}}
      function pp(d){{CP+=d;pg()}}
      {preview_js}
      </script></body></html>'''
          with open(os.path.join(out_dir, 'index.html'), 'w', encoding='utf-8') as f:
              f.write(h)
          if local:
              pass
      
      
      def gen_favicon(out_dir):
          svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="12" fill="#0969da"/><text x="32" y="44" text-anchor="middle" font-size="36" font-weight="bold" fill="white" font-family="sans-serif">DR</text></svg>'
          with open(os.path.join(out_dir, 'favicon.svg'), 'w', encoding='utf-8') as f:
              f.write(svg)
      
      
      def main():
          local = '--local' in sys.argv
          use_fallback = '--fallback' in sys.argv
          reports = []
      
          # Walk reports/ directory
          for root, dirs, files in os.walk(REPORTS_DIR):
              for fname in sorted(files):
                  if not fname.endswith('.md'):
                      continue
                  path = os.path.join(root, fname)
                  try:
                      with open(path, encoding='utf-8') as f:
                          reports.append(parse_content(f.read(), path))
                  except (FileNotFoundError, OSError) as e:
                      if use_fallback:
                          rel = os.path.relpath(path, REPORTS_DIR).lstrip('./')
                          full_path = f'reports/{rel}'
                          try:
                              resp = _api_get(f'contents/{full_path}')
                              text = base64.b64decode(resp['content']).decode('utf-8')
                              reports.append(parse_content(text, full_path))
                              print(f'  API fallback: {full_path}')
                          except Exception as e2:
                              print(f'  BOTH FAILED {fname}: {e2}')
                      else:
                          print(f'  SKIP (not found): {fname}')
      
          out = LOCAL_OUTPUT_DIR if local else OUTPUT_DIR
          reports.sort(key=lambda r: r['date'], reverse=True)
          os.makedirs(out, exist_ok=True)
          with open(os.path.join(out, 'reports.json'), 'w', encoding='utf-8') as f:
              json.dump(reports, f, ensure_ascii=False, indent=2)
          gen_html(reports, local=local, out_dir=out)
          gen_favicon(out)
          loc = '(local report browser)' if local else '(GitHub Pages)'
          print(f'Done: {len(reports)} reports -> {out}/ {loc}')
      
      
      if __name__ == '__main__':
          main()
      
    • lang_config.py 24.1 KB
      #!/usr/bin/env python3
      """
      Language configuration for Deep Research reports.
      
      Shared template for all Latin-script languages, per-language overrides
      only specify the translated strings. `get_lang_config(lang)` returns
      the merged config.
      """
      
      CHINESE_NUMERALS = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十',
                          '十一', '十二', '十三', '十四', '十五']
      
      # ── Shared structural elements (not language-specific) ──
      
      _LATIN_CHAPTER = lambda n, title: f"## {n}. {title}"
      _LATIN_PREFIX = lambda n: f"{n}."
      _LATIN_TOC = lambda prefix, title, anchor: f"- [{prefix} {title}](#{anchor})"
      
      _ZH_CHAPTER = lambda n, title: f"## {CHINESE_NUMERALS[n-1]}、{title}"
      _ZH_PREFIX = lambda n: CHINESE_NUMERALS[n-1] + "、"
      _ZH_TOC = lambda prefix, title, anchor: f"- [{prefix}{title}](#{anchor})"
      
      # Base template for all 18 non-zh languages
      _BASE_LATIN = {
          "sep": " · ",
          "field_sep": ": ",
          "minute_unit": "min",
          "chapter_heading": _LATIN_CHAPTER,
          "toc_prefix": _LATIN_PREFIX,
          "toc_line": _LATIN_TOC,
          "promo_line": "*Generated by [deep-research](https://github.com/hoolulu/deep-research) · One command. Ten minutes. Deep professional reports.*",
          "check": {
              "ch_number_pattern": r'^## \d+\.',
              "ch_has_number_check": True,
          }
      }
      
      _BASE_ZH = {
          "sep": " · ",
          "field_sep": ":",
          "minute_unit": "分钟",
          "chapter_heading": _ZH_CHAPTER,
          "toc_prefix": _ZH_PREFIX,
          "toc_line": _ZH_TOC,
          "promo_line": "*本报告由 [deep-research](https://github.com/hoolulu/deep-research) 自动生成 · 一个命令,十分钟,一份深度专业报告*",
          "check": {
              "ch_number_pattern": r'^## ([' + ''.join(CHINESE_NUMERALS) + r']+)、',
              "ch_has_number_check": False,
          }
      }
      
      # ── Per-language translated strings only ──
      # Each entry specifies only what's different from the base template.
      # The fields are: metadata_fields, metadata_label, references_label,
      # refs_prefix, refs_count_format, toc_heading, disclaimer_title,
      # disclaimer_text, confidence_heading, verdict_labels, report_generated,
      # check:{metadata_pattern, references_pattern, tail_refs, tail_disclaimer, tail_confidence}
      
      _LANG = {
      
      "zh": {
          "metadata_fields": ["总字数", "阅读时间", "数据截至", "生成时间", "调研模式", "Skill版本"],
          "metadata_label": "**元数据**:",
          "references_label": "**参考来源**:",
          "refs_prefix": "## 参考来源",
          "refs_count_format": "共引用 {count} 个来源",
          "toc_heading": "## 目录",
          "disclaimer_title": "## 免责声明",
          "disclaimer_text": "本报告基于公开数据整理,不构成投资建议。部分存疑数据已标明,请自行谨慎判断。",
          "confidence_heading": "## 可信评估",
          "verdict_labels": {"reliable": "结论可靠", "moderate": "基本可信", "caution": "谨慎参考"},
          "report_generated": "*报告生成时间:{time}*",
          "check": {
              "metadata_pattern": r'> \*\*元数据\*\*:',
              "references_pattern": r'> \*\*参考来源\*\*',
              "tail_refs": "## 参考来源",
              "tail_disclaimer": "## 免责声明",
              "tail_confidence": "## 可信评估",
          }
      },
      
      "en": {
          "metadata_fields": ["Word Count", "Reading Time", "Data Until", "Generated", "Mode", "Skill Version"],
          "metadata_label": "**Metadata**:",
          "references_label": "**References**:",
          "refs_prefix": "## References",
          "refs_count_format": "Total {count} sources",
          "toc_heading": "## Table of Contents",
          "disclaimer_title": "## Disclaimer",
          "disclaimer_text": "This report is compiled from publicly available data and does not constitute investment advice. Some data points are marked as questionable — please exercise your own judgment.",
          "confidence_heading": "## Confidence Assessment",
          "verdict_labels": {"reliable": "Reliable", "moderate": "Moderate", "caution": "Caution"},
          "report_generated": "*Report generated: {time}*",
          "check": {
              "metadata_pattern": r'> \*\*Metadata\*\*:',
              "references_pattern": r'> \*\*References\*\*',
              "tail_refs": "## References",
              "tail_disclaimer": "## Disclaimer",
              "tail_confidence": "## Confidence Assessment",
          }
      },
      
      "ja": {
          "metadata_fields": ["文字数", "読了時間", "データ期限", "生成時間", "モード", "Skillバージョン"],
          "metadata_label": "**メタデータ**:",
          "references_label": "**参照元**:",
          "refs_prefix": "## 参照元",
          "refs_count_format": "合計 {count} 件の出典",
          "toc_heading": "## 目次",
          "disclaimer_title": "## 免責事項",
          "disclaimer_text": "本レポートは公開データに基づき作成されたものであり、投資助言を構成するものではありません。疑わしいデータには注釈を付しています。",
          "confidence_heading": "## 信頼性評価",
          "verdict_labels": {"reliable": "信頼できる", "moderate": "ほぼ信頼", "caution": "注意"},
          "report_generated": "*生成時間:{time}*",
          "check": {
              "metadata_pattern": r'> \*\*メタデータ\*\*:',
              "references_pattern": r'> \*\*参照元\*\*',
              "tail_refs": "## 参照元",
              "tail_disclaimer": "## 免責事項",
              "tail_confidence": "## 信頼性評価",
          }
      },
      
      "ko": {
          "metadata_fields": ["글자 수", "읽는 시간", "데이터 기준일", "생성 시간", "모드", "Skill 버전"],
          "metadata_label": "**메타데이터**:",
          "references_label": "**참고 자료**:",
          "refs_prefix": "## 참고 자료",
          "refs_count_format": "총 {count}개 출처",
          "toc_heading": "## 목차",
          "disclaimer_title": "## 면책 조항",
          "disclaimer_text": "본 보고서는 공개 데이터를 기반으로 작성되었으며 투자 조언을 구성하지 않습니다. 의심스러운 데이터는 별도 표기되었습니다.",
          "confidence_heading": "## 신뢰도 평가",
          "verdict_labels": {"reliable": "신뢰 가능", "moderate": "대체로 신뢰", "caution": "주의"},
          "report_generated": "*생성 시간:{time}*",
          "check": {
              "metadata_pattern": r'> \*\*메타데이터\*\*:',
              "references_pattern": r'> \*\*참고 자료\*\*',
              "tail_refs": "## 참고 자료",
              "tail_disclaimer": "## 면책 조항",
              "tail_confidence": "## 신뢰도 평가",
          }
      },
      
      # ── Latin-script languages (en/es/fr/de/pt/it/nl/sv) ──
      # All use _BASE_LATIN for structural fields; only translated strings below.
      
      "es": {
          "metadata_fields": ["Recuento de palabras", "Tiempo de lectura", "Datos hasta", "Generado", "Modo", "Versión Skill"],
          "metadata_label": "**Metadatos**:",
          "references_label": "**Referencias**:",
          "refs_prefix": "## Referencias",
          "refs_count_format": "Total {count} fuentes",
          "toc_heading": "## Índice",
          "disclaimer_title": "## Descargo de responsabilidad",
          "disclaimer_text": "Este informe se elabora a partir de datos públicos y no constituye asesoramiento de inversión. Algunos datos dudosos están señalados.",
          "confidence_heading": "## Evaluación de Confianza",
          "verdict_labels": {"reliable": "Fiable", "moderate": "Moderado", "caution": "Precaución"},
          "report_generated": "*Informe generado: {time}*",
          "check": {
              "metadata_pattern": r'> \*\*Metadatos\*\*:',
              "references_pattern": r'> \*\*Referencias\*\*',
              "tail_refs": "## Referencias",
              "tail_disclaimer": "## Descargo de responsabilidad",
              "tail_confidence": "## Evaluación de Confianza",
          }
      },
      
      "fr": {
          "metadata_fields": ["Nombre de mots", "Temps de lecture", "Données jusqu'au", "Généré le", "Mode", "Version Skill"],
          "metadata_label": "**Métadonnées**:",
          "references_label": "**Références**:",
          "refs_prefix": "## Références",
          "refs_count_format": "Total {count} sources",
          "toc_heading": "## Table des matières",
          "disclaimer_title": "## Avertissement",
          "disclaimer_text": "Ce rapport est basé sur des données publiques et ne constitue pas un conseil en investissement. Certaines données douteuses sont signalées.",
          "confidence_heading": "## Évaluation de Confiance",
          "verdict_labels": {"reliable": "Fiable", "moderate": "Modéré", "caution": "Prudence"},
          "report_generated": "*Rapport généré : {time}*",
          "check": {
              "metadata_pattern": r'> \*\*Métadonnées\*\*:',
              "references_pattern": r'> \*\*Références\*\*',
              "tail_refs": "## Références",
              "tail_disclaimer": "## Avertissement",
              "tail_confidence": "## Évaluation de Confiance",
          }
      },
      
      "de": {
          "metadata_fields": ["Wortanzahl", "Lesezeit", "Daten bis", "Erstellt", "Modus", "Skill-Version"],
          "metadata_label": "**Metadaten**:",
          "references_label": "**Quellen**:",
          "refs_prefix": "## Quellen",
          "refs_count_format": lambda count: f"Insgesamt {count} Quelle" if count == 1 else f"Insgesamt {count} Quellen",
          "toc_heading": "## Inhaltsverzeichnis",
          "disclaimer_title": "## Haftungsausschluss",
          "disclaimer_text": "Dieser Bericht basiert auf öffentlich zugänglichen Daten und stellt keine Anlageberatung dar. Zweifelhafte Daten sind gekennzeichnet.",
          "confidence_heading": "## Vertrauensbewertung",
          "verdict_labels": {"reliable": "Zuverlässig", "moderate": "Mäßig", "caution": "Vorsicht"},
          "report_generated": "*Bericht erstellt: {time}*",
          "check": {
              "metadata_pattern": r'> \*\*Metadaten\*\*:',
              "references_pattern": r'> \*\*Quellen\*\*',
              "tail_refs": "## Quellen",
              "tail_disclaimer": "## Haftungsausschluss",
              "tail_confidence": "## Vertrauensbewertung",
          }
      },
      
      "pt": {
          "metadata_fields": ["Contagem de palavras", "Tempo de leitura", "Dados até", "Gerado em", "Modo", "Versão Skill"],
          "metadata_label": "**Metadados**:",
          "references_label": "**Referências**:",
          "refs_prefix": "## Referências",
          "refs_count_format": "Total de {count} fontes",
          "toc_heading": "## Índice",
          "disclaimer_title": "## Isenção de responsabilidade",
          "disclaimer_text": "Este relatório é baseado em dados públicos e não constitui aconselhamento de investimento. Dados duvidosos estão sinalizados.",
          "confidence_heading": "## Avaliação de Confiança",
          "verdict_labels": {"reliable": "Confiável", "moderate": "Moderado", "caution": "Cautela"},
          "report_generated": "*Relatório gerado: {time}*",
          "check": {
              "metadata_pattern": r'> \*\*Metadados\*\*:',
              "references_pattern": r'> \*\*Referências\*\*',
              "tail_refs": "## Referências",
              "tail_disclaimer": "## Isenção de responsabilidade",
              "tail_confidence": "## Avaliação de Confiança",
          }
      },
      
      "it": {
          "metadata_fields": ["Conteggio parole", "Tempo di lettura", "Dati fino al", "Generato", "Modalità", "Versione Skill"],
          "metadata_label": "**Metadati**:",
          "references_label": "**Fonti**:",
          "refs_prefix": "## Fonti",
          "refs_count_format": "Totale {count} fonti",
          "toc_heading": "## Indice",
          "disclaimer_title": "## Dichiarazione di non responsabilità",
          "disclaimer_text": "Il presente rapporto si basa su dati pubblici e non costituisce consulenza in materia di investimenti. I dati dubbi sono contrassegnati.",
          "confidence_heading": "## Valutazione di Affidabilità",
          "verdict_labels": {"reliable": "Affidabile", "moderate": "Moderato", "caution": "Attenzione"},
          "report_generated": "*Rapporto generato: {time}*",
          "check": {
              "metadata_pattern": r'> \*\*Metadati\*\*:',
              "references_pattern": r'> \*\*Fonti\*\*',
              "tail_refs": "## Fonti",
              "tail_disclaimer": "## Dichiarazione di non responsabilità",
              "tail_confidence": "## Valutazione di Affidabilità",
          }
      },
      
      "nl": {
          "metadata_fields": ["Woordenaantal", "Leestijd", "Gegevens tot", "Gegenereerd", "Modus", "Skill-versie"],
          "metadata_label": "**Metadata**:",
          "references_label": "**Bronnen**:",
          "refs_prefix": "## Bronnen",
          "refs_count_format": "Totaal {count} bronnen",
          "toc_heading": "## Inhoudsopgave",
          "disclaimer_title": "## Vrijwaring",
          "disclaimer_text": "Dit rapport is gebaseerd op openbare gegevens en vormt geen beleggingsadvies. Twijfelachtige gegevens zijn gemarkeerd.",
          "confidence_heading": "## Betrouwbaarheidsbeoordeling",
          "verdict_labels": {"reliable": "Betrouwbaar", "moderate": "Matig", "caution": "Voorzichtig"},
          "report_generated": "*Rapport gegenereerd: {time}*",
          "check": {
              "metadata_pattern": r'> \*\*Metadata\*\*:',
              "references_pattern": r'> \*\*Bronnen\*\*',
              "tail_refs": "## Bronnen",
              "tail_disclaimer": "## Vrijwaring",
              "tail_confidence": "## Betrouwbaarheidsbeoordeling",
          }
      },
      
      "sv": {
          "metadata_fields": ["Ordantal", "Lästid", "Data till", "Skapad", "Läge", "Skill-version"],
          "metadata_label": "**Metadata**:",
          "references_label": "**Källor**:",
          "refs_prefix": "## Källor",
          "refs_count_format": "Totalt {count} källor",
          "toc_heading": "## Innehållsförteckning",
          "disclaimer_title": "## Ansvarsfriskrivning",
          "disclaimer_text": "Denna rapport är baserad på offentligt tillgängliga data och utgör inte investeringsrådgivning. Tveksamma data är markerade.",
          "confidence_heading": "## Förtroendebedömning",
          "verdict_labels": {"reliable": "Tillförlitlig", "moderate": "Måttlig", "caution": "Försiktighet"},
          "report_generated": "*Rapport genererad: {time}*",
          "check": {
              "metadata_pattern": r'> \*\*Metadata\*\*:',
              "references_pattern": r'> \*\*Källor\*\*',
              "tail_refs": "## Källor",
              "tail_disclaimer": "## Ansvarsfriskrivning",
              "tail_confidence": "## Förtroendebedömning",
          }
      },
      
      "ru": {
          "metadata_fields": ["Количество слов", "Время чтения", "Данные до", "Создано", "Режим", "Версия Skill"],
          "metadata_label": "**Метаданные**:",
          "references_label": "**Источники**:",
          "refs_prefix": "## Источники",
          "refs_count_format": "Всего {count} источников",
          "toc_heading": "## Содержание",
          "disclaimer_title": "## Отказ от ответственности",
          "disclaimer_text": "Данный отчет основан на общедоступных данных и не является инвестиционной рекомендацией. Сомнительные данные отмечены.",
          "confidence_heading": "## Оценка достоверности",
          "verdict_labels": {"reliable": "Надёжно", "moderate": "Умеренно", "caution": "Осторожно"},
          "report_generated": "*Отчет создан: {time}*",
          "check": {
              "metadata_pattern": r'> \*\*Метаданные\*\*:',
              "references_pattern": r'> \*\*Источники\*\*',
              "tail_refs": "## Источники",
              "tail_disclaimer": "## Отказ от ответственности",
              "tail_confidence": "## Оценка достоверности",
          }
      },
      
      # ── RTL / non-Latin script languages ──
      
      "ar": {
          "metadata_fields": ["عدد الكلمات", "وقت القراءة", "البيانات حتى", "تاريخ الإنشاء", "الوضع", "إصدار المهارة"],
          "metadata_label": "**البيانات الوصفية**:",
          "references_label": "**المصادر**:",
          "refs_prefix": "## المصادر",
          "refs_count_format": "إجمالي {count} مصدرًا",
          "toc_heading": "## المحتويات",
          "disclaimer_title": "## إخلاء المسؤولية",
          "disclaimer_text": "يعتمد هذا التقرير على بيانات متاحة للعموم ولا يشكل نصيحة استثمارية. تم وضع علامات على البيانات المشكوك فيها.",
          "confidence_heading": "## تقييم الثقة",
          "verdict_labels": {"reliable": "موثوق", "moderate": "معتدل", "caution": "حذر"},
          "report_generated": "*تاريخ إنشاء التقرير: {time}*",
          "check": {
              "metadata_pattern": r'> \*\*البيانات الوصفية\*\*:',
              "references_pattern": r'> \*\*المصادر\*\*',
              "tail_refs": "## المصادر",
              "tail_disclaimer": "## إخلاء المسؤولية",
              "tail_confidence": "## تقييم الثقة",
          }
      },
      
      "hi": {
          "metadata_fields": ["शब्द गणना", "पढ़ने का समय", "डेटा तक", "निर्माण समय", "मोड", "कौशल संस्करण"],
          "metadata_label": "**मेटाडेटा**:",
          "references_label": "**स्रोत**:",
          "refs_prefix": "## स्रोत",
          "refs_count_format": "कुल {count} स्रोत",
          "toc_heading": "## विषय सूची",
          "disclaimer_title": "## अस्वीकरण",
          "disclaimer_text": "यह रिपोर्ट सार्वजनिक डेटा पर आधारित है और निवेश सलाह का गठन नहीं करती है। संदिग्ध डेटा को चिह्नित किया गया है।",
          "confidence_heading": "## विश्वसनीयता मूल्यांकन",
          "verdict_labels": {"reliable": "विश्वसनीय", "moderate": "मध्यम", "caution": "सावधान"},
          "report_generated": "*रिपोर्ट निर्माण समय: {time}*",
          "check": {
              "metadata_pattern": r'> \*\*मेटाडेटा\*\*:',
              "references_pattern": r'> \*\*स्रोत\*\*',
              "tail_refs": "## स्रोत",
              "tail_disclaimer": "## अस्वीकरण",
              "tail_confidence": "## विश्वसनीयता मूल्यांकन",
          }
      },
      
      "vi": {
          "metadata_fields": ["Số từ", "Thời gian đọc", "Dữ liệu đến", "Tạo lúc", "Chế độ", "Phiên bản Skill"],
          "metadata_label": "**Siêu dữ liệu**:",
          "references_label": "**Nguồn tham khảo**:",
          "refs_prefix": "## Nguồn tham khảo",
          "refs_count_format": "Tổng số {count} nguồn",
          "toc_heading": "## Mục lục",
          "disclaimer_title": "## Tuyên bố miễn trách",
          "disclaimer_text": "Báo cáo này được tổng hợp từ dữ liệu công khai và không cấu thành lời khuyên đầu tư. Dữ liệu nghi ngờ đã được đánh dấu.",
          "confidence_heading": "## Đánh giá độ tin cậy",
          "verdict_labels": {"reliable": "Đáng tin cậy", "moderate": "Trung bình", "caution": "Thận trọng"},
          "report_generated": "*Báo cáo tạo lúc: {time}*",
          "check": {
              "metadata_pattern": r'> \*\*Siêu dữ liệu\*\*:',
              "references_pattern": r'> \*\*Nguồn tham khảo\*\*',
              "tail_refs": "## Nguồn tham khảo",
              "tail_disclaimer": "## Tuyên bố miễn trách",
              "tail_confidence": "## Đánh giá độ tin cậy",
          }
      },
      
      "id": {
          "metadata_fields": ["Jumlah kata", "Waktu baca", "Data hingga", "Dibuat", "Mode", "Versi Skill"],
          "metadata_label": "**Metadata**:",
          "references_label": "**Referensi**:",
          "refs_prefix": "## Referensi",
          "refs_count_format": "Total {count} sumber",
          "toc_heading": "## Daftar Isi",
          "disclaimer_title": "## Penyangkalan",
          "disclaimer_text": "Laporan ini disusun berdasarkan data publik dan tidak merupakan saran investasi. Data yang meragukan telah ditandai.",
          "confidence_heading": "## Penilaian Kepercayaan",
          "verdict_labels": {"reliable": "Terpercaya", "moderate": "Sedang", "caution": "Hati-hati"},
          "report_generated": "*Laporan dibuat: {time}*",
          "check": {
              "metadata_pattern": r'> \*\*Metadata\*\*:',
              "references_pattern": r'> \*\*Referensi\*\*',
              "tail_refs": "## Referensi",
              "tail_disclaimer": "## Penyangkalan",
              "tail_confidence": "## Penilaian Kepercayaan",
          }
      },
      
      "th": {
          "metadata_fields": ["จำนวนคำ", "เวลาอ่าน", "ข้อมูลถึง", "สร้างเมื่อ", "โหมด", "เวอร์ชัน Skill"],
          "metadata_label": "**ข้อมูลเมตา**:",
          "references_label": "**แหล่งอ้างอิง**:",
          "refs_prefix": "## แหล่งอ้างอิง",
          "refs_count_format": "รวม {count} แหล่งที่มา",
          "toc_heading": "## สารบัญ",
          "disclaimer_title": "## ข้อจำกัดความรับผิดชอบ",
          "disclaimer_text": "รายงานนี้รวบรวมจากข้อมูลสาธารณะและไม่ถือเป็นคำแนะนำด้านการลงทุน ข้อมูลที่น่าสงสัยถูกทำเครื่องหมายไว้",
          "confidence_heading": "## การประเมินความน่าเชื่อถือ",
          "verdict_labels": {"reliable": "เชื่อถือได้", "moderate": "ปานกลาง", "caution": "ระวัง"},
          "report_generated": "*รายงานสร้างเมื่อ: {time}*",
          "check": {
              "metadata_pattern": r'> \*\*ข้อมูลเมตา\*\*:',
              "references_pattern": r'> \*\*แหล่งอ้างอิง\*\*',
              "tail_refs": "## แหล่งอ้างอิง",
              "tail_disclaimer": "## ข้อจำกัดความรับผิดชอบ",
              "tail_confidence": "## การประเมินความน่าเชื่อถือ",
          }
      },
      
      "tr": {
          "metadata_fields": ["Kelime sayısı", "Okuma süresi", "Veri tarihine kadar", "Oluşturma zamanı", "Mod", "Skill Sürümü"],
          "metadata_label": "**Meta veri**:",
          "references_label": "**Kaynaklar**:",
          "refs_prefix": "## Kaynaklar",
          "refs_count_format": "Toplam {count} kaynak",
          "toc_heading": "## İçindekiler",
          "disclaimer_title": "## Sorumluluk reddi",
          "disclaimer_text": "Bu rapor kamuya açık verilere dayanmaktadır ve yatırım tavsiyesi niteliği taşımaz. Şüpheli veriler işaretlenmiştir.",
          "confidence_heading": "## Güven Değerlendirmesi",
          "verdict_labels": {"reliable": "Güvenilir", "moderate": "Orta", "caution": "Dikkat"},
          "report_generated": "*Rapor oluşturma zamanı: {time}*",
          "check": {
              "metadata_pattern": r'> \*\*Meta veri\*\*:',
              "references_pattern": r'> \*\*Kaynaklar\*\*',
              "tail_refs": "## Kaynaklar",
              "tail_disclaimer": "## Sorumluluk reddi",
              "tail_confidence": "## Güven Değerlendirmesi",
          }
      },
      
      "pl": {
          "metadata_fields": ["Liczba słów", "Czas czytania", "Dane do", "Wygenerowano", "Tryb", "Wersja Skill"],
          "metadata_label": "**Metadane**:",
          "references_label": "**Źródła**:",
          "refs_prefix": "## Źródła",
          "refs_count_format": "Łącznie {count} źródeł",
          "toc_heading": "## Spis treści",
          "disclaimer_title": "## Zrzeczenie się odpowiedzialności",
          "disclaimer_text": "Raport został opracowany na podstawie danych publicznych i nie stanowi porady inwestycyjnej. Wątpliwe dane zostały oznaczone.",
          "confidence_heading": "## Ocena Wiarygodności",
          "verdict_labels": {"reliable": "Godny zaufania", "moderate": "Umiarkowany", "caution": "Ostrożność"},
          "report_generated": "*Raport wygenerowano: {time}*",
          "check": {
              "metadata_pattern": r'> \*\*Metadane\*\*:',
              "references_pattern": r'> \*\*Źródła\*\*',
              "tail_refs": "## Źródła",
              "tail_disclaimer": "## Zrzeczenie się odpowiedzialności",
              "tail_confidence": "## Ocena Wiarygodności",
          }
      },
      
      }  # end _LANG
      
      
      # ── Build LANG_CONFIG by merging base template + per-language overrides ──
      
      LANG_CONFIG = {}
      for code, overrides in _LANG.items():
          base = dict(_BASE_ZH if code == "zh" else _BASE_LATIN)
          # Merge check dict
          if 'check' in overrides:
              merged_check = dict(base.get('check', {}))
              merged_check.update(overrides.pop('check'))
              overrides['check'] = merged_check
          base.update(overrides)
          LANG_CONFIG[code] = base
      
      
      def get_lang_config(lang: str) -> dict:
          """Get language config. Falls back to English for unsupported languages."""
          return LANG_CONFIG.get(lang, LANG_CONFIG["en"])
      
  • LICENSE 1 KB · in bundle
  • profiles.json 344 B
    {
      "quick": {
        "min_chapters": 5,
        "max_chapters": 8,
        "min_paragraphs": 4,
        "max_chars": 15000
      },
      "standard": {
        "min_chapters": 8,
        "max_chapters": 10,
        "min_paragraphs": 5,
        "max_chars": 25000
      },
      "deep": {
        "min_chapters": 10,
        "max_chapters": 12,
        "min_paragraphs": 6,
        "max_chars": 45000
      }
    }
    
  • RULES.md 6.9 KB
    # 质量红线与常见陷阱 — deep-research
    
    所有规则按"在哪拦截"标注,违规即降级。
    
    ---
    
    ## 阶段1 规则
    
    | 规则 | 说明 | 在哪拦截 |
    |:----|:-----|:---------|
    | **章节约束** | quick 5-8 章 / standard 8-10 章 / deep 10-12 章(区间以 profiles.json 的 min_chapters / max_chapters 为准) | 阶段1 大纲 agent |
    | **可搜索性** | 每个子问题必须能通过工具内置引擎(如有)+ 免费源(DuckDuckGo/Semantic Scholar/GDELT/Wikipedia)独立搜索 | 阶段1 大纲 agent |
    | **免费源补强** | 逐子问题质量门未达标时触发 Step 3(与 Step 4 抓取并行,新 URL 追加到下一轮抓取) | 阶段2 Step 3(并行) |
    | **对比视角** | 至少 1 个反方观点子问题 | 阶段1 大纲 agent |
    | **子节结构完整性** | 大纲 agent 必须为每章定义 sections[],deep 3-6 节,standard 2-4 节,quick 1-2 节 | 阶段1 大纲 agent |
    
    ## 阶段2 规则
    
    | 规则 | 说明 | 在哪拦截 |
    |:----|:-----|:---------|
    | **Scrapling 为默认抓取工具** | 安装环节已确保 Scrapling 就绪,Task 2 直接使用 Scrapling 批量抓取;单 URL 失败则回退 webfetch | ⚡ 阻断点(Step 4→Step 5) |
    | **结构化事实** | 数据池使用 JSON facts[] 数组格式,非散装文本 | 阶段2 输出 |
    | **跨源矛盾标记** | 不同来源矛盾数据在 controversies[] 数组中对齐 | 阶段2 数据池 |
    | **来源稀缺** | 连续 3 不同域名 404 → 2 分钟上限 | 阶段2 |
    | **不重复** | 每个新子问题前检查数据池已有内容 | 阶段2 Step 5(数据提取) |
    | **来源多样性硬线** | 补强与 data_limited 均基于逐子问题质量门:子问题 insufficient(可用 URL < 3 / 结果年份早于 target_year-2 / high 优先级无权威域名)单独补搜;insufficient_count ≥ total_sub_questions/3 时标记 data_limited | 阶段2 Step 3 + Step 6 |
    | **数据时序分类** | 每条 fact 必须标注 `data_type`(actual/estimate/forecast)。若该指标的官方发布日在报告生成日之后,自动标记为 estimate 或 forecast | 阶段2 数据池 |
    
    ## 章节与装配规范
    
    | 规则 | 说明 | 在哪拦截 |
    |:----|:-----|:---------|
    | **标题自解释** | 标题不含模拟编号(C1/ch-01 等)。grep 检查 `^##.*ch-\|^##.*C1` | 阶段5 验收 |
    | **子节编号体系分离** | 子节标题(二级 / `###`)不得使用汉字数字编号体系(一、二、三),必须使用阿拉伯数字 x.x 格式(1.1, 1.2…) | 章节 agent prompt + 阶段5 验收 grep `^### [一二三四五六七八九十]` |
    | **时间戳硬编码** | 文件名和报告尾时间必须 `date` 获取 | 阶段5 验收 |
    | **章节 agent 不写最终文件** | 写临时文件,装配 agent 拼装 | 阶段4 Step 2 |
    | **章节默认并行** | 章节撰写优先并行派发(探测到多 agent 工具时);无多 agent 能力的平台自动降级为串行撰写,产物一致,仅耗时略增 | 阶段4 主 agent 循环 |
    | **章节自检** | 章节 agent 写入前必须通过编码洁净、子节编号合规、段落达标、来源可追溯等自检,合格后方可输出 | 阶段4 输出方式 |
    | **跨源一致** | 同一数据在不同章节数值一致 | 阶段5 装配检查 |
    | **三段式顺序** | 报告结构固定为:标题 → 元数据块 → TOC 标题 → 正文各章,不可调换 | 阶段5 验收:第1行 `^# `,第2-6行含元数据和参考来源,之后紧跟语言对应的 TOC 标题 |
    | **参考来源行** | 元数据后另起一行 `> **参考来源/References**:{主要来源} 等/et al. · 共引用 N 个来源/Total N sources`,字段按语言自动适配 | 阶段5 QA 检查 |
    | **报告尾部** | 末尾固定包含参考来源章节(语言对应,如 `## 参考来源`/`## References`)和免责声明(语言对应,如 `## 免责声明`/`## Disclaimer`) | 阶段5 QA 检查末尾 20 行 |
    | **TOC 标题唯一** | 报告有且仅有一个 TOC 标题(语言对应,如 `## 目录`/`## Table of Contents`/`## 목차`) | 阶段5 QA `python dr_tools.py check-toc --lang $LANG` |
    | **乱码零容忍** | data-pool.json 和最终报告均不得含替换字符(\ufffd)或典型 Mojibake 模式或 `???` 连续问号 | Task 2 Step 6 + 阶段5 QA grep |
    | **预测值措辞限定** | 引用 data_type=estimate 或 forecast 的事实,正文必须使用"预估""预计""预测"等限定词,不得作为既定事实陈述。章节 agent 和装配阶段均需检查 | 阶段4 chapter agent prompt + 阶段5 QA |
    | **纯文本公式** | 报告不得使用 LaTeX/math 语法(`$...$` 等),全部用纯文本表达 | 阶段4 prompt + 阶段5 验收 |
    | **可信评估标配** | 报告必须在 `## 参考来源`(语言对应标题)前包含 `## 可信评估`(语言对应),内容由装配阶段 `generate-confidence-section` 命令从 data-pool + manifest 自动聚合生成 | 阶段2b 装配 + 阶段5 QA check_tail |
    | **数据类型行必含** | 可信评估段必须包含「数据类型」行(`**数据类型**` / `**Data Type**`),展示已公布/估算/预测的条数和比例,不得省略 | 阶段5 QA check_tail |
    | **评估意见动态生成** | 评估意见必须引用上方统计数字(实际条数、百分占比),按条件组合判断语句,禁止使用与数字脱节的固定模板 | 阶段5 QA 人工验收 |
    | **路径核验** | 最终报告必须保存在 skill 默认目录或用户自定义目录,两者之一 | 阶段5 QA 检查 |
    | **编码洁净** | 所有中间文件(outline.json / data-pool.json / chapter-*.md)必须使用 UTF-8 无 BOM 编码写入,不得出现替换字符(\ufffd)或 GBK→UTF-8 Mojibake。子 agent 写入前自行校验,不得遗留到主 agent | 阶段1/2/3/5 各输出环节 |
    | **清理** | 调研结束后删除 TMPDIR | 阶段5 Step 4 |
    
    ## 常见陷阱
    
    | 反模式 | 在哪拦截 |
    |:-------|:--------|
    | 数值量级错误(亿/billion 差 10x) | 阶段4 章节 agent 自检 + Task 2 Step 6 |
    | 完美平滑趋势(虚假插值) | 阶段4 章节 agent 自检 |
    | 来源混淆(出货量 vs 零售量) | 阶段4 章节 agent 自检 |
    | 数据不足硬撑行数 | Task 2 Step 6 阻止 + 阶段4 段落数约束 |
    | **Scrapling 被搜索引擎摘要片段替代** | **阶段2 ⚡ 阻断点** |
    | **内部编号泄露到正文**(如出现 "以下是C1的分析") | **阶段4 prompt + 阶段5 grep 检查** |
    | **自定义脚本替代 dr_tools.py**(word-count/check-encoding/assemble-report 等已有命令,不得另写 Python 脚本) | **阶段4/5 主 agent 兜底规则** |
    | **子节使用一、二、三编号**(应与章编号体系分离) | **阶段4 prompt + 阶段5 验收 grep `^### [一二三四五六七八九十]、`** |
    | **章节 agent 写最终文件**(并行竞态) | **阶段4 临时文件方案** |
    | **并行章节数据不一致** | **阶段5 装配一致性检查** |
    | 大纲超约束 | 阶段1 硬约束章节数 |
    
    ---
    ```
    deep-research by hoolulu · github.com/hoolulu/deep-research
    ```
    
  • scrapling-mcp-server.py 6.6 KB
    """
    Scrapling MCP Server (all-platform)
    Exposes: scrapling_bulk_get, scrapling_bulk_stealthy_fetch, scrapling_bulk_fetch
    
    此脚本是一个标准 MCP Server 实现,供 AI 在安装 Scrapling 时参考。
    AI 可以原样使用此脚本,或根据系统环境自行修改后注册到当前工具的 MCP 配置。
    不包含任何硬编码路径,完全由 AI 在注册时动态配置。
    """
    import asyncio
    import json
    import sys
    import traceback
    from typing import Any
    
    import scrapling
    from mcp.server import Server
    from mcp.types import Tool, TextContent
    
    server = Server("scrapling-mcp")
    
    
    @server.list_tools()
    async def list_tools() -> list[Tool]:
        return [
            Tool(
                name="scrapling_bulk_get",
                description="批量抓取网页全文(标准模式,适合大多数网站)",
                inputSchema={
                    "type": "object",
                    "properties": {
                        "urls": {
                            "type": "array",
                            "items": {"type": "string"},
                            "description": "要抓取的 URL 列表",
                        },
                        "timeout": {
                            "type": "number",
                            "description": "超时秒数,默认 12",
                            "default": 12,
                        },
                        "extraction_type": {
                            "type": "string",
                            "description": "提取方式:markdown / html / text,默认 markdown",
                            "default": "markdown",
                        },
                    },
                    "required": ["urls"],
                },
            ),
            Tool(
                name="scrapling_bulk_stealthy_fetch",
                description="批量抓取网页全文(反检测模式,适合有 Cloudflare/WAF 防护的网站)",
                inputSchema={
                    "type": "object",
                    "properties": {
                        "urls": {
                            "type": "array",
                            "items": {"type": "string"},
                            "description": "要抓取的 URL 列表",
                        },
                        "timeout": {
                            "type": "number",
                            "description": "超时秒数,默认 15",
                            "default": 15,
                        },
                    },
                    "required": ["urls"],
                },
            ),
            Tool(
                name="scrapling_bulk_fetch",
                description="批量抓取网页全文(JS 渲染模式,适合需要 JavaScript 执行才能显示内容的页面)",
                inputSchema={
                    "type": "object",
                    "properties": {
                        "urls": {
                            "type": "array",
                            "items": {"type": "string"},
                            "description": "要抓取的 URL 列表",
                        },
                        "timeout": {
                            "type": "number",
                            "description": "超时秒数,默认 15",
                            "default": 15,
                        },
                    },
                    "required": ["urls"],
                },
            ),
        ]
    
    
    def _fetch_single(url: str, timeout: int, method: str) -> dict:
        """Fetch a single URL and return result dict."""
        result = {"url": url, "success": False, "content": "", "error": ""}
        try:
            if method == "get":
                fetcher = scrapling.Fetcher()
                response = fetcher.get(url)
                result["content"] = response.html_content
                result["success"] = True
            elif method == "stealthy":
                try:
                    resp = scrapling.StealthyFetcher.fetch(url, headless=True, timeout=timeout * 1000)
                    result["content"] = resp.html_content
                    result["success"] = True
                except Exception as e:
                    result["error"] = f"{type(e).__name__}: {e}"
            elif method == "dynamic":
                try:
                    resp = scrapling.DynamicFetcher.fetch(url, headless=True, timeout=timeout * 1000)
                    result["content"] = resp.html_content
                    result["success"] = True
                except Exception as e:
                    result["error"] = f"{type(e).__name__}: {e}"
        except Exception as e:
            result["error"] = f"{type(e).__name__}: {e}"
        return result
    
    
    def _bulk_fetch(urls: list[str], timeout: int, method: str) -> list[dict]:
        results = []
        for url in urls:
            r = _fetch_single(url, timeout, method)
            results.append(r)
        return results
    
    
    @server.call_tool()
    async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
        urls = arguments.get("urls", [])
        timeout = arguments.get("timeout", 12)
        extraction_type = arguments.get("extraction_type", "markdown")
    
        if not urls:
            return [TextContent(type="text", text=json.dumps({"error": "urls is required"}))]
    
        try:
            if name == "scrapling_bulk_get":
                raw_results = _bulk_fetch(urls, timeout, "get")
            elif name == "scrapling_bulk_stealthy_fetch":
                raw_results = _bulk_fetch(urls, timeout, "stealthy")
            elif name == "scrapling_bulk_fetch":
                raw_results = _bulk_fetch(urls, timeout, "dynamic")
            else:
                return [TextContent(type="text", text=json.dumps({"error": f"unknown tool: {name}"}))]
    
            output = []
            for r in raw_results:
                entry = {
                    "url": r["url"],
                    "success": r["success"],
                }
                if r["success"]:
                    entry["content_length"] = len(r["content"])
                    entry["content_preview"] = r["content"][:500]
                    entry["content"] = r["content"]
                else:
                    entry["error"] = r["error"]
                output.append(entry)
    
            return [TextContent(
                type="text",
                text=json.dumps({
                    "tool": name,
                    "total_urls": len(urls),
                    "success_count": sum(1 for r in raw_results if r["success"]),
                    "fail_count": sum(1 for r in raw_results if not r["success"]),
                    "results": output,
                }, ensure_ascii=False, indent=2)
            )]
        except Exception as e:
            tb = traceback.format_exc()
            return [TextContent(type="text", text=json.dumps({
                "error": str(e),
                "traceback": tb,
            }))]
    
    
    async def main():
        from mcp.server.stdio import stdio_server
        async with stdio_server() as (read_stream, write_stream):
            await server.run(
                read_stream,
                write_stream,
                server.create_initialization_options(),
            )
    
    
    if __name__ == "__main__":
        asyncio.run(main())
    
  • SKILL.md 34.7 KB
    ---
    name: deep-research
    description: "Professional deep research report generation — multi-agent collaboration with parallel chapter writing, automatic latest-data targeting, multilingual output, and built-in quality checks."
    license: MIT
    metadata:
      version: "6.0.0"
      updated: "2026-08-28"
      risk: medium
      author: hoolulu
      repository: https://github.com/hoolulu/deep-research
    ---
    
    
    # deep-research
    
    生成对标券商/第三方研究机构标准的深度调研报告。
    
    - **架构**:主 agent 调度 4 个子 agent Task(大纲/数据/预检/装配)+ 1 轮主控并行派发章节,中间数据走临时文件
    - **数据源**:在线模式 → 工具内置引擎(Layer 0,如有)+ 大纲建议源定向搜索(Layer 1) + sources.json 优质源搜索(Layer 2)并行 → 按质量触发免费源补强(Layer 3 兜底)→ Scrapling 批量抓取;离线模式 → 用户指定的本地文件(md/txt/pdf/docx)
    - **安装**:见下方「安装与配置」
    - **输出**:`$TMPDIR/outline.json`(临时,非最终报告)
    - **最终报告**:保存到 skill 目录下的 `reports/`
    - **参考文件**:`RULES.md`(硬约束/反模式)、`TYPES.md`(分类标准/编号规范)、**`profiles.json`(三档模式参数,修改后重启软件即全局生效)**
    - **容错原则**:调研不阻塞。所有脚本/命令调用必须有兜底路径。主路径失败 → 自动尝试替代方案(换 `sys.executable`/检查路径/直接 Python 实现)→ 三次失败后向用户报告具体问题。详见「容错原则」。
    
    ---
    
    ## 0. 支付级质量标准(所有 Task 共用,缺任何一项即降级)
    
    | # | 标准 | 说明 |
    |---|------|------|
    | 1 | **结论先行** | 每章以 `> 引用格式` 核心判断开头 |
    | 2 | **来源可追溯** | 每个数字标注(机构,年份) |
    | 3 | **反方视角** | 至少 1 处呈现争议或反对观点 |
    | 4 | **三层深度** | 事实层 → 因果层 → 判断层 |
    | 5 | **零套话** | 无"近年来""值得注意的是"等填充词 |
    | 6 | **标题含判断** | "格局:高度集中"✅ \| "行业概况"❌ |
    | 7 | **可自包含** | 首章必须定义核心概念 |
    | 8 | **无内部编号** | 正文无任何流程编号,标题自解释 |
    | 9 | **时间戳正确** | 文件名和报告尾时间必须 `date` 命令获取 |
    | 10 | **目录源自大纲** | 目录从 outline.json 的第一级章节生成,不从正文提取 |
    | 11 | **强制目录** | 报告正文前必须包含 `## 目录` 标题及自动目录(TOC),列出所有章节标题 |
    | 12 | **元数据完整** | 报告头部必须包含 总字数、阅读时间、数据截至日期(精确到月)、报告生成具体时间(精确到秒)、调研模式、Skill版本 六个字段,用 ` · ` 隔开。另起一行 `> **参考来源**:{主要来源} 等 · 共引用 N 个来源`。报告末尾须附 `## 参考来源`(列出所有引用机构及链接)和 `## 免责声明`。版本号从本 skill 的 VERSION 文件读取。 |
    | 13 | **篇幅达标** | 见项目根目录 [`profiles.json`](profiles.json)。所有模式限制以 `profiles.json` 为准,修改后重启软件即全局生效。 |
    | 14 | **四段式结构** | 顺序固定为:报告标题 → 元数据块(含六字段 + 参考来源行) → `## 目录` → 正文各章 → 尾部(参考来源 + 免责声明) |
    | 15 | **编码洁净** | 所有中间文件(outline.json / data-pool.json / chapter-*.md)必须使用 **UTF-8 无 BOM** 编码写入,不得出现替换字符(\ufffd)或 GBK→UTF-8 Mojibake。子 agent 在写入前必须自行验证编码洁净,不得将编码问题遗留到主 agent |
    | 16 | **纯文本公式** | 报告中不得使用 LaTeX/math 公式语法(`$...$`、`$$...$$`、`\[...\]` 等)。公式必须用纯文本或 Unicode 符号表达,确保复制到任何编辑器都不产生渲染问题 |
    
    ### 时间锚定规则
    
    所有主题默认以 `{CURRENT_YEAR}` 为目标搜索最新数据。时间锚定模式在 Task 1 中由大纲 agent 按以下规则判定:
    
    | 模式 | 符号 | 判定条件 | target_year | 验收 |
    |:----|:-----|:---------|:-----------|:-----|
    | `latest`(默认) | ⏳ | **所有主题的默认值**,除非符合 relaxed 或 user_specified | `{CURRENT_YEAR}` | 严格:≥50% 数据来自当年/前一年 |
    | `relaxed`(放宽) | 🔓 | 指南/教程/概念类主题,或用户问历史/原理("草书发展""起源""背景") | `{CURRENT_YEAR}` | 宽松:标记旧数据但不过滤 |
    | `user_specified` | 📌 | 用户提问显式指定了年份/月份("2025年""2026Q1""2020年至今") | **用户的指定年份** | 硬约束:>50% 匹配用户指定时间 |
    
    > `{CURRENT_YEAR}` 是动态变量,运行时通过 `date +%Y` 解析,无需手动修改。
    
    ---
    
    ## 1. 主 agent 调度流程
    
    **⚠️ CRITICAL — DO NOT SPEAK BEFORE LANGUAGE DETECTION**
    > Your VERY FIRST action (before anything else) must be: detect language → set `$LANG`.
    > Output NOTHING to the user until `$LANG` is set — no thinking aloud, no status messages.
    > After language is detected, ALL output must be in `$LANG`. Period.
    >
    > **IMPORTANT: Clean the topic before detection** — the user input may contain framework wrapper text (e.g. "请使用... skill 执行...用户输入如下:"). Strip all wrapper text and pass ONLY the clean research topic. For example from "请使用...用户输入如下:Quantum computing market outlook -quick" extract only "Quantum computing market outlook".
    
    ```
    你(主 agent)的完整流程:
    
    ══ Setup (必须先执行) ══
    
     → 创建一个带时间戳的临时目录作为 TMPDIR(例如系统临时目录下的 deep-research-YYYYMMDD-HHMMSS)
     → 同时确定 TOOLSDIR(本 skill 的 tools/ 目录)、PROMPTSDIR(本 skill 的 prompts/ 目录)、SKILLDIR(本 skill 的根目录)
     → 读取本 SKILL.md + RULES.md + TYPES.md
    
    ══ Step 0 — Language Detection (output nothing before detection) ══
    
     → Clean topic: strip wrapper text, keep only the user's actual research topic
     → Determine language: analyze the cleaned topic and pick the ISO 639-1 code:
       zh (Chinese), en (English), ja (Japanese), ko (Korean), ru (Russian),
       ar (Arabic), hi (Hindi), vi (Vietnamese), th (Thai), tr (Turkish),
       es (Spanish), fr (French), de (German), pt (Portuguese), it (Italian),
       nl (Dutch), sv (Swedish), pl (Polish), id (Indonesian)
       → If unsure, default to "en".
       → Do NOT output anything during this step.
     → Write language code: use `write` tool to create {TMPDIR}/language.txt with the ISO code
     → Set `$LANG` = language code from the step above
       → **从这一行开始,所有面向用户的输出必须使用 $LANG 语言(不在 $LANG 列表中时默认 en)。SKILL.md 的指令文本不论用什么语言写的,只是供你阅读的上下文;实际输出以 $LANG 为准——你是读到中文指令后意识上翻译成 $LANG 再输出。**
       → Announce detected language to the user (single line, in $LANG, e.g. "🌐 Language detected: en")
    
    ### 🔔 语言自查清单(每次输出前执行)
    
    ```
    ☐ {TMPDIR}/language.txt 的值 = 我的 $LANG?
    ☐ 我正准备输出的这一句/这段,是 $LANG 吗?
    ☐ todo 条目是 $LANG 吗?
    ☐ 给用户的进度通知是 $LANG 吗?
    ☐ 我是否在无意识中用了指令文件的语言(如中文)而非 $LANG?
    如果任一答案为"否"→ 立即改写为 $LANG 再输出。
    ```
    **硬规则**:派发子任务/子 agent 时(如有多 agent 工具),其 prompt 中的 `{LANG}` 必须是你检测到的语言代码。子任务输出的语言由你负责保证。
    
    ══ 主流程 ══
    
     1. ══ 离线模式判定(Step 0.5) ══
        → 你已经读取了用户原始输入。用自然语言理解判断用户关于数据来源的意图,不要用关键词匹配:
          - 用户是否提到了本地文件/目录/资料?
          - 用户是否明确要求不要联网?
          - 用户是否明确要求联网补充?
        → 判断逻辑:
          - 提到本地文件 +(未说联网 / 不联网)→ 离线模式,跳过搜索
          - 提到本地文件 + 说"联网补充" → 正常流程(搜+读本地)
          - 未提本地文件 → 正常流程
        → 离线模式 + 有路径 → {TMPDIR}/offline_mode.txt,`offline_mode=true`,向用户报告单行说明
        → 离线模式 + 无路径 → 回复用户询问路径,不继续
        → 正常模式 → `offline_mode=false`
    
       → **模式解析**:从清洗后的主题中提取调研模式
         - 主题末尾是 ` -quick` → `$DEPTH_MODE=quick`,去除该后缀
         - 主题末尾是 ` -deep` → `$DEPTH_MODE=deep`,去除该后缀
         - 无上述后缀 → `$DEPTH_MODE=standard`(默认)
    
     2. 记录任务开始时间到 {TMPDIR}/start_time.txt
     3. 创建任务清单(使用 `todo_write` 或当前环境等价工具;无则用文本记录),使用 $LANG 语言
      4. ══ Task 1 — 分析主题 + 生成大纲 ══
         → 读取 {PROMPTSDIR}/task1_outline.md,替换 {TMPDIR} {TOOLSDIR} {LANG} {CURRENT_YEAR} {MODE},注入 prompt
         → **只做变量替换,不添加语言、格式、报告结构等额外指令。语言已由 Step 0 判定为 $LANG 并在 prompt 中替换 {LANG}。**
         → 派发子任务(若有多 agent 工具则用之;否则由你本人直接执行本任务),等待完成
         → 用 `read` 确认 {TMPDIR}/outline.json 存在
         → 从 outline.json 读取 title + chapter_count + depth_mode
        → 任务清单标记完成
        → 向用户报告进度(使用 $LANG 语言)
      6. ══ Task 2 — 数据收集 + 结构化数据池 ══
         → 读取 {PROMPTSDIR}/task2_data_collection.md
         → 替换标准变量 {TMPDIR} {TOOLSDIR} {LANG} {COUNTRY}
         → 如果 `offline_mode=true`,额外替换:
           {OFFLINE_MODE} → true
           {LOCAL_PATHS} → 读取 {TMPDIR}/offline_mode.txt 的内容(路径列表)
         → 如果 `offline_mode=false`,替换 {OFFLINE_MODE} → false,{LOCAL_PATHS} → 空字符串
         → 派发子任务(若有多 agent 工具则用之;否则由你本人直接执行本任务),等待返回
        → 如失败(子任务报错或 task2_manifest.json 不存在),**自动重试 1 次**,重新派发。第二次仍失败则向用户报告并终止
         → 读取 {TMPDIR}/task2_manifest.json,提取 source_count + fact_count + search_engine + fetch_method + engines + free_fallback + english_fallback + unique_domains
        → 任务清单标记完成
        → 向用户报告进度(使用 $LANG 语言)
         7. ══ Task 3 — 派发章节撰写 ══
         → 读取 {TMPDIR}/outline.json 获取 chapters 数组;读取 {TMPDIR}/data-pool.json
         → **读取 `profiles.json` 获取当前模式的 `max_chars`**,计算 `per_chapter_chars = max_chars ÷ chapters.length`
         → 从 data-pool.json 提取所有唯一 (src, yr) 组合,按首次出现顺序预分配引用编号 [1], [2], [3]...,写入 {TMPDIR}/citation_map.json
         → 读取 `{PROMPTSDIR}/task3_chapter_agent.md` 模板
         → **根据 $LANG 裁剪 prompt 中的多语言段落**:
           - prompt 中的 `[LANG_en]` 段落:仅当 $LANG=en 时保留,其他语言删除
           - prompt 中的 `[LANG_zh]` 段落:仅当 $LANG=zh 时保留,其他语言删除
           - 删除标记文本本身(`[LANG_en]` `[/LANG_en]` 占位符行)
           - 无标记的段落全部语言通用,保留
          → **撰写方式(所有平台通用,默认并行、无多 agent 时串行)**:
            - **章节执笔者不做任何工具调用**(不跑 prepare-chapter、validate、manifest、word-count),只写文件
            - 探测当前环境是否存在多 agent / 子任务工具(如 `task`、`subagent`、`Task`、`agent` 等):
              - **有** → 为每章逐一构建 prompt,**并行**派发全部章节子任务,全部完成后进入 Round 2
              - **无** → 由你本人按章节顺序**逐一撰写**每章正文并落盘,全部写完后直接进入 Round 2
            - **不得因为无法并行派发而中断调研**:串行撰写与其余步骤完全等价,仅耗时略增
    
          → **并行派发章节**(仅当存在多 agent 工具时):
           - 初始化空列表 task_ids = []
           - For N = 1 to chapters.length:
             - 读取 outline.chapters[N] 的 title、sections
             - 从 data-pool.json 中筛选该章 sub_questions 对应的事实条目
             - **将事实直接嵌入 prompt**:每条事实前标注预分配的 `[N]` 编号
             - 用多 agent 工具的后台/并行模式派出每章
             - 记录每章子任务的句柄/ID 到 task_ids(无句柄可记录时,以章节文件路径为追踪依据)
             - 任务清单标记该章 in_progress
           - 将 task_ids 写入 {TMPDIR}/task3_bg_ids.json(持久化,防止主 agent 中断后丢失状态)
           - 向用户报告:"已并行派出 {N} 章,等待全部完成..."(使用 $LANG 语言)
           - 等待全部章节完成(本轮结束后收到全部完成提示再继续;单章完成通知可忽略)
           - 然后进入 Round 2:
    
           **Round 2 — 收集结果 + 失败重写**:
           - 读取 {TMPDIR}/task3_bg_ids.json 获取所有 task 句柄
           - 对每个子任务收集其章节结果(或直接读取产物文件)
           - 用 `read` 逐一确认 {TMPDIR}/chapters/chapter-{N}.md 是否存在且非空
           - 如果有章节缺失或内容为空:
             - 记录失败章节编号列表
             - **串行重写**:对每个失败章节逐一(同步)重新派发/执笔,等待完成
             - 再次用 `read` 确认
           - 任务清单标记每章 completed
           - 向用户报告最终章节完成情况(使用 $LANG 语言)
         8. ══ Task 4 — 验证 + 装配 + QA(**主 agent 直接执行**) ══
         → **Step 0 — 清理残留**:删除 {SKILLDIR}/reports/ 目录下所有 0 字节文件(前次装配失败的空壳);创建 {SKILLDIR}/reports/$LANG/ 子目录(如果不存在)
         → **Step 1 — 批量验证**:`python {TOOLSDIR}/dr_tools.py validate-all-chapters --chapters-dir {TMPDIR}/chapters/ --chapters {chapter_count}`,内部 ThreadPoolExecutor 并行验证所有章节。从输出 JSON 的 `failed_chapters` 中找到失败章节,逐个重新生成(重新派发章节 agent → 重新验证该章)。
         → **Step 1b — 章节深度均衡检查**:`python {TOOLSDIR}/dr_tools.py depth-balance --chapters-dir {TMPDIR}/chapters/ --chapters {chapter_count}`。如果某章行数 < 平均值的 50%,标记告警(not blocking,仅提示)。
         → Step 1 或 Step 2 失败时,**先删除本次已写入的产物**(报告文件、中间文件等),再重新执行对应步骤,避免残留文件干扰下次运行
         → **Step 2 — 装配**:`python {TOOLSDIR}/dr_tools.py assemble-report --outline {TMPDIR}/outline.json --chapters-dir {TMPDIR}/chapters/ --datapool {TMPDIR}/data-pool.json --mode {depth_mode} --target-year {target_year} --output {SKILLDIR}/reports/$LANG/ --lang $LANG`
        → **$REPORT 提取**:从装配输出中提取 `Report assembled: ...` 行中冒号后的第一个路径,设为 `$REPORT` 变量
         → **Step 2b — 可信评估(数据层)**:`python {TOOLSDIR}/dr_tools.py generate-confidence-section --datapool {TMPDIR}/data-pool.json --manifest {TMPDIR}/task2_manifest.json --report "$REPORT" --lang $LANG`
           从输出中解析 `CONFIDENCE:` 行获取 `conf_coverage`、`conf_total_facts`、`conf_high_pct`、`conf_medium_pct`、`conf_low_pct`、`conf_actual_pct`、`conf_est_pct`、`conf_fct_pct`、`conf_auth_pct`、`conf_data_limited`、`conf_controversies`、`conf_adequate_subq`、`conf_total_subq`、`conf_score` 共 14 个变量。
         → **Step 2c — 可信评估(LLM判断)**:使用上一步的 14 个统计变量 + 报告标题(从 outline.json 读取)在 LLM 上下文内直接生成定性评估意见。
            - 输出必须使用 $LANG 语言,2-4 句,纯定性判断,不重复逐项明细中的具体数字
            - **语气校准(重要)**:本工具是开源信息综合项目,非付费研究报告。评估意见应遵循以下原则:
              - **总分决定基调**:score≥75 → 正面肯定为主;50-74 → 中性平衡;<50 → 温和提醒
              - **不说"缺陷""不足""未能"等负面措辞** → 改为"可进一步关注的方面""仍有补充空间"
              - **不说"无法获取""受限于"** → 改为"部分高频量化指标因商业敏感性未纳入公开讨论范围"
              - **不自我贬低**:不出现"门槛高""可信度大打折扣"等损害报告公信力的表述
              - **正面收尾**:最后一句必须是肯定整体参考价值的结论
              - **定位准确**:强调"综合公开信息形成的参考判断"而非"严谨学术研究"
            - 写出到 `{TMPDIR}/llm_assessment.txt`
            - 用 `edit` 工具将评估意见插入到报告可信评估区的 `**{综合评级标签}**` 行之后(追加 "**{评估意见标签}**:\n\n{文本}"),然后用 `read` 确认插入正确
            - `{综合评级标签}` 和 `{评估意见标签}` 使用语言映射表中的翻译
         → **Step 3 — 数据受限处理**:读取 {TMPDIR}/task2_manifest.json 的 `data_limited` 字段。如果为 true,在报告标题后插入数据说明声明,**使用 $LANG 语言**。
        → **Step 4 — 引用处理**:`python {TOOLSDIR}/dr_tools.py convert-citations --datapool {TMPDIR}/data-pool.json "$REPORT" --lang $LANG`(从 data-pool 构建参考章节,验证正文 `[N]` 引用均有对应条目)
        → **Step 4b — 货币符号转义**:`python {TOOLSDIR}/dr_tools.py escape-currency "$REPORT"`(将 `$` 转义为 `\$`,避免被知乎/Obsidian/Typora 等渲染器错误解析为 LaTeX math mode)
          → **Step 5 — QA**:`python {TOOLSDIR}/dr_tools.py qa-report "$REPORT" --mode {depth_mode} --target-year {target_year} --lang $LANG`,解析 JSON 输出,从 `checks.word_count.count` 取字数,从 `checks.word_count.limit` 取上限
         → **Step 6 — 更新本地报告列表页**:`python {TOOLSDIR}/generate_pages.py --local`(刷新 reports-browser/index.html,将 reports/ 下所有报告打包为嵌入 JS 的可浏览页面)——需在 `{SKILLDIR}` 目录下执行,bash 命令须加 `workdir="{SKILLDIR}"` 参数
         → 任务清单标记完成
        → ⏱ **强制计算总耗时**(读取 start_time.txt + 当前时间算差值)
        → 从 outline.json + task2_manifest.json + qa-report 中提取数据,使用 $LANG 语言汇报最终结果。
    
          **语言自适应标签映射表**(以下所有 <词> 根据 $LANG 替换):
    
          | 中文 | en | ja | ko | fr | de | es | 其余语言 |
          |------|----|----|----|----|----|----|---------|
          | 执行总结 | Execution Summary | 実行サマリー | 실행 요약 | Résumé exécutif | Zusammenfassung | Resumen ejecutivo | Execution Summary |
          | 阶段 | Stage | 段階 | 단계 | Phase | Phase | Fase | Stage |
          | 详情 | Detail | 詳細 | 세부 | Détail | Detail | Detalle | Detail |
          | 大纲/Plan | Plan | 概要 | 개요 | Plan | Plan | Plan | Plan |
          | 观点速览/Insight | Insight | 洞察 | 인사이트 | Aperçu | Einblick | Perspectiva | Insight |
          | 数据/Data | Data | データ | 데이터 | Données | Daten | Datos | Data |
          | 报告/Report | Report | レポート | 보고서 | Rapport | Bericht | Informe | Report |
          | 章 | ch | 章 | 장 | chap. | Kap. | cap. | ch |
          | 来源 | sources | ソース | 출처 | sources | Quellen | fuentes | sources |
          | 事实 | facts | 事実 | 사실 | faits | Fakten | datos | facts |
          | 独立域名 | domains | ドメイン | 도메인 | domaines | Domains | dominios | domains |
    | 行 | lines | 行 | 줄 | lignes | Zeilen | líneas | lines |
    | 字 | chars | 語 | 단어 | mots | Wörter | palabras | chars |
          | 分钟 | min | 分 | 분 | min | Min. | min | min |
          | 生成时间 | Generated | 生成時刻 | 생성 시간 | Généré le | Erzeugt | Generado | Generated |
          | 搜索 | Search | 検索 | 검색 | Recherche | Suche | Búsqueda | Search |
    | 数据充足 | Adequate | 十分 | 충분 | Suffisantes | Ausreichend | Adecuado | Adequate |
    | 数据受限 ⚠ | Limited ⚠ | 制限 ⚠ | 제한 ⚠ | Limitées ⚠ | Eingeschränkt ⚠ | Limitado ⚠ | Limited ⚠ |
    | 可信评估 | Confidence | 信頼性評価 | 신뢰도 평가 | Évaluation de confiance | Vertrauensbewertung | Evaluación de confianza | Confidence |
    | 覆盖充足/部分覆盖/覆盖不足 | Full/Partial/Limited coverage | 完全/部分/不足カバー | 충분/부분/부족 | Couverture complète/partielle/limitée | Vollständige/Teilweise/Eingeschränkte Abdeckung | Cobertura completa/parcial/limitada | Adequate/Partial/Limited |
    | 统计 | Stats | 統計 | 통계 | Statistiques | Statistiken | Estadísticas | Stats |
    | 综合评级 | Rating | 総合評価 | 종합 평가 | Note globale | Gesamtbewertung | Calificación general | Rating |
    | 评估意见 | Assessment | 評価意見 | 평가 의견 | Avis d'évaluation | Bewertung | Opinión de evaluación | Assessment |
    | 耗时 | Duration | 所要時間 | 소요 시간 | Durée | Dauer | Duración | Duration |
          | 免费源补强 | free fallback | 無料補強 | 무료 보강 | sources gratuites | kostenlose Quellen | fuentes gratuitas | free fallback |
          | 本地文件 | local files | ローカル | 로컬 파일 | fichiers locaux | lokale Dateien | archivos locales | local files |
    
          **搜索策略描述拼接规则**(使用映射表中的翻译):
    
          ```
          IF offline_mode=true:
            <搜索词>:{offline_$LANG}
          ELSE:
            engines_names = engines 数组元素大写(["websearch"] → "Websearch")
            desc = engines_names
            IF free_fallback=true: desc += " (+{free_fallback_$LANG})"
            IF english_fallback=true: desc += " (+EN)"
            <搜索词>:{desc}
          ```
    
          **数据质量徽标规则**:
    
          ```
          IF data_limited=true: <质量词> = {limited_$LANG}
          ELSE: <质量词> = {adequate_$LANG}
          ```
    
          严格按以下结构输出:
    
          ```
          📊 **<执行总结词>**
    
          | <阶段词> | <详情词> |
          |:----|:------|
          | 📋 <Plan词> | {outline.title} · {outline.chapter_count} <章词> · {outline.depth_mode} |
          | 🎯 <Insight词> | {outline.chapters[0].description} |
           | 📡 <Data词> | {task2_manifest.source_count} <来源词> · {task2_manifest.unique_domains} <独立域名词> · {task2_manifest.fact_count} <事实词> · <搜索词>:{search_desc} · {task2_manifest.fetch_method} |
           | 📄 <Report词> | {REPORT} |
           | 🌐 <浏览器词/Report List> | {SKILLDIR}/reports-browser/index.html |
           | ✅ <可信评估词> | <覆盖_{coverage_summary}> · 高置信{conf_high_pct}% · 已公布{conf_actual_pct}% · {conf_score}/100 · {data_quality_badge} → {llm_verdict} |
           | 📊 <统计词> | {qa_report.line_count} <行词> · {qa_report.word_count} <字词> · <耗时词>⏱ {totalMin} <分钟词> · <生成时间词>:{gen_time} |
          ```
    
          其中:
          - `{outline.chapters[0].description}` = 从 outline.json 读取第 1 章(核心观点)的 description 字段,作为观点速览摘要
          - `{gen_time}` = 读取 {TMPDIR}/start_time.txt 中的任务开始时间,格式化为 `YYYY-MM-DD HH:mm:ss`
           - `{REPORT}` 仅输出最终报告路径(`{SKILLDIR}/reports/{LANG}/xxx.md`),不包含任何 TMPDIR 中间路径
          - `{search_desc}` = 按搜索策略拼接规则生成,所有中文词根据 $LANG 翻译
           - `{data_quality_badge}` = 按数据质量徽标规则生成
           - `<覆盖_{coverage_summary}>` = 从 `task2_manifest.coverage_summary` 读取(adequate/partial/insufficient),用语言映射表中"覆盖充足/部分覆盖/覆盖不足"行对应翻译替换
           - `{conf_high_pct}`、`{conf_actual_pct}`、`{conf_score}` = 从 Step 2b 的 `CONFIDENCE:` 行解析对应的字段
           - `{llm_verdict}` = 读取 Step 2c 写入的 `{TMPDIR}/llm_assessment.txt` 完整内容
        → 任务清单全部完成
    
    **禁止**:主 agent 不得在 Task 调度之间自行执行搜索引擎调用或数据处理。搜索/抓取归 Task 2,大纲生成归 Task 1,章节撰写归 Task 3,装配验证归 Task 4。Task 间的 handoff 文件读取(outline.json、task2_manifest.json 等)不受此限。
    
    ---
    
    ## 2. Task 1 — 主题分析 + 大纲
    
    **工具**:多 agent / 子任务工具(如有)| 一次调用;无则主 agent 直接执行本任务
    **prompt 文件**:`prompts/task1_outline.md`
    **用法**:读取文件内容,替换 `{TMPDIR}` `{TOOLSDIR} {LANG} {CURRENT_YEAR}` 为实际值后注入 prompt。
    
    **输出**:大纲 agent 直接用 `write` 工具创建 `{TMPDIR}/outline.json`。主 agent 通过 `read` 确认文件存在。
    
    ---
    
    ## 3. Task 2 — 数据收集 + 结构化数据池
    
    **工具**:多 agent / 子任务工具(如有,建议用高并发/高思考 budget 分类);无则主 agent 直接执行本任务
    **prompt 文件**:`prompts/task2_data_collection.md`
    **用法**:读取文件内容,替换 `{TMPDIR}` `{TOOLSDIR}` `{LANG}` `{COUNTRY}` `{OFFLINE_MODE}` `{LOCAL_PATHS}` 为实际值后注入 prompt。
    
    **输出**:{TMPDIR}/data-pool.json + {TMPDIR}/task2_manifest.json(使用 `write` 工具创建)
    
    **LANG→COUNTRY 映射**(用于替换 `{COUNTRY}`):zh→CN, en→US, ru→RU, ja→JP, ko→KR, fr→FR, de→DE, es→ES, pt→PT, it→IT, nl→NL, sv→SE, pl→PL, id→ID, th→TH, tr→TR, vi→VN, ar→SA, hi→IN。未覆盖的语言用空字符串。
    
    ---
    
    ## 3. Task 3 — 章节撰写 & 章节 agent 指令模板
    
    主 agent 为每章准备子任务 prompt 时,参数使用 `{PROMPTSDIR}/task3_chapter_agent.md` 模板。
    
    **用法**:读取 `{PROMPTSDIR}/task3_chapter_agent.md`,替换以下变量:
    - `[章节 title]` → 当前章的 title(从 outline.json chapters 数组读取)
    - `[N]` → 当前章节编号(第 1 章为 1,第 2 章为 2...)
    - `[total]` → chapters 数组总长度
    - `[sections 列表]` → 当前章的 sections 数组(逗号分隔)
    - `{per_chapter_chars}` → `profiles.json` 中当前模式的 `max_chars ÷ 总章数`(主 agent 一次性算好)
    - `{min_paragraphs}` → `profiles.json` 中当前模式的 `min_paragraphs`
    - `{调研模式}` → quick / standard / deep(当前调研模式)
    - `{TMPDIR}` → 运行时临时目录
    - `{TOOLSDIR}` → tools 目录
    
    **输出**:{TMPDIR}/chapters/chapter-{N}.md(使用 `write` 工具创建)
    
    ---
    
    ## 4. Task 4 — 验证 + 装配 + QA(主 agent 直接执行)
    
    装配、引用转换、QA 检查均由主 agent 通过 bash 命令直接执行 `{TOOLSDIR}/dr_tools.py` 完成:
    
    1. `validate-all-chapters` → 批量结构验证(并行)
    2. `assemble-report` → 生成报告
    3. `generate-confidence-section` → 可信评估(从 data-pool + manifest 聚合生成)
    4. `convert-citations` → 引用转换
    5. `escape-currency` → 货币符号转义
    4. `qa-report` → 质量检查
    
    **清理**:装配完成后主 agent 执行 `Remove-Item -Recurse -Force "{TMPDIR}"` 清理临时文件。
    
    ---
    
    ## 6. 输出文件管理
    
    ### 路径优先级
    
    最终报告保存路径按以下优先级判定:
    
    1. **用户自定义路径** — 如果用户显式指定了输出目录(如 `D:\Reports\`),使用指定路径
    2. **Skill 默认路径** — `{SKILLDIR}/reports/`(skill 根目录下的 reports/)
    
    装配阶段(Step 3)根据实际使用的路径写入,文件名格式不变:`<主题>-YYYYMMDD-HHmmss.md`。
    
    ### QA 路径核验
    
    Step 4 QA 必须确认报告文件的保存路径为上述两者之一,如果路径不属于默认目录且非用户指定目录,标记"路径异常"不通过。
    
    ### 日期锚定
    
    文件名中的日期用当前年月日。
    
    ### 清理机制
    
    ```
    Task 4 装配 + QA 通过后,内部已完成清理:
    1. 清理中间文件 `{TMPDIR}` 目录:Windows 用 `Remove-Item -Recurse -Force "{TMPDIR}"`,Linux 用 `rm -rf {TMPDIR}`
    2. 确认 tool-output/ 无残留
    ```
    
    ---
    
    ## 7. 工具依赖速查
    
    | 能力 | 用途 | 免费? | 国内源? |
    |:----|:-----|:-----:|:--------:|
    | 内置搜索(探测) | **主力**搜索引擎(当前工具的内置搜索,如 `websearch`/`web_search`,运行时探测) | 取决于环境 | 视引擎而定 |
    | 其他搜索引擎 | 不同工具的内置搜索(运行时自动适配) | 取决于环境 | — |
    | Scrapling MCP | 全文抓取(MCP,需注册到当前工具的 MCP 配置) | ✅ | **✅ 推荐,国内源主力** |
    | webfetch / fetch | 抓取回退(Scrapling 不可用时替代) | ✅ | ❌ 远端受限,国内源效果一般 |
    | bash / 终端 | date 时间戳 / 文件操作 / 运行 Python | ✅ | — |
    | write | 写文件 | ✅ | — |
    
    搜索策略由 agent 在运行时根据工具集**自动适配**,不依赖预设的搜索引擎配置。
    
    **搜索链路**:
    ```
    Layer 0 — 工具内置引擎探测(扫描可用工具集)
      │
      ├─ 发现内置引擎 → 作为主力搜索,与后续层并行
      └─ 未发现 → 跳过此层,不影响后续
    
    Layer 1 — 大纲建议源定向搜索(site: 搜索,用内置引擎)
             ↓
    Layer 2 — sources.json 优质源搜索(并行)
             ↓
    搜索结果质量评估(Step 3 质量门)
      ├─ 达标 → 直接进入抓取
      └─ 不达标 → Layer 3 免费源补强(A/B 类源 + 区域引擎)
             ↓
    全部 URL → 检测 Scrapling MCP 可用性
        ├─ 🔧 可用 → Scrapling 批量抓取全文 → 数据池
        └─ 🌐 不可用 → webfetch 逐个抓取全文(标注回退)→ 数据池
    ```
    
    ---
    
    ## 8. 安装与配置
    
    ### 前置条件
    
    - Python 3.10+
    - Scrapling(安装方式由 AI 根据官方文档和当前系统自动适配)
    - Playwright(可选,用于 JS 渲染和反检测抓取)
    
    ### 注册 Scrapling MCP Server
    
    Scrapling 通过 MCP(Model Context Protocol)与 AI agent 通信,需注册到当前工具的 MCP 配置后才能被调用(各工具配置文件位置与键名不同:Claude Code 用 `mcpServers`,OpenCode 用 `mcp`,等等——让 AI 现场探测并适配)。
    
    **推荐方式**:运行一次 `/research`,Task 2 在检测到 Scrapling 未注册时,会自动完成安装和注册。
    
    **参考实现**:本项目提供了 `scrapling-mcp-server.py`(与本文件同目录),是一份标准 MCP Server 实现,覆盖了标准抓取、反检测抓取、JS 渲染抓取三种模式。AI 可根据本机环境参考此脚本,如有问题再参考 Scrapling 官方文档。
    
    **手动注册格式**(以 `mcpServers` 标准格式为例,供 AI 安装时参考;按你当前工具的配置键名与数组格式调整):
      ```json
      {
        "mcpServers": {
          "scrapling": {
            "command": "<python-path>",
            "args": ["<mcp-server-script-path>"]
          }
        }
      }
      ```
    
    ### 重启当前工具
    
    MCP Server 在工具启动时加载,注册后**必须重启**才能生效。
    
    ### 验证是否生效
    
    运行 `/research` 调研时,Task 2 阶段如显示 `🔧 Scrapling 抓取` 则表示 MCP 工作正常。若显示 `🌐 webfetch 抓取(Scrapling 已自动安装,重启后生效)` 则表示 Scrapling 已自动安装,重启后下次生效。若显示 `🌐 webfetch 抓取` 则表示安装失败,需检查 Python 环境和网络连接。
    
    ### 抓取回退说明
    
    若 Scrapling 抓取某 URL 失败(WAF/超时/JS 渲染需求),会自动回退到 webfetch 孤立抓取该 URL,不影响其他 URL 的 Scrapling 抓取。若 Scrapling MCP 完全不可用,则全部走 webfetch。调研**不会阻塞**。
    
    ---
    
    ## 9. 跨平台编码规范(Windows/macOS/Linux)
    
    ### 问题根因
    
    Windows PowerShell 5.1 控制台编码为 CP936(GBK 中文编码),**无法表示 18 种非英语语言**:
    俄语西里尔字母、日语假名/汉字、韩语谚文、阿拉伯语、泰语、印地语天城文、越南语调号、
    以及德语 äöüß、法语 éèêç、西班牙语 ñ 等拉丁扩展字符——通过 shell 传参/pipe 时全部损坏。
    
    macOS/Linux 的终端默认 UTF-8,无此问题。
    
    ### 硬性规则(所有 agent 必须遵守)
    
    | # | 规则 | 正确做法 | 错误做法 |
    |---|------|---------|---------|
    | 1 | **非 ASCII 文本不进 shell argv/pipe** | 用 `write` 工具写文件 → Python `--file` 读取 | Python 脚本 argv 传非 ASCII 文本 ❌ |
    | 2 | **所有文件读写用 UTF-8** | Python 统一 `encoding='utf-8-sig'`(BOM 容错) | 依赖 shell 编码 |
    | 3 | **写文件只用 `write` 工具** | `write` 工具 → UTF-8 无 BOM | PowerShell `Set-Content -Encoding UTF8` ❌(会加 BOM) |
    | 4 | **Python stdout 显式设 UTF-8** | `sys.stdout.reconfigure(encoding='utf-8')` | 依赖系统默认编码 |
    | 5 | **Python 子进程输出用 `--output` 文件** | `python script.py --input file --output result` | shell 重定向 `> result.txt` ❌(CP936 编码输出) |
    
    ### 一劳永逸方案:全链路编码安全架构
    
    ```
     ┌─ 数据来源 ──────────────────────────────────┐
     │ write 工具 / Python open(..., 'w', encoding) │ ← UTF-8 无 BOM
     └──────────────┬──────────────────────────────┘
                    ▼
     ┌─ 中间文件 ──────────────────────────────────┐
     │ *.json / *.md : 全部 UTF-8(BOM 容错读取)     │ ← utf-8-sig 代码编
     └──────────────┬──────────────────────────────┘
                    ▼
     ┌─ Python 处理 ───────────────────────────────┐
     │ 所有脚本入口设 `stdout.reconfigure('utf-8')` │ ← 输出安全
     │ 所有文件读用 `encoding='utf-8-sig'`         │ ← 输入安全
      │ Python 脚本统一用 `--input`/`--output` 参数   │ ← 完全绕过 shell
     └──────────────┬──────────────────────────────┘
                    ▼
     ┌─ 最终输出 ──────────────────────────────────┐
     │ 报告文件 : UTF-8 无 BOM,任意语言均可正确显示    │
     └─────────────────────────────────────────────┘
    ```
    
    ### 当前防护状态
    
    | 文件 | 防护措施 | 状态 |
    |------|---------|:----:|
    | `dr_tools.py` | 入口 `stdout.reconfigure` + 所有读操作用 `utf-8-sig` | ✅ |
    | `dr_check.py` | 所有读操作用 `utf-8-sig` | ✅ |
    | `dr_gen.py` | 所有读操作用 `utf-8-sig`,写操作用 `utf-8`(无 BOM) | ✅ |
    
    ---
    
    **Created by [hoolulu](https://github.com/hoolulu)** · [github.com/hoolulu/deep-research](https://github.com/hoolulu/deep-research)
    
  • sources.json 7.5 KB
    {
      "version": 1,
      "description": "Deep-research 优质源列表。Task 2 启动时自动读取,按存活状态动态注入 prompt。死源自动跳过,不影响流程。",
      "sources": [
        {
          "id": "semantic_scholar",
          "name": "Semantic Scholar",
          "type": "academic",
          "url_template": "https://api.semanticscholar.org/graph/v1/paper/search?query={query}&limit=5",
          "lang": ["en"],
          "health_url": "https://api.semanticscholar.org/graph/v1/paper/search?query=test&limit=1",
          "priority": 1
        },
        {
          "id": "arxiv",
          "name": "arXiv",
          "type": "academic",
          "url_template": "https://export.arxiv.org/api/query?search_query=all:{query}&start=0&max_results=5",
          "lang": ["en"],
          "health_url": "https://export.arxiv.org/api/query?search_query=all:test&start=0&max_results=1",
          "priority": 1
        },
        {
          "id": "pubmed",
          "name": "PubMed",
          "type": "academic",
          "url_template": "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi?db=pubmed&term={query}&retmax=5&retmode=json",
          "lang": ["en"],
          "health_url": "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi?db=pubmed&term=test&retmax=1&retmode=json",
          "priority": 1
        },
        {
          "id": "google_scholar",
          "name": "Google Scholar",
          "type": "academic",
          "url_template": "https://scholar.google.com/scholar?q={query}&hl=en&as_sdt=0%2C5&as_ylo=2020",
          "lang": ["en"],
          "health_url": "https://scholar.google.com",
          "priority": 1
        },
        {
          "id": "world_bank",
          "name": "World Bank",
          "type": "data",
          "url_template": "https://www.worldbank.org/en/search?q={query}",
          "lang": ["en"],
          "health_url": "https://www.worldbank.org",
          "priority": 2
        },
        {
          "id": "imf",
          "name": "IMF",
          "type": "data",
          "url_template": "https://www.imf.org/en/Search#q={query}&sort=relevancy",
          "lang": ["en"],
          "health_url": "https://www.imf.org",
          "priority": 2
        },
        {
          "id": "our_world_in_data",
          "name": "Our World in Data",
          "type": "data",
          "url_template": "https://ourworldindata.org/search?q={query}",
          "lang": ["en"],
          "health_url": "https://ourworldindata.org",
          "priority": 2
        },
        {
          "id": "who",
          "name": "WHO",
          "type": "data",
          "url_template": "https://www.who.int/search?q={query}",
          "lang": ["en"],
          "health_url": "https://www.who.int",
          "priority": 2
        },
        {
          "id": "reuters",
          "name": "Reuters",
          "type": "news",
          "url_template": "https://www.reuters.com/search/news?blob={query}",
          "lang": ["en"],
          "health_url": "https://www.reuters.com",
          "priority": 2
        },
        {
          "id": "bbc",
          "name": "BBC",
          "type": "news",
          "url_template": "https://www.bbc.com/search?q={query}",
          "lang": ["en"],
          "health_url": "https://www.bbc.com",
          "priority": 2
        },
        {
          "id": "the_guardian",
          "name": "The Guardian",
          "type": "news",
          "url_template": "https://www.theguardian.com/search?q={query}",
          "lang": ["en"],
          "health_url": "https://www.theguardian.com",
          "priority": 2
        },
        {
          "id": "economist",
          "name": "The Economist",
          "type": "news",
          "url_template": "https://www.economist.com/search?q={query}",
          "lang": ["en"],
          "health_url": "https://www.economist.com",
          "priority": 2
        },
        {
          "id": "nature",
          "name": "Nature",
          "type": "academic",
          "url_template": "https://www.nature.com/search?q={query}",
          "lang": ["en"],
          "health_url": "https://www.nature.com",
          "priority": 1
        },
        {
          "id": "science_direct",
          "name": "ScienceDirect",
          "type": "academic",
          "url_template": "https://www.sciencedirect.com/search?qs={query}",
          "lang": ["en"],
          "health_url": "https://www.sciencedirect.com",
          "priority": 1
        },
        {
          "id": "wikipedia",
          "name": "Wikipedia",
          "type": "reference",
          "url_template": "https://en.wikipedia.org/w/index.php?search={query}&title=Special:Search",
          "lang": ["en"],
          "health_url": "https://en.wikipedia.org",
          "priority": 3
        },
        {
          "id": "github",
          "name": "GitHub",
          "type": "tech",
          "url_template": "https://github.com/search?q={query}&type=repositories",
          "lang": ["en"],
          "health_url": "https://github.com",
          "priority": 3
        },
        {
          "id": "stackoverflow",
          "name": "Stack Overflow",
          "type": "tech",
          "url_template": "https://stackoverflow.com/search?q={query}",
          "lang": ["en"],
          "health_url": "https://stackoverflow.com",
          "priority": 3
        },
        {
          "id": "wikipedia_zh",
          "name": "维基百科(中文)",
          "type": "reference",
          "url_template": "https://zh.wikipedia.org/w/index.php?search={query}&title=Special:Search",
          "lang": ["zh"],
          "health_url": "https://zh.wikipedia.org",
          "priority": 3
        },
        {
          "id": "baidu_baike",
          "name": "百度百科",
          "type": "reference",
          "url_template": "https://baike.baidu.com/item/{query}",
          "lang": ["zh"],
          "health_url": "https://baike.baidu.com",
          "priority": 3
        },
        {
          "id": "zhihu",
          "name": "知乎",
          "type": "media",
          "url_template": "https://www.zhihu.com/search?type=content&q={query}",
          "lang": ["zh"],
          "health_url": "https://www.zhihu.com",
          "priority": 3
        },
        {
          "id": "36kr",
          "name": "36氪",
          "type": "media",
          "url_template": "https://36kr.com/search/articles/{query}",
          "lang": ["zh"],
          "health_url": "https://36kr.com",
          "priority": 3
        },
        {
          "id": "thepaper",
          "name": "澎湃新闻",
          "type": "news",
          "url_template": "https://www.thepaper.cn/search?keyword={query}",
          "lang": ["zh"],
          "health_url": "https://www.thepaper.cn",
          "priority": 3
        },
        {
          "id": "199it",
          "name": "199IT",
          "type": "data",
          "url_template": "https://www.199it.com/?s={query}",
          "lang": ["zh"],
          "health_url": "https://www.199it.com",
          "priority": 3
        },
        {
          "id": "iresearch",
          "name": "艾瑞咨询",
          "type": "data",
          "url_template": "https://report.iresearch.cn/search.aspx?key={query}",
          "lang": ["zh"],
          "health_url": "https://report.iresearch.cn",
          "priority": 3
        },
        {
          "id": "eastmoney",
          "name": "东方财富",
          "type": "data",
          "url_template": "https://so.eastmoney.com/news/s?keyword={query}",
          "lang": ["zh"],
          "health_url": "https://www.eastmoney.com",
          "priority": 3
        },
        {
          "id": "huxiu",
          "name": "虎嗅",
          "type": "media",
          "url_template": "https://www.huxiu.com/search.html?q={query}",
          "lang": ["zh"],
          "health_url": "https://www.huxiu.com",
          "priority": 3
        },
        {
          "id": "csdn",
          "name": "CSDN",
          "type": "tech",
          "url_template": "https://so.csdn.net/so/search?q={query}",
          "lang": ["zh"],
          "health_url": "https://www.csdn.net",
          "priority": 3
        },
        {
          "id": "douban",
          "name": "豆瓣",
          "type": "media",
          "url_template": "https://www.douban.com/search?q={query}",
          "lang": ["zh"],
          "health_url": "https://www.douban.com",
          "priority": 3
        },
        {
          "id": "weibo",
          "name": "微博",
          "type": "media",
          "url_template": "https://s.weibo.com/weibo?q={query}",
          "lang": ["zh"],
          "health_url": "https://weibo.com",
          "priority": 3
        }
      ]
    }
    
  • TYPES.md 4.9 KB
    # 主题类型与章节结构 — deep-research
    
    基于 IDC 文档类型体系、Gartner 研究方法、券商行业深度报告标准、Forrester Tech Tide、IEEE 技术报告规范整理。
    
    ---
    
    ## 类型一:行业研究
    
    | 项目 | 说明 |
    |:----|:-----|
    | 对标标准 | 券商行业深度报告 / IDC Market Forecast / 艾瑞咨询行业报告 |
    | 适用场景 | 分析一个行业的规模、竞争、趋势 |
    | 典型章序 | 定义 → 规模 → 竞争格局 → 产业链 → 政策 → 趋势 → 风险 |
    | 受众侧重 | 投资者、决策层 |
    | 参考来源 | IDC: Market Forecast, Market Share, Spending Guide ([idc.com/help/doctypes](https://www.idc.com/help/doctypes/)) |
    
    ## 类型二:公司/产品研究
    
    | 项目 | 说明 |
    |:----|:-----|
    | 对标标准 | 券商公司深度报告 / Gartner Vendor Assessment / CB Insights Company Profile |
    | 适用场景 | 深入分析一家公司或产品 |
    | 典型章序 | 定位 → 产品/技术 → 商业模式 → 竞争 → 风险 → 展望 |
    | 受众侧重 | 投资者、技术决策者 |
    | 参考来源 | Gartner: Magic Quadrant, Critical Capabilities ([gartner.com](https://www.gartner.com/en/research/magic-quadrant)) |
    
    ## 类型三:技术专题研究
    
    | 项目 | 说明 |
    |:----|:-----|
    | 对标标准 | IEEE 技术报告 / Gartner Hype Cycle / 技术白皮书 |
    | 适用场景 | 调研一项技术的原理、现状与前景 |
    | 典型章序 | 背景 → 架构原理 → 发展现状 → 技术评估 → 应用 → 趋势 |
    | 受众侧重 | 技术决策者、学习者 |
    | 参考来源 | Gartner: Hype Cycle, Tech Trend ([gartner.com](https://www.gartner.com/en/research/methodologies/magic-quadrants-research)) |
    
    ## 类型四:趋势/市场前瞻
    
    | 项目 | 说明 |
    |:----|:-----|
    | 对标标准 | Forrester Tech Tide / Gartner FutureScape / 36Kr 行业趋势 |
    | 适用场景 | 分析新兴趋势的驱动力与影响 |
    | 典型章序 | 现状 → 驱动因素 → 演进脉络 → 影响评估 → 行动建议 |
    | 受众侧重 | 决策层、投资者 |
    | 参考来源 | IDC: FutureScape, PlanScape ([idc.com/help/doctypes](https://www.idc.com/help/doctypes/)) |
    
    ## 类型五:指南/教程
    
    | 项目 | 说明 |
    |:----|:-----|
    | 对标标准 | IEEE 技术白皮书"方法"模版 / 实操文档标准 / GB/T 指南标准 |
    | 适用场景 | 提供可执行的选型或实施指南 |
    | 典型章序 | 前置准备 → 核心概念 → 分步指南 → 常见错误 → 进阶路径 |
    | 受众侧重 | 学习者、技术实践者 |
    | 参考来源 | GB/T 1.1-2020 指南标准 ([openstd.samr.gov.cn](https://openstd.samr.gov.cn/bzgk/std/newGbInfo?hcno=C4BFD981E993C417EF475F2A19B681F1)) |
    
    ---
    
    ## 受众分类(参考Gartner/IDC体系)
    
    | 受众层 | 角色 | 侧重点 |
    |:-------|:-----|:--------|
    | **决策层** | CEO/CIO/CTO | 战略方向、ROI、风险、竞争格局 |
    | **投资层** | 基金经理、分析师 | 市场规模、估值、盈利预测 |
    | **技术层** | 技术负责人、架构师 | 架构、技术栈、部署、安全 |
    | **业务层** | 业务总监、产品经理 | 应用场景、实践案例、ROI |
    | **学习者** | 从业者、学生、媒体 | 概念理解、趋势、全景认知 |
    
    > 来源:Gartner 受众模型 ([portersfiveforce.com](https://portersfiveforce.com/blogs/target-market/gartner)), 证券业协会研究报告受众分类 ([sac.net.cn](https://www.sac.net.cn/zlgl/zlgz/202512/t20251231_69839.html))
    
    ---
    
    ## 报告输出编号体系
    
    报告正文使用三级编号体系,章与子节采用不同编号格式以避免层级冲突:
    
    | 层级 | Markdown | 编号格式 | 示例 | 备注 |
    |------|----------|----------|------|------|
    | **章(一级)** | `##` | 汉字数字 + 顿号:一、二、三… | `## 一、市场规模` | 装配阶段自动分配,每章独占一个汉字编号 |
    | **子节(二级)** | `###` | 阿拉伯数字 x.x:1.1, 1.2… | `### 1.1 全球出货量` | 各章从头编号(每章均从 .1 开始) |
    | **子子节(三级)** | `####` | 括号阿拉伯数字:(1), (2)… | `#### (1) 按地区拆分` | 子节内部可选细化 |
    
    ### 核心约束
    
    1. **同层统一**:同一层级全报告使用相同的编号格式,不得混用
    2. **不可跳级**:一级 → 二级 → 三级递进,不得出现一级后直接三级
    3. **体系分离**:子节编号体系与章编号体系必须不同(章用汉字数字 → 子节用阿拉伯数字)
    4. **每章重置**:子节编号在每章内从 1.1 重新开始,不跨章连续
    
    来源:CY/T 35-2001《科技文献的章节编号方法》([xxmu.edu.cn](https://www.xxmu.edu.cn/qks/CY_T35-2001.pdf)), GB/T 1.1-2020《标准化工作导则》([openstd.samr.gov.cn](https://openstd.samr.gov.cn/bzgk/std/newGbInfo?hcno=C4BFD981E993C417EF475F2A19B681F1)), 公文层次序号规范 ([book118.com](https://m.book118.com/html/2024/1014/7012141001006162.shtm))
    
    ---
    ```
    deep-research by hoolulu · github.com/hoolulu/deep-research
    ```
    
  • VERSION 6 B · in bundle

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related