mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-11 00:11:53 +08:00
Compare commits
259 Commits
vincentkoc
...
vincentkoc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bec437faf8 | ||
|
|
81c8e66f61 | ||
|
|
0eaf03f55b | ||
|
|
dfc237c319 | ||
|
|
98dcbd3e7e | ||
|
|
371366e9eb | ||
|
|
de503dbcbb | ||
|
|
77d0ff629c | ||
|
|
ca6dbc0f0a | ||
|
|
3bfd093cdb | ||
|
|
aa28d1c711 | ||
|
|
47e0bf522f | ||
|
|
be8fef3840 | ||
|
|
ae7f18e503 | ||
|
|
3b26da4b82 | ||
|
|
749f3e7baa | ||
|
|
4572ddfe2c | ||
|
|
8ab01c5c93 | ||
|
|
f71f44576a | ||
|
|
986b772a89 | ||
|
|
d8b927ee6a | ||
|
|
bc6ca4940b | ||
|
|
46482a283a | ||
|
|
84c0326f4d | ||
|
|
d8e138c743 | ||
|
|
a2cb81199e | ||
|
|
3f12e90f3e | ||
|
|
65ec4843e8 | ||
|
|
a97e1e1611 | ||
|
|
fdfefcaa11 | ||
|
|
dd203c8eee | ||
|
|
b580d142cd | ||
|
|
88b8151c52 | ||
|
|
b37085984d | ||
|
|
61bcdcca9c | ||
|
|
66df5f2bcb | ||
|
|
9b7a943eda | ||
|
|
fbe80bdd46 | ||
|
|
53e4359249 | ||
|
|
daecbeeaba | ||
|
|
bd74ce74aa | ||
|
|
e9a8d840a6 | ||
|
|
78f0e35529 | ||
|
|
a1e78fd52f | ||
|
|
86c26bd171 | ||
|
|
fd60b0fc85 | ||
|
|
2ff309db4d | ||
|
|
6f6edbe770 | ||
|
|
99a251e7ca | ||
|
|
3b6652dcd8 | ||
|
|
ee89ffd264 | ||
|
|
8880bc32fa | ||
|
|
7648fe6f8a | ||
|
|
488f1b0ed3 | ||
|
|
4db39a74f1 | ||
|
|
fbc4217443 | ||
|
|
1a0313bb1f | ||
|
|
68a809298d | ||
|
|
8fd4511df5 | ||
|
|
5d4dbf1c7d | ||
|
|
a079c190f9 | ||
|
|
23b68d0349 | ||
|
|
f56aa4dee7 | ||
|
|
3218efcfd8 | ||
|
|
c9a07282e4 | ||
|
|
1c562bf211 | ||
|
|
86befdd2b3 | ||
|
|
2c50e199b7 | ||
|
|
103f92c3ed | ||
|
|
b1ac4e1d8e | ||
|
|
bb34721175 | ||
|
|
96e6ba3046 | ||
|
|
60648a51b3 | ||
|
|
07eae3da90 | ||
|
|
7158406298 | ||
|
|
c2f8549bce | ||
|
|
93e25774da | ||
|
|
8795a5fee6 | ||
|
|
70e4931739 | ||
|
|
c08796b039 | ||
|
|
ac5e97097e | ||
|
|
a516141bda | ||
|
|
0c9428a865 | ||
|
|
7212b5f01a | ||
|
|
ecc688d205 | ||
|
|
acae0b60c2 | ||
|
|
bcdbd03579 | ||
|
|
47a9c1a893 | ||
|
|
6513749ef6 | ||
|
|
c8576ec78b | ||
|
|
38abdea8ce | ||
|
|
6a2efa541b | ||
|
|
c89527f389 | ||
|
|
c6950367fb | ||
|
|
067215629f | ||
|
|
60bf58ddbc | ||
|
|
ec93398d7b | ||
|
|
9785b44307 | ||
|
|
10f4a03de8 | ||
|
|
2b57d3bb34 | ||
|
|
39aba198f1 | ||
|
|
6987a3c8b5 | ||
|
|
0a136f1b90 | ||
|
|
59940cb3ee | ||
|
|
92e765cdee | ||
|
|
7c0cac2740 | ||
|
|
bb76a90dd1 | ||
|
|
6b28668104 | ||
|
|
4ed30abc7a | ||
|
|
70a6d40d37 | ||
|
|
7d2ddf70c1 | ||
|
|
413d2ff3da | ||
|
|
399b6f745a | ||
|
|
57a0534f93 | ||
|
|
fb991e6f31 | ||
|
|
de6666b895 | ||
|
|
d663df7a74 | ||
|
|
1c4f52d6a1 | ||
|
|
961f42e0cf | ||
|
|
1e196db49d | ||
|
|
26a8aee01c | ||
|
|
0958aea112 | ||
|
|
40be12db96 | ||
|
|
71a69e5337 | ||
|
|
9cca8a6de5 | ||
|
|
83ee5c0328 | ||
|
|
9c89a74f84 | ||
|
|
74a57ace10 | ||
|
|
b54e37c71f | ||
|
|
bc5054ce68 | ||
|
|
d56559bad7 | ||
|
|
b8dbc12560 | ||
|
|
7a93f7d9df | ||
|
|
579d0ebe2b | ||
|
|
3aa5f2703c | ||
|
|
e8156c8281 | ||
|
|
59bcac472e | ||
|
|
ae6ee73097 | ||
|
|
66a8c257b9 | ||
|
|
a78b83472e | ||
|
|
18e4e4677c | ||
|
|
8c71b36acb | ||
|
|
a8bee6fb6c | ||
|
|
0da588d2d2 | ||
|
|
33495f32e9 | ||
|
|
da4f82503f | ||
|
|
c0e0115b31 | ||
|
|
a782358c9b | ||
|
|
f87e7be55e | ||
|
|
c455cccd3d | ||
|
|
bad65f130e | ||
|
|
cbb8c43f60 | ||
|
|
eb97535a35 | ||
|
|
dd96be4e95 | ||
|
|
c156f7c7e3 | ||
|
|
a9317a4c28 | ||
|
|
0537f3e597 | ||
|
|
ee7ecb2dd4 | ||
|
|
e42d86afa9 | ||
|
|
1f37203f88 | ||
|
|
c6239bf253 | ||
|
|
70a228cdaa | ||
|
|
1f68e6e89c | ||
|
|
c05cfccc17 | ||
|
|
8e2a1d0941 | ||
|
|
e7555724af | ||
|
|
f4cc93dc7d | ||
|
|
a058bf918d | ||
|
|
c3ed3ba310 | ||
|
|
5a68e8261e | ||
|
|
bb160ebe89 | ||
|
|
6e047eb683 | ||
|
|
c74042ba04 | ||
|
|
fd7e283ac5 | ||
|
|
d040d48af4 | ||
|
|
a4047bf148 | ||
|
|
74c762beb0 | ||
|
|
963237a18f | ||
|
|
9eed6e674b | ||
|
|
684e5ea249 | ||
|
|
4adcfa3256 | ||
|
|
dd40741e18 | ||
|
|
aa1454d1a8 | ||
|
|
4eee827dce | ||
|
|
8b001d6e4d | ||
|
|
392ddb56e2 | ||
|
|
4a0f72866b | ||
|
|
14137bef22 | ||
|
|
50a6902a9a | ||
|
|
1839bc0b1a | ||
|
|
b810e94a17 | ||
|
|
50c8934231 | ||
|
|
5a7aba94a2 | ||
|
|
3735156766 | ||
|
|
47fd8558cd | ||
|
|
7931f06c00 | ||
|
|
4fb0160309 | ||
|
|
b795ba1d02 | ||
|
|
85dd0ab2f8 | ||
|
|
07f890fa45 | ||
|
|
594920f8cc | ||
|
|
a2080421a1 | ||
|
|
4a7fbe090a | ||
|
|
51631e5797 | ||
|
|
42837a04bf | ||
|
|
e2dac5d5cb | ||
|
|
bbb0c3e5d7 | ||
|
|
dd2eb29038 | ||
|
|
c9a8b6f82f | ||
|
|
438991b6a4 | ||
|
|
630958749c | ||
|
|
fc2d29ea92 | ||
|
|
132e459009 | ||
|
|
756d9b5782 | ||
|
|
d88da9f5f8 | ||
|
|
f0202264d0 | ||
|
|
d37e3d582f | ||
|
|
13e256ac9d | ||
|
|
8e97b752d0 | ||
|
|
5e78c8bc95 | ||
|
|
7679eb3752 | ||
|
|
9e2eed211c | ||
|
|
a493f01a90 | ||
|
|
229426a257 | ||
|
|
a47722de7e | ||
|
|
67b2d1b8e8 | ||
|
|
8d44b16b7c | ||
|
|
0c7ae04262 | ||
|
|
7c0a849ed7 | ||
|
|
a60fd3feed | ||
|
|
f5cd7c390d | ||
|
|
87c4ae36b4 | ||
|
|
ec2c6d83b9 | ||
|
|
ff61343d76 | ||
|
|
e4c61723cd | ||
|
|
89e3969d64 | ||
|
|
a472f988d8 | ||
|
|
53462b990d | ||
|
|
b2e9221a8c | ||
|
|
c4265a5f16 | ||
|
|
26e0a3ee9a | ||
|
|
5c5c64b612 | ||
|
|
9d3e653ec9 | ||
|
|
843e3c1efb | ||
|
|
d7ac16788e | ||
|
|
4bb8a65edd | ||
|
|
9616d1e8ba | ||
|
|
a2d73be3a4 | ||
|
|
c33375f843 | ||
|
|
d230bd9c38 | ||
|
|
6a458ef29e | ||
|
|
f77a684131 | ||
|
|
8e04d1fe15 | ||
|
|
3cbf932413 | ||
|
|
d1e4ee03ff | ||
|
|
8e4a1d87e2 | ||
|
|
a97b9014a2 | ||
|
|
8851d06429 | ||
|
|
37c79f84ba |
@@ -1,8 +1,8 @@
|
||||
---
|
||||
description: Update Clawdbot from upstream when branch has diverged (ahead/behind)
|
||||
description: Update OpenClaw from upstream when branch has diverged (ahead/behind)
|
||||
---
|
||||
|
||||
# Clawdbot Upstream Sync Workflow
|
||||
# OpenClaw Upstream Sync Workflow
|
||||
|
||||
Use this workflow when your fork has diverged from upstream (e.g., "18 commits ahead, 29 commits behind").
|
||||
|
||||
@@ -132,16 +132,16 @@ pnpm mac:package
|
||||
|
||||
```bash
|
||||
# Kill running app
|
||||
pkill -x "Clawdbot" || true
|
||||
pkill -x "OpenClaw" || true
|
||||
|
||||
# Move old version
|
||||
mv /Applications/Clawdbot.app /tmp/Clawdbot-backup.app
|
||||
mv /Applications/OpenClaw.app /tmp/OpenClaw-backup.app
|
||||
|
||||
# Install new build
|
||||
cp -R dist/Clawdbot.app /Applications/
|
||||
cp -R dist/OpenClaw.app /Applications/
|
||||
|
||||
# Launch
|
||||
open /Applications/Clawdbot.app
|
||||
open /Applications/OpenClaw.app
|
||||
```
|
||||
|
||||
---
|
||||
@@ -235,7 +235,7 @@ If upstream introduced new model configurations:
|
||||
# Check for OpenRouter API key requirements
|
||||
grep -r "openrouter\|OPENROUTER" src/ --include="*.ts" --include="*.js"
|
||||
|
||||
# Update clawdbot.json with fallback chains
|
||||
# Update openclaw.json with fallback chains
|
||||
# Add model fallback configurations as needed
|
||||
```
|
||||
|
||||
|
||||
80
.github/labeler.yml
vendored
80
.github/labeler.yml
vendored
@@ -198,14 +198,6 @@
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/diagnostics-otel/**"
|
||||
"extensions: google-antigravity-auth":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/google-antigravity-auth/**"
|
||||
"extensions: google-gemini-cli-auth":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/google-gemini-cli-auth/**"
|
||||
"extensions: llm-task":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
@@ -238,15 +230,87 @@
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/acpx/**"
|
||||
"extensions: byteplus":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/byteplus/**"
|
||||
"extensions: anthropic":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/anthropic/**"
|
||||
"extensions: cloudflare-ai-gateway":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/cloudflare-ai-gateway/**"
|
||||
"extensions: minimax-portal-auth":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/minimax-portal-auth/**"
|
||||
"extensions: huggingface":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/huggingface/**"
|
||||
"extensions: kilocode":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/kilocode/**"
|
||||
"extensions: openai":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/openai/**"
|
||||
"extensions: kimi-coding":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/kimi-coding/**"
|
||||
"extensions: minimax":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/minimax/**"
|
||||
"extensions: modelstudio":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/modelstudio/**"
|
||||
"extensions: moonshot":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/moonshot/**"
|
||||
"extensions: nvidia":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/nvidia/**"
|
||||
"extensions: phone-control":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/phone-control/**"
|
||||
"extensions: qianfan":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/qianfan/**"
|
||||
"extensions: synthetic":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/synthetic/**"
|
||||
"extensions: talk-voice":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/talk-voice/**"
|
||||
"extensions: together":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/together/**"
|
||||
"extensions: venice":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/venice/**"
|
||||
"extensions: vercel-ai-gateway":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/vercel-ai-gateway/**"
|
||||
"extensions: volcengine":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/volcengine/**"
|
||||
"extensions: xiaomi":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- "extensions/xiaomi/**"
|
||||
|
||||
23
.github/workflows/ci.yml
vendored
23
.github/workflows/ci.yml
vendored
@@ -232,6 +232,29 @@ jobs:
|
||||
- name: Enforce safe external URL opening policy
|
||||
run: pnpm lint:ui:no-raw-window-open
|
||||
|
||||
startup-memory:
|
||||
name: "startup-memory"
|
||||
needs: [docs-scope, changed-scope]
|
||||
if: needs.docs-scope.outputs.docs_only != 'true' && needs.changed-scope.outputs.run_node == 'true'
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: false
|
||||
|
||||
- name: Setup Node environment
|
||||
uses: ./.github/actions/setup-node-env
|
||||
with:
|
||||
install-bun: "false"
|
||||
use-sticky-disk: "false"
|
||||
|
||||
- name: Build dist
|
||||
run: pnpm build
|
||||
|
||||
- name: Check CLI startup memory
|
||||
run: pnpm test:startup:memory
|
||||
|
||||
# Validate docs (format, lint, broken links) only when docs files changed.
|
||||
check-docs:
|
||||
needs: [docs-scope]
|
||||
|
||||
1
.prettierignore
Normal file
1
.prettierignore
Normal file
@@ -0,0 +1 @@
|
||||
docs/.generated/
|
||||
44
AGENTS.md
44
AGENTS.md
@@ -72,6 +72,8 @@
|
||||
|
||||
- `docs/zh-CN/**` is generated; do not edit unless the user explicitly asks.
|
||||
- Pipeline: update English docs → adjust glossary (`docs/.i18n/glossary.zh-CN.json`) → run `scripts/docs-i18n` → apply targeted fixes only if instructed.
|
||||
- Before rerunning `scripts/docs-i18n`, add glossary entries for any new technical terms, page titles, or short nav labels that must stay in English or use a fixed translation (for example `Doctor` or `Polls`).
|
||||
- `pnpm docs:check-i18n-glossary` enforces glossary coverage for changed English doc titles and short internal doc labels before translation reruns.
|
||||
- Translation memory: `docs/.i18n/zh-CN.tm.jsonl` (generated).
|
||||
- See `docs/.i18n/README.md`.
|
||||
- The pipeline can be slow/inefficient; if it’s dragging, ping @jospalmbier on Discord instead of hacking around it.
|
||||
@@ -97,7 +99,7 @@
|
||||
- Prefer Bun for TypeScript execution (scripts, dev, tests): `bun <file.ts>` / `bunx <tool>`.
|
||||
- Run CLI in dev: `pnpm openclaw ...` (bun) or `pnpm dev`.
|
||||
- Node remains supported for running built output (`dist/*`) and production installs.
|
||||
- Mac packaging (dev): `scripts/package-mac-app.sh` defaults to current arch. Release checklist: `docs/platforms/mac/release.md`.
|
||||
- Mac packaging (dev): `scripts/package-mac-app.sh` defaults to current arch.
|
||||
- Type-check/build: `pnpm build`
|
||||
- TypeScript checks: `pnpm tsgo`
|
||||
- Lint/format: `pnpm check`
|
||||
@@ -179,7 +181,7 @@
|
||||
- Pi sessions live under `~/.openclaw/sessions/` by default; the base directory is not configurable.
|
||||
- Environment variables: see `~/.profile`.
|
||||
- Never commit or publish real phone numbers, videos, or live configuration values. Use obviously fake placeholders in docs, tests, and examples.
|
||||
- Release flow: always read `docs/reference/RELEASING.md` and `docs/platforms/mac/release.md` before any release work; do not ask routine questions once those docs answer them.
|
||||
- Release flow: use the private [maintainer release docs](https://github.com/openclaw/maintainers/blob/main/release/README.md) for the actual runbook; use `docs/reference/RELEASING.md` for the public release policy.
|
||||
|
||||
## GHSA (Repo Advisory) Patch/Publish
|
||||
|
||||
@@ -256,14 +258,13 @@
|
||||
- If shared guardrails are available locally, review them; otherwise follow this repo's guidance.
|
||||
- SwiftUI state management (iOS/macOS): prefer the `Observation` framework (`@Observable`, `@Bindable`) over `ObservableObject`/`@StateObject`; don’t introduce new `ObservableObject` unless required for compatibility, and migrate existing usages when touching related code.
|
||||
- Connection providers: when adding a new connection, update every UI surface and docs (macOS app, web UI, mobile if applicable, onboarding/overview docs) and add matching status + configuration forms so provider lists and settings stay in sync.
|
||||
- Version locations: `package.json` (CLI), `apps/android/app/build.gradle.kts` (versionName/versionCode), `apps/ios/Sources/Info.plist` + `apps/ios/Tests/Info.plist` (CFBundleShortVersionString/CFBundleVersion), `apps/macos/Sources/OpenClaw/Resources/Info.plist` (CFBundleShortVersionString/CFBundleVersion), `docs/install/updating.md` (pinned npm version), `docs/platforms/mac/release.md` (APP_VERSION/APP_BUILD examples), Peekaboo Xcode projects/Info.plists (MARKETING_VERSION/CURRENT_PROJECT_VERSION).
|
||||
- Version locations: `package.json` (CLI), `apps/android/app/build.gradle.kts` (versionName/versionCode), `apps/ios/Sources/Info.plist` + `apps/ios/Tests/Info.plist` (CFBundleShortVersionString/CFBundleVersion), `apps/macos/Sources/OpenClaw/Resources/Info.plist` (CFBundleShortVersionString/CFBundleVersion), `docs/install/updating.md` (pinned npm version), and Peekaboo Xcode projects/Info.plists (MARKETING_VERSION/CURRENT_PROJECT_VERSION).
|
||||
- "Bump version everywhere" means all version locations above **except** `appcast.xml` (only touch appcast when cutting a new macOS Sparkle release).
|
||||
- **Restart apps:** “restart iOS/Android apps” means rebuild (recompile/install) and relaunch, not just kill/launch.
|
||||
- **Device checks:** before testing, verify connected real devices (iOS/Android) before reaching for simulators/emulators.
|
||||
- iOS Team ID lookup: `security find-identity -p codesigning -v` → use Apple Development (…) TEAMID. Fallback: `defaults read com.apple.dt.Xcode IDEProvisioningTeamIdentifiers`.
|
||||
- A2UI bundle hash: `src/canvas-host/a2ui/.bundle.hash` is auto-generated; ignore unexpected changes, and only regenerate via `pnpm canvas:a2ui:bundle` (or `scripts/bundle-a2ui.sh`) when needed. Commit the hash as a separate commit.
|
||||
- Release signing/notary keys are managed outside the repo; follow internal release docs.
|
||||
- Notary auth env vars (`APP_STORE_CONNECT_ISSUER_ID`, `APP_STORE_CONNECT_KEY_ID`, `APP_STORE_CONNECT_API_KEY_P8`) are expected in your environment (per internal release docs).
|
||||
- Release signing/notary credentials are managed outside the repo; maintainers keep that setup in the private [maintainer release docs](https://github.com/openclaw/maintainers/tree/main/release).
|
||||
- **Multi-agent safety:** do **not** create/apply/drop `git stash` entries unless explicitly requested (this includes `git pull --rebase --autostash`). Assume other agents may be working; keep unrelated WIP untouched and avoid cross-cutting state changes.
|
||||
- **Multi-agent safety:** when the user says "push", you may `git pull --rebase` to integrate latest changes (never discard other agents' work). When the user says "commit", scope to your changes only. When the user says "commit all", commit everything in grouped chunks.
|
||||
- **Multi-agent safety:** do **not** create/remove/modify `git worktree` checkouts (or edit `.worktrees/*`) unless explicitly requested.
|
||||
@@ -290,35 +291,12 @@
|
||||
- Release guardrails: do not change version numbers without operator’s explicit consent; always ask permission before running any npm publish/release step.
|
||||
- Beta release guardrail: when using a beta Git tag (for example `vYYYY.M.D-beta.N`), publish npm with a matching beta version suffix (for example `YYYY.M.D-beta.N`) rather than a plain version on `--tag beta`; otherwise the plain version name gets consumed/blocked.
|
||||
|
||||
## NPM + 1Password (publish/verify)
|
||||
## Release Auth
|
||||
|
||||
- Use the 1password skill; all `op` commands must run inside a fresh tmux session.
|
||||
- Correct 1Password path for npm release auth: `op://Private/Npmjs` (use that item; OTP stays `op://Private/Npmjs/one-time password?attribute=otp`).
|
||||
- Sign in: `eval "$(op signin --account my.1password.com)"` (app unlocked + integration on).
|
||||
- OTP: `op read 'op://Private/Npmjs/one-time password?attribute=otp'`.
|
||||
- Publish: `npm publish --access public --otp="<otp>"` (run from the package dir).
|
||||
- Verify without local npmrc side effects: `npm view <pkg> version --userconfig "$(mktemp)"`.
|
||||
- Kill the tmux session after publish.
|
||||
|
||||
## Plugin Release Fast Path (no core `openclaw` publish)
|
||||
|
||||
- Release only already-on-npm plugins. Source list is in `docs/reference/RELEASING.md` under "Current npm plugin list".
|
||||
- Run all CLI `op` calls and `npm publish` inside tmux to avoid hangs/interruption:
|
||||
- `tmux new -d -s release-plugins-$(date +%Y%m%d-%H%M%S)`
|
||||
- `eval "$(op signin --account my.1password.com)"`
|
||||
- 1Password helpers:
|
||||
- password used by `npm login`:
|
||||
`op item get Npmjs --format=json | jq -r '.fields[] | select(.id=="password").value'`
|
||||
- OTP:
|
||||
`op read 'op://Private/Npmjs/one-time password?attribute=otp'`
|
||||
- Fast publish loop (local helper script in `/tmp` is fine; keep repo clean):
|
||||
- compare local plugin `version` to `npm view <name> version`
|
||||
- only run `npm publish --access public --otp="<otp>"` when versions differ
|
||||
- skip if package is missing on npm or version already matches.
|
||||
- Keep `openclaw` untouched: never run publish from repo root unless explicitly requested.
|
||||
- Post-check for each release:
|
||||
- per-plugin: `npm view @openclaw/<name> version --userconfig "$(mktemp)"` should be `2026.2.17`
|
||||
- core guard: `npm view openclaw version --userconfig "$(mktemp)"` should stay at previous version unless explicitly requested.
|
||||
- Core `openclaw` publish uses GitHub trusted publishing; do not use `NPM_TOKEN` or the plugin OTP flow for core releases.
|
||||
- Separate `@openclaw/*` plugin publishes use a different maintainer-only auth flow.
|
||||
- Plugin scope: only publish already-on-npm `@openclaw/*` plugins. Bundled disk-tree-only plugins stay out.
|
||||
- Maintainers: private 1Password item names, tmux rules, plugin publish helpers, and local mac signing/notary setup live in the private [maintainer release docs](https://github.com/openclaw/maintainers/blob/main/release/README.md).
|
||||
|
||||
## Changelog Release Notes
|
||||
|
||||
|
||||
83
CHANGELOG.md
83
CHANGELOG.md
@@ -6,33 +6,79 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Changes
|
||||
|
||||
- Commands/btw: add `/btw` side questions for quick tool-less answers about the current session without changing future session context, with dismissible in-session TUI answers and explicit BTW replies on external channels. (#45444) Thanks @ngutman.
|
||||
- Refactor/channels: remove the legacy channel shim directories and point channel-specific imports directly at the extension-owned implementations. (#45967) thanks @scoootscooob.
|
||||
- Feishu/streaming: add `onReasoningStream` and `onReasoningEnd` support to streaming cards, so `/reasoning stream` renders thinking tokens as markdown blockquotes in the same card — matching the Telegram channel's reasoning lane behavior.
|
||||
- Feishu/cards: add identity-aware structured card headers and note footers for Feishu replies and direct sends, while keeping that presentation wired through the shared outbound identity path. (#29938) Thanks @nszhsl.
|
||||
- Gateway/health monitor: add configurable stale-event thresholds and restart limits, plus per-channel and per-account `healthMonitor.enabled` overrides, while keeping the existing global disable path on `gateway.channelHealthCheckMinutes=0`. (#42107) Thanks @rstar327.
|
||||
- Android/mobile: add a system-aware dark theme across onboarding and post-onboarding screens so the app follows the device theme through setup, chat, and voice flows. (#46249) Thanks @sibbl.
|
||||
- Commands/btw: add `/btw` side questions for quick tool-less answers about the current session without changing future session context, with dismissible in-session TUI answers and explicit BTW replies on external channels. (#45444) Thanks @ngutman.
|
||||
- Gateway/health monitor: add configurable stale-event thresholds and restart limits, plus per-channel and per-account `healthMonitor.enabled` overrides, while keeping the existing global disable path on `gateway.channelHealthCheckMinutes=0`. (#42107) Thanks @rstar327.
|
||||
- Feishu/cards: add identity-aware structured card headers and note footers for Feishu replies and direct sends, while keeping that presentation wired through the shared outbound identity path. (#29938) Thanks @nszhsl.
|
||||
- Feishu/streaming: add `onReasoningStream` and `onReasoningEnd` support to streaming cards, so `/reasoning stream` renders thinking tokens as markdown blockquotes in the same card — matching the Telegram channel's reasoning lane behavior. (#46029)
|
||||
- Web tools/Firecrawl: add Firecrawl as an `onboard`/configure search provider via a bundled plugin, expose explicit `firecrawl_search` and `firecrawl_scrape` tools, and align core `web_fetch` fallback behavior with Firecrawl base-URL/env fallback plus guarded endpoint fetches.
|
||||
- Refactor/channels: remove the legacy channel shim directories and point channel-specific imports directly at the extension-owned implementations. (#45967) thanks @scoootscooob.
|
||||
- Android/nodes: add `callLog.search` plus shared Call Log permission wiring so Android nodes can search recent call history through the gateway. (#44073) Thanks @lxk7280.
|
||||
- Docs/Zalo: clarify the Marketplace-bot support matrix and config guidance so the Zalo channel docs match current Bot Creator behavior more closely. (#47552) Thanks @No898.
|
||||
- Install/update: allow package-manager installs from GitHub `main` via `openclaw update --tag main`, installer `--version main`, or direct npm/pnpm git specs.
|
||||
- Plugins/providers: move OpenRouter, GitHub Copilot, and OpenAI Codex provider/runtime logic into bundled plugins, including dynamic model fallback, runtime auth exchange, stream wrappers, capability hints, and cache-TTL policy.
|
||||
- Plugins/MiniMax: merge the bundled MiniMax API and MiniMax OAuth plugin surfaces into a single default-on `minimax` plugin, while keeping legacy `minimax-portal-auth` config ids aliased for compatibility.
|
||||
- Plugins/bundles: add compatible Codex, Claude, and Cursor bundle discovery/install support, map bundle skills into OpenClaw skills, and apply Claude bundle `settings.json` defaults to embedded Pi with shell overrides sanitized.
|
||||
- Plugins/agent integrations: broaden the plugin surface for app-server integrations with channel-aware commands, interactive callbacks, inbound claims, and Discord/Telegram conversation binding support. (#45318) Thanks @huntharo and @vincentkoc.
|
||||
- Telegram/actions: add `topic-edit` for forum-topic renames and icon updates while sharing the same Telegram topic-edit transport used by the plugin runtime. (#47798) Thanks @obviyus.
|
||||
- secrets: harden read-only SecretRef command paths and diagnostics. (#47794) Thanks @joshavant.
|
||||
- Sandbox/runtime: add pluggable sandbox backends, ship an OpenShell backend with `mirror` and `remote` workspace modes, and make sandbox list/recreate/prune backend-aware instead of Docker-only.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Z.AI/onboarding: detect a working default model even for explicit `zai-coding-*` endpoint choices, so Coding Plan setup can keep the selected endpoint while defaulting to `glm-5` when available or `glm-4.7` as fallback. (#45969)
|
||||
- Zalo/plugin runtime: export `resolveClientIp` from `openclaw/plugin-sdk/zalo` so installed builds no longer crash on startup when the webhook monitor loads from the packaged extension instead of the monorepo source tree. (#46549) Thanks @No898.
|
||||
- Z.AI/onboarding: add `glm-5-turbo` to the default Z.AI provider catalog so onboarding-generated configs expose the new model alongside the existing GLM defaults. (#46670) Thanks @tomsun28.
|
||||
- Group mention gating: reject invalid and unsafe nested-repetition `mentionPatterns`, reuse the shared safe config-regex compiler across mention stripping and detection, and cache strip-time regex compilation so noisy groups avoid repeated recompiles.
|
||||
- Control UI/chat sessions: show human-readable labels in the grouped session dropdown again, keep unique scoped fallbacks when metadata is missing, and disambiguate duplicate labels only when needed. (#45130) thanks @luzhidong.
|
||||
- Control UI: scope persisted session selection per gateway, prevent stale session bleed across tokenized gateway opens, and cap stored gateway session history. (#47453) Thanks @sallyom.
|
||||
- Slack/interactive replies: preserve `channelData.slack.blocks` through live DM delivery and preview-finalized edits so Block Kit button and select directives render instead of falling back to raw text. (#45890) Thanks @vincentkoc.
|
||||
- Feishu/topic threads: fetch full thread context, including prior bot replies, when starting a topic-thread session so follow-up turns in Feishu topics keep the right conversation state. (#45254) Thanks @Coobiw.
|
||||
- Configure/startup: move outbound send-deps resolution into a lightweight helper so `openclaw configure` no longer stalls after the banner while eagerly loading channel plugins. (#46301) thanks @scoootscooob.
|
||||
- Zalo Personal/group gating: stop reapplying `dmPolicy.allowFrom` as a sender gate for already-allowlisted groups when `groupAllowFrom` is unset, so any member of an allowed group can trigger replies while DMs stay restricted. (#40146)
|
||||
- Plugins/install precedence: keep bundled plugins ahead of auto-discovered globals by default, but let an explicitly installed plugin record win its own duplicate-id tie so installed channel plugins load from `~/.openclaw/extensions` after `openclaw plugins install`.
|
||||
- Device pairing/setup codes: bind setup-code pairing to the intended node role and scope set so approval keeps the expected device profile. Thanks @vincentkoc.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Slack/interactive replies: preserve `channelData.slack.blocks` through live DM delivery and preview-finalized edits so Block Kit button and select directives render instead of falling back to raw text. Thanks @vincentkoc.
|
||||
- CI/channel test routing: move the built-in channel suites into `test:channels` and keep them out of `test:extensions`, so extension CI no longer fails after the channel migration while targeted test routing still sends Slack, Signal, and iMessage suites to the right lane. (#46066) Thanks @scoootscooob.
|
||||
- Node/startup: remove leftover debug `console.log("node host PATH: ...")` that printed the resolved PATH on every `openclaw node run` invocation. (#46411)
|
||||
- Control UI/dashboard: preserve structured gateway shutdown reasons across restart disconnects so config-triggered restarts no longer fall back to `disconnected (1006): no reason`. (#46532) Thanks @vincentkoc.
|
||||
- Feishu/topic threads: fetch full thread context, including prior bot replies, when starting a topic-thread session so follow-up turns in Feishu topics keep the right conversation state. Thanks @Coobiw.
|
||||
- Android/chat: theme the thinking dropdown and TLS trust dialogs explicitly so popup surfaces match the active app theme instead of falling back to mismatched Material defaults.
|
||||
- Z.AI/onboarding: detect a working default model even for explicit `zai-coding-*` endpoint choices, so Coding Plan setup can keep the selected endpoint while defaulting to `glm-5` when available or `glm-4.7` as fallback. (#45969)
|
||||
- Models/OpenRouter runtime capabilities: fetch uncatalogued OpenRouter model metadata on first use so newly added vision models keep image input instead of silently degrading to text-only, with top-level capability field fallbacks for `/api/v1/models`. (#45824) Thanks @DJjjjhao.
|
||||
- Z.AI/onboarding: add `glm-5-turbo` to the default Z.AI provider catalog so onboarding-generated configs expose the new model alongside the existing GLM defaults. (#46670) Thanks @tomsun28.
|
||||
- Zalo Personal/group gating: stop reapplying `dmPolicy.allowFrom` as a sender gate for already-allowlisted groups when `groupAllowFrom` is unset, so any member of an allowed group can trigger replies while DMs stay restricted. (#40146)
|
||||
- Channels/plugins: keep shared interactive payloads merge-ready by fixing Slack custom callback routing and repeat-click dedupe, allowing interactive-only sends, and preserving ordered Discord shared text blocks. (#47715) Thanks @vincentkoc.
|
||||
- Browser/remote CDP: honor strict browser SSRF policy during remote CDP reachability and `/json/version` discovery checks, redact sensitive `cdpUrl` tokens from status output, and warn when remote CDP targets private/internal hosts.
|
||||
- Plugins/install precedence: keep bundled plugins ahead of auto-discovered globals by default, but let an explicitly installed plugin record win its own duplicate-id tie so installed channel plugins load from `~/.openclaw/extensions` after `openclaw plugins install`.
|
||||
- ACP/acpx: resolve the bundled plugin root from the actual plugin directory so plugin-local installs stay under `dist/extensions/acpx` instead of escaping to `dist/extensions` and failing runtime setup.
|
||||
- Gateway/auth: ignore spoofed loopback hops in trusted forwarding chains and block device approvals that request scopes above the caller session. Thanks @vincentkoc.
|
||||
- Gateway/config views: strip embedded credentials from URL-based endpoint fields before returning read-only account and config snapshots. Thanks @vincentkoc.
|
||||
- Tools/apply-patch: revalidate workspace-only delete and directory targets immediately before mutating host paths. Thanks @vincentkoc.
|
||||
- Webhooks/runtime: move auth earlier and tighten pre-auth body limits and timeouts across bundled webhook handlers, including slow-body handling for Mattermost slash commands. Thanks @vincentkoc.
|
||||
- Subagents/follow-ups: require the same controller ownership checks for `/subagents send` as other control actions, so leaf sessions cannot message nested child runs they do not control. Thanks @vincentkoc.
|
||||
- Inbound policy hardening: tighten callback and webhook sender checks across Mattermost and Google Chat, match Nextcloud Talk rooms by stable room token, and treat explicit empty Twitch allowlists as deny-all. (#46787) Thanks @zpbrent, @ijxpwastaken and @vincentkoc.
|
||||
- macOS/canvas actions: keep unattended local agent actions on trusted in-app canvas surfaces only, and stop exposing the deep-link fallback key to arbitrary page scripts. (#46790) Thanks @vincentkoc.
|
||||
- Agents/compaction: extend the enclosing run deadline once while compaction is actively in flight, and abort the underlying SDK compaction on timeout/cancel so large-session compactions stop freezing mid-run. (#46889) Thanks @asyncjason.
|
||||
- Models/openai-completions: default non-native OpenAI-compatible providers to omit tool-definition `strict` fields unless users explicitly opt back in, so tool calling keeps working on providers that reject that option. (#45497) Thanks @sahancava.
|
||||
- WhatsApp/reconnect: restore the append recency filter in the extension inbox monitor and handle protobuf `Long` timestamps correctly, so fresh post-reconnect append messages are processed while stale history sync stays suppressed. (#42588) thanks @MonkeyLeeT.
|
||||
- WhatsApp/login: wait for pending creds writes before reopening after Baileys `515` pairing restarts in both QR login and `channels login` flows, and keep the restart coverage pinned to the real wrapped error shape plus per-account creds queues. (#27910) Thanks @asyncjason.
|
||||
- Agents/openai-compatible tool calls: deduplicate repeated tool call ids across live assistant messages and replayed history so OpenAI-compatible backends no longer reject duplicate `tool_call_id` values with HTTP 400. (#40996) Thanks @xaeon2026.
|
||||
- Security/device pairing: harden `device.token.rotate` deny handling by keeping public failures generic while logging internal deny reasons and preserving approved-baseline enforcement. (`GHSA-7jrw-x62h-64p8`)
|
||||
- Slack/interactive replies: preserve `channelData.slack.blocks` through live DM delivery and preview-finalized edits so Block Kit button and select directives render instead of falling back to raw text. (#45890) Thanks @vincentkoc.
|
||||
- Zalo/plugin runtime: export `resolveClientIp` from `openclaw/plugin-sdk/zalo` so installed builds no longer crash on startup when the webhook monitor loads from the packaged extension instead of the monorepo source tree. (#46549) Thanks @No898.
|
||||
- CI/channel test routing: move the built-in channel suites into `test:channels` and keep them out of `test:extensions`, so extension CI no longer fails after the channel migration while targeted test routing still sends Slack, Signal, and iMessage suites to the right lane. (#46066) Thanks @scoootscooob.
|
||||
- Browser/profiles: drop the auto-created `chrome-relay` browser profile; users who need the Chrome extension relay must now create their own profile via `openclaw browser create-profile`. (#45777) Thanks @odysseus0.
|
||||
- Docs/Mintlify: fix MDX marker syntax on Perplexity, Model Providers, Moonshot, and exec approvals pages so local docs preview no longer breaks rendering or leaves stale pages unpublished. (#46695) Thanks @velvet-shark.
|
||||
- Email/webhook wrapping: sanitize sender and subject metadata before external-content wrapping so metadata fields cannot break the wrapper structure. Thanks @vincentkoc.
|
||||
- Node/startup: remove leftover debug `console.log("node host PATH: ...")` that printed the resolved PATH on every `openclaw node run` invocation. (#46411)
|
||||
- Nodes/pending actions: re-check queued foreground actions against the current node command policy before returning them to the node. (#46815) Thanks @zpbrent and @vincentkoc.
|
||||
- ACP/approvals: use canonical tool identity for prompting decisions and fail closed when conflicting tool identity hints are present. (#46817) Thanks @zpbrent and @vincentkoc.
|
||||
- Telegram/message send: forward `--force-document` through the `sendPayload` path as well as `sendMedia`, so Telegram payload sends with `channelData` keep uploading images as documents instead of silently falling back to compressed photo sends. (#47119) Thanks @thepagent.
|
||||
- Telegram/message chunking: preserve spaces, paragraph separators, and word boundaries when HTML overflow rechunking splits formatted replies. (#47274)
|
||||
- Plugins/scoped ids: preserve scoped plugin ids during install and config keying, and keep bundled plugins ahead of discovered duplicate ids by default so `@scope/name` plugins no longer collide with unscoped installs. Thanks @vincentkoc.
|
||||
- CLI: avoid loading provider discovery during startup model normalization. (#46522) Thanks @ItsAditya-xyz and @vincentkoc.
|
||||
- Tlon: honor explicit empty allowlists and defer cite expansion. (#46788) Thanks @zpbrent and @vincentkoc.
|
||||
- ACP: require admin scope for mutating internal actions. (#46789) Thanks @tdjackey and @vincentkoc.
|
||||
- Gateway/config validation: stop treating the implicit default memory slot as a required explicit plugin config, so startup no longer fails with `plugins.slots.memory: plugin not found: memory-core` when `memory-core` was only inferred. (#47494) Thanks @ngutman.
|
||||
- CLI/startup: lazy-load channel add and root help startup paths to trim avoidable RSS and help latency on constrained hosts. (#46784) Thanks @vincentkoc.
|
||||
- CLI/onboarding: import static provider definitions directly for onboarding model/config helpers so those paths no longer pull provider discovery just for built-in defaults. (#47467) Thanks @vincentkoc.
|
||||
- CLI/auth choice: lazy-load plugin/provider fallback resolution so mapped auth choices stay on the static path and only unknown choices pay the heavy provider load. (#47495) Thanks @vincentkoc.
|
||||
- CLI/completion: reduce recursive completion-script string churn and fix nested PowerShell command-path matching so generated nested completions resolve on PowerShell too. (#45537) Thanks @yiShanXin and @vincentkoc.
|
||||
- Gateway/startup: load bundled channel plugins from compiled `dist/extensions` entries in built installs, so gateway boot no longer recompiles bundled extension TypeScript on every startup and WhatsApp-class cold starts drop back to seconds instead of tens of seconds or worse.
|
||||
- Gateway/watch mode: restart on bundled-plugin package and manifest metadata changes, rebuild `dist` for extension source and `tsdown.config.ts` changes, and still ignore extension docs. (#47571) thanks @gumadeiras.
|
||||
- Gateway/watch mode: recreate bundled plugin runtime metadata after clean or stale `dist` states, so `pnpm gateway:watch` no longer fails on missing `dist/extensions/*/openclaw.plugin.json` manifests after a rebuild. Thanks @gumadeiras.
|
||||
- Plugins/context engines: enforce owner-aware context-engine registration on both loader and public SDK paths so plugins cannot spoof privileged ownership, claim the core `legacy` engine id, or overwrite an existing engine id through direct SDK imports. (#47595) Thanks @vincentkoc.
|
||||
|
||||
## 2026.3.13
|
||||
|
||||
@@ -68,6 +114,7 @@ Docs: https://docs.openclaw.ai
|
||||
- macOS/exec approvals: respect per-agent exec approval settings in the gateway prompter, including allowlist fallback when the native prompt cannot be shown, so gateway-triggered `system.run` requests follow configured policy instead of always prompting or denying unexpectedly. (#13707) Thanks @sliekens.
|
||||
- Telegram/media downloads: thread the same direct or proxy transport policy into SSRF-guarded file fetches so inbound attachments keep working when Telegram falls back between env-proxy and direct networking. (#44639) Thanks @obviyus.
|
||||
- Telegram/inbound media IPv4 fallback: retry SSRF-guarded Telegram file downloads once with the same IPv4 fallback policy as Bot API calls so fresh installs on IPv6-broken hosts no longer fail to download inbound images.
|
||||
- Commands/onboarding: split static auth-choice help from the plugin-backed onboarding catalog so `openclaw onboard` registration no longer pulls provider-wizard imports just to describe `--auth-choice`. (#47545) Thanks @vincentkoc.
|
||||
- Windows/gateway install: bound `schtasks` calls and fall back to the Startup-folder login item when task creation hangs, so native `openclaw gateway install` fails fast instead of wedging forever on broken Scheduled Task setups.
|
||||
- Windows/gateway stop: resolve Startup-folder fallback listeners from the installed `gateway.cmd` port, so `openclaw gateway stop` now actually kills fallback-launched gateway processes before restart.
|
||||
- Windows/gateway status: reuse the installed service command environment when reading runtime status, so startup-fallback gateways keep reporting the configured port and running state in `gateway status --json` instead of falling back to `gateway port unknown`.
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/openclaw/openclaw/main/docs/assets/openclaw-logo-text-dark.png">
|
||||
<img src="https://raw.githubusercontent.com/openclaw/openclaw/main/docs/assets/openclaw-logo-text.png" alt="OpenClaw" width="500">
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/openclaw/openclaw/main/docs/assets/openclaw-logo-text-dark.svg">
|
||||
<img src="https://raw.githubusercontent.com/openclaw/openclaw/main/docs/assets/openclaw-logo-text.svg" alt="OpenClaw" width="500">
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
@@ -103,7 +103,7 @@ pnpm build
|
||||
|
||||
pnpm openclaw onboard --install-daemon
|
||||
|
||||
# Dev loop (auto-reload on TS changes)
|
||||
# Dev loop (auto-reload on source/config changes)
|
||||
pnpm gateway:watch
|
||||
```
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
android:maxSdkVersion="32" />
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
||||
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
|
||||
<uses-permission android:name="android.permission.READ_CALL_LOG" />
|
||||
<uses-permission android:name="android.permission.READ_CALENDAR" />
|
||||
<uses-permission android:name="android.permission.WRITE_CALENDAR" />
|
||||
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" />
|
||||
|
||||
@@ -110,6 +110,10 @@ class NodeRuntime(context: Context) {
|
||||
appContext = appContext,
|
||||
)
|
||||
|
||||
private val callLogHandler: CallLogHandler = CallLogHandler(
|
||||
appContext = appContext,
|
||||
)
|
||||
|
||||
private val motionHandler: MotionHandler = MotionHandler(
|
||||
appContext = appContext,
|
||||
)
|
||||
@@ -151,6 +155,7 @@ class NodeRuntime(context: Context) {
|
||||
smsHandler = smsHandlerImpl,
|
||||
a2uiHandler = a2uiHandler,
|
||||
debugHandler = debugHandler,
|
||||
callLogHandler = callLogHandler,
|
||||
isForeground = { _isForeground.value },
|
||||
cameraEnabled = { cameraEnabled.value },
|
||||
locationEnabled = { locationMode.value != LocationMode.Off },
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
package ai.openclaw.app.node
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.provider.CallLog
|
||||
import androidx.core.content.ContextCompat
|
||||
import ai.openclaw.app.gateway.GatewaySession
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.buildJsonArray
|
||||
import kotlinx.serialization.json.put
|
||||
|
||||
private const val DEFAULT_CALL_LOG_LIMIT = 25
|
||||
|
||||
internal data class CallLogRecord(
|
||||
val number: String?,
|
||||
val cachedName: String?,
|
||||
val date: Long,
|
||||
val duration: Long,
|
||||
val type: Int,
|
||||
)
|
||||
|
||||
internal data class CallLogSearchRequest(
|
||||
val limit: Int, // Number of records to return
|
||||
val offset: Int, // Offset value
|
||||
val cachedName: String?, // Search by contact name
|
||||
val number: String?, // Search by phone number
|
||||
val date: Long?, // Search by time (timestamp, deprecated, use dateStart/dateEnd)
|
||||
val dateStart: Long?, // Query start time (timestamp)
|
||||
val dateEnd: Long?, // Query end time (timestamp)
|
||||
val duration: Long?, // Search by duration (seconds)
|
||||
val type: Int?, // Search by call log type
|
||||
)
|
||||
|
||||
internal interface CallLogDataSource {
|
||||
fun hasReadPermission(context: Context): Boolean
|
||||
|
||||
fun search(context: Context, request: CallLogSearchRequest): List<CallLogRecord>
|
||||
}
|
||||
|
||||
private object SystemCallLogDataSource : CallLogDataSource {
|
||||
override fun hasReadPermission(context: Context): Boolean {
|
||||
return ContextCompat.checkSelfPermission(
|
||||
context,
|
||||
Manifest.permission.READ_CALL_LOG
|
||||
) == android.content.pm.PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
override fun search(context: Context, request: CallLogSearchRequest): List<CallLogRecord> {
|
||||
val resolver = context.contentResolver
|
||||
val projection = arrayOf(
|
||||
CallLog.Calls.NUMBER,
|
||||
CallLog.Calls.CACHED_NAME,
|
||||
CallLog.Calls.DATE,
|
||||
CallLog.Calls.DURATION,
|
||||
CallLog.Calls.TYPE,
|
||||
)
|
||||
|
||||
// Build selection and selectionArgs for filtering
|
||||
val selections = mutableListOf<String>()
|
||||
val selectionArgs = mutableListOf<String>()
|
||||
|
||||
request.cachedName?.let {
|
||||
selections.add("${CallLog.Calls.CACHED_NAME} LIKE ?")
|
||||
selectionArgs.add("%$it%")
|
||||
}
|
||||
|
||||
request.number?.let {
|
||||
selections.add("${CallLog.Calls.NUMBER} LIKE ?")
|
||||
selectionArgs.add("%$it%")
|
||||
}
|
||||
|
||||
// Support time range query
|
||||
if (request.dateStart != null && request.dateEnd != null) {
|
||||
selections.add("${CallLog.Calls.DATE} >= ? AND ${CallLog.Calls.DATE} <= ?")
|
||||
selectionArgs.add(request.dateStart.toString())
|
||||
selectionArgs.add(request.dateEnd.toString())
|
||||
} else if (request.dateStart != null) {
|
||||
selections.add("${CallLog.Calls.DATE} >= ?")
|
||||
selectionArgs.add(request.dateStart.toString())
|
||||
} else if (request.dateEnd != null) {
|
||||
selections.add("${CallLog.Calls.DATE} <= ?")
|
||||
selectionArgs.add(request.dateEnd.toString())
|
||||
} else if (request.date != null) {
|
||||
// Compatible with the old date parameter (exact match)
|
||||
selections.add("${CallLog.Calls.DATE} = ?")
|
||||
selectionArgs.add(request.date.toString())
|
||||
}
|
||||
|
||||
request.duration?.let {
|
||||
selections.add("${CallLog.Calls.DURATION} = ?")
|
||||
selectionArgs.add(it.toString())
|
||||
}
|
||||
|
||||
request.type?.let {
|
||||
selections.add("${CallLog.Calls.TYPE} = ?")
|
||||
selectionArgs.add(it.toString())
|
||||
}
|
||||
|
||||
val selection = if (selections.isNotEmpty()) selections.joinToString(" AND ") else null
|
||||
val selectionArgsArray = if (selectionArgs.isNotEmpty()) selectionArgs.toTypedArray() else null
|
||||
|
||||
val sortOrder = "${CallLog.Calls.DATE} DESC"
|
||||
|
||||
resolver.query(
|
||||
CallLog.Calls.CONTENT_URI,
|
||||
projection,
|
||||
selection,
|
||||
selectionArgsArray,
|
||||
sortOrder,
|
||||
).use { cursor ->
|
||||
if (cursor == null) return emptyList()
|
||||
|
||||
val numberIndex = cursor.getColumnIndex(CallLog.Calls.NUMBER)
|
||||
val cachedNameIndex = cursor.getColumnIndex(CallLog.Calls.CACHED_NAME)
|
||||
val dateIndex = cursor.getColumnIndex(CallLog.Calls.DATE)
|
||||
val durationIndex = cursor.getColumnIndex(CallLog.Calls.DURATION)
|
||||
val typeIndex = cursor.getColumnIndex(CallLog.Calls.TYPE)
|
||||
|
||||
// Skip offset rows
|
||||
if (request.offset > 0 && cursor.moveToPosition(request.offset - 1)) {
|
||||
// Successfully moved to offset position
|
||||
}
|
||||
|
||||
val out = mutableListOf<CallLogRecord>()
|
||||
var count = 0
|
||||
while (cursor.moveToNext() && count < request.limit) {
|
||||
out += CallLogRecord(
|
||||
number = cursor.getString(numberIndex),
|
||||
cachedName = cursor.getString(cachedNameIndex),
|
||||
date = cursor.getLong(dateIndex),
|
||||
duration = cursor.getLong(durationIndex),
|
||||
type = cursor.getInt(typeIndex),
|
||||
)
|
||||
count++
|
||||
}
|
||||
return out
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CallLogHandler private constructor(
|
||||
private val appContext: Context,
|
||||
private val dataSource: CallLogDataSource,
|
||||
) {
|
||||
constructor(appContext: Context) : this(appContext = appContext, dataSource = SystemCallLogDataSource)
|
||||
|
||||
fun handleCallLogSearch(paramsJson: String?): GatewaySession.InvokeResult {
|
||||
if (!dataSource.hasReadPermission(appContext)) {
|
||||
return GatewaySession.InvokeResult.error(
|
||||
code = "CALL_LOG_PERMISSION_REQUIRED",
|
||||
message = "CALL_LOG_PERMISSION_REQUIRED: grant Call Log permission",
|
||||
)
|
||||
}
|
||||
|
||||
val request = parseSearchRequest(paramsJson)
|
||||
?: return GatewaySession.InvokeResult.error(
|
||||
code = "INVALID_REQUEST",
|
||||
message = "INVALID_REQUEST: expected JSON object",
|
||||
)
|
||||
|
||||
return try {
|
||||
val callLogs = dataSource.search(appContext, request)
|
||||
GatewaySession.InvokeResult.ok(
|
||||
buildJsonObject {
|
||||
put(
|
||||
"callLogs",
|
||||
buildJsonArray {
|
||||
callLogs.forEach { add(callLogJson(it)) }
|
||||
},
|
||||
)
|
||||
}.toString(),
|
||||
)
|
||||
} catch (err: Throwable) {
|
||||
GatewaySession.InvokeResult.error(
|
||||
code = "CALL_LOG_UNAVAILABLE",
|
||||
message = "CALL_LOG_UNAVAILABLE: ${err.message ?: "call log query failed"}",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseSearchRequest(paramsJson: String?): CallLogSearchRequest? {
|
||||
if (paramsJson.isNullOrBlank()) {
|
||||
return CallLogSearchRequest(
|
||||
limit = DEFAULT_CALL_LOG_LIMIT,
|
||||
offset = 0,
|
||||
cachedName = null,
|
||||
number = null,
|
||||
date = null,
|
||||
dateStart = null,
|
||||
dateEnd = null,
|
||||
duration = null,
|
||||
type = null,
|
||||
)
|
||||
}
|
||||
|
||||
val params = try {
|
||||
Json.parseToJsonElement(paramsJson).asObjectOrNull()
|
||||
} catch (_: Throwable) {
|
||||
null
|
||||
} ?: return null
|
||||
|
||||
val limit = ((params["limit"] as? JsonPrimitive)?.content?.toIntOrNull() ?: DEFAULT_CALL_LOG_LIMIT)
|
||||
.coerceIn(1, 200)
|
||||
val offset = ((params["offset"] as? JsonPrimitive)?.content?.toIntOrNull() ?: 0)
|
||||
.coerceAtLeast(0)
|
||||
val cachedName = (params["cachedName"] as? JsonPrimitive)?.content?.takeIf { it.isNotBlank() }
|
||||
val number = (params["number"] as? JsonPrimitive)?.content?.takeIf { it.isNotBlank() }
|
||||
val date = (params["date"] as? JsonPrimitive)?.content?.toLongOrNull()
|
||||
val dateStart = (params["dateStart"] as? JsonPrimitive)?.content?.toLongOrNull()
|
||||
val dateEnd = (params["dateEnd"] as? JsonPrimitive)?.content?.toLongOrNull()
|
||||
val duration = (params["duration"] as? JsonPrimitive)?.content?.toLongOrNull()
|
||||
val type = (params["type"] as? JsonPrimitive)?.content?.toIntOrNull()
|
||||
|
||||
return CallLogSearchRequest(
|
||||
limit = limit,
|
||||
offset = offset,
|
||||
cachedName = cachedName,
|
||||
number = number,
|
||||
date = date,
|
||||
dateStart = dateStart,
|
||||
dateEnd = dateEnd,
|
||||
duration = duration,
|
||||
type = type,
|
||||
)
|
||||
}
|
||||
|
||||
private fun callLogJson(callLog: CallLogRecord): JsonObject {
|
||||
return buildJsonObject {
|
||||
put("number", JsonPrimitive(callLog.number))
|
||||
put("cachedName", JsonPrimitive(callLog.cachedName))
|
||||
put("date", JsonPrimitive(callLog.date))
|
||||
put("duration", JsonPrimitive(callLog.duration))
|
||||
put("type", JsonPrimitive(callLog.type))
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
internal fun forTesting(
|
||||
appContext: Context,
|
||||
dataSource: CallLogDataSource,
|
||||
): CallLogHandler = CallLogHandler(appContext = appContext, dataSource = dataSource)
|
||||
}
|
||||
}
|
||||
@@ -212,6 +212,13 @@ class DeviceHandler(
|
||||
promptableWhenDenied = true,
|
||||
),
|
||||
)
|
||||
put(
|
||||
"callLog",
|
||||
permissionStateJson(
|
||||
granted = hasPermission(Manifest.permission.READ_CALL_LOG),
|
||||
promptableWhenDenied = true,
|
||||
),
|
||||
)
|
||||
put(
|
||||
"motion",
|
||||
permissionStateJson(
|
||||
|
||||
@@ -5,6 +5,7 @@ import ai.openclaw.app.protocol.OpenClawCanvasA2UICommand
|
||||
import ai.openclaw.app.protocol.OpenClawCanvasCommand
|
||||
import ai.openclaw.app.protocol.OpenClawCameraCommand
|
||||
import ai.openclaw.app.protocol.OpenClawCapability
|
||||
import ai.openclaw.app.protocol.OpenClawCallLogCommand
|
||||
import ai.openclaw.app.protocol.OpenClawContactsCommand
|
||||
import ai.openclaw.app.protocol.OpenClawDeviceCommand
|
||||
import ai.openclaw.app.protocol.OpenClawLocationCommand
|
||||
@@ -84,6 +85,7 @@ object InvokeCommandRegistry {
|
||||
name = OpenClawCapability.Motion.rawValue,
|
||||
availability = NodeCapabilityAvailability.MotionAvailable,
|
||||
),
|
||||
NodeCapabilitySpec(name = OpenClawCapability.CallLog.rawValue),
|
||||
)
|
||||
|
||||
val all: List<InvokeCommandSpec> =
|
||||
@@ -187,6 +189,9 @@ object InvokeCommandRegistry {
|
||||
name = OpenClawSmsCommand.Send.rawValue,
|
||||
availability = InvokeCommandAvailability.SmsAvailable,
|
||||
),
|
||||
InvokeCommandSpec(
|
||||
name = OpenClawCallLogCommand.Search.rawValue,
|
||||
),
|
||||
InvokeCommandSpec(
|
||||
name = "debug.logs",
|
||||
availability = InvokeCommandAvailability.DebugBuild,
|
||||
|
||||
@@ -5,6 +5,7 @@ import ai.openclaw.app.protocol.OpenClawCalendarCommand
|
||||
import ai.openclaw.app.protocol.OpenClawCanvasA2UICommand
|
||||
import ai.openclaw.app.protocol.OpenClawCanvasCommand
|
||||
import ai.openclaw.app.protocol.OpenClawCameraCommand
|
||||
import ai.openclaw.app.protocol.OpenClawCallLogCommand
|
||||
import ai.openclaw.app.protocol.OpenClawContactsCommand
|
||||
import ai.openclaw.app.protocol.OpenClawDeviceCommand
|
||||
import ai.openclaw.app.protocol.OpenClawLocationCommand
|
||||
@@ -27,6 +28,7 @@ class InvokeDispatcher(
|
||||
private val smsHandler: SmsHandler,
|
||||
private val a2uiHandler: A2UIHandler,
|
||||
private val debugHandler: DebugHandler,
|
||||
private val callLogHandler: CallLogHandler,
|
||||
private val isForeground: () -> Boolean,
|
||||
private val cameraEnabled: () -> Boolean,
|
||||
private val locationEnabled: () -> Boolean,
|
||||
@@ -161,6 +163,9 @@ class InvokeDispatcher(
|
||||
// SMS command
|
||||
OpenClawSmsCommand.Send.rawValue -> smsHandler.handleSmsSend(paramsJson)
|
||||
|
||||
// CallLog command
|
||||
OpenClawCallLogCommand.Search.rawValue -> callLogHandler.handleCallLogSearch(paramsJson)
|
||||
|
||||
// Debug commands
|
||||
"debug.ed25519" -> debugHandler.handleEd25519()
|
||||
"debug.logs" -> debugHandler.handleLogs()
|
||||
|
||||
@@ -13,6 +13,7 @@ enum class OpenClawCapability(val rawValue: String) {
|
||||
Contacts("contacts"),
|
||||
Calendar("calendar"),
|
||||
Motion("motion"),
|
||||
CallLog("callLog"),
|
||||
}
|
||||
|
||||
enum class OpenClawCanvasCommand(val rawValue: String) {
|
||||
@@ -137,3 +138,12 @@ enum class OpenClawMotionCommand(val rawValue: String) {
|
||||
const val NamespacePrefix: String = "motion."
|
||||
}
|
||||
}
|
||||
|
||||
enum class OpenClawCallLogCommand(val rawValue: String) {
|
||||
Search("callLog.search"),
|
||||
;
|
||||
|
||||
companion object {
|
||||
const val NamespacePrefix: String = "callLog."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,20 +92,28 @@ fun ConnectTabScreen(viewModel: MainViewModel) {
|
||||
val prompt = pendingTrust!!
|
||||
AlertDialog(
|
||||
onDismissRequest = { viewModel.declineGatewayTrustPrompt() },
|
||||
title = { Text("Trust this gateway?") },
|
||||
containerColor = mobileCardSurface,
|
||||
title = { Text("Trust this gateway?", style = mobileHeadline, color = mobileText) },
|
||||
text = {
|
||||
Text(
|
||||
"First-time TLS connection.\n\nVerify this SHA-256 fingerprint before trusting:\n${prompt.fingerprintSha256}",
|
||||
style = mobileCallout,
|
||||
color = mobileText,
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = { viewModel.acceptGatewayTrustPrompt() }) {
|
||||
TextButton(
|
||||
onClick = { viewModel.acceptGatewayTrustPrompt() },
|
||||
colors = ButtonDefaults.textButtonColors(contentColor = mobileAccent),
|
||||
) {
|
||||
Text("Trust and continue")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { viewModel.declineGatewayTrustPrompt() }) {
|
||||
TextButton(
|
||||
onClick = { viewModel.declineGatewayTrustPrompt() },
|
||||
colors = ButtonDefaults.textButtonColors(contentColor = mobileTextSecondary),
|
||||
) {
|
||||
Text("Cancel")
|
||||
}
|
||||
},
|
||||
|
||||
@@ -121,6 +121,7 @@ private enum class PermissionToggle {
|
||||
Calendar,
|
||||
Motion,
|
||||
Sms,
|
||||
CallLog,
|
||||
}
|
||||
|
||||
private enum class SpecialAccessToggle {
|
||||
@@ -288,6 +289,10 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) {
|
||||
rememberSaveable {
|
||||
mutableStateOf(smsAvailable && isPermissionGranted(context, Manifest.permission.SEND_SMS))
|
||||
}
|
||||
var enableCallLog by
|
||||
rememberSaveable {
|
||||
mutableStateOf(isPermissionGranted(context, Manifest.permission.READ_CALL_LOG))
|
||||
}
|
||||
|
||||
var pendingPermissionToggle by remember { mutableStateOf<PermissionToggle?>(null) }
|
||||
var pendingSpecialAccessToggle by remember { mutableStateOf<SpecialAccessToggle?>(null) }
|
||||
@@ -304,6 +309,7 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) {
|
||||
PermissionToggle.Calendar -> enableCalendar = enabled
|
||||
PermissionToggle.Motion -> enableMotion = enabled && motionAvailable
|
||||
PermissionToggle.Sms -> enableSms = enabled && smsAvailable
|
||||
PermissionToggle.CallLog -> enableCallLog = enabled
|
||||
}
|
||||
}
|
||||
|
||||
@@ -331,6 +337,7 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) {
|
||||
isPermissionGranted(context, Manifest.permission.ACTIVITY_RECOGNITION)
|
||||
PermissionToggle.Sms ->
|
||||
!smsAvailable || isPermissionGranted(context, Manifest.permission.SEND_SMS)
|
||||
PermissionToggle.CallLog -> isPermissionGranted(context, Manifest.permission.READ_CALL_LOG)
|
||||
}
|
||||
|
||||
fun setSpecialAccessToggleEnabled(toggle: SpecialAccessToggle, enabled: Boolean) {
|
||||
@@ -352,6 +359,7 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) {
|
||||
enableCalendar,
|
||||
enableMotion,
|
||||
enableSms,
|
||||
enableCallLog,
|
||||
smsAvailable,
|
||||
motionAvailable,
|
||||
) {
|
||||
@@ -367,6 +375,7 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) {
|
||||
if (enableCalendar) enabled += "Calendar"
|
||||
if (enableMotion && motionAvailable) enabled += "Motion"
|
||||
if (smsAvailable && enableSms) enabled += "SMS"
|
||||
if (enableCallLog) enabled += "Call Log"
|
||||
if (enabled.isEmpty()) "None selected" else enabled.joinToString(", ")
|
||||
}
|
||||
|
||||
@@ -455,19 +464,28 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) {
|
||||
val prompt = pendingTrust!!
|
||||
AlertDialog(
|
||||
onDismissRequest = { viewModel.declineGatewayTrustPrompt() },
|
||||
title = { Text("Trust this gateway?") },
|
||||
containerColor = onboardingSurface,
|
||||
title = { Text("Trust this gateway?", style = onboardingHeadlineStyle, color = onboardingText) },
|
||||
text = {
|
||||
Text(
|
||||
"First-time TLS connection.\n\nVerify this SHA-256 fingerprint before trusting:\n${prompt.fingerprintSha256}",
|
||||
style = onboardingCalloutStyle,
|
||||
color = onboardingText,
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = { viewModel.acceptGatewayTrustPrompt() }) {
|
||||
TextButton(
|
||||
onClick = { viewModel.acceptGatewayTrustPrompt() },
|
||||
colors = ButtonDefaults.textButtonColors(contentColor = onboardingAccent),
|
||||
) {
|
||||
Text("Trust and continue")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { viewModel.declineGatewayTrustPrompt() }) {
|
||||
TextButton(
|
||||
onClick = { viewModel.declineGatewayTrustPrompt() },
|
||||
colors = ButtonDefaults.textButtonColors(contentColor = onboardingTextSecondary),
|
||||
) {
|
||||
Text("Cancel")
|
||||
}
|
||||
},
|
||||
@@ -586,6 +604,7 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) {
|
||||
motionPermissionRequired = motionPermissionRequired,
|
||||
enableSms = enableSms,
|
||||
smsAvailable = smsAvailable,
|
||||
enableCallLog = enableCallLog,
|
||||
context = context,
|
||||
onDiscoveryChange = { checked ->
|
||||
requestPermissionToggle(
|
||||
@@ -683,6 +702,13 @@ fun OnboardingFlow(viewModel: MainViewModel, modifier: Modifier = Modifier) {
|
||||
)
|
||||
}
|
||||
},
|
||||
onCallLogChange = { checked ->
|
||||
requestPermissionToggle(
|
||||
PermissionToggle.CallLog,
|
||||
checked,
|
||||
listOf(Manifest.permission.READ_CALL_LOG),
|
||||
)
|
||||
},
|
||||
)
|
||||
OnboardingStep.FinalCheck ->
|
||||
FinalStep(
|
||||
@@ -1273,6 +1299,7 @@ private fun PermissionsStep(
|
||||
motionPermissionRequired: Boolean,
|
||||
enableSms: Boolean,
|
||||
smsAvailable: Boolean,
|
||||
enableCallLog: Boolean,
|
||||
context: Context,
|
||||
onDiscoveryChange: (Boolean) -> Unit,
|
||||
onLocationChange: (Boolean) -> Unit,
|
||||
@@ -1285,6 +1312,7 @@ private fun PermissionsStep(
|
||||
onCalendarChange: (Boolean) -> Unit,
|
||||
onMotionChange: (Boolean) -> Unit,
|
||||
onSmsChange: (Boolean) -> Unit,
|
||||
onCallLogChange: (Boolean) -> Unit,
|
||||
) {
|
||||
val discoveryPermission = if (Build.VERSION.SDK_INT >= 33) Manifest.permission.NEARBY_WIFI_DEVICES else Manifest.permission.ACCESS_FINE_LOCATION
|
||||
val locationGranted =
|
||||
@@ -1415,6 +1443,15 @@ private fun PermissionsStep(
|
||||
onCheckedChange = onSmsChange,
|
||||
)
|
||||
}
|
||||
InlineDivider()
|
||||
PermissionToggleRow(
|
||||
title = "Call Log",
|
||||
subtitle = "callLog.search",
|
||||
checked = enableCallLog,
|
||||
granted = isPermissionGranted(context, Manifest.permission.READ_CALL_LOG),
|
||||
onCheckedChange = onCallLogChange,
|
||||
)
|
||||
Text("All settings can be changed later in Settings.", style = onboardingCalloutStyle, color = onboardingTextSecondary)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -218,6 +218,18 @@ fun SettingsSheet(viewModel: MainViewModel) {
|
||||
calendarPermissionGranted = readOk && writeOk
|
||||
}
|
||||
|
||||
var callLogPermissionGranted by
|
||||
remember {
|
||||
mutableStateOf(
|
||||
ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CALL_LOG) ==
|
||||
PackageManager.PERMISSION_GRANTED,
|
||||
)
|
||||
}
|
||||
val callLogPermissionLauncher =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
|
||||
callLogPermissionGranted = granted
|
||||
}
|
||||
|
||||
var motionPermissionGranted by
|
||||
remember {
|
||||
mutableStateOf(
|
||||
@@ -266,6 +278,9 @@ fun SettingsSheet(viewModel: MainViewModel) {
|
||||
PackageManager.PERMISSION_GRANTED &&
|
||||
ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_CALENDAR) ==
|
||||
PackageManager.PERMISSION_GRANTED
|
||||
callLogPermissionGranted =
|
||||
ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CALL_LOG) ==
|
||||
PackageManager.PERMISSION_GRANTED
|
||||
motionPermissionGranted =
|
||||
!motionPermissionRequired ||
|
||||
ContextCompat.checkSelfPermission(context, Manifest.permission.ACTIVITY_RECOGNITION) ==
|
||||
@@ -601,6 +616,31 @@ fun SettingsSheet(viewModel: MainViewModel) {
|
||||
}
|
||||
},
|
||||
)
|
||||
HorizontalDivider(color = mobileBorder)
|
||||
ListItem(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = listItemColors,
|
||||
headlineContent = { Text("Call Log", style = mobileHeadline) },
|
||||
supportingContent = { Text("Search recent call history.", style = mobileCallout) },
|
||||
trailingContent = {
|
||||
Button(
|
||||
onClick = {
|
||||
if (callLogPermissionGranted) {
|
||||
openAppSettings(context)
|
||||
} else {
|
||||
callLogPermissionLauncher.launch(Manifest.permission.READ_CALL_LOG)
|
||||
}
|
||||
},
|
||||
colors = settingsPrimaryButtonColors(),
|
||||
shape = RoundedCornerShape(14.dp),
|
||||
) {
|
||||
Text(
|
||||
if (callLogPermissionGranted) "Manage" else "Grant",
|
||||
style = mobileCallout.copy(fontWeight = FontWeight.Bold),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
if (motionAvailable) {
|
||||
HorizontalDivider(color = mobileBorder)
|
||||
ListItem(
|
||||
@@ -782,7 +822,7 @@ private fun openNotificationListenerSettings(context: Context) {
|
||||
private fun hasNotificationsPermission(context: Context): Boolean {
|
||||
if (Build.VERSION.SDK_INT < 33) return true
|
||||
return ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) ==
|
||||
PackageManager.PERMISSION_GRANTED
|
||||
PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
private fun isNotificationListenerEnabled(context: Context): Boolean {
|
||||
@@ -792,5 +832,5 @@ private fun isNotificationListenerEnabled(context: Context): Boolean {
|
||||
private fun hasMotionCapabilities(context: Context): Boolean {
|
||||
val sensorManager = context.getSystemService(SensorManager::class.java) ?: return false
|
||||
return sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) != null ||
|
||||
sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER) != null
|
||||
sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER) != null
|
||||
}
|
||||
|
||||
@@ -128,7 +128,15 @@ fun ChatComposer(
|
||||
}
|
||||
}
|
||||
|
||||
DropdownMenu(expanded = showThinkingMenu, onDismissRequest = { showThinkingMenu = false }) {
|
||||
DropdownMenu(
|
||||
expanded = showThinkingMenu,
|
||||
onDismissRequest = { showThinkingMenu = false },
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
containerColor = mobileCardSurface,
|
||||
tonalElevation = 0.dp,
|
||||
shadowElevation = 8.dp,
|
||||
border = BorderStroke(1.dp, mobileBorder),
|
||||
) {
|
||||
ThinkingMenuItem("off", thinkingLevel, onSetThinkingLevel) { showThinkingMenu = false }
|
||||
ThinkingMenuItem("low", thinkingLevel, onSetThinkingLevel) { showThinkingMenu = false }
|
||||
ThinkingMenuItem("medium", thinkingLevel, onSetThinkingLevel) { showThinkingMenu = false }
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
package ai.openclaw.app.node
|
||||
|
||||
import android.content.Context
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class CallLogHandlerTest : NodeHandlerRobolectricTest() {
|
||||
@Test
|
||||
fun handleCallLogSearch_requiresPermission() {
|
||||
val handler = CallLogHandler.forTesting(appContext(), FakeCallLogDataSource(canRead = false))
|
||||
|
||||
val result = handler.handleCallLogSearch(null)
|
||||
|
||||
assertFalse(result.ok)
|
||||
assertEquals("CALL_LOG_PERMISSION_REQUIRED", result.error?.code)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleCallLogSearch_rejectsInvalidJson() {
|
||||
val handler = CallLogHandler.forTesting(appContext(), FakeCallLogDataSource(canRead = true))
|
||||
|
||||
val result = handler.handleCallLogSearch("invalid json")
|
||||
|
||||
assertFalse(result.ok)
|
||||
assertEquals("INVALID_REQUEST", result.error?.code)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleCallLogSearch_returnsCallLogs() {
|
||||
val callLog =
|
||||
CallLogRecord(
|
||||
number = "+123456",
|
||||
cachedName = "lixuankai",
|
||||
date = 1709280000000L,
|
||||
duration = 60L,
|
||||
type = 1,
|
||||
)
|
||||
val handler =
|
||||
CallLogHandler.forTesting(
|
||||
appContext(),
|
||||
FakeCallLogDataSource(canRead = true, searchResults = listOf(callLog)),
|
||||
)
|
||||
|
||||
val result = handler.handleCallLogSearch("""{"limit":1}""")
|
||||
|
||||
assertTrue(result.ok)
|
||||
val payload = Json.parseToJsonElement(result.payloadJson ?: error("missing payload")).jsonObject
|
||||
val callLogs = payload.getValue("callLogs").jsonArray
|
||||
assertEquals(1, callLogs.size)
|
||||
assertEquals("+123456", callLogs.first().jsonObject.getValue("number").jsonPrimitive.content)
|
||||
assertEquals("lixuankai", callLogs.first().jsonObject.getValue("cachedName").jsonPrimitive.content)
|
||||
assertEquals(1709280000000L, callLogs.first().jsonObject.getValue("date").jsonPrimitive.content.toLong())
|
||||
assertEquals(60L, callLogs.first().jsonObject.getValue("duration").jsonPrimitive.content.toLong())
|
||||
assertEquals(1, callLogs.first().jsonObject.getValue("type").jsonPrimitive.content.toInt())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleCallLogSearch_withFilters() {
|
||||
val callLog =
|
||||
CallLogRecord(
|
||||
number = "+123456",
|
||||
cachedName = "lixuankai",
|
||||
date = 1709280000000L,
|
||||
duration = 120L,
|
||||
type = 2,
|
||||
)
|
||||
val handler =
|
||||
CallLogHandler.forTesting(
|
||||
appContext(),
|
||||
FakeCallLogDataSource(canRead = true, searchResults = listOf(callLog)),
|
||||
)
|
||||
|
||||
val result = handler.handleCallLogSearch(
|
||||
"""{"number":"123456","cachedName":"lixuankai","dateStart":1709270000000,"dateEnd":1709290000000,"duration":120,"type":2}"""
|
||||
)
|
||||
|
||||
assertTrue(result.ok)
|
||||
val payload = Json.parseToJsonElement(result.payloadJson ?: error("missing payload")).jsonObject
|
||||
val callLogs = payload.getValue("callLogs").jsonArray
|
||||
assertEquals(1, callLogs.size)
|
||||
assertEquals("lixuankai", callLogs.first().jsonObject.getValue("cachedName").jsonPrimitive.content)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleCallLogSearch_withPagination() {
|
||||
val callLogs =
|
||||
listOf(
|
||||
CallLogRecord(
|
||||
number = "+123456",
|
||||
cachedName = "lixuankai",
|
||||
date = 1709280000000L,
|
||||
duration = 60L,
|
||||
type = 1,
|
||||
),
|
||||
CallLogRecord(
|
||||
number = "+654321",
|
||||
cachedName = "lixuankai2",
|
||||
date = 1709280001000L,
|
||||
duration = 120L,
|
||||
type = 2,
|
||||
),
|
||||
)
|
||||
val handler =
|
||||
CallLogHandler.forTesting(
|
||||
appContext(),
|
||||
FakeCallLogDataSource(canRead = true, searchResults = callLogs),
|
||||
)
|
||||
|
||||
val result = handler.handleCallLogSearch("""{"limit":1,"offset":1}""")
|
||||
|
||||
assertTrue(result.ok)
|
||||
val payload = Json.parseToJsonElement(result.payloadJson ?: error("missing payload")).jsonObject
|
||||
val callLogsResult = payload.getValue("callLogs").jsonArray
|
||||
assertEquals(1, callLogsResult.size)
|
||||
assertEquals("lixuankai2", callLogsResult.first().jsonObject.getValue("cachedName").jsonPrimitive.content)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleCallLogSearch_withDefaultParams() {
|
||||
val callLog =
|
||||
CallLogRecord(
|
||||
number = "+123456",
|
||||
cachedName = "lixuankai",
|
||||
date = 1709280000000L,
|
||||
duration = 60L,
|
||||
type = 1,
|
||||
)
|
||||
val handler =
|
||||
CallLogHandler.forTesting(
|
||||
appContext(),
|
||||
FakeCallLogDataSource(canRead = true, searchResults = listOf(callLog)),
|
||||
)
|
||||
|
||||
val result = handler.handleCallLogSearch(null)
|
||||
|
||||
assertTrue(result.ok)
|
||||
val payload = Json.parseToJsonElement(result.payloadJson ?: error("missing payload")).jsonObject
|
||||
val callLogs = payload.getValue("callLogs").jsonArray
|
||||
assertEquals(1, callLogs.size)
|
||||
assertEquals("+123456", callLogs.first().jsonObject.getValue("number").jsonPrimitive.content)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleCallLogSearch_withNullFields() {
|
||||
val callLog =
|
||||
CallLogRecord(
|
||||
number = null,
|
||||
cachedName = null,
|
||||
date = 1709280000000L,
|
||||
duration = 60L,
|
||||
type = 1,
|
||||
)
|
||||
val handler =
|
||||
CallLogHandler.forTesting(
|
||||
appContext(),
|
||||
FakeCallLogDataSource(canRead = true, searchResults = listOf(callLog)),
|
||||
)
|
||||
|
||||
val result = handler.handleCallLogSearch("""{"limit":1}""")
|
||||
|
||||
assertTrue(result.ok)
|
||||
val payload = Json.parseToJsonElement(result.payloadJson ?: error("missing payload")).jsonObject
|
||||
val callLogs = payload.getValue("callLogs").jsonArray
|
||||
assertEquals(1, callLogs.size)
|
||||
// Verify null values are properly serialized
|
||||
val callLogObj = callLogs.first().jsonObject
|
||||
assertTrue(callLogObj.containsKey("number"))
|
||||
assertTrue(callLogObj.containsKey("cachedName"))
|
||||
}
|
||||
}
|
||||
|
||||
private class FakeCallLogDataSource(
|
||||
private val canRead: Boolean,
|
||||
private val searchResults: List<CallLogRecord> = emptyList(),
|
||||
) : CallLogDataSource {
|
||||
override fun hasReadPermission(context: Context): Boolean = canRead
|
||||
|
||||
override fun search(context: Context, request: CallLogSearchRequest): List<CallLogRecord> {
|
||||
val startIndex = request.offset.coerceAtLeast(0)
|
||||
val endIndex = (startIndex + request.limit).coerceAtMost(searchResults.size)
|
||||
return if (startIndex < searchResults.size) {
|
||||
searchResults.subList(startIndex, endIndex)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,6 +93,7 @@ class DeviceHandlerTest {
|
||||
"photos",
|
||||
"contacts",
|
||||
"calendar",
|
||||
"callLog",
|
||||
"motion",
|
||||
)
|
||||
for (key in expected) {
|
||||
|
||||
@@ -2,6 +2,7 @@ package ai.openclaw.app.node
|
||||
|
||||
import ai.openclaw.app.protocol.OpenClawCalendarCommand
|
||||
import ai.openclaw.app.protocol.OpenClawCameraCommand
|
||||
import ai.openclaw.app.protocol.OpenClawCallLogCommand
|
||||
import ai.openclaw.app.protocol.OpenClawCapability
|
||||
import ai.openclaw.app.protocol.OpenClawContactsCommand
|
||||
import ai.openclaw.app.protocol.OpenClawDeviceCommand
|
||||
@@ -25,6 +26,7 @@ class InvokeCommandRegistryTest {
|
||||
OpenClawCapability.Photos.rawValue,
|
||||
OpenClawCapability.Contacts.rawValue,
|
||||
OpenClawCapability.Calendar.rawValue,
|
||||
OpenClawCapability.CallLog.rawValue,
|
||||
)
|
||||
|
||||
private val optionalCapabilities =
|
||||
@@ -50,6 +52,7 @@ class InvokeCommandRegistryTest {
|
||||
OpenClawContactsCommand.Add.rawValue,
|
||||
OpenClawCalendarCommand.Events.rawValue,
|
||||
OpenClawCalendarCommand.Add.rawValue,
|
||||
OpenClawCallLogCommand.Search.rawValue,
|
||||
)
|
||||
|
||||
private val optionalCommands =
|
||||
|
||||
@@ -34,6 +34,7 @@ class OpenClawProtocolConstantsTest {
|
||||
assertEquals("contacts", OpenClawCapability.Contacts.rawValue)
|
||||
assertEquals("calendar", OpenClawCapability.Calendar.rawValue)
|
||||
assertEquals("motion", OpenClawCapability.Motion.rawValue)
|
||||
assertEquals("callLog", OpenClawCapability.CallLog.rawValue)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -84,4 +85,9 @@ class OpenClawProtocolConstantsTest {
|
||||
assertEquals("motion.activity", OpenClawMotionCommand.Activity.rawValue)
|
||||
assertEquals("motion.pedometer", OpenClawMotionCommand.Pedometer.rawValue)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun callLogCommandsUseStableStrings() {
|
||||
assertEquals("callLog.search", OpenClawCallLogCommand.Search.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,13 +18,10 @@ final class CanvasA2UIActionMessageHandler: NSObject, WKScriptMessageHandler {
|
||||
func userContentController(_: WKUserContentController, didReceive message: WKScriptMessage) {
|
||||
guard Self.allMessageNames.contains(message.name) else { return }
|
||||
|
||||
// Only accept actions from local Canvas content (not arbitrary web pages).
|
||||
// Only accept actions from the in-app canvas scheme. Local-network HTTP
|
||||
// pages are regular web content and must not get direct agent dispatch.
|
||||
guard let webView = message.webView, let url = webView.url else { return }
|
||||
if let scheme = url.scheme, CanvasScheme.allSchemes.contains(scheme) {
|
||||
// ok
|
||||
} else if Self.isLocalNetworkCanvasURL(url) {
|
||||
// ok
|
||||
} else {
|
||||
guard let scheme = url.scheme, CanvasScheme.allSchemes.contains(scheme) else {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -107,10 +104,5 @@ final class CanvasA2UIActionMessageHandler: NSObject, WKScriptMessageHandler {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func isLocalNetworkCanvasURL(_ url: URL) -> Bool {
|
||||
LocalNetworkURLSupport.isLocalNetworkHTTPURL(url)
|
||||
}
|
||||
|
||||
// Formatting helpers live in OpenClawKit (`OpenClawCanvasA2UIAction`).
|
||||
}
|
||||
|
||||
@@ -50,21 +50,24 @@ final class CanvasWindowController: NSWindowController, WKNavigationDelegate, NS
|
||||
|
||||
// Bridge A2UI "a2uiaction" DOM events back into the native agent loop.
|
||||
//
|
||||
// Prefer WKScriptMessageHandler when WebKit exposes it, otherwise fall back to an unattended deep link
|
||||
// (includes the app-generated key so it won't prompt).
|
||||
// Keep the bridge on the trusted in-app canvas scheme only, and do not
|
||||
// expose unattended deep-link credentials to page JavaScript.
|
||||
canvasWindowLogger.debug("CanvasWindowController init building A2UI bridge script")
|
||||
let deepLinkKey = DeepLinkHandler.currentCanvasKey()
|
||||
let injectedSessionKey = sessionKey.trimmingCharacters(in: .whitespacesAndNewlines).nonEmpty ?? "main"
|
||||
let allowedSchemesJSON = (
|
||||
try? String(
|
||||
data: JSONSerialization.data(withJSONObject: CanvasScheme.allSchemes),
|
||||
encoding: .utf8)
|
||||
) ?? "[]"
|
||||
let bridgeScript = """
|
||||
(() => {
|
||||
try {
|
||||
const allowedSchemes = \(String(describing: CanvasScheme.allSchemes));
|
||||
const allowedSchemes = \(allowedSchemesJSON);
|
||||
const protocol = location.protocol.replace(':', '');
|
||||
if (!allowedSchemes.includes(protocol)) return;
|
||||
if (globalThis.__openclawA2UIBridgeInstalled) return;
|
||||
globalThis.__openclawA2UIBridgeInstalled = true;
|
||||
|
||||
const deepLinkKey = \(Self.jsStringLiteral(deepLinkKey));
|
||||
const sessionKey = \(Self.jsStringLiteral(injectedSessionKey));
|
||||
const machineName = \(Self.jsStringLiteral(InstanceIdentity.displayName));
|
||||
const instanceId = \(Self.jsStringLiteral(InstanceIdentity.instanceId));
|
||||
@@ -104,24 +107,8 @@ final class CanvasWindowController: NSWindowController, WKNavigationDelegate, NS
|
||||
return;
|
||||
}
|
||||
|
||||
const ctx = userAction.context ? (' ctx=' + JSON.stringify(userAction.context)) : '';
|
||||
const message =
|
||||
'CANVAS_A2UI action=' + userAction.name +
|
||||
' session=' + sessionKey +
|
||||
' surface=' + userAction.surfaceId +
|
||||
' component=' + (userAction.sourceComponentId || '-') +
|
||||
' host=' + machineName.replace(/\\s+/g, '_') +
|
||||
' instance=' + instanceId +
|
||||
ctx +
|
||||
' default=update_canvas';
|
||||
const params = new URLSearchParams();
|
||||
params.set('message', message);
|
||||
params.set('sessionKey', sessionKey);
|
||||
params.set('thinking', 'low');
|
||||
params.set('deliver', 'false');
|
||||
params.set('channel', 'last');
|
||||
params.set('key', deepLinkKey);
|
||||
location.href = 'openclaw://agent?' + params.toString();
|
||||
// Without the native handler, fail closed instead of exposing an
|
||||
// unattended deep-link credential to page JavaScript.
|
||||
} catch {}
|
||||
}, true);
|
||||
} catch {}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
{"generatedBy":"scripts/generate-config-doc-baseline.ts","recordType":"meta","totalPaths":4733}
|
||||
{"generatedBy":"scripts/generate-config-doc-baseline.ts","recordType":"meta","totalPaths":4889}
|
||||
{"recordType":"path","path":"acp","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"ACP","help":"ACP runtime controls for enabling dispatch, selecting backends, constraining allowed agent targets, and tuning streamed turn projection behavior.","hasChildren":true}
|
||||
{"recordType":"path","path":"acp.allowedAgents","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"ACP Allowed Agents","help":"Allowlist of ACP target agent ids permitted for ACP runtime sessions. Empty means no additional allowlist restriction.","hasChildren":true}
|
||||
{"recordType":"path","path":"acp.allowedAgents.*","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@@ -101,6 +101,7 @@
|
||||
{"recordType":"path","path":"agents.defaults.compaction.recentTurnsPreserve","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Compaction Preserve Recent Turns","help":"Number of most recent user/assistant turns kept verbatim outside safeguard summarization (default: 3). Raise this to preserve exact recent dialogue context, or lower it to maximize compaction savings.","hasChildren":false}
|
||||
{"recordType":"path","path":"agents.defaults.compaction.reserveTokens","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":["auth","security"],"label":"Compaction Reserve Tokens","help":"Token headroom reserved for reply generation and tool output after compaction runs. Use higher reserves for verbose/tool-heavy sessions, and lower reserves when maximizing retained history matters more.","hasChildren":false}
|
||||
{"recordType":"path","path":"agents.defaults.compaction.reserveTokensFloor","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":["auth","security"],"label":"Compaction Reserve Token Floor","help":"Minimum floor enforced for reserveTokens in Pi compaction paths (0 disables the floor guard). Use a non-zero floor to avoid over-aggressive compression under fluctuating token estimates.","hasChildren":false}
|
||||
{"recordType":"path","path":"agents.defaults.compaction.timeoutSeconds","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":["performance"],"label":"Compaction Timeout (Seconds)","help":"Maximum time in seconds allowed for a single compaction operation before it is aborted (default: 900). Increase this for very large sessions that need more time to summarize, or decrease it to fail faster on unresponsive models.","hasChildren":false}
|
||||
{"recordType":"path","path":"agents.defaults.contextPruning","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"agents.defaults.contextPruning.hardClear","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"agents.defaults.contextPruning.hardClear.enabled","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@@ -143,7 +144,7 @@
|
||||
{"recordType":"path","path":"agents.defaults.heartbeat.prompt","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"agents.defaults.heartbeat.session","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"agents.defaults.heartbeat.suppressToolErrorWarnings","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["automation"],"label":"Heartbeat Suppress Tool Error Warnings","help":"Suppress tool error warning payloads during heartbeat runs.","hasChildren":false}
|
||||
{"recordType":"path","path":"agents.defaults.heartbeat.target","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["automation"],"help":"Delivery target (\"last\", \"none\", or a channel id). Known channels: telegram, whatsapp, discord, irc, googlechat, slack, signal, imessage, line, zalouser, zalo, tlon, feishu, nextcloud-talk, msteams, bluebubbles, synology-chat, mattermost, twitch, matrix, nostr.","hasChildren":false}
|
||||
{"recordType":"path","path":"agents.defaults.heartbeat.target","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["automation"],"help":"Delivery target (\"last\", \"none\", or a channel id). Known channels: telegram, whatsapp, discord, irc, googlechat, slack, signal, imessage, line, bluebubbles, feishu, matrix, mattermost, msteams, nextcloud-talk, nostr, synology-chat, tlon, twitch, zalo, zalouser.","hasChildren":false}
|
||||
{"recordType":"path","path":"agents.defaults.heartbeat.to","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"agents.defaults.humanDelay","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"agents.defaults.humanDelay.maxMs","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":["performance"],"label":"Human Delay Max (ms)","help":"Maximum delay in ms for custom humanDelay (default: 2500).","hasChildren":false}
|
||||
@@ -347,7 +348,7 @@
|
||||
{"recordType":"path","path":"agents.list.*.heartbeat.prompt","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"agents.list.*.heartbeat.session","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"agents.list.*.heartbeat.suppressToolErrorWarnings","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["automation"],"label":"Agent Heartbeat Suppress Tool Error Warnings","help":"Suppress tool error warning payloads during heartbeat runs.","hasChildren":false}
|
||||
{"recordType":"path","path":"agents.list.*.heartbeat.target","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["automation"],"help":"Delivery target (\"last\", \"none\", or a channel id). Known channels: telegram, whatsapp, discord, irc, googlechat, slack, signal, imessage, line, zalouser, zalo, tlon, feishu, nextcloud-talk, msteams, bluebubbles, synology-chat, mattermost, twitch, matrix, nostr.","hasChildren":false}
|
||||
{"recordType":"path","path":"agents.list.*.heartbeat.target","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["automation"],"help":"Delivery target (\"last\", \"none\", or a channel id). Known channels: telegram, whatsapp, discord, irc, googlechat, slack, signal, imessage, line, bluebubbles, feishu, matrix, mattermost, msteams, nextcloud-talk, nostr, synology-chat, tlon, twitch, zalo, zalouser.","hasChildren":false}
|
||||
{"recordType":"path","path":"agents.list.*.heartbeat.to","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"agents.list.*.humanDelay","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"agents.list.*.humanDelay.maxMs","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@@ -912,6 +913,8 @@
|
||||
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.toolsBySender.*.deny.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.users","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.discord.accounts.*.guilds.*.users.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.discord.accounts.*.healthMonitor","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.discord.accounts.*.healthMonitor.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.discord.accounts.*.heartbeat","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.discord.accounts.*.heartbeat.showAlerts","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.discord.accounts.*.heartbeat.showOk","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@@ -1165,6 +1168,8 @@
|
||||
{"recordType":"path","path":"channels.discord.guilds.*.toolsBySender.*.deny.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.discord.guilds.*.users","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.discord.guilds.*.users.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.discord.healthMonitor","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.discord.healthMonitor.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.discord.heartbeat","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.discord.heartbeat.showAlerts","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.discord.heartbeat.showOk","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@@ -1280,61 +1285,182 @@
|
||||
{"recordType":"path","path":"channels.feishu","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Feishu","help":"飞书/Lark enterprise messaging.","hasChildren":true}
|
||||
{"recordType":"path","path":"channels.feishu.accounts","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.actions","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.actions.reactions","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.allowFrom","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.allowFrom.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.appId","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.appSecret","kind":"channel","type":["object","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.appSecret.id","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.appSecret.provider","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.appSecret.source","kind":"channel","type":"string","required":true,"enumValues":["env","file","exec"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.appSecret.source","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.blockStreamingCoalesce","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.blockStreamingCoalesce.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.blockStreamingCoalesce.maxDelayMs","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.blockStreamingCoalesce.minDelayMs","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.capabilities","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.capabilities.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.chunkMode","kind":"channel","type":"string","required":false,"enumValues":["length","newline"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.configWrites","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.connectionMode","kind":"channel","type":"string","required":false,"enumValues":["websocket","webhook"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.dmHistoryLimit","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.dmPolicy","kind":"channel","type":"string","required":false,"enumValues":["open","pairing","allowlist"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.dms","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.dms.*","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.dms.*.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.dms.*.systemPrompt","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.domain","kind":"channel","type":"string","required":false,"enumValues":["feishu","lark"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.encryptKey","kind":"channel","type":["object","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.encryptKey.id","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.encryptKey.provider","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.encryptKey.source","kind":"channel","type":"string","required":true,"enumValues":["env","file","exec"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.encryptKey.source","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.groupAllowFrom","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.groupAllowFrom.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.groupPolicy","kind":"channel","type":"string","required":false,"enumValues":["open","allowlist","disabled"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.groups","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.groups.*","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.groups.*.allowFrom","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.groups.*.allowFrom.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.groups.*.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.groups.*.groupSessionScope","kind":"channel","type":"string","required":false,"enumValues":["group","group_sender","group_topic","group_topic_sender"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.groups.*.replyInThread","kind":"channel","type":"string","required":false,"enumValues":["disabled","enabled"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.groups.*.requireMention","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.groups.*.skills","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.groups.*.skills.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.groups.*.systemPrompt","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.groups.*.tools","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.groups.*.tools.allow","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.groups.*.tools.allow.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.groups.*.tools.deny","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.groups.*.tools.deny.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.groups.*.topicSessionMode","kind":"channel","type":"string","required":false,"enumValues":["disabled","enabled"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.groupSenderAllowFrom","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.groupSenderAllowFrom.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.groupSessionScope","kind":"channel","type":"string","required":false,"enumValues":["group","group_sender","group_topic","group_topic_sender"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.heartbeat","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.heartbeat.intervalMs","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.heartbeat.visibility","kind":"channel","type":"string","required":false,"enumValues":["visible","hidden"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.historyLimit","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.httpTimeoutMs","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.markdown","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.markdown.mode","kind":"channel","type":"string","required":false,"enumValues":["native","escape","strip"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.markdown.tableMode","kind":"channel","type":"string","required":false,"enumValues":["native","ascii","simple"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.mediaMaxMb","kind":"channel","type":"number","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.name","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.reactionNotifications","kind":"channel","type":"string","required":false,"enumValues":["off","own","all"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.renderMode","kind":"channel","type":"string","required":false,"enumValues":["auto","raw","card"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.replyInThread","kind":"channel","type":"string","required":false,"enumValues":["disabled","enabled"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.requireMention","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.resolveSenderNames","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.streaming","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.textChunkLimit","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.tools","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.tools.chat","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.tools.doc","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.tools.drive","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.tools.perm","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.tools.scopes","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.tools.wiki","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.topicSessionMode","kind":"channel","type":"string","required":false,"enumValues":["disabled","enabled"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.typingIndicator","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.verificationToken","kind":"channel","type":["object","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.verificationToken.id","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.verificationToken.provider","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.verificationToken.source","kind":"channel","type":"string","required":true,"enumValues":["env","file","exec"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.verificationToken.source","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.webhookHost","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.webhookPath","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.accounts.*.webhookPort","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.actions","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.feishu.actions.reactions","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.allowFrom","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.feishu.allowFrom.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.appId","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.appSecret","kind":"channel","type":["object","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.feishu.appSecret.id","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.appSecret.provider","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.appSecret.source","kind":"channel","type":"string","required":true,"enumValues":["env","file","exec"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.appSecret.source","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.blockStreamingCoalesce","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.feishu.blockStreamingCoalesce.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.blockStreamingCoalesce.maxDelayMs","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.blockStreamingCoalesce.minDelayMs","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.capabilities","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.feishu.capabilities.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.chunkMode","kind":"channel","type":"string","required":false,"enumValues":["length","newline"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.connectionMode","kind":"channel","type":"string","required":false,"enumValues":["websocket","webhook"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.configWrites","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.connectionMode","kind":"channel","type":"string","required":true,"enumValues":["websocket","webhook"],"defaultValue":"websocket","deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.defaultAccount","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.dmHistoryLimit","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.dmPolicy","kind":"channel","type":"string","required":false,"enumValues":["open","pairing","allowlist"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.domain","kind":"channel","type":"string","required":false,"enumValues":["feishu","lark"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.dmPolicy","kind":"channel","type":"string","required":true,"enumValues":["open","pairing","allowlist"],"defaultValue":"pairing","deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.dms","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.feishu.dms.*","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.feishu.dms.*.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.dms.*.systemPrompt","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.domain","kind":"channel","type":"string","required":true,"enumValues":["feishu","lark"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.dynamicAgentCreation","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.feishu.dynamicAgentCreation.agentDirTemplate","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.dynamicAgentCreation.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.dynamicAgentCreation.maxAgents","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.dynamicAgentCreation.workspaceTemplate","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.encryptKey","kind":"channel","type":["object","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.feishu.encryptKey.id","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.encryptKey.provider","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.encryptKey.source","kind":"channel","type":"string","required":true,"enumValues":["env","file","exec"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.encryptKey.source","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.groupAllowFrom","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.feishu.groupAllowFrom.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.groupPolicy","kind":"channel","type":"string","required":false,"enumValues":["open","allowlist","disabled"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.groupPolicy","kind":"channel","type":"string","required":true,"enumValues":["open","allowlist","disabled"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.groups","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.feishu.groups.*","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.feishu.groups.*.allowFrom","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.feishu.groups.*.allowFrom.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.groups.*.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.groups.*.groupSessionScope","kind":"channel","type":"string","required":false,"enumValues":["group","group_sender","group_topic","group_topic_sender"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.groups.*.replyInThread","kind":"channel","type":"string","required":false,"enumValues":["disabled","enabled"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.groups.*.requireMention","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.groups.*.skills","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.feishu.groups.*.skills.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.groups.*.systemPrompt","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.groups.*.tools","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.feishu.groups.*.tools.allow","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.feishu.groups.*.tools.allow.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.groups.*.tools.deny","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.feishu.groups.*.tools.deny.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.groups.*.topicSessionMode","kind":"channel","type":"string","required":false,"enumValues":["disabled","enabled"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.groupSenderAllowFrom","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.feishu.groupSenderAllowFrom.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.groupSessionScope","kind":"channel","type":"string","required":false,"enumValues":["group","group_sender","group_topic","group_topic_sender"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.heartbeat","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.feishu.heartbeat.intervalMs","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.heartbeat.visibility","kind":"channel","type":"string","required":false,"enumValues":["visible","hidden"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.historyLimit","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.httpTimeoutMs","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.markdown","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.feishu.markdown.mode","kind":"channel","type":"string","required":false,"enumValues":["native","escape","strip"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.markdown.tableMode","kind":"channel","type":"string","required":false,"enumValues":["native","ascii","simple"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.mediaMaxMb","kind":"channel","type":"number","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.reactionNotifications","kind":"channel","type":"string","required":true,"enumValues":["off","own","all"],"defaultValue":"own","deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.renderMode","kind":"channel","type":"string","required":false,"enumValues":["auto","raw","card"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.replyInThread","kind":"channel","type":"string","required":false,"enumValues":["disabled","enabled"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.requireMention","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.requireMention","kind":"channel","type":"boolean","required":true,"defaultValue":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.resolveSenderNames","kind":"channel","type":"boolean","required":true,"defaultValue":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.streaming","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.textChunkLimit","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.tools","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.feishu.tools.chat","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.tools.doc","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.tools.drive","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.tools.perm","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.tools.scopes","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.tools.wiki","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.topicSessionMode","kind":"channel","type":"string","required":false,"enumValues":["disabled","enabled"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.typingIndicator","kind":"channel","type":"boolean","required":true,"defaultValue":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.verificationToken","kind":"channel","type":["object","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.feishu.verificationToken.id","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.verificationToken.provider","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.verificationToken.source","kind":"channel","type":"string","required":true,"enumValues":["env","file","exec"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.verificationToken.source","kind":"channel","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.webhookHost","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.webhookPath","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.webhookPath","kind":"channel","type":"string","required":true,"defaultValue":"/feishu/events","deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.feishu.webhookPort","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.googlechat","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Google Chat","help":"Google Workspace Chat app with HTTP webhook.","hasChildren":true}
|
||||
{"recordType":"path","path":"channels.googlechat.accounts","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
@@ -1342,6 +1468,7 @@
|
||||
{"recordType":"path","path":"channels.googlechat.accounts.*.actions","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.googlechat.accounts.*.actions.reactions","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.googlechat.accounts.*.allowBots","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.googlechat.accounts.*.appPrincipal","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.googlechat.accounts.*.audience","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.googlechat.accounts.*.audienceType","kind":"channel","type":"string","required":false,"enumValues":["app-url","project-number"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.googlechat.accounts.*.blockStreaming","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@@ -1377,6 +1504,8 @@
|
||||
{"recordType":"path","path":"channels.googlechat.accounts.*.groups.*.systemPrompt","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.googlechat.accounts.*.groups.*.users","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.googlechat.accounts.*.groups.*.users.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.googlechat.accounts.*.healthMonitor","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.googlechat.accounts.*.healthMonitor.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.googlechat.accounts.*.historyLimit","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.googlechat.accounts.*.mediaMaxMb","kind":"channel","type":"number","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.googlechat.accounts.*.name","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@@ -1401,6 +1530,7 @@
|
||||
{"recordType":"path","path":"channels.googlechat.actions","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.googlechat.actions.reactions","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.googlechat.allowBots","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.googlechat.appPrincipal","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.googlechat.audience","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.googlechat.audienceType","kind":"channel","type":"string","required":false,"enumValues":["app-url","project-number"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.googlechat.blockStreaming","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@@ -1437,6 +1567,8 @@
|
||||
{"recordType":"path","path":"channels.googlechat.groups.*.systemPrompt","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.googlechat.groups.*.users","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.googlechat.groups.*.users.*","kind":"channel","type":["number","string"],"required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.googlechat.healthMonitor","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.googlechat.healthMonitor.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.googlechat.historyLimit","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.googlechat.mediaMaxMb","kind":"channel","type":"number","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.googlechat.name","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@@ -1504,6 +1636,8 @@
|
||||
{"recordType":"path","path":"channels.imessage.accounts.*.groups.*.toolsBySender.*.alsoAllow.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.imessage.accounts.*.groups.*.toolsBySender.*.deny","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.imessage.accounts.*.groups.*.toolsBySender.*.deny.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.imessage.accounts.*.healthMonitor","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.imessage.accounts.*.healthMonitor.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.imessage.accounts.*.heartbeat","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.imessage.accounts.*.heartbeat.showAlerts","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.imessage.accounts.*.heartbeat.showOk","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@@ -1565,6 +1699,8 @@
|
||||
{"recordType":"path","path":"channels.imessage.groups.*.toolsBySender.*.alsoAllow.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.imessage.groups.*.toolsBySender.*.deny","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.imessage.groups.*.toolsBySender.*.deny.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.imessage.healthMonitor","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.imessage.healthMonitor.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.imessage.heartbeat","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.imessage.heartbeat.showAlerts","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.imessage.heartbeat.showOk","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@@ -1969,6 +2105,8 @@
|
||||
{"recordType":"path","path":"channels.msteams.groupAllowFrom","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.msteams.groupAllowFrom.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.msteams.groupPolicy","kind":"channel","type":"string","required":true,"enumValues":["open","disabled","allowlist"],"defaultValue":"allowlist","deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.msteams.healthMonitor","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.msteams.healthMonitor.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.msteams.heartbeat","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.msteams.heartbeat.showAlerts","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.msteams.heartbeat.showOk","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@@ -2214,6 +2352,8 @@
|
||||
{"recordType":"path","path":"channels.signal.accounts.*.groups.*.toolsBySender.*.alsoAllow.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.signal.accounts.*.groups.*.toolsBySender.*.deny","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.signal.accounts.*.groups.*.toolsBySender.*.deny.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.signal.accounts.*.healthMonitor","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.signal.accounts.*.healthMonitor.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.signal.accounts.*.heartbeat","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.signal.accounts.*.heartbeat.showAlerts","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.signal.accounts.*.heartbeat.showOk","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@@ -2282,6 +2422,8 @@
|
||||
{"recordType":"path","path":"channels.signal.groups.*.toolsBySender.*.alsoAllow.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.signal.groups.*.toolsBySender.*.deny","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.signal.groups.*.toolsBySender.*.deny.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.signal.healthMonitor","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.signal.healthMonitor.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.signal.heartbeat","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.signal.heartbeat.showAlerts","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.signal.heartbeat.showOk","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@@ -2386,6 +2528,8 @@
|
||||
{"recordType":"path","path":"channels.slack.accounts.*.dms.*.historyLimit","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.slack.accounts.*.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.slack.accounts.*.groupPolicy","kind":"channel","type":"string","required":false,"enumValues":["open","disabled","allowlist"],"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.slack.accounts.*.healthMonitor","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.slack.accounts.*.healthMonitor.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.slack.accounts.*.heartbeat","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.slack.accounts.*.heartbeat.showAlerts","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.slack.accounts.*.heartbeat.showOk","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@@ -2509,6 +2653,8 @@
|
||||
{"recordType":"path","path":"channels.slack.dms.*.historyLimit","kind":"channel","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.slack.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.slack.groupPolicy","kind":"channel","type":"string","required":true,"enumValues":["open","disabled","allowlist"],"defaultValue":"allowlist","deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.slack.healthMonitor","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.slack.healthMonitor.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.slack.heartbeat","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.slack.heartbeat.showAlerts","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.slack.heartbeat.showOk","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@@ -2688,6 +2834,8 @@
|
||||
{"recordType":"path","path":"channels.telegram.accounts.*.groups.*.topics.*.skills","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.telegram.accounts.*.groups.*.topics.*.skills.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.telegram.accounts.*.groups.*.topics.*.systemPrompt","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.telegram.accounts.*.healthMonitor","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.telegram.accounts.*.healthMonitor.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.telegram.accounts.*.heartbeat","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.telegram.accounts.*.heartbeat.showAlerts","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.telegram.accounts.*.heartbeat.showOk","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@@ -2862,6 +3010,8 @@
|
||||
{"recordType":"path","path":"channels.telegram.groups.*.topics.*.skills","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.telegram.groups.*.topics.*.skills.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.telegram.groups.*.topics.*.systemPrompt","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.telegram.healthMonitor","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.telegram.healthMonitor.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.telegram.heartbeat","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.telegram.heartbeat.showAlerts","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.telegram.heartbeat.showOk","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@@ -3032,6 +3182,8 @@
|
||||
{"recordType":"path","path":"channels.whatsapp.accounts.*.groups.*.toolsBySender.*.alsoAllow.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.whatsapp.accounts.*.groups.*.toolsBySender.*.deny","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.whatsapp.accounts.*.groups.*.toolsBySender.*.deny.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.whatsapp.accounts.*.healthMonitor","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.whatsapp.accounts.*.healthMonitor.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.whatsapp.accounts.*.heartbeat","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.whatsapp.accounts.*.heartbeat.showAlerts","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.whatsapp.accounts.*.heartbeat.showOk","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@@ -3095,6 +3247,8 @@
|
||||
{"recordType":"path","path":"channels.whatsapp.groups.*.toolsBySender.*.alsoAllow.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.whatsapp.groups.*.toolsBySender.*.deny","kind":"channel","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.whatsapp.groups.*.toolsBySender.*.deny.*","kind":"channel","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.whatsapp.healthMonitor","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.whatsapp.healthMonitor.enabled","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.whatsapp.heartbeat","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
|
||||
{"recordType":"path","path":"channels.whatsapp.heartbeat.showAlerts","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"channels.whatsapp.heartbeat.showOk","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@@ -3330,6 +3484,8 @@
|
||||
{"recordType":"path","path":"gateway.auth.trustedProxy.userHeader","kind":"core","type":"string","required":true,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"gateway.bind","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["network"],"label":"Gateway Bind Mode","help":"Network bind profile: \"auto\", \"lan\", \"loopback\", \"custom\", or \"tailnet\" to control interface exposure. Keep \"loopback\" or \"auto\" for safest local operation unless external clients must connect.","hasChildren":false}
|
||||
{"recordType":"path","path":"gateway.channelHealthCheckMinutes","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":["network","reliability"],"label":"Gateway Channel Health Check Interval (min)","help":"Interval in minutes for automatic channel health probing and status updates. Use lower intervals for faster detection, or higher intervals to reduce periodic probe noise.","hasChildren":false}
|
||||
{"recordType":"path","path":"gateway.channelMaxRestartsPerHour","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":["network","performance"],"label":"Gateway Channel Max Restarts Per Hour","help":"Maximum number of health-monitor-initiated channel restarts allowed within a rolling one-hour window. Once hit, further restarts are skipped until the window expires. Default: 10.","hasChildren":false}
|
||||
{"recordType":"path","path":"gateway.channelStaleEventThresholdMinutes","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":["network"],"label":"Gateway Channel Stale Event Threshold (min)","help":"How many minutes a connected channel can go without receiving any event before the health monitor treats it as a stale socket and triggers a restart. Default: 30.","hasChildren":false}
|
||||
{"recordType":"path","path":"gateway.controlUi","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["network"],"label":"Control UI","help":"Control UI hosting settings including enablement, pathing, and browser-origin/auth hardening behavior. Keep UI exposure minimal and pair with strong auth controls before internet-facing deployments.","hasChildren":true}
|
||||
{"recordType":"path","path":"gateway.controlUi.allowedOrigins","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access","network"],"label":"Control UI Allowed Origins","help":"Allowed browser origins for Control UI/WebChat websocket connections (full origins only, e.g. https://control.example.com). Required for non-loopback Control UI deployments unless dangerous Host-header fallback is explicitly enabled.","hasChildren":true}
|
||||
{"recordType":"path","path":"gateway.controlUi.allowedOrigins.*","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
@@ -3584,7 +3740,7 @@
|
||||
{"recordType":"path","path":"messages.ackReactionScope","kind":"core","type":"string","required":false,"enumValues":["group-mentions","group-all","direct","all","off","none"],"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Ack Reaction Scope","help":"When to send ack reactions (\"group-mentions\", \"group-all\", \"direct\", \"all\", \"off\", \"none\"). \"off\"/\"none\" disables ack reactions entirely.","hasChildren":false}
|
||||
{"recordType":"path","path":"messages.groupChat","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Group Chat Rules","help":"Group-message handling controls including mention triggers and history window sizing. Keep mention patterns narrow so group channels do not trigger on every message.","hasChildren":true}
|
||||
{"recordType":"path","path":"messages.groupChat.historyLimit","kind":"core","type":"integer","required":false,"deprecated":false,"sensitive":false,"tags":["performance"],"label":"Group History Limit","help":"Maximum number of prior group messages loaded as context per turn for group sessions. Use higher values for richer continuity, or lower values for faster and cheaper responses.","hasChildren":false}
|
||||
{"recordType":"path","path":"messages.groupChat.mentionPatterns","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Group Mention Patterns","help":"Regex-like patterns used to detect explicit mentions/trigger phrases in group chats. Use precise patterns to reduce false positives in high-volume channels.","hasChildren":true}
|
||||
{"recordType":"path","path":"messages.groupChat.mentionPatterns","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Group Mention Patterns","help":"Safe case-insensitive regex patterns used to detect explicit mentions/trigger phrases in group chats. Use precise patterns to reduce false positives in high-volume channels; invalid or unsafe nested-repetition patterns are ignored.","hasChildren":true}
|
||||
{"recordType":"path","path":"messages.groupChat.mentionPatterns.*","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
|
||||
{"recordType":"path","path":"messages.inbound","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Inbound Debounce","help":"Direct inbound debounce settings used before queue/turn processing starts. Configure this for provider-specific rapid message bursts from the same sender.","hasChildren":true}
|
||||
{"recordType":"path","path":"messages.inbound.byChannel","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Inbound Debounce by Channel (ms)","help":"Per-channel inbound debounce overrides keyed by provider id in milliseconds. Use this where some providers send message fragments more aggressively than others.","hasChildren":true}
|
||||
|
||||
@@ -123,6 +123,22 @@
|
||||
"source": "Network model",
|
||||
"target": "网络模型"
|
||||
},
|
||||
{
|
||||
"source": "Doctor",
|
||||
"target": "Doctor"
|
||||
},
|
||||
{
|
||||
"source": "Polls",
|
||||
"target": "投票"
|
||||
},
|
||||
{
|
||||
"source": "Release Policy",
|
||||
"target": "发布策略"
|
||||
},
|
||||
{
|
||||
"source": "Release policy",
|
||||
"target": "发布策略"
|
||||
},
|
||||
{
|
||||
"source": "for full details",
|
||||
"target": "了解详情"
|
||||
|
||||
418
docs/assets/openclaw-logo-text-dark.svg
Normal file
418
docs/assets/openclaw-logo-text-dark.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 64 KiB |
418
docs/assets/openclaw-logo-text.svg
Normal file
418
docs/assets/openclaw-logo-text.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 64 KiB |
@@ -532,6 +532,75 @@ Feishu supports streaming replies via interactive cards. When enabled, the bot u
|
||||
|
||||
Set `streaming: false` to wait for the full reply before sending.
|
||||
|
||||
### ACP sessions
|
||||
|
||||
Feishu supports ACP for:
|
||||
|
||||
- DMs
|
||||
- group topic conversations
|
||||
|
||||
Feishu ACP is text-command driven. There are no native slash-command menus, so use `/acp ...` messages directly in the conversation.
|
||||
|
||||
#### Persistent ACP bindings
|
||||
|
||||
Use top-level typed ACP bindings to pin a Feishu DM or topic conversation to a persistent ACP session.
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "codex",
|
||||
runtime: {
|
||||
type: "acp",
|
||||
acp: {
|
||||
agent: "codex",
|
||||
backend: "acpx",
|
||||
mode: "persistent",
|
||||
cwd: "/workspace/openclaw",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
bindings: [
|
||||
{
|
||||
type: "acp",
|
||||
agentId: "codex",
|
||||
match: {
|
||||
channel: "feishu",
|
||||
accountId: "default",
|
||||
peer: { kind: "direct", id: "ou_1234567890" },
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "acp",
|
||||
agentId: "codex",
|
||||
match: {
|
||||
channel: "feishu",
|
||||
accountId: "default",
|
||||
peer: { kind: "group", id: "oc_group_chat:topic:om_topic_root" },
|
||||
},
|
||||
acp: { label: "codex-feishu-topic" },
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
#### Thread-bound ACP spawn from chat
|
||||
|
||||
In a Feishu DM or topic conversation, you can spawn and bind an ACP session in place:
|
||||
|
||||
```text
|
||||
/acp spawn codex --thread here
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- `--thread here` works for DMs and Feishu topics.
|
||||
- Follow-up messages in the bound DM/topic route directly to that ACP session.
|
||||
- v1 does not target generic non-topic group chats.
|
||||
|
||||
### Multi-agent routing
|
||||
|
||||
Use `bindings` to route Feishu DMs or groups to different agents.
|
||||
|
||||
@@ -13,7 +13,7 @@ Note: `agents.list[].groupChat.mentionPatterns` is now used by Telegram/Discord/
|
||||
|
||||
## What’s implemented (2025-12-03)
|
||||
|
||||
- Activation modes: `mention` (default) or `always`. `mention` requires a ping (real WhatsApp @-mentions via `mentionedJids`, regex patterns, or the bot’s E.164 anywhere in the text). `always` wakes the agent on every message but it should reply only when it can add meaningful value; otherwise it returns the silent token `NO_REPLY`. Defaults can be set in config (`channels.whatsapp.groups`) and overridden per group via `/activation`. When `channels.whatsapp.groups` is set, it also acts as a group allowlist (include `"*"` to allow all).
|
||||
- Activation modes: `mention` (default) or `always`. `mention` requires a ping (real WhatsApp @-mentions via `mentionedJids`, safe regex patterns, or the bot’s E.164 anywhere in the text). `always` wakes the agent on every message but it should reply only when it can add meaningful value; otherwise it returns the silent token `NO_REPLY`. Defaults can be set in config (`channels.whatsapp.groups`) and overridden per group via `/activation`. When `channels.whatsapp.groups` is set, it also acts as a group allowlist (include `"*"` to allow all).
|
||||
- Group policy: `channels.whatsapp.groupPolicy` controls whether group messages are accepted (`open|disabled|allowlist`). `allowlist` uses `channels.whatsapp.groupAllowFrom` (fallback: explicit `channels.whatsapp.allowFrom`). Default is `allowlist` (blocked until you add senders).
|
||||
- Per-group sessions: session keys look like `agent:<agentId>:whatsapp:group:<jid>` so commands such as `/verbose on` or `/think high` (sent as standalone messages) are scoped to that group; personal DM state is untouched. Heartbeats are skipped for group threads.
|
||||
- Context injection: **pending-only** group messages (default 50) that _did not_ trigger a run are prefixed under `[Chat messages since your last reply - for context]`, with the triggering line under `[Current message - respond to this]`. Messages already in the session are not re-injected.
|
||||
@@ -50,7 +50,7 @@ Add a `groupChat` block to `~/.openclaw/openclaw.json` so display-name pings wor
|
||||
|
||||
Notes:
|
||||
|
||||
- The regexes are case-insensitive; they cover a display-name ping like `@openclaw` and the raw number with or without `+`/spaces.
|
||||
- The regexes are case-insensitive and use the same safe-regex guardrails as other config regex surfaces; invalid patterns and unsafe nested repetition are ignored.
|
||||
- WhatsApp still sends canonical mentions via `mentionedJids` when someone taps the contact, so the number fallback is rarely needed but is a useful safety net.
|
||||
|
||||
### Activation command (owner-only)
|
||||
|
||||
@@ -243,7 +243,7 @@ Replying to a bot message counts as an implicit mention (when the channel suppor
|
||||
|
||||
Notes:
|
||||
|
||||
- `mentionPatterns` are case-insensitive regexes.
|
||||
- `mentionPatterns` are case-insensitive safe regex patterns; invalid patterns and unsafe nested-repetition forms are ignored.
|
||||
- Surfaces that provide explicit mentions still pass; patterns are a fallback.
|
||||
- Per-agent override: `agents.list[].groupChat.mentionPatterns` (useful when multiple agents share a group).
|
||||
- Mention gating is only enforced when mention detection is possible (native mentions or `mentionPatterns` are configured).
|
||||
|
||||
@@ -40,6 +40,15 @@ openclaw plugins install --link <path-to-openclaw>/extensions/nostr
|
||||
|
||||
Restart the Gateway after installing or enabling plugins.
|
||||
|
||||
### Non-interactive setup
|
||||
|
||||
```bash
|
||||
openclaw channels add --channel nostr --private-key "$NOSTR_PRIVATE_KEY"
|
||||
openclaw channels add --channel nostr --private-key "$NOSTR_PRIVATE_KEY" --relay-urls "wss://relay.damus.io,wss://relay.primal.net"
|
||||
```
|
||||
|
||||
Use `--use-env` to keep `NOSTR_PRIVATE_KEY` in the environment instead of storing the key in config.
|
||||
|
||||
## Quick setup
|
||||
|
||||
1. Generate a Nostr keypair (if needed):
|
||||
|
||||
@@ -27,13 +27,17 @@ Details: [Plugins](/tools/plugin)
|
||||
## Quick setup
|
||||
|
||||
1. Install and enable the Synology Chat plugin.
|
||||
- `openclaw onboard` now shows Synology Chat in the same channel setup list as `openclaw channels add`.
|
||||
- Non-interactive setup: `openclaw channels add --channel synology-chat --token <token> --url <incoming-webhook-url>`
|
||||
2. In Synology Chat integrations:
|
||||
- Create an incoming webhook and copy its URL.
|
||||
- Create an outgoing webhook with your secret token.
|
||||
3. Point the outgoing webhook URL to your OpenClaw gateway:
|
||||
- `https://gateway-host/webhook/synology` by default.
|
||||
- Or your custom `channels.synology-chat.webhookPath`.
|
||||
4. Configure `channels.synology-chat` in OpenClaw.
|
||||
4. Finish setup in OpenClaw.
|
||||
- Guided: `openclaw onboard`
|
||||
- Direct: `openclaw channels add --channel synology-chat --token <token> --url <incoming-webhook-url>`
|
||||
5. Restart gateway and send a DM to the Synology Chat bot.
|
||||
|
||||
Minimal config:
|
||||
|
||||
@@ -7,7 +7,7 @@ title: "Zalo"
|
||||
|
||||
# Zalo (Bot API)
|
||||
|
||||
Status: experimental. DMs are supported; group handling is available with explicit group policy controls.
|
||||
Status: experimental. DMs are supported. The [Capabilities](#capabilities) section below reflects current Marketplace-bot behavior.
|
||||
|
||||
## Plugin required
|
||||
|
||||
@@ -25,7 +25,7 @@ Zalo ships as a plugin and is not bundled with the core install.
|
||||
- Or pick **Zalo** in onboarding and confirm the install prompt
|
||||
2. Set the token:
|
||||
- Env: `ZALO_BOT_TOKEN=...`
|
||||
- Or config: `channels.zalo.botToken: "..."`.
|
||||
- Or config: `channels.zalo.accounts.default.botToken: "..."`.
|
||||
3. Restart the gateway (or finish onboarding).
|
||||
4. DM access is pairing by default; approve the pairing code on first contact.
|
||||
|
||||
@@ -36,8 +36,12 @@ Minimal config:
|
||||
channels: {
|
||||
zalo: {
|
||||
enabled: true,
|
||||
botToken: "12345689:abc-xyz",
|
||||
dmPolicy: "pairing",
|
||||
accounts: {
|
||||
default: {
|
||||
botToken: "12345689:abc-xyz",
|
||||
dmPolicy: "pairing",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -48,10 +52,13 @@ Minimal config:
|
||||
Zalo is a Vietnam-focused messaging app; its Bot API lets the Gateway run a bot for 1:1 conversations.
|
||||
It is a good fit for support or notifications where you want deterministic routing back to Zalo.
|
||||
|
||||
This page reflects current OpenClaw behavior for **Zalo Bot Creator / Marketplace bots**.
|
||||
**Zalo Official Account (OA) bots** are a different Zalo product surface and may behave differently.
|
||||
|
||||
- A Zalo Bot API channel owned by the Gateway.
|
||||
- Deterministic routing: replies go back to Zalo; the model never chooses channels.
|
||||
- DMs share the agent's main session.
|
||||
- Groups are supported with policy controls (`groupPolicy` + `groupAllowFrom`) and default to fail-closed allowlist behavior.
|
||||
- The [Capabilities](#capabilities) section below shows current Marketplace-bot support.
|
||||
|
||||
## Setup (fast path)
|
||||
|
||||
@@ -59,7 +66,7 @@ It is a good fit for support or notifications where you want deterministic routi
|
||||
|
||||
1. Go to [https://bot.zaloplatforms.com](https://bot.zaloplatforms.com) and sign in.
|
||||
2. Create a new bot and configure its settings.
|
||||
3. Copy the bot token (format: `12345689:abc-xyz`).
|
||||
3. Copy the full bot token (typically `numeric_id:secret`). For Marketplace bots, the usable runtime token may appear in the bot's welcome message after creation.
|
||||
|
||||
### 2) Configure the token (env or config)
|
||||
|
||||
@@ -70,13 +77,19 @@ Example:
|
||||
channels: {
|
||||
zalo: {
|
||||
enabled: true,
|
||||
botToken: "12345689:abc-xyz",
|
||||
dmPolicy: "pairing",
|
||||
accounts: {
|
||||
default: {
|
||||
botToken: "12345689:abc-xyz",
|
||||
dmPolicy: "pairing",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
If you later move to a Zalo bot surface where groups are available, you can add group-specific config such as `groupPolicy` and `groupAllowFrom` explicitly. For current Marketplace-bot behavior, see [Capabilities](#capabilities).
|
||||
|
||||
Env option: `ZALO_BOT_TOKEN=...` (works for the default account only).
|
||||
|
||||
Multi-account support: use `channels.zalo.accounts` with per-account tokens and optional `name`.
|
||||
@@ -109,14 +122,23 @@ Multi-account support: use `channels.zalo.accounts` with per-account tokens and
|
||||
|
||||
## Access control (Groups)
|
||||
|
||||
For **Zalo Bot Creator / Marketplace bots**, group support was not available in practice because the bot could not be added to a group at all.
|
||||
|
||||
That means the group-related config keys below exist in the schema, but were not usable for Marketplace bots:
|
||||
|
||||
- `channels.zalo.groupPolicy` controls group inbound handling: `open | allowlist | disabled`.
|
||||
- Default behavior is fail-closed: `allowlist`.
|
||||
- `channels.zalo.groupAllowFrom` restricts which sender IDs can trigger the bot in groups.
|
||||
- If `groupAllowFrom` is unset, Zalo falls back to `allowFrom` for sender checks.
|
||||
- `groupPolicy: "disabled"` blocks all group messages.
|
||||
- `groupPolicy: "open"` allows any group member (mention-gated).
|
||||
- Runtime note: if `channels.zalo` is missing entirely, runtime still falls back to `groupPolicy="allowlist"` for safety.
|
||||
|
||||
The group policy values (when group access is available on your bot surface) are:
|
||||
|
||||
- `groupPolicy: "disabled"` — blocks all group messages.
|
||||
- `groupPolicy: "open"` — allows any group member (mention-gated).
|
||||
- `groupPolicy: "allowlist"` — fail-closed default; only allowed senders are accepted.
|
||||
|
||||
If you are using a different Zalo bot product surface and have verified working group behavior, document that separately rather than assuming it matches the Marketplace-bot flow.
|
||||
|
||||
## Long-polling vs webhook
|
||||
|
||||
- Default: long-polling (no public URL required).
|
||||
@@ -133,23 +155,36 @@ Multi-account support: use `channels.zalo.accounts` with per-account tokens and
|
||||
|
||||
## Supported message types
|
||||
|
||||
For a quick support snapshot, see [Capabilities](#capabilities). The notes below add detail where the behavior needs extra context.
|
||||
|
||||
- **Text messages**: Full support with 2000 character chunking.
|
||||
- **Image messages**: Download and process inbound images; send images via `sendPhoto`.
|
||||
- **Stickers**: Logged but not fully processed (no agent response).
|
||||
- **Unsupported types**: Logged (e.g., messages from protected users).
|
||||
- **Plain URLs in text**: Behave like normal text input.
|
||||
- **Link previews / rich link cards**: See the Marketplace-bot status in [Capabilities](#capabilities); they did not reliably trigger a reply.
|
||||
- **Image messages**: See the Marketplace-bot status in [Capabilities](#capabilities); inbound image handling was unreliable (typing indicator without a final reply).
|
||||
- **Stickers**: See the Marketplace-bot status in [Capabilities](#capabilities).
|
||||
- **Voice notes / audio files / video / generic file attachments**: See the Marketplace-bot status in [Capabilities](#capabilities).
|
||||
- **Unsupported types**: Logged (for example, messages from protected users).
|
||||
|
||||
## Capabilities
|
||||
|
||||
| Feature | Status |
|
||||
| --------------- | -------------------------------------------------------- |
|
||||
| Direct messages | ✅ Supported |
|
||||
| Groups | ⚠️ Supported with policy controls (allowlist by default) |
|
||||
| Media (images) | ✅ Supported |
|
||||
| Reactions | ❌ Not supported |
|
||||
| Threads | ❌ Not supported |
|
||||
| Polls | ❌ Not supported |
|
||||
| Native commands | ❌ Not supported |
|
||||
| Streaming | ⚠️ Blocked (2000 char limit) |
|
||||
This table summarizes current **Zalo Bot Creator / Marketplace bot** behavior in OpenClaw.
|
||||
|
||||
| Feature | Status |
|
||||
| --------------------------- | --------------------------------------- |
|
||||
| Direct messages | ✅ Supported |
|
||||
| Groups | ❌ Not available for Marketplace bots |
|
||||
| Media (inbound images) | ⚠️ Limited / verify in your environment |
|
||||
| Media (outbound images) | ⚠️ Not re-tested for Marketplace bots |
|
||||
| Plain URLs in text | ✅ Supported |
|
||||
| Link previews | ⚠️ Unreliable for Marketplace bots |
|
||||
| Reactions | ❌ Not supported |
|
||||
| Stickers | ⚠️ No agent reply for Marketplace bots |
|
||||
| Voice notes / audio / video | ⚠️ No agent reply for Marketplace bots |
|
||||
| File attachments | ⚠️ No agent reply for Marketplace bots |
|
||||
| Threads | ❌ Not supported |
|
||||
| Polls | ❌ Not supported |
|
||||
| Native commands | ❌ Not supported |
|
||||
| Streaming | ⚠️ Blocked (2000 char limit) |
|
||||
|
||||
## Delivery targets (CLI/cron)
|
||||
|
||||
@@ -175,6 +210,8 @@ Multi-account support: use `channels.zalo.accounts` with per-account tokens and
|
||||
|
||||
Full configuration: [Configuration](/gateway/configuration)
|
||||
|
||||
The flat top-level keys (`channels.zalo.botToken`, `channels.zalo.dmPolicy`, and similar) are a legacy single-account shorthand. Prefer `channels.zalo.accounts.<id>.*` for new configs. Both forms are still documented here because they exist in the schema.
|
||||
|
||||
Provider options:
|
||||
|
||||
- `channels.zalo.enabled`: enable/disable channel startup.
|
||||
@@ -182,7 +219,7 @@ Provider options:
|
||||
- `channels.zalo.tokenFile`: read token from a regular file path. Symlinks are rejected.
|
||||
- `channels.zalo.dmPolicy`: `pairing | allowlist | open | disabled` (default: pairing).
|
||||
- `channels.zalo.allowFrom`: DM allowlist (user IDs). `open` requires `"*"`. The wizard will ask for numeric IDs.
|
||||
- `channels.zalo.groupPolicy`: `open | allowlist | disabled` (default: allowlist).
|
||||
- `channels.zalo.groupPolicy`: `open | allowlist | disabled` (default: allowlist). Present in config; see [Capabilities](#capabilities) and [Access control (Groups)](#access-control-groups) for current Marketplace-bot behavior.
|
||||
- `channels.zalo.groupAllowFrom`: group sender allowlist (user IDs). Falls back to `allowFrom` when unset.
|
||||
- `channels.zalo.mediaMaxMb`: inbound/outbound media cap (MB, default 5).
|
||||
- `channels.zalo.webhookUrl`: enable webhook mode (HTTPS required).
|
||||
@@ -198,7 +235,7 @@ Multi-account options:
|
||||
- `channels.zalo.accounts.<id>.enabled`: enable/disable account.
|
||||
- `channels.zalo.accounts.<id>.dmPolicy`: per-account DM policy.
|
||||
- `channels.zalo.accounts.<id>.allowFrom`: per-account allowlist.
|
||||
- `channels.zalo.accounts.<id>.groupPolicy`: per-account group policy.
|
||||
- `channels.zalo.accounts.<id>.groupPolicy`: per-account group policy. Present in config; see [Capabilities](#capabilities) and [Access control (Groups)](#access-control-groups) for current Marketplace-bot behavior.
|
||||
- `channels.zalo.accounts.<id>.groupAllowFrom`: per-account group sender allowlist.
|
||||
- `channels.zalo.accounts.<id>.webhookUrl`: per-account webhook URL.
|
||||
- `channels.zalo.accounts.<id>.webhookSecret`: per-account webhook secret.
|
||||
|
||||
@@ -30,10 +30,11 @@ openclaw channels logs --channel all
|
||||
|
||||
```bash
|
||||
openclaw channels add --channel telegram --token <bot-token>
|
||||
openclaw channels add --channel nostr --private-key "$NOSTR_PRIVATE_KEY"
|
||||
openclaw channels remove --channel telegram --delete
|
||||
```
|
||||
|
||||
Tip: `openclaw channels add --help` shows per-channel flags (token, app token, signal-cli paths, etc).
|
||||
Tip: `openclaw channels add --help` shows per-channel flags (token, private key, app token, signal-cli paths, etc).
|
||||
|
||||
When you run `openclaw channels add` without flags, the interactive wizard can prompt:
|
||||
|
||||
|
||||
@@ -34,13 +34,15 @@ openclaw daemon uninstall
|
||||
|
||||
## Common options
|
||||
|
||||
- `status`: `--url`, `--token`, `--password`, `--timeout`, `--no-probe`, `--deep`, `--json`
|
||||
- `status`: `--url`, `--token`, `--password`, `--timeout`, `--no-probe`, `--require-rpc`, `--deep`, `--json`
|
||||
- `install`: `--port`, `--runtime <node|bun>`, `--token`, `--force`, `--json`
|
||||
- lifecycle (`uninstall|start|stop|restart`): `--json`
|
||||
|
||||
Notes:
|
||||
|
||||
- `status` resolves configured auth SecretRefs for probe auth when possible.
|
||||
- If a required auth SecretRef is unresolved in this command path, `daemon status --json` reports `rpc.authWarning` when probe connectivity/auth fails; pass `--token`/`--password` explicitly or resolve the secret source first.
|
||||
- If the probe succeeds, unresolved auth-ref warnings are suppressed to avoid false positives.
|
||||
- On Linux systemd installs, `status` token-drift checks include both `Environment=` and `EnvironmentFile=` unit sources.
|
||||
- When token auth requires a token and `gateway.auth.token` is SecretRef-managed, `install` validates that the SecretRef is resolvable but does not persist the resolved token into service environment metadata.
|
||||
- If token auth requires a token and the configured token SecretRef is unresolved, install fails closed.
|
||||
|
||||
@@ -31,6 +31,7 @@ Notes:
|
||||
- Doctor also scans `~/.openclaw/cron/jobs.json` (or `cron.store`) for legacy cron job shapes and can rewrite them in place before the scheduler has to auto-normalize them at runtime.
|
||||
- Doctor includes a memory-search readiness check and can recommend `openclaw configure --section model` when embedding credentials are missing.
|
||||
- If sandbox mode is enabled but Docker is unavailable, doctor reports a high-signal warning with remediation (`install Docker` or `openclaw config set agents.defaults.sandbox.mode off`).
|
||||
- If `gateway.auth.token`/`gateway.auth.password` are SecretRef-managed and unavailable in the current command path, doctor reports a read-only warning and does not write plaintext fallback credentials.
|
||||
|
||||
## macOS: `launchctl` env overrides
|
||||
|
||||
|
||||
@@ -111,7 +111,8 @@ Options:
|
||||
Notes:
|
||||
|
||||
- `gateway status` resolves configured auth SecretRefs for probe auth when possible.
|
||||
- If a required auth SecretRef is unresolved in this command path, probe auth can fail; pass `--token`/`--password` explicitly or resolve the secret source first.
|
||||
- If a required auth SecretRef is unresolved in this command path, `gateway status --json` reports `rpc.authWarning` when probe connectivity/auth fails; pass `--token`/`--password` explicitly or resolve the secret source first.
|
||||
- If the probe succeeds, unresolved auth-ref warnings are suppressed to avoid false positives.
|
||||
- Use `--require-rpc` in scripts and automation when a listening service is not enough and you need the Gateway RPC itself to be healthy.
|
||||
- On Linux systemd installs, service auth drift checks read both `Environment=` and `EnvironmentFile=` values from the unit (including `%h`, quoted paths, multiple files, and optional `-` files).
|
||||
|
||||
|
||||
@@ -676,7 +676,7 @@ Surfaces:
|
||||
Notes:
|
||||
|
||||
- Data comes directly from provider usage endpoints (no estimates).
|
||||
- Providers: Anthropic, GitHub Copilot, OpenAI Codex OAuth, plus Gemini CLI/Antigravity when those provider plugins are enabled.
|
||||
- Providers: Anthropic, GitHub Copilot, OpenAI Codex OAuth, plus Gemini CLI via the bundled `google` plugin and Antigravity where configured.
|
||||
- If no matching credentials exist, usage is hidden.
|
||||
- Details: see [Usage tracking](/concepts/usage-tracking).
|
||||
|
||||
@@ -783,6 +783,7 @@ Notes:
|
||||
- `gateway status` supports `--no-probe`, `--deep`, `--require-rpc`, and `--json` for scripting.
|
||||
- `gateway status` also surfaces legacy or extra gateway services when it can detect them (`--deep` adds system-level scans). Profile-named OpenClaw services are treated as first-class and aren't flagged as "extra".
|
||||
- `gateway status` prints which config path the CLI uses vs which config the service likely uses (service env), plus the resolved probe target URL.
|
||||
- If gateway auth SecretRefs are unresolved in the current command path, `gateway status --json` reports `rpc.authWarning` only when probe connectivity/auth fails (warnings are suppressed when probe succeeds).
|
||||
- On Linux systemd installs, status token-drift checks include both `Environment=` and `EnvironmentFile=` unit sources.
|
||||
- `gateway install|uninstall|start|stop|restart` support `--json` for scripting (default output stays human-friendly).
|
||||
- `gateway install` defaults to Node runtime; bun is **not recommended** (WhatsApp/Telegram bugs).
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
---
|
||||
summary: "CLI reference for `openclaw plugins` (list, install, uninstall, enable/disable, doctor)"
|
||||
read_when:
|
||||
- You want to install or manage in-process Gateway plugins
|
||||
- You want to install or manage Gateway plugins or compatible bundles
|
||||
- You want to debug plugin load failures
|
||||
title: "plugins"
|
||||
---
|
||||
|
||||
# `openclaw plugins`
|
||||
|
||||
Manage Gateway plugins/extensions (loaded in-process).
|
||||
Manage Gateway plugins/extensions and compatible bundles.
|
||||
|
||||
Related:
|
||||
|
||||
- Plugin system: [Plugins](/tools/plugin)
|
||||
- Bundle compatibility: [Plugin bundles](/plugins/bundles)
|
||||
- Plugin manifest + schema: [Plugin manifest](/plugins/manifest)
|
||||
- Security hardening: [Security](/gateway/security)
|
||||
|
||||
@@ -32,9 +33,13 @@ openclaw plugins update --all
|
||||
Bundled plugins ship with OpenClaw but start disabled. Use `plugins enable` to
|
||||
activate them.
|
||||
|
||||
All plugins must ship a `openclaw.plugin.json` file with an inline JSON Schema
|
||||
(`configSchema`, even if empty). Missing/invalid manifests or schemas prevent
|
||||
the plugin from loading and fail config validation.
|
||||
Native OpenClaw plugins must ship `openclaw.plugin.json` with an inline JSON
|
||||
Schema (`configSchema`, even if empty). Compatible bundles use their own bundle
|
||||
manifests instead.
|
||||
|
||||
`plugins list` shows `Format: openclaw` or `Format: bundle`. Verbose list/info
|
||||
output also shows the bundle subtype (`codex`, `claude`, or `cursor`) plus detected bundle
|
||||
capabilities.
|
||||
|
||||
### Install
|
||||
|
||||
@@ -60,6 +65,20 @@ name, use an explicit scoped spec (for example `@scope/diffs`).
|
||||
|
||||
Supported archives: `.zip`, `.tgz`, `.tar.gz`, `.tar`.
|
||||
|
||||
For local paths and archives, OpenClaw auto-detects:
|
||||
|
||||
- native OpenClaw plugins (`openclaw.plugin.json`)
|
||||
- Codex-compatible bundles (`.codex-plugin/plugin.json`)
|
||||
- Claude-compatible bundles (`.claude-plugin/plugin.json` or the default Claude
|
||||
component layout)
|
||||
- Cursor-compatible bundles (`.cursor-plugin/plugin.json`)
|
||||
|
||||
Compatible bundles install into the normal extensions root and participate in
|
||||
the same list/info/enable/disable flow. Today, bundle skills, Claude
|
||||
command-skills, Claude `settings.json` defaults, Cursor command-skills, and compatible Codex hook
|
||||
directories are supported; other detected bundle capabilities are shown in
|
||||
diagnostics/info but are not yet wired into runtime execution.
|
||||
|
||||
Use `--link` to avoid copying a local directory (adds to `plugins.load.paths`):
|
||||
|
||||
```bash
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
---
|
||||
title: Sandbox CLI
|
||||
summary: "Manage sandbox containers and inspect effective sandbox policy"
|
||||
read_when: "You are managing sandbox containers or debugging sandbox/tool-policy behavior."
|
||||
summary: "Manage sandbox runtimes and inspect effective sandbox policy"
|
||||
read_when: "You are managing sandbox runtimes or debugging sandbox/tool-policy behavior."
|
||||
status: active
|
||||
---
|
||||
|
||||
# Sandbox CLI
|
||||
|
||||
Manage Docker-based sandbox containers for isolated agent execution.
|
||||
Manage sandbox runtimes for isolated agent execution.
|
||||
|
||||
## Overview
|
||||
|
||||
OpenClaw can run agents in isolated Docker containers for security. The `sandbox` commands help you manage these containers, especially after updates or configuration changes.
|
||||
OpenClaw can run agents in isolated sandbox runtimes for security. The `sandbox` commands help you inspect and recreate those runtimes after updates or configuration changes.
|
||||
|
||||
Today that usually means:
|
||||
|
||||
- Docker sandbox containers
|
||||
- OpenShell sandbox runtimes when `agents.defaults.sandbox.backend = "openshell"`
|
||||
|
||||
## Commands
|
||||
|
||||
@@ -28,7 +33,7 @@ openclaw sandbox explain --json
|
||||
|
||||
### `openclaw sandbox list`
|
||||
|
||||
List all sandbox containers with their status and configuration.
|
||||
List all sandbox runtimes with their status and configuration.
|
||||
|
||||
```bash
|
||||
openclaw sandbox list
|
||||
@@ -38,15 +43,16 @@ openclaw sandbox list --json # JSON output
|
||||
|
||||
**Output includes:**
|
||||
|
||||
- Container name and status (running/stopped)
|
||||
- Docker image and whether it matches config
|
||||
- Runtime name and status
|
||||
- Backend (`docker`, `openshell`, etc.)
|
||||
- Config label and whether it matches current config
|
||||
- Age (time since creation)
|
||||
- Idle time (time since last use)
|
||||
- Associated session/agent
|
||||
|
||||
### `openclaw sandbox recreate`
|
||||
|
||||
Remove sandbox containers to force recreation with updated images/config.
|
||||
Remove sandbox runtimes to force recreation with updated config.
|
||||
|
||||
```bash
|
||||
openclaw sandbox recreate --all # Recreate all containers
|
||||
@@ -64,11 +70,11 @@ openclaw sandbox recreate --all --force # Skip confirmation
|
||||
- `--browser`: Only recreate browser containers
|
||||
- `--force`: Skip confirmation prompt
|
||||
|
||||
**Important:** Containers are automatically recreated when the agent is next used.
|
||||
**Important:** Runtimes are automatically recreated when the agent is next used.
|
||||
|
||||
## Use Cases
|
||||
|
||||
### After updating Docker images
|
||||
### After updating a Docker image
|
||||
|
||||
```bash
|
||||
# Pull new image
|
||||
@@ -91,6 +97,21 @@ openclaw sandbox recreate --all
|
||||
openclaw sandbox recreate --all
|
||||
```
|
||||
|
||||
### After changing OpenShell source, policy, or mode
|
||||
|
||||
```bash
|
||||
# Edit config:
|
||||
# - agents.defaults.sandbox.backend
|
||||
# - plugins.entries.openshell.config.from
|
||||
# - plugins.entries.openshell.config.mode
|
||||
# - plugins.entries.openshell.config.policy
|
||||
|
||||
openclaw sandbox recreate --all
|
||||
```
|
||||
|
||||
For OpenShell `remote` mode, recreate deletes the canonical remote workspace
|
||||
for that scope. The next run seeds it again from the local workspace.
|
||||
|
||||
### After changing setupCommand
|
||||
|
||||
```bash
|
||||
@@ -108,16 +129,16 @@ openclaw sandbox recreate --agent alfred
|
||||
|
||||
## Why is this needed?
|
||||
|
||||
**Problem:** When you update sandbox Docker images or configuration:
|
||||
**Problem:** When you update sandbox configuration:
|
||||
|
||||
- Existing containers continue running with old settings
|
||||
- Containers are only pruned after 24h of inactivity
|
||||
- Regularly-used agents keep old containers running indefinitely
|
||||
- Existing runtimes continue running with old settings
|
||||
- Runtimes are only pruned after 24h of inactivity
|
||||
- Regularly-used agents keep old runtimes alive indefinitely
|
||||
|
||||
**Solution:** Use `openclaw sandbox recreate` to force removal of old containers. They'll be recreated automatically with current settings when next needed.
|
||||
**Solution:** Use `openclaw sandbox recreate` to force removal of old runtimes. They'll be recreated automatically with current settings when next needed.
|
||||
|
||||
Tip: prefer `openclaw sandbox recreate` over manual `docker rm`. It uses the
|
||||
Gateway’s container naming and avoids mismatches when scope/session keys change.
|
||||
Tip: prefer `openclaw sandbox recreate` over manual backend-specific cleanup.
|
||||
It uses the Gateway’s runtime registry and avoids mismatches when scope/session keys change.
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -129,6 +150,7 @@ Sandbox settings live in `~/.openclaw/openclaw.json` under `agents.defaults.sand
|
||||
"defaults": {
|
||||
"sandbox": {
|
||||
"mode": "all", // off, non-main, all
|
||||
"backend": "docker", // docker, openshell
|
||||
"scope": "agent", // session, agent, shared
|
||||
"docker": {
|
||||
"image": "openclaw-sandbox:bookworm-slim",
|
||||
|
||||
@@ -19,6 +19,8 @@ Related:
|
||||
```bash
|
||||
openclaw security audit
|
||||
openclaw security audit --deep
|
||||
openclaw security audit --deep --password <password>
|
||||
openclaw security audit --deep --token <token>
|
||||
openclaw security audit --fix
|
||||
openclaw security audit --json
|
||||
```
|
||||
@@ -40,6 +42,12 @@ It warns when `gateway.auth.mode="none"` leaves Gateway HTTP APIs reachable with
|
||||
Settings prefixed with `dangerous`/`dangerously` are explicit break-glass operator overrides; enabling one is not, by itself, a security vulnerability report.
|
||||
For the complete dangerous-parameter inventory, see the "Insecure or dangerous flags summary" section in [Security](/gateway/security).
|
||||
|
||||
SecretRef behavior:
|
||||
|
||||
- `security audit` resolves supported SecretRefs in read-only mode for its targeted paths.
|
||||
- If a SecretRef is unavailable in the current command path, audit continues and reports `secretDiagnostics` (instead of crashing).
|
||||
- `--token` and `--password` only override deep-probe auth for that command invocation; they do not rewrite config or SecretRef mappings.
|
||||
|
||||
## JSON output
|
||||
|
||||
Use `--json` for CI/policy checks:
|
||||
|
||||
@@ -26,3 +26,4 @@ Notes:
|
||||
- Update info surfaces in the Overview; if an update is available, status prints a hint to run `openclaw update` (see [Updating](/install/updating)).
|
||||
- Read-only status surfaces (`status`, `status --json`, `status --all`) resolve supported SecretRefs for their targeted config paths when possible.
|
||||
- If a supported channel SecretRef is configured but unavailable in the current command path, status stays read-only and reports degraded output instead of crashing. Human output shows warnings such as “configured token unavailable in this command path”, and JSON output includes `secretDiagnostics`.
|
||||
- When command-local SecretRef resolution succeeds, status prefers the resolved snapshot and clears transient “secret unavailable” channel markers from the final output.
|
||||
|
||||
@@ -21,6 +21,7 @@ openclaw update wizard
|
||||
openclaw update --channel beta
|
||||
openclaw update --channel dev
|
||||
openclaw update --tag beta
|
||||
openclaw update --tag main
|
||||
openclaw update --dry-run
|
||||
openclaw update --no-restart
|
||||
openclaw update --json
|
||||
@@ -31,7 +32,7 @@ openclaw --update
|
||||
|
||||
- `--no-restart`: skip restarting the Gateway service after a successful update.
|
||||
- `--channel <stable|beta|dev>`: set the update channel (git + npm; persisted in config).
|
||||
- `--tag <dist-tag|version>`: override the npm dist-tag or version for this update only.
|
||||
- `--tag <dist-tag|version|spec>`: override the package target for this update only. For package installs, `main` maps to `github:openclaw/openclaw#main`.
|
||||
- `--dry-run`: preview planned update actions (channel/tag/target/restart flow) without writing config, installing, syncing plugins, or restarting.
|
||||
- `--json`: print machine-readable `UpdateRunResult` JSON.
|
||||
- `--timeout <seconds>`: per-step timeout (default is 1200s).
|
||||
|
||||
@@ -16,6 +16,79 @@ For model selection rules, see [/concepts/models](/concepts/models).
|
||||
- Model refs use `provider/model` (example: `opencode/claude-opus-4-6`).
|
||||
- If you set `agents.defaults.models`, it becomes the allowlist.
|
||||
- CLI helpers: `openclaw onboard`, `openclaw models list`, `openclaw models set <provider/model>`.
|
||||
- Provider plugins can inject model catalogs via `registerProvider({ catalog })`;
|
||||
OpenClaw merges that output into `models.providers` before writing
|
||||
`models.json`.
|
||||
- Provider manifests can declare `providerAuthEnvVars` so generic env-based
|
||||
auth probes do not need to load plugin runtime.
|
||||
- Provider plugins can also own provider runtime behavior via
|
||||
`resolveDynamicModel`, `prepareDynamicModel`, `normalizeResolvedModel`,
|
||||
`capabilities`, `prepareExtraParams`, `wrapStreamFn`,
|
||||
`isCacheTtlEligible`, `buildMissingAuthMessage`,
|
||||
`suppressBuiltInModel`, `augmentModelCatalog`, `prepareRuntimeAuth`,
|
||||
`resolveUsageAuth`, and `fetchUsageSnapshot`.
|
||||
|
||||
## Plugin-owned provider behavior
|
||||
|
||||
Provider plugins can now own most provider-specific logic while OpenClaw keeps
|
||||
the generic inference loop.
|
||||
|
||||
Typical split:
|
||||
|
||||
- `catalog`: provider appears in `models.providers`
|
||||
- `resolveDynamicModel`: provider accepts model ids not present in the local
|
||||
static catalog yet
|
||||
- `prepareDynamicModel`: provider needs a metadata refresh before retrying
|
||||
dynamic resolution
|
||||
- `normalizeResolvedModel`: provider needs transport or base URL rewrites
|
||||
- `capabilities`: provider publishes transcript/tooling/provider-family quirks
|
||||
- `prepareExtraParams`: provider defaults or normalizes per-model request params
|
||||
- `wrapStreamFn`: provider applies request headers/body/model compat wrappers
|
||||
- `isCacheTtlEligible`: provider decides which upstream model ids support prompt-cache TTL
|
||||
- `buildMissingAuthMessage`: provider replaces the generic auth-store error
|
||||
with a provider-specific recovery hint
|
||||
- `suppressBuiltInModel`: provider hides stale upstream rows and can return a
|
||||
vendor-owned error for direct resolution failures
|
||||
- `augmentModelCatalog`: provider appends synthetic/final catalog rows after
|
||||
discovery and config merging
|
||||
- `prepareRuntimeAuth`: provider turns a configured credential into a short
|
||||
lived runtime token
|
||||
- `resolveUsageAuth`: provider resolves usage/quota credentials for `/usage`
|
||||
and related status/reporting surfaces
|
||||
- `fetchUsageSnapshot`: provider owns the usage endpoint fetch/parsing while
|
||||
core still owns the summary shell and formatting
|
||||
|
||||
Current bundled examples:
|
||||
|
||||
- `anthropic`: Claude 4.6 forward-compat fallback, usage endpoint fetching,
|
||||
and cache-TTL/provider-family metadata
|
||||
- `openrouter`: pass-through model ids, request wrappers, provider capability
|
||||
hints, and cache-TTL policy
|
||||
- `github-copilot`: forward-compat model fallback, Claude-thinking transcript
|
||||
hints, runtime token exchange, and usage endpoint fetching
|
||||
- `openai`: GPT-5.4 forward-compat fallback, direct OpenAI transport
|
||||
normalization, Codex-aware missing-auth hints, Spark suppression, synthetic
|
||||
OpenAI/Codex catalog rows, and provider-family metadata
|
||||
- `google-gemini-cli`: Gemini 3.1 forward-compat fallback plus usage-token
|
||||
parsing and quota endpoint fetching for usage surfaces
|
||||
- `moonshot`: shared transport, plugin-owned thinking payload normalization
|
||||
- `kilocode`: shared transport, plugin-owned request headers, reasoning payload
|
||||
normalization, Gemini transcript hints, and cache-TTL policy
|
||||
- `zai`: GLM-5 forward-compat fallback, `tool_stream` defaults, cache-TTL
|
||||
policy, and usage auth + quota fetching
|
||||
- `mistral`, `opencode`, and `opencode-go`: plugin-owned capability metadata
|
||||
- `byteplus`, `cloudflare-ai-gateway`, `huggingface`, `kimi-coding`,
|
||||
`minimax-portal`, `modelstudio`, `nvidia`, `qianfan`, `qwen-portal`,
|
||||
`synthetic`, `together`, `venice`, `vercel-ai-gateway`, and `volcengine`:
|
||||
plugin-owned catalogs only
|
||||
- `minimax` and `xiaomi`: plugin-owned catalogs plus usage auth/snapshot logic
|
||||
|
||||
The bundled `openai` plugin now owns both provider ids: `openai` and
|
||||
`openai-codex`.
|
||||
|
||||
That covers providers that still fit OpenClaw's normal transports. A provider
|
||||
that needs a totally custom request executor is a separate, deeper extension
|
||||
surface.
|
||||
|
||||
## API key rotation
|
||||
|
||||
@@ -114,16 +187,13 @@ OpenClaw ships with the pi‑ai catalog. These providers require **no**
|
||||
- Compatibility: legacy OpenClaw config using `google/gemini-3.1-flash-preview` is normalized to `google/gemini-3-flash-preview`
|
||||
- CLI: `openclaw onboard --auth-choice gemini-api-key`
|
||||
|
||||
### Google Vertex, Antigravity, and Gemini CLI
|
||||
### Google Vertex and Gemini CLI
|
||||
|
||||
- Providers: `google-vertex`, `google-antigravity`, `google-gemini-cli`
|
||||
- Auth: Vertex uses gcloud ADC; Antigravity/Gemini CLI use their respective auth flows
|
||||
- Caution: Antigravity and Gemini CLI OAuth in OpenClaw are unofficial integrations. Some users have reported Google account restrictions after using third-party clients. Review Google terms and use a non-critical account if you choose to proceed.
|
||||
- Antigravity OAuth is shipped as a bundled plugin (`google-antigravity-auth`, disabled by default).
|
||||
- Enable: `openclaw plugins enable google-antigravity-auth`
|
||||
- Login: `openclaw models auth login --provider google-antigravity --set-default`
|
||||
- Gemini CLI OAuth is shipped as a bundled plugin (`google-gemini-cli-auth`, disabled by default).
|
||||
- Enable: `openclaw plugins enable google-gemini-cli-auth`
|
||||
- Providers: `google-vertex`, `google-gemini-cli`
|
||||
- Auth: Vertex uses gcloud ADC; Gemini CLI uses its OAuth flow
|
||||
- Caution: Gemini CLI OAuth in OpenClaw is an unofficial integration. Some users have reported Google account restrictions after using third-party clients. Review Google terms and use a non-critical account if you choose to proceed.
|
||||
- Gemini CLI OAuth is shipped as part of the bundled `google` plugin.
|
||||
- Enable: `openclaw plugins enable google`
|
||||
- Login: `openclaw models auth login --provider google-gemini-cli --set-default`
|
||||
- Note: you do **not** paste a client id or secret into `openclaw.json`. The CLI login flow stores
|
||||
tokens in auth profiles on the gateway host.
|
||||
@@ -154,12 +224,26 @@ OpenClaw ships with the pi‑ai catalog. These providers require **no**
|
||||
|
||||
See [/providers/kilocode](/providers/kilocode) for setup details.
|
||||
|
||||
### Other built-in providers
|
||||
### Other bundled provider plugins
|
||||
|
||||
- OpenRouter: `openrouter` (`OPENROUTER_API_KEY`)
|
||||
- Example model: `openrouter/anthropic/claude-sonnet-4-5`
|
||||
- Kilo Gateway: `kilocode` (`KILOCODE_API_KEY`)
|
||||
- Example model: `kilocode/anthropic/claude-opus-4.6`
|
||||
- MiniMax: `minimax` (`MINIMAX_API_KEY`)
|
||||
- Moonshot: `moonshot` (`MOONSHOT_API_KEY`)
|
||||
- Kimi Coding: `kimi-coding` (`KIMI_API_KEY` or `KIMICODE_API_KEY`)
|
||||
- Qianfan: `qianfan` (`QIANFAN_API_KEY`)
|
||||
- Model Studio: `modelstudio` (`MODELSTUDIO_API_KEY`)
|
||||
- NVIDIA: `nvidia` (`NVIDIA_API_KEY`)
|
||||
- Together: `together` (`TOGETHER_API_KEY`)
|
||||
- Venice: `venice` (`VENICE_API_KEY`)
|
||||
- Xiaomi: `xiaomi` (`XIAOMI_API_KEY`)
|
||||
- Vercel AI Gateway: `vercel-ai-gateway` (`AI_GATEWAY_API_KEY`)
|
||||
- Hugging Face Inference: `huggingface` (`HUGGINGFACE_HUB_TOKEN` or `HF_TOKEN`)
|
||||
- Cloudflare AI Gateway: `cloudflare-ai-gateway` (`CLOUDFLARE_AI_GATEWAY_API_KEY`)
|
||||
- Volcengine: `volcengine` (`VOLCANO_ENGINE_API_KEY`)
|
||||
- BytePlus: `byteplus` (`BYTEPLUS_API_KEY`)
|
||||
- xAI: `xai` (`XAI_API_KEY`)
|
||||
- Mistral: `mistral` (`MISTRAL_API_KEY`)
|
||||
- Example model: `mistral/mistral-large-latest`
|
||||
@@ -169,13 +253,17 @@ See [/providers/kilocode](/providers/kilocode) for setup details.
|
||||
- GLM models on Cerebras use ids `zai-glm-4.7` and `zai-glm-4.6`.
|
||||
- OpenAI-compatible base URL: `https://api.cerebras.ai/v1`.
|
||||
- GitHub Copilot: `github-copilot` (`COPILOT_GITHUB_TOKEN` / `GH_TOKEN` / `GITHUB_TOKEN`)
|
||||
- Hugging Face Inference: `huggingface` (`HUGGINGFACE_HUB_TOKEN` or `HF_TOKEN`) — OpenAI-compatible router; example model: `huggingface/deepseek-ai/DeepSeek-R1`; CLI: `openclaw onboard --auth-choice huggingface-api-key`. See [Hugging Face (Inference)](/providers/huggingface).
|
||||
- Hugging Face Inference example model: `huggingface/deepseek-ai/DeepSeek-R1`; CLI: `openclaw onboard --auth-choice huggingface-api-key`. See [Hugging Face (Inference)](/providers/huggingface).
|
||||
|
||||
## Providers via `models.providers` (custom/base URL)
|
||||
|
||||
Use `models.providers` (or `models.json`) to add **custom** providers or
|
||||
OpenAI/Anthropic‑compatible proxies.
|
||||
|
||||
Many of the bundled provider plugins below already publish a default catalog.
|
||||
Use explicit `models.providers.<id>` entries only when you want to override the
|
||||
default base URL, headers, or model list.
|
||||
|
||||
### Moonshot AI (Kimi)
|
||||
|
||||
Moonshot uses OpenAI-compatible endpoints, so configure it as a custom provider:
|
||||
@@ -235,10 +323,9 @@ Kimi Coding uses Moonshot AI's Anthropic-compatible endpoint:
|
||||
### Qwen OAuth (free tier)
|
||||
|
||||
Qwen provides OAuth access to Qwen Coder + Vision via a device-code flow.
|
||||
Enable the bundled plugin, then log in:
|
||||
The bundled provider plugin is enabled by default, so just log in:
|
||||
|
||||
```bash
|
||||
openclaw plugins enable qwen-portal-auth
|
||||
openclaw models auth login --provider qwen-portal --set-default
|
||||
```
|
||||
|
||||
|
||||
@@ -469,7 +469,7 @@
|
||||
},
|
||||
{
|
||||
"source": "/mac/release",
|
||||
"destination": "/platforms/mac/release"
|
||||
"destination": "/reference/RELEASING"
|
||||
},
|
||||
{
|
||||
"source": "/mac/remote",
|
||||
@@ -1046,6 +1046,7 @@
|
||||
"group": "Extensions",
|
||||
"pages": [
|
||||
"plugins/community",
|
||||
"plugins/bundles",
|
||||
"plugins/voice-call",
|
||||
"plugins/zalouser",
|
||||
"plugins/manifest",
|
||||
@@ -1166,7 +1167,6 @@
|
||||
"platforms/mac/permissions",
|
||||
"platforms/mac/remote",
|
||||
"platforms/mac/signing",
|
||||
"platforms/mac/release",
|
||||
"platforms/mac/bundled-gateway",
|
||||
"platforms/mac/xpc",
|
||||
"platforms/mac/skills",
|
||||
@@ -1351,7 +1351,7 @@
|
||||
"pages": ["reference/credits"]
|
||||
},
|
||||
{
|
||||
"group": "Release notes",
|
||||
"group": "Release policy",
|
||||
"pages": ["reference/RELEASING", "reference/test"]
|
||||
},
|
||||
{
|
||||
@@ -1750,7 +1750,6 @@
|
||||
"zh-CN/platforms/mac/permissions",
|
||||
"zh-CN/platforms/mac/remote",
|
||||
"zh-CN/platforms/mac/signing",
|
||||
"zh-CN/platforms/mac/release",
|
||||
"zh-CN/platforms/mac/bundled-gateway",
|
||||
"zh-CN/platforms/mac/xpc",
|
||||
"zh-CN/platforms/mac/skills",
|
||||
@@ -1933,7 +1932,7 @@
|
||||
"pages": ["zh-CN/reference/credits"]
|
||||
},
|
||||
{
|
||||
"group": "发布说明",
|
||||
"group": "发布策略",
|
||||
"pages": ["zh-CN/reference/RELEASING", "zh-CN/reference/test"]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -655,12 +655,12 @@ See the full channel index: [Channels](/channels).
|
||||
|
||||
### Group chat mention gating
|
||||
|
||||
Group messages default to **require mention** (metadata mention or regex patterns). Applies to WhatsApp, Telegram, Discord, Google Chat, and iMessage group chats.
|
||||
Group messages default to **require mention** (metadata mention or safe regex patterns). Applies to WhatsApp, Telegram, Discord, Google Chat, and iMessage group chats.
|
||||
|
||||
**Mention types:**
|
||||
|
||||
- **Metadata mentions**: Native platform @-mentions. Ignored in WhatsApp self-chat mode.
|
||||
- **Text patterns**: Regex patterns in `agents.list[].groupChat.mentionPatterns`. Always checked.
|
||||
- **Text patterns**: Safe regex patterns in `agents.list[].groupChat.mentionPatterns`. Invalid patterns and unsafe nested repetition are ignored.
|
||||
- Mention gating is enforced only when detection is possible (native mentions or at least one pattern).
|
||||
|
||||
```json5
|
||||
@@ -1005,6 +1005,7 @@ Periodic heartbeat runs.
|
||||
defaults: {
|
||||
compaction: {
|
||||
mode: "safeguard", // default | safeguard
|
||||
timeoutSeconds: 900,
|
||||
reserveTokensFloor: 24000,
|
||||
identifierPolicy: "strict", // strict | off | custom
|
||||
identifierInstructions: "Preserve deployment IDs, ticket IDs, and host:port pairs exactly.", // used when identifierPolicy=custom
|
||||
@@ -1023,6 +1024,7 @@ Periodic heartbeat runs.
|
||||
```
|
||||
|
||||
- `mode`: `default` or `safeguard` (chunked summarization for long histories). See [Compaction](/concepts/compaction).
|
||||
- `timeoutSeconds`: maximum seconds allowed for a single compaction operation before OpenClaw aborts it. Default: `900`.
|
||||
- `identifierPolicy`: `strict` (default), `off`, or `custom`. `strict` prepends built-in opaque identifier retention guidance during compaction summarization.
|
||||
- `identifierInstructions`: optional custom identifier-preservation text used when `identifierPolicy=custom`.
|
||||
- `postCompactionSections`: optional AGENTS.md H2/H3 section names to re-inject after compaction. Defaults to `["Session Startup", "Red Lines"]`; set `[]` to disable reinjection. When unset or explicitly set to that default pair, older `Every Session`/`Safety` headings are also accepted as a legacy fallback.
|
||||
@@ -1115,7 +1117,7 @@ See [Typing Indicators](/concepts/typing-indicators).
|
||||
|
||||
### `agents.defaults.sandbox`
|
||||
|
||||
Optional **Docker sandboxing** for the embedded agent. See [Sandboxing](/gateway/sandboxing) for the full guide.
|
||||
Optional sandboxing for the embedded agent. See [Sandboxing](/gateway/sandboxing) for the full guide.
|
||||
|
||||
```json5
|
||||
{
|
||||
@@ -1123,6 +1125,7 @@ Optional **Docker sandboxing** for the embedded agent. See [Sandboxing](/gateway
|
||||
defaults: {
|
||||
sandbox: {
|
||||
mode: "non-main", // off | non-main | all
|
||||
backend: "docker", // docker | openshell
|
||||
scope: "agent", // session | agent | shared
|
||||
workspaceAccess: "none", // none | ro | rw
|
||||
workspaceRoot: "~/.openclaw/sandboxes",
|
||||
@@ -1197,6 +1200,14 @@ Optional **Docker sandboxing** for the embedded agent. See [Sandboxing](/gateway
|
||||
|
||||
<Accordion title="Sandbox details">
|
||||
|
||||
**Backend:**
|
||||
|
||||
- `docker`: local Docker runtime (default)
|
||||
- `openshell`: OpenShell runtime
|
||||
|
||||
When `backend: "openshell"` is selected, runtime-specific settings move to
|
||||
`plugins.entries.openshell.config`.
|
||||
|
||||
**Workspace access:**
|
||||
|
||||
- `none`: per-scope sandbox workspace under `~/.openclaw/sandboxes`
|
||||
@@ -1209,6 +1220,39 @@ Optional **Docker sandboxing** for the embedded agent. See [Sandboxing](/gateway
|
||||
- `agent`: one container + workspace per agent (default)
|
||||
- `shared`: shared container and workspace (no cross-session isolation)
|
||||
|
||||
**OpenShell plugin config:**
|
||||
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
entries: {
|
||||
openshell: {
|
||||
enabled: true,
|
||||
config: {
|
||||
mode: "mirror", // mirror | remote
|
||||
from: "openclaw",
|
||||
remoteWorkspaceDir: "/sandbox",
|
||||
remoteAgentWorkspaceDir: "/agent",
|
||||
gateway: "lab", // optional
|
||||
gatewayEndpoint: "https://lab.example", // optional
|
||||
policy: "strict", // optional OpenShell policy id
|
||||
providers: ["openai"], // optional
|
||||
autoProviders: true,
|
||||
timeoutSeconds: 120,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
**OpenShell mode:**
|
||||
|
||||
- `mirror`: seed remote from local before exec, sync back after exec; local workspace stays canonical
|
||||
- `remote`: seed remote once when the sandbox is created, then keep the remote workspace canonical
|
||||
|
||||
In `remote` mode, host-local edits made outside OpenClaw are not synced into the sandbox automatically after the seed step.
|
||||
|
||||
**`setupCommand`** runs once after container creation (via `sh -lc`). Needs network egress, writable root, root user.
|
||||
|
||||
**Containers default to `network: "none"`** — set to `"bridge"` (or a custom bridge network) if the agent needs outbound access.
|
||||
@@ -1258,6 +1302,8 @@ noVNC observer access uses VNC auth by default and OpenClaw emits a short-lived
|
||||
|
||||
</Accordion>
|
||||
|
||||
Browser sandboxing and `sandbox.docker.binds` are currently Docker-only.
|
||||
|
||||
Build images:
|
||||
|
||||
```bash
|
||||
@@ -2321,12 +2367,14 @@ See [Local Models](/gateway/local-models). TL;DR: run MiniMax M2.5 via LM Studio
|
||||
```
|
||||
|
||||
- Loaded from `~/.openclaw/extensions`, `<workspace>/.openclaw/extensions`, plus `plugins.load.paths`.
|
||||
- Discovery accepts native OpenClaw plugins plus compatible Codex bundles and Claude bundles, including manifestless Claude default-layout bundles.
|
||||
- **Config changes require a gateway restart.**
|
||||
- `allow`: optional allowlist (only listed plugins load). `deny` wins.
|
||||
- `plugins.entries.<id>.apiKey`: plugin-level API key convenience field (when supported by the plugin).
|
||||
- `plugins.entries.<id>.env`: plugin-scoped env var map.
|
||||
- `plugins.entries.<id>.hooks.allowPromptInjection`: when `false`, core blocks `before_prompt_build` and ignores prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride`.
|
||||
- `plugins.entries.<id>.config`: plugin-defined config object (validated by plugin schema).
|
||||
- `plugins.entries.<id>.hooks.allowPromptInjection`: when `false`, core blocks `before_prompt_build` and ignores prompt-mutating fields from legacy `before_agent_start`, while preserving legacy `modelOverride` and `providerOverride`. Applies to native plugin hooks and supported bundle-provided hook directories.
|
||||
- `plugins.entries.<id>.config`: plugin-defined config object (validated by native OpenClaw plugin schema when available).
|
||||
- Enabled Claude bundle plugins can also contribute embedded Pi defaults from `settings.json`; OpenClaw applies those as sanitized agent settings, not as raw OpenClaw config patches.
|
||||
- `plugins.slots.memory`: pick the active memory plugin id, or `"none"` to disable memory plugins.
|
||||
- `plugins.slots.contextEngine`: pick the active context engine plugin id; defaults to `"legacy"` unless you install and select another engine.
|
||||
- `plugins.installs`: CLI-managed install metadata used by `openclaw plugins update`.
|
||||
@@ -2370,6 +2418,7 @@ See [Plugins](/tools/plugin).
|
||||
- `evaluateEnabled: false` disables `act:evaluate` and `wait --fn`.
|
||||
- `ssrfPolicy.dangerouslyAllowPrivateNetwork` defaults to `true` when unset (trusted-network model).
|
||||
- Set `ssrfPolicy.dangerouslyAllowPrivateNetwork: false` for strict public-only browser navigation.
|
||||
- In strict mode, remote CDP profile endpoints (`profiles.*.cdpUrl`) are subject to the same private-network blocking during reachability/discovery checks.
|
||||
- `ssrfPolicy.allowPrivateNetwork` remains supported as a legacy alias.
|
||||
- In strict mode, use `ssrfPolicy.hostnameAllowlist` and `ssrfPolicy.allowedHostnames` for explicit exceptions.
|
||||
- Remote profiles are attach-only (start/stop/reset disabled).
|
||||
@@ -2487,6 +2536,11 @@ See [Plugins](/tools/plugin).
|
||||
- Relay-backed registrations are delegated to a specific gateway identity. The paired iOS app fetches `gateway.identity.get`, includes that identity in the relay registration, and forwards a registration-scoped send grant to the gateway. Another gateway cannot reuse that stored registration.
|
||||
- `OPENCLAW_APNS_RELAY_BASE_URL` / `OPENCLAW_APNS_RELAY_TIMEOUT_MS`: temporary env overrides for the relay config above.
|
||||
- `OPENCLAW_APNS_RELAY_ALLOW_HTTP=true`: development-only escape hatch for loopback HTTP relay URLs. Production relay URLs should stay on HTTPS.
|
||||
- `gateway.channelHealthCheckMinutes`: channel health-monitor interval in minutes. Set `0` to disable health-monitor restarts globally. Default: `5`.
|
||||
- `gateway.channelStaleEventThresholdMinutes`: stale-socket threshold in minutes. Keep this greater than or equal to `gateway.channelHealthCheckMinutes`. Default: `30`.
|
||||
- `gateway.channelMaxRestartsPerHour`: maximum health-monitor restarts per channel/account in a rolling hour. Default: `10`.
|
||||
- `channels.<provider>.healthMonitor.enabled`: per-channel opt-out for health-monitor restarts while keeping the global monitor enabled.
|
||||
- `channels.<provider>.accounts.<accountId>.healthMonitor.enabled`: per-account override for multi-account channels. When set, it takes precedence over the channel-level override.
|
||||
- Local gateway call paths can use `gateway.remote.*` as fallback only when `gateway.auth.*` is unset.
|
||||
- If `gateway.auth.token` / `gateway.auth.password` is explicitly configured via SecretRef and unresolved, resolution fails closed (no remote fallback masking).
|
||||
- `trustedProxies`: reverse proxy IPs that terminate TLS. Only list proxies you control.
|
||||
|
||||
@@ -170,11 +170,41 @@ When validation fails:
|
||||
```
|
||||
|
||||
- **Metadata mentions**: native @-mentions (WhatsApp tap-to-mention, Telegram @bot, etc.)
|
||||
- **Text patterns**: regex patterns in `mentionPatterns`
|
||||
- **Text patterns**: safe regex patterns in `mentionPatterns`
|
||||
- See [full reference](/gateway/configuration-reference#group-chat-mention-gating) for per-channel overrides and self-chat mode.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Tune gateway channel health monitoring">
|
||||
Control how aggressively the gateway restarts channels that look stale:
|
||||
|
||||
```json5
|
||||
{
|
||||
gateway: {
|
||||
channelHealthCheckMinutes: 5,
|
||||
channelStaleEventThresholdMinutes: 30,
|
||||
channelMaxRestartsPerHour: 10,
|
||||
},
|
||||
channels: {
|
||||
telegram: {
|
||||
healthMonitor: { enabled: false },
|
||||
accounts: {
|
||||
alerts: {
|
||||
healthMonitor: { enabled: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
- Set `gateway.channelHealthCheckMinutes: 0` to disable health-monitor restarts globally.
|
||||
- `channelStaleEventThresholdMinutes` should be greater than or equal to the check interval.
|
||||
- Use `channels.<provider>.healthMonitor.enabled` or `channels.<provider>.accounts.<id>.healthMonitor.enabled` to disable auto-restarts for one channel or account without disabling the global monitor.
|
||||
- See [Health Checks](/gateway/health) for operational debugging and the [full reference](/gateway/configuration-reference#gateway) for all fields.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Configure sessions and resets">
|
||||
Sessions control conversation continuity and isolation:
|
||||
|
||||
|
||||
@@ -24,6 +24,15 @@ Short guide to verify channel connectivity without guessing.
|
||||
- Session store: `ls -l ~/.openclaw/agents/<agentId>/sessions/sessions.json` (path can be overridden in config). Count and recent recipients are surfaced via `status`.
|
||||
- Relink flow: `openclaw channels logout && openclaw channels login --verbose` when status codes 409–515 or `loggedOut` appear in logs. (Note: the QR login flow auto-restarts once for status 515 after pairing.)
|
||||
|
||||
## Health monitor config
|
||||
|
||||
- `gateway.channelHealthCheckMinutes`: how often the gateway checks channel health. Default: `5`. Set `0` to disable health-monitor restarts globally.
|
||||
- `gateway.channelStaleEventThresholdMinutes`: how long a connected channel can stay idle before the health monitor treats it as stale and restarts it. Default: `30`. Keep this greater than or equal to `gateway.channelHealthCheckMinutes`.
|
||||
- `gateway.channelMaxRestartsPerHour`: rolling one-hour cap for health-monitor restarts per channel/account. Default: `10`.
|
||||
- `channels.<provider>.healthMonitor.enabled`: disable health-monitor restarts for a specific channel while leaving global monitoring enabled.
|
||||
- `channels.<provider>.accounts.<accountId>.healthMonitor.enabled`: multi-account override that wins over the channel-level setting.
|
||||
- These per-channel overrides apply to the built-in channel monitors that expose them today: Discord, Google Chat, iMessage, Microsoft Teams, Signal, Slack, Telegram, and WhatsApp.
|
||||
|
||||
## When something fails
|
||||
|
||||
- `logged out` or status 409–515 → relink with `openclaw channels logout` then `openclaw channels login`.
|
||||
|
||||
@@ -7,7 +7,7 @@ status: active
|
||||
|
||||
# Sandboxing
|
||||
|
||||
OpenClaw can run **tools inside Docker containers** to reduce blast radius.
|
||||
OpenClaw can run **tools inside sandbox backends** to reduce blast radius.
|
||||
This is **optional** and controlled by configuration (`agents.defaults.sandbox` or
|
||||
`agents.list[].sandbox`). If sandboxing is off, tools run on the host.
|
||||
The Gateway stays on the host; tool execution runs in an isolated sandbox
|
||||
@@ -54,6 +54,120 @@ Not sandboxed:
|
||||
- `"agent"`: one container per agent.
|
||||
- `"shared"`: one container shared by all sandboxed sessions.
|
||||
|
||||
## Backend
|
||||
|
||||
`agents.defaults.sandbox.backend` controls **which runtime** provides the sandbox:
|
||||
|
||||
- `"docker"` (default): local Docker-backed sandbox runtime.
|
||||
- `"openshell"`: OpenShell-backed sandbox runtime.
|
||||
|
||||
OpenShell-specific config lives under `plugins.entries.openshell.config`.
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
sandbox: {
|
||||
mode: "all",
|
||||
backend: "openshell",
|
||||
scope: "session",
|
||||
workspaceAccess: "rw",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
entries: {
|
||||
openshell: {
|
||||
enabled: true,
|
||||
config: {
|
||||
from: "openclaw",
|
||||
mode: "remote", // mirror | remote
|
||||
remoteWorkspaceDir: "/sandbox",
|
||||
remoteAgentWorkspaceDir: "/agent",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
OpenShell modes:
|
||||
|
||||
- `mirror` (default): local workspace stays canonical. OpenClaw syncs local files into OpenShell before exec and syncs the remote workspace back after exec.
|
||||
- `remote`: OpenShell workspace is canonical after the sandbox is created. OpenClaw seeds the remote workspace once from the local workspace, then file tools and exec run directly against the remote sandbox without syncing changes back.
|
||||
|
||||
Current OpenShell limitations:
|
||||
|
||||
- sandbox browser is not supported yet
|
||||
- `sandbox.docker.binds` is not supported on the OpenShell backend
|
||||
- Docker-specific runtime knobs under `sandbox.docker.*` still apply only to the Docker backend
|
||||
|
||||
## OpenShell workspace modes
|
||||
|
||||
OpenShell has two workspace models. This is the part that matters most in practice.
|
||||
|
||||
### `mirror`
|
||||
|
||||
Use `plugins.entries.openshell.config.mode: "mirror"` when you want the **local workspace to stay canonical**.
|
||||
|
||||
Behavior:
|
||||
|
||||
- Before `exec`, OpenClaw syncs the local workspace into the OpenShell sandbox.
|
||||
- After `exec`, OpenClaw syncs the remote workspace back to the local workspace.
|
||||
- File tools still operate through the sandbox bridge, but the local workspace remains the source of truth between turns.
|
||||
|
||||
Use this when:
|
||||
|
||||
- you edit files locally outside OpenClaw and want those changes to show up in the sandbox automatically
|
||||
- you want the OpenShell sandbox to behave as much like the Docker backend as possible
|
||||
- you want the host workspace to reflect sandbox writes after each exec turn
|
||||
|
||||
Tradeoff:
|
||||
|
||||
- extra sync cost before and after exec
|
||||
|
||||
### `remote`
|
||||
|
||||
Use `plugins.entries.openshell.config.mode: "remote"` when you want the **OpenShell workspace to become canonical**.
|
||||
|
||||
Behavior:
|
||||
|
||||
- When the sandbox is first created, OpenClaw seeds the remote workspace from the local workspace once.
|
||||
- After that, `exec`, `read`, `write`, `edit`, and `apply_patch` operate directly against the remote OpenShell workspace.
|
||||
- OpenClaw does **not** sync remote changes back into the local workspace after exec.
|
||||
- Prompt-time media reads still work because file and media tools read through the sandbox bridge instead of assuming a local host path.
|
||||
|
||||
Important consequences:
|
||||
|
||||
- If you edit files on the host outside OpenClaw after the seed step, the remote sandbox will **not** see those changes automatically.
|
||||
- If the sandbox is recreated, the remote workspace is seeded from the local workspace again.
|
||||
- With `scope: "agent"` or `scope: "shared"`, that remote workspace is shared at that same scope.
|
||||
|
||||
Use this when:
|
||||
|
||||
- the sandbox should live primarily on the remote OpenShell side
|
||||
- you want lower per-turn sync overhead
|
||||
- you do not want host-local edits to silently overwrite remote sandbox state
|
||||
|
||||
Choose `mirror` if you think of the sandbox as a temporary execution environment.
|
||||
Choose `remote` if you think of the sandbox as the real workspace.
|
||||
|
||||
## OpenShell lifecycle
|
||||
|
||||
OpenShell sandboxes are still managed through the normal sandbox lifecycle:
|
||||
|
||||
- `openclaw sandbox list` shows OpenShell runtimes as well as Docker runtimes
|
||||
- `openclaw sandbox recreate` deletes the current runtime and lets OpenClaw recreate it on next use
|
||||
- prune logic is backend-aware too
|
||||
|
||||
For `remote` mode, recreate is especially important:
|
||||
|
||||
- recreate deletes the canonical remote workspace for that scope
|
||||
- the next use seeds a fresh remote workspace from the local workspace
|
||||
|
||||
For `mirror` mode, recreate mainly resets the remote execution environment
|
||||
because the local workspace remains canonical anyway.
|
||||
|
||||
## Workspace access
|
||||
|
||||
`agents.defaults.sandbox.workspaceAccess` controls **what the sandbox can see**:
|
||||
@@ -62,6 +176,12 @@ Not sandboxed:
|
||||
- `"ro"`: mounts the agent workspace read-only at `/agent` (disables `write`/`edit`/`apply_patch`).
|
||||
- `"rw"`: mounts the agent workspace read/write at `/workspace`.
|
||||
|
||||
With the OpenShell backend:
|
||||
|
||||
- `mirror` mode still uses the local workspace as the canonical source between exec turns
|
||||
- `remote` mode uses the remote OpenShell workspace as the canonical source after the initial seed
|
||||
- `workspaceAccess: "ro"` and `"none"` still restrict write behavior the same way
|
||||
|
||||
Inbound media is copied into the active sandbox workspace (`media/inbound/*`).
|
||||
Skills note: the `read` tool is sandbox-rooted. With `workspaceAccess: "none"`,
|
||||
OpenClaw mirrors eligible skills into the sandbox workspace (`.../skills`) so
|
||||
@@ -116,7 +236,7 @@ Security notes:
|
||||
|
||||
## Images + setup
|
||||
|
||||
Default image: `openclaw-sandbox:bookworm-slim`
|
||||
Default Docker image: `openclaw-sandbox:bookworm-slim`
|
||||
|
||||
Build it once:
|
||||
|
||||
@@ -145,7 +265,7 @@ Sandboxed browser image:
|
||||
scripts/sandbox-browser-setup.sh
|
||||
```
|
||||
|
||||
By default, sandbox containers run with **no network**.
|
||||
By default, Docker sandbox containers run with **no network**.
|
||||
Override with `agents.defaults.sandbox.docker.network`.
|
||||
|
||||
The bundled sandbox browser image also applies conservative Chromium startup defaults
|
||||
|
||||
@@ -348,7 +348,7 @@ Command paths can opt into supported SecretRef resolution via gateway snapshot R
|
||||
There are two broad behaviors:
|
||||
|
||||
- Strict command paths (for example `openclaw memory` remote-memory paths and `openclaw qr --remote`) read from the active snapshot and fail fast when a required SecretRef is unavailable.
|
||||
- Read-only command paths (for example `openclaw status`, `openclaw status --all`, `openclaw channels status`, `openclaw channels resolve`, and read-only doctor/config repair flows) also prefer the active snapshot, but degrade instead of aborting when a targeted SecretRef is unavailable in that command path.
|
||||
- Read-only command paths (for example `openclaw status`, `openclaw status --all`, `openclaw channels status`, `openclaw channels resolve`, `openclaw security audit`, and read-only doctor/config repair flows) also prefer the active snapshot, but degrade instead of aborting when a targeted SecretRef is unavailable in that command path.
|
||||
|
||||
Read-only behavior:
|
||||
|
||||
|
||||
@@ -40,11 +40,17 @@ pnpm gateway:watch
|
||||
This maps to:
|
||||
|
||||
```bash
|
||||
node --watch-path src --watch-path tsconfig.json --watch-path package.json --watch-preserve-output scripts/run-node.mjs gateway --force
|
||||
node scripts/watch-node.mjs gateway --force
|
||||
```
|
||||
|
||||
Add any gateway CLI flags after `gateway:watch` and they will be passed through
|
||||
on each restart.
|
||||
The watcher restarts on build-relevant files under `src/`, extension source files,
|
||||
extension `package.json` and `openclaw.plugin.json` metadata, `tsconfig.json`,
|
||||
`package.json`, and `tsdown.config.ts`. Extension metadata changes restart the
|
||||
gateway without forcing a `tsdown` rebuild; source and config changes still
|
||||
rebuild `dist` first.
|
||||
|
||||
Add any gateway CLI flags after `gateway:watch` and they will be passed through on
|
||||
each restart.
|
||||
|
||||
## Dev profile + dev gateway (--dev)
|
||||
|
||||
|
||||
@@ -783,7 +783,7 @@ Gemini CLI uses a **plugin auth flow**, not a client id or secret in `openclaw.j
|
||||
|
||||
Steps:
|
||||
|
||||
1. Enable the plugin: `openclaw plugins enable google-gemini-cli-auth`
|
||||
1. Enable the plugin: `openclaw plugins enable google`
|
||||
2. Login: `openclaw models auth login --provider google-gemini-cli --set-default`
|
||||
|
||||
This stores OAuth tokens in auth profiles on the gateway host. Details: [Model providers](/concepts/model-providers).
|
||||
|
||||
@@ -102,6 +102,16 @@ For VPS/cloud hosts, avoid third-party "1-click" marketplace images when possibl
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
Want the current GitHub `main` head with a package-manager install?
|
||||
|
||||
```bash
|
||||
npm install -g github:openclaw/openclaw#main
|
||||
```
|
||||
|
||||
```bash
|
||||
pnpm add -g github:openclaw/openclaw#main
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="From source" icon="github">
|
||||
|
||||
@@ -116,6 +116,11 @@ The script exits with code `2` for invalid method selection or invalid `--instal
|
||||
curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install.sh | bash -s -- --install-method git
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="GitHub main via npm">
|
||||
```bash
|
||||
curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install.sh | bash -s -- --version main
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Dry run">
|
||||
```bash
|
||||
curl -fsSL --proto '=https' --tlsv1.2 https://openclaw.ai/install.sh | bash -s -- --dry-run
|
||||
@@ -126,39 +131,39 @@ The script exits with code `2` for invalid method selection or invalid `--instal
|
||||
<AccordionGroup>
|
||||
<Accordion title="Flags reference">
|
||||
|
||||
| Flag | Description |
|
||||
| ------------------------------- | ---------------------------------------------------------- |
|
||||
| `--install-method npm\|git` | Choose install method (default: `npm`). Alias: `--method` |
|
||||
| `--npm` | Shortcut for npm method |
|
||||
| `--git` | Shortcut for git method. Alias: `--github` |
|
||||
| `--version <version\|dist-tag>` | npm version or dist-tag (default: `latest`) |
|
||||
| `--beta` | Use beta dist-tag if available, else fallback to `latest` |
|
||||
| `--git-dir <path>` | Checkout directory (default: `~/openclaw`). Alias: `--dir` |
|
||||
| `--no-git-update` | Skip `git pull` for existing checkout |
|
||||
| `--no-prompt` | Disable prompts |
|
||||
| `--no-onboard` | Skip onboarding |
|
||||
| `--onboard` | Enable onboarding |
|
||||
| `--dry-run` | Print actions without applying changes |
|
||||
| `--verbose` | Enable debug output (`set -x`, npm notice-level logs) |
|
||||
| `--help` | Show usage (`-h`) |
|
||||
| Flag | Description |
|
||||
| ------------------------------------- | ---------------------------------------------------------- |
|
||||
| `--install-method npm\|git` | Choose install method (default: `npm`). Alias: `--method` |
|
||||
| `--npm` | Shortcut for npm method |
|
||||
| `--git` | Shortcut for git method. Alias: `--github` |
|
||||
| `--version <version\|dist-tag\|spec>` | npm version, dist-tag, or package spec (default: `latest`) |
|
||||
| `--beta` | Use beta dist-tag if available, else fallback to `latest` |
|
||||
| `--git-dir <path>` | Checkout directory (default: `~/openclaw`). Alias: `--dir` |
|
||||
| `--no-git-update` | Skip `git pull` for existing checkout |
|
||||
| `--no-prompt` | Disable prompts |
|
||||
| `--no-onboard` | Skip onboarding |
|
||||
| `--onboard` | Enable onboarding |
|
||||
| `--dry-run` | Print actions without applying changes |
|
||||
| `--verbose` | Enable debug output (`set -x`, npm notice-level logs) |
|
||||
| `--help` | Show usage (`-h`) |
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Environment variables reference">
|
||||
|
||||
| Variable | Description |
|
||||
| ------------------------------------------- | --------------------------------------------- |
|
||||
| `OPENCLAW_INSTALL_METHOD=git\|npm` | Install method |
|
||||
| `OPENCLAW_VERSION=latest\|next\|<semver>` | npm version or dist-tag |
|
||||
| `OPENCLAW_BETA=0\|1` | Use beta if available |
|
||||
| `OPENCLAW_GIT_DIR=<path>` | Checkout directory |
|
||||
| `OPENCLAW_GIT_UPDATE=0\|1` | Toggle git updates |
|
||||
| `OPENCLAW_NO_PROMPT=1` | Disable prompts |
|
||||
| `OPENCLAW_NO_ONBOARD=1` | Skip onboarding |
|
||||
| `OPENCLAW_DRY_RUN=1` | Dry run mode |
|
||||
| `OPENCLAW_VERBOSE=1` | Debug mode |
|
||||
| `OPENCLAW_NPM_LOGLEVEL=error\|warn\|notice` | npm log level |
|
||||
| `SHARP_IGNORE_GLOBAL_LIBVIPS=0\|1` | Control sharp/libvips behavior (default: `1`) |
|
||||
| Variable | Description |
|
||||
| ------------------------------------------------------- | --------------------------------------------- |
|
||||
| `OPENCLAW_INSTALL_METHOD=git\|npm` | Install method |
|
||||
| `OPENCLAW_VERSION=latest\|next\|main\|<semver>\|<spec>` | npm version, dist-tag, or package spec |
|
||||
| `OPENCLAW_BETA=0\|1` | Use beta if available |
|
||||
| `OPENCLAW_GIT_DIR=<path>` | Checkout directory |
|
||||
| `OPENCLAW_GIT_UPDATE=0\|1` | Toggle git updates |
|
||||
| `OPENCLAW_NO_PROMPT=1` | Disable prompts |
|
||||
| `OPENCLAW_NO_ONBOARD=1` | Skip onboarding |
|
||||
| `OPENCLAW_DRY_RUN=1` | Dry run mode |
|
||||
| `OPENCLAW_VERBOSE=1` | Debug mode |
|
||||
| `OPENCLAW_NPM_LOGLEVEL=error\|warn\|notice` | npm log level |
|
||||
| `SHARP_IGNORE_GLOBAL_LIBVIPS=0\|1` | Control sharp/libvips behavior (default: `1`) |
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
@@ -276,6 +281,11 @@ Designed for environments where you want everything under a local prefix (defaul
|
||||
& ([scriptblock]::Create((iwr -useb https://openclaw.ai/install.ps1))) -InstallMethod git
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="GitHub main via npm">
|
||||
```powershell
|
||||
& ([scriptblock]::Create((iwr -useb https://openclaw.ai/install.ps1))) -Tag main
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Custom git directory">
|
||||
```powershell
|
||||
& ([scriptblock]::Create((iwr -useb https://openclaw.ai/install.ps1))) -InstallMethod git -GitDir "C:\openclaw"
|
||||
@@ -299,14 +309,14 @@ Designed for environments where you want everything under a local prefix (defaul
|
||||
<AccordionGroup>
|
||||
<Accordion title="Flags reference">
|
||||
|
||||
| Flag | Description |
|
||||
| ------------------------- | ------------------------------------------------------ |
|
||||
| `-InstallMethod npm\|git` | Install method (default: `npm`) |
|
||||
| `-Tag <tag>` | npm dist-tag (default: `latest`) |
|
||||
| `-GitDir <path>` | Checkout directory (default: `%USERPROFILE%\openclaw`) |
|
||||
| `-NoOnboard` | Skip onboarding |
|
||||
| `-NoGitUpdate` | Skip `git pull` |
|
||||
| `-DryRun` | Print actions only |
|
||||
| Flag | Description |
|
||||
| --------------------------- | ---------------------------------------------------------- |
|
||||
| `-InstallMethod npm\|git` | Install method (default: `npm`) |
|
||||
| `-Tag <tag\|version\|spec>` | npm dist-tag, version, or package spec (default: `latest`) |
|
||||
| `-GitDir <path>` | Checkout directory (default: `%USERPROFILE%\openclaw`) |
|
||||
| `-NoOnboard` | Skip onboarding |
|
||||
| `-NoGitUpdate` | Skip `git pull` |
|
||||
| `-DryRun` | Print actions only |
|
||||
|
||||
</Accordion>
|
||||
|
||||
|
||||
@@ -65,7 +65,25 @@ openclaw update --channel dev
|
||||
openclaw update --channel stable
|
||||
```
|
||||
|
||||
Use `--tag <dist-tag|version>` for a one-off install tag/version.
|
||||
Use `--tag <dist-tag|version|spec>` for a one-off package target override.
|
||||
|
||||
For the current GitHub `main` head via a package-manager install:
|
||||
|
||||
```bash
|
||||
openclaw update --tag main
|
||||
```
|
||||
|
||||
Manual equivalents:
|
||||
|
||||
```bash
|
||||
npm i -g github:openclaw/openclaw#main
|
||||
```
|
||||
|
||||
```bash
|
||||
pnpm add -g github:openclaw/openclaw#main
|
||||
```
|
||||
|
||||
You can also pass an explicit package spec to `--tag` for one-off updates (for example a GitHub ref or tarball URL).
|
||||
|
||||
See [Development channels](/install/development-channels) for channel semantics and release notes.
|
||||
|
||||
|
||||
@@ -285,6 +285,7 @@ Available families:
|
||||
- `photos.latest`
|
||||
- `contacts.search`, `contacts.add`
|
||||
- `calendar.events`, `calendar.add`
|
||||
- `callLog.search`
|
||||
- `motion.activity`, `motion.pedometer`
|
||||
|
||||
Example invokes:
|
||||
|
||||
@@ -163,4 +163,5 @@ See [Camera node](/nodes/camera) for parameters and CLI helpers.
|
||||
- `photos.latest`
|
||||
- `contacts.search`, `contacts.add`
|
||||
- `calendar.events`, `calendar.add`
|
||||
- `callLog.search`
|
||||
- `motion.activity`, `motion.pedometer`
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
---
|
||||
summary: "OpenClaw macOS release checklist (Sparkle feed, packaging, signing)"
|
||||
read_when:
|
||||
- Cutting or validating a OpenClaw macOS release
|
||||
- Updating the Sparkle appcast or feed assets
|
||||
title: "macOS Release"
|
||||
---
|
||||
|
||||
# OpenClaw macOS release (Sparkle)
|
||||
|
||||
This app now ships Sparkle auto-updates. Release builds must be Developer ID–signed, zipped, and published with a signed appcast entry.
|
||||
|
||||
## Prereqs
|
||||
|
||||
- Developer ID Application cert installed (example: `Developer ID Application: <Developer Name> (<TEAMID>)`).
|
||||
- Sparkle private key path set in the environment as `SPARKLE_PRIVATE_KEY_FILE` (path to your Sparkle ed25519 private key; public key baked into Info.plist). If it is missing, check `~/.profile`.
|
||||
- Notary credentials (keychain profile or API key) for `xcrun notarytool` if you want Gatekeeper-safe DMG/zip distribution.
|
||||
- We use a Keychain profile named `openclaw-notary`, created from App Store Connect API key env vars in your shell profile:
|
||||
- `APP_STORE_CONNECT_API_KEY_P8`, `APP_STORE_CONNECT_KEY_ID`, `APP_STORE_CONNECT_ISSUER_ID`
|
||||
- `echo "$APP_STORE_CONNECT_API_KEY_P8" | sed 's/\\n/\n/g' > /tmp/openclaw-notary.p8`
|
||||
- `xcrun notarytool store-credentials "openclaw-notary" --key /tmp/openclaw-notary.p8 --key-id "$APP_STORE_CONNECT_KEY_ID" --issuer "$APP_STORE_CONNECT_ISSUER_ID"`
|
||||
- `pnpm` deps installed (`pnpm install --config.node-linker=hoisted`).
|
||||
- Sparkle tools are fetched automatically via SwiftPM at `apps/macos/.build/artifacts/sparkle/Sparkle/bin/` (`sign_update`, `generate_appcast`, etc.).
|
||||
|
||||
## Build & package
|
||||
|
||||
Notes:
|
||||
|
||||
- `APP_BUILD` maps to `CFBundleVersion`/`sparkle:version`; keep it numeric + monotonic (no `-beta`), or Sparkle compares it as equal.
|
||||
- If `APP_BUILD` is omitted, `scripts/package-mac-app.sh` derives a Sparkle-safe default from `APP_VERSION` (`YYYYMMDDNN`: stable defaults to `90`, prereleases use a suffix-derived lane) and uses the higher of that value and git commit count.
|
||||
- You can still override `APP_BUILD` explicitly when release engineering needs a specific monotonic value.
|
||||
- For `BUILD_CONFIG=release`, `scripts/package-mac-app.sh` now defaults to universal (`arm64 x86_64`) automatically. You can still override with `BUILD_ARCHS=arm64` or `BUILD_ARCHS=x86_64`. For local/dev builds (`BUILD_CONFIG=debug`), it defaults to the current architecture (`$(uname -m)`).
|
||||
- Use `scripts/package-mac-dist.sh` for release artifacts (zip + DMG + notarization). Use `scripts/package-mac-app.sh` for local/dev packaging.
|
||||
|
||||
```bash
|
||||
# From repo root; set release IDs so Sparkle feed is enabled.
|
||||
# This command builds release artifacts without notarization.
|
||||
# APP_BUILD must be numeric + monotonic for Sparkle compare.
|
||||
# Default is auto-derived from APP_VERSION when omitted.
|
||||
SKIP_NOTARIZE=1 \
|
||||
BUNDLE_ID=ai.openclaw.mac \
|
||||
APP_VERSION=2026.3.13 \
|
||||
BUILD_CONFIG=release \
|
||||
SIGN_IDENTITY="Developer ID Application: <Developer Name> (<TEAMID>)" \
|
||||
scripts/package-mac-dist.sh
|
||||
|
||||
# `package-mac-dist.sh` already creates the zip + DMG.
|
||||
# If you used `package-mac-app.sh` directly instead, create them manually:
|
||||
# If you want notarization/stapling in this step, use the NOTARIZE command below.
|
||||
ditto -c -k --sequesterRsrc --keepParent dist/OpenClaw.app dist/OpenClaw-2026.3.13.zip
|
||||
|
||||
# Optional: build a styled DMG for humans (drag to /Applications)
|
||||
scripts/create-dmg.sh dist/OpenClaw.app dist/OpenClaw-2026.3.13.dmg
|
||||
|
||||
# Recommended: build + notarize/staple zip + DMG
|
||||
# First, create a keychain profile once:
|
||||
# xcrun notarytool store-credentials "openclaw-notary" \
|
||||
# --apple-id "<apple-id>" --team-id "<team-id>" --password "<app-specific-password>"
|
||||
NOTARIZE=1 NOTARYTOOL_PROFILE=openclaw-notary \
|
||||
BUNDLE_ID=ai.openclaw.mac \
|
||||
APP_VERSION=2026.3.13 \
|
||||
BUILD_CONFIG=release \
|
||||
SIGN_IDENTITY="Developer ID Application: <Developer Name> (<TEAMID>)" \
|
||||
scripts/package-mac-dist.sh
|
||||
|
||||
# Optional: ship dSYM alongside the release
|
||||
ditto -c -k --keepParent apps/macos/.build/release/OpenClaw.app.dSYM dist/OpenClaw-2026.3.13.dSYM.zip
|
||||
```
|
||||
|
||||
## Appcast entry
|
||||
|
||||
Use the release note generator so Sparkle renders formatted HTML notes:
|
||||
|
||||
```bash
|
||||
SPARKLE_PRIVATE_KEY_FILE=/path/to/ed25519-private-key scripts/make_appcast.sh dist/OpenClaw-2026.3.13.zip https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml
|
||||
```
|
||||
|
||||
Generates HTML release notes from `CHANGELOG.md` (via [`scripts/changelog-to-html.sh`](https://github.com/openclaw/openclaw/blob/main/scripts/changelog-to-html.sh)) and embeds them in the appcast entry.
|
||||
Commit the updated `appcast.xml` alongside the release assets (zip + dSYM) when publishing.
|
||||
|
||||
## Publish & verify
|
||||
|
||||
- Upload `OpenClaw-2026.3.13.zip` (and `OpenClaw-2026.3.13.dSYM.zip`) to the GitHub release for tag `v2026.3.13`.
|
||||
- Ensure the raw appcast URL matches the baked feed: `https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml`.
|
||||
- Sanity checks:
|
||||
- `curl -I https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml` returns 200.
|
||||
- `curl -I <enclosure url>` returns 200 after assets upload.
|
||||
- On a previous public build, run “Check for Updates…” from the About tab and verify Sparkle installs the new build cleanly.
|
||||
|
||||
Definition of done: signed app + appcast are published, update flow works from an older installed version, and release assets are attached to the GitHub release.
|
||||
292
docs/plugins/bundles.md
Normal file
292
docs/plugins/bundles.md
Normal file
@@ -0,0 +1,292 @@
|
||||
---
|
||||
summary: "Unified bundle format guide for Codex, Claude, and Cursor bundles in OpenClaw"
|
||||
read_when:
|
||||
- You want to install or debug a Codex, Claude, or Cursor-compatible bundle
|
||||
- You need to understand how OpenClaw maps bundle content into native features
|
||||
- You are documenting bundle compatibility or current support limits
|
||||
title: "Plugin Bundles"
|
||||
---
|
||||
|
||||
# Plugin bundles
|
||||
|
||||
OpenClaw supports one shared class of external plugin package: **bundle
|
||||
plugins**.
|
||||
|
||||
Today that means three closely related ecosystems:
|
||||
|
||||
- Codex bundles
|
||||
- Claude bundles
|
||||
- Cursor bundles
|
||||
|
||||
OpenClaw shows all of them as `Format: bundle` in `openclaw plugins list`.
|
||||
Verbose output and `openclaw plugins info <id>` also show the subtype
|
||||
(`codex`, `claude`, or `cursor`).
|
||||
|
||||
Related:
|
||||
|
||||
- Plugin system overview: [Plugins](/tools/plugin)
|
||||
- CLI install/list flows: [plugins](/cli/plugins)
|
||||
- Native manifest schema: [Plugin manifest](/plugins/manifest)
|
||||
|
||||
## What a bundle is
|
||||
|
||||
A bundle is a **content/metadata pack**, not a native in-process OpenClaw
|
||||
plugin.
|
||||
|
||||
Today, OpenClaw does **not** execute bundle runtime code in-process. Instead,
|
||||
it detects known bundle files, reads the metadata, and maps supported bundle
|
||||
content into native OpenClaw surfaces such as skills, hook packs, MCP config,
|
||||
and embedded Pi settings.
|
||||
|
||||
That is the main trust boundary:
|
||||
|
||||
- native OpenClaw plugin: runtime module executes in-process
|
||||
- bundle: metadata/content pack, with selective feature mapping
|
||||
|
||||
## Shared bundle model
|
||||
|
||||
Codex, Claude, and Cursor bundles are similar enough that OpenClaw treats them
|
||||
as one normalized model.
|
||||
|
||||
Shared idea:
|
||||
|
||||
- a small manifest file, or a default directory layout
|
||||
- one or more content roots such as `skills/` or `commands/`
|
||||
- optional tool/runtime metadata such as MCP, hooks, agents, or LSP
|
||||
- install as a directory or archive, then enable in the normal plugin list
|
||||
|
||||
Common OpenClaw behavior:
|
||||
|
||||
- detect the bundle subtype
|
||||
- normalize it into one internal bundle record
|
||||
- map supported parts into native OpenClaw features
|
||||
- report unsupported parts as detected-but-not-wired capabilities
|
||||
|
||||
In practice, most users do not need to think about the vendor-specific format
|
||||
first. The more useful question is: which bundle surfaces does OpenClaw map
|
||||
today?
|
||||
|
||||
## Detection order
|
||||
|
||||
OpenClaw prefers native OpenClaw plugin/package layouts before bundle handling.
|
||||
|
||||
Practical effect:
|
||||
|
||||
- `openclaw.plugin.json` wins over bundle detection
|
||||
- package installs with valid `package.json` + `openclaw.extensions` use the
|
||||
native install path
|
||||
- if a directory contains both native and bundle metadata, OpenClaw treats it
|
||||
as native first
|
||||
|
||||
That avoids partially installing a dual-format package as a bundle and then
|
||||
loading it later as a native plugin.
|
||||
|
||||
## What works today
|
||||
|
||||
OpenClaw normalizes bundle metadata into one internal bundle record, then maps
|
||||
supported surfaces into existing native behavior.
|
||||
|
||||
### Supported now
|
||||
|
||||
#### Skill content
|
||||
|
||||
- bundle skill roots load as normal OpenClaw skill roots
|
||||
- Claude `commands` roots are treated as additional skill roots
|
||||
- Cursor `.cursor/commands` roots are treated as additional skill roots
|
||||
|
||||
This means Claude markdown command files work through the normal OpenClaw skill
|
||||
loader. Cursor command markdown works through the same path.
|
||||
|
||||
#### Hook packs
|
||||
|
||||
- bundle hook roots work **only** when they use the normal OpenClaw hook-pack
|
||||
layout. Today this is primarily the Codex-compatible case:
|
||||
- `HOOK.md`
|
||||
- `handler.ts` or `handler.js`
|
||||
|
||||
#### MCP for CLI backends
|
||||
|
||||
- enabled bundles can contribute MCP server config
|
||||
- current runtime wiring is used by the `claude-cli` backend
|
||||
- OpenClaw merges bundle MCP config into the backend `--mcp-config` file
|
||||
|
||||
#### Embedded Pi settings
|
||||
|
||||
- Claude `settings.json` is imported as default embedded Pi settings when the
|
||||
bundle is enabled
|
||||
- OpenClaw sanitizes shell override keys before applying them
|
||||
|
||||
Sanitized keys:
|
||||
|
||||
- `shellPath`
|
||||
- `shellCommandPrefix`
|
||||
|
||||
### Detected but not executed
|
||||
|
||||
These surfaces are detected, shown in bundle capabilities, and may appear in
|
||||
diagnostics/info output, but OpenClaw does not run them yet:
|
||||
|
||||
- Claude `agents`
|
||||
- Claude `hooks.json` automation
|
||||
- Claude `lspServers`
|
||||
- Claude `outputStyles`
|
||||
- Cursor `.cursor/agents`
|
||||
- Cursor `.cursor/hooks.json`
|
||||
- Cursor `.cursor/rules`
|
||||
- Cursor `mcpServers` outside the current mapped runtime paths
|
||||
- Codex inline/app metadata beyond capability reporting
|
||||
|
||||
## Capability reporting
|
||||
|
||||
`openclaw plugins info <id>` shows bundle capabilities from the normalized
|
||||
bundle record.
|
||||
|
||||
Supported capabilities are loaded quietly. Unsupported capabilities produce a
|
||||
warning such as:
|
||||
|
||||
```text
|
||||
bundle capability detected but not wired into OpenClaw yet: agents
|
||||
```
|
||||
|
||||
Current exceptions:
|
||||
|
||||
- Claude `commands` is considered supported because it maps to skills
|
||||
- Claude `settings` is considered supported because it maps to embedded Pi settings
|
||||
- Cursor `commands` is considered supported because it maps to skills
|
||||
- bundle MCP is considered supported where OpenClaw actually imports it
|
||||
- Codex `hooks` is considered supported only for OpenClaw hook-pack layouts
|
||||
|
||||
## Format differences
|
||||
|
||||
The formats are close, but not byte-for-byte identical. These are the practical
|
||||
differences that matter in OpenClaw.
|
||||
|
||||
### Codex
|
||||
|
||||
Typical markers:
|
||||
|
||||
- `.codex-plugin/plugin.json`
|
||||
- optional `skills/`
|
||||
- optional `hooks/`
|
||||
- optional `.mcp.json`
|
||||
- optional `.app.json`
|
||||
|
||||
Codex bundles fit OpenClaw best when they use skill roots and OpenClaw-style
|
||||
hook-pack directories.
|
||||
|
||||
### Claude
|
||||
|
||||
OpenClaw supports both:
|
||||
|
||||
- manifest-based Claude bundles: `.claude-plugin/plugin.json`
|
||||
- manifestless Claude bundles that use the default Claude layout
|
||||
|
||||
Default Claude layout markers OpenClaw recognizes:
|
||||
|
||||
- `skills/`
|
||||
- `commands/`
|
||||
- `agents/`
|
||||
- `hooks/hooks.json`
|
||||
- `.mcp.json`
|
||||
- `.lsp.json`
|
||||
- `settings.json`
|
||||
|
||||
Claude-specific notes:
|
||||
|
||||
- `commands/` is treated like skill content
|
||||
- `settings.json` is imported into embedded Pi settings
|
||||
- `hooks/hooks.json` is detected, but not executed as Claude automation
|
||||
|
||||
### Cursor
|
||||
|
||||
Typical markers:
|
||||
|
||||
- `.cursor-plugin/plugin.json`
|
||||
- optional `skills/`
|
||||
- optional `.cursor/commands/`
|
||||
- optional `.cursor/agents/`
|
||||
- optional `.cursor/rules/`
|
||||
- optional `.cursor/hooks.json`
|
||||
- optional `.mcp.json`
|
||||
|
||||
Cursor-specific notes:
|
||||
|
||||
- `.cursor/commands/` is treated like skill content
|
||||
- `.cursor/rules/`, `.cursor/agents/`, and `.cursor/hooks.json` are
|
||||
detect-only today
|
||||
|
||||
## Claude custom paths
|
||||
|
||||
Claude bundle manifests can declare custom component paths. OpenClaw treats
|
||||
those paths as **additive**, not replacing defaults.
|
||||
|
||||
Currently recognized custom path keys:
|
||||
|
||||
- `skills`
|
||||
- `commands`
|
||||
- `agents`
|
||||
- `hooks`
|
||||
- `mcpServers`
|
||||
- `lspServers`
|
||||
- `outputStyles`
|
||||
|
||||
Examples:
|
||||
|
||||
- default `commands/` plus manifest `commands: "extra-commands"` =>
|
||||
OpenClaw scans both
|
||||
- default `skills/` plus manifest `skills: ["team-skills"]` =>
|
||||
OpenClaw scans both
|
||||
|
||||
## Security model
|
||||
|
||||
Bundle support is intentionally narrower than native plugin support.
|
||||
|
||||
Current behavior:
|
||||
|
||||
- bundle discovery reads files inside the plugin root with boundary checks
|
||||
- skills and hook-pack paths must stay inside the plugin root
|
||||
- bundle settings files are read with the same boundary checks
|
||||
- OpenClaw does not execute arbitrary bundle runtime code in-process
|
||||
|
||||
This makes bundle support safer by default than native plugin modules, but you
|
||||
should still treat third-party bundles as trusted content for the features they
|
||||
do expose.
|
||||
|
||||
## Install examples
|
||||
|
||||
```bash
|
||||
openclaw plugins install ./my-codex-bundle
|
||||
openclaw plugins install ./my-claude-bundle
|
||||
openclaw plugins install ./my-cursor-bundle
|
||||
openclaw plugins install ./my-bundle.tgz
|
||||
openclaw plugins info my-bundle
|
||||
```
|
||||
|
||||
If the directory is a native OpenClaw plugin/package, the native install path
|
||||
still wins.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Bundle is detected but capabilities do not run
|
||||
|
||||
Check `openclaw plugins info <id>`.
|
||||
|
||||
If the capability is listed but OpenClaw says it is not wired yet, that is a
|
||||
real product limit, not a broken install.
|
||||
|
||||
### Claude command files do not appear
|
||||
|
||||
Make sure the bundle is enabled and the markdown files are inside a detected
|
||||
`commands` root or `skills` root.
|
||||
|
||||
### Claude settings do not apply
|
||||
|
||||
Current support is limited to embedded Pi settings from `settings.json`.
|
||||
OpenClaw does not treat bundle settings as raw OpenClaw config patches.
|
||||
|
||||
### Claude hooks do not execute
|
||||
|
||||
`hooks/hooks.json` is only detected today.
|
||||
|
||||
If you need runnable bundle hooks today, use the normal OpenClaw hook-pack
|
||||
layout through a supported Codex hook root or ship a native OpenClaw plugin.
|
||||
@@ -8,10 +8,28 @@ title: "Plugin Manifest"
|
||||
|
||||
# Plugin manifest (openclaw.plugin.json)
|
||||
|
||||
Every plugin **must** ship a `openclaw.plugin.json` file in the **plugin root**.
|
||||
OpenClaw uses this manifest to validate configuration **without executing plugin
|
||||
code**. Missing or invalid manifests are treated as plugin errors and block
|
||||
config validation.
|
||||
This page is for the **native OpenClaw plugin manifest** only.
|
||||
|
||||
For compatible bundle layouts, see [Plugin bundles](/plugins/bundles).
|
||||
|
||||
Compatible bundle formats use different manifest files:
|
||||
|
||||
- Codex bundle: `.codex-plugin/plugin.json`
|
||||
- Claude bundle: `.claude-plugin/plugin.json` or the default Claude component
|
||||
layout without a manifest
|
||||
- Cursor bundle: `.cursor-plugin/plugin.json`
|
||||
|
||||
OpenClaw auto-detects those bundle layouts too, but they are not validated
|
||||
against the `openclaw.plugin.json` schema described here.
|
||||
|
||||
For compatible bundles, OpenClaw currently reads bundle metadata plus declared
|
||||
skill roots, Claude command roots, Claude bundle `settings.json` defaults, and
|
||||
supported hook packs when the layout matches OpenClaw runtime expectations.
|
||||
|
||||
Every native OpenClaw plugin **must** ship a `openclaw.plugin.json` file in the
|
||||
**plugin root**. OpenClaw uses this manifest to validate configuration
|
||||
**without executing plugin code**. Missing or invalid manifests are treated as
|
||||
plugin errors and block config validation.
|
||||
|
||||
See the full plugin system guide: [Plugins](/tools/plugin).
|
||||
|
||||
@@ -38,6 +56,9 @@ Optional keys:
|
||||
- `kind` (string): plugin kind (examples: `"memory"`, `"context-engine"`).
|
||||
- `channels` (array): channel ids registered by this plugin (example: `["matrix"]`).
|
||||
- `providers` (array): provider ids registered by this plugin.
|
||||
- `providerAuthEnvVars` (object): auth env vars keyed by provider id. Use this
|
||||
when OpenClaw should resolve provider credentials from env without loading
|
||||
plugin runtime first.
|
||||
- `skills` (array): skill directories to load (relative to the plugin root).
|
||||
- `name` (string): display name for the plugin.
|
||||
- `description` (string): short plugin summary.
|
||||
@@ -63,9 +84,12 @@ Optional keys:
|
||||
|
||||
## Notes
|
||||
|
||||
- The manifest is **required for all plugins**, including local filesystem loads.
|
||||
- The manifest is **required for native OpenClaw plugins**, including local filesystem loads.
|
||||
- Runtime still loads the plugin module separately; the manifest is only for
|
||||
discovery + validation.
|
||||
- `providerAuthEnvVars` is the cheap metadata path for auth probes, env-marker
|
||||
validation, and similar provider-auth surfaces that should not boot plugin
|
||||
runtime just to inspect env names.
|
||||
- Exclusive plugin kinds are selected through `plugins.slots.*`.
|
||||
- `kind: "memory"` is selected by `plugins.slots.memory`.
|
||||
- `kind: "context-engine"` is selected by `plugins.slots.contextEngine`
|
||||
|
||||
@@ -42,7 +42,7 @@ MiniMax highlights these improvements in M2.5:
|
||||
Enable the bundled OAuth plugin and authenticate:
|
||||
|
||||
```bash
|
||||
openclaw plugins enable minimax-portal-auth # skip if already loaded.
|
||||
openclaw plugins enable minimax # skip if already loaded.
|
||||
openclaw gateway restart # restart if gateway is already running
|
||||
openclaw onboard --auth-choice minimax-portal
|
||||
```
|
||||
@@ -52,7 +52,7 @@ You will be prompted to select an endpoint:
|
||||
- **Global** - International users (`api.minimax.io`)
|
||||
- **CN** - Users in China (`api.minimaxi.com`)
|
||||
|
||||
See [MiniMax OAuth plugin README](https://github.com/openclaw/openclaw/tree/main/extensions/minimax-portal-auth) for details.
|
||||
See [MiniMax plugin README](https://github.com/openclaw/openclaw/tree/main/extensions/minimax) for details.
|
||||
|
||||
### MiniMax M2.5 (API key)
|
||||
|
||||
|
||||
260
docs/refactor/firecrawl-extension.md
Normal file
260
docs/refactor/firecrawl-extension.md
Normal file
@@ -0,0 +1,260 @@
|
||||
---
|
||||
summary: "Design for an opt-in Firecrawl extension that adds search/scrape value without hardwiring Firecrawl into core defaults"
|
||||
read_when:
|
||||
- Designing Firecrawl integration work
|
||||
- Evaluating web_search/web_fetch plugin seams
|
||||
- Deciding whether Firecrawl belongs in core or as an extension
|
||||
title: "Firecrawl Extension Design"
|
||||
---
|
||||
|
||||
# Firecrawl Extension Design
|
||||
|
||||
## Goal
|
||||
|
||||
Ship Firecrawl as an **opt-in extension** that adds:
|
||||
|
||||
- explicit Firecrawl tools for agents,
|
||||
- optional Firecrawl-backed `web_search` integration,
|
||||
- self-hosted support,
|
||||
- stronger security defaults than the current core fallback path,
|
||||
|
||||
without pushing Firecrawl into the default setup/onboarding path.
|
||||
|
||||
## Why this shape
|
||||
|
||||
Recent Firecrawl issues/PRs cluster into three buckets:
|
||||
|
||||
1. **Release/schema drift**
|
||||
- Several releases rejected `tools.web.fetch.firecrawl` even though docs and runtime code supported it.
|
||||
2. **Security hardening**
|
||||
- Current `fetchFirecrawlContent()` still posts to the Firecrawl endpoint with raw `fetch()`, while the main web-fetch path uses the SSRF guard.
|
||||
3. **Product pressure**
|
||||
- Users want Firecrawl-native search/scrape flows, especially for self-hosted/private setups.
|
||||
- Maintainers explicitly rejected wiring Firecrawl deeply into core defaults, setup flow, and browser behavior.
|
||||
|
||||
That combination argues for an extension, not more Firecrawl-specific logic in the default core path.
|
||||
|
||||
## Design principles
|
||||
|
||||
- **Opt-in, vendor-scoped**: no auto-enable, no setup hijack, no default tool-profile widening.
|
||||
- **Extension owns Firecrawl-specific config**: prefer plugin config over growing `tools.web.*` again.
|
||||
- **Useful on day one**: works even if core `web_search` / `web_fetch` seams stay unchanged.
|
||||
- **Security-first**: endpoint fetches use the same guarded networking posture as other web tools.
|
||||
- **Self-hosted-friendly**: config + env fallback, explicit base URL, no hosted-only assumptions.
|
||||
|
||||
## Proposed extension
|
||||
|
||||
Plugin id: `firecrawl`
|
||||
|
||||
### MVP capabilities
|
||||
|
||||
Register explicit tools:
|
||||
|
||||
- `firecrawl_search`
|
||||
- `firecrawl_scrape`
|
||||
|
||||
Optional later:
|
||||
|
||||
- `firecrawl_crawl`
|
||||
- `firecrawl_map`
|
||||
|
||||
Do **not** add Firecrawl browser automation in the first version. That was the part of PR #32543 that pulled Firecrawl too far into core behavior and raised the most maintainership concern.
|
||||
|
||||
## Config shape
|
||||
|
||||
Use plugin-scoped config:
|
||||
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
entries: {
|
||||
firecrawl: {
|
||||
enabled: true,
|
||||
config: {
|
||||
apiKey: "FIRECRAWL_API_KEY",
|
||||
baseUrl: "https://api.firecrawl.dev",
|
||||
timeoutSeconds: 60,
|
||||
maxAgeMs: 172800000,
|
||||
proxy: "auto",
|
||||
storeInCache: true,
|
||||
onlyMainContent: true,
|
||||
search: {
|
||||
enabled: true,
|
||||
defaultLimit: 5,
|
||||
sources: ["web"],
|
||||
categories: [],
|
||||
scrapeResults: false,
|
||||
},
|
||||
scrape: {
|
||||
formats: ["markdown"],
|
||||
fallbackForWebFetchLikeUse: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Credential resolution
|
||||
|
||||
Precedence:
|
||||
|
||||
1. `plugins.entries.firecrawl.config.apiKey`
|
||||
2. `FIRECRAWL_API_KEY`
|
||||
|
||||
Base URL precedence:
|
||||
|
||||
1. `plugins.entries.firecrawl.config.baseUrl`
|
||||
2. `FIRECRAWL_BASE_URL`
|
||||
3. `https://api.firecrawl.dev`
|
||||
|
||||
### Compatibility bridge
|
||||
|
||||
For the first release, the extension may also **read** existing core config at `tools.web.fetch.firecrawl.*` as a fallback source so existing users do not need to migrate immediately.
|
||||
|
||||
Write path stays plugin-local. Do not keep expanding core Firecrawl config surfaces.
|
||||
|
||||
## Tool design
|
||||
|
||||
### `firecrawl_search`
|
||||
|
||||
Inputs:
|
||||
|
||||
- `query`
|
||||
- `limit`
|
||||
- `sources`
|
||||
- `categories`
|
||||
- `scrapeResults`
|
||||
- `timeoutSeconds`
|
||||
|
||||
Behavior:
|
||||
|
||||
- Calls Firecrawl `v2/search`
|
||||
- Returns normalized OpenClaw-friendly result objects:
|
||||
- `title`
|
||||
- `url`
|
||||
- `snippet`
|
||||
- `source`
|
||||
- optional `content`
|
||||
- Wraps result content as untrusted external content
|
||||
- Cache key includes query + relevant provider params
|
||||
|
||||
Why explicit tool first:
|
||||
|
||||
- Works today without changing `tools.web.search.provider`
|
||||
- Avoids current schema/loader constraints
|
||||
- Gives users Firecrawl value immediately
|
||||
|
||||
### `firecrawl_scrape`
|
||||
|
||||
Inputs:
|
||||
|
||||
- `url`
|
||||
- `formats`
|
||||
- `onlyMainContent`
|
||||
- `maxAgeMs`
|
||||
- `proxy`
|
||||
- `storeInCache`
|
||||
- `timeoutSeconds`
|
||||
|
||||
Behavior:
|
||||
|
||||
- Calls Firecrawl `v2/scrape`
|
||||
- Returns markdown/text plus metadata:
|
||||
- `title`
|
||||
- `finalUrl`
|
||||
- `status`
|
||||
- `warning`
|
||||
- Wraps extracted content the same way `web_fetch` does
|
||||
- Shares cache semantics with web tool expectations where practical
|
||||
|
||||
Why explicit scrape tool:
|
||||
|
||||
- Sidesteps the unresolved `Readability -> Firecrawl -> basic HTML cleanup` ordering bug in core `web_fetch`
|
||||
- Gives users a deterministic “always use Firecrawl” path for JS-heavy/bot-protected sites
|
||||
|
||||
## What the extension should not do
|
||||
|
||||
- No auto-adding `browser`, `web_search`, or `web_fetch` to `tools.alsoAllow`
|
||||
- No default onboarding step in `openclaw setup`
|
||||
- No Firecrawl-specific browser session lifecycle in core
|
||||
- No change to built-in `web_fetch` fallback semantics in the extension MVP
|
||||
|
||||
## Phase plan
|
||||
|
||||
### Phase 1: extension-only, no core schema changes
|
||||
|
||||
Implement:
|
||||
|
||||
- `extensions/firecrawl/`
|
||||
- plugin config schema
|
||||
- `firecrawl_search`
|
||||
- `firecrawl_scrape`
|
||||
- tests for config resolution, endpoint selection, caching, error handling, and SSRF guard usage
|
||||
|
||||
This phase is enough to ship real user value.
|
||||
|
||||
### Phase 2: optional `web_search` provider integration
|
||||
|
||||
Support `tools.web.search.provider = "firecrawl"` only after fixing two core constraints:
|
||||
|
||||
1. `src/plugins/web-search-providers.ts` must load configured/installed web-search-provider plugins instead of a hardcoded bundled list.
|
||||
2. `src/config/types.tools.ts` and `src/config/zod-schema.agent-runtime.ts` must stop hardcoding the provider enum in a way that blocks plugin-registered ids.
|
||||
|
||||
Recommended shape:
|
||||
|
||||
- keep built-in providers documented,
|
||||
- allow any registered plugin provider id at runtime,
|
||||
- validate provider-specific config via the provider plugin or a generic provider bag.
|
||||
|
||||
### Phase 3: optional `web_fetch` provider seam
|
||||
|
||||
Do this only if maintainers want vendor-specific fetch backends to participate in `web_fetch`.
|
||||
|
||||
Needed core addition:
|
||||
|
||||
- `registerWebFetchProvider` or equivalent fetch-backend seam
|
||||
|
||||
Without that seam, the extension should keep `firecrawl_scrape` as an explicit tool rather than trying to patch built-in `web_fetch`.
|
||||
|
||||
## Security requirements
|
||||
|
||||
The extension must treat Firecrawl as a **trusted operator-configured endpoint**, but still harden transport:
|
||||
|
||||
- Use SSRF-guarded fetch for the Firecrawl endpoint call, not raw `fetch()`
|
||||
- Preserve self-hosted/private-network compatibility using the same trusted-web-tools endpoint policy used elsewhere
|
||||
- Never log the API key
|
||||
- Keep endpoint/base URL resolution explicit and predictable
|
||||
- Treat Firecrawl-returned content as untrusted external content
|
||||
|
||||
This mirrors the intent behind the SSRF hardening PRs without assuming Firecrawl is a hostile multi-tenant surface.
|
||||
|
||||
## Why not a skill
|
||||
|
||||
The repo already closed a Firecrawl skill PR in favor of ClawHub distribution. That is fine for optional user-installed prompt workflows, but it does not solve:
|
||||
|
||||
- deterministic tool availability,
|
||||
- provider-grade config/credential handling,
|
||||
- self-hosted endpoint support,
|
||||
- caching,
|
||||
- stable typed outputs,
|
||||
- security review on network behavior.
|
||||
|
||||
This belongs as an extension, not a prompt-only skill.
|
||||
|
||||
## Success criteria
|
||||
|
||||
- Users can install/enable one extension and get reliable Firecrawl search/scrape without touching core defaults.
|
||||
- Self-hosted Firecrawl works with config/env fallback.
|
||||
- Extension endpoint fetches use guarded networking.
|
||||
- No new Firecrawl-specific core onboarding/default behavior.
|
||||
- Core can later adopt plugin-native `web_search` / `web_fetch` seams without redesigning the extension.
|
||||
|
||||
## Recommended implementation order
|
||||
|
||||
1. Build `firecrawl_scrape`
|
||||
2. Build `firecrawl_search`
|
||||
3. Add docs and examples
|
||||
4. If desired, generalize `web_search` provider loading so the extension can back `web_search`
|
||||
5. Only then consider a true `web_fetch` provider seam
|
||||
@@ -28,7 +28,7 @@ Contents (examples):
|
||||
- Config helpers: `buildChannelConfigSchema`, `setAccountEnabledInConfigSection`, `deleteAccountFromConfigSection`,
|
||||
`applyAccountNameToChannelSection`.
|
||||
- Pairing helpers: `PAIRING_APPROVED_MESSAGE`, `formatPairingApproveHint`.
|
||||
- Onboarding helpers: `promptChannelAccessConfig`, `addWildcardAllowFrom`, onboarding types.
|
||||
- Setup entry points: host-owned `setup` + `setupWizard`; avoid broad public onboarding helpers.
|
||||
- Tool param helpers: `createActionGate`, `readStringParam`, `readNumberParam`, `readReactionParams`, `jsonResult`.
|
||||
- Docs link helper: `formatDocsLink`.
|
||||
|
||||
|
||||
@@ -1,161 +1,42 @@
|
||||
---
|
||||
title: "Release Checklist"
|
||||
summary: "Step-by-step release checklist for npm + macOS app"
|
||||
title: "Release Policy"
|
||||
summary: "Public release channels, version naming, and cadence"
|
||||
read_when:
|
||||
- Cutting a new npm release
|
||||
- Cutting a new macOS app release
|
||||
- Verifying metadata before publishing
|
||||
- Looking for public release channel definitions
|
||||
- Looking for version naming and cadence
|
||||
---
|
||||
|
||||
# Release Checklist (npm + macOS)
|
||||
# Release Policy
|
||||
|
||||
Use `pnpm` from the repo root with Node 24 by default. Node 22 LTS, currently `22.16+`, remains supported for compatibility. Keep the working tree clean before tagging/publishing.
|
||||
OpenClaw has three public release lanes:
|
||||
|
||||
## Operator trigger
|
||||
- stable: tagged releases that publish to npm `latest`
|
||||
- beta: prerelease tags that publish to npm `beta`
|
||||
- dev: the moving head of `main`
|
||||
|
||||
When the operator says “release”, immediately do this preflight (no extra questions unless blocked):
|
||||
|
||||
- Read this doc and `docs/platforms/mac/release.md`.
|
||||
- Load env from `~/.profile` and confirm `SPARKLE_PRIVATE_KEY_FILE` + App Store Connect vars are set (SPARKLE_PRIVATE_KEY_FILE should live in `~/.profile`).
|
||||
- Use Sparkle keys from `~/Library/CloudStorage/Dropbox/Backup/Sparkle` if needed.
|
||||
|
||||
## Versioning
|
||||
|
||||
Current OpenClaw releases use date-based versioning.
|
||||
## Version naming
|
||||
|
||||
- Stable release version: `YYYY.M.D`
|
||||
- Git tag: `vYYYY.M.D`
|
||||
- Examples from repo history: `v2026.2.26`, `v2026.3.8`
|
||||
- Beta prerelease version: `YYYY.M.D-beta.N`
|
||||
- Git tag: `vYYYY.M.D-beta.N`
|
||||
- Examples from repo history: `v2026.2.15-beta.1`, `v2026.3.8-beta.1`
|
||||
- Fallback correction tag: `vYYYY.M.D-N`
|
||||
- Use only as a last-resort recovery tag when a published immutable release burned the original stable tag and you cannot reuse it.
|
||||
- The npm package version stays `YYYY.M.D`; the `-N` suffix is only for the git tag and GitHub release.
|
||||
- Prefer betas for normal pre-release iteration, then cut a clean stable tag once ready.
|
||||
- Use the same version string everywhere, minus the leading `v` where Git tags are not used:
|
||||
- `package.json`: `2026.3.8`
|
||||
- Git tag: `v2026.3.8`
|
||||
- GitHub release title: `openclaw 2026.3.8`
|
||||
- Do not zero-pad month or day. Use `2026.3.8`, not `2026.03.08`.
|
||||
- Stable and beta are npm dist-tags, not separate release lines:
|
||||
- `latest` = stable
|
||||
- `beta` = prerelease/testing
|
||||
- Dev is the moving head of `main`, not a normal git-tagged release.
|
||||
- The tag-triggered preview run accepts stable, beta, and fallback correction tags, and rejects versions whose CalVer date is more than 2 UTC calendar days away from the release date.
|
||||
- Do not zero-pad month or day
|
||||
- `latest` means the current stable npm release
|
||||
- `beta` means the current prerelease npm release
|
||||
- Beta releases may ship before the macOS app catches up
|
||||
|
||||
Historical note:
|
||||
## Release cadence
|
||||
|
||||
- Older tags such as `v2026.1.11-1`, `v2026.2.6-3`, and `v2.0.0-beta2` exist in repo history.
|
||||
- Treat correction tags as a fallback-only escape hatch. New releases should still use `vYYYY.M.D` for stable and `vYYYY.M.D-beta.N` for beta.
|
||||
- Releases move beta-first
|
||||
- Stable follows only after the latest beta is validated
|
||||
- Detailed release procedure, approvals, credentials, and recovery notes are
|
||||
maintainer-only
|
||||
|
||||
1. **Version & metadata**
|
||||
## Public references
|
||||
|
||||
- [ ] Bump `package.json` version (e.g., `2026.1.29`).
|
||||
- [ ] Run `pnpm plugins:sync` to align extension package versions + changelogs.
|
||||
- [ ] Update CLI/version strings in [`src/version.ts`](https://github.com/openclaw/openclaw/blob/main/src/version.ts) and the Baileys user agent in [`src/web/session.ts`](https://github.com/openclaw/openclaw/blob/main/src/web/session.ts).
|
||||
- [ ] Confirm package metadata (name, description, repository, keywords, license) and `bin` map points to [`openclaw.mjs`](https://github.com/openclaw/openclaw/blob/main/openclaw.mjs) for `openclaw`.
|
||||
- [ ] If dependencies changed, run `pnpm install` so `pnpm-lock.yaml` is current.
|
||||
- [`.github/workflows/openclaw-npm-release.yml`](https://github.com/openclaw/openclaw/blob/main/.github/workflows/openclaw-npm-release.yml)
|
||||
- [`scripts/openclaw-npm-release-check.ts`](https://github.com/openclaw/openclaw/blob/main/scripts/openclaw-npm-release-check.ts)
|
||||
|
||||
2. **Build & artifacts**
|
||||
|
||||
- [ ] If A2UI inputs changed, run `pnpm canvas:a2ui:bundle` and commit any updated [`src/canvas-host/a2ui/a2ui.bundle.js`](https://github.com/openclaw/openclaw/blob/main/src/canvas-host/a2ui/a2ui.bundle.js).
|
||||
- [ ] `pnpm run build` (regenerates `dist/`).
|
||||
- [ ] Verify npm package `files` includes all required `dist/*` folders (notably `dist/node-host/**` and `dist/acp/**` for headless node + ACP CLI).
|
||||
- [ ] Confirm `dist/build-info.json` exists and includes the expected `commit` hash (CLI banner uses this for npm installs).
|
||||
- [ ] Optional: `npm pack --pack-destination /tmp` after the build; inspect the tarball contents and keep it handy for the GitHub release (do **not** commit it).
|
||||
|
||||
3. **Changelog & docs**
|
||||
|
||||
- [ ] Update `CHANGELOG.md` with user-facing highlights (create the file if missing); keep entries strictly descending by version.
|
||||
- [ ] Ensure README examples/flags match current CLI behavior (notably new commands or options).
|
||||
|
||||
4. **Validation**
|
||||
|
||||
- [ ] `pnpm build`
|
||||
- [ ] `pnpm check`
|
||||
- [ ] `pnpm test` (or `pnpm test:coverage` if you need coverage output)
|
||||
- [ ] `pnpm release:check` (verifies npm pack contents)
|
||||
- [ ] If `pnpm config:docs:check` fails as part of release validation and the config-surface change is intentional, run `pnpm config:docs:gen`, review `docs/.generated/config-baseline.json` and `docs/.generated/config-baseline.jsonl`, commit the updated baselines, then rerun `pnpm release:check`.
|
||||
- [ ] `OPENCLAW_INSTALL_SMOKE_SKIP_NONROOT=1 pnpm test:install:smoke` (Docker install smoke test, fast path; required before release)
|
||||
- If the immediate previous npm release is known broken, set `OPENCLAW_INSTALL_SMOKE_PREVIOUS=<last-good-version>` or `OPENCLAW_INSTALL_SMOKE_SKIP_PREVIOUS=1` for the preinstall step.
|
||||
- [ ] (Optional) Full installer smoke (adds non-root + CLI coverage): `pnpm test:install:smoke`
|
||||
- [ ] (Optional) Installer E2E (Docker, runs `curl -fsSL https://openclaw.ai/install.sh | bash`, onboards, then runs real tool calls):
|
||||
- `pnpm test:install:e2e:openai` (requires `OPENAI_API_KEY`)
|
||||
- `pnpm test:install:e2e:anthropic` (requires `ANTHROPIC_API_KEY`)
|
||||
- `pnpm test:install:e2e` (requires both keys; runs both providers)
|
||||
- [ ] (Optional) Spot-check the web gateway if your changes affect send/receive paths.
|
||||
|
||||
5. **macOS app (Sparkle)**
|
||||
|
||||
- [ ] Build + sign the macOS app, then zip it for distribution.
|
||||
- [ ] Generate the Sparkle appcast (HTML notes via [`scripts/make_appcast.sh`](https://github.com/openclaw/openclaw/blob/main/scripts/make_appcast.sh)) and update `appcast.xml`.
|
||||
- [ ] Keep the app zip (and optional dSYM zip) ready to attach to the GitHub release.
|
||||
- [ ] Follow [macOS release](/platforms/mac/release) for the exact commands and required env vars.
|
||||
- `APP_BUILD` must be numeric + monotonic (no `-beta`) so Sparkle compares versions correctly.
|
||||
- If notarizing, use the `openclaw-notary` keychain profile created from App Store Connect API env vars (see [macOS release](/platforms/mac/release)).
|
||||
|
||||
6. **Publish (npm)**
|
||||
|
||||
- [ ] Confirm git status is clean; commit and push as needed.
|
||||
- [ ] Confirm npm trusted publishing is configured for the `openclaw` package.
|
||||
- [ ] Do not rely on an `NPM_TOKEN` secret for this workflow; the publish job uses GitHub OIDC trusted publishing.
|
||||
- [ ] Push the matching git tag to trigger the preview run in `.github/workflows/openclaw-npm-release.yml`.
|
||||
- [ ] Run `OpenClaw NPM Release` manually with the same tag to publish after `npm-release` environment approval.
|
||||
- Stable tags publish to npm `latest`.
|
||||
- Beta tags publish to npm `beta`.
|
||||
- Fallback correction tags like `v2026.3.13-1` map to npm version `2026.3.13`.
|
||||
- Both the preview run and the manual publish run reject tags that do not map back to `package.json`, are not on `main`, or whose CalVer date is more than 2 UTC calendar days away from the release date.
|
||||
- If `openclaw@YYYY.M.D` is already published, a fallback correction tag is still useful for GitHub release and Docker recovery, but npm publish will not republish that version.
|
||||
- [ ] Verify the registry: `npm view openclaw version`, `npm view openclaw dist-tags`, and `npx -y openclaw@X.Y.Z --version` (or `--help`).
|
||||
|
||||
### Troubleshooting (notes from 2.0.0-beta2 release)
|
||||
|
||||
- **npm pack/publish hangs or produces huge tarball**: the macOS app bundle in `dist/OpenClaw.app` (and release zips) get swept into the package. Fix by whitelisting publish contents via `package.json` `files` (include dist subdirs, docs, skills; exclude app bundles). Confirm with `npm pack --dry-run` that `dist/OpenClaw.app` is not listed.
|
||||
- **npm auth web loop for dist-tags**: use legacy auth to get an OTP prompt:
|
||||
- `NPM_CONFIG_AUTH_TYPE=legacy npm dist-tag add openclaw@X.Y.Z latest`
|
||||
- **`npx` verification fails with `ECOMPROMISED: Lock compromised`**: retry with a fresh cache:
|
||||
- `NPM_CONFIG_CACHE=/tmp/npm-cache-$(date +%s) npx -y openclaw@X.Y.Z --version`
|
||||
- **Tag needs recovery after a late fix**: if the original stable tag is tied to an immutable GitHub release, mint a fallback correction tag like `vX.Y.Z-1` instead of trying to force-update `vX.Y.Z`.
|
||||
- Keep the npm package version at `X.Y.Z`; the correction suffix is for the git tag and GitHub release only.
|
||||
- Use this only as a last resort. For normal iteration, prefer beta tags and then cut a clean stable release.
|
||||
|
||||
7. **GitHub release + appcast**
|
||||
|
||||
- [ ] Tag and push: `git tag vX.Y.Z && git push origin vX.Y.Z` (or `git push --tags`).
|
||||
- Pushing the tag also triggers the npm release workflow.
|
||||
- [ ] Create/refresh the GitHub release for `vX.Y.Z` with **title `openclaw X.Y.Z`** (not just the tag); body should include the **full** changelog section for that version (Highlights + Changes + Fixes), inline (no bare links), and **must not repeat the title inside the body**.
|
||||
- [ ] Attach artifacts: `npm pack` tarball (optional), `OpenClaw-X.Y.Z.zip`, and `OpenClaw-X.Y.Z.dSYM.zip` (if generated).
|
||||
- [ ] Commit the updated `appcast.xml` and push it (Sparkle feeds from main).
|
||||
- [ ] From a clean temp directory (no `package.json`), run `npx -y openclaw@X.Y.Z send --help` to confirm install/CLI entrypoints work.
|
||||
- [ ] Announce/share release notes.
|
||||
|
||||
## Plugin publish scope (npm)
|
||||
|
||||
We only publish **existing npm plugins** under the `@openclaw/*` scope. Bundled
|
||||
plugins that are not on npm stay **disk-tree only** (still shipped in
|
||||
`extensions/**`).
|
||||
|
||||
Process to derive the list:
|
||||
|
||||
1. `npm search @openclaw --json` and capture the package names.
|
||||
2. Compare with `extensions/*/package.json` names.
|
||||
3. Publish only the **intersection** (already on npm).
|
||||
|
||||
Current npm plugin list (update as needed):
|
||||
|
||||
- @openclaw/bluebubbles
|
||||
- @openclaw/diagnostics-otel
|
||||
- @openclaw/discord
|
||||
- @openclaw/feishu
|
||||
- @openclaw/lobster
|
||||
- @openclaw/matrix
|
||||
- @openclaw/msteams
|
||||
- @openclaw/nextcloud-talk
|
||||
- @openclaw/nostr
|
||||
- @openclaw/voice-call
|
||||
- @openclaw/zalo
|
||||
- @openclaw/zalouser
|
||||
|
||||
Release notes must also call out **new optional bundled plugins** that are **not
|
||||
on by default** (example: `tlon`).
|
||||
Maintainers use the private release docs in
|
||||
[`openclaw/maintainers/release/README.md`](https://github.com/openclaw/maintainers/blob/main/release/README.md)
|
||||
for the actual runbook.
|
||||
|
||||
@@ -157,7 +157,6 @@ Use these hubs to discover every page, including deep dives and reference docs t
|
||||
- [macOS permissions](/platforms/mac/permissions)
|
||||
- [macOS remote](/platforms/mac/remote)
|
||||
- [macOS signing](/platforms/mac/signing)
|
||||
- [macOS release](/platforms/mac/release)
|
||||
- [macOS gateway (launchd)](/platforms/mac/bundled-gateway)
|
||||
- [macOS XPC](/platforms/mac/xpc)
|
||||
- [macOS skills](/platforms/mac/skills)
|
||||
@@ -190,5 +189,5 @@ Use these hubs to discover every page, including deep dives and reference docs t
|
||||
## Testing + release
|
||||
|
||||
- [Testing](/reference/test)
|
||||
- [Release checklist](/reference/RELEASING)
|
||||
- [Release policy](/reference/RELEASING)
|
||||
- [Device models](/reference/device-models)
|
||||
|
||||
@@ -96,7 +96,8 @@ pnpm install
|
||||
pnpm gateway:watch
|
||||
```
|
||||
|
||||
`gateway:watch` runs the gateway in watch mode and reloads on TypeScript changes.
|
||||
`gateway:watch` runs the gateway in watch mode and reloads on relevant source,
|
||||
config, and bundled-plugin metadata changes.
|
||||
|
||||
### 2) Point the macOS app at your running Gateway
|
||||
|
||||
|
||||
@@ -114,6 +114,7 @@ Notes:
|
||||
- `remoteCdpTimeoutMs` applies to remote (non-loopback) CDP reachability checks.
|
||||
- `remoteCdpHandshakeTimeoutMs` applies to remote CDP WebSocket reachability checks.
|
||||
- Browser navigation/open-tab is SSRF-guarded before navigation and best-effort re-checked on final `http(s)` URL after navigation.
|
||||
- In strict SSRF mode, remote CDP endpoint discovery/probes (`cdpUrl`, including `/json/version` lookups) are checked too.
|
||||
- `browser.ssrfPolicy.dangerouslyAllowPrivateNetwork` defaults to `true` (trusted-network model). Set it to `false` for strict public-only browsing.
|
||||
- `browser.ssrfPolicy.allowPrivateNetwork` remains supported as a legacy alias for compatibility.
|
||||
- `attachOnly: true` means “never launch a local browser; only attach if it is already running.”
|
||||
|
||||
@@ -1,27 +1,71 @@
|
||||
---
|
||||
summary: "Firecrawl fallback for web_fetch (anti-bot + cached extraction)"
|
||||
summary: "Firecrawl search, scrape, and web_fetch fallback"
|
||||
read_when:
|
||||
- You want Firecrawl-backed web extraction
|
||||
- You need a Firecrawl API key
|
||||
- You want Firecrawl as a web_search provider
|
||||
- You want anti-bot extraction for web_fetch
|
||||
title: "Firecrawl"
|
||||
---
|
||||
|
||||
# Firecrawl
|
||||
|
||||
OpenClaw can use **Firecrawl** as a fallback extractor for `web_fetch`. It is a hosted
|
||||
content extraction service that supports bot circumvention and caching, which helps
|
||||
with JS-heavy sites or pages that block plain HTTP fetches.
|
||||
OpenClaw can use **Firecrawl** in three ways:
|
||||
|
||||
- as the `web_search` provider
|
||||
- as explicit plugin tools: `firecrawl_search` and `firecrawl_scrape`
|
||||
- as a fallback extractor for `web_fetch`
|
||||
|
||||
It is a hosted extraction/search service that supports bot circumvention and caching,
|
||||
which helps with JS-heavy sites or pages that block plain HTTP fetches.
|
||||
|
||||
## Get an API key
|
||||
|
||||
1. Create a Firecrawl account and generate an API key.
|
||||
2. Store it in config or set `FIRECRAWL_API_KEY` in the gateway environment.
|
||||
|
||||
## Configure Firecrawl
|
||||
## Configure Firecrawl search
|
||||
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
entries: {
|
||||
firecrawl: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
web: {
|
||||
search: {
|
||||
provider: "firecrawl",
|
||||
firecrawl: {
|
||||
apiKey: "FIRECRAWL_API_KEY_HERE",
|
||||
baseUrl: "https://api.firecrawl.dev",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- Choosing Firecrawl in onboarding or `openclaw configure --section web` enables the bundled Firecrawl plugin automatically.
|
||||
- `web_search` with Firecrawl supports `query` and `count`.
|
||||
- For Firecrawl-specific controls like `sources`, `categories`, or result scraping, use `firecrawl_search`.
|
||||
|
||||
## Configure Firecrawl scrape + web_fetch fallback
|
||||
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
entries: {
|
||||
firecrawl: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
web: {
|
||||
fetch: {
|
||||
@@ -44,6 +88,38 @@ Notes:
|
||||
- Firecrawl fallback attempts run only when an API key is available (`tools.web.fetch.firecrawl.apiKey` or `FIRECRAWL_API_KEY`).
|
||||
- `maxAgeMs` controls how old cached results can be (ms). Default is 2 days.
|
||||
|
||||
`firecrawl_scrape` reuses the same `tools.web.fetch.firecrawl.*` settings and env vars.
|
||||
|
||||
## Firecrawl plugin tools
|
||||
|
||||
### `firecrawl_search`
|
||||
|
||||
Use this when you want Firecrawl-specific search controls instead of generic `web_search`.
|
||||
|
||||
Core parameters:
|
||||
|
||||
- `query`
|
||||
- `count`
|
||||
- `sources`
|
||||
- `categories`
|
||||
- `scrapeResults`
|
||||
- `timeoutSeconds`
|
||||
|
||||
### `firecrawl_scrape`
|
||||
|
||||
Use this for JS-heavy or bot-protected pages where plain `web_fetch` is weak.
|
||||
|
||||
Core parameters:
|
||||
|
||||
- `url`
|
||||
- `extractMode`
|
||||
- `maxChars`
|
||||
- `onlyMainContent`
|
||||
- `maxAgeMs`
|
||||
- `proxy`
|
||||
- `storeInCache`
|
||||
- `timeoutSeconds`
|
||||
|
||||
## Stealth / bot circumvention
|
||||
|
||||
Firecrawl exposes a **proxy mode** parameter for bot circumvention (`basic`, `stealth`, or `auto`).
|
||||
|
||||
@@ -256,7 +256,7 @@ Enable with `tools.loopDetection.enabled: true` (default is `false`).
|
||||
|
||||
### `web_search`
|
||||
|
||||
Search the web using Perplexity, Brave, Gemini, Grok, or Kimi.
|
||||
Search the web using Brave, Firecrawl, Gemini, Grok, Kimi, or Perplexity.
|
||||
|
||||
Core parameters:
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ summary: "OpenClaw plugins/extensions: discovery, config, and safety"
|
||||
read_when:
|
||||
- Adding or modifying plugins/extensions
|
||||
- Documenting plugin install or load rules
|
||||
- Working with Codex/Claude-compatible plugin bundles
|
||||
title: "Plugins"
|
||||
---
|
||||
|
||||
@@ -10,8 +11,13 @@ title: "Plugins"
|
||||
|
||||
## Quick start (new to plugins?)
|
||||
|
||||
A plugin is just a **small code module** that extends OpenClaw with extra
|
||||
features (commands, tools, and Gateway RPC).
|
||||
A plugin is either:
|
||||
|
||||
- a native **OpenClaw plugin** (`openclaw.plugin.json` + runtime module), or
|
||||
- a compatible **bundle** (`.codex-plugin/plugin.json` or `.claude-plugin/plugin.json`)
|
||||
|
||||
Both show up under `openclaw plugins`, but only native OpenClaw plugins execute
|
||||
runtime code in-process.
|
||||
|
||||
Most of the time, you’ll use plugins when you want a feature that’s not built
|
||||
into core OpenClaw yet (or you want to keep optional features out of your main
|
||||
@@ -42,6 +48,14 @@ prerelease tag such as `@beta`/`@rc` or an exact prerelease version.
|
||||
|
||||
See [Voice Call](/plugins/voice-call) for a concrete example plugin.
|
||||
Looking for third-party listings? See [Community plugins](/plugins/community).
|
||||
Need the bundle compatibility details? See [Plugin bundles](/plugins/bundles).
|
||||
|
||||
For compatible bundles, install from a local directory or archive:
|
||||
|
||||
```bash
|
||||
openclaw plugins install ./my-bundle
|
||||
openclaw plugins install ./my-bundle.tgz
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -49,14 +63,15 @@ OpenClaw's plugin system has four layers:
|
||||
|
||||
1. **Manifest + discovery**
|
||||
OpenClaw finds candidate plugins from configured paths, workspace roots,
|
||||
global extension roots, and bundled extensions. Discovery reads
|
||||
`openclaw.plugin.json` plus package metadata first.
|
||||
global extension roots, and bundled extensions. Discovery reads native
|
||||
`openclaw.plugin.json` manifests plus supported bundle manifests first.
|
||||
2. **Enablement + validation**
|
||||
Core decides whether a discovered plugin is enabled, disabled, blocked, or
|
||||
selected for an exclusive slot such as memory.
|
||||
3. **Runtime loading**
|
||||
Enabled plugins are loaded in-process via jiti and register capabilities into
|
||||
a central registry.
|
||||
Native OpenClaw plugins are loaded in-process via jiti and register
|
||||
capabilities into a central registry. Compatible bundles are normalized into
|
||||
registry records without importing runtime code.
|
||||
4. **Surface consumption**
|
||||
The rest of OpenClaw reads the registry to expose tools, channels, provider
|
||||
setup, hooks, HTTP routes, CLI commands, and services.
|
||||
@@ -65,22 +80,68 @@ The important design boundary:
|
||||
|
||||
- discovery + config validation should work from **manifest/schema metadata**
|
||||
without executing plugin code
|
||||
- runtime behavior comes from the plugin module's `register(api)` path
|
||||
- native runtime behavior comes from the plugin module's `register(api)` path
|
||||
|
||||
That split lets OpenClaw validate config, explain missing/disabled plugins, and
|
||||
build UI/schema hints before the full runtime is active.
|
||||
|
||||
## Compatible bundles
|
||||
|
||||
OpenClaw also recognizes two compatible external bundle layouts:
|
||||
|
||||
- Codex-style bundles: `.codex-plugin/plugin.json`
|
||||
- Claude-style bundles: `.claude-plugin/plugin.json` or the default Claude
|
||||
component layout without a manifest
|
||||
- Cursor-style bundles: `.cursor-plugin/plugin.json`
|
||||
|
||||
They are shown in the plugin list as `format=bundle`, with a subtype of
|
||||
`codex` or `claude` in verbose/info output.
|
||||
|
||||
See [Plugin bundles](/plugins/bundles) for the exact detection rules, mapping
|
||||
behavior, and current support matrix.
|
||||
|
||||
Today, OpenClaw treats these as **capability packs**, not native runtime
|
||||
plugins:
|
||||
|
||||
- supported now: bundled `skills`
|
||||
- supported now: Claude `commands/` markdown roots, mapped into the normal
|
||||
OpenClaw skill loader
|
||||
- supported now: Claude bundle `settings.json` defaults for embedded Pi agent
|
||||
settings (with shell override keys sanitized)
|
||||
- supported now: Cursor `.cursor/commands/*.md` roots, mapped into the normal
|
||||
OpenClaw skill loader
|
||||
- supported now: Codex bundle hook directories that use the OpenClaw hook-pack
|
||||
layout (`HOOK.md` + `handler.ts`/`handler.js`)
|
||||
- detected but not wired yet: other declared bundle capabilities such as
|
||||
agents, Claude hook automation, Cursor rules/hooks/MCP metadata, MCP/app/LSP
|
||||
metadata, output styles
|
||||
|
||||
That means bundle install/discovery/list/info/enablement all work, and bundle
|
||||
skills, Claude command-skills, Claude bundle settings defaults, and compatible
|
||||
Codex hook directories load when the bundle is enabled, but bundle runtime code
|
||||
is not executed in-process.
|
||||
|
||||
Bundle hook support is limited to the normal OpenClaw hook directory format
|
||||
(`HOOK.md` plus `handler.ts`/`handler.js` under the declared hook roots).
|
||||
Vendor-specific shell/JSON hook runtimes, including Claude `hooks.json`, are
|
||||
only detected today and are not executed directly.
|
||||
|
||||
## Execution model
|
||||
|
||||
Plugins run **in-process** with the Gateway. They are not sandboxed. A loaded
|
||||
plugin has the same process-level trust boundary as core code.
|
||||
Native OpenClaw plugins run **in-process** with the Gateway. They are not
|
||||
sandboxed. A loaded native plugin has the same process-level trust boundary as
|
||||
core code.
|
||||
|
||||
Implications:
|
||||
|
||||
- a plugin can register tools, network handlers, hooks, and services
|
||||
- a plugin bug can crash or destabilize the gateway
|
||||
- a malicious plugin is equivalent to arbitrary code execution inside the
|
||||
OpenClaw process
|
||||
- a native plugin can register tools, network handlers, hooks, and services
|
||||
- a native plugin bug can crash or destabilize the gateway
|
||||
- a malicious native plugin is equivalent to arbitrary code execution inside
|
||||
the OpenClaw process
|
||||
|
||||
Compatible bundles are safer by default because OpenClaw currently treats them
|
||||
as metadata/content packs. In current releases, that mostly means bundled
|
||||
skills.
|
||||
|
||||
Use allowlists and explicit install/load paths for non-bundled plugins. Treat
|
||||
workspace plugins as development-time code, not production defaults.
|
||||
@@ -103,16 +164,39 @@ Important trust note:
|
||||
- [Nostr](/channels/nostr) — `@openclaw/nostr`
|
||||
- [Zalo](/channels/zalo) — `@openclaw/zalo`
|
||||
- [Microsoft Teams](/channels/msteams) — `@openclaw/msteams`
|
||||
- Google Antigravity OAuth (provider auth) — bundled as `google-antigravity-auth` (disabled by default)
|
||||
- Gemini CLI OAuth (provider auth) — bundled as `google-gemini-cli-auth` (disabled by default)
|
||||
- Qwen OAuth (provider auth) — bundled as `qwen-portal-auth` (disabled by default)
|
||||
- Anthropic provider runtime — bundled as `anthropic` (enabled by default)
|
||||
- BytePlus provider catalog — bundled as `byteplus` (enabled by default)
|
||||
- Cloudflare AI Gateway provider catalog — bundled as `cloudflare-ai-gateway` (enabled by default)
|
||||
- Google web search + Gemini CLI OAuth — bundled as `google` (web search auto-loads it; provider auth stays opt-in)
|
||||
- GitHub Copilot provider runtime — bundled as `github-copilot` (enabled by default)
|
||||
- Hugging Face provider catalog — bundled as `huggingface` (enabled by default)
|
||||
- Kilo Gateway provider runtime — bundled as `kilocode` (enabled by default)
|
||||
- Kimi Coding provider catalog — bundled as `kimi-coding` (enabled by default)
|
||||
- MiniMax provider catalog + usage + OAuth — bundled as `minimax` (enabled by default; owns `minimax` and `minimax-portal`)
|
||||
- Mistral provider capabilities — bundled as `mistral` (enabled by default)
|
||||
- Model Studio provider catalog — bundled as `modelstudio` (enabled by default)
|
||||
- Moonshot provider runtime — bundled as `moonshot` (enabled by default)
|
||||
- NVIDIA provider catalog — bundled as `nvidia` (enabled by default)
|
||||
- OpenAI provider runtime — bundled as `openai` (enabled by default; owns both `openai` and `openai-codex`)
|
||||
- OpenCode Go provider capabilities — bundled as `opencode-go` (enabled by default)
|
||||
- OpenCode Zen provider capabilities — bundled as `opencode` (enabled by default)
|
||||
- OpenRouter provider runtime — bundled as `openrouter` (enabled by default)
|
||||
- Qianfan provider catalog — bundled as `qianfan` (enabled by default)
|
||||
- Qwen OAuth (provider auth + catalog) — bundled as `qwen-portal-auth` (enabled by default)
|
||||
- Synthetic provider catalog — bundled as `synthetic` (enabled by default)
|
||||
- Together provider catalog — bundled as `together` (enabled by default)
|
||||
- Venice provider catalog — bundled as `venice` (enabled by default)
|
||||
- Vercel AI Gateway provider catalog — bundled as `vercel-ai-gateway` (enabled by default)
|
||||
- Volcengine provider catalog — bundled as `volcengine` (enabled by default)
|
||||
- Xiaomi provider catalog + usage — bundled as `xiaomi` (enabled by default)
|
||||
- Z.AI provider runtime — bundled as `zai` (enabled by default)
|
||||
- Copilot Proxy (provider auth) — local VS Code Copilot Proxy bridge; distinct from built-in `github-copilot` device login (bundled, disabled by default)
|
||||
|
||||
OpenClaw plugins are **TypeScript modules** loaded at runtime via jiti. **Config
|
||||
validation does not execute plugin code**; it uses the plugin manifest and JSON
|
||||
Schema instead. See [Plugin manifest](/plugins/manifest).
|
||||
Native OpenClaw plugins are **TypeScript modules** loaded at runtime via jiti.
|
||||
**Config validation does not execute plugin code**; it uses the plugin manifest
|
||||
and JSON Schema instead. See [Plugin manifest](/plugins/manifest).
|
||||
|
||||
Plugins can register:
|
||||
Native OpenClaw plugins can register:
|
||||
|
||||
- Gateway RPC methods
|
||||
- Gateway HTTP routes
|
||||
@@ -120,25 +204,229 @@ Plugins can register:
|
||||
- CLI commands
|
||||
- Background services
|
||||
- Context engines
|
||||
- Provider auth flows and model catalogs
|
||||
- Provider runtime hooks for dynamic model ids, transport normalization, capability metadata, stream wrapping, cache TTL policy, missing-auth hints, built-in model suppression, catalog augmentation, runtime auth exchange, and usage/billing auth + snapshot resolution
|
||||
- Optional config validation
|
||||
- **Skills** (by listing `skills` directories in the plugin manifest)
|
||||
- **Auto-reply commands** (execute without invoking the AI agent)
|
||||
|
||||
Plugins run **in‑process** with the Gateway, so treat them as trusted code.
|
||||
Native OpenClaw plugins run **in‑process** with the Gateway, so treat them as trusted code.
|
||||
Tool authoring guide: [Plugin agent tools](/plugins/agent-tools).
|
||||
|
||||
## Provider runtime hooks
|
||||
|
||||
Provider plugins now have two layers:
|
||||
|
||||
- manifest metadata: `providerAuthEnvVars` for cheap env-auth lookup before
|
||||
runtime load
|
||||
- config-time hooks: `catalog` / legacy `discovery`
|
||||
- runtime hooks: `resolveDynamicModel`, `prepareDynamicModel`, `normalizeResolvedModel`, `capabilities`, `prepareExtraParams`, `wrapStreamFn`, `isCacheTtlEligible`, `buildMissingAuthMessage`, `suppressBuiltInModel`, `augmentModelCatalog`, `prepareRuntimeAuth`, `resolveUsageAuth`, `fetchUsageSnapshot`
|
||||
|
||||
OpenClaw still owns the generic agent loop, failover, transcript handling, and
|
||||
tool policy. These hooks are the seam for provider-specific behavior without
|
||||
needing a whole custom inference transport.
|
||||
|
||||
Use manifest `providerAuthEnvVars` when the provider has env-based credentials
|
||||
that generic auth/status/model-picker paths should see without loading plugin
|
||||
runtime. Keep provider runtime `envVars` for operator-facing hints such as
|
||||
onboarding labels or OAuth client-id/client-secret setup vars.
|
||||
|
||||
### Hook order
|
||||
|
||||
For model/provider plugins, OpenClaw uses hooks in this rough order:
|
||||
|
||||
1. `catalog`
|
||||
Publish provider config into `models.providers` during `models.json`
|
||||
generation.
|
||||
2. built-in/discovered model lookup
|
||||
OpenClaw tries the normal registry/catalog path first.
|
||||
3. `resolveDynamicModel`
|
||||
Sync fallback for provider-owned model ids that are not in the local
|
||||
registry yet.
|
||||
4. `prepareDynamicModel`
|
||||
Async warm-up only on async model resolution paths, then
|
||||
`resolveDynamicModel` runs again.
|
||||
5. `normalizeResolvedModel`
|
||||
Final rewrite before the embedded runner uses the resolved model.
|
||||
6. `capabilities`
|
||||
Provider-owned transcript/tooling metadata used by shared core logic.
|
||||
7. `prepareExtraParams`
|
||||
Provider-owned request-param normalization before generic stream option wrappers.
|
||||
8. `wrapStreamFn`
|
||||
Provider-owned stream wrapper after generic wrappers are applied.
|
||||
9. `isCacheTtlEligible`
|
||||
Provider-owned prompt-cache policy for proxy/backhaul providers.
|
||||
10. `buildMissingAuthMessage`
|
||||
Provider-owned replacement for the generic missing-auth recovery message.
|
||||
11. `suppressBuiltInModel`
|
||||
Provider-owned stale upstream model suppression plus optional user-facing
|
||||
error hint.
|
||||
12. `augmentModelCatalog`
|
||||
Provider-owned synthetic/final catalog rows appended after discovery.
|
||||
13. `prepareRuntimeAuth`
|
||||
Exchanges a configured credential into the actual runtime token/key just
|
||||
before inference.
|
||||
14. `resolveUsageAuth`
|
||||
Resolves usage/billing credentials for `/usage` and related status
|
||||
surfaces.
|
||||
15. `fetchUsageSnapshot`
|
||||
Fetches and normalizes provider-specific usage/quota snapshots after auth
|
||||
is resolved.
|
||||
|
||||
### Which hook to use
|
||||
|
||||
- `catalog`: publish provider config and model catalogs into `models.providers`
|
||||
- `resolveDynamicModel`: handle pass-through or forward-compat model ids that are not in the local registry yet
|
||||
- `prepareDynamicModel`: async warm-up before retrying dynamic resolution (for example refresh provider metadata cache)
|
||||
- `normalizeResolvedModel`: rewrite a resolved model's transport/base URL/compat before inference
|
||||
- `capabilities`: publish provider-family and transcript/tooling quirks without hardcoding provider ids in core
|
||||
- `prepareExtraParams`: set provider defaults or normalize provider-specific per-model params before generic stream wrapping
|
||||
- `wrapStreamFn`: add provider-specific headers/payload/model compat patches while still using the normal `pi-ai` execution path
|
||||
- `isCacheTtlEligible`: decide whether provider/model pairs should use cache TTL metadata
|
||||
- `buildMissingAuthMessage`: replace the generic auth-store error with a provider-specific recovery hint
|
||||
- `suppressBuiltInModel`: hide stale upstream rows and optionally return a provider-owned error for direct resolution failures
|
||||
- `augmentModelCatalog`: append synthetic/final catalog rows after discovery and config merging
|
||||
- `prepareRuntimeAuth`: exchange a configured credential into the actual short-lived runtime token/key used for requests
|
||||
- `resolveUsageAuth`: resolve provider-owned credentials for usage/billing endpoints without hardcoding token parsing in core
|
||||
- `fetchUsageSnapshot`: own provider-specific usage endpoint fetch/parsing while core keeps summary fan-out and formatting
|
||||
|
||||
Rule of thumb:
|
||||
|
||||
- provider owns a catalog or base URL defaults: use `catalog`
|
||||
- provider accepts arbitrary upstream model ids: use `resolveDynamicModel`
|
||||
- provider needs network metadata before resolving unknown ids: add `prepareDynamicModel`
|
||||
- provider needs transport rewrites but still uses a core transport: use `normalizeResolvedModel`
|
||||
- provider needs transcript/provider-family quirks: use `capabilities`
|
||||
- provider needs default request params or per-provider param cleanup: use `prepareExtraParams`
|
||||
- provider needs request headers/body/model compat wrappers without a custom transport: use `wrapStreamFn`
|
||||
- provider needs proxy-specific cache TTL gating: use `isCacheTtlEligible`
|
||||
- provider needs a provider-specific missing-auth recovery hint: use `buildMissingAuthMessage`
|
||||
- provider needs to hide stale upstream rows or replace them with a vendor hint: use `suppressBuiltInModel`
|
||||
- provider needs synthetic forward-compat rows in `models list` and pickers: use `augmentModelCatalog`
|
||||
- provider needs a token exchange or short-lived request credential: use `prepareRuntimeAuth`
|
||||
- provider needs custom usage/quota token parsing or a different usage credential: use `resolveUsageAuth`
|
||||
- provider needs a provider-specific usage endpoint or payload parser: use `fetchUsageSnapshot`
|
||||
|
||||
If the provider needs a fully custom wire protocol or custom request executor,
|
||||
that is a different class of extension. These hooks are for provider behavior
|
||||
that still runs on OpenClaw's normal inference loop.
|
||||
|
||||
### Provider Example
|
||||
|
||||
```ts
|
||||
api.registerProvider({
|
||||
id: "example-proxy",
|
||||
label: "Example Proxy",
|
||||
auth: [],
|
||||
catalog: {
|
||||
order: "simple",
|
||||
run: async (ctx) => {
|
||||
const apiKey = ctx.resolveProviderApiKey("example-proxy").apiKey;
|
||||
if (!apiKey) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
provider: {
|
||||
baseUrl: "https://proxy.example.com/v1",
|
||||
apiKey,
|
||||
api: "openai-completions",
|
||||
models: [{ id: "auto", name: "Auto" }],
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
resolveDynamicModel: (ctx) => ({
|
||||
id: ctx.modelId,
|
||||
name: ctx.modelId,
|
||||
provider: "example-proxy",
|
||||
api: "openai-completions",
|
||||
baseUrl: "https://proxy.example.com/v1",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 128000,
|
||||
maxTokens: 8192,
|
||||
}),
|
||||
prepareRuntimeAuth: async (ctx) => {
|
||||
const exchanged = await exchangeToken(ctx.apiKey);
|
||||
return {
|
||||
apiKey: exchanged.token,
|
||||
baseUrl: exchanged.baseUrl,
|
||||
expiresAt: exchanged.expiresAt,
|
||||
};
|
||||
},
|
||||
resolveUsageAuth: async (ctx) => {
|
||||
const auth = await ctx.resolveOAuthToken();
|
||||
return auth ? { token: auth.token } : null;
|
||||
},
|
||||
fetchUsageSnapshot: async (ctx) => {
|
||||
return await fetchExampleProxyUsage(ctx.token, ctx.timeoutMs, ctx.fetchFn);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Built-in examples
|
||||
|
||||
- Anthropic uses `resolveDynamicModel`, `capabilities`, `resolveUsageAuth`,
|
||||
`fetchUsageSnapshot`, and `isCacheTtlEligible` because it owns Claude 4.6
|
||||
forward-compat, provider-family hints, usage endpoint integration, and
|
||||
prompt-cache eligibility.
|
||||
- OpenAI uses `resolveDynamicModel`, `normalizeResolvedModel`, and
|
||||
`capabilities` plus `buildMissingAuthMessage`, `suppressBuiltInModel`, and
|
||||
`augmentModelCatalog` because it owns GPT-5.4 forward-compat, the direct
|
||||
OpenAI `openai-completions` -> `openai-responses` normalization, Codex-aware
|
||||
auth hints, Spark suppression, and synthetic OpenAI list rows.
|
||||
- OpenRouter uses `catalog` plus `resolveDynamicModel` and
|
||||
`prepareDynamicModel` because the provider is pass-through and may expose new
|
||||
model ids before OpenClaw's static catalog updates.
|
||||
- GitHub Copilot uses `catalog`, `resolveDynamicModel`, and
|
||||
`capabilities` plus `prepareRuntimeAuth` and `fetchUsageSnapshot` because it
|
||||
needs model fallback behavior, Claude transcript quirks, a GitHub token ->
|
||||
Copilot token exchange, and a provider-owned usage endpoint.
|
||||
- OpenAI Codex uses `catalog`, `resolveDynamicModel`,
|
||||
`normalizeResolvedModel`, and `augmentModelCatalog` plus
|
||||
`prepareExtraParams`, `resolveUsageAuth`, and `fetchUsageSnapshot` because it
|
||||
still runs on core OpenAI transports but owns its transport/base URL
|
||||
normalization, default transport choice, synthetic Codex catalog rows, and
|
||||
ChatGPT usage endpoint integration.
|
||||
- Gemini CLI OAuth uses `resolveDynamicModel`, `resolveUsageAuth`, and
|
||||
`fetchUsageSnapshot` because it owns Gemini 3.1 forward-compat fallback plus
|
||||
the token parsing and quota endpoint wiring needed by `/usage`.
|
||||
- OpenRouter uses `capabilities`, `wrapStreamFn`, and `isCacheTtlEligible`
|
||||
to keep provider-specific request headers, routing metadata, reasoning
|
||||
patches, and prompt-cache policy out of core.
|
||||
- Moonshot uses `catalog` plus `wrapStreamFn` because it still uses the shared
|
||||
OpenAI transport but needs provider-owned thinking payload normalization.
|
||||
- Kilocode uses `catalog`, `capabilities`, `wrapStreamFn`, and
|
||||
`isCacheTtlEligible` because it needs provider-owned request headers,
|
||||
reasoning payload normalization, Gemini transcript hints, and Anthropic
|
||||
cache-TTL gating.
|
||||
- Z.AI uses `resolveDynamicModel`, `prepareExtraParams`, `wrapStreamFn`,
|
||||
`isCacheTtlEligible`, `resolveUsageAuth`, and `fetchUsageSnapshot` because it
|
||||
owns GLM-5 fallback, `tool_stream` defaults, and both usage auth + quota
|
||||
fetching.
|
||||
- Mistral, OpenCode Zen, and OpenCode Go use `capabilities` only to keep
|
||||
transcript/tooling quirks out of core.
|
||||
- Catalog-only bundled providers such as `byteplus`, `cloudflare-ai-gateway`,
|
||||
`huggingface`, `kimi-coding`, `minimax-portal`, `modelstudio`, `nvidia`,
|
||||
`qianfan`, `qwen-portal`, `synthetic`, `together`, `venice`,
|
||||
`vercel-ai-gateway`, and `volcengine` use `catalog` only.
|
||||
- MiniMax and Xiaomi use `catalog` plus usage hooks because their `/usage`
|
||||
behavior is plugin-owned even though inference still runs through the shared
|
||||
transports.
|
||||
|
||||
## Load pipeline
|
||||
|
||||
At startup, OpenClaw does roughly this:
|
||||
|
||||
1. discover candidate plugin roots
|
||||
2. read `openclaw.plugin.json` and package metadata
|
||||
2. read native or compatible bundle manifests and package metadata
|
||||
3. reject unsafe candidates
|
||||
4. normalize plugin config (`plugins.enabled`, `allow`, `deny`, `entries`,
|
||||
`slots`, `load.paths`)
|
||||
5. decide enablement for each candidate
|
||||
6. load enabled modules via jiti
|
||||
7. call `register(api)` and collect registrations into the plugin registry
|
||||
6. load enabled native modules via jiti
|
||||
7. call native `register(api)` hooks and collect registrations into the plugin registry
|
||||
8. expose the registry to commands/runtime surfaces
|
||||
|
||||
The safety gates happen **before** runtime execution. Candidates are blocked
|
||||
@@ -150,13 +438,13 @@ ownership looks suspicious for non-bundled plugins.
|
||||
The manifest is the control-plane source of truth. OpenClaw uses it to:
|
||||
|
||||
- identify the plugin
|
||||
- discover declared channels/skills/config schema
|
||||
- discover declared channels/skills/config schema or bundle capabilities
|
||||
- validate `plugins.entries.<id>.config`
|
||||
- augment Control UI labels/placeholders
|
||||
- show install/catalog metadata
|
||||
|
||||
The runtime module is the data-plane part. It registers actual behavior such as
|
||||
hooks, tools, commands, or provider flows.
|
||||
For native plugins, the runtime module is the data-plane part. It registers
|
||||
actual behavior such as hooks, tools, commands, or provider flows.
|
||||
|
||||
### What the loader caches
|
||||
|
||||
@@ -253,8 +541,7 @@ authoring plugins:
|
||||
`openclaw/plugin-sdk/acpx`, `openclaw/plugin-sdk/bluebubbles`,
|
||||
`openclaw/plugin-sdk/copilot-proxy`, `openclaw/plugin-sdk/device-pair`,
|
||||
`openclaw/plugin-sdk/diagnostics-otel`, `openclaw/plugin-sdk/diffs`,
|
||||
`openclaw/plugin-sdk/feishu`,
|
||||
`openclaw/plugin-sdk/google-gemini-cli-auth`, `openclaw/plugin-sdk/googlechat`,
|
||||
`openclaw/plugin-sdk/feishu`, `openclaw/plugin-sdk/googlechat`,
|
||||
`openclaw/plugin-sdk/irc`, `openclaw/plugin-sdk/llm-task`,
|
||||
`openclaw/plugin-sdk/lobster`, `openclaw/plugin-sdk/matrix`,
|
||||
`openclaw/plugin-sdk/mattermost`, `openclaw/plugin-sdk/memory-core`,
|
||||
@@ -268,6 +555,36 @@ authoring plugins:
|
||||
`openclaw/plugin-sdk/twitch`, `openclaw/plugin-sdk/voice-call`,
|
||||
`openclaw/plugin-sdk/zalo`, and `openclaw/plugin-sdk/zalouser`.
|
||||
|
||||
## Provider catalogs
|
||||
|
||||
Provider plugins can define model catalogs for inference with
|
||||
`registerProvider({ catalog: { run(...) { ... } } })`.
|
||||
|
||||
`catalog.run(...)` returns the same shape OpenClaw writes into
|
||||
`models.providers`:
|
||||
|
||||
- `{ provider }` for one provider entry
|
||||
- `{ providers }` for multiple provider entries
|
||||
|
||||
Use `catalog` when the plugin owns provider-specific model ids, base URL
|
||||
defaults, or auth-gated model metadata.
|
||||
|
||||
`catalog.order` controls when a plugin's catalog merges relative to OpenClaw's
|
||||
built-in implicit providers:
|
||||
|
||||
- `simple`: plain API-key or env-driven providers
|
||||
- `profile`: providers that appear when auth profiles exist
|
||||
- `paired`: providers that synthesize multiple related provider entries
|
||||
- `late`: last pass, after other implicit providers
|
||||
|
||||
Later providers win on key collision, so plugins can intentionally override a
|
||||
built-in provider entry with the same provider id.
|
||||
|
||||
Compatibility:
|
||||
|
||||
- `discovery` still works as a legacy alias
|
||||
- if both `catalog` and `discovery` are registered, OpenClaw uses `catalog`
|
||||
|
||||
Compatibility note:
|
||||
|
||||
- `openclaw/plugin-sdk` remains supported for existing external plugins.
|
||||
@@ -334,18 +651,44 @@ OpenClaw scans, in order:
|
||||
- `~/.openclaw/extensions/*.ts`
|
||||
- `~/.openclaw/extensions/*/index.ts`
|
||||
|
||||
4. Bundled extensions (shipped with OpenClaw, mostly disabled by default)
|
||||
4. Bundled extensions (shipped with OpenClaw; mixed default-on/default-off)
|
||||
|
||||
- `<openclaw>/extensions/*`
|
||||
|
||||
Most bundled plugins must be enabled explicitly via
|
||||
`plugins.entries.<id>.enabled` or `openclaw plugins enable <id>`.
|
||||
Many bundled provider plugins are enabled by default so model catalogs/runtime
|
||||
hooks stay available without extra setup. Others still require explicit
|
||||
enablement via `plugins.entries.<id>.enabled` or
|
||||
`openclaw plugins enable <id>`.
|
||||
|
||||
Default-on bundled plugin exceptions:
|
||||
Default-on bundled plugin examples:
|
||||
|
||||
- `byteplus`
|
||||
- `cloudflare-ai-gateway`
|
||||
- `device-pair`
|
||||
- `github-copilot`
|
||||
- `huggingface`
|
||||
- `kilocode`
|
||||
- `kimi-coding`
|
||||
- `minimax`
|
||||
- `minimax`
|
||||
- `modelstudio`
|
||||
- `moonshot`
|
||||
- `nvidia`
|
||||
- `ollama`
|
||||
- `openai`
|
||||
- `openrouter`
|
||||
- `phone-control`
|
||||
- `qianfan`
|
||||
- `qwen-portal-auth`
|
||||
- `sglang`
|
||||
- `synthetic`
|
||||
- `talk-voice`
|
||||
- `together`
|
||||
- `venice`
|
||||
- `vercel-ai-gateway`
|
||||
- `vllm`
|
||||
- `volcengine`
|
||||
- `xiaomi`
|
||||
- active memory slot plugin (default slot: `memory-core`)
|
||||
|
||||
Installed plugins are enabled by default, but can be disabled the same way.
|
||||
@@ -363,9 +706,16 @@ Hardening notes:
|
||||
- path ownership is suspicious for non-bundled plugins (POSIX owner is neither current uid nor root).
|
||||
- Loaded non-bundled plugins without install/load-path provenance emit a warning so you can pin trust (`plugins.allow`) or install tracking (`plugins.installs`).
|
||||
|
||||
Each plugin must include a `openclaw.plugin.json` file in its root. If a path
|
||||
points at a file, the plugin root is the file's directory and must contain the
|
||||
manifest.
|
||||
Each native OpenClaw plugin must include a `openclaw.plugin.json` file in its
|
||||
root. If a path points at a file, the plugin root is the file's directory and
|
||||
must contain the manifest.
|
||||
|
||||
Compatible bundles may instead provide one of:
|
||||
|
||||
- `.codex-plugin/plugin.json`
|
||||
- `.claude-plugin/plugin.json`
|
||||
|
||||
Bundle directories are discovered from the same roots as native plugins.
|
||||
|
||||
If multiple plugins resolve to the same id, the first match in the order above
|
||||
wins and lower-precedence copies are ignored.
|
||||
@@ -394,9 +744,8 @@ Enablement is resolved after discovery:
|
||||
- channel config implicitly enables the bundled channel plugin
|
||||
- exclusive slots can force-enable the selected plugin for that slot
|
||||
|
||||
In current core, bundled default-on ids include local/provider helpers such as
|
||||
`ollama`, `sglang`, `vllm`, plus `device-pair`, `phone-control`, and
|
||||
`talk-voice`.
|
||||
In current core, bundled default-on ids include the local/provider helpers
|
||||
above plus the active memory slot plugin.
|
||||
|
||||
### Package packs
|
||||
|
||||
@@ -406,7 +755,8 @@ A plugin directory may include a `package.json` with `openclaw.extensions`:
|
||||
{
|
||||
"name": "my-pack",
|
||||
"openclaw": {
|
||||
"extensions": ["./src/safety.ts", "./src/tools.ts"]
|
||||
"extensions": ["./src/safety.ts", "./src/tools.ts"],
|
||||
"setupEntry": "./src/setup-entry.ts"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -425,9 +775,16 @@ Security note: `openclaw plugins install` installs plugin dependencies with
|
||||
`npm install --ignore-scripts` (no lifecycle scripts). Keep plugin dependency
|
||||
trees "pure JS/TS" and avoid packages that require `postinstall` builds.
|
||||
|
||||
Optional: `openclaw.setupEntry` can point at a lightweight setup-only module.
|
||||
When OpenClaw needs setup surfaces for a disabled channel plugin, or
|
||||
when a channel plugin is enabled but still unconfigured, it loads `setupEntry`
|
||||
instead of the full plugin entry. This keeps startup and onboarding lighter
|
||||
when your main plugin entry also wires tools, hooks, or other runtime-only
|
||||
code.
|
||||
|
||||
### Channel catalog metadata
|
||||
|
||||
Channel plugins can advertise onboarding metadata via `openclaw.channel` and
|
||||
Channel plugins can advertise setup/discovery metadata via `openclaw.channel` and
|
||||
install hints via `openclaw.install`. This keeps the core catalog data-free.
|
||||
|
||||
Example:
|
||||
@@ -537,8 +894,9 @@ Validation rules (strict):
|
||||
- Unknown plugin ids in `entries`, `allow`, `deny`, or `slots` are **errors**.
|
||||
- Unknown `channels.<id>` keys are **errors** unless a plugin manifest declares
|
||||
the channel id.
|
||||
- Plugin config is validated using the JSON Schema embedded in
|
||||
- Native plugin config is validated using the JSON Schema embedded in
|
||||
`openclaw.plugin.json` (`configSchema`).
|
||||
- Compatible bundles currently do not expose native OpenClaw config schemas.
|
||||
- If a plugin is disabled, its config is preserved and a **warning** is emitted.
|
||||
|
||||
### Disabled vs missing vs invalid
|
||||
@@ -638,6 +996,10 @@ openclaw plugins disable <id>
|
||||
openclaw plugins doctor
|
||||
```
|
||||
|
||||
`openclaw plugins list` shows the top-level format as `openclaw` or `bundle`.
|
||||
Verbose list/info output also shows bundle subtype (`codex` or `claude`) plus
|
||||
detected bundle capabilities.
|
||||
|
||||
`plugins update` only works for npm installs tracked under `plugins.installs`.
|
||||
If stored integrity metadata changes between updates, OpenClaw warns and asks for confirmation (use global `--yes` to bypass prompts).
|
||||
|
||||
@@ -1082,28 +1444,23 @@ Notes:
|
||||
- `meta.preferOver` lists channel ids to skip auto-enable when both are configured.
|
||||
- `meta.detailLabel` and `meta.systemImage` let UIs show richer channel labels/icons.
|
||||
|
||||
### Channel onboarding hooks
|
||||
### Channel setup hooks
|
||||
|
||||
Channel plugins can define optional onboarding hooks on `plugin.onboarding`:
|
||||
Preferred setup split:
|
||||
|
||||
- `configure(ctx)` is the baseline setup flow.
|
||||
- `configureInteractive(ctx)` can fully own interactive setup for both configured and unconfigured states.
|
||||
- `configureWhenConfigured(ctx)` can override behavior only for already configured channels.
|
||||
- `plugin.setup` owns account-id normalization, validation, and config writes.
|
||||
- `plugin.setupWizard` lets the host run the common wizard flow while the channel only supplies status, credential, DM allowlist, and channel-access descriptors.
|
||||
|
||||
Hook precedence in the wizard:
|
||||
`plugin.setupWizard` is best for channels that fit the shared pattern:
|
||||
|
||||
1. `configureInteractive` (if present)
|
||||
2. `configureWhenConfigured` (only when channel status is already configured)
|
||||
3. fallback to `configure`
|
||||
|
||||
Context details:
|
||||
|
||||
- `configureInteractive` and `configureWhenConfigured` receive:
|
||||
- `configured` (`true` or `false`)
|
||||
- `label` (user-facing channel name used by prompts)
|
||||
- plus the shared config/runtime/prompter/options fields
|
||||
- Returning `"skip"` leaves selection and account tracking unchanged.
|
||||
- Returning `{ cfg, accountId? }` applies config updates and records account selection.
|
||||
- one account picker driven by `plugin.config.listAccountIds`
|
||||
- optional preflight/prepare step before prompting (for example installer/bootstrap work)
|
||||
- optional env-shortcut prompt for bundled credential sets (for example paired bot/app tokens)
|
||||
- one or more credential prompts, with each step either writing through `plugin.setup.applyAccountConfig` or a channel-owned partial patch
|
||||
- optional non-secret text prompts (for example CLI paths, base URLs, account ids)
|
||||
- optional channel/group access allowlist prompts resolved by the host
|
||||
- optional DM allowlist resolution (for example `@username` -> numeric id)
|
||||
- optional completion note after setup finishes
|
||||
|
||||
### Write a new messaging channel (step‑by‑step)
|
||||
|
||||
@@ -1130,7 +1487,7 @@ Model provider docs live under `/providers/*`.
|
||||
|
||||
4. Add optional adapters as needed
|
||||
|
||||
- `setup` (wizard), `security` (DM policy), `status` (health/diagnostics)
|
||||
- `setup` (validation + config writes), `setupWizard` (host-owned wizard), `security` (DM policy), `status` (health/diagnostics)
|
||||
- `gateway` (start/stop/login), `mentions`, `threading`, `streaming`
|
||||
- `actions` (message actions), `commands` (native command behavior)
|
||||
|
||||
@@ -1314,6 +1671,7 @@ Recommended packaging:
|
||||
Publishing contract:
|
||||
|
||||
- Plugin `package.json` must include `openclaw.extensions` with one or more entry files.
|
||||
- Optional: `openclaw.setupEntry` may point at a lightweight setup-only entry for disabled or still-unconfigured channel setup.
|
||||
- Entry files can be `.js` or `.ts` (jiti loads TS at runtime).
|
||||
- `openclaw plugins install <npm-spec>` uses `npm pack`, extracts into `~/.openclaw/extensions/<id>/`, and enables it in config.
|
||||
- Config key stability: scoped packages are normalized to the **unscoped** id for `plugins.entries.*`.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
summary: "Web search + fetch tools (Brave, Gemini, Grok, Kimi, and Perplexity providers)"
|
||||
summary: "Web search + fetch tools (Brave, Firecrawl, Gemini, Grok, Kimi, and Perplexity providers)"
|
||||
read_when:
|
||||
- You want to enable web_search or web_fetch
|
||||
- You need provider API key setup
|
||||
@@ -11,7 +11,7 @@ title: "Web Tools"
|
||||
|
||||
OpenClaw ships two lightweight web tools:
|
||||
|
||||
- `web_search` — Search the web using Brave Search API, Gemini with Google Search grounding, Grok, Kimi, or Perplexity Search API.
|
||||
- `web_search` — Search the web using Brave Search API, Firecrawl Search, Gemini with Google Search grounding, Grok, Kimi, or Perplexity Search API.
|
||||
- `web_fetch` — HTTP fetch + readable extraction (HTML → markdown/text).
|
||||
|
||||
These are **not** browser automation. For JS-heavy sites or logins, use the
|
||||
@@ -24,18 +24,20 @@ These are **not** browser automation. For JS-heavy sites or logins, use the
|
||||
- `web_fetch` does a plain HTTP GET and extracts readable content
|
||||
(HTML → markdown/text). It does **not** execute JavaScript.
|
||||
- `web_fetch` is enabled by default (unless explicitly disabled).
|
||||
- The bundled Firecrawl plugin also adds `firecrawl_search` and `firecrawl_scrape` when enabled.
|
||||
|
||||
See [Brave Search setup](/brave-search) and [Perplexity Search setup](/perplexity) for provider-specific details.
|
||||
|
||||
## Choosing a search provider
|
||||
|
||||
| Provider | Result shape | Provider-specific filters | Notes | API key |
|
||||
| ------------------------- | ---------------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------ | ------------------------------------------- |
|
||||
| **Brave Search API** | Structured results with snippets | `country`, `language`, `ui_lang`, time | Supports Brave `llm-context` mode | `BRAVE_API_KEY` |
|
||||
| **Gemini** | AI-synthesized answers + citations | — | Uses Google Search grounding | `GEMINI_API_KEY` |
|
||||
| **Grok** | AI-synthesized answers + citations | — | Uses xAI web-grounded responses | `XAI_API_KEY` |
|
||||
| **Kimi** | AI-synthesized answers + citations | — | Uses Moonshot web search | `KIMI_API_KEY` / `MOONSHOT_API_KEY` |
|
||||
| **Perplexity Search API** | Structured results with snippets | `country`, `language`, time, `domain_filter` | Supports content extraction controls; OpenRouter uses Sonar compatibility path | `PERPLEXITY_API_KEY` / `OPENROUTER_API_KEY` |
|
||||
| Provider | Result shape | Provider-specific filters | Notes | API key |
|
||||
| ------------------------- | ---------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------ | ------------------------------------------- |
|
||||
| **Brave Search API** | Structured results with snippets | `country`, `language`, `ui_lang`, time | Supports Brave `llm-context` mode | `BRAVE_API_KEY` |
|
||||
| **Firecrawl Search** | Structured results with snippets | Use `firecrawl_search` for Firecrawl-specific search options | Best for pairing search with Firecrawl scraping/extraction | `FIRECRAWL_API_KEY` |
|
||||
| **Gemini** | AI-synthesized answers + citations | — | Uses Google Search grounding | `GEMINI_API_KEY` |
|
||||
| **Grok** | AI-synthesized answers + citations | — | Uses xAI web-grounded responses | `XAI_API_KEY` |
|
||||
| **Kimi** | AI-synthesized answers + citations | — | Uses Moonshot web search | `KIMI_API_KEY` / `MOONSHOT_API_KEY` |
|
||||
| **Perplexity Search API** | Structured results with snippets | `country`, `language`, time, `domain_filter` | Supports content extraction controls; OpenRouter uses Sonar compatibility path | `PERPLEXITY_API_KEY` / `OPENROUTER_API_KEY` |
|
||||
|
||||
### Auto-detection
|
||||
|
||||
@@ -46,6 +48,7 @@ The table above is alphabetical. If no `provider` is explicitly set, runtime aut
|
||||
3. **Grok** — `XAI_API_KEY` env var or `tools.web.search.grok.apiKey` config
|
||||
4. **Kimi** — `KIMI_API_KEY` / `MOONSHOT_API_KEY` env var or `tools.web.search.kimi.apiKey` config
|
||||
5. **Perplexity** — `PERPLEXITY_API_KEY`, `OPENROUTER_API_KEY`, or `tools.web.search.perplexity.apiKey` config
|
||||
6. **Firecrawl** — `FIRECRAWL_API_KEY` env var or `tools.web.search.firecrawl.apiKey` config
|
||||
|
||||
If no keys are found, it falls back to Brave (you'll get a missing-key error prompting you to configure one).
|
||||
|
||||
@@ -86,6 +89,7 @@ See [Perplexity Search API Docs](https://docs.perplexity.ai/guides/search-quicks
|
||||
**Via config:** run `openclaw configure --section web`. It stores the key under the provider-specific config path:
|
||||
|
||||
- Brave: `tools.web.search.apiKey`
|
||||
- Firecrawl: `tools.web.search.firecrawl.apiKey`
|
||||
- Gemini: `tools.web.search.gemini.apiKey`
|
||||
- Grok: `tools.web.search.grok.apiKey`
|
||||
- Kimi: `tools.web.search.kimi.apiKey`
|
||||
@@ -96,6 +100,7 @@ All of these fields also support SecretRef objects.
|
||||
**Via environment:** set provider env vars in the Gateway process environment:
|
||||
|
||||
- Brave: `BRAVE_API_KEY`
|
||||
- Firecrawl: `FIRECRAWL_API_KEY`
|
||||
- Gemini: `GEMINI_API_KEY`
|
||||
- Grok: `XAI_API_KEY`
|
||||
- Kimi: `KIMI_API_KEY` or `MOONSHOT_API_KEY`
|
||||
@@ -121,6 +126,34 @@ For a gateway install, put these in `~/.openclaw/.env` (or your service environm
|
||||
}
|
||||
```
|
||||
|
||||
**Firecrawl Search:**
|
||||
|
||||
```json5
|
||||
{
|
||||
plugins: {
|
||||
entries: {
|
||||
firecrawl: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
web: {
|
||||
search: {
|
||||
enabled: true,
|
||||
provider: "firecrawl",
|
||||
firecrawl: {
|
||||
apiKey: "fc-...", // optional if FIRECRAWL_API_KEY is set
|
||||
baseUrl: "https://api.firecrawl.dev",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
When you choose Firecrawl in onboarding or `openclaw configure --section web`, OpenClaw enables the bundled Firecrawl plugin automatically so `web_search`, `firecrawl_search`, and `firecrawl_scrape` are all available.
|
||||
|
||||
**Brave LLM Context mode:**
|
||||
|
||||
```json5
|
||||
@@ -234,6 +267,7 @@ Search the web using your configured provider.
|
||||
- `tools.web.search.enabled` must not be `false` (default: enabled)
|
||||
- API key for your chosen provider:
|
||||
- **Brave**: `BRAVE_API_KEY` or `tools.web.search.apiKey`
|
||||
- **Firecrawl**: `FIRECRAWL_API_KEY` or `tools.web.search.firecrawl.apiKey`
|
||||
- **Gemini**: `GEMINI_API_KEY` or `tools.web.search.gemini.apiKey`
|
||||
- **Grok**: `XAI_API_KEY` or `tools.web.search.grok.apiKey`
|
||||
- **Kimi**: `KIMI_API_KEY`, `MOONSHOT_API_KEY`, or `tools.web.search.kimi.apiKey`
|
||||
@@ -260,7 +294,7 @@ Search the web using your configured provider.
|
||||
|
||||
### Tool parameters
|
||||
|
||||
All parameters work for Brave and for native Perplexity Search API unless noted.
|
||||
Parameters depend on the selected provider.
|
||||
|
||||
Perplexity's OpenRouter / Sonar compatibility path supports only `query` and `freshness`.
|
||||
If you set `tools.web.search.perplexity.baseUrl` / `model`, use `OPENROUTER_API_KEY`, or configure an `sk-or-...` key, Search API-only filters return explicit errors.
|
||||
@@ -279,6 +313,8 @@ If you set `tools.web.search.perplexity.baseUrl` / `model`, use `OPENROUTER_API_
|
||||
| `max_tokens` | Total content budget, default 25000 (Perplexity only) |
|
||||
| `max_tokens_per_page` | Per-page token limit, default 2048 (Perplexity only) |
|
||||
|
||||
Firecrawl `web_search` supports `query` and `count`. For Firecrawl-specific controls like `sources`, `categories`, result scraping, or scrape timeout, use `firecrawl_search` from the bundled Firecrawl plugin.
|
||||
|
||||
**Examples:**
|
||||
|
||||
```javascript
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
- 目标文档:`docs/zh-CN/**/*.md`
|
||||
- 术语表:`docs/.i18n/glossary.zh-CN.json`
|
||||
- 翻译记忆库:`docs/.i18n/zh-CN.tm.jsonl`
|
||||
- 提示词规则:`scripts/docs-i18n/translator.go`
|
||||
- 提示词规则:`scripts/docs-i18n/prompt.go`
|
||||
|
||||
常用运行方式:
|
||||
|
||||
@@ -31,6 +31,8 @@ go run scripts/docs-i18n/main.go -mode segment docs/channels/matrix.md
|
||||
注意事项:
|
||||
|
||||
- doc 模式用于整页翻译;segment 模式用于小范围修补(依赖 TM)。
|
||||
- 新增技术术语、页面标题或短导航标签时,先更新 `docs/.i18n/glossary.zh-CN.json`,再跑 `doc` 模式;不要指望模型自行保留英文术语或固定译名。
|
||||
- `pnpm docs:check-i18n-glossary` 会检查变更过的英文文档标题和短内部链接标签是否已写入 glossary。
|
||||
- 超大文件若超时,优先做**定点替换**或拆分后再跑。
|
||||
- 翻译后检查中文引号、CJK-Latin 间距和术语一致性。
|
||||
|
||||
|
||||
@@ -589,7 +589,7 @@ Gmail Pub/Sub 钩子设置 + 运行器。参见 [/automation/gmail-pubsub](/auto
|
||||
说明:
|
||||
|
||||
- 数据直接来自提供商用量端点(非估算)。
|
||||
- 提供商:Anthropic、GitHub Copilot、OpenAI Codex OAuth,以及启用这些提供商插件时的 Gemini CLI/Antigravity。
|
||||
- 提供商:Anthropic、GitHub Copilot、OpenAI Codex OAuth,以及通过捆绑 `google` 插件提供的 Gemini CLI 和已配置的 Antigravity。
|
||||
- 如果没有匹配的凭证,用量会被隐藏。
|
||||
- 详情:参见[用量跟踪](/concepts/usage-tracking)。
|
||||
|
||||
|
||||
@@ -1,153 +1,265 @@
|
||||
---
|
||||
read_when:
|
||||
- 你需要按提供商分类的模型设置参考
|
||||
- 你需要一份逐提供商的模型设置参考
|
||||
- 你需要模型提供商的示例配置或 CLI 新手引导命令
|
||||
summary: 模型提供商概述,包含示例配置和 CLI 流程
|
||||
summary: 模型提供商概览,包含示例配置和 CLI 流程
|
||||
title: 模型提供商
|
||||
x-i18n:
|
||||
generated_at: "2026-02-03T07:46:28Z"
|
||||
model: claude-opus-4-5
|
||||
generated_at: "2026-03-16T02:12:40Z"
|
||||
model: claude-opus-4-6
|
||||
provider: pi
|
||||
source_hash: 14f73e5a9f9b7c6f017d59a54633942dba95a3eb50f8848b836cfe0b9f6d7719
|
||||
source_hash: 978798c80c5809c162f9807072ab48fdf99bfe0db39b2b3c245ce8b4e5451603
|
||||
source_path: concepts/model-providers.md
|
||||
workflow: 15
|
||||
---
|
||||
|
||||
# 模型提供商
|
||||
|
||||
本页介绍 **LLM/模型提供商**(不是 WhatsApp/Telegram 等聊天渠道)。
|
||||
关于模型选择规则,请参阅 [/concepts/models](/concepts/models)。
|
||||
本页涵盖 **LLM/模型提供商** (不是 WhatsApp/Telegram 等聊天渠道)。
|
||||
有关模型选择规则,请参阅 [/concepts/models](/concepts/models)。
|
||||
|
||||
## 快速规则
|
||||
|
||||
- 模型引用使用 `provider/model` 格式(例如:`opencode/claude-opus-4-5`)。
|
||||
- 如果设置了 `agents.defaults.models`,它将成为允许列表。
|
||||
- CLI 辅助工具:`openclaw onboard`、`openclaw models list`、`openclaw models set <provider/model>`。
|
||||
- 模型引用使用 `provider/model` (例如: `opencode/claude-opus-4-6`)。
|
||||
- 如果你设置了 `agents.defaults.models`,它将成为允许列表。
|
||||
- CLI 辅助命令: `openclaw onboard`, `openclaw models list`, `openclaw models set <provider/model>`。
|
||||
- 提供商插件可以通过以下方式注入模型目录 `registerProvider({ catalog })`;
|
||||
OpenClaw 将该输出合并到 `models.providers` 之后再写入
|
||||
`models.json`。
|
||||
- 提供商插件还可以通过以下方式控制提供商的运行时行为
|
||||
`resolveDynamicModel`, `prepareDynamicModel`, `normalizeResolvedModel`,
|
||||
`capabilities`, `prepareExtraParams`, `wrapStreamFn`,
|
||||
`isCacheTtlEligible`, `prepareRuntimeAuth`, `resolveUsageAuth`,以及
|
||||
`fetchUsageSnapshot`。
|
||||
|
||||
## 插件管理的提供商行为
|
||||
|
||||
提供商插件现在可以管理大部分提供商特定逻辑,而 OpenClaw 负责维护通用推理循环。
|
||||
|
||||
典型分工:
|
||||
|
||||
- `catalog`:提供商出现在 `models.providers`
|
||||
- `resolveDynamicModel`:提供商接受尚未出现在本地静态目录中的模型 ID
|
||||
- `prepareDynamicModel`:提供商在重试动态解析之前需要刷新元数据
|
||||
- `normalizeResolvedModel`:提供商需要传输层或基础 URL 重写
|
||||
- `capabilities`:提供商发布会话记录/工具/提供商系列的特殊行为
|
||||
- `prepareExtraParams`:提供商默认或规范化每个模型的请求参数
|
||||
- `wrapStreamFn`:提供商应用请求头/请求体/模型兼容性封装
|
||||
- `isCacheTtlEligible`:提供商决定哪些上游模型 ID 支持 prompt-cache TTL
|
||||
- `prepareRuntimeAuth`:提供商将配置的凭证转换为短期运行时令牌
|
||||
- `resolveUsageAuth`:提供商为以下用途解析使用量/配额凭证 `/usage`
|
||||
以及相关的状态/报告界面
|
||||
- `fetchUsageSnapshot`:提供商负责使用量端点的获取/解析,而核心仍负责摘要外壳和格式化
|
||||
|
||||
当前内置示例:
|
||||
|
||||
- `anthropic`:Claude 4.6 向前兼容回退、使用量端点获取,以及 cache-TTL/提供商系列元数据
|
||||
- `openrouter`:直通模型 ID、请求封装、提供商能力提示,以及 cache-TTL 策略
|
||||
- `github-copilot`:向前兼容模型回退、Claude-thinking 会话记录提示、运行时令牌交换,以及使用量端点获取
|
||||
- `openai`:GPT-5.4 向前兼容回退、直接 OpenAI 传输规范化,以及提供商系列元数据
|
||||
- `openai-codex`:向前兼容模型回退、传输规范化,以及默认传输参数和使用量端点获取
|
||||
- `google-gemini-cli`:Gemini 3.1 向前兼容回退,以及使用量界面的 usage-token 解析和配额端点获取
|
||||
- `moonshot`:共享传输、插件管理的 thinking 负载规范化
|
||||
- `kilocode`:共享传输、插件管理的请求头、推理负载规范化、Gemini 会话记录提示,以及 cache-TTL 策略
|
||||
- `zai`:GLM-5 向前兼容回退, `tool_stream` 默认值、cache-TTL 策略,以及使用量认证和配额获取
|
||||
- `mistral`, `opencode`,以及`opencode-go`:插件管理的能力元数据
|
||||
- `byteplus`, `cloudflare-ai-gateway`, `huggingface`, `kimi-coding`,
|
||||
`minimax-portal`, `modelstudio`, `nvidia`, `qianfan`, `qwen-portal`,
|
||||
`synthetic`, `together`, `venice`, `vercel-ai-gateway`,以及`volcengine`:仅限插件管理的目录
|
||||
- `minimax` 和 `xiaomi`:插件管理的目录以及使用量认证/快照逻辑
|
||||
|
||||
以上涵盖了仍然适用于 OpenClaw 常规传输层的提供商。如果某个提供商需要完全自定义的请求执行器,则属于一个独立的、更深层的扩展层面。
|
||||
|
||||
## API 密钥轮换
|
||||
|
||||
- 支持对选定提供商的通用提供商轮换。
|
||||
- 通过以下方式配置多个密钥:
|
||||
- `OPENCLAW_LIVE_<PROVIDER>_KEY` (单个实时覆盖,最高优先级)
|
||||
- `<PROVIDER>_API_KEYS` (逗号或分号分隔的列表)
|
||||
- `<PROVIDER>_API_KEY` (主密钥)
|
||||
- `<PROVIDER>_API_KEY_*` (编号列表,例如 `<PROVIDER>_API_KEY_1`)
|
||||
- 对于 Google 提供商, `GOOGLE_API_KEY` 也作为备选项包含在内。
|
||||
- 密钥选择顺序按优先级排列并去除重复值。
|
||||
- 仅在速率限制响应时使用下一个密钥重试请求(例如 `429`, `rate_limit`, `quota`, `resource exhausted`)。
|
||||
- 非速率限制的失败会立即报错;不会尝试密钥轮换。
|
||||
- 当所有候选密钥均失败时,返回最后一次尝试的错误。
|
||||
|
||||
## 内置提供商(pi-ai 目录)
|
||||
|
||||
OpenClaw 附带 pi-ai 目录。这些提供商**不需要** `models.providers` 配置;只需设置认证 + 选择模型。
|
||||
OpenClaw 附带 pi-ai 目录。这些提供商需要 **无需**
|
||||
`models.providers` 配置;只需设置认证并选择一个模型。
|
||||
|
||||
### OpenAI
|
||||
|
||||
- 提供商:`openai`
|
||||
- 认证:`OPENAI_API_KEY`
|
||||
- 示例模型:`openai/gpt-5.2`
|
||||
- CLI:`openclaw onboard --auth-choice openai-api-key`
|
||||
- 提供商: `openai`
|
||||
- 认证: `OPENAI_API_KEY`
|
||||
- 可选轮换: `OPENAI_API_KEYS`, `OPENAI_API_KEY_1`, `OPENAI_API_KEY_2`,加上 `OPENCLAW_LIVE_OPENAI_KEY` (单个覆盖)
|
||||
- 示例模型: `openai/gpt-5.4`, `openai/gpt-5.4-pro`
|
||||
- CLI: `openclaw onboard --auth-choice openai-api-key`
|
||||
- 默认传输为 `auto` (WebSocket 优先,SSE 备选)
|
||||
- 通过以下方式覆盖每个模型 `agents.defaults.models["openai/<model>"].params.transport` (`"sse"`, `"websocket"`,或 `"auto"`)
|
||||
- OpenAI Responses WebSocket 预热默认通过以下方式启用 `params.openaiWsWarmup` (`true`/`false`)
|
||||
- OpenAI 优先处理可以通过以下方式启用 `agents.defaults.models["openai/<model>"].params.serviceTier`
|
||||
- OpenAI 快速模式可以通过以下方式为每个模型启用 `agents.defaults.models["<provider>/<model>"].params.fastMode`
|
||||
- `openai/gpt-5.3-codex-spark` 在 OpenClaw 中被有意屏蔽,因为 OpenAI 实时 API 会拒绝它;Spark 被视为仅限 Codex 使用
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: { defaults: { model: { primary: "openai/gpt-5.2" } } },
|
||||
agents: { defaults: { model: { primary: "openai/gpt-5.4" } } },
|
||||
}
|
||||
```
|
||||
|
||||
### Anthropic
|
||||
|
||||
- 提供商:`anthropic`
|
||||
- 认证:`ANTHROPIC_API_KEY` 或 `claude setup-token`
|
||||
- 示例模型:`anthropic/claude-opus-4-5`
|
||||
- CLI:`openclaw onboard --auth-choice token`(粘贴 setup-token)或 `openclaw models auth paste-token --provider anthropic`
|
||||
- 提供商: `anthropic`
|
||||
- 认证: `ANTHROPIC_API_KEY` 或 `claude setup-token`
|
||||
- 可选轮换: `ANTHROPIC_API_KEYS`, `ANTHROPIC_API_KEY_1`, `ANTHROPIC_API_KEY_2`,加上 `OPENCLAW_LIVE_ANTHROPIC_KEY` (单个覆盖)
|
||||
- 示例模型: `anthropic/claude-opus-4-6`
|
||||
- CLI: `openclaw onboard --auth-choice token` (粘贴 setup-token)或 `openclaw models auth paste-token --provider anthropic`
|
||||
- 直接 API 密钥模型支持共享的 `/fast` 切换和 `params.fastMode`;OpenClaw 将其映射到 Anthropic 的 `service_tier` (`auto` 与 `standard_only`)
|
||||
- 策略说明:setup-token 支持属于技术兼容性;Anthropic 过去曾阻止部分订阅在 Claude Code 之外的使用。请核实当前 Anthropic 条款,并根据你的风险承受能力做出决定。
|
||||
- 建议:Anthropic API 密钥认证是比订阅 setup-token 认证更安全的推荐方式。
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: { defaults: { model: { primary: "anthropic/claude-opus-4-5" } } },
|
||||
agents: { defaults: { model: { primary: "anthropic/claude-opus-4-6" } } },
|
||||
}
|
||||
```
|
||||
|
||||
### OpenAI Code (Codex)
|
||||
|
||||
- 提供商:`openai-codex`
|
||||
- 提供商: `openai-codex`
|
||||
- 认证:OAuth (ChatGPT)
|
||||
- 示例模型:`openai-codex/gpt-5.2`
|
||||
- CLI:`openclaw onboard --auth-choice openai-codex` 或 `openclaw models auth login --provider openai-codex`
|
||||
- 示例模型: `openai-codex/gpt-5.4`
|
||||
- CLI: `openclaw onboard --auth-choice openai-codex` 或 `openclaw models auth login --provider openai-codex`
|
||||
- 默认传输为 `auto` (WebSocket 优先,SSE 备选)
|
||||
- 通过以下方式覆盖每个模型 `agents.defaults.models["openai-codex/<model>"].params.transport` (`"sse"`, `"websocket"`,或 `"auto"`)
|
||||
- 与相同的 `/fast` 切换和 `params.fastMode` 配置共享,如同直接的 `openai/*`
|
||||
- `openai-codex/gpt-5.3-codex-spark` 当 Codex OAuth 目录公开时仍然可用;取决于授权资格
|
||||
- 策略说明:OpenAI Codex OAuth 明确支持 OpenClaw 等外部工具/工作流。
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: { defaults: { model: { primary: "openai-codex/gpt-5.2" } } },
|
||||
agents: { defaults: { model: { primary: "openai-codex/gpt-5.4" } } },
|
||||
}
|
||||
```
|
||||
|
||||
### OpenCode Zen
|
||||
### OpenCode
|
||||
|
||||
- 提供商:`opencode`
|
||||
- 认证:`OPENCODE_API_KEY`(或 `OPENCODE_ZEN_API_KEY`)
|
||||
- 示例模型:`opencode/claude-opus-4-5`
|
||||
- CLI:`openclaw onboard --auth-choice opencode-zen`
|
||||
- 认证: `OPENCODE_API_KEY` (或 `OPENCODE_ZEN_API_KEY`)
|
||||
- Zen 运行时提供商: `opencode`
|
||||
- Go 运行时提供商: `opencode-go`
|
||||
- 示例模型: `opencode/claude-opus-4-6`, `opencode-go/kimi-k2.5`
|
||||
- CLI: `openclaw onboard --auth-choice opencode-zen` 或 `openclaw onboard --auth-choice opencode-go`
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: { defaults: { model: { primary: "opencode/claude-opus-4-5" } } },
|
||||
agents: { defaults: { model: { primary: "opencode/claude-opus-4-6" } } },
|
||||
}
|
||||
```
|
||||
|
||||
### Google Gemini(API 密钥)
|
||||
|
||||
- 提供商:`google`
|
||||
- 认证:`GEMINI_API_KEY`
|
||||
- 示例模型:`google/gemini-3-pro-preview`
|
||||
- CLI:`openclaw onboard --auth-choice gemini-api-key`
|
||||
- 提供商: `google`
|
||||
- 认证: `GEMINI_API_KEY`
|
||||
- 可选轮换: `GEMINI_API_KEYS`, `GEMINI_API_KEY_1`, `GEMINI_API_KEY_2`, `GOOGLE_API_KEY` 备选,以及 `OPENCLAW_LIVE_GEMINI_KEY` (单个覆盖)
|
||||
- 示例模型: `google/gemini-3.1-pro-preview`, `google/gemini-3-flash-preview`
|
||||
- 兼容性:使用旧版 OpenClaw 配置的 `google/gemini-3.1-flash-preview` 会被规范化为 `google/gemini-3-flash-preview`
|
||||
- CLI: `openclaw onboard --auth-choice gemini-api-key`
|
||||
|
||||
### Google Vertex、Antigravity 和 Gemini CLI
|
||||
### Google Vertex 和 Gemini CLI
|
||||
|
||||
- 提供商:`google-vertex`、`google-antigravity`、`google-gemini-cli`
|
||||
- 认证:Vertex 使用 gcloud ADC;Antigravity/Gemini CLI 使用各自的认证流程
|
||||
- Antigravity OAuth 作为捆绑插件提供(`google-antigravity-auth`,默认禁用)。
|
||||
- 启用:`openclaw plugins enable google-antigravity-auth`
|
||||
- 登录:`openclaw models auth login --provider google-antigravity --set-default`
|
||||
- Gemini CLI OAuth 作为捆绑插件提供(`google-gemini-cli-auth`,默认禁用)。
|
||||
- 启用:`openclaw plugins enable google-gemini-cli-auth`
|
||||
- 登录:`openclaw models auth login --provider google-gemini-cli --set-default`
|
||||
- 注意:你**不需要**将客户端 ID 或密钥粘贴到 `openclaw.json` 中。CLI 登录流程将令牌存储在 Gateway 网关主机的认证配置文件中。
|
||||
- 提供商: `google-vertex`, `google-gemini-cli`
|
||||
- 认证:Vertex 使用 gcloud ADC;Gemini CLI 使用其 OAuth 流程
|
||||
- 注意:OpenClaw 中的 Gemini CLI OAuth 是非官方集成。部分用户报告称在使用第三方客户端后 Google 账户受到限制。请查阅 Google 条款,如果你选择继续,建议使用非关键账户。
|
||||
- Gemini CLI OAuth 作为内置的 `google` 插件的一部分提供。
|
||||
- 启用: `openclaw plugins enable google`
|
||||
- 登录: `openclaw models auth login --provider google-gemini-cli --set-default`
|
||||
- 注意:你确实 **不** 需要将 client ID 或 secret 粘贴到 `openclaw.json`中。CLI 登录流程将令牌存储在 Gateway 网关主机的认证配置文件中。
|
||||
|
||||
### Z.AI (GLM)
|
||||
|
||||
- 提供商:`zai`
|
||||
- 认证:`ZAI_API_KEY`
|
||||
- 示例模型:`zai/glm-4.7`
|
||||
- CLI:`openclaw onboard --auth-choice zai-api-key`
|
||||
- 别名:`z.ai/*` 和 `z-ai/*` 规范化为 `zai/*`
|
||||
- 提供商: `zai`
|
||||
- 认证: `ZAI_API_KEY`
|
||||
- 示例模型: `zai/glm-5`
|
||||
- CLI: `openclaw onboard --auth-choice zai-api-key`
|
||||
- 别名: `z.ai/*` 和 `z-ai/*` 规范化为 `zai/*`
|
||||
|
||||
### Vercel AI Gateway
|
||||
|
||||
- 提供商:`vercel-ai-gateway`
|
||||
- 认证:`AI_GATEWAY_API_KEY`
|
||||
- 示例模型:`vercel-ai-gateway/anthropic/claude-opus-4.5`
|
||||
- CLI:`openclaw onboard --auth-choice ai-gateway-api-key`
|
||||
- 提供商: `vercel-ai-gateway`
|
||||
- 认证: `AI_GATEWAY_API_KEY`
|
||||
- 示例模型: `vercel-ai-gateway/anthropic/claude-opus-4.6`
|
||||
- CLI: `openclaw onboard --auth-choice ai-gateway-api-key`
|
||||
|
||||
### 其他内置提供商
|
||||
### Kilo Gateway
|
||||
|
||||
- OpenRouter:`openrouter`(`OPENROUTER_API_KEY`)
|
||||
- 示例模型:`openrouter/anthropic/claude-sonnet-4-5`
|
||||
- xAI:`xai`(`XAI_API_KEY`)
|
||||
- Groq:`groq`(`GROQ_API_KEY`)
|
||||
- Cerebras:`cerebras`(`CEREBRAS_API_KEY`)
|
||||
- 提供商: `kilocode`
|
||||
- 认证: `KILOCODE_API_KEY`
|
||||
- 示例模型: `kilocode/anthropic/claude-opus-4.6`
|
||||
- CLI: `openclaw onboard --kilocode-api-key <key>`
|
||||
- 基础 URL: `https://api.kilo.ai/api/gateway/`
|
||||
- 扩展的内置目录包括 GLM-5 Free、MiniMax M2.5 Free、GPT-5.2、Gemini 3 Pro Preview、Gemini 3 Flash Preview、Grok Code Fast 1 和 Kimi K2.5。
|
||||
|
||||
参阅 [/providers/kilocode](/providers/kilocode) 了解详情。
|
||||
|
||||
### 其他内置提供商插件
|
||||
|
||||
- OpenRouter: `openrouter` (`OPENROUTER_API_KEY`)
|
||||
- 示例模型: `openrouter/anthropic/claude-sonnet-4-5`
|
||||
- Kilo Gateway: `kilocode` (`KILOCODE_API_KEY`)
|
||||
- 示例模型: `kilocode/anthropic/claude-opus-4.6`
|
||||
- MiniMax: `minimax` (`MINIMAX_API_KEY`)
|
||||
- Moonshot: `moonshot` (`MOONSHOT_API_KEY`)
|
||||
- Kimi Coding: `kimi-coding` (`KIMI_API_KEY` 或 `KIMICODE_API_KEY`)
|
||||
- Qianfan: `qianfan` (`QIANFAN_API_KEY`)
|
||||
- Model Studio: `modelstudio` (`MODELSTUDIO_API_KEY`)
|
||||
- NVIDIA: `nvidia` (`NVIDIA_API_KEY`)
|
||||
- Together: `together` (`TOGETHER_API_KEY`)
|
||||
- Venice: `venice` (`VENICE_API_KEY`)
|
||||
- Xiaomi: `xiaomi` (`XIAOMI_API_KEY`)
|
||||
- Vercel AI Gateway: `vercel-ai-gateway` (`AI_GATEWAY_API_KEY`)
|
||||
- Hugging Face Inference: `huggingface` (`HUGGINGFACE_HUB_TOKEN` 或 `HF_TOKEN`)
|
||||
- Cloudflare AI Gateway: `cloudflare-ai-gateway` (`CLOUDFLARE_AI_GATEWAY_API_KEY`)
|
||||
- Volcengine: `volcengine` (`VOLCANO_ENGINE_API_KEY`)
|
||||
- BytePlus: `byteplus` (`BYTEPLUS_API_KEY`)
|
||||
- xAI: `xai` (`XAI_API_KEY`)
|
||||
- Mistral: `mistral` (`MISTRAL_API_KEY`)
|
||||
- 示例模型: `mistral/mistral-large-latest`
|
||||
- CLI: `openclaw onboard --auth-choice mistral-api-key`
|
||||
- Groq: `groq` (`GROQ_API_KEY`)
|
||||
- Cerebras: `cerebras` (`CEREBRAS_API_KEY`)
|
||||
- Cerebras 上的 GLM 模型使用 ID `zai-glm-4.7` 和 `zai-glm-4.6`。
|
||||
- OpenAI 兼容的基础 URL:`https://api.cerebras.ai/v1`。
|
||||
- Mistral:`mistral`(`MISTRAL_API_KEY`)
|
||||
- GitHub Copilot:`github-copilot`(`COPILOT_GITHUB_TOKEN` / `GH_TOKEN` / `GITHUB_TOKEN`)
|
||||
- 兼容 OpenAI 的基础 URL: `https://api.cerebras.ai/v1`。
|
||||
- GitHub Copilot: `github-copilot` (`COPILOT_GITHUB_TOKEN`/`GH_TOKEN`/`GITHUB_TOKEN`)
|
||||
- Hugging Face Inference 示例模型: `huggingface/deepseek-ai/DeepSeek-R1`;CLI: `openclaw onboard --auth-choice huggingface-api-key`。参阅 [Hugging Face (Inference)](/providers/huggingface)。
|
||||
|
||||
## 通过 `models.providers` 配置的提供商(自定义/基础 URL)
|
||||
## 通过以下方式提供的提供商 `models.providers` (自定义/基础 URL)
|
||||
|
||||
使用 `models.providers`(或 `models.json`)添加**自定义**提供商或 OpenAI/Anthropic 兼容的代理。
|
||||
使用 `models.providers` (或 `models.json`)来添加 **自定义** 提供商或 OpenAI/Anthropic 兼容代理。
|
||||
|
||||
下方许多内置提供商插件已经发布了默认目录。
|
||||
使用显式的 `models.providers.<id>` 条目仅在你需要覆盖默认基础 URL、请求头或模型列表时使用。
|
||||
|
||||
### Moonshot AI (Kimi)
|
||||
|
||||
Moonshot 使用 OpenAI 兼容端点,因此将其配置为自定义提供商:
|
||||
Moonshot 使用兼容 OpenAI 的端点,因此将其配置为自定义提供商:
|
||||
|
||||
- 提供商:`moonshot`
|
||||
- 认证:`MOONSHOT_API_KEY`
|
||||
- 示例模型:`moonshot/kimi-k2.5`
|
||||
- 提供商: `moonshot`
|
||||
- 认证: `MOONSHOT_API_KEY`
|
||||
- 示例模型: `moonshot/kimi-k2.5`
|
||||
|
||||
Kimi K2 模型 ID:
|
||||
|
||||
{/_ moonshot-kimi-k2-model-refs:start _/ && null}
|
||||
[//]: # "moonshot-kimi-k2-model-refs:start"
|
||||
|
||||
- `moonshot/kimi-k2.5`
|
||||
- `moonshot/kimi-k2-0905-preview`
|
||||
- `moonshot/kimi-k2-turbo-preview`
|
||||
- `moonshot/kimi-k2-thinking`
|
||||
- `moonshot/kimi-k2-thinking-turbo`
|
||||
{/_ moonshot-kimi-k2-model-refs:end _/ && null}
|
||||
|
||||
[//]: # "moonshot-kimi-k2-model-refs:end"
|
||||
|
||||
```json5
|
||||
{
|
||||
@@ -172,9 +284,9 @@ Kimi K2 模型 ID:
|
||||
|
||||
Kimi Coding 使用 Moonshot AI 的 Anthropic 兼容端点:
|
||||
|
||||
- 提供商:`kimi-coding`
|
||||
- 认证:`KIMI_API_KEY`
|
||||
- 示例模型:`kimi-coding/k2p5`
|
||||
- 提供商: `kimi-coding`
|
||||
- 认证: `KIMI_API_KEY`
|
||||
- 示例模型: `kimi-coding/k2p5`
|
||||
|
||||
```json5
|
||||
{
|
||||
@@ -185,13 +297,12 @@ Kimi Coding 使用 Moonshot AI 的 Anthropic 兼容端点:
|
||||
}
|
||||
```
|
||||
|
||||
### Qwen OAuth(免费层级)
|
||||
### Qwen OAuth(免费套餐)
|
||||
|
||||
Qwen 通过设备码流程提供对 Qwen Coder + Vision 的 OAuth 访问。
|
||||
启用捆绑插件,然后登录:
|
||||
内置提供商插件默认启用,只需登录:
|
||||
|
||||
```bash
|
||||
openclaw plugins enable qwen-portal-auth
|
||||
openclaw models auth login --provider qwen-portal --set-default
|
||||
```
|
||||
|
||||
@@ -200,21 +311,85 @@ openclaw models auth login --provider qwen-portal --set-default
|
||||
- `qwen-portal/coder-model`
|
||||
- `qwen-portal/vision-model`
|
||||
|
||||
参见 [/providers/qwen](/providers/qwen) 了解设置详情和注意事项。
|
||||
参阅 [/providers/qwen](/providers/qwen) 了解详情和注意事项。
|
||||
|
||||
### Synthetic
|
||||
### 火山引擎(豆包)
|
||||
|
||||
Synthetic 通过 `synthetic` 提供商提供 Anthropic 兼容模型:
|
||||
火山引擎提供对豆包及中国其他模型的访问。
|
||||
|
||||
- 提供商:`synthetic`
|
||||
- 认证:`SYNTHETIC_API_KEY`
|
||||
- 示例模型:`synthetic/hf:MiniMaxAI/MiniMax-M2.1`
|
||||
- CLI:`openclaw onboard --auth-choice synthetic-api-key`
|
||||
- 提供商: `volcengine` (编码: `volcengine-plan`)
|
||||
- 认证: `VOLCANO_ENGINE_API_KEY`
|
||||
- 示例模型: `volcengine/doubao-seed-1-8-251228`
|
||||
- CLI: `openclaw onboard --auth-choice volcengine-api-key`
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: { model: { primary: "synthetic/hf:MiniMaxAI/MiniMax-M2.1" } },
|
||||
defaults: { model: { primary: "volcengine/doubao-seed-1-8-251228" } },
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
可用模型:
|
||||
|
||||
- `volcengine/doubao-seed-1-8-251228` (豆包 Seed 1.8)
|
||||
- `volcengine/doubao-seed-code-preview-251028`
|
||||
- `volcengine/kimi-k2-5-260127` (Kimi K2.5)
|
||||
- `volcengine/glm-4-7-251222` (GLM 4.7)
|
||||
- `volcengine/deepseek-v3-2-251201` (DeepSeek V3.2 128K)
|
||||
|
||||
编码模型(`volcengine-plan`):
|
||||
|
||||
- `volcengine-plan/ark-code-latest`
|
||||
- `volcengine-plan/doubao-seed-code`
|
||||
- `volcengine-plan/kimi-k2.5`
|
||||
- `volcengine-plan/kimi-k2-thinking`
|
||||
- `volcengine-plan/glm-4.7`
|
||||
|
||||
### BytePlus(国际版)
|
||||
|
||||
BytePlus ARK 为国际用户提供与火山引擎相同的模型访问。
|
||||
|
||||
- 提供商: `byteplus` (编码: `byteplus-plan`)
|
||||
- 认证: `BYTEPLUS_API_KEY`
|
||||
- 示例模型: `byteplus/seed-1-8-251228`
|
||||
- CLI: `openclaw onboard --auth-choice byteplus-api-key`
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: { model: { primary: "byteplus/seed-1-8-251228" } },
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
可用模型:
|
||||
|
||||
- `byteplus/seed-1-8-251228` (Seed 1.8)
|
||||
- `byteplus/kimi-k2-5-260127` (Kimi K2.5)
|
||||
- `byteplus/glm-4-7-251222` (GLM 4.7)
|
||||
|
||||
编码模型(`byteplus-plan`):
|
||||
|
||||
- `byteplus-plan/ark-code-latest`
|
||||
- `byteplus-plan/doubao-seed-code`
|
||||
- `byteplus-plan/kimi-k2.5`
|
||||
- `byteplus-plan/kimi-k2-thinking`
|
||||
- `byteplus-plan/glm-4.7`
|
||||
|
||||
### Synthetic
|
||||
|
||||
Synthetic 提供 Anthropic 兼容模型,位于 `synthetic` 提供商背后:
|
||||
|
||||
- 提供商: `synthetic`
|
||||
- 认证: `SYNTHETIC_API_KEY`
|
||||
- 示例模型: `synthetic/hf:MiniMaxAI/MiniMax-M2.5`
|
||||
- CLI: `openclaw onboard --auth-choice synthetic-api-key`
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: { model: { primary: "synthetic/hf:MiniMaxAI/MiniMax-M2.5" } },
|
||||
},
|
||||
models: {
|
||||
mode: "merge",
|
||||
@@ -223,7 +398,7 @@ Synthetic 通过 `synthetic` 提供商提供 Anthropic 兼容模型:
|
||||
baseUrl: "https://api.synthetic.new/anthropic",
|
||||
apiKey: "${SYNTHETIC_API_KEY}",
|
||||
api: "anthropic-messages",
|
||||
models: [{ id: "hf:MiniMaxAI/MiniMax-M2.1", name: "MiniMax M2.1" }],
|
||||
models: [{ id: "hf:MiniMaxAI/MiniMax-M2.5", name: "MiniMax M2.5" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -232,21 +407,21 @@ Synthetic 通过 `synthetic` 提供商提供 Anthropic 兼容模型:
|
||||
|
||||
### MiniMax
|
||||
|
||||
MiniMax 通过 `models.providers` 配置,因为它使用自定义端点:
|
||||
MiniMax 通过以下方式配置 `models.providers` ,因为它使用自定义端点:
|
||||
|
||||
- MiniMax(Anthropic 兼容):`--auth-choice minimax-api`
|
||||
- 认证:`MINIMAX_API_KEY`
|
||||
- MiniMax(Anthropic 兼容): `--auth-choice minimax-api`
|
||||
- 认证: `MINIMAX_API_KEY`
|
||||
|
||||
参见 [/providers/minimax](/providers/minimax) 了解设置详情、模型选项和配置片段。
|
||||
参阅 [/providers/minimax](/providers/minimax) 了解详情、模型选项和配置代码片段。
|
||||
|
||||
### Ollama
|
||||
|
||||
Ollama 是提供 OpenAI 兼容 API 的本地 LLM 运行时:
|
||||
Ollama 作为内置提供商插件提供,并使用 Ollama 的原生 API:
|
||||
|
||||
- 提供商:`ollama`
|
||||
- 提供商: `ollama`
|
||||
- 认证:无需(本地服务器)
|
||||
- 示例模型:`ollama/llama3.3`
|
||||
- 安装:https://ollama.ai
|
||||
- 示例模型: `ollama/llama3.3`
|
||||
- 安装: [https://ollama.com/download](https://ollama.com/download)
|
||||
|
||||
```bash
|
||||
# Install Ollama, then pull a model:
|
||||
@@ -261,18 +436,73 @@ ollama pull llama3.3
|
||||
}
|
||||
```
|
||||
|
||||
当 Ollama 在本地 `http://127.0.0.1:11434/v1` 运行时会自动检测。参见 [/providers/ollama](/providers/ollama) 了解模型推荐和自定义配置。
|
||||
Ollama 在本地通过以下地址检测 `http://127.0.0.1:11434` 当你通过以下方式选择启用时
|
||||
`OLLAMA_API_KEY`,内置提供商插件会将 Ollama 直接添加到
|
||||
`openclaw onboard` 和模型选择器中。参阅 [/providers/ollama](/providers/ollama)
|
||||
了解新手引导、云端/本地模式和自定义配置。
|
||||
|
||||
### vLLM
|
||||
|
||||
vLLM 作为内置提供商插件提供,用于本地/自托管的兼容 OpenAI 服务器:
|
||||
|
||||
- 提供商: `vllm`
|
||||
- 认证:可选(取决于你的服务器)
|
||||
- 默认基础 URL: `http://127.0.0.1:8000/v1`
|
||||
|
||||
要在本地选择启用自动发现(如果你的服务器不强制认证,任何值均可):
|
||||
|
||||
```bash
|
||||
export VLLM_API_KEY="vllm-local"
|
||||
```
|
||||
|
||||
然后设置一个模型(替换为由 `/v1/models`):
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: { model: { primary: "vllm/your-model-id" } },
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
参阅 [/providers/vllm](/providers/vllm) 了解详情。
|
||||
|
||||
### SGLang
|
||||
|
||||
SGLang 作为内置提供商插件提供,用于快速自托管的兼容 OpenAI 服务器:
|
||||
|
||||
- 提供商: `sglang`
|
||||
- 认证:可选(取决于你的服务器)
|
||||
- 默认基础 URL: `http://127.0.0.1:30000/v1`
|
||||
|
||||
要在本地选择启用自动发现(如果你的服务器不强制认证,任何值均可):
|
||||
|
||||
```bash
|
||||
export SGLANG_API_KEY="sglang-local"
|
||||
```
|
||||
|
||||
然后设置一个模型(替换为由 `/v1/models`):
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: { model: { primary: "sglang/your-model-id" } },
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
参阅 [/providers/sglang](/providers/sglang) 了解详情。
|
||||
|
||||
### 本地代理(LM Studio、vLLM、LiteLLM 等)
|
||||
|
||||
示例(OpenAI 兼容):
|
||||
示例(兼容 OpenAI):
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "lmstudio/minimax-m2.1-gs32" },
|
||||
models: { "lmstudio/minimax-m2.1-gs32": { alias: "Minimax" } },
|
||||
model: { primary: "lmstudio/minimax-m2.5-gs32" },
|
||||
models: { "lmstudio/minimax-m2.5-gs32": { alias: "Minimax" } },
|
||||
},
|
||||
},
|
||||
models: {
|
||||
@@ -283,8 +513,8 @@ ollama pull llama3.3
|
||||
api: "openai-completions",
|
||||
models: [
|
||||
{
|
||||
id: "minimax-m2.1-gs32",
|
||||
name: "MiniMax M2.1",
|
||||
id: "minimax-m2.5-gs32",
|
||||
name: "MiniMax M2.5",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
@@ -300,21 +530,24 @@ ollama pull llama3.3
|
||||
|
||||
注意事项:
|
||||
|
||||
- 对于自定义提供商,`reasoning`、`input`、`cost`、`contextWindow` 和 `maxTokens` 是可选的。
|
||||
- 对于自定义提供商, `reasoning`, `input`, `cost`, `contextWindow`,以及`maxTokens` 是可选的。
|
||||
省略时,OpenClaw 默认为:
|
||||
- `reasoning: false`
|
||||
- `input: ["text"]`
|
||||
- `cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }`
|
||||
- `contextWindow: 200000`
|
||||
- `maxTokens: 8192`
|
||||
- 建议:设置与你的代理/模型限制匹配的显式值。
|
||||
- 建议:设置与你的代理/模型限制相匹配的显式值。
|
||||
- 对于 `api: "openai-completions"` 在非原生端点上(任何非空的 `baseUrl` 且主机不是 `api.openai.com`),OpenClaw 强制使用 `compat.supportsDeveloperRole: false` 以避免提供商对不支持的 `developer` 角色返回 400 错误。
|
||||
- 如果 `baseUrl` 为空/省略,OpenClaw 保持默认的 OpenAI 行为(解析为 `api.openai.com`)。
|
||||
- 为安全起见,显式的 `compat.supportsDeveloperRole: true` 在非原生 `openai-completions` 端点上仍会被覆盖。
|
||||
|
||||
## CLI 示例
|
||||
|
||||
```bash
|
||||
openclaw onboard --auth-choice opencode-zen
|
||||
openclaw models set opencode/claude-opus-4-5
|
||||
openclaw models set opencode/claude-opus-4-6
|
||||
openclaw models list
|
||||
```
|
||||
|
||||
另请参阅:[/gateway/configuration](/gateway/configuration) 了解完整配置示例。
|
||||
另请参阅: [/gateway/configuration](/gateway/configuration) 查看完整配置示例。
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
summary: 关于 OpenClaw 安装、配置和使用的常见问题
|
||||
title: 常见问题
|
||||
x-i18n:
|
||||
generated_at: "2026-02-01T21:32:04Z"
|
||||
generated_at: "2026-03-16T01:39:16Z"
|
||||
model: claude-opus-4-5
|
||||
provider: pi
|
||||
source_hash: 5a611f2fda3325b1c7a9ec518616d87c78be41e2bfbe86244ae4f48af3815a26
|
||||
source_hash: 6e6a4a63fb73dca24dbe77928b51c6b2e5d51ec883fb36c64e2e40ef027050e9
|
||||
source_path: help/faq.md
|
||||
workflow: 15
|
||||
---
|
||||
@@ -687,7 +687,7 @@ Gemini CLI 使用**插件认证流程**,而不是 `openclaw.json` 中的 clien
|
||||
|
||||
步骤:
|
||||
|
||||
1. 启用插件:`openclaw plugins enable google-gemini-cli-auth`
|
||||
1. 启用插件:`openclaw plugins enable google`
|
||||
2. 登录:`openclaw models auth login --provider google-gemini-cli --set-default`
|
||||
|
||||
这会在 Gateway 网关主机上将 OAuth 令牌存储为认证配置文件。详情:[模型提供商](/concepts/model-providers)。
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
---
|
||||
read_when:
|
||||
- 制作或验证 OpenClaw macOS 发布版本
|
||||
- 更新 Sparkle appcast 或订阅源资源
|
||||
summary: OpenClaw macOS 发布清单(Sparkle 订阅源、打包、签名)
|
||||
title: macOS 发布
|
||||
x-i18n:
|
||||
generated_at: "2026-02-01T21:33:17Z"
|
||||
model: claude-opus-4-5
|
||||
provider: pi
|
||||
source_hash: 703c08c13793cd8c96bd4c31fb4904cdf4ffff35576e7ea48a362560d371cb30
|
||||
source_path: platforms/mac/release.md
|
||||
workflow: 15
|
||||
---
|
||||
|
||||
# OpenClaw macOS 发布(Sparkle)
|
||||
|
||||
本应用现已支持 Sparkle 自动更新。发布构建必须经过 Developer ID 签名、压缩,并发布包含签名的 appcast 条目。
|
||||
|
||||
## 前提条件
|
||||
|
||||
- 已安装 Developer ID Application 证书(示例:`Developer ID Application: <Developer Name> (<TEAMID>)`)。
|
||||
- 环境变量 `SPARKLE_PRIVATE_KEY_FILE` 已设置为 Sparkle ed25519 私钥路径(公钥已嵌入 Info.plist)。如果缺失,请检查 `~/.profile`。
|
||||
- 用于 `xcrun notarytool` 的公证凭据(钥匙串配置文件或 API 密钥),以实现通过 Gatekeeper 安全分发的 DMG/zip。
|
||||
- 我们使用名为 `openclaw-notary` 的钥匙串配置文件,由 shell 配置文件中的 App Store Connect API 密钥环境变量创建:
|
||||
- `APP_STORE_CONNECT_API_KEY_P8`、`APP_STORE_CONNECT_KEY_ID`、`APP_STORE_CONNECT_ISSUER_ID`
|
||||
- `echo "$APP_STORE_CONNECT_API_KEY_P8" | sed 's/\\n/\n/g' > /tmp/openclaw-notary.p8`
|
||||
- `xcrun notarytool store-credentials "openclaw-notary" --key /tmp/openclaw-notary.p8 --key-id "$APP_STORE_CONNECT_KEY_ID" --issuer "$APP_STORE_CONNECT_ISSUER_ID"`
|
||||
- 已安装 `pnpm` 依赖(`pnpm install --config.node-linker=hoisted`)。
|
||||
- Sparkle 工具通过 SwiftPM 自动获取,位于 `apps/macos/.build/artifacts/sparkle/Sparkle/bin/`(`sign_update`、`generate_appcast` 等)。
|
||||
|
||||
## 构建与打包
|
||||
|
||||
注意事项:
|
||||
|
||||
- `APP_BUILD` 映射到 `CFBundleVersion`/`sparkle:version`;保持纯数字且单调递增(不含 `-beta`),否则 Sparkle 会将其视为相同版本。
|
||||
- 默认为当前架构(`$(uname -m)`)。对于发布/通用构建,设置 `BUILD_ARCHS="arm64 x86_64"`(或 `BUILD_ARCHS=all`)。
|
||||
- 使用 `scripts/package-mac-dist.sh` 生成发布产物(zip + DMG + 公证)。使用 `scripts/package-mac-app.sh` 进行本地/开发打包。
|
||||
|
||||
```bash
|
||||
# 从仓库根目录运行;设置发布 ID 以启用 Sparkle 订阅源。
|
||||
# APP_BUILD 必须为纯数字且单调递增,以便 Sparkle 正确比较。
|
||||
BUNDLE_ID=bot.molt.mac \
|
||||
APP_VERSION=2026.1.27-beta.1 \
|
||||
APP_BUILD="$(git rev-list --count HEAD)" \
|
||||
BUILD_CONFIG=release \
|
||||
SIGN_IDENTITY="Developer ID Application: <Developer Name> (<TEAMID>)" \
|
||||
scripts/package-mac-app.sh
|
||||
|
||||
# 打包用于分发的 zip(包含资源分支以支持 Sparkle 增量更新)
|
||||
ditto -c -k --sequesterRsrc --keepParent dist/OpenClaw.app dist/OpenClaw-2026.1.27-beta.1.zip
|
||||
|
||||
# 可选:同时构建适合用户使用的样式化 DMG(拖拽到 /Applications)
|
||||
scripts/create-dmg.sh dist/OpenClaw.app dist/OpenClaw-2026.1.27-beta.1.dmg
|
||||
|
||||
# 推荐:构建 + 公证/装订 zip + DMG
|
||||
# 首先,创建一次钥匙串配置文件:
|
||||
# xcrun notarytool store-credentials "openclaw-notary" \
|
||||
# --apple-id "<apple-id>" --team-id "<team-id>" --password "<app-specific-password>"
|
||||
NOTARIZE=1 NOTARYTOOL_PROFILE=openclaw-notary \
|
||||
BUNDLE_ID=bot.molt.mac \
|
||||
APP_VERSION=2026.1.27-beta.1 \
|
||||
APP_BUILD="$(git rev-list --count HEAD)" \
|
||||
BUILD_CONFIG=release \
|
||||
SIGN_IDENTITY="Developer ID Application: <Developer Name> (<TEAMID>)" \
|
||||
scripts/package-mac-dist.sh
|
||||
|
||||
# 可选:随发布一起提供 dSYM
|
||||
ditto -c -k --keepParent apps/macos/.build/release/OpenClaw.app.dSYM dist/OpenClaw-2026.1.27-beta.1.dSYM.zip
|
||||
```
|
||||
|
||||
## Appcast 条目
|
||||
|
||||
使用发布说明生成器,以便 Sparkle 渲染格式化的 HTML 说明:
|
||||
|
||||
```bash
|
||||
SPARKLE_PRIVATE_KEY_FILE=/path/to/ed25519-private-key scripts/make_appcast.sh dist/OpenClaw-2026.1.27-beta.1.zip https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml
|
||||
```
|
||||
|
||||
从 `CHANGELOG.md`(通过 [`scripts/changelog-to-html.sh`](https://github.com/openclaw/openclaw/blob/main/scripts/changelog-to-html.sh))生成 HTML 发布说明,并将其嵌入 appcast 条目。
|
||||
发布时,将更新后的 `appcast.xml` 与发布资源(zip + dSYM)一起提交。
|
||||
|
||||
## 发布与验证
|
||||
|
||||
- 将 `OpenClaw-2026.1.27-beta.1.zip`(和 `OpenClaw-2026.1.27-beta.1.dSYM.zip`)上传到标签 `v2026.1.27-beta.1` 对应的 GitHub 发布。
|
||||
- 确保原始 appcast URL 与内置的订阅源匹配:`https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml`。
|
||||
- 完整性检查:
|
||||
- `curl -I https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml` 返回 200。
|
||||
- `curl -I <enclosure url>` 在资源上传后返回 200。
|
||||
- 在之前的公开构建版本上,从 About 选项卡运行"Check for Updates…",验证 Sparkle 能正常安装新构建。
|
||||
|
||||
完成定义:已签名的应用 + appcast 已发布,从旧版本的更新流程正常工作,且发布资源已附加到 GitHub 发布。
|
||||
@@ -1,123 +1,48 @@
|
||||
---
|
||||
read_when:
|
||||
- 发布新的 npm 版本
|
||||
- 发布新的 macOS 应用版本
|
||||
- 发布前验证元数据
|
||||
summary: npm + macOS 应用的逐步发布清单
|
||||
- 查找公开发布渠道的定义
|
||||
- 查找版本命名与发布节奏
|
||||
summary: 公开发布渠道、版本命名与发布节奏
|
||||
title: 发布策略
|
||||
x-i18n:
|
||||
generated_at: "2026-02-03T10:09:28Z"
|
||||
model: claude-opus-4-5
|
||||
generated_at: "2026-03-15T19:23:11Z"
|
||||
model: claude-opus-4-6
|
||||
provider: pi
|
||||
source_hash: 1a684bc26665966eb3c9c816d58d18eead008fd710041181ece38c21c5ff1c62
|
||||
source_hash: df332d3169de7099661725d9266955456e80fc3d3ff95cb7aaf9997a02f0baaf
|
||||
source_path: reference/RELEASING.md
|
||||
workflow: 15
|
||||
---
|
||||
|
||||
# 发布清单(npm + macOS)
|
||||
# 发布策略
|
||||
|
||||
从仓库根目录使用 `pnpm`(Node 22+)。在打标签/发布前保持工作树干净。
|
||||
OpenClaw 有三个公开发布渠道:
|
||||
|
||||
## 操作员触发
|
||||
- stable:带标签的正式发布,发布到 npm `latest`
|
||||
- beta:预发布标签,发布到 npm `beta`
|
||||
- dev:`main` 分支的最新提交
|
||||
|
||||
当操作员说"release"时,立即执行此预检(除非遇到阻碍否则不要额外提问):
|
||||
## 版本命名
|
||||
|
||||
- 阅读本文档和 `docs/platforms/mac/release.md`。
|
||||
- 从 `~/.profile` 加载环境变量并确认 `SPARKLE_PRIVATE_KEY_FILE` + App Store Connect 变量已设置(SPARKLE_PRIVATE_KEY_FILE 应位于 `~/.profile` 中)。
|
||||
- 如需要,使用 `~/Library/CloudStorage/Dropbox/Backup/Sparkle` 中的 Sparkle 密钥。
|
||||
- 正式发布版本号:`YYYY.M.D`
|
||||
- Git 标签:`vYYYY.M.D`
|
||||
- Beta 预发布版本号:`YYYY.M.D-beta.N`
|
||||
- Git 标签:`vYYYY.M.D-beta.N`
|
||||
- 月份和日期不补零
|
||||
- `latest` 表示当前 npm 正式发布版本
|
||||
- `beta` 表示当前 npm 预发布版本
|
||||
- Beta 版本可能会在 macOS 应用跟进之前发布
|
||||
|
||||
1. **版本和元数据**
|
||||
## 发布节奏
|
||||
|
||||
- [ ] 更新 `package.json` 版本(例如 `2026.1.29`)。
|
||||
- [ ] 运行 `pnpm plugins:sync` 以对齐扩展包版本和变更日志。
|
||||
- [ ] 更新 CLI/版本字符串:[`src/cli/program.ts`](https://github.com/openclaw/openclaw/blob/main/src/cli/program.ts) 和 [`src/provider-web.ts`](https://github.com/openclaw/openclaw/blob/main/src/provider-web.ts) 中的 Baileys user agent。
|
||||
- [ ] 确认包元数据(name、description、repository、keywords、license)以及 `bin` 映射指向 [`openclaw.mjs`](https://github.com/openclaw/openclaw/blob/main/openclaw.mjs) 作为 `openclaw`。
|
||||
- [ ] 如果依赖项有变化,运行 `pnpm install` 确保 `pnpm-lock.yaml` 是最新的。
|
||||
- 发布遵循 beta 优先原则
|
||||
- 仅在最新的 beta 版本验证通过后才会发布正式版本
|
||||
- 详细的发布流程、审批、凭证和恢复说明仅限维护者查阅
|
||||
|
||||
2. **构建和产物**
|
||||
## 公开参考
|
||||
|
||||
- [ ] 如果 A2UI 输入有变化,运行 `pnpm canvas:a2ui:bundle` 并提交更新后的 [`src/canvas-host/a2ui/a2ui.bundle.js`](https://github.com/openclaw/openclaw/blob/main/src/canvas-host/a2ui/a2ui.bundle.js)。
|
||||
- [ ] `pnpm run build`(重新生成 `dist/`)。
|
||||
- [ ] 验证 npm 包的 `files` 包含所有必需的 `dist/*` 文件夹(特别是用于 headless node + ACP CLI 的 `dist/node-host/**` 和 `dist/acp/**`)。
|
||||
- [ ] 确认 `dist/build-info.json` 存在并包含预期的 `commit` 哈希(CLI 横幅在 npm 安装时使用此信息)。
|
||||
- [ ] 可选:构建后运行 `npm pack --pack-destination /tmp`;检查 tarball 内容并保留以备 GitHub 发布使用(**不要**提交它)。
|
||||
- [`.github/workflows/openclaw-npm-release.yml`](https://github.com/openclaw/openclaw/blob/main/.github/workflows/openclaw-npm-release.yml)
|
||||
- [`scripts/openclaw-npm-release-check.ts`](https://github.com/openclaw/openclaw/blob/main/scripts/openclaw-npm-release-check.ts)
|
||||
|
||||
3. **变更日志和文档**
|
||||
|
||||
- [ ] 更新 `CHANGELOG.md`,添加面向用户的亮点(如果文件不存在则创建);按版本严格降序排列条目。
|
||||
- [ ] 确保 README 示例/标志与当前 CLI 行为匹配(特别是新命令或选项)。
|
||||
|
||||
4. **验证**
|
||||
|
||||
- [ ] `pnpm build`
|
||||
- [ ] `pnpm check`
|
||||
- [ ] `pnpm test`(如需覆盖率输出则使用 `pnpm test:coverage`)
|
||||
- [ ] `pnpm release:check`(验证 npm pack 内容)
|
||||
- [ ] `OPENCLAW_INSTALL_SMOKE_SKIP_NONROOT=1 pnpm test:install:smoke`(Docker 安装冒烟测试,快速路径;发布前必需)
|
||||
- 如果已知上一个 npm 发布版本有问题,为预安装步骤设置 `OPENCLAW_INSTALL_SMOKE_PREVIOUS=<last-good-version>` 或 `OPENCLAW_INSTALL_SMOKE_SKIP_PREVIOUS=1`。
|
||||
- [ ](可选)完整安装程序冒烟测试(添加非 root + CLI 覆盖):`pnpm test:install:smoke`
|
||||
- [ ](可选)安装程序 E2E(Docker,运行 `curl -fsSL https://openclaw.ai/install.sh | bash`,新手引导,然后运行真实工具调用):
|
||||
- `pnpm test:install:e2e:openai`(需要 `OPENAI_API_KEY`)
|
||||
- `pnpm test:install:e2e:anthropic`(需要 `ANTHROPIC_API_KEY`)
|
||||
- `pnpm test:install:e2e`(需要两个密钥;运行两个提供商)
|
||||
- [ ](可选)如果你的更改影响发送/接收路径,抽查 Web Gateway 网关。
|
||||
|
||||
5. **macOS 应用(Sparkle)**
|
||||
|
||||
- [ ] 构建并签名 macOS 应用,然后压缩以供分发。
|
||||
- [ ] 生成 Sparkle appcast(通过 [`scripts/make_appcast.sh`](https://github.com/openclaw/openclaw/blob/main/scripts/make_appcast.sh) 生成 HTML 注释)并更新 `appcast.xml`。
|
||||
- [ ] 保留应用 zip(和可选的 dSYM zip)以便附加到 GitHub 发布。
|
||||
- [ ] 按照 [macOS 发布](/platforms/mac/release) 获取确切命令和所需环境变量。
|
||||
- `APP_BUILD` 必须是数字且单调递增(不带 `-beta`),以便 Sparkle 正确比较版本。
|
||||
- 如果进行公证,使用从 App Store Connect API 环境变量创建的 `openclaw-notary` 钥匙串配置文件(参见 [macOS 发布](/platforms/mac/release))。
|
||||
|
||||
6. **发布(npm)**
|
||||
|
||||
- [ ] 确认 git 状态干净;根据需要提交并推送。
|
||||
- [ ] 如需要,`npm login`(验证 2FA)。
|
||||
- [ ] `npm publish --access public`(预发布版本使用 `--tag beta`)。
|
||||
- [ ] 验证注册表:`npm view openclaw version`、`npm view openclaw dist-tags` 和 `npx -y openclaw@X.Y.Z --version`(或 `--help`)。
|
||||
|
||||
### 故障排除(来自 2.0.0-beta2 发布的笔记)
|
||||
|
||||
- **npm pack/publish 挂起或产生巨大 tarball**:`dist/OpenClaw.app` 中的 macOS 应用包(和发布 zip)被扫入包中。通过 `package.json` 的 `files` 白名单发布内容来修复(包含 dist 子目录、docs、skills;排除应用包)。用 `npm pack --dry-run` 确认 `dist/OpenClaw.app` 未列出。
|
||||
- **npm auth dist-tags 的 Web 循环**:使用旧版认证以获取 OTP 提示:
|
||||
- `NPM_CONFIG_AUTH_TYPE=legacy npm dist-tag add openclaw@X.Y.Z latest`
|
||||
- **`npx` 验证失败并显示 `ECOMPROMISED: Lock compromised`**:使用新缓存重试:
|
||||
- `NPM_CONFIG_CACHE=/tmp/npm-cache-$(date +%s) npx -y openclaw@X.Y.Z --version`
|
||||
- **延迟修复后需要重新指向标签**:强制更新并推送标签,然后确保 GitHub 发布资产仍然匹配:
|
||||
- `git tag -f vX.Y.Z && git push -f origin vX.Y.Z`
|
||||
|
||||
7. **GitHub 发布 + appcast**
|
||||
|
||||
- [ ] 打标签并推送:`git tag vX.Y.Z && git push origin vX.Y.Z`(或 `git push --tags`)。
|
||||
- [ ] 为 `vX.Y.Z` 创建/刷新 GitHub 发布,**标题为 `openclaw X.Y.Z`**(不仅仅是标签);正文应包含该版本的**完整**变更日志部分(亮点 + 更改 + 修复),内联显示(无裸链接),且**不得在正文中重复标题**。
|
||||
- [ ] 附加产物:`npm pack` tarball(可选)、`OpenClaw-X.Y.Z.zip` 和 `OpenClaw-X.Y.Z.dSYM.zip`(如果生成)。
|
||||
- [ ] 提交更新后的 `appcast.xml` 并推送(Sparkle 从 main 获取源)。
|
||||
- [ ] 从干净的临时目录(无 `package.json`),运行 `npx -y openclaw@X.Y.Z send --help` 确认安装/CLI 入口点正常工作。
|
||||
- [ ] 宣布/分享发布说明。
|
||||
|
||||
## 插件发布范围(npm)
|
||||
|
||||
我们只发布 `@openclaw/*` 范围下的**现有 npm 插件**。不在 npm 上的内置插件保持**仅磁盘树**(仍在 `extensions/**` 中发布)。
|
||||
|
||||
获取列表的流程:
|
||||
|
||||
1. `npm search @openclaw --json` 并捕获包名。
|
||||
2. 与 `extensions/*/package.json` 名称比较。
|
||||
3. 只发布**交集**(已在 npm 上)。
|
||||
|
||||
当前 npm 插件列表(根据需要更新):
|
||||
|
||||
- @openclaw/bluebubbles
|
||||
- @openclaw/diagnostics-otel
|
||||
- @openclaw/discord
|
||||
- @openclaw/lobster
|
||||
- @openclaw/matrix
|
||||
- @openclaw/msteams
|
||||
- @openclaw/nextcloud-talk
|
||||
- @openclaw/nostr
|
||||
- @openclaw/voice-call
|
||||
- @openclaw/zalo
|
||||
- @openclaw/zalouser
|
||||
|
||||
发布说明还必须标注**默认未启用**的**新可选内置插件**(例如:`tlon`)。
|
||||
维护者使用
|
||||
[`openclaw/maintainers/release/README.md`](https://github.com/openclaw/maintainers/blob/main/release/README.md)
|
||||
中的私有发布文档作为实际操作手册。
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
---
|
||||
read_when:
|
||||
- 你想要一份完整的文档地图
|
||||
summary: 链接到每篇 OpenClaw 文档的导航中心
|
||||
summary: 链接到所有 OpenClaw 文档的导航中心
|
||||
title: 文档导航中心
|
||||
x-i18n:
|
||||
generated_at: "2026-02-04T17:55:29Z"
|
||||
model: claude-opus-4-5
|
||||
generated_at: "2026-03-15T19:29:16Z"
|
||||
model: claude-opus-4-6
|
||||
provider: pi
|
||||
source_hash: c4b4572b64d36c9690988b8f964b0712f551ee6491b18a493701a17d2d352cb4
|
||||
source_hash: e12e8b7881311fdaf08cd297392911dfa30dc46031a7038b6bb9011d166b1669
|
||||
source_path: start/hubs.md
|
||||
workflow: 15
|
||||
---
|
||||
|
||||
# 文档导航中心
|
||||
|
||||
使用这些导航中心发现每一个页面,包括深入解析和参考文档——它们不一定出现在左侧导航栏中。
|
||||
<Note>
|
||||
如果你是 OpenClaw 新用户,请从[入门指南](/start/getting-started)开始。
|
||||
</Note>
|
||||
|
||||
使用这些导航中心发现每一个页面,包括深入解析和参考文档——它们可能不会出现在左侧导航栏中。
|
||||
|
||||
## 从这里开始
|
||||
|
||||
@@ -75,7 +79,6 @@ x-i18n:
|
||||
- [模型提供商中心](/providers/models)
|
||||
- [WhatsApp](/channels/whatsapp)
|
||||
- [Telegram](/channels/telegram)
|
||||
- [Telegram(grammY 注意事项)](/channels/grammy)
|
||||
- [Slack](/channels/slack)
|
||||
- [Discord](/channels/discord)
|
||||
- [Mattermost](/channels/mattermost)(插件)
|
||||
@@ -113,17 +116,18 @@ x-i18n:
|
||||
- [OpenProse](/prose)
|
||||
- [CLI 参考](/cli)
|
||||
- [Exec 工具](/tools/exec)
|
||||
- [PDF 工具](/tools/pdf)
|
||||
- [提权模式](/tools/elevated)
|
||||
- [定时任务](/automation/cron-jobs)
|
||||
- [定时任务 vs 心跳](/automation/cron-vs-heartbeat)
|
||||
- [思考 + 详细输出](/tools/thinking)
|
||||
- [模型](/concepts/models)
|
||||
- [子智能体](/tools/subagents)
|
||||
- [Agent send CLI](/tools/agent-send)
|
||||
- [智能体发送 CLI](/tools/agent-send)
|
||||
- [终端界面](/web/tui)
|
||||
- [浏览器控制](/tools/browser)
|
||||
- [浏览器(Linux 故障排除)](/tools/browser-linux-troubleshooting)
|
||||
- [轮询](/automation/poll)
|
||||
- [投票](/automation/poll)
|
||||
|
||||
## 节点、媒体、语音
|
||||
|
||||
@@ -160,7 +164,6 @@ x-i18n:
|
||||
- [macOS 权限](/platforms/mac/permissions)
|
||||
- [macOS 远程](/platforms/mac/remote)
|
||||
- [macOS 签名](/platforms/mac/signing)
|
||||
- [macOS 发布](/platforms/mac/release)
|
||||
- [macOS Gateway 网关 (launchd)](/platforms/mac/bundled-gateway)
|
||||
- [macOS XPC](/platforms/mac/xpc)
|
||||
- [macOS Skills](/platforms/mac/skills)
|
||||
@@ -183,8 +186,6 @@ x-i18n:
|
||||
## 实验(探索性)
|
||||
|
||||
- [新手引导配置协议](/experiments/onboarding-config-protocol)
|
||||
- [定时任务加固笔记](/experiments/plans/cron-add-hardening)
|
||||
- [群组策略加固笔记](/experiments/plans/group-policy-hardening)
|
||||
- [研究:记忆](/experiments/research/memory)
|
||||
- [模型配置探索](/experiments/proposals/model-config)
|
||||
|
||||
@@ -195,5 +196,5 @@ x-i18n:
|
||||
## 测试 + 发布
|
||||
|
||||
- [测试](/reference/test)
|
||||
- [发布检查清单](/reference/RELEASING)
|
||||
- [发布策略](/reference/RELEASING)
|
||||
- [设备型号](/reference/device-models)
|
||||
|
||||
@@ -5,10 +5,10 @@ read_when:
|
||||
summary: OpenClaw 插件/扩展:发现、配置和安全
|
||||
title: 插件
|
||||
x-i18n:
|
||||
generated_at: "2026-02-03T07:55:25Z"
|
||||
generated_at: "2026-03-16T01:39:16Z"
|
||||
model: claude-opus-4-5
|
||||
provider: pi
|
||||
source_hash: b36ca6b90ca03eaae25c00f9b12f2717fcd17ac540ba616ee03b398b234c2308
|
||||
source_hash: 3c79de31bf50147bdfa6cfc5ed55185e91bb55a8db986df0596b24d5529c7798
|
||||
source_path: tools/plugin.md
|
||||
workflow: 15
|
||||
---
|
||||
@@ -50,8 +50,7 @@ openclaw plugins install @openclaw/voice-call
|
||||
- [Nostr](/channels/nostr) — `@openclaw/nostr`
|
||||
- [Zalo](/channels/zalo) — `@openclaw/zalo`
|
||||
- [Microsoft Teams](/channels/msteams) — `@openclaw/msteams`
|
||||
- Google Antigravity OAuth(提供商认证)— 作为 `google-antigravity-auth` 捆绑(默认禁用)
|
||||
- Gemini CLI OAuth(提供商认证)— 作为 `google-gemini-cli-auth` 捆绑(默认禁用)
|
||||
- Google 网页搜索 + Gemini CLI OAuth — 作为 `google` 捆绑(网页搜索会自动加载;提供商认证仍需手动启用)
|
||||
- Qwen OAuth(提供商认证)— 作为 `qwen-portal-auth` 捆绑(默认禁用)
|
||||
- Copilot Proxy(提供商认证)— 本地 VS Code Copilot Proxy 桥接;与内置 `github-copilot` 设备登录不同(捆绑,默认禁用)
|
||||
|
||||
|
||||
@@ -1,13 +1,44 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
ACPX_BUNDLED_BIN,
|
||||
ACPX_PINNED_VERSION,
|
||||
createAcpxPluginConfigSchema,
|
||||
resolveAcpxPluginRoot,
|
||||
resolveAcpxPluginConfig,
|
||||
} from "./config.js";
|
||||
|
||||
describe("acpx plugin config parsing", () => {
|
||||
it("resolves source-layout plugin root from a file under src", () => {
|
||||
const pluginRoot = fs.mkdtempSync(path.join(os.tmpdir(), "acpx-root-source-"));
|
||||
try {
|
||||
fs.mkdirSync(path.join(pluginRoot, "src"), { recursive: true });
|
||||
fs.writeFileSync(path.join(pluginRoot, "package.json"), "{}\n", "utf8");
|
||||
fs.writeFileSync(path.join(pluginRoot, "openclaw.plugin.json"), "{}\n", "utf8");
|
||||
|
||||
const moduleUrl = pathToFileURL(path.join(pluginRoot, "src", "config.ts")).href;
|
||||
expect(resolveAcpxPluginRoot(moduleUrl)).toBe(pluginRoot);
|
||||
} finally {
|
||||
fs.rmSync(pluginRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("resolves bundled-layout plugin root from the dist entry file", () => {
|
||||
const pluginRoot = fs.mkdtempSync(path.join(os.tmpdir(), "acpx-root-dist-"));
|
||||
try {
|
||||
fs.writeFileSync(path.join(pluginRoot, "package.json"), "{}\n", "utf8");
|
||||
fs.writeFileSync(path.join(pluginRoot, "openclaw.plugin.json"), "{}\n", "utf8");
|
||||
|
||||
const moduleUrl = pathToFileURL(path.join(pluginRoot, "index.js")).href;
|
||||
expect(resolveAcpxPluginRoot(moduleUrl)).toBe(pluginRoot);
|
||||
} finally {
|
||||
fs.rmSync(pluginRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("resolves bundled acpx with pinned version by default", () => {
|
||||
const resolved = resolveAcpxPluginConfig({
|
||||
rawConfig: {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type { OpenClawPluginConfigSchema } from "openclaw/plugin-sdk/acpx";
|
||||
@@ -11,7 +12,27 @@ export type AcpxNonInteractivePermissionPolicy = (typeof ACPX_NON_INTERACTIVE_PO
|
||||
export const ACPX_PINNED_VERSION = "0.1.16";
|
||||
export const ACPX_VERSION_ANY = "any";
|
||||
const ACPX_BIN_NAME = process.platform === "win32" ? "acpx.cmd" : "acpx";
|
||||
export const ACPX_PLUGIN_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
|
||||
export function resolveAcpxPluginRoot(moduleUrl: string = import.meta.url): string {
|
||||
let cursor = path.dirname(fileURLToPath(moduleUrl));
|
||||
for (let i = 0; i < 3; i += 1) {
|
||||
// Bundled entries live at the plugin root while source files still live under src/.
|
||||
if (
|
||||
fs.existsSync(path.join(cursor, "openclaw.plugin.json")) &&
|
||||
fs.existsSync(path.join(cursor, "package.json"))
|
||||
) {
|
||||
return cursor;
|
||||
}
|
||||
const parent = path.dirname(cursor);
|
||||
if (parent === cursor) {
|
||||
break;
|
||||
}
|
||||
cursor = parent;
|
||||
}
|
||||
return path.resolve(path.dirname(fileURLToPath(moduleUrl)), "..");
|
||||
}
|
||||
|
||||
export const ACPX_PLUGIN_ROOT = resolveAcpxPluginRoot();
|
||||
export const ACPX_BUNDLED_BIN = path.join(ACPX_PLUGIN_ROOT, "node_modules", ".bin", ACPX_BIN_NAME);
|
||||
export function buildAcpxLocalInstallCommand(version: string = ACPX_PINNED_VERSION): string {
|
||||
return `npm install --omit=dev --no-save acpx@${version}`;
|
||||
|
||||
91
extensions/anthropic/index.test.ts
Normal file
91
extensions/anthropic/index.test.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { registerSingleProviderPlugin } from "../../src/test-utils/plugin-registration.js";
|
||||
import {
|
||||
createProviderUsageFetch,
|
||||
makeResponse,
|
||||
} from "../../src/test-utils/provider-usage-fetch.js";
|
||||
import anthropicPlugin from "./index.js";
|
||||
|
||||
const registerProvider = () => registerSingleProviderPlugin(anthropicPlugin);
|
||||
|
||||
describe("anthropic plugin", () => {
|
||||
it("owns anthropic 4.6 forward-compat resolution", () => {
|
||||
const provider = registerProvider();
|
||||
const model = provider.resolveDynamicModel?.({
|
||||
provider: "anthropic",
|
||||
modelId: "claude-sonnet-4.6-20260219",
|
||||
modelRegistry: {
|
||||
find: (_provider: string, id: string) =>
|
||||
id === "claude-sonnet-4.5-20260219"
|
||||
? {
|
||||
id,
|
||||
name: id,
|
||||
api: "anthropic-messages",
|
||||
provider: "anthropic",
|
||||
baseUrl: "https://api.anthropic.com",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 200_000,
|
||||
maxTokens: 8_192,
|
||||
}
|
||||
: null,
|
||||
} as never,
|
||||
});
|
||||
|
||||
expect(model).toMatchObject({
|
||||
id: "claude-sonnet-4.6-20260219",
|
||||
provider: "anthropic",
|
||||
api: "anthropic-messages",
|
||||
baseUrl: "https://api.anthropic.com",
|
||||
});
|
||||
});
|
||||
|
||||
it("owns usage auth resolution", async () => {
|
||||
const provider = registerProvider();
|
||||
await expect(
|
||||
provider.resolveUsageAuth?.({
|
||||
config: {} as never,
|
||||
env: {} as NodeJS.ProcessEnv,
|
||||
provider: "anthropic",
|
||||
resolveApiKeyFromConfigAndStore: () => undefined,
|
||||
resolveOAuthToken: async () => ({
|
||||
token: "anthropic-oauth-token",
|
||||
}),
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
token: "anthropic-oauth-token",
|
||||
});
|
||||
});
|
||||
|
||||
it("owns usage snapshot fetching", async () => {
|
||||
const provider = registerProvider();
|
||||
const mockFetch = createProviderUsageFetch(async (url) => {
|
||||
if (url.includes("api.anthropic.com/api/oauth/usage")) {
|
||||
return makeResponse(200, {
|
||||
five_hour: { utilization: 20, resets_at: "2026-01-07T01:00:00Z" },
|
||||
seven_day: { utilization: 35, resets_at: "2026-01-09T01:00:00Z" },
|
||||
});
|
||||
}
|
||||
return makeResponse(404, "not found");
|
||||
});
|
||||
|
||||
const snapshot = await provider.fetchUsageSnapshot?.({
|
||||
config: {} as never,
|
||||
env: {} as NodeJS.ProcessEnv,
|
||||
provider: "anthropic",
|
||||
token: "anthropic-oauth-token",
|
||||
timeoutMs: 5_000,
|
||||
fetchFn: mockFetch as unknown as typeof fetch,
|
||||
});
|
||||
|
||||
expect(snapshot).toEqual({
|
||||
provider: "anthropic",
|
||||
displayName: "Claude",
|
||||
windows: [
|
||||
{ label: "5h", usedPercent: 20, resetAt: Date.parse("2026-01-07T01:00:00Z") },
|
||||
{ label: "Week", usedPercent: 35, resetAt: Date.parse("2026-01-09T01:00:00Z") },
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
124
extensions/anthropic/index.ts
Normal file
124
extensions/anthropic/index.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import {
|
||||
emptyPluginConfigSchema,
|
||||
type OpenClawPluginApi,
|
||||
type ProviderResolveDynamicModelContext,
|
||||
type ProviderRuntimeModel,
|
||||
} from "openclaw/plugin-sdk/core";
|
||||
import { normalizeModelCompat } from "../../src/agents/model-compat.js";
|
||||
import { fetchClaudeUsage } from "../../src/infra/provider-usage.fetch.js";
|
||||
|
||||
const PROVIDER_ID = "anthropic";
|
||||
const ANTHROPIC_OPUS_46_MODEL_ID = "claude-opus-4-6";
|
||||
const ANTHROPIC_OPUS_46_DOT_MODEL_ID = "claude-opus-4.6";
|
||||
const ANTHROPIC_OPUS_TEMPLATE_MODEL_IDS = ["claude-opus-4-5", "claude-opus-4.5"] as const;
|
||||
const ANTHROPIC_SONNET_46_MODEL_ID = "claude-sonnet-4-6";
|
||||
const ANTHROPIC_SONNET_46_DOT_MODEL_ID = "claude-sonnet-4.6";
|
||||
const ANTHROPIC_SONNET_TEMPLATE_MODEL_IDS = ["claude-sonnet-4-5", "claude-sonnet-4.5"] as const;
|
||||
|
||||
function cloneFirstTemplateModel(params: {
|
||||
modelId: string;
|
||||
templateIds: readonly string[];
|
||||
ctx: ProviderResolveDynamicModelContext;
|
||||
}): ProviderRuntimeModel | undefined {
|
||||
const trimmedModelId = params.modelId.trim();
|
||||
for (const templateId of [...new Set(params.templateIds)].filter(Boolean)) {
|
||||
const template = params.ctx.modelRegistry.find(
|
||||
PROVIDER_ID,
|
||||
templateId,
|
||||
) as ProviderRuntimeModel | null;
|
||||
if (!template) {
|
||||
continue;
|
||||
}
|
||||
return normalizeModelCompat({
|
||||
...template,
|
||||
id: trimmedModelId,
|
||||
name: trimmedModelId,
|
||||
} as ProviderRuntimeModel);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolveAnthropic46ForwardCompatModel(params: {
|
||||
ctx: ProviderResolveDynamicModelContext;
|
||||
dashModelId: string;
|
||||
dotModelId: string;
|
||||
dashTemplateId: string;
|
||||
dotTemplateId: string;
|
||||
fallbackTemplateIds: readonly string[];
|
||||
}): ProviderRuntimeModel | undefined {
|
||||
const trimmedModelId = params.ctx.modelId.trim();
|
||||
const lower = trimmedModelId.toLowerCase();
|
||||
const is46Model =
|
||||
lower === params.dashModelId ||
|
||||
lower === params.dotModelId ||
|
||||
lower.startsWith(`${params.dashModelId}-`) ||
|
||||
lower.startsWith(`${params.dotModelId}-`);
|
||||
if (!is46Model) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const templateIds: string[] = [];
|
||||
if (lower.startsWith(params.dashModelId)) {
|
||||
templateIds.push(lower.replace(params.dashModelId, params.dashTemplateId));
|
||||
}
|
||||
if (lower.startsWith(params.dotModelId)) {
|
||||
templateIds.push(lower.replace(params.dotModelId, params.dotTemplateId));
|
||||
}
|
||||
templateIds.push(...params.fallbackTemplateIds);
|
||||
|
||||
return cloneFirstTemplateModel({
|
||||
modelId: trimmedModelId,
|
||||
templateIds,
|
||||
ctx: params.ctx,
|
||||
});
|
||||
}
|
||||
|
||||
function resolveAnthropicForwardCompatModel(
|
||||
ctx: ProviderResolveDynamicModelContext,
|
||||
): ProviderRuntimeModel | undefined {
|
||||
return (
|
||||
resolveAnthropic46ForwardCompatModel({
|
||||
ctx,
|
||||
dashModelId: ANTHROPIC_OPUS_46_MODEL_ID,
|
||||
dotModelId: ANTHROPIC_OPUS_46_DOT_MODEL_ID,
|
||||
dashTemplateId: "claude-opus-4-5",
|
||||
dotTemplateId: "claude-opus-4.5",
|
||||
fallbackTemplateIds: ANTHROPIC_OPUS_TEMPLATE_MODEL_IDS,
|
||||
}) ??
|
||||
resolveAnthropic46ForwardCompatModel({
|
||||
ctx,
|
||||
dashModelId: ANTHROPIC_SONNET_46_MODEL_ID,
|
||||
dotModelId: ANTHROPIC_SONNET_46_DOT_MODEL_ID,
|
||||
dashTemplateId: "claude-sonnet-4-5",
|
||||
dotTemplateId: "claude-sonnet-4.5",
|
||||
fallbackTemplateIds: ANTHROPIC_SONNET_TEMPLATE_MODEL_IDS,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const anthropicPlugin = {
|
||||
id: PROVIDER_ID,
|
||||
name: "Anthropic Provider",
|
||||
description: "Bundled Anthropic provider plugin",
|
||||
configSchema: emptyPluginConfigSchema(),
|
||||
register(api: OpenClawPluginApi) {
|
||||
api.registerProvider({
|
||||
id: PROVIDER_ID,
|
||||
label: "Anthropic",
|
||||
docsPath: "/providers/models",
|
||||
envVars: ["ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY"],
|
||||
auth: [],
|
||||
resolveDynamicModel: (ctx) => resolveAnthropicForwardCompatModel(ctx),
|
||||
capabilities: {
|
||||
providerFamily: "anthropic",
|
||||
dropThinkingBlockModelHints: ["claude"],
|
||||
},
|
||||
resolveUsageAuth: async (ctx) => await ctx.resolveOAuthToken(),
|
||||
fetchUsageSnapshot: async (ctx) =>
|
||||
await fetchClaudeUsage(ctx.token, ctx.timeoutMs, ctx.fetchFn),
|
||||
isCacheTtlEligible: () => true,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default anthropicPlugin;
|
||||
12
extensions/anthropic/openclaw.plugin.json
Normal file
12
extensions/anthropic/openclaw.plugin.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"id": "anthropic",
|
||||
"providers": ["anthropic"],
|
||||
"providerAuthEnvVars": {
|
||||
"anthropic": ["ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "@openclaw/google-gemini-cli-auth",
|
||||
"name": "@openclaw/anthropic-provider",
|
||||
"version": "2026.3.14",
|
||||
"private": true,
|
||||
"description": "OpenClaw Gemini CLI OAuth provider plugin",
|
||||
"description": "OpenClaw Anthropic provider plugin",
|
||||
"type": "module",
|
||||
"openclaw": {
|
||||
"extensions": [
|
||||
@@ -10,6 +10,7 @@
|
||||
"extensions": [
|
||||
"./index.ts"
|
||||
],
|
||||
"setupEntry": "./setup-entry.ts",
|
||||
"channel": {
|
||||
"id": "bluebubbles",
|
||||
"label": "BlueBubbles",
|
||||
|
||||
5
extensions/bluebubbles/setup-entry.ts
Normal file
5
extensions/bluebubbles/setup-entry.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { bluebubblesPlugin } from "./src/channel.js";
|
||||
|
||||
export default {
|
||||
plugin: bluebubblesPlugin,
|
||||
};
|
||||
@@ -1,18 +1,11 @@
|
||||
import type {
|
||||
ChannelAccountSnapshot,
|
||||
ChannelPlugin,
|
||||
OpenClawConfig,
|
||||
} from "openclaw/plugin-sdk/bluebubbles";
|
||||
import type { ChannelAccountSnapshot, ChannelPlugin } from "openclaw/plugin-sdk/bluebubbles";
|
||||
import {
|
||||
applyAccountNameToChannelSection,
|
||||
buildChannelConfigSchema,
|
||||
buildComputedAccountStatusSnapshot,
|
||||
buildProbeChannelStatusSummary,
|
||||
collectBlueBubblesStatusIssues,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
deleteAccountFromConfigSection,
|
||||
migrateBaseNameToDefaultAccount,
|
||||
normalizeAccountId,
|
||||
PAIRING_APPROVED_MESSAGE,
|
||||
resolveBlueBubblesGroupRequireMention,
|
||||
resolveBlueBubblesGroupToolPolicy,
|
||||
@@ -32,14 +25,14 @@ import {
|
||||
resolveDefaultBlueBubblesAccountId,
|
||||
} from "./accounts.js";
|
||||
import { bluebubblesMessageActions } from "./actions.js";
|
||||
import { applyBlueBubblesConnectionConfig } from "./config-apply.js";
|
||||
import { BlueBubblesConfigSchema } from "./config-schema.js";
|
||||
import { sendBlueBubblesMedia } from "./media-send.js";
|
||||
import { resolveBlueBubblesMessageId } from "./monitor.js";
|
||||
import { monitorBlueBubblesProvider, resolveWebhookPathFromConfig } from "./monitor.js";
|
||||
import { blueBubblesOnboardingAdapter } from "./onboarding.js";
|
||||
import { probeBlueBubbles, type BlueBubblesProbe } from "./probe.js";
|
||||
import { sendMessageBlueBubbles } from "./send.js";
|
||||
import { blueBubblesSetupAdapter } from "./setup-core.js";
|
||||
import { blueBubblesSetupWizard } from "./setup-surface.js";
|
||||
import {
|
||||
extractHandleFromChatGuid,
|
||||
looksLikeBlueBubblesTargetId,
|
||||
@@ -88,7 +81,7 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
|
||||
},
|
||||
reload: { configPrefixes: ["channels.bluebubbles"] },
|
||||
configSchema: buildChannelConfigSchema(BlueBubblesConfigSchema),
|
||||
onboarding: blueBubblesOnboardingAdapter,
|
||||
setupWizard: blueBubblesSetupWizard,
|
||||
config: {
|
||||
listAccountIds: (cfg) => listBlueBubblesAccountIds(cfg),
|
||||
resolveAccount: (cfg, accountId) => resolveBlueBubblesAccount({ cfg: cfg, accountId }),
|
||||
@@ -223,53 +216,7 @@ export const bluebubblesPlugin: ChannelPlugin<ResolvedBlueBubblesAccount> = {
|
||||
return display?.trim() || target?.trim() || "";
|
||||
},
|
||||
},
|
||||
setup: {
|
||||
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
|
||||
applyAccountName: ({ cfg, accountId, name }) =>
|
||||
applyAccountNameToChannelSection({
|
||||
cfg: cfg,
|
||||
channelKey: "bluebubbles",
|
||||
accountId,
|
||||
name,
|
||||
}),
|
||||
validateInput: ({ input }) => {
|
||||
if (!input.httpUrl && !input.password) {
|
||||
return "BlueBubbles requires --http-url and --password.";
|
||||
}
|
||||
if (!input.httpUrl) {
|
||||
return "BlueBubbles requires --http-url.";
|
||||
}
|
||||
if (!input.password) {
|
||||
return "BlueBubbles requires --password.";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
applyAccountConfig: ({ cfg, accountId, input }) => {
|
||||
const namedConfig = applyAccountNameToChannelSection({
|
||||
cfg: cfg,
|
||||
channelKey: "bluebubbles",
|
||||
accountId,
|
||||
name: input.name,
|
||||
});
|
||||
const next =
|
||||
accountId !== DEFAULT_ACCOUNT_ID
|
||||
? migrateBaseNameToDefaultAccount({
|
||||
cfg: namedConfig,
|
||||
channelKey: "bluebubbles",
|
||||
})
|
||||
: namedConfig;
|
||||
return applyBlueBubblesConnectionConfig({
|
||||
cfg: next,
|
||||
accountId,
|
||||
patch: {
|
||||
serverUrl: input.httpUrl,
|
||||
password: input.password,
|
||||
webhookPath: input.webhookPath,
|
||||
},
|
||||
onlyDefinedFields: true,
|
||||
});
|
||||
},
|
||||
},
|
||||
setup: blueBubblesSetupAdapter,
|
||||
pairing: {
|
||||
idLabel: "bluebubblesSenderId",
|
||||
normalizeAllowEntry: (entry) => normalizeBlueBubblesHandle(entry.replace(/^bluebubbles:/i, "")),
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
import type { WizardPrompter } from "openclaw/plugin-sdk/bluebubbles";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/bluebubbles", () => ({
|
||||
DEFAULT_ACCOUNT_ID: "default",
|
||||
addWildcardAllowFrom: vi.fn(),
|
||||
formatDocsLink: (_url: string, fallback: string) => fallback,
|
||||
hasConfiguredSecretInput: (value: unknown) => {
|
||||
if (typeof value === "string") {
|
||||
return value.trim().length > 0;
|
||||
}
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
return false;
|
||||
}
|
||||
const ref = value as { source?: unknown; provider?: unknown; id?: unknown };
|
||||
const validSource = ref.source === "env" || ref.source === "file" || ref.source === "exec";
|
||||
return (
|
||||
validSource &&
|
||||
typeof ref.provider === "string" &&
|
||||
ref.provider.trim().length > 0 &&
|
||||
typeof ref.id === "string" &&
|
||||
ref.id.trim().length > 0
|
||||
);
|
||||
},
|
||||
mergeAllowFromEntries: (_existing: unknown, entries: string[]) => entries,
|
||||
createAccountListHelpers: () => ({
|
||||
listAccountIds: () => ["default"],
|
||||
resolveDefaultAccountId: () => "default",
|
||||
}),
|
||||
normalizeSecretInputString: (value: unknown) => {
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length > 0 ? trimmed : undefined;
|
||||
},
|
||||
normalizeAccountId: (value?: string | null) =>
|
||||
value && value.trim().length > 0 ? value : "default",
|
||||
promptAccountId: vi.fn(),
|
||||
resolveAccountIdForConfigure: async (params: {
|
||||
accountOverride?: string;
|
||||
defaultAccountId: string;
|
||||
}) => params.accountOverride?.trim() || params.defaultAccountId,
|
||||
}));
|
||||
|
||||
describe("bluebubbles onboarding SecretInput", () => {
|
||||
it("preserves existing password SecretRef when user keeps current credential", async () => {
|
||||
const { blueBubblesOnboardingAdapter } = await import("./onboarding.js");
|
||||
type ConfigureContext = Parameters<
|
||||
NonNullable<typeof blueBubblesOnboardingAdapter.configure>
|
||||
>[0];
|
||||
const passwordRef = { source: "env", provider: "default", id: "BLUEBUBBLES_PASSWORD" };
|
||||
const confirm = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce(true) // keep server URL
|
||||
.mockResolvedValueOnce(true) // keep password SecretRef
|
||||
.mockResolvedValueOnce(false); // keep default webhook path
|
||||
const text = vi.fn();
|
||||
const note = vi.fn();
|
||||
|
||||
const prompter = {
|
||||
confirm,
|
||||
text,
|
||||
note,
|
||||
} as unknown as WizardPrompter;
|
||||
|
||||
const context = {
|
||||
cfg: {
|
||||
channels: {
|
||||
bluebubbles: {
|
||||
enabled: true,
|
||||
serverUrl: "http://127.0.0.1:1234",
|
||||
password: passwordRef,
|
||||
},
|
||||
},
|
||||
},
|
||||
prompter,
|
||||
runtime: { ...console, exit: vi.fn() } as ConfigureContext["runtime"],
|
||||
forceAllowFrom: false,
|
||||
accountOverrides: {},
|
||||
shouldPromptAccountIds: false,
|
||||
} satisfies ConfigureContext;
|
||||
|
||||
const result = await blueBubblesOnboardingAdapter.configure(context);
|
||||
|
||||
expect(result.cfg.channels?.bluebubbles?.password).toEqual(passwordRef);
|
||||
expect(text).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,289 +0,0 @@
|
||||
import type {
|
||||
ChannelOnboardingAdapter,
|
||||
ChannelOnboardingDmPolicy,
|
||||
OpenClawConfig,
|
||||
DmPolicy,
|
||||
WizardPrompter,
|
||||
} from "openclaw/plugin-sdk/bluebubbles";
|
||||
import {
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
formatDocsLink,
|
||||
mergeAllowFromEntries,
|
||||
normalizeAccountId,
|
||||
patchScopedAccountConfig,
|
||||
resolveAccountIdForConfigure,
|
||||
setTopLevelChannelDmPolicyWithAllowFrom,
|
||||
} from "openclaw/plugin-sdk/bluebubbles";
|
||||
import {
|
||||
listBlueBubblesAccountIds,
|
||||
resolveBlueBubblesAccount,
|
||||
resolveDefaultBlueBubblesAccountId,
|
||||
} from "./accounts.js";
|
||||
import { applyBlueBubblesConnectionConfig } from "./config-apply.js";
|
||||
import { hasConfiguredSecretInput, normalizeSecretInputString } from "./secret-input.js";
|
||||
import { parseBlueBubblesAllowTarget } from "./targets.js";
|
||||
import { normalizeBlueBubblesServerUrl } from "./types.js";
|
||||
|
||||
const channel = "bluebubbles" as const;
|
||||
|
||||
function setBlueBubblesDmPolicy(cfg: OpenClawConfig, dmPolicy: DmPolicy): OpenClawConfig {
|
||||
return setTopLevelChannelDmPolicyWithAllowFrom({
|
||||
cfg,
|
||||
channel: "bluebubbles",
|
||||
dmPolicy,
|
||||
});
|
||||
}
|
||||
|
||||
function setBlueBubblesAllowFrom(
|
||||
cfg: OpenClawConfig,
|
||||
accountId: string,
|
||||
allowFrom: string[],
|
||||
): OpenClawConfig {
|
||||
return patchScopedAccountConfig({
|
||||
cfg,
|
||||
channelKey: channel,
|
||||
accountId,
|
||||
patch: { allowFrom },
|
||||
ensureChannelEnabled: false,
|
||||
ensureAccountEnabled: false,
|
||||
});
|
||||
}
|
||||
|
||||
function parseBlueBubblesAllowFromInput(raw: string): string[] {
|
||||
return raw
|
||||
.split(/[\n,]+/g)
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
async function promptBlueBubblesAllowFrom(params: {
|
||||
cfg: OpenClawConfig;
|
||||
prompter: WizardPrompter;
|
||||
accountId?: string;
|
||||
}): Promise<OpenClawConfig> {
|
||||
const accountId =
|
||||
params.accountId && normalizeAccountId(params.accountId)
|
||||
? (normalizeAccountId(params.accountId) ?? DEFAULT_ACCOUNT_ID)
|
||||
: resolveDefaultBlueBubblesAccountId(params.cfg);
|
||||
const resolved = resolveBlueBubblesAccount({ cfg: params.cfg, accountId });
|
||||
const existing = resolved.config.allowFrom ?? [];
|
||||
await params.prompter.note(
|
||||
[
|
||||
"Allowlist BlueBubbles DMs by handle or chat target.",
|
||||
"Examples:",
|
||||
"- +15555550123",
|
||||
"- user@example.com",
|
||||
"- chat_id:123",
|
||||
"- chat_guid:iMessage;-;+15555550123",
|
||||
"Multiple entries: comma- or newline-separated.",
|
||||
`Docs: ${formatDocsLink("/channels/bluebubbles", "bluebubbles")}`,
|
||||
].join("\n"),
|
||||
"BlueBubbles allowlist",
|
||||
);
|
||||
const entry = await params.prompter.text({
|
||||
message: "BlueBubbles allowFrom (handle or chat_id)",
|
||||
placeholder: "+15555550123, user@example.com, chat_id:123",
|
||||
initialValue: existing[0] ? String(existing[0]) : undefined,
|
||||
validate: (value) => {
|
||||
const raw = String(value ?? "").trim();
|
||||
if (!raw) {
|
||||
return "Required";
|
||||
}
|
||||
const parts = parseBlueBubblesAllowFromInput(raw);
|
||||
for (const part of parts) {
|
||||
if (part === "*") {
|
||||
continue;
|
||||
}
|
||||
const parsed = parseBlueBubblesAllowTarget(part);
|
||||
if (parsed.kind === "handle" && !parsed.handle) {
|
||||
return `Invalid entry: ${part}`;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
const parts = parseBlueBubblesAllowFromInput(String(entry));
|
||||
const unique = mergeAllowFromEntries(undefined, parts);
|
||||
return setBlueBubblesAllowFrom(params.cfg, accountId, unique);
|
||||
}
|
||||
|
||||
const dmPolicy: ChannelOnboardingDmPolicy = {
|
||||
label: "BlueBubbles",
|
||||
channel,
|
||||
policyKey: "channels.bluebubbles.dmPolicy",
|
||||
allowFromKey: "channels.bluebubbles.allowFrom",
|
||||
getCurrent: (cfg) => cfg.channels?.bluebubbles?.dmPolicy ?? "pairing",
|
||||
setPolicy: (cfg, policy) => setBlueBubblesDmPolicy(cfg, policy),
|
||||
promptAllowFrom: promptBlueBubblesAllowFrom,
|
||||
};
|
||||
|
||||
export const blueBubblesOnboardingAdapter: ChannelOnboardingAdapter = {
|
||||
channel,
|
||||
getStatus: async ({ cfg }) => {
|
||||
const configured = listBlueBubblesAccountIds(cfg).some((accountId) => {
|
||||
const account = resolveBlueBubblesAccount({ cfg, accountId });
|
||||
return account.configured;
|
||||
});
|
||||
return {
|
||||
channel,
|
||||
configured,
|
||||
statusLines: [`BlueBubbles: ${configured ? "configured" : "needs setup"}`],
|
||||
selectionHint: configured ? "configured" : "iMessage via BlueBubbles app",
|
||||
quickstartScore: configured ? 1 : 0,
|
||||
};
|
||||
},
|
||||
configure: async ({ cfg, prompter, accountOverrides, shouldPromptAccountIds }) => {
|
||||
const defaultAccountId = resolveDefaultBlueBubblesAccountId(cfg);
|
||||
const accountId = await resolveAccountIdForConfigure({
|
||||
cfg,
|
||||
prompter,
|
||||
label: "BlueBubbles",
|
||||
accountOverride: accountOverrides.bluebubbles,
|
||||
shouldPromptAccountIds,
|
||||
listAccountIds: listBlueBubblesAccountIds,
|
||||
defaultAccountId,
|
||||
});
|
||||
|
||||
let next = cfg;
|
||||
const resolvedAccount = resolveBlueBubblesAccount({ cfg: next, accountId });
|
||||
const validateServerUrlInput = (value: unknown): string | undefined => {
|
||||
const trimmed = String(value ?? "").trim();
|
||||
if (!trimmed) {
|
||||
return "Required";
|
||||
}
|
||||
try {
|
||||
const normalized = normalizeBlueBubblesServerUrl(trimmed);
|
||||
new URL(normalized);
|
||||
return undefined;
|
||||
} catch {
|
||||
return "Invalid URL format";
|
||||
}
|
||||
};
|
||||
const promptServerUrl = async (initialValue?: string): Promise<string> => {
|
||||
const entered = await prompter.text({
|
||||
message: "BlueBubbles server URL",
|
||||
placeholder: "http://192.168.1.100:1234",
|
||||
initialValue,
|
||||
validate: validateServerUrlInput,
|
||||
});
|
||||
return String(entered).trim();
|
||||
};
|
||||
|
||||
// Prompt for server URL
|
||||
let serverUrl = resolvedAccount.config.serverUrl?.trim();
|
||||
if (!serverUrl) {
|
||||
await prompter.note(
|
||||
[
|
||||
"Enter the BlueBubbles server URL (e.g., http://192.168.1.100:1234).",
|
||||
"Find this in the BlueBubbles Server app under Connection.",
|
||||
`Docs: ${formatDocsLink("/channels/bluebubbles", "bluebubbles")}`,
|
||||
].join("\n"),
|
||||
"BlueBubbles server URL",
|
||||
);
|
||||
serverUrl = await promptServerUrl();
|
||||
} else {
|
||||
const keepUrl = await prompter.confirm({
|
||||
message: `BlueBubbles server URL already set (${serverUrl}). Keep it?`,
|
||||
initialValue: true,
|
||||
});
|
||||
if (!keepUrl) {
|
||||
serverUrl = await promptServerUrl(serverUrl);
|
||||
}
|
||||
}
|
||||
|
||||
// Prompt for password
|
||||
const existingPassword = resolvedAccount.config.password;
|
||||
const existingPasswordText = normalizeSecretInputString(existingPassword);
|
||||
const hasConfiguredPassword = hasConfiguredSecretInput(existingPassword);
|
||||
let password: unknown = existingPasswordText;
|
||||
if (!hasConfiguredPassword) {
|
||||
await prompter.note(
|
||||
[
|
||||
"Enter the BlueBubbles server password.",
|
||||
"Find this in the BlueBubbles Server app under Settings.",
|
||||
].join("\n"),
|
||||
"BlueBubbles password",
|
||||
);
|
||||
const entered = await prompter.text({
|
||||
message: "BlueBubbles password",
|
||||
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
||||
});
|
||||
password = String(entered).trim();
|
||||
} else {
|
||||
const keepPassword = await prompter.confirm({
|
||||
message: "BlueBubbles password already set. Keep it?",
|
||||
initialValue: true,
|
||||
});
|
||||
if (!keepPassword) {
|
||||
const entered = await prompter.text({
|
||||
message: "BlueBubbles password",
|
||||
validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
|
||||
});
|
||||
password = String(entered).trim();
|
||||
} else if (!existingPasswordText) {
|
||||
password = existingPassword;
|
||||
}
|
||||
}
|
||||
|
||||
// Prompt for webhook path (optional)
|
||||
const existingWebhookPath = resolvedAccount.config.webhookPath?.trim();
|
||||
const wantsWebhook = await prompter.confirm({
|
||||
message: "Configure a custom webhook path? (default: /bluebubbles-webhook)",
|
||||
initialValue: Boolean(existingWebhookPath && existingWebhookPath !== "/bluebubbles-webhook"),
|
||||
});
|
||||
let webhookPath = "/bluebubbles-webhook";
|
||||
if (wantsWebhook) {
|
||||
const entered = await prompter.text({
|
||||
message: "Webhook path",
|
||||
placeholder: "/bluebubbles-webhook",
|
||||
initialValue: existingWebhookPath || "/bluebubbles-webhook",
|
||||
validate: (value) => {
|
||||
const trimmed = String(value ?? "").trim();
|
||||
if (!trimmed) {
|
||||
return "Required";
|
||||
}
|
||||
if (!trimmed.startsWith("/")) {
|
||||
return "Path must start with /";
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
webhookPath = String(entered).trim();
|
||||
}
|
||||
|
||||
// Apply config
|
||||
next = applyBlueBubblesConnectionConfig({
|
||||
cfg: next,
|
||||
accountId,
|
||||
patch: {
|
||||
serverUrl,
|
||||
password,
|
||||
webhookPath,
|
||||
},
|
||||
accountEnabled: "preserve-or-true",
|
||||
});
|
||||
|
||||
await prompter.note(
|
||||
[
|
||||
"Configure the webhook URL in BlueBubbles Server:",
|
||||
"1. Open BlueBubbles Server → Settings → Webhooks",
|
||||
"2. Add your OpenClaw gateway URL + webhook path",
|
||||
" Example: https://your-gateway-host:3000/bluebubbles-webhook",
|
||||
"3. Enable the webhook and save",
|
||||
"",
|
||||
`Docs: ${formatDocsLink("/channels/bluebubbles", "bluebubbles")}`,
|
||||
].join("\n"),
|
||||
"BlueBubbles next steps",
|
||||
);
|
||||
|
||||
return { cfg: next, accountId };
|
||||
},
|
||||
dmPolicy,
|
||||
disable: (cfg) => ({
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
bluebubbles: { ...cfg.channels?.bluebubbles, enabled: false },
|
||||
},
|
||||
}),
|
||||
};
|
||||
84
extensions/bluebubbles/src/setup-core.ts
Normal file
84
extensions/bluebubbles/src/setup-core.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { setTopLevelChannelDmPolicyWithAllowFrom } from "../../../src/channels/plugins/setup-flow-helpers.js";
|
||||
import {
|
||||
applyAccountNameToChannelSection,
|
||||
migrateBaseNameToDefaultAccount,
|
||||
patchScopedAccountConfig,
|
||||
} from "../../../src/channels/plugins/setup-helpers.js";
|
||||
import type { ChannelSetupAdapter } from "../../../src/channels/plugins/types.adapters.js";
|
||||
import type { OpenClawConfig } from "../../../src/config/config.js";
|
||||
import type { DmPolicy } from "../../../src/config/types.js";
|
||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../../src/routing/session-key.js";
|
||||
import { applyBlueBubblesConnectionConfig } from "./config-apply.js";
|
||||
|
||||
const channel = "bluebubbles" as const;
|
||||
|
||||
export function setBlueBubblesDmPolicy(cfg: OpenClawConfig, dmPolicy: DmPolicy): OpenClawConfig {
|
||||
return setTopLevelChannelDmPolicyWithAllowFrom({
|
||||
cfg,
|
||||
channel,
|
||||
dmPolicy,
|
||||
});
|
||||
}
|
||||
|
||||
export function setBlueBubblesAllowFrom(
|
||||
cfg: OpenClawConfig,
|
||||
accountId: string,
|
||||
allowFrom: string[],
|
||||
): OpenClawConfig {
|
||||
return patchScopedAccountConfig({
|
||||
cfg,
|
||||
channelKey: channel,
|
||||
accountId,
|
||||
patch: { allowFrom },
|
||||
ensureChannelEnabled: false,
|
||||
ensureAccountEnabled: false,
|
||||
});
|
||||
}
|
||||
|
||||
export const blueBubblesSetupAdapter: ChannelSetupAdapter = {
|
||||
resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
|
||||
applyAccountName: ({ cfg, accountId, name }) =>
|
||||
applyAccountNameToChannelSection({
|
||||
cfg,
|
||||
channelKey: channel,
|
||||
accountId,
|
||||
name,
|
||||
}),
|
||||
validateInput: ({ input }) => {
|
||||
if (!input.httpUrl && !input.password) {
|
||||
return "BlueBubbles requires --http-url and --password.";
|
||||
}
|
||||
if (!input.httpUrl) {
|
||||
return "BlueBubbles requires --http-url.";
|
||||
}
|
||||
if (!input.password) {
|
||||
return "BlueBubbles requires --password.";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
applyAccountConfig: ({ cfg, accountId, input }) => {
|
||||
const namedConfig = applyAccountNameToChannelSection({
|
||||
cfg,
|
||||
channelKey: channel,
|
||||
accountId,
|
||||
name: input.name,
|
||||
});
|
||||
const next =
|
||||
accountId !== DEFAULT_ACCOUNT_ID
|
||||
? migrateBaseNameToDefaultAccount({
|
||||
cfg: namedConfig,
|
||||
channelKey: channel,
|
||||
})
|
||||
: namedConfig;
|
||||
return applyBlueBubblesConnectionConfig({
|
||||
cfg: next,
|
||||
accountId,
|
||||
patch: {
|
||||
serverUrl: input.httpUrl,
|
||||
password: input.password,
|
||||
webhookPath: input.webhookPath,
|
||||
},
|
||||
onlyDefinedFields: true,
|
||||
});
|
||||
},
|
||||
};
|
||||
154
extensions/bluebubbles/src/setup-surface.test.ts
Normal file
154
extensions/bluebubbles/src/setup-surface.test.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { buildChannelSetupFlowAdapterFromSetupWizard } from "../../../src/channels/plugins/setup-wizard.js";
|
||||
import { DEFAULT_ACCOUNT_ID } from "../../../src/routing/session-key.js";
|
||||
import type { WizardPrompter } from "../../../src/wizard/prompts.js";
|
||||
import { resolveBlueBubblesAccount } from "./accounts.js";
|
||||
import { DEFAULT_WEBHOOK_PATH } from "./monitor-shared.js";
|
||||
|
||||
async function createBlueBubblesConfigureAdapter() {
|
||||
const { blueBubblesSetupAdapter, blueBubblesSetupWizard } = await import("./setup-surface.js");
|
||||
const plugin = {
|
||||
id: "bluebubbles",
|
||||
meta: {
|
||||
id: "bluebubbles",
|
||||
label: "BlueBubbles",
|
||||
selectionLabel: "BlueBubbles",
|
||||
docsPath: "/channels/bluebubbles",
|
||||
blurb: "iMessage via BlueBubbles",
|
||||
},
|
||||
config: {
|
||||
listAccountIds: () => [DEFAULT_ACCOUNT_ID],
|
||||
defaultAccountId: () => DEFAULT_ACCOUNT_ID,
|
||||
resolveAccount: (cfg, accountId) => resolveBlueBubblesAccount({ cfg, accountId }),
|
||||
resolveAllowFrom: ({ cfg, accountId }: { cfg: unknown; accountId: string }) =>
|
||||
resolveBlueBubblesAccount({
|
||||
cfg: cfg as Parameters<typeof resolveBlueBubblesAccount>[0]["cfg"],
|
||||
accountId,
|
||||
}).config.allowFrom ?? [],
|
||||
},
|
||||
setup: blueBubblesSetupAdapter,
|
||||
} as Parameters<typeof buildChannelSetupFlowAdapterFromSetupWizard>[0]["plugin"];
|
||||
return buildChannelSetupFlowAdapterFromSetupWizard({
|
||||
plugin,
|
||||
wizard: blueBubblesSetupWizard,
|
||||
});
|
||||
}
|
||||
|
||||
describe("bluebubbles setup surface", () => {
|
||||
it("preserves existing password SecretRef and keeps default webhook path", async () => {
|
||||
const adapter = await createBlueBubblesConfigureAdapter();
|
||||
type ConfigureContext = Parameters<NonNullable<typeof adapter.configure>>[0];
|
||||
const passwordRef = { source: "env", provider: "default", id: "BLUEBUBBLES_PASSWORD" };
|
||||
const confirm = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce(false)
|
||||
.mockResolvedValueOnce(true)
|
||||
.mockResolvedValueOnce(true);
|
||||
const text = vi.fn();
|
||||
const note = vi.fn();
|
||||
|
||||
const prompter = { confirm, text, note } as unknown as WizardPrompter;
|
||||
const context = {
|
||||
cfg: {
|
||||
channels: {
|
||||
bluebubbles: {
|
||||
enabled: true,
|
||||
serverUrl: "http://127.0.0.1:1234",
|
||||
password: passwordRef,
|
||||
},
|
||||
},
|
||||
},
|
||||
prompter,
|
||||
runtime: { ...console, exit: vi.fn() } as ConfigureContext["runtime"],
|
||||
forceAllowFrom: false,
|
||||
accountOverrides: {},
|
||||
shouldPromptAccountIds: false,
|
||||
} satisfies ConfigureContext;
|
||||
|
||||
const result = await adapter.configure(context);
|
||||
|
||||
expect(result.cfg.channels?.bluebubbles?.password).toEqual(passwordRef);
|
||||
expect(result.cfg.channels?.bluebubbles?.webhookPath).toBe(DEFAULT_WEBHOOK_PATH);
|
||||
expect(text).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("applies a custom webhook path when requested", async () => {
|
||||
const adapter = await createBlueBubblesConfigureAdapter();
|
||||
type ConfigureContext = Parameters<NonNullable<typeof adapter.configure>>[0];
|
||||
const confirm = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce(true)
|
||||
.mockResolvedValueOnce(true)
|
||||
.mockResolvedValueOnce(true);
|
||||
const text = vi.fn().mockResolvedValueOnce("/custom-bluebubbles");
|
||||
const note = vi.fn();
|
||||
|
||||
const prompter = { confirm, text, note } as unknown as WizardPrompter;
|
||||
const context = {
|
||||
cfg: {
|
||||
channels: {
|
||||
bluebubbles: {
|
||||
enabled: true,
|
||||
serverUrl: "http://127.0.0.1:1234",
|
||||
password: "secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
prompter,
|
||||
runtime: { ...console, exit: vi.fn() } as ConfigureContext["runtime"],
|
||||
forceAllowFrom: false,
|
||||
accountOverrides: {},
|
||||
shouldPromptAccountIds: false,
|
||||
} satisfies ConfigureContext;
|
||||
|
||||
const result = await adapter.configure(context);
|
||||
|
||||
expect(result.cfg.channels?.bluebubbles?.webhookPath).toBe("/custom-bluebubbles");
|
||||
expect(text).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: "Webhook path",
|
||||
placeholder: DEFAULT_WEBHOOK_PATH,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("validates server URLs before accepting input", async () => {
|
||||
const adapter = await createBlueBubblesConfigureAdapter();
|
||||
type ConfigureContext = Parameters<NonNullable<typeof adapter.configure>>[0];
|
||||
const confirm = vi.fn().mockResolvedValueOnce(false);
|
||||
const text = vi.fn().mockResolvedValueOnce("127.0.0.1:1234").mockResolvedValueOnce("secret");
|
||||
const note = vi.fn();
|
||||
|
||||
const prompter = { confirm, text, note } as unknown as WizardPrompter;
|
||||
const context = {
|
||||
cfg: { channels: { bluebubbles: {} } },
|
||||
prompter,
|
||||
runtime: { ...console, exit: vi.fn() } as ConfigureContext["runtime"],
|
||||
forceAllowFrom: false,
|
||||
accountOverrides: {},
|
||||
shouldPromptAccountIds: false,
|
||||
} satisfies ConfigureContext;
|
||||
|
||||
await adapter.configure(context);
|
||||
|
||||
const serverUrlPrompt = text.mock.calls[0]?.[0] as {
|
||||
validate?: (value: string) => string | undefined;
|
||||
};
|
||||
expect(serverUrlPrompt.validate?.("bad url")).toBe("Invalid URL format");
|
||||
expect(serverUrlPrompt.validate?.("127.0.0.1:1234")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("disables the channel through the setup wizard", async () => {
|
||||
const { blueBubblesSetupWizard } = await import("./setup-surface.js");
|
||||
const next = blueBubblesSetupWizard.disable?.({
|
||||
channels: {
|
||||
bluebubbles: {
|
||||
enabled: true,
|
||||
serverUrl: "http://127.0.0.1:1234",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(next?.channels?.bluebubbles?.enabled).toBe(false);
|
||||
});
|
||||
});
|
||||
314
extensions/bluebubbles/src/setup-surface.ts
Normal file
314
extensions/bluebubbles/src/setup-surface.ts
Normal file
@@ -0,0 +1,314 @@
|
||||
import {
|
||||
mergeAllowFromEntries,
|
||||
resolveSetupAccountId,
|
||||
} from "../../../src/channels/plugins/setup-flow-helpers.js";
|
||||
import type { ChannelSetupDmPolicy } from "../../../src/channels/plugins/setup-flow-types.js";
|
||||
import type { ChannelSetupWizard } from "../../../src/channels/plugins/setup-wizard.js";
|
||||
import type { OpenClawConfig } from "../../../src/config/config.js";
|
||||
import type { DmPolicy } from "../../../src/config/types.js";
|
||||
import { DEFAULT_ACCOUNT_ID } from "../../../src/routing/session-key.js";
|
||||
import { formatDocsLink } from "../../../src/terminal/links.js";
|
||||
import type { WizardPrompter } from "../../../src/wizard/prompts.js";
|
||||
import {
|
||||
listBlueBubblesAccountIds,
|
||||
resolveBlueBubblesAccount,
|
||||
resolveDefaultBlueBubblesAccountId,
|
||||
} from "./accounts.js";
|
||||
import { applyBlueBubblesConnectionConfig } from "./config-apply.js";
|
||||
import { DEFAULT_WEBHOOK_PATH } from "./monitor-shared.js";
|
||||
import { hasConfiguredSecretInput, normalizeSecretInputString } from "./secret-input.js";
|
||||
import {
|
||||
blueBubblesSetupAdapter,
|
||||
setBlueBubblesAllowFrom,
|
||||
setBlueBubblesDmPolicy,
|
||||
} from "./setup-core.js";
|
||||
import { parseBlueBubblesAllowTarget } from "./targets.js";
|
||||
import { normalizeBlueBubblesServerUrl } from "./types.js";
|
||||
|
||||
const channel = "bluebubbles" as const;
|
||||
const CONFIGURE_CUSTOM_WEBHOOK_FLAG = "__bluebubblesConfigureCustomWebhookPath";
|
||||
|
||||
function parseBlueBubblesAllowFromInput(raw: string): string[] {
|
||||
return raw
|
||||
.split(/[\n,]+/g)
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function validateBlueBubblesAllowFromEntry(value: string): string | null {
|
||||
try {
|
||||
if (value === "*") {
|
||||
return value;
|
||||
}
|
||||
const parsed = parseBlueBubblesAllowTarget(value);
|
||||
if (parsed.kind === "handle" && !parsed.handle) {
|
||||
return null;
|
||||
}
|
||||
return value.trim() || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function promptBlueBubblesAllowFrom(params: {
|
||||
cfg: OpenClawConfig;
|
||||
prompter: WizardPrompter;
|
||||
accountId?: string;
|
||||
}): Promise<OpenClawConfig> {
|
||||
const accountId = resolveSetupAccountId({
|
||||
accountId: params.accountId,
|
||||
defaultAccountId: resolveDefaultBlueBubblesAccountId(params.cfg),
|
||||
});
|
||||
const resolved = resolveBlueBubblesAccount({ cfg: params.cfg, accountId });
|
||||
const existing = resolved.config.allowFrom ?? [];
|
||||
await params.prompter.note(
|
||||
[
|
||||
"Allowlist BlueBubbles DMs by handle or chat target.",
|
||||
"Examples:",
|
||||
"- +15555550123",
|
||||
"- user@example.com",
|
||||
"- chat_id:123",
|
||||
"- chat_guid:iMessage;-;+15555550123",
|
||||
"Multiple entries: comma- or newline-separated.",
|
||||
`Docs: ${formatDocsLink("/channels/bluebubbles", "bluebubbles")}`,
|
||||
].join("\n"),
|
||||
"BlueBubbles allowlist",
|
||||
);
|
||||
const entry = await params.prompter.text({
|
||||
message: "BlueBubbles allowFrom (handle or chat_id)",
|
||||
placeholder: "+15555550123, user@example.com, chat_id:123",
|
||||
initialValue: existing[0] ? String(existing[0]) : undefined,
|
||||
validate: (value) => {
|
||||
const raw = String(value ?? "").trim();
|
||||
if (!raw) {
|
||||
return "Required";
|
||||
}
|
||||
const parts = parseBlueBubblesAllowFromInput(raw);
|
||||
for (const part of parts) {
|
||||
if (!validateBlueBubblesAllowFromEntry(part)) {
|
||||
return `Invalid entry: ${part}`;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
const parts = parseBlueBubblesAllowFromInput(String(entry));
|
||||
const unique = mergeAllowFromEntries(undefined, parts);
|
||||
return setBlueBubblesAllowFrom(params.cfg, accountId, unique);
|
||||
}
|
||||
|
||||
function validateBlueBubblesServerUrlInput(value: unknown): string | undefined {
|
||||
const trimmed = String(value ?? "").trim();
|
||||
if (!trimmed) {
|
||||
return "Required";
|
||||
}
|
||||
try {
|
||||
const normalized = normalizeBlueBubblesServerUrl(trimmed);
|
||||
new URL(normalized);
|
||||
return undefined;
|
||||
} catch {
|
||||
return "Invalid URL format";
|
||||
}
|
||||
}
|
||||
|
||||
function applyBlueBubblesSetupPatch(
|
||||
cfg: OpenClawConfig,
|
||||
accountId: string,
|
||||
patch: {
|
||||
serverUrl?: string;
|
||||
password?: unknown;
|
||||
webhookPath?: string;
|
||||
},
|
||||
): OpenClawConfig {
|
||||
return applyBlueBubblesConnectionConfig({
|
||||
cfg,
|
||||
accountId,
|
||||
patch,
|
||||
onlyDefinedFields: true,
|
||||
accountEnabled: "preserve-or-true",
|
||||
});
|
||||
}
|
||||
|
||||
function resolveBlueBubblesServerUrl(cfg: OpenClawConfig, accountId: string): string | undefined {
|
||||
return resolveBlueBubblesAccount({ cfg, accountId }).config.serverUrl?.trim() || undefined;
|
||||
}
|
||||
|
||||
function resolveBlueBubblesWebhookPath(cfg: OpenClawConfig, accountId: string): string | undefined {
|
||||
return resolveBlueBubblesAccount({ cfg, accountId }).config.webhookPath?.trim() || undefined;
|
||||
}
|
||||
|
||||
function validateBlueBubblesWebhookPath(value: string): string | undefined {
|
||||
const trimmed = String(value ?? "").trim();
|
||||
if (!trimmed) {
|
||||
return "Required";
|
||||
}
|
||||
if (!trimmed.startsWith("/")) {
|
||||
return "Path must start with /";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const dmPolicy: ChannelSetupDmPolicy = {
|
||||
label: "BlueBubbles",
|
||||
channel,
|
||||
policyKey: "channels.bluebubbles.dmPolicy",
|
||||
allowFromKey: "channels.bluebubbles.allowFrom",
|
||||
getCurrent: (cfg) => cfg.channels?.bluebubbles?.dmPolicy ?? "pairing",
|
||||
setPolicy: (cfg, policy) => setBlueBubblesDmPolicy(cfg, policy),
|
||||
promptAllowFrom: promptBlueBubblesAllowFrom,
|
||||
};
|
||||
|
||||
export const blueBubblesSetupWizard: ChannelSetupWizard = {
|
||||
channel,
|
||||
stepOrder: "text-first",
|
||||
status: {
|
||||
configuredLabel: "configured",
|
||||
unconfiguredLabel: "needs setup",
|
||||
configuredHint: "configured",
|
||||
unconfiguredHint: "iMessage via BlueBubbles app",
|
||||
configuredScore: 1,
|
||||
unconfiguredScore: 0,
|
||||
resolveConfigured: ({ cfg }) =>
|
||||
listBlueBubblesAccountIds(cfg).some((accountId) => {
|
||||
const account = resolveBlueBubblesAccount({ cfg, accountId });
|
||||
return account.configured;
|
||||
}),
|
||||
resolveStatusLines: ({ configured }) => [
|
||||
`BlueBubbles: ${configured ? "configured" : "needs setup"}`,
|
||||
],
|
||||
resolveSelectionHint: ({ configured }) =>
|
||||
configured ? "configured" : "iMessage via BlueBubbles app",
|
||||
},
|
||||
prepare: async ({ cfg, accountId, prompter, credentialValues }) => {
|
||||
const existingWebhookPath = resolveBlueBubblesWebhookPath(cfg, accountId);
|
||||
const wantsCustomWebhook = await prompter.confirm({
|
||||
message: `Configure a custom webhook path? (default: ${DEFAULT_WEBHOOK_PATH})`,
|
||||
initialValue: Boolean(existingWebhookPath && existingWebhookPath !== DEFAULT_WEBHOOK_PATH),
|
||||
});
|
||||
return {
|
||||
cfg: wantsCustomWebhook
|
||||
? cfg
|
||||
: applyBlueBubblesSetupPatch(cfg, accountId, { webhookPath: DEFAULT_WEBHOOK_PATH }),
|
||||
credentialValues: {
|
||||
...credentialValues,
|
||||
[CONFIGURE_CUSTOM_WEBHOOK_FLAG]: wantsCustomWebhook ? "1" : "0",
|
||||
},
|
||||
};
|
||||
},
|
||||
credentials: [
|
||||
{
|
||||
inputKey: "password",
|
||||
providerHint: channel,
|
||||
credentialLabel: "server password",
|
||||
helpTitle: "BlueBubbles password",
|
||||
helpLines: [
|
||||
"Enter the BlueBubbles server password.",
|
||||
"Find this in the BlueBubbles Server app under Settings.",
|
||||
],
|
||||
envPrompt: "",
|
||||
keepPrompt: "BlueBubbles password already set. Keep it?",
|
||||
inputPrompt: "BlueBubbles password",
|
||||
inspect: ({ cfg, accountId }) => {
|
||||
const existingPassword = resolveBlueBubblesAccount({ cfg, accountId }).config.password;
|
||||
return {
|
||||
accountConfigured: resolveBlueBubblesAccount({ cfg, accountId }).configured,
|
||||
hasConfiguredValue: hasConfiguredSecretInput(existingPassword),
|
||||
resolvedValue: normalizeSecretInputString(existingPassword) ?? undefined,
|
||||
};
|
||||
},
|
||||
applySet: async ({ cfg, accountId, value }) =>
|
||||
applyBlueBubblesSetupPatch(cfg, accountId, {
|
||||
password: value,
|
||||
}),
|
||||
},
|
||||
],
|
||||
textInputs: [
|
||||
{
|
||||
inputKey: "httpUrl",
|
||||
message: "BlueBubbles server URL",
|
||||
placeholder: "http://192.168.1.100:1234",
|
||||
helpTitle: "BlueBubbles server URL",
|
||||
helpLines: [
|
||||
"Enter the BlueBubbles server URL (e.g., http://192.168.1.100:1234).",
|
||||
"Find this in the BlueBubbles Server app under Connection.",
|
||||
`Docs: ${formatDocsLink("/channels/bluebubbles", "bluebubbles")}`,
|
||||
],
|
||||
currentValue: ({ cfg, accountId }) => resolveBlueBubblesServerUrl(cfg, accountId),
|
||||
validate: ({ value }) => validateBlueBubblesServerUrlInput(value),
|
||||
normalizeValue: ({ value }) => String(value).trim(),
|
||||
applySet: async ({ cfg, accountId, value }) =>
|
||||
applyBlueBubblesSetupPatch(cfg, accountId, {
|
||||
serverUrl: value,
|
||||
}),
|
||||
},
|
||||
{
|
||||
inputKey: "webhookPath",
|
||||
message: "Webhook path",
|
||||
placeholder: DEFAULT_WEBHOOK_PATH,
|
||||
currentValue: ({ cfg, accountId }) => {
|
||||
const value = resolveBlueBubblesWebhookPath(cfg, accountId);
|
||||
return value && value !== DEFAULT_WEBHOOK_PATH ? value : undefined;
|
||||
},
|
||||
shouldPrompt: ({ credentialValues }) =>
|
||||
credentialValues[CONFIGURE_CUSTOM_WEBHOOK_FLAG] === "1",
|
||||
validate: ({ value }) => validateBlueBubblesWebhookPath(value),
|
||||
normalizeValue: ({ value }) => String(value).trim(),
|
||||
applySet: async ({ cfg, accountId, value }) =>
|
||||
applyBlueBubblesSetupPatch(cfg, accountId, {
|
||||
webhookPath: value,
|
||||
}),
|
||||
},
|
||||
],
|
||||
completionNote: {
|
||||
title: "BlueBubbles next steps",
|
||||
lines: [
|
||||
"Configure the webhook URL in BlueBubbles Server:",
|
||||
"1. Open BlueBubbles Server -> Settings -> Webhooks",
|
||||
"2. Add your OpenClaw gateway URL + webhook path",
|
||||
` Example: https://your-gateway-host:3000${DEFAULT_WEBHOOK_PATH}`,
|
||||
"3. Enable the webhook and save",
|
||||
"",
|
||||
`Docs: ${formatDocsLink("/channels/bluebubbles", "bluebubbles")}`,
|
||||
],
|
||||
},
|
||||
dmPolicy,
|
||||
allowFrom: {
|
||||
helpTitle: "BlueBubbles allowlist",
|
||||
helpLines: [
|
||||
"Allowlist BlueBubbles DMs by handle or chat target.",
|
||||
"Examples:",
|
||||
"- +15555550123",
|
||||
"- user@example.com",
|
||||
"- chat_id:123",
|
||||
"- chat_guid:iMessage;-;+15555550123",
|
||||
"Multiple entries: comma- or newline-separated.",
|
||||
`Docs: ${formatDocsLink("/channels/bluebubbles", "bluebubbles")}`,
|
||||
],
|
||||
message: "BlueBubbles allowFrom (handle or chat_id)",
|
||||
placeholder: "+15555550123, user@example.com, chat_id:123",
|
||||
invalidWithoutCredentialNote:
|
||||
"Use a BlueBubbles handle or chat target like +15555550123 or chat_id:123.",
|
||||
parseInputs: parseBlueBubblesAllowFromInput,
|
||||
parseId: (raw) => validateBlueBubblesAllowFromEntry(raw),
|
||||
resolveEntries: async ({ entries }) =>
|
||||
entries.map((entry) => ({
|
||||
input: entry,
|
||||
resolved: Boolean(validateBlueBubblesAllowFromEntry(entry)),
|
||||
id: validateBlueBubblesAllowFromEntry(entry),
|
||||
})),
|
||||
apply: async ({ cfg, accountId, allowFrom }) =>
|
||||
setBlueBubblesAllowFrom(cfg, accountId, allowFrom),
|
||||
},
|
||||
disable: (cfg) => ({
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
bluebubbles: {
|
||||
...cfg.channels?.bluebubbles,
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
export { blueBubblesSetupAdapter };
|
||||
32
extensions/brave/index.ts
Normal file
32
extensions/brave/index.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
createPluginBackedWebSearchProvider,
|
||||
getTopLevelCredentialValue,
|
||||
setTopLevelCredentialValue,
|
||||
} from "../../src/agents/tools/web-search-plugin-factory.js";
|
||||
import { emptyPluginConfigSchema } from "../../src/plugins/config-schema.js";
|
||||
import type { OpenClawPluginApi } from "../../src/plugins/types.js";
|
||||
|
||||
const bravePlugin = {
|
||||
id: "brave",
|
||||
name: "Brave Plugin",
|
||||
description: "Bundled Brave plugin",
|
||||
configSchema: emptyPluginConfigSchema(),
|
||||
register(api: OpenClawPluginApi) {
|
||||
api.registerWebSearchProvider(
|
||||
createPluginBackedWebSearchProvider({
|
||||
id: "brave",
|
||||
label: "Brave Search",
|
||||
hint: "Structured results · country/language/time filters",
|
||||
envVars: ["BRAVE_API_KEY"],
|
||||
placeholder: "BSA...",
|
||||
signupUrl: "https://brave.com/search/api/",
|
||||
docsUrl: "https://docs.openclaw.ai/brave-search",
|
||||
autoDetectOrder: 10,
|
||||
getCredentialValue: getTopLevelCredentialValue,
|
||||
setCredentialValue: setTopLevelCredentialValue,
|
||||
}),
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default bravePlugin;
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"id": "minimax-portal-auth",
|
||||
"providers": ["minimax-portal"],
|
||||
"id": "brave",
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "@openclaw/minimax-portal-auth",
|
||||
"name": "@openclaw/brave-plugin",
|
||||
"version": "2026.3.14",
|
||||
"private": true,
|
||||
"description": "OpenClaw MiniMax Portal OAuth provider plugin",
|
||||
"description": "OpenClaw Brave plugin",
|
||||
"type": "module",
|
||||
"openclaw": {
|
||||
"extensions": [
|
||||
40
extensions/byteplus/index.ts
Normal file
40
extensions/byteplus/index.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
||||
import {
|
||||
buildBytePlusCodingProvider,
|
||||
buildBytePlusProvider,
|
||||
} from "../../src/agents/models-config.providers.static.js";
|
||||
|
||||
const PROVIDER_ID = "byteplus";
|
||||
|
||||
const byteplusPlugin = {
|
||||
id: PROVIDER_ID,
|
||||
name: "BytePlus Provider",
|
||||
description: "Bundled BytePlus provider plugin",
|
||||
configSchema: emptyPluginConfigSchema(),
|
||||
register(api: OpenClawPluginApi) {
|
||||
api.registerProvider({
|
||||
id: PROVIDER_ID,
|
||||
label: "BytePlus",
|
||||
docsPath: "/concepts/model-providers#byteplus-international",
|
||||
envVars: ["BYTEPLUS_API_KEY"],
|
||||
auth: [],
|
||||
catalog: {
|
||||
order: "paired",
|
||||
run: async (ctx) => {
|
||||
const apiKey = ctx.resolveProviderApiKey(PROVIDER_ID).apiKey;
|
||||
if (!apiKey) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
providers: {
|
||||
byteplus: { ...buildBytePlusProvider(), apiKey },
|
||||
"byteplus-plan": { ...buildBytePlusCodingProvider(), apiKey },
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default byteplusPlugin;
|
||||
12
extensions/byteplus/openclaw.plugin.json
Normal file
12
extensions/byteplus/openclaw.plugin.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"id": "byteplus",
|
||||
"providers": ["byteplus", "byteplus-plan"],
|
||||
"providerAuthEnvVars": {
|
||||
"byteplus": ["BYTEPLUS_API_KEY"]
|
||||
},
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user