dsh-vscode-mode 0.7.0 → 0.9.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 +37 -6
- package/assets/icon.svg +7 -0
- package/cordis.patch.yml +16 -0
- package/lib/client.js +2804 -300
- package/lib/client.js.map +1 -1
- package/lib/index.js +1975 -191
- package/lib/index.js.map +1 -1
- package/locale/en.json +4 -0
- package/locale/zh.json +4 -0
- package/package.json +4 -2
- package/src/ai/inline.ts +19 -2
- package/src/ai/svnTriage.ts +150 -0
- package/src/capture.ts +225 -51
- package/src/client/addToConversation.ts +203 -1
- package/src/client/compat.ts +134 -4
- package/src/client/imagePreview.ts +76 -2
- package/src/client/index.ts +65 -36
- package/src/client/monaco/lsp/lspClient.ts +46 -0
- package/src/client/monaco/lsp/providers.ts +245 -1
- package/src/client/nativeOpenStore.ts +91 -0
- package/src/client/officialSidebar.ts +11 -45
- package/src/client/outline/index.ts +1 -1
- package/src/client/pdf/pdfPanel.ts +18 -0
- package/src/client/pendingNav.ts +141 -0
- package/src/client/settingsContext.ts +6 -4
- package/src/client/sidebar/panels/FileExplorer.ts +2 -15
- package/src/client/sidebar/panels/SvnPanel.ts +682 -72
- package/src/client/sidebar/panels/index.ts +1 -1
- package/src/client/sidebar/panels/svnListBudget.ts +58 -0
- package/src/client/state/records.ts +7 -1
- package/src/client/styles/editor.css +47 -5
- package/src/client/svnStatus.ts +206 -1
- package/src/client/svnStore.ts +6 -1
- package/src/client/ui/AiSettings.ts +47 -14
- package/src/client/ui/DiffBox.ts +1 -1
- package/src/client/ui/DiffLauncher.ts +6 -4
- package/src/client/ui/EditorView.ts +149 -22
- package/src/client/ui/McpSettings.ts +30 -0
- package/src/client/ui/PerfSettings.ts +4 -1
- package/src/client/ui/SvnAiPlanDialog.ts +293 -0
- package/src/client/ui/icons.ts +91 -0
- package/src/compat.ts +26 -5
- package/src/dshVersion.ts +2 -1
- package/src/fileOpenSettings.ts +274 -49
- package/src/index.ts +18 -1
- package/src/lsp/client.ts +24 -0
- package/src/lsp/rpc.ts +57 -0
- package/src/lsp/server.ts +272 -2
- package/src/model.ts +2 -1
- package/src/perf.ts +90 -4
- package/src/remoteWorkspace.ts +177 -0
- package/src/routes.ts +13 -1
- package/src/rpc.ts +115 -39
- package/src/shared/ai.ts +12 -0
- package/src/shared/lsp.ts +79 -0
- package/src/shared/nativeOpen.ts +91 -0
- package/src/shared/rpc.ts +162 -2
- package/src/shared/svn.ts +296 -10
- package/src/shared/types.ts +5 -1
- package/src/svn.ts +235 -13
package/src/shared/rpc.ts
CHANGED
|
@@ -17,9 +17,9 @@ 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 { SnippetEntry, SnippetInfo, SnippetProject, SnippetRefInput, SnippetSaveInput } from './snippets.js'
|
|
20
|
-
import type { LspEnvInstallState, LspExtInfo, LspExtUpdate, LspHover, LspLocation, LspMarketItem, LspPosition, LspSemanticTokens, LspServerStatus, LspSymbol } from './lsp.js'
|
|
20
|
+
import type { LspCompletionItem, LspCompletionList, LspEnvInstallState, LspExtInfo, LspExtUpdate, LspHover, LspLocation, LspMarketItem, LspPosition, LspSemanticTokens, LspServerStatus, LspSignatureHelp, LspSymbol } from './lsp.js'
|
|
21
21
|
import type { AiConfigPatch, AiConfigView, AiDirectoryView, AiInlineRequest, AiInlineResult } from './ai.js'
|
|
22
|
-
import type { SvnAction, SvnChangeEntry, SvnConflictArtifact, SvnDiffRevResult, SvnLogEntry, SvnRemoteOutdatedEntry, SvnStatusPayload, SvnSumEntry, SvnUpdateResult } from './svn.js'
|
|
22
|
+
import type { SvnAction, SvnAiPlan, SvnChangeEntry, SvnConflictArtifact, SvnDiffRevResult, SvnIgnoreItem, SvnLogEntry, SvnRemoteOutdatedEntry, SvnStatusPayload, SvnSumEntry, SvnUpdateResult } from './svn.js'
|
|
23
23
|
import type { DapAction, DapAdapterInfo, DapBreakpointAck, DapBreakpointInput, DapConfigSnippet, DapConfigSource, DapDebugConfig, DapFrameView, DapPhase, DapPollResult, DapProcessInfo, DapScopeView, DapVariableView } from './dap.js'
|
|
24
24
|
import { DAP_ACTIONS } from './dap.js'
|
|
25
25
|
import type { LoggerLevel } from './logger.js'
|
|
@@ -73,6 +73,139 @@ export function binaryMimeOf(path: string): string {
|
|
|
73
73
|
return imageMimeOf(path)
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
// --region 二进制预览通道(edrv.readBinary:响应侧原始字节,省 ~33% base64 编码开销)
|
|
77
|
+
|
|
78
|
+
/** 二进制读成功响应头(octet-stream 携带元数据;同源请求可直接读取自定义头)。 */
|
|
79
|
+
export const BINARY_READ_HEADERS = {
|
|
80
|
+
mime: 'x-edrv-mime',
|
|
81
|
+
size: 'x-edrv-size',
|
|
82
|
+
version: 'x-edrv-version',
|
|
83
|
+
} as const
|
|
84
|
+
|
|
85
|
+
/** edrv.readBinary 成功载荷(host Uint8Array 信封;routes 接线后以 octet-stream 原样直出)。 */
|
|
86
|
+
export interface BinaryReadPayload {
|
|
87
|
+
bytes: Uint8Array
|
|
88
|
+
mime: string
|
|
89
|
+
size: number
|
|
90
|
+
version: string
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** 普通 JSON RPC 调用签名(回退通道复用;shared 不反向依赖 client 包装)。 */
|
|
94
|
+
export type RpcJsonCall = <M extends RpcMethod>(method: M, args: RpcRequestMap[M]) => Promise<RpcResult<M>>
|
|
95
|
+
|
|
96
|
+
/** 二进制预览读取统一结果:binary=新通道原始字节,base64=回退通道,probed=本次尝试过新通道。 */
|
|
97
|
+
export type PreviewRead =
|
|
98
|
+
| ({ ok: true; via: 'binary' } & BinaryReadPayload)
|
|
99
|
+
| { ok: true; via: 'base64'; content: string; size: number; encoding?: 'base64'; mime?: string; version?: string; probed?: boolean }
|
|
100
|
+
| { ok: false; error: string; resolvedPath?: string; probed?: boolean }
|
|
101
|
+
|
|
102
|
+
/** 通道探测状态:unknown=按次尝试;unwired=host 有 handler 但 routes 仍强制 JSON(本页不再尝试)。 */
|
|
103
|
+
let binaryTransport: 'unknown' | 'unwired' = 'unknown'
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 二进制预览读取(edrv.readBinary,失败自动回退 edrv.read base64 现有路径)。
|
|
107
|
+
* 降级链:非 200 / 非 octet-stream / 元数据头缺失或长度不符 / 网络错 / host 未接线
|
|
108
|
+
* 把信封 JSON 化 → 一律立即回退 base64 路径,功能不降级;
|
|
109
|
+
* 其中「信封被 JSON 化」判定为未接线,探测一次后本页生命周期内不再尝试新通道
|
|
110
|
+
* (避免每次读图都付一次无效往返与大响应序列化);其余失败不关停通道(瞬时故障可自愈)。
|
|
111
|
+
* @author ddj 2026年09月22号
|
|
112
|
+
* @param rpcJson 类型化 JSON RPC 调用(调用方传入 client rpc 包装)
|
|
113
|
+
* @param args edrv.readBinary 请求参数
|
|
114
|
+
* @returns 统一读取结果(via 标记实际通道;probed 供调用方做一次性回退日志)
|
|
115
|
+
*/
|
|
116
|
+
export async function readBinaryPreview(
|
|
117
|
+
rpcJson: RpcJsonCall,
|
|
118
|
+
args: RpcRequestMap['edrv.readBinary'],
|
|
119
|
+
): Promise<PreviewRead> {
|
|
120
|
+
const attempted = binaryTransport !== 'unwired'
|
|
121
|
+
if (attempted) {
|
|
122
|
+
const direct = await tryBinaryRead(args)
|
|
123
|
+
if (direct) return direct
|
|
124
|
+
}
|
|
125
|
+
return fallbackRead(rpcJson, args, attempted)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 新通道单次尝试:响应完全符合约定才返回二进制结果,否则返回 null 触发回退。
|
|
130
|
+
* @author ddj 2026年09月22号
|
|
131
|
+
* @param args edrv.readBinary 请求参数
|
|
132
|
+
* @returns 二进制读取结果或 null(null=回退 base64)
|
|
133
|
+
*/
|
|
134
|
+
async function tryBinaryRead(args: RpcRequestMap['edrv.readBinary']): Promise<PreviewRead | null> {
|
|
135
|
+
try {
|
|
136
|
+
const res = await fetch(RPC_PATH, {
|
|
137
|
+
method: 'POST',
|
|
138
|
+
headers: { 'content-type': 'application/json' },
|
|
139
|
+
body: JSON.stringify({ method: 'edrv.readBinary', args }),
|
|
140
|
+
})
|
|
141
|
+
if (!res.ok) return null
|
|
142
|
+
const type = res.headers.get('content-type') ?? ''
|
|
143
|
+
if (!type.includes('application/octet-stream')) return noteJsonBody(res)
|
|
144
|
+
const size = Number(res.headers.get(BINARY_READ_HEADERS.size))
|
|
145
|
+
const bytes = new Uint8Array(await res.arrayBuffer())
|
|
146
|
+
// 元数据头长度必须与实际字节数一致,否则视为非本插件响应(防御中间层/代理)
|
|
147
|
+
if (!Number.isFinite(size) || size !== bytes.byteLength) return null
|
|
148
|
+
return {
|
|
149
|
+
ok: true,
|
|
150
|
+
via: 'binary',
|
|
151
|
+
bytes,
|
|
152
|
+
mime: res.headers.get(BINARY_READ_HEADERS.mime) || binaryMimeOf(args.path),
|
|
153
|
+
size: bytes.byteLength,
|
|
154
|
+
version: res.headers.get(BINARY_READ_HEADERS.version) || '',
|
|
155
|
+
}
|
|
156
|
+
} catch (error) {
|
|
157
|
+
return null // 网络错按瞬时故障处理:回退但不关停通道
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 非 octet-stream 响应体判定:host 已有 edrv.readBinary 但 routes 尚未接线时,
|
|
163
|
+
* 二进制信封会被 JSON 序列化成 {bytes:{0:..}} 普通对象——识别到该形态即标记
|
|
164
|
+
* 「未接线」(本页不再尝试);其余 JSON(错误响应/未知方法)仅本次回退不关停。
|
|
165
|
+
* @author ddj 2026年09月22号
|
|
166
|
+
* @param res JSON 形态的响应
|
|
167
|
+
* @returns 恒为 null(统一由调用方回退)
|
|
168
|
+
*/
|
|
169
|
+
async function noteJsonBody(res: Response): Promise<null> {
|
|
170
|
+
try {
|
|
171
|
+
const body = (await res.json()) as { ok?: unknown; binary?: { bytes?: unknown } } | null
|
|
172
|
+
if (body && body.ok === true && body.binary && typeof body.binary === 'object' && body.binary.bytes != null) {
|
|
173
|
+
binaryTransport = 'unwired'
|
|
174
|
+
}
|
|
175
|
+
} catch (error) {
|
|
176
|
+
/* 非 JSON(代理页/空体):不判定,仅本次回退 */
|
|
177
|
+
}
|
|
178
|
+
return null
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* base64 回退读取(现有 edrv.read JSON 通道,载荷形状与历史实现一致)。
|
|
183
|
+
* @author ddj 2026年09月22号
|
|
184
|
+
* @param rpcJson 类型化 JSON RPC 调用
|
|
185
|
+
* @param args 请求参数
|
|
186
|
+
* @param attempted 本次是否尝试过新通道(回带给调用方做一次性日志)
|
|
187
|
+
* @returns 统一读取结果(调用失败转错误结果,不向上抛)
|
|
188
|
+
*/
|
|
189
|
+
async function fallbackRead(
|
|
190
|
+
rpcJson: RpcJsonCall,
|
|
191
|
+
args: RpcRequestMap['edrv.readBinary'],
|
|
192
|
+
attempted: boolean,
|
|
193
|
+
): Promise<PreviewRead> {
|
|
194
|
+
const probed = attempted ? { probed: true } : {}
|
|
195
|
+
try {
|
|
196
|
+
const res = await rpcJson('edrv.read', { sessionId: args.sessionId, path: args.path, encoding: 'base64' })
|
|
197
|
+
if (!res.ok) {
|
|
198
|
+
const legacy = res as { ok: false; error: string; resolvedPath?: string }
|
|
199
|
+
return { ok: false, error: legacy.error, ...probed, ...(legacy.resolvedPath !== undefined ? { resolvedPath: legacy.resolvedPath } : {}) }
|
|
200
|
+
}
|
|
201
|
+
if (res.encoding !== 'base64') return { ok: false, error: '读取失败', ...probed }
|
|
202
|
+
return { ok: true, via: 'base64', content: res.content, size: res.size, encoding: res.encoding, mime: res.mime, version: res.version, ...probed }
|
|
203
|
+
} catch (error) {
|
|
204
|
+
return { ok: false, error: 'read异常:' + String(error), ...probed }
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// --endregion
|
|
208
|
+
|
|
76
209
|
/** 决策作用域:call=整条记录,hunk=单个差异块。 */
|
|
77
210
|
export type RpcScope = 'call' | 'hunk'
|
|
78
211
|
|
|
@@ -101,6 +234,10 @@ export interface PerfSession {
|
|
|
101
234
|
bytes: number
|
|
102
235
|
mtime: number
|
|
103
236
|
active: boolean
|
|
237
|
+
/** 官方侧栏已归档(0.1.7+ workspaceRegistry.archivedSessionIds 持久位;旧 host 不回传 → undefined,按 false 渲染)。 */
|
|
238
|
+
archived?: boolean
|
|
239
|
+
/** 官方侧栏已置顶(0.1.7+ workspaceRegistry.pinnedSessionIds 持久位;旧 host 不回传 → undefined,按 false 渲染)。 */
|
|
240
|
+
pinned?: boolean
|
|
104
241
|
}
|
|
105
242
|
|
|
106
243
|
/** 一个工作区的会话聚合(edrv.perf.inventory)。 */
|
|
@@ -200,6 +337,7 @@ export interface RpcRequestMap {
|
|
|
200
337
|
'edrv.reject': { sessionId?: string; callId: string; scope?: RpcScope; hunkIndex?: number }
|
|
201
338
|
'edrv.decideBatch': { sessionId?: string; items: DecideItem[] }
|
|
202
339
|
'edrv.read': { sessionId?: string; path: string; encoding?: 'base64' }
|
|
340
|
+
'edrv.readBinary': { sessionId?: string; path: string }
|
|
203
341
|
'edrv.versions': { sessionId?: string; paths: string[] }
|
|
204
342
|
'edrv.original': { sessionId?: string; path: string }
|
|
205
343
|
'edrv.save': { sessionId?: string; path: string; content: string; rev?: string }
|
|
@@ -258,6 +396,9 @@ export interface RpcRequestMap {
|
|
|
258
396
|
'edrv.lsp.workspaceSymbol': { sessionId?: string; query: string }
|
|
259
397
|
'edrv.lsp.hover': { sessionId?: string; path: string; position: LspPosition }
|
|
260
398
|
'edrv.lsp.semanticTokens': { sessionId?: string; path: string }
|
|
399
|
+
'edrv.lsp.completion': { sessionId?: string; path: string; position: LspPosition; context?: { triggerKind?: number; triggerCharacter?: string } }
|
|
400
|
+
'edrv.lsp.resolveCompletion': { sessionId?: string; path: string; item: LspCompletionItem }
|
|
401
|
+
'edrv.lsp.signatureHelp': { sessionId?: string; path: string; position: LspPosition }
|
|
261
402
|
'edrv.lsp.redetect': { languageId: string }
|
|
262
403
|
'edrv.lsp.envInstall': { languageId: string; id: string }
|
|
263
404
|
'edrv.lsp.envState': {}
|
|
@@ -298,6 +439,7 @@ export interface RpcRequestMap {
|
|
|
298
439
|
'svn.diffBase': { sessionId?: string; path: string }
|
|
299
440
|
'svn.revert': { sessionId?: string; paths: string[] }
|
|
300
441
|
'svn.add': { sessionId?: string; paths: string[] }
|
|
442
|
+
'svn.changelist': { sessionId?: string; paths: string[]; name?: string; remove?: boolean }
|
|
301
443
|
'svn.log': { sessionId?: string; path?: string; limit?: number; stopOnCopy?: boolean; startRev?: number; endRev?: number; showMerged?: boolean }
|
|
302
444
|
'svn.diffRev': { sessionId?: string; path: string; revision: number }
|
|
303
445
|
'svn.diffPair': { sessionId?: string; path: string; revA: number; revB: number }
|
|
@@ -312,6 +454,14 @@ export interface RpcRequestMap {
|
|
|
312
454
|
'svn.conflictArtifacts': { sessionId?: string; path: string }
|
|
313
455
|
'svn.diffLocalPair': { sessionId?: string; left: string; right: string }
|
|
314
456
|
'svn.fileSizes': { sessionId?: string; paths: string[] }
|
|
457
|
+
/** AI 智能整理:只读分析全部变更,产出分组/还原/忽略三段方案(不写工作副本)。 */
|
|
458
|
+
'svn.aiPlan': { sessionId?: string }
|
|
459
|
+
/** AI 智能整理执行段:按目录合并写入 svn:ignore 属性(propget 合并,不整体覆盖)。 */
|
|
460
|
+
'svn.ignore': { sessionId?: string; items: SvnIgnoreItem[] }
|
|
461
|
+
/** 混合通道投递口:会话 agent 深度分析后把方案 POST 回面板收件箱(免鉴权本地 RPC)。 */
|
|
462
|
+
'svn.aiPlanSubmit': { sessionId?: string; plan: unknown }
|
|
463
|
+
/** 混合通道取件口:面板轮询取收件箱方案(since = 注入时刻,早于它的旧投递不算新件)。 */
|
|
464
|
+
'svn.aiPlanPending': { sessionId?: string; since?: number }
|
|
315
465
|
'edrv.dap.configs': { workspacePath: string }
|
|
316
466
|
'edrv.dap.snippets': {}
|
|
317
467
|
'edrv.dap.processes': { processName?: string }
|
|
@@ -335,6 +485,7 @@ export interface RpcOkMap {
|
|
|
335
485
|
'edrv.reject': { record: RecordView }
|
|
336
486
|
'edrv.decideBatch': { results: DecideResult[] }
|
|
337
487
|
'edrv.read': { content: string; size: number; encoding?: 'base64'; mime?: string; version?: string }
|
|
488
|
+
'edrv.readBinary': { binary: BinaryReadPayload }
|
|
338
489
|
'edrv.versions': { items: FileVersionItem[] }
|
|
339
490
|
'edrv.original': { content: string; size: number; stale: StaleHunk[]; fallback: boolean }
|
|
340
491
|
'edrv.save': { rev?: string; conflict?: boolean }
|
|
@@ -393,6 +544,9 @@ export interface RpcOkMap {
|
|
|
393
544
|
'edrv.lsp.workspaceSymbol': { symbols: LspSymbol[]; truncated?: boolean }
|
|
394
545
|
'edrv.lsp.hover': { hover?: LspHover }
|
|
395
546
|
'edrv.lsp.semanticTokens': { tokens?: LspSemanticTokens }
|
|
547
|
+
'edrv.lsp.completion': { completions?: LspCompletionList }
|
|
548
|
+
'edrv.lsp.resolveCompletion': { item?: LspCompletionItem }
|
|
549
|
+
'edrv.lsp.signatureHelp': { signatureHelp?: LspSignatureHelp }
|
|
396
550
|
'edrv.lsp.redetect': { servers: LspServerStatus[] }
|
|
397
551
|
'edrv.lsp.envInstall': { started: boolean }
|
|
398
552
|
'edrv.lsp.envState': { states: LspEnvInstallState[] }
|
|
@@ -433,6 +587,7 @@ export interface RpcOkMap {
|
|
|
433
587
|
'svn.diffBase': { base: string | null; working: string; reason?: string; error?: string; binary?: boolean; encodingHint?: boolean }
|
|
434
588
|
'svn.revert': { count: number; summary: string; output: string }
|
|
435
589
|
'svn.add': { count: number; summary: string; output: string }
|
|
590
|
+
'svn.changelist': { count: number; summary: string; output: string }
|
|
436
591
|
'svn.log': { entries: SvnLogEntry[]; truncated: boolean; target: string; limit?: number }
|
|
437
592
|
'svn.diffRev': SvnDiffRevResult
|
|
438
593
|
'svn.diffPair': SvnDiffRevResult
|
|
@@ -447,6 +602,11 @@ export interface RpcOkMap {
|
|
|
447
602
|
'svn.conflictArtifacts': { artifacts: SvnConflictArtifact[] }
|
|
448
603
|
'svn.diffLocalPair': SvnDiffRevResult
|
|
449
604
|
'svn.fileSizes': { sizes: Array<{ path: string; size: number | null }> }
|
|
605
|
+
'svn.aiPlan': { plan: SvnAiPlan; entriesCount: number; diffIncluded: boolean; dropped: number; model?: string }
|
|
606
|
+
'svn.ignore': { count: number; summary: string; output: string }
|
|
607
|
+
/** accepted = 归一后三段总路径数(0 = ok:false 拒收);dropped = 幻觉/落选丢弃数。 */
|
|
608
|
+
'svn.aiPlanSubmit': { accepted: number; dropped: number }
|
|
609
|
+
'svn.aiPlanPending': { plan: SvnAiPlan | null; dropped: number; at: number }
|
|
450
610
|
'edrv.dap.configs': { configs: DapDebugConfig[]; adapters: DapAdapterInfo[]; source: DapConfigSource }
|
|
451
611
|
'edrv.dap.snippets': { snippets: DapConfigSnippet[] }
|
|
452
612
|
'edrv.dap.processes': { items: DapProcessInfo[] }
|
package/src/shared/svn.ts
CHANGED
|
@@ -292,24 +292,39 @@ export interface SvnChangeFilter {
|
|
|
292
292
|
name?: string
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
+
/**
|
|
296
|
+
* 名称过滤匹配器(性能版):模式解析与正则编译只做一次,循环内零构造。
|
|
297
|
+
* `*` 任意串、`?` 单字符(忽略大小写);无通配符回落子串包含;
|
|
298
|
+
* 非法正则元字符已转义,永不抛错。与 changeNameMatch 语义一致。
|
|
299
|
+
* @author ddj 2026年09月23号
|
|
300
|
+
* @param pattern 过滤串(空/空白 = 全命中)
|
|
301
|
+
* @returns 单条路径命中判定
|
|
302
|
+
*/
|
|
303
|
+
export function nameMatcherOf(pattern: string | undefined): (path: string) => boolean {
|
|
304
|
+
const query = String(pattern ?? '').trim().toLowerCase()
|
|
305
|
+
if (!query) return () => true
|
|
306
|
+
if (!query.includes('*') && !query.includes('?')) {
|
|
307
|
+
return (path) => String(path ?? '').toLowerCase().includes(query)
|
|
308
|
+
}
|
|
309
|
+
const source = query
|
|
310
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
311
|
+
.replaceAll('*', '.*')
|
|
312
|
+
.replaceAll('?', '.')
|
|
313
|
+
const tester = new RegExp('^' + source + '$')
|
|
314
|
+
return (path) => tester.test(String(path ?? '').toLowerCase())
|
|
315
|
+
}
|
|
316
|
+
|
|
295
317
|
/**
|
|
296
318
|
* 路径名称过滤匹配(W1-2):`*` 任意串、`?` 单字符(忽略大小写);
|
|
297
319
|
* 无通配符时回落子串包含。非法正则元字符已转义,永不抛错。
|
|
320
|
+
* 大列表循环场景请改用 nameMatcherOf(预编译,避免逐条构造正则)。
|
|
298
321
|
* @author ddj 2026年09月20号
|
|
299
322
|
* @param path 条目路径(`/` 分隔)
|
|
300
323
|
* @param pattern 过滤串(空/空白 = 不过滤)
|
|
301
324
|
* @returns 是否命中
|
|
302
325
|
*/
|
|
303
326
|
export function changeNameMatch(path: string, pattern: string | undefined): boolean {
|
|
304
|
-
|
|
305
|
-
if (!query) return true
|
|
306
|
-
const target = String(path ?? '').toLowerCase()
|
|
307
|
-
if (!query.includes('*') && !query.includes('?')) return target.includes(query)
|
|
308
|
-
const source = query
|
|
309
|
-
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
310
|
-
.replaceAll('*', '.*')
|
|
311
|
-
.replaceAll('?', '.')
|
|
312
|
-
return new RegExp('^' + source + '$').test(target)
|
|
327
|
+
return nameMatcherOf(pattern)(path)
|
|
313
328
|
}
|
|
314
329
|
|
|
315
330
|
/**
|
|
@@ -325,8 +340,10 @@ export function svnVisibleChanges(
|
|
|
325
340
|
): SvnChangeEntry[] {
|
|
326
341
|
const showUnversioned = filter.unversioned !== false
|
|
327
342
|
const showIgnored = filter.ignored === true
|
|
343
|
+
// 名称匹配器循环外编译一次(大列表逐条 new RegExp 是键入卡顿主源之一)
|
|
344
|
+
const nameHit = nameMatcherOf(filter.name)
|
|
328
345
|
return entries.filter((entry) => {
|
|
329
|
-
if (!
|
|
346
|
+
if (!nameHit(entry.path)) return false
|
|
330
347
|
if (entry.status === 'ignored') return showIgnored
|
|
331
348
|
if (entry.status === 'unversioned') return showUnversioned
|
|
332
349
|
return isSvnChange(entry)
|
|
@@ -443,6 +460,53 @@ export interface SvnRenamePair {
|
|
|
443
460
|
unversionedPath: string
|
|
444
461
|
}
|
|
445
462
|
|
|
463
|
+
/** 改名配对 size 批量查询单次上限(host `svn.fileSizes` 同值护栏;client 预截断对齐,防请求白打)。 */
|
|
464
|
+
export const FILE_SIZES_CAP = 200
|
|
465
|
+
|
|
466
|
+
/** 批量路径动作单次上限(revert/add/changelist 共用;host mutate 权威护栏,client 预分块对齐)。 */
|
|
467
|
+
export const BATCH_PATHS_CAP = 64
|
|
468
|
+
|
|
469
|
+
/** 分区名(changelist 名)长度上限(过长名可读性差且易混入选项位)。 */
|
|
470
|
+
export const SVN_CHANGELIST_NAME_MAX = 64
|
|
471
|
+
|
|
472
|
+
// --region P5 分区管理(changelist)
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* 分区名(changelist 名)合法性校验:返回错误文案,合法返回 null。
|
|
476
|
+
* 规则:去首尾空白后非空、不以 `-` 开头(防被 svn 当成选项解析)、长度 ≤ 上限。
|
|
477
|
+
* client 预检 + host 权威校验共用,避免两处漂移。
|
|
478
|
+
* @author ddj 2026年09月23号
|
|
479
|
+
* @param name 用户输入的分区名
|
|
480
|
+
* @returns 错误文案;合法为 null
|
|
481
|
+
*/
|
|
482
|
+
export function svnChangelistNameErrorOf(name: string): string | null {
|
|
483
|
+
const trimmed = String(name ?? '').trim()
|
|
484
|
+
if (!trimmed) return '分区名不能为空'
|
|
485
|
+
if (trimmed.startsWith('-')) return '分区名不能以 - 开头'
|
|
486
|
+
if (trimmed.length > SVN_CHANGELIST_NAME_MAX) return '分区名最长 ' + SVN_CHANGELIST_NAME_MAX + ' 字符'
|
|
487
|
+
return null
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* 路径清单按单次上限分块(host 批量路径上限护栏的客户端对齐)。
|
|
492
|
+
* @author ddj 2026年09月23号
|
|
493
|
+
* @param paths 路径列表
|
|
494
|
+
* @param cap 单块上限(≤0 或非有限值视为不分块,整包一块)
|
|
495
|
+
* @returns 分块数组(保持原顺序;空入参返回 [])
|
|
496
|
+
*/
|
|
497
|
+
export function svnChunksOf(paths: readonly string[], cap: number): string[][] {
|
|
498
|
+
const size = Number.isFinite(cap) && cap > 0 ? Math.floor(cap) : 0
|
|
499
|
+
if (!paths.length) return []
|
|
500
|
+
if (!size || paths.length <= size) return [[...paths]]
|
|
501
|
+
const chunks: string[][] = []
|
|
502
|
+
for (let start = 0; start < paths.length; start += size) {
|
|
503
|
+
chunks.push(paths.slice(start, start + size))
|
|
504
|
+
}
|
|
505
|
+
return chunks
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// --endregion
|
|
509
|
+
|
|
446
510
|
/** 取路径 basename(`/`/`\` 通用;纯函数)。 */
|
|
447
511
|
function baseNameOf(path: string): string {
|
|
448
512
|
return String(path ?? '').split(/[\\/]/).pop() ?? ''
|
|
@@ -522,3 +586,225 @@ export interface SvnConflictArtifact {
|
|
|
522
586
|
}
|
|
523
587
|
|
|
524
588
|
// --endregion
|
|
589
|
+
|
|
590
|
+
// --region AI 智能整理(方案类型/归一化,11-ai-changelist-triage)
|
|
591
|
+
|
|
592
|
+
/** AI 整理方案单条建议(还原/忽略)。 */
|
|
593
|
+
export interface SvnAiPlanItem {
|
|
594
|
+
/** 工作区相对路径。 */
|
|
595
|
+
path: string
|
|
596
|
+
/** AI 判定理由(预览展示用;可缺省)。 */
|
|
597
|
+
reason?: string
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/** AI 整理方案分组建议(目标 changelist 组)。 */
|
|
601
|
+
export interface SvnAiPlanGroup {
|
|
602
|
+
/** 目标分区名(changelist 名)。 */
|
|
603
|
+
name: string
|
|
604
|
+
/** 组内路径(工作区相对)。 */
|
|
605
|
+
paths: string[]
|
|
606
|
+
/** 分组理由(预览展示用;可缺省)。 */
|
|
607
|
+
reason?: string
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/** AI 整理方案(三段互斥:分组/还原/忽略;normalizeAiPlan 归一后一路径至多一段)。 */
|
|
611
|
+
export interface SvnAiPlan {
|
|
612
|
+
/** 建议分组(changelist)。 */
|
|
613
|
+
groups: SvnAiPlanGroup[]
|
|
614
|
+
/** 建议还原(revert)。 */
|
|
615
|
+
reverts: SvnAiPlanItem[]
|
|
616
|
+
/** 建议忽略(写 svn:ignore 属性)。 */
|
|
617
|
+
ignores: SvnAiPlanItem[]
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/** svn:ignore 写入项(目标目录 + 该目录下新增忽略名列表)。 */
|
|
621
|
+
export interface SvnIgnoreItem {
|
|
622
|
+
/** 目录(工作区相对;'' = 工作副本根)。 */
|
|
623
|
+
dir: string
|
|
624
|
+
/** 该目录下要忽略的文件/目录名(basename)。 */
|
|
625
|
+
names: string[]
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/** AI 方案分组数上限(超出丢弃,防止过度碎片化)。 */
|
|
629
|
+
export const AI_PLAN_GROUPS_CAP = 20
|
|
630
|
+
|
|
631
|
+
/** AI 方案分析条目数上限(超出仅按路径分析并标记截断)。 */
|
|
632
|
+
export const AI_PLAN_PATHS_CAP = 1500
|
|
633
|
+
|
|
634
|
+
/** AI 方案 diff 上下文字节上限(超出降级 paths-only)。 */
|
|
635
|
+
export const AI_PLAN_DIFF_CAP = 2 * 1024 * 1024
|
|
636
|
+
|
|
637
|
+
/** AI 方案 LLM 调用超时(毫秒;推理模型 + 高思考档位 + diff 上下文实测 120s 不够(huoshan/glm-5.3-flash),放宽到 5 分钟)。 */
|
|
638
|
+
export const AI_PLAN_TIMEOUT_MS = 300 * 1000
|
|
639
|
+
|
|
640
|
+
/** 松散数组取值(非数组按空处理;AI 输出形状容错)。 */
|
|
641
|
+
function listOf(raw: unknown): unknown[] {
|
|
642
|
+
return Array.isArray(raw) ? raw : []
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* AI 方案条目解析(宽松:接受 `{path, reason}` 或裸路径字符串;反斜杠归一为 `/`)。
|
|
647
|
+
* @author ddj 2026年09月23号
|
|
648
|
+
* @param raw 原始元素
|
|
649
|
+
* @returns 解析结果(不可解析返回 null——非路径垃圾不作数)
|
|
650
|
+
*/
|
|
651
|
+
export function planItemOf(raw: unknown): SvnAiPlanItem | null {
|
|
652
|
+
const obj = typeof raw === 'string' ? { path: raw } : (raw && typeof raw === 'object' ? raw as { path?: unknown; reason?: unknown } : null)
|
|
653
|
+
if (!obj) return null
|
|
654
|
+
const path = typeof obj.path === 'string' ? obj.path.trim().replace(/\\/g, '/') : ''
|
|
655
|
+
if (!path) return null
|
|
656
|
+
const reason = typeof obj.reason === 'string' ? obj.reason.trim().slice(0, 120) : ''
|
|
657
|
+
return reason ? { path, reason } : { path }
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* AI 方案分组解析(宽松:name + paths 数组;路径逐个规范化)。
|
|
662
|
+
* @author ddj 2026年09月23号
|
|
663
|
+
* @param raw 原始元素
|
|
664
|
+
* @returns 解析结果(无合法名返回 null)
|
|
665
|
+
*/
|
|
666
|
+
export function planGroupOf(raw: unknown): { name: string; paths: string[]; reason?: string } | null {
|
|
667
|
+
if (!raw || typeof raw !== 'object') return null
|
|
668
|
+
const obj = raw as { name?: unknown; paths?: unknown; reason?: unknown }
|
|
669
|
+
const name = typeof obj.name === 'string' ? obj.name.trim() : ''
|
|
670
|
+
if (!name) return null
|
|
671
|
+
const paths: string[] = []
|
|
672
|
+
for (const p of listOf(obj.paths)) {
|
|
673
|
+
const path = typeof p === 'string' ? p.trim().replace(/\\/g, '/') : ''
|
|
674
|
+
if (path) paths.push(path)
|
|
675
|
+
}
|
|
676
|
+
const reason = typeof obj.reason === 'string' ? obj.reason.trim().slice(0, 120) : ''
|
|
677
|
+
return reason ? { name, paths, reason } : { name, paths }
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* AI 方案归一化(纯函数):以真实变更清单为白名单裁决三段建议。
|
|
682
|
+
* - 幻觉路径(不在清单中)/状态不符(revert 须 versioned、ignore 须 unversioned)→ 丢弃并计数;
|
|
683
|
+
* - 互斥优先级 revert > ignore > group(同一路径至多归一段,还原后条目消失故优先);
|
|
684
|
+
* - 组名过 svnChangelistNameErrorOf、组数上限 AI_PLAN_GROUPS_CAP,空组剔除。
|
|
685
|
+
* @author ddj 2026年09月23号
|
|
686
|
+
* @param raw AI 输出解析结果(形状不定,内部容错)
|
|
687
|
+
* @param entries 真实变更清单(白名单来源)
|
|
688
|
+
* @returns 归一化方案与被丢弃的候选路径数
|
|
689
|
+
*/
|
|
690
|
+
export function normalizeAiPlan(raw: unknown, entries: readonly SvnChangeEntry[]): { plan: SvnAiPlan; dropped: number } {
|
|
691
|
+
const plan: SvnAiPlan = { groups: [], reverts: [], ignores: [] }
|
|
692
|
+
if (!raw || typeof raw !== 'object') return { plan, dropped: 0 }
|
|
693
|
+
const byPath = new Map<string, SvnChangeEntry>()
|
|
694
|
+
for (const entry of entries) if (entry?.path) byPath.set(entry.path, entry)
|
|
695
|
+
const claimed = new Set<string>()
|
|
696
|
+
let dropped = 0
|
|
697
|
+
/** 单条裁决:真实存在 + 状态符合 + 未被更高优先级段认领。 */
|
|
698
|
+
const accept = (path: string, wantVersioned: boolean): boolean => {
|
|
699
|
+
const entry = byPath.get(path)
|
|
700
|
+
if (!entry || !isSvnChange(entry) || entry.versioned !== wantVersioned || claimed.has(path)) { dropped++; return false }
|
|
701
|
+
claimed.add(path)
|
|
702
|
+
return true
|
|
703
|
+
}
|
|
704
|
+
const src = raw as { groups?: unknown; reverts?: unknown; ignores?: unknown }
|
|
705
|
+
// 互斥优先级:revert > ignore > group
|
|
706
|
+
for (const el of listOf(src.reverts)) {
|
|
707
|
+
const item = planItemOf(el)
|
|
708
|
+
if (item && accept(item.path, true)) plan.reverts.push(item)
|
|
709
|
+
}
|
|
710
|
+
for (const el of listOf(src.ignores)) {
|
|
711
|
+
const item = planItemOf(el)
|
|
712
|
+
if (item && accept(item.path, false)) plan.ignores.push(item)
|
|
713
|
+
}
|
|
714
|
+
for (const el of listOf(src.groups)) {
|
|
715
|
+
const group = planGroupOf(el)
|
|
716
|
+
if (!group) {
|
|
717
|
+
// 形状不整但含路径候选(如空名组):候选路径同样计入 dropped,保持丢弃口径一致
|
|
718
|
+
const obj = el && typeof el === 'object' ? el as { paths?: unknown } : null
|
|
719
|
+
for (const p of listOf(obj?.paths)) {
|
|
720
|
+
if (typeof p === 'string' && p.trim()) dropped++
|
|
721
|
+
}
|
|
722
|
+
continue
|
|
723
|
+
}
|
|
724
|
+
const nameError = svnChangelistNameErrorOf(group.name) !== null
|
|
725
|
+
const over = plan.groups.length >= AI_PLAN_GROUPS_CAP
|
|
726
|
+
const valid: string[] = []
|
|
727
|
+
for (const path of group.paths) {
|
|
728
|
+
if (nameError || over) { dropped++; continue }
|
|
729
|
+
if (accept(path, true)) valid.push(path)
|
|
730
|
+
}
|
|
731
|
+
if (valid.length && !nameError && !over) {
|
|
732
|
+
plan.groups.push(group.reason ? { name: group.name, paths: valid, reason: group.reason } : { name: group.name, paths: valid })
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
return { plan, dropped }
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* 忽略路径清单 → 目录聚合写入项(纯函数):路径拆 dir + basename,同目录合并、同名去重。
|
|
740
|
+
* 供 `svn.ignore` 的执行载荷构造(一个目录一次 propset,进度粒度按目录)。
|
|
741
|
+
* @author ddj 2026年09月23号
|
|
742
|
+
* @param paths 未版本化路径清单(工作区相对)
|
|
743
|
+
* @returns 目录聚合写入项(保持目录首次出现顺序;空入参返回 [])
|
|
744
|
+
*/
|
|
745
|
+
export function ignoreItemsOf(paths: readonly string[]): SvnIgnoreItem[] {
|
|
746
|
+
const byDir = new Map<string, string[]>()
|
|
747
|
+
for (const raw of paths) {
|
|
748
|
+
const norm = String(raw ?? '').trim().replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+$/, '')
|
|
749
|
+
if (!norm) continue
|
|
750
|
+
const cut = norm.lastIndexOf('/')
|
|
751
|
+
const dir = cut <= 0 ? '' : norm.slice(0, cut)
|
|
752
|
+
const name = cut <= 0 ? norm : norm.slice(cut + 1)
|
|
753
|
+
if (!name) continue
|
|
754
|
+
const bucket = byDir.get(dir)
|
|
755
|
+
if (bucket) {
|
|
756
|
+
if (!bucket.includes(name)) bucket.push(name)
|
|
757
|
+
} else {
|
|
758
|
+
byDir.set(dir, [name])
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
return [...byDir.entries()].map(([dir, names]) => ({ dir, names }))
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/** 深度分析投递口地址(host RPC 本地免鉴权;agent 用 shell 直接 POST)。 */
|
|
765
|
+
const AGENT_SUBMIT_URL = 'http://127.0.0.1:3080/edrv/rpc'
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* 会话 agent 深度分析任务文本(混合通道 02-deep-session-prompt 修正版)。
|
|
769
|
+
* 在**独立新会话**草稿箱填入后由用户发送;agent 自跑只读 `svn status` 取清单
|
|
770
|
+
* (prompt 定长瘦身,不随变更数膨胀),借 codegraph/读文件/sub agent 深度分析,
|
|
771
|
+
* 产出方案经免鉴权 RPC `svn.aiPlanSubmit` 投递回面板收件箱——确认与执行仍全部
|
|
772
|
+
* 走面板(agent 禁写、只读放行)。
|
|
773
|
+
* @author ddj 2026年09月23号
|
|
774
|
+
* @param wcRoot 工作副本根(取数指引内嵌)
|
|
775
|
+
* @param sessionId 会话 id(投递命令内嵌;host 按它解析工作副本)
|
|
776
|
+
* @returns 任务文本(定长 ~1KB:取数指引/分析要求/投递命令/硬约束)
|
|
777
|
+
*/
|
|
778
|
+
export function buildAgentPrompt(wcRoot: string, sessionId: string): string {
|
|
779
|
+
return [
|
|
780
|
+
'请对 SVN 工作副本「' + wcRoot + '」的本地变更做**深度分析**,产出整理方案(分组/还原/忽略三段)。',
|
|
781
|
+
'',
|
|
782
|
+
'取数(只读命令允许):',
|
|
783
|
+
'- 先执行 `svn status`(只读)获取完整变更清单;',
|
|
784
|
+
'- 判断改动性质时用 `svn diff`(只读)查看具体改动。',
|
|
785
|
+
'',
|
|
786
|
+
'分析要求(按需使用你的工具):',
|
|
787
|
+
'- 借助 codegraph 查询调用链/模块归属,判断哪些文件属于同一功能主题(分组依据);',
|
|
788
|
+
'- 按需读取文件内容,判断改动性质(误改/行尾空白噪音/试验性改动 → 建议还原);',
|
|
789
|
+
'- 未版本化条目判断是否为生成物/临时文件/日志/缓存(建议忽略);',
|
|
790
|
+
'- 需要并行探索时可用 sub agent。',
|
|
791
|
+
'',
|
|
792
|
+
'方案 JSON 形状(reason 为简短中文一句话):',
|
|
793
|
+
'{ "groups": [{ "name": "英文短横线名", "paths": ["路径1"], "reason": "理由" }],',
|
|
794
|
+
' "reverts": [{ "path": "路径", "reason": "理由" }],',
|
|
795
|
+
' "ignores": [{ "path": "路径", "reason": "理由" }] }',
|
|
796
|
+
'',
|
|
797
|
+
'硬约束:',
|
|
798
|
+
'1. 禁止任何**写**操作(revert/commit/changelist/propset/update/merge 等一律禁止);只读命令(status/diff/log/cat/info)允许;',
|
|
799
|
+
'2. 每个路径至多归一类(groups/reverts/ignores 互斥),拿不准的不填;',
|
|
800
|
+
'3. 组名用简短英文、不以 - 开头;',
|
|
801
|
+
'4. 只使用 `svn status` 实际返回的路径,绝不虚构或改写路径。',
|
|
802
|
+
'',
|
|
803
|
+
'分析完成后,用 shell 执行下面的命令把方案投递回面板(把 <PLAN_JSON> 替换为方案 JSON):',
|
|
804
|
+
'Invoke-RestMethod -Uri ' + AGENT_SUBMIT_URL + ' -Method Post -ContentType \'application/json\' -Body (@{ method = \'svn.aiPlanSubmit\'; args = @{ sessionId = \'' + sessionId + '\'; plan = <PLAN_JSON> } } | ConvertTo-Json -Depth 8)',
|
|
805
|
+
'',
|
|
806
|
+
'投递成功会返回 { ok, accepted, dropped };随后我会在面板弹窗中确认执行,你无需再做任何操作。',
|
|
807
|
+
].join('\n')
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// --endregion
|
package/src/shared/types.ts
CHANGED
|
@@ -38,7 +38,7 @@ export interface Decisions {
|
|
|
38
38
|
/** 审查记录(host 内部存储与传给 client 的视图共用核心字段)。 */
|
|
39
39
|
export interface RecordBase {
|
|
40
40
|
callId: string
|
|
41
|
-
toolName: 'edit' | 'write'
|
|
41
|
+
toolName: 'edit' | 'write' | 'remote_ssh_write'
|
|
42
42
|
path: string
|
|
43
43
|
create: boolean
|
|
44
44
|
callHunk: CallHunk | null
|
|
@@ -68,6 +68,8 @@ export interface DiffRecord extends RecordBase {
|
|
|
68
68
|
/** 发往 client 的记录视图(不含 before 全文,仅长度)。 */
|
|
69
69
|
export interface RecordView extends RecordBase {
|
|
70
70
|
beforeLen: number
|
|
71
|
+
/** 展示路径(远程工作区为远端路径;运行时计算,不持久化)。 */
|
|
72
|
+
displayPath?: string
|
|
71
73
|
}
|
|
72
74
|
|
|
73
75
|
/** 归档条目内单条记录的存档视图。 */
|
|
@@ -109,6 +111,8 @@ export interface ArchiveEntry {
|
|
|
109
111
|
at: string
|
|
110
112
|
lastAt: string
|
|
111
113
|
path: string
|
|
114
|
+
/** 展示路径(远程工作区为远端路径;运行时计算,不持久化)。 */
|
|
115
|
+
displayPath?: string
|
|
112
116
|
batch: number | null
|
|
113
117
|
reason: string | null
|
|
114
118
|
nRecords: number
|