方向反转:此前 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 重写为单源开发流程。
23 KiB
23 KiB
title, created, updated, tags, parent, related
| title | created | updated | tags | parent | related | |||||
|---|---|---|---|---|---|---|---|---|---|---|
| 批量同步设计 — 项目清单驱动 | 2026-05-25 | 2026-05-25 |
|
sap-cli/doc/sap-cli使用指南 |
|
批量同步设计 — 项目清单驱动
[!abstract] 概述 为 sap-cli 工具增加项目清单机制,支持批量源代码同步。核心思路:在源代码目录下维护
manifest.json清单文件,记录每个开发对象在 SAP 系统中的状态,后续操作基于清单驱动,避免重复查询。
[!info] 设计动机 现有
sync命令只支持单对象操作,缺少以下能力:
- 批量扫描本地文件并识别对象
- 预查询 SAP 系统状态,制定执行计划
- 按依赖关系排序,批量执行同步
- 缓存对象状态,避免每次重复查询
1. 目录约定
1.1 项目目录结构
项目根目录下按对象类型组织子目录,manifest.json 位于根目录:
project/ ← --path 指向这里(项目根目录)
├── manifest.json ← 状态清单(init 自动生成)
├── reports/
│ └── {对象名小写}.abap
├── classes/
│ └── {对象名小写}.abap
├── interfaces/
│ └── {对象名小写}.abap
├── functions/
│ └── {函数组名小写}/
│ └── {函数模块名小写}.abap
├── domains/
│ └── {对象名小写}.abap
├── dataelements/
│ └── {对象名小写}.abap
├── tables/
│ └── {对象名小写}.abap
└── tabletypes/
└── {对象名小写}.abap
1.2 目录名 → 类型映射
init 扫描时按以下映射表从目录名推断对象类型:
| 目录名 | 对象类型 | 文件 → 对象名规则 |
|---|---|---|
reports/ |
report |
zmy_report.abap → ZMY_REPORT |
classes/ |
class |
zcl_my_class.abap → ZCL_MY_CLASS |
interfaces/ |
interface |
zif_my_interface.abap → ZIF_MY_INTERFACE |
functions/ |
function |
zgroup/z_my_func.abap → ZGROUP/Z_MY_FUNC |
domains/ |
domain |
z_status.abap → Z_STATUS |
dataelements/ |
dataelement |
z_status.abap → Z_STATUS |
tables/ |
table |
zmy_table.abap → ZMY_TABLE |
tabletypes/ |
tabletype |
zty_table.abap → ZTY_TABLE |
[!note] 文件名 → 对象名规则
- 文件名去掉
.abap后缀后转为大写即为对象名functions/下的子目录名作为函数组名,子目录内的文件名作为函数模块名- 对象名为
函数组名大写/函数模块名大写格式
2. 清单文件 (manifest.json)
2.1 结构定义
{
"version": 1,
"last_init": "2026-05-25T10:00:00",
"last_refresh": "2026-05-25T14:00:00",
"objects": {
"<对象名>": {
"type": "<对象类型>",
"file": "<相对文件路径>",
"system_status": "<状态>",
"corr_nr": "<传输请求号或null>",
"depends_on": ["<依赖对象名>"],
"last_sync": "<ISO时间戳或null>",
"last_sync_result": "<结果>"
}
}
}
2.2 字段说明
| 字段 | 类型 | 说明 |
|---|---|---|
version |
int | 清单格式版本号,当前为 1 |
last_init |
string | 最近一次 init 的时间戳 |
last_refresh |
string | 最近一次 refresh 的时间戳 |
objects |
object | 以对象名为 key 的字典 |
objects.<name>.type |
string | 对象类型(report/class/interface/...) |
objects.<name>.file |
string | 相对于项目根目录的文件路径 |
objects.<name>.system_status |
string | active / inactive / not_exists |
objects.<name>.corr_nr |
string|null | 传输请求号,null 表示本地对象 $TMP |
objects.<name>.depends_on |
string[] | 依赖的对象名列表 |
objects.<name>.last_sync |
string|null | 最近一次成功同步的时间 |
objects.<name>.last_sync_result |
string | success / failed / pending |
2.3 示例
{
"version": 1,
"last_init": "2026-05-25T10:00:00",
"last_refresh": null,
"objects": {
"Z_STATUS": {
"type": "domain",
"file": "domains/z_status.abap",
"system_status": "active",
"corr_nr": "DEVK901362",
"depends_on": [],
"last_sync": "2026-05-25T10:30:00",
"last_sync_result": "success"
},
"ZIF_MY_INTERFACE": {
"type": "interface",
"file": "interfaces/zif_my_interface.abap",
"system_status": "active",
"corr_nr": "DEVK901362",
"depends_on": [],
"last_sync": "2026-05-25T10:30:00",
"last_sync_result": "success"
},
"ZCL_MY_CLASS": {
"type": "class",
"file": "classes/zcl_my_class.abap",
"system_status": "active",
"corr_nr": "DEVK901362",
"depends_on": ["ZIF_MY_INTERFACE"],
"last_sync": "2026-05-25T11:00:00",
"last_sync_result": "success"
},
"ZMY_REPORT": {
"type": "report",
"file": "reports/zmy_report.abap",
"system_status": "inactive",
"corr_nr": null,
"depends_on": ["ZCL_MY_CLASS"],
"last_sync": null,
"last_sync_result": "pending"
},
"ZGROUP/Z_MY_FUNC": {
"type": "function",
"file": "functions/zgroup/z_my_func.abap",
"system_status": "active",
"corr_nr": "DEVK901362",
"depends_on": [],
"last_sync": "2026-05-25T10:00:00",
"last_sync_result": "success"
}
}
}
3. 命令变更一览
| 命令 | 变更类型 | 说明 |
|---|---|---|
init |
新增 | 扫描项目目录 → 查询 SAP → 生成 manifest.json |
create |
变更 | 新增 --path 参数;创建成功后自动写入清单 |
sync |
变更 | 新增 --all 批量模式;单对象模式操作后更新清单 |
delete |
变更 | 新增 --path 参数;删除成功后从清单移除 |
refresh |
新增 | 重新查询清单中所有对象的 SAP 状态 |
4. 命令详细设计
4.1 init — 项目初始化(新增)
命令格式
python main.py init --path <项目根目录>
执行流程
扫描项目目录
├─ 按目录映射表识别文件 → 生成 {对象名, 类型, 文件路径} 列表
├─ 输出: "扫描到 N 个本地文件"
└─ 忽略非 .abap 文件和隐藏文件
逐个查询 SAP 系统
├─ 对象存在 → 查询 info → 获取 system_status + corr_nr
├─ 对象不存在 → 标记 system_status: "not_exists"
└─ 查询失败 → 标记 system_status: "unknown"
应用类型默认优先级生成初始 depends_on
(init 时只设置类型层面的默认依赖,用户可后续手动编辑)
domain/dataelement/table → 无依赖
tabletype → 依赖同项目中的 table
interface → 无依赖
class → 依赖同项目中被引用的 interface
function → 无显式依赖
report → 无显式依赖
写入 manifest.json
输出汇总报告
输出示例
============================================================
sap-cli 项目初始化
============================================================
项目路径: ./project
扫描到 5 个本地文件
→ 正在查询 SAP 系统状态...
✓ Z_STATUS (domain) active corr: DEVK901362
✓ ZIF_MY_INTERFACE (interface) active corr: DEVK901362
✓ ZCL_MY_CLASS (class) active corr: DEVK901362
⚠ ZMY_REPORT (report) inactive corr: 本地对象
✗ ZCL_NEW_CLASS (class) 不存在
✓ 清单已生成: ./project/manifest.json
5 个对象: 3 已激活, 1 未激活, 1 不存在
[!tip] 清单可手动编辑
manifest.json生成后,用户可以手动编辑depends_on字段来补充对象间的显式依赖关系。init只设置类型层面的默认依赖,具体的引用关系(如某个 class 实现了哪个 interface)需要用户自行标注。
4.2 sync --all — 批量同步(新增)
命令格式
python main.py sync --all --path <项目根目录> [--corr_nr <传输请求号>] [--fail-fast] [--dry-run]
| 参数 | 必需 | 说明 |
|---|---|---|
--all |
是 | 启用批量模式 |
--path |
是 | 项目根目录(包含 manifest.json) |
--corr_nr |
否 | 统一传输请求号(跳过交互选择,所有对象共用) |
--fail-fast |
否 | 遇到失败立即停止(默认:跳过并继续) |
--dry-run |
否 | 仅输出执行计划,不实际操作 |
执行流程
读取 manifest.json
├─ 不存在 → 报错: "请先执行 init"
└─ 存在 → 继续
扫描本地文件 vs 清单差异
├─ 本地有但清单没有的文件 → 提示: "N 个未纳入清单的文件,建议先 create"
└─ 清单有但本地被删的文件 → 警告,跳过
过滤待处理对象
├─ system_status == "active" && last_sync_result == "success" → 跳过(已同步)
├─ system_status == "not_exists" → 标记为 [create + sync]
└─ 其他 → 标记为 [sync]
拓扑排序
├─ 按 depends_on 建有向图
├─ 无显式依赖的按类型默认优先级排序
└─ 检测循环依赖 → 报错退出
--dry-run 模式:
输出执行计划,不实际操作
传输请求预选择(一次性交互):
├─ 扫描待处理对象,统计需要传输请求号的数量
├─ 指定了 --corr_nr → 直接使用,无交互
├─ 全部对象已绑定请求号或为本地对象 → 无交互
└─ 有对象需要请求号 → 弹出一次选择,选定后全局复用:
├─ 选择已有请求号 → 后续所有需要请求号的对象都使用此请求号
├─ 新建请求号 → 同上
└─ 不使用请求号 → 所需对象都作为本地对象处理
执行阶段(按排序顺序逐个处理,使用预选的传输请求号):
对每个对象:
├─ 本地文件不存在 → 跳过,警告
├─ system_status == "not_exists" → 先执行 create(使用预选请求号)
├─ 执行 sync 逻辑(使用预选请求号,跳过逐对象交互):
│ ├─ 对象已绑定请求号 → 使用对象自身绑定的请求号
│ ├─ 本地对象($TMP)→ 无需请求号
│ └─ 未绑定 → 使用预选的统一请求号(不再弹出交互)
├─ 成功 → 更新 manifest:
│ system_status: "active"
│ corr_nr: 实际使用的请求号
│ last_sync: 当前时间
│ last_sync_result: "success"
└─ 失败 →
├─ --fail-fast → 停止,输出已处理和未处理的对象
└─ 默认 → 标记 last_sync_result: "failed",跳过,继续下一个
后续依赖此对象的对象也自动跳过(标记 skipped)
输出汇总报告
dry-run 输出示例
============================================================
sap-cli 批量同步 (dry-run)
============================================================
项目路径: ./project
清单对象: 5
执行计划(按依赖排序):
1. Z_STATUS (domain) → sync [inactive → active]
2. ZIF_MY_INTERFACE (interface) → 跳过 [已是最新]
3. ZCL_MY_CLASS (class) → sync [依赖: ZIF_MY_INTERFACE ✓]
4. ZMY_REPORT (report) → sync [依赖: ZCL_MY_CLASS]
5. ZGROUP/Z_MY_FUNC (function) → 跳过 [已是最新]
待处理: 3 个 | 跳过: 2 个
执行输出示例
============================================================
sap-cli 批量同步
============================================================
项目路径: ./project
待处理: 3 个对象
→ 预检传输请求需求...
2 个对象已绑定请求号: Z_STATUS(DEVK901362), ZCL_MY_CLASS(DEVK901362)
1 个对象需要传输请求: ZMY_REPORT
→ 查询可用的传输请求...
找到 2 个可修改的传输请求:
1. DEVK901362 Implement sales validation (所有者: ADMIN2)
2. DEVK901365 Bugfix for invoice printing (所有者: ADMIN2)
3. 新建传输请求
0. 不使用传输请求(本地对象)
请选择 [0-3]: 1
✓ 统一传输请求: DEVK901362(后续所有对象共用此请求号)
── [1/3] Z_STATUS (domain) ──
✓ 锁定成功(对象已绑定传输请求: DEVK901362)
✓ 源代码写入成功
✓ 解锁成功
✓ 语法检查通过
✓ 激活成功
── [2/3] ZCL_MY_CLASS (class) ──
✓ 锁定成功(对象已绑定传输请求: DEVK901362)
✓ 源代码写入成功
✓ 解锁成功
✓ 语法检查通过
✓ 激活成功
── [3/3] ZMY_REPORT (report) ──
✓ 锁定成功(使用统一传输请求: DEVK901362)
✓ 源代码写入成功
✓ 解锁成功
✗ 语法检查未通过! 1 个错误
[错误] 行 10: Field "XXX" is unknown
⊘ 跳过(标记为 failed)
============================================================
批量同步完成
============================================================
✓ 成功: Z_STATUS, ZCL_MY_CLASS
✗ 失败: ZMY_REPORT (语法错误,行 10)
⊘ 跳过: (无)
清单已更新: ./project/manifest.json
[!tip] 指定
--corr_nr跳过交互 如果已知传输请求号,使用--corr_nr参数可以直接跳过交互选择:python main.py sync --all --path ./project --corr_nr DEVK901362此时所有未绑定请求号的对象都会使用指定的请求号,全程零交互。
4.3 refresh — 刷新清单状态(新增)
命令格式
python main.py refresh --path <项目根目录>
执行流程
读取 manifest.json
逐个重新查询 SAP info
更新每个对象的 system_status 和 corr_nr
(不修改 last_sync 和 last_sync_result)
输出变更摘要
使用场景
- 其他人修改了 SAP 端的对象
- 传输请求状态发生变化(释放、导入等)
- 需要确认当前系统状态但不想执行同步
输出示例
============================================================
sap-cli 清单刷新
============================================================
项目路径: ./project
清单对象: 5
→ 正在查询 SAP 系统状态...
✓ Z_STATUS (domain) active corr: DEVK901362 (未变化)
✓ ZIF_MY_INTERFACE (interface) active corr: DEVK901362 (未变化)
~ ZCL_MY_CLASS (class) inactive corr: DEVK901362 (状态变更: active → inactive)
✓ ZMY_REPORT (report) inactive corr: null (未变化)
✓ ZGROUP/Z_MY_FUNC (function) active corr: DEVK901362 (未变化)
✓ 刷新完成
变更: 1 个 | 未变化: 4 个
4.4 create — 变更:自动写入清单
变更内容
- 新增
--path参数:指向项目根目录 - 创建成功后:自动在
manifest.json中新增一条记录
新增的清单记录
{
"ZCL_NEW_CLASS": {
"type": "class",
"file": "classes/zcl_new_class.abap",
"system_status": "active",
"corr_nr": "DEVK901362",
"depends_on": [],
"last_sync": "2026-05-25T14:00:00",
"last_sync_result": "success"
}
}
向后兼容
- 如果未指定
--path,或指定目录下没有manifest.json:仅创建对象,不写清单 - 现有不含
--path的用法不受影响
4.5 delete — 变更:从清单移除
变更内容
- 新增
--path参数:指向项目根目录 - 删除成功后:从
manifest.json中移除该对象记录
向后兼容
- 如果未指定
--path,或指定目录下没有manifest.json:仅删除对象,不更新清单
4.6 sync(单对象) — 变更:操作后更新清单
变更内容
- 新增
--path参数:指向项目根目录(可选) - sync 成功或失败后:更新
manifest.json中对应对象的记录
向后兼容
- 如果未指定
--path,或指定目录下没有manifest.json:行为与现有完全一致
5. 依赖排序策略
5.1 类型默认优先级
无显式 depends_on 时,按类型优先级排序:
| 优先级 | 数值 | 类型 | 说明 |
|---|---|---|---|
| 1 | 10 | domain |
基础域定义 |
| 2 | 20 | dataelement |
依赖 domain |
| 3 | 30 | table |
依赖 dataelement |
| 4 | 40 | tabletype |
依赖 table |
| 5 | 50 | interface |
接口定义 |
| 6 | 60 | class |
可能实现 interface |
| 7 | 70 | function |
函数模块 |
| 8 | 80 | report |
程序,可能引用以上所有 |
5.2 排序算法
采用拓扑排序(Kahn 算法):
1. 以 manifest 中所有对象为节点
2. 建立有向边:
- 显式依赖: depends_on 中列出的对象 → 当前对象
- 类型默认优先级: 仅当两个对象之间没有显式依赖时作为 tie-breaker
3. 执行 Kahn 拓扑排序
4. 检测循环依赖 → 若存在未入队的节点则报错,输出循环链
5.3 依赖失败传播
批量 sync 时,若对象 A 依赖对象 B,而 B 同步失败:
A.depends_on = ["B"]
B.last_sync_result = "failed"
→ A 自动跳过,标记 last_sync_result: "skipped"
→ 依赖 A 的对象同样跳过(级联跳过)
6. 边界情况处理
| 场景 | 处理方式 |
|---|---|
| 本地新增了 .abap 文件但清单中没有 | sync --all 时提示: "N 个未纳入清单的文件",建议先执行 create |
| 清单中有对象但本地文件被删了 | sync --all 时警告并跳过该对象 |
manifest.json 不存在 |
sync --all 报错,提示先执行 init |
| 依赖对象 sync 失败 | 跳过所有依赖它的对象(级联标记 skipped) |
| 循环依赖 | 检测到后报错,输出循环链中的对象名 |
| 非项目目录(无子目录结构) | init 时只扫描当前目录下的 .abap 文件 |
| SAP 连接中断 | 标记已处理的对象,输出中断位置,下次可从断点继续 |
manifest.json 版本不匹配 |
提示版本不兼容,建议重新 init |
| 部分对象已绑定请求号,部分未绑定 | 已绑定的使用自身请求号,未绑定的使用预选的统一请求号 |
| 全部对象都是本地对象($TMP) | 无需传输请求,直接执行,零交互 |
指定了 --corr_nr 但部分对象已绑定其他请求号 |
已绑定的使用自身请求号(不受 --corr_nr 覆盖) |
7. 项目生命周期
┌─────────────────────────────────────────────────────────┐
│ │
│ init ────────────────────────────────────────────── │
│ │ 扫描本地文件 → 查询 SAP → 生成 manifest.json │
│ │ │
│ ▼ │
│ create(新增对象时) ──────────────────────────────── │
│ │ 在 SAP 创建 → 自动写入 manifest │
│ │ │
│ ▼ │
│ sync --all(日常同步)───────────────────────────── │
│ │ 读 manifest → 拓扑排序 → 逐个 sync → 更新 manifest │
│ │ │
│ ▼ │
│ refresh(SAP 端有变化时)──────────────────────────── │
│ 重新查询 SAP → 更新 manifest 中的状态字段 │
│ │
└─────────────────────────────────────────────────────────┘
[!tip] 典型工作流
# 1. 首次使用:初始化项目清单 python main.py init --path ./my_project # 2. 日常开发:新增对象 python main.py create --name ZCL_NEW --type class --path ./my_project --description "新类" # 3. 日常开发:修改本地 .abap 文件后,批量同步 python main.py sync --all --path ./my_project # 4. 如需查看执行计划(不实际操作) python main.py sync --all --path ./my_project --dry-run # 5. 其他人修改了 SAP 端对象后,刷新清单 python main.py refresh --path ./my_project
8. 模块设计(实现层面)
8.1 新增模块
| 模块 | 职责 |
|---|---|
sapcli/manifest.py |
清单文件的读取、写入、查询、更新 |
sapcli/scanner.py |
扫描本地目录,识别对象名和类型 |
sapcli/sorter.py |
拓扑排序,依赖检测 |
8.2 清单模块 (manifest.py) 接口
@dataclass
class ManifestEntry:
name: str
type: str
file: str
system_status: str # "active" / "inactive" / "not_exists"
corr_nr: str | None
depends_on: list[str]
last_sync: str | None # ISO timestamp
last_sync_result: str # "success" / "failed" / "pending"
class Manifest:
version: int
last_init: str | None
last_refresh: str | None
objects: dict[str, ManifestEntry]
@classmethod
def load(cls, project_path: str) -> "Manifest": ...
def save(self) -> None: ...
def upsert(self, entry: ManifestEntry) -> None: ...
def remove(self, name: str) -> None: ...
def get(self, name: str) -> ManifestEntry | None: ...
def pending_objects(self) -> list[ManifestEntry]: ...
8.3 扫描模块 (scanner.py) 接口
DIRECTORY_TYPE_MAP = {
"reports": "report",
"classes": "class",
"interfaces": "interface",
"functions": "function",
"domains": "domain",
"dataelements": "dataelement",
"tables": "table",
"tabletypes": "tabletype",
}
@dataclass
class ScannedObject:
name: str # 大写对象名,function 类型含 /
type: str # 对象类型
file: str # 相对于项目根目录的路径
def scan_project(project_path: str) -> list[ScannedObject]: ...
8.4 排序模块 (sorter.py) 接口
TYPE_PRIORITY = {
"domain": 10,
"dataelement": 20,
"table": 30,
"tabletype": 40,
"interface": 50,
"class": 60,
"function": 70,
"report": 80,
}
class CyclicDependencyError(SapCliError): ...
def topological_sort(
objects: list[ManifestEntry],
) -> list[ManifestEntry]: ...
# → 按 depends_on + TYPE_PRIORITY 排序
# → 检测循环依赖,抛出 CyclicDependencyError
9. 相关笔记
- sap-cli/doc/sap-cli使用指南 — 现有命令的使用说明
- sap-cli/doc/技术说明 — 技术架构与模块设计
- sap-cli/doc/ADT/03.修改代码-原理 — 锁定/编辑/激活工作流