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/src/index.ts
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* 职责:装配 capture(tools/result 捕获 edit/write 差异)、RPC 分发(edrv.* / mcp.* / vscode.* / compat)、
|
|
4
4
|
* webServer 路由(/edrv/rpc、/edrv/assets/*、/edrv/vendor/*)、工作区旁车持久化、
|
|
5
5
|
* 兼容层装配(依赖守卫 + 重复装配自诊断 + 启动日志兼容性报告)。
|
|
6
|
-
* 结构:shared/(双面契约) + model/store/capture/workspace/revert/registry/rpc/routes/compat(host
|
|
6
|
+
* 结构:shared/(双面契约) + model/store/capture/workspace/revert/registry/rpc/routes/compat(host 模块)
|
|
7
|
+
* + log/debugLog(统一日志:bindHostLog 绑定 ctx.logger,edrv.debug 诊断文件通道)。
|
|
7
8
|
* 作者 ddj 2026-08-20
|
|
8
9
|
*/
|
|
9
10
|
import { registerRoutes } from './routes.js'
|
|
@@ -18,10 +19,12 @@ import { setupOpenSettings } from './fileOpenSettings.js'
|
|
|
18
19
|
import { shellMenuLifecycle } from './integrate.js'
|
|
19
20
|
import { disposeIndex } from './treeIndex.js'
|
|
20
21
|
import { sweepTreeCache } from './paths.js'
|
|
21
|
-
import {
|
|
22
|
+
import { buildReport } from './compat.js'
|
|
23
|
+
import { bindHostLog, log } from './log.js'
|
|
22
24
|
import { createLspManager } from './lsp/manager.js'
|
|
23
25
|
import { createLspRpc } from './lsp/rpc.js'
|
|
24
26
|
import { disposeAllServers } from './lsp/transport.js'
|
|
27
|
+
import { createAiRpc } from './ai/rpc.js'
|
|
25
28
|
import { installRulesSection } from './rules.js'
|
|
26
29
|
import type { RpcHandlerMap } from './shared/rpc.js'
|
|
27
30
|
import type { Registry } from './registry.js'
|
|
@@ -37,21 +40,27 @@ export const inject = ['sessions', 'fs', 'webServer', 'loader', 'tools', 'worksp
|
|
|
37
40
|
* @param config 插件配置(可选 imageDir 覆盖图标目录)
|
|
38
41
|
*/
|
|
39
42
|
export function apply(ctx: Ctx, config?: unknown): void {
|
|
43
|
+
// 统一日志出口:全部 host 日志经 ctx.logger(缺失时回退 console),此后任何模块 log.* 即生效
|
|
44
|
+
bindHostLog(ctx)
|
|
40
45
|
const registry: Registry = new Map()
|
|
41
46
|
const searcher = newSearcher(ctx)
|
|
42
47
|
const contentSearcher = newContentSearcher(ctx)
|
|
43
|
-
/** LSP
|
|
44
|
-
const lspManager = createLspManager((line) =>
|
|
48
|
+
/** LSP 服务器管理器(语言智能:跳转/引用/大纲;诊断行带模块内前缀如 [lsp-manager])。 */
|
|
49
|
+
const lspManager = createLspManager((line) => log.debug(line))
|
|
45
50
|
/** 兼容性警告收集(route 护栏等写入,启动日志一并输出)。 */
|
|
46
51
|
const warnings: string[] = []
|
|
47
|
-
|
|
52
|
+
/** 设置状态(fileOpenTool + AI 补全配置读写;settings 不可用时内存态降级)。 */
|
|
53
|
+
const openSettings = setupOpenSettings(ctx, config, () => {})
|
|
48
54
|
/** LSP 配置为配置值模式:插件组合配置 + 会话内运行时覆盖(原 settings section 命名空间不合法,见 lsp/config.ts)。 */
|
|
49
55
|
/** 规则注入 section(~/.dsh/rules 与 <工作区>/.dsh/rules;旧版 DSH 无 systemPrompt 时静默降级)。 */
|
|
50
56
|
const rulesInstalled = installRulesSection(ctx)
|
|
51
|
-
if (!rulesInstalled)
|
|
57
|
+
if (!rulesInstalled) log.warn('未检测到 systemPrompt 服务,规则仅可管理不注入')
|
|
52
58
|
/** LSP RPC 与会话清理(一次性创建,tracker 状态跨请求保留)。 */
|
|
53
59
|
const lspRpc = createLspRpc({ ctx, pluginConfig: config, manager: lspManager })
|
|
54
60
|
const lspHandlers: Partial<RpcHandlerMap> = lspRpc.handlers
|
|
61
|
+
/** AI 内联补全 RPC(settings 状态桥接 + llm 惰性获取)。 */
|
|
62
|
+
const aiRpc = createAiRpc({ ctx, settings: openSettings })
|
|
63
|
+
const aiHandlers: Partial<RpcHandlerMap> = aiRpc.handlers
|
|
55
64
|
|
|
56
65
|
ctx.on('tools/result', (exec: unknown, result: unknown) => {
|
|
57
66
|
void captureToolResult(ctx, registry, exec, result)
|
|
@@ -70,7 +79,7 @@ export function apply(ctx: Ctx, config?: unknown): void {
|
|
|
70
79
|
if (typeof sid === 'string') lspRpc.disposeSession(sid)
|
|
71
80
|
})
|
|
72
81
|
|
|
73
|
-
registerRoutes(ctx, config, (method, args) => handleRpc(ctx, registry, method, args, searcher, contentSearcher, lspHandlers), (warning) => warnings.push(warning))
|
|
82
|
+
registerRoutes(ctx, config, (method, args) => handleRpc(ctx, registry, method, args, searcher, contentSearcher, lspHandlers, aiHandlers), (warning) => warnings.push(warning))
|
|
74
83
|
installIsolation(ctx)
|
|
75
84
|
// 系统集成生命周期:启动自动恢复右键菜单注册(marker 存在时);插件卸载/reload 清理注册痕迹
|
|
76
85
|
ctx.effect(() => shellMenuLifecycle(ctx))
|
|
@@ -83,7 +92,7 @@ export function apply(ctx: Ctx, config?: unknown): void {
|
|
|
83
92
|
disposeAllServers()
|
|
84
93
|
})
|
|
85
94
|
|
|
86
|
-
|
|
95
|
+
log.info('编辑差异审查已装配(/edrv/rpc 路由就绪,项目 MCP 隔离已启用,语言服务器 LSP 已接入,规则注入' + (rulesInstalled ? '已接入' : '未接入') + ')')
|
|
87
96
|
}
|
|
88
97
|
|
|
89
98
|
/**
|
|
@@ -96,16 +105,14 @@ async function logCompatSummary(ctx: Ctx, warnings: string[]): Promise<void> {
|
|
|
96
105
|
try {
|
|
97
106
|
const report = await buildReport(ctx)
|
|
98
107
|
for (const warning of warnings) report.warnings.push(warning)
|
|
99
|
-
const logger = ctx.logger
|
|
100
|
-
if (!logger?.info) return
|
|
101
108
|
const dsh = report.dshVersion ? 'DSH ' + report.dshVersion : 'DSH 未探测'
|
|
102
|
-
const head = '
|
|
109
|
+
const head = '兼容性:' + dsh + ' · ' + report.external.length + ' 项外部适配 / ' + report.guards.length + ' 项护栏 / ' + (report.adapters?.length ?? 0) + ' 项版本适配'
|
|
103
110
|
if (report.warnings.length) {
|
|
104
|
-
|
|
111
|
+
log.warn(head + ',警告 ' + report.warnings.length + ' 条:' + report.warnings.join(';'))
|
|
105
112
|
} else {
|
|
106
|
-
|
|
113
|
+
log.info(head + ',无警告')
|
|
107
114
|
}
|
|
108
115
|
} catch (error) {
|
|
109
|
-
|
|
116
|
+
log.warn('兼容性报告生成失败:' + String(error))
|
|
110
117
|
}
|
|
111
118
|
}
|
package/src/log.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode host — 日志单例(全 host 面唯一日志入口)。
|
|
3
|
+
* 默认 console 出口;apply() 首行调 bindHostLog(ctx) 切到 ctx.logger(缺失回退 console)。
|
|
4
|
+
* 用法:`import { log } from './log.js'` → `log.debug/info/warn/error(msg)`;子域用 `log.child('scope')`。
|
|
5
|
+
* 作者 ddj 2026-09-08
|
|
6
|
+
*/
|
|
7
|
+
import { createLogger, ctxLogSink } from './shared/logger.js'
|
|
8
|
+
|
|
9
|
+
/** host 面插件日志单例(格式/级别语义见 shared/logger.ts 头注释)。 */
|
|
10
|
+
export const log = createLogger()
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 装配期绑定 host 日志出口:ctx.logger 可用时全部日志转经 DSH 日志系统。
|
|
14
|
+
* @author ddj 2026年09月08号
|
|
15
|
+
* @param ctx DSH host 上下文(logger 服务缺失时保持 console 出口)
|
|
16
|
+
*/
|
|
17
|
+
export function bindHostLog(ctx: unknown): void {
|
|
18
|
+
log.bind(ctxLogSink(ctx))
|
|
19
|
+
}
|
package/src/mcpIsolation.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { LEGACY_PROJECT_PREFIX, entryHash, isProjectEntryId } from './compat.js'
|
|
|
8
8
|
import { entriesOf } from './mcp.js'
|
|
9
9
|
import { hashWorkspace } from './mcpProject.js'
|
|
10
10
|
import type { Ctx } from './store.js'
|
|
11
|
+
import { log } from './log.js'
|
|
11
12
|
|
|
12
13
|
const UNKNOWN_PROJECT = '__unknown_project__'
|
|
13
14
|
|
|
@@ -83,7 +84,7 @@ function syncAgent(ctx: Ctx, state: AgentState, agent: any, tools: ToolProjects)
|
|
|
83
84
|
try {
|
|
84
85
|
state.dispose = agent.ctx.tools.restrict({ deny: denied })
|
|
85
86
|
} catch (error) {
|
|
86
|
-
|
|
87
|
+
log.warn('MCP agent restriction failed: ' + String(error))
|
|
87
88
|
return
|
|
88
89
|
}
|
|
89
90
|
}
|
package/src/rpc.ts
CHANGED
|
@@ -43,84 +43,16 @@ import { findProfileDir, readDevForm, setDevForm } from './devForm.js'
|
|
|
43
43
|
import { normalizeRel } from './tree.js'
|
|
44
44
|
import { invalidateIndex, listDirCached } from './treeIndex.js'
|
|
45
45
|
import { revealInExplorer } from './reveal.js'
|
|
46
|
-
import {
|
|
46
|
+
import { dshHome } from './paths.js'
|
|
47
|
+
import { debugRecord } from './debugLog.js'
|
|
47
48
|
import { markActiveSessions, moveOutSessions, planMoveOut, purgeArchive, restoreSession, scanSessionInventory, sessionsArchiveRoot, sessionSizeOf, sidecarSummaryOf } from './perf.js'
|
|
48
49
|
import { patchHasPerfConfig, patchInsertPerfConfig, patchRemovePerfConfig, perfConfigBlock } from './perfPatch.js'
|
|
49
50
|
|
|
50
|
-
/** cwd → 内存缓冲:行数组 + 累计字节数 + 待触发 flush 定时器(攒批落盘,避免每条日志全文件读改写)。 */
|
|
51
|
-
const debugBuffers = new Map<string, { lines: string[]; len: number; timer: ReturnType<typeof setTimeout> | null }>()
|
|
52
|
-
/** cwd → Promise 链:串行化 debug 日志落盘(fs read+write 非原子,避免并发丢行)。 */
|
|
53
|
-
const debugWriteQueues = new Map<string, Promise<void>>()
|
|
54
|
-
/** debug 日志单文件上限:超限截断保留尾部,防文件无限增长拖慢每次追加。 */
|
|
55
|
-
const DEBUG_LOG_CAP = 512 * 1024
|
|
56
|
-
/** debug 日志批量缓冲上限:攒满即落盘(一次读改写),上限内不逐条写文件。 */
|
|
57
|
-
const DEBUG_BUF_CAP = 32 * 1024
|
|
58
|
-
/** debug 日志空闲 flush 延迟:缓冲未满时,静默一段时间后落盘一次。 */
|
|
59
|
-
const DEBUG_FLUSH_IDLE_MS = 1000
|
|
60
51
|
/** cwd → 上次 stale 自动清理时间:全量轮询(EditorView/DiffBadge/Dock 各自 5s)节流,避免每轮都读文件算指纹。 */
|
|
61
52
|
const staleCheckedAt = new Map<string, number>()
|
|
62
53
|
/** stale 自动清理最小间隔。 */
|
|
63
54
|
const STALE_CHECK_MIN_MS = 10_000
|
|
64
55
|
|
|
65
|
-
/**
|
|
66
|
-
* 调试日志入队:先攒内存缓冲(行 + 字节数),满 DEBUG_BUF_CAP 立即落盘,
|
|
67
|
-
* 否则空闲 DEBUG_FLUSH_IDLE_MS 后落盘;落盘 = 读旧文件 + 追加整批 + 超限截断 + 一次写入。
|
|
68
|
-
* @author ddj 2026年08月26号 / 2026年09月01号
|
|
69
|
-
* @param ctx DSH 上下文
|
|
70
|
-
* @param cwd 工作区(日志按 cwd hash 存 ~/.dsh/dsh-vscode-mode/logs/)
|
|
71
|
-
* @param line 单条日志文本(不含换行)
|
|
72
|
-
*/
|
|
73
|
-
function enqueueDebug(ctx: Ctx, cwd: string, line: string): void {
|
|
74
|
-
const st = debugBuffers.get(cwd) ?? { lines: [], len: 0, timer: null }
|
|
75
|
-
st.lines.push(line)
|
|
76
|
-
st.len += line.length
|
|
77
|
-
const flush = () => {
|
|
78
|
-
st.timer = null
|
|
79
|
-
const batch = st.lines
|
|
80
|
-
st.lines = []
|
|
81
|
-
st.len = 0
|
|
82
|
-
void flushDebug(cwd, batch)
|
|
83
|
-
}
|
|
84
|
-
if (st.len >= DEBUG_BUF_CAP) {
|
|
85
|
-
if (st.timer) clearTimeout(st.timer)
|
|
86
|
-
flush()
|
|
87
|
-
} else if (!st.timer) {
|
|
88
|
-
st.timer = setTimeout(flush, DEBUG_FLUSH_IDLE_MS)
|
|
89
|
-
}
|
|
90
|
-
debugBuffers.set(cwd, st)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** cwd → 已清理旧工作区 debug 日志标记(一次性)。 */
|
|
94
|
-
const debugLegacyCleaned = new Set<string>()
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* 批量落盘一条 debug 日志缓冲:读旧文件 → 追加 → 超上限截断保留尾部 → 一次写回。
|
|
98
|
-
* 串行链保证同一 cwd 的读改写不交错丢行;写失败静默忽略(调试日志不阻塞业务)。
|
|
99
|
-
* @author ddj 2026年08月26号 / 2026年09月01号
|
|
100
|
-
* @param cwd 工作区(日志落 ~/.dsh/dsh-vscode-mode/logs/debug.<cwdHash>.log)
|
|
101
|
-
* @param batch 本批日志行
|
|
102
|
-
*/
|
|
103
|
-
function flushDebug(cwd: string, batch: string[]): Promise<void> {
|
|
104
|
-
const prev = debugWriteQueues.get(cwd) ?? Promise.resolve()
|
|
105
|
-
const task = prev.then(async () => {
|
|
106
|
-
try {
|
|
107
|
-
await mkdir(pluginLogRoot(), { recursive: true })
|
|
108
|
-
const target = debugLogFile(cwd)
|
|
109
|
-
const old = await readFile(target, 'utf8').catch(() => '')
|
|
110
|
-
const appended = old + batch.map((l) => new Date().toISOString() + ' ' + l + '\n').join('')
|
|
111
|
-
const next = appended.length > DEBUG_LOG_CAP ? appended.slice(-Math.floor(DEBUG_LOG_CAP / 2)) : appended
|
|
112
|
-
await writeFile(target, next, 'utf8')
|
|
113
|
-
// 迁移后一次性清理旧工作区 debug 日志(best-effort)
|
|
114
|
-
if (!debugLegacyCleaned.has(cwd)) {
|
|
115
|
-
debugLegacyCleaned.add(cwd)
|
|
116
|
-
await rm(join(cwd, DEBUG_LOG), { force: true }).catch(() => {})
|
|
117
|
-
}
|
|
118
|
-
} catch (e) { /* 写日志失败忽略 */ }
|
|
119
|
-
})
|
|
120
|
-
debugWriteQueues.set(cwd, task)
|
|
121
|
-
return task
|
|
122
|
-
}
|
|
123
|
-
|
|
124
56
|
/** 记录 → 客户端视图(不含 before 全文,仅长度)。 */
|
|
125
57
|
function recView(record: DiffRecord): RecordView {
|
|
126
58
|
return {
|
|
@@ -268,10 +200,13 @@ export function buildHandlers(
|
|
|
268
200
|
searcher = newSearcher(ctx),
|
|
269
201
|
contentSearcher = newContentSearcher(ctx),
|
|
270
202
|
lspHandlers?: Partial<RpcHandlerMap>,
|
|
203
|
+
aiHandlers?: Partial<RpcHandlerMap>,
|
|
271
204
|
): RpcHandlerMap {
|
|
272
205
|
return {
|
|
273
206
|
// edrv.lsp.* 由 createLspRpc 一次性提供(tracker 跨请求保留),这里并入。
|
|
274
207
|
...((lspHandlers ?? {}) as RpcHandlerMap),
|
|
208
|
+
// edrv.ai.* 由 createAiRpc 提供(AI 内联补全/模型目录/配置读写),这里并入。
|
|
209
|
+
...((aiHandlers ?? {}) as RpcHandlerMap),
|
|
275
210
|
'edrv.list': async (args) => {
|
|
276
211
|
const sc = await requireSession(ctx, args.sessionId)
|
|
277
212
|
if ('err' in sc) return { ok: false, error: sc.err }
|
|
@@ -478,13 +413,11 @@ export function buildHandlers(
|
|
|
478
413
|
return { ok: true, path: args.path, batch: batch ?? null }
|
|
479
414
|
},
|
|
480
415
|
'edrv.debug': async (args) => {
|
|
481
|
-
// 诊断日志:client 上报 →
|
|
482
|
-
//
|
|
416
|
+
// 诊断日志:client 上报 → debugLog 模块缓冲批量落盘 ~/.dsh/dsh-vscode-mode/logs/
|
|
417
|
+
// (console 不一定落盘,文件可靠)。只出现在调试开关开启时(client dbg 默认关)。
|
|
483
418
|
const sc = await requireSession(ctx, args.sessionId)
|
|
484
419
|
if ('err' in sc) return { ok: false, error: sc.err }
|
|
485
|
-
|
|
486
|
-
enqueueDebug(ctx, sc.cwd, text)
|
|
487
|
-
console.error('[edrv-debug] ' + text)
|
|
420
|
+
debugRecord(ctx, sc.cwd, String(args.text ?? ''))
|
|
488
421
|
return { ok: true }
|
|
489
422
|
},
|
|
490
423
|
'edrv.searchFiles': async (args) => {
|
|
@@ -852,8 +785,9 @@ export async function handleRpc<M extends RpcMethod>(
|
|
|
852
785
|
searcher = newSearcher(ctx),
|
|
853
786
|
contentSearcher = newContentSearcher(ctx),
|
|
854
787
|
lspHandlers?: Partial<RpcHandlerMap>,
|
|
788
|
+
aiHandlers?: Partial<RpcHandlerMap>,
|
|
855
789
|
): Promise<RpcResult<M>> {
|
|
856
|
-
const handlers = buildHandlers(ctx, registry, searcher, contentSearcher, lspHandlers)
|
|
790
|
+
const handlers = buildHandlers(ctx, registry, searcher, contentSearcher, lspHandlers, aiHandlers)
|
|
857
791
|
const handler = handlers[method]
|
|
858
792
|
if (!handler) return { ok: false, error: '未知方法: ' + String(method) } as RpcResult<M>
|
|
859
793
|
return handler(args)
|
package/src/shared/ai.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode shared — AI 内联补全契约与纯函数(双面,禁 node/react)。
|
|
3
|
+
* 只放"过 RPC 的载荷形状"与无副作用变换:窗口裁剪、prompt 拼装、输出清理。
|
|
4
|
+
* host 与语言模型之间的流式交互在 host 侧 src/ai/inline.ts。
|
|
5
|
+
* 作者 ddj
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** 上下文窗口:前缀字符上限(含未保存编辑的当前文档)。 */
|
|
9
|
+
export const AI_PREFIX_MAX = 2000
|
|
10
|
+
/** 上下文窗口:后缀字符上限(光标之后不参与补全语义的部分)。 */
|
|
11
|
+
export const AI_SUFFIX_MAX = 500
|
|
12
|
+
/** 触发门槛:前缀至少 4 个字符才请求(省掉空转调用)。 */
|
|
13
|
+
export const AI_MIN_PREFIX = 4
|
|
14
|
+
/** 单次补全输出上限(token 预算 + 行数双约束,早停兜底)。 */
|
|
15
|
+
export const AI_MAX_TOKENS = 120
|
|
16
|
+
/** 输出行数上限:超过即截断(ghost text 太长不可用)。 */
|
|
17
|
+
export const AI_MAX_LINES = 3
|
|
18
|
+
/** 流式早停:已收字符数达到该值即 abort(省尾部延迟)。 */
|
|
19
|
+
export const AI_EARLY_STOP_CHARS = 300
|
|
20
|
+
/** 补全请求超时(毫秒):codebuddy CLI 网关实测 TTFT 3.5~8.4s 抖动(debug 实证),
|
|
21
|
+
* 12s 内无任何 token 即放弃;token 一到仍按行数/字符早停。 */
|
|
22
|
+
export const AI_TIMEOUT_MS = 12000
|
|
23
|
+
|
|
24
|
+
/** client → host 的补全请求载荷。 */
|
|
25
|
+
export interface AiInlineRequest {
|
|
26
|
+
/** 当前文档工作区相对路径(语言提示用)。 */
|
|
27
|
+
path: string
|
|
28
|
+
/** 光标前文本(client 已裁剪到 AI_PREFIX_MAX)。 */
|
|
29
|
+
prefix: string
|
|
30
|
+
/** 光标后文本(client 已裁剪到 AI_SUFFIX_MAX)。 */
|
|
31
|
+
suffix: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** host → client 的补全结果(text 为空 = 无建议)。 */
|
|
35
|
+
export interface AiInlineResult {
|
|
36
|
+
/** 建议插入文本(已清理:剥围栏/截行数/去重复)。 */
|
|
37
|
+
text: string
|
|
38
|
+
/** 失败原因(ok:false 或 text 为空时的诊断文案,供状态栏展示)。 */
|
|
39
|
+
note?: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 模型目录条目(含该模型的思考档位元数据)。 */
|
|
43
|
+
export interface AiModelEntry {
|
|
44
|
+
provider: string
|
|
45
|
+
model: string
|
|
46
|
+
name: string
|
|
47
|
+
/** 无 reasoning 元数据 = 空数组(UI 隐藏档位选择器)。 */
|
|
48
|
+
efforts: Array<{ id: string; name: string }>
|
|
49
|
+
/** 模型默认档位 id(无则空)。 */
|
|
50
|
+
defaultEffort: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 模型目录视图(edrv.ai.models 载荷)。 */
|
|
54
|
+
export interface AiDirectoryView {
|
|
55
|
+
providers: Array<{ id: string; name: string; models: AiModelEntry[] }>
|
|
56
|
+
/** 目录不可用原因(llm 服务缺失等;ok:false 时亦有 error,此为软降级说明)。 */
|
|
57
|
+
note?: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** AI 补全配置视图(edrv.ai.configGet 载荷)。 */
|
|
61
|
+
export interface AiConfigView {
|
|
62
|
+
enabled: boolean
|
|
63
|
+
/** 空 = 自动选首个 provider/model。 */
|
|
64
|
+
provider: string
|
|
65
|
+
/** 空 = 自动选该 provider 首个模型。 */
|
|
66
|
+
model: string
|
|
67
|
+
/** 空 = 跟随模型默认(请求不携带 reasoningEffort)。 */
|
|
68
|
+
effort: string
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** AI 补全配置更新载荷(edrv.ai.configUpdate)。 */
|
|
72
|
+
export interface AiConfigPatch {
|
|
73
|
+
enabled?: boolean
|
|
74
|
+
provider?: string
|
|
75
|
+
model?: string
|
|
76
|
+
/** 空 = 清除(跟随默认)。 */
|
|
77
|
+
effort?: string
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 按 AI_PREFIX_MAX/AI_SUFFIX_MAX 裁剪前后缀(client 侧发请求前调用)。
|
|
82
|
+
* @author ddj
|
|
83
|
+
* @param prefix 光标前全文
|
|
84
|
+
* @param suffix 光标后全文
|
|
85
|
+
* @returns 裁剪后的窗口
|
|
86
|
+
*/
|
|
87
|
+
export function trimInlineWindow(prefix: string, suffix: string): { prefix: string; suffix: string } {
|
|
88
|
+
const pre = typeof prefix === 'string' ? prefix : ''
|
|
89
|
+
const suf = typeof suffix === 'string' ? suffix : ''
|
|
90
|
+
return {
|
|
91
|
+
prefix: pre.length > AI_PREFIX_MAX ? pre.slice(pre.length - AI_PREFIX_MAX) : pre,
|
|
92
|
+
suffix: suf.length > AI_SUFFIX_MAX ? suf.slice(0, AI_SUFFIX_MAX) : suf,
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 是否值得发起补全请求:前缀达标且非纯空白(空行/行首不请求)。
|
|
98
|
+
* @author ddj
|
|
99
|
+
* @param prefix 光标前文本
|
|
100
|
+
* @returns 是否可请求
|
|
101
|
+
*/
|
|
102
|
+
export function inlineWorth(prefix: string): boolean {
|
|
103
|
+
const pre = typeof prefix === 'string' ? prefix : ''
|
|
104
|
+
if (pre.length < AI_MIN_PREFIX) return false
|
|
105
|
+
// 只拦"整体无内容"(纯空白);结尾空白(如 `local a = `)是最自然的补全触发
|
|
106
|
+
// 形态,必须放行——原尾字符守卫把这类场景全数拦截(debug 实证 H4)
|
|
107
|
+
return pre.trim().length > 0
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 拼装补全 user 消息文本:语言/文件名 + 前缀 + 补全点标记 + 后缀。
|
|
112
|
+
* host 侧与单测共用,保证请求形状一致。
|
|
113
|
+
* @author ddj
|
|
114
|
+
* @param req 补全请求(已裁剪)
|
|
115
|
+
* @returns 用户消息文本
|
|
116
|
+
*/
|
|
117
|
+
export function buildInlinePrompt(req: AiInlineRequest): string {
|
|
118
|
+
const name = String(req.path || '').split(/[\\/]/).pop() || 'untitled'
|
|
119
|
+
return [
|
|
120
|
+
'文件:' + name,
|
|
121
|
+
'现有代码(光标位于 <CURSOR> 标记处):',
|
|
122
|
+
req.prefix + '<CURSOR>' + req.suffix,
|
|
123
|
+
].join('\n')
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** 补全 system 提示词:硬约束输出形态(只给插入文本)。 */
|
|
127
|
+
export const AI_INLINE_SYSTEM =
|
|
128
|
+
'你是代码自动补全引擎。只输出应插入 <CURSOR> 处的代码文本:' +
|
|
129
|
+
'禁止任何解释、注释性语句、markdown 代码围栏或对原文的复述;' +
|
|
130
|
+
'最多 ' + AI_MAX_LINES + ' 行,从光标处自然衔接前文,不要重复前缀已有内容;' +
|
|
131
|
+
'无法给出高置信度补全时输出空文本。'
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 清理模型输出:剥 markdown 围栏、剥对 <CURSOR> 标记的复述、按行数截断、去首尾空行。
|
|
135
|
+
* 产出空文本的判定也在这里(纯函数,单测覆盖)。
|
|
136
|
+
* @author ddj
|
|
137
|
+
* @param raw 模型原始输出
|
|
138
|
+
* @param prefix 请求时的前缀(复述检测)
|
|
139
|
+
* @returns 清理后的插入文本(可能为空串)
|
|
140
|
+
*/
|
|
141
|
+
export function cleanInlineText(raw: string, prefix: string): string {
|
|
142
|
+
let text = typeof raw === 'string' ? raw : ''
|
|
143
|
+
if (!text) return ''
|
|
144
|
+
// markdown 围栏剥离(```lang … ``` 或 ```…```)
|
|
145
|
+
const fenced = /^```[^\n]*\n([\s\S]*?)\n?```$/.exec(text.trim())
|
|
146
|
+
if (fenced) text = fenced[1] ?? ''
|
|
147
|
+
// 模型复述了标记本身:取标记之后的部分
|
|
148
|
+
const cursorAt = text.indexOf('<CURSOR>')
|
|
149
|
+
if (cursorAt >= 0) text = text.slice(cursorAt + '<CURSOR>'.length)
|
|
150
|
+
// 整体就是前缀结尾片段的复述:剥掉(防"从行首重抄一遍"类退化输出)
|
|
151
|
+
const tailPrefix = prefix.length > 0 ? prefix.slice(-40) : ''
|
|
152
|
+
if (tailPrefix && text.startsWith(tailPrefix)) text = text.slice(tailPrefix.length)
|
|
153
|
+
// 行数截断 + 行尾空白清理
|
|
154
|
+
const lines = text.split('\n').slice(0, AI_MAX_LINES).map((l) => l.replace(/\s+$/, ''))
|
|
155
|
+
while (lines.length > 0 && lines[0] === '') lines.shift()
|
|
156
|
+
while (lines.length > 0 && lines[lines.length - 1] === '') lines.pop()
|
|
157
|
+
return lines.join('\n')
|
|
158
|
+
}
|
package/src/shared/diff.ts
CHANGED
|
@@ -53,6 +53,24 @@ export function isNoopHunk(hunk: Hunk | null | undefined): boolean {
|
|
|
53
53
|
return !!hunk && hunk.oldText !== null && hunk.oldText === hunk.newText
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* 差异比较口径归一化:剥离 UTF-8 BOM、统一 CRLF 为 LF。
|
|
58
|
+
* 背景:外部工具(pwsh/编辑器)可能改变 BOM/行尾,导致 after 指纹与 hunk
|
|
59
|
+
* 定位在内容语义未变的情况下误报 conflict,把待确认修改从 UI 排除。
|
|
60
|
+
* @author ddj 2026年09月09号
|
|
61
|
+
*/
|
|
62
|
+
export function normalizeForCompare(text: string): string {
|
|
63
|
+
return text.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 归一化一个 hunk 的两侧文本(与 normalizeForCompare 同口径)。 */
|
|
67
|
+
export function normalizeHunk(hunk: Hunk): Hunk {
|
|
68
|
+
return {
|
|
69
|
+
oldText: hunk.oldText === null ? null : normalizeForCompare(hunk.oldText),
|
|
70
|
+
newText: normalizeForCompare(hunk.newText),
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
56
74
|
/** splitLines 的空文本语义:真实空文件没有一行变更内容。 */
|
|
57
75
|
export function splitLines(text: string): string[] {
|
|
58
76
|
return text.length ? text.split('\n') : []
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode — 统一日志核心(双面共享,平台中立)。
|
|
3
|
+
* 输出规范:
|
|
4
|
+
* - 格式:`[dsh-vscode-mode][:scope] 消息`;前缀只在模块内部拼接,业务代码禁止手写前缀
|
|
5
|
+
* - 级别语义:debug=诊断明细 / info=装配与路由里程碑 / warn=降级与兼容回退 / error=失败
|
|
6
|
+
* - 出口:host 面经 bindHostLog(ctx) 绑定 ctx.logger(缺失回退 console);client 面固定 console
|
|
7
|
+
* - 诊断文件通道(edrv.debug → ~/.dsh/dsh-vscode-mode/logs/)见 debugLog.ts,与本模块前缀共用
|
|
8
|
+
* 新增日志一律 `import { log } from '../log.js'`(client 为 './log.js')后调 log.debug/info/warn/error;
|
|
9
|
+
* 子域日志用 log.child('scope'),禁止另起 console。
|
|
10
|
+
* 作者 ddj 2026-09-08
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** 插件统一日志前缀(与包安装名一致)。 */
|
|
14
|
+
export const LOG_PREFIX = '[dsh-vscode-mode]'
|
|
15
|
+
|
|
16
|
+
/** 日志级别(与 console 方法名对齐)。 */
|
|
17
|
+
export type LoggerLevel = 'debug' | 'info' | 'warn' | 'error'
|
|
18
|
+
|
|
19
|
+
/** 日志出口:接收拼好前缀的整行(host 面接 ctx.logger,client 面接 console)。 */
|
|
20
|
+
export type LoggerSink = (level: LoggerLevel, line: string) => void
|
|
21
|
+
|
|
22
|
+
/** 默认级别到 console 方法名的映射。 */
|
|
23
|
+
const CONSOLE_METHODS: Record<LoggerLevel, string> = { debug: 'debug', info: 'info', warn: 'warn', error: 'error' }
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 默认出口:console(级别直映方法;方法缺失时静默丢弃,不抛错)。
|
|
27
|
+
* @author ddj 2026年09月08号
|
|
28
|
+
* @returns console 出口
|
|
29
|
+
*/
|
|
30
|
+
export function consoleSink(): LoggerSink {
|
|
31
|
+
return (level, line) => {
|
|
32
|
+
const emit = (console as unknown as Record<string, unknown>)[CONSOLE_METHODS[level]]
|
|
33
|
+
if (typeof emit === 'function') (emit as (message: string) => void).call(console, line)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* DSH host 出口工厂:转接 ctx.logger(方法级可选调用,缺服务/缺方法回退 console)。
|
|
39
|
+
* @author ddj 2026年09月08号
|
|
40
|
+
* @param ctx DSH host 上下文
|
|
41
|
+
* @returns 日志出口
|
|
42
|
+
*/
|
|
43
|
+
export function ctxLogSink(ctx: unknown): LoggerSink {
|
|
44
|
+
const base = consoleSink()
|
|
45
|
+
return (level, line) => {
|
|
46
|
+
const logger = (ctx as { logger?: Record<string, unknown> } | null | undefined)?.logger
|
|
47
|
+
const emit = logger?.[level]
|
|
48
|
+
if (typeof emit === 'function') (emit as (message: string) => void).call(logger, line)
|
|
49
|
+
else base(level, line)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 拼一行日志:`[dsh-vscode-mode][:scope] 消息`(scope 为空省略冒号段)。
|
|
55
|
+
* @author ddj 2026年09月08号
|
|
56
|
+
* @param scope 子域(可空)
|
|
57
|
+
* @param message 消息文本
|
|
58
|
+
* @returns 整行文本
|
|
59
|
+
*/
|
|
60
|
+
export function formatLine(scope: string, message: string): string {
|
|
61
|
+
const text = String(message)
|
|
62
|
+
return scope ? LOG_PREFIX + ':' + scope + ' ' + text : LOG_PREFIX + ' ' + text
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 日志器实例(四级别统一入口 + 换出口/派生子域)。 */
|
|
66
|
+
export interface PluginLogger {
|
|
67
|
+
debug(message: string): void
|
|
68
|
+
info(message: string): void
|
|
69
|
+
warn(message: string): void
|
|
70
|
+
error(message: string): void
|
|
71
|
+
/** 更换出口(同一实例树共享,host 装配期绑定 ctx.logger 用;client 单例不调用)。 */
|
|
72
|
+
bind(sink: LoggerSink): void
|
|
73
|
+
/** 派生带子域前缀的新实例(共享出口,如 log.child('debug') → `[dsh-vscode-mode:debug]`)。 */
|
|
74
|
+
child(scope: string): PluginLogger
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** 实例树共享的出口状态(bind 换出口对整棵树生效)。 */
|
|
78
|
+
interface SinkState {
|
|
79
|
+
sink: LoggerSink
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 日志器工厂(内部):emit 时读共享出口状态,保证父 bind 后子实例跟随。
|
|
84
|
+
* @author ddj 2026年09月08号
|
|
85
|
+
* @param state 共享出口状态
|
|
86
|
+
* @param scope 子域文本(可空)
|
|
87
|
+
* @returns 日志器
|
|
88
|
+
*/
|
|
89
|
+
function makeLogger(state: SinkState, scope: string): PluginLogger {
|
|
90
|
+
const emit = (level: LoggerLevel, message: string): void => state.sink(level, formatLine(scope, message))
|
|
91
|
+
return {
|
|
92
|
+
debug: (message) => emit('debug', message),
|
|
93
|
+
info: (message) => emit('info', message),
|
|
94
|
+
warn: (message) => emit('warn', message),
|
|
95
|
+
error: (message) => emit('error', message),
|
|
96
|
+
bind(next: LoggerSink): void {
|
|
97
|
+
state.sink = next
|
|
98
|
+
},
|
|
99
|
+
child(next: string): PluginLogger {
|
|
100
|
+
return makeLogger(state, scope ? scope + '.' + next : next)
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 创建插件日志器(默认 console 出口;host 面单例由 bindHostLog 换到 ctx.logger)。
|
|
107
|
+
* @author ddj 2026年09月08号
|
|
108
|
+
* @param sink 初始出口(缺省 console)
|
|
109
|
+
* @param scope 初始子域(业务方一般经 child 派生,不直接传)
|
|
110
|
+
* @returns 日志器
|
|
111
|
+
*/
|
|
112
|
+
export function createLogger(sink: LoggerSink = consoleSink(), scope = ''): PluginLogger {
|
|
113
|
+
return makeLogger({ sink }, scope)
|
|
114
|
+
}
|
package/src/shared/rpc.ts
CHANGED
|
@@ -17,6 +17,7 @@ import type { CompatReport, DevFormInfo } from './compat.js'
|
|
|
17
17
|
import type { ShellIntegrationStatus, UnityListPayload, UnityProjectEntry } from './integration.js'
|
|
18
18
|
import type { RuleInfo, RuleProject, RuleRefInput, RuleSaveInput } from './rules.js'
|
|
19
19
|
import type { LspEnvInstallState, LspExtInfo, LspExtUpdate, LspHover, LspLocation, LspMarketItem, LspPosition, LspSemanticTokens, LspServerStatus, LspSymbol } from './lsp.js'
|
|
20
|
+
import type { AiConfigPatch, AiConfigView, AiDirectoryView, AiInlineRequest, AiInlineResult } from './ai.js'
|
|
20
21
|
|
|
21
22
|
/** webServer 精确路由。 */
|
|
22
23
|
export const RPC_PATH = '/edrv/rpc'
|
|
@@ -216,6 +217,10 @@ export interface RpcRequestMap {
|
|
|
216
217
|
'edrv.lsp.ext.update': { id: string }
|
|
217
218
|
'edrv.lsp.ext.updates': {}
|
|
218
219
|
'edrv.lsp.ext.market': { query: string; size?: number }
|
|
220
|
+
'edrv.ai.inline': AiInlineRequest
|
|
221
|
+
'edrv.ai.models': { force?: boolean }
|
|
222
|
+
'edrv.ai.configGet': {}
|
|
223
|
+
'edrv.ai.configUpdate': AiConfigPatch
|
|
219
224
|
'edrv.perf.inventory': { sessionId?: string }
|
|
220
225
|
'edrv.perf.sessionSize': { sessionId?: string; cwd: string }
|
|
221
226
|
'edrv.perf.movePlan': { workspaceKey?: string; sessionIds?: string[]; minBytes?: number; olderThanDays?: number }
|
|
@@ -300,6 +305,10 @@ export interface RpcOkMap {
|
|
|
300
305
|
'edrv.lsp.ext.update': { extension: LspExtInfo; updated: boolean }
|
|
301
306
|
'edrv.lsp.ext.updates': { updates: LspExtUpdate[] }
|
|
302
307
|
'edrv.lsp.ext.market': { extensions: LspMarketItem[] }
|
|
308
|
+
'edrv.ai.inline': AiInlineResult
|
|
309
|
+
'edrv.ai.models': AiDirectoryView
|
|
310
|
+
'edrv.ai.configGet': AiConfigView
|
|
311
|
+
'edrv.ai.configUpdate': AiConfigView
|
|
303
312
|
'edrv.perf.inventory': { workspaces: PerfWorkspace[]; sessions: PerfSession[]; totals: PerfTotals; activeIds: string[] }
|
|
304
313
|
'edrv.perf.sessionSize': { bytes: number; exists: boolean }
|
|
305
314
|
'edrv.perf.movePlan': { items: PerfMoveItem[]; reclaimedBytes: number }
|
package/src/store.ts
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { ArchiveBatch, ArchiveData, DiffRecord, SidecarData } from './shared/types.js'
|
|
7
7
|
import type { ReadState } from './shared/diff.js'
|
|
8
|
-
import { fingerprint, isNoopHunk, locateHunks, preciseHunk } from './shared/diff.js'
|
|
8
|
+
import { fingerprint, isNoopHunk, locateHunks, normalizeForCompare, normalizeHunk, preciseHunk } from './shared/diff.js'
|
|
9
9
|
import { archiveEntryFor, groupByBatch, normalizeRecord } from './model.js'
|
|
10
10
|
import { SIDECAR, SIDECAR_ARCHIVE } from './paths.js'
|
|
11
|
+
import { log } from './log.js'
|
|
11
12
|
|
|
12
13
|
export { SIDECAR, SIDECAR_ARCHIVE }
|
|
13
14
|
export const READ_CAP = 8 * 1024 * 1024
|
|
@@ -100,7 +101,7 @@ export async function saveBucket(ctx: Ctx, cwd: string, recsMap: Map<string, Dif
|
|
|
100
101
|
existing.updatedAt = new Date().toISOString()
|
|
101
102
|
await fs.writeText(target, JSON.stringify(existing), void 0, void 0, policyOf(ctx, session))
|
|
102
103
|
} catch (error) {
|
|
103
|
-
|
|
104
|
+
log.error('saveBucket failed: ' + String(error))
|
|
104
105
|
}
|
|
105
106
|
}
|
|
106
107
|
|
|
@@ -146,7 +147,10 @@ export async function recordIsStale(ctx: Ctx, session: Session, cache: Map<strin
|
|
|
146
147
|
if (!pending.length) return true
|
|
147
148
|
const currentFingerprint = fingerprint(state.content)
|
|
148
149
|
if (rec.afterFingerprint && currentFingerprint === rec.afterFingerprint) { rec.conflict = false; return false }
|
|
149
|
-
|
|
150
|
+
// 归一化口径对比:外部工具可能改变 BOM/行尾,内容语义未变不应判冲突
|
|
151
|
+
const normalizedContent = normalizeForCompare(state.content)
|
|
152
|
+
if (rec.after != null && fingerprint(normalizeForCompare(rec.after)) === fingerprint(normalizedContent)) { rec.conflict = false; return false }
|
|
153
|
+
const locations = locateHunks(normalizedContent, pending.map((item) => normalizeHunk(item.hunk!)))
|
|
150
154
|
if (locations.some((location) => location.matched)) { rec.conflict = false; return false }
|
|
151
155
|
rec.conflict = true
|
|
152
156
|
return false
|
|
@@ -221,7 +225,7 @@ export async function appendArchiveEntries(ctx: Ctx, cwd: string, entries: Array
|
|
|
221
225
|
const data: ArchiveData = { version: 1, updatedAt: new Date().toISOString(), batches: existing }
|
|
222
226
|
await fs.writeText(target, JSON.stringify(data), void 0, void 0, policyOf(ctx, session))
|
|
223
227
|
} catch (error) {
|
|
224
|
-
|
|
228
|
+
log.error('appendArchiveEntries failed: ' + String(error))
|
|
225
229
|
}
|
|
226
230
|
}
|
|
227
231
|
|
package/src/workspace.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { Ctx, Session } from './store.js'
|
|
7
7
|
import { policyOf } from './store.js'
|
|
8
|
+
import { log } from './log.js'
|
|
8
9
|
|
|
9
10
|
/** 文件清单缓存 TTL 与扫描上限(避免大工作区反复全量扫描)。 */
|
|
10
11
|
const FILE_INDEX_TTL = 60_000
|
|
@@ -60,7 +61,7 @@ export async function listWorkspaceFiles(ctx: Ctx, session: Session, cwd: string
|
|
|
60
61
|
await run(['/bin/sh', '-c', cmd])
|
|
61
62
|
}
|
|
62
63
|
} catch (error) {
|
|
63
|
-
|
|
64
|
+
log.error('scan failed: ' + String(error))
|
|
64
65
|
return null
|
|
65
66
|
}
|
|
66
67
|
const files = lines.slice(0, SCAN_CAP)
|