Skill: ai-byte/daily-investment-digest (version 1.0.0)

--- name: daily-investment-digest description: Fetch financing event lists from the iYiou skill API and generate a daily financing report in Markdown to stdout. Use when the task asks to pull investment/financing events via `https://api.iyiou.com/skill/info?page=...&pageSize=...`, paginate with `pageSize=10` and `page<=5`, deduplicate records, default to yesterday's date, and optionally use today's date only when explicitly requested by the user. --- # Daily Investment Digest ## Overview - Pull investment/financing events from `https://api.iyiou.com/skill/info`. - Normalize fields, deduplicate rows, and generate a structured Chinese daily report. ## Workflow 1. One-command full report (recommended, default uses yesterday). ```bash node "{baseDir}/scripts/run_full_report.mjs" \ --max-page 5 ``` 2. If user explicitly asks for today's report, pass today's date. ```bash node "{baseDir}/scripts/run_full_report.mjs" \ --report-date 2026-03-11 \ --max-page 5 ``` 3. Direct output mode (stdout only, no file). ```bash node "{baseDir}/scripts/fetch_events.mjs" \ --report-date 2026-03-11 \ --stdout-json | \ node "{baseDir}/scripts/generate_report.mjs" \ --input-json - \ --top-n 0 \ --stdout ``` ## Path Safety - Always call scripts with quoted `{baseDir}` paths to avoid whitespace-path issues. - Scripts resolve relative input paths against the skill root directory. ## Required API Rules - Use `pageSize=10`. - Start at `page=1`. - Increment `page` by 1 each request. - Stop when `page>5` or API event list is empty. - Parse response strictly by schema: `response.code` -> `response.data.posts`. - Treat non-zero `code` as API failure. - Retry failed requests up to 3 times before skipping a page. ## Script Interfaces ### `scripts/run_full_report.mjs` - One-command pipeline: fetch + generate - Defaults to full output (`top-n=0`) - Supports `--report-date`, `--max-page`, `--page-size`, `--timeout-seconds`, `--retry`, `--delay-seconds` - Supports `--top-n` (`0` means all events) ### `scripts/fetch_events.mjs` - `--page-size` default `10` - `--max-page` default `5` - `--report-date` default yesterday (`YYYY-MM-DD`) - `--timeout-seconds` default `15` - `--retry` default `3` - `--delay-seconds` default `0` - Always prints JSON to stdout (`--stdout-json` kept only for compatibility) - Numeric args are bounded for safety: `page-size[1,100]`, `max-page[1,500]`, `retry[1,10]` ### `scripts/generate_report.mjs` - `--input-json` required - `--top-n` default `0` (`0` means all events) - Always prints report text to stdout (`--stdout` kept only for compatibility) - Numeric args are bounded for safety: `top-n[0,500]` ## Output Files - Disabled by design. This skill is stdout-only and does not write report artifacts to disk. ## Data Mapping - Follow [field_mapping.md](references/field_mapping.md) for source-to-target mapping and fallback rules. - To reduce context size, the fetch output keeps only: `brief`, `createdAt`, `originalLink`, `postTitle`, `tags`. ## Failure Handling - Continue on single-page failure after retries. - Use progressive retry backoff (`0.5s`, `1.0s`, `1.5s`, ...). - Record page-level errors in output JSON `meta.errors`. - Generate a report even when no events are found, and clearly mark it as an empty-day report. ## Output Policy - Date policy: default to yesterday; only use today when the user explicitly asks for today. - If user asks full detail, run with `--top-n 0`. - Use script stdout as the main body and keep event entries unchanged. - Output order is mandatory: 1. First output the full event list. 2. Each event must include: `公司简称`、`轮次`、`投资方`、`事件摘要`、`来源链接`. 3. After the full event list, append one ending section titled `投资事件总结`. - Do not output `投资事件总结` before event entries. ## Quick Checks 1. Run fetch step and confirm `meta.total_unique_events > 0` on active days. 2. Run report step and confirm stdout contains: - `核心数据概览` - `融资事件按行业分类` 3. In final AI response, confirm order: - Event list appears first and each item includes `公司简称`、`轮次`、`投资方`、`事件摘要`、`来源链接`. - `投资事件总结` appears only after the event list. - `投资事件总结` appears exactly once at the end. 4. In final AI response, confirm it appends: - `投资事件总结` ## Example End-to-End Command ```bash node "{baseDir}/scripts/run_full_report.mjs" \ --report-date 2026-03-11 \ --max-page 5 ```

