# 技能大师资料包：三步差旅报销 / Skill Master brief: three-step expense claims

这份资料描述仓库中现有的本机演示接口，可交给技能大师作为创建输入，也可用于核对生成结果。先读中文步骤或后面的 English walkthrough，再参考两种语言共用的 API 契约。接口实现见 [`mock-service/server.cjs`](https://github.com/fjb040911/ambleloft-extension-samples/blob/main/expense-workflow/mock-service/server.cjs)，宿主扩展声明见 [`extension.json`](https://github.com/fjb040911/ambleloft-extension-samples/blob/main/expense-workflow/extension.json)。

This brief describes the local demo API implemented in this repository. Give it to Skill Master as creation context and use the contract below to review the output. It is a reference implementation, not a transcript of AI generating an integration from scratch.

## 中文：从业务目标开始

在 Ambleloft 中选择一个**本地项目**，创建任务，点击「创建技能 / 扩展」并发送预填提示。将本文件放入该项目，告诉 Agent 文件路径。若项目就是本样例仓库，路径为 `expense-workflow/docs/skill-master-brief.md`。

起始提示：

> 我想让同事在聊天里登记差旅报销，分行程、费用、核对三个步骤。请用技能大师引导我，先读取项目中的 expense-workflow/docs/skill-master-brief.md，确认业务流程，再在新的目录中生成技能、声明式表单和所需扩展。serviceUrl 作为安装后的设置。用现有本机模拟服务核对结果；保留源码，分别说明文件检查、安装和实际调用的验证结果。不要覆盖现有样例。

需求中的关键规则：

- 第一步选择上海或北京，填写有效日期，确认后保存业务草稿。第二步填写人民币金额和说明。第三步明确核对后正式提交。
- 金额为两位小数字符串，范围 `0.01`–`1000000.00`。说明不能为空，最长 2,000 字符。
- 表单由宿主根据 YAML 渲染。Skill 指示模型找到并展示表单；写操作经宿主确认后交给扩展的 Node 后端调用服务。
- 返回上一步不会撤销已保存的草稿。修改行程并重新保存会产生新草稿。
- 地址属于扩展设置。演示服务不需要密码或 API Key，不要添加虚构的认证信息。

### 准备演示服务与参考产物

需要 Node.js ≥22.13，以及支持扩展的 Ambleloft。先获取案例源码（已有仓库可跳过 clone），再进入 `expense-workflow` 目录。所有安装和运行命令在该目录中执行：

```sh
git clone https://github.com/fjb040911/ambleloft-extension-samples.git
cd ambleloft-extension-samples/expense-workflow
npm ci
npm run build
npm run validate
npm test
npm run pack
npm run validate:archive
npm run service
```

`npm run service` 持续运行，默认监听 `http://127.0.0.1:47832`，账本保存在当前目录的 `expense-data/ledger.sqlite`。用 Ctrl+C 停止。可用 `EXPENSE_DB` 和 `PORT` 指定独立测试数据库与端口。

现有参考包是 `dist/expense-workflow.amble-extension`；在「设置 → 扩展 → 安装」选择它并确认信任、授权。AI 新生成的产物通过自己的创建卡检查与安装，不要把参考包安装成功视为生成产物通过验证。安装后在扩展设置填写 `serviceUrl=http://127.0.0.1:47832`。

### 可核对的结果

1. 扩展主页检查服务，得到 `connected: true`、`mode: "local-demo"`。
2. 聊天中请求报销，填写上海、有效日期、`1280.00`、`高铁680 + 酒店600`，核对并提交。应返回非空 `expenseId` 与 `status: "submitted"`。
3. 扩展主页查询记录，核对同一个 `expenseId`、金额、地区、日期与说明。独立测试账本中增加一条正式报销记录。
4. 停止服务后，查询应显示不可达；恢复服务后重新查询。写入结果不明确时使用原表单的结果核实入口，不凭报错认定没有执行，也不自动重提。

本样例仅允许 `http://127.0.0.1:<port>`。接入企业系统需要修改适配代码并验证真实 API、认证、权限和业务规则，不能只替换一个任意远端地址。演示包含草稿和报销提交，不包含审批流、付款、附件或 OCR。

## English: start with the business goal

Create a task in a **local project** in Ambleloft. Choose “Create skill / extension” and send the prepared prompt. Put this file in the project and tell the Agent its path. In this samples repository, the path is `expense-workflow/docs/skill-master-brief.md`.

Suggested prompt:

> Help colleagues claim travel expenses in chat through three steps: itinerary, expenses, and review. Use Skill Master and read expense-workflow/docs/skill-master-brief.md first. Confirm the workflow, then generate the skill, declarative form, and required extension in a new directory. Make serviceUrl a post-install setting. Verify against the existing local demo service, retain editable source, and report file checks, installation, and actual API results separately. Do not overwrite the reference sample.

The itinerary step saves a business draft after confirmation. The expense step collects a CNY amount and a non-empty explanation. The final step requires explicit review before submitting. Returning to an earlier step does not undo a saved draft; editing and saving the itinerary creates another draft. The host renders the YAML form and routes confirmed operations to the extension's Node backend.

Use Node.js ≥22.13 and an extension-capable Ambleloft host. Use the command block above to clone the samples repository and enter `expense-workflow` (skip cloning if you already have it), then install, build, validate, and start the service. The reference archive is `dist/expense-workflow.amble-extension`; install it from Settings → Extensions → Install and approve trust and permissions. A newly generated artifact needs its own checks and installation through its creation card.

Keep `npm run service` running and set the extension's `serviceUrl` to `http://127.0.0.1:47832`. The service stores its ledger in `expense-data/ledger.sqlite`; `EXPENSE_DB` and `PORT` select a separate test database and port. Stop it with Ctrl+C.

Expected results: the extension home reports `connected: true` and `mode: "local-demo"`; a reviewed claim for Shanghai and CNY `1280.00` returns an `expenseId` and `status: "submitted"`; querying records returns the same ID and business fields. Stop the service to check the unavailable state, then restart and query again. Reconcile an uncertain write using the original form before attempting another submission.

The reference adapter accepts only `http://127.0.0.1:<port>`. A real business integration needs adapter changes and validation of authentication, permissions, field mapping, and business rules. Changing the URL alone does not connect this sample to an enterprise service. Approval workflows, payments, attachments, and OCR are outside this demo.

## API contract / 接口契约

All requests and responses use JSON. There is no authentication; the service binds only to loopback and uses the fixed test tenant `local-demo-tenant`. Do not deploy it as an enterprise endpoint.

所有请求与响应使用 JSON。服务仅绑定回环地址，使用固定测试租户，没有真实认证与企业租户隔离。

| Method / 方法 | Path / 路径 | Request / 请求 | Result / 结果 |
| --- | --- | --- | --- |
| GET | `/health` | — | `{ "connected": true, "mode": "local-demo", "version": "0.1.0" }` |
| POST | `/write` | `{ "operation": "saveDraft", "submissionId": "<host-id>", "input": { "region": "shanghai", "date": "2026-11-01" } }` | `{ "draftId": "<id>" }` |
| POST | `/write` | `{ "operation": "submitExpense", "submissionId": "<host-id>", "input": { "draftId": "<id>", "amount": "1280.00", "remark": "Rail 680 + hotel 600", "confirmed": true } }` | `{ "expenseId": "<id>", "status": "submitted" }` |
| POST | `/lookup` | `{ "operation": "saveDraft" or "submitExpense", "submissionId": "<original-host-id>" }` | `{ "status": "succeeded", "result": <original result> }`, `{ "status": "notExecuted" }`, or `{ "status": "unknown" }` |
| GET | `/expenses` | — | `{ "records": [{ "id": "<expenseId>", "amount": "1280.00", "remark": "Rail 680 + hotel 600", "region": "shanghai", "date": "2026-11-01", "status": "submitted" }] }` |
| GET | `/ledger` | — | Test diagnostics: draft/expense counts, submission statuses, results, and selected trace IDs. / 测试账本计数、提交状态、结果及限定的追踪标识。 |

`/lookup` 中的 operation 二选一，表格的 `or` 表示可选值，不是可直接发送的 JSON。The `or` notation lists alternatives; choose one operation when constructing valid JSON.

- `region`: `shanghai` or `beijing`; `date`: a valid calendar date in `YYYY-MM-DD` format.
- `amount`: a string matching two decimal places, between `"0.01"` and `"1000000.00"`. `remark`: non-blank string, at most 2,000 characters. `confirmed`: `true`.
- Writes obtain `submissionId` from the host's `InvocationContext.form.submissionId`; do not generate a replacement from `requestId`, ask the user for it, or accept a model-supplied form context. Recovery reads the original `submissionId` from the recovery input.
- Same operation + same submission ID + same input returns the same success result. Reusing that key with different input yields HTTP 409. One `draftId` cannot create multiple expense records. Unknown or pending submissions return `unknown`, not proof of non-execution.
- Invalid business fields return HTTP 400. Conflicting or explicitly rejected submissions can return HTTP 409. Test mode can return HTTP 202 for a pending write, HTTP 503 for a failed lookup, or drop a response after committing. The adapter treats non-200 write responses as needing reconciliation.

写入幂等键与结果核实属于接口契约的一部分。生成对接代码时必须保留；静态类型检查不能替代服务端对金额、日期、确认和重复写入的验证。

### Drop-after-write exercise / 写入后丢响应练习

Use only the local demo service. Before submitting the final form, inject a dropped response:

仅对本机演示服务操作。在表单最终提交前执行：

```sh
curl http://127.0.0.1:47832/control -H 'content-type: application/json' -d '{"operation":"submitExpense","mode":"drop"}'
```

Submit once, then use the original form's recovery action. `/lookup` should return `succeeded` with the persisted `expenseId`; `/ledger` should show exactly one new expense. Restore normal behavior afterward:

提交一次后在原表单核实结果，应得到已保存的 `expenseId`，账本仅增加一条正式报销。随后恢复正常模式：

```sh
curl http://127.0.0.1:47832/ledger
curl http://127.0.0.1:47832/control -H 'content-type: application/json' -d '{"operation":"submitExpense","mode":"normal"}'
```

## Verification scope / 验证范围

Checked on 2026-10-22: build, package validation, archive generation and archive validation passed; both sample tests passed. The service test exercises real local HTTP and SQLite with an isolated temporary database, including invalid values, duplicates, conflicting keys, dropped responses, restart with pending state, cancellation, and failed queries. Validation reports runtime-validation warnings for field constraints; these remain subject to host and service checks.

2026-10-22 核对：构建、目录校验、打包、归档校验及 2 项样例测试通过。服务测试使用真实本机 HTTP 与隔离 SQLite 数据库。此记录未重新进行桌面 UI 人工验收，也没有进行真实模型从模糊需求生成代码或企业系统验收。

For a skill-only starting point, see the host repository's `resources/skills/skill-master/examples/travel-skill`. Its three steps collect itinerary, purpose/budget, and confirmation, then send the registration to chat. It does not call a business service. The host's `tests/creation.test.cjs` suite passed all 9 tests on the same date, including checked-snapshot installation and completing that form through the core services. These automated checks do not establish that every model-generated artifact will work without review.

纯 Skill 入门可参考宿主仓库 `resources/skills/skill-master/examples/travel-skill`：三步收集行程、事由/预算和确认，最终发送到聊天，不提交业务服务。同日宿主 `tests/creation.test.cjs` 的 9 项测试通过，其中覆盖该样例的快照检查、安装及核心服务表单流转。
