@bolloon/bolloon-agent 0.5.0 → 0.5.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.
Files changed (121) hide show
  1. package/README.md +39 -0
  2. package/dist/agents/agent-lang.js +101 -0
  3. package/dist/agents/agent-manifest-protocol.js +4 -0
  4. package/dist/agents/execution-supervisor.js +87 -5
  5. package/dist/agents/gateway-group.js +44 -3
  6. package/dist/agents/model-policy.js +651 -0
  7. package/dist/agents/pi-sdk-tools.js +84 -33
  8. package/dist/agents/pi-sdk.js +184 -31
  9. package/dist/agents/run-store.js +53 -0
  10. package/dist/agents/task/task-runner.js +8 -1
  11. package/dist/agents/workflow-pivot-loop.js +22 -13
  12. package/dist/cli/clipboard.js +65 -0
  13. package/dist/cli/ink-app.js +145 -17
  14. package/dist/cli/log-gate.js +386 -0
  15. package/dist/cli/mention-data.js +2 -0
  16. package/dist/cli/model-selector.js +538 -0
  17. package/dist/cli/reply-hygiene.js +54 -0
  18. package/dist/cli/setup-wizard.js +686 -69
  19. package/dist/cli/startup-notice.js +160 -0
  20. package/dist/cli/stores.js +22 -1
  21. package/dist/cli/theme.js +49 -0
  22. package/dist/cli/tui-select.js +970 -0
  23. package/dist/cli-entry.js +125 -18
  24. package/dist/efficode/compress.js +78 -0
  25. package/dist/efficode/crc32.js +38 -0
  26. package/dist/efficode/did.js +66 -0
  27. package/dist/efficode/index.js +24 -0
  28. package/dist/efficode/lz77.js +170 -0
  29. package/dist/efficode/negotiate.js +187 -0
  30. package/dist/efficode/ops.js +87 -0
  31. package/dist/efficode/packet.js +271 -0
  32. package/dist/efficode/types.js +45 -0
  33. package/dist/electron-build/agents/agent-manifest-protocol.js +4 -0
  34. package/dist/electron-build/agents/agent-manifest-protocol.js.map +1 -1
  35. package/dist/electron-build/agents/execution-supervisor.js +86 -4
  36. package/dist/electron-build/agents/execution-supervisor.js.map +1 -1
  37. package/dist/electron-build/agents/goal-store.js.map +1 -1
  38. package/dist/electron-build/agents/model-policy.js +706 -0
  39. package/dist/electron-build/agents/model-policy.js.map +1 -0
  40. package/dist/electron-build/agents/pi-sdk-tools.js +84 -33
  41. package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
  42. package/dist/electron-build/agents/pi-sdk-types.js.map +1 -1
  43. package/dist/electron-build/agents/pi-sdk.js +185 -31
  44. package/dist/electron-build/agents/pi-sdk.js.map +1 -1
  45. package/dist/electron-build/agents/run-store.js +55 -1
  46. package/dist/electron-build/agents/run-store.js.map +1 -1
  47. package/dist/electron-build/agents/task/task-runner.js +8 -1
  48. package/dist/electron-build/agents/task/task-runner.js.map +1 -1
  49. package/dist/electron-build/agents/workflow-pivot-loop.js +22 -13
  50. package/dist/electron-build/agents/workflow-pivot-loop.js.map +1 -1
  51. package/dist/electron-build/llm/config-store.js +146 -6
  52. package/dist/electron-build/llm/config-store.js.map +1 -1
  53. package/dist/electron-build/llm/connection-probe.js +764 -0
  54. package/dist/electron-build/llm/connection-probe.js.map +1 -0
  55. package/dist/electron-build/llm/custom-provider-store.js +272 -0
  56. package/dist/electron-build/llm/custom-provider-store.js.map +1 -0
  57. package/dist/electron-build/llm/data/provider-catalog-baked.js +23 -0
  58. package/dist/electron-build/llm/data/provider-catalog-baked.js.map +1 -0
  59. package/dist/electron-build/llm/model-catalog.js +778 -0
  60. package/dist/electron-build/llm/model-catalog.js.map +1 -0
  61. package/dist/electron-build/llm/model-discovery.js +1202 -0
  62. package/dist/electron-build/llm/model-discovery.js.map +1 -0
  63. package/dist/electron-build/llm/model-selection.js +1315 -0
  64. package/dist/electron-build/llm/model-selection.js.map +1 -0
  65. package/dist/electron-build/llm/pi-ai.js +751 -55
  66. package/dist/electron-build/llm/pi-ai.js.map +1 -1
  67. package/dist/electron-build/llm/provider-catalog.js +969 -0
  68. package/dist/electron-build/llm/provider-catalog.js.map +1 -0
  69. package/dist/electron-build/llm/provider-registry.js +950 -0
  70. package/dist/electron-build/llm/provider-registry.js.map +1 -0
  71. package/dist/electron-build/llm/tool-name.js +266 -0
  72. package/dist/electron-build/llm/tool-name.js.map +1 -0
  73. package/dist/electron-build/utils/dual-source-facts.js +236 -0
  74. package/dist/electron-build/utils/dual-source-facts.js.map +1 -0
  75. package/dist/electron-build/utils/dual-source.js +32 -156
  76. package/dist/electron-build/utils/dual-source.js.map +1 -1
  77. package/dist/electron-build/utils/update-manager.js +22 -72
  78. package/dist/electron-build/utils/update-manager.js.map +1 -1
  79. package/dist/electron-build/utils/update-state.js +2 -19
  80. package/dist/electron-build/utils/update-state.js.map +1 -1
  81. package/dist/electron-build/utils/version-identity.js +193 -0
  82. package/dist/electron-build/utils/version-identity.js.map +1 -0
  83. package/dist/electron-build/utils/version-info.js +47 -101
  84. package/dist/electron-build/utils/version-info.js.map +1 -1
  85. package/dist/index.js +282 -96
  86. package/dist/ios/bolloon-web.json +10 -0
  87. package/dist/ios/mobile-contacts.js +44 -11
  88. package/dist/ios/mobile-core.js +1723 -84
  89. package/dist/ios/mobile-tasks.js +339 -0
  90. package/dist/ios/mobile-update.js +1442 -0
  91. package/dist/ios/mobile.js +308 -35
  92. package/dist/ios/routes-llm-config.js +344 -52
  93. package/dist/ios/routes-mobile-tasks.js +353 -0
  94. package/dist/ios/server.js +163 -4
  95. package/dist/ios/sw.js +1 -1
  96. package/dist/llm/config-store.js +146 -6
  97. package/dist/llm/connection-probe.js +744 -0
  98. package/dist/llm/custom-provider-store.js +224 -0
  99. package/dist/llm/data/provider-catalog-baked.js +19 -0
  100. package/dist/llm/model-catalog.js +717 -0
  101. package/dist/llm/model-discovery.js +1137 -0
  102. package/dist/llm/model-selection.js +1241 -0
  103. package/dist/llm/pi-ai.js +732 -54
  104. package/dist/llm/provider-catalog.js +898 -0
  105. package/dist/llm/provider-registry.js +917 -0
  106. package/dist/llm/tool-name.js +220 -0
  107. package/dist/setup/onboard.js +34 -8
  108. package/dist/utils/dual-source-facts.js +223 -0
  109. package/dist/utils/dual-source.js +12 -141
  110. package/dist/utils/update-manager.js +16 -59
  111. package/dist/utils/update-state.js +6 -18
  112. package/dist/utils/version-identity.js +176 -0
  113. package/dist/utils/version-info.js +9 -78
  114. package/dist/web/bolloon-web.json +10 -0
  115. package/dist/web/mobile-core.js +1721 -84
  116. package/dist/web/mobile-update.js +1442 -0
  117. package/dist/web/mobile.js +372 -35
  118. package/dist/web/routes-llm-config.js +344 -52
  119. package/dist/web/sw.js +1 -1
  120. package/package.json +1 -1
  121. package/scripts/build-web.ts +36 -0
