@optima-chat/optima-agent 0.9.7 → 0.9.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/.claude/settings.local.json +166 -0
  2. package/.claude/skills/gen/SKILL.md +5 -1
  3. package/.claude/skills/multigrid-poster/SKILL.md +192 -0
  4. package/.claude/skills/multigrid-poster/layouts/2x2.json +34 -0
  5. package/.claude/skills/multigrid-poster/layouts/3x3.json +43 -0
  6. package/.claude/skills/multigrid-poster/scripts/compose.py +116 -0
  7. package/.claude/skills/multigrid-poster/scripts/placeholder.png +0 -0
  8. package/.claude/skills/multigrid-poster/shared/fonts/MaShanZheng-Regular.ttf +0 -0
  9. package/.claude/skills/scout/SKILL.md +136 -23
  10. package/.claude/skills/video-gen/SKILL.md +56 -79
  11. package/dist/bin/bi-cli.js +0 -0
  12. package/dist/bin/browser-cli.js +0 -0
  13. package/dist/bin/comfy.d.ts +3 -0
  14. package/dist/bin/comfy.d.ts.map +1 -0
  15. package/dist/bin/comfy.js +3 -0
  16. package/dist/bin/comfy.js.map +1 -0
  17. package/dist/bin/commerce.js +0 -0
  18. package/dist/bin/gen.js +0 -0
  19. package/dist/bin/google-ads.js +0 -0
  20. package/dist/bin/growth.d.ts +3 -0
  21. package/dist/bin/growth.d.ts.map +1 -0
  22. package/dist/bin/growth.js +3 -0
  23. package/dist/bin/growth.js.map +1 -0
  24. package/dist/bin/logistics.js +0 -0
  25. package/dist/bin/optima.js +0 -0
  26. package/dist/bin/scout.js +0 -0
  27. package/dist/bin/sentinel.js +0 -0
  28. package/dist/bin/shopify.js +0 -0
  29. package/dist/src/hooks-loader.d.ts +6 -0
  30. package/dist/src/hooks-loader.d.ts.map +1 -0
  31. package/dist/src/hooks-loader.js +215 -0
  32. package/dist/src/hooks-loader.js.map +1 -0
  33. package/dist/src/system-prompt.d.ts.map +1 -1
  34. package/dist/src/system-prompt.js +4 -0
  35. package/dist/src/system-prompt.js.map +1 -1
  36. package/dist/src/ui/App.d.ts +6 -0
  37. package/dist/src/ui/App.d.ts.map +1 -0
  38. package/dist/src/ui/App.js +164 -0
  39. package/dist/src/ui/App.js.map +1 -0
  40. package/dist/src/ui/components/Composer.d.ts +10 -0
  41. package/dist/src/ui/components/Composer.d.ts.map +1 -0
  42. package/dist/src/ui/components/Composer.js +13 -0
  43. package/dist/src/ui/components/Composer.js.map +1 -0
  44. package/dist/src/ui/components/Header.d.ts +7 -0
  45. package/dist/src/ui/components/Header.d.ts.map +1 -0
  46. package/dist/src/ui/components/Header.js +7 -0
  47. package/dist/src/ui/components/Header.js.map +1 -0
  48. package/dist/src/ui/components/Message.d.ts +12 -0
  49. package/dist/src/ui/components/Message.d.ts.map +1 -0
  50. package/dist/src/ui/components/Message.js +21 -0
  51. package/dist/src/ui/components/Message.js.map +1 -0
  52. package/dist/src/ui/components/MessageList.d.ts +9 -0
  53. package/dist/src/ui/components/MessageList.d.ts.map +1 -0
  54. package/dist/src/ui/components/MessageList.js +18 -0
  55. package/dist/src/ui/components/MessageList.js.map +1 -0
  56. package/dist/src/ui/components/Spinner.d.ts +6 -0
  57. package/dist/src/ui/components/Spinner.d.ts.map +1 -0
  58. package/dist/src/ui/components/Spinner.js +7 -0
  59. package/dist/src/ui/components/Spinner.js.map +1 -0
  60. package/dist/src/ui/components/StatusBar.d.ts +11 -0
  61. package/dist/src/ui/components/StatusBar.d.ts.map +1 -0
  62. package/dist/src/ui/components/StatusBar.js +7 -0
  63. package/dist/src/ui/components/StatusBar.js.map +1 -0
  64. package/dist/src/ui/components/index.d.ts +7 -0
  65. package/dist/src/ui/components/index.d.ts.map +1 -0
  66. package/dist/src/ui/components/index.js +7 -0
  67. package/dist/src/ui/components/index.js.map +1 -0
  68. package/dist/src/validation/error-formatter.d.ts +21 -0
  69. package/dist/src/validation/error-formatter.d.ts.map +1 -0
  70. package/dist/src/validation/error-formatter.js +98 -0
  71. package/dist/src/validation/error-formatter.js.map +1 -0
  72. package/dist/src/validation/index.d.ts +10 -0
  73. package/dist/src/validation/index.d.ts.map +1 -0
  74. package/dist/src/validation/index.js +10 -0
  75. package/dist/src/validation/index.js.map +1 -0
  76. package/dist/src/validation/json-validator.d.ts +25 -0
  77. package/dist/src/validation/json-validator.d.ts.map +1 -0
  78. package/dist/src/validation/json-validator.js +173 -0
  79. package/dist/src/validation/json-validator.js.map +1 -0
  80. package/dist/src/validation/schema.d.ts +353 -0
  81. package/dist/src/validation/schema.d.ts.map +1 -0
  82. package/dist/src/validation/schema.js +57 -0
  83. package/dist/src/validation/schema.js.map +1 -0
  84. package/dist/src/validation/suggestions.d.ts +25 -0
  85. package/dist/src/validation/suggestions.d.ts.map +1 -0
  86. package/dist/src/validation/suggestions.js +144 -0
  87. package/dist/src/validation/suggestions.js.map +1 -0
  88. package/dist/src/validation/types.d.ts +40 -0
  89. package/dist/src/validation/types.d.ts.map +1 -0
  90. package/dist/src/validation/types.js +5 -0
  91. package/dist/src/validation/types.js.map +1 -0
  92. package/dist/src/validation/yaml-validator.d.ts +25 -0
  93. package/dist/src/validation/yaml-validator.d.ts.map +1 -0
  94. package/dist/src/validation/yaml-validator.js +177 -0
  95. package/dist/src/validation/yaml-validator.js.map +1 -0
  96. package/package.json +1 -1
