sapbuddy-dsh 0.0.8 → 0.0.10

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
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/sapbuddy-dsh)](https://www.npmjs.com/package/sapbuddy-dsh) [GitHub](https://github.com/gxx950224/sapbuddy-dsh)
4
4
 
5
- [SapBuddy](https://github.com/gxx950224/sapbuddy) 的 DeepSeek Harness 插件,提供 41 个 SAP ABAP 工具、8 个技能、SapBuddy agent preset 和 SAP 连接设置页。
5
+ [SapBuddy](https://github.com/gxx950224/sapbuddy) 的 DeepSeek Harness 插件,提供 41 个 SAP ABAP 工具、10 个技能、SapBuddy agent preset 和 SAP 连接设置页。
6
6
 
7
7
  ## 安装
8
8
 
@@ -10,13 +10,13 @@
10
10
  在 DSH 的插件管理页添加:
11
11
 
12
12
  ```text
13
- sapbuddy-dsh@0.0.8
13
+ sapbuddy-dsh@0.0.10
14
14
  ```
15
15
 
16
16
  也可以通过 DSH 命令行安装:
17
17
 
18
18
  ```bash
19
- dsh plugin --profile desktop add sapbuddy-dsh@0.0.8
19
+ dsh plugin --profile desktop add sapbuddy-dsh@0.0.10
20
20
  ```
21
21
 
22
22
  更新已安装插件后,完整退出并重新启动 DSH,再新建会话选择 **SapBuddy**。
@@ -34,16 +34,16 @@ dsh plugin --profile desktop add sapbuddy-dsh@0.0.8
34
34
 
35
35
  ## 连接配置与排查
36
36
 
37
- 地址必须写全协议,协议要和端口一致:
37
+ 在 **设置 → SAP 连接** 中填写主机地址时,必须带协议,且协议与端口一致:
38
38
 
39
39
  | 端口 | 协议 | 示例 |
40
40
  |---|---|---|
41
- | ICM HTTP(常见 8000) | `http://` | `http://192.168.111.85:8000` |
42
- | ICM HTTPS(常见 44300) | `https://` | `https://10.8.202.35:44300` |
41
+ | ICM HTTP(常见 8000) | `http://` | `http://<host>:8000` |
42
+ | ICM HTTPS(常见 44300) | `https://` | `https://<host>:44300` |
43
43
 
44
- 把 HTTP 端口写成 `https://`(或反过来)会在 TLS 阶段直接失败。客户端号就是登录用的 `sap-client`,必须与要登录的客户端一致——系统是 300 却填 200 会直接返回 401 认证失败;登录语言不确定时填 `EN`。内部系统若是自签名证书,编辑连接时保持「允许自签名证书」勾选。
44
+ 协议写错(例如 HTTP 端口却用 `https://`)会导致 TLS/连接失败。客户端号即登录用的 `sap-client`,必须与目标客户端一致,否则会返回 401。登录语言不确定时建议填 `EN`。内网自签名证书可勾选「允许自签名证书」。
45
45
 
46
- 设置页的 **测试** 按钮与真实 SAP 工具走同一套协议/客户端号/语言(早期版本把探活写死成 https 且只发 `X-sap-login-client` 头,会出现"账号密码都对却报 401"或"http 端口永远探测失败"的假阴性)。测试返回 401 时按提示核对客户端号、账号状态与登录语言;403/404 多为 ADT 服务未激活或用户无权限。
46
+ 设置页的 **测试** 与真实 SAP 工具使用同一套协议、客户端号与语言。返回 401 时核对客户端号、账号与语言;403/404 多为 ADT 未开通或权限不足。
47
47
 
48
48
  ## 连接切换与会话自愈
49
49
 
@@ -57,7 +57,7 @@ dsh plugin --profile desktop add sapbuddy-dsh@0.0.8
57
57
 
58
58
  工具涵盖对象搜索与读码、代码导航、语法诊断与激活、ATC 与单元测试、SQL 查询、DDIC、文本池、屏幕文本、消息类、函数模块接口和传输请求。
59
59
 
60
- 技能包括 `abap-code-review`、`abap-explain`、`abap-templates`、`abap-wiki`、`clean-abap`、`dev-write`、`sap-abap-skills`、`translate-workflow`。
60
+ 技能包括 `abap-code-review`、`abap-explain`、`abap-templates`、`abap-wiki`、`clean-abap`、`dev-write`、`sap-abap-skills`、`translate-workflow`、`sap-fs`(四类 FS:MD + Claude 风 HTML)、`grilling`(经 sap-fs 加载的追问澄清)。
61
61
 
62
62
  ## 配置和写入保护
63
63
 
@@ -74,7 +74,7 @@ dsh plugin --profile desktop add sapbuddy-dsh@0.0.8
74
74
  从 GitHub 克隆仓库后,在 `packages/sapbuddy-dsh` 下运行激活回归测试(需要 Node.js 24+,无需连接 SAP):
75
75
 
76
76
  ```bash
77
- npm test # = test:activation + test:session + test:store + test:probe + test:route + test:bundle
77
+ npm test # = test:activation + test:session + test:store + test:probe + test:route + test:bundle + test:sap-fs
78
78
  npm pack --dry-run --ignore-scripts
79
79
  ```
80
80
 
@@ -32,7 +32,8 @@ SapBuddy — SAP ABAP AI 全能助手,面向**开发顾问与业务顾问**。
32
32
  - **工具硬化**:`create_object_programmatically` 缺包名/描述会被拦截;传 $TMP 需用户单独确认(不进传输请求);`replace_string` 无未释放请求时自动建请求——这些无需 LLM 自觉,工具强制执行。
33
33
  6c. **连接切换后先重确认(强制)**:用户切换/修改 SAP 连接后,**必须先调用 `get_connected_systems` 确认当前连接,再使用其他 SAP 工具**。工具层强制拦截:未确认前其他 SAP 工具一律失败(返回"连接配置已变更,先调用 get_connected_systems")。看到该拦截错误时,立即调用 `get_connected_systems`,再重试被拦的操作。
34
34
  7. **写操作谨慎**:写工具(创建/编辑/激活)需要服务器 `security.readOnly=false`;生产系统只读。
35
- 8. **按技能加载(强制)**:写代码/写 SAP 对象(函数模块/表结构/函数组 include/文本元素等)先遵循 `dev-write` 技能(编码规范、写入规范);批量翻译(Excel 清单→SAP)先遵循 `translate-workflow` 技能(先 list 后 set、匹配不上报告、**禁止猜 key 写入**)。技能按话题自动加载,做对应任务时**必须遵守**。
35
+ 8. **按技能加载(强制)**:写代码/写 SAP 对象(函数模块/表结构/函数组 include/文本元素等)先遵循 `dev-write` 技能(编码规范、写入规范);批量翻译(Excel 清单→SAP)先遵循 `translate-workflow` 技能(先 list 后 set、匹配不上报告、**禁止猜 key 写入**)。技能按话题自动加载,做对应任务时**必须遵守**;写 FS / 功能设计书时遵循 `sap-fs` 技能(意图不明先问,见铁律 9)。
36
+ 9. **FS 意图门禁(强制)**:仅当用户**明确**要写/改 FS、功能设计书、接口/报表/增强**规格文档**(或明确说按 sap-fs 出可交付文档)时,才加载并执行 `sap-fs`。日常问答、配置怎么配、表/事务码解释、程序解释/审查、写 ABAP 代码 → **禁止**触发 sap-fs(写码→`dev-write`;审查→`abap-code-review`;解释逻辑→`abap-explain` / `abap-wiki`)。用户只抛业务描述或方案讨论、**未明确**要出 FS → **先问一句**是否要按 sap-fs 产出 `FS.md` + `FS.html`(接口另附 JSON);未确认前只澄清/回答,**不**套 FS 模板、不落盘 `output/fs/`。用户确认后才 Skill 加载 `sap-fs`;追问业务决策时再按 sap-fs 流程加载 `grilling`。
36
37
 
37
38
  ## 防死循环与止损(软约束,靠自觉;能代码强制的已由工具层兜底)
38
39
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sapbuddy-dsh",
3
- "version": "0.0.8",
4
- "description": "SapBuddy —— SAP ABAP AI 全能助手(DeepSeek Harness 插件):41 个 SAP 工具 + 8 个 ABAP 技能 + SapBuddy preset + 连接设置页。一个包名装完。",
3
+ "version": "0.0.10",
4
+ "description": "SapBuddy —— SAP ABAP AI 全能助手(DeepSeek Harness 插件):41 个 SAP 工具 + 10 个 ABAP 技能 + SapBuddy preset + 连接设置页。一个包名装完。",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "exports": {
@@ -77,13 +77,14 @@
77
77
  "typescript": "^5.6.0"
78
78
  },
79
79
  "scripts": {
80
- "test": "npm run test:activation && npm run test:session && npm run test:store && npm run test:probe && npm run test:route && npm run test:bundle",
80
+ "test": "npm run test:activation && npm run test:session && npm run test:store && npm run test:probe && npm run test:route && npm run test:bundle && npm run test:sap-fs",
81
81
  "test:activation": "node --experimental-vm-modules --test --test-isolation=none test/activation.test.mjs",
82
82
  "test:session": "node --experimental-vm-modules --test --test-isolation=none test/session-recovery.test.mjs",
83
83
  "test:store": "node --test --test-isolation=none test/connections-store.test.mjs",
84
84
  "test:probe": "node --test --test-isolation=none test/connection-probe.test.mjs",
85
85
  "test:route": "node --test --test-isolation=none test/connection-test-route.test.mjs",
86
- "test:bundle": "node --test --test-isolation=none test/client-bundle.test.mjs"
86
+ "test:bundle": "node --test --test-isolation=none test/client-bundle.test.mjs",
87
+ "test:sap-fs": "node --test --test-isolation=none test/sap-fs/loading.test.mjs"
87
88
  },
88
89
  "publishConfig": {
89
90
  "access": "public"
@@ -0,0 +1,43 @@
1
+ ---
2
+ name: grilling
3
+ description: >-
4
+ Grill the user relentlessly about a plan, decision, or idea. Use when the user
5
+ wants to stress-test their thinking, or uses any 'grill' trigger phrases
6
+ (including "grill me"). Also used by sap-fs when clarifying FS business decisions;
7
+ not the entry skill for FS intent (load via sap-fs after user confirms FS deliverable).
8
+ ---
9
+
10
+ <!--
11
+ Third-party: mattpocock/skills (MIT, Copyright 2026 Matt Pocock)
12
+ Upstream: https://github.com/mattpocock/skills
13
+ skills/productivity/grilling/SKILL.md
14
+ Pinned blob: df69d9936880cd3314c28858c6c52f48db23b856 (fetched 2026-10-28)
15
+ Adaptation: bundled under sapbuddy-dsh/skills for DSH discovery; description notes sap-fs;
16
+ "grill me" triggers are covered here directly (no separate grill-me alias).
17
+ -->
18
+
19
+ Interview the user relentlessly until you reach a shared understanding. Map this as a **design tree**: every decision branches into the decisions that hang off it.
20
+
21
+ Work the tree in **rounds**. The **frontier** is every decision whose prerequisites are already settled: the questions you can ask _now_ without guessing at answers you haven't heard yet. Ask the whole frontier in one round: number each question and give your recommended answer. Then wait for the user's answers before the next round.
22
+
23
+ Format a round like so:
24
+
25
+ ```
26
+ ❓ **Q1** - **<question title>**: <question body, might be multiple paragraphs, including multiple choices>
27
+
28
+ ➡️ <your recommended answer>
29
+
30
+ ---
31
+
32
+ ❓ **Q2** - **<question title>**: <question body, might be multiple paragraphs, including multiple choices>
33
+
34
+ ➡️ <your recommended answer>
35
+ ```
36
+
37
+ Word each question so "yes" accepts your recommended answer.
38
+
39
+ Each round the user answers reshapes the tree: settled decisions push the frontier outward and unblock questions that depended on them. Recompute the frontier and ask the next round. A question whose answer depends on another question still open in this round belongs to a _later_ round, not this one.
40
+
41
+ Finding _facts_ is your job, never the user's. When a frontier question needs a fact from the environment (filesystem, tools, etc.), dispatch a sub-agent to find it; don't ask the user for anything you could look up yourself. Don't block on it: a running exploration is an unsettled prerequisite, so only the questions downstream of it wait for the sub-agent to report; ask the rest of the frontier now. The _decisions_ are the user's: put each to them and wait.
42
+
43
+ The session is done when the frontier is empty: every branch of the design tree visited, nothing left silently assumed. Do not act on it until the user confirms you have reached a shared understanding.
@@ -0,0 +1,76 @@
1
+ ---
2
+ name: sap-fs
3
+ description: >-
4
+ 编写 SAP 功能规格书(FS / 功能设计书)。仅在用户明确要求写/改 FS、功能设计书、
5
+ 接口设计、报表设计、增强设计,或明确说按 sap-fs 出可交付规格文档时触发。
6
+ 日常问答、配置咨询、解释程序、代码审查、写 ABAP 不触发;意图不明时先问是否出 FS。
7
+ 确认后:识别需求类型 → 系统取证 → 用 grilling 追问业务决策 → 按模板生成 FS。
8
+ 触发关键词:写FS、功能规格、功能设计书、接口设计、报表设计、增强设计、出一份FS。
9
+ agent_created: true
10
+ disable: false
11
+ ---
12
+
13
+ # sap-fs — 写 SAP 功能规格书
14
+
15
+ > 草稿结构(Codex 第一版目录)。模板在 `assets/templates/`,过程规范在 `references/`。
16
+ > 追问逻辑不内嵌:加载 `grilling`。上游许可与钉选说明见 `grilling/SKILL.md` 顶部注释。
17
+
18
+ ## 何时使用
19
+
20
+ - 用户**明确**要写 / 改 FS、功能设计书、接口 / 报表 / 增强**规格文档**,或明确说按 sap-fs 出可交付文档。
21
+ - 用户确认「要出 FS」之后(见 SYSTEM 铁律 FS 意图门禁)。
22
+
23
+ 不触发:日常问答、配置怎么配、表/事务码解释、程序解释(`abap-explain` / `abap-wiki`)、代码审查(`abap-code-review`)、写 ABAP(`dev-write` / `abap-templates`)。
24
+ 意图不明(只抛业务描述 / 方案讨论、未说要出 FS):**先问一句**是否要按 sap-fs 产出 `FS.md` + `FS.html`(接口另附 JSON);未确认前不套模板、不落盘 `output/fs/`。
25
+
26
+ ## 资源地图(按需加载,勿一次全读)
27
+
28
+ | 时机 | 读什么 |
29
+ | --- | --- |
30
+ | 触发后立刻 | 本文件 + `references/fs-contract.md` |
31
+ | 需要连系统 | `references/system-verification.md` |
32
+ | 需要追问 | `references/grilling-integration.md`,并 **Skill 加载 `grilling`**(含 “grill me” 类触发) |
33
+ | 已判定类型 | `references/types/<type>.md` + `assets/templates/fs-base.md` + `assets/templates/<type>.md` |
34
+ | 对齐详略 | `references/example-enhancement.md`(仅增强类样例) |
35
+ | 交付前 | `references/review-checklist.md` |
36
+ | 出归档 HTML / 接口 JSON | `references/archive-html.md` + `assets/templates/fs-archive.html` |
37
+
38
+ 类型文件名:`interface` / `query-report` / `functional-report` / `enhancement`。
39
+
40
+ ## 工作流程
41
+
42
+ 1. **判类型**(混合取主导;ALV 按钮会改业务状态 → 功能报表)。读对应 `types/` 与模板。
43
+ 2. **整理已知**:用户已说内容分到需求 / 现状 / 规则,先不提问。
44
+ 3. **取证**:按 `system-verification.md` 只查能查的。
45
+ 4. **追问**:按 `grilling-integration.md` 加载 `grilling`,只问业务决策。
46
+ 5. **生成**:拼装 `fs-base` + 类型专项模板;规则 / 验收 / 待确认 / 附录按 `fs-contract.md`。
47
+ 6. **自检**:`review-checklist.md` 通过后再交付。
48
+
49
+ ## 拼装
50
+
51
+ ```
52
+ assets/templates/fs-base.md # 第1、2、4、5、6、7节 + 附录A
53
+ + assets/templates/<type>.md # 第3节
54
+ + 头部状态行
55
+ = 一份完整 FS
56
+ ```
57
+
58
+ 默认落盘目录(用户未指定时):`output/fs/<需求名>/`(允许的 `output` 子树);详见下方交付物。
59
+
60
+ ## 交付物(Markdown + HTML,接口另附 JSON)
61
+
62
+ 同一需求目录默认:`output/fs/<需求名>/`
63
+
64
+ 1. `FS.md` — 协作 / AI 主规格。
65
+ 2. `FS.html` — 企业归档:套 `assets/templates/fs-archive.html`,规则见 `references/archive-html.md`。
66
+ 3. **禁止 logo**(徽标、水印、品牌图都不放)。
67
+ 4. **接口类**另出:正文 3.6 样例 JSON + 同目录 `io-request.json` / `io-response.json`(及必要的回调文件)。
68
+
69
+ 先定稿 Markdown,再渲染 HTML / 写出 JSON;自检通过后一并交付路径。
70
+
71
+ ## 硬边界
72
+
73
+ - 本技能流程**只读 SAP**;FS 确认 ≠ 写入许可。
74
+ - 正文只写事实与决定;训诫与生成纪律留在技能文件,不进 FS。
75
+ - 不虚构表名 / 字段 / 增强点 / 签署与测试结果。
76
+ - HTML 归档不使用任何 logo。
@@ -0,0 +1,52 @@
1
+ <!-- 本片段插入骨架第3节。增强只描述"在哪个时点、改变哪段行为、其余保持原样"。
2
+ 第2节写整体现状;这里只写本次涉及的动作。 -->
3
+
4
+ ## 3. 增强设计
5
+
6
+ ### 3.1 改变的行为
7
+
8
+ | 动作 | 现状 | 目标 | 保持不变 |
9
+ | --- | --- | --- | --- |
10
+ | <保存 / 过账 / 审批> | <现在怎么处理> | <改变后怎么处理> | <仍须沿用的标准行为> |
11
+
12
+ **实施方式**:<标准配置能否满足;选增强的理由。未验证的判断标 Q。>
13
+
14
+ ### 3.2 触发点
15
+
16
+ | 入口 | 时点 | 触发前状态 | 取得的数据 |
17
+ | --- | --- | --- | --- |
18
+ | <事务码 / 接口 / 后台> | <校验阶段 / 保存前 / 提交后> | <订单已保存 / 未保存> | <当前输入值 / 数据库旧值> |
19
+
20
+ - **覆盖入口**:<前台、后台、接口分别是否覆盖;未确认的标 Q。>
21
+ - **新旧值**:<规则用的是用户当前输入,还是已落库的值;修改后是否重新校验。>
22
+ - **未保存数据**:<字段尚未落库时以什么为准。>
23
+ - **派生值**:<标准逻辑或其它增强是否也会改同一字段;先后关系。>
24
+
25
+ ### 3.3 适用与排除
26
+
27
+ | 项目 | 定义 |
28
+ | --- | --- |
29
+ | 命中条件 | <组织、单据类型、业务状态、角色> |
30
+ | 排除条件 | <必须绕过增强的场景> |
31
+ | 与其它规则的优先级 | <同时命中时先后;先报哪条> |
32
+ | 不命中时 | <维持原有行为> |
33
+ | 既有校验先失败时 | <是否继续执行本增强;用户最终看到什么> |
34
+
35
+ ### 3.4 处理逻辑
36
+
37
+ <按顺序写:取数 → 判断 → 结果。引用 R-01 等规则编号,不在这里重复条件。>
38
+
39
+ ### 3.5 重复触发与事务
40
+
41
+ | 场景 | 期望 |
42
+ | --- | --- |
43
+ | 同一操作多次触发 | <哪些可重复,哪些副作用只能发生一次> |
44
+ | 保存后再次修改 | <哪些字段变化需要重新判断> |
45
+ | 标准动作最终失败 | <本次已产生的变化是否必须一同撤销> |
46
+ | 已调用外部系统 | <外部成功但本地失败的核对与补偿> |
47
+
48
+ ### 3.6 停用与回归
49
+
50
+ - **停用后**:<恢复原有行为;已保存数据不回滚。>
51
+ - **在途业务**:<处理中的单据、任务、外部请求如何处理。>
52
+ - **回归范围**:<既有增强、标准校验、相关模块(凭证流、库存、财务)需要复测的部分。>
@@ -0,0 +1,320 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>{{DOC_TITLE}}</title>
7
+ <style>
8
+ /* Claude Design System tokens (from open-claude-ui cds-theme) — archive sheet, no logo */
9
+ :root {
10
+ --brand: #c6613f;
11
+ --cds-clay: #d97757;
12
+ --cds-clay-light: #fbf3ef;
13
+ --cds-gray-0: #fff;
14
+ --cds-gray-10: #fcfcfb;
15
+ --cds-gray-20: #f9f9f7;
16
+ --cds-gray-40: #f3f3f0;
17
+ --cds-gray-100: #e1e0d9;
18
+ --cds-gray-200: #c3c2b7;
19
+ --cds-gray-500: #6d6b67;
20
+ --cds-gray-600: #52514e;
21
+ --cds-gray-850: #151515;
22
+ --cds-text-primary: #151515;
23
+ --cds-text-secondary: #525252;
24
+ --cds-text-muted: #8a8a8a;
25
+ --cds-border: hsla(0, 0%, 8%, 0.10);
26
+ --cds-border-strong: hsla(0, 0%, 8%, 0.20);
27
+ --cds-shadow-sm: 0 1px 2px rgba(0,0,0,.05), 0 2px 8px rgba(0,0,0,.08);
28
+ --cds-shadow-md: 0 2px 4px rgba(0,0,0,.05), 0 6px 16px rgba(0,0,0,.08);
29
+ --radius-md: 6px;
30
+ --radius-lg: 8px;
31
+ --radius-xl: 12px;
32
+ --radius-2xl: 16px;
33
+ --font: "Anthropic Sans", "Segoe UI", "PingFang SC", "Microsoft YaHei", "Noto Sans CJK SC", sans-serif;
34
+ --mono: "Anthropic Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
35
+ }
36
+ * { box-sizing: border-box; }
37
+ body {
38
+ margin: 0;
39
+ min-height: 100vh;
40
+ color: var(--cds-text-primary);
41
+ background: var(--cds-gray-20);
42
+ font: 15px/1.65 var(--font);
43
+ -webkit-font-smoothing: antialiased;
44
+ }
45
+ .sheet {
46
+ max-width: 880px;
47
+ margin: 28px auto 48px;
48
+ padding: 36px 40px 40px;
49
+ background: var(--cds-gray-0);
50
+ border: 1px solid var(--cds-border);
51
+ border-radius: var(--radius-2xl);
52
+ box-shadow: var(--cds-shadow-md);
53
+ }
54
+ .accent-bar {
55
+ height: 3px;
56
+ width: 48px;
57
+ border-radius: 999px;
58
+ background: var(--cds-clay);
59
+ margin-bottom: 18px;
60
+ }
61
+ h1 {
62
+ margin: 0 0 10px;
63
+ font-size: 1.55rem;
64
+ font-weight: 600;
65
+ letter-spacing: -0.02em;
66
+ line-height: 1.3;
67
+ }
68
+ h2 {
69
+ margin: 32px 0 12px;
70
+ padding-bottom: 8px;
71
+ font-size: 1.1rem;
72
+ font-weight: 600;
73
+ letter-spacing: -0.01em;
74
+ border-bottom: 1px solid var(--cds-border);
75
+ }
76
+ h3 {
77
+ margin: 20px 0 8px;
78
+ font-size: 0.98rem;
79
+ font-weight: 600;
80
+ color: var(--cds-text-primary);
81
+ }
82
+ h4 {
83
+ margin: 14px 0 6px;
84
+ font-size: 0.9rem;
85
+ font-weight: 600;
86
+ color: var(--cds-text-secondary);
87
+ }
88
+ p, li { margin: 6px 0; color: var(--cds-text-primary); }
89
+ .lede { color: var(--cds-text-secondary); font-size: 0.92rem; }
90
+ .status {
91
+ display: inline-flex;
92
+ align-items: center;
93
+ gap: 6px;
94
+ margin: 4px 0 18px;
95
+ padding: 4px 10px;
96
+ border-radius: 999px;
97
+ background: var(--cds-clay-light);
98
+ color: var(--brand);
99
+ border: 1px solid color-mix(in srgb, var(--cds-clay) 28%, transparent);
100
+ font-size: 0.78rem;
101
+ font-weight: 500;
102
+ }
103
+ .status::before {
104
+ content: "";
105
+ width: 6px; height: 6px;
106
+ border-radius: 50%;
107
+ background: var(--cds-clay);
108
+ }
109
+ .meta {
110
+ display: grid;
111
+ grid-template-columns: 7.5rem 1fr;
112
+ gap: 8px 14px;
113
+ margin: 0 0 8px;
114
+ padding: 14px 16px;
115
+ background: var(--cds-gray-10);
116
+ border: 1px solid var(--cds-border);
117
+ border-radius: var(--radius-xl);
118
+ font-size: 0.88rem;
119
+ }
120
+ .meta dt { color: var(--cds-text-muted); font-weight: 500; }
121
+ .meta dd { margin: 0; color: var(--cds-text-secondary); }
122
+ table {
123
+ width: 100%;
124
+ border-collapse: separate;
125
+ border-spacing: 0;
126
+ margin: 12px 0 18px;
127
+ font-size: 0.86rem;
128
+ border: 1px solid var(--cds-border);
129
+ border-radius: var(--radius-xl);
130
+ overflow: hidden;
131
+ background: var(--cds-gray-0);
132
+ }
133
+ th, td {
134
+ padding: 9px 11px;
135
+ text-align: left;
136
+ vertical-align: top;
137
+ border-bottom: 1px solid var(--cds-border);
138
+ }
139
+ tr:last-child td { border-bottom: none; }
140
+ th {
141
+ background: var(--cds-gray-40);
142
+ color: var(--cds-text-secondary);
143
+ font-weight: 600;
144
+ }
145
+ tbody tr:nth-child(even) td { background: var(--cds-gray-10); }
146
+ .card {
147
+ margin: 12px 0;
148
+ padding: 12px 14px;
149
+ border: 1px solid var(--cds-border);
150
+ border-radius: var(--radius-xl);
151
+ background: var(--cds-gray-10);
152
+ box-shadow: var(--cds-shadow-sm);
153
+ }
154
+ .card > :first-child { margin-top: 0; }
155
+ pre, code { font-family: var(--mono); font-size: 0.8rem; }
156
+ code {
157
+ padding: 1px 5px;
158
+ border-radius: var(--radius-md);
159
+ background: var(--cds-gray-40);
160
+ color: var(--cds-gray-850);
161
+ }
162
+ .code-block {
163
+ position: relative;
164
+ margin: 10px 0 16px;
165
+ }
166
+ pre {
167
+ margin: 10px 0 16px;
168
+ padding: 14px 16px;
169
+ background: #1e1e1d;
170
+ color: #f5f5f5;
171
+ border-radius: var(--radius-xl);
172
+ border: 1px solid var(--cds-gray-850);
173
+ overflow: auto;
174
+ white-space: pre-wrap;
175
+ word-break: break-word;
176
+ }
177
+ .code-block pre {
178
+ margin: 0;
179
+ padding-top: 40px;
180
+ }
181
+ pre code { background: transparent; color: inherit; padding: 0; }
182
+ .copy-btn {
183
+ position: absolute;
184
+ top: 8px;
185
+ right: 8px;
186
+ z-index: 1;
187
+ padding: 4px 10px;
188
+ font: 500 0.72rem/1.4 var(--font);
189
+ color: var(--cds-gray-600);
190
+ background: var(--cds-gray-10);
191
+ border: 1px solid var(--cds-border-strong);
192
+ border-radius: var(--radius-md);
193
+ cursor: pointer;
194
+ box-shadow: var(--cds-shadow-sm);
195
+ }
196
+ .copy-btn:hover {
197
+ color: var(--brand);
198
+ border-color: color-mix(in srgb, var(--cds-clay) 45%, var(--cds-border-strong));
199
+ background: var(--cds-clay-light);
200
+ }
201
+ .copy-btn.copied {
202
+ color: var(--brand);
203
+ border-color: color-mix(in srgb, var(--cds-clay) 45%, transparent);
204
+ background: var(--cds-clay-light);
205
+ }
206
+ ul { padding-left: 1.2rem; }
207
+ footer {
208
+ margin-top: 36px;
209
+ padding-top: 14px;
210
+ border-top: 1px solid var(--cds-border);
211
+ color: var(--cds-text-muted);
212
+ font-size: 0.75rem;
213
+ }
214
+ /* Explicitly no logo / brand mark images */
215
+ @media print {
216
+ body { background: #fff; }
217
+ .sheet {
218
+ margin: 0; max-width: none;
219
+ border: none; box-shadow: none; border-radius: 0; padding: 0;
220
+ }
221
+ h2, .card, table, pre { break-inside: avoid; }
222
+ }
223
+ </style>
224
+ </head>
225
+ <body>
226
+ <article class="sheet">
227
+ <div class="accent-bar" aria-hidden="true"></div>
228
+ <header>
229
+ <h1>{{DOC_TITLE}}</h1>
230
+ <p class="status">{{STATUS_LINE}}</p>
231
+ <dl class="meta">
232
+ <dt>项目 / 模块</dt><dd>{{PROJECT_MODULE}}</dd>
233
+ <dt>系统与客户端</dt><dd>{{SYSTEM_CLIENT}}</dd>
234
+ <dt>需求提出人</dt><dd>{{REQUESTER}}</dd>
235
+ <dt>类型</dt><dd>{{FS_TYPE}}</dd>
236
+ <dt>更新</dt><dd>{{UPDATED}}</dd>
237
+ </dl>
238
+ </header>
239
+ <div class="body">
240
+ {{BODY_HTML}}
241
+ </div>
242
+ <footer>
243
+ Generated by SapBuddy
244
+ </footer>
245
+ </article>
246
+ <script>
247
+ (function () {
248
+ function getCodeText(pre) {
249
+ var code = pre.querySelector("code");
250
+ return (code || pre).textContent;
251
+ }
252
+ function wrapPre(pre) {
253
+ if (pre.closest(".code-block")) return;
254
+ var wrap = document.createElement("div");
255
+ wrap.className = "code-block";
256
+ pre.parentNode.insertBefore(wrap, pre);
257
+ wrap.appendChild(pre);
258
+ var btn = document.createElement("button");
259
+ btn.type = "button";
260
+ btn.className = "copy-btn";
261
+ btn.setAttribute("aria-label", "复制代码");
262
+ btn.textContent = "复制";
263
+ btn.addEventListener("click", function () {
264
+ var text = getCodeText(pre);
265
+ function ok() {
266
+ btn.textContent = "已复制";
267
+ btn.classList.add("copied");
268
+ setTimeout(function () {
269
+ btn.textContent = "复制";
270
+ btn.classList.remove("copied");
271
+ }, 1500);
272
+ }
273
+ function fail() {
274
+ btn.textContent = "失败";
275
+ setTimeout(function () { btn.textContent = "复制"; }, 1500);
276
+ }
277
+ if (navigator.clipboard && navigator.clipboard.writeText) {
278
+ navigator.clipboard.writeText(text).then(ok).catch(function () {
279
+ try {
280
+ var ta = document.createElement("textarea");
281
+ ta.value = text;
282
+ ta.setAttribute("readonly", "");
283
+ ta.style.position = "fixed";
284
+ ta.style.left = "-9999px";
285
+ document.body.appendChild(ta);
286
+ ta.select();
287
+ document.execCommand("copy");
288
+ document.body.removeChild(ta);
289
+ ok();
290
+ } catch (e) { fail(); }
291
+ });
292
+ } else {
293
+ try {
294
+ var ta2 = document.createElement("textarea");
295
+ ta2.value = text;
296
+ ta2.setAttribute("readonly", "");
297
+ ta2.style.position = "fixed";
298
+ ta2.style.left = "-9999px";
299
+ document.body.appendChild(ta2);
300
+ ta2.select();
301
+ document.execCommand("copy");
302
+ document.body.removeChild(ta2);
303
+ ok();
304
+ } catch (e2) { fail(); }
305
+ }
306
+ });
307
+ wrap.appendChild(btn);
308
+ }
309
+ function init() {
310
+ document.querySelectorAll("pre").forEach(wrapPre);
311
+ }
312
+ if (document.readyState === "loading") {
313
+ document.addEventListener("DOMContentLoaded", init);
314
+ } else {
315
+ init();
316
+ }
317
+ })();
318
+ </script>
319
+ </body>
320
+ </html>