@bolloon/bolloon-agent 0.4.32 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/dist/agents/chain/explorer.js +52 -0
  2. package/dist/agents/chain/index.js +2 -0
  3. package/dist/agents/chain/onchain-trade.js +15 -1
  4. package/dist/agents/contacts/chain.js +27 -7
  5. package/dist/agents/execution-supervisor.js +493 -35
  6. package/dist/agents/external-events.js +35 -6
  7. package/dist/agents/gateway-group.js +100 -12
  8. package/dist/agents/goal-criteria.js +10 -3
  9. package/dist/agents/goal-flywheel/block-executor.js +346 -0
  10. package/dist/agents/goal-flywheel/continuation-decision.js +592 -0
  11. package/dist/agents/goal-flywheel/goal-change.js +754 -0
  12. package/dist/agents/goal-flywheel/index.js +26 -0
  13. package/dist/agents/goal-flywheel/memory-layers.js +227 -0
  14. package/dist/agents/goal-flywheel/run-closure.js +809 -0
  15. package/dist/agents/goal-flywheel/skill-candidate.js +628 -0
  16. package/dist/agents/goal-flywheel/types.js +329 -0
  17. package/dist/agents/goal-flywheel/wiring/change.js +288 -0
  18. package/dist/agents/goal-flywheel/wiring/closure.js +697 -0
  19. package/dist/agents/goal-flywheel/wiring/continuation.js +432 -0
  20. package/dist/agents/goal-flywheel/wiring/contract.js +270 -0
  21. package/dist/agents/goal-flywheel/wiring/index.js +26 -0
  22. package/dist/agents/goal-flywheel/wiring/monitor.js +254 -0
  23. package/dist/agents/goal-flywheel/wiring/seams.js +592 -0
  24. package/dist/agents/goal-flywheel/work-contract.js +478 -0
  25. package/dist/agents/goal-flywheel/work-monitor.js +488 -0
  26. package/dist/agents/goal-flywheel-wiring.js +1870 -0
  27. package/dist/agents/goal-state-reducer.js +406 -0
  28. package/dist/agents/goal-store.js +35 -1
  29. package/dist/agents/mobile-flywheel-view.js +144 -0
  30. package/dist/agents/mobile-task-actions-verify.js +74 -0
  31. package/dist/agents/mobile-task-actions.js +178 -0
  32. package/dist/agents/mobile-task-views.js +199 -0
  33. package/dist/agents/network-pulse.js +525 -27
  34. package/dist/agents/pi-sdk.js +75 -20
  35. package/dist/agents/run-store.js +18 -5
  36. package/dist/agents/skill-readiness.js +52 -10
  37. package/dist/agents/subagent-manager.js +209 -3
  38. package/dist/agents/task/task-onchain-runner.js +19 -1
  39. package/dist/agents/task/task-runner.js +36 -6
  40. package/dist/agents/task-board.js +702 -0
  41. package/dist/agents/task-group.js +524 -0
  42. package/dist/agents/task-public-text.js +89 -0
  43. package/dist/cli/commands/index.js +3 -0
  44. package/dist/cli/commands/tasks.js +977 -8
  45. package/dist/cli/identity-command.js +177 -0
  46. package/dist/cli/protocol-envelope.js +4 -0
  47. package/dist/cli/setup-wizard.js +71 -0
  48. package/dist/cli/update-commands.js +150 -24
  49. package/dist/cli-entry.js +12 -2
  50. package/dist/electron-build/agents/chain/explorer.js +59 -0
  51. package/dist/electron-build/agents/chain/explorer.js.map +1 -0
  52. package/dist/electron-build/agents/contacts/chain.js +26 -6
  53. package/dist/electron-build/agents/contacts/chain.js.map +1 -1
  54. package/dist/electron-build/agents/execution-supervisor.js +492 -34
  55. package/dist/electron-build/agents/execution-supervisor.js.map +1 -1
  56. package/dist/electron-build/agents/external-events.js +34 -5
  57. package/dist/electron-build/agents/external-events.js.map +1 -1
  58. package/dist/electron-build/agents/goal-criteria.js +10 -3
  59. package/dist/electron-build/agents/goal-criteria.js.map +1 -1
  60. package/dist/electron-build/agents/goal-flywheel/continuation-decision.js +598 -0
  61. package/dist/electron-build/agents/goal-flywheel/continuation-decision.js.map +1 -0
  62. package/dist/electron-build/agents/goal-flywheel/goal-change.js +773 -0
  63. package/dist/electron-build/agents/goal-flywheel/goal-change.js.map +1 -0
  64. package/dist/electron-build/agents/goal-flywheel/memory-layers.js +269 -0
  65. package/dist/electron-build/agents/goal-flywheel/memory-layers.js.map +1 -0
  66. package/dist/electron-build/agents/goal-flywheel/run-closure.js +818 -0
  67. package/dist/electron-build/agents/goal-flywheel/run-closure.js.map +1 -0
  68. package/dist/electron-build/agents/goal-flywheel/skill-candidate.js +642 -0
  69. package/dist/electron-build/agents/goal-flywheel/skill-candidate.js.map +1 -0
  70. package/dist/electron-build/agents/goal-flywheel/types.js +333 -0
  71. package/dist/electron-build/agents/goal-flywheel/types.js.map +1 -0
  72. package/dist/electron-build/agents/goal-flywheel/wiring/change.js +294 -0
  73. package/dist/electron-build/agents/goal-flywheel/wiring/change.js.map +1 -0
  74. package/dist/electron-build/agents/goal-flywheel/wiring/closure.js +742 -0
  75. package/dist/electron-build/agents/goal-flywheel/wiring/closure.js.map +1 -0
  76. package/dist/electron-build/agents/goal-flywheel/wiring/continuation.js +444 -0
  77. package/dist/electron-build/agents/goal-flywheel/wiring/continuation.js.map +1 -0
  78. package/dist/electron-build/agents/goal-flywheel/wiring/contract.js +279 -0
  79. package/dist/electron-build/agents/goal-flywheel/wiring/contract.js.map +1 -0
  80. package/dist/electron-build/agents/goal-flywheel/wiring/index.js +111 -0
  81. package/dist/electron-build/agents/goal-flywheel/wiring/index.js.map +1 -0
  82. package/dist/electron-build/agents/goal-flywheel/wiring/monitor.js +264 -0
  83. package/dist/electron-build/agents/goal-flywheel/wiring/monitor.js.map +1 -0
  84. package/dist/electron-build/agents/goal-flywheel/wiring/seams.js +612 -0
  85. package/dist/electron-build/agents/goal-flywheel/wiring/seams.js.map +1 -0
  86. package/dist/electron-build/agents/goal-flywheel/work-contract.js +488 -0
  87. package/dist/electron-build/agents/goal-flywheel/work-contract.js.map +1 -0
  88. package/dist/electron-build/agents/goal-flywheel/work-monitor.js +501 -0
  89. package/dist/electron-build/agents/goal-flywheel/work-monitor.js.map +1 -0
  90. package/dist/electron-build/agents/goal-flywheel-wiring.js +1962 -0
  91. package/dist/electron-build/agents/goal-flywheel-wiring.js.map +1 -0
  92. package/dist/electron-build/agents/goal-state-reducer.js +414 -0
  93. package/dist/electron-build/agents/goal-state-reducer.js.map +1 -0
  94. package/dist/electron-build/agents/goal-store.js +35 -1
  95. package/dist/electron-build/agents/goal-store.js.map +1 -1
  96. package/dist/electron-build/agents/local-signer.js +101 -0
  97. package/dist/electron-build/agents/local-signer.js.map +1 -0
  98. package/dist/electron-build/agents/network-pulse.js +531 -28
  99. package/dist/electron-build/agents/network-pulse.js.map +1 -1
  100. package/dist/electron-build/agents/pi-sdk.js +74 -19
  101. package/dist/electron-build/agents/pi-sdk.js.map +1 -1
  102. package/dist/electron-build/agents/run-store.js +20 -5
  103. package/dist/electron-build/agents/run-store.js.map +1 -1
  104. package/dist/electron-build/agents/skill-readiness.js +52 -9
  105. package/dist/electron-build/agents/skill-readiness.js.map +1 -1
  106. package/dist/electron-build/agents/task/task-runner.js +35 -5
  107. package/dist/electron-build/agents/task/task-runner.js.map +1 -1
  108. package/dist/electron-build/agents/task-board.js +761 -0
  109. package/dist/electron-build/agents/task-board.js.map +1 -0
  110. package/dist/electron-build/agents/task-contract.js +382 -0
  111. package/dist/electron-build/agents/task-contract.js.map +1 -0
  112. package/dist/electron-build/orbitdb/cid-database.js +83 -17
  113. package/dist/electron-build/orbitdb/cid-database.js.map +1 -1
  114. package/dist/electron-build/orbitdb/ipfs-node.js +31 -2
  115. package/dist/electron-build/orbitdb/ipfs-node.js.map +1 -1
  116. package/dist/electron-build/utils/dual-source.js +471 -0
  117. package/dist/electron-build/utils/dual-source.js.map +1 -0
  118. package/dist/electron-build/utils/update-manager.js +331 -57
  119. package/dist/electron-build/utils/update-manager.js.map +1 -1
  120. package/dist/electron-build/utils/update-state.js +16 -1
  121. package/dist/electron-build/utils/update-state.js.map +1 -1
  122. package/dist/electron-build/utils/version-info.js +82 -2
  123. package/dist/electron-build/utils/version-info.js.map +1 -1
  124. package/dist/index.js +103 -12
  125. package/dist/ios/chain-explorer.js +617 -0
  126. package/dist/ios/explorer.html +163 -0
  127. package/dist/ios/index.html +96 -0
  128. package/dist/ios/mobile-agent.js +2 -2
  129. package/dist/ios/mobile-contacts.js +329 -0
  130. package/dist/ios/mobile-core.js +1379 -226
  131. package/dist/ios/mobile-privacy.js +185 -0
  132. package/dist/ios/mobile.css +19 -0
  133. package/dist/ios/mobile.html +96 -0
  134. package/dist/ios/mobile.js +706 -7
  135. package/dist/ios/routes-chain-index.js +108 -0
  136. package/dist/ios/routes-contacts.js +420 -0
  137. package/dist/ios/routes-onchain-trade.js +99 -0
  138. package/dist/ios/routes-x402-info.js +1 -0
  139. package/dist/ios/server.js +762 -0
  140. package/dist/ios/task-frame-server.js +211 -0
  141. package/dist/orbitdb/cid-database.js +81 -16
  142. package/dist/orbitdb/ipfs-node.js +31 -2
  143. package/dist/utils/dual-source.js +426 -0
  144. package/dist/utils/update-cli.js +16 -21
  145. package/dist/utils/update-health.js +35 -5
  146. package/dist/utils/update-manager.js +329 -57
  147. package/dist/utils/update-state.js +16 -1
  148. package/dist/utils/version-info.js +75 -1
  149. package/dist/web/mobile-contacts.js +44 -11
  150. package/dist/web/mobile-core.js +1197 -513
  151. package/dist/web/mobile-tasks.js +339 -0
  152. package/dist/web/mobile.css +19 -0
  153. package/dist/web/mobile.html +64 -0
  154. package/dist/web/mobile.js +531 -1
  155. package/dist/web/routes-mobile-tasks.js +353 -0
  156. package/dist/web/server.js +163 -4
  157. package/package.json +3 -1
