Files
boss/docs/superpowers/plans/2026-04-05-adb-wireless-keeper.md
2026-04-05 06:06:14 +08:00

25 KiB
Raw Permalink Blame History

ADB Wireless Keeper Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 为 Boss 本机增加一套多 Android 设备无线 ADB 自动扫描与后台自动重连守护方案,并提供状态、安装和文档入口。

Architecture: 保持轻量本机运维路线,不改 Web 或 Android 业务链路。核心逻辑集中在一个可测试的 Node ESM 脚本里,外层再包一层 zsh 脚本和 launchd 常驻安装脚本;运行态写入本机 data/ 日志和状态文件,不把运行态文件提交到仓库。

Tech Stack: Node.js ESM、node:testnode:assert/strictzsh、macOS launchd、本机 adb


File Map

New tracked files

  • scripts/adb-wireless-keeper.mjs
    • 核心守护逻辑与 CLI 入口。
    • 负责配置解析、状态读写、子网推断、候选过滤、ADB 重连、日志落盘、状态摘要。
  • tests/adb-wireless-keeper.test.mjs
    • Node 侧单测,覆盖纯逻辑和一次扫描/重连编排。
  • scripts/adb-wireless-keeper-once.sh
    • 单次执行入口,便于人工调试一轮扫描。
  • scripts/adb-wireless-keeper-status.sh
    • 状态查看入口,读取状态文件并打印当前摘要。
  • scripts/install-adb-wireless-keeper.sh
    • 安装并启动 launchd 常驻服务。
  • scripts/uninstall-adb-wireless-keeper.sh
    • 卸载并停止 launchd 常驻服务。
  • deployment/launchd/com.hyzq.boss.adb-wireless-keeper.plist
    • launchd 模板,带仓库根路径和配置占位符。
  • config/adb-wireless-keeper.json
    • 默认配置文件,定义扫描周期、白名单和日志/状态路径。
  • docs/superpowers/specs/2026-04-05-adb-wireless-keeper-design.md
    • 已存在 spec本计划直接实现它。

Modified tracked files

  • README.md
    • 新增脚本用法、安装命令、限制说明。
  • docs/architecture/current_runtime_and_deploy_status_cn.md
    • 把新的守护脚本和运行态约束写进当前运行时文档。

Runtime-only files created by script, not committed

  • data/adb-wireless-keeper-state.json
  • data/adb-wireless-keeper.log.jsonl

Task 1: 建立可测试的核心配置与过滤逻辑

Files:

  • Create: tests/adb-wireless-keeper.test.mjs

  • Create: scripts/adb-wireless-keeper.mjs

  • Step 1: 写第一个失败测试,覆盖配置默认值、白名单匹配和退避规则

import test from "node:test";
import assert from "node:assert/strict";
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";

import {
  computeBackoffMs,
  loadKeeperConfig,
  shouldManageHost,
} from "../scripts/adb-wireless-keeper.mjs";

let runtimeRoot = "";

test.before(async () => {
  runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-adb-keeper-"));
});

test.after(async () => {
  if (runtimeRoot) {
    await rm(runtimeRoot, { recursive: true, force: true });
  }
});

test("loadKeeperConfig fills defaults and preserves allowedDevices", async () => {
  const configPath = path.join(runtimeRoot, "keeper.json");
  await writeFile(
    configPath,
    JSON.stringify({
      allowedDevices: [{ label: "boss-oppo-main", serialHint: "SER-001" }],
    }),
    "utf8",
  );

  const config = await loadKeeperConfig(configPath);

  assert.equal(config.scanIntervalSeconds, 30);
  assert.equal(config.connectTimeoutMs, 4000);
  assert.equal(config.allowedDevices.length, 1);
  assert.equal(config.allowedDevices[0].label, "boss-oppo-main");
});

