dsh-remote-control 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/LICENSE +21 -0
  2. package/cordis.patch.yml +21 -0
  3. package/dist/bundle/index.js +33956 -0
  4. package/dist/src/core/clock.d.ts +45 -0
  5. package/dist/src/core/clock.js +83 -0
  6. package/dist/src/core/clock.js.map +1 -0
  7. package/dist/src/core/keys.d.ts +98 -0
  8. package/dist/src/core/keys.js +176 -0
  9. package/dist/src/core/keys.js.map +1 -0
  10. package/dist/src/core/pairing-window.d.ts +95 -0
  11. package/dist/src/core/pairing-window.js +92 -0
  12. package/dist/src/core/pairing-window.js.map +1 -0
  13. package/dist/src/core/runtime.d.ts +104 -0
  14. package/dist/src/core/runtime.js +491 -0
  15. package/dist/src/core/runtime.js.map +1 -0
  16. package/dist/src/core/sleep-policy.d.ts +81 -0
  17. package/dist/src/core/sleep-policy.js +134 -0
  18. package/dist/src/core/sleep-policy.js.map +1 -0
  19. package/dist/src/core/window.d.ts +88 -0
  20. package/dist/src/core/window.js +185 -0
  21. package/dist/src/core/window.js.map +1 -0
  22. package/dist/src/index.d.ts +70 -0
  23. package/dist/src/index.js +882 -0
  24. package/dist/src/index.js.map +1 -0
  25. package/dist/src/platform/carrier-services.d.ts +96 -0
  26. package/dist/src/platform/carrier-services.js +982 -0
  27. package/dist/src/platform/carrier-services.js.map +1 -0
  28. package/dist/src/platform/guard.d.ts +60 -0
  29. package/dist/src/platform/guard.js +124 -0
  30. package/dist/src/platform/guard.js.map +1 -0
  31. package/dist/src/platform/mock-kernel.d.ts +95 -0
  32. package/dist/src/platform/mock-kernel.js +180 -0
  33. package/dist/src/platform/mock-kernel.js.map +1 -0
  34. package/dist/src/platform/qr.d.ts +28 -0
  35. package/dist/src/platform/qr.js +80 -0
  36. package/dist/src/platform/qr.js.map +1 -0
  37. package/dist/src/platform/sleep-posix.d.ts +17 -0
  38. package/dist/src/platform/sleep-posix.js +91 -0
  39. package/dist/src/platform/sleep-posix.js.map +1 -0
  40. package/dist/src/ports/index.d.ts +246 -0
  41. package/dist/src/ports/index.js +2 -0
  42. package/dist/src/ports/index.js.map +1 -0
  43. package/dist/src/shell/config.d.ts +56 -0
  44. package/dist/src/shell/config.js +212 -0
  45. package/dist/src/shell/config.js.map +1 -0
  46. package/dist/src/shell/pairing-text.d.ts +38 -0
  47. package/dist/src/shell/pairing-text.js +22 -0
  48. package/dist/src/shell/pairing-text.js.map +1 -0
  49. package/dist/src/shell/status.d.ts +39 -0
  50. package/dist/src/shell/status.js +119 -0
  51. package/dist/src/shell/status.js.map +1 -0
  52. package/dist/src/transport/relay.d.ts +107 -0
  53. package/dist/src/transport/relay.js +359 -0
  54. package/dist/src/transport/relay.js.map +1 -0
  55. package/dist/tests/carrier-events.test.d.ts +1 -0
  56. package/dist/tests/carrier-events.test.js +88 -0
  57. package/dist/tests/carrier-events.test.js.map +1 -0
  58. package/dist/tests/carrier-services.test.d.ts +1 -0
  59. package/dist/tests/carrier-services.test.js +562 -0
  60. package/dist/tests/carrier-services.test.js.map +1 -0
  61. package/dist/tests/carrier-vocabulary.test.d.ts +1 -0
  62. package/dist/tests/carrier-vocabulary.test.js +149 -0
  63. package/dist/tests/carrier-vocabulary.test.js.map +1 -0
  64. package/dist/tests/clock-probe-timers.test.d.ts +1 -0
  65. package/dist/tests/clock-probe-timers.test.js +73 -0
  66. package/dist/tests/clock-probe-timers.test.js.map +1 -0
  67. package/dist/tests/command-result.test.d.ts +1 -0
  68. package/dist/tests/command-result.test.js +185 -0
  69. package/dist/tests/command-result.test.js.map +1 -0
  70. package/dist/tests/config.test.d.ts +1 -0
  71. package/dist/tests/config.test.js +223 -0
  72. package/dist/tests/config.test.js.map +1 -0
  73. package/dist/tests/guard.test.d.ts +1 -0
  74. package/dist/tests/guard.test.js +175 -0
  75. package/dist/tests/guard.test.js.map +1 -0
  76. package/dist/tests/ids-and-keys.test.d.ts +1 -0
  77. package/dist/tests/ids-and-keys.test.js +211 -0
  78. package/dist/tests/ids-and-keys.test.js.map +1 -0
  79. package/dist/tests/pairing-text.test.d.ts +1 -0
  80. package/dist/tests/pairing-text.test.js +62 -0
  81. package/dist/tests/pairing-text.test.js.map +1 -0
  82. package/dist/tests/pairing-window.test.d.ts +1 -0
  83. package/dist/tests/pairing-window.test.js +165 -0
  84. package/dist/tests/pairing-window.test.js.map +1 -0
  85. package/dist/tests/relay-client.test.d.ts +1 -0
  86. package/dist/tests/relay-client.test.js +557 -0
  87. package/dist/tests/relay-client.test.js.map +1 -0
  88. package/dist/tests/runtime.test.d.ts +1 -0
  89. package/dist/tests/runtime.test.js +963 -0
  90. package/dist/tests/runtime.test.js.map +1 -0
  91. package/dist/tests/sleep.test.d.ts +1 -0
  92. package/dist/tests/sleep.test.js +264 -0
  93. package/dist/tests/sleep.test.js.map +1 -0
  94. package/dist/tests/status.test.d.ts +1 -0
  95. package/dist/tests/status.test.js +238 -0
  96. package/dist/tests/status.test.js.map +1 -0
  97. package/dist/tests/window.test.d.ts +1 -0
  98. package/dist/tests/window.test.js +210 -0
  99. package/dist/tests/window.test.js.map +1 -0
  100. package/package.json +70 -0
