This commit is contained in:
+113
-119
@@ -10,24 +10,29 @@ uv sync --locked --all-packages
|
||||
## Local SQLite
|
||||
|
||||
SQLite is intended for local development and tests with one Cloud API process.
|
||||
Configure public and host credentials even in local mode because anonymous
|
||||
development access cannot authorize a host identity.
|
||||
The Cloud API uses persistent user sessions for human access and a Host-generated
|
||||
secret for later Host operations; no static bearer credential configuration is
|
||||
required.
|
||||
|
||||
```powershell
|
||||
$env:CLOUD_ENVIRONMENT = "local"
|
||||
$env:CLOUD_DATABASE_URL = "sqlite:///cloud/cloud.sqlite3"
|
||||
$env:CLOUD_PUBLIC_CREDENTIALS_JSON = '[{"principal_id":"local-sdk","token":"replace-public-token","scopes":["tasks:submit","tasks:read","pool:read","plugins:read","plugins:admin"]}]'
|
||||
$env:CLOUD_HOST_CREDENTIALS_JSON = '[{"principal_id":"local-host","token":"replace-host-token","scopes":[],"host_id":"host-local"}]'
|
||||
$env:CLOUD_SESSION_COOKIE_SECURE = "false"
|
||||
uv run --package device-cloud-api device-cloud-api --host 127.0.0.1 --port 8001
|
||||
```
|
||||
|
||||
In a second terminal, start the Host Agent with the matching host identity and
|
||||
token:
|
||||
Create the first administrator interactively, then open the Console and sign
|
||||
in with that account:
|
||||
|
||||
```bash
|
||||
uv run --package device-cloud-api device-cloud-admin users create --username admin --display-name "Local Administrator" --role admin
|
||||
```
|
||||
|
||||
In a second terminal, configure the local Host Agent to reach this Cloud API:
|
||||
|
||||
```powershell
|
||||
$env:HOST_AGENT_CONTROL_PLANE_URL = "http://127.0.0.1:8001"
|
||||
$env:HOST_AGENT_HOST_ID = "host-local"
|
||||
$env:HOST_AGENT_TOKEN = "replace-host-token"
|
||||
uv run --package device-host-agent device-host-agent setup
|
||||
uv run --package device-host-agent device-host-agent
|
||||
```
|
||||
|
||||
@@ -41,66 +46,23 @@ defaults to `./tasks`.
|
||||
The Host Agent only initiates outbound HTTP requests. It does not expose an
|
||||
inbound port.
|
||||
|
||||
## Managed Edge Enrollment
|
||||
|
||||
New edge installations do not need a pre-coordinated Host or device ID. The
|
||||
Cloud API accepts configured one-time enrollment credentials:
|
||||
|
||||
```powershell
|
||||
$env:CLOUD_ENROLLMENT_TOKENS_JSON = '[{"principal_id":"edge-installer","token":"replace-with-a-long-random-one-time-token"}]'
|
||||
```
|
||||
|
||||
On the edge Host, omit `HOST_AGENT_HOST_ID` and `HOST_AGENT_TOKEN` and provide
|
||||
the enrollment token only for the first successful enrollment:
|
||||
|
||||
```bash
|
||||
export HOST_AGENT_CONTROL_PLANE_URL="https://cloud.example.com"
|
||||
export HOST_AGENT_ENROLLMENT_TOKEN="replace-with-a-long-random-one-time-token"
|
||||
export HOST_AGENT_IDENTITY_PATH="tasks/host_identity.json"
|
||||
export HOST_AGENT_DISPLAY_NAME="Edge Mac 01"
|
||||
uv run --package device-host-agent device-host-agent
|
||||
```
|
||||
|
||||
Before its first request the Host Agent creates `HOST_AGENT_IDENTITY_PATH` with
|
||||
an instance identifier and long-lived random Host secret. The cloud consumes
|
||||
the enrollment token, assigns `host_id`, stores only credential digests, and
|
||||
returns the assigned ID. The Host Agent then enrolls each record from
|
||||
`tasks/device_config.sqlite3`, stores its cloud-assigned `device_id` in that
|
||||
database, connects the resulting devices, and starts heartbeat/claim loops.
|
||||
|
||||
Keep the identity file and device configuration database on persistent edge
|
||||
storage with permissions limited to the service account. The identity file is
|
||||
a bearer secret: do not put it in an image, repository, log, or general backup.
|
||||
After successful enrollment, remove `HOST_AGENT_ENROLLMENT_TOKEN` from the edge
|
||||
environment. An intact identity file is sufficient for restart; if only a
|
||||
device mapping is lost, device enrollment reconstructs the same cloud ID.
|
||||
|
||||
Enrollment tokens are one-time even when they remain in Cloud API environment
|
||||
configuration: their consumed digest is stored in the database. Reusing a token
|
||||
for another edge instance returns a conflict. Create a distinct token for every
|
||||
edge installation.
|
||||
|
||||
Explicit `HOST_AGENT_HOST_ID` plus `HOST_AGENT_TOKEN` takes precedence and keeps
|
||||
the previous legacy behavior, including locally selected device IDs. This is
|
||||
the rollback and staged-migration path for existing deployments.
|
||||
|
||||
## Self-Service Edge Enrollment (Zero-Token)
|
||||
## Direct Edge Enrollment
|
||||
|
||||
The Host Agent's default `HOST_AGENT_CONTROL_PLANE_URL` is
|
||||
`https://amcp.home.jerryyan.top`. This is a single-operator home deployment
|
||||
default; override the environment variable for local/dev/test runs pointed at
|
||||
a different Cloud API.
|
||||
|
||||
When `HOST_AGENT_ENROLLMENT_TOKEN` is not set and no cached identity exists,
|
||||
the Host Agent enrolls with no bearer credential at all. The Cloud API only
|
||||
accepts that request when `CLOUD_SELF_SERVICE_ENROLLMENT_ENABLED=true`
|
||||
(default `false`); a configured enrollment token, if presented, always takes
|
||||
priority over self-service. This trades away any approval step: **any caller
|
||||
that can reach the control-plane URL can register itself as a new Host.**
|
||||
There is no rate limiting or throttling on this path by design — the intended
|
||||
mitigation is network-perimeter control (firewall/reverse-proxy access to the
|
||||
URL), not an in-process limiter. Only enable the flag on a deployment where
|
||||
that network boundary is enforced.
|
||||
When no cached identity exists, the Host Agent creates a random instance
|
||||
identifier and Host secret, registers directly with the Cloud API, persists the
|
||||
assigned `host_id`, and uses that secret for all later device enrollment,
|
||||
heartbeat, claim, renewal, and result calls. The Cloud stores only the secret
|
||||
digest. There is no configured enrollment-token or static Host-credential path.
|
||||
|
||||
This trades away any approval step: **any caller that can reach the control
|
||||
plane can register itself as a new Host.** There is no rate limiting or
|
||||
throttling on this path by design; restrict access at the firewall or reverse
|
||||
proxy before exposing the endpoint.
|
||||
|
||||
Before the Host Agent's first unattended start, create the one-time local
|
||||
operator account interactively:
|
||||
@@ -124,6 +86,54 @@ docker compose run --rm host-agent device-host-agent setup
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Keep `HOST_AGENT_IDENTITY_PATH`, the local-account file, and
|
||||
`tasks/device_config.sqlite3` on persistent storage with permissions limited to
|
||||
the service account. The identity file is a bearer secret: do not put it in an
|
||||
image, repository, log, or general backup.
|
||||
|
||||
### Local Web Console
|
||||
|
||||
The Host Agent can optionally serve a small local-only web console on the
|
||||
edge machine: heartbeat/enrollment status, registered local devices, current
|
||||
assignment progress, local device add/edit/remove, a local account password
|
||||
change, and recent assignment/heartbeat history. It authenticates with the
|
||||
same local account created by `device-host-agent setup` above — there is no
|
||||
separate console credential.
|
||||
|
||||
```text
|
||||
HOST_AGENT_CONSOLE_ENABLED=false
|
||||
HOST_AGENT_CONSOLE_BIND_HOST=127.0.0.1
|
||||
HOST_AGENT_CONSOLE_PORT=8765
|
||||
HOST_AGENT_CONSOLE_ALLOW_NON_LOOPBACK=false
|
||||
HOST_AGENT_CONSOLE_SESSION_TTL_SECONDS=43200
|
||||
HOST_AGENT_CONSOLE_HISTORY_LIMIT=200
|
||||
```
|
||||
|
||||
- `HOST_AGENT_CONSOLE_ENABLED` — starts the console when `true`; disabled by
|
||||
default, so existing deployments see no new listening port.
|
||||
- `HOST_AGENT_CONSOLE_BIND_HOST` — the address the console binds to; defaults
|
||||
to loopback-only.
|
||||
- `HOST_AGENT_CONSOLE_PORT` — the TCP port the console listens on.
|
||||
- `HOST_AGENT_CONSOLE_ALLOW_NON_LOOPBACK` — required opt-in before
|
||||
`HOST_AGENT_CONSOLE_BIND_HOST` may be a non-loopback address; the Host
|
||||
Agent refuses to start otherwise.
|
||||
- `HOST_AGENT_CONSOLE_SESSION_TTL_SECONDS` — sliding idle timeout, in seconds,
|
||||
for an authenticated console session.
|
||||
- `HOST_AGENT_CONSOLE_HISTORY_LIMIT` — number of recent assignment/heartbeat
|
||||
entries the console retains before pruning older ones.
|
||||
|
||||
Treat `HOST_AGENT_CONSOLE_ALLOW_NON_LOOPBACK` as an explicit,
|
||||
operator-accepted risk: the console has no built-in TLS and no rate
|
||||
limiting, so a non-loopback bind exposes an unencrypted login form to
|
||||
whatever network can reach that port. To reach the console from another
|
||||
machine instead, keep it bound to loopback and open an SSH local
|
||||
port-forward to the edge machine:
|
||||
|
||||
```bash
|
||||
ssh -L 8765:127.0.0.1:8765 user@edge-host
|
||||
# then open http://127.0.0.1:8765 from the local browser
|
||||
```
|
||||
|
||||
## PostgreSQL Deployment
|
||||
|
||||
Start from `.env.example`, replace every `change-me-*` value, and keep the
|
||||
@@ -151,10 +161,10 @@ the pytest suite (`-m "not integration"`), builds the image from the same
|
||||
`device-host-agent --help`), and pushes `<REGISTRY>/<IMAGE_NAME>:<BUILD_NUMBER>-<git short sha>`
|
||||
plus `:latest` to the configured registry.
|
||||
|
||||
`compose.deploy.yaml` is the same three-service stack as `compose.yaml`
|
||||
except `cloud-api` and `host-agent` reference `image:` instead of `build:`.
|
||||
Set `REGISTRY`, `IMAGE_NAME`, and `IMAGE_TAG` (see `.env.example`) to the tag
|
||||
Jenkins published, then deploy without a local build step:
|
||||
`compose.deploy.yaml` contains only PostgreSQL and the Cloud API; Host Agents
|
||||
run at their edge sites rather than beside the Cloud API. It references the
|
||||
fixed Jenkins registry image and interpolates only `IMAGE_TAG` (see
|
||||
`.env.example`). Deploy without a local build step:
|
||||
|
||||
```bash
|
||||
docker compose -f compose.deploy.yaml pull
|
||||
@@ -172,36 +182,22 @@ uv run --package device-cloud-api device-cloud-api --host 0.0.0.0 --port 8001
|
||||
```
|
||||
|
||||
Production startup requires `CLOUD_ENVIRONMENT=production`, a current schema,
|
||||
and at least one configured bearer credential.
|
||||
and HTTPS before browser sessions are exposed. It does not require credential
|
||||
JSON. Create the first administrator interactively after startup, before
|
||||
opening the Console to operators.
|
||||
|
||||
## Credentials And Scopes
|
||||
## Authentication And Host Identities
|
||||
|
||||
`CLOUD_PUBLIC_CREDENTIALS_JSON` is a JSON array of public API principals. Grant
|
||||
only the scopes required by each integration:
|
||||
Cloud users are the human authorization boundary. Their `viewer`, `operator`,
|
||||
and `admin` roles map to the existing API scopes, and the browser sends an
|
||||
`HttpOnly` session cookie plus CSRF proof for unsafe operations. The deployment
|
||||
does not accept `CLOUD_PUBLIC_CREDENTIALS_JSON`,
|
||||
`CLOUD_HOST_CREDENTIALS_JSON`, or `CLOUD_ENROLLMENT_TOKENS_JSON`.
|
||||
|
||||
- `tasks:submit`: submit tasks.
|
||||
- `tasks:read`: read task status and failure metadata.
|
||||
- `pool:read`: list hosts and devices.
|
||||
- `plugins:read`: list installed plugin registrations.
|
||||
- `plugins:admin`: register installed plugin entry points.
|
||||
|
||||
`CLOUD_HOST_CREDENTIALS_JSON` contains Host Agent principals. Every entry must
|
||||
include exactly one `host_id`; its token is valid only for heartbeat, claim,
|
||||
renewal, and result operations for that host.
|
||||
|
||||
`CLOUD_ENROLLMENT_TOKENS_JSON` contains bootstrap principals with only
|
||||
`principal_id` and `token`. These credentials cannot submit tasks, read the
|
||||
pool, or operate as a Host; they can only create one durable Host binding.
|
||||
Use high-entropy values generated by the deployment secret manager.
|
||||
|
||||
`CLOUD_SELF_SERVICE_ENROLLMENT_ENABLED` (default `false`) additionally accepts
|
||||
Host enrollment requests with no bearer token at all — see
|
||||
[Self-Service Edge Enrollment](#self-service-edge-enrollment-zero-token).
|
||||
|
||||
Do not place bearer tokens in command history, image layers, Compose files, or
|
||||
logs. Use environment injection or the deployment platform's secret manager.
|
||||
Rotate a token by deploying the updated Cloud API credential set and Host Agent
|
||||
configuration together.
|
||||
A fresh Host sends its generated candidate secret only during direct
|
||||
registration. The Cloud stores its digest and returns a `host_id`; later Host
|
||||
operations use that secret and are strictly bound to the returned `host_id`.
|
||||
Protect the persisted Host identity file as a bearer secret.
|
||||
|
||||
Dynamically enrolled Host credentials are stored as digests in the cloud
|
||||
database. This release exposes repository-level revocation rather than a public
|
||||
@@ -230,12 +226,10 @@ PY
|
||||
## Cloud Console (Web UI)
|
||||
|
||||
The repository ships an independent Vue 3 + Vite SPA at `cloud-console/` that
|
||||
renders task history, devices, hosts, plugins, and the user directory. Human
|
||||
operators sign in with a username and password; the Cloud API creates an
|
||||
expiring, revocable `HttpOnly` session cookie and uses a separate CSRF
|
||||
cookie/header for writes. Existing bearer tokens remain available through the
|
||||
Console's explicit **Use API token** action and for SDK, Host Agent, and
|
||||
automation compatibility.
|
||||
renders task history, devices, hosts, and plugins. Human operators sign in with
|
||||
a username and password; the Cloud API creates an expiring, revocable
|
||||
`HttpOnly` session cookie and uses a separate CSRF cookie/header for writes.
|
||||
The Console has no bearer-token fallback or user-directory view.
|
||||
|
||||
### HTTPS and session configuration
|
||||
|
||||
@@ -258,7 +252,9 @@ CLOUD_TRUST_PROXY_HEADERS=false
|
||||
```
|
||||
|
||||
Set `CLOUD_TRUST_PROXY_HEADERS=true` only when a trusted proxy overwrites
|
||||
`X-Forwarded-For` before requests reach the Cloud API.
|
||||
`X-Forwarded-For` before requests reach the Cloud API. This affects the
|
||||
client-address bucket used for login throttling; the default is safe when the
|
||||
application is reached directly.
|
||||
|
||||
### Create and recover administrator accounts
|
||||
|
||||
@@ -281,11 +277,11 @@ docker compose exec cloud-api device-cloud-admin users revoke-sessions --usernam
|
||||
```
|
||||
|
||||
Roles are fixed: `viewer` can read tasks/pool/plugins; `operator` additionally
|
||||
submits tasks; `admin` has unrestricted Cloud API access and manages users.
|
||||
Administrators create users, reset passwords, change roles, disable accounts,
|
||||
and revoke sessions from the **Users** Console view. New and reset users must
|
||||
change their temporary password before accessing other resources, and the API
|
||||
will not disable or demote the last enabled administrator.
|
||||
submits tasks; `admin` has unrestricted Cloud API access. The administration
|
||||
CLI creates users, resets passwords, changes roles, enables accounts, and
|
||||
revokes sessions. New and reset users must change their temporary password
|
||||
before accessing other resources, and the API will not disable or demote the
|
||||
last enabled administrator.
|
||||
|
||||
### Configure the CORS allow-list
|
||||
|
||||
@@ -328,22 +324,20 @@ at build time. The deployed origin must be in `CLOUD_CONSOLE_CORS_ORIGINS`.
|
||||
|
||||
### Same-origin deployment (baked into the Cloud API image)
|
||||
|
||||
The Jenkins-built Docker image already carries the SPA at `/app/console-static`,
|
||||
and `compose.yaml` / `compose.deploy.yaml` set
|
||||
`CLOUD_CONSOLE_STATIC_DIR=/app/console-static` on the `cloud-api` service. In
|
||||
this mode the Cloud API itself serves the console at `/console/` (visiting `/`
|
||||
307-redirects there). Put that origin behind an HTTPS reverse proxy, then open
|
||||
for example `https://cloud.example.com/` directly — no separate dev server, no
|
||||
static host, and no CORS allow-list are needed because the SPA and API share one
|
||||
origin.
|
||||
The Jenkins-built Docker image already carries the SPA at `/app/console-static`
|
||||
and sets `CLOUD_CONSOLE_STATIC_DIR` in the image itself. The Cloud API serves
|
||||
the Console at `/console/` (visiting `/` 307-redirects there). Put that origin
|
||||
behind an HTTPS reverse proxy, then open for example
|
||||
`https://cloud.example.com/` directly — no separate dev server, static host, or
|
||||
CORS allow-list is needed because the SPA and API share one origin.
|
||||
|
||||
The SPA shell (`index.html`, JS, CSS) is served without credentials by design
|
||||
so it can render the login page. All `/v1/*` resource calls remain scope-gated,
|
||||
and unsafe cookie-authenticated calls require CSRF proof. The browser receives
|
||||
only the non-secret CSRF value; it never receives the `HttpOnly` session secret.
|
||||
|
||||
To opt out (e.g. for local development where you run `npm run dev`), leave
|
||||
`CLOUD_CONSOLE_STATIC_DIR` unset. The mount is conditional on that env var.
|
||||
Source-based local development leaves `CLOUD_CONSOLE_STATIC_DIR` unset; the
|
||||
mount remains conditional on that image-provided setting.
|
||||
|
||||
Jenkins build args (`NODE_IMAGE`, `NPM_REGISTRY`, `UV_IMAGE`, `APT_MIRROR`,
|
||||
`UV_INDEX_URL`) default to CN mirrors so builds don't time out pulling from
|
||||
@@ -406,9 +400,9 @@ For rollback:
|
||||
|
||||
1. Stop all Host Agents and the Cloud API.
|
||||
2. Back up PostgreSQL or the SQLite database file.
|
||||
3. Before rolling back to a release without enrollment support, provision
|
||||
temporary static Host credentials for every managed edge that must continue
|
||||
operating. Stop those Host Agents and set their explicit Host ID/token.
|
||||
3. Before rolling back to an image that still requires static credentials,
|
||||
restore that image's matching deployment configuration and provision the
|
||||
required legacy credentials outside this release's Compose contract.
|
||||
4. If the previous application version cannot use the current schema, run the
|
||||
tested downgrade while no application process is connected:
|
||||
|
||||
@@ -417,7 +411,7 @@ For rollback:
|
||||
```
|
||||
|
||||
5. Restore the previous application image or checkout and start the Cloud API.
|
||||
6. Verify `/health/ready`, then restart Host Agents with credentials compatible
|
||||
6. Verify `/health/ready`, then restart Host Agents with identities compatible
|
||||
with the restored Cloud API.
|
||||
|
||||
Downgrading revision 0002 removes dynamic credential bindings and durable
|
||||
|
||||
+40
-12
@@ -376,10 +376,8 @@ uv run --package device-host-agent device-host-agent setup
|
||||
|
||||
`HOST_AGENT_CONTROL_PLANE_URL` 默认已固定为 `https://amcp.home.jerryyan.top`;
|
||||
仅在连接其他云端环境(如本地/测试用的 `cloud-api`)时才需要覆盖该变量。未设置
|
||||
`HOST_AGENT_ENROLLMENT_TOKEN` 时,Host Agent 会直接向云端发起零 token 自助注册
|
||||
(需要云端 `CLOUD_SELF_SERVICE_ENROLLMENT_ENABLED=true`);若仍持有云端管理员
|
||||
签发的一次性 enrollment token,也可以继续设置 `HOST_AGENT_ENROLLMENT_TOKEN`
|
||||
走原有的 token 注册路径:
|
||||
缓存身份不存在时,Host Agent 会直接向云端注册,由云端返回 `host_id`;后续运行
|
||||
使用本地持久化的随机 Host secret。无需配置静态 Host 或 enrollment token:
|
||||
|
||||
```bash
|
||||
export HOST_AGENT_IDENTITY_PATH="tasks/host_identity.json"
|
||||
@@ -394,17 +392,47 @@ uv run --package device-host-agent device-host-agent
|
||||
|
||||
首次启动顺序为:持久化候选 Host secret、向云端换取 `host_id`、为每个本地设备
|
||||
换取 `device_id`、保存映射、连接 WDA、发送 heartbeat、开始 long-poll 领取任务。
|
||||
Host Agent 不监听入站端口。若使用了一次性 enrollment token,成功后可以将其从
|
||||
边缘环境移除;但必须保留并保护 `tasks/host_identity.json` 和
|
||||
Host Agent 不监听入站端口。必须保留并保护 `tasks/host_identity.json` 和
|
||||
`tasks/device_config.sqlite3`;前者等同于 Host bearer credential。
|
||||
|
||||
零 token 自助注册意味着任何能访问该云端地址的设备都能自行注册成为 Host,没有
|
||||
审批环节,也没有限流保护;这一取舍依赖网络边界(防火墙/反向代理)而非应用层
|
||||
限制,仅应在受控网络中开启 `CLOUD_SELF_SERVICE_ENROLLMENT_ENABLED`。
|
||||
直接注册意味着任何能访问该云端地址的设备都能自行注册成为 Host,没有审批环节,
|
||||
也没有限流保护;这一取舍依赖网络边界(防火墙/反向代理)而非应用层限制。
|
||||
|
||||
需要回滚到静态模式时,停止 Host Agent,由云端管理员配置匹配的静态 Host
|
||||
credential,然后显式设置 `HOST_AGENT_HOST_ID` 与 `HOST_AGENT_TOKEN`。这两个变量
|
||||
同时存在时优先于 enrollment state。
|
||||
### 启用本地 Web Console(可选)
|
||||
|
||||
Host Agent 内置一个默认关闭的本地 Web Console,用于在这台 Mac 上直接查看和管理
|
||||
正在运行的 Host,无需 SSH 进去读原始状态文件。启用时追加环境变量后再启动:
|
||||
|
||||
```bash
|
||||
export HOST_AGENT_CONSOLE_ENABLED="true"
|
||||
|
||||
uv run --package device-host-agent device-host-agent
|
||||
```
|
||||
|
||||
不要修改 `HOST_AGENT_CONSOLE_BIND_HOST`,保持默认回环地址 `127.0.0.1`;Console
|
||||
启动后在同一台 Mac 上打开:
|
||||
|
||||
```
|
||||
http://127.0.0.1:8765
|
||||
```
|
||||
|
||||
`8765` 是 `HOST_AGENT_CONSOLE_PORT` 的默认值(见
|
||||
`apps/device-host-agent/host_agent/config.py`)。登录使用与
|
||||
`uv run --package device-host-agent device-host-agent setup` 创建的同一个本地账号,
|
||||
没有单独的 Console 账号体系。
|
||||
|
||||
登录后可以看到:
|
||||
|
||||
- 状态仪表盘:最近一次 heartbeat 结果与时间、enrollment/identity 状态、本地登记
|
||||
设备及其连接状态、当前 assignment 执行进度。
|
||||
- 本地设备管理:新增、编辑、删除登记在这台 Host 上的设备,改动会立即在运行中的
|
||||
`DeviceManager` 上生效,无需重启 Host Agent。
|
||||
- 修改密码:更新本地操作账号密码,需要先输入当前密码。
|
||||
- 最近历史:近期 assignment 与 heartbeat 的执行记录。
|
||||
|
||||
完整的 `HOST_AGENT_CONSOLE_*` 环境变量列表(端口、非回环 bind 的显式 opt-in、
|
||||
session TTL、历史记录条数上限等)参见 `docs/CLOUD_DEPLOYMENT.md`;生产/远程场景下
|
||||
应优先使用 SSH 端口转发访问该 Console,而不是直接把它暴露到非回环地址。
|
||||
|
||||
## 10. 多设备与端口
|
||||
|
||||
|
||||
Reference in New Issue
Block a user