test("shouldManageHost only admits whitelist or known-device hosts", () => {
  const allowedDevices = [{ label: "boss-oppo-main", serialHint: "SER-001", ipHint: "192.168.31.18" }];
  const knownDevices = {
    "boss-oppo-main": {
      serial: "SER-001",
      lastKnownIp: "192.168.31.18",
      failureCount: 0,
      backoffUntil: "",
    },
  };

  assert.equal(
    shouldManageHost({
      ip: "192.168.31.18",
      allowedDevices,
      knownDevices,
    }).allowed,
    true,
  );
  assert.equal(
    shouldManageHost({
      ip: "192.168.31.99",
      allowedDevices,
      knownDevices,
    }).allowed,
    false,
  );
});

test("computeBackoffMs escalates after repeated failures", () => {
  assert.equal(computeBackoffMs(1), 30_000);
  assert.equal(computeBackoffMs(2), 120_000);
  assert.equal(computeBackoffMs(4), 300_000);
});
  • Step 2: 跑测试确认它先失败

Run: node --test tests/adb-wireless-keeper.test.mjs

Expected: FAILERR_MODULE_NOT_FOUND 或缺失导出,例如 loadKeeperConfig is not exported

  • Step 3: 在核心脚本里补最小实现,让这些纯逻辑先可测
#!/usr/bin/env node

import { readFile } from "node:fs/promises";
import { resolve } from "node:path";

const DEFAULT_CONFIG = Object.freeze({
  subnets: [],
  scanIntervalSeconds: 30,
  connectTimeoutMs: 4000,
  adbPath: "",
  logFile: "data/adb-wireless-keeper.log.jsonl",
  stateFile: "data/adb-wireless-keeper-state.json",
  allowedDevices: [],
});

function normalizeAllowedDevice(raw = {}) {
  return {
    label: typeof raw.label === "string" ? raw.label.trim() : "",
    serialHint: typeof raw.serialHint === "string" ? raw.serialHint.trim() : "",
    ipHint: typeof raw.ipHint === "string" ? raw.ipHint.trim() : "",
  };
}

export async function loadKeeperConfig(configPath) {
  const resolvedPath = resolve(configPath);
  const raw = JSON.parse(await readFile(resolvedPath, "utf8"));
  return {
    ...DEFAULT_CONFIG,
    ...raw,
    allowedDevices: Array.isArray(raw.allowedDevices)
      ? raw.allowedDevices.map((item) => normalizeAllowedDevice(item))
      : [],
  };
}

export function computeBackoffMs(failureCount) {
  if (failureCount >= 4) return 300_000;
  if (failureCount >= 2) return 120_000;
  return 30_000;
}

export function shouldManageHost({ ip, allowedDevices, knownDevices }) {
  const allowedByConfig = allowedDevices.find((device) => device.ipHint === ip);
  if (allowedByConfig) {
    return { allowed: true, label: allowedByConfig.label, source: "config" };
  }

  const knownEntry = Object.entries(knownDevices ?? {}).find(([, value]) => value?.lastKnownIp === ip);
  if (knownEntry) {
    return { allowed: true, label: knownEntry[0], source: "known" };
  }

  return { allowed: false, label: "", source: "unknown" };
}
  • Step 4: 再跑测试确认第一批逻辑通过

Run: node --test tests/adb-wireless-keeper.test.mjs

Expected: PASS3 个测试全部通过。

  • Step 5: 提交这一批纯逻辑与测试
git add tests/adb-wireless-keeper.test.mjs scripts/adb-wireless-keeper.mjs
git commit -m "feat: add adb wireless keeper core helpers"

Task 2: 实现一次扫描与自动重连编排

Files:

  • Modify: tests/adb-wireless-keeper.test.mjs

  • Modify: scripts/adb-wireless-keeper.mjs

  • Step 1: 先写失败测试,覆盖“已授权设备自动重连”和“未知设备跳过”

import {
  runKeeperOnce,
} from "../scripts/adb-wireless-keeper.mjs";

