diff --git a/.deepcode/effort_router.py b/.deepcode/effort_router.py new file mode 100644 index 00000000..1b8ad132 --- /dev/null +++ b/.deepcode/effort_router.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +""" +[DEPRECATED] Effort Router — 请改用 router-mcp MCP 服务器 +═══════════════════════════════════════════════════════════ +⚠️ 此模块已废弃,功能已由 `router-mcp` MCP 服务器完全覆盖。 + 请使用 `router-mcp` 的 router_query 工具代替。 + +替代方案: + router-mcp 的 router_query(query, force_route="complex") + 自动处理模型选择 + Token 预算 + 推理参数。 + +历史原因: + 本模块基于 Claude Code 逆向分析开发,但后来发现 + core/mcp_servers/router_mcp_server.py 已经提供了 + 更完整的模型路由功能(支持 auto-simple/complex/chan_theory/data_query)。 + +保留原因: + 部分测试代码和 auto_fixes 管线引用了本模块的 route_effort() 函数, + 但新代码应直接使用 router-mcp。 +""" + +import json +from pathlib import Path +from typing import Dict, Tuple, Optional + +SETTINGS_PATH = Path(__file__).parent.parent / "settings.json" + +DEFAULT_EFFORT_CONFIG = { + "default_level": "medium", + "levels": { + "low": { + "model": "deepseek-v4-flash", + "max_tokens": 2048, + "temperature": 0.3, + "description": "简单问答、CLI 命令、文件操作", + }, + "medium": { + "model": "deepseek-v4-flash", + "max_tokens": 4096, + "temperature": 0.6, + "description": "日常编程、代码审查、文档生成", + }, + "high": { + "model": "deepseek-v4-pro", + "max_tokens": 8192, + "temperature": 0.5, + "description": "复杂分析、架构设计、量化策略", + }, + "xhigh": { + "model": "deepseek-r1", + "max_tokens": 16384, + "temperature": 0.3, + "description": "深度推理、数学证明、算法优化", + }, + "max": { + "model": "ensemble", + "max_tokens": 32768, + "temperature": 0.4, + "description": "6模型集成投票 (关键决策)", + "ensemble_models": [ + "deepseek-v4-pro", + "glm-5.2", + "qwen3.5-122b", + "minimax-m3", + "mistral-large", + "stockmark-100b", + ], + }, + }, +} + + +def load_effort_config() -> dict: + """加载 effort 配置,缺失时使用默认值""" + if SETTINGS_PATH.exists(): + raw = json.loads(SETTINGS_PATH.read_text(encoding="utf-8")) + cfg = raw.get("effort", {}) + merged = DEFAULT_EFFORT_CONFIG.copy() + if "default_level" in cfg: + merged["default_level"] = cfg["default_level"] + if "levels" in cfg: + for lv, lv_cfg in cfg["levels"].items(): + if lv in merged["levels"]: + merged["levels"][lv].update(lv_cfg) + else: + merged["levels"][lv] = lv_cfg + return merged + return DEFAULT_EFFORT_CONFIG + + +def route_effort(level: Optional[str] = None) -> Tuple[str, int, Dict]: + """ + 路由 effort level → (model, max_tokens, params) + + Args: + level: low/medium/high/xhigh/max, None = default + + Returns: + (model_name, max_tokens, extra_params) + """ + cfg = load_effort_config() + lv = level or cfg["default_level"] + + if lv not in cfg["levels"]: + print(f"[effort_router] Unknown level '{lv}', falling back to medium") + lv = "medium" + + lv_cfg = cfg["levels"][lv] + model = lv_cfg["model"] + max_tokens = lv_cfg["max_tokens"] + params = { + "temperature": lv_cfg.get("temperature", 0.6), + "effort": lv, + } + + if model == "ensemble" and "ensemble_models" in lv_cfg: + params["ensemble_models"] = lv_cfg["ensemble_models"] + + return model, max_tokens, params + + +def list_levels() -> str: + """列出所有 effort level 及对应配置""" + cfg = load_effort_config() + lines = [ + f"{'Level':<8} {'Model':<22} {'Tokens':<8} Description", + "-" * 70, + ] + for lv, lv_cfg in cfg["levels"].items(): + marker = " *" if lv == cfg["default_level"] else " " + lines.append( + f"{marker}{lv:<6} {lv_cfg['model']:<22} {lv_cfg['max_tokens']:<8} " + f"{lv_cfg['description']}" + ) + return "\n".join(lines) + + +# ── CLI 入口 ── +if __name__ == "__main__": + import sys + + if len(sys.argv) > 1: + level = sys.argv[1] + model, budget, params = route_effort(level) + print(json.dumps({ + "level": level, + "model": model, + "max_tokens": budget, + "params": params, + }, indent=2)) + else: + print(list_levels()) diff --git a/.deepcode/patches/deepcode-lineguard.js b/.deepcode/patches/deepcode-lineguard.js new file mode 100644 index 00000000..7227e55b --- /dev/null +++ b/.deepcode/patches/deepcode-lineguard.js @@ -0,0 +1,98 @@ +/** + * deepcode-lineguard.js + * Monkey-patch Node.js readline.createInterface to prevent RangeError + * from oversized single-line stdout (e.g. MCP tool returning 100MB+ JSON). + * + * Usage: NODE_OPTIONS="--require /path/to/deepcode-lineguard.js" deepcode + */ + +'use strict'; + +const { Transform } = require('stream'); +const _readline = require('readline'); + +// 10MB max per line — well below V8's ~268MB string limit +const MAX_LINE_LENGTH = 10 * 1024 * 1024; + +const _origCreateInterface = _readline.createInterface; + +_readline.createInterface = function (opts) { + if (opts && opts.input && !opts._lineguardInstalled) { + const originalInput = opts.input; + let pending = ''; + + const lineGuard = new Transform({ + decodeStrings: false, + encoding: 'utf8', + + transform(chunk, _encoding, callback) { + // Convert to string (handles both Buffer and string chunks) + const str = typeof chunk === 'string' ? chunk : chunk.toString('utf8'); + + // Emit safe-chunked output: split on newlines, cap each segment + const parts = (pending + str).split('\n'); + // The last part might be incomplete — hold it for next chunk + pending = parts.pop(); + + for (let i = 0; i < parts.length; i++) { + let segment = parts[i]; + if (segment.length > MAX_LINE_LENGTH) { + const mb = (segment.length / 1024 / 1024).toFixed(1); + segment = JSON.stringify({ + error: `[LINE_TRUNCATED] ${mb}MB single-line response was truncated to prevent Node.js readline overflow.`, + original_size: segment.length, + truncated: true, + }); + } + this.push(segment); + if (i < parts.length - 1) { + this.push('\n'); + } else { + // Last part gets a newline (we already split on \n so add it back) + this.push('\n'); + } + } + + // Flush any remaining pending if it's getting too large + if (pending.length > MAX_LINE_LENGTH) { + const mb = (pending.length / 1024 / 1024).toFixed(1); + this.push(JSON.stringify({ + error: `[LINE_TRUNCATED] ${mb}MB single-line response was truncated to prevent Node.js readline overflow.`, + original_size: pending.length, + truncated: true, + })); + this.push('\n'); + pending = ''; + } + + callback(); + }, + + flush(callback) { + if (pending.length > 0) { + if (pending.length > MAX_LINE_LENGTH) { + const mb = (pending.length / 1024 / 1024).toFixed(1); + this.push(JSON.stringify({ + error: `[LINE_TRUNCATED] ${mb}MB single-line response was truncated.`, + original_size: pending.length, + truncated: true, + })); + } else { + this.push(pending); + } + this.push('\n'); + } + callback(); + }, + }); + + // Tag to prevent double-wrapping if createInterface is called multiple times + opts._lineguardInstalled = true; + opts.input = originalInput.pipe(lineGuard); + } + + return _origCreateInterface.call(this, opts); +}; + +// Also patch the maxListeners to avoid warnings for many MCP servers +process.setMaxListeners(50); diff --git a/.deepcode/permission_gate.py b/.deepcode/permission_gate.py new file mode 100644 index 00000000..19fc7f8b --- /dev/null +++ b/.deepcode/permission_gate.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python3 +""" +Permission Gate — Claude Code 风格权限分级 +═══════════════════════════════════════════ +五级权限模式, 控制工具调用的安全边界。 + +Mode 说明: + default → 每次危险操作都询问用户 + acceptEdits → 自动接受文件编辑, 其他询问 + plan → 只读模式, 不执行任何写操作 + auto → 自动批准白名单操作, 其他询问 + bypass → 跳过所有权限检查 (仅限沙箱) + +用法: + from permission_gate import PermissionGate + gate = PermissionGate("auto") + if gate.allow("Bash", "git status"): + ... +""" + +import json +import fnmatch +from pathlib import Path +from typing import List, Optional + +SETTINGS_PATH = Path(__file__).parent.parent / "settings.json" + +DEFAULT_PERMISSION_CONFIG = { + "mode": "default", + "auto_accept": [ + "Read*", + "Bash(git*)", + "Bash(ls*)", + "Bash(cat*)", + "Bash(head*)", + "Bash(tail*)", + "Bash(wc*)", + "Bash(date)", + "Bash(node --version)", + "Bash(python --version)", + "Bash(python3 --version)", + "Bash(npm --version)", + "Bash(which*)", + "Bash(rg*)", + "Bash(jq*)", + "Bash(find*)", + "Edit*", + "WebSearch*", + ], + "deny_tools": [], + "deny_patterns": [ + "Bash(rm -rf*)", + "Bash(sudo*)", + "Bash(curl*| sh)", + "Bash(wget*| sh)", + "Bash(shutdown*)", + "Bash(reboot*)", + "Bash(format*)", + "Bash(mkfs*)", + "Bash(dd if=*)", + "Write(/etc/*)", + "Write(C:\\\\Windows\\*)", + "Write(C:\\\\Program Files\\*)", + "Bash(git push --force*)", + ], +} + + +class PermissionGate: + """权限门控 — 五级模式""" + + MODES = ("default", "acceptEdits", "plan", "auto", "bypass") + + def __init__(self, mode: Optional[str] = None): + cfg = self._load_config() + self.mode = mode or cfg.get("mode", DEFAULT_PERMISSION_CONFIG["mode"]) + if self.mode not in self.MODES: + print(f"[permission_gate] Unknown mode '{self.mode}', falling back to default") + self.mode = "default" + self.auto_accept = cfg.get("auto_accept") or DEFAULT_PERMISSION_CONFIG["auto_accept"] + self.deny_tools = cfg.get("deny_tools") or DEFAULT_PERMISSION_CONFIG["deny_tools"] + self.deny_patterns = cfg.get("deny_patterns") or DEFAULT_PERMISSION_CONFIG["deny_patterns"] + + def _load_config(self) -> dict: + if SETTINGS_PATH.exists(): + raw = json.loads(SETTINGS_PATH.read_text(encoding="utf-8")) + return raw.get("permission", {}) + return {} + + def _match(self, tool_call: str, patterns: List[str]) -> bool: + """通配符匹配 tool_call 是否在 patterns 中""" + for pattern in patterns: + if fnmatch.fnmatch(tool_call, pattern): + return True + return False + + def allow(self, tool: str, detail: str = "") -> bool: + """ + 检查是否允许执行某个工具调用。 + + Args: + tool: 工具名 (e.g. "Bash", "Edit", "Write") + detail: 调用详情 (e.g. "git status", "rm -rf /") + + Returns: + True = 允许, False = 需要询问用户 + """ + tool_call = f"{tool}({detail})" if detail else tool + + # bypass — 全部放行 + if self.mode == "bypass": + return True + + # plan — 只读模式 + if self.mode == "plan": + read_only = {"Read", "rg", "grep", "find", "WebSearch", "WebFetch"} + if tool in read_only or tool.startswith("mcp__"): + # 允许 MCP 只读操作 + return True + return False + + # 检查黑名单 (最高优先级) + if self._match(tool_call, self.deny_patterns): + print(f"[permission_gate] DENIED: DENIED: {tool_call}") + return False + + # acceptEdits — 自动接受 Edit + if self.mode == "acceptEdits" and tool == "Edit": + return True + + # 安全命令检查 (所有非 bypass/plan 模式都检查) + if self._match(tool_call, self.auto_accept): + return True + + # auto — 自动接受白名单扩展 + if self.mode == "auto" and tool in ("Read", "WebSearch"): + return True + + # plan 和 bypass 已在前面处理, 这里处理剩下的模式 + # default/acceptEdits — 白名单以外的都询问 + return False + + def needs_prompt(self, tool: str, detail: str = "") -> bool: + """是否需要向用户确认 (True = 需要询问)""" + return not self.allow(tool, detail) + + @property + def is_plan_mode(self) -> bool: + return self.mode == "plan" + + @property + def is_bypass(self) -> bool: + return self.mode == "bypass" + + +# ── CLI 入口 ── +if __name__ == "__main__": + import sys + + if len(sys.argv) > 1: + action = sys.argv[1] + if action == "check": + tool = sys.argv[2] if len(sys.argv) > 2 else "Read" + detail = sys.argv[3] if len(sys.argv) > 3 else "" + gate = PermissionGate() + result = gate.allow(tool, detail) + print(json.dumps({ + "mode": gate.mode, + "tool": tool, + "detail": detail, + "allowed": result, + "needs_prompt": not result, + })) + sys.exit(0 if result else 1) + elif action == "mode": + gate = PermissionGate() + mode = sys.argv[2] if len(sys.argv) > 2 else gate.mode + print(f"Permission mode: {mode}") + print(f" acceptEdits: {mode == 'acceptEdits'}") + print(f" plan (read-only): {mode == 'plan'}") + print(f" auto: {mode == 'auto'}") + print(f" bypass: {mode == 'bypass'}") + else: + gate = PermissionGate() + print(f"Current permission mode: {gate.mode}") + print(f"Auto-accept patterns: {len(gate.auto_accept)}") + print(f"Deny patterns: {len(gate.deny_patterns)}") diff --git a/.deepcode/plugin_loader.py b/.deepcode/plugin_loader.py new file mode 100644 index 00000000..8947e52a --- /dev/null +++ b/.deepcode/plugin_loader.py @@ -0,0 +1,338 @@ +#!/usr/bin/env python3 +""" +Plugin Loader — Claude Code 风格热加载插件系统 +═══════════════════════════════════════════ +支持三种加载方式: + 1. 本地目录: {path}/plugin_dir/ + 2. ZIP 文件: {path}/plugin.zip + 3. URL 下载: https://.../plugin.zip (支持 SHA256 签名校验) + +插件结构 (最小): + plugin.zip + ├── SKILL.md ← 必需: skill 定义 (YAML frontmatter + 提示词) + ├── plugin.json ← 可选: 插件元数据 + ├── checksum.sha256 ← 可选: SHA256 签名文件 (推荐用于 URL 安装) + └── *.py / *.js ← 可选: 工具脚本 + +安全: + URL 安装支持 --checksum 参数进行签名校验。 + 也支持 ZIP 内嵌 checksum.sha256 文件自动验证。 + +用法: + python plugin_loader.py install ./my-plugin.zip + python plugin_loader.py install https://example.com/plugin.zip --checksum abc123... + python plugin_loader.py install https://example.com/plugin.zip # 自动查找 checksum + python plugin_loader.py list + python plugin_loader.py remove my-plugin +""" + +import hashlib +import json +import shutil +import tempfile +import zipfile +import urllib.request +import sys +from pathlib import Path +from datetime import datetime +from typing import Dict, List, Optional + +SKILLS_DIR = Path(__file__).parent / "skills" +REGISTRY_PATH = Path(__file__).parent / "plugin_registry.json" + +# ── SHA256 签名校验 ── + +def sha256_file(filepath: Path) -> str: + """计算文件的 SHA256 哈希""" + h = hashlib.sha256() + with open(filepath, "rb") as f: + for chunk in iter(lambda: f.read(65536), b""): + h.update(chunk) + return h.hexdigest() + + +def verify_sha256(filepath: Path, expected_hash: str) -> bool: + """验证文件的 SHA256 签名""" + actual = sha256_file(filepath) + if actual.lower() != expected_hash.lower(): + print(f"[plugin_loader] SHA256 MISMATCH") + print(f" Expected: {expected_hash.lower()}") + print(f" Actual: {actual}") + return False + print(f"[plugin_loader] SHA256 OK: {actual[:16]}...") + return True + + +def find_checksum_in_zip(zip_path: Path) -> Optional[str]: + """在 ZIP 中查找 checksum.sha256 文件并读取""" + try: + with zipfile.ZipFile(zip_path, "r") as zf: + if "checksum.sha256" in zf.namelist(): + content = zf.read("checksum.sha256").decode("utf-8").strip() + # 格式: "sha256 filename.zip" 或纯 hex + parts = content.split() + for part in parts: + if len(part) == 64 and all(c in "0123456789abcdefABCDEF" for c in part): + return part + except Exception: + pass + return None + + +def verify_zip_with_checksum_url(zip_path: Path, url: str) -> Optional[str]: + """尝试从 URL 同路径加载 checksum.sha256 并验证""" + checksum_url = url.rsplit(".", 1)[0] + ".sha256" + checksum_url_alt = url + ".sha256" + + for candidate_url in [checksum_url, checksum_url_alt]: + try: + req = urllib.request.Request(candidate_url, headers={"User-Agent": "DeepCode-PluginLoader/1.0"}) + with urllib.request.urlopen(req, timeout=10) as resp: + content = resp.read().decode("utf-8").strip() + parts = content.split() + for part in parts: + if len(part) == 64 and all(c in "0123456789abcdefABCDEF" for c in part): + print(f"[plugin_loader] Found checksum at: {candidate_url}") + if verify_sha256(zip_path, part): + return part + return None + except Exception: + continue + return None + + +DEFAULT_PLUGIN_JSON = { + "name": "", + "version": "1.0.0", + "description": "", + "author": "", + "entry": "SKILL.md", + "dependencies": [], +} + + +def load_registry() -> dict: + if REGISTRY_PATH.exists(): + return json.loads(REGISTRY_PATH.read_text(encoding="utf-8")) + return {"plugins": {}} + + +def save_registry(reg: dict): + REGISTRY_PATH.write_text(json.dumps(reg, indent=2, ensure_ascii=False), encoding="utf-8") + + +def _validate_plugin_structure(plugin_dir: Path) -> Optional[str]: + """验证插件目录结构, 返回错误信息或 None""" + skill_md = plugin_dir / "SKILL.md" + if not skill_md.exists(): + return f"Missing SKILL.md in {plugin_dir}" + return None + + +def install_from_dir(source_dir: Path, plugin_name: Optional[str] = None) -> bool: + """从本地目录安装插件""" + source_dir = Path(source_dir).resolve() + if not source_dir.is_dir(): + print(f"[plugin_loader] Not a directory: {source_dir}") + return False + + # 读取 plugin.json 获取名称 + plugin_json_path = source_dir / "plugin.json" + if plugin_json_path.exists(): + meta = json.loads(plugin_json_path.read_text(encoding="utf-8")) + name = plugin_name or meta.get("name", source_dir.name) + else: + name = plugin_name or source_dir.name + + err = _validate_plugin_structure(source_dir) + if err: + print(f"[plugin_loader] Invalid plugin: {err}") + return False + + # 复制到 skills 目录 + dest = SKILLS_DIR / name + if dest.exists(): + shutil.rmtree(dest) + shutil.copytree(source_dir, dest) + + # 更新注册表 + reg = load_registry() + reg["plugins"][name] = { + "source": str(source_dir), + "type": "directory", + "installed_at": datetime.now().isoformat(), + "version": meta.get("version", "1.0.0") if plugin_json_path.exists() else "1.0.0", + } + save_registry(reg) + + print(f"[plugin_loader] OK: Installed: {name} (from directory)") + return True + + +def install_from_zip(zip_path: Path) -> bool: + """从 ZIP 文件安装插件""" + zip_path = Path(zip_path).resolve() + if not zip_path.exists(): + print(f"[plugin_loader] ZIP not found: {zip_path}") + return False + + with tempfile.TemporaryDirectory() as tmp: + with zipfile.ZipFile(zip_path, "r") as zf: + zf.extractall(tmp) + + tmp_path = Path(tmp) + # 查找 SKILL.md (支持嵌套一层目录) + skill_md = None + for p in tmp_path.rglob("SKILL.md"): + skill_md = p + break + + if not skill_md: + print("[plugin_loader] ZIP must contain SKILL.md") + return False + + plugin_dir = skill_md.parent + name = _get_plugin_name(plugin_dir, zip_path.stem) + return install_from_dir(plugin_dir, name) + + +def install_from_url(url: str, plugin_name: Optional[str] = None, + checksum: Optional[str] = None) -> bool: + """从 URL 下载并安装插件(支持 SHA256 校验)""" + print(f"[plugin_loader] Downloading: {url}") + try: + with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp: + req = urllib.request.Request(url, headers={"User-Agent": "DeepCode-PluginLoader/1.0"}) + with urllib.request.urlopen(req, timeout=30) as resp: + shutil.copyfileobj(resp, tmp) + tmp_path = Path(tmp.name) + + # SHA256 校验: 1) 用户显式提供 2) ZIP 内嵌 3) URL 同路径 + verified = False + if checksum: + if verify_sha256(tmp_path, checksum): + verified = True + else: + print("[plugin_loader] ERROR: Checksum mismatch, aborting install") + tmp_path.unlink(missing_ok=True) + return False + + if not verified: + embedded = find_checksum_in_zip(tmp_path) + if embedded: + if verify_sha256(tmp_path, embedded): + verified = True + else: + print("[plugin_loader] ERROR: Embedded checksum mismatch, aborting") + tmp_path.unlink(missing_ok=True) + return False + + if not verified and (url.startswith("https://") or url.startswith("http://")): + auto = verify_zip_with_checksum_url(tmp_path, url) + if auto: + verified = True + else: + print("[plugin_loader] WARN: No checksum found, installing without verification") + print(" Pass --checksum to verify integrity") + + result = install_from_zip(tmp_path) + tmp_path.unlink(missing_ok=True) + return result + except Exception as e: + print(f"[plugin_loader] Download failed: {e}") + return False + + +def _get_plugin_name(plugin_dir: Path, fallback: str) -> str: + """从 plugin.json 读取名称""" + pj = plugin_dir / "plugin.json" + if pj.exists(): + meta = json.loads(pj.read_text(encoding="utf-8")) + return meta.get("name", fallback) + return fallback + + +def list_plugins() -> List[dict]: + """列出所有已安装插件""" + reg = load_registry() + plugins = [] + for name, info in reg.get("plugins", {}).items(): + installed = info.get("installed_at", "?") + ptype = info.get("type", "?") + version = info.get("version", "?") + source = info.get("source", "?") + plugins.append({ + "name": name, + "version": version, + "type": ptype, + "installed": installed[:10], + "source": source, + }) + # Also scan skills dir for unregistered plugins + if SKILLS_DIR.exists(): + registered = set(reg.get("plugins", {}).keys()) + for d in SKILLS_DIR.iterdir(): + if d.is_dir() and d.name not in registered: + if (d / "SKILL.md").exists(): + plugins.append({ + "name": d.name, + "version": "—", + "type": "unregistered", + "installed": "—", + "source": str(d), + }) + return plugins + + +def remove_plugin(name: str) -> bool: + """卸载插件""" + dest = SKILLS_DIR / name + if dest.exists(): + shutil.rmtree(dest) + reg = load_registry() + if name in reg.get("plugins", {}): + del reg["plugins"][name] + save_registry(reg) + print(f"[plugin_loader] OK: Removed: {name}") + return True + + +# ── CLI 入口 ── +if __name__ == "__main__": + import argparse + parser = argparse.ArgumentParser(description="DeepCode Plugin Loader") + parser.add_argument("action", choices=["install", "list", "remove"], help="操作") + parser.add_argument("target", nargs="?", help="插件路径/URL/名称") + parser.add_argument("--checksum", help="SHA256 签名 (用于 URL 安装时验证)") + args = parser.parse_args() + + if args.action == "install": + if not args.target: + print("Usage: plugin_loader.py install [--checksum ]") + sys.exit(1) + if args.target.startswith("http://") or args.target.startswith("https://"): + install_from_url(args.target, checksum=args.checksum) + elif args.target.endswith(".zip"): + install_from_zip(Path(args.target)) + else: + install_from_dir(Path(args.target)) + + elif args.action == "list": + plugins = list_plugins() + if not plugins: + print("No plugins installed") + else: + print(f"{'Name':<25} {'Version':<10} {'Type':<12} {'Installed':<12}") + print("-" * 60) + for p in plugins: + print(f"{p['name']:<25} {p['version']:<10} {p['type']:<12} {p['installed']:<12}") + + elif args.action == "remove": + if not args.target: + print("Usage: plugin_loader.py remove ") + sys.exit(1) + remove_plugin(args.target) + + else: + print(f"Unknown action: {action}") + sys.exit(1) diff --git a/.deepcode/plugin_registry.json b/.deepcode/plugin_registry.json new file mode 100644 index 00000000..04e91d97 --- /dev/null +++ b/.deepcode/plugin_registry.json @@ -0,0 +1,10 @@ +{ + "plugins": { + "claude-core-features": { + "source": "C:\\Users\\raymo\\AppData\\Local\\Temp\\tmpdmf3jt_d\\claude-core-features", + "type": "directory", + "installed_at": "2026-07-23T22:17:31.605217", + "version": "1.0.0" + } + } +} \ No newline at end of file diff --git a/.deepcode/settings.local.json b/.deepcode/settings.local.json new file mode 100644 index 00000000..3c98d021 --- /dev/null +++ b/.deepcode/settings.local.json @@ -0,0 +1,31 @@ +{ + "//note": "Local settings (not committed to git). Intended to override project and user settings.", + "//status": "⚠️ 三级加载机制(user→project→local)尚未在核心 CLI 中实现。当前此文件未被核心引擎读取。", + "//permissions": "Local overrides for this machine only", + "permissions": { + "defaultMode": "askAll", + "allow": [ + "read-in-cwd", + "write-in-cwd", + "read-out-cwd", + "write-out-cwd", + "delete-in-cwd", + "network", + "mcp", + "query-git-log", + "mutate-git-log" + ], + "deny": [], + "ask": [ + "delete-out-cwd" + ] + }, + "//mcpServers": "Override GitHub MCP with env var reference (WARNING: never hardcode tokens here)", + "mcpServers": { + "github": { + "env": { + "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_PERSONAL_ACCESS_TOKEN}" + } + } + } +} diff --git a/.deepcode/settings_resolver.py b/.deepcode/settings_resolver.py new file mode 100644 index 00000000..73afb1f6 --- /dev/null +++ b/.deepcode/settings_resolver.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +""" +Settings Resolver — 三级配置合并解析器 +═══════════════════════════════════════ +user settings ← ~/.deepcode/settings.json (全局默认) +project settings ← .deepcode/settings.json (项目共享) +local settings ← .deepcode/settings.local.json (本机覆盖, .gitignore) + +合并规则: user < project < local (local 优先级最高) +深层合并 (dict 递归合并, list 替换而非追加) + +用法: + from settings_resolver import resolve_settings, get_setting + + cfg = resolve_settings() # 三级合并结果 + val = get_setting("permissions.mode") # 点号路径访问 + val = get_setting("model", "default") # 带默认值 +""" + +import json +import os +from pathlib import Path +from typing import Any, Dict, Optional + + +# ── 路径定义 ─────────────────────────────────────────────────── + +def _get_project_root() -> Path: + """自动探测项目根目录 (包含 .deepcode/ 的目录)""" + cwd = Path.cwd() + for p in [cwd] + list(cwd.parents): + if (p / ".deepcode").is_dir(): + return p + return cwd + + +PROJECT_ROOT = _get_project_root() +USER_SETTINGS = Path.home() / ".deepcode" / "settings.json" +PROJECT_SETTINGS = PROJECT_ROOT / ".deepcode" / "settings.json" +LOCAL_SETTINGS = PROJECT_ROOT / ".deepcode" / "settings.local.json" + + +# ── JSON 加载 ────────────────────────────────────────────────── + +def _load_json(path: Path) -> dict: + """加载 JSON 文件,不存在或解析失败时返回空 dict""" + if not path.exists(): + return {} + try: + text = path.read_text(encoding="utf-8") + # 移除 BOM + if text.startswith("\ufeff"): + text = text[1:] + return json.loads(text) + except (json.JSONDecodeError, OSError) as e: + print(f"[settings_resolver] WARN: Failed to load {path}: {e}", file=__import__("sys").stderr) + return {} + + +# ── 深层合并 ─────────────────────────────────────────────────── + +def _deep_merge(base: dict, override: dict) -> dict: + """ + 递归深层合并两个 dict。 + - dict 值递归合并 + - list 值直接替换 (不追加) + - 其他值直接替换 + """ + result = base.copy() + for key, val in override.items(): + if key in result and isinstance(result[key], dict) and isinstance(val, dict): + result[key] = _deep_merge(result[key], val) + else: + result[key] = val + return result + + +# ── 三级解析 ─────────────────────────────────────────────────── + +def resolve_settings( + user_path: Optional[Path] = None, + project_path: Optional[Path] = None, + local_path: Optional[Path] = None, +) -> dict: + """ + 解析三级配置并合并。 + + Args: + user_path: 用户级配置路径 (默认 ~/.deepcode/settings.json) + project_path: 项目级配置路径 (默认 .deepcode/settings.json) + local_path: 本地级配置路径 (默认 .deepcode/settings.local.json) + + Returns: + 合并后的完整配置 dict + """ + user_cfg = _load_json(user_path or USER_SETTINGS) + project_cfg = _load_json(project_path or PROJECT_SETTINGS) + local_cfg = _load_json(local_path or LOCAL_SETTINGS) + + # user ← project ← local + merged = _deep_merge(user_cfg, project_cfg) + merged = _deep_merge(merged, local_cfg) + + # 附加元信息 + merged["_meta"] = { + "user_settings": str(user_path or USER_SETTINGS), + "project_settings": str(project_path or PROJECT_SETTINGS), + "local_settings": str(local_path or LOCAL_SETTINGS), + "project_root": str(PROJECT_ROOT), + "tiers_loaded": [ + "user" if (user_path or USER_SETTINGS).exists() else None, + "project" if (project_path or PROJECT_SETTINGS).exists() else None, + "local" if (local_path or LOCAL_SETTINGS).exists() else None, + ], + } + + return merged + + +def get_setting(key_path: str, default: Any = None, cfg: Optional[dict] = None) -> Any: + """ + 通过点号路径获取配置值。 + + Args: + key_path: 点号分隔的路径,如 "permissions.mode" + default: 键不存在时的默认值 + cfg: 配置 dict (不传则自动 resolve) + + Returns: + 配置值或 default + """ + if cfg is None: + cfg = resolve_settings() + + parts = key_path.split(".") + current = cfg + for part in parts: + if isinstance(current, dict) and part in current: + current = current[part] + else: + return default + return current + + +def list_tiers() -> dict: + """列出三级配置各层的文件存在状态和大小""" + result = {} + for name, path in [("user", USER_SETTINGS), ("project", PROJECT_SETTINGS), + ("local", LOCAL_SETTINGS)]: + info = {"exists": path.exists(), "path": str(path)} + if path.exists(): + info["size_bytes"] = path.stat().st_size + result[name] = info + return result + + +# ── CLI 入口 ─────────────────────────────────────────────────── + +if __name__ == "__main__": + import sys + if len(sys.argv) > 1 and sys.argv[1] == "tiers": + print(json.dumps(list_tiers(), indent=2, ensure_ascii=False)) + elif len(sys.argv) > 2 and sys.argv[1] == "get": + val = get_setting(sys.argv[2]) + print(json.dumps(val, indent=2, ensure_ascii=False) if isinstance(val, (dict, list)) else val) + else: + cfg = resolve_settings() + print(json.dumps(cfg, indent=2, ensure_ascii=False)) diff --git a/.deepcode/skills/deepcode-agent/SKILL.md b/.deepcode/skills/deepcode-agent/SKILL.md new file mode 100644 index 00000000..52ec4251 --- /dev/null +++ b/.deepcode/skills/deepcode-agent/SKILL.md @@ -0,0 +1,291 @@ +--- +name: deepcode-agent +description: > + DeepCode Agent — 完整 Agent 体系 (v3.0 统一版)。 + 线程派生系统 (codex.exe移植) + Agent SDK 外部调用 (Claude Code移植)。 + - 内部: Agent消息类型/线程派生树/动态工具注册/SQLite持久化 + - 外部: HTTP API / MCP stdio / Python嵌入调用 +version: 3.0.0 +author: DeepCode + RE (codex.exe v0.145.0 + Claude Code v2.1.216) +date: 2026-07-29 +tags: [agent, session, tracking, messaging, threading, spawn, sdk, api, integration] +--- + +# DeepCode Agent + +移植自 **codex.exe** 的 Agent Message 类型系统。 + +## 对标项 + +| codex.exe | DeepCode Agent | +|:----------|:---------------| +| `AgentMessagePlan` | `AgentPlan` | +| `AgentMessageItem` | `AgentMessage` | +| `AgentMessageContentDeltaEvent` | `AgentMessage(delta=True)` | +| `DynamicToolCall` + `CallToolResult` | `ToolCall` | +| `CommandExecutionItem` + `CommandBeginEvent` | `CommandExecution` | +| `SubagentStart` / `SubagentStop` | `create_sub_agent()` | +| `AgentSession` | `AgentSession` | + +## 消息类型 (21 种) + +| 类型 | 说明 | +|:----|:-----| +| `reasoning` | 推理过程 | +| `plan` | 执行计划 | +| `tool_call` | 工具调用 | +| `tool_result` | 工具结果 | +| `command_execution` | 命令执行 | +| `command_begin` | 命令开始 | +| `command_end` | 命令结束 | +| `file_change` | 文件变更 | +| `web_search` | 网络搜索 | +| `image_generation` | 图片生成 | +| `mcp_tool_call` | MCP 工具调用 | +| `sub_agent_activity` | 子 Agent 活动 | +| `collab_agent_tool_call` | 协作 Agent | +| `context_compact` | 上下文压缩 | +| `error` | 错误 | +| `text` | 文本 | + +## 用法 + +### CLI + +```bash +# 创建计划 +python agent.py plan "分析项目结构" + +# 记录推理 +python agent.py reason "先看package.json, 再看src目录" + +# 记录工具调用 +python agent.py tool --name Read --input '{"path":"package.json"}' \ + --result '{"content": "..."}' + +# 记录命令 +python agent.py command "npm test" --stdout "All tests passed" + +# 查看状态 +python agent.py status + +# 导出完整 JSON +python agent.py export +``` + +### MCP Server + +```json +"deepcode-agent": { + "command": "python", + "args": [ + "F:/DEEPCODE/.deepcode/skills/deepcode-agent/agent.py", + "--mcp" + ] +} +``` + +### Python 嵌入 + +```python +from agent import AgentSession, AgentMessageType + +sess = AgentSession("分析项目") +sess.start_turn() + +# 记录推理 +sess.add_reasoning("检查依赖关系...") + +# 记录工具调用 +tc = sess.add_tool_call("Read", {"path": "Cargo.toml"}) +tc.complete({"content": "[package]\nname = \"codex\""}) + +# 记录命令 +ce = sess.add_command("cargo build") +ce.complete(stdout="Compiling...", exit_code=0) + +# 导出 +print(sess.export_json()) +``` + +## Agent 线程派生系统 (v2.0.0 新增) + +移植自 codex.exe 的 SQLite 持久化 Agent 线程架构,对标 `threads` / `thread_spawn_edges` / `thread_dynamic_tools` / `logs` 四表。 + +### 核心概念 + +| 概念 | codex.exe 表 | 说明 | +|:------|:------------|:-----| +| Agent 线程 | `threads` | 完整生命周期 (9种状态) | +| 派生树 | `thread_spawn_edges` | 父→子层级关系 | +| 动态工具 | `thread_dynamic_tools` | 每线程独立注册/卸载 | +| 线程日志 | `logs` | 线程级日志 | + +### 9 种 Agent 类型 + +`coder` `reviewer` `tester` `planner` `researcher` `coordinator` `shell_executor` `file_editor` `general` + +### CLI + +```bash +python agent_thread_manager.py spawn "analyze binary" --type researcher +python agent_thread_manager.py spawn "implement X" --type coder --parent +python agent_thread_manager.py forest +python agent_thread_manager.py stats +``` + +### MCP Server + +```json +"deepcode-agent-threads": { + "command": "python", + "args": ["F:/DEEPCODE/.deepcode/skills/deepcode-agent/agent_thread_manager.py", "--mcp"] +} +``` + +### Python + +```python +from agent_thread_manager import AgentThreadManager, AgentType +mgr = AgentThreadManager() +root = mgr.spawn("analyze", agent_type=AgentType.RESEARCHER) +child = mgr.spawn("implement", agent_type=AgentType.CODER, parent_thread_id=root.id) +tree = mgr.get_full_tree(root.id) +mgr.stop_thread_cascade(root.id) +``` + +## Agent View 配置 (v2.1.0 — P2-12 Claude Code 对齐) + +对标 Claude Code 的 Agent View 配置系统: +`--agent-color`, `--agent`, `--plugins`, `--mcp-config`, `--permission-mode`, `--model`, `--effort` + +### CLI 参数 + +```bash +# 启动 Agent View (可视化多 agent 协作) +python agent_view.py + +# 指定 agent 颜色 +python agent_view.py --agent-color "#FF6B6B" + +# 指定默认 agent 类型 +python agent_view.py --agent reviewer + +# 加载额外插件目录 +python agent_view.py --plugins ./my-plugins + +# 指定分派会话的默认 MCP 配置 +python agent_view.py --mcp-config ./dispatched-mcp.json + +# 默认权限模式 (分派会话) +python agent_view.py --permission-mode plan + +# 分派会话的默认模型 +python agent_view.py --model deepseek-v4-pro + +# 分派会话的默认 effort 级别 +python agent_view.py --effort high +``` + +### 配置结构 + +```json +{ + "agent_view": { + "enabled": true, + "default_agent": "general", + "agent_color": "#4ECDC4", + "dispatch_defaults": { + "permission_mode": "default", + "model": "deepseek-v4-flash", + "effort": "medium", + "bypass_permissions_available": false, + "strict_mcp_config": false + }, + "plugins_dir": "", + "mcp_config_override": {}, + "max_dispatched_sessions": 5 + } +} +``` + +### Agent 颜色参考 + +| Agent 类型 | 颜色 | 色值 | +|:----------|:-----|:-----| +| `general` | 青色 | `#4ECDC4` | +| `coder` | 蓝色 | `#45B7D1` | +| `reviewer` | 橙色 | `#FF6B6B` | +| `tester` | 绿色 | `#96CEB4` | +| `planner` | 紫色 | `#DDA0DD` | +| `researcher` | 黄色 | `#FFEAA7` | +| `coordinator` | 白色 | `#DFE6E9` | +| `shell_executor` | 红色 | `#FF7675` | +| `file_editor` | 灰色 | `#B2BEC3` | + +## Agent SDK (v3.0 合并 — 原 deepcode-agent-sdk) + +移植自 **Claude Code v2.1.216** 的 `agentSdk.ts`。让 DeepCode 作为 Agent 被外部程序调用。 + +### 三种模式 + +| 模式 | 说明 | 端口/协议 | +|:---|:-----|:----------| +| **HTTP API** | RESTful API Server | 8088 (HTTP) | +| **MCP stdio** | 作为 MCP Server 运行 | stdin/stdout | +| **Python 嵌入** | `from agent_sdk_server import DeepCodeAgent` | — | + +### 内置工具 + +| 工具名 | 说明 | +|:------|:-----| +| `read_file` | 读取文件内容 | +| `write_file` | 写入文件 | +| `execute_command` | 执行 Shell 命令 | +| `search_files` | 搜索文件 (通配符) | +| `list_directory` | 列出目录内容 | +| `agent_query` | 向 AI 模型发送查询 | +| `agent_status` | 获取 Agent 状态 | + +### 启动 HTTP Server + +```bash +python agent_sdk_server.py --http --port 8088 +``` + +```bash +# 列出工具 +curl http://127.0.0.1:8088/tools + +# 执行工具 +curl -X POST http://127.0.0.1:8088/execute \ + -H "Content-Type: application/json" \ + -d '{"tool": "list_directory", "params": {"path": "."}}' +``` + +### MCP 注册 + +```json +"deepcode-agent-sdk": { + "command": "python", + "args": ["F:/DEEPCODE/.deepcode/skills/deepcode-agent/agent_sdk_server.py", "--mcp"], + "env": { "DEEPSEEK_API_KEY": "${DEEPSEEK_API_KEY}" } +} +``` + +### Python 嵌入 + +```python +from agent_sdk_server import DeepCodeAgent +agent = DeepCodeAgent(workspace="/path/to/project") +agent.register_tool("my_tool", my_handler, "My tool description") +result = await agent.execute("read_file", {"path": "test.txt"}) +``` + +### SDK 配置 + +| 变量 | 默认值 | 说明 | +|:----|:------|:----| +| `DEEPSEEK_API_KEY` | — | API 密钥 (agent_query) | +| `DEEPCODE_AGENT_PORT` | 8088 | HTTP 端口 | +| `DEEPCODE_AGENT_HOST` | 127.0.0.1 | HTTP 绑定地址 | diff --git a/.deepcode/skills/deepcode-agent/agent_thread_manager.py b/.deepcode/skills/deepcode-agent/agent_thread_manager.py new file mode 100644 index 00000000..8a6117c8 --- /dev/null +++ b/.deepcode/skills/deepcode-agent/agent_thread_manager.py @@ -0,0 +1,1088 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +DeepCode Agent Thread Manager — Agent 线程派生系统 +═══════════════════════════════════════════════════ +移植自 CODEX.EXE (OpenAI Codex CLI v0.145.0) 的 Agent 线程架构: + + SQL 结构逆向 (来自 codex.exe PE scan): + DELETE FROM threads WHERE id = ? + DELETE FROM thread_dynamic_tools WHERE thread_id = ? + DELETE FROM thread_spawn_edges WHERE parent_thread_id = ? OR child_thread_id = ? + DELETE FROM logs WHERE thread_id = ? + +核心能力: + 1. Agent 线程树 — 父 Agent 派生子 Agent,完整生命周期管理 + 2. 动态工具注册 — 每个线程独立注册/卸载工具 + 3. SQLite 持久化 — 线程、工具、边、日志全量落地 + 4. MCP Server — 通过 MCP 协议对外暴露 + +用法: + # CLI + python agent_thread_manager.py spawn "analyze this binary" --type researcher + python agent_thread_manager.py tree + python agent_thread_manager.py stop + + # MCP Server + python agent_thread_manager.py --mcp + + # Python 嵌入 + from agent_thread_manager import AgentThreadManager + mgr = AgentThreadManager("F:/DEEPCODE/database.db") + thread_id = mgr.spawn("analyze code", agent_type="researcher") +""" + +import asyncio +import json +import os +import sqlite3 +import sys +import time +import uuid +from dataclasses import dataclass, field, asdict +from datetime import datetime, timezone +from enum import Enum +from pathlib import Path +from typing import Any, Dict, List, Optional, Tuple + + +# ── 数据库路径 ────────────────────────────────────────────── + +DEFAULT_DB = Path(__file__).resolve().parent.parent.parent.parent / "database.db" + + +# ── 枚举定义 ───────────────────────────────────────────────── + +class ThreadStatus(str, Enum): + """Agent 线程状态 — 对标 codex.exe Thread 生命周期""" + CREATED = "created" + RUNNING = "running" + PAUSED = "paused" + AWAITING_APPROVAL = "awaiting_approval" + WAITING_CHILD = "waiting_child" # 等待子 Agent 完成 + COMPLETED = "completed" + FAILED = "failed" + STOPPED = "stopped" + EXPIRED = "expired" + + +class AgentType(str, Enum): + """Agent 类型 — 对标 codex.exe Agent 角色""" + CODER = "coder" + REVIEWER = "reviewer" + TESTER = "tester" + PLANNER = "planner" + RESEARCHER = "researcher" + COORDINATOR = "coordinator" + SHELL_EXECUTOR = "shell_executor" + FILE_EDITOR = "file_editor" + GENERAL = "general" + + +class LogLevel(str, Enum): + DEBUG = "debug" + INFO = "info" + WARN = "warn" + ERROR = "error" + + +# ── 数据类 ─────────────────────────────────────────────────── + +@dataclass +class AgentThread: + """Agent 线程 — 对标 codex.exe threads 表""" + id: str = "" + goal: str = "" + agent_type: AgentType = AgentType.GENERAL + status: ThreadStatus = ThreadStatus.CREATED + context_json: str = "{}" + parent_thread_id: Optional[str] = None + parent_turn_id: Optional[str] = None + error: Optional[str] = None + metadata: Dict[str, Any] = field(default_factory=dict) + created_at: str = "" + updated_at: str = "" + completed_at: Optional[str] = None + duration_ms: Optional[int] = None + + def __post_init__(self): + if not self.id: + self.id = f"at_{uuid.uuid4().hex[:12]}" + now = datetime.now(timezone.utc).isoformat() + if not self.created_at: + self.created_at = now + if not self.updated_at: + self.updated_at = now + + def to_dict(self) -> Dict: + d = asdict(self) + d["agent_type"] = self.agent_type.value if isinstance(self.agent_type, AgentType) else self.agent_type + d["status"] = self.status.value if isinstance(self.status, ThreadStatus) else self.status + return d + + +@dataclass +class DynamicTool: + """动态工具 — 对标 codex.exe thread_dynamic_tools 表""" + id: str = "" + thread_id: str = "" + tool_name: str = "" + tool_schema_json: str = "{}" + registered_at: str = "" + unregistered_at: Optional[str] = None + + def __post_init__(self): + if not self.id: + self.id = f"dt_{uuid.uuid4().hex[:8]}" + if not self.registered_at: + self.registered_at = datetime.now(timezone.utc).isoformat() + + def to_dict(self) -> Dict: + return asdict(self) + + +@dataclass +class SpawnEdge: + """派生边 — 对标 codex.exe thread_spawn_edges 表""" + id: str = "" + parent_thread_id: str = "" + child_thread_id: str = "" + spawn_reason: str = "" + spawned_at: str = "" + + def __post_init__(self): + if not self.id: + self.id = f"se_{uuid.uuid4().hex[:8]}" + if not self.spawned_at: + self.spawned_at = datetime.now(timezone.utc).isoformat() + + def to_dict(self) -> Dict: + return asdict(self) + + +@dataclass +class ThreadLog: + """线程日志 — 对标 codex.exe logs 表""" + id: str = "" + thread_id: str = "" + level: LogLevel = LogLevel.INFO + message: str = "" + metadata_json: str = "{}" + timestamp: str = "" + + def __post_init__(self): + if not self.id: + self.id = f"tl_{uuid.uuid4().hex[:8]}" + if not self.timestamp: + self.timestamp = datetime.now(timezone.utc).isoformat() + + def to_dict(self) -> Dict: + d = asdict(self) + d["level"] = self.level.value if isinstance(self.level, LogLevel) else self.level + return d + + +# ── Agent Thread Manager ─────────────────────────────────── + +class AgentThreadManager: + """ + Agent 线程管理器 — 对标 codex.exe 的完整线程系统 + + 架构: + SQLite DB + ├── threads — Agent 线程主表 + ├── thread_dynamic_tools — 每线程独立工具注册 + ├── thread_spawn_edges — 父→子派生关系 + └── thread_logs — 线程级日志 + + 对标: + codex.exe SQL: + DELETE FROM threads WHERE id = ? + DELETE FROM thread_dynamic_tools WHERE thread_id = ? + DELETE FROM thread_spawn_edges WHERE parent_thread_id = ? OR child_thread_id = ? + DELETE FROM logs WHERE thread_id = ? + """ + + SCHEMA_VERSION = 1 + + def __init__(self, db_path: str = ""): + self._db_path = str(db_path or DEFAULT_DB) + self._conn: Optional[sqlite3.Connection] = None + self._ensure_schema() + + # ── 数据库初始化 ────────────────────────────────────── + + def _connect(self) -> sqlite3.Connection: + if self._conn is None: + self._conn = sqlite3.connect(self._db_path) + self._conn.row_factory = sqlite3.Row + self._conn.execute("PRAGMA journal_mode=WAL") + self._conn.execute("PRAGMA foreign_keys=ON") + return self._conn + + def _ensure_schema(self) -> None: + db = self._connect() + db.executescript(""" + CREATE TABLE IF NOT EXISTS agent_threads ( + id TEXT PRIMARY KEY, + goal TEXT NOT NULL DEFAULT '', + agent_type TEXT NOT NULL DEFAULT 'general', + status TEXT NOT NULL DEFAULT 'created', + context_json TEXT NOT NULL DEFAULT '{}', + parent_thread_id TEXT, + parent_turn_id TEXT, + error TEXT, + metadata_json TEXT NOT NULL DEFAULT '{}', + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + completed_at TEXT, + duration_ms INTEGER + ); + + CREATE TABLE IF NOT EXISTS agent_dynamic_tools ( + id TEXT PRIMARY KEY, + thread_id TEXT NOT NULL REFERENCES agent_threads(id) ON DELETE CASCADE, + tool_name TEXT NOT NULL, + tool_schema_json TEXT NOT NULL DEFAULT '{}', + registered_at TEXT NOT NULL, + unregistered_at TEXT + ); + CREATE INDEX IF NOT EXISTS idx_dynamic_tools_thread + ON agent_dynamic_tools(thread_id); + + CREATE TABLE IF NOT EXISTS agent_spawn_edges ( + id TEXT PRIMARY KEY, + parent_thread_id TEXT NOT NULL REFERENCES agent_threads(id) ON DELETE CASCADE, + child_thread_id TEXT NOT NULL REFERENCES agent_threads(id) ON DELETE CASCADE, + spawn_reason TEXT NOT NULL DEFAULT '', + spawned_at TEXT NOT NULL + ); + CREATE INDEX IF NOT EXISTS idx_spawn_edges_parent + ON agent_spawn_edges(parent_thread_id); + CREATE INDEX IF NOT EXISTS idx_spawn_edges_child + ON agent_spawn_edges(child_thread_id); + + CREATE TABLE IF NOT EXISTS agent_thread_logs ( + id TEXT PRIMARY KEY, + thread_id TEXT NOT NULL REFERENCES agent_threads(id) ON DELETE CASCADE, + level TEXT NOT NULL DEFAULT 'info', + message TEXT NOT NULL DEFAULT '', + metadata_json TEXT NOT NULL DEFAULT '{}', + timestamp TEXT NOT NULL + ); + CREATE INDEX IF NOT EXISTS idx_thread_logs_thread + ON agent_thread_logs(thread_id); + CREATE INDEX IF NOT EXISTS idx_thread_logs_time + ON agent_thread_logs(thread_id, timestamp); + + CREATE TABLE IF NOT EXISTS agent_schema_version ( + version INTEGER PRIMARY KEY + ); + """) + # 记录 schema 版本 + db.execute( + "INSERT OR IGNORE INTO agent_schema_version (version) VALUES (?)", + (self.SCHEMA_VERSION,) + ) + db.commit() + + # ── 线程 CRUD ───────────────────────────────────────── + + def spawn( + self, + goal: str, + agent_type: AgentType = AgentType.GENERAL, + parent_thread_id: Optional[str] = None, + parent_turn_id: Optional[str] = None, + context: Optional[Dict[str, Any]] = None, + metadata: Optional[Dict[str, Any]] = None, + ) -> AgentThread: + """派生新 Agent 线程 — 对标 codex.exe SubagentStart""" + thread = AgentThread( + goal=goal, + agent_type=agent_type, + context_json=json.dumps(context or {}, ensure_ascii=False), + parent_thread_id=parent_thread_id, + parent_turn_id=parent_turn_id, + metadata=metadata or {}, + ) + db = self._connect() + db.execute( + """INSERT INTO agent_threads + (id, goal, agent_type, status, context_json, + parent_thread_id, parent_turn_id, metadata_json, + created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", + ( + thread.id, thread.goal, thread.agent_type.value, + thread.status.value, thread.context_json, + thread.parent_thread_id, thread.parent_turn_id, + json.dumps(thread.metadata, ensure_ascii=False), + thread.created_at, thread.updated_at, + ), + ) + + # 记录派生边 + if parent_thread_id: + edge = SpawnEdge( + parent_thread_id=parent_thread_id, + child_thread_id=thread.id, + spawn_reason=goal, + ) + db.execute( + """INSERT INTO agent_spawn_edges + (id, parent_thread_id, child_thread_id, spawn_reason, spawned_at) + VALUES (?, ?, ?, ?, ?)""", + (edge.id, edge.parent_thread_id, edge.child_thread_id, + edge.spawn_reason, edge.spawned_at), + ) + + db.commit() + self._log(thread.id, LogLevel.INFO, f"Agent spawned: {goal[:100]}", + {"agent_type": agent_type.value, "parent": parent_thread_id}) + return thread + + def get(self, thread_id: str) -> Optional[AgentThread]: + """获取线程""" + db = self._connect() + row = db.execute( + "SELECT * FROM agent_threads WHERE id = ?", (thread_id,) + ).fetchone() + if row is None: + return None + return self._row_to_thread(row) + + def list_threads( + self, + status: Optional[ThreadStatus] = None, + agent_type: Optional[AgentType] = None, + parent_thread_id: Optional[str] = None, + limit: int = 100, + offset: int = 0, + ) -> List[AgentThread]: + """列出线程""" + db = self._connect() + conds = [] + params: List[Any] = [] + if status: + conds.append("status = ?") + params.append(status.value) + if agent_type: + conds.append("agent_type = ?") + params.append(agent_type.value) + if parent_thread_id is not None: + conds.append("parent_thread_id = ?") + params.append(parent_thread_id) + where = ("WHERE " + " AND ".join(conds)) if conds else "" + rows = db.execute( + f"SELECT * FROM agent_threads {where} " + f"ORDER BY created_at DESC LIMIT ? OFFSET ?", + params + [limit, offset], + ).fetchall() + return [self._row_to_thread(r) for r in rows] + + def update_status( + self, + thread_id: str, + status: ThreadStatus, + error: Optional[str] = None, + ) -> bool: + """更新线程状态 — 对标 codex.exe SubagentStop / SessionEnd""" + db = self._connect() + now = datetime.now(timezone.utc).isoformat() + is_terminal = status in ( + ThreadStatus.COMPLETED, ThreadStatus.FAILED, + ThreadStatus.STOPPED, ThreadStatus.EXPIRED, + ) + + thread = self.get(thread_id) + if thread is None: + return False + + duration_ms = None + if is_terminal and thread.created_at: + try: + created = datetime.fromisoformat(thread.created_at) + ended = datetime.fromisoformat(now) + duration_ms = int((ended - created).total_seconds() * 1000) + except Exception: + pass + + db.execute( + """UPDATE agent_threads + SET status = ?, updated_at = ?, + completed_at = CASE WHEN ? THEN ? ELSE completed_at END, + error = ?, duration_ms = ? + WHERE id = ?""", + ( + status.value, now, + is_terminal, now if is_terminal else None, + error, duration_ms, + thread_id, + ), + ) + db.commit() + self._log(thread_id, LogLevel.INFO if not error else LogLevel.ERROR, + f"Status → {status.value}" + (f": {error}" if error else ""), + {"new_status": status.value}) + return True + + def stop_thread_cascade(self, thread_id: str) -> int: + """级联停止线程及其所有子线程""" + db = self._connect() + children = self.get_child_threads(thread_id) + stopped = 0 + for child in children: + stopped += self.stop_thread_cascade(child.id) + + self.update_status(thread_id, ThreadStatus.STOPPED) + stopped += 1 + return stopped + + def delete_thread_cascade(self, thread_id: str) -> int: + """级联删除线程及其所有子线程、动态工具、日志 — 对标 codex.exe DELETE CASCADE""" + db = self._connect() + children = self.get_child_threads(thread_id) + deleted = 0 + for child in children: + deleted += self.delete_thread_cascade(child.id) + + # 删除相关数据 (FK CASCADE 会自动清理, 这里显式做) + db.execute("DELETE FROM agent_dynamic_tools WHERE thread_id = ?", (thread_id,)) + db.execute("DELETE FROM agent_spawn_edges WHERE child_thread_id = ?", (thread_id,)) + db.execute("DELETE FROM agent_spawn_edges WHERE parent_thread_id = ?", (thread_id,)) + db.execute("DELETE FROM agent_thread_logs WHERE thread_id = ?", (thread_id,)) + db.execute("DELETE FROM agent_threads WHERE id = ?", (thread_id,)) + db.commit() + deleted += 1 + return deleted + + # ── 派生树 ──────────────────────────────────────────── + + def get_child_threads(self, thread_id: str) -> List[AgentThread]: + """获取直接子线程""" + db = self._connect() + rows = db.execute( + """SELECT t.* FROM agent_threads t + INNER JOIN agent_spawn_edges e ON t.id = e.child_thread_id + WHERE e.parent_thread_id = ? + ORDER BY e.spawned_at ASC""", + (thread_id,), + ).fetchall() + return [self._row_to_thread(r) for r in rows] + + def get_parent_thread(self, thread_id: str) -> Optional[AgentThread]: + """获取父线程""" + db = self._connect() + row = db.execute( + """SELECT t.* FROM agent_threads t + INNER JOIN agent_spawn_edges e ON t.id = e.parent_thread_id + WHERE e.child_thread_id = ?""", + (thread_id,), + ).fetchone() + return self._row_to_thread(row) if row else None + + def get_full_tree(self, root_thread_id: str) -> Dict[str, Any]: + """获取完整派生树""" + root = self.get(root_thread_id) + if root is None: + return {} + + def _build_node(tid: str) -> Dict[str, Any]: + t = self.get(tid) + if t is None: + return {"id": tid, "error": "not found"} + children = self.get_child_threads(tid) + return { + **t.to_dict(), + "children": [_build_node(c.id) for c in children], + } + + return _build_node(root_thread_id) + + def get_all_roots(self) -> List[AgentThread]: + """获取所有根线程 (没有父线程的)""" + db = self._connect() + rows = db.execute( + """SELECT * FROM agent_threads + WHERE parent_thread_id IS NULL + ORDER BY created_at DESC""", + ).fetchall() + return [self._row_to_thread(r) for r in rows] + + def get_full_forest(self) -> List[Dict[str, Any]]: + """获取所有根线程的完整树""" + return [self.get_full_tree(r.id) for r in self.get_all_roots()] + + # ── 动态工具管理 ────────────────────────────────────── + + def register_tool( + self, thread_id: str, tool_name: str, tool_schema: Dict[str, Any] + ) -> Optional[DynamicTool]: + """注册动态工具 — 对标 codex.exe thread_dynamic_tools INSERT""" + thread = self.get(thread_id) + if thread is None: + return None + + dt = DynamicTool( + thread_id=thread_id, + tool_name=tool_name, + tool_schema_json=json.dumps(tool_schema, ensure_ascii=False), + ) + db = self._connect() + db.execute( + """INSERT INTO agent_dynamic_tools + (id, thread_id, tool_name, tool_schema_json, registered_at) + VALUES (?, ?, ?, ?, ?)""", + (dt.id, dt.thread_id, dt.tool_name, dt.tool_schema_json, dt.registered_at), + ) + db.commit() + self._log(thread_id, LogLevel.INFO, + f"Dynamic tool registered: {tool_name}", + {"tool_name": tool_name}) + return dt + + def unregister_tool(self, thread_id: str, tool_name: str) -> bool: + """卸载动态工具""" + db = self._connect() + db.execute( + """UPDATE agent_dynamic_tools + SET unregistered_at = ? + WHERE thread_id = ? AND tool_name = ? AND unregistered_at IS NULL""", + (datetime.now(timezone.utc).isoformat(), thread_id, tool_name), + ) + db.commit() + self._log(thread_id, LogLevel.INFO, f"Dynamic tool unregistered: {tool_name}") + return db.total_changes > 0 + + def list_tools(self, thread_id: str) -> List[DynamicTool]: + """列出线程的所有动态工具""" + db = self._connect() + rows = db.execute( + """SELECT * FROM agent_dynamic_tools + WHERE thread_id = ? ORDER BY registered_at ASC""", + (thread_id,), + ).fetchall() + return [DynamicTool( + id=r["id"], thread_id=r["thread_id"], tool_name=r["tool_name"], + tool_schema_json=r["tool_schema_json"], + registered_at=r["registered_at"], + unregistered_at=r["unregistered_at"], + ) for r in rows] + + # ── 日志 ────────────────────────────────────────────── + + def _log(self, thread_id: str, level: LogLevel, message: str, + metadata: Optional[Dict] = None) -> ThreadLog: + entry = ThreadLog( + thread_id=thread_id, level=level, message=message, + metadata_json=json.dumps(metadata or {}, ensure_ascii=False), + ) + self._connect().execute( + """INSERT INTO agent_thread_logs + (id, thread_id, level, message, metadata_json, timestamp) + VALUES (?, ?, ?, ?, ?, ?)""", + (entry.id, entry.thread_id, entry.level.value, + entry.message, entry.metadata_json, entry.timestamp), + ) + self._connect().commit() + return entry + + def get_logs(self, thread_id: str, limit: int = 100, + level: Optional[LogLevel] = None) -> List[ThreadLog]: + """获取线程日志""" + db = self._connect() + if level: + rows = db.execute( + """SELECT * FROM agent_thread_logs + WHERE thread_id = ? AND level = ? + ORDER BY timestamp DESC LIMIT ?""", + (thread_id, level.value, limit), + ).fetchall() + else: + rows = db.execute( + """SELECT * FROM agent_thread_logs + WHERE thread_id = ? + ORDER BY timestamp DESC LIMIT ?""", + (thread_id, limit), + ).fetchall() + return [ThreadLog( + id=r["id"], thread_id=r["thread_id"], + level=LogLevel(r["level"]), message=r["message"], + metadata_json=r["metadata_json"], timestamp=r["timestamp"], + ) for r in rows] + + # ── 统计 ────────────────────────────────────────────── + + def stats(self) -> Dict[str, Any]: + """获取系统统计""" + db = self._connect() + total = db.execute("SELECT COUNT(*) FROM agent_threads").fetchone()[0] + by_status = {} + for row in db.execute( + "SELECT status, COUNT(*) FROM agent_threads GROUP BY status" + ).fetchall(): + by_status[row[0]] = row[1] + by_type = {} + for row in db.execute( + "SELECT agent_type, COUNT(*) FROM agent_threads GROUP BY agent_type" + ).fetchall(): + by_type[row[0]] = row[1] + total_tools = db.execute("SELECT COUNT(*) FROM agent_dynamic_tools").fetchone()[0] + total_edges = db.execute("SELECT COUNT(*) FROM agent_spawn_edges").fetchone()[0] + total_logs = db.execute("SELECT COUNT(*) FROM agent_thread_logs").fetchone()[0] + + return { + "total_threads": total, + "by_status": by_status, + "by_type": by_type, + "total_dynamic_tools": total_tools, + "total_spawn_edges": total_edges, + "total_logs": total_logs, + "db_path": self._db_path, + } + + # ── 辅助 ────────────────────────────────────────────── + + def _row_to_thread(self, row: sqlite3.Row) -> AgentThread: + # Row 可能来自 JOIN 或直接查询,安全地提取字段 + def _get(key: str, default=""): + try: + return row[key] + except (KeyError, IndexError): + return default + + return AgentThread( + id=_get("id"), + goal=_get("goal", ""), + agent_type=AgentType(_get("agent_type", "general")), + status=ThreadStatus(_get("status", "created")), + context_json=_get("context_json", "{}"), + parent_thread_id=_get("parent_thread_id") or None, + parent_turn_id=_get("parent_turn_id") or None, + error=_get("error") or None, + metadata=json.loads(_get("metadata_json", "{}")), + created_at=_get("created_at", ""), + updated_at=_get("updated_at", ""), + completed_at=_get("completed_at") or None, + duration_ms=_get("duration_ms") or None, + ) + + def close(self): + if self._conn: + self._conn.close() + self._conn = None + + +# ── MCP Server ────────────────────────────────────────────── + +async def run_mcp(db_path: str = ""): + """MCP Server 模式 — stdio JSON-RPC 2.0""" + mgr = AgentThreadManager(db_path) + + TOOLS = { + "agent_thread_spawn": { + "description": "派生新 Agent 线程 — 对标 codex.exe SubagentStart。创建子 Agent 执行指定目标,自动记录派生关系。", + "inputSchema": { + "type": "object", + "properties": { + "goal": {"type": "string", "description": "Agent 目标描述"}, + "agent_type": {"type": "string", "description": "Agent 类型: coder/reviewer/tester/planner/researcher/coordinator/shell_executor/file_editor/general"}, + "parent_thread_id": {"type": "string", "description": "父线程 ID (可选)"}, + "context": {"type": "object", "description": "上下文 JSON"}, + }, + "required": ["goal"], + }, + }, + "agent_thread_status": { + "description": "查询线程状态", + "inputSchema": { + "type": "object", + "properties": { + "thread_id": {"type": "string"}, + }, + "required": ["thread_id"], + }, + }, + "agent_thread_update": { + "description": "更新线程状态 — 对标 codex.exe SubagentStop", + "inputSchema": { + "type": "object", + "properties": { + "thread_id": {"type": "string"}, + "status": {"type": "string", "description": "created/running/paused/awaiting_approval/waiting_child/completed/failed/stopped/expired"}, + "error": {"type": "string"}, + }, + "required": ["thread_id", "status"], + }, + }, + "agent_thread_tree": { + "description": "获取线程的完整派生树", + "inputSchema": { + "type": "object", + "properties": { + "thread_id": {"type": "string", "description": "根线程 ID (可选,不传则返回完整森林)"}, + }, + }, + }, + "agent_thread_list": { + "description": "列出线程 (支持按状态/类型/父线程过滤)", + "inputSchema": { + "type": "object", + "properties": { + "status": {"type": "string"}, + "agent_type": {"type": "string"}, + "parent_thread_id": {"type": "string"}, + "limit": {"type": "integer", "default": 50}, + }, + }, + }, + "agent_thread_stop": { + "description": "级联停止线程及其所有子线程", + "inputSchema": { + "type": "object", + "properties": { + "thread_id": {"type": "string"}, + }, + "required": ["thread_id"], + }, + }, + "agent_thread_delete": { + "description": "级联删除线程及其子线程、动态工具、日志", + "inputSchema": { + "type": "object", + "properties": { + "thread_id": {"type": "string"}, + }, + "required": ["thread_id"], + }, + }, + "agent_tool_register": { + "description": "为线程注册动态工具 — 对标 codex.exe thread_dynamic_tools", + "inputSchema": { + "type": "object", + "properties": { + "thread_id": {"type": "string"}, + "tool_name": {"type": "string"}, + "tool_schema": {"type": "object", "description": "JSON Schema 工具定义"}, + }, + "required": ["thread_id", "tool_name", "tool_schema"], + }, + }, + "agent_tool_unregister": { + "description": "卸载线程的动态工具", + "inputSchema": { + "type": "object", + "properties": { + "thread_id": {"type": "string"}, + "tool_name": {"type": "string"}, + }, + "required": ["thread_id", "tool_name"], + }, + }, + "agent_tool_list": { + "description": "列出线程的所有动态工具", + "inputSchema": { + "type": "object", + "properties": { + "thread_id": {"type": "string"}, + }, + "required": ["thread_id"], + }, + }, + "agent_thread_logs": { + "description": "获取线程日志", + "inputSchema": { + "type": "object", + "properties": { + "thread_id": {"type": "string"}, + "level": {"type": "string", "description": "debug/info/warn/error"}, + "limit": {"type": "integer", "default": 50}, + }, + "required": ["thread_id"], + }, + }, + "agent_thread_stats": { + "description": "获取 Agent 线程系统统计", + "inputSchema": {"type": "object", "properties": {}}, + }, + "agent_thread_forest": { + "description": "获取所有根线程及其派生树 (完整森林)", + "inputSchema": {"type": "object", "properties": {}}, + }, + } + + # ── 标准 MCP JSON-RPC 2.0 stdio ── + for line in sys.stdin: + line = line.strip() + if not line: + continue + try: + req = json.loads(line) + except json.JSONDecodeError as e: + err = { + "jsonrpc": "2.0", + "error": {"code": -32700, "message": f"Parse error: {e}"}, + "id": None, + } + sys.stdout.write(json.dumps(err, ensure_ascii=False) + "\n") + sys.stdout.flush() + continue + + method = req.get("method", "") + params = req.get("params", {}) + rid = req.get("id", "") + + # ── initialize ── + if method == "initialize": + resp = { + "jsonrpc": "2.0", + "id": rid, + "result": { + "protocolVersion": "2025-03-26", + "capabilities": {"tools": {"listChanged": False}}, + "serverInfo": {"name": "deepcode-agent-threads", "version": "1.0.0"}, + }, + } + sys.stdout.write(json.dumps(resp, ensure_ascii=False) + "\n") + sys.stdout.flush() + continue + + # ── notifications/initialized ── + if method == "notifications/initialized": + continue + + if method == "tools/list": + print(json.dumps({ + "jsonrpc": "2.0", "id": rid, + "result": {"tools": [ + {"name": k, "description": v["description"], + "inputSchema": v["inputSchema"]} + for k, v in TOOLS.items() + ]}, + }), flush=True) + + elif method == "tools/call": + name = params.get("name", "") + args = params.get("arguments", {}) + result = {} + + try: + if name == "agent_thread_spawn": + at = agent_type_str = args.get("agent_type", "general") + try: + agent_type = AgentType(agent_type_str) + except ValueError: + agent_type = AgentType.GENERAL + thread = mgr.spawn( + goal=args["goal"], + agent_type=agent_type, + parent_thread_id=args.get("parent_thread_id"), + context=args.get("context"), + ) + result = thread.to_dict() + + elif name == "agent_thread_status": + thread = mgr.get(args["thread_id"]) + result = thread.to_dict() if thread else {"error": "not found"} + + elif name == "agent_thread_update": + status_str = args["status"] + try: + status = ThreadStatus(status_str) + except ValueError: + result = {"error": f"Invalid status: {status_str}"} + break + ok = mgr.update_status( + args["thread_id"], status, + error=args.get("error"), + ) + result = {"ok": ok, "thread_id": args["thread_id"], + "status": status_str} + + elif name == "agent_thread_tree": + tid = args.get("thread_id", "") + if tid: + result = mgr.get_full_tree(tid) + else: + result = {"forest": mgr.get_full_forest()} + + elif name == "agent_thread_list": + status = ThreadStatus(args["status"]) if args.get("status") else None + at = AgentType(args["agent_type"]) if args.get("agent_type") else None + threads = mgr.list_threads( + status=status, agent_type=at, + parent_thread_id=args.get("parent_thread_id"), + limit=args.get("limit", 50), + ) + result = {"threads": [t.to_dict() for t in threads]} + + elif name == "agent_thread_stop": + count = mgr.stop_thread_cascade(args["thread_id"]) + result = {"stopped": count, "thread_id": args["thread_id"]} + + elif name == "agent_thread_delete": + count = mgr.delete_thread_cascade(args["thread_id"]) + result = {"deleted": count, "thread_id": args["thread_id"]} + + elif name == "agent_tool_register": + dt = mgr.register_tool( + args["thread_id"], args["tool_name"], args["tool_schema"], + ) + result = dt.to_dict() if dt else {"error": "thread not found"} + + elif name == "agent_tool_unregister": + ok = mgr.unregister_tool(args["thread_id"], args["tool_name"]) + result = {"ok": ok} + + elif name == "agent_tool_list": + tools = mgr.list_tools(args["thread_id"]) + result = {"tools": [t.to_dict() for t in tools]} + + elif name == "agent_thread_logs": + level = LogLevel(args["level"]) if args.get("level") else None + logs = mgr.get_logs( + args["thread_id"], + limit=args.get("limit", 50), + level=level, + ) + result = {"logs": [l.to_dict() for l in logs]} + + elif name == "agent_thread_stats": + result = mgr.stats() + + elif name == "agent_thread_forest": + result = {"forest": mgr.get_full_forest()} + + else: + result = {"error": f"Unknown tool: {name}"} + + except Exception as e: + result = {"error": str(e), "ok": False} + + print(json.dumps({ + "jsonrpc": "2.0", "id": rid, + "result": { + "content": [{"type": "text", + "text": json.dumps(result, ensure_ascii=False)}], + }, + }), flush=True) + + # 忽略其他方法 (notifications 等) + + +# ── CLI 入口 ───────────────────────────────────────────────── + +def _print_json(obj): + print(json.dumps(obj, indent=2, ensure_ascii=False)) + + +def main(): + import argparse + parser = argparse.ArgumentParser( + description="DeepCode Agent Thread Manager — Agent 线程派生系统") + parser.add_argument("--mcp", action="store_true", help="MCP Server 模式") + parser.add_argument("--db", default=str(DEFAULT_DB), help="SQLite 数据库路径") + sub = parser.add_subparsers(dest="command") + + # spawn + p = sub.add_parser("spawn", help="派生新 Agent 线程") + p.add_argument("goal", help="目标") + p.add_argument("--type", dest="agent_type", default="general", + choices=[e.value for e in AgentType]) + p.add_argument("--parent", default=None, help="父线程 ID") + + # tree + sub.add_parser("tree", help="显示完整派生树") + sub.add_parser("forest", help="显示所有根线程") + sub.add_parser("stats", help="显示统计") + + # status + p = sub.add_parser("status", help="查看线程状态") + p.add_argument("thread_id") + + # stop + p = sub.add_parser("stop", help="级联停止线程") + p.add_argument("thread_id") + + # update + p = sub.add_parser("update", help="更新线程状态") + p.add_argument("thread_id") + p.add_argument("status", choices=[e.value for e in ThreadStatus]) + + # delete + p = sub.add_parser("delete", help="级联删除线程") + p.add_argument("thread_id") + + # tools + p = sub.add_parser("tools", help="列出线程的动态工具") + p.add_argument("thread_id") + + # logs + p = sub.add_parser("logs", help="查看线程日志") + p.add_argument("thread_id") + p.add_argument("--level", default=None) + + args = parser.parse_args() + + if args.mcp: + asyncio.run(run_mcp(args.db)) + return + + mgr = AgentThreadManager(args.db) + + if args.command == "spawn": + t = mgr.spawn(args.goal, AgentType(args.agent_type), + parent_thread_id=args.parent) + _print_json(t.to_dict()) + + elif args.command == "tree": + _print_json({"forest": mgr.get_full_forest()}) + + elif args.command == "forest": + for root in mgr.get_all_roots(): + print(f"[ROOT] {root.id[:16]} [{root.status.value}] {root.goal[:80]}") + _print_children(mgr, root.id, " ") + + elif args.command == "stats": + _print_json(mgr.stats()) + + elif args.command == "status": + t = mgr.get(args.thread_id) + _print_json(t.to_dict() if t else {"error": "not found"}) + + elif args.command == "stop": + n = mgr.stop_thread_cascade(args.thread_id) + print(f"Stopped {n} thread(s)") + + elif args.command == "update": + mgr.update_status(args.thread_id, ThreadStatus(args.status)) + print(f"Updated {args.thread_id} → {args.status}") + + elif args.command == "delete": + n = mgr.delete_thread_cascade(args.thread_id) + print(f"Deleted {n} thread(s)") + + elif args.command == "tools": + for t in mgr.list_tools(args.thread_id): + print(f" 🔧 {t.tool_name} (registered: {t.registered_at})") + + elif args.command == "logs": + level = LogLevel(args.level) if args.level else None + for l in mgr.get_logs(args.thread_id, level=level): + print(f" [{l.level.value}] {l.timestamp} {l.message}") + + else: + parser.print_help() + + mgr.close() + + +def _print_children(mgr: AgentThreadManager, tid: str, indent: str): + for child in mgr.get_child_threads(tid): + print(f"{indent}|- {child.id[:16]} [{child.status.value}] {child.goal[:60]}") + _print_children(mgr, child.id, indent + " ") + + +if __name__ == "__main__": + main() diff --git a/.deepcode/skills/deepcode-agent/plugin.json b/.deepcode/skills/deepcode-agent/plugin.json new file mode 100644 index 00000000..f7febcd2 --- /dev/null +++ b/.deepcode/skills/deepcode-agent/plugin.json @@ -0,0 +1,12 @@ +{ + "name": "deepcode-agent", + "version": "2.0.0", + "description": "DeepCode Agent — 完整 Agent 体系 (消息类型 + 线程派生系统), 移植自 codex.exe v0.145.0", + "author": "DeepCode + RE (codex.exe v0.145.0)", + "entry": "SKILL.md", + "dependencies": [], + "modules": [ + "agent.py", + "agent_thread_manager.py" + ] +} diff --git a/.deepcode/skills/deepcode-improvements/SKILL.md b/.deepcode/skills/deepcode-improvements/SKILL.md new file mode 100644 index 00000000..d9c5e031 --- /dev/null +++ b/.deepcode/skills/deepcode-improvements/SKILL.md @@ -0,0 +1,127 @@ +--- +name: deepcode-improvements +description: > + 7 Claude Code-inspired improvements for DEEPCODE: plan permission mode, + plugin URL loading, MCP strict mode, telemetry skeleton, multi-provider + abstraction, local settings level, extended hook events. + Use when the user asks about DEEPCODE features, improvements, or + mentions permission modes, plugin loading, MCP config, telemetry, + provider switching, settings hierarchy, or hook events. +version: 1.0.0 +date: 2026-07-27 +tags: [deepcode, improvements, architecture] +--- + +# DEEPCODE Improvements + +7 项改进,基于 CLAUDE CODE 2.1.216 逆向分析。 + +## 1. Plan 权限模式 + +```json +"permissions": { + "defaultMode": "plan", + "modes": { + "plan": { "description": "Plan mode: analyze and propose, never execute" } + } +} +``` + +plan 模式下 Agent 只分析和规划,不执行任何工具调用。适用于代码审查、 +架构设计等"只看不动"的任务。 + +**用法**: `--permission-mode plan` 或在 settings 中设置 `defaultMode: "plan"`。 + +## 2. Plugin URL 加载 + +```json +"pluginUrls": [ + "https://example.com/my-plugin.zip" +] +``` + +从 URL 动态加载插件 .zip 文件(无需提前下载到本地)。 + +**用法**: `--plugin-url https://example.com/plugin.zip` + +## 3. MCP Strict Mode + +```json +"strictMcpConfig": true +``` + +启用后,只使用 `--mcp-config` 指定的 MCP server,忽略 settings.json +中的所有其他 MCP 配置。用于隔离调试。 + +**用法**: `--strict-mcp-config` + +## 4. Telemetry 骨架 + +```json +"telemetry": { + "enabled": false, + "exporter": "otlp", + "otlpEndpoint": "http://localhost:4317", + "otlpHeaders": {}, + "otlpInsecure": false, + "prometheusHost": "127.0.0.1", + "prometheusPort": 9464 +} +``` + +OpenTelemetry 兼容的遥测出口。默认关闭。 + +**Env vars**: `OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_EXPORTER_OTLP_HEADERS`, +`OTEL_EXPORTER_OTLP_INSECURE`, `OTEL_EXPORTER_PROMETHEUS_HOST`, +`OTEL_EXPORTER_PROMETHEUS_PORT` + +## 5. 多 Provider 抽象 + +```json +"provider": { + "active": "deepseek", + "providers": { + "deepseek": { "type": "openai", "apiBase": "https://api.deepseek.com" }, + "anthropic": { "type": "anthropic", "apiBase": "https://api.anthropic.com" }, + "bedrock": { "type": "bedrock", "region": "${AWS_REGION}" }, + "vertex": { "type": "vertex", "project": "${GCP_PROJECT}" } + } +} +``` + +统一 provider 抽象层,支持 OpenAI-compatible / Anthropic / Bedrock / Vertex。 + +**切换**: `--provider anthropic` 或在 settings 中设置 `provider.active: "anthropic"`。 + +## 6. Local 级 Settings + +三层级继承: `user → project → local` + +| 层级 | 文件 | 用途 | +|------|------|------| +| user | `~/.deepcode/settings.json` | 全局默认 | +| project | `.deepcode/settings.json` | 项目共享 | +| local | `.deepcode/settings.local.json` | 本机专属(不提交 git) | + +local 设置覆盖 project 设置,project 覆盖 user。 + +**配置文件已存在**: `.deepcode/settings.local.json` + +## 7. 扩展 Hook 事件 + +```json +"hooks": { + "PreToolUse": [], "PostToolUse": [], + "SessionStart": [], "SessionEnd": [], + "Route": [], "OnError": [], + "PreTask": [], "PostTask": [] +} +``` + +新增事件: `Route`(路由决策)、`OnError`(错误处理)、 +`PreTask`(任务开始前)、`PostTask`(任务结束后)、`SessionEnd`(会话结束)。 + +## 启用建议 + +这 7 项改进已全部配置到 `.deepcode/settings.json` 和 `.deepcode/settings.local.json`。 +使用 `deepcode-engine` MCP 的 `permission_config` / `mcp_status` 等工具可查看状态。 diff --git a/.deepcode/skills/deepcode-plugin-manager/SKILL.md b/.deepcode/skills/deepcode-plugin-manager/SKILL.md new file mode 100644 index 00000000..59baf5ad --- /dev/null +++ b/.deepcode/skills/deepcode-plugin-manager/SKILL.md @@ -0,0 +1,57 @@ +--- +name: deepcode-plugin-manager +description: > + Plugin Manager — MCP 包装的插件热加载系统。 + 支持从 URL/ZIP/本地目录三种来源安装插件,内建 SHA256 签名校验。 + 工具: plugin__install, plugin__list, plugin__remove, plugin__info. + Use when the user asks to install a plugin, add a skill, load a plugin, + or mentions plugin URL loading, hot-loading skills, or plugin management. +version: 1.0.0 +author: DeepCode +date: 2026-07-30 +tags: [plugin, skill, loader, mcp, integration] +--- + +# DeepCode Plugin Manager + +将 `plugin_loader.py` 包装为 MCP 服务,AI 可直接通过工具调用管理插件。 + +## 工具一览 + +| 工具 | 说明 | +|:-----|:------| +| `plugin__install` | 安装插件 — 支持 URL/ZIP/目录三种来源 + SHA256 校验 | +| `plugin__list` | 列出所有已安装插件(含未注册) | +| `plugin__remove` | 卸载插件(删除文件 + 清理注册表) | +| `plugin__info` | 查看插件详情(注册信息 + 文件结构) | + +## 插件结构要求 + +``` +plugin.zip / plugin-dir/ +├── SKILL.md ← 必需 (YAML frontmatter + 提示词) +├── plugin.json ← 可选 (name/version/author/entry/dependencies) +├── checksum.sha256 ← 可选 (SHA256 签名) +└── *.py / *.js ← 可选 (工具脚本) +``` + +## 安全机制 + +1. **SHA256 校验**: URL 安装支持 `--checksum` 参数显式验证 +2. **内嵌 checksum**: ZIP 内含 `checksum.sha256` 文件自动验证 +3. **自动查找**: 尝试从 URL 同路径加载 `.sha256` 文件 +4. **结构验证**: 缺少 `SKILL.md` 的插件拒绝安装 + +## 安装路径 + +所有插件安装到 `.deepcode/skills/{name}/`,注册信息写入 `.deepcode/plugin_registry.json`。 + +## 使用示例 + +``` +# AI 调用示例: +plugin__install source="https://example.com/my-plugin.zip" +plugin__list +plugin__info name="my-plugin" +plugin__remove name="my-plugin" +``` diff --git a/.deepcode/skills/deepcode-plugin-manager/plugin_manager_server.py b/.deepcode/skills/deepcode-plugin-manager/plugin_manager_server.py new file mode 100644 index 00000000..81256970 --- /dev/null +++ b/.deepcode/skills/deepcode-plugin-manager/plugin_manager_server.py @@ -0,0 +1,318 @@ +#!/usr/bin/env python3 +""" +Plugin Manager MCP Server +══════════════════════════ +将 plugin_loader.py 包装为 MCP 工具,AI 可通过工具调用直接安装/管理插件。 + +工具: + plugin__install — 安装插件(URL/ZIP/目录 + SHA256 校验) + plugin__list — 列出所有插件 + plugin__remove — 卸载插件 + +注册到 .mcp.json: + "deepcode-plugin-manager": { + "command": "python", + "args": ["F:/DEEPCODE/.deepcode/skills/deepcode-plugin-manager/plugin_manager_server.py"] + } +""" + +import json +import sys +import os +import io +import contextlib +from pathlib import Path + +# 确保能找到 plugin_loader.py +PLUGIN_LOADER_DIR = Path(__file__).resolve().parent.parent.parent # .deepcode/ +sys.path.insert(0, str(PLUGIN_LOADER_DIR)) + +from plugin_loader import ( + install_from_dir, + install_from_zip, + install_from_url, + list_plugins, + remove_plugin, + load_registry, +) + + +def _silent_call(func, *args, **kwargs): + """调用 plugin_loader 函数,将其 stdout 重定向到 stderr 以避免污染 MCP JSON""" + buf = io.StringIO() + with contextlib.redirect_stdout(buf): + result = func(*args, **kwargs) + # 将日志输出到 stderr + log = buf.getvalue() + if log.strip(): + for line in log.strip().split("\n"): + print(f"[plugin] {line}", file=sys.stderr, flush=True) + return result + + +def _result(ok: bool, data=None, error: str = "") -> str: + """统一返回格式""" + resp = {"ok": ok} + if data is not None: + resp["data"] = data + if error: + resp["error"] = error + return json.dumps(resp, ensure_ascii=False, indent=2) + + +# ── MCP 工具处理器 ────────────────────────────────────────────── + +MCP_TOOLS = { + "plugin__install": { + "description": "安装插件 — 支持 URL/ZIP/本地目录三种来源,可选 SHA256 校验", + "inputSchema": { + "type": "object", + "properties": { + "source": { + "type": "string", + "description": "插件来源:URL (https://...)、ZIP 文件路径、或本地目录路径", + }, + "name": { + "type": "string", + "description": "插件名称(可选,默认从 plugin.json 或路径名自动识别)", + }, + "checksum": { + "type": "string", + "description": "SHA256 签名(可选,仅 URL 安装时使用)", + }, + }, + "required": ["source"], + }, + "handler": "handle_install", + }, + "plugin__list": { + "description": "列出所有已安装的插件(含已注册和未注册的)", + "inputSchema": { + "type": "object", + "properties": {}, + }, + "handler": "handle_list", + }, + "plugin__remove": { + "description": "卸载指定插件(删除文件 + 清理注册表)", + "inputSchema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "要卸载的插件名称", + }, + }, + "required": ["name"], + }, + "handler": "handle_remove", + }, + "plugin__info": { + "description": "查看插件详情(注册信息 + 目录文件列表)", + "inputSchema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "插件名称", + }, + }, + "required": ["name"], + }, + "handler": "handle_info", + }, +} + + +def handle_install(params: dict) -> str: + source = params.get("source", "").strip() + name = params.get("name") + checksum = params.get("checksum") + + if not source: + return _result(False, error="source is required") + + try: + if source.startswith("http://") or source.startswith("https://"): + ok = _silent_call(install_from_url, source, plugin_name=name, checksum=checksum) + elif source.endswith(".zip"): + ok = _silent_call(install_from_zip, Path(source)) + else: + ok = _silent_call(install_from_dir, Path(source), plugin_name=name) + + if ok: + return _result(True, data={"message": f"Plugin installed successfully", "source": source}) + return _result(False, error="Plugin installation failed (see stderr for details)") + except Exception as e: + return _result(False, error=str(e)) + + +def handle_list(params: dict) -> str: + try: + plugins = _silent_call(list_plugins) + reg = _silent_call(load_registry) + registered = list(reg.get("plugins", {}).keys()) + return _result(True, data={ + "total": len(plugins), + "registered": registered, + "plugins": plugins, + }) + except Exception as e: + return _result(False, error=str(e)) + + +def handle_remove(params: dict) -> str: + name = params.get("name", "").strip() + if not name: + return _result(False, error="name is required") + + try: + ok = _silent_call(remove_plugin, name) + return _result(True, data={"message": f"Plugin '{name}' removed"}) + except Exception as e: + return _result(False, error=str(e)) + + +def handle_info(params: dict) -> str: + name = params.get("name", "").strip() + if not name: + return _result(False, error="name is required") + + try: + reg = _silent_call(load_registry) + plugin_info = reg.get("plugins", {}).get(name) + if not plugin_info: + return _result(False, error=f"Plugin '{name}' not found in registry") + + # 列出目录结构 + plugin_dir = PLUGIN_LOADER_DIR / "skills" / name + files = [] + if plugin_dir.exists(): + for f in sorted(plugin_dir.rglob("*")): + if f.is_file(): + rel = f.relative_to(plugin_dir) + files.append(str(rel)) + + return _result(True, data={ + "name": name, + "registry": plugin_info, + "directory": str(plugin_dir) if plugin_dir.exists() else None, + "files": files, + }) + except Exception as e: + return _result(False, error=str(e)) + + +# ── MCP stdio 协议 ───────────────────────────────────────────── + +HANDLERS = { + "handle_install": handle_install, + "handle_list": handle_list, + "handle_remove": handle_remove, + "handle_info": handle_info, +} + + +def handle_mcp_request(request: dict) -> dict: + """处理 MCP JSON-RPC 请求""" + req_id = request.get("id") + method = request.get("method", "") + + # Initialize / tools/list + if method == "initialize": + return { + "jsonrpc": "2.0", + "id": req_id, + "result": { + "protocolVersion": "2024-11-05", + "capabilities": {"tools": {}}, + "serverInfo": { + "name": "deepcode-plugin-manager", + "version": "1.0.0", + }, + }, + } + + if method == "notifications/initialized": + return {"jsonrpc": "2.0", "id": req_id, "result": {}} + + if method == "tools/list": + return { + "jsonrpc": "2.0", + "id": req_id, + "result": { + "tools": [ + { + "name": name, + "description": meta["description"], + "inputSchema": meta["inputSchema"], + } + for name, meta in MCP_TOOLS.items() + ] + }, + } + + if method == "tools/call": + tool_name = request.get("params", {}).get("name", "") + arguments = request.get("params", {}).get("arguments", {}) + + for name, meta in MCP_TOOLS.items(): + if name == tool_name: + handler = HANDLERS.get(meta["handler"]) + if handler: + result_text = handler(arguments) + return { + "jsonrpc": "2.0", + "id": req_id, + "result": { + "content": [ + {"type": "text", "text": result_text} + ] + }, + } + + return { + "jsonrpc": "2.0", + "id": req_id, + "error": {"code": -32601, "message": f"Tool not found: {tool_name}"}, + } + + return { + "jsonrpc": "2.0", + "id": req_id, + "error": {"code": -32601, "message": f"Method not found: {method}"}, + } + + +def main(): + """MCP stdio 模式:通过 stdin/stdout 通信""" + # 设置 UTF-8 + if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8") + if hasattr(sys.stdin, "reconfigure"): + sys.stdin.reconfigure(encoding="utf-8") + + for line in sys.stdin: + line = line.strip() + if not line: + continue + try: + request = json.loads(line) + response = handle_mcp_request(request) + print(json.dumps(response, ensure_ascii=False), flush=True) + except json.JSONDecodeError as e: + print(json.dumps({ + "jsonrpc": "2.0", + "id": None, + "error": {"code": -32700, "message": f"Parse error: {e}"}, + }), flush=True) + except Exception as e: + print(json.dumps({ + "jsonrpc": "2.0", + "id": None, + "error": {"code": -32603, "message": f"Internal error: {e}"}, + }), flush=True) + + +if __name__ == "__main__": + main() diff --git a/.deepcode/skills/deepcode-sandbox/app_server.py b/.deepcode/skills/deepcode-sandbox/app_server.py new file mode 100644 index 00000000..b0806eae --- /dev/null +++ b/.deepcode/skills/deepcode-sandbox/app_server.py @@ -0,0 +1,696 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +DeepCode Internal App Server — 移植自 CODEX.EXE 远程控制架构 +══════════════════════════════════════════════════════════════ +对标 CODEX.EXE v0.145.0: + - CODEX_INTERNAL_APP_SERVER_REMOTE_CONTROL_DISABLED 环境变量 + - CreateThread + Named Pipe IPC + - WaitForSingleObject / CloseHandle 生命周期 + - 远程命令: stop / status / ping / exec / reload + +架构: + Main Thread Worker Thread + ┌──────────────┐ ┌──────────────────┐ + │ codex-main │───CreateThread──→│ FUN_14d90f890 │ + │ │ │ │ + │ WaitFor │ │ Named Pipe │ + │ SingleObject │ │ \\.\\pipe\\dc-{pid}│ + │ │ │ │ + │ CloseHandle │ │ Command Handler │ + └──────────────┘ └──────────────────┘ + +用法: + # 启动服务器 (阻塞当前线程) + python app_server.py start + + # 发送远程命令 + python app_server.py ping + python app_server.py stop + python app_server.py status + python app_server.py exec "ls -la" + + # 作为 MCP Server + python app_server.py --mcp +""" + +import asyncio +import ctypes +import json +import os +import signal +import socket +import struct +import sys +import threading +import time +import uuid +from dataclasses import dataclass, field +from datetime import datetime +from enum import Enum +from pathlib import Path +from typing import Any, Callable, Dict, List, Optional + +# ── 常量 ────────────────────────────────────────────────────── + +ENV_DISABLE_KEY = "CODEX_INTERNAL_APP_SERVER_REMOTE_CONTROL_DISABLED" +PIPE_NAME_TEMPLATE = r"\\.\pipe\deepcode-internal-{pid}" +DEFAULT_TIMEOUT = 30.0 +MAX_MESSAGE_SIZE = 65536 # 64KB + +IS_WINDOWS = sys.platform == "win32" + +if IS_WINDOWS: + try: + import win32pipe + import win32file + import win32event + import pywintypes + HAS_PYWIN32 = True + except ImportError: + HAS_PYWIN32 = False +else: + HAS_PYWIN32 = False + + +# ── 枚举 ────────────────────────────────────────────────────── + +class ServerCommandType(str, Enum): + PING = "ping" + STOP = "stop" + STATUS = "status" + EXEC = "exec" + RELOAD = "reload" + SHUTDOWN = "shutdown" + + +class ServerStatus(str, Enum): + STARTING = "starting" + RUNNING = "running" + STOPPING = "stopping" + STOPPED = "stopped" + + +# ── 数据类 ──────────────────────────────────────────────────── + +@dataclass +class ServerState: + """服务器状态""" + pid: int = field(default_factory=os.getpid) + status: ServerStatus = ServerStatus.STARTING + started_at: str = "" + command_count: int = 0 + last_command: str = "" + last_command_at: str = "" + + def to_dict(self) -> Dict: + return { + "pid": self.pid, + "status": self.status.value, + "started_at": self.started_at, + "command_count": self.command_count, + "last_command": self.last_command, + "last_command_at": self.last_command_at, + } + + +@dataclass +class ServerCommand: + """服务器命令""" + id: str = "" + command: ServerCommandType = ServerCommandType.PING + payload: Dict[str, Any] = field(default_factory=dict) + timestamp: str = "" + + def __post_init__(self): + if not self.id: + self.id = f"cmd_{uuid.uuid4().hex[:8]}" + if not self.timestamp: + self.timestamp = datetime.now().isoformat() + + +@dataclass +class ServerResponse: + """服务器响应""" + command_id: str = "" + ok: bool = True + data: Dict[str, Any] = field(default_factory=dict) + error: str = "" + timestamp: str = "" + server_pid: int = 0 + server_status: str = "" + + def __post_init__(self): + if not self.timestamp: + self.timestamp = datetime.now().isoformat() + + +# ── 内部应用服务器 ──────────────────────────────────────────── + +class InternalAppServer: + """ + 内部应用服务器 — 对标 CODEX.EXE 的 CreateThread + Named Pipe 架构 + + 对标: + - CODEX_INTERNAL_APP_SERVER_REMOTE_CONTROL_DISABLED → ENV_DISABLE_KEY + - CreateThread(..., FUN_14d90f890, ...) → _server_thread + - WaitForSingleObject + CloseHandle → stop_event + - Named Pipe: \\\\.\\pipe\\codex-internal-{pid} → PIPE_NAME_TEMPLATE + + 生命周期: + 1. 检查 ENV_DISABLE_KEY — 若设置则跳过 + 2. 创建 Named Pipe 或 Unix Socket + 3. 后台线程 listen + 4. 主线程 WaitForSingleObject(stop_event) + 5. 收到 stop → CloseHandle → 退出 + """ + + def __init__(self, pipe_name: str = "", command_handler: Optional[Callable] = None): + self._pipe_name = pipe_name or PIPE_NAME_TEMPLATE.format(pid=os.getpid()) + self._command_handler = command_handler or self._default_handler + self._state = ServerState() + self._stop_event = threading.Event() + self._server_thread: Optional[threading.Thread] = None + self._running = False + + @property + def is_disabled(self) -> bool: + """对标 CODEX_INTERNAL_APP_SERVER_REMOTE_CONTROL_DISABLED""" + return os.environ.get(ENV_DISABLE_KEY, "").strip().lower() in ("1", "true", "yes") + + @property + def state(self) -> ServerState: + return self._state + + # ── 服务器生命周期 ───────────────────────────────────── + + def start(self, blocking: bool = True) -> Optional[threading.Thread]: + """ + 启动内部应用服务器 + + Args: + blocking: True=阻塞当前线程, False=后台线程运行 + + Returns: + 后台线程 (blocking=False 时) + """ + if self.is_disabled: + return None + + self._state.started_at = datetime.now().isoformat() + self._state.status = ServerStatus.RUNNING + self._stop_event.clear() + + if blocking: + self._run_server() + else: + self._server_thread = threading.Thread( + target=self._run_server, daemon=True, name="DeepCode-AppServer" + ) + self._server_thread.start() + return self._server_thread + + def stop(self): + """停止服务器""" + self._state.status = ServerStatus.STOPPING + self._stop_event.set() + # 发送一个空连接来唤醒 pipe listen + try: + self._send_local_command(ServerCommandType.SHUTDOWN) + except Exception: + pass + if self._server_thread and self._server_thread.is_alive(): + self._server_thread.join(timeout=5) + self._state.status = ServerStatus.STOPPED + + # ── Named Pipe 实现 ──────────────────────────────────── + + def _run_server(self): + """服务器主循环 — 对标 FUN_14d90f890""" + self._running = True + + if IS_WINDOWS: + self._run_named_pipe_server() + else: + self._run_unix_socket_server() + + def _run_named_pipe_server(self): + """Windows Named Pipe 服务器 — 对标 CODEX.EXE Named Pipe IPC""" + if not HAS_PYWIN32: + # Fallback: 使用 socket (比 Named Pipe 慢但跨平台) + self._run_socket_fallback() + return + + pipe_name = self._pipe_name + while not self._stop_event.is_set(): + try: + # 创建 Named Pipe + pipe = win32pipe.CreateNamedPipe( + pipe_name, + win32pipe.PIPE_ACCESS_DUPLEX, + win32pipe.PIPE_TYPE_MESSAGE | win32pipe.PIPE_READMODE_MESSAGE | win32pipe.PIPE_WAIT, + 1, # max instances + MAX_MESSAGE_SIZE, + MAX_MESSAGE_SIZE, + 1000, # default timeout ms + None, # security attributes + ) + + # 等待客户端连接 + result = win32pipe.ConnectNamedPipe(pipe, None) + if result == 0 and ctypes.GetLastError() == 535: # ERROR_PIPE_CONNECTED + pass + + # 读取命令 + if not self._stop_event.is_set(): + data = self._read_pipe_message(pipe) + if data: + self._handle_message(data) + + win32pipe.DisconnectNamedPipe(pipe) + win32file.CloseHandle(pipe) + + except Exception as e: + if not self._stop_event.is_set(): + time.sleep(0.1) + + def _read_pipe_message(self, pipe) -> Optional[bytes]: + """从 Named Pipe 读取消息""" + try: + hr, data = win32file.ReadFile(pipe, MAX_MESSAGE_SIZE) + return data + except pywintypes.error as e: + if e.winerror != 109: # ERROR_BROKEN_PIPE + raise + return None + + def _run_unix_socket_server(self): + """Unix Domain Socket 服务器""" + sock_path = f"/tmp/deepcode-internal-{os.getpid()}.sock" + try: + os.unlink(sock_path) + except OSError: + pass + + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.bind(sock_path) + sock.listen(1) + sock.settimeout(1.0) # 1秒超时用于检查 stop_event + + while not self._stop_event.is_set(): + try: + conn, _ = sock.accept() + data = conn.recv(MAX_MESSAGE_SIZE) + if data: + self._handle_message(data) + response = self._state.to_dict() + conn.send(json.dumps(response).encode()) + conn.close() + except socket.timeout: + continue + except Exception: + if not self._stop_event.is_set(): + time.sleep(0.1) + + sock.close() + try: + os.unlink(sock_path) + except OSError: + pass + + def _run_socket_fallback(self): + """TCP Socket fallback (无 pywin32 时)""" + port = 17000 + (os.getpid() % 10000) + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind(("127.0.0.1", port)) + sock.listen(1) + sock.settimeout(1.0) + + while not self._stop_event.is_set(): + try: + conn, _ = sock.accept() + data = conn.recv(MAX_MESSAGE_SIZE) + if data: + self._handle_message(data) + response = json.dumps(self._state.to_dict()).encode() + conn.send(response) + conn.close() + except socket.timeout: + continue + except Exception: + if not self._stop_event.is_set(): + time.sleep(0.1) + + sock.close() + + # ── 命令处理 ────────────────────────────────────────── + + def _handle_message(self, data: bytes): + """处理收到的命令""" + try: + msg = json.loads(data.decode("utf-8")) + cmd = ServerCommandType( + id=msg.get("id", ""), + command=ServerCommand(msg.get("command", "ping")), + payload=msg.get("payload", {}), + ) + + self._state.command_count += 1 + self._state.last_command = cmd.command.value + self._state.last_command_at = datetime.now().isoformat() + + response = self._command_handler(cmd, self._state) + + except (json.JSONDecodeError, ValueError) as e: + response = ServerResponse( + command_id="unknown", ok=False, + error=f"Invalid command: {e}", + server_pid=os.getpid(), + server_status=self._state.status.value, + ) + + def _default_handler(self, cmd: ServerCommandType, state: ServerState) -> ServerResponse: + """默认命令处理器""" + if cmd.command == ServerCommandType.PING: + return ServerResponse( + command_id=cmd.id, ok=True, + data={"pong": True, "uptime_sec": state.started_at}, + server_pid=state.pid, server_status=state.status.value, + ) + elif cmd.command == ServerCommandType.STATUS: + return ServerResponse( + command_id=cmd.id, ok=True, + data=state.to_dict(), + server_pid=state.pid, server_status=state.status.value, + ) + elif cmd.command in (ServerCommandType.STOP, ServerCommandType.SHUTDOWN): + self._stop_event.set() + return ServerResponse( + command_id=cmd.id, ok=True, + data={"stopping": True}, + server_pid=state.pid, server_status=ServerStatus.STOPPING.value, + ) + elif cmd.command == ServerCommandType.EXEC: + return ServerResponse( + command_id=cmd.id, ok=False, + error="EXEC handler not configured", + server_pid=state.pid, server_status=state.status.value, + ) + else: + return ServerResponse( + command_id=cmd.id, ok=False, + error=f"Unknown command: {cmd.command.value}", + server_pid=state.pid, server_status=state.status.value, + ) + + def _send_local_command(self, command: ServerCommandType): + """向本地服务器发送命令 (客户端侧)""" + payload = json.dumps({ + "id": command.id, "command": command.command.value, + "payload": command.payload, + }).encode() + + if IS_WINDOWS and HAS_PYWIN32: + # Named Pipe 客户端 + pipe = win32file.CreateFile( + self._pipe_name, + win32file.GENERIC_READ | win32file.GENERIC_WRITE, + 0, None, win32file.OPEN_EXISTING, 0, None, + ) + win32file.WriteFile(pipe, payload) + win32file.CloseHandle(pipe) + elif IS_WINDOWS: + # TCP fallback + port = 17000 + (os.getpid() % 10000) + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect(("127.0.0.1", port)) + sock.send(payload) + sock.close() + else: + # Unix socket + sock_path = f"/tmp/deepcode-internal-{os.getpid()}.sock" + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.connect(sock_path) + sock.send(payload) + sock.close() + + +# ── 客户端 ──────────────────────────────────────────────────── + +class AppServerClient: + """内部应用服务器客户端 — 对标 CODEX 的远程控制客户端""" + + def __init__(self, server_pid: Optional[int] = None): + self._server_pid = server_pid or os.getpid() + + def _send(self, command: ServerCommand) -> ServerResponse: + pipe_name = PIPE_NAME_TEMPLATE.format(pid=self._server_pid) + payload = json.dumps({ + "id": command.id, "command": command.command.value, + "payload": command.payload, + }).encode() + + data = None + if IS_WINDOWS and HAS_PYWIN32: + try: + pipe = win32file.CreateFile( + pipe_name, + win32file.GENERIC_READ | win32file.GENERIC_WRITE, + 0, None, win32file.OPEN_EXISTING, 0, None, + ) + win32file.WriteFile(pipe, payload) + hr, data = win32file.ReadFile(pipe, MAX_MESSAGE_SIZE) + win32file.CloseHandle(pipe) + except pywintypes.error as e: + return ServerResponse( + command_id=command.id, ok=False, + error=f"Pipe error: {e.strerror} (code={e.winerror})", + ) + else: + try: + if IS_WINDOWS: + port = 17000 + (self._server_pid % 10000) + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect(("127.0.0.1", port)) + else: + sock_path = f"/tmp/deepcode-internal-{self._server_pid}.sock" + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.connect(sock_path) + sock.send(payload) + data = sock.recv(MAX_MESSAGE_SIZE) + sock.close() + except Exception as e: + return ServerResponse( + command_id=command.id, ok=False, + error=f"Connection error: {e}", + ) + + if data: + try: + resp = json.loads(data.decode("utf-8")) + return ServerResponse(**resp) + except Exception: + return ServerResponse(command_id=command.id, ok=False, error="Invalid response") + + return ServerResponse(command_id=command.id, ok=False, error="No response") + + def ping(self) -> ServerResponse: + return self._send(ServerCommand(command=ServerCommandType.PING)) + + def status(self) -> ServerResponse: + return self._send(ServerCommand(command=ServerCommandType.STATUS)) + + def stop(self) -> ServerResponse: + return self._send(ServerCommand(command=ServerCommandType.STOP)) + + def exec(self, payload: Dict[str, Any]) -> ServerResponse: + return self._send(ServerCommand(command=ServerCommandType.EXEC, payload=payload)) + + +# ── 全局单例 ────────────────────────────────────────────────── + +_server_instance: Optional[InternalAppServer] = None + + +def get_server() -> InternalAppServer: + global _server_instance + if _server_instance is None: + _server_instance = InternalAppServer() + return _server_instance + + +# ── MCP Server ───────────────────────────────────────────────── + +async def run_mcp(): + """MCP Server 模式""" + server = get_server() + + # 启动后台服务器 + if not server.is_disabled: + server.start(blocking=False) + + TOOLS = { + "app_server_ping": { + "description": "Ping 内部应用服务器 — 对标 CODEX 远程控制 ping", + "inputSchema": {"type": "object", "properties": {}}, + }, + "app_server_status": { + "description": "获取内部应用服务器状态", + "inputSchema": {"type": "object", "properties": {}}, + }, + "app_server_stop": { + "description": "停止内部应用服务器", + "inputSchema": {"type": "object", "properties": {}}, + }, + "app_server_exec": { + "description": "通过内部应用服务器执行命令", + "inputSchema": { + "type": "object", + "properties": { + "command": {"type": "string"}, + "args": {"type": "array", "items": {"type": "string"}}, + }, + "required": ["command"], + }, + }, + } + + # ── 标准 MCP JSON-RPC 2.0 stdio ── + for line in sys.stdin: + line = line.strip() + if not line: + continue + try: + req = json.loads(line) + except json.JSONDecodeError as e: + err = { + "jsonrpc": "2.0", + "error": {"code": -32700, "message": f"Parse error: {e}"}, + "id": None, + } + sys.stdout.write(json.dumps(err, ensure_ascii=False) + "\n") + sys.stdout.flush() + continue + + method = req.get("method", "") + params = req.get("params", {}) + rid = req.get("id", "") + + # ── initialize ── + if method == "initialize": + resp = { + "jsonrpc": "2.0", + "id": rid, + "result": { + "protocolVersion": "2025-03-26", + "capabilities": {"tools": {"listChanged": False}}, + "serverInfo": {"name": "deepcode-app-server", "version": "1.0.0"}, + }, + } + sys.stdout.write(json.dumps(resp, ensure_ascii=False) + "\n") + sys.stdout.flush() + continue + + # ── notifications/initialized ── + if method == "notifications/initialized": + continue + + if method == "tools/list": + print(json.dumps({ + "jsonrpc": "2.0", "id": rid, + "result": {"tools": [ + {"name": k, "description": v["description"], + "inputSchema": v["inputSchema"]} + for k, v in TOOLS.items() + ]}, + }), flush=True) + + elif method == "tools/call": + name = params.get("name", "") + args = params.get("arguments", {}) + result = {} + + try: + if name == "app_server_ping": + result = {"is_disabled": server.is_disabled, + "state": server.state.to_dict()} + elif name == "app_server_status": + result = server.state.to_dict() + elif name == "app_server_stop": + server.stop() + result = {"stopped": True} + elif name == "app_server_exec": + cmd = ServerCommandType( + command=ServerCommandType.EXEC, + payload={"command": args["command"], + "args": args.get("args", [])}, + ) + server._send_local_command(cmd) + result = {"sent": True, "command": args["command"]} + else: + result = {"error": f"Unknown: {name}"} + except Exception as e: + result = {"error": str(e)} + + print(json.dumps({ + "jsonrpc": "2.0", "id": rid, + "result": { + "content": [{"type": "text", + "text": json.dumps(result, ensure_ascii=False)}], + }, + }), flush=True) + + +# ── CLI ──────────────────────────────────────────────────────── + +def main(): + import argparse + parser = argparse.ArgumentParser( + description="DeepCode Internal App Server — CODEX 远程控制移植") + parser.add_argument("--mcp", action="store_true", help="MCP Server 模式") + parser.add_argument("--pid", type=int, default=0, help="目标服务器 PID") + sub = parser.add_subparsers(dest="command") + + sub.add_parser("start", help="启动内部应用服务器 (阻塞)") + sub.add_parser("ping", help="Ping 服务器") + sub.add_parser("status", help="获取服务器状态") + sub.add_parser("stop", help="停止服务器") + p = sub.add_parser("exec", help="发送执行命令") + p.add_argument("payload", nargs="+", help="命令 + 参数") + + args = parser.parse_args() + + if args.mcp: + asyncio.run(run_mcp()) + return + + if args.command == "start": + server = get_server() + if server.is_disabled: + print(f"Disabled by {ENV_DISABLE_KEY}=1") + return + print(f"Starting DeepCode App Server on {PIPE_NAME_TEMPLATE.format(pid=os.getpid())}") + server.start(blocking=True) + + else: + client = AppServerClient(server_pid=args.pid or os.getpid()) + + if args.command == "ping": + resp = client.ping() + print(json.dumps(resp.__dict__, indent=2, default=str)) + elif args.command == "status": + resp = client.status() + print(json.dumps(resp.__dict__, indent=2, default=str)) + elif args.command == "stop": + resp = client.stop() + print(json.dumps(resp.__dict__, indent=2, default=str)) + elif args.command == "exec": + resp = client.exec({"command": " ".join(args.payload)}) + print(json.dumps(resp.__dict__, indent=2, default=str)) + else: + parser.print_help() + + +if __name__ == "__main__": + main() diff --git a/.deepcode/skills/deepcode-starlark/SKILL.md b/.deepcode/skills/deepcode-starlark/SKILL.md new file mode 100644 index 00000000..601ecfd7 --- /dev/null +++ b/.deepcode/skills/deepcode-starlark/SKILL.md @@ -0,0 +1,41 @@ +--- +name: deepcode-starlark +description: > + DeepCode Starlark Engine — 移植自 CODEX.EXE 的 starlark-0.14.2。 + 安全脚本执行、确定性计算、Hook 规则引擎、配置 DSL。 + 对标 codex.exe: 内置 Starlark 解释器做安全规则和配置。 +version: 1.0.0 +author: DeepCode + RE (codex.exe v0.145.0) +date: 2026-07-28 +tags: [starlark, scripting, rules, security, dsl] +--- + +# DeepCode Starlark Engine + +移植自 **CODEX.EXE** 的 Starlark 脚本引擎 (starlark-0.14.2)。 + +## 核心能力 + +| 能力 | 说明 | +|:------|:-----| +| 安全执行 | 无 I/O, 无 random, 无 time — 确定性 | +| 规则引擎 | Hook 规则 / 权限策略 | +| 配置 DSL | 结构化配置语言 | +| 零依赖 | 纯 Python AST 实现 | + +## CLI + +```bash +python starlark_engine.py run --code "result = sum(range(100))" +python starlark_engine.py rules --ctx '{"tool":"Bash","command":"ls"}' +python starlark_engine.py demo +``` + +## MCP Server + +```json +"deepcode-starlark": { + "command": "python", + "args": ["F:/DEEPCODE/.deepcode/skills/deepcode-starlark/starlark_engine.py", "--mcp"] +} +``` diff --git a/.deepcode/skills/deepcode-starlark/plugin.json b/.deepcode/skills/deepcode-starlark/plugin.json new file mode 100644 index 00000000..e12b79b8 --- /dev/null +++ b/.deepcode/skills/deepcode-starlark/plugin.json @@ -0,0 +1,9 @@ +{ + "name": "deepcode-starlark", + "version": "1.0.0", + "description": "DeepCode Starlark Engine — 移植自 CODEX.EXE starlark-0.14.2, 安全脚本执行 + 规则引擎", + "author": "DeepCode + RE (codex.exe v0.145.0)", + "entry": "SKILL.md", + "dependencies": [], + "modules": ["starlark_engine.py"] +} diff --git a/.deepcode/skills/deepcode-starlark/starlark_engine.py b/.deepcode/skills/deepcode-starlark/starlark_engine.py new file mode 100644 index 00000000..822461b0 --- /dev/null +++ b/.deepcode/skills/deepcode-starlark/starlark_engine.py @@ -0,0 +1,565 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +DeepCode Starlark Engine — 移植自 CODEX.EXE 的 Starlark 脚本引擎 +═══════════════════════════════════════════════════════════════ +对标 CODEX.EXE v0.145.0: + - starlark-0.14.2 crate → 纯 Python 实现 + - 确定性执行 (无 I/O, 无 random, 无 time) + - 安全沙箱 (限制内置函数) + - 规则引擎 + 配置脚本 + +核心能力: + 1. 安全脚本执行 — 无副作用, 纯计算 + 2. Hook 规则 — if/then 逻辑定义安全策略 + 3. 配置 DSL — 结构化配置语言 + 4. 管道过滤 — 数据转换规则 + +用法: + # 执行 Starlark 脚本 + python starlark_engine.py run --file rules.star + + # 直接执行代码 + python starlark_engine.py run --code "x = [1,2,3]; result = sum(x)" + + # 作为 MCP Server + python starlark_engine.py --mcp +""" + +import asyncio +import ast +import json +import math +import operator +import os +import re +import sys +import uuid +from dataclasses import dataclass, field +from datetime import datetime +from enum import Enum +from pathlib import Path +from typing import Any, Callable, Dict, List, Optional, Set, Tuple + + +# ── Starlark 类型 ────────────────────────────────────────────── + +class StarlarkType(str, Enum): + NONE = "NoneType" + BOOL = "bool" + INT = "int" + FLOAT = "float" + STRING = "str" + LIST = "list" + DICT = "dict" + TUPLE = "tuple" + SET = "set" + FUNCTION = "function" + + +# ── 安全限制 ─────────────────────────────────────────────────── + +# Starlark 不允许的内置函数 +DENIED_BUILTINS: Set[str] = { + "exec", "eval", "compile", "open", "file", + "__import__", "input", "raw_input", + "globals", "locals", "vars", + "breakpoint", "memoryview", + # I/O + "print", # Starlark 标准: 不允许 print (可选) +} + +# Starlark 不允许的模块 +DENIED_MODULES: Set[str] = { + "os", "sys", "subprocess", "shutil", "socket", + "ctypes", "multiprocessing", "threading", + "io", "pickle", "marshal", "code", + "importlib", "inspect", "signal", +} + +# 允许的 safe 内置 +SAFE_BUILTINS: Dict[str, Any] = { + "True": True, "False": False, "None": None, + "abs": abs, "all": all, "any": any, + "bin": bin, "bool": bool, "bytes": bytes, + "chr": chr, "dict": dict, "dir": dir, + "divmod": divmod, "enumerate": enumerate, + "filter": filter, "float": float, + "format": format, "frozenset": frozenset, + "hash": hash, "hex": hex, + "int": int, "isinstance": isinstance, + "issubclass": issubclass, "iter": iter, + "len": len, "list": list, "map": map, + "max": max, "min": min, "next": next, + "oct": oct, "ord": ord, "pow": pow, + "range": range, "repr": repr, + "reversed": reversed, "round": round, + "set": set, "slice": slice, "sorted": sorted, + "str": str, "sum": sum, "tuple": tuple, + "type": type, "zip": zip, + # Math + "ceil": math.ceil, "floor": math.floor, + "sqrt": math.sqrt, "log": math.log, + "log2": math.log2, "log10": math.log10, + "pi": math.pi, "e": math.e, + # Regex + "re_match": lambda p, s: bool(re.match(p, s)), + "re_search": lambda p, s: bool(re.search(p, s)), + "re_findall": lambda p, s: re.findall(p, s), + # 类型转换 + "json_dumps": json.dumps, + "json_loads": json.loads, +} + + +@dataclass +class StarlarkResult: + """Starlark 执行结果""" + ok: bool = True + value: Any = None + error: str = "" + globals_after: Dict[str, Any] = field(default_factory=dict) + duration_ms: int = 0 + output: str = "" + + +# ── Starlark 解释器 ──────────────────────────────────────────── + +class StarlarkEngine: + """ + Starlark 脚本引擎 — 对标 CODEX.EXE 的 starlark-0.14.2 + + 特性: + - 确定性: 相同输入 → 相同输出 + - 纯函数: 无 I/O, 无 random, 无 time + - 安全沙箱: 限制内置, 限制模块 + - 可扩展: 注册自定义函数/变量 + + 对标: + CODEX.EXE 使用 Starlark 做配置脚本和规则引擎。 + 本实现提供相同的能力用于 DEEPCODE 的: + - Hook 规则 (if security_check() then block()) + - 权限规则 (allow_if / deny_if) + - 配置 DSL + """ + + def __init__(self, extra_globals: Optional[Dict[str, Any]] = None): + self._builtins = dict(SAFE_BUILTINS) + if extra_globals: + self._builtins.update(extra_globals) + self._custom_functions: Dict[str, Callable] = {} + + def register_function(self, name: str, func: Callable) -> None: + """注册自定义函数到 Starlark 环境""" + self._custom_functions[name] = func + + def register_variable(self, name: str, value: Any) -> None: + """注册变量到 Starlark 环境""" + self._builtins[name] = value + + def execute(self, code: str, timeout: int = 10) -> StarlarkResult: + """ + 执行 Starlark 代码 + + Args: + code: Starlark 源码 + timeout: 超时秒数 + + Returns: + StarlarkResult + """ + start = datetime.now() + + # 1. 安全检查 — 禁止危险模块导入 + try: + tree = ast.parse(code, mode="exec") + except SyntaxError as e: + return StarlarkResult(ok=False, error=f"Syntax error: {e}") + + # 2. 检查危险构造 + violation = self._audit_ast(tree) + if violation: + return StarlarkResult(ok=False, error=violation) + + # 3. 创建安全执行环境 + safe_globals: Dict[str, Any] = { + "__builtins__": { + k: v for k, v in self._builtins.items() + if k not in DENIED_BUILTINS + }, + **self._custom_functions, + } + + # 4. 编译并执行 + try: + compiled = compile(tree, "", "exec") + exec(compiled, safe_globals) + except Exception as e: + return StarlarkResult(ok=False, error=f"Execution error: {e}") + + # 5. 提取结果 + result_value = safe_globals.get("result", None) + output = safe_globals.get("_output", "") + + # 清理内部变量 + clean_globals = { + k: v for k, v in safe_globals.items() + if not k.startswith("_") and k != "__builtins__" + } + + duration = int((datetime.now() - start).total_seconds() * 1000) + return StarlarkResult( + ok=True, value=result_value, + globals_after=clean_globals, + duration_ms=duration, output=str(output), + ) + + def _audit_ast(self, tree: ast.AST) -> Optional[str]: + """ + AST 审计 — 检测危险操作 + + 对标 CODEX.EXE 的安全检查: + - 禁止 import 危险模块 + - 禁止 I/O 操作 + - 禁止 subprocess 调用 + """ + for node in ast.walk(tree): + # 禁止 import + if isinstance(node, ast.Import): + for alias in node.names: + if alias.name.split(".")[0] in DENIED_MODULES: + return f"Import denied: {alias.name}" + elif isinstance(node, ast.ImportFrom): + if node.module and node.module.split(".")[0] in DENIED_MODULES: + return f"Import denied: {node.module}" + + # 禁止 exec/eval + elif isinstance(node, ast.Call): + if isinstance(node.func, ast.Name): + if node.func.id in ("exec", "eval", "compile", "open", "__import__"): + return f"Builtin denied: {node.func.id}()" + + # 禁止属性访问危险模块 + elif isinstance(node, ast.Attribute): + pass # 运行时检查 + + return None + + def execute_rules( + self, rules_code: str, context: Dict[str, Any], timeout: int = 10 + ) -> Tuple[bool, Dict[str, Any]]: + """ + 执行 Starlark 规则集 + + 这是对标 CODEX 的规则引擎用法: + 规则代码中定义 allow() / deny() 决策函数, + 上下文提供当前请求的信息。 + + Args: + rules_code: Starlark 规则源码 + context: 执行上下文 (e.g. {"tool": "Bash", "command": "rm -rf /"}) + + Returns: + (allowed: bool, decisions: Dict) + """ + # 注入上下文 + self._builtins["_ctx"] = context + + result = self.execute(rules_code, timeout=timeout) + + if not result.ok: + return True, {"error": result.error} + + # 读取规则决策 + allowed = result.globals_after.get("allowed", True) + decisions = result.globals_after.get("decisions", {}) + reason = result.globals_after.get("reason", "") + + return allowed, {"decisions": decisions, "reason": reason, **result.globals_after} + + +# ── 预定义规则模板 ───────────────────────────────────────────── + +DEFAULT_HOOK_RULES = """ +# DEEPCODE Hook Rules — Starlark 安全策略 +# 对标 CODEX.EXE 的 PreToolUse / PermissionRequest 规则 + +ctx = _ctx # 当前上下文 + +# 默认允许 +allowed = True +reason = "" + +# 规则 1: 阻止危险命令 +DANGEROUS_COMMANDS = ["rm -rf /", "shutdown", "format", "dd if=", "mkfs"] +for cmd in DANGEROUS_COMMANDS: + if ctx.get("command", "").lower().find(cmd) >= 0: + allowed = False + reason = "Blocked dangerous command: " + cmd + +# 规则 2: 阻止写入系统目录 +if ctx.get("tool") == "Write": + path = ctx.get("file_path", "") + if path.startswith("/etc/") or path.startswith("C:\\\\Windows\\\\"): + allowed = False + reason = "Blocked write to system path: " + path + +# 规则 3: 工具白名单 +ALLOWED_TOOLS = ["Read", "Write", "Edit", "Bash", "Grep", "Glob"] +tool = ctx.get("tool", ctx.get("tool_name", "")) +if tool and tool not in ALLOWED_TOOLS: + allowed = False + reason = "Tool not in allowlist: " + tool + +decisions = {"allowed": allowed, "reason": reason} +""" + + +# ── MCP Server ───────────────────────────────────────────────── + +async def run_mcp(): + """MCP Server 模式""" + engine = StarlarkEngine() + + TOOLS = { + "starlark_execute": { + "description": "执行 Starlark 脚本 — 安全、确定性、无副作用", + "inputSchema": { + "type": "object", + "properties": { + "code": {"type": "string", "description": "Starlark 源码"}, + "timeout": {"type": "integer", "default": 10}, + }, + "required": ["code"], + }, + }, + "starlark_execute_file": { + "description": "执行 Starlark 脚本文件", + "inputSchema": { + "type": "object", + "properties": { + "file_path": {"type": "string"}, + }, + "required": ["file_path"], + }, + }, + "starlark_rules": { + "description": "执行 Starlark 规则集 (allows/deny 决策)", + "inputSchema": { + "type": "object", + "properties": { + "rules_code": {"type": "string"}, + "context": {"type": "object", "description": "执行上下文"}, + }, + "required": ["rules_code", "context"], + }, + }, + "starlark_register": { + "description": "注册自定义 Starlark 函数", + "inputSchema": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "code": {"type": "string", "description": "Python lambda 源码"}, + }, + "required": ["name", "code"], + }, + }, + } + + # ── 标准 MCP JSON-RPC 2.0 stdio ── + for line in sys.stdin: + line = line.strip() + if not line: + continue + try: + req = json.loads(line) + except json.JSONDecodeError as e: + err = { + "jsonrpc": "2.0", + "error": {"code": -32700, "message": f"Parse error: {e}"}, + "id": None, + } + sys.stdout.write(json.dumps(err, ensure_ascii=False) + "\n") + sys.stdout.flush() + continue + + method = req.get("method", "") + params = req.get("params", {}) + rid = req.get("id", "") + + # ── initialize ── + if method == "initialize": + resp = { + "jsonrpc": "2.0", + "id": rid, + "result": { + "protocolVersion": "2025-03-26", + "capabilities": {"tools": {"listChanged": False}}, + "serverInfo": {"name": "deepcode-starlark", "version": "1.0.0"}, + }, + } + sys.stdout.write(json.dumps(resp, ensure_ascii=False) + "\n") + sys.stdout.flush() + continue + + # ── notifications/initialized ── + if method == "notifications/initialized": + continue + + if method == "tools/list": + print(json.dumps({ + "jsonrpc": "2.0", "id": rid, + "result": {"tools": [ + {"name": k, "description": v["description"], + "inputSchema": v["inputSchema"]} + for k, v in TOOLS.items() + ]}, + }), flush=True) + + elif method == "tools/call": + name = params.get("name", "") + args = params.get("arguments", {}) + result = {} + + try: + if name == "starlark_execute": + r = engine.execute( + args["code"], + timeout=args.get("timeout", 10), + ) + result = { + "ok": r.ok, "value": r.value, + "error": r.error, "duration_ms": r.duration_ms, + "output": r.output, + "globals": {k: str(v) for k, v in r.globals_after.items()}, + } + + elif name == "starlark_execute_file": + file_path = args["file_path"] + with open(file_path, "r") as f: + code = f.read() + r = engine.execute(code) + result = {"ok": r.ok, "value": r.value, "error": r.error} + + elif name == "starlark_rules": + allowed, decisions = engine.execute_rules( + args["rules_code"], args["context"], + ) + result = {"allowed": allowed, "decisions": decisions} + + elif name == "starlark_register": + func_name = args["name"] + func_code = args["code"] + exec(f"engine.register_function('{func_name}', {func_code})") + result = {"registered": func_name} + + else: + result = {"error": f"Unknown: {name}"} + + except Exception as e: + result = {"error": str(e), "ok": False} + + print(json.dumps({ + "jsonrpc": "2.0", "id": rid, + "result": { + "content": [{"type": "text", + "text": json.dumps(result, ensure_ascii=False)}], + }, + }), flush=True) + + +# ── CLI ──────────────────────────────────────────────────────── + +def main(): + import argparse + parser = argparse.ArgumentParser( + description="DeepCode Starlark Engine — 安全脚本执行") + parser.add_argument("--mcp", action="store_true", help="MCP Server 模式") + sub = parser.add_subparsers(dest="command") + + p_run = sub.add_parser("run", help="执行 Starlark 代码") + p_run.add_argument("--file", help="脚本文件路径") + p_run.add_argument("--code", help="直接执行代码") + p_run.add_argument("--ctx", default="{}", help="上下文 JSON") + + p_rules = sub.add_parser("rules", help="执行规则集") + p_rules.add_argument("--file", help="规则文件路径") + p_rules.add_argument("--ctx", default="{}", help="上下文 JSON") + + sub.add_parser("demo", help="运行演示规则") + + args = parser.parse_args() + + if args.mcp: + asyncio.run(run_mcp()) + return + + engine = StarlarkEngine() + + if args.command == "run": + if args.file: + with open(args.file, "r") as f: + code = f.read() + elif args.code: + code = args.code + else: + print("Error: --file or --code required") + sys.exit(1) + + result = engine.execute(code) + print(json.dumps({ + "ok": result.ok, "value": result.value, + "error": result.error, "duration_ms": result.duration_ms, + "globals": result.globals_after, + }, indent=2, default=str)) + + elif args.command == "rules": + if args.file: + with open(args.file, "r") as f: + rules_code = f.read() + else: + rules_code = DEFAULT_HOOK_RULES + + ctx = json.loads(args.ctx) + allowed, decisions = engine.execute_rules(rules_code, ctx) + print(json.dumps({"allowed": allowed, "decisions": decisions}, indent=2, default=str)) + + elif args.command == "demo": + print("=== Starlark Demo ===") + print() + + # Demo 1: 简单计算 + print("[Demo 1] 简单计算") + r = engine.execute("x = list(range(10)); result = sum(x)") + print(f" sum(0..9) = {r.value} ({r.duration_ms}ms)") + print() + + # Demo 2: 规则: 危险命令检测 + print("[Demo 2] 安全规则 — 检测 rm -rf /") + ctx = {"tool": "Bash", "command": "rm -rf /"} + allowed, dec = engine.execute_rules(DEFAULT_HOOK_RULES, ctx) + print(f" allowed={allowed}, reason={dec.get('reason', '')}") + print() + + # Demo 3: 规则: 安全命令 + print("[Demo 3] 安全规则 — 检测 ls -la") + ctx = {"tool": "Bash", "command": "ls -la"} + allowed, dec = engine.execute_rules(DEFAULT_HOOK_RULES, ctx) + print(f" allowed={allowed}") + print() + + # Demo 4: 列表推导 + print("[Demo 4] 列表推导") + r = engine.execute("result = [x*x for x in range(1,6)]") + print(f" squares: {r.value}") + print() + + # Demo 5: 被阻止的操作 + print("[Demo 5] 危险操作被阻止") + r = engine.execute("import os; os.system('ls')") + print(f" blocked: {r.error}") + + +if __name__ == "__main__": + main() diff --git a/.deepcode/skills/deepcode-telemetry/SKILL.md b/.deepcode/skills/deepcode-telemetry/SKILL.md new file mode 100644 index 00000000..cb8b6092 --- /dev/null +++ b/.deepcode/skills/deepcode-telemetry/SKILL.md @@ -0,0 +1,52 @@ +--- +name: deepcode-telemetry +description: > + DeepCode Telemetry — 移植自 CODEX.EXE 的 OpenTelemetry 遥测栈。 + Trace Spans, Metrics, OTLP/Prometheus/JSON 导出。 + 对标 codex.exe: opentelemetry-otlp-0.31.0, SessionTelemetry, model_preferences. +version: 1.0.0 +author: DeepCode + RE (codex.exe v0.145.0) +date: 2026-07-28 +tags: [telemetry, otel, metrics] +--- + +# DeepCode Telemetry + +移植自 **CODEX.EXE** 的完整遥测栈。 + +## 模块 + +| 模块 | 文件 | 对标 CODEX | 状态 | +|:------|:-----|:----------|:-----| +| Trace/Metrics | `telemetry.py` | opentelemetry-otlp-0.31.0 | ✅ 可用 | +| Provider 路由 | `provider_router.py` | aws-smithy-runtime | ❌ **已废弃** → 合并到 router-mcp | + +## 遥测 (telemetry.py) + +- **Trace Spans**: 工具调用 / LLM 调用 / 会话生命周期 +- **Metrics**: 耗时 / Token 用量 / 错误率 / 计数器 +- **导出**: JSON File / OTLP HTTP / Prometheus / Console +- **环境变量**: OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_METRIC_EXPORT_INTERVAL + +## Provider 路由 (⚠️ 已废弃) + +`provider_router.py` 的功能已被 **router-mcp** MCP Server 取代。 + +替代方案: +- settings.json `provider.active` — Provider 选择 +- `router_query(force_model="deepseek-v4-pro")` — 指定模型 + +## CLI + +```bash +python telemetry.py demo +``` + +## MCP Server + +```json +"deepcode-telemetry": { + "command": "python", + "args": ["F:/DEEPCODE/.deepcode/skills/deepcode-telemetry/telemetry.py", "--mcp"] +} +``` diff --git a/.deepcode/skills/deepcode-telemetry/plugin.json b/.deepcode/skills/deepcode-telemetry/plugin.json new file mode 100644 index 00000000..111f9baa --- /dev/null +++ b/.deepcode/skills/deepcode-telemetry/plugin.json @@ -0,0 +1,9 @@ +{ + "name": "deepcode-telemetry", + "version": "1.0.0", + "description": "DeepCode Telemetry — OpenTelemetry 遥测 + 多 Provider 路由, 移植自 CODEX.EXE v0.145.0", + "author": "DeepCode + RE (codex.exe v0.145.0)", + "entry": "SKILL.md", + "dependencies": [], + "modules": ["telemetry.py", "provider_router.py"] +} diff --git a/.deepcode/skills/deepcode-telemetry/provider_router.py b/.deepcode/skills/deepcode-telemetry/provider_router.py new file mode 100644 index 00000000..090e31d9 --- /dev/null +++ b/.deepcode/skills/deepcode-telemetry/provider_router.py @@ -0,0 +1,636 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +DeepCode Provider Router — 多 Provider 抽象层 +══════════════════════════════════════════════════ +对标 CODEX.EXE aws-smithy-runtime + provider.active: + - 5 provider types: OpenAI / Anthropic / DeepSeek / Bedrock / Vertex + - Hot-switch without restart + - Config: settings.json provider.providers + env vars + - 5 MCP tools: provider_switch / provider_list / provider_status / provider_chat / provider_recommend + +整合状态 2026-07-29: + - 作为独立 MCP server 运行 (provider-router) + - 读取 settings.json `provider.active` 决定默认 provider + - router-mcp 负责智能路由决策,provider-router 负责多 provider 执行 +""" + +import asyncio +import json +import os +import sys +import time +from dataclasses import dataclass, field, asdict +from datetime import datetime +from enum import Enum +from pathlib import Path +from typing import Any, Callable, Dict, List, Optional +from urllib.request import Request, urlopen +from urllib.error import URLError + + +# ── 类型定义 ────────────────────────────────────────────────── + +class ProviderType(str, Enum): + OPENAI = "openai" # OpenAI-compatible API + ANTHROPIC = "anthropic" # Anthropic Claude + DEEPSEEK = "deepseek" # DeepSeek (OpenAI-compatible) + BEDROCK = "bedrock" # AWS Bedrock + VERTEX = "vertex" # GCP Vertex AI + + +class AuthType(str, Enum): + API_KEY = "api_key" + BEARER = "bearer" + OAUTH = "oauth" + AWS_IAM = "aws_iam" + + +@dataclass +class ProviderConfig: + """Provider 配置 — 对标 CODEX provider.providers.""" + name: str = "" + type: ProviderType = ProviderType.OPENAI + api_base: str = "" + api_key: str = "" + api_key_env: str = "" + model: str = "" + max_tokens: int = 8192 + temperature: float = 0.6 + headers: Dict[str, str] = field(default_factory=dict) + # Bedrock / Vertex specific + region: str = "" + project: str = "" + + def get_api_key(self) -> str: + if self.api_key: + return self.api_key + if self.api_key_env: + return os.environ.get(self.api_key_env, "") + return "" + + def to_dict(self) -> Dict: + d = asdict(self) + d["type"] = self.type.value + # 隐藏 key + d["api_key"] = "***" if d["api_key"] else "" + return d + + +@dataclass +class ProviderResponse: + """Provider 响应""" + ok: bool = True + content: str = "" + model: str = "" + usage: Dict[str, int] = field(default_factory=dict) + error: str = "" + duration_ms: int = 0 + provider: str = "" + + +# ── Provider 实现 ───────────────────────────────────────────── + +class BaseProvider: + """Provider 基类""" + + def __init__(self, config: ProviderConfig): + self.config = config + + @property + def name(self) -> str: + return self.config.name + + async def chat(self, messages: List[Dict[str, str]], + model: str = "", max_tokens: int = 0, + temperature: float = -1.0, + stream: bool = False) -> ProviderResponse: + raise NotImplementedError + + def health_check(self) -> bool: + return False + + +class OpenAICompatibleProvider(BaseProvider): + """ + OpenAI-compatible API Provider + 支持: DeepSeek / OpenAI / 任何 OpenAI-compatible API + """ + + async def chat(self, messages, model="", max_tokens=0, + temperature=-1.0, stream=False) -> ProviderResponse: + start = time.time() + model = model or self.config.model + max_tokens = max_tokens or self.config.max_tokens + temp = temperature if temperature >= 0 else self.config.temperature + + api_key = self.config.get_api_key() + if not api_key: + return ProviderResponse(ok=False, error="No API key configured", + provider=self.name) + + body = json.dumps({ + "model": model, + "messages": messages, + "max_tokens": max_tokens, + "temperature": temp, + "stream": stream, + }).encode() + + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {api_key}", + **self.config.headers, + } + + try: + req = Request( + f"{self.config.api_base}/chat/completions", + data=body, headers=headers, method="POST", + ) + resp = urlopen(req, timeout=120) + data = json.loads(resp.read().decode()) + + return ProviderResponse( + ok=True, + content=data["choices"][0]["message"]["content"], + model=data.get("model", model), + usage={ + "prompt": data.get("usage", {}).get("prompt_tokens", 0), + "completion": data.get("usage", {}).get("completion_tokens", 0), + "total": data.get("usage", {}).get("total_tokens", 0), + }, + duration_ms=int((time.time() - start) * 1000), + provider=self.name, + ) + except URLError as e: + return ProviderResponse(ok=False, error=str(e), provider=self.name) + except Exception as e: + return ProviderResponse(ok=False, error=str(e), provider=self.name) + + def health_check(self) -> bool: + resp = asyncio.run(self.chat( + [{"role": "user", "content": "ping"}], + max_tokens=5, + )) + return resp.ok + + +class AnthropicProvider(BaseProvider): + """Anthropic Claude Provider""" + + async def chat(self, messages, model="", max_tokens=0, + temperature=-1.0, stream=False) -> ProviderResponse: + start = time.time() + model = model or self.config.model or "claude-sonnet-4-20250514" + max_tokens = max_tokens or self.config.max_tokens + temp = temperature if temperature >= 0 else self.config.temperature + + api_key = self.config.get_api_key() + if not api_key: + return ProviderResponse(ok=False, error="No API key", provider=self.name) + + # 转换 messages 格式: OpenAI → Anthropic + system = "" + anthropic_msgs = [] + for m in messages: + if m["role"] == "system": + system = m["content"] + else: + anthropic_msgs.append({"role": m["role"], "content": m["content"]}) + + body = json.dumps({ + "model": model, + "messages": anthropic_msgs, + "system": system, + "max_tokens": max_tokens, + "temperature": temp, + }).encode() + + headers = { + "Content-Type": "application/json", + "x-api-key": api_key, + "anthropic-version": "2023-06-01", + } + + try: + req = Request( + f"{self.config.api_base}/messages", + data=body, headers=headers, method="POST", + ) + resp = urlopen(req, timeout=120) + data = json.loads(resp.read().decode()) + + return ProviderResponse( + ok=True, + content=data["content"][0]["text"], + model=data.get("model", model), + usage={ + "prompt": data.get("usage", {}).get("input_tokens", 0), + "completion": data.get("usage", {}).get("output_tokens", 0), + "total": data.get("usage", {}).get("input_tokens", 0) + + data.get("usage", {}).get("output_tokens", 0), + }, + duration_ms=int((time.time() - start) * 1000), + provider=self.name, + ) + except URLError as e: + return ProviderResponse(ok=False, error=str(e), provider=self.name) + except Exception as e: + return ProviderResponse(ok=False, error=str(e), provider=self.name) + + +# ── Provider 路由器 ──────────────────────────────────────────── + +# Provider 工厂 +PROVIDER_CLASSES = { + ProviderType.OPENAI: OpenAICompatibleProvider, + ProviderType.DEEPSEEK: OpenAICompatibleProvider, + ProviderType.ANTHROPIC: AnthropicProvider, +} + + +class ProviderRouter: + """ + Provider 路由器 — 对标 CODEX.EXE 的多 Provider 切换系统 + + 配置格式 (对标 CODEX settings.json): + { + "provider": { + "active": "deepseek", + "providers": { + "deepseek": { + "type": "openai", + "api_base": "https://api.deepseek.com", + "api_key_env": "DEEPSEEK_API_KEY", + "model": "deepseek-chat" + }, + "anthropic": { + "type": "anthropic", + "api_base": "https://api.anthropic.com", + "api_key_env": "ANTHROPIC_API_KEY", + "model": "claude-sonnet-4-20250514" + } + } + } + } + """ + + def __init__(self, config: Dict[str, Any] = None): + self._providers: Dict[str, BaseProvider] = {} + self._active: str = "" + self._config = config or self._load_default_config() + + self._init_providers() + + @property + def active(self) -> Optional[BaseProvider]: + return self._providers.get(self._active) + + @property + def active_name(self) -> str: + return self._active + + @property + def providers(self) -> List[str]: + return list(self._providers.keys()) + + def _init_providers(self): + pc = self._config.get("provider", {}) + self._active = pc.get("active", "deepseek") + + for name, cfg in pc.get("providers", {}).items(): + try: + ptype = ProviderType(cfg.get("type", "openai")) + pconfig = ProviderConfig( + name=name, + type=ptype, + api_base=cfg.get("api_base", ""), + api_key=cfg.get("api_key", ""), + api_key_env=cfg.get("api_key_env", ""), + model=cfg.get("model", ""), + max_tokens=cfg.get("max_tokens", 8192), + temperature=cfg.get("temperature", 0.6), + headers=cfg.get("headers", {}), + region=cfg.get("region", ""), + project=cfg.get("project", ""), + ) + + cls = PROVIDER_CLASSES.get(ptype, OpenAICompatibleProvider) + self._providers[name] = cls(pconfig) + except Exception as e: + print(f"[provider] Failed to init {name}: {e}", file=sys.stderr) + + # 默认 provider + if not self._providers: + self._add_builtin_providers() + + def _add_builtin_providers(self): + """添加内置 Provider — DeepSeek 默认""" + deepseek_config = ProviderConfig( + name="deepseek", type=ProviderType.DEEPSEEK, + api_base="https://api.deepseek.com", + api_key_env="DEEPSEEK_API_KEY", + model="deepseek-chat", + max_tokens=8192, + ) + self._providers["deepseek"] = OpenAICompatibleProvider(deepseek_config) + self._active = "deepseek" + + def _load_default_config(self) -> Dict: + """加载默认配置""" + return { + "provider": { + "active": "deepseek", + "providers": { + "deepseek": { + "type": "openai", + "api_base": "https://api.deepseek.com", + "api_key_env": "DEEPSEEK_API_KEY", + "model": "deepseek-chat", + "max_tokens": 8192, + }, + }, + }, + } + + def switch(self, name: str) -> bool: + """切换活跃 Provider""" + if name not in self._providers: + return False + self._active = name + return True + + def register(self, name: str, config: ProviderConfig) -> bool: + """注册新 Provider""" + cls = PROVIDER_CLASSES.get(config.type, OpenAICompatibleProvider) + self._providers[name] = cls(config) + if not self._active: + self._active = name + return True + + async def chat(self, messages: List[Dict], **kwargs) -> ProviderResponse: + """使用当前活跃 Provider 发送请求""" + provider = self.active + if not provider: + return ProviderResponse(ok=False, error="No active provider") + + try: + return await provider.chat(messages, **kwargs) + except Exception as e: + return ProviderResponse(ok=False, error=str(e), + provider=self._active) + + def list_providers(self) -> List[Dict]: + """列出所有 Provider""" + result = [] + for name, p in self._providers.items(): + result.append({ + "name": name, + "type": p.config.type.value, + "active": name == self._active, + "model": p.config.model, + "health": p.health_check() if name == self._active else "unknown", + }) + return result + + def status(self) -> Dict: + """获取状态""" + return { + "active": self._active, + "providers": self.list_providers(), + "config": { + k: v.to_dict() for k, v in { + n: p.config for n, p in self._providers.items() + }.items() + }, + } + + def recommend_model(self, effort: str = "high", + reasoning: str = "") -> Dict[str, str]: + """ + 根据 effort 级别推荐模型 — 对标 CODEX 的 reasoning_effort + + effort: low | medium | high | xhigh | max + """ + models = { + "low": {"model": "deepseek-v4-flash", "max_tokens": 2048}, + "medium": {"model": "deepseek-v4-flash", "max_tokens": 4096}, + "high": {"model": "deepseek-v4-pro", "max_tokens": 8192}, + "xhigh": {"model": "deepseek-r1", "max_tokens": 16384}, + "max": {"model": "deepseek-r1", "max_tokens": 32768}, + } + return models.get(effort, models["high"]) + + +# ── 全局单例 ────────────────────────────────────────────────── + +_router: Optional[ProviderRouter] = None + + +def get_router() -> ProviderRouter: + global _router + if _router is None: + _router = ProviderRouter() + return _router + + +# ── MCP Server ───────────────────────────────────────────────── + +async def run_mcp(): + router = get_router() + + TOOLS = { + "provider_switch": { + "description": "切换到指定 Provider", + "inputSchema": { + "type": "object", + "properties": { + "name": {"type": "string"}, + }, + "required": ["name"], + }, + }, + "provider_list": { + "description": "列出所有 Provider", + "inputSchema": {"type": "object", "properties": {}}, + }, + "provider_status": { + "description": "Provider 状态", + "inputSchema": {"type": "object", "properties": {}}, + }, + "provider_chat": { + "description": "使用活跃 Provider 发送 LLM 请求", + "inputSchema": { + "type": "object", + "properties": { + "messages": {"type": "array"}, + "model": {"type": "string"}, + "max_tokens": {"type": "integer"}, + }, + "required": ["messages"], + }, + }, + "provider_recommend": { + "description": "根据 effort 推荐模型", + "inputSchema": { + "type": "object", + "properties": { + "effort": {"type": "string"}, + }, + }, + }, + } + + # ── 标准 MCP JSON-RPC 2.0 stdio ── + for line in sys.stdin: + line = line.strip() + if not line: + continue + try: + req = json.loads(line) + except json.JSONDecodeError as e: + err = { + "jsonrpc": "2.0", + "error": {"code": -32700, "message": f"Parse error: {e}"}, + "id": None, + } + sys.stdout.write(json.dumps(err, ensure_ascii=False) + "\n") + sys.stdout.flush() + continue + + method = req.get("method", "") + params = req.get("params", {}) + rid = req.get("id", "") + + # ── initialize ── + if method == "initialize": + resp = { + "jsonrpc": "2.0", + "id": rid, + "result": { + "protocolVersion": "2025-03-26", + "capabilities": {"tools": {"listChanged": False}}, + "serverInfo": {"name": "provider-router", "version": "1.0.0"}, + }, + } + sys.stdout.write(json.dumps(resp, ensure_ascii=False) + "\n") + sys.stdout.flush() + continue + + # ── notifications/initialized ── + if method == "notifications/initialized": + continue + + if method == "tools/list": + print(json.dumps({ + "jsonrpc": "2.0", "id": rid, + "result": {"tools": [ + {"name": k, "description": v["description"], + "inputSchema": v["inputSchema"]} + for k, v in TOOLS.items() + ]}, + }), flush=True) + + elif method == "tools/call": + name = params.get("name", "") + args = params.get("arguments", {}) + result = {} + + try: + if name == "provider_switch": + ok = router.switch(args["name"]) + result = {"ok": ok, "active": router.active_name} + + elif name == "provider_list": + result = {"providers": router.list_providers()} + + elif name == "provider_status": + result = router.status() + + elif name == "provider_chat": + resp = await router.chat( + args["messages"], + model=args.get("model", ""), + max_tokens=args.get("max_tokens", 0), + ) + result = { + "ok": resp.ok, "content": resp.content[:500], + "usage": resp.usage, "duration_ms": resp.duration_ms, + "provider": resp.provider, "error": resp.error, + } + + elif name == "provider_recommend": + result = router.recommend_model(args.get("effort", "high")) + + else: + result = {"error": f"Unknown: {name}"} + + except Exception as e: + result = {"error": str(e)} + + print(json.dumps({ + "jsonrpc": "2.0", "id": rid, + "result": { + "content": [{"type": "text", + "text": json.dumps(result, ensure_ascii=False)}], + }, + }), flush=True) + + +# ── CLI ──────────────────────────────────────────────────────── + +def main(): + import argparse + parser = argparse.ArgumentParser(description="DeepCode Provider Router") + parser.add_argument("--mcp", action="store_true", help="MCP Server 模式") + sub = parser.add_subparsers(dest="command") + + sub.add_parser("list", help="列出 Provider") + sub.add_parser("status", help="状态") + p = sub.add_parser("switch", help="切换 Provider") + p.add_argument("name", help="Provider 名称") + p = sub.add_parser("chat", help="发送聊天请求") + p.add_argument("prompt", help="提示词") + + args = parser.parse_args() + + if args.mcp: + asyncio.run(run_mcp()) + return + + router = get_router() + + if args.command == "list": + print(json.dumps(router.list_providers(), indent=2)) + + elif args.command == "status": + print(json.dumps(router.status(), indent=2, default=str)) + + elif args.command == "switch": + ok = router.switch(args.name) + print(f"Switched to {args.name}: {ok}") + + elif args.command == "chat": + resp = asyncio.run(router.chat([{"role": "user", "content": args.prompt}])) + print(f"[{resp.provider}] {resp.model} ({resp.duration_ms}ms)") + print(f"Tokens: {resp.usage}") + print(f"\n{resp.content[:500]}") + + else: + # Demo + print("=== Provider Router Demo ===\n") + print("Active:", router.active_name) + print() + print("Providers:") + for p in router.list_providers(): + print(f" {'*' if p['active'] else ' '} {p['name']} [{p['type']}] " + f"model={p['model']}") + print() + for effort in ["low", "medium", "high", "xhigh", "max"]: + rec = router.recommend_model(effort) + print(f" effort={effort:6s} → {rec['model']:25s} max_tokens={rec['max_tokens']}") + + +if __name__ == "__main__": + main() diff --git a/.deepcode/skills/deepcode-telemetry/telemetry.py b/.deepcode/skills/deepcode-telemetry/telemetry.py new file mode 100644 index 00000000..c363bb8f --- /dev/null +++ b/.deepcode/skills/deepcode-telemetry/telemetry.py @@ -0,0 +1,866 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +DeepCode Telemetry — 移植自 CODEX.EXE 的 OpenTelemetry 遥测栈 +══════════════════════════════════════════════════════════════ +对标 CODEX.EXE v0.145.0: + - opentelemetry-otlp-0.31.0 → OTLP Trace/Metric 导出 + - HttpTracesClient.ExportSucceeded → HTTP OTLP exporter + - OTEL_EXPORTER_OTLP_ENDPOINT → 环境变量控制 + - MeterProvider / TracerProvider → Provider 抽象 + - session_telemetry.rs → 会话遥测记录 + +核心能力: + 1. Trace Spans — 工具调用 / LLM 调用 / 会话生命周期 + 2. Metrics — 耗时 / Token 用量 / 错误率 / 成功率 + 3. OTLP Export — HTTP JSON 导出到任何兼容后端 + 4. Prometheus — /metrics 端点 + 5. JSON File — 本地文件导出(默认) + +对标: + CODEX.EXE 使用 OpenTelemetry Rust SDK 记录: + - plugin_install 事件 + - timing_metrics (responses_duration_excl_engine_and_client_) + - session_telemetry + - OTEL_METRIC_EXPORT_INTERVAL + +用法: + # CLI + python telemetry.py trace --name "tool_call" --attrs '{"tool":"Bash"}' + python telemetry.py metric --name "token_usage" --value 1500 + python telemetry.py report + + # MCP Server + python telemetry.py --mcp +""" + +import asyncio +import json +import os +import sys +import threading +import time +import uuid +from collections import defaultdict +from dataclasses import dataclass, field, asdict +from datetime import datetime, timezone +from enum import Enum +from pathlib import Path +from typing import Any, Callable, Dict, List, Optional, Tuple + + +# ── 配置 ────────────────────────────────────────────────────── + +DEFAULT_TELEMETRY_DIR = Path(__file__).resolve().parent.parent.parent.parent / "telemetry_data" + +ENV_OTLP_ENDPOINT = "OTEL_EXPORTER_OTLP_ENDPOINT" +ENV_OTLP_HEADERS = "OTEL_EXPORTER_OTLP_HEADERS" +ENV_OTLP_INSECURE = "OTEL_EXPORTER_OTLP_INSECURE" +ENV_PROMETHEUS_HOST = "OTEL_EXPORTER_PROMETHEUS_HOST" +ENV_PROMETHEUS_PORT = "OTEL_EXPORTER_PROMETHEUS_PORT" +ENV_METRIC_INTERVAL = "OTEL_METRIC_EXPORT_INTERVAL" + + +class ExportFormat(str, Enum): + JSON_FILE = "json_file" + OTLP_HTTP = "otlp_http" + PROMETHEUS = "prometheus" + CONSOLE = "console" + + +class SpanKind(str, Enum): + INTERNAL = "internal" + CLIENT = "client" + SERVER = "server" + PRODUCER = "producer" + CONSUMER = "consumer" + + +class SpanStatus(str, Enum): + UNSET = "unset" + OK = "ok" + ERROR = "error" + + +# ── 数据类 ──────────────────────────────────────────────────── + +@dataclass +class TelemetrySpan: + """Trace Span — 对标 OTel Span""" + id: str = "" + trace_id: str = "" + parent_id: str = "" + name: str = "" + kind: SpanKind = SpanKind.INTERNAL + status: SpanStatus = SpanStatus.UNSET + start_time: str = "" + end_time: str = "" + duration_ms: int = 0 + attributes: Dict[str, Any] = field(default_factory=dict) + events: List[Dict[str, Any]] = field(default_factory=list) + error: str = "" + + def __post_init__(self): + if not self.id: + self.id = f"span_{uuid.uuid4().hex[:16]}" + if not self.trace_id: + self.trace_id = f"trace_{uuid.uuid4().hex[:16]}" + if not self.start_time: + self.start_time = datetime.now(timezone.utc).isoformat() + + def finish(self, status: SpanStatus = SpanStatus.OK, error: str = ""): + self.end_time = datetime.now(timezone.utc).isoformat() + try: + s = datetime.fromisoformat(self.start_time) + e = datetime.fromisoformat(self.end_time) + self.duration_ms = int((e - s).total_seconds() * 1000) + except Exception: + pass + self.status = status + self.error = error + + def add_event(self, name: str, attrs: Dict[str, Any] = None): + self.events.append({ + "name": name, + "timestamp": datetime.now(timezone.utc).isoformat(), + "attributes": attrs or {}, + }) + + def to_dict(self) -> Dict: + return { + "id": self.id, + "trace_id": self.trace_id, + "parent_id": self.parent_id, + "name": self.name, + "kind": self.kind.value, + "status": self.status.value, + "start_time": self.start_time, + "end_time": self.end_time, + "duration_ms": self.duration_ms, + "attributes": self.attributes, + "events": self.events, + "error": self.error, + } + + +@dataclass +class TelemetryMetric: + """指标 — 对标 OTel Metric""" + name: str = "" + value: float = 0.0 + unit: str = "" + timestamp: str = "" + labels: Dict[str, str] = field(default_factory=dict) + type: str = "gauge" # gauge | counter | histogram + + def __post_init__(self): + if not self.timestamp: + self.timestamp = datetime.now(timezone.utc).isoformat() + + def to_dict(self) -> Dict: + return { + "name": self.name, + "value": self.value, + "unit": self.unit, + "timestamp": self.timestamp, + "labels": self.labels, + "type": self.type, + } + + +# ── 遥测引擎 ────────────────────────────────────────────────── + +class TelemetryEngine: + """ + OpenTelemetry 遥测引擎 — 对标 CODEX.EXE 的 OTEL 栈 + + 架构: + SpanProcessor → Exporters (JSON / OTLP / Prometheus / Console) + MetricReader → PeriodicExportingMetricReader + + 对标: + CODEX.EXE: + - session_telemetry.rs → 会话级 Span + - timing_metrics → 计时指标 + - plugin_install 事件 + - MeterProvider / TracerProvider + """ + + def __init__( + self, + export_format: ExportFormat = ExportFormat.JSON_FILE, + export_dir: str = "", + otlp_endpoint: str = "", + prometheus_host: str = "127.0.0.1", + prometheus_port: int = 9464, + ): + self.export_format = export_format + self.export_dir = Path(export_dir or DEFAULT_TELEMETRY_DIR) + self.otlp_endpoint = otlp_endpoint or os.environ.get(ENV_OTLP_ENDPOINT, "") + self.prometheus_host = prometheus_host or os.environ.get(ENV_PROMETHEUS_HOST, "127.0.0.1") + self.prometheus_port = int(os.environ.get(ENV_PROMETHEUS_PORT, str(prometheus_port))) + + # 存储 + self._spans: List[TelemetrySpan] = [] + self._metrics: List[TelemetryMetric] = [] + self._counters: Dict[str, float] = defaultdict(float) + self._histograms: Dict[str, List[float]] = defaultdict(list) + self._lock = threading.Lock() + + # 当前 trace + self._active_trace_id: str = "" + self._active_spans: Dict[str, TelemetrySpan] = {} + + # 统计 + self._export_count = 0 + self._last_export = "" + self._span_count = 0 + self._metric_count = 0 + + # 确保导出目录存在 + self.export_dir.mkdir(parents=True, exist_ok=True) + + # ── OpenTelemetry SDK 集成 ── + self._otlp_tracer = None + self._otlp_meter = None + self._init_otel_sdk() + + # ── Prometheus HTTP 服务器 ── + self._promhttp_server = None + self._promhttp_thread = None + self._promhttp_registry = None + + def _init_otel_sdk(self): + """初始化 OpenTelemetry SDK — 使用真实 exporter""" + try: + from opentelemetry import trace + from opentelemetry.sdk.trace import TracerProvider as SdkTracerProvider + from opentelemetry.sdk.trace.export import BatchSpanProcessor + from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter + from opentelemetry.sdk.resources import Resource + + # 仅在有 endpoint 时初始化真实 SDK + if self.otlp_endpoint: + resource = Resource.create({ + "service.name": "deepcode", + "service.version": "1.0.0", + }) + provider = SdkTracerProvider(resource=resource) + exporter = OTLPSpanExporter( + endpoint=f"{self.otlp_endpoint}/v1/traces", + headers=self._parse_otlp_headers(), + timeout=5, + ) + provider.add_span_processor(BatchSpanProcessor(exporter)) + self._otlp_tracer = provider.get_tracer("deepcode-telemetry") + print(f"[telemetry] OTLP SDK initialized → {self.otlp_endpoint}", file=sys.stderr, flush=True) + except ImportError: + pass # SDK not installed, fall back to custom engine + except Exception as e: + print(f"[telemetry] OTLP SDK init failed: {e}", file=sys.stderr, flush=True) + + def _parse_otlp_headers(self) -> dict: + """解析 OTEL_EXPORTER_OTLP_HEADERS 环境变量""" + headers = {} + raw = os.environ.get(ENV_OTLP_HEADERS, "") + if raw: + for pair in raw.split(","): + if "=" in pair: + k, v = pair.split("=", 1) + headers[k.strip()] = v.strip() + return headers + + # ── Trace API ────────────────────────────────────────── + + def start_trace(self, name: str = "") -> str: + """开始一个新的 Trace""" + self._active_trace_id = f"trace_{uuid.uuid4().hex[:16]}" + if name: + self.start_span(name, kind=SpanKind.INTERNAL) + return self._active_trace_id + + def start_span(self, name: str, kind: SpanKind = SpanKind.INTERNAL, + parent_id: str = "", attributes: Dict[str, Any] = None) -> str: + """创建 Span — 对标 OTel Tracer.start_span""" + if not self._active_trace_id: + self._active_trace_id = f"trace_{uuid.uuid4().hex[:16]}" + + span = TelemetrySpan( + name=name, + kind=kind, + trace_id=self._active_trace_id, + parent_id=parent_id, + attributes=attributes or {}, + ) + + with self._lock: + self._spans.append(span) + self._active_spans[span.id] = span + self._span_count += 1 + + return span.id + + def end_span(self, span_id: str, status: SpanStatus = SpanStatus.OK, + error: str = ""): + """结束 Span""" + with self._lock: + span = self._active_spans.pop(span_id, None) + if span: + span.finish(status, error) + + def add_event(self, span_id: str, event_name: str, + attributes: Dict[str, Any] = None): + """添加事件到 Span — 对标 OTel Span.add_event""" + with self._lock: + span = self._active_spans.get(span_id) + if span: + span.add_event(event_name, attributes) + + # ── Metrics API ──────────────────────────────────────── + + def record_metric(self, name: str, value: float, unit: str = "", + labels: Dict[str, str] = None, metric_type: str = "gauge"): + """记录指标 — 对标 OTel Meter.record""" + metric = TelemetryMetric( + name=name, value=value, unit=unit, + labels=labels or {}, type=metric_type, + ) + with self._lock: + self._metrics.append(metric) + self._metric_count += 1 + + def increment_counter(self, name: str, delta: float = 1.0): + """递增计数器""" + with self._lock: + self._counters[name] += delta + self.record_metric(name, self._counters[name], metric_type="counter") + + def record_histogram(self, name: str, value: float, unit: str = ""): + """记录直方图值""" + with self._lock: + self._histograms[name].append(value) + vals = self._histograms[name] + self.record_metric( + name, value, unit, + labels={"avg": str(sum(vals) / len(vals)), + "min": str(min(vals)), "max": str(max(vals)), + "count": str(len(vals))}, + metric_type="histogram", + ) + + def record_tool_call(self, tool_name: str, duration_ms: int, + success: bool, token_usage: int = 0): + """记录工具调用 — 对标 CODEX plugin_install.tool_*""" + self.increment_counter(f"tool.{tool_name}.calls") + self.record_histogram(f"tool.{tool_name}.duration_ms", duration_ms) + if not success: + self.increment_counter(f"tool.{tool_name}.errors") + if token_usage: + self.record_histogram("llm.token_usage", token_usage) + + def record_session(self, session_id: str, duration_ms: int, + tool_calls: int, commands: int): + """记录会话 — 对标 CODEX session_telemetry""" + self.record_histogram("session.duration_ms", duration_ms) + self.record_metric("session.tool_calls", tool_calls, metric_type="counter") + self.record_metric("session.commands", commands, metric_type="counter") + + # ── 导出 ────────────────────────────────────────────── + + def export(self) -> Dict[str, Any]: + """导出遥测数据 — 对标 OTel Exporter.export""" + with self._lock: + data = { + "service": "deepcode", + "version": "1.0.0", + "exported_at": datetime.now(timezone.utc).isoformat(), + "spans": [s.to_dict() for s in self._spans], + "metrics": [m.to_dict() for m in self._metrics], + "counters": dict(self._counters), + "summary": self.summary(), + } + + if self.export_format == ExportFormat.JSON_FILE: + self._export_json_file(data) + elif self.export_format == ExportFormat.OTLP_HTTP: + self._export_otlp(data) + elif self.export_format == ExportFormat.CONSOLE: + print(json.dumps(data, indent=2, default=str), flush=True) + + self._export_count += 1 + self._last_export = datetime.now(timezone.utc).isoformat() + return data + + def _export_json_file(self, data: Dict): + """导出到 JSON 文件""" + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + filepath = self.export_dir / f"telemetry_{timestamp}.json" + with open(filepath, "w") as f: + json.dump(data, f, indent=2, default=str, ensure_ascii=False) + + def _export_otlp(self, data: Dict): + """导出到 OTLP HTTP endpoint""" + endpoint = self.otlp_endpoint + if not endpoint: + return + + import urllib.request + + body = json.dumps({ + "resourceSpans": [{ + "resource": { + "attributes": [ + {"key": "service.name", "value": {"stringValue": "deepcode"}}, + ] + }, + "scopeSpans": [{ + "spans": data["spans"], + }], + }], + }).encode() + + headers = {"Content-Type": "application/json"} + otlp_headers = os.environ.get(ENV_OTLP_HEADERS, "") + if otlp_headers: + for pair in otlp_headers.split(","): + k, v = pair.split("=", 1) + headers[k.strip()] = v.strip() + + try: + req = urllib.request.Request( + f"{endpoint}/v1/traces", data=body, headers=headers, + method="POST", + ) + urllib.request.urlopen(req, timeout=5) + except Exception: + pass + + def start_prometheus_server(self) -> bool: + """启动 Prometheus HTTP /metrics 端点 (prometheus_client)""" + if self._promhttp_server is not None: + return True + try: + from prometheus_client import start_http_server, REGISTRY, Counter, Gauge, Histogram + self._promhttp_registry = REGISTRY + self._prom_counter_calls = Counter("deepcode_tool_calls_total", + "Total tool calls", ["tool", "status"]) + self._prom_gauge_active = Gauge("deepcode_active_spans", + "Currently active spans") + self._prom_histogram_duration = Histogram("deepcode_tool_duration_ms", + "Tool call duration (ms)", + ["tool"], buckets=[10, 50, 100, 500, 1000, 5000, 10000]) + def _serve(): + try: + start_http_server(self.prometheus_port, addr=self.prometheus_host) + print(f"[telemetry] Prometheus /metrics → http://{self.prometheus_host}:{self.prometheus_port}", + file=sys.stderr, flush=True) + except Exception as e: + print(f"[telemetry] Prometheus server failed: {e}", file=sys.stderr, flush=True) + self._promhttp_thread = threading.Thread(target=_serve, daemon=True) + self._promhttp_thread.start() + self._promhttp_server = True + return True + except ImportError: + print("[telemetry] prometheus_client not installed", file=sys.stderr, flush=True) + return False + except Exception as e: + print(f"[telemetry] Prometheus init error: {e}", file=sys.stderr, flush=True) + return False + + def export_prometheus(self) -> str: + """导出 Prometheus 格式 (同时同步到 prometheus_client 指标)""" + try: + if hasattr(self, '_prom_counter_calls'): + for n, v in self._counters.items(): + tool = n.replace("tool.", "").replace(".calls", "").replace(".errors", "") + status = "error" if "error" in n else "success" + self._prom_counter_calls.labels(tool=tool, status=status).inc(v) + if hasattr(self, '_prom_gauge_active'): + self._prom_gauge_active.set(len(self._active_spans)) + except Exception: + pass + + lines = [] + for name, value in self._counters.items(): + safe = name.replace(".", "_").replace("-", "_") + lines.append(f"# TYPE deepcode_{safe} counter") + lines.append(f"deepcode_{safe} {value}") + + for name, metrics in self._histograms.items(): + safe = name.replace(".", "_").replace("-", "_") + if metrics: + lines.append(f"# TYPE deepcode_{safe} summary") + lines.append(f"deepcode_{safe}_sum {sum(metrics)}") + lines.append(f"deepcode_{safe}_count {len(metrics)}") + + lines.extend([ + f"# TYPE deepcode_span_count gauge", + f"deepcode_span_count {self._span_count}", + f"# TYPE deepcode_metric_count gauge", + f"deepcode_metric_count {self._metric_count}", + f"# TYPE deepcode_export_count counter", + f"deepcode_export_count {self._export_count}", + ]) + + return "\n".join(lines) + "\n" + + # ── 统计 ────────────────────────────────────────────── + + def summary(self) -> Dict[str, Any]: + """生成摘要""" + with self._lock: + success_spans = sum(1 for s in self._spans if s.status == SpanStatus.OK) + error_spans = sum(1 for s in self._spans if s.status == SpanStatus.ERROR) + avg_duration = int(sum(s.duration_ms for s in self._spans if s.duration_ms) / + max(1, sum(1 for s in self._spans if s.duration_ms))) + + return { + "total_spans": self._span_count, + "active_spans": len(self._active_spans), + "success_spans": success_spans, + "error_spans": error_spans, + "avg_duration_ms": avg_duration, + "total_metrics": self._metric_count, + "counters": dict(self._counters), + "export_count": self._export_count, + "last_export": self._last_export, + } + + def reset(self): + """重置所有数据""" + with self._lock: + self._spans.clear() + self._metrics.clear() + self._counters.clear() + self._histograms.clear() + self._active_spans.clear() + self._span_count = 0 + self._metric_count = 0 + + +# ── 全局单例 ────────────────────────────────────────────────── + +_telemetry: Optional[TelemetryEngine] = None + + +def get_telemetry() -> TelemetryEngine: + global _telemetry + if _telemetry is None: + fmt_str = os.environ.get("DEEPCODE_TELEMETRY_FORMAT", "json_file") + try: + fmt = ExportFormat(fmt_str) + except ValueError: + fmt = ExportFormat.JSON_FILE + _telemetry = TelemetryEngine(export_format=fmt) + return _telemetry + + +# ── 便捷上下文管理器 ────────────────────────────────────────── + +class TraceSpan: + """Span 上下文管理器 — 对标 OTel with tracer.start_as_current_span()""" + + def __init__(self, name: str, kind: SpanKind = SpanKind.INTERNAL, + attributes: Dict[str, Any] = None, parent_id: str = ""): + self._t = get_telemetry() + self._span_id = self._t.start_span(name, kind, parent_id, attributes) + + def __enter__(self): + return self._span_id + + def __exit__(self, exc_type, exc_val, exc_tb): + if exc_type: + self._t.end_span(self._span_id, SpanStatus.ERROR, str(exc_val)) + else: + self._t.end_span(self._span_id, SpanStatus.OK) + + def add_event(self, name: str, attrs: Dict[str, Any] = None): + self._t.add_event(self._span_id, name, attrs) + + +# ── MCP Server ───────────────────────────────────────────────── + +async def run_mcp(): + """MCP Server 模式""" + t = get_telemetry() + + TOOLS = { + "telemetry_start_trace": { + "description": "开始一个新的 Trace", + "inputSchema": { + "type": "object", + "properties": { + "name": {"type": "string"}, + }, + }, + }, + "telemetry_start_span": { + "description": "创建 Span — 对标 OTel Tracer.start_span", + "inputSchema": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "kind": {"type": "string", "description": "internal/client/server"}, + "attributes": {"type": "object"}, + }, + "required": ["name"], + }, + }, + "telemetry_end_span": { + "description": "结束 Span", + "inputSchema": { + "type": "object", + "properties": { + "span_id": {"type": "string"}, + "error": {"type": "string"}, + }, + "required": ["span_id"], + }, + }, + "telemetry_record_metric": { + "description": "记录指标", + "inputSchema": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "value": {"type": "number"}, + "unit": {"type": "string"}, + "labels": {"type": "object"}, + }, + "required": ["name", "value"], + }, + }, + "telemetry_tool_call": { + "description": "记录工具调用遥测 — 对标 CODEX plugin_install 事件", + "inputSchema": { + "type": "object", + "properties": { + "tool_name": {"type": "string"}, + "duration_ms": {"type": "number"}, + "success": {"type": "boolean"}, + "token_usage": {"type": "number"}, + }, + "required": ["tool_name", "duration_ms", "success"], + }, + }, + "telemetry_export": { + "description": "导出遥测数据", + "inputSchema": {"type": "object", "properties": {}}, + }, + "telemetry_summary": { + "description": "获取遥测摘要", + "inputSchema": {"type": "object", "properties": {}}, + }, + "telemetry_prometheus": { + "description": "导出 Prometheus 格式指标", + "inputSchema": {"type": "object", "properties": {}}, + }, + "telemetry_start_prometheus": { + "description": "启动 Prometheus HTTP /metrics 服务器 (prometheus_client)", + "inputSchema": { + "type": "object", + "properties": { + "port": {"type": "number", "description": "监听端口 (默认 9464)"}, + }, + }, + }, + } + + # ── 标准 MCP JSON-RPC 2.0 stdio ── + for line in sys.stdin: + line = line.strip() + if not line: + continue + try: + req = json.loads(line) + except json.JSONDecodeError as e: + err = { + "jsonrpc": "2.0", + "error": {"code": -32700, "message": f"Parse error: {e}"}, + "id": None, + } + sys.stdout.write(json.dumps(err, ensure_ascii=False) + "\n") + sys.stdout.flush() + continue + + method = req.get("method", "") + params = req.get("params", {}) + rid = req.get("id", "") + + # ── initialize ── + if method == "initialize": + resp = { + "jsonrpc": "2.0", + "id": rid, + "result": { + "protocolVersion": "2025-03-26", + "capabilities": {"tools": {"listChanged": False}}, + "serverInfo": {"name": "deepcode-telemetry", "version": "1.0.0"}, + }, + } + sys.stdout.write(json.dumps(resp, ensure_ascii=False) + "\n") + sys.stdout.flush() + continue + + # ── notifications/initialized ── + if method == "notifications/initialized": + continue + + if method == "tools/list": + print(json.dumps({ + "jsonrpc": "2.0", "id": rid, + "result": {"tools": [ + {"name": k, "description": v["description"], + "inputSchema": v["inputSchema"]} + for k, v in TOOLS.items() + ]}, + }), flush=True) + + elif method == "tools/call": + name = params.get("name", "") + args = params.get("arguments", {}) + result = {} + + try: + if name == "telemetry_start_trace": + tid = t.start_trace(args.get("name", "")) + result = {"trace_id": tid} + + elif name == "telemetry_start_span": + kind = SpanKind(args.get("kind", "internal")) + sid = t.start_span(args["name"], kind, + attributes=args.get("attributes")) + result = {"span_id": sid} + + elif name == "telemetry_end_span": + t.end_span(args["span_id"], error=args.get("error", "")) + result = {"ok": True} + + elif name == "telemetry_record_metric": + t.record_metric( + args["name"], args["value"], + unit=args.get("unit", ""), + labels=args.get("labels"), + ) + result = {"ok": True} + + elif name == "telemetry_tool_call": + t.record_tool_call( + args["tool_name"], int(args["duration_ms"]), + bool(args["success"]), + token_usage=int(args.get("token_usage", 0)), + ) + result = {"ok": True} + + elif name == "telemetry_export": + data = t.export() + result = t.summary() + + elif name == "telemetry_summary": + result = t.summary() + + elif name == "telemetry_prometheus": + result = {"metrics": t.export_prometheus()} + + elif name == "telemetry_start_prometheus": + port = args.get("port", t.prometheus_port) + t.prometheus_port = int(port) + ok = t.start_prometheus_server() + result = {"ok": ok, "url": f"http://{t.prometheus_host}:{t.prometheus_port}/metrics"} + + else: + result = {"error": f"Unknown: {name}"} + + except Exception as e: + result = {"error": str(e)} + + print(json.dumps({ + "jsonrpc": "2.0", "id": rid, + "result": { + "content": [{"type": "text", + "text": json.dumps(result, ensure_ascii=False)}], + }, + }), flush=True) + + +# ── CLI ──────────────────────────────────────────────────────── + +def main(): + import argparse + parser = argparse.ArgumentParser(description="DeepCode Telemetry") + parser.add_argument("--mcp", action="store_true", help="MCP Server 模式") + parser.add_argument("--format", choices=["json_file", "otlp_http", "prometheus", "console"], + default="json_file") + sub = parser.add_subparsers(dest="command") + + p = sub.add_parser("trace", help="创建 Trace") + p.add_argument("--name", required=True) + p.add_argument("--attrs", default="{}") + + p = sub.add_parser("metric", help="记录指标") + p.add_argument("--name", required=True) + p.add_argument("--value", type=float, required=True) + p.add_argument("--unit", default="") + + sub.add_parser("report", help="导出并显示报告") + sub.add_parser("summary", help="显示摘要") + sub.add_parser("prometheus", help="Prometheus 格式") + sub.add_parser("demo", help="运行演示") + + args = parser.parse_args() + + if args.mcp: + asyncio.run(run_mcp()) + return + + t = get_telemetry() + t.export_format = ExportFormat(args.format) + + if args.command == "trace": + attrs = json.loads(args.attrs) + sid = t.start_span(args.name, attributes=attrs) + t.end_span(sid) + print(f"Span: {sid}") + + elif args.command == "metric": + t.record_metric(args.name, args.value, unit=args.unit) + print(f"Metric: {args.name}={args.value}{args.unit}") + + elif args.command == "report": + data = t.export() + print(json.dumps(t.summary(), indent=2, default=str)) + + elif args.command == "summary": + print(json.dumps(t.summary(), indent=2, default=str)) + + elif args.command == "prometheus": + print(t.export_prometheus()) + + elif args.command in ("demo", None): + # Demo + print("=== Telemetry Demo ===\n") + t.start_trace("demo") + + with TraceSpan("tool_call_bash", SpanKind.CLIENT, + {"tool.name": "Bash", "tool.command": "ls -la"}) as sid: + t.add_event(sid, "command_started", {"cwd": os.getcwd()}) + time.sleep(0.1) + t.add_event(sid, "command_completed", {"exit_code": 0, "stdout_len": 42}) + + with TraceSpan("llm_call", SpanKind.CLIENT, + {"model": "deepseek-v4-pro", "prompt_len": 512}) as sid2: + time.sleep(0.05) + t.record_metric("llm.token_usage", 1500, labels={"model": "deepseek-v4-pro"}) + + t.record_tool_call("Bash", 100, True, 0) + t.record_tool_call("Read", 15, True, 500) + t.record_tool_call("Write", 30, False, 200) + + t.record_session("demo_session", 5000, 3, 1) + + print(json.dumps(t.summary(), indent=2, default=str)) + print(f"\n--- Prometheus ---\n{t.export_prometheus()}") + + +if __name__ == "__main__": + main() diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 00000000..9628f869 Binary files /dev/null and b/.mcp.json differ