path: references/field_mapping.md
content: # Field Mapping

## Request contract

- Endpoint: `https://api.iyiou.com/skill/info`
- Query params:
- `page={1..5}`
- `pageSize=10`
- Response schema (strict):
- top-level object with `code`, `data`, `message`
- success condition: `code == 0`
- event list path: `data.posts` (must be array)
- Stop rule:
- Stop when `page > 5`
- Stop early when the current page returns an empty event list

## Source to normalized fields

| Output field | Source field(s) | Rule |
| --- | --- | --- |
| `brief` | `brief`, `description` | Prefer `brief` |
| `createdAt` | `createdAt` | Keep raw timestamp string |
| `originalLink` | `originalLink` | Source detail URL |
| `postTitle` | `postTitle`, `originalTitle` | Prefer `postTitle` |
| `tags` | `tags[].tagName` | Keep unique tag names only |

## Output schema

Output JSON file contains:

- `meta`: run metadata and page-level errors
- `events`: compact event list (only 5 fields)


path: agents/openai.yaml
content: interface:
  display_name: "投融资日报"
  short_description: "调用分页投融资事件接口，清洗去重并自动生成投融资日报"
  default_prompt: "Use $daily-investment-digest to fetch financing events and generate the financing daily report. By default, use yesterday as report date; only use today when the user explicitly asks for today. The final output must include a mandatory ending section titled '投资事件总结' that summarizes all fetched events."

policy:
  allow_implicit_invocation: true


path: scripts/fetch_events.mjs
content: #!/usr/bin/env node


const API_URL = "https://api.iyiou.com/skill/info";
const EVENT_HINT_KEYS = new Set([
  "id",
  "idStr",
  "postTitle",
  "brief",
  "originalLink",
  "createdAt",
]);


function usage(exitCode = 2) {
  console.error(
    [
      "Usage: fetch_events.mjs [options]",
      "",
      "Options:",
      "  --page-size <n>           Page size (default: 10, bounds: 1-100)",
      "  --max-page <n>            Max page number (default: 5, bounds: 1-500)",
      "  --report-date <YYYY-MM-DD>Report date (default: local date)",
      "  --timeout-seconds <n>     Request timeout in seconds (default: 15)",
      "  --retry <n>               Retry count per page (default: 3, bounds: 1-10)",
      "  --delay-seconds <n>       Delay between pages (default: 0)",
      "  --stdout-json             Kept for compatibility (stdout is always enabled)",
      "  -h, --help                Show this help",
    ].join("\n")
  );
  process.exit(exitCode);
}

function localDateISO() {
  const now = new Date();
  const year = String(now.getFullYear());
  const month = String(now.getMonth() + 1).padStart(2, "0");
  const day = String(now.getDate()).padStart(2, "0");
  return `${year}-${month}-${day}`;
}

function yesterdayDateISO() {
  const now = new Date();
  now.setDate(now.getDate() - 1);
  const year = String(now.getFullYear());
  const month = String(now.getMonth() + 1).padStart(2, "0");
  const day = String(now.getDate()).padStart(2, "0");
  return `${year}-${month}-${day}`;
}

function localDateTime() {
  const now = new Date();
  const year = String(now.getFullYear());
  const month = String(now.getMonth() + 1).padStart(2, "0");
  const day = String(now.getDate()).padStart(2, "0");
  const hours = String(now.getHours()).padStart(2, "0");
  const minutes = String(now.getMinutes()).padStart(2, "0");
  const seconds = String(now.getSeconds()).padStart(2, "0");
  return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}

function clamp(value, low, high) {
  return Math.max(low, Math.min(value, high));
}

function parseIntOrDefault(value, defaultValue) {
  const num = Number.parseInt(String(value ?? ""), 10);
  return Number.isFinite(num) ? num : defaultValue;
}

function parseFloatOrDefault(value, defaultValue) {
  const num = Number.parseFloat(String(value ?? ""));
  return Number.isFinite(num) ? num : defaultValue;
}

