@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.
Files changed (143) hide show
  1. package/dist/agents/contacts/chain.js +27 -7
  2. package/dist/agents/execution-supervisor.js +493 -35
  3. package/dist/agents/external-events.js +35 -6
  4. package/dist/agents/gateway-group.js +100 -12
  5. package/dist/agents/goal-criteria.js +10 -3
  6. package/dist/agents/goal-flywheel/block-executor.js +346 -0
  7. package/dist/agents/goal-flywheel/continuation-decision.js +592 -0
  8. package/dist/agents/goal-flywheel/goal-change.js +754 -0
  9. package/dist/agents/goal-flywheel/index.js +26 -0
  10. package/dist/agents/goal-flywheel/memory-layers.js +227 -0
  11. package/dist/agents/goal-flywheel/run-closure.js +809 -0
  12. package/dist/agents/goal-flywheel/skill-candidate.js +628 -0
  13. package/dist/agents/goal-flywheel/types.js +329 -0
  14. package/dist/agents/goal-flywheel/wiring/change.js +288 -0
  15. package/dist/agents/goal-flywheel/wiring/closure.js +697 -0
  16. package/dist/agents/goal-flywheel/wiring/continuation.js +432 -0
  17. package/dist/agents/goal-flywheel/wiring/contract.js +270 -0
  18. package/dist/agents/goal-flywheel/wiring/index.js +26 -0
  19. package/dist/agents/goal-flywheel/wiring/monitor.js +254 -0
  20. package/dist/agents/goal-flywheel/wiring/seams.js +592 -0
  21. package/dist/agents/goal-flywheel/work-contract.js +478 -0
  22. package/dist/agents/goal-flywheel/work-monitor.js +488 -0
  23. package/dist/agents/goal-flywheel-wiring.js +1870 -0
  24. package/dist/agents/goal-state-reducer.js +406 -0
  25. package/dist/agents/goal-store.js +35 -1
  26. package/dist/agents/mobile-flywheel-view.js +144 -0
  27. package/dist/agents/mobile-task-actions-verify.js +74 -0
  28. package/dist/agents/mobile-task-actions.js +178 -0
  29. package/dist/agents/mobile-task-views.js +199 -0
  30. package/dist/agents/network-pulse.js +304 -18
  31. package/dist/agents/pi-sdk.js +75 -20
  32. package/dist/agents/run-store.js +18 -5
  33. package/dist/agents/skill-readiness.js +52 -10
  34. package/dist/agents/subagent-manager.js +209 -3
  35. package/dist/agents/task/task-runner.js +9 -3
  36. package/dist/agents/task-group.js +8 -51
  37. package/dist/agents/task-public-text.js +89 -0
  38. package/dist/cli/commands/index.js +1 -0
  39. package/dist/cli/commands/tasks.js +350 -2
  40. package/dist/cli/identity-command.js +177 -0
  41. package/dist/cli/setup-wizard.js +71 -0
  42. package/dist/cli/update-commands.js +150 -24
  43. package/dist/cli-entry.js +12 -2
  44. package/dist/electron-build/agents/contacts/chain.js +26 -6
  45. package/dist/electron-build/agents/contacts/chain.js.map +1 -1
  46. package/dist/electron-build/agents/execution-supervisor.js +492 -34
  47. package/dist/electron-build/agents/execution-supervisor.js.map +1 -1
  48. package/dist/electron-build/agents/external-events.js +34 -5
  49. package/dist/electron-build/agents/external-events.js.map +1 -1
  50. package/dist/electron-build/agents/goal-criteria.js +10 -3
  51. package/dist/electron-build/agents/goal-criteria.js.map +1 -1
  52. package/dist/electron-build/agents/goal-flywheel/continuation-decision.js +598 -0
  53. package/dist/electron-build/agents/goal-flywheel/continuation-decision.js.map +1 -0
  54. package/dist/electron-build/agents/goal-flywheel/goal-change.js +773 -0
  55. package/dist/electron-build/agents/goal-flywheel/goal-change.js.map +1 -0
  56. package/dist/electron-build/agents/goal-flywheel/memory-layers.js +269 -0
  57. package/dist/electron-build/agents/goal-flywheel/memory-layers.js.map +1 -0
  58. package/dist/electron-build/agents/goal-flywheel/run-closure.js +818 -0
  59. package/dist/electron-build/agents/goal-flywheel/run-closure.js.map +1 -0
  60. package/dist/electron-build/agents/goal-flywheel/skill-candidate.js +642 -0
  61. package/dist/electron-build/agents/goal-flywheel/skill-candidate.js.map +1 -0
  62. package/dist/electron-build/agents/goal-flywheel/types.js +333 -0
  63. package/dist/electron-build/agents/goal-flywheel/types.js.map +1 -0
  64. package/dist/electron-build/agents/goal-flywheel/wiring/change.js +294 -0
  65. package/dist/electron-build/agents/goal-flywheel/wiring/change.js.map +1 -0
  66. package/dist/electron-build/agents/goal-flywheel/wiring/closure.js +742 -0
  67. package/dist/electron-build/agents/goal-flywheel/wiring/closure.js.map +1 -0
  68. package/dist/electron-build/agents/goal-flywheel/wiring/continuation.js +444 -0
  69. package/dist/electron-build/agents/goal-flywheel/wiring/continuation.js.map +1 -0
  70. package/dist/electron-build/agents/goal-flywheel/wiring/contract.js +279 -0
  71. package/dist/electron-build/agents/goal-flywheel/wiring/contract.js.map +1 -0
  72. package/dist/electron-build/agents/goal-flywheel/wiring/index.js +111 -0
  73. package/dist/electron-build/agents/goal-flywheel/wiring/index.js.map +1 -0
  74. package/dist/electron-build/agents/goal-flywheel/wiring/monitor.js +264 -0
  75. package/dist/electron-build/agents/goal-flywheel/wiring/monitor.js.map +1 -0
  76. package/dist/electron-build/agents/goal-flywheel/wiring/seams.js +612 -0
  77. package/dist/electron-build/agents/goal-flywheel/wiring/seams.js.map +1 -0
  78. package/dist/electron-build/agents/goal-flywheel/work-contract.js +488 -0
  79. package/dist/electron-build/agents/goal-flywheel/work-contract.js.map +1 -0
  80. package/dist/electron-build/agents/goal-flywheel/work-monitor.js +501 -0
  81. package/dist/electron-build/agents/goal-flywheel/work-monitor.js.map +1 -0
  82. package/dist/electron-build/agents/goal-flywheel-wiring.js +1962 -0
  83. package/dist/electron-build/agents/goal-flywheel-wiring.js.map +1 -0
  84. package/dist/electron-build/agents/goal-state-reducer.js +414 -0
  85. package/dist/electron-build/agents/goal-state-reducer.js.map +1 -0
  86. package/dist/electron-build/agents/goal-store.js +35 -1
  87. package/dist/electron-build/agents/goal-store.js.map +1 -1
  88. package/dist/electron-build/agents/network-pulse.js +307 -19
  89. package/dist/electron-build/agents/network-pulse.js.map +1 -1
  90. package/dist/electron-build/agents/pi-sdk.js +74 -19
  91. package/dist/electron-build/agents/pi-sdk.js.map +1 -1
  92. package/dist/electron-build/agents/run-store.js +20 -5
  93. package/dist/electron-build/agents/run-store.js.map +1 -1
  94. package/dist/electron-build/agents/skill-readiness.js +52 -9
  95. package/dist/electron-build/agents/skill-readiness.js.map +1 -1
  96. package/dist/electron-build/agents/task/task-runner.js +8 -2
  97. package/dist/electron-build/agents/task/task-runner.js.map +1 -1
  98. package/dist/electron-build/orbitdb/cid-database.js +83 -17
  99. package/dist/electron-build/orbitdb/cid-database.js.map +1 -1
  100. package/dist/electron-build/orbitdb/ipfs-node.js +31 -2
  101. package/dist/electron-build/orbitdb/ipfs-node.js.map +1 -1
  102. package/dist/electron-build/utils/dual-source.js +471 -0
  103. package/dist/electron-build/utils/dual-source.js.map +1 -0
  104. package/dist/electron-build/utils/update-manager.js +331 -57
  105. package/dist/electron-build/utils/update-manager.js.map +1 -1
  106. package/dist/electron-build/utils/update-state.js +16 -1
  107. package/dist/electron-build/utils/update-state.js.map +1 -1
  108. package/dist/electron-build/utils/version-info.js +82 -2
  109. package/dist/electron-build/utils/version-info.js.map +1 -1
  110. package/dist/index.js +103 -12
  111. package/dist/ios/chain-explorer.js +617 -0
  112. package/dist/ios/explorer.html +163 -0
  113. package/dist/ios/index.html +96 -0
  114. package/dist/ios/mobile-agent.js +2 -2
  115. package/dist/ios/mobile-contacts.js +329 -0
  116. package/dist/ios/mobile-core.js +1379 -226
  117. package/dist/ios/mobile-privacy.js +185 -0
  118. package/dist/ios/mobile.css +19 -0
  119. package/dist/ios/mobile.html +96 -0
  120. package/dist/ios/mobile.js +706 -7
  121. package/dist/ios/routes-chain-index.js +108 -0
  122. package/dist/ios/routes-contacts.js +420 -0
  123. package/dist/ios/routes-onchain-trade.js +99 -0
  124. package/dist/ios/routes-x402-info.js +1 -0
  125. package/dist/ios/server.js +762 -0
  126. package/dist/ios/task-frame-server.js +211 -0
  127. package/dist/orbitdb/cid-database.js +81 -16
  128. package/dist/orbitdb/ipfs-node.js +31 -2
  129. package/dist/utils/dual-source.js +426 -0
  130. package/dist/utils/update-cli.js +16 -21
  131. package/dist/utils/update-health.js +35 -5
  132. package/dist/utils/update-manager.js +329 -57
  133. package/dist/utils/update-state.js +16 -1
  134. package/dist/utils/version-info.js +75 -1
  135. package/dist/web/mobile-contacts.js +44 -11
  136. package/dist/web/mobile-core.js +1197 -513
  137. package/dist/web/mobile-tasks.js +339 -0
  138. package/dist/web/mobile.css +19 -0
  139. package/dist/web/mobile.html +64 -0
  140. package/dist/web/mobile.js +531 -1
  141. package/dist/web/routes-mobile-tasks.js +353 -0
  142. package/dist/web/server.js +163 -4
  143. package/package.json +3 -1
