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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vscode-mode",
3
- "version": "0.1.57",
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",
@@ -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
- console.error('edrv capture failed', error)
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
+ }
@@ -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
- console.warn('[' + PLUGIN_NAME + '] slots 服务不可用,跳过 slot ' + spec.name)
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
- console.warn('[' + PLUGIN_NAME + '] slot ' + spec.name + ' 注册失败(' + String(error) + '),已跳过')
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
- console.warn('[dsh-vscode-mode] 已忽略跨源深链请求(referrer=' + referrer + ')')
42
+ log.warn('已忽略跨源深链请求(referrer=' + referrer + ')')
42
43
  return
43
44
  }
44
45
  stripCurrentUrl()
45
46
  void openDeepLink(ctx, params).catch((error) => {
46
- console.warn('[dsh-vscode-mode] 深链打开失败:' + String(error))
47
+ log.warn('深链打开失败:' + String(error))
47
48
  toastDom('深链打开失败:' + String((error as Error)?.message ?? error))
48
49
  })
49
50
  }
@@ -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 => console.warn('[dsh-vscode-mode] ' + message)
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) console.warn('[dsh-vscode-mode] 未探测到 remote.session.openWorkspacePath,0.1.3+ 会话文件链接路由未安装')
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
- console.warn('[dsh-vscode-mode] remote.session 存在但 openWorkspacePath 不可补丁,会话文件链接路由未安装')
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
- console.info('[dsh-vscode-mode] 已安装 0.1.3+ 会话文件链接路由(remote.session.openWorkspacePath)')
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
- console.info('[dsh-vscode-mode] 检测到 ' + SIDEBAR_PLUGIN + ',切换侧边栏编辑形态')
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()