function parseArgs(argv) {
  const options = {
    pageSize: 10,
    maxPage: 5,
    reportDate: yesterdayDateISO(),
    timeoutSeconds: 15,
    retry: 3,
    delaySeconds: 0,
    stdoutJson: true,
  };

  for (let i = 0; i < argv.length; i += 1) {
    const arg = argv[i];
    if (arg === "-h" || arg === "--help") usage(0);

    if (arg === "--page-size") {
      options.pageSize = parseIntOrDefault(argv[i + 1], options.pageSize);
      i += 1;
      continue;
    }
    if (arg === "--max-page") {
      options.maxPage = parseIntOrDefault(argv[i + 1], options.maxPage);
      i += 1;
      continue;
    }
    if (arg === "--report-date") {
      options.reportDate = argv[i + 1] ?? options.reportDate;
      i += 1;
      continue;
    }
    if (arg === "--timeout-seconds") {
      options.timeoutSeconds = parseIntOrDefault(argv[i + 1], options.timeoutSeconds);
      i += 1;
      continue;
    }
    if (arg === "--retry") {
      options.retry = parseIntOrDefault(argv[i + 1], options.retry);
      i += 1;
      continue;
    }
    if (arg === "--delay-seconds") {
      options.delaySeconds = parseFloatOrDefault(argv[i + 1], options.delaySeconds);
      i += 1;
      continue;
    }
    if (arg === "--stdout-json") {
      options.stdoutJson = true;
      continue;
    }

    console.error(`Unknown arg: ${arg}`);
    usage();
  }

  options.pageSize = clamp(options.pageSize, 1, 100);
  options.maxPage = clamp(options.maxPage, 1, 500);
  options.retry = clamp(options.retry, 1, 10);
  options.timeoutSeconds = clamp(options.timeoutSeconds, 1, 120);
  options.delaySeconds = Math.max(0, options.delaySeconds);
  return options;
}

async function fetchJson(params, timeoutSeconds) {
  const url = new URL(API_URL);
  for (const [key, value] of Object.entries(params)) {
    url.searchParams.set(key, String(value));
  }

  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeoutSeconds * 1000);

  let response;
  try {
    response = await fetch(url, {
      method: "GET",
      headers: {
        "User-Agent": "daily-investment-digest/1.0",
        Accept: "application/json,text/plain,*/*",
      },
      signal: controller.signal,
    });
  } catch (err) {
    clearTimeout(timer);
    throw new Error(`request failed: ${err.message}`);
  }
  clearTimeout(timer);

  if (!response.ok) {
    const detail = (await response.text().catch(() => ""))
      .replace(/\s+/g, " ")
      .slice(0, 300);
    throw new Error(`HTTP ${response.status}: ${detail || "<empty>"}`);
  }

  const rawText = await response.text();
  try {
    return { payload: JSON.parse(rawText), requestUrl: url.toString() };
  } catch {
    const preview = rawText.replace(/\s+/g, " ").slice(0, 300);
    throw new Error(`invalid JSON: ${preview}`);
  }
}

function looksLikeEvent(item) {
  if (!item || typeof item !== "object" || Array.isArray(item)) return false;
  return Object.keys(item).some((key) => EVENT_HINT_KEYS.has(key));
}

function looksLikeEventList(value, allowEmpty = false) {
  if (!Array.isArray(value)) return false;
  if (value.length === 0) return allowEmpty;
  if (!value.every((item) => item && typeof item === "object" && !Array.isArray(item))) {
    return false;
  }
  return value.some((item) => looksLikeEvent(item));
}

function extractPostsStrict(payload) {
  if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
    throw new Error("response is not an object");
  }

  if ("code" in payload && Number(payload.code) !== 0) {
    const message = String(payload.message ?? "").trim();
    throw new Error(`api code=${payload.code}${message ? `, message=${message}` : ""}`);
  }

  const data = payload.data;
  if (!data || typeof data !== "object" || Array.isArray(data)) {
    throw new Error("missing object at response.data");
  }

  const posts = data.posts;
  if (!Array.isArray(posts)) {
    throw new Error("missing array at response.data.posts");
  }
  if (!looksLikeEventList(posts, true)) {
    throw new Error("response.data.posts is not a valid event list");
  }
  return posts;
}

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

