dsh-model 0.18.1 → 0.19.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/plugin/index.js CHANGED
@@ -9,7 +9,8 @@
9
9
 
10
10
  import { spawn } from 'node:child_process'
11
11
  import { randomBytes } from 'node:crypto'
12
- import { access, readFile } from 'node:fs/promises'
12
+ import { readFileSync } from 'node:fs'
13
+ import { access, readFile, writeFile } from 'node:fs/promises'
13
14
  import { homedir } from 'node:os'
14
15
  import { join } from 'node:path'
15
16
  import { fileURLToPath } from 'node:url'
@@ -17,6 +18,30 @@ import { fileURLToPath } from 'node:url'
17
18
  /** 本包根目录:插件就是完整的 dsh-model 包,自带命令行 */
18
19
  const PKG_ROOT = fileURLToPath(new URL('..', import.meta.url))
19
20
  const VPS_ROOT = '/opt/dsh-vps'
21
+ const ADMIN_PATH_UNIT = '/etc/systemd/system/dsh-model-admin.path'
22
+
23
+ /** 这份代码加载时的版本:插件装了新版但 dsh 没重启时,它和磁盘上的版本不一样 */
24
+ const RUNNING_VERSION = (() => {
25
+ try {
26
+ return JSON.parse(readFileSync(join(PKG_ROOT, 'package.json'), 'utf8')).version
27
+ } catch {
28
+ return ''
29
+ }
30
+ })()
31
+
32
+ async function installedVersion() {
33
+ try {
34
+ return JSON.parse(await readFile(join(PKG_ROOT, 'package.json'), 'utf8')).version
35
+ } catch {
36
+ return RUNNING_VERSION
37
+ }
38
+ }
39
+
40
+ /** 页面能不能直接重启 dsh:只在 dsh-vps 上(交给 root 的 dsh-model-admin 执行 systemctl restart dsh-gate);其他环境提示手动重启 */
41
+ async function restartMode() {
42
+ if ((await exists(VPS_ROOT)) && (await exists(ADMIN_PATH_UNIT))) return 'vps'
43
+ return process.versions.electron ? 'desktop' : 'manual'
44
+ }
20
45
 
21
46
  export const name = 'dsh-model'
22
47
  export const inject = []
