dsh-feiyu-image 0.0.0-stage → 0.1.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 ADDED
@@ -0,0 +1,8 @@
1
+ # 更新记录
2
+
3
+ ## 0.1.0
4
+
5
+ - 首个版本:`shoot` 工具(selfie / scene),调百炼 Token Plan 生图;
6
+ - 提示词自动拼接外观、此刻穿着(联动 `dsh-feiyu-life`)、场景、光线、风格;
7
+ - 出图落盘 `~/.dsh/feiyu-image/`,带每日上限与最短间隔;
8
+ - 生图失败返回原因,不阻塞对话。
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vansnc
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,77 @@
1
- # Temporary Holding Version
1
+ # dsh-feiyu-image
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![npm](https://img.shields.io/npm/v/dsh-feiyu-image)](https://www.npmjs.com/package/dsh-feiyu-image)
4
+ [![license](https://img.shields.io/npm/l/dsh-feiyu-image)](./LICENSE)
5
+ [![node](https://img.shields.io/badge/node-%E2%89%A522-informational)](https://nodejs.org/)
6
+
7
+ 给陪伴角色一个相机:模型调一个工具就能「拍一张」,**自动带上她此刻的穿着和所在场景**,
8
+ 存成本地图片,回复里写 `![描述](路径)` 就能发到 QQ。
9
+
10
+ 生图走 **阿里百炼 Token Plan**(`wan2.7-image`,compatible-mode `chat/completions`,回图片 URL,2048×2048)。
11
+
12
+ ## 为什么不是 dsh-image-studio
13
+
14
+ [dsh-image-studio](https://github.com/meliodascz89/dsh-image-studio) 停在 0.1.7(2026-08),
15
+ 依赖 `@deepseek-ai/dsh-tools ^0.1.0-rc.6`、`dsh-settings ^0.1.1-rc.2`,跟 dsh 0.2.x 不兼容,
16
+ 要装得给确切版本盖豁免(可能崩、可能丢数据)。而这里真正有用的只有一个工具,就自己写了。
17
+
18
+ ## 能力
19
+
20
+ | 工具 | 说明 |
21
+ | --- | --- |
22
+ | `shoot` | `提示`(可选,画面里还想要什么)+ `类型`(`selfie` 自拍 / `scene` 只拍场景)→ 生成并保存 |
23
+
24
+ - 提示词自动拼:外观(配置)+ 此刻穿着(读 `dsh-feiyu-life` 的今日剧本)+ 所在场景 + 光线 + 风格;
25
+ - 出图存 `~/.dsh/feiyu-image/feiyu-<时间戳>.png`;
26
+ - 节流:默认每天 8 张、两次至少隔 30 秒(配置可调);
27
+ - 生图失败会返回原因,不会卡住整轮对话。
28
+
29
+ ## 安装
30
+
31
+ ```sh
32
+ dsh plugin --profile web add dsh-feiyu-image
33
+ ```
34
+
35
+ 需要 `~/.dsh/bailian-tp/config.json`(Token Plan 的 key 和 base,跟 `bailian-tp` 插件共用)。
36
+
37
+ ## 配置
38
+
39
+ `~/.dsh/feiyu-image-config.json`(没有就用默认):
40
+
41
+ ```json
42
+ {
43
+ "总开关": true,
44
+ "模型": "wan2.7-image",
45
+ "外观": "中国大陆 21 岁女大学生,素颜淡妆,黑色齐肩发,皮肤偏白,身形偏瘦",
46
+ "风格": "手机随手拍,自然光,轻微胶片颗粒,真实质感,不过度磨皮",
47
+ "每日上限": 8,
48
+ "最短间隔秒": 30,
49
+ "存图目录": ""
50
+ }
51
+ ```
52
+
53
+ ## 用法
54
+
55
+ 模型侧:
56
+
57
+ ```
58
+ shoot 类型=selfie 提示="把刚取到的画册摊在桌上,低头看画面"
59
+ shoot 类型=scene 提示="宿舍窗台上的绿萝,傍晚的光"
60
+ ```
61
+
62
+ 回复里用 markdown 引用路径即可发图(QQ 桥会把 `![描述](绝对路径)` 抽成图片段)。
63
+
64
+ ## 成本与耗时
65
+
66
+ - 单张约 **18–22 秒**,2048×2048 PNG(7MB 左右);
67
+ - 按 Token Plan 的图片额度计费,一张一次。
68
+
69
+ ## 已知边界
70
+
71
+ - 同一个人的脸**只能做到「像同一个人」**,不是完全一致;要稳定得配 LoRA 或参考图(未实现);
72
+ - 中文文字(黑板、书页)容易糊成乱码,尽量别让画面里出现要认的字;
73
+ - 依赖 `dsh-feiyu-life` 才有「穿着/场景」;没装也能用,只是提示词里少这两项。
74
+
75
+ ## 反馈
76
+
77
+ 开 [issue](https://github.com/Vansnc/dsh-feiyu-image/issues),带上版本和那张图。
@@ -0,0 +1,4 @@
1
+ # dsh-feiyu-image 的 bundle 补丁层。
2
+ - insert:
3
+ - id: feiyu-image
4
+ name: dsh-feiyu-image
package/lib/index.js ADDED
@@ -0,0 +1,156 @@
1
+ // dsh-feiyu-image —— 给大肥鱼一个相机
2
+ //
3
+ // 为什么自己做:dsh-image-studio 停在 0.1.7(8 月),跟 dsh 0.2.x 的依赖对不上,
4
+ // 要装就得给它盖版本豁免,风险不值当。而真正有用的只有一件事:
5
+ // 让模型能「拍一张」——自动带上她此刻的穿着和所在场景,存成本地文件,能直接发到 QQ。
6
+ //
7
+ // 生图走百炼 Token Plan(wan2.7-image,compatible-mode chat/completions,回图片 URL)。
8
+ // 外观和风格写在配置里,跟生活线(dsh-feiyu-life)的今日剧本联动。
9
+
10
+ import { readFileSync, writeFileSync, mkdirSync, statSync, readdirSync } from 'node:fs'
11
+ import { join } from 'node:path'
12
+ import { homedir } from 'node:os'
13
+
14
+ const DSH_HOME = process.env.DSH_HOME ?? join(homedir(), '.dsh')
15
+ const CONFIG = join(DSH_HOME, 'feiyu-image-config.json')
16
+ const OUT_DIR = join(DSH_HOME, 'feiyu-image')
17
+ const TP_CFG = join(DSH_HOME, 'bailian-tp', 'config.json')
18
+ const DAYS = join(DSH_HOME, 'feiyu-life-days')
19
+
20
+ const 默认 = {
21
+ 总开关: true,
22
+ 模型: 'wan2.7-image',
23
+ 外观: '中国大陆 21 岁女大学生,素颜淡妆,黑色齐肩发,皮肤偏白,身形偏瘦',
24
+ 风格: '手机随手拍,自然光,轻微胶片颗粒,真实质感,不过度磨皮',
25
+ 每日上限: 8,
26
+ 最短间隔秒: 30,
27
+ 存图目录: '',
28
+ }
29
+
30
+ export const inject = ['tools']
31
+
32
+ function 读配置() {
33
+ try {
34
+ const raw = JSON.parse(readFileSync(CONFIG, 'utf8'))
35
+ return { ...默认, ...raw }
36
+ } catch { return { ...默认 } }
37
+ }
38
+
39
+ function 存配置(patch) {
40
+ const next = { ...读配置(), ...patch }
41
+ try { mkdirSync(DSH_HOME, { recursive: true }); writeFileSync(CONFIG, JSON.stringify(next, null, 2), 'utf8') } catch {}
42
+ return next
43
+ }
44
+
45
+ function 生活此刻() {
46
+ try {
47
+ const now = new Date()
48
+ const 北京 = new Date(now.getTime() + 8 * 3600e3)
49
+ const ymd = 北京.toISOString().slice(0, 10)
50
+ const 分 = 北京.getUTCHours() * 60 + 北京.getUTCMinutes()
51
+ const day = JSON.parse(readFileSync(join(DAYS, ymd + '.json'), 'utf8'))
52
+ const items = day.items || []
53
+ let cur = null
54
+ for (const x of items) if (分 >= x.起 && 分 < x.止) cur = x
55
+ const 时 = 北京.getUTCHours()
56
+ const 光线 = 时 < 6 ? '天还没亮' : 时 < 9 ? '清晨的柔光' : 时 < 11 ? '上午的日光' : 时 < 14 ? '正午偏亮的光' : 时 < 17 ? '午后的斜光' : 时 < 19 ? '傍晚的金色光' : 时 < 22 ? '夜里的室内灯' : '深夜只开着台灯'
57
+ return { 在: cur?.在 || '', 穿: cur?.穿 || '', 光线, 场景: cur?.在 || '' }
58
+ } catch { return { 在: '', 穿: '', 光线: '自然光', 场景: '' } }
59
+ }
60
+
61
+ function 拼提示词(cfg, 参数) {
62
+ const 生活 = 生活此刻()
63
+ const 类型 = String(参数.类型 || 'selfie')
64
+ const 主体 = String(参数.提示 || 参数.prompt || '').trim()
65
+ const 片段 = []
66
+ if (类型 === 'selfie') {
67
+ 片段.push(cfg.外观)
68
+ if (生活.穿) 片段.push(`穿着${生活.穿}`)
69
+ if (生活.场景) 片段.push(`此刻在${生活.场景}`)
70
+ 片段.push('第一人称手持手机自拍视角,脸在画面里,表情自然')
71
+ } else {
72
+ if (生活.场景) 片段.push(`地点:${生活.场景}`)
73
+ }
74
+ if (主体) 片段.push(主体)
75
+ 片段.push(生活.光线)
76
+ 片段.push(cfg.风格)
77
+ return 片段.filter(Boolean).join(';')
78
+ }
79
+
80
+ async function 生图(cfg, prompt) {
81
+ const tp = JSON.parse(readFileSync(TP_CFG, 'utf8'))
82
+ const r = await fetch(tp.chatBaseUrl.replace(/\/+$/, '') + '/chat/completions', {
83
+ method: 'POST',
84
+ headers: { Authorization: 'Bearer ' + tp.tokenPlanKey, 'Content-Type': 'application/json' },
85
+ body: JSON.stringify({ model: cfg.模型, messages: [{ role: 'user', content: [{ type: 'text', text: prompt }] }] }),
86
+ })
87
+ const data = await r.json().catch(() => ({}))
88
+ if (!r.ok) throw new Error('生图接口 ' + r.status + ':' + JSON.stringify(data).slice(0, 200))
89
+ const content = data?.output?.choices?.[0]?.message?.content || []
90
+ const url = content.find?.((x) => x && x.image)?.image
91
+ if (!url) throw new Error('返回里没有图片:' + JSON.stringify(data).slice(0, 200))
92
+ const img = await fetch(url)
93
+ if (!img.ok) throw new Error('下载失败 ' + img.status)
94
+ const buf = Buffer.from(await img.arrayBuffer())
95
+ const dir = cfg.存图目录 || OUT_DIR
96
+ mkdirSync(dir, { recursive: true })
97
+ const path = join(dir, `feiyu-${Date.now()}.png`)
98
+ writeFileSync(path, buf)
99
+ return { path, bytes: buf.length, prompt }
100
+ }
101
+
102
+ /** 今日已出图数 / 上次出图时间(简单节流,够用) */
103
+ function 今日用量(cfg) {
104
+ const dir = cfg.存图目录 || OUT_DIR
105
+ try {
106
+ const 今天 = new Date(Date.now() + 8 * 3600e3).toISOString().slice(0, 10)
107
+ let n = 0
108
+ let 最后 = 0
109
+ for (const f of 列目录(dir)) {
110
+ if (!f.endsWith('.png')) continue
111
+ const st = statSync(join(dir, f))
112
+ if (new Date(st.mtimeMs + 8 * 3600e3).toISOString().slice(0, 10) === 今天) n++
113
+ 最后 = Math.max(最后, st.mtimeMs)
114
+ }
115
+ return { n, 最后 }
116
+ } catch { return { n: 0, 最后: 0 } }
117
+ }
118
+
119
+ function 列目录(dir) {
120
+ try { return readdirSync(dir) } catch { return [] }
121
+ }
122
+
123
+ export function apply(ctx) {
124
+ ctx.effect(() => ctx.tools.register({
125
+ name: 'shoot',
126
+ description: '拍一张照片并保存成图片文件(自拍 / 随手拍)。会自动带上她此刻的穿着和所在场景,跟她今天的日程对得上。回复里拿到路径后用 ![描述](路径) 发出去。',
127
+ parameters: {
128
+ type: 'object',
129
+ properties: {
130
+ 提示: { type: 'string', description: '画面里还想要什么(姿势、表情、手里的东西、场景细节)。不填就按此刻的状态拍' },
131
+ 类型: { type: 'string', enum: ['selfie', 'scene'], description: 'selfie=自拍(有她本人),scene=只拍场景/物(不出现人)' },
132
+ },
133
+ additionalProperties: false,
134
+ },
135
+ output: {
136
+ schema: { type: 'object', properties: { ok: { type: 'boolean' }, 图: { type: 'string' }, 说: { type: 'string' }, 提示词: { type: 'string' } }, additionalProperties: true },
137
+ render: (_a, v) => [{ type: 'text', text: String(v?.说 || '') + (v?.图 ? '\n图片:' + v.图 : '') }],
138
+ },
139
+ async execute(参数 = {}) {
140
+ const cfg = 读配置()
141
+ if (!cfg.总开关) return { ok: false, 说: '相机没开' }
142
+ const 用 = 今日用量(cfg)
143
+ if (用.n >= cfg.每日上限) return { ok: false, 说: `今天已经拍了 ${用.n} 张,够了` }
144
+ if (Date.now() - 用.最后 < cfg.最短间隔秒 * 1000) return { ok: false, 说: '刚拍过,等会儿再拍' }
145
+ const prompt = 拼提示词(cfg, 参数)
146
+ try {
147
+ const r = await 生图(cfg, prompt)
148
+ return { ok: true, 图: r.path, 说: `拍好了(${Math.round(r.bytes / 1024)}KB)`, 提示词: prompt }
149
+ } catch (e) {
150
+ return { ok: false, 说: '没拍成:' + String((e && e.message) || e).slice(0, 160) }
151
+ }
152
+ },
153
+ }))
154
+ }
155
+
156
+ export { 拼提示词, 生图, 读配置, 存配置 }
package/package.json CHANGED
@@ -1,6 +1,22 @@
1
1
  {
2
2
  "name": "dsh-feiyu-image",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "给大肥鱼一个相机:调百炼 Token Plan 生图(wan2.7-image),自动沿用她此刻的穿着和场景,出图存本地、可直接发到 QQ。",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "engines": { "node": ">=22" },
8
+ "keywords": ["dsh", "dsh-plugin", "deepseek-harness", "image", "wanx", "bailian", "companion"],
9
+ "author": "Vansnc",
10
+ "repository": { "type": "git", "url": "git+https://github.com/Vansnc/dsh-feiyu-image.git" },
11
+ "homepage": "https://github.com/Vansnc/dsh-feiyu-image#readme",
12
+ "bugs": { "url": "https://github.com/Vansnc/dsh-feiyu-image/issues" },
13
+ "main": "lib/index.js",
14
+ "exports": {
15
+ ".": "./lib/index.js",
16
+ "./cordis.patch.yml": "./cordis.patch.yml",
17
+ "./package.json": "./package.json"
18
+ },
19
+ "files": ["lib", "cordis.patch.yml", "README.md", "CHANGELOG.md", "LICENSE"],
20
+ "publishConfig": { "access": "public" },
21
+ "dsh": { "bundle": { "patch": "./cordis.patch.yml" } }
22
+ }