dsh-plugin-maoxuan-pet 0.2.3 → 0.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,40 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 0.3.1 — 2026-11-01
4
+
5
+ 修复:预设面板上 maoxuan-pet 显示「加载失败 / Failed to load」;桌宠壳补上宿主鉴权支持。
6
+
7
+ ### 修复
8
+ - **preset 挂载必败的缺参**:`cordis.patch.yml` 里 preset 的 `tool-todo` 子项没有给 `allowParallelInProgress`,而宿主的 `@deepseek-ai/dsh-tool-todo` 从 rc.2 起就把它声明为 **required**(且无默认值可合并)。挂载时校验失败,注册表把该行标记为 `broken`,界面徽章显示「加载失败」,悬停可见原文:`tool-todo (@deepseek-ai/dsh-tool-todo): invalid config: $.allowParallelInProgress missing required value`。现在显式声明 `allowParallelInProgress: false`(本预设一次只推进一件事,与 `false` 语义一致)。
9
+ - 用 dsh 0.2.1-rc.5 运行时在真实桌面同构环境里探测过 `agentPresets.list()`:修复前 maoxuan-pet 带 `broken` 字段,修复后消失(dev-assistant 的失败是 dsh-plugin-dev-workflow 自己的 `connection` 注入问题,与本插件无关)。
10
+
11
+ ### 桌宠壳
12
+ - **壳不再被宿主鉴权卡死**:DSH `0.2.1-rc.2` 起 Web API 全部要鉴权(launch token 只在 `GET /` 换签名 cookie,`/api/*` 裸请求一律 401,且不接受 Authorization 头),而 token 只有 CLI 宿主会打印、桌面 App 从不外泄——壳此前在桌面 App 旁边永远是「连接中断」。现在两条路:
13
+ - **`--public`(默认推荐)**:插件在 web server 上直接注册一组 loopback 公开路由 `SHELL_ROUTES`(`/dsh-maoxuan-pet/pet.json`、`asset.png`、`generate`、`dismiss`),GET 只读、POST 强制 JSON + `x-dsh-maoxuan-pet: 1` 自定义头(跨站页面发不出来,非 CSRF 洞)、监听只绑 loopback;壳不需要 token 也不需要 cookie,桌面 App 旁边同样满血。可用 `publicRoutes: false` 关闭。
14
+ - **`--token=<TOKEN>`**:给 CLI 宿主用,`GET /?token=…` 换 `dsh-auth-*` 签名 cookie(URLSession 内存 jar、不落盘、遇 401 自动重换)。
15
+ - **桌面模式互斥显示(新)**:壳在桌面上时,界面里的小毛自动隐身,右下角只留一枚「小毛在桌面上 · 点此收回」小徽章;收回(或右键壳退出)后界面宠物自动回来。避免「两只小毛同时显示」的困惑。
16
+ - **界面开关(新)**:红皮书卡片页脚新增 **「放她到桌面 / 收回桌宠」**——宿主新增第六条路由 `GET|POST /api/dsh-maoxuan-pet.shell`(状态快照 + launch/quit,origin 只接受 `http://127.0.0.1:<port>` 防注入;产物缺失时先自动跑 `desktop/build.sh`,最长 120s)。壳写 pidfile(`$DSH_HOME/storages/dsh-maoxuan-pet.shell.pid`,读取方探活、退出清理)+ 自身单实例保护,手动双击与按钮互相认得,不会叠出两只。已对活宿主端到端验证:GET 状态 → launch 起进程并落 pidfile → 重复 launch 幂等 → quit 退净并清 pidfile。
17
+ - **双击备用**:仓库根目录新增 `启动小毛.command`——自动编译(首次)、自动找最近写入的 `~/.dsh/agent-tree-dock-<port>.json` 得到宿主地址、自动 `--public` 起壳,重复双击不会叠出第二只。
18
+ - 验证:13091(鉴权过、插件未装:404 非 401)、30888/30891(maoxuan 宿主:`--public` 下 state 200 + 四帧立绘原生拉取、POST 缺头 403 / 带头 200);`verify/host.test.mjs` 新增 2 条公开路由用例(假 ctx 的 webServer 记录注册 + 走 node 风格 req/res 断言 200/403),全套 20/20。
19
+
20
+ ### 排查备注
21
+ - 挂载失败只在客户端读取名册(打开预设面板)时可见,宿主启动日志默认不打印该警告,所以此前一直没暴露。
22
+
23
+ ## 0.3.0 — 2026-11-01
24
+
25
+ 新能力:独立 macOS 桌宠壳——小毛从 DSH 界面里走出来,站到真正的桌面上;DSH 最小化甚至关掉界面,她都站在原地。
26
+
27
+ ### 桌宠壳(`desktop/`)
28
+ - **为什么需要它**:Web 宠物是 `position:fixed` 的 DOM,永远画不出浏览器视口之外——DSH 最小化她就没。DSH 自带宠物能「跑出去」,是因为壳层进程建了第二个原生窗口(透明、置顶、跨所有 Space)。本壳用纯 Swift + 系统 WebKit(单文件约 450 行、零第三方依赖)给小毛配了同款独立窗口,进程独立于 DSH。
29
+ - **窗口参数对照**(从 DSH Tauri 壳层二进制核实的参数 → 本壳实现):`transparent`/`alwaysOnTop`/`visibleOnAllWorkspaces`/`skipTaskbar` → 透明无边框 `NSWindow` + `.floating` + `canJoinAllSpaces` + accessory 策略(不占 Dock、不进 Cmd-Tab);`set_pet_input_region`/`set_pet_ignore_cursor_events` → 容器 `hitTest` 白名单矩形,矩形外鼠标事件全部穿透回桌面。
30
+ - **数据通道**:`URLSession`(ephemeral)原生轮询既有同源路由——state 平时 20s、生成中 3s、离线 8s;`再督促`/`收起` POST 同源接口;宿主 `index.js` 零改动,Web 宠物照常并存。
31
+ - **素材也走原生层**:WebKit 的 ATS 不认 loopback 豁免,`file://` 页面直挂 `http://127.0.0.1` 的图会被拦(只剩占位鲸),所以精灵图由 URLSession 拉取后转 data URL 注入(`window.__petSprites`);WebKit 零网络请求,壳零磁盘写入(`nonPersistent` 数据存储 + ephemeral 会话)。
32
+ - **交互语义与 Web 宠物一致**:红皮书卡片、新气泡自动弹出、打开 600ms 后标已读、翻书中状态、倒计时标签、`‹ ›` 回翻、Esc 收起;右键小毛 = 复位位置 / 退出;拖小毛或卡片标题栏 = 整窗移动(`screenX/screenY` 差值,无反馈回环),位置存 `UserDefaults`(suite `dsh-maoxuan-pet.window`)跨启动记忆。
33
+ - **构建与运行**:`npm run pet:build`(swiftc 约几秒,模块缓存收进项目内)/ `npm run pet`;参数 `--origin=` / `--port=` / `--reset` / `--verbose`;详见 `desktop/README.md`。
34
+
35
+ ### 打包
36
+ - `package.json` `files` 收录 `desktop/` 五个源码文件(Info.plist/README/build.sh/maoxuan-pet.swift/pet.html),源码随 npm 包分发;`desktop/build/` 构建产物(二进制 + Swift 模块缓存,约 70MB)不进包体——`.gitignore` 只拦 git 不拦 `npm pack`,files 白名单目录内的产物必须逐文件点名(0.3.0 发布前实测修正)。
37
+
3
38
  ## 0.2.3 — 2026-11-01
