sapbuddy-dsh 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,158 +1,66 @@
1
1
  # sapbuddy-dsh
2
2
 
3
- [SapBuddy](https://github.com/gxx950224/sapbuddy) 的 **DeepSeek Harness(DSH)** 底座:
4
- **41 个 SAP ABAP 工具 + 8 个 ABAP 技能 + agent preset `sapbuddy` + 三层写门禁 + 连接设置页**。
3
+ [![npm](https://img.shields.io/npm/v/sapbuddy-dsh)](https://www.npmjs.com/package/sapbuddy-dsh) [GitHub](https://github.com/gxx950224/sapbuddy-dsh)
5
4
 
6
- 面向 **DSH 0.2.0-rc.2**,需要 Node >= 22。**一个包名装完全部。**
7
-
8
- ---
5
+ [SapBuddy](https://github.com/gxx950224/sapbuddy) 的 DeepSeek Harness 插件,提供 41 个 SAP ABAP 工具、8 个技能、SapBuddy agent preset 和 SAP 连接设置页。
9
6
 
10
7
  ## 安装
11
8
 
12
- 在 DSH 的 **插件 → 添加插件** 对话框里填:
13
-
14
- ```
15
- sapbuddy-dsh
16
- ```
17
-
18
- 或命令行:
19
-
20
- ```bash
21
- dsh plugin --profile <profile> add sapbuddy-dsh
22
- ```
23
-
24
- **装完必须完整重启 DSH**(托盘退出,不是关窗口)——插件代码在宿主启动时加载。
25
- 重启后新建会话时选择 **SapBuddy** preset,并在 **设置 → SAP 连接** 里新建 SAP 连接。
26
-
27
- ### ⚠️ 手工安装前必须配好构建放行
28
-
29
- 依赖链里有个遥测包会注册构建脚本:
30
-
31
- ```
32
- sapbuddy-dsh → abap-adt-api → io-ts-reporters → @scarf/scarf(遥测,无需构建)
33
- ```
34
-
35
- pnpm 11 把「存在未批准的构建脚本」当**错误**(`ERR_PNPM_IGNORED_BUILDS`,退出码 1)。
36
- 而 **`dsh plugin add` 在 pnpm 失败后不会再注册 bundle** —— 包进了 `node_modules`,
37
- `dsh.profile.bundles` 却没更新,于是 **preset 永远不出现,且没有任何明显报错**。
38
-
39
- 在目标 profile 的 `pnpm-workspace.yaml` 里放行:
9
+ 需要 Node.js 22+ 和兼容的 DSH;具体宿主依赖要求见 package.json 的 peerDependencies。
10
+ 在 DSH 的插件管理页添加:
40
11
 
41
- ```yaml
42
- allowBuilds:
43
- '@scarf/scarf': false
12
+ ```text
13
+ sapbuddy-dsh@0.0.6
44
14
  ```
45
15
 
46
- 或在仓库里跑安装器(会自动做这一步):
16
+ 也可以通过 DSH 命令行安装:
47
17
 
48
18
  ```bash
49
- node scripts/install.mjs --profile desktop
19
+ dsh plugin --profile desktop add sapbuddy-dsh@0.0.6
50
20
  ```
51
21
 
52
- > 包自身声明 `pnpm.ignoredBuiltDependencies` **不足以**覆盖三级传递依赖(已实测),
53
- > 必须在安装侧声明。
22
+ 更新已安装插件后,完整退出并重新启动 DSH,再新建会话选择 **SapBuddy**。
23
+ 在 **设置 → SAP 连接** 中配置连接。请只安装裸包名 `sapbuddy-dsh`,避免与同功能的作用域包或拆分插件同时启用。
54
24
 
55
- ### 这个包往 profile 里插两条行
25
+ 依赖 `@scarf/scarf` 的遥测脚本不影响 SAP 功能。如果包管理器提示未批准构建脚本,可通过宿主提供的依赖脚本管理禁用该遥测脚本,再重试安装。
56
26
 
57
- | 行 | 作用 |
58
- |---|---|
59
- | `sapbuddy-dsh`(顶层) | 设置页的 HTTP 路由,并让客户端页面被加载 |
60
- | preset 内的 `sapbuddy-dsh/tools` | 41 个工具 + 写门禁 + 提示词注入 |
27
+ ## 激活结果核对
61
28
 
62
- 两条都必须存在,写法也受 DSH 约束(`dsh-client-modules` 只扫描**顶层**行、
63
- 且只认**精确包名**)。这也是包的主入口是设置页、工具走子路径的原因。
64
- 详见仓库 README 的「为什么只有一个包」。
29
+ `abap_activate` 先检查待激活源码的语法,执行激活后读取 active 源码进行核对;激活阶段返回错误或抛异常时也会继续核对。
65
30
 
66
- ---
31
+ 例如 INCLUDE 阶段报错,后续主程序实际已经激活:只有 active 源码与激活前快照一致,并且实时未激活清单中没有目标、直接引用的 INCLUDE 或激活响应中报告的未激活对象时,才确认成功并清零该对象的连续失败计数。原始阶段诊断保留,本次不再登记为审计失败。
67
32
 
68
- ## 首次运行会落盘什么
33
+ 源码不一致、相关对象仍未激活或核对查询失败时,继续保留失败。对象元数据中的 `active` 字段不能单独证明本次修改已生效。非源码对象沿用其激活接口结果。
69
34
 
70
- 插件加载时把包内 `defaults/` 中**缺失**的文件补写到配置根
71
- (`<DSH_HOME|~/.dsh>/.agent-presets/sapbuddy/`),并创建 `prompts/ logs/ output/ uploads/`:
35
+ ## 工具与技能
72
36
 
73
- ```
74
- prompts/SYSTEM.md # 铁律与输出约定(每次会话实时注入)
75
- prompts/Memory.md # 避坑记录,随会话累积
76
- AGENTS.md # 项目开发指南
77
- connections.example.json # 连接配置示例(参考用)
78
- settings.example.json
79
- ```
80
-
81
- **仅补缺,绝不覆盖**——`Memory.md` 是累积数据,用户改过的 `SYSTEM.md` / `AGENTS.md` 同理。
82
- 配置根可用环境变量 `SAPBUDDY_HOME` 覆盖。
83
-
84
- 本包**不创建 `connections.json`**:那是用户数据(含凭据),请在
85
- 「设置 → SAP 连接」中新建,或参照 `connections.example.json` 手工创建。
86
- 密码在设置页的响应中脱敏为 `***`。
87
-
88
- ---
89
-
90
- ## 写门禁
91
-
92
- 写操作受三层约束,任一层都能单独拒绝:
93
-
94
- 1. **连接级只读** —— 连接未声明 `security.readOnly: false` 即拒写(连接级优先、顶层兜底,
95
- 读取失败 fail-closed)。连接级判定不会被"不传 `connectionId`"绕过,所以可以做到
96
- 开发系统可写、QAS/生产锁死。
97
- 2. **开发客户端守卫** —— 读 `T000.CCCATEGORY`,默认只放行 `C`(定制/客户开发);
98
- 查不到时 fail-closed。生产系统(`P`)即使关掉只读也写不进去。
99
- 3. **人工确认** —— 需用户批准(确认词须位于句首;窗口默认 15 分钟,见配置根
100
- `settings.json` 的 `approvalWindowMinutes`)。
37
+ 工具涵盖对象搜索与读码、代码导航、语法诊断与激活、ATC 与单元测试、SQL 查询、DDIC、文本池、屏幕文本、消息类、函数模块接口和传输请求。
101
38
 
102
- 另有内容级硬校验:写入含用户可见中文字面量的代码会被拒(须走消息类 / 文本元素)。
103
- 命名空间强制:只写 `Z*`/`Y*`,SAP 标准对象只读。
39
+ 技能包括 `abap-code-review`、`abap-explain`、`abap-templates`、`abap-wiki`、`clean-abap`、`dev-write`、`sap-abap-skills`、`translate-workflow`。
104
40
 
105
- ---
41
+ ## 配置和写入保护
106
42
 
107
- ## 工具清单
43
+ 首次加载时,插件会在 `<DSH_HOME|~/.dsh>/.agent-presets/sapbuddy/` 补充缺失的默认提示词与示例配置;也可用 `SAPBUDDY_HOME` 指定位置。已有连接、Memory 和自定义提示词不会被覆盖。连接凭据通过 SAP 连接设置页管理,不应提交到 Git。
108
44
 
109
- 41 个工具覆盖:对象搜索 / 读码 / 代码定位、ADT 文档、ATC 检查、单元测试、SQL 查询、
110
- DDIC(域 / 数据元素 / 结构 / 表)、文本池与文本元素、屏幕文字、消息类、函数模块接口、
111
- 传输请求、激活与语法诊断、Mermaid 校验等。
45
+ 写操作受连接级只读、开发客户端检查、Z/Y 命名空间及人工授权约束。中文文案采用维护建议,不再按源码中的中文字符统一阻断。
112
46
 
113
- 8 个技能:`abap-code-review`、`abap-explain`、`abap-templates`、`abap-wiki`、
114
- `clean-abap`、`dev-write`、`sap-abap-skills`、`translate-workflow`。
47
+ 插件包含顶层设置入口 `sapbuddy-dsh` 和 preset 内工具入口 `sapbuddy-dsh/tools`;使用插件管理器安装以同时登记 bundle 和 preset。
115
48
 
116
- 完整清单见技能 `sap-abap-skills/references/tool-guide.md`(场景 → 工具 → 传参示例)。
49
+ ## 开发与验证
117
50
 
118
- ---
51
+ 仓库中的 `packages/sapbuddy-dsh` 是可发布的统一插件,包含已构建运行代码、类型声明、默认提示词和技能。维护此目录时直接验证运行代码;这里不提供重建该统一包的 TypeScript 开发树。
119
52
 
120
- ## 导出
121
-
122
- | 子路径 | 内容 |
123
- |---|---|
124
- | `sapbuddy-dsh`(主入口) | 顶层插件:设置页的 `webServer` 路由 |
125
- | `sapbuddy-dsh/tools` | preset 内插件:41 工具注册 + 写门禁 + 提示词注入 |
126
- | `sapbuddy-dsh/client` | 设置页浏览器端 bundle(`dsh.client`) |
127
-
128
- ---
129
-
130
- ## 开发
53
+ 从 GitHub 克隆仓库后,在 `packages/sapbuddy-dsh` 下运行激活回归测试(需要 Node.js 24+,无需连接 SAP):
131
54
 
132
55
  ```bash
133
- pnpm install
134
- pnpm --filter sapbuddy-dsh build # clean + tsc + esbuild → dist/
135
- pnpm --filter sapbuddy-dsh test # 172 用例
56
+ npm run test:activation
57
+ npm pack --dry-run --ignore-scripts
136
58
  ```
137
59
 
138
- 技能与默认提示词分别在 `skills/` 与 `defaults/`,两者都随包发布——改动后请跑一次
139
- `npm pack --dry-run` 确认内容仍在(`README.md` 与 `LICENSE` 是 npm 自动收录的)。
140
-
141
- ---
142
-
143
- ## 链接
144
-
145
- - 仓库与完整文档:<https://github.com/gxx950224/sapbuddy-dsh>
146
- - 上游 CLI / Web 版(pi 引擎):<https://github.com/gxx950224/sapbuddy>
147
- - 问题反馈:<https://github.com/gxx950224/sapbuddy-dsh/issues>
148
-
149
- > 注意区分:npm 上的 `sapbuddy` 是上游 CLI 版;本包 `sapbuddy-dsh` 是 DSH 底座版。
150
- > 旧的 `sapbuddy-tools` / `sapbuddy-settings` / `sapbuddy-settings-ui` 已合并进本包并弃用。
60
+ 回归测试使用模拟 SAP 响应,覆盖误报恢复、失败计数、审计记录和核对失败边界;不能替代真实 SAP 联机验收。
151
61
 
152
62
  ## 许可证
153
63
 
154
- MIT。工具实现同步自上游 [gxx950224/sapbuddy](https://github.com/gxx950224/sapbuddy)。
155
-
156
- ## 0.0.4
64
+ MIT。工具实现来自 [SapBuddy](https://github.com/gxx950224/sapbuddy)。
157
65
 
158
- 本版本基于已构建产物修改发布。取消 ABAP 源码硬编码中文的写入阻断;修复 Web 原生审批获批后未开启默认 15 分钟授权窗口的问题,写入与激活共用窗口。中文文案改为维护建议,其他连接与对象保护保持生效。
66
+ [![](https://img.shields.io/badge/powered_by-dsh-4D6BFE?style=flat-square&logo=deepseek&logoColor=white)](https://github.com/deepseek-ai/deepseek-harness)
package/cordis.patch.yml CHANGED
@@ -52,7 +52,10 @@
52
52
  - id: persona
53
53
  name: '@deepseek-ai/dsh-persona'
54
54
  config:
55
- suffix: Your working directory is {{cwd}}.
55
+ # ⚠️ 不要在这里写双花括号插值的 cwd(或任何未注册变量):DSH 的提示词
56
+ # 插值只注册 provider / model,未知变量会让提示词装配在模型请求前直接
57
+ # 抛错(0.0.4 的自定义后缀就是这么整轮失败的)。工作目录已由运行时
58
+ # 上下文(Current working directory: ...)提供,无需自定义后缀。
56
59
  # 字面块(|)而非折叠块(>-):铁律/要点必须逐行保留,
57
60
  # 折叠会把整段压成一行,编号列表全部失效。
58
61
  prefix: |
@@ -41,7 +41,7 @@ SapBuddy — SAP ABAP AI 全能助手,面向**开发顾问与业务顾问**。
41
41
  1. **用户纠错后先整体复盘**:用户指出"有问题/错了/没完成"时,先重新读取对象现状(激活状态、当前源码、诊断结果),列出全部异常清单、确认排查顺序后再动手;禁止直接埋头修最新一条报错。
42
42
  2. **覆盖型写操作前先备份**:整体覆盖类工具(manage_text_elements 写、translate_text_pool、replace_string)动手前先读当前完整内容备份;操作可能影响多个条目时先说明覆盖范围;一旦发现"修一处坏一处"(补 A 丢 B)立即停手,先还原/说明,禁止继续覆盖式修补。
43
43
  3. **工具不可用即换路**:工具返回 not found / 对象不存在时,说明该路径不通,换可行工具或如实告知用户,禁止继续深挖不存在的能力(如表、工具)。
44
- 4. **报完成前核对本次激活结果**:宣称"完成/验证通过"前,必须确认 abap_activate 本次调用成功;ABAP 源码对象还须通过工具内的 active 源码核验。工具按顺序执行语法检查、激活、版本核验;任一步失败或未确认都应如实报告,不能用 get_abap_object_info 的 active 元数据覆盖失败结论。区分“已保存”“激活失败”“激活未确认”,不得把未验证的错误说成历史提示。
44
+ 4. **报完成前核对本次激活结果**:宣称"完成/验证通过"前,必须确认 abap_activate 本次调用成功;ABAP 源码对象还须通过工具内的 active 源码核验。阶段报错后的最终结论以工具核验结果为准:确认源码一致且无相关未激活对象时,工具清零连续失败计数并保留原始诊断;核验失败或未确认则如实报告,不能仅用 get_abap_object_info 的 active 元数据判定成功。
45
45
  5. **先读后说:位置/调用点类答案必须实证**(回答"在哪改 / 哪个程序 / FORM / 方法 / 增强点 / 第几行"时):
46
46
  - 写进结论的**具体位置**(程序名 / FORM 名 / 方法名 / 行号 / 增强点名)必须**先读到真实源码或对象确认**才能落笔;
47
47
  - 凭记忆记得的调用链只能当"待核实线索",明确标注,**禁止当成事实写进答案**(S/4 与 ECC 结构差异大,凭旧版本记忆必出错——如老版本 FORM `XINBILL_DOCUMENT_SAVE` 在 S/4 不存在);
@@ -6,13 +6,15 @@ export interface ActivationStage {
6
6
  }
7
7
  export interface ActivationReport extends ActivationResult {
8
8
  stages: ActivationStage[];
9
- verification?: "matched" | "mismatch" | "unavailable" | "not_applicable";
9
+ verification?: "matched" | "mismatch" | "unavailable" | "not_applicable" | "pending";
10
+ recovered?: boolean;
10
11
  sourceHash?: string;
11
12
  }
12
13
  /** Preserve the origin of errors; an inactive object or error cannot be a successful stage. */
13
14
  export declare function mergeActivationResults(results: ActivationResult[], names: string[]): ActivationReport;
14
15
  /** Called inside the existing connection write mutex: check -> activate -> read active source. */
15
- export declare function verifyActivation(client: Pick<ADTClient, "getObjectSource" | "syntaxCheck">, obj: {
16
+ export declare function verifyActivation(client: Pick<ADTClient, "getObjectSource" | "syntaxCheck" | "inactiveObjects">, obj: {
16
17
  "adtcore:uri": string;
18
+ "adtcore:name"?: string;
17
19
  "adtcore:type"?: string;
18
20
  }, activate: () => Promise<ActivationReport>): Promise<ActivationReport>;
@@ -55,17 +55,46 @@ export async function verifyActivation(client, obj, activate) {
55
55
  return failedStage("激活前语法检查", `无法确认待激活源码或语法结果,未执行激活:${sanitizeErrMsg(err)}`);
56
56
  }
57
57
  const expected = sourceHash(source);
58
- const result = mergeActivationResults([syntax, await activate()], []);
59
- if (!result.success)
60
- return { ...result, sourceHash: expected };
58
+ let activation;
59
+ try {
60
+ activation = await activate();
61
+ }
62
+ catch (err) {
63
+ activation = failedStage("激活请求", sanitizeErrMsg(err));
64
+ }
65
+ const result = mergeActivationResults([syntax, activation], []);
61
66
  try {
62
67
  const active = await client.getObjectSource(uri, { version: "active" });
63
68
  if (sourceHash(active) !== expected) {
64
69
  return { ...mergeActivationResults([result, failedStage("激活版本核验", "active 源码与激活前核对的源码不一致,本次修改未确认生效。")], []), verification: "mismatch", sourceHash: expected };
65
70
  }
71
+ if (!result.success) {
72
+ // An unchanged main source alone cannot prove its INCLUDE changes took effect.
73
+ // Re-read the live inactive list; the activation response may describe an earlier stage.
74
+ const related = [obj, ...result.inactive.map(r => r.object).filter(Boolean)];
75
+ for (const line of source.split("\n")) {
76
+ const match = /^\s*INCLUDE\s+([A-Za-z/][A-Za-z0-9_/]*)\s*\./i.exec(line);
77
+ if (match)
78
+ related.push({ "adtcore:name": match[1], "adtcore:type": "PROG/I" });
79
+ }
80
+ const normalizeUri = value => String(value ?? "").split(/[?#]/)[0].replace(/\/source\/main\/?$/i, "").replace(/\/$/, "").toLowerCase();
81
+ const pending = await client.inactiveObjects();
82
+ if (!Array.isArray(pending))
83
+ throw new Error("未激活对象清单无效");
84
+ const remaining = pending.filter(r => r.object && related.some(o =>
85
+ (o["adtcore:uri"] && normalizeUri(o["adtcore:uri"]) === normalizeUri(r.object["adtcore:uri"])) ||
86
+ (o["adtcore:name"] && o["adtcore:name"].toUpperCase() === String(r.object["adtcore:name"] ?? "").toUpperCase())));
87
+ if (remaining.length) {
88
+ return { ...mergeActivationResults([result, failedStage("激活状态核验", "目标或相关 INCLUDE 仍存在未激活版本,保留失败计数。")], []), inactive: remaining, verification: "pending", sourceHash: expected };
89
+ }
90
+ return {
91
+ ...result, success: true, inactive: [], verification: "matched", recovered: true, sourceHash: expected,
92
+ stages: [...result.stages, { name: "激活后核验(源码一致且无相关未激活对象)", success: true, messages: [] }],
93
+ };
94
+ }
66
95
  return { ...result, verification: "matched", sourceHash: expected };
67
96
  }
68
97
  catch (err) {
69
- return { ...mergeActivationResults([result, failedStage("激活版本核验", `激活请求已返回,但无法读取 active 源码,结果未确认:${sanitizeErrMsg(err)}`)], []), verification: "unavailable", sourceHash: expected };
98
+ return { ...mergeActivationResults([result, failedStage("激活版本核验", `激活请求已返回,但无法完成激活状态核对,结果未确认:${sanitizeErrMsg(err)}`)], []), verification: "unavailable", sourceHash: expected };
70
99
  }
71
100
  }
@@ -313,7 +313,7 @@ async function activateFunctionGroupFormIncludes(client, fgName) {
313
313
  catch (err) {
314
314
  results.push({
315
315
  success: false,
316
- messages: [{ objDescr: "", type: "E", line: 0, href: "", forceSupported: false, shortText: toToolError(err) }],
316
+ messages: [{ objDescr: "", type: "E", line: 0, href: "", forceSupported: false, shortText: sanitizeErrMsg(err) }],
317
317
  inactive: [],
318
318
  });
319
319
  }
@@ -321,7 +321,7 @@ async function activateFunctionGroupFormIncludes(client, fgName) {
321
321
  }
322
322
  catch (err) {
323
323
  names.push("读取函数组 include 清单");
324
- results.push({ success: false, inactive: [], messages: [{ objDescr: "", type: "E", line: 0, href: "", forceSupported: false, shortText: toToolError(err) }] });
324
+ results.push({ success: false, inactive: [], messages: [{ objDescr: "", type: "E", line: 0, href: "", forceSupported: false, shortText: sanitizeErrMsg(err) }] });
325
325
  }
326
326
  return mergeActivationResults(results, names);
327
327
  }
@@ -351,7 +351,7 @@ async function activateWithIncludes(client, obj) {
351
351
  catch (err) {
352
352
  results.push({
353
353
  success: false,
354
- messages: [{ objDescr: "", type: "E", line: 0, href: "", forceSupported: false, shortText: toToolError(err) }],
354
+ messages: [{ objDescr: "", type: "E", line: 0, href: "", forceSupported: false, shortText: sanitizeErrMsg(err) }],
355
355
  inactive: [],
356
356
  });
357
357
  }
@@ -361,7 +361,7 @@ async function activateWithIncludes(client, obj) {
361
361
  catch (err) {
362
362
  results.push({
363
363
  success: false,
364
- messages: [{ objDescr: "", type: "E", line: 0, href: "", forceSupported: false, shortText: toToolError(err) }],
364
+ messages: [{ objDescr: "", type: "E", line: 0, href: "", forceSupported: false, shortText: sanitizeErrMsg(err) }],
365
365
  inactive: [],
366
366
  });
367
367
  }
@@ -371,7 +371,7 @@ async function activateWithIncludes(client, obj) {
371
371
  catch (err) {
372
372
  results.push({
373
373
  success: false,
374
- messages: [{ objDescr: "", type: "E", line: 0, href: "", forceSupported: false, shortText: toToolError(err) }],
374
+ messages: [{ objDescr: "", type: "E", line: 0, href: "", forceSupported: false, shortText: sanitizeErrMsg(err) }],
375
375
  inactive: [],
376
376
  });
377
377
  }
@@ -433,7 +433,7 @@ async function activateWithIncludes(client, obj) {
433
433
  catch (err) {
434
434
  results.push({
435
435
  success: false,
436
- messages: [{ objDescr: "", type: "E", line: 0, href: "", forceSupported: false, shortText: toToolError(err) }],
436
+ messages: [{ objDescr: "", type: "E", line: 0, href: "", forceSupported: false, shortText: sanitizeErrMsg(err) }],
437
437
  inactive: [],
438
438
  });
439
439
  }
@@ -445,7 +445,7 @@ async function activateWithIncludes(client, obj) {
445
445
  catch (err) {
446
446
  results.push({
447
447
  success: false,
448
- messages: [{ objDescr: "", type: "E", line: 0, href: "", forceSupported: false, shortText: toToolError(err) }],
448
+ messages: [{ objDescr: "", type: "E", line: 0, href: "", forceSupported: false, shortText: sanitizeErrMsg(err) }],
449
449
  inactive: [],
450
450
  });
451
451
  }
@@ -454,7 +454,7 @@ async function activateWithIncludes(client, obj) {
454
454
  export const activateTool = {
455
455
  name: "abap_activate",
456
456
  title: "Activate ABAP Object",
457
- description: "激活 ABAP 对象。ABAP 源码对象在同一次调用内串行执行语法检查、激活、active 源码核验;任一步失败即未完成。返回分阶段结果,不能用对象元数据的 active 字段覆盖失败结论。写入后必须激活才能生效。\n" +
457
+ description: "激活 ABAP 对象。ABAP 源码对象会先检查语法,激活请求返回或报错后均核对 active 源码。阶段报错但源码一致且无相关未激活对象时,确认已激活并清零该对象连续失败计数;原始诊断保留。核验不一致或不可用则保留失败,不能仅凭元数据 active 判定成功。写入后必须激活才能生效。\n" +
458
458
  "⚠️ 函数组(FUGR)激活不会自动连带激活其内部的函数模块——函数模块需单独激活:objectName=函数模块名,objectType=FUGR/FF。激活函数模块时工具会自动先激活函数组的 FORM include(把未激活的 FORM 改动固化,否则报 FORM ... does not exist)、再激活主程序(SAPL<fg>,让主程序里新增的 INCLUDE 语句先固化生效)、最后激活函数模块本身,AI 无需手动排顺序。\n" +
459
459
  "⚠️ 拆 INCLUDE 的可执行程序(主程序 PROG/P + 其 INCLUDE PROG/I)无法逐个编译——单独激活某个 INCLUDE 会对着兄弟 INCLUDE 的旧版本编译,必报 \"Field/Type unknown\"。激活时工具会自动先批量激活其全部 INCLUDE、再单独激活主程序(两步);同一对象连续激活失败 3 次会被强制拦截,需停下向用户说明。",
460
460
  write: true,
@@ -492,13 +492,15 @@ export const activateTool = {
492
492
  lines.push(`✅ ${obj["adtcore:type"]} ${obj["adtcore:name"]} 激活成功`);
493
493
  if (result.verification === "matched")
494
494
  lines.push("已核对 active 源码与本次待激活源码一致。");
495
+ if (result.recovered)
496
+ lines.push("激活阶段曾报错,但核对确认已激活且无相关未激活对象;该对象连续失败计数已清零,本次不计失败。以下保留原始阶段诊断。");
495
497
  }
496
498
  else {
497
499
  lines.push(`❌ ${result.verification === "unavailable" ? "激活未确认" : "激活失败"}(${obj["adtcore:type"]} ${obj["adtcore:name"]}):`);
498
500
  lines.push("本次操作未确认完成;对象元数据的 active 状态不能替代本次激活结果。");
499
501
  }
500
502
  for (const stage of result.stages) {
501
- lines.push(`- ${stage.name}:${stage.success ? "通过" : "未完成"}`);
503
+ lines.push(`- ${stage.name}:${stage.success ? "通过" : result.recovered ? "阶段报错(最终核验已通过)" : "未完成"}`);
502
504
  for (const m of stage.messages.slice(0, 10))
503
505
  lines.push(` [${m.type}] 行 ${m.line}: ${m.shortText}`);
504
506
  if (stage.messages.length > 10)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sapbuddy-dsh",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "SapBuddy —— SAP ABAP AI 全能助手(DeepSeek Harness 插件):41 个 SAP 工具 + 8 个 ABAP 技能 + SapBuddy preset + 连接设置页。一个包名装完。",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -77,10 +77,8 @@
77
77
  "typescript": "^5.6.0"
78
78
  },
79
79
  "scripts": {
80
- "build": "node scripts/clean.mjs && tsc && node scripts/build-client.mjs",
81
- "test": "node scripts/clean.mjs && tsc && node --test",
82
- "check": "tsc --noEmit",
83
- "prepack": "node scripts/clean.mjs && tsc && node scripts/build-client.mjs"
80
+ "test": "npm run test:activation",
81
+ "test:activation": "node --experimental-vm-modules --test --test-isolation=none test/activation.test.mjs"
84
82
  },
85
83
  "publishConfig": {
86
84
  "access": "public"