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/LICENSE +21 -0
- package/README.md +170 -0
- package/client.js +261 -0
- package/clipboard.mjs +146 -0
- package/config.example.json +21 -0
- package/config.mjs +79 -0
- package/cordis.patch.yml +5 -0
- package/core.mjs +65 -0
- package/index.mjs +272 -0
- package/ocr.mjs +55 -0
- package/organize.mjs +155 -0
- package/package.json +80 -0
- package/screenshots.json +5 -0
- package/scripts/clip-dialog.ps1 +200 -0
- package/storage.mjs +126 -0
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
|
+
}
|
package/screenshots.json
ADDED
|
@@ -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
|
+
``,
|
|
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
|
+
}
|