test("runKeeperOnce reconnects a known offline device discovered on the subnet", async () => {
  const events = [];
  const writes = [];
  const now = new Date("2026-04-05T12:00:00.000Z");
  const config = {
    subnets: ["192.168.31.0/24"],
    scanIntervalSeconds: 30,
    connectTimeoutMs: 4000,
    allowedDevices: [{ label: "boss-oppo-main", serialHint: "SER-001", ipHint: "192.168.31.18" }],
  };

  const summary = await runKeeperOnce(config, {
    now: () => now,
    loadState: async () => ({ knownDevices: {} }),
    saveState: async (state) => writes.push(state),
    listConnectedDevices: async () => [],
    scanSubnetHosts: async () => [{ ip: "192.168.31.18", port: 5555 }],
    adbConnect: async (ip) => `connected to ${ip}:5555`,
    appendLog: async (event) => events.push(event),
  });

  assert.equal(summary.connectAttempts, 1);
  assert.equal(summary.connectSuccesses, 1);
  assert.equal(events.some((item) => item.event === "connect_success"), true);
  assert.equal(writes.length > 0, true);
});

test("runKeeperOnce skips unknown hosts and respects active backoff", async () => {
  const events = [];
  const now = new Date("2026-04-05T12:00:00.000Z");
  const config = {
    subnets: ["192.168.31.0/24"],
    scanIntervalSeconds: 30,
    connectTimeoutMs: 4000,
    allowedDevices: [{ label: "boss-oppo-main", serialHint: "SER-001", ipHint: "192.168.31.18" }],
  };

  const summary = await runKeeperOnce(config, {
    now: () => now,
    loadState: async () => ({
      knownDevices: {
        "boss-oppo-main": {
          serial: "SER-001",
          lastKnownIp: "192.168.31.18",
          failureCount: 2,
          backoffUntil: "2026-04-05T12:01:00.000Z",
        },
      },
    }),
    saveState: async () => {},
    listConnectedDevices: async () => [],
    scanSubnetHosts: async () => [
      { ip: "192.168.31.18", port: 5555 },
      { ip: "192.168.31.77", port: 5555 },
    ],
    adbConnect: async () => {
      throw new Error("should not connect during backoff");
    },
    appendLog: async (event) => events.push(event),
  });

  assert.equal(summary.connectAttempts, 0);
  assert.equal(events.some((item) => item.event === "device_skipped_unknown"), true);
  assert.equal(events.some((item) => item.event === "backoff_applied"), true);
});
  • Step 2: 跑测试确认它们按预期失败

Run: node --test tests/adb-wireless-keeper.test.mjs

Expected: FAIL提示 runKeeperOnce is not a function 或摘要字段缺失。

  • Step 3: 给核心脚本补一次扫描/重连的最小编排实现
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";

function toIso(value) {
  return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
}

export async function loadState(statePath, fallback = { knownDevices: {} }) {
  try {
    return JSON.parse(await readFile(resolve(statePath), "utf8"));
  } catch {
    return fallback;
  }
}

export async function saveState(statePath, state) {
  const resolved = resolve(statePath);
  await mkdir(dirname(resolved), { recursive: true });
  await writeFile(resolved, `${JSON.stringify(state, null, 2)}\n`, "utf8");
}

export async function appendLog(logPath, payload) {
  const resolved = resolve(logPath);
  await mkdir(dirname(resolved), { recursive: true });
  const line = `${JSON.stringify(payload)}\n`;
  await writeFile(resolved, line, { encoding: "utf8", flag: "a" });
}

function isInBackoff(deviceState, now) {
  return Boolean(deviceState?.backoffUntil) && new Date(deviceState.backoffUntil).getTime() > now.getTime();
}

