Skip to content

Commit 09fc64a

Browse files
committed
release: 0.3.5 路由反转 + Mermaid/PlantUML 高清化
修复两个用户实际使用中暴露的问题: 1. 路由策略反转:之前默认走 Mermaid,且用户明示 「用 image2 画」时还会被关键词路由偷换。现在: - 用户明示引擎 → 严格按指定(绝不偷换,守则 #11) - 没指定时仅工程图表(流程/时序/UML 等)走代码引擎 - 默认走 AI 生图(视觉效果好) - 模糊场景不再反问,直接 AI 生图 2. Mermaid/PlantUML 出图清晰度提升: - Mermaid URL 自动带 ?width=1600&scale=2,出 3200 宽 - PlantUML 自动注入 skinparam dpi 200(之前默认 96) - 用户已写过 dpi 时不重复注入 - SVG 输出天然矢量清晰,不做处理 - 新增 --scale --width(mermaid)、--dpi(plantuml) 测试:8 个新测试覆盖高清参数和注入逻辑,56/56 通过。 真实烟测:两个引擎出图明显锐利,中文文字清晰可读。 参考:mermaid.ink 官方文档 width/scale 参数, plantuml.com skinparam dpi 标准用法。
1 parent b94c336 commit 09fc64a

8 files changed

Lines changed: 267 additions & 21 deletions

File tree

CHANGELOG.md

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,37 @@
44

55
## [Unreleased]
66

7+
## [0.3.5] - 2026-05-24
8+
9+
### 重要:路由策略反转
10+
11+
之前默认走 Mermaid(视觉一般),且用户明示「用 image2 画」时还会被关键词路由偷换成 Mermaid。
12+
现在:
13+
14+
- **第 0 优先级**:用户明示引擎(「用 image2 画」「用 mermaid 画」「用 plantuml 画」)→ 严格按指定,**绝不偷换**
15+
- **第 1 优先级**:用户没指定时,只有「流程图 / 时序图 / 类图 / UML / 状态机 / 用例图 / 架构图 / 甘特图 / 思维导图」等明确**工程图表**才走 Mermaid/PlantUML
16+
- **默认**:其他所有视觉需求(照片、插画、海报、封面、原型、示意图、模糊「画一张」请求)都走 AI 生图
17+
- 不再「反问用户用哪个引擎」,模糊场景直接用 AI 生图(视觉效果好)
18+
19+
### 改进:清晰度
20+
21+
之前 Mermaid/PlantUML 出图都有点糊,现在调清晰度参数为默认行为:
22+
23+
- **Mermaid**:URL 自动带 `?width=1600&scale=2`,PNG 出图 3200 宽,清晰度约 4x
24+
- **PlantUML**:自动在 `@startuml` 后注入 `skinparam dpi 200`,PNG 像素约 2x(默认 dpi 96)
25+
- 用户已在源码自己写过 `skinparam dpi` 时不重复注入
26+
- SVG 输出(`--format svg`)天然矢量清晰,不做这些处理
27+
28+
新增 CLI 参数:
29+
30+
- Mermaid: `--scale 1|2|3` 默认 `2``--width <100-4000>` 默认 `1600`
31+
- PlantUML: `--dpi <50-600>` 默认 `200`
32+
33+
### 守则
34+
35+
- 新增守则 #11**用户明示引擎绝不偷换**(违例时 Claude 必须按用户指定执行,再事后委婉提醒)
36+
- 新增守则 #12:清晰度参数已设合理默认,不要主动调小
37+
738
## [0.3.4] - 2026-05-24
839

940
### 新增
@@ -72,7 +103,8 @@
72103
- `update` 命令本身不会就地升级已全局安装的 npm 包,需手动 `npm install -g @openx123/universal-image-skill@latest` 后再次执行 `install`
73104
- Mermaid / PlantUML 源码会上传至各自公共服务,敏感场景请通过 `MERMAID_INK_URL` / `PLANTUML_SERVER_URL` 切换到自建实例
74105

75-
[Unreleased]: https://github.com/openx123/universal-image-skill/compare/v0.3.4...HEAD
106+
[Unreleased]: https://github.com/openx123/universal-image-skill/compare/v0.3.5...HEAD
107+
[0.3.5]: https://github.com/openx123/universal-image-skill/releases/tag/v0.3.5
76108
[0.3.4]: https://github.com/openx123/universal-image-skill/releases/tag/v0.3.4
77109
[0.3.3]: https://github.com/openx123/universal-image-skill/releases/tag/v0.3.3
78110
[0.3.2]: https://github.com/openx123/universal-image-skill/releases/tag/v0.3.2

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@openx123/universal-image-skill",
3-
"version": "0.3.4",
3+
"version": "0.3.5",
44
"description": "Claude Code 万能生图 Skill:Mermaid / PlantUML / AI 生图,一键安装",
55
"type": "module",
66
"bin": {

skill/SKILL.md

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -23,22 +23,30 @@ description: 万能生图 Skill。根据用户意图自动选用 Mermaid / Plant
2323

2424
## 2. 路由决策表(关键)
2525

26-
按下表匹配,优先级从上到下,命中即停。
26+
**第 0 优先级****用户明示了引擎就严格按用户的来,不要换**
27+
- 「用 image2 画」「让 GPT 画」「用 AI 生图」「画一张照片/插画/原型图」→ 必须用 AI 生图,**绝不能因为「画的是流程图」就偷换成 Mermaid**
28+
- 「用 mermaid 画」「画一个 mermaid 时序图」→ 必须用 Mermaid
29+
- 「用 plantuml 画」「画一个 PlantUML 架构」→ 必须用 PlantUML
2730

28-
| 用户意图关键词 | 引擎 | 脚本 |
29-
| -------------------------------------------------------------------------------------- | ------------- | -------------------------- |
30-
| 流程图、时序图、状态机、状态图、类图、甘特图、思维导图、用户旅程图、git 分支图 | **Mermaid** | `render-mermaid.mjs` |
31-
| 用例图、组件图、部署图、ER 图、复杂时序图(含 alt/loop/par)、对象图、活动图、思维导图(带样式) | **PlantUML** | `render-plantuml.mjs` |
32-
| 云架构(AWS / Azure / GCP / K8s)、C4 架构(系统/容器/组件/代码)、系统架构图(带图标) | **PlantUML + include** | `render-plantuml.mjs` |
33-
| 真实照片、写实、插画、艺术、油画、赛博朋克、产品概念图、海报、营销素材、封面、Logo、IP 形象 | **AI 生图** | `render-image.mjs` |
34-
| 模糊:「画个图」「来张图」未指明类型 | **反问用户** ||
31+
**第 1 优先级**:用户没指定引擎时,按内容类型路由。**默认走 AI 生图**(视觉效果好),仅当用户明确要"工程图表"才走代码引擎。
32+
33+
| 用户意图 | 引擎 | 脚本 |
34+
| --------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | -------------------------- |
35+
| 流程图(flowchart)、状态图(FSM)、甘特图、用户旅程图、思维导图、git 分支图——**Mermaid 强项** | **Mermaid** | `render-mermaid.mjs` |
36+
| 时序图(**含 alt/loop/par 等复杂分支**)、类图、用例图、组件图、部署图、ER 图、对象图、活动图、C4 架构、云架构(AWS/Azure/GCP/K8s)——**PlantUML 强项** | **PlantUML** | `render-plantuml.mjs` |
37+
| 其他一切视觉需求:照片、插画、艺术、海报、封面、IP 形象、Logo、产品概念图、营销素材、原型图、示意图、概念图,**以及所有模糊「画一张」类请求** | **AI 生图(默认)** | `render-image.mjs` |
38+
39+
**为什么 AI 生图是默认而不是「反问」**:用户说「画一张 X」往往就是想要好看的视觉成品。AI 生图视觉效果远好于 Mermaid/PlantUML(后者是工程线框风格),且 image2 对"原型图/示意图"也能给出可用的视觉草稿。代码引擎只在用户明确需要**精确、结构化、可文本编辑**的图表时才用。
3540

3641
**反例(错误路由,禁止)**
3742

38-
- 不要用 Mermaid 画 ER 图:Mermaid 的 ER 语法太弱,应用 PlantUML
39-
- 不要用 PlantUML 画海报/营销图:PlantUML 没有写实样式,应用 AI 生图
40-
- 不要用 AI 生图画精确的流程图/UML:不可控、文字会乱、关系会错,应用 Mermaid / PlantUML
41-
- 不要用 Mermaid 画云架构图:缺少官方图标库,应用 PlantUML + AWS/Azure Icons
43+
- 用户说「用 image2 画一张登录流程图」→ **必须**走 AI 生图(用户明示了引擎)。绝不要因为关键词「流程图」改路由
44+
- 用户说「用 mermaid 画一张赛博朋克城市」→ **必须**走 Mermaid(用户明示了引擎,结果可能不好看,那是用户的选择)
45+
- 用户说「画一张登录流程图」(**没指定引擎**)→ 走 Mermaid("流程图"是它的强项)
46+
- 用户说「画一张登录页原型图」(**没指定引擎**)→ 走 AI 生图("原型图"想要视觉成品,不是流程图)
47+
- 用户说「画一张图给我看看」(模糊)→ 默认走 AI 生图(不再反问)
48+
- 不要用 Mermaid 画 ER 图(语法弱),用 PlantUML
49+
- 不要用 Mermaid 画云架构图(无图标库),用 PlantUML + AWS/Azure Icons
4250

4351
---
4452

@@ -55,6 +63,9 @@ description: 万能生图 Skill。根据用户意图自动选用 Mermaid / Plant
5563
| `--output-dir <dir>` | 全部 | 图片输出目录,默认 `./output` |
5664
| `--source-dir <dir>` | mermaid / plantuml | 源码(.mmd/.puml)输出目录,默认同 `--output-dir`。文档模式专用 |
5765
| `--format png\|svg` | mermaid / plantuml | 输出格式,默认 png |
66+
| `--scale 1\|2\|3` | mermaid | PNG 高清倍数,默认 `2`(已经够清晰,**不要随便改小**|
67+
| `--width <px>` | mermaid | PNG 渲染宽度,默认 `1600`,配合 scale 出 3200 高清 |
68+
| `--dpi <50-600>` | plantuml | PNG 渲染 dpi,默认 `200`(约 2x 清晰度,**不要随便改小**|
5869
| `--format png\|jpeg\|webp` | image | 输出格式,默认 png |
5970
| `--ratio <ratio>` | image | **推荐用这个**,6 个比例预设(见 4.5 节比例表) |
6071
| `--tier 1k\|2k\|4k` | image | 配合 `--ratio` 选档位,默认 `2k`(主流推荐) |
@@ -373,3 +384,8 @@ EOF
373384
- 第一反应:**「这通常是中转站排队或网络抖动,正在自动重试一次」**(一句话告知用户)
374385
- 然后用**完全相同的命令**再调一次脚本
375386
- 仍失败再展示错误细节并征求用户意见,**不要**第二次就改参数或换引擎
387+
11. **用户明示引擎绝不偷换**。识别明示词:「用 X 画」「用 X 生成」「让 X 来画」「换 X」(X 可以是 image2 / gpt / AI / mermaid / plantuml)。
388+
- 命中明示词后,**忽略所有路由表**,直接走用户指定的引擎,哪怕内容明显是别的引擎更合适
389+
- 如果用户指定的引擎对该内容做不出好效果(如用 Mermaid 画照片),先做完再委婉提醒「你指定了 Mermaid,画照片它真不擅长,下次可以不指定让我自动选 AI 生图」
390+
- **禁止**理由:「检测到流程图意图,所以改用 Mermaid 了」——这是把路由规则凌驾于用户明示之上,错。
391+
12. **Mermaid/PlantUML 的清晰度参数(--scale / --width / --dpi)已经设了合理默认值****不要因为图片"够看"就主动调小**。只有用户说「文件太大了」「想要更高清」时才调整。

skill/scripts/render-mermaid.mjs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,22 @@ async function main() {
7575
}
7676

7777
// mermaid.ink: /img/<BASE64URL> 或 /svg/<BASE64URL>
78+
// 高清参数:?width=W&scale=S,scale 必须搭配 width 才生效(mermaid.ink 文档限制)
79+
// 默认 width=1600 scale=2 → 出图 3200 宽,对绝大多数文档/聊天清晰够用
7880
const b64url = Buffer.from(source, 'utf8').toString('base64url')
79-
const path = format === 'svg' ? `/svg/${b64url}` : `/img/${b64url}`
81+
let path = format === 'svg' ? `/svg/${b64url}` : `/img/${b64url}`
82+
83+
if (format !== 'svg') {
84+
const width = args.width ? Number(args.width) : 1600
85+
const scale = args.scale ? Number(args.scale) : 2
86+
if (!Number.isFinite(width) || width < 100 || width > 4000) {
87+
throw new Error(`--width 需要 100-4000 之间的整数,收到:${args.width}`)
88+
}
89+
if (!Number.isFinite(scale) || scale < 1 || scale > 3) {
90+
throw new Error(`--scale 仅支持 1 / 2 / 3,收到:${args.scale}`)
91+
}
92+
path += `?type=png&width=${width}&scale=${scale}`
93+
}
8094
const url = `${base}${path}`
8195

8296
process.stderr.write(`[mermaid] GET ${url}\n`)

skill/scripts/render-plantuml.mjs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,18 @@ function ensureUmlWrappers(source) {
5757
return `@startuml\n${source}\n@enduml\n`
5858
}
5959

60+
// 在 @startuml 之后第一行注入 skinparam dpi(如果用户没自己写),提升 PNG 清晰度
61+
// PlantUML 默认 dpi 96,提到 200 可让线条/文字明显更清晰,svg 不需要
62+
function injectDpi(source, dpi) {
63+
if (!dpi || dpi === 96) return source
64+
// 用户已经显式设过就不动
65+
if (/^\s*skinparam\s+dpi\s+\d+/im.test(source)) return source
66+
return source.replace(
67+
/(@start[a-z]+[^\n]*\n)/i,
68+
`$1skinparam dpi ${dpi}\n`,
69+
)
70+
}
71+
6072
async function main() {
6173
const start = Date.now()
6274
const args = parseArgs(process.argv.slice(2))
@@ -71,7 +83,13 @@ async function main() {
7183
if (!rawSource || !rawSource.trim()) {
7284
throw new Error('PlantUML 源码为空')
7385
}
74-
const source = ensureUmlWrappers(rawSource)
86+
// dpi 默认 200(约 2x 清晰度);svg 输出不需要 dpi(矢量天然清晰)
87+
const dpi = args.dpi ? Number(args.dpi) : 200
88+
if (!Number.isFinite(dpi) || dpi < 50 || dpi > 600) {
89+
throw new Error(`--dpi 需要 50-600 之间的整数,收到:${args.dpi}`)
90+
}
91+
const wrapped = ensureUmlWrappers(rawSource)
92+
const source = format === 'svg' ? wrapped : injectDpi(wrapped, dpi)
7593

7694
// ~h<HEX> 是 PlantUML 服务端官方支持的明文 hex 编码
7795
const hex = Buffer.from(source, 'utf8').toString('hex')

skill/version.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"version": "0.3.4",
2+
"version": "0.3.5",
33
"installedAt": null,
44
"source": "@openx123/universal-image-skill",
55
"channel": "stable"

tests/render-mermaid.test.mjs

Lines changed: 86 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,12 @@ async function startMermaidMock({ status = 200, body = TINY_PNG, svgBody = TINY_
1919
return await mockHttpServer({
2020
routes: {
2121
'/img/': (req, res) => {
22-
// path 形如 /img/<base64url>
23-
const segment = decodeURIComponent(req.url.slice('/img/'.length))
24-
// 把 base64url 还原成 base64
22+
// path 形如 /img/<base64url>?type=png&width=...&scale=...
23+
// 先剥离查询串再 base64url 解码
24+
const afterPrefix = req.url.slice('/img/'.length)
25+
const qIdx = afterPrefix.indexOf('?')
26+
const segmentRaw = qIdx === -1 ? afterPrefix : afterPrefix.slice(0, qIdx)
27+
const segment = decodeURIComponent(segmentRaw)
2528
const std = segment.replace(/-/g, '+').replace(/_/g, '/')
2629
const padded = std + '=='.slice((std.length + 2) % 4)
2730
let decoded
@@ -214,6 +217,81 @@ describe('render-mermaid.mjs', () => {
214217
}
215218
})
216219

220+
it('默认调用带 width=1600 scale=2 高清参数', async () => {
221+
const mock = await startMermaidMock()
222+
try {
223+
await withTempDir(async (dir) => {
224+
const { exitCode } = await runScript(SCRIPT, [
225+
'--inline', 'graph TD; A-->B',
226+
'--output-dir', dir
227+
], { env: { MERMAID_INK_URL: mock.url } })
228+
229+
assert.equal(exitCode, 0)
230+
const imgReq = mock.requests.find((r) => r.url && r.url.startsWith('/img/'))
231+
assert.ok(imgReq, '应有 /img/ 请求')
232+
assert.match(imgReq.url, /[?&]width=1600/)
233+
assert.match(imgReq.url, /[?&]scale=2/)
234+
})
235+
} finally {
236+
await mock.close()
237+
}
238+
})
239+
240+
it('--scale 3 --width 2000 透传到 URL', async () => {
241+
const mock = await startMermaidMock()
242+
try {
243+
await withTempDir(async (dir) => {
244+
const { exitCode } = await runScript(SCRIPT, [
245+
'--inline', 'graph TD; A-->B',
246+
'--output-dir', dir,
247+
'--scale', '3',
248+
'--width', '2000'
249+
], { env: { MERMAID_INK_URL: mock.url } })
250+
251+
assert.equal(exitCode, 0)
252+
const imgReq = mock.requests.find((r) => r.url && r.url.startsWith('/img/'))
253+
assert.match(imgReq.url, /[?&]width=2000/)
254+
assert.match(imgReq.url, /[?&]scale=3/)
255+
})
256+
} finally {
257+
await mock.close()
258+
}
259+
})
260+
261+
it('--scale 越界(如 5)报错退出', async () => {
262+
await withTempDir(async (dir) => {
263+
const { exitCode, lastJsonLine } = await runScript(SCRIPT, [
264+
'--inline', 'graph TD; A-->B',
265+
'--output-dir', dir,
266+
'--scale', '5'
267+
], { env: { MERMAID_INK_URL: 'http://127.0.0.1:1' } })
268+
269+
assert.equal(exitCode, 1)
270+
assert.equal(lastJsonLine.ok, false)
271+
assert.match(lastJsonLine.error.message, /scale/i)
272+
})
273+
})
274+
275+
it('SVG 输出不带 width/scale 查询参数', async () => {
276+
const mock = await startMermaidMock()
277+
try {
278+
await withTempDir(async (dir) => {
279+
const { exitCode } = await runScript(SCRIPT, [
280+
'--inline', 'graph TD; A-->B',
281+
'--output-dir', dir,
282+
'--format', 'svg'
283+
], { env: { MERMAID_INK_URL: mock.url } })
284+
285+
assert.equal(exitCode, 0)
286+
const svgReq = mock.requests.find((r) => r.url && r.url.startsWith('/svg/'))
287+
assert.ok(svgReq)
288+
assert.ok(!svgReq.url.includes('?'), 'svg 不应带查询参数')
289+
})
290+
} finally {
291+
await mock.close()
292+
}
293+
})
294+
217295
it('base64url 编码正确:mock 端能 decode 回原文', async () => {
218296
const mock = await startMermaidMock()
219297
try {
@@ -228,7 +306,11 @@ describe('render-mermaid.mjs', () => {
228306
// 找到 /img/ 请求并断言解码回的原文等于 inline
229307
const imgReq = mock.requests.find((r) => r.url && r.url.startsWith('/img/'))
230308
assert.ok(imgReq, '应当至少有一次 /img/ 请求')
231-
const segment = decodeURIComponent(imgReq.url.slice('/img/'.length))
309+
// 剥离查询串再解码(高清参数化后 URL 形如 /img/<b64>?type=png&width=...&scale=...)
310+
const afterPrefix = imgReq.url.slice('/img/'.length)
311+
const qIdx = afterPrefix.indexOf('?')
312+
const segmentRaw = qIdx === -1 ? afterPrefix : afterPrefix.slice(0, qIdx)
313+
const segment = decodeURIComponent(segmentRaw)
232314
const std = segment.replace(/-/g, '+').replace(/_/g, '/')
233315
const padded = std + '=='.slice((std.length + 2) % 4)
234316
const decoded = Buffer.from(padded, 'base64').toString('utf8')

0 commit comments

Comments
 (0)