dsh-island-overlay 0.2.1 → 0.2.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.
- package/README.md +101 -10
- package/lib/index.js +7 -4
- package/overlay/activate.ps1 +4 -1
- package/overlay/index.html +19 -7
- package/overlay/main.js +32 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -83,15 +83,16 @@ dsh plugin --profile desktop add dsh-island-overlay
|
|
|
83
83
|
| 事件 | 显示 |
|
|
84
84
|
|---|---|
|
|
85
85
|
| `turn/start` | 思考(小鲸鱼 + 计时,2.5s 后缩成小图标) |
|
|
86
|
-
| `approval/asked` |
|
|
86
|
+
| `approval/asked` | 等待审批(琥珀脉冲,常驻展开,计时接着走) |
|
|
87
87
|
| `tool/call` = `ask_user_question` | 等待回答(同上) |
|
|
88
88
|
| `turn/end` = `completed` | 完成(绿勾,3s 后消失) |
|
|
89
89
|
| `turn/end` = `error` / `blocked` / `max-tokens` | 失败(红色抖动,6s 后消失) |
|
|
90
90
|
| `turn/end` = `aborted` / `interrupted` / `forked` | 不显示,直接消失 |
|
|
91
91
|
|
|
92
92
|
多个会话同时有事时只显示优先级最高的一个(审批/回答 > 失败 > 完成 > 思考),其余用 `+N` 角标;最多并列显示 3 个会话,每段各自计时。
|
|
93
|
+
计时读的是**回合开始时间**,所以「思考 20 分钟 → 弹审批 → 继续思考」是一条连续的时间轴,不会归零。
|
|
93
94
|
|
|
94
|
-
|
|
95
|
+
交互:拖动可移动位置(自动切到「自定义位置」,记在 `~/.dsh/dsh-island-pos.json`);点一下把 DSH 拉到前台;
|
|
95
96
|
鼠标划过展开全文;检测到前台是全屏程序(视频/游戏)时自动隐藏。
|
|
96
97
|
|
|
97
98
|
## 卸载
|
|
@@ -102,12 +103,102 @@ dsh plugin --profile desktop add dsh-island-overlay
|
|
|
102
103
|
|
|
103
104
|
然后重启 DSH,再把文件夹和 `%LOCALAPPDATA%\dsh-island-runtime` 删掉。
|
|
104
105
|
|
|
105
|
-
##
|
|
106
|
+
## 常见问题
|
|
107
|
+
|
|
108
|
+
### 安装
|
|
109
|
+
|
|
110
|
+
**`dsh` 命令找不到 / `'dsh' 不是内部或外部命令`**
|
|
111
|
+
DSH 不会把 `dsh` 加进 PATH,得用完整路径:
|
|
112
|
+
|
|
113
|
+
```powershell
|
|
114
|
+
& "$env:LOCALAPPDATA\Programs\DeepSeek Harness\resources\runtime\cli\bin\dsh.cmd" plugin --profile desktop add dsh-island-overlay
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
装在非默认目录(比如 `D:\`)时,把上面路径换成你的实际位置。
|
|
118
|
+
|
|
119
|
+
**`install.ps1` 报 `dsh.cmd not found`**
|
|
120
|
+
脚本只在 `%LOCALAPPDATA%\Programs`、`Program Files`、`Program Files (x86)` 三个位置找 DSH。装在别处就手动指定:
|
|
121
|
+
|
|
122
|
+
```powershell
|
|
123
|
+
powershell -ExecutionPolicy Bypass -File install.ps1 -Cli "D:\...\DeepSeek Harness\resources\runtime\cli\bin\dsh.cmd"
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**`无法加载文件 install.ps1,因为在此系统上禁止运行脚本`**
|
|
127
|
+
Windows 默认执行策略拦脚本。加 `-ExecutionPolicy Bypass`(上面的命令已经带了),不要改全局策略。
|
|
128
|
+
|
|
129
|
+
**需要自己装 pnpm / node 吗**
|
|
130
|
+
不需要。DSH 自带一份 pnpm(`resources\runtime\primary-runtime\dependencies\pnpm\bin\pnpm.mjs`),
|
|
131
|
+
`dsh plugin` 内部用 DSH 自己的 Electron 跑它,跟系统的 node/pnpm 无关。系统里有别的 pnpm 也不会冲突。
|
|
132
|
+
|
|
133
|
+
**pnpm 报 `ERR_PNPM_...` / 网络超时**
|
|
134
|
+
`dsh plugin` 用的是你 npm 配置里的 registry。国内网络建议:
|
|
135
|
+
|
|
136
|
+
```powershell
|
|
137
|
+
npm config get registry # 一般是 https://registry.npmmirror.com
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
本包在 npmmirror 已同步。若公司网络拦 npm,可临时指定源:
|
|
141
|
+
`dsh plugin --profile desktop add dsh-island-overlay --registry=https://registry.npmmirror.com`。
|
|
142
|
+
|
|
143
|
+
**装完没反应**
|
|
144
|
+
装完必须**重启 DSH**(托盘右键退出,再打开)。热重载不会拉起浮层。
|
|
145
|
+
确认 `~/.dsh/profiles/desktop/package.json` 的 `dsh.profile.bundles` 里有 `dsh-island-overlay`。
|
|
146
|
+
|
|
147
|
+
**已经用 `link:` 装过本地目录,又想换成 npm 版**
|
|
148
|
+
先卸再装,否则 pnpm 会把 link 改写成 npm 版本、你本地的改动就不再生效:
|
|
149
|
+
|
|
150
|
+
```powershell
|
|
151
|
+
& "<dsh.cmd 完整路径>" plugin --profile desktop remove dsh-island-overlay
|
|
152
|
+
& "<dsh.cmd 完整路径>" plugin --profile desktop add dsh-island-overlay
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
反过来(npm 版换成本地开发)把 `add dsh-island-overlay` 换成 `add link:<本目录绝对路径>`。
|
|
156
|
+
|
|
157
|
+
### 运行时
|
|
158
|
+
|
|
159
|
+
**状态文件在变,但屏幕上看不到胶囊**
|
|
160
|
+
先看日志,两个文件都在 `%USERPROFILE%\.dsh\`:
|
|
161
|
+
|
|
162
|
+
- `dsh-island-plugin.log`:`runtime prepared` + `spawn ...` 说明浮层被拉起过;
|
|
163
|
+
只有 `skip: isElectron=false` 说明插件没跑在 DSH 主进程里。
|
|
164
|
+
- `dsh-island-overlay.log`:每次启动写 `start v0.2.x`,之后每 60 秒一条 `alive`。
|
|
165
|
+
没有 `start` → 浮层根本没起来。
|
|
166
|
+
|
|
167
|
+
手动起浮层验证:
|
|
168
|
+
|
|
169
|
+
```powershell
|
|
170
|
+
& "$env:LOCALAPPDATA\dsh-island-runtime\DeepSeek Harness.exe" "<本目录>\overlay"
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
正常会弹出一条黑色胶囊(控制台里的 `disk_cache` 报错可以忽略)。
|
|
174
|
+
|
|
175
|
+
**浮层起不来,日志里连 `start` 都没有**
|
|
176
|
+
多半是运行时镜像不完整。删掉 `%LOCALAPPDATA%\dsh-island-runtime` 再重启 DSH,插件会自动重建
|
|
177
|
+
(同盘用硬链接,不额外占空间)。镜像里必须有 `resources\default_app.asar`,缺它 Electron 就不认命令行给的 app 目录。
|
|
178
|
+
|
|
179
|
+
**跑一阵子自己消失了**
|
|
180
|
+
已修(0.2.0+)。旧版在心跳短暂中断后不会恢复显示,看起来就是「消失了」。升级后:心跳断 30 秒先隐藏,
|
|
181
|
+
恢复后自动重新显示,连续 5 分钟没恢复才退出进程。仍复现的话看 `dsh-island-overlay.log` 最后几行。
|
|
182
|
+
|
|
183
|
+
**点胶囊后 DSH 变成窗口化 / 反应慢**
|
|
184
|
+
已修(0.2.0+)。旧版用 `SW_RESTORE` 会把最大化窗口还原;现在只在窗口最小化时才还原。
|
|
185
|
+
慢是因为旧版每次点击都新起一个 PowerShell,现在复用常驻的 `watch.ps1`(约 150–250ms)。
|
|
186
|
+
|
|
187
|
+
**胶囊挡到浏览器标签页**
|
|
188
|
+
鼠标事件是穿透的(只挡视觉,不挡点击)。想少挡:设置里把「迷你态」调小,或改用「顶部居中 → 右上角」,
|
|
189
|
+
或开「仅 DSH 不在前台时显示」。拖动可以直接改位置(记在 `~/.dsh/dsh-island-pos.json`)。
|
|
190
|
+
|
|
191
|
+
**换了显示器 / 拔了副屏后胶囊不见了**
|
|
192
|
+
位置会被夹回最近的屏幕工作区,理论上不会跑到屏幕外。真丢了就删 `~/.dsh/dsh-island-pos.json` 重启 DSH。
|
|
193
|
+
|
|
194
|
+
**改了 `overlay/index.html` 没生效**
|
|
195
|
+
浮层是 DSH 启动时拉起的独立进程,不热重载。改完重启 DSH,或手动 `taskkill` 掉浮层进程(插件 3 秒内会重拉)。
|
|
196
|
+
|
|
197
|
+
**全屏看视频 / 打游戏时胶囊还在**
|
|
198
|
+
设置里开「全屏应用时自动隐藏」。注意它只认「窗口铺满整块屏幕」,最大化窗口不算全屏。
|
|
199
|
+
少数独占全屏的游戏连置顶窗口都能盖住,这是 Windows 的限制,插件绕不过去。
|
|
200
|
+
|
|
201
|
+
**多个 DSH 实例会不会叠出两个胶囊**
|
|
202
|
+
不会,浮层有单实例锁,第二个实例直接退出。
|
|
203
|
+
|
|
106
204
|
|
|
107
|
-
- **没反应**:确认 `~/.dsh/dsh-island-state.json` 有内容在变(3 秒刷新一次)。没有 → 插件没加载,
|
|
108
|
-
看 DSH 启动日志,或重新跑一次 `install.ps1`。
|
|
109
|
-
- **有 JSON 没胶囊**:浮层没起来。手动试:
|
|
110
|
-
`"C:\Users\<你>\AppData\Local\Programs\DeepSeek Harness\DeepSeek Harness.exe" <本目录>\overlay`
|
|
111
|
-
正常会弹出一条黑色胶囊,控制台有无关紧要的 disk_cache 报错。
|
|
112
|
-
- **胶囊不置顶 / 盖住别人**:Windows 的置顶窗口在少数全屏独占程序(部分游戏)上会被盖住,这是系统限制。
|
|
113
|
-
- **改了 `overlay/index.html` 要重启 DSH**:浮层是启动时拉起的,不热重载。
|
package/lib/index.js
CHANGED
|
@@ -167,24 +167,27 @@ export default {
|
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
const name = titleOf(session)
|
|
170
|
+
// since = 这一回合的开始时间,只有 turn/start 能改它。
|
|
171
|
+
// 以前 approval/asked、approval/decided、tool/call 都把它重置成 now,
|
|
172
|
+
// 于是「思考 20 分钟 → 弹审批 → 继续思考」的计时会从 0 重新数。
|
|
170
173
|
switch (type) {
|
|
171
174
|
case 'turn/start':
|
|
172
175
|
touch(sid, { state: 'thinking', detail: '', since: Date.now(), title: name })
|
|
173
176
|
break
|
|
174
177
|
case 'approval/asked':
|
|
175
|
-
touch(sid, { state: 'approval', detail: String(data.toolName || '需要授权'),
|
|
178
|
+
touch(sid, { state: 'approval', detail: String(data.toolName || '需要授权'), title: name })
|
|
176
179
|
break
|
|
177
180
|
case 'approval/decided':
|
|
178
|
-
if (items.has(sid)) touch(sid, { state: 'thinking', detail: '',
|
|
181
|
+
if (items.has(sid)) touch(sid, { state: 'thinking', detail: '', title: name })
|
|
179
182
|
break
|
|
180
183
|
case 'tool/call':
|
|
181
184
|
if (String(data.name || '') === 'ask_user_question') {
|
|
182
|
-
touch(sid, { state: 'ask', detail: '等待你的回答',
|
|
185
|
+
touch(sid, { state: 'ask', detail: '等待你的回答', title: name })
|
|
183
186
|
}
|
|
184
187
|
break
|
|
185
188
|
case 'tool/result': {
|
|
186
189
|
const cur = items.get(sid)
|
|
187
|
-
if (cur && cur.state === 'ask') touch(sid, { state: 'thinking', detail: '',
|
|
190
|
+
if (cur && cur.state === 'ask') touch(sid, { state: 'thinking', detail: '', title: name })
|
|
188
191
|
break
|
|
189
192
|
}
|
|
190
193
|
case 'turn/end': {
|
package/overlay/activate.ps1
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# Bring the DSH main window to the foreground (click-through pill -> click).
|
|
2
|
+
# Fallback path only: the overlay normally sends "activate" over watch.ps1's stdin.
|
|
2
3
|
$ErrorActionPreference = 'SilentlyContinue'
|
|
3
4
|
|
|
4
5
|
Add-Type -TypeDefinition @"
|
|
@@ -7,6 +8,7 @@ using System.Runtime.InteropServices;
|
|
|
7
8
|
public class IslandFg {
|
|
8
9
|
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
|
|
9
10
|
[DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int cmd);
|
|
11
|
+
[DllImport("user32.dll")] public static extern bool IsIconic(IntPtr h);
|
|
10
12
|
[DllImport("user32.dll")] public static extern void keybd_event(byte vk, byte scan, uint flags, IntPtr extra);
|
|
11
13
|
}
|
|
12
14
|
"@
|
|
@@ -20,5 +22,6 @@ if (-not $p) { exit }
|
|
|
20
22
|
# 先按一下 Alt 骗过前台锁定,再抢焦点
|
|
21
23
|
[IslandFg]::keybd_event(0x12, 0, 0, [IntPtr]::Zero)
|
|
22
24
|
[IslandFg]::keybd_event(0x12, 0, 2, [IntPtr]::Zero)
|
|
23
|
-
|
|
25
|
+
# 只在真最小化时还原。SW_RESTORE 会把最大化窗口缩回普通窗口,就是用户报的「点了变成窗口化」
|
|
26
|
+
if ([IslandFg]::IsIconic($p.MainWindowHandle)) { [IslandFg]::ShowWindow($p.MainWindowHandle, 9) | Out-Null }
|
|
24
27
|
[IslandFg]::SetForegroundWindow($p.MainWindowHandle) | Out-Null
|
package/overlay/index.html
CHANGED
|
@@ -150,7 +150,12 @@ function esc(s) {
|
|
|
150
150
|
|
|
151
151
|
function fmtTime(sec) {
|
|
152
152
|
sec = Math.max(0, Math.floor(sec))
|
|
153
|
-
|
|
153
|
+
if (sec < 60) return sec + 's'
|
|
154
|
+
const m = Math.floor(sec / 60)
|
|
155
|
+
const s = sec % 60
|
|
156
|
+
if (m < 60) return s ? m + 'm' + s + 's' : m + 'm'
|
|
157
|
+
const h = Math.floor(m / 60)
|
|
158
|
+
return m % 60 ? h + 'h' + (m % 60) + 'm' : h + 'h'
|
|
154
159
|
}
|
|
155
160
|
|
|
156
161
|
function icon(state) {
|
|
@@ -202,8 +207,11 @@ function ordered() {
|
|
|
202
207
|
.sort((a, b) => (RANK[b.state] || 0) - (RANK[a.state] || 0) || (a.since || 0) - (b.since || 0))
|
|
203
208
|
}
|
|
204
209
|
|
|
210
|
+
const TIMED = { thinking: 1, approval: 1, ask: 1 }
|
|
211
|
+
|
|
205
212
|
function segHtml(it, primary) {
|
|
206
|
-
|
|
213
|
+
// 计时都读 since(= 回合开始),所以「思考 → 审批 → 继续思考」是一条连续的时间轴
|
|
214
|
+
const meta = TIMED[it.state]
|
|
207
215
|
? '<span class="meta" data-since="' + (Number(it.since) || Date.now()) + '">' + fmtTime((Date.now() - (Number(it.since) || Date.now())) / 1000) + '</span>'
|
|
208
216
|
: it.state === 'done' ? '<span class="meta">刚刚</span>' : ''
|
|
209
217
|
// 只有主段展开标题,其余段只留图标 + 状态词,避免胶囊被撑爆
|
|
@@ -228,6 +236,9 @@ function render() {
|
|
|
228
236
|
curState = ''
|
|
229
237
|
clearTimeout(shrinkTimer)
|
|
230
238
|
if (tick) { clearInterval(tick); tick = null }
|
|
239
|
+
// 悬停中被清空时必须把鼠标穿透还回去:窗口本身还在,ignoreMouseEvents=false
|
|
240
|
+
// 会让它在「看不见」的状态下继续吃掉顶部中间那块区域的点击。
|
|
241
|
+
if (hovered) { hovered = false; window.islandApi.hover(false) }
|
|
231
242
|
return
|
|
232
243
|
}
|
|
233
244
|
|
|
@@ -235,8 +246,9 @@ function render() {
|
|
|
235
246
|
const e = cfg.effects || DEFAULTS.effects
|
|
236
247
|
const shown = list.slice(0, MAXSEG)
|
|
237
248
|
const rest = list.length - shown.length
|
|
238
|
-
// cfg 也进 key:设置页改了缩放/迷你尺寸/动效开关时,items
|
|
239
|
-
|
|
249
|
+
// cfg 也进 key:设置页改了缩放/迷你尺寸/动效开关时,items 没变也得重画。
|
|
250
|
+
// title/detail 同样要进:会话改名、失败原因变化时 since 不变,漏了就不会重绘。
|
|
251
|
+
const key = list.map(it => it.state + it.id + it.since + '|' + (it.title || '') + '|' + (it.detail || '')).join(',') + '|' + rest + '|' + JSON.stringify(cfg)
|
|
240
252
|
if (key !== curKey) {
|
|
241
253
|
curKey = key
|
|
242
254
|
curState = best.state
|
|
@@ -255,9 +267,9 @@ function render() {
|
|
|
255
267
|
scheduleMini(hovered ? 0 : holdFor(best.state))
|
|
256
268
|
}
|
|
257
269
|
|
|
258
|
-
//
|
|
259
|
-
const
|
|
260
|
-
if (
|
|
270
|
+
// 只要有一段带计时(思考/审批/等待回答)就跑秒表
|
|
271
|
+
const anyTimed = list.some(it => TIMED[it.state])
|
|
272
|
+
if (anyTimed) {
|
|
261
273
|
if (!tick) { paintTimer(); tick = setInterval(paintTimer, 1000) }
|
|
262
274
|
} else if (tick) { clearInterval(tick); tick = null }
|
|
263
275
|
}
|
package/overlay/main.js
CHANGED
|
@@ -7,6 +7,7 @@ const { execFile, spawn } = require('child_process')
|
|
|
7
7
|
const DSH_HOME = process.env.DSH_HOME || path.join(os.homedir(), '.dsh')
|
|
8
8
|
const STATE_FILE = path.join(DSH_HOME, 'dsh-island-state.json')
|
|
9
9
|
const POS_FILE = path.join(DSH_HOME, 'dsh-island-pos.json')
|
|
10
|
+
const CONFIG_FILE = path.join(DSH_HOME, 'dsh-island-config.json')
|
|
10
11
|
const DEAD_MS = 30000
|
|
11
12
|
const LOG_FILE = path.join(DSH_HOME, 'dsh-island-overlay.log')
|
|
12
13
|
|
|
@@ -57,6 +58,20 @@ function savePos() {
|
|
|
57
58
|
} catch (err) {}
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
// 拖动 = 切到「自定义位置」。宿主 watchFile 监听同一个配置文件,会立刻把新
|
|
62
|
+
// 配置下发回来(不需要浮层自己刷新界面)。写失败就算了,位置至少本次有效。
|
|
63
|
+
function persistPositionCustom() {
|
|
64
|
+
try {
|
|
65
|
+
let cur = {}
|
|
66
|
+
try { cur = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')) || {} } catch (err) {}
|
|
67
|
+
if (cur.position === 'custom') return
|
|
68
|
+
cur.position = 'custom'
|
|
69
|
+
const tmp = CONFIG_FILE + '.tmp'
|
|
70
|
+
fs.writeFileSync(tmp, JSON.stringify(cur, null, 2), 'utf8')
|
|
71
|
+
fs.renameSync(tmp, CONFIG_FILE)
|
|
72
|
+
} catch (err) {}
|
|
73
|
+
}
|
|
74
|
+
|
|
60
75
|
// 窗口比胶囊大一圈:缩放后的胶囊要放得下,还要留出呼吸/涟漪的溢出空间
|
|
61
76
|
function winSize(s) {
|
|
62
77
|
return { w: Math.round(BASE_W * s), h: Math.round(BASE_H * s) }
|
|
@@ -113,15 +128,27 @@ function create() {
|
|
|
113
128
|
win.setIgnoreMouseEvents(true, { forward: true })
|
|
114
129
|
win.loadFile(path.join(__dirname, 'index.html'))
|
|
115
130
|
win.once('ready-to-show', () => win.showInactive())
|
|
131
|
+
// 首帧会丢:tick() 在页面还没加载完时就把状态 send 出去了(被丢弃),
|
|
132
|
+
// 而 last 已经记下,之后状态不再变化就一直不重发 —— 结果是浮层起来了却空白。
|
|
133
|
+
// 页面加载完清掉 last,让下一次 tick 无条件重发。
|
|
134
|
+
win.webContents.on('did-finish-load', () => { last = '' })
|
|
116
135
|
|
|
117
136
|
// 诊断:之前浮层会莫名其妙消失且日志里什么都没留下,这里把每种死法都记下来
|
|
118
|
-
|
|
137
|
+
let reloads = 0
|
|
138
|
+
win.webContents.on('render-process-gone', (_e, d) => {
|
|
139
|
+
log('render-process-gone: ' + JSON.stringify(d))
|
|
140
|
+
// 渲染进程挂了窗口会留在那但永远空白。重载一次;连续崩就退出,
|
|
141
|
+
// 让插件的 3 秒心跳重新拉起一个干净进程,而不是卡在空白窗口上。
|
|
142
|
+
if (reloads++ < 2) setTimeout(() => { try { win.reload() } catch (err) {} }, 1000)
|
|
143
|
+
else { log('quit: renderer keeps dying'); app.quit() }
|
|
144
|
+
})
|
|
119
145
|
win.webContents.on('unresponsive', () => log('unresponsive'))
|
|
120
146
|
win.webContents.on('responsive', () => log('responsive'))
|
|
121
147
|
app.on('child-process-gone', (_e, d) => log('child-process-gone: ' + JSON.stringify(d)))
|
|
122
148
|
win.on('hide', () => log('window hidden'))
|
|
123
149
|
win.on('show', () => log('window shown'))
|
|
124
|
-
|
|
150
|
+
// 窗口被关掉但进程还在 = 不会再有任何界面;直接退出让宿主重新拉起
|
|
151
|
+
win.on('closed', () => { log('window closed'); app.quit() })
|
|
125
152
|
|
|
126
153
|
// 别的置顶窗口偶尔会把我们压下去,定期重新抬一次
|
|
127
154
|
setInterval(() => {
|
|
@@ -276,6 +303,9 @@ ipcMain.on('drag-end', () => {
|
|
|
276
303
|
if (!dragOrigin) return
|
|
277
304
|
dragOrigin = null
|
|
278
305
|
savePos()
|
|
306
|
+
// 只写 pos.json 没用:place() 只在 position==='custom' 时才读它,
|
|
307
|
+
// 所以拖完重启又回到顶部居中。这里把配置切成 custom(宿主 watchFile 会接住)。
|
|
308
|
+
persistPositionCustom()
|
|
279
309
|
})
|
|
280
310
|
|
|
281
311
|
app.whenReady().then(() => {
|