@@ -0,0 +1,166 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(gh api:*)",
5
+ "WebFetch(domain:platform.claude.com)",
6
+ "Bash(git init:*)",
7
+ "Bash(mkdir:*)",
8
+ "Bash(npm run typecheck:*)",
9
+ "Bash(npm view:*)",
10
+ "WebSearch",
11
+ "Bash(commerce --help)",
12
+ "Bash(done)",
13
+ "Bash(commerce product:*)",
14
+ "Bash(commerce order:*)",
15
+ "Bash(commerce i18n:*)",
16
+ "Bash(google-ads:*)",
17
+ "Bash(scout --help:*)",
18
+ "Bash(tree:*)",
19
+ "Bash(cloc:*)",
20
+ "Bash(npm run build:*)",
21
+ "Bash(git restore:*)",
22
+ "Bash(gh repo view:*)",
23
+ "Bash(mv:*)",
24
+ "Bash(rmdir:*)",
25
+ "Bash(git add:*)",
26
+ "Bash(git commit:*)",
27
+ "Bash(git push)",
28
+ "Bash(timeout 5 npm run optima:*)",
29
+ "Bash(npm install:*)",
30
+ "Bash(cat:*)",
31
+ "Bash(gh issue create:*)",
32
+ "Bash(npx tsx:*)",
33
+ "Bash(timeout 30 npx tsx:*)",
34
+ "Bash(git push origin feature/ask-user-question)",
35
+ "Bash(node:*)",
36
+ "Bash(npm version:*)",
37
+ "Bash(git push:*)",
38
+ "Bash(npm publish:*)",
39
+ "Bash(pkill:*)",
40
+ "Bash(git -C /Users/verypro/optima-agent log --oneline --all -- \".claude/\")",
41
+ "Bash(wc:*)",
42
+ "Bash(grep:*)",
43
+ "Bash(find:*)",
44
+ "Bash(commerce collection --help:*)",
45
+ "Bash(commerce collection update --help:*)",
46
+ "Bash(commerce collection set-cover:*)",
47
+ "Bash(commerce collection get --help:*)",
48
+ "Bash(commerce collection list --help:*)",
49
+ "Bash(commerce collection create --help:*)",
50
+ "Bash(commerce collection remove-products:*)",
51
+ "Bash(commerce collection list-products:*)",
52
+ "Bash(commerce --version:*)",
53
+ "Bash(bi-cli --version:*)",
54
+ "Bash(commerce homepage create --help:*)",
55
+ "Bash(commerce homepage reorder --help:*)",
56
+ "Bash(commerce homepage delete --help:*)",
57
+ "Bash(commerce homepage update-images:*)",
58
+ "Bash(commerce homepage update-collections:*)",
59
+ "Bash(commerce homepage update-target:*)",
60
+ "Bash(commerce homepage switch-template:*)",
61
+ "Bash(commerce inventory:*)",
62
+ "Bash(commerce merchant:*)",
63
+ "Bash(commerce review:*)",
64
+ "Bash(commerce product-page:*)",
65
+ "Bash(bi-cli:*)",
66
+ "Bash(comfy:*)",
67
+ "Bash(scout search:*)",
68
+ "Bash(scout product:*)",
69
+ "Bash(commerce homepage create-collections:*)",
70
+ "Bash(commerce homepage create-featured:*)",
71
+ "Bash(commerce homepage create-collection-products:*)",
72
+ "Bash(commerce homepage create-banner:*)",
73
+ "Bash(xargs -I {} sh -c 'echo \"\"\"\"=== {} ===\"\"\"\"; head -3 /Users/verypro/optima-agent/.claude/skills/{}/SKILL.md | grep \"\"\"\"name:\"\"\"\"')",
74
+ "Bash(ls:*)",
75
+ "Bash(gh issue view:*)",
76
+ "Bash(npx markdownlint-cli:*)",
77
+ "Bash(chmod:*)",
78
+ "Bash(npm whoami:*)",
79
+ "Bash(tsx test-scripts/test-headless-progress.ts:*)",
80
+ "Bash(DEBUG_STREAM=1 node dist/bin/optima.js:*)",
81
+ "Bash(git describe:*)",
82
+ "WebFetch(domain:github.com)",
83
+ "Bash(./scripts/test-headless.sh:*)",
84
+ "Bash(./scripts/test-headless-simple.sh:*)",
85
+ "Bash(env)",
86
+ "Bash(gh pr list:*)",
87
+ "Bash(gh pr view:*)",
88
+ "Bash(gh pr diff:*)",
89
+ "Bash(optima --version:*)",
90
+ "Bash(optima agent headless:*)",
91
+ "Bash(optima headless:*)",
92
+ "Bash(/Users/verypro/optima-agent/scripts/test-headless.sh:*)",
93
+ "Bash(/Users/verypro/optima-agent/scripts/test-headless-simple.sh:*)",
94
+ "Bash(tee:*)",
95
+ "Bash(CONV_ID=\"conv-1\":*)",
96
+ "Bash(echo:*)",
97
+ "Bash(scout tiktok trending --help:*)",
98
+ "Bash(scout tiktok trending:*)",
99
+ "Bash(git checkout:*)",
100
+ "Bash(npm test:*)",
101
+ "Bash(git tag:*)",
102
+ "Bash(/private/tmp/claude/-Users-verypro-optima-agent/68a9ac2c-def2-44e1-b42b-e53bd9022ab6/scratchpad/test-canUseTool.sh)",
103
+ "Bash(optima --help:*)",
104
+ "Bash(npx @optima-chat/ads-cli:*)",
105
+ "Bash(head:*)",
106
+ "Bash(git pull:*)",
107
+ "Bash(pnpm build:*)",
108
+ "Skill(read-code)",
109
+ "Bash(npm run cli:*)",
110
+ "Bash(scout:*)",
111
+ "WebFetch(domain:docs.scrapecreators.com)",
112
+ "WebFetch(domain:scrapecreators.com)",
113
+ "Bash(gh auth status:*)",
114
+ "Bash(optima-agent:*)",
115
+ "Bash(python3:*)",
116
+ "Bash(python3 -c \" import sys content = sys.stdin.read\\(\\) # Find flushMessageQueueSync idx = content.find\\(''flushMessageQueueSync''\\) # Get context around it lines = content.split\\(''\\\\n''\\) for i, line in enumerate\\(lines\\): if ''flushMessageQueueSync'' in line and ''private'' in line: for j in range\\(i, min\\(i+20, len\\(lines\\)\\)\\): print\\(f''{j+1}: {lines[j]}''\\) break \")",
117
+ "Bash(optima:*)",
118
+ "WebFetch(domain:www.npmjs.com)",
119
+ "WebFetch(domain:registry.npmjs.org)",
120
+ "WebFetch(domain:zod.dev)",
121
+ "Bash(npm ls:*)",
122
+ "Bash(NODE_DEBUG=child_process npx tsx:*)",
123
+ "Bash(DEBUG_CLAUDE_AGENT_SDK=1 npx tsx:*)",
124
+ "Bash(CLAUDECODE= npx tsx:*)",
125
+ "Bash(env:*)",
126
+ "Bash(gh release:*)",
127
+ "Bash(npm info:*)",
128
+ "Bash(gh run:*)",
129
+ "Bash(gtimeout 90:*)",
130
+ "Bash(sentinel:*)",
131
+ "Bash(gh pr:*)",
132
+ "Bash(git fetch:*)",
133
+ "Bash(git log:*)",
134
+ "Bash(npm bin:*)",
135
+ "Bash(git status:*)",
136
+ "Bash(browser-cli --version && browser-cli --help)",
137
+ "Bash(browser-cli status:*)",
138
+ "Bash(browser-cli launch:*)",
139
+ "Bash(browser-cli screenshot:*)",
140
+ "Bash(browser-cli --version)",
141
+ "Bash(browser-cli close:*)",
142
+ "Bash(npm update:*)",
143
+ "Bash(node -e \"console.log\\(require\\('./node_modules/@optima-chat/browser-cli/package.json'\\).version\\)\")",
144
+ "WebFetch(domain:raw.githubusercontent.com)",
145
+ "Bash(aws ecs:*)",
146
+ "Bash(aws ecr:*)",
147
+ "Bash(gh search:*)",
148
+ "Bash(npx tsc:*)",
149
+ "Bash(find /Users/verypro/optima-agent -path */node_modules -prune -o -type f \\\\\\(-name *comfy* -o -name *gen* \\\\\\) -print)",
150
+ "Bash(export PATH=\"/usr/local/bin:$PATH\")",
151
+ "Bash(pnpm add:*)",
152
+ "Bash(cp -r ~/optima-agent/test/skills/kol-outreach/fixtures/one-active-campaign ~/optima-agent/test/skills/kol-outreach/fixtures/max-rounds-hit)",
153
+ "Bash(cp -r ~/optima-agent/test/skills/kol-outreach/fixtures/one-active-campaign ~/optima-agent/test/skills/kol-outreach/fixtures/budget-almost-spent)",
154
+ "Bash(cp -r ~/optima-agent/test/skills/kol-outreach/fixtures/one-active-campaign ~/optima-agent/test/skills/kol-outreach/fixtures/two-active-campaigns)",
155
+ "Bash(cp -r ~/optima-agent/test/skills/kol-outreach/fixtures/one-active-campaign ~/optima-agent/test/skills/kol-outreach/fixtures/brand-only)",
156
+ "Bash(pnpm vitest:*)",
157
+ "Bash(pnpm test:*)",
158
+ "Bash(pnpm --filter @optima-scout/backend test --reporter=verbose backend/src/routes/outreach/__tests__/lock.test.ts backend/src/routes/outreach/__tests__/inbound.test.ts)",
159
+ "Bash(npx vitest:*)",
160
+ "Bash(DATABASE_URL=postgres://optima_scout:optima_scout_dev@localhost:7293/optima_scout pnpm --filter @optima-scout/backend test)",
161
+ "Bash(pnpm --filter @optima-scout/backend exec vitest run src/routes/outreach/__tests__/inbound.test.ts)"
162
+ ],
163
+ "deny": [],
164
+ "ask": []
165
+ }
166
+ }
@@ -38,6 +38,10 @@ gen image <prompt> [options]
38
38
  | `-o, --output <path>` | 输出目录或完整路径 |
