dsh-feiyu-image 0.0.0-stage → 0.1.1
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 +15 -0
- package/LICENSE +21 -0
- package/README.md +81 -2
- package/cordis.patch.yml +4 -0
- package/lib/index.js +161 -0
- package/package.json +46 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# 更新记录
|
|
2
|
+
|
|
3
|
+
## 0.1.1
|
|
4
|
+
|
|
5
|
+
- 画风改成二次元(日系动画、赛璐璐上色);
|
|
6
|
+
- 角色形象锁定:固定「角色设定」每次一字不差带上 + 固定 `seed`,跨场景保持同一个人;
|
|
7
|
+
- 角色设定图存 `~/.dsh/feiyu-image/角色-设定.png`(不计入每日张数);
|
|
8
|
+
- 实测该接口不接受图片输入,所以改用文字设定 + seed 锁脸,README 里写清了边界。
|
|
9
|
+
|
|
10
|
+
## 0.1.0
|
|
11
|
+
|
|
12
|
+
- 首个版本:`shoot` 工具(selfie / scene),调百炼 Token Plan 生图;
|
|
13
|
+
- 提示词自动拼接外观、此刻穿着(联动 `dsh-feiyu-life`)、场景、光线、风格;
|
|
14
|
+
- 出图落盘 `~/.dsh/feiyu-image/`,带每日上限与最短间隔;
|
|
15
|
+
- 生图失败返回原因,不阻塞对话。
|
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,82 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-feiyu-image
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/dsh-feiyu-image)
|
|
4
|
+
[](./LICENSE)
|
|
5
|
+
[](https://nodejs.org/)
|
|
6
|
+
|
|
7
|
+
给陪伴角色一个相机:模型调一个工具就能「拍一张」,**自动带上她此刻的穿着和所在场景**,
|
|
8
|
+
存成本地图片,回复里写 `` 就能发到 QQ。
|
|
9
|
+
|
|
10
|
+
生图走 **阿里百炼 Token Plan**(`wan2.7-image`,compatible-mode `chat/completions`,回图片 URL,2048×2048)。
|
|
11
|
+
画风是**二次元**(日系动画、赛璐璐上色),角色长相靠「固定设定 + 固定 seed」钉住。
|
|
12
|
+
|
|
13
|
+
## 为什么不是 dsh-image-studio
|
|
14
|
+
|
|
15
|
+
[dsh-image-studio](https://github.com/meliodascz89/dsh-image-studio) 停在 0.1.7(2026-08),
|
|
16
|
+
依赖 `@deepseek-ai/dsh-tools ^0.1.0-rc.6`、`dsh-settings ^0.1.1-rc.2`,跟 dsh 0.2.x 不兼容,
|
|
17
|
+
要装得给确切版本盖豁免(可能崩、可能丢数据)。而这里真正有用的只有一个工具,就自己写了。
|
|
18
|
+
|
|
19
|
+
## 能力
|
|
20
|
+
|
|
21
|
+
| 工具 | 说明 |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| `shoot` | `提示`(可选,画面里还想要什么)+ `类型`(`selfie` 自拍 / `scene` 只拍场景)→ 生成并保存 |
|
|
24
|
+
|
|
25
|
+
- 提示词自动拼:**固定角色设定**(配置,一字不差每次带上)+ 此刻穿着(读 `dsh-feiyu-life` 的今日剧本)+ 场景 + 光线 + 画风;
|
|
26
|
+
- 固定 `seed`,同一个角色跨场景保持同一张脸(发色、发型、瞳色、鲸鱼发夹、卫衣);
|
|
27
|
+
- 出图存 `~/.dsh/feiyu-image/feiyu-<时间戳>.png`;
|
|
28
|
+
- 节流:默认每天 8 张、两次至少隔 30 秒(配置可调);
|
|
29
|
+
- 生图失败会返回原因,不会卡住整轮对话。
|
|
30
|
+
|
|
31
|
+
## 安装
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
dsh plugin --profile web add dsh-feiyu-image
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
需要 `~/.dsh/bailian-tp/config.json`(Token Plan 的 key 和 base,跟 `bailian-tp` 插件共用)。
|
|
38
|
+
|
|
39
|
+
## 配置
|
|
40
|
+
|
|
41
|
+
`~/.dsh/feiyu-image-config.json`(没有就用默认):
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"总开关": true,
|
|
46
|
+
"模型": "wan2.7-image",
|
|
47
|
+
"外观": "中国大陆 21 岁女大学生,素颜淡妆,黑色齐肩发,皮肤偏白,身形偏瘦",
|
|
48
|
+
"风格": "手机随手拍,自然光,轻微胶片颗粒,真实质感,不过度磨皮",
|
|
49
|
+
"每日上限": 8,
|
|
50
|
+
"最短间隔秒": 30,
|
|
51
|
+
"存图目录": ""
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## 用法
|
|
56
|
+
|
|
57
|
+
模型侧:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
shoot 类型=selfie 提示="把刚取到的画册摊在桌上,低头看画面"
|
|
61
|
+
shoot 类型=scene 提示="宿舍窗台上的绿萝,傍晚的光"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
回复里用 markdown 引用路径即可发图(QQ 桥会把 `` 抽成图片段)。
|
|
65
|
+
|
|
66
|
+
## 成本与耗时
|
|
67
|
+
|
|
68
|
+
- 单张约 **18–22 秒**,2048×2048 PNG(7MB 左右);
|
|
69
|
+
- 按 Token Plan 的图片额度计费,一张一次。
|
|
70
|
+
|
|
71
|
+
## 已知边界
|
|
72
|
+
|
|
73
|
+
- 实测这条接口**不接受图片输入**(传参考图会 400:`Either 'text' or 'image' must be provided, but not both`),
|
|
74
|
+
所以没法用「角色参考图 + 指令」的方式锁脸;现在靠固定设定 + 固定 seed,能做到「一看就是同一个人」,但细节会飘。
|
|
75
|
+
要真正锁死得上 LoRA 或支持 image-edit 的接口;
|
|
76
|
+
- 角色设定图存在 `~/.dsh/feiyu-image/角色-设定.png`,改设定时对着它改;
|
|
77
|
+
- 中文文字(黑板、书页)容易糊成乱码,尽量别让画面里出现要认的字;
|
|
78
|
+
- 依赖 `dsh-feiyu-life` 才有「穿着/场景」;没装也能用,只是提示词里少这两项。
|
|
79
|
+
|
|
80
|
+
## 反馈
|
|
81
|
+
|
|
82
|
+
开 [issue](https://github.com/Vansnc/dsh-feiyu-image/issues),带上版本和那张图。
|
package/cordis.patch.yml
ADDED
package/lib/index.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
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
|
+
种子: 20261109,
|
|
24
|
+
外观: '21 岁中国女大学生,黑色齐肩直发配齐刘海,浅褐色大眼,皮肤白皙,小圆脸,左耳上方一枚白色小鲸鱼发夹',
|
|
25
|
+
风格: '日系动画风格,赛璐璐上色,清晰线稿,柔和光,背景干净',
|
|
26
|
+
每日上限: 8,
|
|
27
|
+
最短间隔秒: 30,
|
|
28
|
+
存图目录: '',
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const inject = ['tools']
|
|
32
|
+
|
|
33
|
+
function 读配置() {
|
|
34
|
+
try {
|
|
35
|
+
const raw = JSON.parse(readFileSync(CONFIG, 'utf8'))
|
|
36
|
+
return { ...默认, ...raw }
|
|
37
|
+
} catch { return { ...默认 } }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function 存配置(patch) {
|
|
41
|
+
const next = { ...读配置(), ...patch }
|
|
42
|
+
try { mkdirSync(DSH_HOME, { recursive: true }); writeFileSync(CONFIG, JSON.stringify(next, null, 2), 'utf8') } catch {}
|
|
43
|
+
return next
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function 生活此刻() {
|
|
47
|
+
try {
|
|
48
|
+
const now = new Date()
|
|
49
|
+
const 北京 = new Date(now.getTime() + 8 * 3600e3)
|
|
50
|
+
const ymd = 北京.toISOString().slice(0, 10)
|
|
51
|
+
const 分 = 北京.getUTCHours() * 60 + 北京.getUTCMinutes()
|
|
52
|
+
const day = JSON.parse(readFileSync(join(DAYS, ymd + '.json'), 'utf8'))
|
|
53
|
+
const items = day.items || []
|
|
54
|
+
let cur = null
|
|
55
|
+
for (const x of items) if (分 >= x.起 && 分 < x.止) cur = x
|
|
56
|
+
const 时 = 北京.getUTCHours()
|
|
57
|
+
const 光线 = 时 < 6 ? '天还没亮' : 时 < 9 ? '清晨的柔光' : 时 < 11 ? '上午的日光' : 时 < 14 ? '正午偏亮的光' : 时 < 17 ? '午后的斜光' : 时 < 19 ? '傍晚的金色光' : 时 < 22 ? '夜里的室内灯' : '深夜只开着台灯'
|
|
58
|
+
return { 在: cur?.在 || '', 穿: cur?.穿 || '', 光线, 场景: cur?.在 || '' }
|
|
59
|
+
} catch { return { 在: '', 穿: '', 光线: '自然光', 场景: '' } }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function 拼提示词(cfg, 参数) {
|
|
63
|
+
const 生活 = 生活此刻()
|
|
64
|
+
const 类型 = String(参数.类型 || 'selfie')
|
|
65
|
+
const 主体 = String(参数.提示 || 参数.prompt || '').trim()
|
|
66
|
+
// 角色设定放最前面、每次一字不差 —— 二次元模型靠这段把「同一张脸」钉住
|
|
67
|
+
const 片段 = [`【固定角色设定,必须完全一致】${cfg.外观}。`]
|
|
68
|
+
if (类型 === 'selfie') {
|
|
69
|
+
if (生活.穿) 片段.push(`穿着${生活.穿}`)
|
|
70
|
+
if (生活.场景) 片段.push(`场景:${生活.场景}`)
|
|
71
|
+
片段.push('第一人称手机自拍构图,脸在画面里,表情自然')
|
|
72
|
+
} else {
|
|
73
|
+
if (生活.场景) 片段.push(`场景:${生活.场景}`)
|
|
74
|
+
}
|
|
75
|
+
if (主体) 片段.push(主体)
|
|
76
|
+
片段.push(生活.光线)
|
|
77
|
+
片段.push(`画风:${cfg.风格}`)
|
|
78
|
+
return 片段.filter(Boolean).join(';')
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function 生图(cfg, prompt) {
|
|
82
|
+
const tp = JSON.parse(readFileSync(TP_CFG, 'utf8'))
|
|
83
|
+
const r = await fetch(tp.chatBaseUrl.replace(/\/+$/, '') + '/chat/completions', {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
headers: { Authorization: 'Bearer ' + tp.tokenPlanKey, 'Content-Type': 'application/json' },
|
|
86
|
+
body: JSON.stringify({
|
|
87
|
+
model: cfg.模型,
|
|
88
|
+
...(Number(cfg.种子) ? { seed: Number(cfg.种子) } : {}),
|
|
89
|
+
messages: [{ role: 'user', content: [{ type: 'text', text: prompt }] }],
|
|
90
|
+
}),
|
|
91
|
+
})
|
|
92
|
+
const data = await r.json().catch(() => ({}))
|
|
93
|
+
if (!r.ok) throw new Error('生图接口 ' + r.status + ':' + JSON.stringify(data).slice(0, 200))
|
|
94
|
+
const content = data?.output?.choices?.[0]?.message?.content || []
|
|
95
|
+
const url = content.find?.((x) => x && x.image)?.image
|
|
96
|
+
if (!url) throw new Error('返回里没有图片:' + JSON.stringify(data).slice(0, 200))
|
|
97
|
+
const img = await fetch(url)
|
|
98
|
+
if (!img.ok) throw new Error('下载失败 ' + img.status)
|
|
99
|
+
const buf = Buffer.from(await img.arrayBuffer())
|
|
100
|
+
const dir = cfg.存图目录 || OUT_DIR
|
|
101
|
+
mkdirSync(dir, { recursive: true })
|
|
102
|
+
const path = join(dir, `feiyu-${Date.now()}.png`)
|
|
103
|
+
writeFileSync(path, buf)
|
|
104
|
+
return { path, bytes: buf.length, prompt }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** 今日已出图数 / 上次出图时间(简单节流,够用) */
|
|
108
|
+
function 今日用量(cfg) {
|
|
109
|
+
const dir = cfg.存图目录 || OUT_DIR
|
|
110
|
+
try {
|
|
111
|
+
const 今天 = new Date(Date.now() + 8 * 3600e3).toISOString().slice(0, 10)
|
|
112
|
+
let n = 0
|
|
113
|
+
let 最后 = 0
|
|
114
|
+
for (const f of 列目录(dir)) {
|
|
115
|
+
if (!/^feiyu-.*\.png$/.test(f)) continue // 角色设定图不算拍的照片
|
|
116
|
+
const st = statSync(join(dir, f))
|
|
117
|
+
if (new Date(st.mtimeMs + 8 * 3600e3).toISOString().slice(0, 10) === 今天) n++
|
|
118
|
+
最后 = Math.max(最后, st.mtimeMs)
|
|
119
|
+
}
|
|
120
|
+
return { n, 最后 }
|
|
121
|
+
} catch { return { n: 0, 最后: 0 } }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function 列目录(dir) {
|
|
125
|
+
try { return readdirSync(dir) } catch { return [] }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function apply(ctx) {
|
|
129
|
+
ctx.effect(() => ctx.tools.register({
|
|
130
|
+
name: 'shoot',
|
|
131
|
+
description: '拍一张照片并保存成图片文件(自拍 / 随手拍)。会自动带上她此刻的穿着和所在场景,跟她今天的日程对得上。回复里拿到路径后用  发出去。',
|
|
132
|
+
parameters: {
|
|
133
|
+
type: 'object',
|
|
134
|
+
properties: {
|
|
135
|
+
提示: { type: 'string', description: '画面里还想要什么(姿势、表情、手里的东西、场景细节)。不填就按此刻的状态拍' },
|
|
136
|
+
类型: { type: 'string', enum: ['selfie', 'scene'], description: 'selfie=自拍(有她本人),scene=只拍场景/物(不出现人)' },
|
|
137
|
+
},
|
|
138
|
+
additionalProperties: false,
|
|
139
|
+
},
|
|
140
|
+
output: {
|
|
141
|
+
schema: { type: 'object', properties: { ok: { type: 'boolean' }, 图: { type: 'string' }, 说: { type: 'string' }, 提示词: { type: 'string' } }, additionalProperties: true },
|
|
142
|
+
render: (_a, v) => [{ type: 'text', text: String(v?.说 || '') + (v?.图 ? '\n图片:' + v.图 : '') }],
|
|
143
|
+
},
|
|
144
|
+
async execute(参数 = {}) {
|
|
145
|
+
const cfg = 读配置()
|
|
146
|
+
if (!cfg.总开关) return { ok: false, 说: '相机没开' }
|
|
147
|
+
const 用 = 今日用量(cfg)
|
|
148
|
+
if (用.n >= cfg.每日上限) return { ok: false, 说: `今天已经拍了 ${用.n} 张,够了` }
|
|
149
|
+
if (Date.now() - 用.最后 < cfg.最短间隔秒 * 1000) return { ok: false, 说: '刚拍过,等会儿再拍' }
|
|
150
|
+
const prompt = 拼提示词(cfg, 参数)
|
|
151
|
+
try {
|
|
152
|
+
const r = await 生图(cfg, prompt)
|
|
153
|
+
return { ok: true, 图: r.path, 说: `拍好了(${Math.round(r.bytes / 1024)}KB)`, 提示词: prompt }
|
|
154
|
+
} catch (e) {
|
|
155
|
+
return { ok: false, 说: '没拍成:' + String((e && e.message) || e).slice(0, 160) }
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
}))
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export { 拼提示词, 生图, 读配置, 存配置 }
|
package/package.json
CHANGED
|
@@ -1,6 +1,49 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-feiyu-image",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "给大肥鱼一个相机:调百炼 wan2.7-image 出二次元图,角色形象用固定设定+固定种子钉住,自动带此刻的穿着和场景,出图可直接发到 QQ。",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=22"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"dsh",
|
|
12
|
+
"dsh-plugin",
|
|
13
|
+
"deepseek-harness",
|
|
14
|
+
"image",
|
|
15
|
+
"wanx",
|
|
16
|
+
"bailian",
|
|
17
|
+
"companion"
|
|
18
|
+
],
|
|
19
|
+
"author": "Vansnc",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/Vansnc/dsh-feiyu-image.git"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://github.com/Vansnc/dsh-feiyu-image#readme",
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/Vansnc/dsh-feiyu-image/issues"
|
|
27
|
+
},
|
|
28
|
+
"main": "lib/index.js",
|
|
29
|
+
"exports": {
|
|
30
|
+
".": "./lib/index.js",
|
|
31
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
32
|
+
"./package.json": "./package.json"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"lib",
|
|
36
|
+
"cordis.patch.yml",
|
|
37
|
+
"README.md",
|
|
38
|
+
"CHANGELOG.md",
|
|
39
|
+
"LICENSE"
|
|
40
|
+
],
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
},
|
|
44
|
+
"dsh": {
|
|
45
|
+
"bundle": {
|
|
46
|
+
"patch": "./cordis.patch.yml"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
6
49
|
}
|