export async function runKeeperOnce(config, deps = {}) {
  const now = deps.now ? deps.now() : new Date();
  const statePath = config.stateFile ?? DEFAULT_CONFIG.stateFile;
  const logPath = config.logFile ?? DEFAULT_CONFIG.logFile;
  const state = deps.loadState
    ? await deps.loadState()
    : await loadState(statePath);
  const connected = deps.listConnectedDevices ? await deps.listConnectedDevices() : [];
  const onlineIps = new Set(connected.map((item) => item.ip).filter(Boolean));
  const subnets = Array.isArray(config.subnets) && config.subnets.length > 0 ? config.subnets : [];

  let connectAttempts = 0;
  let connectSuccesses = 0;

  for (const subnet of subnets) {
    const hosts = deps.scanSubnetHosts ? await deps.scanSubnetHosts(subnet) : [];
    for (const host of hosts) {
      const match = shouldManageHost({
        ip: host.ip,
        allowedDevices: config.allowedDevices,
        knownDevices: state.knownDevices ?? {},
      });
      if (!match.allowed) {
        await (deps.appendLog ? deps.appendLog({ ts: toIso(now), event: "device_skipped_unknown", ip: host.ip, subnet }) : appendLog(logPath, { ts: toIso(now), event: "device_skipped_unknown", ip: host.ip, subnet }));
        continue;
      }

      const deviceState = state.knownDevices?.[match.label] ?? {};
      if (onlineIps.has(host.ip)) {
        await (deps.appendLog ? deps.appendLog({ ts: toIso(now), event: "device_already_online", ip: host.ip, subnet, label: match.label }) : appendLog(logPath, { ts: toIso(now), event: "device_already_online", ip: host.ip, subnet, label: match.label }));
        continue;
      }
      if (isInBackoff(deviceState, now)) {
        await (deps.appendLog ? deps.appendLog({ ts: toIso(now), event: "backoff_applied", ip: host.ip, subnet, label: match.label }) : appendLog(logPath, { ts: toIso(now), event: "backoff_applied", ip: host.ip, subnet, label: match.label }));
        continue;
      }

      connectAttempts += 1;
      try {
        await (deps.adbConnect ? deps.adbConnect(host.ip) : Promise.resolve());
        connectSuccesses += 1;
        state.knownDevices = state.knownDevices ?? {};
        state.knownDevices[match.label] = {
          ...(state.knownDevices[match.label] ?? {}),
          lastKnownIp: host.ip,
          lastConnectedAt: toIso(now),
          failureCount: 0,
          backoffUntil: "",
        };
        await (deps.appendLog ? deps.appendLog({ ts: toIso(now), event: "connect_success", ip: host.ip, subnet, label: match.label }) : appendLog(logPath, { ts: toIso(now), event: "connect_success", ip: host.ip, subnet, label: match.label }));
      } catch (error) {
        const failureCount = Number(deviceState.failureCount ?? 0) + 1;
        state.knownDevices = state.knownDevices ?? {};
        state.knownDevices[match.label] = {
          ...(state.knownDevices[match.label] ?? {}),
          lastKnownIp: host.ip,
          lastFailureAt: toIso(now),
          failureCount,
          backoffUntil: toIso(now.getTime() + computeBackoffMs(failureCount)),
        };
        await (deps.appendLog ? deps.appendLog({ ts: toIso(now), event: "connect_failed", ip: host.ip, subnet, label: match.label, reason: error instanceof Error ? error.message : String(error) }) : appendLog(logPath, { ts: toIso(now), event: "connect_failed", ip: host.ip, subnet, label: match.label, reason: error instanceof Error ? error.message : String(error) }));
      }
    }
  }

  state.lastScanAt = toIso(now);
  state.nextScanAt = toIso(now.getTime() + config.scanIntervalSeconds * 1000);
  await (deps.saveState ? deps.saveState(state) : saveState(statePath, state));

  return {
    subnetCount: subnets.length,
    connectAttempts,
    connectSuccesses,
  };
}
  • Step 4: 再跑测试确认编排行为通过

Run: node --test tests/adb-wireless-keeper.test.mjs

Expected: PASS新增的编排测试通过且原有纯逻辑测试不回退。

  • Step 5: 提交扫描与重连编排
git add tests/adb-wireless-keeper.test.mjs scripts/adb-wireless-keeper.mjs
git commit -m "feat: add adb wireless keeper reconnect loop"

Task 3: 补 CLI 模式、壳脚本与 launchd 安装链

