Metadata-Version: 2.4
Name: workertower-client
Version: 0.6.2
Summary: Framework-neutral client runtime for WorkerTower
License-Expression: MIT
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: jsonschema<5,>=4.23
Provides-Extra: dev
Requires-Dist: pytest<10,>=8; extra == "dev"
Requires-Dist: ruff<0.16,>=0.15; extra == "dev"
Dynamic: license-file

# WorkerTower Client

Framework-neutral local runtime for consuming assignments from WorkerTower
Server. Codex is one adapter; it is not part of the protocol contract.

## Boundaries

| Component | Responsibility |
|---|---|
| Server | Select projects, Subflow（子流程）Steps（步骤）, roles, prompts, and task objectives |
| Client | Persist execution evidence, renew leases, and deliver results reliably |
| Adapter | Map the server identity and objective to framework-native prompt layers |

The client never creates or extends runtime prompts. The identity layer is the
exact server `PromptSnapshot.content`. The objective layer contains the task
envelope, including `constraints.output_locale`. Runtime user-visible prose must
come from the server assignment and use that locale. Client-generated output is
limited to stable protocol and status codes.

## Configuration

Install the released Client CLI（客户端命令行）from PyPI:

```bash
uv tool install workertower-client
workertower --help
workertower --version
```

For repository development only:

```bash
uv tool install ./client
```

Initialize and pair a new Worker（工作节点）with the local Community Server at
`http://127.0.0.1:8000`:

```bash
workertower init
```

Select a self-hosted Community Server（社区版服务端）explicitly:

```bash
workertower init --server-url https://community.example.com
```

`init` is an idempotent Worker-first（工作节点优先）state machine. On first
use it creates non-secret local configuration, generates a short-lived pairing
session, and returns only a verification URL, pairing code, complete QR payload
URL, and expiry. Open the URL in Server Web（服务端网页）and approve the Worker,
then run the same command again:

```bash
workertower init
# Explicit but equivalent:
workertower init --resume
```

While approval is outstanding the status is `PAIRING_PENDING`. The approval
page can optionally grant Device Operator（设备操作员）access to this trusted
machine. After approval,
the Client validates the authenticated Agent Client Profile（智能体客户端资料）,
calibrates the Server client key, validates capabilities, and synchronizes
configuration. When Operator access was granted, `init` also returns all
Server Projects as read-only discovery data so the local LLM can immediately
offer an existing-project binding or a new-project flow. A newly paired Worker with no projects returns
`PAIRED_NO_PROJECTS`; this is a successful state, not an error.

No Project（项目）is required to pair a Worker. After the Skill has inspected the
local repository and the user confirms its proposed project JSON, create the
Project and local binding together:

```bash
workertower project-create \
  --project /private/path/project.json \
  --path /local/repository
```

The project JSON may contain an `initial_workflow` object for the built-in
`code_maintenance_review` Workflow（代码维护审查工作流）. Omitting the field
accepts the Server default; `"initial_workflow": null` explicitly opts out.
The Client preserves this selection but never chooses the Workflow's Subflows（子流程）or
Steps（步骤）. Successful project creation returns `READY` plus `workflow_id`
when a Workflow was created. The repository path remains local and is never
sent as Server project configuration. The generated candidate
Worker and Operator candidate credentials are generated independently and
stored in separate macOS Keychain（钥匙串）items before pairing
and never appear in stdout, command arguments, config, or state files. The
Operator candidate is deleted when the approval does not grant that access.
The retained Operator credential is read only by explicit conversational
control-plane commands; scheduled execution, Claim, Adapter, Task Envelope,
Heartbeat, and Completion paths can read only the Worker credential. The
private `device_code` is held temporarily in a mode `0600` pairing record for
crash recovery and deleted only after authenticated profile validation and
configuration sync succeed.

Fresh bare initialization uses `http://127.0.0.1:8000`. Existing
configuration always resumes its saved Server URL and rejects an implicit
cross-Server migration. Pass `--server-url https://server.example.com`, or
`--server-url http://localhost:8000` when both self-hosted components run
locally. Remote URLs require HTTPS（加密连接）. Pairing currently supports the
`codex` adapter only and
returns `PAIRING_ADAPTER_UNSUPPORTED` for other adapters without falling back to
a manual token flow.
`connect --token-stdin` remains available only as a compatibility path for
Servers that issued a legacy setup token. It validates the Framework（框架）and
replaces the temporary identity with the registered Server client key before
configuration synchronization. It never embeds or prints an API Token（接口令牌）.

