Skill: josephyb97/scholargraph (version 1.4.3)

--- name: scholargraph description: Academic literature intelligence toolkit for multi-source paper search, analysis, and knowledge graph building with AI assistance. metadata: openclaw: emoji: "📚" version: "1.0.0" source: type: github url: https://github.com/Josephyb97/ScholarGraph license: MIT requires: bins: - bun optionalBins: - python3 env: - AI_PROVIDER optionalEnv: - OPENAI_API_KEY - DEEPSEEK_API_KEY - QWEN_API_KEY - ZHIPU_API_KEY - SERPER_API_KEY - NCBI_API_KEY - IEEE_API_KEY - CORE_API_KEY - UNPAYWALL_EMAIL - CROSSREF_MAILTO - SERPAPI_KEY install: command: bun install verify: bun run cli.ts --help security: network: true filesystem: true llmPrompts: true notes: | - Makes API calls to academic sources (arXiv, Semantic Scholar, etc.) - Stores data in local SQLite database - Uses custom LLM system prompts for structured output - Optional Python dependencies (pymupdf, python-pptx) for PDF/PPT features --- # ScholarGraph - Academic Literature Intelligence Toolkit ## Overview ScholarGraph is a comprehensive academic literature intelligence toolkit that helps researchers efficiently search, analyze, and manage academic papers using AI-powered tools. Features 11 academic search sources with intelligent domain-based source selection and PDF download capabilities. ## Security & Privacy This skill operates with the following permissions: - **Network Access**: Queries academic APIs (arXiv, Semantic Scholar, OpenAlex, PubMed, CrossRef, DBLP, IEEE, CORE, Google Scholar, Unpaywall) and web search services - **File System**: Reads/writes configuration files, downloads PDFs, stores knowledge graphs in SQLite database (`data/knowledge-graphs.db`) - **LLM Integration**: Sends custom system prompts to AI providers for structured JSON output (concept extraction, paper analysis, etc.) - **Optional Python**: PDF figure extraction (pymupdf) and PPT export (python-pptx) require Python 3.8+ **Data Storage**: All data is stored locally. No telemetry or analytics are collected. **API Keys**: Optional API keys are only used for their respective services and are never transmitted elsewhere. **Source Code**: Open source under MIT license at https://github.com/Josephyb97/ScholarGraph ## Features ### Core Modules (6) 1. **Literature Search** - Multi-source academic paper discovery (11 sources) - **Free sources**: arXiv, Semantic Scholar, OpenAlex (250M+), PubMed (biomedical), CrossRef (150M+ DOI), DBLP (CS), Web Search - **API-key sources**: IEEE Xplore, CORE, Google Scholar (SerpAPI), Unpaywall (OA PDF) - Adapter-based plugin architecture for easy extension - Complementary search strategy with auto domain detection (biomedical/cs/engineering/physics) - Priority-based source selection per domain - Query expansion for better search results - PDF download with multi-strategy URL resolution 2. **Concept Learner** - Rapid knowledge framework construction - Generate structured learning cards - Include code examples and related papers - Support beginner/intermediate/advanced depth levels 3. **Knowledge Gap Detector** - Proactive blind spot identification - Analyze knowledge coverage in specific domains - Identify critical, recommended, and optional gaps - Provide learning recommendations and time estimates 4. **Progress Tracker** - Real-time field monitoring - Track research topics and keywords - Generate daily/weekly/monthly reports - Monitor trending papers and topics 5. **Paper Analyzer** - Deep paper analysis - Extract key contributions and insights - Support quick/standard/deep analysis modes - Generate structured analysis reports 6. **Knowledge Graph Builder** - Concept relationship visualization - Build interactive knowledge graphs - Support Mermaid and JSON output formats - Find learning paths between concepts - SQLite-based persistent storage - Bidirectional concept-paper indexing ### Advanced Features (9) 7. **Review Detector** - Automatic review paper identification - Multi-dimensional scoring (title 30% + citations 25% + abstract 25% + AI 20%) - Chinese and English keyword support - Confidence-based filtering with user confirmation 8. **Concept Extractor** - Extract concepts from review papers - AI-powered extraction of 15-30 core concepts - Four-level categorization (foundation/core/advanced/application) - Importance scoring and relationship identification - Cross-review deduplication and merging 9. **Review-to-Graph Workflow** - End-to-end pipeline - Search reviews -> Detect -> Confirm -> Analyze -> Extract concepts - Build knowledge graph -> Enrich with key papers -> Index -> Store - Interactive or automatic confirmation mode 10. **Knowledge Graph Query** - Bidirectional literature indexing - Concept -> papers: find papers related to a concept - Paper -> concepts: find concepts covered by a paper - Paper recommendations based on multiple concepts - SQLite-optimized high-performance queries 11. **Compare Concepts** - Compare two concepts - Identify similarities and differences - Provide use case recommendations 12. **Compare Papers** - Compare multiple papers - Find common themes and differences - Generate synthesis analysis 13. **Critique** - Critical paper analysis - Identify strengths and weaknesses - Find research gaps and improvement suggestions - Support custom focus areas 14. **Learning Path** - Find optimal learning paths - Discover paths between concepts - Generate topological learning order - Visualize with Mermaid diagrams 15. **Graph Management** - Manage persistent knowledge graphs - List all saved graphs - View graph statistics - Export graphs to JSON - Visualize with Mermaid 16. **Paper Visualization** - Interactive paper presentation - Convert paper analysis to HTML slide presentations - Academic dark/light themes with responsive typography - Keyboard/touch/scroll navigation, edit mode (E key) - PDF figure extraction (pymupdf) and PPT export (python-pptx) - 8+ slides: title, abstract, key points, methodology, experiments, contributions, limitations, references 17. **Interactive Knowledge Graph** - D3.js force-directed visualization - Convert knowledge graphs to interactive HTML with D3.js v7 - Node size reflects paper count, edge thickness reflects concept tightness - Zoom/pan, node dragging, click-to-detail panel, search, legend - Paper preview bridge: click "View Presentation" to open paper slides in new tab - Category colors: foundation=#4FC3F7, core=#FFB74D, advanced=#CE93D8, application=#81C784 ## Technical Features - **11 Academic Search Sources**: arXiv, Semantic Scholar, OpenAlex, PubMed, CrossRef, DBLP, IEEE Xplore, CORE, Google Scholar, Unpaywall, Web Search - **Complementary Search Strategy**: Auto-detects query domain and selects optimal source combination - **Adapter Pattern**: Plugin-based search source architecture for easy extension - **PDF Download**: Multi-strategy URL resolution (direct, Unpaywall, OpenAlex OA, CORE) - **Multi-AI Provider Support**: 15+ AI providers including OpenAI, Anthropic, DeepSeek, Qwen, Zhipu AI, etc. - **SQLite Persistence**: Knowledge graphs stored in SQLite database via bun:sqlite - **Bidirectional Indexing**: Concept-paper and paper-concept bidirectional query support - **Rate Limiting**: Per-source rate limiting with automatic retry and delay - **Interactive HTML Output**: Paper slide presentations, D3.js knowledge graph visualizations - **Multiple Output Formats**: Markdown, JSON, Mermaid, HTML, PPTX - **TypeScript + Bun**: Fast and type-safe runtime - **CLI + API**: Both command-line and programmatic interfaces ## Installation ```bash # Clone repository git clone https://github.com/Josephyb97/ScholarGraph.git cd ScholarGraph # Install dependencies bun install # Initialize configuration bun run cli.ts config init ``` ## Configuration Set up your AI provider: ```bash # Using OpenAI export AI_PROVIDER=openai export OPENAI_API_KEY="your-api-key" # Using DeepSeek export AI_PROVIDER=deepseek export DEEPSEEK_API_KEY="your-api-key" # Using Qwen (通义千问) export AI_PROVIDER=qwen export QWEN_API_KEY="your-api-key" ``` ### Academic Source API Keys (optional, expand search coverage) ```bash export NCBI_API_KEY="your-key" # PubMed high-speed access (10 req/s) export IEEE_API_KEY="your-key" # IEEE Xplore engineering papers export CORE_API_KEY="your-key" # CORE open access full text export UNPAYWALL_EMAIL="[REDACTED_SECRET]" # Unpaywall OA PDF resolver export CROSSREF_MAILTO="[REDACTED_SECRET]" # CrossRef polite pool (higher rate) export SERPAPI_KEY="your-key" # Google Scholar (via SerpAPI) export SERPER_API_KEY="your-key" # Web search via Serper ``` ## Usage Examples ### Search Literature ```bash # Auto-select best sources based on query domain lit search "transformer attention" --limit 20 # Specify domain for optimized source selection lit search "CRISPR gene editing" --domain biomedical # Use specific sources (comma-separated) lit search "deep learning" --source semantic_scholar,arxiv,openalex --sort citations # Search and download PDFs lit search "attention is all you need" --download --limit 3 ``` ### Download PDFs ```bash # Search and download PDFs lit download "transformer" --limit 5 --output ./papers ``` ### Learn Concepts ```bash lit learn "BERT" --depth advanced --papers --code --output bert-card.md ``` ### Detect Knowledge Gaps ```bash lit detect --domain "Deep Learning" --known "CNN,RNN" --output gaps.md ``` ### Analyze Papers ```bash lit analyze "https://arxiv.org/abs/1706.03762" --mode deep --output analysis.md ``` ### Build Knowledge Graph ```bash lit graph transformer attention BERT GPT --format mermaid --output graph.md ``` ### Compare Concepts ```bash lit compare concepts CNN RNN --output comparison.md ``` ### Compare Papers ```bash lit compare papers "url1" "url2" "url3" --output comparison.md ``` ### Critical Analysis ```bash lit critique "paper-url" --focus "novelty,scalability" --output critique.md ``` ### Find Learning Path ```bash lit path "Machine Learning" "Deep Learning" --concepts "Neural Networks" --output path.md ``` ### Search Review Papers ```bash lit review-search "attention mechanism" --limit 10 ``` ### Build Knowledge Graph from Reviews ```bash # From search query (interactive mode) lit review-graph "deep learning" --output dl-graph --enrich # From specific URL lit review-graph "https://arxiv.org/abs/xxxx" --output my-graph --enrich # Auto-confirm mode (non-interactive) lit review-graph "transformer" --output tf-graph --enrich --auto-confirm ``` ### Query Knowledge Graph ```bash # Find papers by concept lit query concept "transformer" --graph dl-graph --limit 20 # Find concepts by paper lit query paper "https://arxiv.org/abs/1706.03762" --graph dl-graph ``` ### Manage Knowledge Graphs ```bash # List all graphs lit graph-list # View graph statistics lit graph-stats dl-graph # Visualize graph lit graph-viz dl-graph --format mermaid --output graph.md # Export graph lit graph-export dl-graph --output dl-graph.json ``` ### Paper Visualization ```bash # Generate interactive HTML presentation lit paper-viz "https://arxiv.org/abs/1706.03762" --output attention.html # With theme and PPT export lit paper-viz "https://arxiv.org/abs/1706.03762" --mode deep --theme academic-light --ppt # Manually provide figures lit paper-viz "https://example.com/paper" --figures ./my-figures ``` ### Interactive Knowledge Graph ```bash # Generate interactive D3.js graph from existing knowledge graph lit graph-interactive dl-graph --output dl-interactive.html # Without paper data (lighter weight) lit graph-interactive my-graph --no-paper-viz ``` ## Use Cases ### 1. Quick Field Onboarding - Learn core concepts - Detect prerequisite gaps - Build knowledge graph - Plan learning path ### 2. Deep Paper Understanding - Analyze paper in depth - Perform critical analysis - Learn new concepts from paper - Compare with related papers ### 3. Research Progress Tracking - Monitor research topics - Track latest papers - Generate progress reports ### 4. Concept Comparison - Compare technical approaches - Evaluate different models - Build comparison graphs ### 5. Review-Driven Knowledge Building - Search and identify review papers - Extract concepts from reviews - Build persistent knowledge graphs - Query concept-paper relationships ### 6. Paper Visualization & Graph Exploration - Analyze paper and generate interactive HTML presentation - Build knowledge graph from reviews - Generate interactive D3.js graph with paper preview - Click nodes to view paper details and open presentations ## Project Structure ``` ScholarGraph/ ├── cli.ts # Unified CLI entry ├── config.ts # Configuration management ├── README.md # Project documentation ├── CHANGELOG.md # Version history ├── SKILL.md # This file │ ├── shared/ # Shared modules │ ├── ai-provider.ts # AI provider abstraction │ ├── types.ts # Type definitions │ ├── validators.ts # Parameter validation │ ├── errors.ts # Error handling │ └── utils.ts # Utility functions │ ├── literature-search/ # Literature search module │ └── scripts/ │ ├── search.ts # Search engine core │ ├── types.ts # Type definitions │ ├── query-expander.ts # Query expansion │ ├── search-strategy.ts # Complementary search strategy │ ├── pdf-downloader.ts # PDF download module │ └── adapters/ # Search source adapters │ ├── base.ts # Adapter interface & base class │ ├── registry.ts # Adapter registry │ ├── index.ts # Barrel export │ ├── arxiv-adapter.ts │ ├── semantic-scholar-adapter.ts │ ├── web-adapter.ts │ ├── openalex-adapter.ts │ ├── pubmed-adapter.ts │ ├── crossref-adapter.ts │ ├── dblp-adapter.ts │ ├── ieee-adapter.ts │ ├── core-adapter.ts │ ├── unpaywall-adapter.ts │ └── google-scholar-adapter.ts │ ├── concept-learner/ # Concept learning module ├── knowledge-gap-detector/ # Gap detection module ├── progress-tracker/ # Progress tracking module ├── paper-analyzer/ # Paper analysis module │ ├── review-detector/ # Review paper identification │ └── scripts/ │ ├── detect.ts # Multi-dimensional scoring │ └── types.ts │ ├── concept-extractor/ # Concept extraction from reviews │ └── scripts/ │ ├── extract.ts # AI-powered extraction │ └── types.ts │ ├── knowledge-graph/ # Knowledge graph module │ └── scripts/ │ ├── graph.ts # Graph building core │ ├── indexer.ts # Bidirectional indexing │ ├── storage.ts # SQLite persistence │ └── enricher.ts # Key paper association │ ├── paper-viz/ # Paper visualization │ └── scripts/ │ ├── types.ts # Presentation data interfaces │ ├── slide-builder.ts # PaperAnalysis → slides │ ├── html-generator.ts # Self-contained HTML generation │ ├── pdf-figure-extractor.ts # PDF figure extraction (pymupdf) │ └── ppt-exporter.ts # PPT export (python-pptx) │ ├── graph-viz/ # Interactive knowledge graph │ └── scripts/ │ ├── types.ts # D3 graph data interfaces │ ├── graph-data-adapter.ts # KnowledgeGraph → D3 data │ ├── html-generator.ts # Interactive HTML (D3.js v7) │ └── paper-viz-bridge.ts # Graph → paper presentation bridge │ ├── workflows/ # End-to-end workflows │ └── review-to-graph.ts # Review to graph pipeline │ ├── data/ # Data directory (auto-created) │ └── knowledge-graphs.db # SQLite database │ ├── downloads/ # PDF downloads (auto-created) │ └── pdfs/ │ └── metadata.json # Download index │ └── test/ # Tests and documentation ├── ADVANCED_FEATURES.md ├── TEST_RESULTS.md └── scripts/ ``` ## Supported AI Providers ### International - OpenAI - Anthropic (Claude) - Azure OpenAI - Groq - Together AI - Ollama (local) ### China - 通义千问 (Qwen/DashScope) - DeepSeek - 智谱 AI (GLM) - MiniMax - Moonshot (Kimi) - 百川 AI (Baichuan) - 零一万物 (Yi) - 豆包 (Doubao) ## Output Formats ### Markdown Reports - Concept cards with definitions, components, history, applications - Gap reports with analysis and recommendations - Progress reports with trending topics - Paper analyses with methods, experiments, contributions - Comparison analyses with similarities and differences - Critical analyses with strengths, weaknesses, and suggestions ### JSON Data Structured data for programmatic processing ### Mermaid Diagrams Interactive knowledge graphs and learning paths ### Interactive HTML - Paper slide presentations with keyboard/scroll/touch navigation - D3.js force-directed knowledge graph with zoom, search, and paper panel ## Requirements - Bun 1.3+ or Node.js 18+ - AI provider API key - Internet connection for paper search - Python 3.8+ (optional, for PDF figure extraction and PPT export) ## License MIT License ## Links - GitHub: https://github.com/Josephyb97/ScholarGraph - Issues: https://github.com/Josephyb97/ScholarGraph/issues - Discussions: https://github.com/Josephyb97/ScholarGraph/discussions ## Version Current version: 1.0.0 ## Author ScholarGraph Team --- **Design Inspirations**: - [frontend-slides](https://github.com/zarazhangrui/frontend-slides) - Paper slide presentation design reference - [Argo Scholar](https://github.com/poloclub/argo-scholar) - Interactive knowledge graph design reference *For detailed documentation, see README.md* *For advanced features, see test/ADVANCED_FEATURES.md* *For test results, see test/TEST_RESULTS.md*

path: CHANGELOG.md
content: # 更新日志 (CHANGELOG)

## [1.0.0] - 2026-02-28

### 🎉 首次发布

完整的学术文献智能工具包，包含 10 大核心功能。

### ✨ 新增功能

#### 基础功能 (6个)
- **文献检索**: 跨 arXiv、Semantic Scholar 和 Web 的多源检索
- **概念学习**: 生成结构化学习卡片，支持代码示例和相关论文
- **知识盲区检测**: 主动识别知识体系中的盲点
- **进展追踪**: 实时监控研究领域动态，自动生成报告
- **论文分析**: 深度论文分析，支持 quick/standard/deep 三种模式
- **知识图谱**: 可视化概念关系，支持 Mermaid 和 JSON 格式

#### 高级功能 (4个)
- **概念对比**: 对比两个概念的相似点、差异点和使用场景
- **论文对比**: 对比 2-N 篇论文的共同主题和差异
- **批判性分析**: 识别论文的优点、缺点、研究空白和改进建议
- **学习路径**: 查找从一个概念到另一个概念的最优学习路径

### 🔧 技术特性

- **多 AI 提供商支持**: 支持 15+ AI 提供商
  - 国际: OpenAI, Anthropic, Azure OpenAI, Groq, Together AI, Ollama
  - 国内: 通义千问, DeepSeek, 智谱 AI, MiniMax, Moonshot, 百川 AI, 零一万物, 豆包

- **速率限制处理**:
  - Semantic Scholar API 自动重试和延迟
  - 最小 1 秒请求间隔
  - 指数退避重试机制

- **多种输出格式**:
  - Markdown 报告（便于阅读和分享）
  - JSON 数据（便于程序化处理）
  - Mermaid 图表（便于可视化）

### 📚 文档

- **README.md**: 完整的项目文档和使用指南
- **ADVANCED_FEATURES.md**: 高级功能详细说明
- **TEST_RESULTS.md**: 全面的测试结果报告

### ✅ 测试覆盖

- ✅ 所有 10 个核心功能测试通过
- ✅ CLI 和编程 API 均可正常使用
- ✅ 支持文件输出和控制台输出
- ✅ 多 AI 提供商测试通过

### 🐛 已知问题

- Semantic Scholar API 有速率限制，建议使用 arXiv URL
- 需要配置 SERPER_API_KEY 以启用 web 搜索降级

### 📦 依赖

- Bun 1.3+
- TypeScript 5.0+
- z-ai-web-dev-sdk
- axios

---

## 未来计划

### v1.1.0 (计划中)
- [ ] 添加更多数据源支持（Google Scholar, PubMed）
- [ ] 支持批量论文分析
- [ ] 添加论文引用网络分析
- [ ] 支持自定义 AI 模型配置

### v1.2.0 (计划中)
- [ ] Web UI 界面
- [ ] 数据库持久化
- [ ] 协作功能
- [ ] 导出为 PDF/Word 格式

---

*最后更新: 2026-02-28*


path: cli.ts
content: #!/usr/bin/env bun
/**
 * Literature Skills - Unified CLI
 * 文献检索总结工具统一命令行入口
 *
 * 用法:
 *   lit <command> [options]
 *
 * 命令:
 *   search <query>          检索文献
 *   download <query>        检索并下载 PDF
 *   learn <concept>         学习概念
 *   detect <domain>         检测知识盲区
 *   track <action>          进展追踪
 *   analyze <url>           分析论文
 *   graph <concepts...>     构建知识图谱
 *   config <action>         配置管理
 */

import LiteratureSearch from './literature-search/scripts/search';
import { PdfDownloader } from './literature-search/scripts/pdf-downloader';
import ConceptLearner from './concept-learner/scripts/learn';
import KnowledgeGapDetector from './knowledge-gap-detector/scripts/detect';
import ProgressTracker from './progress-tracker/scripts/track';
import PaperAnalyzer from './paper-analyzer/scripts/analyze';
import KnowledgeGraphBuilder from './knowledge-graph/scripts/graph';
import { ConfigManager, defaultConfig } from './config';
import { getErrorMessage } from './shared/errors';
import { SlideBuilder } from './paper-viz/scripts/slide-builder';
import { generatePaperHtml } from './paper-viz/scripts/html-generator';
import { PdfFigureExtractor } from './paper-viz/scripts/pdf-figure-extractor';
import { PptExporter } from './paper-viz/scripts/ppt-exporter';
import { ACADEMIC_DARK_THEME, ACADEMIC_LIGHT_THEME } from './paper-viz/scripts/types';
import { GraphDataAdapter } from './graph-viz/scripts/graph-data-adapter';
import { generateGraphHtml } from './graph-viz/scripts/html-generator';
import { PaperVizBridge } from './graph-viz/scripts/paper-viz-bridge';
import GraphStorage from './knowledge-graph/scripts/storage';
import {
  validateSearchParams,
  validateLearnParams,
  validateDetectParams,
  validateAnalyzeParams,
  validateGraphParams,
  validatePdfDownloadParams,
  validatePaperVizParams,
  validateGraphInteractiveParams,
  formatValidationErrors,
  isValidSearchSource,
  isValidSortBy,
  isValidLearningDepth,
  isValidAnalysisMode,
  isValidReportType
} from './shared/validators';
import type { SearchSource, SortBy, LearningDepth, AnalysisMode, ReportType } from './shared/types';

const COMMANDS = {
  search: '检索文献',
  download: '检索并下载 PDF',
  learn: '学习概念',
  detect: '检测知识盲区',
  track: '进展追踪',
  analyze: '分析论文',
  graph: '构建知识图谱',
  config: '配置管理',
  compare: '对比分析',
  critique: '批判性分析',
  path: '学习路径',
  'paper-viz': '论文可视化演示',
  'graph-interactive': '交互式知识图谱'
};

function showHelp() {
  console.log(`
📚 Literature Skills - 文献检索总结工具
==========================================

用法:
  lit <command> [options]

命令:
  search <query>              检索相关文献
    --limit <n>               结果数量 (默认: 10, 范围: 1-100)
    --source <s1,s2,...>      数据源 (逗号分隔, 见下方列表)
    --sort <by>               排序方式 (relevance|date|citations)
    --domain <hint>           领域提示 (biomedical|cs|engineering|physics|general)
    --download                同时下载 PDF

  download <query>            检索并下载 PDF
    --limit <n>               结果数量 (默认: 5)
    --source <s1,s2,...>      数据源
    --output <dir>            下载目录 (默认: ./downloads/pdfs)

  learn <concept>             学习概念并生成知识卡片
    --depth <d>               学习深度 (beginner|intermediate|advanced)
    --papers                  包含相关论文
    --code                    包含代码示例
    --output <file>           输出文件

  detect --domain <d>         检测知识盲区
    --known <list>            已知概念 (逗号分隔)
    --output <file>           输出报告文件

  track <action>              进展追踪
    add <type> <value>        添加监控项
    report                    生成报告
    --type <t>                报告类型 (daily|weekly|monthly)
    --output <file>           输出文件

  analyze <url>               分析论文
    --mode <m>                分析模式 (quick|standard|deep)
    --output <file>           输出文件

  graph <concepts...>         构建知识图谱
    --format <f>              输出格式 (mermaid|json)
    --output <file>           输出文件

  compare <type> <items...>   对比分析
    concepts <c1> <c2>        对比两个概念
    papers <url1> <url2>...   对比多篇论文
    --output <file>           输出文件

  critique <url>              批判性分析论文
    --focus <areas>           重点关注领域 (逗号分隔)
    --output <file>           输出文件

  path <from> <to>            查找学习路径
    --concepts <list>         概念列表 (逗号分隔)
    --output <file>           输出文件

  paper-viz <url>              论文可视化演示
    --mode <m>                分析模式 (quick|standard|deep)
    --theme <t>               主题 (academic-dark|academic-light)
    --output <file>           输出 HTML 文件
    --ppt                     同时导出 PPT
    --figures <dir>           手动指定图表目录

  graph-interactive <name>     交互式知识图谱
    --output <file>           输出 HTML 文件
    --no-paper-viz            不嵌入论文数据

  config <action>             配置管理
    init                      初始化配置文件
    show                      显示当前配置
    set <key> <value>         设置配置项
    reset                     重置为默认配置

搜索数据源:
  免费源 (无需 API Key):
    arxiv               arXiv 预印本 (物理/数学/CS)
    semantic_scholar     Semantic Scholar (200M+ 论文)
    openalex             OpenAlex (250M+ 开放学术数据)
    pubmed               PubMed (生物医学文献)
    crossref             CrossRef (DOI 元数据)
    dblp                 DBLP (计算机科学)
    web                  Web 搜索 (需 SERPER_API_KEY)

  需 API Key:
    ieee                 IEEE Xplore (需 IEEE_API_KEY)
    core                 CORE 开放获取 (需 CORE_API_KEY)
    google_scholar       Google Scholar (需 SERPAPI_KEY)
    unpaywall            Unpaywall OA PDF 解析 (需 UNPAYWALL_EMAIL)

环境变量:
  AI_PROVIDER                 AI 提供商 (见下方支持列表)

  # 国际厂商
  OPENAI_API_KEY              OpenAI API 密钥
  ANTHROPIC_API_KEY           Anthropic API 密钥
  AZURE_OPENAI_ENDPOINT       Azure OpenAI 端点
  AZURE_OPENAI_API_KEY        Azure OpenAI 密钥
  GROQ_API_KEY                Groq API 密钥
  TOGETHER_API_KEY            Together AI API 密钥
  OLLAMA_BASE_URL             Ollama 服务地址 (默认: http://localhost:11434)

  # 国内厂商
  QWEN_API_KEY                通义千问 API 密钥 (或 DASHSCOPE_API_KEY)
  DEEPSEEK_API_KEY            DeepSeek API 密钥
  ZHIPU_API_KEY               智谱 AI (GLM) API 密钥
  MINIMAX_API_KEY             MiniMax API 密钥
  MOONSHOT_API_KEY            Moonshot (Kimi) API 密钥
  BAICHUAN_API_KEY            百川 AI API 密钥
  YI_API_KEY                  零一万物 API 密钥
  DOUBAO_API_KEY              豆包 API 密钥

  # 搜索
  SERPER_API_KEY              Serper 搜索 API 密钥 (用于 web 搜索功能)

  # 学术数据源 API Key
  NCBI_API_KEY                PubMed 高速访问密钥
  IEEE_API_KEY                IEEE Xplore API 密钥
  CORE_API_KEY                CORE API 密钥
  UNPAYWALL_EMAIL             Unpaywall 邮箱 (用于 OA PDF 解析)
  CROSSREF_MAILTO             CrossRef 礼貌池邮箱
  SERPAPI_KEY                 SerpAPI 密钥 (Google Scholar)

支持的 AI 提供商:
  zai, openai, anthropic, azure, ollama, qwen, deepseek, zhipu,
  minimax, moonshot, baichuan, yi, doubao, groq, together

示例:
  lit search "transformer attention" --limit 20
  lit search "CRISPR gene editing" --domain biomedical
  lit search "deep learning" --source semantic_scholar,arxiv --sort citations
  lit search "attention is all you need" --download --limit 3
  lit download "transformer" --limit 5 --output ./papers
  lit learn "BERT" --depth advanced --output bert-card.md
  lit detect --domain "NLP" --known "transformer,attention"
  lit track report --type weekly --output weekly-report.md
  lit analyze "https://arxiv.org/abs/2301.07001" --output analysis.md
  lit graph transformer attention BERT GPT --format mermaid

  # 对比两个概念
  lit compare concepts CNN RNN --output cnn-vs-rnn.md

  # 对比多篇论文
  lit compare papers "url1" "url2" --output comparison.md

  # 批判性分析论文
  lit critique "https://arxiv.org/abs/xxx" --focus "scalability,efficiency"

  # 查找学习路径
  lit path "Machine Learning" "Deep Learning" --concepts "ML,NN,DL,CNN"

  # 论文可视化演示
  lit paper-viz "https://arxiv.org/abs/1706.03762" --output attention.html --ppt

  # 交互式知识图谱
  lit graph-interactive dl-graph --output dl-interactive.html

  # 使用 OpenAI 提供商
  AI_PROVIDER=openai lit search "transformer"

  # 使用 DeepSeek 提供商
  AI_PROVIDER=deepseek lit learn "BERT"

  # 使用智谱 AI
  AI_PROVIDER=zhipu lit detect --domain "NLP"
`);
}

async function main() {
  const args = process.argv.slice(2);

  if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
    showHelp();
    process.exit(0);
  }

  const command = args[0];
  const cmdArgs = args.slice(1);

  try {
    switch (command) {
      case 'search':
        await handleSearch(cmdArgs);
        break;

      case 'download':
        await handleDownload(cmdArgs);
        break;

      case 'learn':
        await handleLearn(cmdArgs);
        break;

      case 'detect':
        await handleDetect(cmdArgs);
        break;

      case 'track':
        await handleTrack(cmdArgs);
        break;

      case 'analyze':
        await handleAnalyze(cmdArgs);
        break;

      case 'graph':
        await handleGraph(cmdArgs);
        break;

      case 'config':
        handleConfig(cmdArgs);
        break;

      case 'compare':
        await handleCompare(cmdArgs);
        break;

      case 'critique':
        await handleCritique(cmdArgs);
        break;

      case 'path':
        await handlePath(cmdArgs);
        break;

      case 'paper-viz':
        await handlePaperViz(cmdArgs);
        break;

      case 'graph-interactive':
        await handleGraphInteractive(cmdArgs);
        break;

      default:
        console.error(`Unknown command: ${command}`);
        console.log('Run "lit --help" for usage information.');
        process.exit(1);
    }
  } catch (error: unknown) {
    console.error('Error:', getErrorMessage(error));
    process.exit(1);
  }
}

/**
 * Parse comma-separated source list
 */
function parseSources(sourceArg: string): SearchSource[] {
  return sourceArg.split(',')
    .map(s => s.trim())
    .filter(s => isValidSearchSource(s)) as SearchSource[];
}

async function handleSearch(args: string[]) {
  const query = args[0];

  const limitIndex = args.indexOf('--limit');
  const limit = limitIndex > -1 ? parseInt(args[limitIndex + 1]) : 10;

  const sourceIndex = args.indexOf('--source');
  const sourceArg = sourceIndex > -1 ? args[sourceIndex + 1] : undefined;
  const sources: SearchSource[] | undefined = sourceArg ? parseSources(sourceArg) : undefined;

  const sortIndex = args.indexOf('--sort');
  const sortArg = sortIndex > -1 ? args[sortIndex + 1] : 'relevance';
  const sortBy: SortBy = isValidSortBy(sortArg) ? sortArg : 'relevance';

  const domainIndex = args.indexOf('--domain');
  const domainHint = domainIndex > -1 ? args[domainIndex + 1] : undefined;

  const shouldDownload = args.includes('--download');

  // 验证参数
  const validation = validateSearchParams({
    query,
    limit,
    source: sources?.[0],
    sortBy
  });
  if (!validation.valid) {
    console.error('Validation errors:\n' + formatValidationErrors(validation.errors));
    process.exit(1);
  }

  const searcher = new LiteratureSearch();
  await searcher.initialize();

  // Show domain detection info
  if (!sources) {
    const strategy = searcher.getStrategy();
    const domainInfo = strategy.getDomainInfo(query, domainHint);
    const selectedSources = strategy.selectSources(query, searcher.getRegistry(), domainHint);
    console.log(`🔍 Searching for "${query}" [domain: ${domainInfo.domain}${domainInfo.isHinted ? ' (hinted)' : ' (auto)'}]`);
    console.log(`   Sources: ${selectedSources.join(', ')}`);
  } else {
    console.log(`🔍 Searching for "${query}" [sources: ${sources.join(', ')}]`);
  }

  const results = await searcher.search(query, {
    limit,
    sources,
    sortBy,
    domainHint
  });

  console.log(`\n📚 Found ${results.totalResults} results:\n`);

  results.results.forEach((paper, i) => {
    console.log(`${i + 1}. ${paper.title}`);
    console.log(`   Authors: ${paper.authors.slice(0, 3).join(', ')}${paper.authors.length > 3 ? '...' : ''}`);
    console.log(`   Source: ${paper.source} | Date: ${paper.publishDate}${paper.citations ? ` | Citations: ${paper.citations}` : ''}`);
    console.log(`   URL: ${paper.url}`);
    if (paper.doi) console.log(`   DOI: ${paper.doi}`);
    if (paper.openAccess) console.log(`   Open Access: Yes`);
    console.log('');
  });

  // Download PDFs if requested
  if (shouldDownload && results.results.length > 0) {
    console.log('📥 Downloading PDFs...\n');
    const downloader = new PdfDownloader(undefined, searcher.getRegistry());
    const downloads = await downloader.downloadResults(results.results);
    console.log(`\n✅ Downloaded ${downloads.length} PDFs`);
  }
}