@@ -36,11 +61,14 @@ const ALLOWED = [
36
61
  ['POST', /^\/keys\/[A-Za-z0-9._-]{1,40}\/reveal$/],
37
62
  ['GET', /^\/endpoints$/],
38
63
  ['GET', /^\/self$/],
64
+ ['GET', /^\/failures$/],
65
+ ['POST', /^\/models\/test$/],
39
66
  ['POST', /^\/self\/(update|uninstall)$/],
40
67
  ['GET', /^\/sources\/[a-z-]{2,20}\/models$/],
41
68
  ['POST', /^\/sources\/[a-z-]{2,20}\/models$/],
42
69
  ['POST', /^\/sources\/[a-z-]{2,20}\/(enable|disable|logout)$/],
43
70
  ['POST', /^\/opencode\/key$/],
71
+ ['POST', /^\/apikey\/[a-z-]{2,20}$/],
44
72
  ['GET', /^\/login\/[A-Za-z0-9._-]{1,120}$/],
45
73
  ['POST', /^\/login\/[A-Za-z0-9._-]{1,120}\/callback$/],
46
74
  ['DELETE', /^\/login\/[A-Za-z0-9._-]{1,120}$/],
@@ -98,25 +126,26 @@ function check(req, token, needToken = true) {
98
126
  const exists = (p) => access(p).then(() => true, () => false)
99
127
 
100
128
  /** 从页面一键执行 setup / repair(只在本机模式:VPS 上要 root,页面只给命令)。同一时间只跑一个 */
101
- let running = null
102
- function runCli(args) {
103
- if (running) return running
104
- running = new Promise((resolve) => {
105
- let out = ''
106
- const child = spawn(process.execPath, [join(PKG_ROOT, 'bin/dsh-model.js'), ...args], {
107
- windowsHide: true,
108
- // dsh 桌面版可能跑在 Electron 里:让它当普通 Node 用
109
- env: { ...process.env, ELECTRON_RUN_AS_NODE: '1', NO_COLOR: '1', FORCE_COLOR: '0' },
110
- stdio: ['ignore', 'pipe', 'pipe'],
111
- })
112
- const add = (b) => { out = (out + b.toString()).slice(-20000) }
113
- child.stdout.on('data', add)
114
- child.stderr.on('data', add)
115
- const timer = setTimeout(() => child.kill('SIGTERM'), 10 * 60 * 1000)
116
- child.on('error', (e) => { clearTimeout(timer); resolve({ code: -1, output: `${out}\n${e.message}` }) })
117
- child.on('exit', (code) => { clearTimeout(timer); resolve({ code: code ?? -1, output: out.replace(/\x1b\[[0-9;]*m/g, '') }) })
118
- }).finally(() => { running = null })
119
- return running
129
+ /** 一键安装 / 修复的进度:页面每秒来取一次新输出 */
130
+ let job = null
131
+ function startCli(action, args) {
132
+ if (job && job.running) return job
133
+ job = { action, running: true, output: '', code: null, startedAt: Date.now() }
134
+ const current = job
135
+ const child = spawn(process.execPath, [join(PKG_ROOT, 'bin/dsh-model.js'), ...args], {
136
+ windowsHide: true,
137
+ // dsh 桌面版可能跑在 Electron 里:让它当普通 Node 用
138
+ env: { ...process.env, ELECTRON_RUN_AS_NODE: '1', NO_COLOR: '1', FORCE_COLOR: '0' },
139
+ stdio: ['ignore', 'pipe', 'pipe'],
140
+ })
141
+ const add = (b) => { current.output = (current.output + b.toString().replace(/\x1b\[[0-9;]*m/g, '')).slice(-20000) }
142
+ child.stdout.on('data', add)
143
+ child.stderr.on('data', add)
144
+ const timer = setTimeout(() => child.kill('SIGTERM'), 10 * 60 * 1000)
145
+ const done = (code) => { clearTimeout(timer); current.running = false; current.code = code }
146
+ child.on('error', (e) => { add(`\n${e.message}`); done(-1) })
147
+ child.on('exit', (code) => done(code ?? -1))
148
+ return job
120
149
  }
121
150
 
122
151
  export function apply(ctx) {
@@ -168,6 +197,30 @@ export function apply(ctx) {
168
197
  }),
169
198
  )
170
199
 
200
+ // 插件版本与重启:装了新版但 dsh 还在跑旧代码时,页面提示(VPS 上可一键重启)
201
+ track(
202
+ ws.register({
203
+ kind: 'exact',
204
+ path: '/api-dsh-model/host',
205
+ handler: async (req, res) => {
206
+ const bad = check(req, token)
207
+ if (bad) return json(res, bad.code, { error: { code: 'refused', message: bad.error } })
208
+ let body
209
+ try {
210
+ body = JSON.parse((await readBody(req)) || '{}')
211
+ } catch {
212
+ return json(res, 400, { error: { code: 'invalid_json', message: 'invalid JSON' } })
213
+ }
214
+ const mode = await restartMode()
215
+ if (body.action !== 'restart') return json(res, 200, { running: RUNNING_VERSION, installed: await installedVersion(), restart: mode })
216
+ if (mode !== 'vps') return json(res, 409, { error: { code: 'restart_unsupported', message: 'restart dsh manually' } })
217
+ // 写给 root 的 dsh-model-admin:它只认几件固定的事,这里是 restart-dsh
218
+ await writeFile(join(home(), 'admin', 'request'), JSON.stringify({ action: 'restart-dsh' }), { mode: 0o600 })
219
+ return json(res, 200, { restarting: true })
220
+ },
221
+ }),
222
+ )
223
+
171
224
  // 页面上的「一键安装 / 修复」
172
225
  track(
173
226
  ws.register({
@@ -183,10 +236,12 @@ export function apply(ctx) {
183
236
  return json(res, 400, { error: { code: 'invalid_json', message: 'invalid JSON' } })
184
237
  }
185
238
  if (await exists(VPS_ROOT)) return json(res, 200, { vps: true })
239
+ // { poll: true }:只取进度;否则开始(已经在跑就接着看同一个)
240
+ if (body.poll) return json(res, 200, job ? { running: job.running, output: job.output, code: job.code, action: job.action } : { running: false, output: '', code: null })
186
241
  const action = body.action === 'repair' ? 'repair' : 'setup'
187
242
  const lang = body.lang === 'en' ? 'en' : 'zh'
188
- const r = await runCli(action === 'repair' ? ['repair', '--lang', lang] : ['setup', '--skip-dsh-plugin', '--lang', lang])
189
- return json(res, 200, r)
243
+ const j = startCli(action, action === 'repair' ? ['repair', '--lang', lang] : ['setup', '--skip-dsh-plugin', '--lang', lang])
244
+ return json(res, 200, { running: j.running, output: j.output, code: j.code, action: j.action })
190
245
  },
191
246
  }),
192
247
  )