@@ -9,6 +9,11 @@
9
9
  * - 错误分类 `classifyGithubError` 与 §3 的 `classifyRegistryError` **并列, 不合并**
10
10
  * - dev 快照的**取源与构建**也在这里 (update-manager 只负责"切换 + 验证 + 回滚", 不另造一套)
11
11
  *
12
+ * 2026-09-26 拆分 (手机端要复用同一份语义): **纯语义那一半**去 `dual-source-facts.ts`
13
+ * (分类 / 交叉校验 / dev 快照比较 —— 无 `node:`, 因此能打进手机端 WebView bundle);
14
+ * 本文件只留**需要网络与子进程**的那一半 (真调 api.github.com / codeload / 现场构建),
15
+ * 并把纯那一半的名字原样再导出 —— 既有 `from './dual-source.js'` 的 import 一行不用改。
16
+ *
12
17
  * 硬口径 (§12.5, 验收核心):
13
18
  * - 源不可达 / 版本不存在 → **拒绝并说清** (带具体分类), 绝不静默装回旧版
14
19
  * - 不编造不存在的路径: 本仓 GitHub 上**真的没有 Release** (0 个), 所以 "Release 缺" 时
@@ -21,34 +26,13 @@ import * as http from 'http';
21
26
  import * as https from 'https';
22
27
  import { spawnSync } from 'child_process';
23
28
  import { GITHUB_API_BASE, GITHUB_UPSTREAM_SLUG, DEV_BRANCH, devIdentity, baseVersionOf, readPackageAt, DEV_CHANNEL_WARNING, } from './version-info.js';
24
- export { DEV_CHANNEL_WARNING };
25
- export function toGithubReport(r) {
26
- if (!r.ok) {
27
- return {
28
- reachable: false, reason: r.reason, detail: r.detail, retryAt: r.retryAt,
29
- headSha: r.facts?.headSha ?? null, ref: r.facts?.ref ?? `refs/heads/${DEV_BRANCH}`,
30
- releases: r.facts?.releases.length, tags: r.facts?.tags.length,
31
- newestVersion: r.facts ? newestGithubVersion(r.facts) : null,
32
- partial: r.facts?.partial, fetchedAt: r.facts?.fetchedAt,
33
- };
34
- }
35
- return {
36
- reachable: true, headSha: r.facts.headSha, ref: r.facts.ref,
37
- releases: r.facts.releases.length, tags: r.facts.tags.length,
38
- newestVersion: newestGithubVersion(r.facts), partial: r.facts.partial, fetchedAt: r.facts.fetchedAt,
39
- };
40
- }
41
- export function renderGithubReportLine(g) {
42
- if (!g)
43
- return 'GitHub 源: 未查询';
44
- if (!g.reachable) {
45
- const retry = g.retryAt ? `, 限流重试时间 ${g.retryAt}` : '';
46
- return `GitHub 源: 不可达 — github_unavailable(${g.reason}) ${g.detail || ''}${retry}`;
47
- }
48
- return `GitHub 源: 可达 · master HEAD ${g.headSha ? g.headSha.slice(0, 7) : '未知'} · Release ${g.releases ?? 0} 个 · Tag ${g.tags ?? 0} 个 · 最新版本标签 ${g.newestVersion || '无'}${g.partial ? ' (部分请求失败)' : ''}`;
49
- }
50
- // ── HTTP ────────────────────────────────────────────────────────────────────
51
- const OFFLINE_CODES = new Set(['ENOTFOUND', 'EAI_AGAIN', 'ECONNREFUSED', 'ECONNRESET', 'ETIMEDOUT', 'EHOSTUNREACH', 'ENETUNREACH', 'ENETDOWN', 'EAI_FAIL', 'UND_ERR_CONNECT_TIMEOUT', 'CERT_HAS_EXPIRED']);
29
+ import { classifyGithubError, } from './dual-source-facts.js';
30
+ /**
31
+ * 纯语义那一半 (错误分类 / 交叉校验 / dev 快照比较 / 报告渲染) —— **唯一一份在
32
+ * `dual-source-facts.ts`** (桌面与手机端共用)。这里原样再导出: 桌面既有 import 路径不变,
33
+ * 手机端 WebView 直接 import 那份纯模块 (没有 `node:` ⇒ 能进 bundle)。
34
+ */
35
+ export { GITHUB_REASONS, OFFLINE_CODES, classifyGithubError, classifyRegistryError, newestGithubVersion, numericCompare, toGithubReport, renderGithubReportLine, crossCheckStable, compareDevSnapshots, DEV_CHANNEL_WARNING, } from './dual-source-facts.js';
52
36
  /** 可选 GitHub token (只为提配额: 匿名 60 次/小时, 认证 5000 次/小时)。**永不打印、永不入库**。 */
53
37
  function githubToken(env = process.env) {
54
38
  const t = env.GITHUB_TOKEN || env.GH_TOKEN || env.BOLLOON_GITHUB_TOKEN;
@@ -74,26 +58,6 @@ function httpGetRaw(url, timeoutMs, token) {
74
58
  req.on('timeout', () => { req.destroy(new Error('timeout')); });
75
59
  });
76
60
  }
