用户在地址编辑页输入:“这次放到驿站。”Agent 把文字整理成表单,用户看了一眼,随后又把收件地点改成前台。如果此前的确认按钮还能直接提交,系统到底保存的是用户看过的版本,还是后来改过的新内容?
填表场景最关键的边界,在于生成内容怎样进入业务写操作。模型可以帮助组织字段,但保存动作需要有一份明确的、经过用户核对的内容。
这一篇用 Vue 3、TypeScript 和本地 Node 模拟服务完成一个地址修改流程:生成草稿 → 编辑字段 → 预览差异 → 确认当前版本 → 保存演示地址。 确认只对那次预览的快照有效。
自然语言提取使用固定关键词分支模拟,没有调用真实模型。姓名、联系方式和地址全部是专门编写的演示占位数据,不处理真实订单。公开页面使用浏览器内存模拟服务;下载源码另可启动真实本地 HTTP 服务,两者都不连接任何商城。

把草稿与已保存地址分开
页面同时保留三份信息:当前已保存的演示地址、可编辑草稿、用户正在核对的预览快照。生成草稿只改变编辑区,保存区必须等确认成功后才变化。
输入“前台”或“驿站”会走固定模拟分支,把对应演示地点填到表单。其他说法不会被模型自动理解,也不会编造一个猜测地址。本例研究的是生成后如何确认,固定提取只用来触发流程。
字段包括演示收件人、演示联系方式和详细地点。校验规则刻意只接受以“演示”开头的姓名与地址,以及 DEMO- 加四位数字的联系方式。这不是现实地址或手机号校验器;它让读者可以完整运行示例而不用输入私人信息。
草稿生成后仍可手动改字段。普通 Vue 表单绑定只负责把输入同步到 JavaScript 状态;它并不表示用户已经同意保存。Vue 官方也明确以绑定的 JavaScript 状态作为表单的数据来源。Vue:表单输入绑定
确认对象需要带版本和快照
本例的服务端内存中保存一份草稿,每次成功同步草稿,版本递增。预览时返回本次版本、修改前后内容,以及只用于本次确认的 token:
type Address = Readonly<{
recipient: string;
phone: string;
detail: string;
}>;
type Preview = Readonly<{
version: number;
token: string;
before: Address;
after: Address;
}>;
差异区域按字段展示原值与新值,供用户检查改了什么。预览返回的是独立快照,不是跟着输入框继续变化的对象引用。这样用户确认的内容才有稳定含义。
同步草稿也要带上读取到的 baseVersion。如果其他操作已经推进版本,服务拒绝旧基线,页面提示重新核对,不能把它强行当成最新版覆盖。版本检查解决的是编辑冲突,不是账号权限。
正式 API 可以用 HTTP 409 表达请求与资源当前状态不匹配。本例为了简化错误路由,业务拒绝统一返回 400,并在错误正文说明重新预览要求;这里没有实现 409 分类。MDN:409 Conflict
先废弃旧确认,再允许修改
一个容易忽略的地方是:客户端取消勾选或隐藏按钮,服务端旧 token 可能仍然有效。只要旧请求能够到达接口,前端按钮状态就不足以约束保存。
因此本例在生成预览后锁定字段,提供明确的“修改此草稿”入口。用户选择修改时,先请求服务废弃当前确认;收到成功后,页面才重新开放编辑,并清除差异预览。重新生成草稿也经过这个入口。
如果废弃请求失败,页面保持锁定并提示重试,不能假装已经安全进入编辑。这样修改动作与服务端确认资格之间有一个可核对的交接点,而不是只更新本地变量。
服务端也不会凭空知道浏览器里所有未提交文本。本例通过受控编辑入口同步废弃确认;如果用户绕过页面直接发送请求,服务端仍只按自己保存的草稿版本和预览快照判断。它不能把一个旧确认解释成对任意新字段的授权。
预览期间与保存期间,表单的可编辑状态应保持一致。尤其确认请求已发出时,不要同时允许修改草稿、再次生成和重复点击保存,给用户制造“到底保存哪份”的歧义。
保存接口只接受确认标识
确认请求不再携带整份可任意替换的地址。它提交版本与 token,服务端取出此前保存的预览,并完成以下核对:
请求版本 = 当前草稿版本 = 预览版本
请求 token = 当前待确认 token
全部通过后,保存的来源是服务端预览中的 after 快照。即使调用方在确认请求里附带另一份字段,也不能让它成为本次保存内容。
下面摘出 src/service.ts 中通过版本与 token 校验后的分支。pending 是服务端持有的预览,失败开关只是演示夹具:
const accepted = pending;
pending = undefined; // 先消费本次确认
if (x.failSave === true) {
throw new Error('模拟保存失败,未修改保存地址;请重新预览。');
}
saved = clone(accepted.after);
saveCount += 1;
return state();
服务在模拟保存前消费 token。同一 token 再次提交会被拒绝,保存计数不会重复增加。本例故意选择“拒绝已消费确认”的简单策略,没有实现跨进程、跨重启的完整幂等业务协议。
模拟保存失败也会消费 token,但不改变已保存地址。用户需要重新预览并重新确认,不能沿用旧按钮假装重试成功。这里的失败是确定发生在修改保存值之前的演示故障,不包括“后台成功、响应丢失”那种结果未知情形。
前端只在有效保存响应返回后更新保存区域。请求正在发送、按钮变灰、连接关闭,都不能单独作为保存成功的证据。
同一规则,区分离线与 HTTP 两条路径
直接打开公开演示,服务对象存在当前页面的 JavaScript 内存里。同步草稿、预览、废弃确认和保存都只调用本地模拟方法,不产生业务接口网络请求;刷新页面会丢失这份状态。
下载包中的 Node 服务复用同一份业务规则,通过 HTTP 提供草稿与确认接口,状态放在服务进程内存。它证明的是浏览器与本地接口能够走完整个流程,服务重启仍会清空记录。它没有数据库,也没有对接物流或订单。
Node 的底层 HTTP API 不会自动把请求体变成经过验证的业务对象。示例显式读取有大小上限的 JSON,再检查字段与版本;静态文件采用固定白名单,只监听回环地址。Node:HTTP API
公开 GitHub Pages 不运行 Node。HTTP 模式只在本地服务页面启用,公开页面也不会尝试访问读者电脑上的端口。运行模式需要在界面上清楚标注,否则“离线通过”和“HTTP 通过”很容易被写成同一份证据。
失败测试要直接碰服务边界
只验证按钮灰掉不够。核心用例还要拿着旧版本与旧 token 直接调用服务,确认服务本身也拒绝,而不只是浏览器阻止点击。
| 场景 | 应满足的约束 |
|---|---|
| 生成草稿但未确认 | 已保存地址和保存计数不变 |
| 当前预览后明确确认 | 只保存该快照,计数增加一次 |
| 预览后请求修改 | 废弃旧 token,再开放编辑 |
| 修改并同步新版本,再提交旧确认 | 服务拒绝,不能保存新内容 |
| 相同 token 再次提交 | 不增加第二次保存 |
| 缺字段或演示格式不合法 | 拒绝预览或写入,不补造缺失值 |
| 模拟保存失败 | 保留已保存地址,不显示成功 |
| 废弃确认失败 | 保持锁定,不直接进入编辑 |
异步页面也需要处理过期响应。开始预览时记录本地修订标识,只接受仍对应当前操作的返回。页面卸载后停止更新;请求忙碌期间限制会互相冲突的动作。服务端版本保护与页面响应保护分别守住不同边界。
HTTP 集成测试还应检查路径白名单、请求体格式和大小限制。离线断言可以覆盖业务规则,只有实际启动服务并发送 HTTP 请求,才能对本地传输作出验证结论。
运行示例并核对保存记录
打开地址草稿演示 · 下载 Vue / TypeScript 与 Node 服务源码
解压后直接打开 index.html 即可使用离线模式。Vue 已打包进本地文件,不需要模型密钥或 CDN。输入演示提示,生成草稿,预览差异,最后点击确认;在确认前观察已保存区域,内容应保持不变。
验证本地 HTTP 时,在解压目录按 README 启动服务。用浏览器打开 http://127.0.0.1:8089/,页面会自动进入本地 HTTP 模式;不能把普通静态预览服务器当成确认后端。
npm run serve
修改源码时,按锁文件安装依赖,并执行类型检查、核心测试、构建和 HTTP 集成验证:
npm ci
npm run typecheck
npm test
npm run build
npm run test:http
本次类型检查、16 组核心测试及 12 个本地 HTTP 断言通过。浏览器分别操作离线和本地 HTTP 的生成、预览、修改、保存失败与成功路径,并检查电脑、手机页面。核心结果随源码附带,浏览器证据单独保存在当日工作区。所有验证均使用固定模拟输出和演示地址,没有验证真实模型提取能力,也没有修改真实业务数据。
接到真实地址业务时补上服务端约束
真实接入时,可以把固定提取分支替换为模型结构化输出,但模型仍只能提出草稿。用户手动输入、模型建议和已保存信息应有清楚的来源,缺少字段交回用户补充,不能靠模型猜手机号或收件人。
版本和 token 需要绑定账号、业务对象及有效期;保存时还要重新检查权限与订单是否允许修改。数据库中的版本核对、确认消费和写入,需要合适的事务边界。本例的单进程内存逻辑不能直接当作生产并发保证。
网络超时后也不能简单重新生成 token 并再次保存,首先应核对上一次操作的结果。后续文章会单独讨论幂等与结果未知;本篇只完成确定成功或确定失败的最小确认流程。
这个案例保留了一条明确的业务约束:Agent 可以帮用户填,用户确认的是一份具体内容;内容变了,就重新预览和确认。把这条约束落实到保存接口,前端的确认步骤才有实际作用。