dsh-model 0.15.0 → 0.16.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/dist/cli.js +3 -1
- package/dist/commands/self.js +31 -1
- package/dist/commands/service.js +3 -0
- package/dist/commands/setup.js +4 -0
- package/dist/commands/uninstall.js +5 -0
- package/dist/daemon/core.js +15 -7
- package/dist/service/admin.js +86 -0
- package/package.json +1 -1
- package/plugin/client.js +12 -1
package/dist/cli.js
CHANGED
|
@@ -9,7 +9,7 @@ import { repair, service } from './commands/service.js';
|
|
|
9
9
|
import { setup } from './commands/setup.js';
|
|
10
10
|
import { doctor, status } from './commands/status.js';
|
|
11
11
|
import { uninstall } from './commands/uninstall.js';
|
|
12
|
-
import { selfUpdate } from './commands/self.js';
|
|
12
|
+
import { adminRun, selfUpdate } from './commands/self.js';
|
|
13
13
|
import { opencode } from './commands/opencode.js';
|
|
14
14
|
import { workbuddy } from './commands/workbuddy.js';
|
|
15
15
|
import { bridge } from './commands/bridge.js';
|
|
@@ -190,6 +190,8 @@ export async function main(argv) {
|
|
|
190
190
|
return await logs(ctx, o.lines ? Number(o.lines) : undefined, o.bridge ? 'bridge' : 'engine');
|
|
191
191
|
case 'uninstall':
|
|
192
192
|
return await uninstall(ctx, { yes: o.yes, keepAuth: o['keep-auth'], removePlugin: o['remove-plugin'] });
|
|
193
|
+
case 'admin-run':
|
|
194
|
+
return await adminRun(ctx);
|
|
193
195
|
case 'self-update':
|
|
194
196
|
case 'update':
|
|
195
197
|
return await selfUpdate(ctx, { to: o.to });
|
package/dist/commands/self.js
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
// 1. 新版本装到原来的位置:npm 全局装的就 npm install -g;从 dsh 管理页一键安装的(程序就在插件目录里)跳过这步;
|
|
4
4
|
// 2. dsh 里的插件换成同一版本(插件页的新代码要重启一次 dsh 才生效);
|
|
5
5
|
// 3. 用新版本执行一次 repair:重写服务文件、重启引擎与守护进程、同步模型。
|
|
6
|
-
import {
|
|
6
|
+
import { spawn } from 'node:child_process';
|
|
7
|
+
import { closeSync, openSync } from 'node:fs';
|
|
8
|
+
import { chown, realpath } from 'node:fs/promises';
|
|
7
9
|
import { dirname, join } from 'node:path';
|
|
8
10
|
import { requireRootInVps } from '../context.js';
|
|
9
11
|
import { DshModelError, isDshModelError } from '../errors.js';
|
|
@@ -15,6 +17,7 @@ import { run, runInherit } from '../util/exec.js';
|
|
|
15
17
|
import { exists } from '../util/fs.js';
|
|
16
18
|
import { info, next, ok, skip, warn } from '../util/output.js';
|
|
17
19
|
import { PKG_ROOT, pkgVersion } from '../util/pkg.js';
|
|
20
|
+
import { takeAdminRequest } from '../service/admin.js';
|
|
18
21
|
const REGISTRY = 'https://registry.npmjs.org/dsh-model';
|
|
19
22
|
/** a 比 b 新(只比 major.minor.patch) */
|
|
20
23
|
export function isNewer(a, b) {
|
|
@@ -114,3 +117,30 @@ export async function selfUpdate(ctx, opts = {}) {
|
|
|
114
117
|
next(L('重启一次 dsh,管理页的新代码才会生效', 'Restart dsh once so the management page picks up the new code'));
|
|
115
118
|
return 0;
|
|
116
119
|
}
|
|
120
|
+
/** 管理页请求的日志:更新写在 home 里;卸载会删 home,写在它外面 */
|
|
121
|
+
export function selfJobLog(ctx, action) {
|
|
122
|
+
return action === 'uninstall' ? join(dirname(ctx.paths.home), 'dsh-model-uninstall.log') : join(ctx.paths.home, 'update.log');
|
|
123
|
+
}
|
|
124
|
+
/** 由 dsh-model-admin.service(root)执行:取出管理页的请求单,只做 update / uninstall */
|
|
125
|
+
export async function adminRun(ctx) {
|
|
126
|
+
if (!ctx.isRoot)
|
|
127
|
+
throw new DshModelError('needs_root', L('admin-run 只由 dsh-model-admin 服务以 root 执行', 'admin-run is run by the dsh-model-admin service as root'));
|
|
128
|
+
const req = await takeAdminRequest(ctx);
|
|
129
|
+
if (!req)
|
|
130
|
+
return 0;
|
|
131
|
+
const log = selfJobLog(ctx, req.action);
|
|
132
|
+
const out = openSync(log, 'w', 0o600);
|
|
133
|
+
if (ctx.owner)
|
|
134
|
+
await chown(log, ctx.owner.uid, ctx.owner.gid).catch(() => { });
|
|
135
|
+
const args = req.action === 'update' ? ['update'] : ['uninstall', '--yes', '--remove-plugin', ...(req.keepAuth ? ['--keep-auth'] : [])];
|
|
136
|
+
const code = await new Promise((resolve) => {
|
|
137
|
+
const child = spawn(process.execPath, [join(PKG_ROOT, 'bin', 'dsh-model.js'), ...args, '--lang', req.lang ?? 'zh'], {
|
|
138
|
+
stdio: ['ignore', out, out],
|
|
139
|
+
env: { ...process.env, NO_COLOR: '1', NODE_NO_WARNINGS: '1' },
|
|
140
|
+
});
|
|
141
|
+
child.on('error', () => resolve(-1));
|
|
142
|
+
child.on('exit', (c) => resolve(c ?? -1));
|
|
143
|
+
});
|
|
144
|
+
closeSync(out);
|
|
145
|
+
return code;
|
|
146
|
+
}
|
package/dist/commands/service.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// commands/service.ts — service install|start|stop|restart|status|uninstall,以及 repair
|
|
2
|
+
import { installAdmin } from '../service/admin.js';
|
|
2
3
|
import { requireRootInVps } from '../context.js';
|
|
3
4
|
import { DshModelError } from '../errors.js';
|
|
4
5
|
import { L } from '../i18n.js';
|
|
@@ -65,6 +66,8 @@ export async function repair(ctx) {
|
|
|
65
66
|
throw new DshModelError('engine_unhealthy', L('引擎没有启动,查看 dsh-model logs', 'Engine did not start; see dsh-model logs'));
|
|
66
67
|
ok(L('服务已重装并重启', 'Service reinstalled and restarted'));
|
|
67
68
|
await ensureDaemon(ctx, all);
|
|
69
|
+
if (ctx.mode === 'vps')
|
|
70
|
+
await installAdmin(ctx);
|
|
68
71
|
}
|
|
69
72
|
await syncAll(ctx, all);
|
|
70
73
|
return 0;
|
package/dist/commands/setup.js
CHANGED
|
@@ -22,6 +22,7 @@ import { opencodeConfigured } from '../integrations/opencode.js';
|
|
|
22
22
|
import { ensureDaemon } from '../daemon/service.js';
|
|
23
23
|
import { installDshPlugin } from '../integrations/dshplugin.js';
|
|
24
24
|
import { enableCaddy } from './remote.js';
|
|
25
|
+
import { installAdmin } from '../service/admin.js';
|
|
25
26
|
import { DEFAULT_PUBLIC_PORT, publicHost } from '../remote/public.js';
|
|
26
27
|
export async function setup(ctx, opts) {
|
|
27
28
|
requireRootInVps(ctx);
|
|
@@ -91,6 +92,9 @@ export async function setup(ctx, opts) {
|
|
|
91
92
|
skip(L('已跳过(--skip-dsh-plugin)', 'Skipped (--skip-dsh-plugin)'));
|
|
92
93
|
else
|
|
93
94
|
await step('dsh-plugin', () => installDshPlugin(ctx, all));
|
|
95
|
+
// vps:让 dsh 管理页能直接点「更新 / 卸载」(root 执行,只认这两件事)
|
|
96
|
+
if (ctx.mode === 'vps' && !ctx.serviceDisabled)
|
|
97
|
+
await step('admin', () => installAdmin(ctx));
|
|
94
98
|
// vps:自动在 dsh 的域名上开对外端点(没有域名就用 IP),自己的其他设备直接能用
|
|
95
99
|
if (ctx.mode === 'vps') {
|
|
96
100
|
info('');
|
|
@@ -14,6 +14,7 @@ import { fail, info, ok, warn } from '../util/output.js';
|
|
|
14
14
|
import { disable as disableRemote } from './remote.js';
|
|
15
15
|
import { disableWorkbuddy, removeWorkbuddyPlugin } from '../integrations/workbuddy.js';
|
|
16
16
|
import { removeDshPlugin } from '../integrations/dshplugin.js';
|
|
17
|
+
import { removeAdmin } from '../service/admin.js';
|
|
17
18
|
async function confirm(question) {
|
|
18
19
|
if (!process.stdin.isTTY)
|
|
19
20
|
return false;
|
|
@@ -73,6 +74,10 @@ export async function uninstall(ctx, opts) {
|
|
|
73
74
|
ok(L(`dsh 配置已还原(patch:${r.patch},凭据:${r.cred})`, `dsh config restored (patch: ${r.patch}, credentials: ${r.cred})`));
|
|
74
75
|
});
|
|
75
76
|
await saveAll(ctx, all).catch(() => { });
|
|
77
|
+
await step('admin', async () => {
|
|
78
|
+
if (ctx.mode === 'vps')
|
|
79
|
+
await removeAdmin();
|
|
80
|
+
});
|
|
76
81
|
await step('service', async () => {
|
|
77
82
|
if (ctx.serviceDisabled)
|
|
78
83
|
return;
|
package/dist/daemon/core.js
CHANGED
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
import { spawn } from 'node:child_process';
|
|
8
8
|
import { closeSync, openSync } from 'node:fs';
|
|
9
9
|
import { readFile } from 'node:fs/promises';
|
|
10
|
-
import { homedir } from 'node:os';
|
|
11
10
|
import { join } from 'node:path';
|
|
12
|
-
import { latestVersion } from '../commands/self.js';
|
|
11
|
+
import { latestVersion, selfJobLog } from '../commands/self.js';
|
|
12
|
+
import { adminInstalled, adminRequestPath } from '../service/admin.js';
|
|
13
|
+
import { atomicWrite } from '../util/fs.js';
|
|
13
14
|
import { lang } from '../i18n.js';
|
|
14
15
|
import { which } from '../util/exec.js';
|
|
15
16
|
import { PKG_ROOT, pkgVersion } from '../util/pkg.js';
|
|
@@ -642,7 +643,7 @@ export class Daemon {
|
|
|
642
643
|
return {
|
|
643
644
|
version: pkgVersion(),
|
|
644
645
|
latest: await latestVersion(),
|
|
645
|
-
canManage: !vps,
|
|
646
|
+
canManage: !vps || (await adminInstalled()),
|
|
646
647
|
...(this.selfJob ? { job: { action: this.selfJob.action, startedAt: this.selfJob.startedAt, log: log.replace(/\x1b\[[0-9;]*m/g, '').slice(-6000) } } : {}),
|
|
647
648
|
commands: vps
|
|
648
649
|
? { update: 'sudo npm install -g dsh-model@latest && sudo dsh-model setup', uninstall: 'sudo dsh-model uninstall' }
|
|
@@ -654,10 +655,17 @@ export class Daemon {
|
|
|
654
655
|
* 用 detached 让它脱离守护进程的进程组(launchd 重启服务时不会被一起杀掉)。
|
|
655
656
|
*/
|
|
656
657
|
async startSelfJob(action, opts = {}) {
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
658
|
+
const log = selfJobLog(this.ctx, action);
|
|
659
|
+
if (this.ctx.mode === 'vps') {
|
|
660
|
+
// 守护进程是 dsh 用户:写请求单,由 dsh-model-admin(root)执行
|
|
661
|
+
if (!(await adminInstalled()))
|
|
662
|
+
throw new DshModelError('needs_root', L('服务器上要 root 权限,请在终端里执行命令(重跑一次 sudo dsh-model setup 之后就能在页面上点)', 'On the server this needs root; run the command in a terminal (after re-running sudo dsh-model setup, the buttons work here)'));
|
|
663
|
+
const req = { action, keepAuth: opts.keepAuth === true, lang: lang() };
|
|
664
|
+
await atomicWrite(adminRequestPath(this.ctx), JSON.stringify(req), { mode: 0o600 });
|
|
665
|
+
this.selfJob = { action, startedAt: new Date().toISOString(), log };
|
|
666
|
+
log_(`daemon: requested ${action} from dsh-model-admin`);
|
|
667
|
+
return { started: true, log };
|
|
668
|
+
}
|
|
661
669
|
const args = action === 'update' ? ['update'] : ['uninstall', '--yes', '--remove-plugin', ...(opts.keepAuth ? ['--keep-auth'] : [])];
|
|
662
670
|
const out = openSync(log, 'w', 0o600);
|
|
663
671
|
const cmd = [process.execPath, join(PKG_ROOT, 'bin', 'dsh-model.js'), ...args, '--lang', lang()];
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// service/admin.ts — VPS 上让 dsh 管理页能「更新 / 卸载」(设计 §14.9)
|
|
2
|
+
//
|
|
3
|
+
// 守护进程以 dsh 用户运行,而更新(npm 全局目录、系统服务)和卸载(Caddy、防火墙、系统服务)要 root。
|
|
4
|
+
// 所以 setup(root)装一个 systemd path 单元:守护进程在 <home>/admin/request 写一张请求单,
|
|
5
|
+
// systemd 以 root 运行 `dsh-model admin-run`,它只认两件固定的事:更新到 npm 最新版、卸载。别的一律拒绝。
|
|
6
|
+
import { chown, mkdir, readFile, rm } from 'node:fs/promises';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { run } from '../util/exec.js';
|
|
9
|
+
import { atomicWrite, exists } from '../util/fs.js';
|
|
10
|
+
import { PKG_ROOT } from '../util/pkg.js';
|
|
11
|
+
export const ADMIN_PATH_UNIT = '/etc/systemd/system/dsh-model-admin.path';
|
|
12
|
+
export const ADMIN_SERVICE_UNIT = '/etc/systemd/system/dsh-model-admin.service';
|
|
13
|
+
export const adminDir = (ctx) => join(ctx.paths.home, 'admin');
|
|
14
|
+
export const adminRequestPath = (ctx) => join(adminDir(ctx), 'request');
|
|
15
|
+
export async function adminInstalled() {
|
|
16
|
+
return exists(ADMIN_PATH_UNIT);
|
|
17
|
+
}
|
|
18
|
+
/** setup / repair(root)时装上;幂等 */
|
|
19
|
+
export async function installAdmin(ctx) {
|
|
20
|
+
const dir = adminDir(ctx);
|
|
21
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
22
|
+
if (ctx.owner)
|
|
23
|
+
await chown(dir, ctx.owner.uid, ctx.owner.gid);
|
|
24
|
+
const path = [
|
|
25
|
+
'# Generated by dsh-model. 由 dsh-model 生成:dsh 管理页的「更新 / 卸载」请求',
|
|
26
|
+
'[Unit]',
|
|
27
|
+
'Description=dsh-model admin requests from the dsh page',
|
|
28
|
+
'',
|
|
29
|
+
'[Path]',
|
|
30
|
+
`PathExists=${adminRequestPath(ctx)}`,
|
|
31
|
+
'Unit=dsh-model-admin.service',
|
|
32
|
+
'',
|
|
33
|
+
'[Install]',
|
|
34
|
+
'WantedBy=multi-user.target',
|
|
35
|
+
'',
|
|
36
|
+
].join('\n');
|
|
37
|
+
const service = [
|
|
38
|
+
'# Generated by dsh-model. 由 dsh-model 生成:以 root 执行管理页的请求(只认 update / uninstall)',
|
|
39
|
+
'[Unit]',
|
|
40
|
+
'Description=dsh-model admin (update / uninstall from the dsh page)',
|
|
41
|
+
'',
|
|
42
|
+
'[Service]',
|
|
43
|
+
'Type=oneshot',
|
|
44
|
+
`ExecStart=${process.execPath} ${join(PKG_ROOT, 'bin', 'dsh-model.js')} admin-run`,
|
|
45
|
+
`Environment=DSH_MODEL_HOME=${ctx.paths.home}`,
|
|
46
|
+
// 系统服务默认没有 HOME,npm 要用它放缓存
|
|
47
|
+
'Environment=HOME=/root',
|
|
48
|
+
'',
|
|
49
|
+
].join('\n');
|
|
50
|
+
const before = (await readFile(ADMIN_PATH_UNIT, 'utf8').catch(() => '')) + (await readFile(ADMIN_SERVICE_UNIT, 'utf8').catch(() => ''));
|
|
51
|
+
if (before === path + service)
|
|
52
|
+
return;
|
|
53
|
+
await atomicWrite(ADMIN_PATH_UNIT, path, { mode: 0o644 });
|
|
54
|
+
await atomicWrite(ADMIN_SERVICE_UNIT, service, { mode: 0o644 });
|
|
55
|
+
await run('systemctl', ['daemon-reload']);
|
|
56
|
+
await run('systemctl', ['enable', '--now', 'dsh-model-admin.path']);
|
|
57
|
+
}
|
|
58
|
+
export async function removeAdmin() {
|
|
59
|
+
if (!(await exists(ADMIN_PATH_UNIT)) && !(await exists(ADMIN_SERVICE_UNIT)))
|
|
60
|
+
return;
|
|
61
|
+
await run('systemctl', ['disable', '--now', 'dsh-model-admin.path']);
|
|
62
|
+
await rm(ADMIN_PATH_UNIT, { force: true });
|
|
63
|
+
await rm(ADMIN_SERVICE_UNIT, { force: true });
|
|
64
|
+
await run('systemctl', ['daemon-reload']);
|
|
65
|
+
}
|
|
66
|
+
/** 读并删掉请求单(先删,免得 path 单元反复触发);不合规的返回 null */
|
|
67
|
+
export async function takeAdminRequest(ctx) {
|
|
68
|
+
const file = adminRequestPath(ctx);
|
|
69
|
+
let raw = '';
|
|
70
|
+
try {
|
|
71
|
+
raw = await readFile(file, 'utf8');
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
await rm(file, { force: true });
|
|
77
|
+
try {
|
|
78
|
+
const r = JSON.parse(raw);
|
|
79
|
+
if (r.action !== 'update' && r.action !== 'uninstall')
|
|
80
|
+
return null;
|
|
81
|
+
return { action: r.action, keepAuth: r.keepAuth === true, lang: r.lang === 'en' ? 'en' : 'zh' };
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-model",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Many model sources behind one authenticated OpenAI-compatible endpoint for DeepSeek Harness (dsh) and your tools: WorkBuddy, OpenCode Zen and your CLI subscriptions (Codex, Kimi…), with a management page inside dsh. 多家模型汇成一个 OpenAI 兼容地址,dsh 和其他软件都能直接用。",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh-plugin",
|
package/plugin/client.js
CHANGED
|
@@ -565,10 +565,21 @@ window.__ModuleLoader__.load({
|
|
|
565
565
|
const [err, setErr] = useState('')
|
|
566
566
|
useEffect(() => {
|
|
567
567
|
let alive = true
|
|
568
|
-
|
|
568
|
+
// 服务比页面旧(没有 /self):也要给出办法,不能整块不显示
|
|
569
|
+
api('GET', '/self').then((r) => alive && setInfo(r), (e) => alive && setInfo({ old: e.code === 'not_found' }))
|
|
569
570
|
return () => { alive = false }
|
|
570
571
|
}, [])
|
|
571
572
|
if (!info) return null
|
|
573
|
+
if (!info.version) {
|
|
574
|
+
const cmd = 'npm install -g dsh-model && dsh-model setup'
|
|
575
|
+
return h('div', { style: S.section },
|
|
576
|
+
h('div', { style: S.h }, L('关于', 'About')),
|
|
577
|
+
h('div', { style: { ...S.card, padding: '12px 14px' } },
|
|
578
|
+
h('div', { style: S.meta }, info.old
|
|
579
|
+
? L('这台机器上的 dsh-model 服务比这个页面旧,不支持在页面上更新。在终端执行一次(服务器上加 sudo),之后就能在这里更新和卸载:', 'The dsh-model service on this machine is older than this page and cannot update from here. Run this once in a terminal (with sudo on a server); after that you can update and uninstall here:')
|
|
580
|
+
: L('暂时查不到版本信息。可以在终端执行(服务器上加 sudo):', 'Version info is unavailable right now. You can run in a terminal (with sudo on a server):')),
|
|
581
|
+
h('div', { style: { ...S.line, minHeight: 30, marginTop: 6 } }, h('code', { style: { ...S.mono, ...S.grow } }, cmd), h(CopyButton, { get: cmd }))))
|
|
582
|
+
}
|
|
572
583
|
const hasUpdate = info.latest && newer(info.latest, info.version)
|
|
573
584
|
const start = async (action) => {
|
|
574
585
|
setErr('')
|