4
39
 
5
40
  首次安装修复:模型配置自动从宿主 profile 桥接进 headless 子进程,新装机基本零配置。
package/README.en.md CHANGED
@@ -14,6 +14,22 @@ dsh plugin --profile web add dsh-plugin-maoxuan-pet
14
14
  Requirements: DSH `>=0.2.1-rc.2 <0.3.0`, Node `^22.19.0 || >=24.0.0`. Web shell only.
15
15
  Source install / uninstall / troubleshooting: [docs/INSTALL.md](docs/INSTALL.md).
16
16
 
17
+ ## Stand on the real desktop (macOS, optional)
18
+
19
+ The web pet lives inside the DSH window and disappears when it is minimized — a hard browser
20
+ sandbox boundary. `desktop/` ships a **standalone-process pet shell** (pure Swift + system
21
+ WebKit, ~450 lines, zero dependencies): transparent, always-on-top, visible on all Spaces.
22
+ Minimize DSH or close the window entirely — she stays. The plugin still owns the brain (the
23
+ hourly essay reading); the shell only relocates the display.
24
+
25
+ ```bash
26
+ npm run pet:build # swiftc build, a few seconds
27
+ npm run pet # she stands at the bottom-right of your screen
28
+ ```
29
+
30
+ Click to open the red-book card, drag to move (position is remembered), right-click for
31
+ reset/quit. See [desktop/README.md](desktop/README.md) for details.
32
+
17
33
  ## Screenshot