Reinstall after a protocol-version upgrade. Server and Client must both support Task Envelope schema `1.0`.

Configuration files contain no credentials:

```json
{
  "server_url": "http://127.0.0.1:8000",
  "client_instance_id": "workstation-01",
  "state_root": "~/.workertower",
  "adapter_name": "codex",
  "keychain_account": "workstation-01",
  "operator_keychain_account": "workstation-01:operator",
  "config_keychain_account": "workstation-01:config",
  "max_concurrent_assignments": 5,
  "capabilities": [
    "repository_read",
    "repository_write",
    "task_record_write",
    "git_worktree",
    "test_execute"
  ]
}
```

Client `0.6.2` defaults `max_concurrent_assignments` to `5`. The effective
capacity is `min(Client limit, Server max_concurrent_runs)` only when both
sides advertise `concurrent_assignments_v1`; an old or incomplete Server
profile fails safe to one Assignment. Override the local ceiling with the
non-secret configuration field or `WORKERTOWER_MAX_CONCURRENT_ASSIGNMENTS`. `status`
reports configured, Server, effective, active, and remaining capacity without
exposing local repository paths.

Set `WORKERTOWER_API_TOKEN` or configure `WORKERTOWER_KEYCHAIN_ACCOUNT`. Tokens are only read
through the environment or macOS Keychain credential provider. The normal
execution credential reads and acknowledges configuration; it must not submit
configuration changes. Use the separate optional `WORKERTOWER_CONFIG_API_TOKEN` or
`WORKERTOWER_CONFIG_KEYCHAIN_ACCOUNT` credential for `config:write`. A copied one-time
configuration token can be stored without shell history through
`pbpaste | workertower connect --config-token-stdin`.
The optional Device Operator credential uses `WORKERTOWER_OPERATOR_KEYCHAIN_ACCOUNT`.
It is never used as a fallback for the Worker or configuration credential.
After revocation, `operator-access-regrant` creates a separate short-lived
candidate in a derived Keychain item and sends only its prefix and SHA-256 hash
to the Server. Approval promotes that candidate into the Operator Keychain item
without changing the Worker identity or Bindings; denial and expiry delete it.
If final local cleanup is interrupted, a secret-free promoted record verifies
the primary Keychain hash before retrying cleanup; an old revoked token cannot
be mistaken for the new credential.
The command prints the approval URL/code and polls automatically, so the user
does not need to return and run a second command. `--no-wait` is available only
for scripted one-poll workflows.

When the Server requires User Presence（用户在场）, an Operator write receives
`428 USER_PRESENCE_REQUIRED`. The Client creates a challenge bound to the exact
HTTP method, path, and raw JSON body hash, prints only its Web URL/code to
stderr, polls automatically, and retries the unchanged write once with an
in-memory one-time header. Reads and scheduled Worker operations never enter
this flow, and the presence secret is neither returned by the Server nor stored
on disk or printed.

Worker transfer uses an explicit, short-lived, single-use Transfer Ticket（迁移
票据）, never the active Worker Bearer Token. The destination generates its own
candidate, sends only its prefix/hash, and promotes it after a successful or
idempotently repeated consume. Server-side Worker ID and Bindings are retained;
the old Worker, Operator, and `config:write` credentials are revoked. Consumption
requires `--ticket-stdin`, so the ticket never enters shell history or the
process list.
Pause the source scheduler and let its active Assignment finish before issuing
the ticket. A live Lease makes consumption fail safely with
`WORKER_TRANSFER_ACTIVE_ASSIGNMENT`; the Server never cuts off heartbeat or
completion for in-progress work.
An interrupted post-commit cleanup is resumed only after both the saved
`client_key` and primary Worker Keychain hash match the secret-free local
commit record; the Server consume request is not repeated.
If a parent environment still injects any revoked credential, the command
returns only the affected variable names under `external_cleanup_required`;
remove those overrides before the next Worker operation.
Remote Server URLs must use HTTPS. Plain HTTP is accepted only for loopback
hosts such as `localhost`, `127.0.0.1`, and `::1`. The client refuses a
`state_root` at or below `~/.workbuddy`.

