dsh-novelai 1.0.0 → 1.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/lib/index.js +333 -329
- package/package.json +3 -3
package/lib/index.js
CHANGED
|
@@ -1,329 +1,333 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* dsh-novelai — 把 NovelAI 生图接进 DeepSeek Harness
|
|
3
|
-
*
|
|
4
|
-
* 三个工具:
|
|
5
|
-
* nai_image 给提示词,出图(图会作为附件直接给模型看);可选 style 预设 + enhance 自动套用教程经验
|
|
6
|
-
* nai_status 查账号:档位、到期、用量
|
|
7
|
-
* nai_prompt 拼提示词 / 体检提示词 / 出心法
|
|
8
|
-
*
|
|
9
|
-
* 一个页面:/dsh-novelai/ —— 完整的画室 UI(提示词、参数、进度预览、历史)
|
|
10
|
-
*
|
|
11
|
-
* 配置:~/.dsh/novelai.json(apiKey / proxy / outDir / 默认参数)
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { readFileSync } from 'node:fs'
|
|
15
|
-
import { createHandler } from './routes.mjs'
|
|
16
|
-
import { loadConfig, generate, saveImages, fetchAccount, MODELS, SAMPLERS, SIZES, DEFAULT_NEGATIVE } from './nai.mjs'
|
|
17
|
-
import { STYLE_PRESETS, WEIGHT_RULES, GUIDE, QUALITY, NEG_V45, NEG_V5, normalizeWeights, buildPrompt, checkPrompt, isV5 } from './prompt.mjs'
|
|
18
|
-
|
|
19
|
-
export const name = 'novelai'
|
|
20
|
-
export const inject = ['tools', 'webServer', 'attachments']
|
|
21
|
-
|
|
22
|
-
const TAG = '[dsh-novelai]'
|
|
23
|
-
function log(msg) {
|
|
24
|
-
try {
|
|
25
|
-
console.error(TAG, msg)
|
|
26
|
-
} catch {}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** 宿主的 dsh-tools:先裸包名,再按安装树找,最后认 DSH_TOOLS_PATH。 */
|
|
30
|
-
async function loadDefineTool() {
|
|
31
|
-
try {
|
|
32
|
-
const mod = await import('@deepseek-ai/dsh-tools')
|
|
33
|
-
if (mod?.defineTool) return mod.defineTool
|
|
34
|
-
} catch {}
|
|
35
|
-
const override = process.env.DSH_TOOLS_PATH
|
|
36
|
-
if (override) {
|
|
37
|
-
try {
|
|
38
|
-
const { pathToFileURL } = await import('node:url')
|
|
39
|
-
const mod = await import(pathToFileURL(override).href)
|
|
40
|
-
if (mod?.defineTool) return mod.defineTool
|
|
41
|
-
} catch {}
|
|
42
|
-
}
|
|
43
|
-
const { existsSync } = await import('node:fs')
|
|
44
|
-
const { join } = await import('node:path')
|
|
45
|
-
const { homedir } = await import('node:os')
|
|
46
|
-
const { pathToFileURL } = await import('node:url')
|
|
47
|
-
const appdata = process.env.APPDATA
|
|
48
|
-
const roots = []
|
|
49
|
-
if (appdata) {
|
|
50
|
-
roots.push(join(appdata, 'npm', 'node_modules', '@deepseek-ai', 'dsh', 'node_modules', '@deepseek-ai', 'dsh-tools'))
|
|
51
|
-
roots.push(join(appdata, 'npm', 'node_modules', '@deepseek-ai', 'dsh-tools'))
|
|
52
|
-
}
|
|
53
|
-
const dshHome = process.env.DSH_HOME || join(homedir(), '.dsh')
|
|
54
|
-
roots.push(join(dshHome, 'node_modules', '@deepseek-ai', 'dsh-tools'))
|
|
55
|
-
for (const root of roots) {
|
|
56
|
-
const file = join(root, 'lib', 'index.js')
|
|
57
|
-
if (!existsSync(file)) continue
|
|
58
|
-
try {
|
|
59
|
-
const mod = await import(pathToFileURL(file).href)
|
|
60
|
-
if (mod?.defineTool) return mod.defineTool
|
|
61
|
-
} catch {}
|
|
62
|
-
}
|
|
63
|
-
return null
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function makeImageTool(defineTool, ctx) {
|
|
67
|
-
return defineTool({
|
|
68
|
-
name: 'nai_image',
|
|
69
|
-
description:
|
|
70
|
-
'用 NovelAI(本机已配置好 key)画一张二次元插画。' +
|
|
71
|
-
'画好会保存成 PNG 并直接显示在回复里,同时给出文件路径。' +
|
|
72
|
-
'提示词用英文 Danbooru 标签最准(1girl, solo, silver hair, looking at viewer…)。' +
|
|
73
|
-
'**默认用 nai-diffusion-4-5-full**;只有用户明确说「用 V5 / v5」时才选 5 系(nai-diffusion-5-full / 5-curated)。' +
|
|
74
|
-
'
|
|
75
|
-
'
|
|
76
|
-
'
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
head.push(
|
|
144
|
-
notes.push(
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
if (
|
|
155
|
-
|
|
156
|
-
notes.push('
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
return {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
'
|
|
263
|
-
'
|
|
264
|
-
'',
|
|
265
|
-
'
|
|
266
|
-
'
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
if (
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
1
|
+
/**
|
|
2
|
+
* dsh-novelai — 把 NovelAI 生图接进 DeepSeek Harness
|
|
3
|
+
*
|
|
4
|
+
* 三个工具:
|
|
5
|
+
* nai_image 给提示词,出图(图会作为附件直接给模型看);可选 style 预设 + enhance 自动套用教程经验
|
|
6
|
+
* nai_status 查账号:档位、到期、用量
|
|
7
|
+
* nai_prompt 拼提示词 / 体检提示词 / 出心法
|
|
8
|
+
*
|
|
9
|
+
* 一个页面:/dsh-novelai/ —— 完整的画室 UI(提示词、参数、进度预览、历史)
|
|
10
|
+
*
|
|
11
|
+
* 配置:~/.dsh/novelai.json(apiKey / proxy / outDir / 默认参数)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readFileSync } from 'node:fs'
|
|
15
|
+
import { createHandler } from './routes.mjs'
|
|
16
|
+
import { loadConfig, generate, saveImages, fetchAccount, MODELS, SAMPLERS, SIZES, DEFAULT_NEGATIVE } from './nai.mjs'
|
|
17
|
+
import { STYLE_PRESETS, WEIGHT_RULES, GUIDE, QUALITY, NEG_V45, NEG_V5, normalizeWeights, buildPrompt, checkPrompt, isV5 } from './prompt.mjs'
|
|
18
|
+
|
|
19
|
+
export const name = 'novelai'
|
|
20
|
+
export const inject = ['tools', 'webServer', 'attachments']
|
|
21
|
+
|
|
22
|
+
const TAG = '[dsh-novelai]'
|
|
23
|
+
function log(msg) {
|
|
24
|
+
try {
|
|
25
|
+
console.error(TAG, msg)
|
|
26
|
+
} catch {}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 宿主的 dsh-tools:先裸包名,再按安装树找,最后认 DSH_TOOLS_PATH。 */
|
|
30
|
+
async function loadDefineTool() {
|
|
31
|
+
try {
|
|
32
|
+
const mod = await import('@deepseek-ai/dsh-tools')
|
|
33
|
+
if (mod?.defineTool) return mod.defineTool
|
|
34
|
+
} catch {}
|
|
35
|
+
const override = process.env.DSH_TOOLS_PATH
|
|
36
|
+
if (override) {
|
|
37
|
+
try {
|
|
38
|
+
const { pathToFileURL } = await import('node:url')
|
|
39
|
+
const mod = await import(pathToFileURL(override).href)
|
|
40
|
+
if (mod?.defineTool) return mod.defineTool
|
|
41
|
+
} catch {}
|
|
42
|
+
}
|
|
43
|
+
const { existsSync } = await import('node:fs')
|
|
44
|
+
const { join } = await import('node:path')
|
|
45
|
+
const { homedir } = await import('node:os')
|
|
46
|
+
const { pathToFileURL } = await import('node:url')
|
|
47
|
+
const appdata = process.env.APPDATA
|
|
48
|
+
const roots = []
|
|
49
|
+
if (appdata) {
|
|
50
|
+
roots.push(join(appdata, 'npm', 'node_modules', '@deepseek-ai', 'dsh', 'node_modules', '@deepseek-ai', 'dsh-tools'))
|
|
51
|
+
roots.push(join(appdata, 'npm', 'node_modules', '@deepseek-ai', 'dsh-tools'))
|
|
52
|
+
}
|
|
53
|
+
const dshHome = process.env.DSH_HOME || join(homedir(), '.dsh')
|
|
54
|
+
roots.push(join(dshHome, 'node_modules', '@deepseek-ai', 'dsh-tools'))
|
|
55
|
+
for (const root of roots) {
|
|
56
|
+
const file = join(root, 'lib', 'index.js')
|
|
57
|
+
if (!existsSync(file)) continue
|
|
58
|
+
try {
|
|
59
|
+
const mod = await import(pathToFileURL(file).href)
|
|
60
|
+
if (mod?.defineTool) return mod.defineTool
|
|
61
|
+
} catch {}
|
|
62
|
+
}
|
|
63
|
+
return null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function makeImageTool(defineTool, ctx) {
|
|
67
|
+
return defineTool({
|
|
68
|
+
name: 'nai_image',
|
|
69
|
+
description:
|
|
70
|
+
'用 NovelAI(本机已配置好 key)画一张二次元插画。' +
|
|
71
|
+
'画好会保存成 PNG 并直接显示在回复里,同时给出文件路径。' +
|
|
72
|
+
'提示词用英文 Danbooru 标签最准(1girl, solo, silver hair, looking at viewer…)。' +
|
|
73
|
+
'**默认用 nai-diffusion-4-5-full**;只有用户明确说「用 V5 / v5」时才选 5 系(nai-diffusion-5-full / 5-curated)。' +
|
|
74
|
+
'**每次都会自动套用教程经验**:按模型转换权重写法、缺质量词自动补、缺负面自动补默认负面、超 token 预算会提醒;' +
|
|
75
|
+
'所以直接给 tag 就行,不必自己写 quality/negative(想完全手动可传 enhance:false)。' +
|
|
76
|
+
'有画风偏好就传 style,想更贴某个画风就传 artist(画师名,最有效)。' +
|
|
77
|
+
'默认 832×1216、28 步,一般 10~30 秒出图。' +
|
|
78
|
+
'给用户写回复时不要复述文件路径(路径含本机目录,会被 QQ 侧安全策略拦截),只说画好了、附上参数即可。' +
|
|
79
|
+
'同一时间只能有一个任务:上一张没结束就调用会返回「并发锁」。',
|
|
80
|
+
parameters: {
|
|
81
|
+
prompt: { type: 'string', required: true, description: '英文提示词,逗号分隔的标签' },
|
|
82
|
+
negative: { type: 'string', description: '负面提示词;不填用内置默认(低清、坏手、水印…);完全不要负面就传 "none"' },
|
|
83
|
+
model: {
|
|
84
|
+
type: 'string',
|
|
85
|
+
enum: Object.keys(MODELS),
|
|
86
|
+
description: '模型,默认 nai-diffusion-4-5-full;用户点名 V5 才用 nai-diffusion-5-full / nai-diffusion-5-curated',
|
|
87
|
+
},
|
|
88
|
+
size: {
|
|
89
|
+
type: 'string',
|
|
90
|
+
description: `尺寸,格式 宽x高。常用:${SIZES.map((s) => s[0]).join(' / ')}。默认 832x1216(竖)`,
|
|
91
|
+
},
|
|
92
|
+
steps: { type: 'number', description: '步数 1-50,默认 28(25~32 比较甜)' },
|
|
93
|
+
scale: { type: 'number', description: '提示词贴合度 CFG 1-10,默认 5' },
|
|
94
|
+
sampler: { type: 'string', enum: SAMPLERS, description: '采样器,默认 k_euler_ancestral' },
|
|
95
|
+
seed: { type: 'number', description: '种子;不填就是随机。想复现某张图就填上次返回的 seed' },
|
|
96
|
+
n: { type: 'number', description: '一次画几张 1-4,默认 1' },
|
|
97
|
+
style: {
|
|
98
|
+
type: 'string',
|
|
99
|
+
enum: Object.keys(STYLE_PRESETS),
|
|
100
|
+
description: '画风预设,会自动把该风格的招牌词拼到提示词前面(厚涂/平涂/水彩/油画/素描/线稿/像素/3D/Q版/漫画/立绘)',
|
|
101
|
+
},
|
|
102
|
+
artist: { type: 'string', description: '画师名,逗号分隔;会拼成 [[[artist:name]]],是控制画风最有效的手段' },
|
|
103
|
+
enhance: {
|
|
104
|
+
type: 'boolean',
|
|
105
|
+
description: '默认 true。自动补充(补质量词、补默认负面)默认开启;传 false 只关掉这两项补充,权重写法仍会按模型转换',
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
output: {
|
|
109
|
+
schema: {
|
|
110
|
+
type: 'object',
|
|
111
|
+
additionalProperties: false,
|
|
112
|
+
properties: {
|
|
113
|
+
images: { type: 'array', required: true, items: { type: 'json' }, description: '图片附件' },
|
|
114
|
+
text: { type: 'string', required: true, description: '给模型看的说明:路径 + 参数' },
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
render(_args, value) {
|
|
118
|
+
const blocks = (value.images || []).map((attachment) => ({ type: 'image', attachment }))
|
|
119
|
+
blocks.push({ type: 'text', text: value.text })
|
|
120
|
+
return blocks
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
timeoutMs: 300000,
|
|
124
|
+
isConcurrencySafe() {
|
|
125
|
+
return false
|
|
126
|
+
},
|
|
127
|
+
async execute(args) {
|
|
128
|
+
const cfg = loadConfig()
|
|
129
|
+
if (!cfg.apiKey) throw new Error('还没配置 NovelAI API Key(~/.dsh/novelai.json)')
|
|
130
|
+
const model = args.model || cfg.model
|
|
131
|
+
const v5 = isV5(model)
|
|
132
|
+
|
|
133
|
+
// 把教程里的经验落成行为:画风预设、画师串、权重写法按模型转换、质量词/负面兜底
|
|
134
|
+
let prompt = String(args.prompt || '').trim()
|
|
135
|
+
let negative = args.negative === 'none' ? ' ' : args.negative
|
|
136
|
+
const notes = []
|
|
137
|
+
const auto = args.enhance !== false
|
|
138
|
+
{
|
|
139
|
+
const pre = STYLE_PRESETS[args.style]
|
|
140
|
+
const head = []
|
|
141
|
+
if (args.artist) {
|
|
142
|
+
const names = String(args.artist).split(/[,,]/).map((x) => x.trim()).filter(Boolean)
|
|
143
|
+
if (names.length) head.push(names.map((n) => `[[[artist:${n}]]]`).join(', '))
|
|
144
|
+
if (pre && pre.artist === false) notes.push(`「${args.style}」不建议堆画师串,可能变成假${args.style}`)
|
|
145
|
+
}
|
|
146
|
+
if (pre) {
|
|
147
|
+
head.push(pre.tags.join(', '))
|
|
148
|
+
notes.push(`画风「${args.style}」:${pre.tip || '已套用'}`)
|
|
149
|
+
}
|
|
150
|
+
prompt = head.concat([prompt]).filter(Boolean).join(', ')
|
|
151
|
+
const before = prompt
|
|
152
|
+
prompt = normalizeWeights(prompt, model)
|
|
153
|
+
if (prompt !== before) notes.push(`权重写法已按 ${v5 ? 'V5(1.5:tag)' : 'V4.5(1.5::tag::)'} 转换`)
|
|
154
|
+
if (auto && !/quality|aesthetic|absurdres|masterpiece/i.test(prompt)) {
|
|
155
|
+
prompt += `, ${QUALITY}`
|
|
156
|
+
notes.push('补了质量词')
|
|
157
|
+
}
|
|
158
|
+
if (auto && !negative) {
|
|
159
|
+
negative = v5 ? NEG_V5 : NEG_V45
|
|
160
|
+
notes.push('补了默认负面')
|
|
161
|
+
}
|
|
162
|
+
const est = Math.round(prompt.split(/[\s,,]+/).filter(Boolean).length * 1.35)
|
|
163
|
+
if ((v5 && est > 750) || (!v5 && est > 512)) notes.push(`提示词约 ${est} token,超过${v5 ? ' V5 750' : ' V4.5 512'} 上限,后面会被稀释`)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const t0 = Date.now()
|
|
167
|
+
const result = await generate(cfg, { ...args, model, prompt, negative }, () => {})
|
|
168
|
+
const files = saveImages(cfg, result)
|
|
169
|
+
const attachments = []
|
|
170
|
+
for (const f of files) {
|
|
171
|
+
try {
|
|
172
|
+
attachments.push(
|
|
173
|
+
await ctx.attachments.saveImage({
|
|
174
|
+
data: readFileSync(f.path),
|
|
175
|
+
mediaType: 'image/png',
|
|
176
|
+
name: f.name,
|
|
177
|
+
}),
|
|
178
|
+
)
|
|
179
|
+
} catch (e) {
|
|
180
|
+
log('saveImage failed: ' + (e && e.message))
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const m = result.meta
|
|
184
|
+
const secs = ((Date.now() - t0) / 1000).toFixed(1)
|
|
185
|
+
const text = [
|
|
186
|
+
`已生成 ${files.length} 张 · ${secs}s · ${m.modelLabel} · ${m.width}×${m.height} · ${m.steps} 步 · CFG ${m.scale} · ${m.sampler} · seed ${m.seed ?? '随机'}`,
|
|
187
|
+
...files.map((f) => `文件:${f.path}`),
|
|
188
|
+
`提示词:${m.prompt}`,
|
|
189
|
+
...(notes.length ? ['套用:' + notes.join(';')] : []),
|
|
190
|
+
].join('\n')
|
|
191
|
+
return { images: attachments, text }
|
|
192
|
+
},
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function makeStatusTool(defineTool) {
|
|
197
|
+
return defineTool({
|
|
198
|
+
name: 'nai_status',
|
|
199
|
+
description: '查 NovelAI 账号状态:订阅档位、是否有效、到期时间、本月用量。生图失败怀疑额度/订阅问题时先查它。',
|
|
200
|
+
parameters: {},
|
|
201
|
+
output: {
|
|
202
|
+
schema: {
|
|
203
|
+
type: 'object',
|
|
204
|
+
additionalProperties: false,
|
|
205
|
+
properties: { text: { type: 'string', required: true } },
|
|
206
|
+
},
|
|
207
|
+
render(_args, value) {
|
|
208
|
+
return [{ type: 'text', text: value.text }]
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
timeoutMs: 30000,
|
|
212
|
+
async execute() {
|
|
213
|
+
const cfg = loadConfig()
|
|
214
|
+
if (!cfg.apiKey) return { text: '还没配置 API Key' }
|
|
215
|
+
try {
|
|
216
|
+
const a = await fetchAccount(cfg)
|
|
217
|
+
const exp = a.expiresAt ? new Date(a.expiresAt * 1000).toLocaleDateString('zh-CN') : '—'
|
|
218
|
+
return {
|
|
219
|
+
text: `${a.tierName}(tier ${a.tier})· ${a.active ? '有效' : '无效'} · 到期 ${exp} · ${a.unlimited ? 'Opus 不限量' : '用量 ' + (a.usage ?? '?') + '%'}`,
|
|
220
|
+
}
|
|
221
|
+
} catch (e) {
|
|
222
|
+
return { text: '查询失败:' + ((e && e.message) || e) }
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function makePromptTool(defineTool) {
|
|
229
|
+
return defineTool({
|
|
230
|
+
name: 'nai_prompt',
|
|
231
|
+
description:
|
|
232
|
+
'NovelAI 提示词助手(不用出图)。action=build:按主体/画风/模型组装一条能直接用的提示词+负面;action=check:体检一条提示词(括号、V4.5/V5 权重写法、长度、主体词、画面内文字语言);action=guide:返回提示词心法(结构、画师串、作品名复刻、负面、V5 长句、Anlas 规则)。style 可选:' +
|
|
233
|
+
Object.keys(STYLE_PRESETS).join('/'),
|
|
234
|
+
parameters: {
|
|
235
|
+
action: { type: 'string', required: true, enum: ['build', 'check', 'guide'], description: 'build=组装 / check=体检 / guide=心法' },
|
|
236
|
+
subject: { type: 'string', description: 'build 必填:主体描述,如「1girl, silver hair, school uniform, standing」' },
|
|
237
|
+
style: { type: 'string', enum: Object.keys(STYLE_PRESETS), description: 'build:画风预设' },
|
|
238
|
+
artist: { type: 'string', description: 'build:画师名,逗号分隔,会拼成 [[[artist:x]]]' },
|
|
239
|
+
scene: { type: 'string', description: 'build:场景/镜头,如「classroom, depth of field, upper body」' },
|
|
240
|
+
complexity: { type: 'string', enum: ['low', 'medium', 'high', 'ultra'], description: 'build:画面复杂度(V5 词条,一般用 high)' },
|
|
241
|
+
year: { type: 'string', description: 'build:年代词,如 2025' },
|
|
242
|
+
quality: { type: 'string', enum: ['auto', 'hi', 'none'], description: 'build:质量词强度,默认 auto' },
|
|
243
|
+
model: { type: 'string', description: '模型 id,默认取配置里的(决定 V4.5/V5 权重写法)' },
|
|
244
|
+
prompt: { type: 'string', description: 'check 必填:要体检的提示词' },
|
|
245
|
+
},
|
|
246
|
+
output: {
|
|
247
|
+
schema: {
|
|
248
|
+
type: 'object',
|
|
249
|
+
additionalProperties: false,
|
|
250
|
+
properties: { text: { type: 'string', required: true } },
|
|
251
|
+
},
|
|
252
|
+
render(_args, value) {
|
|
253
|
+
return [{ type: 'text', text: value.text }]
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
timeoutMs: 20000,
|
|
257
|
+
async execute(args = {}) {
|
|
258
|
+
const cfg = loadConfig()
|
|
259
|
+
const model = args.model || cfg.model || 'nai-diffusion-4-5-full'
|
|
260
|
+
if (args.action === 'guide') {
|
|
261
|
+
const lines = [
|
|
262
|
+
'【结构】' + GUIDE.structure,
|
|
263
|
+
'【画风】' + GUIDE.artists,
|
|
264
|
+
'【作品名】' + GUIDE.officialArt,
|
|
265
|
+
'【负面】' + GUIDE.negative,
|
|
266
|
+
'【V5】' + GUIDE.v5,
|
|
267
|
+
'【积分】' + GUIDE.anlas,
|
|
268
|
+
'',
|
|
269
|
+
'【画风预设】' + Object.keys(STYLE_PRESETS).join('、'),
|
|
270
|
+
'【权重】V4.5: ' + WEIGHT_RULES.v4 + ';V5: ' + WEIGHT_RULES.v5,
|
|
271
|
+
]
|
|
272
|
+
return { text: lines.join('\n') }
|
|
273
|
+
}
|
|
274
|
+
if (args.action === 'check') {
|
|
275
|
+
if (!args.prompt) return { text: 'check 需要 prompt 参数' }
|
|
276
|
+
const r = checkPrompt(args.prompt, model)
|
|
277
|
+
const out = [`模型 ${model}(${isV5(model) ? 'V5' : 'V4.5'})· 词条数 ${r.tags}`, r.ok ? '语法没发现问题。' : '问题:']
|
|
278
|
+
for (const i of r.issues) out.push('- ' + i)
|
|
279
|
+
if (r.suggest.length) {
|
|
280
|
+
out.push('建议:')
|
|
281
|
+
for (const s of r.suggest) out.push('- ' + s)
|
|
282
|
+
}
|
|
283
|
+
return { text: out.join('\n') }
|
|
284
|
+
}
|
|
285
|
+
if (!args.subject) return { text: 'build 需要 subject 参数' }
|
|
286
|
+
const b = buildPrompt({ ...args, model })
|
|
287
|
+
const out = ['【正向】', b.prompt, '', '【负面】', b.negative]
|
|
288
|
+
if (b.notes.length) {
|
|
289
|
+
out.push('', '【说明】')
|
|
290
|
+
for (const n of b.notes) out.push('- ' + n)
|
|
291
|
+
}
|
|
292
|
+
return { text: out.join('\n') }
|
|
293
|
+
},
|
|
294
|
+
})
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export async function apply(ctx) {
|
|
298
|
+
const offs = []
|
|
299
|
+
try {
|
|
300
|
+
const defineTool = await loadDefineTool()
|
|
301
|
+
if (!defineTool) throw new Error('找不到 @deepseek-ai/dsh-tools(可设 DSH_TOOLS_PATH)')
|
|
302
|
+
offs.push(ctx.tools.register(makeImageTool(defineTool, ctx)))
|
|
303
|
+
offs.push(ctx.tools.register(makeStatusTool(defineTool)))
|
|
304
|
+
offs.push(ctx.tools.register(makePromptTool(defineTool)))
|
|
305
|
+
log('tools registered: nai_image, nai_status, nai_prompt')
|
|
306
|
+
} catch (e) {
|
|
307
|
+
log('tool 注册失败:' + ((e && e.stack) || e))
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
const handler = createHandler({ prefix: '/dsh-novelai', log })
|
|
312
|
+
offs.push(
|
|
313
|
+
ctx.effect(() =>
|
|
314
|
+
ctx.webServer.register({
|
|
315
|
+
kind: 'prefix',
|
|
316
|
+
path: '/dsh-novelai',
|
|
317
|
+
handler: (req, res) => handler(req, res),
|
|
318
|
+
}),
|
|
319
|
+
),
|
|
320
|
+
)
|
|
321
|
+
log('page: /dsh-novelai/')
|
|
322
|
+
} catch (e) {
|
|
323
|
+
log('页面路由注册失败:' + ((e && e.stack) || e))
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return () => {
|
|
327
|
+
for (const off of offs.reverse()) {
|
|
328
|
+
try {
|
|
329
|
+
off && off()
|
|
330
|
+
} catch {}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-novelai",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "NovelAI 生图接入 DeepSeek Harness:nai_image / nai_status / nai_prompt 三个工具 + 画室页面 + 独立 studio,自带提示词知识库",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -45,9 +45,9 @@
|
|
|
45
45
|
"stable-diffusion",
|
|
46
46
|
"prompt"
|
|
47
47
|
],
|
|
48
|
-
"homepage": "https://github.com/
|
|
48
|
+
"homepage": "https://github.com/ysiguu/dsh-novelai",
|
|
49
49
|
"repository": {
|
|
50
50
|
"type": "git",
|
|
51
|
-
"url": "git+https://github.com/
|
|
51
|
+
"url": "git+https://github.com/ysiguu/dsh-novelai.git"
|
|
52
52
|
}
|
|
53
53
|
}
|