function uniqueTagNames(value) {
  if (!Array.isArray(value)) return [];
  const seen = new Set();
  const names = [];
  for (const item of value) {
    if (!item || typeof item !== "object") continue;
    const name = String(item.tagName ?? "").trim();
    if (!name || seen.has(name)) continue;
    seen.add(name);
    names.push(name);
  }
  return names;
}

function eventKey(item, index) {
  for (const key of ["id", "idStr", "originalLink"]) {
    const value = item?.[key];
    if (value !== undefined && value !== null && String(value) !== "") {
      return `${key}:${String(value)}`;
    }
  }
  const title = String(item?.postTitle ?? item?.originalTitle ?? "").trim();
  const createdAt = String(item?.createdAt ?? "").trim();
  if (title || createdAt) return `title:${title}|createdAt:${createdAt}`;
  return `index:${index}`;
}

function normalizeEvent(item, index) {
  const brief = String(item?.brief ?? item?.description ?? "").trim();
  const createdAt = String(item?.createdAt ?? "").trim();
  const originalLink = String(item?.originalLink ?? "").trim();
  const postTitle = String(item?.postTitle ?? item?.originalTitle ?? "").trim();
  const tags = uniqueTagNames(item?.tags);

  if (!brief && !createdAt && !originalLink && !postTitle) {
    return {
      brief: "",
      createdAt: "",
      originalLink: "",
      postTitle: eventKey(item, index),
      tags: [],
    };
  }

  return {
    brief,
    createdAt,
    originalLink,
    postTitle,
    tags,
  };
}

function parseDateTime(value) {
  if (!value) return 0;
  const ts = new Date(value.replace(" ", "T")).getTime();
  return Number.isFinite(ts) ? ts : 0;
}

function dateFromCreatedAt(value) {
  const text = String(value ?? "").trim();
  if (text.length >= 10) {
    return text.slice(0, 10);
  }
  return "";
}

async function main() {
  const options = parseArgs(process.argv.slice(2));

  const uniqueRaw = [];
  const seenKeys = new Set();
  let totalRawEvents = 0;
  let rawIndex = 0;
  const errors = [];
  let fetchedPages = 0;
  let lastPageChecked = 0;

  for (let page = 1; page <= options.maxPage; page += 1) {
    lastPageChecked = page;
    let payload = null;
    let requestUrl = "";

    for (let attempt = 1; attempt <= options.retry; attempt += 1) {
      try {
        const result = await fetchJson(
          {
            page,
            pageSize: options.pageSize,
          },
          options.timeoutSeconds
        );
        payload = result.payload;
        requestUrl = result.requestUrl;
        break;
      } catch (err) {
        if (attempt === options.retry) {
          errors.push(`page ${page}: failed after ${options.retry} attempts: ${err.message}`);
        } else {
          await sleep(500 * attempt);
        }
      }
    }

    if (!payload) continue;

    let items;
    try {
      items = extractPostsStrict(payload);
    } catch (err) {
      errors.push(`page ${page}: invalid response schema from ${requestUrl}: ${err.message}`);
      continue;
    }

    fetchedPages += 1;
    if (items.length === 0) break;
    totalRawEvents += items.length;
    for (const item of items) {
      const eventDate = dateFromCreatedAt(item?.createdAt);
      if (eventDate !== options.reportDate) {
        continue;
      }
      const key = eventKey(item, rawIndex);
      rawIndex += 1;
      if (seenKeys.has(key)) continue;
      seenKeys.add(key);
      uniqueRaw.push(item);
    }
    if (options.delaySeconds > 0) {
      await sleep(Math.round(options.delaySeconds * 1000));
    }
  }

  const events = uniqueRaw.map((item, index) => normalizeEvent(item, index));
  events.sort((a, b) => parseDateTime(b.createdAt) - parseDateTime(a.createdAt));

  const outputPayload = {
    meta: {
      endpoint: API_URL,
      page_size: options.pageSize,
      max_page: options.maxPage,
      last_page_checked: lastPageChecked,
      fetched_pages: fetchedPages,
      total_raw_events: totalRawEvents,
      total_unique_events: uniqueRaw.length,
      generated_at: localDateTime(),
      report_date: options.reportDate,
      current_date: localDateISO(),
      filter_rule: "createdAt date must equal report_date (default: yesterday)",
      source_list_field: "posts",
      response_path: "data.posts",
      kept_fields: ["brief", "createdAt", "originalLink", "postTitle", "tags"],
      errors,
    },
    events,
  };

  const outputJsonText = `${JSON.stringify(outputPayload)}\n`;
  process.stdout.write(outputJsonText);
}

