refactor: 本仓升为唯一源(原 sap-cli 源码仓归档)

方向反转:此前 SKILL.md 是「模板渲染产物」、sap-cli 是源;现 sap-cli 归档,
sap-cli-skill 承接开发与分发,SKILL.md 回归手工维护的正本。

迁移(来自 sap-cli,共 104 文件):
- tests/           692 例测试(15 个文件的内联 sys.path 改指 assets/)
- openspec/        SDD 规格与归档变更(42 文件)
- docs/            开发文档与 ADT 原理(含 dev/CLAUDE.md、AGENTS.md)
- .claude/         rules 副本 + settings.json(供 Claude Code)
- .github/ .hermes/ .pre-commit-config.yaml .editorconfig CLAUDE.md
- scripts/ 保持仅 setup.py(pack_skill.py 已随旧仓归档,不迁)

修复(迁移暴露的真实缺陷):
- assets/pyproject.toml 的 build-backend 写作 `setuptools.backends._legacy:_Backend`,
  该模块在 setuptools 中不存在 → `pip install -e` 从来装不上。改为 build_meta。
  实测:临时 venv 安装成功,sap-cli --help 正常列出 31 个命令
- pyproject readme 指向不存在的 assets/README.md(editable 安装会失败)→ 改内联文本
- pyproject urls 改指 sap-cli-skill

机制调整:
- .github/workflows/ci.yml 适配 assets/ 布局;顶部注明该工作流仅 GitHub 执行,
  本仓在 Gitee 不会自动跑
- pre-commit 增本地测试门禁(Gitee 上真正生效的那道)
- .gitignore 合并旧仓完整规则(保留 log/ 下 md 知识库入库,只忽略运行日志)
- 大文件上限 100KB→1MB(架构图 512KB)

守卫测试 tests/unit/test_repo_guards.py(10 → 18 例):
- SKILL.md 须记录 parser 全部 CLI 命令 / 铁律 1-5 须为真实小节标题 / 示例不得违反铁律 5
- references/ 规则齐备;.claude/rules 与 references 必须一致(实测抓到一次真实漂移)
- VERSION == sapcli.__version__ == README 版本
- 仓内不得再出现 pack_skill.py / skill-src(防废弃流程回潮)