77
- /** 网络类 → offline; 403/429 → rate_limited; 404 → not_found; 其它 → http_error。 */
78
- export function classifyGithubError(err, httpStatus, headers = {}) {
79
- const retryAt = headers['x-ratelimit-reset']
80
- ? new Date(Number(headers['x-ratelimit-reset']) * 1000).toISOString()
81
- : null;
82
- const code = String(err?.code || '');
83
- if (OFFLINE_CODES.has(code) || /timeout|timed out|ENOTFOUND|getaddrinfo|network|socket hang up|超时/i.test(String(err?.message || ''))) {
84
- return { kind: 'github_unavailable', reason: 'offline', detail: `${code || err?.message || '网络不可达'}`, retryAt: null };
85
- }
86
- if (httpStatus === 403 || httpStatus === 429) {
87
- return { kind: 'github_unavailable', reason: 'rate_limited', detail: `GitHub 返回 HTTP ${httpStatus} (限流/权限)`, retryAt };
88
- }
89
- if (httpStatus === 404) {
90
- return { kind: 'github_unavailable', reason: 'not_found', detail: `GitHub 上没有 ${GITHUB_UPSTREAM_SLUG} 的该项记录 (HTTP 404)`, retryAt: null };
91
- }
92
- if (httpStatus && httpStatus >= 400) {
93
- return { kind: 'github_unavailable', reason: 'http_error', detail: `GitHub 返回 HTTP ${httpStatus}`, retryAt };
94
- }
95
- return { kind: 'github_unavailable', reason: 'http_error', detail: code || err?.message || '未知错误', retryAt: null };
96
- }
97
61
  /**
98
62
  * 查 GitHub 的三个事实: Releases / Tags / master HEAD。
99
63
  * **任何一个请求失败 → 整条 `github_unavailable`** (带分类), 但把已拿到的那部分 facts 一起带回去
@@ -172,99 +136,6 @@ export async function fetchGithubFacts(opts = {}) {
172
136
  }
173
137
  return { ok: true, facts };
174
138
  }
175
- /** GitHub 上出现过的最高版本号 (Release ∪ Tag, 去掉 v 前缀)。没有 → null。 */
176
- export function newestGithubVersion(facts) {
177
- const all = [...facts.releases, ...facts.tags.map((t) => t.name)]
178
- .map((n) => String(n).trim().replace(/^v/, ''))
179
- .filter((n) => /^\d+\.\d+\.\d+/.test(n));
180
- if (all.length === 0)
181
- return null;
182
- return all.sort((a, b) => (numericCompare(a, b) < 0 ? 1 : -1))[0];
183
- }
184
- /** 只比数值段 (与 update-manager 的 compareVersions 同语义, 这里不反向依赖它)。 */
185
- function numericCompare(a, b) {
186
- const p = (v) => v.split('-')[0].split('.').map((x) => parseInt(x.replace(/\D.*$/, ''), 10) || 0);
187
- const pa = p(a);
188
- const pb = p(b);
189
- for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
190
- const x = pa[i] ?? 0;
191
- const y = pb[i] ?? 0;
192
- if (x < y)
193
- return -1;
194
- if (x > y)
195
- return 1;
196
- }
197
- return 0;
198
- }
199
- function hasName(names, v) {
200
- const set = new Set(names.map((n) => String(n).trim().replace(/^v/, '')));
201
- return set.has(v.replace(/^v/, ''));
202
- }
203
- /**
204
- * stable 的交叉校验 (§12.1/§12.5): npm 仍是权威, GitHub 只回答"这次发布的记录在不在"。
205
- *
206
- * agree npm 的 latest 在 GitHub 上有同名 tag/Release → 两条记录指向同一版
207
- * mismatch GitHub 记录了**更新的**版本而 registry 上没有 → 拒绝 (不许按 tag 去装)
208
- * missing_record registry 有该版本但 GitHub 缺这条记录 → **提醒级, 不阻塞** (§12.5 原文:
209
- * "报发布记录缺 Tag (提醒级, 不阻塞 stable 更新)")
210
- * no_record_at_all GitHub 上一条版本记录都没有 → 如实报"暂无 tag 可交叉校验"(不编造路径)
211
- */
212
- export function crossCheckStable(registryLatest, facts) {
213
- const common = {
214
- registryLatest, githubVersion: newestGithubVersion(facts),
215
- releases: facts.releases.length, tags: facts.tags.length,
216
- };
217
- if (!registryLatest) {
218
- return { ...common, kind: 'no_record_at_all', hasTag: false, hasRelease: false, blocking: false, detail: 'npm 没有给出 latest, 无从交叉校验' };
219
- }
220
- const allNames = [...facts.releases, ...facts.tags.map((t) => t.name)];
221
- const hasTag = hasName(facts.tags.map((t) => t.name), registryLatest);
222
- const hasRelease = hasName(facts.releases, registryLatest);
223
- const ghNewest = common.githubVersion;
224
- /**
225
- * **先判"GitHub 记录了比 registry 更新的发布"** —— 这是 §12.5 的 "Release 存在但 registry 上没有该版本 → cross_check_mismatch"。
226
- * 放在 agree 之前: 即使 registry 的 latest 自己也有 tag, 只要 GitHub 上还多出一条更新的发布记录, 两个源对
227
- * "当前最新是哪一版"就是不一致的 —— 这种不一致必须拦住, 不能因为"latest 有 tag"就宣布一致。
228
- */
229
- if (ghNewest && numericCompare(ghNewest, registryLatest) > 0) {
230
- return {
231
- ...common, kind: 'mismatch', hasTag, hasRelease, blocking: true,
232
- detail: `GitHub 上有 ${ghNewest} 的发布记录, 但 npm registry 的 latest 还是 ${registryLatest} — 拒绝按 GitHub 的记录安装`,
233
- };
234
- }
235
- if (hasTag || hasRelease) {
236
- return {
237
- ...common, kind: 'agree', hasTag, hasRelease, blocking: false,
238
- detail: `npm latest=${registryLatest} 在 GitHub 上有同名记录 (${hasRelease ? `Release v${registryLatest}` : `Tag v${registryLatest}`}) — 两个源指向同一版`,
239
- };
240
- }
241
- if (allNames.length === 0) {
242
- return {
243
- ...common, kind: 'no_record_at_all', hasTag: false, hasRelease: false, blocking: false,
244
- detail: `GitHub 上暂无任何 tag / Release (0 个) — 暂无可交叉校验的记录, npm 仍是权威 (latest=${registryLatest})`,
245
- };
246
- }
247
- return {
248
- ...common, kind: 'missing_record', hasTag, hasRelease, blocking: false,
249
- detail: `npm latest=${registryLatest} 在 GitHub 上没有对应记录 (GitHub 最新版本标签 ${ghNewest || '无'}; Release ${facts.releases.length} 个 / Tag ${facts.tags.length} 个) — 发布记录缺 Tag, 不阻塞 stable 更新`,
250
- };
251
- }
252
- export function compareDevSnapshots(installedSha, headSha, ref = `refs/heads/${DEV_BRANCH}`) {
253
- const short = (s) => (s ? s.slice(0, 7) : '未知');
254
- if (!headSha) {
255
- return { ref, headSha, installedSha, same: false, behind: false, detail: `${ref} 的 HEAD 读不到 — 无法判定 dev 快照新旧` };
256
- }
257
- if (!installedSha) {
258
- return { ref, headSha, installedSha, same: false, behind: false, detail: `当前不是 dev 快照 (stable 安装) — 切到 dev 将得到 ${ref} @ ${short(headSha)}` };
259
- }
260
- const same = installedSha.slice(0, 7) === headSha.slice(0, 7);
261
- return {
262
- ref, headSha, installedSha, same, behind: !same,
263
- detail: same
264
- ? `已是最新的 dev 快照 (${ref} @ ${short(headSha)}) — 同一个 commit`
265
- : `dev 快照落后: 已装 ${short(installedSha)} → master HEAD ${short(headSha)} (按 commit sha 判定, 不看版本号)`,
266
- };
267
- }
268
139
  function defaultRunNpm(args, cwd) {
269
140
  const r = spawnSync('npm', args, { cwd, encoding: 'utf-8', timeout: 900_000, maxBuffer: 32 * 1024 * 1024 });
270
141
  return { code: r.status ?? 1, stdout: r.stdout || '', stderr: r.stderr || '' };
@@ -25,10 +25,16 @@ import * as https from 'https';
25
25
  import * as http from 'http';
26
26
  import { spawnSync } from 'child_process';
27
27
  import { PKG_NAME, CONSTRAINT_PKG_NAME, NPM_REGISTRY_BASE, resolveUpdateChannel, distTagForChannel, detectInstallation, readPackageAt, channelKindOf, isDevIdentity, devShaFromIdentity, devIdentity, baseVersionOf, DEV_CHANNEL_WARNING, DEV_BACK_TO_STABLE_HINT, } from './version-info.js';
28
+ import { parseVersion, compareVersions, isKnownVersion, checkExitCode, REFUSED_STATUSES, } from './version-identity.js';
29
+ // registry 侧的错误分类也在纯那一半 (手机端要用同一份分类) —— 定义在 dual-source-facts.ts。
30
+ import { classifyRegistryError } from './dual-source-facts.js';
28
31
  import { fetchGithubFacts, toGithubReport, crossCheckStable, compareDevSnapshots, prepareDevSnapshot, renderGithubReportLine, } from './dual-source.js';
29
32
  import { readUpdateState, writeUpdateState, appendUpdateHistory, readUpdateHistory, acquireUpdateLock, releaseUpdateLock, readUpdateLock, lockIsStale, readUpdatePrefs, } from './update-state.js';
30
33
  export { DEV_CHANNEL_WARNING, DEV_BACK_TO_STABLE_HINT };
31
- // ── 版本比较 (唯一一份) ─────────────────────────────────────────────────────
34
+ // 版本比较 / 结论→退出码 / 拒绝语义: **唯一一份在 `version-identity.ts`** (桌面与手机共用)。
35
+ // 这里原样再导出, 避免出现第二套实现 (手机端 WebView 里也要判同一批结论)。
36
+ export { parseVersion, compareVersions, isKnownVersion, checkExitCode, REFUSED_STATUSES };
37
+ // ── 版本比较 (唯一一份在 version-identity.ts; 上面已再导出) ────────────────
32
38
  /**
33
39
  * 从子进程 stdout 里取 JSON —— **不要**按"行首是否 { "过滤:
34
40
  * `JSON.stringify(x, null, 2)` 是**多行**的, 只留第一行 `{` 会解析失败
@@ -56,28 +62,6 @@ export function parseJsonFromStdout(stdout) {
56
62
  return null;
57
63
  }
58
64
  }
59
- export function parseVersion(v) {
60
- const clean = String(v || '').trim().replace(/^v/, '').split('-')[0];
61
- const parts = clean.split('.').map((p) => parseInt(p.replace(/\D.*$/, ''), 10));
62
- return parts.map((n) => (Number.isFinite(n) ? n : 0));
63
- }
64
- /** -1 = a<b, 0 = 相等, 1 = a>b (只比数值段, 忽略预发布标签) */
65
- export function compareVersions(a, b) {
66
- const pa = parseVersion(a);
67
- const pb = parseVersion(b);
68
- for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
69
- const x = pa[i] ?? 0;
70
- const y = pb[i] ?? 0;
71
- if (x < y)
72
- return -1;
73
- if (x > y)
74
- return 1;
75
- }
76
- return 0;
77
- }
78
- export function isKnownVersion(v) {
79
- return !!v && v !== 'unknown' && /^\d/.test(String(v).trim().replace(/^v/, ''));
80
- }
81
65
  function httpGetJson(url, timeoutMs = 10000) {
82
66
  return new Promise((resolve, reject) => {
83
67
  const client = url.startsWith('https') ? https : http;
@@ -90,21 +74,11 @@ function httpGetJson(url, timeoutMs = 10000) {
90
74
  req.on('timeout', () => { req.destroy(new Error('timeout')); });
91
75
  });
92
76
  }
93
- const OFFLINE_CODES = new Set(['ENOTFOUND', 'EAI_AGAIN', 'ECONNREFUSED', 'ECONNRESET', 'ETIMEDOUT', 'EHOSTUNREACH', 'ENETUNREACH', 'ENETDOWN', 'EAI_FAIL', 'UND_ERR_CONNECT_TIMEOUT']);
94
- /** 网络类错误 → offline; 其它 (HTTP 5xx / 解析失败 / 包不存在) → registry_unavailable。 */
95
- export function classifyRegistryError(err, httpStatus) {
96
- const code = String(err?.code || '');
97
- if (OFFLINE_CODES.has(code) || /timeout|timed out|ENOTFOUND|getaddrinfo|network|socket hang up|超时/i.test(String(err?.message || ''))) {
98
- return { kind: 'offline', detail: `${code || err?.message || '网络不可达'}` };
99
- }
100
- if (httpStatus && httpStatus >= 500)
101
- return { kind: 'registry_unavailable', detail: `registry 返回 HTTP ${httpStatus}` };
102
- if (httpStatus === 404)
103
- return { kind: 'registry_unavailable', detail: `registry 上没有 ${PKG_NAME} (HTTP 404)` };
104
- if (httpStatus && httpStatus >= 400)
105
- return { kind: 'registry_unavailable', detail: `registry 返回 HTTP ${httpStatus}` };
106
- return { kind: 'registry_unavailable', detail: code || err?.message || '未知错误' };
107
- }
77
+ /**
78
+ * 网络类错误 → offline; 其它 (HTTP 5xx / 解析失败 / 包不存在) → registry_unavailable。
79
+ * **唯一一份在 `dual-source-facts.ts`** (上面已 import+再导出) —— 桌面与手机端同一个分类器。
80
+ */
81
+ export { classifyRegistryError };
108
82
  export async function queryRegistryDoc(pkg = PKG_NAME, timeoutMs = 10000) {
109
83
  const url = `${NPM_REGISTRY_BASE}/${encodeURIComponent(pkg).replace('%40', '@')}`;
110
84
  try {
@@ -136,28 +110,11 @@ export async function queryRegistryDoc(pkg = PKG_NAME, timeoutMs = 10000) {
136
110
  }
137
111
  }
138
112
  /**
139
- * 检查结论 → 退出码 (稳定约定, §2): 0 正常 / 2 检查不可用。
140
- *
113
+ * 检查结论 → 退出码 / "源不可达必拒" 的这一组结论:
114
+ * **唯一一份在 `version-identity.ts`** (上面已 import+再导出)。
141
115
  * §12.5 的核心就是这一句: **源不可达 / 版本不存在 → 拒绝并说清 (退出码 2)**,
142
- * 不许静默装回旧版, 也不许打印"✅ 已是最新"。
143
- */
144
- export function checkExitCode(status) {
145
- switch (status) {
146
- case 'offline':
147
- case 'registry_unavailable':
148
- case 'local_version_unknown':
149
- case 'github_unavailable':
150
- case 'cross_check_mismatch':
151
- return 2;
152
- default:
153
- return 0;
154
- }
155
- }
156
- /**
157
- * "源不可达 / 版本不存在" 这一组结论 —— 它们必须**拒绝执行并说清**。
158
- * 这是 §12.5 那一条的代码落点: 不许静默装回旧版, 也不许打印"已是最新"。
116
+ * 不许静默装回旧版, 也不许打印"✅ 已是最新"。桌面与手机共用同一份。
159
117
  */
160
- export const REFUSED_STATUSES = ['offline', 'registry_unavailable', 'local_version_unknown', 'github_unavailable', 'cross_check_mismatch'];
161
118
  /**
162
119
  * 检查更新。**这是唯一的检查入口** —— CLI / 启动后台 / Python 检查器 / 安装脚本都走它。
163
120
  *
@@ -775,7 +732,7 @@ export async function applyUpdate(opts = {}) {
775
732
  if (!lock.ok) {
776
733
  const held = lock.heldBy;
777
734
  await stage('blocked', `已有更新进程 (pid ${held?.pid})`);
778
- return record({ stage: 'blocked', ok: false, from, to, durationMs: Date.now() - started, reason: `已有更新进程持有锁 (pid ${held?.pid}, ${held?.at})`, needsRestart: false }, 'blocked');
735
+ return record({ stage: 'blocked', ok: false, from, to, durationMs: Date.now() - started, reason: `另一个更新进程持有锁 (pid ${held?.pid}, ${held?.at})`, needsRestart: false }, 'blocked');
779
736
  }
780
737
  const tmpRoot = await fsp.mkdtemp(path.join(os.tmpdir(), 'bolloon-update-'));
781
738
  const runNpm = opts.runNpm || defaultRunNpm;
@@ -17,25 +17,13 @@ import * as path from 'path';
17
17
  import * as fs from 'fs';
18
18
  import * as fsp from 'fs/promises';
19
19
  import { resolveBolloonHome } from '../setup/setup-store.js';
20
+ import { CHECK_STATUSES } from './version-identity.js';
20
21
  // ── 枚举 (冻结: 出口/状态都不许临时造词) ────────────────────────────────────
21
- /**
22
- * 一次"检查"的结论。**9 个值就是全部**, 不认识的一律落进 registry_unavailable 并带原因。
23
- *
24
- * 2026-09-25 (update-protocol §12.3, 双源): **只增不改** —— 新增 2 个, 错误分类**不合并**:
25
- * github_unavailable GitHub 不可达 / 403·429 限流 / 没有 Release / 没有 master
26
- * cross_check_mismatch stable 下 npm 与 GitHub 指向不同版本 (两个源的事实都摆出来)
27
- */
28
- export const CHECK_STATUSES = [
29
- 'up_to_date', // 确认最新
30
- 'update_available', // 有新版本
31
- 'check_skipped', // 节流/显式跳过, 结论来自缓存
32
- 'offline', // 网络不可达 (≠ 最新)
33
- 'registry_unavailable', // 可达但拿不到有效数据 (5xx / 包不存在 / 无 dist-tags)
34
- 'local_version_unknown', // 读不到本地版本 (不猜 0.0.0)
35
- 'unsupported_installation', // 认得出本地版本, 但这种安装方式不支持自动更新
36
- 'github_unavailable', // GitHub 源不可用 (offline / rate_limited / not_found / http_error)
37
- 'cross_check_mismatch', // npm 与 GitHub 两个源不一致 → 拒绝更新
38
- ];
22
+ //
23
+ // 2026-09-26: 结论枚举与 UpdatePrefs 的结构定义抽到 `version-identity.ts` (纯模块, 无 node:)。
24
+ // **只有一个原因**: 手机端 WebView 里没有 `fs`/`os`, 但手机端必须用**同一份**结论语义
25
+ // (9 个结论 + REFUSED_STATUSES 的拒绝口径)。这里原样再导出, 既有 import 路径不变。
26
+ export { CHECK_STATUSES };
39
27
  /** 一次"更新执行"的流水线状态。 */
40
28
  export const UPDATE_RUN_STATUSES = [
41
29
  'planned', 'downloading', 'staged', 'switching', 'verifying',
@@ -0,0 +1,176 @@
1
+ /**
2
+ * version-identity.ts — 版本身份 / 通道 / 结论分类的**纯词汇表** (2026-09-26)
3
+ *
4
+ * 为什么要从 `version-info.ts` 里抽出来 (抽出 ≠ 改写): 手机端 WebView **没有 node:**
5
+ * (`fs`/`os`/`path`/`child_process` 都没有), 而手机端双源更新必须用**和桌面同一份**
6
+ * 身份与结论语义 —— 否则同一台机器上的 `bolloon update status` 和手机上的"当前源"
7
+ * 会长出两套说法。所以:
8
+ *
9
+ * 本模块 = **不碰 node 的那一半** (纯函数 + 常量), 桌面与手机都 import 它;
10
+ * `version-info.ts` = 本模块 + 需要 node 的探测 (安装识别 / git / npm / python / VersionInfo)
11
+ *
12
+ * 纪律 (改这里之前先读):
13
+ * · 不许 import 任何 `node:*` / 不许在模块顶层直读 `process.env` (浏览器里会 ReferenceError);
14
+ * 环境变量一律走 `envValue()` (`typeof process` 守卫)。
15
+ * · 版本比较语义与结论枚举**只此一份** (§12.1 / §3): stable 比 semver, dev 比 commit sha。
16
+ */
17
+ // ── 包与上游 ────────────────────────────────────────────────────────────────
18
+ export const PKG_NAME = '@bolloon/bolloon-agent';
19
+ export const CONSTRAINT_PKG_NAME = '@bolloon/constraint-runtime';
20
+ export const UPSTREAM_REPO = 'https://github.com/logos-42/bolloon';
21
+ /** GitHub 上游 (owner/repo) —— 与 UPSTREAM_REPO 同一处, 这里给 API 用。 */
22
+ export const GITHUB_UPSTREAM_SLUG = 'logos-42/bolloon';
23
+ /**
24
+ * 环境变量读取 (浏览器安全的唯一入口)。
25
+ * `process` 在 WebView 里不存在 —— 直接写 `process.env.X` 会在**模块求值期**抛,
26
+ * 表现为整页 JS 不执行 (那样的红只有真浏览器能看见)。这里用 `typeof` 守卫。
27
+ */
28
+ export function envValue(key) {
29
+ try {
30
+ // ① 壳层/页面的配置通道 (浏览器没有 process.env; 手机端由原生壳或验收 harness 注入)
31
+ const cfg = globalThis?.__bolloonUpdateConfig;
32
+ if (cfg && cfg[key] !== undefined && cfg[key] !== null)
33
+ return String(cfg[key]);
34
+ // ② 进程环境 (桌面 CLI / Node 验收)
35
+ const env = typeof process !== 'undefined' ? process?.env : null;
36
+ const v = env ? env[key] : undefined;
37
+ return v === undefined || v === null ? undefined : String(v);
38
+ }
39
+ catch {
40
+ return undefined;
41
+ }
42
+ }
43
+ /** npm registry (可用 BOLLOON_NPM_REGISTRY 指向镜像/内网; 末尾斜杠统一去掉) */
44
+ export const NPM_REGISTRY_BASE = (envValue('BOLLOON_NPM_REGISTRY') || 'https://registry.npmjs.org').replace(/\/+$/, '');
45
+ /** GitHub API 根 (可被 BOLLOON_GITHUB_API 覆盖 → 受控假 API / 企业实例)。 */
46
+ export const GITHUB_API_BASE = (envValue('BOLLOON_GITHUB_API') || 'https://api.github.com').replace(/\/+$/, '');
47
+ /** dev 通道盯的分支 (冻结: master)。 */
48
+ export const DEV_BRANCH = 'master';
49
+ export const DEV_REF = `refs/heads/${DEV_BRANCH}`;
50
+ // ── 安装方式 / 来源 / 通道 ──────────────────────────────────────────────────
51
+ export const INSTALL_METHODS = [
52
+ 'npm-global', 'npm-local', 'source-git', 'release-binary', 'development', 'unknown',
53
+ ];
54
+ export const UPDATE_SOURCES = ['npm', 'github-release', 'git', 'unknown'];
55
+ export const UPDATE_CHANNELS = ['stable', 'beta', 'dev'];
56
+ /** 发行通道: 环境变量 > 配置 > stable。npm 只有 `latest` (stable) 一个 dist-tag, beta/dev 暂走同一标签。 */
57
+ export function resolveUpdateChannel(explicit, env) {
58
+ const raw = (explicit || (env ? env.BOLLOON_UPDATE_CHANNEL : envValue('BOLLOON_UPDATE_CHANNEL')) || '').trim().toLowerCase();
59
+ if (raw === 'stable' || raw === 'beta' || raw === 'dev')
60
+ return raw;
61
+ return 'stable';
62
+ }
63
+ /** dist-tag 映射: stable → latest; beta/dev 暂与 stable 同源 (明说, 不假装有独立通道)。 */
64
+ export function distTagForChannel(channel) {
65
+ return channel === 'beta' ? 'beta' : 'latest';
66
+ }
67
+ // ── 身份 (dev 用 commit sha, 不用 semver) ───────────────────────────────────
68
+ /**
69
+ * 版本比较语义 (显式, 不是隐含约定):
70
+ * semver —— stable: 按 semver 数值段比大小
71
+ * git-ref —— dev: **不用 semver**, 只判"是否同一 commit / 是否落后"
72
+ */
73
+ export const CHANNEL_KINDS = ['semver', 'git-ref'];
74
+ export function channelKindOf(channel) {
75
+ return channel === 'dev' ? 'git-ref' : 'semver';
76
+ }
77
+ /** dev 快照身份里的后缀标记 (`0.4.33+dev.a1b2c3d`)。 */
78
+ export const DEV_IDENTITY_SEP = '+dev.';
79
+ /** dev 快照身份 —— 用 commit sha (不是 semver) 当身份, 版本号只作参考展示。 */
80
+ export function devIdentity(baseVersion, sha) {
81
+ return `${baseVersionOf(baseVersion)}${DEV_IDENTITY_SEP}${String(sha).slice(0, 7)}`;
82
+ }
83
+ /** 去掉 `+dev.<sha>` 后缀, 拿到基础版本号 (stable 目标的比对基准)。 */
84
+ export function baseVersionOf(v) {
85
+ return String(v || '').split(DEV_IDENTITY_SEP)[0].trim();
86
+ }
87
+ /** 这个版本号是不是一个 dev 快照身份。 */
88
+ export function isDevIdentity(v) {
89
+ return String(v || '').includes(DEV_IDENTITY_SEP);
90
+ }
91
+ /** 从 dev 身份里取 commit sha (前 7 位); 不是 dev 身份返回 null。 */
92
+ export function devShaFromIdentity(v) {
93
+ if (!isDevIdentity(v))
94
+ return null;
95
+ const sha = String(v).split(DEV_IDENTITY_SEP)[1]?.trim();
96
+ return sha ? sha : null;
97
+ }
98
+ /**
99
+ * dev 通道的**唯一一句警告** (§12.4 硬约束 1) —— 检查 / 计划 / 执行 / 手机端四处打印同一句话。
100
+ * 定义在这里 (唯一事实层), 由 dual-source / update-manager / CLI / 手机端复用, 不允许各处自己造一句。
101
+ */
102
+ export const DEV_CHANNEL_WARNING = '⚠️ dev 通道 = GitHub master HEAD 的即时快照 (未走发布门): 可能中断正在跑的 Goal/Run, 且不保证可回滚到上一个 dev 版。';
103
+ /** 一键回 stable 的提示语 (同一份措辞)。 */
104
+ export const DEV_BACK_TO_STABLE_HINT = '一键回稳定版: bolloon update now --channel stable';
105
+ /** 手机端同一件事的说法 (原生层规则不同: 手机走 web 层 OTA, 不是 npm 全局安装)。 */
106
+ export const DEV_BACK_TO_STABLE_HINT_MOBILE = '一键回稳定版: 手机端「更新」页 → 切回 stable';
107
+ // ── 版本比较 (唯一一份) ─────────────────────────────────────────────────────
108
+ export function parseVersion(v) {
109
+ const clean = String(v || '').trim().replace(/^v/, '').split('-')[0];
110
+ const parts = clean.split('.').map((p) => parseInt(p.replace(/\D.*$/, ''), 10));
111
+ return parts.map((n) => (Number.isFinite(n) ? n : 0));
112
+ }
113
+ /** -1 = a<b, 0 = 相等, 1 = a>b (只比数值段, 忽略预发布标签) */
114
+ export function compareVersions(a, b) {
115
+ const pa = parseVersion(a);
116
+ const pb = parseVersion(b);
117
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
118
+ const x = pa[i] ?? 0;
119
+ const y = pb[i] ?? 0;
120
+ if (x < y)
121
+ return -1;
122
+ if (x > y)
123
+ return 1;
124
+ }
125
+ return 0;
126
+ }
127
+ export function isKnownVersion(v) {
128
+ return !!v && v !== 'unknown' && /^\d/.test(String(v).trim().replace(/^v/, ''));
129
+ }
130
+ // ── 检查结论 + 拒绝语义 (§3 / §12.3 / §12.5) ────────────────────────────────
131
+ /**
132
+ * 一次"检查"的结论。**9 个值就是全部** (桌面侧), 不认识的一律落进 registry_unavailable 并带原因。
133
+ *
134
+ * 2026-09-25 (update-protocol §12.3, 双源): **只增不改** —— 新增 2 个, 错误分类**不合并**:
135
+ * github_unavailable GitHub 不可达 / 403·429 限流 / 没有 Release / 没有 master
136
+ * cross_check_mismatch stable 下 npm 与 GitHub 指向不同版本 (两个源的事实都摆出来)
137
+ */
138
+ export const CHECK_STATUSES = [
139
+ 'up_to_date', // 确认最新
140
+ 'update_available', // 有新版本
141
+ 'check_skipped', // 节流/显式跳过, 结论来自缓存
142
+ 'offline', // 网络不可达 (≠ 最新)
143
+ 'registry_unavailable', // 可达但拿不到有效数据 (5xx / 包不存在 / 无 dist-tags)
144
+ 'local_version_unknown', // 读不到本地版本 (不猜 0.0.0)
145
+ 'unsupported_installation', // 认得出本地版本, 但这种安装方式不支持自动更新
146
+ 'github_unavailable', // GitHub 源不可用 (offline / rate_limited / not_found / http_error)
147
+ 'cross_check_mismatch', // npm 与 GitHub 两个源不一致 → 拒绝更新
148
+ ];
149
+ /**
150
+ * "源不可达 / 版本不存在" 这一组结论 —— 它们必须**拒绝执行并说清**。
151
+ * 这是 §12.5 那一条的代码落点: 不许静默装回旧版, 也不许打印"已是最新"。
152
+ *
153
+ * **桌面与手机共用同一份** —— 手机上这几个状态同样会让 apply 直接 blocked, 一个字节都不下载。
154
+ */
155
+ export const REFUSED_STATUSES = ['offline', 'registry_unavailable', 'local_version_unknown', 'github_unavailable', 'cross_check_mismatch'];
156
+ /** 检查结论 → 退出码 (稳定约定, §2): 0 正常 / 2 检查不可用。 */
157
+ export function checkExitCode(status) {
158
+ switch (status) {
159
+ case 'offline':
160
+ case 'registry_unavailable':
161
+ case 'local_version_unknown':
162
+ case 'github_unavailable':
163
+ case 'cross_check_mismatch':
164
+ return 2;
165
+ default:
166
+ return 0;
167
+ }
168
+ }
169
+ // ── 身份那一行的渲染 (§12.5: 桌面与手机同一份措辞) ──────────────────────────
170
+ /** 双源身份那一行 (谁装的 / 哪个 sha / 能切回哪) —— 桌面 `--version`/`update status` 与手机端共用同一份措辞。 */
171
+ export function describeSourceIdentity(info) {
172
+ const src = info.installedDevSha ? 'dev (GitHub master 快照)' : 'stable (npm registry)';
173
+ const ident = info.installedDevSha ? `commit ${info.installedDevSha}` : baseVersionOf(info.packageVersion);
174
+ const back = info.switchableTo ? ` · 可切回 ${info.switchableTo.channel} (${info.switchableTo.source})` : '';
175
+ return `当前安装源: ${src} · 版本/commit: ${ident} · 比较语义: ${info.channelKind}${back}`;
176
+ }
@@ -26,79 +26,13 @@ import { execFileSync } from 'child_process';
26
26
  import { fileURLToPath } from 'url';
27
27
  import { resolveBolloonHome } from '../setup/setup-store.js';
28
28
  import { renderRuntimeLines } from './runtime-bootstrap.js';
29
- export const PKG_NAME = '@bolloon/bolloon-agent';
30
- export const CONSTRAINT_PKG_NAME = '@bolloon/constraint-runtime';
31
- /** npm registry (可用 BOLLOON_NPM_REGISTRY 指向镜像/内网; 末尾斜杠统一去掉) */
32
- export const NPM_REGISTRY_BASE = (process.env.BOLLOON_NPM_REGISTRY || 'https://registry.npmjs.org').replace(/\/+$/, '');
33
- export const UPSTREAM_REPO = 'https://github.com/logos-42/bolloon';
34
- export const INSTALL_METHODS = [
35
- 'npm-global', 'npm-local', 'source-git', 'release-binary', 'development', 'unknown',
36
- ];
37
- export const UPDATE_SOURCES = ['npm', 'github-release', 'git', 'unknown'];
38
- export const UPDATE_CHANNELS = ['stable', 'beta', 'dev'];
39
- /** 发行通道: 环境变量 > 配置 > stable。npm 只有 `latest` (stable) 一个 dist-tag, beta/dev 暂走同一标签。 */
40
- export function resolveUpdateChannel(explicit) {
41
- const raw = (explicit || process.env.BOLLOON_UPDATE_CHANNEL || '').trim().toLowerCase();
42
- if (raw === 'stable' || raw === 'beta' || raw === 'dev')
43
- return raw;
44
- return 'stable';
45
- }
46
- /** dist-tag 映射: stable → latest; beta/dev 暂与 stable 同源 (明说, 不假装有独立通道)。 */
47
- export function distTagForChannel(channel) {
48
- return channel === 'beta' ? 'beta' : 'latest';
49
- }
50
- // ── 双源 (update-protocol §12, 2026-09-25) ──────────────────────────────────
51
- //
52
- // stable: npm registry (`dist-tags.latest`) 是**权威**, GitHub Release/Tag 是**交叉校验源**;
53
- // dev : GitHub `master` HEAD 是**唯一源**, 版本身份 = `<package.json 版本>+dev.<commit sha 前 7>`。
54
- //
55
- // 两条硬口径 (从这里开始, 全局只有这一份):
56
- // ① 两套版本比较语义**显式分开** —— stable 比 semver, dev 比 commit sha (`ChannelKind`)
57
- // ② 源不可达 / 版本不存在 → **拒绝并说清**, 不许静默装回旧版 (§12.5)
58
- /** GitHub 上游 (owner/repo) —— 与 UPSTREAM_REPO 同一处, 这里给 API 用。 */
59
- export const GITHUB_UPSTREAM_SLUG = 'logos-42/bolloon';
60
- /** GitHub API 根 (可被 BOLLOON_GITHUB_API 覆盖 → 受控假 API / 企业实例)。 */
61
- export const GITHUB_API_BASE = (process.env.BOLLOON_GITHUB_API || 'https://api.github.com').replace(/\/+$/, '');
62
- /** dev 通道盯的分支 (冻结: master)。 */
63
- export const DEV_BRANCH = 'master';
64
- export const DEV_REF = `refs/heads/${DEV_BRANCH}`;
29
+ import { PKG_NAME, CONSTRAINT_PKG_NAME, UPSTREAM_REPO, GITHUB_UPSTREAM_SLUG, INSTALL_METHODS, UPDATE_SOURCES, UPDATE_CHANNELS, CHANNEL_KINDS, DEV_BRANCH, DEV_REF, DEV_IDENTITY_SEP, DEV_CHANNEL_WARNING, DEV_BACK_TO_STABLE_HINT, NPM_REGISTRY_BASE, GITHUB_API_BASE, resolveUpdateChannel, distTagForChannel, channelKindOf, devIdentity, baseVersionOf, isDevIdentity, devShaFromIdentity, parseVersion, compareVersions, isKnownVersion, checkExitCode, REFUSED_STATUSES, CHECK_STATUSES, describeSourceIdentity, } from './version-identity.js';
65
30
  /**
66
- * 版本比较语义 (显式, 不是隐含约定):
67
- * semver —— stable: 按 semver 数值段比大小
68
- * git-ref —— dev: **不用 semver**, 只判"是否同一 commit / 是否落后"
31
+ * 版本身份 / 通道 / 结论分类的**纯词汇表**已抽到 `version-identity.ts` (2026-09-26),
32
+ * 这里原样再导出 —— 既有 import 路径 (`from './version-info.js'`) 一行都不用改,
33
+ * 手机端 (无 node:) 直接 import 那份纯模块即可, 两边共用同一套语义。
69
34
  */
70
- export const CHANNEL_KINDS = ['semver', 'git-ref'];
71
- export function channelKindOf(channel) {
72
- return channel === 'dev' ? 'git-ref' : 'semver';
73
- }
74
- /** dev 快照身份里的后缀标记 (`0.4.33+dev.a1b2c3d`)。 */
75
- export const DEV_IDENTITY_SEP = '+dev.';
76
- /** dev 快照身份 —— 用 commit sha (不是 semver) 当身份, 版本号只作参考展示。 */
77
- export function devIdentity(baseVersion, sha) {
78
- return `${baseVersionOf(baseVersion)}${DEV_IDENTITY_SEP}${String(sha).slice(0, 7)}`;
79
- }
80
- /**
81
- * dev 通道的**唯一一句警告** (§12.4 硬约束 1) —— 检查 / 计划 / 执行三处打印同一句话。
82
- * 定义在这里 (唯一事实层), 由 dual-source / update-manager / CLI 复用, 不允许各处自己造一句。
83
- */
84
- export const DEV_CHANNEL_WARNING = '⚠️ dev 通道 = GitHub master HEAD 的即时快照 (未走发布门): 可能中断正在跑的 Goal/Run, 且不保证可回滚到上一个 dev 版。';
85
- /** 一键回 stable 的提示语 (同一份措辞)。 */
86
- export const DEV_BACK_TO_STABLE_HINT = '一键回稳定版: bolloon update now --channel stable';
87
- /** 去掉 `+dev.<sha>` 后缀, 拿到基础版本号 (stable 目标的比对基准)。 */
88
- export function baseVersionOf(v) {
89
- return String(v || '').split(DEV_IDENTITY_SEP)[0].trim();
90
- }
91
- /** 这个版本号是不是一个 dev 快照身份。 */
92
- export function isDevIdentity(v) {
93
- return String(v || '').includes(DEV_IDENTITY_SEP);
94
- }
95
- /** 从 dev 身份里取 commit sha (前 7 位); 不是 dev 身份返回 null。 */
96
- export function devShaFromIdentity(v) {
97
- if (!isDevIdentity(v))
98
- return null;
99
- const sha = String(v).split(DEV_IDENTITY_SEP)[1]?.trim();
100
- return sha ? sha : null;
101
- }
35
+ export { PKG_NAME, CONSTRAINT_PKG_NAME, UPSTREAM_REPO, GITHUB_UPSTREAM_SLUG, INSTALL_METHODS, UPDATE_SOURCES, UPDATE_CHANNELS, CHANNEL_KINDS, DEV_BRANCH, DEV_REF, DEV_IDENTITY_SEP, DEV_CHANNEL_WARNING, DEV_BACK_TO_STABLE_HINT, NPM_REGISTRY_BASE, GITHUB_API_BASE, resolveUpdateChannel, distTagForChannel, channelKindOf, devIdentity, baseVersionOf, isDevIdentity, devShaFromIdentity, parseVersion, compareVersions, isKnownVersion, checkExitCode, REFUSED_STATUSES, CHECK_STATUSES, describeSourceIdentity, };
102
36
  // ── 包根定位 ────────────────────────────────────────────────────────────────
103
37
  /**
104
38
  * 从当前模块 URL 定位**运行中的**包根。
@@ -509,13 +443,10 @@ export function describeUpdateLine(info) {
509
443
  default: return `更新检查: ${u.lastCheckStatus}`;
510
444
  }
511
445
  }
512
- /** 双源身份那一行 (谁装的 / 哪个 sha / 能切回哪) —— `--version` 与 `update status` 共用同一份措辞。 */
513
- export function describeSourceIdentity(info) {
514
- const src = info.installedDevSha ? 'dev (GitHub master 快照)' : 'stable (npm registry)';
515
- const ident = info.installedDevSha ? `commit ${info.installedDevSha}` : baseVersionOf(info.packageVersion);
516
- const back = info.switchableTo ? ` · 可切回 ${info.switchableTo.channel} (${info.switchableTo.source})` : '';
517
- return `当前安装源: ${src} · 版本/commit: ${ident} · 比较语义: ${info.channelKind}${back}`;
518
- }
446
+ /**
447
+ * 双源身份那一行 (谁装的 / 哪个 sha / 能切回哪) —— 已在 `version-identity.ts` (唯一一份,
448
+ * 桌面 `--version` 与 `update status` 共用同一份措辞)。这里只留注释, 避免出现第二份实现。
449
+ */
519
450
  export function renderVersionText(info, opts = {}) {
520
451
  if (opts.verbose) {
521
452
  const lines = [
@@ -0,0 +1,10 @@
1
+ {
2
+ "schema": "bolloon-mobile-web/1",
3
+ "version": "0.5.2",
4
+ "channel": "stable",
5
+ "build": "stable",
6
+ "commit": "0f98f0fc9bbcd60f04105402a5163b85c58bc785",
7
+ "builtAt": "2026-09-28T08:27:37.768Z",
8
+ "layer": "web",
9
+ "note": "web 资源层构建戳 (手机端 OTA 的身份第 ③ 级回退; 装机后以 OTA state.json 为准)"
10
+ }