@alook/daemon

中文文档
0.1.65发布于 2026/11/07Apache-2.0ES Module构建来源已验证近 30 天下载 3145

Alook agent daemon — host-side runtime backend, process manager, credential proxy, and control plane.

原始使用文档
文档目录18 节
  1. Daemon lifecycle commands
  2. The big idea
  3. Persistent delivery models (the heart of it)
  4. Safe-boundary runtimes (Claude and Codex)
  5. In-process SDK runtime (Pi)
  6. Persistent steering runtime (Cursor)
  7. Persistent service runtime (OpenCode)
  8. Runtime comparison
  9. Layout
  10. Host orchestration (manager + server)
  11. Credentials (zero-trust isolation)
  12. Three key tiers (and where each lives)
  13. Inbox layer (freshness)
  14. Build & run
  15. Point a daemon at real infra locally
  16. Usage sketch
  17. Alook PII
  18. User automation

@alook/daemon

Alook's host daemon: control-plane connectivity, agent scheduling, credentials, diagnostics, and lifecycle orchestration.

Runtime execution lives in the independently buildable @alook/agent-driver package under agent-driver/. That package adapts Claude Code, Codex, Cursor, OpenCode, and Pi behind one public AgentSession contract. The daemon consumes that contract and does not speak vendor process or SDK protocols.

The driver package remains host-neutral. Its CLI transport creates a per-launch state directory, exposes a stable alook command through PATH, and composes explicit environment layers supplied by the host. Vendor adapters never import daemon credentials or control-plane code.

Provenance. The shapes, protocols, flag strings, and control flow here were derived by studying how production agent-runtime daemons drive these CLIs, then re-expressed as a tidy, generic abstraction. It is original code, not a copy of any vendor source, and carries no platform-specific glue.


Daemon lifecycle commands

The web Machines sheet generates the full command. Production commands use the built-in Alook endpoints; local development commands include explicit local URLs:

npx --yes @alook/daemon@latest daemon start --machine-key '<cmt_pair_token>'

Restart a previously paired machine without rotating its credential:

npx --yes @alook/daemon@latest daemon start --id '<machine_id>'

Reconnect/rotate only with the command generated for that exact machine:

npx --yes @alook/daemon@latest daemon reconnect \
  --id '<machine_id>' --machine-key '<cmt_reconnect_token>'

Reconnect first takes replacement ownership and stops only the matching saved PID, then activates the token. A proven pre-commit rejection resumes the prior launch record. If a committed response is received and local launch then fails, the newly persisted record remains offline and is recovered with daemon start --id '<machine_id>'. A network error, timeout, 5xx, or otherwise ambiguous activation response never restarts the possibly revoked old credential; because the new credential was not received, mint a fresh reconnect token in the UI and run daemon reconnect again. Never paste pairing or machine credentials into logs, bug reports, or chat.


The big idea

The daemon creates an AgentDriverSdk, probes or opens a selected backend, and then works only with an AgentSession:

  • start(message) admits the first command.
  • send(message) accepts, queues, or rejects a later command with a typed receipt.
  • interrupt(...) and stop(...) provide bounded lifecycle control.
  • snapshot() exposes logical state without leaking process/SDK objects.
  • events is an async stream of normalized AgentEvent values.
  • closed settles exactly once with terminal and host-cleanup facts.

Inside @alook/agent-driver, a backend adapter declares its execution model and normalizes vendor output. LogicalAgentSession owns admission, FIFO queueing, safe-boundary delivery, terminal ordering, and cleanup. Internal ProcessLane and SdkLane hosts make child-process and in-process SDK transports look alike; they are deliberately not public daemon primitives.


Persistent delivery models (the heart of it)

When a message arrives, what happens depends on the runtime's lifecycle:

Safe-boundary runtimes (Claude and Codex)

One child process spans many turns. Both adapters declare lifetime: "session"; their public capability is midTurnDelivery: "safe_boundary_queue". A message arriving during tool use, compaction, or review is queued by LogicalAgentSession and receives a later command_accepted or command_failed event. The daemon does not implement that protocol or queue.