main().catch((err) => {
  console.error(`[ERROR] ${err.message}`);
  process.exit(1);
});


path: scripts/run_full_report.mjs
content: #!/usr/bin/env node

import { spawn } from "node:child_process";
import path from "node:path";
import { fileURLToPath } from "node:url";

const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
const FETCH_SCRIPT = path.join(SCRIPT_DIR, "fetch_events.mjs");
const REPORT_SCRIPT = path.join(SCRIPT_DIR, "generate_report.mjs");

function usage(exitCode = 2) {
  console.error(
    [
      "Usage: run_full_report.mjs [options]",
      "",
      "Options:",
      "  --report-date <YYYY-MM-DD>  Report date (optional)",
      "  --max-page <n>              Max pages (default: 5)",
      "  --page-size <n>             Page size (default: 10)",
      "  --timeout-seconds <n>       Request timeout (default: 15)",
      "  --retry <n>                 Retry times (default: 3)",
      "  --delay-seconds <n>         Delay between page requests (default: 0)",
      "  --top-n <n>                 Render cap; 0 means all (default: 0)",
      "  -h, --help                  Show this help",
    ].join("\n")
  );
  process.exit(exitCode);
}

function parseArgs(argv) {
  const options = {
    reportDate: "",
    maxPage: "5",
    pageSize: "10",
    timeoutSeconds: "15",
    retry: "3",
    delaySeconds: "0",
    topN: "0",
  };

  for (let i = 0; i < argv.length; i += 1) {
    const arg = argv[i];
    if (arg === "-h" || arg === "--help") usage(0);

    if (arg === "--report-date") {
      options.reportDate = argv[i + 1] ?? "";
      i += 1;
      continue;
    }
    if (arg === "--max-page") {
      options.maxPage = argv[i + 1] ?? options.maxPage;
      i += 1;
      continue;
    }
    if (arg === "--page-size") {
      options.pageSize = argv[i + 1] ?? options.pageSize;
      i += 1;
      continue;
    }
    if (arg === "--timeout-seconds") {
      options.timeoutSeconds = argv[i + 1] ?? options.timeoutSeconds;
      i += 1;
      continue;
    }
    if (arg === "--retry") {
      options.retry = argv[i + 1] ?? options.retry;
      i += 1;
      continue;
    }
    if (arg === "--delay-seconds") {
      options.delaySeconds = argv[i + 1] ?? options.delaySeconds;
      i += 1;
      continue;
    }
    if (arg === "--top-n") {
      options.topN = argv[i + 1] ?? options.topN;
      i += 1;
      continue;
    }
    console.error(`Unknown arg: ${arg}`);
    usage();
  }

  return options;
}

function run(options) {
  return new Promise((resolve, reject) => {
    const fetchArgs = [
      FETCH_SCRIPT,
      "--max-page",
      options.maxPage,
      "--page-size",
      options.pageSize,
      "--timeout-seconds",
      options.timeoutSeconds,
      "--retry",
      options.retry,
      "--delay-seconds",
      options.delaySeconds,
      "--stdout-json",
    ];
    if (options.reportDate) {
      fetchArgs.push("--report-date", options.reportDate);
    }

    const reportArgs = [
      REPORT_SCRIPT,
      "--input-json",
      "-",
      "--top-n",
      options.topN,
      "--stdout",
    ];

    const fetchProc = spawn(process.execPath, fetchArgs, {
      stdio: ["ignore", "pipe", "pipe"],
    });
    const reportProc = spawn(process.execPath, reportArgs, {
      stdio: ["pipe", "pipe", "pipe"],
    });

    fetchProc.stdout.pipe(reportProc.stdin);
    fetchProc.stderr.pipe(process.stderr);
    reportProc.stdout.pipe(process.stdout);
    reportProc.stderr.pipe(process.stderr);

    let fetchCode = null;
    let reportCode = null;

    const maybeDone = () => {
      if (fetchCode === null || reportCode === null) return;
      if (fetchCode !== 0) {
        reject(new Error(`fetch step failed with code ${fetchCode}`));
        return;
      }
      if (reportCode !== 0) {
        reject(new Error(`report step failed with code ${reportCode}`));
        return;
      }
      resolve();
    };

    fetchProc.on("error", (err) => reject(err));
    reportProc.on("error", (err) => reject(err));
    fetchProc.on("close", (code) => {
      fetchCode = code ?? 1;
      maybeDone();
    });
    reportProc.on("close", (code) => {
      reportCode = code ?? 1;
      maybeDone();
    });
  });
}

