dsh-vscode-mode 0.14.6 → 0.14.8
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 +10 -7
- package/lib/client.js +818 -123
- package/lib/client.js.map +1 -1
- package/lib/index.js +1002 -19
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/capture.ts +4 -1
- package/src/client/events.ts +11 -5
- package/src/client/openFlow.ts +33 -3
- package/src/client/rpc.ts +10 -0
- package/src/client/sidebar/menuItems.ts +7 -6
- package/src/client/sidebar/panels/FileExplorer.ts +89 -72
- package/src/client/sidebar/panels/explorerView.ts +165 -0
- package/src/client/sidebar/panels/useExplorerSvn.ts +141 -0
- package/src/client/styles/editor.css +12 -0
- package/src/client/svnNotice.ts +24 -0
- package/src/client/ui/EditorView.ts +11 -10
- package/src/client/ui/SvnOverlay.ts +24 -0
- package/src/dap/manager.ts +65 -17
- package/src/index.ts +9 -1
- package/src/model.ts +112 -1
- package/src/openBridge.ts +13 -0
- package/src/rpc.ts +3 -1
- package/src/shared/rpc.ts +6 -1
- package/src/shared/svn.ts +55 -0
- package/src/shared/svnExplorer.ts +97 -0
- package/src/store.ts +36 -1
- package/src/svn.ts +108 -6
- package/src/svnExplorer.ts +56 -0
- package/src/svnExplorerCache.ts +304 -0
- package/src/svnStatusStream.ts +135 -0
- package/unity/com.dsh.editor/Editor/DshCodeEditor.cs +58 -4
- package/unity/com.dsh.editor/Editor/DshOpenWorker.cs +13 -2
- package/unity/com.dsh.editor/package.json +2 -2
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/** Explorer lifecycle: local refresh is offline, remote checks are explicit. @author ddj 2026年10月09号 */
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import { bindSvnScope } from '../../svnNotice.js'
|
|
4
|
+
import { rpc } from '../../rpc.js'
|
|
5
|
+
import { explorerPathOf, remoteIndexOf } from '../../../shared/svnExplorer.js'
|
|
6
|
+
import type { SvnViewRequest, SvnViewRow } from '../../../shared/svnExplorer.js'
|
|
7
|
+
import { ExplorerViewClient } from './explorerView.js'
|
|
8
|
+
import type { ExplorerNotice, ExplorerView } from './explorerView.js'
|
|
9
|
+
import type { SvnRemoteOutdatedEntry } from '../../../shared/svn.js'
|
|
10
|
+
import type { SidebarCtx } from '../types.js'
|
|
11
|
+
|
|
12
|
+
interface LocalSnapshot { view: ExplorerView; key: string }
|
|
13
|
+
|
|
14
|
+
// --region Explicit repository check
|
|
15
|
+
/** @private @author ddj 2026年10月09号 @param ctx Sidebar context. @param local Confirmed snapshot. @returns Expiring read-only remote evidence and check action. */
|
|
16
|
+
function useRemoteCheck(ctx: SidebarCtx | undefined, local: LocalSnapshot | null, resetToken: number, scope: string) {
|
|
17
|
+
const [remote, setRemote] = React.useState<Map<string, number> | null>(null)
|
|
18
|
+
const [message, setMessage] = React.useState('')
|
|
19
|
+
const [busy, setBusy] = React.useState(false)
|
|
20
|
+
const control = React.useRef<AbortController | null>(null)
|
|
21
|
+
const epoch = React.useRef(0)
|
|
22
|
+
const expiry = React.useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
23
|
+
|
|
24
|
+
/** @private @author ddj 2026年10月09号 Cancel stale request/timer without updating an unmounted component. */
|
|
25
|
+
function cancel() {
|
|
26
|
+
epoch.current++
|
|
27
|
+
control.current?.abort()
|
|
28
|
+
control.current = null
|
|
29
|
+
if (expiry.current) clearTimeout(expiry.current)
|
|
30
|
+
expiry.current = null
|
|
31
|
+
}
|
|
32
|
+
/** @private @author ddj 2026年10月09号 @returns Cleanup; scope/local changes invalidate repository evidence. */
|
|
33
|
+
function reset() { cancel(); setRemote(null); setMessage(''); setBusy(false); return cancel }
|
|
34
|
+
React.useEffect(reset, [scope, resetToken])
|
|
35
|
+
|
|
36
|
+
/** @public @author ddj 2026年10月09号 Contact repository once; failures/expiry never imply up-to-date. */
|
|
37
|
+
async function checkRemote() {
|
|
38
|
+
if (!local || control.current) return
|
|
39
|
+
cancel()
|
|
40
|
+
setRemote(null)
|
|
41
|
+
const controller = new AbortController()
|
|
42
|
+
control.current = controller
|
|
43
|
+
const seq = epoch.current
|
|
44
|
+
setBusy(true)
|
|
45
|
+
setMessage('正在检查仓库更新(只读)…')
|
|
46
|
+
try {
|
|
47
|
+
const out = await rpc('svn.remoteStatus', { sessionId: ctx?.sessionId, path: '' }, controller.signal)
|
|
48
|
+
if (controller.signal.aborted || seq !== epoch.current) return
|
|
49
|
+
if (!out.ok) { setMessage(out.error); return }
|
|
50
|
+
/** @private @author ddj 2026年10月09号 @param entry WC-relative change. @returns Session subset. */
|
|
51
|
+
function scopeEntry(entry: SvnRemoteOutdatedEntry): SvnRemoteOutdatedEntry[] {
|
|
52
|
+
const path = explorerPathOf(entry.path, local!.view.prefix)
|
|
53
|
+
return path === null ? [] : [{ ...entry, path }]
|
|
54
|
+
}
|
|
55
|
+
const scoped = out.outdated.flatMap(scopeEntry)
|
|
56
|
+
setRemote(remoteIndexOf(scoped))
|
|
57
|
+
setMessage('检查于 ' + new Date().toLocaleTimeString() + ':' + scoped.length + ' 项可更新'
|
|
58
|
+
+ (out.truncated ? '(结果不完整)' : '') + (out.againstRev === null ? '' : ',仓库 r' + out.againstRev))
|
|
59
|
+
/** @private @author ddj 2026年10月09号 Expire evidence; no automatic repository recheck. */
|
|
60
|
+
function expire() { setRemote(null); setMessage('远端检查已过期,请重新检查仓库更新') }
|
|
61
|
+
expiry.current = setTimeout(expire, 60_000)
|
|
62
|
+
} catch (error) { if (!controller.signal.aborted && seq === epoch.current) setMessage(String(error)) }
|
|
63
|
+
finally { if (seq === epoch.current) { control.current = null; setBusy(false) } }
|
|
64
|
+
}
|
|
65
|
+
return { remote, message, busy, checkRemote }
|
|
66
|
+
}
|
|
67
|
+
// --endregion
|
|
68
|
+
|
|
69
|
+
/** @private @author ddj 2026年10月10号 @param view Compact metadata. @returns Honest refresh/failure/last-confirmed status. */
|
|
70
|
+
function localMessage(view?: ExplorerView): string {
|
|
71
|
+
if (!view) return '正在读取本地 SVN 状态…'
|
|
72
|
+
const time = view.confirmedAt ? ';最后确认 ' + new Date(view.confirmedAt).toLocaleTimeString() : ';尚未确认'
|
|
73
|
+
if (view.error) return 'SVN 状态陈旧:' + view.error + time
|
|
74
|
+
return (view.refreshing ? 'SVN 状态刷新中' : view.complete ? 'SVN 本地状态已确认' : 'SVN 状态未完整确认') + time
|
|
75
|
+
}
|
|
76
|
+
// --region Offline local status
|
|
77
|
+
/** @public @author ddj 2026年10月09号 @param ctx Sidebar context. @returns Scope-bound local/remote icons; unknown entries never show green. */
|
|
78
|
+
export function useExplorerSvn(ctx: SidebarCtx | undefined, rows: readonly SvnViewRow[] = []) {
|
|
79
|
+
const [local, setLocal] = React.useState<LocalSnapshot | null>(null)
|
|
80
|
+
const [remoteToken, setRemoteToken] = React.useState(0)
|
|
81
|
+
const owner = React.useRef<ExplorerViewClient | null>(null)
|
|
82
|
+
const ready = ctx?.svn?.managed === true && ctx?.svn?.svnCli === true
|
|
83
|
+
const scope = ctx?.scope ?? ctx?.sessionId ?? ''
|
|
84
|
+
const key = JSON.stringify([scope, ready])
|
|
85
|
+
const snapshot = local?.key === key ? local : null
|
|
86
|
+
const remote = useRemoteCheck(ctx, snapshot, remoteToken, key)
|
|
87
|
+
|
|
88
|
+
/** @private @author ddj 2026年10月10号 @returns Scope owner cleanup; late results cannot populate a new workspace. */
|
|
89
|
+
function mount() {
|
|
90
|
+
setLocal(null)
|
|
91
|
+
bindSvnScope(ctx?.sessionId, scope)
|
|
92
|
+
if (!ready) return
|
|
93
|
+
/** @private @author ddj 2026年10月10号 @param view Compact state. Publish only this mounted scope. */
|
|
94
|
+
function publish(view: ExplorerView) { setLocal({ view, key }) }
|
|
95
|
+
/** @private @author ddj 2026年10月10号 Invalidate explicit remote evidence after non-text changes. */
|
|
96
|
+
function dropRemote() { setRemoteToken((value) => value + 1) }
|
|
97
|
+
/** @private @author ddj 2026年10月10号 @param args Compact projection. @param signal UI fetch lifetime. @returns Authenticated response. */
|
|
98
|
+
function fetch(args: SvnViewRequest, signal: AbortSignal) { return rpc('svn.explorerView', args, signal) }
|
|
99
|
+
const client = new ExplorerViewClient(scope, fetch, publish, dropRemote)
|
|
100
|
+
client.sessionId = ctx?.sessionId
|
|
101
|
+
owner.current = client
|
|
102
|
+
client.setRows(rows)
|
|
103
|
+
client.visibility(document.visibilityState !== 'hidden')
|
|
104
|
+
return () => { client.dispose(); if (owner.current === client) owner.current = null }
|
|
105
|
+
}
|
|
106
|
+
React.useEffect(mount, [key])
|
|
107
|
+
/** @private @author ddj 2026年10月10号 Row expansion/session switch only reprojects; Host owns shared workspace cache. */
|
|
108
|
+
function project() {
|
|
109
|
+
bindSvnScope(ctx?.sessionId, scope)
|
|
110
|
+
if (owner.current) { owner.current.sessionId = ctx?.sessionId; owner.current.setRows(rows) }
|
|
111
|
+
}
|
|
112
|
+
React.useEffect(project, [rows, ctx?.sessionId])
|
|
113
|
+
/** @private @author ddj 2026年10月10号 Unknown changes-list origins calibrate once; known event bursts are already scheduled. */
|
|
114
|
+
function changes() { owner.current?.changes() }
|
|
115
|
+
React.useEffect(changes, [ctx?.svnChanges])
|
|
116
|
+
|
|
117
|
+
/** @private @author ddj 2026年10月10号 @returns Scoped notices/focus/visibility listener cleanup. */
|
|
118
|
+
function listen() {
|
|
119
|
+
/** @private @author ddj 2026年10月10号 @param event Pathful or legacy notification. Legacy unknown scope safely falls back to full. */
|
|
120
|
+
function notice(event: Event) {
|
|
121
|
+
const detail = (event as CustomEvent<ExplorerNotice & { path?: string }>).detail ?? {}
|
|
122
|
+
owner.current?.notify({ ...detail, paths: detail.paths ?? (detail.path ? [detail.path] : undefined) })
|
|
123
|
+
}
|
|
124
|
+
/** @private @author ddj 2026年10月10号 Trigger only offline calibration; focus and visibility coalesce. */
|
|
125
|
+
function visibility() { owner.current?.visibility(document.visibilityState !== 'hidden') }
|
|
126
|
+
window.addEventListener('edrv:refresh', notice)
|
|
127
|
+
window.addEventListener('edrv:file-changed', notice)
|
|
128
|
+
window.addEventListener('focus', visibility)
|
|
129
|
+
document.addEventListener('visibilitychange', visibility)
|
|
130
|
+
return () => {
|
|
131
|
+
window.removeEventListener('edrv:refresh', notice); window.removeEventListener('edrv:file-changed', notice)
|
|
132
|
+
window.removeEventListener('focus', visibility); document.removeEventListener('visibilitychange', visibility)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
React.useEffect(listen, [])
|
|
136
|
+
/** @public @author ddj 2026年10月10号 Request an explicit offline full scan. */
|
|
137
|
+
function refreshLocal() { owner.current?.refresh() }
|
|
138
|
+
return { ...remote, local: snapshot?.view.marks ?? null, remote: snapshot ? remote.remote : null,
|
|
139
|
+
message: localMessage(snapshot?.view) + (remote.message ? ';' + remote.message : ''), canCheck: ready && snapshot !== null, refreshLocal }
|
|
140
|
+
}
|
|
141
|
+
// --endregion
|
|
@@ -277,6 +277,16 @@
|
|
|
277
277
|
/* 树行图标:官方原语(文件夹/文件类型),尺寸与行高对齐 @author ddj 2026-09-10 */
|
|
278
278
|
[data-edrv-view] .edrv-tree-icon { flex-shrink: 0; display: inline-flex; align-items: center; color: var(--dsw-alias-label-tertiary, #9aa5b1); }
|
|
279
279
|
[data-edrv-view] .edrv-tree-row.edrv-tree-active .edrv-tree-icon { color: var(--dsw-alias-label-secondary, #52606d); }
|
|
280
|
+
/* SVN overlays: official file icon remains visible, independent local/remote signs. ddj 2026-10-09 */
|
|
281
|
+
[data-edrv-view] .edrv-tree-iconbox { position: relative; display: inline-flex; width: 20px; height: 20px; align-items: center; flex-shrink: 0; }
|
|
282
|
+
[data-edrv-view] .edrv-svn-overlay { position: absolute; inset: 0; }
|
|
283
|
+
[data-edrv-view] .edrv-svn-local { position: absolute; right: -1px; bottom: -2px; display: flex; border-radius: 50%; box-shadow: 0 0 0 1px var(--dsw-alias-bg-layer-1, #fff); }
|
|
284
|
+
[data-edrv-view] .edrv-svn-icon-clean { color: #16853b; }
|
|
285
|
+
[data-edrv-view] .edrv-svn-icon-modified, [data-edrv-view] .edrv-svn-icon-deleted { color: #d12e35; }
|
|
286
|
+
[data-edrv-view] .edrv-svn-icon-conflict { color: #c27300; }
|
|
287
|
+
[data-edrv-view] .edrv-svn-icon-added { color: #2465c9; }
|
|
288
|
+
[data-edrv-view] .edrv-svn-icon-unversioned, [data-edrv-view] .edrv-svn-icon-ignored, [data-edrv-view] .edrv-svn-icon-external { color: #75808c; }
|
|
289
|
+
[data-edrv-view] .edrv-svn-remote { position: absolute; right: -3px; top: -4px; color: #2465c9; background: var(--dsw-alias-bg-layer-1, #fff); border-radius: 3px; font-size: 11px; font-weight: 900; line-height: 12px; }
|
|
280
290
|
[data-edrv-view] .edrv-rail-badge { position: absolute; top: 2px; right: 2px; min-width: 14px; height: 14px; padding: 0 3px; border-radius: 999px; background: var(--dsw-alias-brand-primary, #0f9d58); color: var(--dsw-alias-label-primary-inverted, #fff); font-size: 9px; font-weight: 600; line-height: 14px; text-align: center; box-sizing: border-box; }
|
|
281
291
|
[data-edrv-view] .edrv-side-body { flex: 1; min-width: 0; min-height: 0; display: flex; overflow: hidden; }
|
|
282
292
|
[data-edrv-view] .edrv-side-panel { flex: 1; min-width: 0; min-height: 0; display: flex; flex-direction: column; position: relative; }
|
|
@@ -298,6 +308,8 @@
|
|
|
298
308
|
[data-edrv-view] .edrv-tree-name.edrv-tree-dim { color: var(--dsw-alias-label-tertiary, #9aa5b1); }
|
|
299
309
|
[data-edrv-view] .edrv-tree-badge { min-width: 14px; height: 14px; padding: 0 4px; border-radius: 999px; background: var(--dsw-alias-state-warn-primary, #d97706); color: var(--dsw-alias-label-primary-inverted, #fff); font-size: 9px; font-weight: 600; line-height: 14px; text-align: center; box-sizing: border-box; flex-shrink: 0; }
|
|
300
310
|
[data-edrv-view] .edrv-tree-loading { height: 22px; line-height: 22px; color: var(--dsw-alias-label-tertiary, #9aa5b1); font-size: 11px; }
|
|
311
|
+
/* SVN 状态独立占位:换行不覆盖文件树,长错误在栏内滚动。@author ddj 2026-10-12 */
|
|
312
|
+
[data-edrv-view] .edrv-tree-status { flex: 0 0 auto; min-width: 0; max-height: 96px; overflow: auto; padding: 4px 8px; box-sizing: border-box; line-height: 18px; white-space: normal; overflow-wrap: anywhere; color: var(--dsw-alias-label-tertiary, #9aa5b1); font-size: 11px; }
|
|
301
313
|
[data-edrv-view] .edrv-tree-error { display: flex; align-items: center; gap: 8px; margin: 4px 2px 6px; padding: 6px 8px; border: 1px solid var(--dsw-alias-state-error-primary, rgba(217,83,79,.4)); border-radius: 6px; background: var(--dsw-alias-interactive-bg-hover-danger, rgba(217,83,79,.12)); color: var(--dsw-alias-state-error-primary, #d9534f); font-size: 11px; }
|
|
302
314
|
/* 大纲面板:kind 字形 / detail / 行号 */
|
|
303
315
|
[data-edrv-view] .edrv-outline-kind { display: inline-flex; align-items: center; justify-content: center; width: 14px; flex-shrink: 0; font-size: 12px; font-weight: 600; text-align: center; }
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** SVN mutation notifications, independent of UI stores. @author ddj 2026年10月10号 */
|
|
2
|
+
import type { RpcMethod, RpcRequestMap } from '../shared/rpc.js'
|
|
3
|
+
let sequence = 0
|
|
4
|
+
const scopes = new Map<string, string>()
|
|
5
|
+
/** @public @author ddj 2026年10月10号 @param sessionId Active session. @param scope Canonical client workspace. Record bounded routing hints; unknown sessions invalidate globally, never misapply relative paths. */
|
|
6
|
+
export function bindSvnScope(sessionId: string | undefined, scope: string): void {
|
|
7
|
+
if (!sessionId) return
|
|
8
|
+
scopes.delete(sessionId); scopes.set(sessionId, scope)
|
|
9
|
+
if (scopes.size > 512) scopes.delete(scopes.keys().next().value!)
|
|
10
|
+
}
|
|
11
|
+
const WRITES = new Set<RpcMethod>(['svn.add', 'svn.revert', 'svn.ignore', 'svn.cleanup', 'svn.update', 'svn.tortoise'])
|
|
12
|
+
/** @public @author ddj 2026年10月10号 @param method RPC operation. @returns Whether local evidence may change (launching a writable native dialog counts, not its completion). */
|
|
13
|
+
export function isSvnWrite(method: RpcMethod): boolean { return WRITES.has(method) }
|
|
14
|
+
/** @public @author ddj 2026年10月10号 @param method Operation. @param args Typed request. @param ok Completed successfully. Notify even failed/partially applied operations; no repository access. */
|
|
15
|
+
export function svnNotice<M extends RpcMethod>(method: M, args: RpcRequestMap[M], ok: boolean): void {
|
|
16
|
+
if (!isSvnWrite(method) || typeof window === 'undefined') return
|
|
17
|
+
const request = args as { sessionId?: string; path?: string; paths?: string[]; action?: string; items?: { dir: string }[] }
|
|
18
|
+
if (method === 'svn.tortoise' && !['commit', 'revert'].includes(request.action ?? '')) return
|
|
19
|
+
const scope = request.sessionId ? scopes.get(request.sessionId) : undefined
|
|
20
|
+
let paths: string[] | undefined
|
|
21
|
+
if (scope && ok && method !== 'svn.cleanup') paths = request.paths ?? request.items?.map((item) => item.dir) ?? (request.path ? [request.path] : undefined)
|
|
22
|
+
window.dispatchEvent(new CustomEvent('edrv:refresh', { detail: { scope, paths,
|
|
23
|
+
kind: 'svn', eventId: 'svn-write-' + Date.now() + '-' + (++sequence) } }))
|
|
24
|
+
}
|
|
@@ -1143,14 +1143,14 @@ export function EditorView(props) {
|
|
|
1143
1143
|
const onDiskChange = (change) => {
|
|
1144
1144
|
if (!change || !change.path) return
|
|
1145
1145
|
// 文件树同步:目录缓存按该路径失效并强制重列(外部新增/删除/改名都能看到)
|
|
1146
|
-
emitFileChanged(change.path)
|
|
1146
|
+
const eventId = emitFileChanged(change.path, { scope, kind: change.kind === 'deleted' ? 'structure' : 'text' })
|
|
1147
1147
|
if (change.kind === 'modified') {
|
|
1148
1148
|
// 干净缓冲:强制从磁盘重读(跳过 model 复用),差异标记随之重算
|
|
1149
1149
|
loadContent(change.path, sessionId, true)
|
|
1150
1150
|
clearSync(scope, change.path)
|
|
1151
1151
|
setDiskFlag((prev) => (prev && prev.path === change.path ? null : prev))
|
|
1152
1152
|
setStatus('已同步外部修改 ' + new Date().toTimeString().slice(0, 8))
|
|
1153
|
-
emitRefresh()
|
|
1153
|
+
emitRefresh({ scope, paths: [change.path], kind: 'text', eventId })
|
|
1154
1154
|
return
|
|
1155
1155
|
}
|
|
1156
1156
|
const kind = change.kind === 'deleted' ? 'deleted' : 'conflict'
|
|
@@ -1968,7 +1968,7 @@ export function EditorView(props) {
|
|
|
1968
1968
|
clearSync(scope, path)
|
|
1969
1969
|
acceptSaved(path, text, savedModel)
|
|
1970
1970
|
refreshRecords()
|
|
1971
|
-
emitRefresh()
|
|
1971
|
+
emitRefresh({ scope, paths: [path], kind: 'text' })
|
|
1972
1972
|
// 片段配置文件保存后失效补全缓存(下次补全即读到新片段)
|
|
1973
1973
|
if (/\.code-snippets$/i.test(path)) window.dispatchEvent(new CustomEvent('edrv:snippets-changed'))
|
|
1974
1974
|
return
|
|
@@ -2556,7 +2556,7 @@ export function EditorView(props) {
|
|
|
2556
2556
|
setDirtyMap((d) => Object.assign({}, d, { [path]: false }))
|
|
2557
2557
|
setDiskFlag((prev) => (prev && prev.path === path ? null : prev))
|
|
2558
2558
|
setStatus('已覆盖磁盘')
|
|
2559
|
-
emitRefresh()
|
|
2559
|
+
emitRefresh({ scope, paths: [path], kind: 'text' })
|
|
2560
2560
|
return
|
|
2561
2561
|
}
|
|
2562
2562
|
setStatus('覆盖失败')
|
|
@@ -2769,7 +2769,7 @@ export function EditorView(props) {
|
|
|
2769
2769
|
// #endregion
|
|
2770
2770
|
if (res && res.ok) {
|
|
2771
2771
|
setRecords((prev) => Object.assign({}, prev, { [region.callId]: res.record }))
|
|
2772
|
-
if (!silent) { reloadFile(); emitRefresh() }
|
|
2772
|
+
if (!silent) { reloadFile(); emitRefresh({ scope, paths: [res.record?.path || active], kind: 'structure' }) }
|
|
2773
2773
|
return true
|
|
2774
2774
|
}
|
|
2775
2775
|
if (!silent) setError(res?.error ? String(res.error) : '操作失败')
|
|
@@ -2858,7 +2858,7 @@ export function EditorView(props) {
|
|
|
2858
2858
|
actMany(decideRegions.map((r) => itemOf(r, false))).then((result) => {
|
|
2859
2859
|
batchBusyRef.current = false
|
|
2860
2860
|
reloadFile(true)
|
|
2861
|
-
emitRefresh()
|
|
2861
|
+
emitRefresh({ scope, paths: active ? [active] : undefined, kind: 'structure' })
|
|
2862
2862
|
setStatus(decideStatus('已采纳 ', result))
|
|
2863
2863
|
if (result.fail) setError(result.fail + ' 处差异处理失败(可能已被后续修改影响),可刷新后重试')
|
|
2864
2864
|
}).catch(() => { batchBusyRef.current = false })
|
|
@@ -2869,7 +2869,7 @@ export function EditorView(props) {
|
|
|
2869
2869
|
actMany([...decideRegions].reverse().map((r) => itemOf(r, true))).then((result) => {
|
|
2870
2870
|
batchBusyRef.current = false
|
|
2871
2871
|
reloadFile(true)
|
|
2872
|
-
emitRefresh()
|
|
2872
|
+
emitRefresh({ scope, paths: active ? [active] : undefined, kind: 'structure' })
|
|
2873
2873
|
setStatus(decideStatus('已不采纳 ', result))
|
|
2874
2874
|
if (result.fail) setError(result.fail + ' 处差异处理失败(可能已被后续修改影响),可刷新后重试')
|
|
2875
2875
|
}).catch(() => { batchBusyRef.current = false })
|
|
@@ -2899,10 +2899,11 @@ export function EditorView(props) {
|
|
|
2899
2899
|
if (batchBusyRef.current || !allPending.length) return
|
|
2900
2900
|
batchBusyRef.current = true
|
|
2901
2901
|
const list = reject ? [...allPending].sort((a, b) => (a.at < b.at ? 1 : a.at > b.at ? -1 : b.idx - a.idx)) : allPending
|
|
2902
|
+
const paths = [...new Set(list.map((r) => records[r.callId]?.path).filter(Boolean))]
|
|
2902
2903
|
actMany(list.map((r) => itemOf(r, reject))).then((result) => {
|
|
2903
2904
|
batchBusyRef.current = false
|
|
2904
2905
|
reloadFile(true)
|
|
2905
|
-
emitRefresh()
|
|
2906
|
+
emitRefresh({ scope, paths: paths.length ? paths : undefined, kind: 'structure' })
|
|
2906
2907
|
setStatus(decideStatus(reject ? '已不采纳 ' : '已采纳 ', result))
|
|
2907
2908
|
if (result.fail) setError(result.fail + ' 处差异处理失败(可能已被后续修改影响),可刷新后重试')
|
|
2908
2909
|
}).catch(() => { batchBusyRef.current = false })
|
|
@@ -2915,7 +2916,7 @@ export function EditorView(props) {
|
|
|
2915
2916
|
rpc('edrv.rollback', { sessionId, path: active }).then((res) => {
|
|
2916
2917
|
if (res && res.ok) {
|
|
2917
2918
|
setStatus('已回滚'); reloadFile()
|
|
2918
|
-
emitRefresh()
|
|
2919
|
+
emitRefresh({ scope, paths: [active], kind: 'structure' })
|
|
2919
2920
|
} else setError(res?.error ? String(res.error) : '回滚失败')
|
|
2920
2921
|
}).catch((e) => setError('回滚异常:' + String(e)))
|
|
2921
2922
|
}
|
|
@@ -4197,7 +4198,7 @@ export function EditorView(props) {
|
|
|
4197
4198
|
setStatus(message)
|
|
4198
4199
|
setRecentUpdate({ path: result.target, sessionId, scope, resumeJobId: result.jobId })
|
|
4199
4200
|
refreshSvnChanges(sessionId, scope)
|
|
4200
|
-
emitRefresh()
|
|
4201
|
+
emitRefresh({ scope, kind: 'svn', eventId: 'svn-update-' + result.jobId })
|
|
4201
4202
|
}
|
|
4202
4203
|
const svnUpdateEl = sameUpdateOwner(svnUpdateDialog, sessionId, scope)
|
|
4203
4204
|
? React.createElement(SvnUpdateDialog, {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Small vector overlays over official file-type icons. @author ddj 2026年10月09号 */
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import type { SvnExplorerMark, SvnViewMark } from '../../shared/svnExplorer.js'
|
|
4
|
+
/** @public @author ddj 2026年10月10号 @param row Compact mark. @returns Overlay description; stale evidence never renders a green check. */
|
|
5
|
+
export function viewOverlayOf(row?: SvnViewMark): SvnExplorerMark | null {
|
|
6
|
+
if (!row?.kind || (row.stale && row.kind === 'clean')) return null
|
|
7
|
+
const names = { clean: '本地正常(不表示仓库无更新)', modified: '本地修改', conflict: '冲突或阻塞',
|
|
8
|
+
added: '新增', deleted: '删除或缺失', unversioned: '未受控', ignored: '忽略', external: '外部引用' }
|
|
9
|
+
return { kind: row.kind, title: names[row.kind] + (row.count ? ';子项 ' + row.count + ' 处变更' : '')
|
|
10
|
+
+ (row.stale ? ';状态陈旧,等待确认' : '') }
|
|
11
|
+
}
|
|
12
|
+
const SIGNS = { clean: '✓', modified: '!', conflict: '×', added: '+', deleted: '−', unversioned: '?', ignored: '·', external: '↗' }
|
|
13
|
+
|
|
14
|
+
/** @public @author ddj 2026年10月09号 @param mark Confirmed local mark. @param remote Remote changed-item count. @returns Accessible vector overlay; remote arrow coexists with local conflict. */
|
|
15
|
+
export function svnOverlayEl(mark: SvnExplorerMark | null, remote: number): React.ReactElement | null {
|
|
16
|
+
if (!mark && !remote) return null
|
|
17
|
+
const title = 'SVN ' + (mark?.title ?? '本地状态未确认') + (remote ? ';仓库有可更新内容(' + remote + ' 项)' : '')
|
|
18
|
+
const svg = mark ? React.createElement('svg', { viewBox: '0 0 12 12', width: 12, height: 12, 'aria-hidden': true },
|
|
19
|
+
React.createElement('circle', { cx: 6, cy: 6, r: 5.5, fill: 'currentColor' }),
|
|
20
|
+
React.createElement('text', { x: 6, y: 9, textAnchor: 'middle', fill: '#fff', fontSize: 9, fontFamily: 'Arial, sans-serif', fontWeight: 700 }, SIGNS[mark.kind])) : null
|
|
21
|
+
return React.createElement('span', { className: 'edrv-svn-overlay', title, 'aria-label': title, role: 'img' },
|
|
22
|
+
mark ? React.createElement('span', { className: 'edrv-svn-local edrv-svn-icon-' + mark.kind }, svg) : null,
|
|
23
|
+
remote ? React.createElement('span', { className: 'edrv-svn-remote', 'aria-hidden': true }, '↓') : null)
|
|
24
|
+
}
|
package/src/dap/manager.ts
CHANGED
|
@@ -22,7 +22,7 @@ import { childEnv } from '../childEnv.js'
|
|
|
22
22
|
import { stopTree } from '../processTree.js'
|
|
23
23
|
import { join } from 'node:path'
|
|
24
24
|
import { createFrameParser, encodeMessage } from '../lsp/jsonrpc.js'
|
|
25
|
-
import { sourceBaseOf, sourceKeyOf, sourcePathOf, normalizeSourcePath } from './sourcePath.js'
|
|
25
|
+
import { sourceBaseOf, sourceKeyOf, sourcePathOf, normalizeSourcePath, rankSourceCandidates } from './sourcePath.js'
|
|
26
26
|
import { bpAcksOfResponse, DAP_ACTIONS, needsConfigurationDone, requestOfConfig, threadIdOfStopped, type DapAction, type DapBreakpointInput, type DapBreakpointAck, type DapDebugConfig, type DapEventView, type DapFrameView, type DapPhase, type DapPollResult, type DapScopeView, type DapStateView, type DapVariableView } from '../shared/dap.js'
|
|
27
27
|
import { dapRequest, eventBody, initializeArgs, isDapEvent, isDapResponse, responseBody, type DapEvent, type DapMessage, type DapRequest } from './protocol.js'
|
|
28
28
|
import type { DapAdapterSpec } from './provider.js'
|
|
@@ -458,18 +458,50 @@ export class DapSession {
|
|
|
458
458
|
return '会话在初始化完成前终止(attach/连接目标失败)' + dotrushLog
|
|
459
459
|
}
|
|
460
460
|
|
|
461
|
-
/**
|
|
461
|
+
/**
|
|
462
|
+
* 按当前语言过滤并排序源码候选,供反向应答与栈帧解析共用。
|
|
463
|
+
* @private
|
|
464
|
+
* @author ddj 2026年10月09号
|
|
465
|
+
* @param chunk 适配器上报的源码标识
|
|
466
|
+
* @returns 语言合法、按路径相关度排序的候选
|
|
467
|
+
*/
|
|
468
|
+
private async sourceCandidates(chunk: string): Promise<string[]> {
|
|
469
|
+
const files = await this.deps.findFiles(this.workspacePath, chunk)
|
|
470
|
+
const exts = this.frameExts()
|
|
471
|
+
const allowed = exts.length ? files.filter((file) => hasSourceExtension(file, exts)) : files
|
|
472
|
+
return rankSourceCandidates(chunk, allowed, this.workspacePath)
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* 更新源码标识及 basename 缓存;无合法候选时删除旧映射。
|
|
477
|
+
* @private
|
|
478
|
+
* @author ddj 2026年10月09号
|
|
479
|
+
* @param chunk 适配器上报的源码标识
|
|
480
|
+
* @param file 合法候选首项,空值表示未解析
|
|
481
|
+
* @description 副作用:更新当前会话源码缓存
|
|
482
|
+
*/
|
|
483
|
+
private cacheSource(chunk: string, file: string): void {
|
|
484
|
+
for (const key of [sourceKeyOf(chunk), sourceKeyOf(sourceBaseOf(chunk))]) {
|
|
485
|
+
if (!key) continue
|
|
486
|
+
if (file) this.sourceFiles.set(key, file)
|
|
487
|
+
else this.sourceFiles.delete(key)
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* findFileReq 反向应答:只返回当前语言合法源码并缓存首命中。
|
|
493
|
+
* @private
|
|
494
|
+
* @author ddj 2026年10月09号
|
|
495
|
+
* @param body 反向请求参数
|
|
496
|
+
* @description 副作用:更新源码缓存并发送 findFileRsp
|
|
497
|
+
*/
|
|
462
498
|
private async respondFindFile(body: Record<string, unknown>): Promise<void> {
|
|
463
499
|
const seq = typeof body.seq === 'number' ? body.seq : -1
|
|
464
500
|
const chunk = String(body.file ?? '')
|
|
465
|
-
const files = await this.
|
|
501
|
+
const files = await this.sourceCandidates(chunk)
|
|
466
502
|
log.debug('[dap-trace] findFileReq seq=' + seq + ' chunk=' + chunk + ' candidates=' + files.length + ' first=' + (files[0] ?? ''))
|
|
467
503
|
this.deps.trace?.(this.workspacePath, '[DEBUG findFileReq] seq=' + seq + ' chunk=' + chunk + ' candidates=' + files.length + ' first=' + (files[0] ?? ''))
|
|
468
|
-
|
|
469
|
-
if (first) {
|
|
470
|
-
this.sourceFiles.set(sourceKeyOf(chunk), first)
|
|
471
|
-
this.sourceFiles.set(sourceKeyOf(sourceBaseOf(chunk)), first)
|
|
472
|
-
}
|
|
504
|
+
this.cacheSource(chunk, files[0] ?? '')
|
|
473
505
|
this.send({ seq: this.msgSeq++, type: 'request', command: 'findFileRsp', arguments: { seq, files } })
|
|
474
506
|
}
|
|
475
507
|
|
|
@@ -583,27 +615,43 @@ export class DapSession {
|
|
|
583
615
|
if (phase === 'idle') { this.events = []; this.config = null; this.pid = undefined; this.error = undefined }
|
|
584
616
|
}
|
|
585
617
|
|
|
586
|
-
/**
|
|
618
|
+
/**
|
|
619
|
+
* 适配器源码标识转帧视图;完整合法路径优先,错误语言缓存须重新解析。
|
|
620
|
+
* @private
|
|
621
|
+
* @author ddj 2026年10月09号
|
|
622
|
+
* @param item 适配器栈帧
|
|
623
|
+
* @param index 栈帧下标(EmmyLua frameId)
|
|
624
|
+
* @returns 保留原始行号、只含合法源码路径的帧视图
|
|
625
|
+
* @description 副作用:按需查找源码并更新会话缓存
|
|
626
|
+
*/
|
|
587
627
|
private async frameViewOf(item: unknown, index: number): Promise<DapFrameView> {
|
|
588
628
|
const raw = item as Record<string, unknown>
|
|
589
629
|
const source = (raw.source ?? {}) as Record<string, unknown>
|
|
590
630
|
const sourcePath = typeof source.path === 'string' ? source.path : ''
|
|
591
631
|
const sourceName = typeof source.name === 'string' ? source.name : ''
|
|
632
|
+
const exts = this.frameExts()
|
|
592
633
|
const cached = this.sourceFiles.get(sourceKeyOf(sourcePath)) ?? this.sourceFiles.get(sourceKeyOf(sourceName))
|
|
593
|
-
|
|
594
|
-
|
|
634
|
+
const validPath = sourcePath && !isBareChunk(sourcePath)
|
|
635
|
+
&& (exts.length ? hasSourceExtension(sourcePath, exts) : sourceBaseOf(sourcePath).includes('.'))
|
|
636
|
+
const validCache = cached && (!exts.length || hasSourceExtension(cached, exts))
|
|
637
|
+
let selected = validPath ? sourcePath : validCache ? cached : sourcePath || sourceName
|
|
638
|
+
const needsLookup = !validPath && !validCache
|
|
639
|
+
if (cached && !validCache) {
|
|
640
|
+
this.cacheSource(sourcePath, '')
|
|
641
|
+
this.cacheSource(sourceName, '')
|
|
642
|
+
}
|
|
595
643
|
log.debug('[dap-trace] frame rawPath=' + sourcePath + ' name=' + sourceName + ' cached=' + (cached ?? '') + ' needsLookup=' + needsLookup + ' selected=' + selected)
|
|
596
644
|
this.deps.trace?.(this.workspacePath, '[DEBUG frame] rawPath=' + sourcePath + ' name=' + sourceName + ' cached=' + (cached ?? '') + ' needsLookup=' + needsLookup + ' selected=' + selected)
|
|
597
|
-
if (
|
|
598
|
-
const candidates = await this.
|
|
645
|
+
if (needsLookup) {
|
|
646
|
+
const candidates = await this.sourceCandidates(sourceName || sourcePath)
|
|
599
647
|
log.debug('[dap-trace] frameLookup source=' + (sourceName || sourcePath) + ' candidates=' + candidates.length + ' first=' + (candidates[0] ?? ''))
|
|
600
648
|
this.deps.trace?.(this.workspacePath, '[DEBUG frameLookup] source=' + (sourceName || sourcePath) + ' candidates=' + candidates.length + ' first=' + (candidates[0] ?? ''))
|
|
601
649
|
if (candidates[0]) {
|
|
602
650
|
selected = candidates[0]
|
|
603
|
-
this.
|
|
604
|
-
} else if (isBareChunk(sourceName || sourcePath)) {
|
|
605
|
-
//
|
|
606
|
-
//
|
|
651
|
+
this.cacheSource(sourceName || sourcePath, candidates[0])
|
|
652
|
+
} else if (exts.length || isBareChunk(sourceName || sourcePath)) {
|
|
653
|
+
// 反查失败且当前语言已知或只有裸 chunk:不回退到错误语言文件,
|
|
654
|
+
// 也不让客户端按工作区根拼出不存在的 chunk 路径。
|
|
607
655
|
selected = ''
|
|
608
656
|
}
|
|
609
657
|
}
|
package/src/index.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { registerRoutes } from './routes.js'
|
|
3
3
|
import { captureToolResult } from './capture.js'
|
|
4
4
|
import { closeArchiveDbs } from './archiveDb.js'
|
|
5
|
-
import { handleRpc } from './rpc.js'
|
|
5
|
+
import { handleRpc, integrationBaseUrlOf } from './rpc.js'
|
|
6
6
|
import { newSearcher } from './search/orchestrator.js'
|
|
7
7
|
import { newContentSearcher } from './search/content.js'
|
|
8
8
|
import { installIsolation } from './mcpIsolation.js'
|
|
@@ -13,6 +13,7 @@ import type { SettingsDeps } from './fileOpenSettings.js'
|
|
|
13
13
|
import z from '@deepseek-ai/schemastery'
|
|
14
14
|
import { shellMenuLifecycle } from './integrate.js'
|
|
15
15
|
import { startOpenInbox } from './openInbox.js'
|
|
16
|
+
import { ensureActiveBridge } from './openBridge.js'
|
|
16
17
|
import { installPlanTool } from './svnPlanTool.js'
|
|
17
18
|
import { disposeIndex } from './treeIndex.js'
|
|
18
19
|
import { sweepTreeCache } from './paths.js'
|
|
@@ -142,6 +143,13 @@ export async function apply(ctx: Ctx, config?: unknown): Promise<void> {
|
|
|
142
143
|
await installPlanTool(ctx, submitPlan)
|
|
143
144
|
ctx.effect(mountInbox, 'vscode-mode:open-inbox')
|
|
144
145
|
ctx.effect(() => shellMenuLifecycle(ctx), 'vscode-mode:integration')
|
|
146
|
+
/** @author ddj 2026年10月12号 @returns Active-profile bridge disposer; failures never block boot. */
|
|
147
|
+
async function provisionBridge(): Promise<() => void> {
|
|
148
|
+
try { await ensureActiveBridge(ctx, await integrationBaseUrlOf(ctx)) }
|
|
149
|
+
catch (error) { noteWarning('外部打开桥接未就绪:' + String(error)) }
|
|
150
|
+
return () => {}
|
|
151
|
+
}
|
|
152
|
+
ctx.effect(provisionBridge, 'vscode-mode:bridge')
|
|
145
153
|
void sweepTreeCache()
|
|
146
154
|
await logCompatSummary(ctx, warnings)
|
|
147
155
|
log.info('编辑差异审查已装配(认证路由、项目 MCP 作用域、SVN 方案工具与外部打开通道)')
|
package/src/model.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* 作者 ddj 2026-08-20
|
|
5
5
|
*/
|
|
6
6
|
import type { Decision, DiffRecord, Hunk, RecordSummary } from './shared/types.js'
|
|
7
|
-
import { applyLocations, fingerprint, isNoopHunk, locateHunks, preciseHunk } from './shared/diff.js'
|
|
7
|
+
import { applyLocations, fingerprint, isNoopHunk, locateHunks, normalizeForCompare, normalizeHunk, preciseHunk } from './shared/diff.js'
|
|
8
8
|
|
|
9
9
|
/** 工作区记录桶上限(超过后按 at 最旧剔除)。 */
|
|
10
10
|
export const MAX_RECORDS = 200
|
|
@@ -157,6 +157,117 @@ export function reconstructOriginal(records: DiffRecord[], content: string): { c
|
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
// #region 过程抵消
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* @public
|
|
164
|
+
* @author ddj 2026年10月10号
|
|
165
|
+
* 判断记录是否完全未决策;缺少逐块状态时沿用 pending 的 call 状态。
|
|
166
|
+
* @param record 审查记录
|
|
167
|
+
* @returns 未归档、未覆盖且没有用户决策时为 true
|
|
168
|
+
*/
|
|
169
|
+
export function fullyPending(record: DiffRecord): boolean {
|
|
170
|
+
return !record.archived && !record.superseded && record.decisions.call === 'pending'
|
|
171
|
+
&& record.decisions.perHunk.every((value) => value === 'pending')
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* @public
|
|
176
|
+
* @author ddj 2026年10月10号
|
|
177
|
+
* 比较整组 hunk 是否逐块精确逆反;不把新建或纯 no-op 当作抵消。
|
|
178
|
+
* @param prior 先前修改的差异块
|
|
179
|
+
* @param later 后续修改的差异块
|
|
180
|
+
* @returns 文本归一化后整组逆反且包含真实修改时为 true
|
|
181
|
+
*/
|
|
182
|
+
export function hunksInverse(prior: Hunk[], later: Hunk[]): boolean {
|
|
183
|
+
if (!prior.length || prior.length !== later.length) return false
|
|
184
|
+
let changed = false
|
|
185
|
+
for (let idx = 0; idx < prior.length; idx++) {
|
|
186
|
+
const a = normalizeHunk(prior[idx])
|
|
187
|
+
const b = normalizeHunk(later[idx])
|
|
188
|
+
if (a.oldText !== b.newText || a.newText !== b.oldText) return false
|
|
189
|
+
if (!isNoopHunk(a)) changed = true
|
|
190
|
+
}
|
|
191
|
+
return changed
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* @private
|
|
196
|
+
* @author ddj 2026年10月10号
|
|
197
|
+
* 同路径优先按捕获批次排序;旧记录仅使用有效时间,不拿 callId 伪造先后证据。
|
|
198
|
+
* @param a 第一条记录
|
|
199
|
+
* @param b 第二条记录
|
|
200
|
+
* @returns 负数表示 a 更早,零表示先后无法区分
|
|
201
|
+
*/
|
|
202
|
+
function recordOrder(a: DiffRecord, b: DiffRecord): number {
|
|
203
|
+
if (a.path !== b.path) return a.path.localeCompare(b.path)
|
|
204
|
+
if (Number.isInteger(a.batch) && Number.isInteger(b.batch)
|
|
205
|
+
&& a.batch > 0 && b.batch > 0 && a.batch !== b.batch) return a.batch - b.batch
|
|
206
|
+
const delta = Date.parse(a.at) - Date.parse(b.at)
|
|
207
|
+
return Number.isFinite(delta) ? delta : 0
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* @private
|
|
212
|
+
* @author ddj 2026年10月10号
|
|
213
|
+
* 确认完整文件快照往返,防止相同文本在不同位置的反向修改被误隐藏。
|
|
214
|
+
* @param prior 先前记录
|
|
215
|
+
* @param later 后续记录
|
|
216
|
+
* @returns 两端快照互换且文件存在性未变化时为 true;缺少证据时保留审查
|
|
217
|
+
*/
|
|
218
|
+
function snapshotsRestore(prior: DiffRecord, later: DiffRecord): boolean {
|
|
219
|
+
if (prior.create || later.create) return false
|
|
220
|
+
if (typeof prior.before !== 'string' || typeof prior.after !== 'string'
|
|
221
|
+
|| typeof later.before !== 'string' || typeof later.after !== 'string') return false
|
|
222
|
+
return normalizeForCompare(prior.before) === normalizeForCompare(later.after)
|
|
223
|
+
&& normalizeForCompare(prior.after) === normalizeForCompare(later.before)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* @public
|
|
228
|
+
* @author ddj 2026年10月10号
|
|
229
|
+
* 查找最近一条未决的整记录逆反;必须有文件快照证明原内容确实恢复。
|
|
230
|
+
* @param records 工作区记录(不修改输入)
|
|
231
|
+
* @param record 新记录
|
|
232
|
+
* @returns 可安全配对的先前记录,证据不足或部分撤销时为 null
|
|
233
|
+
*/
|
|
234
|
+
export function findInversePrior(records: DiffRecord[], record: DiffRecord): DiffRecord | null {
|
|
235
|
+
if (!fullyPending(record)) return null
|
|
236
|
+
let found: DiffRecord | null = null
|
|
237
|
+
for (const prior of records) {
|
|
238
|
+
if (prior.callId === record.callId || prior.path !== record.path || !fullyPending(prior)) continue
|
|
239
|
+
if (recordOrder(prior, record) >= 0 || !hunksInverse(prior.hunks, record.hunks)) continue
|
|
240
|
+
if (!snapshotsRestore(prior, record)) continue
|
|
241
|
+
if (!found || recordOrder(found, prior) < 0) found = prior
|
|
242
|
+
}
|
|
243
|
+
return found
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* @public
|
|
248
|
+
* @author ddj 2026年10月10号
|
|
249
|
+
* 按捕获顺序贪心配对历史抵消记录,每条最多使用一次;桶上限为 200。
|
|
250
|
+
* @param records 工作区记录(不修改输入或记录状态)
|
|
251
|
+
* @returns 可以整对归档的记录对
|
|
252
|
+
*/
|
|
253
|
+
export function findCanceledPairs(records: DiffRecord[]): Array<[DiffRecord, DiffRecord]> {
|
|
254
|
+
const available: DiffRecord[] = []
|
|
255
|
+
const pairs: Array<[DiffRecord, DiffRecord]> = []
|
|
256
|
+
const ordered = [...records].sort(recordOrder)
|
|
257
|
+
for (const record of ordered) {
|
|
258
|
+
const prior = findInversePrior(available, record)
|
|
259
|
+
if (!prior) {
|
|
260
|
+
if (fullyPending(record)) available.push(record)
|
|
261
|
+
continue
|
|
262
|
+
}
|
|
263
|
+
pairs.push([prior, record])
|
|
264
|
+
available.splice(available.indexOf(prior), 1)
|
|
265
|
+
}
|
|
266
|
+
return pairs
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// #endregion
|
|
270
|
+
|
|
160
271
|
/** 文件当前最大批次号(无记录为 0)。 */
|
|
161
272
|
export function fileMaxBatch(records: Map<string, DiffRecord>, path: string): number {
|
|
162
273
|
let m = 0
|
package/src/openBridge.ts
CHANGED
|
@@ -56,6 +56,19 @@ export async function ensureOpenBridge(ctx: Ctx, baseUrl: string): Promise<strin
|
|
|
56
56
|
return join(dir, 'dsh-open.ini')
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/** @public @author ddj 2026年10月12号
|
|
60
|
+
* Provision the active profile's independent bridge at startup (desktop or web).
|
|
61
|
+
* Mirrors unityInstall's per-profile bridge without requiring a registered project,
|
|
62
|
+
* so Unity can select either target. Unknown profile returns null instead of failing.
|
|
63
|
+
* @param ctx Active host context.
|
|
64
|
+
* @param baseUrl Selected public Web URL (unused for desktop wake).
|
|
65
|
+
* @returns Absolute profile bridge INI path, or null when no owned profile is active.
|
|
66
|
+
*/
|
|
67
|
+
export async function ensureActiveBridge(ctx: Ctx, baseUrl: string): Promise<string | null> {
|
|
68
|
+
if (!findProfileDir(ctx)) return null
|
|
69
|
+
return ensureOpenBridge(ctx, baseUrl)
|
|
70
|
+
}
|
|
71
|
+
|
|
59
72
|
/** @private @author ddj 2026年09月28号
|
|
60
73
|
* Replace a bridge file atomically without following a preexisting file link.
|
|
61
74
|
* @param path Destination in the validated bridge directory.
|
package/src/rpc.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
READ_CAP,
|
|
14
14
|
appendArchiveEntries,
|
|
15
15
|
archiveRecords,
|
|
16
|
+
archiveCanceled,
|
|
16
17
|
autoArchiveStale,
|
|
17
18
|
policyOf,
|
|
18
19
|
resolveTarget,
|
|
@@ -460,7 +461,7 @@ async function latestPatchBackup(patchPath: string): Promise<string | undefined>
|
|
|
460
461
|
}
|
|
461
462
|
|
|
462
463
|
/** 读取设置中的深链基址(缺省/非法回退默认 3080)。 */
|
|
463
|
-
async function integrationBaseUrlOf(ctx: Ctx): Promise<string> {
|
|
464
|
+
export async function integrationBaseUrlOf(ctx: Ctx): Promise<string> {
|
|
464
465
|
const settings = ctx.get('settings')
|
|
465
466
|
const value = sectionOf(settings, FILE_OPEN_SETTINGS_NS)?.value as { integrationBaseUrl?: unknown } | undefined
|
|
466
467
|
const raw = typeof value?.integrationBaseUrl === 'string' ? value.integrationBaseUrl.trim() : ''
|
|
@@ -613,6 +614,7 @@ export function buildHandlers(
|
|
|
613
614
|
const staleStart = performance.now()
|
|
614
615
|
// #endregion
|
|
615
616
|
await autoArchiveStale(ctx, sc.session, sc.cwd, bucket)
|
|
617
|
+
await archiveCanceled(ctx, sc.session, sc.cwd, bucket)
|
|
616
618
|
// #region debug log
|
|
617
619
|
staleMs = performance.now() - staleStart
|
|
618
620
|
// #endregion
|