docs: fix English link audits (#57039)

Merged via squash.

Prepared head SHA: d20a3b620f
Co-authored-by: velvet-shark <126378+velvet-shark@users.noreply.github.com>
Reviewed-by: @velvet-shark
This commit is contained in:
Radek Sienkiewicz 2026-03-30 01:21:00 +02:00 committed by GitHub
parent 6c91b27756
commit 4680335b2a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 301 additions and 45 deletions

View File

@ -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

View File

@ -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
![Create enterprise app](../images/feishu-step2-create-app.png)
![Create enterprise app](/images/feishu-step2-create-app.png)
### 3. Copy credentials
@ -92,7 +92,7 @@ From **Credentials & Basic Info**, copy:
**Important:** keep the App Secret private.
![Get credentials](../images/feishu-step3-credentials.png)
![Get credentials](/images/feishu-step3-credentials.png)
### 4. Configure permissions
@ -126,7 +126,7 @@ On **Permissions**, click **Batch import** and paste:
}
```
![Configure permissions](../images/feishu-step4-permissions.png)
![Configure permissions](/images/feishu-step4-permissions.png)
### 5. Enable bot capability
@ -135,7 +135,7 @@ In **App Capability** > **Bot**:
1. Enable bot capability
2. Set the bot name
![Enable bot capability](../images/feishu-step5-bot-capability.png)
![Enable bot capability](/images/feishu-step5-bot-capability.png)
### 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.
![Configure event subscription](../images/feishu-step6-event-subscription.png)
![Configure event subscription](/images/feishu-step6-event-subscription.png)
### 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.
![Verification Token location](../images/feishu-verification-token.png)
![Verification Token location](/images/feishu-verification-token.png)
### Configure via environment variables
@ -395,6 +395,8 @@ In addition to allowing the group itself, **all messages** in that group are gat
---
<a id="get-groupuser-ids"></a>
## Get group/user IDs
### Group IDs (chat_id)

View File

@ -54,6 +54,8 @@ If you want...
- Direct chats use the main session (or per-sender if configured).
- Heartbeats are skipped for group sessions.
<a id="pattern-personal-dms-public-groups-single-agent"></a>
## Pattern: personal DMs + public groups (single agent)
Yes — this works well if your “personal” traffic is **DMs** and your “public” traffic is **groups**.

View File

@ -1125,6 +1125,8 @@ See [Streaming](/concepts/streaming) for behavior + chunking details.
See [Typing Indicators](/concepts/typing-indicators).
<a id="agentsdefaultssandbox"></a>
### `agents.defaults.sandbox`
Optional sandboxing for the embedded agent. See [Sandboxing](/gateway/sandboxing) for the full guide.

View File

@ -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.
<a id="reasoning-verbose-output-in-groups"></a>
## Reasoning & verbose output in groups
`/reasoning` and `/verbose` can expose internal reasoning or tool output that

View File

@ -585,11 +585,12 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS,
</Accordion>
<Accordion title="Why am I seeing HTTP 429 rate_limit_error from Anthropic?">
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.
<a id="why-am-i-seeing-http-429-ratelimiterror-from-anthropic"></a>
<Accordion title="Why am I seeing HTTP 429 rate_limit_error from Anthropic?">
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

View File

@ -58,6 +58,8 @@ If install succeeds but `openclaw` is not found in a new terminal, see [Node.js
---
<a id="installsh"></a>
## install.sh
<Tip>
@ -170,6 +172,8 @@ The script exits with code `2` for invalid method selection or invalid `--instal
---
<a id="install-clish"></a>
## install-cli.sh
<Info>
@ -248,6 +252,8 @@ Designed for environments where you want everything under a local prefix (defaul
---
<a id="installps1"></a>
## install.ps1
### Flow (install.ps1)

View File

@ -165,6 +165,8 @@ openclaw devices list \
--token "$(sed -n 's/^OPENCLAW_GATEWAY_TOKEN=//p' ~/.openclaw/.env | head -n1)"
```
<a id="podman--tailscale"></a>
## Podman + Tailscale
For HTTPS or remote browser access, follow the main Tailscale docs.

View File

@ -37,6 +37,7 @@ dispatch.
## Walkthrough
<Steps>
<a id="step-1-package-and-manifest"></a>
<Step title="Package and manifest">
Create the standard plugin files. The `channel` field in `package.json` is
what makes this a channel plugin:
@ -298,8 +299,9 @@ dispatch.
</Step>
<Step title="Test">
Write colocated tests in `src/channel.test.ts`:
<a id="step-6-test"></a>
<Step title="Test">
Write colocated tests in `src/channel.test.ts`:
```typescript src/channel.test.ts
import { describe, it, expect } from "vitest";

View File

@ -23,6 +23,7 @@ API key auth, and dynamic model resolution.
## Walkthrough
<Steps>
<a id="step-1-package-and-manifest"></a>
<Step title="Package and manifest">
<CodeGroup>
```json package.json
@ -319,6 +320,7 @@ API key auth, and dynamic model resolution.
</Step>
<Step title="Add extra capabilities (optional)">
<a id="step-5-add-extra-capabilities"></a>
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.
</Step>
<Step title="Test">
<a id="step-6-test"></a>
```typescript src/provider.test.ts
import { describe, it, expect } from "vitest";
// Export your provider config object from index.ts or a dedicated file

View File

@ -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.

View File

@ -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).
<a id="if-you-see-unauthorized-1008"></a>
## 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/`).

View File

@ -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<string, unknown>} */ (value);
if (typeof record.language === "string" && record.language !== "en") {
return undefined;
}
/** @type {Record<string, unknown>} */
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();

View File

@ -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<string, string>; routes?: Set<string> },
) => { 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<string, string>; routes?: Set<string> },
) => { 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);
});
});