Production clients should obtain credentials through macOS Keychain or a
daemon credential broker. Git subprocesses receive a scrubbed environment with
token-, lease-, secret-, password-, credential-, and private-key-shaped values
removed. The process that launches the execution Agent must apply the same
boundary so the Agent never inherits the Server API token or lease secrets.

## Commands

```text
workertower --config client.json claim
workertower init
workertower init --server-url https://community.example.com
workertower --config client.json init [--resume]
workertower --config client.json project-create --project project.json --path /local/repository
workertower --config client.json connect
workertower --config client.json bind --binding-id <server-binding-id> --path /local/repository --worktree-root /local/worktrees
workertower --config client.json config-manifest
workertower --config client.json config-pull --project-id <project-id>
workertower --config client.json config-show [--project-id <project-id>]
workertower --config client.json config-change-queue --project-id <project-id> --patch patch.json --confirmed --confirmed-by <actor>
workertower --config client.json config-push
workertower --config client.json config-ack --project-id <project-id>
workertower --config client.json config-sync
workertower --config client.json config-conflict-resolve --request-id <request-id> --accept-server
workertower --config client.json heartbeat --assignment-id <id> --execution-token <token>
workertower --config client.json artifact-register --assignment-id <id> --execution-token <token> --artifact-type <type> --name <relative-name> --source <file>
workertower --config client.json artifact-register ... --review-content
workertower --config client.json complete --assignment-id <id> --execution-token <token> --result result.json
workertower --config client.json outbox-flush
workertower --config client.json outbox-retry --completion-id <id>
workertower --config client.json outbox-resolve-stale [--completion-id <id>]
workertower --config client.json reconcile
workertower --config client.json resume --assignment-id <id> --execution-token <token>
workertower --config client.json status
workertower --config client.json status --local-only
workertower --config client.json operator-status
workertower --config client.json project-list [--limit <count>]
workertower --config client.json operator-project-create --project project.json --confirmed --confirmed-by <actor>
workertower --config client.json project-update --project-id <id> --patch patch.json --config-hash <sha256> --config-revision <revision> --confirmed --confirmed-by <actor>
workertower --config client.json worker-list [--limit <count>]
workertower --config client.json worker-update --worker-id <id> --enabled <true|false> --confirmed --confirmed-by <actor>
workertower --config client.json operator-access-show --worker-id <id>
workertower --config client.json operator-access-revoke --worker-id <id> --confirmed --confirmed-by <actor>
workertower --config client.json operator-access-regrant [--no-wait] --confirmed --confirmed-by <actor>
workertower --config client.json user-presence-policy-show --worker-id <id>
workertower --config client.json user-presence-policy-update --worker-id <id> --required <true|false> --confirmed --confirmed-by <actor>
workertower --config client.json worker-transfer-create --confirmed --confirmed-by <actor>
workertower --config client.json worker-transfer-consume --ticket-stdin --confirmed --confirmed-by <actor>
workertower --config client.json binding-list [--worker-id <id>] [--project-id <id>] [--limit <count>]
workertower --config client.json binding-create --worker-id <id> --project-id <id> [--path /local/repository] --confirmed --confirmed-by <actor>
workertower --config client.json binding-update --binding-id <id> --enabled <true|false> --confirmed --confirmed-by <actor>
workertower --config client.json task-list
workertower --config client.json task-create --task task.json [--idempotency-key <key>] --confirmed --confirmed-by <actor>
workertower --config client.json task-clear-affinity --task-id <id> --confirmed --confirmed-by <actor>
workertower --config client.json run-list [--project-id <id>]
workertower --config client.json sync-conflict-list
workertower --config client.json sync-conflict-resolve --request-id <id> --accept-server --confirmed --confirmed-by <actor>
workertower --config client.json project-config-history --project-id <id>
workertower --config client.json project-sync-status --project-id <id>
workertower --config client.json pairing-show --user-code <code>
workertower --config client.json pairing-approve --user-code <code> [--grant-operator-access] --confirmed --confirmed-by <actor>
workertower --config client.json pairing-deny --user-code <code> --confirmed --confirmed-by <actor>
workertower --config client.json human-gate-show --task-id <id>
workertower --config client.json human-gate-decide --task-id <id> --decision <approve|dismiss|request_revision> [--reason <text>] --confirmed --confirmed-by <actor>
workertower legacy-export --output legacy-bundle.json
workertower --config client.json uninstall --yes
```

