@bolloon/bolloon-agent 0.4.33 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/dist/agents/contacts/chain.js +27 -7
  2. package/dist/agents/execution-supervisor.js +580 -40
  3. package/dist/agents/external-events.js +35 -6
  4. package/dist/agents/gateway-group.js +100 -12
  5. package/dist/agents/goal-criteria.js +10 -3
  6. package/dist/agents/goal-flywheel/block-executor.js +346 -0
  7. package/dist/agents/goal-flywheel/continuation-decision.js +592 -0
  8. package/dist/agents/goal-flywheel/goal-change.js +754 -0
  9. package/dist/agents/goal-flywheel/index.js +26 -0
  10. package/dist/agents/goal-flywheel/memory-layers.js +227 -0
  11. package/dist/agents/goal-flywheel/run-closure.js +809 -0
  12. package/dist/agents/goal-flywheel/skill-candidate.js +628 -0
  13. package/dist/agents/goal-flywheel/types.js +329 -0
  14. package/dist/agents/goal-flywheel/wiring/change.js +288 -0
  15. package/dist/agents/goal-flywheel/wiring/closure.js +697 -0
  16. package/dist/agents/goal-flywheel/wiring/continuation.js +432 -0
  17. package/dist/agents/goal-flywheel/wiring/contract.js +270 -0
  18. package/dist/agents/goal-flywheel/wiring/index.js +26 -0
  19. package/dist/agents/goal-flywheel/wiring/monitor.js +254 -0
  20. package/dist/agents/goal-flywheel/wiring/seams.js +592 -0
  21. package/dist/agents/goal-flywheel/work-contract.js +478 -0
  22. package/dist/agents/goal-flywheel/work-monitor.js +488 -0
  23. package/dist/agents/goal-flywheel-wiring.js +1870 -0
  24. package/dist/agents/goal-state-reducer.js +406 -0
  25. package/dist/agents/goal-store.js +35 -1
  26. package/dist/agents/mobile-flywheel-view.js +144 -0
  27. package/dist/agents/mobile-task-actions-verify.js +74 -0
  28. package/dist/agents/mobile-task-actions.js +178 -0
  29. package/dist/agents/mobile-task-views.js +199 -0
  30. package/dist/agents/model-policy.js +651 -0
  31. package/dist/agents/network-pulse.js +304 -18
  32. package/dist/agents/pi-sdk-tools.js +84 -33
  33. package/dist/agents/pi-sdk.js +157 -42
  34. package/dist/agents/run-store.js +71 -5
  35. package/dist/agents/skill-readiness.js +52 -10
  36. package/dist/agents/subagent-manager.js +209 -3
  37. package/dist/agents/task/task-runner.js +17 -4
  38. package/dist/agents/task-group.js +8 -51
  39. package/dist/agents/task-public-text.js +89 -0
  40. package/dist/agents/workflow-pivot-loop.js +19 -12
  41. package/dist/cli/clipboard.js +65 -0
  42. package/dist/cli/commands/index.js +1 -0
  43. package/dist/cli/commands/tasks.js +350 -2
  44. package/dist/cli/identity-command.js +177 -0
  45. package/dist/cli/ink-app.js +145 -17
  46. package/dist/cli/log-gate.js +386 -0
  47. package/dist/cli/mention-data.js +2 -0
  48. package/dist/cli/model-selector.js +538 -0
  49. package/dist/cli/reply-hygiene.js +54 -0
  50. package/dist/cli/setup-wizard.js +757 -69
  51. package/dist/cli/startup-notice.js +160 -0
  52. package/dist/cli/stores.js +22 -1
  53. package/dist/cli/theme.js +49 -0
  54. package/dist/cli/tui-select.js +970 -0
  55. package/dist/cli/update-commands.js +150 -24
  56. package/dist/cli-entry.js +137 -20
  57. package/dist/electron-build/agents/contacts/chain.js +26 -6
  58. package/dist/electron-build/agents/contacts/chain.js.map +1 -1
  59. package/dist/electron-build/agents/execution-supervisor.js +578 -38
  60. package/dist/electron-build/agents/execution-supervisor.js.map +1 -1
  61. package/dist/electron-build/agents/external-events.js +34 -5
  62. package/dist/electron-build/agents/external-events.js.map +1 -1
  63. package/dist/electron-build/agents/goal-criteria.js +10 -3
  64. package/dist/electron-build/agents/goal-criteria.js.map +1 -1
  65. package/dist/electron-build/agents/goal-flywheel/continuation-decision.js +598 -0
  66. package/dist/electron-build/agents/goal-flywheel/continuation-decision.js.map +1 -0
  67. package/dist/electron-build/agents/goal-flywheel/goal-change.js +773 -0
  68. package/dist/electron-build/agents/goal-flywheel/goal-change.js.map +1 -0
  69. package/dist/electron-build/agents/goal-flywheel/memory-layers.js +269 -0
  70. package/dist/electron-build/agents/goal-flywheel/memory-layers.js.map +1 -0
  71. package/dist/electron-build/agents/goal-flywheel/run-closure.js +818 -0
  72. package/dist/electron-build/agents/goal-flywheel/run-closure.js.map +1 -0
  73. package/dist/electron-build/agents/goal-flywheel/skill-candidate.js +642 -0
  74. package/dist/electron-build/agents/goal-flywheel/skill-candidate.js.map +1 -0
  75. package/dist/electron-build/agents/goal-flywheel/types.js +333 -0
  76. package/dist/electron-build/agents/goal-flywheel/types.js.map +1 -0
  77. package/dist/electron-build/agents/goal-flywheel/wiring/change.js +294 -0
  78. package/dist/electron-build/agents/goal-flywheel/wiring/change.js.map +1 -0
  79. package/dist/electron-build/agents/goal-flywheel/wiring/closure.js +742 -0
  80. package/dist/electron-build/agents/goal-flywheel/wiring/closure.js.map +1 -0
  81. package/dist/electron-build/agents/goal-flywheel/wiring/continuation.js +444 -0
  82. package/dist/electron-build/agents/goal-flywheel/wiring/continuation.js.map +1 -0
  83. package/dist/electron-build/agents/goal-flywheel/wiring/contract.js +279 -0
  84. package/dist/electron-build/agents/goal-flywheel/wiring/contract.js.map +1 -0
  85. package/dist/electron-build/agents/goal-flywheel/wiring/index.js +111 -0
  86. package/dist/electron-build/agents/goal-flywheel/wiring/index.js.map +1 -0
  87. package/dist/electron-build/agents/goal-flywheel/wiring/monitor.js +264 -0
  88. package/dist/electron-build/agents/goal-flywheel/wiring/monitor.js.map +1 -0
  89. package/dist/electron-build/agents/goal-flywheel/wiring/seams.js +612 -0
  90. package/dist/electron-build/agents/goal-flywheel/wiring/seams.js.map +1 -0
  91. package/dist/electron-build/agents/goal-flywheel/work-contract.js +488 -0
  92. package/dist/electron-build/agents/goal-flywheel/work-contract.js.map +1 -0
  93. package/dist/electron-build/agents/goal-flywheel/work-monitor.js +501 -0
  94. package/dist/electron-build/agents/goal-flywheel/work-monitor.js.map +1 -0
  95. package/dist/electron-build/agents/goal-flywheel-wiring.js +1962 -0
  96. package/dist/electron-build/agents/goal-flywheel-wiring.js.map +1 -0
  97. package/dist/electron-build/agents/goal-state-reducer.js +414 -0
  98. package/dist/electron-build/agents/goal-state-reducer.js.map +1 -0
  99. package/dist/electron-build/agents/goal-store.js +35 -1
  100. package/dist/electron-build/agents/goal-store.js.map +1 -1
  101. package/dist/electron-build/agents/model-policy.js +706 -0
  102. package/dist/electron-build/agents/model-policy.js.map +1 -0
  103. package/dist/electron-build/agents/network-pulse.js +307 -19
  104. package/dist/electron-build/agents/network-pulse.js.map +1 -1
  105. package/dist/electron-build/agents/pi-sdk-tools.js +84 -33
  106. package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
  107. package/dist/electron-build/agents/pi-sdk-types.js.map +1 -1
  108. package/dist/electron-build/agents/pi-sdk.js +156 -41
  109. package/dist/electron-build/agents/pi-sdk.js.map +1 -1
  110. package/dist/electron-build/agents/run-store.js +75 -6
  111. package/dist/electron-build/agents/run-store.js.map +1 -1
  112. package/dist/electron-build/agents/skill-readiness.js +52 -9
  113. package/dist/electron-build/agents/skill-readiness.js.map +1 -1
  114. package/dist/electron-build/agents/task/task-runner.js +16 -3
  115. package/dist/electron-build/agents/task/task-runner.js.map +1 -1
  116. package/dist/electron-build/agents/workflow-pivot-loop.js +19 -12
  117. package/dist/electron-build/agents/workflow-pivot-loop.js.map +1 -1
  118. package/dist/electron-build/llm/config-store.js +146 -6
  119. package/dist/electron-build/llm/config-store.js.map +1 -1
  120. package/dist/electron-build/llm/connection-probe.js +764 -0
  121. package/dist/electron-build/llm/connection-probe.js.map +1 -0
  122. package/dist/electron-build/llm/custom-provider-store.js +272 -0
  123. package/dist/electron-build/llm/custom-provider-store.js.map +1 -0
  124. package/dist/electron-build/llm/data/provider-catalog-baked.js +23 -0
  125. package/dist/electron-build/llm/data/provider-catalog-baked.js.map +1 -0
  126. package/dist/electron-build/llm/model-catalog.js +778 -0
  127. package/dist/electron-build/llm/model-catalog.js.map +1 -0
  128. package/dist/electron-build/llm/model-discovery.js +1202 -0
  129. package/dist/electron-build/llm/model-discovery.js.map +1 -0
  130. package/dist/electron-build/llm/model-selection.js +1315 -0
  131. package/dist/electron-build/llm/model-selection.js.map +1 -0
  132. package/dist/electron-build/llm/pi-ai.js +70 -11
  133. package/dist/electron-build/llm/pi-ai.js.map +1 -1
  134. package/dist/electron-build/llm/provider-catalog.js +969 -0
  135. package/dist/electron-build/llm/provider-catalog.js.map +1 -0
  136. package/dist/electron-build/llm/provider-registry.js +950 -0
  137. package/dist/electron-build/llm/provider-registry.js.map +1 -0
  138. package/dist/electron-build/llm/tool-name.js +266 -0
  139. package/dist/electron-build/llm/tool-name.js.map +1 -0
  140. package/dist/electron-build/orbitdb/cid-database.js +83 -17
  141. package/dist/electron-build/orbitdb/cid-database.js.map +1 -1
  142. package/dist/electron-build/orbitdb/ipfs-node.js +31 -2
  143. package/dist/electron-build/orbitdb/ipfs-node.js.map +1 -1
  144. package/dist/electron-build/utils/dual-source-facts.js +236 -0
  145. package/dist/electron-build/utils/dual-source-facts.js.map +1 -0
  146. package/dist/electron-build/utils/dual-source.js +347 -0
  147. package/dist/electron-build/utils/dual-source.js.map +1 -0
  148. package/dist/electron-build/utils/update-manager.js +328 -104
  149. package/dist/electron-build/utils/update-manager.js.map +1 -1
  150. package/dist/electron-build/utils/update-state.js +9 -11
  151. package/dist/electron-build/utils/update-state.js.map +1 -1
  152. package/dist/electron-build/utils/version-identity.js +193 -0
  153. package/dist/electron-build/utils/version-identity.js.map +1 -0
  154. package/dist/electron-build/utils/version-info.js +57 -31
  155. package/dist/electron-build/utils/version-info.js.map +1 -1
  156. package/dist/index.js +385 -108
  157. package/dist/ios/bolloon-web.json +10 -0
  158. package/dist/ios/chain-explorer.js +617 -0
  159. package/dist/ios/explorer.html +163 -0
  160. package/dist/ios/index.html +96 -0
  161. package/dist/ios/mobile-agent.js +2 -2
  162. package/dist/ios/mobile-contacts.js +362 -0
  163. package/dist/ios/mobile-core.js +3102 -310
  164. package/dist/ios/mobile-privacy.js +185 -0
  165. package/dist/ios/mobile-tasks.js +339 -0
  166. package/dist/ios/mobile-update.js +1442 -0
  167. package/dist/ios/mobile.css +19 -0
  168. package/dist/ios/mobile.html +96 -0
  169. package/dist/ios/mobile.js +1013 -41
  170. package/dist/ios/routes-chain-index.js +108 -0
  171. package/dist/ios/routes-contacts.js +420 -0
  172. package/dist/ios/routes-llm-config.js +344 -52
  173. package/dist/ios/routes-mobile-tasks.js +353 -0
  174. package/dist/ios/routes-onchain-trade.js +99 -0
  175. package/dist/ios/routes-x402-info.js +1 -0
  176. package/dist/ios/server.js +921 -0
  177. package/dist/ios/sw.js +1 -1
  178. package/dist/ios/task-frame-server.js +211 -0
  179. package/dist/llm/config-store.js +146 -6
  180. package/dist/llm/connection-probe.js +744 -0
  181. package/dist/llm/custom-provider-store.js +224 -0
  182. package/dist/llm/data/provider-catalog-baked.js +19 -0
  183. package/dist/llm/model-catalog.js +717 -0
  184. package/dist/llm/model-discovery.js +1137 -0
  185. package/dist/llm/model-selection.js +1241 -0
  186. package/dist/llm/pi-ai.js +70 -11
  187. package/dist/llm/provider-catalog.js +898 -0
  188. package/dist/llm/provider-registry.js +917 -0
  189. package/dist/llm/tool-name.js +220 -0
  190. package/dist/orbitdb/cid-database.js +81 -16
  191. package/dist/orbitdb/ipfs-node.js +31 -2
  192. package/dist/setup/onboard.js +34 -8
  193. package/dist/utils/dual-source-facts.js +223 -0
  194. package/dist/utils/dual-source.js +297 -0
  195. package/dist/utils/update-cli.js +16 -21
  196. package/dist/utils/update-health.js +35 -5
  197. package/dist/utils/update-manager.js +325 -96
  198. package/dist/utils/update-state.js +13 -10
  199. package/dist/utils/version-identity.js +176 -0
  200. package/dist/utils/version-info.js +27 -22
  201. package/dist/web/bolloon-web.json +10 -0
  202. package/dist/web/mobile-contacts.js +44 -11
  203. package/dist/web/mobile-core.js +2901 -580
  204. package/dist/web/mobile-tasks.js +339 -0
  205. package/dist/web/mobile-update.js +1442 -0
  206. package/dist/web/mobile.css +19 -0
  207. package/dist/web/mobile.html +64 -0
  208. package/dist/web/mobile.js +838 -35
  209. package/dist/web/routes-llm-config.js +344 -52
  210. package/dist/web/routes-mobile-tasks.js +353 -0
  211. package/dist/web/server.js +163 -4
  212. package/dist/web/sw.js +1 -1
  213. package/package.json +3 -1
  214. package/scripts/build-web.ts +36 -0
