mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-07-27 00:17:53 +00:00
fix(docs): repair broken documentation links (#4330)
* fix(docs): repair broken documentation links * revert(docs): restore ACP protocol naming
This commit is contained in:
parent
3c0a45ad77
commit
cd86275638
@ -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
|
||||
|
||||
@ -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}
|
||||
|
||||
49
frontend/src/components/docs/docs-page-map.ts
Normal file
49
frontend/src/components/docs/docs-page-map.ts
Normal 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) }
|
||||
: {}),
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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`:
|
||||
|
||||
|
||||
@ -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).
|
||||
|
||||
@ -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).
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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 循环的每一轮,提供记忆、摘要和澄清等跨领域能力。
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
|
||||
@ -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 应先压制这些工具调用,再把这一轮作为安全中止事件记录下来。
|
||||
|
||||
## 为什么需要单独处理安全中止
|
||||
|
||||
|
||||
@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
99
frontend/tests/unit/components/docs/docs-page-map.test.ts
Normal file
99
frontend/tests/unit/components/docs/docs-page-map.test.ts
Normal 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" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
89
frontend/tests/unit/content/docs-links.test.ts
Normal file
89
frontend/tests/unit/content/docs-links.test.ts
Normal 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([]);
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user