@@ -0,0 +1,81 @@
1
+ /**
2
+ * sleep-policy — 什么时候持锁、什么时候放锁(纯逻辑,执行交给 `SleepPort`)。
3
+ *
4
+ * 为什么把策略与执行拆开:旧实现把两者写在一个 `KeepAwake` 类里(在协议包里
5
+ * spawn 子进程),于是"空闲自动释放"和"挂起审批时续锁"这两条没有任何测试,
6
+ * 而其中一条正是旧实现的**持锁泄漏**:它靠"收到对应的终结事件"来解除
7
+ * 60 秒心跳续锁,但那三类事件里**没有一条带 requestId**,判据永不成立,
8
+ * 锁一旦因审批挂起就再也不会释放(取证 docs/legacy-spec/host-plugin-runtime.md §4
9
+ * 与 host-plugin-cordis.md §3 表第 9b 行)。
10
+ *
11
+ * 本实现的判据全部用**插件自己生成的 requestId**,并且每条都有单测:
12
+ * - 有活动(文本/工具/运行中)→ `markActive`;
13
+ * - 有挂起的人工交互 → `hold(id)`,每 `pendingRefreshMs` 续一次锁;
14
+ * - 终结只认 `release(id)`(手机答了、被平台撤销、或超时)——**不会**因为
15
+ * "看到一个没有 requestId 的事件"就以为它终结了;
16
+ * - 空闲超过 `idleReleaseMs` 且无挂起 → 放锁;`idleReleaseMs <= 0` 表示永不自动释放。
17
+ */
18
+ import type { Clock, SleepPort } from '../ports/index.js';
19
+ export interface KeepAwakeOptions {
20
+ /** 空闲多久后自动释放锁(毫秒)。0 或负数 = 不自动释放。 */
21
+ idleReleaseMs: number;
22
+ keepDisplay: boolean;
23
+ /** 锁断言绑定的宿主 pid:宿主崩了锁随之消失,不会留下永不睡机的机器。 */
24
+ ownerPid: number;
25
+ /** 有人工交互挂起时,多久续一次锁。 */
26
+ pendingRefreshMs: number;
27
+ /** 检查节拍。 */
28
+ tickMs: number;
29
+ }
30
+ export interface KeepAwakeSnapshot {
31
+ enabled: boolean;
32
+ active: boolean;
33
+ platform: string;
34
+ backend: string;
35
+ reason?: string;
36
+ }
37
+ export declare class KeepAwake {
38
+ private readonly port;
39
+ private readonly clock;
40
+ private options;
41
+ private enabled;
42
+ private held;
43
+ private lastActiveAt;
44
+ private lastRefreshAt;
45
+ private readonly holds;
46
+ private timer;
47
+ private stopped;
48
+ private reason;
49
+ constructor(port: SleepPort, clock: Clock, options?: Partial<KeepAwakeOptions>);
50
+ /**
51
+ * 开关。关掉时立刻放锁——用户明确要求过"本地空闲时机器照常睡眠"。
52
+ * `idleReleaseSec`(协议字段,单位秒)在这里换算;`0` 传进来表示不自动释放。
53
+ */
54
+ setEnabled(enabled: boolean, idleReleaseSec?: number): KeepAwakeSnapshot;
55
+ /** 任何内核活动都算一次"还活着"。 */
56
+ markActive(): void;
57
+ /** 一次人工交互挂起(审批或提问)。id 必须是插件自己生成的 requestId。 */
58
+ hold(id: string): void;
59
+ /** 一次人工交互结束了(手机答了 / 平台撤销 / 超时)。 */
60
+ releaseHold(id: string): void;
61
+ /**
62
+ * 判定此刻该不该持锁,并启动/维持节拍定时器。
63
+ * 拆成单独方法是为了能被单测直接驱动(注入假时钟,不真的等 300 秒)。
64
+ */
65
+ evaluate(): KeepAwakeSnapshot;
66
+ tick(): void;
67
+ snapshot(): KeepAwakeSnapshot;
68
+ /**
69
+ * 停机:放锁并撤掉节拍。
70
+ *
71
+ * `stopped` 这个闸门不是洁癖。审批的 `releaseHold()` 可能在 `stop()` **之后**才到达
72
+ * (手机端迟迟没点、平台撤销、超时结算三条路都可能),而 `releaseHold → evaluate →
73
+ * ensureTimer` 会再装一个 `tickMs` 自循环定时器——没有这个标记时它永远清不掉:
74
+ * 插件已经拆了 socket,节拍还在续锁,表现是"进程永不退出"(e2e 里那 60 秒的尾巴)
75
+ * 和"用户关了远程控制,caffeinate 还在拦住机器睡眠"。
76
+ */
77
+ stop(): void;
78
+ private acquire;
79
+ private stopLock;
80
+ private ensureTimer;
81
+ }
@@ -0,0 +1,134 @@
1
+ const DEFAULTS = {
2
+ idleReleaseMs: 300_000,
3
+ keepDisplay: false,
4
+ ownerPid: process.pid,
5
+ pendingRefreshMs: 60_000,
6
+ tickMs: 10_000,
7
+ };
8
+ export class KeepAwake {
9
+ port;
10
+ clock;
11
+ options;
12
+ enabled = false;
13
+ held = false;
14
+ lastActiveAt = 0;
15
+ lastRefreshAt = 0;
16
+ holds = new Set();
17
+ timer;
18
+ stopped = false;
19
+ reason;
20
+ constructor(port, clock, options = {}) {
21
+ this.port = port;
22
+ this.clock = clock;
23
+ this.options = { ...DEFAULTS, ...options };
24
+ this.lastActiveAt = this.clock.now();
25
+ }
26
+ /**
27
+ * 开关。关掉时立刻放锁——用户明确要求过"本地空闲时机器照常睡眠"。
28
+ * `idleReleaseSec`(协议字段,单位秒)在这里换算;`0` 传进来表示不自动释放。
29
+ */
30
+ setEnabled(enabled, idleReleaseSec) {
31
+ this.enabled = enabled;
32
+ if (typeof idleReleaseSec === 'number') {
33
+ this.options = { ...this.options, idleReleaseMs: idleReleaseSec * 1000 };
34
+ }
35
+ if (!enabled)
36
+ this.stopLock();
37
+ else
38
+ this.markActive();
39
+ return this.snapshot();
40
+ }
41
+ /** 任何内核活动都算一次"还活着"。 */
42
+ markActive() {
43
+ this.lastActiveAt = this.clock.now();
44
+ this.evaluate();
45
+ }
46
+ /** 一次人工交互挂起(审批或提问)。id 必须是插件自己生成的 requestId。 */
47
+ hold(id) {
48
+ this.holds.add(id);
49
+ this.lastActiveAt = this.clock.now();
50
+ this.evaluate();
51
+ }
52
+ /** 一次人工交互结束了(手机答了 / 平台撤销 / 超时)。 */
53
+ releaseHold(id) {
54
+ this.holds.delete(id);
55
+ this.evaluate();
56
+ }
57
+ /**
58
+ * 判定此刻该不该持锁,并启动/维持节拍定时器。
59
+ * 拆成单独方法是为了能被单测直接驱动(注入假时钟,不真的等 300 秒)。
60
+ */
61
+ evaluate() {
62
+ // 停机之后一律惰性:不持锁、不装定时器,只回一份"已经关了"的快照。
63
+ if (this.stopped)
64
+ return this.snapshot();
65
+ const now = this.clock.now();
66
+ const idleExpired = this.options.idleReleaseMs > 0 && now - this.lastActiveAt > this.options.idleReleaseMs;
67
+ const shouldHold = this.enabled && (this.holds.size > 0 || !idleExpired);
68
+ if (shouldHold && !this.held)
69
+ this.acquire();
70
+ if (!shouldHold && this.held)
71
+ this.stopLock();
72
+ if (this.enabled && this.holds.size > 0 && now - this.lastRefreshAt >= this.options.pendingRefreshMs) {
73
+ // 挂起期间续锁:即使空闲计时已过期,也不能因为"手机还没点"就放锁。
74
+ this.lastRefreshAt = now;
75
+ this.lastActiveAt = now;
76
+ if (!this.held)
77
+ this.acquire();
78
+ }
79
+ this.ensureTimer();
80
+ return this.snapshot();
81
+ }
82
+ tick() {
83
+ this.evaluate();
84
+ }
85
+ snapshot() {
86
+ return {
87
+ enabled: this.enabled,
88
+ active: this.held,
89
+ platform: this.port.platform(),
90
+ backend: this.port.backend(),
91
+ ...(this.reason === undefined ? {} : { reason: this.reason }),
92
+ };
93
+ }
94
+ /**
95
+ * 停机:放锁并撤掉节拍。
96
+ *
97
+ * `stopped` 这个闸门不是洁癖。审批的 `releaseHold()` 可能在 `stop()` **之后**才到达
98
+ * (手机端迟迟没点、平台撤销、超时结算三条路都可能),而 `releaseHold → evaluate →
99
+ * ensureTimer` 会再装一个 `tickMs` 自循环定时器——没有这个标记时它永远清不掉:
100
+ * 插件已经拆了 socket,节拍还在续锁,表现是"进程永不退出"(e2e 里那 60 秒的尾巴)
101
+ * 和"用户关了远程控制,caffeinate 还在拦住机器睡眠"。
102
+ */
103
+ stop() {
104
+ this.stopped = true;
105
+ this.stopLock();
106
+ if (this.timer !== undefined) {
107
+ this.clock.clearTimeout(this.timer);
108
+ this.timer = undefined;
109
+ }
110
+ }
111
+ acquire() {
112
+ const started = this.port.start(this.options.ownerPid, this.options.keepDisplay);
113
+ this.held = started.ok;
114
+ this.reason = started.message;
115
+ this.lastRefreshAt = this.clock.now();
116
+ }
117
+ stopLock() {
118
+ if (!this.held)
119
+ return;
120
+ this.port.stop();
121
+ this.held = false;
122
+ }
123
+ ensureTimer() {
124
+ if (this.stopped || this.timer !== undefined)
125
+ return;
126
+ this.timer = this.clock.setTimeout(() => {
127
+ this.timer = undefined;
128
+ if (this.stopped)
129
+ return;
130
+ this.tick();
131
+ }, this.options.tickMs);
132
+ }
133
+ }
134
+ //# sourceMappingURL=sleep-policy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sleep-policy.js","sourceRoot":"","sources":["../../../src/core/sleep-policy.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,88 @@
1
+ /**
2
+ * window — 流式增量(delta)的合帧器。纯逻辑,不起定时器、不认识 cordis。
3
+ *
4
+ * 手机端的体验直接由这几个参数决定,所以它们必须是**可单测的参数**而不是散在
5
+ * socket 回调里的魔数。旧实现的窗口化写在 host-runtime 的事件循环里,
6
+ * 测试要靠假定时器凑环境,因此有几条行为从来没被真正锁住——
7
+ * 而其中一条(空文本的 done 帧被合并逻辑吞掉)会让手机**永远转圈**。
8
+ *
9
+ * 四条不变量:
10
+ *
11
+ * 1. **done 帧永不丢**,哪怕这一条消息一个字都没有(例如中断收尾)。
12
+ * 判据是"文本为空**且**没有 done"才丢,写成 `!text && !done`。
13
+ * 2. **窗口计时器只在首字到达时装填,后续字符不续期**。否则一条持续输出的长回答
14
+ * 会把 flush 一路推迟,手机端表现为"卡住不动"。
15
+ * 3. **同一会话的非 delta 事件之前必须先 flush**(由调用方走 `flushSession`),
16
+ * 不然工具行会插到它自己的文本前面。
17
+ * 4. **一轮最多处理 N 个会话**,超出就从最旧的开始冲刷。会话多时这保证
18
+ * 单个吵闹的会话不会饿死其它会话(也顺带给出内存上界)。
19
+ *
20
+ * 另有两条是重写时补的(第一版只做"触发时机",没做"大小"与"连续性"):
21
+ * `maxCharsPerFrame` 是**出站硬上限**——一帧最多这么多字符,超了就切多帧;
22
+ * `part` 在**同一条 messageId 内连续**,跨 flush、跨切帧都不归零,
23
+ * 否则手机上"分片是否完整"这件事没有任何可核对依据。
24
+ */
25
+ import type { EvMessageDelta } from 'dsh-remote-wire';
26
+ import type { Clock } from '../ports/index.js';
27
+ export interface WindowOptions {
28
+ /** 合帧窗口(毫秒)。首字到达后到 flush 之间的最长等待。 */
29
+ windowMs: number;
30
+ /** 单次 flush 的文本上限(字符)。超过就切帧,避免单帧顶到中继帧上限。 */
31
+ maxCharsPerFrame: number;
32
+ /** 一轮 flush 最多处理的会话数。 */
33
+ maxSessionsPerRound: number;
34
+ }
35
+ export declare const DEFAULT_WINDOW_OPTIONS: WindowOptions;
36
+ export declare class DeltaWindow {
37
+ private readonly emit;
38
+ private readonly clock;
39
+ private readonly options;
40
+ private readonly entries;
41
+ /**
42
+ * 每个会话"这条消息已经发到第几片"。必须活在 entry 之外:flush 会把 entry 删掉,
43
+ * 下一条同 messageId 的增量于是重建 entry、`part` 归零——一条长回答发出去全是
44
+ * `part:0`,手机与日志都失去了"分片是否连续"这个唯一的判据。
45
+ */
46
+ private readonly partCursor;
47
+ private flushed;
48
+ private droppedEmpty;
49
+ constructor(emit: (payload: EvMessageDelta) => void, clock: Clock, options?: WindowOptions);
50
+ /**
51
+ * 收到一段文本。同一 `messageId` 会继续累加;换了 messageId 就先把上一条冲掉,
52
+ * 保证"一条消息的所有 delta 先于下一条出现"。
53
+ */
54
+ push(sessionId: string, messageId: string, text: string, role?: 'assistant' | 'user' | 'system'): void;
55
+ /**
56
+ * 标记某条消息结束。**文本为空也必须发**——这是手机端唯一的停止转圈信号(F9)。
57
+ */
58
+ complete(sessionId: string, messageId: string, role?: 'assistant' | 'user' | 'system'): void;
59
+ /** 冲刷某个会话的缓冲(非 delta 事件之前、停机时、命令回执前都要调)。 */
60
+ flushSession(sessionId: string): void;
61
+ /**
62
+ * 一轮冲刷:先处理缓冲最久的会话,超过 `maxSessionsPerRound` 就停在这里
63
+ * (下一轮继续),避免吵闹的会话饿死其它会话。
64
+ */
65
+ flushRound(): void;
66
+ /** 停机/断线时全量冲刷。 */
67
+ flushAll(): void;
68
+ stop(): void;
69
+ get stats(): {
70
+ buffered: number;
71
+ flushed: number;
72
+ droppedEmpty: number;
73
+ };
74
+ /**
75
+ * 硬切成 `maxCharsPerFrame` 以内的片。
76
+ *
77
+ * 第一版只在 `entry.text.length >= cap` 时"触发一次 flush",而 flush 是整串发出去的:
78
+ * 一次 push 就写进 5 万字符时,出站的那一帧仍然是 5 万字符——上限只管了时机、没管大小,
79
+ * 于是手机可能顶到中继的帧上限(`MAX_FRAME_BYTES`)而被整条连接断开。
80
+ */
81
+ private splitToCap;
82
+ /** 这条消息的下一个分片序号:跨 flush 连续,换 messageId 就归零。 */
83
+ private nextPart;
84
+ /** 缓冲区里从来没有过这条消息的收尾帧(例如一条工具调用一个字都没出就直接结束)。 */
85
+ private flushedDoneOnly;
86
+ private emitFrame;
87
+ private clearTimer;
88
+ }
@@ -0,0 +1,185 @@
1
+ export const DEFAULT_WINDOW_OPTIONS = {
2
+ windowMs: 120,
3
+ maxCharsPerFrame: 4096,
4
+ maxSessionsPerRound: 32,
5
+ };
6
+ export class DeltaWindow {
7
+ emit;
8
+ clock;
9
+ options;
10
+ entries = new Map();
11
+ /**
12
+ * 每个会话"这条消息已经发到第几片"。必须活在 entry 之外:flush 会把 entry 删掉,
13
+ * 下一条同 messageId 的增量于是重建 entry、`part` 归零——一条长回答发出去全是
14
+ * `part:0`,手机与日志都失去了"分片是否连续"这个唯一的判据。
15
+ */
16
+ partCursor = new Map();
17
+ flushed = 0;
18
+ droppedEmpty = 0;
19
+ constructor(emit, clock, options = DEFAULT_WINDOW_OPTIONS) {
20
+ this.emit = emit;
21
+ this.clock = clock;
22
+ this.options = options;
23
+ }
24
+ /**
25
+ * 收到一段文本。同一 `messageId` 会继续累加;换了 messageId 就先把上一条冲掉,
26
+ * 保证"一条消息的所有 delta 先于下一条出现"。
27
+ */
28
+ push(sessionId, messageId, text, role) {
29
+ const current = this.entries.get(sessionId);
30
+ if (current && current.messageId !== messageId) {
31
+ // 上一条必须带 done 才算结束;没有 done 就补一条,否则手机端为上一条一直转圈。
32
+ if (!current.done)
33
+ this.complete(sessionId, current.messageId);
34
+ else
35
+ this.flushSession(sessionId);
36
+ }
37
+ let entry = this.entries.get(sessionId);
38
+ if (!entry) {
39
+ entry = {
40
+ sessionId,
41
+ messageId,
42
+ text: '',
43
+ role,
44
+ done: false,
45
+ part: this.nextPart(sessionId, messageId),
46
+ since: this.clock.now(),
47
+ timer: undefined,
48
+ };
49
+ this.entries.set(sessionId, entry);
50
+ // 只在首字装填定时器:后续字符不续期,长输出才不会把 flush 一路推后。
51
+ entry.timer = this.clock.setTimeout(() => this.flushSession(sessionId), this.options.windowMs);
52
+ }
53
+ entry.text += text;
54
+ if (role)
55
+ entry.role = role;
56
+ if (entry.text.length >= this.options.maxCharsPerFrame)
57
+ this.flushSession(sessionId);
58
+ }
59
+ /**
60
+ * 标记某条消息结束。**文本为空也必须发**——这是手机端唯一的停止转圈信号(F9)。
61
+ */
62
+ complete(sessionId, messageId, role) {
63
+ const entry = this.entries.get(sessionId);
64
+ if (!entry || entry.messageId !== messageId) {
65
+ // 缓冲里根本没有这条消息(例如一条工具调用没产生任何文本就直接结束):
66
+ // 仍然要补一条 done-only 帧。走 flushedDoneOnly() 而不是裸 emit——
67
+ // 直接 emit 会让 stats.flushed 与实际出站帧数分叉("发出去了但没人知道"是红线)。
68
+ this.flushedDoneOnly(sessionId, messageId, role);
69
+ return;
70
+ }
71
+ entry.done = true;
72
+ if (role)
73
+ entry.role = role;
74
+ this.flushSession(sessionId);
75
+ }
76
+ /** 冲刷某个会话的缓冲(非 delta 事件之前、停机时、命令回执前都要调)。 */
77
+ flushSession(sessionId) {
78
+ const entry = this.entries.get(sessionId);
79
+ if (!entry)
80
+ return;
81
+ this.clearTimer(entry);
82
+ this.entries.delete(sessionId);
83
+ if (!entry.text && !entry.done) {
84
+ // 唯一允许丢弃的情形:既没文本也不是收尾帧。计数暴露出去,防止"静默丢帧"无人知晓。
85
+ this.droppedEmpty += 1;
86
+ return;
87
+ }
88
+ const chunks = this.splitToCap(entry.text);
89
+ if (chunks.length === 0) {
90
+ // 收尾且一个字都没有:仍必须出站(F9),手机只认 done。
91
+ this.emitFrame(entry, '', true);
92
+ }
93
+ else {
94
+ chunks.forEach((chunk, index) => this.emitFrame(entry, chunk, index === chunks.length - 1 ? entry.done : false));
95
+ }
96
+ // 分片序号要跨 flush 接着数;这条消息已经带 done 收尾了就把游标清掉,
97
+ // 下一条消息从 0 开始才是它自己的序列。
98
+ if (entry.done)
99
+ this.partCursor.delete(sessionId);
100
+ else
101
+ this.partCursor.set(sessionId, { messageId: entry.messageId, next: entry.part });
102
+ entry.text = '';
103
+ }
104
+ /**
105
+ * 一轮冲刷:先处理缓冲最久的会话,超过 `maxSessionsPerRound` 就停在这里
106
+ * (下一轮继续),避免吵闹的会话饿死其它会话。
107
+ */
108
+ flushRound() {
109
+ const ordered = [...this.entries.values()].sort((a, b) => a.since - b.since);
110
+ for (const entry of ordered.slice(0, this.options.maxSessionsPerRound)) {
111
+ this.flushSession(entry.sessionId);
112
+ }
113
+ }
114
+ /** 停机/断线时全量冲刷。 */
115
+ flushAll() {
116
+ for (const sessionId of [...this.entries.keys()])
117
+ this.flushSession(sessionId);
118
+ }
119
+ stop() {
120
+ this.flushAll();
121
+ for (const entry of this.entries.values())
122
+ this.clearTimer(entry);
123
+ this.entries.clear();
124
+ // 游标也要清:会话没了还留着它的分片序号,等于给未来同 id 的消息一个凭空的起点。
125
+ this.partCursor.clear();
126
+ }
127
+ get stats() {
128
+ return { buffered: this.entries.size, flushed: this.flushed, droppedEmpty: this.droppedEmpty };
129
+ }
130
+ /**
131
+ * 硬切成 `maxCharsPerFrame` 以内的片。
132
+ *
133
+ * 第一版只在 `entry.text.length >= cap` 时"触发一次 flush",而 flush 是整串发出去的:
134
+ * 一次 push 就写进 5 万字符时,出站的那一帧仍然是 5 万字符——上限只管了时机、没管大小,
135
+ * 于是手机可能顶到中继的帧上限(`MAX_FRAME_BYTES`)而被整条连接断开。
136
+ */
137
+ splitToCap(text) {
138
+ const cap = this.options.maxCharsPerFrame;
139
+ if (!Number.isFinite(cap) || cap <= 0 || text.length <= cap)
140
+ return text.length === 0 ? [] : [text];
141
+ const parts = [];
142
+ for (let at = 0; at < text.length; at += cap)
143
+ parts.push(text.slice(at, at + cap));
144
+ return parts;
145
+ }
146
+ /** 这条消息的下一个分片序号:跨 flush 连续,换 messageId 就归零。 */
147
+ nextPart(sessionId, messageId) {
148
+ const cursor = this.partCursor.get(sessionId);
149
+ if (!cursor)
150
+ return 0;
151
+ if (cursor.messageId !== messageId) {
152
+ this.partCursor.delete(sessionId);
153
+ return 0;
154
+ }
155
+ return cursor.next;
156
+ }
157
+ /** 缓冲区里从来没有过这条消息的收尾帧(例如一条工具调用一个字都没出就直接结束)。 */
158
+ flushedDoneOnly(sessionId, messageId, role) {
159
+ this.flushed += 1;
160
+ // 收尾帧同样是这条消息的一片:序号要接得上(前面已经发过 2 片时它就是第 3 片),
161
+ // 从来没有过缓冲时才回到 0。发完就把游标清掉,下一条消息重新计数。
162
+ const part = this.nextPart(sessionId, messageId);
163
+ this.partCursor.delete(sessionId);
164
+ this.emit({ t: 'ev.message_delta', sessionId, messageId, delta: '', part, done: true, ...(role ? { role } : {}) });
165
+ }
166
+ emitFrame(entry, text, done) {
167
+ this.flushed += 1;
168
+ this.emit({
169
+ t: 'ev.message_delta',
170
+ sessionId: entry.sessionId,
171
+ messageId: entry.messageId,
172
+ delta: text,
173
+ part: entry.part++,
174
+ ...(entry.role ? { role: entry.role } : {}),
175
+ ...(done ? { done } : {}),
176
+ });
177
+ }
178
+ clearTimer(entry) {
179
+ if (entry.timer !== undefined) {
180
+ this.clock.clearTimeout(entry.timer);
181
+ entry.timer = undefined;
182
+ }
183
+ }
184
+ }
185
+ //# sourceMappingURL=window.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"window.js","sourceRoot":"","sources":["../../../src/core/window.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,70 @@
1
+ import { DEFAULT_CONFIG, type PluginConfig } from './shell/config.js';
2
+ import type { Clock } from './ports/index.js';
3
+ /** 我们只用到 ctx 的几个成员,所以不硬依赖 @deepseek-ai/cordis 的类型。 */
4
+ interface LooseContext {
5
+ get?<T>(name: string, optional: true): T | undefined;
6
+ /**
7
+ * `inject(names, cb)` 的回调参数**不是服务实例,而是已作用域化的上下文**(`c.get(name, true)`
8
+ * 才拿到服务),并且这条回调 fiber 挂在我们这条 fiber 下面——我们的 fiber 一旦失败,
9
+ * 它会连带被 dispose,回调就永远不会触发了(真机取证见 `apply()` 末尾那条纪律)。
10
+ */
11
+ inject?(names: string[], callback: (scoped: LooseContext) => void): unknown;
12
+ /** 事件注册口;作用域化上下文上才有,对 waterfall 是参与式监听(见 platform/guard.ts)。 */
13
+ on?(name: string, listener: (...args: unknown[]) => void): unknown;
14
+ off?(name: string, listener: (...args: unknown[]) => void): unknown;
15
+ provide?(name: string, value: unknown): unknown;
16
+ /**
17
+ * cordis 唯一的卸载钩子:`ctx.effect(() => disposer)`。
18
+ * **没有 `onDispose` 这个东西**——ctx 是 Proxy,读不存在的属性名是"抛错"而不是
19
+ * 返回 undefined,`?.` 挡不住(真机取证:`cannot get property "onDispose" without inject`
20
+ * 让 apply 整体失败 → fiber FAILED → 六个 inject 子 fiber 全被 dispose → carrier=none)。
21
+ */
22
+ effect?(execute: () => (() => unknown) | void): unknown;
23
+ commands?: {
24
+ register?(definition: {
25
+ name: string;
26
+ description: string;
27
+ input?: {
28
+ hint?: string;
29
+ };
30
+ handler?: (invocation: {
31
+ commandId: string;
32
+ rawInput: string;
33
+ agent?: unknown;
34
+ }) => Promise<unknown> | unknown;
35
+ }): unknown;
36
+ };
37
+ }
38
+ export interface RuntimeHandle {
39
+ readonly config: PluginConfig;
40
+ state(): Record<string, unknown>;
41
+ createPairing(): {
42
+ qr: string;
43
+ token: string;
44
+ psk: string;
45
+ expiresAt: number;
46
+ } | null;
47
+ /** 某条会话的工作区目录;解析不出来是 undefined(二维码 PNG 因此退回 `~/.dsh/`)。 */
48
+ sessionWorkspace(sessionId: string): string | undefined;
49
+ stop(): void;
50
+ }
51
+ /**
52
+ * 宿主命令的返回契约(`@deepseek-ai/dsh-commands` 的 `normalizeResult`)。
53
+ *
54
+ * 只有两种 kind;别的值会在注册边界上 `throw TypeError("...unknown result kind...")`,
55
+ * 整条命令失败。**把它写成类型**是为了让"写错 kind"在编译期就红,而不是等用户在
56
+ * 真机上看到一句宿主内部错误。
57
+ */
58
+ export type CommandResult = {
59
+ kind: 'success';
60
+ text?: string;
61
+ } | {
62
+ kind: 'error';
63
+ text: string;
64
+ };
65
+ /** 默认时钟:真实定时器。测试注入假时钟(见 core/clock.ts)。 */
66
+ export declare const systemClock: Clock;
67
+ export declare function apply(ctx: LooseContext, injected?: Partial<PluginConfig>): void;
68
+ export { DEFAULT_CONFIG };
69
+ export { readConfig, validateConfig } from './shell/config.js';
70
+ export type { KernelPort, Clock } from './ports/index.js';