dsh-gufa-code 0.0.0-stage → 1.0.1
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/LICENSE +21 -0
- package/README.md +93 -2
- package/client.js +11548 -0
- package/cordis.patch.yml +8 -0
- package/docs/icon-180.png +0 -0
- package/docs/icon.svg +18 -0
- package/docs/market-submission.md +32 -0
- package/docs/screenshot-ide.jpg +0 -0
- package/host/assets.js +31 -0
- package/host/chat.js +523 -0
- package/host/context.js +438 -0
- package/host/dev.js +35 -0
- package/host/fs.js +306 -0
- package/host/git.js +532 -0
- package/host/project.js +305 -0
- package/host/ssh.js +54 -0
- package/host/terminal-pty.js +237 -0
- package/host/terminal.js +947 -0
- package/icon.svg +18 -0
- package/index.js +63 -0
- package/locale/en.json +121 -0
- package/locale/zh.json +121 -0
- package/package.json +67 -4
- package/screenshots.json +3 -0
- package/scripts/build-client.mjs +38 -0
- package/scripts/selftest-client.mjs +457 -0
- package/scripts/selftest-host.mjs +197 -0
- package/scripts/selftest-http.mjs +284 -0
- package/scripts/selftest-router.mjs +101 -0
- package/scripts/selftest-terminal-client.mjs +314 -0
- package/scripts/selftest-terminal-pty.mjs +190 -0
- package/scripts/selftest-terminal.mjs +351 -0
- package/scripts/test-chat-render.mjs +702 -0
- package/scripts/test-chat.mjs +761 -0
- package/scripts/test-editor.mjs +516 -0
- package/scripts/verify-workspace-sessions.mjs +135 -0
- package/src/client/00-core.js +1686 -0
- package/src/client/10-icons.js +205 -0
- package/src/client/20-editor.js +1318 -0
- package/src/client/30-terminal.js +1510 -0
- package/src/client/40-chat.js +1184 -0
- package/src/client/46-gitgraph.js +278 -0
- package/src/client/50-panels.js +916 -0
- package/src/client/55-backdrop.js +508 -0
- package/src/client/60-dock.js +662 -0
- package/src/client/85-shortcuts.js +516 -0
- package/src/client/90-app.js +2272 -0
- package/src/client/99-main.js +449 -0
- package/vendor/monaco/LICENSE +21 -0
- package/vendor/monaco/ThirdPartyNotices.txt +448 -0
- package/vendor/monaco/vs/base/browser/ui/codicons/codicon/codicon.ttf +0 -0
- package/vendor/monaco/vs/base/worker/workerMain.js +31 -0
- package/vendor/monaco/vs/basic-languages/abap/abap.js +10 -0
- package/vendor/monaco/vs/basic-languages/apex/apex.js +10 -0
- package/vendor/monaco/vs/basic-languages/azcli/azcli.js +10 -0
- package/vendor/monaco/vs/basic-languages/bat/bat.js +10 -0
- package/vendor/monaco/vs/basic-languages/bicep/bicep.js +11 -0
- package/vendor/monaco/vs/basic-languages/cameligo/cameligo.js +10 -0
- package/vendor/monaco/vs/basic-languages/clojure/clojure.js +10 -0
- package/vendor/monaco/vs/basic-languages/coffee/coffee.js +10 -0
- package/vendor/monaco/vs/basic-languages/cpp/cpp.js +10 -0
- package/vendor/monaco/vs/basic-languages/csharp/csharp.js +10 -0
- package/vendor/monaco/vs/basic-languages/csp/csp.js +10 -0
- package/vendor/monaco/vs/basic-languages/css/css.js +12 -0
- package/vendor/monaco/vs/basic-languages/cypher/cypher.js +10 -0
- package/vendor/monaco/vs/basic-languages/dart/dart.js +10 -0
- package/vendor/monaco/vs/basic-languages/dockerfile/dockerfile.js +10 -0
- package/vendor/monaco/vs/basic-languages/ecl/ecl.js +10 -0
- package/vendor/monaco/vs/basic-languages/elixir/elixir.js +10 -0
- package/vendor/monaco/vs/basic-languages/flow9/flow9.js +10 -0
- package/vendor/monaco/vs/basic-languages/freemarker2/freemarker2.js +12 -0
- package/vendor/monaco/vs/basic-languages/fsharp/fsharp.js +10 -0
- package/vendor/monaco/vs/basic-languages/go/go.js +10 -0
- package/vendor/monaco/vs/basic-languages/graphql/graphql.js +10 -0
- package/vendor/monaco/vs/basic-languages/handlebars/handlebars.js +10 -0
- package/vendor/monaco/vs/basic-languages/hcl/hcl.js +10 -0
- package/vendor/monaco/vs/basic-languages/html/html.js +10 -0
- package/vendor/monaco/vs/basic-languages/ini/ini.js +10 -0
- package/vendor/monaco/vs/basic-languages/java/java.js +10 -0
- package/vendor/monaco/vs/basic-languages/javascript/javascript.js +10 -0
- package/vendor/monaco/vs/basic-languages/julia/julia.js +10 -0
- package/vendor/monaco/vs/basic-languages/kotlin/kotlin.js +10 -0
- package/vendor/monaco/vs/basic-languages/less/less.js +11 -0
- package/vendor/monaco/vs/basic-languages/lexon/lexon.js +10 -0
- package/vendor/monaco/vs/basic-languages/liquid/liquid.js +10 -0
- package/vendor/monaco/vs/basic-languages/lua/lua.js +10 -0
- package/vendor/monaco/vs/basic-languages/m3/m3.js +10 -0
- package/vendor/monaco/vs/basic-languages/markdown/markdown.js +10 -0
- package/vendor/monaco/vs/basic-languages/mdx/mdx.js +10 -0
- package/vendor/monaco/vs/basic-languages/mips/mips.js +10 -0
- package/vendor/monaco/vs/basic-languages/msdax/msdax.js +10 -0
- package/vendor/monaco/vs/basic-languages/mysql/mysql.js +10 -0
- package/vendor/monaco/vs/basic-languages/objective-c/objective-c.js +10 -0
- package/vendor/monaco/vs/basic-languages/pascal/pascal.js +10 -0
- package/vendor/monaco/vs/basic-languages/pascaligo/pascaligo.js +10 -0
- package/vendor/monaco/vs/basic-languages/perl/perl.js +10 -0
- package/vendor/monaco/vs/basic-languages/pgsql/pgsql.js +10 -0
- package/vendor/monaco/vs/basic-languages/php/php.js +10 -0
- package/vendor/monaco/vs/basic-languages/pla/pla.js +10 -0
- package/vendor/monaco/vs/basic-languages/postiats/postiats.js +10 -0
- package/vendor/monaco/vs/basic-languages/powerquery/powerquery.js +10 -0
- package/vendor/monaco/vs/basic-languages/powershell/powershell.js +10 -0
- package/vendor/monaco/vs/basic-languages/protobuf/protobuf.js +11 -0
- package/vendor/monaco/vs/basic-languages/pug/pug.js +10 -0
- package/vendor/monaco/vs/basic-languages/python/python.js +10 -0
- package/vendor/monaco/vs/basic-languages/qsharp/qsharp.js +10 -0
- package/vendor/monaco/vs/basic-languages/r/r.js +10 -0
- package/vendor/monaco/vs/basic-languages/razor/razor.js +10 -0
- package/vendor/monaco/vs/basic-languages/redis/redis.js +10 -0
- package/vendor/monaco/vs/basic-languages/redshift/redshift.js +10 -0
- package/vendor/monaco/vs/basic-languages/restructuredtext/restructuredtext.js +10 -0
- package/vendor/monaco/vs/basic-languages/ruby/ruby.js +10 -0
- package/vendor/monaco/vs/basic-languages/rust/rust.js +10 -0
- package/vendor/monaco/vs/basic-languages/sb/sb.js +10 -0
- package/vendor/monaco/vs/basic-languages/scala/scala.js +10 -0
- package/vendor/monaco/vs/basic-languages/scheme/scheme.js +10 -0
- package/vendor/monaco/vs/basic-languages/scss/scss.js +12 -0
- package/vendor/monaco/vs/basic-languages/shell/shell.js +10 -0
- package/vendor/monaco/vs/basic-languages/solidity/solidity.js +10 -0
- package/vendor/monaco/vs/basic-languages/sophia/sophia.js +10 -0
- package/vendor/monaco/vs/basic-languages/sparql/sparql.js +10 -0
- package/vendor/monaco/vs/basic-languages/sql/sql.js +10 -0
- package/vendor/monaco/vs/basic-languages/st/st.js +10 -0
- package/vendor/monaco/vs/basic-languages/swift/swift.js +13 -0
- package/vendor/monaco/vs/basic-languages/systemverilog/systemverilog.js +10 -0
- package/vendor/monaco/vs/basic-languages/tcl/tcl.js +10 -0
- package/vendor/monaco/vs/basic-languages/twig/twig.js +10 -0
- package/vendor/monaco/vs/basic-languages/typescript/typescript.js +10 -0
- package/vendor/monaco/vs/basic-languages/typespec/typespec.js +10 -0
- package/vendor/monaco/vs/basic-languages/vb/vb.js +10 -0
- package/vendor/monaco/vs/basic-languages/wgsl/wgsl.js +307 -0
- package/vendor/monaco/vs/basic-languages/xml/xml.js +10 -0
- package/vendor/monaco/vs/basic-languages/yaml/yaml.js +10 -0
- package/vendor/monaco/vs/editor/editor.main.css +8 -0
- package/vendor/monaco/vs/editor/editor.main.js +798 -0
- package/vendor/monaco/vs/language/css/cssMode.js +13 -0
- package/vendor/monaco/vs/language/css/cssWorker.js +77 -0
- package/vendor/monaco/vs/language/html/htmlMode.js +13 -0
- package/vendor/monaco/vs/language/html/htmlWorker.js +454 -0
- package/vendor/monaco/vs/language/json/jsonMode.js +19 -0
- package/vendor/monaco/vs/language/json/jsonWorker.js +42 -0
- package/vendor/monaco/vs/language/typescript/tsMode.js +20 -0
- package/vendor/monaco/vs/language/typescript/tsWorker.js +51328 -0
- package/vendor/monaco/vs/loader.js +11 -0
- package/vendor/monaco/vs/nls.messages.de.js +21 -0
- package/vendor/monaco/vs/nls.messages.es.js +21 -0
- package/vendor/monaco/vs/nls.messages.fr.js +19 -0
- package/vendor/monaco/vs/nls.messages.it.js +19 -0
- package/vendor/monaco/vs/nls.messages.ja.js +21 -0
- package/vendor/monaco/vs/nls.messages.ko.js +19 -0
- package/vendor/monaco/vs/nls.messages.ru.js +21 -0
- package/vendor/monaco/vs/nls.messages.zh-cn.js +21 -0
- package/vendor/monaco/vs/nls.messages.zh-tw.js +19 -0
- package/vendor/xterm/LICENSE +21 -0
- package/vendor/xterm/LICENSE.addon-fit +19 -0
- package/vendor/xterm/addon-fit.js +2 -0
- package/vendor/xterm/xterm.css +285 -0
- package/vendor/xterm/xterm.js +2 -0
package/host/context.js
ADDED
|
@@ -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
|
+
}
|