dsh-feiyu-wardrobe 0.1.0 → 0.2.0

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.0 — 2026-10-30
4
+
5
+ 「换装是视觉性的」:补齐出图能力,可直接发 QQ 群 / 评论区。
6
+
7
+ ### 新增
8
+
9
+ - `/fish png [尺寸]`:渲染当前穿搭的 PNG(默认 768,可选 256–1600)
10
+ - `dsh-feiyu-report --png <path>` / `--size <像素>`:CLI 直接出图
11
+ - PNG 合成:360×360 场景 + 底部水印条(repo 地址 + 鱼名 + 饲养天数)
12
+ - 依赖 `@resvg/resvg-js`(预编译二进制,本地渲染,无浏览器/无网络)
13
+ - 新增 `screenshots.json`(插件市场 / awesome 列表详情页展示卡图)
14
+ - README 增加「接 QQ / 聊天机器人」章节
15
+
16
+ ### 验证
17
+
18
+ - `npm run verify`:tsc --checkJs + 53 tests
19
+
3
20
  ## 0.1.0 — 2026-10-30
4
21
 
5
22
  第一个公开版本:把 DSH 的 token 花销变成大肥鱼的衣柜。
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  > 把 DSH 的 token 账单,变成一条鱼的衣柜。
8
8
 
9
- 当前版本:`0.1.0`
9
+ 当前版本:`0.2.0`
10
10
 
11
11
  DeepSeek Harness 插件:agent 每花掉一次 token,你的大肥鱼就**吃一顿饭**;一顿顿饭攒成**鱼粮币**,鱼粮币解锁 **21 件原创 SVG 穿搭**;`/fish report` 生成一张 **1080×1440 的饲养报告卡**,可以直接发小红书 / QQ 群。
12
12
 
@@ -14,11 +14,14 @@ DeepSeek Harness 插件:agent 每花掉一次 token,你的大肥鱼就**吃
14
14
  - 🎨 **原创形象**:胖鲸 + 21 件手绘 SVG 单品(帽子 / 面部 / 颈部 / 身体 / 手持 / 背景),不打包任何画师二创素材
15
15
  - 🔒 **纯本地**:零额外模型调用、零网络请求、不存聊天原文,只存聚合数字
16
16
  - 📸 **可晒卡**:饲养天数、投喂金额、吃掉 token、亲密度、心情、近 7 天伙食、今日穿搭
17
+ - 🖼️ **可视化出图**:`/fish png` 或 `dsh-feiyu-report --png` 直接渲染 PNG(默认 768,带 repo 水印),无需浏览器,可直接发 QQ 群 / B站评论区 / 小红书
17
18
 
