dsh-deeppilot 0.6.1 → 0.6.2
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/COMPATIBILITY.md +4 -4
- package/PRIVACY.md +9 -1
- package/README.md +19 -19
- package/README.zh-CN.md +17 -17
- package/lib/index.d.ts +3 -0
- package/lib/index.js +193 -3
- package/lib/index.js.map +1 -1
- package/package.json +16 -16
package/COMPATIBILITY.md
CHANGED
|
@@ -7,7 +7,7 @@ This file separates tested evidence from intended behavior. Passing unit tests d
|
|
|
7
7
|
| Component | Baseline | Evidence |
|
|
8
8
|
|---|---|---|
|
|
9
9
|
| Node.js | 22 or newer | package engine and CI |
|
|
10
|
-
| DSH CLI and Host API | `0.1.
|
|
10
|
+
| DSH CLI and Host API | `0.1.5-rc.1` minimum for plugin `0.6.x` | rc.1 package-family typecheck/build; bridge and `/phone` protocol tests |
|
|
11
11
|
| Host OS | macOS, Linux, Windows on packaged amd64/arm64 helper targets | helper checksums plus user-confirmed Windows/Linux Funnel launch and connection; local LAN validation remains part of alpha testing |
|
|
12
12
|
| Remote access | Tailscale Funnel, ports 443/8443/10000 | helper and supervisor tests |
|
|
13
13
|
| iOS | native DeepPilot client, protocol v2 | simulator build and v2 pairing/challenge evidence |
|
|
@@ -18,8 +18,8 @@ This file separates tested evidence from intended behavior. Passing unit tests d
|
|
|
18
18
|
- Bearer authentication, URL credentials, and first-frame shared tokens are rejected. A supported client registers a P-256 public key through `/phone/pair` and signs each WebSocket challenge.
|
|
19
19
|
- Without a compatible embedded helper, the core bridge and trusted-LAN mode can still run; remote Funnel reports `unavailable`.
|
|
20
20
|
- If a DSH Host API is missing, only the dependent capability should be disabled. The plugin must not crash the Host.
|
|
21
|
-
- Plugin `0.6.
|
|
22
|
-
|
|
21
|
+
- Plugin `0.6.x` requires DSH `0.1.5-rc.1` or newer. Older plugin
|
|
22
|
+
versions used developer-preview DSH builds and remain historical artifacts.
|
|
23
23
|
- The compatibility facade converts the
|
|
24
24
|
current Session controller's raw event arrays into the stable wrapped
|
|
25
25
|
history entries consumed by the phone bridge. Sessions persisted by earlier
|
|
@@ -31,7 +31,7 @@ This file separates tested evidence from intended behavior. Passing unit tests d
|
|
|
31
31
|
|
|
32
32
|
- Intel macOS support for the embedded helper;
|
|
33
33
|
- signed/notarized helper distribution;
|
|
34
|
-
- repair or migration of persisted history rejected by the `0.1.
|
|
34
|
+
- repair or migration of persisted history rejected by the `0.1.5-rc.1`
|
|
35
35
|
Host's own session reader;
|
|
36
36
|
- every DSH developer-preview revision;
|
|
37
37
|
- physical-device performance and every carrier/network combination;
|
package/PRIVACY.md
CHANGED
|
@@ -7,7 +7,7 @@ DeepPilot connects directly to the DSH Host controlled by the user. The plugin d
|
|
|
7
7
|
By default, runtime state is stored under `$DSH_HOME/deeppilot/`:
|
|
8
8
|
|
|
9
9
|
- `host-id`: a random, non-secret stable audience identifier;
|
|
10
|
-
- `devices-v2.json`: paired public keys, fingerprints, scopes, device metadata, revocation/last-seen timestamps, notification preferences, APNs alert registrations,
|
|
10
|
+
- `devices-v2.json`: paired public keys, fingerprints, scopes, device metadata, revocation/last-seen timestamps, notification preferences, APNs alert registrations, WidgetKit push registrations (a separate token per device used only for content-free widget refresh invalidations), and at most one Live Activity registration per device (activity identifier, session identifier, APNs Live Activity update token, environment, and expiry);
|
|
11
11
|
- `tailscale/`: local state for the optional embedded tsnet node.
|
|
12
12
|
|
|
13
13
|
The directory is owner-only mode `0700`. The Mac never receives or stores iPhone private signing keys. Pairing codes exist only in plugin memory for up to five minutes and are not written to disk.
|
|
@@ -35,6 +35,14 @@ The relay does not receive complete history, attachments, live token streams, un
|
|
|
35
35
|
|
|
36
36
|
The iOS home-screen widget may register a separate WidgetKit push token (iOS 26+). These invalidations are content-free: the APNs payload is fixed to `{ "aps": { "content-changed": true } }` with no session, title, or body fields, so neither Apple nor the relay learns anything about Host activity beyond "something changed, please refetch". The widget then pulls data over its own short-lived, authenticated read-only connection directly to the user's DSH Host. Widget tokens are stored alongside the alert token in `devices-v2.json`, are throttled to at most one invalidation per Host per 30 seconds, expire after 7 days without re-registration, and are deleted when the device is revoked.
|
|
37
37
|
|
|
38
|
+
## Live Activity push
|
|
39
|
+
|
|
40
|
+
A Live Activity on the Lock Screen or in the Dynamic Island may register a separate ActivityKit **update** token. Unlike the WidgetKit path, Live Activity updates are **not content-free**: each push carries a bounded progress projection — the session title (up to 100 Unicode code points), the current in-progress or pending todo item's text (up to 160 code points), the todo done/total counts, and a fixed phase value (`running`, `approval`, `question`, `ended`, or `unavailable`). This projection goes to Apple in direct APNs mode and to the relay in relay mode. It never contains conversation history, message bodies, prompt text, attachments, full todo lists, file paths, or model output.
|
|
41
|
+
|
|
42
|
+
The activity must be started by the user on the iPhone; there is no push-to-start, and the activity opens no network connection of its own. Only one Live Activity registration is kept per device, registrations expire after eight hours, and the registration is deleted on unregister, token rotation, or device revocation. Relay mode carries only this bounded projection and rejects unknown content-state fields, oversized text, invalid counts, stale timestamps, and event/phase mismatches; the updated relay must be deployed before background Live Activity updates work through relay mode. System delivery and refresh budgets still apply, so updates may be coalesced, delayed, or dropped by iOS.
|
|
43
|
+
|
|
44
|
+
`push.contentMode: generic` applies to alert notifications only. Live Activity content states always carry the bounded title and current-task text described above, because the activity is rendered from this push; do not register a Live Activity on a device whose Lock Screen should never show session text.
|
|
45
|
+
|
|
38
46
|
## Logs
|
|
39
47
|
|
|
40
48
|
Routine logs contain lifecycle state, delivery outcomes, and process-local salted hashes for source/device identifiers. Logs are designed not to contain pairing codes, key material, APNs tokens, or message bodies. Frame diagnostics appear only with `debug`.
|
package/README.md
CHANGED
|
@@ -37,21 +37,21 @@ does not require the helper.
|
|
|
37
37
|
|
|
38
38
|
| Plugin version | Required DSH | How to install |
|
|
39
39
|
|---|---|---|
|
|
40
|
-
| `0.6.
|
|
41
|
-
| `0.5.x` (previous stable
|
|
40
|
+
| `0.6.x` (current stable, `latest`) | DSH `0.1.5-rc.1` or newer | `dsh plugin --profile web add dsh-deeppilot` |
|
|
41
|
+
| `0.5.x` (previous stable) | DSH `0.1.1-rc.2`–`0.1.2-alpha.1` | `dsh plugin --profile web add dsh-deeppilot@0.5.0` |
|
|
42
42
|
|
|
43
|
-
`0.6.
|
|
44
|
-
[0.1.
|
|
43
|
+
`0.6.x` is built and typechecked against the DSH
|
|
44
|
+
[0.1.5-rc.1](https://www.npmjs.com/package/@deepseek-ai/dsh/v/0.1.5-rc.1)
|
|
45
45
|
Host and client package family. It uses the Gateway multi-client Remote Events
|
|
46
46
|
routing that keeps Web and DeepPilot independently answerable. Earlier plugin
|
|
47
|
-
alphas remain historical artifacts; users installing the
|
|
48
|
-
should update DSH to `0.1.
|
|
47
|
+
alphas remain historical artifacts; users installing the `alpha` tag for
|
|
48
|
+
pre-release testing should update DSH to `0.1.5-rc.1` or newer.
|
|
49
49
|
|
|
50
50
|
```sh
|
|
51
|
-
# DSH 0.1.
|
|
52
|
-
dsh plugin --profile web add dsh-deeppilot@alpha
|
|
53
|
-
# DSH 0.1.1-rc.2 through 0.1.2-alpha.1 (previous stable):
|
|
51
|
+
# DSH 0.1.5-rc.1 or newer (recommended):
|
|
54
52
|
dsh plugin --profile web add dsh-deeppilot
|
|
53
|
+
# DSH 0.1.1-rc.2 through 0.1.2-alpha.1 (previous stable):
|
|
54
|
+
dsh plugin --profile web add dsh-deeppilot@0.5.0
|
|
55
55
|
dsh web
|
|
56
56
|
```
|
|
57
57
|
|
|
@@ -78,33 +78,33 @@ DeepPilot state under `$DSH_HOME/deeppilot/`.
|
|
|
78
78
|
|
|
79
79
|
## Publishing (maintainers)
|
|
80
80
|
|
|
81
|
-
`0.6.
|
|
81
|
+
`0.6.x` targets DSH `0.1.5-rc.1`+; `0.5.x` stays compatible with
|
|
82
82
|
DSH `0.1.1-rc.2`–`0.1.2-alpha.1`. Keep both published:
|
|
83
83
|
|
|
84
84
|
1. Bump `version` in `package.json` and in the root `""` entry of
|
|
85
85
|
`package-lock.json`, then run `npm test && npm run typecheck && npm run build`
|
|
86
86
|
and inspect `npm pack --dry-run --json` (the check
|
|
87
|
-
`tests/compatibility-metadata.test.ts` enforces the `^0.1.
|
|
87
|
+
`tests/compatibility-metadata.test.ts` enforces the `^0.1.5-rc.1` peer
|
|
88
88
|
ranges).
|
|
89
89
|
2. Commit the release and push it. `npm publish` runs `prepack` (build) and
|
|
90
90
|
`prepublishOnly` (test + typecheck) automatically.
|
|
91
|
-
3. Publish
|
|
91
|
+
3. Publish pre-releases without touching `latest`:
|
|
92
92
|
|
|
93
93
|
```sh
|
|
94
94
|
npm publish --tag alpha
|
|
95
95
|
```
|
|
96
96
|
|
|
97
97
|
After a successful publish, `npm view dsh-deeppilot dist-tags --json` shows
|
|
98
|
-
`"latest": "0.
|
|
99
|
-
package by installing it into a DSH `0.1.
|
|
98
|
+
`"latest": "0.6.x"` and `"alpha": "0.6.x-alpha.y"`. Verify the published
|
|
99
|
+
package by installing it into a DSH `0.1.5-rc.1` profile before pointing
|
|
100
100
|
users at it.
|
|
101
|
-
4. Tag the release commit `
|
|
101
|
+
4. Tag the release commit `vX.Y.Z` and prepare a GitHub Release
|
|
102
102
|
(English + 简体中文 notes) that links this README section.
|
|
103
|
-
5.
|
|
104
|
-
`
|
|
105
|
-
|
|
103
|
+
5. Publish stable releases with `npm publish --tag latest`, which moves
|
|
104
|
+
`latest` to the new version. Stable releases must never be published
|
|
105
|
+
with `--tag alpha`.
|
|
106
106
|
|
|
107
|
-
Never run `npm publish` from a copy that still has
|
|
107
|
+
Never run `npm publish` from a copy that still has an older version.
|
|
108
108
|
|
|
109
109
|
|
|
110
110
|
## Connection and privacy
|
package/README.zh-CN.md
CHANGED
|
@@ -34,20 +34,20 @@
|
|
|
34
34
|
|
|
35
35
|
| 插件版本 | 所需 DSH | 安装命令 |
|
|
36
36
|
|---|---|---|
|
|
37
|
-
| `0.6.
|
|
38
|
-
| `0.5.x
|
|
37
|
+
| `0.6.x`(当前稳定版,`latest`) | DSH `0.1.5-rc.1` 或更高 | `dsh plugin --profile web add dsh-deeppilot` |
|
|
38
|
+
| `0.5.x`(旧版稳定版) | DSH `0.1.1-rc.2`–`0.1.2-alpha.1` | `dsh plugin --profile web add dsh-deeppilot@0.5.0` |
|
|
39
39
|
|
|
40
|
-
`0.6.
|
|
41
|
-
[0.1.
|
|
40
|
+
`0.6.x` 基于 DSH
|
|
41
|
+
[0.1.5-rc.1](https://www.npmjs.com/package/@deepseek-ai/dsh/v/0.1.5-rc.1)
|
|
42
42
|
的 Host 与 client 包族构建并完成类型检查。它使用 Gateway 多 Client Remote Events
|
|
43
43
|
路由,使 Web 与 DeepPilot 能独立处理同一交互。旧 alpha 仅保留为历史产物;安装
|
|
44
|
-
|
|
44
|
+
`alpha` tag 进行预发布测试的用户应先把 DSH 更新到 `0.1.5-rc.1` 或更高版本。
|
|
45
45
|
|
|
46
46
|
```sh
|
|
47
|
-
# DSH 0.1.
|
|
48
|
-
dsh plugin --profile web add dsh-deeppilot@alpha
|
|
49
|
-
# DSH 0.1.1-rc.2 至 0.1.2-alpha.1(旧版稳定):
|
|
47
|
+
# DSH 0.1.5-rc.1 或更高(推荐):
|
|
50
48
|
dsh plugin --profile web add dsh-deeppilot
|
|
49
|
+
# DSH 0.1.1-rc.2 至 0.1.2-alpha.1(旧版稳定):
|
|
50
|
+
dsh plugin --profile web add dsh-deeppilot@0.5.0
|
|
51
51
|
dsh web
|
|
52
52
|
```
|
|
53
53
|
|
|
@@ -71,30 +71,30 @@ dsh plugin --profile web remove dsh-deeppilot
|
|
|
71
71
|
|
|
72
72
|
## 发布说明(维护者)
|
|
73
73
|
|
|
74
|
-
`0.6.
|
|
74
|
+
`0.6.x` 面向 DSH `0.1.5-rc.1`+;`0.5.x` 保持兼容 DSH
|
|
75
75
|
`0.1.1-rc.2`–`0.1.2-alpha.1`,两个版本线都要保持发布:
|
|
76
76
|
|
|
77
77
|
1. 同步修改 `package.json` 与 `package-lock.json` 根 `""` 条目中的
|
|
78
78
|
`version`,然后运行 `npm test && npm run typecheck && npm run build`,
|
|
79
79
|
并检查 `npm pack --dry-run --json`(`tests/compatibility-metadata.test.ts`
|
|
80
|
-
会强制校验 `^0.1.
|
|
80
|
+
会强制校验 `^0.1.5-rc.1` 的 peer 范围)。
|
|
81
81
|
2. 提交发布并推送。`npm publish` 会自动执行 `prepack`(构建)与
|
|
82
82
|
`prepublishOnly`(测试 + 类型检查)。
|
|
83
|
-
3.
|
|
83
|
+
3. 发布预发布版本,不要动 `latest`:
|
|
84
84
|
|
|
85
85
|
```sh
|
|
86
86
|
npm publish --tag alpha
|
|
87
87
|
```
|
|
88
88
|
|
|
89
89
|
发布成功后,`npm view dsh-deeppilot dist-tags --json` 应显示
|
|
90
|
-
`"latest": "0.
|
|
91
|
-
DSH `0.1.
|
|
92
|
-
4. 为发布提交打 `
|
|
90
|
+
`"latest": "0.6.x"` 与 `"alpha": "0.6.x-alpha.y"`。向用户推荐前,请先在
|
|
91
|
+
DSH `0.1.5-rc.1` profile 中安装验证发布的包。
|
|
92
|
+
4. 为发布提交打 `vX.Y.Z` tag,并准备包含英文与简体中文说明的
|
|
93
93
|
GitHub Release,链接本 README 的发布说明。
|
|
94
|
-
5.
|
|
95
|
-
|
|
94
|
+
5. 稳定版用 `npm publish --tag latest` 发布,使 `latest` 切换到新版本。
|
|
95
|
+
稳定版绝不使用 `--tag alpha` 发布。
|
|
96
96
|
|
|
97
|
-
|
|
97
|
+
绝不要从仍是旧版本的副本执行 `npm publish`。
|
|
98
98
|
|
|
99
99
|
|
|
100
100
|
## 连接与隐私
|
package/lib/index.d.ts
CHANGED
|
@@ -474,6 +474,7 @@ interface BridgeSink {
|
|
|
474
474
|
interface PushOutlet {
|
|
475
475
|
fanOut(notification: PushNotification): void;
|
|
476
476
|
widgetChanged?(): void;
|
|
477
|
+
liveActivityChanged?(sessions: SessionSummary[]): void;
|
|
477
478
|
/**
|
|
478
479
|
* Whether offline push is currently configured and usable. Drives the
|
|
479
480
|
* welcome capability bit: advertising push while no APNs credentials are
|
|
@@ -530,6 +531,7 @@ declare class HostBridge {
|
|
|
530
531
|
projectSelection: boolean;
|
|
531
532
|
push: boolean;
|
|
532
533
|
widgetPush: boolean;
|
|
534
|
+
liveActivityPush: boolean;
|
|
533
535
|
};
|
|
534
536
|
diagnostic(message: string): void;
|
|
535
537
|
currentCursor(): number;
|
|
@@ -568,6 +570,7 @@ declare class HostBridge {
|
|
|
568
570
|
* frames back (R1/P2).
|
|
569
571
|
*/
|
|
570
572
|
resumeFrom(cursor: number, target?: BridgeSink): boolean;
|
|
573
|
+
refreshLiveActivities(): void;
|
|
571
574
|
private record;
|
|
572
575
|
/** Start consuming host + mux streams. Idempotent; aborts on dispose(). */
|
|
573
576
|
start(): void;
|
package/lib/index.js
CHANGED
|
@@ -243,6 +243,10 @@ function validateRequest(type, value) {
|
|
|
243
243
|
ids.add(answer.id);
|
|
244
244
|
}
|
|
245
245
|
}
|
|
246
|
+
if (type === "c2s.liveActivity.register" || type === "c2s.liveActivity.unregister") {
|
|
247
|
+
if (!text(p.activityId, 128)) return "invalid activityId";
|
|
248
|
+
if (type === "c2s.liveActivity.register" && (!text(p.sessionId) || typeof p.deviceToken !== "string" || !/^[0-9a-fA-F]{32,512}$/.test(p.deviceToken) || !["development", "production"].includes(p.environment) || !optional("enrollKey", (v) => text(v, 128)))) return "invalid live activity registration";
|
|
249
|
+
}
|
|
246
250
|
if (type === "c2s.push.register" || type === "c2s.widget.push.register") {
|
|
247
251
|
if (p.environment !== void 0 && p.environment !== "production" && p.environment !== "development") return "invalid APNs environment";
|
|
248
252
|
if (!optional("enrollKey", (v) => text(v, 128))) return "invalid enrollKey";
|
|
@@ -462,7 +466,8 @@ var DeviceStore = class DeviceStore {
|
|
|
462
466
|
firstSeenTs: existing?.firstSeenTs ?? now,
|
|
463
467
|
lastSeenTs: now,
|
|
464
468
|
...existing?.apns ? { apns: existing.apns } : {},
|
|
465
|
-
...existing?.widgetApns ? { widgetApns: existing.widgetApns } : {}
|
|
469
|
+
...existing?.widgetApns ? { widgetApns: existing.widgetApns } : {},
|
|
470
|
+
...existing?.liveActivity ? { liveActivity: existing.liveActivity } : {}
|
|
466
471
|
};
|
|
467
472
|
this.devices.set(deviceId, next);
|
|
468
473
|
this.flush();
|
|
@@ -488,6 +493,7 @@ var DeviceStore = class DeviceStore {
|
|
|
488
493
|
record.revokedAt = now;
|
|
489
494
|
delete record.apns;
|
|
490
495
|
delete record.widgetApns;
|
|
496
|
+
delete record.liveActivity;
|
|
491
497
|
this.flush();
|
|
492
498
|
return true;
|
|
493
499
|
}
|
|
@@ -533,6 +539,29 @@ var DeviceStore = class DeviceStore {
|
|
|
533
539
|
delete record.apns;
|
|
534
540
|
this.flush();
|
|
535
541
|
}
|
|
542
|
+
setLiveActivity(deviceId, registration) {
|
|
543
|
+
const record = this.authorized(deviceId);
|
|
544
|
+
if (!record || !isValidApnsToken(registration.token)) return;
|
|
545
|
+
const old = record.liveActivity;
|
|
546
|
+
record.liveActivity = old?.activityId === registration.activityId ? {
|
|
547
|
+
...registration,
|
|
548
|
+
expiresAt: old.expiresAt,
|
|
549
|
+
endedState: old.endedState
|
|
550
|
+
} : registration;
|
|
551
|
+
this.flush();
|
|
552
|
+
}
|
|
553
|
+
endLiveActivity(deviceId, token, state) {
|
|
554
|
+
const registration = this.devices.get(deviceId)?.liveActivity;
|
|
555
|
+
if (!registration || registration.token !== token || registration.endedState) return;
|
|
556
|
+
registration.endedState = state;
|
|
557
|
+
this.flush();
|
|
558
|
+
}
|
|
559
|
+
clearLiveActivity(deviceId, activityId, token) {
|
|
560
|
+
const record = this.devices.get(deviceId);
|
|
561
|
+
if (record?.liveActivity?.activityId !== activityId || token !== void 0 && record.liveActivity.token !== token) return;
|
|
562
|
+
delete record.liveActivity;
|
|
563
|
+
this.flush();
|
|
564
|
+
}
|
|
536
565
|
setWidgetPushToken(deviceId, token, environment, now) {
|
|
537
566
|
const record = this.authorized(deviceId);
|
|
538
567
|
if (!record || !isValidApnsToken(token)) return;
|
|
@@ -613,6 +642,7 @@ function requiredScope(type) {
|
|
|
613
642
|
if (type === "c2s.session.sendPrompt" || type === "c2s.session.delivery") return "prompt.send";
|
|
614
643
|
if (type === "c2s.pending.list") return "interactions.respond";
|
|
615
644
|
if (type === "c2s.approval.respond" || type === "c2s.question.respond") return "interactions.respond";
|
|
645
|
+
if (type === "c2s.liveActivity.register" || type === "c2s.liveActivity.unregister") return "notifications.register";
|
|
616
646
|
if (type === "c2s.push.register" || type === "c2s.widget.push.register") return "notifications.register";
|
|
617
647
|
if (type === "c2s.workspace.create" || type === "c2s.session.create" || type === "c2s.session.rename" || type === "c2s.session.archive" || type === "c2s.session.cancel" || type === "c2s.session.selectModel") return "sessions.manage";
|
|
618
648
|
if (type.startsWith("c2s.")) return "sessions.read";
|
|
@@ -693,6 +723,8 @@ var BridgeConnection = class {
|
|
|
693
723
|
deviceId;
|
|
694
724
|
scopes = /* @__PURE__ */ new Set();
|
|
695
725
|
widgetClient = false;
|
|
726
|
+
liveActivityRegistrationGeneration = 0;
|
|
727
|
+
pendingLiveActivityId;
|
|
696
728
|
authChallenge;
|
|
697
729
|
constructor(ws, deps) {
|
|
698
730
|
this.ws = ws;
|
|
@@ -1119,6 +1151,49 @@ var BridgeConnection = class {
|
|
|
1119
1151
|
this.send("s2c.ack", {}, env.id);
|
|
1120
1152
|
return;
|
|
1121
1153
|
}
|
|
1154
|
+
case "c2s.liveActivity.unregister": {
|
|
1155
|
+
const p = env.payload;
|
|
1156
|
+
if (this.pendingLiveActivityId === p.activityId) {
|
|
1157
|
+
this.liveActivityRegistrationGeneration += 1;
|
|
1158
|
+
this.pendingLiveActivityId = void 0;
|
|
1159
|
+
}
|
|
1160
|
+
this.deps.devices.clearLiveActivity(this.deviceId, p.activityId);
|
|
1161
|
+
this.send("s2c.ack", {}, env.id);
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
case "c2s.liveActivity.register": {
|
|
1165
|
+
const p = env.payload;
|
|
1166
|
+
const initial = this.deviceId && this.deps.devices.authorized(this.deviceId);
|
|
1167
|
+
if (!initial || ![
|
|
1168
|
+
"notifications.register",
|
|
1169
|
+
"sessions.read",
|
|
1170
|
+
"interactions.respond"
|
|
1171
|
+
].every((scope) => initial.scopes?.includes(scope))) return this.fail(env.id, "E_FORBIDDEN", "live activity permissions required");
|
|
1172
|
+
const generation = ++this.liveActivityRegistrationGeneration;
|
|
1173
|
+
this.pendingLiveActivityId = p.activityId;
|
|
1174
|
+
if (p.enrollKey) await this.deps.onPushEnrollKey?.(p.enrollKey);
|
|
1175
|
+
if (generation !== this.liveActivityRegistrationGeneration) return this.fail(env.id, "E_BUSY", "live activity registration superseded");
|
|
1176
|
+
const record = this.deviceId && this.deps.devices.authorized(this.deviceId);
|
|
1177
|
+
if (this.closed || !record || ![
|
|
1178
|
+
"notifications.register",
|
|
1179
|
+
"sessions.read",
|
|
1180
|
+
"interactions.respond"
|
|
1181
|
+
].every((scope) => record.scopes?.includes(scope))) return this.fail(env.id, "E_FORBIDDEN", "live activity permissions required");
|
|
1182
|
+
if (!this.deps.bridge.listSessions().some((row) => row.id === p.sessionId)) return this.fail(env.id, "E_NOT_FOUND", "session not found");
|
|
1183
|
+
const previous = record.liveActivity;
|
|
1184
|
+
if (previous?.activityId === p.activityId && previous.sessionId !== p.sessionId) return this.fail(env.id, "E_PROTOCOL", "activity is bound to another session");
|
|
1185
|
+
this.deps.devices.setLiveActivity(record.deviceId, {
|
|
1186
|
+
activityId: p.activityId,
|
|
1187
|
+
sessionId: p.sessionId,
|
|
1188
|
+
token: p.deviceToken.toLowerCase(),
|
|
1189
|
+
environment: p.environment,
|
|
1190
|
+
updatedAt: Date.now(),
|
|
1191
|
+
expiresAt: Date.now() + 288e5
|
|
1192
|
+
});
|
|
1193
|
+
this.deps.bridge.refreshLiveActivities();
|
|
1194
|
+
this.send("s2c.ack", { enabled: this.deps.bridge.capabilities.push }, env.id);
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1122
1197
|
case "c2s.widget.push.register":
|
|
1123
1198
|
case "c2s.push.register": {
|
|
1124
1199
|
const widget = env.type === "c2s.widget.push.register";
|
|
@@ -1298,6 +1373,7 @@ function projectEvent(sessionId, event) {
|
|
|
1298
1373
|
kind: "turn.end",
|
|
1299
1374
|
data: { ok: event.data?.reason?.kind === "completed" }
|
|
1300
1375
|
};
|
|
1376
|
+
case "system/message": return null;
|
|
1301
1377
|
case "user/message": return {
|
|
1302
1378
|
kind: "message.final",
|
|
1303
1379
|
data: { ...limitMessageProjection({
|
|
@@ -1560,6 +1636,7 @@ function projectHistory(events) {
|
|
|
1560
1636
|
ts: tsOf(event)
|
|
1561
1637
|
};
|
|
1562
1638
|
switch (event.type) {
|
|
1639
|
+
case "system/message": break;
|
|
1563
1640
|
case "user/message":
|
|
1564
1641
|
messages.push({
|
|
1565
1642
|
...base,
|
|
@@ -1777,7 +1854,8 @@ var HostBridge = class {
|
|
|
1777
1854
|
sessionManagement: typeof this.apiProxy.sessions.rename === "function" && typeof this.apiProxy.workspace?.archiveSession === "function",
|
|
1778
1855
|
projectSelection: typeof this.apiProxy.workspace?.list === "function" && typeof this.apiProxy.workspace?.create === "function",
|
|
1779
1856
|
push: this.pushOutlet?.isAvailable() === true,
|
|
1780
|
-
widgetPush: true
|
|
1857
|
+
widgetPush: true,
|
|
1858
|
+
liveActivityPush: true
|
|
1781
1859
|
};
|
|
1782
1860
|
}
|
|
1783
1861
|
diagnostic(message) {
|
|
@@ -1890,16 +1968,23 @@ var HostBridge = class {
|
|
|
1890
1968
|
for (const sink of receivers) sink.replayDone();
|
|
1891
1969
|
return true;
|
|
1892
1970
|
}
|
|
1971
|
+
refreshLiveActivities() {
|
|
1972
|
+
try {
|
|
1973
|
+
this.pushOutlet?.liveActivityChanged?.(this.listSessions());
|
|
1974
|
+
} catch {}
|
|
1975
|
+
}
|
|
1893
1976
|
record(type, payload, except) {
|
|
1894
1977
|
if (this.disposed) return;
|
|
1978
|
+
if (type === "s2c.sessions.delta") this.refreshLiveActivities();
|
|
1895
1979
|
if (type === "s2c.sessions.delta" || type.startsWith("s2c.pending.")) {
|
|
1896
1980
|
const fingerprint = JSON.stringify([
|
|
1897
|
-
this.listSessions().map(({ id, title, status, lastActivityTs, todos, pendingApproval, pendingQuestion }) => ({
|
|
1981
|
+
this.listSessions().map(({ id, title, status, lastActivityTs, todos, todoItems, pendingApproval, pendingQuestion }) => ({
|
|
1898
1982
|
id,
|
|
1899
1983
|
title,
|
|
1900
1984
|
status,
|
|
1901
1985
|
lastActivityTs,
|
|
1902
1986
|
todos,
|
|
1987
|
+
todoItems,
|
|
1903
1988
|
pendingApproval,
|
|
1904
1989
|
pendingQuestion
|
|
1905
1990
|
})),
|
|
@@ -4094,6 +4179,12 @@ function p8ToDer(pem) {
|
|
|
4094
4179
|
}
|
|
4095
4180
|
/** Pure payload builder so tests can assert the wire format without sockets. */
|
|
4096
4181
|
function apnsPayload(notification) {
|
|
4182
|
+
if ("kind" in notification && notification.kind === "liveactivity") return { aps: {
|
|
4183
|
+
timestamp: notification.timestamp,
|
|
4184
|
+
event: notification.event,
|
|
4185
|
+
"content-state": notification.contentState,
|
|
4186
|
+
...notification.event === "end" ? { "dismissal-date": notification.timestamp + 300 } : { "stale-date": notification.timestamp + 180 }
|
|
4187
|
+
} };
|
|
4097
4188
|
if ("kind" in notification && notification.kind === "widget") return { aps: { "content-changed": true } };
|
|
4098
4189
|
return alertPayload(notification);
|
|
4099
4190
|
}
|
|
@@ -4117,6 +4208,12 @@ function alertPayload(notification) {
|
|
|
4117
4208
|
};
|
|
4118
4209
|
}
|
|
4119
4210
|
function pushHeaders(bundleId, notification) {
|
|
4211
|
+
if ("kind" in notification && notification.kind === "liveactivity") return {
|
|
4212
|
+
"apns-topic": bundleId + ".push-type.liveactivity",
|
|
4213
|
+
"apns-push-type": "liveactivity",
|
|
4214
|
+
"apns-priority": notification.event === "end" ? "10" : "5",
|
|
4215
|
+
"apns-expiration": String(notification.timestamp + 180)
|
|
4216
|
+
};
|
|
4120
4217
|
if ("kind" in notification && notification.kind === "widget") return {
|
|
4121
4218
|
"apns-topic": bundleId + ".push-type.widgets",
|
|
4122
4219
|
"apns-push-type": "widgets",
|
|
@@ -5331,6 +5428,79 @@ var WidgetPushScheduler = class {
|
|
|
5331
5428
|
}
|
|
5332
5429
|
};
|
|
5333
5430
|
//#endregion
|
|
5431
|
+
//#region src/live-activity.ts
|
|
5432
|
+
function liveActivityState(session) {
|
|
5433
|
+
const total = Math.max(0, session?.todos?.total ?? 0);
|
|
5434
|
+
const done = Math.min(total, Math.max(0, session?.todos?.done ?? 0));
|
|
5435
|
+
const task = session?.todoItems?.find((item) => item.status === "in_progress") ?? session?.todoItems?.find((item) => item.status === "pending");
|
|
5436
|
+
return {
|
|
5437
|
+
title: Array.from(session?.title ?? "").slice(0, 100).join(""),
|
|
5438
|
+
task: Array.from(task?.content ?? "").slice(0, 160).join(""),
|
|
5439
|
+
done,
|
|
5440
|
+
total,
|
|
5441
|
+
phase: !session ? "unavailable" : session.pendingApproval ? "approval" : session.pendingQuestion ? "question" : session.status === "running" ? "running" : session.status === "idle" ? "ended" : "unavailable"
|
|
5442
|
+
};
|
|
5443
|
+
}
|
|
5444
|
+
/** Persist terminal state before coalescing so a rapid next round cannot revive an activity. */
|
|
5445
|
+
var LiveActivityPushManager = class {
|
|
5446
|
+
devices;
|
|
5447
|
+
send;
|
|
5448
|
+
latest = /* @__PURE__ */ new Map();
|
|
5449
|
+
sent = /* @__PURE__ */ new Map();
|
|
5450
|
+
scheduler;
|
|
5451
|
+
constructor(devices, send, intervalMs = 15e3) {
|
|
5452
|
+
this.devices = devices;
|
|
5453
|
+
this.send = send;
|
|
5454
|
+
this.scheduler = new WidgetPushScheduler(() => this.flush(), intervalMs);
|
|
5455
|
+
}
|
|
5456
|
+
changed(sessions) {
|
|
5457
|
+
this.latest = new Map(sessions.map((session) => [session.id, session]));
|
|
5458
|
+
for (const device of this.devices()?.list() ?? []) {
|
|
5459
|
+
const r = device.liveActivity;
|
|
5460
|
+
if (!r) continue;
|
|
5461
|
+
const state = liveActivityState(this.latest.get(r.sessionId));
|
|
5462
|
+
if (state.phase === "ended" || state.phase === "unavailable") this.devices()?.endLiveActivity(device.deviceId, r.token, state);
|
|
5463
|
+
}
|
|
5464
|
+
this.scheduler.changed();
|
|
5465
|
+
}
|
|
5466
|
+
async flush() {
|
|
5467
|
+
const devices = this.devices();
|
|
5468
|
+
if (!devices) return;
|
|
5469
|
+
const valid = /* @__PURE__ */ new Set();
|
|
5470
|
+
for (const device of devices.list()) {
|
|
5471
|
+
const r = device.liveActivity;
|
|
5472
|
+
if (!r) continue;
|
|
5473
|
+
const key = device.deviceId + ":" + r.token;
|
|
5474
|
+
valid.add(key);
|
|
5475
|
+
if (device.revokedAt !== void 0 || ![
|
|
5476
|
+
"notifications.register",
|
|
5477
|
+
"sessions.read",
|
|
5478
|
+
"interactions.respond"
|
|
5479
|
+
].every((scope) => device.scopes?.some((value) => value === scope)) || r.expiresAt <= Date.now()) {
|
|
5480
|
+
devices.clearLiveActivity(device.deviceId, r.activityId, r.token);
|
|
5481
|
+
continue;
|
|
5482
|
+
}
|
|
5483
|
+
const state = r.endedState ?? liveActivityState(this.latest.get(r.sessionId));
|
|
5484
|
+
const fingerprint = JSON.stringify(state);
|
|
5485
|
+
if (this.sent.get(key) === fingerprint) continue;
|
|
5486
|
+
if (devices.authorized(device.deviceId)?.liveActivity?.token !== r.token) continue;
|
|
5487
|
+
const result = await this.send(r.token, r.environment, {
|
|
5488
|
+
kind: "liveactivity",
|
|
5489
|
+
event: r.endedState ? "end" : "update",
|
|
5490
|
+
timestamp: Math.floor(Date.now() / 1e3),
|
|
5491
|
+
contentState: state
|
|
5492
|
+
}).catch(() => ({ outcome: "failed" }));
|
|
5493
|
+
if (result.outcome === "sent") this.sent.set(key, fingerprint);
|
|
5494
|
+
else if (result.outcome === "invalid-token") devices.clearLiveActivity(device.deviceId, r.activityId, r.token);
|
|
5495
|
+
else this.scheduler.changed();
|
|
5496
|
+
}
|
|
5497
|
+
for (const key of this.sent.keys()) if (!valid.has(key)) this.sent.delete(key);
|
|
5498
|
+
}
|
|
5499
|
+
dispose() {
|
|
5500
|
+
this.scheduler.dispose();
|
|
5501
|
+
}
|
|
5502
|
+
};
|
|
5503
|
+
//#endregion
|
|
5334
5504
|
//#region src/phone-server.ts
|
|
5335
5505
|
/**
|
|
5336
5506
|
* A deliberately narrow transport listener. Both the LAN endpoint and the
|
|
@@ -5818,8 +5988,27 @@ function apply(ctx, options) {
|
|
|
5818
5988
|
}
|
|
5819
5989
|
}
|
|
5820
5990
|
});
|
|
5991
|
+
const liveActivityPush = new LiveActivityPushManager(() => auth.devices ?? void 0, async (deviceToken, environment, notification) => {
|
|
5992
|
+
if (!enabledNow()) return { outcome: "failed" };
|
|
5993
|
+
const resolved = resolvePushConfig(currentConfig());
|
|
5994
|
+
if (!resolved.ok) return { outcome: "failed" };
|
|
5995
|
+
const send = await senderFor(resolved.value);
|
|
5996
|
+
if (!send) return { outcome: "failed" };
|
|
5997
|
+
const result = await send({
|
|
5998
|
+
deviceToken,
|
|
5999
|
+
environment,
|
|
6000
|
+
notification
|
|
6001
|
+
});
|
|
6002
|
+
if (resolved.value.kind === "relay" && result.reason === "HTTP 401" && enrollmentCell.token === resolved.value.token && enrollmentCell.enrollKey) {
|
|
6003
|
+
enrollmentCell.token = void 0;
|
|
6004
|
+
persistEnrollment();
|
|
6005
|
+
await ensureRelayEnrolled(resolved.value.url);
|
|
6006
|
+
}
|
|
6007
|
+
return result;
|
|
6008
|
+
});
|
|
5821
6009
|
const makePushOutlet = () => ({
|
|
5822
6010
|
widgetChanged: () => widgetPush.changed(),
|
|
6011
|
+
liveActivityChanged: (sessions) => liveActivityPush.changed(sessions),
|
|
5823
6012
|
isAvailable: () => {
|
|
5824
6013
|
const resolved = resolvePushConfig(currentConfig());
|
|
5825
6014
|
if (!resolved.ok) return false;
|
|
@@ -6455,6 +6644,7 @@ function apply(ctx, options) {
|
|
|
6455
6644
|
cachedSender = void 0;
|
|
6456
6645
|
updateChecker.dispose();
|
|
6457
6646
|
widgetPush.dispose();
|
|
6647
|
+
liveActivityPush.dispose();
|
|
6458
6648
|
const wssClosed = new Promise((resolve) => wss.close(() => resolve()));
|
|
6459
6649
|
await Promise.allSettled([
|
|
6460
6650
|
enrollmentWriteTail,
|