39
39
  | `-s, --seed <number>` | 随机种子(可复现结果) |
40
40
  | `-f, --format <fmt>` | 输出格式:jpeg \| png |
41
+ | `--provider <name>` | 供应商:grsai(默认) \| bfl |
42
+ | `-m, --model <model>` | 模型:gpt-image-2(默认) \| nano-banana-fast \| nano-banana-pro \| flux-kontext-pro \| flux-kontext-max 等 |
43
+ | `--aspect-ratio <ratio>` | 宽高比:1:1 \| 16:9 \| 9:16 \| 4:3 等 |
44
+ | `--image-size <size>` | nano-banana-pro 输出尺寸:1K \| 2K \| 4K |
41
45
 
42
46
  ### 工作流示例
43
47
 
@@ -86,7 +90,7 @@ gen image "a modern logo with brand color #02eb3c as the primary accent" -o /hom
86
90
  gen image "Disney Pixar style 3D poster, [产品描述], cute character, vibrant gradient background, professional studio lighting" -W 1024 -H 1024 -o /home/aiuser/project/poster.png
87
91
  ```
88
92
 
89
- ### 提示词指南(FLUX.2)
93
+ ### 提示词指南
90
94
 
91
95
  #### 结构
92
96
 
@@ -0,0 +1,192 @@
1
+ ---
2
+ name: multigrid-poster
3
+ description: "为商家生成小红书 2×2 四宫格 / 3×3 九宫格封面图。触发场景:做小红书封面 / 小红书首图 / 种草帖封面 / 爆款封面 / 四宫格 / 九宫格。一句话指令产出 1242×1660 成片,支持自然语言迭代(换版式 / 重抽某格 / 改文案)。本 skill 只生成封面图,搜索小红书笔记 / 分析博主请用 'xhs' skill。"
4
+ ---
5
+
6
+ # 小红书多宫格封面生成
7
+
8
+ 帮电商商家用 AI 图 + 通用网格布局合成小红书封面。**一句话从意图到 1242×1660 成片**,支持 4 宫格 / 9 宫格两种版式。
9
+
10
+ ## Global Rules
11
+
12
+ 优先级高于任何 pipeline 步骤。
13
+
14
+ 1. **User-facing 不出现模型名 / 服务名**
15
+ status / 成本 / 进度统一用"封面生成中 / 素材生成中 / 合成封面中"。`gen image` 作为 CLI 字面值可以,但不要把整条命令原文回显给用户。
16
+
17
+ 2. **花钱前必走 COST-GATE**
18
+ 任何 `gen image` 批量调用之前必走一次成本确认。**Fast-path、迭代、重试均无例外**。2×2 = 4 张,3×3 = 9 张,SKU 拉图 = 0 张。rate 按 `gen image` 每次 1 积分估。
19
+
20
+ 3. **Per-post init 是任何 pipeline 第一步**
21
+ 出图 / 合成执行前先建目录 + cd。否则 write 写错位置。迭代场景用 `{旧id}-vN`。
22
+
23
+ 4. **Anti-fabrication**
24
+ 未在本 skill 显式列出的命令 / flag / 参数,不允许凭印象拼。`gen image` / `commerce` / `compose.py` 子命令同样适用。
25
+
26
+ 5. **不自动发帖**
27
+ 只产 PNG,**绝对不调用**任何自动登录 / 发帖 / 上传命令。完成后给路径,用户自己下载手动发。
28
+
29
+ ## 工作目录
30
+
31
+ ```
32
+ ~/multigrid-poster/
33
+ ├── preferences.md
34
+ ├── history.md
35
+ └── posters/{post-id}/
36
+ ├── intent.md # 用户意图 + layout + 文案
37
+ ├── cells/cell_0..N.png # 4 或 9 张素材
38
+ ├── cover.png # 成片
39
+ └── cost.md
40
+ ```
41
+
42
+ 整个 `~/multigrid-poster/` 是一个 git repo。每步完成后 `git add -A && git commit`。
43
+
44
+ ## 启动流程
45
+
46
+ 1. **首次** (`ls ~/multigrid-poster/preferences.md` 不存在) → `mkdir -p ~/multigrid-poster/posters && cd ~/multigrid-poster && git init -b main`,创建空的 `preferences.md` (字段:merchant_id / brand_name / category / xhs_account_id / default_layout) 和 `history.md` (表头:date | post-id | layout | title | parent | status)。`.gitignore` 加 `posters/**/cells/*.png` 和 `posters/**/cover.png`。
47
+ 2. **扫未完成**:`posters/` 下有 `intent.md` 但无 `cover.png` 且 < 7 天 → 提一次"你有 N 个封面没完成"。
48
+ 3. **读 preferences.md / history.md**,继续主流程。
49
+
50
+ ## 主流程
51
+
52
+ ### Step 1: 选 layout
53
+
54
+ | 用户原话 | layout | cells |
55
+ |---|---|---|
56
+ | 含"九宫格 / 9 格 / 9 张 / 清单 / N 款 / 礼物推荐 / 榜单" | **3×3** | 9 |
57
+ | 其他(包含"四宫格 / 4 格 / 4 张" 或没指定) | **2×2** | 4 |
58
+
59
+ ### Step 2: Per-post init
60
+
61
+ ```bash
62
+ # slug = 用户意图前 20 字内的 kebab-case
63
+ POST_ID="$(date +%Y%m%d-%H%M)-<slug>"
64
+ mkdir -p ~/multigrid-poster/posters/$POST_ID/cells
65
+ cd ~/multigrid-poster/posters/$POST_ID
66
+ ```
67
+
68
+ 迭代("换版式 / 重抽 / 改文案")时:轻迭代沿用旧目录,重迭代新建 `{旧id}-vN`。
69
+
70
+ ### Step 3: 写文案
71
+
72
+ agent 自己写,不调外部生成器。约束:
73
+ - **title**:2 行 × 8-12 字 / 行(2×2 适合)或 2 行 × 6-10 字 / 行(3×3 标题挤)
74
+ - **caption**:2 行 × 15-20 字 / 行
75
+ - 硬禁:医疗 / 保健 / 绝对化用语(最 / 第一 / 唯一 / 100%)
76
+
77
+ 写到 `intent.md`:
78
+
79
+ ```markdown
80
+ # Intent
81
+ | 项目 | 值 |
82
+ |---|---|
83
+ | 用户原话 | <原话> |
84
+ | layout | 2x2 / 3x3 |
85
+ | title 行 1 | <8-12 字> |
86
+ | title 行 2 | <8-12 字> |
87
+ | caption 行 1 | <15-20 字> |
88
+ | caption 行 2 | <15-20 字> |
89
+ ```
90
+
91
+ 展示给用户:"标题'XXX / YYY',副标题'AAA / BBB'。OK 吗?"
92
+ - Fast-path(意图明确):告知,用户喊停才停
93
+ - 意图模糊:必须等确认
94
+
95
+ ### Step 4: COST-GATE
96
+
97
+ **生成前必做**(包括 Fast-path / 迭代 / 重试):
98
+
99
+ > 即将生成封面(布局: 2x2 / 3x3),预计:
100
+ > - 素材调用: N 次(2×2=4 / 3×3=9 / SKU 拉图=0)
101
+ > - 预估耗时: ~X 分钟
102
+ > - 预估成本: ~Y 积分
103
+ >
104
+ > 继续?
105
+
106
+ 用户说"继续 / 好" → 执行。"太贵 / 换便宜的" → 提议降级 (3×3 → 2×2,或 SKU 拉图)。不回应 → 等。
107
+
108
+ ### Step 5: 出图
109
+
110
+ **默认走 AI 生图**。每个 cell 并行调一次:
111
+
112
+ ```bash
113
+ # 2×2: cell 尺寸 621×830;3×3: cell 尺寸 414×420
114
+ gen image "<subprompt>" -W <W> -H <H> -o ./cells/cell_<i>.png -s <seed> -f png
115
+ ```
116
+
117
+ `<subprompt>` 由 agent 根据用户意图为每格独立设计(不同视角 / 不同 step / 不同场景 / 不同 SKU 等)。`<seed>` 用确定性 hash(POST_ID + cell_index),迭代复用同格 seed。
118
+
119
+ **SKU 拉图模式**(用户明确说"用我店里商品图"做 listicle):
120
+
121
+ ```bash
122
+ commerce product list --limit 9
123
+ ```
124
+
125
+ 下载到 `./cells/cell_0..8.png`。商品 < 9 → 降级 2×2 取前 4。商品 < 4 → 报错。
126
+
127
+ **失败容忍**:单格生图失败 → 重试 1 次(换 seed)。两次失败 → 用 `${CLAUDE_SKILL_DIR}/scripts/placeholder.png` 占位,告知用户"第 N 格失败,先占位,要重抽直接说"。
128
+
129
+ ### Step 6: 合成
130
+
131
+ ```bash
132
+ python3 $CLAUDE_SKILL_DIR/scripts/compose.py \
133
+ --layout $CLAUDE_SKILL_DIR/layouts/2x2.json \
134
+ --cells ./cells/cell_0.png ./cells/cell_1.png ... \
135
+ --title-line "<title 行 1>" \
136
+ --title-line "<title 行 2>" \
137
+ --caption-line "<caption 行 1>" \
138
+ --caption-line "<caption 行 2>" \
139
+ --output ./cover.png
140
+ ```
141
+
142
+ 依赖:Pillow(容器自带)。失败常见原因:
143
+
144
+ | 错误 | 处理 |
145
+ |---|---|
146
+ | `cell 数量不对` | layout 要求 4 / 9,检查 `--cells` 参数 |
147
+ | `font not found` | 检查 `$CLAUDE_SKILL_DIR/shared/fonts/` 完整 |
148
+ | 中文显示方块 | 同上,字体没加载 |
149
+
150
+ ### Step 7: 交付
151
+
152
+ 写 `cost.md`,追加 `~/multigrid-poster/history.md` 一行,告知用户:
153
+
154
+ > 封面在 `~/multigrid-poster/posters/<POST_ID>/cover.png`,可以下载发帖了。
155
+ > 换版式 / 改文案 / 重抽某格直接告诉我。
156
+
157
+ 用户说"好 / 完美" → preferences.md `Learned` 追加一条 → commit。
158
+
159
+ ## 迭代
160
+
161
+ | 类型 | 重跑步骤 | 新目录 | 成本 |
162
+ |---|---|---|---|
163
+ | 换 layout(2×2 ↔ 3×3) | 文案 → 出图 → 合成 | 是(`-vN`) | 全成本 |
164
+ | 重抽全部 | 出图 → 合成 | 是 | 全素材 |
165
+ | 重抽单格 N | 出图(单格) → 合成 | 否 | 1 素材 |
166
+ | 改文案 | 合成 | 否 | 0 |
167
+
168
+ **每次迭代也走 COST-GATE**,即使 0 积分。
169
+
170
+ ## 错误处理
171
+
172
+ | 故障 | 处理 |
173
+ |---|---|
174
+ | `gen image` 返回 failed | 重试 1 次换 seed → 仍失败用占位图 |
175
+ | 超配额 / 余额不足 | 告知用户,不自动降级 |
176
+ | `commerce product list` < 9 | 降级 2×2 取前 4 |
177
+ | 会话关闭 | 状态在文件系统 + git,下次接续 |
178
+
179
+ ## 相关工具
180
+
181
+ - `gen image` — 文生图(详见 `gen` skill)
182
+ - `commerce merchant get` / `commerce product list` — 商家档案 / 商品(详见 `merchant` skill 和 `product` skill)
183
+ - `compose.py` — 本 skill 自带的 Pillow 渲染器
184
+
185
+ ## 流程偏好
186
+
187
+ - **信息够就直接做(Fast-path)**
188
+ - **`intent.md` 是可追溯产物**
189
+ - **每步完成立刻 git commit**
190
+ - **生成过程零打扰**
191
+ - **迭代用 `-vN` 不覆盖**
192
+ - **新会话有未完成先告知一次**
@@ -0,0 +1,34 @@
1
+ {
2
+ "_comment": "通用 2×2 网格布局 — 适合 4 张 cell 的所有 intent (创业故事/对比测评/教程/场景)",
3
+ "canvas_size": [1242, 1660],
4
+ "cells": {
5
+ "positions": [[0, 0], [621, 0], [0, 830], [621, 830]],
6
+ "sizes": [[621, 830], [621, 830], [621, 830], [621, 830]]
7
+ },
8
+ "text_zones": {
9
+ "title": {
10
+ "_comment": "中央偏上 2 行标题 - 8-12 字 / 行最佳",
11
+ "font": "shared/fonts/MaShanZheng-Regular.ttf",
12
+ "size": 110,
13
+ "color": "#FFB940",
14
+ "stroke_w": 8,
15
+ "stroke_color": "#D63D3D",
16
+ "lines": [
17
+ {"position": [621, 480], "anchor": "mm"},
18
+ {"position": [621, 620], "anchor": "mm"}
19
+ ]
20
+ },
21
+ "caption": {
22
+ "_comment": "底部 2 行 caption - 15-20 字 / 行最佳",
23
+ "font": "shared/fonts/MaShanZheng-Regular.ttf",
24
+ "size": 78,
25
+ "color": "#FFB940",
26
+ "stroke_w": 6,
27
+ "stroke_color": "#D63D3D",
28
+ "lines": [
29
+ {"position": [621, 1340], "anchor": "mm"},
30
+ {"position": [621, 1450], "anchor": "mm"}
31
+ ]
32
+ }
33
+ }
34
+ }
@@ -0,0 +1,43 @@
1
+ {
2
+ "_comment": "通用 3×3 网格布局 - 适合 9 张 cell (商品清单 / 多角度展示)",
3
+ "canvas_size": [1242, 1660],
4
+ "cells": {
5
+ "_comment": "9 格 414×420。顶部 200px 标题区,cells y=200..1460,底部 200px caption 区。3×420 + 200×2 = 1660 = canvas_h ✓",
6
+ "positions": [
7
+ [0, 200], [414, 200], [828, 200],
8
+ [0, 620], [414, 620], [828, 620],
9
+ [0, 1040], [414, 1040], [828, 1040]
10
+ ],
11
+ "sizes": [
12
+ [414, 420], [414, 420], [414, 420],
13
+ [414, 420], [414, 420], [414, 420],
14
+ [414, 420], [414, 420], [414, 420]
15
+ ]
16
+ },
17
+ "text_zones": {
18
+ "title": {
19
+ "_comment": "顶部白边 2 行标题(y < 200 区间)",
20
+ "font": "shared/fonts/MaShanZheng-Regular.ttf",
21
+ "size": 78,
22
+ "color": "#FFB940",
23
+ "stroke_w": 6,
24
+ "stroke_color": "#D63D3D",
25
+ "lines": [
26
+ {"position": [621, 60], "anchor": "mm"},
27
+ {"position": [621, 150], "anchor": "mm"}
28
+ ]
29
+ },
30
+ "caption": {
31
+ "_comment": "底部白边 2 行 caption(cells 结束于 y=1460,留 200px)",
32
+ "font": "shared/fonts/MaShanZheng-Regular.ttf",
33
+ "size": 60,
34
+ "color": "#FFB940",
35
+ "stroke_w": 5,
36
+ "stroke_color": "#D63D3D",
37
+ "lines": [
38
+ {"position": [621, 1530], "anchor": "mm"},
39
+ {"position": [621, 1610], "anchor": "mm"}
40
+ ]
41
+ }
42
+ }
43
+ }
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env python3
2
+ """multigrid-poster compose — Pillow 版渲染器
3
+
4
+ 输入:layout.json + N 张 cell 图片 + 标题文字 + caption 文字
5
+ 输出:1242×1660 PNG(小红书封面标准)
6
+
7
+ 用法:
8
+ python compose.py \
9
+ --layout <SKILL_DIR>/layouts/2x2.json \
10
+ --cells cell_0.png cell_1.png cell_2.png cell_3.png \
11
+ --title-line "26岁一个人创业" \
12
+ --title-line "跨境电商月入10w+" \
13
+ --caption-line "只需要一部手机就可以完成!" \
14
+ --caption-line "跨境人的必备app推荐" \
15
+ --output cover.png
16
+
17
+ 依赖:Pillow (容器自带,无需额外安装)
18
+ 字体:从 SKILL_DIR/shared/fonts/ 加载(layout.json 里指定)
19
+ """
20
+ import argparse
21
+ import json
22
+ import sys
23
+ from pathlib import Path
24
+ from PIL import Image, ImageDraw, ImageFont
25
+
26
+
27
+ def compose(layout_path: Path, cell_paths: list[Path],
28
+ title_lines: list[str], caption_lines: list[str],
29
+ output_path: Path) -> Path:
30
+ """根据 layout.json 渲染海报。
31
+
32
+ layout.json 字段:
33
+ canvas_size [w, h]
34
+ cells.positions [[x,y], ...] cell 左上角
35
+ cells.sizes [[w,h], ...] cell 尺寸
36
+ text_zones.title {lines: [...], size, color, stroke_w, stroke_color, font}
37
+ text_zones.caption {同上}
38
+ """
39
+ layout = json.loads(layout_path.read_text(encoding="utf-8"))
40
+ # 约定:layout 必须放在 <skill>/layouts/ 下,字体路径相对 <skill>/ 解析
41
+ skill_dir = layout_path.parent.parent
42
+
43
+ # 文本行数 vs layout 配置行数:行多了 zip 会静默截断,显式 fail-fast
44
+ # 错误信息用英文 — sys.exit 走 stderr,Windows GBK locale 下中文会乱码
45
+ for zone_key, lines in [("title", title_lines), ("caption", caption_lines)]:
46
+ zone = layout.get("text_zones", {}).get(zone_key)
47
+ if not zone:
48
+ continue
49
+ max_lines = len(zone["lines"])
50
+ if len(lines) > max_lines:
51
+ sys.exit(f"too many {zone_key} lines: got {len(lines)}, layout supports {max_lines}")
52
+
53
+ # Canvas
54
+ canvas = Image.new("RGB", tuple(layout["canvas_size"]), "white")
55
+
56
+ # Cells
57
+ cell_count = len(layout["cells"]["positions"])
58
+ if len(cell_paths) != cell_count:
59
+ sys.exit(f"cell count mismatch: layout expects {cell_count}, got {len(cell_paths)}")
60
+ for cp, pos, size in zip(cell_paths,
61
+ layout["cells"]["positions"],
62
+ layout["cells"]["sizes"]):
63
+ img = Image.open(cp).convert("RGB").resize(tuple(size), Image.LANCZOS)
64
+ canvas.paste(img, tuple(pos))
65
+
66
+ draw = ImageDraw.Draw(canvas)
67
+
68
+ # Text zones (title + caption 通用绘制)
69
+ for zone_key, lines in [("title", title_lines), ("caption", caption_lines)]:
70
+ if zone_key not in layout["text_zones"]:
71
+ continue
72
+ zone = layout["text_zones"][zone_key]
73
+ font_path = skill_dir / zone["font"]
74
+ font = ImageFont.truetype(str(font_path), zone["size"])
75
+ for text, line_cfg in zip(lines, zone["lines"]):
76
+ if not text:
77
+ continue
78
+ draw.text(
79
+ tuple(line_cfg["position"]), text,
80
+ fill=zone["color"], font=font,
81
+ stroke_width=zone.get("stroke_w", 0),
82
+ stroke_fill=zone.get("stroke_color", zone["color"]),
83
+ anchor=line_cfg.get("anchor", "la"),
84
+ )
85
+
86
+ canvas.save(output_path, optimize=True)
87
+ return output_path
88
+
89
+
90
+ def main():
91
+ ap = argparse.ArgumentParser(description=__doc__,
92
+ formatter_class=argparse.RawDescriptionHelpFormatter)
93
+ ap.add_argument("--layout", required=True, type=Path,
94
+ help="layout.json 路径(如 SKILL_DIR/layouts/2x2.json)")
95
+ ap.add_argument("--cells", required=True, nargs="+", type=Path,
96
+ help="N 张 cell 图片路径,顺序对应 layout.cells.positions")
97
+ ap.add_argument("--title-line", action="append", default=[],
98
+ help="标题行(可重复)")
99
+ ap.add_argument("--caption-line", action="append", default=[],
100
+ help="底部 caption 行(可重复)")
101
+ ap.add_argument("--output", required=True, type=Path,
102
+ help="输出 PNG 路径")
103
+ args = ap.parse_args()
104
+
105
+ out = compose(
106
+ layout_path=args.layout,
107
+ cell_paths=args.cells,
108
+ title_lines=args.title_line,
109
+ caption_lines=args.caption_line,
110
+ output_path=args.output,
111
+ )
112
+ print(f"saved {out} ({out.stat().st_size // 1024} KB)")
113
+
114
+
115
+ if __name__ == "__main__":
116
+ main()