@bolloon/bolloon-agent 0.4.33 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/contacts/chain.js +27 -7
- package/dist/agents/execution-supervisor.js +580 -40
- package/dist/agents/external-events.js +35 -6
- package/dist/agents/gateway-group.js +100 -12
- package/dist/agents/goal-criteria.js +10 -3
- package/dist/agents/goal-flywheel/block-executor.js +346 -0
- package/dist/agents/goal-flywheel/continuation-decision.js +592 -0
- package/dist/agents/goal-flywheel/goal-change.js +754 -0
- package/dist/agents/goal-flywheel/index.js +26 -0
- package/dist/agents/goal-flywheel/memory-layers.js +227 -0
- package/dist/agents/goal-flywheel/run-closure.js +809 -0
- package/dist/agents/goal-flywheel/skill-candidate.js +628 -0
- package/dist/agents/goal-flywheel/types.js +329 -0
- package/dist/agents/goal-flywheel/wiring/change.js +288 -0
- package/dist/agents/goal-flywheel/wiring/closure.js +697 -0
- package/dist/agents/goal-flywheel/wiring/continuation.js +432 -0
- package/dist/agents/goal-flywheel/wiring/contract.js +270 -0
- package/dist/agents/goal-flywheel/wiring/index.js +26 -0
- package/dist/agents/goal-flywheel/wiring/monitor.js +254 -0
- package/dist/agents/goal-flywheel/wiring/seams.js +592 -0
- package/dist/agents/goal-flywheel/work-contract.js +478 -0
- package/dist/agents/goal-flywheel/work-monitor.js +488 -0
- package/dist/agents/goal-flywheel-wiring.js +1870 -0
- package/dist/agents/goal-state-reducer.js +406 -0
- package/dist/agents/goal-store.js +35 -1
- package/dist/agents/mobile-flywheel-view.js +144 -0
- package/dist/agents/mobile-task-actions-verify.js +74 -0
- package/dist/agents/mobile-task-actions.js +178 -0
- package/dist/agents/mobile-task-views.js +199 -0
- package/dist/agents/model-policy.js +651 -0
- package/dist/agents/network-pulse.js +304 -18
- package/dist/agents/pi-sdk-tools.js +84 -33
- package/dist/agents/pi-sdk.js +157 -42
- package/dist/agents/run-store.js +71 -5
- package/dist/agents/skill-readiness.js +52 -10
- package/dist/agents/subagent-manager.js +209 -3
- package/dist/agents/task/task-runner.js +17 -4
- package/dist/agents/task-group.js +8 -51
- package/dist/agents/task-public-text.js +89 -0
- package/dist/agents/workflow-pivot-loop.js +19 -12
- package/dist/cli/clipboard.js +65 -0
- package/dist/cli/commands/index.js +1 -0
- package/dist/cli/commands/tasks.js +350 -2
- package/dist/cli/identity-command.js +177 -0
- package/dist/cli/ink-app.js +145 -17
- package/dist/cli/log-gate.js +386 -0
- package/dist/cli/mention-data.js +2 -0
- package/dist/cli/model-selector.js +538 -0
- package/dist/cli/reply-hygiene.js +54 -0
- package/dist/cli/setup-wizard.js +757 -69
- package/dist/cli/startup-notice.js +160 -0
- package/dist/cli/stores.js +22 -1
- package/dist/cli/theme.js +49 -0
- package/dist/cli/tui-select.js +970 -0
- package/dist/cli/update-commands.js +150 -24
- package/dist/cli-entry.js +137 -20
- package/dist/electron-build/agents/contacts/chain.js +26 -6
- package/dist/electron-build/agents/contacts/chain.js.map +1 -1
- package/dist/electron-build/agents/execution-supervisor.js +578 -38
- package/dist/electron-build/agents/execution-supervisor.js.map +1 -1
- package/dist/electron-build/agents/external-events.js +34 -5
- package/dist/electron-build/agents/external-events.js.map +1 -1
- package/dist/electron-build/agents/goal-criteria.js +10 -3
- package/dist/electron-build/agents/goal-criteria.js.map +1 -1
- package/dist/electron-build/agents/goal-flywheel/continuation-decision.js +598 -0
- package/dist/electron-build/agents/goal-flywheel/continuation-decision.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/goal-change.js +773 -0
- package/dist/electron-build/agents/goal-flywheel/goal-change.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/memory-layers.js +269 -0
- package/dist/electron-build/agents/goal-flywheel/memory-layers.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/run-closure.js +818 -0
- package/dist/electron-build/agents/goal-flywheel/run-closure.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/skill-candidate.js +642 -0
- package/dist/electron-build/agents/goal-flywheel/skill-candidate.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/types.js +333 -0
- package/dist/electron-build/agents/goal-flywheel/types.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/change.js +294 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/change.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/closure.js +742 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/closure.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/continuation.js +444 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/continuation.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/contract.js +279 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/contract.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/index.js +111 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/index.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/monitor.js +264 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/monitor.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/seams.js +612 -0
- package/dist/electron-build/agents/goal-flywheel/wiring/seams.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/work-contract.js +488 -0
- package/dist/electron-build/agents/goal-flywheel/work-contract.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel/work-monitor.js +501 -0
- package/dist/electron-build/agents/goal-flywheel/work-monitor.js.map +1 -0
- package/dist/electron-build/agents/goal-flywheel-wiring.js +1962 -0
- package/dist/electron-build/agents/goal-flywheel-wiring.js.map +1 -0
- package/dist/electron-build/agents/goal-state-reducer.js +414 -0
- package/dist/electron-build/agents/goal-state-reducer.js.map +1 -0
- package/dist/electron-build/agents/goal-store.js +35 -1
- package/dist/electron-build/agents/goal-store.js.map +1 -1
- package/dist/electron-build/agents/model-policy.js +706 -0
- package/dist/electron-build/agents/model-policy.js.map +1 -0
- package/dist/electron-build/agents/network-pulse.js +307 -19
- package/dist/electron-build/agents/network-pulse.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk-tools.js +84 -33
- package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk-types.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk.js +156 -41
- package/dist/electron-build/agents/pi-sdk.js.map +1 -1
- package/dist/electron-build/agents/run-store.js +75 -6
- package/dist/electron-build/agents/run-store.js.map +1 -1
- package/dist/electron-build/agents/skill-readiness.js +52 -9
- package/dist/electron-build/agents/skill-readiness.js.map +1 -1
- package/dist/electron-build/agents/task/task-runner.js +16 -3
- package/dist/electron-build/agents/task/task-runner.js.map +1 -1
- package/dist/electron-build/agents/workflow-pivot-loop.js +19 -12
- package/dist/electron-build/agents/workflow-pivot-loop.js.map +1 -1
- package/dist/electron-build/llm/config-store.js +146 -6
- package/dist/electron-build/llm/config-store.js.map +1 -1
- package/dist/electron-build/llm/connection-probe.js +764 -0
- package/dist/electron-build/llm/connection-probe.js.map +1 -0
- package/dist/electron-build/llm/custom-provider-store.js +272 -0
- package/dist/electron-build/llm/custom-provider-store.js.map +1 -0
- package/dist/electron-build/llm/data/provider-catalog-baked.js +23 -0
- package/dist/electron-build/llm/data/provider-catalog-baked.js.map +1 -0
- package/dist/electron-build/llm/model-catalog.js +778 -0
- package/dist/electron-build/llm/model-catalog.js.map +1 -0
- package/dist/electron-build/llm/model-discovery.js +1202 -0
- package/dist/electron-build/llm/model-discovery.js.map +1 -0
- package/dist/electron-build/llm/model-selection.js +1315 -0
- package/dist/electron-build/llm/model-selection.js.map +1 -0
- package/dist/electron-build/llm/pi-ai.js +70 -11
- package/dist/electron-build/llm/pi-ai.js.map +1 -1
- package/dist/electron-build/llm/provider-catalog.js +969 -0
- package/dist/electron-build/llm/provider-catalog.js.map +1 -0
- package/dist/electron-build/llm/provider-registry.js +950 -0
- package/dist/electron-build/llm/provider-registry.js.map +1 -0
- package/dist/electron-build/llm/tool-name.js +266 -0
- package/dist/electron-build/llm/tool-name.js.map +1 -0
- package/dist/electron-build/orbitdb/cid-database.js +83 -17
- package/dist/electron-build/orbitdb/cid-database.js.map +1 -1
- package/dist/electron-build/orbitdb/ipfs-node.js +31 -2
- package/dist/electron-build/orbitdb/ipfs-node.js.map +1 -1
- package/dist/electron-build/utils/dual-source-facts.js +236 -0
- package/dist/electron-build/utils/dual-source-facts.js.map +1 -0
- package/dist/electron-build/utils/dual-source.js +347 -0
- package/dist/electron-build/utils/dual-source.js.map +1 -0
- package/dist/electron-build/utils/update-manager.js +328 -104
- package/dist/electron-build/utils/update-manager.js.map +1 -1
- package/dist/electron-build/utils/update-state.js +9 -11
- package/dist/electron-build/utils/update-state.js.map +1 -1
- package/dist/electron-build/utils/version-identity.js +193 -0
- package/dist/electron-build/utils/version-identity.js.map +1 -0
- package/dist/electron-build/utils/version-info.js +57 -31
- package/dist/electron-build/utils/version-info.js.map +1 -1
- package/dist/index.js +385 -108
- package/dist/ios/bolloon-web.json +10 -0
- package/dist/ios/chain-explorer.js +617 -0
- package/dist/ios/explorer.html +163 -0
- package/dist/ios/index.html +96 -0
- package/dist/ios/mobile-agent.js +2 -2
- package/dist/ios/mobile-contacts.js +362 -0
- package/dist/ios/mobile-core.js +3102 -310
- package/dist/ios/mobile-privacy.js +185 -0
- package/dist/ios/mobile-tasks.js +339 -0
- package/dist/ios/mobile-update.js +1442 -0
- package/dist/ios/mobile.css +19 -0
- package/dist/ios/mobile.html +96 -0
- package/dist/ios/mobile.js +1013 -41
- package/dist/ios/routes-chain-index.js +108 -0
- package/dist/ios/routes-contacts.js +420 -0
- package/dist/ios/routes-llm-config.js +344 -52
- package/dist/ios/routes-mobile-tasks.js +353 -0
- package/dist/ios/routes-onchain-trade.js +99 -0
- package/dist/ios/routes-x402-info.js +1 -0
- package/dist/ios/server.js +921 -0
- package/dist/ios/sw.js +1 -1
- package/dist/ios/task-frame-server.js +211 -0
- package/dist/llm/config-store.js +146 -6
- package/dist/llm/connection-probe.js +744 -0
- package/dist/llm/custom-provider-store.js +224 -0
- package/dist/llm/data/provider-catalog-baked.js +19 -0
- package/dist/llm/model-catalog.js +717 -0
- package/dist/llm/model-discovery.js +1137 -0
- package/dist/llm/model-selection.js +1241 -0
- package/dist/llm/pi-ai.js +70 -11
- package/dist/llm/provider-catalog.js +898 -0
- package/dist/llm/provider-registry.js +917 -0
- package/dist/llm/tool-name.js +220 -0
- package/dist/orbitdb/cid-database.js +81 -16
- package/dist/orbitdb/ipfs-node.js +31 -2
- package/dist/setup/onboard.js +34 -8
- package/dist/utils/dual-source-facts.js +223 -0
- package/dist/utils/dual-source.js +297 -0
- package/dist/utils/update-cli.js +16 -21
- package/dist/utils/update-health.js +35 -5
- package/dist/utils/update-manager.js +325 -96
- package/dist/utils/update-state.js +13 -10
- package/dist/utils/version-identity.js +176 -0
- package/dist/utils/version-info.js +27 -22
- package/dist/web/bolloon-web.json +10 -0
- package/dist/web/mobile-contacts.js +44 -11
- package/dist/web/mobile-core.js +2901 -580
- package/dist/web/mobile-tasks.js +339 -0
- package/dist/web/mobile-update.js +1442 -0
- package/dist/web/mobile.css +19 -0
- package/dist/web/mobile.html +64 -0
- package/dist/web/mobile.js +838 -35
- package/dist/web/routes-llm-config.js +344 -52
- package/dist/web/routes-mobile-tasks.js +353 -0
- package/dist/web/server.js +163 -4
- package/dist/web/sw.js +1 -1
- package/package.json +3 -1
- package/scripts/build-web.ts +36 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mobile-task-actions-verify.ts — 桌面端**验签**实现 (2026-09-25)
|
|
3
|
+
*
|
|
4
|
+
* 与 `contacts/grants.ts::verifyGrantSignature` 同一套纪律 (同一批已登记设备公钥, 同样逐项给理由):
|
|
5
|
+
* 形状 → 设备已登记 → payloadHash 对得上 → Ed25519 真验签 → 时效 → 内容摘要对得上。
|
|
6
|
+
*
|
|
7
|
+
* 为什么内容摘要要**桌面重算**: 手机签的是 "kind + 规范化内容" 的摘要。桌面在执行前用**请求里的原始字段**
|
|
8
|
+
* 重算一遍, 于是签名天然绑定了"要执行的那件事"本身 —— 既不能换类型 (入群签名拿去发公告),
|
|
9
|
+
* 也不能换内容 (换群/换正文/换公告号)。
|
|
10
|
+
*
|
|
11
|
+
* node 侧实现 (crypto.verify) 与手机 WebCrypto 签名对得上, 因为**规范载荷用的是同一份纯函数**。
|
|
12
|
+
*/
|
|
13
|
+
import * as crypto from 'crypto';
|
|
14
|
+
import { canonicalTaskActionPayload, taskActionContentText, checkTaskActionShape, checkTaskActionFreshness, actionKindAllowsRequest, TASK_ACTION_TTL_MS, } from './mobile-task-actions.js';
|
|
15
|
+
/** sha256(规范化动作内容) —— 与手机侧 crypto.subtle.digest('SHA-256', …) 同值 */
|
|
16
|
+
export function contentDigestOf(req) {
|
|
17
|
+
return crypto.createHash('sha256').update(taskActionContentText(req), 'utf8').digest('hex');
|
|
18
|
+
}
|
|
19
|
+
/** sha256(规范载荷) —— 与手机侧 payloadHash 同值 */
|
|
20
|
+
export function payloadHashOf(a) {
|
|
21
|
+
return crypto.createHash('sha256').update(canonicalTaskActionPayload(a), 'utf8').digest('hex');
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* 完整验签 (桌面路由唯一入口)。
|
|
25
|
+
*
|
|
26
|
+
* @param input 手机请求里的 { action, signature }
|
|
27
|
+
* @param pub 已登记设备公钥 (按 action.deviceId 查; 查不到传 null)
|
|
28
|
+
* @param opts.req 桌面准备执行的业务字段 → 重算内容摘要比对 (不传 = 跳过内容比对, 只有明确不需要时才这样用)
|
|
29
|
+
* @param opts.now 当前时间 (测试注入)
|
|
30
|
+
*/
|
|
31
|
+
export function verifyTaskAction(input, pub, opts = {}) {
|
|
32
|
+
const shape = checkTaskActionShape(input);
|
|
33
|
+
if (!shape.ok)
|
|
34
|
+
return { ok: false, reason: shape.reason, message: `动作载荷形状不合法: ${shape.reason}` };
|
|
35
|
+
const action = shape.action;
|
|
36
|
+
const signature = input?.signature;
|
|
37
|
+
if (!signature || !signature.sig)
|
|
38
|
+
return { ok: false, reason: 'unsigned', message: '动作没有签名 (桌面拒收未签名的手机动作)' };
|
|
39
|
+
if (!pub)
|
|
40
|
+
return { ok: false, reason: 'unknown_device', message: `未知设备 ${action.deviceId} (先在桌面登记这台手机的公钥)` };
|
|
41
|
+
if (pub.deviceId !== action.deviceId || signature.deviceId !== action.deviceId) {
|
|
42
|
+
return { ok: false, reason: 'device_mismatch', message: '签名设备与载荷设备不一致' };
|
|
43
|
+
}
|
|
44
|
+
if (signature.alg !== 'ed25519')
|
|
45
|
+
return { ok: false, reason: 'bad_signature', message: `签名算法不是 ed25519: ${String(signature.alg)}` };
|
|
46
|
+
const payload = canonicalTaskActionPayload(action);
|
|
47
|
+
if (signature.payloadHash && signature.payloadHash !== payloadHashOf(action)) {
|
|
48
|
+
return { ok: false, reason: 'payload_tampered', message: '载荷哈希对不上 (动作在签名后被改过)' };
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const ok = crypto.verify(null, Buffer.from(payload, 'utf8'), crypto.createPublicKey(String(pub.publicKeyPem)), Buffer.from(String(signature.sig), 'base64'));
|
|
52
|
+
if (!ok)
|
|
53
|
+
return { ok: false, reason: 'bad_signature', message: '设备签名验不过 (不是这台设备签的, 或载荷被改过)' };
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
return { ok: false, reason: 'verify_error', message: `验签抛出: ${String(err?.message || err).slice(0, 120)}` };
|
|
57
|
+
}
|
|
58
|
+
const fresh = checkTaskActionFreshness(action, opts.now ?? Date.now());
|
|
59
|
+
if (!fresh.ok)
|
|
60
|
+
return { ok: false, reason: fresh.reason, message: `动作时效不合法: ${fresh.reason} (TTL 上限 ${TASK_ACTION_TTL_MS / 60000} 分钟)` };
|
|
61
|
+
if (opts.req) {
|
|
62
|
+
if (String(opts.req.kind) !== action.kind) {
|
|
63
|
+
return { ok: false, reason: 'kind_mismatch', message: `请求类型 (${opts.req.kind}) 与签名动作类型 (${action.kind}) 不一致` };
|
|
64
|
+
}
|
|
65
|
+
const kindOk = actionKindAllowsRequest(action.kind, opts.req);
|
|
66
|
+
if (!kindOk.ok)
|
|
67
|
+
return { ok: false, reason: 'kind_shape_mismatch', message: kindOk.error || '动作类型与请求字段不匹配' };
|
|
68
|
+
const expect = contentDigestOf(opts.req);
|
|
69
|
+
if (expect !== action.contentDigest) {
|
|
70
|
+
return { ok: false, reason: 'content_mismatch', message: '内容摘要对不上: 签名绑定的不是你这次要执行的内容' };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return { ok: true, action };
|
|
74
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mobile-task-actions.ts — 手机端**高风险任务动作**的授权载荷 (单一规范, 2026-09-25)
|
|
3
|
+
*
|
|
4
|
+
* 为什么需要它: 手机新增的四项能力里, 有三个是**会对外产生后果**的动作 ——
|
|
5
|
+
* 入群 (让本机进入一个公开 store) · 发任务公告 (把公告发进群 / 往注册表公告) · 过程留痕 (接单/交付/初筛/终审)。
|
|
6
|
+
* 这些动作**不能**只凭"手机发了个 HTTP 请求"就执行: 任何能碰到桌面的本地进程都能伪造这样一个请求。
|
|
7
|
+
* 所以沿用联系方式授权已有的那套纪律 (contacts/grant-payload.ts + mobile-contacts.ts):
|
|
8
|
+
* **手机展示待发内容 → 用户确认 → 设备私钥 (Ed25519) 签一条规范载荷 → 桌面验签后才执行**。
|
|
9
|
+
*
|
|
10
|
+
* 本文件只放**纯函数与类型** (无 node: 导入), 理由与 grant-payload 相同:
|
|
11
|
+
* 手机 (WebCrypto) 与桌面 (Node crypto) 必须对同一条动作算出**同一个字节串**, 否则签名永远验不过。
|
|
12
|
+
*
|
|
13
|
+
* 三条不可回退的性质:
|
|
14
|
+
* 1. **绑定内容**: 载荷里带 `contentDigest` = sha256(规范化后的动作内容)。桌面在执行前**自己重算一遍**,
|
|
15
|
+
* 对不上就拒 —— 签名对"入群"有效, 就不能被换成一个"发公告"的请求 (类型绑定), 也不能被换内容。
|
|
16
|
+
* 2. **短时效**: `expiresAt - createdAt ≤ TASK_ACTION_TTL_MS`。一张"很久以后还能用"的空白批准等于没有批准。
|
|
17
|
+
* 3. **不带私钥材料**: 载荷里只有 deviceId + 摘要 + 时间, 永远不含私钥/明文。
|
|
18
|
+
*
|
|
19
|
+
* 桌面侧验签实现见 `mobile-task-actions-verify.ts` (node crypto); 手机侧签名见 `src/web/mobile-tasks.ts`。
|
|
20
|
+
*/
|
|
21
|
+
/** 手机可发起的高风险任务动作 (闭集; 每一项都必须走签名纪律) */
|
|
22
|
+
export const MOBILE_TASK_ACTION_KINDS = [
|
|
23
|
+
'group_join', // 入群 (加入一个 OrbitDB 群 store)
|
|
24
|
+
'group_leave', // 退群
|
|
25
|
+
'group_create', // 建群 (会自动入群)
|
|
26
|
+
'announce_publish', // 发布任务公告 (落盘公告 + 向注册表公告 + 记脉冲)
|
|
27
|
+
'announce_to_group', // 把板上的公告推进群里 (CLI: bolloon task announce)
|
|
28
|
+
'trail_post', // 过程留痕: 接单/交付/初筛/终审 (CLI: bolloon task post)
|
|
29
|
+
];
|
|
30
|
+
/** 参与签名的字段 (顺序即规范顺序, 改动等于换协议版本) */
|
|
31
|
+
export const TASK_ACTION_SIGNED_FIELDS = [
|
|
32
|
+
'actionId', 'kind', 'deviceId', 'ownerDid', 'targetRef', 'contentDigest',
|
|
33
|
+
'createdAt', 'expiresAt', 'grantedBy', 'via',
|
|
34
|
+
];
|
|
35
|
+
/** 动作是干什么的 (人话) —— 手机确认页与桌面错误信息共用 */
|
|
36
|
+
export const TASK_ACTION_LABELS = {
|
|
37
|
+
group_join: { zh: '加入这个群', en: 'Join this group' },
|
|
38
|
+
group_leave: { zh: '退出这个群', en: 'Leave this group' },
|
|
39
|
+
group_create: { zh: '建一个新群', en: 'Create a new group' },
|
|
40
|
+
announce_publish: { zh: '发布任务公告', en: 'Publish a task announcement' },
|
|
41
|
+
announce_to_group: { zh: '把这条公告发进群', en: 'Post this announcement into the group' },
|
|
42
|
+
trail_post: { zh: '往群里留一条过程痕迹', en: 'Post a process trail entry into the group' },
|
|
43
|
+
};
|
|
44
|
+
/** 动作的时效上限 (10 分钟)。超过 = 拒 (长命批准 = 没有批准) */
|
|
45
|
+
export const TASK_ACTION_TTL_MS = 10 * 60_000;
|
|
46
|
+
/** 手机侧默认签多长 (5 分钟): 够用户点一下, 又不会留下一张长期通行证 */
|
|
47
|
+
export const TASK_ACTION_DEFAULT_TTL_MS = 5 * 60_000;
|
|
48
|
+
/** createdAt 允许的时钟偏移 (桌面/手机不是同一块表) */
|
|
49
|
+
export const TASK_ACTION_CLOCK_SKEW_MS = 2 * 60_000;
|
|
50
|
+
/** 空值一律折成 `-` 并转义分隔符, 保证同一份请求算出的文本**逐字节稳定** */
|
|
51
|
+
function tok(v) {
|
|
52
|
+
if (v === null || v === undefined)
|
|
53
|
+
return '-';
|
|
54
|
+
const s = String(v);
|
|
55
|
+
if (!s)
|
|
56
|
+
return '-';
|
|
57
|
+
// 换行会破坏"一行一字段"的稳定性 (也不该出现在摘要输入里)
|
|
58
|
+
return s.replace(/\r?\n/g, '\\n');
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* 规范化动作内容 (唯一实现; 手机与桌面都调它, 再各自做 sha256)。
|
|
62
|
+
* 字段顺序固定 —— 顺序变了等于换协议。
|
|
63
|
+
*/
|
|
64
|
+
export function taskActionContentText(req) {
|
|
65
|
+
const kind = String(req?.kind ?? '');
|
|
66
|
+
const base = [
|
|
67
|
+
`kind=${tok(kind)}`,
|
|
68
|
+
`group=${tok(req?.groupRef)}`,
|
|
69
|
+
`ann=${tok(req?.announcementId)}`,
|
|
70
|
+
];
|
|
71
|
+
if (kind === 'announce_publish') {
|
|
72
|
+
base.push(`cap=${tok(req?.capability)}`);
|
|
73
|
+
base.push(`budget=${tok(req?.budgetHuman)}${req?.currency ? `@${tok(req.currency)}` : ''}`);
|
|
74
|
+
base.push(`deadline=${tok(req?.deadline)}`);
|
|
75
|
+
base.push(`instruction=${tok(req?.instruction)}`);
|
|
76
|
+
}
|
|
77
|
+
else if (kind === 'announce_to_group') {
|
|
78
|
+
base.push(`criteria=${tok(req?.criteria)}`);
|
|
79
|
+
}
|
|
80
|
+
else if (kind === 'trail_post') {
|
|
81
|
+
base.push(`trail=${tok(req?.trailKind)}`);
|
|
82
|
+
base.push(`round=${tok(req?.round)}`);
|
|
83
|
+
base.push(`price=${tok(req?.price)}`);
|
|
84
|
+
base.push(`hash=${tok(req?.hash)}`);
|
|
85
|
+
base.push(`bytes=${tok(req?.bytes)}`);
|
|
86
|
+
base.push(`checks=${tok(req?.checks)}`);
|
|
87
|
+
base.push(`verdict=${tok(req?.verdict)}`);
|
|
88
|
+
}
|
|
89
|
+
return base.join('\n');
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* 规范载荷 (逐字节稳定: 显式字段序 + 空值折 `''`, 不含 signature 自身)。
|
|
93
|
+
* 与 `canonicalGrantPayload` 同风格 —— 两端算出的字节串必须完全一致。
|
|
94
|
+
*/
|
|
95
|
+
export function canonicalTaskActionPayload(a) {
|
|
96
|
+
return JSON.stringify({
|
|
97
|
+
actionId: a.actionId, kind: a.kind, deviceId: a.deviceId, ownerDid: a.ownerDid,
|
|
98
|
+
targetRef: a.targetRef || '', contentDigest: a.contentDigest,
|
|
99
|
+
createdAt: a.createdAt, expiresAt: a.expiresAt, grantedBy: a.grantedBy, via: a.via,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
/** 动作 id 形状 (幂等/追溯用; 不含任何身份材料) */
|
|
103
|
+
export function isTaskActionId(id) {
|
|
104
|
+
return /^act-[A-Za-z0-9._-]{4,64}$/.test(String(id || '').trim());
|
|
105
|
+
}
|
|
106
|
+
/** sha256 hex 形状 */
|
|
107
|
+
export function isDigestHex(s) {
|
|
108
|
+
return /^[0-9a-f]{64}$/.test(String(s || '').trim());
|
|
109
|
+
}
|
|
110
|
+
/** ISO 时间串形状 (可被 Date.parse 解出) */
|
|
111
|
+
function isIso(s) {
|
|
112
|
+
return typeof s === 'string' && s.length >= 20 && Number.isFinite(Date.parse(s));
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* 时效检查 (桌面与手机都跑):
|
|
116
|
+
* · createdAt/expiresAt 必须是能解析的 ISO 串
|
|
117
|
+
* · expiresAt - createdAt ≤ TTL 上限
|
|
118
|
+
* · now 落在 [createdAt - skew, expiresAt] 之内
|
|
119
|
+
*/
|
|
120
|
+
export function checkTaskActionFreshness(a, now) {
|
|
121
|
+
if (!isIso(a?.createdAt) || !isIso(a?.expiresAt))
|
|
122
|
+
return { ok: false, reason: 'bad_time' };
|
|
123
|
+
const created = Date.parse(a.createdAt);
|
|
124
|
+
const expires = Date.parse(a.expiresAt);
|
|
125
|
+
if (expires - created > TASK_ACTION_TTL_MS)
|
|
126
|
+
return { ok: false, reason: 'ttl_too_long' };
|
|
127
|
+
if (expires < created)
|
|
128
|
+
return { ok: false, reason: 'bad_time' };
|
|
129
|
+
if (now > expires)
|
|
130
|
+
return { ok: false, reason: 'expired' };
|
|
131
|
+
if (now < created - TASK_ACTION_CLOCK_SKEW_MS)
|
|
132
|
+
return { ok: false, reason: 'not_yet_valid' };
|
|
133
|
+
return { ok: true };
|
|
134
|
+
}
|
|
135
|
+
/** 载荷形状检查 (验签前的形状闸: 少了字段就别去碰公钥) */
|
|
136
|
+
export function checkTaskActionShape(input) {
|
|
137
|
+
const a = input?.action || input;
|
|
138
|
+
if (!a || typeof a !== 'object')
|
|
139
|
+
return { ok: false, reason: 'missing_action' };
|
|
140
|
+
if (!isTaskActionId(a.actionId))
|
|
141
|
+
return { ok: false, reason: 'bad_action_id' };
|
|
142
|
+
if (!MOBILE_TASK_ACTION_KINDS.includes(String(a.kind)))
|
|
143
|
+
return { ok: false, reason: 'unknown_kind' };
|
|
144
|
+
if (!/^dev-[A-Za-z0-9._-]{4,40}$/.test(String(a.deviceId || '')))
|
|
145
|
+
return { ok: false, reason: 'bad_device_id' };
|
|
146
|
+
if (!isDigestHex(a.contentDigest))
|
|
147
|
+
return { ok: false, reason: 'bad_content_digest' };
|
|
148
|
+
if (a.via !== 'mobile')
|
|
149
|
+
return { ok: false, reason: 'bad_via' };
|
|
150
|
+
if (!isIso(a.createdAt) || !isIso(a.expiresAt))
|
|
151
|
+
return { ok: false, reason: 'bad_time' };
|
|
152
|
+
return { ok: true, action: a };
|
|
153
|
+
}
|
|
154
|
+
/** 桌面侧: 请求里必须只有**属于这个 kind** 的业务字段 (防止一次入群签名被塞进发公告的字段) */
|
|
155
|
+
export function actionKindAllowsRequest(kind, req) {
|
|
156
|
+
const hasGroup = !!(req?.groupRef && String(req.groupRef).trim());
|
|
157
|
+
const hasAnn = !!(req?.announcementId && String(req.announcementId).trim());
|
|
158
|
+
switch (kind) {
|
|
159
|
+
case 'group_join':
|
|
160
|
+
return hasGroup ? { ok: true } : { ok: false, error: '入群需要群链接 (groupRef)' };
|
|
161
|
+
case 'group_leave':
|
|
162
|
+
return hasGroup ? { ok: true } : { ok: false, error: '退群需要群 (groupRef)' };
|
|
163
|
+
case 'group_create':
|
|
164
|
+
return { ok: true };
|
|
165
|
+
case 'announce_publish':
|
|
166
|
+
return hasGroup ? { ok: false, error: '发布公告不进群 (要发进群请用 announce_to_group) —— 这类动作必须分开签' } : { ok: true };
|
|
167
|
+
case 'announce_to_group':
|
|
168
|
+
return hasGroup && hasAnn ? { ok: true } : { ok: false, error: '公告入群需要群 (groupRef) 与公告号 (announcementId)' };
|
|
169
|
+
case 'trail_post':
|
|
170
|
+
if (!hasGroup || !hasAnn)
|
|
171
|
+
return { ok: false, error: '过程留痕要挂在某一条公告上, 且要指定群 (groupRef + announcementId)' };
|
|
172
|
+
return ['claim', 'deliver', 'screen', 'final'].includes(String(req?.trailKind))
|
|
173
|
+
? { ok: true }
|
|
174
|
+
: { ok: false, error: '过程留痕需要 trailKind (claim|deliver|screen|final)' };
|
|
175
|
+
default:
|
|
176
|
+
return { ok: false, error: `未知动作: ${String(kind || '')}` };
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mobile-task-views.ts — 手机端「任务」页的**视图投影** (2026-09-25)
|
|
3
|
+
*
|
|
4
|
+
* 桌面 API 返回的是完整事实 (给机器看的); 手机页面要的是**能直接渲染、且不含标识符**的行。
|
|
5
|
+
* 投影集中放在这里 (纯函数), 因为这几条是硬要求, 必须能被单测钉住:
|
|
6
|
+
* · **不出现** 原始 DID / peerId / IP / 钱包地址 / 群 store 地址 / 任务正文 —— 每条 id 只给缩短形式
|
|
7
|
+
* · 群消息里的事实本来就由桌面侧 `summarizeTrail` 遮蔽过 (这里只再挡一道, 不重复造规则表)
|
|
8
|
+
* · 相对时间给**毫秒差**, 由 UI 只改文字节点 (不在投影里拼字符串, 免得时间一成不变)
|
|
9
|
+
*
|
|
10
|
+
* 词汇表 (中文) 与 `task-group.ts` 的 `TRAIL_KIND_LABEL` 对齐 —— 有单测钉住"不许漂移"。
|
|
11
|
+
*/
|
|
12
|
+
import { scanPublicText } from './task-public-text.js';
|
|
13
|
+
/** id 缩短显示 (前 n 位 + 省略号); 空 → 空串 (不显示 `undefined`) */
|
|
14
|
+
export function shortId(id, n = 12) {
|
|
15
|
+
const s = String(id ?? '').trim();
|
|
16
|
+
if (!s)
|
|
17
|
+
return '';
|
|
18
|
+
return s.length <= n ? s : `${s.slice(0, n)}…`;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* 协议 id (公告号 / 动作号 / OrbitDB 地址) —— 这些不是 DID/地址, 规则表不认,
|
|
22
|
+
* 但**原文一样不许上手机页面**, 一律走 shortened 形式。
|
|
23
|
+
*/
|
|
24
|
+
export function isProtocolId(s) {
|
|
25
|
+
const v = String(s ?? '').trim();
|
|
26
|
+
return /^ann-[0-9a-f]{8,}$/i.test(v)
|
|
27
|
+
|| /^act-[a-z0-9-]{6,}$/i.test(v)
|
|
28
|
+
|| /^zdpu[0-9a-zA-Z]{8,}$/.test(v)
|
|
29
|
+
|| /^\/orbitdb\//.test(v);
|
|
30
|
+
}
|
|
31
|
+
/** 「多久以前 / 还有多久」的人话 (毫秒差 → 双语; UI 只把结果写进文字节点) */
|
|
32
|
+
export function formatRelative(ms, lang) {
|
|
33
|
+
if (ms === null || !Number.isFinite(ms))
|
|
34
|
+
return lang === 'zh' ? '—' : '—';
|
|
35
|
+
const past = ms >= 0;
|
|
36
|
+
const abs = Math.abs(ms);
|
|
37
|
+
const min = Math.floor(abs / 60_000);
|
|
38
|
+
const hr = Math.floor(abs / 3_600_000);
|
|
39
|
+
const day = Math.floor(abs / 86_400_000);
|
|
40
|
+
const unit = day >= 1
|
|
41
|
+
? (lang === 'zh' ? `${day} 天` : `${day} d`)
|
|
42
|
+
: hr >= 1
|
|
43
|
+
? (lang === 'zh' ? `${hr} 小时` : `${hr} h`)
|
|
44
|
+
: min >= 1
|
|
45
|
+
? (lang === 'zh' ? `${min} 分钟` : `${min} min`)
|
|
46
|
+
: (lang === 'zh' ? '不到 1 分钟' : 'under a minute');
|
|
47
|
+
if (lang === 'zh')
|
|
48
|
+
return past ? `${unit}前` : `${unit}后`;
|
|
49
|
+
return past ? `${unit} ago` : `in ${unit}`;
|
|
50
|
+
}
|
|
51
|
+
// ── 公告板 ──────────────────────────────────────────────────────────────────
|
|
52
|
+
export const STATUS_LABELS = {
|
|
53
|
+
open: { zh: '可接单', en: 'Open for claims' },
|
|
54
|
+
claimed: { zh: '已被接单', en: 'Claimed' },
|
|
55
|
+
cancelled: { zh: '已取消', en: 'Cancelled' },
|
|
56
|
+
unknown: { zh: '状态未知', en: 'Unknown status' },
|
|
57
|
+
};
|
|
58
|
+
function budgetLabelOf(b) {
|
|
59
|
+
if (!b || typeof b !== 'object')
|
|
60
|
+
return '';
|
|
61
|
+
const amt = String(b.maxAmount ?? '').trim();
|
|
62
|
+
if (!amt)
|
|
63
|
+
return '';
|
|
64
|
+
return `${amt} ${String(b.currency || '').toUpperCase()}${b.network ? ` · ${String(b.network)}` : ''}`;
|
|
65
|
+
}
|
|
66
|
+
/** 板上的一条 → 手机行 (buyerDid / claimedBy 一律丢弃: 那是身份, 不是手机该显示的东西) */
|
|
67
|
+
export function boardItemOf(e, now) {
|
|
68
|
+
const dl = Number.isFinite(Number(e?.deadline)) ? Number(e.deadline) : null;
|
|
69
|
+
const ca = Number.isFinite(Number(e?.createdAt)) ? Number(e.createdAt) : null;
|
|
70
|
+
const status = String(e?.status || 'unknown');
|
|
71
|
+
return {
|
|
72
|
+
idShort: shortId(e?.announcementId),
|
|
73
|
+
ref: String(e?.announcementId || ''),
|
|
74
|
+
capability: String(e?.capability || ''),
|
|
75
|
+
status,
|
|
76
|
+
statusLabel: STATUS_LABELS[status] || STATUS_LABELS.unknown,
|
|
77
|
+
budgetLabel: budgetLabelOf(e?.budget),
|
|
78
|
+
deadlineInMs: dl === null ? null : dl - now,
|
|
79
|
+
createdAtMs: ca,
|
|
80
|
+
claimCount: Number.isFinite(Number(e?.claimCount)) ? Number(e.claimCount) : 0,
|
|
81
|
+
preview: e?.instructionPreview ? String(e.instructionPreview).slice(0, 120) : null,
|
|
82
|
+
remote: !!e?.remote,
|
|
83
|
+
claimable: !!e?.claimable,
|
|
84
|
+
signatureVerified: e?.signatureVerified === true ? true : (e?.signatureVerified === false ? false : null),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
export function buildBoardView(view, now) {
|
|
88
|
+
const items = (Array.isArray(view?.entries) ? view.entries : []).map((e) => boardItemOf(e, now));
|
|
89
|
+
return {
|
|
90
|
+
items,
|
|
91
|
+
localCount: Number(view?.localCount) || 0,
|
|
92
|
+
remoteCount: Number(view?.remoteCount) || 0,
|
|
93
|
+
registryReady: view?.registryReady === true,
|
|
94
|
+
registryError: view?.registryError ? String(view.registryError).slice(0, 160) : null,
|
|
95
|
+
notes: Array.isArray(view?.notes) ? view.notes.map((n) => String(n)) : [],
|
|
96
|
+
openCount: items.filter((i) => i.claimable).length,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
// ── 过程痕迹 (trail) ────────────────────────────────────────────────────────
|
|
100
|
+
export const TRAIL_KIND_LABELS = {
|
|
101
|
+
announce: { zh: '公告', en: 'Announce' },
|
|
102
|
+
claim: { zh: '接单声明', en: 'Claim' },
|
|
103
|
+
deliver: { zh: '交付', en: 'Delivery' },
|
|
104
|
+
screen: { zh: '初筛结果', en: 'Screening' },
|
|
105
|
+
final: { zh: '终审结论', en: 'Final verdict' },
|
|
106
|
+
};
|
|
107
|
+
export const INCONSISTENCY_LABELS = {
|
|
108
|
+
'final-accept-without-delivery': { zh: '说通过了, 但群里没有交付痕迹', en: 'Accepted, but no delivery trace in the group' },
|
|
109
|
+
'screen-without-delivery': { zh: '有初筛结果, 但没有交付痕迹', en: 'Screening exists without a delivery trace' },
|
|
110
|
+
'claim-without-announce': { zh: '有人接单, 但群里没有公告痕迹', en: 'Claim without an announcement trace' },
|
|
111
|
+
'deliver-without-claim': { zh: '有交付, 但没人接过单', en: 'Delivery without a claim' },
|
|
112
|
+
'both-accept-and-reject': { zh: '同时出现通过和拒绝', en: 'Both accepted and rejected appear' },
|
|
113
|
+
'group-message-hit-privacy-rule': { zh: '群里有人发了带标识符的消息 (已遮蔽, 不当事实采用)', en: 'A group message hit a privacy rule (masked, not used as fact)' },
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* 时间线 → 手机行 (字段再过一遍规则表: 命中就只报规则名, 不回显内容)
|
|
117
|
+
*
|
|
118
|
+
* 2026-09-25: 除了规则表,**协议 id 一律缩短**。原因: `ann-<16hex>` 不是 DID/地址, 规则表不认它,
|
|
119
|
+
* 但它就是公告号原文 —— 手机页面上不许出现 (要认哪条公告用缩短形式 + 页面自己按行索引回填)。
|
|
120
|
+
*/
|
|
121
|
+
export function buildTrailView(summary) {
|
|
122
|
+
const entries = (Array.isArray(summary?.entries) ? summary.entries : []).map((e) => {
|
|
123
|
+
const facts = [];
|
|
124
|
+
for (const [k, v] of Object.entries(e?.fields || {})) {
|
|
125
|
+
if (k === 'kind')
|
|
126
|
+
continue;
|
|
127
|
+
const raw = String(v ?? '');
|
|
128
|
+
const hit = scanPublicText(raw);
|
|
129
|
+
if (hit.length) {
|
|
130
|
+
facts.push({ k: String(k), v: `[已遮蔽:${hit[0].rule}]` });
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
facts.push({ k: String(k), v: k === 'id' || isProtocolId(raw) ? shortId(raw) : raw });
|
|
134
|
+
}
|
|
135
|
+
const kind = String(e?.kind || '');
|
|
136
|
+
return {
|
|
137
|
+
idShort: shortId(e?.announcementId),
|
|
138
|
+
kind,
|
|
139
|
+
kindLabel: TRAIL_KIND_LABELS[kind] || { zh: kind, en: kind },
|
|
140
|
+
sender: String(e?.sender || 'unknown'),
|
|
141
|
+
atMs: Number.isFinite(Number(e?.at)) ? Number(e.at) : 0,
|
|
142
|
+
facts,
|
|
143
|
+
};
|
|
144
|
+
});
|
|
145
|
+
const inc = (Array.isArray(summary?.inconsistencies) ? summary.inconsistencies : []).map((c) => ({
|
|
146
|
+
code: String(c),
|
|
147
|
+
label: INCONSISTENCY_LABELS[String(c)] || { zh: String(c), en: String(c) },
|
|
148
|
+
}));
|
|
149
|
+
return {
|
|
150
|
+
count: Number(summary?.count) || entries.length,
|
|
151
|
+
byKind: (summary?.byKind || {}),
|
|
152
|
+
entries,
|
|
153
|
+
announcementIdsShort: (Array.isArray(summary?.announcements) ? summary.announcements : []).map((a) => shortId(a)),
|
|
154
|
+
flags: (summary?.flags || {}),
|
|
155
|
+
inconsistencies: inc,
|
|
156
|
+
redactedCount: Array.isArray(summary?.redacted) ? summary.redacted.length : 0,
|
|
157
|
+
ignoredMessages: Number(summary?.ignoredMessages) || 0,
|
|
158
|
+
privacyHits: inc.some((x) => x.code === 'group-message-hit-privacy-rule') || (Array.isArray(summary?.redacted) && summary.redacted.length > 0),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
/** 本机已加入的群 → 手机行 (store 地址/邀请链接都不在这里: 要链接得显式取, 与 CLI 同一纪律) */
|
|
162
|
+
export function buildGroupsView(groups) {
|
|
163
|
+
return (Array.isArray(groups) ? groups : []).map((g) => ({
|
|
164
|
+
idShort: shortId(g?.id),
|
|
165
|
+
name: String(g?.name || ''),
|
|
166
|
+
joinedAt: String(g?.createdAt || ''),
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
169
|
+
// ── 动作请求 → 确认页文案 (展示待发内容) ────────────────────────────────────
|
|
170
|
+
/** 把动作请求压成"你将要发出什么"的人话 (确认页用; 文案里不含标识符) */
|
|
171
|
+
export function describeTaskActionForConfirm(kind, req) {
|
|
172
|
+
const lines = [];
|
|
173
|
+
const push = (k, kEn, v) => { const s = String(v ?? '').trim(); if (s && s !== '-')
|
|
174
|
+
lines.push({ k, kEn, v: s }); };
|
|
175
|
+
push('群', 'group', req?.groupRef ? shortId(req.groupRef, 24) : '');
|
|
176
|
+
push('公告号', 'announcement', req?.announcementId ? shortId(req.announcementId) : '');
|
|
177
|
+
push('能力', 'capability', req?.capability);
|
|
178
|
+
push('预算', 'budget', req?.budgetHuman ? `${req.budgetHuman} ${String(req?.currency || '').toUpperCase()}` : '');
|
|
179
|
+
push('任务正文', 'instruction', req?.instruction);
|
|
180
|
+
push('截止', 'deadline', req?.deadline);
|
|
181
|
+
push('验收判据', 'criteria', req?.criteria);
|
|
182
|
+
push('期号', 'round', req?.round);
|
|
183
|
+
push('痕迹类型', 'trail kind', req?.trailKind ? (TRAIL_KIND_LABELS[String(req.trailKind)]?.zh || String(req.trailKind)) : '');
|
|
184
|
+
push('声明价格', 'declared price', req?.price);
|
|
185
|
+
push('交付哈希', 'delivery hash', req?.hash);
|
|
186
|
+
push('字节数', 'bytes', req?.bytes);
|
|
187
|
+
push('逐条初筛', 'per-item screen', req?.checks);
|
|
188
|
+
push('终审结论', 'final verdict', req?.verdict);
|
|
189
|
+
const titles = {
|
|
190
|
+
group_join: { zh: '加入这个群', en: 'Join this group' },
|
|
191
|
+
group_leave: { zh: '退出这个群', en: 'Leave this group' },
|
|
192
|
+
group_create: { zh: '建一个新群', en: 'Create a new group' },
|
|
193
|
+
announce_publish: { zh: '发布任务公告', en: 'Publish a task announcement' },
|
|
194
|
+
announce_to_group: { zh: '把这期公告发进群', en: 'Post this announcement into the group' },
|
|
195
|
+
trail_post: { zh: '往群里留一条过程痕迹', en: 'Post a process trail entry into the group' },
|
|
196
|
+
};
|
|
197
|
+
const t = titles[String(kind)] || { zh: '执行这个动作', en: 'Run this action' };
|
|
198
|
+
return { titleZh: t.zh, titleEn: t.en, lines };
|
|
199
|
+
}
|