async function main() {
  const options = parseArgs(process.argv.slice(2));
  await run(options);
}

main().catch((err) => {
  console.error(`[ERROR] ${err.message}`);
  process.exit(1);
});


path: scripts/generate_report.mjs
content: #!/usr/bin/env node

import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";

const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
const SKILL_DIR = path.dirname(SCRIPT_DIR);

function usage(exitCode = 2) {
  console.error(
    [
      "Usage: generate_report.mjs --input-json <path|-> [--output <path>] [--top-n 0] [--stdout]",
      "",
      "Options:",
      "  --input-json <path|-> Input normalized JSON file, or '-' for stdin (required)",
      "  --output <path>       Disabled (report is always printed to stdout)",
      "  --top-n <n>           Max events to display (default: 0=all, bounds: 0-500)",
      "  --stdout              Kept for compatibility (stdout is always enabled)",
      "  -h, --help            Show this help",
    ].join("\n")
  );
  process.exit(exitCode);
}

function parseIntOrDefault(value, defaultValue) {
  const num = Number.parseInt(String(value ?? ""), 10);
  return Number.isFinite(num) ? num : defaultValue;
}

function localDateISO() {
  const now = new Date();
  const year = String(now.getFullYear());
  const month = String(now.getMonth() + 1).padStart(2, "0");
  const day = String(now.getDate()).padStart(2, "0");
  return `${year}-${month}-${day}`;
}

function clamp(value, low, high) {
  return Math.max(low, Math.min(value, high));
}

function resolvePath(value, fallbackAbsolutePath = "") {
  if (!value) return fallbackAbsolutePath;
  if (path.isAbsolute(value)) return value;
  return path.join(SKILL_DIR, value);
}

function parseArgs(argv) {
  const options = {
    inputJson: "",
    topN: 0,
  };

  for (let i = 0; i < argv.length; i += 1) {
    const arg = argv[i];
    if (arg === "-h" || arg === "--help") usage(0);

    if (arg === "--input-json") {
      options.inputJson = argv[i + 1] ?? "";
      i += 1;
      continue;
    }
    if (arg === "--output") {
      console.error("--output is disabled. This script is stdout-only.");
      process.exit(2);
    }
    if (arg === "--top-n") {
      options.topN = parseIntOrDefault(argv[i + 1], options.topN);
      i += 1;
      continue;
    }
    if (arg === "--stdout") {
      continue;
    }

    console.error(`Unknown arg: ${arg}`);
    usage();
  }

  if (!options.inputJson) usage();
  options.topN = clamp(options.topN, 0, 500);
  return options;
}

function parseDateTime(value) {
  if (!value) return 0;
  const ts = new Date(String(value).replace(" ", "T")).getTime();
  return Number.isFinite(ts) ? ts : 0;
}

function normalizeTags(value) {
  if (!Array.isArray(value)) return [];
  const seen = new Set();
  const tags = [];
  for (const item of value) {
    const tag = typeof item === "string" ? item : item?.tagName;
    const name = String(tag ?? "").trim();
    if (!name || seen.has(name)) continue;
    seen.add(name);
    tags.push(name);
  }
  return tags;
}

function extractRounds(text) {
  const source = String(text ?? "");
  const roundRegex =
    /(Pre-?A\+?轮|Pre-?B\+?轮|Pre-?C\+?轮|A\+\+轮|A\+轮|A轮|B\+\+轮|B\+轮|B轮|C\+\+轮|C\+轮|C轮|D\+\+轮|D\+轮|D轮|E轮|F轮|天使轮|种子轮|战略投资|股权融资)/g;
  const rounds = [];
  const seen = new Set();
  for (const match of source.matchAll(roundRegex)) {
    const value = String(match[1] ?? "").trim();
    if (!value || seen.has(value)) continue;
    seen.add(value);
    rounds.push(value);
  }
  return rounds;
}

