# CodexLens 技术方案 > 融合 code-index-mcp 与 codanna 最佳特性的代码智能分析平台 > > 目标:接入 CCW (Claude Code Workflow) 工具端点 --- ## 目录 1. [项目概览](#1-项目概览) 2. [架构设计](#2-架构设计) 3. [目录结构](#3-目录结构) 4. [核心模块设计](#4-核心模块设计) 5. [CCW 集成设计](#5-ccw-集成设计) 6. [数据存储设计](#6-数据存储设计) 7. [语义搜索架构](#7-语义搜索架构) 8. [CLI 命令设计](#8-cli-命令设计) 9. [开发路线图](#9-开发路线图) 10. [技术依赖](#10-技术依赖) 11. [npm 分发策略](#11-npm-分发策略) --- ## 1. 项目概览 ### 1.1 项目信息 | 属性 | 值 | |------|-----| | **项目名称** | CodexLens | | **包名** | `codex_lens` | | **语言** | Python 3.10+ | | **定位** | 多模态代码分析平台 | | **集成目标** | CCW 工具端点 (`D:\Claude_dms3\ccw`) | ### 1.2 核心能力 ``` ┌─────────────────────────────────────────────────────────────┐ │ CodexLens 能力矩阵 │ ├─────────────────────────────────────────────────────────────┤ │ 🔍 结构索引 │ AST 解析、符号提取、调用关系图 │ │ 🧠 语义搜索 │ 自然语言查询、向量嵌入、相似度匹配 │ │ 📊 代码分析 │ 复杂度计算、影响分析、依赖追踪 │ │ 🔗 CCW 集成 │ JSON 协议、工具注册、命令行接口 │ └─────────────────────────────────────────────────────────────┘ ``` ### 1.3 设计原则 - **CLI-First**: 无服务器依赖,通过命令行调用 - **JSON 协议**: 标准化输入输出,便于 CCW 解析 - **增量索引**: 仅处理变更文件,提升性能 - **可选语义**: 语义搜索作为可选功能,保持核心轻量 --- ## 2. 架构设计 ### 2.1 整体架构 ``` ┌─────────────────────────────────────────────────────────────────┐ │ CCW 层 │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ ccw/src/tools/codex-lens.js (CCW Tool Wrapper) │ │ │ │ - 注册 CodexLens 工具到 CCW │ │ │ │ - 参数验证与转换 │ │ │ │ - 调用 Python CLI │ │ │ │ - 解析 JSON 输出 │ │ │ └─────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ │ │ spawn / exec ▼ ┌─────────────────────────────────────────────────────────────────┐ │ CodexLens CLI │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ codexlens [options] --json │ │ │ │ │ │ │ │ Commands: │ │ │ │ - init 初始化项目索引 │ │ │ │ - search 文本/正则搜索 │ │ │ │ - find 文件查找 (glob) │ │ │ │ - symbol 符号查找 │ │ │ │ - inspect 文件/符号详情 │ │ │ │ - graph 调用关系图 │ │ │ │ - semantic 语义搜索 (可选) │ │ │ │ - status 索引状态 │ │ │ └─────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ Core Engine │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ Indexer │ │ Searcher │ │ Analyzer │ │ │ │ (索引引擎) │ │ (搜索引擎) │ │ (分析引擎) │ │ │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ Storage Layer │ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ │ │ SQLite │ │ ChromaDB │ │ FileCache │ │ │ │ │ │ (符号索引) │ │ (向量存储) │ │ (文件缓存) │ │ │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ └─────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ ``` ### 2.2 数据流 ``` 用户/CCW 请求 │ ▼ ┌─────────────┐ │ CLI 解析 │ ──→ 验证参数 ──→ 加载配置 └──────┬──────┘ │ ▼ ┌─────────────┐ │ 命令路由器 │ ──→ 选择处理器 └──────┬──────┘ │ ├──→ SearchHandler ──→ ripgrep/SQLite ├──→ SymbolHandler ──→ SQLite 符号表 ├──→ GraphHandler ──→ NetworkX 图 └──→ SemanticHandler ──→ ChromaDB 向量 │ ▼ ┌─────────────┐ │ JSON 输出 │ ──→ stdout └─────────────┘ ``` --- ## 3. 目录结构 ### 3.1 Python 项目结构 ``` codex-lens/ ├── .codexlens/ # 项目级配置目录 (git ignored) │ ├── config.toml # 项目配置 │ ├── index.db # SQLite 索引 │ └── vectors/ # ChromaDB 向量存储 │ ├── src/ │ └── codex_lens/ │ ├── __init__.py │ ├── __main__.py # python -m codex_lens 入口 │ │ │ ├── cli/ # CLI 层 │ │ ├── __init__.py │ │ ├── main.py # Typer 应用主入口 │ │ ├── commands/ # 命令实现 │ │ │ ├── __init__.py │ │ │ ├── init.py # codexlens init │ │ │ ├── search.py # codexlens search │ │ │ ├── find.py # codexlens find │ │ │ ├── symbol.py # codexlens symbol │ │ │ ├── inspect.py # codexlens inspect │ │ │ ├── graph.py # codexlens graph │ │ │ ├── semantic.py # codexlens semantic │ │ │ └── status.py # codexlens status │ │ └── output.py # JSON 输出格式化 │ │ │ ├── core/ # 核心领域层 │ │ ├── __init__.py │ │ ├── entities.py # 数据实体: Symbol, File, Relation │ │ ├── interfaces.py # 抽象接口: Indexer, Searcher │ │ ├── config.py # Pydantic 配置模型 │ │ └── errors.py # 自定义异常 │ │ │ ├── engine/ # 引擎层 │ │ ├── __init__.py │ │ ├── indexer.py # 索引编排器 │ │ ├── searcher.py # 搜索编排器 │ │ ├── analyzer.py # 分析编排器 │ │ └── watcher.py # 文件监控 (可选) │ │ │ ├── parsing/ # 解析层 │ │ ├── __init__.py │ │ ├── base.py # ParsingStrategy ABC │ │ ├── factory.py # 策略工厂 │ │ ├── python_parser.py # Python AST 解析 │ │ ├── js_parser.py # JavaScript/TS 解析 │ │ ├── rust_parser.py # Rust 解析 │ │ └── fallback.py # 通用回退解析 │ │ │ ├── semantic/ # 语义搜索层 (可选) │ │ ├── __init__.py │ │ ├── embedder.py # 嵌入生成器 │ │ ├── chunker.py # 代码分块 │ │ └── search.py # 向量搜索 │ │ │ ├── storage/ # 存储层 │ │ ├── __init__.py │ │ ├── sqlite_store.py # SQLite 存储 │ │ ├── vector_store.py # ChromaDB 适配 │ │ └── file_cache.py # 文件哈希缓存 │ │ │ └── utils/ # 工具层 │ ├── __init__.py │ ├── git.py # Git 集成 │ ├── ripgrep.py # ripgrep 包装 │ └── logging.py # 日志配置 │ ├── tests/ # 测试 │ ├── __init__.py │ ├── test_indexer.py │ ├── test_search.py │ └── fixtures/ │ ├── pyproject.toml # 项目配置 ├── codexlens.spec # PyInstaller 配置 └── README.md ``` ### 3.2 CCW 集成文件 ``` D:\Claude_dms3\ccw\src\tools\ └── codex-lens.js # CCW 工具包装器 ``` --- ## 4. 核心模块设计 ### 4.1 核心实体 (`core/entities.py`) ```python from dataclasses import dataclass, field from typing import List, Optional, Dict, Any from enum import Enum class SymbolType(Enum): FUNCTION = "function" CLASS = "class" METHOD = "method" VARIABLE = "variable" INTERFACE = "interface" MODULE = "module" IMPORT = "import" class RelationType(Enum): CALLS = "calls" CALLED_BY = "called_by" IMPORTS = "imports" IMPORTED_BY = "imported_by" EXTENDS = "extends" IMPLEMENTS = "implements" @dataclass class Location: """代码位置""" file_path: str line_start: int line_end: int column_start: int = 0 column_end: int = 0 @dataclass class Symbol: """代码符号""" id: str # 唯一标识: file_path::name name: str # 符号名称 short_name: str # 短名称 (用于模糊匹配) type: SymbolType # 符号类型 location: Location # 位置信息 signature: Optional[str] = None # 函数签名 docstring: Optional[str] = None # 文档字符串 language: str = "unknown" # 语言 metadata: Dict[str, Any] = field(default_factory=dict) @dataclass class FileInfo: """文件信息""" path: str # 相对路径 language: str # 语言 line_count: int # 行数 hash: str # 内容哈希 (用于增量索引) imports: List[str] = field(default_factory=list) exports: List[str] = field(default_factory=list) symbols: List[str] = field(default_factory=list) # symbol_ids @dataclass class Relation: """符号关系""" source_id: str # 源符号 ID target_id: str # 目标符号 ID relation_type: RelationType # 关系类型 metadata: Dict[str, Any] = field(default_factory=dict) @dataclass class SearchResult: """搜索结果""" file_path: str line: int column: int content: str context_before: List[str] = field(default_factory=list) context_after: List[str] = field(default_factory=list) score: float = 1.0 # 相关性得分 ``` ### 4.2 配置模型 (`core/config.py`) ```python from pydantic import BaseModel, Field from typing import List, Optional from pathlib import Path class IndexConfig(BaseModel): """索引配置""" include_patterns: List[str] = Field( default=["**/*.py", "**/*.js", "**/*.ts", "**/*.rs"], description="包含的文件模式" ) exclude_patterns: List[str] = Field( default=["**/node_modules/**", "**/.git/**", "**/dist/**", "**/__pycache__/**"], description="排除的文件模式" ) max_file_size: int = Field( default=1024 * 1024, # 1MB description="最大文件大小 (bytes)" ) enable_semantic: bool = Field( default=False, description="启用语义搜索" ) class SemanticConfig(BaseModel): """语义搜索配置""" model_name: str = Field( default="all-MiniLM-L6-v2", description="嵌入模型名称" ) chunk_size: int = Field( default=512, description="代码块大小 (tokens)" ) chunk_overlap: int = Field( default=50, description="块重叠大小" ) class ProjectConfig(BaseModel): """项目配置""" project_root: Path index: IndexConfig = Field(default_factory=IndexConfig) semantic: SemanticConfig = Field(default_factory=SemanticConfig) @classmethod def load(cls, config_path: Path) -> "ProjectConfig": """从配置文件加载""" import tomli with open(config_path, "rb") as f: data = tomli.load(f) return cls(**data) def save(self, config_path: Path): """保存到配置文件""" import tomli_w with open(config_path, "wb") as f: tomli_w.dump(self.model_dump(), f) ``` ### 4.3 解析策略接口 (`parsing/base.py`) ```python from abc import ABC, abstractmethod from typing import List, Tuple, Dict, Any from ..core.entities import Symbol, FileInfo class ParsingStrategy(ABC): """语言解析策略基类""" @abstractmethod def get_language_name(self) -> str: """返回语言名称""" pass @abstractmethod def get_supported_extensions(self) -> List[str]: """返回支持的文件扩展名""" pass @abstractmethod def parse_file( self, file_path: str, content: str ) -> Tuple[List[Symbol], FileInfo, List[Dict[str, Any]]]: """ 解析文件 Returns: - symbols: 提取的符号列表 - file_info: 文件信息 - pending_calls: 待解析的调用关系 """ pass def supports_file(self, file_path: str) -> bool: """检查是否支持该文件""" ext = file_path.rsplit(".", 1)[-1] if "." in file_path else "" return f".{ext}" in self.get_supported_extensions() ``` ### 4.4 索引引擎 (`engine/indexer.py`) ```python import hashlib from pathlib import Path from typing import List, Optional, Generator from concurrent.futures import ThreadPoolExecutor, as_completed from ..core.config import ProjectConfig from ..core.entities import Symbol, FileInfo, Relation from ..parsing.factory import ParserFactory from ..storage.sqlite_store import SQLiteStore from ..storage.file_cache import FileCache from ..utils.git import get_git_files class Indexer: """索引引擎""" def __init__(self, config: ProjectConfig): self.config = config self.store = SQLiteStore(config.project_root / ".codexlens" / "index.db") self.cache = FileCache(config.project_root / ".codexlens" / "cache.json") self.parser_factory = ParserFactory() def build_index(self, incremental: bool = True) -> dict: """ 构建索引 Args: incremental: 是否增量索引 Returns: 索引统计信息 """ stats = { "files_scanned": 0, "files_indexed": 0, "files_skipped": 0, "symbols_extracted": 0, "relations_resolved": 0, "errors": [] } # 1. 发现文件 files = list(self._discover_files()) stats["files_scanned"] = len(files) # 2. 过滤需要重新索引的文件 if incremental: files = self._filter_changed_files(files) # 3. 并行解析 pending_calls = [] with ThreadPoolExecutor(max_workers=4) as executor: futures = { executor.submit(self._parse_file, f): f for f in files } for future in as_completed(futures): file_path = futures[future] try: symbols, file_info, calls = future.result() # 存储文件信息 self.store.upsert_file(file_info) # 存储符号 for symbol in symbols: self.store.upsert_symbol(symbol) stats["symbols_extracted"] += 1 # 收集待解析调用 pending_calls.extend(calls) # 更新缓存 self.cache.update(file_path, file_info.hash) stats["files_indexed"] += 1 except Exception as e: stats["errors"].append({ "file": file_path, "error": str(e) }) # 4. 解析调用关系 stats["relations_resolved"] = self._resolve_calls(pending_calls) # 5. 保存缓存 self.cache.save() return stats def _discover_files(self) -> Generator[str, None, None]: """发现项目文件""" # 优先使用 git ls-files git_files = get_git_files(self.config.project_root) if git_files: for f in git_files: if self._should_include(f): yield f else: # 回退到 glob for pattern in self.config.index.include_patterns: for f in self.config.project_root.glob(pattern): if self._should_include(str(f)): yield str(f.relative_to(self.config.project_root)) def _should_include(self, file_path: str) -> bool: """检查文件是否应该被索引""" from fnmatch import fnmatch for pattern in self.config.index.exclude_patterns: if fnmatch(file_path, pattern): return False return True def _filter_changed_files(self, files: List[str]) -> List[str]: """过滤出变更的文件""" changed = [] for f in files: full_path = self.config.project_root / f current_hash = self._compute_hash(full_path) cached_hash = self.cache.get(f) if current_hash != cached_hash: changed.append(f) return changed def _compute_hash(self, file_path: Path) -> str: """计算文件哈希""" with open(file_path, "rb") as f: return hashlib.md5(f.read()).hexdigest() def _parse_file(self, file_path: str): """解析单个文件""" full_path = self.config.project_root / file_path content = full_path.read_text(encoding="utf-8", errors="ignore") parser = self.parser_factory.get_parser(file_path) return parser.parse_file(file_path, content) def _resolve_calls(self, pending_calls: List[dict]) -> int: """解析调用关系""" resolved = 0 for call in pending_calls: caller_id = call["caller_id"] callee_name = call["callee_name"] # 查找被调用符号 callee = self.store.find_symbol_by_name(callee_name) if callee: relation = Relation( source_id=caller_id, target_id=callee.id, relation_type="calls" ) self.store.upsert_relation(relation) resolved += 1 return resolved ``` --- ## 5. CCW 集成设计 ### 5.1 JSON 输出协议 所有 CLI 命令使用 `--json` 标志输出标准化 JSON。 **成功响应**: ```json { "success": true, "data": { "results": [...], "metadata": { "count": 10, "elapsed_ms": 45, "mode": "exact" } } } ``` **错误响应**: ```json { "success": false, "error": { "code": "INDEX_NOT_FOUND", "message": "Project not initialized. Run 'codexlens init' first.", "suggestion": "codexlens init /path/to/project" } } ``` ### 5.2 CCW 工具包装器 (`ccw/src/tools/codex-lens.js`) ```javascript /** * CodexLens Tool - Code Intelligence Integration for CCW * * Provides: * - Symbol search and navigation * - Semantic code search * - Dependency graph analysis * - File inspection */ import { spawn } from 'child_process'; import { existsSync } from 'fs'; import { resolve } from 'path'; // CodexLens binary path (configurable) const CODEXLENS_BIN = process.env.CODEXLENS_BIN || 'codexlens'; /** * Execute CodexLens CLI command * @param {string[]} args - Command arguments * @param {string} cwd - Working directory * @returns {Promise} - Parsed JSON result */ async function execCodexLens(args, cwd = process.cwd()) { return new Promise((resolve, reject) => { const child = spawn(CODEXLENS_BIN, [...args, '--json'], { cwd, stdio: ['ignore', 'pipe', 'pipe'] }); let stdout = ''; let stderr = ''; child.stdout.on('data', (data) => { stdout += data.toString(); }); child.stderr.on('data', (data) => { stderr += data.toString(); }); child.on('close', (code) => { try { const result = JSON.parse(stdout); resolve(result); } catch (err) { reject(new Error(`Failed to parse CodexLens output: ${stderr || stdout}`)); } }); child.on('error', (err) => { reject(new Error(`Failed to execute CodexLens: ${err.message}`)); }); }); } /** * Main execute function */ async function execute(params) { const { command, query, path, mode = 'auto', limit = 50, contextLines = 2, includeRelations = false, projectPath = process.cwd() } = params; // Validate command const validCommands = ['search', 'find', 'symbol', 'inspect', 'graph', 'semantic', 'status', 'init']; if (!validCommands.includes(command)) { throw new Error(`Invalid command: ${command}. Valid: ${validCommands.join(', ')}`); } // Build arguments based on command const args = [command]; switch (command) { case 'init': args.push(projectPath); break; case 'search': if (!query) throw new Error('Parameter "query" required for search'); args.push(query); if (path) args.push('--path', path); args.push('--context', contextLines.toString()); args.push('--limit', limit.toString()); break; case 'find': if (!query) throw new Error('Parameter "query" (glob pattern) required for find'); args.push(query); args.push('--limit', limit.toString()); break; case 'symbol': if (!query) throw new Error('Parameter "query" (symbol name) required'); args.push(query); args.push('--mode', mode); // exact, fuzzy args.push('--limit', limit.toString()); if (includeRelations) args.push('--relations'); break; case 'inspect': if (!path) throw new Error('Parameter "path" required for inspect'); args.push(path); break; case 'graph': if (!query) throw new Error('Parameter "query" (symbol name) required for graph'); args.push(query); args.push('--depth', (params.depth || 2).toString()); args.push('--direction', params.direction || 'both'); // callers, callees, both break; case 'semantic': if (!query) throw new Error('Parameter "query" required for semantic search'); args.push(query); args.push('--limit', limit.toString()); break; case 'status': // No additional args break; } // Execute command const result = await execCodexLens(args, projectPath); // Transform result for CCW consumption return { command, ...result, metadata: { ...result.metadata, tool: 'codex_lens', projectPath } }; } /** * Tool Definition for CCW Registry */ export const codexLensTool = { name: 'codex_lens', description: `Code intelligence tool for symbol search, semantic search, and dependency analysis. Commands: - init: Initialize project index - search: Text/regex code search (ripgrep backend) - find: File path search (glob patterns) - symbol: Symbol name lookup with optional relations - inspect: Get file/symbol details - graph: Dependency graph traversal - semantic: Natural language code search - status: Index status and statistics Examples: - Search for function: codex_lens symbol "handleRequest" - Find files: codex_lens find "**/*.test.ts" - Semantic search: codex_lens semantic "authentication middleware" - Get callers: codex_lens graph "UserService.login" --direction callers`, parameters: { type: 'object', properties: { command: { type: 'string', enum: ['init', 'search', 'find', 'symbol', 'inspect', 'graph', 'semantic', 'status'], description: 'CodexLens command to execute' }, query: { type: 'string', description: 'Search query (text, pattern, or natural language)' }, path: { type: 'string', description: 'File path or glob pattern' }, mode: { type: 'string', enum: ['exact', 'fuzzy', 'regex'], description: 'Search mode (default: exact)', default: 'exact' }, limit: { type: 'number', description: 'Maximum results (default: 50)', default: 50 }, contextLines: { type: 'number', description: 'Context lines around matches (default: 2)', default: 2 }, depth: { type: 'number', description: 'Graph traversal depth (default: 2)', default: 2 }, direction: { type: 'string', enum: ['callers', 'callees', 'both'], description: 'Graph direction (default: both)', default: 'both' }, includeRelations: { type: 'boolean', description: 'Include symbol relations in results', default: false }, projectPath: { type: 'string', description: 'Project root path (default: cwd)' } }, required: ['command'] }, execute }; ``` ### 5.3 注册到 CCW 在 `ccw/src/tools/index.js` 中添加: ```javascript import { codexLensTool } from './codex-lens.js'; // ... 现有 imports ... // Register CodexLens tool registerTool(codexLensTool); ``` --- ## 6. 数据存储设计 ### 6.1 SQLite Schema ```sql -- 版本控制 CREATE TABLE IF NOT EXISTS schema_version ( version INTEGER PRIMARY KEY, applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 文件表 CREATE TABLE IF NOT EXISTS files ( id INTEGER PRIMARY KEY AUTOINCREMENT, path TEXT UNIQUE NOT NULL, language TEXT NOT NULL, line_count INTEGER DEFAULT 0, hash TEXT NOT NULL, imports TEXT DEFAULT '[]', -- JSON array exports TEXT DEFAULT '[]', -- JSON array indexed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_files_language (language), INDEX idx_files_hash (hash) ); -- 符号表 CREATE TABLE IF NOT EXISTS symbols ( id INTEGER PRIMARY KEY AUTOINCREMENT, symbol_id TEXT UNIQUE NOT NULL, -- file_path::name file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE, name TEXT NOT NULL, short_name TEXT NOT NULL, -- 用于模糊搜索 type TEXT NOT NULL, -- function, class, method, etc. line_start INTEGER NOT NULL, line_end INTEGER NOT NULL, column_start INTEGER DEFAULT 0, column_end INTEGER DEFAULT 0, signature TEXT, docstring TEXT, language TEXT NOT NULL, metadata TEXT DEFAULT '{}', -- JSON object INDEX idx_symbols_name (name), INDEX idx_symbols_short_name (short_name), INDEX idx_symbols_type (type), INDEX idx_symbols_file_id (file_id) ); -- 关系表 CREATE TABLE IF NOT EXISTS relations ( id INTEGER PRIMARY KEY AUTOINCREMENT, source_id TEXT NOT NULL, -- symbol_id target_id TEXT NOT NULL, -- symbol_id relation_type TEXT NOT NULL, -- calls, imports, extends, etc. metadata TEXT DEFAULT '{}', -- JSON object UNIQUE(source_id, target_id, relation_type), INDEX idx_relations_source (source_id), INDEX idx_relations_target (target_id), INDEX idx_relations_type (relation_type) ); -- FTS5 全文搜索索引 CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5( symbol_id, name, short_name, signature, docstring, content='symbols', content_rowid='id' ); -- 触发器:保持 FTS 索引同步 CREATE TRIGGER symbols_ai AFTER INSERT ON symbols BEGIN INSERT INTO symbols_fts(rowid, symbol_id, name, short_name, signature, docstring) VALUES (new.id, new.symbol_id, new.name, new.short_name, new.signature, new.docstring); END; CREATE TRIGGER symbols_ad AFTER DELETE ON symbols BEGIN INSERT INTO symbols_fts(symbols_fts, rowid, symbol_id, name, short_name, signature, docstring) VALUES('delete', old.id, old.symbol_id, old.name, old.short_name, old.signature, old.docstring); END; CREATE TRIGGER symbols_au AFTER UPDATE ON symbols BEGIN INSERT INTO symbols_fts(symbols_fts, rowid, symbol_id, name, short_name, signature, docstring) VALUES('delete', old.id, old.symbol_id, old.name, old.short_name, old.signature, old.docstring); INSERT INTO symbols_fts(rowid, symbol_id, name, short_name, signature, docstring) VALUES (new.id, new.symbol_id, new.name, new.short_name, new.signature, new.docstring); END; ``` ### 6.2 SQLite Store 实现 (`storage/sqlite_store.py`) ```python import sqlite3 import json from pathlib import Path from typing import List, Optional from contextlib import contextmanager from ..core.entities import Symbol, FileInfo, Relation, SymbolType SCHEMA_VERSION = 1 class SQLiteStore: """SQLite 存储管理器""" def __init__(self, db_path: Path): self.db_path = db_path self.db_path.parent.mkdir(parents=True, exist_ok=True) self._init_schema() @contextmanager def _connection(self): """获取数据库连接""" conn = sqlite3.connect(self.db_path) conn.row_factory = sqlite3.Row conn.execute("PRAGMA foreign_keys = ON") conn.execute("PRAGMA journal_mode = WAL") try: yield conn conn.commit() finally: conn.close() def _init_schema(self): """初始化数据库 schema""" with self._connection() as conn: # 检查版本 conn.execute(""" CREATE TABLE IF NOT EXISTS schema_version ( version INTEGER PRIMARY KEY ) """) row = conn.execute("SELECT version FROM schema_version").fetchone() current_version = row["version"] if row else 0 if current_version < SCHEMA_VERSION: self._apply_schema(conn) conn.execute( "INSERT OR REPLACE INTO schema_version (version) VALUES (?)", (SCHEMA_VERSION,) ) def _apply_schema(self, conn): """应用 schema""" conn.executescript(""" CREATE TABLE IF NOT EXISTS files ( id INTEGER PRIMARY KEY AUTOINCREMENT, path TEXT UNIQUE NOT NULL, language TEXT NOT NULL, line_count INTEGER DEFAULT 0, hash TEXT NOT NULL, imports TEXT DEFAULT '[]', exports TEXT DEFAULT '[]', indexed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS symbols ( id INTEGER PRIMARY KEY AUTOINCREMENT, symbol_id TEXT UNIQUE NOT NULL, file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE, name TEXT NOT NULL, short_name TEXT NOT NULL, type TEXT NOT NULL, line_start INTEGER NOT NULL, line_end INTEGER NOT NULL, column_start INTEGER DEFAULT 0, column_end INTEGER DEFAULT 0, signature TEXT, docstring TEXT, language TEXT NOT NULL, metadata TEXT DEFAULT '{}' ); CREATE TABLE IF NOT EXISTS relations ( id INTEGER PRIMARY KEY AUTOINCREMENT, source_id TEXT NOT NULL, target_id TEXT NOT NULL, relation_type TEXT NOT NULL, metadata TEXT DEFAULT '{}', UNIQUE(source_id, target_id, relation_type) ); CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name); CREATE INDEX IF NOT EXISTS idx_symbols_short_name ON symbols(short_name); CREATE INDEX IF NOT EXISTS idx_symbols_type ON symbols(type); CREATE INDEX IF NOT EXISTS idx_relations_source ON relations(source_id); CREATE INDEX IF NOT EXISTS idx_relations_target ON relations(target_id); """) def upsert_file(self, file_info: FileInfo) -> int: """插入或更新文件""" with self._connection() as conn: cursor = conn.execute(""" INSERT INTO files (path, language, line_count, hash, imports, exports) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(path) DO UPDATE SET language = excluded.language, line_count = excluded.line_count, hash = excluded.hash, imports = excluded.imports, exports = excluded.exports, indexed_at = CURRENT_TIMESTAMP RETURNING id """, ( file_info.path, file_info.language, file_info.line_count, file_info.hash, json.dumps(file_info.imports), json.dumps(file_info.exports) )) return cursor.fetchone()["id"] def upsert_symbol(self, symbol: Symbol) -> int: """插入或更新符号""" with self._connection() as conn: # 获取 file_id file_row = conn.execute( "SELECT id FROM files WHERE path = ?", (symbol.location.file_path,) ).fetchone() if not file_row: raise ValueError(f"File not found: {symbol.location.file_path}") cursor = conn.execute(""" INSERT INTO symbols ( symbol_id, file_id, name, short_name, type, line_start, line_end, column_start, column_end, signature, docstring, language, metadata ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(symbol_id) DO UPDATE SET name = excluded.name, short_name = excluded.short_name, type = excluded.type, line_start = excluded.line_start, line_end = excluded.line_end, signature = excluded.signature, docstring = excluded.docstring, metadata = excluded.metadata RETURNING id """, ( symbol.id, file_row["id"], symbol.name, symbol.short_name, symbol.type.value, symbol.location.line_start, symbol.location.line_end, symbol.location.column_start, symbol.location.column_end, symbol.signature, symbol.docstring, symbol.language, json.dumps(symbol.metadata) )) return cursor.fetchone()["id"] def find_symbol_by_name( self, name: str, exact: bool = False ) -> Optional[Symbol]: """按名称查找符号""" with self._connection() as conn: if exact: row = conn.execute( "SELECT * FROM symbols WHERE name = ?", (name,) ).fetchone() else: row = conn.execute( "SELECT * FROM symbols WHERE short_name LIKE ?", (f"%{name}%",) ).fetchone() return self._row_to_symbol(row) if row else None def search_symbols( self, query: str, limit: int = 50 ) -> List[Symbol]: """搜索符号""" with self._connection() as conn: rows = conn.execute(""" SELECT * FROM symbols WHERE name LIKE ? OR short_name LIKE ? OR signature LIKE ? LIMIT ? """, (f"%{query}%", f"%{query}%", f"%{query}%", limit)).fetchall() return [self._row_to_symbol(row) for row in rows] def get_relations( self, symbol_id: str, direction: str = "both" ) -> List[Relation]: """获取符号关系""" with self._connection() as conn: relations = [] if direction in ("both", "outgoing"): rows = conn.execute( "SELECT * FROM relations WHERE source_id = ?", (symbol_id,) ).fetchall() relations.extend([self._row_to_relation(r) for r in rows]) if direction in ("both", "incoming"): rows = conn.execute( "SELECT * FROM relations WHERE target_id = ?", (symbol_id,) ).fetchall() relations.extend([self._row_to_relation(r) for r in rows]) return relations def get_stats(self) -> dict: """获取索引统计""" with self._connection() as conn: file_count = conn.execute("SELECT COUNT(*) FROM files").fetchone()[0] symbol_count = conn.execute("SELECT COUNT(*) FROM symbols").fetchone()[0] relation_count = conn.execute("SELECT COUNT(*) FROM relations").fetchone()[0] languages = conn.execute(""" SELECT language, COUNT(*) as count FROM files GROUP BY language """).fetchall() return { "files": file_count, "symbols": symbol_count, "relations": relation_count, "languages": {r["language"]: r["count"] for r in languages} } def _row_to_symbol(self, row) -> Symbol: """将数据库行转换为 Symbol""" return Symbol( id=row["symbol_id"], name=row["name"], short_name=row["short_name"], type=SymbolType(row["type"]), location=Location( file_path=row["path"] if "path" in row.keys() else "", line_start=row["line_start"], line_end=row["line_end"], column_start=row["column_start"], column_end=row["column_end"] ), signature=row["signature"], docstring=row["docstring"], language=row["language"], metadata=json.loads(row["metadata"]) ) def _row_to_relation(self, row) -> Relation: """将数据库行转换为 Relation""" return Relation( source_id=row["source_id"], target_id=row["target_id"], relation_type=row["relation_type"], metadata=json.loads(row["metadata"]) ) ``` --- ## 7. 语义搜索架构 ### 7.1 嵌入生成器 (`semantic/embedder.py`) ```python from typing import List, Optional from functools import lru_cache class SemanticEmbedder: """语义嵌入生成器 (懒加载)""" def __init__(self, model_name: str = "all-MiniLM-L6-v2"): self.model_name = model_name self._model = None @property def model(self): """懒加载模型""" if self._model is None: from sentence_transformers import SentenceTransformer self._model = SentenceTransformer(self.model_name) return self._model def embed(self, text: str) -> List[float]: """生成单个文本的嵌入""" return self.model.encode(text).tolist() def embed_batch(self, texts: List[str]) -> List[List[float]]: """批量生成嵌入""" return self.model.encode(texts).tolist() def embed_symbol(self, symbol) -> List[float]: """为符号生成嵌入""" text = self._build_semantic_text(symbol) return self.embed(text) def _build_semantic_text(self, symbol) -> str: """构建符号的语义文本""" parts = [ f"[{symbol.type.value}] {symbol.name}", ] if symbol.signature: parts.append(f"Signature: {symbol.signature}") if symbol.docstring: parts.append(f"Description: {symbol.docstring}") return "\n".join(parts) ``` ### 7.2 向量存储 (`semantic/vector_store.py`) ```python from typing import List, Dict, Any, Optional from pathlib import Path class VectorStore: """ChromaDB 向量存储适配器""" def __init__(self, persist_dir: Path): self.persist_dir = persist_dir self._client = None self._collection = None @property def client(self): """懒加载 ChromaDB 客户端""" if self._client is None: import chromadb self._client = chromadb.PersistentClient( path=str(self.persist_dir) ) return self._client @property def collection(self): """获取或创建集合""" if self._collection is None: self._collection = self.client.get_or_create_collection( name="codexlens_symbols", metadata={"hnsw:space": "cosine"} ) return self._collection def upsert( self, id: str, embedding: List[float], metadata: Dict[str, Any], document: str = "" ): """插入或更新向量""" self.collection.upsert( ids=[id], embeddings=[embedding], metadatas=[metadata], documents=[document] ) def upsert_batch( self, ids: List[str], embeddings: List[List[float]], metadatas: List[Dict[str, Any]], documents: List[str] = None ): """批量插入""" self.collection.upsert( ids=ids, embeddings=embeddings, metadatas=metadatas, documents=documents or [""] * len(ids) ) def search( self, query_embedding: List[float], limit: int = 10, where: Optional[Dict] = None ) -> List[Dict]: """向量相似度搜索""" results = self.collection.query( query_embeddings=[query_embedding], n_results=limit, where=where, include=["metadatas", "distances", "documents"] ) # 转换为统一格式 items = [] for i in range(len(results["ids"][0])): items.append({ "id": results["ids"][0][i], "metadata": results["metadatas"][0][i], "distance": results["distances"][0][i], "document": results["documents"][0][i] if results["documents"] else "" }) return items def delete(self, ids: List[str]): """删除向量""" self.collection.delete(ids=ids) def count(self) -> int: """获取向量数量""" return self.collection.count() ``` --- ## 8. CLI 命令设计 ### 8.1 主入口 (`cli/main.py`) ```python import typer from typing import Optional from pathlib import Path from .commands import init, search, find, symbol, inspect, graph, semantic, status from .output import JSONOutput app = typer.Typer( name="codexlens", help="Code intelligence tool for symbol search, semantic search, and dependency analysis.", add_completion=False ) # 全局选项 json_option = typer.Option(False, "--json", "-j", help="Output as JSON") project_option = typer.Option(None, "--project", "-p", help="Project root path") # 注册子命令 app.command()(init.command) app.command()(search.command) app.command()(find.command) app.command()(symbol.command) app.command()(inspect.command) app.command()(graph.command) app.command()(semantic.command) app.command()(status.command) def main(): app() if __name__ == "__main__": main() ``` ### 8.2 输出格式化 (`cli/output.py`) ```python import json import sys from typing import Any, Dict, List, Optional from dataclasses import dataclass, asdict @dataclass class CLIResponse: """CLI 响应结构""" success: bool data: Optional[Dict[str, Any]] = None error: Optional[Dict[str, str]] = None def to_json(self) -> str: """转换为 JSON 字符串""" result = {"success": self.success} if self.data: result["data"] = self.data if self.error: result["error"] = self.error return json.dumps(result, indent=2, ensure_ascii=False) def print(self, as_json: bool = False): """输出结果""" if as_json: print(self.to_json()) else: self._print_human_readable() def _print_human_readable(self): """人类可读格式输出""" if not self.success: print(f"Error: {self.error.get('message', 'Unknown error')}", file=sys.stderr) if suggestion := self.error.get('suggestion'): print(f"Suggestion: {suggestion}", file=sys.stderr) return if not self.data: print("No results") return # 根据数据类型格式化输出 if "results" in self.data: for item in self.data["results"]: self._print_result_item(item) elif "stats" in self.data: self._print_stats(self.data["stats"]) else: print(json.dumps(self.data, indent=2)) def _print_result_item(self, item: Dict): """打印单个结果项""" if "file_path" in item and "line" in item: # 搜索结果 print(f"{item['file_path']}:{item['line']}") if "content" in item: print(f" {item['content']}") elif "symbol_id" in item: # 符号结果 print(f"{item['type']}: {item['name']}") print(f" Location: {item['file_path']}:{item['line_start']}") if item.get("signature"): print(f" Signature: {item['signature']}") print() def _print_stats(self, stats: Dict): """打印统计信息""" print("Index Statistics:") print(f" Files: {stats.get('files', 0)}") print(f" Symbols: {stats.get('symbols', 0)}") print(f" Relations: {stats.get('relations', 0)}") if languages := stats.get("languages"): print(" Languages:") for lang, count in languages.items(): print(f" {lang}: {count}") def success(data: Dict[str, Any]) -> CLIResponse: """创建成功响应""" return CLIResponse(success=True, data=data) def error(code: str, message: str, suggestion: str = None) -> CLIResponse: """创建错误响应""" err = {"code": code, "message": message} if suggestion: err["suggestion"] = suggestion return CLIResponse(success=False, error=err) ``` ### 8.3 命令示例: search (`cli/commands/search.py`) ```python import typer from typing import Optional, List from pathlib import Path import time from ..output import success, error, CLIResponse from ...engine.searcher import Searcher from ...core.config import ProjectConfig from ...utils.ripgrep import ripgrep_search def command( query: str = typer.Argument(..., help="Search query (text or regex)"), path: Optional[str] = typer.Option(None, "--path", "-p", help="Path filter (glob)"), regex: bool = typer.Option(False, "--regex", "-r", help="Treat query as regex"), context: int = typer.Option(2, "--context", "-C", help="Context lines"), limit: int = typer.Option(50, "--limit", "-l", help="Max results"), json_output: bool = typer.Option(False, "--json", "-j", help="JSON output"), project: Optional[Path] = typer.Option(None, "--project", help="Project root") ): """ Search code content using text or regex patterns. Uses ripgrep for fast searching with optional context lines. Examples: codexlens search "handleRequest" codexlens search "def.*test" --regex codexlens search "TODO" --path "**/*.py" """ start_time = time.time() try: # 确定项目根目录 project_root = project or Path.cwd() # 检查项目是否已初始化 config_path = project_root / ".codexlens" / "config.toml" if not config_path.exists(): response = error( "PROJECT_NOT_INITIALIZED", "Project not initialized", f"Run: codexlens init {project_root}" ) response.print(json_output) raise typer.Exit(1) # 执行搜索 results = ripgrep_search( query=query, path=project_root, pattern_filter=path, is_regex=regex, context_lines=context, max_results=limit ) elapsed_ms = int((time.time() - start_time) * 1000) response = success({ "results": results, "metadata": { "query": query, "mode": "regex" if regex else "literal", "count": len(results), "elapsed_ms": elapsed_ms } }) response.print(json_output) except Exception as e: response = error("SEARCH_FAILED", str(e)) response.print(json_output) raise typer.Exit(1) ``` ### 8.4 命令示例: symbol (`cli/commands/symbol.py`) ```python import typer from typing import Optional from pathlib import Path import time from ..output import success, error from ...storage.sqlite_store import SQLiteStore from ...core.config import ProjectConfig def command( query: str = typer.Argument(..., help="Symbol name to search"), mode: str = typer.Option("fuzzy", "--mode", "-m", help="Search mode: exact, fuzzy"), type_filter: Optional[str] = typer.Option(None, "--type", "-t", help="Filter by type: function, class, method"), limit: int = typer.Option(50, "--limit", "-l", help="Max results"), relations: bool = typer.Option(False, "--relations", "-r", help="Include relations"), json_output: bool = typer.Option(False, "--json", "-j", help="JSON output"), project: Optional[Path] = typer.Option(None, "--project", help="Project root") ): """ Search for code symbols (functions, classes, methods). Examples: codexlens symbol "UserService" codexlens symbol "handle" --mode fuzzy codexlens symbol "test_" --type function """ start_time = time.time() try: project_root = project or Path.cwd() db_path = project_root / ".codexlens" / "index.db" if not db_path.exists(): response = error( "INDEX_NOT_FOUND", "Index not found", f"Run: codexlens init {project_root}" ) response.print(json_output) raise typer.Exit(1) store = SQLiteStore(db_path) # 搜索符号 if mode == "exact": symbol = store.find_symbol_by_name(query, exact=True) symbols = [symbol] if symbol else [] else: symbols = store.search_symbols(query, limit=limit) # 类型过滤 if type_filter: symbols = [s for s in symbols if s.type.value == type_filter] # 构建结果 results = [] for sym in symbols[:limit]: item = { "symbol_id": sym.id, "name": sym.name, "type": sym.type.value, "file_path": sym.location.file_path, "line_start": sym.location.line_start, "line_end": sym.location.line_end, "signature": sym.signature, "docstring": sym.docstring, "language": sym.language } # 包含关系 if relations: rels = store.get_relations(sym.id) item["relations"] = { "callers": [r.source_id for r in rels if r.relation_type == "calls" and r.target_id == sym.id], "callees": [r.target_id for r in rels if r.relation_type == "calls" and r.source_id == sym.id] } results.append(item) elapsed_ms = int((time.time() - start_time) * 1000) response = success({ "results": results, "metadata": { "query": query, "mode": mode, "count": len(results), "elapsed_ms": elapsed_ms } }) response.print(json_output) except Exception as e: response = error("SYMBOL_SEARCH_FAILED", str(e)) response.print(json_output) raise typer.Exit(1) ``` --- ## 9. 开发路线图 ### Phase 1: 基础框架 (Week 1-2) **目标**: 可运行的 CLI 骨架 + 基础搜索 **任务清单**: - [ ] 项目骨架搭建 (pyproject.toml, 目录结构) - [ ] 核心实体定义 (entities.py, config.py) - [ ] CLI 框架 (Typer 集成) - [ ] JSON 输出协议实现 - [ ] ripgrep 包装器 - [ ] `init` 命令实现 - [ ] `search` 命令实现 (ripgrep 后端) - [ ] `find` 命令实现 (glob) - [ ] `status` 命令实现 **里程碑**: `codexlens search "pattern" --json` 可工作 **交付物**: ```bash codexlens init /path/to/project codexlens search "function" --json codexlens find "**/*.py" --json codexlens status --json ``` --- ### Phase 2: 深度索引 (Week 3-4) **目标**: AST 解析 + 符号提取 + SQLite 存储 **任务清单**: - [ ] SQLite 存储层实现 - [ ] 文件哈希缓存 (增量索引) - [ ] Python 解析器 (ast 模块) - [ ] JavaScript/TypeScript 解析器 (tree-sitter) - [ ] Rust 解析器 (tree-sitter) - [ ] 通用回退解析器 - [ ] 解析器工厂 - [ ] 索引引擎编排器 - [ ] `symbol` 命令实现 - [ ] `inspect` 命令实现 **里程碑**: `codexlens symbol "ClassName"` 返回符号详情 **交付物**: ```bash codexlens symbol "handleRequest" --json codexlens inspect src/main.py --json ``` --- ### Phase 3: 关系图谱 (Week 5) **目标**: 调用关系解析 + 图查询 **任务清单**: - [ ] 调用关系提取 (pending_calls 解析) - [ ] 关系存储 (relations 表) - [ ] NetworkX 图构建 - [ ] 图遍历算法 (BFS/DFS) - [ ] `graph` 命令实现 - [ ] 影响分析功能 **里程碑**: `codexlens graph "Symbol" --direction callers` 返回调用链 **交付物**: ```bash codexlens graph "UserService.login" --depth 3 --json codexlens graph "handleError" --direction callees --json ``` --- ### Phase 4: CCW 集成 (Week 6) **目标**: CCW 工具包装器 + 端到端测试 **任务清单**: - [ ] CCW 工具包装器 (codex-lens.js) - [ ] 注册到 CCW 工具系统 - [ ] 参数验证与转换 - [ ] 错误处理与重试 - [ ] 集成测试 - [ ] 文档更新 **里程碑**: `ccw tool exec codex_lens '{"command": "search", "query": "test"}'` 可工作 **交付物**: ```bash ccw tool exec codex_lens '{"command": "symbol", "query": "handleRequest"}' ccw tool list | grep codex_lens ``` --- ### Phase 5: 语义搜索 (Week 7-8) [可选] **目标**: 自然语言代码搜索 **任务清单**: - [ ] sentence-transformers 集成 - [ ] ChromaDB 向量存储 - [ ] 代码分块策略 - [ ] 嵌入生成管道 - [ ] 向量索引构建 - [ ] `semantic` 命令实现 - [ ] 混合搜索 (关键词 + 语义) **里程碑**: `codexlens semantic "authentication logic"` 返回相关代码 **交付物**: ```bash codexlens semantic "user authentication middleware" --json codexlens semantic "error handling" --limit 10 --json ``` --- ### Phase 6: npm 分发 (Week 9) **目标**: npm 包装与分发 **任务清单**: - [ ] PyInstaller 配置 - [ ] 多平台构建 (Windows, macOS, Linux) - [ ] GitHub Actions CI/CD - [ ] npm 包装器 - [ ] 安装脚本 - [ ] 文档与示例 **里程碑**: `npm install -g codexlens` 可工作 --- ## 10. 技术依赖 ### 10.1 核心依赖 ```toml [project] name = "codex-lens" version = "0.1.0" requires-python = ">=3.10" dependencies = [ # CLI 框架 "typer>=0.9.0", "rich>=13.0.0", # 配置 "pydantic>=2.0.0", "tomli>=2.0.0", "tomli-w>=1.0.0", # 代码解析 "tree-sitter>=0.20.0", "tree-sitter-python>=0.20.0", "tree-sitter-javascript>=0.20.0", "tree-sitter-typescript>=0.20.0", "tree-sitter-rust>=0.20.0", # 图分析 "networkx>=3.0", ] [project.optional-dependencies] semantic = [ "sentence-transformers>=2.2.0", "chromadb>=0.4.0", ] dev = [ "pytest>=7.0.0", "pytest-cov>=4.0.0", "mypy>=1.0.0", "ruff>=0.1.0", ] [project.scripts] codexlens = "codex_lens.cli.main:main" ``` ### 10.2 外部工具依赖 | 工具 | 用途 | 安装方式 | |------|------|----------| | ripgrep (rg) | 快速文本搜索 | `scoop install ripgrep` / `brew install ripgrep` | | git | 文件发现 | 系统自带 | --- ## 11. npm 分发策略 ### 11.1 PyInstaller 配置 (`codexlens.spec`) ```python # -*- mode: python ; coding: utf-8 -*- from PyInstaller.utils.hooks import collect_all block_cipher = None # 收集 tree-sitter 语言绑定 datas = [] binaries = [] hiddenimports = [ 'tree_sitter_python', 'tree_sitter_javascript', 'tree_sitter_typescript', 'tree_sitter_rust', ] for pkg in hiddenimports: try: d, b, h = collect_all(pkg) datas += d binaries += b except Exception: pass a = Analysis( ['src/codex_lens/__main__.py'], pathex=['src'], binaries=binaries, datas=datas, hiddenimports=hiddenimports, hookspath=[], hooksconfig={}, runtime_hooks=[], excludes=['tkinter', 'matplotlib'], win_no_prefer_redirects=False, win_private_assemblies=False, cipher=block_cipher, noarchive=False, ) pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) exe = EXE( pyz, a.scripts, a.binaries, a.zipfiles, a.datas, [], name='codexlens', debug=False, bootloader_ignore_signals=False, strip=False, upx=True, upx_exclude=[], runtime_tmpdir=None, console=True, disable_windowed_traceback=False, argv_emulation=False, target_arch=None, codesign_identity=None, entitlements_file=None, ) ``` ### 11.2 GitHub Actions 构建 ```yaml # .github/workflows/build.yml name: Build Binaries on: push: tags: - 'v*' jobs: build: strategy: matrix: include: - os: ubuntu-latest artifact: codexlens-linux-x64 - os: windows-latest artifact: codexlens-win-x64.exe - os: macos-latest artifact: codexlens-macos-x64 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.11' - name: Install dependencies run: | pip install -e ".[dev]" pip install pyinstaller - name: Build binary run: pyinstaller codexlens.spec - name: Rename artifact shell: bash run: | cd dist if [ "${{ runner.os }}" == "Windows" ]; then mv codexlens.exe ../${{ matrix.artifact }} else mv codexlens ../${{ matrix.artifact }} fi - name: Upload artifact uses: actions/upload-artifact@v4 with: name: ${{ matrix.artifact }} path: ${{ matrix.artifact }} release: needs: build runs-on: ubuntu-latest steps: - uses: actions/download-artifact@v4 - name: Create Release uses: softprops/action-gh-release@v1 with: files: | codexlens-linux-x64/codexlens-linux-x64 codexlens-win-x64.exe/codexlens-win-x64.exe codexlens-macos-x64/codexlens-macos-x64 ``` ### 11.3 npm 包结构 ``` npm-codexlens/ ├── package.json ├── bin/ │ └── cli.js └── scripts/ └── install.js ``` **package.json**: ```json { "name": "codexlens", "version": "0.1.0", "description": "Code intelligence tool for symbol search and dependency analysis", "bin": { "codexlens": "bin/cli.js" }, "scripts": { "postinstall": "node scripts/install.js" }, "repository": { "type": "git", "url": "https://github.com/user/codex-lens.git" }, "os": ["darwin", "linux", "win32"], "cpu": ["x64", "arm64"] } ``` --- ## 附录 A: 命令速查表 | 命令 | 描述 | 示例 | |------|------|------| | `init` | 初始化项目索引 | `codexlens init .` | | `search` | 文本/正则搜索 | `codexlens search "TODO" --path "**/*.py"` | | `find` | 文件查找 | `codexlens find "**/*.test.ts"` | | `symbol` | 符号查找 | `codexlens symbol "handleRequest" --relations` | | `inspect` | 文件/符号详情 | `codexlens inspect src/main.py` | | `graph` | 调用关系图 | `codexlens graph "UserService" --depth 3` | | `semantic` | 语义搜索 | `codexlens semantic "authentication logic"` | | `status` | 索引状态 | `codexlens status` | --- ## 附录 B: CCW 调用示例 ```bash # 初始化项目 ccw tool exec codex_lens '{"command": "init", "projectPath": "/path/to/project"}' # 搜索代码 ccw tool exec codex_lens '{"command": "search", "query": "handleRequest", "limit": 20}' # 查找符号 ccw tool exec codex_lens '{"command": "symbol", "query": "UserService", "includeRelations": true}' # 获取调用图 ccw tool exec codex_lens '{"command": "graph", "query": "login", "depth": 2, "direction": "callers"}' # 语义搜索 ccw tool exec codex_lens '{"command": "semantic", "query": "user authentication middleware"}' ``` --- *文档版本: 1.0.0* *最后更新: 2024-12*