dsh-deeppilot 0.9.2 → 0.9.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/docs/DSH_RELEASE_MEMORY.md +48 -7
- package/lib/index.d.ts +33 -3
- package/lib/index.js +170 -27
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -29,16 +29,32 @@
|
|
|
29
29
|
- **Protocol verdict:** no required DeepPilot phone-protocol break. Protocol v2,
|
|
30
30
|
pairing state, and device records are unchanged; already-paired phones need no
|
|
31
31
|
action.
|
|
32
|
-
- **Release verdict (`0.9.2`,
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
- **Release verdict (`0.9.2`, published 2026-10-05, tag `v0.9.2` @ `4b43671`):**
|
|
33
|
+
shipped to `latest`. Verified on the registry: version present, `dist-tags.latest`
|
|
34
|
+
moved off `0.9.1`, `gitHead` `4b436715` matches the tag and HEAD, 65 files,
|
|
35
|
+
and a clean DSH profile install of `dsh-deeppilot@0.9.2` composes with the
|
|
36
|
+
plugin's three `cordis.patch.yml` entries present and no missing-module error.
|
|
37
|
+
`scripts/smoke-live.mts` passed with **0 failures** against a live
|
|
38
|
+
**`dsh-v0.2.1-alpha.1`** web profile — the first live-host verification since
|
|
39
|
+
the 0.9.1 entry, retiring the gate it left open ("pending separate
|
|
40
|
+
authorization before the `0.9.1` release ships"), on a newer host than the
|
|
37
41
|
rc.2 that entry contemplated, which is strictly stronger evidence: the
|
|
38
42
|
`0.2.1-alpha.1` source-diff above found zero changed `src/` files across all
|
|
39
43
|
12 peer packages, and the live run agrees. The plugin's own build and type
|
|
40
|
-
baselines stay pinned to `0.2.0-rc.2`; nothing about the pin changed
|
|
41
|
-
release
|
|
44
|
+
baselines stay pinned to `0.2.0-rc.2`; nothing about the pin changed.
|
|
45
|
+
- **Install caveat users will hit — DSH's release-age policy.** On a clean
|
|
46
|
+
profile, `dsh plugin add dsh-deeppilot` installed **`0.9.1`**, not the just
|
|
47
|
+
published `0.9.2`, even though `latest` already pointed at `0.9.2`. DSH
|
|
48
|
+
deliberately prefers a slightly older release and offers an exact-version
|
|
49
|
+
path instead; `dsh plugin add dsh-deeppilot@0.9.2` installs the new one.
|
|
50
|
+
This is expected DSH behaviour, not a publishing failure — do not read the
|
|
51
|
+
older version in the installer as a broken release.
|
|
52
|
+
- **Peer-range note for future audits.** `evaluatePluginCompatibility` checks
|
|
53
|
+
each peer against the *running DSH version*, but `cordis` and `schemastery`
|
|
54
|
+
are host-vendored third parties and `react` comes from the Web app. Judging
|
|
55
|
+
those three against a DSH version number reports false mismatches. They are
|
|
56
|
+
also `optional` in `peerDependenciesMeta`. Confirm installability by
|
|
57
|
+
composing a real profile, not by iterating the peer table.
|
|
42
58
|
- **Two tracked follow-ups:**
|
|
43
59
|
1. **Schedule documentation is now stale.** Automation tasks became a
|
|
44
60
|
built-in Web capability and `@deepseek-ai/dsh-experimental-schedule-bundle`
|
|
@@ -201,6 +217,31 @@ in the same class as the other input-shaped Schedule refusals. Without the
|
|
|
201
217
|
mapping `wireErrorOf` falls through to `WIRE_ERROR_FALLBACK` (`E_INTERNAL`), which
|
|
202
218
|
would tell the client the failure was worth retrying.
|
|
203
219
|
|
|
220
|
+
### Cross-platform trap found while shipping 0.9.2
|
|
221
|
+
|
|
222
|
+
CI's `plugin` job hung on `npm test` four times while shipping 0.9.2. Worth
|
|
223
|
+
recording because macOS could never reproduce it and the fix is one line each.
|
|
224
|
+
|
|
225
|
+
`tests/journal-parity.test.ts` used `join('/proc/definitely/not/writable', …)`
|
|
226
|
+
as its "unwritable path" probe, in two places. `/proc` is a Linux-only pseudo
|
|
227
|
+
filesystem: on macOS the path simply does not exist, so the branch had only ever
|
|
228
|
+
been exercised on macOS and had never run on Linux at all. On Linux, writing
|
|
229
|
+
under `/proc` does not return an error — and because `dispatch-journal`'s
|
|
230
|
+
`save()` is entirely synchronous `fs` calls (`mkdirSync`/`openSync`/
|
|
231
|
+
`writeFileSync`/`fsyncSync`/`renameSync`), a single write blocks the whole event
|
|
232
|
+
loop. Even `setTimeout` stops firing, and node's `--test-timeout` can only report
|
|
233
|
+
a file-level timeout. Both sites now build the unwritable target inside the
|
|
234
|
+
scratch directory (a file-with-a-child shape that yields `ENOTDIR` identically on
|
|
235
|
+
every platform); recorded values are unchanged because all three candidate paths
|
|
236
|
+
returned the same status on macOS.
|
|
237
|
+
|
|
238
|
+
Two lessons for future audits: the workflow sets no `timeout-minutes`, so a hang
|
|
239
|
+
costs the full 6-hour default before failing — cancel and re-run rather than
|
|
240
|
+
waiting; and reproduce Linux-only behaviour in a container instead of inferring
|
|
241
|
+
it. `docker run --rm --cpus=2 -v $PWD:/app -w /app node:22` matches the runner
|
|
242
|
+
closely enough (2 cores, and the same `v22.23.x` that `node-version: 22` floats
|
|
243
|
+
to).
|
|
244
|
+
|
|
204
245
|
### Re-audit procedure used
|
|
205
246
|
|
|
206
247
|
Same blobless-clone method as the rc.2 and rc.1 entries, with two refinements:
|
package/lib/index.d.ts
CHANGED
|
@@ -59,7 +59,7 @@ type DispatchOutcome =
|
|
|
59
59
|
{
|
|
60
60
|
kind: 'mismatch';
|
|
61
61
|
} |
|
|
62
|
-
/**
|
|
62
|
+
/** 构造期解析失败:journal 已永久降级,本次进程不再投递。 */
|
|
63
63
|
{
|
|
64
64
|
kind: 'unavailable';
|
|
65
65
|
} |
|
|
@@ -81,7 +81,10 @@ type DispatchOutcome =
|
|
|
81
81
|
{
|
|
82
82
|
kind: 'crashed';
|
|
83
83
|
} |
|
|
84
|
-
/**
|
|
84
|
+
/**
|
|
85
|
+
* 预留条目时落盘失败——上游**没有**被调用,但无从判断 rename 是否已经
|
|
86
|
+
* 生效(save() 可能在 rename 之后才抛),因此条目留在内存里按 unknown 处理。
|
|
87
|
+
*/
|
|
85
88
|
{
|
|
86
89
|
kind: 'reserve-failed';
|
|
87
90
|
} |
|
|
@@ -125,12 +128,26 @@ interface DispatchJournalOptions<TRequest = unknown, TResult = unknown> {
|
|
|
125
128
|
* mutation 否(重复方立刻拿到 unknown → E_INTERNAL)。两者都由对等快照钉住。
|
|
126
129
|
*/
|
|
127
130
|
joinInFlight?: boolean;
|
|
131
|
+
/**
|
|
132
|
+
* 落盘失败的诊断输出。由调用方注入它已有的 logger;缺省即静默。
|
|
133
|
+
* 只打阶段与 errno,绝不打条目内容或消息体(plugin 的日志禁令)。
|
|
134
|
+
*/
|
|
135
|
+
log?: (message: string) => void;
|
|
136
|
+
/**
|
|
137
|
+
* 目录 fsync 里「对 fd 调用 fsync」这一步的覆盖点,缺省即 `fsyncSync`。
|
|
138
|
+
*
|
|
139
|
+
* 存在的唯一理由是可测:Linux CI 上复现不出 Windows 的 EPERM。注入点刻意
|
|
140
|
+
* 停在 fd 这一层而不是替换整个目录 fsync——容忍 EPERM 的判断必须留在真实
|
|
141
|
+
* 路径里被测到,换掉整函数就等于把待测逻辑一起换掉了。
|
|
142
|
+
*/
|
|
143
|
+
fsyncFileDescriptor?: (fd: number) => void;
|
|
128
144
|
}
|
|
129
145
|
/** Durable at-most-once dispatch. Unknown outcomes are never automatically retried. */
|
|
130
146
|
declare class DispatchJournal<TRequest = unknown, TResult = unknown> {
|
|
131
147
|
private readonly options;
|
|
132
148
|
private entries;
|
|
133
149
|
private inFlight;
|
|
150
|
+
/** 只由构造期的解析失败置位,见下方 catch;运行期落盘失败不碰它。 */
|
|
134
151
|
private healthy;
|
|
135
152
|
private readonly maxFileBytes;
|
|
136
153
|
private readonly capacity;
|
|
@@ -141,6 +158,15 @@ declare class DispatchJournal<TRequest = unknown, TResult = unknown> {
|
|
|
141
158
|
/** id 的纪元前缀是否还在保留窗口内(未来 5 分钟以上同样算非法)。 */
|
|
142
159
|
private expired;
|
|
143
160
|
private save;
|
|
161
|
+
/**
|
|
162
|
+
* 落盘一次,失败只报不降级。
|
|
163
|
+
*
|
|
164
|
+
* 与 `healthy` 的分工:`healthy` 只由构造期的解析失败置位,运行期的磁盘错误
|
|
165
|
+
* 不再拉黑整个进程——此前一次 save() 失败就让本进程余下的所有投递永久返回
|
|
166
|
+
* unknown,直到 DSH 重启(issue #20 / #26)。现在只影响当次投递,下一次
|
|
167
|
+
* dispatch 会重新尝试。
|
|
168
|
+
*/
|
|
169
|
+
private persist;
|
|
144
170
|
/** 查询既有回执;`request` 交给 codec 的 matches 判定(缺省即不提供 lookup)。 */
|
|
145
171
|
lookup(deviceId: string, id: string, request?: unknown): TResult | undefined;
|
|
146
172
|
/**
|
|
@@ -797,6 +823,8 @@ type ScheduleBridgeResult<T> = {
|
|
|
797
823
|
declare class HostBridge {
|
|
798
824
|
private readonly apiProxy;
|
|
799
825
|
private readonly historyBufferMax;
|
|
826
|
+
/** 三个 journal 的落盘失败诊断输出;缺省即静默。不打任何消息体。 */
|
|
827
|
+
private readonly log?;
|
|
800
828
|
readonly id: number;
|
|
801
829
|
private summaries;
|
|
802
830
|
private activeTools;
|
|
@@ -820,7 +848,9 @@ declare class HostBridge {
|
|
|
820
848
|
}, DeliveryReceipt>;
|
|
821
849
|
readonly scheduleMutations: DispatchJournal<unknown, MutationDispatchResult<unknown>>;
|
|
822
850
|
readonly forkMutations: DispatchJournal<unknown, MutationDispatchResult<unknown>>;
|
|
823
|
-
constructor(apiProxy: ApiProxyLike, historyBufferMax?: number, deliveryJournalPath?: string, scheduleJournalPath?: string, forkJournalPath?: string
|
|
851
|
+
constructor(apiProxy: ApiProxyLike, historyBufferMax?: number, deliveryJournalPath?: string, scheduleJournalPath?: string, forkJournalPath?: string,
|
|
852
|
+
/** 三个 journal 的落盘失败诊断输出;缺省即静默。不打任何消息体。 */
|
|
853
|
+
log?: ((message: string) => void) | undefined);
|
|
824
854
|
private pushOutlet;
|
|
825
855
|
private widgetFingerprint;
|
|
826
856
|
/**
|
package/lib/index.js
CHANGED
|
@@ -47,6 +47,21 @@ const ERROR_CODES = {
|
|
|
47
47
|
const WIRE_ERROR_FALLBACK = "E_INTERNAL";
|
|
48
48
|
const DOMAIN_TABLES = {
|
|
49
49
|
session: {
|
|
50
|
+
"session/not-found": "E_NOT_FOUND",
|
|
51
|
+
"workspace/not-found": "E_NOT_FOUND",
|
|
52
|
+
"session/agent-busy": "E_BUSY",
|
|
53
|
+
"session/writer-held": "E_BUSY",
|
|
54
|
+
"session/conflict": "E_BUSY",
|
|
55
|
+
"workspace/session-active": "E_BUSY",
|
|
56
|
+
"gateway/bad-request": "E_PROTOCOL",
|
|
57
|
+
"session/title-invalid": "E_PROTOCOL",
|
|
58
|
+
"session/attachment-invalid": "E_PROTOCOL",
|
|
59
|
+
"session/invalid-time-zone": "E_PROTOCOL",
|
|
60
|
+
"workspace/invalid-path": "E_PROTOCOL",
|
|
61
|
+
"workspace/name-conflict": "E_PROTOCOL",
|
|
62
|
+
"workspace/move-invalid": "E_PROTOCOL",
|
|
63
|
+
"workspace-file/not-found": "E_NOT_FOUND",
|
|
64
|
+
"directory-picker/unavailable": "E_UNSUPPORTED",
|
|
50
65
|
"session-not-found": "E_NOT_FOUND",
|
|
51
66
|
"workspace-not-found": "E_NOT_FOUND",
|
|
52
67
|
"agent-busy": "E_BUSY",
|
|
@@ -60,6 +75,11 @@ const DOMAIN_TABLES = {
|
|
|
60
75
|
"directory-picker-unavailable": "E_UNSUPPORTED"
|
|
61
76
|
},
|
|
62
77
|
model: {
|
|
78
|
+
"session/not-found": "E_NOT_FOUND",
|
|
79
|
+
"session/agent-busy": "E_BUSY",
|
|
80
|
+
"session/writer-held": "E_BUSY",
|
|
81
|
+
"session/conflict": "E_BUSY",
|
|
82
|
+
"session/model-unavailable": "E_NOT_FOUND",
|
|
63
83
|
"session-not-found": "E_NOT_FOUND",
|
|
64
84
|
"agent-busy": "E_BUSY",
|
|
65
85
|
"session-conflict": "E_BUSY",
|
|
@@ -80,18 +100,55 @@ const DOMAIN_TABLES = {
|
|
|
80
100
|
"subagent_session": "E_PROTOCOL"
|
|
81
101
|
}
|
|
82
102
|
};
|
|
103
|
+
/**
|
|
104
|
+
* 剥掉 `namespace/` 前缀:`session/agent-busy` -> `agent-busy`。
|
|
105
|
+
*
|
|
106
|
+
* DSH 的 API 层现在抛的是带命名空间的码(`session/agent-busy`、
|
|
107
|
+
* `workspace/not-found`、`gateway/internal`……),而下面的表一开始只有扁平写法。
|
|
108
|
+
* 两边对不上时整表失配、一律落进 `E_INTERNAL`,于是 `E_BUSY` 永远出不来——
|
|
109
|
+
* 而 `E_BUSY` 正是客户端决定「该不该重试」的那个码(issue #26)。
|
|
110
|
+
*/
|
|
111
|
+
function canonicalHostCode(code) {
|
|
112
|
+
const slash = code.lastIndexOf("/");
|
|
113
|
+
return slash === -1 ? code : code.slice(slash + 1);
|
|
114
|
+
}
|
|
115
|
+
/** 查表顺序:原样命中优先,其次剥掉命名空间后命中,最后才是兜底。 */
|
|
116
|
+
function lookupCode(domain, code) {
|
|
117
|
+
const table = DOMAIN_TABLES[domain];
|
|
118
|
+
return table[code] ?? table[canonicalHostCode(code)] ?? "E_INTERNAL";
|
|
119
|
+
}
|
|
83
120
|
/** 一个 Host 失败翻译成 Bridge 的失败结果:直接携带 wire 错误码。 */
|
|
84
121
|
function wireErrorOf(domain, error) {
|
|
122
|
+
const code = lookupCode(domain, error.code);
|
|
85
123
|
return {
|
|
86
124
|
ok: false,
|
|
87
|
-
code
|
|
88
|
-
message: error
|
|
125
|
+
code,
|
|
126
|
+
message: describeFailure(error, code)
|
|
89
127
|
};
|
|
90
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* 组装给手机看的那句话。
|
|
131
|
+
*
|
|
132
|
+
* DSH 把 prompt 准入的**任意**内部失败包成 `session/agent-busy`,真实原因只留在
|
|
133
|
+
* `details.reason`(`@deepseek-ai/dsh-api-session-controller` 的 `types/commands.js`
|
|
134
|
+
* 里那一句 `throw new RemoteError('session/agent-busy', 'prompt rejected', { reason: … })`)。
|
|
135
|
+
* 插件原先只取 `message`,于是手机端永远只显示笼统的「发送未确认」。
|
|
136
|
+
*
|
|
137
|
+
* 只在落到 `E_INTERNAL` 时追加 reason:这类码按定义对客户端没有可执行含义,
|
|
138
|
+
* 多这一句不改变「该不该重试」的判断,却能让用户和排查者看到真正的原因。已映射
|
|
139
|
+
* 的码本身已经可执行,保持原样不动。
|
|
140
|
+
*/
|
|
141
|
+
function describeFailure(error, wireCode) {
|
|
142
|
+
const base = error.message ?? error.code;
|
|
143
|
+
if (wireCode !== "E_INTERNAL") return base;
|
|
144
|
+
const reason = error.details?.reason;
|
|
145
|
+
if (typeof reason !== "string" || reason.length === 0) return base;
|
|
146
|
+
return `${base} (${error.code}): ${reason}`;
|
|
147
|
+
}
|
|
91
148
|
/** 只有一个 Host code(没有附带 message)时取它的 wire 码。 */
|
|
92
149
|
function wireCodeFor(domain, hostCode) {
|
|
93
150
|
if (hostCode === void 0) return WIRE_ERROR_FALLBACK;
|
|
94
|
-
return
|
|
151
|
+
return lookupCode(domain, hostCode);
|
|
95
152
|
}
|
|
96
153
|
function pendingResponseErrorCode(reason) {
|
|
97
154
|
switch (reason) {
|
|
@@ -4720,6 +4777,11 @@ function createRemoteTransport(deps) {
|
|
|
4720
4777
|
* 核心拥有:key 派生、原子写盘(temp + fsync + rename + 目录 fsync)、加载与
|
|
4721
4778
|
* 逐字段校验、保留清扫、容量、healthy 降级、在途去重、opener 单例。
|
|
4722
4779
|
*
|
|
4780
|
+
* healthy 的唯一来源是**构造期的解析失败**(文件损坏,续写会写坏格式);
|
|
4781
|
+
* 运行期的落盘失败只影响当次投递,下次投递重新尝试落盘。两者曾经共用同一个
|
|
4782
|
+
* 标志位,一次瞬时磁盘错误就让整个进程的所有投递永久短路到 DSH 重启
|
|
4783
|
+
* (issue #20 / #26)。
|
|
4784
|
+
*
|
|
4723
4785
|
* codec 拥有:一次投递的全部「方言」。它同时是两个 adapter——prompt 回执与
|
|
4724
4786
|
* mutation 结果——因此这条缝是真的,不是假想的。
|
|
4725
4787
|
*/
|
|
@@ -4727,11 +4789,51 @@ const DEFAULT_CAPACITY = 1e4;
|
|
|
4727
4789
|
const DEFAULT_RETENTION_MS = 6048e5;
|
|
4728
4790
|
/** 比保留窗口多宽限 5 分钟:边界附近的条目不会被反复删了又写。 */
|
|
4729
4791
|
const SWEEP_GRACE_MS = 3e5;
|
|
4792
|
+
/**
|
|
4793
|
+
* 目录 fsync 的三个「平台不支持」码。
|
|
4794
|
+
*
|
|
4795
|
+
* Windows 上 `openSync(dir, 'r')` 成功但 `fsyncSync(dirfd)` 必定抛错(社区实测
|
|
4796
|
+
* 报 EPERM,issue #20 / #26);EISDIR / EINVAL 是同一现象的其他平台变体。
|
|
4797
|
+
* 这三个码在本调用点都不代表数据损坏:文件内容已在 rename 之前 fsync 过,且
|
|
4798
|
+
* rename 在同一目录内是原子的,跳过的只是目录项的额外耐久性保证。
|
|
4799
|
+
*/
|
|
4800
|
+
const TOLERATED_DIR_FSYNC_CODES = /* @__PURE__ */ new Set([
|
|
4801
|
+
"EPERM",
|
|
4802
|
+
"EISDIR",
|
|
4803
|
+
"EINVAL"
|
|
4804
|
+
]);
|
|
4805
|
+
/** errno 优先,取不到就退回错误本身的字符串(诊断用,不参与控制流)。 */
|
|
4806
|
+
function errnoOf(error) {
|
|
4807
|
+
const code = error?.code;
|
|
4808
|
+
return typeof code === "string" ? code : String(error);
|
|
4809
|
+
}
|
|
4810
|
+
/** 这类错误是否属于「该平台不支持目录 fsync」而非真的写坏了。 */
|
|
4811
|
+
function isToleratedDirectoryFsyncError(error) {
|
|
4812
|
+
return TOLERATED_DIR_FSYNC_CODES.has(errnoOf(error));
|
|
4813
|
+
}
|
|
4814
|
+
/**
|
|
4815
|
+
* 目录 fsync:把 rename 造成的目录项变更推给磁盘。
|
|
4816
|
+
*
|
|
4817
|
+
* 按 errno 而非 `process.platform` 判断:WSL / Wine 之类环境下平台名并不可靠,
|
|
4818
|
+
* 而 errno 才是真正决定「这个平台支不支持」的信号(issue #26 给的两种建议里,
|
|
4819
|
+
* 按 errno 的那种才是对的)。其余 errno(EIO、ENOSPC……)照旧抛出。
|
|
4820
|
+
*/
|
|
4821
|
+
function fsyncDirectory(dirPath, fsync = fsyncSync) {
|
|
4822
|
+
const fd = openSync(dirPath, "r");
|
|
4823
|
+
try {
|
|
4824
|
+
fsync(fd);
|
|
4825
|
+
} catch (error) {
|
|
4826
|
+
if (!isToleratedDirectoryFsyncError(error)) throw error;
|
|
4827
|
+
} finally {
|
|
4828
|
+
closeSync(fd);
|
|
4829
|
+
}
|
|
4830
|
+
}
|
|
4730
4831
|
/** Durable at-most-once dispatch. Unknown outcomes are never automatically retried. */
|
|
4731
4832
|
var DispatchJournal = class {
|
|
4732
4833
|
options;
|
|
4733
4834
|
entries = Object.create(null);
|
|
4734
4835
|
inFlight = /* @__PURE__ */ new Map();
|
|
4836
|
+
/** 只由构造期的解析失败置位,见下方 catch;运行期落盘失败不碰它。 */
|
|
4735
4837
|
healthy = true;
|
|
4736
4838
|
maxFileBytes;
|
|
4737
4839
|
capacity;
|
|
@@ -4782,11 +4884,23 @@ var DispatchJournal = class {
|
|
|
4782
4884
|
closeSync(fd);
|
|
4783
4885
|
}
|
|
4784
4886
|
renameSync(temp, path);
|
|
4785
|
-
|
|
4887
|
+
fsyncDirectory(dirname(path), this.options.fsyncFileDescriptor);
|
|
4888
|
+
}
|
|
4889
|
+
/**
|
|
4890
|
+
* 落盘一次,失败只报不降级。
|
|
4891
|
+
*
|
|
4892
|
+
* 与 `healthy` 的分工:`healthy` 只由构造期的解析失败置位,运行期的磁盘错误
|
|
4893
|
+
* 不再拉黑整个进程——此前一次 save() 失败就让本进程余下的所有投递永久返回
|
|
4894
|
+
* unknown,直到 DSH 重启(issue #20 / #26)。现在只影响当次投递,下一次
|
|
4895
|
+
* dispatch 会重新尝试。
|
|
4896
|
+
*/
|
|
4897
|
+
persist() {
|
|
4786
4898
|
try {
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4899
|
+
this.save();
|
|
4900
|
+
return true;
|
|
4901
|
+
} catch (error) {
|
|
4902
|
+
this.options.log?.("journal 落盘失败,本次投递按未确认处理,下次投递会重新尝试:" + errnoOf(error));
|
|
4903
|
+
return false;
|
|
4790
4904
|
}
|
|
4791
4905
|
}
|
|
4792
4906
|
/** 查询既有回执;`request` 交给 codec 的 matches 判定(缺省即不提供 lookup)。 */
|
|
@@ -4831,12 +4945,7 @@ var DispatchJournal = class {
|
|
|
4831
4945
|
...codec.reserve(request, id)
|
|
4832
4946
|
};
|
|
4833
4947
|
this.entries[key] = entry;
|
|
4834
|
-
|
|
4835
|
-
this.save();
|
|
4836
|
-
} catch {
|
|
4837
|
-
this.healthy = false;
|
|
4838
|
-
return codec.resultOf({ kind: "reserve-failed" }, id);
|
|
4839
|
-
}
|
|
4948
|
+
if (!this.persist()) return codec.resultOf({ kind: "reserve-failed" }, id);
|
|
4840
4949
|
const run = (async () => {
|
|
4841
4950
|
let ran;
|
|
4842
4951
|
try {
|
|
@@ -4846,11 +4955,7 @@ var DispatchJournal = class {
|
|
|
4846
4955
|
} catch {
|
|
4847
4956
|
return codec.resultOf({ kind: "crashed" }, id);
|
|
4848
4957
|
}
|
|
4849
|
-
|
|
4850
|
-
this.save();
|
|
4851
|
-
} catch {
|
|
4852
|
-
this.healthy = false;
|
|
4853
|
-
}
|
|
4958
|
+
this.persist();
|
|
4854
4959
|
return codec.resultOf({
|
|
4855
4960
|
kind: "ran",
|
|
4856
4961
|
entry,
|
|
@@ -5163,6 +5268,7 @@ let BRIDGE_SEQ = 0;
|
|
|
5163
5268
|
var HostBridge = class {
|
|
5164
5269
|
apiProxy;
|
|
5165
5270
|
historyBufferMax;
|
|
5271
|
+
log;
|
|
5166
5272
|
id = ++BRIDGE_SEQ;
|
|
5167
5273
|
summaries = /* @__PURE__ */ new Map();
|
|
5168
5274
|
activeTools = /* @__PURE__ */ new Map();
|
|
@@ -5183,26 +5289,30 @@ var HostBridge = class {
|
|
|
5183
5289
|
promptDeliveries;
|
|
5184
5290
|
scheduleMutations;
|
|
5185
5291
|
forkMutations;
|
|
5186
|
-
constructor(apiProxy, historyBufferMax = MAX_RING_DEFAULT, deliveryJournalPath, scheduleJournalPath, forkJournalPath) {
|
|
5292
|
+
constructor(apiProxy, historyBufferMax = MAX_RING_DEFAULT, deliveryJournalPath, scheduleJournalPath, forkJournalPath, log) {
|
|
5187
5293
|
this.apiProxy = apiProxy;
|
|
5188
5294
|
this.historyBufferMax = historyBufferMax;
|
|
5295
|
+
this.log = log;
|
|
5189
5296
|
this.promptDeliveries = openDispatchJournal({
|
|
5190
5297
|
path: deliveryJournalPath,
|
|
5191
5298
|
codec: promptDeliveryCodec,
|
|
5192
5299
|
joinInFlight: true,
|
|
5193
|
-
maxFileBytes: 8388608
|
|
5300
|
+
maxFileBytes: 8388608,
|
|
5301
|
+
log: this.log
|
|
5194
5302
|
});
|
|
5195
5303
|
this.scheduleMutations = openDispatchJournal({
|
|
5196
5304
|
path: scheduleJournalPath,
|
|
5197
5305
|
codec: scheduleMutationCodec(false),
|
|
5198
5306
|
joinInFlight: false,
|
|
5199
|
-
maxFileBytes: 4194304
|
|
5307
|
+
maxFileBytes: 4194304,
|
|
5308
|
+
log: this.log
|
|
5200
5309
|
});
|
|
5201
5310
|
this.forkMutations = openDispatchJournal({
|
|
5202
5311
|
path: forkJournalPath,
|
|
5203
5312
|
codec: scheduleMutationCodec(true),
|
|
5204
5313
|
joinInFlight: false,
|
|
5205
|
-
maxFileBytes: 4194304
|
|
5314
|
+
maxFileBytes: 4194304,
|
|
5315
|
+
log: this.log
|
|
5206
5316
|
});
|
|
5207
5317
|
}
|
|
5208
5318
|
pushOutlet;
|
|
@@ -7841,16 +7951,49 @@ function applyReportRemote(ctx, snapshot, pairingStarter, deviceRevoker, deviceR
|
|
|
7841
7951
|
}
|
|
7842
7952
|
//#endregion
|
|
7843
7953
|
//#region src/local-address.ts
|
|
7844
|
-
function
|
|
7954
|
+
function octetsOf(address) {
|
|
7845
7955
|
const octets = address.split(".").map(Number);
|
|
7846
|
-
if (octets.length !== 4 || octets.some((value) => !Number.isInteger(value) || value < 0 || value > 255)) return
|
|
7847
|
-
|
|
7956
|
+
if (octets.length !== 4 || octets.some((value) => !Number.isInteger(value) || value < 0 || value > 255)) return null;
|
|
7957
|
+
return octets;
|
|
7958
|
+
}
|
|
7959
|
+
/** RFC1918 私有地址。名字保持诚实:它**不**包含 CGNAT 段,见 isSharedAddressSpaceIPv4。 */
|
|
7960
|
+
function isPrivateIPv4(address) {
|
|
7961
|
+
const parsed = octetsOf(address);
|
|
7962
|
+
if (parsed === null) return false;
|
|
7963
|
+
const [a, b] = parsed;
|
|
7848
7964
|
return a === 10 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168;
|
|
7849
7965
|
}
|
|
7966
|
+
/**
|
|
7967
|
+
* RFC 6598 共享地址空间(100.64.0.0/10)。
|
|
7968
|
+
*
|
|
7969
|
+
* Tailscale 与 Headscale 都从这个段里给节点分配地址(Headscale 的
|
|
7970
|
+
* `prefixes.v4` 默认就是它),因此它在实践中是私有的,只是不是 RFC1918——
|
|
7971
|
+
* 这正是 `isPrivateIPv4` 会把它拒掉的原因。把它单列成一个谓词而不是塞进
|
|
7972
|
+
* `isPrivateIPv4`,是为了不谎报那个名字(issue #24)。
|
|
7973
|
+
*/
|
|
7974
|
+
function isSharedAddressSpaceIPv4(address) {
|
|
7975
|
+
const parsed = octetsOf(address);
|
|
7976
|
+
if (parsed === null) return false;
|
|
7977
|
+
const [a, b] = parsed;
|
|
7978
|
+
return a === 100 && b >= 64 && b <= 127;
|
|
7979
|
+
}
|
|
7980
|
+
/**
|
|
7981
|
+
* 可以交给手机的私有地址候选:RFC1918 加 CGNAT 段。
|
|
7982
|
+
*
|
|
7983
|
+
* 候选多于一个时,配对二维码页才会渲染地址选择器——只有一个候选时用户没有
|
|
7984
|
+
* 任何办法指定别的地址。把 tailnet 地址放进来,选择器才会出现,物理局域网
|
|
7985
|
+
* 离开之后用户仍可切到 tailnet 那个。
|
|
7986
|
+
*
|
|
7987
|
+
* 排序**不动**:物理 en* 接口仍然排在隧道接口之前,默认拿到的还是局域网地址,
|
|
7988
|
+
* 改变的只是「多了一个可选项」这件事本身。
|
|
7989
|
+
*/
|
|
7990
|
+
function isRoutablePrivateIPv4(address) {
|
|
7991
|
+
return isPrivateIPv4(address) || isSharedAddressSpaceIPv4(address);
|
|
7992
|
+
}
|
|
7850
7993
|
/** Private IPv4 candidates, preferring physical en* interfaces over tunnels. */
|
|
7851
7994
|
function localLANIPv4Addresses() {
|
|
7852
7995
|
const candidates = [];
|
|
7853
|
-
for (const [name, entries] of Object.entries(networkInterfaces())) for (const entry of entries ?? []) if (entry.family === "IPv4" && !entry.internal &&
|
|
7996
|
+
for (const [name, entries] of Object.entries(networkInterfaces())) for (const entry of entries ?? []) if (entry.family === "IPv4" && !entry.internal && isRoutablePrivateIPv4(entry.address)) candidates.push({
|
|
7854
7997
|
name,
|
|
7855
7998
|
address: entry.address
|
|
7856
7999
|
});
|
|
@@ -8785,7 +8928,7 @@ function apply(ctx, options) {
|
|
|
8785
8928
|
log("DSH session bridge unavailable: " + String(error));
|
|
8786
8929
|
return;
|
|
8787
8930
|
}
|
|
8788
|
-
const bridge = new HostBridge(proxy, cfg.historyBufferMax, join(dataDir, "prompt-deliveries-v1.json"), join(dataDir, "schedule-mutations-v1.json"), join(dataDir, "fork-mutations-v1.json"));
|
|
8931
|
+
const bridge = new HostBridge(proxy, cfg.historyBufferMax, join(dataDir, "prompt-deliveries-v1.json"), join(dataDir, "schedule-mutations-v1.json"), join(dataDir, "fork-mutations-v1.json"), log);
|
|
8789
8932
|
bridge.setPushOutlet(pushGateway);
|
|
8790
8933
|
state.bridge = bridge;
|
|
8791
8934
|
bridge.start();
|