@pointylab/dsh 0.0.0-stage → 0.4.3

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.
Files changed (88) hide show
  1. package/ASSETS.md +20 -0
  2. package/LICENSE +21 -0
  3. package/README.md +85 -2
  4. package/cordis.patch.yml +13 -0
  5. package/lib/client.js +33 -0
  6. package/package.json +38 -4
  7. package/pet/app/card/card.css +365 -0
  8. package/pet/app/card/card.html +68 -0
  9. package/pet/app/card/card.js +896 -0
  10. package/pet/app/card/markdown.js +64 -0
  11. package/pet/app/card/mascot/blink.png +0 -0
  12. package/pet/app/card/mascot/done.png +0 -0
  13. package/pet/app/card/mascot/error.png +0 -0
  14. package/pet/app/card/mascot/idle.png +0 -0
  15. package/pet/app/card/mascot/peek.png +0 -0
  16. package/pet/app/card/mascot/think.png +0 -0
  17. package/pet/app/card/preload.js +21 -0
  18. package/pet/app/fullscreen.js +131 -0
  19. package/pet/app/main.js +1252 -0
  20. package/pet/app/native/uia-selection.ps1 +158 -0
  21. package/pet/app/notice/notice.html +37 -0
  22. package/pet/app/notice/notice.js +31 -0
  23. package/pet/app/notice/preload.js +7 -0
  24. package/pet/app/overlay/overlay.html +30 -0
  25. package/pet/app/overlay/overlay.js +42 -0
  26. package/pet/app/overlay/preload.js +7 -0
  27. package/pet/app/pill/pill.css +73 -0
  28. package/pet/app/pill/pill.html +11 -0
  29. package/pet/app/pill/pill.js +35 -0
  30. package/pet/app/pill/preload.js +8 -0
  31. package/pet/app/preload.js +16 -0
  32. package/pet/app/renderer/index.html +24 -0
  33. package/pet/app/renderer/lines.js +25 -0
  34. package/pet/app/renderer/pet.js +292 -0
  35. package/pet/app/renderer/renderer2d.js +71 -0
  36. package/pet/app/renderer/style.css +157 -0
  37. package/pet/app/selection.js +246 -0
  38. package/pet/app/shake.js +76 -0
  39. package/pet/app/uipick.js +314 -0
  40. package/pet/assets/cursor/cursor-32.cur +0 -0
  41. package/pet/assets/cursor/cursor-32.png +0 -0
  42. package/pet/assets/cursor/cursor-48.cur +0 -0
  43. package/pet/assets/cursor/cursor-48.png +0 -0
  44. package/pet/assets/cursor/cursor-64.cur +0 -0
  45. package/pet/assets/cursor/cursor-64.png +0 -0
  46. package/pet/assets/cursor/cursor-clean.png +0 -0
  47. package/pet/assets/cursor/make_cursor.py +85 -0
  48. package/pet/assets/cursor/source.png +0 -0
  49. package/pet/assets/pointy.ico +0 -0
  50. package/pet/assets/tray.png +0 -0
  51. package/pet/assets/tray@2x.png +0 -0
  52. package/pet/boot.js +6 -0
  53. package/pet/extension/background.js +252 -0
  54. package/pet/extension/cursor-32.png +0 -0
  55. package/pet/extension/cursor-48.png +0 -0
  56. package/pet/extension/cursor-64.png +0 -0
  57. package/pet/extension/icon.png +0 -0
  58. package/pet/extension/manifest.json +12 -0
  59. package/pet/extension/pick.js +391 -0
  60. package/pet/extension/popup.html +29 -0
  61. package/pet/extension/popup.js +30 -0
  62. package/pet/package.json +11 -0
  63. package/pet/vendor/koffi/LICENSE.txt +22 -0
  64. package/pet/vendor/koffi/build/koffi/win32_x64/koffi.exp +0 -0
  65. package/pet/vendor/koffi/build/koffi/win32_x64/koffi.lib +0 -0
  66. package/pet/vendor/koffi/build/koffi/win32_x64/koffi.node +0 -0
  67. package/pet/vendor/koffi/index.js +634 -0
  68. package/pet/vendor/koffi/indirect.js +533 -0
  69. package/pet/vendor/koffi/package.json +35 -0
  70. package/pet/vendor/ws/LICENSE +20 -0
  71. package/pet/vendor/ws/index.js +22 -0
  72. package/pet/vendor/ws/lib/buffer-util.js +142 -0
  73. package/pet/vendor/ws/lib/constants.js +19 -0
  74. package/pet/vendor/ws/lib/event-target.js +292 -0
  75. package/pet/vendor/ws/lib/extension.js +203 -0
  76. package/pet/vendor/ws/lib/limiter.js +55 -0
  77. package/pet/vendor/ws/lib/permessage-deflate.js +530 -0
  78. package/pet/vendor/ws/lib/receiver.js +743 -0
  79. package/pet/vendor/ws/lib/sender.js +610 -0
  80. package/pet/vendor/ws/lib/stream.js +161 -0
  81. package/pet/vendor/ws/lib/subprotocol.js +62 -0
  82. package/pet/vendor/ws/lib/validation.js +152 -0
  83. package/pet/vendor/ws/lib/websocket-server.js +562 -0
  84. package/pet/vendor/ws/lib/websocket.js +1426 -0
  85. package/pet/vendor/ws/package.json +74 -0
  86. package/pet/vendor/ws/wrapper.mjs +21 -0
  87. package/src/node.js +732 -0
  88. package/src/runtime.js +178 -0
