@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,761 @@
1
+ "use strict";
2
+ /**
3
+ * task-board.ts — 任务对外发布 + 接单 (C1/C2, 2026-09-23)
4
+ *
5
+ * 补的是 M1 一个真实断点: 买方要委托任务时, 如果对方不在自己的注册表里, **根本发不出去** ——
6
+ * 没有任何地方能"把一个待接单的任务公告出去让别人发现", 所以 M1 只能"自己买自己的技能"。
7
+ * 本模块给出最小可用的对外通道:
8
+ *
9
+ * publish → 公告落盘 `~/.bolloon/tasks/board/<announcementId>.json` + 向 agent-registry 公告
10
+ * (`service.name = 'task.announce'`) + 记一条脉冲事件 `task_announced`
11
+ * board → 本地公告 + **注册表发现的远端公告**, 按 announcementId 去重
12
+ * claim → 一个 provider 接单: 记认领者 DID / 时间 / 声明价格; 重复认领/已取消/不存在一律拒
13
+ *
14
+ * 三条纪律 (与 task-inbox / task-contract 同源):
15
+ * · **任务正文只在本地**: instruction 只进本地公告文件; 注册表/公开投影里只有 sha256 摘要 + 60 字预览。
16
+ * · **公告必须签名**: 签名覆盖"公开载荷"(不含正文、不含可变 status); 验签或摘要对不上 → 接单被拒
17
+ * (拿不到事实就不许往下走, 绝不静默放行)。
18
+ * · **本模块一分钱都不动**: 认领只记账; 释放/验真是**纯裁决函数** `decideAnnouncementRelease`,
19
+ * 未交付不得释放, 未链上结算不得标 verified, local-dev 永远不是链上结算。
20
+ *
21
+ * 未做 (如实标注, 不假装):
22
+ * · 远端公告的认领**只落本机台账**, 本版没有"向远端投递认领"的通道 (真交接仍走
23
+ * `bolloon task send` → 对方 `task accept`); 远端 claim 的返回值里明写 `deliveredToBuyer:false`。
24
+ * · 释放裁决只出结论,**不发链上交易** (真发交易属 chain 命令组/另一条并行线)。
25
+ * · 公告没有任何到期自动下架之外的清理 (过期只是不再可接单, 文件不删)。
26
+ */
27
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
28
+ if (k2 === undefined) k2 = k;
29
+ var desc = Object.getOwnPropertyDescriptor(m, k);
30
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
31
+ desc = { enumerable: true, get: function() { return m[k]; } };
32
+ }
33
+ Object.defineProperty(o, k2, desc);
34
+ }) : (function(o, m, k, k2) {
35
+ if (k2 === undefined) k2 = k;
36
+ o[k2] = m[k];
37
+ }));
38
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
39
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
40
+ }) : function(o, v) {
41
+ o["default"] = v;
42
+ });
43
+ var __importStar = (this && this.__importStar) || (function () {
44
+ var ownKeys = function(o) {
45
+ ownKeys = Object.getOwnPropertyNames || function (o) {
46
+ var ar = [];
47
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
48
+ return ar;
49
+ };
50
+ return ownKeys(o);
51
+ };
52
+ return function (mod) {
53
+ if (mod && mod.__esModule) return mod;
54
+ var result = {};
55
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
56
+ __setModuleDefault(result, mod);
57
+ return result;
58
+ };
59
+ })();
60
+ Object.defineProperty(exports, "__esModule", { value: true });
61
+ exports.boardDir = exports.PREVIEW_MAX = exports.REMOTE_CLAIMS_FILE = exports.ANNOUNCE_CAPABILITY = exports.ANNOUNCE_DESC_PREFIX = exports.ANNOUNCE_SERVICE_NAME = exports.BOARD_KIND = exports.BOARD_PROTOCOL = void 0;
62
+ exports.announcementFile = announcementFile;
63
+ exports.budgetKey = budgetKey;
64
+ exports.deriveAnnouncementId = deriveAnnouncementId;
65
+ exports.previewOf = previewOf;
66
+ exports.publicPayloadOf = publicPayloadOf;
67
+ exports.readAnnouncement = readAnnouncement;
68
+ exports.listAnnouncements = listAnnouncements;
69
+ exports.saveAnnouncement = saveAnnouncement;
70
+ exports.verifyAnnouncementSignature = verifyAnnouncementSignature;
71
+ exports.publishAnnouncement = publishAnnouncement;
72
+ exports.buildAnnouncementService = buildAnnouncementService;
73
+ exports.refreshRegistryAnnouncements = refreshRegistryAnnouncements;
74
+ exports.parseRegistryAnnouncements = parseRegistryAnnouncements;
75
+ exports.boardEntryOf = boardEntryOf;
76
+ exports.remoteBoardEntryOf = remoteBoardEntryOf;
77
+ exports.listBoard = listBoard;
78
+ exports.findOpenAnnouncementsForCapability = findOpenAnnouncementsForCapability;
79
+ exports.readRemoteClaims = readRemoteClaims;
80
+ exports.claimAnnouncement = claimAnnouncement;
81
+ exports.cancelAnnouncement = cancelAnnouncement;
82
+ exports.decideAnnouncementRelease = decideAnnouncementRelease;
83
+ const fs = __importStar(require("fs"));
84
+ const os = __importStar(require("os"));
85
+ const path = __importStar(require("path"));
86
+ const crypto = __importStar(require("crypto"));
87
+ // ── 常量 ────────────────────────────────────────────────────────────────────
88
+ exports.BOARD_PROTOCOL = 'bolloon-task/1';
89
+ exports.BOARD_KIND = 'task_announcement';
90
+ /** 注册表里可发现的公告服务名 (远端靠它 + `announce:<id>` capability 找到公告) */
91
+ exports.ANNOUNCE_SERVICE_NAME = 'task.announce';
92
+ /** 注册表 description 里的载荷前缀 (结构化 JSON 跟在这个标记后) */
93
+ exports.ANNOUNCE_DESC_PREFIX = 'bolloon-task-announce/1 ';
94
+ exports.ANNOUNCE_CAPABILITY = 'task.announce';
95
+ /** 远端认领落盘文件 (本机台账, 未投递) */
96
+ exports.REMOTE_CLAIMS_FILE = 'remote-claims.json';
97
+ exports.PREVIEW_MAX = 60;
98
+ // ── 路径 (全部在 ~/.bolloon/tasks/ 下, 与 inbox/local/results/bodies 同级) ────
99
+ const homeOf = (home) => home || process.env.HOME || os.homedir();
100
+ const boardDir = (home) => path.join(homeOf(home), '.bolloon', 'tasks', 'board');
101
+ exports.boardDir = boardDir;
102
+ /** 只允许 announcementId 当文件名 (防路径穿越); 非法 → null */
103
+ function safeId(id) {
104
+ const s = String(id || '').trim();
105
+ return /^ann-[A-Za-z0-9._-]{4,96}$/.test(s) ? s : null;
106
+ }
107
+ function announcementFile(announcementId, home) {
108
+ const id = safeId(announcementId);
109
+ return id ? path.join((0, exports.boardDir)(home), `${id}.json`) : null;
110
+ }
111
+ function ensure(dir) {
112
+ try {
113
+ fs.mkdirSync(dir, { recursive: true });
114
+ return true;
115
+ }
116
+ catch {
117
+ return false;
118
+ }
119
+ }
120
+ function writeJson(file, value) {
121
+ try {
122
+ ensure(path.dirname(file));
123
+ fs.writeFileSync(file, JSON.stringify(value, null, 2) + '\n', 'utf8');
124
+ return { ok: true };
125
+ }
126
+ catch (e) {
127
+ return { ok: false, error: String(e?.message || e).slice(0, 200) };
128
+ }
129
+ }
130
+ function readJson(file) {
131
+ try {
132
+ if (!fs.existsSync(file))
133
+ return null;
134
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
135
+ }
136
+ catch {
137
+ return null;
138
+ }
139
+ }
140
+ const sha256Hex = (s) => crypto.createHash('sha256').update(s, 'utf8').digest('hex');
141
+ // ── id 派生 ─────────────────────────────────────────────────────────────────
142
+ function budgetKey(budget) {
143
+ return budget ? `${budget.maxAmount}:${budget.currency}:${budget.network}` : 'none';
144
+ }
145
+ /**
146
+ * **稳定** announcementId: 由 (capability, 正文, 买方, 预算) 确定性派生。
147
+ * 同一买方对同一个能力 + 同一段正文 + 同一预算重复 publish → 同一个 id (幂等, 不产生第二条公告)。
148
+ * deadline **不参与** id (它是时限而不是身份): 重发只会在既有公告上留一句说明, 不覆盖既有认领。
149
+ */
150
+ function deriveAnnouncementId(input) {
151
+ const h = sha256Hex(`${String(input.capability).trim()}|${String(input.instruction).trim()}|${input.buyerDid}|${budgetKey(input.budget)}`);
152
+ return `ann-${h.slice(0, 16)}`;
153
+ }
154
+ function previewOf(instruction) {
155
+ const s = String(instruction || '').replace(/\s+/g, ' ').trim();
156
+ return s.length > exports.PREVIEW_MAX ? `${s.slice(0, exports.PREVIEW_MAX)}…` : s;
157
+ }
158
+ function publicPayloadOf(a) {
159
+ return {
160
+ protocol: exports.BOARD_PROTOCOL,
161
+ kind: exports.BOARD_KIND,
162
+ announcementId: a.announcementId,
163
+ capability: a.capability,
164
+ instructionDigest: String(a.instructionDigest || ''),
165
+ instructionPreview: String(a.instructionPreview || ''),
166
+ buyerDid: a.buyerDid,
167
+ buyerPublicKeyHex: String(a.buyerPublicKeyHex || ''),
168
+ budget: a.budget ?? null,
169
+ deadline: Number(a.deadline || 0),
170
+ paymentMode: a.paymentMode || 'policy',
171
+ createdAt: Number(a.createdAt || 0),
172
+ };
173
+ }
174
+ // ── 读 / 写 ─────────────────────────────────────────────────────────────────
175
+ function readAnnouncement(announcementId, home) {
176
+ const f = announcementFile(announcementId, home);
177
+ if (!f)
178
+ return null;
179
+ const a = readJson(f);
180
+ return a && a.announcementId ? a : null;
181
+ }
182
+ function listAnnouncements(home) {
183
+ try {
184
+ const dir = (0, exports.boardDir)(home);
185
+ if (!fs.existsSync(dir))
186
+ return [];
187
+ return fs.readdirSync(dir)
188
+ .filter((f) => f.endsWith('.json') && f.startsWith('ann-') && f !== exports.REMOTE_CLAIMS_FILE)
189
+ .map((f) => readJson(path.join(dir, f)))
190
+ .filter((x) => !!x?.announcementId)
191
+ .sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0));
192
+ }
193
+ catch {
194
+ return [];
195
+ }
196
+ }
197
+ function saveAnnouncement(a, home) {
198
+ const f = announcementFile(a.announcementId, home);
199
+ if (!f)
200
+ return { ok: false, error: `announcementId 非法 (不能当文件名): ${a.announcementId}` };
201
+ return writeJson(f, a);
202
+ }
203
+ /** 公告签名验签 (真验, 不是硬编 true); 缺签名/公钥非法/验不过 → false */
204
+ async function verifyAnnouncementSignature(a) {
205
+ try {
206
+ const { verifyTaskEnvelope } = await Promise.resolve().then(() => __importStar(require('./task-contract.js')));
207
+ const { verifierFor } = await Promise.resolve().then(() => __importStar(require('./local-signer.js')));
208
+ const sig = String(a?.signature || '');
209
+ const pub = String(a?.buyerPublicKeyHex || '');
210
+ if (!sig || !pub)
211
+ return false;
212
+ const vf = verifierFor(pub);
213
+ if (!vf)
214
+ return false;
215
+ const payload = { ...publicPayloadOf(a), signature: sig };
216
+ return await verifyTaskEnvelope(payload, vf);
217
+ }
218
+ catch {
219
+ return false;
220
+ }
221
+ }
222
+ async function defaultRegistry() {
223
+ try {
224
+ const { getAgentRegistry } = await Promise.resolve().then(() => __importStar(require('./agent-registry.js')));
225
+ return getAgentRegistry();
226
+ }
227
+ catch {
228
+ return null;
229
+ }
230
+ }
231
+ /**
232
+ * 发布一条待接单任务公告。
233
+ * **幂等**: 同一 (capability, 正文, 买方, 预算) → 同一个 announcementId; 已存在 → 返回既有公告 (dup:true),
234
+ * 不覆盖已有认领、不重复向注册表写第二份。
235
+ */
236
+ async function publishAnnouncement(input, deps = {}) {
237
+ const now = deps.now ?? Date.now();
238
+ const capability = String(input.capability || '').trim();
239
+ const instruction = String(input.instruction || '').trim();
240
+ if (!capability)
241
+ return { ok: false, dup: false, signed: false, registry: { attempted: false, announced: false, ready: false, error: null, entries: 0 }, pulse: { attempted: false, ok: false, reason: null }, error: 'capability 必填' };
242
+ if (!instruction)
243
+ return { ok: false, dup: false, signed: false, registry: { attempted: false, announced: false, ready: false, error: null, entries: 0 }, pulse: { attempted: false, ok: false, reason: null }, error: 'instruction 必填 (任务正文不能是空的)' };
244
+ if (!Number.isFinite(input.deadline) || input.deadline <= 0)
245
+ return { ok: false, dup: false, signed: false, registry: { attempted: false, announced: false, ready: false, error: null, entries: 0 }, pulse: { attempted: false, ok: false, reason: null }, error: 'deadline 必须是未来毫秒时间戳' };
246
+ if (!String(input.buyerDid || '').trim())
247
+ return { ok: false, dup: false, signed: false, registry: { attempted: false, announced: false, ready: false, error: null, entries: 0 }, pulse: { attempted: false, ok: false, reason: null }, error: 'buyerDid 必填 (公告必须有可追责的买方身份)' };
248
+ const announcementId = deriveAnnouncementId({ capability, instruction, buyerDid: input.buyerDid, budget: input.budget ?? null });
249
+ const existing = readAnnouncement(announcementId, deps.home);
250
+ if (existing) {
251
+ return {
252
+ ok: true, dup: true, announcement: existing,
253
+ file: `~/.bolloon/tasks/board/${announcementId}.json`,
254
+ signed: !!existing.signature,
255
+ registry: { attempted: false, announced: false, ready: false, error: null, entries: 0 },
256
+ pulse: { attempted: false, ok: false, reason: '已存在的公告不重复记事件' },
257
+ };
258
+ }
259
+ const base = {
260
+ protocol: exports.BOARD_PROTOCOL,
261
+ kind: exports.BOARD_KIND,
262
+ announcementId,
263
+ capability,
264
+ instruction,
265
+ instructionDigest: sha256Hex(instruction),
266
+ instructionPreview: previewOf(instruction),
267
+ buyerDid: input.buyerDid,
268
+ buyerPublicKeyHex: String(input.buyerPublicKeyHex || ''),
269
+ budget: input.budget ?? null,
270
+ deadline: input.deadline,
271
+ paymentMode: input.paymentMode,
272
+ createdAt: now,
273
+ status: 'open',
274
+ updatedAt: now,
275
+ claims: [],
276
+ replyTo: input.replyTo ?? null,
277
+ signature: '',
278
+ };
279
+ // 签名 (覆盖公开载荷); 没有 keypair → 不签 (接单时会被拒 —— 如实, 不糊)
280
+ let signed = false;
281
+ if (input.signerKeypair) {
282
+ try {
283
+ const { signTaskEnvelope } = await Promise.resolve().then(() => __importStar(require('./task-contract.js')));
284
+ const s = await signTaskEnvelope(publicPayloadOf(base), input.signerKeypair);
285
+ base.signature = String(s.signature || '');
286
+ signed = !!base.signature;
287
+ }
288
+ catch {
289
+ signed = false;
290
+ }
291
+ }
292
+ const w = saveAnnouncement(base, deps.home);
293
+ if (!w.ok) {
294
+ return { ok: false, dup: false, signed, registry: { attempted: false, announced: false, ready: false, error: null, entries: 0 }, pulse: { attempted: false, ok: false, reason: null }, error: `公告落盘失败: ${w.error}` };
295
+ }
296
+ // 向注册表公告 (把该买方的全部 open 公告一起写进同一个 AgentService 条目 —— 注册表按 agentId upsert)
297
+ const registryResult = deps.offline
298
+ ? { attempted: false, announced: false, ready: false, error: null, entries: 0 }
299
+ : await refreshRegistryAnnouncements(input.buyerDid, { home: deps.home, registry: deps.registry });
300
+ // 脉冲事件 (匿名): 复用经济事件族, 新增 task_announced
301
+ const pulse = { attempted: false, ok: false, reason: null };
302
+ if (!deps.offline) {
303
+ pulse.attempted = true;
304
+ try {
305
+ const rec = deps.recordEvent || (async (ev) => {
306
+ const { recordNetworkEvent } = await Promise.resolve().then(() => __importStar(require('./network-pulse.js')));
307
+ return await recordNetworkEvent(ev, deps.home);
308
+ });
309
+ const r = await rec({ type: 'task_announced', capability, did: input.buyerDid, taskId: announcementId, at: now });
310
+ pulse.ok = r.ok === true;
311
+ pulse.reason = r.reason ?? null;
312
+ }
313
+ catch (e) {
314
+ pulse.reason = String(e?.message || e).slice(0, 160);
315
+ }
316
+ }
317
+ return {
318
+ ok: true, dup: false, announcement: base,
319
+ file: `~/.bolloon/tasks/board/${announcementId}.json`,
320
+ signed, registry: registryResult, pulse,
321
+ };
322
+ }
323
+ /**
324
+ * 把一个买方的 open 公告打包成注册表可发现的 AgentService (正文绝不进去)。
325
+ * `capabilities` 里带 `announce:<id>` 便于 discover; 结构化字段在 description 的
326
+ * `bolloon-task-announce/1 <JSON>` 载荷里 (远端要用的都在这里: 能力/预算/截止/摘要/签名)。
327
+ */
328
+ function buildAnnouncementService(buyerDid, open) {
329
+ const payloads = open.map((a) => ({
330
+ announcementId: a.announcementId,
331
+ capability: a.capability,
332
+ instructionDigest: a.instructionDigest,
333
+ instructionPreview: a.instructionPreview,
334
+ buyerDid: a.buyerDid,
335
+ buyerPublicKeyHex: a.buyerPublicKeyHex,
336
+ budget: a.budget ?? null,
337
+ deadline: a.deadline,
338
+ paymentMode: a.paymentMode,
339
+ status: a.status,
340
+ createdAt: a.createdAt,
341
+ signature: a.signature,
342
+ replyTo: a.replyTo ?? null,
343
+ }));
344
+ const first = open[0];
345
+ return {
346
+ agentId: buyerDid,
347
+ name: `任务公告 (${open.length})`,
348
+ wallet: '0x0',
349
+ service: {
350
+ name: exports.ANNOUNCE_SERVICE_NAME,
351
+ description: `${exports.ANNOUNCE_DESC_PREFIX}${JSON.stringify(payloads)}`,
352
+ price: first?.budget
353
+ ? { amount: String(first.budget.maxAmount), currency: String(first.budget.currency), per: 'task' }
354
+ : { amount: '0', currency: 'USDC', per: 'task' },
355
+ },
356
+ capabilities: [exports.ANNOUNCE_CAPABILITY, ...open.map((a) => `announce:${a.announcementId}`)],
357
+ registeredAt: new Date().toISOString(),
358
+ updatedAt: new Date().toISOString(),
359
+ };
360
+ }
361
+ /**
362
+ * 把某买方**当前 open** 的公告集合同步到注册表 (注册表按 agentId upsert, 所以一个买方一条条目)。
363
+ * 幂等; 失败如实回 `announced:false + error` (不假装公告出去了)。publish 与 cancel 都用它 ——
364
+ * 取消后也能立刻把条目刷成新事实 (否则远端会短暂看到已撤销的公告)。
365
+ */
366
+ async function refreshRegistryAnnouncements(buyerDid, deps = {}) {
367
+ const out = { attempted: true, announced: false, ready: false, error: null, entries: 0 };
368
+ const reg = deps.registry !== undefined ? deps.registry : await defaultRegistry();
369
+ if (!reg) {
370
+ out.error = '注册表不可用 (拿不到 registry 实例)';
371
+ return out;
372
+ }
373
+ out.ready = reg.ready === true;
374
+ try {
375
+ const open = listAnnouncements(deps.home).filter((a) => a.buyerDid === buyerDid && a.status === 'open');
376
+ const svc = buildAnnouncementService(buyerDid, open);
377
+ out.entries = svc.capabilities?.length ?? 0;
378
+ const r = await reg.register(svc);
379
+ out.announced = r.ok === true;
380
+ out.error = r.ok ? null : String(r.error || '注册表拒绝');
381
+ }
382
+ catch (e) {
383
+ out.error = String(e?.message || e).slice(0, 160);
384
+ }
385
+ return out;
386
+ }
387
+ /** 从注册表条目里解析远端公告 (坏 JSON/坏字段 → 跳过, 不猜) */
388
+ function parseRegistryAnnouncements(services) {
389
+ const out = [];
390
+ for (const s of services || []) {
391
+ const caps = Array.isArray(s?.capabilities) ? s.capabilities : [];
392
+ const desc = String(s?.service?.description || '');
393
+ if (!caps.includes(exports.ANNOUNCE_CAPABILITY) && !desc.startsWith(exports.ANNOUNCE_DESC_PREFIX))
394
+ continue;
395
+ if (!desc.startsWith(exports.ANNOUNCE_DESC_PREFIX))
396
+ continue;
397
+ let arr = [];
398
+ try {
399
+ arr = JSON.parse(desc.slice(exports.ANNOUNCE_DESC_PREFIX.length));
400
+ }
401
+ catch {
402
+ continue;
403
+ }
404
+ if (!Array.isArray(arr))
405
+ continue;
406
+ for (const p of arr) {
407
+ const id = String(p?.announcementId || '');
408
+ if (!safeId(id) || !p?.capability)
409
+ continue;
410
+ out.push({
411
+ announcementId: id,
412
+ capability: String(p.capability),
413
+ instructionDigest: p.instructionDigest ? String(p.instructionDigest) : null,
414
+ instructionPreview: p.instructionPreview ? String(p.instructionPreview) : null,
415
+ buyerDid: String(p.buyerDid || s.agentId || ''),
416
+ buyerPublicKeyHex: p.buyerPublicKeyHex ? String(p.buyerPublicKeyHex) : null,
417
+ budget: p.budget && typeof p.budget === 'object' ? { maxAmount: String(p.budget.maxAmount), currency: p.budget.currency, network: String(p.budget.network) } : null,
418
+ deadline: Number.isFinite(Number(p.deadline)) ? Number(p.deadline) : null,
419
+ paymentMode: p.paymentMode ? String(p.paymentMode) : null,
420
+ status: ['open', 'claimed', 'cancelled'].includes(String(p.status)) ? String(p.status) : 'unknown',
421
+ createdAt: Number.isFinite(Number(p.createdAt)) ? Number(p.createdAt) : null,
422
+ signature: p.signature ? String(p.signature) : null,
423
+ registryAgentId: String(s.agentId || ''),
424
+ remote: true,
425
+ });
426
+ }
427
+ }
428
+ return out;
429
+ }
430
+ // ── board ───────────────────────────────────────────────────────────────────
431
+ function boardEntryOf(a, now, verified) {
432
+ const last = a.claims[a.claims.length - 1] ?? null;
433
+ return {
434
+ announcementId: a.announcementId,
435
+ capability: a.capability,
436
+ buyerDid: a.buyerDid,
437
+ buyerShort: String(a.buyerDid).slice(0, 28),
438
+ status: a.status,
439
+ budget: a.budget ?? null,
440
+ deadline: a.deadline ?? null,
441
+ deadlineInMs: a.deadline ? a.deadline - now : null,
442
+ createdAt: a.createdAt ?? null,
443
+ claimCount: a.claims.length,
444
+ claimedBy: last?.providerDid ?? null,
445
+ claimedAt: last?.claimedAt ?? null,
446
+ instructionDigest: a.instructionDigest,
447
+ instructionPreview: a.instructionPreview,
448
+ remote: false,
449
+ source: 'local',
450
+ signatureVerified: verified,
451
+ claimable: a.status === 'open' && (a.deadline ?? 0) > now,
452
+ };
453
+ }
454
+ function remoteBoardEntryOf(r, now, verified) {
455
+ return {
456
+ announcementId: r.announcementId,
457
+ capability: r.capability,
458
+ buyerDid: r.buyerDid,
459
+ buyerShort: String(r.buyerDid).slice(0, 28),
460
+ status: r.status,
461
+ budget: r.budget,
462
+ deadline: r.deadline,
463
+ deadlineInMs: r.deadline ? r.deadline - now : null,
464
+ createdAt: r.createdAt,
465
+ claimCount: 0, // 远端的认领数本机看不到 —— 不编
466
+ claimedBy: null,
467
+ claimedAt: null,
468
+ instructionDigest: r.instructionDigest,
469
+ instructionPreview: r.instructionPreview,
470
+ remote: true,
471
+ source: 'registry',
472
+ signatureVerified: verified,
473
+ claimable: r.status === 'open' && (r.deadline ?? 0) > now,
474
+ };
475
+ }
476
+ /**
477
+ * 板: 本地公告 + 注册表发现的远端公告, **按 announcementId 去重** (本地胜出)。
478
+ * 注册表读不到 → `registryError` 如实带出 (不是静默空板)。
479
+ */
480
+ async function listBoard(opts = {}) {
481
+ const now = opts.now ?? Date.now();
482
+ const local = listAnnouncements(opts.home);
483
+ const entries = [];
484
+ const seen = new Set();
485
+ const notes = [];
486
+ for (const a of local) {
487
+ const v = await verifyAnnouncementSignature(a);
488
+ entries.push(boardEntryOf(a, now, v));
489
+ seen.add(a.announcementId);
490
+ }
491
+ let remoteCount = 0;
492
+ let registryReady = false;
493
+ let registryError = null;
494
+ const duplicates = [];
495
+ if (!opts.localOnly) {
496
+ const reg = opts.registry !== undefined ? opts.registry : await defaultRegistry();
497
+ if (!reg) {
498
+ registryError = '注册表不可用 (拿不到 registry 实例)';
499
+ }
500
+ else {
501
+ registryReady = reg.ready === true;
502
+ try {
503
+ const services = await reg.list();
504
+ const remotes = parseRegistryAnnouncements(services || []);
505
+ for (const r of remotes) {
506
+ if (seen.has(r.announcementId)) {
507
+ if (!duplicates.includes(r.announcementId))
508
+ duplicates.push(r.announcementId);
509
+ continue;
510
+ }
511
+ seen.add(r.announcementId);
512
+ const v = await verifyAnnouncementSignature(r);
513
+ entries.push(remoteBoardEntryOf(r, now, v));
514
+ remoteCount++;
515
+ }
516
+ }
517
+ catch (e) {
518
+ registryError = String(e?.message || e).slice(0, 160);
519
+ }
520
+ }
521
+ }
522
+ if (!registryReady && !registryError)
523
+ notes.push('注册表未就绪 (OrbitDB 离线): 本地公告仍在, 远端公告可能看不全');
524
+ notes.push('instruction 正文只在本地公告文件里; 板上只有 sha256 摘要 + 预览');
525
+ const filtered = opts.openOnly ? entries.filter((e) => e.claimable) : entries;
526
+ return { entries: filtered, localCount: local.length, remoteCount, duplicates, registryReady, registryError, notes };
527
+ }
528
+ /** 有匹配的**可接单**公告 (给 `task send` 的"没 provider"提示用; 拿不到事实 → 空数组) */
529
+ async function findOpenAnnouncementsForCapability(capability, opts = {}) {
530
+ const cap = String(capability || '').trim().toLowerCase();
531
+ if (!cap)
532
+ return [];
533
+ try {
534
+ const view = await listBoard(opts);
535
+ return view.entries.filter((e) => e.claimable && String(e.capability).trim().toLowerCase() === cap);
536
+ }
537
+ catch {
538
+ return [];
539
+ }
540
+ }
541
+ /**
542
+ * 认领的价格声明 (只陈述事实): 声明了就说声明了多少, 没声明就说没声明 ——
543
+ * 绝不替 provider 编一个价格出来。
544
+ */
545
+ function claimPriceNote(c) {
546
+ return c.priceAmountAtomic
547
+ ? `声明价格 ${c.priceAmountAtomic} ${String(c.currency || 'USDC').toUpperCase()} (原子单位)`
548
+ : '未声明价格 (按公告预算执行; 不编价)';
549
+ }
550
+ function refuse(announcementId, reason, message, extra = {}) {
551
+ return {
552
+ ok: false, reason, announcementId, deliveredToBuyer: false, remote: false,
553
+ fundsMoved: false, paid: false, verified: false, message, ...extra,
554
+ };
555
+ }
556
+ /** 远端认领台账 (~/.bolloon/tasks/board/remote-claims.json) */
557
+ function readRemoteClaims(home) {
558
+ const list = readJson(path.join((0, exports.boardDir)(home), exports.REMOTE_CLAIMS_FILE));
559
+ return Array.isArray(list) ? list : [];
560
+ }
561
+ function saveRemoteClaim(claim, home) {
562
+ const f = path.join((0, exports.boardDir)(home), exports.REMOTE_CLAIMS_FILE);
563
+ const list = readRemoteClaims(home);
564
+ list.push(claim);
565
+ return writeJson(f, list);
566
+ }
567
+ /**
568
+ * 一个 provider 接单。记录: 认领者 DID + 时间 + 声明价格。
569
+ * **一律拒绝并给原因**的情形: 重复认领 (已被认领 / 同一 provider 再claim) · 已取消 · 不存在 ·
570
+ * 签名验不过 / 正文摘要对不上 · 已过期。
571
+ */
572
+ async function claimAnnouncement(announcementId, input, opts = {}) {
573
+ const now = opts.now ?? Date.now();
574
+ const id = String(announcementId || '').trim();
575
+ if (!safeId(id))
576
+ return refuse(id, 'invalid_id', `announcementId 不合法: ${id || '(空)'} (形如 ann-xxxxxxxxxxxxxxxx)`);
577
+ const providerDid = String(input.providerDid || '').trim();
578
+ if (!providerDid)
579
+ return refuse(id, 'invalid_id', '接单必须有认领者 DID (没有身份 → 不接单, 不假装)');
580
+ // ① 本地公告
581
+ const local = readAnnouncement(id, opts.home);
582
+ if (local) {
583
+ if (local.status === 'claimed') {
584
+ const first = local.claims[0] ?? null;
585
+ return refuse(id, 'already_claimed', `这条公告已经被认领了 (认领者 ${first?.providerDid || '未知'}, ${first ? new Date(first.claimedAt).toISOString() : ''}); 同一 id 不重复认领`, { existingClaim: first, status: local.status, claim: first ?? undefined });
586
+ }
587
+ if (local.status === 'cancelled') {
588
+ return refuse(id, 'cancelled', `这条公告已取消 (${local.cancelReason || '未给原因'}) → 不接单`, { status: local.status });
589
+ }
590
+ if (!local.instruction || sha256Hex(local.instruction) !== local.instructionDigest) {
591
+ return refuse(id, 'instruction_digest_mismatch', '公告正文与签名时的摘要对不上 (文件被改过) → 拒 (不拿不一致的事实往下走)', { status: local.status });
592
+ }
593
+ const sigOk = await verifyAnnouncementSignature(local);
594
+ if (!sigOk) {
595
+ return refuse(id, 'signature_invalid', '公告签名验不过 (缺签名/公钥非法/被改过) → 拒 (不接没签名的单)', { status: local.status });
596
+ }
597
+ if (Number(local.deadline) <= now) {
598
+ return refuse(id, 'deadline_expired', `公告已过期 (截止 ${new Date(Number(local.deadline)).toISOString()}) → 不接单`, { status: local.status });
599
+ }
600
+ if (local.claims.some((c) => c.providerDid === providerDid)) {
601
+ const mine = local.claims.find((c) => c.providerDid === providerDid);
602
+ return refuse(id, 'already_claimed', `你已经认领过这条公告 (${new Date(mine.claimedAt).toISOString()}) → 幂等: 不重复认领`, { existingClaim: mine, status: local.status });
603
+ }
604
+ const claim = {
605
+ announcementId: id,
606
+ providerDid,
607
+ providerPublicKeyHex: input.providerPublicKeyHex ?? null,
608
+ claimedAt: now,
609
+ priceAmountAtomic: input.priceAmountAtomic ? String(input.priceAmountAtomic) : null,
610
+ currency: String(input.currency || local.budget?.currency || 'USDC').toUpperCase(),
611
+ network: String(input.network || local.budget?.network || ''),
612
+ deliveredToBuyer: false,
613
+ };
614
+ if (input.signerKeypair) {
615
+ try {
616
+ const { signTaskEnvelope } = await Promise.resolve().then(() => __importStar(require('./task-contract.js')));
617
+ const s = await signTaskEnvelope({ ...claim, signature: undefined }, input.signerKeypair);
618
+ claim.signature = String(s.signature || '') || null;
619
+ }
620
+ catch {
621
+ claim.signature = null;
622
+ }
623
+ }
624
+ const next = { ...local, status: 'claimed', updatedAt: now, claims: [...local.claims, claim] };
625
+ const w = saveAnnouncement(next, opts.home);
626
+ if (!w.ok)
627
+ return refuse(id, 'not_announcement', `认领落盘失败: ${w.error}`, { status: local.status });
628
+ // 脉冲: 复用既有经济事件 task_accepted (匿名)
629
+ try {
630
+ const rec = opts.recordEvent || (async (ev) => {
631
+ const { recordNetworkEvent } = await Promise.resolve().then(() => __importStar(require('./network-pulse.js')));
632
+ return await recordNetworkEvent(ev, opts.home);
633
+ });
634
+ await rec({ type: 'task_accepted', capability: local.capability, did: providerDid, taskId: id, at: now });
635
+ }
636
+ catch { /* 脉冲失败不影响认领事实 */ }
637
+ return {
638
+ ok: true, reason: 'claimed', announcementId: id, claim, existingClaim: null,
639
+ status: 'claimed', deliveredToBuyer: false, remote: false,
640
+ fundsMoved: false, paid: false, verified: false,
641
+ message: `已认领 ${id} (${local.capability}); ${claimPriceNote(claim)}; 只记认领事实: 不执行、不付款、不标 verified`,
642
+ priceNote: claimPriceNote(claim),
643
+ file: `~/.bolloon/tasks/board/${id}.json`,
644
+ };
645
+ }
646
+ // ② 远端公告 (注册表里发现的) —— 认领只落本机台账 (本版没有投递通道)
647
+ const reg = opts.registry !== undefined ? opts.registry : await defaultRegistry();
648
+ let remote = null;
649
+ let registryError = null;
650
+ if (reg) {
651
+ try {
652
+ const services = await reg.list();
653
+ remote = parseRegistryAnnouncements(services || []).find((r) => r.announcementId === id) ?? null;
654
+ }
655
+ catch (e) {
656
+ registryError = String(e?.message || e).slice(0, 160);
657
+ }
658
+ }
659
+ else
660
+ registryError = '注册表不可用';
661
+ if (!remote) {
662
+ return refuse(id, 'not_found', `板上没有这个公告: ${id} (本地 + 注册表都查过)${registryError ? ` — 注册表读取失败: ${registryError}` : ''}`);
663
+ }
664
+ if (remote.status === 'claimed')
665
+ return refuse(id, 'already_claimed', `远端公告显示已被认领 (status=claimed) → 不重复认领`, { remote: true, status: remote.status });
666
+ if (remote.status === 'cancelled')
667
+ return refuse(id, 'cancelled', '远端公告已取消 → 不接单', { remote: true, status: remote.status });
668
+ if (remote.status !== 'open')
669
+ return refuse(id, 'not_found', `远端公告状态未知 (${remote.status}) → 拒 (不拿不确定的事实接单)`, { remote: true, status: remote.status });
670
+ if (remote.deadline !== null && remote.deadline <= now)
671
+ return refuse(id, 'deadline_expired', `远端公告已过期 (截止 ${new Date(remote.deadline).toISOString()}) → 不接单`, { remote: true, status: remote.status });
672
+ const remoteSigOk = await verifyAnnouncementSignature(remote);
673
+ if (!remoteSigOk)
674
+ return refuse(id, 'signature_invalid', '远端公告签名验不过 → 拒 (不接没签名的单)', { remote: true, status: remote.status });
675
+ const already = readRemoteClaims(opts.home).find((c) => c.announcementId === id && c.providerDid === providerDid);
676
+ if (already)
677
+ return refuse(id, 'already_claimed', `你已经认领过这条远端公告 (${new Date(already.claimedAt).toISOString()}) → 幂等: 不重复认领`, { remote: true, existingClaim: already, status: remote.status });
678
+ const claim = {
679
+ announcementId: id,
680
+ providerDid,
681
+ providerPublicKeyHex: input.providerPublicKeyHex ?? null,
682
+ claimedAt: now,
683
+ priceAmountAtomic: input.priceAmountAtomic ? String(input.priceAmountAtomic) : null,
684
+ currency: String(input.currency || remote.budget?.currency || 'USDC').toUpperCase(),
685
+ network: String(input.network || remote.budget?.network || ''),
686
+ deliveredToBuyer: false,
687
+ };
688
+ const w = saveRemoteClaim(claim, opts.home);
689
+ if (!w.ok)
690
+ return refuse(id, 'not_announcement', `远端认领台账落盘失败: ${w.error}`, { remote: true });
691
+ return {
692
+ ok: true, reason: 'claimed', announcementId: id, claim, existingClaim: null, status: remote.status,
693
+ deliveredToBuyer: false, remote: true,
694
+ fundsMoved: false, paid: false, verified: false,
695
+ message: `已认领远端公告 ${id} (${claimPriceNote(claim)}; 只落本机台账: **没有投递给买方**; 真交接走 bolloon task send → 对方 accept)`,
696
+ priceNote: claimPriceNote(claim),
697
+ file: `~/.bolloon/tasks/board/${exports.REMOTE_CLAIMS_FILE}`,
698
+ };
699
+ }
700
+ // ── cancel (本地事实; 没有 CLI 入口, 由买方在本地调用) ──────────────────────
701
+ /**
702
+ * 买方撤下自己的公告 (本地事实) + 立刻把注册表条目刷成新事实 (否则远端会短暂看到已撤销的公告)。
703
+ * **已认领的公告不许取消** —— 认领是别人已经在走的事实, 静默取消会让对方白干 (拒, 并给原因)。
704
+ * 没有 CLI 入口 (任务书只点名 publish/board/claim); 这里导出是为了让"已取消的公告被拒接单"这条
705
+ * 负控制走**真代码**, 而不是手工改文件。
706
+ */
707
+ async function cancelAnnouncement(announcementId, reason, opts = {}) {
708
+ const now = opts.now ?? Date.now();
709
+ const cur = readAnnouncement(announcementId, opts.home);
710
+ if (!cur)
711
+ return { ok: false, error: `板上没有这条公告: ${announcementId}` };
712
+ if (cur.status === 'claimed')
713
+ return { ok: false, error: `已认领的公告不许取消 (认领者 ${cur.claims[0]?.providerDid || '未知'} 已在执行; 静默取消 = 让人白干)` };
714
+ if (cur.status === 'cancelled')
715
+ return { ok: true, announcement: cur };
716
+ const next = { ...cur, status: 'cancelled', updatedAt: now, cancelledAt: now, cancelReason: String(reason || '').slice(0, 200) };
717
+ const w = saveAnnouncement(next, opts.home);
718
+ if (!w.ok)
719
+ return { ok: false, error: w.error };
720
+ const registry = await refreshRegistryAnnouncements(cur.buyerDid, { home: opts.home, registry: opts.registry });
721
+ return { ok: true, announcement: next, registry };
722
+ }
723
+ /**
724
+ * 释放裁决 (冻结顺序, 每条拒绝都给机器可读原因):
725
+ * 已释放 → 未认领 → 争议中 → **未交付不得释放** → local-dev 永远不是链上结算 →
726
+ * **未链上结算 (chainSettled!==true 或缺 txHash) 不得标 verified** → 才 RELEASE_OK。
727
+ */
728
+ function decideAnnouncementRelease(input) {
729
+ const ev = (s) => `证据: ${s}`;
730
+ const id = String(input.announcementId || '');
731
+ const chainSettled = input.chainSettled === true;
732
+ const txHash = String(input.txHash || '');
733
+ const evidence = [
734
+ ev(`status=${input.status}`),
735
+ ev(`delivered=${input.delivery?.delivered === true}`),
736
+ ev(`contentHash=${input.delivery?.contentHash ? String(input.delivery.contentHash).slice(0, 16) : '(无)'}`),
737
+ ev(`paymentMode=${input.paymentMode}`),
738
+ ev(`chainSettled=${chainSettled}`),
739
+ ev(`txHash=${txHash ? `${txHash.slice(0, 18)}…` : '(无)'}`),
740
+ ];
741
+ const mustNotRepay = input.alreadyReleased === true || chainSettled;
742
+ const no = (code, reason) => ({ announcementId: id, action: 'refuse', code, reason, canMarkVerified: false, mustNotRepay, evidence, fundsMoved: false });
743
+ if (input.alreadyReleased === true)
744
+ return no('ALREADY_RELEASED', '这条已经释放过 (幂等: 不重复释放/不重复付款)');
745
+ if (input.status !== 'claimed')
746
+ return no('NOT_CLAIMED', `还没人认领 (status=${input.status}) → 没有可释放的对象`);
747
+ if (input.disputeOpen === true)
748
+ return no('DISPUTE_OPEN', '争议中: 不自动重付、不标 verified、不静默关闭 (先 resolveDispute 带证据)');
749
+ if (input.delivery?.delivered !== true)
750
+ return no('NOT_DELIVERED', '未交付不得释放 (没有交付事实 → 钱不动)');
751
+ if (String(input.paymentMode) === 'local-dev')
752
+ return no('LOCAL_DEV_NOT_CHAIN', 'local-dev 结算**永远不是**链上结算 → 不释放、不标 verified');
753
+ if (!chainSettled || !txHash)
754
+ return no('NOT_CHAIN_SETTLED', '未链上结算 (chainSettled!==true 或缺 txHash) → 不释放、不标 verified');
755
+ return {
756
+ announcementId: id, action: 'release', code: 'RELEASE_OK',
757
+ reason: '已认领 + 已交付 + 链上结算事实齐 (txHash 在手) → 可释放; 验真可标 verified',
758
+ canMarkVerified: true, mustNotRepay, evidence, fundsMoved: false,
759
+ };
760
+ }
761
+ //# sourceMappingURL=task-board.js.map