function splitEntityList(text) {
  return String(text ?? "")
    .split(/[、,，\/]+/)
    .map((x) => x.trim())
    .filter(Boolean);
}

function extractInvestors(title, brief) {
  const fromBrief = [];
  const patterns = [
    /由([^，。；]+?)(领投|投资|参投|联合投资)/,
    /投资方(?:为|是)([^，。；]+)/,
    /获([^，。；]+?)投资/,
    /([^，。；]+?)领投/,
  ];

  for (const pattern of patterns) {
    const m = String(brief ?? "").match(pattern);
    if (m?.[1]) fromBrief.push(...splitEntityList(m[1]));
  }

  const titleInvestor = String(title ?? "").match(/^(.+?)(?:战略)?投资/);
  if (titleInvestor?.[1] && !String(title ?? "").includes("获")) {
    fromBrief.push(...splitEntityList(titleInvestor[1]));
  }

  const rounds = extractRounds(`${title} ${brief}`);
  const cleaned = [];
  const seen = new Set();
  for (const item of fromBrief) {
    let value = item;
    for (const round of rounds) value = value.replaceAll(round, "");
    value = value
      .replace(/^由/, "")
      .replace(/^(多家|机构联合)$/g, "")
      .trim();
    if (!value || seen.has(value)) continue;
    seen.add(value);
    cleaned.push(value);
  }
  return cleaned;
}

function extractCompanyName(title, brief) {
  const titleText = String(title ?? "").trim();
  if (!titleText) return "未披露";

  if (titleText.includes("获")) {
    const left = titleText.split("获")[0]?.trim();
    if (left) return left;
  }

  if (titleText.includes("战略投资")) {
    const right = titleText.split("战略投资")[1]?.trim();
    if (right) return right.replace(/(Pre-?[A-Z]\+?轮|[A-F]\+?轮|天使轮|种子轮)$/, "").trim() || right;
  }

  if (titleText.includes("投资")) {
    const right = titleText.split("投资")[1]?.trim();
    if (right) {
      return right.replace(/(Pre-?[A-Z]\+?轮|[A-F]\+?轮|天使轮|种子轮)$/, "").trim() || right;
    }
  }

  const m = String(brief ?? "").match(/(?:服务提供商|研发商|公司|企业)([A-Za-z0-9\u4e00-\u9fa5·（）()\-]+?)(?:宣布|完成|获)/);
  if (m?.[1]) return m[1].trim();
  return titleText;
}

function enrichEvent(event) {
  const title = String(event?.postTitle ?? event?.title ?? "").trim();
  const brief = String(event?.brief ?? event?.summary ?? "").trim();
  const createdAt = String(event?.createdAt ?? event?.raw_created_at ?? "").trim();
  const originalLink = String(event?.originalLink ?? event?.source_url ?? "").trim();
  const tags = normalizeTags(event?.tags ?? event?.industry_tags ?? []);
  const industry = String(tags[0] ?? "").trim() || "未分类";
  return {
    title,
    brief,
    createdAt,
    originalLink,
    tags,
    industry,
    rounds: extractRounds(`${title} ${brief}`),
    investors: extractInvestors(title, brief),
    companyName: extractCompanyName(title, brief),
  };
}

function summarizeEvents(events) {
  const industryCounts = new Map();
  const roundCounts = new Map();
  for (const event of events) {
    const industry = String(event.industry ?? "").trim() || "未分类";
    industryCounts.set(industry, (industryCounts.get(industry) ?? 0) + 1);
    const rounds = Array.isArray(event.rounds) && event.rounds.length > 0 ? event.rounds : ["未披露"];
    for (const round of rounds) {
      const key = String(round ?? "").trim() || "未披露";
      roundCounts.set(key, (roundCounts.get(key) ?? 0) + 1);
    }
  }
  return { industryCounts, roundCounts };
}

function formatDistribution(countMap) {
  return [...countMap.entries()]
    .sort((a, b) => b[1] - a[1])
    .map(([name, count]) => `${name}（${count}起）`)
    .join("、");
}

function cleanCell(value, maxLen = 90) {
  const text = String(value ?? "")
    .replace(/\s+/g, " ")
    .trim();
  if (text.length <= maxLen) return text || "-";
  return `${text.slice(0, maxLen)}...`;
}

