feat(docs): add documentation site and demo features

- Introduced a new documentation site for Cursor BYOK using Next.js and Fumadocs.
- Added a product demo page with a corresponding Vite configuration.
- Implemented a demo API to simulate LLM calls and responses.
- Enhanced the Makefile to include new build and development commands for the documentation.
- Updated package.json scripts for building and running the documentation site.
- Created various components and layouts for the documentation structure, including blog and user documentation sections.
- Added styling for the new components and layouts to ensure a cohesive design.
- Included a README and other necessary files for local development and deployment.
This commit is contained in:
leokun
2026-08-27 21:45:28 +08:00
parent ee915ee760
commit c5d578c5b1
70 changed files with 13299 additions and 567 deletions
+29
View File
@@ -0,0 +1,29 @@
# deps
/node_modules
# generated desktop product demo
/public/product-demo
# generated content
.source
# test & build
/coverage
/.next/
/out/
/build
*.tsbuildinfo
# misc
.DS_Store
*.pem
/.pnp
.pnp.js
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# others
.env*.local
.vercel
next-env.d.ts
+9
View File
@@ -0,0 +1,9 @@
<!-- BEGIN:nextjs-agent-rules -->
# This is NOT the Next.js you know
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.
This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
<!-- END:nextjs-agent-rules -->
+1
View File
@@ -0,0 +1 @@
@AGENTS.md
+48
View File
@@ -0,0 +1,48 @@
# cursor-byok 文档站
基于 [Fumadocs](https://fumadocs.dev) 和 Next.js 的独立内容应用。
## 目录
```text
apps/docs/
├── app/
│ ├── (home)/ # Hero 首页与开发者博客
│ └── docs/ # 用户文档布局和页面
├── components/ # MDX 组件
├── content/
│ ├── docs/ # 用户文档
│ └── blog/ # 开发者博客
└── lib/ # 内容源、站点信息与布局配置
```
## 本地开发
先安装依赖:
```bash
npm --prefix apps/docs install
```
从仓库根目录启动:
```bash
make dev-docs
```
浏览器打开 <http://localhost:3000>。
## 修改内容
- 在 `content/docs` 中维护面向使用者的 `.mdx` 文档。
- 在 `content/docs/meta.json` 中维护文档侧边栏标题与页面顺序。
- 在 `content/blog` 中维护开发者文章,文件名以 `YYYY-MM-DD-` 开头用于排序和展示日期。
- 在 `lib/shared.ts` 中维护站点名称、仓库和发布地址。
## 检查与构建
```bash
npm --prefix apps/docs run check
```
生产构建输出由 Next.js 管理。部署环境可通过 `NEXT_PUBLIC_SITE_URL` 设置站点公开地址;未设置时使用 `https://docs.leokun.cn`。
+61
View File
@@ -0,0 +1,61 @@
import Link from 'next/link';
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { ArrowLeft } from 'lucide-react';
import { InlineTOC } from 'fumadocs-ui/components/inline-toc';
import { createRelativeLink } from 'fumadocs-ui/mdx';
import { getMDXComponents } from '@/components/mdx';
import { blogSource, formatBlogDate } from '@/lib/blog';
export default async function BlogPostPage(props: PageProps<'/blog/[slug]'>) {
const { slug } = await props.params;
const page = blogSource.getPage([slug]);
if (!page) notFound();
const MDX = page.data.body;
return (
<main className="mx-auto w-full max-w-3xl px-6 py-12 sm:py-20">
<article>
<Link
href="/blog"
className="mb-10 inline-flex items-center gap-2 text-sm text-fd-muted-foreground transition-colors hover:text-fd-foreground"
>
<ArrowLeft className="size-4" />
返回开发者博客
</Link>
<header className="border-b pb-8">
<time className="text-sm text-fd-muted-foreground">{formatBlogDate(page.path)}</time>
<h1 className="mt-4 text-3xl font-bold tracking-tight sm:text-4xl">{page.data.title}</h1>
<p className="mt-4 text-lg leading-8 text-fd-muted-foreground">{page.data.description}</p>
<p className="mt-5 text-sm font-medium">cursor-byok 开发团队</p>
</header>
<div className="prose mt-10 min-w-0">
<InlineTOC items={page.data.toc} />
<MDX
components={getMDXComponents({
a: createRelativeLink(blogSource, page),
})}
/>
</div>
</article>
</main>
);
}
export function generateStaticParams() {
return blogSource.getPages().map((page) => ({ slug: page.slugs[0] }));
}
export async function generateMetadata(props: PageProps<'/blog/[slug]'>): Promise<Metadata> {
const { slug } = await props.params;
const page = blogSource.getPage([slug]);
if (!page) notFound();
return {
title: page.data.title,
description: page.data.description,
};
}
+44
View File
@@ -0,0 +1,44 @@
import Link from 'next/link';
import type { Metadata } from 'next';
import { ArrowRight } from 'lucide-react';
import { blogSource, formatBlogDate, sortBlogPages } from '@/lib/blog';
export const metadata: Metadata = {
title: '开发者博客',
description: 'cursor-byok 的架构决策、协议实现与开发进展。',
};
export default function BlogPage() {
const posts = sortBlogPages(blogSource.getPages());
return (
<main className="mx-auto w-full max-w-5xl px-6 py-16 sm:py-24">
<div className="max-w-2xl">
<p className="font-mono text-sm font-medium text-fd-primary">DEVELOPER BLOG</p>
<h1 className="mt-4 text-4xl font-bold tracking-tight">开发者博客</h1>
<p className="mt-4 text-lg leading-8 text-fd-muted-foreground">
记录 cursor-byok 的架构决策、协议实现和开发进展。
</p>
</div>
<div className="mt-12 divide-y border-y">
{posts.map((post) => (
<Link
key={post.url}
href={post.url}
className="group grid gap-3 py-7 transition-colors hover:text-fd-primary sm:grid-cols-[10rem_1fr_auto] sm:items-center"
>
<time className="text-sm text-fd-muted-foreground">{formatBlogDate(post.path)}</time>
<div>
<h2 className="font-semibold">{post.data.title}</h2>
<p className="mt-1 text-sm leading-6 text-fd-muted-foreground">
{post.data.description}
</p>
</div>
<ArrowRight className="hidden size-4 transition-transform group-hover:translate-x-1 sm:block" />
</Link>
))}
</div>
</main>
);
}
+6
View File
@@ -0,0 +1,6 @@
import { HomeLayout } from 'fumadocs-ui/layouts/home';
import { baseOptions } from '@/lib/layout.shared';
export default function Layout({ children }: LayoutProps<'/'>) {
return <HomeLayout {...baseOptions()}>{children}</HomeLayout>;
}
+135
View File
@@ -0,0 +1,135 @@
import Link from 'next/link';
import {
ArrowRight,
BookOpen,
Download,
Settings2,
Wrench,
} from 'lucide-react';
import { DesktopDemo } from '@/components/hero/DesktopDemo';
import { blogSource, formatBlogDate, sortBlogPages } from '@/lib/blog';
import { appDescription, releaseUrl } from '@/lib/shared';
const docs = [
{
icon: BookOpen,
title: '快速开始',
description: '完成安装、初始化和第一次模型调用。',
href: '/docs',
},
{
icon: Settings2,
title: '模型配置',
description: '配置协议、服务地址、凭据和生成参数。',
href: '/docs/model-configuration',
},
{
icon: Wrench,
title: '故障排查',
description: '解决证书、连接和模型测试问题。',
href: '/docs/troubleshooting',
},
];
export default function HomePage() {
const posts = sortBlogPages(blogSource.getPages()).slice(0, 3);
return (
<main className="flex flex-1 flex-col">
<section className="border-b px-4 pb-16 pt-20 sm:px-6 sm:pb-24 sm:pt-28">
<div className="mx-auto max-w-6xl">
<div className="mx-auto max-w-3xl text-center">
<p className="font-mono text-sm font-medium text-fd-primary">开源 · 本地运行 · 自由接入</p>
<h1 className="mt-5 text-4xl font-bold tracking-tight sm:text-6xl">
{appDescription}
</h1>
<p className="mx-auto mt-6 max-w-2xl text-lg leading-8 text-fd-muted-foreground">
在本机运行自己的模型网关,接入 OpenAI、Anthropic 等兼容服务,继续使用 Cursor Agent 的工具调用、Skills 和 MCP。
</p>
<div className="mt-10 flex flex-wrap justify-center gap-3">
<Link
href="/docs"
className="inline-flex items-center gap-2 rounded-lg bg-fd-primary px-5 py-3 font-medium text-fd-primary-foreground transition-opacity hover:opacity-90"
>
阅读文档
<ArrowRight className="size-4" />
</Link>
<a
href={releaseUrl}
className="inline-flex items-center gap-2 rounded-lg border bg-fd-card px-5 py-3 font-medium transition-colors hover:bg-fd-accent"
>
<Download className="size-4" />
下载最新版
</a>
</div>
</div>
<DesktopDemo />
</div>
</section>
<section className="border-b px-6 py-16 sm:py-20">
<div className="mx-auto max-w-5xl">
<div className="flex items-end justify-between gap-6">
<div>
<p className="font-mono text-sm font-medium text-fd-primary">DOCUMENTATION</p>
<h2 className="mt-3 text-3xl font-bold tracking-tight">文档</h2>
</div>
<Link href="/docs" className="hidden items-center gap-2 text-sm font-medium sm:flex">
查看全部
<ArrowRight className="size-4" />
</Link>
</div>
<div className="mt-10 grid gap-4 md:grid-cols-3">
{docs.map(({ icon: Icon, title, description, href }) => (
<Link
key={href}
href={href}
className="group rounded-xl border bg-fd-card p-6 transition-colors hover:bg-fd-accent"
>
<Icon className="mb-5 size-5 text-fd-primary" />
<h3 className="flex items-center justify-between font-semibold">
{title}
<ArrowRight className="size-4 transition-transform group-hover:translate-x-1" />
</h3>
<p className="mt-2 text-sm leading-6 text-fd-muted-foreground">{description}</p>
</Link>
))}
</div>
</div>
</section>
<section className="px-6 py-16 sm:py-20">
<div className="mx-auto max-w-5xl">
<div className="flex items-end justify-between gap-6">
<div>
<p className="font-mono text-sm font-medium text-fd-primary">DEVELOPER BLOG</p>
<h2 className="mt-3 text-3xl font-bold tracking-tight">开发者博客</h2>
</div>
<Link href="/blog" className="flex items-center gap-2 text-sm font-medium">
查看全部
<ArrowRight className="size-4" />
</Link>
</div>
<div className="mt-10 divide-y border-y">
{posts.map((post) => (
<Link
key={post.url}
href={post.url}
className="group grid gap-3 py-6 transition-colors hover:text-fd-primary sm:grid-cols-[10rem_1fr_auto] sm:items-center"
>
<time className="text-sm text-fd-muted-foreground">{formatBlogDate(post.path)}</time>
<div>
<h3 className="font-semibold">{post.data.title}</h3>
<p className="mt-1 text-sm text-fd-muted-foreground">{post.data.description}</p>
</div>
<ArrowRight className="hidden size-4 transition-transform group-hover:translate-x-1 sm:block" />
</Link>
))}
</div>
</div>
</section>
</main>
);
}
+4
View File
@@ -0,0 +1,4 @@
import { source } from '@/lib/source';
import { createFromSource } from 'fumadocs-core/search/server';
export const { GET } = createFromSource(source);
+63
View File
@@ -0,0 +1,63 @@
import { getPageImageUrl, getPageMarkdownUrl, source } from '@/lib/source';
import {
DocsBody,
DocsDescription,
DocsPage,
DocsTitle,
MarkdownCopyButton,
ViewOptionsPopover,
} from 'fumadocs-ui/layouts/docs/page';
import { notFound } from 'next/navigation';
import { getMDXComponents } from '@/components/mdx';
import type { Metadata } from 'next';
import { createRelativeLink } from 'fumadocs-ui/mdx';
import { gitConfig } from '@/lib/shared';
export default async function Page(props: PageProps<'/docs/[[...slug]]'>) {
const params = await props.params;
const page = source.getPage(params.slug);
if (!page) notFound();
const MDX = page.data.body;
const markdownUrl = getPageMarkdownUrl(page).url;
return (
<DocsPage toc={page.data.toc} full={page.data.full}>
<DocsTitle>{page.data.title}</DocsTitle>
<DocsDescription className="mb-0">{page.data.description}</DocsDescription>
<div className="flex flex-row gap-2 items-center border-b pb-6">
<MarkdownCopyButton markdownUrl={markdownUrl} />
<ViewOptionsPopover
markdownUrl={markdownUrl}
githubUrl={`https://github.com/${gitConfig.user}/${gitConfig.repo}/blob/${gitConfig.branch}/content/docs/${page.path}`}
/>
</div>
<DocsBody>
<MDX
components={getMDXComponents({
// this allows you to link to other pages with relative file paths
a: createRelativeLink(source, page),
})}
/>
</DocsBody>
</DocsPage>
);
}
export async function generateStaticParams() {
return source.generateParams();
}
export async function generateMetadata(props: PageProps<'/docs/[[...slug]]'>): Promise<Metadata> {
const params = await props.params;
const page = source.getPage(params.slug);
if (!page) notFound();
return {
title: page.data.title,
description: page.data.description,
openGraph: {
images: getPageImageUrl(page).url,
},
};
}
+11
View File
@@ -0,0 +1,11 @@
import { source } from '@/lib/source';
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
import { baseOptions } from '@/lib/layout.shared';
export default function Layout({ children }: LayoutProps<'/docs'>) {
return (
<DocsLayout tree={source.getPageTree()} {...baseOptions()}>
{children}
</DocsLayout>
);
}
+16
View File
@@ -0,0 +1,16 @@
@import 'tailwindcss';
@import 'fumadocs-ui/css/neutral.css';
@import 'fumadocs-ui/css/preset.css';
:root {
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
html {
scrollbar-gutter: stable;
}
html > body[data-scroll-locked] {
margin-right: 0 !important;
--removed-body-scroll-bar-size: 0px !important;
}
+26
View File
@@ -0,0 +1,26 @@
import { RootProvider } from 'fumadocs-ui/provider/next';
import { i18nProvider } from 'fumadocs-ui/i18n';
import type { Metadata } from 'next';
import { translations } from '@/lib/layout.shared';
import './global.css';
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL ?? 'https://docs.leokun.cn';
export const metadata: Metadata = {
metadataBase: new URL(siteUrl),
title: {
default: 'cursor-byok 文档',
template: '%s | cursor-byok',
},
description: 'cursor-byok 的安装、模型配置与故障排查指南。',
};
export default function Layout({ children }: LayoutProps<'/'>) {
return (
<html lang="zh-CN" suppressHydrationWarning>
<body className="flex min-h-screen flex-col">
<RootProvider i18n={i18nProvider(translations)}>{children}</RootProvider>
</body>
</html>
);
}
+10
View File
@@ -0,0 +1,10 @@
import { getLLMText, source } from '@/lib/source';
export const revalidate = false;
export async function GET() {
const scan = source.getPages().map(getLLMText);
const scanned = await Promise.all(scan);
return new Response(scanned.join('\n\n'));
}
@@ -0,0 +1,23 @@
import { getLLMText, getPageMarkdownUrl, source } from '@/lib/source';
import { notFound } from 'next/navigation';
export const revalidate = false;
export async function GET(_req: Request, { params }: RouteContext<'/llms.mdx/docs/[[...slug]]'>) {
const { slug } = await params;
const page = source.getPage(slug?.slice(0, -1));
if (!page) notFound();
return new Response(await getLLMText(page), {
headers: {
'Content-Type': 'text/markdown',
},
});
}
export function generateStaticParams() {
return source.getPages().map((page) => ({
lang: page.locale,
slug: getPageMarkdownUrl(page).segments,
}));
}
+8
View File
@@ -0,0 +1,8 @@
import { source } from '@/lib/source';
import { llms } from 'fumadocs-core/source';
export const revalidate = false;
export function GET() {
return new Response(llms(source).index());
}
+28
View File
@@ -0,0 +1,28 @@
import { getPageImageUrl, source } from '@/lib/source';
import { notFound } from 'next/navigation';
import { ImageResponse } from 'next/og';
import { generate as DefaultImage } from 'fumadocs-ui/og';
import { appName } from '@/lib/shared';
export const revalidate = false;
export async function GET(_req: Request, { params }: RouteContext<'/og/docs/[...slug]'>) {
const { slug } = await params;
const page = source.getPage(slug.slice(0, -1));
if (!page) notFound();
return new ImageResponse(
<DefaultImage title={page.data.title} description={page.data.description} site={appName} />,
{
width: 1200,
height: 630,
},
);
}
export function generateStaticParams() {
return source.getPages().map((page) => ({
lang: page.locale,
slug: getPageImageUrl(page).segments,
}));
}
@@ -0,0 +1,45 @@
.stage {
width: 100%;
max-width: 960px;
margin: 64px auto 0;
}
.viewport {
position: relative;
width: 100%;
aspect-ratio: 960 / 620;
overflow: hidden;
background: #141414;
border: 1px solid rgb(255 255 255 / 10%);
border-radius: 14px;
box-shadow:
0 42px 90px -34px rgb(0 0 0 / 50%),
0 12px 32px -16px rgb(0 0 0 / 42%);
}
.demo {
position: absolute;
inset: 0;
display: block;
width: 100%;
height: 100%;
background: #141414;
border: 0;
}
.stage p {
margin: 14px 0 0;
color: var(--color-fd-muted-foreground);
font-size: 12px;
text-align: center;
}
@media (max-width: 680px) {
.stage {
margin-top: 42px;
}
.viewport {
border-radius: 10px;
}
}
+18
View File
@@ -0,0 +1,18 @@
'use client';
import styles from './DesktopDemo.module.css';
export function DesktopDemo() {
return (
<div className={styles.stage}>
<div className={styles.viewport}>
<iframe
title="Cursor BYOK 真实产品界面演示"
src="/product-demo/demo/index.html"
className={styles.demo}
/>
</div>
<p>真实桌面端组件 · 使用隔离的 Mock 数据,可直接操作菜单、筛选和设置</p>
</div>
);
}
+18
View File
@@ -0,0 +1,18 @@
import defaultMdxComponents from 'fumadocs-ui/mdx';
import { Step, Steps } from 'fumadocs-ui/components/steps';
import type { MDXComponents } from 'mdx/types';
export function getMDXComponents(components?: MDXComponents) {
return {
...defaultMdxComponents,
Step,
Steps,
...components,
} satisfies MDXComponents;
}
export const useMDXComponents = getMDXComponents;
declare global {
type MDXProvidedComponents = ReturnType<typeof getMDXComponents>;
}
@@ -0,0 +1,37 @@
---
title: 为什么我们建立新的文档站
description: 将用户指南和开发过程放回代码仓库,让文档与产品一起演进。
---
cursor-byok 的功能已经从单一模型转发扩展到多协议模型配置、工具调用、会话观测和跨平台桌面应用。散落在发布说明与讨论区中的信息,已经不足以支持第一次使用产品的人,也不利于开发者理解系统边界。
## 文档也是产品的一部分
新的文档站与应用代码放在同一个仓库中:
```text
apps/
├── desktop/ # 桌面应用
└── docs/ # 文档站
├── content/docs/
└── content/blog/
```
用户文档负责回答“如何使用”,开发者博客负责记录“为什么这样设计”。两类内容分开维护,但使用同一套构建和审查流程。
## 为什么选择 Fumadocs
Fumadocs 提供了文档布局、全文搜索、代码高亮、目录和 MDX 内容层,我们只需要维护产品信息与视觉样式,不必重新实现通用文档能力。
文档应用保持独立,桌面端不会引入 Next.js 或 Fumadocs 依赖。开发、构建和部署也可以分别进行。
## 接下来会记录什么
开发者博客将持续记录:
- Cursor 协议适配与模型兼容性设计。
- Agent 工具调用和多轮会话的实现取舍。
- 本地存储、可观测性与性能优化。
- 桌面端跨平台开发和发布过程。
这些文章以当前代码为准,不为已经删除的旧实现保留兼容说明。
+64
View File
@@ -0,0 +1,64 @@
---
title: 快速开始
description: 安装 cursor-byok,并让 Cursor 使用你自己的模型 API。
icon: Rocket
---
cursor-byok 是运行在本机的 Cursor 模型网关。它接收 Cursor Agent 请求,将请求转换后发送到你配置的 OpenAI 或 Anthropic 兼容服务。
<Callout type="warn" title="使用前须知">
cursor-byok 是独立开源项目,与 Cursor 及其开发者没有关联。软件本身免费,但模型服务商可能按用量收费。
</Callout>
## 三步开始使用
<Steps>
<Step>
### 下载并启动
从 [GitHub Releases](https://github.com/leookun/cursor-byok/releases/latest) 下载与你的操作系统对应的最新版本,然后启动 cursor-byok。
</Step>
<Step>
### 初始化并配置模型
打开 **Cursor 配置**,按界面提示初始化本地 CA,然后添加模型。填写服务地址、API Key 和模型名称,并运行连通性测试。
</Step>
<Step>
### 在 Cursor 中使用
保持 cursor-byok 运行,打开 Cursor,在模型列表中选择刚刚配置的模型,然后开始使用 Agent。
</Step>
</Steps>
## 接下来
<Cards>
<Card title="安装指南" description="下载、初始化和首次运行。" href="/docs/installation" />
<Card title="模型配置" description="选择协议并填写上游模型参数。" href="/docs/model-configuration" />
<Card title="故障排查" description="处理证书、连接与模型测试问题。" href="/docs/troubleshooting" />
<Card title="查看源码" description="了解实现或参与项目开发。" href="https://github.com/leookun/cursor-byok" external />
</Cards>
## 数据如何流转
```text
Cursor 客户端
│ Agent 请求与工具结果
▼
cursor-byok 本地服务
│ OpenAI / Anthropic 兼容请求
▼
你配置的模型 API
```
API Key、模型配置和应用设置保存在本机。模型请求仍会发送到你选择的上游服务商。
+68
View File
@@ -0,0 +1,68 @@
---
title: 安装指南
description: 下载 cursor-byok,完成本地初始化并验证运行状态。
icon: Download
---
## 下载应用
前往 [最新版本页面](https://github.com/leookun/cursor-byok/releases/latest),下载适用于 macOS、Windows 或 Linux 的安装包。
<Callout type="info">
优先使用最新正式版本。发行页面会列出该版本包含的安装包和更新说明。
</Callout>
## 首次启动
<Steps>
<Step>
### 打开 Cursor 配置
启动应用后进入 **Cursor 配置**。如果本地 CA 尚未初始化,页面会显示初始化入口。
</Step>
<Step>
### 初始化本地 CA
点击 **初始化 CA**。本地 CA 用于在你的设备上解析 Cursor 发出的 HTTPS 请求,其文件只保存在本机。
系统要求授权时,按照应用显示的说明在终端中完成信任操作,然后返回应用点击 **我已初始化,刷新**。
</Step>
<Step>
### 添加第一个模型
点击 **添加模型**,选择 OpenAI 或 Anthropic 类型,填写上游服务参数并保存。详细字段说明见[模型配置](./model-configuration.mdx)。
</Step>
<Step>
### 运行连通性测试
点击模型的 **测试**。测试成功后,该模型即可出现在 Cursor 的模型列表中。
</Step>
</Steps>
## 验证安装
完成配置后,请确认:
- Cursor 配置页面不再显示 CA 初始化提示。
- 至少一个模型的连通性测试成功。
- cursor-byok 保持运行。
- Cursor 模型列表中能看到配置的显示名称。
如果其中任何一步失败,请前往[故障排查](./troubleshooting.mdx)。
## 更新
在应用设置中检查新版本,或直接访问 [GitHub Releases](https://github.com/leookun/cursor-byok/releases/latest)。更新前无需删除现有模型配置。
+5
View File
@@ -0,0 +1,5 @@
{
"title": "使用指南",
"root": true,
"pages": ["index", "installation", "model-configuration", "troubleshooting"]
}
@@ -0,0 +1,67 @@
---
title: 模型配置
description: 配置模型协议、服务地址、凭据与生成参数。
icon: Settings2
---
每个模型配置都是一个独立的上游通道,可以使用不同服务商、协议、凭据和生成参数。
## 必填字段
### 模型类型
选择上游接口格式:
- **OpenAI**:支持 Responses API 和 Chat Completions API。
- **Anthropic**:支持 Messages API 兼容服务。
### 请求协议
OpenAI 类型需要继续选择 **Responses API** 或 **Chat Completions API**。该选项决定请求和响应格式,不会单独改变你填写的服务地址。
### 服务器地址
可以填写服务商的基础地址,也可以选择使用完整请求 URL:
- 使用基础地址时,cursor-byok 会根据协议追加标准端点路径。
- 使用完整请求 URL 时,cursor-byok 会原样使用该地址。
优先使用界面中的常用服务商预设,减少协议与端点不匹配的情况。
### API Key
填写上游服务要求的访问密钥。密钥保存在本机,用于发送模型请求。
### 模型名称
填写服务商接口接受的模型标识。你也可以点击 **获取模型** 读取接口返回的模型列表。
## 展示信息
- **显示名称**:Cursor 模型列表中看到的名称,不会改变发送给上游的模型标识。
- **备注**:显示在 Cursor 的模型说明中。
## 可选参数
按模型能力填写以下字段;留空时使用应用或上游的默认值:
- 上下文窗口 Token
- 最大输出 Token
- 推理强度或思考强度
- 自定义 Headers
- OpenAI 或 Anthropic 额外参数
<Callout type="warn" title="额外参数格式">
自定义 Headers 和额外参数必须是 JSON 对象。额外参数会直接影响上游请求,只添加服务商明确支持的字段。
</Callout>
## 测试配置
保存后运行 **测试**。结果会显示首字延迟、生成速度、总耗时和模型输出,便于确认:
1. 地址和协议是否匹配。
2. API Key 是否有效。
3. 模型标识是否存在且可访问。
4. 上游是否能正常返回流式内容。
测试通过后再前往 Cursor 使用该模型。
@@ -0,0 +1,62 @@
---
title: 故障排查
description: 处理本地 CA、管理服务、模型连接和 Cursor 模型列表问题。
icon: Wrench
---
## 需要先初始化本地 CA
进入 **Cursor 配置**,点击 **初始化 CA**。本地 CA 只保存在本机,用于解析 Cursor 发出的 HTTPS 请求。
## 需要在系统中信任本地 CA
点击 **打开终端安装 CA**,按照终端提示完成系统授权。完成后返回应用,点击 **我已初始化,刷新**。
如果状态没有更新,请完全退出后重新启动 cursor-byok,再次打开 Cursor 配置页面。
## 无法连接本地管理服务
1. 完全退出并重新启动 cursor-byok。
2. 检查是否有安全软件拦截本地回环连接。
3. 如果修改过管理服务端口,将端口恢复为 `0`,让应用在启动时自动选择可用端口。
4. 再次启动应用并刷新页面。
## 模型连通性测试失败
按测试错误逐项检查:
- **认证错误**:确认 API Key 有效,且账户有权访问目标模型。
- **未找到接口**:确认模型类型、请求协议与服务器地址匹配。
- **模型不存在**:使用 **获取模型** 检查上游返回的模型标识。
- **参数错误**:暂时关闭自定义 Headers 和额外参数,再重新测试。
- **连接超时**:检查网络、系统代理和上游服务状态。
## Cursor 中看不到模型
确认以下条件全部满足:
1. 本地 CA 状态正常。
2. 已保存至少一个模型配置。
3. 模型连通性测试成功。
4. cursor-byok 正在运行。
5. 重启 Cursor 后重新打开模型列表。
## 请求失败但测试成功
模型测试只验证基础连接。Agent 请求还会包含更长上下文、工具定义和流式响应。请检查:
- 上游模型是否支持工具调用。
- 上下文窗口和最大输出 Token 是否符合模型限制。
- 自定义参数是否与实际协议兼容。
- 调用详情中的上游状态码和响应内容。
## 继续反馈
如果问题仍然存在,请在 [GitHub Issues](https://github.com/leookun/cursor-byok/issues) 提交问题,并附上:
- 操作系统与 cursor-byok 版本。
- 选择的模型类型和请求协议。
- 已脱敏的服务地址与错误信息。
- 复现步骤。
请勿公开 API Key 或其他凭据。
+16
View File
@@ -0,0 +1,16 @@
import { defineConfig, globalIgnores } from 'eslint/config';
import nextVitals from 'eslint-config-next/core-web-vitals';
const eslintConfig = defineConfig([
...nextVitals,
globalIgnores([
'.next/**',
'out/**',
'build/**',
'next-env.d.ts',
'.source/**',
'public/product-demo/**',
]),
]);
export default eslintConfig;
+35
View File
@@ -0,0 +1,35 @@
import { loader } from 'fumadocs-core/source';
import { pageSchema } from 'fumadocs-core/source/schema';
import { defineCollections } from 'fumadocs-mdx/macro';
const blog = defineCollections({
type: 'doc',
dir: 'content/blog',
schema: pageSchema,
});
export const blogSource = loader({
baseUrl: '/blog',
source: blog.toFumadocsSource(),
});
export function getBlogDate(path: string): Date {
const fileName = path.split('/').at(-1) ?? '';
const match = /^(\d{4}-\d{2}-\d{2})/.exec(fileName);
return match ? new Date(`${match[1]}T00:00:00Z`) : new Date(0);
}
export function formatBlogDate(path: string): string {
return new Intl.DateTimeFormat('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric',
timeZone: 'UTC',
}).format(getBlogDate(path));
}
export function sortBlogPages<T extends { path: string }>(pages: T[]): T[] {
return [...pages].sort((left, right) => {
return getBlogDate(right.path).getTime() - getBlogDate(left.path).getTime();
});
}
+1
View File
@@ -0,0 +1 @@
export { cn } from 'cnfast';
+31
View File
@@ -0,0 +1,31 @@
import { zhCN } from '@fumadocs/language/zh-cn';
import { defineTranslations } from 'fumadocs-core/i18n';
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
import { uiTranslations } from 'fumadocs-ui/i18n';
import { appName, gitConfig, releaseUrl } from './shared';
export const translations = defineTranslations().extend(uiTranslations()).preset(zhCN());
export function baseOptions(): BaseLayoutProps {
return {
nav: {
title: appName,
},
links: [
{
text: '文档',
url: '/docs',
},
{
text: '开发者博客',
url: '/blog',
},
{
text: '下载',
url: releaseUrl,
external: true,
},
],
githubUrl: `https://github.com/${gitConfig.user}/${gitConfig.repo}`,
};
}
+13
View File
@@ -0,0 +1,13 @@
export const appName = 'cursor-byok';
export const appDescription = 'Cursor 服务端的开源替代';
export const docsRoute = '/docs';
export const docsImageRoute = '/og/docs';
export const docsContentRoute = '/llms.mdx/docs';
export const repositoryUrl = 'https://github.com/leookun/cursor-byok';
export const releaseUrl = `${repositoryUrl}/releases/latest`;
export const gitConfig = {
user: 'leookun',
repo: 'cursor-byok',
branch: 'main',
};
+51
View File
@@ -0,0 +1,51 @@
import { loader } from 'fumadocs-core/source';
import { lucideIconsPlugin } from 'fumadocs-core/source/lucide-icons';
import { docsContentRoute, docsImageRoute, docsRoute } from './shared';
import { defineDocs } from 'fumadocs-mdx/macro';
import { metaSchema, pageSchema } from 'fumadocs-core/source/schema';
const docs = defineDocs({
dir: 'content/docs',
docs: {
schema: pageSchema,
postprocess: {
includeProcessedMarkdown: true,
},
},
meta: {
schema: metaSchema,
},
});
// See https://fumadocs.dev/docs/headless/source-api for more info
export const source = loader({
baseUrl: docsRoute,
source: docs.toFumadocsSource(),
plugins: [lucideIconsPlugin()],
});
export function getPageImageUrl(page: (typeof source)['$inferPage']) {
const segments = [...page.slugs, 'image.png'];
return {
segments,
url: '/' + [page.locale, ...docsImageRoute.split('/'), ...segments].filter(Boolean).join('/'),
};
}
export function getPageMarkdownUrl(page: (typeof source)['$inferPage']) {
const segments = [...page.slugs, 'content.md'];
return {
segments,
url: '/' + [page.locale, ...docsContentRoute.split('/'), ...segments].filter(Boolean).join('/'),
};
}
export async function getLLMText(page: (typeof source)['$inferPage']) {
const processed = await page.data.getText('processed');
return `# ${page.data.title} (${page.url})
${processed}`;
}
+10
View File
@@ -0,0 +1,10 @@
import { createMDX } from 'fumadocs-mdx/next';
const withMDX = createMDX();
/** @type {import('next').NextConfig} */
const config = {
reactStrictMode: true,
};
export default withMDX(config);
+10518
View File
File diff suppressed because it is too large Load Diff
+39
View File
@@ -0,0 +1,39 @@
{
"name": "cursor-byok-docs",
"version": "0.1.0",
"private": true,
"scripts": {
"build:product-demo": "node scripts/build-product-demo.mjs",
"prebuild": "npm run build:product-demo",
"build": "next build",
"predev": "npm run build:product-demo",
"dev": "next dev",
"start": "next start",
"types:check": "next typegen && tsc --noEmit",
"lint": "eslint",
"check": "npm run types:check && npm run lint && npm run build"
},
"dependencies": {
"@fumadocs/language": "^0.2.4",
"cnfast": "^0.1.0",
"fumadocs-core": "16.15.2",
"fumadocs-mdx": "15.3.1",
"fumadocs-ui": "npm:@fumadocs/base-ui@16.15.2",
"lucide-react": "^1.34.0",
"next": "16.3.2",
"react": "^19.2.8",
"react-dom": "^19.2.8"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.3.3",
"@types/mdx": "^2.0.14",
"@types/node": "^26.3.0",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.5",
"eslint": "^9.39.4",
"eslint-config-next": "16.3.2",
"postcss": "^8.5.26",
"tailwindcss": "^4.3.3",
"typescript": "^6.0.3"
}
}
+7
View File
@@ -0,0 +1,7 @@
const config = {
plugins: {
'@tailwindcss/postcss': {},
},
};
export default config;
+32
View File
@@ -0,0 +1,32 @@
import { NextRequest, NextResponse } from 'next/server';
import { isMarkdownPreferred, rewritePath } from 'fumadocs-core/negotiation';
import { docsContentRoute, docsRoute } from '@/lib/shared';
const { rewrite: rewriteDocs } = rewritePath(
`${docsRoute}{/*path}`,
`${docsContentRoute}{/*path}/content.md`,
);
const { rewrite: rewriteSuffix } = rewritePath(
`${docsRoute}{/*path}.md`,
`${docsContentRoute}{/*path}/content.md`,
);
export default function proxy(request: NextRequest) {
const result = rewriteSuffix(request.nextUrl.pathname);
if (result) {
return NextResponse.rewrite(new URL(result, request.nextUrl));
}
if (isMarkdownPreferred(request)) {
const result = rewriteDocs(request.nextUrl.pathname);
if (result) {
return NextResponse.rewrite(new URL(result, request.nextUrl), {
// this URL has two representations, selected by `Accept`
headers: { Vary: 'Accept' },
});
}
}
return NextResponse.next();
}
+18
View File
@@ -0,0 +1,18 @@
import { existsSync } from 'node:fs';
import { spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import path from 'node:path';
const docsRoot = path.dirname(fileURLToPath(new URL('../package.json', import.meta.url)));
const desktopRoot = path.resolve(docsRoot, '../desktop');
const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
const vite = path.join(desktopRoot, 'node_modules', '.bin', process.platform === 'win32' ? 'vite.cmd' : 'vite');
if (!existsSync(vite)) run(['ci'], desktopRoot);
run(['run', 'build:demo'], desktopRoot);
function run(args, cwd) {
const result = spawnSync(npm, args, { cwd, stdio: 'inherit' });
if (result.error) throw result.error;
if (result.status !== 0) process.exit(result.status ?? 1);
}
+34
View File
@@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "ESNext",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"paths": {
"@/*": ["./*"]
},
"plugins": [
{
"name": "next"
}
]
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": ["node_modules"]
}