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
package/plugin/index.js
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
// plugin/index.js — dsh-model 的 dsh 插件(宿主端)
|
|
2
|
+
//
|
|
3
|
+
// 只做一件事:把页面的请求转给 dsh-model 守护进程的 /control/*(设计 §14.1)。
|
|
4
|
+
// - 路由 /api-dsh-model/call 与 /api-dsh-model/token,挂在 dsh 自己的 webServer 上,所以先过 dsh 的登录校验;
|
|
5
|
+
// - 只收同源的 POST JSON,并且要带页面令牌(宿主经 index-inject / tapIndex 注入页面,跨站网页读不到);
|
|
6
|
+
// - 守护进程的地址与控制密钥从 ~/.dsh-model/bridge.json 读(dsh 与守护进程是同一个用户),浏览器永远拿不到;
|
|
7
|
+
// - 只转发白名单里的控制接口。
|
|
8
|
+
// 写法照 dsh-vps-manager(同作者)的 routes.js。
|
|
9
|
+
|
|
10
|
+
import { spawn } from 'node:child_process'
|
|
11
|
+
import { randomBytes } from 'node:crypto'
|
|
12
|
+
import { access, readFile } from 'node:fs/promises'
|
|
13
|
+
import { homedir } from 'node:os'
|
|
14
|
+
import { join } from 'node:path'
|
|
15
|
+
import { fileURLToPath } from 'node:url'
|
|
16
|
+
|
|
17
|
+
/** 本包根目录:插件就是完整的 dsh-model 包,自带命令行 */
|
|
18
|
+
const PKG_ROOT = fileURLToPath(new URL('..', import.meta.url))
|
|
19
|
+
const VPS_ROOT = '/opt/dsh-vps'
|
|
20
|
+
|
|
21
|
+
export const name = 'dsh-model'
|
|
22
|
+
export const inject = []
|
|
23
|
+
|
|
24
|
+
const TOKEN_HEADER = 'x-dsh-model-token'
|
|
25
|
+
|
|
26
|
+
/** 允许页面调用的守护进程接口:[方法, 路径正则] */
|
|
27
|
+
const ALLOWED = [
|
|
28
|
+
['GET', /^\/status$/],
|
|
29
|
+
['GET', /^\/sources(\?refresh=1)?$/],
|
|
30
|
+
['GET', /^\/usage(\?refresh=1)?$/],
|
|
31
|
+
['GET', /^\/stats$/],
|
|
32
|
+
['GET', /^\/keys$/],
|
|
33
|
+
['POST', /^\/keys$/],
|
|
34
|
+
['DELETE', /^\/keys\/[A-Za-z0-9._-]{1,40}$/],
|
|
35
|
+
['POST', /^\/keys\/[A-Za-z0-9._-]{1,40}\/rotate$/],
|
|
36
|
+
['POST', /^\/keys\/[A-Za-z0-9._-]{1,40}\/reveal$/],
|
|
37
|
+
['GET', /^\/endpoints$/],
|
|
38
|
+
['GET', /^\/self$/],
|
|
39
|
+
['POST', /^\/self\/(update|uninstall)$/],
|
|
40
|
+
['GET', /^\/sources\/[a-z-]{2,20}\/models$/],
|
|
41
|
+
['POST', /^\/sources\/[a-z-]{2,20}\/models$/],
|
|
42
|
+
['POST', /^\/sources\/[a-z-]{2,20}\/(enable|disable|logout)$/],
|
|
43
|
+
['POST', /^\/opencode\/key$/],
|
|
44
|
+
['GET', /^\/login\/[A-Za-z0-9._-]{1,120}$/],
|
|
45
|
+
['POST', /^\/login\/[A-Za-z0-9._-]{1,120}\/callback$/],
|
|
46
|
+
['DELETE', /^\/login\/[A-Za-z0-9._-]{1,120}$/],
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
function home() {
|
|
50
|
+
return process.env.DSH_MODEL_HOME || join(homedir(), '.dsh-model')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function daemonConfig() {
|
|
54
|
+
return JSON.parse(await readFile(join(home(), 'bridge.json'), 'utf8'))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function sameOrigin(req) {
|
|
58
|
+
const origin = req.headers?.origin
|
|
59
|
+
if (!origin) return true
|
|
60
|
+
try {
|
|
61
|
+
return new URL(origin).host === req.headers.host
|
|
62
|
+
} catch {
|
|
63
|
+
return false
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function json(res, code, data) {
|
|
68
|
+
res.writeHead(code, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' })
|
|
69
|
+
res.end(JSON.stringify(data))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function readBody(req) {
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
let size = 0
|
|
75
|
+
const chunks = []
|
|
76
|
+
req.on('data', (c) => {
|
|
77
|
+
size += c.length
|
|
78
|
+
if (size > 1024 * 1024) {
|
|
79
|
+
reject(new Error('body too large'))
|
|
80
|
+
req.destroy()
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
chunks.push(c)
|
|
84
|
+
})
|
|
85
|
+
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')))
|
|
86
|
+
req.on('error', reject)
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function check(req, token, needToken = true) {
|
|
91
|
+
if (req.method !== 'POST') return { code: 405, error: 'POST only' }
|
|
92
|
+
if (!String(req.headers['content-type'] ?? '').includes('application/json')) return { code: 415, error: 'application/json only' }
|
|
93
|
+
if (!sameOrigin(req)) return { code: 403, error: 'cross-site request refused' }
|
|
94
|
+
if (needToken && String(req.headers[TOKEN_HEADER] ?? '') !== token) return { code: 403, error: 'wrong token' }
|
|
95
|
+
return null
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const exists = (p) => access(p).then(() => true, () => false)
|
|
99
|
+
|
|
100
|
+
/** 从页面一键执行 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
|
+
// dsh 桌面版可能跑在 Electron 里:让它当普通 Node 用
|
|
108
|
+
env: { ...process.env, ELECTRON_RUN_AS_NODE: '1', NO_COLOR: '1', FORCE_COLOR: '0' },
|
|
109
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
110
|
+
})
|
|
111
|
+
const add = (b) => { out = (out + b.toString()).slice(-20000) }
|
|
112
|
+
child.stdout.on('data', add)
|
|
113
|
+
child.stderr.on('data', add)
|
|
114
|
+
const timer = setTimeout(() => child.kill('SIGTERM'), 10 * 60 * 1000)
|
|
115
|
+
child.on('error', (e) => { clearTimeout(timer); resolve({ code: -1, output: `${out}\n${e.message}` }) })
|
|
116
|
+
child.on('exit', (code) => { clearTimeout(timer); resolve({ code: code ?? -1, output: out.replace(/\x1b\[[0-9;]*m/g, '') }) })
|
|
117
|
+
}).finally(() => { running = null })
|
|
118
|
+
return running
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function apply(ctx) {
|
|
122
|
+
const token = randomBytes(24).toString('hex')
|
|
123
|
+
ctx.inject(['webServer'], (webCtx) => {
|
|
124
|
+
const ws = webCtx.webServer
|
|
125
|
+
if (!ws || typeof ws.register !== 'function') return
|
|
126
|
+
const disposers = []
|
|
127
|
+
const track = (d) => disposers.push(d)
|
|
128
|
+
|
|
129
|
+
track(
|
|
130
|
+
ws.register({
|
|
131
|
+
kind: 'exact',
|
|
132
|
+
path: '/api-dsh-model/call',
|
|
133
|
+
handler: async (req, res) => {
|
|
134
|
+
const bad = check(req, token)
|
|
135
|
+
if (bad) return json(res, bad.code, { error: { code: 'refused', message: bad.error } })
|
|
136
|
+
let call
|
|
137
|
+
try {
|
|
138
|
+
call = JSON.parse((await readBody(req)) || '{}')
|
|
139
|
+
} catch {
|
|
140
|
+
return json(res, 400, { error: { code: 'invalid_json', message: 'invalid JSON' } })
|
|
141
|
+
}
|
|
142
|
+
const method = String(call.method ?? 'GET').toUpperCase()
|
|
143
|
+
const path = String(call.path ?? '')
|
|
144
|
+
if (!ALLOWED.some(([m, re]) => m === method && re.test(path))) {
|
|
145
|
+
return json(res, 403, { error: { code: 'not_allowed', message: `${method} ${path}` } })
|
|
146
|
+
}
|
|
147
|
+
let cfg
|
|
148
|
+
try {
|
|
149
|
+
cfg = await daemonConfig()
|
|
150
|
+
} catch {
|
|
151
|
+
return json(res, 503, { error: { code: 'daemon_not_configured', message: 'dsh-model daemon not configured (run dsh-model setup)' } })
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
const r = await fetch(`http://127.0.0.1:${cfg.port}/control${path}`, {
|
|
155
|
+
method,
|
|
156
|
+
headers: { Authorization: `Bearer ${cfg.secret}`, 'x-dsh-model-lang': call.lang === 'en' ? 'en' : 'zh', ...(call.body !== undefined ? { 'Content-Type': 'application/json' } : {}) },
|
|
157
|
+
body: call.body !== undefined && method !== 'GET' ? JSON.stringify(call.body) : undefined,
|
|
158
|
+
signal: AbortSignal.timeout(90_000),
|
|
159
|
+
})
|
|
160
|
+
const text = await r.text()
|
|
161
|
+
res.writeHead(r.status, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' })
|
|
162
|
+
res.end(text || '{}')
|
|
163
|
+
} catch {
|
|
164
|
+
return json(res, 503, { error: { code: 'daemon_unreachable', message: 'dsh-model daemon is not running (run dsh-model repair)' } })
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
}),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
// 页面上的「一键安装 / 修复」
|
|
171
|
+
track(
|
|
172
|
+
ws.register({
|
|
173
|
+
kind: 'exact',
|
|
174
|
+
path: '/api-dsh-model/setup',
|
|
175
|
+
handler: async (req, res) => {
|
|
176
|
+
const bad = check(req, token)
|
|
177
|
+
if (bad) return json(res, bad.code, { error: { code: 'refused', message: bad.error } })
|
|
178
|
+
let body
|
|
179
|
+
try {
|
|
180
|
+
body = JSON.parse((await readBody(req)) || '{}')
|
|
181
|
+
} catch {
|
|
182
|
+
return json(res, 400, { error: { code: 'invalid_json', message: 'invalid JSON' } })
|
|
183
|
+
}
|
|
184
|
+
if (await exists(VPS_ROOT)) return json(res, 200, { vps: true })
|
|
185
|
+
const action = body.action === 'repair' ? 'repair' : 'setup'
|
|
186
|
+
const lang = body.lang === 'en' ? 'en' : 'zh'
|
|
187
|
+
const r = await runCli(action === 'repair' ? ['repair', '--lang', lang] : ['setup', '--skip-dsh-plugin', '--lang', lang])
|
|
188
|
+
return json(res, 200, r)
|
|
189
|
+
},
|
|
190
|
+
}),
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
// 令牌对不上时(dsh 重启、插件热更新)页面悄悄来换:只认同源 JSON,并且先过 dsh 自己的登录
|
|
194
|
+
track(
|
|
195
|
+
ws.register({
|
|
196
|
+
kind: 'exact',
|
|
197
|
+
path: '/api-dsh-model/token',
|
|
198
|
+
handler: (req, res) => {
|
|
199
|
+
const bad = check(req, token, false)
|
|
200
|
+
if (bad) return json(res, bad.code, { error: { code: 'refused', message: bad.error } })
|
|
201
|
+
return json(res, 200, { token })
|
|
202
|
+
},
|
|
203
|
+
}),
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
if (typeof webCtx.on === 'function') {
|
|
207
|
+
track(webCtx.on('webserver/index-inject', (table) => {
|
|
208
|
+
if (Array.isArray(table)) table.push({ kind: 'global', name: '__DSH_MODEL_TOKEN__', value: token })
|
|
209
|
+
}))
|
|
210
|
+
}
|
|
211
|
+
if (typeof ws.tapIndex === 'function') {
|
|
212
|
+
track(ws.tapIndex((html) => html.replace('</head>', `<script>window.__DSH_MODEL_TOKEN__=${JSON.stringify(token)}</script></head>`)))
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
webCtx.effect?.(() => () => {
|
|
217
|
+
for (const d of disposers.reverse()) {
|
|
218
|
+
try {
|
|
219
|
+
d?.()
|
|
220
|
+
} catch {
|
|
221
|
+
// 反注册失败不影响卸载
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}, 'dsh-model: routes')
|
|
225
|
+
} catch {
|
|
226
|
+
// 宿主不支持就算了
|
|
227
|
+
}
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export default { name, inject, apply }
|