内容
项目设置指南
## 本项目介绍
一个使用Fiori Elements UI的SAP BTP CAP(Cloud Application Programming)Node.js应用程序,用于管理商业伙伴。它公开了一个OData V4服务,包含列表报告和对象页面UI,基于角色的访问控制,以及支持草稿的创建/编辑流程。
## 项目结构
```
bp-cap-fiori/
│
├── db/
│ ├── schema.cds ← 所有实体(BusinessPartners、Addresses、Roles、Tax、CodeLists)
│ ├── dev.sqlite ← 本地开发数据库文件(gitignored,自动创建 - 见下文)
│ └── data/
│ ├── com.bp.manager-BPCategories.csv ← 种子:人员 / 组织 / 群组
│ ├── com.bp.manager-BPStatuses.csv ← 种子:ACTIVE / BLOCKED
│ └── com.bp.manager-BusinessPartners.csv ← 9个示例商业伙伴
│
├── srv/
│ ├── bp-service.cds ← 服务定义、投影、@restrict、操作
│ ├── annotations.cds ← 所有Fiori Elements注释(列表报告 + 对象页面)
│ ├── mcp.cds ← MCP(AI代理)注释 - @gavdi/cap-mcp
│ ├── bp-service.js ← Node.js处理程序(BEFORE/AFTER/ON钩子)
│ └── server.js ← /health端点,用于CF健康检查
│
├── app/
│ └── bp-manager/
│ ├── xs-app.json ← Approuter路由
│ └── webapp/
│ ├── manifest.json ← Fiori应用程序描述符(列表报告 → 对象页面路由)
│ └── i18n/i18n.properties ← 所有UI文本标签
│
├── package.json ← CAP依赖 + 本地开发模拟身份验证配置
├── xs-security.json ← XSUAA范围 + 3个角色模板
├── mta.yaml ← CF部署:approuter + srv + db-deployer + 服务
├── .nvmrc ← 通过nvm固定Node LTS(仅限此项目)
└── CLAUDE.md ← 本文件
```
## 已完成步骤
### 1. 解压源文件
将`bp_cap_mcp.zip`解压到项目根目录。所有11个文件都放在根目录中。
```
schema.cds, bp-service.cds, annotations.cds, bp-service.js, server.js,
manifest.json, i18n.properties, xs-app.json, package.json, xs-security.json, mta.yaml
```
### 2. 重新组织到CAP文件夹结构
将每个文件移动到其正确的CAP约定位置:
| 从(根目录) | 到 |
|---|---|
| `schema.cds` | `db/schema.cds` |
| `bp-service.cds` | `srv/bp-service.cds` |
| `annotations.cds` | `srv/annotations.cds` |
| `bp-service.js` | `srv/bp-service.js` |
| `server.js` | `srv/server.js` |
| `manifest.json` | `app/bp-manager/webapp/manifest.json` |
| `i18n.properties` | `app/bp-manager/webapp/i18n/i18n.properties` |
| `xs-app.json` | `app/bp-manager/xs-app.json` |
| `package.json` | *(根目录 - 不变)* |
| `xs-security.json` | *(根目录 - 不变)* |
| `mta.yaml` | *(根目录 - 不变)* |
### 3. 创建CSV种子数据
创建`db/data/`,并在第一次创建`db/dev.sqlite`时,使用`cds deploy`加载三个CSV文件:
- **BPCategories**:`1=Person`, `2=Organisation`, `3=Group`
- **BPStatuses**:`ACTIVE`, `BLOCKED`
- **BusinessPartners**:9个示例记录(混合人员、组织、群组;2个已阻止)
### 4. 修复npm依赖(CDS 7 → 8)
更新`package.json`以解决弃用警告和引擎不匹配:
```json
"dependencies": {
"@sap/cds": "^8.0.0" // 曾是^7.9.0(已弃用)
},
"devDependencies": {
"@sap/cds-dk": "^8.0.0", // 曾是^7.9.0(已弃用)
"@sap/ux-specification": "~1.120.0" // 曾是^1.120.0 - 固定以避免1.144+,需要Node 22
}
```
然后运行`npm install` - 漏洞从41个降到18个。
> **注意:** 使用Node v20.9.0。升级到v20.17.0+以清除剩余的引擎警告:
> ```bash
> nvm install 20 && nvm use 20
> ```
### 5. 修复CDS 8导入语法以适应`sap.common`
在CDS 8中,`sap.common`是一个**上下文**,位于`@sap/cds/common`内部,不能使用命名空间语法导入。在`db/schema.cds`中应用了两个修复:
**导入修复:**
```cds
// 之前(CDS 7样式 - 在CDS 8中损坏)
using { cuid, managed, sap.common } from '@sap/cds/common';
// 之后(CDS 8正确样式)
using { cuid, managed, Country, sap.common.CodeList } from '@sap/cds/common';
```
**实体修复:**
```cds
// 之前
entity BPCategories : sap.common.CodeList { ... }
entity BPStatuses : sap.common.CodeList { ... }
country : sap.common.Country;
// 之后
entity BPCategories : CodeList { ... }
entity BPStatuses : CodeList { ... }
country : Country;
```
### 6. 服务器运行中
`cds watch`(通过`npm run dev`)成功启动:
```
[cds] - serving BPService { impl: 'srv/bp-service.js', path: '/api/v1/bp' }
[cds] - server listening on { url: 'http://localhost:4004' }
```
`db/dev.sqlite`中创建了所有表,并加载了所有CSV种子数据。
### 7. 持久化开发数据库
将开发数据库从`:memory:`切换到`db/dev.sqlite`下的文件,因为内存中的SQLite在每次`cds watch`重启时都会重置(每次文件保存时自动发生)- 删除任何在会话中通过UI进行的阻止/取消阻止更改。
`cds watch`不会像`:memory:`一样自动为文件数据库提供种子数据,因此`package.json`增加了一个`predev`钩子,在`npm run dev`之前运行:
```json
"predev": "test -f db/dev.sqlite || npx cds deploy",
"dev": "cds watch"
```
这会在`db/dev.sqlite`不存在时部署架构并加载CSV文件 - 一个全新的克隆可以直接使用,而现有的本地数据库(带有测试数据)在每次后续重启时都会保持不变。
`db/dev.sqlite*`被gitignore(也涵盖了SQLite创建的 `-wal`/`-shm` 配套文件),它从不被提交。要重置为原始种子数据:`rm -f db/dev.sqlite* && npm run dev`.
### 8. 修复阻止/取消阻止操作
`blockBP`/`unblockBP`最初在`bp-service.cds`的服务级别声明,CDS将其编译为*未绑定*操作(`ActionImport`s没有实体键上下文)- 对象页面按钮无法针对特定记录。移动到`BusinessPartners`的`actions { }`块中,使其编译为绑定操作(`IsBound="true"`)。
两个后续问题都已修复:
- **草稿上下文**:调用操作时,记录正在被编辑(`IsActiveEntity=false`)路由到`BusinessPartners.drafts`影子实体,该实体没有处理程序(`501`)。现在为`[BusinessPartners, BusinessPartners.drafts]`注册处理程序,始终更新持久化的活动行,并将更改镜像到草稿(包括草稿表的物化`statusCode`/`statusName`列,它们是平面副本,而不是实时视图),以便编辑模式表单立即反映新值,而后续保存不会恢复它。
- **过时的UI注释**:`annotations.cds`仍然引用操作为`BPService.EntityContainer/BusinessPartners_blockBP`(未绑定的`ActionImport`路径),在操作成为绑定操作后。UI5在客户端静默失败(`Unknown action import`)- 按钮看起来可点击,但实际上没有OData请求发出。修复为普通的绑定操作限定名称:`BPService.blockBP`。
还在两个操作上添加了`@Common.SideEffects: { TargetEntities: ['_it'] }`,以便Fiori客户端在成功调用后刷新整个BP(包括虚拟`statusCriticality`),而不是显示过时的缓存值。
### 9. 添加MCP服务器(AI代理访问)
集成了[`@gavdi/cap-mcp`](https://github.com/gavdilabs/cap-mcp-plugin),以便MCP兼容的AI代理(例如Claude Desktop)可以查询和操作商业伙伴数据。在`http://localhost:4004/mcp`处公开(在`/mcp/health`处进行健康检查),通过`cds.mcp`在`package.json`中配置,使用`auth: "inherit"` - MCP请求与OData请求使用相同的身份验证方式,并继承完全相同的`@restrict`角色检查(没有授予工具的角色永远不会看到它在`tools/list`中)。
`@mcp`注释位于一个新的专用文件中,`srv/mcp.cds`,镜像了`annotations.cds`如何隔离`@UI`问题与服务定义:
- `BusinessPartners` → 具有OData查询支持的资源模板(`filter`/`orderby`/`select`/`top`/`skip`/`expand`)以及`query`/`get`包装工具。故意**不**为创建/更新包装 - 它启用了草稿功能,具有多步骤的Fiori编辑流程,原始MCP工具调用无法安全复制。
- `BPCategories`/`BPStatuses` → 用于解析编码值的小さ静态资源。
- `blockBP`/`unblockBP` → 具有`elicit: ['confirm']`的工具,以便插件在执行状态更改操作之前需要调用客户端确认。
- `getBPSummary` → 只读工具。
- 字段级`@mcp.hint`注释解释编码值(`categoryCode` 1/2/3,`statusCode` ACTIVE/BLOCKED),LLM无法仅凭字段名称推断。
**此插件需要CDS 9和Express 5**,这级联到一个真正的升级链:`@sap/cds` 8→9,`express` 4→5,`@sap/xssec` 3→4,`@cap-js/sqlite` 1→2(1.x行`@cap-js/db-service` peer-caps`@sap/cds`在`<9`),`eslint` 8→10。两个真正的破坏性变化出现并被修复,而不是被规避:
1. **`@sap/cds-fiori@1.x`在Express 5下损坏** - 它使用一个裸`*`通配符构建Fiori预览模拟路由,而Express 5的严格`path-to-regexp`拒绝在服务器启动时(`PathError: Missing parameter name`)。通过升级到`@sap/cds-fiori@^2.3.0`修复,该版本检测Express主版本并使用`*splat`代替。
2. **持久化的`db/dev.sqlite`变得模式过时** - CDS 9在内部的`DraftAdministrativeData`草稿表中添加了一个`DraftMessages`列。在CDS 8下部署的数据库文件在每次草稿操作时都会失败,提示`no such column: DraftMessages`,直到针对当前模型重新部署:`rm -f db/dev.sqlite* && npx cds deploy`。 **在任何主要的`@sap/cds`版本升级后执行此操作。**
更新的本地`better-sqlite3`(由`@cap-js/sqlite@2.x`拉取)还需要一个当前的Node - 本地系统Node太旧,无法拥有预构建的二进制文件,而源编译失败(`node-gyp`需要Python的`distutils`,在3.12+中删除)。而不是升级系统范围的Node(会影响机器上的其他项目)或修补系统Python,安装了`nvm`并通过`.nvmrc`固定了一个Node LTS,仅限于此项目:
```bash
nvm install # 读取.nvmrc
nvm use
```
### 10. 使用Claude Code CLI验证MCP
注册本地服务器并使用真正的Claude Code进程(而不仅仅是curl)确认它端到端:
```bash
claude mcp add --transport http bp-manager http://localhost:4004/mcp \
--header "Authorization: Basic $(echo -n 'admin@test.com:x' | base64)"
claude mcp get bp-manager # 应该显示状态:✔ 已连接
```
新注册的服务器不会热加载到已经运行的Claude Code会话中 - 工具发现发生在会话开始时。使用全新的无头进程验证:
```bash
claude -p "使用bp-manager MCP服务器,获取商业伙伴摘要计数。" \
--allowedTools "mcp__bp-manager__get-bp-summary"
```
返回正确的数据(9个总数,2个已阻止,匹配种子数据)。无头运行需要 `--allowedTools` 列出确切的 `mcp__<server>__<tool>` 名称才能获得非交互式权限审批。
这也确认了`elicit: ['confirm']`安全门在真实客户端(不仅仅是curl)下工作:无头模式无法显示交互式确认对话框,因此通过 `-p` 调用 `block-business-partner` 正确 **拒绝并未执行** - 验证了实际服务器状态(仍然为 `ACTIVE`/`isBlocked: false`),而不仅仅是信任报告的结果。要通过Claude Code实际运行编写工具,请使用交互式 `claude` 会话,其中可以批准确认提示。
**完整验证结果:
| 测试 | 结果 |
|---|---|
| 注册 (`claude mcp add`) | 已连接 |
| 工具发现 | 找到所有6种工具:`cap_describe_model`, `block-business-partner`, `unblock-business-partner`, `BPService_BusinessPartners_query`, `BPService_BusinessPartners_get`, `get-bp-summary` |
| `get-bp-summary` | 正确的数据:9个总数,7个活动,2个已阻止,4个人,4个组织 |
| `BPService_BusinessPartners_query`(过滤) | 正确返回2个已知的已阻止BP(Jane Smith,Apex Innovations) |
| `elicit: ['confirm']` 安全门 | 无头模式正确拒绝在没有交互确认提示的情况下执行 `block-business-partner` - 验证实际服务器状态(仍然为 `ACTIVE`/`isBlocked: false`),而不仅仅是信任报告的结果 |
---
## 本地运行
```bash
npm install
npm run dev # 自动提供db/dev.sqlite,并在第一次运行后启动cds watch,监听http://localhost:4004
```
---
## 模拟身份验证用户(仅限本地开发)
| 用户名 | 密码 | 角色 | 权限 |
|---|---|---|---|
| `viewer@test.com` | *(任意)* | `BP_VIEWER` | 只读 |
| `processor@test.com` | *(任意)* | `BP_PROCESSOR` | 创建 / 更新 / 阻止 |
| `admin@test.com` | *(任意)* | `BP_ADMIN` | 完全访问权限,包括取消阻止 |
当浏览器显示基本身份验证弹出窗口时,输入上述用户名和任意密码。
---
## 关键服务端点
| URL | 描述 |
|---|---|
| `http://localhost:4004` | CAP欢迎页面 |
| `http://localhost:4004/api/v1/bp` | OData服务根 |
| `http://localhost:4004/api/v1/bp/$metadata` | OData元数据 |
| `http://localhost:4004/api/v1/bp/BusinessPartners` | 商业伙伴列表 |
| `http://localhost:4004/api/v1/bp/getBPSummary()` | 聚合摘要函数 |
| `http://localhost:4004/health` | 健康检查(CF) |
| `http://localhost:4004/mcp` | MCP服务器端点(AI代理访问) |
| `http://localhost:4004/mcp/health` | MCP健康检查 |
### 快速curl测试
```bash
# 列出所有商业伙伴
curl -u "admin@test.com:x" http://localhost:4004/api/v1/bp/BusinessPartners
# 摘要计数
curl -u "admin@test.com:x" "http://localhost:4004/api/v1/bp/getBPSummary()"
# 阻止BP - 草稿实体需要复合键incl. IsActiveEntity
curl -u "processor@test.com:x" -X POST \
"http://localhost:4004/api/v1/bp/BusinessPartners(ID={ID},IsActiveEntity=true)/BPService.blockBP" \
-H "Content-Type: application/json" -d "{}"
# 取消阻止BP(仅限admin)curl -u "admin@test.com:x" -X POST \
"http://localhost:4004/api/v1/bp/BusinessPartners(ID={ID},IsActiveEntity=true)/BPService.unblockBP" \
-H "Content-Type: application/json" -d "{}"
```
`blockBP`/`unblockBP`是绑定操作 `BusinessPartners`(在`bp-service.cds`的实体`actions { }`块中声明,而不是在服务级别)- 在`UI.Identification`中使用限定名称 `BPService.blockBP`,而不是 `EntityContainer/...` ActionImport路径。它们也适用于打开的草稿(`IsActiveEntity=false`),例如,从编辑模式下的对象页面:处理程序始终更新持久化的活动行,并将更改镜像到草稿,因此状态/阻止立即在屏幕上反映,随后的保存不会恢复它。