async function handleDownload(args: string[]) {
  const query = args[0];

  const limitIndex = args.indexOf('--limit');
  const limit = limitIndex > -1 ? parseInt(args[limitIndex + 1]) : 5;

  const sourceIndex = args.indexOf('--source');
  const sourceArg = sourceIndex > -1 ? args[sourceIndex + 1] : undefined;
  const sources: SearchSource[] | undefined = sourceArg ? parseSources(sourceArg) : undefined;

  const outputIndex = args.indexOf('--output');
  const outputDir = outputIndex > -1 ? args[outputIndex + 1] : undefined;

  // 验证参数
  const validation = validatePdfDownloadParams({ query, limit });
  if (!validation.valid) {
    console.error('Validation errors:\n' + formatValidationErrors(validation.errors));
    process.exit(1);
  }

  const searcher = new LiteratureSearch();
  await searcher.initialize();

  console.log(`🔍 Searching for "${query}"...`);

  const results = await searcher.search(query, { limit, sources });

  if (results.results.length === 0) {
    console.log('No results found.');
    return;
  }

  console.log(`📚 Found ${results.totalResults} results. Downloading PDFs...\n`);

  const downloader = new PdfDownloader(
    { outputDir },
    searcher.getRegistry()
  );
  const downloads = await downloader.downloadResults(results.results);

  console.log(`\n✅ Downloaded ${downloads.length}/${results.results.length} PDFs`);
  if (downloads.length > 0) {
    console.log(`   Saved to: ${outputDir || './downloads/pdfs'}`);
  }
}

async function handleLearn(args: string[]) {
  const concept = args[0];

  const depthIndex = args.indexOf('--depth');
  const depthArg = depthIndex > -1 ? args[depthIndex + 1] : 'intermediate';
  const depth: LearningDepth = isValidLearningDepth(depthArg) ? depthArg : 'intermediate';

  const includePapers = args.includes('--papers');
  const includeCode = args.includes('--code');

  const outputIndex = args.indexOf('--output');
  const outputFile = outputIndex > -1 ? args[outputIndex + 1] : null;

  // 验证参数
  const validation = validateLearnParams({ concept, depth });
  if (!validation.valid) {
    console.error('Validation errors:\n' + formatValidationErrors(validation.errors));
    process.exit(1);
  }

  const learner = new ConceptLearner();
  await learner.initialize();

  console.log(`📖 Learning "${concept}"...`);

  const card = await learner.learn(concept, {
    depth,
    includePapers,
    includeCode
  });

  if (outputFile) {
    const fs = await import('fs');
    fs.writeFileSync(outputFile, learner.toMarkdown(card));
    console.log(`\n✅ Concept card saved to ${outputFile}`);
  } else {
    console.log('\n📋 Concept Card:\n');
    console.log(`Title: ${card.concept}`);
    console.log(`Definition: ${card.definition}`);
    console.log(`\nCore Components: ${card.coreComponents.length}`);
    console.log(`Applications: ${card.applications.length}`);
    console.log(`Related Concepts: ${card.relatedConcepts.length}`);
  }
}

async function handleDetect(args: string[]) {
  const domainIndex = args.indexOf('--domain');
  const domain = domainIndex > -1 ? args[domainIndex + 1] : 'Machine Learning';

  const knownIndex = args.indexOf('--known');
  const known = knownIndex > -1 ? args[knownIndex + 1].split(',').map(s => s.trim()) : [];

  const outputIndex = args.indexOf('--output');
  const outputFile = outputIndex > -1 ? args[outputIndex + 1] : null;

  // 验证参数
  const validation = validateDetectParams({ domain });
  if (!validation.valid) {
    console.error('Validation errors:\n' + formatValidationErrors(validation.errors));
    process.exit(1);
  }

  const detector = new KnowledgeGapDetector();
  await detector.initialize();

  console.log(`🔍 Detecting knowledge gaps in "${domain}"...`);

  const report = await detector.detect({
    domain,
    knownConcepts: known
  });

  if (outputFile) {
    const fs = await import('fs');
    fs.writeFileSync(outputFile, detector.toMarkdown(report));
    console.log(`\n✅ Gap report saved to ${outputFile}`);
  } else {
    console.log('\n📊 Gap Analysis Summary:\n');
    console.log(`Domain: ${report.domain}`);
    console.log(`Coverage: ${report.summary.coveragePercentage}%`);
    console.log(`Total Gaps: ${report.summary.totalGaps}`);
    console.log(`  - Critical: ${report.summary.criticalCount}`);
    console.log(`  - Recommended: ${report.summary.recommendedCount}`);
    console.log(`  - Optional: ${report.summary.optionalCount}`);

    if (report.criticalGaps.length > 0) {
      console.log('\n🚨 Critical Gaps:');
      report.criticalGaps.slice(0, 5).forEach(gap => {
        console.log(`  - ${gap.concept}: ${gap.reason}`);
      });
    }
  }
}

async function handleTrack(args: string[]) {
  const action = args[0];

  const tracker = new ProgressTracker();
  await tracker.initialize();

  if (action === 'report') {
    const typeIndex = args.indexOf('--type');
    const typeArg = typeIndex > -1 ? args[typeIndex + 1] : 'daily';
    const type: ReportType = isValidReportType(typeArg) ? typeArg : 'daily';

    const outputIndex = args.indexOf('--output');
    const outputFile = outputIndex > -1 ? args[outputIndex + 1] : null;

    // 支持 --topic 参数来指定追踪主题
    const topicIndex = args.indexOf('--topic');
    const topic = topicIndex > -1 ? args[topicIndex + 1] : null;

    // 如果指定了主题，添加临时监控项
    if (topic) {
      await tracker.addWatch({ type: 'keyword', value: topic, frequency: type as 'daily' | 'weekly' | 'monthly' });
    } else {
      // 添加一些默认的热门主题用于演示
      await tracker.addWatches([
        { type: 'keyword', value: 'large language model', frequency: 'weekly' },
        { type: 'keyword', value: 'transformer', frequency: 'weekly' },
        { type: 'keyword', value: 'RAG retrieval augmented', frequency: 'weekly' }
      ]);
    }

    console.log(`📊 Generating ${type} report...`);

    const report = await tracker.generateReport({ type, topic: topic || undefined });

    if (outputFile) {
      const fs = await import('fs');
      fs.writeFileSync(outputFile, tracker.toMarkdown(report));
      console.log(`\n✅ Report saved to ${outputFile}`);
    } else {
      console.log('\n📈 Progress Report:\n');
      console.log(`Period: ${report.period.start} ~ ${report.period.end}`);
      console.log(`Total Papers: ${report.summary.totalPapers}`);
      console.log(`Highlighted: ${report.summary.highlightedPapers}`);
      console.log(`Trending: ${report.summary.trendingTopics.slice(0, 5).join(', ')}`);
    }
  } else {
    console.error('Error: Unknown track action. Use "report".');
    process.exit(1);
  }
}

async function handleAnalyze(args: string[]) {
  const url = args[0];

  const modeIndex = args.indexOf('--mode');
  const modeArg = modeIndex > -1 ? args[modeIndex + 1] : 'standard';
  const mode: AnalysisMode = isValidAnalysisMode(modeArg) ? modeArg : 'standard';

  const outputIndex = args.indexOf('--output');
  const outputFile = outputIndex > -1 ? args[outputIndex + 1] : null;

  // 验证参数
  const validation = validateAnalyzeParams({ url, mode });
  if (!validation.valid) {
    console.error('Validation errors:\n' + formatValidationErrors(validation.errors));
    process.exit(1);
  }

  const analyzer = new PaperAnalyzer();
  await analyzer.initialize();

  console.log(`📄 Analyzing paper...`);

  const analysis = await analyzer.analyze({ url, mode });

  if (outputFile) {
    const fs = await import('fs');
    fs.writeFileSync(outputFile, analyzer.toMarkdown(analysis));
    console.log(`\n✅ Analysis saved to ${outputFile}`);
  } else {
    console.log('\n📑 Paper Analysis:\n');
    console.log(`Title: ${analysis.metadata.title}`);
    console.log(`Authors: ${analysis.metadata.authors.join(', ')}`);
    console.log(`Year: ${analysis.metadata.year}`);
    console.log(`\nSummary: ${analysis.summary}`);
    console.log(`\nKey Points: ${analysis.keyPoints.length}`);
    console.log(`Contributions: ${analysis.contributions.length}`);
    console.log(`Limitations: ${analysis.limitations.length}`);
  }
}

async function handleGraph(args: string[]) {
  const formatIndex = args.indexOf('--format');
  const format = formatIndex > -1 ? args[formatIndex + 1] : 'mermaid';

  const outputIndex = args.indexOf('--output');
  const outputFile = outputIndex > -1 ? args[outputIndex + 1] : null;

  // 过滤掉选项参数及其值
  const optionIndices = new Set<number>();
  if (formatIndex > -1) {
    optionIndices.add(formatIndex);
    optionIndices.add(formatIndex + 1);
  }
  if (outputIndex > -1) {
    optionIndices.add(outputIndex);
    optionIndices.add(outputIndex + 1);
  }

  const concepts = args.filter((_, index) => !optionIndices.has(index));

  // 验证参数
  const validation = validateGraphParams({ concepts });
  if (!validation.valid) {
    console.error('Validation errors:\n' + formatValidationErrors(validation.errors));
    process.exit(1);
  }

  const builder = new KnowledgeGraphBuilder();
  await builder.initialize();

  console.log(`🔗 Building knowledge graph...`);

  const graph = await builder.build(concepts);

  let output: string;
  if (format === 'json') {
    output = builder.toJSON(graph);
  } else {
    output = builder.toMermaid(graph);
  }

  if (outputFile) {
    const fs = await import('fs');
    fs.writeFileSync(outputFile, output);
    console.log(`\n✅ Graph saved to ${outputFile}`);
  } else {
    console.log('\n📈 Knowledge Graph:\n');
    console.log(output);
  }
}

function handleConfig(args: string[]) {
  const action = args[0];
  const manager = new ConfigManager();

  switch (action) {
    case 'init':
      manager.save();
      console.log('✅ Configuration initialized at ./literature-config.json');
      break;

    case 'show':
      const config = manager.load();
      console.log(JSON.stringify(config, null, 2));
      break;

    case 'set':
      const key = args[1];
      const value = args[2];
      if (key && value) {
        try {
          const currentConfig = manager.load();

          // 解析嵌套路径 (e.g., "user.level" -> ["user", "level"])
          const keys = key.split('.');
          let target: any = currentConfig;

          // 导航到目标对象
          for (let i = 0; i < keys.length - 1; i++) {
            if (!target[keys[i]]) {
              target[keys[i]] = {};
            }
            target = target[keys[i]];
          }

          // 设置值
          const lastKey = keys[keys.length - 1];
          try {
            target[lastKey] = JSON.parse(value);
          } catch {
            // 如果不是有效 JSON，作为字符串处理
            target[lastKey] = value;
          }

          manager.update(currentConfig);
          console.log(`✅ Set ${key} = ${value}`);
        } catch (error) {
          console.error(`Error setting ${key}:`, error);
          process.exit(1);
        }
      } else {
        console.error('Error: config set requires <key> and <value>');
        process.exit(1);
      }
      break;

    case 'reset':
      manager.reset();
      console.log('✅ Configuration reset to defaults');
      break;

    default:
      console.error('Error: Unknown config action. Use init|show|set|reset.');
      process.exit(1);
  }
}

async function handleCompare(args: string[]) {
  const type = args[0];

  if (type === 'concepts') {
    const concept1 = args[1];
    const concept2 = args[2];

    if (!concept1 || !concept2) {
      console.error('Error: compare concepts requires two concept names');
      process.exit(1);
    }

    const outputIndex = args.indexOf('--output');
    const outputFile = outputIndex > -1 ? args[outputIndex + 1] : null;

    const learner = new ConceptLearner();
    await learner.initialize();

    console.log(`🔄 Comparing "${concept1}" vs "${concept2}"...`);

    const comparison = await learner.compare(concept1, concept2);

    const markdown = `# ${comparison.concept1} vs ${comparison.concept2} - 概念对比

## 📊 相似点

${comparison.similarities.map(s => `- ${s}`).join('\n')}

## 🔀 差异点

${comparison.differences.map(d => `- ${d}`).join('\n')}

## 🎯 使用场景

### 优先使用 ${comparison.concept1}

${comparison.useCases.preferConcept1}

### 优先使用 ${comparison.concept2}

${comparison.useCases.preferConcept2}

---
*生成时间: ${new Date().toISOString()}*
`;

    if (outputFile) {
      const fs = await import('fs');
      fs.writeFileSync(outputFile, markdown);
      console.log(`\n✅ Comparison saved to ${outputFile}`);
    } else {
      console.log('\n📊 Concept Comparison:\n');
      console.log(`${comparison.concept1} vs ${comparison.concept2}\n`);
      console.log('相似点:');
      comparison.similarities.forEach(s => console.log(`  - ${s}`));
      console.log('\n差异点:');
      comparison.differences.forEach(d => console.log(`  - ${d}`));
      console.log('\n使用场景:');
      console.log(`  ${comparison.concept1}: ${comparison.useCases.preferConcept1}`);
      console.log(`  ${comparison.concept2}: ${comparison.useCases.preferConcept2}`);
    }
  } else if (type === 'papers') {
    const outputIndex = args.indexOf('--output');
    const outputFile = outputIndex > -1 ? args[outputIndex + 1] : null;

    // Filter out the type and option arguments
    const optionIndices = new Set<number>();
    optionIndices.add(0); // 'papers'
    if (outputIndex > -1) {
      optionIndices.add(outputIndex);
      optionIndices.add(outputIndex + 1);
    }

    const urls = args.filter((_, index) => !optionIndices.has(index));

    if (urls.length < 2) {
      console.error('Error: compare papers requires at least 2 paper URLs');
      process.exit(1);
    }

    const analyzer = new PaperAnalyzer();
    await analyzer.initialize();

    console.log(`📄 Comparing ${urls.length} papers...`);

    const comparison = await analyzer.compare(urls);

    const markdown = `# 论文对比分析

## 📚 对比论文

${comparison.papers.map((p, i) => `${i + 1}. ${p.title} (${p.year})`).join('\n')}

## 🔗 共同主题

${comparison.commonThemes.map(t => `- ${t}`).join('\n')}

## 🔀 主要差异

${comparison.differences.map(d => `- ${d}`).join('\n')}

## 💡 综合分析

${comparison.synthesis}

---
*生成时间: ${new Date().toISOString()}*
`;

    if (outputFile) {
      const fs = await import('fs');
      fs.writeFileSync(outputFile, markdown);
      console.log(`\n✅ Comparison saved to ${outputFile}`);
    } else {
      console.log('\n📊 Paper Comparison:\n');
      console.log('共同主题:');
      comparison.commonThemes.forEach(t => console.log(`  - ${t}`));
      console.log('\n主要差异:');
      comparison.differences.forEach(d => console.log(`  - ${d}`));
      console.log('\n综合分析:');
      console.log(comparison.synthesis);
    }
  } else {
    console.error('Error: Unknown compare type. Use "concepts" or "papers".');
    process.exit(1);
  }
}

async function handleCritique(args: string[]) {
  const url = args[0];

  if (!url) {
    console.error('Error: critique requires a paper URL');
    process.exit(1);
  }

  const focusIndex = args.indexOf('--focus');
  const focusAreas = focusIndex > -1 ? args[focusIndex + 1].split(',').map(s => s.trim()) : undefined;

  const outputIndex = args.indexOf('--output');
  const outputFile = outputIndex > -1 ? args[outputIndex + 1] : null;

  const analyzer = new PaperAnalyzer();
  await analyzer.initialize();

  console.log(`🔍 Performing critical analysis...`);

  // First get paper metadata
  const analysis = await analyzer.analyze({ url, mode: 'quick' });

  // Then perform critique
  const critique = await analyzer.critique({ url, focusAreas });

  const markdown = `# 论文批判性分析

## 📄 论文信息

- **标题**: ${analysis.metadata.title}
- **作者**: ${analysis.metadata.authors.join(', ')}
- **年份**: ${analysis.metadata.year}

${focusAreas ? `## 🎯 关注领域\n\n${focusAreas.map(a => `- ${a}`).join('\n')}\n` : ''}
## ✅ 优点

${critique.strengths.map(s => `- ${s}`).join('\n')}

## ⚠️ 缺点

${critique.weaknesses.map(w => `- ${w}`).join('\n')}

## 🔍 研究空白

${critique.gaps.map(g => `- ${g}`).join('\n')}

## 💡 改进建议

${critique.suggestions.map(s => `- ${s}`).join('\n')}

## 📊 总体评价

${critique.overallAssessment}

---
*生成时间: ${new Date().toISOString()}*
`;

  if (outputFile) {
    const fs = await import('fs');
    fs.writeFileSync(outputFile, markdown);
    console.log(`\n✅ Critique saved to ${outputFile}`);
  } else {
    console.log('\n🔍 Critical Analysis:\n');
    console.log(`Paper: ${analysis.metadata.title}\n`);
    console.log('优点:');
    critique.strengths.forEach(s => console.log(`  - ${s}`));
    console.log('\n缺点:');
    critique.weaknesses.forEach(w => console.log(`  - ${w}`));
    console.log('\n研究空白:');
    critique.gaps.forEach(g => console.log(`  - ${g}`));
    console.log('\n改进建议:');
    critique.suggestions.forEach(s => console.log(`  - ${s}`));
    console.log('\n总体评价:');
    console.log(critique.overallAssessment);
  }
}

async function handlePath(args: string[]) {
  const from = args[0];
  const to = args[1];

  if (!from || !to) {
    console.error('Error: path requires <from> and <to> concepts');
    process.exit(1);
  }

  const conceptsIndex = args.indexOf('--concepts');
  const conceptsList = conceptsIndex > -1 ? args[conceptsIndex + 1].split(',').map(s => s.trim()) : [];

  const outputIndex = args.indexOf('--output');
  const outputFile = outputIndex > -1 ? args[outputIndex + 1] : null;

  // Build the full concept list including from and to
  const allConcepts = [from, ...conceptsList, to];

  const builder = new KnowledgeGraphBuilder();
  await builder.initialize();

  console.log(`🗺️ Finding learning path from "${from}" to "${to}"...`);

  const graph = await builder.build(allConcepts);
  const path = builder.findPath(graph, from, to);

  if (path.length === 0) {
    console.log(`\n⚠️ No path found from "${from}" to "${to}"`);
    return;
  }

  const markdown = `# 学习路径: ${from} → ${to}

## 🗺️ 推荐路径

${path.map((concept, i) => `${i + 1}. ${concept}`).join('\n')}

## 📊 路径可视化

\`\`\`mermaid
graph LR
${path.map((concept, i) => i < path.length - 1 ? `  ${concept.replace(/\s+/g, '_')}[${concept}] --> ${path[i + 1].replace(/\s+/g, '_')}[${path[i + 1]}]` : '').filter(Boolean).join('\n')}
\`\`\`

## 📚 学习建议

按照上述路径顺序学习，每个概念都是下一个概念的基础。

---
*生成时间: ${new Date().toISOString()}*
`;

  if (outputFile) {
    const fs = await import('fs');
    fs.writeFileSync(outputFile, markdown);
    console.log(`\n✅ Learning path saved to ${outputFile}`);
  } else {
    console.log('\n🗺️ Learning Path:\n');
    console.log(path.map((concept, i) => `  ${i + 1}. ${concept}`).join('\n'));
    console.log('\n路径: ' + path.join(' → '));
  }
}

/**
 * 处理论文可视化命令
 */
async function handlePaperViz(args: string[]) {
  const url = args[0];
  const modeIndex = args.indexOf('--mode');
  const mode = modeIndex > -1 ? args[modeIndex + 1] : 'deep';
  const themeIndex = args.indexOf('--theme');
  const themeName = themeIndex > -1 ? args[themeIndex + 1] : 'academic-dark';
  const outputIndex = args.indexOf('--output');
  const outputFile = outputIndex > -1 ? args[outputIndex + 1] : undefined;
  const exportPpt = args.includes('--ppt');
  const figuresIndex = args.indexOf('--figures');
  const figuresDir = figuresIndex > -1 ? args[figuresIndex + 1] : undefined;

  // 验证参数
  const validation = validatePaperVizParams({ url, mode, theme: themeName });
  if (!validation.valid) {
    console.error('Validation errors:\n' + formatValidationErrors(validation.errors));
    process.exit(1);
  }

  console.log(`\n🎬 Analyzing paper: ${url}`);
  console.log(`   Mode: ${mode} | Theme: ${themeName}\n`);

  // 1. 分析论文
  const analyzer = new PaperAnalyzer();
  await analyzer.initialize();
  const analysis = await analyzer.analyze({ url, mode: mode as AnalysisMode });

  console.log(`✅ Analysis complete: "${analysis.metadata.title}"`);

  // 2. 提取图表（如果有 PDF）
  let figures: import('./paper-viz/scripts/types').ExtractedFigure[] = [];
  if (figuresDir) {
    console.log(`📊 Loading figures from: ${figuresDir}`);
    const fs = await import('fs');
    const path = await import('path');
    if (fs.existsSync(figuresDir)) {
      const files = fs.readdirSync(figuresDir).filter((f: string) =>
        /\.(png|jpg|jpeg|gif|webp)$/i.test(f)
      );
      figures = files.map((f: string, i: number) => ({
        path: path.join(figuresDir, f),
        mimeType: `image/${f.split('.').pop()?.toLowerCase() === 'jpg' ? 'jpeg' : f.split('.').pop()?.toLowerCase()}`,
        caption: `Figure ${i + 1}`,
        pageNumber: 0,
        width: 0,
        height: 0,
      }));
      console.log(`   Found ${figures.length} figures`);
    }
  } else if (analysis.metadata.url && analysis.metadata.url.includes('arxiv')) {
    const pdfPath = `./downloads/pdfs/${analysis.metadata.arxivId || 'paper'}.pdf`;
    const fs = await import('fs');
    if (fs.existsSync(pdfPath)) {
      console.log(`📊 Extracting figures from PDF...`);
      const extractor = new PdfFigureExtractor();
      figures = await extractor.extract(pdfPath);
      console.log(`   Extracted ${figures.length} figures`);
    }
  }

  // 3. 构建幻灯片数据
  const theme = themeName === 'academic-light' ? ACADEMIC_LIGHT_THEME : ACADEMIC_DARK_THEME;
  const builder = new SlideBuilder({ theme, figures });
  const presentation = builder.buildFromAnalysis(analysis);

  console.log(`📝 Generated ${presentation.slides.length} slides`);

  // 4. 生成 HTML
  const html = generatePaperHtml(presentation);
  const htmlPath = outputFile || `${analysis.metadata.title.slice(0, 50).replace(/[^\w\s-]/g, '').trim().replace(/\s+/g, '-')}-viz.html`;

  const fs = await import('fs');
  fs.writeFileSync(htmlPath, html);
  console.log(`\n✅ Presentation saved: ${htmlPath}`);

  // 5. 可选 PPT 导出
  if (exportPpt) {
    const pptPath = htmlPath.replace(/\.html$/, '.pptx');
    console.log(`📊 Exporting PPT...`);
    try {
      const exporter = new PptExporter();
      await exporter.export(presentation, { outputPath: pptPath });
      console.log(`✅ PPT saved: ${pptPath}`);
    } catch (err) {
      console.error(`⚠️  PPT export failed: ${getErrorMessage(err)}`);
      console.log('   Make sure Python and python-pptx are installed.');
    }
  }
}

/**
 * 处理交互式图谱命令
 */
async function handleGraphInteractive(args: string[]) {
  const graphName = args[0];
  const outputIndex = args.indexOf('--output');
  const outputFile = outputIndex > -1 ? args[outputIndex + 1] : undefined;
  const noPaperViz = args.includes('--no-paper-viz');

  // 验证参数
  const validation = validateGraphInteractiveParams({ graphName });
  if (!validation.valid) {
    console.error('Validation errors:\n' + formatValidationErrors(validation.errors));
    process.exit(1);
  }

  console.log(`\n🌐 Building interactive graph: ${graphName}\n`);

  // 1. 加载图谱
  const storage = new GraphStorage();
  try {
    const graph = storage.loadGraph(graphName);
    if (!graph) {
      console.error(`Error: Graph "${graphName}" not found`);
      console.log('Available graphs:');
      const list = storage.listGraphs();
      list.forEach((g) => console.log(`  - ${g.name} (${g.totalConcepts} concepts, ${g.totalPapers} papers)`));
      process.exit(1);
    }

    console.log(`✅ Loaded graph: ${graph.nodes.length} nodes, ${graph.edges.length} edges`);

    // 2. 转换为 D3 数据
    const adapter = new GraphDataAdapter();
    const d3Data = adapter.convert(graph);

    // 3. 构建论文数据（可选）
    let paperPayload = {};
    if (!noPaperViz) {
      const bridge = new PaperVizBridge();
      paperPayload = bridge.buildAllPaperPayloads(graph);
      const totalPapers = Object.values(paperPayload).reduce(
        (sum: number, papers: unknown) => sum + (papers as unknown[]).length, 0
      );
      console.log(`📚 Embedded ${totalPapers} paper references`);
    }

    // 4. 生成 HTML
    const html = generateGraphHtml(d3Data, {
      includePaperData: !noPaperViz,
      paperPayload,
    });

    const htmlPath = outputFile || `${graphName}-interactive.html`;
    const fs = await import('fs');
    fs.writeFileSync(htmlPath, html);
    console.log(`\n✅ Interactive graph saved: ${htmlPath}`);
    console.log(`   Open in browser to explore the knowledge graph.`);
  } finally {
    storage.close();
  }
}

main().catch(err => {
  console.error('Fatal error:', getErrorMessage(err));
  process.exit(1);
});


path: concept-extractor/scripts/extract.ts
content: /**
 * Concept Extractor - 概念提取模块
 * 从综述论文中提取核心概念，分类并识别关系
 */

import { createAIProvider, type AIProvider } from '../../shared/ai-provider';
import { extractJson } from '../../shared/utils';
import { ApiInitializationError, getErrorMessage } from '../../shared/errors';
import type { PaperAnalysis } from '../../paper-analyzer/scripts/types';
import type {
  ExtractedConcept,
  ConceptRelation,
  ConceptTaxonomy,
  ConceptExtractionResult,
  ConceptExtractionOptions,
  ConceptCategory,
  MergedConcept,
  ConceptMergeResult
} from './types';

export default class ConceptExtractor {
  private ai: AIProvider | null = null;

  async initialize(): Promise<void> {
    if (!this.ai) {
      try {
        this.ai = await createAIProvider();
      } catch (error) {
        throw new ApiInitializationError(
          `Failed to initialize AI provider: ${getErrorMessage(error)}`,
          error instanceof Error ? error : undefined
        );
      }
    }
  }

  /**
   * 从论文分析结果中提取概念
   */
  async extractFromReview(
    analysis: PaperAnalysis,
    options: ConceptExtractionOptions = {}
  ): Promise<ConceptExtractionResult> {
    await this.initialize();

    const {
      minConcepts = 15,
      maxConcepts = 30,
      minImportance = 1,
      extractRelations = true
    } = options;

    // 提取概念
    const concepts = await this.extractConcepts(analysis, minConcepts, maxConcepts);

    // 过滤低重要性概念
    const filtered = concepts.filter(c => c.importance >= minImportance);

    // 提取关系
    let relations: ConceptRelation[] = [];
    if (extractRelations && filtered.length > 1) {
      relations = await this.extractRelations(filtered);
    }

    // 构建分类体系
    const taxonomy = this.buildTaxonomy(filtered);

    // 统计信息
    const byCategory: Record<ConceptCategory, number> = {
      foundation: 0, core: 0, advanced: 0, application: 0
    };
    const byImportance: Record<number, number> = {};
    for (const c of filtered) {
      byCategory[c.category]++;
      byImportance[c.importance] = (byImportance[c.importance] || 0) + 1;
    }

    return {
      sourceTitle: analysis.metadata.title,
      sourceUrl: analysis.metadata.url,
      concepts: filtered,
      relations,
      taxonomy,
      stats: {
        totalConcepts: filtered.length,
        byCategory,
        byImportance
      }
    };
  }

  /**
   * 使用AI提取概念
   */
  private async extractConcepts(
    analysis: PaperAnalysis,
    min: number,
    max: number
  ): Promise<ExtractedConcept[]> {
    const prompt = `从以下论文分析中提取${min}-${max}个核心学术概念。

论文标题: ${analysis.metadata.title}
关键词: ${analysis.metadata.keywords.join(', ')}
摘要: ${analysis.abstract}
要点: ${analysis.keyPoints.map(kp => kp.point).join('; ')}
贡献: ${analysis.contributions.map(c => c.description).join('; ')}

请提取核心概念并返回JSON格式:
{
  "concepts": [
    {
      "name": "概念名称",
      "nameEn": "English Name",
      "nameZh": "中文名称",
      "category": "foundation|core|advanced|application",
      "importance": 1-5,
      "definition": "简短定义",
      "frequency": "high|medium|low",
      "aliases": ["别名1", "别名2"]
    }
  ]
}

分类标准:
- foundation: 基础理论概念（如线性代数、概率论）
- core: 核心技术概念（如注意力机制、Transformer）
- advanced: 进阶概念（如多头注意力、位置编码）
- application: 应用概念（如机器翻译、文本生成）

重要性标准:
- 5: 论文核心主题
- 4: 论文重要组成部分
- 3: 论文中频繁提及
- 2: 论文中有涉及
- 1: 论文中偶尔提到`;

    const responseText = await this.ai!.chat([
      { role: 'system', content: '你是学术概念提取专家。只返回JSON格式。' },
      { role: 'user', content: prompt }
    ], { temperature: 0.2 });

    const result = extractJson<{
      concepts?: Array<{
        name: string;
        nameEn?: string;
        nameZh?: string;
        category?: string;
        importance?: number;
        definition?: string;
        frequency?: string;
        aliases?: string[];
      }>;
    }>(responseText);

    if (!result.success || !result.data?.concepts) {
      console.error('Failed to extract concepts:', result.error);
      return [];
    }

    return result.data.concepts.map((c, index) => ({
      id: `concept_${index}`,
      name: c.name,
      nameEn: c.nameEn,
      nameZh: c.nameZh,
      category: this.validateCategory(c.category),
      importance: Math.min(5, Math.max(1, c.importance || 3)),
      definition: c.definition || '',
      frequency: this.validateFrequency(c.frequency),
      relatedConcepts: [],
      aliases: c.aliases || []
    }));
  }

  /**
   * 提取概念间关系
   */
  private async extractRelations(concepts: ExtractedConcept[]): Promise<ConceptRelation[]> {
    const conceptNames = concepts.map(c => c.name).join(', ');

    const prompt = `分析以下概念之间的关系:

概念列表: ${conceptNames}

请返回JSON格式的关系列表:
{
  "relations": [
    {
      "source": "源概念名称",
      "target": "目标概念名称",
      "relationType": "prerequisite|related|derived|component|contrast",
      "strength": 0.0-1.0,
      "description": "关系描述"
    }
  ]
}

关系类型说明:
- prerequisite: 前置依赖关系
- related: 相关关系
- derived: 衍生关系
- component: 组成部分
- contrast: 对比关系`;

    const responseText = await this.ai!.chat([
      { role: 'system', content: '你是学术概念关系分析专家。只返回JSON格式。' },
      { role: 'user', content: prompt }
    ], { temperature: 0.2 });

    const result = extractJson<{
      relations?: Array<{
        source: string;
        target: string;
        relationType?: string;
        strength?: number;
        description?: string;
      }>;
    }>(responseText);

    if (!result.success || !result.data?.relations) {
      return [];
    }

    return result.data.relations
      .map(r => {
        const sourceConcept = concepts.find(c =>
          c.name === r.source || c.nameEn === r.source || c.nameZh === r.source
        );
        const targetConcept = concepts.find(c =>
          c.name === r.target || c.nameEn === r.target || c.nameZh === r.target
        );

        if (!sourceConcept || !targetConcept) return null;

        return {
          sourceId: sourceConcept.id,
          targetId: targetConcept.id,
          relationType: this.validateRelationType(r.relationType),
          strength: Math.min(1, Math.max(0, r.strength || 0.5)),
          description: r.description
        };
      })
      .filter((r): r is ConceptRelation => r !== null);
  }

