方向反转:此前 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 重写为单源开发流程。
17 KiB
17 KiB
sap-cli — 技术说明
目录
项目定位
sap-cli 是一个轻量级命令行工具,通过 SAP ADT (ABAP Development Tools) REST API 与 SAP NetWeaver 系统交互,实现 ABAP 开发对象的远程管理(创建、同步、下载、查询、删除)。
核心设计原则:
- 零 SAP 专有客户端依赖,仅基于标准 HTTP 协议
- 唯一第三方依赖为
requests - 所有通信均通过 ADT REST API 完成,数据格式为 XML / Plain Text
架构概览
架构分为三层:
| 层级 | 组件 | 说明 |
|---|---|---|
| 用户终端 | 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/REST(Basic 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 │
└─────────────────────────────────────────────────────────────┘
锁定与传输请求 — 容错机制
工具内置了两层自动重试策略:
- 锁定重试 — 若对象已在其他传输请求中被锁定,从错误响应中提取已有的传输请求号,使用该传输号重新锁定
- 写入重试 — 若写入源码时因传输号不匹配而失败,自动使用正确的传输号重试
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 确认 |
— |
错误处理设计原则
- 快速失败 — 非预期错误立即终止,避免静默失败
- 自动恢复 — 锁定冲突和写入冲突具备自动重试能力
- 信息透明 — 所有错误均输出详细信息,日志记录完整上下文
测试架构
测试套件位于 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 标准库,无需额外安装。
