dsh-copilot-vision 1.1.0 → 1.3.1

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.
@@ -0,0 +1,131 @@
1
+ // 语义级记忆压缩:把关键截图归档,其余临时碎片一次清空。
2
+ //
3
+ // 与 paths.js 的 cleanupVisualFragments 的分工(两者不冲突):
4
+ // - cleanupVisualFragments 是「按时间的兜底」:只在插件启动时删 3 天前的 diff/grid,
5
+ // 永远不碰 vp-shot-*,永远不递归子目录。
6
+ // - 本函数是「按语义的主清理」:由模型在收工时显式调用,删所有 diff/grid 和没被保留的 vp-shot-*。
7
+ // 主清理的目标集合是兜底清理的超集,先跑谁都不会破坏另一方。
8
+ //
9
+ // 绝对不动的三样:audit.log(证据链)、shot-index.json(shotId 播种与相对点击依赖它)、archives/(归档本体)。
10
+ import { existsSync } from 'node:fs'
11
+ import { appendFile, mkdir, readdir, rename, stat, unlink } from 'node:fs/promises'
12
+ import { basename, join } from 'node:path'
13
+ import { resolveShotsDir } from './paths.js'
14
+ import { get, relocate } from './shot-index.js'
15
+
16
+ export const ARCHIVE_DIR_NAME = 'archives'
17
+ export const SUMMARY_FILE_NAME = 'archive-summary.txt'
18
+
19
+ export function archiveDir() {
20
+ return join(resolveShotsDir(), ARCHIVE_DIR_NAME)
21
+ }
22
+
23
+ export function summaryPath() {
24
+ return join(resolveShotsDir(), SUMMARY_FILE_NAME)
25
+ }
26
+
27
+ const DIFF_RE = /^vp-diff-\d+-\d+\.png$/
28
+ const GRID_RE = /^vp-grid-\d+-\d+\.png$/
29
+ const SHOT_RE = /^vp-shot-.*\.png$/
30
+
31
+ /**
32
+ * 收工清理。
33
+ * @param {{keepShotIds: number[], summary: string}} args
34
+ * @returns 归档/缺失清单与各类删除计数(供工具层回显)。
35
+ */
36
+ export async function summarizeAction({ keepShotIds = [], summary } = {}) {
37
+ if (!Array.isArray(keepShotIds) || keepShotIds.some((id) => !Number.isInteger(id))) {
38
+ throw new Error(`vp_summarize: keepShotIds 必须是整数数组,收到 ${JSON.stringify(keepShotIds)}`)
39
+ }
40
+ if (typeof summary !== 'string' || summary.trim() === '') {
41
+ throw new Error('vp_summarize: summary 必须是非空字符串')
42
+ }
43
+
44
+ const dir = resolveShotsDir()
45
+ const archives = archiveDir()
46
+ await mkdir(archives, { recursive: true })
47
+
48
+ const archivedShotIds = []
49
+ const missingShotIds = []
50
+ const failedShotIds = []
51
+ const keepNames = new Set()
52
+
53
+ // 先归档再删除:顺序反了会把要保留的图当成「没被选中」删掉。
54
+ for (const shotId of new Set(keepShotIds)) {
55
+ const meta = get(shotId)
56
+ if (meta === undefined) {
57
+ missingShotIds.push(shotId)
58
+ continue
59
+ }
60
+ const name = basename(meta.path)
61
+ const sourcePath = join(dir, name)
62
+ const archivedPath = join(archives, name)
63
+
64
+ try {
65
+ if (existsSync(sourcePath)) {
66
+ await rename(sourcePath, archivedPath)
67
+ }
68
+ } catch {
69
+ // 归档失败(文件被别的程序占用等)时绝不能继续往下走:
70
+ // 删循环会把仍在原位的源文件当成「没被保留」删掉。先把它记进 keepNames 保命。
71
+ keepNames.add(name)
72
+ failedShotIds.push(shotId)
73
+ continue
74
+ }
75
+ // 源和归档都不在,说明这张图已经丢过一次了,如实报缺失。
76
+ if (!existsSync(archivedPath)) {
77
+ missingShotIds.push(shotId)
78
+ continue
79
+ }
80
+
81
+ relocate(shotId, archivedPath)
82
+ keepNames.add(name)
83
+ archivedShotIds.push(shotId)
84
+ }
85
+
86
+ const block = [
87
+ `[${new Date().toISOString()}] keep=${archivedShotIds.join(',') || '(none)'}`,
88
+ summary.trim(),
89
+ '',
90
+ ].join('\n')
91
+ await appendFile(summaryPath(), block, 'utf8')
92
+
93
+ let removedShots = 0
94
+ let removedDiff = 0
95
+ let removedGrid = 0
96
+ let failedDeletes = 0
97
+
98
+ // 只扫顶层:archives/ 是目录,正则匹配不到;即便将来名字撞上,isFile 检查也会挡住。
99
+ for (const name of await readdir(dir)) {
100
+ const isDiff = DIFF_RE.test(name)
101
+ const isGrid = GRID_RE.test(name)
102
+ const isShot = SHOT_RE.test(name)
103
+ if (!isDiff && !isGrid && !isShot) continue
104
+ if (isShot && keepNames.has(name)) continue
105
+ try {
106
+ const info = await stat(join(dir, name))
107
+ if (!info.isFile()) continue
108
+ await unlink(join(dir, name))
109
+ if (isDiff) removedDiff++
110
+ else if (isGrid) removedGrid++
111
+ else removedShots++
112
+ } catch {
113
+ failedDeletes++
114
+ }
115
+ }
116
+
117
+ return {
118
+ summary: summary.trim(),
119
+ archivedShotIds,
120
+ missingShotIds,
121
+ failedShotIds,
122
+ failedArchives: failedShotIds.length,
123
+ removedShots,
124
+ removedDiff,
125
+ removedGrid,
126
+ removedTotal: removedShots + removedDiff + removedGrid,
127
+ failedDeletes,
128
+ archivesDir: archives,
129
+ summaryPath: summaryPath(),
130
+ }
131
+ }
package/src/wait.js CHANGED
@@ -13,7 +13,7 @@ import { readFileSync } from 'node:fs'
13
13
  import { PNG } from 'pngjs'
14
14
  import pixelmatch from 'pixelmatch'
15
15
  import { loadRobotTs } from './robot-ts.js'
16
- import { get } from './shot-index.js'
16
+ import { get, assertShotFileExists } from './shot-index.js'
17
17
  import { screenshotAction } from './actions.js'
18
18
  import { diffPair, connectedComponents, DEFAULT_MIN_REGION_AREA } from './diff.js'
19
19
 
@@ -72,6 +72,7 @@ export async function waitChangeAction({
72
72
  `vp_wait_change: baseline shotId ${baseline} 不存在,可先用 vp_screenshot 截一张`,
73
73
  )
74
74
  }
75
+ assertShotFileExists(baseline, baselineMeta, 'vp_wait_change')
75
76
  const baselineImg = readPng(baselineMeta.path)
76
77
 
77
78
  const { Session } = loadRobotTs()