Pass a Transfer Ticket through standard input; do not put it in shell history.
For a new destination, add `--server-url https://community.example.com` and a
new `--config` path to the same `--ticket-stdin` command.

`uninstall --yes` is a destructive local reset. Stop and remove every external
cron / framework trigger first. The command acquires the Client locks, removes
all recorded Git Worktrees even when dirty, deletes Client configuration,
current and default legacy state, and the configured Worker, Operator, and
`config:write` Keychain items, including any pending Operator re-grant and
Worker-transfer candidate items.
It preserves project repositories, branches, commits, and Server records. A
custom `state_root` is cleaned by known Client-owned entry rather than blindly
deleting unknown sibling files. Environment-injected credentials and scheduler
definitions are reported by name for removal through their external owner.
The executable and Skill remain installed; remove those packages separately
through their original installer only when full software removal is requested.

`complete` accepts a semantic Result Draft（结果草稿）and deterministically fills
`schema_version`, assignment ID, output locale, start/completion timestamps, and
the registered Artifact Index（产物索引）before validating and writing the
canonical request to `outbox/pending`. The LLM supplies only `outcome`,
`summary`, Step-specific `outputs`, and any explicit remote artifact references.
Retries reuse the same `completion_id`. Transient failures remain pending;
permanent failures and exhausted delivery retries move to `outbox/dead-letter`.
An explicit Server rejection with `state_changed=false` moves to
`outbox/rejected`: deterministic-field failures are rebuilt under a new
Completion ID with a persisted supersession link, while semantic failures return
`retry_action=repair_result` for at most two LLM repairs. Exhaustion returns
`retry_action=stop_for_human`. Network/timeout delivery returns
`retry_action=retry_unchanged` and never mutates the queued request.
Persistent dead-letter records are Assignment-scoped. The affected Assignment
retains its capacity slot and local resource ownership and reports
`COMPLETION_DEAD_LETTER_REQUIRES_ATTENTION`, while unrelated Assignments may
still be claimed when negotiated capacity remains. For a completion rejected
with a stale assignment or lease code, `claim` queries authoritative assignment
state and, only when `lease_active=false` is confirmed, moves it to
`outbox/abandoned`, releases the local execution session and resource, and
continues. This is not a successful receipt; the complete rejection and
resolution evidence remain available for audit. An unavailable, ambiguous, or
active lease leaves only that Assignment blocked. `outbox-resolve-stale`
exposes the same fail-closed reconciliation for an operator-selected Completion
ID.
Before enqueueing, the client validates the result against the pinned outcome,
locale, and artifact contract. Every produced `client://` artifact must exist as
a regular local file and match its required SHA-256 digest.

CLI failures use a secret-safe JSON error contract with stable `error_code`,
`stage`, `operation`, `retry_class`, `retryable`, `retry_action`, locally
generated `remediation`, and `exit_code` fields. Filesystem diagnostics expose
only a `path_kind` and errno name, never a raw local path. Server diagnostics
retain well-formed UUID request IDs and only schema-allowlisted field-error
`path`, `code`, and `owner` values. Server-supplied `stage` is ignored;
Server-supplied `remediation` and `field_errors.message` are never copied.
Unknown remote error codes normalize to `PROTOCOL_ERROR`. Tokens, Device Codes,
lease data, and arbitrary remote details are never copied into error output or
Outbox diagnostics.

The HTTP transport accepts at most 16 MiB for a successful JSON response and
1 MiB for an error response. It fails closed with `RESPONSE_TOO_LARGE` before
parsing an oversized body. Same-origin redirects remain supported; a redirect
to a different scheme, host, or port is rejected before credentials or request
content can be forwarded. Redirect response bodies are also bounded and every
handled response is closed on success or failure.

`artifact-register` always records safe deterministic metadata (`artifact_id`,
type, relative `name`, canonical URI, digest, size, and inferred media type).
It streams the copy and SHA-256 calculation instead of loading an ordinary
Artifact into memory as one byte string. The local last line of defense allows
at most 256 MiB per file, 1 GiB and 256 files per Assignment, while preserving
at least 256 MiB of free disk. Structured JSON Artifacts that require schema
validation are additionally limited to 16 MiB before hashing or parsing.
Artifact contents are not uploaded by default. Use
`--review-content` only when the type is
both required by the selected outcome and listed in the pinned Completion
Contract's `reviewable_artifact_types`; the
Client then fail-closed validates a non-empty, at-most-256-KiB UTF-8 Markdown,
text, or object/array JSON document and keeps its exact text for the Server
Human Gate（人工门禁）. The command never echoes the inline body.

