Skill Hub · 诺亚 AI 能力中心 静态参考版

让每一个技能成为可信资产 —— 诺亚 Agent 生态的技能资产中心:统一注册、检测把关、按需复用。本站为 skill-hub.noahgroup.com 于 2026-08-21 的全量数据镜像。

镜像采集 2026-08-2166 个已发布技能18 个分类 · 3 条分发渠道含技能包 SKILL.md 全文
← 返回目录

nav-review-cost-report

SKILL_712700669 · vv1.1 · Olive 海外运营 · Owner:Olive 基金运营 · 发布于 2026-08-10
调用 20 下载 0 点赞 0 浏览 0
简介
基于第三方凭证独立重算NAV投资成本,生成交互式HTML报告与验证JSON。
触发词
投资成本复核,Cost阶段复核,生成成本报告
分发渠道
ARK Engine
功能测试
✅ 通过 · 业务评审:✅ 通过
技能包文件
nav-review-cost-report/SKILL.md、nav-review-cost-report/references/cost_calculation_rules.md、nav-review-cost-report/references/inv_cost_review_html_template.html、nav-review-cost-report/references/inv_cost_review_template.md、nav-review-cost-report/references/mmf_reinvestment_period_rules.md
使用示例:请对本基金执行Stage 3.2成本复核并生成报告。

SKILL.md 全文

Frontmatter

namenav-review-cost-report
description|

NAV Review Stage 3.2 — Investment Cost Review 报告生成与上传 (HTML 版)

⛔ 模板强制遵循规则(最高优先级,不可跳过)

