@bolloon/bolloon-agent 0.4.33 → 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.
- package/dist/agents/contacts/chain.js +27 -7
- package/dist/agents/execution-supervisor.js +493 -35
- 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/network-pulse.js +304 -18
- package/dist/agents/pi-sdk.js +75 -20
- package/dist/agents/run-store.js +18 -5
- package/dist/agents/skill-readiness.js +52 -10
- package/dist/agents/subagent-manager.js +209 -3
- package/dist/agents/task/task-runner.js +9 -3
- package/dist/agents/task-group.js +8 -51
- package/dist/agents/task-public-text.js +89 -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/setup-wizard.js +71 -0
- package/dist/cli/update-commands.js +150 -24
- package/dist/cli-entry.js +12 -2
- 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 +492 -34
- 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/network-pulse.js +307 -19
- package/dist/electron-build/agents/network-pulse.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk.js +74 -19
- package/dist/electron-build/agents/pi-sdk.js.map +1 -1
- package/dist/electron-build/agents/run-store.js +20 -5
- 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 +8 -2
- package/dist/electron-build/agents/task/task-runner.js.map +1 -1
- 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.js +471 -0
- package/dist/electron-build/utils/dual-source.js.map +1 -0
- package/dist/electron-build/utils/update-manager.js +331 -57
- package/dist/electron-build/utils/update-manager.js.map +1 -1
- package/dist/electron-build/utils/update-state.js +16 -1
- package/dist/electron-build/utils/update-state.js.map +1 -1
- package/dist/electron-build/utils/version-info.js +82 -2
- package/dist/electron-build/utils/version-info.js.map +1 -1
- package/dist/index.js +103 -12
- 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 +329 -0
- package/dist/ios/mobile-core.js +1379 -226
- package/dist/ios/mobile-privacy.js +185 -0
- package/dist/ios/mobile.css +19 -0
- package/dist/ios/mobile.html +96 -0
- package/dist/ios/mobile.js +706 -7
- package/dist/ios/routes-chain-index.js +108 -0
- package/dist/ios/routes-contacts.js +420 -0
- package/dist/ios/routes-onchain-trade.js +99 -0
- package/dist/ios/routes-x402-info.js +1 -0
- package/dist/ios/server.js +762 -0
- package/dist/ios/task-frame-server.js +211 -0
- package/dist/orbitdb/cid-database.js +81 -16
- package/dist/orbitdb/ipfs-node.js +31 -2
- package/dist/utils/dual-source.js +426 -0
- package/dist/utils/update-cli.js +16 -21
- package/dist/utils/update-health.js +35 -5
- package/dist/utils/update-manager.js +329 -57
- package/dist/utils/update-state.js +16 -1
- package/dist/utils/version-info.js +75 -1
- package/dist/web/mobile-contacts.js +44 -11
- package/dist/web/mobile-core.js +1197 -513
- package/dist/web/mobile-tasks.js +339 -0
- package/dist/web/mobile.css +19 -0
- package/dist/web/mobile.html +64 -0
- package/dist/web/mobile.js +531 -1
- package/dist/web/routes-mobile-tasks.js +353 -0
- package/dist/web/server.js +163 -4
- package/package.json +3 -1
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mobile-tasks.ts — 手机端「任务协作」能力层 (2026-09-25)
|
|
3
|
+
*
|
|
4
|
+
* 四项能力的手机侧实现 (桌面/CLI 同一套存储与协议, **手机不是第二个权威源**):
|
|
5
|
+
* ① 入群 → 桌面 `/api/mobile/tasks/groups*` → `gateway-group.ts` (与 CLI `bolloon task group …` 同一份 store)
|
|
6
|
+
* ② 发任务公告 → 桌面 `/api/mobile/tasks/announce|publish|post` → `task-group.ts` / `task-board.ts` (与 CLI 同函数)
|
|
7
|
+
* ③ 看飞轮进度 → 桌面 `/api/mobile/tasks/flywheel` → `goal-store` 只读 + `goal-flywheel` 冻结类型投影
|
|
8
|
+
* ④ 授权签名 → 每个**高风险**动作: 展示待发内容 → 设备私钥 (Ed25519) 签规范载荷 → 桌面验签才执行
|
|
9
|
+
*
|
|
10
|
+
* 纪律 (与本仓其它手机模块一致):
|
|
11
|
+
* · **浏览器安全**: 不 import 任何 node: 模块 (纯函数与 fetch)。
|
|
12
|
+
* · **不支持设备签名就明说**: 没有 WebCrypto/Ed25519 → 返回 device_signing_unavailable, **绝不发未签名请求**。
|
|
13
|
+
* · **桌面离线不假装成功**: 直接返回 desktop_offline (任务动作**不进离线队列** ——
|
|
14
|
+
* 一张几小时后突然生效的"发公告/入群"批准比失败更危险; 这条与 contacts 队列的取舍不同, 是刻意的)。
|
|
15
|
+
* · **本地先过隐私闸**: 群消息文本用桌面同一份规则表 (`task-public-text.ts`) 预检, 命中就本地拒发。
|
|
16
|
+
* · **只渲染脱敏视图**: 页面数据一律经 `mobile-task-views.ts` 投影 (id 只给缩短形式)。
|
|
17
|
+
*/
|
|
18
|
+
import { canonicalTaskActionPayload, taskActionContentText, checkTaskActionFreshness, TASK_ACTION_DEFAULT_TTL_MS, MOBILE_TASK_ACTION_KINDS, } from '../agents/mobile-task-actions.js';
|
|
19
|
+
import { requirePublicText } from '../agents/task-public-text.js';
|
|
20
|
+
import { buildBoardView, buildTrailView, buildGroupsView, describeTaskActionForConfirm, } from '../agents/mobile-task-views.js';
|
|
21
|
+
import { findInternalFieldLeaks } from '../agents/mobile-flywheel-view.js';
|
|
22
|
+
import { sha256HexOf, ed25519Available } from './mobile-contacts.js';
|
|
23
|
+
const K_TASKS_HINT = 'bolloon.mobile.tasks.hint.v1';
|
|
24
|
+
function defaultStorage() {
|
|
25
|
+
try {
|
|
26
|
+
if (typeof localStorage !== 'undefined')
|
|
27
|
+
return localStorage;
|
|
28
|
+
}
|
|
29
|
+
catch { /* 隐私模式 */ }
|
|
30
|
+
const mem = new Map();
|
|
31
|
+
return { getItem: (k) => (mem.has(k) ? mem.get(k) : null), setItem: (k, v) => { mem.set(k, v); }, removeItem: (k) => { mem.delete(k); } };
|
|
32
|
+
}
|
|
33
|
+
async function baseOf(deps) {
|
|
34
|
+
if (deps.base && String(deps.base).trim())
|
|
35
|
+
return String(deps.base).replace(/\/+$/, '');
|
|
36
|
+
try {
|
|
37
|
+
if (typeof localStorage !== 'undefined') {
|
|
38
|
+
const v = localStorage.getItem('bolloon_desktop_base_url');
|
|
39
|
+
if (v)
|
|
40
|
+
return String(v).replace(/\/+$/, '');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch { /* 忽略 */ }
|
|
44
|
+
return '';
|
|
45
|
+
}
|
|
46
|
+
function fetchOf(deps) {
|
|
47
|
+
return deps.fetchImpl || globalThis.fetch;
|
|
48
|
+
}
|
|
49
|
+
async function getJson(base, path, f) {
|
|
50
|
+
const res = await f(`${base}${path}`);
|
|
51
|
+
let json = null;
|
|
52
|
+
try {
|
|
53
|
+
json = await res.json();
|
|
54
|
+
}
|
|
55
|
+
catch { /* 空响应 */ }
|
|
56
|
+
return { status: res.status, json };
|
|
57
|
+
}
|
|
58
|
+
async function postJson(base, path, body, f) {
|
|
59
|
+
const res = await f(`${base}${path}`, {
|
|
60
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body ?? {}),
|
|
61
|
+
});
|
|
62
|
+
let json = null;
|
|
63
|
+
try {
|
|
64
|
+
json = await res.json();
|
|
65
|
+
}
|
|
66
|
+
catch { /* 空响应 */ }
|
|
67
|
+
return { status: res.status, json };
|
|
68
|
+
}
|
|
69
|
+
const OFFLINE_NOTE = '地址在 我 → 设置 → 电脑端地址 里填; 手机不替桌面记账';
|
|
70
|
+
/** 读接口统一错误映射 (桌面不在线 → 明说, 不返回空壳当成功) */
|
|
71
|
+
function mapFail(r, what) {
|
|
72
|
+
const code = r.json?.code ? String(r.json.code) : (r.status ? `desktop_http_${r.status}` : 'desktop_unreachable');
|
|
73
|
+
return { ok: false, code, error: `${what} 失败: ${r.json?.error || r.json?.message || code}`, note: OFFLINE_NOTE };
|
|
74
|
+
}
|
|
75
|
+
// ── 请求构造 (空字段一律显式 null: 桌面据此重算内容摘要) ─────────────────────
|
|
76
|
+
export function emptyTaskActionRequest(kind) {
|
|
77
|
+
return {
|
|
78
|
+
kind, groupRef: null, announcementId: null, capability: null, instruction: null,
|
|
79
|
+
budgetHuman: null, currency: null, deadline: null, criteria: null, round: null, price: null,
|
|
80
|
+
hash: null, bytes: null, checks: null, verdict: null, trailKind: null,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* 构造并签一个动作。**没有设备签名能力 → 直接失败** (没有"不签名也能过"的路径)。
|
|
85
|
+
* 群消息类动作在签名前先过本地隐私闸 (与桌面同一份规则表)。
|
|
86
|
+
*/
|
|
87
|
+
export async function signTaskAction(req, deps = {}) {
|
|
88
|
+
const storage = deps.storage || defaultStorage();
|
|
89
|
+
const c = deps.cryptoObj || globalThis.crypto;
|
|
90
|
+
if (!ed25519Available(c)) {
|
|
91
|
+
return { ok: false, code: 'device_signing_unavailable', error: '本机 WebView 不支持 Ed25519 设备签名 —— 请在桌面端执行这个动作 (或升级系统 WebView)' };
|
|
92
|
+
}
|
|
93
|
+
if (!MOBILE_TASK_ACTION_KINDS.includes(String(req?.kind))) {
|
|
94
|
+
return { ok: false, code: 'unknown_kind', error: `未知动作: ${String(req?.kind || '')}` };
|
|
95
|
+
}
|
|
96
|
+
// 群消息类动作: 先本地过闸 (桌面还会再过一遍, 两道都要)
|
|
97
|
+
const text = taskActionContentText(req);
|
|
98
|
+
if (req.kind === 'trail_post' || req.kind === 'announce_publish' || req.kind === 'announce_to_group') {
|
|
99
|
+
for (const fieldText of [req.round, req.price, req.criteria, req.checks, req.verdict, req.capability, req.hash]) {
|
|
100
|
+
if (!fieldText)
|
|
101
|
+
continue;
|
|
102
|
+
const gate = requirePublicText(String(fieldText));
|
|
103
|
+
if (!gate.ok) {
|
|
104
|
+
return { ok: false, code: 'privacy_denied', error: `本地隐私闸拦下: ${gate.violations.map((v) => v.rule).join(', ')} (群里/公告里不放标识符)`, violations: gate.violations };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const now = deps.now ? deps.now() : Date.now();
|
|
109
|
+
const ttl = Math.min(Number(deps.ttlMs) || TASK_ACTION_DEFAULT_TTL_MS, TASK_ACTION_DEFAULT_TTL_MS);
|
|
110
|
+
let contentDigest;
|
|
111
|
+
try {
|
|
112
|
+
contentDigest = await sha256HexOf(text, c);
|
|
113
|
+
}
|
|
114
|
+
catch (e) {
|
|
115
|
+
return { ok: false, code: 'device_signing_unavailable', error: String(e?.message || e) };
|
|
116
|
+
}
|
|
117
|
+
const action = {
|
|
118
|
+
actionId: `act-${now.toString(36)}-${Math.random().toString(36).slice(2, 8)}`,
|
|
119
|
+
kind: req.kind,
|
|
120
|
+
deviceId: '', // 由 signPayloadOnDevice 填 (它知道设备 id)
|
|
121
|
+
ownerDid: String(deps.ownerDid || '').trim(),
|
|
122
|
+
targetRef: String(req.groupRef || req.announcementId || ''),
|
|
123
|
+
contentDigest,
|
|
124
|
+
createdAt: new Date(now).toISOString(),
|
|
125
|
+
expiresAt: new Date(now + ttl).toISOString(),
|
|
126
|
+
grantedBy: deps.grantedBy || 'leo',
|
|
127
|
+
via: 'mobile',
|
|
128
|
+
};
|
|
129
|
+
// 设备 id 必须先落到载荷里 (签名是对**含 deviceId 的载荷**做的), 且与签名同源
|
|
130
|
+
let signature;
|
|
131
|
+
try {
|
|
132
|
+
const { loadOrCreateDeviceKey, signPayloadWithMaterial } = await import('./mobile-contacts.js');
|
|
133
|
+
const material = await loadOrCreateDeviceKey(storage, c);
|
|
134
|
+
action.deviceId = material.deviceId;
|
|
135
|
+
signature = await signPayloadWithMaterial(canonicalTaskActionPayload(action), material, c);
|
|
136
|
+
if (signature.deviceId !== action.deviceId) {
|
|
137
|
+
return { ok: false, code: 'device_signing_unavailable', error: '设备密钥不一致 (签名与载荷设备对不上) → 不发送' };
|
|
138
|
+
}
|
|
139
|
+
const fresh = checkTaskActionFreshness(action, now);
|
|
140
|
+
if (!fresh.ok)
|
|
141
|
+
return { ok: false, code: fresh.reason, error: `动作时效不合法: ${fresh.reason}` };
|
|
142
|
+
}
|
|
143
|
+
catch (e) {
|
|
144
|
+
return { ok: false, code: 'device_signing_unavailable', error: String(e?.message || e).slice(0, 160) };
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
ok: true,
|
|
148
|
+
data: { signed: { action, signature }, contentText: text, confirm: describeTaskActionForConfirm(req.kind, req) },
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
// ── 执行 (签名 → 桌面验签 → 桌面执行) ────────────────────────────────────────
|
|
152
|
+
export async function executeTaskAction(req, deps = {}) {
|
|
153
|
+
const storage = deps.storage || defaultStorage();
|
|
154
|
+
const base = await baseOf(deps);
|
|
155
|
+
if (!base)
|
|
156
|
+
return { ok: false, code: 'desktop_offline', error: '还没设置电脑端地址: 高风险动作必须由桌面执行 (手机签了名也要送到桌面)', note: OFFLINE_NOTE };
|
|
157
|
+
const signed = await signTaskAction(req, { ...deps, storage });
|
|
158
|
+
if (!signed.ok || !signed.data)
|
|
159
|
+
return { ok: false, code: signed.code, error: signed.error, violations: signed.violations };
|
|
160
|
+
const f = fetchOf(deps);
|
|
161
|
+
let r;
|
|
162
|
+
try {
|
|
163
|
+
r = await postJson(base, '/api/mobile/tasks/execute', { request: req, ...signed.data.signed }, f);
|
|
164
|
+
}
|
|
165
|
+
catch (e) {
|
|
166
|
+
return { ok: false, code: 'desktop_offline', error: `桌面不可达: ${String(e?.message || e).slice(0, 120)}`, note: OFFLINE_NOTE };
|
|
167
|
+
}
|
|
168
|
+
if (r.status !== 200 || !r.json?.ok)
|
|
169
|
+
return mapFail(r, '执行');
|
|
170
|
+
return { ok: true, data: r.json, note: '桌面已验签并执行 (同一套存储与协议)' };
|
|
171
|
+
}
|
|
172
|
+
/** 预览群消息正文 (由桌面用**将要发送的同一个构造器**生成; 只给要发进群的那一行) */
|
|
173
|
+
export async function previewGroupMessage(req, deps = {}) {
|
|
174
|
+
const base = await baseOf(deps);
|
|
175
|
+
if (!base)
|
|
176
|
+
return { ok: false, code: 'desktop_offline', error: '预览需要桌面在线 (群消息由桌面的构造器生成)', note: OFFLINE_NOTE };
|
|
177
|
+
const c = deps.cryptoObj || globalThis.crypto;
|
|
178
|
+
let digestPreview = '';
|
|
179
|
+
try {
|
|
180
|
+
digestPreview = await sha256HexOf(taskActionContentText(req), c);
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
digestPreview = '';
|
|
184
|
+
}
|
|
185
|
+
const f = fetchOf(deps);
|
|
186
|
+
try {
|
|
187
|
+
const r = await postJson(base, '/api/mobile/tasks/preview', { request: req }, f);
|
|
188
|
+
if (r.status !== 200 || !r.json?.ok)
|
|
189
|
+
return mapFail(r, '预览');
|
|
190
|
+
return { ok: true, data: { text: String(r.json.text || ''), contentText: taskActionContentText(req), digestPreview } };
|
|
191
|
+
}
|
|
192
|
+
catch (e) {
|
|
193
|
+
return { ok: false, code: 'desktop_offline', error: `桌面不可达: ${String(e?.message || e).slice(0, 120)}`, note: OFFLINE_NOTE };
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// ── ① 入群 (与 CLI `task group list|join|leave|create` 同一份 store) ─────────
|
|
197
|
+
export async function listGroups(deps = {}) {
|
|
198
|
+
const base = await baseOf(deps);
|
|
199
|
+
if (!base)
|
|
200
|
+
return { ok: false, code: 'desktop_offline', error: '看群列表需要电脑端在线 (群存在于桌面节点的 OrbitDB)', note: OFFLINE_NOTE };
|
|
201
|
+
try {
|
|
202
|
+
const r = await getJson(base, '/api/mobile/tasks/groups', fetchOf(deps));
|
|
203
|
+
if (r.status !== 200 || !r.json?.ok)
|
|
204
|
+
return mapFail(r, '读群列表');
|
|
205
|
+
return { ok: true, data: buildGroupsView(r.json.groups || []) };
|
|
206
|
+
}
|
|
207
|
+
catch (e) {
|
|
208
|
+
return { ok: false, code: 'desktop_offline', error: `桌面不可达: ${String(e?.message || e).slice(0, 120)}`, note: OFFLINE_NOTE };
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/** 入群 (高风险: 要签名) */
|
|
212
|
+
export function joinGroup(link, deps = {}) {
|
|
213
|
+
const req = { ...emptyTaskActionRequest('group_join'), groupRef: String(link || '').trim() };
|
|
214
|
+
return executeTaskAction(req, deps);
|
|
215
|
+
}
|
|
216
|
+
/** 退群 (高风险: 要签名) */
|
|
217
|
+
export function leaveGroup(groupRef, deps = {}) {
|
|
218
|
+
const req = { ...emptyTaskActionRequest('group_leave'), groupRef: String(groupRef || '').trim() };
|
|
219
|
+
return executeTaskAction(req, deps);
|
|
220
|
+
}
|
|
221
|
+
/** 建群 (高风险: 要签名; 建完自动入群) */
|
|
222
|
+
export function createGroup(name, deps = {}) {
|
|
223
|
+
const req = { ...emptyTaskActionRequest('group_create'), groupRef: String(name || '').trim() };
|
|
224
|
+
return executeTaskAction(req, deps);
|
|
225
|
+
}
|
|
226
|
+
// ── ② 发任务公告 ────────────────────────────────────────────────────────────
|
|
227
|
+
/** 看板 (只读): 与 CLI `bolloon task board` 同一份事实 */
|
|
228
|
+
export async function loadBoard(deps = {}) {
|
|
229
|
+
const base = await baseOf(deps);
|
|
230
|
+
if (!base)
|
|
231
|
+
return { ok: false, code: 'desktop_offline', error: '看板需要电脑端在线 (公告板在桌面节点上)', note: OFFLINE_NOTE };
|
|
232
|
+
const now = deps.now ? deps.now() : Date.now();
|
|
233
|
+
try {
|
|
234
|
+
const r = await getJson(base, '/api/mobile/tasks/board', fetchOf(deps));
|
|
235
|
+
if (r.status !== 200 || !r.json?.ok)
|
|
236
|
+
return mapFail(r, '读看板');
|
|
237
|
+
return { ok: true, data: buildBoardView(r.json, now) };
|
|
238
|
+
}
|
|
239
|
+
catch (e) {
|
|
240
|
+
return { ok: false, code: 'desktop_offline', error: `桌面不可达: ${String(e?.message || e).slice(0, 120)}`, note: OFFLINE_NOTE };
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/** 发布一条待接单公告 (高风险: 要签名; 与 CLI `task publish` 同一函数) */
|
|
244
|
+
export function publishAnnouncement(input, deps = {}) {
|
|
245
|
+
const req = {
|
|
246
|
+
...emptyTaskActionRequest('announce_publish'),
|
|
247
|
+
capability: String(input.capability || '').trim(),
|
|
248
|
+
instruction: String(input.instruction || ''),
|
|
249
|
+
budgetHuman: String(input.budgetHuman || '').trim(),
|
|
250
|
+
currency: String(input.currency || 'USDC').toUpperCase(),
|
|
251
|
+
deadline: input.deadline ? String(input.deadline).trim() : null,
|
|
252
|
+
};
|
|
253
|
+
return executeTaskAction(req, deps);
|
|
254
|
+
}
|
|
255
|
+
/** 把板上的一条公告发进群 (高风险: 要签名; 与 CLI `task announce` 同一函数) */
|
|
256
|
+
export function announceIntoGroup(input, deps = {}) {
|
|
257
|
+
const req = {
|
|
258
|
+
...emptyTaskActionRequest('announce_to_group'),
|
|
259
|
+
groupRef: String(input.groupRef || '').trim(),
|
|
260
|
+
announcementId: String(input.announcementId || '').trim(),
|
|
261
|
+
round: input.round ? String(input.round) : null,
|
|
262
|
+
criteria: input.criteria ? String(input.criteria) : null,
|
|
263
|
+
};
|
|
264
|
+
return executeTaskAction(req, deps);
|
|
265
|
+
}
|
|
266
|
+
/** 过程留痕: 接单 / 交付 / 初筛 / 终审 (高风险: 要签名; 与 CLI `task post` 同一函数) */
|
|
267
|
+
export function postTrail(input, deps = {}) {
|
|
268
|
+
const req = {
|
|
269
|
+
...emptyTaskActionRequest('trail_post'),
|
|
270
|
+
groupRef: String(input.groupRef || '').trim(),
|
|
271
|
+
announcementId: String(input.announcementId || '').trim(),
|
|
272
|
+
round: input.round ? String(input.round) : null,
|
|
273
|
+
price: input.price ? String(input.price) : null,
|
|
274
|
+
hash: input.hash ? String(input.hash) : null,
|
|
275
|
+
bytes: typeof input.bytes === 'number' ? input.bytes : null,
|
|
276
|
+
checks: input.checks ? String(input.checks) : null,
|
|
277
|
+
verdict: input.verdict ? String(input.verdict) : null,
|
|
278
|
+
trailKind: input.kind,
|
|
279
|
+
};
|
|
280
|
+
return executeTaskAction(req, deps);
|
|
281
|
+
}
|
|
282
|
+
/** 读群里的一期过程痕迹 (只读): 与 CLI `bolloon task trail --group …` 同一份汇总 */
|
|
283
|
+
export async function loadTrail(input, deps = {}) {
|
|
284
|
+
const base = await baseOf(deps);
|
|
285
|
+
if (!base)
|
|
286
|
+
return { ok: false, code: 'desktop_offline', error: '读群痕迹需要电脑端在线', note: OFFLINE_NOTE };
|
|
287
|
+
const q = new URLSearchParams();
|
|
288
|
+
q.set('group', String(input.groupRef || '').trim());
|
|
289
|
+
if (input.announcementId)
|
|
290
|
+
q.set('announcementId', String(input.announcementId));
|
|
291
|
+
if (input.limit)
|
|
292
|
+
q.set('limit', String(input.limit));
|
|
293
|
+
try {
|
|
294
|
+
const r = await getJson(base, `/api/mobile/tasks/trail?${q.toString()}`, fetchOf(deps));
|
|
295
|
+
if (r.status !== 200 || !r.json?.ok)
|
|
296
|
+
return mapFail(r, '读群痕迹');
|
|
297
|
+
return { ok: true, data: buildTrailView(r.json.summary || r.json) };
|
|
298
|
+
}
|
|
299
|
+
catch (e) {
|
|
300
|
+
return { ok: false, code: 'desktop_offline', error: `桌面不可达: ${String(e?.message || e).slice(0, 120)}`, note: OFFLINE_NOTE };
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
// ── ③ 看飞轮进度 (只读消费 goal-flywheel 冻结类型) ───────────────────────────
|
|
304
|
+
export async function loadFlywheel(deps = {}) {
|
|
305
|
+
const base = await baseOf(deps);
|
|
306
|
+
if (!base)
|
|
307
|
+
return { ok: false, code: 'desktop_offline', error: '看飞轮进度需要电脑端在线 (Goal 记录在桌面节点上)', note: OFFLINE_NOTE };
|
|
308
|
+
try {
|
|
309
|
+
const r = await getJson(base, '/api/mobile/tasks/flywheel', fetchOf(deps));
|
|
310
|
+
if (r.status !== 200 || !r.json?.ok)
|
|
311
|
+
return mapFail(r, '读飞轮进度');
|
|
312
|
+
const goals = Array.isArray(r.json.goals) ? r.json.goals : [];
|
|
313
|
+
// 桌面投影里若混进内部字段 → 手机**拒绝渲染** (宁可显示"读不到", 也不把内部状态摆给用户)
|
|
314
|
+
const leaks = findInternalFieldLeaks(JSON.stringify(goals));
|
|
315
|
+
if (leaks.length)
|
|
316
|
+
return { ok: false, code: 'view_leak', error: `视图里含内部字段 (${leaks.join(', ')}) → 拒绝渲染` };
|
|
317
|
+
return { ok: true, data: { goals, hint: r.json.hint ? String(r.json.hint) : null } };
|
|
318
|
+
}
|
|
319
|
+
catch (e) {
|
|
320
|
+
return { ok: false, code: 'desktop_offline', error: `桌面不可达: ${String(e?.message || e).slice(0, 120)}`, note: OFFLINE_NOTE };
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
// ── 小工具 (UI 直接用) ─────────────────────────────────────────────────────
|
|
324
|
+
/** 上一次操作的提示 (给 UI 跨页显示; 只存极短文本, 不含标识符) */
|
|
325
|
+
export function setTaskHint(text, storage = defaultStorage()) {
|
|
326
|
+
try {
|
|
327
|
+
storage.setItem(K_TASKS_HINT, String(text || '').slice(0, 160));
|
|
328
|
+
}
|
|
329
|
+
catch { /* 忽略 */ }
|
|
330
|
+
}
|
|
331
|
+
export function getTaskHint(storage = defaultStorage()) {
|
|
332
|
+
try {
|
|
333
|
+
return String(storage.getItem(K_TASKS_HINT) || '');
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
return '';
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
export { requirePublicText, describeTaskActionForConfirm, buildBoardView, buildTrailView, buildGroupsView };
|
package/dist/web/mobile.css
CHANGED
|
@@ -703,3 +703,22 @@ body {
|
|
|
703
703
|
.policy-body p { font-size: 13px; line-height: 1.75; color: var(--text-secondary); margin: 0 0 10px; }
|
|
704
704
|
.policy-body a { color: var(--accent); word-break: break-all; }
|
|
705
705
|
.policy-meta { font-size: 12px; color: var(--text-muted); }
|
|
706
|
+
|
|
707
|
+
/* ============ 任务 tab (2026-09-25) ============ */
|
|
708
|
+
/* 页面里的列表/按钮都复用既有 .list / .list-item / .sheet-choice;
|
|
709
|
+
这里只补两件事: 让长文本行不撑破卡片, 以及「减少动态效果」时把过渡/动画压到 0。 */
|
|
710
|
+
#page-tasks .list-item { align-items: flex-start; word-break: break-word; }
|
|
711
|
+
#page-tasks .list-item > div { min-width: 0; }
|
|
712
|
+
#page-tasks .section-label { word-break: break-word; }
|
|
713
|
+
#task-confirm-lines, #task-confirm-content { word-break: break-word; }
|
|
714
|
+
/* 标记: 页面顶部的签名状态行 (不能签名时用警示色, 但仍然只是一行文字) */
|
|
715
|
+
#tasks-signing[data-signing="no"] { color: #d08a2a; }
|
|
716
|
+
|
|
717
|
+
@media (prefers-reduced-motion: reduce) {
|
|
718
|
+
*, *::before, *::after {
|
|
719
|
+
animation-duration: 0.001ms !important;
|
|
720
|
+
animation-iteration-count: 1 !important;
|
|
721
|
+
transition-duration: 0.001ms !important;
|
|
722
|
+
scroll-behavior: auto !important;
|
|
723
|
+
}
|
|
724
|
+
}
|
package/dist/web/mobile.html
CHANGED
|
@@ -104,6 +104,49 @@
|
|
|
104
104
|
<div id="a2ui-root"></div>
|
|
105
105
|
</section>
|
|
106
106
|
|
|
107
|
+
<!-- 任务 tab (2026-09-25): 与桌面/CLI 同一套任务闭环 —— 入群 · 发公告 · 看飞轮进度
|
|
108
|
+
分工不变: 手机 = 输入 · 展示待发内容 · 设备签名确认; 桌面 = 落盘 · 执行 · 等待回复 · 证据。
|
|
109
|
+
高风险动作 (入群/建群/发公告/发进群/留痕) 一律走确认页 → 设备签名 → 桌面验签。 -->
|
|
110
|
+
<section class="page" id="page-tasks" data-tab="tasks" hidden>
|
|
111
|
+
<div class="section-label" data-zh="本机授权与签名" data-en="This device: approval & signing">本机授权与签名</div>
|
|
112
|
+
<div class="list">
|
|
113
|
+
<div class="list-item" id="tasks-signing-item"><span class="list-icon"><svg class="ico" viewBox="0 0 24 24"><rect x="4.5" y="10.5" width="15" height="9.5" rx="2.5"/><path d="M8 10.5V7.5a4 4 0 0 1 8 0v3"/><path d="M12 14v3"/></svg></span><span id="tasks-signing" style="flex:1">检查中…</span></div>
|
|
114
|
+
</div>
|
|
115
|
+
<div class="sheet-text" id="tasks-hint" style="font-size:12px;color:var(--text-secondary);padding:0 14px 6px"></div>
|
|
116
|
+
<div class="list" style="padding:0 14px 4px">
|
|
117
|
+
<button class="sheet-choice" id="tasks-refresh" data-zh="刷新任务数据" data-en="Refresh task data" style="width:100%">刷新任务数据</button>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<div class="section-label" data-zh="群 (与 CLI task group 同一份记录)" data-en="Groups (same records as CLI task group)">群 (与 CLI task group 同一份记录)</div>
|
|
121
|
+
<div class="list" id="tasks-groups"><div class="sheet-text" style="padding:0 14px">加载中…</div></div>
|
|
122
|
+
<div style="padding:0 14px 8px">
|
|
123
|
+
<input id="tasks-group-link" placeholder="群邀请链接 (orbitdb://…?type=group&name=…)" style="width:100%;padding:10px 12px;border-radius:10px;border:1px solid var(--border);background:transparent;color:var(--text);font-size:14px" />
|
|
124
|
+
<button class="sheet-choice" id="tasks-group-join" data-zh="入群(需手机确认 + 签名)" data-en="Join group (needs on-device confirm + signature)" style="width:100%;margin-top:8px">入群(需手机确认 + 签名)</button>
|
|
125
|
+
<input id="tasks-group-name" placeholder="新群名称" style="width:100%;margin-top:8px;padding:10px 12px;border-radius:10px;border:1px solid var(--border);background:transparent;color:var(--text);font-size:14px" />
|
|
126
|
+
<button class="sheet-choice" id="tasks-group-create" data-zh="建群(需手机确认 + 签名)" data-en="Create group (needs on-device confirm + signature)" style="width:100%;margin-top:8px">建群(需手机确认 + 签名)</button>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<div class="section-label" data-zh="公告板 (与 CLI task board 同一份事实)" data-en="Task board (same facts as CLI task board)">公告板 (与 CLI task board 同一份事实)</div>
|
|
130
|
+
<div class="list" id="tasks-board"><div class="sheet-text" style="padding:0 14px">加载中…</div></div>
|
|
131
|
+
<div style="padding:0 14px 8px">
|
|
132
|
+
<input id="tasks-pub-capability" placeholder="能力 (例如 research)" style="width:100%;padding:10px 12px;border-radius:10px;border:1px solid var(--border);background:transparent;color:var(--text);font-size:14px" />
|
|
133
|
+
<input id="tasks-pub-instruction" placeholder="任务正文 (一句能执行的委托)" style="width:100%;margin-top:8px;padding:10px 12px;border-radius:10px;border:1px solid var(--border);background:transparent;color:var(--text);font-size:14px" />
|
|
134
|
+
<input id="tasks-pub-budget" placeholder="预算 (例如 0.05)" inputmode="decimal" style="width:100%;margin-top:8px;padding:10px 12px;border-radius:10px;border:1px solid var(--border);background:transparent;color:var(--text);font-size:14px" />
|
|
135
|
+
<input id="tasks-pub-deadline" placeholder="截止 (+24h / +2h / +1d, 留空=桌面默认 24h)" style="width:100%;margin-top:8px;padding:10px 12px;border-radius:10px;border:1px solid var(--border);background:transparent;color:var(--text);font-size:14px" />
|
|
136
|
+
<button class="sheet-choice" id="tasks-pub-submit" data-zh="发布公告(需手机确认 + 签名)" data-en="Publish announcement (needs on-device confirm + signature)" style="width:100%;margin-top:8px">发布公告(需手机确认 + 签名)</button>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<div class="section-label" data-zh="群过程痕迹 (只读, 脱敏)" data-en="Group trail (read-only, redacted)">群过程痕迹 (只读, 脱敏)</div>
|
|
140
|
+
<div class="list" id="tasks-trail-group-picker"></div>
|
|
141
|
+
<div class="list" id="tasks-trail"><div class="sheet-text" style="padding:0 14px">选一个群看这一期的痕迹</div></div>
|
|
142
|
+
<div style="padding:0 14px 8px">
|
|
143
|
+
<button class="sheet-choice" id="tasks-trail-refresh" data-zh="读群痕迹" data-en="Read group trail" style="width:100%">读群痕迹</button>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<div class="section-label" data-zh="飞轮进度 (只读, 与桌面同一状态)" data-en="Goal flywheel progress (read-only, same state as desktop)">飞轮进度 (只读, 与桌面同一状态)</div>
|
|
147
|
+
<div class="list" id="tasks-flywheel"><div class="sheet-text" style="padding:0 14px">加载中…</div></div>
|
|
148
|
+
</section>
|
|
149
|
+
|
|
107
150
|
<!-- 我 tab -->
|
|
108
151
|
<section class="page" id="page-me" data-tab="me" hidden>
|
|
109
152
|
<div class="profile-card" id="profile-card">
|
|
@@ -133,6 +176,7 @@
|
|
|
133
176
|
<button class="tab active" data-tab="main"><span class="tab-icon"><svg class="ico" viewBox="0 0 24 24"><rect x="3.5" y="3.5" width="7" height="7" rx="1.6"/><rect x="13.5" y="3.5" width="7" height="7" rx="1.6"/><rect x="3.5" y="13.5" width="7" height="7" rx="1.6"/><rect x="13.5" y="13.5" width="7" height="7" rx="1.6"/></svg></span><span>首页</span></button>
|
|
134
177
|
<button class="tab" data-tab="friends"><span class="tab-icon"><svg class="ico" viewBox="0 0 24 24"><circle cx="9" cy="8" r="3.2"/><path d="M3.5 19c.8-3 3-4.6 5.5-4.6S13.7 16 14.5 19"/><path d="M17 8v6M14 11h6"/></svg></span><span>好友</span></button>
|
|
135
178
|
<button class="tab" data-tab="network"><span class="tab-icon"><svg class="ico" viewBox="0 0 24 24"><circle cx="12" cy="12" r="8.5"/><path d="M3.5 12h17"/><path d="M12 3.5c2.3 2.3 3.5 5.3 3.5 8.5s-1.2 6.2-3.5 8.5c-2.3-2.3-3.5-5.3-3.5-8.5S9.7 5.8 12 3.5z"/></svg></span><span>网络</span></button>
|
|
179
|
+
<button class="tab" data-tab="tasks"><span class="tab-icon"><svg class="ico" viewBox="0 0 24 24"><path d="M4 7.5h10M4 12h7M4 16.5h12"/><path d="M18 5.5l1.6 1.6L22 4.5"/><path d="M18 14.5l1.6 1.6L22 13.5"/></svg></span><span>任务</span></button>
|
|
136
180
|
<button class="tab" data-tab="me"><span class="tab-icon"><svg class="ico" viewBox="0 0 24 24"><circle cx="12" cy="8.5" r="4"/><path d="M4.5 20.5a7.5 7.5 0 0 1 15 0"/></svg></span><span>我</span></button>
|
|
137
181
|
</nav>
|
|
138
182
|
|
|
@@ -267,3 +311,23 @@ if ('serviceWorker' in navigator) {
|
|
|
267
311
|
</div>
|
|
268
312
|
</div>
|
|
269
313
|
</div>
|
|
314
|
+
|
|
315
|
+
<!-- 任务动作确认页 (2026-09-25): 展示待发内容 → 手机确认 → 设备签名 → 桌面验签执行
|
|
316
|
+
高风险动作不许绕过这里: 入群 / 建群 / 发公告 / 发进群 / 过程留痕 都走这一个入口。 -->
|
|
317
|
+
<div class="sheet" id="task-confirm-sheet" hidden>
|
|
318
|
+
<div class="sheet-inner">
|
|
319
|
+
<div class="sheet-title" id="task-confirm-title">确认这个动作</div>
|
|
320
|
+
<div class="sheet-text" id="task-confirm-lines" style="font-size:12px"></div>
|
|
321
|
+
<div class="sheet-text" id="task-confirm-what" style="font-size:12px;color:var(--text-secondary)"></div>
|
|
322
|
+
<div id="task-confirm-group-wrap" hidden style="margin-top:8px">
|
|
323
|
+
<div class="sheet-text" data-zh="发到哪个群" data-en="Which group" style="font-size:12px"></div>
|
|
324
|
+
<div class="list" id="task-confirm-group"></div>
|
|
325
|
+
</div>
|
|
326
|
+
<div id="task-confirm-fields" style="margin-top:8px"></div>
|
|
327
|
+
<button class="sheet-choice" id="task-confirm-preview" hidden data-zh="预览要发进群的那一行" data-en="Preview the exact line to be posted">预览要发进群的那一行</button>
|
|
328
|
+
<div class="sheet-title" id="task-confirm-content-title" style="margin-top:10px" hidden data-zh="要发进群的内容 (原文)" data-en="Content to be posted (verbatim)">要发进群的内容 (原文)</div>
|
|
329
|
+
<div class="sheet-text" id="task-confirm-content" style="font-size:12px;white-space:pre-wrap;word-break:break-word" hidden></div>
|
|
330
|
+
<button class="sheet-choice" id="task-confirm-yes">确认并签名</button>
|
|
331
|
+
<button class="sheet-choice sheet-cancel" id="task-confirm-no">取消</button>
|
|
332
|
+
</div>
|
|
333
|
+
</div>
|