18
19
  ![饲养报告示例](https://raw.githubusercontent.com/GIT121995/dsh-feiyu-wardrobe/main/docs/preview/card.png)
19
20
 
20
21
  ![衣柜目录](https://raw.githubusercontent.com/GIT121995/dsh-feiyu-wardrobe/main/docs/preview/wardrobe.png)
21
22
 
23
+ ![出图示例](https://raw.githubusercontent.com/GIT121995/dsh-feiyu-wardrobe/main/docs/preview/fish-demo.png)
24
+
22
25
  > 名字与形象都是原创,属于社区玩梗项目,**不是 DeepSeek 官方产品**;「大肥鱼」一词来自社区对 DeepSeek 蓝色鲸鱼标识的可爱误认。
23
26
 
24
27
  ---
@@ -51,6 +54,7 @@ dsh --profile web
51
54
  | `/fish wear <名字\|id>` | 换上已拥有的单品 |
52
55
  | `/fish takeoff <部位>` | 脱掉某个部位(`bg` / `body` / `neck` / `face` / `hand` / `hat`) |
53
56
  | `/fish rename <名字>` | 给鱼改名 |
57
+ | `/fish png [尺寸]` | 渲染当前穿搭的 PNG(默认 768;可直接发群) |
54
58
  | `/fish report` | 生成饲养报告卡(HTML,1080×1440) |
55
59
  | `/fish help` | 帮助 |
56
60
 
@@ -67,13 +71,17 @@ dsh --profile web
67
71
  ## 报告卡 CLI
68
72
 
69
73
  ```sh
70
- dsh-feiyu-report # 用本机 state 生成卡片
71
- dsh-feiyu-report --demo # 没有鱼也能预览一张演示卡
74
+ dsh-feiyu-report # 用本机 state 生成 HTML 卡片
75
+ dsh-feiyu-report --png fish.png # 直接出 PNG(QQ 群 / 评论区可用)
76
+ dsh-feiyu-report --png fish.png --size 1024
77
+ dsh-feiyu-report --demo --png demo.png
72
78
  dsh-feiyu-report --open # 生成后用默认浏览器打开
73
- dsh-feiyu-report --out card.html # 自定义输出路径
79
+ dsh-feiyu-report --out card.html # 自定义 HTML 输出路径
74
80
  dsh-feiyu-report --sheet # 渲染整个衣柜目录页
75
81
  ```
76
82
 
83
+ 出图是纯本地 SVG → PNG(`@resvg/resvg-js`,预编译二进制),不调用浏览器、不联网。
84
+
77
85
  ## 配置
78
86
 
79
87
  在 profile 的 `cordis.patch.yml` 里覆盖(都是可选项):
@@ -99,11 +107,22 @@ dsh-feiyu-report --sheet # 渲染整个衣柜目录页
99
107
  - **非官方**:本项目是社区插件,不属于 DeepSeek 官方项目;「大肥鱼」相关梗的归属属于社区。
100
108
  - 后续开放「皮肤包」格式时,同样只支持**本地加载**用户自己的素材,不代分发未授权作品。
101
109
 
110
+ ## 接 QQ / 聊天机器人(出图能力)
111
+
112
+ 「换装」是视觉性的,所以核心能力是**出图**而不是出文字:
113
+
114
+ - 任何能执行命令的 QQ 机器人(LangBot、DSH + QQ 适配器等)都可以在 `wear` 之后调一次
115
+ `dsh-feiyu-report --png /tmp/fish.png`,把 PNG 直接发到群里;
116
+ - DSH 内也可以用 `/fish png` 生成文件,再由机器人发送;
117
+ - 图片带仓库水印,别人转发一次就是一次曝光。
118
+
119
+ > 注意:本项目只负责「渲染并给出 PNG 文件」,QQ 平台的接入、账号风控由你选用的机器人框架负责。
120
+
102
121
  ## 兼容性与测试
103
122
 
104
123
  - DSH `>= 0.1.0`(本版在 `0.2.1-rc.2` 实测)
105
124
  - Node.js `>= 22.5`,零运行时依赖
106
- - `npm run verify` = `tsc --checkJs` 类型检查 + 49 个单元/集成测试(定价、经济、状态迁移、渲染、命令、CLI)
125
+ - `npm run verify` = `tsc --checkJs` 类型检查 + 53 个单元/集成测试(定价、经济、状态迁移、渲染、PNG、命令、CLI)
107
126
 
108
127
  ```sh
109
128
  npm run verify
@@ -111,7 +130,7 @@ npm run verify
111
130
 
112
131
  ## 路线图
113
132
 
114
- - **v0.2**:DSH Web 面板(页内衣橱 UI)、皮肤包格式(`.dshskin`,本地加载)、心情联动穿搭
133
+ - **v0.3**:DSH Web 面板(页内衣橱 UI)、皮肤包格式(`.dshskin`,本地加载)、心情联动穿搭
115
134
  - **v0.3**:多鱼塘(多会话鱼)、每日小任务、社区衣服交换码
116
135
  - 更远:把 `dsh-memory-gate` 的记忆做成「鱼记得你说过什么」,但依然只读聚合摘要
117
136
 
@@ -13,12 +13,13 @@
13
13
  import { existsSync, readFileSync } from 'node:fs';
14
14
  import { spawn } from 'node:child_process';
15
15
  import { dirname, join, resolve } from 'node:path';
16
+ import { renderFishPng } from '../lib/image.js';
16
17
  import { renderWardrobeSheetHtml } from '../lib/render.js';
17
18
  import { createDemoState } from '../lib/demo.js';
18
19
  import { validateConfig } from '../lib/config.js';
19
20
  import { loadState } from '../lib/state.js';
20
21
  import { defaultReportPath, writeReportCard } from '../lib/report.js';
21
- import { atomicWriteFile } from '../lib/util.js';
22
+ import { atomicWriteBuffer, atomicWriteFile } from '../lib/util.js';
22
23
 
23
24
  const PACKAGE = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
24
25
  const VERSION = PACKAGE.version ?? '0.0.0';
@@ -29,7 +30,9 @@ const HELP = `dsh-feiyu-report — 大肥鱼衣橱饲养报告卡
29
30
  dsh-feiyu-report 从本机 state 生成报告卡
30
31
  dsh-feiyu-report --demo 用演示鱼生成一张预览卡
31
32
  dsh-feiyu-report --sheet 渲染整个衣柜目录页
32
- dsh-feiyu-report --out <path> 指定输出路径
33
+ dsh-feiyu-report --png <path> 渲染当前穿搭的 PNG(可直接发 QQ 群)
34
+ dsh-feiyu-report --size <像素> PNG 边长(256–1600,默认 768)
35
+ dsh-feiyu-report --out <path> 指定 HTML 输出路径
33
36
  dsh-feiyu-report --state <path> 指定 state.json
34
37
  dsh-feiyu-report --open 生成后用默认浏览器打开
35
38
  dsh-feiyu-report --version 版本号
@@ -47,6 +50,8 @@ function parseArgs(argv) {
47
50
  help: false,
48
51
  version: false,
49
52
  out: null,
53
+ png: null,
54
+ size: null,
50
55
  state: null,
51
56
  };
52
57
  for (let index = 0; index < argv.length; index += 1) {
@@ -74,6 +79,16 @@ function parseArgs(argv) {
74
79
  index += 1;
75
80
  break;
76
81
  }
82
+ case '--png': {
83
+ options.png = argv[index + 1] ?? null;
84
+ index += 1;
85
+ break;
86
+ }
87
+ case '--size': {
88
+ options.size = argv[index + 1] ?? null;
89
+ index += 1;
90
+ break;
91
+ }
77
92
  case '--state': {
78
93
  options.state = argv[index + 1] ?? null;
79
94
  index += 1;
@@ -134,6 +149,20 @@ function main() {
134
149
  const loaded = loadState(config.statePath, config);
135
150
  state = loaded.state;
136
151
  }
152
+ if (options.png) {
153
+ const sizeValue = options.size == null ? undefined : Number(options.size);
154
+ if (sizeValue !== undefined && (!Number.isFinite(sizeValue) || sizeValue < 256 || sizeValue > 1600)) {
155
+ throw new Error('--size 需要 256–1600 之间的整数');
156
+ }
157
+ const png = renderFishPng(state, config, {
158
+ ...(sizeValue === undefined ? {} : { size: sizeValue }),
159
+ });
160
+ const pngPath = resolve(options.png);
161
+ atomicWriteBuffer(pngPath, png);
162
+ process.stdout.write(`${pngPath}\n`);
163
+ if (options.open) openInBrowser(pngPath);
164
+ return;
165
+ }
137
166
  const outPath = writeReportCard(state, config, {
138
167
  ...(options.out ? { outPath: resolve(options.out) } : {}),
139
168
  });
Binary file
package/lib/commands.js CHANGED
@@ -6,8 +6,10 @@
6
6
 
7
7
  import { ITEMS, RARITIES, SLOT_ORDER, findItem, rarityLabel, slotLabel } from './catalog.js';
8
8
  import { summaryOf } from './economy.js';
9
+ import { renderFishPng } from './image.js';
9
10
  import { writeReportCard } from './report.js';
10
- import { formatCNY, formatCoins, formatTokens } from './util.js';
11
+ import { atomicWriteBuffer, formatCNY, formatCoins, formatTokens } from './util.js';
12
+ import { dirname, join } from 'node:path';
11
13
 
12
14
  const USAGE = [
13
15
  '/fish 看鱼今天的状态',
@@ -16,6 +18,7 @@ const USAGE = [
16
18
  '/fish wear <名字|id> 换上已拥有的衣服',
17
19
  '/fish takeoff <部位> 脱掉某个部位(bg/body/neck/face/hand/hat)',
18
20
  '/fish rename <名字> 给鱼改名',
21
+ '/fish png [尺寸] 渲染当前穿搭的 PNG(默认 768,可直接发群)',
19
22
  '/fish report 生成一张可晒的饲养报告卡',
20
23
  '/fish help 显示这份说明',
21
24
  ].join('\n');
@@ -114,6 +117,20 @@ export function executeFishCommand(runtime, rawInput, options = {}) {
114
117
  const result = runtime.rename(tokens.join(' '), now);
115
118
  return { kind: result.ok ? 'success' : 'error', text: result.message };
116
119
  }
120
+ case 'png':
121
+ case 'image':
122
+ case 'img': {
123
+ const requested = Number(tokens[0]);
124
+ const size = Number.isFinite(requested) && requested >= 256 ? Math.min(1600, Math.round(requested)) : undefined;
125
+ const png = renderFishPng(runtime.state, runtime.config, { now, ...(size ? { size } : {}) });
126
+ const stamp = new Date(now).toISOString().replace(/[:.]/g, '-').slice(0, 19);
127
+ const outPath = join(dirname(runtime.config.statePath), `fish-${stamp}.png`);
128
+ atomicWriteBuffer(outPath, png);
129
+ return {
130
+ kind: 'success',
131
+ text: `出图了:${outPath}\n直接把这张 PNG 发到 QQ 群 / B站评论区即可。`,
132
+ };
133
+ }
117
134
  case 'report':
118
135
  case 'card': {
119
136
  const path = writeReportCard(runtime.state, runtime.config, { now });
package/lib/image.js ADDED
@@ -0,0 +1,93 @@
1
+ /**
2
+ * PNG rendering: turns the 360×360 scene into a shareable image with no
3
+ * browser and no network. This is the "visual" half of the wardrobe — the
4
+ * output can be dropped into a QQ group, a B站 comment or a phone album.
5
+ *
6
+ * Uses @resvg/resvg-js (prebuilt Rust binding) so rendering is deterministic
7
+ * and fast; the only external input is the local state file.
8
+ * @module dsh-feiyu-wardrobe/image
9
+ */
10
+
11
+ import { Resvg } from '@resvg/resvg-js';
12
+ import { moodOf } from './economy.js';
13
+ import { CANVAS, sceneInner } from './render.js';
14
+
15
+ export const DEFAULT_PNG_SIZE = 768;
16
+ export const FOOTER_RATIO = 0.085;
17
+
18
+ /**
19
+ * @param {string} text
20
+ * @returns {string}
21
+ */
22
+ function escapeXml(text) {
23
+ return String(text)
24
+ .replaceAll('&', '&amp;')
25
+ .replaceAll('<', '&lt;')
26
+ .replaceAll('>', '&gt;')
27
+ .replaceAll('"', '&quot;')
28
+ .replaceAll("'", '&apos;');
29
+ }
30
+
31
+ /**
32
+ * Build the full PNG-ready SVG: the scene square plus an optional footer strip
33
+ * carrying the repo URL and the fish's name.
34
+ * @param {import('./state.js').FishState} state
35
+ * @param {import('./config.js').FeiyuConfig} config
36
+ * @param {{ now?: number, size?: number, watermark?: boolean }} [options]
37
+ * @returns {{ svg: string, width: number, height: number, footerHeight: number }}
38
+ */
39
+ export function buildPngSvg(state, config, options = {}) {
40
+ const size = Math.max(256, Math.min(1600, Math.round(options.size ?? DEFAULT_PNG_SIZE)));
41
+ const watermark = options.watermark !== false;
42
+ const now = options.now ?? Date.now();
43
+ const mood = moodOf(state, now, config);
44
+ const scale = size / CANVAS;
45
+ const footerHeight = watermark ? Math.round(size * FOOTER_RATIO) : 0;
46
+ const height = size + footerHeight;
47
+ const fontSize = Math.max(12, Math.round(size * 0.024));
48
+ const inner = sceneInner(state, { face: mood.face });
49
+ const footer = watermark
50
+ ? `<g font-family="'Noto Sans CJK SC','PingFang SC','Microsoft YaHei',sans-serif">
51
+ <rect x="0" y="${size}" width="${size}" height="${footerHeight}" fill="#0D1524"/>
52
+ <text x="${Math.round(size * 0.035)}" y="${size + Math.round(footerHeight * 0.62)}"
53
+ font-size="${fontSize}" fill="#9FB0C8">大肥鱼衣橱 · github.com/GIT121995/dsh-feiyu-wardrobe</text>
54
+ <text x="${size - Math.round(size * 0.035)}" y="${size + Math.round(footerHeight * 0.62)}"
55
+ font-size="${fontSize}" fill="#F5C542" text-anchor="end">${escapeXml(state.fishName)} · 第 ${Math.max(1, Math.ceil((now - state.bornAt) / 86_400_000))} 天</text>
56
+ </g>`
57
+ : '';
58
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${height}" viewBox="0 0 ${size} ${height}">
59
+ <rect width="${size}" height="${height}" fill="#0D1524"/>
60
+ <g transform="scale(${scale})">${inner}</g>
61
+ ${footer}
62
+ </svg>`;
63
+ return { svg, width: size, height, footerHeight };
64
+ }
65
+
66
+ /**
67
+ * Render the dressed fish as a PNG buffer.
68
+ * @param {import('./state.js').FishState} state
69
+ * @param {import('./config.js').FeiyuConfig} config
70
+ * @param {{ now?: number, size?: number, watermark?: boolean }} [options]
71
+ * @returns {Buffer}
72
+ */
73
+ export function renderFishPng(state, config, options = {}) {
74
+ const { svg, width, height } = buildPngSvg(state, config, options);
75
+ const renderer = new Resvg(svg, {
76
+ fitTo: { mode: 'width', value: width },
77
+ background: '#0D1524',
78
+ font: { loadSystemFonts: true, defaultFontFamily: 'Noto Sans CJK SC' },
79
+ });
80
+ return Buffer.from(renderer.render().asPng());
81
+ }
82
+
83
+ /**
84
+ * PNG IHDR dimensions (for tests and CLI logs).
85
+ * @param {Buffer} png
86
+ * @returns {{ width: number, height: number }}
87
+ */
88
+ export function pngSize(png) {
89
+ if (!Buffer.isBuffer(png) || png.length < 24) return { width: 0, height: 0 };
90
+ const signature = png.subarray(0, 8).toString('hex');
91
+ if (signature !== '89504e470d0a1a0a') return { width: 0, height: 0 };
92
+ return { width: png.readUInt32BE(16), height: png.readUInt32BE(20) };
93
+ }
package/lib/util.js CHANGED
@@ -50,6 +50,18 @@ export function atomicWriteFile(path, text) {
50
50
  renameSync(tmp, path);
51
51
  }
52
52
 
53
+ /**
54
+ * Write a Buffer atomically (temp file + rename).
55
+ * @param {string} path
56
+ * @param {Buffer} buffer
57
+ */
58
+ export function atomicWriteBuffer(path, buffer) {
59
+ ensureParent(path);
60
+ const tmp = `${path}.${process.pid}.${Math.random().toString(36).slice(2, 8)}.tmp`;
61
+ writeFileSync(tmp, buffer);
62
+ renameSync(tmp, path);
63
+ }
64
+
53
65
  /**
54
66
  * Serialize and write JSON atomically.
55
67
  * @param {string} path
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-feiyu-wardrobe",
3
- "version": "0.1.0",
4
- "description": "大肥鱼衣橱 — a chubby-whale wardrobe economy for DeepSeek Harness: every token you burn becomes fish food, every meal buys outfits for your fish. Local-only, no extra model calls, no chat text stored.",
3
+ "version": "0.2.0",
4
+ "description": "大肥鱼衣橱 — DSH 插件:token 花销变鱼粮,解锁 21 件原创换装,一键出 PNG 可发 QQ 群 / 1080×1440 饲养报告卡(纯本地,零额外模型调用)",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "main": "./lib/index.js",
@@ -22,7 +22,9 @@
22
22
  "README.md",
23
23
  "CHANGELOG.md",
24
24
  "LICENSE",
25
- "docs/preview"
25
+ "docs/preview",
26
+ "screenshots.json",
27
+ "docs/preview/fish-demo.png"
26
28
  ],
27
29
  "scripts": {
28
30
  "typecheck": "tsc -p tsconfig.json",
@@ -32,6 +34,7 @@
32
34
  "preview": "node scripts/preview.mjs"
33
35
  },
34
36
  "keywords": [
37
+ "qq-bot",
35
38
  "deepseek",
36
39
  "deepseek-harness",
37
40
  "dsh",
@@ -70,5 +73,8 @@
70
73
  "homepage": "https://github.com/GIT121995/dsh-feiyu-wardrobe#readme",
71
74
  "bugs": {
72
75
  "url": "https://github.com/GIT121995/dsh-feiyu-wardrobe/issues"
76
+ },
77
+ "dependencies": {
78
+ "@resvg/resvg-js": "^2.6.2"
73
79
  }
74
80
  }
@@ -0,0 +1,5 @@
1
+ [
2
+ "docs/preview/card.png",
3
+ "docs/preview/fish-demo.png",
4
+ "docs/preview/wardrobe.png"
5
+ ]