Files:

  • Modify: tests/adb-wireless-keeper.test.mjs

  • Modify: scripts/adb-wireless-keeper.mjs

  • Create: scripts/adb-wireless-keeper-once.sh

  • Create: scripts/adb-wireless-keeper-status.sh

  • Create: scripts/install-adb-wireless-keeper.sh

  • Create: scripts/uninstall-adb-wireless-keeper.sh

  • Create: deployment/launchd/com.hyzq.boss.adb-wireless-keeper.plist

  • Create: config/adb-wireless-keeper.json

  • Step 1: 先写失败测试,覆盖状态摘要和 launchd 模板渲染

import {
  formatStatusSummary,
  renderLaunchAgentPlist,
} from "../scripts/adb-wireless-keeper.mjs";

test("formatStatusSummary shows subnet, next scan and recent failures", () => {
  const summary = formatStatusSummary({
    lastSubnet: "192.168.31.0/24",
    nextScanAt: "2026-04-05T12:00:30.000Z",
    knownDevices: {
      "boss-oppo-main": {
        lastKnownIp: "192.168.31.18",
        failureCount: 1,
        backoffUntil: "2026-04-05T12:01:00.000Z",
      },
    },
  });

  assert.match(summary, /192\\.168\\.31\\.0\\/24/);
  assert.match(summary, /boss-oppo-main/);
  assert.match(summary, /2026-04-05T12:00:30\\.000Z/);
});

test("renderLaunchAgentPlist injects repo root and config path", () => {
  const rendered = renderLaunchAgentPlist({
    template: `
<plist version="1.0">
  <dict>
    <key>ProgramArguments</key>
    <array>
      <string>__BOSS_ROOT__</string>
      <string>__CONFIG_PATH__</string>
    </array>
  </dict>
</plist>`,
    rootDir: "/Users/kris/code/boss",
    configPath: "/Users/kris/code/boss/config/adb-wireless-keeper.json",
  });

  assert.match(rendered, /\\/Users\\/kris\\/code\\/boss/);
  assert.match(rendered, /adb-wireless-keeper\\.json/);
});
  • Step 2: 跑测试确认先失败

Run: node --test tests/adb-wireless-keeper.test.mjs

Expected: FAIL提示缺失 formatStatusSummaryrenderLaunchAgentPlist

  • Step 3: 在 Node 脚本里补 CLI、状态摘要和 plist 渲染
export function formatStatusSummary(state) {
  const lines = [
    `lastSubnet=${state.lastSubnet ?? ""}`,
    `lastScanAt=${state.lastScanAt ?? ""}`,
    `nextScanAt=${state.nextScanAt ?? ""}`,
  ];
  for (const [label, device] of Object.entries(state.knownDevices ?? {})) {
    lines.push(
      `${label} ip=${device.lastKnownIp ?? ""} failures=${device.failureCount ?? 0} backoffUntil=${device.backoffUntil ?? ""}`,
    );
  }
  return lines.join("\n");
}

export function renderLaunchAgentPlist({ template, rootDir, configPath }) {
  return template
    .replaceAll("__BOSS_ROOT__", rootDir)
    .replaceAll("__CONFIG_PATH__", configPath);
}

async function main(argv = process.argv.slice(2)) {
  const mode = argv[0] ?? "once";
  const configPath = argv[1] ?? "config/adb-wireless-keeper.json";
  const config = await loadKeeperConfig(configPath);
  if (mode === "status") {
    const state = await loadState(config.stateFile);
    process.stdout.write(`${formatStatusSummary(state)}\n`);
    return;
  }
  if (mode === "once") {
    const summary = await runKeeperOnce(config);
    process.stdout.write(`${JSON.stringify(summary)}\n`);
    return;
  }
  throw new Error(`UNKNOWN_MODE: ${mode}`);
}

if (import.meta.url === `file://${process.argv[1]}`) {
  main().catch((error) => {
    process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
    process.exit(1);
  });
}
  • Step 4: 新增配置文件、launchd 模板和壳脚本