  /**
   * 构建概念分类体系
   */
  private buildTaxonomy(concepts: ExtractedConcept[]): ConceptTaxonomy {
    return {
      foundation: concepts.filter(c => c.category === 'foundation'),
      core: concepts.filter(c => c.category === 'core'),
      advanced: concepts.filter(c => c.category === 'advanced'),
      application: concepts.filter(c => c.category === 'application')
    };
  }

  /**
   * 合并多个提取结果（去重）
   */
  mergeConcepts(results: ConceptExtractionResult[]): ConceptMergeResult {
    const conceptMap = new Map<string, MergedConcept>();
    const allRelations: ConceptRelation[] = [];
    let totalBefore = 0;

    for (const result of results) {
      totalBefore += result.concepts.length;

      for (const concept of result.concepts) {
        const key = this.normalizeConceptName(concept.name);
        const existing = conceptMap.get(key);

        if (existing) {
          // 合并已存在的概念
          existing.sources.push(result.sourceTitle);
          existing.occurrenceCount++;
          existing.mergedImportance = Math.max(existing.mergedImportance, concept.importance);
          // 合并别名
          for (const alias of concept.aliases) {
            if (!existing.aliases.includes(alias)) {
              existing.aliases.push(alias);
            }
          }
        } else {
          conceptMap.set(key, {
            ...concept,
            id: `merged_${conceptMap.size}`,
            sources: [result.sourceTitle],
            mergedImportance: concept.importance,
            occurrenceCount: 1
          });
        }
      }

      allRelations.push(...result.relations);
    }

    const mergedConcepts = Array.from(conceptMap.values());

    // 更新关系中的概念ID
    const nameToId = new Map<string, string>();
    for (const c of mergedConcepts) {
      nameToId.set(this.normalizeConceptName(c.name), c.id);
      for (const alias of c.aliases) {
        nameToId.set(this.normalizeConceptName(alias), c.id);
      }
    }

    // 去重关系
    const relationSet = new Set<string>();
    const dedupedRelations: ConceptRelation[] = [];
    for (const r of allRelations) {
      const key = `${r.sourceId}-${r.targetId}-${r.relationType}`;
      if (!relationSet.has(key)) {
        relationSet.add(key);
        dedupedRelations.push(r);
      }
    }

    return {
      concepts: mergedConcepts,
      relations: dedupedRelations,
      stats: {
        totalBefore,
        totalAfter: mergedConcepts.length,
        duplicatesRemoved: totalBefore - mergedConcepts.length,
        sourcesCount: results.length
      }
    };
  }

  /**
   * 标准化概念名称用于去重
   */
  private normalizeConceptName(name: string): string {
    return name.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fa5]/g, '').trim();
  }

  private validateCategory(category?: string): ConceptCategory {
    const valid: ConceptCategory[] = ['foundation', 'core', 'advanced', 'application'];
    return valid.includes(category as ConceptCategory) ? category as ConceptCategory : 'core';
  }

  private validateFrequency(freq?: string): 'high' | 'medium' | 'low' {
    const valid = ['high', 'medium', 'low'];
    return valid.includes(freq as string) ? freq as 'high' | 'medium' | 'low' : 'medium';
  }

  private validateRelationType(type?: string): ConceptRelation['relationType'] {
    const valid = ['prerequisite', 'related', 'derived', 'component', 'contrast'];
    return valid.includes(type as string) ? type as ConceptRelation['relationType'] : 'related';
  }

  /**
   * 格式化概念提取结果
   */
  formatResult(result: ConceptExtractionResult): string {
    const lines: string[] = [
      `📚 概念提取结果 - ${result.sourceTitle}\n`,
      `📊 统计: ${result.stats.totalConcepts} 个概念\n`
    ];

    const categoryLabels: Record<ConceptCategory, string> = {
      foundation: '🔹 基础概念',
      core: '🔸 核心概念',
      advanced: '🔶 进阶概念',
      application: '🔷 应用概念'
    };

    for (const [category, label] of Object.entries(categoryLabels)) {
      const concepts = result.taxonomy[category as keyof ConceptTaxonomy];
      if (concepts.length > 0) {
        lines.push(`\n${label} (${concepts.length}):`);
        for (const c of concepts) {
          const stars = '★'.repeat(c.importance) + '☆'.repeat(5 - c.importance);
          lines.push(`  ${stars} ${c.name}${c.nameEn ? ` (${c.nameEn})` : ''}`);
          if (c.definition) {
            lines.push(`       ${c.definition}`);
          }
        }
      }
    }

    if (result.relations.length > 0) {
      lines.push(`\n🔗 概念关系 (${result.relations.length}):`);
      for (const r of result.relations.slice(0, 15)) {
        const source = result.concepts.find(c => c.id === r.sourceId);
        const target = result.concepts.find(c => c.id === r.targetId);
        if (source && target) {
          lines.push(`  ${source.name} --[${r.relationType}]--> ${target.name}`);
        }
      }
    }

    return lines.join('\n');
  }
}


path: concept-extractor/scripts/types.ts
content: /**
 * Concept Extractor - Type Definitions
 * 概念提取模块类型定义
 */

/**
 * 概念类别
 */
export type ConceptCategory = 'foundation' | 'core' | 'advanced' | 'application';

/**
 * 提取的概念
 */
export interface ExtractedConcept {
  /** 概念ID */
  id: string;
  /** 概念名称 */
  name: string;
  /** 英文名 */
  nameEn?: string;
  /** 中文名 */
  nameZh?: string;
  /** 概念类别 */
  category: ConceptCategory;
  /** 重要性评分 (1-5) */
  importance: number;
  /** 概念定义 */
  definition: string;
  /** 在综述中的出现频率估计 */
  frequency: 'high' | 'medium' | 'low';
  /** 相关概念ID列表 */
  relatedConcepts: string[];
  /** 关键词/别名 */
  aliases: string[];
}

/**
 * 概念间关系
 */
export interface ConceptRelation {
  /** 源概念ID */
  sourceId: string;
  /** 目标概念ID */
  targetId: string;
  /** 关系类型 */
  relationType: 'prerequisite' | 'related' | 'derived' | 'component' | 'contrast';
  /** 关系强度 (0-1) */
  strength: number;
  /** 关系描述 */
  description?: string;
}

/**
 * 概念分类体系
 */
export interface ConceptTaxonomy {
  /** 基础概念 */
  foundation: ExtractedConcept[];
  /** 核心概念 */
  core: ExtractedConcept[];
  /** 进阶概念 */
  advanced: ExtractedConcept[];
  /** 应用概念 */
  application: ExtractedConcept[];
}

/**
 * 概念提取结果
 */
export interface ConceptExtractionResult {
  /** 提取来源论文标题 */
  sourceTitle: string;
  /** 提取来源URL */
  sourceUrl: string;
  /** 提取的所有概念 */
  concepts: ExtractedConcept[];
  /** 概念间关系 */
  relations: ConceptRelation[];
  /** 概念分类体系 */
  taxonomy: ConceptTaxonomy;
  /** 统计信息 */
  stats: {
    totalConcepts: number;
    byCategory: Record<ConceptCategory, number>;
    byImportance: Record<number, number>;
  };
}

/**
 * 概念提取选项
 */
export interface ConceptExtractionOptions {
  /** 提取概念数量范围 */
  minConcepts?: number;
  maxConcepts?: number;
  /** 最小重要性 */
  minImportance?: number;
  /** 语言偏好 */
  language?: 'zh-CN' | 'en';
  /** 是否提取关系 */
  extractRelations?: boolean;
}

/**
 * 合并后的概念（多综述去重后） */
export interface MergedConcept extends ExtractedConcept {
  /** 来源综述列表 */
  sources: string[];
  /** 合并后的综合重要性 */
  mergedImportance: number;
  /** 出现次数 */
  occurrenceCount: number;
}

/**
 * 概念合并结果
 */
export interface ConceptMergeResult {
  /** 合并后的概念列表 */
  concepts: MergedConcept[];
  /** 合并后的关系列表 */
  relations: ConceptRelation[];
  /** 合并统计 */
  stats: {
    totalBefore: number;
    totalAfter: number;
    duplicatesRemoved: number;
    sourcesCount: number;
  };
}


path: concept-learner/scripts/learn.ts
content: /**
 * Concept Learner - Core Module
 * 概念学习核心模块
 *
 * 帮助用户快速构建知识框架：
 * - 概念定义与解释
 * - 核心组成要素
 * - 历史演进
 * - 应用场景
 * - 相关概念
 * - 学习路径
 */

import { createAIProvider, type AIProvider } from '../../shared/ai-provider';
import { extractJson } from '../../shared/utils';
import { ApiInitializationError, getErrorMessage } from '../../shared/errors';
import type { WebSearchResultItem } from '../../shared/types';
import type { LearnOptions, ConceptCard, ComparisonResult, LearningPathPlan, Paper, CodeExample } from './types';

export default class ConceptLearner {
  private ai: AIProvider | null = null;

  async initialize(): Promise<void> {
    if (!this.ai) {
      try {
        this.ai = await createAIProvider();
      } catch (error) {
        throw new ApiInitializationError(
          `Failed to initialize AI provider: ${getErrorMessage(error)}`,
          error instanceof Error ? error : undefined
        );
      }
    }
  }

  /**
   * 学习一个概念
   */
  async learn(concept: string, options: LearnOptions = {}): Promise<ConceptCard> {
    await this.initialize();

    const {
      depth = 'intermediate',
      includePapers = true,
      includeCode = false,
      language = 'zh-CN',
      focusAreas
    } = options;

    // 获取概念基础信息
    const basicInfo = await this.fetchConceptBasics(concept, language);

    // 获取相关论文
    let keyPapers: Paper[] = [];
    if (includePapers) {
      keyPapers = await this.fetchKeyPapers(concept);
    }

    // 获取代码示例
    let codeExamples: CodeExample[] = [];
    if (includeCode) {
      codeExamples = await this.fetchCodeExamples(concept);
    }

    // 生成学习路径
    const learningPath = await this.generateLearningPath(concept, depth, language);

    // 组装概念卡片
    const card: ConceptCard = {
      concept,
      ...basicInfo,
      learningPath,
      keyPapers,
      codeExamples,
      generatedAt: new Date().toISOString()
    };

    return card;
  }

  /**
   * 获取概念基础信息
   */
  private async fetchConceptBasics(concept: string, language: string): Promise<Partial<ConceptCard>> {
    const langPrompt = language === 'zh-CN'
      ? '请用中文回答'
      : 'Please answer in English';

    const prompt = `${langPrompt}

请为概念"${concept}"生成一份详细的知识卡片，包含以下内容：

1. **定义**: 给出清晰的定义，包括一句话简洁解释
2. **核心组成**: 列出3-5个核心组成部分，每个说明其作用和重要性
3. **历史演进**: 说明概念的起源、关键发展节点、当前状态
4. **应用场景**: 列出主要应用领域和具体案例
5. **相关概念**: 列出相关概念，区分前置知识、相关知识、衍生概念、替代方案

请以JSON格式返回，结构如下：
{
  "definition": "详细定义",
  "shortExplanation": "一句话解释",
  "coreComponents": [
    {"name": "组成名称", "description": "描述", "importance": "high|medium|low"}
  ],
  "history": {
    "origin": "起源背景",
    "keyDevelopments": [
      {"year": "年份", "event": "事件", "significance": "意义"}
    ],
    "currentStatus": "当前状态"
  },
  "applications": [
    {"domain": "领域", "examples": ["案例1", "案例2"], "impact": "影响"}
  ],
  "relatedConcepts": [
    {"concept": "概念名", "relationship": "prerequisite|related|derived|alternative", "briefExplanation": "简要说明"}
  ],
  "resources": [
    {"type": "paper|tutorial|course|book|code", "title": "标题", "url": "链接", "description": "描述", "difficulty": "beginner|intermediate|advanced"}
  ]
}`;

    const responseText = await this.ai!.chat([
      { role: 'system', content: '你是一位知识结构化专家，擅长将复杂概念转化为清晰的学习框架。' },
      { role: 'user', content: prompt }
    ], { temperature: 0.3 });

    // 使用改进的 JSON 提取
    const result = extractJson<Partial<ConceptCard>>(responseText);

    if (result.success && result.data) {
      return result.data;
    }

    console.error('Failed to parse concept response:', result.error);
    return {
      definition: responseText,
      shortExplanation: '',
      coreComponents: [],
      history: { origin: '', keyDevelopments: [], currentStatus: '' },
      applications: [],
      relatedConcepts: [],
      resources: []
    };
  }

  /**
   * 获取关键论文
   */
  private async fetchKeyPapers(concept: string): Promise<Paper[]> {
    try {
      // 优先使用 Semantic Scholar API
      const papers = await this.searchSemanticScholar(concept);
      if (papers.length > 0) {
        return papers;
      }

      // 回退到 web search（如果可用）
      if (this.ai!.webSearch) {
        const results: WebSearchResultItem[] = await this.ai!.webSearch(
          `${concept} paper arxiv research`,
          5
        );

        return results.slice(0, 5).map((item) => ({
          title: item.name,
          authors: [],
          year: item.date?.split('-')[0] || '',
          url: item.url,
          summary: item.snippet || ''
        }));
      }

      console.warn('No paper search method available');
      return [];
    } catch (error) {
      console.error('Failed to fetch papers:', getErrorMessage(error));
      return [];
    }
  }

  /**
   * 从 Semantic Scholar 搜索论文
   */
  private async searchSemanticScholar(concept: string): Promise<Paper[]> {
    try {
      const fields = 'paperId,title,abstract,authors,year,citationCount,venue,url';
      const url = `https://api.semanticscholar.org/graph/v1/paper/search?query=${encodeURIComponent(concept)}&limit=5&fields=${fields}`;

      const response = await fetch(url);
      if (!response.ok) {
        console.warn(`Semantic Scholar API error: ${response.status}`);
        return [];
      }

      const data = await response.json() as {
        data?: Array<{
          paperId: string;
          title: string;
          abstract?: string;
          authors?: Array<{ name: string }>;
          year?: number;
          citationCount?: number;
          venue?: string;
          url?: string;
        }>;
      };

      if (!data.data) return [];

      return data.data.map(paper => ({
        title: paper.title,
        authors: paper.authors?.map(a => a.name) || [],
        year: paper.year?.toString() || '',
        url: paper.url || `https://www.semanticscholar.org/paper/${paper.paperId}`,
        summary: paper.abstract?.substring(0, 200) + '...' || ''
      }));
    } catch (error) {
      console.error('Semantic Scholar search error:', getErrorMessage(error));
      return [];
    }
  }

  /**
   * 获取代码示例
   */
  private async fetchCodeExamples(concept: string): Promise<CodeExample[]> {
    const prompt = `为概念"${concept}"生成1-2个简洁的代码示例，帮助理解其实现原理。

返回JSON格式：
{
  "examples": [
    {
      "title": "示例标题",
      "description": "说明这个示例展示什么",
      "language": "python|javascript|etc",
      "code": "代码内容"
    }
  ]
}`;

    try {
      const responseText = await this.ai!.chat([
        { role: 'system', content: '你是一位代码教学专家，擅长用简洁的代码说明复杂概念。' },
        { role: 'user', content: prompt }
      ], { temperature: 0.2 });

      const result = extractJson<{ examples?: CodeExample[] }>(responseText);

      if (result.success && result.data?.examples) {
        return result.data.examples;
      }
    } catch (error) {
      console.error('Failed to generate code examples:', getErrorMessage(error));
    }

    return [];
  }

  /**
   * 生成学习路径
   */
  private async generateLearningPath(
    concept: string,
    depth: string,
    language: string
  ): Promise<ConceptCard['learningPath']> {
    const langPrompt = language === 'zh-CN' ? '请用中文回答' : 'Please answer in English';

    const prompt = `${langPrompt}

为学习"${concept}"设计一条从入门到${depth === 'advanced' ? '精通' : '进阶'}的学习路径。

返回JSON格式：
{
  "stages": [
    {
      "stage": "阶段名称（如：基础概念）",
      "concepts": ["需要学习的概念1", "概念2"],
      "estimatedTime": "预计时间",
      "resources": ["推荐资源1", "资源2"]
    }
  ]
}`;

    try {
      const responseText = await this.ai!.chat([
        { role: 'system', content: '你是一位学习路径规划专家。' },
        { role: 'user', content: prompt }
      ], { temperature: 0.3 });

      const result = extractJson<{ stages?: ConceptCard['learningPath'] }>(responseText);

      if (result.success && result.data?.stages) {
        return result.data.stages;
      }
    } catch (error) {
      console.error('Failed to generate learning path:', getErrorMessage(error));
    }

    return [];
  }

  /**
   * 对比两个概念
   */
  async compare(concept1: string, concept2: string, language: string = 'zh-CN'): Promise<ComparisonResult> {
    await this.initialize();

    const langPrompt = language === 'zh-CN' ? '请用中文回答' : 'Please answer in English';

    const prompt = `${langPrompt}

对比分析"${concept1}"和"${concept2}"两个概念：

返回JSON格式：
{
  "similarities": ["相似点1", "相似点2"],
  "differences": ["差异点1", "差异点2"],
  "useCases": {
    "preferConcept1": ["适合使用${concept1}的场景"],
    "preferConcept2": ["适合使用${concept2}的场景"]
  }
}`;

    const responseText = await this.ai!.chat([
      { role: 'system', content: '你是一位技术对比分析专家。' },
      { role: 'user', content: prompt }
    ], { temperature: 0.2 });

    const result = extractJson<{
      similarities?: string[];
      differences?: string[];
      useCases?: { preferConcept1?: string[]; preferConcept2?: string[] };
    }>(responseText);

    if (result.success && result.data) {
      return {
        concept1,
        concept2,
        similarities: result.data.similarities || [],
        differences: result.data.differences || [],
        useCases: {
          preferConcept1: result.data.useCases?.preferConcept1 || [],
          preferConcept2: result.data.useCases?.preferConcept2 || []
        }
      };
    }

    return {
      concept1,
      concept2,
      similarities: [],
      differences: [],
      useCases: { preferConcept1: [], preferConcept2: [] }
    };
  }

  /**
   * 规划学习路径
   */
  async planLearningPath(
    topic: string,
    options: {
      currentLevel: string;
      targetLevel: string;
      timeCommitment: string;
    }
  ): Promise<LearningPathPlan> {
    await this.initialize();

    const { currentLevel, targetLevel, timeCommitment } = options;

    const prompt = `为学习主题"${topic}"规划学习路径：

当前水平：${currentLevel}
目标水平：${targetLevel}
可用时间：${timeCommitment}

返回JSON格式：
{
  "topic": "${topic}",
  "currentLevel": "${currentLevel}",
  "targetLevel": "${targetLevel}",
  "estimatedDuration": "总预计时长",
  "stages": [
    {
      "stage": "阶段名",
      "concepts": ["概念"],
      "estimatedTime": "时间",
      "resources": ["资源"]
    }
  ],
  "milestones": ["里程碑1", "里程碑2"],
  "recommendedOrder": ["建议学习顺序"]
}`;

    const responseText = await this.ai!.chat([
      { role: 'system', content: '你是一位学习路径规划专家。' },
      { role: 'user', content: prompt }
    ], { temperature: 0.3 });

    const result = extractJson<LearningPathPlan>(responseText);

    if (result.success && result.data) {
      return result.data;
    }

    return {
      topic,
      currentLevel,
      targetLevel,
      estimatedDuration: '',
      stages: [],
      milestones: [],
      recommendedOrder: []
    };
  }

  /**
   * 导出为Markdown
   */
  toMarkdown(card: ConceptCard): string {
    const md = `# ${card.concept} - 概念学习卡片

## 📖 定义

${card.definition}

**一句话解释**: ${card.shortExplanation}

## 🔧 核心组成

${card.coreComponents.map(c => `
### ${c.name}
${c.description}

*重要性*: ${c.importance}
`).join('\n')}

## 📜 历史演进

**起源**: ${card.history.origin}

${card.history.keyDevelopments.map(d => `
- **${d.year}**: ${d.event} - ${d.significance}
`).join('\n')}

**当前状态**: ${card.history.currentStatus}

## 🎯 应用场景

${card.applications.map(a => `
### ${a.domain}
- 案例: ${a.examples.join(', ')}
- 影响: ${a.impact}
`).join('\n')}

## 🔗 相关概念

| 概念 | 关系 | 说明 |
|------|------|------|
${card.relatedConcepts.map(c => `| ${c.concept} | ${c.relationship} | ${c.briefExplanation} |`).join('\n')}

## 📚 学习路径

${card.learningPath.map((stage, i) => `
### 阶段${i + 1}: ${stage.stage}
- 学习内容: ${stage.concepts.join(', ')}
- 预计时间: ${stage.estimatedTime}
- 推荐资源: ${stage.resources.join(', ')}
`).join('\n')}

## 📄 关键论文

${card.keyPapers?.map(p => `
- [${p.title}](${p.url || '#'}) (${p.year}) - ${p.summary}
`).join('\n') || '暂无'}

${card.codeExamples && card.codeExamples.length > 0 ? `
## 💻 代码示例

${card.codeExamples.map((example, i) => `
### 示例 ${i + 1}: ${example.title}

${example.description}

\`\`\`${example.language}
${example.code}
\`\`\`
`).join('\n')}
` : ''}

---
*生成时间: ${card.generatedAt}*
`;

    return md;
  }
}

// CLI 支持
if (import.meta.main) {
  const args = process.argv.slice(2);
  const concept = args[0];

  if (!concept) {
    console.error('Usage: bun run learn.ts <concept> [--depth beginner|intermediate|advanced] [--output <file>]');
    process.exit(1);
  }

  const depthIndex = args.indexOf('--depth');
  const depth = depthIndex > -1 ? args[depthIndex + 1] as 'beginner' | 'intermediate' | 'advanced' : 'intermediate';

  const outputIndex = args.indexOf('--output');
  const outputFile = outputIndex > -1 ? args[outputIndex + 1] : null;

  const learner = new ConceptLearner();

  learner.learn(concept, { depth }).then(card => {
    if (outputFile) {
      const fs = require('fs');
      fs.writeFileSync(outputFile, learner.toMarkdown(card));
      console.log(`Concept card saved to ${outputFile}`);
    } else {
      console.log(JSON.stringify(card, null, 2));
    }
  }).catch(err => {
    console.error('Error:', getErrorMessage(err));
    process.exit(1);
  });
}


path: concept-learner/scripts/types.ts
content: /**
 * Concept Learner - Type Definitions
 * 概念学习器类型定义
 */

export interface LearnOptions {
  depth?: 'beginner' | 'intermediate' | 'advanced';
  includePapers?: boolean;
  includeCode?: boolean;
  language?: 'zh-CN' | 'en-US';
  focusAreas?: string[];
}

export interface ConceptCard {
  concept: string;
  definition: string;
  shortExplanation: string;
  coreComponents: CoreComponent[];
  history: ConceptHistory;
  applications: Application[];
  relatedConcepts: RelatedConcept[];
  learningPath: LearningStage[];
  resources: LearningResource[];
  keyPapers?: Paper[];
  codeExamples?: CodeExample[];
  generatedAt: string;
}

export interface CoreComponent {
  name: string;
  description: string;
  importance: 'high' | 'medium' | 'low';
}

export interface ConceptHistory {
  origin: string;
  keyDevelopments: KeyDevelopment[];
  currentStatus: string;
}

export interface KeyDevelopment {
  year: string;
  event: string;
  significance: string;
}

export interface Application {
  domain: string;
  examples: string[];
  impact: string;
}

export interface RelatedConcept {
  concept: string;
  relationship: 'prerequisite' | 'related' | 'derived' | 'alternative';
  briefExplanation: string;
}

export interface LearningStage {
  stage: string;
  concepts: string[];
  estimatedTime: string;
  resources: string[];
}

export interface LearningResource {
  type: 'paper' | 'tutorial' | 'course' | 'book' | 'code';
  title: string;
  url?: string;
  description: string;
  difficulty: 'beginner' | 'intermediate' | 'advanced';
}

export interface Paper {
  title: string;
  authors: string[];
  year: string;
  venue?: string;
  url?: string;
  summary: string;
}

export interface CodeExample {
  title: string;
  description: string;
  language: string;
  code: string;
  source?: string;
}

export interface ComparisonResult {
  concept1: string;
  concept2: string;
  similarities: string[];
  differences: string[];
  useCases: {
    preferConcept1: string[];
    preferConcept2: string[];
  };
  relationshipDiagram?: string;
}

export interface LearningPathPlan {
  topic: string;
  currentLevel: string;
  targetLevel: string;
  estimatedDuration: string;
  stages: LearningStage[];
  milestones: string[];
  recommendedOrder: string[];
}


path: config.ts
content: /**
 * Literature Skills - Configuration
 * 文献检索总结工具配置系统
 */

import type { AIProviderType, SearchSource } from './shared/types';

export interface LiteratureSkillConfig {
  // AI 提供商配置
  ai: {
    provider: AIProviderType;
    apiKey?: string;
    baseUrl?: string;
    model?: string;
    timeout: number;
  };

  // 用户配置
  user: {
    name?: string;
    interests: string[];
    level: 'beginner' | 'intermediate' | 'advanced' | 'expert';
    primaryLanguage: 'zh-CN' | 'en-US';
  };

  // 检索配置
  search: {
    defaultSources: SearchSource[];
    maxResults: number;
    sortBy: 'relevance' | 'date' | 'citations';
    cacheResults: boolean;
    cacheDuration: number; // 毫秒
  };

  // 数据源 API 密钥配置
  sourceApiKeys: {
    ncbiApiKey?: string;
    ieeeApiKey?: string;
    coreApiKey?: string;
    unpaywallEmail?: string;
    crossrefMailto?: string;
    serpApiKey?: string;
  };

  // 搜索策略配置
  searchStrategy: {
    defaultPriority: SearchSource[];
    domainPriorities: Record<string, SearchSource[]>;
    maxConcurrentSources: number;
    useComplementaryStrategy: boolean;
  };

  // PDF 下载配置
  pdf: {
    downloadDir: string;
    namingStrategy: 'title' | 'id' | 'doi';
    maxFileSize: number; // bytes
    skipExisting: boolean;
    concurrency: number;
    autoDownload: boolean;
  };

  // 学习配置
  learning: {
    depth: 'quick' | 'standard' | 'deep';
    includePapers: boolean;
    includeCode: boolean;
    focusAreas: string[];
  };

  // 追踪配置
  tracking: {
    enabled: boolean;
    frequency: 'daily' | 'weekly' | 'monthly';
    keywords: string[];
    authors: string[];
    conferences: string[];
    // 内存管理配置
    maxHistoryEntries: number;
    maxTotalEntries: number;
    maxAgeMs: number;
  };

  // 输出配置
  output: {
    format: 'json' | 'markdown' | 'html';
    savePath: string;
    generateReports: boolean;
    reportFrequency: 'daily' | 'weekly' | 'monthly';
  };
}

export const defaultConfig: LiteratureSkillConfig = {
  ai: {
    provider: (process.env.AI_PROVIDER as AIProviderType) || 'zai',
    [REDACTED_SECRET],
    baseUrl: process.env.AI_BASE_URL,
    model: process.env.AI_MODEL,
    timeout: 30000
  },

  user: {
    interests: [],
    level: 'intermediate',
    primaryLanguage: 'zh-CN'
  },

  search: {
    defaultSources: ['arxiv', 'semantic_scholar', 'web'],
    maxResults: 20,
    sortBy: 'relevance',
    cacheResults: true,
    cacheDuration: 3600000 // 1小时
  },

  sourceApiKeys: {
    ncbiApiKey: process.env.NCBI_API_KEY,
    ieeeApiKey: process.env.IEEE_API_KEY,
    coreApiKey: process.env.CORE_API_KEY,
    unpaywallEmail: process.env.UNPAYWALL_EMAIL,
    crossrefMailto: process.env.CROSSREF_MAILTO,
    serpApiKey: process.env.SERPAPI_KEY
  },

  searchStrategy: {
    defaultPriority: [
      'semantic_scholar', 'openalex', 'arxiv', 'pubmed',
      'crossref', 'dblp', 'core', 'ieee', 'web'
    ],
    domainPriorities: {
      biomedical: ['pubmed', 'semantic_scholar', 'openalex', 'crossref'],
      cs: ['semantic_scholar', 'arxiv', 'dblp', 'openalex'],
      engineering: ['ieee', 'semantic_scholar', 'openalex', 'crossref'],
      physics: ['arxiv', 'semantic_scholar', 'openalex'],
      general: ['semantic_scholar', 'openalex', 'crossref', 'arxiv']
    },
    maxConcurrentSources: 4,
    useComplementaryStrategy: true
  },

  pdf: {
    downloadDir: './downloads/pdfs',
    namingStrategy: 'title',
    maxFileSize: 50 * 1024 * 1024, // 50MB
    skipExisting: true,
    concurrency: 3,
    autoDownload: false
  },

  learning: {
    depth: 'standard',
    includePapers: true,
    includeCode: false,
    focusAreas: []
  },

  tracking: {
    enabled: true,
    frequency: 'weekly',
    keywords: [],
    authors: [],
    conferences: [],
    maxHistoryEntries: 100,
    maxTotalEntries: 1000,
    maxAgeMs: 7 * 24 * 60 * 60 * 1000 // 7 days
  },

  output: {
    format: 'markdown',
    savePath: './output',
    generateReports: true,
    reportFrequency: 'weekly'
  }
};

/**
 * 配置管理器
 */
export class ConfigManager {
  private config: LiteratureSkillConfig;
  private configPath: string;

  constructor(configPath: string = './literature-config.json') {
    this.configPath = configPath;
    this.config = { ...defaultConfig };
  }

  /**
   * 加载配置
   */
  load(): LiteratureSkillConfig {
    try {
      const fs = require('fs');
      if (fs.existsSync(this.configPath)) {
        const loaded = JSON.parse(fs.readFileSync(this.configPath, 'utf-8'));
        this.config = { ...defaultConfig, ...loaded };
      }
    } catch (error) {
      console.warn('Failed to load config, using defaults:', error);
    }
    return this.config;
  }

  /**
   * 保存配置
   */
  save(): void {
    try {
      const fs = require('fs');
      fs.writeFileSync(this.configPath, JSON.stringify(this.config, null, 2));
    } catch (error) {
      console.error('Failed to save config:', error);
    }
  }

  /**
   * 更新配置
   */
  update(partial: Partial<LiteratureSkillConfig>): void {
    this.config = {
      ...this.config,
      ...partial,
      ai: { ...this.config.ai, ...partial.ai },
      user: { ...this.config.user, ...partial.user },
      search: { ...this.config.search, ...partial.search },
      sourceApiKeys: { ...this.config.sourceApiKeys, ...partial.sourceApiKeys },
      searchStrategy: { ...this.config.searchStrategy, ...partial.searchStrategy },
      pdf: { ...this.config.pdf, ...partial.pdf },
      learning: { ...this.config.learning, ...partial.learning },
      tracking: { ...this.config.tracking, ...partial.tracking },
      output: { ...this.config.output, ...partial.output }
    };
    this.save();
  }

  /**
   * 获取当前配置
   */
  get(): LiteratureSkillConfig {
    return this.config;
  }

  /**
   * 重置为默认配置
   */
  reset(): void {
    this.config = { ...defaultConfig };
    this.save();
  }
}