For Claude specifically, the input channel is stream-json: the process is launched with --input-format stream-json --output-format stream-json --include-partial-messages, and each message is one NDJSON line {"type":"user","message":{"role":"user","content":[{"type":"text","text":…}]}}.

In-process SDK runtime (Pi)

Pi declares the in_process_sdk transport and midTurnDelivery: "steer". Its lane delegates prompt, steer, abort, and dispose to the SDK while preserving the same receipts, events, and terminal contract.

Persistent steering runtime (Cursor)

Cursor keeps one cursor-agent acp process and ACP session for the logical session. A root command starts a session/prompt; busy commands steer the same logical turn through concurrent prompt requests. Each request has its own JSON-RPC id, while the root receipt remains the stable terminal owner. Old responses cannot settle the current turn. Interrupt sends session/cancel without killing the process.

Persistent service runtime (OpenCode)

OpenCode starts one authenticated, loopback-only service per logical session. The actual CLI major selects its adapter: v1 retains --pure and durable event replay; v2 uses its native API, persisted inbox/messages, and a new idle record after all accepted inputs. Root prompts and busy steers share the same service and vendor session. Live SSE handles permissions and tool progress. Unknown majors or missing native capabilities fail explicitly.


Runtime comparison

RuntimeLifecycleTransport / protocolSteeringInitial inputOutput format
claudepersistent sessionstream-json NDJSONsafe_boundary_queuestdin user-message linestream-json
codexpersistent sessionJSON-RPC 2.0 (app-server --listen stdio://)safe_boundary_queueinitialize → thread/start/resumeJSON-RPC notifications
pipersistent session@earendil-works/pi-coding-agentsteersession.prompt()SDK callback
cursorpersistent sessionACP JSON-RPC 2.0 (cursor-agent acp)steersession/promptsession/update + correlated prompt response
opencodepersistent sessionauthenticated HTTP + SSE (opencode serve; v1 adds --pure)steerCLI-major session prompt APInative recovery + live SSE

(Exact launch flags live in agent-driver/src/adapters/<backend>/.)


Layout

agent-driver/
  src/
    contract.ts                  # public SDK, session, receipt, event, and result types
    sdk.ts                       # createAgentDriverSdk
    registry.ts                  # built-in ids and capabilities
    controller/
      logical-session.ts         # admission, queueing, turns, stop, cleanup
      process-host.ts            # internal child-process lane
      sdk-host.ts                # internal in-process SDK lane
    adapters/<backend>/          # vendor launch + normalization
    host/default-host.ts         # standalone host implementation
    internal/                    # adapter-only transport/config/process helpers
    testing/                     # public conformance fixtures
src/
  index.ts                       # daemon exports; re-exports the driver public API
  drivers/index.ts               # daemon runtime lookup/probe facade only
  logger.ts                      # structured daemon logging
  runtime/                       # manager liveness, notification, error helpers
  inbox/                         # unread projection and freshness policy
  manager/
    agentDriverHost.ts           # host resource/env/CLI preparation
    managerPolicy.ts             # pure scheduling reducer
    managerRuntime.ts            # applies effects through AgentSession
  server/                        # data/control-plane contracts and WebSockets
  daemon/createDaemon.ts         # runtime-agnostic daemon composition
  cli/                           # lifecycle CLI and proxy data-plane client
  credentials/                   # voucher broker and key-swapping proxy
scripts/
  daemon.ts                      # local daemon entry

Host orchestration (manager + server)

Beyond driving a single runtime, the backend includes the host-side orchestration so it can run agents end-to-end:

  • manager/ — AgentProcessManager schedules processes. Decisions live in a pure reducer (managerPolicy.ts: single-flight one-process-per-agent, wake/sleep, message queue + coalesce, stalled detection → spawn/send/stop/terminate_stalled effects); a thin executor (managerRuntime.ts) applies them to real sessions and feeds runtime events back in. AgentRouter consumes the control plane (below).
  • server/ — the agent ⇄ server boundary, split into two planes that share one contract (contract.ts):
    • data plane (ServerApi): what the alook CLI calls — send / inboxPull / ack / read / listChannels, addressed by path-style ChannelRefs.
    • control plane (HostCommand + HostControlChannel): server → host commands (agent:wake / agent:stop; plus bot:* lifecycle frames). agent:wake carries a bodiless UnreadNotice (channel + latest seq, no message content) — the DAEMON (AgentRouter/AgentProcessManager), not the server, decides whether to spawn a process, notify an already-running one, or coalesce the notice for the next turn. There is no in-process fixture standing in for the server: wsControlServer.ts (server end) and wsControlChannel.ts (host end, injectable socket + exponential-backoff reconnect + heartbeat) carry the HostCommand frames over a real WebSocket (ws://127.0.0.1 in local dev, a real ws-do URL in production). A real server connection is the same WsControlChannel pointed at a real URL — nothing upstream changes.

tests/integration/daemon/control-plane.test.ts wires a real WsControlChannel against a running ws-do dev server into a complete loop: seed a server/bot/ machine, post a message as a human owner over real HTTP, and assert the daemon's control channel receives the resulting agent:wake HostCommand — then, acting as the agent (no CLI spawned), replies over the real credential chain (enroll → voucher → proxy → X-Agent-Id) and asserts the reply is readable back over real HTTP. See "Point a daemon at real infra locally" below for how to run the pieces this test drives by hand.

Credentials (zero-trust isolation)

A spawned runtime needs some credential to call back to the server, but handing it the real API key means the agent process (and any tool it runs) can read it, can't be scoped, and a leak forces a rotation. credentials/credentialProxy.ts implements the voucher pattern instead — the same shape a production daemon uses:

  1. The host starts one local key-swapping proxy on 127.0.0.1 (loopback only).
  2. For each launch, a CredentialBroker mints a per-launch voucher (vch_…) into a 0600 file. The child is given <PREFIX>_PROXY_URL + <PREFIX>_PROXY_TOKEN_FILE + its capability set — never the real key.
  3. The child's CLI calls the proxy with Authorization: Bearer vch_….
  4. The proxy validates the voucher, swaps in the real Bearer <hostApiKey>, stamps X-Agent-Id / X-Client / X-Agent-Active-Capabilities, and forwards to the host-supplied upstream.

This buys credential isolation (the agent only ever holds a voucher), capability scoping (the proxy rejects endpoints outside the voucher's caps), and revocability (vouchers are per-launch and individually revocable; rotating a leaked voucher never touches the real key). This is the only credential path — cliTransport requires ctx.credentialProxy (a running broker + proxy URL) and throws without it; there is no plaintext fallback that would hand the agent a real key. Everything is host-neutral: the real key, upstream URL, voucher prefix, and header names all come from the host via CredentialBrokerConfig.

src/credentials/credentialProxy.test.ts covers the swap, voucher rejection (invalid local agent proxy token), capability scoping, and revocation against a throwaway upstream.

Three key tiers (and where each lives)

A production daemon keeps three credential tiers, not two — important for understanding what hostApiKey should be:

  1. Machine master key — authenticates the daemon to the server and is used to mint per-agent credentials. Held in daemon memory only (not persisted to disk or keychain). ⚠️ In the current daemon it is passed as a process argv (--api-key …), so any local process can read its full value via ps aux. Mitigation: source it from an env var, a 0600 file, or the OS keychain instead of argv. This proxy never forwards the master key.
  2. Per-agent runner credential — the server mints one per agent (scoped to that agent, individually revocable) when the daemon asks with the master key. This is what the proxy actually swaps in and forwards upstream, and what belongs in CredentialBrokerConfig.hostApiKey. Minting it from the master key is host-side orchestration and is intentionally out of scope for this backend.
  3. vch_ voucher — minted by CredentialBroker per launch, written to a 0600 file, and the only credential the agent process ever sees.

Inbox layer (freshness)

Two pieces govern how messages reach the agent and how outgoing actions are gated:

  • inbox/projection.ts — projectAgentInboxSnapshot buckets pending messages by target (channel / DM / thread) and projects each into the metadata-only summary surfaced as an [inbox notice: …] (count, first, latest, sender, flags). No bodies — the agent pulls those with alook inbox pull.
  • inbox/stateMachine.ts — planAgentInboxSideEffect is the "don't reply on stale context" guard. Before an outward action (send / task_claim / task_update) it compares the model's seen seq boundary against pending messages and returns forward (let it through), held (hold the action, surface the latest few unseen messages as held context to reconcile first — this is the "Freshness hold → saved as a draft" you can hit when messaging a busy agent), or bypass (explicit continueAnyway). It is a pure function producing a stable producerFactId; clause ids SMR-002 (consume) and SMR-006 (held envelope) are preserved.

Build & run

This project uses pnpm (see the packageManager field).

pnpm install
pnpm run typecheck           # tsc --noEmit (passes clean)
pnpm test                    # Vitest package suite, including its packed/runtime categories
pnpm run test:integration    # real infra: requires wrangler dev + ws-do dev + queue-worker dev (see below)

Unit behavior is covered by src/**/*.test.ts. Full control-plane and credential-chain behavior against real infra is covered by tests/integration/daemon/ — see that directory for the executable reference of how the pieces fit together over the network (real WsControlChannel over a real WebSocket, real enroll-agent/credential-proxy HTTP calls, no in-process shortcuts).

Point a daemon at real infra locally

There's no mock server to stand in for src/web/src/ws-do — a daemon always talks to the real thing. To drive one by hand in local dev, run the real servers and walk the real credential chain (cmt_ pairing token → cmk_ daemon credential → crk_ per-agent runner key):

Terminal 1 — the web app + control-plane DO:

pnpm --filter @alook/web dev      # http://localhost:3000
pnpm --filter @alook/ws-do dev    # ws://localhost:8789

Terminal 2 — pair + activate a machine, then start a daemon with the resulting cmk_... credential:

# 1. As a signed-in user, create a pairing token:
curl -s -X POST http://localhost:3000/api/community/machines/pair \
  -H "Cookie: <session-cookie>" | tee /tmp/pair.json
# → { "tokenId": "cmt_…", "expiresAt": "…" }

# 2. Exchange it for a daemon credential:
curl -s -X POST http://localhost:3000/api/community/daemon/activate \
  -H "Authorization: Bearer $(jq -r .tokenId /tmp/pair.json)" \
  -H "content-type: application/json" \
  -d '{"hostname":"my-laptop","platform":"darwin","arch":"arm64"}'
# → { "credential": "cmk_…", "machineId": "cm_…", "expiresAt": null }

# 3. Start it detached (the default):
pnpm run daemon -- start --machine-key cmk_… \
  --server-url http://localhost:3000 \
  --ws-url ws://localhost:8789

# Keep it attached for local debugging:
pnpm run daemon -- start --foreground --machine-key cmk_… \
  --server-url http://localhost:3000 \
  --ws-url ws://localhost:8789

A bot bound to this machine (via the community UI/API) can now be woken by posting a message in a channel it's a member of — the real wake-producer path (src/web → src/queue-worker → src/ws-do) delivers agent:wake over the daemon's real WsControlChannel.

pnpm --filter @alook/daemon build emits a self-contained daemon bundle. The published tarball includes the runtime-driver implementation, while @alook/agent-driver is also independently buildable and publishable for hosts that want only the logical-session SDK.

Usage sketch

import { createAgentDriverSdk } from "@alook/agent-driver";

const sdk = createAgentDriverSdk();
const opened = await sdk.open({
  backend: "codex",
  launch: {
    workingDirectory: ".",
    instructions: { format: "markdown", content: "Be concise." },
    launchId: "launch-example",
  },
  config: {
    model: { kind: "default" },
    mode: "default",
  },
});
if (!opened.ok) throw new Error(opened.error.message);

const { session } = opened;
const eventsDone = (async () => {
  for await (const event of session.events) {
    if (event.type === "assistant_message_completed") console.log(event.text);
  }
})();

await session.start({ id: "command-1", kind: "user", text: "Inspect this repository." });
await session.send({ id: "command-2", kind: "user", text: "Now summarize the risks." });
await session.stop({ reason: "owner_request", forceAfterMs: 5_000 });
const result = await session.closed;
await eventsDone;
void result;

Alook PII

The daemon filters outgoing Alook message body text locally, in pipeline order, before forwarding a send. Each bot has its own persisted pipeline at <baseDir>/daemons/<machineId>/<botId>/pii/pipelines.json, containing only that bot's plugin source and configuration. It loads that file once and reuses its configuration and compiled filters in memory; subsequent sends do not reread JSON or recompile unchanged filters. Successful management changes replace only that bot's snapshot. Manual file edits require a daemon restart. Another bot's invalid configuration does not block it. Attachments and requests to cloud models are outside this filter. Enabled filters fail closed: an error, invalid result, or execution limit blocks the send.

Fresh configurations contain five default filters: Keys, Email addresses, ID numbers, Phone numbers and Bank cards. Keys runs the previous private-key, recognized API-key, JWT/Authorization and credential-assignment/connection-password checks together, preserving their category-specific replacement markers. Already saved pipelines keep their installed filters, sources, flags and deletions; there is no automatic consolidation.

The default ID numbers filter covers selected identity, social-security and personal tax numbers across 16 countries/regions. Ambiguous numeric formats require a document label. See the supported formats, checks and limitations. Previously initialized configurations retain their installed rule sources.

The default Phone numbers filter recognizes international numbers and phone-labelled national numbers using pinned Google numbering metadata, alongside the existing standalone formats. See recognition boundaries and source attribution.

Inside a bot session:

alook pii list
alook pii add --file ./company-terms.ts --description "Redacts internal project names"

The TypeScript file default-exports a synchronous pure transform returning { text: string, triggered: boolean }:

export default (text: string) => {
  let triggered = false;
  const output = text.replace(/Project Wren/g, () => {
    triggered = true;
    return "[INTERNAL_PROJECT]";
  });
  return { text: output, triggered };
};

The bool is explicit per plugin, including when a later plugin restores the text. Legacy string-returning sources remain executable with per-node input/output comparison as a compatibility estimate; that cannot report no-op matches. Successful sends include pii.triggeredFilters only when a filter reports a hit, without original values. See credential formats, protocol and receipt limits.

Plugins have no provided filesystem, process, environment, network, or module-import capabilities. They run with memory and execution limits. Adding appends a new filter and cannot replace an existing filter. The CLI has no disable/delete operation. The human owner manages those actions in Privacy while the daemon is online, including for inactive bots. A dispatched request is not an applied change; the page waits for the local daemon's result. Deleted defaults stay deleted after restart. Rules can miss sensitive values; review the pipeline and its scope before relying on it.

The Privacy playground relays test text through Alook's server to the daemon and returns the selected pipeline's output. It uses the same runner and checks the displayed configuration revision. Test input and output are transient: they are not stored in configuration, chat history, replay queues or body logs. No chat message or model request is sent. Input and output must each fit 1024 UTF-8 bytes; a failed filter returns a safe error instead of partial output.

User automation

Create an access code in user Settings → Automation. Choose Write permissions (Create bots, Create servers, Invite friends to servers, Send messages), an expiry, and the rate for each permission. Read permissions (List machines, List friends, List servers) start enabled and can each be turned off. Save the full code when it appears: it is returned only once. Editing name, permissions or rates preserves the expiry; explicitly saving a new expiry starts that duration again. Deleting a code rejects subsequent requests.

use runs independently of a daemon connection or Alook bot session. It calls dedicated Automation APIs authenticated by access code; the normal client and bot APIs keep their original authentication and do not consume Automation quotas. Set ALOOK_ACCESS_CODE in the calling process, or pass --access-code-file <path> before the resource command. Keep the file private. Do not put the code in a command argument or URL.

npx @alook/daemon use bot --create '{"name":"Helper","machineId":"mac1","backendId":"codex","model":"gpt-6"}'
npx @alook/daemon use server --create '{"name":"demo","description":"Team work"}'
npx @alook/daemon use server --invite '{"server":"demo#0042","friend":"alice#0042"}'
npx @alook/daemon use machine --list
npx @alook/daemon use friend --list
npx @alook/daemon use server --list
npx @alook/daemon use message --send --target '/demo#0042/general' --as me --file './message.txt'
npx @alook/daemon use message --send --target '/.dm/alice#0042' --as '@Helper#0042' --file './message.txt'

Run npx @alook/daemon use expire to expire the supplied access code immediately. It needs no extra permission or remaining quota and affects only that code. Later requests, including repeated expiry, are refused. Expired codes leave the active list; their records remain stored. Returning to Automation refreshes the list.

These are examples. Use an existing machine ID owned by the code's user, an available backend, and a supported model. Optional bot fields are description and reasoningEffort. Bot creation keeps the same machine readiness, model and plan checks as the web form. The code grants each selected action directly. Machine, friend and server listing each require their own Read permission; other account actions are unavailable. Creating a server does not add bots to it.

The invite command sends the normal server invitation link to an accepted friend by DM; it does not force that person to join. It accepts no arbitrary message content. The caller must be a member of that server. Server and friend arguments use the full name#NNNN handles returned by the list commands.

message --send requires the default-off message.send Write permission. It sends the UTF-8 file contents, preserving line breaks, rather than attaching the file or reading stdin. --as me selects the code owner; --as @name#NNNN selects an eligible bot owned by that same human. Other identities are rejected. Target refs and permissions are resolved for the selected sender through the existing send functions. Bot unread alignment, CAS and duplicate checks remain; this command does not pull its inbox. The standalone access-code path does not execute the bot credential proxy's local PII filters.

Read responses are deliberately small: machines return id/name/status/backends (machines have no name#NNNN handle); friends return name/handle/is_my_bot; servers return name/handle/channels. The Automation API applies this projection before returning data.

Each command returns one JSON line, for example:

{"success":{"bot":{"id":"b1","handle":"Helper#0042"}}}
{"success":{"server":{"id":"s1","handle":"demo#0042","url":"https://alook.ai/c/channels/s1"}}}

A code's seven permissions count separately. Rates use the preceding hour or 24 hours, with a 64/hour cap per code and permission even for a day rate. Valid authenticated requests admitted by the limiter consume a request even if business validation or creation subsequently fails. Invalid basic input and over-limit requests do not consume another request. Policy edits preserve recent history. A denied request returns an error; HTTP 429 includes a retry interval.

Write POSTs are sent once. If the connection fails, a creation, invitation or message may already have succeeded; inspect Alook before trying again. use never automatically repeats a write request.

use server --list includes each server's participating top-level channels, with name, type (text or forum) and canonical ref. It uses server.read: public channels follow server membership; private channels require the code owner's creator or access membership. Threads and messages are not returned. Servers with no visible top-level channels keep an empty channels array.

For local development only, pass use --server-url http://localhost:<port> … or set ALOOK_SERVER_URL. Remote origins require HTTPS. use does not follow HTTP redirects with the access code.

版本与发布记录

@alook/daemon 共发布 74 个版本,首次发布于 2026/07/15;过去 12 个月发布了 74 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.1.65latest2026/11/07今天
0.1.642026/11/052 天前
0.1.632026/11/025 天前
0.1.622026/10/317 天前
0.1.612026/10/309 天前
0.1.602026/10/2711 天前

关于 @alook/daemon 的常见问题

@alook/daemon 是什么?

Alook agent daemon — host-side runtime backend, process manager, credential proxy, and control plane.

如何安装 @alook/daemon?

在项目目录运行 npm install @alook/daemon,或使用 pnpm add / yarn add / bun add。它还提供命令行工具 alook-daemon,可以用 npx alook-daemon 直接运行。

@alook/daemon 支持 TypeScript 吗?

该包的 package.json 没有声明类型入口。需要类型提示时,可以在 npm 上查找社区维护的 @types/alook__daemon 是否存在。

@alook/daemon 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@alook/daemon 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=20.9。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@alook/daemon 可以商用吗?

@alook/daemon 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@alook/daemon 的下载量有多少?

根据 npm 官方下载统计,@alook/daemon 在 2026/10/06 – 2026/11/04 这 30 天内被下载 3,145 次,日均约 105 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@alook/daemon 还在维护吗?

最新版本 0.1.65 发布于 2026/11/07(今天);至今共发布 74 个版本,其中过去 12 个月发布了 74 个;首次发布于 2026/07/15。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。