dsh-model 0.0.1 → 0.15.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/README.en.md +127 -0
- package/README.md +96 -43
- package/bin/dsh-model.js +8 -0
- package/dist/bridge/aggregate.js +91 -0
- package/dist/bridge/login.js +127 -0
- package/dist/bridge/runtime.js +136 -0
- package/dist/bridge/server.js +183 -0
- package/dist/bridge/workbuddy/_compat.js +52 -0
- package/dist/bridge/workbuddy/app-version.js +141 -0
- package/dist/bridge/workbuddy/auth.js +548 -0
- package/dist/bridge/workbuddy/catalog.js +154 -0
- package/dist/bridge/workbuddy/client-identity.js +234 -0
- package/dist/bridge/workbuddy/desktop-credential-protection.js +992 -0
- package/dist/bridge/workbuddy/loopback.js +52 -0
- package/dist/bridge/workbuddy/probe.js +153 -0
- package/dist/bridge/workbuddy/status-paths.js +49 -0
- package/dist/bridge/workbuddy/upstream.js +1180 -0
- package/dist/bridge/workbuddy/variants.js +101 -0
- package/dist/cli.js +215 -0
- package/dist/commands/bridge.js +68 -0
- package/dist/commands/connect.js +67 -0
- package/dist/commands/engine.js +122 -0
- package/dist/commands/key.js +75 -0
- package/dist/commands/login.js +113 -0
- package/dist/commands/logs.js +19 -0
- package/dist/commands/opencode.js +83 -0
- package/dist/commands/remote.js +129 -0
- package/dist/commands/self.js +116 -0
- package/dist/commands/service.js +72 -0
- package/dist/commands/setup.js +219 -0
- package/dist/commands/sources.js +196 -0
- package/dist/commands/status.js +216 -0
- package/dist/commands/uninstall.js +97 -0
- package/dist/commands/workbuddy.js +117 -0
- package/dist/context.js +86 -0
- package/dist/daemon/client.js +27 -0
- package/dist/daemon/core.js +850 -0
- package/dist/daemon/service.js +33 -0
- package/dist/daemon/stats.js +126 -0
- package/dist/daemon/usage.js +213 -0
- package/dist/dsh/cli.js +90 -0
- package/dist/dsh/connect.js +257 -0
- package/dist/dsh/credentials.js +55 -0
- package/dist/dsh/locate.js +129 -0
- package/dist/dsh/patch.js +106 -0
- package/dist/dsh/pick.js +59 -0
- package/dist/engine/auth.js +40 -0
- package/dist/engine/client.js +104 -0
- package/dist/engine/compat.js +94 -0
- package/dist/engine/config.js +47 -0
- package/dist/engine/install.js +118 -0
- package/dist/engine/mgmt.js +99 -0
- package/dist/engine/sourcemap.js +48 -0
- package/dist/errors.js +14 -0
- package/dist/i18n.js +39 -0
- package/dist/integrations/dshplugin.js +89 -0
- package/dist/integrations/opencode.js +101 -0
- package/dist/integrations/workbuddy.js +114 -0
- package/dist/keys.js +54 -0
- package/dist/ops.js +178 -0
- package/dist/remote/public.js +109 -0
- package/dist/secrets.js +13 -0
- package/dist/service/index.js +48 -0
- package/dist/service/launchd.js +89 -0
- package/dist/service/systemd.js +83 -0
- package/dist/sources.js +36 -0
- package/dist/state.js +90 -0
- package/dist/upstreams.js +23 -0
- package/dist/util/diff.js +30 -0
- package/dist/util/exec.js +42 -0
- package/dist/util/fs.js +107 -0
- package/dist/util/output.js +50 -0
- package/dist/util/pkg.js +13 -0
- package/dist/util/port.js +16 -0
- package/dist/util/prompt.js +40 -0
- package/dist/util/proxy.js +73 -0
- package/dist/util/redact.js +7 -0
- package/engine-manifest.json +28 -0
- package/package.json +77 -7
- package/plugin/client.js +688 -0
- package/plugin/cordis.patch.yml +4 -0
- package/plugin/index.js +231 -0
- package/docs/DESIGN.md +0 -610
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// commands/uninstall.ts — 按台账逆序拆除:remote → dsh 接线 → 服务 → home。每步失败不中断,最后汇总
|
|
2
|
+
import { cp, rm } from 'node:fs/promises';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { createInterface } from 'node:readline/promises';
|
|
6
|
+
import { requireRootInVps } from '../context.js';
|
|
7
|
+
import { disconnectDsh } from '../dsh/connect.js';
|
|
8
|
+
import { listAuthFiles } from '../engine/auth.js';
|
|
9
|
+
import { L } from '../i18n.js';
|
|
10
|
+
import { loadAll, saveAll } from '../ops.js';
|
|
11
|
+
import { serviceFor } from '../service/index.js';
|
|
12
|
+
import { exists, timestamp } from '../util/fs.js';
|
|
13
|
+
import { fail, info, ok, warn } from '../util/output.js';
|
|
14
|
+
import { disable as disableRemote } from './remote.js';
|
|
15
|
+
import { disableWorkbuddy, removeWorkbuddyPlugin } from '../integrations/workbuddy.js';
|
|
16
|
+
import { removeDshPlugin } from '../integrations/dshplugin.js';
|
|
17
|
+
async function confirm(question) {
|
|
18
|
+
if (!process.stdin.isTTY)
|
|
19
|
+
return false;
|
|
20
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
21
|
+
try {
|
|
22
|
+
return /^(y|yes|是)$/i.test((await rl.question(`${question} [y/N] `)).trim());
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
rl.close();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export async function uninstall(ctx, opts) {
|
|
29
|
+
requireRootInVps(ctx);
|
|
30
|
+
if (!(await exists(ctx.paths.home))) {
|
|
31
|
+
info(L('没有安装过 dsh-model(或已卸载)', 'dsh-model is not installed (or already removed)'));
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
const all = await loadAll(ctx);
|
|
35
|
+
const authCount = (await listAuthFiles(ctx)).length;
|
|
36
|
+
if (!opts.yes) {
|
|
37
|
+
info(L('将要:关闭远程访问 → 移除 WorkBuddy bridge → 还原 dsh 配置 → 移除引擎服务 → 删除 ' + ctx.paths.home + '(含 OpenCode key 与 WorkBuddy 令牌副本)', 'This will: disable remote access → remove the WorkBuddy bridge → restore dsh config → remove the engine service → delete ' + ctx.paths.home + ' (incl. the OpenCode key and WorkBuddy token copies)'));
|
|
38
|
+
if (authCount && !opts.keepAuth)
|
|
39
|
+
info(L(`其中包括 ${authCount} 个上游登录凭据(加 --keep-auth 可先备份出来)`, `including ${authCount} upstream login credentials (add --keep-auth to back them up first)`));
|
|
40
|
+
if (!(await confirm(L('确认卸载?', 'Proceed with uninstall?')))) {
|
|
41
|
+
info(L('已取消', 'Cancelled'));
|
|
42
|
+
return 1;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const errors = [];
|
|
46
|
+
const step = async (name, fn) => {
|
|
47
|
+
try {
|
|
48
|
+
await fn();
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
errors.push(`${name}: ${error.message}`);
|
|
52
|
+
fail(`${name}: ${error.message}`);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
await step('remote', async () => {
|
|
56
|
+
if (all.state.remote && all.state.remote.mode !== 'off')
|
|
57
|
+
await disableRemote(ctx, all);
|
|
58
|
+
});
|
|
59
|
+
await step('dsh-plugin', async () => {
|
|
60
|
+
if (await removeDshPlugin(ctx, all, { force: opts.removePlugin }))
|
|
61
|
+
ok(L('已从 dsh 移除 dsh-model 插件', 'Removed the dsh-model plugin from dsh'));
|
|
62
|
+
});
|
|
63
|
+
await step('workbuddy', async () => {
|
|
64
|
+
if (await removeWorkbuddyPlugin(ctx, all))
|
|
65
|
+
ok(L('已移除上一版安装的 WorkBuddy 插件', 'Removed the WorkBuddy plugin installed by a previous version'));
|
|
66
|
+
if (await disableWorkbuddy(ctx, all))
|
|
67
|
+
ok(L('WorkBuddy bridge 服务已移除', 'WorkBuddy bridge service removed'));
|
|
68
|
+
});
|
|
69
|
+
await step('dsh', async () => {
|
|
70
|
+
if (!all.state.dsh)
|
|
71
|
+
return;
|
|
72
|
+
const r = await disconnectDsh(ctx, all.state);
|
|
73
|
+
ok(L(`dsh 配置已还原(patch:${r.patch},凭据:${r.cred})`, `dsh config restored (patch: ${r.patch}, credentials: ${r.cred})`));
|
|
74
|
+
});
|
|
75
|
+
await saveAll(ctx, all).catch(() => { });
|
|
76
|
+
await step('service', async () => {
|
|
77
|
+
if (ctx.serviceDisabled)
|
|
78
|
+
return;
|
|
79
|
+
await serviceFor(ctx).uninstall();
|
|
80
|
+
ok(L('系统服务已移除', 'Service removed'));
|
|
81
|
+
});
|
|
82
|
+
await step('auth-backup', async () => {
|
|
83
|
+
if (!opts.keepAuth || !authCount)
|
|
84
|
+
return;
|
|
85
|
+
const dest = join(homedir(), `dsh-model-auth-backup-${timestamp()}`);
|
|
86
|
+
await cp(ctx.paths.auth, dest, { recursive: true, filter: (src) => !src.includes('/logs') });
|
|
87
|
+
ok(L(`登录凭据已备份到 ${dest}`, `Credentials backed up to ${dest}`));
|
|
88
|
+
});
|
|
89
|
+
if (errors.length) {
|
|
90
|
+
warn(L(`有 ${errors.length} 步失败,保留了 ${ctx.paths.home} 以便重试(再次执行 uninstall)`, `${errors.length} step(s) failed; kept ${ctx.paths.home} so you can retry (run uninstall again)`));
|
|
91
|
+
return 1;
|
|
92
|
+
}
|
|
93
|
+
await rm(ctx.paths.home, { recursive: true, force: true });
|
|
94
|
+
ok(L(`已删除 ${ctx.paths.home},卸载完成`, `Deleted ${ctx.paths.home}; uninstall complete`));
|
|
95
|
+
info(L('最后可执行:npm uninstall -g dsh-model', 'Finally you can run: npm uninstall -g dsh-model'));
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// commands/workbuddy.ts — dsh-model workbuddy [status|login|logout|enable|refresh|disable]
|
|
2
|
+
import { requireRootInVps } from '../context.js';
|
|
3
|
+
import { availableVariants, loadCatalogs } from '../bridge/runtime.js';
|
|
4
|
+
import { DshModelError } from '../errors.js';
|
|
5
|
+
import { L } from '../i18n.js';
|
|
6
|
+
import { disableWorkbuddy, enableWorkbuddy, refreshBridge, riskNotice } from '../integrations/workbuddy.js';
|
|
7
|
+
import { workbuddyLogin, workbuddyLogout } from '../bridge/login.js';
|
|
8
|
+
import { WORKBUDDY_VARIANTS } from '../bridge/workbuddy/variants.js';
|
|
9
|
+
import { bold } from '../util/output.js';
|
|
10
|
+
import { loadAll, saveAll, syncAll } from '../ops.js';
|
|
11
|
+
import { withLock } from '../state.js';
|
|
12
|
+
import { info, isJsonMode, ok, printJson, skip, warn } from '../util/output.js';
|
|
13
|
+
/** cn / ai(也认 workbuddy / workbuddy-ai),默认国内版 */
|
|
14
|
+
function pickVariant(arg) {
|
|
15
|
+
const a = (arg ?? 'cn').toLowerCase();
|
|
16
|
+
const id = a === 'ai' || a === 'workbuddy-ai' || a === 'global' ? 'workbuddy-ai' : a === 'cn' || a === 'workbuddy' ? 'workbuddy' : '';
|
|
17
|
+
const v = WORKBUDDY_VARIANTS.find((x) => x.id === id);
|
|
18
|
+
if (!v)
|
|
19
|
+
throw new DshModelError('unknown_workbuddy_product', L(`不认识的产品:${arg}(cn 或 ai)`, `Unknown product: ${arg} (cn or ai)`));
|
|
20
|
+
return v;
|
|
21
|
+
}
|
|
22
|
+
export async function workbuddy(ctx, sub, arg) {
|
|
23
|
+
if (sub === 'login')
|
|
24
|
+
return login(ctx, pickVariant(arg));
|
|
25
|
+
if (sub === 'logout')
|
|
26
|
+
return logout(ctx, pickVariant(arg));
|
|
27
|
+
if (!sub || sub === 'status') {
|
|
28
|
+
const [apps, cats] = await Promise.all([availableVariants(ctx.paths.home), loadCatalogs(ctx.paths.home)]);
|
|
29
|
+
if (isJsonMode()) {
|
|
30
|
+
printJson({ apps: apps.map((v) => v.displayName), catalogs: cats.map(({ models, ...c }) => ({ ...c, models: models.length })) });
|
|
31
|
+
return 0;
|
|
32
|
+
}
|
|
33
|
+
info(L(`App:${apps.length ? apps.map((v) => v.displayName).join('、') : '未检测到'}`, `App: ${apps.length ? apps.map((v) => v.displayName).join(', ') : 'not found'}`));
|
|
34
|
+
for (const c of cats) {
|
|
35
|
+
info(` ${c.label}: ${c.signedIn ? L(`已登录${c.nickname ? `(${c.nickname})` : ''},${c.models.length} 个模型`, `signed in${c.nickname ? ` (${c.nickname})` : ''}, ${c.models.length} models`) : L('未登录', 'not signed in')}${c.error ? ` — ${c.error}` : ''}`);
|
|
36
|
+
}
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
requireRootInVps(ctx);
|
|
40
|
+
if (sub === 'refresh') {
|
|
41
|
+
if (await refreshBridge(ctx))
|
|
42
|
+
ok(L('bridge 已重读 WorkBuddy 登录态与目录,并同步到引擎和 dsh', 'bridge re-read WorkBuddy sign-in and catalog and synced the engine and dsh'));
|
|
43
|
+
else
|
|
44
|
+
warn(L('bridge 没在运行(dsh-model workbuddy enable)', 'bridge is not running (dsh-model workbuddy enable)'));
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
return withLock(ctx, async () => {
|
|
48
|
+
const all = await loadAll(ctx);
|
|
49
|
+
if (sub === 'enable') {
|
|
50
|
+
await enableWorkbuddy(ctx, all);
|
|
51
|
+
await saveAll(ctx, all);
|
|
52
|
+
await syncAll(ctx, all);
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
if (sub === 'disable') {
|
|
56
|
+
if (!(await disableWorkbuddy(ctx, all))) {
|
|
57
|
+
skip(L('WorkBuddy 本来就没启用', 'WorkBuddy was not enabled'));
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
|
60
|
+
delete all.config.bridge;
|
|
61
|
+
await saveAll(ctx, all);
|
|
62
|
+
// bridge 不在了,目录文件也作废:删掉后重新生成引擎配置
|
|
63
|
+
const { rm } = await import('node:fs/promises');
|
|
64
|
+
const { bridgeDir, bridgeConfigPath } = await import('../bridge/runtime.js');
|
|
65
|
+
await rm(bridgeDir(ctx.paths.home), { recursive: true, force: true });
|
|
66
|
+
await rm(bridgeConfigPath(ctx.paths.home), { force: true });
|
|
67
|
+
await syncAll(ctx, all, { quiet: true });
|
|
68
|
+
ok(L('已停用 WorkBuddy(bridge 服务、目录与登录副本已移除)', 'WorkBuddy disabled (bridge service, catalogs and token copies removed)'));
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
throw new DshModelError('unknown_command', L(`未知子命令:workbuddy ${sub}`, `Unknown subcommand: workbuddy ${sub}`));
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* 登录:轮询期间不持锁(最长 10 分钟),拿到令牌后再持锁启用 / 重启 bridge 并同步。
|
|
76
|
+
*/
|
|
77
|
+
async function login(ctx, variant) {
|
|
78
|
+
requireRootInVps(ctx);
|
|
79
|
+
const all = await loadAll(ctx);
|
|
80
|
+
if (!all.config.bridge?.riskNoticeAt)
|
|
81
|
+
warn(riskNotice());
|
|
82
|
+
info(L(`正在向 ${variant.displayName} 申请授权链接…`, `Requesting an authorization link from ${variant.displayName}…`));
|
|
83
|
+
const credential = await workbuddyLogin(variant, ctx.paths.home, {
|
|
84
|
+
proxy: all.config.proxy,
|
|
85
|
+
owner: ctx.owner,
|
|
86
|
+
onUrl: (url) => {
|
|
87
|
+
info('');
|
|
88
|
+
info(bold(L('在任意设备的浏览器里打开下面的链接并授权(服务器上不需要浏览器):', 'Open this link in a browser on any device and approve (no browser needed on the server):')));
|
|
89
|
+
info(`\n ${url}\n`);
|
|
90
|
+
info(L('等待授权中(最长 10 分钟,Ctrl+C 取消)…', 'Waiting for approval (up to 10 minutes, Ctrl+C to cancel)…'));
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
ok(L(`${variant.displayName} 已登录${credential.nickname ? `(${credential.nickname})` : ''};令牌只存在 dsh-model 自己的目录(0600)`, `${variant.displayName} signed in${credential.nickname ? ` (${credential.nickname})` : ''}; the token is kept only in dsh-model's own directory (0600)`));
|
|
94
|
+
return withLock(ctx, async () => {
|
|
95
|
+
const fresh = await loadAll(ctx);
|
|
96
|
+
await enableWorkbuddy(ctx, fresh);
|
|
97
|
+
await saveAll(ctx, fresh);
|
|
98
|
+
await syncAll(ctx, fresh);
|
|
99
|
+
return 0;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
async function logout(ctx, variant) {
|
|
103
|
+
requireRootInVps(ctx);
|
|
104
|
+
if (!(await workbuddyLogout(variant, ctx.paths.home))) {
|
|
105
|
+
skip(L(`${variant.displayName} 没有用 dsh-model 登录过(桌面 App 的登录请在 App 里退出)`, `${variant.displayName} was not signed in through dsh-model (sign out of the desktop app in the app itself)`));
|
|
106
|
+
return 0;
|
|
107
|
+
}
|
|
108
|
+
ok(L(`已删除 dsh-model 保存的 ${variant.displayName} 令牌`, `Deleted the ${variant.displayName} token saved by dsh-model`));
|
|
109
|
+
return withLock(ctx, async () => {
|
|
110
|
+
const all = await loadAll(ctx);
|
|
111
|
+
if (all.state.bridgeService)
|
|
112
|
+
await enableWorkbuddy(ctx, all); // 重启 bridge,让它不再服务这个产品
|
|
113
|
+
await saveAll(ctx, all);
|
|
114
|
+
await syncAll(ctx, all, { quiet: true });
|
|
115
|
+
return 0;
|
|
116
|
+
});
|
|
117
|
+
}
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// context.ts — 运行模式与所有路径
|
|
2
|
+
//
|
|
3
|
+
// local:普通用户在 Mac / Linux 上自用,状态在 ~/.dsh-model,服务是用户级的
|
|
4
|
+
// vps :检测到 dsh-vps(/opt/dsh-vps),需要 root;引擎以 dsh 用户运行,状态在 /home/dsh/.dsh-model
|
|
5
|
+
//
|
|
6
|
+
// 测试与特殊场景可用环境变量覆盖:DSH_MODEL_MODE、DSH_MODEL_HOME、DSH_HOME、DSH_MODEL_SERVICE=none
|
|
7
|
+
import { existsSync } from 'node:fs';
|
|
8
|
+
import { homedir } from 'node:os';
|
|
9
|
+
import { join, resolve } from 'node:path';
|
|
10
|
+
import { DshModelError } from './errors.js';
|
|
11
|
+
import { L } from './i18n.js';
|
|
12
|
+
import { run } from './util/exec.js';
|
|
13
|
+
export const VPS_ROOT = '/opt/dsh-vps';
|
|
14
|
+
export const VPS_USER = 'dsh';
|
|
15
|
+
export const VPS_USER_HOME = '/home/dsh';
|
|
16
|
+
export function detectMode(env = process.env, platform = process.platform) {
|
|
17
|
+
const forced = env.DSH_MODEL_MODE;
|
|
18
|
+
if (forced === 'local' || forced === 'vps')
|
|
19
|
+
return forced;
|
|
20
|
+
return platform === 'linux' && existsSync(VPS_ROOT) ? 'vps' : 'local';
|
|
21
|
+
}
|
|
22
|
+
export function makePaths(home) {
|
|
23
|
+
const engineDir = join(home, 'engine');
|
|
24
|
+
const auth = join(home, 'auth');
|
|
25
|
+
return {
|
|
26
|
+
home,
|
|
27
|
+
config: join(home, 'config.json'),
|
|
28
|
+
state: join(home, 'state.json'),
|
|
29
|
+
keys: join(home, 'keys.json'),
|
|
30
|
+
engineYaml: join(home, 'engine.yaml'),
|
|
31
|
+
engineDir,
|
|
32
|
+
versionsDir: join(engineDir, 'versions'),
|
|
33
|
+
current: join(engineDir, 'current'),
|
|
34
|
+
auth,
|
|
35
|
+
replaced: join(auth, '.replaced'),
|
|
36
|
+
backups: join(home, 'backups'),
|
|
37
|
+
logs: join(auth, 'logs'), // 引擎把日志写在 auth-dir/logs 下(实测 v8.0.13)
|
|
38
|
+
tmp: join(home, 'tmp'),
|
|
39
|
+
lock: join(home, '.lock'),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export async function createContext(env = process.env) {
|
|
43
|
+
const platform = process.platform;
|
|
44
|
+
if (platform !== 'darwin' && platform !== 'linux') {
|
|
45
|
+
throw new DshModelError('unsupported_platform', L(`暂不支持这个平台:${platform}(只支持 macOS 和 Linux)`, `Unsupported platform: ${platform} (macOS and Linux only)`));
|
|
46
|
+
}
|
|
47
|
+
const mode = detectMode(env, platform);
|
|
48
|
+
const isRoot = typeof process.getuid === 'function' && process.getuid() === 0;
|
|
49
|
+
const serviceDisabled = env.DSH_MODEL_SERVICE === 'none';
|
|
50
|
+
if (mode === 'vps') {
|
|
51
|
+
const home = env.DSH_MODEL_HOME ? resolve(env.DSH_MODEL_HOME) : join(VPS_USER_HOME, '.dsh-model');
|
|
52
|
+
return {
|
|
53
|
+
mode,
|
|
54
|
+
platform,
|
|
55
|
+
paths: makePaths(home),
|
|
56
|
+
owner: await lookupOwner(VPS_USER),
|
|
57
|
+
runAs: VPS_USER,
|
|
58
|
+
dshHome: env.DSH_HOME ? resolve(env.DSH_HOME) : join(VPS_USER_HOME, '.dsh'),
|
|
59
|
+
isRoot,
|
|
60
|
+
serviceDisabled,
|
|
61
|
+
env,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const home = env.DSH_MODEL_HOME ? resolve(env.DSH_MODEL_HOME) : join(homedir(), '.dsh-model');
|
|
65
|
+
return {
|
|
66
|
+
mode,
|
|
67
|
+
platform,
|
|
68
|
+
paths: makePaths(home),
|
|
69
|
+
dshHome: env.DSH_HOME ? resolve(env.DSH_HOME) : join(homedir(), '.dsh'),
|
|
70
|
+
isRoot,
|
|
71
|
+
serviceDisabled,
|
|
72
|
+
env,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/** vps 模式下要改系统服务 / Caddy,必须 root */
|
|
76
|
+
export function requireRootInVps(ctx) {
|
|
77
|
+
if (ctx.mode === 'vps' && !ctx.isRoot && !ctx.serviceDisabled) {
|
|
78
|
+
throw new DshModelError('need_root', L('检测到 dsh-vps,这台机器按 VPS 模式运行,需要 root。', 'dsh-vps detected: running in VPS mode, which needs root.'), L('请用 sudo 重新执行,例如:sudo dsh-model setup', 'Re-run with sudo, e.g. sudo dsh-model setup'));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function lookupOwner(user) {
|
|
82
|
+
const [u, g] = await Promise.all([run('id', ['-u', user]), run('id', ['-g', user])]);
|
|
83
|
+
if (u.code !== 0 || g.code !== 0)
|
|
84
|
+
return undefined;
|
|
85
|
+
return { uid: Number(u.stdout.trim()), gid: Number(g.stdout.trim()) };
|
|
86
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// daemon/client.ts — 调守护进程 /control/*(CLI 用;dsh 插件宿主端也照这个格式调)
|
|
2
|
+
import { loadBridgeConfig } from '../bridge/runtime.js';
|
|
3
|
+
import { DshModelError } from '../errors.js';
|
|
4
|
+
import { L, lang } from '../i18n.js';
|
|
5
|
+
export async function control(ctx, method, path, body, timeoutMs = 60_000) {
|
|
6
|
+
const cfg = await loadBridgeConfig(ctx.paths.home);
|
|
7
|
+
if (!cfg)
|
|
8
|
+
throw new DshModelError('daemon_not_configured', L('守护进程未配置(dsh-model setup)', 'Daemon not configured (dsh-model setup)'));
|
|
9
|
+
let res;
|
|
10
|
+
try {
|
|
11
|
+
res = await fetch(`http://127.0.0.1:${cfg.port}/control${path}`, {
|
|
12
|
+
method,
|
|
13
|
+
headers: { Authorization: `Bearer ${cfg.secret}`, 'x-dsh-model-lang': lang(), ...(body !== undefined ? { 'Content-Type': 'application/json' } : {}) },
|
|
14
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
15
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
throw new DshModelError('daemon_unreachable', L(`守护进程没在 127.0.0.1:${cfg.port} 上运行`, `Daemon is not running on 127.0.0.1:${cfg.port}`), L('执行 dsh-model repair', 'Run dsh-model repair'));
|
|
20
|
+
}
|
|
21
|
+
const data = (await res.json().catch(() => ({})));
|
|
22
|
+
if (!res.ok || data.error) {
|
|
23
|
+
const e = data.error ?? { code: 'daemon_error', message: `HTTP ${res.status}` };
|
|
24
|
+
throw new DshModelError(e.code, e.message, e.hint);
|
|
25
|
+
}
|
|
26
|
+
return data;
|
|
27
|
+
}
|