// CLI 支持
if (import.meta.main) {
  const args = process.argv.slice(2);
  const command = args[0];

  const manager = new ConfigManager();

  switch (command) {
    case 'init':
      manager.save();
      console.log('Configuration initialized at ./literature-config.json');
      break;

    case 'show':
      const config = manager.load();
      console.log(JSON.stringify(config, null, 2));
      break;

    case 'set':
      const key = args[1];
      const value = args[2];
      if (key && value) {
        try {
          const parsed = JSON.parse(value);
          manager.update({ [key]: parsed });
          console.log(`Set ${key} = ${value}`);
        } catch {
          manager.update({ [key]: value });
          console.log(`Set ${key} = "${value}"`);
        }
      }
      break;

    case 'reset':
      manager.reset();
      console.log('Configuration reset to defaults');
      break;

    default:
      console.log(`
Usage:
  config.ts init              - Initialize configuration file
  config.ts show              - Show current configuration
  config.ts set <key> <value> - Set configuration value
  config.ts reset             - Reset to defaults

Environment Variables:
  AI_PROVIDER     - AI provider: zai | openai | anthropic | azure | ollama | qwen (default: zai)
  AI_API_KEY      - API key for the selected provider
  AI_BASE_URL     - Custom API base URL (optional)
  AI_MODEL        - Model name (optional)

  OPENAI_API_KEY       - OpenAI API key
  ANTHROPIC_API_KEY    - Anthropic API key
  AZURE_OPENAI_ENDPOINT - Azure OpenAI endpoint
  AZURE_OPENAI_API_KEY  - Azure OpenAI API key
  OLLAMA_BASE_URL      - Ollama server URL (default: http://localhost:11434)

  QWEN_API_KEY         - Qwen (通义千问) API key
  DASHSCOPE_API_KEY    - Alias for QWEN_API_KEY
  QWEN_BASE_URL        - Qwen API base URL (default: https://dashscope.aliyuncs.com/compatible-mode/v1)
  QWEN_MODEL           - Qwen model name (default: qwen-plus)

  SERPER_API_KEY       - Serper API key for web search (optional)
`);
  }
}


path: graph-viz/scripts/graph-data-adapter.ts
content: /**
 * @module graph-viz/graph-data-adapter
 * @description 将 KnowledgeGraphData 转换为 D3 可视化数据
 *
 * 核心计算：
 * - 节点半径 = 15 + sqrt(paperCount) * 8, 上限 60
 * - 边粗细 = relationWeight + sharedPaperCount * 0.5, 范围 [1, 8]
 */

import type {
  KnowledgeGraphData,
  KnowledgeNode,
  KnowledgeEdge,
  LiteratureReference,
} from '../../knowledge-graph/scripts/graph';
import type {
  D3GraphData,
  D3GraphNode,
  D3GraphEdge,
  NodeCategory,
  RelationType,
  EdgeStyle,
} from './types';
import {
  CATEGORY_COLORS,
  RELATION_WEIGHTS,
  RELATION_STYLES,
} from './types';

/**
 * GraphDataAdapter — KnowledgeGraphData → D3GraphData 转换器
 *
 * @example
 * ```ts
 * const adapter = new GraphDataAdapter();
 * const d3Data = adapter.convert(knowledgeGraph);
 * ```
 */
export class GraphDataAdapter {
  /**
   * 将 KnowledgeGraphData 转换为 D3GraphData
   * @param graph - 知识图谱数据
   * @returns D3 可视化数据
   */
  convert(graph: KnowledgeGraphData): D3GraphData {
    const nodeMap = new Map<string, KnowledgeNode>();
    for (const node of graph.nodes) {
      nodeMap.set(node.id, node);
    }

    // 计算每个节点的论文数量
    const paperCounts = this.computePaperCounts(graph);

    // 计算每条边的共享论文数
    const sharedPaperCounts = this.computeSharedPaperCounts(graph);

    const nodes: D3GraphNode[] = graph.nodes.map((node) =>
      this.convertNode(node, paperCounts.get(node.id) ?? 0)
    );

    const edges: D3GraphEdge[] = graph.edges.map((edge) =>
      this.convertEdge(edge, sharedPaperCounts.get(this.edgeKey(edge)) ?? 0)
    );

    const totalPapers = graph.literatureIndex?.papers?.size ?? 0;

    return {
      nodes,
      edges,
      metadata: {
        name: graph.graphMetadata?.name ?? 'Knowledge Graph',
        totalNodes: nodes.length,
        totalEdges: edges.length,
        totalPapers,
        generatedAt: new Date().toISOString(),
      },
    };
  }

  /**
   * 计算节点半径
   * @param paperCount - 关联论文数
   * @returns 半径（px）
   */
  static computeRadius(paperCount: number): number {
    const r = 15 + Math.sqrt(paperCount) * 8;
    return Math.min(r, 60);
  }

  /**
   * 计算边线宽度
   * @param relation - 关系类型
   * @param sharedPaperCount - 共享论文数
   * @returns 线宽（px）
   */
  static computeStrokeWidth(relation: RelationType, sharedPaperCount: number): number {
    const base = RELATION_WEIGHTS[relation] ?? 1;
    const width = base + sharedPaperCount * 0.5;
    return Math.max(1, Math.min(width, 8));
  }

  /** 转换单个节点 */
  private convertNode(node: KnowledgeNode, paperCount: number): D3GraphNode {
    const category = node.category as NodeCategory;
    return {
      id: node.id,
      label: node.label,
      category,
      importance: node.importance,
      description: node.description ?? '',
      radius: GraphDataAdapter.computeRadius(paperCount),
      paperCount,
      keyPaperIds: node.keyPapers ?? [],
      color: CATEGORY_COLORS[category] ?? CATEGORY_COLORS.core,
    };
  }

  /** 转换单条边 */
  private convertEdge(edge: KnowledgeEdge, sharedPaperCount: number): D3GraphEdge {
    const relation = edge.relation as RelationType;
    const strokeWidth = GraphDataAdapter.computeStrokeWidth(relation, sharedPaperCount);
    const relStyle = RELATION_STYLES[relation] ?? RELATION_STYLES.related;

    const style: EdgeStyle = {
      color: relStyle.color,
      dasharray: relStyle.dasharray,
      opacity: Math.min(0.4 + strokeWidth * 0.08, 0.9),
    };

    return {
      source: edge.source,
      target: edge.target,
      relation,
      label: edge.label ?? relation,
      strokeWidth,
      sharedPaperCount,
      style,
    };
  }

  /** 计算每个节点的关联论文数 */
  private computePaperCounts(graph: KnowledgeGraphData): Map<string, number> {
    const counts = new Map<string, number>();

    for (const node of graph.nodes) {
      // 来源1: literatureReferences
      const refCount = node.literatureReferences?.length ?? 0;

      // 来源2: conceptIndex
      let indexCount = 0;
      if (graph.conceptIndex?.conceptToPapers) {
        const refs = graph.conceptIndex.conceptToPapers.get(node.id);
        if (refs) indexCount = refs.length;
      }

      counts.set(node.id, Math.max(refCount, indexCount));
    }

    return counts;
  }

  /** 计算每条边两端节点共享的论文数 */
  private computeSharedPaperCounts(graph: KnowledgeGraphData): Map<string, number> {
    const shared = new Map<string, number>();

    if (!graph.conceptIndex?.conceptToPapers) {
      // 无索引数据时全部返回 0
      for (const edge of graph.edges) {
        shared.set(this.edgeKey(edge), 0);
      }
      return shared;
    }

    const ctp = graph.conceptIndex.conceptToPapers;

    for (const edge of graph.edges) {
      const sourcePapers = new Set(
        (ctp.get(edge.source) ?? []).map((r) => r.paperId)
      );
      const targetPapers = (ctp.get(edge.target) ?? []).map((r) => r.paperId);

      let count = 0;
      for (const pid of targetPapers) {
        if (sourcePapers.has(pid)) count++;
      }

      shared.set(this.edgeKey(edge), count);
    }

    return shared;
  }

  /** 生成边的唯一 key */
  private edgeKey(edge: KnowledgeEdge): string {
    return `${edge.source}--${edge.target}`;
  }
}


path: graph-viz/scripts/html-generator.ts
content: /**
 * @module graph-viz/html-generator
 * @description 生成交互式知识图谱 HTML（D3.js v7 力导向布局）
 *
 * 特性：
 * - D3.js v7 从 CDN 加载
 * - 力导向布局：forceLink / forceManyBody / forceCollide / forceCenter
 * - 缩放平移（d3.zoom）、节点拖拽（d3.drag）
 * - 点击节点：右侧面板显示详情 + 关联论文
 * - 悬停 tooltip、搜索栏、图例
 * - 可选嵌入论文数据，支持"查看演示"功能
 */

import type {
  D3GraphData,
  D3GraphNode,
  D3GraphEdge,
  GraphHtmlOptions,
  PaperPayload,
} from './types';
import { CATEGORY_COLORS, RELATION_STYLES } from './types';

/**
 * 生成交互式图谱 HTML
 * @param data - D3 图谱数据
 * @param options - HTML 生成选项
 * @returns 完整 HTML 字符串
 */
export function generateGraphHtml(data: D3GraphData, options?: GraphHtmlOptions): string {
  const opts: Required<GraphHtmlOptions> = {
    includePaperData: options?.includePaperData ?? true,
    paperPayload: options?.paperPayload ?? {},
    width: options?.width ?? 1200,
    height: options?.height ?? 800,
  };

  return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>${escapeHtml(data.metadata.name)} — Knowledge Graph</title>
<style>
${generateCSS()}
</style>
</head>
<body>
<div class="app">
  <header class="toolbar">
    <h1 class="graph-title">${escapeHtml(data.metadata.name)}</h1>
    <div class="search-box">
      <input type="text" id="searchInput" placeholder="Search nodes..." autocomplete="off">
      <span class="search-icon">&#128269;</span>
    </div>
    <div class="stats">
      <span>${data.metadata.totalNodes} nodes</span>
      <span>${data.metadata.totalEdges} edges</span>
      <span>${data.metadata.totalPapers} papers</span>
    </div>
  </header>
  <div class="main">
    <div class="graph-container" id="graphContainer">
      <svg id="graphSvg"></svg>
    </div>
    <aside class="panel" id="panel">
      <div class="panel-placeholder">
        <p>Click a node to see details</p>
      </div>
      <div class="panel-content" id="panelContent" style="display:none"></div>
    </aside>
  </div>
  <div class="legend" id="legend">
    <div class="legend-title">Categories</div>
    ${Object.entries(CATEGORY_COLORS)
      .map(([cat, color]) => `<div class="legend-item"><span class="legend-dot" style="background:${color}"></span>${cat}</div>`)
      .join('\n    ')}
    <div class="legend-title" style="margin-top:8px">Node Size = Paper Count</div>
    <div class="legend-title">Relations</div>
    ${Object.entries(RELATION_STYLES)
      .map(([rel, s]) => `<div class="legend-item"><span class="legend-line" style="background:${s.color};${s.dasharray ? 'border-style:dashed' : ''}"></span>${rel}</div>`)
      .join('\n    ')}
  </div>
  <div class="tooltip" id="tooltip"></div>
</div>
<script src="https://d3js.org/d3.v7.min.js"><\/script>
<script>
const GRAPH_DATA = ${JSON.stringify({ nodes: data.nodes, edges: data.edges })};
const PAPER_DATA = ${JSON.stringify(opts.paperPayload)};
const INCLUDE_PAPERS = ${opts.includePaperData};
${generateJS(opts)}
<\/script>
</body>
</html>`;
}

/** 生成 CSS */
function generateCSS(): string {
  return `
:root {
  --bg: #0F172A;
  --surface: #1E293B;
  --surface-hover: #334155;
  --text: #F1F5F9;
  --text-secondary: #94A3B8;
  --primary: #60A5FA;
  --accent: #F59E0B;
  --border: rgba(255,255,255,0.08);
  --radius: 10px;
  --panel-width: 360px;
}

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

body {
  font-family: 'Inter', 'Noto Sans SC', system-ui, sans-serif;
  background: var(--bg);
  color: var(--text);
  overflow: hidden;
  height: 100vh;
}

.app {
  display: flex;
  flex-direction: column;
  height: 100vh;
}

/* === Toolbar === */
.toolbar {
  display: flex;
  align-items: center;
  gap: 1.5rem;
  padding: 0.75rem 1.5rem;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  z-index: 10;
}
.graph-title {
  font-size: 1.1rem;
  font-weight: 700;
  color: var(--primary);
  white-space: nowrap;
}
.search-box {
  position: relative;
  flex: 0 1 280px;
}
.search-box input {
  width: 100%;
  padding: 0.45rem 0.75rem 0.45rem 2rem;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text);
  font-size: 0.85rem;
  outline: none;
  transition: border-color 0.2s;
}
.search-box input:focus { border-color: var(--primary); }
.search-icon {
  position: absolute;
  left: 0.6rem;
  top: 50%;
  transform: translateY(-50%);
  font-size: 0.8rem;
  opacity: 0.5;
}
.stats {
  display: flex;
  gap: 1rem;
  font-size: 0.8rem;
  color: var(--text-secondary);
  margin-left: auto;
}
.stats span {
  background: var(--bg);
  padding: 0.2rem 0.6rem;
  border-radius: 4px;
}

/* === Main Layout === */
.main {
  display: flex;
  flex: 1;
  overflow: hidden;
}
.graph-container {
  flex: 1;
  position: relative;
}
#graphSvg {
  width: 100%;
  height: 100%;
  cursor: grab;
}
#graphSvg:active { cursor: grabbing; }

/* === Side Panel === */
.panel {
  width: var(--panel-width);
  background: var(--surface);
  border-left: 1px solid var(--border);
  overflow-y: auto;
  transition: transform 0.3s;
  padding: 1.2rem;
}
.panel-placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
  color: var(--text-secondary);
  font-size: 0.9rem;
}

.panel-header {
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  margin-bottom: 1rem;
}
.panel-node-dot {
  width: 14px;
  height: 14px;
  border-radius: 50%;
  flex-shrink: 0;
  margin-top: 4px;
}
.panel-node-label {
  font-size: 1.15rem;
  font-weight: 700;
  line-height: 1.3;
}
.panel-category {
  display: inline-block;
  padding: 0.1rem 0.5rem;
  border-radius: 4px;
  font-size: 0.7rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
.panel-desc {
  font-size: 0.88rem;
  color: var(--text-secondary);
  line-height: 1.6;
  margin-bottom: 1rem;
}
.panel-section-title {
  font-size: 0.8rem;
  font-weight: 600;
  color: var(--primary);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin: 1rem 0 0.5rem;
}

/* Paper list */
.paper-card {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.75rem;
  margin-bottom: 0.5rem;
  transition: border-color 0.2s;
  cursor: default;
}
.paper-card:hover { border-color: var(--primary); }
.paper-title {
  font-size: 0.85rem;
  font-weight: 600;
  line-height: 1.4;
  margin-bottom: 0.3rem;
}
.paper-authors {
  font-size: 0.75rem;
  color: var(--text-secondary);
  margin-bottom: 0.3rem;
}
.paper-meta {
  display: flex;
  gap: 0.75rem;
  font-size: 0.7rem;
  color: var(--text-secondary);
}
.paper-meta span {
  background: var(--surface);
  padding: 0.1rem 0.4rem;
  border-radius: 3px;
}
.paper-actions {
  margin-top: 0.5rem;
  display: flex;
  gap: 0.5rem;
}
.btn-sm {
  padding: 0.25rem 0.6rem;
  font-size: 0.72rem;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--text);
  border-radius: 4px;
  cursor: pointer;
  transition: all 0.2s;
  text-decoration: none;
}
.btn-sm:hover { background: var(--primary); color: var(--bg); border-color: var(--primary); }
.btn-primary { background: var(--primary); color: var(--bg); border-color: var(--primary); }
.btn-primary:hover { opacity: 0.85; }

/* === Legend === */
.legend {
  position: fixed;
  bottom: 1rem;
  left: 1rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0.75rem;
  font-size: 0.75rem;
  z-index: 10;
  max-width: 180px;
}
.legend-title {
  font-weight: 600;
  color: var(--text-secondary);
  margin-bottom: 0.3rem;
  font-size: 0.7rem;
  text-transform: uppercase;
}
.legend-item {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.15rem 0;
  color: var(--text-secondary);
}
.legend-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  flex-shrink: 0;
}
.legend-line {
  width: 18px;
  height: 3px;
  border-radius: 2px;
  flex-shrink: 0;
}

/* === Tooltip === */
.tooltip {
  position: fixed;
  padding: 0.5rem 0.75rem;
  background: rgba(15,23,42,0.95);
  border: 1px solid var(--border);
  border-radius: 6px;
  font-size: 0.8rem;
  color: var(--text);
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.15s;
  z-index: 100;
  max-width: 250px;
}
.tooltip.visible { opacity: 1; }
.tooltip-label { font-weight: 600; }
.tooltip-count { color: var(--text-secondary); font-size: 0.7rem; }

/* Node highlight */
.node-dimmed circle { opacity: 0.15; }
.node-dimmed text { opacity: 0.15; }
.edge-dimmed { opacity: 0.05 !important; }
.node-highlight circle { filter: drop-shadow(0 0 8px currentColor); }

/* Search highlight */
.node-search-match circle { stroke: var(--accent); stroke-width: 3px; }

@media (max-width: 768px) {
  .panel { width: 100%; position: fixed; bottom: 0; left: 0; height: 50vh; border-left: none; border-top: 1px solid var(--border); transform: translateY(100%); z-index: 20; }
  .panel.open { transform: translateY(0); }
  .legend { display: none; }
  .stats { display: none; }
}
`;
}

/** 生成交互 JS */
function generateJS(opts: Required<GraphHtmlOptions>): string {
  return `
(function() {
  'use strict';

  var nodes = GRAPH_DATA.nodes;
  var edges = GRAPH_DATA.edges;

  var container = document.getElementById('graphContainer');
  var svg = d3.select('#graphSvg');
  var width = container.clientWidth;
  var height = container.clientHeight;

  svg.attr('viewBox', [0, 0, width, height]);

  // === Zoom ===
  var g = svg.append('g').attr('class', 'graph-root');
  var zoom = d3.zoom()
    .scaleExtent([0.1, 5])
    .on('zoom', function(event) {
      g.attr('transform', event.transform);
    });
  svg.call(zoom);

  // === Arrow markers ===
  svg.append('defs').selectAll('marker')
    .data(['prerequisite', 'derived', 'component', 'related'])
    .join('marker')
    .attr('id', function(d) { return 'arrow-' + d; })
    .attr('viewBox', '0 -5 10 10')
    .attr('refX', 20)
    .attr('refY', 0)
    .attr('markerWidth', 6)
    .attr('markerHeight', 6)
    .attr('orient', 'auto')
    .append('path')
    .attr('d', 'M0,-5L10,0L0,5')
    .attr('fill', '#78909C');

  // === Edges ===
  var edgeGroup = g.append('g').attr('class', 'edges');
  var edgeEls = edgeGroup.selectAll('line')
    .data(edges)
    .join('line')
    .attr('stroke', function(d) { return d.style.color; })
    .attr('stroke-width', function(d) { return d.strokeWidth; })
    .attr('stroke-opacity', function(d) { return d.style.opacity; })
    .attr('stroke-dasharray', function(d) { return d.style.dasharray; })
    .attr('marker-end', function(d) { return 'url(#arrow-' + d.relation + ')'; });

  // === Nodes ===
  var nodeGroup = g.append('g').attr('class', 'nodes');
  var nodeEls = nodeGroup.selectAll('g')
    .data(nodes)
    .join('g')
    .attr('class', 'node')
    .style('cursor', 'pointer');

  nodeEls.append('circle')
    .attr('r', function(d) { return d.radius; })
    .attr('fill', function(d) { return d.color; })
    .attr('stroke', 'rgba(255,255,255,0.1)')
    .attr('stroke-width', 1.5);

  nodeEls.append('text')
    .text(function(d) { return d.label; })
    .attr('text-anchor', 'middle')
    .attr('dy', function(d) { return d.radius + 14; })
    .attr('fill', '#94A3B8')
    .attr('font-size', function(d) { return Math.max(10, Math.min(d.radius * 0.45, 14)) + 'px'; })
    .attr('font-weight', '500')
    .attr('pointer-events', 'none');

  // === Drag ===
  var drag = d3.drag()
    .on('start', function(event, d) {
      if (!event.active) simulation.alphaTarget(0.3).restart();
      d.fx = d.x;
      d.fy = d.y;
    })
    .on('drag', function(event, d) {
      d.fx = event.x;
      d.fy = event.y;
    })
    .on('end', function(event, d) {
      if (!event.active) simulation.alphaTarget(0);
      d.fx = null;
      d.fy = null;
    });
  nodeEls.call(drag);

  // === Force Simulation ===
  var simulation = d3.forceSimulation(nodes)
    .force('link', d3.forceLink(edges)
      .id(function(d) { return d.id; })
      .distance(function(d) { return 120 - d.strokeWidth * 5; })
    )
    .force('charge', d3.forceManyBody()
      .strength(function(d) { return -200 - d.radius * 5; })
    )
    .force('collide', d3.forceCollide()
      .radius(function(d) { return d.radius + 8; })
    )
    .force('center', d3.forceCenter(width / 2, height / 2))
    .on('tick', ticked);

  function ticked() {
    edgeEls
      .attr('x1', function(d) { return d.source.x; })
      .attr('y1', function(d) { return d.source.y; })
      .attr('x2', function(d) { return d.target.x; })
      .attr('y2', function(d) { return d.target.y; });
    nodeEls.attr('transform', function(d) { return 'translate(' + d.x + ',' + d.y + ')'; });
  }

  // === Tooltip ===
  var tooltip = document.getElementById('tooltip');
  nodeEls.on('mouseenter', function(event, d) {
    tooltip.innerHTML = '<div class="tooltip-label">' + escHtml(d.label) + '</div>' +
      '<div class="tooltip-count">' + d.paperCount + ' papers · importance ' + d.importance + '</div>';
    tooltip.classList.add('visible');
  }).on('mousemove', function(event) {
    tooltip.style.left = (event.clientX + 12) + 'px';
    tooltip.style.top = (event.clientY - 10) + 'px';
  }).on('mouseleave', function() {
    tooltip.classList.remove('visible');
  });

  // === Click → Panel ===
  var selectedNode = null;
  var panel = document.getElementById('panel');
  var panelContent = document.getElementById('panelContent');
  var panelPlaceholder = panel.querySelector('.panel-placeholder');

  nodeEls.on('click', function(event, d) {
    event.stopPropagation();
    selectedNode = d;
    showPanel(d);
    highlightNode(d);
  });

  svg.on('click', function() {
    selectedNode = null;
    hidePanel();
    clearHighlight();
  });

  function showPanel(d) {
    panelPlaceholder.style.display = 'none';
    panelContent.style.display = 'block';
    panel.classList.add('open');

    var html = '<div class="panel-header">' +
      '<span class="panel-node-dot" style="background:' + d.color + '"></span>' +
      '<div><div class="panel-node-label">' + escHtml(d.label) + '</div>' +
      '<span class="panel-category" style="background:' + d.color + '22;color:' + d.color + '">' + d.category + '</span>' +
      '</div></div>';

    if (d.description) {
      html += '<p class="panel-desc">' + escHtml(d.description) + '</p>';
    }

    html += '<div style="display:flex;gap:0.75rem;flex-wrap:wrap;font-size:0.78rem;color:#94A3B8;margin-bottom:0.75rem">' +
      '<span>Importance: ' + d.importance + '/5</span>' +
      '<span>Papers: ' + d.paperCount + '</span></div>';

    // Connected nodes
    var connected = edges.filter(function(e) {
      var sid = typeof e.source === 'object' ? e.source.id : e.source;
      var tid = typeof e.target === 'object' ? e.target.id : e.target;
      return sid === d.id || tid === d.id;
    });

    if (connected.length > 0) {
      html += '<div class="panel-section-title">Connections (' + connected.length + ')</div>';
      connected.forEach(function(e) {
        var sid = typeof e.source === 'object' ? e.source.id : e.source;
        var tid = typeof e.target === 'object' ? e.target.id : e.target;
        var otherId = sid === d.id ? tid : sid;
        var other = nodes.find(function(n) { return n.id === otherId; });
        if (other) {
          var dir = sid === d.id ? ' → ' : ' ← ';
          html += '<div class="legend-item" style="padding:0.2rem 0"><span class="legend-dot" style="background:' +
            other.color + '"></span>' + dir + escHtml(other.label) +
            ' <span style="color:#64748B;font-size:0.7rem">(' + e.relation + ')</span></div>';
        }
      });
    }

    // Papers
    if (INCLUDE_PAPERS && PAPER_DATA[d.id] && PAPER_DATA[d.id].length > 0) {
      var papers = PAPER_DATA[d.id];
      html += '<div class="panel-section-title">Related Papers (' + papers.length + ')</div>';
      papers.forEach(function(p) {
        html += '<div class="paper-card">' +
          '<div class="paper-title">' + escHtml(p.title) + '</div>' +
          '<div class="paper-authors">' + escHtml(p.authors.slice(0, 3).join(', ')) + (p.authors.length > 3 ? ' et al.' : '') + '</div>' +
          '<div class="paper-meta">' +
          (p.citations != null ? '<span>' + p.citations + ' citations</span>' : '') +
          '<span>relevance: ' + (p.relevance * 100).toFixed(0) + '%</span>' +
          '<span>' + p.mentionType + '</span></div>' +
          '<div class="paper-actions">';

        if (p.url) {
          html += '<a class="btn-sm" href="' + escHtml(p.url) + '" target="_blank">Open Paper</a>';
        }
        html += '<button class="btn-sm btn-primary" onclick="openPaperPreview(\\'' + escapeForJs(p.id) + '\\')">View Presentation</button>';
        html += '</div></div>';
      });
    } else if (d.paperCount > 0) {
      html += '<div class="panel-section-title">Papers</div>';
      html += '<p class="panel-desc">' + d.paperCount + ' papers associated. Run full analysis for details.</p>';
    }

    panelContent.innerHTML = html;
  }

  function hidePanel() {
    panelPlaceholder.style.display = 'flex';
    panelContent.style.display = 'none';
    panel.classList.remove('open');
  }

  // === Highlight ===
  function highlightNode(d) {
    var connectedIds = new Set();
    connectedIds.add(d.id);
    edges.forEach(function(e) {
      var sid = typeof e.source === 'object' ? e.source.id : e.source;
      var tid = typeof e.target === 'object' ? e.target.id : e.target;
      if (sid === d.id) connectedIds.add(tid);
      if (tid === d.id) connectedIds.add(sid);
    });

    nodeEls.classed('node-dimmed', function(n) { return !connectedIds.has(n.id); });
    nodeEls.classed('node-highlight', function(n) { return n.id === d.id; });
    edgeEls.classed('edge-dimmed', function(e) {
      var sid = typeof e.source === 'object' ? e.source.id : e.source;
      var tid = typeof e.target === 'object' ? e.target.id : e.target;
      return !connectedIds.has(sid) || !connectedIds.has(tid);
    });
  }

  function clearHighlight() {
    nodeEls.classed('node-dimmed', false).classed('node-highlight', false);
    edgeEls.classed('edge-dimmed', false);
  }

  // === Search ===
  var searchInput = document.getElementById('searchInput');
  searchInput.addEventListener('input', function() {
    var q = this.value.trim().toLowerCase();
    nodeEls.classed('node-search-match', function(d) {
      return q && d.label.toLowerCase().includes(q);
    });
    if (q) {
      var match = nodes.find(function(n) { return n.label.toLowerCase().includes(q); });
      if (match && match.x != null) {
        var t = d3.zoomIdentity.translate(width/2 - match.x, height/2 - match.y);
        svg.transition().duration(500).call(zoom.transform, t);
      }
    } else {
      nodeEls.classed('node-search-match', false);
    }
  });

  // === Paper Preview (client-side mini presentation) ===
  window.openPaperPreview = function(paperId) {
    if (!PAPER_DATA) return;
    var paper = null;
    for (var nid in PAPER_DATA) {
      var found = PAPER_DATA[nid].find(function(p) { return p.id === paperId; });
      if (found) { paper = found; break; }
    }
    if (!paper) return;

    // Build minimal presentation HTML
    var html = '<!DOCTYPE html><html><head><meta charset="UTF-8"><title>' + escHtml(paper.title) +
      '</title><style>' +
      'body{margin:0;font-family:Inter,system-ui,sans-serif;background:#0F172A;color:#F1F5F9}' +
      '.slide{min-height:100vh;display:flex;flex-direction:column;justify-content:center;padding:4rem 8rem;scroll-snap-align:start}' +
      'h1{font-size:2.5rem;color:#60A5FA;margin-bottom:1rem}' +
      'h2{font-size:1.8rem;color:#60A5FA;margin-bottom:1rem}' +
      '.authors{color:#F59E0B;font-size:1.2rem;margin-bottom:0.5rem}' +
      'p{font-size:1.1rem;line-height:1.8;color:#94A3B8;max-width:900px}' +
      '.meta{font-size:0.9rem;color:#64748B;margin-top:1rem}' +
      'html{scroll-snap-type:y mandatory;scrollbar-width:none}' +
      '</style></head><body>' +
      '<section class="slide" style="text-align:center;align-items:center">' +
      '<h1>' + escHtml(paper.title) + '</h1>' +
      '<div class="authors">' + escHtml(paper.authors.join(', ')) + '</div>' +
      (paper.url ? '<div class="meta"><a href="' + escHtml(paper.url) + '" style="color:#60A5FA" target="_blank">View Paper</a></div>' : '') +
      '</section>' +
      '<section class="slide"><h2>Abstract</h2><p>' + escHtml(paper.abstract) + '</p></section>' +
      '</body></html>';

    var blob = new Blob([html], {type: 'text/html'});
    var url = URL.createObjectURL(blob);
    window.open(url, '_blank');
  };

  // === Resize ===
  window.addEventListener('resize', function() {
    width = container.clientWidth;
    height = container.clientHeight;
    svg.attr('viewBox', [0, 0, width, height]);
    simulation.force('center', d3.forceCenter(width / 2, height / 2));
    simulation.alpha(0.3).restart();
  });

  function escHtml(s) {
    if (!s) return '';
    return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
  }

  function escapeForJs(s) {
    return s.replace(/'/g, "\\\\'").replace(/"/g, '\\\\"');
  }
})();
`;
}

/** HTML 转义（构建时） */
function escapeHtml(str: string): string {
  return str
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');
}


path: graph-viz/scripts/paper-viz-bridge.ts
content: /**
 * @module graph-viz/paper-viz-bridge
 * @description 图谱→论文演示的数据桥接
 *
 * 从 KnowledgeGraphData 的 literatureIndex 和 conceptIndex 中
 * 为每个节点构建论文摘要数据，嵌入图谱 HTML 供前端使用。
 */

import type {
  KnowledgeGraphData,
  KnowledgeNode,
  LiteratureReference,
  PaperMetadata,
} from '../../knowledge-graph/scripts/graph';
import type { PaperPayload, NodePaperSummary } from './types';

/** 桥接配置选项 */
export interface BridgeOptions {
  /** 每个节点最多包含的论文数 */
  maxPapersPerNode?: number;
  /** 最小相关度阈值 */
  minRelevance?: number;
  /** 摘要截断长度 */
  abstractMaxLength?: number;
}

const DEFAULT_OPTIONS: Required<BridgeOptions> = {
  maxPapersPerNode: 10,
  minRelevance: 0.1,
  abstractMaxLength: 300,
};

/**
 * PaperVizBridge — 构建图谱节点的论文数据 payload
 *
 * @example
 * ```ts
 * const bridge = new PaperVizBridge();
 * const payload = bridge.buildAllPaperPayloads(graph);
 * // payload 可直接传给 generateGraphHtml 的 paperPayload 选项
 * ```
 */
export class PaperVizBridge {
  private options: Required<BridgeOptions>;

  constructor(options?: BridgeOptions) {
    this.options = { ...DEFAULT_OPTIONS, ...options };
  }

  /**
   * 为所有节点构建论文数据
   * @param graph - 知识图谱数据
   * @returns 节点 ID → 论文列表的映射
   */
  buildAllPaperPayloads(graph: KnowledgeGraphData): PaperPayload {
    const payload: PaperPayload = {};

    for (const node of graph.nodes) {
      const papers = this.buildNodePapers(node, graph);
      if (papers.length > 0) {
        payload[node.id] = papers;
      }
    }

    return payload;
  }

  /**
   * 为单个节点构建论文列表
   * @param node - 知识节点
   * @param graph - 知识图谱数据
   * @returns 论文摘要列表（按相关度排序）
   */
  buildNodePapers(node: KnowledgeNode, graph: KnowledgeGraphData): NodePaperSummary[] {
    const paperMap = new Map<string, NodePaperSummary>();

    // 来源 1: node.literatureReferences
    if (node.literatureReferences) {
      for (const ref of node.literatureReferences) {
        if (ref.relevance < this.options.minRelevance) continue;
        const meta = this.findPaperMetadata(ref.paperId, graph);
        paperMap.set(ref.paperId, this.buildSummary(ref, meta));
      }
    }

    // 来源 2: conceptIndex.conceptToPapers
    if (graph.conceptIndex?.conceptToPapers) {
      const refs = graph.conceptIndex.conceptToPapers.get(node.id);
      if (refs) {
        for (const ref of refs) {
          if (ref.relevance < this.options.minRelevance) continue;
          if (paperMap.has(ref.paperId)) continue; // 去重
          const meta = this.findPaperMetadata(ref.paperId, graph);
          paperMap.set(ref.paperId, this.buildSummary(ref, meta));
        }
      }
    }

    // 按相关度降序排序，截断
    return Array.from(paperMap.values())
      .sort((a, b) => b.relevance - a.relevance)
      .slice(0, this.options.maxPapersPerNode);
  }

  /** 从图谱 literatureIndex 中查找论文元数据 */
  private findPaperMetadata(paperId: string, graph: KnowledgeGraphData): PaperMetadata | null {
    if (graph.literatureIndex?.papers) {
      return graph.literatureIndex.papers.get(paperId) ?? null;
    }
    return null;
  }

  /** 构建论文摘要 */
  private buildSummary(ref: LiteratureReference, meta: PaperMetadata | null): NodePaperSummary {
    const maxLen = this.options.abstractMaxLength;

    return {
      id: ref.paperId,
      title: meta?.title ?? ref.title,
      authors: meta?.authors ?? [],
      abstract: meta?.abstract
        ? (meta.abstract.length > maxLen
          ? meta.abstract.slice(0, maxLen) + '...'
          : meta.abstract)
        : '',
      url: meta?.url ?? '',
      citations: meta?.citations,
      relevance: ref.relevance,
      mentionType: ref.mentionType,
    };
  }
}


path: graph-viz/scripts/types.ts
content: /**
 * @module graph-viz/types
 * @description 交互式知识图谱可视化的数据类型定义
 */

/** 节点类别 */
export type NodeCategory = 'foundation' | 'core' | 'advanced' | 'application';

/** 关系类型 */
export type RelationType = 'prerequisite' | 'related' | 'derived' | 'component';

/** 类别颜色映射 */
export const CATEGORY_COLORS: Record<NodeCategory, string> = {
  foundation: '#4FC3F7',
  core: '#FFB74D',
  advanced: '#CE93D8',
  application: '#81C784',
};

/** 关系权重映射 */
export const RELATION_WEIGHTS: Record<RelationType, number> = {
  prerequisite: 3,
  derived: 2.5,
  component: 2,
  related: 1,
};

/** D3 力导向图节点 */
export interface D3GraphNode {
  /** 节点唯一 ID */
  id: string;
  /** 显示标签 */
  label: string;
  /** 类别 */
  category: NodeCategory;
  /** 重要度 (1-5) */
  importance: number;
  /** 节点描述 */
  description: string;
  /** 节点半径（根据关联论文数计算） */
  radius: number;
  /** 关联论文数量 */
  paperCount: number;
  /** 关键论文 ID 列表 */
  keyPaperIds: string[];
  /** 节点颜色 */
  color: string;
  /** D3 力模拟坐标（运行时填充） */
  x?: number;
  y?: number;
  fx?: number | null;
  fy?: number | null;
}

/** D3 力导向图边 */
export interface D3GraphEdge {
  /** 源节点 ID */
  source: string;
  /** 目标节点 ID */
  target: string;
  /** 关系类型 */
  relation: RelationType;
  /** 显示标签 */
  label: string;
  /** 边线宽度（根据关系权重 + 共享论文数计算） */
  strokeWidth: number;
  /** 共享论文数 */
  sharedPaperCount: number;
  /** 样式 */
  style: EdgeStyle;
}

/** 边样式 */
export interface EdgeStyle {
  /** 颜色 */
  color: string;
  /** SVG dash 样式（虚线/实线） */
  dasharray: string;
  /** 透明度 */
  opacity: number;
}

/** D3 完整图谱数据 */
export interface D3GraphData {
  /** 节点列表 */
  nodes: D3GraphNode[];
  /** 边列表 */
  edges: D3GraphEdge[];
  /** 图谱元数据 */
  metadata: GraphVizMetadata;
}

/** 图谱可视化元数据 */
export interface GraphVizMetadata {
  /** 图谱名称 */
  name: string;
  /** 总节点数 */
  totalNodes: number;
  /** 总边数 */
  totalEdges: number;
  /** 总论文数 */
  totalPapers: number;
  /** 生成时间 */
  generatedAt: string;
}

/** 节点关联的论文摘要（嵌入图谱 HTML） */
export interface NodePaperSummary {
  /** 论文 ID */
  id: string;
  /** 标题 */
  title: string;
  /** 作者 */
  authors: string[];
  /** 摘要（截断） */
  abstract: string;
  /** URL */
  url: string;
  /** 引用数 */
  citations?: number;
  /** 关联度 */
  relevance: number;
  /** 引用类型 */
  mentionType: string;
}

/** 嵌入图谱的论文数据 payload */
export interface PaperPayload {
  /** 节点 ID → 论文列表 */
  [nodeId: string]: NodePaperSummary[];
}

/** 图谱 HTML 生成选项 */
export interface GraphHtmlOptions {
  /** 是否嵌入论文数据 */
  includePaperData?: boolean;
  /** 论文数据 payload */
  paperPayload?: PaperPayload;
  /** 画布宽度 */
  width?: number;
  /** 画布高度 */
  height?: number;
}

/** 关系样式映射 */
export const RELATION_STYLES: Record<RelationType, Omit<EdgeStyle, 'opacity'>> = {
  prerequisite: { color: '#EF5350', dasharray: '' },
  derived: { color: '#AB47BC', dasharray: '8 4' },
  component: { color: '#42A5F5', dasharray: '4 2' },
  related: { color: '#78909C', dasharray: '2 4' },
};


path: index.js
content: #!/usr/bin/env node

/**
 * ScholarGraph - Academic Literature Intelligence Toolkit
 * Entry point for the skill
 */

// Re-export all modules for programmatic usage
export { default as LiteratureSearch } from './literature-search/scripts/search.ts';
export { default as ConceptLearner } from './concept-learner/scripts/learn.ts';
export { default as KnowledgeGapDetector } from './knowledge-gap-detector/scripts/detect.ts';
export { default as ProgressTracker } from './progress-tracker/scripts/track.ts';
export { default as PaperAnalyzer } from './paper-analyzer/scripts/analyze.ts';
export { default as KnowledgeGraphBuilder } from './knowledge-graph/scripts/graph.ts';

// Re-export types
export * from './shared/types.ts';

// Re-export configuration
export { ConfigManager, defaultConfig } from './config.ts';

// Main CLI entry point
if (import.meta.main) {
  // Import and run CLI
  await import('./cli.ts');
}


path: knowledge-gap-detector/scripts/detect.ts
content: /**
 * Knowledge Gap Detector - Core Module
 * 知识盲区检测核心模块
 *
 * 主动识别知识缺口：
 * - 基于领域知识图谱分析
 * - 识别相关但未探索的方向
 * - 发现跨学科交叉点
 * - 推荐学习优先级
 */

import { createAIProvider, type AIProvider } from '../../shared/ai-provider';
import { extractJson } from '../../shared/utils';
import { ApiInitializationError, getErrorMessage } from '../../shared/errors';
import type { WebSearchResultItem } from '../../shared/types';
import type {
  DetectOptions,
  GapReport,
  KnowledgeGap,
  GapSummary,
  EstimatedEffort,
  KnowledgeProfile,
  LearningPathAnalysisOptions,
  CrossDisciplinaryOptions
} from './types';

// 领域知识图谱（简化版，实际应用可扩展）
const DOMAIN_GRAPHS: Record<string, string[]> = {
  'Natural Language Processing': [
    'tokenization', 'word embeddings', 'language models', 'sequence modeling',
    'RNN', 'LSTM', 'GRU', 'attention mechanism', 'transformer',
    'BERT', 'GPT', 'text classification', 'named entity recognition',
    'sentiment analysis', 'machine translation', 'question answering',
    'text generation', 'summarization', 'coreference resolution',
    'dependency parsing', 'constituency parsing', 'part-of-speech tagging',
    'word2vec', 'GloVe', 'FastText', 'ELMo', 'positional encoding',
    'self-attention', 'multi-head attention', 'encoder-decoder',
    'pre-training', 'fine-tuning', 'prompt engineering', 'RAG',
    'semantic search', 'knowledge graphs for NLP', 'dialogue systems'
  ],
  'Machine Learning': [
    'supervised learning', 'unsupervised learning', 'reinforcement learning',
    'regression', 'classification', 'clustering', 'dimensionality reduction',
    'decision trees', 'random forests', 'gradient boosting', 'XGBoost',
    'SVM', 'naive bayes', 'k-nearest neighbors', 'logistic regression',
    'neural networks', 'CNN', 'RNN', 'deep learning',
    'backpropagation', 'optimization', 'regularization', 'cross-validation',
    'feature engineering', 'feature selection', 'hyperparameter tuning',
    'ensemble methods', 'bagging', 'boosting', 'stacking',
    'overfitting', 'underfitting', 'bias-variance tradeoff',
    'data preprocessing', 'normalization', 'standardization',
    'model evaluation', 'precision', 'recall', 'F1 score', 'AUC-ROC',
    'transfer learning', 'meta-learning', 'few-shot learning'
  ],
  'Deep Learning': [
    'neural network fundamentals', 'activation functions', 'loss functions',
    'optimizers', 'gradient descent', 'backpropagation',
    'CNN', 'RNN', 'LSTM', 'GRU', 'transformer',
    'attention mechanism', 'self-attention', 'multi-head attention',
    'batch normalization', 'layer normalization', 'dropout',
    'weight initialization', 'residual connections', 'skip connections',
    'autoencoders', 'VAE', 'GANs', 'diffusion models',
    'transfer learning', 'fine-tuning', 'domain adaptation',
    'computer vision', 'image classification', 'object detection',
    'semantic segmentation', 'instance segmentation',
    'natural language processing', 'text classification', 'sequence labeling',
    'speech recognition', 'time series analysis',
    'model compression', 'quantization', 'pruning', 'knowledge distillation'
  ],
  'Computer Vision': [
    'image processing fundamentals', 'feature detection', 'edge detection',
    'CNN architectures', 'AlexNet', 'VGG', 'ResNet', 'Inception', 'EfficientNet',
    'object detection', 'YOLO', 'R-CNN', 'Faster R-CNN', 'SSD',
    'semantic segmentation', 'instance segmentation', 'panoptic segmentation',
    'image classification', 'transfer learning for vision',
    'object tracking', 'optical flow', 'pose estimation',
    'face recognition', 'face detection', 'facial landmark detection',
    'OCR', 'text detection', 'text recognition',
    'image generation', 'GANs', 'diffusion models', 'VAE',
    'video analysis', 'action recognition', 'video segmentation',
    '3D vision', 'point clouds', 'depth estimation', 'SLAM',
    'vision transformers', 'ViT', 'Swin Transformer',
    'multimodal learning', 'vision-language models', 'CLIP'
  ]
};

export default class KnowledgeGapDetector {
  private ai: AIProvider | null = null;

  async initialize(): Promise<void> {
    if (!this.ai) {
      try {
        this.ai = await createAIProvider();
      } catch (error) {
        throw new ApiInitializationError(
          `Failed to initialize AI provider: ${getErrorMessage(error)}`,
          error instanceof Error ? error : undefined
        );
      }
    }
  }

  /**
   * 检测知识盲区
   */
  async detect(options: DetectOptions): Promise<GapReport> {
    await this.initialize();

    const {
      domain,
      knownConcepts,
      targetLevel = 'intermediate',
      focusAreas,
      excludeConcepts = []
    } = options;

    // 获取领域知识图谱
    const domainGraph = this.getDomainGraph(domain);

    // 识别未掌握的概念
    const unknownConcepts = this.findUnknownConcepts(
      domainGraph,
      knownConcepts,
      excludeConcepts
    );

    // 分类知识缺口
    const classifiedGaps = await this.classifyGaps(
      unknownConcepts,
      knownConcepts,
      domain,
      targetLevel
    );

    // 发现新兴主题
    const emergingTopics = await this.discoverEmergingTopics(domain, knownConcepts);

    // 发现跨学科机会
    const crossDisciplinary = await this.findCrossDisciplinary(
      domain,
      knownConcepts
    );

    // 生成建议学习顺序
    const suggestedOrder = this.generateSuggestedOrder([
      ...classifiedGaps.critical,
      ...classifiedGaps.recommended
    ]);

    // 计算摘要
    const summary = this.calculateSummary(
      classifiedGaps,
      domainGraph,
      knownConcepts
    );

    // 估算学习工作量
    const estimatedEffort = this.estimateEffort(classifiedGaps);

    return {
      domain,
      analysisDate: new Date().toISOString(),
      summary,
      criticalGaps: classifiedGaps.critical,
      recommendedGaps: classifiedGaps.recommended,
      optionalGaps: classifiedGaps.optional,
      crossDisciplinary,
      emergingTopics,
      suggestedOrder,
      estimatedEffort
    };
  }

  /**
   * 从知识档案检测
   */
  async detectFromProfile(options: { profilePath: string; domain: string }): Promise<GapReport> {
    // 读取档案（简化实现）
    const profile: KnowledgeProfile = {
      domain: options.domain,
      knownConcepts: [],
      skillLevel: {},
      learningGoals: [],
      lastUpdated: new Date().toISOString()
    };

    return this.detect({
      domain: options.domain,
      knownConcepts: profile.knownConcepts
    });
  }

  /**
   * 分析学习路径缺口
   */
  async analyzeLearningPath(options: LearningPathAnalysisOptions): Promise<GapReport> {
    await this.initialize();

    const { currentPath, targetRole } = options;

    // 获取目标角色所需技能
    const requiredSkills = await this.getRoleRequiredSkills(targetRole);

    // 识别缺口
    const gaps = requiredSkills.filter(skill => !currentPath.includes(skill));

    return this.detect({
      domain: targetRole,
      knownConcepts: currentPath
    });
  }

  /**
   * 发现跨学科机会
   */
  async discoverCrossDisciplinary(options: CrossDisciplinaryOptions): Promise<KnowledgeGap[]> {
    await this.initialize();

    const { primaryDomain, relatedDomains, knownConcepts = [] } = options;

    const prompt = `作为一个跨学科研究专家，分析以下领域的交叉机会：