@@ -0,0 +1,158 @@
1
+ # 常驻小进程:从 stdin 读一行命令,用 UI Automation 查一次,输出一行 JSON。只在被叫到时查,不监听任何东西。
2
+ # grab 焦点控件里选中的文字
3
+ # point X Y 坐标下的控件,以及往上几层祖先(高亮框用)
4
+ # element X Y UP 坐标下的控件往上 UP 层,取它的名字、值、文字(确认指认时用)
5
+ # 坐标都是物理像素,所以先把自己设成按显示器感知 DPI
6
+ Add-Type -AssemblyName UIAutomationClient, UIAutomationTypes, WindowsBase
7
+ Add-Type 'using System; using System.Runtime.InteropServices; public static class DshPetDpi { [DllImport("user32.dll")] public static extern bool SetProcessDpiAwarenessContext(IntPtr v); }'
8
+ [DshPetDpi]::SetProcessDpiAwarenessContext([IntPtr](-4)) | Out-Null
9
+ [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
10
+ $ErrorActionPreference = 'Stop'
11
+ # 类型要在程序集加载之后按名字取;直接写 [System.Windows.Automation.X] 会在脚本解析时就报找不到
12
+ $AutomationElement = 'System.Windows.Automation.AutomationElement' -as [type]
13
+ $TextPattern = 'System.Windows.Automation.TextPattern' -as [type]
14
+ $ValuePattern = 'System.Windows.Automation.ValuePattern' -as [type]
15
+ $TreeWalker = 'System.Windows.Automation.TreeWalker' -as [type]
16
+ $MAX_TEXT = 4000
17
+
18
+ function Rect($el) {
19
+ $r = $el.Current.BoundingRectangle
20
+ if ($r.IsEmpty) { return $null }
21
+ return @([int]$r.X, [int]$r.Y, [int]$r.Width, [int]$r.Height)
22
+ }
23
+
24
+ # 顶层窗口里点到的就是窗口本身(Windows Terminal 这类 XAML 宿主窗口常这样):找它里面盖住这个点、带文字接口的控件
25
+ function TextChildAt($win, $x, $y) {
26
+ $cond = New-Object System.Windows.Automation.PropertyCondition($AutomationElement::IsTextPatternAvailableProperty, $true)
27
+ $best = $null
28
+ $bestArea = [double]::MaxValue
29
+ foreach ($e in $win.FindAll([System.Windows.Automation.TreeScope]::Descendants, $cond)) {
30
+ $r = $e.Current.BoundingRectangle
31
+ if ($r.IsEmpty -or -not $r.Contains($x, $y)) { continue }
32
+ $area = $r.Width * $r.Height
33
+ if ($area -lt $bestArea) { $best = $e; $bestArea = $area }
34
+ }
35
+ return $best
36
+ }
37
+
38
+ function Chain($x, $y, $depth) {
39
+ $el = $AutomationElement::FromPoint((New-Object System.Windows.Point $x, $y))
40
+ if ($null -ne $el -and $el.Current.ControlType -eq [System.Windows.Automation.ControlType]::Window -and -not (Readable $el)) {
41
+ $inner = TextChildAt $el $x $y
42
+ if ($null -ne $inner) { $el = $inner }
43
+ }
44
+ $out = @()
45
+ $walker = $TreeWalker::ControlViewWalker
46
+ while ($null -ne $el -and $out.Count -lt $depth) {
47
+ $out += $el
48
+ $parent = $walker.GetParent($el)
49
+ if ($null -eq $parent -or $parent -eq $AutomationElement::RootElement) { break }
50
+ $el = $parent
51
+ }
52
+ return $out
53
+ }
54
+
55
+ # 判断"整个窗口只露出一个空壳"用:有没有子控件、有没有能读文字的接口、所在顶层窗口多大
56
+ function HasKids($el) { return $null -ne $TreeWalker::ControlViewWalker.GetFirstChild($el) }
57
+ function Readable($el) {
58
+ $p = $null
59
+ return $el.TryGetCurrentPattern($TextPattern::Pattern, [ref]$p) -or $el.TryGetCurrentPattern($ValuePattern::Pattern, [ref]$p)
60
+ }
61
+ function WindowRect($el) {
62
+ $walker = $TreeWalker::ControlViewWalker
63
+ $top = $el
64
+ while ($true) {
65
+ $parent = $walker.GetParent($top)
66
+ if ($null -eq $parent -or $parent -eq $AutomationElement::RootElement) { break }
67
+ $top = $parent
68
+ }
69
+ return Rect $top
70
+ }
71
+
72
+ # 控件自身的文字:TextPattern 的整段文字,否则 ValuePattern,否则子孙控件的 Name 拼起来
73
+ function TextOf($el) {
74
+ $p = $null
75
+ if ($el.TryGetCurrentPattern($TextPattern::Pattern, [ref]$p)) {
76
+ # 终端这类缓冲区很长的:整段从头取只会拿到很久以前的输出,改取屏幕上看得见的那部分
77
+ $doc = $p.DocumentRange.GetText($MAX_TEXT + 1)
78
+ if ($doc.Length -le $MAX_TEXT) { return $doc }
79
+ $visible = ($p.GetVisibleRanges() | ForEach-Object { $_.GetText($MAX_TEXT) }) -join "`n"
80
+ if ($visible.Trim()) { return $visible }
81
+ return $doc.Substring(0, $MAX_TEXT)
82
+ }
83
+ if ($el.TryGetCurrentPattern($ValuePattern::Pattern, [ref]$p) -and $p.Current.Value) { return $p.Current.Value }
84
+ $sb = New-Object System.Text.StringBuilder
85
+ $walker = $TreeWalker::ControlViewWalker
86
+ $stack = New-Object System.Collections.Stack
87
+ $stack.Push($el)
88
+ $seen = 0
89
+ while ($stack.Count -gt 0 -and $seen -lt 300 -and $sb.Length -lt $MAX_TEXT) {
90
+ $n = $stack.Pop()
91
+ $seen++
92
+ $name = $n.Current.Name
93
+ if ($name) { [void]$sb.AppendLine($name) }
94
+ $kids = @()
95
+ $c = $walker.GetFirstChild($n)
96
+ while ($null -ne $c) { $kids += $c; $c = $walker.GetNextSibling($c) }
97
+ for ($i = $kids.Count - 1; $i -ge 0; $i--) { $stack.Push($kids[$i]) }
98
+ }
99
+ return $sb.ToString()
100
+ }
101
+
102
+ while ($true) {
103
+ $line = [Console]::In.ReadLine()
104
+ if ($null -eq $line) { break }
105
+ $parts = $line.Trim() -split '\s+'
106
+ $result = @{ ok = $false }
107
+ try {
108
+ switch ($parts[0]) {
109
+ 'grab' {
110
+ $el = $AutomationElement::FocusedElement
111
+ if ($null -ne $el) {
112
+ $result.isPassword = $el.Current.IsPassword
113
+ $result.control = $el.Current.ControlType.ProgrammaticName
114
+ $pattern = $null
115
+ if (-not $result.isPassword -and $el.TryGetCurrentPattern($TextPattern::Pattern, [ref]$pattern)) {
116
+ $ranges = $pattern.GetSelection()
117
+ $result.ok = $true
118
+ $result.text = ($ranges | ForEach-Object { $_.GetText(20000) }) -join "`n"
119
+ }
120
+ }
121
+ }
122
+ 'point' {
123
+ $chain = @(Chain ([double]$parts[1]) ([double]$parts[2]) 8)
124
+ $result.chain = @($chain | ForEach-Object {
125
+ @{ rect = (Rect $_); control = $_.Current.ControlType.ProgrammaticName; cls = $_.Current.ClassName; name = ($_.Current.Name -replace '\s+', ' ').Substring(0, [Math]::Min(60, ($_.Current.Name -replace '\s+', ' ').Length)) }
126
+ })
127
+ # 最深那个控件:是不是空壳(没子控件、没文字接口),以及它所在窗口多大
128
+ if ($chain.Count) {
129
+ $result.leafKids = HasKids $chain[0]
130
+ $result.leafReadable = Readable $chain[0]
131
+ $result.win = WindowRect $chain[0]
132
+ }
133
+ $result.ok = $true
134
+ }
135
+ 'element' {
136
+ $chain = @(Chain ([double]$parts[1]) ([double]$parts[2]) 8)
137
+ $el = $chain[[Math]::Min([int]$parts[3], $chain.Count - 1)]
138
+ $result.isPassword = $el.Current.IsPassword
139
+ if (-not $result.isPassword) {
140
+ $result.name = $el.Current.Name
141
+ $result.control = $el.Current.ControlType.ProgrammaticName
142
+ $result.className = $el.Current.ClassName
143
+ $result.rect = Rect $el
144
+ $result.kids = HasKids $el
145
+ $result.readable = Readable $el
146
+ $result.win = WindowRect $el
147
+ $result.text = (TextOf $el).Trim()
148
+ if ($result.text.Length -gt $MAX_TEXT) { $result.text = $result.text.Substring(0, $MAX_TEXT) }
149
+ $result.ok = $true
150
+ }
151
+ }
152
+ }
153
+ } catch {
154
+ $result.error = $_.Exception.Message
155
+ }
156
+ [Console]::Out.WriteLine(($result | ConvertTo-Json -Compress -Depth 5))
157
+ [Console]::Out.Flush()
158
+ }
@@ -0,0 +1,37 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'">
6
+ <style>
7
+ :root { --bg: #fff; --text: #1f2328; --muted: #6e7781; --line: #e6e8eb; --chip: #f2f3f5; --chip-hover: #e8eaed; --blue: #4d6bfe; --blue-hover: #3f5ce8;
8
+ --shadow: 0 3px 12px rgba(15, 23, 42, 0.08), 0 1px 3px rgba(15, 23, 42, 0.05), 0 0 0 1px rgba(15, 23, 42, 0.05); }
9
+ @media (prefers-color-scheme: dark) {
10
+ :root { --bg: #23252a; --text: #e6e8eb; --muted: #a0a7b1; --line: #34373d; --chip: #2f3237; --chip-hover: #3a3d43; --blue: #6b84ff; --blue-hover: #8096ff;
11
+ --shadow: 0 3px 12px rgba(0, 0, 0, 0.35), 0 1px 3px rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(255, 255, 255, 0.06); }
12
+ }
13
+ [hidden] { display: none !important; }
14
+ html, body { margin: 0; background: transparent; overflow: hidden; font: 14px "Segoe UI Variable Text", "Segoe UI", "Microsoft YaHei UI", sans-serif; color: var(--text); }
15
+ #box { margin: 14px; padding: 14px 16px 12px; background: var(--bg); border-radius: 12px; box-shadow: var(--shadow); position: relative; }
16
+ h1 { font-size: 15px; margin: 0 22px 8px 0; font-weight: 600; }
17
+ p { margin: 0 0 6px; line-height: 1.55; color: var(--muted); }
18
+ p b { color: var(--text); font-weight: 600; }
19
+ #x { position: absolute; top: 8px; right: 8px; width: 24px; height: 24px; border: 0; border-radius: 6px; background: none; color: var(--muted); cursor: pointer; font-size: 15px; }
20
+ #x:hover { background: var(--chip); }
21
+ #buttons { display: flex; gap: 8px; justify-content: flex-end; margin-top: 12px; flex-wrap: wrap; }
22
+ button.b { border: 0; border-radius: 8px; padding: 6px 14px; font: inherit; cursor: pointer; background: var(--chip); color: var(--text); }
23
+ button.b:hover { background: var(--chip-hover); }
24
+ button.b.primary { background: var(--blue); color: #fff; }
25
+ button.b.primary:hover { background: var(--blue-hover); }
26
+ </style>
27
+ </head>
28
+ <body>
29
+ <div id="box">
30
+ <button id="x" title="关闭">✕</button>
31
+ <h1 id="title"></h1>
32
+ <div id="body"></div>
33
+ <div id="buttons"></div>
34
+ </div>
35
+ <script src="notice.js"></script>
36
+ </body>
37
+ </html>
@@ -0,0 +1,31 @@
1
+ // 小提示窗:一个标题、几段话、几个按钮。点了哪个按钮(或关掉)告诉 main.js。
2
+ // 段落里只认 **粗体**,其余按纯文本放进去。
3
+ const $ = (id) => document.getElementById(id)
4
+
5
+ function paragraph(text) {
6
+ const p = document.createElement('p')
7
+ String(text).split(/(\*\*[^*]+\*\*)/).forEach((part) => {
8
+ if (/^\*\*[^*]+\*\*$/.test(part)) {
9
+ const b = document.createElement('b')
10
+ b.textContent = part.slice(2, -2)
11
+ p.append(b)
12
+ } else if (part) p.append(part)
13
+ })
14
+ return p
15
+ }
16
+
17
+ window.notice.onInit((data) => {
18
+ $('title').textContent = data.title ?? ''
19
+ $('body').replaceChildren(...(data.body ?? []).map(paragraph))
20
+ $('buttons').replaceChildren(...(data.buttons ?? []).map((b) => {
21
+ const el = document.createElement('button')
22
+ el.className = b.primary ? 'b primary' : 'b'
23
+ el.textContent = b.label
24
+ el.addEventListener('click', () => window.notice.choose(b.id))
25
+ return el
26
+ }))
27
+ requestAnimationFrame(() => window.notice.resize(Math.ceil(document.documentElement.scrollHeight)))
28
+ })
29
+
30
+ $('x').addEventListener('click', () => window.notice.choose(null))
31
+ document.addEventListener('keydown', (e) => { if (e.key === 'Escape') window.notice.choose(null) })
@@ -0,0 +1,7 @@
1
+ const { contextBridge, ipcRenderer } = require('electron')
2
+
3
+ contextBridge.exposeInMainWorld('notice', {
4
+ onInit: (fn) => ipcRenderer.on('notice-init', (_e, data) => fn(data)),
5
+ choose: (id) => ipcRenderer.send('notice-choose', id),
6
+ resize: (height) => ipcRenderer.send('notice-resize', height),
7
+ })
@@ -0,0 +1,30 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'unsafe-inline'; script-src 'self'">
6
+ <style>
7
+ html, body { margin: 0; height: 100%; background: transparent; overflow: hidden; }
8
+ #box { position: fixed; left: 0; top: 0; opacity: 0; border: 1.5px solid rgba(138, 180, 248, 0.95); background: rgba(138, 180, 248, 0.08); border-radius: 8px; box-sizing: border-box; box-shadow: 0 0 0 3px rgba(138, 180, 248, 0.16), 0 0 16px 4px rgba(138, 180, 248, 0.28); will-change: transform, width, height; }
9
+ /* 空壳程序里跟着鼠标的小框:虚线,和大框同色 */
10
+ #focus { position: fixed; left: 0; top: 0; opacity: 0; border: 1.5px dashed rgba(138, 180, 248, 0.95); background: rgba(138, 180, 248, 0.10); border-radius: 6px; box-sizing: border-box; pointer-events: none; will-change: transform; }
11
+ /* 虚线小框里靠右居中的竖条:上下箭头夹着鼠标滚轮,提示滚轮能调大小 */
12
+ #wheel { position: absolute; right: 8px; top: 50%; transform: translateY(-50%); width: 28px; height: 88px; border-radius: 14px; background: rgba(77, 107, 254, 0.45); box-shadow: 0 0 0 1px rgba(138, 180, 248, 0.6); display: flex; flex-direction: column; align-items: center; justify-content: space-between; padding: 6px 0; box-sizing: border-box; color: rgba(255, 255, 255, 0.92); }
13
+ #wheel svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-linecap: round; }
14
+ #focus.live { transition: transform 120ms ease-out, width 120ms ease-out, height 120ms ease-out, opacity 120ms ease-out; }
15
+ #box.live { transition: transform 170ms cubic-bezier(0.34, 1.2, 0.64, 1), width 170ms cubic-bezier(0.34, 1.2, 0.64, 1), height 170ms cubic-bezier(0.34, 1.2, 0.64, 1), opacity 120ms ease-out; }
16
+ #what { position: fixed; display: none; padding: 2px 8px; border-radius: 6px; background: rgba(60, 64, 67, 0.82); color: #fff; font: 12px "Segoe UI", "Microsoft YaHei UI", sans-serif; white-space: nowrap; }
17
+ /* 屏幕正下方的操作提示,和网页里的一样 */
18
+ #pill { position: fixed; left: 50%; bottom: 48px; transform: translateX(-50%); display: flex; align-items: center; gap: 10px; padding: 10px 22px; border-radius: 999px; background: rgba(32, 33, 36, 0.92); color: #fff; font: 16px "Segoe UI", "Microsoft YaHei UI", sans-serif; white-space: nowrap; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18); }
19
+ kbd { font: inherit; font-size: 14px; padding: 1px 9px; border: 1px solid rgba(255, 255, 255, 0.45); border-radius: 7px; }
20
+ #pill svg { width: 15px; height: 20px; }
21
+ </style>
22
+ </head>
23
+ <body>
24
+ <div id="box"></div>
25
+ <div id="focus"><div id="wheel"><svg viewBox="0 0 16 16" stroke-width="1.6"><path d="M4 10l4-4 4 4"/></svg><svg viewBox="0 0 16 16" stroke-width="1.3"><rect x="4.5" y="1.5" width="7" height="13" rx="3.5"/><path d="M8 4v3"/></svg><svg viewBox="0 0 16 16" stroke-width="1.6"><path d="M4 6l4 4 4-4"/></svg></div></div>
26
+ <div id="what"></div>
27
+ <div id="pill"><svg viewBox="-1 -1 14 22"><path d="M0 0V17L4 13L7 20L10 18.7L7 12H12Z" fill="#fff"/></svg>右键 或 <kbd>Esc</kbd> 退出<span id="wheel-hint" hidden>· 滚轮 调大小</span></div>
28
+ <script src="overlay.js"></script>
29
+ </body>
30
+ </html>
@@ -0,0 +1,42 @@
1
+ // 指认遮罩:主进程告诉高亮框画在哪(窗口内 CSS 像素),这里只负责画
2
+ const box = document.getElementById('box')
3
+ const what = document.getElementById('what')
4
+ const focus = document.getElementById('focus')
5
+ let bx = 0
6
+ let by = 0 // 高亮框现在的位置,跟着来源窗口挪时用
7
+
8
+ window.overlay.onHighlight((h) => {
9
+ if (!h) { box.style.opacity = '0'; focus.style.opacity = '0'; what.style.display = 'none'; document.getElementById('wheel-hint').hidden = true; return }
10
+ // 空壳程序(QQ、网易云)里多一个跟着鼠标的小框,截图只截它
11
+ document.getElementById('wheel-hint').hidden = !h.focus
12
+ if (h.focus) {
13
+ Object.assign(focus.style, { transform: `translate(${h.focus.x}px, ${h.focus.y}px)`, width: `${h.focus.width}px`, height: `${h.focus.height}px`, opacity: '1' })
14
+ if (!focus.classList.contains('live')) { focus.getBoundingClientRect(); focus.classList.add('live') }
15
+ } else {
16
+ focus.style.opacity = '0'
17
+ }
18
+ bx = h.x - 3
19
+ by = h.y - 3
20
+ Object.assign(box.style, { transform: `translate(${bx}px, ${by}px)`, width: `${h.width + 6}px`, height: `${h.height + 6}px`, opacity: '1' })
21
+ // 第一次直接出现在原地,之后才开过渡
22
+ if (!box.classList.contains('live')) { box.getBoundingClientRect(); box.classList.add('live') }
23
+ // 高亮框上角只标是什么控件,怎么操作看屏幕下方的提示
24
+ what.textContent = h.label
25
+ Object.assign(what.style, { display: h.label ? 'block' : 'none', left: `${h.x}px`, top: `${Math.max(0, h.y - 26)}px` })
26
+ })
27
+
28
+
29
+ // 选中之后:只留高亮框,控件名和操作提示收起来
30
+ window.overlay.onHold(() => {
31
+ what.style.display = 'none'
32
+ document.getElementById('pill').style.display = 'none'
33
+ document.getElementById('wheel').style.display = 'none'
34
+ })
35
+
36
+ // 选中之后来源窗口被拖动:高亮框跟着平移,不要过渡动画(否则拖起来拖泥带水)
37
+ window.overlay.onShift(({ dx, dy }) => {
38
+ box.classList.remove('live')
39
+ bx += dx
40
+ by += dy
41
+ box.style.transform = `translate(${bx}px, ${by}px)`
42
+ })
@@ -0,0 +1,7 @@
1
+ const { contextBridge, ipcRenderer } = require('electron')
2
+
3
+ contextBridge.exposeInMainWorld('overlay', {
4
+ onHighlight: (fn) => ipcRenderer.on('highlight', (_e, h) => fn(h)),
5
+ onHold: (fn) => ipcRenderer.on('hold', () => fn()),
6
+ onShift: (fn) => ipcRenderer.on('shift', (_e, d) => fn(d)),
7
+ })
@@ -0,0 +1,73 @@
1
+ /* 收起的任务:右下角托盘上方一叠胶囊。颜色和卡片同一套 */
2
+ :root {
3
+ --bg: #ffffff;
4
+ --text: #1f2328;
5
+ --muted: #6e7781;
6
+ --faint: #9aa1aa;
7
+ --chip: #f2f3f5;
8
+ --blue: #4d6bfe;
9
+ --blue-soft: #eef2ff;
10
+ --ok: #2da44e;
11
+ --ok-soft: #e3f3e8;
12
+ --error: #cf222e;
13
+ --error-soft: #fde8ea;
14
+ --shadow: 0 3px 12px rgba(15, 23, 42, 0.08), 0 1px 3px rgba(15, 23, 42, 0.05), 0 0 0 1px rgba(15, 23, 42, 0.05);
15
+ }
16
+ @media (prefers-color-scheme: dark) {
17
+ :root {
18
+ --bg: #23252a;
19
+ --text: #e6e8eb;
20
+ --muted: #a0a7b1;
21
+ --faint: #7c838d;
22
+ --chip: #2f3237;
23
+ --blue: #6b84ff;
24
+ --blue-soft: #2a3150;
25
+ --ok: #3fb950;
26
+ --ok-soft: #1f3a2a;
27
+ --error: #ff7b72;
28
+ --error-soft: #4a2326;
29
+ --shadow: 0 3px 12px rgba(0, 0, 0, 0.35), 0 1px 3px rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(255, 255, 255, 0.06);
30
+ }
31
+ }
32
+
33
+ html, body {
34
+ margin: 0;
35
+ background: transparent;
36
+ font-family: "Segoe UI Variable Text", "Segoe UI", "Microsoft YaHei UI", "PingFang SC", sans-serif;
37
+ font-size: 13px;
38
+ color: var(--text);
39
+ overflow: hidden;
40
+ user-select: none;
41
+ }
42
+ /* 右对齐往上叠;四周留出阴影的位置 */
43
+ #list { display: flex; flex-direction: column; align-items: flex-end; gap: 8px; padding: 12px 14px 10px; }
44
+
45
+ .pill {
46
+ display: inline-flex; align-items: center; gap: 8px;
47
+ height: 34px; max-width: 100%; padding: 0 6px 0 6px;
48
+ border-radius: 17px;
49
+ background: var(--bg);
50
+ box-shadow: var(--shadow);
51
+ cursor: pointer;
52
+ animation: in 160ms ease-out;
53
+ }
54
+ .pill.out { animation: out 140ms ease-in forwards; pointer-events: none; }
55
+ @keyframes in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
56
+ @keyframes out { to { opacity: 0; transform: translateX(12px); } }
57
+
58
+ .pill img { width: 24px; height: 24px; border-radius: 50%; object-fit: cover; object-position: 50% 12%; background: var(--blue-soft); image-rendering: pixelated; }
59
+ .pill .dot { flex: none; width: 8px; height: 8px; border-radius: 50%; }
60
+ .pill.run .dot { background: var(--blue); box-shadow: 0 0 0 3px var(--blue-soft); }
61
+ .pill.done .dot { background: var(--ok); box-shadow: 0 0 0 3px var(--ok-soft); }
62
+ .pill.error .dot { background: var(--error); box-shadow: 0 0 0 3px var(--error-soft); }
63
+ .pill .title { min-width: 0; max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
64
+ .pill.run .title { color: var(--muted); }
65
+ .pill:hover .title { color: var(--text); }
66
+ /* × 只是不看了,不打断 DSH */
67
+ .pill .x {
68
+ flex: none; width: 22px; height: 22px; margin-left: -2px;
69
+ display: inline-flex; align-items: center; justify-content: center;
70
+ border: none; border-radius: 50%; background: transparent; color: var(--faint);
71
+ font: inherit; font-size: 14px; cursor: pointer;
72
+ }
73
+ .pill .x:hover { background: var(--chip); color: var(--text); }
@@ -0,0 +1,11 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <link rel="stylesheet" href="pill.css">
6
+ </head>
7
+ <body>
8
+ <div id="list"></div>
9
+ <script src="pill.js"></script>
10
+ </body>
11
+ </html>
@@ -0,0 +1,35 @@
1
+ // 主进程给整份列表,这里照着画;点胶囊去 DSH 看,× 不看了
2
+ const list = document.getElementById('list')
3
+ const rows = new Map() // id -> element
4
+
5
+ function row(p) {
6
+ const el = document.createElement('div')
7
+ el.dataset.id = p.id
8
+ el.innerHTML = '<img src="../card/mascot/idle.png" alt=""><span class="dot"></span><span class="title"></span><button class="x" type="button" title="不看了">×</button>'
9
+ el.addEventListener('click', () => window.pill.open(p.id))
10
+ el.querySelector('.x').addEventListener('click', (e) => { e.stopPropagation(); window.pill.dismiss(p.id) })
11
+ return el
12
+ }
13
+
14
+ function fit() {
15
+ // 等动画的那帧量不准,下一帧再报高度
16
+ requestAnimationFrame(() => window.pill.resize(Math.ceil(list.getBoundingClientRect().height)))
17
+ }
18
+
19
+ window.pill.onList((items) => {
20
+ const keep = new Set(items.map(p => p.id))
21
+ for (const [id, el] of rows) {
22
+ if (keep.has(id)) continue
23
+ rows.delete(id)
24
+ el.classList.add('out')
25
+ el.addEventListener('animationend', () => { el.remove(); fit() }, { once: true })
26
+ }
27
+ for (const p of items) {
28
+ let el = rows.get(p.id)
29
+ if (!el) { el = row(p); rows.set(p.id, el); list.append(el) }
30
+ el.className = `pill ${p.state}`
31
+ el.querySelector('.title').textContent = p.title
32
+ el.title = { run: '在后台做:', done: '做完了:', error: '出错了:' }[p.state] + p.title + '\n点一下到 DSH 里看'
33
+ }
34
+ fit()
35
+ })
@@ -0,0 +1,8 @@
1
+ const { contextBridge, ipcRenderer } = require('electron')
2
+
3
+ contextBridge.exposeInMainWorld('pill', {
4
+ onList: (fn) => ipcRenderer.on('pills', (_e, items) => fn(items)),
5
+ open: (id) => ipcRenderer.send('pill-open', id),
6
+ dismiss: (id) => ipcRenderer.send('pill-dismiss', id),
7
+ resize: (height) => ipcRenderer.send('pill-resize', height),
8
+ })
@@ -0,0 +1,16 @@
1
+ const { contextBridge, ipcRenderer, webUtils } = require('electron')
2
+
3
+ contextBridge.exposeInMainWorld('pet', {
4
+ onDsh: (fn) => ipcRenderer.on('dsh', (_e, msg) => fn(msg)),
5
+ send: (msg) => ipcRenderer.invoke('to-plugin', msg),
6
+ moveBy: (dx, dy) => ipcRenderer.send('move-by', dx, dy),
7
+ front: (on) => ipcRenderer.send('front', on),
8
+ menu: () => ipcRenderer.send('menu'),
9
+ approvals: (count) => ipcRenderer.send('approvals', count),
10
+ clicked: () => ipcRenderer.send('pet-clicked'),
11
+ rendered: (state) => ipcRenderer.send('rendered', state),
12
+ pathForFile: (file) => webUtils.getPathForFile(file),
13
+ layout: (layout) => ipcRenderer.send('pet-layout', layout),
14
+ dragging: (on) => ipcRenderer.send('pet-dragging', on),
15
+ scaleBy: (delta) => ipcRenderer.send('pet-scale-by', delta),
16
+ })
@@ -0,0 +1,24 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src 'self' data:; style-src 'self'">
6
+ <title>Pointy</title>
7
+ <link rel="stylesheet" href="style.css">
8
+ </head>
9
+ <body>
10
+ <div id="stage">
11
+ <div id="bubble" hidden>
12
+ <div id="bubble-text"></div>
13
+ <div id="bubble-detail" hidden></div>
14
+ <div id="bubble-actions" hidden>
15
+ <button id="btn-allow" type="button">允许一次</button>
16
+ <button id="btn-reject" type="button">拒绝</button>
17
+ </div>
18
+ </div>
19
+ <div id="character"></div>
20
+ <div id="badge" hidden></div>
21
+ </div>
22
+ <script type="module" src="pet.js"></script>
23
+ </body>
24
+ </html>
@@ -0,0 +1,25 @@
1
+ // 内置台词:DSH 没连上、模型超时或被限频时用
2
+ const LINES = {
3
+ idle: ['主人,今天也要加油哦', '有活儿就叫我~', '发呆中……'],
4
+ thinking: ['让我想想……', '嗯……这个有点意思', '在想了在想了'],
5
+ running: ['敲命令中,别催', '终端启动!', '跑起来跑起来'],
6
+ editing: ['改文件中,别眨眼', '这里要改一下', '动手了哦'],
7
+ reading: ['翻翻文件……', '我看看写了啥', '找找在哪'],
8
+ web: ['上网查一下', '搜索中……'],
9
+ waiting: ['主人,要批准一下吗?', '等你点头哦', '这步要你同意才行'],
10
+ error: ['呜,出错了……', '啊,失败了', '不、不是我的错!'],
11
+ done: ['搞定啦!', '收工~', '做完了,夸我'],
12
+ sleep: ['Zzz……', '好困……'],
13
+ poke: ['干、干嘛啦', '别戳了!', '在呢在呢', '嗯?'],
14
+ files: ['收到文件啦', '交给我吧'],
15
+ offline: ['DSH 没连上……', '主人,DSH 开了吗?'],
16
+ }
17
+
18
+ let last
19
+ export function pickLine(kind) {
20
+ const pool = LINES[kind] ?? LINES.idle
21
+ let line
22
+ do { line = pool[Math.floor(Math.random() * pool.length)] } while (pool.length > 1 && line === last)
23
+ last = line
24
+ return line
25
+ }