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/svn.ts
CHANGED
|
@@ -11,8 +11,10 @@ import { basename, dirname, join } from 'node:path'
|
|
|
11
11
|
import { tmpdir } from 'node:os'
|
|
12
12
|
import type { Ctx, Session } from './store.js'
|
|
13
13
|
import { cwdOf, sessionOf } from './registry.js'
|
|
14
|
-
import type { SvnAction, SvnChangeEntry, SvnConflictArtifact, SvnFeature, SvnItemStatus, SvnLogEntry, SvnRemoteOutdatedEntry, SvnStatusPayload, SvnSumEntry } from './shared/svn.js'
|
|
15
|
-
import { SVN_CLI_DEFAULT, TORTOISE_EXE, contentGuardOf, tortoiseLaunchArgv } from './shared/svn.js'
|
|
14
|
+
import type { SvnAction, SvnAiPlan, SvnChangeEntry, SvnConflictArtifact, SvnFeature, SvnIgnoreItem, SvnItemStatus, SvnLogEntry, SvnRemoteOutdatedEntry, SvnStatusPayload, SvnSumEntry } from './shared/svn.js'
|
|
15
|
+
import { AI_PLAN_DIFF_CAP, AI_PLAN_PATHS_CAP, BATCH_PATHS_CAP, FILE_SIZES_CAP, SVN_CLI_DEFAULT, TORTOISE_EXE, contentGuardOf, normalizeAiPlan, svnChangelistNameErrorOf, tortoiseLaunchArgv } from './shared/svn.js'
|
|
16
|
+
import type { AiConfigView } from './shared/ai.js'
|
|
17
|
+
import { buildAiPrompt, svnAiPlanOf } from './ai/svnTriage.js'
|
|
16
18
|
import { attrOf, numAttrOf, scanXmlTags, textOf, unescapeXml } from './svnXml.js'
|
|
17
19
|
import { TEXT_OUTPUT_CAP, batchResultOf, tailOfText, updateResultOf } from './svnText.js'
|
|
18
20
|
import { LOG_CAP, LOG_DEFAULT_LIMIT, SVN_LOG_SHOW_ALL_CAP, parseLogXml, parseSvnInfoRevision } from './svnLog.js'
|
|
@@ -46,8 +48,9 @@ const OUTPUT_CAP = TEXT_OUTPUT_CAP
|
|
|
46
48
|
const LOG_OUTPUT_CAP = 64 * 1024 * 1024
|
|
47
49
|
/** 变更条目上限(防超大工作副本撑爆载荷)。 */
|
|
48
50
|
export const CHANGES_CAP = 4000
|
|
49
|
-
/**
|
|
50
|
-
export const
|
|
51
|
+
/** 混合通道收件箱 TTL(毫秒):agent 投递方案 30 分钟未取即过期(防陈旧方案误执行)。 */
|
|
52
|
+
export const AI_INBOX_TTL_MS = 30 * 60 * 1000
|
|
53
|
+
// 批量动作路径数上限(BATCH_PATHS_CAP)改由 shared/svn.ts 持有,revert/add/changelist 客户端分块同源对齐。
|
|
51
54
|
/** `svn status --xml` 超时(大工作副本留足预算)。 */
|
|
52
55
|
const STATUS_GRACE_MS = 120_000
|
|
53
56
|
/** 其他只读子命令(cat/info)超时。 */
|
|
@@ -73,8 +76,7 @@ const SUM_OUTPUT_CAP = 8 * 1024 * 1024
|
|
|
73
76
|
const REMOTE_GRACE_MS = 15_000
|
|
74
77
|
/** W2-2 summarize 服务器往返预算(大目录长区间可能较慢)。 */
|
|
75
78
|
const SUM_GRACE_MS = 60_000
|
|
76
|
-
|
|
77
|
-
export const FILE_SIZES_CAP = 200
|
|
79
|
+
// W2-5 fileSizes 单次查询路径上限(FILE_SIZES_CAP)改由 shared/svn.ts 持有,host/client 同源防漂移。
|
|
78
80
|
|
|
79
81
|
/** 文件存在性探测(可注入替身)。 */
|
|
80
82
|
export type ExistsFn = (path: string) => Promise<boolean>
|
|
@@ -148,6 +150,25 @@ export async function findTortoiseProc(
|
|
|
148
150
|
/** 设置里 SVN 路径的最小读取面(setupOpenSettings 提供)。 */
|
|
149
151
|
export interface SvnSettingsLike {
|
|
150
152
|
svn: () => { svnPath: string; tortoisePath: string }
|
|
153
|
+
/** AI 配置读取面(svn.aiPlan 模型路由;缺省自动选首个 provider/model)。 */
|
|
154
|
+
ai?: () => AiConfigView
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* svn:ignore 名称合法性校验(host 权威;防换行注入 propset 值与路径穿越)。
|
|
159
|
+
* 规则:去空白后非空、无换行、无路径分隔符、不以 `-` 开头(防被 svn 当选项解析)。
|
|
160
|
+
* @author ddj 2026年09月23号
|
|
161
|
+
* @param name 待写入的忽略名(basename / 通配模式)
|
|
162
|
+
* @returns 错误文案;合法为 null
|
|
163
|
+
*/
|
|
164
|
+
export function ignoreNameErrorOf(name: unknown): string | null {
|
|
165
|
+
if (typeof name !== 'string') return '忽略名必须是字符串'
|
|
166
|
+
const trimmed = name.trim()
|
|
167
|
+
if (!trimmed) return '忽略名不能为空'
|
|
168
|
+
if (/[\r\n]/.test(trimmed)) return '忽略名不能包含换行'
|
|
169
|
+
if (trimmed.includes('/') || trimmed.includes('\\')) return '忽略名不能包含路径分隔符'
|
|
170
|
+
if (trimmed.startsWith('-')) return '忽略名不能以 - 开头'
|
|
171
|
+
return null
|
|
151
172
|
}
|
|
152
173
|
|
|
153
174
|
/**
|
|
@@ -459,6 +480,8 @@ export function createSvnRpc(deps: SvnRpcDeps): { handlers: Record<string, unkno
|
|
|
459
480
|
const statusCache = new Map<string, { at: number; payload: SvnStatusPayload }>()
|
|
460
481
|
/** 变更清单缓存(键 = 工作副本根;TTL 短,仅合并同一轮 UI 的多处拉取)。 */
|
|
461
482
|
const changesCache = new Map<string, { at: number; value: { entries: SvnChangeEntry[]; truncated: boolean } }>()
|
|
483
|
+
/** 混合通道方案收件箱(key = 工作副本根;覆盖式入箱 + TTL,见 svn.aiPlanSubmit/Pending)。 */
|
|
484
|
+
const planInbox = new Map<string, { plan: SvnAiPlan; dropped: number; at: number }>()
|
|
462
485
|
/** 工作副本根 relative-url 缓存(日志路径映射用;探测失败也缓存空串避免反复 spawn)。 */
|
|
463
486
|
const urlCache = new Map<string, { at: number; value: string }>()
|
|
464
487
|
let cliProbe: { at: number; exe: string; ok: boolean } | null = null
|
|
@@ -806,6 +829,54 @@ export function createSvnRpc(deps: SvnRpcDeps): { handlers: Record<string, unkno
|
|
|
806
829
|
return merged.binary || merged.encodingHint ? merged : {}
|
|
807
830
|
}
|
|
808
831
|
|
|
832
|
+
/**
|
|
833
|
+
* svn:ignore 写入执行(AI 整理执行段③):逐目录 `propget` 读既有值 → 合并去重 → `propset`。
|
|
834
|
+
* 不整体覆盖(保留团队既有 ignore);逐目录失败收集不中断,全部失败才 ok:false。
|
|
835
|
+
* @author ddj 2026年09月23号
|
|
836
|
+
* @param wcRoot 工作副本根
|
|
837
|
+
* @param items 目录聚合写入项(dir 已归一,根为 '.')
|
|
838
|
+
* @returns 新增计数/摘要/原文
|
|
839
|
+
*/
|
|
840
|
+
const ignoreApplyOf = async (
|
|
841
|
+
wcRoot: string,
|
|
842
|
+
items: Array<{ dir: string; names: string[] }>,
|
|
843
|
+
): Promise<{ ok: boolean; count: number; summary: string; output: string }> => {
|
|
844
|
+
let count = 0
|
|
845
|
+
const notes: string[] = []
|
|
846
|
+
const failures: string[] = []
|
|
847
|
+
for (const item of items) {
|
|
848
|
+
const getOutcome = await runSvn(wcRoot, ['propget', 'svn:ignore', '--', item.dir], READ_GRACE_MS)
|
|
849
|
+
if (getOutcome.code !== 0) {
|
|
850
|
+
failures.push(item.dir + ' 读取失败:' + tailOfText(getOutcome.stderr || getOutcome.stdout))
|
|
851
|
+
continue
|
|
852
|
+
}
|
|
853
|
+
const existing = getOutcome.stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean)
|
|
854
|
+
const merged = [...existing]
|
|
855
|
+
let added = 0
|
|
856
|
+
for (const name of item.names) {
|
|
857
|
+
if (merged.includes(name)) continue
|
|
858
|
+
merged.push(name)
|
|
859
|
+
added++
|
|
860
|
+
}
|
|
861
|
+
if (!added) {
|
|
862
|
+
notes.push(item.dir + ':无新增(已在忽略列表)')
|
|
863
|
+
continue
|
|
864
|
+
}
|
|
865
|
+
const setOutcome = await runSvn(wcRoot, ['propset', 'svn:ignore', merged.join('\n'), '--', item.dir], MUTATE_GRACE_MS)
|
|
866
|
+
if (setOutcome.code !== 0) {
|
|
867
|
+
failures.push(item.dir + ' 写入失败:' + tailOfText(setOutcome.stderr || setOutcome.stdout))
|
|
868
|
+
continue
|
|
869
|
+
}
|
|
870
|
+
count += added
|
|
871
|
+
notes.push(item.dir + ':新增 ' + added + ' 项')
|
|
872
|
+
}
|
|
873
|
+
const ok = failures.length < items.length
|
|
874
|
+
const summary = failures.length
|
|
875
|
+
? 'svn:ignore 写入 ' + count + ' 项;失败 ' + failures.length + ' 个目录'
|
|
876
|
+
: count > 0 ? 'svn:ignore 写入 ' + count + ' 项' : '无新增忽略项'
|
|
877
|
+
return { ok, count, summary, output: [...notes, ...failures].join('\n') }
|
|
878
|
+
}
|
|
879
|
+
|
|
809
880
|
const handlers = {
|
|
810
881
|
'svn.status': async (args: { sessionId?: string; force?: boolean }) => {
|
|
811
882
|
const sc = await requireSvnSession(ctx, args.sessionId)
|
|
@@ -865,6 +936,28 @@ export function createSvnRpc(deps: SvnRpcDeps): { handlers: Record<string, unkno
|
|
|
865
936
|
'svn.add': async (args: { sessionId?: string; paths?: string[] }) => {
|
|
866
937
|
return mutate('加入版本控制', ['add'], args.sessionId, args.paths)
|
|
867
938
|
},
|
|
939
|
+
/**
|
|
940
|
+
* 分区(changelist)批量操作:关联或解除工作副本条目的 changelist 登记。
|
|
941
|
+
* 复用 mutate 全套护栏(路径白名单 / BATCH_PATHS_CAP / 超时 / 变更缓存作废);
|
|
942
|
+
* 分区名 host 权威校验(`-` 开头会被 svn 当成选项解析),remove 分支不需要名字。
|
|
943
|
+
* @author ddj 2026年09月23号
|
|
944
|
+
* @param args.paths 工作区相对路径列表
|
|
945
|
+
* @param args.name 目标分区名(remove 时忽略)
|
|
946
|
+
* @param args.remove true = 移出分区(`changelist --remove`)
|
|
947
|
+
* @returns 批量动作结果
|
|
948
|
+
*/
|
|
949
|
+
'svn.changelist': async (args: { sessionId?: string; paths?: string[]; name?: string; remove?: boolean }) => {
|
|
950
|
+
if (args.remove !== true) {
|
|
951
|
+
const nameError = svnChangelistNameErrorOf(String(args.name ?? ''))
|
|
952
|
+
if (nameError) return { ok: false as const, error: nameError }
|
|
953
|
+
}
|
|
954
|
+
return mutate(
|
|
955
|
+
args.remove === true ? '移出分区' : '分区',
|
|
956
|
+
args.remove === true ? ['changelist', '--remove'] : ['changelist', String(args.name ?? '').trim()],
|
|
957
|
+
args.sessionId,
|
|
958
|
+
args.paths,
|
|
959
|
+
)
|
|
960
|
+
},
|
|
868
961
|
'svn.log': async (args: { sessionId?: string; path?: string; limit?: number; stopOnCopy?: boolean; startRev?: number; endRev?: number; showMerged?: boolean }) => {
|
|
869
962
|
const sc = await requireSvnSession(ctx, args.sessionId)
|
|
870
963
|
if ('err' in sc) return { ok: false as const, error: sc.err }
|
|
@@ -1084,20 +1177,149 @@ export function createSvnRpc(deps: SvnRpcDeps): { handlers: Record<string, unkno
|
|
|
1084
1177
|
stat?: (t: unknown) => Promise<{ type?: string; size?: number } | null>
|
|
1085
1178
|
} | null | undefined
|
|
1086
1179
|
if (!fs?.resolve || !fs?.stat) return { ok: false as const, error: '缺少 fs' }
|
|
1087
|
-
|
|
1088
|
-
|
|
1180
|
+
// 单条 stat(异常回 null,语义与旧串行版一致)
|
|
1181
|
+
const sizeOne = async (raw: string): Promise<{ path: string; size: number | null }> => {
|
|
1089
1182
|
const rel = safeRel(raw)
|
|
1090
|
-
if (rel === null || rel === '')
|
|
1183
|
+
if (rel === null || rel === '') return { path: String(raw ?? ''), size: null }
|
|
1091
1184
|
try {
|
|
1092
|
-
const resolved = await fs.resolve(rel, { cwd: sc.cwd })
|
|
1093
|
-
const info = await fs.stat(resolved)
|
|
1094
|
-
|
|
1185
|
+
const resolved = await fs.resolve!(rel, { cwd: sc.cwd })
|
|
1186
|
+
const info = await fs.stat!(resolved)
|
|
1187
|
+
return { path: rel, size: info && info.type === 'file' ? (info.size ?? null) : null }
|
|
1095
1188
|
} catch {
|
|
1096
|
-
|
|
1189
|
+
return { path: rel, size: null }
|
|
1097
1190
|
}
|
|
1098
1191
|
}
|
|
1192
|
+
// 分批并发(批 16):上限 200 条时旧串行版要 400 次往返 await,批量并发显著缩短尾巴;
|
|
1193
|
+
// 结果顺序按输入还原(并发不改变载荷语义)。
|
|
1194
|
+
const sizes: Array<{ path: string; size: number | null }> = []
|
|
1195
|
+
const BATCH = 16
|
|
1196
|
+
for (let start = 0; start < list.length; start += BATCH) {
|
|
1197
|
+
sizes.push(...await Promise.all(list.slice(start, start + BATCH).map(sizeOne)))
|
|
1198
|
+
}
|
|
1099
1199
|
return { ok: true as const, sizes }
|
|
1100
1200
|
},
|
|
1201
|
+
/**
|
|
1202
|
+
* AI 智能整理分析(只读):拉最新变更 → 附 diff 上下文(超限降级 paths-only)→
|
|
1203
|
+
* LLM 流式产出 JSON → normalizeAiPlan 以真实清单白名单归一。任何失败零执行只报错。
|
|
1204
|
+
* @author ddj 2026年09月23号
|
|
1205
|
+
* @param args.sessionId 会话 id
|
|
1206
|
+
* @returns 三段方案 + 分析元信息(条目数/是否含 diff/丢弃数/模型)
|
|
1207
|
+
*/
|
|
1208
|
+
'svn.aiPlan': async (args: { sessionId?: string }) => {
|
|
1209
|
+
const sc = await requireSvnSession(ctx, args.sessionId)
|
|
1210
|
+
if ('err' in sc) return { ok: false as const, error: sc.err }
|
|
1211
|
+
const status = await statusOf(sc.cwd)
|
|
1212
|
+
if (!status.managed || !status.wcRoot) return { ok: false as const, error: '当前工作区不受 SVN 管理' }
|
|
1213
|
+
if (!status.svnCli) return { ok: false as const, error: 'svn 命令不可用(可在设置页配置 svn 路径)' }
|
|
1214
|
+
try {
|
|
1215
|
+
// 方案必须基于最新清单:强制重查,不吃 TTL 缓存
|
|
1216
|
+
const { entries } = await changesOf(status.wcRoot, true)
|
|
1217
|
+
// diff 上下文:单次 spawn(-x -U0 零上下文行压缩载荷);失败/近上限(收集器超限留尾,
|
|
1218
|
+
// 按长度近似判超限)降级 paths-only,不阻塞分析
|
|
1219
|
+
let diffText: string | null = null
|
|
1220
|
+
try {
|
|
1221
|
+
const outcome = await runSvn(status.wcRoot, ['diff', '-x', '-U0'], READ_GRACE_MS, AI_PLAN_DIFF_CAP)
|
|
1222
|
+
if (outcome.code === 0 && outcome.stdout.trim() && outcome.stdout.length < AI_PLAN_DIFF_CAP) diffText = outcome.stdout
|
|
1223
|
+
} catch { /* 降级 paths-only */ }
|
|
1224
|
+
// 条目上限:超 AI_PLAN_PATHS_CAP 只喂前缀并标记截断,diff 不再附带
|
|
1225
|
+
const capped = entries.length > AI_PLAN_PATHS_CAP
|
|
1226
|
+
const used = capped ? entries.slice(0, AI_PLAN_PATHS_CAP) : entries
|
|
1227
|
+
const diffIncluded = diffText !== null && !capped
|
|
1228
|
+
const prompt = buildAiPrompt(used, diffIncluded ? diffText : null, capped)
|
|
1229
|
+
const cfg = settings.ai ? settings.ai() : { enabled: true, provider: '', model: '', effort: '' }
|
|
1230
|
+
const result = await svnAiPlanOf(ctx, cfg, prompt)
|
|
1231
|
+
const { plan, dropped } = normalizeAiPlan(result.raw, entries)
|
|
1232
|
+
return { ok: true as const, plan, entriesCount: entries.length, diffIncluded, dropped, model: result.model }
|
|
1233
|
+
} catch (error) {
|
|
1234
|
+
return { ok: false as const, error: String(error instanceof Error ? error.message : error) }
|
|
1235
|
+
}
|
|
1236
|
+
},
|
|
1237
|
+
/**
|
|
1238
|
+
* AI 智能整理执行段③:按目录合并写入 svn:ignore 属性(propget 合并,不整体覆盖)。
|
|
1239
|
+
* 复用受管理/svnCli 校验与变更缓存作废语义(对齐 mutate)。
|
|
1240
|
+
* @author ddj 2026年09月23号
|
|
1241
|
+
* @param args.items 目录聚合写入项(dir + 该目录下新增忽略名)
|
|
1242
|
+
* @returns 新增计数/摘要/原文
|
|
1243
|
+
*/
|
|
1244
|
+
'svn.ignore': async (args: { sessionId?: string; items?: SvnIgnoreItem[] }) => {
|
|
1245
|
+
const sc = await requireSvnSession(ctx, args.sessionId)
|
|
1246
|
+
if ('err' in sc) return { ok: false as const, error: sc.err }
|
|
1247
|
+
const items = Array.isArray(args.items) ? args.items : []
|
|
1248
|
+
if (!items.length) return { ok: false as const, error: '未选择任何路径' }
|
|
1249
|
+
const clean: Array<{ dir: string; names: string[] }> = []
|
|
1250
|
+
let total = 0
|
|
1251
|
+
for (const item of items) {
|
|
1252
|
+
const rel = safeRel(item?.dir)
|
|
1253
|
+
if (rel === null) return { ok: false as const, error: '路径不合法' }
|
|
1254
|
+
const names = Array.isArray(item?.names) ? item.names : []
|
|
1255
|
+
if (!names.length) return { ok: false as const, error: '忽略名列表为空' }
|
|
1256
|
+
const checked: string[] = []
|
|
1257
|
+
for (const name of names) {
|
|
1258
|
+
const nameError = ignoreNameErrorOf(name)
|
|
1259
|
+
if (nameError) return { ok: false as const, error: nameError }
|
|
1260
|
+
checked.push(String(name).trim())
|
|
1261
|
+
}
|
|
1262
|
+
total += checked.length
|
|
1263
|
+
if (total > BATCH_PATHS_CAP) return { ok: false as const, error: '一次最多处理 ' + BATCH_PATHS_CAP + ' 个路径' }
|
|
1264
|
+
clean.push({ dir: rel === '' ? '.' : rel, names: checked })
|
|
1265
|
+
}
|
|
1266
|
+
const status = await statusOf(sc.cwd)
|
|
1267
|
+
if (!status.managed || !status.wcRoot) return { ok: false as const, error: '当前工作区不受 SVN 管理' }
|
|
1268
|
+
if (!status.svnCli) return { ok: false as const, error: 'svn 命令不可用(可在设置页配置 svn 路径)' }
|
|
1269
|
+
try {
|
|
1270
|
+
const result = await ignoreApplyOf(status.wcRoot, clean)
|
|
1271
|
+
// 写属性改动工作副本属性状态(目录进变更清单):作废缓存
|
|
1272
|
+
changesCache.delete(status.wcRoot)
|
|
1273
|
+
if (!result.ok) return { ok: false as const, error: result.summary + (result.output ? '\n' + tailOfText(result.output) : '') }
|
|
1274
|
+
return { ok: true as const, count: result.count, summary: result.summary, output: result.output }
|
|
1275
|
+
} catch (error) {
|
|
1276
|
+
return { ok: false as const, error: 'svn:ignore 写入失败:' + String(error) }
|
|
1277
|
+
}
|
|
1278
|
+
},
|
|
1279
|
+
/**
|
|
1280
|
+
* 混合通道投递口(01-hybrid-deep-analysis):会话 agent 深度分析后 POST 方案入收件箱。
|
|
1281
|
+
* 只读 + 内存态:经 normalizeAiPlan 以最新变更清单白名单归一(幻觉路径丢弃计数),
|
|
1282
|
+
* 零接受拒收(防 agent 空转误报成功);覆盖式入箱(新投递顶掉旧件)。
|
|
1283
|
+
* @author ddj 2026年09月23号
|
|
1284
|
+
* @param args.plan agent 产出的方案 JSON(形状宽松,归一兜底)
|
|
1285
|
+
* @returns 接受路径数与丢弃数
|
|
1286
|
+
*/
|
|
1287
|
+
'svn.aiPlanSubmit': async (args: { sessionId?: string; plan?: unknown }) => {
|
|
1288
|
+
const sc = await requireSvnSession(ctx, args.sessionId)
|
|
1289
|
+
if ('err' in sc) return { ok: false as const, error: sc.err }
|
|
1290
|
+
const status = await statusOf(sc.cwd)
|
|
1291
|
+
if (!status.managed || !status.wcRoot) return { ok: false as const, error: '当前工作区不受 SVN 管理' }
|
|
1292
|
+
if (!status.svnCli) return { ok: false as const, error: 'svn 命令不可用(可在设置页配置 svn 路径)' }
|
|
1293
|
+
try {
|
|
1294
|
+
const { entries } = await changesOf(status.wcRoot, true)
|
|
1295
|
+
const { plan, dropped } = normalizeAiPlan(args.plan, entries)
|
|
1296
|
+
const accepted = plan.reverts.length + plan.ignores.length + plan.groups.reduce((n, group) => n + group.paths.length, 0)
|
|
1297
|
+
if (!accepted) return { ok: false as const, error: '方案为空:无任何路径通过白名单校验(丢弃 ' + dropped + ' 项)' }
|
|
1298
|
+
planInbox.set(status.wcRoot, { plan, dropped, at: now() })
|
|
1299
|
+
return { ok: true as const, accepted, dropped }
|
|
1300
|
+
} catch (error) {
|
|
1301
|
+
return { ok: false as const, error: '方案投递失败:' + String(error) }
|
|
1302
|
+
}
|
|
1303
|
+
},
|
|
1304
|
+
/**
|
|
1305
|
+
* 混合通道取件口:面板轮询取收件箱方案。
|
|
1306
|
+
* since = 注入时刻——早于它的旧投递不算新件(防上一轮残留被误认为本轮结果);
|
|
1307
|
+
* TTL 过期返回 plan:null。非破坏性读取(重复取到同一份由 client 侧取件即停消化)。
|
|
1308
|
+
* @author ddj 2026年09月23号
|
|
1309
|
+
* @param args.since 注入时刻时间戳(缺省 0 = 取任意件)
|
|
1310
|
+
* @returns 方案与元信息(无新件 plan 为 null)
|
|
1311
|
+
*/
|
|
1312
|
+
'svn.aiPlanPending': async (args: { sessionId?: string; since?: number }) => {
|
|
1313
|
+
const sc = await requireSvnSession(ctx, args.sessionId)
|
|
1314
|
+
if ('err' in sc) return { ok: false as const, error: sc.err }
|
|
1315
|
+
const status = await statusOf(sc.cwd)
|
|
1316
|
+
if (!status.managed || !status.wcRoot) return { ok: false as const, error: '当前工作区不受 SVN 管理' }
|
|
1317
|
+
const hit = planInbox.get(status.wcRoot)
|
|
1318
|
+
const since = Number(args.since) || 0
|
|
1319
|
+
const stale = !hit || hit.at < since || now() - hit.at > AI_INBOX_TTL_MS
|
|
1320
|
+
if (stale) return { ok: true as const, plan: null, dropped: 0, at: 0 }
|
|
1321
|
+
return { ok: true as const, plan: hit.plan, dropped: hit.dropped, at: hit.at }
|
|
1322
|
+
},
|
|
1101
1323
|
'svn.wcRev': async (args: { sessionId?: string; path?: string }) => {
|
|
1102
1324
|
const sc = await requireSvnSession(ctx, args.sessionId)
|
|
1103
1325
|
if ('err' in sc) return { ok: false as const, error: sc.err }
|