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.
Files changed (82) hide show
  1. package/README.en.md +123 -0
  2. package/README.md +92 -43
  3. package/bin/dsh-model.js +8 -0
  4. package/dist/bridge/aggregate.js +91 -0
  5. package/dist/bridge/login.js +127 -0
  6. package/dist/bridge/runtime.js +136 -0
  7. package/dist/bridge/server.js +178 -0
  8. package/dist/bridge/workbuddy/_compat.js +52 -0
  9. package/dist/bridge/workbuddy/app-version.js +141 -0
  10. package/dist/bridge/workbuddy/auth.js +548 -0
  11. package/dist/bridge/workbuddy/catalog.js +154 -0
  12. package/dist/bridge/workbuddy/client-identity.js +234 -0
  13. package/dist/bridge/workbuddy/desktop-credential-protection.js +992 -0
  14. package/dist/bridge/workbuddy/loopback.js +52 -0
  15. package/dist/bridge/workbuddy/probe.js +153 -0
  16. package/dist/bridge/workbuddy/status-paths.js +49 -0
  17. package/dist/bridge/workbuddy/upstream.js +1180 -0
  18. package/dist/bridge/workbuddy/variants.js +101 -0
  19. package/dist/cli.js +207 -0
  20. package/dist/commands/bridge.js +68 -0
  21. package/dist/commands/connect.js +67 -0
  22. package/dist/commands/engine.js +122 -0
  23. package/dist/commands/key.js +75 -0
  24. package/dist/commands/login.js +113 -0
  25. package/dist/commands/logs.js +19 -0
  26. package/dist/commands/opencode.js +77 -0
  27. package/dist/commands/remote.js +129 -0
  28. package/dist/commands/service.js +72 -0
  29. package/dist/commands/setup.js +216 -0
  30. package/dist/commands/sources.js +196 -0
  31. package/dist/commands/status.js +216 -0
  32. package/dist/commands/uninstall.js +97 -0
  33. package/dist/commands/workbuddy.js +117 -0
  34. package/dist/context.js +86 -0
  35. package/dist/daemon/client.js +27 -0
  36. package/dist/daemon/core.js +790 -0
  37. package/dist/daemon/service.js +33 -0
  38. package/dist/daemon/stats.js +126 -0
  39. package/dist/daemon/usage.js +213 -0
  40. package/dist/dsh/cli.js +72 -0
  41. package/dist/dsh/connect.js +257 -0
  42. package/dist/dsh/credentials.js +55 -0
  43. package/dist/dsh/locate.js +129 -0
  44. package/dist/dsh/patch.js +106 -0
  45. package/dist/dsh/pick.js +59 -0
  46. package/dist/engine/auth.js +40 -0
  47. package/dist/engine/client.js +104 -0
  48. package/dist/engine/compat.js +94 -0
  49. package/dist/engine/config.js +47 -0
  50. package/dist/engine/install.js +118 -0
  51. package/dist/engine/mgmt.js +99 -0
  52. package/dist/engine/sourcemap.js +48 -0
  53. package/dist/errors.js +14 -0
  54. package/dist/i18n.js +39 -0
  55. package/dist/integrations/dshplugin.js +42 -0
  56. package/dist/integrations/opencode.js +101 -0
  57. package/dist/integrations/workbuddy.js +114 -0
  58. package/dist/keys.js +54 -0
  59. package/dist/ops.js +178 -0
  60. package/dist/remote/public.js +109 -0
  61. package/dist/secrets.js +13 -0
  62. package/dist/service/index.js +46 -0
  63. package/dist/service/launchd.js +89 -0
  64. package/dist/service/systemd.js +83 -0
  65. package/dist/sources.js +36 -0
  66. package/dist/state.js +90 -0
  67. package/dist/upstreams.js +23 -0
  68. package/dist/util/diff.js +30 -0
  69. package/dist/util/exec.js +42 -0
  70. package/dist/util/fs.js +107 -0
  71. package/dist/util/output.js +50 -0
  72. package/dist/util/pkg.js +13 -0
  73. package/dist/util/port.js +16 -0
  74. package/dist/util/prompt.js +40 -0
  75. package/dist/util/proxy.js +73 -0
  76. package/dist/util/redact.js +7 -0
  77. package/engine-manifest.json +28 -0
  78. package/package.json +77 -7
  79. package/plugin/client.js +549 -0
  80. package/plugin/cordis.patch.yml +4 -0
  81. package/plugin/index.js +177 -0
  82. package/docs/DESIGN.md +0 -610
@@ -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 }