@@ -0,0 +1,177 @@
1
+ /**
2
+ * identity-command.ts — `bolloon identity init|show` (2026-09-24)
3
+ *
4
+ * 补的真缺口: 建本机身份以前**只有** `bolloon setup` —— 一个 readline 交互向导。
5
+ * 没有 TTY 的环境 (CI / 容器 / ssh 管道 / 第二实例的自动化) 跑它会
6
+ * `readline was closed` (ERR_USE_AFTER_CLOSE), 于是"新机器/第二个身份"根本建不出来,
7
+ * 而群/任务/签名这条链全靠 `~/.bolloon/identity.json`。
8
+ *
9
+ * 本模块只做薄包装 (不重实现任何密钥学):
10
+ * · `init` — 幂等建 `~/.bolloon/identity.json` (复用 `setup-wizard.initLocalIdentity`,
11
+ * 它内部走 `KeyManager.generate/saveToFile`, 与 `src/index.ts:bootstrapIdentity` 同一条路径);
12
+ * 已存在 → **一个字不改** + `action:'reused'` + exit 0; 损坏 → **拒绝覆盖** (要 --force)。
13
+ * · `show` — 只出 did / 公钥指纹 / 文件权限; **绝不打印私钥**。
14
+ *
15
+ * 红线: 本模块任何路径都不把 `privateKey` 写进 stdout / data / human。
16
+ * 兜底还有 `finalizeEnvelope` 的 `redactSecrets` (AUDIT_FORBIDDEN_KEYS 含 `privateKey`)。
17
+ */
18
+ import * as fs from 'fs/promises';
19
+ import * as os from 'os';
20
+ import * as crypto from 'crypto';
21
+ import { okEnvelope, failEnvelope, line, title, hint, plain, has, } from './protocol-envelope.js';
22
+ import { getLocalIdentityFile, initLocalIdentity } from './setup-wizard.js';
23
+ import { AUDIT_FORBIDDEN_KEYS } from '../agents/task-contract.js';
24
+ export const IDENTITY_USAGE = `
25
+ ${title('bolloon identity')}
26
+ bolloon identity init [--force] [--json]
27
+ 非交互建本机身份 → ~/.bolloon/identity.json (文件模式 0600)
28
+ 字段与既有完全一致: keyType='Ed25519' · privateKey · publicKey · did · createdAt · version
29
+ 幂等: 已存在 → 一个字不改 (action=reused), 仍然 exit 0
30
+ 损坏 (读不出 did) → 拒绝覆盖 (exit 1); 确认重来加 --force (会先备份成 .bak-<时间戳>)
31
+ **绝不打印私钥** (输出只有 did / 文件路径 / 文件权限)
32
+ bolloon identity show [--json]
33
+ 看本机身份: did · 公钥指纹(sha256 前 16 位) · 文件权限 · 创建时间 (不含私钥)
34
+
35
+ 选项: --json · --quiet · --force (只对 init 有效)
36
+ 说明: 只建 identity.json; 供应商/API key 仍走 bolloon setup (或 bolloon setup --provider … --api-key …);
37
+ 用户称呼/归属身份在 ~/.bolloon/identity/user.json (bolloon setup --name 写它)
38
+ `;
39
+ /** 公钥指纹 (sha256 前 16 位 hex) —— 不可逆, 不含密钥材料 */
40
+ function fingerprint(pub) {
41
+ return `sha256:${crypto.createHash('sha256').update(String(pub || '')).digest('hex').slice(0, 16)}`;
42
+ }
43
+ /** 读现有身份文件 (只取**非私密**字段; privateKey 连读都不读出来) */
44
+ async function readIdentityPublicFacts(home) {
45
+ const file = getLocalIdentityFile(home);
46
+ try {
47
+ const j = JSON.parse(await fs.readFile(file, 'utf-8'));
48
+ const pub = typeof j?.publicKey === 'string' ? j.publicKey : null;
49
+ return {
50
+ exists: true,
51
+ did: typeof j?.did === 'string' && j.did ? j.did : null,
52
+ keyType: typeof j?.keyType === 'string' ? j.keyType : null,
53
+ createdAt: typeof j?.createdAt === 'string' ? j.createdAt : null,
54
+ version: typeof j?.version === 'string' ? j.version : null,
55
+ fingerprint: pub ? fingerprint(pub) : null,
56
+ // 只报**非私密**字段名 (schema 一致性的证据落在测试里: 那边直接读文件比对 6 个键)。
57
+ // 私钥类字段名 (AUDIT_FORBIDDEN_KEYS: privateKey/…) 连名字都不出现在输出里 ——
58
+ // 免得"输出里有个 privateKey 字样"被当成可疑, 也让 `grep privateKey` 这种粗检查能直接过。
59
+ keys: Object.keys(j ?? {}).filter((k) => !AUDIT_FORBIDDEN_KEYS.includes(k)).sort(),
60
+ };
61
+ }
62
+ catch {
63
+ return { exists: false };
64
+ }
65
+ }
66
+ /** `bolloon identity init` —— 幂等建本机身份 */
67
+ async function identityInit(flags) {
68
+ const head = 'bolloon identity init';
69
+ const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
70
+ const force = has(flags, '--force');
71
+ let r;
72
+ try {
73
+ r = await initLocalIdentity(home, { force });
74
+ }
75
+ catch (e) {
76
+ return {
77
+ envelope: failEnvelope('INTERNAL_ERROR', `建身份失败: ${String(e?.message || e).slice(0, 200)}`, { file: getLocalIdentityFile(home), created: false }, [], 'needs_human'),
78
+ human: `${title(head)}\n 建身份失败: ${String(e?.message || e).slice(0, 200)}`,
79
+ };
80
+ }
81
+ if (!r.ok) {
82
+ // 拒绝覆盖 (损坏 / force 下备份失败): 如实失败, 不静默重建
83
+ return {
84
+ envelope: failEnvelope('POLICY_DENIED', r.reason || '拒绝覆盖既有身份文件', { file: r.file, action: r.action, created: false, overwritten: false }, [], 'needs_human'),
85
+ human: `${title(head)}\n ${r.reason || '拒绝覆盖'}\n\n${hint('静默盖掉一个可能还能救的身份 = 丢钥匙: 本命令宁可失败')}`,
86
+ };
87
+ }
88
+ const reused = r.action === 'reused';
89
+ const facts = await readIdentityPublicFacts(home);
90
+ const payload = {
91
+ action: r.action,
92
+ did: r.did ?? null,
93
+ file: r.file,
94
+ mode: r.mode ?? null,
95
+ created: !reused,
96
+ changed: false,
97
+ keyMaterialInOutput: false,
98
+ publicFields: facts.exists ? facts.keys : null,
99
+ fingerprint: facts.exists ? facts.fingerprint : null,
100
+ keyType: facts.exists ? facts.keyType : null,
101
+ createdAt: facts.exists ? facts.createdAt : null,
102
+ version: facts.exists ? facts.version : null,
103
+ nextSteps: reused
104
+ ? ['身份已存在, 未改动 (幂等); 直接用即可']
105
+ : ['跑任务/建群/签名现在有身份可用: bolloon task group create --name <群名>', '要配模型供应商/API key: bolloon setup'],
106
+ };
107
+ return {
108
+ envelope: okEnvelope('OK', reused
109
+ ? `本机身份已存在, 未改动 (${String(r.did).slice(0, 24)}…)`
110
+ : `已建本机身份 (${String(r.did).slice(0, 24)}…), 文件模式 ${r.mode}`, payload, [], null),
111
+ human: [
112
+ title(head),
113
+ line('动作', reused ? 'reused (已存在, 一个字没改 — 幂等)' : 'created (新建)'),
114
+ line('DID', r.did ?? '(无)'),
115
+ line('身份文件', r.file),
116
+ line('文件模式', r.mode ?? '(读不到)'),
117
+ line('字段', facts.exists ? facts.keys.join(' · ') : '(无)'),
118
+ line('公钥指纹', facts.exists ? facts.fingerprint : '(无)'),
119
+ '',
120
+ ` ${hint(reused ? '幂等: 第二个身份请换一个 HOME (HOME=/tmp/x bolloon identity init)' : '下一步')}`,
121
+ ...(reused
122
+ ? [' 要用第二个身份: `HOME=/tmp/second bolloon identity init` (每个 HOME 一对自己的密钥)']
123
+ : [' 建群: bolloon task group create --name <群名>', ' 模型/API key: bolloon setup (交互向导)']),
124
+ ].join('\n'),
125
+ };
126
+ }
127
+ /** `bolloon identity show` —— 看本机身份 (不含私钥) */
128
+ async function identityShow(flags) {
129
+ const head = 'bolloon identity show';
130
+ const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
131
+ const facts = await readIdentityPublicFacts(home);
132
+ const file = getLocalIdentityFile(home);
133
+ if (!facts.exists) {
134
+ return {
135
+ envelope: failEnvelope('NOT_FOUND', `本机还没有身份文件: ${file}`, { file, exists: false, howTo: 'bolloon identity init', keyMaterialInOutput: false }, [], 'needs_human'),
136
+ human: `${title(head)}\n 本机还没有身份文件: ${file}\n\n${hint('建一个: bolloon identity init (非交互, 0600, 幂等)')}`,
137
+ };
138
+ }
139
+ const payload = {
140
+ file,
141
+ exists: true,
142
+ did: facts.did,
143
+ keyType: facts.keyType,
144
+ createdAt: facts.createdAt,
145
+ version: facts.version,
146
+ fingerprint: facts.fingerprint,
147
+ publicFields: facts.keys,
148
+ keyMaterialInOutput: false,
149
+ };
150
+ return {
151
+ envelope: okEnvelope('OK', `本机身份: ${String(facts.did).slice(0, 24)}…`, payload, [], null),
152
+ human: [
153
+ title(head),
154
+ line('DID', facts.did ?? '(缺 did — 文件可能损坏)'),
155
+ line('身份文件', file),
156
+ line('keyType', facts.keyType ?? '(无)'),
157
+ line('version', facts.version ?? '(无)'),
158
+ line('createdAt', facts.createdAt ?? '(无)'),
159
+ line('公钥指纹', facts.fingerprint ?? '(无)'),
160
+ line('非私密字段', facts.keys.join(' · ')),
161
+ '',
162
+ ' 私钥只在本机文件里 (0600), 不进任何输出/日志',
163
+ ].join('\n'),
164
+ };
165
+ }
166
+ /** 命令入口 (cli-entry 用 runCommand 包它: 统一 --json/--quiet/--timeout/异常兜底) */
167
+ export async function identityCommand(flags) {
168
+ const sub = String(flags.positionals[0] ?? '').trim().toLowerCase();
169
+ if (sub === 'init')
170
+ return identityInit(flags);
171
+ if (sub === 'show' || sub === 'status')
172
+ return identityShow(flags);
173
+ return {
174
+ envelope: failEnvelope('INVALID_ARGUMENT', sub ? `未知 identity 子命令: ${sub}` : '缺少 identity 子命令 (init | show)', { usage: plain(IDENTITY_USAGE.trim()), accepted: ['bolloon identity init', 'bolloon identity show'] }, [], 'needs_human'),
175
+ human: IDENTITY_USAGE,
176
+ };
177
+ }
@@ -71,6 +71,10 @@ const OPTIONS_WITH_VALUE = new Set([
71
71
  '--confirmation-window', '--proof-version', '--from-block', '--gate',
72
72
  // 2026-09-22 (P6b): 链上**写**操作的**授权意图**声明 (MCP 写 tool 必填; 只可能收紧, 不可能放权)
73
73
  '--payment-mode',
74
+ // 2026-09-23 (C1): 任务公告板 `bolloon task publish|board|claim` 的显式公告号
75
+ '--announcement-id',
76
+ // 2026-09-23 (C7): 群聊通道 `bolloon task announce|trail|post` 的群链接/groupId + 极短事实字段
77
+ '--group', '--round', '--criteria', '--kind', '--hash', '--bytes', '--checks', '--verdict', '--limit', '--from',
74
78
  ]);
