dsh-gufa-code 0.0.0-stage → 1.0.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 (158) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +96 -2
  3. package/client.js +11548 -0
  4. package/cordis.patch.yml +8 -0
  5. package/docs/icon-180.png +0 -0
  6. package/docs/icon.svg +18 -0
  7. package/docs/market-submission.md +32 -0
  8. package/docs/screenshot-editor.jpg +0 -0
  9. package/docs/screenshot-ide.jpg +0 -0
  10. package/host/assets.js +31 -0
  11. package/host/chat.js +523 -0
  12. package/host/context.js +438 -0
  13. package/host/dev.js +35 -0
  14. package/host/fs.js +306 -0
  15. package/host/git.js +532 -0
  16. package/host/project.js +305 -0
  17. package/host/ssh.js +54 -0
  18. package/host/terminal-pty.js +237 -0
  19. package/host/terminal.js +947 -0
  20. package/icon.svg +18 -0
  21. package/index.js +63 -0
  22. package/locale/en.json +121 -0
  23. package/locale/zh.json +121 -0
  24. package/package.json +66 -4
  25. package/scripts/build-client.mjs +38 -0
  26. package/scripts/selftest-client.mjs +457 -0
  27. package/scripts/selftest-host.mjs +197 -0
  28. package/scripts/selftest-http.mjs +284 -0
  29. package/scripts/selftest-router.mjs +101 -0
  30. package/scripts/selftest-terminal-client.mjs +314 -0
  31. package/scripts/selftest-terminal-pty.mjs +190 -0
  32. package/scripts/selftest-terminal.mjs +351 -0
  33. package/scripts/test-chat-render.mjs +702 -0
  34. package/scripts/test-chat.mjs +761 -0
  35. package/scripts/test-editor.mjs +516 -0
  36. package/scripts/verify-workspace-sessions.mjs +135 -0
  37. package/src/client/00-core.js +1686 -0
  38. package/src/client/10-icons.js +205 -0
  39. package/src/client/20-editor.js +1318 -0
  40. package/src/client/30-terminal.js +1510 -0
  41. package/src/client/40-chat.js +1184 -0
  42. package/src/client/46-gitgraph.js +278 -0
  43. package/src/client/50-panels.js +916 -0
  44. package/src/client/55-backdrop.js +508 -0
  45. package/src/client/60-dock.js +662 -0
  46. package/src/client/85-shortcuts.js +516 -0
  47. package/src/client/90-app.js +2272 -0
  48. package/src/client/99-main.js +449 -0
  49. package/vendor/monaco/LICENSE +21 -0
  50. package/vendor/monaco/ThirdPartyNotices.txt +448 -0
  51. package/vendor/monaco/vs/base/browser/ui/codicons/codicon/codicon.ttf +0 -0
  52. package/vendor/monaco/vs/base/worker/workerMain.js +31 -0
  53. package/vendor/monaco/vs/basic-languages/abap/abap.js +10 -0
  54. package/vendor/monaco/vs/basic-languages/apex/apex.js +10 -0
  55. package/vendor/monaco/vs/basic-languages/azcli/azcli.js +10 -0
  56. package/vendor/monaco/vs/basic-languages/bat/bat.js +10 -0
  57. package/vendor/monaco/vs/basic-languages/bicep/bicep.js +11 -0
  58. package/vendor/monaco/vs/basic-languages/cameligo/cameligo.js +10 -0
  59. package/vendor/monaco/vs/basic-languages/clojure/clojure.js +10 -0
  60. package/vendor/monaco/vs/basic-languages/coffee/coffee.js +10 -0
  61. package/vendor/monaco/vs/basic-languages/cpp/cpp.js +10 -0
  62. package/vendor/monaco/vs/basic-languages/csharp/csharp.js +10 -0
  63. package/vendor/monaco/vs/basic-languages/csp/csp.js +10 -0
  64. package/vendor/monaco/vs/basic-languages/css/css.js +12 -0
  65. package/vendor/monaco/vs/basic-languages/cypher/cypher.js +10 -0
  66. package/vendor/monaco/vs/basic-languages/dart/dart.js +10 -0
  67. package/vendor/monaco/vs/basic-languages/dockerfile/dockerfile.js +10 -0
  68. package/vendor/monaco/vs/basic-languages/ecl/ecl.js +10 -0
  69. package/vendor/monaco/vs/basic-languages/elixir/elixir.js +10 -0
  70. package/vendor/monaco/vs/basic-languages/flow9/flow9.js +10 -0
  71. package/vendor/monaco/vs/basic-languages/freemarker2/freemarker2.js +12 -0
  72. package/vendor/monaco/vs/basic-languages/fsharp/fsharp.js +10 -0
  73. package/vendor/monaco/vs/basic-languages/go/go.js +10 -0
  74. package/vendor/monaco/vs/basic-languages/graphql/graphql.js +10 -0
  75. package/vendor/monaco/vs/basic-languages/handlebars/handlebars.js +10 -0
  76. package/vendor/monaco/vs/basic-languages/hcl/hcl.js +10 -0
  77. package/vendor/monaco/vs/basic-languages/html/html.js +10 -0
  78. package/vendor/monaco/vs/basic-languages/ini/ini.js +10 -0
  79. package/vendor/monaco/vs/basic-languages/java/java.js +10 -0
  80. package/vendor/monaco/vs/basic-languages/javascript/javascript.js +10 -0
  81. package/vendor/monaco/vs/basic-languages/julia/julia.js +10 -0
  82. package/vendor/monaco/vs/basic-languages/kotlin/kotlin.js +10 -0
  83. package/vendor/monaco/vs/basic-languages/less/less.js +11 -0
  84. package/vendor/monaco/vs/basic-languages/lexon/lexon.js +10 -0
  85. package/vendor/monaco/vs/basic-languages/liquid/liquid.js +10 -0
  86. package/vendor/monaco/vs/basic-languages/lua/lua.js +10 -0
  87. package/vendor/monaco/vs/basic-languages/m3/m3.js +10 -0
  88. package/vendor/monaco/vs/basic-languages/markdown/markdown.js +10 -0
  89. package/vendor/monaco/vs/basic-languages/mdx/mdx.js +10 -0
  90. package/vendor/monaco/vs/basic-languages/mips/mips.js +10 -0
  91. package/vendor/monaco/vs/basic-languages/msdax/msdax.js +10 -0
  92. package/vendor/monaco/vs/basic-languages/mysql/mysql.js +10 -0
  93. package/vendor/monaco/vs/basic-languages/objective-c/objective-c.js +10 -0
  94. package/vendor/monaco/vs/basic-languages/pascal/pascal.js +10 -0
  95. package/vendor/monaco/vs/basic-languages/pascaligo/pascaligo.js +10 -0
  96. package/vendor/monaco/vs/basic-languages/perl/perl.js +10 -0
  97. package/vendor/monaco/vs/basic-languages/pgsql/pgsql.js +10 -0
  98. package/vendor/monaco/vs/basic-languages/php/php.js +10 -0
  99. package/vendor/monaco/vs/basic-languages/pla/pla.js +10 -0
  100. package/vendor/monaco/vs/basic-languages/postiats/postiats.js +10 -0
  101. package/vendor/monaco/vs/basic-languages/powerquery/powerquery.js +10 -0
  102. package/vendor/monaco/vs/basic-languages/powershell/powershell.js +10 -0
  103. package/vendor/monaco/vs/basic-languages/protobuf/protobuf.js +11 -0
  104. package/vendor/monaco/vs/basic-languages/pug/pug.js +10 -0
  105. package/vendor/monaco/vs/basic-languages/python/python.js +10 -0
  106. package/vendor/monaco/vs/basic-languages/qsharp/qsharp.js +10 -0
  107. package/vendor/monaco/vs/basic-languages/r/r.js +10 -0
  108. package/vendor/monaco/vs/basic-languages/razor/razor.js +10 -0
  109. package/vendor/monaco/vs/basic-languages/redis/redis.js +10 -0
  110. package/vendor/monaco/vs/basic-languages/redshift/redshift.js +10 -0
  111. package/vendor/monaco/vs/basic-languages/restructuredtext/restructuredtext.js +10 -0
  112. package/vendor/monaco/vs/basic-languages/ruby/ruby.js +10 -0
  113. package/vendor/monaco/vs/basic-languages/rust/rust.js +10 -0
  114. package/vendor/monaco/vs/basic-languages/sb/sb.js +10 -0
  115. package/vendor/monaco/vs/basic-languages/scala/scala.js +10 -0
  116. package/vendor/monaco/vs/basic-languages/scheme/scheme.js +10 -0
  117. package/vendor/monaco/vs/basic-languages/scss/scss.js +12 -0
  118. package/vendor/monaco/vs/basic-languages/shell/shell.js +10 -0
  119. package/vendor/monaco/vs/basic-languages/solidity/solidity.js +10 -0
  120. package/vendor/monaco/vs/basic-languages/sophia/sophia.js +10 -0
  121. package/vendor/monaco/vs/basic-languages/sparql/sparql.js +10 -0
  122. package/vendor/monaco/vs/basic-languages/sql/sql.js +10 -0
  123. package/vendor/monaco/vs/basic-languages/st/st.js +10 -0
  124. package/vendor/monaco/vs/basic-languages/swift/swift.js +13 -0
  125. package/vendor/monaco/vs/basic-languages/systemverilog/systemverilog.js +10 -0
  126. package/vendor/monaco/vs/basic-languages/tcl/tcl.js +10 -0
  127. package/vendor/monaco/vs/basic-languages/twig/twig.js +10 -0
  128. package/vendor/monaco/vs/basic-languages/typescript/typescript.js +10 -0
  129. package/vendor/monaco/vs/basic-languages/typespec/typespec.js +10 -0
  130. package/vendor/monaco/vs/basic-languages/vb/vb.js +10 -0
  131. package/vendor/monaco/vs/basic-languages/wgsl/wgsl.js +307 -0
  132. package/vendor/monaco/vs/basic-languages/xml/xml.js +10 -0
  133. package/vendor/monaco/vs/basic-languages/yaml/yaml.js +10 -0
  134. package/vendor/monaco/vs/editor/editor.main.css +8 -0
  135. package/vendor/monaco/vs/editor/editor.main.js +798 -0
  136. package/vendor/monaco/vs/language/css/cssMode.js +13 -0
  137. package/vendor/monaco/vs/language/css/cssWorker.js +77 -0
  138. package/vendor/monaco/vs/language/html/htmlMode.js +13 -0
  139. package/vendor/monaco/vs/language/html/htmlWorker.js +454 -0
  140. package/vendor/monaco/vs/language/json/jsonMode.js +19 -0
  141. package/vendor/monaco/vs/language/json/jsonWorker.js +42 -0
  142. package/vendor/monaco/vs/language/typescript/tsMode.js +20 -0
  143. package/vendor/monaco/vs/language/typescript/tsWorker.js +51328 -0
  144. package/vendor/monaco/vs/loader.js +11 -0
  145. package/vendor/monaco/vs/nls.messages.de.js +21 -0
  146. package/vendor/monaco/vs/nls.messages.es.js +21 -0
  147. package/vendor/monaco/vs/nls.messages.fr.js +19 -0
  148. package/vendor/monaco/vs/nls.messages.it.js +19 -0
  149. package/vendor/monaco/vs/nls.messages.ja.js +21 -0
  150. package/vendor/monaco/vs/nls.messages.ko.js +19 -0
  151. package/vendor/monaco/vs/nls.messages.ru.js +21 -0
  152. package/vendor/monaco/vs/nls.messages.zh-cn.js +21 -0
  153. package/vendor/monaco/vs/nls.messages.zh-tw.js +19 -0
  154. package/vendor/xterm/LICENSE +21 -0
  155. package/vendor/xterm/LICENSE.addon-fit +19 -0
  156. package/vendor/xterm/addon-fit.js +2 -0
  157. package/vendor/xterm/xterm.css +285 -0
  158. package/vendor/xterm/xterm.js +2 -0