`claim` first retries pending completions, pushes only explicitly confirmed local
configuration changes, compares Server Manifest（服务端清单）revision and SHA-256
hashes, pulls changed configuration snapshots, and acknowledges the applied
revision. Only then can it claim work. A transient sync failure or unresolved
configuration conflict prevents Claim（领取）instead of running with ambiguous
configuration.

`config-change-queue` is the only local configuration write entrypoint. It
requires an explicit `--confirmed` marker and records the actor separately from
runtime credentials. Scheduled runs can deliver existing confirmed records but
cannot create or confirm a change. A `409 Conflict（冲突）` is retained under
`config-sync/changes/conflicts` for the user to resolve; it is never silently
merged or overwritten.
Every later sync queries `/v1/config-sync/changes/{request_id}`. A remote
`accept_server` resolution triggers an automatic pull, acknowledgment, and
local archive before Claim（领取）can continue. An explicit local resolution first
closes the matching Server conflict with the separate `config:write` credential.
The explicit local
`config-conflict-resolve --accept-server` command remains available.

The Client hashes the canonical object containing both `config_schema_version`
and the complete `config` snapshot. It uses UTF-8 JSON with sorted keys,
compact separators, `ensure_ascii=false`, no trailing newline, and the
`sha256:` prefix. Comparing only the inner configuration object is invalid.

`claim` persists its Wake ID before the network call and reuses it after a lost response. It reconciles old local records before requesting new work. An assigned response includes a local `execution_token`; retain it for this wake's Client commands but never place it in Task Envelope, source code, reports, or artifacts. It also includes an `artifact_staging_path` outside the Worktree; generate reports there and register them from that path. A Step with `workspace_mutation=preserve` must not write generated files into `execution_path`.

`reconcile` compares local active runs with the authoritative server assignment
status. Active leases return the `RESUME` action; terminal or lost leases return
`STOP`. Resume also compares the local Task Envelope with the authoritative
Server `envelope_digest`; missing or mismatched integrity data returns
`INTEGRITY_FAILURE`. The client does not choose another project or role.
Concurrent `claim` calls may each receive one Assignment until the negotiated
capacity is full. The short global Claim lock protects only capacity recheck,
Server Claim, and durable admission; setup and execution remain outside it.
`busy` returns without an invocation or execution token when capacity is full
or the same Assignment already has an operation in progress. `resume` performs
the same online check and recreates the workspace resolution only when the
caller supplies the matching local execution token. If an Agent loses that
token, it must not take over an active lease; Server expiry and a later Claim（领取）provide safe recovery.

`init` and online `status` discover Worker-authorized Project Bindings from the
authenticated Server endpoint. `BINDING_REQUIRED` lists only non-sensitive
project metadata. With Device Operator access they also report Operator status
and all Server Projects for onboarding discovery. `status --local-only` never accesses credentials or the
network and validates existing Git roots locally.

`bind` first verifies that the opaque binding belongs to the authenticated
Worker and is enabled, then stores its mapping to a local Git repository root
and synchronizes configuration. The next Claim is blocked unless remote
authorization and every required local mapping are `READY`.
`binding-create` can authorize any Worker. When `--path` is supplied, the target
must be the current Worker: the Client validates the Git root before mutating
Server state, then creates the Server Binding, stores the local mapping, and
synchronizes configuration. If local persistence fails after Server success,
the result is `AUTHORIZED_LOCAL_BIND_REQUIRED` with the exact safe `bind`
recovery action; it never pretends the Server mutation was rolled back.
The local path never enters server configuration or the Task Envelope. Claim
persists the resolved workspace separately from the two framework prompt layers.
When `constraints.worktree_required` is `true` (the Server default), the client creates or resumes a
deterministic Git worktree keyed by `objective.task_id` and the binding. Every Step（步骤，协议名 `Pipeline Node`）assignment for that Task reuses the same path, branch, and first `base_commit`, so uncommitted handoff changes remain visible. When it is explicitly `false`, the bound path must be a Git root and execution is strictly read-only: Client snapshots HEAD and a content-sensitive status digest and rejects completion after any Git-visible change. Claim output names these values explicitly as
`execution_path` and `workspace_record_path`.
The worktree base defaults to `parameters.default_branch` from Server. Pass
`--default-branch <name>` to `bind` only when a client-local override is needed.