75
79
  export function parseFlags(args) {
76
80
  const flags = { json: false, quiet: false, positionals: [], options: new Map(), raw: args };
@@ -109,6 +109,77 @@ export async function writeUserIdentity(name, home = os.homedir()) {
109
109
  await fs.writeFile(file, JSON.stringify(identity, null, 2), { encoding: 'utf-8', mode: 0o600 });
110
110
  return { identity, created, file };
111
111
  }
112
+ // ---------------------------------------------------------------- 本机 DIAP 身份 (非交互)
113
+ /** 本机 DIAP 身份文件 —— 与 index.ts `bootstrapIdentity()` / local-signer / network-pulse 同源 */
114
+ export function getLocalIdentityFile(home = os.homedir()) {
115
+ return path.join(home, '.bolloon', 'identity.json');
116
+ }
117
+ /**
118
+ * **非交互**建本机 DIAP 身份 (2026-09-24)。给新机器 / 第二实例用 ——
119
+ * `bolloon setup` 是 readline 交互向导, 在没有 TTY 的环境里会 `readline was closed`
120
+ * (ERR_USE_AFTER_CLOSE), 于是自动化流程根本建不出身份。
121
+ *
122
+ * 三条硬纪律:
123
+ * ① **复用现有生成逻辑**, 不新写一套密钥学 —— 走 `KeyManager.generate()` +
124
+ * `KeyManager.saveToFile()` (与 `src/index.ts:bootstrapIdentity` 完全同一条路径),
125
+ * 落盘字段因此天然一致: `{ keyType:'Ed25519', privateKey, publicKey, did, createdAt, version }`,
126
+ * 文件模式 `0600`。
127
+ * ② **幂等**: 已存在且能解析出 did → 不动、`action:'reused'` (调用方 exit 0)。
128
+ * 文件存在但**读不出 did** (损坏) → `action:'refused'` 并**拒绝覆盖** ——
129
+ * 静默盖掉一个可能还能救的身份是丢钥匙, 比失败糟得多; 要重来必须显式 `force`
130
+ * (覆盖前先把老文件备份成 `identity.json.bak-<时间戳>`)。
131
+ * ③ **绝不打印/返回私钥**: 返回体里只有 did / 文件路径 / 权限。
132
+ */
133
+ export async function initLocalIdentity(home = os.homedir(), opts = {}) {
134
+ const file = getLocalIdentityFile(home);
135
+ const readDid = async () => {
136
+ try {
137
+ const j = JSON.parse(await fs.readFile(file, 'utf-8'));
138
+ return j && typeof j.did === 'string' && j.did ? j.did : null;
139
+ }
140
+ catch {
141
+ return null;
142
+ }
143
+ };
144
+ const exists = await fs.stat(file).then(() => true).catch(() => false);
145
+ if (exists) {
146
+ const did = await readDid();
147
+ if (did && !opts.force) {
148
+ return { ok: true, action: 'reused', file, did, mode: await fileMode(file) };
149
+ }
150
+ if (!did && !opts.force) {
151
+ return {
152
+ ok: false, action: 'refused', file,
153
+ reason: `${file} 已存在但读不出 did (损坏?) → **不覆盖** (那可能是丢钥匙)。确认要重建再加 --force (会先备份成 .bak-<时间戳>)`,
154
+ };
155
+ }
156
+ // force: 先备份再重建 (备份失败 → 不重建)
157
+ try {
158
+ await fs.copyFile(file, `${file}.bak-${Date.now()}`);
159
+ }
160
+ catch (e) {
161
+ return { ok: false, action: 'refused', file, reason: `--force 下备份老文件失败, 拒绝重建: ${String(e?.message || e).slice(0, 160)}` };
162
+ }
163
+ }
164
+ const { KeyManager } = await import('@diap/sdk');
165
+ const kp = KeyManager.generate();
166
+ await fs.mkdir(path.dirname(file), { recursive: true });
167
+ await KeyManager.saveToFile(kp, file); // 同 bootstrapIdentity: 0600 + 6 字段
168
+ try {
169
+ await fs.chmod(file, 0o600);
170
+ }
171
+ catch { /* 某些平台/文件系统 no-op */ }
172
+ return { ok: true, action: 'created', file, did: String(kp.did || ''), mode: await fileMode(file) };
173
+ }
174
+ /** 文件权限 (八进制字符串; 读不到 → undefined) */
175
+ async function fileMode(file) {
176
+ try {
177
+ return ((await fs.stat(file)).mode & 0o777).toString(8);
178
+ }
179
+ catch {
180
+ return undefined;
181
+ }
182
+ }
112
183
  // ---------------------------------------------------------------- 首次运行判断
113
184
  // providerUsable 只有一份实现 (setup-store)
114
185
  /** 首次运行: 没有可用供应商, 或还没有用户身份 */
@@ -13,8 +13,9 @@
13
13
  * 2 检查不可用 (离线 / registry 不可用 / 读不到本地版本) —— 与"没有更新"区分开
14
14
  */
15
15
  import { resolveBolloonHome } from '../setup/setup-store.js';
16
- import { collectVersionInfo, renderVersionText, renderVersionJson, } from '../utils/version-info.js';
17
- import { checkForUpdate, buildUpdatePlan, renderUpdatePlan, applyUpdate, readUpdateStatus, readHistory, renderHistory, updateSummaryFor, compareVersions, } from '../utils/update-manager.js';
16
+ import { collectVersionInfo, renderVersionText, renderVersionJson, DEV_CHANNEL_WARNING, DEV_BACK_TO_STABLE_HINT, channelKindOf, isDevIdentity, } from '../utils/version-info.js';
17
+ import { checkForUpdate, buildUpdatePlan, renderUpdatePlan, applyUpdate, readUpdateStatus, readHistory, renderHistory, updateSummaryFor, compareVersions, checkExitCode, REFUSED_STATUSES, } from '../utils/update-manager.js';
18
+ import { renderGithubReportLine } from '../utils/dual-source.js';
18
19
  import { runDoctor, renderDoctor } from '../utils/update-health.js';
19
20
  import { detectRuntimeReport, bootstrapRuntimes, renderRuntimeReport, renderBootstrapPlan, RUNTIME_MIN, readRuntimeConfigWithFallback, } from '../utils/runtime-bootstrap.js';
20
21
  const RESET = '\x1b[0m';
@@ -25,6 +26,45 @@ const YELLOW = '\x1b[38;2;245;158;11m';
25
26
  const RED = '\x1b[38;2;239;68;68m';
26
27
  const out = (s) => process.stdout.write(s + '\n');
27
28
  const err = (s) => process.stderr.write(s + '\n');
29
+ // ── --channel (§12.2: 只覆盖本次进程, 不落盘) ───────────────────────────────
30
+ /** 允许的通道字面量 (`--channel stable|dev`; beta 仍是"与 stable 同源"的老口径)。 */
31
+ const CHANNEL_VALUES = ['stable', 'dev', 'beta'];
32
+ /**
33
+ * 解析 `--channel <v>` / `--channel=<v>`。
34
+ * 给了不认识的值 → **拒绝并说清** (不静默落回 stable, 那会让人以为切成功了)。
35
+ */
36
+ export function parseChannelArg(args) {
37
+ let raw = null;
38
+ for (let i = 0; i < args.length; i++) {
39
+ const a = args[i];
40
+ if (a === '--channel') {
41
+ raw = args[i + 1] ?? '';
42
+ break;
43
+ }
44
+ if (a.startsWith('--channel=')) {
45
+ raw = a.slice('--channel='.length);
46
+ break;
47
+ }
48
+ if (a === 'channel') {
49
+ raw = args[i + 1] ?? '';
50
+ break;
51
+ }
52
+ }
53
+ if (raw === null)
54
+ return { given: false };
55
+ const v = raw.trim().toLowerCase();
56
+ if (!CHANNEL_VALUES.includes(v)) {
57
+ return { given: true, error: `--channel 只接受 ${CHANNEL_VALUES.join('|')}, 收到 "${raw}" — 拒绝执行 (没有静默落回 stable)` };
58
+ }
59
+ return { channel: v, given: true };
60
+ }
61
+ /** dev 通道的三条硬约束 (§12.4) 在这里统一打印: 显式警告 + 记录 sha + 一键回 stable。 */
62
+ function printDevNotice() {
63
+ out('');
64
+ out(DEV_CHANNEL_WARNING);
65
+ out(DEV_BACK_TO_STABLE_HINT);
66
+ out('');
67
+ }
28
68
  // ── 版本 ────────────────────────────────────────────────────────────────────
29
69
  export async function runVersionCommand(args = []) {
30
70
  // 2026-09-19 (leo): 子命令一律**裸词**, 不再带 `--` 前缀 (只有 `--version` 本身保留 --)
@@ -78,22 +118,39 @@ function statusConclusion(status, latest, current) {
78
118
  const cmp = !latest ? '' : compareVersions(current, latest) < 0 ? ` — 已有更新版 ${latest}, 需按该安装方式手动升级` : ` — 本地 ${current} 已是 registry 上最新`;
79
119
  return `${YELLOW}当前安装方式不支持自动更新${RESET}${cmp}`;
80
120
  }
121
+ case 'github_unavailable': return `${RED}GitHub 源不可用${RESET} — ${BOLD}不能判定为最新${RESET}`;
122
+ case 'cross_check_mismatch': return `${RED}两个源不一致${RESET} — npm 与 GitHub 指向不同版本, ${BOLD}拒绝更新${RESET}`;
81
123
  default: return '尚未检查';
82
124
  }
83
125
  }
