fix(docs): repair broken documentation links (#4330)

* fix(docs): repair broken documentation links

* revert(docs): restore ACP protocol naming
This commit is contained in:
urzeye 2026-07-22 07:32:05 +08:00 committed by GitHub
parent 3c0a45ad77
commit cd86275638
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 284 additions and 30 deletions

View File

@ -1,7 +1,7 @@
import type { PageMapItem } from "nextra";
import { getPageMap } from "nextra/page-map";
import { Layout } from "nextra-theme-docs";
import { buildLocalizedDocsPageMap } from "@/components/docs/docs-page-map";
import { Footer } from "@/components/landing/footer";
import { Header } from "@/components/landing/header";
import { getLocaleByLang } from "@/core/i18n/locale";
@ -12,23 +12,11 @@ const i18n = [
{ locale: "zh", name: "中文" },
];
function formatPageRoute(base: string, items: PageMapItem[]): PageMapItem[] {
return items.map((item) => {
if ("route" in item && !item.route.startsWith(base)) {
item.route = `${base}${item.route}`;
}
if ("children" in item && item.children) {
item.children = formatPageRoute(base, item.children);
}
return item;
});
}
export default async function DocLayout({ children, params }) {
const { lang } = await params;
const locale = getLocaleByLang(lang);
const pages = await getPageMap(`/${lang}`);
const pageMap = formatPageRoute(`/${lang}/docs`, pages);
const pageMap = buildLocalizedDocsPageMap(`/${lang}/docs`, pages);
return (
<Layout
@ -40,7 +28,7 @@ export default async function DocLayout({ children, params }) {
/>
}
pageMap={pageMap}
docsRepositoryBase="https://github.com/bytedance/deerflow/tree/main/frontend/src/content"
docsRepositoryBase="https://github.com/bytedance/deer-flow/tree/main/frontend"
footer={<Footer className="mt-0" />}
i18n={i18n}
// ... Your additional layout options

View File

@ -13,7 +13,7 @@ export default async function BlogLayout({ children }) {
navbar={<Header className="relative max-w-full px-10" homeURL="/" />}
pageMap={pageMap}
sidebar={{ defaultOpen: true }}
docsRepositoryBase="https://github.com/bytedance/deerflow/tree/main/frontend/src/content"
docsRepositoryBase="https://github.com/bytedance/deer-flow/tree/main/frontend"
footer={<Footer />}
>
{children}

View File

@ -0,0 +1,49 @@
import type { PageMapItem } from "nextra";
// Nextra's generated page map also contains unrelated App Router pages. Only
// these top-level content directories belong under /{lang}/docs.
export const DOCS_CONTENT_ROOTS = [
"application",
"harness",
"introduction",
"posts",
"reference",
"tutorials",
] as const;
const DOCS_CONTENT_ROOT_SET = new Set<string>(DOCS_CONTENT_ROOTS);
function isLocalizedDocsRoute(route: string, base: string): boolean {
return route === base || route.startsWith(`${base}/`);
}
function isDocsContentRoute(route: string): boolean {
const root = route.split("/").find(Boolean);
return root === undefined || DOCS_CONTENT_ROOT_SET.has(root);
}
export function buildLocalizedDocsPageMap(
base: string,
items: PageMapItem[],
): PageMapItem[] {
return items.flatMap<PageMapItem>((item) => {
if (!("route" in item)) {
return [item];
}
const alreadyLocalized = isLocalizedDocsRoute(item.route, base);
if (!alreadyLocalized && !isDocsContentRoute(item.route)) {
return [];
}
return [
{
...item,
route: alreadyLocalized ? item.route : `${base}${item.route}`,
...("children" in item
? { children: buildLocalizedDocsPageMap(base, item.children) }
: {}),
},
];
});
}

View File

@ -33,7 +33,7 @@ The Lead Agent does not hardcode a specific workflow. It uses the model's reason
The Lead Agent is built on **LangGraph** and **LangChain Agent** primitives. Specifically:
- [`create_agent`](https://python.langchain.com/docs/concepts/agents/) from `langchain.agents` wraps the LLM into a tool-calling agent loop.
- [`create_agent`](https://docs.langchain.com/oss/python/langchain/agents) from `langchain.agents` wraps the LLM into a tool-calling agent loop.
- LangGraph manages the `ThreadState` and provides the checkpointing, streaming, and graph execution model.
- A **middleware chain** wraps every turn of the agent loop, providing cross-cutting capabilities like memory, summarization, and clarification.

View File

@ -96,7 +96,7 @@ If the agent tries to call more subagents than the limits allow, the middleware
## ACP agents (external agents)
In addition to the built-in subagents, DeerFlow supports delegating to external agents through the **Agent Connect Protocol (ACP)**. ACP allows DeerFlow to invoke agents running as separate processes (including third-party CLI tools wrapped with an ACP adapter).
In addition to the built-in subagents, DeerFlow supports delegating to external agents through the **Agent Client Protocol (ACP)**. ACP allows DeerFlow to invoke agents running as separate processes (including third-party CLI tools wrapped with an ACP adapter).
Configure ACP agents in `config.yaml`:

View File

@ -38,9 +38,9 @@ Start with the Harness section. This path is for teams who want to integrate Dee
Start with the App section. This path is for teams who want to run DeerFlow as a complete application and understand how to configure, operate, and use it in practice.
- [DeerFlow App](./docs/application)
- [Quick Start](./docs/app/quick-start)
- [Deployment Guide](./docs/app/deployment-guide)
- [Workspace Usage](./docs/app/workspace-usage)
- [Quick Start](./docs/application/quick-start)
- [Deployment Guide](./docs/application/deployment-guide)
- [Workspace Usage](./docs/application/workspace-usage)
## Documentation structure
@ -79,17 +79,17 @@ The App section is written for teams who want to deploy DeerFlow as a usable pro
The Tutorials section is for hands-on, task-oriented learning.
- [Tutorials](./docs/tutorials)
- [Tutorials](./docs/tutorials/first-conversation)
### Reference
The Reference section is for detailed lookup material, including configuration, runtime modes, APIs, and source-oriented mapping.
- [Reference](./docs/reference)
- [Reference](./docs/reference/model-providers)
## Choose the right path
- If you are **evaluating the project**, start with [Introduction](./docs/introduction).
- If you are **building your own agent system**, start with [DeerFlow Harness](./docs/harness).
- If you are **deploying DeerFlow for users**, start with [DeerFlow App](./docs/application).
- If you want to **learn by doing**, go to [Tutorials](./docs/tutorials).
- If you want to **learn by doing**, go to [Tutorials](./docs/tutorials/first-conversation).

View File

@ -95,7 +95,7 @@ The two layers are complementary. The Harness gives you the agent runtime. The A
<Cards num={2}>
<Cards.Card title="DeerFlow Harness" href="/docs/harness" />
<Cards.Card title="DeerFlow App" href="/docs/app" />
<Cards.Card title="DeerFlow App" href="/docs/application" />
</Cards>
## One foundation, two entry points
@ -105,4 +105,4 @@ DeerFlow is not split into two unrelated products. It is one system with two ent
The **Harness** is the core runtime for developers.
The **App** is the best-practice Super Agent application built on top of that runtime.
If you want to go deeper into the runtime itself, continue with the [DeerFlow Harness](/docs/harness). If you want to run DeerFlow as a product, continue with the [DeerFlow App](/docs/app).
If you want to go deeper into the runtime itself, continue with the [DeerFlow Harness](/docs/harness). If you want to run DeerFlow as a product, continue with the [DeerFlow App](/docs/application).

View File

@ -14,7 +14,7 @@ When a large model provider decides that an input or output has triggered a safe
These partial tool calls must not be executed as normal intent. A truncated `write_file` call may write an incomplete report. A truncated `bash` call may enter the sandbox with incomplete arguments. After seeing the failed result, the Agent may retry and trigger the same safety rule repeatedly.
[PR #3035](https://github.com/bytedance/deer-flow/pull/3035) addresses this boundary: when a provider stops generation with a safety signal while the response still contains tool calls, DeerFlow should suppress those tool calls first and record the turn as a safety termination event.
[the implementation commit](https://github.com/bytedance/deer-flow/commit/be0eae9825619b63ca0c67b253d40b5eee76a2d6) addresses this boundary: when a provider stops generation with a safety signal while the response still contains tool calls, DeerFlow should suppress those tool calls first and record the turn as a safety termination event.
## Why Safety Termination Needs Dedicated Handling

View File

@ -33,7 +33,7 @@ Lead Agent 不硬编码特定的工作流。它使用模型的推理能力来适
Lead Agent 基于 **LangGraph** 和 **LangChain Agent** 原语构建:
- `langchain.agents` 的 [`create_agent`](https://python.langchain.com/docs/concepts/agents/) 将 LLM 封装成工具调用 Agent 循环。
- `langchain.agents` 的 [`create_agent`](https://docs.langchain.com/oss/python/langchain/agents) 将 LLM 封装成工具调用 Agent 循环。
- LangGraph 管理 `ThreadState`,提供检查点、流式传输和图执行模型。
- **中间件链**包裹 Agent 循环的每一轮,提供记忆、摘要和澄清等跨领域能力。

View File

@ -95,7 +95,7 @@ subagents:
## ACP Agent外部 Agent
除内置子 Agent 外DeerFlow 还通过 **Agent Connect Protocol (ACP)** 支持委派给外部 Agent。ACP 允许 DeerFlow 调用作为独立进程运行的 Agent包括用 ACP 适配器包装的第三方 CLI 工具)。
除内置子 Agent 外DeerFlow 还通过 **Agent Client Protocol (ACP)** 支持委派给外部 Agent。ACP 允许 DeerFlow 调用作为独立进程运行的 Agent包括用 ACP 适配器包装的第三方 CLI 工具)。
在 `config.yaml` 中配置 ACP Agent

View File

@ -14,7 +14,7 @@ tags:
这类半截工具调用不应被当成正常意图执行。一个被截断的 `write_file` 可能写出不完整的报告;一个被截断的 `bash` 调用可能带着残缺参数进入沙箱Agent 看到失败结果后还可能继续重试,反复触发同一条安全规则。
[PR #3035](https://github.com/bytedance/deer-flow/pull/3035) 处理的就是这个边界:当 provider 用安全信号中止生成同时响应仍带有工具调用时DeerFlow 应先压制这些工具调用,再把这一轮作为安全中止事件记录下来。
[对应的实现提交](https://github.com/bytedance/deer-flow/commit/be0eae9825619b63ca0c67b253d40b5eee76a2d6) 处理的就是这个边界:当 provider 用安全信号中止生成同时响应仍带有工具调用时DeerFlow 应先压制这些工具调用,再把这一轮作为安全中止事件记录下来。
## 为什么需要单独处理安全中止

View File

@ -51,4 +51,33 @@ test.describe("Localized documentation links", () => {
);
await expect(page.locator("main h1")).toContainText("Agents and Threads");
});
test("excludes non-documentation app routes from the docs navigation", async ({
page,
}) => {
await page.goto("/en/docs");
const invalidDocsLinks = page.locator(
["auth", "blog", "login", "setup", "workspace"]
.map((root) => `a[href^="/en/docs/${root}"]`)
.join(", "),
);
await expect(invalidDocsLinks).toHaveCount(0);
});
test("uses valid repository links for documentation feedback and edits", async ({
page,
}) => {
await page.goto("/en/docs/application/quick-start");
await expect(
page.getByRole("link", { name: "Question? Give us feedback" }),
).toHaveAttribute("href", /github\.com\/bytedance\/deer-flow\/issues\/new/);
await expect(
page.getByRole("link", { name: "Edit this page" }),
).toHaveAttribute(
"href",
"https://github.com/bytedance/deer-flow/tree/main/frontend/src/content/en/application/quick-start.mdx",
);
});
});

View File

@ -0,0 +1,99 @@
import { readdirSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "@rstest/core";
import type { PageMapItem } from "nextra";
import {
buildLocalizedDocsPageMap,
DOCS_CONTENT_ROOTS,
} from "@/components/docs/docs-page-map";
describe("buildLocalizedDocsPageMap", () => {
it.each(["en", "zh"])(
"keeps the %s content roots in the docs page map",
(lang) => {
const contentRoots = readdirSync(
join(process.cwd(), "src/content", lang),
{
withFileTypes: true,
},
)
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name)
.sort();
expect(contentRoots).toEqual([...DOCS_CONTENT_ROOTS].sort());
},
);
it("localizes content routes and removes unrelated App Router pages", () => {
const source: PageMapItem[] = [
{ name: "index", route: "/" },
{
name: "application",
route: "/application",
children: [{ name: "quick-start", route: "/application/quick-start" }],
},
{
name: "posts",
route: "/posts",
children: [{ name: "weekly", route: "/posts/weekly/2026-04-06" }],
},
{
name: "workspace",
route: "/workspace",
children: [
{ name: "chats", route: "/workspace/chats" },
{
name: "scheduled-tasks",
route: "/workspace/scheduled-tasks",
},
],
},
{
name: "blog",
route: "/blog",
children: [{ name: "posts", route: "/blog/posts" }],
},
{ name: "login", route: "/login" },
{ name: "callback", route: "/auth/callback" },
];
expect(buildLocalizedDocsPageMap("/en/docs", source)).toEqual([
{ name: "index", route: "/en/docs/" },
{
name: "application",
route: "/en/docs/application",
children: [
{
name: "quick-start",
route: "/en/docs/application/quick-start",
},
],
},
{
name: "posts",
route: "/en/docs/posts",
children: [
{
name: "weekly",
route: "/en/docs/posts/weekly/2026-04-06",
},
],
},
]);
expect(source[0]).toEqual({ name: "index", route: "/" });
});
it("preserves routes already localized to the requested docs base", () => {
const source: PageMapItem[] = [
{ name: "harness", route: "/zh/docs/harness" },
{ name: "not-docs", route: "/zh/docs-extra" },
];
expect(buildLocalizedDocsPageMap("/zh/docs", source)).toEqual([
{ name: "harness", route: "/zh/docs/harness" },
]);
});
});

View File

@ -0,0 +1,89 @@
import { readFileSync, readdirSync } from "node:fs";
import { join, relative, sep } from "node:path";
import { describe, expect, it } from "@rstest/core";
const CONTENT_ROOT = join(process.cwd(), "src/content");
const DOC_LANGUAGES = ["en", "zh"] as const;
const DOCS_ORIGIN = "https://docs.example";
const LINK_PATTERN =
/(?<!!)\[[^\]]*\]\(\s*(?:<([^>]+)>|([^\s)]+))(?:\s+["'][^"']*["'])?\s*\)|\bhref\s*=\s*(?:\{\s*)?["']([^"']+)["'](?:\s*\})?/g;
const UNLOCALIZED_DOCS_PATH = /^\/docs(?=\/|[?#]|$)/;
function findMdxFiles(directory: string): string[] {
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
const path = join(directory, entry.name);
return entry.isDirectory()
? findMdxFiles(path)
: entry.name.endsWith(".mdx")
? [path]
: [];
});
}
function routeForMdx(path: string, lang: string): string {
const localeRoot = join(CONTENT_ROOT, lang);
const relativePath = relative(localeRoot, path).split(sep).join("/");
const pagePath = relativePath
.replace(/\.mdx$/, "")
.replace(/(?:^|\/)index$/, "");
return `/${lang}/docs${pagePath ? `/${pagePath}` : ""}`;
}
function extractLinks(source: string): Array<{ href: string; line: number }> {
return [...source.matchAll(LINK_PATTERN)].map((match) => ({
href: match[1] ?? match[2] ?? match[3] ?? "",
line: source.slice(0, match.index).split("\n").length,
}));
}
function resolveDocsPath(
href: string,
lang: string,
sourceRoute: string,
): string | undefined {
const localizedHref = UNLOCALIZED_DOCS_PATH.test(href)
? `/${lang}${href}`
: href;
const url = new URL(localizedHref, `${DOCS_ORIGIN}${sourceRoute}`);
if (
url.origin !== DOCS_ORIGIN ||
!/^\/(?:en|zh)\/docs(?:\/|$)/.test(url.pathname)
) {
return undefined;
}
return url.pathname.replace(/\/$/, "");
}
describe("documentation content links", () => {
it("only links to documentation routes backed by MDX pages", () => {
const mdxFiles = DOC_LANGUAGES.flatMap((lang) =>
findMdxFiles(join(CONTENT_ROOT, lang)),
);
const routes = new Set(
DOC_LANGUAGES.flatMap((lang) =>
mdxFiles
.filter((path) => path.startsWith(join(CONTENT_ROOT, lang)))
.map((path) => routeForMdx(path, lang)),
),
);
const brokenLinks = DOC_LANGUAGES.flatMap((lang) =>
findMdxFiles(join(CONTENT_ROOT, lang)).flatMap((path) => {
const source = readFileSync(path, "utf8");
const sourceRoute = routeForMdx(path, lang);
return extractLinks(source).flatMap(({ href, line }) => {
const targetRoute = resolveDocsPath(href, lang, sourceRoute);
if (!targetRoute || routes.has(targetRoute)) {
return [];
}
return [
`${relative(CONTENT_ROOT, path).split(sep).join("/")}:${line} -> ${href} (${targetRoute})`,
];
});
}),
);
expect(brokenLinks).toEqual([]);
});
});