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

788 lines
25 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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:test``node:assert/strict``zsh`、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: 写第一个失败测试,覆盖配置默认值、白名单匹配和退避规则**
```js
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: FAIL`ERR_MODULE_NOT_FOUND` 或缺失导出,例如 `loadKeeperConfig is not exported`
- [ ] **Step 3: 在核心脚本里补最小实现,让这些纯逻辑先可测**
```js
#!/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: 提交这一批纯逻辑与测试**
```bash
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: 先写失败测试,覆盖“已授权设备自动重连”和“未知设备跳过”**
```js
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: 给核心脚本补一次扫描/重连的最小编排实现**
```js
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: 提交扫描与重连编排**
```bash
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 模板渲染**
```js
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提示缺失 `formatStatusSummary``renderLaunchAgentPlist`
- [ ] **Step 3: 在 Node 脚本里补 CLI、状态摘要和 plist 渲染**
```js
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`
```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
<?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`
```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`
```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`
```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`
```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:
```bash
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 与守护安装链**
```bash
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 段落补实际脚本入口和限制,文本至少包含:
```md
- 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` 补一段实际运行说明,文本至少包含:
```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:
```bash
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:
```bash
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: 提交文档和最终收口**
```bash
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 的 `runKeeperOnce``knownDevices`/白名单逻辑覆盖。
- 自动扫描同网段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`