docs/design/parser-two-layer-refactor-plan.md
| 项目 | 信息 |
|---|---|
| 状态 | 已完成 |
| 创建日期 | 2026-04-13 |
| 完成日期 | 2026-04-14 |
将原有的单一层 Parser 架构拆分为 Accessor(数据访问层) 和 Parser(数据解析层) 两层,实现职责分离和代码复用。
| 问题 | 说明 |
|---|---|
| 平铺式注册 | 所有 Parser 在同一层级,职责不清晰 |
| 后缀冲突 | .zip 可被 CodeRepositoryParser 和 ZipParser 同时处理 |
| URL 处理逻辑分散 | UnifiedResourceProcessor._process_url() 和 ParserRegistry.parse() 都有 URL 检测 |
| 职责混合 | 部分 Parser 既负责下载又负责解析 |
.zip 等后缀冲突| 层级 | 抽象接口 | 职责 | 示例 |
|---|---|---|---|
| L1: Accessor | DataAccessor | 获取数据:远程 URL / 特殊路径 → 本地文件/目录 | GitAccessor, HTTPAccessor, FeishuAccessor |
| L2: Parser | BaseParser | 解析数据:本地文件/目录 → ParseResult | MarkdownParser, PDFParser, ZipParser |
add_resource(path)
↓
ResourceProcessor.process_resource()
↓
UnifiedResourceProcessor.process()
↓
┌─────────────────────────────────────────┐
│ 第一阶段:数据访问 (Accessor Layer) │
├─────────────────────────────────────────┤
│ AccessorRegistry.route(source) │
│ ├─→ GitAccessor (priority: 80) │
│ ├─→ FeishuAccessor (priority: 100) │
│ ├─→ HTTPAccessor (priority: 50) │
│ └─→ LocalAccessor (priority: 10) │
│ ↓ │
│ 返回: LocalResource │
│ (本地路径 + 元数据) │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 第二阶段:数据解析 (Parser Layer) │
├─────────────────────────────────────────┤
│ ParserRegistry.route(local_resource) │
│ ├─→ 是目录? → DirectoryParser │
│ ├─→ 是文件? → 按扩展名匹配 │
│ │ ├─→ .md → MarkdownParser │
│ │ ├─→ .pdf → PDFParser │
│ │ ├─→ .zip → ZipParser │
│ │ └─→ ... │
│ └─→ 返回: ParseResult │
└─────────────────────────────────────────┘
↓
TreeBuilder + SemanticQueue (保持不变)
位置:openviking/parse/accessors/base.py
表示一个可在本地访问的资源,是 Accessor 层的输出。
@dataclass
class LocalResource:
path: Path # 本地文件/目录路径
source_type: str # 原始来源类型 (SourceType.GIT/HTTP/FEISHU/LOCAL)
original_source: str # 原始 source 字符串
meta: Dict[str, Any] # 元数据(repo_name, branch, content_type 等)
is_temporary: bool = True # 是否为临时文件,解析后可清理
def cleanup(self) -> None # 清理临时资源
def __enter__/__exit__ # 支持上下文管理器
位置:openviking/parse/accessors/base.py
class DataAccessor(ABC):
@abstractmethod
def can_handle(self, source: Union[str, Path]) -> bool
"""判断是否能处理该来源"""
@abstractmethod
async def access(self, source: Union[str, Path], **kwargs) -> LocalResource
"""获取数据到本地,返回 LocalResource"""
@property
@abstractmethod
def priority(self) -> int
"""优先级:数字越大优先级越高
- 100: 特定服务 (Feishu)
- 80: 版本控制 (Git)
- 50: 通用协议 (HTTP)
- 10: 兜底 (Local)
"""
def cleanup(self, resource: LocalResource) -> None
"""清理资源(默认调用 resource.cleanup())"""
位置:openviking/parse/accessors/registry.py
class AccessorRegistry:
def __init__(self, register_default: bool = True)
"""初始化注册表,可选是否注册默认 Accessor"""
def register(self, accessor: DataAccessor) -> None
"""注册 Accessor(按优先级降序插入)"""
def unregister(self, accessor_name: str) -> bool
"""注销 Accessor"""
def get_accessor(self, source) -> Optional[DataAccessor]
"""获取能处理该 source 的最高优先级 Accessor"""
async def access(self, source, **kwargs) -> LocalResource
"""路由到合适的 Accessor 获取数据"""
默认注册的 Accessor(按优先级):
FeishuAccessor (100) - 处理飞书/ Lark 文档GitAccessor (80) - 处理 Git 仓库HTTPAccessor (50) - 处理 HTTP/HTTPS URLLocalAccessor (10) - 处理本地文件(兜底)openviking/parse/accessors/ 目录结构DataAccessor 抽象基类和 LocalResource 数据类AccessorRegistry(含优先级机制)GitAccessor - 处理 Git 仓库HTTPAccessor - 处理 HTTP URLFeishuAccessor - 处理飞书文档LocalAccessor - 处理本地文件get_accessor_registry()PDFParser、resources.py、local_input_guard.py 使用新架构openviking/parse/
├── accessors/ # ✅ 新增:数据访问层
│ ├── __init__.py
│ ├── base.py # DataAccessor, LocalResource, SourceType
│ ├── registry.py # AccessorRegistry
│ ├── git_accessor.py # GitAccessor
│ ├── http_accessor.py # HTTPAccessor
│ ├── feishu_accessor.py # FeishuAccessor
│ └── local_accessor.py # LocalAccessor
├── parsers/ # 数据解析层(保持不变)
│ ├── base_parser.py
│ ├── markdown.py
│ ├── pdf.py
│ ├── zip_parser.py
│ └── ...
└── registry.py # ParserRegistry(保持不变)
from openviking.parse.accessors import get_accessor_registry
# 获取全局注册表
registry = get_accessor_registry()
# 访问资源(自动路由)
async with await registry.access("https://github.com/user/repo") as resource:
print(f"本地路径: {resource.path}")
print(f"来源类型: {resource.source_type}")
# 使用 resource.path 进行解析...
# 退出 with 块时自动清理临时资源
from openviking.parse.accessors import DataAccessor, LocalResource
class MyAccessor(DataAccessor):
@property
def priority(self) -> int:
return 90
def can_handle(self, source: str) -> bool:
return source.startswith("myprotocol://")
async def access(self, source: str, **kwargs) -> LocalResource:
# 获取数据到本地...
temp_path = self._create_temp_dir()
# ... 下载逻辑 ...
return LocalResource(
path=temp_path,
source_type="myprotocol",
original_source=source,
meta={},
is_temporary=True
)
# 注册
registry = get_accessor_registry()
registry.register(MyAccessor())