@@ -17,17 +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
- /** 一次"检查"的结论。7 个值就是全部, 不认识的一律落进 registry_unavailable 并带原因。 */
22
- export const CHECK_STATUSES = [
23
- 'up_to_date', // 确认最新
24
- 'update_available', // 有新版本
25
- 'check_skipped', // 节流/显式跳过, 结论来自缓存
26
- 'offline', // 网络不可达 (≠ 最新)
27
- 'registry_unavailable', // 可达但拿不到有效数据 (5xx / 包不存在 / 无 dist-tags)
28
- 'local_version_unknown', // 读不到本地版本 (不猜 0.0.0)
29
- 'unsupported_installation', // 认得出本地版本, 但这种安装方式不支持自动更新
30
- ];
22
+ //
23
+ // 2026-09-26: 结论枚举与 UpdatePrefs 的结构定义抽到 `version-identity.ts` (纯模块, 无 node:)。
24
+ // **只有一个原因**: 手机端 WebView 里没有 `fs`/`os`, 但手机端必须用**同一份**结论语义
25
+ // (9 个结论 + REFUSED_STATUSES 的拒绝口径)。这里原样再导出, 既有 import 路径不变。
26
+ export { CHECK_STATUSES };
31
27
  /** 一次"更新执行"的流水线状态。 */