主领域: ${primaryDomain}
相关领域: ${relatedDomains.join(', ')}
已知概念: ${knownConcepts.join(', ')}

请识别：
1. 主领域与相关领域的交叉点
2. 可迁移的技术和方法
3. 有研究价值的跨学科方向

返回JSON格式：
{
  "crossDisciplinaryGaps": [
    {
      "concept": "交叉概念名称",
      "category": "cross-disciplinary",
      "reason": "为什么重要",
      "importance": 1-5,
      "prerequisites": ["前置知识"],
      "relatedKnown": ["相关已知知识"],
      "estimatedTime": "学习时间估计",
      "impactIfLearned": "学会后的影响"
    }
  ]
}`;

    const responseText = await this.ai!.chat([
      { role: 'system', content: '你是一位跨学科研究专家，擅长发现学科交叉创新机会。' },
      { role: 'user', content: prompt }
    ], { temperature: 0.4 });

    const result = extractJson<{ crossDisciplinaryGaps?: KnowledgeGap[] }>(responseText);

    if (result.success && result.data?.crossDisciplinaryGaps) {
      return result.data.crossDisciplinaryGaps.map(gap => ({
        ...gap,
        resources: []
      }));
    }

    return [];
  }

  /**
   * 获取领域知识图谱
   */
  private getDomainGraph(domain: string): string[] {
    // 查找匹配的领域图谱
    for (const [key, concepts] of Object.entries(DOMAIN_GRAPHS)) {
      if (domain.toLowerCase().includes(key.toLowerCase()) ||
          key.toLowerCase().includes(domain.toLowerCase())) {
        return concepts;
      }
    }

    // 如果没有找到，返回基础ML概念
    return DOMAIN_GRAPHS['Machine Learning'];
  }

  /**
   * 查找未掌握概念
   */
  private findUnknownConcepts(
    domainGraph: string[],
    knownConcepts: string[],
    excludeConcepts: string[]
  ): string[] {
    const normalizedKnown = new Set(
      knownConcepts.map(c => c.toLowerCase().trim())
    );
    const normalizedExclude = new Set(
      excludeConcepts.map(c => c.toLowerCase().trim())
    );

    return domainGraph.filter(concept => {
      const normalized = concept.toLowerCase().trim();
      return !normalizedKnown.has(normalized) && !normalizedExclude.has(normalized);
    });
  }

  /**
   * 分类知识缺口
   */
  private async classifyGaps(
    unknownConcepts: string[],
    knownConcepts: string[],
    domain: string,
    targetLevel: string
  ): Promise<{
    critical: KnowledgeGap[];
    recommended: KnowledgeGap[];
    optional: KnowledgeGap[];
  }> {
    const prompt = `作为知识体系专家，分析以下知识缺口并分类：

领域: ${domain}
未知概念: ${unknownConcepts.join(', ')}
已知概念: ${knownConcepts.join(', ')}
目标水平: ${targetLevel}

请将每个未知概念分类为：
- critical: 必须掌握的基础或核心概念
- recommended: 建议学习的扩展概念
- optional: 可选的高级概念

返回JSON格式：
{
  "gaps": [
    {
      "concept": "概念名",
      "category": "critical|recommended|optional",
      "reason": "分类原因",
      "importance": 1-5,
      "prerequisites": ["前置知识"],
      "relatedKnown": ["相关已知概念"],
      "estimatedTime": "学习时间",
      "impactIfLearned": "学习后的影响"
    }
  ]
}`;

    const responseText = await this.ai!.chat([
      { role: 'system', content: '你是一位知识体系分析专家。' },
      { role: 'user', content: prompt }
    ], { temperature: 0.2 });

    const result = extractJson<{ gaps?: Array<KnowledgeGap & { category: string }> }>(responseText);

    const critical: KnowledgeGap[] = [];
    const recommended: KnowledgeGap[] = [];
    const optional: KnowledgeGap[] = [];

    if (result.success && result.data?.gaps) {
      for (const gap of result.data.gaps) {
        const gapWithResources = { ...gap, resources: [] };
        if (gap.category === 'critical') {
          critical.push(gapWithResources);
        } else if (gap.category === 'recommended') {
          recommended.push(gapWithResources);
        } else {
          optional.push(gapWithResources);
        }
      }
    }

    return { critical, recommended, optional };
  }

  /**
   * 发现新兴主题
   */
  private async discoverEmergingTopics(
    domain: string,
    knownConcepts: string[]
  ): Promise<KnowledgeGap[]> {
    try {
      // 检查 AI 提供商是否支持 web search
      if (!this.ai!.webSearch) {
        console.warn('Current AI provider does not support web search');
        return [];
      }

      const results: WebSearchResultItem[] = await this.ai!.webSearch(
        `${domain} emerging trends 2024 new techniques`,
        5
      );

      return results.map((item) => ({
        concept: item.name,
        category: 'emerging' as const,
        reason: '该主题是近期新兴的研究方向',
        importance: 3 as const,
        prerequisites: [],
        relatedKnown: [],
        resources: [{
          type: 'paper' as const,
          title: item.name,
          url: item.url,
          description: item.snippet || '',
          difficulty: 'advanced' as const
        }],
        estimatedTime: '2-4周',
        impactIfLearned: '掌握前沿技术，保持竞争力'
      }));
    } catch (error) {
      console.error('Failed to discover emerging topics:', getErrorMessage(error));
      return [];
    }
  }

  /**
   * 发现跨学科机会
   */
  private async findCrossDisciplinary(
    domain: string,
    knownConcepts: string[]
  ): Promise<KnowledgeGap[]> {
    // 获取相关领域
    const relatedDomains = this.getRelatedDomains(domain);

    if (relatedDomains.length === 0) return [];

    return this.discoverCrossDisciplinary({
      primaryDomain: domain,
      relatedDomains,
      knownConcepts
    });
  }

  /**
   * 获取相关领域
   */
  private getRelatedDomains(domain: string): string[] {
    const domainRelations: Record<string, string[]> = {
      'Natural Language Processing': ['Machine Learning', 'Deep Learning', 'Knowledge Graphs', 'Speech Recognition'],
      'Machine Learning': ['Statistics', 'Deep Learning', 'Optimization', 'Data Science'],
      'Deep Learning': ['Machine Learning', 'Computer Vision', 'NLP', 'Reinforcement Learning'],
      'Computer Vision': ['Deep Learning', 'Image Processing', 'Robotics', 'Graphics']
    };

    for (const [key, related] of Object.entries(domainRelations)) {
      if (domain.toLowerCase().includes(key.toLowerCase())) {
        return related;
      }
    }

    return [];
  }

  /**
   * 生成建议学习顺序
   */
  private generateSuggestedOrder(gaps: KnowledgeGap[]): string[] {
    // 按重要性和前置关系排序
    const sorted = [...gaps].sort((a, b) => {
      // 先按重要性排序
      if (a.importance !== b.importance) {
        return b.importance - a.importance;
      }
      // 再按前置知识数量排序（少的先学）
      return a.prerequisites.length - b.prerequisites.length;
    });

    return sorted.map(g => g.concept);
  }

  /**
   * 计算摘要
   */
  private calculateSummary(
    classifiedGaps: { critical: KnowledgeGap[]; recommended: KnowledgeGap[]; optional: KnowledgeGap[] },
    domainGraph: string[],
    knownConcepts: string[]
  ): GapSummary {
    const criticalCount = classifiedGaps.critical.length;
    const recommendedCount = classifiedGaps.recommended.length;
    const optionalCount = classifiedGaps.optional.length;
    const totalGaps = criticalCount + recommendedCount + optionalCount;

    const coveragePercentage = Math.round(
      (knownConcepts.length / (knownConcepts.length + totalGaps)) * 100
    );

    return {
      totalGaps,
      criticalCount,
      recommendedCount,
      optionalCount,
      coveragePercentage
    };
  }

  /**
   * 估算学习工作量
   */
  private estimateEffort(
    classifiedGaps: { critical: KnowledgeGap[]; recommended: KnowledgeGap[]; optional: KnowledgeGap[] }
  ): EstimatedEffort {
    const criticalHours = classifiedGaps.critical.length * 10; // 每个关键概念约10小时
    const recommendedHours = classifiedGaps.recommended.length * 5; // 每个推荐概念约5小时

    return {
      critical: `约 ${Math.ceil(criticalHours / 10)} 周 (每周10小时)`,
      recommended: `约 ${Math.ceil(recommendedHours / 10)} 周 (每周10小时)`,
      total: `约 ${Math.ceil((criticalHours + recommendedHours) / 10)} 周 (每周10小时)`
    };
  }

  /**
   * 获取角色所需技能
   */
  private async getRoleRequiredSkills(role: string): Promise<string[]> {
    const responseText = await this.ai!.chat([
      {
        role: 'system',
        content: '你是一位职业发展专家，了解各类技术岗位的技能要求。'
      },
      {
        role: 'user',
        content: `列出${role}需要掌握的核心技能，返回JSON数组格式：["skill1", "skill2", ...]`
      }
    ], { temperature: 0.2 });

    const result = extractJson<string[]>(responseText);

    if (result.success && Array.isArray(result.data)) {
      return result.data;
    }

    return [];
  }

  /**
   * 导出为Markdown报告
   */
  toMarkdown(report: GapReport): string {
    return `# ${report.domain} 知识盲区分析报告

## 📊 总览

- 分析时间: ${report.analysisDate}
- 知识覆盖率: ${report.summary.coveragePercentage}%
- 发现缺口总数: ${report.summary.totalGaps}

| 类型 | 数量 |
|------|------|
| 关键缺口 | ${report.summary.criticalCount} |
| 建议学习 | ${report.summary.recommendedCount} |
| 可选扩展 | ${report.summary.optionalCount} |

## 🚨 关键缺口 (必须掌握)

${report.criticalGaps.map(g => `
### ${g.concept}
- **重要性**: ${'⭐'.repeat(g.importance)}
- **原因**: ${g.reason}
- **前置知识**: ${g.prerequisites.join(', ') || '无'}
- **预计时间**: ${g.estimatedTime}
- **学习影响**: ${g.impactIfLearned}
`).join('\n')}

## 📚 建议学习

${report.recommendedGaps.map(g => `
### ${g.concept}
- **重要性**: ${'⭐'.repeat(g.importance)}
- **原因**: ${g.reason}
- **预计时间**: ${g.estimatedTime}
`).join('\n')}

## 🔗 跨学科机会

${report.crossDisciplinary.map(g => `
### ${g.concept}
- **原因**: ${g.reason}
- **影响**: ${g.impactIfLearned}
`).join('\n')}

## 📈 新兴主题

${report.emergingTopics.map(g => `
- **${g.concept}**: ${g.reason}
`).join('\n')}

## 🎯 建议学习顺序

1. ${report.suggestedOrder.join('\n2. ')}

## ⏱️ 预计学习工作量

- 补齐关键缺口: ${report.estimatedEffort.critical}
- 完成建议学习: ${report.estimatedEffort.recommended}
- **总计**: ${report.estimatedEffort.total}

---
*由知识盲区检测器生成*
`;
  }
}

// CLI 支持
if (import.meta.main) {
  const args = process.argv.slice(2);

  const domainIndex = args.indexOf('--domain');
  const knownIndex = args.indexOf('--known');
  const outputIndex = args.indexOf('--output');

  const domain = domainIndex > -1 ? args[domainIndex + 1] : 'Machine Learning';
  const known = knownIndex > -1 ? args[knownIndex + 1].split(',').map(s => s.trim()) : [];
  const outputFile = outputIndex > -1 ? args[outputIndex + 1] : null;

  const detector = new KnowledgeGapDetector();

  detector.detect({ domain, knownConcepts: known }).then(report => {
    if (outputFile) {
      const fs = require('fs');
      fs.writeFileSync(outputFile, detector.toMarkdown(report));
      console.log(`Gap report saved to ${outputFile}`);
    } else {
      console.log(JSON.stringify(report, null, 2));
    }
  }).catch(err => {
    console.error('Error:', getErrorMessage(err));
    process.exit(1);
  });
}


path: knowledge-gap-detector/scripts/types.ts
content: /**
 * Knowledge Gap Detector - Type Definitions
 * 知识盲区检测器类型定义
 */

export interface DetectOptions {
  domain: string;
  knownConcepts: string[];
  targetLevel?: 'beginner' | 'intermediate' | 'advanced' | 'expert';
  focusAreas?: string[];
  excludeConcepts?: string[];
}

export interface ProfileBasedOptions {
  profilePath: string;
  domain: string;
}

export interface LearningPathAnalysisOptions {
  currentPath: string[];
  targetRole: string;
  industry?: string;
}

export interface CrossDisciplinaryOptions {
  primaryDomain: string;
  relatedDomains: string[];
  knownConcepts?: string[];
}

export interface GapReport {
  domain: string;
  analysisDate: string;
  summary: GapSummary;
  criticalGaps: KnowledgeGap[];
  recommendedGaps: KnowledgeGap[];
  optionalGaps: KnowledgeGap[];
  crossDisciplinary: KnowledgeGap[];
  emergingTopics: KnowledgeGap[];
  suggestedOrder: string[];
  estimatedEffort: EstimatedEffort;
}

export interface GapSummary {
  totalGaps: number;
  criticalCount: number;
  recommendedCount: number;
  optionalCount: number;
  coveragePercentage: number;
}

export interface KnowledgeGap {
  concept: string;
  category: 'critical' | 'recommended' | 'optional' | 'cross-disciplinary' | 'emerging';
  reason: string;
  importance: 1 | 2 | 3 | 4 | 5;
  prerequisites: string[];
  relatedKnown: string[];
  resources: LearningResource[];
  estimatedTime: string;
  impactIfLearned: string;
}

export interface LearningResource {
  type: 'paper' | 'tutorial' | 'course' | 'book' | 'documentation';
  title: string;
  url?: string;
  description: string;
  difficulty: 'beginner' | 'intermediate' | 'advanced';
}

export interface EstimatedEffort {
  critical: string;
  recommended: string;
  total: string;
}

export interface KnowledgeProfile {
  domain: string;
  knownConcepts: string[];
  skillLevel: Record<string, 'beginner' | 'intermediate' | 'advanced'>;
  learningGoals: string[];
  lastUpdated: string;
}

export interface DomainConcept {
  name: string;
  importance: number;
  prerequisites: string[];
  related: string[];
  category: 'foundation' | 'core' | 'advanced' | 'emerging';
  description: string;
}

export interface CitationBasedGap {
  paperTitle: string;
  missingConcepts: string[];
  relevance: number;
}


path: knowledge-graph/scripts/enricher.ts
content: /**
 * Graph Enricher - 图谱丰富模块
 * 搜索并关联关键论文到知识图谱中的概念
 */

import LiteratureSearch from '../../literature-search/scripts/search';
import ReviewDetector from '../../review-detector/scripts/detect';
import KnowledgeGraphIndexer from './indexer';
import type { SearchResult } from '../../literature-search/scripts/types';
import type { ExtractedConcept } from '../../concept-extractor/scripts/types';
import type {
  KnowledgeGraphData,
  PaperMetadata,
  LiteratureReference
} from './graph';

/**
 * 丰富选项
 */
export interface EnrichOptions {
  /** 每个概念搜索的论文数 */
  papersPerConcept?: number;
  /** 最小引用数过滤 */
  minCitations?: number;
  /** 最小相关度阈值 */
  minRelevance?: number;
  /** 是否过滤掉综述 */
  filterReviews?: boolean;
  /** 搜索数据源 */
  sources?: ('arxiv' | 'semantic_scholar' | 'web')[];
}

export default class GraphEnricher {
  private searcher: LiteratureSearch;
  private detector: ReviewDetector;
  private indexer: KnowledgeGraphIndexer;

  constructor() {
    this.searcher = new LiteratureSearch();
    this.detector = new ReviewDetector();
    this.indexer = new KnowledgeGraphIndexer();
  }

  async initialize(): Promise<void> {
    await this.searcher.initialize();
    // detector初始化延迟到需要AI判断时
  }

  /**
   * 为图谱中的概念搜索并关联关键论文
   */
  async enrichWithKeyPapers(
    graph: KnowledgeGraphData,
    concepts: ExtractedConcept[],
    options: EnrichOptions = {}
  ): Promise<KnowledgeGraphData> {
    const {
      papersPerConcept = 5,
      minCitations = 0,
      minRelevance = 0.3,
      filterReviews = true,
      sources = ['arxiv', 'semantic_scholar']
    } = options;

    console.log('\n🔍 搜索关键论文...');

    let totalAssociated = 0;

    for (const concept of concepts) {
      try {
        // 搜索论文
        const searchResults = await this.searcher.search(concept.name, {
          limit: papersPerConcept * 2, // 多搜索一些以备过滤
          sources,
          sortBy: 'citations',
          filters: { minCitations }
        });

        let candidatePapers = searchResults.results;

        // 过滤综述（只保留研究论文）
        if (filterReviews) {
          candidatePapers = await this.filterOutReviews(candidatePapers);
        }

        // 取前N篇
        const keyPapers = candidatePapers.slice(0, papersPerConcept);

        // 找到图谱中对应的节点
        const node = graph.nodes.find(n =>
          n.label === concept.name ||
          n.label === concept.nameEn ||
          n.label === concept.nameZh
        );

        if (!node) continue;

        let conceptAssociated = 0;

        for (const paper of keyPapers) {
          const relevance = this.calculateRelevance(paper, concept);
          if (relevance < minRelevance) continue;

          const paperMetadata: PaperMetadata = {
            id: paper.id,
            title: paper.title,
            authors: paper.authors,
            abstract: paper.abstract,
            publishDate: paper.publishDate,
            url: paper.url,
            pdfUrl: paper.pdfUrl,
            citations: paper.citations,
            paperType: 'research',
            keywords: paper.keywords || []
          };

          const reference: LiteratureReference = {
            paperId: paper.id,
            title: paper.title,
            relevance,
            mentionType: this.determineMentionType(concept, paper),
            contexts: []
          };

          this.indexer.addPaperToIndex(graph, node.id, paperMetadata, reference);
          conceptAssociated++;
          totalAssociated++;
        }

        console.log(`  ✓ ${concept.name}: 关联 ${conceptAssociated} 篇关键论文`);
      } catch (error) {
        console.error(`  ✗ ${concept.name}: 搜索失败 - ${error instanceof Error ? error.message : String(error)}`);
      }
    }

    console.log(`\n📊 共关联 ${totalAssociated} 篇关键论文`);

    // 重建索引
    this.indexer.buildIndex(graph);

    return graph;
  }

  /**
   * 添加综述论文到图谱
   */
  addReviewToGraph(
    graph: KnowledgeGraphData,
    review: SearchResult,
    relatedConceptIds: string[]
  ): void {
    const paperMetadata: PaperMetadata = {
      id: review.id,
      title: review.title,
      authors: review.authors,
      abstract: review.abstract,
      publishDate: review.publishDate,
      url: review.url,
      pdfUrl: review.pdfUrl,
      citations: review.citations,
      paperType: 'review',
      keywords: review.keywords || []
    };

    for (const conceptId of relatedConceptIds) {
      const reference: LiteratureReference = {
        paperId: review.id,
        title: review.title,
        relevance: 0.9, // 综述与其提取的概念高度相关
        mentionType: 'foundational',
        contexts: []
      };

      this.indexer.addPaperToIndex(graph, conceptId, paperMetadata, reference);
    }
  }

  /**
   * 过滤掉综述论文
   */
  private async filterOutReviews(papers: SearchResult[]): Promise<SearchResult[]> {
    const result: SearchResult[] = [];

    for (const paper of papers) {
      // 快速启发式检测（不使用AI，速度更快）
      const detection = await this.detector.detectReview(paper, { useAI: false });
      if (!detection.isReview || detection.confidence < 0.5) {
        result.push(paper);
      }
    }

    return result;
  }

  /**
   * 计算论文与概念的相关度
   */
  private calculateRelevance(paper: SearchResult, concept: ExtractedConcept): number {
    let score = 0;

    const titleLower = paper.title.toLowerCase();
    const abstractLower = (paper.abstract || '').toLowerCase();
    const conceptLower = concept.name.toLowerCase();

    // 标题包含概念名 - 高相关
    if (titleLower.includes(conceptLower)) {
      score += 0.4;
    }

    // 摘要包含概念名
    if (abstractLower.includes(conceptLower)) {
      score += 0.2;
    }

    // 检查别名匹配
    for (const alias of concept.aliases) {
      const aliasLower = alias.toLowerCase();
      if (titleLower.includes(aliasLower)) {
        score += 0.2;
        break;
      }
      if (abstractLower.includes(aliasLower)) {
        score += 0.1;
        break;
      }
    }

    // 引用数加分
    if (paper.citations) {
      if (paper.citations >= 500) score += 0.2;
      else if (paper.citations >= 100) score += 0.15;
      else if (paper.citations >= 50) score += 0.1;
      else if (paper.citations >= 10) score += 0.05;
    }

    return Math.min(1, Math.round(score * 100) / 100);
  }

  /**
   * 确定引用类型
   */
  private determineMentionType(
    concept: ExtractedConcept,
    paper: SearchResult
  ): 'foundational' | 'methodological' | 'application' {
    const titleLower = paper.title.toLowerCase();
    const abstractLower = (paper.abstract || '').toLowerCase();
    const text = `${titleLower} ${abstractLower}`;

    // 方法论相关
    const methodKeywords = ['method', 'algorithm', 'approach', 'technique', 'framework', 'architecture',
      '方法', '算法', '技术', '框架', '架构'];
    if (methodKeywords.some(k => text.includes(k))) {
      return 'methodological';
    }

    // 应用相关
    const appKeywords = ['application', 'applied', 'use case', 'deploy', 'system',
      '应用', '部署', '系统', '实践'];
    if (appKeywords.some(k => text.includes(k))) {
      return 'application';
    }

    return 'foundational';
  }
}


path: knowledge-graph/scripts/graph.ts
content: /**
 * Knowledge Graph Builder - Core Module
 * 知识图谱构建核心模块
 *
 * 功能：
 * - 从概念列表构建知识图谱
 * - 识别概念间关系
 * - 生成可视化输出
 */

import { createAIProvider, type AIProvider } from '../../shared/ai-provider';
import { extractJson } from '../../shared/utils';
import { ApiInitializationError, getErrorMessage } from '../../shared/errors';

/**
 * 文献引用信息
 */
export interface LiteratureReference {
  /** 论文ID */
  paperId: string;
  /** 论文标题 */
  title: string;
  /** 相关度 (0-1) */
  relevance: number;
  /** 引用类型 */
  mentionType: 'foundational' | 'methodological' | 'application';
  /** 引用上下文 */
  contexts: string[];
}

/**
 * 概念定义（可多来源）
 */
export interface ConceptDefinition {
  /** 定义内容 */
  text: string;
  /** 来源论文ID */
  sourceId: string;
  /** 来源论文标题 */
  sourceTitle: string;
}

/**
 * 论文元数据（用于索引）
 */
export interface PaperMetadata {
  /** 论文ID */
  id: string;
  /** 标题 */
  title: string;
  /** 作者 */
  authors: string[];
  /** 摘要 */
  abstract: string;
  /** 发表日期 */
  publishDate: string;
  /** URL */
  url: string;
  /** PDF URL */
  pdfUrl?: string;
  /** 引用数 */
  citations?: number;
  /** 论文类型 */
  paperType: 'review' | 'research';
  /** 关键词 */
  keywords: string[];
  /** 额外元数据 */
  metadata?: Record<string, unknown>;
}

/**
 * 文献索引（论文→概念映射）
 */
export interface LiteratureIndex {
  /** 论文ID → 论文元数据 */
  papers: Map<string, PaperMetadata>;
  /** 论文ID → 关联概念ID列表 */
  paperToConcepts: Map<string, string[]>;
}

/**
 * 概念索引（概念→论文映射）
 */
export interface ConceptIndex {
  /** 概念ID → 论文引用列表 */
  conceptToPapers: Map<string, LiteratureReference[]>;
}

/**
 * 图谱元数据
 */
export interface GraphMetadata {
  /** 图谱名称 */
  name: string;
  /** 创建时间 */
  createdAt: string;
  /** 更新时间 */
  updatedAt: string;
  /** 来源综述 */
  sourceReviews: string[];
  /** 统计 */
  totalConcepts: number;
  totalPapers: number;
  totalRelations: number;
}

export interface KnowledgeNode {
  id: string;
  label: string;
  category: 'foundation' | 'core' | 'advanced' | 'application';
  importance: number;
  description?: string;
  /** 文献引用列表 */
  literatureReferences?: LiteratureReference[];
  /** 多来源定义 */
  definitions?: ConceptDefinition[];
  /** 关键论文ID */
  keyPapers?: string[];
  /** 节点元数据 */
  metadata?: {
    extractedFrom?: string[];
    lastUpdated?: string;
    confidence?: number;
  };
}

export interface KnowledgeEdge {
  source: string;
  target: string;
  relation: 'prerequisite' | 'related' | 'derived' | 'component';
  label?: string;
}

export interface KnowledgeGraphData {
  nodes: KnowledgeNode[];
  edges: KnowledgeEdge[];
  clusters: Map<string, string[]>;
  /** 文献索引 */
  literatureIndex?: LiteratureIndex;
  /** 概念索引 */
  conceptIndex?: ConceptIndex;
  /** 图谱元数据 */
  graphMetadata?: GraphMetadata;
}

export default class KnowledgeGraphBuilder {
  private ai: AIProvider | null = null;

  async initialize(): Promise<void> {
    if (!this.ai) {
      try {
        this.ai = await createAIProvider();
      } catch (error) {
        throw new ApiInitializationError(
          `Failed to initialize AI provider: ${getErrorMessage(error)}`,
          error instanceof Error ? error : undefined
        );
      }
    }
  }

  /**
   * 从概念列表构建知识图谱
   */
  async build(concepts: string[]): Promise<KnowledgeGraphData> {
    await this.initialize();

    // 识别概念间关系
    const relations = await this.identifyRelations(concepts);

    // 构建节点
    const nodes: KnowledgeNode[] = concepts.map((concept, index) => ({
      id: `node_${index}`,
      label: concept,
      category: this.categorizeConcept(concept, relations),
      importance: this.calculateImportance(concept, relations)
    }));

    // 构建边
    const edges: KnowledgeEdge[] = relations.map(r => ({
      source: nodes.find(n => n.label === r.source)?.id || r.source,
      target: nodes.find(n => n.label === r.target)?.id || r.target,
      relation: r.relation,
      label: r.relation
    }));

    // 构建聚类
    const clusters = this.buildClusters(nodes, edges);

    return { nodes, edges, clusters };
  }

  /**
   * 识别概念间关系
   */
  private async identifyRelations(
    concepts: string[]
  ): Promise<Array<{ source: string; target: string; relation: string }>> {
    const prompt = `分析以下概念之间的关系:

概念列表: ${concepts.join(', ')}

请识别概念之间的依赖和关联关系。返回JSON格式:
{
  "relations": [
    {
      "source": "源概念",
      "target": "目标概念",
      "relation": "prerequisite|related|derived|component"
    }
  ]
}

说明:
- prerequisite: 前置关系，学习target需要先学习source
- related: 相关关系，两个概念有关联但无直接依赖
- derived: 衍生关系，target是从source发展而来
- component: 组成关系，target是source的组成部分`;

    const responseText = await this.ai!.chat([
      { role: 'system', content: '你是一位知识体系分析专家。' },
      { role: 'user', content: prompt }
    ], { temperature: 0.2 });

    const result = extractJson<{ relations?: Array<{ source: string; target: string; relation: string }> }>(responseText);

    if (result.success && result.data?.relations) {
      return result.data.relations;
    }

    console.error('Error parsing relations response:', result.error);
    return [];
  }

  /**
   * 对概念分类
   */
  private categorizeConcept(
    concept: string,
    relations: Array<{ source: string; target: string; relation: string }>
  ): 'foundation' | 'core' | 'advanced' | 'application' {
    // 基于关系数量判断类别
    const asSource = relations.filter(r => r.source === concept).length;
    const asTarget = relations.filter(r => r.target === concept).length;

    if (asSource > asTarget + 2) {
      return 'foundation';
    } else if (asTarget > asSource + 2) {
      return 'application';
    } else if (asSource + asTarget > 4) {
      return 'core';
    }
    return 'advanced';
  }

  /**
   * 计算概念重要性
   */
  private calculateImportance(
    concept: string,
    relations: Array<{ source: string; target: string; relation: string }>
  ): number {
    const degree = relations.filter(r => r.source === concept || r.target === concept).length;
    return Math.min(5, Math.max(1, degree));
  }

  /**
   * 构建聚类
   */
  private buildClusters(
    nodes: KnowledgeNode[],
    edges: KnowledgeEdge[]
  ): Map<string, string[]> {
    const clusters = new Map<string, string[]>();

    // 按类别聚类
    const categories = ['foundation', 'core', 'advanced', 'application'];
    for (const category of categories) {
      const clusterNodes = nodes.filter(n => n.category === category).map(n => n.id);
      if (clusterNodes.length > 0) {
        clusters.set(category, clusterNodes);
      }
    }

    return clusters;
  }

  /**
   * 发现学习路径
   */
  findPath(
    graph: KnowledgeGraphData,
    from: string,
    to: string
  ): string[] {
    const { nodes, edges } = graph;

    // BFS寻找路径
    const fromNode = nodes.find(n => n.label === from || n.id === from);
    const toNode = nodes.find(n => n.label === to || n.id === to);

    if (!fromNode || !toNode) return [];

    const visited = new Set<string>();
    const queue: Array<{ node: string; path: string[] }> = [
      { node: fromNode.id, path: [fromNode.label] }
    ];

    while (queue.length > 0) {
      const { node, path } = queue.shift()!;

      if (node === toNode.id) {
        return path;
      }

      if (visited.has(node)) continue;
      visited.add(node);

      // 找到相邻节点
      const neighbors = edges
        .filter(e => e.source === node)
        .map(e => e.target);

      for (const neighbor of neighbors) {
        if (!visited.has(neighbor)) {
          const neighborNode = nodes.find(n => n.id === neighbor);
          queue.push({
            node: neighbor,
            path: [...path, neighborNode?.label || neighbor]
          });
        }
      }
    }

    return [];
  }

  /**
   * 导出为Mermaid格式
   */
  toMermaid(graph: KnowledgeGraphData): string {
    const { nodes, edges } = graph;

    let mermaid = 'graph TD\n';

    // 添加节点
    for (const node of nodes) {
      const shape = this.getMermaidShape(node.category);
      mermaid += `    ${node.id}${shape.left}${node.label}${shape.right}\n`;
    }

    // 添加边
    for (const edge of edges) {
      const arrow = this.getMermaidArrow(edge.relation);
      mermaid += `    ${edge.source} ${arrow} ${edge.target}\n`;
    }

    // 添加样式
    mermaid += '\n    %% 样式定义\n';
    mermaid += '    classDef foundation fill:#e1f5fe,stroke:#01579b\n';
    mermaid += '    classDef core fill:#fff3e0,stroke:#e65100\n';
    mermaid += '    classDef advanced fill:#f3e5f5,stroke:#4a148c\n';
    mermaid += '    classDef application fill:#e8f5e9,stroke:#1b5e20\n';

    for (const node of nodes) {
      mermaid += `    class ${node.id} ${node.category}\n`;
    }

    return mermaid;
  }

  /**
   * 获取Mermaid节点形状
   */
  private getMermaidShape(category: string): { left: string; right: string } {
    switch (category) {
      case 'foundation':
        return { left: '[', right: ']' };
      case 'core':
        return { left: '((', right: '))' };
      case 'advanced':
        return { left: '{{', right: '}}' };
      case 'application':
        return { left: '[[', right: ']]' };
      default:
        return { left: '[', right: ']' };
    }
  }

  /**
   * 获取Mermaid箭头样式
   */
  private getMermaidArrow(relation: string): string {
    switch (relation) {
      case 'prerequisite':
        return '-->';
      case 'related':
        return '-.->';
      case 'derived':
        return '==>';
      case 'component':
        return '--o';
      default:
        return '-->';
    }
  }

  /**
   * 导出为JSON
   */
  toJSON(graph: KnowledgeGraphData): string {
    return JSON.stringify({
      nodes: graph.nodes,
      edges: graph.edges,
      clusters: Object.fromEntries(graph.clusters),
      literatureIndex: graph.literatureIndex ? {
        papers: Object.fromEntries(graph.literatureIndex.papers),
        paperToConcepts: Object.fromEntries(graph.literatureIndex.paperToConcepts)
      } : undefined,
      conceptIndex: graph.conceptIndex ? {
        conceptToPapers: Object.fromEntries(graph.conceptIndex.conceptToPapers)
      } : undefined,
      graphMetadata: graph.graphMetadata
    }, null, 2);
  }

  /**
   * 从JSON导入
   */
  fromJSON(json: string): KnowledgeGraphData {
    const data = JSON.parse(json);
    const result: KnowledgeGraphData = {
      nodes: data.nodes,
      edges: data.edges,
      clusters: new Map(Object.entries(data.clusters || {}))
    };

    if (data.literatureIndex) {
      result.literatureIndex = {
        papers: new Map(Object.entries(data.literatureIndex.papers || {})),
        paperToConcepts: new Map(Object.entries(data.literatureIndex.paperToConcepts || {}))
      };
    }

    if (data.conceptIndex) {
      result.conceptIndex = {
        conceptToPapers: new Map(Object.entries(data.conceptIndex.conceptToPapers || {}))
      };
    }

    if (data.graphMetadata) {
      result.graphMetadata = data.graphMetadata;
    }

    return result;
  }

  /**
   * 获取拓扑排序（学习顺序）
   */
  getTopologicalOrder(graph: KnowledgeGraphData): string[] {
    const { nodes, edges } = graph;
    const inDegree = new Map<string, number>();
    const adjacency = new Map<string, string[]>();

    // 初始化
    for (const node of nodes) {
      inDegree.set(node.id, 0);
      adjacency.set(node.id, []);
    }

    // 计算入度
    for (const edge of edges) {
      if (edge.relation === 'prerequisite' || edge.relation === 'component') {
        inDegree.set(edge.target, (inDegree.get(edge.target) || 0) + 1);
        adjacency.get(edge.source)?.push(edge.target);
      }
    }

    // Kahn算法
    const queue: string[] = [];
    const result: string[] = [];

    for (const [node, degree] of inDegree) {
      if (degree === 0) {
        queue.push(node);
      }
    }

    while (queue.length > 0) {
      const current = queue.shift()!;
      const nodeLabel = nodes.find(n => n.id === current)?.label || current;
      result.push(nodeLabel);

      for (const neighbor of adjacency.get(current) || []) {
        const newDegree = (inDegree.get(neighbor) || 0) - 1;
        inDegree.set(neighbor, newDegree);
        if (newDegree === 0) {
          queue.push(neighbor);
        }
      }
    }

    return result;
  }
}

// CLI 支持
if (import.meta.main) {
  const args = process.argv.slice(2);
  const concepts = args.filter(a => !a.startsWith('--'));
  const outputIndex = args.indexOf('--output');
  const outputFile = outputIndex > -1 ? args[outputIndex + 1] : null;
  const formatIndex = args.indexOf('--format');
  const format = formatIndex > -1 ? args[formatIndex + 1] : 'mermaid';

  if (concepts.length === 0) {
    console.error('Usage: graph.ts <concept1> <concept2> ... [--output <file>] [--format mermaid|json]');
    process.exit(1);
  }

  const builder = new KnowledgeGraphBuilder();

  builder.initialize().then(() => builder.build(concepts)).then(graph => {
    let output: string;

    if (format === 'json') {
      output = builder.toJSON(graph);
    } else {
      output = builder.toMermaid(graph);
    }

    if (outputFile) {
      const fs = require('fs');
      fs.writeFileSync(outputFile, output);
      console.log(`Graph saved to ${outputFile}`);
    } else {
      console.log(output);
    }
  }).catch(err => {
    console.error('Error:', getErrorMessage(err));
    process.exit(1);
  });
}


path: knowledge-graph/scripts/indexer.ts
content: /**
 * Knowledge Graph Indexer - 双向索引系统
 * 构建概念→文献和文献→概念的双向索引
 * 支持按相关度、引用数、日期排序
 */

import type {
  KnowledgeGraphData,
  KnowledgeNode,
  PaperMetadata,
  LiteratureReference,
  LiteratureIndex,
  ConceptIndex
} from './graph';

/**
 * 论文查询选项
 */
export interface PaperQueryOptions {
  minRelevance?: number;
  sortBy?: 'relevance' | 'citations' | 'date';
  limit?: number;
  paperType?: 'review' | 'research' | 'all';
}

/**
 * 概念查询选项
 */
export interface ConceptQueryOptions {
  minRelevance?: number;
  sortBy?: 'relevance' | 'importance';
  limit?: number;
}

/**
 * 索引统计信息
 */
export interface IndexStatistics {
  totalConcepts: number;
  totalPapers: number;
  totalIndexEntries: number;
  avgPapersPerConcept: number;
  avgConceptsPerPaper: number;
  papersByType: { review: number; research: number };
}

/**
 * 带相关度的论文结果
 */
export interface PaperWithRelevance {
  paper: PaperMetadata;
  relevance: number;
  mentionType: string;
}

/**
 * 带相关度的概念结果
 */
export interface ConceptWithRelevance {
  concept: KnowledgeNode;
  relevance: number;
  mentionType: string;
}

export default class KnowledgeGraphIndexer {

  /**
   * 为图谱构建双向索引
   */
  buildIndex(graph: KnowledgeGraphData): KnowledgeGraphData {
    if (!graph.literatureIndex) {
      graph.literatureIndex = {
        papers: new Map(),
        paperToConcepts: new Map()
      };
    }
    if (!graph.conceptIndex) {
      graph.conceptIndex = {
        conceptToPapers: new Map()
      };
    }

    // 从节点的literatureReferences构建索引
    for (const node of graph.nodes) {
      if (!node.literatureReferences) continue;

      for (const ref of node.literatureReferences) {
        // 更新概念→论文索引
        const existingRefs = graph.conceptIndex.conceptToPapers.get(node.id) || [];
        if (!existingRefs.find(r => r.paperId === ref.paperId)) {
          existingRefs.push(ref);
          graph.conceptIndex.conceptToPapers.set(node.id, existingRefs);
        }

        // 更新论文→概念索引
        const existingConcepts = graph.literatureIndex.paperToConcepts.get(ref.paperId) || [];
        if (!existingConcepts.includes(node.id)) {
          existingConcepts.push(node.id);
          graph.literatureIndex.paperToConcepts.set(ref.paperId, existingConcepts);
        }
      }
    }

    return graph;
  }

  /**
   * 添加论文到索引
   */
  addPaperToIndex(
    graph: KnowledgeGraphData,
    conceptId: string,
    paper: PaperMetadata,
    reference: LiteratureReference
  ): void {
    if (!graph.literatureIndex) {
      graph.literatureIndex = { papers: new Map(), paperToConcepts: new Map() };
    }
    if (!graph.conceptIndex) {
      graph.conceptIndex = { conceptToPapers: new Map() };
    }

    // 存储论文元数据
    graph.literatureIndex.papers.set(paper.id, paper);

    // 更新概念→论文索引
    const refs = graph.conceptIndex.conceptToPapers.get(conceptId) || [];
    if (!refs.find(r => r.paperId === paper.id)) {
      refs.push(reference);
      graph.conceptIndex.conceptToPapers.set(conceptId, refs);
    }

    // 更新论文→概念索引
    const concepts = graph.literatureIndex.paperToConcepts.get(paper.id) || [];
    if (!concepts.includes(conceptId)) {
      concepts.push(conceptId);
      graph.literatureIndex.paperToConcepts.set(paper.id, concepts);
    }

    // 更新节点的literatureReferences
    const node = graph.nodes.find(n => n.id === conceptId);
    if (node) {
      if (!node.literatureReferences) node.literatureReferences = [];
      if (!node.literatureReferences.find(r => r.paperId === paper.id)) {
        node.literatureReferences.push(reference);
      }
      if (!node.keyPapers) node.keyPapers = [];
      if (!node.keyPapers.includes(paper.id)) {
        node.keyPapers.push(paper.id);
      }
    }
  }

  /**
   * 按概念查找论文
   */
  findPapersByConcept(
    graph: KnowledgeGraphData,
    conceptId: string,
    options: PaperQueryOptions = {}
  ): PaperWithRelevance[] {
    const {
      minRelevance = 0,
      sortBy = 'relevance',
      limit = 10,
      paperType = 'all'
    } = options;

    if (!graph.conceptIndex || !graph.literatureIndex) return [];

    const refs = graph.conceptIndex.conceptToPapers.get(conceptId) || [];
    let results: PaperWithRelevance[] = [];

    for (const ref of refs) {
      if (ref.relevance < minRelevance) continue;

      const paper = graph.literatureIndex.papers.get(ref.paperId);
      if (!paper) continue;
      if (paperType !== 'all' && paper.paperType !== paperType) continue;

      results.push({
        paper,
        relevance: ref.relevance,
        mentionType: ref.mentionType
      });
    }

    // 排序
    switch (sortBy) {
      case 'relevance':
        results.sort((a, b) => b.relevance - a.relevance);
        break;
      case 'citations':
        results.sort((a, b) => (b.paper.citations || 0) - (a.paper.citations || 0));
        break;
      case 'date':
        results.sort((a, b) =>
          new Date(b.paper.publishDate).getTime() - new Date(a.paper.publishDate).getTime()
        );
        break;
    }

    return results.slice(0, limit);
  }

  /**
   * 按论文查找概念
   */
  findConceptsByPaper(
    graph: KnowledgeGraphData,
    paperId: string,
    options: ConceptQueryOptions = {}
  ): ConceptWithRelevance[] {
    const { minRelevance = 0, sortBy = 'relevance', limit = 20 } = options;

    if (!graph.literatureIndex || !graph.conceptIndex) return [];

    const conceptIds = graph.literatureIndex.paperToConcepts.get(paperId) || [];
    let results: ConceptWithRelevance[] = [];

    for (const conceptId of conceptIds) {
      const node = graph.nodes.find(n => n.id === conceptId);
      if (!node) continue;

      // 找到该论文在该概念下的引用信息
      const refs = graph.conceptIndex.conceptToPapers.get(conceptId) || [];
      const ref = refs.find(r => r.paperId === paperId);
      const relevance = ref?.relevance || 0;

      if (relevance < minRelevance) continue;

      results.push({
        concept: node,
        relevance,
        mentionType: ref?.mentionType || 'related'
      });
    }

    // 排序
    switch (sortBy) {
      case 'relevance':
        results.sort((a, b) => b.relevance - a.relevance);
        break;
      case 'importance':
        results.sort((a, b) => b.concept.importance - a.concept.importance);
        break;
    }

    return results.slice(0, limit);
  }

  /**
   * 推荐论文（基于多个概念）
   */
  recommendPapers(
    graph: KnowledgeGraphData,
    conceptIds: string[],
    limit: number = 10
  ): PaperWithRelevance[] {
    if (!graph.conceptIndex || !graph.literatureIndex) return [];

    // 收集所有相关论文及其累计相关度
    const paperScores = new Map<string, { totalRelevance: number; matchedConcepts: number; mentionType: string }>();

    for (const conceptId of conceptIds) {
      const refs = graph.conceptIndex.conceptToPapers.get(conceptId) || [];
      for (const ref of refs) {
        const existing = paperScores.get(ref.paperId) || { totalRelevance: 0, matchedConcepts: 0, mentionType: ref.mentionType };
        existing.totalRelevance += ref.relevance;
        existing.matchedConcepts++;
        paperScores.set(ref.paperId, existing);
      }
    }

    // 构建结果，按综合评分排序
    const results: PaperWithRelevance[] = [];
    for (const [paperId, score] of paperScores) {
      const paper = graph.literatureIndex.papers.get(paperId);
      if (!paper) continue;

      // 综合评分 = 平均相关度 * 匹配概念比例
      const avgRelevance = score.totalRelevance / score.matchedConcepts;
      const coverageBonus = score.matchedConcepts / conceptIds.length;
      const compositeScore = avgRelevance * 0.6 + coverageBonus * 0.4;

      results.push({
        paper,
        relevance: Math.round(compositeScore * 100) / 100,
        mentionType: score.mentionType
      });
    }

    results.sort((a, b) => b.relevance - a.relevance);
    return results.slice(0, limit);
  }

  /**
   * 获取索引统计信息
   */
  getIndexStats(graph: KnowledgeGraphData): IndexStatistics {
    const totalConcepts = graph.nodes.length;
    let totalPapers = 0;
    let totalIndexEntries = 0;
    let reviewCount = 0;
    let researchCount = 0;

    if (graph.literatureIndex) {
      totalPapers = graph.literatureIndex.papers.size;
      for (const paper of graph.literatureIndex.papers.values()) {
        if (paper.paperType === 'review') reviewCount++;
        else researchCount++;
      }
      for (const concepts of graph.literatureIndex.paperToConcepts.values()) {
        totalIndexEntries += concepts.length;
      }
    }

    return {
      totalConcepts,
      totalPapers,
      totalIndexEntries,
      avgPapersPerConcept: totalConcepts > 0
        ? Math.round((totalIndexEntries / totalConcepts) * 10) / 10
        : 0,
      avgConceptsPerPaper: totalPapers > 0
        ? Math.round((totalIndexEntries / totalPapers) * 10) / 10
        : 0,
      papersByType: { review: reviewCount, research: researchCount }
    };
  }

  /**
   * 格式化统计信息
   */
  formatStats(stats: IndexStatistics): string {
    return `📊 图谱统计信息:
  概念数: ${stats.totalConcepts}
  文献数: ${stats.totalPapers} (综述: ${stats.papersByType.review}, 研究: ${stats.papersByType.research})
  索引条目: ${stats.totalIndexEntries}
  平均每概念关联论文: ${stats.avgPapersPerConcept}
  平均每论文关联概念: ${stats.avgConceptsPerPaper}`;
  }

  /**
   * 格式化概念查询结果
   */
  formatPaperResults(conceptLabel: string, results: PaperWithRelevance[]): string {
    const reviews = results.filter(r => r.paper.paperType === 'review');
    const research = results.filter(r => r.paper.paperType === 'research');

    const lines: string[] = [`🔍 "${conceptLabel}" 相关文献:\n`];

    if (reviews.length > 0) {
      lines.push(`📚 综述文献 (${reviews.length}篇):`);
      reviews.forEach((r, i) => {
        lines.push(`  ${i + 1}. ${r.paper.title} (相关度: ${(r.relevance * 100).toFixed(0)}%)`);
        lines.push(`     ${r.paper.url}`);
      });
      lines.push('');
    }

    if (research.length > 0) {
      lines.push(`📄 关键论文 (${research.length}篇):`);
      research.forEach((r, i) => {
        lines.push(`  ${i + 1}. ${r.paper.title} (相关度: ${(r.relevance * 100).toFixed(0)}%)`);
        if (r.paper.citations) {
          lines.push(`     引用: ${r.paper.citations} | ${r.paper.url}`);
        } else {
          lines.push(`     ${r.paper.url}`);
        }
      });
    }

    if (results.length === 0) {
      lines.push('  未找到相关文献');
    }

    return lines.join('\n');
  }
}


path: knowledge-graph/scripts/storage.ts
content: /**
 * Graph Storage - SQLite 持久化存储
 * 使用 bun:sqlite 实现知识图谱的持久化
 */

import { Database } from 'bun:sqlite';
import type {
  KnowledgeGraphData,
  KnowledgeNode,
  KnowledgeEdge,
  PaperMetadata,
  LiteratureReference,
  LiteratureIndex,
  ConceptIndex,
  GraphMetadata
} from './graph';

/** 数据库目录路径 */
const DEFAULT_DB_DIR = './data';
const DEFAULT_DB_NAME = 'knowledge-graphs.db';

export default class GraphStorage {
  private db: Database;

  constructor(dbPath?: string) {
    const path = dbPath || `${DEFAULT_DB_DIR}/${DEFAULT_DB_NAME}`;

    // 确保目录存在
    const fs = require('fs');
    const dir = path.substring(0, path.lastIndexOf('/'));
    if (dir && !fs.existsSync(dir)) {
      fs.mkdirSync(dir, { recursive: true });
    }

    this.db = new Database(path);
    this.db.exec('PRAGMA journal_mode = WAL');
    this.db.exec('PRAGMA foreign_keys = ON');
    this.initSchema();
  }

  /**
   * 初始化数据库表结构
   */
  private initSchema(): void {
    this.db.exec(`
      CREATE TABLE IF NOT EXISTS graphs (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT UNIQUE NOT NULL,
        created_at TEXT NOT NULL,
        updated_at TEXT NOT NULL,
        source_reviews TEXT,
        total_concepts INTEGER DEFAULT 0,
        total_papers INTEGER DEFAULT 0,
        total_relations INTEGER DEFAULT 0
      );

      CREATE TABLE IF NOT EXISTS nodes (
        id TEXT NOT NULL,
        graph_id INTEGER NOT NULL,
        label TEXT NOT NULL,
        category TEXT NOT NULL,
        importance INTEGER DEFAULT 1,
        description TEXT,
        definitions TEXT,
        key_papers TEXT,
        metadata TEXT,
        PRIMARY KEY (id, graph_id),
        FOREIGN KEY (graph_id) REFERENCES graphs(id) ON DELETE CASCADE
      );

      CREATE TABLE IF NOT EXISTS edges (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        graph_id INTEGER NOT NULL,
        source TEXT NOT NULL,
        target TEXT NOT NULL,
        relation TEXT NOT NULL,
        label TEXT,
        FOREIGN KEY (graph_id) REFERENCES graphs(id) ON DELETE CASCADE
      );

      CREATE TABLE IF NOT EXISTS papers (
        id TEXT NOT NULL,
        graph_id INTEGER NOT NULL,
        title TEXT NOT NULL,
        authors TEXT,
        abstract TEXT,
        publish_date TEXT,
        url TEXT,
        pdf_url TEXT,
        citations INTEGER,
        paper_type TEXT,
        keywords TEXT,
        metadata TEXT,
        PRIMARY KEY (id, graph_id),
        FOREIGN KEY (graph_id) REFERENCES graphs(id) ON DELETE CASCADE
      );

      CREATE TABLE IF NOT EXISTS concept_paper_index (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        graph_id INTEGER NOT NULL,
        concept_id TEXT NOT NULL,
        paper_id TEXT NOT NULL,
        relevance REAL NOT NULL,
        mention_type TEXT,
        mention_contexts TEXT,
        indexed_at TEXT NOT NULL,
        FOREIGN KEY (graph_id) REFERENCES graphs(id) ON DELETE CASCADE
      );

      CREATE INDEX IF NOT EXISTS idx_concept_paper ON concept_paper_index(graph_id, concept_id, relevance DESC);
      CREATE INDEX IF NOT EXISTS idx_paper_concept ON concept_paper_index(graph_id, paper_id);
      CREATE INDEX IF NOT EXISTS idx_nodes_graph ON nodes(graph_id);
      CREATE INDEX IF NOT EXISTS idx_edges_graph ON edges(graph_id);
      CREATE INDEX IF NOT EXISTS idx_papers_graph ON papers(graph_id);
    `);
  }

  /**
   * 保存图谱
   */
  saveGraph(graph: KnowledgeGraphData, name: string): void {
    const now = new Date().toISOString();
    const sourceReviews = graph.graphMetadata?.sourceReviews || [];

    const saveTransaction = this.db.transaction(() => {
      // 删除旧图谱（如果存在）
      const existing = this.db.query('SELECT id FROM graphs WHERE name = ?').get(name) as { id: number } | null;
      if (existing) {
        this.db.run('DELETE FROM graphs WHERE id = ?', [existing.id]);
      }

      // 插入图谱元数据
      this.db.run(
        `INSERT INTO graphs (name, created_at, updated_at, source_reviews, total_concepts, total_papers, total_relations)
         VALUES (?, ?, ?, ?, ?, ?, ?)`,
        [
          name,
          graph.graphMetadata?.createdAt || now,
          now,
          JSON.stringify(sourceReviews),
          graph.nodes.length,
          graph.literatureIndex?.papers.size || 0,
          graph.edges.length
        ]
      );

      const graphId = (this.db.query('SELECT last_insert_rowid() as id').get() as { id: number }).id;

      // 插入节点
      const insertNode = this.db.prepare(
        `INSERT INTO nodes (id, graph_id, label, category, importance, description, definitions, key_papers, metadata)
         VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
      );
      for (const node of graph.nodes) {
        insertNode.run(
          node.id,
          graphId,
          node.label,
          node.category,
          node.importance,
          node.description || null,
          node.definitions ? JSON.stringify(node.definitions) : null,
          node.keyPapers ? JSON.stringify(node.keyPapers) : null,
          node.metadata ? JSON.stringify(node.metadata) : null
        );
      }

      // 插入边
      const insertEdge = this.db.prepare(
        `INSERT INTO edges (graph_id, source, target, relation, label)
         VALUES (?, ?, ?, ?, ?)`
      );
      for (const edge of graph.edges) {
        insertEdge.run(graphId, edge.source, edge.target, edge.relation, edge.label || null);
      }

      // 插入文献
      if (graph.literatureIndex) {
        const insertPaper = this.db.prepare(
          `INSERT INTO papers (id, graph_id, title, authors, abstract, publish_date, url, pdf_url, citations, paper_type, keywords, metadata)
           VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
        );
        for (const [paperId, paper] of graph.literatureIndex.papers) {
          insertPaper.run(
            paperId,
            graphId,
            paper.title,
            JSON.stringify(paper.authors),
            paper.abstract || null,
            paper.publishDate || null,
            paper.url || null,
            paper.pdfUrl || null,
            paper.citations || null,
            paper.paperType || null,
            JSON.stringify(paper.keywords || []),
            paper.metadata ? JSON.stringify(paper.metadata) : null
          );
        }
      }

      // 插入索引
      if (graph.conceptIndex) {
        const insertIndex = this.db.prepare(
          `INSERT INTO concept_paper_index (graph_id, concept_id, paper_id, relevance, mention_type, mention_contexts, indexed_at)
           VALUES (?, ?, ?, ?, ?, ?, ?)`
        );
        for (const [conceptId, refs] of graph.conceptIndex.conceptToPapers) {
          for (const ref of refs) {
            insertIndex.run(
              graphId,
              conceptId,
              ref.paperId,
              ref.relevance,
              ref.mentionType,
              JSON.stringify(ref.contexts || []),
              now
            );
          }
        }
      }
    });

    saveTransaction();
  }

  /**
   * 加载图谱
   */
  loadGraph(name: string): KnowledgeGraphData | null {
    const graphRow = this.db.query('SELECT * FROM graphs WHERE name = ?').get(name) as Record<string, any> | null;
    if (!graphRow) return null;

    const graphId = graphRow.id;

    // 加载节点
    const nodeRows = this.db.query('SELECT * FROM nodes WHERE graph_id = ?').all(graphId) as Record<string, any>[];
    const nodes: KnowledgeNode[] = nodeRows.map(row => ({
      id: row.id,
      label: row.label,
      category: row.category,
      importance: row.importance,
      description: row.description || undefined,
      definitions: row.definitions ? JSON.parse(row.definitions) : undefined,
      keyPapers: row.key_papers ? JSON.parse(row.key_papers) : undefined,
      metadata: row.metadata ? JSON.parse(row.metadata) : undefined,
      literatureReferences: []
    }));

    // 加载边
    const edgeRows = this.db.query('SELECT * FROM edges WHERE graph_id = ?').all(graphId) as Record<string, any>[];
    const edges: KnowledgeEdge[] = edgeRows.map(row => ({
      source: row.source,
      target: row.target,
      relation: row.relation,
      label: row.label || undefined
    }));

    // 加载文献
    const paperRows = this.db.query('SELECT * FROM papers WHERE graph_id = ?').all(graphId) as Record<string, any>[];
    const papers = new Map<string, PaperMetadata>();
    for (const row of paperRows) {
      papers.set(row.id, {
        id: row.id,
        title: row.title,
        authors: row.authors ? JSON.parse(row.authors) : [],
        abstract: row.abstract || '',
        publishDate: row.publish_date || '',
        url: row.url || '',
        pdfUrl: row.pdf_url || undefined,
        citations: row.citations || undefined,
        paperType: row.paper_type || 'research',
        keywords: row.keywords ? JSON.parse(row.keywords) : [],
        metadata: row.metadata ? JSON.parse(row.metadata) : undefined
      });
    }

    // 加载索引
    const indexRows = this.db.query('SELECT * FROM concept_paper_index WHERE graph_id = ?').all(graphId) as Record<string, any>[];

    const conceptToPapers = new Map<string, LiteratureReference[]>();
    const paperToConcepts = new Map<string, string[]>();

    for (const row of indexRows) {
      // 概念→论文
      const refs = conceptToPapers.get(row.concept_id) || [];
      refs.push({
        paperId: row.paper_id,
        title: papers.get(row.paper_id)?.title || '',
        relevance: row.relevance,
        mentionType: row.mention_type || 'foundational',
        contexts: row.mention_contexts ? JSON.parse(row.mention_contexts) : []
      });
      conceptToPapers.set(row.concept_id, refs);

      // 论文→概念
      const concepts = paperToConcepts.get(row.paper_id) || [];
      if (!concepts.includes(row.concept_id)) {
        concepts.push(row.concept_id);
        paperToConcepts.set(row.paper_id, concepts);
      }
    }

    // 设置节点的 literatureReferences
    for (const node of nodes) {
      node.literatureReferences = conceptToPapers.get(node.id) || [];
    }

    // 构建聚类
    const clusters = new Map<string, string[]>();
    for (const category of ['foundation', 'core', 'advanced', 'application']) {
      const categoryNodes = nodes.filter(n => n.category === category).map(n => n.id);
      if (categoryNodes.length > 0) {
        clusters.set(category, categoryNodes);
      }
    }

    return {
      nodes,
      edges,
      clusters,
      literatureIndex: { papers, paperToConcepts },
      conceptIndex: { conceptToPapers },
      graphMetadata: {
        name,
        createdAt: graphRow.created_at,
        updatedAt: graphRow.updated_at,
        sourceReviews: graphRow.source_reviews ? JSON.parse(graphRow.source_reviews) : [],
        totalConcepts: graphRow.total_concepts,
        totalPapers: graphRow.total_papers,
        totalRelations: graphRow.total_relations
      }
    };
  }

  /**
   * 列出所有图谱
   */
  listGraphs(): GraphMetadata[] {
    const rows = this.db.query('SELECT * FROM graphs ORDER BY updated_at DESC').all() as Record<string, any>[];
    return rows.map(row => ({
      name: row.name,
      createdAt: row.created_at,
      updatedAt: row.updated_at,
      sourceReviews: row.source_reviews ? JSON.parse(row.source_reviews) : [],
      totalConcepts: row.total_concepts,
      totalPapers: row.total_papers,
      totalRelations: row.total_relations
    }));
  }

  /**
   * 删除图谱
   */
  deleteGraph(name: string): boolean {
    const result = this.db.run('DELETE FROM graphs WHERE name = ?', [name]);
    return result.changes > 0;
  }

  /**
   * 检查图谱是否存在
   */
  graphExists(name: string): boolean {
    const row = this.db.query('SELECT 1 FROM graphs WHERE name = ?').get(name);
    return row !== null;
  }

  /**
   * 通过SQL查询概念关联的论文（高性能）
   */
  queryPapersByConcept(
    graphName: string,
    conceptLabel: string,
    options: { minRelevance?: number; limit?: number; paperType?: string; sortBy?: string } = {}
  ): Array<PaperMetadata & { relevance: number; mentionType: string }> {
    const { minRelevance = 0, limit = 10, paperType, sortBy = 'relevance' } = options;

    let orderClause: string;
    switch (sortBy) {
      case 'citations': orderClause = 'p.citations DESC'; break;
      case 'date': orderClause = 'p.publish_date DESC'; break;
      default: orderClause = 'cpi.relevance DESC'; break;
    }

    let query = `
      SELECT p.*, cpi.relevance, cpi.mention_type
      FROM papers p
      JOIN concept_paper_index cpi ON p.id = cpi.paper_id AND p.graph_id = cpi.graph_id
      JOIN nodes n ON cpi.concept_id = n.id AND cpi.graph_id = n.graph_id
      JOIN graphs g ON g.id = p.graph_id
      WHERE g.name = ? AND n.label = ? AND cpi.relevance >= ?
    `;

    const params: any[] = [graphName, conceptLabel, minRelevance];

    if (paperType && paperType !== 'all') {
      query += ' AND p.paper_type = ?';
      params.push(paperType);
    }

    query += ` ORDER BY ${orderClause} LIMIT ?`;
    params.push(limit);

    const rows = this.db.query(query).all(...params) as Record<string, any>[];

    return rows.map(row => ({
      id: row.id,
      title: row.title,
      authors: row.authors ? JSON.parse(row.authors) : [],
      abstract: row.abstract || '',
      publishDate: row.publish_date || '',
      url: row.url || '',
      pdfUrl: row.pdf_url || undefined,
      citations: row.citations || undefined,
      paperType: row.paper_type || 'research',
      keywords: row.keywords ? JSON.parse(row.keywords) : [],
      relevance: row.relevance,
      mentionType: row.mention_type
    }));
  }

  /**
   * 通过SQL查询论文关联的概念（高性能）
   */
  queryConceptsByPaper(
    graphName: string,
    paperUrl: string
  ): Array<{ id: string; label: string; category: string; importance: number; relevance: number }> {
    const rows = this.db.query(`
      SELECT n.id, n.label, n.category, n.importance, cpi.relevance
      FROM nodes n
      JOIN concept_paper_index cpi ON n.id = cpi.concept_id AND n.graph_id = cpi.graph_id
      JOIN papers p ON cpi.paper_id = p.id AND cpi.graph_id = p.graph_id
      JOIN graphs g ON g.id = n.graph_id
      WHERE g.name = ? AND p.url = ?
      ORDER BY cpi.relevance DESC
    `).all(graphName, paperUrl) as Record<string, any>[];

    return rows.map(row => ({
      id: row.id,
      label: row.label,
      category: row.category,
      importance: row.importance,
      relevance: row.relevance
    }));
  }

  /**
   * 关闭数据库
   */
  close(): void {
    this.db.close();
  }
}


path: literature-search/scripts/adapters/arxiv-adapter.ts
content: /**
 * arXiv Adapter - arXiv API search
 */

import { AbstractSearchAdapter, type AdapterMeta } from './base';
import type { SearchResult } from '../types';
import { parseArxivXml } from '../../../shared/utils';
import { getErrorMessage } from '../../../shared/errors';

const ARXIV_API_URL = 'http://export.arxiv.org/api/query';

export class ArxivAdapter extends AbstractSearchAdapter {
  readonly meta: AdapterMeta = {
    name: 'arXiv',
    sourceId: 'arxiv',
    tier: 'free',
    domains: ['physics', 'cs', 'general'],
    rateLimit: { requests: 3, windowMs: 1000 },
    description: 'Open access preprint repository for physics, mathematics, CS, and more'
  };

  isAvailable(): boolean {
    return true; // No API key required
  }

  async search(query: string, limit: number): Promise<SearchResult[]> {
    try {
      const searchQuery = encodeURIComponent(`all:${query}`);
      const url = `${ARXIV_API_URL}?search_query=${searchQuery}&max_results=${limit}&sortBy=relevance`;

      const response = await this.rateLimitedFetch(url, undefined, { maxRetries: 3 });
      const text = await response.text();

      const entries = parseArxivXml(text);
      return entries.map(entry => ({
        id: entry.id,
        title: entry.title,
        authors: entry.authors,
        abstract: entry.abstract,
        publishDate: entry.published,
        source: 'arxiv',
        url: `https://arxiv.org/abs/${entry.id}`,
        pdfUrl: entry.pdfUrl,
        keywords: entry.categories,
        openAccess: true
      }));
    } catch (error) {
      console.error('arXiv search error:', getErrorMessage(error));
      return [];
    }
  }
}