18
34
 
19
35
  ![red-book bubble](https://raw.githubusercontent.com/yuna78/dsh-maoxuan-pet/main/docs/screenshot-hero.png)
@@ -70,7 +86,10 @@ fan-made character; not affiliated with DeepSeek.
70
86
 
71
87
  ## Known limitations
72
88
 
73
- - web shell only (`dsh.client.platform: web`) — no pet in the terminal / TUI;
89
+ - web pet only (`dsh.client.platform: web`) — no pet in the terminal / TUI; on macOS use the
90
+ desktop shell above to leave the window;
91
+ - the desktop shell is macOS-only (system WebKit); Windows / Linux need a Tauri 2 / Electron
92
+ shell — `pet.html` can be reused as-is;
74
93
  - the timer only runs while this `dsh web` process is alive; a stopped process does not catch up,
75
94
  and on restart `nextRunAt` decides whether to run once more;
76
95
  - each headless generation is a fresh session (no shared context): stable, but she "doesn't
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # 鲸鱼娘 · 小毛 —— 住在 DSH 右下角、每小时陪你读一篇毛选的桌宠
1
+ # 鲸鱼娘 · 小毛 —— 每小时陪你读一篇毛选的桌宠,macOS 上还能站上真正的桌面
2
2
 
3
3
  红皮书气泡 + 四帧立绘;每条内容 = 原文引文(带来源日期)+ 一句说给你听的督促 + 一条能当天验证的启示,整条 ≤ 200 字。
4
4
 
@@ -13,6 +13,21 @@ dsh plugin --profile web add dsh-plugin-maoxuan-pet
13
13
  - 要求:DSH `>=0.2.1-rc.2 <0.3.0`、Node `^22.19.0 || >=24.0.0`,仅 Web 界面;
14
14
  - 从源码安装、卸载、排错:[docs/INSTALL.md](docs/INSTALL.md)。
15
15
 
16
+ ## 让她站上真正的桌面(macOS,可选)
17
+
18
+ Web 宠物住在 DSH 界面里,界面最小化她就跟着没了——这是浏览器沙箱的硬边界。`desktop/` 目录自带一个**独立进程的桌宠壳**(纯 Swift + 系统 WebKit,约 450 行、零依赖):透明、置顶、跨所有桌面空间,DSH 最小化甚至关掉界面她都站着;毛选的灵魂(每小时读一篇)仍由插件负责,壳只把「显示器」搬了出来。
19
+
20
+ **最省事:不用碰终端。** 打开小毛的红皮书卡片,页脚有一个 **「放她到桌面」** 按钮——点她一下,小毛就以独立窗口站上桌面(首次会自动编译,几秒);再点一次「收回桌宠」她就回界面里。不需要 token,不需要双击任何文件。
21
+
22
+ 两条备用路:
23
+
24
+ - 双击仓库根目录的 `启动小毛.command`(自动连当前 DSH、自动 `--public`,重复双击不会叠出第二只);
25
+ - 命令行:`./desktop/build/maoxuan-pet --public`。
26
+
27
+ 点她开合红皮书卡片、拖她换位置(下次还在那儿、能拖出 DSH 窗口)、右键她 = 复位位置 / 退出。详见 [desktop/README.md](desktop/README.md)。
28
+
29
+ > Windows / Linux:界面里的宠物照常可用,但「拖出窗口」的独立壳目前只有 macOS 实现;页面 `pet.html` 与路由是跨平台的,照 `desktop/README.md` 用 Tauri/Electron 包一层即可。
30
+
16
31
  ## 界面
17
32
 
18
33
  ![红皮书气泡](https://raw.githubusercontent.com/yuna78/dsh-maoxuan-pet/main/docs/screenshot-hero.png)
@@ -67,7 +82,8 @@ dsh plugin --profile web add dsh-plugin-maoxuan-pet
67
82
 
68
83
  ## 已知限制
69
84
 
70
- - 只支持 Web 界面(`dsh.client.platform: web`),终端 / TUI 里没有宠物;
85
+ - Web 宠物只支持 Web 界面(`dsh.client.platform: web`),终端 / TUI 里没有宠物;macOS 想让她离开界面,用上面的桌宠壳;
86
+ - 桌宠壳仅 macOS(系统 WebKit);Windows / Linux 需要 Tauri 2 / Electron 起同款壳,`pet.html` 可直接复用;
71
87
  - 定时器只在这个 `dsh web` 进程活着时走;进程停了不会补跑,重启后按 `nextRunAt` 决定是否补一次;
72
88
  - headless 生成每次是独立会话(不复用上下文):稳定,但「不记得昨天说过什么」——所以 prompt 里带最近 6 条督促来避免重复。
73
89
 
package/client.js CHANGED
@@ -22,6 +22,7 @@ window.__ModuleLoader__.load({
22
22
  state: '/api/dsh-maoxuan-pet.state',
23
23
  generate: '/api/dsh-maoxuan-pet.generate',
24
24
  dismiss: '/api/dsh-maoxuan-pet.dismiss',
25
+ shell: '/api/dsh-maoxuan-pet.shell',
25
26
  };
26
27
  const POLL_MS = 20000;
27
28
  const POLL_BUSY_MS = 3000;
@@ -195,6 +196,8 @@ window.__ModuleLoader__.load({
195
196
  const [data, setData] = useState(null);
196
197
  const [error, setError] = useState(null);
197
198
  const [busy, setBusy] = useState(false);
199
+ const [shell, setShell] = useState(null);
200
+ const [shellBusy, setShellBusy] = useState(false);
198
201
  const [open, setOpen] = useState(false);
199
202
  const [pos, setPos] = useState(readPos);
200
203
  const [spriteBroken, setSpriteBroken] = useState(false);
@@ -228,8 +231,12 @@ window.__ModuleLoader__.load({
228
231
 
229
232
  const refresh = useCallback(async () => {
230
233
  try {
231
- const next = await fetchState();
234
+ const [next, shellState] = await Promise.all([
235
+ fetchState(),
236
+ fetch(`${API.shell}?t=${Date.now()}`).then((r) => (r.ok ? r.json() : null)).catch(() => null),
237
+ ]);
232
238
  setData(next);
239
+ setShell(shellState);
233
240
  setError(null);
234
241
  return next;
235
242
  } catch (failure) {
@@ -342,6 +349,32 @@ window.__ModuleLoader__.load({
342
349
  }
343
350
  }, [refresh]);
344
351
 
352
+ // 独立桌宠壳:macOS 上把小毛请出 DSH 窗口(再点一次收回)。
353
+ const toggleShell = useCallback(async () => {
354
+ if (shellBusy) return;
355
+ setShellBusy(true);
356
+ try {
357
+ const response = await fetch(API.shell, {
358
+ method: 'POST',
359
+ headers: { 'content-type': 'application/json' },
360
+ body: JSON.stringify({
361
+ action: shell?.running ? 'quit' : 'launch',
362
+ origin: window.location.origin,
363
+ }),
364
+ });
365
+ const result = await response.json().catch(() => null);
366
+ // 400/500 的 error 也值得说人话:编译失败、平台不支持都会带中文指引
367
+ if (result && result.error) {
368
+ setShell((prev) => ({ ...(prev || {}), error: result.error }));
369
+ }
370
+ await refresh();
371
+ } catch (failure) {
372
+ setShell((prev) => ({ ...(prev || {}), error: String(failure?.message || failure) }));
373
+ } finally {
374
+ setShellBusy(false);
375
+ }
376
+ }, [shell, shellBusy, refresh]);
377
+
345
378
  const startDrag = useCallback((event) => {
346
379
  if (event.button !== 0) return;
347
380
  suppress.current = false;
@@ -427,6 +460,17 @@ window.__ModuleLoader__.load({
427
460
  : '内置语料',
428
461
  // 离线摘录不再用 chip 抢眼位,改成页脚一行小字,诚实但不喧宾夺主。
429
462
  current.mode === 'offline' ? h('span', { className: 'mp-offline' }, ' · 离线摘录') : null),
463
+ shell?.supported
464
+ ? h('button', {
465
+ className: 'mp-btn', type: 'button',
466
+ disabled: shellBusy || shell?.building,
467
+ title: shell?.running
468
+ ? '关掉独立桌宠窗口(她回到 DSH 界面里)'
469
+ : '让她作为一个独立窗口站到桌面上——可以拖到 DSH 窗口外面,关掉 DSH 也还在',
470
+ onClick: toggleShell,
471
+ 'aria-label': shell?.running ? '收回独立桌宠' : '放她到桌面',
472
+ }, shellBusy || shell?.building ? '开关中…' : shell?.running ? '收回桌宠' : '放她到桌面')
473
+ : null,
430
474
  h('button', { className: 'mp-btn', type: 'button', onClick: dismiss, 'aria-label': '收起这条督促' }, '收起'),
431
475
  h('button', {
432
476
  className: 'mp-btn', type: 'button', 'data-primary': 'on',
@@ -438,6 +482,7 @@ window.__ModuleLoader__.load({
438
482
  className: 'mp-btn', type: 'button', 'data-primary': 'on',
439
483
  disabled: running, onClick: regenerate, 'aria-label': '让小毛现在读一篇',
440
484
  }, running ? '翻书中…' : '现在读一篇')),
485
+ shell?.error ? h('p', { className: 'mp-warn' }, `桌宠壳:${String(shell.error).slice(0, 90)}`) : null,
441
486
  data?.lastError ? h('p', { className: 'mp-warn' }, `上次生成没成功:${String(data.lastError.message || '').slice(0, 90)}`) : null,
442
487
  error ? h('p', { className: 'mp-warn' }, '连接中断,重试中…') : null)));
443
488
 
@@ -480,6 +525,22 @@ window.__ModuleLoader__.load({
480
525
  // dragged close to the left edge does it flip to the right side.
481
526
  const side = pos && pos.x < 380 ? 'right' : 'left';
482
527
 
528
+ // 桌面模式:独立壳已经站在真桌面上了,界面里的小毛就隐身,
529
+ // 只留一枚收回徽章——两只同时显示只会让人困惑「哪只才是她」。
530
+ if (shell?.running) {
531
+ return h('button', {
532
+ className: 'mp-dock-chip', type: 'button', onClick: toggleShell,
533
+ style: {
534
+ position: 'fixed', right: 18, bottom: 18, zIndex: 2147483000,
535
+ border: '1px solid #ffd9a855', borderRadius: 999, cursor: 'pointer',
536
+ padding: '6px 14px', font: '12px/1.6 -apple-system, sans-serif',
537
+ color: '#ffe9c8', background: 'linear-gradient(180deg,#8f1d22,#6d1418)',
538
+ boxShadow: '0 4px 14px rgba(0,0,0,.28)',
539
+ },
540
+ 'aria-label': '收回桌面上的小毛',
541
+ }, shellBusy ? '收回中…' : '小毛在桌面上 · 点此收回');
542
+ }
543
+
483
544
  return h('div', { style, 'data-maoxuan-pet': true },
484
545
  h('style', { dangerouslySetInnerHTML: { __html: CSS } }),
485
546
  h('div', { className: 'mp-anchor', 'data-side': side }, pet, open ? card : null));
package/cordis.patch.yml CHANGED
@@ -63,6 +63,12 @@
63
63
  name: '@deepseek-ai/dsh-tool-skill'
64
64
  - id: tool-todo
65
65
  name: '@deepseek-ai/dsh-tool-todo'
66
+ config:
67
+ # @deepseek-ai/dsh-tool-todo 把 allowParallelInProgress 声明为
68
+ # required(rc.2 起如此),缺省不会合并默认值;不写这里 preset
69
+ # 挂载就会以 “$.allowParallelInProgress missing required value”
70
+ # 失败,预设面板上表现为「加载失败 / Failed to load」。
71
+ allowParallelInProgress: false
66
72
  - id: tool-ask-user
67
73
  name: '@deepseek-ai/dsh-tool-ask-user'
68
74
  - id: tool-web
@@ -0,0 +1,20 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>CFBundleIdentifier</key>
6
+ <string>dev.yuna.dsh-maoxuan-pet.shell</string>
7
+ <key>CFBundleName</key>
8
+ <string>maoxuan-pet</string>
9
+ <key>CFBundlePackageType</key>
10
+ <string>APPL</string>
11
+ <!-- 透明窗要画的宿主数据走 http://127.0.0.1,必须放开 ATS -->
12
+ <key>NSAppTransportSecurity</key>
13
+ <dict>
14
+ <key>NSAllowsArbitraryLoads</key>
15
+ <true/>
16
+ </dict>
17
+ <key>NSHighResolutionCapable</key>
18
+ <true/>
19
+ </dict>
20
+ </plist>
@@ -0,0 +1,111 @@
1
+ # 桌面壳:让小毛跑出 DSH 窗口(macOS)
2
+
3
+ DSH 自带宠物之所以能「跑出去」,是因为它**根本不在网页里**——壳层 Rust 进程建了第二个
4
+ 原生窗口(透明、置顶、跨所有 Space)。网页(无论浏览器还是 iframe)永远画不出视口之外,
5
+ 这是浏览器沙箱的硬边界。
6
+
7
+ 本目录给「鲸鱼娘·小毛」配了一个**独立的原生壳**:进程独立于 DSH,DSH 主窗口最小化、
8
+ 甚至 Web 界面整个关掉,她都站在桌面上。毛选的灵魂(每小时一篇的 headless 生成)
9
+ 仍由 `index.js` 宿主插件负责,壳只是把「显示器」搬了出来。
10
+
11
+ ## 架构
12
+
13
+ ```
14
+ ┌─ DSH 宿主(照旧,什么都不用改) ─────────────┐
15
+ │ index.js 插件:每小时 headless 读毛选 → │
16
+ │ GET /api/dsh-maoxuan-pet.state │
17
+ │ POST /api/dsh-maoxuan-pet.generate|dismiss │
18
+ │ GET /api/dsh-maoxuan-pet.asset?name=… │
19
+ └──────────────┬──────────────────────────────┘
20
+ │ URLSession 原生轮询(绕开 WKWebView 跨域)
21
+ ┌──────────────┴──────────────────────────────┐
22
+ │ maoxuan-pet(独立 Swift 进程) │
23
+ │ · 透明无边框 NSWindow,.floating 置顶, │
24
+ │ canJoinAllSpaces(所有桌面/全屏旁可见) │
25
+ │ · WKWebView 渲染 pet.html(宠物+红皮书卡片) │
26
+ │ · hitTest 白名单矩形 = 点击穿透 │
27
+ │ (等价 dsh-tauri 的 set_pet_input_region) │
28
+ │ · 拖拽/位置持久化/右键菜单/退出 │
29
+ └─────────────────────────────────────────────┘
30
+ ```
31
+
32
+ 对照 DSH 自带宠物窗口(从壳层二进制里核实到的参数):
33
+ `transparent` / `alwaysOnTop` / `visibleOnAllWorkspaces` / `skipTaskbar` /
34
+ `set_pet_input_region` / `set_pet_ignore_cursor_events` / 位置持久化到 store——
35
+ 本壳一一对应,只是把宿主从 Tauri 换成了一个几十 KB 的独立进程。
36
+
37
+ ## 构建与运行
38
+
39
+ ```sh
40
+ npm run pet:build # swiftc 编译(约几秒,产物 desktop/build/maoxuan-pet)
41
+ npm run pet # 编译并前台运行(Ctrl-C 或右键小毛→退出)
42
+ ```
43
+
44
+ 参数:
45
+
46
+ | 参数 | 说明 |
47
+ | --- | --- |
48
+ | `--origin=URL` | 宿主地址,默认 `http://127.0.0.1:3081` |
49
+ | `--port=N` | 等价 `--origin=http://127.0.0.1:N` |
50
+ | `--public` | **推荐**:走插件注册的 cookie-less 公开路由(`/dsh-maoxuan-pet/…`),不需要 token |
51
+ | `--token=TOKEN` | 备选:宿主 launch token,先换签名 cookie 再走 `/api` |
52
+ | `--reset` | 忽略保存的位置,回到屏幕右下 |
53
+ | `--verbose` | 向 stderr 打轮询/POST/输入区日志 |
54
+
55
+ 无 Dock 图标、不进 Cmd-Tab(`NSActivationPolicy.accessory`)。窗口位置存在
56
+ `UserDefaults`(suite `dsh-maoxuan-pet.window`),拖到哪儿下次就在哪儿。
57
+
58
+ ## 鉴权(DSH 0.2.1-rc.2+ 宿主必读)
59
+
60
+ 从 DSH `0.2.1-rc.2` 起宿主 Web API 全部要鉴权:launch token 只在 `GET /` 换签名
61
+ cookie(`dsh-auth-*`),`/api/*` 不带 cookie 一律 401,也不接受 Authorization 头。
62
+ 而 token 只有 CLI 宿主(`dsh web`)会打印,桌面 App 从不外泄——所以壳不能只依赖 `/api`。
63
+
64
+ 本插件因此自己开了一组 **loopback 公开路由**(`SHELL_ROUTES`,默认开启):
65
+
66
+ | 路由 | 方法 | 说明 |
67
+ | --- | --- | --- |
68
+ | `/dsh-maoxuan-pet/pet.json` | GET | 与 `/api/dsh-maoxuan-pet.state` 同一份快照 |
69
+ | `/dsh-maoxuan-pet/asset.png?name=idle` | GET | 立绘(idle/talk/blink/read) |
70
+ | `/dsh-maoxuan-pet/generate` | POST | 同「再督促」(要 `x-dsh-maoxuan-pet: 1` 头) |
71
+ | `/dsh-maoxuan-pet/dismiss` | POST | 同「收起」(要 `x-dsh-maoxuan-pet: 1` 头) |
72
+
73
+ 安全性:监听只绑 loopback,公网到不了;GET 只有毛选快照与立绘;POST 强制 JSON
74
+ content-type + 自定义头,跨站页面发不出这样的请求(需要 CORS 预检),因此不是 CSRF 洞。
75
+ 不需要时可用 `publicRoutes: false` 在插件配置里关掉。
76
+
77
+ 两条路怎么选:
78
+
79
+ - **`--public`(默认推荐)**:不需要 token,桌面 App 旁边也能满血。
80
+ - **`--token=<TOKEN>`**:CLI 宿主会打印 `dsh web: http://127.0.0.1:<port>/?token=<TOKEN>`,
81
+ 复制给壳即可;壳用它换签名 cookie 后走 `/api`(cookie 失效会自动重换)。
82
+
83
+ 日常最省事:双击仓库根目录的 `启动小毛.command`——它自动找最近一次写入的
84
+ `~/.dsh/agent-tree-dock-<port>.json` 得到宿主地址,然后用 `--public` 起壳。
85
+
86
+ ## 界面开关(不用双击)
87
+
88
+ 打开 Web 界面里小毛的红皮书卡片,页脚有 **「放她到桌面」/「收回桌宠」** 按钮:
89
+ 宿主插件通过 `POST /api/dsh-maoxuan-pet.shell` 拉起/结束本进程(见 `docs/API-CONTRACT.md`)。
90
+ 状态读 pidfile,所以无论壳是按钮起的还是双击 `启动小毛.command` 起的,按钮都认得出;
91
+ 壳自己还有单实例保护(已有活着的实例时新实例直接退出),不会叠出两只。
92
+
93
+ ## 交互
94
+
95
+ - **点小毛**:开合红皮书卡片;新气泡照样自动弹出、600ms 后自动标记已读——与 web 版同语义。
96
+ - **拖小毛或卡片标题栏**:整窗移动;原地松手才算点击(>4px 判定为拖拽)。
97
+ - **右键小毛**:复位位置 / 退出小毛。
98
+ - **Esc**:收起卡片。
99
+ - 宿主不在时:圆点变红、卡片内提示「连接中断,重试中…」,人继续站着。
100
+
101
+ ## 已知边界
102
+
103
+ - 仅 macOS(用系统 WebKit,零第三方依赖)。Windows/Linux 想要同款,
104
+ 照 `docs/` 里 dsh-tauri 的窗口参数用 Tauri 2 或 Electron 起一个同样的壳即可,
105
+ 页面(pet.html)不用改——它只依赖 `__petState/__petSprites/__petOffline/__petActionDone`
106
+ 几个注入点和 `webkit.messageHandlers.pet` 一个消息通道。
107
+ - **精灵图走原生层转 data URL**:WebKit 的 ATS 不认 loopback 豁免,
108
+ `file://` 页面直挂 `http://127.0.0.1` 的图会被拦(表现为只剩占位鲸),
109
+ 所以素材由 URLSession 拉取后 base64 注入,WebKit 全程零网络请求。
110
+ - 宿主关掉后她只是「待机」并不会死——离线轮询 8s 一次,开销可忽略。
111
+ - 本壳不上传任何东西;全部数据都来自本机 harness 端口。
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env bash
2
+ # 构建独立桌宠壳:纯 Swift + 系统 WebKit,无第三方依赖。
3
+ # 产物:desktop/build/maoxuan-pet(单文件可执行,pet.html 同目录随行)。
4
+ set -euo pipefail
5
+ cd "$(dirname "$0")/.."
6
+
7
+ mkdir -p desktop/build
8
+ # 模块缓存放进项目内:系统默认缓存目录(/var/folders/.../C/clang)可能不可写
9
+ swiftc -O \
10
+ -module-cache-path desktop/build/.module-cache \
11
+ -o desktop/build/maoxuan-pet \
12
+ desktop/maoxuan-pet.swift \
13
+ -Xlinker -sectcreate -Xlinker __TEXT -Xlinker __info_plist -Xlinker desktop/Info.plist
14
+ cp desktop/pet.html desktop/build/pet.html
15
+
16
+ echo "built: desktop/build/maoxuan-pet"
17
+ echo "run: ./desktop/build/maoxuan-pet [--origin=http://127.0.0.1:3081] [--token=…] [--reset] [--verbose]"