dsh-vscode-mode 0.12.0 → 0.13.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.md +129 -103
- package/assets/shell/dsh-open.cs +65 -149
- package/assets/shell/dsh-open.mjs +241 -0
- package/assets/shell/dsh-open.ps1 +28 -47
- package/assets/shell/dsh-open.sh +25 -66
- package/lib/client.js +12465 -11526
- package/lib/client.js.map +1 -1
- package/lib/index.js +3889 -2336
- package/lib/index.js.map +1 -1
- package/package.json +9 -8
- package/skills/dsh-vscodemode-mcp/SKILL.md +94 -132
- package/src/childEnv.ts +19 -0
- package/src/client/addToConversation.ts +4 -2
- package/src/client/ai/inlineProvider.ts +1 -8
- package/src/client/commandBridge.ts +6 -2
- package/src/client/externalOpen.ts +40 -19
- package/src/client/index.ts +10 -4
- package/src/client/keybindings.ts +18 -1
- package/src/client/monaco/loader.ts +3 -1
- package/src/client/openFlow.ts +36 -28
- package/src/client/openReceipt.ts +64 -0
- package/src/client/panelCommands.ts +101 -0
- package/src/client/pdf/pdfLoader.ts +4 -2
- package/src/client/receiptState.ts +120 -0
- package/src/client/rpc.ts +30 -4
- package/src/client/shortcutRecorder.ts +161 -0
- package/src/client/shortcutsOfficial.ts +13 -4
- package/src/client/sidebarBridge.ts +7 -0
- package/src/client/ui/DiffBadge.ts +2 -1
- package/src/client/ui/EditorView.ts +237 -55
- package/src/client/ui/McpSettings.ts +6 -3
- package/src/client/ui/OfficialSideTab.ts +5 -2
- package/src/client/ui/QuickOpen.ts +10 -9
- package/src/client/ui/ShortcutSettings.ts +34 -54
- package/src/client/ui/commandCatalog.ts +5 -4
- package/src/client/useOpenReceipt.ts +128 -0
- package/src/compat.ts +5 -3
- package/src/dap/manager.ts +46 -13
- package/src/devForm.ts +252 -92
- package/src/externalHandoff.ts +93 -52
- package/src/fileOpenSettings.ts +1 -24
- package/src/hostImport.ts +19 -0
- package/src/httpGuard.ts +101 -0
- package/src/index.ts +106 -97
- package/src/integrate.ts +56 -50
- package/src/lsp/transport.ts +84 -127
- package/src/mcp.ts +58 -9
- package/src/mcpIsolation.ts +211 -102
- package/src/mcpProject.ts +220 -133
- package/src/mcpRuntime.ts +343 -0
- package/src/openBridge.ts +69 -0
- package/src/openInbox.ts +207 -0
- package/src/paths.ts +8 -5
- package/src/processTree.ts +63 -0
- package/src/reveal.ts +5 -3
- package/src/routes.ts +156 -154
- package/src/rpc.ts +14 -13
- package/src/shared/appUrl.ts +19 -0
- package/src/shared/externalOpen.ts +71 -0
- package/src/shared/integration.ts +2 -0
- package/src/shared/keybindings.ts +8 -5
- package/src/shared/mcp.ts +4 -1
- package/src/shared/rpc.ts +7 -4
- package/src/shared/svn.ts +6 -8
- package/src/svnPlanTool.ts +51 -0
- package/src/unityBridge.ts +26 -3
- package/unity/com.dsh.editor/Editor/DshCodeEditor.cs +99 -203
- package/unity/com.dsh.editor/Editor/DshOpenWorker.cs +166 -0
- package/unity/com.dsh.editor/Editor/DshOpenWorker.cs.meta +2 -0
- package/unity/com.dsh.editor/README.md +16 -25
- package/unity/com.dsh.editor/package.json +2 -2
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** LSP/DAP 共用进程树终止;POSIX 子进程必须 detached 成为独立进程组。 */
|
|
2
|
+
import { spawn, spawnSync } from 'node:child_process'
|
|
3
|
+
import type { ChildProcess } from 'node:child_process'
|
|
4
|
+
import { childEnv } from './childEnv.js'
|
|
5
|
+
|
|
6
|
+
type Logger = (line: string) => void
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 按平台回收进程树;Windows 在父进程仍存活时直接 taskkill /T,避免先杀父丢失后代。
|
|
10
|
+
* @public
|
|
11
|
+
* @author ddj 2026年09月28号
|
|
12
|
+
* @param child 已启动的独立进程组根
|
|
13
|
+
* @param logger 可选错误记录器
|
|
14
|
+
* @param sync 宿主 exit 事件中必须同步完成
|
|
15
|
+
* @param platform 当前系统,测试可注入
|
|
16
|
+
*/
|
|
17
|
+
export function killTree(child: ChildProcess, logger?: Logger, sync = false, platform = process.platform): void {
|
|
18
|
+
if (!child.pid) return
|
|
19
|
+
try {
|
|
20
|
+
if (platform !== 'win32') {
|
|
21
|
+
process.kill(-child.pid, 'SIGKILL')
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
const args = ['/PID', String(child.pid), '/T', '/F']
|
|
25
|
+
const options = { windowsHide: true, stdio: 'ignore' as const, env: childEnv() }
|
|
26
|
+
if (sync) {
|
|
27
|
+
const result = spawnSync('taskkill', args, { ...options, timeout: 5000 })
|
|
28
|
+
if (result.error) logger?.('taskkill error: ' + String(result.error))
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
const killer = spawn('taskkill', args, options)
|
|
32
|
+
killer.once('error', (error) => logger?.('taskkill error: ' + String(error)))
|
|
33
|
+
killer.unref()
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if ((error as NodeJS.ErrnoException).code !== 'ESRCH') logger?.('kill tree error: ' + String(error))
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 停止独立进程树;POSIX 先通知整组,定时升级不依赖根进程 alive 状态。
|
|
41
|
+
* @public
|
|
42
|
+
* @author ddj 2026年09月28号
|
|
43
|
+
* @param child 进程树根
|
|
44
|
+
* @param logger 可选诊断输出
|
|
45
|
+
* @param platform 当前系统,测试可注入
|
|
46
|
+
* @returns 可立即强杀的收尾函数(幂等)
|
|
47
|
+
*/
|
|
48
|
+
export function stopTree(child: ChildProcess, logger?: Logger, platform = process.platform): () => void {
|
|
49
|
+
let finished = false
|
|
50
|
+
let timer: NodeJS.Timeout | undefined
|
|
51
|
+
/** @private @author ddj 2026年09月28号 @description 强杀树并清理升级定时器。 */
|
|
52
|
+
const finish = (): void => {
|
|
53
|
+
if (finished) return
|
|
54
|
+
finished = true
|
|
55
|
+
if (timer) clearTimeout(timer)
|
|
56
|
+
killTree(child, logger, false, platform)
|
|
57
|
+
}
|
|
58
|
+
if (platform === 'win32') { finish(); return finish }
|
|
59
|
+
try { if (child.pid) process.kill(-child.pid, 'SIGTERM') } catch { /* 根可能已经退出,仍保留升级 */ }
|
|
60
|
+
timer = setTimeout(finish, 1500)
|
|
61
|
+
timer.unref()
|
|
62
|
+
return finish
|
|
63
|
+
}
|
package/src/reveal.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { spawn } from 'node:child_process'
|
|
13
13
|
import { dirname } from 'node:path'
|
|
14
|
+
import { childEnv } from './childEnv.js'
|
|
14
15
|
|
|
15
16
|
/** reveal 结果(沿 revert.ts 的 Result 风格)。 */
|
|
16
17
|
export type RevealResult = { ok: true } | { ok: false; error: string }
|
|
@@ -42,12 +43,13 @@ export function revealCommand(absPath: string, isDir: boolean, platform: NodeJS.
|
|
|
42
43
|
* 构造 opener 子进程启动选项。
|
|
43
44
|
* `windowsHide` 必须为 **false**:置 true 会把 Explorer 等 GUI 窗口一起隐藏,
|
|
44
45
|
* 症状即「RPC 回 ok:true 但窗口不出现」(本文件头注释记录的回归)。
|
|
45
|
-
* @
|
|
46
|
+
* @public
|
|
47
|
+
* @author ddj 2026年09月28号
|
|
46
48
|
* @param cwd 子进程工作目录(打开文件时用其父目录)
|
|
47
49
|
* @returns child_process.spawn 选项
|
|
48
50
|
*/
|
|
49
|
-
export function revealSpawnOpts(cwd: string): { cwd: string; stdio: 'ignore'; windowsHide: false; detached: true } {
|
|
50
|
-
return { cwd, stdio: 'ignore', windowsHide: false, detached: true }
|
|
51
|
+
export function revealSpawnOpts(cwd: string): { cwd: string; stdio: 'ignore'; windowsHide: false; detached: true; env: NodeJS.ProcessEnv } {
|
|
52
|
+
return { cwd, stdio: 'ignore', windowsHide: false, detached: true, env: childEnv() }
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
/**
|
package/src/routes.ts
CHANGED
|
@@ -1,183 +1,185 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
*/
|
|
6
|
-
import { readFile, stat } from 'node:fs/promises'
|
|
7
|
-
import { extname, join, normalize, resolve, sep } from 'node:path'
|
|
1
|
+
/** Authenticated plugin HTTP routes and offline editor assets. @author ddj 2026年09月28号 */
|
|
2
|
+
import { readFile, realpath, stat } from 'node:fs/promises'
|
|
3
|
+
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
4
|
+
import { extname, isAbsolute, join, relative, resolve } from 'node:path'
|
|
8
5
|
import { ROUTE_PREFIX, noteOwnRoute, resetOwnRoutes, routeConflict } from './compat.js'
|
|
9
6
|
import { RPC_PATH } from './shared/rpc.js'
|
|
10
7
|
import type { RpcMethod, RpcRequestMap } from './shared/rpc.js'
|
|
11
8
|
import type { Ctx } from './store.js'
|
|
12
9
|
import { VENDOR_PREFIX, imageDirOf, vendorDirOf } from './paths.js'
|
|
10
|
+
import { admitRoute, readRpc, replyJson } from './httpGuard.js'
|
|
13
11
|
|
|
14
12
|
const IMG_ROUTES = [
|
|
15
13
|
{ url: '/edrv/assets/compare-idle.png', file: 'compare_idle.png' },
|
|
16
14
|
{ url: '/edrv/assets/compare-select.png', file: 'compare_select.png' },
|
|
17
15
|
]
|
|
18
|
-
|
|
19
|
-
/** Monaco AMD 构建静态资源(随包发布,离线可用;前缀路由 /edrv/vendor/*)。 */
|
|
20
16
|
const VENDOR_MIME: Record<string, string> = {
|
|
21
|
-
'.js': 'text/javascript; charset=utf-8',
|
|
22
|
-
'.
|
|
23
|
-
'.
|
|
24
|
-
'.
|
|
25
|
-
'.
|
|
26
|
-
'.
|
|
27
|
-
'.svg': 'image/svg+xml',
|
|
28
|
-
'.png': 'image/png',
|
|
29
|
-
'.gif': 'image/gif',
|
|
30
|
-
'.ico': 'image/x-icon',
|
|
31
|
-
'.woff': 'font/woff',
|
|
32
|
-
'.woff2': 'font/woff2',
|
|
33
|
-
'.ttf': 'font/ttf',
|
|
34
|
-
'.html': 'text/html; charset=utf-8',
|
|
35
|
-
'.md': 'text/markdown',
|
|
36
|
-
'.bcmap': 'application/octet-stream',
|
|
37
|
-
'.pfb': 'application/octet-stream',
|
|
17
|
+
'.js': 'text/javascript; charset=utf-8', '.mjs': 'text/javascript; charset=utf-8',
|
|
18
|
+
'.css': 'text/css; charset=utf-8', '.json': 'application/json', '.map': 'application/json',
|
|
19
|
+
'.ts': 'text/plain; charset=utf-8', '.svg': 'image/svg+xml', '.png': 'image/png',
|
|
20
|
+
'.gif': 'image/gif', '.ico': 'image/x-icon', '.woff': 'font/woff', '.woff2': 'font/woff2',
|
|
21
|
+
'.ttf': 'font/ttf', '.html': 'text/html; charset=utf-8', '.md': 'text/markdown',
|
|
22
|
+
'.bcmap': 'application/octet-stream', '.pfb': 'application/octet-stream',
|
|
38
23
|
}
|
|
39
24
|
|
|
40
|
-
|
|
41
|
-
|
|
25
|
+
type RpcHandler = <M extends RpcMethod>(method: M, args: RpcRequestMap[M]) => Promise<unknown>
|
|
26
|
+
type HttpHandler = (req: IncomingMessage, res: ServerResponse) => Promise<void>
|
|
27
|
+
interface PluginRoute { kind: 'exact' | 'prefix'; path: string; methods: string[]; handler: HttpHandler }
|
|
42
28
|
|
|
43
|
-
|
|
44
|
-
|
|
29
|
+
// #region RPC
|
|
30
|
+
/**
|
|
31
|
+
* Serialize RPC results, retaining the binary preview transport.
|
|
32
|
+
* @author ddj 2026年09月28号
|
|
33
|
+
* @param res HTTP response.
|
|
34
|
+
* @param result RPC business result or binary envelope.
|
|
35
|
+
*/
|
|
36
|
+
function sendRpc(res: ServerResponse, result: unknown): void {
|
|
37
|
+
const envelope = result as { ok?: boolean; binary?: { bytes?: Uint8Array; mime?: string; size?: number; version?: string } } | null
|
|
38
|
+
const binary = envelope?.binary
|
|
39
|
+
if (envelope?.ok !== true || !(binary?.bytes instanceof Uint8Array)) {
|
|
40
|
+
replyJson(res, 200, result)
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
res.statusCode = 200
|
|
44
|
+
res.setHeader('cache-control', 'no-store')
|
|
45
|
+
res.setHeader('content-type', 'application/octet-stream')
|
|
46
|
+
res.setHeader('x-edrv-mime', String(binary.mime ?? ''))
|
|
47
|
+
res.setHeader('x-edrv-version', String(binary.version ?? ''))
|
|
48
|
+
res.setHeader('x-edrv-size', String(binary.size ?? binary.bytes.byteLength))
|
|
49
|
+
res.end(Buffer.from(binary.bytes))
|
|
50
|
+
}
|
|
45
51
|
|
|
46
52
|
/**
|
|
47
|
-
*
|
|
48
|
-
* @author ddj 2026年
|
|
49
|
-
* @param
|
|
50
|
-
* @
|
|
51
|
-
* @param handleRpc RPC 分发入口(method,args → RpcResult)
|
|
52
|
-
* @param onWarning 兼容性警告收集(可选)
|
|
53
|
+
* Build a bounded RPC handler; authorization is applied by mountRoute.
|
|
54
|
+
* @author ddj 2026年09月28号
|
|
55
|
+
* @param dispatch Business dispatcher.
|
|
56
|
+
* @returns HTTP handler rejecting malformed bodies before dispatch.
|
|
53
57
|
*/
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
function rpcHandler(dispatch: RpcHandler): HttpHandler {
|
|
59
|
+
/** @author ddj 2026年09月28号 @param req Admitted request. @param res HTTP response. */
|
|
60
|
+
async function handle(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
|
61
|
+
const decoded = await readRpc(req)
|
|
62
|
+
if ('status' in decoded) {
|
|
63
|
+
replyJson(res, decoded.status, { ok: false, error: decoded.error })
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
const { method, args } = decoded.input
|
|
67
|
+
sendRpc(res, await dispatch(method as RpcMethod, args as never))
|
|
68
|
+
}
|
|
69
|
+
return handle
|
|
70
|
+
}
|
|
71
|
+
// #endregion
|
|
63
72
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
73
|
+
// #region Static files
|
|
74
|
+
/**
|
|
75
|
+
* Check a resolved asset remains under its canonical asset root.
|
|
76
|
+
* @author ddj 2026年09月28号
|
|
77
|
+
* @param root Canonical asset directory.
|
|
78
|
+
* @param target Candidate canonical file path.
|
|
79
|
+
* @returns Whether the target lies strictly below the root.
|
|
80
|
+
*/
|
|
81
|
+
function assetInside(root: string, target: string): boolean {
|
|
82
|
+
const rel = relative(root, target)
|
|
83
|
+
return rel !== '' && rel !== '..' && !rel.startsWith('../') && !rel.startsWith('..\\') && !isAbsolute(rel)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Serve a file with conditional caching, honoring HEAD without loading its body.
|
|
88
|
+
* @author ddj 2026年09月28号
|
|
89
|
+
* @param req Admitted GET or HEAD request.
|
|
90
|
+
* @param res HTTP response.
|
|
91
|
+
* @param target Canonical asset file.
|
|
92
|
+
* @param mime Content type.
|
|
93
|
+
*/
|
|
94
|
+
async function sendAsset(req: IncomingMessage, res: ServerResponse, target: string, mime: string): Promise<void> {
|
|
95
|
+
const info = await stat(target)
|
|
96
|
+
if (!info.isFile()) { res.statusCode = 404; res.end(); return }
|
|
97
|
+
const etag = 'W/"' + info.mtimeMs.toString(16) + '-' + info.size.toString(16) + '"'
|
|
98
|
+
res.setHeader('etag', etag)
|
|
99
|
+
res.setHeader('cache-control', 'private, max-age=3600')
|
|
100
|
+
if (req.headers['if-none-match'] === etag) { res.statusCode = 304; res.end(); return }
|
|
101
|
+
res.statusCode = 200
|
|
102
|
+
res.setHeader('content-type', mime)
|
|
103
|
+
res.setHeader('content-length', info.size)
|
|
104
|
+
res.end(req.method === 'HEAD' ? undefined : await readFile(target))
|
|
105
|
+
}
|
|
67
106
|
|
|
68
|
-
|
|
69
|
-
|
|
107
|
+
/**
|
|
108
|
+
* Build an asset handler that refuses traversal and symlinks outside its root.
|
|
109
|
+
* @author ddj 2026年09月28号
|
|
110
|
+
* @param directory Static assets directory.
|
|
111
|
+
* @param prefix URL prefix, absent for a fixed asset filename.
|
|
112
|
+
* @param filename Optional fixed asset name.
|
|
113
|
+
* @returns Authenticated route body handler.
|
|
114
|
+
*/
|
|
115
|
+
function assetHandler(directory: string, prefix?: string, filename?: string): HttpHandler {
|
|
116
|
+
/** @author ddj 2026年09月28号 @param req Admitted request. @param res HTTP response. */
|
|
117
|
+
async function handle(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
|
118
|
+
try {
|
|
119
|
+
const pathname = new URL(req.url ?? '/', 'http://localhost').pathname
|
|
120
|
+
const name = filename ?? decodeURIComponent(pathname.slice(prefix!.length)).replace(/^\/+/, '')
|
|
121
|
+
if (!name) { res.statusCode = 404; res.end(); return }
|
|
122
|
+
const root = await realpath(directory)
|
|
123
|
+
const target = await realpath(resolve(root, name))
|
|
124
|
+
if (!assetInside(root, target)) { res.statusCode = 403; res.end(); return }
|
|
125
|
+
await sendAsset(req, res, target, VENDOR_MIME[extname(target)] ?? 'application/octet-stream')
|
|
126
|
+
} catch {
|
|
127
|
+
if (!res.headersSent) res.statusCode = 404
|
|
128
|
+
res.end()
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return handle
|
|
132
|
+
}
|
|
133
|
+
// #endregion
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Register one owned route, enforcing authentication before any asset or RPC access.
|
|
137
|
+
* @author ddj 2026年09月28号
|
|
138
|
+
* @param ctx Host context.
|
|
139
|
+
* @param route Plugin route descriptor.
|
|
140
|
+
* @param warning Optional registration diagnostic sink.
|
|
141
|
+
*/
|
|
142
|
+
function mountRoute(ctx: Ctx, route: PluginRoute, warning?: (text: string) => void): void {
|
|
143
|
+
/** @author ddj 2026年09月28号 @param req Original HTTP request. @param res HTTP response. */
|
|
144
|
+
async function guarded(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
|
145
|
+
if (!admitRoute(ctx, req, res, route.methods)) return
|
|
146
|
+
try { await route.handler(req, res) } catch {
|
|
147
|
+
if (!res.headersSent) replyJson(res, 500, { ok: false, error: 'Plugin request failed' })
|
|
148
|
+
else res.end()
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/** @author ddj 2026年09月28号 @returns Route disposer, or undefined on a registration conflict. */
|
|
152
|
+
function install(): (() => void) | undefined {
|
|
70
153
|
try {
|
|
71
|
-
const dispose =
|
|
154
|
+
const dispose = ctx.get('webServer').register({ kind: route.kind, path: route.path, handler: guarded })
|
|
72
155
|
noteOwnRoute(route.kind, route.path)
|
|
73
156
|
return dispose
|
|
74
157
|
} catch (error) {
|
|
75
|
-
|
|
158
|
+
warning?.('兼容性:' + route.path + ' 注册失败(' + String(error) + ')')
|
|
76
159
|
return undefined
|
|
77
160
|
}
|
|
78
161
|
}
|
|
162
|
+
ctx.effect(install, 'edrv: ' + route.path)
|
|
163
|
+
}
|
|
79
164
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (result && (result as { ok?: boolean }).ok === true && bin?.bytes instanceof Uint8Array) {
|
|
99
|
-
res.statusCode = 200
|
|
100
|
-
res.setHeader('content-type', 'application/octet-stream')
|
|
101
|
-
res.setHeader('x-edrv-mime', String(bin.mime ?? ''))
|
|
102
|
-
res.setHeader('x-edrv-version', String(bin.version ?? ''))
|
|
103
|
-
res.setHeader('x-edrv-size', String(bin.size ?? bin.bytes.byteLength))
|
|
104
|
-
res.end(Buffer.from(bin.bytes))
|
|
105
|
-
return
|
|
106
|
-
}
|
|
107
|
-
res.statusCode = 200
|
|
108
|
-
res.setHeader('content-type', 'application/json')
|
|
109
|
-
res.end(JSON.stringify(result))
|
|
110
|
-
} catch (error) {
|
|
111
|
-
res.statusCode = 500
|
|
112
|
-
res.end(JSON.stringify({ ok: false, error: String(error) }))
|
|
113
|
-
}
|
|
114
|
-
},
|
|
115
|
-
}, '/edrv/rpc 精确路由'), 'edrv: routes')
|
|
116
|
-
|
|
117
|
-
for (const r of IMG_ROUTES) {
|
|
118
|
-
ctx.effect(() => guarded({
|
|
119
|
-
kind: 'exact',
|
|
120
|
-
path: r.url,
|
|
121
|
-
handler: async (req: unknown, res: { statusCode?: number; setHeader: (k: string, v: string) => void; end: (b?: string | Uint8Array) => void }) => {
|
|
122
|
-
try {
|
|
123
|
-
const body = await readFile(imgDir + '/' + r.file)
|
|
124
|
-
res.statusCode = 200
|
|
125
|
-
res.setHeader('content-type', 'image/png')
|
|
126
|
-
res.setHeader('cache-control', 'no-cache')
|
|
127
|
-
res.end(body)
|
|
128
|
-
} catch (error) {
|
|
129
|
-
res.statusCode = 404
|
|
130
|
-
res.end()
|
|
131
|
-
}
|
|
132
|
-
},
|
|
133
|
-
}, '静态资源 ' + r.file), 'edrv: routes')
|
|
165
|
+
/**
|
|
166
|
+
* Register plugin RPC and offline asset routes with the official connection fence.
|
|
167
|
+
* @author ddj 2026年09月28号
|
|
168
|
+
* @param ctx Host context.
|
|
169
|
+
* @param config Plugin configuration.
|
|
170
|
+
* @param handleRpc Business RPC dispatcher.
|
|
171
|
+
* @param onWarning Optional compatibility diagnostic sink.
|
|
172
|
+
*/
|
|
173
|
+
export function registerRoutes(ctx: Ctx, config: unknown, handleRpc: RpcHandler, onWarning?: (text: string) => void): void {
|
|
174
|
+
const web = ctx.get('webServer')
|
|
175
|
+
if (!web) return
|
|
176
|
+
resetOwnRoutes()
|
|
177
|
+
const conflict = routeConflict(web, ROUTE_PREFIX)
|
|
178
|
+
if (conflict) onWarning?.('兼容性:' + conflict)
|
|
179
|
+
mountRoute(ctx, { kind: 'exact', path: RPC_PATH, methods: ['POST'], handler: rpcHandler(handleRpc) }, onWarning)
|
|
180
|
+
const images = imageDirOf(config, import.meta.url)
|
|
181
|
+
for (const image of IMG_ROUTES) {
|
|
182
|
+
mountRoute(ctx, { kind: 'exact', path: image.url, methods: ['GET', 'HEAD'], handler: assetHandler(images, undefined, image.file) }, onWarning)
|
|
134
183
|
}
|
|
135
|
-
|
|
136
|
-
// Monaco Editor AMD 构建:/edrv/vendor/* → assets/vendor/*(路径穿越防护 + 按扩展名 MIME)
|
|
137
|
-
// ETag 协商缓存:弱 etag(mtime+size),配合 max-age 让浏览器 304 复用,插件升级内容变化自动失效。
|
|
138
|
-
ctx.effect(() => guarded({
|
|
139
|
-
kind: 'prefix',
|
|
140
|
-
path: VENDOR_PREFIX,
|
|
141
|
-
handler: async (req: { method?: string; url?: string; headers?: Record<string, string | string[] | undefined> }, res: { statusCode?: number; setHeader: (k: string, v: string) => void; end: (b?: string | Uint8Array) => void }) => {
|
|
142
|
-
try {
|
|
143
|
-
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
144
|
-
res.statusCode = 405
|
|
145
|
-
res.end()
|
|
146
|
-
return
|
|
147
|
-
}
|
|
148
|
-
const pathname = new URL(req.url ?? '/', 'http://x').pathname
|
|
149
|
-
const rel = decodeURIComponent(pathname.slice(VENDOR_PREFIX.length)).replace(/^\/+/, '')
|
|
150
|
-
if (!rel) { res.statusCode = 404; res.end(); return }
|
|
151
|
-
const target = resolve(normalize(join(vendorDir, rel)))
|
|
152
|
-
if (target !== vendorDir && !target.startsWith(vendorDir + sep)) {
|
|
153
|
-
res.statusCode = 403
|
|
154
|
-
res.end()
|
|
155
|
-
return
|
|
156
|
-
}
|
|
157
|
-
const info = await stat(target).catch(() => null)
|
|
158
|
-
if (!info || !info.isFile()) { res.statusCode = 404; res.end(); return }
|
|
159
|
-
const etag = 'W/"' + info.mtimeMs.toString(16) + '-' + info.size.toString(16) + '"'
|
|
160
|
-
const reqEtag = (() => {
|
|
161
|
-
const raw = req.headers?.['if-none-match']
|
|
162
|
-
if (Array.isArray(raw)) return raw[0]
|
|
163
|
-
return raw
|
|
164
|
-
})()
|
|
165
|
-
if (reqEtag === etag) {
|
|
166
|
-
res.statusCode = 304
|
|
167
|
-
res.setHeader('etag', etag)
|
|
168
|
-
res.end()
|
|
169
|
-
return
|
|
170
|
-
}
|
|
171
|
-
const body = await readFile(target)
|
|
172
|
-
res.statusCode = 200
|
|
173
|
-
res.setHeader('content-type', VENDOR_MIME[extname(target)] ?? 'application/octet-stream')
|
|
174
|
-
res.setHeader('cache-control', 'public, max-age=3600')
|
|
175
|
-
res.setHeader('etag', etag)
|
|
176
|
-
res.end(body)
|
|
177
|
-
} catch (error) {
|
|
178
|
-
res.statusCode = 404
|
|
179
|
-
res.end()
|
|
180
|
-
}
|
|
181
|
-
},
|
|
182
|
-
}, '/edrv/vendor 前缀路由'), 'edrv: routes')
|
|
184
|
+
mountRoute(ctx, { kind: 'prefix', path: VENDOR_PREFIX, methods: ['GET', 'HEAD'], handler: assetHandler(vendorDirOf(import.meta.url), VENDOR_PREFIX) }, onWarning)
|
|
183
185
|
}
|
package/src/rpc.ts
CHANGED
|
@@ -38,7 +38,7 @@ import { normalizeFileOpenTool, FILE_OPEN_DEFAULT, FILE_OPEN_SETTINGS_NS, sectio
|
|
|
38
38
|
import { INTEGRATION_BASE_DEFAULT } from './shared/integration.js'
|
|
39
39
|
import { shellMenuRegister, shellMenuRemove, shellMenuStatus } from './integrate.js'
|
|
40
40
|
import { unityAdd, unityInstall, unityList, unityRemove } from './unityBridge.js'
|
|
41
|
-
import { handoffOpen, pendingState, pollPending } from './externalHandoff.js'
|
|
41
|
+
import { ackPending, handoffOpen, pendingState, pollPending } from './externalHandoff.js'
|
|
42
42
|
import { buildReport } from './compat.js'
|
|
43
43
|
import { findProfileDir, readDevForm, setDevForm } from './devForm.js'
|
|
44
44
|
import { normalizeRel } from './tree.js'
|
|
@@ -296,8 +296,8 @@ function activeSessionIds(ctx: Ctx): Set<string> {
|
|
|
296
296
|
}
|
|
297
297
|
|
|
298
298
|
/** profile patch 文件定位(依赖本插件的 profile,同 devForm)。 */
|
|
299
|
-
async function patchFileInfo(): Promise<{ profileDir?: string; patchPath?: string }> {
|
|
300
|
-
const profileDir = findProfileDir()
|
|
299
|
+
async function patchFileInfo(ctx: Ctx): Promise<{ profileDir?: string; patchPath?: string }> {
|
|
300
|
+
const profileDir = findProfileDir(ctx)
|
|
301
301
|
if (!profileDir) return {}
|
|
302
302
|
return { profileDir, patchPath: join(profileDir, 'cordis.patch.yml') }
|
|
303
303
|
}
|
|
@@ -1009,12 +1009,12 @@ export function buildHandlers(
|
|
|
1009
1009
|
return { ok: true, fileOpenTool: normalizeFileOpenTool(value?.fileOpenTool), integrationBaseUrl: await integrationBaseUrlOf(ctx), revision: section?.revision }
|
|
1010
1010
|
},
|
|
1011
1011
|
'compat': async () => ({ ok: true, report: await buildReport(ctx) }),
|
|
1012
|
-
'vscode.devFormGet': async () => ({ ok: true, devForm: readDevForm() }),
|
|
1012
|
+
'vscode.devFormGet': async () => ({ ok: true, devForm: readDevForm(ctx) }),
|
|
1013
1013
|
'vscode.devFormSet': async (args) => {
|
|
1014
1014
|
try {
|
|
1015
1015
|
const result = await setDevForm(ctx, args.enabled === true, args.path)
|
|
1016
1016
|
if (!result.ok) return { ok: false, error: result.error ?? '切换开发形态失败' }
|
|
1017
|
-
return { ok: true, devForm: readDevForm(), restart: result.restart }
|
|
1017
|
+
return { ok: true, devForm: readDevForm(ctx), restart: result.restart }
|
|
1018
1018
|
} catch (error) {
|
|
1019
1019
|
return { ok: false, error: String(error) }
|
|
1020
1020
|
}
|
|
@@ -1064,7 +1064,7 @@ export function buildHandlers(
|
|
|
1064
1064
|
},
|
|
1065
1065
|
'edrv.unity.install': async (args) => {
|
|
1066
1066
|
try {
|
|
1067
|
-
return { ok: true, project: await unityInstall(args.path) }
|
|
1067
|
+
return { ok: true, project: await unityInstall(args.path, undefined, { ctx, baseUrl: await integrationBaseUrlOf(ctx) }) }
|
|
1068
1068
|
} catch (error) {
|
|
1069
1069
|
return { ok: false, error: String(error) }
|
|
1070
1070
|
}
|
|
@@ -1086,10 +1086,11 @@ export function buildHandlers(
|
|
|
1086
1086
|
ok: true,
|
|
1087
1087
|
...handoffOpen({ paths: args.paths, line: args.line, column: args.column }),
|
|
1088
1088
|
}),
|
|
1089
|
-
'edrv.external.pending': async () => ({ ok: true, open: pollPending() }),
|
|
1089
|
+
'edrv.external.pending': async (args) => ({ ok: true, open: pollPending(args) }),
|
|
1090
|
+
'edrv.external.ack': async (args) => ({ ok: true, accepted: ackPending(args) }),
|
|
1090
1091
|
'edrv.external.pendingState': async (args) => ({
|
|
1091
1092
|
ok: true,
|
|
1092
|
-
|
|
1093
|
+
...pendingState(args.token, args.take === true),
|
|
1093
1094
|
}),
|
|
1094
1095
|
'edrv.perf.inventory': async () => {
|
|
1095
1096
|
try {
|
|
@@ -1163,7 +1164,7 @@ export function buildHandlers(
|
|
|
1163
1164
|
},
|
|
1164
1165
|
'edrv.perf.configGet': async () => {
|
|
1165
1166
|
try {
|
|
1166
|
-
const { profileDir, patchPath } = await patchFileInfo()
|
|
1167
|
+
const { profileDir, patchPath } = await patchFileInfo(ctx)
|
|
1167
1168
|
const block = perfConfigBlock()
|
|
1168
1169
|
if (!patchPath) return { ok: true, applied: false, block }
|
|
1169
1170
|
const text = await readFile(patchPath, 'utf8').catch(() => '')
|
|
@@ -1175,7 +1176,7 @@ export function buildHandlers(
|
|
|
1175
1176
|
},
|
|
1176
1177
|
'edrv.perf.configApply': async () => {
|
|
1177
1178
|
try {
|
|
1178
|
-
const { patchPath } = await patchFileInfo()
|
|
1179
|
+
const { patchPath } = await patchFileInfo(ctx)
|
|
1179
1180
|
if (!patchPath) return { ok: false, error: '未找到依赖本插件的 profile(检查 DSH_HOME/profiles)' }
|
|
1180
1181
|
const text = await readFile(patchPath, 'utf8').catch(() => '')
|
|
1181
1182
|
const backup = patchPath + '.bak-' + Date.now()
|
|
@@ -1189,7 +1190,7 @@ export function buildHandlers(
|
|
|
1189
1190
|
},
|
|
1190
1191
|
'edrv.perf.configUndo': async () => {
|
|
1191
1192
|
try {
|
|
1192
|
-
const { patchPath } = await patchFileInfo()
|
|
1193
|
+
const { patchPath } = await patchFileInfo(ctx)
|
|
1193
1194
|
if (!patchPath) return { ok: false, error: '未找到依赖本插件的 profile(检查 DSH_HOME/profiles)' }
|
|
1194
1195
|
const backup = await latestPatchBackup(patchPath)
|
|
1195
1196
|
if (backup) {
|
|
@@ -1305,7 +1306,7 @@ export async function handleRpc<M extends RpcMethod>(
|
|
|
1305
1306
|
dapHandlers?: Partial<RpcHandlerMap>,
|
|
1306
1307
|
): Promise<RpcResult<M>> {
|
|
1307
1308
|
const handlers = buildHandlers(ctx, registry, searcher, contentSearcher, lspHandlers, aiHandlers, fileVersions, svnHandlers, dapHandlers)
|
|
1308
|
-
const handler = handlers[method]
|
|
1309
|
-
if (
|
|
1309
|
+
const handler = Object.hasOwn(handlers, method) ? handlers[method] : undefined
|
|
1310
|
+
if (typeof handler !== 'function') return { ok: false, error: '未知方法: ' + String(method) } as RpcResult<M>
|
|
1310
1311
|
return handler(args)
|
|
1311
1312
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Browser application URL resolution shared by transport and asset loaders. @author ddj 2026年09月28号 */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolve a product-local path under the current application's document base.
|
|
5
|
+
* Supports HTTP reverse-proxy prefixes and the official Desktop dsh-app origin.
|
|
6
|
+
* @author ddj 2026年09月28号
|
|
7
|
+
* @param path Root-shaped internal endpoint or asset path; never an external URL.
|
|
8
|
+
* @param base Explicit document base for tests; otherwise document.baseURI or location.href.
|
|
9
|
+
* @returns Absolute application URL, or the original root path outside a browser.
|
|
10
|
+
*/
|
|
11
|
+
export function appUrl(path: string, base?: string): string {
|
|
12
|
+
if (!path.startsWith('/') || path.startsWith('//')) throw new Error('Expected an application-local path')
|
|
13
|
+
const source = base ?? (typeof document !== 'undefined' ? document.baseURI : undefined)
|
|
14
|
+
?? (typeof location !== 'undefined' ? location.href : undefined)
|
|
15
|
+
if (!source) return path
|
|
16
|
+
const root = new URL('.', source)
|
|
17
|
+
if (!['http:', 'https:', 'dsh-app:'].includes(root.protocol)) throw new Error('Unsupported application origin')
|
|
18
|
+
return new URL(path.slice(1), root).href
|
|
19
|
+
}
|
|
@@ -21,6 +21,77 @@ export const EDRV_PARAM_KEYS = [EDRV_OPEN_FLAG, EDRV_PATHS_KEY, EDRV_LINE_KEY, E
|
|
|
21
21
|
/** 路径分隔符(编码后路径不含裸逗号,分隔安全)。 */
|
|
22
22
|
export const PATHS_SEPARATOR = ','
|
|
23
23
|
|
|
24
|
+
export const OPEN_TTL_MS = 60_000
|
|
25
|
+
export const OPEN_MAX_BYTES = 65_536
|
|
26
|
+
export const OPEN_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
27
|
+
|
|
28
|
+
export interface InboxOpen extends OpenParams {
|
|
29
|
+
version: 1
|
|
30
|
+
requestId: string
|
|
31
|
+
profile: string
|
|
32
|
+
createdAt: number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface InboxAck {
|
|
36
|
+
version: 1
|
|
37
|
+
requestId: string
|
|
38
|
+
profile: string
|
|
39
|
+
createdAt: number
|
|
40
|
+
success: boolean
|
|
41
|
+
error?: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Detect the host platform without importing node builtins.
|
|
46
|
+
* @author ddj 2026年09月28号
|
|
47
|
+
* @returns Platform name, or an empty string outside Node.
|
|
48
|
+
*/
|
|
49
|
+
function platformName(): string {
|
|
50
|
+
return typeof process === 'object' && process !== null ? String(process.platform) : ''
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Compare profile directories by physical identity, not by spelling.
|
|
55
|
+
* Windows accepts either separator and folds case; other platforms stay exact.
|
|
56
|
+
* @author ddj 2026年09月28号
|
|
57
|
+
* @param left Declared profile directory.
|
|
58
|
+
* @param right Expected profile directory.
|
|
59
|
+
* @param platform Platform name for separator and case rules.
|
|
60
|
+
* @returns Whether both name the same profile.
|
|
61
|
+
*/
|
|
62
|
+
export function sameProfile(left: string, right: string, platform = platformName()): boolean {
|
|
63
|
+
if (platform === 'win32') {
|
|
64
|
+
const fold = (value: string) => value.replace(/\//g, '\\').replace(/\\+$/, '').toLowerCase()
|
|
65
|
+
return fold(left) === fold(right)
|
|
66
|
+
}
|
|
67
|
+
return left.replace(/\/+$/, '') === right.replace(/\/+$/, '')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** @public @author ddj 2026年09月28号
|
|
71
|
+
* Validate the open-only envelope before accepting filesystem input.
|
|
72
|
+
* @param value Parsed untrusted JSON.
|
|
73
|
+
* @param profile Exact expected profile directory.
|
|
74
|
+
* @param now Current epoch milliseconds.
|
|
75
|
+
* @param platform Platform name used for profile identity.
|
|
76
|
+
* @returns A typed request or null.
|
|
77
|
+
*/
|
|
78
|
+
export function parseInboxOpen(value: unknown, profile: string, now = Date.now(), platform = platformName()): InboxOpen | null {
|
|
79
|
+
if (!value || typeof value !== 'object') return null
|
|
80
|
+
const row = value as Record<string, unknown>
|
|
81
|
+
const keys = ['version', 'requestId', 'profile', 'paths', 'line', 'column', 'createdAt']
|
|
82
|
+
if (Object.keys(row).some((key) => !keys.includes(key))) return null
|
|
83
|
+
if (row.version !== 1 || typeof row.profile !== 'string' || !sameProfile(row.profile, profile, platform)
|
|
84
|
+
|| typeof row.requestId !== 'string' || !OPEN_ID.test(row.requestId)) return null
|
|
85
|
+
if (typeof row.createdAt !== 'number' || !Number.isSafeInteger(row.createdAt)) return null
|
|
86
|
+
if (now - row.createdAt >= OPEN_TTL_MS || row.createdAt > now + 5000) return null
|
|
87
|
+
if (!Array.isArray(row.paths) || !row.paths.length || row.paths.length > 20) return null
|
|
88
|
+
if (!row.paths.every((path) => typeof path === 'string' && path.length <= 8192 && !/[\u0000-\u001f]/.test(path) && /^(?:\/|[a-z]:[\\/]|\\\\)/i.test(path))) return null
|
|
89
|
+
for (const key of ['line', 'column']) {
|
|
90
|
+
if (row[key] !== undefined && (!Number.isSafeInteger(row[key]) || Number(row[key]) < 1)) return null
|
|
91
|
+
}
|
|
92
|
+
return row as unknown as InboxOpen
|
|
93
|
+
}
|
|
94
|
+
|
|
24
95
|
/** 一次外部深链请求(解析结果)。 */
|
|
25
96
|
export interface OpenParams {
|
|
26
97
|
/** 待打开路径(已解码,首个路径可携带行列)。 */
|