32
28
  export const UPDATE_RUN_STATUSES = [
33
29
  'planned', 'downloading', 'staged', 'switching', 'verifying',
@@ -72,6 +68,13 @@ export function emptyUpdateState(now = new Date()) {
72
68
  autoRestart: false,
73
69
  needsRestart: false,
74
70
  updatedAt: now.toISOString(),
71
+ installedChannel: null,
72
+ installedDevSha: null,
73
+ devSha: null,
74
+ devRef: null,
75
+ devCheckedAt: null,
76
+ sourceFacts: null,
77
+ switchableTo: null,
75
78
  };
76
79
  }
77
80
  // ── 读写 (原子) ─────────────────────────────────────────────────────────────
@@ -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,27 +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
- }
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';
30
+ /**
31
+ * 版本身份 / 通道 / 结论分类的**纯词汇表**已抽到 `version-identity.ts` (2026-09-26),
32
+ * 这里原样再导出 —— 既有 import 路径 (`from './version-info.js'`) 一行都不用改,
33
+ * 手机端 (无 node:) 直接 import 那份纯模块即可, 两边共用同一套语义。
34
+ */
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, };
50
36
  // ── 包根定位 ────────────────────────────────────────────────────────────────
51
37
  /**
52
38
  * 从当前模块 URL 定位**运行中的**包根。
@@ -385,6 +371,11 @@ export function collectVersionInfo(opts = {}) {
385
371
  }
386
372
  if (!gitCommit)
387
373
  gitCommitSource = 'unknown';
374
+ // dev 身份从**磁盘上的版本号**读 (不是从状态文件猜): `0.4.33+dev.a1b2c3d` → sha=a1b2c3d
375
+ const installedDevSha = devShaFromIdentity(pkg?.version) || (opts.update?.installedDevSha ?? null);
376
+ const switchableTo = opts.update?.switchableTo ?? (installedDevSha
377
+ ? { channel: 'stable', source: 'npm', target: null }
378
+ : null);
388
379
  return {
389
380
  schema: 'bolloon-version/1',
390
381
  packageName: pkg?.name || PKG_NAME,
@@ -406,11 +397,14 @@ export function collectVersionInfo(opts = {}) {
406
397
  binPath: install.binPath,
407
398
  configDir: home,
408
399
  channel,
400
+ channelKind: channelKindOf(channel),
409
401
  upstream: UPSTREAM_REPO,
410
402
  registry: `${NPM_REGISTRY_BASE}/${PKG_NAME}`,
411
403
  updateSource: install.updateSource,
412
404
  autoUpdatable: install.autoUpdatable,
413
405
  installReason: install.reason,
406
+ installedDevSha,
407
+ switchableTo,
414
408
  update: opts.update ?? null,
415
409
  runtime: opts.runtime ?? null,
416
410
  runtimeConfigBacked: opts.runtimeConfigBacked ?? false,
@@ -444,9 +438,15 @@ export function describeUpdateLine(info) {
444
438
  case 'registry_unavailable': return `更新检查: registry 不可用${u.lastCheckReason ? ` (${u.lastCheckReason})` : ''} (不等于最新)`;
445
439
  case 'local_version_unknown': return '更新检查: 读不到本地版本 — 不判断是否有更新';
446
440
  case 'unsupported_installation': return '更新检查: 当前安装方式不支持自动更新';
441
+ case 'github_unavailable': return `更新检查: GitHub 源不可用${u.lastCheckReason ? ` (${u.lastCheckReason})` : ''} (不等于最新)`;
442
+ case 'cross_check_mismatch': return `更新检查: 两个源不一致 (npm ↔ GitHub) — 不执行更新`;
447
443
  default: return `更新检查: ${u.lastCheckStatus}`;
448
444
  }
449
445
  }
446
+ /**
447
+ * 双源身份那一行 (谁装的 / 哪个 sha / 能切回哪) —— 已在 `version-identity.ts` (唯一一份,
448
+ * 桌面 `--version` 与 `update status` 共用同一份措辞)。这里只留注释, 避免出现第二份实现。
449
+ */
450
450
  export function renderVersionText(info, opts = {}) {
451
451
  if (opts.verbose) {
452
452
  const lines = [
@@ -464,10 +464,12 @@ export function renderVersionText(info, opts = {}) {
464
464
  `运行入口: ${info.entryPath}`,
465
465
  `bin shim: ${info.binPath || 'unknown'}`,
466
466
  `配置目录: ${info.configDir}`,
467
- `更新通道: ${info.channel}`,
467
+ `更新通道: ${info.channel} (比较语义: ${info.channelKind})`,
468
468
  `更新来源: ${info.updateSource}${info.autoUpdatable ? '' : ' (不支持自动更新)'}`,
469
+ describeSourceIdentity(info),
469
470
  `上游地址: ${info.upstream}`,
470
471
  `registry: ${info.registry}`,
472
+ `GitHub: ${GITHUB_UPSTREAM_SLUG} (${DEV_BRANCH} HEAD = dev 通道的源)`,
471
473
  describeUpdateLine(info),
472
474
  `上次检查: ${fmtTime(info.update?.lastCheckAt)}`,
473
475
  ];
@@ -493,12 +495,15 @@ export function renderVersionText(info, opts = {}) {
493
495
  `安装目录: ${info.installDir}`,
494
496
  `运行入口: ${info.binPath || info.entryPath}`,
495
497
  `更新通道: ${info.channel}`,
498
+ describeSourceIdentity(info),
496
499
  `Node.js: ${info.nodeVersion}`,
497
500
  `平台: ${info.platform} ${info.arch}`,
498
501
  `上游提交: ${info.gitCommit || 'unknown'}`,
499
502
  describeUpdateLine(info),
500
503
  `上次检查: ${fmtTime(info.update?.lastCheckAt)}`,
501
504
  ];
505
+ if (info.installedDevSha)
506
+ lines.push(DEV_CHANNEL_WARNING, DEV_BACK_TO_STABLE_HINT);
502
507
  if (info.runtime) {
503
508
  lines.push('');
504
509
  lines.push(...renderRuntimeLines(info.runtime, { verbose: false, configBacked: info.runtimeConfigBacked }));
@@ -0,0 +1,10 @@
1
+ {
2
+ "schema": "bolloon-mobile-web/1",
3
+ "version": "0.5.1",
4
+ "channel": "stable",
5
+ "build": "stable",
6
+ "commit": "8efb696025ea635cc4f0c907c275ea41c1ecf7a1",
7
+ "builtAt": "2026-09-27T15:24:38.506Z",
8
+ "layer": "web",
9
+ "note": "web 资源层构建戳 (手机端 OTA 的身份第 ③ 级回退; 装机后以 OTA state.json 为准)"
10
+ }
@@ -89,21 +89,54 @@ async function importPrivate(storage, c) {
89
89
  }
90
90
  /** 用设备私钥签一条 Grant (核心: 手机确认 → 桌面才认) */
91
91
  export async function signGrantOnDevice(grant, storage, c) {
92
- const cobj = cryptoOf(c);
93
- const key = await importPrivate(storage, cobj);
94
- const payload = canonicalGrantPayload(grant);
95
- const sig = await cobj.subtle.sign({ name: 'Ed25519' }, key, bytesOf(payload));
96
- const dev = await loadOrCreateDeviceKey(storage, cobj);
97
- return { deviceId: dev.deviceId, alg: 'ed25519', payloadHash: await sha256Hex(cobj, payload), sig: b64(sig) };
92
+ return signPayloadOnDevice(canonicalGrantPayload(grant), storage, c);
98
93
  }
99
94
  /** 撤销也要签名 (桌面拒收未登记设备的撤销) */
100
95
  export async function signRevocationOnDevice(rev, storage, c) {
96
+ return signPayloadOnDevice(canonicalRevocationPayload(rev), storage, c);
97
+ }
98
+ /**
99
+ * 通用: 用**同一把设备私钥**签任意规范载荷 (2026-09-25)。
100
+ *
101
+ * 抽出来的原因: 手机上所有"要桌面认账"的动作 (长期授权 · 撤销 · 高风险任务动作
102
+ * 入群/发公告/过程留痕) 必须是**同一把设备密钥 + 同一套验签纪律** ——
103
+ * 各写一份签名实现迟早出现"某条路径没签名也能过"的缝。
104
+ * 载荷的规范化由调用方提供 (grant-payload / mobile-task-actions), 本函数只管签。
105
+ */
106
+ export async function signPayloadOnDevice(payload, storage, c) {
107
+ const cobj = cryptoOf(c);
108
+ if (!cobj?.subtle)
109
+ throw new Error('device_signing_unavailable: 本机没有 WebCrypto');
110
+ // 只加载一次设备材料: deviceId 与签名必须来自**同一份**密钥
111
+ // (2026-09-25 修: 之前在这里二次 loadOrCreateDeviceKey, storage 不持久化时会生成另一把密钥
112
+ // → 桌面 device_mismatch 403。真机 localStorage 下看不出来, 但那是运气不是设计。)
113
+ const material = await loadOrCreateDeviceKey(storage, cobj);
114
+ const key = await cobj.subtle.importKey('jwk', material.privateKeyJwk, { name: 'Ed25519' }, false, ['sign']);
115
+ const bytes = bytesOf(String(payload ?? ''));
116
+ const sig = await cobj.subtle.sign({ name: 'Ed25519' }, key, bytes);
117
+ return { deviceId: material.deviceId, alg: 'ed25519', payloadHash: await sha256Hex(cobj, String(payload ?? '')), sig: b64(sig) };
118
+ }
119
+ /** sha256 hex (手机侧与桌面 Node 同值) —— 任务动作的内容摘要用它算 */
120
+ export async function sha256HexOf(text, c) {
101
121
  const cobj = cryptoOf(c);
102
- const key = await importPrivate(storage, cobj);
103
- const payload = canonicalRevocationPayload(rev);
104
- const sig = await cobj.subtle.sign({ name: 'Ed25519' }, key, bytesOf(payload));
105
- const dev = await loadOrCreateDeviceKey(storage, cobj);
106
- return { deviceId: dev.deviceId, alg: 'ed25519', payloadHash: await sha256Hex(cobj, payload), sig: b64(sig) };
122
+ if (!cobj?.subtle)
123
+ throw new Error('device_signing_unavailable: 本机没有 WebCrypto');
124
+ return sha256Hex(cobj, String(text ?? ''));
125
+ }
126
+ /**
127
+ * 用**已经加载好的**设备材料签名 (调用方自己拿 deviceId)。
128
+ *
129
+ * 为什么留这个口: 高风险任务动作的载荷里含 deviceId, 签名必须与那个 deviceId 同源。
130
+ * 若"取 deviceId"与"签名"各调一次 loadOrCreateDeviceKey, 在 storage 不持久化的环境
131
+ * (隐私模式 / storage 被禁) 会拿到两把不同密钥 → 桌面 device_mismatch 403。
132
+ */
133
+ export async function signPayloadWithMaterial(payload, material, c) {
134
+ const cobj = cryptoOf(c);
135
+ if (!cobj?.subtle)
136
+ throw new Error('device_signing_unavailable: 本机没有 WebCrypto');
137
+ const key = await cobj.subtle.importKey('jwk', material.privateKeyJwk, { name: 'Ed25519' }, false, ['sign']);
138
+ const sig = await cobj.subtle.sign({ name: 'Ed25519' }, key, bytesOf(String(payload ?? '')));
139
+ return { deviceId: material.deviceId, alg: 'ed25519', payloadHash: await sha256Hex(cobj, String(payload ?? '')), sig: b64(sig) };
107
140
  }
108
141
  async function post(base, path, body, f) {
109
142
  const res = await f(`${String(base).replace(/\/+$/, '')}${path}`, {