84
126
  export function renderCheckResult(r) {
127
+ const dev = r.channelKind === 'git-ref';
85
128
  const L = [];
86
- L.push(`当前版本: ${r.currentVersion}`);
87
- L.push(`最新版本: ${r.latestVersion || '未知'}`);
88
- L.push(`更新通道: ${r.channel} (来源: ${r.source})`);
129
+ L.push(`当前版本: ${r.currentVersion}${r.installedDevSha ? ` (dev 快照 commit ${r.installedDevSha})` : ''}`);
130
+ L.push(`${dev ? '最新快照' : '最新版本'}: ${r.latestVersion || '未知'}`);
131
+ L.push(`更新通道: ${r.channel} (比较语义: ${r.channelKind || channelKindOf(r.channel)})`);
132
+ L.push(`当前来源: ${r.installedChannel === 'dev' ? `dev (GitHub master 快照 @ ${r.installedDevSha})` : 'stable (npm registry)'}`);
89
133
  L.push(`安装方式: ${r.installMethod}`);
134
+ if (dev)
135
+ L.push(renderGithubReportLine(r.github));
136
+ else {
137
+ L.push(renderGithubReportLine(r.github));
138
+ if (r.crossCheck)
139
+ L.push(`交叉校验: ${r.crossCheck.kind}${r.crossCheck.blocking ? ' (阻塞)' : ''} — ${r.crossCheck.detail}`);
140
+ }
141
+ if (r.dev)
142
+ L.push(`dev 判定: ${r.dev.detail}`);
143
+ if (r.switchableTo)
144
+ L.push(`能切回: ${r.switchableTo.channel} (${r.switchableTo.source}${r.switchableTo.target ? ` @ ${r.switchableTo.target}` : ''})`);
90
145
  L.push(`检查时间: ${r.checkedAt}${r.fromCache ? ' (缓存)' : ''}`);
91
146
  L.push(`结论: ${statusConclusion(r.status, r.latestVersion, r.currentVersion)}`);
92
147
  if (r.reason)
93
148
  L.push(`说明: ${r.reason}`);
94
149
  L.push('');
95
150
  if (r.status === 'update_available') {
96
- L.push(`下一步: ${BOLD}bolloon update plan${RESET} 看计划, ${BOLD}bolloon update now${RESET} 执行更新`);
151
+ L.push(`下一步: ${BOLD}bolloon update plan --channel ${r.channel}${RESET} 看计划, ${BOLD}bolloon update now --channel ${r.channel}${RESET} 执行更新`);
152
+ if (dev)
153
+ L.push(DEV_CHANNEL_WARNING), L.push(DEV_BACK_TO_STABLE_HINT);
97
154
  }
98
155
  else if (r.status === 'unsupported_installation') {
99
156
  L.push('下一步: 按上面"说明"里对应安装方式更新 (源码目录: git pull && npm install && npm run build:all)');
@@ -101,6 +158,14 @@ export function renderCheckResult(r) {
101
158
  else if (r.status === 'offline' || r.status === 'registry_unavailable') {
102
159
  L.push('下一步: 检查网络/代理后重跑 `bolloon update`; 也可以 `bolloon update status` 看上次结论');
103
160
  }
161
+ else if (r.status === 'github_unavailable') {
162
+ L.push('下一步: 检查到 GitHub 的网络/代理/限流后重跑 `bolloon update --channel dev`;'
163
+ + `${BOLD}dev 通道不会退回 stable 装旧版${RESET} —— 要稳定版请显式 --channel stable`);
164
+ }
165
+ else if (r.status === 'cross_check_mismatch') {
166
+ L.push('下一步: 两个发布源不一致, 先查清哪个是对的 (npm: registry dist-tags / GitHub: Release 记录);'
167
+ + '本条不执行任何安装, 也不会按 GitHub 的 tag 去装。');
168
+ }
104
169
  else if (r.status === 'local_version_unknown') {
105
170
  L.push('下一步: 重新安装 (npm install -g @bolloon/bolloon-agent) 以恢复 package.json');
106
171
  }
@@ -109,32 +174,63 @@ export function renderCheckResult(r) {
109
174
  export function renderStatusReport(s) {
110
175
  const L = [];
111
176
  const lu = s.lastUpdate;
177
+ const dev = s.installedChannel === 'dev';
112
178
  L.push(`${BOLD}更新状态${RESET}`);
113
179
  L.push(`当前版本: ${s.currentVersion}`);
180
+ // ── §12.5: 必须说清"当前装的是哪个源 + 哪个版本/commit sha + 能切回哪个源" ──
181
+ L.push(`安装来源: ${dev
182
+ ? `dev (GitHub master 快照, ref ${s.devRef || 'refs/heads/master'}, commit ${s.installedDevSha || '未知'})`
183
+ : 'stable (npm registry)'}`);
184
+ L.push(`身份标识: ${dev ? `commit ${s.installedDevSha || '未知'}` : s.currentVersion}`);
185
+ L.push(`能切回: ${s.switchableTo
186
+ ? `${s.switchableTo.channel} (${s.switchableTo.source}${s.switchableTo.target ? ` @ ${s.switchableTo.target}` : ''}) — 一键切: bolloon update now --channel ${s.switchableTo.channel}`
187
+ : '无 (未知来源)'}`);
188
+ if (s.lastDevSha && !dev)
189
+ L.push(`上次 dev: commit ${s.lastDevSha}${s.devCheckedAt ? ` @ ${s.devCheckedAt}` : ''} (已切回 stable)`);
114
190
  L.push(`最新版本: ${s.latestVersion || (s.lastCheckStatus ? '未知' : '尚未检查')}`);
115
191
  L.push(`是否最新: ${statusConclusion(s.lastCheckStatus, s.latestVersion, s.currentVersion)}`);
116
- L.push(`更新通道: ${s.channel}`);
192
+ L.push(`更新通道: ${s.channel} (比较语义: ${s.channelKind})`);
117
193
  L.push(`安装方式: ${s.installMethod}`);
118
194
  L.push(`安装目录: ${s.installDir}`);
119
195
  L.push(`运行入口: ${s.entryPath}`);
196
+ if (s.sourceFacts) {
197
+ const f = s.sourceFacts;
198
+ L.push(`源事实: npm ${f.npm?.reachable ? `可达 (latest=${f.npm.latest || '未知'})` : `不可达${f.npm?.detail ? ` (${f.npm.detail})` : ''}`}`
199
+ + ` · GitHub ${f.github?.reachable
200
+ ? `可达 (HEAD ${f.github.headSha ? f.github.headSha.slice(0, 7) : '未知'}, Release ${f.github.releases ?? 0}, Tag ${f.github.tags ?? 0})`
201
+ : `不可达${f.github?.reason ? ` (github_unavailable(${f.github.reason}))` : ' (未查询)'}`}`);
202
+ if (f.crossCheck)
203
+ L.push(`交叉校验: ${f.crossCheck.kind}${f.crossCheck.blocking ? ' (阻塞)' : ''} — ${f.crossCheck.detail}`);
204
+ }
120
205
  L.push(`最近检查: ${s.lastCheckAt || '从未'}${s.lastCheckReason ? ` (${s.lastCheckReason})` : ''}`);
121
206
  L.push(`最近更新: ${lu ? `${lu.at} ${lu.from} → ${lu.to} [${lu.status}]${lu.reason ? ` ${lu.reason}` : ''}` : '无记录'}`);
122
207
  L.push(`最近失败: ${s.lastFailure ? `${s.lastFailure.at} @${s.lastFailure.stage}: ${s.lastFailure.reason}` : '无'}`);
123
208
  L.push(`需要重启: ${s.needsRestart ? '是 (新版已就位, 重启后生效)' : '否'}`);
124
209
  L.push(`更新锁: ${s.lock ? `有 (pid ${s.lock.pid}, ${s.lock.at}${s.lock.stale ? ', 已陈旧' : ''})` : '无'}`);
125
210
  L.push(`开关: 检查更新=${s.prefs.checkUpdates} (${s.prefs.sources.checkUpdates}) · 自动安装=${s.prefs.autoInstall} (${s.prefs.sources.autoInstall}) · 自动重启=${s.prefs.autoRestart} (${s.prefs.sources.autoRestart})`);
211
+ if (dev) {
212
+ L.push('');
213
+ L.push(DEV_CHANNEL_WARNING);
214
+ L.push(DEV_BACK_TO_STABLE_HINT);
215
+ }
126
216
  return L.join('\n');
127
217
  }
128
218
  // ── update 子命令 ───────────────────────────────────────────────────────────
129
219
  export async function runUpdateCommand(args = []) {
130
220
  const home = resolveBolloonHome();
131
- // 裸词优先 (bolloon update plan / status / history / now / wait / force / json),
221
+ // 裸词优先 (bolloon update plan / status / history / now / wait / force / json / --channel dev),
132
222
  // 同时兼容旧的 --plan / --status / ... 写法 (已有脚本不断裂)。
133
223
  const has = (...f) => f.some((x) => args.includes(x));
134
224
  const json = has('json', '--json');
225
+ const ch = parseChannelArg(args);
226
+ if (ch.error) {
227
+ err(`${RED}✗ ${ch.error}${RESET}`);
228
+ return 2;
229
+ }
230
+ const channel = ch.channel;
135
231
  // ── 状态 ──
136
232
  if (has('status', '--status')) {
137
- const s = await readUpdateStatus({ home });
233
+ const s = await readUpdateStatus({ home, channel });
138
234
  if (json) {
139
235
  out(JSON.stringify(s, null, 2));
140
236
  return 0;
@@ -156,21 +252,26 @@ export async function runUpdateCommand(args = []) {
156
252
  }
157
253
  // ── 计划 ──
158
254
  if (has('plan', '--plan')) {
159
- const plan = await buildUpdatePlan({ home, force: true });
255
+ const plan = await buildUpdatePlan({ home, force: true, channel });
160
256
  if (json) {
161
257
  out(JSON.stringify(plan, null, 2));
162
258
  return 0;
163
259
  }
164
260
  out(renderUpdatePlan(plan));
261
+ if (plan.channelKind === 'git-ref')
262
+ printDevNotice();
165
263
  return 0;
166
264
  }
167
265
  // ── 执行 ──
168
266
  if (has('now', '--now')) {
169
267
  const wait = has('wait', '--wait');
170
268
  const force = has('force', '--force', '-f');
171
- const plan = await buildUpdatePlan({ home, force: true });
172
- if (!json)
269
+ const plan = await buildUpdatePlan({ home, force: true, channel });
270
+ if (!json) {
173
271
  out(renderUpdatePlan(plan));
272
+ if (plan.channelKind === 'git-ref')
273
+ printDevNotice();
274
+ }
174
275
  if (!plan.ok && plan.blockers.length > 0) {
175
276
  if (json)
176
277
  out(JSON.stringify({ stage: 'blocked', reason: plan.blockers.join('; ') }, null, 2));
@@ -178,6 +279,17 @@ export async function runUpdateCommand(args = []) {
178
279
  return 1;
179
280
  }
180
281
  if (!plan.targetVersion) {
282
+ /**
283
+ * §12.5: 源不可达 / 版本不存在 → **拒绝并说清**, 退出码 2。
284
+ * 绝不打印"✅ 已是最新, 无需更新" (那正是这次要收敛掉的骗人行为)。
285
+ */
286
+ if (REFUSED_STATUSES.includes(plan.check.status)) {
287
+ if (json)
288
+ out(JSON.stringify({ stage: 'blocked', refused: true, status: plan.check.status, reason: plan.check.reason }, null, 2));
289
+ err(`${RED}✗ 拒绝更新${RESET} (${plan.check.status}): ${plan.check.reason || '源不可达 / 版本不存在'}`);
290
+ err(' 没有安装任何东西, 也没有静默退回旧版。');
291
+ return 2;
292
+ }
181
293
  if (json)
182
294
  out(JSON.stringify({ stage: 'succeeded', ok: true, reason: '已是最新', from: plan.currentVersion, to: plan.currentVersion }, null, 2));
183
295
  out(`${GREEN}✓ 已是最新, 无需更新${RESET} (${plan.currentVersion})`);
@@ -185,27 +297,34 @@ export async function runUpdateCommand(args = []) {
185
297
  }
186
298
  const res = await applyUpdate({
187
299
  home,
300
+ channel,
188
301
  strategy: wait ? 'wait' : 'now',
189
302
  force,
190
303
  onStage: json ? undefined : (stage, detail) => out(` [${stage}] ${detail}`),
191
304
  });
192
305
  if (json) {
193
306
  out(JSON.stringify(res, null, 2));
194
- return res.ok ? 0 : 1;
307
+ return res.ok ? 0 : (res.stage === 'blocked' ? 2 : 1);
195
308
  }
196
309
  if (res.ok) {
197
310
  out(`${GREEN}✓ 更新成功${RESET}: ${res.from} → ${res.to} (${(res.durationMs / 1000).toFixed(1)}s)`);
311
+ if (isDevIdentity(res.to)) {
312
+ out(` dev 快照已记录: commit ${res.to.split('+dev.')[1] || '未知'} (ref refs/heads/master) — bolloon update status 可查`);
313
+ }
198
314
  out(' 正在做更新后健康检查...');
199
315
  const health = await runDoctor({ bolloonHome: home }).catch(() => null);
200
316
  if (health) {
201
317
  out(renderDoctor(health));
202
318
  }
319
+ if (isDevIdentity(res.to))
320
+ printDevNotice();
203
321
  out(`${YELLOW}请重启 bolloon 以使用新版本${RESET}`);
204
322
  return 0;
205
323
  }
206
324
  if (res.stage === 'blocked') {
325
+ const refused = /拒绝更新|github_unavailable|cross_check_mismatch/.test(res.reason || '');
207
326
  err(`${YELLOW}! ${res.reason}${RESET}`);
208
- return 1;
327
+ return refused ? 2 : 1;
209
328
  }
210
329
  err(`${RED}✗ 更新失败${RESET} (${res.stage}): ${res.reason}`);
211
330
  err(` 旧版本仍在: ${res.from} — 可继续使用; 详情: bolloon update status`);
@@ -214,21 +333,17 @@ export async function runUpdateCommand(args = []) {
214
333
  return 1;
215
334
  }
216
335
  // ── 默认: 只检查, 给清晰结论 (显式命令 = 忽略节流) ──
217
- const r = await checkForUpdate({ home, force: true });
336
+ const r = await checkForUpdate({ home, force: true, channel });
218
337
  if (json) {
219
338
  out(JSON.stringify(r, null, 2));
220
- return r.status === 'offline' || r.status === 'registry_unavailable' || r.status === 'local_version_unknown' ? 2 : 0;
339
+ return checkExitCode(r.status);
221
340
  }
222
341
  out(renderCheckResult(r));
223
342
  if (r.status !== 'up_to_date' && r.status !== 'check_skipped') {
224
343
  out('');
225
344
  out(`${YELLOW}提示${RESET}: 本次只检查, 没有安装任何东西。`);
226
345
  }
227
- if (r.status === 'offline' || r.status === 'registry_unavailable' || r.status === 'local_version_unknown')
228
- return 2;
229
- if (r.status === 'update_available' && compareVersions(r.currentVersion, r.latestVersion || r.currentVersion) >= 0)
230
- return 0;
231
- return 0;
346
+ return checkExitCode(r.status);
232
347
  }
233
348
  // ── doctor ──────────────────────────────────────────────────────────────────
234
349
  export async function runDoctorCommand(args = []) {
@@ -323,12 +438,12 @@ export async function runRuntimeCommand(args = []) {
323
438
  }
324
439
  export const UPDATE_HELP = `
325
440
  ${BOLD}版本与更新:${RESET} (子命令一律裸词, 不带 -- 前缀)
326
- bolloon --version 版本 + 安装方式/目录/入口/通道/上游/是否最新
441
+ bolloon --version 版本 + 安装方式/目录/入口/通道/来源身份/上游/是否最新
327
442
  bolloon --version verbose 诊断版 (构建时间/git/Node/npm/Python/配置目录/最近更新)
328
443
  bolloon --version json 机器版 (同一份 VersionInfo, 给脚本与问题报告)
329
444
  bolloon update 只检查并给结论 (不安装任何东西)
330
445
  bolloon update plan 更新计划: 要更新什么 / 不会动什么 / 风险检查
331
- bolloon update status 更新状态: 最近检查/最近更新/失败/是否需重启/开关
446
+ bolloon update status 更新状态: 装的哪个源+版本/commit+能切回哪/最近检查/失败/开关
332
447
  bolloon update history [N] 最近 N 次更新 (时间 · 版本变化 · 结果 · 耗时 · 原因)
333
448
  bolloon update now 真正执行更新 (默认先看计划)
334
449
  bolloon update now wait 等当前 Run 结束后再更新 (有长期任务时的默认建议)
@@ -336,6 +451,15 @@ ${BOLD}版本与更新:${RESET} (子命令一律裸词, 不带 -- 前缀)
336
451
  bolloon doctor json 同上, 机器可读
337
452
  bolloon runtime [plan|install|json] 运行时 (Node/npm/Git/Python) 检查与安装 (install 需显式 yes)
338
453
 
454
+ ${BOLD}双源 (--channel):${RESET}
455
+ --channel stable (默认) npm registry 是权威 (dist-tags.latest), GitHub Tag/Release 只做交叉校验;
456
+ 比较语义 = semver (npm 的 latest ↔ GitHub 同名 tag)
457
+ --channel dev GitHub master HEAD 是唯一源; 比较语义 = git ref + commit sha (不用 semver);
458
+ 身份 = <版本>+dev.<sha7>; 未走发布门, 会打印警告并记录 sha
459
+ 一键回稳定版: bolloon update now --channel stable
460
+ --channel 只覆盖本次进程, 不落盘 (config 里的 updateChannel 仍是默认值)
461
+ 源不可达 / 版本不存在 → 拒绝并说清 (退出码 2), 不会静默装回旧版
462
+
339
463
  ${BOLD}开关 (config.json):${RESET}
340
464
  checkUpdates (默认 true) 启动时后台检查
341
465
  autoInstall (默认 false) 自动安装 —— 默认关, 需显式打开
@@ -344,6 +468,8 @@ ${BOLD}环境变量 (临时覆盖, 不落盘):${RESET}
344
468
  BOLLOON_SKIP_UPDATE=1 本次不检查
345
469
  BOLLOON_AUTO_UPDATE=1 本次允许自动安装
346
470
  BOLLOON_UPDATE_CHANNEL=stable|beta|dev
471
+ BOLLOON_GITHUB_API 覆盖 GitHub API 根 (镜像/受控验收用)
472
+ BOLLOON_DEV_SOURCE_DIR dev 快照用本地源码树 (必须与该 sha 一致, 否则拒绝)
347
473
 
348
- ${BOLD}退出码:${RESET} 0 正常 · 1 执行失败 · 2 检查不可用 (离线/registry 不可用/读不到本地版本)
474
+ ${BOLD}退出码:${RESET} 0 正常 · 1 执行失败 · 2 检查不可用 (离线/registry 不可用/GitHub 不可用/两源不一致/读不到本地版本)
349
475
  `;
package/dist/cli-entry.js CHANGED
@@ -26,11 +26,12 @@ import { collectVersionInfo } from './utils/version-info.js';
26
26
  import { runServiceGroup, GROUPS_HELP } from './cli/commands/index.js';
27
27
  // 2026-09-21 (P4): MCP 适配层 (`bolloon mcp serve` = stdio, 只调 P3 服务层)
28
28
  import { runMcpCommand } from './cli/commands/mcp.js';
29
- import { legacyJson } from './cli/protocol-envelope.js';
29
+ import { legacyJson, parseFlags, runCommand } from './cli/protocol-envelope.js';
30
+ import { identityCommand } from './cli/identity-command.js';
30
31
  import { createRequire } from 'module';
31
32
  const _require = createRequire(import.meta.url);
32
33
  /** P3 `bolloon task <子命令>` 的已知子命令 —— 其它一律当 M1 任务正文 (既有体验不动) */
33
- const TASK_SUBCOMMANDS = new Set(['send', 'list', 'status', 'cancel', 'retry', 'result', 'inbox', 'accept', 'reject', 'run', 'complete']);
34
+ const TASK_SUBCOMMANDS = new Set(['send', 'list', 'status', 'cancel', 'retry', 'result', 'inbox', 'accept', 'reject', 'run', 'complete', 'publish', 'board', 'claim', 'announce', 'trail', 'post', 'group']);
34
35
  const isWindows = process.platform === 'win32';
35
36
  // ANSI 颜色 — Bolloon Web UI 配色 truecolor
36
37
  function _fg(r, g, b) { return `\x1b[38;2;${r};${g};${b}m`; }
@@ -62,6 +63,8 @@ ${BOLD}选项:${RESET}
62
63
 
63
64
  ${BOLD}命令:${RESET}
64
65
  bolloon setup 初始化向导 (你的称呼 + 模型供应商 + API key + 连通性测试)
66
+ bolloon identity init 非交互建本机身份 (~/.bolloon/identity.json, 0600, 幂等; 新机器/第二实例用)
67
+ bolloon identity show 看本机身份 (只出 DID/指纹, 绝不打印私钥)
65
68
  bolloon update [plan|status|history|now] 检查更新 / 计划 / 状态 / 历史 / 执行 (默认只检查)
66
69
  bolloon doctor 安装入口 + 版本事实 + 更新状态自洽性诊断
67
70
  bolloon runtime [plan|install] 运行时 (Node/npm/Git/Python) 检查与安装
@@ -203,6 +206,9 @@ function parseArgs() {
203
206
  case 'setup':
204
207
  case 'init':
205
208
  return { mode: 'setup', args: args.slice(1) };
209
+ // 2026-09-24: 非交互建本机身份 (`bolloon identity init` → ~/.bolloon/identity.json)
210
+ case 'identity':
211
+ return { mode: 'identity', args: args.slice(1) };
206
212
  case 'read':
207
213
  case 'summarize':
208
214
  case 'improve':
@@ -890,6 +896,10 @@ async function main() {
890
896
  case 'setup':
891
897
  await handleSetupCommand(args);
892
898
  break;
899
+ // 2026-09-24: bolloon identity — 非交互建/看本机身份 (新机器/第二实例用; 幂等, 不打印私钥)
900
+ case 'identity':
901
+ process.exit(await runCommand(parseFlags(args), identityCommand));
902
+ break;
893
903
  case 'passthrough':
894
904
  // 传递所有参数给主程序
895
905
  try {