生成 HTML 报告时,必须先 read_file("references/inv_cost_review_html_template.html") 加载模板,然后严格按照模板的完整结构生成报告。禁止自行设计布局、样式或交互方式。 具体要求: 1. 三栏布局不可变更:左侧导航栏(240px, #1E293B 深色背景) + 中间复核面板(margin-left:240px, margin-right:520px) + 右侧 Supporting 证据面板(520px, fixed, #F1F5F9),三者缺一不可 2. CSS/JS 必须从模板复制:不得自行编写替代样式或脚本,必须使用模板中定义的完整 CSS 和 Vanilla JS 交互逻辑(含 Lightbox) 3. Section 结构必须与模板一致:TB vs N4 → Continuity → Cost Movement → MMF Detail → Individual Investments → Distribution Impact → Fund Statements → Conclusion,不得省略、合并或重新排序 4. 橙色主题色(#ff9800)不可变更:导航栏标题、Section 标题下划线、交互高亮等均使用模板定义的橙色主题 5. 交互行为必须保留:所有数据行必须是 .interactive-row + data-supporting-id,右侧面板默认可见并联动切换 6. 禁止降级为单栏/双栏布局:任何情况下不得生成简单的单页报告或传统文档式布局 违反上述任何一条即视为报告生成失败,必须重新生成。 ---

⛔ Supporting 证据来源限制(强制,最高优先级)

只有第三方/外部独立文件才能作为 Supporting 证据。基金自身内部系统生成的文件严禁作为 Supporting。 | 禁止作为 Supporting | 允许作为 Supporting | |---|---| | ❌ Investment Position Appraisal (IPA/N4) | ✅ 银行对账单(DBS Bank Statement 等) | | ❌ Trial Balance (TB) / TB Detail | ✅ 基金行政管理人报表(Citco MV Statement、Citco Capital Account Statement) | | ❌ Purchases and Sales Report | ✅ 交易合同/确认函(Contract Note) | | ❌ Nominal Ledger / Detailed General Ledger | ✅ 银行付款确认(Cash Payment Confirmation) | | ❌ Statement of Operations (SOFI) | ✅ 托管人报告 | | ❌ Change in Unrealized Gains Losses Report | ✅ Payment Log(独立第三方核对记录) | 核对逻辑:中间面板的复核数据可引用 IPA/TB 数字进行对比展示,但右侧 Supporting 面板只展示第三方证据截图来佐证这些数字的正确性。对于 Investment Cost 核对,典型第三方证据包括:Citco Capital Account Statement(确认份额/金额)、Contract Note(确认交易)、Cash Payment Confirmation(确认资金流)。如果某个核对项无对应第三方文件,则该行不设置 Supporting 证据。 ---

⛔ Supporting 面板截图与计算过程规则(强制)

规则 A:高清截图 + 红框高亮关键数据

每个 .supporting-item 必须包含对应源文件的高清截图(base64 内嵌),且用红框高亮标注关键数字/字段: 1. 截图范围 — 按数据密度决定 2. 红框高亮规则 3. 截图分段标题:每张截图上方须有 <div class="sup-section-label"> 标注来源文件名和取数用途,如 "IPA — SR13 Cost (May-31)"

规则 B:计算过程 Supporting(涉及测算时强制输出)

当复核过程中涉及数值测算(而非简单读取对比)时,必须在该行对应的 .supporting-item 最下方新增一个计算过程区块。典型场景包括但不限于: 计算过程区块格式(混合样式)
<div class="calc-block">
  <div class="calc-title">📐 Calculation: [计算目的,如 "Redemption Cost for SR13"]</div>
  <pre class="calc-steps">
Step 1: Identify redeemed units
  Redeemed Units = 4,176.4832 (from Contract Note)

Step 2: Determine average cost per unit
  Prior Period Avg Cost = Prior Total Cost / Prior Total Units
                        = 17,054,611.89 / 18,222.8478
                        = 935.9221 per unit

Step 3: Calculate redemption cost
  Redemption Cost = Redeemed Units × Avg Cost Per Unit
                  = 4,176.4832 × 935.9221
                  = 3,908,724.05
  </pre>
  <div class="calc-result">✓ Result: USD 3,908,724.05 — matches TB credit</div>
</div>
CSS 样式(须写入 HTML <style> 中)
.calc-block {
  margin-top: 12px;
  border: 1px solid #E2E8F0;
  border-radius: 6px;
  overflow: hidden;
  background: #FAFBFC;
}
.calc-title {
  padding: 8px 12px;
  font-size: 12px;
  font-weight: 600;
  color: #1E293B;
  background: #F1F5F9;
  border-bottom: 1px solid #E2E8F0;
}
.calc-steps {
  padding: 10px 14px;
  font-size: 11px;
  font-family: 'Consolas', 'Courier New', monospace;
  line-height: 1.6;
  color: #334155;
  white-space: pre-wrap;
  margin: 0;
  background: #FAFBFC;
}
.calc-result {
  padding: 8px 12px;
  font-size: 12px;
  font-weight: 600;
  color: #0A8754;
  background: #E6F4ED;
  border-top: 1px solid #B7E4C7;
}
执行规则 ---

When to Use

本 skill 只生成 Investment Cost Review HTML 交互式报告 + verified_cost.json + iGowin 上传。HTML 即为最终交付物,不再生成 PDF。 ---

输入

输出

| 报告 | HTML 文件名 | iGowin Subject | |------|------------|----------------| | Investment Cost Review | Investment Cost Review - [基金名] [YYYYMMDD].html | Investments Long 中的 Cost 子科目 | 额外强制产物<target_dir>/.cortex/nav_data/verified_cost.json(供 UGL Review 消费) HTML 交互式报告输出至 <target_dir>/ 根目录(用户双击可直接在浏览器中打开),并作为 iGowin 系统上传的最终交付物。中间脚本放 <target_dir>/.cortex/。 ---

外置文件索引

| 文件 | 路径 | 加载时机 | |------|------|---------| | Investment Cost Review 内容模板 | references/inv_cost_review_template.md | 启动时 read_file(Section 结构 + 核对逻辑) | | Investment Cost Review HTML 模板 | references/inv_cost_review_html_template.html | 启动时 read_file(固定 HTML/CSS/JS 三栏结构) | | 投资成本变动计算规则 | references/cost_calculation_rules.md | 启动时 read_file — 定义期初成本、赎回/转出扣减、转入增加的取值规则与限制性条件 | | MMF Reinvestment 取数期间规则 | references/mmf_reinvestment_period_rules.md | 启动时 read_file — 定义 MMF 红利转投的跨期取数逻辑(季度=M2+M3+M4,月度=M+1),含 Posn 验证和 TB 交叉验证。违反将导致 Reinvestment 与 TB Dividend Income 不匹配 | | Distribution 五步框架 + JE 矩阵 | ../nav-review-report/references/distribution_framework.md | 按需:仅当存在 Distribution 时 read_file | | iGowin 上传流程 | ../nav-review-report/references/igowin_upload.md | 上传阶段 read_file |
所有路径相对于 skill 目录:c:\Users\syy10787\.cortex\skills\nav-review-cost-report\
---

UI 与格式约束

HTML 报告为最终交付物,必须采用响应式、现代化双栏交互式设计,提供极佳的屏幕核对体验:

1. 结构与排版布局(三栏固定布局)

2. 交互体验设计 (Vanilla JS)

3. 颜色与视觉风格

---

执行流程

Step 1:加载 profile + 数据 + 模板 + 计算规则

编写 Python 脚本加载数据:
import os, json, sys

BASE = r"<target_dir>"
PROFILE = json.load(open(os.path.join(BASE, ".cortex", "nav_profile.json"), encoding="utf-8"))
加载 <target_dir>/.cortex/nav_data/ 下对应的所有核对 JSON 数据。 ⚠️ 必须先 read_file 加载 references/cost_calculation_rules.md,确保投资成本变动的计算逻辑严格遵守其中定义的规则(期初成本取上期IPA、赎回/转出扣减用上期平均成本×份额、转入用当期交易金额、当期Report仅作比对)。 ⚠️ 必须 read_file 加载 references/mmf_reinvestment_period_rules.md,确保 MMF Reinvestment 取数期间正确:季度频率=M2+M3+M4(不含首月M1),月度频率=次月M+1(不含当月)。M1 reinvestment 属于上季度末月红利,计入当季是严重错误。 ⚠️ 必须 read_file 加载 references/inv_cost_review_html_template.html,生成报告时严格遵循此 HTML 模板的三栏布局结构、CSS 样式(橙色主题色 #ff9800)、交互式 JS 脚本。模板定义了完整的 Section 结构(TB vs N4 → Continuity → Cost Movement → MMF Detail → Individual Investments → Distribution Impact → Fund Statements → Conclusion),不得省略或改变顺序。

Step 2:生成交互式 HTML 报告

使用 Python 脚本拼装 HTML 字符串,将数据填入 HTML/CSS 模板中: 1. 渲染左侧导航栏:显示 5 个 Section 链接: 2. 渲染中间复核面板:根据 investment_movement.json 逐持仓循环生成表格行。行中插入 class="interactive-row" data-supporting-id="support-[持仓名]-overview"。 3. 渲染右侧 Supporting 面板:在 .supporting-panel 中,针对每个持仓的截图,渲染 <div id="support-[持仓名]" class="supporting-item">,内部包含来自 .cortex/screenshots/inv_cost/ 目录的图片。 ⚠️ 截图内嵌强制规则:所有截图必须以 base64 data URI 内嵌到 HTML 文件中,禁止使用相对路径或外部文件引用。实现方式:在 Python 脚本中,对每个 PNG 文件读取二进制内容并转为 base64 编码,然后以 data:image/png;base64,<base64_string> 格式写入 <img src="...">
   import base64

   def embed_image(base_dir, folder, filename):
       """读取截图文件并返回 base64 data URI"""
       path = os.path.join(base_dir, ".cortex", "screenshots", folder, filename)
       if os.path.exists(path):
           with open(path, "rb") as f:
               data = base64.b64encode(f.read()).decode("utf-8")
           return f"data:image/png;base64,{data}"
       return ""  # 文件不存在时返回空字符串

   def embed_all_pages(base_dir, folder, prefix):
       """读取所有匹配 prefix_pageN.png 的截图并返回 base64 data URI 列表"""
       import os as _os
       shot_dir = _os.path.join(base_dir, ".cortex", "screenshots", folder)
       pages = []
       if _os.path.isdir(shot_dir):
           for f in sorted(_os.listdir(shot_dir)):
               if f.startswith(prefix) and f.endswith(".png"):
                   pages.append((f, embed_image(base_dir, folder, f)))
       return pages
   
HTML 中使用:<img src="{embed_image(BASE, 'inv_cost', 'prior_ipa.png')}" alt="Prior IPA"> > 理由:内嵌截图使 HTML 文件完全自包含,双击即可在任何位置打开查看,不依赖 .cortex/ 目录结构。上传至 iGowin 后也能直接在浏览器中完整展示,无需额外文件。 ⚠️ 截图清晰度强制规则:使用 PyMuPDF (fitz) 渲染 PDF 页面为 PNG 时,必须使用 4x zoomfitz.Matrix(4, 4)),确保文字和数字在浏览器中清晰可读。2x zoom 不足以保证小字清晰度。 ⚠️ 多页截图完整展示强制规则:当源 PDF 被转换为多页 PNG 截图时(如 prior_ipa_page1.pngprior_ipa_page2.png),必须全部嵌入到 Supporting 面板中,禁止只展示第一页而遗漏后续页面。使用 embed_all_pages() 函数遍历所有匹配页面并逐一嵌入。这是确保 Supporting 证据完整性的硬性要求。 ⚠️ Portfolio Overview(Section 1)Supporting 强制规则:Portfolio Overview 每行的 Supporting 面板必须同时展示两类截图,缺一不可: 实现方式:每个 Portfolio Overview 行对应一个复合 .supporting-item,内部用分段标题(<div class="sup-section-label">标题</div>)将多张截图分组展示,每段标题清晰标注取数用途。所有 <img> 标签的 src 均使用 embed_image()embed_all_pages() 返回的 base64 data URI。 > 分段标题样式padding:6px 10px; font-size:11px; color:#475569; background:#F8FAFC; border-bottom:1px solid #E2E8F0; border-top:1px solid #E2E8F0; font-weight:600; margin-top:8px;(首段无 border-top)。 ⚠️ 所有 Section 的 Supporting 均须完整展示多页截图:不仅限于 Portfolio Overview,其他 Section(交易明细、Cost Movement、差异分析、TB 验证等)的 Supporting 面板同样必须使用 embed_all_pages() 完整嵌入所有页面,不允许只展示第一页。 4. 嵌入交互式 JS 脚本(双击截图 → 全屏 Lightbox 预览 + 工具栏缩放 + 滚轮缩放 + 拖拽平移): CSS 新增(在 <style> 末尾添加):
   / ── Supporting 图片悬停提示 ── /
   .supporting-item img {{
       cursor: zoom-in;
       transition: opacity 0.15s;
   }}
   .supporting-item img:hover {{ opacity: 0.9; }}

   / ── Lightbox 全屏遮罩 ── /
   .lightbox-overlay {{
       display: none;
       position: fixed;
       inset: 0;
       background: rgba(0, 0, 0, 0.88);
       z-index: 9999;
       overflow: hidden;
   }}
   .lightbox-overlay.active {{ display: block; }}
   .lightbox-toolbar {{
       position: fixed;
       top: 12px;
       left: 50%;
       transform: translateX(-50%);
       display: flex;
       align-items: center;
       gap: 8px;
       padding: 6px 12px;
       background: rgba(255, 255, 255, 0.95);
       border-radius: 8px;
       box-shadow: 0 4px 14px rgba(0,0,0,0.35);
       font-size: 12px;
       color: #1E293B;
       z-index: 10000;
       user-select: none;
   }}
   .lightbox-btn {{
       border: 1px solid #CBD5E1;
       background: #F8FAFC;
       color: #1E293B;
       width: 30px;
       height: 26px;
       line-height: 1;
       font-size: 14px;
       border-radius: 4px;
       cursor: pointer;
       padding: 0;
   }}
   .lightbox-btn:hover {{ background: #E2E8F0; }}
   .lightbox-btn.reset,
   .lightbox-btn.close {{ width: auto; padding: 0 10px; font-size: 11px; }}
   .lightbox-btn.close {{ border-color: #FCA5A5; color: #B91C1C; background: #FEF2F2; }}
   .lightbox-level {{ min-width: 46px; text-align: center; font-variant-numeric: tabular-nums; }}
   .lightbox-stage {{
       position: absolute;
       inset: 0;
       overflow: auto;
       cursor: grab;
       display: flex;
       align-items: center;
       justify-content: center;
   }}
   .lightbox-stage.grabbing {{ cursor: grabbing; }}
   .lightbox-stage img {{
       max-width: 95vw;
       max-height: 95vh;
       transform-origin: center center;
       user-select: none;
       -webkit-user-drag: none;
       transition: transform 0.05s linear;
       box-shadow: 0 0 30px rgba(0,0,0,0.5);
   }}
   
HTML 结构(在 </body> 之前注入一次即可):
   <div class="lightbox-overlay" id="lightbox" aria-hidden="true">
       <div class="lightbox-toolbar">
           <button class="lightbox-btn" data-zoom="out" title="缩小">−</button>
           <span class="lightbox-level">100%</span>
           <button class="lightbox-btn" data-zoom="in" title="放大">+</button>
           <button class="lightbox-btn reset" data-zoom="reset" title="重置">Reset</button>
           <button class="lightbox-btn close" data-zoom="close" title="关闭">✕ 关闭</button>
       </div>
       <div class="lightbox-stage" id="lightbox-stage">
           <img id="lightbox-img" src="" alt="Zoomed view" />
       </div>
   </div>
   
JS 完整脚本
<script>
    document.addEventListener('DOMContentLoaded', function() {
        const rows = document.querySelectorAll('.interactive-row');
        const items = document.querySelectorAll('.supporting-item');
        const placeholder = document.getElementById('no-supporting-placeholder');

        if (items.length > 0) {
            activateSupporting(items[0].id);
            const firstRow = document.querySelector('.interactive-row[data-supporting-id="' + items[0].id + '"]');
            if (firstRow) firstRow.classList.add('highlighted');
        }

        rows.forEach(row => {
            row.addEventListener('click', function() {
                rows.forEach(r => r.classList.remove('highlighted'));
                this.classList.add('highlighted');
                activateSupporting(this.getAttribute('data-supporting-id'));
            });
        });

        function activateSupporting(id) {
            if (!id) return;
            const targetItem = document.getElementById(id);
            if (targetItem) {
                if (placeholder) placeholder.style.display = 'none';
                items.forEach(i => i.classList.remove('active'));
                targetItem.classList.add('active');
                targetItem.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
            }
        }

        // ── 双击 Supporting 截图 → 全屏 Lightbox 预览 ──
        const ZOOM_MIN = 0.5, ZOOM_MAX = 4.0, STEP_BTN = 0.25, STEP_WHEEL = 0.1;
        const overlay = document.getElementById('lightbox');
        const stage = document.getElementById('lightbox-stage');
        const lbImg = document.getElementById('lightbox-img');
        const lbLevel = overlay ? overlay.querySelector('.lightbox-level') : null;
        let scale = 1;

        const apply = () => {
            if (lbImg) lbImg.style.transform = 'scale(' + scale + ')';
            if (lbLevel) lbLevel.textContent = Math.round(scale * 100) + '%';
        };
        const setScale = v => { scale = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, v)); apply(); };

        function openLightbox(src, alt) {
            if (!overlay || !lbImg) return;
            lbImg.src = src;
            lbImg.alt = alt || 'Zoomed view';
            scale = 1;
            apply();
            if (stage) { stage.scrollLeft = 0; stage.scrollTop = 0; }
            overlay.classList.add('active');
            overlay.setAttribute('aria-hidden', 'false');
        }
        function closeLightbox() {
            if (!overlay) return;
            overlay.classList.remove('active');
            overlay.setAttribute('aria-hidden', 'true');
            if (lbImg) lbImg.src = '';
        }

        document.querySelectorAll('.supporting-item img').forEach(img => {
            img.addEventListener('dblclick', function(e) {
                e.preventDefault();
                e.stopPropagation();
                openLightbox(this.src, this.alt);
            });
        });

        if (overlay) {
            overlay.querySelectorAll('.lightbox-btn').forEach(btn => {
                btn.addEventListener('click', e => {
                    e.preventDefault();
                    e.stopPropagation();
                    const action = btn.getAttribute('data-zoom');
                    if (action === 'in')    setScale(scale + STEP_BTN);
                    if (action === 'out')   setScale(scale - STEP_BTN);
                    if (action === 'reset') {
                        if (stage) { stage.scrollLeft = 0; stage.scrollTop = 0; }
                        setScale(1);
                    }
                    if (action === 'close') closeLightbox();
                });
            });

            overlay.addEventListener('click', e => {
                if (e.target === overlay || e.target === stage) closeLightbox();
            });

            document.addEventListener('keydown', e => {
                if (e.key === 'Escape' && overlay.classList.contains('active')) closeLightbox();
            });

            stage.addEventListener('wheel', e => {
                if (!overlay.classList.contains('active')) return;
                e.preventDefault();
                const rect = stage.getBoundingClientRect();
                const px = e.clientX - rect.left + stage.scrollLeft;
                const py = e.clientY - rect.top + stage.scrollTop;
                const oldScale = scale;
                setScale(scale + (e.deltaY < 0 ? STEP_WHEEL : -STEP_WHEEL));
                const ratio = scale / oldScale;
                stage.scrollLeft = px * ratio - (e.clientX - rect.left);
                stage.scrollTop  = py * ratio - (e.clientY - rect.top);
            }, { passive: false });

            let dragging = false, sx = 0, sy = 0, ssl = 0, sst = 0;
            stage.addEventListener('mousedown', e => {
                if (e.button !== 0) return;
                if (e.target.closest('.lightbox-toolbar')) return;
                dragging = true;
                sx = e.clientX; sy = e.clientY;
                ssl = stage.scrollLeft; sst = stage.scrollTop;
                stage.classList.add('grabbing');
                e.preventDefault();
            });
            document.addEventListener('mousemove', e => {
                if (!dragging) return;
                stage.scrollLeft = ssl - (e.clientX - sx);
                stage.scrollTop  = sst - (e.clientY - sy);
            });
            document.addEventListener('mouseup', () => {
                if (!dragging) return;
                dragging = false;
                stage.classList.remove('grabbing');
            });
        }
    });
</script>
5. 将完整 HTML 内容写入 <target_dir>/Investment Cost Review - [基金名] [YYYYMMDD].html

Step 3:产出 verified_cost.json

报告生成时,必须同时将逐持仓的核对结果输出至 <target_dir>/.cortex/nav_data/verified_cost.json。 格式必须与之前保持一致,以便 downstream UGL Review 消费:
{
  "generated_by": "nav-review-cost-report",
  "nav_date": "YYYY-MM-DD",
  "investments": [
    {
      "name": "持仓名",
      "type": "Partnership Interest / Money Market Instrument / Hedge Fund",
      "verified_cost": 12345.67,
      "source": "IPA / SOFI / CAS"
    }
  ]
}

Step 4:上传 iGowin

read_file("../nav-review-report/references/igowin_upload.md") 获取上传流程,然后执行 6 步(上传 HTML 文件本身,不再做 PDF 转换): 1. get_nav_packs(fund_name) → 获取 applicationNo 2. get_subject_names(application_no) → 找到 Investment Cost 类 subject_name 3. get_upload_url(file_name, content_type="text/html") → 拿到 uploadUrl + objectName 4. ⚠️ 上传前确认关卡(强制):调用 ask_questions 弹窗向用户展示完整上传摘要并取得明确确认。摘要必须包含:基金名、NAV 日期、流程单号、目标 subject、文件名、文件大小(MB)、报告核对结论(PASS / FAIL,及差异金额)。选项至少包含「确认上传」「跳过该报告」「全部取消」。未取得确认前,不得执行 Step 5 的 PUT 上传或 Step 6 的 save_check_result。即便此前在主 skill 入口已确认整体计划,此处仍须再次确认(理由见 igowin_upload.md 的"强制原则"段)。 5. PowerShell 上传(仅在 Step 4 通过后执行):Invoke-WebRequest -UseBasicParsing -Method Put -Uri $uploadUrl -InFile $htmlPath -ContentType "text/html" 6. save_check_result(object_name, file_name, subject_name, application_no)(仅在 Step 5 PUT 返回 200 OK 后执行) ---

完成检查表

---

截图质量与标注规则

截图清晰度要求(最高质量)

清晰度为第一优先级。所有内嵌 HTML 的截图必须确保数字、文字完全清晰可读。 | 参数 | 值 | 说明 | |------|-----|------| | DPI | 150 | HTML 内嵌 JPEG 渲染分辨率 | | JPEG quality | 85 | 高品质,文字无锯齿 | | 红框线宽 | 3px | PIL ImageDraw,高清图上清晰可见 | | 落盘 PNG | 300 DPI | 独立 PNG 文件最高清晰度 | | 宽表 zoom | | IPA 等宽表裁剪用 4x zoom | 不设单文件体积上限。如需控制体积,通过 clip_box 裁剪区域(只截关键数据行),禁止降低 DPI 或 quality
import fitz, io, base64
from PIL import Image, ImageDraw

def screenshot_for_embed(pdf_path, page_num, red_rects=None, clip_box=None, dpi=150, quality=85):
    """高清 JPEG 截图用于 HTML 内嵌"""
    doc = fitz.open(pdf_path)
    page = doc[page_num]
    mat = fitz.Matrix(dpi/72, dpi/72)
    pix = page.get_pixmap(matrix=mat, clip=fitz.Rect(*clip_box) if clip_box else None)
    img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
    if red_rects:
        draw = ImageDraw.Draw(img)
        scale = dpi / 72.0
        ox, oy = (clip_box[0], clip_box[1]) if clip_box else (0, 0)
        for box in red_rects:
            draw.rectangle([(box[0]-ox)scale-1, (box[1]-oy)scale-1,
                           (box[2]-ox)scale+1, (box[3]-oy)scale+1], outline='red', width=3)
    buf = io.BytesIO()
    img.save(buf, format='JPEG', quality=quality, optimize=True)
    doc.close()
    return f"data:image/jpeg;base64,{base64.b64encode(buf.getvalue()).decode()}"

截图红框标注规则

所有 Supporting 截图必须用红框高亮关键数字: 1. 定位方法:使用 page.get_text("words") 精确提取数字坐标 2. 标注内容:仅标注与当前核对项直接相关的金额(如 proceeds、NAV/share、quantity、cost basis) 3. 红框样式:红色描边 3px(PIL),无填充,略扩展 3px 边距 4. 禁止:圈整行/大区域、不透明填充、标注非关键信息

数据层级标注强制规则 (Data Hierarchy)

报告中必须严格区分 Reference(被核对对象)Supporting(第三方证据): | Category | Documents | 标签色 | |----------|-----------|--------| | Reference(被核对) | TB, IPA, Realized GL Report, P&S Report, Change in UGL, Maples NAV Per Share, DGL | 紫色 #f3e5f5 | | Supporting(第三方证据) | Contract Note, SS&C/GP Investor Statement, Bank Statement, Redemption Confirmation | 绿色 #e8f5e9 | 强制规则 ---

关键注意事项

1. 本 skill 只负责 Investment Cost Review,不涉及 Cash 或 UGL。 2. HTML 即最终交付物:不再生成 PDF,所有上传与展示统一基于 HTML 文件。 3. 所有截图必须以 base64 data URI 内嵌到 HTML 文件中,使 HTML 完全自包含,不依赖外部文件。禁止使用相对路径或外部文件引用。 4. 不要使用任何第三方繁重 JS 库,交互动作必须全部由精简的原生 Vanilla JS 编写。 5. verified_cost.json 是下游 UGL Review 的强制依赖,必须产出且字段完整。 6. Distribution 必走五步框架:只有 Recallable 减少 Cost。 7. MMF 校验公式硬性:Cost = MV = (A) End-month Closing MV + (B) Next-month Reinvestment。 8. Portfolio Overview Supporting 必须双覆盖:期末数据取值(上期/当期 IPA)+ 期间发生额取数(Investment Movement + 各活动原始 Notice),不允许只展示期末截图而遗漏期间发生额证据。 9. ⚠️ monthlyStatement Transaction Details 作为交易确认 Supporting(强制) 9. ⚠️ 投资成本变动计算规则(强制遵守):详见 references/cost_calculation_rules.md,核心规则如下: 10. ⚠️ 所有数据行必须与 Supporting 一一对应:报告中每个 Section(期初成本、交易明细、Portfolio Overview、Cost Movement、差异分析、TB 验证等)的每一行数据都必须是 .interactive-row 并设置 data-supporting-id,点击后右侧 Supporting 面板切换到对应证据。不允许出现无 Supporting 映射的数据行。 11. ⚠️ Supporting 面板默认可见:右侧 Supporting 面板始终展示(position: fixed; right: 0),不需要点击按钮切换显示/隐藏。中间数据面板需预留右侧空间(margin-right: 520px)。 12. ⚠️ 多页截图必须完整展示:当源 PDF 被转换为多页 PNG 时(如 xxx_page1.png + xxx_page2.png),必须使用 embed_all_pages() 全部嵌入到 Supporting 面板中,禁止只展示第一页而遗漏后续页面。 13. 截图清晰度必须使用 4x zoom 渲染:PyMuPDF 渲染 PDF 页面时使用 fitz.Matrix(4, 4)(最高清晰度),2x zoom 不足以保证小字清晰度。双击 Supporting 截图打开 Lightbox 预览(顶部 −/倍数/+/Reset/✕ 工具栏 + 滚轮缩放 + 拖拽平移)让审阅人可进一步查看细节。 14. iGowin 上传前必须经过 ask_questions 强制确认PUT 上传与 save_check_result 都是不可逆操作,因此在调用 get_upload_url 之后、PUT 之前必须ask_questions 弹窗向用户确认(含基金名 / NAV 日期 / 流程单号 / subject / 文件名 / 文件大小 / 核对结论)。即使本 skill 被 nav-review 主 skill 调用、用户已在主入口确认过整体计划,此处仍须再次确认;本 skill 单独运行时同样适用。用户选择「跳过」则不上传该报告,「全部取消」则终止。不允许通过任何"批处理静默上传"绕过此关卡。