@@ -47,6 +47,58 @@ export function resolveUpdateChannel(explicit) {
47
47
  export function distTagForChannel(channel) {
48
48
  return channel === 'beta' ? 'beta' : 'latest';
49
49
  }
50
+ // ── 双源 (update-protocol §12, 2026-09-25) ──────────────────────────────────
51
+ //
52
+ // stable: npm registry (`dist-tags.latest`) 是**权威**, GitHub Release/Tag 是**交叉校验源**;
53
+ // dev : GitHub `master` HEAD 是**唯一源**, 版本身份 = `<package.json 版本>+dev.<commit sha 前 7>`。
54
+ //
55
+ // 两条硬口径 (从这里开始, 全局只有这一份):
56
+ // ① 两套版本比较语义**显式分开** —— stable 比 semver, dev 比 commit sha (`ChannelKind`)
57
+ // ② 源不可达 / 版本不存在 → **拒绝并说清**, 不许静默装回旧版 (§12.5)
58
+ /** GitHub 上游 (owner/repo) —— 与 UPSTREAM_REPO 同一处, 这里给 API 用。 */
59
+ export const GITHUB_UPSTREAM_SLUG = 'logos-42/bolloon';
60
+ /** GitHub API 根 (可被 BOLLOON_GITHUB_API 覆盖 → 受控假 API / 企业实例)。 */
61
+ export const GITHUB_API_BASE = (process.env.BOLLOON_GITHUB_API || 'https://api.github.com').replace(/\/+$/, '');
62
+ /** dev 通道盯的分支 (冻结: master)。 */
63
+ export const DEV_BRANCH = 'master';
64
+ export const DEV_REF = `refs/heads/${DEV_BRANCH}`;
65
+ /**
66
+ * 版本比较语义 (显式, 不是隐含约定):
67
+ * semver —— stable: 按 semver 数值段比大小
68
+ * git-ref —— dev: **不用 semver**, 只判"是否同一 commit / 是否落后"
69
+ */
70
+ export const CHANNEL_KINDS = ['semver', 'git-ref'];
71
+ export function channelKindOf(channel) {
72
+ return channel === 'dev' ? 'git-ref' : 'semver';
73
+ }
74
+ /** dev 快照身份里的后缀标记 (`0.4.33+dev.a1b2c3d`)。 */
75
+ export const DEV_IDENTITY_SEP = '+dev.';
76
+ /** dev 快照身份 —— 用 commit sha (不是 semver) 当身份, 版本号只作参考展示。 */
77
+ export function devIdentity(baseVersion, sha) {
78
+ return `${baseVersionOf(baseVersion)}${DEV_IDENTITY_SEP}${String(sha).slice(0, 7)}`;
79
+ }
80
+ /**
81
+ * dev 通道的**唯一一句警告** (§12.4 硬约束 1) —— 检查 / 计划 / 执行三处打印同一句话。
82
+ * 定义在这里 (唯一事实层), 由 dual-source / update-manager / CLI 复用, 不允许各处自己造一句。
83
+ */
84
+ export const DEV_CHANNEL_WARNING = '⚠️ dev 通道 = GitHub master HEAD 的即时快照 (未走发布门): 可能中断正在跑的 Goal/Run, 且不保证可回滚到上一个 dev 版。';
85
+ /** 一键回 stable 的提示语 (同一份措辞)。 */
86
+ export const DEV_BACK_TO_STABLE_HINT = '一键回稳定版: bolloon update now --channel stable';
87
+ /** 去掉 `+dev.<sha>` 后缀, 拿到基础版本号 (stable 目标的比对基准)。 */
88
+ export function baseVersionOf(v) {
89
+ return String(v || '').split(DEV_IDENTITY_SEP)[0].trim();
90
+ }
91
+ /** 这个版本号是不是一个 dev 快照身份。 */
92
+ export function isDevIdentity(v) {
93
+ return String(v || '').includes(DEV_IDENTITY_SEP);
94
+ }
95
+ /** 从 dev 身份里取 commit sha (前 7 位); 不是 dev 身份返回 null。 */
96
+ export function devShaFromIdentity(v) {
97
+ if (!isDevIdentity(v))
98
+ return null;
99
+ const sha = String(v).split(DEV_IDENTITY_SEP)[1]?.trim();
100
+ return sha ? sha : null;
101
+ }
50
102
  // ── 包根定位 ────────────────────────────────────────────────────────────────
51
103
  /**
52
104
  * 从当前模块 URL 定位**运行中的**包根。
@@ -385,6 +437,11 @@ export function collectVersionInfo(opts = {}) {
385
437
  }
386
438
  if (!gitCommit)
387
439
  gitCommitSource = 'unknown';
440
+ // dev 身份从**磁盘上的版本号**读 (不是从状态文件猜): `0.4.33+dev.a1b2c3d` → sha=a1b2c3d
441
+ const installedDevSha = devShaFromIdentity(pkg?.version) || (opts.update?.installedDevSha ?? null);
442
+ const switchableTo = opts.update?.switchableTo ?? (installedDevSha
443
+ ? { channel: 'stable', source: 'npm', target: null }
444
+ : null);
388
445
  return {
389
446
  schema: 'bolloon-version/1',
390
447
  packageName: pkg?.name || PKG_NAME,
@@ -406,11 +463,14 @@ export function collectVersionInfo(opts = {}) {
406
463
  binPath: install.binPath,
407
464
  configDir: home,
408
465
  channel,
466
+ channelKind: channelKindOf(channel),
409
467
  upstream: UPSTREAM_REPO,
410
468
  registry: `${NPM_REGISTRY_BASE}/${PKG_NAME}`,
411
469
  updateSource: install.updateSource,
412
470
  autoUpdatable: install.autoUpdatable,
413
471
  installReason: install.reason,
472
+ installedDevSha,
473
+ switchableTo,
414
474
  update: opts.update ?? null,
415
475
  runtime: opts.runtime ?? null,
416
476
  runtimeConfigBacked: opts.runtimeConfigBacked ?? false,
@@ -444,9 +504,18 @@ export function describeUpdateLine(info) {
444
504
  case 'registry_unavailable': return `更新检查: registry 不可用${u.lastCheckReason ? ` (${u.lastCheckReason})` : ''} (不等于最新)`;
445
505
  case 'local_version_unknown': return '更新检查: 读不到本地版本 — 不判断是否有更新';
446
506
  case 'unsupported_installation': return '更新检查: 当前安装方式不支持自动更新';
507
+ case 'github_unavailable': return `更新检查: GitHub 源不可用${u.lastCheckReason ? ` (${u.lastCheckReason})` : ''} (不等于最新)`;
508
+ case 'cross_check_mismatch': return `更新检查: 两个源不一致 (npm ↔ GitHub) — 不执行更新`;
447
509
  default: return `更新检查: ${u.lastCheckStatus}`;
448
510
  }
449
511
  }
512
+ /** 双源身份那一行 (谁装的 / 哪个 sha / 能切回哪) —— `--version` 与 `update status` 共用同一份措辞。 */
513
+ export function describeSourceIdentity(info) {
514
+ const src = info.installedDevSha ? 'dev (GitHub master 快照)' : 'stable (npm registry)';
515
+ const ident = info.installedDevSha ? `commit ${info.installedDevSha}` : baseVersionOf(info.packageVersion);
516
+ const back = info.switchableTo ? ` · 可切回 ${info.switchableTo.channel} (${info.switchableTo.source})` : '';
517
+ return `当前安装源: ${src} · 版本/commit: ${ident} · 比较语义: ${info.channelKind}${back}`;
518
+ }
450
519
  export function renderVersionText(info, opts = {}) {
451
520
  if (opts.verbose) {
452
521
  const lines = [
@@ -464,10 +533,12 @@ export function renderVersionText(info, opts = {}) {
464
533
  `运行入口: ${info.entryPath}`,
465
534
  `bin shim: ${info.binPath || 'unknown'}`,
466
535
  `配置目录: ${info.configDir}`,
467
- `更新通道: ${info.channel}`,
536
+ `更新通道: ${info.channel} (比较语义: ${info.channelKind})`,
468
537
  `更新来源: ${info.updateSource}${info.autoUpdatable ? '' : ' (不支持自动更新)'}`,
538
+ describeSourceIdentity(info),
469
539
  `上游地址: ${info.upstream}`,
470
540
  `registry: ${info.registry}`,
541
+ `GitHub: ${GITHUB_UPSTREAM_SLUG} (${DEV_BRANCH} HEAD = dev 通道的源)`,
471
542
  describeUpdateLine(info),
472
543
  `上次检查: ${fmtTime(info.update?.lastCheckAt)}`,
473
544
  ];
@@ -493,12 +564,15 @@ export function renderVersionText(info, opts = {}) {
493
564
  `安装目录: ${info.installDir}`,
494
565
  `运行入口: ${info.binPath || info.entryPath}`,
495
566
  `更新通道: ${info.channel}`,
567
+ describeSourceIdentity(info),
496
568
  `Node.js: ${info.nodeVersion}`,
497
569
  `平台: ${info.platform} ${info.arch}`,
498
570
  `上游提交: ${info.gitCommit || 'unknown'}`,
499
571
  describeUpdateLine(info),
500
572
  `上次检查: ${fmtTime(info.update?.lastCheckAt)}`,
501
573
  ];
574
+ if (info.installedDevSha)
575
+ lines.push(DEV_CHANNEL_WARNING, DEV_BACK_TO_STABLE_HINT);
502
576
  if (info.runtime) {
503
577
  lines.push('');
504
578
  lines.push(...renderRuntimeLines(info.runtime, { verbose: false, configBacked: info.runtimeConfigBacked }));
@@ -89,21 +89,54 @@ async function importPrivate(storage, c) {
89
89
  }
90
90
  /** 用设备私钥签一条 Grant (核心: 手机确认 → 桌面才认) */
91
91
  export async function signGrantOnDevice(grant, storage, c) {
92
- const cobj = cryptoOf(c);
93
- const key = await importPrivate(storage, cobj);
94
- const payload = canonicalGrantPayload(grant);
95
- const sig = await cobj.subtle.sign({ name: 'Ed25519' }, key, bytesOf(payload));
96
- const dev = await loadOrCreateDeviceKey(storage, cobj);
97
- return { deviceId: dev.deviceId, alg: 'ed25519', payloadHash: await sha256Hex(cobj, payload), sig: b64(sig) };
92
+ return signPayloadOnDevice(canonicalGrantPayload(grant), storage, c);
98
93
  }
99
94
  /** 撤销也要签名 (桌面拒收未登记设备的撤销) */
100
95
  export async function signRevocationOnDevice(rev, storage, c) {
96
+ return signPayloadOnDevice(canonicalRevocationPayload(rev), storage, c);
97
+ }
98
+ /**
99
+ * 通用: 用**同一把设备私钥**签任意规范载荷 (2026-09-25)。
100
+ *
101
+ * 抽出来的原因: 手机上所有"要桌面认账"的动作 (长期授权 · 撤销 · 高风险任务动作
102
+ * 入群/发公告/过程留痕) 必须是**同一把设备密钥 + 同一套验签纪律** ——
103
+ * 各写一份签名实现迟早出现"某条路径没签名也能过"的缝。
104
+ * 载荷的规范化由调用方提供 (grant-payload / mobile-task-actions), 本函数只管签。
105
+ */
106
+ export async function signPayloadOnDevice(payload, storage, c) {
107
+ const cobj = cryptoOf(c);
108
+ if (!cobj?.subtle)
109
+ throw new Error('device_signing_unavailable: 本机没有 WebCrypto');
110
+ // 只加载一次设备材料: deviceId 与签名必须来自**同一份**密钥
111
+ // (2026-09-25 修: 之前在这里二次 loadOrCreateDeviceKey, storage 不持久化时会生成另一把密钥
112
+ // → 桌面 device_mismatch 403。真机 localStorage 下看不出来, 但那是运气不是设计。)
113
+ const material = await loadOrCreateDeviceKey(storage, cobj);
114
+ const key = await cobj.subtle.importKey('jwk', material.privateKeyJwk, { name: 'Ed25519' }, false, ['sign']);
115
+ const bytes = bytesOf(String(payload ?? ''));
116
+ const sig = await cobj.subtle.sign({ name: 'Ed25519' }, key, bytes);
117
+ return { deviceId: material.deviceId, alg: 'ed25519', payloadHash: await sha256Hex(cobj, String(payload ?? '')), sig: b64(sig) };
118
+ }
119
+ /** sha256 hex (手机侧与桌面 Node 同值) —— 任务动作的内容摘要用它算 */
120
+ export async function sha256HexOf(text, c) {
101
121
  const cobj = cryptoOf(c);
102
- const key = await importPrivate(storage, cobj);
103
- const payload = canonicalRevocationPayload(rev);
104
- const sig = await cobj.subtle.sign({ name: 'Ed25519' }, key, bytesOf(payload));
105
- const dev = await loadOrCreateDeviceKey(storage, cobj);
106
- return { deviceId: dev.deviceId, alg: 'ed25519', payloadHash: await sha256Hex(cobj, payload), sig: b64(sig) };
122
+ if (!cobj?.subtle)
123
+ throw new Error('device_signing_unavailable: 本机没有 WebCrypto');
124
+ return sha256Hex(cobj, String(text ?? ''));
125
+ }
126
+ /**
127
+ * 用**已经加载好的**设备材料签名 (调用方自己拿 deviceId)。
128
+ *
129
+ * 为什么留这个口: 高风险任务动作的载荷里含 deviceId, 签名必须与那个 deviceId 同源。
130
+ * 若"取 deviceId"与"签名"各调一次 loadOrCreateDeviceKey, 在 storage 不持久化的环境
131
+ * (隐私模式 / storage 被禁) 会拿到两把不同密钥 → 桌面 device_mismatch 403。
132
+ */
133
+ export async function signPayloadWithMaterial(payload, material, c) {
134
+ const cobj = cryptoOf(c);
135
+ if (!cobj?.subtle)
136
+ throw new Error('device_signing_unavailable: 本机没有 WebCrypto');
137
+ const key = await cobj.subtle.importKey('jwk', material.privateKeyJwk, { name: 'Ed25519' }, false, ['sign']);
138
+ const sig = await cobj.subtle.sign({ name: 'Ed25519' }, key, bytesOf(String(payload ?? '')));
139
+ return { deviceId: material.deviceId, alg: 'ed25519', payloadHash: await sha256Hex(cobj, String(payload ?? '')), sig: b64(sig) };
107
140
  }
108
141
  async function post(base, path, body, f) {
109
142
  const res = await f(`${String(base).replace(/\/+$/, '')}${path}`, {