@@ -0,0 +1,438 @@
1
+ /**
2
+ * Host 半的公共设施:错误、HTTP 读写、鉴权、项目根白名单、路由注册器。
3
+ *
4
+ * 所有路由挂在同一个前缀 `/gufa-code` 下:
5
+ * POST /gufa-code/api/<method> JSON 请求/响应(鉴权 + 同源)
6
+ * POST /gufa-code/api/<stream> SSE(chat 流式)
7
+ * GET /gufa-code/asset/<prefix>/<path> 静态资产(Monaco / xterm)
8
+ * WS /gufa-code/ws/<path> 终端等长连接
9
+ *
10
+ * 鉴权复用 Harness 的 connection 服务(requestRejection),是本机已登录浏览器才能用。
11
+ */
12
+ import { createReadStream } from 'node:fs'
13
+ import { realpath, stat } from 'node:fs/promises'
14
+ import { extname, isAbsolute, normalize, resolve, sep } from 'node:path'
15
+ import { WebSocket, WebSocketServer } from 'ws'
16
+
17
+ /** 业务错误:带稳定 code 与 HTTP 状态。 */
18
+ export class ApiError extends Error {
19
+ constructor(code, message, status = 400) {
20
+ super(message)
21
+ this.name = 'ApiError'
22
+ this.code = code
23
+ this.status = status
24
+ }
25
+ }
26
+
27
+ export function badRequest(message) {
28
+ return new ApiError('bad-request', message, 400)
29
+ }
30
+
31
+ /** 读取 JSON 请求体(对象)。 */
32
+ export async function readBody(req, limit = 8 * 1024 * 1024) {
33
+ const chunks = []
34
+ let size = 0
35
+ for await (const chunk of req) {
36
+ const value = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
37
+ size += value.length
38
+ if (size > limit) throw new ApiError('too-large', 'request body is too large', 413)
39
+ chunks.push(value)
40
+ }
41
+ const text = Buffer.concat(chunks).toString('utf8')
42
+ if (text.trim() === '') return {}
43
+ let value
44
+ try {
45
+ value = JSON.parse(text)
46
+ } catch {
47
+ throw badRequest('request body must be valid JSON')
48
+ }
49
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) throw badRequest('request body must be a JSON object')
50
+ return value
51
+ }
52
+
53
+ /** 发送 JSON 响应。 */
54
+ export function send(res, status, value) {
55
+ res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' })
56
+ res.end(JSON.stringify(value))
57
+ }
58
+
59
+ /** 发送统一错误响应。 */
60
+ export function sendError(res, error) {
61
+ const known = error instanceof ApiError
62
+ ? error
63
+ : new ApiError('internal', error instanceof Error ? error.message : String(error), 500)
64
+ if (res.headersSent) {
65
+ try {
66
+ res.end()
67
+ } catch {
68
+ /* 流已中断 */
69
+ }
70
+ return
71
+ }
72
+ send(res, known.status, { ok: false, error: { code: known.code, message: known.message } })
73
+ }
74
+
75
+ /** 复用 Harness 鉴权与 Host/Origin 围栏。返回 true 表示放行。 */
76
+ export function authorize(req, res, connection) {
77
+ const rejection = connection.requestRejection(req)
78
+ if (rejection === undefined) return true
79
+ send(res, rejection, { ok: false, error: { code: rejection === 401 ? 'unauthorized' : 'forbidden', message: 'unauthorized' } })
80
+ return false
81
+ }
82
+
83
+ /** 仅允许本机来源(终端等敏感长连接额外收紧)。 */
84
+ export function localRequestAllowed(req) {
85
+ const host = req.headers.host
86
+ if (host === undefined || req.headers['sec-fetch-site'] === 'cross-site') return false
87
+ let hostname
88
+ try {
89
+ hostname = new URL(`http://${host}`).hostname
90
+ } catch {
91
+ return false
92
+ }
93
+ if (hostname !== 'localhost' && hostname !== '[::1]' && !hostname.startsWith('127.')) return false
94
+ const origin = req.headers.origin
95
+ try {
96
+ return origin === undefined || new URL(origin).host === host
97
+ } catch {
98
+ return false
99
+ }
100
+ }
101
+
102
+ /**
103
+ * 已打开项目的白名单。只有用户显式「打开/新建项目」过的目录才能被后续文件操作访问,
104
+ * 避免本插件变成任意路径读写器。
105
+ */
106
+ export class ProjectRegistry {
107
+ /** @type {Map<string, { root: string, name: string, isGit: boolean, openedAt: number }>} */
108
+ #projects = new Map()
109
+ #order = []
110
+
111
+ get size() {
112
+ return this.#projects.size
113
+ }
114
+
115
+ list() {
116
+ return this.#order.map((root) => ({ ...this.#projects.get(root) }))
117
+ }
118
+
119
+ /** 登记一个已存在的目录为项目根;返回项目描述。 */
120
+ async open(rawPath) {
121
+ if (typeof rawPath !== 'string' || rawPath.trim() === '') throw badRequest('path is required')
122
+ if (!isAbsolute(rawPath)) throw badRequest('path must be absolute')
123
+ const absolute = resolve(normalize(rawPath))
124
+ let canonical
125
+ try {
126
+ canonical = await realpath(absolute)
127
+ } catch {
128
+ throw new ApiError('not-found', `目录不存在:${absolute}`, 404)
129
+ }
130
+ const info = await stat(canonical)
131
+ if (!info.isDirectory()) throw new ApiError('not-directory', `不是目录:${canonical}`, 400)
132
+ const isGit = await stat(resolve(canonical, '.git')).then(() => true, () => false)
133
+ const name = canonical.split(sep).filter(Boolean).at(-1) ?? canonical
134
+ if (!this.#projects.has(canonical)) this.#order.push(canonical)
135
+ this.#projects.set(canonical, { root: canonical, name, isGit, openedAt: Date.now() })
136
+ return { root: canonical, name, isGit }
137
+ }
138
+
139
+ forget(root) {
140
+ this.#projects.delete(root)
141
+ this.#order = this.#order.filter((item) => item !== root)
142
+ }
143
+
144
+ /** 校验并返回项目根;root 必须是白名单内目录。 */
145
+ ensure(root) {
146
+ if (typeof root !== 'string' || root === '') throw badRequest('root is required')
147
+ const found = this.#projects.get(root)
148
+ if (found === undefined) throw new ApiError('project-not-open', '项目未打开,请先打开项目', 403)
149
+ return found
150
+ }
151
+ }
152
+
153
+ export function isInside(root, target) {
154
+ const rel = target.slice(root.length)
155
+ if (target === root) return true
156
+ return (rel.startsWith(sep) || rel.startsWith('/')) && !rel.includes('..')
157
+ }
158
+
159
+ /**
160
+ * 创建路由注册器,并支持 Host 模块热重载:
161
+ * - createScope() / installScope():每一代 host 模块注册进独立 scope,重载时整体替换并执行旧 scope 的清理;
162
+ * - setReloader(load, signature):每次请求前比较 signature(host/*.js mtime),变化则重新 import 并替换。
163
+ *
164
+ * 这样改 host/*.js 后无需重启 Harness(下一次请求自动生效),便于联调。
165
+ */
166
+ export function createRouter(ctx) {
167
+ const wss = new WebSocketServer({ noServer: true })
168
+ let scope = emptyScope()
169
+ let reloader
170
+ let signatureFn
171
+ let loadedSignature
172
+ let failedSignature
173
+ let reloading
174
+
175
+ function emptyScope() {
176
+ return { methods: new Map(), streams: new Map(), sockets: [], assets: [], disposers: [] }
177
+ }
178
+
179
+ /** 新一代模块的注册器(模块通过 register(registrar, env) 使用)。 */
180
+ function createScope() {
181
+ const next = emptyScope()
182
+ const registrar = {
183
+ ctx,
184
+ /** 注册一个 JSON RPC 方法:fn(body, { req, res, signal }) -> value */
185
+ method(name, fn) {
186
+ if (next.methods.has(name)) throw new Error(`duplicate api method ${name}`)
187
+ next.methods.set(name, fn)
188
+ },
189
+ /** 注册一个 SSE 方法:fn(body, { req, res, signal, send, close }) */
190
+ stream(name, fn) {
191
+ if (next.streams.has(name)) throw new Error(`duplicate api stream ${name}`)
192
+ next.streams.set(name, fn)
193
+ },
194
+ /** 注册静态资产:resolver(rawPath) -> { path, type } | undefined */
195
+ asset(prefix, resolver) {
196
+ next.assets.push({ prefix, resolver })
197
+ },
198
+ /** 注册 WebSocket 通道(客户端连 /gufa-code/ws?channel=<name>):handler(connection, url, req) -> cleanup */
199
+ ws(channel, handler) {
200
+ if (next.sockets.some((item) => item.channel === channel)) throw new Error(`duplicate ws channel ${channel}`)
201
+ next.sockets.push({ channel, handler })
202
+ },
203
+ /** 注册本代作用域的清理(重载/卸载时执行)。 */
204
+ onDispose(fn) {
205
+ next.disposers.push(fn)
206
+ },
207
+ /** 测试用:直接访问本代方法表。 */
208
+ peek(name) {
209
+ return next.methods.get(name) ?? next.streams.get(name)
210
+ },
211
+ }
212
+ return { scope: next, registrar }
213
+ }
214
+
215
+ /** 安装新一代 scope:切换后执行上一代清理。 */
216
+ function installScope(next) {
217
+ const previous = scope
218
+ scope = next
219
+ for (const dispose of previous.disposers.splice(0)) {
220
+ try {
221
+ dispose()
222
+ } catch (error) {
223
+ console.error('[gufa-code] scope dispose failed', error)
224
+ }
225
+ }
226
+ }
227
+
228
+ async function ensureFresh() {
229
+ if (reloader === undefined) return
230
+ const signature = typeof signatureFn === 'function' ? signatureFn() : ''
231
+ if (loadedSignature === signature || failedSignature === signature) return
232
+ if (reloading !== undefined) {
233
+ await reloading
234
+ return
235
+ }
236
+ reloading = (async () => {
237
+ try {
238
+ await reloader()
239
+ loadedSignature = signature
240
+ failedSignature = undefined
241
+ } catch (error) {
242
+ failedSignature = signature
243
+ console.error('[gufa-code] host 模块加载失败', error)
244
+ if (scope.methods.size === 0) throw new ApiError('module-load-failed', `host 模块加载失败:${error instanceof Error ? error.message : String(error)}`, 500)
245
+ } finally {
246
+ reloading = undefined
247
+ }
248
+ })()
249
+ await reloading
250
+ }
251
+
252
+ const router = {
253
+ ctx,
254
+ createScope,
255
+ installScope,
256
+ setReloader(load, signature) {
257
+ reloader = load
258
+ signatureFn = signature
259
+ },
260
+ /** 测试钩子:直接调用一个 JSON 方法(跳过 HTTP 鉴权)。 */
261
+ async __dispatch(name, body = {}) {
262
+ await ensureFresh()
263
+ const fn = scope.methods.get(name)
264
+ if (fn === undefined) throw new ApiError('not-found', `unknown api method: ${name}`, 404)
265
+ return await fn(body, {})
266
+ },
267
+ /** 测试钩子:当前已注册的方法/通道/资产数量。 */
268
+ __stats() {
269
+ return { methods: scope.methods.size, streams: scope.streams.size, sockets: scope.sockets.length, assets: scope.assets.length }
270
+ },
271
+ /** 把全部路由挂到 webServer 生命周期上。 */
272
+ mount() {
273
+ ctx.effect(() => ctx.webServer.register({
274
+ kind: 'prefix',
275
+ path: '/gufa-code/api',
276
+ handler: async (req, res) => {
277
+ if (!authorize(req, res, ctx.connection)) return
278
+ if (req.method !== 'POST') return send(res, 405, { ok: false, error: { code: 'method', message: 'POST required' } })
279
+ const url = new URL(req.url ?? '/', 'http://dsh.local')
280
+ const name = decodeURIComponent(url.pathname.slice('/gufa-code/api/'.length))
281
+ const controller = new AbortController()
282
+ req.once('aborted', () => controller.abort())
283
+ try {
284
+ await ensureFresh()
285
+ const body = await readBody(req)
286
+ const streamHandler = scope.streams.get(name)
287
+ if (streamHandler !== undefined) {
288
+ res.writeHead(200, {
289
+ 'content-type': 'text/event-stream; charset=utf-8',
290
+ 'cache-control': 'no-store',
291
+ 'x-accel-buffering': 'no',
292
+ })
293
+ await streamHandler(body, {
294
+ req,
295
+ res,
296
+ signal: controller.signal,
297
+ send(event) {
298
+ if (!res.writableEnded) res.write(`data: ${JSON.stringify(event)}\n\n`)
299
+ },
300
+ close() {
301
+ if (!res.writableEnded) res.end()
302
+ },
303
+ })
304
+ if (!res.writableEnded) res.end()
305
+ return
306
+ }
307
+ const fn = scope.methods.get(name)
308
+ if (fn === undefined) throw new ApiError('not-found', `unknown api method: ${name}`, 404)
309
+ const value = await fn(body, { req, res, signal: controller.signal })
310
+ send(res, 200, { ok: true, value: value ?? null })
311
+ } catch (error) {
312
+ sendError(res, error)
313
+ }
314
+ },
315
+ }), 'dsh-gufa-code: api')
316
+
317
+ ctx.effect(() => ctx.webServer.register({
318
+ kind: 'prefix',
319
+ path: '/gufa-code/asset',
320
+ handler: async (req, res) => {
321
+ if (!authorize(req, res, ctx.connection)) return
322
+ if (req.method !== 'GET' && req.method !== 'HEAD') return send(res, 405, { ok: false, error: { code: 'method', message: 'GET required' } })
323
+ const url = new URL(req.url ?? '/', 'http://dsh.local')
324
+ const rest = decodeURIComponent(url.pathname.slice('/gufa-code/asset/'.length))
325
+ try {
326
+ await ensureFresh()
327
+ for (const { prefix, resolver } of scope.assets) {
328
+ if (!rest.startsWith(`${prefix}/`)) continue
329
+ const raw = rest.slice(prefix.length + 1)
330
+ const found = await resolver(raw)
331
+ if (found === undefined) throw new ApiError('not-found', 'asset not found', 404)
332
+ const info = await stat(found.path)
333
+ if (!info.isFile()) throw new ApiError('not-found', 'asset not found', 404)
334
+ res.writeHead(200, {
335
+ 'content-type': found.type,
336
+ 'content-length': String(info.size),
337
+ 'cache-control': found.cache ?? 'public, max-age=31536000, immutable',
338
+ 'x-content-type-options': 'nosniff',
339
+ })
340
+ if (req.method === 'HEAD') {
341
+ res.end()
342
+ return
343
+ }
344
+ await new Promise((resolvePipe, rejectPipe) => {
345
+ const stream = createReadStream(found.path)
346
+ stream.on('error', rejectPipe)
347
+ stream.on('end', resolvePipe)
348
+ stream.pipe(res)
349
+ })
350
+ return
351
+ }
352
+ throw new ApiError('not-found', 'asset not found', 404)
353
+ } catch (error) {
354
+ sendError(res, error)
355
+ }
356
+ },
357
+ }), 'dsh-gufa-code: assets')
358
+
359
+ ctx.effect(() => ctx.webServer.registerUpgrade({
360
+ path: '/gufa-code/ws',
361
+ handler: async (req, socket, head) => {
362
+ if (ctx.connection.requestRejection(req) !== undefined || !localRequestAllowed(req)) {
363
+ socket.destroy()
364
+ return
365
+ }
366
+ await ensureFresh()
367
+ const url = new URL(req.url ?? '/', 'http://dsh.local')
368
+ const channel = url.searchParams.get('channel')
369
+ const route = scope.sockets.find((item) => item.channel === channel)
370
+ if (route === undefined) {
371
+ socket.destroy()
372
+ return
373
+ }
374
+ wss.handleUpgrade(req, socket, head, (connection) => {
375
+ let cleanup
376
+ try {
377
+ cleanup = route.handler(connection, url, req)
378
+ } catch (error) {
379
+ try {
380
+ connection.close(1011, String(error instanceof Error ? error.message : error))
381
+ } catch {
382
+ /* ignore */
383
+ }
384
+ return
385
+ }
386
+ if (typeof cleanup === 'function') connection.once('close', () => {
387
+ try {
388
+ cleanup()
389
+ } catch {
390
+ /* ignore */
391
+ }
392
+ })
393
+ })
394
+ },
395
+ }), 'dsh-gufa-code: websocket')
396
+
397
+ ctx.effect(() => () => {
398
+ for (const dispose of scope.disposers.splice(0)) {
399
+ try {
400
+ dispose()
401
+ } catch {
402
+ /* ignore */
403
+ }
404
+ }
405
+ wss.close()
406
+ }, 'dsh-gufa-code: teardown')
407
+ },
408
+ }
409
+ return router
410
+ }
411
+
412
+ /** 统一 WS 文本消息协议:JSON 对象。 */
413
+ export function wsJson(connection, value) {
414
+ if (connection.readyState === WebSocket.OPEN) connection.send(JSON.stringify(value))
415
+ }
416
+
417
+ /** 解析 WS 文本消息,非法时返回 undefined。 */
418
+ export function parseWsJson(raw) {
419
+ try {
420
+ const value = JSON.parse(raw.toString())
421
+ return value !== null && typeof value === 'object' ? value : undefined
422
+ } catch {
423
+ return undefined
424
+ }
425
+ }
426
+
427
+ /** 常用 MIME 表。 */
428
+ export function mimeOf(path) {
429
+ const ext = extname(path).toLowerCase()
430
+ if (ext === '.js' || ext === '.mjs') return 'text/javascript; charset=utf-8'
431
+ if (ext === '.css') return 'text/css; charset=utf-8'
432
+ if (ext === '.json' || ext === '.map') return 'application/json; charset=utf-8'
433
+ if (ext === '.ttf') return 'font/ttf'
434
+ if (ext === '.woff' || ext === '.woff2') return 'font/woff2'
435
+ if (ext === '.svg') return 'image/svg+xml'
436
+ if (ext === '.png') return 'image/png'
437
+ return 'application/octet-stream'
438
+ }
package/host/dev.js ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * 开发/诊断信息:供客户端在启动时探测「Host 半是否为最新代码」。
3
+ * 旧版本 Host(没有本方法)会让 api('dev.version') 返回 not-found,客户端据此提示重启 Harness。
4
+ */
5
+ import { appendFileSync } from 'node:fs'
6
+
7
+ const DEBUG_LOG_PATH = '/tmp/gufa-code-debug.log'
8
+
9
+ /** 追加一条客户端诊断日志(排查桌面壳/终端连接问题用)。 */
10
+ export function debugLog(message) {
11
+ try {
12
+ appendFileSync(DEBUG_LOG_PATH, `[${new Date().toISOString()}] client: ${String(message).slice(0, 4000)}\n`, { mode: 0o600 })
13
+ } catch {
14
+ /* 调试日志失败不影响功能 */
15
+ }
16
+ }
17
+
18
+ export function register(router) {
19
+ const startedAt = new Date().toISOString()
20
+
21
+ router.method('dev.log', async (body) => {
22
+ const message = body !== null && typeof body === 'object' && typeof body.message === 'string' ? body.message : ''
23
+ if (message !== '') debugLog(message)
24
+ return { ok: true }
25
+ })
26
+ router.method('dev.version', async () => ({
27
+ name: 'dsh-gufa-code',
28
+ version: '1.0.0',
29
+ host: 'v2-terminal-ws',
30
+ node: process.version,
31
+ platform: process.platform,
32
+ pid: process.pid,
33
+ startedAt,
34
+ }))
35
+ }