function groupEventsByIndustry(events) {
  const groups = new Map();
  for (const event of events) {
    const industry = String(event.industry ?? "").trim() || "未分类";
    if (!groups.has(industry)) groups.set(industry, []);
    groups.get(industry).push(event);
  }
  return [...groups.entries()].sort((a, b) => b[1].length - a[1].length);
}

function buildIndustryGroupedLines(events) {
  const lines = [];
  if (events.length === 0) {
    lines.push("暂无融资事件。");
    return lines;
  }

  const groups = groupEventsByIndustry(events);
  let index = 1;
  for (const [industry, group] of groups) {
    lines.push(`### 行业：${industry}（${group.length}起）`);
    lines.push("");
    for (const event of group) {
      const companyName = cleanCell(event.companyName || "未披露", 50);
      const rounds = Array.isArray(event.rounds) && event.rounds.length > 0
        ? cleanCell(event.rounds.join("、"), 40)
        : "未披露";
      const investors = Array.isArray(event.investors) && event.investors.length > 0
        ? cleanCell(event.investors.join("、"), 60)
        : "未披露";
      const intro = cleanCell(event.brief || "-", 110);
      const link = String(event.originalLink ?? "").trim();
      const linkCell = link ? `[查看原文](${link})` : "-";

      lines.push(`${index}. 公司简称：${companyName}`);
      lines.push(`轮次：${rounds}`);
      lines.push(`投资方：${investors}`);
      lines.push(`事件摘要：${intro}`);
      lines.push(`来源链接：${linkCell}`);
      lines.push("");
      index += 1;
    }
    lines.push("");
  }
  return lines;
}

async function readStdin() {
  const chunks = [];
  for await (const chunk of process.stdin) {
    chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
  }
  return Buffer.concat(chunks).toString("utf-8");
}

function formatCnDate(reportDate) {
  const match = String(reportDate).match(/^(\d{4})-(\d{2})-(\d{2})$/);
  if (!match) return `${reportDate}投融资日报`;
  const year = Number.parseInt(match[1], 10);
  const month = Number.parseInt(match[2], 10);
  const day = Number.parseInt(match[3], 10);
  return `${year}年${month}月${day}日投融资日报`;
}

function buildReportText(reportDate, allEvents, renderedEvents) {
  const lines = [];
  lines.push(formatCnDate(reportDate));
  lines.push("核心数据概览");
  lines.push(`融资事件总数：${allEvents.length}起`);

  const { industryCounts, roundCounts } = summarizeEvents(allEvents);
  lines.push(`涉及行业分布：${formatDistribution(industryCounts) || "暂无"}`);
  lines.push(`融资轮次分布：${formatDistribution(roundCounts) || "暂无"}`);
  lines.push("注：部分事件涉及多轮次或多投资方");
  lines.push(`展示条数：${renderedEvents.length}/${allEvents.length}`);
  lines.push("");
  lines.push("融资事件按行业分类");
  lines.push("");
  lines.push(...buildIndustryGroupedLines(renderedEvents));
  if (renderedEvents.length < allEvents.length) {
    lines.push("");
    lines.push("说明：当前为简表模式。需要全量请使用 `--top-n 0`。");
  }
  return `${lines.join("\n")}\n`;
}

async function main() {
  const options = parseArgs(process.argv.slice(2));
  const inputPath = options.inputJson === "-" ? "-" : resolvePath(options.inputJson);
  const raw = inputPath === "-" ? await readStdin() : await fs.readFile(inputPath, "utf-8");
  const payload = JSON.parse(raw);

  const meta = payload?.meta ?? {};
  const reportDate = String(meta.report_date ?? "").trim() || localDateISO();

  const events = Array.isArray(payload?.events)
    ? payload.events.filter((x) => x && typeof x === "object").map((x) => enrichEvent(x))
    : [];
  events.sort((a, b) => parseDateTime(b.createdAt) - parseDateTime(a.createdAt));

  const renderedEvents = options.topN > 0 ? events.slice(0, options.topN) : events;

  const reportText = buildReportText(reportDate, events, renderedEvents);

  process.stdout.write(reportText);
}

main().catch((err) => {
  console.error(`[ERROR] ${err.message}`);
  process.exit(1);
});


Security scan verdicts — ClawScan: clean | static analysis: suspicious | VirusTotal: clean | SkillSpector: clean (LOW)