diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2c7c61f5b8b..5d378bea3f6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -66,6 +66,7 @@ Docs: https://docs.openclaw.ai
- Plugins/CLI: add descriptor-backed lazy plugin CLI registration so Matrix can keep its CLI module lazy-loaded without dropping `openclaw matrix ...` from parse-time command registration. (#57165) Thanks @gumadeiras.
- Plugins/CLI: collect root-help plugin descriptors through a dedicated non-activating CLI metadata path so enabled plugins keep validated config semantics without triggering runtime-only plugin registration work, while preserving runtime CLI command registration for legacy channel plugins that still wire commands from full registration. (#57294) thanks @gumadeiras.
- Anthropic/OAuth: inject `/fast` `service_tier` hints for direct `sk-ant-oat-*` requests so OAuth-authenticated Anthropic runs stop missing the same overload-routing signal as API-key traffic. Fixes #55758. Thanks @Cypherm and @vincentkoc.
+- Docs/anchors: fix broken English docs links and make Mint anchor audits run against the English-source docs tree. (#57039) thanks @velvet-shark.
## 2026.3.28
diff --git a/docs/channels/feishu.md b/docs/channels/feishu.md
index c2a0def6af0..6e196b5bee8 100644
--- a/docs/channels/feishu.md
+++ b/docs/channels/feishu.md
@@ -81,7 +81,7 @@ Lark (global) tenants should use [https://open.larksuite.com/app](https://open.l
2. Fill in the app name + description
3. Choose an app icon
-
+
### 3. Copy credentials
@@ -92,7 +92,7 @@ From **Credentials & Basic Info**, copy:
❗ **Important:** keep the App Secret private.
-
+
### 4. Configure permissions
@@ -126,7 +126,7 @@ On **Permissions**, click **Batch import** and paste:
}
```
-
+
### 5. Enable bot capability
@@ -135,7 +135,7 @@ In **App Capability** > **Bot**:
1. Enable bot capability
2. Set the bot name
-
+
### 6. Configure event subscription
@@ -151,7 +151,7 @@ In **Event Subscription**:
⚠️ If the gateway is not running, the long-connection setup may fail to save.
-
+
### 7. Publish the app
@@ -206,7 +206,7 @@ When using webhook mode, set both `channels.feishu.verificationToken` and `chann
The screenshot below shows where to find the **Verification Token**. The **Encrypt Key** is listed in the same **Encryption** section.
-
+
### Configure via environment variables
@@ -395,6 +395,8 @@ In addition to allowing the group itself, **all messages** in that group are gat
---
+
+
## Get group/user IDs
### Group IDs (chat_id)
diff --git a/docs/channels/groups.md b/docs/channels/groups.md
index 66b8720a8c4..58490e8d247 100644
--- a/docs/channels/groups.md
+++ b/docs/channels/groups.md
@@ -54,6 +54,8 @@ If you want...
- Direct chats use the main session (or per-sender if configured).
- Heartbeats are skipped for group sessions.
+
+
## Pattern: personal DMs + public groups (single agent)
Yes — this works well if your “personal” traffic is **DMs** and your “public” traffic is **groups**.
diff --git a/docs/gateway/configuration-reference.md b/docs/gateway/configuration-reference.md
index 96a547888e9..7e9486b5721 100644
--- a/docs/gateway/configuration-reference.md
+++ b/docs/gateway/configuration-reference.md
@@ -1125,6 +1125,8 @@ See [Streaming](/concepts/streaming) for behavior + chunking details.
See [Typing Indicators](/concepts/typing-indicators).
+
+
### `agents.defaults.sandbox`
Optional sandboxing for the embedded agent. See [Sandboxing](/gateway/sandboxing) for the full guide.
diff --git a/docs/gateway/security/index.md b/docs/gateway/security/index.md
index a20361a00f8..c5dc4e365e9 100644
--- a/docs/gateway/security/index.md
+++ b/docs/gateway/security/index.md
@@ -602,6 +602,8 @@ Recommendations:
- When running small models, **enable sandboxing for all sessions** and **disable web_search/web_fetch/browser** unless inputs are tightly controlled.
- For chat-only personal assistants with trusted input and no tools, smaller models are usually fine.
+
+
## Reasoning & verbose output in groups
`/reasoning` and `/verbose` can expose internal reasoning or tool output that
diff --git a/docs/help/faq.md b/docs/help/faq.md
index 1d7c164efaa..4f20649936d 100644
--- a/docs/help/faq.md
+++ b/docs/help/faq.md
@@ -585,11 +585,12 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS,
-
- That means your **Anthropic quota/rate limit** is exhausted for the current window. If you
- use a **Claude subscription** (setup-token), wait for the window to
- reset or upgrade your plan. If you use an **Anthropic API key**, check the Anthropic Console
- for usage/billing and raise limits as needed.
+
+
+That means your **Anthropic quota/rate limit** is exhausted for the current window. If you
+use a **Claude subscription** (setup-token), wait for the window to
+reset or upgrade your plan. If you use an **Anthropic API key**, check the Anthropic Console
+for usage/billing and raise limits as needed.
If the message is specifically:
`Extra usage is required for long context requests`, the request is trying to use
diff --git a/docs/install/installer.md b/docs/install/installer.md
index dcf1ec8b759..f42fe335377 100644
--- a/docs/install/installer.md
+++ b/docs/install/installer.md
@@ -58,6 +58,8 @@ If install succeeds but `openclaw` is not found in a new terminal, see [Node.js
---
+
+
## install.sh
@@ -170,6 +172,8 @@ The script exits with code `2` for invalid method selection or invalid `--instal
---
+
+
## install-cli.sh
@@ -248,6 +252,8 @@ Designed for environments where you want everything under a local prefix (defaul
---
+
+
## install.ps1
### Flow (install.ps1)
diff --git a/docs/install/podman.md b/docs/install/podman.md
index f99ad257fb1..decfbb390df 100644
--- a/docs/install/podman.md
+++ b/docs/install/podman.md
@@ -165,6 +165,8 @@ openclaw devices list \
--token "$(sed -n 's/^OPENCLAW_GATEWAY_TOKEN=//p' ~/.openclaw/.env | head -n1)"
```
+
+
## Podman + Tailscale
For HTTPS or remote browser access, follow the main Tailscale docs.
diff --git a/docs/plugins/sdk-channel-plugins.md b/docs/plugins/sdk-channel-plugins.md
index 53ff0e11b49..2c4e1eb9bdd 100644
--- a/docs/plugins/sdk-channel-plugins.md
+++ b/docs/plugins/sdk-channel-plugins.md
@@ -37,6 +37,7 @@ dispatch.
## Walkthrough
+
Create the standard plugin files. The `channel` field in `package.json` is
what makes this a channel plugin:
@@ -298,8 +299,9 @@ dispatch.
-
- Write colocated tests in `src/channel.test.ts`:
+
+
+Write colocated tests in `src/channel.test.ts`:
```typescript src/channel.test.ts
import { describe, it, expect } from "vitest";
diff --git a/docs/plugins/sdk-provider-plugins.md b/docs/plugins/sdk-provider-plugins.md
index e6a5aac2eb2..86a7c4f0546 100644
--- a/docs/plugins/sdk-provider-plugins.md
+++ b/docs/plugins/sdk-provider-plugins.md
@@ -23,6 +23,7 @@ API key auth, and dynamic model resolution.
## Walkthrough
+
```json package.json
@@ -319,6 +320,7 @@ API key auth, and dynamic model resolution.
+
A provider plugin can register speech, media understanding, image
generation, and web search alongside text inference:
@@ -360,6 +362,7 @@ API key auth, and dynamic model resolution.
+
```typescript src/provider.test.ts
import { describe, it, expect } from "vitest";
// Export your provider config object from index.ts or a dedicated file
diff --git a/docs/providers/qwen.md b/docs/providers/qwen.md
index 3a969a54e44..05a3576faf7 100644
--- a/docs/providers/qwen.md
+++ b/docs/providers/qwen.md
@@ -19,7 +19,7 @@ background.
## Recommended: Model Studio (Alibaba Cloud Coding Plan)
-Use [Model Studio](/providers/modelstudio) for officially supported access to
+Use [Model Studio](/providers/qwen_modelstudio) for officially supported access to
Qwen models (Qwen 3.5 Plus, GLM-4.7, Kimi K2.5, and more).
```bash
@@ -30,4 +30,4 @@ openclaw onboard --auth-choice modelstudio-api-key
openclaw onboard --auth-choice modelstudio-api-key-cn
```
-See [Model Studio](/providers/modelstudio) for full setup details.
+See [Model Studio](/providers/qwen_modelstudio) for full setup details.
diff --git a/docs/web/dashboard.md b/docs/web/dashboard.md
index 71238e0b2bc..682c155b841 100644
--- a/docs/web/dashboard.md
+++ b/docs/web/dashboard.md
@@ -42,6 +42,8 @@ Prefer localhost, Tailscale Serve, or an SSH tunnel.
- If `gateway.auth.token` is configured as a SecretRef and is unresolved in your current shell, `openclaw dashboard` still prints a non-tokenized URL plus actionable auth setup guidance.
- **Not localhost**: use Tailscale Serve (tokenless for Control UI/WebSocket if `gateway.auth.allowTailscale: true`, assumes trusted gateway host; HTTP APIs still need token/password), tailnet bind with a token, or an SSH tunnel. See [Web surfaces](/web).
+
+
## If you see "unauthorized" / 1008
- Ensure the gateway is reachable (local: `openclaw status`; remote: SSH tunnel `ssh -N -L 18789:127.0.0.1:18789 user@host` then open `http://127.0.0.1:18789/`).
diff --git a/scripts/docs-link-audit.mjs b/scripts/docs-link-audit.mjs
index ad0e40f3264..1c1e899c1ba 100644
--- a/scripts/docs-link-audit.mjs
+++ b/scripts/docs-link-audit.mjs
@@ -2,6 +2,7 @@
import { spawnSync } from "node:child_process";
import fs from "node:fs";
+import os from "node:os";
import path from "node:path";
import { pathToFileURL } from "node:url";
@@ -67,8 +68,12 @@ for (const item of docsConfig.redirects || []) {
const allFiles = walk(DOCS_DIR);
const relAllFiles = new Set(allFiles.map((abs) => normalizeSlashes(path.relative(DOCS_DIR, abs))));
+function isLocalizedDocPath(p) {
+ return /^\/?[a-z]{2}(?:-[A-Za-z]{2,8})+\//.test(p);
+}
+
function isGeneratedTranslatedDoc(relPath) {
- return relPath.startsWith("zh-CN/");
+ return isLocalizedDocPath(relPath);
}
const markdownFiles = allFiles.filter((abs) => {
@@ -169,6 +174,95 @@ function collectNavPageEntries(node) {
const markdownLinkRegex = /!?\[[^\]]*\]\(([^)]+)\)/g;
+export function sanitizeDocsConfigForEnglishOnly(value) {
+ if (Array.isArray(value)) {
+ return value
+ .map((item) => sanitizeDocsConfigForEnglishOnly(item))
+ .filter((item) => item !== undefined);
+ }
+
+ if (!value || typeof value !== "object") {
+ if (typeof value === "string" && isLocalizedDocPath(value)) {
+ return undefined;
+ }
+ return value;
+ }
+
+ const record = /** @type {Record} */ (value);
+ if (typeof record.language === "string" && record.language !== "en") {
+ return undefined;
+ }
+
+ /** @type {Record} */
+ const sanitized = {};
+ for (const [key, child] of Object.entries(record)) {
+ const next = sanitizeDocsConfigForEnglishOnly(child);
+ if (next === undefined) {
+ continue;
+ }
+ if (Array.isArray(next) && next.length === 0) {
+ continue;
+ }
+ if (
+ next &&
+ typeof next === "object" &&
+ !Array.isArray(next) &&
+ Object.keys(next).length === 0
+ ) {
+ continue;
+ }
+ sanitized[key] = next;
+ }
+
+ if (record.pages && !Array.isArray(sanitized.pages)) {
+ return undefined;
+ }
+ if (record.groups && !Array.isArray(sanitized.groups)) {
+ return undefined;
+ }
+ if (record.tabs && !Array.isArray(sanitized.tabs)) {
+ return undefined;
+ }
+ if (
+ "source" in record &&
+ typeof record.source === "string" &&
+ typeof sanitized.source !== "string"
+ ) {
+ return undefined;
+ }
+ if (
+ "destination" in record &&
+ typeof record.destination === "string" &&
+ typeof sanitized.destination !== "string"
+ ) {
+ return undefined;
+ }
+
+ return Object.keys(sanitized).length > 0 ? sanitized : undefined;
+}
+
+export function prepareAnchorAuditDocsDir(sourceDir = DOCS_DIR) {
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-docs-anchor-audit-"));
+ fs.cpSync(sourceDir, tempDir, { recursive: true });
+
+ for (const entry of fs.readdirSync(tempDir, { withFileTypes: true })) {
+ if (!entry.isDirectory()) {
+ continue;
+ }
+ if (!isGeneratedTranslatedDoc(`${entry.name}/`)) {
+ continue;
+ }
+ fs.rmSync(path.join(tempDir, entry.name), { recursive: true, force: true });
+ }
+
+ const docsJsonPath = path.join(tempDir, "docs.json");
+ const docsConfig = JSON.parse(fs.readFileSync(docsJsonPath, "utf8"));
+ const sanitized = sanitizeDocsConfigForEnglishOnly(docsConfig);
+ fs.writeFileSync(docsJsonPath, `${JSON.stringify(sanitized, null, 2)}\n`, "utf8");
+
+ return tempDir;
+}
+
export function auditDocsLinks() {
/** @type {{file: string; line: number; link: string; reason: string}[]} */
const broken = [];
@@ -293,26 +387,39 @@ export function auditDocsLinks() {
* @param {{
* args?: string[];
* spawnSyncImpl?: typeof spawnSync;
+ * prepareAnchorAuditDocsDirImpl?: (sourceDir?: string) => string;
+ * cleanupAnchorAuditDocsDirImpl?: (dir: string) => void;
* }} [options]
*/
export function runDocsLinkAuditCli(options = {}) {
const args = options.args ?? process.argv.slice(2);
if (args.includes("--anchors")) {
const spawnSyncImpl = options.spawnSyncImpl ?? spawnSync;
- const result = spawnSyncImpl("mint", ["broken-links", "--check-anchors"], {
- cwd: DOCS_DIR,
- stdio: "inherit",
- });
+ const prepareAnchorAuditDocsDirImpl =
+ options.prepareAnchorAuditDocsDirImpl ?? prepareAnchorAuditDocsDir;
+ const cleanupAnchorAuditDocsDirImpl =
+ options.cleanupAnchorAuditDocsDirImpl ??
+ ((dir) => fs.rmSync(dir, { recursive: true, force: true }));
+ const anchorDocsDir = prepareAnchorAuditDocsDirImpl(DOCS_DIR);
- if (result.error?.code === "ENOENT") {
- const fallback = spawnSyncImpl("pnpm", ["dlx", "mint", "broken-links", "--check-anchors"], {
- cwd: DOCS_DIR,
+ try {
+ const result = spawnSyncImpl("mint", ["broken-links", "--check-anchors"], {
+ cwd: anchorDocsDir,
stdio: "inherit",
});
- return fallback.status ?? 1;
- }
- return result.status ?? 1;
+ if (result.error?.code === "ENOENT") {
+ const fallback = spawnSyncImpl("pnpm", ["dlx", "mint", "broken-links", "--check-anchors"], {
+ cwd: anchorDocsDir,
+ stdio: "inherit",
+ });
+ return fallback.status ?? 1;
+ }
+
+ return result.status ?? 1;
+ } finally {
+ cleanupAnchorAuditDocsDirImpl(anchorDocsDir);
+ }
}
const { checked, broken } = auditDocsLinks();
diff --git a/src/scripts/docs-link-audit.test.ts b/src/scripts/docs-link-audit.test.ts
index 01fb0d362f6..b87252b1e3b 100644
--- a/src/scripts/docs-link-audit.test.ts
+++ b/src/scripts/docs-link-audit.test.ts
@@ -1,22 +1,33 @@
+import fs from "node:fs";
+import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
-const { normalizeRoute, resolveRoute, runDocsLinkAuditCli } =
- (await import("../../scripts/docs-link-audit.mjs")) as unknown as {
- normalizeRoute: (route: string) => string;
- resolveRoute: (
- route: string,
- options?: { redirects?: Map; routes?: Set },
- ) => { ok: boolean; terminal: string; loop?: boolean };
- runDocsLinkAuditCli: (options?: {
- args?: string[];
- spawnSyncImpl?: (
- command: string,
- args: string[],
- options: { cwd: string; stdio: string },
- ) => { status: number | null; error?: { code?: string } };
- }) => number;
- };
+const {
+ normalizeRoute,
+ prepareAnchorAuditDocsDir,
+ resolveRoute,
+ runDocsLinkAuditCli,
+ sanitizeDocsConfigForEnglishOnly,
+} = (await import("../../scripts/docs-link-audit.mjs")) as unknown as {
+ normalizeRoute: (route: string) => string;
+ prepareAnchorAuditDocsDir: (sourceDir?: string) => string;
+ resolveRoute: (
+ route: string,
+ options?: { redirects?: Map; routes?: Set },
+ ) => { ok: boolean; terminal: string; loop?: boolean };
+ runDocsLinkAuditCli: (options?: {
+ args?: string[];
+ spawnSyncImpl?: (
+ command: string,
+ args: string[],
+ options: { cwd: string; stdio: string },
+ ) => { status: number | null; error?: { code?: string } };
+ prepareAnchorAuditDocsDirImpl?: (sourceDir?: string) => string;
+ cleanupAnchorAuditDocsDirImpl?: (dir: string) => void;
+ }) => number;
+ sanitizeDocsConfigForEnglishOnly: (value: unknown) => unknown;
+};
describe("docs-link-audit", () => {
it("normalizes route fragments away", () => {
@@ -38,6 +49,101 @@ describe("docs-link-audit", () => {
});
});
+ it("sanitizes docs.json to English-only route targets", () => {
+ expect(
+ sanitizeDocsConfigForEnglishOnly({
+ navigation: [
+ {
+ language: "en",
+ tabs: [
+ {
+ tab: "Docs",
+ groups: [
+ {
+ group: "Keep",
+ pages: ["help/testing", "zh-CN/help/testing", "ja-JP/help/testing"],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ language: "zh-Hans",
+ tabs: [{ tab: "中文", groups: [{ group: "帮助", pages: ["zh-CN/help/testing"] }] }],
+ },
+ ],
+ redirects: [
+ { source: "/help/testing", destination: "/help/testing" },
+ { source: "/zh-CN/help/testing", destination: "/help/testing" },
+ { source: "/help/testing", destination: "/ja-JP/help/testing" },
+ ],
+ }),
+ ).toEqual({
+ navigation: [
+ {
+ language: "en",
+ tabs: [
+ {
+ tab: "Docs",
+ groups: [{ group: "Keep", pages: ["help/testing"] }],
+ },
+ ],
+ },
+ ],
+ redirects: [{ source: "/help/testing", destination: "/help/testing" }],
+ });
+ });
+
+ it("builds an English-only docs tree for anchor audits", () => {
+ const fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), "docs-link-audit-fixture-"));
+ const docsRoot = path.join(fixtureRoot, "docs");
+ fs.mkdirSync(path.join(docsRoot, "help"), { recursive: true });
+ fs.mkdirSync(path.join(docsRoot, "zh-CN", "help"), { recursive: true });
+ fs.writeFileSync(
+ path.join(docsRoot, "docs.json"),
+ `${JSON.stringify(
+ {
+ navigation: [
+ {
+ language: "en",
+ tabs: [{ tab: "Docs", groups: [{ group: "Help", pages: ["help/testing"] }] }],
+ },
+ {
+ language: "zh-Hans",
+ tabs: [{ tab: "中文", groups: [{ group: "帮助", pages: ["zh-CN/help/testing"] }] }],
+ },
+ ],
+ },
+ null,
+ 2,
+ )}\n`,
+ "utf8",
+ );
+ fs.writeFileSync(path.join(docsRoot, "help", "testing.md"), "# testing\n", "utf8");
+ fs.writeFileSync(path.join(docsRoot, "zh-CN", "help", "testing.md"), "# 测试\n", "utf8");
+
+ const anchorDocsDir = prepareAnchorAuditDocsDir(docsRoot);
+ try {
+ expect(fs.existsSync(path.join(anchorDocsDir, "help", "testing.md"))).toBe(true);
+ expect(fs.existsSync(path.join(anchorDocsDir, "zh-CN"))).toBe(false);
+
+ const sanitizedDocsJson = JSON.parse(
+ fs.readFileSync(path.join(anchorDocsDir, "docs.json"), "utf8"),
+ );
+ expect(sanitizedDocsJson).toEqual({
+ navigation: [
+ {
+ language: "en",
+ tabs: [{ tab: "Docs", groups: [{ group: "Help", pages: ["help/testing"] }] }],
+ },
+ ],
+ });
+ } finally {
+ fs.rmSync(anchorDocsDir, { recursive: true, force: true });
+ fs.rmSync(fixtureRoot, { recursive: true, force: true });
+ }
+ });
+
it("prefers a local mint binary for anchor validation", () => {
let invocation:
| {
@@ -46,9 +152,17 @@ describe("docs-link-audit", () => {
options: { cwd: string; stdio: string };
}
| undefined;
+ let cleanedDir: string | undefined;
+ const anchorDocsDir = path.join(os.tmpdir(), "docs-link-audit-anchor");
const exitCode = runDocsLinkAuditCli({
args: ["--anchors"],
+ prepareAnchorAuditDocsDirImpl() {
+ return anchorDocsDir;
+ },
+ cleanupAnchorAuditDocsDirImpl(dir) {
+ cleanedDir = dir;
+ },
spawnSyncImpl(command, args, options) {
invocation = { command, args, options };
return { status: 0 };
@@ -60,7 +174,8 @@ describe("docs-link-audit", () => {
expect(invocation?.command).toBe("mint");
expect(invocation?.args).toEqual(["broken-links", "--check-anchors"]);
expect(invocation?.options.stdio).toBe("inherit");
- expect(path.basename(invocation?.options.cwd ?? "")).toBe("docs");
+ expect(invocation?.options.cwd).toBe(anchorDocsDir);
+ expect(cleanedDir).toBe(anchorDocsDir);
});
it("falls back to pnpm dlx when mint is not on PATH", () => {
@@ -69,9 +184,17 @@ describe("docs-link-audit", () => {
args: string[];
options: { cwd: string; stdio: string };
}> = [];
+ let cleanedDir: string | undefined;
+ const anchorDocsDir = path.join(os.tmpdir(), "docs-link-audit-anchor");
const exitCode = runDocsLinkAuditCli({
args: ["--anchors"],
+ prepareAnchorAuditDocsDirImpl() {
+ return anchorDocsDir;
+ },
+ cleanupAnchorAuditDocsDirImpl(dir) {
+ cleanedDir = dir;
+ },
spawnSyncImpl(command, args, options) {
invocations.push({ command, args, options });
if (command === "mint") {
@@ -93,7 +216,8 @@ describe("docs-link-audit", () => {
args: ["dlx", "mint", "broken-links", "--check-anchors"],
options: { stdio: "inherit" },
});
- expect(path.basename(invocations[0]?.options.cwd ?? "")).toBe("docs");
- expect(path.basename(invocations[1]?.options.cwd ?? "")).toBe("docs");
+ expect(invocations[0]?.options.cwd).toBe(anchorDocsDir);
+ expect(invocations[1]?.options.cwd).toBe(anchorDocsDir);
+ expect(cleanedDir).toBe(anchorDocsDir);
});
});