dsh-vscode-mode 0.1.57 → 0.1.59
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/lib/client.js +551 -31
- package/lib/client.js.map +1 -1
- package/lib/index.js +561 -98
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/ai/inline.ts +192 -0
- package/src/ai/llmTypes.ts +83 -0
- package/src/ai/rpc.ts +35 -0
- package/src/capture.ts +2 -1
- package/src/client/ai/inlineProvider.ts +190 -0
- package/src/client/compat.ts +3 -2
- package/src/client/externalOpen.ts +3 -2
- package/src/client/index.ts +7 -6
- package/src/client/log.ts +10 -0
- package/src/client/sidebarBridge.ts +6 -5
- package/src/client/state/records.ts +1 -1
- package/src/client/state/regions.ts +13 -6
- package/src/client/ui/AiSettings.ts +104 -0
- package/src/client/ui/EditorView.ts +65 -4
- package/src/client/ui/McpSettings.ts +3 -1
- package/src/debugLog.ts +84 -0
- package/src/fileOpenSettings.ts +70 -8
- package/src/host-deps.d.ts +3 -0
- package/src/index.ts +21 -14
- package/src/log.ts +19 -0
- package/src/mcpIsolation.ts +2 -1
- package/src/rpc.ts +10 -76
- package/src/shared/ai.ts +158 -0
- package/src/shared/diff.ts +18 -0
- package/src/shared/logger.ts +114 -0
- package/src/shared/rpc.ts +9 -0
- package/src/store.ts +8 -4
- package/src/workspace.ts +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vscode-mode",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.59",
|
|
4
4
|
"description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 dsh-better-sidebar 侧边栏与对话同屏)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚)+ 语言服务器(LSP)智能(跳转定义/引用/hover/大纲 + VSIX 扩展市场安装),状态持久化到工作区旁车",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
package/src/ai/inline.ts
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode host — AI 内联补全服务。
|
|
3
|
+
* 一次性非会话调用:配置解析(开关→provider/model 路由→思考档位)→ ctx.llm.stream
|
|
4
|
+
* 流式汇聚(只收 text-delta,reasoning-delta 天然不进补全)→ 早停 → 输出清理。
|
|
5
|
+
* llm 服务经 ctx.get('llm') 惰性获取(不用 inject:缺失时插件仍加载,AI 补全降级)。
|
|
6
|
+
* 模型目录(edrv.ai.models):listProviders/listModels/resolveModelInfo 逐模型取档位,
|
|
7
|
+
* 进程级缓存 5 分钟,单模型 resolve 失败降级为无档位条目。
|
|
8
|
+
* 作者 ddj
|
|
9
|
+
*/
|
|
10
|
+
import type { Ctx } from '../store.js'
|
|
11
|
+
import { log } from '../log.js'
|
|
12
|
+
import type { LlmRuntimeLike, StreamChunk } from './llmTypes.js'
|
|
13
|
+
import {
|
|
14
|
+
AI_EARLY_STOP_CHARS, AI_INLINE_SYSTEM, AI_MAX_LINES, AI_MAX_TOKENS, AI_TIMEOUT_MS,
|
|
15
|
+
buildInlinePrompt, cleanInlineText,
|
|
16
|
+
type AiConfigView, type AiDirectoryView, type AiInlineRequest, type AiInlineResult,
|
|
17
|
+
} from '../shared/ai.js'
|
|
18
|
+
|
|
19
|
+
/** AI 补全配置来源(settings section,fileOpenSettings.ts 提供 getter)。 */
|
|
20
|
+
export interface AiConfigSource {
|
|
21
|
+
get(): AiConfigView
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** host 补全服务依赖(装配时一次性注入)。 */
|
|
25
|
+
export interface AiInlineDeps {
|
|
26
|
+
ctx: Ctx
|
|
27
|
+
config: AiConfigSource
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 模型目录缓存(进程级:5 分钟 TTL,避免每次打开设置页都逐模型 resolve)。 */
|
|
31
|
+
const DIR_TTL_MS = 5 * 60 * 1000
|
|
32
|
+
let dirCache: { view: AiDirectoryView; at: number } | null = null
|
|
33
|
+
|
|
34
|
+
/** 复位目录缓存(测试隔离用)。 */
|
|
35
|
+
export function resetAiDirCache(): void {
|
|
36
|
+
dirCache = null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 取 llm 运行时(惰性;缺失/未装配返回 null 并给诊断)。
|
|
41
|
+
* @author ddj
|
|
42
|
+
* @param ctx DSH 上下文
|
|
43
|
+
* @returns llm 运行时或 null
|
|
44
|
+
*/
|
|
45
|
+
function llmOf(ctx: Ctx): LlmRuntimeLike | null {
|
|
46
|
+
try {
|
|
47
|
+
const llm = ctx.get('llm') as LlmRuntimeLike | undefined
|
|
48
|
+
return llm && typeof llm.stream === 'function' ? llm : null
|
|
49
|
+
} catch {
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 解析请求路由:用户配置优先(provider/model 均非空),否则取目录首个 provider+model。
|
|
56
|
+
* @author ddj
|
|
57
|
+
* @param llm llm 运行时
|
|
58
|
+
* @param cfg AI 配置
|
|
59
|
+
* @returns 路由(provider/model);不可路由返回 null
|
|
60
|
+
*/
|
|
61
|
+
async function routeOf(llm: LlmRuntimeLike, cfg: AiConfigView): Promise<{ provider: string; model: string } | null> {
|
|
62
|
+
if (cfg.provider && cfg.model) return { provider: cfg.provider, model: cfg.model }
|
|
63
|
+
const providers = llm.listProviders()
|
|
64
|
+
if (!providers.length) return null
|
|
65
|
+
const provider = cfg.provider || providers[0].id
|
|
66
|
+
const models = await llm.listModels(provider).catch(() => [])
|
|
67
|
+
if (!models.length) return null
|
|
68
|
+
return { provider, model: models[0].id }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 单次 AI 内联补全。
|
|
73
|
+
* @author ddj
|
|
74
|
+
* @param deps 服务依赖
|
|
75
|
+
* @param req 补全请求(client 已裁剪窗口)
|
|
76
|
+
* @returns 补全结果(text 空 = 无建议)
|
|
77
|
+
*/
|
|
78
|
+
export async function aiInlineComplete(deps: AiInlineDeps, req: AiInlineRequest): Promise<AiInlineResult> {
|
|
79
|
+
const cfg = deps.config.get()
|
|
80
|
+
if (!cfg.enabled) return { text: '' }
|
|
81
|
+
if (!req || typeof req.prefix !== 'string' || !req.prefix) return { text: '' }
|
|
82
|
+
const llm = llmOf(deps.ctx)
|
|
83
|
+
if (!llm) return { text: '', note: 'llm 服务不可用' }
|
|
84
|
+
const route = await routeOf(llm, cfg)
|
|
85
|
+
if (!route) return { text: '', note: '未找到可用模型(请检查 DSH 模型配置)' }
|
|
86
|
+
const controller = new AbortController()
|
|
87
|
+
const timer = setTimeout(() => controller.abort(new Error('timeout')), AI_TIMEOUT_MS)
|
|
88
|
+
try {
|
|
89
|
+
const stream = llm.stream({
|
|
90
|
+
provider: route.provider,
|
|
91
|
+
model: route.model,
|
|
92
|
+
...(cfg.effort ? { reasoningEffort: cfg.effort } : {}),
|
|
93
|
+
messages: [
|
|
94
|
+
{
|
|
95
|
+
role: 'user',
|
|
96
|
+
content: [{ type: 'text', text: buildInlinePrompt(req) }],
|
|
97
|
+
source: { kind: 'user' },
|
|
98
|
+
} as never,
|
|
99
|
+
],
|
|
100
|
+
system: AI_INLINE_SYSTEM,
|
|
101
|
+
temperature: 0.2,
|
|
102
|
+
maxTokens: AI_MAX_TOKENS,
|
|
103
|
+
// 不传 stop:llm-pi-ai 等适配器不支持该选项(UNSUPPORTED_OPTION 直接 error finish);
|
|
104
|
+
// 截断由行数/字符早停兜底
|
|
105
|
+
signal: controller.signal,
|
|
106
|
+
})
|
|
107
|
+
let text = ''
|
|
108
|
+
let aborted = false
|
|
109
|
+
let failNote = ''
|
|
110
|
+
let reasoningChars = 0
|
|
111
|
+
for await (const chunk of stream) {
|
|
112
|
+
const c = chunk as StreamChunk
|
|
113
|
+
if (c.type === 'text-delta') {
|
|
114
|
+
text += c.text
|
|
115
|
+
// 早停:满 3 行或 300 字符即掐断流,省尾部 token 延迟
|
|
116
|
+
if (text.split('\n').length > AI_MAX_LINES || text.length >= AI_EARLY_STOP_CHARS) {
|
|
117
|
+
controller.abort(new Error('early-stop'))
|
|
118
|
+
aborted = true
|
|
119
|
+
}
|
|
120
|
+
} else if (c.type === 'reasoning-delta') {
|
|
121
|
+
reasoningChars += c.text.length
|
|
122
|
+
} else if (c.type === 'finish') {
|
|
123
|
+
// 适配器失败走 finish(error/aborted) 而非抛异常:检查 reason.kind 带回诊断
|
|
124
|
+
const kind = (c.reason as { kind?: string })?.kind
|
|
125
|
+
if (kind === 'error' || kind === 'aborted') {
|
|
126
|
+
const failure = (c.reason as { failure?: { code?: string; message?: string } }).failure
|
|
127
|
+
failNote = (failure?.code ?? kind) + ': ' + (failure?.message ?? '模型调用失败')
|
|
128
|
+
log.debug('[ai-inline] 流失败:' + failNote)
|
|
129
|
+
}
|
|
130
|
+
break
|
|
131
|
+
} else if (c.type === 'usage' || c.type === 'block-start' || c.type === 'block-end') {
|
|
132
|
+
// 汇聚无关块:跳过
|
|
133
|
+
} else if (c.type === 'tool-call-delta') {
|
|
134
|
+
// 工具块不进补全
|
|
135
|
+
}
|
|
136
|
+
if (aborted) break
|
|
137
|
+
}
|
|
138
|
+
if (failNote) return { text: '', note: 'AI 补全失败:' + failNote }
|
|
139
|
+
const clean = cleanInlineText(text, req.prefix)
|
|
140
|
+
if (!clean) {
|
|
141
|
+
// 思考型模型常见症状:正文空但思考有内容 → 明确提示换非思考模型(debug 实证 isd/deepseek-v4-flash)
|
|
142
|
+
if (text.length === 0 && reasoningChars > 0) {
|
|
143
|
+
log.debug('[ai-inline] 仅思考无正文(reasoning ' + reasoningChars + ' 字符)')
|
|
144
|
+
return { text: '', note: '该模型只返回了思考没有正文,请在设置页换非思考模型' }
|
|
145
|
+
}
|
|
146
|
+
log.debug('[ai-inline] 空输出(原始 ' + text.length + ' 字符)')
|
|
147
|
+
return { text: '' }
|
|
148
|
+
}
|
|
149
|
+
return { text: clean }
|
|
150
|
+
} catch (error) {
|
|
151
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
152
|
+
log.debug('[ai-inline] 失败:' + msg)
|
|
153
|
+
return { text: '', note: 'AI 补全失败:' + msg }
|
|
154
|
+
} finally {
|
|
155
|
+
clearTimeout(timer)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 模型目录视图(edrv.ai.models):DSH 已配置 provider 全量 + 逐模型思考档位。
|
|
161
|
+
* @author ddj
|
|
162
|
+
* @param ctx DSH 上下文
|
|
163
|
+
* @param force 跳过缓存强制刷新
|
|
164
|
+
* @returns 目录视图(llm 缺失时 note 说明 + 空目录)
|
|
165
|
+
*/
|
|
166
|
+
export async function aiModels(ctx: Ctx, force = false): Promise<AiDirectoryView> {
|
|
167
|
+
if (!force && dirCache && Date.now() - dirCache.at < DIR_TTL_MS) return dirCache.view
|
|
168
|
+
const llm = llmOf(ctx)
|
|
169
|
+
if (!llm) {
|
|
170
|
+
return { providers: [], note: 'llm 服务不可用(无法枚举模型)' }
|
|
171
|
+
}
|
|
172
|
+
const out: AiDirectoryView = { providers: [] }
|
|
173
|
+
for (const p of llm.listProviders()) {
|
|
174
|
+
const models = await llm.listModels(p.id).catch(() => [])
|
|
175
|
+
const entries = []
|
|
176
|
+
for (const m of models) {
|
|
177
|
+
let efforts: Array<{ id: string; name: string }> = []
|
|
178
|
+
let def = ''
|
|
179
|
+
try {
|
|
180
|
+
const info = await llm.resolveModelInfo(p.id, m.id)
|
|
181
|
+
efforts = (info.reasoning?.efforts ?? []).map((e: { id: string; name: string }) => ({ id: e.id, name: e.name }))
|
|
182
|
+
def = info.reasoning?.defaultEffort ?? ''
|
|
183
|
+
} catch {
|
|
184
|
+
// 单模型元数据失败:降级为无档位条目(不影响目录其余部分)
|
|
185
|
+
}
|
|
186
|
+
entries.push({ provider: p.id, model: m.id, name: m.name || m.id, efforts, defaultEffort: def })
|
|
187
|
+
}
|
|
188
|
+
out.providers.push({ id: p.id, name: p.name || p.id, models: entries })
|
|
189
|
+
}
|
|
190
|
+
dirCache = { view: out, at: Date.now() }
|
|
191
|
+
return out
|
|
192
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode host — @deepseek-ai/dsh-llm 精简类型(AI 内联补全消费面)。
|
|
3
|
+
* 以 profile 实装 0.1.3-alpha.2 d.ts 为准的最小子集;宿主环境经 external 解析,
|
|
4
|
+
* 这里只声明本插件用到的形状(运行时值来自 ctx.get('llm'),无直接 import)。
|
|
5
|
+
* 作者 ddj
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** 文本块(模型可见)。 */
|
|
9
|
+
export interface TextBlock {
|
|
10
|
+
type: 'text'
|
|
11
|
+
text: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** 消息内容块(本插件只产 text)。 */
|
|
15
|
+
export type ContentBlock = TextBlock
|
|
16
|
+
|
|
17
|
+
/** 消息来源:手工构造一次性请求只产 user。 */
|
|
18
|
+
export type MessageSource = { kind: 'user' }
|
|
19
|
+
|
|
20
|
+
/** 流式模型调用的一条消息。 */
|
|
21
|
+
export interface LlmMessage {
|
|
22
|
+
readonly id: string
|
|
23
|
+
readonly role: 'system' | 'user' | 'assistant'
|
|
24
|
+
readonly content: ContentBlock[]
|
|
25
|
+
readonly source: MessageSource
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 生成请求(reasoningEffort 为模型元数据里的档位 id)。 */
|
|
29
|
+
export interface GenerateOptions {
|
|
30
|
+
provider: string
|
|
31
|
+
model: string
|
|
32
|
+
reasoningEffort?: string
|
|
33
|
+
messages: readonly LlmMessage[]
|
|
34
|
+
system?: string
|
|
35
|
+
temperature?: number
|
|
36
|
+
maxTokens?: number
|
|
37
|
+
stop?: string[]
|
|
38
|
+
signal?: AbortSignal
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 流块:本插件消费 text-delta / finish(其余忽略)。 */
|
|
42
|
+
export type StreamChunk =
|
|
43
|
+
| { type: 'block-start'; index: number; blockType: string }
|
|
44
|
+
| { type: 'text-delta'; index: number; text: string }
|
|
45
|
+
| { type: 'reasoning-delta'; index: number; text: string }
|
|
46
|
+
| { type: 'tool-call-delta'; index: number; id: string; name?: string; argumentsDelta: string }
|
|
47
|
+
| { type: 'block-end'; index: number; block: unknown }
|
|
48
|
+
| { type: 'usage'; usage: unknown }
|
|
49
|
+
| { type: 'finish'; reason: { kind: string }; replayState?: unknown }
|
|
50
|
+
|
|
51
|
+
/** provider 展示元数据。 */
|
|
52
|
+
export interface LlmProviderInfo {
|
|
53
|
+
id: string
|
|
54
|
+
name: string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** 模型条目(目录级,未解档位)。 */
|
|
58
|
+
export interface LlmModelInfo {
|
|
59
|
+
provider: string
|
|
60
|
+
id: string
|
|
61
|
+
name: string
|
|
62
|
+
description?: string
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 可选思考档位表(模型元数据驱动)。 */
|
|
66
|
+
export interface LlmModelReasoningInfo {
|
|
67
|
+
efforts: readonly { id: string; name: string }[]
|
|
68
|
+
defaultEffort?: string
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** 精确模型元数据(含 reasoning)。 */
|
|
72
|
+
export interface LlmResolvedModelInfo extends LlmModelInfo {
|
|
73
|
+
context?: { contextWindow: number }
|
|
74
|
+
reasoning?: LlmModelReasoningInfo
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** llm 运行时(ctx.get('llm') 取得)的最小子集。 */
|
|
78
|
+
export interface LlmRuntimeLike {
|
|
79
|
+
listProviders(): LlmProviderInfo[]
|
|
80
|
+
listModels(provider: string): Promise<LlmModelInfo[]>
|
|
81
|
+
resolveModelInfo(provider: string, model: string, signal?: AbortSignal): Promise<LlmResolvedModelInfo>
|
|
82
|
+
stream(options: GenerateOptions): AsyncIterable<StreamChunk>
|
|
83
|
+
}
|
package/src/ai/rpc.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode host — edrv.ai.* RPC handlers。
|
|
3
|
+
* inline:AI 内联补全(一次调用一次结果,无状态);models:模型目录(缓存);
|
|
4
|
+
* configGet/configUpdate:settings section 的 AI 字段读写。
|
|
5
|
+
* 作者 ddj
|
|
6
|
+
*/
|
|
7
|
+
import type { RpcHandlerMap } from '../shared/rpc.js'
|
|
8
|
+
import type { FileOpenSettingsState } from '../fileOpenSettings.js'
|
|
9
|
+
import type { AiConfigView } from '../shared/ai.js'
|
|
10
|
+
import { aiInlineComplete, aiModels, type AiInlineDeps } from './inline.js'
|
|
11
|
+
|
|
12
|
+
/** AI RPC 依赖(settings 状态由 index.ts 装配时桥接)。 */
|
|
13
|
+
export interface AiRpcDeps {
|
|
14
|
+
ctx: AiInlineDeps['ctx']
|
|
15
|
+
settings: FileOpenSettingsState & {
|
|
16
|
+
ai: () => AiConfigView
|
|
17
|
+
aiUpdate: (patch: import('../shared/ai.js').AiConfigPatch, expectedRevision?: number) => Promise<AiConfigView>
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 构造 edrv.ai.* handlers(一次性创建,无跨请求状态——目录缓存在 inline.ts 模块级)。
|
|
23
|
+
* @author ddj
|
|
24
|
+
* @param deps 依赖(ctx/settings)
|
|
25
|
+
* @returns handlers
|
|
26
|
+
*/
|
|
27
|
+
export function createAiRpc(deps: AiRpcDeps): { handlers: Partial<RpcHandlerMap> } {
|
|
28
|
+
const handlers: Partial<RpcHandlerMap> = {
|
|
29
|
+
'edrv.ai.inline': async (args) => ({ ok: true, ...(await aiInlineComplete({ ctx: deps.ctx, config: { get: () => deps.settings.ai() } }, args)) }),
|
|
30
|
+
'edrv.ai.models': async (args) => ({ ok: true, ...(await aiModels(deps.ctx, args?.force === true)) }),
|
|
31
|
+
'edrv.ai.configGet': async () => ({ ok: true, ...deps.settings.ai() }),
|
|
32
|
+
'edrv.ai.configUpdate': async (args) => ({ ok: true, ...(await deps.settings.aiUpdate(args)) }),
|
|
33
|
+
}
|
|
34
|
+
return { handlers }
|
|
35
|
+
}
|
package/src/capture.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { fileMaxBatch, prune } from './model.js'
|
|
|
11
11
|
import type { Registry } from './registry.js'
|
|
12
12
|
import { cwdOf } from './registry.js'
|
|
13
13
|
import { invalidateIndex } from './treeIndex.js'
|
|
14
|
+
import { log } from './log.js'
|
|
14
15
|
|
|
15
16
|
/** 校验并提取工具 result meta 中的文件 hunk。 */
|
|
16
17
|
function validHunks(raw: unknown): Hunk[] {
|
|
@@ -101,6 +102,6 @@ export async function captureToolResult(ctx: Ctx, registry: Registry, exec: any,
|
|
|
101
102
|
// agent 写盘后目录树可能变化(新建/删除文件/目录):父目录+祖先进失效,后台自愈。
|
|
102
103
|
invalidateIndex(ctx, cwd, path)
|
|
103
104
|
} catch (error) {
|
|
104
|
-
|
|
105
|
+
log.error('capture failed: ' + String(error))
|
|
105
106
|
}
|
|
106
107
|
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* dsh-vscode-mode client — AI 内联补全(ghost text)provider。
|
|
4
|
+
* 速度设计:内部 150ms 去抖(Monaco 逐键调用 provider,不逐键打 LLM)、
|
|
5
|
+
* 在飞取消(新请求 abort 旧的)+ 序号守卫(过期响应丢弃)、
|
|
6
|
+
* 位置+前缀缓存(dismiss 后同位置重查不重复计费)、前后缀窗口裁剪。
|
|
7
|
+
* 状态经 edrv:ai-status 事件广播给编辑器底部状态栏;
|
|
8
|
+
* 差异审查期间静默(edrv 差异块存在时返回空,不与 Keep/Undo 浮层打架)。
|
|
9
|
+
* 作者 ddj
|
|
10
|
+
*/
|
|
11
|
+
import { rpc } from '../rpc.js'
|
|
12
|
+
import { RPC_PATH } from '../../shared/rpc.js'
|
|
13
|
+
import {
|
|
14
|
+
AI_PREFIX_MAX, AI_SUFFIX_MAX, inlineWorth, trimInlineWindow,
|
|
15
|
+
} from '../../shared/ai.js'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 广播补全状态给编辑器底部状态栏(EditorView 监听 edrv:ai-status 渲染)。
|
|
19
|
+
* @author ddj
|
|
20
|
+
* @param state busy=请求中 ok=完成(detail.ms 耗时) error=失败(detail.note) idle=就绪 config=开关变化
|
|
21
|
+
* @param detail 附加信息
|
|
22
|
+
*/
|
|
23
|
+
function emitAiStatus(state, detail) {
|
|
24
|
+
try { window.dispatchEvent(new CustomEvent('edrv:ai-status', { detail: { state, detail } })) } catch { /* 状态广播失败不影响补全 */ }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** 去抖等待(毫秒):Monaco 逐键调用 provider,静默该时长才真正发 RPC;
|
|
28
|
+
* 网关 TTFT 本身 3.5s+(debug 实证),client 侧去抖压到 150ms 不再叠加明显延迟。 */
|
|
29
|
+
const DEBOUNCE_MS = 150
|
|
30
|
+
/** 缓存容量上限(LRU 超限即整体清空;键空间小,够用)。 */
|
|
31
|
+
const CACHE_MAX = 30
|
|
32
|
+
/** 缓存 TTL(毫秒)。 */
|
|
33
|
+
const CACHE_TTL_MS = 60_000
|
|
34
|
+
|
|
35
|
+
let registered = false
|
|
36
|
+
let seq = 0
|
|
37
|
+
let inFlight = null
|
|
38
|
+
let debounceTimer = null
|
|
39
|
+
const cache = new Map()
|
|
40
|
+
|
|
41
|
+
/** 上次开关状态(configUpdate 后经事件刷新,关闭时立即静默)。 */
|
|
42
|
+
let enabled = false
|
|
43
|
+
|
|
44
|
+
/** 供设置面板保存后同步开关状态(不重载页面即时生效)。 */
|
|
45
|
+
export function setAiInlineEnabled(value) {
|
|
46
|
+
enabled = value === true
|
|
47
|
+
emitAiStatus('config', { enabled })
|
|
48
|
+
if (!enabled) {
|
|
49
|
+
seq += 1 // 作废在飞响应
|
|
50
|
+
cache.clear()
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 当前开关(EditorView 装配时决定是否注册)。 */
|
|
55
|
+
export function aiInlineEnabled() {
|
|
56
|
+
return enabled
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 初始化开关(启动时从 configGet 拉一次)。 */
|
|
60
|
+
export async function initAiInlineState() {
|
|
61
|
+
try {
|
|
62
|
+
const res = await rpc('edrv.ai.configGet', {})
|
|
63
|
+
setAiInlineEnabled(res?.ok && res.enabled === true)
|
|
64
|
+
} catch {
|
|
65
|
+
setAiInlineEnabled(false)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 缓存键:路径 + 行:列 + 前缀尾 24 字符(同位置不同文本区分)。
|
|
71
|
+
* @author ddj
|
|
72
|
+
*/
|
|
73
|
+
function cacheKey(path, line, column, prefix) {
|
|
74
|
+
return path + ':' + line + ':' + column + ':' + String(prefix || '').slice(-24)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 差异审查期间静默:编辑器上存在 AI 补全应避让的 pending 差异装饰时返回 true。
|
|
79
|
+
* 通过编辑器实例上的 edrv 差异装饰类名判定,缺失视为无差异。
|
|
80
|
+
* @author ddj
|
|
81
|
+
*/
|
|
82
|
+
function diffPending(editor) {
|
|
83
|
+
try {
|
|
84
|
+
const model = editor?.getModel?.()
|
|
85
|
+
const decos = model?.getAllDecorations?.() ?? []
|
|
86
|
+
return decos.some((d) => String(d?.options?.className || '').includes('edrv-mn-'))
|
|
87
|
+
} catch {
|
|
88
|
+
return false
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 注册 AI 内联补全 provider(幂等;Monaco 就绪后调用一次)。
|
|
94
|
+
* @author ddj
|
|
95
|
+
* @param monaco window.monaco
|
|
96
|
+
*/
|
|
97
|
+
export function registerAiInline(monaco) {
|
|
98
|
+
if (registered || !monaco?.languages?.registerInlineCompletionsProvider) return
|
|
99
|
+
registered = true
|
|
100
|
+
monaco.languages.registerInlineCompletionsProvider('*', {
|
|
101
|
+
async provideInlineCompletions(model, position, context, token) {
|
|
102
|
+
const t0 = Date.now()
|
|
103
|
+
if (!enabled || token?.isCancellationRequested) return { items: [] }
|
|
104
|
+
const path = decodeURIComponent(String(model?.uri?.path || '').replace(/^\//, ''))
|
|
105
|
+
if (!path || model.uri.scheme !== 'edrv') return { items: [] }
|
|
106
|
+
const editor = monacoRef?.activeEditor
|
|
107
|
+
if (editor && diffPending(editor)) return { items: [] }
|
|
108
|
+
const offset = model.getOffsetAt(position)
|
|
109
|
+
const value = model.getValue()
|
|
110
|
+
const trimmed = trimInlineWindow(value.slice(0, offset), value.slice(offset))
|
|
111
|
+
if (!inlineWorth(trimmed.prefix)) return { items: [] }
|
|
112
|
+
const key = cacheKey(path, position.lineNumber, position.column, trimmed.prefix)
|
|
113
|
+
const hit = cache.get(key)
|
|
114
|
+
if (hit && Date.now() - hit.at < CACHE_TTL_MS) {
|
|
115
|
+
return hit.items.length ? { items: hit.items } : { items: [] }
|
|
116
|
+
}
|
|
117
|
+
return new Promise((resolve) => {
|
|
118
|
+
// 去抖:登记最新意图,静默 DEBOUNCE_MS 后才发 RPC;期间新请求顶掉旧的
|
|
119
|
+
seq += 1
|
|
120
|
+
const mySeq = seq
|
|
121
|
+
if (inFlight) { try { inFlight.abort() } catch { /* 已结束 */ } inFlight = null }
|
|
122
|
+
if (debounceTimer) clearTimeout(debounceTimer)
|
|
123
|
+
debounceTimer = setTimeout(async () => {
|
|
124
|
+
debounceTimer = null
|
|
125
|
+
if (mySeq !== seq || token?.isCancellationRequested) { resolve({ items: [] }); return }
|
|
126
|
+
const controller = new AbortController()
|
|
127
|
+
inFlight = controller
|
|
128
|
+
emitAiStatus('busy')
|
|
129
|
+
try {
|
|
130
|
+
const res = await fetch(RPC_PATH, {
|
|
131
|
+
method: 'POST',
|
|
132
|
+
headers: { 'content-type': 'application/json' },
|
|
133
|
+
body: JSON.stringify({ method: 'edrv.ai.inline', args: { path, prefix: trimmed.prefix, suffix: trimmed.suffix } }),
|
|
134
|
+
signal: controller.signal,
|
|
135
|
+
})
|
|
136
|
+
const data = await res.json()
|
|
137
|
+
if (mySeq !== seq || !data?.ok || typeof data.text !== 'string' || !data.text) {
|
|
138
|
+
cache.set(key, { items: [], at: Date.now() })
|
|
139
|
+
if (data?.note) emitAiStatus('error', { note: String(data.note) })
|
|
140
|
+
else emitAiStatus('idle')
|
|
141
|
+
resolve({ items: [] })
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
const items = [{ insertText: data.text, range: undefined }]
|
|
145
|
+
cacheSet(key, items)
|
|
146
|
+
emitAiStatus('ok', { ms: Date.now() - t0 })
|
|
147
|
+
resolve({ items })
|
|
148
|
+
} catch (error) {
|
|
149
|
+
emitAiStatus('error', { note: String(error && error.message || error).slice(0, 80) })
|
|
150
|
+
resolve({ items: [] })
|
|
151
|
+
} finally {
|
|
152
|
+
if (inFlight === controller) inFlight = null
|
|
153
|
+
}
|
|
154
|
+
}, DEBOUNCE_MS)
|
|
155
|
+
// 提前取消:token 取消/新请求顶替时清掉待发定时器
|
|
156
|
+
const guard = setInterval(() => {
|
|
157
|
+
if (mySeq !== seq || token?.isCancellationRequested) {
|
|
158
|
+
clearInterval(guard)
|
|
159
|
+
if (debounceTimer) { clearTimeout(debounceTimer); debounceTimer = null }
|
|
160
|
+
resolve({ items: [] })
|
|
161
|
+
} else if (!debounceTimer) {
|
|
162
|
+
clearInterval(guard)
|
|
163
|
+
}
|
|
164
|
+
}, 120)
|
|
165
|
+
})
|
|
166
|
+
},
|
|
167
|
+
freeInlineCompletions() { /* 无资源需释放 */ },
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** 缓存写入(超限整体清空,LRU 简化)。 */
|
|
172
|
+
function cacheSet(key, items) {
|
|
173
|
+
if (cache.size >= CACHE_MAX) cache.clear()
|
|
174
|
+
cache.set(key, { items, at: Date.now() })
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** EditorView ref 注入口(差异静默判定用当前编辑器)。 */
|
|
178
|
+
let monacoRef = null
|
|
179
|
+
|
|
180
|
+
/** EditorView 装配后调用(注册 provider + 初始化开关)。 */
|
|
181
|
+
export function setupAiInline(monaco) {
|
|
182
|
+
monacoRef = monaco && { activeEditor: null }
|
|
183
|
+
if (!registered) registerAiInline(monaco)
|
|
184
|
+
void initAiInlineState()
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** EditorView 每次创建/切换编辑器实例时刷新引用。 */
|
|
188
|
+
export function trackAiEditor(editor) {
|
|
189
|
+
if (monacoRef) monacoRef.activeEditor = editor
|
|
190
|
+
}
|
package/src/client/compat.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* 纯逻辑与薄 ctx 胶水,node 环境可单测(不依赖 DOM)。
|
|
4
4
|
* 作者 ddj 2026年08月24号
|
|
5
5
|
*/
|
|
6
|
+
import { log } from './log.js'
|
|
6
7
|
|
|
7
8
|
/** 本插件包名(与 host compat.PLUGIN_NAME 对应)。 */
|
|
8
9
|
export const PLUGIN_NAME = 'dsh-vscode-mode'
|
|
@@ -59,14 +60,14 @@ export interface SlotSpec {
|
|
|
59
60
|
export function registerSlotSafely(ctx: { slots?: { inject?: (name: string, register: () => unknown) => unknown; register?: (...args: unknown[]) => unknown } }, spec: SlotSpec, render: (props: unknown) => unknown): (() => void) | null {
|
|
60
61
|
const slots = ctx?.slots as { inject: (name: string, register: () => unknown) => unknown; register: (...args: unknown[]) => unknown } | undefined
|
|
61
62
|
if (!slots || typeof slots.inject !== 'function' || typeof slots.register !== 'function') {
|
|
62
|
-
|
|
63
|
+
log.warn('slots 服务不可用,跳过 slot ' + spec.name)
|
|
63
64
|
return null
|
|
64
65
|
}
|
|
65
66
|
try {
|
|
66
67
|
const disposer = slots.inject(spec.name, () => slots.register(spec, render))
|
|
67
68
|
return typeof disposer === 'function' ? disposer as () => void : null
|
|
68
69
|
} catch (error) {
|
|
69
|
-
|
|
70
|
+
log.warn('slot ' + spec.name + ' 注册失败(' + String(error) + '),已跳过')
|
|
70
71
|
return null
|
|
71
72
|
}
|
|
72
73
|
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { EDRV_PARAM_KEYS, parseOpenParams } from '../shared/externalOpen.js'
|
|
9
9
|
import { openDeepLink } from './openFlow.js'
|
|
10
10
|
import { rpc } from './rpc.js'
|
|
11
|
+
import { log } from './log.js'
|
|
11
12
|
|
|
12
13
|
/** toast 自隐时长。 */
|
|
13
14
|
const TOAST_MS = 5000
|
|
@@ -38,12 +39,12 @@ export function setupExtOpen(ctx: unknown, options: ExtOpenOptions = {}): void {
|
|
|
38
39
|
const referrer = options.referrer ?? (typeof document !== 'undefined' ? document.referrer : '')
|
|
39
40
|
const origin = options.origin ?? (typeof location !== 'undefined' ? location.origin : '')
|
|
40
41
|
if (!referrerAllowed(referrer, origin)) {
|
|
41
|
-
|
|
42
|
+
log.warn('已忽略跨源深链请求(referrer=' + referrer + ')')
|
|
42
43
|
return
|
|
43
44
|
}
|
|
44
45
|
stripCurrentUrl()
|
|
45
46
|
void openDeepLink(ctx, params).catch((error) => {
|
|
46
|
-
|
|
47
|
+
log.warn('深链打开失败:' + String(error))
|
|
47
48
|
toastDom('深链打开失败:' + String((error as Error)?.message ?? error))
|
|
48
49
|
})
|
|
49
50
|
}
|
package/src/client/index.ts
CHANGED
|
@@ -43,6 +43,7 @@ import { createOutlineSourceRegistry, registerBuiltinOutlineSources } from './ou
|
|
|
43
43
|
import { createLspOutlineSource } from './outline/lspSource.js'
|
|
44
44
|
import { keybindingsApply } from './keybindings.js'
|
|
45
45
|
import { sidebarMinApply } from './sidebarMin.js'
|
|
46
|
+
import { log } from './log.js'
|
|
46
47
|
import { setupLsp, setSession } from './monaco/lsp/index.js'
|
|
47
48
|
import type { CompatAdapter } from '../shared/compat.js'
|
|
48
49
|
|
|
@@ -101,8 +102,8 @@ export function apply(ctx: any): void {
|
|
|
101
102
|
let selected = autoValue('auto')
|
|
102
103
|
/** 0.1.3+ 会话文件链接路由(remote.session.openWorkspacePath)是否已安装(compatSummary 展示用)。 */
|
|
103
104
|
let remoteOpenInstalled = false
|
|
104
|
-
/** 两条文件链接路由共用的路由日志(openPathRouter / remoteOpenRouter
|
|
105
|
-
const routeLogger = (message: string): void =>
|
|
105
|
+
/** 两条文件链接路由共用的路由日志(openPathRouter / remoteOpenRouter;统一走插件日志器)。 */
|
|
106
|
+
const routeLogger = (message: string): void => log.warn(message)
|
|
106
107
|
/** 两条路由共用的 FileOpenContext:当前会话 id 与工作区 cwd。 */
|
|
107
108
|
const openContext = (): FileOpenContext => {
|
|
108
109
|
const current = sessions?.list?.getSnapshot?.()
|
|
@@ -198,7 +199,7 @@ export function apply(ctx: any): void {
|
|
|
198
199
|
let remoteRetries = 0
|
|
199
200
|
const retryRemoteOpen = (): void => {
|
|
200
201
|
if (remoteOpenInstalled || remoteRetries >= 15) {
|
|
201
|
-
if (!remoteOpenInstalled)
|
|
202
|
+
if (!remoteOpenInstalled) log.warn('未探测到 remote.session.openWorkspacePath,0.1.3+ 会话文件链接路由未安装')
|
|
202
203
|
return
|
|
203
204
|
}
|
|
204
205
|
remoteRetries += 1
|
|
@@ -211,12 +212,12 @@ export function apply(ctx: any): void {
|
|
|
211
212
|
}
|
|
212
213
|
const disposer = patchRemoteOpen(service, { registry, selected: () => selected, context: openContext, logger: routeLogger })
|
|
213
214
|
if (!disposer) {
|
|
214
|
-
|
|
215
|
+
log.warn('remote.session 存在但 openWorkspacePath 不可补丁,会话文件链接路由未安装')
|
|
215
216
|
return
|
|
216
217
|
}
|
|
217
218
|
remoteOpenInstalled = true
|
|
218
219
|
ctx.effect(() => disposer, 'vscode-mode: remote file link routing')
|
|
219
|
-
|
|
220
|
+
log.info('已安装 0.1.3+ 会话文件链接路由(remote.session.openWorkspacePath)')
|
|
220
221
|
}, 2000)
|
|
221
222
|
}
|
|
222
223
|
retryRemoteOpen()
|
|
@@ -271,7 +272,7 @@ export function apply(ctx: any): void {
|
|
|
271
272
|
if (sideService !== undefined) return
|
|
272
273
|
sideService = detectSidebarService(ctx)
|
|
273
274
|
if (sideService !== undefined) {
|
|
274
|
-
|
|
275
|
+
log.info('检测到 ' + SIDEBAR_PLUGIN + ',切换侧边栏编辑形态')
|
|
275
276
|
applySideForm()
|
|
276
277
|
} else {
|
|
277
278
|
retrySideService()
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 日志单例(全 client 面唯一日志入口)。
|
|
3
|
+
* client 无 ctx.logger 服务,固定 console 出口;格式/级别语义与 host 面一致(shared/logger.ts)。
|
|
4
|
+
* 用法:`import { log } from './log.js'` → `log.debug/info/warn/error(msg)`;子域用 `log.child('scope')`。
|
|
5
|
+
* 作者 ddj 2026-09-08
|
|
6
|
+
*/
|
|
7
|
+
import { createLogger } from '../shared/logger.js'
|
|
8
|
+
|
|
9
|
+
/** client 面插件日志单例。 */
|
|
10
|
+
export const log = createLogger()
|