path: literature-search/scripts/adapters/base.ts
content: /**
 * Search Source Adapter - Base Interface & Abstract Class
 * 搜索源适配器基类
 */

import type { SearchResult } from '../types';
import type { SearchSource } from '../../../shared/types';
import { fetchWithRetry } from '../../../shared/utils';
import type { RetryOptions } from '../../../shared/types';

/** Source tier classification */
export type SourceTier = 'free' | 'freemium' | 'paid';

/** Academic domain coverage */
export type SourceDomain = 'general' | 'biomedical' | 'cs' | 'engineering' | 'physics' | 'multidisciplinary';

/** Adapter metadata */
export interface AdapterMeta {
  name: string;
  sourceId: SearchSource;
  tier: SourceTier;
  domains: SourceDomain[];
  rateLimit: { requests: number; windowMs: number };
  description: string;
}

/**
 * SearchSourceAdapter interface - contract for all search adapters
 */
export interface SearchSourceAdapter {
  readonly meta: AdapterMeta;

  /** Check if this adapter is available (API keys present, etc.) */
  isAvailable(): boolean;

  /** Execute a search query */
  search(query: string, limit: number): Promise<SearchResult[]>;

  /** Optional: resolve PDF URL for a given result */
  getPdfUrl?(result: SearchResult): Promise<string | undefined>;
}

/**
 * AbstractSearchAdapter - provides common functionality
 */
export abstract class AbstractSearchAdapter implements SearchSourceAdapter {
  abstract readonly meta: AdapterMeta;

  abstract isAvailable(): boolean;
  abstract search(query: string, limit: number): Promise<SearchResult[]>;

  private lastRequestTime = 0;

  /**
   * Rate-limited fetch wrapper
   */
  protected async rateLimitedFetch(
    url: string,
    init?: RequestInit,
    retryOpts?: RetryOptions
  ): Promise<Response> {
    const { requests, windowMs } = this.meta.rateLimit;
    const minInterval = windowMs / requests;

    const now = Date.now();
    const elapsed = now - this.lastRequestTime;
    if (elapsed < minInterval) {
      await new Promise(resolve => setTimeout(resolve, minInterval - elapsed));
    }

    this.lastRequestTime = Date.now();
    return fetchWithRetry(url, init, retryOpts ?? { maxRetries: 2 });
  }
}


path: literature-search/scripts/adapters/core-adapter.ts
content: /**
 * CORE Adapter - Open access full text
 * Requires API key for access
 */

import { AbstractSearchAdapter, type AdapterMeta } from './base';
import type { SearchResult } from '../types';
import { getErrorMessage } from '../../../shared/errors';

const CORE_API_URL = 'https://api.core.ac.uk/v3/search/works';

export class CoreAdapter extends AbstractSearchAdapter {
  readonly meta: AdapterMeta = {
    name: 'CORE',
    sourceId: 'core',
    tier: 'freemium',
    domains: ['general', 'multidisciplinary'],
    rateLimit: { requests: 1, windowMs: 10000 }, // 1 req/10s on free tier
    description: 'Open access aggregator with full text from 10K+ repositories'
  };

  private apiKey?: string;

  constructor(apiKey?: string) {
    super();
    this.[REDACTED_SECRET] || process.env.CORE_API_KEY;
  }

  isAvailable(): boolean {
    return !!this.apiKey;
  }

  async search(query: string, limit: number): Promise<SearchResult[]> {
    if (!this.apiKey) return [];

    try {
      const url = `${CORE_API_URL}?q=${encodeURIComponent(query)}&limit=${limit}`;

      const response = await this.rateLimitedFetch(url, {
        headers: {
          'Authorization': `Bearer ${this.apiKey}`
        }
      });
      const data = await response.json() as {
        results?: Array<{
          id?: string;
          title?: string;
          authors?: Array<{ name?: string }>;
          abstract?: string;
          yearPublished?: number;
          doi?: string;
          downloadUrl?: string;
          sourceFulltextUrls?: string[];
          citationCount?: number;
          journals?: Array<{ title?: string }>;
        }>;
      };

      if (!data.results) return [];

      return data.results.map(work => ({
        id: `core:${work.id || ''}`,
        title: work.title || '',
        authors: work.authors?.map(a => a.name || '').filter(Boolean) || [],
        abstract: work.abstract || '',
        publishDate: work.yearPublished?.toString() || '',
        source: 'core',
        url: work.doi ? `https://doi.org/${work.doi}` : (work.sourceFulltextUrls?.[0] || ''),
        doi: work.doi,
        pdfUrl: work.downloadUrl,
        citations: work.citationCount,
        journal: work.journals?.[0]?.title,
        openAccess: true
      }));
    } catch (error) {
      console.error('CORE search error:', getErrorMessage(error));
      return [];
    }
  }

  async getPdfUrl(result: SearchResult): Promise<string | undefined> {
    if (!this.apiKey || !result.doi) return undefined;
    // CORE already provides download URLs in search results
    return result.pdfUrl;
  }
}


path: literature-search/scripts/adapters/crossref-adapter.ts
content: /**
 * CrossRef Adapter - DOI registration and metadata
 * 150M+ records, excellent for DOI resolution
 */

import { AbstractSearchAdapter, type AdapterMeta } from './base';
import type { SearchResult, CrossRefWork } from '../types';
import { getErrorMessage } from '../../../shared/errors';

const CROSSREF_API_URL = 'https://api.crossref.org/works';

export class CrossRefAdapter extends AbstractSearchAdapter {
  readonly meta: AdapterMeta = {
    name: 'CrossRef',
    sourceId: 'crossref',
    tier: 'free',
    domains: ['general', 'multidisciplinary'],
    rateLimit: { requests: 5, windowMs: 1000 },
    description: 'Official DOI registration agency with 150M+ metadata records'
  };

  private mailto?: string;

  constructor(mailto?: string) {
    super();
    this.mailto = mailto || process.env.CROSSREF_MAILTO;
  }

  isAvailable(): boolean {
    return true; // No API key required
  }

  async search(query: string, limit: number): Promise<SearchResult[]> {
    try {
      const url = `${CROSSREF_API_URL}?query=${encodeURIComponent(query)}&rows=${limit}&sort=relevance`;

      const headers: Record<string, string> = {};
      if (this.mailto) {
        headers['User-Agent'] = `ScholarGraph/1.0 (mailto:${this.mailto})`;
      }

      const response = await this.rateLimitedFetch(url, { headers });
      const data = await response.json() as {
        message?: { items?: CrossRefWork[] };
      };

      if (!data.message?.items) return [];

      return data.message.items.map((work) => {
        const authors = work.author?.map(a => {
          const parts = [a.given, a.family].filter(Boolean);
          return parts.join(' ');
        }) || [];

        const dateParts = work['published-print']?.['date-parts']?.[0]
          || work['published-online']?.['date-parts']?.[0];
        const publishDate = dateParts
          ? dateParts.filter(Boolean).join('-')
          : '';

        // Find PDF link if available
        const pdfLink = work.link?.find(l => l['content-type']?.includes('pdf'));

        return {
          id: `doi:${work.DOI}`,
          title: work.title?.[0] || '',
          authors,
          abstract: work.abstract?.replace(/<[^>]*>/g, '') || '', // Strip JATS XML tags
          publishDate,
          source: 'crossref',
          url: work.URL || `https://doi.org/${work.DOI}`,
          doi: work.DOI,
          citations: work['is-referenced-by-count'],
          venue: work['container-title']?.[0],
          journal: work['container-title']?.[0],
          pdfUrl: pdfLink?.URL
        };
      });
    } catch (error) {
      console.error('CrossRef search error:', getErrorMessage(error));
      return [];
    }
  }
}


path: literature-search/scripts/adapters/dblp-adapter.ts
content: /**
 * DBLP Adapter - Computer science bibliography
 */

import { AbstractSearchAdapter, type AdapterMeta } from './base';
import type { SearchResult, DblpHit } from '../types';
import { getErrorMessage } from '../../../shared/errors';

const DBLP_API_URL = 'https://dblp.org/search/publ/api';

export class DblpAdapter extends AbstractSearchAdapter {
  readonly meta: AdapterMeta = {
    name: 'DBLP',
    sourceId: 'dblp',
    tier: 'free',
    domains: ['cs'],
    rateLimit: { requests: 1, windowMs: 1000 },
    description: 'Computer science bibliography with comprehensive CS publication coverage'
  };

  isAvailable(): boolean {
    return true; // No API key required
  }

  async search(query: string, limit: number): Promise<SearchResult[]> {
    try {
      const url = `${DBLP_API_URL}?q=${encodeURIComponent(query)}&format=json&h=${limit}`;

      const response = await this.rateLimitedFetch(url);
      const data = await response.json() as {
        result?: { hits?: { hit?: DblpHit[] } };
      };

      const hits = data.result?.hits?.hit;
      if (!hits) return [];

      return hits.map((hit, index) => {
        const info = hit.info;

        // Parse authors - can be array or single object
        let authors: string[] = [];
        if (info.authors?.author) {
          const authorData = info.authors.author;
          if (Array.isArray(authorData)) {
            authors = authorData.map(a => a.text);
          } else {
            authors = [authorData.text];
          }
        }

        return {
          id: `dblp_${index}`,
          title: info.title || '',
          authors,
          abstract: '', // DBLP doesn't provide abstracts
          publishDate: info.year || '',
          source: 'dblp',
          url: info.ee || info.url || '',
          doi: info.doi,
          venue: info.venue
        };
      });
    } catch (error) {
      console.error('DBLP search error:', getErrorMessage(error));
      return [];
    }
  }
}


path: literature-search/scripts/adapters/google-scholar-adapter.ts
content: /**
 * Google Scholar Adapter (via SerpAPI)
 * Requires SERPAPI_KEY
 */

import { AbstractSearchAdapter, type AdapterMeta } from './base';
import type { SearchResult } from '../types';
import { getErrorMessage } from '../../../shared/errors';

const SERPAPI_URL = 'https://serpapi.com/search';

export class GoogleScholarAdapter extends AbstractSearchAdapter {
  readonly meta: AdapterMeta = {
    name: 'Google Scholar',
    sourceId: 'google_scholar',
    tier: 'paid',
    domains: ['general', 'multidisciplinary', 'cs', 'biomedical', 'engineering', 'physics'],
    rateLimit: { requests: 1, windowMs: 2000 },
    description: 'Google Scholar search via SerpAPI proxy'
  };

  private apiKey?: string;

  constructor(apiKey?: string) {
    super();
    this.[REDACTED_SECRET] || process.env.SERPAPI_KEY;
  }

  isAvailable(): boolean {
    return !!this.apiKey;
  }

  async search(query: string, limit: number): Promise<SearchResult[]> {
    if (!this.apiKey) return [];

    try {
      const url = `${SERPAPI_URL}?engine=google_scholar&q=${encodeURIComponent(query)}&num=${limit}&[REDACTED_SECRET]`;

      const response = await this.rateLimitedFetch(url);
      const data = await response.json() as {
        organic_results?: Array<{
          position?: number;
          title?: string;
          result_id?: string;
          link?: string;
          snippet?: string;
          publication_info?: {
            summary?: string;
            authors?: Array<{ name: string }>;
          };
          inline_links?: {
            cited_by?: { total?: number };
          };
          resources?: Array<{
            title?: string;
            file_format?: string;
            link?: string;
          }>;
        }>;
      };

      if (!data.organic_results) return [];

      return data.organic_results.map((result, index) => {
        const pdfResource = result.resources?.find(r =>
          r.file_format?.toLowerCase() === 'pdf' || r.link?.endsWith('.pdf')
        );

        const authors = result.publication_info?.authors?.map(a => a.name) || [];
        // Try to extract year from publication summary
        const yearMatch = result.publication_info?.summary?.match(/\b(19|20)\d{2}\b/);

        return {
          id: result.result_id || `gs_${index}`,
          title: result.title || '',
          authors,
          abstract: result.snippet || '',
          publishDate: yearMatch ? yearMatch[0] : '',
          source: 'google_scholar',
          url: result.link || '',
          pdfUrl: pdfResource?.link,
          citations: result.inline_links?.cited_by?.total,
          snippet: result.snippet
        };
      });
    } catch (error) {
      console.error('Google Scholar search error:', getErrorMessage(error));
      return [];
    }
  }
}


path: literature-search/scripts/adapters/ieee-adapter.ts
content: /**
 * IEEE Xplore Adapter
 * Engineering/EE/CS papers, requires API key
 */

import { AbstractSearchAdapter, type AdapterMeta } from './base';
import type { SearchResult } from '../types';
import { getErrorMessage } from '../../../shared/errors';

const IEEE_API_URL = 'https://ieeexploreapi.ieee.org/api/v1/search/articles';

export class IeeeAdapter extends AbstractSearchAdapter {
  readonly meta: AdapterMeta = {
    name: 'IEEE Xplore',
    sourceId: 'ieee',
    tier: 'freemium',
    domains: ['engineering', 'cs'],
    rateLimit: { requests: 3, windowMs: 1000 },
    description: 'IEEE digital library for engineering, CS, and EE research'
  };

  private apiKey?: string;

  constructor(apiKey?: string) {
    super();
    this.[REDACTED_SECRET] || process.env.IEEE_API_KEY;
  }

  isAvailable(): boolean {
    return !!this.apiKey;
  }

  async search(query: string, limit: number): Promise<SearchResult[]> {
    if (!this.apiKey) return [];

    try {
      const url = `${IEEE_API_URL}?[REDACTED_SECRET])}&max_records=${limit}&sort_field=article_title&sort_order=asc`;

      const response = await this.rateLimitedFetch(url);
      const data = await response.json() as {
        articles?: Array<{
          article_number?: string;
          title?: string;
          authors?: { authors: Array<{ full_name: string }> };
          abstract?: string;
          publication_year?: string;
          publication_title?: string;
          doi?: string;
          html_url?: string;
          pdf_url?: string;
          citing_paper_count?: number;
          is_open_access?: boolean;
        }>;
      };

      if (!data.articles) return [];

      return data.articles.map(article => ({
        id: `ieee:${article.article_number || ''}`,
        title: article.title || '',
        authors: article.authors?.authors.map(a => a.full_name) || [],
        abstract: article.abstract || '',
        publishDate: article.publication_year || '',
        source: 'ieee',
        url: article.html_url || `https://ieeexplore.ieee.org/document/${article.article_number}`,
        doi: article.doi,
        pdfUrl: article.pdf_url,
        citations: article.citing_paper_count,
        venue: article.publication_title,
        journal: article.publication_title,
        openAccess: article.is_open_access
      }));
    } catch (error) {
      console.error('IEEE Xplore search error:', getErrorMessage(error));
      return [];
    }
  }
}


path: literature-search/scripts/adapters/index.ts
content: /**
 * Adapters - Barrel Export
 */

export { type SearchSourceAdapter, AbstractSearchAdapter, type AdapterMeta, type SourceTier, type SourceDomain } from './base';
export { SearchSourceRegistry } from './registry';

// Source adapters
export { ArxivAdapter } from './arxiv-adapter';
export { SemanticScholarAdapter } from './semantic-scholar-adapter';
export { WebAdapter } from './web-adapter';
export { OpenAlexAdapter } from './openalex-adapter';
export { PubMedAdapter } from './pubmed-adapter';
export { CrossRefAdapter } from './crossref-adapter';
export { DblpAdapter } from './dblp-adapter';
export { IeeeAdapter } from './ieee-adapter';
export { CoreAdapter } from './core-adapter';
export { UnpaywallAdapter } from './unpaywall-adapter';
export { GoogleScholarAdapter } from './google-scholar-adapter';


path: literature-search/scripts/adapters/openalex-adapter.ts
content: /**
 * OpenAlex Adapter - Open scholarly metadata
 * 250M+ records, comprehensive coverage
 */

import { AbstractSearchAdapter, type AdapterMeta } from './base';
import type { SearchResult, OpenAlexWork } from '../types';
import { reconstructAbstract } from '../../../shared/utils';
import { getErrorMessage } from '../../../shared/errors';

const OPENALEX_API_URL = 'https://api.openalex.org/works';

export class OpenAlexAdapter extends AbstractSearchAdapter {
  readonly meta: AdapterMeta = {
    name: 'OpenAlex',
    sourceId: 'openalex',
    tier: 'free',
    domains: ['general', 'multidisciplinary', 'biomedical', 'cs', 'physics', 'engineering'],
    rateLimit: { requests: 10, windowMs: 1000 },
    description: 'Open catalog of 250M+ scholarly works, authors, venues, and concepts'
  };

  private mailto?: string;

  constructor(mailto?: string) {
    super();
    this.mailto = mailto || process.env.OPENALEX_MAILTO;
  }

  isAvailable(): boolean {
    return true; // No API key required
  }

  async search(query: string, limit: number): Promise<SearchResult[]> {
    try {
      let url = `${OPENALEX_API_URL}?search=${encodeURIComponent(query)}&per_page=${limit}`;
      if (this.mailto) {
        url += `&mailto=${encodeURIComponent(this.mailto)}`;
      }

      const response = await this.rateLimitedFetch(url);
      const data = await response.json() as { results?: OpenAlexWork[] };

      if (!data.results) return [];

      return data.results.map((work) => {
        const abstract = work.abstract_inverted_index
          ? reconstructAbstract(work.abstract_inverted_index)
          : '';

        const authors = work.authorships?.map(a => a.author.display_name) || [];
        const doi = work.doi?.replace('https://doi.org/', '');

        return {
          id: work.id.replace('https://openalex.org/', ''),
          title: work.display_name || work.title || '',
          authors,
          abstract,
          publishDate: work.publication_date || work.publication_year?.toString() || '',
          source: 'openalex',
          url: work.doi || work.id,
          doi,
          pdfUrl: work.best_oa_location?.pdf_url || work.open_access?.oa_url,
          citations: work.cited_by_count,
          venue: work.primary_location?.source?.display_name,
          openAccess: work.is_oa || work.open_access?.is_oa,
          concepts: work.concepts?.filter(c => c.score > 0.3).map(c => c.display_name)
        };
      });
    } catch (error) {
      console.error('OpenAlex search error:', getErrorMessage(error));
      return [];
    }
  }
}


path: literature-search/scripts/adapters/pubmed-adapter.ts
content: /**
 * PubMed Adapter - NCBI E-utilities
 * Best for biomedical/life sciences
 */

import { AbstractSearchAdapter, type AdapterMeta } from './base';
import type { SearchResult } from '../types';
import { parsePubMedXml } from '../../../shared/utils';
import { getErrorMessage } from '../../../shared/errors';

const EUTILS_BASE = 'https://eutils.ncbi.nlm.nih.gov/entrez/eutils';

export class PubMedAdapter extends AbstractSearchAdapter {
  readonly meta: AdapterMeta = {
    name: 'PubMed',
    sourceId: 'pubmed',
    tier: 'free',
    domains: ['biomedical'],
    rateLimit: { requests: 3, windowMs: 1000 },
    description: 'NCBI biomedical literature database with 35M+ citations'
  };

  private apiKey?: string;

  constructor(apiKey?: string) {
    super();
    this.[REDACTED_SECRET] || process.env.NCBI_API_KEY;
    // With API key, rate limit is 10/s
    if (this.apiKey) {
      (this.meta as AdapterMeta).rateLimit = { requests: 10, windowMs: 1000 };
    }
  }

  isAvailable(): boolean {
    return true; // Works without API key (lower rate limit)
  }

  async search(query: string, limit: number): Promise<SearchResult[]> {
    try {
      // Step 1: esearch to get PMIDs
      let searchUrl = `${EUTILS_BASE}/esearch.fcgi?db=pubmed&term=${encodeURIComponent(query)}&retmax=${limit}&retmode=json`;
      if (this.apiKey) searchUrl += `&[REDACTED_SECRET]`;

      const searchResp = await this.rateLimitedFetch(searchUrl);
      const searchData = await searchResp.json() as {
        esearchresult?: { idlist?: string[] };
      };

      const pmids = searchData.esearchresult?.idlist;
      if (!pmids || pmids.length === 0) return [];

      // Step 2: efetch to get article details
      let fetchUrl = `${EUTILS_BASE}/efetch.fcgi?db=pubmed&id=${pmids.join(',')}&rettype=xml&retmode=xml`;
      if (this.apiKey) fetchUrl += `&[REDACTED_SECRET]`;

      const fetchResp = await this.rateLimitedFetch(fetchUrl);
      const xml = await fetchResp.text();

      const articles = parsePubMedXml(xml);

      return articles.map(article => ({
        id: `pmid:${article.pmid}`,
        title: article.title,
        authors: article.authors,
        abstract: article.abstract,
        publishDate: article.publishDate,
        source: 'pubmed',
        url: `https://pubmed.ncbi.nlm.nih.gov/${article.pmid}/`,
        doi: article.doi,
        journal: article.journal,
        meshTerms: article.meshTerms,
        pdfUrl: article.doi ? `https://doi.org/${article.doi}` : undefined
      }));
    } catch (error) {
      console.error('PubMed search error:', getErrorMessage(error));
      return [];
    }
  }
}