config/adb-wireless-keeper.json

{
  "subnets": [],
  "scanIntervalSeconds": 30,
  "connectTimeoutMs": 4000,
  "adbPath": "",
  "logFile": "data/adb-wireless-keeper.log.jsonl",
  "stateFile": "data/adb-wireless-keeper-state.json",
  "allowedDevices": []
}

deployment/launchd/com.hyzq.boss.adb-wireless-keeper.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>Label</key>
    <string>com.hyzq.boss.adb-wireless-keeper</string>
    <key>ProgramArguments</key>
    <array>
      <string>/bin/zsh</string>
      <string>-lc</string>
      <string>cd __BOSS_ROOT__ &amp;&amp; node ./scripts/adb-wireless-keeper.mjs once __CONFIG_PATH__</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <true/>
    <key>StandardOutPath</key>
    <string>/tmp/boss-adb-wireless-keeper.out</string>
    <key>StandardErrorPath</key>
    <string>/tmp/boss-adb-wireless-keeper.err</string>
  </dict>
</plist>

scripts/adb-wireless-keeper-once.sh

#!/bin/zsh
set -euo pipefail

ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
CONFIG_PATH="${1:-$ROOT_DIR/config/adb-wireless-keeper.json}"

cd "$ROOT_DIR"
exec node ./scripts/adb-wireless-keeper.mjs once "$CONFIG_PATH"

scripts/adb-wireless-keeper-status.sh

#!/bin/zsh
set -euo pipefail

ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
CONFIG_PATH="${1:-$ROOT_DIR/config/adb-wireless-keeper.json}"

cd "$ROOT_DIR"
exec node ./scripts/adb-wireless-keeper.mjs status "$CONFIG_PATH"

scripts/install-adb-wireless-keeper.sh

#!/bin/zsh
set -euo pipefail

ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
PLIST_SOURCE="$ROOT_DIR/deployment/launchd/com.hyzq.boss.adb-wireless-keeper.plist"
PLIST_TARGET="$HOME/Library/LaunchAgents/com.hyzq.boss.adb-wireless-keeper.plist"
CONFIG_PATH="${1:-$ROOT_DIR/config/adb-wireless-keeper.json}"

command -v adb >/dev/null
mkdir -p "$HOME/Library/LaunchAgents"
python3 - <<'PY' "$PLIST_SOURCE" "$PLIST_TARGET" "$ROOT_DIR" "$CONFIG_PATH"
from pathlib import Path
import sys

source = Path(sys.argv[1]).read_text()
target = Path(sys.argv[2])
target.write_text(
    source.replace("__BOSS_ROOT__", sys.argv[3]).replace("__CONFIG_PATH__", sys.argv[4])
)
PY
plutil -lint "$PLIST_TARGET" >/dev/null
launchctl unload "$PLIST_TARGET" >/dev/null 2>&1 || true
launchctl load "$PLIST_TARGET"
echo "Loaded $PLIST_TARGET"

scripts/uninstall-adb-wireless-keeper.sh

#!/bin/zsh
set -euo pipefail

PLIST_TARGET="$HOME/Library/LaunchAgents/com.hyzq.boss.adb-wireless-keeper.plist"
launchctl unload "$PLIST_TARGET" >/dev/null 2>&1 || true
rm -f "$PLIST_TARGET"
echo "Removed $PLIST_TARGET"
  • Step 5: 跑测试和模板校验

Run:

node --test tests/adb-wireless-keeper.test.mjs
plutil -lint deployment/launchd/com.hyzq.boss.adb-wireless-keeper.plist
zsh ./scripts/adb-wireless-keeper-status.sh config/adb-wireless-keeper.json

Expected:

  • Node tests PASS

  • plutil -lint 输出 OK

  • status 能打印 lastSubnet= / nextScanAt= 这类摘要字段,即使状态文件还不存在也不报错

  • Step 6: 提交 CLI 与守护安装链

