# 技能契约

可执行技能写在员工市场 `.dshpreset` 包里，由 Host `drowup_listing_run` 或过渡 CLI invoke 装配 ctx。不要做成公开货架。

## 字段类型规范（对象还是字符串）

页面脚本里往服务端写结构化字段时，**传对象，不要 JSON.stringify 成字符串**：

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `attributes` | 对象 `{材质:"…", 风格:"…"}` | 服务端会再序列化一次，字符串会双重转义、前端逐字符显示 |
| `specs` / `variants` | 对象 / 数组 | 同上，直接传原生结构 |
| `price` / `stock` / `weight` / `length/width/height` | number | 数字，不要带单位字符串 |
| `title` / `titleEn` / `description` | string | 纯文本 |
| `media` / `sourceImages` | 数组 | URL 数组 |

CLI 侧 `parseMaybeJsonField` 会把「看起来像 JSON 的字符串」解析回对象，但那只兜字符串化误传；**正确姿势是源头就别字符串化**。

## 文件结构

员工包里的平台技能：

```text
preset/skills/<平台>/
├── SKILL.md           # 必需：说明书
├── skill.json         # 运行契约：needs / params
└── scripts/index.cjs  # Host drowup_listing_run / 过渡 CLI invoke 入口
```

员工 `.dshpreset` 写最短路径：用户点名环境并说上 N 个品时，每个环境一条 Host 工具 `drowup_listing_run`（`env` / `products` / `skill` / 默认草稿 params）。不要写「先问齐店铺/仓库/英文标题/类目再跑」，也不要写「先问他要哪一步」。标题、类目、图、价从产品库读。整批 params 不要放按品变化的字段。然后写清做不到的事、失败了怎么办。单步脚本只给排障。无 Desktop / Codex 调试才写过渡 CLI invoke。

## 脚本

```js
module.exports = async (ctx) => {
  const title = ctx.input.titleEn
    || await ctx.ai.ask('把中文标题翻成英文，只输出标题', { 标题: ctx.input.title })
    || ctx.input.title
  await ctx.page.fill('#title', title)
  ctx.log('已填标题：' + title)
  return { 标题: title }
}
```

名字自取。Desktop 员工会话由 Host `drowup_listing_run` require 脚本。无 Desktop / Codex 调试才 `drowup invoke <脚本.cjs>`。新的可执行员工走 Desktop 员工市场。

填写属性、翻译、从候选里挑类目，用 `ctx.ai`（Desktop 自己的模型），不要在脚本里再接一套大模型：

```js
await ctx.ai.ask('给商品属性「材质」填一个合理短值，只输出值本身', { 标题: ctx.input.title })
await ctx.ai.pick('选最合适的类目', options, { 标题: ctx.input.title })
```

`ctx.ai` 失败返回 `null`，每处都要有兜底。底层走 Desktop 本机模型（`drowup.exe ai ask`），也不进员工对话。Agent 在 runner 外试一问用这条命令。

**不要**在技能包里自行安装浏览器自动化依赖，不要自写 runner、checkpoint、报告。`ctx.page` 已经是连上指纹环境的页面对象，技能脚本只填表、点按钮。