698 tests OK;editable 安装与 CLI 入口经临时 venv 实测通过。
docs/RELEASING.md 重写为单源开发流程。
This commit is contained in:
吴让宇
2026-09-11 00:40:15 +08:00
parent e786742bcb
commit c5905a5b1e
104 changed files with 20744 additions and 10 deletions
+379
View File
@@ -0,0 +1,379 @@
# sap-cli — 技术说明
## 目录
- [项目定位](#项目定位)
- [架构概览](#架构概览)
- [包结构](#包结构)
- [模块职责](#模块职责)
- [通信流程](#通信流程)
- [对象类型映射](#对象类型映射)
- [异常体系](#异常体系)
- [配置加载策略](#配置加载策略)
- [日志机制](#日志机制)
- [错误处理策略](#错误处理策略)
- [测试架构](#测试架构)
- [依赖关系](#依赖关系)
---
## 项目定位
sap-cli 是一个轻量级命令行工具,通过 SAP ADT (ABAP Development Tools) REST API 与 SAP NetWeaver 系统交互,实现 ABAP 开发对象的远程管理(创建、同步、下载、查询、删除)。
**核心设计原则:**
- 零 SAP 专有客户端依赖,仅基于标准 HTTP 协议
- 唯一第三方依赖为 `requests`
- 所有通信均通过 ADT REST API 完成,数据格式为 XML / Plain Text
---
## 架构概览
![[sap-cli/doc/assets/architecture.drawio.png]]
架构分为三层:
| 层级 | 组件 | 说明 |
|------|------|------|
| **用户终端** | `main.py` | CLI 入口,argparse 参数解析与命令分发 |
| **核心包** | `sapcli/` | 5 个模块:config / types / exceptions / client / commands |
| **外部系统** | SAP NetWeaver | ADT REST API 端点(Programs / OO Objects / DDIC / CTS |
左侧外部文件通过虚线连接至核心包:
- `config.ini` — 连接配置(读取)
- `.abap 文件` — 本地源代码(读写)
- `log/` — 运行日志(写入)
核心通信链路:`client.py (ADTClient)` 通过 HTTP/RESTBasic Auth + CSRF Token)与 SAP 系统交互。
## 包结构
```
sap-cli/
├── main.py # CLI 入口(argparse + 命令分发)
├── config.ini # 默认连接配置文件
├── sapcli/ # 核心功能包
│ ├── __init__.py
│ ├── config.py # 配置加载(INI / 环境变量)
│ ├── types.py # 对象类型定义与名称解析
│ ├── client.py # ADT REST API 客户端
│ ├── commands.py # 5 个业务命令的实现
│ └── exceptions.py # 自定义异常层级
├── test/
│ └── script/
│ └── test_main.py # 集成测试套件(子进程调用)
├── doc/ # 文档
│ ├── ADT/ # ADT 技术原理系列
│ ├── 技术说明.md # 本文档
│ ├── sap-cli使用指南.md # 用户操作指南
│ └── 测试报告.md # 测试覆盖报告
└── log/ # 运行时日志目录
```
---
## 模块职责
### main.py — CLI 入口
| 组件 | 职责 |
|------|------|
| 参数定义 | 定义 5 个子命令(download / sync / delete / info / create)及其参数 |
| 配置加载 | 调用 `sapcli.config.load_config()` 获取连接参数 |
| 客户端初始化 | 创建 `ADTClient` 实例并执行登录(获取 CSRF Token |
| 命令分发 | 根据子命令名称映射到对应的 `cmd_*()` 函数 |
### sapcli/config.py — 配置管理
| 组件 | 职责 |
|------|------|
| `SAPConfig` | 连接参数数据类(host / client / user / password |
| `load_config()` | 按优先级链加载配置:环境变量 → 指定文件 → 工作目录 → 脚本目录 |
### sapcli/types.py — 对象类型系统
| 组件 | 职责 |
|------|------|
| `ObjectTypeConfig` | 单个 ABAP 对象类型的 URI 模板与元数据(数据类) |
| `OBJECT_TYPE_CONFIG` | 9 种 ABAP 对象类型的注册表 |
| `ParsedName` | 解析后的对象名组件(数据类) |
| `parse_object_name()` | 将用户输入解析为 ADT URI 所需的路径片段 |
| `get_type_config()` | 按类型键查询对象类型配置 |
| `all_type_keys()` | 返回所有受支持的对象类型列表 |
### sapcli/client.py — ADT 通信层
| 方法 | 职责 |
|------|------|
| `login()` | Basic Auth 认证 + 获取 CSRF Token |
| `object_exists()` | 检查对象是否存在于 SAP 系统 |
| `get_source()` | 下载对象源代码 |
| `set_source()` | 上传(写入)对象源代码 |
| `lock()` / `unlock()` | 对象锁定 / 解锁(stateful session |
| `get_transport_request()` | 获取可用的传输请求号 |
| `activate()` | 激活对象 |
| `syntax_check()` | 语法检查 |
| `create_object()` | 创建新对象(支持 9 种类型) |
| `delete_object()` | 删除对象 |
| `function_group_exists()` | 检查函数组是否存在 |
| `create_function_group()` | 创建函数组容器 |
### sapcli/commands.py — 命令处理
| 函数 | 职责 |
|------|------|
| `cmd_download()` | 下载对象源码到本地 `.abap` 文件 |
| `cmd_sync()` | 同步本地源码到 SAP(含自动创建、锁定、写入、检查、激活) |
| `cmd_info()` | 查询并展示对象元数据 |
| `cmd_delete()` | 从 SAP 系统删除对象(需用户确认) |
| `cmd_create()` | 在 SAP 系统创建新对象(支持模板预填充) |
### sapcli/exceptions.py — 异常层级
| 异常类 | 触发场景 |
|--------|---------|
| `SapCliError` | 所有自定义异常的基类 |
| `ConfigError` | 配置文件缺失或格式错误 |
| `LoginError` | SAP 登录失败(认证错误或网络不通) |
| `ObjectNotFoundError` | 请求的对象在 SAP 系统中不存在 |
| `ObjectAlreadyExistsError` | 创建时对象已存在 |
| `LockError` | 对象锁定冲突 |
| `ActivationError` | 对象激活失败 |
| `SyntaxCheckError` | 语法检查未通过 |
| `DeleteError` | 删除操作失败 |
| `CreateError` | 创建操作失败 |
| `InvalidNameError` | 对象名称格式不合法 |
---
## 通信流程
### 认证机制
采用 HTTP Basic Auth + CSRF Token 双重验证:
```
┌────────┐ ┌────────┐
│ Client │ │ SAP │
└───┬────┘ └───┬────┘
│ GET /sap/bc/adt/compatibility/graph │
│ Header: x-csrf-token: fetch │
│ Header: Authorization: Basic *** │
│ ──────────────────────────────────────────► │
│ │
│ 200 OK │
│ Header: x-csrf-token: <TOKEN> │
│ ◄────────────────────────────────────────── │
│ │
│ 后续所有请求携带 CSRF Token │
│ (POST / PUT / DELETE 必须携带) │
│ ──────────────────────────────────────────► │
```
### sync 命令 — 完整 API 调用链
`sync` 是最核心的命令,执行以下完整流程:
```
┌─────────────────────────────────────────────────────────────┐
│ Step 1: 登录获取 CSRF Token │
│ GET /sap/bc/adt/compatibility/graph │
├─────────────────────────────────────────────────────────────┤
│ Step 2: 检查对象存在性(不存在则自动创建) │
│ GET /sap/bc/adt/programs/programs/{name} │
├─────────────────────────────────────────────────────────────┤
│ Step 3: 获取传输请求号 │
│ GET /sap/bc/adt/cts/transportrequests │
├─────────────────────────────────────────────────────────────┤
│ Step 4: 锁定对象(stateful session
│ POST /sap/bc/adt/programs/programs/{name} │
│ ?_action=LOCK&accessMode=MODIFY │
├─────────────────────────────────────────────────────────────┤
│ Step 5: 写入源代码 │
│ PUT /sap/bc/adt/programs/programs/{name}/source/main │
│ ?lockHandle={handle} │
├─────────────────────────────────────────────────────────────┤
│ Step 6: 解锁对象 │
│ POST /sap/bc/adt/programs/programs/{name} │
│ ?_action=UNLOCK&lockHandle={handle} │
├─────────────────────────────────────────────────────────────┤
│ Step 7: 语法检查(可选) │
│ POST /sap/bc/adt/activation?method=check │
├─────────────────────────────────────────────────────────────┤
│ Step 8: 激活对象 │
│ POST /sap/bc/adt/activation?method=activate │
└─────────────────────────────────────────────────────────────┘
```
### 锁定与传输请求 — 容错机制
工具内置了两层自动重试策略:
1. **锁定重试** — 若对象已在其他传输请求中被锁定,从错误响应中提取已有的传输请求号,使用该传输号重新锁定
2. **写入重试** — 若写入源码时因传输号不匹配而失败,自动使用正确的传输号重试
### Session 管理
| 操作类型 | Session 模式 | 说明 |
|---------|-------------|------|
| 锁定 / 写入 / 解锁 | **stateful** | 通过 `X-sap-adt-sessiontype: stateful` 头维持会话状态 |
| 读取 / 激活 | **stateless** | 无状态请求,独立响应 |
| 解锁后 | 自动切换回 **stateless** | 确保会话资源释放 |
---
## 对象类型映射
每种 ABAP 对象类型对应不同的 ADT URI 模板:
| 类型 | 对象 URI | 源码 URI | 备注 |
|------|---------|---------|------|
| `report` | `/sap/bc/adt/programs/programs/{name}` | `.../source/main` | ABAP 报表程序 |
| `class` | `/sap/bc/adt/oo/classes/{name}` | `.../source/main` | ABAP 类 |
| `interface` | `/sap/bc/adt/oo/interfaces/{name}` | `.../source/main` | ABAP 接口 |
| `function` | `/sap/bc/adt/functions/groups/{group}/fmodules/{name}` | `.../source/main` | 需 `函数组/函数名` 格式 |
| `functiongroup` | `/sap/bc/adt/functions/groups/{name}` | 无源码 | 仅容器,不含源码 |
| `domain` | `/sap/bc/adt/ddic/domains/{name}` | `.../source/main` | ABAP 域 |
| `dataelement` | `/sap/bc/adt/ddic/dataelements/{name}` | `.../source/main` | ABAP 数据元素 |
| `table` | `/sap/bc/adt/ddic/tables/{name}` | `.../source/main` | ABAP 透明表 |
| `tabletype` | `/sap/bc/adt/vit/wb/object_type/ttypda/object_name/{name}` | 无源码 (VIT 端点) | ABAP 表类型,仅支持元数据查询 |
> [!note] function 类型的特殊性
> `function` 类型需要 `函数组名/函数名` 格式输入(如 `ZGROUP/Z_MY_FUNC`),URI 中包含两层路径参数(`{group}` 和 `{name}`)。
---
## 配置加载策略
配置按以下优先级链加载,高优先级源覆盖低优先级源:
```
┌──────────────────────────────┐
│ 环境变量 (SAP_HOST 等) │ ← 最高优先级
└──────────────┬───────────────┘
│ 覆盖
┌──────────────▼───────────────┐
│ --config 指定的 INI 文件 │
└──────────────┬───────────────┘
│ 回退
┌──────────────▼───────────────┐
│ 当前工作目录 / config.ini │
└──────────────┬───────────────┘
│ 回退
┌──────────────▼───────────────┐
│ 脚本同目录 / config.ini │ ← 最低优先级
└──────────────────────────────┘
```
支持的环境变量映射:
| INI 字段 | 环境变量 |
|---------|---------|
| `host` | `SAP_HOST` |
| `client` | `SAP_CLIENT` |
| `user` | `SAP_USER` |
| `password` | `SAP_PASSWORD` |
---
## 日志机制
| 属性 | 值 |
|------|-----|
| 日志路径 | `log/adt_tools.log` |
| 日志级别 | `DEBUG`(记录所有 HTTP 请求/响应详情) |
| 日志格式 | `时间 │ 级别 │ 消息` |
| 写入模式 | 追加写入(append) |
日志记录每次 API 调用的完整上下文:请求 URL、请求参数、响应状态码、响应体摘要,便于生产环境问题排查。
---
## 错误处理策略
### 按场景分类
| 场景 | 处理方式 | 对应异常 |
|------|---------|---------|
| 登录失败 | 终止并报错 | `LoginError` |
| 对象不存在(download / info / delete | 提示不存在并退出 | `ObjectNotFoundError` |
| 对象不存在(sync) | 自动创建空对象后继续 | — |
| 对象已存在(create) | 提示已存在并退出 | `ObjectAlreadyExistsError` |
| 语法检查失败 | 显示错误详情,不执行激活 | `SyntaxCheckError` |
| 激活失败 | 显示错误/警告详情并退出 | `ActivationError` |
| 锁定冲突 | 自动提取已有传输号重试 | `LockError` |
| 写入冲突 | 自动使用正确传输号重试 | — |
| 删除操作 | 需用户输入 `yes` 确认 | — |
### 错误处理设计原则
1. **快速失败** — 非预期错误立即终止,避免静默失败
2. **自动恢复** — 锁定冲突和写入冲突具备自动重试能力
3. **信息透明** — 所有错误均输出详细信息,日志记录完整上下文
---
## 测试架构
测试套件位于 `test/script/test_main.py`,通过子进程调用 `main.py`,确保端到端验证。
### 测试设计
- **独立性** — 每个测试场景独立运行,使用时间戳生成唯一临时对象名
- **完整性** — 每个场景覆盖完整生命周期(创建 → 验证 → 修改 → 验证 → 删除 → 验证)
- **自清理** — 测试结束后自动清理临时文件(`test_output/` 目录)
- **可分组** — 支持按场景组选择运行
### 测试场景组
| 组名 | 说明 | 包含场景 |
|------|------|---------|
| `happy` | 正常流程 | Report / Class / Function+FunctionGroup / Interface 生命周期 |
| `error` | 错误处理 | 语法错误 / 对象不存在 / 重复创建 |
| `edge` | 边界条件 | sync 自动创建 / 删除确认 / DDIC 查询 |
| `existing` | 已有对象操作 | 对已有对象的下载和同步 |
| `config` | 配置验证 | 配置文件加载测试 |
---
## 依赖关系
### 运行时依赖
```
main.py
├── sapcli/ # 内部包
│ ├── config.py
│ │ └── configparser # 解析 INI 配置文件
│ ├── client.py
│ │ ├── requests # HTTP 通信(唯一第三方依赖)
│ │ └── xml.etree # 解析 ADT XML 响应
│ ├── commands.py
│ │ └── logging # 日志记录
│ ├── types.py
│ │ └── re # 对象名称解析
│ └── exceptions.py
│ └── (无外部依赖)
├── argparse # 命令行参数解析
└── logging # 日志配置
```
### 测试时依赖
```
test/script/test_main.py
├── subprocess # 子进程调用 main.py
└── 标准库(os, sys, time, shutil, re
```
### 第三方依赖汇总
| 依赖 | 用途 | 安装方式 |
|------|------|---------|
| `requests` | HTTP 通信 | `pip install requests` |
> [!tip] 零额外依赖
> 除 `requests` 外,所有依赖均为 Python 标准库,无需额外安装。