git add \
  tests/adb-wireless-keeper.test.mjs \
  scripts/adb-wireless-keeper.mjs \
  scripts/adb-wireless-keeper-once.sh \
  scripts/adb-wireless-keeper-status.sh \
  scripts/install-adb-wireless-keeper.sh \
  scripts/uninstall-adb-wireless-keeper.sh \
  deployment/launchd/com.hyzq.boss.adb-wireless-keeper.plist \
  config/adb-wireless-keeper.json
git commit -m "feat: add adb wireless keeper launchd tooling"

Task 4: 文档收口与整体验证

Files:

  • Modify: README.md

  • Modify: docs/architecture/current_runtime_and_deploy_status_cn.md

  • Verify: tests/adb-wireless-keeper.test.mjs

  • Verify: scripts/adb-wireless-keeper-status.sh

  • Step 1: 把实际脚本名称和使用方式写进 README

README.md 的 Android 段落补实际脚本入口和限制,文本至少包含:

- Android ADB 无线保活脚本:`scripts/adb-wireless-keeper.mjs`
- 单次执行:`zsh ./scripts/adb-wireless-keeper-once.sh`
- 查看状态:`zsh ./scripts/adb-wireless-keeper-status.sh`
- 安装常驻守护:`zsh ./scripts/install-adb-wireless-keeper.sh`
- 卸载守护:`zsh ./scripts/uninstall-adb-wireless-keeper.sh`
- 该守护只能自动恢复“仍允许 TCP/IP ADB 的设备连接”,不能替 Android 永久保持无线调试开启
  • Step 2: 把当前运行时文档改成和真实交付物一致

docs/architecture/current_runtime_and_deploy_status_cn.md 补一段实际运行说明,文本至少包含:

- 本机当前提供 `adb-wireless-keeper` 守护,用于多 Android 设备在同网段内自动扫描 `5555` 并对白名单/历史已授权设备执行 `adb connect`
- 守护配置文件位于 `config/adb-wireless-keeper.json`
- 运行态写入 `data/adb-wireless-keeper-state.json``data/adb-wireless-keeper.log.jsonl`
- 安装方式走 `launchd`,由 `scripts/install-adb-wireless-keeper.sh` 管理
  • Step 3: 跑最终验证命令

Run:

node --test tests/adb-wireless-keeper.test.mjs
npm run lint
npm run build
zsh ./scripts/adb-wireless-keeper-status.sh config/adb-wireless-keeper.json

Expected:

  • node --test PASS

  • npm run lint 0 errors

  • npm run build success

  • status 命令可运行并输出状态摘要

  • Step 4: 做一轮人工无线重连验收

Run:

adb devices -l
adb disconnect 192.168.31.18:5555
zsh ./scripts/adb-wireless-keeper-once.sh config/adb-wireless-keeper.json
adb devices -l

Expected:

  • 目标设备在断开后被重新连接回 192.168.31.18:5555

  • 如果设备当前已不允许 TCP/IP ADB脚本写失败日志并进入退避而不是无限刷 adb connect

  • Step 5: 提交文档和最终收口

git add \
  README.md \
  docs/architecture/current_runtime_and_deploy_status_cn.md
git commit -m "docs: wire adb wireless keeper into runtime docs"

Plan Self-Review

Spec coverage

  • 多设备Task 2 的 runKeeperOnceknownDevices/白名单逻辑覆盖。
  • 自动扫描同网段Task 2 负责一次扫描编排Task 4 做人工验收。
  • 后台常驻Task 3 补 launchd 模板与安装脚本。
  • 状态/日志/退避Task 2 和 Task 3 覆盖。
  • 文档入口Task 4 覆盖。

Placeholder scan

  • 本计划没有留下未决占位标记。
  • 每个代码步骤都给了实际文件路径、命令和最小代码。

Type consistency

  • 核心导出统一使用:
    • loadKeeperConfig
    • computeBackoffMs
    • shouldManageHost
    • loadState
    • saveState
    • appendLog
    • runKeeperOnce
    • formatStatusSummary
    • renderLaunchAgentPlist