dsh-screenshot-capture 0.2.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/package.json ADDED
@@ -0,0 +1,80 @@
1
+ {
2
+ "name": "dsh-screenshot-capture",
3
+ "version": "0.2.0",
4
+ "description": "Point-and-shoot screenshot capture for DeepSeek Harness: clipboard watcher + system floating window (comment & key-point, copy/save-doc/save-image) + instant OCR + Obsidian per-day merging + evening AI organization. 指哪拍哪 · 截图即存:剪贴板监听 + 鼠标位置系统级悬浮窗 + 即时 OCR + Obsidian 按天合并 + 晚间 AI 整理打双链",
5
+ "type": "module",
6
+ "main": "index.mjs",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/wangzhanchao883/dsh-screenshot-capture.git"
10
+ },
11
+ "homepage": "https://github.com/wangzhanchao883/dsh-screenshot-capture",
12
+ "bugs": {
13
+ "url": "https://github.com/wangzhanchao883/dsh-screenshot-capture/issues"
14
+ },
15
+ "author": "wangzhanchao883",
16
+ "exports": {
17
+ ".": "./index.mjs",
18
+ "./client": "./client.js",
19
+ "./package.json": "./package.json"
20
+ },
21
+ "files": [
22
+ "index.mjs",
23
+ "client.js",
24
+ "core.mjs",
25
+ "config.mjs",
26
+ "clipboard.mjs",
27
+ "storage.mjs",
28
+ "ocr.mjs",
29
+ "organize.mjs",
30
+ "scripts/",
31
+ "cordis.patch.yml",
32
+ "config.example.json",
33
+ "README.md",
34
+ "screenshots.json"
35
+ ],
36
+ "dsh": {
37
+ "bundle": {
38
+ "patch": "./cordis.patch.yml"
39
+ },
40
+ "client": {
41
+ "inject": [
42
+ "@deepseek-ai/dsh-client-locale",
43
+ "@deepseek-ai/dsh-client-runtime",
44
+ "@deepseek-ai/dsh-client-ui-settings"
45
+ ],
46
+ "platform": "web"
47
+ }
48
+ },
49
+ "scripts": {
50
+ "dev": "node dev-run.mjs",
51
+ "dev:auto": "node dev-run.mjs --auto doc",
52
+ "test": "node test/test-format.mjs && node test/test-storage.mjs",
53
+ "test:ocr": "node test/test-ocr.mjs"
54
+ },
55
+ "dependencies": {
56
+ "@deepseek-ai/schemastery": "^3.18.1"
57
+ },
58
+ "peerDependencies": {
59
+ "@deepseek-ai/dsh-tools": ">=0.0.1-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.2.0-0"
60
+ },
61
+ "engines": {
62
+ "node": "^22.0.0 || >=24"
63
+ },
64
+ "keywords": [
65
+ "deepseek-harness",
66
+ "dsh",
67
+ "dsh-plugin",
68
+ "plugin",
69
+ "screenshot",
70
+ "screenshot-capture",
71
+ "clipboard",
72
+ "ocr",
73
+ "obsidian",
74
+ "windows",
75
+ "winforms",
76
+ "productivity",
77
+ "automation"
78
+ ],
79
+ "license": "MIT"
80
+ }
@@ -0,0 +1,5 @@
1
+ [
2
+ "assets/screenshots/settings.png",
3
+ "assets/screenshots/floating-window.png",
4
+ "assets/screenshots/obsidian-note.png"
5
+ ]
@@ -0,0 +1,200 @@
1
+ <#
2
+ dsh-screenshot-capture · 剪贴板监听 + 系统级悬浮窗 (PowerShell 5.1, 零依赖)
3
+
4
+ 职责:
5
+ 1. 轮询剪贴板序列号(GetClipboardSequenceNumber),检测新图片(截图/Ctrl+C 图片)
6
+ 2. 检测到后:原图存到 $env:TEMP\dsh-capture\clip_<ts>.png
7
+ 3. 在鼠标当前位置弹出置顶小窗:图片预览 + 注释输入框 + 「重点」复选框
8
+ + 【复制截图】【存文档】【存图片】
9
+ 4. 结果写入事件日志文件(每行一个 JSON),也尝试写 stdout
10
+
11
+ 协议(每行一个 JSON,写日志 + stdout):
12
+ {"t":"ready"}
13
+ {"t":"img","path":"...","seq":123}
14
+ {"t":"choice","action":"doc|img|copy","path":"...","note":"用户注释","isKey":true}
15
+ {"t":"err","msg":"..."}
16
+
17
+ 参数:
18
+ -ConfigPath JSON 配置文件(可选): pollIntervalMs / cooldownMs / offsetX / offsetY / previewMaxWidth
19
+ -AutoAction doc|img|copy|none 自动选择,不弹窗(自动化测试用);none=只检测不处理
20
+ -Once 只处理一次后退出
21
+ #>
22
+ param(
23
+ [string]$ConfigPath = "",
24
+ [string]$AutoAction = "",
25
+ [switch]$Once
26
+ )
27
+ $ErrorActionPreference = "Stop"
28
+ Add-Type -AssemblyName System.Windows.Forms
29
+ Add-Type -AssemblyName System.Drawing
30
+
31
+ Add-Type @"
32
+ using System;
33
+ using System.Runtime.InteropServices;
34
+ public static class WinClip {
35
+ [DllImport("user32.dll")]
36
+ public static extern uint GetClipboardSequenceNumber();
37
+ }
38
+ "@
39
+
40
+ $tempDir = Join-Path $env:TEMP "dsh-capture"
41
+ New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
42
+ $logPath = Join-Path $tempDir "events.log"
43
+
44
+ function Write-Event([hashtable]$obj) {
45
+ $line = $obj | ConvertTo-Json -Compress
46
+ try { [System.IO.File]::AppendAllText($logPath, $line + [Environment]::NewLine, [System.Text.Encoding]::UTF8) } catch {}
47
+ try { [Console]::Out.WriteLine($line); [Console]::Out.Flush() } catch {}
48
+ }
49
+
50
+ $config = @{ pollIntervalMs = 200; cooldownMs = 2000; offsetX = 16; offsetY = 16; previewMaxWidth = 320 }
51
+ if ($ConfigPath -and (Test-Path $ConfigPath)) {
52
+ try {
53
+ $cfg = Get-Content $ConfigPath -Raw | ConvertFrom-Json
54
+ foreach ($k in @('pollIntervalMs','cooldownMs','offsetX','offsetY','previewMaxWidth')) {
55
+ if ($null -ne $cfg.$k) { $config[$k] = $cfg.$k }
56
+ }
57
+ } catch { Write-Event @{ t = "err"; msg = "config parse: $($_.Exception.Message)" } }
58
+ }
59
+
60
+ function Show-CaptureDialog {
61
+ param([string]$ImagePath)
62
+ $form = New-Object System.Windows.Forms.Form
63
+ $img = $null
64
+ try {
65
+ $form.Text = "截图入库"
66
+ $form.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::FixedToolWindow
67
+ $form.StartPosition = [System.Windows.Forms.FormStartPosition]::Manual
68
+ $form.TopMost = $true
69
+ $form.ShowInTaskbar = $false
70
+ $form.KeyPreview = $true
71
+ $form.MaximizeBox = $false
72
+ $form.MinimizeBox = $false
73
+
74
+ $img = [System.Drawing.Image]::FromFile($ImagePath)
75
+ $maxW = [int]$config.previewMaxWidth
76
+ $w = [Math]::Max(1, $img.Width)
77
+ $h = [Math]::Max(1, $img.Height)
78
+ $scale = [Math]::Min(1.0, $maxW / $w)
79
+ $pw = [Math]::Max(160, [int]($w * $scale))
80
+ $ph = [Math]::Max(100, [int]($h * $scale))
81
+
82
+ $pic = New-Object System.Windows.Forms.PictureBox
83
+ $pic.Width = $pw
84
+ $pic.Height = $ph
85
+ $pic.Image = $img
86
+ $pic.SizeMode = [System.Windows.Forms.PictureBoxSizeMode]::Zoom
87
+ $pic.Location = New-Object System.Drawing.Point(8, 8)
88
+
89
+ $bw = 88; $bh = 32; $pad = 8; $gap = 6
90
+ $totalW = $pad * 2 + $bw * 3 + $gap * 2
91
+ $noteW = $totalW - $pad * 2
92
+ $noteH = 60
93
+ $noteY = 8 + $ph + 8
94
+ $chkY = $noteY + $noteH + 6
95
+ $rowY = $chkY + 24 + 10
96
+ $totalH = $rowY + $bh + $pad
97
+
98
+ $noteBox = New-Object System.Windows.Forms.TextBox
99
+ $noteBox.Multiline = $true
100
+ $noteBox.AcceptsReturn = $true
101
+ $noteBox.ScrollBars = [System.Windows.Forms.ScrollBars]::Vertical
102
+ $noteBox.Width = $noteW
103
+ $noteBox.Height = $noteH
104
+ $noteBox.Location = New-Object System.Drawing.Point($pad, $noteY)
105
+ $noteBox.Font = New-Object System.Drawing.Font("Microsoft YaHei", 9)
106
+ try { $noteBox.PlaceholderText = "截图注释 / 评论(可选)…" } catch {}
107
+
108
+ $chkKey = New-Object System.Windows.Forms.CheckBox
109
+ $chkKey.Text = "重点"
110
+ $chkKey.Width = 80; $chkKey.Height = 24
111
+ $chkKey.Location = New-Object System.Drawing.Point($pad, $chkY)
112
+ $chkKey.Font = New-Object System.Drawing.Font("Microsoft YaHei", 9)
113
+ $chkKey.Checked = $false
114
+
115
+ $bCopy = New-Object System.Windows.Forms.Button
116
+ $bCopy.Text = "复制截图"; $bCopy.Tag = "copy"; $bCopy.Width = $bw; $bCopy.Height = $bh
117
+ $bCopy.Location = New-Object System.Drawing.Point($pad, $rowY)
118
+ $bCopy.Add_Click({ $form.Tag = "copy"; $form.DialogResult = [System.Windows.Forms.DialogResult]::Cancel })
119
+
120
+ $bDoc = New-Object System.Windows.Forms.Button
121
+ $bDoc.Text = "存文档"; $bDoc.Tag = "doc"; $bDoc.Width = $bw; $bDoc.Height = $bh
122
+ $bDoc.Location = New-Object System.Drawing.Point(($pad + $bw + $gap), $rowY)
123
+ $bDoc.Add_Click({ $form.Tag = "doc"; $form.DialogResult = [System.Windows.Forms.DialogResult]::OK })
124
+
125
+ $bImg = New-Object System.Windows.Forms.Button
126
+ $bImg.Text = "存图片"; $bImg.Tag = "img"; $bImg.Width = $bw; $bImg.Height = $bh
127
+ $bImg.Location = New-Object System.Drawing.Point(($pad + ($bw + $gap) * 2), $rowY)
128
+ $bImg.Add_Click({ $form.Tag = "img"; $form.DialogResult = [System.Windows.Forms.DialogResult]::Yes })
129
+
130
+ $form.AcceptButton = $bDoc
131
+ $form.CancelButton = $bCopy
132
+ $form.Add_KeyDown({ param($s, $e) if ($e.KeyCode -eq [System.Windows.Forms.Keys]::Escape) { $form.Tag = "copy"; $form.DialogResult = [System.Windows.Forms.DialogResult]::Cancel } })
133
+
134
+ $form.Controls.AddRange(@($pic, $noteBox, $chkKey, $bCopy, $bDoc, $bImg))
135
+ $form.ClientSize = New-Object System.Drawing.Size($totalW, $totalH)
136
+
137
+ $cursor = [System.Windows.Forms.Cursor]::Position
138
+ $screen = [System.Windows.Forms.Screen]::FromPoint($cursor)
139
+ $wa = $screen.WorkingArea
140
+ $x = $cursor.X + [int]$config.offsetX
141
+ $y = $cursor.Y + [int]$config.offsetY
142
+ if ($x + $totalW -gt $wa.Right) { $x = $cursor.X - $totalW - [int]$config.offsetX }
143
+ if ($y + $totalH -gt $wa.Bottom) { $y = $cursor.Y - $totalH - [int]$config.offsetY }
144
+ $x = [Math]::Max($wa.Left, $x)
145
+ $y = [Math]::Max($wa.Top, $y)
146
+ $form.Location = New-Object System.Drawing.Point($x, $y)
147
+
148
+ [void]$form.ShowDialog()
149
+ $action = if ($form.Tag) { $form.Tag.ToString() } else { "copy" }
150
+ $note = $noteBox.Text.Trim()
151
+ $isKey = [bool]$chkKey.Checked
152
+ return @{ action = $action; note = $note; isKey = $isKey }
153
+ } finally {
154
+ if ($null -ne $form) { $form.Dispose() }
155
+ if ($null -ne $img) { $img.Dispose() }
156
+ }
157
+ }
158
+
159
+ Write-Event @{ t = "ready" }
160
+ $lastSeq = [WinClip]::GetClipboardSequenceNumber()
161
+ $lastHandled = [DateTime]::Now
162
+
163
+ while ($true) {
164
+ Start-Sleep -Milliseconds ([int]$config.pollIntervalMs)
165
+ $seq = 0
166
+ try { $seq = [WinClip]::GetClipboardSequenceNumber() } catch { continue }
167
+ if ($seq -eq $lastSeq) { continue }
168
+ $lastSeq = $seq
169
+ $now = [DateTime]::Now
170
+ if (($now - $lastHandled).TotalMilliseconds -lt [int]$config.cooldownMs) { continue }
171
+ if (-not [System.Windows.Forms.Clipboard]::ContainsImage()) { continue }
172
+ $img = $null
173
+ try { $img = [System.Windows.Forms.Clipboard]::GetImage() } catch { continue }
174
+ if ($null -eq $img) { continue }
175
+ $lastHandled = $now
176
+ $ts = Get-Date -Format "yyyyMMdd_HHmmss_fff"
177
+ $pngPath = Join-Path $tempDir ("clip_{0}.png" -f $ts)
178
+ try {
179
+ $img.Save($pngPath, [System.Drawing.Imaging.ImageFormat]::Png)
180
+ } catch {
181
+ $img.Dispose()
182
+ Write-Event @{ t = "err"; msg = "save: $($_.Exception.Message)" }
183
+ continue
184
+ }
185
+ $img.Dispose()
186
+ Write-Event @{ t = "img"; path = $pngPath; seq = $seq }
187
+
188
+ if ($AutoAction) {
189
+ if ($AutoAction -ne "none") { Write-Event @{ t = "choice"; action = $AutoAction; path = $pngPath } }
190
+ if ($Once) { break }
191
+ continue
192
+ }
193
+
194
+ $res = @{ action = "copy"; note = ""; isKey = $false }
195
+ try { $res = Show-CaptureDialog -ImagePath $pngPath } catch {
196
+ Write-Event @{ t = "err"; msg = "dialog: $($_.Exception.Message)" }
197
+ }
198
+ Write-Event @{ t = "choice"; action = $res.action; path = $pngPath; note = $res.note; isKey = $res.isKey }
199
+ if ($Once) { break }
200
+ }
package/storage.mjs ADDED
@@ -0,0 +1,126 @@
1
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync, renameSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { ensureVault } from "./config.mjs";
4
+
5
+ export const KIND = { DOC: "文档", IMG: "图片" };
6
+
7
+ export function stampParts(now = new Date()) {
8
+ const pad = (n) => String(n).padStart(2, "0");
9
+ const date = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
10
+ const fileStamp = `${date.replaceAll("-", "")}_${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
11
+ const time = `${pad(now.getHours())}:${pad(now.getMinutes())}`;
12
+ return { date, fileStamp, time };
13
+ }
14
+
15
+ export function dailyNotePath(config, date) {
16
+ return join(config.vaultPath, config.inboxFolder, `${date}.md`);
17
+ }
18
+
19
+ export function attachmentRelPath(config, date, fileStamp, kind) {
20
+ return `${config.attachmentsFolder}/${fileStamp}_${kind}.png`;
21
+ }
22
+
23
+ /** 把剪贴板临时图复制进 vault 附件,返回 vault 内相对路径 */
24
+ export function saveAttachment(config, srcPath, date, fileStamp, kind) {
25
+ ensureVault(config);
26
+ const rel = attachmentRelPath(config, date, fileStamp, kind);
27
+ const dest = join(config.vaultPath, config.inboxFolder, rel);
28
+ copyFileSync(srcPath, dest);
29
+ return rel;
30
+ }
31
+
32
+ const NOTE_HEADER = `# {date} 收件箱
33
+
34
+ > 每日截图收件箱,晚间整理后归档。\`#文档\` = 含 OCR 文字,\`#图片\` = 纯图片。
35
+
36
+ `;
37
+
38
+ function ensureDailyNote(config, date) {
39
+ const path = dailyNotePath(config, date);
40
+ if (!existsSync(path)) {
41
+ writeFileSync(path, NOTE_HEADER.replace("{date}", date), "utf8");
42
+ }
43
+ return path;
44
+ }
45
+
46
+ /** 向当天笔记追加一条记录;OCR 结果可为 null(占位待更新),note/isKey 为悬浮窗注释与重点标记 */
47
+ export function appendEntry(config, { date, time, kind, imageRel, ocrText = null, note = "", isKey = false }) {
48
+ const path = ensureDailyNote(config, date);
49
+ const block = [
50
+ "",
51
+ `## ${time} #${kind}`,
52
+ "",
53
+ `![${imageRel.split("/").pop()}](<${imageRel}>)`,
54
+ "",
55
+ ];
56
+ if (kind === KIND.DOC) {
57
+ block.push(ocrText === null ? "> OCR: 识别中…" : `> OCR: ${ocrText || "(无文字)"}`, "");
58
+ }
59
+ if (note || isKey) {
60
+ if (isKey) block.push("# **重点**", "");
61
+ if (note) block.push(note, "");
62
+ }
63
+ const existing = readFileSync(path, "utf8");
64
+ const updated = existing.replace(/\n*$/, "\n") + block.join("\n") + "\n";
65
+ writeFileSync(path, updated, "utf8");
66
+ return path;
67
+ }
68
+
69
+ /** 更新某条记录的 OCR 文字(按时间+图片文件名定位占位行) */
70
+ export function updateEntryOcr(config, { date, time, imageRel, ocrText }) {
71
+ const path = dailyNotePath(config, date);
72
+ if (!existsSync(path)) return null;
73
+ const text = readFileSync(path, "utf8");
74
+ const fileName = imageRel.split("/").pop();
75
+ const marker = "> OCR: 识别中…";
76
+ const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
77
+ const re = new RegExp(
78
+ `(## ${time} #文档\\n\\n!\\[[^\\]]*${esc(fileName)}[^\\]]*\\]\\([^)]*\\)\\n\\n)(> OCR: 识别中…)`,
79
+ );
80
+ if (!re.test(text)) return null;
81
+ const updated = text.replace(re, `$1> OCR: ${ocrText || "(无文字)"}`);
82
+ writeFileSync(path, updated, "utf8");
83
+ return path;
84
+ }
85
+
86
+ /** 解析当天笔记为条目列表 */
87
+ export function parseEntries(noteText) {
88
+ const entries = [];
89
+ const chunks = noteText.split(/^## /m).slice(1);
90
+ for (const chunk of chunks) {
91
+ const header = chunk.match(/^(\d{2}:\d{2}) (#文档|#图片)/);
92
+ if (!header) continue;
93
+ const [, time, tag] = header;
94
+ const body = chunk.slice(header[0].length);
95
+ const img = body.match(/!\[[^\]]*\]\(<([^>]+)>\)/)?.[1] ?? null;
96
+ const ocr = body.match(/^> OCR: (.*)$/m)?.[1] ?? null;
97
+ const isKey = /^# \*\*重点\*\*\s*$/m.test(body);
98
+ const noteLines = body
99
+ .split(/\r?\n/)
100
+ .map((l) => l.trim())
101
+ .filter((l) => l && !l.startsWith("![") && !l.startsWith("> OCR") && l !== "# **重点**");
102
+ const note = noteLines.length ? noteLines.join("\n") : null;
103
+ entries.push({ time, kind: tag.slice(1), imageRel: img, ocrText: ocr, note, isKey });
104
+ }
105
+ return entries;
106
+ }
107
+
108
+ export function readDailyNote(config, date) {
109
+ const path = dailyNotePath(config, date);
110
+ return existsSync(path) ? readFileSync(path, "utf8") : "";
111
+ }
112
+
113
+ /** 归档:当天收件箱笔记移动到 归档 文件夹 */
114
+ export function archiveDailyNote(config, date) {
115
+ ensureVault(config);
116
+ const src = dailyNotePath(config, date);
117
+ if (!existsSync(src)) return null;
118
+ const dest = join(config.vaultPath, config.archiveFolder, `${date}.md`);
119
+ renameSync(src, dest);
120
+ return dest;
121
+ }
122
+
123
+ export function todayString(now = new Date()) {
124
+ const pad = (n) => String(n).padStart(2, "0");
125
+ return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
126
+ }