dsh-vscode-mode 0.8.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 +16 -2
- package/cordis.patch.yml +7 -3
- package/lib/client.js +1646 -121
- package/lib/client.js.map +1 -1
- package/lib/index.js +1141 -106
- package/lib/index.js.map +1 -1
- package/package.json +2 -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/pendingNav.ts +141 -0
- package/src/client/sidebar/panels/FileExplorer.ts +2 -15
- package/src/client/sidebar/panels/SvnPanel.ts +682 -72
- package/src/client/sidebar/panels/svnListBudget.ts +58 -0
- package/src/client/state/records.ts +7 -1
- package/src/client/styles/editor.css +42 -2
- package/src/client/svnStatus.ts +206 -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 +32 -4
- package/src/client/ui/SvnAiPlanDialog.ts +293 -0
- package/src/client/ui/icons.ts +19 -0
- package/src/compat.ts +25 -4
- package/src/fileOpenSettings.ts +128 -26
- package/src/index.ts +7 -4
- package/src/model.ts +2 -1
- package/src/remoteWorkspace.ts +177 -0
- package/src/rpc.ts +19 -5
- package/src/shared/ai.ts +12 -0
- package/src/shared/rpc.ts +16 -1
- package/src/shared/svn.ts +296 -10
- package/src/shared/types.ts +5 -1
- package/src/svn.ts +235 -13
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode host — dsh-remote-ssh 远程工作区探测与镜像↔远端路径映射。
|
|
3
|
+
* 背景(issue #7):dsh-remote-ssh 的「原生远程工作区」cwd 指向本机镜像目录
|
|
4
|
+
* (如 ~/.dsh/remote-workspaces/<id>,根下含 .remote-ssh.json {profileId, host,
|
|
5
|
+
* user, remotePath}),agent 的 remote_ssh_write 直写远端;差异审查需要把
|
|
6
|
+
* 远端参数路径映射回镜像(捕获/读/决策复用本地链路),并把展示路径翻译成远端形态。
|
|
7
|
+
* 判据与上游 dsh-remote-ssh 的 findMirrorRoot 对齐(host/user/remotePath 必填、
|
|
8
|
+
* 向上 ≤8 层有界);纯函数为主,node:fs 仅用于标记读取(带 60s TTL 缓存)。
|
|
9
|
+
* 作者 ddj 2026年09月23号
|
|
10
|
+
*/
|
|
11
|
+
import { readFileSync } from 'node:fs'
|
|
12
|
+
import { dirname, join } from 'node:path'
|
|
13
|
+
|
|
14
|
+
// --region 常量与类型
|
|
15
|
+
/** dsh-remote-ssh 工作区标记文件名。 */
|
|
16
|
+
const MARKER = '.remote-ssh.json'
|
|
17
|
+
/** 向上探测的最大层数(含 cwd 自身;与上游 findMirrorRoot 的 8 层有界一致)。 */
|
|
18
|
+
const MAX_DEPTH = 8
|
|
19
|
+
/** 探测结果 TTL(edrv.list 为 5s 轮询,避免每轮都做文件 IO)。 */
|
|
20
|
+
const TTL_MS = 60_000
|
|
21
|
+
|
|
22
|
+
/** 远程工作区(镜像根 + 标记内容)。 */
|
|
23
|
+
export interface RemoteWs {
|
|
24
|
+
/** 镜像根(含标记文件的目录,平台绝对路径)。 */
|
|
25
|
+
root: string
|
|
26
|
+
host: string
|
|
27
|
+
user: string
|
|
28
|
+
/** 远端工作区目录(`~/proj` 或绝对路径,原样保留)。 */
|
|
29
|
+
remotePath: string
|
|
30
|
+
profileId?: string
|
|
31
|
+
}
|
|
32
|
+
// --endregion
|
|
33
|
+
|
|
34
|
+
// --region 探测
|
|
35
|
+
/** cwd → { at, ws } 探测缓存(含阴性结果)。 */
|
|
36
|
+
const cache = new Map<string, { at: number; ws: RemoteWs | null }>()
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 标记内容 → 远程工作区(host/user/remotePath 缺任一字段判非法,与上游一致)。
|
|
40
|
+
* @author ddj 2026年09月23号
|
|
41
|
+
* @param root 标记所在目录
|
|
42
|
+
* @param raw 标记文件文本
|
|
43
|
+
* @returns 合法返回 RemoteWs;非法返回 null
|
|
44
|
+
*/
|
|
45
|
+
function markerWsOf(root: string, raw: string): RemoteWs | null {
|
|
46
|
+
try {
|
|
47
|
+
const info = JSON.parse(raw) as Record<string, unknown>
|
|
48
|
+
if (typeof info.host !== 'string' || !info.host) return null
|
|
49
|
+
if (typeof info.user !== 'string' || !info.user) return null
|
|
50
|
+
if (typeof info.remotePath !== 'string' || !info.remotePath) return null
|
|
51
|
+
const ws: RemoteWs = { root, host: info.host, user: info.user, remotePath: info.remotePath }
|
|
52
|
+
if (typeof info.profileId === 'string' && info.profileId) ws.profileId = info.profileId
|
|
53
|
+
return ws
|
|
54
|
+
} catch {
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 从 cwd 起向上有界探测标记(不走缓存)。
|
|
61
|
+
* @author ddj 2026年09月23号
|
|
62
|
+
* @param cwd 会话工作区根
|
|
63
|
+
* @returns 命中合法标记返回 RemoteWs;否则 null
|
|
64
|
+
*/
|
|
65
|
+
function walkRemoteWs(cwd: string): RemoteWs | null {
|
|
66
|
+
let dir = cwd
|
|
67
|
+
for (let i = 0; i < MAX_DEPTH; i++) {
|
|
68
|
+
try {
|
|
69
|
+
const ws = markerWsOf(dir, readFileSync(join(dir, MARKER), 'utf8'))
|
|
70
|
+
if (ws) return ws
|
|
71
|
+
} catch {
|
|
72
|
+
// 无标记或不可读/JSON 损坏:继续向上(与上游 findMirrorRoot 行为一致)
|
|
73
|
+
}
|
|
74
|
+
const parent = dirname(dir)
|
|
75
|
+
if (parent === dir) break
|
|
76
|
+
dir = parent
|
|
77
|
+
}
|
|
78
|
+
return null
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 探测工作区是否为 dsh-remote-ssh 远程工作区(cwd 或向上 ≤8 层找 .remote-ssh.json)。
|
|
83
|
+
* 结果按 cwd 缓存 60s(含阴性);标记变更后可用 clearWsCache 立即重置。
|
|
84
|
+
* @author ddj 2026年09月23号
|
|
85
|
+
* @param cwd 会话工作区根(可空)
|
|
86
|
+
* @returns 远程工作区信息;非远程或标记非法返回 null
|
|
87
|
+
*/
|
|
88
|
+
export function findRemoteWs(cwd: string | null | undefined): RemoteWs | null {
|
|
89
|
+
if (!cwd) return null
|
|
90
|
+
const now = Date.now()
|
|
91
|
+
const hit = cache.get(cwd)
|
|
92
|
+
if (hit && now - hit.at < TTL_MS) return hit.ws
|
|
93
|
+
const ws = walkRemoteWs(cwd)
|
|
94
|
+
cache.set(cwd, { at: now, ws })
|
|
95
|
+
return ws
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 清空探测缓存(测试与标记变更后重置用)。
|
|
100
|
+
* @author ddj 2026年09月23号
|
|
101
|
+
*/
|
|
102
|
+
export function clearWsCache(): void {
|
|
103
|
+
cache.clear()
|
|
104
|
+
}
|
|
105
|
+
// --endregion
|
|
106
|
+
|
|
107
|
+
// --region 路径映射
|
|
108
|
+
/**
|
|
109
|
+
* 前缀剥离:path 必须严格位于 root 之下;等于 root 或不匹配返回 null。
|
|
110
|
+
* @author ddj 2026年09月23号
|
|
111
|
+
* @param path 待剥离路径(posix 形态)
|
|
112
|
+
* @param root 前缀(posix 形态、无尾斜杠)
|
|
113
|
+
* @returns 相对段;不匹配返回 null
|
|
114
|
+
*/
|
|
115
|
+
function prefixRel(path: string, root: string): string | null {
|
|
116
|
+
if (path === root || !path.startsWith(root + '/')) return null
|
|
117
|
+
return path.slice(root.length + 1)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 远端参数路径 → 相对远端工作区目录的相对段。
|
|
122
|
+
* 支持:相对路径(工具语义即基于远端工作区目录)、绝对路径且 remotePath 为绝对
|
|
123
|
+
* 形态并为其前缀、`~` 形态且 remotePath 同为 `~` 前缀;Windows 盘符形态按
|
|
124
|
+
* 非远端语义拒绝。含 `..` 逃逸或空相对段返回 null。
|
|
125
|
+
* @author ddj 2026年09月23号
|
|
126
|
+
* @param argPath remote_ssh_write 的 path 参数
|
|
127
|
+
* @param ws 远程工作区
|
|
128
|
+
* @returns posix 相对段;不可映射返回 null
|
|
129
|
+
*/
|
|
130
|
+
function remoteRelOf(argPath: string, ws: RemoteWs): string | null {
|
|
131
|
+
const path = argPath.replace(/\\/g, '/').replace(/^\.\//, '')
|
|
132
|
+
if (!path) return null
|
|
133
|
+
const root = ws.remotePath.replace(/\\/g, '/').replace(/\/+$/, '')
|
|
134
|
+
if (!root) return null
|
|
135
|
+
let rel: string | null
|
|
136
|
+
if (path.startsWith('/')) {
|
|
137
|
+
rel = root.startsWith('/') ? prefixRel(path, root) : null
|
|
138
|
+
} else if (path === '~' || path.startsWith('~/')) {
|
|
139
|
+
rel = root.startsWith('~/') ? prefixRel(path, root) : null
|
|
140
|
+
} else if (/^[A-Za-z]:\//.test(path)) {
|
|
141
|
+
rel = null
|
|
142
|
+
} else {
|
|
143
|
+
rel = path
|
|
144
|
+
}
|
|
145
|
+
if (rel === null || rel.split('/').some((seg) => seg === '..')) return null
|
|
146
|
+
return rel
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 远端工具参数路径 → 本机镜像绝对路径。
|
|
151
|
+
* @author ddj 2026年09月23号
|
|
152
|
+
* @param argPath remote_ssh_write 的 path 参数
|
|
153
|
+
* @param ws 远程工作区
|
|
154
|
+
* @returns 镜像内绝对路径;镜像外/前缀形态不一致/逃逸返回 null
|
|
155
|
+
*/
|
|
156
|
+
export function mirrorTargetOf(argPath: string, ws: RemoteWs): string | null {
|
|
157
|
+
const rel = remoteRelOf(argPath, ws)
|
|
158
|
+
return rel === null ? null : join(ws.root, rel)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 镜像内绝对路径 → 远端展示路径(remotePath + 相对段,posix 拼接)。
|
|
163
|
+
* @author ddj 2026年09月23号
|
|
164
|
+
* @param mirrorAbs 镜像内绝对路径(通常是记录 path)
|
|
165
|
+
* @param ws 远程工作区
|
|
166
|
+
* @returns 远端路径;不在镜像根下返回 null(调用方回落原 path)
|
|
167
|
+
*/
|
|
168
|
+
export function remoteDisplayOf(mirrorAbs: string, ws: RemoteWs): string | null {
|
|
169
|
+
const path = mirrorAbs.replace(/\\/g, '/')
|
|
170
|
+
const root = ws.root.replace(/\\/g, '/').replace(/\/+$/, '')
|
|
171
|
+
if (!root) return null
|
|
172
|
+
const rel = prefixRel(path, root)
|
|
173
|
+
if (rel === null || rel.split('/').some((seg) => seg === '..')) return null
|
|
174
|
+
const remoteRoot = ws.remotePath.replace(/\/+$/, '')
|
|
175
|
+
return remoteRoot + '/' + rel
|
|
176
|
+
}
|
|
177
|
+
// --endregion
|
package/src/rpc.ts
CHANGED
|
@@ -44,6 +44,7 @@ import { findProfileDir, readDevForm, setDevForm } from './devForm.js'
|
|
|
44
44
|
import { normalizeRel } from './tree.js'
|
|
45
45
|
import { baseNameOf, checkNewName, checkRenameName, isSubPath, joinRelPath, parentRelOf } from './shared/fsNames.js'
|
|
46
46
|
import { invalidateIndex, listDirCached } from './treeIndex.js'
|
|
47
|
+
import { findRemoteWs, remoteDisplayOf, type RemoteWs } from './remoteWorkspace.js'
|
|
47
48
|
import { revealInExplorer } from './reveal.js'
|
|
48
49
|
import { dshHome, debugLogFile, pluginLogRoot } from './paths.js'
|
|
49
50
|
import { clearDebugLog, debugRecord, isDebugLogName, listDebugLogs, readDebugLog } from './debugLog.js'
|
|
@@ -56,8 +57,8 @@ const staleCheckedAt = new Map<string, number>()
|
|
|
56
57
|
/** stale 自动清理最小间隔。 */
|
|
57
58
|
const STALE_CHECK_MIN_MS = 10_000
|
|
58
59
|
|
|
59
|
-
/** 记录 → 客户端视图(不含 before
|
|
60
|
-
function recView(record: DiffRecord): RecordView {
|
|
60
|
+
/** 记录 → 客户端视图(不含 before 全文,仅长度);远程工作区附远端展示路径(issue #7)。 */
|
|
61
|
+
function recView(record: DiffRecord, ws: RemoteWs | null = null): RecordView {
|
|
61
62
|
return {
|
|
62
63
|
callId: record.callId,
|
|
63
64
|
toolName: record.toolName,
|
|
@@ -75,6 +76,7 @@ function recView(record: DiffRecord): RecordView {
|
|
|
75
76
|
conflict: record.conflict === true,
|
|
76
77
|
legacy: record.legacy === true,
|
|
77
78
|
at: record.at,
|
|
79
|
+
...(ws ? { displayPath: remoteDisplayOf(record.path, ws) ?? undefined } : {}),
|
|
78
80
|
}
|
|
79
81
|
}
|
|
80
82
|
|
|
@@ -232,6 +234,7 @@ async function applyDecisions(
|
|
|
232
234
|
): Promise<DecideResult[]> {
|
|
233
235
|
const results: DecideResult[] = []
|
|
234
236
|
const resolved: DiffRecord[] = []
|
|
237
|
+
const ws = findRemoteWs(cwd)
|
|
235
238
|
let changed = false
|
|
236
239
|
for (const item of items) {
|
|
237
240
|
const record = bucket.get(item.callId)
|
|
@@ -255,7 +258,7 @@ async function applyDecisions(
|
|
|
255
258
|
markDecision(record, item.scope ?? 'call', item.hunkIndex, item.decision)
|
|
256
259
|
changed = true
|
|
257
260
|
if (!wasResolved && recordResolved(record)) resolved.push(record)
|
|
258
|
-
const result: DecideResult = { callId: item.callId, ok: true, record: recView(record) }
|
|
261
|
+
const result: DecideResult = { callId: item.callId, ok: true, record: recView(record, ws) }
|
|
259
262
|
if (revertedStale) result.stale = true
|
|
260
263
|
results.push(result)
|
|
261
264
|
}
|
|
@@ -434,11 +437,12 @@ export function buildHandlers(
|
|
|
434
437
|
}
|
|
435
438
|
}
|
|
436
439
|
const out: RecordView[] = []
|
|
440
|
+
const ws = findRemoteWs(sc.cwd)
|
|
437
441
|
for (const rec of bucket.values()) {
|
|
438
442
|
// 面板全量查询过滤已归档;聊天条按 callId 查询保留(状态徽章仍需正确显示)
|
|
439
443
|
if (!want && rec.archived) continue
|
|
440
444
|
if (want && !want.has(rec.callId)) continue
|
|
441
|
-
out.push(recView(rec))
|
|
445
|
+
out.push(recView(rec, ws))
|
|
442
446
|
}
|
|
443
447
|
out.sort((a, b) => (a.at < b.at ? -1 : 1))
|
|
444
448
|
return { ok: true, records: out }
|
|
@@ -610,6 +614,7 @@ export function buildHandlers(
|
|
|
610
614
|
const sc = await requireSession(ctx, args.sessionId)
|
|
611
615
|
if ('err' in sc) return { ok: false, error: sc.err }
|
|
612
616
|
const batches = parseArchive(await readArchiveText(ctx, sc.cwd)).filter((b) => b.cwd === sc.cwd)
|
|
617
|
+
const ws = findRemoteWs(sc.cwd)
|
|
613
618
|
const entries = batches.map((b) => {
|
|
614
619
|
const recs = Array.isArray(b.records) ? b.records : []
|
|
615
620
|
const sum = recs.reduce((s, r) => {
|
|
@@ -620,7 +625,16 @@ export function buildHandlers(
|
|
|
620
625
|
if (sm.superseded) s.superseded++
|
|
621
626
|
return s
|
|
622
627
|
}, { accepted: 0, rejected: 0, pending: 0, superseded: 0 })
|
|
623
|
-
return {
|
|
628
|
+
return {
|
|
629
|
+
at: b.at,
|
|
630
|
+
lastAt: b.lastAt || b.at,
|
|
631
|
+
path: b.path,
|
|
632
|
+
...(ws ? { displayPath: remoteDisplayOf(b.path, ws) ?? undefined } : {}),
|
|
633
|
+
batch: b.batch ?? null,
|
|
634
|
+
reason: b.reason ?? null,
|
|
635
|
+
nRecords: recs.length,
|
|
636
|
+
summary: sum,
|
|
637
|
+
}
|
|
624
638
|
})
|
|
625
639
|
entries.sort((a, b) => (Number(b.batch ?? -1) - Number(a.batch ?? -1)) || (a.at < b.at ? 1 : -1))
|
|
626
640
|
return { ok: true, entries }
|
package/src/shared/ai.ts
CHANGED
|
@@ -66,6 +66,12 @@ export interface AiConfigView {
|
|
|
66
66
|
model: string
|
|
67
67
|
/** 空 = 跟随模型默认(请求不携带 reasoningEffort)。 */
|
|
68
68
|
effort: string
|
|
69
|
+
/** AI 任务模型(非补全场景):空/缺省 = 跟随补全配置。 */
|
|
70
|
+
taskProvider?: string
|
|
71
|
+
/** AI 任务模型 id(与 taskProvider 成对;空/缺省 = 跟随补全配置)。 */
|
|
72
|
+
taskModel?: string
|
|
73
|
+
/** AI 任务模型思考档位(空/缺省 = 跟随补全配置的 effort)。 */
|
|
74
|
+
taskEffort?: string
|
|
69
75
|
}
|
|
70
76
|
|
|
71
77
|
/** AI 补全配置更新载荷(edrv.ai.configUpdate)。 */
|
|
@@ -75,6 +81,12 @@ export interface AiConfigPatch {
|
|
|
75
81
|
model?: string
|
|
76
82
|
/** 空 = 清除(跟随默认)。 */
|
|
77
83
|
effort?: string
|
|
84
|
+
/** AI 任务模型(非补全场景):空 = 清除(跟随补全配置)。 */
|
|
85
|
+
taskProvider?: string
|
|
86
|
+
/** AI 任务模型 id(与 taskProvider 成对更新)。 */
|
|
87
|
+
taskModel?: string
|
|
88
|
+
/** AI 任务模型思考档位:空 = 清除(跟随补全配置的 effort)。 */
|
|
89
|
+
taskEffort?: string
|
|
78
90
|
}
|
|
79
91
|
|
|
80
92
|
/**
|
package/src/shared/rpc.ts
CHANGED
|
@@ -19,7 +19,7 @@ import type { RuleInfo, RuleProject, RuleRefInput, RuleSaveInput } from './rules
|
|
|
19
19
|
import type { SnippetEntry, SnippetInfo, SnippetProject, SnippetRefInput, SnippetSaveInput } from './snippets.js'
|
|
20
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'
|
|
@@ -439,6 +439,7 @@ export interface RpcRequestMap {
|
|
|
439
439
|
'svn.diffBase': { sessionId?: string; path: string }
|
|
440
440
|
'svn.revert': { sessionId?: string; paths: string[] }
|
|
441
441
|
'svn.add': { sessionId?: string; paths: string[] }
|
|
442
|
+
'svn.changelist': { sessionId?: string; paths: string[]; name?: string; remove?: boolean }
|
|
442
443
|
'svn.log': { sessionId?: string; path?: string; limit?: number; stopOnCopy?: boolean; startRev?: number; endRev?: number; showMerged?: boolean }
|
|
443
444
|
'svn.diffRev': { sessionId?: string; path: string; revision: number }
|
|
444
445
|
'svn.diffPair': { sessionId?: string; path: string; revA: number; revB: number }
|
|
@@ -453,6 +454,14 @@ export interface RpcRequestMap {
|
|
|
453
454
|
'svn.conflictArtifacts': { sessionId?: string; path: string }
|
|
454
455
|
'svn.diffLocalPair': { sessionId?: string; left: string; right: string }
|
|
455
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 }
|
|
456
465
|
'edrv.dap.configs': { workspacePath: string }
|
|
457
466
|
'edrv.dap.snippets': {}
|
|
458
467
|
'edrv.dap.processes': { processName?: string }
|
|
@@ -578,6 +587,7 @@ export interface RpcOkMap {
|
|
|
578
587
|
'svn.diffBase': { base: string | null; working: string; reason?: string; error?: string; binary?: boolean; encodingHint?: boolean }
|
|
579
588
|
'svn.revert': { count: number; summary: string; output: string }
|
|
580
589
|
'svn.add': { count: number; summary: string; output: string }
|
|
590
|
+
'svn.changelist': { count: number; summary: string; output: string }
|
|
581
591
|
'svn.log': { entries: SvnLogEntry[]; truncated: boolean; target: string; limit?: number }
|
|
582
592
|
'svn.diffRev': SvnDiffRevResult
|
|
583
593
|
'svn.diffPair': SvnDiffRevResult
|
|
@@ -592,6 +602,11 @@ export interface RpcOkMap {
|
|
|
592
602
|
'svn.conflictArtifacts': { artifacts: SvnConflictArtifact[] }
|
|
593
603
|
'svn.diffLocalPair': SvnDiffRevResult
|
|
594
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 }
|
|
595
610
|
'edrv.dap.configs': { configs: DapDebugConfig[]; adapters: DapAdapterInfo[]; source: DapConfigSource }
|
|
596
611
|
'edrv.dap.snippets': { snippets: DapConfigSnippet[] }
|
|
597
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
|