Understand-Anything 语言提取器架构:用 LanguageExtractor 接口让 tree-sitter 结构分析支持十余种语言
本文基于 Understand-Anything 仓库中的实施计划文档 docs/superpowers/plans/2026-04-15-language-extractors-impl.md,完整讲解该项目的多语言结构提取(language extractor)架构:从 LanguageExtractor 接口设计、TreeSitterPlugin 分发机制,到 extract-structure.mjs 确定性提取脚本与 file-analyzer agent 的接入方式。读完后你将理解如何把一个只为 TypeScript/JavaScript 硬编码的 AST 解析器,重构为可插拔的多语言提取架构,并能复现"接口定义 → 语言配置 → 逐语言实现 → 打包脚本 → Agent 集成"的完整落地路径。
背景与目标:两个核心动机
该计划文档开篇明确了两个目标(Goal):
- 把 AST 提取逻辑与 TS/JS 专属节点类型解耦,让另外 8 种代码语言(Python、Go、Rust、Java、Ruby、PHP、C/C++、C#)获得基于 tree-sitter 的结构化分析能力。计划中明确排除了 Swift 和 Kotlin——当时没有可用的 WASM 语法包(注:当前仓库后续通过工作区自编译的 WASM 包补上了这些语言,见文末"仓库现状"一节)。
- 用确定性的、预构建的 tree-sitter 提取脚本,替换 file-analyzer agent 每次临时让 LLM 编写的一次性正则脚本。此前 Phase 1 要求 agent 现场生成正则提取脚本,既慢又不确定,且完全浪费了 tree-sitter 基础设施。
技术栈为:web-tree-sitter(WASM)、TypeScript、Vitest。
总体架构与文件布局
计划的 Architecture 一节给出了整体设计:引入 LanguageExtractor 接口,每种语言实现一份;TreeSitterPlugin 把提取工作委派给按文件语言注册好的 extractor;skills/understand/ 目录下的 extract-structure.mjs 脚本通过 PluginRegistry(同时包含 TreeSitterPlugin 与非代码文件的正则解析器)为 file-analyzer agent 提供确定性的结构提取。
计划中的文件结构如下:
packages/core/src/plugins/
├── extractors/
│ ├── types.ts # LanguageExtractor 接口 + TreeSitterNode 类型再导出
│ ├── base-extractor.ts # 共享工具(traverse、getStringValue 等)
│ ├── typescript-extractor.ts # TS/JS(从 tree-sitter-plugin.ts 迁出)
│ ├── python-extractor.ts
│ ├── go-extractor.ts
│ ├── rust-extractor.ts
│ ├── java-extractor.ts
│ ├── ruby-extractor.ts
│ ├── php-extractor.ts
│ ├── cpp-extractor.ts
│ ├── csharp-extractor.ts
│ └── index.ts # builtinExtractors 数组 + 再导出
├── tree-sitter-plugin.ts # 重构为基于 extractor 分发
└── tree-sitter-plugin.test.ts # 既有测试(必须保持通过)
packages/core/src/plugins/__tests__/
└── extractors.test.ts # 所有新 extractor 的测试
skills/understand/
├── extract-structure.mjs # 预构建的 tree-sitter 提取脚本(新增)
└── SKILL.md # 更新为引用 extract-structure.mjs
agents/
└── file-analyzer.md # Phase 1 改写为执行预构建脚本
路径说明:下文所有仓库路径以仓库根目录为起点,核心代码位于 packages/core 包内,实际路径前缀为
understand-anything-plugin/packages/core/。
Task 1:LanguageExtractor 接口与共享工具库
接口定义
接口放在 types.ts,核心是把 tree-sitter AST 映射到项目统一的 StructuralAnalysis / CallGraphEntry 类型上:
// packages/core/src/plugins/extractors/types.ts
import type { StructuralAnalysis, CallGraphEntry } from "../../types.js";
// Re-export the tree-sitter Node type for use by extractors
export type TreeSitterNode = import("web-tree-sitter").Node;
/**
* Language-specific extractor that maps a tree-sitter AST
* to the common StructuralAnalysis / CallGraphEntry types.
*/
export interface LanguageExtractor {
/** Language IDs this extractor handles (must match LanguageConfig.id) */
languageIds: string[];
/** Extract functions, classes, imports, exports from the root AST node */
extractStructure(rootNode: TreeSitterNode): StructuralAnalysis;
/** Extract caller→callee relationships from the root AST node */
extractCallGraph(rootNode: TreeSitterNode): CallGraphEntry[];
}
三个契约点值得注意:
languageIds必须与LanguageConfig.id严格对齐(例如 Python 配置中id: "python",见 python.ts),这样插件注册与查询才能闭环;extractStructure只接收 root 节点,所有语言都输出同一个StructuralAnalysis结构(functions/classes/imports/exports),下游(图谱构建、指纹、仪表盘)无需感知语言差异;extractCallGraph提取 caller→callee 关系,输出CallGraphEntry[](含行号)。
共享工具函数
base-extractor.ts 把原先散落在 tree-sitter-plugin.ts 中的 AST 遍历工具抽成共享模块,所有语言 extractor 复用:
import type { TreeSitterNode } from "./types.js";
/** Recursively traverse an AST tree, calling the visitor for each node. */
export function traverse(
node: TreeSitterNode,
visitor: (node: TreeSitterNode) => void,
): void {
visitor(node);
for (let i = 0; i < node.childCount; i++) {
const child = node.child(i);
if (child) traverse(child, visitor);
}
}
/** Extract the unquoted string value from a string-like node. */
export function getStringValue(node: TreeSitterNode): string {
for (let i = 0; i < node.childCount; i++) {
const child = node.child(i);
if (child && child.type === "string_fragment") {
return child.text;
}
}
return node.text.replace(/^['"`]|['"`]$/g, "");
}
/** Find the first child matching a type. */
export function findChild(node: TreeSitterNode, type: string): TreeSitterNode | null {
for (let i = 0; i < node.childCount; i++) {
const child = node.child(i);
if (child && child.type === type) return child;
}
return null;
}
/** Find all children matching a type. */
export function findChildren(node: TreeSitterNode, type: string): TreeSitterNode[] {
const result: TreeSitterNode[] = [];
for (let i = 0; i < node.childCount; i++) {
const child = node.child(i);
if (child && child.type === type) result.push(child);
}
return result;
}
/** Check if a node has a child of the given type (used for export/visibility checks). */
export function hasChildOfType(node: TreeSitterNode, type: string): boolean {
for (let i = 0; i < node.childCount; i++) {
const child = node.child(i);
if (child && child.type === type) return true;
}
return false;
}
这些工具覆盖了三类高频操作:全树遍历(调用图游走)、字符串节点取值(import/include 路径)、子节点查找(导出/可见性判断)。以 python-extractor.ts 为例,findChild 被用于 extractClass 中识别类型注解赋值(name: str),findChildren 被用于收集 dotted_name import 说明符——这正是计划"共享工具"意图的落地证据。
Task 2:TS/JS 提取逻辑迁入 TypeScriptExtractor 并重构插件分发
这是一次纯重构:所有既有测试必须零修改通过。
TypeScriptExtractor 迁移要点
把 tree-sitter-plugin.ts 中所有 TS/JS 专属提取方法(extractFunction、extractClass、extractVariableDeclarations、extractImport、processExportStatement、extractParams、extractReturnType、extractImportSpecifiers 及调用图游走器)整体迁入 typescript-extractor.ts,实现 LanguageExtractor 接口。
计划特别强调一个易错点:languageIds 应为 ["typescript", "javascript"],不能包含 "tsx"——tsx 是 TreeSitterPlugin 内部用于选择语法的合成键(.tsx 文件需要独立的 TSX 语法包),不是 LanguageConfig.id;tsx→typescript 的映射由 getExtractor() 处理。
插件侧的 extractor 分发
重构后的 tree-sitter-plugin.ts 维护一张 Map<string, LanguageExtractor>,注册与查询逻辑(见源码 L104-L123):
// In TreeSitterPlugin
private extractors = new Map<string, LanguageExtractor>();
registerExtractor(extractor: LanguageExtractor): void {
for (const id of extractor.languageIds) {
this.extractors.set(id, extractor);
}
}
private getExtractor(langKey: string): LanguageExtractor | null {
// tsx is a synthetic grammar key — extraction logic is identical to typescript
const key = langKey === "tsx" ? "typescript" : langKey;
return this.extractors.get(key) ?? null;
}
analyzeFile() 变为"取 parser → 解析 → 按扩展名查语言键 → 分发给 extractor"的标准流程:
analyzeFile(filePath: string, content: string): StructuralAnalysis {
const parser = this.getParser(filePath);
if (!parser) return { functions: [], classes: [], imports: [], exports: [] };
const tree = parser.parse(content);
if (!tree) { parser.delete(); return { functions: [], classes: [], imports: [], exports: [] }; }
const langKey = this.languageKeyFromPath(filePath);
const extractor = langKey ? this.getExtractor(langKey) : null;
let result: StructuralAnalysis;
if (extractor) {
result = extractor.extractStructure(tree.rootNode);
} else {
result = { functions: [], classes: [], imports: [], exports: [] };
}
tree.delete();
parser.delete();
return result;
}
extractCallGraph() 遵循同样模式,且必须保持完全一致的 parser/tree 生命周期管理(tree.delete()、parser.delete()),避免 WASM 内存泄漏。构造函数接受可选 extractors 数组;若不提供则注册内置 TypeScriptExtractor 以兼容旧调用方。
验证标准:运行 pnpm --filter @understand-anything/core test,期望全部 426 个测试通过(与重构前一致)。
值得补充的是当前源码在此基础上的演进:TreeSitterPlugin 现在按语言键缓存可复用 parser(_parsers Map,见 L47),并新增了单次解析同时返回结构与调用图的 analyzeFileFull()(L275-L300),因为两个 extractor 都是同一 rootNode 的纯函数,单次解析即可得到字节一致的结果,把索引热路径上的解析工作量削减约 40%。
Task 2.5:PluginRegistry 补上 extractCallGraph,DEFAULT_PLUGIN_CONFIG 改为动态派生
计划指出两个"配套缺口":
PluginRegistry此前只暴露analyzeFile与resolveImports,没有extractCallGraph,而 Task 13 的脚本需要通过 registry 拿到调用图数据;DEFAULT_PLUGIN_CONFIG硬编码了["typescript", "javascript"],无法反映新增语言。
registry.ts 新增方法
当前 registry.ts 的实现与计划一致:
extractCallGraph(filePath: string, content: string): CallGraphEntry[] | null {
const plugin = this.getPluginForFile(filePath);
if (!plugin?.extractCallGraph) return null;
return plugin.extractCallGraph(filePath, content);
}
返回 null(而非空数组)用于区分"没有插件支持该文件"与"支持但无调用"两种情况。registry 通过 LanguageRegistry 完成扩展名→语言→插件的映射,而非硬编码查找表。
discovery.ts 动态派生语言列表
当前 discovery.ts 已按计划落地:
import { builtinLanguageConfigs } from "../languages/configs/index.js";
export const DEFAULT_PLUGIN_CONFIG: PluginConfig = {
plugins: [
{
name: "tree-sitter",
enabled: true,
languages: builtinLanguageConfigs
.filter((c) => c.treeSitter)
.map((c) => c.id),
},
],
};
这意味着新增一种语言只需改语言配置(加 treeSitter 块),默认插件配置自动跟随,不会再出现"支持了 Python 但默认配置里没有 python"的不一致。
Task 3:添加 8 个语法依赖与 treeSitter 语言配置
语法包依赖
计划要求在 package.json 的 dependencies 中新增 8 个 tree-sitter 语法包。当前仓库实际版本:
"tree-sitter-c-sharp": "^0.23.1",
"tree-sitter-cpp": "^0.23.4",
"tree-sitter-go": "^0.25.0",
"tree-sitter-java": "^0.23.5",
"tree-sitter-php": "^0.23.11",
"tree-sitter-python": "^0.25.0",
"tree-sitter-ruby": "^0.23.1",
"tree-sitter-rust": "^0.24.0"
此外还有 tree-sitter-typescript、tree-sitter-javascript、tree-sitter-scala,以及 @tree-sitter-grammars/tree-sitter-kotlin 和两个工作区包 @understand-anything/tree-sitter-dart-wasm、@understand-anything/tree-sitter-swift-wasm(仓库后续演进,见文末)。
语言配置的 treeSitter 块
10 个语言配置各加一个 treeSitter 字段,声明 WASM 包名与文件名。以 python.ts 为例(当前仓库实态):
export const pythonConfig = {
id: "python",
displayName: "Python",
extensions: [".py", ".pyi"],
treeSitter: {
wasmPackage: "tree-sitter-python",
wasmFile: "tree-sitter-python.wasm",
},
concepts: [ /* 语言概念提示词列表 */ ],
filePatterns: { /* 入口/桶文件/测试/配置的 glob 规则 */ },
} satisfies LanguageConfig;
计划给出的各语言配置示例:
// python.ts
treeSitter: { wasmPackage: "tree-sitter-python", wasmFile: "tree-sitter-python.wasm" },
// go.ts
treeSitter: { wasmPackage: "tree-sitter-go", wasmFile: "tree-sitter-go.wasm" },
// rust.ts
treeSitter: { wasmPackage: "tree-sitter-rust", wasmFile: "tree-sitter-rust.wasm" },
// java.ts
treeSitter: { wasmPackage: "tree-sitter-java", wasmFile: "tree-sitter-java.wasm" },
// ruby.ts
treeSitter: { wasmPackage: "tree-sitter-ruby", wasmFile: "tree-sitter-ruby.wasm" },
// php.ts
treeSitter: { wasmPackage: "tree-sitter-php", wasmFile: "tree-sitter-php.wasm" },
// cpp.ts
treeSitter: { wasmPackage: "tree-sitter-cpp", wasmFile: "tree-sitter-cpp.wasm" },
// csharp.ts
treeSitter: { wasmPackage: "tree-sitter-c-sharp", wasmFile: "tree-sitter-c_sharp.wasm" },
注意两个细节:C# 的包名是 tree-sitter-c-sharp,但 wasm 文件名是 tree-sitter-c_sharp.wasm(下划线),包名与文件名不一致是新手容易踩的坑;Swift 与 Kotlin 配置在计划执行时不改动(当时无 WASM 包可用)。
验证 WASM 可解析的命令(计划 Step 3):
pnpm install
node -e "const r=require('module').createRequire(import.meta.url??__filename); console.log(r.resolve('tree-sitter-python/tree-sitter-python.wasm'))"
加载侧的容错设计在 tree-sitter-plugin.ts 中体现:每个语法的加载包在 try/catch 内执行,加载失败只打印 debug 日志并"优雅降级"——该语言被跳过,LLM agent 在 Phase 2 兜底分析,而不是让整个插件初始化失败。
Task 4–11:八种语言提取器的关键 AST 节点与测试用例
计划为每种语言列出了 tree-sitter 的关键节点类型,并给出代表性测试样例与预期结果。这一节是理解"每种语言 extractor 在做什么"的核心。
Python(Task 4)
关键节点类型:
- 函数:
function_definition(name、parameters、return_type) - 类:
class_definition(name、body → 方法 + 类型注解赋值作为属性) - 导入:
import_statement、import_from_statement - 装饰:
decorated_definition包裹function_definition或class_definition - 调用:
call(function 字段) - 无正式导出语法(所有顶层名字视为"已导出")
计划给定的测试样例与断言:
import os
from pathlib import Path
from typing import Optional
class DataProcessor:
name: str
def __init__(self, name: str):
self.name = name
def process(self, data: list) -> dict:
return transform(data)
def helper(x: int) -> str:
return str(x)
@decorator
def decorated_func():
pass
预期:2 个函数(helper、decorated_func)、1 个类(DataProcessor,含方法 __init__/process 与属性 name)、3 条 import、调用图 process→transform。
对照当前实现 python-extractor.ts,可以看到这些断言的实现细节:unwrapDecorated() 负责剥开 decorated_definition(L85-L93);extractParams() 会过滤掉隐式的 self/cls,并把 *args/**kwargs 输出为带前缀的参数名;extractFromImport() 通过节点 id 对比跳过 module_name 本身,支持 from foo import bar as baz 与 from os import * 通配导入。调用图提取使用"函数栈"(functionStack)跟踪当前所在函数,遇到 call 节点且栈非空时记录 caller→callee 与行号。
Go(Task 5)
- 函数:
function_declaration;方法:method_declaration(receiver、name) - 结构体:
type_declaration→type_spec→struct_type;接口:interface_type - 导入:
import_declaration→import_spec_list→import_spec - 导出判定:首字母大写(Go 的可见性约定)
- 调用:
call_expression
测试样例与断言:
package main
import (
"fmt"
"os"
)
type Server struct {
Host string
Port int
}
func (s *Server) Start() error {
fmt.Println("starting")
return nil
}
func NewServer(host string, port int) *Server {
return &Server{Host: host, Port: port}
}
预期:2 个函数(Start、NewServer)、1 个 struct(Server,方法 Start、属性 Host/Port)、2 条 import、导出(Server、Start、NewServer——全部大写开头)、调用图 Start→fmt.Println。
Rust(Task 6)
- 函数:
function_item;struct:struct_item;枚举:enum_item - 实现块:
impl_item(方法体在 impl 内部) - 导入:
use_declaration(scoped_identifier、use_list、use_wildcard) - 导出判定:
visibility_modifier含pub - 调用:
call_expression
测试样例:Config struct + impl Config(pub fn new / fn validate)+ pub fn check_port。预期:3 个函数、1 个 struct(含 new/validate 方法与 name/port 属性)、2 条 import、导出为带 pub 的项(Config、new、check_port)、调用图 validate→check_port。这里体现了 Rust extractor 的特殊性:方法不直接挂在 struct 上,必须解析 impl_item 把方法归并到对应类型。
Java(Task 7)
- 方法:
method_declaration;构造器:constructor_declaration - 类:
class_declaration(name、class_body);接口:interface_declaration - 字段:
field_declaration(declarator → variable_declarator → identifier) - 导入:
import_declaration(scoped_identifier) - 导出判定:
modifiers节点中的public - 调用:
method_invocation(name、object、arguments)
Ruby(Task 8)
- 方法:
method;类:class;模块:module - 导入:
call节点且方法名为require或require_relative(Ruby 用方法调用代替 import 语句) - 调用:
call(method、receiver、arguments) - 无正式导出语法
Ruby 的特点是"导入"与"调用"共用 call 节点,extractor 需要按方法名区分二者。
PHP(Task 9)
- 函数:
function_definition;方法:method_declaration;类:class_declaration(name、declaration_list) - 导入:
namespace_use_declaration(namespace_use_clause) - 调用:
function_call_expression/member_call_expression - 注意:PHP AST 把一切包在
program→php_tag+ 语句 之下
C/C++(Task 10)
- 函数:
function_definition(declarator → function_declarator → identifier + parameter_list) - 类:
class_specifier;结构体:struct_specifier - 包含:
preproc_include(path → string_literal 或 system_lib_string) - 命名空间:
namespace_definition - 调用:
call_expression
两个实现要点:C/C++ 的函数签名是嵌套结构,函数名藏在 function_declarator 内部;cppConfig 的 id: "cpp" 覆盖扩展名 [".cpp", ".cc", ".cxx", ".c", ".h", ".hpp", ".hxx"],纯 C 文件用 C++ 语法解析,extractor 必须优雅处理 C++ 专属节点缺失的情况(解析纯 C 时类返回空数组)。
C#(Task 11)
- 方法:
method_declaration;构造器:constructor_declaration - 类:
class_declaration;接口:interface_declaration;属性:property_declaration - 导入:
using_directive(qualified_name) - 调用:
invocation_expression(identifier/member_access、argument_list)
Task 12 与 Task 15:builtinExtractors 聚合与对外导出
extractors/index.ts
index.ts 按计划聚合所有内置 extractor:
export const builtinExtractors: LanguageExtractor[] = [
new TypeScriptExtractor(),
new PythonExtractor(),
new GoExtractor(),
new RustExtractor(),
new JavaExtractor(),
new RubyExtractor(),
new PhpExtractor(),
new CppExtractor(),
new CSharpExtractor(),
// 仓库后续演进:
new DartExtractor(),
new KotlinExtractor(),
new SwiftExtractor(),
new ScalaExtractor(),
];
TreeSitterPlugin 构造函数中,若调用方未提供 extractors,默认注册 builtinExtractors(见 tree-sitter-plugin.ts L92-L101),这保证"什么都不传也能用全部内置语言"。
Task 15 还要求在 packages/core/src/index.ts 中补上对外导出,因为 extract-structure.mjs 与外部消费者依赖这些导出:
export type { LanguageExtractor } from "./plugins/extractors/types.js";
export { builtinExtractors } from "./plugins/extractors/index.js";
最终验证三步:pnpm --filter @understand-anything/core build → 全量测试 → 收尾提交。
Task 13:extract-structure.mjs 确定性提取脚本
这是计划中最具实战价值的一环:把"LLM 每次现场写正则脚本"替换为"执行预构建脚本"。
使用方式与 I/O 契约
node skills/understand/extract-structure.mjs test-input.json test-output.json
- 输入 JSON:
{ projectRoot, batchFiles: [{path, language, sizeLines, fileCategory}], batchImportData } - 输出 JSON:
{ scriptCompleted, filesAnalyzed, filesSkipped, results: [...] }
计划规定脚本的行为契约:
- 接收输入/输出 JSON 路径两个参数;
- 用
createRequire相对于脚本自身位置(向上两级到插件根)解析@understand-anything/core; - 创建
PluginRegistry,注册TreeSitterPlugin(全部内置语言配置)+ 所有非代码解析器; - 对每个文件:读取内容、调用
registry.analyzeFile(),按既有脚本输出 schema 格式化(functions、classes、exports、sections、definitions、services 等); - 对支持 tree-sitter 的代码文件:额外通过
plugin.extractCallGraph()提取调用图; - 对无插件支持的文件(Swift、Kotlin、未知语言):输出
{ path, language, fileCategory, totalLines, nonEmptyLines, metrics }及空结构数据,交由 LLM 在 Phase 2 处理; - 写出与既有
scriptCompleted/filesAnalyzed/filesSkipped/resultsschema 一致的结果。
当前实现的健壮性增强
对照仓库中的 extract-structure.mjs,实际落地时补上了几处计划未细述的健壮性处理,值得工程参考:
const __dirname = dirname(fileURLToPath(import.meta.url));
// skills/understand/ -> plugin root is two dirs up
const pluginRoot = resolve(__dirname, '../..');
const require = createRequire(resolve(pluginRoot, 'package.json'));
let core;
try {
core = await import(pathToFileURL(require.resolve('@understand-anything/core')).href);
} catch {
// Fallback: direct path for installed plugin cache layouts
core = await import(pathToFileURL(resolve(pluginRoot, 'packages/core/dist/index.js')).href);
}
- Windows 兼容:Node ESM 的动态
import()在 Windows 上要求file://URL,裸绝对路径C:\...会抛ERR_UNSUPPORTED_ESM_URL_SCHEME(loader 把C:当 URL scheme),因此两处解析都包了pathToFileURL(); - 符号链接安装路径:Claude Code / Copilot CLI 的插件缓存默认走符号链接,
import.meta.url经 symlink 解析而process.argv[1]保留 symlink,裸相等判断会让"是否 CLI 入口"检查静默失效,实现中改用realpathSync对两侧规范化后再比较(isCliEntry(),L152-L161),并保证作为模块被 import 时(测试场景)不触发main(); - 行计数语义对齐:POSIX 文本文件以尾换行结尾,
split('\n')会多出一个空元素,实现对totalLines做了与wc -l一致的修正,确保与项目扫描器sizeLines的口径吻合; - 结构化校验:结果映射逻辑被拆到 extract-structure-result.mjs,用纯函数 + 字段校验器(
functions必须有name/lineRange/params,classes必须有name/lineRange/methods/properties等)保证输出 schema 稳定,且单测无需 import 带 shebang 的 CLI 脚本; - 结果计数:输出中新增
analysisOutcomes(structure 与 callGraph 各自的 succeeded/failed/skipped 计数),便于诊断批量提取中哪些文件降级了。
Task 14:重写 file-analyzer.md 的 Phase 1
计划要求删除 file-analyzer agent 中约 150 行的"现场生成正则脚本"指令,替换为三步确定性流程。当前 file-analyzer.md 的实态与计划一致:
- 准备输入 JSON(格式不变):
cat > $PROJECT_ROOT/.understand-anything/tmp/ua-file-analyzer-input-<batchIndex>.json << 'ENDJSON'
{
"projectRoot": "<project-root>",
"batchFiles": [<本批文件,含 fileCategory>],
"batchImportData": <batchImportData JSON>
}
ENDJSON
- 执行预构建脚本:
node <SKILL_DIR>/extract-structure.mjs \
$PROJECT_ROOT/.understand-anything/tmp/ua-file-analyzer-input-<batchIndex>.json \
$PROJECT_ROOT/.understand-anything/tmp/ua-file-extract-results-<batchIndex>.json
- 失败即报告,禁止回退:脚本非零退出时读 stderr 诊断并报告错误,不得回退到手写脚本——预构建脚本是唯一提取路径。另外要求验证输出文件存在且非空(
test -s ...),exit 0 但缺输出文件视为硬失败。
同时,SKILL.md 在派发 file-analyzer 的 prompt 中追加技能目录路径,使 agent 能定位脚本:
> Skill directory (for bundled scripts): `<SKILL_DIR>`
这复用了既有机制——SKILL.md 对 merge-batch-graphs.py、merge-subdomain-graphs.py 等脚本已经通过同样的 <SKILL_DIR> 方式传参。Phase 2(语义分析)读取的 JSON 结构不变,无需改动。
Implementation Notes:四条实施备注
计划末尾的四条备注包含了不少"为什么",这里逐条说明:
-
测试文件约定:每种语言 extractor 有独立测试文件
packages/core/src/plugins/extractors/__tests__/<language>-extractor.test.ts,与tree-sitter-plugin.test.ts就近放置的既有模式一致。当前仓库中 extractors 目录 下确实存在 cpp、csharp、dart、go、java、kotlin、php、python、ruby、rust、scala、swift、typescript 十三份 extractor 测试。 -
语法惰性加载(未来优化):
TreeSitterPlugin.init()通过Promise.all预加载全部语法 WASM。10 个语法合计约 12MB,初始化延迟可能可感。计划的建议是"先测再改"——TS/JS 急加载(最常见),其余延迟到首次使用。这是典型的"不要过早优化"约束。 -
指纹的免费收益:
fingerprint.ts中的buildFingerprintStore内部调用PluginRegistry.analyzeFile。新 extractor 接入后,Python/Go/Rust 等语言的指纹自动从"仅内容哈希"升级为"结构化指纹",零代码改动。这说明统一走 registry 这一抽象层的长期价值:任何消费方都自动获得新语言能力。 -
PHP 语法注意:
tree-sitter-php同时提供tree-sitter-php.wasm(完整 PHP + 内嵌 HTML/CSS/JS)与tree-sitter-php_only.wasm。计划选用完整的tree-sitter-php.wasm,因此 PHP extractor 必须对解析内嵌 HTML 模板时出现的非 PHP AST 节点保持鲁棒。
仓库现状:从计划到实现的完整闭环
对照当前仓库,该计划的所有环节都已落地,且有几处超出原计划范围的演进:
- 语言覆盖从 10 种扩展到 13 种:计划执行时 Swift/Kotlin "无 WASM 包可用",当前仓库通过
@tree-sitter-grammars/tree-sitter-kotlinnpm 包与两个工作区自编译包(tree-sitter-swift-wasm、tree-sitter-dart-wasm)补齐了 Kotlin、Swift、Dart,并新增了 Scala; - 单次解析优化:
analyzeFileFull()让脚本一次 parse 同时拿到结构与调用图,消除了对同一内容的双重解析; - 降级路径明确:
TreeSitterPlugin对无 extractor 的语言、语法加载失败、无插件文件三类情况分别返回空结构/跳过/交给非代码解析器,整个链路保证"任何文件都有确定性的输出形态",LLM 只负责结构化数据之上的语义分析。
对希望在自己的多语言分析器中复用这一套模式的人,该架构给出的可迁移经验可以概括为四条:接口先行(LanguageExtractor 只有两个方法,语言差异被锁在实现类内部)、配置驱动(新语言 = 一份 LanguageConfig + 一个 extractor 注册,默认配置自动派生)、确定性下沉(能用预构建脚本做的绝不留给运行时生成)、优雅降级(缺能力时输出空结构而非报错,把不确定性留给上层兜底)。
参考文件清单
- 实施计划:docs/superpowers/plans/2026-04-15-language-extractors-impl.md
- 接口与工具:types.ts、base-extractor.ts、index.ts
- 插件与注册表:tree-sitter-plugin.ts、registry.ts、discovery.ts
- 语言配置:python.ts、configs/index.ts
- 脚本与 Agent:extract-structure.mjs、extract-structure-result.mjs、file-analyzer.md、SKILL.md
- 相关测试:extractor 测试目录、test_extract_structure_outcomes.test.mjs
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00