path: literature-search/scripts/adapters/registry.ts
content: /**
 * Search Source Registry
 * 搜索源注册表 - 管理所有可用的搜索适配器
 */

import type { SearchSourceAdapter, SourceTier, SourceDomain } from './base';
import type { SearchSource } from '../../../shared/types';

export class SearchSourceRegistry {
  private adapters = new Map<SearchSource, SearchSourceAdapter>();

  /**
   * Register an adapter
   */
  register(adapter: SearchSourceAdapter): void {
    this.adapters.set(adapter.meta.sourceId, adapter);
  }

  /**
   * Get adapter by source ID
   */
  get(sourceId: SearchSource): SearchSourceAdapter | undefined {
    return this.adapters.get(sourceId);
  }

  /**
   * Get all registered adapters
   */
  getAll(): SearchSourceAdapter[] {
    return Array.from(this.adapters.values());
  }

  /**
   * Get only available adapters (API keys present, etc.)
   */
  getAvailable(): SearchSourceAdapter[] {
    return this.getAll().filter(a => a.isAvailable());
  }

  /**
   * Get adapters by tier
   */
  getByTier(tier: SourceTier): SearchSourceAdapter[] {
    return this.getAll().filter(a => a.meta.tier === tier);
  }

  /**
   * Get adapters by domain
   */
  getByDomain(domain: SourceDomain): SearchSourceAdapter[] {
    return this.getAll().filter(a => a.meta.domains.includes(domain));
  }

  /**
   * Get available adapters filtered by source IDs, maintaining order
   */
  getBySourceIds(sourceIds: SearchSource[]): SearchSourceAdapter[] {
    const result: SearchSourceAdapter[] = [];
    for (const id of sourceIds) {
      const adapter = this.adapters.get(id);
      if (adapter && adapter.isAvailable()) {
        result.push(adapter);
      }
    }
    return result;
  }

  /**
   * Check if a source is registered and available
   */
  isAvailable(sourceId: SearchSource): boolean {
    const adapter = this.adapters.get(sourceId);
    return adapter ? adapter.isAvailable() : false;
  }

  /**
   * Get all registered source IDs
   */
  getSourceIds(): SearchSource[] {
    return Array.from(this.adapters.keys());
  }
}


path: literature-search/scripts/adapters/semantic-scholar-adapter.ts
content: /**
 * Semantic Scholar Adapter
 */

import { AbstractSearchAdapter, type AdapterMeta } from './base';
import type { SearchResult, SemanticScholarPaper } from '../types';
import { getErrorMessage } from '../../../shared/errors';

const S2_API_URL = 'https://api.semanticscholar.org/graph/v1';

export class SemanticScholarAdapter extends AbstractSearchAdapter {
  readonly meta: AdapterMeta = {
    name: 'Semantic Scholar',
    sourceId: 'semantic_scholar',
    tier: 'free',
    domains: ['general', 'cs', 'biomedical', 'multidisciplinary'],
    rateLimit: { requests: 10, windowMs: 1000 },
    description: 'AI-powered academic search engine with 200M+ papers'
  };

  isAvailable(): boolean {
    return true; // No API key required (has optional key for higher limits)
  }

  async search(query: string, limit: number): Promise<SearchResult[]> {
    try {
      const fields = 'paperId,title,abstract,authors,year,citationCount,venue,url,openAccessPdf';
      const url = `${S2_API_URL}/paper/search?query=${encodeURIComponent(query)}&limit=${limit}&fields=${fields}`;

      const response = await this.rateLimitedFetch(url, undefined, { maxRetries: 3 });
      const data = await response.json() as { data?: SemanticScholarPaper[] };

      if (!data.data) return [];

      return data.data.map((paper) => ({
        id: paper.paperId,
        title: paper.title,
        authors: paper.authors.map(a => a.name),
        abstract: paper.abstract || '',
        publishDate: paper.year?.toString() || '',
        source: 'semantic_scholar',
        url: paper.url,
        pdfUrl: paper.openAccessPdf?.url,
        citations: paper.citationCount,
        venue: paper.venue,
        openAccess: !!paper.openAccessPdf
      }));
    } catch (error) {
      console.error('Semantic Scholar search error:', getErrorMessage(error));
      return [];
    }
  }
}


path: literature-search/scripts/adapters/unpaywall-adapter.ts
content: /**
 * Unpaywall Adapter - DOI-to-OA-PDF resolver
 * Not a search engine - resolves DOIs to open access PDF URLs
 */

import { AbstractSearchAdapter, type AdapterMeta } from './base';
import type { SearchResult } from '../types';
import { getErrorMessage } from '../../../shared/errors';

const UNPAYWALL_API_URL = 'https://api.unpaywall.org/v2';

export class UnpaywallAdapter extends AbstractSearchAdapter {
  readonly meta: AdapterMeta = {
    name: 'Unpaywall',
    sourceId: 'unpaywall',
    tier: 'free',
    domains: ['general', 'multidisciplinary'],
    rateLimit: { requests: 10, windowMs: 1000 },
    description: 'DOI-to-open-access PDF resolver (enrichment, not search)'
  };

  private email?: string;

  constructor(email?: string) {
    super();
    this.email = email || process.env.UNPAYWALL_EMAIL;
  }

  isAvailable(): boolean {
    return !!this.email;
  }

  /**
   * Unpaywall is not a search engine - returns empty for search queries.
   * Use getPdfUrl() to resolve DOIs to PDF URLs.
   */
  async search(_query: string, _limit: number): Promise<SearchResult[]> {
    return []; // Not a search engine
  }

  /**
   * Resolve a DOI to an open access PDF URL
   */
  async getPdfUrl(result: SearchResult): Promise<string | undefined> {
    if (!this.email || !result.doi) return undefined;

    try {
      const url = `${UNPAYWALL_API_URL}/${encodeURIComponent(result.doi)}?email=${encodeURIComponent(this.email)}`;
      const response = await this.rateLimitedFetch(url);
      const data = await response.json() as {
        best_oa_location?: {
          url_for_pdf?: string;
          url?: string;
        };
        is_oa?: boolean;
      };

      return data.best_oa_location?.url_for_pdf || data.best_oa_location?.url;
    } catch (error) {
      console.error('Unpaywall lookup error:', getErrorMessage(error));
      return undefined;
    }
  }

  /**
   * Batch resolve DOIs
   */
  async batchResolvePdfUrls(results: SearchResult[]): Promise<Map<string, string>> {
    const urlMap = new Map<string, string>();
    const resultsWithDois = results.filter(r => r.doi && !r.pdfUrl);

    for (const result of resultsWithDois) {
      const pdfUrl = await this.getPdfUrl(result);
      if (pdfUrl && result.doi) {
        urlMap.set(result.doi, pdfUrl);
      }
    }

    return urlMap;
  }
}


path: literature-search/scripts/adapters/web-adapter.ts
content: /**
 * Web Search Adapter - via AI provider web search
 */

import { AbstractSearchAdapter, type AdapterMeta } from './base';
import type { SearchResult } from '../types';
import type { AIProvider } from '../../../shared/ai-provider';
import type { WebSearchResultItem } from '../../../shared/types';
import { getErrorMessage } from '../../../shared/errors';

export class WebAdapter extends AbstractSearchAdapter {
  readonly meta: AdapterMeta = {
    name: 'Web Search',
    sourceId: 'web',
    tier: 'free',
    domains: ['general', 'multidisciplinary'],
    rateLimit: { requests: 5, windowMs: 1000 },
    description: 'Web search via AI provider (Serper API)'
  };

  private ai: AIProvider | null = null;

  setAIProvider(ai: AIProvider): void {
    this.ai = ai;
  }

  isAvailable(): boolean {
    return this.ai !== null && typeof this.ai.webSearch === 'function';
  }

  async search(query: string, limit: number): Promise<SearchResult[]> {
    if (!this.ai?.webSearch) {
      return [];
    }

    try {
      const academicQuery = `${query} research paper arxiv OR scholar OR "paper" OR "publication"`;
      const results: WebSearchResultItem[] = await this.ai.webSearch(academicQuery, limit);

      return results.map((item, index) => ({
        id: `web_${index}`,
        title: item.name,
        authors: [],
        abstract: item.snippet || '',
        publishDate: item.date || '',
        source: 'web',
        url: item.url,
        snippet: item.snippet
      }));
    } catch (error) {
      console.error('Web search error:', getErrorMessage(error));
      return [];
    }
  }
}


path: literature-search/scripts/pdf-downloader.ts
content: /**
 * PDF Downloader - Multi-strategy PDF download module
 * PDF 下载器 - 多策略 PDF 下载模块
 */

import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
import { join, resolve } from 'path';
import type { SearchResult, PdfDownloadOptions, PdfDownloadResult, PdfMetadataIndex } from './types';
import type { UnpaywallAdapter } from './adapters/unpaywall-adapter';
import type { SearchSourceRegistry } from './adapters/registry';
import { getErrorMessage } from '../../shared/errors';

const DEFAULT_OPTIONS: Required<PdfDownloadOptions> = {
  outputDir: './downloads/pdfs',
  maxFileSize: 50 * 1024 * 1024, // 50MB
  skipExisting: true,
  concurrency: 3,
  namingStrategy: 'title'
};

/** PDF magic bytes check */
const PDF_MAGIC = '%PDF';

export class PdfDownloader {
  private options: Required<PdfDownloadOptions>;
  private registry?: SearchSourceRegistry;
  private unpaywallAdapter?: UnpaywallAdapter;

  constructor(options?: PdfDownloadOptions, registry?: SearchSourceRegistry) {
    this.options = { ...DEFAULT_OPTIONS, ...options };
    this.registry = registry;

    // Try to get Unpaywall adapter from registry
    if (registry) {
      const unpaywall = registry.get('unpaywall');
      if (unpaywall && 'getPdfUrl' in unpaywall) {
        this.unpaywallAdapter = unpaywall as UnpaywallAdapter;
      }
    }
  }

  /**
   * Download PDFs for search results
   */
  async downloadResults(results: SearchResult[]): Promise<PdfDownloadResult[]> {
    // Ensure output directory exists
    const outputDir = resolve(this.options.outputDir);
    if (!existsSync(outputDir)) {
      mkdirSync(outputDir, { recursive: true });
    }

    const downloadResults: PdfDownloadResult[] = [];

    // Process in batches based on concurrency limit
    for (let i = 0; i < results.length; i += this.options.concurrency) {
      const batch = results.slice(i, i + this.options.concurrency);
      const batchPromises = batch.map(result =>
        this.downloadSingle(result, outputDir)
          .then(dlResult => {
            if (dlResult) downloadResults.push(dlResult);
          })
          .catch(err => {
            console.error(`Failed to download "${result.title}":`, getErrorMessage(err));
          })
      );
      await Promise.allSettled(batchPromises);
    }

    // Update metadata index
    this.updateMetadataIndex(outputDir, downloadResults, results);

    return downloadResults;
  }

  /**
   * Download a single PDF
   */
  private async downloadSingle(
    result: SearchResult,
    outputDir: string
  ): Promise<PdfDownloadResult | null> {
    const filename = this.generateFilename(result);
    const filePath = join(outputDir, filename);

    // Skip existing
    if (this.options.skipExisting && existsSync(filePath)) {
      console.log(`  Skipping (exists): ${filename}`);
      return null;
    }

    // Resolve PDF URL
    const pdfUrl = await this.resolvePdfUrl(result);
    if (!pdfUrl) {
      console.log(`  No PDF URL found for: ${result.title}`);
      return null;
    }

    try {
      console.log(`  Downloading: ${filename}`);
      const response = await fetch(pdfUrl, {
        headers: {
          'User-Agent': 'ScholarGraph/1.0 (academic research tool)',
          'Accept': 'application/pdf'
        },
        redirect: 'follow'
      });

      if (!response.ok) {
        console.error(`  HTTP ${response.status} for ${pdfUrl}`);
        return null;
      }

      // Check content length
      const contentLength = response.headers.get('content-length');
      if (contentLength && parseInt(contentLength) > this.options.maxFileSize) {
        console.error(`  File too large (${contentLength} bytes): ${filename}`);
        return null;
      }

      const buffer = Buffer.from(await response.arrayBuffer());

      // Size check on actual content
      if (buffer.length > this.options.maxFileSize) {
        console.error(`  File too large (${buffer.length} bytes): ${filename}`);
        return null;
      }

      // PDF magic bytes verification
      const header = buffer.slice(0, 4).toString('ascii');
      if (!header.startsWith(PDF_MAGIC)) {
        console.error(`  Not a valid PDF (header: ${header}): ${filename}`);
        return null;
      }

      writeFileSync(filePath, buffer);
      console.log(`  Saved: ${filename} (${(buffer.length / 1024).toFixed(1)} KB)`);

      return {
        filePath,
        title: result.title,
        source: result.source,
        size: buffer.length,
        doi: result.doi
      };
    } catch (error) {
      console.error(`  Download error: ${getErrorMessage(error)}`);
      return null;
    }
  }

  /**
   * Multi-strategy PDF URL resolution
   */
  private async resolvePdfUrl(result: SearchResult): Promise<string | undefined> {
    // Strategy 1: Direct pdfUrl from search result
    if (result.pdfUrl) return result.pdfUrl;

    // Strategy 2: Unpaywall lookup by DOI
    if (result.doi && this.unpaywallAdapter?.isAvailable()) {
      const url = await this.unpaywallAdapter.getPdfUrl(result);
      if (url) return url;
    }

    // Strategy 3: OpenAlex OA location (already in pdfUrl if from OpenAlex)

    // Strategy 4: CORE full text
    if (result.doi && this.registry) {
      const coreAdapter = this.registry.get('core');
      if (coreAdapter && 'getPdfUrl' in coreAdapter && coreAdapter.isAvailable()) {
        const url = await (coreAdapter as any).getPdfUrl(result);
        if (url) return url;
      }
    }

    return undefined;
  }

  /**
   * Generate filename from result
   */
  private generateFilename(result: SearchResult): string {
    let baseName: string;

    switch (this.options.namingStrategy) {
      case 'doi':
        baseName = result.doi ? result.doi.replace(/[/\\:]/g, '_') : this.sanitizeTitle(result.title);
        break;
      case 'id':
        baseName = result.id.replace(/[/\\:]/g, '_');
        break;
      case 'title':
      default:
        baseName = this.sanitizeTitle(result.title);
        break;
    }

    return `${baseName}.pdf`;
  }

  /**
   * Sanitize title for use as filename
   */
  private sanitizeTitle(title: string): string {
    return title
      .replace(/[<>:"/\\|?*]/g, '')
      .replace(/\s+/g, '_')
      .substring(0, 100)
      .replace(/_+$/, '');
  }

  /**
   * Update metadata.json index
   */
  private updateMetadataIndex(
    outputDir: string,
    downloadResults: PdfDownloadResult[],
    searchResults: SearchResult[]
  ): void {
    const indexPath = join(outputDir, 'metadata.json');

    let index: PdfMetadataIndex = { files: [] };

    // Load existing index
    try {
      if (existsSync(indexPath)) {
        index = JSON.parse(readFileSync(indexPath, 'utf-8'));
      }
    } catch { /* ignore */ }

    // Add new entries
    for (const dl of downloadResults) {
      const searchResult = searchResults.find(r => r.title === dl.title);
      if (!searchResult) continue;

      // Check if already indexed
      if (index.files.some(f => f.filePath === dl.filePath)) continue;

      index.files.push({
        filePath: dl.filePath,
        title: dl.title,
        authors: searchResult.authors,
        source: dl.source,
        doi: dl.doi,
        url: searchResult.url,
        downloadedAt: new Date().toISOString()
      });
    }

    writeFileSync(indexPath, JSON.stringify(index, null, 2));
  }
}


path: literature-search/scripts/query-expander.ts
content: /**
 * Query Expander - 查询扩展模块
 *
 * 将用户模糊的研究兴趣转化为具体的搜索关键词
 */

import { createAIProvider, type AIProvider } from '../../shared/ai-provider';
import { ApiInitializationError, getErrorMessage } from '../../shared/errors';

export interface QueryExpansionResult {
  // 确认的用户需求信息
  confirmation: {
    coreTheme?: string;        // 核心主题
    subField?: string;         // 子领域/方向
    applicationScenario?: string; // 应用场景
    timeRange?: string;        // 时间范围
    preferredSources?: string; // 偏好来源
  };

  // 生成的关键词列表
  keywords: Array<{
    term: string;              // 关键词
    description: string;       // 简短说明
    type: 'core' | 'synonym' | 'abbreviation' | 'related' | 'application'; // 关键词类型
  }>;

  // 追问问题（如果需要更多信息）
  followUpQuestions?: string[];

  // 是否需要更多信息
  needsMoreInfo: boolean;
}

export default class QueryExpander {
  private ai: AIProvider | null = null;

  async initialize(): Promise<void> {
    if (!this.ai) {
      try {
        this.ai = await createAIProvider();
      } catch (error) {
        throw new ApiInitializationError(
          `Failed to initialize AI provider: ${getErrorMessage(error)}`,
          error instanceof Error ? error : undefined
        );
      }
    }
  }

  /**
   * 扩展用户查询，生成多个搜索关键词
   */
  async expandQuery(userInput: string, context?: string): Promise<QueryExpansionResult> {
    await this.initialize();

    const systemPrompt = `你是一个专业的学术文献搜索助手 "Literature Search Prompt Engineer"。
你的任务是根据用户模糊的研究兴趣，通过分析来厘清领域，并生成多个可执行的搜索关键词。

## 工作流程

### 阶段 1：理解需求
用户给你的输入可能是模糊的，你需要分析以下信息：
1. **核心主题**：具体想研究什么？
2. **子领域/方向**：更具体的角度？
3. **应用场景**：研究目的是什么？
4. **时间范围**：需要多新的论文？
5. **偏好来源**：对某些期刊/会议有偏好吗？

### 阶段 2：生成关键词
生成关键词的策略：
- **核心词**：用户提到的核心概念
- **同义词**：常见的替换词
- **缩写**：全称和缩写都要
- **相关术语**：上下游技术、工具、框架
- **应用领域**：具体应用场景

### 输出格式
返回 JSON 格式：
{
  "confirmation": {
    "coreTheme": "核心主题",
    "subField": "子领域",
    "applicationScenario": "应用场景",
    "timeRange": "时间范围",
    "preferredSources": "偏好来源"
  },
  "keywords": [
    {
      "term": "关键词",
      "description": "说明",
      "type": "core|synonym|abbreviation|related|application"
    }
  ],
  "followUpQuestions": ["问题1", "问题2"],
  "needsMoreInfo": true/false
}`;

    const userPrompt = context
      ? `用户输入：${userInput}\n\n上下文信息：${context}\n\n请分析用户需求并生成搜索关键词。`
      : `用户输入：${userInput}\n\n请分析用户需求并生成搜索关键词。`;

    try {
      const response = await this.ai!.chat([
        { role: 'system', content: systemPrompt },
        { role: 'user', content: userPrompt }
      ], { temperature: 0.7 });

      // 尝试解析 JSON 响应
      const jsonMatch = response.match(/\{[\s\S]*\}/);
      if (jsonMatch) {
        const result = JSON.parse(jsonMatch[0]);
        return result as QueryExpansionResult;
      }

      // 如果无法解析 JSON，返回基础结果
      return {
        confirmation: {
          coreTheme: userInput
        },
        keywords: [
          {
            term: userInput,
            description: '用户原始输入',
            type: 'core'
          }
        ],
        needsMoreInfo: true,
        followUpQuestions: [
          '你更偏向于哪个具体方向？',
          '是为了写论文、找创新点，还是为了工程应用？'
        ]
      };
    } catch (error) {
      throw new Error(`Query expansion failed: ${getErrorMessage(error)}`);
    }
  }

  /**
   * 交互式查询扩展（多轮对话）
   */
  async interactiveExpand(
    userInput: string,
    conversationHistory: Array<{ role: 'user' | 'assistant'; content: string }> = []
  ): Promise<QueryExpansionResult> {
    await this.initialize();

    const systemPrompt = `你是一个专业的学术文献搜索助手。通过多轮对话来帮助用户明确研究方向并生成搜索关键词。

每次对话都要：
1. 理解用户的回答
2. 更新对用户需求的理解
3. 如果信息足够，生成关键词列表
4. 如果信息不足，提出1-2个关键问题

返回 JSON 格式的结果。`;

    const messages = [
      { role: 'system' as const, content: systemPrompt },
      ...conversationHistory,
      { role: 'user' as const, content: userInput }
    ];

    try {
      const response = await this.ai!.chat(messages, { temperature: 0.7 });

      const jsonMatch = response.match(/\{[\s\S]*\}/);
      if (jsonMatch) {
        const result = JSON.parse(jsonMatch[0]);
        return result as QueryExpansionResult;
      }

      return {
        confirmation: {},
        keywords: [],
        needsMoreInfo: true,
        followUpQuestions: ['请提供更多信息以帮助我理解你的研究方向。']
      };
    } catch (error) {
      throw new Error(`Interactive expansion failed: ${getErrorMessage(error)}`);
    }
  }

  /**
   * 格式化输出结果（用于 CLI 显示）
   */
  formatResult(result: QueryExpansionResult): string {
    let output = '\n📋 确认信息：\n';

    const { confirmation } = result;
    if (confirmation.coreTheme) output += `- 核心主题：${confirmation.coreTheme}\n`;
    if (confirmation.subField) output += `- 子领域：${confirmation.subField}\n`;
    if (confirmation.applicationScenario) output += `- 应用场景：${confirmation.applicationScenario}\n`;
    if (confirmation.timeRange) output += `- 时间范围：${confirmation.timeRange}\n`;
    if (confirmation.preferredSources) output += `- 偏好来源：${confirmation.preferredSources}\n`;

    if (result.keywords.length > 0) {
      output += '\n🔍 建议关键词：\n';
      result.keywords.forEach((kw, index) => {
        const typeEmoji = {
          core: '🎯',
          synonym: '🔄',
          abbreviation: '📝',
          related: '🔗',
          application: '💡'
        }[kw.type] || '•';
        output += `${index + 1}. ${typeEmoji} ${kw.term} - ${kw.description}\n`;
      });
    }

    if (result.followUpQuestions && result.followUpQuestions.length > 0) {
      output += '\n❓ 追问：\n';
      result.followUpQuestions.forEach((q, index) => {
        output += `${index + 1}. ${q}\n`;
      });
    }

    return output;
  }
}


path: literature-search/scripts/search-strategy.ts
content: /**
 * Complementary Search Strategy
 * 互补搜索策略 - 根据查询自动选择最优数据源组合
 */

import type { SearchSource } from '../../shared/types';
import type { SearchSourceRegistry } from './adapters/registry';

/** Domain detection keywords */
const DOMAIN_KEYWORDS: Record<string, string[]> = {
  biomedical: [
    'gene', 'protein', 'genome', 'clinical', 'drug', 'disease', 'medical',
    'surgery', 'patient', 'therapy', 'cancer', 'cell', 'dna', 'rna',
    'molecular', 'biology', 'pathology', 'neuroscience', 'brain',
    'pharmaceutical', 'crispr', 'biomarker', 'epidemiology', 'virus',
    'vaccine', 'immune', 'metabol', 'cardio', 'oncology',
    '基因', '蛋白质', '临床', '药物', '疾病', '细胞', '分子', '生物'
  ],
  cs: [
    'algorithm', 'neural network', 'deep learning', 'machine learning',
    'transformer', 'nlp', 'computer vision', 'natural language', 'software',
    'compiler', 'database', 'distributed', 'operating system', 'gpu',
    'reinforcement learning', 'generative', 'llm', 'language model',
    'attention mechanism', 'bert', 'gpt', 'convolutional', 'recurrent',
    'classification', 'segmentation', 'object detection',
    '算法', '神经网络', '深度学习', '机器学习', '自然语言'
  ],
  engineering: [
    'circuit', 'signal processing', 'antenna', 'semiconductor', 'vlsi',
    'embedded', 'control system', 'robotics', 'sensor', 'actuator',
    'power electronics', 'communication system', 'wireless', '5g', '6g',
    'iot', 'fpga', 'microprocessor', 'rf',
    '电路', '信号处理', '半导体', '传感器', '机器人'
  ],
  physics: [
    'quantum', 'particle', 'cosmology', 'astrophysics', 'condensed matter',
    'gravitational', 'photon', 'dark matter', 'dark energy', 'superconductor',
    'plasma', 'relativity', 'boson', 'fermion', 'higgs', 'string theory',
    'thermodynamics', 'optics', 'laser', 'nuclear',
    '量子', '粒子', '宇宙', '凝聚态', '光学'
  ]
};

/** Default priority order */
const DEFAULT_PRIORITY: SearchSource[] = [
  'semantic_scholar', 'openalex', 'arxiv', 'pubmed',
  'crossref', 'dblp', 'core', 'ieee', 'web'
];

/** Domain-specific priority orders */
const DOMAIN_PRIORITIES: Record<string, SearchSource[]> = {
  biomedical: ['pubmed', 'semantic_scholar', 'openalex', 'crossref'],
  cs: ['semantic_scholar', 'arxiv', 'dblp', 'openalex'],
  engineering: ['ieee', 'semantic_scholar', 'openalex', 'crossref'],
  physics: ['arxiv', 'semantic_scholar', 'openalex'],
  general: ['semantic_scholar', 'openalex', 'crossref', 'arxiv']
};

export interface StrategyConfig {
  defaultPriority?: SearchSource[];
  domainPriorities?: Record<string, SearchSource[]>;
  maxConcurrentSources?: number;
  useComplementaryStrategy?: boolean;
}

export class ComplementarySearchStrategy {
  private config: Required<StrategyConfig>;

  constructor(config?: StrategyConfig) {
    this.config = {
      defaultPriority: config?.defaultPriority || DEFAULT_PRIORITY,
      domainPriorities: config?.domainPriorities || DOMAIN_PRIORITIES,
      maxConcurrentSources: config?.maxConcurrentSources || 4,
      useComplementaryStrategy: config?.useComplementaryStrategy ?? true
    };
  }

  /**
   * Detect academic domain from query keywords
   */
  detectDomain(query: string): string {
    const lowerQuery = query.toLowerCase();

    const scores: Record<string, number> = {};
    for (const [domain, keywords] of Object.entries(DOMAIN_KEYWORDS)) {
      scores[domain] = keywords.filter(kw => lowerQuery.includes(kw)).length;
    }

    // Find domain with highest score
    let bestDomain = 'general';
    let bestScore = 0;
    for (const [domain, score] of Object.entries(scores)) {
      if (score > bestScore) {
        bestScore = score;
        bestDomain = domain;
      }
    }

    return bestScore > 0 ? bestDomain : 'general';
  }

  /**
   * Select optimal sources for a query
   */
  selectSources(
    query: string,
    registry: SearchSourceRegistry,
    domainHint?: string
  ): SearchSource[] {
    if (!this.config.useComplementaryStrategy) {
      return this.config.defaultPriority
        .filter(s => registry.isAvailable(s))
        .slice(0, this.config.maxConcurrentSources);
    }

    const domain = domainHint || this.detectDomain(query);
    const priority = this.config.domainPriorities[domain] || this.config.defaultPriority;

    const selected: SearchSource[] = [];
    const usedIds = new Set<SearchSource>();

    // 1. Add sources from domain priority list
    for (const sourceId of priority) {
      if (selected.length >= this.config.maxConcurrentSources) break;
      if (registry.isAvailable(sourceId) && !usedIds.has(sourceId)) {
        selected.push(sourceId);
        usedIds.add(sourceId);
      }
    }

    // 2. Fill remaining slots from default priority
    if (selected.length < this.config.maxConcurrentSources) {
      for (const sourceId of this.config.defaultPriority) {
        if (selected.length >= this.config.maxConcurrentSources) break;
        if (registry.isAvailable(sourceId) && !usedIds.has(sourceId)) {
          selected.push(sourceId);
          usedIds.add(sourceId);
        }
      }
    }

    return selected;
  }

  /**
   * Get the detected or hinted domain for display
   */
  getDomainInfo(query: string, domainHint?: string): { domain: string; isHinted: boolean } {
    if (domainHint) {
      return { domain: domainHint, isHinted: true };
    }
    return { domain: this.detectDomain(query), isHinted: false };
  }
}


path: package.json
content: {
  "name": "scholargraph",
  "version": "1.0.0",
  "description": "学术文献智能工具包 - 高效、系统性的学术文献分析与知识管理工具",
  "type": "module",
  "main": "./index.js",
  "bin": {
    "lit": "./cli.ts"
  },
  "scripts": {
    "search": "bun run cli.ts search",
    "learn": "bun run cli.ts learn",
    "detect": "bun run cli.ts detect",
    "track": "bun run cli.ts track",
    "analyze": "bun run cli.ts analyze",
    "graph": "bun run cli.ts graph",
    "config": "bun run cli.ts config",
    "test": "bun test",
    "test:utils": "bun test tests/utils.test.ts",
    "test:validators": "bun test tests/validators.test.ts",
    "test:errors": "bun test tests/errors.test.ts",
    "typecheck": "bun run tsc --noEmit",
    "lint": "bun lint"
  },
  "keywords": [
    "scholargraph",
    "literature",
    "research",
    "academic",
    "knowledge-management",
    "paper-analysis",
    "learning",
    "nlp",
    "machine-learning",
    "ai",
    "knowledge-graph"
  ],
  "author": "ScholarGraph Team",
  "license": "MIT",
  "dependencies": {
    "z-ai-web-dev-sdk": "latest"
  },
  "optionalDependencies": {
    "openai": "^4.0.0",
    "@anthropic-ai/sdk": "^0.30.0"
  },
  "devDependencies": {
    "@types/bun": "latest",
    "typescript": "^5.0.0"
  },
  "engines": {
    "bun": ">=1.0.0"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/Josephyb97/ScholarGraph.git"
  },
  "bugs": {
    "url": "https://github.com/Josephyb97/ScholarGraph/issues"
  },
  "homepage": "https://github.com/Josephyb97/ScholarGraph#readme"
}


path: review-detector/scripts/types.ts
content: /**
 * Review Detector - Type Definitions
 * 综述识别模块类型定义
 */

import type { SearchResult } from '../../literature-search/scripts/types';

/**
 * 综述类型
 */
export type ReviewType = 'survey' | 'systematic_review' | 'tutorial' | 'meta_analysis' | 'none';

/**
 * 综述识别结果
 */
export interface ReviewDetectionResult {
  /** 原始论文信息 */
  paper: SearchResult;
  /** 是否为综述 */
  isReview: boolean;
  /** 置信度 (0-1) */
  confidence: number;
  /** 综述类型 */
  reviewType: ReviewType;
  /** 判断依据说明（给用户看） */
  reasoning: string;
  /** 各维度评分详情 */
  scores: {
    /** 标题关键词得分 (0-1) */
    titleScore: number;
    /** 引用数得分 (0-1) */
    citationScore: number;
    /** 摘要模式得分 (0-1) */
    abstractScore: number;
    /** AI辅助判断得分 (0-1) */
    aiScore: number;
  };
}

/**
 * 综述识别选项
 */
export interface ReviewDetectionOptions {
  /** 是否启用AI辅助判断 */
  useAI?: boolean;
  /** 最小置信度阈值 */
  minConfidence?: number;
  /** 语言偏好 */
  language?: 'zh-CN' | 'en';
}

/**
 * 批量识别结果
 */
export interface BatchDetectionResult {
  /** 所有识别结果 */
  results: ReviewDetectionResult[];
  /** 识别为综述的论文 */
  reviews: ReviewDetectionResult[];
  /** 非综述论文 */
  nonReviews: ReviewDetectionResult[];
  /** 统计信息 */
  stats: {
    total: number;
    reviewCount: number;
    averageConfidence: number;
    byType: Record<ReviewType, number>;
  };
}

/**
 * 综述关键词配置
 */
export interface ReviewKeywordsConfig {
  /** 英文综述关键词（标题） */
  englishTitle: string[];
  /** 中文综述关键词（标题） */
  chineseTitle: string[];
  /** 英文综述关键词（摘要） */
  englishAbstract: string[];
  /** 中文综述关键词（摘要） */
  chineseAbstract: string[];
}


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