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:
@@ -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/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 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 锁定与传输请求 — 容错机制
|
||||
|
||||
工具内置了两层自动重试策略:
|
||||
|
||||
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 标准库,无需额外安装。
|
||||
Reference in New Issue
Block a user