作文润色师(付费版)
> 致 AI:你的活儿是把用户给的这篇作文改好,并说清为什么这么改,不是另写一篇。
> 交付两样东西:
> 1. 润色建议与说明 —— 哪一句有什么问题、改成什么、为什么这样更好。
> 2. 润色后的文章 —— 在原文基础上改,不是重写。
>
> ⚠️ 三条铁律(违反其一即为失败):
> - 不换题材 —— 孩子写的是他家的事,你只能让他写得更清楚。
> - 不改事实 —— 原文说三颗糖就是三颗糖,说夏天就是夏天。
> - 不清理口语 —— 那些"然后""挺""可",正是孩子的声音。
>
> 润色的天花板是:让读者觉得这孩子本来就能写得这么好。
> 一旦改到读者会怀疑"这是小学生写的吗",就已经改过头了。
>
> 本 Skill 是付费技能:润色知识包托管在服务端,需按次付费取回。知识包到手后,由你在本地据此润色。
隐私:作文正文不出本机
用户给的作文正文,一个字都不上传。 这是本 Skill 的硬保证,不是尽量做到。
客户端脚本(scripts/essay-paid.mjs)在上传前就地完成三件事:
| 步骤 | 做什么 | 结果 |
|---|---|---|
| 1 | 用本机私钥(~/.essay-pay/essay-salt,0600)对规范化正文做 HMAC-SHA256 |
essay_digest —— 带密钥的承诺,服务端反推不出正文 |
| 2 | 数中文字数 | essay_hanzi |
| 3 | 数自然段数 | essay_paras |
因此真正发出去的请求体只有这 5 个字段,没有正文:
{ "essay_digest": "<64位hex>", "essay_hanzi": 106, "essay_paras": 4, "grade": "六年级", "title": "我的妈妈" }
配套的几条约束:
- 长度校验也在本地做 —— 正文太短/过长时根本不发请求,不产生订单。
- 原文只落在本机 —— 存在 state 目录的
essay-original.txt(0600),供 P4 的--compare使用。 - 私钥只在本机 —— 服务端永远拿不到,所以即使它存下摘要也无法与任何文本比对。
title是用户单独给的题目(可选)。若题目本身也不宜外传,不传即可,知识包会写「未提供」。- 原文在本机只有一处 ——
<state_dir>/essay-original.txt。用户若要求"用完删掉",
在交付完成后删除整个 state 目录即可(headers.txt/payment-needed.txt等中间值也可一并清掉)。
> ⚠️ 三条给 AI 的隐私纪律:
> 1. 不要把正文回传到任何服务(包括"为了诊断"再发一遍)。
> 2. 不要在对话里反复复述整篇正文 —— 用户已经在本机给了,直接用。
> 3. 交付物是「建议 + 润色稿」,不是原文的副本。
服务与定价
| 项 | 值 |
|---|---|
| 业务端点 | https://paynotify.aidangs.com/payNotify/api/v1/essay(可用环境变量 ESSAY_PAY_RESOURCE_URL 覆盖) |
| 单次价格 | 0.5 元 / 篇 |
| 计费单元 | 每篇润色结果(交付一份「建议 + 润色稿」计一次) |
| 免费额度 | 前 3 篇免费(服务端按 x-buyer-id 计数) |
| 计费依据 | 实际收费以服务端 402 账单金额为准 |
> 单价必须与支付宝服务市场登记价、SkillHub 展示价保持一致。
请求次数:业务端点只有一处出口
本 Skill 里访问业务端点的请求,只有一处代码实现 —— scripts/essay-paid.mjs 的 postEssay()。
端点地址也只有一处出处(RESOURCE_URL),pay / complete / status 全部复用它或从它派生;
status 走的是 /api/pay/status,不访问业务端点。
单次润色对业务端点的请求次数,是 402 协议的硬下限:
| 场景 | 次数 | 谁发起 |
|---|---|---|
| 命中免费额度 | 1 次 | probe —— 一次请求直接取回知识包 |
| 付费首单 | 2 次 | 第 1 次 probe 探价拿账单;第 2 次官方 CLI 402-query-payment-status 带 Payment-Proof 取货 |
| 已付款未取回的重试 | 仍是那第 2 次 | 官方 CLI —— 复用同一交易号与原请求体,不重新下单 |
status 查单 |
0 次(不碰业务端点) | 脚本走 /api/pay/status |
> ⚠️ 第 2 次不可省,这不是实现偷懒。 官方买家技能《HTTP 402 支付》规定
> 「--amt-payment-link 与 --file 只能有一个」,账单只能来自上一次真实的 402 响应;
> 实测 alipay-bot 402-buyer-pay 不给 --file 时只会交互索要「402 响应文件路径」,
> 不会自己去请求资源。因此「一次请求端到端」在协议上不存在:
> 不要试图跳过 probe,也不要自己造 Payment-Proof。
> ✅ 可以省的是重复的 complete:若 pay 的输出里已经给出了知识包正文
> (付款与履约同步完成),说明资源已取回,不要再跑 complete —— 官方 CLI 在取得非空资源后
> 已自动发送履约回执,不追加独立履约或重复查询。
付费前置检查(第一步,不可跳过)
调本 Skill 前先确认买家侧支付能力可用:
- 确认买家侧官方支付能力可用 —— 技能
alipay-payment-skill可加载,且命令行alipay-bot可执行。 - 不可用 → 停下,向用户说明并引导安装:
bash npx -y @alipay/agent-payment@latest install
安装完成、确认alipay-bot可用后再继续。 - 可用 → 直接进入下面的调用流程,不做任何钱包预检。
> 🔒 钱包就绪不由本 Skill 负责。 官方 alipay-payment-skill 明令:
> 「支付命令负责自身的钱包就绪处理;不要用额外的准备命令替代它」。
> 因此本 Skill 不预检、不预热、不代开钱包:
> - 免费额度路径压根用不到钱包,预检是白跑一次;
> - 付费路径由 402-buyer-pay / submit-payment 自行处理钱包就绪。
>
> 只有当支付命令的输出明确把控制权交回独立钱包生命周期时,
> 才调用官方钱包能力 alipay-authenticate-wallet 完成开通/授权,然后重试原流程。
> 不要用 check-wallet 之类的命令"先看一眼" —— 那正是官方禁止的额外准备命令。
> ⚠️ 三个「不」:不自行实现付款、不自行生成凭证或签名、不接触任何支付密钥。
> 账单(Payment-Needed)原样保存,禁止解码、改写或追加字符 —— 官方 CLI 直接读该文件。
输入参数
| 参数 | 必填 | 说明 |
|---|---|---|
essay |
是 | 要润色的作文正文。原文照录,不要自己先改一遍。只在本机处理,不上传 |
grade |
是 | 学段或年级:低段/中段/高段,或一~六年级。决定润色标准与改动力度 |
title |
否 | 作文题目,用于对齐立意。会随请求上传;题目敏感就别传 |
关于学段:润色标准必须按学段定——同一个句子,对二年级是加分,对六年级是扣分。
学段不明时先问一次,不要猜(猜错方向的润色比不润色更糟)。
关于正文:正文交给脚本后,脚本会先做本地校验(≥30 字、≤6000 字符)再算出摘要。
校验不过就直接返回错误、不发请求,此时不会产生任何订单与费用。
只有正文、没有学段时:向用户问一句"这是几年级的作文?",问完再走流程。
关于正文:
- 直接粘贴的正文、或从 .txt / .docx 里读出的文字,都放进 essay。
- 保留原文的错别字、病句、口语 —— 这些都是要诊断的对象,不要预处理掉。
- 正文过短(不足 30 个中文字)或过长(超过 6000 字符)服务端会返回 400,需先与用户确认。
调用流程
第 1 步 · probe(请求资源)
node scripts/essay-paid.mjs probe --grade "三年级" --title "我的妈妈" --essay "<作文正文>"
> 正文较长时建议把正文写进临时文件再用 --essay-file,避免命令行转义出问题:
> bash
> node scripts/essay-paid.mjs probe --grade "三年级" --title "我的妈妈" --essay-file "/tmp/essay.txt"
>
> --essay 与 --essay-file 二选一。
脚本在这一步内部做两件事,都不出本机:
- 本地校验 —— 正文为空 / 不足 30 字 / 超过 6000 字符时直接返回
error,不发任何请求。 - 本地派生 —— 算出
essay_digest(HMAC-SHA256)与字数、段数,只把这几个量上传;
原文另存为 state 目录下的essay-original.txt。
> 这次请求就是全链路唯一一次由脚本发起的业务请求(postEssay());
> 输出里的 endpoint_requests: 1 即本次对业务端点的累计请求数。
结果有三种:
kind: "resource"—— 无账单,已直接拿到知识包。跳到第 4 步。kind: "payment"—— 拿到账单,脚本已把账单原文与原始请求落盘(payment-needed.txt/state.json,权限收紧;POSIX 下为 0600)。
向用户展示 服务名称与金额,说明本次计费,等用户确认后进入第 2 步。
输出里的privacy字段会如实说明「正文未上传」。kind: "error"—— 状态异常或带业务错误码,按下面「异常处理速查」处理。
> ⚠️ 判据是响应头,不是 HTTP 状态码。 服务若经 API 网关「透传响应」暴露(云函数等 serverless 平台的常见挂法),
> 网关按「函数是否执行成功」决定外层状态码,成功恒为 200 —— 服务端的 402 会被吃掉。
> 因此:只要响应里出现 Payment-Needed 头,就是待付账单,无论外层是 200 还是 402。
> 客户端已按此实现;输出里的 status_rewritten: true 即表示「外层状态码被网关改写过」。
> ⚠️ Windows 上 NTFS 不套用 POSIX 权限位,0600 不会真正生效。敏感中间目录请放在用户私有目录下,勿放共享路径。
> ⚠️ 若 error_code 字段非空(如 VERIFY_FAILED / ORDER_NOT_FOUND / RESOURCE_ID_MISMATCH),
> 按下面「异常处理速查」处理,不要当作普通账单推进。
第 2 步 · pay(推进支付)
node scripts/essay-paid.mjs pay --state-dir "<上一步输出的 state_dir>" --session-id "<会话号>"
脚本把账单文件与原始请求交给官方买家 CLI(alipay-bot 402-buyer-pay),
展示用途、金额与支付入口,等待用户扫码授权。
把 CLI 输出原样展示给用户(含支付链接与交易号),不要自行解释或改写。
不要自行实现支付,不要接触任何支付密钥。
> ⚠️ 看输出决定要不要走第 3 步:若 pay 的输出里已经带了知识包正文,
> 说明付款与履约同步完成、资源已取回 —— 不要再执行 complete,否则会对业务端点多打一次请求。
第 3 步 · complete(取回资源)
node scripts/essay-paid.mjs complete --state-dir "<state_dir>" --trade-no "<交易号>"
脚本用交易号 + 完全相同的原始请求调用 alipay-bot 402-query-payment-status
查询原订单并继续原请求,服务端验付后返回资源。同样原样透传 CLI 输出。
> 这是本次润色对业务端点的第 2 次、也是最后一次请求(由官方 CLI 携带 Payment-Proof 发起)。
> 脚本自己不会再补发一次,也不接受任何「为了诊断再请求一遍」的做法。
> 仅当第 2 步的输出没给出知识包时才执行这一步;已取回就跳过。
第 4 步 · 润色(本 Skill 的核心)
拿到知识包(content 字段)后,按包内说明执行五步工作流:
| 步 | 做什么 | 用知识包的哪一部分 |
|---|---|---|
| P1 定标准 | 按学段定「往哪个方向改」+ 改动处数上限 | 学段润色标准 |
| P2 诊断 | 逐条对照,挑出命中的问题,按 P0→P2 排序 | 常见问题清单(含黑名单) |
| P3 动手改 | 用「增 / 删 / 改 / 移 / 升级」改 | 润色手法库(+ 写作手法库、细节灵感库) |
| P4 收敛 | 砍到学段上限的处数;跑 essay_lint.py --compare |
体检脚本 |
| P5 自检 | 10 项全过才交 | 润色交付自检清单 |
改动处数上限(不可超):低段 ≤ 3 处 · 中段 3–6 处 · 高段 5–10 处。
关键动作:P4 必须跑对比脚本。 把原文与润色稿各写一个临时文件:
python essay_lint.py --compare <原文文件> <润色稿文件> 中段
它会卡住三类错误:
- 改过头 —— 原文已达标时,字数变化超 ±15%;
- 该补没补 —— 原文未达标时(孩子写得比学段要求短很多),
判定「润色稿有没有朝学段区间靠拢」;没靠拢反而更短就 FAIL。
注意:这种情况下不要硬把字数压回原文的 ±15% —— 那会把正确的补写判成违规; - 黑名单没改干净 / 越改越脏 —— A 类硬禁词没减少,或比原文还多(你改的时候自己写出来了)。
默认只输出两样东西:
## 一、润色建议与说明
**第 2 段**:「我的妈妈很辛苦」是概括,没有事实支撑。
> 原文:我的妈妈很辛苦。她每天早上很早就起来了,给我做饭,然后送我上学。
> 改成:六点半,厨房里的抽油烟机先响起来。我趴在门框上看她,她一手按着菜,一手使刀,砧板咚咚地响。
> 为什么:把"辛苦"这个结论,换成她自己看见的一幕。读者自己会得出"辛苦",不用说出来。
……(按 P0 → P2 顺序,每条都是「原文 + 改成 + 为什么」三件套)
## 二、润色后的文章
妈妈进门先把菜袋往地上一放,甩甩手腕,才顾上跟我说话。
(正文,按自然段分段,段与段之间空一行)
……(原文的最后一段)
—— 改了 5 处,都在第二、三段。
建议的写法要求(严格执行):
- 三件套齐:原文引用 + 改后示例 + 一句为什么。缺一不可。
- 不许空评语:不写"语言不够生动""描写不够具体",要写清是哪一句、怎么改。
- 不用课堂术语当唯一解释:不写"要总分总""要点题",用孩子能听懂的话说清道理。
- 限量:按学段上限挑,不许把所有能改的都列出来。
- 位置准确:说"第 2 段"就必须真的是第 2 段。
免费额度提示(位置与条件严格):
- 服务端返回 free_quota: true 时,在交付末尾如实带一句,语气是提示不是推销:
—— 这是第 2 篇免费润色,还剩 1 篇。
- 没返回 free_quota 时,一个字都不许提免费。
原样重试铁律(不可违背)
支付成功后重试必须复用原请求:
- 请求参数一字不改 ——
essay正文、grade、title必须与首次完全一致。
服务端的resource_id由这三项派生,正文改一个字都会返回409 RESOURCE_ID_MISMATCH。 - 订单信息只走官方 CLI —— 账单与凭证一律交给
alipay-bot处理,
不要把它们写进请求体,也不要改 payload 业务字段来传订单号。 - 状态不明先查单,不重复付款 —— 拿不准是否已付款时,先查原订单:
bash node scripts/essay-paid.mjs status --state-dir "<state_dir>" - 已付款未取回 → 复用同一交易号与原请求重试,不要重新下单。
- 成功路径不追加独立回执 —— 官方 CLI 取得非空资源后会按当前流程处理回执。
- 账单有效期 15 分钟 —— 过期需重新 probe 生成新账单;已付款的旧订单仍可用于重试履约。
异常处理速查
| 响应 | 含义 | 动作 |
|---|---|---|
带 Payment-Needed 头 |
未付费(正常账单)。外层状态码可能是 200(被网关改写) | 走 pay → 用户扫码付款 → complete |
ESSAY_REQUIRED |
没给作文正文 | 向用户要正文(本地报错,未发请求) |
ESSAY_TOO_SHORT |
正文不足 30 个中文字 | 与用户确认是否是完整作文(本地报错,未发请求) |
ESSAY_TOO_LONG |
正文超过 6000 字符 | 请用户拆分后分次提交(本地报错,未发请求) |
ESSAY_IN_BODY_REJECTED |
请求体里出现了 essay 字段(老客户端) |
更新客户端脚本;正文不该出现在请求里 |
400 DIGEST_INVALID |
摘要不是 64 位十六进制 | 重新跑 probe(摘要由脚本本机生成,不该出现此错) |
400 ESSAY_STATS_INVALID |
上报的字数/段数不是合法整数或超出合理范围 | 重新跑 probe;持续出现则检查脚本版本 |
400 GRADE_REQUIRED |
没传学段 | 问清年级后重试 |
402 VERIFY_FAILED |
凭证无效或过期(支付宝侧拒绝) | 重新 probe 生成新账单后付款;不要重复用同一凭证 |
402 ORDER_NOT_FOUND |
验付成功但服务端无此订单 | 重新 probe 生成新订单;已扣款的联系商家 |
409 RESOURCE_ID_MISMATCH |
请求内容与原订单不一致(正文被改过,或本机摘要私钥丢失) | 用完全相同的正文重试;若换过机器/删过 ~/.essay-pay,需重新下单 |
502 FULFILLMENT_CONFIRM_FAILED |
履约回执未成功 | 携同一凭证重试;不要重新付款 |
5xx / 网络错误 |
服务端异常 | 状态不明时先 status 查单,再决定重试 |
> 排障提示:脚本输出的 status 是还原后的真实意图码,status_rewritten: true 表示外层状态码被网关改写过。
> 若既无 Payment-Needed 头、状态码又正常,却拿到的是账单 JSON,说明部署链路上有中间层吃掉了响应头 —— 检查网关是否透传自定义头。
不可回退原则
- 先查支付能力 —— 买家侧依赖(
alipay-bot)不可用时停下说明,不硬走流程。
但只查依赖,不预检钱包 —— 钱包就绪由支付命令自己负责,见上文「付费前置检查」。 - 不碰支付密钥 —— 付款、签名、凭证处理一律交给官方 CLI,本 Skill 只负责编排。
- 不改账单 ——
Payment-Needed原样保存原样传递,禁止解码后重编码。 - 正文不出本机 —— 绝不上传、不转发、不回传用户作文的任何片段;只传摘要与统计量。需要原文时从本地
essay-original.txt取。 - 不认状态码只认头 —— 判定「要不要付钱」只看
Payment-Needed,不信外层状态码(可能被网关统一改写为 200)。 - 不重复扣费 —— 状态不明先
status查原订单;已付款的绝不用新账单重新下单。 - 不换题材、不改事实、不改视角 —— 只改表达,第一人称、"我"的经历、所有人名地名数字全部保留。
- 不清理孩子的口语 —— "然后""挺""可""就是"是学生味的一部分,删掉文章就假了。
- 不新增原文没有的内容 —— 不编新人物、新情节、新对话、新细节。
- 改动限量 —— 严格按学段上限(低段 ≤3 · 中段 3–6 · 高段 5–10),宁少勿多。
- 必须交建议 —— 只给润色稿不给建议是失败的交付;每条建议都要能回答"为什么"。
- 业务端点只有一个出口 —— 任何要访问业务端点的代码,都必须走
postEssay()与唯一的RESOURCE_URL;
不得另开请求路径、不得自己造Payment-Proof、不得在状态不明时补发请求。请求次数见上面的「请求次数」一节。
依赖与安装
| 侧 | 依赖 | 安装 |
|---|---|---|
| 买家(使用端) | @alipay/agent-payment,提供技能 alipay-payment-skill 与命令行 alipay-bot |
npx -y @alipay/agent-payment@latest install |
| 卖家(服务端) | 本 Skill 的业务服务,独立部署 | 不随本包分发,见项目 backend/README.md |
> 注意:@alipay/alipay-aipay(卖家集成用)与 @alipay/agent-payment(买家支付用)用途不同,别装错。
文件结构
wenxin-polish-tutor/
├── SKILL.md
├── scripts/
│ └── essay-paid.mjs probe / pay / complete / status
│ (业务请求唯一出口:postEssay + 唯一 RESOURCE_URL)
└── references/
└── service-api.md 服务端接口与错误码说明
> ⚠️ 目录层级硬约束:Skill 包仅支持两级目录(根目录 / 二级目录 / 文件),禁止三级及以上嵌套。
> 包内不含任何密钥、.env、状态文件或 node_modules。
三级加载
- L0 —— frontmatter 的
name/description/triggers,常驻触发。 - L1 —— 本文件 body:付费前置检查、输入参数、五步润色工作流、原样重试铁律、异常速查。
- L2 ——
scripts/essay-paid.mjs与references/service-api.md按需展开。