dsh-model 0.0.1 → 0.14.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 +123 -0
- package/README.md +92 -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 +178 -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 +207 -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 +77 -0
- package/dist/commands/remote.js +129 -0
- package/dist/commands/service.js +72 -0
- package/dist/commands/setup.js +216 -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 +790 -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 +72 -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 +42 -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 +46 -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 +549 -0
- package/plugin/cordis.patch.yml +4 -0
- package/plugin/index.js +177 -0
- package/docs/DESIGN.md +0 -610
package/plugin/index.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
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 { randomBytes } from 'node:crypto'
|
|
11
|
+
import { readFile } from 'node:fs/promises'
|
|
12
|
+
import { homedir } from 'node:os'
|
|
13
|
+
import { join } from 'node:path'
|
|
14
|
+
|
|
15
|
+
export const name = 'dsh-model'
|
|
16
|
+
export const inject = []
|
|
17
|
+
|
|
18
|
+
const TOKEN_HEADER = 'x-dsh-model-token'
|
|
19
|
+
|
|
20
|
+
/** 允许页面调用的守护进程接口:[方法, 路径正则] */
|
|
21
|
+
const ALLOWED = [
|
|
22
|
+
['GET', /^\/status$/],
|
|
23
|
+
['GET', /^\/sources(\?refresh=1)?$/],
|
|
24
|
+
['GET', /^\/usage(\?refresh=1)?$/],
|
|
25
|
+
['GET', /^\/stats$/],
|
|
26
|
+
['GET', /^\/keys$/],
|
|
27
|
+
['POST', /^\/keys$/],
|
|
28
|
+
['DELETE', /^\/keys\/[A-Za-z0-9._-]{1,40}$/],
|
|
29
|
+
['POST', /^\/keys\/[A-Za-z0-9._-]{1,40}\/rotate$/],
|
|
30
|
+
['POST', /^\/keys\/[A-Za-z0-9._-]{1,40}\/reveal$/],
|
|
31
|
+
['GET', /^\/endpoints$/],
|
|
32
|
+
['GET', /^\/sources\/[a-z-]{2,20}\/models$/],
|
|
33
|
+
['POST', /^\/sources\/[a-z-]{2,20}\/models$/],
|
|
34
|
+
['POST', /^\/sources\/[a-z-]{2,20}\/(enable|disable|logout)$/],
|
|
35
|
+
['POST', /^\/opencode\/key$/],
|
|
36
|
+
['GET', /^\/login\/[A-Za-z0-9._-]{1,120}$/],
|
|
37
|
+
['POST', /^\/login\/[A-Za-z0-9._-]{1,120}\/callback$/],
|
|
38
|
+
['DELETE', /^\/login\/[A-Za-z0-9._-]{1,120}$/],
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
function home() {
|
|
42
|
+
return process.env.DSH_MODEL_HOME || join(homedir(), '.dsh-model')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function daemonConfig() {
|
|
46
|
+
return JSON.parse(await readFile(join(home(), 'bridge.json'), 'utf8'))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function sameOrigin(req) {
|
|
50
|
+
const origin = req.headers?.origin
|
|
51
|
+
if (!origin) return true
|
|
52
|
+
try {
|
|
53
|
+
return new URL(origin).host === req.headers.host
|
|
54
|
+
} catch {
|
|
55
|
+
return false
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function json(res, code, data) {
|
|
60
|
+
res.writeHead(code, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' })
|
|
61
|
+
res.end(JSON.stringify(data))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function readBody(req) {
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
let size = 0
|
|
67
|
+
const chunks = []
|
|
68
|
+
req.on('data', (c) => {
|
|
69
|
+
size += c.length
|
|
70
|
+
if (size > 1024 * 1024) {
|
|
71
|
+
reject(new Error('body too large'))
|
|
72
|
+
req.destroy()
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
chunks.push(c)
|
|
76
|
+
})
|
|
77
|
+
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')))
|
|
78
|
+
req.on('error', reject)
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function check(req, token, needToken = true) {
|
|
83
|
+
if (req.method !== 'POST') return { code: 405, error: 'POST only' }
|
|
84
|
+
if (!String(req.headers['content-type'] ?? '').includes('application/json')) return { code: 415, error: 'application/json only' }
|
|
85
|
+
if (!sameOrigin(req)) return { code: 403, error: 'cross-site request refused' }
|
|
86
|
+
if (needToken && String(req.headers[TOKEN_HEADER] ?? '') !== token) return { code: 403, error: 'wrong token' }
|
|
87
|
+
return null
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function apply(ctx) {
|
|
91
|
+
const token = randomBytes(24).toString('hex')
|
|
92
|
+
ctx.inject(['webServer'], (webCtx) => {
|
|
93
|
+
const ws = webCtx.webServer
|
|
94
|
+
if (!ws || typeof ws.register !== 'function') return
|
|
95
|
+
const disposers = []
|
|
96
|
+
const track = (d) => disposers.push(d)
|
|
97
|
+
|
|
98
|
+
track(
|
|
99
|
+
ws.register({
|
|
100
|
+
kind: 'exact',
|
|
101
|
+
path: '/api-dsh-model/call',
|
|
102
|
+
handler: async (req, res) => {
|
|
103
|
+
const bad = check(req, token)
|
|
104
|
+
if (bad) return json(res, bad.code, { error: { code: 'refused', message: bad.error } })
|
|
105
|
+
let call
|
|
106
|
+
try {
|
|
107
|
+
call = JSON.parse((await readBody(req)) || '{}')
|
|
108
|
+
} catch {
|
|
109
|
+
return json(res, 400, { error: { code: 'invalid_json', message: 'invalid JSON' } })
|
|
110
|
+
}
|
|
111
|
+
const method = String(call.method ?? 'GET').toUpperCase()
|
|
112
|
+
const path = String(call.path ?? '')
|
|
113
|
+
if (!ALLOWED.some(([m, re]) => m === method && re.test(path))) {
|
|
114
|
+
return json(res, 403, { error: { code: 'not_allowed', message: `${method} ${path}` } })
|
|
115
|
+
}
|
|
116
|
+
let cfg
|
|
117
|
+
try {
|
|
118
|
+
cfg = await daemonConfig()
|
|
119
|
+
} catch {
|
|
120
|
+
return json(res, 503, { error: { code: 'daemon_not_configured', message: 'dsh-model daemon not configured (run dsh-model setup)' } })
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
const r = await fetch(`http://127.0.0.1:${cfg.port}/control${path}`, {
|
|
124
|
+
method,
|
|
125
|
+
headers: { Authorization: `Bearer ${cfg.secret}`, ...(call.body !== undefined ? { 'Content-Type': 'application/json' } : {}) },
|
|
126
|
+
body: call.body !== undefined && method !== 'GET' ? JSON.stringify(call.body) : undefined,
|
|
127
|
+
signal: AbortSignal.timeout(90_000),
|
|
128
|
+
})
|
|
129
|
+
const text = await r.text()
|
|
130
|
+
res.writeHead(r.status, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' })
|
|
131
|
+
res.end(text || '{}')
|
|
132
|
+
} catch {
|
|
133
|
+
return json(res, 503, { error: { code: 'daemon_unreachable', message: 'dsh-model daemon is not running (run dsh-model repair)' } })
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
}),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
// 令牌对不上时(dsh 重启、插件热更新)页面悄悄来换:只认同源 JSON,并且先过 dsh 自己的登录
|
|
140
|
+
track(
|
|
141
|
+
ws.register({
|
|
142
|
+
kind: 'exact',
|
|
143
|
+
path: '/api-dsh-model/token',
|
|
144
|
+
handler: (req, res) => {
|
|
145
|
+
const bad = check(req, token, false)
|
|
146
|
+
if (bad) return json(res, bad.code, { error: { code: 'refused', message: bad.error } })
|
|
147
|
+
return json(res, 200, { token })
|
|
148
|
+
},
|
|
149
|
+
}),
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
if (typeof webCtx.on === 'function') {
|
|
153
|
+
track(webCtx.on('webserver/index-inject', (table) => {
|
|
154
|
+
if (Array.isArray(table)) table.push({ kind: 'global', name: '__DSH_MODEL_TOKEN__', value: token })
|
|
155
|
+
}))
|
|
156
|
+
}
|
|
157
|
+
if (typeof ws.tapIndex === 'function') {
|
|
158
|
+
track(ws.tapIndex((html) => html.replace('</head>', `<script>window.__DSH_MODEL_TOKEN__=${JSON.stringify(token)}</script></head>`)))
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
webCtx.effect?.(() => () => {
|
|
163
|
+
for (const d of disposers.reverse()) {
|
|
164
|
+
try {
|
|
165
|
+
d?.()
|
|
166
|
+
} catch {
|
|
167
|
+
// 反注册失败不影响卸载
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}, 'dsh-model: routes')
|
|
171
|
+
} catch {
|
|
172
|
+
// 宿主不支持就算了
|
|
173
|
+
}
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export default { name, inject, apply }
|