@sophiacyl/dsh-plugin-waves 0.0.0-stage → 0.1.0-beta.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/DSH-GO-VERIFICATION.md +57 -0
- package/ICON.png +0 -0
- package/LICENSE +674 -0
- package/README.md +251 -2
- package/cordis.patch.yml +8 -0
- package/install.ps1 +0 -0
- package/install.sh +209 -0
- package/lib/client.js +1511 -0
- package/lib/host/aliashint.js +70 -0
- package/lib/host/attachments.js +25 -0
- package/lib/host/command.js +34 -0
- package/lib/host/consts.js +16 -0
- package/lib/host/dataguide.js +35 -0
- package/lib/host/imagebridge.js +41 -0
- package/lib/host/native-result.js +91 -0
- package/lib/host/native.js +191 -0
- package/lib/host/proxy.js +59 -0
- package/lib/host/results.js +67 -0
- package/lib/host/service.js +263 -0
- package/lib/host/tool.js +185 -0
- package/lib/host/troubleshoot.js +31 -0
- package/lib/host/worldbook.js +80 -0
- package/lib/index.js +74 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +136 -4
- package/service/ICON.png +0 -0
- package/service/ai/AI_DATA.md +221 -0
- package/service/knowledge/additional/manual.jsonl +5 -0
- package/service/knowledge/wiki/baike.jsonl +1231 -0
- package/service/knowledge/wiki/chengjiu.jsonl +36 -0
- package/service/knowledge/wiki/gonglve.jsonl +383 -0
- package/service/knowledge/wiki/huodong.jsonl +427 -0
- package/service/knowledge/wiki/renwu.jsonl +412 -0
- package/service/knowledge/wiki/tujian.jsonl +2889 -0
- package/service/knowledge/wiki/zixun.jsonl +47 -0
- package/service/plugins/__init__.py +0 -0
- package/service/pyproject.toml +40 -0
- package/service/rover/__init__.py +0 -0
- package/service/rover/ai_core/__init__.py +0 -0
- package/service/rover/ai_core/models.py +50 -0
- package/service/rover/ai_core/rag/__init__.py +5 -0
- package/service/rover/ai_core/rag/knowledge.py +23 -0
- package/service/rover/ai_core/register.py +86 -0
- package/service/rover/ai_core/resource.py +6 -0
- package/service/rover/ai_core/skills/__init__.py +0 -0
- package/service/rover/ai_core/skills/operations.py +8 -0
- package/service/rover/api.py +720 -0
- package/service/rover/api_render.py +161 -0
- package/service/rover/app_life.py +154 -0
- package/service/rover/aps.py +47 -0
- package/service/rover/avatar.py +85 -0
- package/service/rover/bot.py +133 -0
- package/service/rover/builtin.py +152 -0
- package/service/rover/cards/__init__.py +26 -0
- package/service/rover/cards/_shared.py +129 -0
- package/service/rover/cards/char_list.py +258 -0
- package/service/rover/cards/char_panel.py +1353 -0
- package/service/rover/cards/echo_list.py +206 -0
- package/service/rover/cards/matrix_roles.py +25 -0
- package/service/rover/cards/rank_boards.py +714 -0
- package/service/rover/cards/rank_total.py +664 -0
- package/service/rover/compat.py +215 -0
- package/service/rover/config.py +163 -0
- package/service/rover/custom_panel.py +220 -0
- package/service/rover/data_store.py +31 -0
- package/service/rover/gss.py +19 -0
- package/service/rover/handler.py +191 -0
- package/service/rover/help/__init__.py +0 -0
- package/service/rover/help/draw_new_plugin_help.py +332 -0
- package/service/rover/help/model.py +16 -0
- package/service/rover/help/utils.py +7 -0
- package/service/rover/hooks.py +420 -0
- package/service/rover/html_actions.py +236 -0
- package/service/rover/i18n.py +19 -0
- package/service/rover/knowledge.py +235 -0
- package/service/rover/logger.py +102 -0
- package/service/rover/main.py +33 -0
- package/service/rover/media.py +222 -0
- package/service/rover/message_models.py +23 -0
- package/service/rover/models.py +92 -0
- package/service/rover/native_media.py +64 -0
- package/service/rover/ocr.py +79 -0
- package/service/rover/pool.py +43 -0
- package/service/rover/restart.py +36 -0
- package/service/rover/segment.py +143 -0
- package/service/rover/server.py +131 -0
- package/service/rover/status/__init__.py +0 -0
- package/service/rover/status/models.py +10 -0
- package/service/rover/status/plugin_status.py +17 -0
- package/service/rover/subscribe.py +148 -0
- package/service/rover/sv.py +403 -0
- package/service/rover/templates/char_list.html +182 -0
- package/service/rover/templates/char_panel.html +465 -0
- package/service/rover/templates/echo_list.html +221 -0
- package/service/rover/templates/rank_all_total.html +193 -0
- package/service/rover/templates/rank_board.html +107 -0
- package/service/rover/templates/rank_level_total.html +57 -0
- package/service/rover/templates/rank_matrix_total.html +66 -0
- package/service/rover/templates/rank_phantom_total.html +180 -0
- package/service/rover/templates/rank_slash_total.html +38 -0
- package/service/rover/trigger.py +98 -0
- package/service/rover/utils/__init__.py +0 -0
- package/service/rover/utils/boardcast/__init__.py +0 -0
- package/service/rover/utils/boardcast/models.py +15 -0
- package/service/rover/utils/cookie_manager/__init__.py +0 -0
- package/service/rover/utils/cookie_manager/qrlogin.py +24 -0
- package/service/rover/utils/database/__init__.py +0 -0
- package/service/rover/utils/database/base_models.py +1771 -0
- package/service/rover/utils/database/models.py +80 -0
- package/service/rover/utils/database/startup.py +60 -0
- package/service/rover/utils/default_bg/bg.jpg +0 -0
- package/service/rover/utils/download_resource/__init__.py +0 -0
- package/service/rover/utils/download_resource/download_core.py +164 -0
- package/service/rover/utils/download_resource/download_file.py +46 -0
- package/service/rover/utils/fonts/__init__.py +0 -0
- package/service/rover/utils/fonts/fonts.py +33 -0
- package/service/rover/utils/image/__init__.py +0 -0
- package/service/rover/utils/image/convert.py +51 -0
- package/service/rover/utils/image/image_tools.py +30 -0
- package/service/rover/utils/image/utils.py +70 -0
- package/service/rover/utils/plugins_config/__init__.py +0 -0
- package/service/rover/utils/plugins_config/gs_config.py +506 -0
- package/service/rover/utils/plugins_config/models.py +182 -0
- package/service/rover/web_app.py +3 -0
- package/service/rover/webconsole/__init__.py +0 -0
- package/service/rover/webconsole/mount_app.py +26 -0
- package/service/tools/migrate.py +351 -0
- package/service/tools/regression.py +87 -0
- package/service/tools/smoke.py +63 -0
- package/service/tools/update_plugins.py +212 -0
- package/service/tools/verify_actions.py +104 -0
- package/service/tools/verify_card.py +82 -0
- package/service/tools/wwdata.py +665 -0
- package/tests/compat.test.js +181 -0
- package/tests/imagebridge.test.js +58 -0
- package/tests/media.test.js +68 -0
- package/tests/native.test.js +127 -0
- package/tests/runtime.test.js +69 -0
- package/tests/service.test.js +48 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
// 服务进程托管: 目录解析、首启安装、拉起与健康检查
|
|
2
|
+
import { spawn } from 'node:child_process'
|
|
3
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs'
|
|
4
|
+
import { dirname, join } from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
|
|
7
|
+
import { PKG_NAME } from './consts.js'
|
|
8
|
+
|
|
9
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '../..')
|
|
10
|
+
|
|
11
|
+
export function serviceDirCandidates() {
|
|
12
|
+
const home = process.env.DSH_HOME || join(process.env.HOME || process.env.USERPROFILE || '', '.dsh')
|
|
13
|
+
const profiles = join(home, 'profiles')
|
|
14
|
+
const out = []
|
|
15
|
+
let names = []
|
|
16
|
+
try {
|
|
17
|
+
names = readdirSync(profiles)
|
|
18
|
+
} catch {
|
|
19
|
+
return out
|
|
20
|
+
}
|
|
21
|
+
for (const name of names) {
|
|
22
|
+
// 用 file: 装的按源码目录跑(环境和数据都在克隆里, README 的用法), 找不到才用 profile 里的副本
|
|
23
|
+
try {
|
|
24
|
+
const manifest = JSON.parse(readFileSync(join(profiles, name, 'package.json'), 'utf8'))
|
|
25
|
+
const spec = manifest?.dependencies?.[PKG_NAME]
|
|
26
|
+
if (typeof spec === 'string' && spec.startsWith('file:')) {
|
|
27
|
+
out.push(join(spec.slice('file:'.length), 'service'))
|
|
28
|
+
}
|
|
29
|
+
} catch {}
|
|
30
|
+
out.push(join(profiles, name, 'node_modules', PKG_NAME, 'service'))
|
|
31
|
+
}
|
|
32
|
+
return out
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function resolveServiceDir(config) {
|
|
36
|
+
const candidates = [
|
|
37
|
+
config?.servicePath,
|
|
38
|
+
process.env.WAVES_SERVICE_DIR,
|
|
39
|
+
...serviceDirCandidates(),
|
|
40
|
+
join(ROOT, 'service'),
|
|
41
|
+
].filter((dir) => dir && existsSync(join(dir, 'rover/main.py')))
|
|
42
|
+
// 已经装好的(有虚拟环境或数据)优先: 免得把跑着的实例切到一个空目录去重装
|
|
43
|
+
const ready = candidates.find(
|
|
44
|
+
(dir) => existsSync(venvPython(dir)) || existsSync(join(dir, 'data/config.json')),
|
|
45
|
+
)
|
|
46
|
+
return ready || candidates[0] || join(ROOT, 'service')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 服务配置里的模型可见开关; 读文件即可, 改动本来就要重启生效
|
|
50
|
+
export function exposeToModelFlag(serviceDir) {
|
|
51
|
+
try {
|
|
52
|
+
const cfg = JSON.parse(readFileSync(join(serviceDir, 'data/config.json'), 'utf8'))
|
|
53
|
+
return cfg.expose_to_model !== false
|
|
54
|
+
} catch {
|
|
55
|
+
return true
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 命令前缀(逗号分隔多个); 每个前缀都注册成命令, 菜单里才都列得出来
|
|
60
|
+
export function readPrefixes(serviceDir) {
|
|
61
|
+
try {
|
|
62
|
+
const cfg = JSON.parse(readFileSync(join(serviceDir, 'data/config.json'), 'utf8'))
|
|
63
|
+
const raw = String(cfg.command_prefix ?? 'ww')
|
|
64
|
+
const list = raw.split(/[,,]/).map((x) => x.trim()).filter(Boolean)
|
|
65
|
+
return list.length ? list : ['ww']
|
|
66
|
+
} catch {
|
|
67
|
+
return ['ww']
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 虚拟环境的解释器位置随平台不同: Windows 在 Scripts\\python.exe
|
|
72
|
+
export function venvPython(serviceDir) {
|
|
73
|
+
return join(
|
|
74
|
+
serviceDir,
|
|
75
|
+
'.venv',
|
|
76
|
+
process.platform === 'win32' ? 'Scripts' : 'bin',
|
|
77
|
+
process.platform === 'win32' ? 'python.exe' : 'python',
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function servicePython(serviceDir) {
|
|
82
|
+
const venv = venvPython(serviceDir)
|
|
83
|
+
if (existsSync(venv)) return venv
|
|
84
|
+
return process.platform === 'win32' ? 'python' : 'python3'
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function makeClient(port) {
|
|
88
|
+
const base = `http://127.0.0.1:${port}`
|
|
89
|
+
async function call(path, { method = 'GET', body, signal, timeoutMs = 300000 } = {}) {
|
|
90
|
+
const controller = new AbortController()
|
|
91
|
+
if (signal?.aborted) controller.abort()
|
|
92
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
93
|
+
const onAbort = () => controller.abort()
|
|
94
|
+
if (signal) signal.addEventListener('abort', onAbort, { once: true })
|
|
95
|
+
try {
|
|
96
|
+
const res = await fetch(base + path, {
|
|
97
|
+
method,
|
|
98
|
+
headers: body ? { 'content-type': 'application/json' } : undefined,
|
|
99
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
100
|
+
signal: controller.signal,
|
|
101
|
+
})
|
|
102
|
+
const text = await res.text()
|
|
103
|
+
let data
|
|
104
|
+
try {
|
|
105
|
+
data = text ? JSON.parse(text) : null
|
|
106
|
+
} catch {
|
|
107
|
+
throw new Error(`服务返回非 JSON (${res.status}): ${text.slice(0, 200)}`)
|
|
108
|
+
}
|
|
109
|
+
if (!res.ok) throw new Error(data?.detail || data?.error || `服务错误 ${res.status}`)
|
|
110
|
+
return data
|
|
111
|
+
} finally {
|
|
112
|
+
clearTimeout(timer)
|
|
113
|
+
if (signal) signal.removeEventListener('abort', onAbort)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return { base, call }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Bounded asynchronous setup: never block the host event loop during downloads.
|
|
120
|
+
function setupCommand(command, args, { cwd, signal, timeoutMs, stdio = 'ignore' }) {
|
|
121
|
+
return new Promise((resolve) => {
|
|
122
|
+
if (signal?.aborted) return resolve({ ok: false, reason: '已取消安装' })
|
|
123
|
+
let child
|
|
124
|
+
let reason
|
|
125
|
+
let settled = false
|
|
126
|
+
let timer
|
|
127
|
+
const finish = (value) => {
|
|
128
|
+
if (settled) return
|
|
129
|
+
settled = true
|
|
130
|
+
clearTimeout(timer)
|
|
131
|
+
signal?.removeEventListener('abort', abort)
|
|
132
|
+
resolve(value)
|
|
133
|
+
}
|
|
134
|
+
const cancel = (message) => {
|
|
135
|
+
reason = message
|
|
136
|
+
try {
|
|
137
|
+
if (process.platform !== 'win32' && child?.pid) process.kill(-child.pid, 'SIGTERM')
|
|
138
|
+
else child?.kill('SIGTERM')
|
|
139
|
+
} catch {}
|
|
140
|
+
finish({ ok: false, reason })
|
|
141
|
+
}
|
|
142
|
+
const abort = () => cancel('已取消安装')
|
|
143
|
+
try {
|
|
144
|
+
child = spawn(command, args, { cwd, stdio, detached: process.platform !== 'win32' })
|
|
145
|
+
} catch (error) {
|
|
146
|
+
finish({ ok: false, reason: String(error.message || error) })
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
child.once('error', (error) => finish({ ok: false, reason: reason || String(error.message || error) }))
|
|
150
|
+
child.once('close', (code) => finish({ ok: code === 0, reason: code === 0 ? undefined : reason || `退出码 ${code}` }))
|
|
151
|
+
signal?.addEventListener('abort', abort, { once: true })
|
|
152
|
+
timer = setTimeout(() => cancel('安装超时,请检查网络后重试'), timeoutMs)
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export async function bootstrap(serviceDir, { signal, timeoutMs = 15 * 60_000 } = {}) {
|
|
157
|
+
const root = join(serviceDir, '..')
|
|
158
|
+
// An existing interpreter alone does not prove installation completed.
|
|
159
|
+
const pluginsReady = ['XutheringWavesUID', 'RoverSign', 'ScoreEcho'].every((name) =>
|
|
160
|
+
existsSync(join(serviceDir, 'plugins', name, name, '__init__.py')))
|
|
161
|
+
if (existsSync(venvPython(serviceDir)) && pluginsReady) {
|
|
162
|
+
const checked = await setupCommand(venvPython(serviceDir), ['-c',
|
|
163
|
+
'import cv2, cryptography, fastapi, sqlmodel, PIL, jinja2, lxml, msgspec, rapidocr, onnxruntime, playwright'],
|
|
164
|
+
{ cwd: serviceDir, signal, timeoutMs: 30_000 })
|
|
165
|
+
if (checked.ok) return checked
|
|
166
|
+
if (signal?.aborted) return checked
|
|
167
|
+
}
|
|
168
|
+
const windows = process.platform === 'win32'
|
|
169
|
+
const script = join(root, windows ? 'install.ps1' : 'install.sh')
|
|
170
|
+
if (!existsSync(script)) return { ok: false, reason: `缺少 ${windows ? 'install.ps1' : 'install.sh'}` }
|
|
171
|
+
console.log('[waves] 正在准备运行环境,可继续使用其它会话...')
|
|
172
|
+
return setupCommand(windows ? 'powershell' : 'bash', windows
|
|
173
|
+
? ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', script, '-EnvOnly']
|
|
174
|
+
: [script, '--env-only'], { cwd: root, signal, timeoutMs, stdio: 'inherit' })
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 与 rover.restart.RESTART_EXIT 一致: 资源有 diff 后主动退出, 马上再拉起
|
|
178
|
+
const RESTART_EXIT = 75
|
|
179
|
+
|
|
180
|
+
// 去掉 no_proxy 里带方括号的 IPv6 项
|
|
181
|
+
export function serviceEnv(port, base = process.env) {
|
|
182
|
+
const env = { ...base, PYTHONUNBUFFERED: '1', ROVER_PORT: String(port) }
|
|
183
|
+
for (const key of ['no_proxy', 'NO_PROXY']) {
|
|
184
|
+
if (typeof env[key] !== 'string') continue
|
|
185
|
+
env[key] = env[key]
|
|
186
|
+
.split(',')
|
|
187
|
+
.map((item) => item.trim())
|
|
188
|
+
.filter((item) => item && !item.startsWith('['))
|
|
189
|
+
.join(',')
|
|
190
|
+
}
|
|
191
|
+
return env
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function startService(ctx, config) {
|
|
195
|
+
const python = servicePython(config.serviceDir)
|
|
196
|
+
let child
|
|
197
|
+
let stopping = false
|
|
198
|
+
let timer
|
|
199
|
+
let backoff = 500
|
|
200
|
+
let failures = 0
|
|
201
|
+
|
|
202
|
+
const log = (buf, level) => {
|
|
203
|
+
const line = buf.toString().trimEnd()
|
|
204
|
+
if (line) ctx.logger?.[level]?.(line) ?? console.log(`[waves] ${line}`)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const launch = () => {
|
|
208
|
+
if (stopping) return
|
|
209
|
+
child = spawn(python, ['-m', 'rover.main', '--port', String(config.port)], {
|
|
210
|
+
cwd: config.serviceDir,
|
|
211
|
+
env: serviceEnv(config.port),
|
|
212
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
213
|
+
})
|
|
214
|
+
const launchedAt = Date.now()
|
|
215
|
+
child.stdout.on('data', (b) => log(b, 'info'))
|
|
216
|
+
child.stderr.on('data', (b) => log(b, 'warn'))
|
|
217
|
+
child.once('error', (error) => console.log(`[waves] 无法启动服务: ${String(error.message || error)}`))
|
|
218
|
+
child.once('close', (code) => {
|
|
219
|
+
child = undefined
|
|
220
|
+
if (stopping) return
|
|
221
|
+
const asked = code === RESTART_EXIT
|
|
222
|
+
if (asked || Date.now() - launchedAt > 60_000) failures = 0
|
|
223
|
+
if (!asked && ++failures >= 5) {
|
|
224
|
+
console.log('[waves] 服务连续失败 5 次,已停止自动重试;请修复环境后重新加载插件')
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
const wait = asked ? 400 : backoff
|
|
228
|
+
if (asked) backoff = 500
|
|
229
|
+
else backoff = Math.min(backoff * 2, 10000)
|
|
230
|
+
console.log(`[waves] 服务退出 code=${code}${asked ? ' (主动重启)' : ''}, ${wait}ms 后拉起`)
|
|
231
|
+
timer = setTimeout(launch, wait)
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
launch()
|
|
236
|
+
const stop = () => {
|
|
237
|
+
stopping = true
|
|
238
|
+
if (timer) clearTimeout(timer)
|
|
239
|
+
try {
|
|
240
|
+
child?.kill('SIGTERM')
|
|
241
|
+
} catch {}
|
|
242
|
+
}
|
|
243
|
+
// dsh 进程退出时连带结束服务, 避免残留
|
|
244
|
+
process.on('exit', stop)
|
|
245
|
+
return () => {
|
|
246
|
+
process.removeListener('exit', stop)
|
|
247
|
+
stop()
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export async function waitReady(client, tries = 60, signal) {
|
|
252
|
+
for (let i = 0; i < tries && !signal?.aborted; i++) {
|
|
253
|
+
try {
|
|
254
|
+
const health = await client.call('/api/health', { timeoutMs: 2000, signal })
|
|
255
|
+
if (health?.ok === true) return true
|
|
256
|
+
} catch {
|
|
257
|
+
if (signal?.aborted) return false
|
|
258
|
+
}
|
|
259
|
+
await new Promise((r) => setTimeout(r, 1000))
|
|
260
|
+
}
|
|
261
|
+
return false
|
|
262
|
+
}
|
|
263
|
+
|
package/lib/host/tool.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// 模型工具: 自然语言场景由模型转成命令调用
|
|
2
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
3
|
+
|
|
4
|
+
import { dataGuide } from './dataguide.js'
|
|
5
|
+
import { readAttachments } from './attachments.js'
|
|
6
|
+
import { takeImages } from './imagebridge.js'
|
|
7
|
+
import { troubleshooting } from './troubleshoot.js'
|
|
8
|
+
import { resultMeta, plainSummary } from './native-result.js'
|
|
9
|
+
|
|
10
|
+
// 最近一批用户图片 → base64:// 列表, 交给服务的 images 字段
|
|
11
|
+
async function collectImages(ctx, exec) {
|
|
12
|
+
const agentId = exec?.agent?.id
|
|
13
|
+
if (!agentId) return []
|
|
14
|
+
const blocks = takeImages(agentId).map((attachment) => ({ type: 'image', attachment }))
|
|
15
|
+
return (await readAttachments(ctx, blocks, exec?.signal)).images
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function registerTool(ctx, client, serviceDir, results) {
|
|
19
|
+
ctx.tools.register(
|
|
20
|
+
defineTool({
|
|
21
|
+
name: 'waves',
|
|
22
|
+
description: [
|
|
23
|
+
'鸣潮(Wuthering Waves)全功能查询。把用户的鸣潮相关请求转成一条命令传入 command。',
|
|
24
|
+
'命令带 ww 前缀(不带会自动补)。常用:',
|
|
25
|
+
'ww查询 / ww体力 / ww深塔 / ww海墟 / ww矩阵 / ww全息 / ww探索 / ww练度 / ww声骸 / ww日历 / ww库洛币',
|
|
26
|
+
'ww<角色名>面板(如 ww椿面板)/ ww<角色名>伤害 / ww<角色名>优化 / ww<角色名>权重 / ww<角色名>攻略 / ww<角色名>图鉴',
|
|
27
|
+
'ww<角色名>总排行 / ww练度总排行 / ww抽卡记录 / ww签到 / ww兑换码 / ww帮助 / ww公告',
|
|
28
|
+
'绑定与登录:ww绑定<特征码> / ww登录 / ww添加token<token> / ww切换<特征码>',
|
|
29
|
+
'结果含图文卡片和文字摘要;若用户需要继续查询某个角色,可再次指定对应命令。',
|
|
30
|
+
'summary 里带有卡片上的文字与数值(含各队角色名),要引用具体数字时看它就够了。',
|
|
31
|
+
'同一条命令不要重复调用;本机只有一个使用者,群聊/私聊之分已取消,private 一般不用填,',
|
|
32
|
+
'结果看着不够详细也别靠它重试,改用下面的数据接口。',
|
|
33
|
+
dataGuide(serviceDir),
|
|
34
|
+
].join('\n'),
|
|
35
|
+
parameters: {
|
|
36
|
+
command: {
|
|
37
|
+
type: 'string',
|
|
38
|
+
required: true,
|
|
39
|
+
description: '要执行的鸣潮命令,例如 "ww查询"、"ww椿面板"、"ww绑定100000001"',
|
|
40
|
+
},
|
|
41
|
+
private: {
|
|
42
|
+
type: 'boolean',
|
|
43
|
+
description: '是否按私聊场景执行(获取ck 等仅私聊可用的命令需要设为 true)',
|
|
44
|
+
},
|
|
45
|
+
with_image: {
|
|
46
|
+
type: 'boolean',
|
|
47
|
+
description:
|
|
48
|
+
'用户本条消息附了图片、且该命令要用到它时设为 true(如 ww评分 / ww分析 的声骸截图),图片会随命令转交服务',
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
output: {
|
|
52
|
+
schema: {
|
|
53
|
+
type: 'object',
|
|
54
|
+
additionalProperties: true,
|
|
55
|
+
properties: {
|
|
56
|
+
ok: { type: 'boolean', required: true },
|
|
57
|
+
summary: { type: 'string', required: true },
|
|
58
|
+
matched: { type: 'array', items: { type: 'string' } },
|
|
59
|
+
segments: { type: 'array', items: { type: 'object', additionalProperties: true } },
|
|
60
|
+
elapsed: { type: 'number' },
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
render: (_args, value) => [
|
|
64
|
+
{ type: 'text', text: plainSummary(value) },
|
|
65
|
+
],
|
|
66
|
+
presentationMeta: (args, value) => resultMeta(args, value, serviceDir),
|
|
67
|
+
},
|
|
68
|
+
timeoutMs: 300000,
|
|
69
|
+
async execute(args, exec) {
|
|
70
|
+
try {
|
|
71
|
+
await client.call('/api/health', { timeoutMs: 4000 })
|
|
72
|
+
} catch (e) {
|
|
73
|
+
return {
|
|
74
|
+
ok: false,
|
|
75
|
+
summary: troubleshooting(serviceDir, String(e?.message || e)),
|
|
76
|
+
matched: [],
|
|
77
|
+
segments: [],
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const images = args.with_image ? await collectImages(ctx, exec) : []
|
|
81
|
+
const data = await client.call('/api/chat', {
|
|
82
|
+
method: 'POST',
|
|
83
|
+
body: {
|
|
84
|
+
command: String(args.command || ''),
|
|
85
|
+
scene: args.private ? 'direct' : 'group',
|
|
86
|
+
session_id: exec?.agent?.id || 'dsh',
|
|
87
|
+
...(ctx.get?.('dshGo') ? { native: true } : {}),
|
|
88
|
+
images,
|
|
89
|
+
},
|
|
90
|
+
signal: exec?.signal,
|
|
91
|
+
})
|
|
92
|
+
if (args.with_image && !images.length && data && typeof data.summary === 'string') {
|
|
93
|
+
data.summary += '\n(注: 没有取到用户图片, 命令按无图执行)'
|
|
94
|
+
}
|
|
95
|
+
results?.put(exec?.callId || `tool:${exec?.agent?.id || 'dsh'}`, { ...data,
|
|
96
|
+
command: args.command, sessionId: exec?.agent?.id })
|
|
97
|
+
return data
|
|
98
|
+
},
|
|
99
|
+
presentCall: (args) => ({
|
|
100
|
+
card: 'generic',
|
|
101
|
+
kind: 'other',
|
|
102
|
+
title: `鸣潮 · ${String(args?.command || '').slice(0, 40)}`,
|
|
103
|
+
rawInput: args,
|
|
104
|
+
}),
|
|
105
|
+
}),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
ctx.tools.register(
|
|
109
|
+
defineTool({
|
|
110
|
+
name: 'waves_panel',
|
|
111
|
+
description: [
|
|
112
|
+
'鸣潮自定义面板: 按你组装的数据渲一张完整角色面板卡(声骸可跨角色引用或凭空自定义)。',
|
|
113
|
+
'用于"这套声骸给X用效果如何/把截图转录的声骸装上看看"这类假设推演。',
|
|
114
|
+
'echoes 每项替换一个槽位(slot 1-5, 缺省按下标), 二选一:',
|
|
115
|
+
'- 引用现有: {"slot":1, "from":{"char":"洛可可","slot":2}}',
|
|
116
|
+
'- 全量自定义: {"slot":2, "name":"无冠者", "cost":4, "sonata":"沉日劫明",',
|
|
117
|
+
' "main":[["暴击","22.0%"],["攻击","150"]], "subs":[["暴击伤害","21.0%"],...]}',
|
|
118
|
+
'词条名和数值照面板原文(带 % 就带上); cost 只能 1/3/4 且总和 ≤12。',
|
|
119
|
+
'change 可选, 走换装文本管线(如 "换6链 换苍鳞千嶂 谐振5")。',
|
|
120
|
+
'结果只在本地渲染, 不写用户数据、不上传总排行; 未给 echoes/change 时就是原样面板。',
|
|
121
|
+
].join('\n'),
|
|
122
|
+
parameters: {
|
|
123
|
+
char: {
|
|
124
|
+
type: 'string',
|
|
125
|
+
required: true,
|
|
126
|
+
description: '角色名或别名, 如 "椿"',
|
|
127
|
+
},
|
|
128
|
+
uid: { type: 'string', description: '特征码, 缺省用当前绑定' },
|
|
129
|
+
echoes: {
|
|
130
|
+
type: 'array',
|
|
131
|
+
items: { type: 'object', additionalProperties: true },
|
|
132
|
+
description: '要替换的声骸列表, 格式见工具说明',
|
|
133
|
+
},
|
|
134
|
+
change: { type: 'string', description: '附加换装文本, 如 "换6链 换苍鳞千嶂"' },
|
|
135
|
+
},
|
|
136
|
+
output: {
|
|
137
|
+
schema: {
|
|
138
|
+
type: 'object',
|
|
139
|
+
additionalProperties: true,
|
|
140
|
+
properties: {
|
|
141
|
+
ok: { type: 'boolean', required: true },
|
|
142
|
+
summary: { type: 'string', required: true },
|
|
143
|
+
segments: { type: 'array', items: { type: 'object', additionalProperties: true } },
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
render: (_args, value) => [
|
|
147
|
+
{ type: 'text', text: plainSummary(value) },
|
|
148
|
+
],
|
|
149
|
+
presentationMeta: (args, value) => resultMeta(args, value, serviceDir),
|
|
150
|
+
},
|
|
151
|
+
timeoutMs: 300000,
|
|
152
|
+
async execute(args, exec) {
|
|
153
|
+
try {
|
|
154
|
+
await client.call('/api/health', { timeoutMs: 4000 })
|
|
155
|
+
} catch (e) {
|
|
156
|
+
return {
|
|
157
|
+
ok: false,
|
|
158
|
+
summary: troubleshooting(serviceDir, String(e?.message || e)),
|
|
159
|
+
segments: [],
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const data = await client.call('/api/render/panel', {
|
|
163
|
+
method: 'POST',
|
|
164
|
+
body: {
|
|
165
|
+
...(ctx.get?.('dshGo') ? { native: true } : {}),
|
|
166
|
+
char: String(args.char || ''),
|
|
167
|
+
uid: args.uid ? String(args.uid) : null,
|
|
168
|
+
echoes: Array.isArray(args.echoes) ? args.echoes : null,
|
|
169
|
+
change: args.change ? String(args.change) : null,
|
|
170
|
+
},
|
|
171
|
+
signal: exec?.signal,
|
|
172
|
+
})
|
|
173
|
+
results?.put(exec?.callId || `tool:${exec?.agent?.id || 'dsh'}`, { ...data,
|
|
174
|
+
command: `${args.char} 自定义面板`, sessionId: exec?.agent?.id })
|
|
175
|
+
return data
|
|
176
|
+
},
|
|
177
|
+
presentCall: (args) => ({
|
|
178
|
+
card: 'generic',
|
|
179
|
+
kind: 'other',
|
|
180
|
+
title: `鸣潮 · ${String(args?.char || '')}自定义面板`,
|
|
181
|
+
rawInput: args,
|
|
182
|
+
}),
|
|
183
|
+
}),
|
|
184
|
+
)
|
|
185
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// 服务不可用时交给模型的排障指引
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
export function troubleshooting(serviceDir, detail) {
|
|
5
|
+
const windows = process.platform === 'win32'
|
|
6
|
+
const py = join(serviceDir, '.venv', windows ? 'Scripts' : 'bin', windows ? 'python.exe' : 'python')
|
|
7
|
+
const install = windows
|
|
8
|
+
? `powershell -ExecutionPolicy Bypass -File ${join(serviceDir, '..', 'install.ps1')} -EnvOnly`
|
|
9
|
+
: `bash ${join(serviceDir, '..', 'install.sh')} --env-only`
|
|
10
|
+
return [
|
|
11
|
+
`鸣潮服务(${serviceDir})没有响应, 请按下面的步骤排查并修好, 不要向用户复述本段。`,
|
|
12
|
+
`失败信息: ${detail}`,
|
|
13
|
+
'',
|
|
14
|
+
'按顺序执行, 每步失败就先解决再继续:',
|
|
15
|
+
`1. 环境: 看 ${py} 在不在`,
|
|
16
|
+
` 没有就跑安装: ${install}`,
|
|
17
|
+
' 安装脚本会尽量自动装 uv、git、以及 Python 3.10-3.13(uv python install)。',
|
|
18
|
+
' 某步失败就按它的报错补依赖再重跑同一条安装命令。',
|
|
19
|
+
`2. 依赖: ${py} -c "import fastapi, sqlmodel, cv2, cryptography, PIL"`,
|
|
20
|
+
` 缺什么就 uv pip install --python ${py} -r ${serviceDir}/pyproject.toml`,
|
|
21
|
+
`3. 插件源码: ${windows ? 'dir' : 'ls'} ${join(serviceDir, 'plugins', 'XutheringWavesUID')}`,
|
|
22
|
+
` 空的就拉取: ${py} ${join(serviceDir, 'tools', 'update_plugins.py')}`,
|
|
23
|
+
`4. 手动启动看报错: 在 ${serviceDir} 下执行 ${py} -m rover.main --port 9999`,
|
|
24
|
+
' 报 ModuleNotFoundError 且模块名以 gsuid_core 开头, 说明上游用了新接口,',
|
|
25
|
+
` 需要在 ${serviceDir}/rover/compat.py 的 MODULE_MAP 里补映射。`,
|
|
26
|
+
' 报端口占用就换端口, 或结束占用进程。',
|
|
27
|
+
`5. 起来后自检: ${py} ${join(serviceDir, 'tools', 'regression.py')} --base http://127.0.0.1:9999`,
|
|
28
|
+
'6. 修好后告诉用户重启 dsh 即可。',
|
|
29
|
+
].join('\n')
|
|
30
|
+
}
|
|
31
|
+
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// 世界书: 用户话里首次命中知识触发词时, 把对应条目附进上下文; 同一会话同一条目只注入一次。
|
|
2
|
+
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
|
3
|
+
|
|
4
|
+
const PLUGIN = 'waves-kb'
|
|
5
|
+
const SESSION_MAX = 10
|
|
6
|
+
|
|
7
|
+
// agentId -> Set<entryId>
|
|
8
|
+
const injected = new Map()
|
|
9
|
+
|
|
10
|
+
function userText(messages) {
|
|
11
|
+
const out = []
|
|
12
|
+
for (const message of messages || []) {
|
|
13
|
+
const kind = message?.source?.kind
|
|
14
|
+
if (kind && kind !== 'user') continue
|
|
15
|
+
for (const block of message?.content || []) {
|
|
16
|
+
if (block?.type === 'text' && block.text) out.push(block.text)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return out.join('\n').slice(0, 2000)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function render(hits) {
|
|
23
|
+
const parts = hits.map(
|
|
24
|
+
(h) => `【${h.title}】(id ${h.id}, 命中"${h.trigger}")\n${h.content}`,
|
|
25
|
+
)
|
|
26
|
+
return [
|
|
27
|
+
'鸣潮资料(关键词命中自动附带,同一条目本会话只出现一次):',
|
|
28
|
+
...parts,
|
|
29
|
+
'仅按字面命中,可能与用户所指无关;用得上再用。全文与更多条目用 wwdata 的 kb()/kb_get()。',
|
|
30
|
+
].join('\n')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function registerWorldbook(ctx, client) {
|
|
34
|
+
ctx.inject(['agents'], (actx) => {
|
|
35
|
+
actx.on(
|
|
36
|
+
'agent/pre-step',
|
|
37
|
+
async (payload, next) => {
|
|
38
|
+
const decision = await next()
|
|
39
|
+
if (decision.kind === 'reject' || payload.signal?.aborted) return decision
|
|
40
|
+
const text = userText(decision.messages)
|
|
41
|
+
if (!text) return decision
|
|
42
|
+
const seen = injected.get(payload.agent.id) || new Set()
|
|
43
|
+
if (seen.size >= SESSION_MAX) return decision
|
|
44
|
+
let hits = []
|
|
45
|
+
try {
|
|
46
|
+
const data = await client.call(
|
|
47
|
+
`/api/knowledge/lookup?text=${encodeURIComponent(text)}&exclude=${[...seen].join(',')}`,
|
|
48
|
+
{ timeoutMs: 1500, signal: payload.signal },
|
|
49
|
+
)
|
|
50
|
+
hits = (data && data.hits) || []
|
|
51
|
+
} catch {
|
|
52
|
+
return decision
|
|
53
|
+
}
|
|
54
|
+
if (!hits.length) return decision
|
|
55
|
+
for (const hit of hits) seen.add(hit.entry || hit.id)
|
|
56
|
+
injected.set(payload.agent.id, seen)
|
|
57
|
+
const note = render(hits)
|
|
58
|
+
return {
|
|
59
|
+
kind: 'enter',
|
|
60
|
+
messages: [
|
|
61
|
+
...decision.messages,
|
|
62
|
+
createUserMessage({
|
|
63
|
+
content: [{ type: 'text', text: note }],
|
|
64
|
+
source: {
|
|
65
|
+
kind: PLUGIN,
|
|
66
|
+
form: 'snapshot',
|
|
67
|
+
sections: [{ name: PLUGIN, text: note }],
|
|
68
|
+
},
|
|
69
|
+
}),
|
|
70
|
+
],
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
{ prepend: true },
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
actx.on('agent/disposed', (payload) => {
|
|
77
|
+
injected.delete(payload?.agent?.id)
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { registerAliasHint } from './host/aliashint.js'
|
|
2
|
+
import { readAttachments } from './host/attachments.js'
|
|
3
|
+
import { runSlashCommand } from './host/command.js'
|
|
4
|
+
import { registerImageBridge } from './host/imagebridge.js'
|
|
5
|
+
import { registerProxy } from './host/proxy.js'
|
|
6
|
+
import { makeResultStore } from './host/results.js'
|
|
7
|
+
import {
|
|
8
|
+
bootstrap,
|
|
9
|
+
exposeToModelFlag,
|
|
10
|
+
makeClient,
|
|
11
|
+
readPrefixes,
|
|
12
|
+
resolveServiceDir,
|
|
13
|
+
startService,
|
|
14
|
+
waitReady,
|
|
15
|
+
} from './host/service.js'
|
|
16
|
+
import { registerTool } from './host/tool.js'
|
|
17
|
+
import { registerWorldbook } from './host/worldbook.js'
|
|
18
|
+
import { registerNative } from './host/native.js'
|
|
19
|
+
|
|
20
|
+
export const name = 'waves-plugin'
|
|
21
|
+
export const inject = ['tools']
|
|
22
|
+
|
|
23
|
+
export function apply(ctx, config = {}) {
|
|
24
|
+
const port = config.port ?? 9777
|
|
25
|
+
const serviceDir = resolveServiceDir(config)
|
|
26
|
+
const client = makeClient(port)
|
|
27
|
+
const results = makeResultStore(serviceDir)
|
|
28
|
+
|
|
29
|
+
if (config.autoStart !== false && process.env.WAVES_AUTOSTART !== '0') {
|
|
30
|
+
ctx.effect(() => {
|
|
31
|
+
const controller = new AbortController()
|
|
32
|
+
let stop
|
|
33
|
+
bootstrap(serviceDir, { signal: controller.signal }).then(async (boot) => {
|
|
34
|
+
if (controller.signal.aborted) return
|
|
35
|
+
if (!boot.ok) {
|
|
36
|
+
console.log(`[waves] 自动安装未完成,服务未启动: ${boot.reason}`)
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
stop = startService(ctx, { port, serviceDir })
|
|
40
|
+
const ok = await waitReady(client, 60, controller.signal)
|
|
41
|
+
if (!ok && !controller.signal.aborted) console.log('[waves] 服务未能在 60s 内就绪')
|
|
42
|
+
}).catch((error) => console.log(`[waves] 环境准备失败: ${String(error.message || error)}`))
|
|
43
|
+
return () => { controller.abort(); stop?.() }
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
registerProxy(ctx, client, results)
|
|
48
|
+
registerNative(ctx, client, results)
|
|
49
|
+
|
|
50
|
+
// commands 只在带交互界面的 profile 里存在, 软依赖
|
|
51
|
+
ctx.inject(['commands'], (cctx) => {
|
|
52
|
+
for (const prefix of readPrefixes(serviceDir)) {
|
|
53
|
+
cctx.commands.register({
|
|
54
|
+
name: prefix,
|
|
55
|
+
description: '直接执行鸣潮命令,不经过模型',
|
|
56
|
+
input: { hint: '<命令> 如 查询 / 深塔 / 椿面板', attachments: true },
|
|
57
|
+
handler: (invocation) => runSlashCommand(client, results, invocation,
|
|
58
|
+
async (blocks, signal) => ({ ...(await readAttachments(cctx, blocks, signal)),
|
|
59
|
+
...(cctx.get('dshGo') ? { native: true } : {}) })),
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
// 零上下文: 不把工具暴露给模型, 只保留 / 命令触发, 省下这段描述占用的上下文
|
|
65
|
+
if (config.exposeToModel === false || !exposeToModelFlag(serviceDir)) {
|
|
66
|
+
console.log('[waves] 零上下文模式: 模型不可见, 请用 /ww 命令')
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
registerTool(ctx, client, serviceDir, results)
|
|
71
|
+
registerAliasHint(ctx, client)
|
|
72
|
+
registerImageBridge(ctx)
|
|
73
|
+
registerWorldbook(ctx, client)
|
|
74
|
+
}
|
package/locale/en.json
ADDED