@bolloon/bolloon-agent 0.4.31 → 0.4.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/chain/chain-config.js +504 -0
- package/dist/agents/chain/chain-index-query.js +135 -0
- package/dist/agents/chain/chain-indexer.js +1045 -0
- package/dist/agents/chain/chain-settlement.js +284 -0
- package/dist/agents/chain/chain-state-store.js +245 -0
- package/dist/agents/chain/chain-wallet.js +198 -0
- package/dist/agents/chain/escrow-client.js +433 -0
- package/dist/agents/chain/explorer.js +52 -0
- package/dist/agents/chain/index.js +22 -0
- package/dist/agents/chain/onchain-trade.js +705 -0
- package/dist/agents/network-pulse.js +676 -2
- package/dist/agents/task/task-onchain-runner.js +302 -0
- package/dist/agents/task/task-runner.js +27 -3
- package/dist/agents/task-board.js +702 -0
- package/dist/agents/task-group.js +567 -0
- package/dist/agents/x402/paid-info-store.js +75 -6
- package/dist/cli/commands/chain.js +926 -0
- package/dist/cli/commands/index.js +12 -2
- package/dist/cli/commands/tasks.js +627 -6
- package/dist/cli/mcp/server.js +4 -1
- package/dist/cli/mcp/tools.js +348 -5
- package/dist/cli/protocol-envelope.js +16 -0
- package/dist/cli-entry.js +6 -1
- package/dist/electron-build/agents/chain/chain-config.js +556 -0
- package/dist/electron-build/agents/chain/chain-config.js.map +1 -0
- package/dist/electron-build/agents/chain/chain-index-query.js +145 -0
- package/dist/electron-build/agents/chain/chain-index-query.js.map +1 -0
- package/dist/electron-build/agents/chain/chain-indexer.js +1095 -0
- package/dist/electron-build/agents/chain/chain-indexer.js.map +1 -0
- package/dist/electron-build/agents/chain/chain-settlement.js +291 -0
- package/dist/electron-build/agents/chain/chain-settlement.js.map +1 -0
- package/dist/electron-build/agents/chain/escrow-client.js +443 -0
- package/dist/electron-build/agents/chain/escrow-client.js.map +1 -0
- package/dist/electron-build/agents/chain/explorer.js +59 -0
- package/dist/electron-build/agents/chain/explorer.js.map +1 -0
- package/dist/electron-build/agents/local-signer.js +101 -0
- package/dist/electron-build/agents/local-signer.js.map +1 -0
- package/dist/electron-build/agents/network-pulse.js +693 -3
- package/dist/electron-build/agents/network-pulse.js.map +1 -1
- package/dist/electron-build/agents/task/task-runner.js +27 -3
- package/dist/electron-build/agents/task/task-runner.js.map +1 -1
- package/dist/electron-build/agents/task-board.js +761 -0
- package/dist/electron-build/agents/task-board.js.map +1 -0
- package/dist/electron-build/agents/task-contract.js +382 -0
- package/dist/electron-build/agents/task-contract.js.map +1 -0
- package/dist/electron-build/agents/x402/paid-info-store.js +76 -6
- package/dist/electron-build/agents/x402/paid-info-store.js.map +1 -1
- package/dist/web/chain-explorer.js +617 -0
- package/dist/web/explorer.html +163 -0
- package/dist/web/index.html +5 -0
- package/dist/web/routes-chain-index.js +108 -0
- package/dist/web/routes-onchain-trade.js +99 -0
- package/dist/web/server.js +13 -0
- package/package.json +2 -1
- package/scripts/build-web.ts +20 -1
|
@@ -40,9 +40,37 @@ ${title('bolloon task')}
|
|
|
40
40
|
bolloon task reject <requestId|taskId> --reason "<原因>"
|
|
41
41
|
bolloon task complete|cancel 仍未实现 → 如实报 C_NOT_IMPLEMENTED
|
|
42
42
|
|
|
43
|
+
── 任务对外发布 + 接单 (C1/C2, 2026-09-23: 公告板) ──
|
|
44
|
+
bolloon task publish --capability research --instruction "调研 X" --budget 0.05 \\
|
|
45
|
+
[--currency USDC] [--network base-sepolia] [--deadline +24h] [--reply-to http://me:port]
|
|
46
|
+
发布一条**待接单**任务: 落盘 ~/.bolloon/tasks/board/<announcementId>.json +
|
|
47
|
+
向 agent-registry 公告 (service.name = task.announce) + 记脉冲事件 task_announced;
|
|
48
|
+
输出稳定 announcementId (同一 能力+正文+买方+预算 → 同一个 id, 重发幂等)
|
|
49
|
+
bolloon task board [--json] [--capability <名>] [--open] [--local]
|
|
50
|
+
板上可接单的任务: **本地公告 + 注册表发现的远端公告**, 按 announcementId 去重
|
|
51
|
+
bolloon task claim <announcementId> [--price 0.02] [--group <群链接|groupId>]
|
|
52
|
+
provider 接单: 记认领者 DID + 时间 + 声明价格 (不执行、不付款、不标 verified);
|
|
53
|
+
重复认领 / 已取消 / 不存在的 id → 一律拒绝并给原因;
|
|
54
|
+
带 --group 时: 往群里发一条极短接单声明 (群非法 → 整条命令拒绝, 不静默降级)
|
|
55
|
+
|
|
56
|
+
── 群聊通道 (C7, 2026-09-23: 过程留痕) ──
|
|
57
|
+
bolloon task announce --group <群链接|groupId> [--announcement-id <id>] [--capability <名>]
|
|
58
|
+
[--round <期号>] [--criteria "<验收判据摘要>"] [--from <短显示名>] [--json]
|
|
59
|
+
把板上**一条待接单公告**压成一行极短事实 (期号 · capability · 预算 · 判据摘要 · 公告 id) 发进群;
|
|
60
|
+
正文不进群; 缺 --group / 群非法 / 本机没这个群 → 拒绝执行 (绝不降级为只写本地)
|
|
61
|
+
bolloon task trail --group <群链接|groupId> [--announcement-id <id>] [--limit 300] [--json]
|
|
62
|
+
从群消息读回本期过程留痕 (公告/接单/交付/初筛/终审) → 按时间排序的时间线, 每条带时间 + 发送者标记
|
|
63
|
+
bolloon task post --kind deliver|screen|final --group <群链接|groupId> --announcement-id <id> \\
|
|
64
|
+
[--hash sha256:<hex>] [--bytes <n>] [--checks "渠道结构=pass,价格带=fail"] [--verdict accept|reject|unknown]
|
|
65
|
+
[--round <期号>] [--from <短显示名>] [--json]
|
|
66
|
+
交付只贴哈希 · 初筛逐条结果 · 终审结论; 群消息一律无钱包地址/DID/peerId/multiaddr/IP/私钥形态
|
|
67
|
+
(命中任一条 → 拒发并报出命中的规则)
|
|
68
|
+
|
|
43
69
|
选项: --json · --quiet · --request-id <id> · --timeout <ms> · --resume <goalId> (同 bolloon task)
|
|
44
70
|
说明: 收件箱落盘 ~/.bolloon/tasks/inbox/; 本机发出的任务台账 ~/.bolloon/tasks/local/;
|
|
45
|
-
结果 ~/.bolloon/tasks/results/, 交付正文 ~/.bolloon/tasks/bodies/ (私有层, 不进 stdout)
|
|
71
|
+
结果 ~/.bolloon/tasks/results/, 交付正文 ~/.bolloon/tasks/bodies/ (私有层, 不进 stdout);
|
|
72
|
+
任务公告 ~/.bolloon/tasks/board/ (正文只在本地, 注册表里只有 sha256 摘要 + 60 字预览);
|
|
73
|
+
群里的过程痕迹**只在群里** (群消息就是留痕本体, 本机不留影子副本); 结算仍只在链上。
|
|
46
74
|
`;
|
|
47
75
|
/** 交易记录判定 (P1 §5.1 红线: local-dev 永不 verified; 成功判据见 §7.1) */
|
|
48
76
|
function judge(rec) {
|
|
@@ -135,6 +163,12 @@ export async function taskCommand(flags) {
|
|
|
135
163
|
case 'reject': return taskReject(flags);
|
|
136
164
|
case 'complete': return taskComplete(flags);
|
|
137
165
|
case 'cancel': return taskCancel(flags);
|
|
166
|
+
case 'publish': return taskPublish(flags);
|
|
167
|
+
case 'board': return taskBoard(flags);
|
|
168
|
+
case 'claim': return taskClaim(flags);
|
|
169
|
+
case 'announce': return taskAnnounce(flags);
|
|
170
|
+
case 'trail': return taskTrail(flags);
|
|
171
|
+
case 'post': return taskPost(flags);
|
|
138
172
|
default:
|
|
139
173
|
return {
|
|
140
174
|
envelope: failEnvelope('INVALID_ARGUMENT', sub ? `未知 task 子命令: ${sub}` : '缺少 task 子命令', { usage: plain(TASK_USAGE.trim()) }, [], 'needs_human'),
|
|
@@ -735,6 +769,32 @@ function transferView(t) {
|
|
|
735
769
|
return t ? { kind: t.kind, target: t.target, ok: t.ok, elapsedMs: t.elapsedMs, duplicate: t.duplicate === true, error: t.error || null, detail: t.detail || null } : null;
|
|
736
770
|
}
|
|
737
771
|
// ── send ────────────────────────────────────────────────────────────────────
|
|
772
|
+
/**
|
|
773
|
+
* 没有匹配的 provider 时: 板上若有**可接单**的公告 → 给可操作提示 (指向 board/claim)。
|
|
774
|
+
* 拿不到板上的事实 → `hint:null` + 如实带 `boardError` (不猜、不假装有公告)。
|
|
775
|
+
*/
|
|
776
|
+
async function boardHintForCapability(capability) {
|
|
777
|
+
try {
|
|
778
|
+
const { findOpenAnnouncementsForCapability } = await import('../../agents/task-board.js');
|
|
779
|
+
const open = await findOpenAnnouncementsForCapability(capability);
|
|
780
|
+
if (!open.length)
|
|
781
|
+
return { count: 0, announcements: [], hint: null, boardError: null };
|
|
782
|
+
const first = open[0];
|
|
783
|
+
return {
|
|
784
|
+
count: open.length,
|
|
785
|
+
announcements: open.slice(0, 5).map((a) => ({
|
|
786
|
+
announcementId: a.announcementId, capability: a.capability, status: a.status,
|
|
787
|
+
budget: a.budget, deadline: a.deadline, buyer: a.buyerShort, remote: a.remote,
|
|
788
|
+
signatureVerified: a.signatureVerified,
|
|
789
|
+
})),
|
|
790
|
+
hint: `板上有 ${open.length} 条可接单的 '${capability}' 任务: 先 bolloon task board 看全, 或直接 bolloon task claim ${first.announcementId}`,
|
|
791
|
+
boardError: null,
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
catch (e) {
|
|
795
|
+
return { count: 0, announcements: [], hint: null, boardError: String(e?.message || e).slice(0, 160) };
|
|
796
|
+
}
|
|
797
|
+
}
|
|
738
798
|
async function taskSend(flags) {
|
|
739
799
|
const instruction = opt(flags, '--instruction') || flags.positionals.slice(1).join(' ').trim();
|
|
740
800
|
const capability = opt(flags, '--capability');
|
|
@@ -810,18 +870,29 @@ async function taskSend(flags) {
|
|
|
810
870
|
providerDid = providerDid || resolved.target.providerDid || '';
|
|
811
871
|
}
|
|
812
872
|
else if (via === 'gateway') {
|
|
873
|
+
const boardHint = await boardHintForCapability(capability);
|
|
813
874
|
return {
|
|
814
|
-
envelope: failEnvelope('CAPABILITY_NOT_FOUND', `注册表里没有能力 '${capability}' 的 provider (registry ${resolved.registryReady ? '就绪' : '未就绪'}, ${resolved.candidates} 条候选)`, { capability, registryReady: resolved.registryReady, candidates: resolved.candidates, via: 'gateway' }, [], 'redefine_capability'),
|
|
815
|
-
human:
|
|
875
|
+
envelope: failEnvelope('CAPABILITY_NOT_FOUND', `注册表里没有能力 '${capability}' 的 provider (registry ${resolved.registryReady ? '就绪' : '未就绪'}, ${resolved.candidates} 条候选)${boardHint.hint ? `;${boardHint.hint}` : ''}`, { capability, registryReady: resolved.registryReady, candidates: resolved.candidates, via: 'gateway', board: boardHint }, [], 'redefine_capability'),
|
|
876
|
+
human: [
|
|
877
|
+
`${title('bolloon task send')}`,
|
|
878
|
+
` 注册表里没有这个能力的 provider, gateway 传输没有目标`,
|
|
879
|
+
...(boardHint.hint ? [`\n ${hint(boardHint.hint)}`] : []),
|
|
880
|
+
].join('\n'),
|
|
816
881
|
};
|
|
817
882
|
}
|
|
818
883
|
else {
|
|
819
884
|
const joined = resolved.registryReady;
|
|
885
|
+
const boardHint = await boardHintForCapability(capability);
|
|
820
886
|
return {
|
|
821
887
|
envelope: failEnvelope(joined ? 'CAPABILITY_NOT_FOUND' : 'NETWORK_NOT_JOINED', joined
|
|
822
|
-
? `注册表里没有能力 '${capability}' 的 provider (${resolved.candidates} 条候选, 且都没有 endpoint)`
|
|
823
|
-
:
|
|
824
|
-
human:
|
|
888
|
+
? `注册表里没有能力 '${capability}' 的 provider (${resolved.candidates} 条候选, 且都没有 endpoint)${boardHint.hint ? `;${boardHint.hint}` : ''}`
|
|
889
|
+
: `本机 registry 未就绪 (没有入网/没拉过任何服务声明), 无法解析目标`, { capability, registryReady: joined, candidates: resolved.candidates, board: boardHint, howTo: '用 --endpoint http://host:port 显式给目标, 或先 bolloon network join / bolloon agent register' }, [], joined ? 'redefine_capability' : 'rejoin_network'),
|
|
890
|
+
human: [
|
|
891
|
+
`${title('bolloon task send')}`,
|
|
892
|
+
` ${joined ? '注册表里没有这个能力' : '本机还没入网, 也没有显式 --endpoint'}`,
|
|
893
|
+
...(boardHint.hint ? [`\n ${hint(boardHint.hint)}`] : []),
|
|
894
|
+
`\n${hint('示例: bolloon task send --capability research --instruction "…" --endpoint http://127.0.0.1:54901')}`,
|
|
895
|
+
].join('\n'),
|
|
825
896
|
};
|
|
826
897
|
}
|
|
827
898
|
}
|
|
@@ -1216,6 +1287,556 @@ async function taskReject(flags) {
|
|
|
1216
1287
|
].join('\n'),
|
|
1217
1288
|
};
|
|
1218
1289
|
}
|
|
1290
|
+
// ── publish / board / claim (C1/C2 任务对外发布 + 接单, 2026-09-23) ──────────
|
|
1291
|
+
/** 板上一行的对外投影 (不含正文; 远端认领数的诚实标注) */
|
|
1292
|
+
function boardEntryView(e) {
|
|
1293
|
+
return {
|
|
1294
|
+
announcementId: e.announcementId,
|
|
1295
|
+
capability: e.capability,
|
|
1296
|
+
buyer: e.buyerShort,
|
|
1297
|
+
status: e.status,
|
|
1298
|
+
budget: e.budget,
|
|
1299
|
+
deadline: e.deadline,
|
|
1300
|
+
deadlineInMs: e.deadlineInMs,
|
|
1301
|
+
claimCount: e.claimCount,
|
|
1302
|
+
claimedBy: e.claimedBy,
|
|
1303
|
+
claimedAt: e.claimedAt,
|
|
1304
|
+
remote: e.remote,
|
|
1305
|
+
source: e.source,
|
|
1306
|
+
signatureVerified: e.signatureVerified,
|
|
1307
|
+
claimable: e.claimable,
|
|
1308
|
+
instructionDigest: e.instructionDigest,
|
|
1309
|
+
instructionPreview: e.instructionPreview,
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
async function taskPublish(flags) {
|
|
1313
|
+
const capability = opt(flags, '--capability');
|
|
1314
|
+
const instruction = opt(flags, '--instruction') || flags.positionals.slice(1).join(' ').trim();
|
|
1315
|
+
const budgetHuman = opt(flags, '--budget');
|
|
1316
|
+
if (!capability || !instruction) {
|
|
1317
|
+
return {
|
|
1318
|
+
envelope: failEnvelope('INVALID_ARGUMENT', '缺少 --capability 或 --instruction (公告必须有能力和任务正文)', { usage: plain(TASK_USAGE.trim()), got: { capability: capability || null, instruction: !!instruction } }, [], 'needs_human'),
|
|
1319
|
+
human: `${TASK_USAGE}\n${hint('示例: bolloon task publish --capability research --instruction "调研某类厨房用品的日本市场" --budget 0.05')}`,
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
const currency = String(opt(flags, '--currency') || 'USDC').toUpperCase();
|
|
1323
|
+
if (currency !== 'USDC' && currency !== 'ETH') {
|
|
1324
|
+
return { envelope: failEnvelope('INVALID_ARGUMENT', `--currency 只支持 USDC/ETH (收到 ${currency})`, { currency }, [], 'needs_human'), human: TASK_USAGE };
|
|
1325
|
+
}
|
|
1326
|
+
const network = opt(flags, '--network') || 'base-sepolia';
|
|
1327
|
+
if (budgetHuman === undefined) {
|
|
1328
|
+
return {
|
|
1329
|
+
envelope: failEnvelope('INVALID_ARGUMENT', '缺少 --budget (待接单的公告必须带预算: 没有预算就没有可核验的委托口径)', { usage: plain(TASK_USAGE.trim()), accepted: ['--budget 0.05'] }, [], 'needs_human'),
|
|
1330
|
+
human: `${TASK_USAGE}\n${hint('示例: bolloon task publish --capability research --instruction "…" --budget 0.05')}`,
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
const atomic = toAtomic(budgetHuman, currency === 'USDC' ? 6 : 18);
|
|
1334
|
+
if (!atomic) {
|
|
1335
|
+
return {
|
|
1336
|
+
envelope: failEnvelope('INVALID_ARGUMENT', `--budget 必须是正数 (收到 ${budgetHuman}); 原子单位串由本命令换算, 不接受 0/负/超精度`, { budget: budgetHuman, currency, accepted: ['--budget 0.05'] }, [], 'needs_human'),
|
|
1337
|
+
human: TASK_USAGE,
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1340
|
+
const modeRaw = opt(flags, '--mode') || 'policy';
|
|
1341
|
+
const { isPaymentMode } = await import('../../agents/task-contract.js');
|
|
1342
|
+
if (!isPaymentMode(modeRaw)) {
|
|
1343
|
+
return {
|
|
1344
|
+
envelope: failEnvelope('INVALID_ARGUMENT', `--mode 非法: ${modeRaw} (要 manual|policy|autonomous|agent-authorized)`, { mode: modeRaw }, [], 'needs_human'),
|
|
1345
|
+
human: TASK_USAGE,
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
const dl = parseDeadline(opt(flags, '--deadline'));
|
|
1349
|
+
if (typeof dl === 'string') {
|
|
1350
|
+
return { envelope: failEnvelope('INVALID_ARGUMENT', dl, { deadline: opt(flags, '--deadline') }, [], 'needs_human'), human: TASK_USAGE };
|
|
1351
|
+
}
|
|
1352
|
+
const s = await requireSigner('bolloon task publish');
|
|
1353
|
+
if ('fail' in s)
|
|
1354
|
+
return s.fail;
|
|
1355
|
+
const signer = s.signer;
|
|
1356
|
+
const { publishAnnouncement } = await import('../../agents/task-board.js');
|
|
1357
|
+
const r = await publishAnnouncement({
|
|
1358
|
+
capability, instruction,
|
|
1359
|
+
buyerDid: signer.did,
|
|
1360
|
+
buyerPublicKeyHex: signer.publicKeyHex,
|
|
1361
|
+
budget: { maxAmount: atomic, currency, network },
|
|
1362
|
+
deadline: dl,
|
|
1363
|
+
paymentMode: modeRaw,
|
|
1364
|
+
replyTo: opt(flags, '--reply-to') || null,
|
|
1365
|
+
signerKeypair: signer.keypair,
|
|
1366
|
+
});
|
|
1367
|
+
if (!r.ok || !r.announcement) {
|
|
1368
|
+
return {
|
|
1369
|
+
envelope: failEnvelope('INTERNAL_ERROR', `公告没发布成功: ${r.error || '未知原因'}`, { capability, announcementId: null, paid: false, fundsMoved: false }, [], 'needs_human'),
|
|
1370
|
+
human: `${title('bolloon task publish')}\n 没发布成功: ${r.error || '未知原因'}`,
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
const a = r.announcement;
|
|
1374
|
+
const data = {
|
|
1375
|
+
announcementId: a.announcementId,
|
|
1376
|
+
capability: a.capability,
|
|
1377
|
+
buyerDid: a.buyerDid,
|
|
1378
|
+
status: a.status,
|
|
1379
|
+
budget: a.budget,
|
|
1380
|
+
deadline: a.deadline,
|
|
1381
|
+
paymentMode: a.paymentMode,
|
|
1382
|
+
instructionDigest: a.instructionDigest,
|
|
1383
|
+
instructionPreview: a.instructionPreview,
|
|
1384
|
+
signed: r.signed,
|
|
1385
|
+
duplicate: r.dup,
|
|
1386
|
+
registry: r.registry,
|
|
1387
|
+
pulse: r.pulse,
|
|
1388
|
+
file: r.file || `~/.bolloon/tasks/board/${a.announcementId}.json`,
|
|
1389
|
+
registryService: 'task.announce (agent-registry; description 里只有摘要+预览, 没有正文)',
|
|
1390
|
+
paid: false,
|
|
1391
|
+
fundsMoved: false,
|
|
1392
|
+
note: '公告只是"把待接单的任务放出去": 不执行、不付款、不产生交易; 接单: bolloon task claim <announcementId>',
|
|
1393
|
+
};
|
|
1394
|
+
const human = [
|
|
1395
|
+
title('bolloon task publish'),
|
|
1396
|
+
line('公告号', a.announcementId),
|
|
1397
|
+
line('capability', a.capability),
|
|
1398
|
+
line('预算', a.budget ? `≤ ${a.budget.maxAmount} 原子 (${a.budget.currency} @ ${a.budget.network})` : '(无)'),
|
|
1399
|
+
line('截止时间', new Date(a.deadline).toISOString()),
|
|
1400
|
+
line('签名', r.signed ? '已签名 (覆盖公开载荷: 能力/预算/截止/正文摘要)' : '**没有签名** (没有本机身份 → 接单会被拒)'),
|
|
1401
|
+
line('注册表', r.registry.attempted ? (r.registry.announced ? `已公告 (${r.registry.entries} 条 capability)` : `没公告: ${r.registry.error || '未知'}`) : '没有尝试 (离线)'),
|
|
1402
|
+
line('脉冲事件', r.pulse.attempted ? (r.pulse.ok ? 'task_announced 已记' : `没记: ${r.pulse.reason || '未知'}`) : '没有尝试 (离线)'),
|
|
1403
|
+
r.dup ? line('重复发布', '同一 (能力+正文+买方+预算) → 复用既有公告 (幂等), 没覆盖既有认领') : '',
|
|
1404
|
+
line('本次付款', '没有 (发布公告不付款)'),
|
|
1405
|
+
`\n ${hint('下一步: 别人看板 bolloon task board · 接单 bolloon task claim ' + a.announcementId)}`,
|
|
1406
|
+
].filter(Boolean).join('\n');
|
|
1407
|
+
return {
|
|
1408
|
+
envelope: okEnvelope('OK', `已发布待接单任务 ${a.announcementId} (${a.capability})${r.dup ? ' — 复用既有公告 (幂等)' : ''}`, data, [a.announcementId], null),
|
|
1409
|
+
human,
|
|
1410
|
+
};
|
|
1411
|
+
}
|
|
1412
|
+
async function taskBoard(flags) {
|
|
1413
|
+
const { listBoard } = await import('../../agents/task-board.js');
|
|
1414
|
+
const capFilter = opt(flags, '--capability');
|
|
1415
|
+
const openOnly = has(flags, '--open');
|
|
1416
|
+
const localOnly = has(flags, '--local');
|
|
1417
|
+
const view = await listBoard({ openOnly, localOnly });
|
|
1418
|
+
const capLower = String(capFilter || '').trim().toLowerCase();
|
|
1419
|
+
const entries = capLower ? view.entries.filter((e) => String(e.capability).trim().toLowerCase() === capLower) : view.entries;
|
|
1420
|
+
const data = {
|
|
1421
|
+
count: entries.length,
|
|
1422
|
+
local: view.localCount,
|
|
1423
|
+
remote: view.remoteCount,
|
|
1424
|
+
duplicatesDeduped: view.duplicates,
|
|
1425
|
+
filter: { capability: capFilter || null, openOnly, localOnly },
|
|
1426
|
+
registryReady: view.registryReady,
|
|
1427
|
+
registryError: view.registryError,
|
|
1428
|
+
dir: '~/.bolloon/tasks/board',
|
|
1429
|
+
entries: entries.map(boardEntryView),
|
|
1430
|
+
notes: view.notes,
|
|
1431
|
+
note: '板上只有摘要/预览: 任务正文只在买方本地; 接单: bolloon task claim <announcementId>',
|
|
1432
|
+
next: entries.some((e) => e.claimable) ? `bolloon task claim ${entries.find((e) => e.claimable).announcementId}` : null,
|
|
1433
|
+
};
|
|
1434
|
+
const human = entries.length
|
|
1435
|
+
? entries.map((e) => [
|
|
1436
|
+
`\n ${e.announcementId}${e.remote ? ' (远端公告)' : ''}`,
|
|
1437
|
+
` capability ${e.capability}`,
|
|
1438
|
+
` 买方 ${e.buyerShort || '(无)'}`,
|
|
1439
|
+
` 状态 ${e.status}${e.claimable ? ' · 可接单' : e.status === 'open' ? ' · 已过期' : ''}`,
|
|
1440
|
+
` 预算 ${e.budget ? `≤ ${e.budget.maxAmount} 原子 ${e.budget.currency} @ ${e.budget.network}` : '(无)'}`,
|
|
1441
|
+
` 截止时间 ${e.deadline ? `${new Date(e.deadline).toISOString()}${e.deadlineInMs !== null ? ` (${e.deadlineInMs > 0 ? `还剩 ${Math.round(e.deadlineInMs / 1000)}s` : '已过期'})` : ''}` : '(无)'}`,
|
|
1442
|
+
` 认领 ${e.claimCount === 0 ? '还没有人接' : `${e.claimCount} 人 (最近: ${String(e.claimedBy).slice(0, 24)})`}${e.remote ? ' · 远端的认领数本机看不到' : ''}`,
|
|
1443
|
+
` 签名 ${e.signatureVerified === true ? '验签通过' : e.signatureVerified === false ? '**验签不过** (接不了单)' : '未验签(如实)'}`,
|
|
1444
|
+
` 预览 ${e.instructionPreview || '(无)'}`,
|
|
1445
|
+
].join('\n')).join('')
|
|
1446
|
+
: '\n (板上没有可接单的任务)';
|
|
1447
|
+
return {
|
|
1448
|
+
envelope: okEnvelope('OK', `板上 ${entries.length} 条${openOnly ? '可接单' : ''}公告 (本地 ${view.localCount} · 远端 ${view.remoteCount})${capFilter ? ` [capability=${capFilter}]` : ''}`, data, entries.map((e) => e.announcementId), null),
|
|
1449
|
+
human: [
|
|
1450
|
+
`${title('bolloon task board')}${human}`,
|
|
1451
|
+
'',
|
|
1452
|
+
` ${String(data.note)}`,
|
|
1453
|
+
view.registryError ? ` ${hint(`注册表读取失败 (远端公告可能不全): ${view.registryError}`)}` : '',
|
|
1454
|
+
data.next ? `\n ${hint('下一步: ' + data.next)}` : '',
|
|
1455
|
+
].filter(Boolean).join('\n'),
|
|
1456
|
+
};
|
|
1457
|
+
}
|
|
1458
|
+
async function taskClaim(flags) {
|
|
1459
|
+
const id = flags.positionals[1] || opt(flags, '--announcement-id') || flags.requestId;
|
|
1460
|
+
if (!id) {
|
|
1461
|
+
return {
|
|
1462
|
+
envelope: failEnvelope('INVALID_ARGUMENT', '缺少 announcementId (bolloon task claim <announcementId>)', { usage: plain(TASK_USAGE.trim()) }, [], 'needs_human'),
|
|
1463
|
+
human: `${TASK_USAGE}\n${hint('先看板: bolloon task board')}`,
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
const s = await requireSigner('bolloon task claim');
|
|
1467
|
+
if ('fail' in s)
|
|
1468
|
+
return s.fail;
|
|
1469
|
+
const signer = s.signer;
|
|
1470
|
+
// ★ C7: 带 --group 时**在认领之前**把群定死 —— 群非法/没加入 → 整条命令拒绝 (绝不"认领落本地 + 悄悄不发群")
|
|
1471
|
+
const wantsGroup = opt(flags, '--group') !== undefined;
|
|
1472
|
+
const gctx = wantsGroup ? await requireGroup('bolloon task claim ' + id, flags) : null;
|
|
1473
|
+
if (gctx && !gctx.ok)
|
|
1474
|
+
return gctx.result;
|
|
1475
|
+
// 声明价格 (可选): 不声明 → null, 不替它猜
|
|
1476
|
+
const priceHuman = opt(flags, '--price');
|
|
1477
|
+
const currency = String(opt(flags, '--currency') || 'USDC').toUpperCase();
|
|
1478
|
+
let priceAtomic = null;
|
|
1479
|
+
if (priceHuman !== undefined) {
|
|
1480
|
+
priceAtomic = toAtomic(priceHuman, currency === 'USDC' ? 6 : 18);
|
|
1481
|
+
if (!priceAtomic) {
|
|
1482
|
+
return {
|
|
1483
|
+
envelope: failEnvelope('INVALID_ARGUMENT', `--price 必须是正数 (收到 ${priceHuman})`, { price: priceHuman, currency, accepted: ['--price 0.02'] }, [], 'needs_human'),
|
|
1484
|
+
human: TASK_USAGE,
|
|
1485
|
+
};
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
const { claimAnnouncement } = await import('../../agents/task-board.js');
|
|
1489
|
+
const r = await claimAnnouncement(id, {
|
|
1490
|
+
providerDid: signer.did,
|
|
1491
|
+
providerPublicKeyHex: signer.publicKeyHex,
|
|
1492
|
+
priceAmountAtomic: priceAtomic,
|
|
1493
|
+
currency,
|
|
1494
|
+
network: opt(flags, '--network') || '',
|
|
1495
|
+
signerKeypair: signer.keypair,
|
|
1496
|
+
});
|
|
1497
|
+
const baseData = {
|
|
1498
|
+
announcementId: id,
|
|
1499
|
+
reason: r.reason,
|
|
1500
|
+
remote: r.remote,
|
|
1501
|
+
// ★ 本命令任何路径都不动钱、不执行、不标 verified
|
|
1502
|
+
executed: false,
|
|
1503
|
+
paid: r.paid,
|
|
1504
|
+
fundsMoved: r.fundsMoved,
|
|
1505
|
+
verified: r.verified,
|
|
1506
|
+
deliveredToBuyer: r.deliveredToBuyer,
|
|
1507
|
+
};
|
|
1508
|
+
if (!r.ok) {
|
|
1509
|
+
const code = r.reason === 'not_found' ? 'NOT_FOUND'
|
|
1510
|
+
: r.reason === 'already_claimed' ? 'DUPLICATE_REQUEST'
|
|
1511
|
+
: r.reason === 'cancelled' ? 'TASK_CANCELLED'
|
|
1512
|
+
: r.reason === 'signature_invalid' || r.reason === 'instruction_digest_mismatch' ? 'SIGNATURE_INVALID'
|
|
1513
|
+
: r.reason === 'deadline_expired' ? 'DEADLINE_EXPIRED'
|
|
1514
|
+
: 'INVALID_ARGUMENT';
|
|
1515
|
+
return {
|
|
1516
|
+
envelope: failEnvelope(code, `没接成单: ${r.message}`, { ...baseData, status: r.status ?? null, existingClaim: r.existingClaim ?? null }, [id], 'needs_human'),
|
|
1517
|
+
human: `${title('bolloon task claim ' + id)}\n ${r.message}\n${line('本次付款', '没有')}\n\n${hint('看板: bolloon task board')}`,
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1520
|
+
const c = r.claim;
|
|
1521
|
+
// ★ C7: 认领成功 → 往群里发一条极短接单声明 (群已在认领**之前**定死; 这里最坏也只是"发失败")
|
|
1522
|
+
let groupPost = null;
|
|
1523
|
+
if (gctx && gctx.ok) {
|
|
1524
|
+
const TG = await import('../../agents/task-group.js');
|
|
1525
|
+
const priceTok = c.priceAmountAtomic ? `${c.priceAmountAtomic}${String(c.currency || '').toUpperCase()}` : null;
|
|
1526
|
+
const built = TG.buildPostMessage({ kind: 'claim', announcementId: id, price: priceTok });
|
|
1527
|
+
const sent = built.ok
|
|
1528
|
+
? await TG.sendTrailMessage(gctx.group.groupId, built.text, gctx.fromTag)
|
|
1529
|
+
: { ok: false, sent: false, error: built.message, violations: null };
|
|
1530
|
+
groupPost = {
|
|
1531
|
+
requested: true,
|
|
1532
|
+
posted: sent.sent === true,
|
|
1533
|
+
error: sent.sent ? null : (sent.error || '未知原因'),
|
|
1534
|
+
message: built.ok ? built.text : null,
|
|
1535
|
+
group: gctx.group.groupId,
|
|
1536
|
+
senderTag: gctx.fromTag,
|
|
1537
|
+
localFallback: false,
|
|
1538
|
+
/** 群消息没发出去**不等于**认领没发生: 认领事实已写进本地账 (上面那步真写过了) */
|
|
1539
|
+
claimStillRecorded: true,
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
else if (!wantsGroup) {
|
|
1543
|
+
groupPost = { requested: false, posted: false, reason: '没有 --group: 本命令原有语义不变 (只记本地认领账); 群里留痕要显式给 --group' };
|
|
1544
|
+
}
|
|
1545
|
+
const data = {
|
|
1546
|
+
...baseData,
|
|
1547
|
+
claim: {
|
|
1548
|
+
providerDid: c.providerDid,
|
|
1549
|
+
providerPublicKeyHex: c.providerPublicKeyHex ? `${String(c.providerPublicKeyHex).slice(0, 16)}…` : null,
|
|
1550
|
+
claimedAt: c.claimedAt,
|
|
1551
|
+
priceAmountAtomic: c.priceAmountAtomic,
|
|
1552
|
+
currency: c.currency,
|
|
1553
|
+
network: c.network,
|
|
1554
|
+
signed: !!c.signature,
|
|
1555
|
+
},
|
|
1556
|
+
status: r.status ?? null,
|
|
1557
|
+
file: r.file,
|
|
1558
|
+
priceNote: r.priceNote ?? null,
|
|
1559
|
+
/** C7 群聊侧: 认领有没有在群里留下接单声明 (发失败也如实标; 认领事实不受影响) */
|
|
1560
|
+
group: groupPost,
|
|
1561
|
+
honestyNote: r.remote
|
|
1562
|
+
? '远端公告的认领只落本机台账 (**没有投递给买方**); 真交接走 bolloon task send → 对方 task accept'
|
|
1563
|
+
: '认领只是"我来做"的本地事实: 不执行任务、不付款、不标 verified (释放/验真另走裁决函数)',
|
|
1564
|
+
};
|
|
1565
|
+
const groupNext = groupPost && groupPost.requested === true && groupPost.posted !== true ? 'needs_human' : null;
|
|
1566
|
+
return {
|
|
1567
|
+
envelope: okEnvelope('OK', `${r.message}${groupPost && groupPost.requested === true ? (groupPost.posted === true ? ' (接单声明已进群)' : ' (接单声明没发进群)') : ''}`, data, [id], groupNext ?? (r.remote ? 'needs_human' : null)),
|
|
1568
|
+
human: [
|
|
1569
|
+
title('bolloon task claim ' + id),
|
|
1570
|
+
line('认领者', c.providerDid),
|
|
1571
|
+
line('认领时间', new Date(c.claimedAt).toISOString()),
|
|
1572
|
+
line('声明价格', c.priceAmountAtomic ? `${c.priceAmountAtomic} 原子 ${c.currency}` : '(没声明价)'),
|
|
1573
|
+
line('公告来源', r.remote ? '注册表 (远端)' : '本地公告文件'),
|
|
1574
|
+
line('投递给买方', r.deliveredToBuyer ? '已投递' : '**没有** (本版没有远端认领投递通道; 真交接走 task send/accept)'),
|
|
1575
|
+
...(groupPost ? [line('群聊留痕', groupPost.requested === true
|
|
1576
|
+
? (groupPost.posted === true ? `接单声明已进群 (发送者标记 ${String(groupPost.senderTag)})` : `**没发进群**: ${String(groupPost.error)} — 认领事实已记账, 但群里没有这条痕迹`)
|
|
1577
|
+
: `没有 --group: 只记本地账 (${String(groupPost.reason)})`)] : []),
|
|
1578
|
+
line('本次执行', '没有 (接单不执行任务)'),
|
|
1579
|
+
line('本次付款', '没有 (接单不付款)'),
|
|
1580
|
+
r.remote ? `\n ${hint('下一步: 让买方把任务发过来 (bolloon task send); 对方 accept 后才有执行/交付')}` : `\n ${hint('下一步: 等买方的 task send/accept; 交付后用 decideAnnouncementRelease 判是否可释放 (未交付不得释放)')}`,
|
|
1581
|
+
].join('\n'),
|
|
1582
|
+
};
|
|
1583
|
+
}
|
|
1584
|
+
// ── 群聊通道 (C7: announce / trail / post, 2026-09-23) ───────────────────────
|
|
1585
|
+
/** C7 群上下文: 群必须先定下来 (缺/非法/没加入 → 拒跑); 发送者标记只用短假名 */
|
|
1586
|
+
async function requireGroup(head, flags) {
|
|
1587
|
+
const TG = await import('../../agents/task-group.js');
|
|
1588
|
+
const ref = opt(flags, '--group');
|
|
1589
|
+
const g = await TG.resolveGroupRef(ref);
|
|
1590
|
+
if (!g.ok) {
|
|
1591
|
+
return {
|
|
1592
|
+
ok: false,
|
|
1593
|
+
result: {
|
|
1594
|
+
envelope: failEnvelope(g.code, `${g.message}`, { group: ref ?? null, ...g.detail, sent: false, localFallback: false, why: '缺群/群非法/群没加入一律拒绝执行: 本命令不做"只写本地"的降级' }, [], 'needs_human'),
|
|
1595
|
+
human: [
|
|
1596
|
+
title(head),
|
|
1597
|
+
` 群没定下来: ${g.message}`,
|
|
1598
|
+
'',
|
|
1599
|
+
hint('群聊通道就是留痕的地方: 没有群就没有留痕 —— 这里不会静默降级成只写本地'),
|
|
1600
|
+
...(Array.isArray(g.detail.joined) && g.detail.joined.length
|
|
1601
|
+
? [line('本机已加入的群', g.detail.joined.map((x) => `${x.id}${x.name ? ` (${x.name})` : ''}`).join(' · '))]
|
|
1602
|
+
: [line('本机已加入的群', '(没有)')]),
|
|
1603
|
+
].join('\n'),
|
|
1604
|
+
},
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
const s = await TG.resolveSenderTag(opt(flags, '--from'));
|
|
1608
|
+
if (!s.ok) {
|
|
1609
|
+
return {
|
|
1610
|
+
ok: false,
|
|
1611
|
+
result: {
|
|
1612
|
+
envelope: failEnvelope(s.code, `${s.message}`, { ...s.detail, group: g.group, sent: false, localFallback: false }, [], 'needs_human'),
|
|
1613
|
+
human: `${title(head)}\n 发送者标记定不下来: ${s.message}\n\n${hint('群里只出现短假名 (agent-xxxxxxxx) 或 --from 给的短显示名; 原始 DID 永不进群')}`,
|
|
1614
|
+
},
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1617
|
+
return { ok: true, group: g.group, fromTag: s.tag, fromSource: s.source };
|
|
1618
|
+
}
|
|
1619
|
+
/** 时间线条目 → 对外投影 (不含原消息文本; 字段已被遮蔽过) */
|
|
1620
|
+
function trailEntryView(e) {
|
|
1621
|
+
return { kind: e.kind, at: e.at, atIso: e.at ? new Date(e.at).toISOString() : null, sender: e.sender, announcementId: e.announcementId, fields: e.fields };
|
|
1622
|
+
}
|
|
1623
|
+
/**
|
|
1624
|
+
* `bolloon task announce --group <群链接|groupId>` —— 把板上一条待接单公告压成一行极短事实发进群。
|
|
1625
|
+
* 内容只有: 期号 · capability · 预算 · 验收判据摘要 · 公告 id (任务正文不进群)。
|
|
1626
|
+
*/
|
|
1627
|
+
async function taskAnnounce(flags) {
|
|
1628
|
+
const head = 'bolloon task announce';
|
|
1629
|
+
const ctx = await requireGroup(head, flags);
|
|
1630
|
+
if (!ctx.ok)
|
|
1631
|
+
return ctx.result;
|
|
1632
|
+
const TG = await import('../../agents/task-group.js');
|
|
1633
|
+
const picked = TG.pickAnnouncement({ announcementId: opt(flags, '--announcement-id'), capability: opt(flags, '--capability') });
|
|
1634
|
+
if (!picked.ok) {
|
|
1635
|
+
return {
|
|
1636
|
+
envelope: failEnvelope(picked.code, picked.message, { ...picked.detail, group: ctx.group, sent: false, localFallback: false }, [], 'needs_human'),
|
|
1637
|
+
human: `${title(head)}\n 没得可发: ${picked.message}\n\n${hint('先看板: bolloon task board; 发公告: bolloon task publish --capability … --instruction "…" --budget 0.05')}`,
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
const a = picked.announcement;
|
|
1641
|
+
const text = TG.buildAnnounceMessage(a, { round: opt(flags, '--round'), criteria: opt(flags, '--criteria') });
|
|
1642
|
+
const sent = await TG.sendTrailMessage(ctx.group.groupId, text, ctx.fromTag);
|
|
1643
|
+
if (!sent.ok) {
|
|
1644
|
+
const violations = sent.violations || null;
|
|
1645
|
+
return {
|
|
1646
|
+
envelope: failEnvelope(violations ? 'POLICY_DENIED' : 'TRANSPORT_FAILED', violations ? `公告命中隐私红线, 拒绝发进群: ${violations.map((v) => v.rule).join(', ')}` : `群消息没发出去: ${sent.error}`, { announcementId: a.announcementId, group: ctx.group.groupId, message: text, sent: false, violations, localFallback: false, paid: false }, [a.announcementId], 'needs_human'),
|
|
1647
|
+
human: [
|
|
1648
|
+
title(head),
|
|
1649
|
+
` ${violations ? '命中隐私红线, 拒发' : '没发出去'}: ${sent.error}`,
|
|
1650
|
+
...(violations ? violations.map((v) => ` · ${v.rule} — ${v.why} (${v.masked}, ${v.chars} 字符)`) : []),
|
|
1651
|
+
'',
|
|
1652
|
+
hint(violations ? '把公告的预览/判据摘要改成不含标识符的短事实后重发 (本命令不静默脱敏: 脱敏后的公告不是你要发的那条)' : '群 store 不可达时不会落本地: 就是没发出去'),
|
|
1653
|
+
].join('\n'),
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
const data = {
|
|
1657
|
+
announcementId: a.announcementId,
|
|
1658
|
+
capability: a.capability,
|
|
1659
|
+
budget: a.budget,
|
|
1660
|
+
deadline: a.deadline,
|
|
1661
|
+
round: opt(flags, '--round') || null,
|
|
1662
|
+
judgeStated: !!opt(flags, '--criteria'),
|
|
1663
|
+
message: sent.text,
|
|
1664
|
+
group: { id: ctx.group.groupId, name: ctx.group.name, via: ctx.group.via },
|
|
1665
|
+
sender: { tag: ctx.fromTag, source: ctx.fromSource, didPrinted: false },
|
|
1666
|
+
/** 群里只有过程事实: 不执行、不付款、不标 verified */
|
|
1667
|
+
executed: false,
|
|
1668
|
+
paid: false,
|
|
1669
|
+
fundsMoved: false,
|
|
1670
|
+
localFallback: false,
|
|
1671
|
+
note: '公告入群只发极短事实 (期号/capability/预算/判据摘要/公告 id): 任务正文不进群; 群里的话不替代链上 releaseV2',
|
|
1672
|
+
};
|
|
1673
|
+
return {
|
|
1674
|
+
envelope: okEnvelope('OK', `已把公告 ${a.announcementId} 发进群 (${a.capability})`, data, [a.announcementId], null),
|
|
1675
|
+
human: [
|
|
1676
|
+
title(head),
|
|
1677
|
+
line('公告号', a.announcementId),
|
|
1678
|
+
line('capability', a.capability),
|
|
1679
|
+
line('预算', a.budget ? `≤ ${a.budget.maxAmount} 原子 (${a.budget.currency} @ ${a.budget.network})` : '(无)'),
|
|
1680
|
+
line('期号', opt(flags, '--round') || '(未标)'),
|
|
1681
|
+
line('判据摘要', opt(flags, '--criteria') || `未声明 (只放任务书摘要 sha256:${String(a.instructionDigest || '').slice(0, 16)}…)`),
|
|
1682
|
+
line('群', `${ctx.group.name || '(无名)'} · ${ctx.group.groupId}`),
|
|
1683
|
+
line('发送者标记', `${ctx.fromTag} (${ctx.fromSource === 'identity' ? '本机身份派生假名' : '--from 显式'}; 原始 DID 未进群)`),
|
|
1684
|
+
line('本次执行/付款', '没有 (公告入群只是过程留痕)'),
|
|
1685
|
+
'',
|
|
1686
|
+
hint(`下一步: 别人 bolloon task claim ${a.announcementId} --group <群链接>; 回看留痕: bolloon task trail --group <群链接>`),
|
|
1687
|
+
].join('\n'),
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1690
|
+
/** `bolloon task trail --group <群链接|groupId>` —— 从群消息读回本期过程痕迹并汇总成时间线 */
|
|
1691
|
+
async function taskTrail(flags) {
|
|
1692
|
+
const head = 'bolloon task trail';
|
|
1693
|
+
const ctx = await requireGroup(head, flags);
|
|
1694
|
+
if (!ctx.ok)
|
|
1695
|
+
return ctx.result;
|
|
1696
|
+
const TG = await import('../../agents/task-group.js');
|
|
1697
|
+
const limitRaw = opt(flags, '--limit');
|
|
1698
|
+
const limit = limitRaw === undefined ? 300 : Number(limitRaw);
|
|
1699
|
+
if (limitRaw !== undefined && (!Number.isFinite(limit) || limit < 1)) {
|
|
1700
|
+
return {
|
|
1701
|
+
envelope: failEnvelope('INVALID_ARGUMENT', `--limit 必须是正数 (收到 ${limitRaw})`, { limit: limitRaw }, [], 'needs_human'),
|
|
1702
|
+
human: TASK_USAGE,
|
|
1703
|
+
};
|
|
1704
|
+
}
|
|
1705
|
+
let summary;
|
|
1706
|
+
try {
|
|
1707
|
+
summary = await TG.readTrail(ctx.group.groupId, { limit, announcementId: opt(flags, '--announcement-id') });
|
|
1708
|
+
}
|
|
1709
|
+
catch (e) {
|
|
1710
|
+
return {
|
|
1711
|
+
envelope: failEnvelope('TRANSPORT_FAILED', `群消息读不出来: ${String(e?.message || e).slice(0, 160)}`, { group: ctx.group.groupId, read: false, localFallback: false }, [], 'needs_human'),
|
|
1712
|
+
human: `${title(head)}\n 群消息读不出来: ${String(e?.message || e).slice(0, 160)}\n\n${hint('群不可达时如实报错: 不用本地缓存假装读过群')}`,
|
|
1713
|
+
};
|
|
1714
|
+
}
|
|
1715
|
+
const data = {
|
|
1716
|
+
group: { id: ctx.group.groupId, name: ctx.group.name, via: ctx.group.via },
|
|
1717
|
+
count: summary.count,
|
|
1718
|
+
byKind: summary.byKind,
|
|
1719
|
+
announcements: summary.announcements,
|
|
1720
|
+
flags: summary.flags,
|
|
1721
|
+
inconsistencies: summary.inconsistencies,
|
|
1722
|
+
redacted: summary.redacted,
|
|
1723
|
+
ignoredMessages: summary.ignoredMessages,
|
|
1724
|
+
timeline: summary.entries.map(trailEntryView),
|
|
1725
|
+
limit,
|
|
1726
|
+
announcementFilter: opt(flags, '--announcement-id') || null,
|
|
1727
|
+
readOnly: true,
|
|
1728
|
+
localFallback: false,
|
|
1729
|
+
note: '时间线只汇总群里真发过的事实 (公告/接单/交付/初筛/终审); 没发过的环节不会凭空出现; 发送者只显示短标记 (原始 DID 不在输出里)',
|
|
1730
|
+
};
|
|
1731
|
+
const label = (k) => `${TG.TRAIL_KIND_LABEL[k]}=${summary.byKind[k]}`;
|
|
1732
|
+
const human = [
|
|
1733
|
+
title(`${head} (${ctx.group.name || ctx.group.groupId})`),
|
|
1734
|
+
line('条目', `${summary.count} 条 (${TG.TRAIL_KINDS.map(label).join(' · ')})`),
|
|
1735
|
+
...(summary.announcements.length ? [line('涉及公告', summary.announcements.join(' · '))] : []),
|
|
1736
|
+
...(summary.inconsistencies.length ? [line('事实矛盾/遮蔽', summary.inconsistencies.join(' · '))] : []),
|
|
1737
|
+
...(summary.redacted.length ? [line('被遮蔽的字段', summary.redacted.join(' · ') + ' (别人发的消息里含标识符形态, 读回时已遮蔽)')] : []),
|
|
1738
|
+
'',
|
|
1739
|
+
...(summary.entries.length
|
|
1740
|
+
? summary.entries.map((e) => ` ${TG.formatTrailLine(e)}`)
|
|
1741
|
+
: [' (群里还没有本期的过程痕迹: 先用 bolloon task announce --group … 发公告)']),
|
|
1742
|
+
'',
|
|
1743
|
+
` ${String(data.note)}`,
|
|
1744
|
+
].join('\n');
|
|
1745
|
+
return {
|
|
1746
|
+
envelope: okEnvelope('OK', summary.count
|
|
1747
|
+
? `群里本期 ${summary.count} 条过程痕迹 (${summary.announcements.length} 个公告)`
|
|
1748
|
+
: '群里本期没有过程痕迹 (群里真没有, 不是读失败)', data, summary.announcements, null),
|
|
1749
|
+
human,
|
|
1750
|
+
};
|
|
1751
|
+
}
|
|
1752
|
+
/** `bolloon task post --kind deliver|screen|final --group <…> --announcement-id <id>` —— 交付/初筛/终审的群消息 */
|
|
1753
|
+
async function taskPost(flags) {
|
|
1754
|
+
const head = 'bolloon task post';
|
|
1755
|
+
const kind = String(opt(flags, '--kind') || '').trim().toLowerCase();
|
|
1756
|
+
if (!['deliver', 'screen', 'final'].includes(kind)) {
|
|
1757
|
+
return {
|
|
1758
|
+
envelope: failEnvelope('INVALID_ARGUMENT', `--kind 必须是 deliver|screen|final (收到 ${kind || '(空)'})`, {
|
|
1759
|
+
accepted: ['--kind deliver --hash sha256:<hex>', '--kind screen --checks "渠道结构=pass,价格带=fail"', '--kind final --verdict accept'],
|
|
1760
|
+
claimKindNote: kind === 'claim'
|
|
1761
|
+
? '接单痕迹不用 task post 发: 用 `bolloon task claim <announcementId> --group <群>` —— 这样"接单痕迹"必定对应一笔真认领账, 不会出现没有认领者却挂在群里的接单声明 (反过来也不会有认领了却没有任何群痕迹的静默)'
|
|
1762
|
+
: null,
|
|
1763
|
+
why: '群里只认这四种过程事实: 公告 / 交付 / 初筛 / 终审 (接单走 task claim)',
|
|
1764
|
+
}, [], 'needs_human'),
|
|
1765
|
+
human: TASK_USAGE,
|
|
1766
|
+
};
|
|
1767
|
+
}
|
|
1768
|
+
const ctx = await requireGroup(head, flags);
|
|
1769
|
+
if (!ctx.ok)
|
|
1770
|
+
return ctx.result;
|
|
1771
|
+
const TG = await import('../../agents/task-group.js');
|
|
1772
|
+
const bytesRaw = opt(flags, '--bytes');
|
|
1773
|
+
const built = TG.buildPostMessage({
|
|
1774
|
+
kind,
|
|
1775
|
+
announcementId: opt(flags, '--announcement-id'),
|
|
1776
|
+
round: opt(flags, '--round'),
|
|
1777
|
+
price: opt(flags, '--price'),
|
|
1778
|
+
hash: opt(flags, '--hash'),
|
|
1779
|
+
bytes: bytesRaw === undefined ? null : Number(bytesRaw),
|
|
1780
|
+
checksRaw: opt(flags, '--checks'),
|
|
1781
|
+
verdict: opt(flags, '--verdict'),
|
|
1782
|
+
});
|
|
1783
|
+
if (!built.ok) {
|
|
1784
|
+
return {
|
|
1785
|
+
envelope: failEnvelope(built.code, built.message, { ...built.detail, kind, group: ctx.group, sent: false, localFallback: false }, [], 'needs_human'),
|
|
1786
|
+
human: `${title(head)}\n ${built.message}\n\n${TASK_USAGE.split('选项:')[0].trim().split('\n').slice(-6).join('\n')}`,
|
|
1787
|
+
};
|
|
1788
|
+
}
|
|
1789
|
+
const sent = await TG.sendTrailMessage(ctx.group.groupId, built.text, ctx.fromTag);
|
|
1790
|
+
if (!sent.ok) {
|
|
1791
|
+
const violations = sent.violations || null;
|
|
1792
|
+
return {
|
|
1793
|
+
envelope: failEnvelope(violations ? 'POLICY_DENIED' : 'TRANSPORT_FAILED', violations ? `过程痕迹命中隐私红线, 拒绝发进群: ${violations.map((v) => v.rule).join(', ')}` : `群消息没发出去: ${sent.error}`, { kind, group: ctx.group.groupId, message: built.text, sent: false, violations, localFallback: false }, [], 'needs_human'),
|
|
1794
|
+
human: [
|
|
1795
|
+
title(head),
|
|
1796
|
+
` ${violations ? '命中隐私红线, 拒发' : '没发出去'}: ${sent.error}`,
|
|
1797
|
+
...(violations ? violations.map((v) => ` · ${v.rule} — ${v.why} (${v.masked}, ${v.chars} 字符)`) : []),
|
|
1798
|
+
'',
|
|
1799
|
+
hint('群里不放钱包地址/DID/peerId/multiaddr/IP/私钥形态; 换个不含标识符的短事实重发'),
|
|
1800
|
+
].join('\n'),
|
|
1801
|
+
};
|
|
1802
|
+
}
|
|
1803
|
+
const onBoard = !!TG.readBoardAnnouncement(String(opt(flags, '--announcement-id') || ''));
|
|
1804
|
+
const data = {
|
|
1805
|
+
kind,
|
|
1806
|
+
announcementId: opt(flags, '--announcement-id'),
|
|
1807
|
+
message: sent.text,
|
|
1808
|
+
checks: built.checks ? built.checks : null,
|
|
1809
|
+
group: { id: ctx.group.groupId, name: ctx.group.name, via: ctx.group.via },
|
|
1810
|
+
sender: { tag: ctx.fromTag, source: ctx.fromSource, didPrinted: false },
|
|
1811
|
+
announcementOnLocalBoard: onBoard,
|
|
1812
|
+
/** 群里只是留痕: 不执行、不付款、也不标 verified (终审结论不替代链上 release) */
|
|
1813
|
+
executed: false,
|
|
1814
|
+
paid: false,
|
|
1815
|
+
fundsMoved: false,
|
|
1816
|
+
verified: false,
|
|
1817
|
+
localFallback: false,
|
|
1818
|
+
note: kind === 'deliver'
|
|
1819
|
+
? '交付痕迹只贴内容哈希 (群里没有正文); 真交接仍走 bolloon task send → 对方 task accept'
|
|
1820
|
+
: kind === 'screen'
|
|
1821
|
+
? '初筛通过 ≠ 已验收: 终审结论另发 (--kind final); 线上一分钱都没动'
|
|
1822
|
+
: '终审结论只是过程事实: 结算仍只在链上 (群里的话不替代 releaseV2; 争议期不自动重付/不标 verified)',
|
|
1823
|
+
};
|
|
1824
|
+
return {
|
|
1825
|
+
envelope: okEnvelope('OK', `已把${TG.TRAIL_KIND_LABEL[kind]}发进群 (${opt(flags, '--announcement-id')})`, data, [String(opt(flags, '--announcement-id'))], null),
|
|
1826
|
+
human: [
|
|
1827
|
+
title(`${head} --kind ${kind}`),
|
|
1828
|
+
line('公告号', String(opt(flags, '--announcement-id'))),
|
|
1829
|
+
line('群消息', sent.text),
|
|
1830
|
+
...(built.checks ? [line('初筛逐条', built.checks.map((c) => `${c.name}=${c.result}`).join(' · '))] : []),
|
|
1831
|
+
line('群', `${ctx.group.name || '(无名)'} · ${ctx.group.groupId}`),
|
|
1832
|
+
line('发送者标记', `${ctx.fromTag} (原始 DID 未进群)`),
|
|
1833
|
+
line('本地板上有这条公告吗', onBoard ? '有' : '没有 (本机不是买方; 只发痕迹不假装认识这条公告)'),
|
|
1834
|
+
line('本次执行/付款/验真', '都没有 (群消息只是过程留痕)'),
|
|
1835
|
+
'',
|
|
1836
|
+
` ${data.note}`,
|
|
1837
|
+
].join('\n'),
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1219
1840
|
// ── 仍未实现的两个 (任务书没点名; 如实报, 不假装) ────────────────────────────
|
|
1220
1841
|
async function taskComplete(_flags) {
|
|
1221
1842
|
const env = notImplemented('task complete 还没实现: 没有任务状态存储可写 (交易层有非法迁移保护, 不许绕过)', {
|