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
+352
View File
@@ -0,0 +1,352 @@
---
title: ADT 代码同步工具 — 使用指南
created: 2026-05-19
tags:
- SAP
- ADT
- REST
- python
- sync
- pull
- push
- activate
parent: "[[sap-cli/README|SAP ADT 学习笔记总览]]"
prev: "[[sap-cli/06.ADT测试代码|06.ADT 测试代码]]"
---
# ADT 代码同步工具 — 使用指南
> [!abstract] 概述
> 通过 `test_adt_client.py` 脚本实现本地与 SAP 系统之间的 ABAP 代码双向同步。支持拉取代码到本地编辑、修改后推送回 SAP 并自动激活。
>
> 完整工作流遵循 [[sap-cli/03.修改代码-原理]] 中描述的 **锁定 → 编辑 → 保存 → 解锁 → 激活** 流程。
## 1. 快速开始
### 前置条件
```bash
pip install requests
```
### 基本用法
```bash
# 拉取代码(从 SAP 下载到本地)
python sap-cli/test_adt_client.py pull
# 推送代码(从本地上传到 SAP 并激活)
python sap-cli/test_adt_client.py push
```
### 配置修改
脚本顶部的配置区域:
```python
ADT_BASE_URL = "http://support.learningleader.com.cn:55955" # SAP 服务器地址
SAP_CLIENT = "100" # SAP 客户端
USER = "admin2" # 用户名
PASSWORD = "654321" # 密码
PROGRAM_NAME = "ZIDTR_IMPORT_ACCOUNTING_ORDER" # 程序名
```
## 2. 拉取代码 (Pull)
### 流程
```
登录 → GET 源代码 → 保存到本地 .abap 文件
```
### REST API 调用序列
| 步骤 | HTTP 方法 | URL | 说明 |
|------|----------|-----|------|
| 登录 | `GET` | `/sap/bc/adt/compatibility/graph` | 获取 CSRF Token |
| 读取源码 | `GET` | `/sap/bc/adt/programs/programs/{name}/source/main` | Accept: text/plain |
### 关键请求头
```http
GET /sap/bc/adt/programs/programs/zidtr_import_accounting_order/source/main
Accept: text/plain
x-csrf-token: {token}
X-sap-adt-sessiontype: stateless
sap-client: 100
```
### 输出
源代码保存到脚本同目录下的 `{程序名小写}.abap` 文件,例如:
```
sap-cli/zidtr_import_accounting_order.abap
```
> [!tip] 格式说明
> SAP ADT API 返回的源代码每行之间可能插入多余空行。脚本已内置格式优化逻辑,自动清理连续空行。
## 3. 推送代码 (Push)
### 完整流程
```
登录 → 查询传输请求 → 锁定对象 → 写入源代码 → 解锁对象 → 激活对象
```
对应 [[sap-cli/03.修改代码-原理]] 中的完整工作流:
```
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ 1.锁定 │───▶│ 2.写入 │───▶│ 3.解锁 │───▶│ 4.激活 │
│ Lock │ │ Write │ │ Unlock │ │ Activate│
└─────────┘ └─────────┘ └─────────┘ └─────────┘
```
### REST API 调用序列
#### Step 0: 登录获取 CSRF Token
```http
GET /sap/bc/adt/compatibility/graph
x-csrf-token: fetch
X-sap-adt-sessiontype: stateless
```
响应头中返回 CSRF Token,后续所有写操作必须携带。
#### Step 1: 查询传输请求
```http
GET /sap/bc/adt/cts/transportrequests
Accept: application/vnd.sap.adt.transportorganizer.v1+xml
```
响应 XML 中解析第一个状态为 `D`(可修改)的传输请求号。
响应示例:
```xml
<tm:request tm:number="DEVK901360" tm:status="D" tm:desc="更改记录" />
```
> [!important] 传输请求号 (corrNr)
> 传输请求号是代码写入 SAP 的必要参数。脚本自动查询并使用第一个可修改的请求。
>
> XML 属性名带有命名空间前缀:`{http://www.sap.com/cts/adt/tm}number`
#### Step 2: 锁定对象
```http
POST /sap/bc/adt/programs/programs/{name}
?_action=LOCK
&accessMode=MODIFY
X-sap-adt-sessiontype: stateful
x-csrf-token: {token}
```
响应:
```xml
<adtcore:object LOCK_HANDLE="Jn333u2TRIqrbxSAs9ib7Ket9q0=" />
```
> [!warning] 会话状态
> 锁定操作**必须在 stateful 会话**中执行(`X-sap-adt-sessiontype: stateful`)。
> 登录端点必须使用 `/sap/bc/adt/compatibility/graph`(不是 `/discovery`),否则锁定会失败。
#### Step 3: 写入源代码
```http
PUT /sap/bc/adt/programs/programs/{name}/source/main
?lockHandle={lockHandle}
&corrNr={}
Content-Type: text/plain; charset=utf-8
X-sap-adt-sessiontype: stateful
x-csrf-token: {token}
{ABAP }
```
> [!important] 关键参数
> - **lockHandle**:来自锁定步骤的返回值,作为**查询参数**传递
> - **corrNr**:传输请求号,也是查询参数
> - 请求体是**纯文本**(不是 XML),UTF-8 编码
#### Step 4: 解锁对象
```http
POST /sap/bc/adt/programs/programs/{name}
?_action=UNLOCK
&lockHandle={lockHandle}
X-sap-adt-sessiontype: stateful
x-csrf-token: {token}
```
#### Step 5: 激活对象
```http
POST /sap/bc/adt/activation
?method=activate
&preauditRequested=true
Content-Type: application/xml
x-csrf-token: {token}
<?xml version="1.0" encoding="UTF-8"?>
<adtcore:objectReferences xmlns:adtcore="http://www.sap.com/adt/core">
<adtcore:objectReference
adtcore:uri="/sap/bc/adt/programs/programs/{name}"
adtcore:name="{PROGRAM_NAME}"/>
</adtcore:objectReferences>
```
激活响应可能包含错误消息:
```xml
<chkl:messages>
<msg type="E" line="1">
<shortText><txt>Type "XXX" is unknown.</txt></shortText>
</msg>
</chkl:messages>
```
| type | 含义 |
|------|------|
| `E` | 错误 — 阻止激活 |
| `W` | 警告 — 不阻止 |
| `I` | 信息 — 不阻止 |
| `S` | 成功 |
> [!note] 空响应
> 激活成功时可能返回空响应体(HTTP 200),这表示激活无错误。
## 4. 传输请求详解
### 查询可用的传输请求
```http
GET /sap/bc/adt/cts/transportrequests
Accept: application/vnd.sap.adt.transportorganizer.v1+xml
```
### 响应结构
```xml
<feed xmlns="http://www.w3.org/2005/Atom">
<entry>
<tm:request xmlns:tm="http://www.sap.com/cts/adt/tm"
tm:number="DEVK901360"
tm:status="D"
tm:owner="ADMIN2"
tm:desc="更改记录的已生成请求"/>
<tm:task tm:number="DEVK901361" tm:status="D" tm:owner="ADMIN2"/>
</entry>
</feed>
```
### 属性说明
| 属性 | 值 | 说明 |
|------|-----|------|
| `status` | `D` | 可修改 (Modifiable) |
| `status` | `R` | 已发布 (Released) |
| `number` | `DEVK901360` | 传输请求号 |
| `desc` | 文本描述 | 请求描述 |
### XML 解析注意事项
属性名带有命名空间前缀,解析时需要使用完整名称:
```python
tm_ns = "http://www.sap.com/cts/adt/tm"
for req in root.findall(f".//{{{tm_ns}}}request"):
number = req.attrib.get(f"{{{tm_ns}}}number", "") # 不是 "number"
status = req.attrib.get(f"{{{tm_ns}}}status", "") # 不是 "status"
```
## 5. 日志系统
每次执行 push/pull 操作时,详细的 API 请求/响应日志会写入:
```
sap-cli/adt_client.log
```
### 日志内容
```
2026-05-19 10:57:56 | INFO | REQUEST: POST http://.../activation
2026-05-19 10:57:56 | INFO | PARAMS: {'method': 'activate', 'preauditRequested': 'true'}
2026-05-19 10:57:56 | INFO | HEADER: x-csrf-token = xxx...
2026-05-19 10:57:56 | INFO | BODY: <?xml version="1.0" ...>
2026-05-19 10:57:56 | INFO | RESPONSE [ACTIVATE]: HTTP 200 OK
2026-05-19 10:57:56 | INFO | BODY (xml): <chkl:messages>...
2026-05-19 10:57:56 | INFO | Activation msg [E] line=1: Type "XXX" is unknown.
2026-05-19 10:57:56 | INFO | Activation summary: 1 errors, 0 warnings, 0 info
2026-05-19 10:57:56 | WARNING | Activation has ERRORS - object may NOT be active!
```
### 排查激活失败
1. 查看 `adt_client.log``[ACTIVATE]` 部分
2. 搜索 `Activation msg [E]` 找到错误消息
3. 错误消息中的 `line` 是语句序号(不是源文件行号)
4. `href` 中的 `start=XXX` 是字符偏移量,可用于定位源码位置
## 6. 完整 API 端点速查
### 通用端点
| 操作 | 方法 | 端点 | 关键参数 |
|------|------|------|---------|
| 登录/CSRF | `GET` | `/sap/bc/adt/compatibility/graph` | `x-csrf-token: fetch` |
| 服务发现 | `GET` | `/sap/bc/adt/discovery` | — |
| 激活 | `POST` | `/sap/bc/adt/activation` | `method=activate` |
### 程序 (PROG) 端点
| 操作 | 方法 | 端点 | 关键参数 |
|------|------|------|---------|
| 读取源码 | `GET` | `/sap/bc/adt/programs/programs/{name}/source/main` | `Accept: text/plain` |
| 锁定 | `POST` | `/sap/bc/adt/programs/programs/{name}` | `_action=LOCK` |
| 写入源码 | `PUT` | `/sap/bc/adt/programs/programs/{name}/source/main` | `lockHandle`, `corrNr` |
| 解锁 | `POST` | `/sap/bc/adt/programs/programs/{name}` | `_action=UNLOCK` |
### 类 (CLASS) 端点
| 操作 | 方法 | 端点 |
|------|------|------|
| 读取源码 | `GET` | `/sap/bc/adt/oo/classes/{name}/source/main` |
| 锁定 | `POST` | `/sap/bc/adt/oo/classes/{name}` |
| 写入源码 | `PUT` | `/sap/bc/adt/oo/classes/{name}/source/main` |
| 解锁 | `POST` | `/sap/bc/adt/oo/classes/{name}` |
### CTS 传输管理端点
| 操作 | 方法 | 端点 |
|------|------|------|
| 查询传输请求 | `GET` | `/sap/bc/adt/cts/transportrequests` |
| 创建传输请求 | `POST` | `/sap/bc/adt/cts/transportrequests` |
| 发布传输请求 | `POST` | `/sap/bc/adt/cts/transportrequests/{number}/newreleasejobs` |
## 7. ABAP 开发对象激活步骤
> [!important] 标准激活流程
> 1. **修改本地源代码** — 在 `.abap` 文件中编辑
> 2. **同步到 SAP** — `python test_adt_client.py push`
> - 脚本自动执行:锁定 → 写入 → 解锁 → 激活
> 3. **查看日志确认** — 检查 `adt_client.log` 确认无激活错误
## 🔗 相关笔记
- [[sap-cli/03.修改代码-原理|03.修改代码 - 原理]] — 锁定/编辑/激活原理
- [[sap-cli/04.检查代码-原理|04.检查代码 - 原理]] — 语法检查与 ATC
- [[sap-cli/05.提交与传输-原理|05.提交与传输 - 原理]] — CTS 传输管理
- [[sap-cli/06.ADT测试代码|06.ADT 测试代码]] — 初始测试脚本
## 📚 参考来源
- [abap-adt-py (GitHub)](https://github.com/timkoehne/abap-adt-py) — Python ADT 客户端库
- [SAP Help Portal - ADT User Guide](https://help.sap.com/docs/abap-cloud/abap-development-tools-user-guide/about-abap-development-tools-user-guide)
- [End-to-End SAP Automation with ADT REST Services](https://medium.com/@onuryz.itu/end-to-end-sap-automation-with-adt-rest-services-and-ai-a-modern-alternative-to-gui-scripting-343ea86064ea)