@bolloon/bolloon-agent 0.4.24 → 0.4.25
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/execution-supervisor.js +391 -0
- package/dist/agents/goal-store.js +451 -0
- package/dist/agents/pi-harness.js +263 -0
- package/dist/agents/pi-sdk.js +568 -126
- package/dist/agents/run-store.js +772 -0
- package/dist/agents/runner-resolver.js +225 -0
- package/dist/agents/skills-manager.js +435 -0
- package/dist/agents/supervisor-host.js +249 -0
- package/dist/cron/tick-lock.js +1 -1
- package/dist/index.js +428 -22
- package/dist/ios/agent-delegate-server.js +58 -12
- package/dist/ios/icons/icon-1024x1024.png +0 -0
- package/dist/ios/icons/icon-1024x1024.webp +0 -0
- package/dist/ios/icons/icon-216x216.png +0 -0
- package/dist/ios/icons/icon-216x216.webp +0 -0
- package/dist/ios/index.html +21 -1
- package/dist/ios/manifest.json +1 -1
- package/dist/ios/mobile-agent.js +195 -1
- package/dist/ios/mobile-core.js +24876 -24723
- package/dist/ios/mobile.css +15 -0
- package/dist/ios/mobile.html +21 -1
- package/dist/ios/mobile.js +143 -0
- package/dist/ios/server.js +51 -4
- package/dist/web/icons/icon-1024x1024.png +0 -0
- package/dist/web/icons/icon-1024x1024.webp +0 -0
- package/dist/web/icons/icon-216x216.png +0 -0
- package/dist/web/icons/icon-216x216.webp +0 -0
- package/dist/web/manifest.json +1 -1
- package/dist/web/mobile-agent.js +2 -2
- package/dist/web/mobile-core.js +24884 -24726
- package/dist/web/mobile-privacy.js +185 -0
- package/dist/web/mobile.css +15 -0
- package/dist/web/mobile.html +21 -1
- package/dist/web/mobile.js +179 -6
- package/dist/web/server.js +386 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1267,6 +1267,274 @@ async function processInputInner(input, comm) {
|
|
|
1267
1267
|
appendLine(`${C_DIM}用法: /net join <链接> | /net status | /net ctx <文本>${RESET}`);
|
|
1268
1268
|
return;
|
|
1269
1269
|
}
|
|
1270
|
+
// /runs — 持久化运行记录 (跨重载可读): 谁在跑 / 跑完了 / 被打断 / 卡住了
|
|
1271
|
+
if (cmd === '/runs' || cmd.startsWith('/runs ')) {
|
|
1272
|
+
const arg = trimmed.slice('/runs'.length).trim();
|
|
1273
|
+
try {
|
|
1274
|
+
const { listRuns, formatRunLine, readRun } = await import('./agents/run-store.js');
|
|
1275
|
+
if (arg) {
|
|
1276
|
+
const rec = await readRun(arg);
|
|
1277
|
+
if (!rec) {
|
|
1278
|
+
appendLine(`${C_ERROR}没有这个运行: ${arg}${RESET}`);
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
appendLine(`${C_DIM}run ${rec.runId} [${rec.surface}] ${rec.status} · ${rec.steps.length} 步 · pid ${rec.pid}${RESET}`);
|
|
1282
|
+
appendLine(`${C_DIM}目标: ${rec.goal.slice(0, 120)}${RESET}`);
|
|
1283
|
+
for (const s of rec.steps) {
|
|
1284
|
+
appendLine(` ${s.ok ? '✓' : '✗'} ${String(s.n).padStart(2)} ${s.tool}${s.ms ? ` (${s.ms}ms)` : ''} ${C_DIM}${(s.summary || s.error || '').slice(0, 80)}${RESET}`);
|
|
1285
|
+
}
|
|
1286
|
+
if (rec.error)
|
|
1287
|
+
appendLine(`${C_ERROR}结束原因: ${rec.error}${RESET}`);
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
const runs = await listRuns({ limit: 15 });
|
|
1291
|
+
if (!runs.length) {
|
|
1292
|
+
appendLine(`${C_DIM}还没有运行记录 (每次 agent 运行都会落盘到 ~/.bolloon/runs/)${RESET}`);
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
appendLine(`${C_DIM}最近 ${runs.length} 次运行 (落盘记录, 重开也还在):${RESET}`);
|
|
1296
|
+
for (const r of runs)
|
|
1297
|
+
appendLine(` ${formatRunLine(r)}`);
|
|
1298
|
+
appendLine(`${C_DIM}/runs <runId> 看逐步明细${RESET}`);
|
|
1299
|
+
}
|
|
1300
|
+
catch (e) {
|
|
1301
|
+
appendLine(`${C_ERROR}/runs 失败: ${String(e?.message || e).slice(0, 150)}${RESET}`);
|
|
1302
|
+
}
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
// 2026-09-16 (M2/M5): /resume <runId> · /pause <runId> · /approve <runId> · /goals
|
|
1306
|
+
// 恢复走的是同一条 run (从 checkpoint 继续), 不是"重发原 prompt"; 非幂等动作由重放守卫挡住。
|
|
1307
|
+
if (cmd === '/resume' || cmd.startsWith('/resume ')) {
|
|
1308
|
+
const runId = trimmed.slice('/resume'.length).trim();
|
|
1309
|
+
if (!runId) {
|
|
1310
|
+
const { listRuns, formatRunLine, RESUMABLE_STATUSES } = await import('./agents/run-store.js');
|
|
1311
|
+
const runs = await listRuns({ limit: 30 });
|
|
1312
|
+
const ok = runs.filter((r) => RESUMABLE_STATUSES.includes(r.status));
|
|
1313
|
+
if (!ok.length) {
|
|
1314
|
+
appendLine(`${C_DIM}没有可恢复的运行 (可恢复状态: ${RESUMABLE_STATUSES.join('/')})${RESET}`);
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
appendLine(`${C_DIM}可恢复的运行 (用 /resume <runId> 继续):${RESET}`);
|
|
1318
|
+
for (const r of ok)
|
|
1319
|
+
appendLine(` ${formatRunLine(r)}`);
|
|
1320
|
+
return;
|
|
1321
|
+
}
|
|
1322
|
+
try {
|
|
1323
|
+
const { readRun, prepareResume, buildResumeInstruction } = await import('./agents/run-store.js');
|
|
1324
|
+
const rec = await readRun(runId);
|
|
1325
|
+
if (!rec) {
|
|
1326
|
+
appendLine(`${C_ERROR}没有这个运行: ${runId}${RESET}`);
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
const agent = await getAgent();
|
|
1330
|
+
const active = String(agent?.currentChannelId || '');
|
|
1331
|
+
if (rec.channelId && active && rec.channelId !== active) {
|
|
1332
|
+
appendLine(`${C_ERROR}该运行属于 channel ${rec.channelId} (当前 ${active}) — 先 /channel 切过去再 /resume${RESET}`);
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
const prep = await prepareResume(runId);
|
|
1336
|
+
if (!prep.ok || !prep.plan) {
|
|
1337
|
+
appendLine(`${C_ERROR}无法恢复: ${prep.reason}${RESET}`);
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
appendLine(`${C_ACCENT}♻️ 从 checkpoint 恢复 ${runId} (已完成 ${prep.plan.completedSteps.length} 步, 非幂等守卫 ${prep.plan.replayGuards.length} 条)${RESET}`);
|
|
1341
|
+
appendLine(`${C_DIM}${buildResumeInstruction(prep.plan).split('\n').slice(0, 6).join('\n')}${RESET}`);
|
|
1342
|
+
if (!agent?.resumeRun) {
|
|
1343
|
+
appendLine(`${C_ERROR}当前 agent 不支持 resumeRun${RESET}`);
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
const r = await agent.resumeRun(runId);
|
|
1347
|
+
appendLine(r.ok ? `${C_ACCENT}✅ 恢复执行完成${RESET}` : `${C_ERROR}恢复失败: ${r.reason}${RESET}`);
|
|
1348
|
+
}
|
|
1349
|
+
catch (e) {
|
|
1350
|
+
appendLine(`${C_ERROR}/resume 失败: ${String(e?.message || e).slice(0, 200)}${RESET}`);
|
|
1351
|
+
}
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
if (cmd === '/pause' || cmd.startsWith('/pause ')) {
|
|
1355
|
+
const runId = trimmed.slice('/pause'.length).trim();
|
|
1356
|
+
if (!runId) {
|
|
1357
|
+
const { listRuns, formatRunLine } = await import('./agents/run-store.js');
|
|
1358
|
+
const runs = (await listRuns({ status: 'running', limit: 10 }));
|
|
1359
|
+
if (!runs.length) {
|
|
1360
|
+
appendLine(`${C_DIM}没有正在运行的 run${RESET}`);
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
appendLine(`${C_DIM}正在运行 (用 /pause <runId> 暂停):${RESET}`);
|
|
1364
|
+
for (const r of runs)
|
|
1365
|
+
appendLine(` ${formatRunLine(r)}`);
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
try {
|
|
1369
|
+
const { setRunStatus } = await import('./agents/run-store.js');
|
|
1370
|
+
const r = await setRunStatus(runId, 'paused', { error: '外部请求 (pause)' });
|
|
1371
|
+
appendLine(r.ok ? `${C_ACCENT}⏸️ 已请求暂停 ${runId} (运行中的 agent 会在下一轮循环停下)${RESET}` : `${C_ERROR}暂停被拒: ${r.reason}${RESET}`);
|
|
1372
|
+
}
|
|
1373
|
+
catch (e) {
|
|
1374
|
+
appendLine(`${C_ERROR}/pause 失败: ${String(e?.message || e).slice(0, 200)}${RESET}`);
|
|
1375
|
+
}
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
if (cmd === '/approve' || cmd.startsWith('/approve ')) {
|
|
1379
|
+
const runId = trimmed.slice('/approve'.length).trim();
|
|
1380
|
+
if (!runId) {
|
|
1381
|
+
const { listRuns, formatRunLine } = await import('./agents/run-store.js');
|
|
1382
|
+
const runs = (await listRuns({ status: 'needs_human', limit: 10 }));
|
|
1383
|
+
if (!runs.length) {
|
|
1384
|
+
appendLine(`${C_DIM}没有等待人工处置的 run${RESET}`);
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
appendLine(`${C_DIM}等待人工处置 (用 /approve <runId> 批准继续):${RESET}`);
|
|
1388
|
+
for (const r of runs)
|
|
1389
|
+
appendLine(` ${formatRunLine(r)}`);
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1392
|
+
try {
|
|
1393
|
+
const { readRun, recordRecovery } = await import('./agents/run-store.js');
|
|
1394
|
+
const rec = await readRun(runId);
|
|
1395
|
+
if (!rec) {
|
|
1396
|
+
appendLine(`${C_ERROR}没有这个运行: ${runId}${RESET}`);
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
if (rec.status !== 'needs_human') {
|
|
1400
|
+
appendLine(`${C_ERROR}只有 needs_human 的运行需要批准 (当前 ${rec.status})${RESET}`);
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
await recordRecovery(runId, { errorClass: rec.errorClass || 'unknown', message: '人工批准后继续', action: 'resume' });
|
|
1404
|
+
const agent = await getAgent();
|
|
1405
|
+
if (!agent?.resumeRun) {
|
|
1406
|
+
appendLine(`${C_ERROR}当前 agent 不支持 resumeRun${RESET}`);
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
const r = await agent.resumeRun(runId);
|
|
1410
|
+
appendLine(r.ok ? `${C_ACCENT}✅ 已批准并继续执行${RESET}` : `${C_ERROR}批准后恢复失败: ${r.reason}${RESET}`);
|
|
1411
|
+
}
|
|
1412
|
+
catch (e) {
|
|
1413
|
+
appendLine(`${C_ERROR}/approve 失败: ${String(e?.message || e).slice(0, 200)}${RESET}`);
|
|
1414
|
+
}
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
if (cmd === '/goals' || cmd.startsWith('/goals ')) {
|
|
1418
|
+
const arg = trimmed.slice('/goals'.length).trim();
|
|
1419
|
+
try {
|
|
1420
|
+
const { listGoals, readGoal, formatGoalLine, evaluateGoalCompletion } = await import('./agents/goal-store.js');
|
|
1421
|
+
if (arg) {
|
|
1422
|
+
const g = await readGoal(arg);
|
|
1423
|
+
if (!g) {
|
|
1424
|
+
appendLine(`${C_ERROR}没有这个目标: ${arg}${RESET}`);
|
|
1425
|
+
return;
|
|
1426
|
+
}
|
|
1427
|
+
appendLine(`${C_DIM}goal ${g.goalId} [${g.status}] 创建 ${g.createdAt}${RESET}`);
|
|
1428
|
+
appendLine(` 目标: ${g.objective}`);
|
|
1429
|
+
g.successCriteria.forEach((c, i) => appendLine(` ${g.completedCriteria.includes(i) ? '✓' : '·'} [${i}] ${c}`));
|
|
1430
|
+
appendLine(` runs: ${g.runs.join(', ') || '(无)'} 当前: ${g.currentRunId || '-'}`);
|
|
1431
|
+
if (g.evidence.length)
|
|
1432
|
+
appendLine(` 证据: ${g.evidence.slice(-3).join(' | ')}`);
|
|
1433
|
+
const v = evaluateGoalCompletion(g);
|
|
1434
|
+
appendLine(` 完成门: ${v.complete ? '✅ 可判完成' : `❌ ${v.reason}`}`);
|
|
1435
|
+
return;
|
|
1436
|
+
}
|
|
1437
|
+
const goals = await listGoals({ limit: 15 });
|
|
1438
|
+
if (!goals.length) {
|
|
1439
|
+
appendLine(`${C_DIM}还没有目标记录 (每次 prompt 都会建/续一个 Goal, 落盘 ~/.bolloon/goals/)${RESET}`);
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
appendLine(`${C_DIM}最近 ${goals.length} 个目标:${RESET}`);
|
|
1443
|
+
for (const g of goals)
|
|
1444
|
+
appendLine(` ${formatGoalLine(g)}`);
|
|
1445
|
+
appendLine(`${C_DIM}/goals <goalId> 看判据与证据${RESET}`);
|
|
1446
|
+
}
|
|
1447
|
+
catch (e) {
|
|
1448
|
+
appendLine(`${C_ERROR}/goals 失败: ${String(e?.message || e).slice(0, 200)}${RESET}`);
|
|
1449
|
+
}
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
// 2026-09-16 (M2-B): /supervise — 长期执行层 (状态/唤醒原因/手动推进一个周期) · /wake <goalId> — 外部事件唤醒
|
|
1453
|
+
if (cmd === '/supervise' || cmd.startsWith('/supervise ')) {
|
|
1454
|
+
const arg = trimmed.slice('/supervise'.length).trim();
|
|
1455
|
+
try {
|
|
1456
|
+
const { getSupervisor } = await import('./agents/execution-supervisor.js');
|
|
1457
|
+
const { wakeReport, listRunnableGoals, formatGoalLine } = await import('./agents/goal-store.js');
|
|
1458
|
+
const { runnable, skipped } = await listRunnableGoals({ now: Date.now() });
|
|
1459
|
+
const st = getSupervisor().status();
|
|
1460
|
+
appendLine(`${C_DIM}supervisor: owner=${st.owner} running=${st.running ? 'yes' : 'no'} tick=${st.tickIntervalMs}ms lease=${st.leaseTtlMs}ms dryRun=${st.dryRun ? 'yes' : 'no'} ticks=${st.ticks}${RESET}`);
|
|
1461
|
+
// 2026-09-16 (2-C.2): 宿主状态里的最近一次"执行器解析"阶段报告 —— 回答"这个 Goal 为什么没被执行、卡在哪一阶段"
|
|
1462
|
+
try {
|
|
1463
|
+
const { readSupervisorState } = await import('./agents/supervisor-host.js');
|
|
1464
|
+
const hostState = await readSupervisorState();
|
|
1465
|
+
if (hostState) {
|
|
1466
|
+
appendLine(`${C_DIM}宿主: worker=${hostState.workerId} pid=${hostState.pid} ticks=${hostState.ticks ?? 0}${hostState.stoppedAt ? ` 已停止(${hostState.stopReason})` : ''}${RESET}`);
|
|
1467
|
+
appendLine(`${C_DIM}最近一轮: ${hostState.lastSummary || '(无)'}${RESET}`);
|
|
1468
|
+
const lr = hostState.lastResolution;
|
|
1469
|
+
if (lr) {
|
|
1470
|
+
appendLine(`${lr.ok ? C_ACCENT : C_WARN}解析 ${lr.goalId} ${lr.ok ? '✅ 可执行' : `⛔ 卡在 ${lr.failedStage}`}${RESET}${lr.reason ? ` — ${lr.reason}` : ''}`);
|
|
1471
|
+
appendLine(` ${C_DIM}阶段: ${lr.stages}${RESET}`);
|
|
1472
|
+
for (const d of (lr.detail || []))
|
|
1473
|
+
if (d.note || d.error)
|
|
1474
|
+
appendLine(` ${C_DIM}· ${d.stage}: ${d.error || d.note}${RESET}`);
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
catch { /* 宿主状态可读性不影响诊断 */ }
|
|
1479
|
+
if (arg === 'tick' || arg === 'start') {
|
|
1480
|
+
// CLI 侧执行器: 用当前会话的 agent (没人注入 agent 时只诊断, 不假装跑过)
|
|
1481
|
+
const runner = agent
|
|
1482
|
+
? async (req) => {
|
|
1483
|
+
if (req.kind === 'resume' && req.prevRunId && typeof agent.resumeRun === 'function') {
|
|
1484
|
+
const r = await agent.resumeRun(req.prevRunId);
|
|
1485
|
+
return { runId: req.prevRunId, status: r?.ok ? 'done' : 'failed', error: r?.ok ? undefined : r?.reason };
|
|
1486
|
+
}
|
|
1487
|
+
agent.setGoalId?.(req.goal.goalId);
|
|
1488
|
+
agent.setContinuationGuards?.(req.guards || []);
|
|
1489
|
+
await agent.prompt(req.instruction);
|
|
1490
|
+
return { runId: agent.getLastRunId?.() || agent.getRunId?.(), status: 'done' };
|
|
1491
|
+
}
|
|
1492
|
+
: undefined;
|
|
1493
|
+
const { ExecutionSupervisor } = await import('./agents/execution-supervisor.js');
|
|
1494
|
+
const sup = new ExecutionSupervisor({ runner: runner, maxPerTick: 1, log: (m) => appendLine(`${C_DIM}${m}${RESET}`) });
|
|
1495
|
+
const rep = await sup.tickOnce();
|
|
1496
|
+
appendLine(`${C_ACCENT}调度周期 #${rep.tick}${RESET} 认领 ${rep.claimed.length} · 执行 ${rep.executed.length}${rep.skipped.length ? ` · 跳过 ${rep.skipped.length}` : ''}`);
|
|
1497
|
+
for (const s of rep.skipped.slice(0, 6))
|
|
1498
|
+
appendLine(` ${C_DIM}跳过 ${s.goalId}: ${s.reason}${RESET}`);
|
|
1499
|
+
for (const e of rep.executed)
|
|
1500
|
+
appendLine(` ▶ ${e.goalId} → run=${e.runId || '-'} ${e.status || ''}${e.error ? ` (${e.error})` : ''}`);
|
|
1501
|
+
return;
|
|
1502
|
+
}
|
|
1503
|
+
const rows = await wakeReport();
|
|
1504
|
+
if (!rows.length) {
|
|
1505
|
+
appendLine(`${C_DIM}还没有目标。${RESET}`);
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1508
|
+
for (const r of rows.slice(0, 12))
|
|
1509
|
+
appendLine(` ${r.goalId} [${r.status}] 唤醒: ${r.wake}${r.lease ? ` (lease ${r.lease})` : ''}`);
|
|
1510
|
+
if (runnable.length) {
|
|
1511
|
+
appendLine(`${C_DIM}现在可推进:${RESET}`);
|
|
1512
|
+
for (const g of runnable.slice(0, 5))
|
|
1513
|
+
appendLine(` ${formatGoalLine(g)}`);
|
|
1514
|
+
}
|
|
1515
|
+
appendLine(`${C_DIM}/supervise tick 手动推进一个周期${RESET}`);
|
|
1516
|
+
}
|
|
1517
|
+
catch (e) {
|
|
1518
|
+
appendLine(`${C_ERROR}/supervise 失败: ${String(e?.message || e).slice(0, 200)}${RESET}`);
|
|
1519
|
+
}
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
if (cmd === '/wake' || cmd.startsWith('/wake ')) {
|
|
1523
|
+
const goalId = trimmed.slice('/wake'.length).trim();
|
|
1524
|
+
if (!goalId) {
|
|
1525
|
+
appendLine(`${C_ERROR}用法: /wake <goalId> (外部事件到达后唤醒在等它的目标)${RESET}`);
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1528
|
+
try {
|
|
1529
|
+
const { getSupervisor } = await import('./agents/execution-supervisor.js');
|
|
1530
|
+
const woke = await getSupervisor().notifyExternal(goalId);
|
|
1531
|
+
appendLine(woke ? `${C_ACCENT}✅ 已唤醒 ${goalId} (下一次 tick 推进; 不重发已成功的请求)${RESET}` : `${C_DIM}${goalId} 不在等待外部事件 (未改动)${RESET}`);
|
|
1532
|
+
}
|
|
1533
|
+
catch (e) {
|
|
1534
|
+
appendLine(`${C_ERROR}/wake 失败: ${String(e?.message || e).slice(0, 200)}${RESET}`);
|
|
1535
|
+
}
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1270
1538
|
// /model — 无参: 交互选择器 (ink 渲染, 复用 MentionPopup); 有参: 直接切换/测连通/看状态
|
|
1271
1539
|
if (cmd === '/model' || cmd.startsWith('/model ')) {
|
|
1272
1540
|
const modelArg = trimmed.slice('/model'.length).trim();
|
|
@@ -1870,40 +2138,39 @@ async function processInputInner(input, comm) {
|
|
|
1870
2138
|
// /skills [名] — 查看正式技能 (2026-08-12 Task5): 无参列全部, 带名看详情. 运行时开始前的技能 view.
|
|
1871
2139
|
if (cmd === '/skills' || cmd.startsWith('/skills ')) {
|
|
1872
2140
|
try {
|
|
1873
|
-
|
|
1874
|
-
const
|
|
1875
|
-
const
|
|
1876
|
-
|
|
1877
|
-
const m = await loadSkillsDir(d);
|
|
1878
|
-
for (const s of m)
|
|
1879
|
-
if (s.status === 'active' && !metas.some(x => x.name === s.name))
|
|
1880
|
-
metas.push(s);
|
|
1881
|
-
}
|
|
2141
|
+
// 2026-09-16 (2-G.1): 改走统一 Skills Manager —— CLI / Web / agent 看的是同一份事实
|
|
2142
|
+
const { getSkillsManager, formatSkillLine } = await import('./agents/skills-manager.js');
|
|
2143
|
+
const sm = getSkillsManager();
|
|
2144
|
+
const list = await sm.view();
|
|
1882
2145
|
const q = cmd.startsWith('/skills ') ? cmd.slice('/skills '.length).trim().toLowerCase() : '';
|
|
1883
2146
|
if (!q) {
|
|
1884
|
-
appendLine(`${C_ACCENT}技能 (${
|
|
1885
|
-
if (
|
|
1886
|
-
appendLine(` ${C_DIM}
|
|
1887
|
-
for (const s of
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
}
|
|
1891
|
-
appendLine(`${C_DIM}用法: /skills <名>
|
|
2147
|
+
appendLine(`${C_ACCENT}技能 (${list.length}) [status/source/trust/版本/hash]:${RESET}`);
|
|
2148
|
+
if (list.length === 0)
|
|
2149
|
+
appendLine(` ${C_DIM}暂无技能 — run-end 经验可沉淀为 skill${RESET}`);
|
|
2150
|
+
for (const s of list.slice(0, 20))
|
|
2151
|
+
appendLine(` ${C_DIM}·${RESET} ${formatSkillLine(s)}`);
|
|
2152
|
+
const h = await sm.health();
|
|
2153
|
+
appendLine(`${C_DIM}健康: ${JSON.stringify(h.byStatus)}${h.drifted.length ? ` · 内容漂移 ${h.drifted.length}` : ''}${h.invalid.length ? ` · 不合格 ${h.invalid.length}` : ''}${RESET}`);
|
|
2154
|
+
appendLine(`${C_DIM}用法: /skills <名> 详情 · /skill health | inspect|enable|disable|approve|validate|import|export <名|链接>${RESET}`);
|
|
1892
2155
|
}
|
|
1893
2156
|
else {
|
|
1894
|
-
const hit =
|
|
2157
|
+
const hit = list.find(s => s.name.toLowerCase() === q) || list.find(s => s.name.toLowerCase().includes(q));
|
|
1895
2158
|
if (!hit) {
|
|
1896
2159
|
appendLine(`${C_WARN}未找到技能: '${q}'${RESET}`);
|
|
1897
2160
|
return;
|
|
1898
2161
|
}
|
|
1899
2162
|
appendLine(`${C_ACCENT}═ ${hit.name} ═${RESET}`);
|
|
2163
|
+
appendLine(` ${C_DIM}状态:${RESET} ${hit.status} ${C_DIM}来源:${RESET} ${hit.source}${hit.sourceRef ? ` (${hit.sourceRef.slice(0, 60)})` : ''} ${C_DIM}信任:${RESET} ${hit.trust}`);
|
|
2164
|
+
appendLine(` ${C_DIM}版本:${RESET} v${hit.version} ${C_DIM}内容哈希:${RESET} ${hit.contentHash}${hit.registryHash ? ` (registry ${hit.registryHash.slice(0, 10)}${hit.registryHash !== hit.contentHash ? ' ⚠ 已漂移' : ''})` : ''}`);
|
|
2165
|
+
appendLine(` ${C_DIM}目录:${RESET} ${hit.dir} ${hit.fileCount} 个文件 / ${Math.round(hit.bytes / 1024)}KB`);
|
|
1900
2166
|
if (hit.description)
|
|
1901
2167
|
appendLine(` ${C_DIM}描述:${RESET} ${hit.description}`);
|
|
1902
|
-
if (hit.triggers
|
|
2168
|
+
if (hit.triggers.length)
|
|
1903
2169
|
appendLine(` ${C_DIM}触发:${RESET} ${hit.triggers.join(', ')}`);
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
2170
|
+
if (hit.issues.length)
|
|
2171
|
+
appendLine(` ${C_WARN}问题:${RESET} ${hit.issues.join('; ')}`);
|
|
2172
|
+
const body = (await import('fs/promises')).readFile(hit.skillFile, 'utf-8').catch(() => '');
|
|
2173
|
+
void body;
|
|
1907
2174
|
}
|
|
1908
2175
|
}
|
|
1909
2176
|
catch (e) {
|
|
@@ -1911,6 +2178,101 @@ async function processInputInner(input, comm) {
|
|
|
1911
2178
|
}
|
|
1912
2179
|
return;
|
|
1913
2180
|
}
|
|
2181
|
+
// 2026-09-16 (2-G.1): /skill <子命令> —— 统一管理面 (enable/disable/approve/validate/quarantine/import/export/inspect/health)
|
|
2182
|
+
if (cmd === '/skill' || cmd.startsWith('/skill ')) {
|
|
2183
|
+
const rest = trimmed.slice('/skill'.length).trim();
|
|
2184
|
+
const [sub, ...args] = rest.split(/\s+/).filter(Boolean);
|
|
2185
|
+
const arg = args.join(' ').trim();
|
|
2186
|
+
try {
|
|
2187
|
+
const { getSkillsManager, formatSkillLine } = await import('./agents/skills-manager.js');
|
|
2188
|
+
const sm = getSkillsManager();
|
|
2189
|
+
if (!sub || sub === 'list') {
|
|
2190
|
+
for (const s of await sm.view())
|
|
2191
|
+
appendLine(` ${formatSkillLine(s)}`);
|
|
2192
|
+
return;
|
|
2193
|
+
}
|
|
2194
|
+
if (sub === 'health') {
|
|
2195
|
+
const h = await sm.health();
|
|
2196
|
+
appendLine(`${C_ACCENT}技能健康:${RESET} 共 ${h.total} 状态 ${JSON.stringify(h.byStatus)} 来源 ${JSON.stringify(h.bySource)}`);
|
|
2197
|
+
if (h.drifted.length) {
|
|
2198
|
+
appendLine(`${C_WARN}内容漂移 (SKILL.md 被改过, 与 registry 基线不一致):${RESET}`);
|
|
2199
|
+
for (const d of h.drifted)
|
|
2200
|
+
appendLine(` ${d.name} registry=${String(d.expected).slice(0, 10)} 现在=${d.actual.slice(0, 10)}`);
|
|
2201
|
+
}
|
|
2202
|
+
if (h.invalid.length) {
|
|
2203
|
+
appendLine(`${C_WARN}不合格:${RESET}`);
|
|
2204
|
+
for (const i of h.invalid)
|
|
2205
|
+
appendLine(` ${i.name}: ${i.issues.join('; ')}`);
|
|
2206
|
+
}
|
|
2207
|
+
if (h.duplicates.length) {
|
|
2208
|
+
appendLine(`${C_WARN}同名多处:${RESET}`);
|
|
2209
|
+
for (const d of h.duplicates)
|
|
2210
|
+
appendLine(` ${d.name}: ${d.dirs.join(' | ')}`);
|
|
2211
|
+
}
|
|
2212
|
+
if (h.missing.length)
|
|
2213
|
+
appendLine(`${C_WARN}registry 里记着但盘上没有:${RESET} ${h.missing.join(', ')}`);
|
|
2214
|
+
return;
|
|
2215
|
+
}
|
|
2216
|
+
if (sub === 'import') {
|
|
2217
|
+
if (!arg) {
|
|
2218
|
+
appendLine(`${C_ERROR}用法: /skill import <bolloon://skill/<cid> | ipfs://<cid> | <cid>>${RESET}`);
|
|
2219
|
+
return;
|
|
2220
|
+
}
|
|
2221
|
+
appendLine(`${C_DIM}导入中: ${arg.slice(0, 80)} …${RESET}`);
|
|
2222
|
+
const r = await sm.import(arg);
|
|
2223
|
+
appendLine(r.ok ? `${C_ACCENT}✅ 已导入 ${r.name}@${r.version} (状态 installed, 信任 unverified)${RESET}` : `${C_ERROR}导入失败: ${r.error}${RESET}`);
|
|
2224
|
+
return;
|
|
2225
|
+
}
|
|
2226
|
+
if (sub === 'export') {
|
|
2227
|
+
if (!arg) {
|
|
2228
|
+
appendLine(`${C_ERROR}用法: /skill export <名>${RESET}`);
|
|
2229
|
+
return;
|
|
2230
|
+
}
|
|
2231
|
+
const r = await sm.export(arg);
|
|
2232
|
+
if (!r.ok) {
|
|
2233
|
+
appendLine(`${C_ERROR}导出失败: ${r.error}${RESET}`);
|
|
2234
|
+
return;
|
|
2235
|
+
}
|
|
2236
|
+
appendLine(`${C_ACCENT}技能包 JSON (${Object.keys(r.bundle.files).length} 个文件):${RESET}`);
|
|
2237
|
+
appendLine(JSON.stringify(r.bundle).slice(0, 400));
|
|
2238
|
+
return;
|
|
2239
|
+
}
|
|
2240
|
+
if (!arg) {
|
|
2241
|
+
appendLine(`${C_ERROR}用法: /skill ${sub} <名>${RESET}`);
|
|
2242
|
+
return;
|
|
2243
|
+
}
|
|
2244
|
+
const act = {
|
|
2245
|
+
enable: () => sm.enable(arg),
|
|
2246
|
+
disable: () => sm.disable(arg),
|
|
2247
|
+
approve: () => sm.approve(arg, 'cli'),
|
|
2248
|
+
validate: () => sm.validate(arg),
|
|
2249
|
+
quarantine: () => sm.quarantine(arg, 'cli 手动隔离'),
|
|
2250
|
+
inspect: async () => ({ ok: true, skill: await sm.inspect(arg) }),
|
|
2251
|
+
};
|
|
2252
|
+
const fn = act[sub];
|
|
2253
|
+
if (!fn) {
|
|
2254
|
+
appendLine(`${C_ERROR}未知子命令: ${sub} (可用: health|import|export|inspect|enable|disable|approve|validate|quarantine)${RESET}`);
|
|
2255
|
+
return;
|
|
2256
|
+
}
|
|
2257
|
+
const r = await fn();
|
|
2258
|
+
if (!r.ok) {
|
|
2259
|
+
appendLine(`${C_WARN}${sub} 未完成: ${r.reason || (r.issues || []).join('; ') || '未知原因'}${RESET}`);
|
|
2260
|
+
}
|
|
2261
|
+
if (r.skill)
|
|
2262
|
+
appendLine(` ${formatSkillLine(r.skill)}`);
|
|
2263
|
+
else if (r.ok)
|
|
2264
|
+
appendLine(`${C_ACCENT}✅ ${sub} ${arg}${RESET}`);
|
|
2265
|
+
if (sub === 'inspect' && r.skill) {
|
|
2266
|
+
appendLine(` ${C_DIM}目录:${RESET} ${r.skill.dir}`);
|
|
2267
|
+
appendLine(` ${C_DIM}SKILL.md:${RESET} ${r.skill.skillFile}`);
|
|
2268
|
+
appendLine(` ${C_DIM}问题:${RESET} ${r.skill.issues.length ? r.skill.issues.join('; ') : '无'}`);
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
catch (e) {
|
|
2272
|
+
appendLine(`${C_ERROR}/skill 失败: ${String(e?.message || e).slice(0, 160)}${RESET}`);
|
|
2273
|
+
}
|
|
2274
|
+
return;
|
|
2275
|
+
}
|
|
1914
2276
|
// /mcp — MCP 插件/工具列表
|
|
1915
2277
|
if (cmd === '/mcp') {
|
|
1916
2278
|
try {
|
|
@@ -2260,6 +2622,13 @@ async function processInputInner(input, comm) {
|
|
|
2260
2622
|
appendLine(` ${C_ACCENT}/dequeue${RESET} 出队一条`);
|
|
2261
2623
|
appendLine(` ${C_ACCENT}/channel [名字|id|序号]${RESET} 切换当前智能体 ${C_DIM}无参列出所有; 支持名字/ID/序号三种解析${RESET}`);
|
|
2262
2624
|
appendLine(` ${C_ACCENT}/model${RESET} 模型供应商选择器 ${C_DIM}无参=选择器 · /model <名> [模型] 直接切换 · /model test 测连通${RESET}`);
|
|
2625
|
+
appendLine(` ${C_ACCENT}/runs${RESET} 运行记录 (落盘, 跨重载可读) ${C_DIM}/runs · /runs <runId> 看逐步明细${RESET}`);
|
|
2626
|
+
appendLine(` ${C_ACCENT}/resume${RESET} 从 checkpoint 继续一次运行 ${C_DIM}/resume · /resume <runId> (不是重发原 prompt)${RESET}`);
|
|
2627
|
+
appendLine(` ${C_ACCENT}/pause${RESET} 暂停一次运行 ${C_DIM}/pause · /pause <runId>${RESET}`);
|
|
2628
|
+
appendLine(` ${C_ACCENT}/approve${RESET} 批准等待人工处置的运行 ${C_DIM}/approve · /approve <runId>${RESET}`);
|
|
2629
|
+
appendLine(` ${C_ACCENT}/supervise${RESET} 长期执行层状态与唤醒原因 ${C_DIM}/supervise · /supervise tick${RESET}`);
|
|
2630
|
+
appendLine(` ${C_ACCENT}/wake${RESET} 外部事件到达 → 唤醒在等的目标 ${C_DIM}/wake <goalId>${RESET}`);
|
|
2631
|
+
appendLine(` ${C_ACCENT}/goals${RESET} 目标 (判据/证据/完成门) ${C_DIM}/goals · /goals <goalId>${RESET}`);
|
|
2263
2632
|
appendLine(` ${C_ACCENT}/setup${RESET} 初始化 / 配置总览 ${C_DIM}身份 + 供应商 + 配置文件路径${RESET}`);
|
|
2264
2633
|
appendLine(` ${C_ACCENT}/questions${RESET} 待回答的问题 ${C_DIM}智能体 clarify 提问时, 直接输入即回答 (或 /answer <文本>)${RESET}`);
|
|
2265
2634
|
appendLine(` ${C_ACCENT}/login${RESET} 登录 GitHub/Google 账号 (骨架) ${C_DIM}暂无真实 OAuth${RESET}`);
|
|
@@ -3528,6 +3897,18 @@ function parseArgs() {
|
|
|
3528
3897
|
case '--web':
|
|
3529
3898
|
result.web = true;
|
|
3530
3899
|
break;
|
|
3900
|
+
// 2026-09-16 (2-C.1): 独立 Supervisor 宿主 —— 长期执行不依附 web 进程
|
|
3901
|
+
case '--supervise':
|
|
3902
|
+
result.supervise = true;
|
|
3903
|
+
break;
|
|
3904
|
+
case '--supervise-once':
|
|
3905
|
+
result.supervise = true;
|
|
3906
|
+
result.superviseOnce = true;
|
|
3907
|
+
break;
|
|
3908
|
+
case '--supervise-dry-run':
|
|
3909
|
+
result.supervise = true;
|
|
3910
|
+
result.superviseDryRun = true;
|
|
3911
|
+
break;
|
|
3531
3912
|
case '--help':
|
|
3532
3913
|
case '-h':
|
|
3533
3914
|
result.help = true;
|
|
@@ -3950,6 +4331,31 @@ async function main() {
|
|
|
3950
4331
|
})();
|
|
3951
4332
|
}
|
|
3952
4333
|
const mode = args.web ? 'web' : 'cli';
|
|
4334
|
+
// 2026-09-16 (2-C.1): 独立 Supervisor 宿主 —— `bolloon --supervise [--supervise-once|--supervise-dry-run]`
|
|
4335
|
+
// 长期执行不再依附 web 进程 (页面关掉/CLI 没开也能继续推进 Goal)。
|
|
4336
|
+
if (args.supervise) {
|
|
4337
|
+
const { runStandaloneSupervisorHost } = await import('./agents/supervisor-host.js');
|
|
4338
|
+
const res = await runStandaloneSupervisorHost({
|
|
4339
|
+
once: !!args.superviseOnce,
|
|
4340
|
+
dryRun: !!args.superviseDryRun,
|
|
4341
|
+
log: (m) => console.log(m),
|
|
4342
|
+
});
|
|
4343
|
+
if (args.superviseOnce) {
|
|
4344
|
+
const rep = res.lastReport;
|
|
4345
|
+
if (rep) {
|
|
4346
|
+
console.log(`调度周期 #${rep.tick}: 认领 ${rep.claimed.length} · 执行 ${rep.executed.length} · 跳过 ${rep.skipped.length}${rep.errors.length ? ` · 错误 ${rep.errors.length}` : ''}`);
|
|
4347
|
+
for (const s of rep.skipped.slice(0, 8))
|
|
4348
|
+
console.log(` 跳过 ${s.goalId}: ${s.reason}`);
|
|
4349
|
+
for (const e of rep.executed)
|
|
4350
|
+
console.log(` ▶ ${e.goalId} → run=${e.runId || '-'} ${e.status || ''}${e.error ? ` (${e.error})` : ''}`);
|
|
4351
|
+
}
|
|
4352
|
+
console.log(`supervisor 宿主: owner=${res.state.owner} worker=${res.state.workerId} ticks=${res.ticks} 状态文件=~/.bolloon/supervisor.json`);
|
|
4353
|
+
}
|
|
4354
|
+
else {
|
|
4355
|
+
console.log(`[supervisor] 常驻宿主已启动 (owner=${res.state.owner}, worker=${res.state.workerId}); Ctrl-C 优雅停止`);
|
|
4356
|
+
}
|
|
4357
|
+
return;
|
|
4358
|
+
}
|
|
3953
4359
|
const isNonInteractive = !!(args.tool || args.prompt);
|
|
3954
4360
|
const originalLog = console.log;
|
|
3955
4361
|
const originalInfo = console.info;
|
|
@@ -17,9 +17,10 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import express from 'express';
|
|
19
19
|
import { buildAgentDelegateRequest, buildAgentResponse, buildManifestPayload, parseFrame, setLocalManifest, getLocalManifest, getRemoteManifests, cacheRemoteManifest, pickAgent, } from '../agents/agent-manifest-protocol.js';
|
|
20
|
-
export function createAgentDelegateApp(transport) {
|
|
20
|
+
export function createAgentDelegateApp(transport, options = {}) {
|
|
21
21
|
const app = express();
|
|
22
22
|
app.use(express.json({ limit: '2mb' }));
|
|
23
|
+
const executeTimeoutMs = options.executeTimeoutMs ?? 60_000;
|
|
23
24
|
// ---- 本地 manifest ----
|
|
24
25
|
app.get('/api/agent/local-manifest', (_req, res) => {
|
|
25
26
|
res.json(getLocalManifest());
|
|
@@ -81,7 +82,10 @@ export function createAgentDelegateApp(transport) {
|
|
|
81
82
|
}
|
|
82
83
|
res.json({
|
|
83
84
|
ok: true,
|
|
84
|
-
|
|
85
|
+
// 2026-09-15: 没缓存到对端 manifest 就如实给 null —— 旧版会编一个
|
|
86
|
+
// 「capabilities:[capability], name:<id>」的假目标, 让人以为已经知道对端是谁。
|
|
87
|
+
targetAgent: targetAgent || null,
|
|
88
|
+
targetAgentKnown: !!targetAgent,
|
|
85
89
|
response: f.payload,
|
|
86
90
|
});
|
|
87
91
|
}
|
|
@@ -104,18 +108,60 @@ export function createAgentDelegateApp(transport) {
|
|
|
104
108
|
return null; // 不需要回包
|
|
105
109
|
}
|
|
106
110
|
if (f.type === 'agent_delegate') {
|
|
107
|
-
// 路由到本地匹配 agent
|
|
108
111
|
const req = f.payload;
|
|
112
|
+
const capability = String(req?.capability || '');
|
|
113
|
+
// 2026-09-15: 严格按文档 §6 / §9 —— 只认 capabilities 含该能力且 active 的 agent。
|
|
114
|
+
// 旧实现 `|| local.agents[0]` 会把不匹配的指令塞给任意一个本地 agent,
|
|
115
|
+
// 与「pick 404 = 没有匹配能力」的语义自相矛盾。
|
|
109
116
|
const local = getLocalManifest();
|
|
110
|
-
const target = local.agents.find((a) => a.capabilities.includes(
|
|
111
|
-
if (!target)
|
|
112
|
-
return buildAgentResponse({
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
117
|
+
const target = local.agents.find((a) => a.capabilities.includes(capability) && a.status === 'active');
|
|
118
|
+
if (!target) {
|
|
119
|
+
return buildAgentResponse({
|
|
120
|
+
ok: false,
|
|
121
|
+
delegatedTo: 'none',
|
|
122
|
+
summary: `no local agent available for capability '${capability}'`,
|
|
123
|
+
error: 'no-capability-match',
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
// 匹配到了, 但本节点没接执行器 → 如实说"干不了", 不假签收
|
|
127
|
+
if (!options.execute) {
|
|
128
|
+
return buildAgentResponse({
|
|
129
|
+
ok: false,
|
|
130
|
+
delegatedTo: target.id,
|
|
131
|
+
summary: `matched agent '${target.name}' but this node has no executor wired`,
|
|
132
|
+
error: 'no-executor',
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
const raced = await Promise.race([
|
|
137
|
+
options.execute({
|
|
138
|
+
capability,
|
|
139
|
+
instruction: String(req?.instruction || ''),
|
|
140
|
+
docPath: req?.docPath ? String(req.docPath) : undefined,
|
|
141
|
+
docContent: req?.docContent ? String(req.docContent) : undefined,
|
|
142
|
+
fromAgentId: req?.fromAgentId ? String(req.fromAgentId) : undefined,
|
|
143
|
+
fromPublicKey,
|
|
144
|
+
targetAgentId: target.id,
|
|
145
|
+
targetAgentName: target.name,
|
|
146
|
+
}),
|
|
147
|
+
new Promise((resolve) => setTimeout(() => resolve({ ok: false, summary: `executor timed out after ${executeTimeoutMs}ms`, error: 'executor-timeout' }), executeTimeoutMs)),
|
|
148
|
+
]);
|
|
149
|
+
return buildAgentResponse({
|
|
150
|
+
ok: !!raced.ok,
|
|
151
|
+
delegatedTo: target.id,
|
|
152
|
+
resultCid: raced.resultCid,
|
|
153
|
+
summary: String(raced.summary || '').slice(0, 4000),
|
|
154
|
+
error: raced.error,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
catch (e) {
|
|
158
|
+
return buildAgentResponse({
|
|
159
|
+
ok: false,
|
|
160
|
+
delegatedTo: target.id,
|
|
161
|
+
summary: `executor threw: ${String(e?.message || e).slice(0, 300)}`,
|
|
162
|
+
error: 'executor-error',
|
|
163
|
+
});
|
|
164
|
+
}
|
|
119
165
|
}
|
|
120
166
|
return null;
|
|
121
167
|
});
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/ios/index.html
CHANGED
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
</header>
|
|
44
44
|
|
|
45
45
|
<!-- 主页面: 双频滑动卡片 -->
|
|
46
|
-
<main class="page-container" id="page-main" data-tab="main">
|
|
46
|
+
<main class="page-container" id="page-main" data-tab="main" hidden>
|
|
47
47
|
<!-- 水平滑动卡片轨道 -->
|
|
48
48
|
<div class="card-carousel" id="card-carousel">
|
|
49
49
|
<div class="card-track" id="card-track">
|
|
@@ -139,6 +139,26 @@
|
|
|
139
139
|
<button class="create-session-btn" id="btn-create-session" hidden>+ 创建新会话</button>
|
|
140
140
|
|
|
141
141
|
<!-- 创建智能体: 底部滑入加载 sheet -->
|
|
142
|
+
<!-- 2026-09-16: 首启隐私同意门 — 上架要求: 用户同意前不读取本机数据 / 不连网 / 不申请任何权限 -->
|
|
143
|
+
<div class="sheet" id="privacy-gate" hidden>
|
|
144
|
+
<div class="sheet-inner">
|
|
145
|
+
<div class="sheet-title" id="privacy-gate-title">隐私政策与权限说明</div>
|
|
146
|
+
<div class="sheet-text" id="privacy-gate-body"></div>
|
|
147
|
+
<a class="privacy-link" href="#" id="privacy-gate-link">阅读完整隐私政策 ›</a>
|
|
148
|
+
<button class="sheet-choice" id="privacy-agree">同意并继续</button>
|
|
149
|
+
<button class="sheet-choice sheet-cancel" id="privacy-decline">不同意</button>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
<!-- 2026-09-16: 应用内隐私政策 (全屏, 离线可用; 权威版 = bolloon.cn/privacy.html) -->
|
|
154
|
+
<div class="chat-page" id="policy-page" hidden>
|
|
155
|
+
<div class="chat-topbar">
|
|
156
|
+
<button class="icon-btn" id="policy-back">←</button>
|
|
157
|
+
<div style="flex:1;font-weight:600">隐私政策</div>
|
|
158
|
+
</div>
|
|
159
|
+
<div class="policy-body" id="policy-body"></div>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
142
162
|
<div class="sheet" id="create-sheet" hidden>
|
|
143
163
|
<div class="sheet-inner">
|
|
144
164
|
<div class="spinner"></div>
|
package/dist/ios/manifest.json
CHANGED