Files
吴让宇 c5905a5b1e 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 重写为单源开发流程。
2026-09-11 00:40:15 +08:00

380 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 标准库,无需额外安装。