@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,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mobile-privacy.ts — 手机端隐私合规: 首启同意门 + 应用内政策摘要 + 本机数据注销
|
|
3
|
+
*
|
|
4
|
+
* 为什么单独成模块: 这三件事都必须能被单测锁住 —— 摘要少一个必填要素、同意版本号算错、
|
|
5
|
+
* 注销漏删一个库, 都是"上架被拒/合规缺口"级别的问题, 不能只靠肉眼看 UI。
|
|
6
|
+
*
|
|
7
|
+
* 上架要求对照 (华为/工信部口径):
|
|
8
|
+
* ① 首次启动必须先展示隐私政策, 用户同意前不得收集任何信息/申请任何权限 → needsPrivacyConsent + 门
|
|
9
|
+
* ② 隐私政策须含: 收集类型 · 用途 · 存储期限 · 第三方 SDK 清单 · 账号注销路径 → PRIVACY_SUMMARY
|
|
10
|
+
* ③ 必须提供注销入口且处理时限 ≤ 7 个工作日 → WIPE_TARGETS + wipeLocalData (本地即时)
|
|
11
|
+
*/
|
|
12
|
+
/** localStorage 键: 记录用户已同意的政策版本 */
|
|
13
|
+
export const PRIVACY_CONSENT_KEY = 'bolloon_privacy_consent';
|
|
14
|
+
/** 政策版本: 摘要或权威页内容发生实质变化时必须 +1 → 会重新征求同意 */
|
|
15
|
+
export const PRIVACY_CONSENT_VERSION = '1';
|
|
16
|
+
/** 权威政策页 (应用市场表单要填的链接; 应用内摘要以它为准) */
|
|
17
|
+
export const PRIVACY_POLICY_URL = 'https://bolloon.cn/privacy.html';
|
|
18
|
+
/** APP 备案号: 备案完成后填入 (形如 浙ICP备XXXXXXXX号-XA), 留空时 UI 显示"备案办理中" */
|
|
19
|
+
export const APP_FILING_NO = '';
|
|
20
|
+
/** 主体联系邮箱 (政策里承诺 7 个工作日答复) */
|
|
21
|
+
export const PRIVACY_CONTACT = 'yuanjieliu65@gmail.com';
|
|
22
|
+
/** 注销时限承诺 (工作日) */
|
|
23
|
+
export const WIPE_SLA_WORKDAYS = 7;
|
|
24
|
+
/**
|
|
25
|
+
* 应用内政策摘要 —— 必须覆盖上架要求的必填要素。
|
|
26
|
+
* 单测逐个要素断言 (mobile-privacy.test.ts), 删要素即红。
|
|
27
|
+
*/
|
|
28
|
+
export const PRIVACY_SUMMARY = [
|
|
29
|
+
{
|
|
30
|
+
title: '一、我们收集什么',
|
|
31
|
+
body: '本机生成的网络身份标识(DID)与密钥、你创建的智能体、会话与消息内容、好友与已知节点列表。' +
|
|
32
|
+
'这些数据默认只保存在你自己的设备上(IndexedDB),我们不设账号体系,不收集手机号、邮箱、通讯录、' +
|
|
33
|
+
'短信、通话记录、精确定位、设备唯一标识(IMEI/MAC/广告标识),也不做使用行为统计。',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
title: '二、用途',
|
|
37
|
+
body: '上述数据仅用于提供你要求的功能:P2P 连接与身份验证、智能体会话、本地存储与展示。' +
|
|
38
|
+
'我们不用于广告、画像或向第三方出售。',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
title: '三、存储位置与期限',
|
|
42
|
+
body: '全部保存在本机。卸载应用或在「设置 → 清除本机数据(注销)」即删除;我们服务器上没有你的副本,' +
|
|
43
|
+
'因此不存在"提交删除申请后还要等待"的环节。',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
title: '四、第三方 SDK 与服务',
|
|
47
|
+
body: '你自选的大模型服务商(API Key 由你填写,你的设备直连该服务商,我们不经手);' +
|
|
48
|
+
'网站与安装包托管 Cloudflare / GitHub(境外)、版本号查询 npm registry、网页字体 Google Fonts。' +
|
|
49
|
+
'本应用不集成任何广告、行为统计或崩溃上报 SDK。若你启用微支付,链上交易记录公开且不可删除。',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
title: '五、系统权限',
|
|
53
|
+
body: '蓝牙扫描与连接:发现并连接附近设备与好友(已声明 neverForLocation,不用于推断位置)。' +
|
|
54
|
+
'位置权限:仅 Android 11 及以下系统要求蓝牙扫描需要它(已限制 maxSdkVersion=30,Android 12+ 不再申请)。' +
|
|
55
|
+
'网络:P2P 连接与下载。相机:调用系统相机应用完成拍摄,本应用不申请相机权限。' +
|
|
56
|
+
'完全访问权限(让智能体在这台手机上点按、滑动屏幕或调用系统能力)与 Shizuku:仅官网直装版本包含,默认开启' +
|
|
57
|
+
'(首次使用会直接带你去系统开关页确认一次,之后一直有效)、随时可以关闭;' +
|
|
58
|
+
'应用商店下载的版本不含这两项,也没有这个能力。',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
title: '六、你的权利与注销',
|
|
62
|
+
body: '你可以随时在本机查看、修改、删除全部数据。注销入口:设置 → 清除本机数据(注销),' +
|
|
63
|
+
'即时删除本机数据(异常情况最长 ' + WIPE_SLA_WORKDAYS + ' 个工作日)。' +
|
|
64
|
+
'已经写入公开区块链的交易记录任何服务方都无法删除。',
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
title: '七、联系我们',
|
|
68
|
+
body: '疑问或投诉请发邮件至 ' + PRIVACY_CONTACT + ',我们会在 ' + WIPE_SLA_WORKDAYS + ' 个工作日内答复。',
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
/**
|
|
72
|
+
* 是否需要弹同意门。
|
|
73
|
+
* @param stored 本机已记录的同意值 (localStorage.getItem(PRIVACY_CONSENT_KEY))
|
|
74
|
+
* @returns true = 需要征求同意 (未同意 / 版本过期 / 值非法)
|
|
75
|
+
*/
|
|
76
|
+
export function needsPrivacyConsent(stored) {
|
|
77
|
+
if (typeof stored !== 'string')
|
|
78
|
+
return true;
|
|
79
|
+
return stored.trim() !== PRIVACY_CONSENT_VERSION;
|
|
80
|
+
}
|
|
81
|
+
/** 同意后写入的值 */
|
|
82
|
+
export function consentRecord() {
|
|
83
|
+
return PRIVACY_CONSENT_VERSION;
|
|
84
|
+
}
|
|
85
|
+
/** 备案号展示文案 (未备案时如实说明, 不伪造编号) */
|
|
86
|
+
export function filingDisplay(no = APP_FILING_NO) {
|
|
87
|
+
const v = (no || '').trim();
|
|
88
|
+
return v ? `APP 备案号 ${v}` : 'APP 备案号:备案办理中';
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* 注销要清掉的东西 —— 清单必须与"我们声明收集的数据"一一对应, 少一个就是合规缺口。
|
|
92
|
+
* 注意: 不删 consent 记录 (同意记录不是个人信息, 且删了会在注销后立刻再弹一次门)。
|
|
93
|
+
*/
|
|
94
|
+
export const WIPE_TARGETS = {
|
|
95
|
+
/** IndexedDB 库 (名称与各模块里的常量逐一核对过) */
|
|
96
|
+
databases: ['bolloon-mobile-data', 'bolloon-mobile', 'bolloon-mobile-payments', 'bolloon'],
|
|
97
|
+
/** localStorage 键前缀: 命中即清 */
|
|
98
|
+
localStoragePrefixes: ['bolloon_', 'bolloon.', 'bolloon-'],
|
|
99
|
+
/** 明确保留: 同意记录 + 界面偏好 (不含个人信息) */
|
|
100
|
+
keep: [PRIVACY_CONSENT_KEY, 'bolloon-lang', 'bolloon_theme'],
|
|
101
|
+
};
|
|
102
|
+
/** 注销后给用户看的说明 (诚实: 链上记录删不掉) */
|
|
103
|
+
export const WIPE_NOTICE = '已删除本机的身份标识(DID)、智能体、会话消息与钱包账本。' +
|
|
104
|
+
'已经写入公开区块链的交易记录无法删除,这一点任何服务方都做不到。' +
|
|
105
|
+
`如遇异常,我们的处理时限为 ${WIPE_SLA_WORKDAYS} 个工作日(${PRIVACY_CONTACT})。`;
|
|
106
|
+
/** 同意门文案 (集中在这里, 便于审计与翻译) */
|
|
107
|
+
export const CONSENT_TEXT = {
|
|
108
|
+
title: '隐私政策与权限说明',
|
|
109
|
+
intro: 'Bolloon 是本地优先的:没有账号体系,不收集手机号或邮箱,你的智能体、会话与消息只存在这台设备上。' +
|
|
110
|
+
'在你点击「同意并继续」之前,应用不会读取本机数据、不会连接网络、也不会申请任何系统权限。',
|
|
111
|
+
agree: '同意并继续',
|
|
112
|
+
decline: '不同意',
|
|
113
|
+
policyLink: '阅读完整隐私政策',
|
|
114
|
+
declinedTitle: '未同意,已停在说明页',
|
|
115
|
+
declinedBody: '你可以继续留在本页查阅说明,或退出应用。我们不会在此状态下收集任何信息。' +
|
|
116
|
+
'随时重新打开应用都可以再次选择。',
|
|
117
|
+
declinedReread: '重新阅读',
|
|
118
|
+
declinedExit: '退出应用',
|
|
119
|
+
};
|
|
120
|
+
/** 删除单个 IndexedDB 库 (供没有 reset 导出的模块使用, 如钱包库) */
|
|
121
|
+
function deleteDatabase(name) {
|
|
122
|
+
return new Promise((resolve, reject) => {
|
|
123
|
+
if (typeof indexedDB === 'undefined') {
|
|
124
|
+
resolve();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const req = indexedDB.deleteDatabase(name);
|
|
128
|
+
req.onsuccess = () => resolve();
|
|
129
|
+
req.onerror = () => reject(req.error);
|
|
130
|
+
// 还有别的连接开着时会 blocked: 不等死, 如实返回 (下次启动会清掉)
|
|
131
|
+
req.onblocked = () => resolve();
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* 注销 / 清除本机数据 —— 应用内唯一的删除入口。
|
|
136
|
+
* 覆盖: 身份 DID+密钥库 · 智能体与会话消息库 · 支付库 · 钱包库 · localStorage 里的本机键。
|
|
137
|
+
* 不覆盖: 公开区块链上的历史交易 (任何服务方都删不了, 文案里已向用户说明)。
|
|
138
|
+
*/
|
|
139
|
+
export async function wipeLocalData() {
|
|
140
|
+
const result = { deletedDatabases: [], clearedStorageKeys: [], failed: [] };
|
|
141
|
+
// ① 三个自带 reset 的库: 复用模块自己的实现 (它们会先关闭连接, 避免 onblocked)
|
|
142
|
+
const resets = [
|
|
143
|
+
['bolloon-mobile-data', async () => (await import('./mobile-data.js')).resetDataDb()],
|
|
144
|
+
['bolloon-mobile', async () => (await import('./mobile-agent.js')).resetAgentDb()],
|
|
145
|
+
['bolloon-mobile-payments', async () => (await import('./mobile-payments.js')).resetPaymentsDb()],
|
|
146
|
+
];
|
|
147
|
+
for (const [name, fn] of resets) {
|
|
148
|
+
try {
|
|
149
|
+
await fn();
|
|
150
|
+
result.deletedDatabases.push(name);
|
|
151
|
+
}
|
|
152
|
+
catch (e) {
|
|
153
|
+
result.failed.push(`${name}: ${e?.message || String(e)}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// ② 钱包库 (mobile-wallet.ts) 没有 reset 导出 → 直接删库
|
|
157
|
+
try {
|
|
158
|
+
await deleteDatabase('bolloon');
|
|
159
|
+
result.deletedDatabases.push('bolloon');
|
|
160
|
+
}
|
|
161
|
+
catch (e) {
|
|
162
|
+
result.failed.push(`bolloon: ${e?.message || String(e)}`);
|
|
163
|
+
}
|
|
164
|
+
// ③ localStorage: 按前缀清, 保留同意记录与界面偏好
|
|
165
|
+
try {
|
|
166
|
+
const keys = [];
|
|
167
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
168
|
+
const k = localStorage.key(i);
|
|
169
|
+
if (k)
|
|
170
|
+
keys.push(k);
|
|
171
|
+
}
|
|
172
|
+
const keep = WIPE_TARGETS.keep;
|
|
173
|
+
for (const k of keys) {
|
|
174
|
+
const hit = WIPE_TARGETS.localStoragePrefixes.some((p) => k.startsWith(p));
|
|
175
|
+
if (!hit || keep.includes(k))
|
|
176
|
+
continue;
|
|
177
|
+
localStorage.removeItem(k);
|
|
178
|
+
result.clearedStorageKeys.push(k);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch (e) {
|
|
182
|
+
result.failed.push(`localStorage: ${e?.message || String(e)}`);
|
|
183
|
+
}
|
|
184
|
+
return result;
|
|
185
|
+
}
|
|
@@ -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 };
|