release: public alpha 0.6.0 baseline
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
# SDK API Reference
|
||||
|
||||
Package:
|
||||
|
||||
```text
|
||||
jwebot-sdk
|
||||
```
|
||||
|
||||
Current API version:
|
||||
|
||||
```text
|
||||
1
|
||||
```
|
||||
|
||||
## Event
|
||||
|
||||
`Event` is the normalized incoming message exposed to extension code.
|
||||
|
||||
Important fields:
|
||||
|
||||
- `event_id`
|
||||
- `observed_at`
|
||||
- `sender`
|
||||
- `text`
|
||||
- `group_fingerprint`
|
||||
- `mention_bot`
|
||||
- `confidence`
|
||||
- `sender_confidence`
|
||||
- `body_confidence`
|
||||
- `source`
|
||||
|
||||
`sender` is an untrusted display label. `group_fingerprint` binds kernel
|
||||
delivery safety but is not a selectable target.
|
||||
|
||||
## ReplyPlan
|
||||
|
||||
```python
|
||||
ReplyPlan.text("visible reply")
|
||||
ReplyPlan.no_reply()
|
||||
```
|
||||
|
||||
Maximum visible text length is 900 characters.
|
||||
|
||||
## Fact
|
||||
|
||||
```python
|
||||
Fact(kind="topic", text="likes servers", confidence=0.8)
|
||||
```
|
||||
|
||||
Facts are candidates. The kernel still applies scope, sensitivity, evidence,
|
||||
expiry, and deduplication policy.
|
||||
|
||||
## ExtensionResult
|
||||
|
||||
```python
|
||||
ExtensionResult(
|
||||
reply=ReplyPlan.text("visible reply"),
|
||||
facts=(Fact(...),),
|
||||
metadata={"source": "plugin-id"},
|
||||
)
|
||||
```
|
||||
|
||||
Limits:
|
||||
|
||||
- at most 12 facts;
|
||||
- at most 16 metadata entries;
|
||||
- short string metadata only.
|
||||
|
||||
## SingleGroupContext
|
||||
|
||||
Protocols exposed through the context:
|
||||
|
||||
- `commands`
|
||||
- `storage`
|
||||
- `http`
|
||||
- `services`
|
||||
- `facts`
|
||||
- `scheduler`
|
||||
- `logger`
|
||||
|
||||
Not exposed:
|
||||
|
||||
- GUI sender;
|
||||
- group selector;
|
||||
- raw OCR or screenshots;
|
||||
- ledger or SQLite;
|
||||
- filesystem;
|
||||
- environment;
|
||||
- subprocess or shell;
|
||||
- arbitrary sockets or HTTP;
|
||||
- credentials.
|
||||
|
||||
## CommandRegistry
|
||||
|
||||
```python
|
||||
context.commands.register("/hello", handler)
|
||||
```
|
||||
|
||||
The registered command must be declared in `plugin.toml`. The Kernel catalog
|
||||
detects conflicts during startup and freezes before runtime routing.
|
||||
User-visible `description` and `help` metadata also live in `plugin.toml`;
|
||||
bundled `/功能列表` output is generated from the reviewed metadata rather than
|
||||
from handler code.
|
||||
|
||||
## ApprovedHttpClient
|
||||
|
||||
```python
|
||||
context.http.request(
|
||||
"approved-profile",
|
||||
method="GET",
|
||||
path="/bounded/path",
|
||||
query={"key": "value"},
|
||||
)
|
||||
```
|
||||
|
||||
The kernel owns host allowlists, credentials, proxy, timeout, cache, and
|
||||
response limits. API version 1 does not expose a general-purpose URL client.
|
||||
|
||||
## ApprovedServiceBroker
|
||||
|
||||
```python
|
||||
context.services.call(
|
||||
"reviewed-profile",
|
||||
operation="execute",
|
||||
payload={"bounded": "data"},
|
||||
)
|
||||
```
|
||||
|
||||
Reviewed local adapters use named profiles to request Kernel-owned behavior without
|
||||
receiving raw Ledger, provider, process, or GUI objects. Undeclared or
|
||||
unconfigured profiles fail closed.
|
||||
@@ -0,0 +1,111 @@
|
||||
# Extension Author Guide
|
||||
|
||||
JWeBot extensions are reviewed Python packages for one preselected
|
||||
WeChat group. They do not receive WeChat GUI access or server credentials.
|
||||
|
||||
## Create A Command Extension
|
||||
|
||||
From the repository root:
|
||||
|
||||
```sh
|
||||
./tools/create-plugin.py weather-command /weather \
|
||||
--author FriendName \
|
||||
--description "查询指定城市天气" \
|
||||
--help-entry "/weather 城市"
|
||||
```
|
||||
|
||||
This creates:
|
||||
|
||||
```text
|
||||
extensions/friends/weather-command/
|
||||
plugin.toml
|
||||
README.md
|
||||
src/weather_command.py
|
||||
tests/test_plugin.py
|
||||
```
|
||||
|
||||
The generator refuses invalid IDs, invalid command names, and existing
|
||||
destinations. The required author and user-facing description/help metadata are
|
||||
recorded in `plugin.toml`. The Kernel uses that metadata to generate help.
|
||||
|
||||
## Implement Behavior
|
||||
|
||||
The handler receives:
|
||||
|
||||
```python
|
||||
def handle(event: Event, arguments: str) -> ExtensionResult:
|
||||
...
|
||||
```
|
||||
|
||||
Return:
|
||||
|
||||
```python
|
||||
ExtensionResult(reply=ReplyPlan.text("result"))
|
||||
```
|
||||
|
||||
Use `ReplyPlan.no_reply()` when no visible response is appropriate.
|
||||
|
||||
Do not return raw strings or call a sender.
|
||||
|
||||
## Request Capabilities
|
||||
|
||||
Declare only what the extension needs:
|
||||
|
||||
```toml
|
||||
author = "FriendName"
|
||||
description = "查询指定城市天气"
|
||||
help = ["/weather 城市"]
|
||||
|
||||
[capabilities]
|
||||
commands = ["/weather"]
|
||||
storage = false
|
||||
network_profiles = []
|
||||
```
|
||||
|
||||
Network access requires a separately reviewed named profile. Never put an API
|
||||
key in source, tests, manifest, logs, or README.
|
||||
|
||||
The plugin entrypoint registers exactly the commands declared in the manifest.
|
||||
Registration freezes at startup; runtime command mutation is rejected.
|
||||
|
||||
## Test Locally
|
||||
|
||||
Validate the manifest:
|
||||
|
||||
```sh
|
||||
./tools/validate-plugin.py extensions/friends/weather-command/plugin.toml
|
||||
```
|
||||
|
||||
Run the repository gate:
|
||||
|
||||
```sh
|
||||
./tools/check-local.sh
|
||||
```
|
||||
|
||||
On Python earlier than 3.11, install `requirements-dev.txt` so TOML validation
|
||||
uses `tomli`.
|
||||
|
||||
## Submit
|
||||
|
||||
1. Create a branch.
|
||||
2. Add the extension, tests, and README.
|
||||
3. Run the local gate.
|
||||
4. Open a PR in the Git hosting repository.
|
||||
5. Explain every requested command, storage permission, and network profile.
|
||||
6. Wait for capability and code review.
|
||||
|
||||
Merge does not imply immediate production activation. A reviewed publisher
|
||||
must sign the canonical release record with a key that is not available to
|
||||
extension code. Production provisioning and isolated-host activation remain
|
||||
separately controlled steps.
|
||||
|
||||
## Review Checklist
|
||||
|
||||
- Uses only imports from `jwebot_sdk` plus Python pure-data helpers.
|
||||
- Returns structured plans.
|
||||
- Does not read environment variables or files.
|
||||
- Does not use subprocess, sockets, SSH, Docker, VNC, or GUI APIs.
|
||||
- Does not accept or construct a target group.
|
||||
- Does not log message bodies or secrets.
|
||||
- Has deterministic tests for success and failure behavior.
|
||||
- Manifest matches registered commands exactly.
|
||||
@@ -0,0 +1,163 @@
|
||||
# Capability Catalog
|
||||
|
||||
Status: API version 1.
|
||||
|
||||
Capabilities are denied by default and declared in `plugin.toml`.
|
||||
|
||||
## Commands
|
||||
|
||||
Manifest field:
|
||||
|
||||
```toml
|
||||
commands = ["/hello"]
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- command names begin with one `/`;
|
||||
- one extension owns a reviewed namespace;
|
||||
- duplicates and undeclared registrations are rejected;
|
||||
- command extensions declare at least one command;
|
||||
- enrichment and scheduled extensions cannot declare commands.
|
||||
|
||||
Command plugins also carry non-capability display metadata:
|
||||
|
||||
```toml
|
||||
description = "Return a local greeting"
|
||||
help = ["/hello [name]"]
|
||||
```
|
||||
|
||||
The Kernel uses signed description/help metadata for automatic help and Host
|
||||
health validation. Display metadata never grants storage, network, or service
|
||||
access.
|
||||
|
||||
## Storage
|
||||
|
||||
Manifest field:
|
||||
|
||||
```toml
|
||||
storage = true
|
||||
```
|
||||
|
||||
The host exposes only a private key/value namespace for that extension.
|
||||
|
||||
It does not expose:
|
||||
|
||||
- filesystem paths;
|
||||
- another extension's state;
|
||||
- the edge ledger;
|
||||
- OCR history;
|
||||
- environment variables.
|
||||
|
||||
Storage quotas and persistence format remain kernel-owned.
|
||||
|
||||
## Approved Network Profiles
|
||||
|
||||
Manifest field:
|
||||
|
||||
```toml
|
||||
network_profiles = ["steam-api"]
|
||||
```
|
||||
|
||||
An approved profile owns:
|
||||
|
||||
- allowed hosts and paths;
|
||||
- credentials;
|
||||
- proxy policy;
|
||||
- timeout;
|
||||
- cache;
|
||||
- response-size limit;
|
||||
- error mapping.
|
||||
|
||||
Extensions receive profile names, not secrets. The current Steam behavior is the
|
||||
reference: Mihomo is mandatory and proxy failure never falls back to direct
|
||||
network access.
|
||||
|
||||
No network profile is currently part of the public example extension.
|
||||
|
||||
## Approved Service Profiles
|
||||
|
||||
Manifest field:
|
||||
|
||||
```toml
|
||||
service_profiles = ["coin"]
|
||||
```
|
||||
|
||||
Service profiles are narrow Kernel-owned operations used by reviewed local
|
||||
adapters. They do not expose raw Ledger or provider objects.
|
||||
|
||||
Current profiles:
|
||||
|
||||
- `person-memory`
|
||||
- `dice`
|
||||
- `coin`
|
||||
- `random-number`
|
||||
- `draw-lot`
|
||||
- `daily-fortune`
|
||||
- `guess-number`
|
||||
- `rock-paper-scissors`
|
||||
- `scan-ranking`
|
||||
- `repeat-ranking`
|
||||
- `history-search`
|
||||
- `server-status`
|
||||
- `jwebot-version`
|
||||
- `debug-telemetry`
|
||||
- `steam-presence`
|
||||
|
||||
`person-memory`, `server-status`, `jwebot-version`, and `debug-telemetry` are
|
||||
official-authored. The others are authored by JWeBot. Kernel direct commands
|
||||
(`/功能列表`, `/帮助`, `/help`, `/余额`) have no service profile. Other
|
||||
extensions do not receive these profiles by default. Every
|
||||
profile request requires capability review, and an unavailable broker fails
|
||||
closed.
|
||||
|
||||
## Facts
|
||||
|
||||
`FactWriter` and `ExtensionResult.facts` accept bounded fact candidates.
|
||||
|
||||
The kernel still controls:
|
||||
|
||||
- sensitive-content filtering;
|
||||
- group and person scope;
|
||||
- evidence requirements;
|
||||
- expiry;
|
||||
- deduplication;
|
||||
- cloud projection.
|
||||
|
||||
An extension cannot directly edit profile memory.
|
||||
|
||||
## Scheduling
|
||||
|
||||
The scheduler accepts a named future intent. The kernel owns the clock, durable
|
||||
queue, idempotency key, target group binding, and eventual delivery checks.
|
||||
|
||||
API version 1 does not permit arbitrary cron expressions or direct timers owned
|
||||
by extension code.
|
||||
|
||||
## Logging
|
||||
|
||||
The extension logger accepts short messages and structured fields.
|
||||
|
||||
Logs must not contain:
|
||||
|
||||
- message bodies;
|
||||
- group names;
|
||||
- reply text;
|
||||
- credentials;
|
||||
- environment values;
|
||||
- raw HTTP payloads.
|
||||
|
||||
## Explicitly Unavailable
|
||||
|
||||
The SDK intentionally has no capability for:
|
||||
|
||||
- GUI or VNC;
|
||||
- group selection;
|
||||
- raw OCR;
|
||||
- SQLite;
|
||||
- filesystem;
|
||||
- environment;
|
||||
- subprocess;
|
||||
- SSH or Docker;
|
||||
- unrestricted sockets or HTTP;
|
||||
- direct model credentials.
|
||||
@@ -0,0 +1,149 @@
|
||||
# Command Registration And Execution Paths
|
||||
|
||||
Status: authoritative local registration and routing contract for
|
||||
`alpha 0.6.x`.
|
||||
|
||||
## Three Execution Paths
|
||||
|
||||
Every accepted message resolves to exactly one named execution path:
|
||||
|
||||
| Execution path | Input | Runtime | Reply behavior |
|
||||
|---|---|---|---|
|
||||
| `local-command` | Registered `/command` | Kernel command or reviewed plugin | Synchronous local reply, except explicitly asynchronous Steam |
|
||||
| `deepseek` | Ordinary `@JWeBot` message | Direct DeepSeek client | Fast ACK, background generation, ACK-gated final reply |
|
||||
| `openclaw` | `@JWeBot #task` | OpenClaw cloud runner | Fast ACK, durable cloud outbox, ACK-gated final reply |
|
||||
|
||||
The compatibility `route` values remain `local`, `chat`, and `cloud` in stored
|
||||
events. API responses and telemetry also include the explicit
|
||||
`execution_path`, so operators and plugin authors do not need to infer the
|
||||
actual provider from a legacy storage label.
|
||||
|
||||
Unknown slash commands remain on `local-command` and fail with a local command
|
||||
error. They never fall through to DeepSeek or OpenClaw.
|
||||
|
||||
## Fast Local Ingress
|
||||
|
||||
The local command path is:
|
||||
|
||||
1. XDamage reports a relevant message-region change.
|
||||
2. OpenCV segments incoming bubbles without running OCR over the whole screen.
|
||||
3. The OCR engine reuses exact visual-fingerprint matches from a bounded LRU.
|
||||
4. Only new or changed sender/body crops use RapidOCR.
|
||||
5. Both recognition-only and detection fallback paths are prewarmed at startup.
|
||||
6. A command event is written to an atomic, fsynced spool file.
|
||||
7. The in-process responder is signalled immediately after durable replacement.
|
||||
8. Edge durably records and executes the local command.
|
||||
9. The guarded GUI sender revalidates the selected group and sends the reply.
|
||||
|
||||
The queue directory remains the recovery source of truth. The wake signal is
|
||||
only a latency optimization; a separate responder process or a missed signal
|
||||
still discovers work through the bounded one-second fallback scan.
|
||||
|
||||
Exact image bytes, crop dimensions, and sender pixels participate in the OCR
|
||||
cache key. Identical repeated messages remain separate visible occurrences
|
||||
because each bubble position still produces its own `IncomingMessage`.
|
||||
Changed pixels force fresh OCR. The cache is bounded and periodically
|
||||
revalidated.
|
||||
|
||||
## Bundled Plugin Registration
|
||||
|
||||
Bundled command metadata has one source:
|
||||
|
||||
```text
|
||||
packages/jwebot-official-extensions/
|
||||
src/jwebot_official_extensions/descriptors.py
|
||||
```
|
||||
|
||||
Each `LocalExtensionDescriptor` declares:
|
||||
|
||||
- `extension_id`;
|
||||
- `author`;
|
||||
- user-visible `description`;
|
||||
- user-visible `help_entries`;
|
||||
- owned internal `command_kinds`.
|
||||
|
||||
Aliases are resolved from the same inventory. Run:
|
||||
|
||||
```sh
|
||||
./tools/sync-local-extensions.py
|
||||
```
|
||||
|
||||
The generator writes matching:
|
||||
|
||||
- `plugin.toml`;
|
||||
- SDK adapter source;
|
||||
- package README;
|
||||
- package registration test;
|
||||
- in-process compatibility adapter.
|
||||
|
||||
`--check` is part of the repository gate and rejects generated drift.
|
||||
|
||||
## Friend Plugin Registration
|
||||
|
||||
Create a command plugin with explicit user-facing metadata:
|
||||
|
||||
```sh
|
||||
./tools/create-plugin.py weather-command /weather \
|
||||
--author FriendName \
|
||||
--description "查询指定城市天气" \
|
||||
--help-entry "/weather 城市"
|
||||
```
|
||||
|
||||
The command must also be registered in the plugin entrypoint:
|
||||
|
||||
```python
|
||||
def plugin(context):
|
||||
context.commands.register("/weather", handle)
|
||||
```
|
||||
|
||||
Startup fails if registered commands differ from signed manifest commands.
|
||||
The catalog freezes after startup, so command ownership cannot change during
|
||||
request processing.
|
||||
|
||||
## Automatic Help
|
||||
|
||||
`/功能列表`, `/帮助`, and `/help` are Kernel commands. Their visible output is
|
||||
generated from:
|
||||
|
||||
- Kernel command descriptors;
|
||||
- each bundled plugin's `description`;
|
||||
- each bundled plugin's `help_entries`;
|
||||
- the fixed DeepSeek and OpenClaw path descriptions.
|
||||
|
||||
There is no separate hand-maintained help list. Adding or changing a bundled
|
||||
plugin command without updating its descriptor changes generated files and
|
||||
fails the sync gate.
|
||||
|
||||
Signed Host health responses include description and help metadata. Edge
|
||||
compares them with the startup inventory together with author, commands, and
|
||||
service profiles before activating the Host.
|
||||
|
||||
## Manifest Metadata
|
||||
|
||||
Command manifests must declare:
|
||||
|
||||
```toml
|
||||
description = "查询指定城市天气"
|
||||
help = ["/weather 城市"]
|
||||
```
|
||||
|
||||
`description` and `help` must be declared together. Help entries are bounded,
|
||||
single-line command usage strings beginning with `/`. Bundled plugins require
|
||||
both fields.
|
||||
|
||||
User-visible help text is metadata only. It grants no capability and cannot
|
||||
change routing ownership.
|
||||
|
||||
## Official Operational Plugins
|
||||
|
||||
Operational slash commands are implemented through official reviewed plugin
|
||||
descriptors unless they are strictly Kernel-only. The current official
|
||||
operational commands are:
|
||||
|
||||
- `/状态` and `/status`: server, Edge, OCR, and queue status.
|
||||
- `/version`: installed JWeBot package versions.
|
||||
- `/debug`: recent privacy-safe telemetry p50/p95/max summary.
|
||||
|
||||
These commands delegate to Kernel-owned service profiles through generated
|
||||
adapters. They do not give plugin code direct access to raw SQLite, systemd,
|
||||
journal, GUI, shell, or secrets.
|
||||
@@ -0,0 +1,176 @@
|
||||
# Configuration Guide
|
||||
|
||||
JWeBot configuration is a versioned profile package compiled into runtime
|
||||
files. Source checkout paths and Git history never contain a populated profile.
|
||||
|
||||
## Storage
|
||||
|
||||
Defaults:
|
||||
|
||||
| Purpose | Path |
|
||||
|---|---|
|
||||
| Profiles | `~/.config/jwebot/profiles/` |
|
||||
| Active profile | `~/.config/jwebot/current` |
|
||||
| Generated runtime | `~/.config/jwebot/current/generated/` |
|
||||
| Private state | `~/.local/state/jwebot/profiles/<profile>/` |
|
||||
|
||||
Override roots with `JWEBOT_CONFIG_ROOT` and `JWEBOT_CONFIG_STATE_ROOT`.
|
||||
|
||||
## Lifecycle
|
||||
|
||||
Create:
|
||||
|
||||
```sh
|
||||
./tools/jwebot-config.py init personal
|
||||
```
|
||||
|
||||
Validate and activate:
|
||||
|
||||
```sh
|
||||
./tools/jwebot-config.py validate personal
|
||||
./tools/jwebot-config.py activate personal
|
||||
```
|
||||
|
||||
Import:
|
||||
|
||||
```sh
|
||||
./tools/jwebot-config.py import /protected/profile.tar.gz --activate
|
||||
```
|
||||
|
||||
Redacted export:
|
||||
|
||||
```sh
|
||||
./tools/jwebot-config.py export personal \
|
||||
--output /tmp/personal-redacted.tar.gz
|
||||
```
|
||||
|
||||
The compiler validates every component, writes a temporary `generated`
|
||||
directory, records SHA-256 digests in `manifest.json`, and renames it
|
||||
atomically. Services never read partially generated config.
|
||||
|
||||
## Components
|
||||
|
||||
### Bot
|
||||
|
||||
`components/bot.toml` controls:
|
||||
|
||||
- `JWEBOT_BOT_MENTION`;
|
||||
- the private identity note written to `JWEBOT_PRIVATE_CONTEXT_FILE`;
|
||||
- cloud agent name;
|
||||
- bounded remote memory path.
|
||||
|
||||
### Providers
|
||||
|
||||
`components/providers.toml` controls:
|
||||
|
||||
- DeepSeek base URL;
|
||||
- optional cloud host;
|
||||
- Steam owner ID;
|
||||
- mandatory loopback Steam proxy.
|
||||
|
||||
API keys belong only in `secrets/runtime.env`:
|
||||
|
||||
```ini
|
||||
JWEBOT_EDGE_DEEPSEEK_API_KEY=
|
||||
JWEBOT_EDGE_STEAM_API_KEY=
|
||||
```
|
||||
|
||||
Steam uses `JWEBOT_EDGE_STEAM_ID64` and
|
||||
`JWEBOT_EDGE_STEAM_PROXY_URL`. No source file contains a fixed friend roster.
|
||||
|
||||
### Runtime
|
||||
|
||||
`components/runtime.toml` controls:
|
||||
|
||||
- Edge and local extension enablement;
|
||||
- telemetry and OCR prewarm;
|
||||
- delivery enablement;
|
||||
- extension mode and transport;
|
||||
- publisher principal;
|
||||
- Host timeout and fallback;
|
||||
- GUI sender timeout.
|
||||
|
||||
### Debug
|
||||
|
||||
`components/debug.toml` generates:
|
||||
|
||||
- `JWEBOT_DEBUG_ENABLED`;
|
||||
- `JWEBOT_DEBUG_CAPTURE_PAYLOADS`;
|
||||
- `JWEBOT_DEBUG_INCIDENT_RETENTION_DAYS`;
|
||||
- `JWEBOT_DEBUG_SESSION_RETENTION_DAYS`;
|
||||
- `JWEBOT_DEBUG_INCIDENT_ROOT`;
|
||||
- `JWEBOT_DEBUG_SESSION_ROOT`.
|
||||
|
||||
Message bodies, sender names, raw group identifiers, provider payloads, and
|
||||
environment values remain forbidden in telemetry and public incident records.
|
||||
Schemas live in `config/schemas/`.
|
||||
|
||||
### Notification
|
||||
|
||||
`components/notification.toml` compiles to the file referenced by
|
||||
`JWEBOT_HEALTH_NOTIFICATION_CONFIG`.
|
||||
|
||||
The adapter is an absolute argv array. JWeBot does not invoke a shell. The
|
||||
notification body is supplied on stdin, and a zero exit status confirms
|
||||
delivery. Provider credentials belong in the secret environment.
|
||||
|
||||
### Deployment
|
||||
|
||||
`components/deployment.toml` generates root-only values:
|
||||
|
||||
- `JWEBOT_RUNTIME_USER`;
|
||||
- `JWEBOT_RUNTIME_HOME`;
|
||||
- `JWEBOT_RUNTIME_PYTHON`;
|
||||
- `JWEBOT_PRODUCTION_BACKUP_ROOT`.
|
||||
|
||||
The generated file is `current/generated/root.env`. Root deployment tooling
|
||||
may source only this validated file.
|
||||
|
||||
### Private Context
|
||||
|
||||
`context/people.json` contains person aliases, optional identity facts, display
|
||||
IDs, and clock targets. It compiles to `JWEBOT_PRIVATE_CONTEXT_FILE`.
|
||||
|
||||
Each person may also define a private `steam_id64` and `steam_aliases`. When at
|
||||
least one mapping is present, Steam status and playtime are strictly scoped to
|
||||
those mapped people and use their group display IDs. With no private mapping,
|
||||
the provider falls back to public friend discovery.
|
||||
|
||||
This file is local-only PII. Default export removes all entries.
|
||||
|
||||
### Overrides
|
||||
|
||||
`overrides/runtime.env` accepts non-secret `JWEBOT_*` and `WECHAT_*` values.
|
||||
`overrides/root.env` accepts deployment overrides. Secret-shaped keys are
|
||||
rejected from runtime overrides and must live under `secrets/`.
|
||||
|
||||
## Legacy Migration
|
||||
|
||||
```sh
|
||||
./tools/jwebot-config.py migrate-legacy personal \
|
||||
--repository /path/to/old/checkout \
|
||||
--environment /protected/legacy/environment \
|
||||
--activate
|
||||
```
|
||||
|
||||
The migration:
|
||||
|
||||
1. archives root-level debug, incident, report, and operations material;
|
||||
2. converts static aliases and bot identity into private context;
|
||||
3. separates secret environment keys from non-secret overrides;
|
||||
4. compiles and activates the profile;
|
||||
5. leaves the source checkout unchanged.
|
||||
|
||||
## Runtime Compatibility
|
||||
|
||||
The Edge CLI still reads the former
|
||||
`~/.config/wechat-health-watch/environment` only as a compatibility fallback.
|
||||
New systemd units use the generated profile directly.
|
||||
|
||||
## Validation
|
||||
|
||||
```sh
|
||||
./tools/check-config.sh
|
||||
./tools/check-secrets.py
|
||||
PATH="$PWD/.venv/bin:$PATH" ./tools/check-local.sh
|
||||
```
|
||||
@@ -0,0 +1,72 @@
|
||||
# Configuration Inventory
|
||||
|
||||
This inventory defines ownership and persistence boundaries. It intentionally
|
||||
contains no real host, user, address, account, person, or credential value.
|
||||
|
||||
## Committed
|
||||
|
||||
| Path | Contents |
|
||||
|---|---|
|
||||
| `config/templates/default/` | Empty, public profile template. |
|
||||
| `config/schemas/` | Profile, private-context, incident, and debug-event schemas. |
|
||||
| `deploy/systemd/` | Generic service units with no credentials. |
|
||||
| `deploy/defaults/` | Safe runtime defaults for the generic `jwebot` account. |
|
||||
| `examples/` | Synthetic SDK and plugin examples. |
|
||||
|
||||
## Local Configuration
|
||||
|
||||
| Path | Mode | Contents |
|
||||
|---|---:|---|
|
||||
| `~/.config/jwebot/` | `0700` | Configuration root. |
|
||||
| `profiles/<id>/` | `0700` | Source profile package. |
|
||||
| `profiles/<id>/secrets/` | `0700` | API keys and notification secrets. |
|
||||
| `profiles/<id>/context/` | `0700` | Private person and bot context. |
|
||||
| `profiles/<id>/generated/` | `0700` | Atomic runtime output. |
|
||||
| `current` | symlink | Active profile. |
|
||||
|
||||
Generated files are mode `0600`.
|
||||
|
||||
## Local State
|
||||
|
||||
| Path | Contents |
|
||||
|---|---|
|
||||
| `~/.local/state/jwebot/profiles/<id>/debug/sessions/` | Local debugger sessions. |
|
||||
| `~/.local/state/jwebot/profiles/<id>/incidents/` | Privacy-safe incident records. |
|
||||
| `~/.local/state/jwebot/profiles/<id>/telemetry/` | Optional bounded telemetry exports. |
|
||||
| `~/.local/state/jwebot/profiles/<id>/archives/` | Migrated legacy debug/reports. |
|
||||
| `~/.local/state/jwebot/profiles/<id>/root/` | Protected root-operation evidence. |
|
||||
| `~/.local/state/jwebot-edge/` | Edge socket and SQLite ledger. |
|
||||
| `~/.local/state/wechat-adaptive-ocr/` | OCR queue, history, and failed events. |
|
||||
|
||||
## Secret Keys
|
||||
|
||||
The config compiler recognizes these explicit secrets:
|
||||
|
||||
- `JWEBOT_EDGE_DEEPSEEK_API_KEY`;
|
||||
- `JWEBOT_EDGE_STEAM_API_KEY`;
|
||||
- `JWEBOT_HEALTH_NOTIFICATION_TOKEN`.
|
||||
|
||||
Additional names ending in `_KEY`, `_TOKEN`, `_PASSWORD`, or `_SECRET` are
|
||||
treated as sensitive by export and scanning tools.
|
||||
|
||||
## Runtime Files
|
||||
|
||||
| Generated file | Consumer |
|
||||
|---|---|
|
||||
| `runtime.env` | Edge, OCR, health monitor, and user services. |
|
||||
| `root.env` | Bootstrap and coordinated root deployer. |
|
||||
| `private-context.json` | Kernel identity/alias registry only. |
|
||||
| `notification.json` | Health notification adapter only. |
|
||||
| `manifest.json` | Digest and generation metadata. |
|
||||
|
||||
Plugins receive none of these files or environment values.
|
||||
|
||||
## Never Commit
|
||||
|
||||
- populated profiles or generated output;
|
||||
- debug payloads, incident archives, OCR history, screenshots, or message logs;
|
||||
- SQLite, WAL, sockets, or process files;
|
||||
- private person aliases, display names, SteamIDs, or group identifiers;
|
||||
- host addresses, SSH aliases, usernames, or absolute personal home paths;
|
||||
- API keys, passwords, tokens, private keys, or signing keys;
|
||||
- configuration exports created with `--include-private`.
|
||||
@@ -0,0 +1,185 @@
|
||||
# Extension Host
|
||||
|
||||
Status: signed-release Host and local Edge routing integration implemented.
|
||||
|
||||
The Edge supports `in-process`, `prefer-host`, and `host-only` execution modes.
|
||||
The production default remains `in-process`; the Host is not installed or
|
||||
enabled on production host.
|
||||
|
||||
## Process Model
|
||||
|
||||
One host process:
|
||||
|
||||
1. receives an explicit verified release directory;
|
||||
2. re-verifies installed release content, record, and publisher signature;
|
||||
3. checks the publisher against an explicit OpenSSH allowed-signers file;
|
||||
4. re-validates manifest and source policy;
|
||||
5. loads one plugin entrypoint;
|
||||
6. freezes declared command registration;
|
||||
7. reads one bounded JSON request from stdin;
|
||||
8. invokes one command;
|
||||
9. exchanges bounded service requests with the Kernel when declared;
|
||||
10. writes one final JSON response to stdout;
|
||||
11. exits.
|
||||
|
||||
Plugin stdout is redirected away from the protocol stream.
|
||||
Plugin stdout and stderr are bounded before reaching the parent pipe.
|
||||
|
||||
## Request
|
||||
|
||||
```json
|
||||
{
|
||||
"op": "invoke_command",
|
||||
"command": "/hello",
|
||||
"arguments": "world",
|
||||
"event": {
|
||||
"version": 1,
|
||||
"event_id": "event-1",
|
||||
"observed_at": "2026-10-14T10:00:00+08:00",
|
||||
"sender": "JWeBot",
|
||||
"text": "@JWeBot /hello world",
|
||||
"confidence": 1.0,
|
||||
"sender_confidence": 1.0,
|
||||
"body_confidence": 1.0,
|
||||
"side": "incoming",
|
||||
"mention_bot": true,
|
||||
"group_fingerprint": "opaque-fingerprint",
|
||||
"source": "rapidocr-local"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The request contains no delivery target. The group fingerprint remains an
|
||||
opaque safety binding for the Kernel.
|
||||
|
||||
## Response
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"api_version": "1",
|
||||
"plugin_id": "hello-command",
|
||||
"result": {
|
||||
"api_version": "1",
|
||||
"reply": {"kind": "text", "text": "hello JWeBot world"},
|
||||
"facts": [],
|
||||
"metadata": {}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Errors return a short error class and do not include plugin exception text.
|
||||
|
||||
Declared service calls use an intermediate JSON-line exchange:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "service_request",
|
||||
"call_id": "1",
|
||||
"profile": "coin",
|
||||
"operation": "execute",
|
||||
"payload": {"command": "/翻硬币"}
|
||||
}
|
||||
```
|
||||
|
||||
The parent validates the profile, performs the Kernel-owned operation, and
|
||||
returns a matching `service_result`. Calls, message sizes, and total wall time
|
||||
are bounded. An undeclared profile, mismatched call ID, malformed result, or
|
||||
unavailable broker fails closed.
|
||||
|
||||
## Context Capabilities
|
||||
|
||||
The prototype provides:
|
||||
|
||||
- manifest-bound command registration;
|
||||
- declared, bounded Kernel service profiles;
|
||||
- bounded fact collection;
|
||||
- hashed logger messages with field names only.
|
||||
|
||||
The following brokers intentionally fail closed until implemented:
|
||||
|
||||
- storage;
|
||||
- approved HTTP;
|
||||
- scheduler.
|
||||
|
||||
No GUI sender, group selector, raw ledger, environment, shell, SSH, Docker, or
|
||||
arbitrary network client is supplied.
|
||||
|
||||
## Environment Handling
|
||||
|
||||
Before loading extension code, the CLI removes environment variables except
|
||||
`LANG`, `LC_ALL`, and `TZ`.
|
||||
|
||||
This reduces accidental secret exposure but is not a security sandbox.
|
||||
The parent interpreter also sets `PYTHONDONTWRITEBYTECODE=1`, and the loader
|
||||
disables bytecode writes while importing a plugin, so verified release
|
||||
directories cannot acquire unrecorded `__pycache__` files.
|
||||
|
||||
## Required Production Isolation
|
||||
|
||||
Before Edge integration, the host still needs an OS-enforced launcher with:
|
||||
|
||||
- dedicated dynamic or unprivileged identity;
|
||||
- read-only verified release directory;
|
||||
- private writable state directory;
|
||||
- no inherited secrets;
|
||||
- no network by default;
|
||||
- restricted address families;
|
||||
- no new privileges and empty capability set;
|
||||
- process, memory, CPU, and wall-time limits;
|
||||
- parent-enforced timeout and kill;
|
||||
- fixed Unix-socket or pipe protocol.
|
||||
|
||||
Static source checks and Python-level context restrictions do not replace these
|
||||
controls.
|
||||
|
||||
Repository templates:
|
||||
|
||||
```text
|
||||
deploy/systemd/system/jwebot-extension-host@.service
|
||||
deploy/systemd/system/jwebot-extension-host@.socket
|
||||
```
|
||||
|
||||
They specify a fixed unprivileged `jwebot-ext` identity, private AF_UNIX-only
|
||||
network namespace, empty capabilities, read-only system and release paths,
|
||||
private devices/tmp, syscall filtering, and CPU/memory/process/runtime limits.
|
||||
The service refuses to start unless
|
||||
`/etc/jwebot-extension-host/allowed_signers` is readable and passes that trust
|
||||
file to the Host. No private signing key belongs on the target.
|
||||
|
||||
The templates are not installed or enabled. A later provisioning phase must
|
||||
create the service identity, install the Host virtual environment, prepare
|
||||
release ownership and public-key trust file, run `systemd-analyze verify`, and
|
||||
test the socket protocol on the target Debian version.
|
||||
|
||||
## Parent Client
|
||||
|
||||
`jwebot_kernel.ExtensionHostClient` starts one Host in a new process group with:
|
||||
|
||||
- an explicit verified release directory;
|
||||
- `python -I` by default;
|
||||
- only `LANG`, `LC_ALL`, and `TZ` inherited;
|
||||
- closed unrelated file descriptors;
|
||||
- bounded request/response/stderr sizes;
|
||||
- a parent-enforced wall-time timeout;
|
||||
- process-group kill on timeout.
|
||||
|
||||
The parent discards Host stderr from protocol results and returns generic
|
||||
transport errors. It services only reviewed broker callbacks supplied by the
|
||||
Kernel.
|
||||
|
||||
At Edge startup, signed manifests are loaded from `<state-root>/<plugin-id>/current`,
|
||||
checked against the fixed ownership inventory, registered into one catalog,
|
||||
and frozen. Host health responses must exactly match the signed author,
|
||||
commands, service profiles, plugin ID, and API version.
|
||||
|
||||
`prefer-host` may use the in-process adapter only when a signed release is
|
||||
missing or its Host fails startup health. Once a Host invocation starts, the
|
||||
Kernel never replays that command through another path because the broker may
|
||||
already have committed state. `host-only` requires every reviewed plugin to be
|
||||
signed, installed, and healthy before Edge startup succeeds.
|
||||
|
||||
`jwebot_kernel.UnixExtensionHostClient` matches the systemd socket template. It
|
||||
uses the same bounded JSON-line service exchange and applies one end-to-end
|
||||
socket deadline. Socket absence, timeout, malformed JSON, service protocol
|
||||
violations, and Host error status all fail closed.
|
||||
@@ -0,0 +1,153 @@
|
||||
# Plugin Contract
|
||||
|
||||
Status: authoritative for the `alpha 0.6.x` extension API.
|
||||
|
||||
## Scope
|
||||
|
||||
JWeBot extensions run for one configured, preselected WeChat group. They do not
|
||||
discover groups, choose a destination, switch chats, or send messages directly.
|
||||
|
||||
The kernel owns:
|
||||
|
||||
- OCR event ordering and deduplication;
|
||||
- the durable ledger and outboxes;
|
||||
- acknowledgement ordering;
|
||||
- model and approved network credentials;
|
||||
- selected-group fingerprint verification;
|
||||
- the GUI interaction lock and final WeChat delivery.
|
||||
|
||||
Extensions return structured intent. The kernel decides whether and when that
|
||||
intent becomes a user-visible action.
|
||||
|
||||
## API Version
|
||||
|
||||
The current extension API is:
|
||||
|
||||
```text
|
||||
api_version = "1"
|
||||
```
|
||||
|
||||
An unsupported API version fails manifest validation before extension code is
|
||||
loaded.
|
||||
|
||||
## Event Input
|
||||
|
||||
Extensions receive `jwebot_sdk.Event`, a normalized incoming event containing:
|
||||
|
||||
- event ID and observation timestamp;
|
||||
- untrusted display sender label;
|
||||
- bounded message text;
|
||||
- confidence values;
|
||||
- the current group fingerprint;
|
||||
- mention flag and source metadata.
|
||||
|
||||
The group fingerprint is evidence for kernel delivery safety. It is not a group
|
||||
selector and must not be used to discover or address another chat.
|
||||
|
||||
Display labels are conversational context only. They never grant permission.
|
||||
|
||||
## Result Output
|
||||
|
||||
An extension returns `jwebot_sdk.ExtensionResult`:
|
||||
|
||||
```python
|
||||
ExtensionResult(
|
||||
reply=ReplyPlan.text("hello"),
|
||||
facts=(Fact(kind="topic", text="likes servers"),),
|
||||
metadata={"source": "example"},
|
||||
)
|
||||
```
|
||||
|
||||
Limits:
|
||||
|
||||
- reply text: at most 900 characters;
|
||||
- facts: at most 12;
|
||||
- fact text: at most 120 characters;
|
||||
- metadata: at most 16 short string entries.
|
||||
|
||||
`ReplyPlan.no_reply()` explicitly requests no visible response.
|
||||
|
||||
The extension cannot select a target group, draft GUI input, or press Send.
|
||||
|
||||
## Extension Types
|
||||
|
||||
### Command
|
||||
|
||||
Registers one or more reviewed slash-command namespaces. It may return one
|
||||
reply plan and bounded facts.
|
||||
|
||||
### Enrichment
|
||||
|
||||
Consumes a normalized event off the reply-critical path. It may emit bounded
|
||||
facts but cannot register commands or send replies by itself.
|
||||
|
||||
### Scheduled
|
||||
|
||||
Creates a scheduled intent that re-enters the kernel's ordered event path. It
|
||||
never delivers directly.
|
||||
|
||||
Passive automatic reply extensions are not supported in API version 1.
|
||||
|
||||
## Manifest
|
||||
|
||||
Every extension has `plugin.toml`:
|
||||
|
||||
```toml
|
||||
id = "hello-command"
|
||||
author = "example"
|
||||
description = "Return a local greeting"
|
||||
help = ["/hello [name]"]
|
||||
version = "1.0.0"
|
||||
api_version = "1"
|
||||
entrypoint = "hello_command:plugin"
|
||||
type = "command"
|
||||
|
||||
[capabilities]
|
||||
commands = ["/hello"]
|
||||
storage = false
|
||||
network_profiles = []
|
||||
```
|
||||
|
||||
Unknown manifest keys and unknown capability names are rejected.
|
||||
`author` is required, bounded, included in release metadata, and reviewed
|
||||
against the extension's ownership directory.
|
||||
Command plugins must declare bounded `description` and `help` metadata. The two
|
||||
fields appear together; help entries begin with `/` and feed the
|
||||
Kernel-generated `/功能列表`.
|
||||
|
||||
## Lifecycle
|
||||
|
||||
1. CI validates source, tests, manifest, API version, and capabilities.
|
||||
2. Review approves the requested command and network namespaces.
|
||||
3. CI builds an immutable artifact and canonical release record.
|
||||
4. A reviewed publisher signs that exact release record with OpenSSH `sshsig`.
|
||||
5. The target verifies the publisher, artifact digest, and API version.
|
||||
6. The extension host re-verifies installed content and the publisher signature.
|
||||
7. The extension host starts the approved release under resource limits.
|
||||
8. The kernel registers approved manifests during startup and rejects conflicts.
|
||||
9. The kernel freezes the extension catalog before runtime routing begins.
|
||||
10. The kernel supplies a `SingleGroupContext`.
|
||||
11. The extension registers only its declared behavior.
|
||||
12. Invocation returns an `ExtensionResult` before its deadline.
|
||||
13. The kernel validates and orders any resulting action.
|
||||
|
||||
The local Host, signed activation, and rollback contracts are implemented.
|
||||
Provisioning and production enablement on production host remain separate work.
|
||||
|
||||
Static source validation rejects obvious access to processes, sockets, files,
|
||||
dynamic execution, and Python object-introspection escape hatches. This is a CI
|
||||
prefilter only. Production activation still requires a separately sandboxed
|
||||
extension host with OS-enforced limits.
|
||||
|
||||
## Non-Negotiable Rules
|
||||
|
||||
- No GUI access.
|
||||
- No direct WeChat send.
|
||||
- No chat switching or group enumeration.
|
||||
- No raw OCR frame or screenshot access.
|
||||
- No direct ledger or SQLite access.
|
||||
- No environment or secret reads.
|
||||
- No shell, subprocess, SSH, or Docker access.
|
||||
- No arbitrary network access.
|
||||
- No background thread or process ownership.
|
||||
- All behavior requires deterministic tests.
|
||||
@@ -0,0 +1,119 @@
|
||||
# Project Rules
|
||||
|
||||
These rules describe the mandatory workflow for JWeBot changes. They are not
|
||||
advice; future changes must either follow them or document an explicit user
|
||||
exception.
|
||||
|
||||
## Release Discipline
|
||||
|
||||
- Version format is `alpha X.Y.Z.MMDD`.
|
||||
- Patch changes increment `Z`; minor version changes require explicit user
|
||||
approval.
|
||||
- A release change must update:
|
||||
- `VERSION`;
|
||||
- root `pyproject.toml`;
|
||||
- every package `pyproject.toml`;
|
||||
- every package `__version__`;
|
||||
- `README.md`;
|
||||
- `CHANGELOG.md`.
|
||||
- Do not mark production complete merely because code was committed. Production
|
||||
is complete only after staging, cutover, acceptance, observation, and backup
|
||||
restore checks pass.
|
||||
|
||||
## Mandatory Pre-Production Gates
|
||||
|
||||
Before touching production runtime files on production host:
|
||||
|
||||
1. Local working tree must be understood and intentionally staged.
|
||||
2. Local gate must pass:
|
||||
|
||||
```sh
|
||||
PATH="$PWD/.venv/bin:$PATH" ./tools/check-local.sh
|
||||
```
|
||||
|
||||
3. Upload a clean staging snapshot using Git-tracked files only.
|
||||
4. Verify staging file hashes against the local tree.
|
||||
5. Run the complete production host gate with:
|
||||
|
||||
```sh
|
||||
PATH=/opt/wechat-local-ocr/venv/bin:$PATH ./tools/check-local.sh
|
||||
```
|
||||
|
||||
6. Verify WeChat is logged in and the ready main window is at least `700x500`.
|
||||
7. Verify OCR queues are empty.
|
||||
8. Verify Edge SQLite `PRAGMA quick_check` returns `ok`.
|
||||
|
||||
## Production Cutover
|
||||
|
||||
- Use only `deploy/scripts/deploy-jwebot-production.sh` for normal production
|
||||
cutover.
|
||||
- The deployer must record both root and user systemd topology, runtime files,
|
||||
configuration, and an online SQLite backup.
|
||||
- The deployer must disable legacy system OCR services and keep the active
|
||||
production topology to:
|
||||
|
||||
```text
|
||||
jwebot-edge.service
|
||||
wechat-ocr-adaptive-recorder.service
|
||||
```
|
||||
|
||||
- Never skip Edge health, adaptive restart stability, queue, WeChat ready, or
|
||||
SQLite gates.
|
||||
- If any gate fails, confirm rollback state before retrying.
|
||||
|
||||
## Backup And Restore
|
||||
|
||||
- Pre-deploy and post-deploy baselines must include runtime files, systemd
|
||||
units, environment/configuration, service states, and an online SQLite backup.
|
||||
- Local and router Restic snapshots must be restored into isolated directories.
|
||||
- Restore validation requires:
|
||||
- `sha256sum -c SHA256SUMS`;
|
||||
- SQLite `PRAGMA quick_check`;
|
||||
- no restore into live paths during validation.
|
||||
|
||||
## GUI Delivery
|
||||
|
||||
- The guarded sender must validate the selected group before drafting and after
|
||||
drafting.
|
||||
- The sender may click a scaled send-button coordinate first, but must retain a
|
||||
fallback path and visual confirmation.
|
||||
- Any GUI send optimization must keep:
|
||||
- GUI lock acquisition;
|
||||
- tokenized draft/send protocol;
|
||||
- group fingerprint revalidation;
|
||||
- uncertain-delivery quarantine;
|
||||
- privacy-safe per-step telemetry.
|
||||
|
||||
## Telemetry And Reports
|
||||
|
||||
- Latency work must preserve or improve per-step telemetry.
|
||||
- The following stages must be visible when relevant:
|
||||
- OCR capture and scan;
|
||||
- queue wait and process;
|
||||
- Edge routing;
|
||||
- fast ACK preparation, GUI send, confirmation, and end-to-end latency;
|
||||
- provider generation;
|
||||
- final GUI delivery;
|
||||
- `gui.send.step`, `gui.bridge.step`, and `gui.send.total`.
|
||||
- Reports must separate p50/p95 for DeepSeek and OpenClaw when both are used.
|
||||
- Reports must describe fixed waits, polling intervals, screenshot captures,
|
||||
VNC input actions, and fallback paths in plain Chinese.
|
||||
- Telemetry must not log message body, sender name, raw group id, reply text,
|
||||
secrets, environment values, or provider payloads.
|
||||
|
||||
## Plugin Rules
|
||||
|
||||
- Registered slash commands must be declared in the ownership inventory.
|
||||
- Prefer reviewed plugin descriptors plus generated adapters for new commands.
|
||||
- Run `tools/sync-local-extensions.py --check` after plugin inventory changes.
|
||||
- Plugin code must not access GUI, raw SQLite/Ledger, shell, SSH, Docker,
|
||||
environment secrets, or unrestricted network.
|
||||
- Runtime command ownership freezes at startup. Do not mutate command ownership
|
||||
during request handling.
|
||||
|
||||
## Access Rules
|
||||
|
||||
- External production access uses an operator-provided SSH host alias.
|
||||
- Root access is obtained by logging in as `jwebot` and then running `su -`.
|
||||
- Do not enable direct root SSH or loosen the jump key `no-port-forwarding`
|
||||
restrictions without explicit user approval.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Prompt For AI Extension Authors
|
||||
|
||||
Use this prompt with an AI coding assistant:
|
||||
|
||||
```text
|
||||
Create or update a JWeBot extension using only the repository's
|
||||
jwebot_sdk public API and docs/plugin-contract.md.
|
||||
|
||||
Non-negotiable constraints:
|
||||
- One preselected group only.
|
||||
- No GUI, VNC, WeChat send, or chat switching.
|
||||
- No raw OCR frames or screenshots.
|
||||
- No direct SQLite, ledger, filesystem, or environment access.
|
||||
- No subprocess, shell, SSH, Docker, or arbitrary sockets.
|
||||
- No API keys or secrets in source, tests, manifest, logs, or docs.
|
||||
- Network access only through a declared approved network profile.
|
||||
- Return ExtensionResult with ReplyPlan and bounded Facts.
|
||||
- Register only commands declared in plugin.toml.
|
||||
- Preserve the reviewed author field in plugin.toml.
|
||||
- Add a concise description and command-oriented help entries in plugin.toml.
|
||||
- Add deterministic unit tests for success and failure behavior.
|
||||
- Keep api_version = "1".
|
||||
|
||||
Start from a skeleton generated by:
|
||||
./tools/create-plugin.py <plugin-id> /<command> --author <author> \
|
||||
--description "<visible description>" --help-entry "/<command> [arguments]"
|
||||
|
||||
Before finishing, run:
|
||||
./tools/validate-plugin.py <extension-dir>/plugin.toml
|
||||
./tools/check-local.sh
|
||||
```
|
||||
@@ -0,0 +1,182 @@
|
||||
# Extension Release Artifacts
|
||||
|
||||
Status: digest verification, OpenSSH publisher signing, and local atomic
|
||||
activation implemented; production host provisioning and activation are not yet
|
||||
implemented.
|
||||
|
||||
## Build
|
||||
|
||||
```sh
|
||||
./tools/package-plugin.py examples/hello-command --output dist
|
||||
```
|
||||
|
||||
The builder:
|
||||
|
||||
1. validates `plugin.toml`;
|
||||
2. applies the static source policy;
|
||||
3. rejects symlinks and unsupported runtime files;
|
||||
4. bounds file count and expanded size;
|
||||
5. hashes canonical relative paths and bytes;
|
||||
6. creates a deterministic gzip/tar artifact;
|
||||
7. writes a canonical release record.
|
||||
|
||||
Output names are content-addressed:
|
||||
|
||||
```text
|
||||
<plugin-id>-<version>-<source-digest-prefix>.tar.gz
|
||||
<plugin-id>-<version>-<source-digest-prefix>.release.json
|
||||
```
|
||||
|
||||
## Verify
|
||||
|
||||
```sh
|
||||
./tools/verify-release.py \
|
||||
dist/<artifact>.tar.gz \
|
||||
dist/<artifact>.release.json
|
||||
```
|
||||
|
||||
Verification does not extract the archive. It checks:
|
||||
|
||||
- artifact name, size, and SHA-256;
|
||||
- regular-file-only tar members;
|
||||
- no absolute paths or `..` traversal;
|
||||
- fixed `plugin/` archive root;
|
||||
- bounded expanded size and file count;
|
||||
- exact file list;
|
||||
- canonical source digest.
|
||||
|
||||
## Determinism
|
||||
|
||||
File modification times, owners, groups, and host paths are not included in the
|
||||
artifact. Identical reviewed inputs produce identical artifact bytes and
|
||||
digests.
|
||||
|
||||
If a content-addressed destination already exists with different bytes, the
|
||||
builder fails instead of replacing it.
|
||||
|
||||
## Release Record
|
||||
|
||||
The JSON record includes:
|
||||
|
||||
- schema version `2`;
|
||||
- plugin ID, manifest author, and plugin version;
|
||||
- SDK API version;
|
||||
- source digest;
|
||||
- artifact file name;
|
||||
- artifact SHA-256 and size;
|
||||
- exact runtime file list.
|
||||
|
||||
## Publisher Signing
|
||||
|
||||
Signing uses the OpenSSH `sshsig` format with namespace
|
||||
`jwebot-release-v1`. Its domain-separated payload binds the publisher
|
||||
principal, release filename, and exact canonical release JSON. The release JSON
|
||||
already binds the artifact digest, size, source digest, author, and file list.
|
||||
|
||||
Create an Ed25519 publisher key outside the repository:
|
||||
|
||||
```sh
|
||||
ssh-keygen -t ed25519 -f ~/.config/jwebot/publisher -C jwebot-publisher
|
||||
```
|
||||
|
||||
Create an OpenSSH allowed-signers file containing only reviewed public keys:
|
||||
|
||||
```text
|
||||
example-publisher ssh-ed25519 AAAA... jwebot-publisher
|
||||
```
|
||||
|
||||
The verifier intentionally accepts only simple entries with explicit
|
||||
lowercase publisher principals and Ed25519 keys. Wildcards, signer options, and
|
||||
other key algorithms are rejected.
|
||||
|
||||
Build and sign in one step:
|
||||
|
||||
```sh
|
||||
./tools/package-plugin.py examples/hello-command \
|
||||
--output dist \
|
||||
--signing-key ~/.config/jwebot/publisher \
|
||||
--publisher example-publisher
|
||||
```
|
||||
|
||||
An existing canonical release record can be signed separately:
|
||||
|
||||
```sh
|
||||
./tools/sign-release.py dist/<release>.release.json \
|
||||
--private-key ~/.config/jwebot/publisher \
|
||||
--publisher example-publisher
|
||||
```
|
||||
|
||||
The signing operation first verifies the referenced artifact's name, digest,
|
||||
size, archive structure, file list, and source digest. It refuses to sign a
|
||||
record whose sibling artifact is missing or inconsistent.
|
||||
|
||||
The private key must never be committed, copied into an extension artifact, or
|
||||
installed on production host. The target receives only the reviewed public-key
|
||||
allowed-signers file.
|
||||
|
||||
## Security Boundary
|
||||
|
||||
Digest verification proves content integrity relative to the release record.
|
||||
Publisher verification proves that the exact canonical record was signed by a
|
||||
key authorized for the declared publisher principal. The manifest `author`
|
||||
identifies who wrote the plugin; signature `publisher` identifies who approved
|
||||
and released those exact bytes.
|
||||
|
||||
Before production activation, a later phase must still add:
|
||||
|
||||
- reviewed public-key provisioning on production host;
|
||||
- isolated extension-host health checks;
|
||||
- target Debian `systemd-analyze verify` and socket protocol checks.
|
||||
|
||||
production host must never execute a mutable branch checkout or ordinary `git pull`.
|
||||
|
||||
## Local Activation Simulation
|
||||
|
||||
Activation requires an explicit state root:
|
||||
|
||||
```sh
|
||||
./tools/activate-release.py \
|
||||
dist/<artifact>.tar.gz \
|
||||
dist/<artifact>.release.json \
|
||||
--signature dist/<release>.release.sig.json \
|
||||
--allowed-signers ~/.config/jwebot/allowed_signers \
|
||||
--publisher example-publisher \
|
||||
--state-root /tmp/jwebot-extension-state
|
||||
```
|
||||
|
||||
The activator:
|
||||
|
||||
- verifies the artifact before writing release data;
|
||||
- verifies the publisher signature against the explicit trust file;
|
||||
- installs under `releases/<full-source-digest>/`;
|
||||
- preserves the signature record beside the installed release record;
|
||||
- writes files with private permissions;
|
||||
- re-verifies an already installed release before reuse;
|
||||
- starts the isolated Host and requires a matching health response before
|
||||
switching `current`;
|
||||
- switches a relative `current` symlink atomically;
|
||||
- preserves the prior verified target as `previous`;
|
||||
- serializes changes with a file lock.
|
||||
|
||||
Rollback:
|
||||
|
||||
```sh
|
||||
./tools/rollback-release.py \
|
||||
--state-root /tmp/jwebot-extension-state \
|
||||
--allowed-signers ~/.config/jwebot/allowed_signers \
|
||||
--publisher example-publisher
|
||||
```
|
||||
|
||||
Rollback re-verifies the destination content and publisher signature, then
|
||||
atomically swaps `current` and `previous`. It does not execute plugin code.
|
||||
|
||||
Unsigned activation is rejected by default. Tests may opt into the explicit
|
||||
`--allow-unsigned-local` flag; that flag cannot be combined with signature
|
||||
arguments and must never be used for production state.
|
||||
|
||||
Local packaging tests that cannot launch the installed Host may use
|
||||
`--skip-health-check-local`. This is also an explicit non-production override;
|
||||
signed production activation fails closed when no health check is supplied.
|
||||
|
||||
These commands have no default production path. A caller must provide
|
||||
`--state-root`. The repository does not invoke them against production host.
|
||||
@@ -0,0 +1,95 @@
|
||||
# Repository Layout
|
||||
|
||||
JWeBot is a monorepo with explicit boundaries between public contracts,
|
||||
privileged runtime code, reviewed plugins, deployment tooling, and local-only
|
||||
configuration.
|
||||
|
||||
```text
|
||||
JWeBot/
|
||||
config/
|
||||
templates/
|
||||
schemas/
|
||||
deploy/
|
||||
defaults/
|
||||
scripts/
|
||||
systemd/
|
||||
docs/
|
||||
runbooks/
|
||||
examples/
|
||||
extensions/
|
||||
official/
|
||||
jwebot/
|
||||
friends/
|
||||
packages/
|
||||
jwebot-config/
|
||||
jwebot-sdk/
|
||||
jwebot-kernel/
|
||||
jwebot-extension-host/
|
||||
jwebot-official-extensions/
|
||||
tests/
|
||||
tools/
|
||||
jwebot_edge.py
|
||||
jwebot_edge_delivery.py
|
||||
wechat_local_ocr.py
|
||||
wechat_adaptive_recorder.py
|
||||
wechat_adaptive_gui_bridge.py
|
||||
wechat_health_watch.py
|
||||
wechat-gui-bridge
|
||||
```
|
||||
|
||||
## Package Responsibilities
|
||||
|
||||
`jwebot-config`
|
||||
|
||||
- validates profile packages;
|
||||
- compiles generated runtime files atomically;
|
||||
- imports and redacts profile archives;
|
||||
- migrates private context and diagnostic material out of old checkouts.
|
||||
|
||||
`jwebot-sdk`
|
||||
|
||||
- contains stable data contracts exposed to plugins;
|
||||
- has no process, filesystem, raw socket, GUI, or environment capability.
|
||||
|
||||
`jwebot-kernel`
|
||||
|
||||
- owns routing, command ownership, capability enforcement, ledger access,
|
||||
release verification, provider adapters, and delivery policy.
|
||||
|
||||
`jwebot-extension-host`
|
||||
|
||||
- executes one verified extension release per isolated invocation;
|
||||
- runs behind an AF_UNIX socket and systemd sandbox.
|
||||
|
||||
`jwebot-official-extensions`
|
||||
|
||||
- contains the reviewed local command engine;
|
||||
- provides generated adapters that return structured SDK plans;
|
||||
- owns the fixed-host Steam service profile, not raw plugin network access.
|
||||
|
||||
## Root Entrypoints
|
||||
|
||||
Root Python and shell files remain compatibility entrypoints for systemd and
|
||||
existing operators. Business logic belongs in packages. Compatibility modules
|
||||
must stay thin and tested.
|
||||
|
||||
## Extension Collections
|
||||
|
||||
- `extensions/official/`: platform-owned service adapters.
|
||||
- `extensions/jwebot/`: built-in reviewed commands.
|
||||
- `extensions/friends/`: contributor-owned plugins.
|
||||
|
||||
The ownership inventory is authoritative. `tools/sync-local-extensions.py`
|
||||
generates manifests, adapters, package tests, and README files from it.
|
||||
|
||||
## Configuration Boundary
|
||||
|
||||
The repository contains only templates and schemas. Real values live under
|
||||
`~/.config/jwebot/`; runtime/diagnostic data lives under
|
||||
`~/.local/state/jwebot/`. See `config/README.md`.
|
||||
|
||||
## Documentation Boundary
|
||||
|
||||
Committed documentation describes architecture, contracts, generic operations,
|
||||
and sanitized examples. Machine-specific incident reports and debug logs are
|
||||
local profile state, not documentation source.
|
||||
@@ -0,0 +1,134 @@
|
||||
# Edge Runbook
|
||||
|
||||
JWeBot Edge is the local source of truth for accepted events, command state,
|
||||
provider work, acknowledgements, and final delivery.
|
||||
|
||||
## Routes
|
||||
|
||||
| Input after mention removal | Route | Execution |
|
||||
|---|---|---|
|
||||
| `/...` | Local | Kernel command or reviewed plugin |
|
||||
| ordinary text | Chat | Direct DeepSeek client |
|
||||
| `#...` | Cloud | Optional configured cloud agent |
|
||||
|
||||
The bot mention is read from the active profile. The public default is
|
||||
`@JWeBot`.
|
||||
|
||||
## Steam
|
||||
|
||||
Commands:
|
||||
|
||||
```text
|
||||
/steam
|
||||
/steam在线
|
||||
/steam游戏
|
||||
/steam全部
|
||||
/steam刷新
|
||||
/steam时长 游戏名
|
||||
/steam时长游戏名
|
||||
/steam时长游戏名、玩家名
|
||||
/steam时长游戏名人=玩家名
|
||||
```
|
||||
|
||||
The configured Steam account supplies the owner ID and API key. At runtime the
|
||||
provider:
|
||||
|
||||
1. uses private per-person SteamID mappings when configured, otherwise fetches
|
||||
the public friend list;
|
||||
2. batches public player summaries in groups of 100;
|
||||
3. labels mapped accounts with their group display IDs and accepts their
|
||||
private Steam query aliases;
|
||||
4. fuzzy matches the requested game against the owner's public library;
|
||||
5. falls back to scoped public libraries if the owner has no match;
|
||||
6. requests the selected AppID for every scoped account with bounded
|
||||
concurrency;
|
||||
7. sorts public playtime descending and summarizes private/unavailable data.
|
||||
|
||||
For a player-specific query, `、`, `,`, `,`, `@`, or `人=` explicitly separates
|
||||
the game from the player. The provider matches the live persona name and only
|
||||
requests that account's game library; ambiguous matches are rejected instead
|
||||
of guessed.
|
||||
|
||||
Steam access is fixed to `api.steampowered.com` through the configured loopback
|
||||
Mihomo HTTP proxy. There is no direct-network fallback.
|
||||
|
||||
## Config
|
||||
|
||||
Inspect active profile metadata without printing values:
|
||||
|
||||
```sh
|
||||
./tools/jwebot-config.py status
|
||||
```
|
||||
|
||||
Validate and atomically regenerate:
|
||||
|
||||
```sh
|
||||
./tools/jwebot-config.py activate <profile>
|
||||
```
|
||||
|
||||
Services read:
|
||||
|
||||
```text
|
||||
~/.config/jwebot/current/generated/runtime.env
|
||||
```
|
||||
|
||||
Private context, debug state, incidents, and root evidence live outside Git as
|
||||
documented in `config/README.md`.
|
||||
|
||||
## Health
|
||||
|
||||
```sh
|
||||
runtime_python=${JWEBOT_RUNTIME_PYTHON:-/opt/jwebot/venv/bin/python}
|
||||
|
||||
printf '%s\n' '{"op":"health"}' |
|
||||
"$runtime_python" ~/.local/lib/wechat-health-watch/jwebot_edge.py respond
|
||||
|
||||
"$runtime_python" ~/.local/lib/wechat-health-watch/jwebot_edge.py status
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
- Edge and adaptive recorder active;
|
||||
- `NRestarts=0` after the settle window;
|
||||
- OCR queues empty;
|
||||
- direct/cloud outboxes empty or actively progressing;
|
||||
- SQLite `PRAGMA quick_check` returns `ok`;
|
||||
- legacy OCR services disabled and inactive.
|
||||
|
||||
## Delivery Contract
|
||||
|
||||
- Fast ACK is persisted before GUI delivery.
|
||||
- Final work starts independently from final GUI delivery.
|
||||
- A final reply is not delivered until its ACK is visibly confirmed.
|
||||
- Steam uses a dedicated progress ACK.
|
||||
- ACK/local work may pass a queued final that has not started.
|
||||
- Active GUI work is non-preemptive.
|
||||
- The GUI sender remains strictly serial.
|
||||
- Every final send rechecks the selected group fingerprint.
|
||||
|
||||
## Failure Handling
|
||||
|
||||
Provider failure:
|
||||
|
||||
- keep credentials and provider payloads out of logs;
|
||||
- record only bounded error classes;
|
||||
- retry through durable queue policy;
|
||||
- do not bypass the configured proxy.
|
||||
|
||||
GUI uncertainty:
|
||||
|
||||
- never blindly replay a send after `send_started`;
|
||||
- preserve the uncertain artifact in private incident state;
|
||||
- require visual/operator confirmation before retry.
|
||||
|
||||
Group mismatch:
|
||||
|
||||
- skip delivery;
|
||||
- keep the event ledgered;
|
||||
- do not select or navigate to another group automatically.
|
||||
|
||||
## Telemetry
|
||||
|
||||
Telemetry may include step name, status, elapsed time, queue state, retry count,
|
||||
and hashed event/group IDs. It must never include message bodies, sender names,
|
||||
reply text, environment values, credentials, SteamIDs, or provider payloads.
|
||||
@@ -0,0 +1,115 @@
|
||||
# Production Deployment
|
||||
|
||||
Use this runbook only after a target host has passed the zero-to-running
|
||||
bootstrap and staging gate.
|
||||
|
||||
## Inputs
|
||||
|
||||
- clean repository snapshot;
|
||||
- active validated profile for the target runtime user;
|
||||
- configured production Python;
|
||||
- operator-provided SSH alias and local privilege elevation;
|
||||
- ready WeChat main window on display `:1`.
|
||||
|
||||
Do not place hostnames, addresses, usernames, passwords, private keys, or
|
||||
profile values in the repository or command history.
|
||||
|
||||
## Local Gate
|
||||
|
||||
```sh
|
||||
PATH="$PWD/.venv/bin:$PATH" ./tools/check-local.sh
|
||||
./tools/check-secrets.py --history
|
||||
```
|
||||
|
||||
## Staging
|
||||
|
||||
Transfer a clean snapshot without `.git`, local profiles, virtualenvs, debug
|
||||
state, captures, databases, or archives. On the target:
|
||||
|
||||
```sh
|
||||
JWEBOT_RUNTIME_PYTHON=/opt/jwebot/venv/bin/python \
|
||||
PATH="/opt/jwebot/venv/bin:$PATH" \
|
||||
./tools/check-local.sh
|
||||
```
|
||||
|
||||
Verify the active profile:
|
||||
|
||||
```sh
|
||||
/opt/jwebot/venv/bin/python -m jwebot_config status
|
||||
/opt/jwebot/venv/bin/python -m jwebot_config validate <profile>
|
||||
```
|
||||
|
||||
## Pre-Cutover
|
||||
|
||||
Confirm:
|
||||
|
||||
1. WeChat main window is visible and meets minimum geometry.
|
||||
2. Edge and adaptive recorder are active with stable restart counts.
|
||||
3. Adaptive and legacy OCR queues are empty.
|
||||
4. Edge direct/cloud outboxes have no failed backlog.
|
||||
5. Edge SQLite `quick_check` is `ok`.
|
||||
6. Legacy OCR services are disabled and inactive.
|
||||
7. A restorable baseline exists.
|
||||
|
||||
## Coordinated Cutover
|
||||
|
||||
Elevate locally on the target and run:
|
||||
|
||||
```sh
|
||||
JWEBOT_RUNTIME_PYTHON=/opt/jwebot/venv/bin/python \
|
||||
./deploy/scripts/deploy-jwebot-production.sh \
|
||||
/path/to/staged/JWeBot \
|
||||
jwebot
|
||||
```
|
||||
|
||||
The deployer prints an eight-stage progress bar and:
|
||||
|
||||
- records root and user service topology;
|
||||
- copies root and user runtime files;
|
||||
- backs up generated configuration;
|
||||
- creates a SQLite online backup and runs `quick_check`;
|
||||
- refuses non-empty queues or an unready WeChat window;
|
||||
- installs the runtime;
|
||||
- leaves legacy OCR disabled;
|
||||
- verifies Edge health and adaptive restart stability;
|
||||
- writes and verifies backup SHA-256 checksums;
|
||||
- rolls back files, state, and unit topology on failure.
|
||||
|
||||
## Post-Cutover
|
||||
|
||||
```sh
|
||||
systemctl --user is-active jwebot-edge.service
|
||||
systemctl --user is-active wechat-ocr-adaptive-recorder.service
|
||||
systemctl --user show wechat-ocr-adaptive-recorder.service \
|
||||
-p NRestarts --value
|
||||
```
|
||||
|
||||
```sh
|
||||
printf '%s\n' '{"op":"health"}' |
|
||||
/opt/jwebot/venv/bin/python \
|
||||
~/.local/lib/wechat-health-watch/jwebot_edge.py respond
|
||||
```
|
||||
|
||||
```sh
|
||||
/opt/jwebot/venv/bin/python - <<'PY'
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
|
||||
path = Path.home() / ".local/state/jwebot-edge/edge.sqlite3"
|
||||
print(sqlite3.connect(f"file:{path}?mode=ro", uri=True)
|
||||
.execute("pragma quick_check").fetchone()[0])
|
||||
PY
|
||||
```
|
||||
|
||||
Player-visible acceptance must use deliberate synthetic messages in the
|
||||
configured group and confirm exactly one ACK and one final response.
|
||||
|
||||
## Rollback
|
||||
|
||||
If any gate fails:
|
||||
|
||||
1. stop further cutover attempts;
|
||||
2. verify the coordinated deployer's rollback result;
|
||||
3. confirm unit topology and SQLite integrity;
|
||||
4. archive privacy-safe evidence under the active profile;
|
||||
5. fix and rerun staging before another production attempt.
|
||||
@@ -0,0 +1,138 @@
|
||||
# Zero To Running
|
||||
|
||||
This runbook prepares a new apt-based Linux host without embedding any private
|
||||
host, account, group, or provider value in the repository.
|
||||
|
||||
JWeBot does not install desktop WeChat itself. Install a compatible client,
|
||||
create an X11/VNC desktop on display `:1`, sign in manually, and keep the main
|
||||
window visible before deployment.
|
||||
|
||||
## 1. Obtain A Clean Snapshot
|
||||
|
||||
Clone or unpack the public repository. Before using a fork:
|
||||
|
||||
```sh
|
||||
./tools/check-secrets.py --history
|
||||
```
|
||||
|
||||
## 2. Prepare The Host
|
||||
|
||||
Run as root:
|
||||
|
||||
```sh
|
||||
./deploy/scripts/bootstrap-jwebot.sh \
|
||||
prepare \
|
||||
/path/to/JWeBot \
|
||||
jwebot \
|
||||
default
|
||||
```
|
||||
|
||||
The six-stage prepare flow:
|
||||
|
||||
1. installs OS packages for Python, X11 interaction, screenshots, clipboard,
|
||||
and OCR fallback;
|
||||
2. creates the runtime account when absent;
|
||||
3. creates `/opt/jwebot/venv`;
|
||||
4. installs runtime Python dependencies and JWeBot packages;
|
||||
5. generates a private config profile;
|
||||
6. creates private state directories.
|
||||
|
||||
## 3. Configure
|
||||
|
||||
Edit:
|
||||
|
||||
```text
|
||||
/home/jwebot/.config/jwebot/profiles/default/
|
||||
```
|
||||
|
||||
At minimum:
|
||||
|
||||
- set the bot mention and provider URLs in component TOML;
|
||||
- put API keys only in `secrets/runtime.env`;
|
||||
- configure Steam owner ID and loopback proxy when using `/steam`;
|
||||
- put person aliases only in `context/people.json`;
|
||||
- configure a notification argv adapter or leave it disabled;
|
||||
- verify deployment paths.
|
||||
|
||||
Activate as the runtime user:
|
||||
|
||||
```sh
|
||||
sudo -u jwebot -H /opt/jwebot/venv/bin/python \
|
||||
-m jwebot_config activate default
|
||||
```
|
||||
|
||||
## 4. Prepare The Desktop
|
||||
|
||||
The runtime expects:
|
||||
|
||||
- an X11 desktop at `DISPLAY=:1`;
|
||||
- the runtime user's `.Xauthority`;
|
||||
- a visible WeChat main window named `Weixin`;
|
||||
- a maximized window large enough for the adaptive layout gate;
|
||||
- loopback VNC on port `5901` for guarded input;
|
||||
- no second process controlling the same GUI.
|
||||
|
||||
VNC/noVNC must remain loopback-only or be reached through an authenticated SSH
|
||||
tunnel. Never expose it directly.
|
||||
|
||||
## 5. Verify
|
||||
|
||||
```sh
|
||||
./deploy/scripts/bootstrap-jwebot.sh \
|
||||
verify \
|
||||
/path/to/JWeBot \
|
||||
jwebot \
|
||||
default
|
||||
```
|
||||
|
||||
Verify checks runtime imports, required executables, profile validation,
|
||||
repository config/privacy gates, and the visible WeChat window.
|
||||
|
||||
## 6. Deploy
|
||||
|
||||
```sh
|
||||
./deploy/scripts/bootstrap-jwebot.sh \
|
||||
deploy \
|
||||
/path/to/JWeBot \
|
||||
jwebot \
|
||||
default
|
||||
```
|
||||
|
||||
This invokes the coordinated production deployer. It does not bypass queue,
|
||||
ready-window, health, SQLite, or restart-stability gates.
|
||||
|
||||
## 7. Acceptance
|
||||
|
||||
```sh
|
||||
systemctl --user is-active jwebot-edge.service
|
||||
systemctl --user is-active wechat-ocr-adaptive-recorder.service
|
||||
```
|
||||
|
||||
```sh
|
||||
printf '%s\n' '{"op":"health"}' |
|
||||
/opt/jwebot/venv/bin/python \
|
||||
~/.local/lib/wechat-health-watch/jwebot_edge.py respond
|
||||
```
|
||||
|
||||
Send synthetic messages in the configured group:
|
||||
|
||||
```text
|
||||
@JWeBot /version
|
||||
@JWeBot /steam
|
||||
@JWeBot /steam时长Dota2
|
||||
```
|
||||
|
||||
Confirm one ACK, one final reply, no duplicate delivery, empty queues, stable
|
||||
restart counts, and SQLite `quick_check=ok`.
|
||||
|
||||
## Non-Apt Hosts
|
||||
|
||||
Use `prepare` as a dependency manifest:
|
||||
|
||||
- install equivalent X11, OCR, Python, and clipboard packages;
|
||||
- create the same runtime user and virtualenv;
|
||||
- run `jwebot_config init/activate`;
|
||||
- run the verify and coordinated deployment gates manually.
|
||||
|
||||
Do not translate package names into an unreviewed installer and run it directly
|
||||
on production.
|
||||
@@ -0,0 +1,129 @@
|
||||
# Runtime Telemetry And ACK Semantics
|
||||
|
||||
Status: local instrumentation contract for `alpha 0.6.x`.
|
||||
|
||||
## What ACK Means
|
||||
|
||||
ACK is the short acknowledgement sent immediately after Edge accepts a request.
|
||||
It is not the final answer and it does not mean the expensive work has
|
||||
completed.
|
||||
|
||||
The ordered path is:
|
||||
|
||||
1. OCR/responder submits one normalized event to Edge.
|
||||
2. Edge durably records the event and queues the requested work.
|
||||
3. Edge returns the short ACK (`喵......` or the cloud acknowledgement).
|
||||
4. The recorder sends that ACK through the guarded GUI path.
|
||||
5. The recorder calls Edge with `fast_ack_sent` or `fast_ack_failed`.
|
||||
6. Expensive work may run concurrently, but its final reply cannot be delivered
|
||||
until ACK delivery is confirmed.
|
||||
|
||||
This separates perceived response latency from completion latency while
|
||||
preserving message order. A failed ACK prevents the later final reply from
|
||||
appearing without its expected acknowledgement.
|
||||
|
||||
## Privacy Boundary
|
||||
|
||||
Telemetry never records message bodies, sender names, raw group identifiers,
|
||||
reply text, secrets, environment values, or service payloads.
|
||||
|
||||
Event and group identifiers are represented only by 12-character SHA-256
|
||||
prefixes. Plugin IDs, route names, queue states, operation names, counts,
|
||||
bounded error class names, and durations are allowed.
|
||||
|
||||
Every structured log uses schema version `1`:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema_version": 1,
|
||||
"name": "edge.ack.confirmed",
|
||||
"status": "ok",
|
||||
"component": "edge",
|
||||
"event_hash": "0123456789ab",
|
||||
"elapsed_ms": 83.417,
|
||||
"ack_kind": "chat",
|
||||
"delivered": true
|
||||
}
|
||||
```
|
||||
|
||||
## Instrumented Stages
|
||||
|
||||
| Stage | Events |
|
||||
|---|---|
|
||||
| Capture and OCR | `ocr.warmup`, `ocr.capture`, `ocr.frame.wait`, `ocr.scan`, `ocr.pipeline` |
|
||||
| OCR queue | `ocr.queue.wait`, `ocr.queue.process` |
|
||||
| Recorder to Edge | `recorder.edge.request` |
|
||||
| ACK GUI and confirmation | `recorder.ack.gui`, `recorder.ack.end_to_end`, `recorder.ack.confirm`, `edge.ack.prepared`, `edge.ack.confirmed` |
|
||||
| Fast ACK dedicated view | `recorder.fast_ack.gui`, `recorder.fast_ack.end_to_end`, `recorder.fast_ack.confirm`, `edge.fast_ack.prepared`, `edge.fast_ack.confirmed` |
|
||||
| Edge routing | `edge.route.classified`, `edge.local.complete`, `edge.alias.complete` |
|
||||
| Extension startup and execution | `edge.extensions.startup`, `edge.extension.execute` |
|
||||
| Host process and IPC | `host.process.spawn`, `host.runtime.load`, `host.request.received`, `host.service.request`, `host.service.wait`, `host.service.complete`, `host.invoke`, `host.process.complete` |
|
||||
| Kernel broker | `edge.service.call` |
|
||||
| Model and cloud providers | `edge.provider.complete` |
|
||||
| Durable queues | `edge.queue.wait`, `edge.queue.claim`, `edge.queue.work` |
|
||||
| Final GUI delivery | `edge.delivery.process`, `edge.delivery.complete`, `edge.delivery.end_to_end`, `delivery.gui.send` |
|
||||
| Guarded GUI send internals | `gui.send.step`, `gui.bridge.step`, `gui.send.total` |
|
||||
|
||||
`gui.send.step` is emitted by the parent OCR process around safety boundaries:
|
||||
GUI lock acquisition, pre-draft screenshot, pre-draft group check, bridge
|
||||
process calls, post-draft screenshot, post-draft group check, callbacks, and
|
||||
total completion. `gui.bridge.step` is emitted from the child bridge timing
|
||||
metadata and covers VNC focus clicks, fixed settle waits, screenshot polling,
|
||||
draft confirmation, send-button confirmation, and Enter fallback.
|
||||
|
||||
The Kernel also retains process-local low-cardinality counts and
|
||||
average/maximum latency summaries. They are exposed through Edge health output
|
||||
for diagnosis and reset when the process restarts.
|
||||
|
||||
`edge.route.classified` records one bounded `execution_path` value:
|
||||
`local-command`, `deepseek`, or `openclaw`. OCR scan events record cache hits,
|
||||
misses, and periodic refresh without recording image or message content.
|
||||
Direct-request queue events use `queue=direct_generation` for parallel provider
|
||||
or Steam work and `queue=direct_delivery` for the single ordered GUI worker.
|
||||
Their bounded `worker` field distinguishes `generation-0`, `generation-1`, and
|
||||
`delivery-0` without exposing event content.
|
||||
|
||||
## Error Rules
|
||||
|
||||
- Record only the exception class, never exception text from plugin or user
|
||||
data boundaries.
|
||||
- Distinguish controlled errors, retries, uncertain delivery, startup fallback,
|
||||
timeouts, and final failures.
|
||||
- Do not replay an active Host invocation after a service call may have
|
||||
committed state.
|
||||
- Keep existing durable queue state as the source of truth; telemetry is
|
||||
observational and must never control delivery.
|
||||
- Telemetry observer and logger failures are swallowed so instrumentation
|
||||
cannot break the reply path.
|
||||
|
||||
## Performance Analysis
|
||||
|
||||
The current points support separate measurement of:
|
||||
|
||||
- screen capture delay;
|
||||
- OCR recognition cost and bubble count;
|
||||
- stable-frame/debounce delay;
|
||||
- recorder-to-Edge socket latency;
|
||||
- ACK GUI latency and ACK confirmation latency;
|
||||
- Host process startup and plugin load cost;
|
||||
- service broker round trips;
|
||||
- model/provider execution;
|
||||
- queue waiting and retry time;
|
||||
- final guarded GUI delivery.
|
||||
|
||||
Future optimization can then trade compute for latency deliberately, for
|
||||
example:
|
||||
|
||||
- narrower OCR regions or separate sender/body recognizers;
|
||||
- more frequent capture with bounded coalescing;
|
||||
- selective high-cost OCR only for likely incoming bubbles;
|
||||
- warm or persistent Extension Hosts;
|
||||
- concurrent ACK delivery and expensive computation;
|
||||
- queue prioritization by route and age;
|
||||
- preloaded models and cached layout/title fingerprints.
|
||||
|
||||
No such optimization should be accepted without comparing end-to-end ACK and
|
||||
final-delivery percentiles and checking duplicate/ordering regressions.
|
||||
|
||||
Run `tools/performance-baseline.py` for repository-only microbenchmarks or pass
|
||||
one or more private `--telemetry` logs to calculate stage distributions.
|
||||
@@ -0,0 +1,26 @@
|
||||
# Versioning
|
||||
|
||||
JWeBot uses an alpha version string:
|
||||
|
||||
```text
|
||||
alpha X.Y.Z.MMDD
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- `alpha` means the public API and deployment process may still change.
|
||||
- `X.Y.Z` is the project version.
|
||||
- `MMDD` is the delivery date.
|
||||
- Normal small updates increment only the patch version, for example
|
||||
`alpha 0.0.1.1014` to `alpha 0.0.2.1014`.
|
||||
- Minor versions such as `0.1.x` require explicit user review and milestone
|
||||
approval.
|
||||
- Local repository migration does not imply a production deployment.
|
||||
- Production services must not be changed unless the operator explicitly asks
|
||||
for deployment or runtime verification.
|
||||
|
||||
The exact current version is stored in:
|
||||
|
||||
```text
|
||||
VERSION
|
||||
```
|
||||
Reference in New Issue
Block a user