claude agents 与 /bg —— Agent View 实现剖析
源码:
2.1.139-unbundled/extracted/src/entrypoints/cli.js(构建版本208bf4b4,2026-05-11)。 官方博客参考:Agent view in Claude Code。
"Agent view" 是一个特性、三个入口、一份磁盘上的 job 模型:
| 入口 | 行为 |
|---|---|
claude agents(CLI 子命令) | 当 daemon 已启用时,走 fast path,在 Commander 解析 argv 之前就拦截并挂载 FleetView TUI。当 daemon 被禁用时,回退到 Commander 注册的 agentsHandler,那只是一个 已配置子代理(subagent)定义 的纯文本列表。 |
claude --bg [task] / --background | 全新会话直接由 on-demand daemon 后台拉起,父进程打印一段提示后退出。 |
/bg(斜杠命令,/background 的别名) | 把当前的 REPL 会话 fork 到后台,前台进程退出。在交互式 REPL 中,空输入下按左方向键 也会触发同一条 fork 路径,并额外在进程内重新挂载 FleetView。 |
三条路径最终都收敛到 ~/.claude/bg-jobs/<short>/state.json 文件 + Unix 域套接字 daemon(即 "on-demand daemon")。FleetView 本质上是对那个目录的轮询读取器,外加一个连接 daemon 的 RPC 客户端。
下面使用的命名约定:把被混淆过的符号重命名为有语义的名字,并在首次出现时附 /* <原始名> */。没有重命名的(oqH、pT、Q7、B9、Q、yH、xH 等)是宿主进程共享的辅助函数(状态文件 IO、遥测、优雅退出)——你在移植到别的工程时要逐个替换。
1. 数据模型
// state.json —— 每个后台 job 一份,存放在 ~/.claude/bg-jobs/<short>/
interface JobState {
proto: number; // I1,schema 版本号
short: string; // 8 位 16 进制前缀,CLI / FleetView 中用于寻址
sessionId: string; // 完整 uuid,对应 transcript 文件名
createdAt: string; // ISO
updatedAt: string; // ISO,每次写入都 bump
firstTerminalAt?: string; // 首次进入终止态的时间
// 身份 / 显示
template: { name: string; description: string; initialPrompt?: string };
agent?: string; // 如果用了 --agent,这里是 subagent 类型
intent: string; // 首条用户消息,≤200 字符
name?: string; // 显式 -n/--name;否则自动派生
nameSource?: "user" | "auto";
detail?: string; // 最近一次 assistant 输出节选,≤120 字符
color?: string;
// 活动状态
state: "running" | "stopped" | "errored" | "done" | ...;
tempo?: "active" | "idle" | "blocked";
needs?: string; // agent 正在等待的东西
block?: { questions?: Question[] }; // 当 tempo === "blocked" 时
suggestedReply?: string; // PeekPane 中按 Tab 可补全的预填回复
output?: Record<string, string>; // 行内显示的若干 headline 行
inFlight?: unknown;
resumeSessionId?: string;
// 排序 / 置顶
sortOrder?: number;
stateSortOrder?: number;
pinned?: boolean;
// 位置
cwd: string;
originCwd?: string;
worktreePath?: string;
worktreeBranch?: string;
worktreeHookBased?: boolean;
// 链接
children?: Array<<ruby>id: string; kind: "frame"<rt>"pr" | ...; href: string</rt></ruby>>;
source?: "shell" | "slash" | "fleet" | "spare" | "respawn" | "left_arrow";
// 其他簿记
respawnFlags?: string[];
env?: Record<string, string>;
}
interface Question { text: string; options: string[] }
// FleetView 内存中从一份 JobState 构造出来的行结构
interface JobRow <ruby>id: string; // === short
state: JobState;
activity: "flowing"<rt>"slowing" | "stuck" | "success" | "failure" | "stopped";
tempo?: "active" | "idle" | "blocked";
template: string; // state.template.name
children?: JobState["children"];
origin?: "C8" | ...; // 当前 cwd 还是其他 cwd
group?: string;</rt></ruby>
// FleetView 通过此 action 回报给 mountFleetView
type FleetAction =
| { type: "done" }
| <ruby>type: "open"; job: JobRow; query?: string; collapsed: string[];
groupMode: "state"<rt>"directory"; jobs: JobRow[];
loopKicks: Map<string,number>; statuses: Map<string,number>;
statusesTs: number; prStatuses: Map<string,unknown>;
respawnResult?: RespawnResult; freshDispatch?: boolean</rt></ruby>;
agentsHandler 回退处理器看到的"agents"是另一种东西——subagent 定义 列表(由 uk() 返回的 activeAgents),按来源(user / project / local / plugin / built-in)分组。这跟 FleetView 中的 "session 行" 没有关系。
2. 常量与文案
// 总开关
const ENV_DISABLE = "CLAUDE_CODE_DISABLE_AGENT_VIEW";
const SETTING_DISABLE = "disableAgentView"; // 托管配置
const SETTING_DEFAULT_TO_FLEET = "defaultToAgentsView";// 用户配置(462620 行)
// 从 fork 中启动 `claude agents` 时预选 job
const ENV_PRESELECT = "CLAUDE_AGENTS_SELECT";
// 自动重启失焦的 fleet view(心跳)
const ENV_AUTORELAUNCH = "CLAUDE_AGENTS_AUTO_RELAUNCHED_AT";
const AUTO_RELAUNCH_UNFOCUSED_MS = 3_600_000; // 1 小时
const AUTO_RELAUNCH_MIN_INTERVAL_MS = 21_600_000; // 6 小时
// daemon 协议版本
declare const I1: number;
// `claude --bg < piped` 时 stdin 上限
const zB8 = 16 * 1024; // 字节
// 每次 bg dispatch 之后打印的提示块
function formatPostBackgroundHints(short: string, idle?: string) {
return [
`backgrounded · ${cyan(short)}${idle ? dim(" " + idle) : ""}`,
dim(" claude agents".padEnd(28) + "list sessions"),
dim(` claude attach ${short}`.padEnd(28) + "open in this terminal"),
dim(` claude logs ${short}`.padEnd(28) + "show recent output"),
dim(` claude stop ${short}`.padEnd(28) + "stop this session"),
].join("\n");
}
后台化相关的几条关键文案:
"Backgrounding…"—— 后台拉起过程中的 spinner。"<N> tasks running — the forked session won't carry live processes."—— 确认框副标题。"Background anyway (tasks will be abandoned)"/"Stay"—— 确认 / 取消按钮。"Cannot background — session persistence is disabled, so the forked job would have nothing to resume."
3. Host-Framework 契约
下表是 agent view 模块依赖的、cli.js 中由其他模块提供的辅助函数。移植到其他工程时,需要把每一项替换成你那边对应的实现。
| 符号 | 签名 | 行号 | 用途 |
|---|---|---|---|
oqH | (seed) => JobState | 542886 处使用 | 用默认值构造一份新的 JobState。 |
pT | (jobDir, partial) => Promise<void> | 542884 处使用 | 原子地 merge 写 state.json。 |
Q7 | (jobDir) => Promise<JobState|null> | 542964 | 读 state.json。 |
W4 | (short) => string | 542879 | ~/.claude/bg-jobs/<short>/。 |
rW | () => string | 543233 | jobs 根目录;前缀查找用 readdir 扫这里。 |
k_ | () => string | 542814 | 当前进程的 session id(在 fork 中被设置)。 |
L7 | () => boolean | 543959 | CLAUDE_JOB_DIR 是否存在 → 即"我是否已经是后台 fork"。 |
getCurrentJobDir / Xv3 | () => string | undefined | 544011 | 返回 process.env.CLAUDE_JOB_DIR。 |
V$ | (req, opts?) => Promise<DaemonResp> | 543031 | 与 daemon 的 Unix socket RPC(op: "list" | "dispatch")。 |
mW_ | () => Promise<{ok,reason?}> | 543277 | daemon 健康检查(claude attach 用)。 |
MQ | (short) => Promise<{outcome,msg?}> | 543286 | "把我接到这个 job 上去" RPC。 |
PGK | (short, {alreadyInAlt}) => Promise<AttachResult> | 606723 | attach 期间负责本地 PTY ↔ daemon 中 job 的双向桥接。 |
fW6 | (jobId, {force?,knownState?,knownAlive?}) => Promise<RespawnResult> | 606700 | 若 worker 已死则唤醒(Re·spawn)。 |
B9 | (exitCode, reason, opts?) => Promise<never> | 543885 | 前台进程的优雅退出。 |
aH7.setupGracefulShutdown | — | 651511 | 把 SIGINT/SIGTERM 接入 B9。 |
cH.createRoot / Mu | (opts) => Promise<InkRoot> | 651518 | 创建一个共用 stdout 的 Ink 根节点。 |
v3_ | ({exitOnCtrlC}) => Promise<InkRoot> | 606748 | PTY 移交之后重建 Ink 根。 |
Q5.get(process.stdout)?.handoffAltScreen | — | 606681 | 把 alt-screen 控制权交给 PTY 用于 attach。 |
Q5.get(process.stdout)?.unmount | — | 606926 | 在 in-process 重新挂载 fleet view 之前先卸载 Ink。 |
Q("tengu_*") / yH / xH / j6 | 遥测 sink | 各处 | tengu_bg_dispatch、tengu_background_fork、fleet_view_*。 |
IK("allow_remote_sessions") | 策略检查 | 644468 | 组织级开关。 |
Xc = isAgentsFleetEnabled() | — | 149497 | 总闸门:合并托管配置、env、fast-path policy。 |
fleetGateRejected(verb) | (string) => Promise<never> | 651424 | 打印拒绝理由并退出。 |
EAH("claude agents") | — | 604574 | 设置终端窗口标题。 |
vh() / zAH() | 字符串 | 606686/606747 | 进入 / 退出 alt-screen 的 ANSI 序列。 |
b_() / H6(updater) | 配置仓库 | 604558/605650 | 持久化 fleetViewGroupMode 等。 |
B26(cwd, {q,collapsed}) / ebK / qxK | bg-view UI 状态 | 604647 | 按 cwd 记忆"上次的搜索与折叠状态"。 |
s6() | () => {columns, rows} | 604583 | 终端尺寸。 |
pA() | () => boolean | 604578 | 终端当前是否获焦。 |
un_() | alt-screen flag | 606680 | 是否拥有 alt-screen。 |
lT() | () => WorktreeInfo | null | 606956 | 当前 worktree 信息。 |
g$() / qp() | name 查找 | 543840 | 用户设定 / 自动派生的 job 名。 |
WE(msg) / oI(msg) | transcript 辅助 | 543828 | 抽取 assistant/user 消息正文。 |
viH(text) | 元信息文本判定 | 543834 | 用来在挑选 intent 时跳过斜杠命令回显。 |
LD(state) | (JobState) => boolean | 544019 | job 处于终止态。 |
npH(state) | activity 分类器 | 602954 | "success"/"failure"/"stopped" 或 undefined。 |
daemon 协议(V$)至少接受这些请求:
type DaemonReq =
| { proto: number; op: "list" }
| { proto: number; op: "dispatch"; d: JobDispatch; timeoutMs?: number };
type JobDispatch = { // 543001-543027
proto: number; short: string; sessionId: string; createdAt: number;
source: "shell"|"slash"|"fleet"|"spare"|"respawn"|"left_arrow";
cwd: string;
launch:
| { mode: "prompt"; args: string[] }
| { mode: "resume"; sessionId: string; fork: boolean; flagArgs: string[] };
respawnFlags: string[]; env: Record<string,string>; reattachEnv?: unknown;
worktree?: { path: string; ownershipToken: string };
isolation: "worktree" | "none";
agent?: string; routine?: string;
seed: { intent: string; name?: string };
cols?: number; rows?: number;
};
4. 注册
4a. /bg 斜杠命令(543995 行)
const backgroundCommand = {
type: "local-jsx",
name: "background",
aliases: ["bg"],
description: "Continue this session in the background and free the terminal",
immediate: (raw) => !raw.trim(), // 无额外 prompt 时跳过确认
isEnabled: () => true,
load: () => Promise.resolve().then(() => (_loadBgImpl(), _bgImplExports)),
};
load() 解析后得到一个导出 { default: handleBgSlashCommand } 的模块,其中:
// 543958 行 —— Yv3
const handleBgSlashCommand = async (
onDone: (err?: string) => void,
ctx: { messages: Message[] },
args: string
) => {
if (L7() /* 自己就是 bg session */) {
Q("tengu_background_already_bg", {});
onDone(); q4H();
return null;
}
if (YDH() /* 会话持久化被禁用 */)
return (onDone(
"Cannot background — session persistence is disabled, so the forked job would have nothing to resume."
), null);
const extraPrompt = (args ?? "").trim();
const seed = extractSeedFromTranscript /*Bf6*/ (ctx.messages, extraPrompt);
if (seed === null)
return (onDone("Nothing to background yet — send a message first."), null);
return React.createElement(BackgroundConfirmDialog /*Jv3*/, {
onDone, prompt: extraPrompt, seed, messages: ctx.messages,
});
};
4b. /stop 斜杠命令(只在 bg job 中生效,544066 行)
const stopJsx = {
type: "local-jsx", name: "stop", immediate: true, isEnabled: L7,
description: "Stop this background session; transcript and worktree are kept",
requires: { ink: true },
load: () => ({ call: (done) => (done(), markSelfStoppedAndExit("stop_command"), null) }),
};
const stopNonInteractive = {
type: "local", name: "stop", supportsNonInteractive: true,
isEnabled: L7,
load: () => ({ call: () => (markSelfStoppedAndExit("bridge"), { type: "skip" }) }),
};
4c. claude agents Commander 子命令(647846 行,仅作 fallback)
H.command("agents")
.description("Manage background and configured agents")
.option("--setting-sources <sources>",
"Comma-separated list of setting sources to load (user, project, local).")
.action(async () => {
const [{ agentsHandler }, { createSubcommandRoot }] = await Promise.all([
import("./agents-handler.js"),
import("./subcommand-root.js"),
]);
await agentsHandler(await createSubcommandRoot());
process.exit(0);
});
4d. 总开关配置项(53308 行)
disableAgentView: {
type: "boolean",
description:
"Disable agent view (`claude agents`, `--bg`, /background, the on-demand daemon). " +
"Typically set in managed settings. Equivalent to CLAUDE_CODE_DISABLE_AGENT_VIEW=1.",
};
5. 入口 / 分发 —— mainCliEntry()(cli 的 main)
mainCliEntry(混淆名 je3)跑在 Commander 之前,让 claude agents 不必走 main() 的重型导入,直接进入 FleetView。
// 651265
async function mainCliEntry /*je3*/ () {
const argv = process.argv.slice(2);
// … --version / --daemon-worker / --bg-pty-host / --bg-spare / bridge / daemon 等早返回路径
// ───────── bg 动词 fast path(651387 行) ─────────
if (
argv[0] === "logs" || argv[0] === "attach" || argv[0] === "stop" ||
argv[0] === "kill" || argv[0] === "respawn" || argv[0] === "rm" ||
argv.includes("--bg") || argv.includes("--background")
) {
if (!isAgentsFleetEnabled()) return fleetGateRejected(verb);
const bg = await import("./bg-cli.js"); // QGK
switch (argv[0]) {
case "logs": return bg.logsHandler(argv[1]);
case "attach": return bg.attachHandler(argv[1]);
case "stop": case "kill": return bg.stopHandler(argv[1]);
case "rm": return bg.rmHandler(argv[1]);
case "respawn": return bg.respawnHandler(argv[1]);
default: {
// --bg / --background 仍在 argv 中,第一个 positional 才是 prompt
Q("tengu_background", { via_flag: true, via: "flag" });
await bg.handleBgFlag(argv);
return process.exit(process.exitCode ?? 0);
}
}
}
// ───────── fleet-view fast path(651485 行) ─────────
const wantsAgents = argv[0] === "agents" && argsAreOnlyDebugFlags(argv.slice(1));
const wantsDefault = argsAreOnlyDebugFlags(argv) && process.stdin.isTTY && process.stdout.isTTY;
if ((wantsAgents || wantsDefault)) {
enableConfigs();
const defaultsToAgents = getGlobalConfig().defaultToAgentsView === true;
if (wantsAgents || defaultsToAgents) {
const policyErr = await loadFastPathPolicy();
if (policyErr) exitWithError(policyErr);
await ensureFleetGateHydrated();
if (isAgentsFleetEnabled()) {
initSinks(); setupGracefulShutdown(); setIsInteractive(true);
Q("tengu_fleetview", { defaultToAgentsView: defaultsToAgents });
const root = await createRoot({ exitOnCtrlC: false });
consumeEarlyInput();
await mountFleetView(root);
await gracefulShutdown(0, "other", { suppressResumeHint: true });
return;
}
}
}
// …其余情况 → main()(即 Commander,包含 agents 子命令的 fallback)
}
function argsAreOnlyDebugFlags /*caK*/ (argv) {
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (["--debug","-d","--debug-to-stderr","-d2e"].includes(a)
|| a.startsWith("--debug=") || a.startsWith("--debug-file=")) continue;
if (a === "--debug-file" && i + 1 < argv.length) { i++; continue; }
return false;
}
return true;
}
两个关键结论:
claude agents是 Commander 子命令里唯一在 parse 之前就被劫持的,所以它真正的实现是mountFleetView,而不是.action()里那个 body —— 后者只在isAgentsFleetEnabled()返回 false 时才执行。- 无参数的
claude在defaultToAgentsView: true时也会直接进入 FleetView;否则正常落入main()走 REPL。
6. --bg 标志 → handleBgFlag → daemon dispatch
// 543089 行 —— sN3
async function handleBgFlag(argvWithoutFlag: string[]) {
// argvWithoutFlag 是去掉 --bg / --background 之后的 argv
const rest = argvWithoutFlag.filter((a) => !["--bg","--background"].includes(a));
const stdinTail = await readBgStdin(); // pGK;超过 16 KiB 会截断
const merged = stdinTail ? withStdinPositional(rest, stdinTail) : rest;
const res = await spawnBgSession(merged); // pHH → aN3
if (!res.ok) {
process.stderr.write(res.error + "\n");
process.exitCode = 1;
return;
}
process.stdout.write(formatPostBackgroundHints(res.short, res.idle ? "(idle — waiting for…)" : undefined) + "\n");
}
6a. spawnBgSession 核心 —— spawnBgSessionAndDispatch / aN3(542926 行)
下面是把它拆开后的伪代码:
async function spawnBgSessionAndDispatch /*aN3*/ (
argv, source, cwd, seed /* Bf6 的结果 */, reattachEnv, t /*ids*/
) {
const { sessionId, short, jobDir, freshDir } = t;
const dashDash = argv.indexOf("--");
const flagArgs = dashDash >= 0 ? argv.slice(0, dashDash) : argv;
const agentName = readFlag("--agent", flagArgs);
const sessionName = readFlag("--name", flagArgs) ?? readFlag("-n", flagArgs);
const resumeId = parseResumeTarget(flagArgs); // FGK
const promptArg = dashDash >= 0
? argv.slice(dashDash + 1).join(" ")
: positionalArgFor(argv, resumeId);
const isResume = flagArgs.some(f => f === "-c" || f === "--continue" ||
f === "-r" || f === "--resume" ||
f.startsWith("--resume=") || f.startsWith("-r="));
const isFork = flagArgs.includes("--fork-session");
const respawnFlags = respawnableFlags(flagArgs); // Ov3
const dispatchFlagArgs = dashDash >= 0 ? respawnFlags : flagsWithoutPositional(respawnFlags);
const resumingSelf = resumeId !== undefined && resumeId === sessionId;
const forkFlag = isResume && !isFork ? ["--fork-session"] : [];
const sessionArgs = resumingSelf ? [] : ["--session-id", sessionId, ...forkFlag];
if (source === "shell" && readFlag("--session-id", flagArgs))
warn("--bg manages the session id; ignoring --session-id (use --resume <id> instead)");
const subagent = agentName
? (await uk(cwd ?? process.cwd())).activeAgents.find(a => a.agentType === agentName)
: undefined;
if (agentName && !subagent && source === "shell")
warn(`no agent named '${agentName}' — spawning with default template`);
const intent = seed?.intent ?? promptArg ?? "";
const isIdle = !agentName && intent === "" && !seed?.detail;
// 预写 state.json(fleet / spare 来源跳过,它们通过 dispatch 携带 seed)
if (source !== "fleet" && source !== "spare") {
const prior = freshDir ? null : await readJobState(jobDir);
if (prior === null)
writeJobState(jobDir, oqH({
template: {
name: agentName ?? "bg",
description: subagent?.whenToUse ?? "",
initialPrompt: subagent?.initialPrompt,
},
respawnFlags: dispatchFlagArgs,
intent,
name: sessionName ?? seed?.name,
nameSource: sessionName ? "user" : seed?.nameSource,
detail: seed?.detail ?? (isIdle ? "(blocked)" : undefined),
tempo: isIdle ? "blocked" : undefined,
sessionId, cwd: cwd ?? process.cwd(),
worktreePath: seed?.worktree?.path,
worktreeBranch: seed?.worktree?.branch,
worktreeHookBased: seed?.worktree?.hookBased,
originCwd: seed?.worktree?.originCwd,
}));
else if (dispatchFlagArgs.length && !prior.respawnFlags.length)
writeJobState(jobDir, { ...prior, respawnFlags: dispatchFlagArgs });
}
// 投递给 daemon
const dispatch = {
proto: I1, short, sessionId, createdAt: Date.now(),
source: source === "repl" ? "slash" : source,
cwd: cwd ?? process.cwd(),
launch: isResume && resumeId
? <ruby>mode: "resume", sessionId: resumeId,
fork: !resumingSelf && (isFork<rt>| forkFlag.length > 0),
flagArgs: [...respawnFlags, ...(dashDash >= 0 ? argv.slice(dashDash) : [])]</rt></ruby>
: { mode: "prompt", args: [...sessionArgs, ...positionalsAfterFlags(argv)] },
respawnFlags, env: bgEnvWhitelist(), reattachEnv,
worktree: seed?.worktree && { path: seed.worktree.path, ownershipToken: sessionId },
isolation: subagent?.isolation === "worktree" && subagent.source !== "built-in" ? "worktree" : "none",
agent: agentName, routine: undefined,
seed: { intent, name: sessionName ?? seed?.name },
cols: process.stdout.columns, rows: process.stdout.rows,
};
const r = await daemonRpcDispatch(dispatch); // AB8 → V$
if (r.ok) return { ok: true, short, sessionId, idle: isIdle };
// 兜底分支:
// - ack-timeout / enoconn 但 op:"list" 显示 worker 还在 → 判定成功
// - ack-timeout 且 list 中没出现 → 重投一次
// - short-alive → "session is already running" 的友好错误
// - stale-short → "前一会话还在关闭,稍后重试"
// - daemon-unreachable → 退出并提示运行 status
//(参见 543030-543087)
}
6b. stdin 作为 positional 合并 —— withStdinPositional / BGK(543129 行)
如果用户把 prompt 文本管道送进 --bg 且 argv 中已经有 positional,则 stdin 的尾部会被追加到最后那个 positional(中间用换行隔开),从而 daemon 只看到一个合并后的 prompt。若 argv 中没有 positional,则在 -- 哨兵后面新增一条。
6c. 配套 CLI 动词(543217 行起)
claude logs <prefix>→logsHandler—— 向 daemon 开一个 500 ms 的 RPC 订阅,打印streamTail快照后退出。claude attach <prefix>→attachHandler—— 先做健康检查(mW_),然后MQ(short)接管 PTY;遇到ERESPAWNING最多重试 20 次;若是ENOJOB,从state.json读一份内容用于更有用的错误信息。claude stop / kill / rm / respawn <prefix>→ 对应处理器(在 542861 行通过M_(QGK,…)一并注册)。
所有前缀参数都经过 resolveJobByShortPrefix /* jB8 */(543217 行)解析:readdir 扫 ~/.claude/bg-jobs/,确保前缀唯一。
7. /bg 斜杠命令 —— 确认对话与 seed 抽取
7a. extractSeedFromTranscript —— Bf6(543822 行)
从下往上遍历 transcript,产出 { intent, name, nameSource, detail }:
function extractSeedFromTranscript(messages, override) {
let firstUserText = override, hasNonMetaUser = false, lastAssistant: string|undefined;
for (let i = messages.length - 1; i >= 0; i--) {
const m = messages[i];
if (m.type === "assistant" && lastAssistant === undefined) {
const text = WE(m); // 最底部那条 assistant 消息正文
if (text) lastAssistant = text.replace(/\s+/g," ").trim().slice(0,120);
}
if (m.type === "user" && !m.isMeta && !QY6(m)) {
const text = oI(m)?.trim();
if (text && viH(text)) continue; // 跳过斜杠命令的回显
hasNonMetaUser = true;
if (!firstUserText && text) firstUserText = text;
}
if (hasNonMetaUser && firstUserText && lastAssistant !== undefined) break;
}
if (!hasNonMetaUser) return null; // "Nothing to background yet"
const userName = g$(jobSessionId());
const autoName = qp(jobSessionId());
return <ruby>intent: (firstUserText<rt>| "(backgrounded)").slice(0, 200),
name: userName ?? autoName,
nameSource: userName ? "user" : autoName ? "auto" : undefined,
detail: lastAssistant,</rt></ruby>;
}
7b. BackgroundConfirmDialog —— Jv3(543849 行)
- 通过
J_读两个 AppState 字段:effortValue(Dv3)taskCount(jv3——state.tasks)
- 若 in-flight task 数为 0,组件初次渲染就自动确认(
React.useState(taskCount === 0))。 - 否则渲染一个
<ConfirmDialog>,标题 "Background this session?"、副标题"<N> tasks running — the forked session won't carry live processes.",按钮分别是 "Background anyway (tasks will be abandoned)" 与 "Stay"。 - 确认后:
$ ts const r = await forkCurrentSessionToBackground /*pf6*/ ( seed, extraPrompt, effortValue, "command", messages ); Q("tengu_background_fork", { confirmed: taskCount > 0, inflight_count: taskCount, had_prompt: extraPrompt.length > 0, had_worktree: r.hadWorktree, worktree_handed_off: r.handedOff, }); onDone(); await B9(0, "prompt_input_exit", { suppressResumeHint: true, finalMessage: formatPostBackgroundHints(r.short, r.handedOff ? "(worktree handed off)" : undefined), }); forkCurrentSessionToBackground(pf6,同模块)—— 以source: "slash"调用spawnBgSession,移交活动的 worktree,并拆掉 bridge / daemon 监听器(DV())。
7c. markSelfStoppedAndExit —— Uf6(544013 行)
async function markSelfStoppedAndExit(source: "stop_command"|"bridge") {
Q("tengu_bg_agent_action", { action: "stop", source, jobSessionId: jobSessionId() });
const dir = process.env.CLAUDE_JOB_DIR;
if (L7() && dir) {
const now = new Date().toISOString();
const cur = await readJobState(dir);
if (cur && !isJobTerminal(cur))
await writeJobState(dir, {
...cur, state: "stopped", detail: "stopped from session",
tempo: "idle", needs: undefined, block: undefined, inFlight: undefined,
updatedAt: now,
firstTerminalAt: cur.firstTerminalAt ?? now,
});
if (isInteractive()) process.stdout.write(formatExitNotice("Session stopped."));
}
return B9(0, "prompt_input_exit", { suppressResumeHint: true });
}
8. FleetView —— TUI 部分
8a. mountFleetView —— uQ3(606636 行)
主循环就是渲染 → 等动作 → 可能 attach → 卸载重挂。它在 agent view 的整段生命周期里独占 alt-screen。
async function mountFleetView(inkRoot) {
Q("tengu_bg_agent_action", { action: "list_open" });
setTerminalTitle("claude agents");
const preselectId = process.env.CLAUDE_AGENTS_SELECT;
delete process.env.CLAUDE_AGENTS_SELECT; // 单次使用
const persisted = await loadFleetUiState(currentCwd());// qxK
let query = persisted?.q;
let collapsed = persisted?.collapsed;
let jobId = preselectId;
let error: string | undefined;
let groupMode: "state"|"directory"|undefined;
let savedAppState: any;
for (;;) {
const action = await new Promise<FleetAction>((resolve) => {
inkRoot.render(
<ProcessTitleProvider>
<AppStateBoundary
initialState={savedAppState && {...savedAppState, notifications:{current:null,queue:[]}}}
onChangeAppState={({newState}) => savedAppState = newState}
>
<ContextProviders>
<ScrollFrame>
<FleetView
onAction={resolve}
initialJobId={jobId}
initialQuery={query}
initialCollapsed={collapsed}
initialError={error}
initialGroupMode={groupMode}
/>
</ScrollFrame>
</ContextProviders>
</AppStateBoundary>
</ProcessTitleProvider>,
);
});
const inAlt = ownsAltScreen(); // un_
if (inAlt && action.type === "open") handoffAltScreen();
if (!inAlt) inkRoot.render(null);
inkRoot.unmount();
if (action.type === "done") break;
// ── action.type === "open" → attach ──
if (isWindows() && process.stdin.isTTY) { process.stdin.setRawMode(true); process.stdin.ref(); }
const restoreCursor = inAlt
? batchUpdate(() => process.stdout.write(altScreenReentry()))
: () => {};
jobId = action.job.id;
query = action.query;
collapsed = action.collapsed;
groupMode = action.groupMode;
cachedJobs = action.jobs;
cachedLoopKicks = action.loopKicks;
cachedStatuses = action.statuses;
cachedStatusesTs = action.statusesTs;
cachedPrStatuses = action.prStatuses;
cachedFirstMount = true;
const respawn = action.respawnResult ?? await respawnJob(action.job.id, {
knownState: action.freshDispatch ? undefined : action.job.state,
knownAlive: (Date.now() - action.statusesTs < 1500
&& action.statuses.get(action.job.state.resumeSessionId
?? action.job.state.sessionId) !== undefined)
? true : undefined,
});
if (respawn.ok || respawn.alive) {
logFleetViewLifecycle("attach", action.job.state);
process.stdout.write(SET_TITLE(renderJobLabel(action.job.state, true)));
let attach = await attachJob(respawn.short ?? action.job.id, { alreadyInAlt: inAlt });
if (attach.kind === "error" && attach.orphaned) {
// 最后一次努力:强制 respawn 一次再试
const re = await respawnJob(action.job.id, { force: true, knownState: action.job.state });
if (re.ok || re.alive) attach = await attachJob(re.short ?? action.job.id, { alreadyInAlt: inAlt });
else attach = { kind: "error", msg: re.error };
}
if (attach.kind === "error" && !attach.ended) {
error = attach.msg;
xH("fleet_view_open", "attach_failed");
} else {
if (attach.msg) error = attach.msg;
yH("fleet_view_open");
}
logFleetViewLifecycle("detach", action.job.state, { attachDurationMs: Date.now() - tStart });
} else {
error = respawn.error;
xH("fleet_view_open", "respawn_failed");
}
if (inAlt) process.stdout.write(restoreAltMargins());
inkRoot = await createRoot({ exitOnCtrlC: false });
restoreCursor();
}
}
8b. FleetView 组件 —— tmK(604467 行)—— 状态机
组件内部状态(按 useState/useRef 顺序大致排列):
| 槽位 | 用途 |
|---|---|
jobs / setJobs (A, z) | 当前挂载的 JobRow 列表。bootstrap 中为 null。快照镜像到 $.current 供回调使用。 |
incomingJobs (Y, w) | 本次挂载内新发起、还在等 state.json 落盘的 job。 |
prStatuses (F, g) | PR check 状态缓存。 |
cursorIndex (d, a) | 扁平化后行列表 h9 的索引。 |
peekJob (_H, t) | 当前正在 peek 的 job。 |
dispatchFolded (qH, i) | 是否折叠 "queued/folded" 段落。 |
replyDrafts (e.current) | Map<jobId, draftText>:peek 面板的内联回复草稿。 |
replyErr ($H, AH) | 最近一次回复失败的信息。 |
helpOpen (s, o) / confirmDelete (HH, KH) / selectMenu (jH, YH) | 各种 overlay 模态。 |
exiting (MH, kH) | 退出二次确认;LH = ph(setExiting, () => onAction({type:"done"})) 把 Ctrl+C 串起来。 |
renaming (WH, DH) | 正在重命名的 job 或 null。 |
attaching (XH, NH) | 正在 attach 前 respawn 的 job id。 |
error ($9, L6) | toast / 错误行。 |
groupMode (tH, v_) —— "state" | "directory" | 持久化到 b_().fleetViewGroupMode。 |
collapsedGroups (UH, BH) —— Set<string> | 按 cwd 持久化。 |
manuallyCollapsedPinned (FH, q_) | pinned 段被手动折叠但不影响持久化集合时使用。 |
cwd (L) → 最终解析为 repo 根(_Y(cwd))。 | |
pinnedDir map (u, b) | 按 cwd 的快速 pin 映射。 |
query / cursorOffset(来自 v0(...) 多行输入 hook) | 搜索框。 |
它还从同模块(602820–602850 行)导入 repoGroup({cwd}) (Nd8)、pickIcon、jobLabel、childStatusColor 等若干辅助。
8c. 键位绑定 —— handleFleetViewKeyDown / wS(605589 行)
当 overlay 显示中(renaming / attaching):
Ctrl+C 取消 overlay
up/down 穿透给 overlay 自身的滚动逻辑
其他 转交给 overlay 内部的输入处理
全局:
Ctrl+C 清空输入 → 帮助/确认删除若开则关 → 否则退出
Escape 关帮助 → 关确认删除 → 关折叠帮助 → 关菜单 → 清输入 → 撤销 armed 动作 → onAction("done")
Shift+up/dn 在组内移动 job(Sy/SY)
Ctrl+R 重命名所选 job (renaming = job.id)
Ctrl+S 在 "state" ↔ "directory" 分组模式之间切换
Ctrl+G 在查询中展开 / 替换
Ctrl+T pin / unpin 所选 job
up / Ctrl+P 移动光标(或滚动补全建议)
down / Ctrl+N 移动光标
Ctrl+X (帮助打开时) → 杀掉 armed 动作目标
Tab 接受斜杠建议 / 切换 dispatchFolded
→ attach 当前行(也就是博客中的 right-arrow)
⌘/Super+1-9 attach 当前 cwd 组的第 N 行
Return 若输入为空:识别 /exit/quit 退出 → 否则提交建议 → 解析 "<template> <intent>",
分发一个新 bg job →(Shift+Return ⇒ 额外通过 {type:"open", freshDispatch:true} 自动 attach)
空格-空输入 切换当前行的 peek 模式 (也就是博客中的 "peek")
FleetView 中的"左方向键"由多行输入 hook(v0 的默认 cancel)处理 —— 输入为空时回退到 onCancel: setExiting,由 ph 接管做一次"再按一次确认退出"(LH)。
8d. peek 面板 —— PeekPane / WQ3(603580 行)
这就是博客描述的 "peek mode":选中某一行但不完整 attach。
- 读
q.state.updatedAt渲染r7(now - updated)显示 "Last interaction X ago"。最近 1 分钟内每 1s 重渲,否则 30s。 - 从
q.state.output里挑三条信息行(剔除与内联子列表标签重复的),再加state.needs与state.detail,根据终端高度自适应。 - 它有自己的
v0输入框,绑定replyDrafts.get(jobId):onSpaceOnEmpty: onBack—— 空格退出 peek。onTabOnEmpty: () => setQuery(suggestedReply); Q("tengu_prompt_suggestion", { outcome: "accepted", source: "fleetview_peek" })—— Tab 接受state.suggestedReply(仅当tempo === "blocked"且没有 questions 表单时才存在)。onExit→ 通过onReply(formattedDraft)提交草稿;成功后 daemon 那边的 worker 自动恢复 —— 这就是博客中"内联回复自动 resume"的实现。
- 若
state.tempo === "blocked"且state.block.questions存在,渲染一个<QuestionForm>(jH = 2 + opts.length + 1行的额外布局空间)。
8e. 视觉指示规则
- 行颜色 / activity 字形 =
jobActivityForRow(jobRow, prStatuses)→"flowing" | "slowing" | "stuck" | "success" | "failure" | "stopped"。"slowing" / "stuck" 阈值取自updatedAt:tempo === "active":3 分钟 → "flowing",超过 15 分钟 → "stuck"。- 否则:15 分钟 → "flowing",超过 75 分钟 → "stuck"。
- "awaits input" =
tempo === "blocked"(block.questions 存在)或state.needs非空。 - "actively processing" =
tempo === "active"且非终止态。 - "completed" =
LD(state)为真。 - "next run time"(循环 job)=
vd8(jobRow, loopNextRun),显示"in <时长>"。
9. 持久化
- 每个 job 一份
state.json—— FleetView 与claude logs/attach/...的唯一真相源。通过pT(jobDir, partial)原子 merge 写入。 ~/.claude/bg-jobs/——<short>/子目录的集合。stdout/stderr 是 worker 的 PTY 历史(通过 daemon RPC 读取,不经磁盘)。- FleetView UI 状态(按 cwd) ——
B26(cwd, { q, collapsed })以 repo 路径为 key,加载时通过qxK(repoRoot),写入有 300 ms debounce。 - 分组模式偏好 ——
b_().fleetViewGroupMode通过H6(updater)写入用户配置仓库。 - Daemon socket / 心跳 ——
.fleetview-heartbeat锁文件(Moq,104916 行)。
10. 端到端时序
A) `claude --bg "ship the redis fix"`
argv 进入 mainCliEntry() → 命中 `argv.includes("--bg")`(651394 行)
│
▼
fleetGate ok → handleBgFlag(argv) [543089]
│ stdin (TTY) → "";rest = argv \ ["--bg"]
▼
spawnBgSession(rest) [pHH, 542905]
│ 生成 sessionId, short, jobDir
▼
spawnBgSessionAndDispatch [aN3, 542926]
│ • 写 state.json(seed 数据)
│ • 构造 JobDispatch
▼
daemon V$ {op:"dispatch", d} [543031]
│ daemon 可能回 ok / ack-timeout / enoconn / short-alive / stale-short
▼ ack-timeout 的重投 / 救援循环
stdout ← formatPostBackgroundHints(short, idle?) [543177]
exit 0
B) 实时会话里输入 `/bg`
REPL 接到 /bg → 加载 handleBgSlashCommand [Yv3, 543958]
│ 守卫:已经是 bg / 持久化关闭
▼
extractSeedFromTranscript 走 transcript → {intent,name,detail}
▼
<BackgroundConfirmDialog/> [Jv3, 543849]
│ task=0 自动确认;否则弹 <ConfirmDialog>
▼
forkCurrentSessionToBackground(seed, prompt, effort, "command", messages) [pf6]
│ 与 --bg 同一条 dispatch 路径,但 source="slash",移交 worktree、拆 bridge
▼
Q("tengu_background_fork", …)
onDone() → B9(0, "prompt_input_exit", {finalMessage: formatPostBackgroundHints(short, …)})
C) `claude agents`(或 REPL 中按 ←)
REPL 中 ← → forkAndOpenFleetViewFromRepl(messages, effort, autoName) [KpK, 606947]
│ extractSeedFromTranscript(允许空)→ wB8 预写 state.json
│ forkCurrentSessionToBackground 拉起 bg fork
│ Q("tengu_open_agents_via_left")
│ 若 tengu_bg_leftarrow_inprocess 开关为 true:
│ → remountFleetViewInProcess(带这个 short 预选)
│ 否则:
│ → FjH({ args:["agents"], env:{CLAUDE_AGENTS_SELECT: short} }) ← exec 一个新的 claude
`claude agents` → mainCliEntry() → argsAreOnlyDebugFlags 通过 → isAgentsFleetEnabled()
│
▼
mountFleetView(root) [uQ3, 606636]
┌──────────────── 循环 ────────────────┐
▼ │
<FleetView/> 渲染 → 用户导航 ─────────────────┤
│ │
▼ onAction === "open" │
respawnJob → attachJob (PTY) │
│ 用户工作 → 最终 detach │
▼ onAction === "done" │
gracefulShutdown(0) │
└──────────────────────────────────────┘
11. 坑 / 校正记录
claude agents是两条命令的合体。 647846 行 Commander 注册的agentsHandler(Cs3)只在 fleet view 关闭时生效;它输出的是按 source 分组的 subagent 定义列表。博客所说的 "agent view" 实际是 651485 行的 fast-path FleetView。/bg是别名。 真名是/background(543999 行),bg在aliases。immediate: (raw) => !raw.trim()表示纯输/bg<Enter>跳过 JSX 确认;带 prompt 文本则会卡在 React 树。/bg与--bg走同一条 dispatch。 唯一差异是:(a) 写入state.json的source字段不同;(b) 父进程是否需要拆 worktree/bridge。daemon 完全不区分。L7()("是否在 bg 中")≠ "会话已被后台化"。 它当且仅当本进程的CLAUDE_JOB_DIR被设置时返回 true —— 即"我是那个 fork"。刚刚起 fork 的那个前台进程仍然不属于"已后台化";所以/bg在 dispatch 之后调B9(0, "prompt_input_exit")让前台主动退出。CLAUDE_AGENTS_SELECT是一次性的。mountFleetView一拿到就deleteenv(606640 行),避免长生命周期的 fleet view 锁死在某个过期 id 上。- "Background anyway" 文案说的是 tasks,不是 files。 它读的
taskCount(jv3 = state => state.tasks)来自 AppState 的tasksmap —— 也就是 bash/长跑工具 —— 不是 in-flight 请求。是两个不同概念,但 UI 文案只提到 tasks。 - 左方向键退出只在输入为空时生效。 实际触发的是多行输入 hook
v0的onCancel;输入有内容时左键只是移动光标。 disableAgentView是唯一一刀切。 一旦置位(托管配置或 env),claude agents会回退到agentsHandler、--bg/--background会被 gate 拦下、/background斜杠命令虽然isEnabled永远为 true,但内部pHH仍会在fleetGateRejected处失败,daemon 也拒绝拉起。spawnBgSession救援两个特定 daemon 故障:ack-timeout(worker 还在,只是 daemon 回包丢了)与enoconn(RPC 半途 socket 没了)。它先发op:"list"探一下,要么宣告成功,要么尝试重投一次。- 轮询节奏。 FleetView 中 peek 时间戳每 1s(近 1 分钟)或 30s(更久)重渲。但文件 watcher / RPC poller 频率在另外的模块里(602820 一带的
seedLastJobs加另一组 hook)。移植时这块要单独重写。 claude配合defaultToAgentsView: true默认进 FleetView。 用户在设置面板(462620 行)勾上后再裸跑claude就直接进去 —— 容易被意外触发。
12. 移植到其他项目的最小落地清单
如果想把 "agent view + /bg" 带到别的 shell(比如某个 Anthropic CLI 的 fork,或者一个无关的助手 CLI):
- 磁盘模型。
~/.claude/bg-jobs/<short>/state.json:原子 merge 写、readdir前缀查找、第 1 节定义的JobStateschema。 - Daemon。 一个 Unix socket 服务,至少支持
{op:"list"}、{op:"dispatch", d:JobDispatch}以及一个按 job 的 PTY attach RPC。macOS/Linux 每用户一个unix:-协议 socket;Windows 用同样 JSON 信封的 named pipe。带fast_path_policy标记,便于被托管配置硬关。 - Worker。 由 daemon 用
JobDispatch.env中的环境拉起,环境包含CLAUDE_JOB_DIR=<dir>、--session-id <uuid>,prompt 作为最后一个 positional。worker 自己 updatestate.json(intent、name、output、tempo、block)。 /bg斜杠命令。local-jsx,aliases: ["bg"]。seed 由 transcript 自下而上扫描得到(跳过 meta 和斜杠命令回显)。state.tasks > 0时弹确认;否则自动确认。--bgCLI 标志。 去掉该 flag,读管道 stdin(截断 16 KiB),调入同一个 dispatcher。成功时打印 5 行提示块。claude logs / attach / stop / kill / rm / respawn <prefix>。 把 prefix 解析为完整 short,发 RPC。attach要小心:必须接管 PTY 并能扛住ERESPAWNING。- FleetView TUI。
mountFleetView(inkRoot)循环:渲染 → 等<ruby>type:"open"<rt>"done"</rt></ruby>→ 若open则 respawn + attachJob + detach 后再渲染。- 组件内部:按 cwd 持久化的 UI 状态(query + collapsed)、state/directory 分组切换、双重用途的 peek 面板(既看又回)、多行输入、重命名 overlay、删除确认 overlay、帮助 overlay、组内拖拽排序。
- 单独维护
attachingref,避免同一行被双击触发两次。
- 闸门。 托管配置
disableAgentView+ envCLAUDE_CODE_DISABLE_AGENT_VIEW+ 组织策略返回的fast_path_policy。每个入口前都过一次同一个isAgentsFleetEnabled()。 - 遥测。 至少:
tengu_background(flag/slash)、tengu_background_fork、tengu_bg_dispatch、tengu_bg_dispatch_fallback、tengu_bg_dispatch_rescued、tengu_open_agents_via_left、tengu_fleetview、tengu_bg_agent_action、tengu_prompt_suggestion,以及所有fleet_view_*动作动词(open / rename / pin_toggle / reorder / stop / attach / detach)。 - 左方向键 → agents。 Hook 住 REPL 输入框"空 + 左方向键":(a) 从当前 transcript seed 一个 fresh bg fork,(b) 拆当前 worktree/bridge,(c) 优先 in-process 重挂 fleet view;失败回退
exec。 - 风险声明的 gating。
--bg配合bypassPermissions或auto权限模式需要先在交互式中确认一次 —— 不要绕过 543643-543646 行的检查,避免 headless dispatch 偷过 UX 关卡。
13. 符号映射速查表
bg-cli 与斜杠命令模块(QGK / gW_)
| 重命名后 | 混淆名 | 行号 |
|---|---|---|
preSeedReplBgJob | wB8 | 542877 |
spawnBgSession | pHH | 542905 |
spawnBgSessionAndDispatch | aN3 | 542926 |
markBgDispatchSucceeded | IGK | 542802 |
markBgDispatchFellThrough | OB8 | 542820 |
handleBgFlag | sN3 | 543089 |
readBgStdin | pGK | 543108 |
withStdinPositional | BGK | 543129 |
humanizeDispatchFailureReason | tN3 | 543163 |
formatPostBackgroundHints | mf6 | 543177 |
bgVerbExtraArgsNote | UGK | 543188 |
resolveJobByShortPrefix | jB8 | 543217 |
logsHandler | eN3 | 543246 |
attachHandler | Hv3 | 543271 |
stopHandler | qv3 | (export 见 542863) |
rmHandler | Kv3 | (export 见 542865) |
respawnHandler | _v3 | (export 见 542866) |
parseResumeTarget | FGK | (export 见 542869) |
flagsWithoutPositional | gGK | (export 见 542873) |
extractSeedFromTranscript | Bf6 | 543822 |
BackgroundConfirmDialog | Jv3 | 543849 |
handleBgSlashCommand | Yv3 | 543958 |
BG_CLI_FLAG_ALIASES (["--bg","--background"]) | oN3 | 543733 |
forkCurrentSessionToBackground | pf6 | (调用见 543875、606981) |
BG_SLASH_COMMAND_DEF | Mv3 / fv3 | 543999 |
getCurrentJobDir | Xv3 | 544011 |
markSelfStoppedAndExit | Uf6 | 544013 |
stopSlashJsxCall | Pv3 | 544047 |
stopBridgeNonInteractiveCall | Wv3 | 544055 |
FleetView 模块(bd8 / xd8)
| 重命名后 | 混淆名 | 行号 |
|---|---|---|
FleetView(组件) | tmK | 604467 |
mountFleetView | uQ3 | 606636 |
remountFleetViewInProcess | emK | 606921 |
forkAndOpenFleetViewFromRepl | KpK | 606947 |
PeekPane(组件) | WQ3 | 603580 |
renderJobLabel | ZW6 | 602868 |
formatJobAgeOrCountdown | vd8 | 602863 |
classifyJobActivity | jW6 | 602953 |
deriveStateBand | IvH | 602971 |
repoGroupKey | Nd8 | 604497 |
parseDispatchInput | Zd8 | 603040 |
parseSearchQuery | BmK | 603002 |
pruneMap | Pd8 | 602855 |
logFleetViewLifecycle("peek"|"attach"|"detach") | Rd8 | 603598 |
handleFleetViewKeyDown | wS | 605589 |
swapAdjacentJobsInGroup | SY | 605532 |
attachJobOrSurfaceError | YS | 605454 |
runRowKeyAction | lj | 605425 |
CLI 入口与 Commander
| 重命名后 | 混淆名 | 行号 |
|---|---|---|
mainCliEntry | je3 | 651265 |
argsAreOnlyDebugFlags | caK | 651245 |
agentsHandler(fallback —— 列出 agent 定义,不是 jobs) | Cs3 | 644428 |
AgentsList(fallback 组件) | FoK | 644390 |
formatAgentDefinitionRow | BoK | 644383 |
renderAgentSourceGroup | Es3 | 644416 |
isAgentsFleetEnabled | Xc | 149497 |
fleetGateRejected | (以同名 re-export) | 651424 |
配置 / 常量
| 重命名后 | 混淆名 | 行号 |
|---|---|---|
SETTING_DISABLE("disableAgentView") | y | 53308 |
SETTING_DEFAULT_TO_FLEET("defaultToAgentsView") | — | 462620 |
ENV_PRESELECT("CLAUDE_AGENTS_SELECT") | — | 606639 |
ENV_AUTORELAUNCH("CLAUDE_AGENTS_AUTO_RELAUNCHED_AT") | Xd8 | 606765 |
AUTO_RELAUNCH_UNFOCUSED_MS | YW6 | 606763 |
AUTO_RELAUNCH_MIN_INTERVAL_MS | umK | 606764 |
FLEETVIEW_HEARTBEAT(.fleetview-heartbeat) | Moq | 104916 |
评论
评论发布后会立即公开,如触发规则可能被审核下架。