## Local state

```text
bindings/
onboarding/pairing.json  # temporary, private, removed after successful pairing
config-sync/mirrors/<project-id>.json
config-sync/changes/pending/<request-id>.json
config-sync/changes/acknowledged/<request-id>.json
config-sync/changes/conflicts/<request-id>.json
config-sync/changes/rejected/<request-id>.json
config-sync/changes/resolved/<request-id>.json
config-sync/acknowledgements/<project-id>.json
task-workspaces/<task-binding-hash>.json
assignments/<assignment-id>/envelope.json
assignments/<assignment-id>/control.json
assignments/<assignment-id>/execution.json
assignments/<assignment-id>/workspace.json
claims/pending.json
runs/<assignment-id>/manifest.json
runs/<assignment-id>/journal.jsonl
runs/<assignment-id>/result.json
runs/<assignment-id>/artifact-index.json
runs/<assignment-id>/completion-repair.json
runs/<assignment-id>/artifacts/
guards/no-push/pre-push
worktrees/<task-slug>-<task-binding-hash-prefix>/
outbox/pending/
outbox/acknowledged/
outbox/rejected/
outbox/dead-letter/
outbox/abandoned/
declines/pending/
declines/acknowledged/
declines/rejected/
resources/owners/
resources/assignments/
```

JSON state uses atomic replacement and private file permissions. Journal events
use append-only JSON Lines. The short Claim admission section uses one
inter-process File Lock（进程间文件锁）; Outbox delivery, finalization, decline,
and execution operations use per-Assignment locks. Persistent resource ownership
prevents two Assignments from sharing one Task Worktree（任务工作树）or direct
repository. The durable execution session survives the short CLI process: only
the wake holding that Assignment's `execution_token` can heartbeat, register
artifacts, resume, or complete. Its handoff is persisted as `prepared` before
the assigned response is written and changed to `delivered` only after stdout
is flushed, so a crash can replay the same Wake ID（唤醒编号）and token without
creating a second owner.

The client verifies the canonical Task Envelope SHA-256 and the embedded
PromptSnapshot content SHA-256 on claim, persistence, load, and immediately
before Adapter mapping. Lease and control data never enter either prompt layer.

`workspace.json` records `repository_path`, `execution_path`, `kind`, Task ID（任务编号）, the deterministic worktree `branch`, `base_ref`, and `base_commit`; direct read-only records also include `baseline_head` and `baseline_status_digest`. The Client installs a worktree-local `pre-push` guard when Push（推送）is forbidden and validates commit, merge, changed-file, and patch-line constraints before completion. The guard is a bypassable accidental-push Guardrail（防误操作护栏）, not strong enforcement against `git push --no-verify`; strong enforcement requires a controlled Git Gateway（Git 网关）or OS / network Sandbox（操作系统／网络沙箱）. Local paths remain client-only and are not
inserted into the saved Task Envelope or adapter prompt payload.

## Legacy export

`legacy-export` reads the former `ai-team-config.json` and `ai-team-state`
directories without modifying them or contacting Server:

```text
workertower legacy-export \
  --legacy-config ~/.codex/ai-team-config.json \
  --legacy-state-root ~/.codex/ai-team-state \
  --output ./legacy-bundle.json
```

The versioned bundle contains projects, client-local bindings, human tasks,
backlogs, and historical run/artifact metadata. Repository paths appear only in
`client_bindings`; artifact content and local artifact paths are not exported.
Credential-, token-, lease-, and secret-shaped fields are removed.

The bundle declares `source.authority = "none"` and
`source.scheduler_state_imported = false`. Old decisions, scheduler scores,
locks, leases, and active-run ownership never become new assignments or Server
authority. Project configuration snapshots require explicit review before any
future import.

## License（许可证）

本仓库采用 [MIT License（MIT 许可证）](LICENSE)。官方云订阅能力和企业版扩展不包含在本开源仓库中。
