feat(docs): enhance documentation site with internationalization and layout updates
- Updated the demo API to support locale-based content rendering. - Refactored the main layout to accommodate language-specific metadata and routing. - Introduced new components for blog and documentation pages, ensuring proper language handling. - Added styles for improved UI consistency across different sections. - Removed deprecated layout files and streamlined the structure for better maintainability.
@@ -243,8 +243,9 @@ function createSeries(count: number, step: number, end: number): OverviewTokenUs
|
||||
const wave = 0.72 + ((index * 17) % 31) / 50;
|
||||
return {
|
||||
bucket_start_ms: end - (count - index) * step,
|
||||
input_tokens: Math.round(18_000 * wave),
|
||||
cache_read_tokens: Math.round(62_000 * wave),
|
||||
// input : cache_read = 1 : 99,使默认口径缓存命中率恰为 99%
|
||||
input_tokens: Math.round(800 * wave),
|
||||
cache_read_tokens: Math.round(79_200 * wave),
|
||||
cache_write_tokens: Math.round(7_500 * wave),
|
||||
output_tokens: Math.round(12_500 * wave),
|
||||
};
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { installDemoApi } from "./api";
|
||||
|
||||
installDemoApi();
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const locale = params.get("locale") === "en-US" ? "en-US" : "zh-CN";
|
||||
const theme = params.get("theme") === "default-light" ? "default-light" : "default-dark";
|
||||
|
||||
document.documentElement.dataset.platform = "macos";
|
||||
localStorage.setItem("cursor-byok.locale", "zh-CN");
|
||||
localStorage.setItem("cursor-byok.theme", "default-dark");
|
||||
localStorage.setItem("cursor-byok.locale", locale);
|
||||
localStorage.setItem("cursor-byok.theme", theme);
|
||||
|
||||
void import("../index");
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { HomeLayout } from 'fumadocs-ui/layouts/home';
|
||||
import { baseOptions } from '@/lib/layout.shared';
|
||||
|
||||
export default function Layout({ children }: LayoutProps<'/'>) {
|
||||
return <HomeLayout {...baseOptions()}>{children}</HomeLayout>;
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -6,30 +6,42 @@ 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';
|
||||
import { i18n, isLanguage, type Language } from '@/lib/i18n';
|
||||
|
||||
export default async function BlogPostPage(props: PageProps<'/blog/[slug]'>) {
|
||||
const { slug } = await props.params;
|
||||
const page = blogSource.getPage([slug]);
|
||||
const copy: Record<Language, { back: string; team: string }> = {
|
||||
zh: { back: '返回开发者博客', team: 'cursor-byok 开发团队' },
|
||||
en: { back: 'Back to Developer Blog', team: 'The cursor-byok team' },
|
||||
};
|
||||
|
||||
export default async function BlogPostPage(props: PageProps<'/[lang]/blog/[slug]'>) {
|
||||
const { lang, slug } = await props.params;
|
||||
if (!isLanguage(lang)) notFound();
|
||||
|
||||
const page = blogSource.getPage([slug], lang);
|
||||
if (!page) notFound();
|
||||
|
||||
const t = copy[lang];
|
||||
const prefix = lang === i18n.defaultLanguage ? '' : `/${lang}`;
|
||||
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"
|
||||
href={`${prefix}/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" />
|
||||
返回开发者博客
|
||||
{t.back}
|
||||
</Link>
|
||||
|
||||
<header className="border-b pb-8">
|
||||
<time className="text-sm text-fd-muted-foreground">{formatBlogDate(page.path)}</time>
|
||||
<time className="text-sm text-fd-muted-foreground">
|
||||
{formatBlogDate(page.path, lang)}
|
||||
</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>
|
||||
<p className="mt-5 text-sm font-medium">{t.team}</p>
|
||||
</header>
|
||||
|
||||
<div className="prose mt-10 min-w-0">
|
||||
@@ -46,12 +58,18 @@ export default async function BlogPostPage(props: PageProps<'/blog/[slug]'>) {
|
||||
}
|
||||
|
||||
export function generateStaticParams() {
|
||||
return blogSource.getPages().map((page) => ({ slug: page.slugs[0] }));
|
||||
return i18n.languages.flatMap((lang) =>
|
||||
blogSource.getPages(lang).map((page) => ({ lang, slug: page.slugs[0] })),
|
||||
);
|
||||
}
|
||||
|
||||
export async function generateMetadata(props: PageProps<'/blog/[slug]'>): Promise<Metadata> {
|
||||
const { slug } = await props.params;
|
||||
const page = blogSource.getPage([slug]);
|
||||
export async function generateMetadata(
|
||||
props: PageProps<'/[lang]/blog/[slug]'>,
|
||||
): Promise<Metadata> {
|
||||
const { lang, slug } = await props.params;
|
||||
if (!isLanguage(lang)) notFound();
|
||||
|
||||
const page = blogSource.getPage([slug], lang);
|
||||
if (!page) notFound();
|
||||
|
||||
return {
|
||||
@@ -0,0 +1,68 @@
|
||||
import Link from 'next/link';
|
||||
import type { Metadata } from 'next';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import { blogSource, formatBlogDate, sortBlogPages } from '@/lib/blog';
|
||||
import { isLanguage, type Language } from '@/lib/i18n';
|
||||
|
||||
const copy: Record<Language, { title: string; description: string; intro: string }> = {
|
||||
zh: {
|
||||
title: '开发者博客',
|
||||
description: 'cursor-byok 的架构决策、协议实现与开发进展。',
|
||||
intro: '记录 cursor-byok 的架构决策、协议实现和开发进展。',
|
||||
},
|
||||
en: {
|
||||
title: 'Developer Blog',
|
||||
description: 'Architecture decisions, protocol work, and development progress of cursor-byok.',
|
||||
intro: 'Notes on architecture decisions, protocol work, and development progress of cursor-byok.',
|
||||
},
|
||||
};
|
||||
|
||||
export async function generateMetadata(props: PageProps<'/[lang]/blog'>): Promise<Metadata> {
|
||||
const { lang } = await props.params;
|
||||
if (!isLanguage(lang)) notFound();
|
||||
|
||||
return {
|
||||
title: copy[lang].title,
|
||||
description: copy[lang].description,
|
||||
};
|
||||
}
|
||||
|
||||
export default async function BlogPage(props: PageProps<'/[lang]/blog'>) {
|
||||
const { lang } = await props.params;
|
||||
if (!isLanguage(lang)) notFound();
|
||||
|
||||
const t = copy[lang];
|
||||
const posts = sortBlogPages(blogSource.getPages(lang));
|
||||
|
||||
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">{t.title}</h1>
|
||||
<p className="mt-4 text-lg leading-8 text-fd-muted-foreground">{t.intro}</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, lang)}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { HomeLayout } from 'fumadocs-ui/layouts/home';
|
||||
import { baseOptions } from '@/lib/layout.shared';
|
||||
import { isLanguage, i18n } from '@/lib/i18n';
|
||||
|
||||
export default async function Layout({ params, children }: LayoutProps<'/[lang]'>) {
|
||||
const { lang } = await params;
|
||||
const base = baseOptions(isLanguage(lang) ? lang : i18n.defaultLanguage);
|
||||
|
||||
return (
|
||||
<HomeLayout
|
||||
{...base}
|
||||
nav={{ ...base.nav, transparentMode: 'always' }}
|
||||
className="home-fullbleed-nav"
|
||||
>
|
||||
{children}
|
||||
</HomeLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
ArrowRight,
|
||||
BookOpen,
|
||||
Settings2,
|
||||
Star,
|
||||
Wrench,
|
||||
} from 'lucide-react';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { DesktopDemo } from '@/components/hero/DesktopDemo';
|
||||
import { DownloadButton } from '@/components/hero/DownloadButton';
|
||||
import { blogSource, formatBlogDate, sortBlogPages } from '@/lib/blog';
|
||||
import { formatStars, getRepoStats } from '@/lib/github';
|
||||
import { i18n, isLanguage, type Language } from '@/lib/i18n';
|
||||
import { releaseUrl, repositoryUrl } from '@/lib/shared';
|
||||
|
||||
const copy: Record<
|
||||
Language,
|
||||
{
|
||||
pillReleased: (version: string) => string;
|
||||
pillFallback: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
readDocs: string;
|
||||
flow: [string, string, string];
|
||||
docsEyebrow: string;
|
||||
docsTitle: string;
|
||||
viewAll: string;
|
||||
docs: { icon: typeof BookOpen; title: string; description: string; href: string }[];
|
||||
blogEyebrow: string;
|
||||
blogTitle: string;
|
||||
}
|
||||
> = {
|
||||
zh: {
|
||||
pillReleased: (version) => `${version} 已发布 · 开源 · 本地运行`,
|
||||
pillFallback: '开源 · 本地运行 · 自由接入',
|
||||
title: 'Cursor 服务端的开源替代',
|
||||
subtitle:
|
||||
'在本机运行自己的模型网关,用自己的 API Key 接入 OpenAI、Anthropic 兼容服务或自定义端点,完整保留 Cursor Agent 的工具调用、Skills 和 MCP。',
|
||||
readDocs: '阅读文档',
|
||||
flow: ['Cursor 客户端', 'cursor-byok 本地服务', '你的模型 API(OpenAI / Anthropic 兼容)'],
|
||||
docsEyebrow: 'DOCUMENTATION',
|
||||
docsTitle: '文档',
|
||||
viewAll: '查看全部',
|
||||
docs: [
|
||||
{
|
||||
icon: BookOpen,
|
||||
title: '快速开始',
|
||||
description: '完成安装、初始化和第一次模型调用。',
|
||||
href: '/docs',
|
||||
},
|
||||
{
|
||||
icon: Settings2,
|
||||
title: '模型配置',
|
||||
description: '配置协议、服务地址、凭据和生成参数。',
|
||||
href: '/docs/model-configuration',
|
||||
},
|
||||
{
|
||||
icon: Wrench,
|
||||
title: '故障排查',
|
||||
description: '解决证书、连接和模型测试问题。',
|
||||
href: '/docs/troubleshooting',
|
||||
},
|
||||
],
|
||||
blogEyebrow: 'DEVELOPER BLOG',
|
||||
blogTitle: '开发者博客',
|
||||
},
|
||||
en: {
|
||||
pillReleased: (version) => `${version} released · Open source · Runs locally`,
|
||||
pillFallback: 'Open source · Runs locally · Any provider',
|
||||
title: "The open-source alternative to Cursor's backend",
|
||||
subtitle:
|
||||
'Run your own model gateway locally, connect OpenAI- and Anthropic-compatible services or custom endpoints with your own API keys, and keep Cursor Agent tool calling, Skills, and MCP intact.',
|
||||
readDocs: 'Read the docs',
|
||||
flow: ['Cursor client', 'cursor-byok local service', 'Your model API (OpenAI / Anthropic compatible)'],
|
||||
docsEyebrow: 'DOCUMENTATION',
|
||||
docsTitle: 'Documentation',
|
||||
viewAll: 'View all',
|
||||
docs: [
|
||||
{
|
||||
icon: BookOpen,
|
||||
title: 'Quick Start',
|
||||
description: 'Install, initialize, and make your first model call.',
|
||||
href: '/docs',
|
||||
},
|
||||
{
|
||||
icon: Settings2,
|
||||
title: 'Model Configuration',
|
||||
description: 'Configure protocols, server addresses, credentials, and parameters.',
|
||||
href: '/docs/model-configuration',
|
||||
},
|
||||
{
|
||||
icon: Wrench,
|
||||
title: 'Troubleshooting',
|
||||
description: 'Resolve certificate, connection, and model test issues.',
|
||||
href: '/docs/troubleshooting',
|
||||
},
|
||||
],
|
||||
blogEyebrow: 'DEVELOPER BLOG',
|
||||
blogTitle: 'Developer Blog',
|
||||
},
|
||||
};
|
||||
|
||||
export default async function HomePage(props: PageProps<'/[lang]'>) {
|
||||
const { lang } = await props.params;
|
||||
if (!isLanguage(lang)) notFound();
|
||||
|
||||
const t = copy[lang];
|
||||
const prefix = lang === i18n.defaultLanguage ? '' : `/${lang}`;
|
||||
const posts = sortBlogPages(blogSource.getPages(lang)).slice(0, 3);
|
||||
const { stars, version } = await getRepoStats();
|
||||
|
||||
return (
|
||||
<main className="flex flex-1 flex-col">
|
||||
<section className="relative -mt-14 overflow-hidden border-b px-4 pb-16 pt-34 sm:px-6 sm:pb-24 sm:pt-42">
|
||||
<div aria-hidden className="pointer-events-none absolute inset-0 -z-10">
|
||||
<div className="absolute inset-0 bg-[linear-gradient(to_right,var(--color-fd-border)_1px,transparent_1px),linear-gradient(to_bottom,var(--color-fd-border)_1px,transparent_1px)] bg-[size:56px_56px] opacity-40 [mask-image:radial-gradient(ellipse_70%_60%_at_50%_0%,#000_20%,transparent_75%)]" />
|
||||
<div className="absolute left-1/2 top-[-14rem] h-[26rem] w-[44rem] -translate-x-1/2 rounded-full bg-fd-primary/10 blur-3xl" />
|
||||
</div>
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<div className="mx-auto max-w-3xl text-center">
|
||||
<a
|
||||
href={releaseUrl}
|
||||
className="inline-flex items-center gap-2 rounded-full border bg-fd-card px-4 py-1.5 text-sm text-fd-muted-foreground transition-colors hover:text-fd-foreground"
|
||||
>
|
||||
<span className="relative flex size-2">
|
||||
<span className="absolute inline-flex size-full animate-ping rounded-full bg-fd-primary opacity-50" />
|
||||
<span className="relative inline-flex size-2 rounded-full bg-fd-primary" />
|
||||
</span>
|
||||
{version ? t.pillReleased(version) : t.pillFallback}
|
||||
<ArrowRight className="size-3.5" />
|
||||
</a>
|
||||
<h1 className="mt-6 text-4xl font-bold tracking-tight sm:text-6xl">{t.title}</h1>
|
||||
<p className="mx-auto mt-6 max-w-2xl text-lg leading-8 text-fd-muted-foreground">
|
||||
{t.subtitle}
|
||||
</p>
|
||||
<div className="mt-10 flex flex-wrap items-center justify-center gap-3">
|
||||
<DownloadButton lang={lang} />
|
||||
<Link
|
||||
href={`${prefix}/docs`}
|
||||
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"
|
||||
>
|
||||
{t.readDocs}
|
||||
<ArrowRight className="size-4" />
|
||||
</Link>
|
||||
<a
|
||||
href={repositoryUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
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"
|
||||
>
|
||||
<GitHubIcon className="size-4" />
|
||||
GitHub
|
||||
{stars !== null ? (
|
||||
<span className="flex items-center gap-1 border-l pl-2.5 text-sm text-fd-muted-foreground">
|
||||
<Star className="size-3.5" />
|
||||
{formatStars(stars)}
|
||||
</span>
|
||||
) : null}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<DesktopDemo lang={lang} />
|
||||
<div className="mx-auto mt-10 flex flex-wrap items-center justify-center gap-x-3 gap-y-2 font-mono text-xs text-fd-muted-foreground sm:text-sm">
|
||||
<span>{t.flow[0]}</span>
|
||||
<ArrowRight className="size-3.5 shrink-0" />
|
||||
<span className="font-medium text-fd-foreground">{t.flow[1]}</span>
|
||||
<ArrowRight className="size-3.5 shrink-0" />
|
||||
<span>{t.flow[2]}</span>
|
||||
</div>
|
||||
</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">{t.docsEyebrow}</p>
|
||||
<h2 className="mt-3 text-3xl font-bold tracking-tight">{t.docsTitle}</h2>
|
||||
</div>
|
||||
<Link
|
||||
href={`${prefix}/docs`}
|
||||
className="hidden items-center gap-2 text-sm font-medium sm:flex"
|
||||
>
|
||||
{t.viewAll}
|
||||
<ArrowRight className="size-4" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="mt-10 grid gap-4 md:grid-cols-3">
|
||||
{t.docs.map(({ icon: Icon, title, description, href }) => (
|
||||
<Link
|
||||
key={href}
|
||||
href={`${prefix}${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">{t.blogEyebrow}</p>
|
||||
<h2 className="mt-3 text-3xl font-bold tracking-tight">{t.blogTitle}</h2>
|
||||
</div>
|
||||
<Link href={`${prefix}/blog`} className="flex items-center gap-2 text-sm font-medium">
|
||||
{t.viewAll}
|
||||
<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, lang)}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
function GitHubIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden className={className}>
|
||||
<path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0 1 12 6.844a9.59 9.59 0 0 1 2.504.337c1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.02 10.02 0 0 0 22 12.017C22 6.484 17.522 2 12 2Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -13,16 +13,16 @@ 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]]'>) {
|
||||
export default async function Page(props: PageProps<'/[lang]/docs/[[...slug]]'>) {
|
||||
const params = await props.params;
|
||||
const page = source.getPage(params.slug);
|
||||
const page = source.getPage(params.slug, params.lang);
|
||||
if (!page) notFound();
|
||||
|
||||
const MDX = page.data.body;
|
||||
const markdownUrl = getPageMarkdownUrl(page).url;
|
||||
|
||||
return (
|
||||
<DocsPage toc={page.data.toc} full={page.data.full}>
|
||||
<DocsPage toc={page.data.toc} full={page.data.full} tableOfContent={{ style: 'clerk' }}>
|
||||
<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">
|
||||
@@ -48,9 +48,11 @@ export async function generateStaticParams() {
|
||||
return source.generateParams();
|
||||
}
|
||||
|
||||
export async function generateMetadata(props: PageProps<'/docs/[[...slug]]'>): Promise<Metadata> {
|
||||
export async function generateMetadata(
|
||||
props: PageProps<'/[lang]/docs/[[...slug]]'>,
|
||||
): Promise<Metadata> {
|
||||
const params = await props.params;
|
||||
const page = source.getPage(params.slug);
|
||||
const page = source.getPage(params.slug, params.lang);
|
||||
if (!page) notFound();
|
||||
|
||||
return {
|
||||
@@ -0,0 +1,15 @@
|
||||
import { source } from '@/lib/source';
|
||||
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
|
||||
import { baseOptions } from '@/lib/layout.shared';
|
||||
import { i18n, isLanguage } from '@/lib/i18n';
|
||||
|
||||
export default async function Layout({ params, children }: LayoutProps<'/[lang]/docs'>) {
|
||||
const { lang } = await params;
|
||||
const language = isLanguage(lang) ? lang : i18n.defaultLanguage;
|
||||
|
||||
return (
|
||||
<DocsLayout tree={source.getPageTree(language)} {...baseOptions(language)}>
|
||||
{children}
|
||||
</DocsLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { RootProvider } from 'fumadocs-ui/provider/next';
|
||||
import { i18nProvider } from 'fumadocs-ui/i18n';
|
||||
import { notFound } from 'next/navigation';
|
||||
import type { Metadata } from 'next';
|
||||
import { htmlLang, i18n, isLanguage } from '@/lib/i18n';
|
||||
import { translations } from '@/lib/layout.shared';
|
||||
import '../global.css';
|
||||
|
||||
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL ?? 'https://docs.leokun.cn';
|
||||
|
||||
export async function generateMetadata(props: LayoutProps<'/[lang]'>): Promise<Metadata> {
|
||||
const { lang } = await props.params;
|
||||
|
||||
if (lang === 'en') {
|
||||
return {
|
||||
metadataBase: new URL(siteUrl),
|
||||
title: {
|
||||
default: 'cursor-byok Docs',
|
||||
template: '%s | cursor-byok',
|
||||
},
|
||||
description: 'Installation, model configuration, and troubleshooting guide for cursor-byok.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
metadataBase: new URL(siteUrl),
|
||||
title: {
|
||||
default: 'cursor-byok 文档',
|
||||
template: '%s | cursor-byok',
|
||||
},
|
||||
description: 'cursor-byok 的安装、模型配置与故障排查指南。',
|
||||
};
|
||||
}
|
||||
|
||||
export function generateStaticParams() {
|
||||
return i18n.languages.map((lang) => ({ lang }));
|
||||
}
|
||||
|
||||
export default async function Layout({ params, children }: LayoutProps<'/[lang]'>) {
|
||||
const { lang } = await params;
|
||||
if (!isLanguage(lang)) notFound();
|
||||
|
||||
return (
|
||||
<html lang={htmlLang[lang]} suppressHydrationWarning>
|
||||
<body className="flex min-h-screen flex-col">
|
||||
<RootProvider i18n={i18nProvider(translations, lang)}>{children}</RootProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { getLLMText, getPageMarkdownUrl, source } from '@/lib/source';
|
||||
import { i18n } from '@/lib/i18n';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
export const revalidate = false;
|
||||
|
||||
export async function GET(
|
||||
_req: Request,
|
||||
{ params }: RouteContext<'/[lang]/llms.mdx/docs/[[...slug]]'>,
|
||||
) {
|
||||
const { lang, slug } = await params;
|
||||
const page = source.getPage(slug?.slice(0, -1), lang);
|
||||
if (!page) notFound();
|
||||
|
||||
return new Response(await getLLMText(page), {
|
||||
headers: {
|
||||
'Content-Type': 'text/markdown',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function generateStaticParams() {
|
||||
return i18n.languages.flatMap((lang) =>
|
||||
source.getPages(lang).map((page) => ({
|
||||
lang,
|
||||
slug: getPageMarkdownUrl(page).segments,
|
||||
})),
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getPageImageUrl, source } from '@/lib/source';
|
||||
import { i18n } from '@/lib/i18n';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { ImageResponse } from 'next/og';
|
||||
import { generate as DefaultImage } from 'fumadocs-ui/og';
|
||||
@@ -6,9 +7,9 @@ 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));
|
||||
export async function GET(_req: Request, { params }: RouteContext<'/[lang]/og/docs/[...slug]'>) {
|
||||
const { lang, slug } = await params;
|
||||
const page = source.getPage(slug.slice(0, -1), lang);
|
||||
if (!page) notFound();
|
||||
|
||||
return new ImageResponse(
|
||||
@@ -21,8 +22,10 @@ export async function GET(_req: Request, { params }: RouteContext<'/og/docs/[...
|
||||
}
|
||||
|
||||
export function generateStaticParams() {
|
||||
return source.getPages().map((page) => ({
|
||||
lang: page.locale,
|
||||
slug: getPageImageUrl(page).segments,
|
||||
}));
|
||||
return i18n.languages.flatMap((lang) =>
|
||||
source.getPages(lang).map((page) => ({
|
||||
lang,
|
||||
slug: getPageImageUrl(page).segments,
|
||||
})),
|
||||
);
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -14,3 +14,27 @@ html > body[data-scroll-locked] {
|
||||
margin-right: 0 !important;
|
||||
--removed-body-scroll-bar-size: 0px !important;
|
||||
}
|
||||
|
||||
/* 文档页目录:隐藏默认折线,改用单条直线导轨 */
|
||||
#nd-toc a[href^='#'] > svg,
|
||||
#nd-toc div.absolute:has(> svg) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#nd-toc a[href^='#'] {
|
||||
border-inline-start: 1px solid var(--color-fd-border);
|
||||
}
|
||||
|
||||
#nd-toc a[href^='#'][data-active='true'] {
|
||||
border-inline-start-color: var(--color-fd-primary);
|
||||
}
|
||||
|
||||
/* 首页导航:通铺整行、无背景无分隔线 */
|
||||
.home-fullbleed-nav {
|
||||
--fd-layout-width: 100%;
|
||||
}
|
||||
|
||||
.home-fullbleed-nav #nd-nav > * {
|
||||
border-bottom-color: transparent;
|
||||
backdrop-filter: none;
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
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,
|
||||
}));
|
||||
}
|
||||
@@ -1,17 +1,36 @@
|
||||
.stage {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 960px;
|
||||
margin: 64px auto 0;
|
||||
}
|
||||
|
||||
.halo {
|
||||
position: absolute;
|
||||
inset: -10% -14%;
|
||||
background: radial-gradient(
|
||||
50% 55% at 50% 45%,
|
||||
color-mix(in srgb, var(--color-fd-primary) 9%, transparent),
|
||||
transparent 72%
|
||||
);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.viewport {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 960 / 620;
|
||||
overflow: hidden;
|
||||
background: #141414;
|
||||
border: 1px solid rgb(255 255 255 / 10%);
|
||||
background: #f5f5f5;
|
||||
border: 1px solid var(--color-fd-border);
|
||||
border-radius: 14px;
|
||||
box-shadow:
|
||||
0 42px 90px -34px rgb(0 0 0 / 35%),
|
||||
0 12px 32px -16px rgb(0 0 0 / 28%);
|
||||
}
|
||||
|
||||
.viewport[data-dark] {
|
||||
background: #141414;
|
||||
box-shadow:
|
||||
0 42px 90px -34px rgb(0 0 0 / 50%),
|
||||
0 12px 32px -16px rgb(0 0 0 / 42%);
|
||||
@@ -23,15 +42,18 @@
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #141414;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.stage p {
|
||||
margin: 14px 0 0;
|
||||
color: var(--color-fd-muted-foreground);
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
.poster {
|
||||
object-fit: cover;
|
||||
transition: opacity 0.6s ease;
|
||||
}
|
||||
|
||||
.posterHidden {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (max-width: 680px) {
|
||||
|
||||
@@ -1,18 +1,114 @@
|
||||
'use client';
|
||||
|
||||
import Image from 'next/image';
|
||||
import { useEffect, useRef, useState, useSyncExternalStore } from 'react';
|
||||
import { demoLocale, type Language } from '@/lib/i18n';
|
||||
import styles from './DesktopDemo.module.css';
|
||||
|
||||
export function DesktopDemo() {
|
||||
// 小屏或触屏设备内嵌交互体验差,只展示 poster 静态图
|
||||
const embedQuery = '(min-width: 680px) and (hover: hover)';
|
||||
|
||||
const alt: Record<Language, string> = {
|
||||
zh: 'cursor-byok 桌面端数据概览界面',
|
||||
en: 'cursor-byok desktop overview dashboard',
|
||||
};
|
||||
|
||||
function subscribeEmbedQuery(callback: () => void) {
|
||||
const media = window.matchMedia(embedQuery);
|
||||
media.addEventListener('change', callback);
|
||||
return () => media.removeEventListener('change', callback);
|
||||
}
|
||||
|
||||
function subscribeTheme(callback: () => void) {
|
||||
const observer = new MutationObserver(callback);
|
||||
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
|
||||
return () => observer.disconnect();
|
||||
}
|
||||
|
||||
export function DesktopDemo({ lang }: { lang: Language }) {
|
||||
const stageRef = useRef<HTMLDivElement>(null);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const embeddable = useSyncExternalStore(
|
||||
subscribeEmbedQuery,
|
||||
() => window.matchMedia(embedQuery).matches,
|
||||
() => true,
|
||||
);
|
||||
const dark = useSyncExternalStore(
|
||||
subscribeTheme,
|
||||
() => document.documentElement.classList.contains('dark'),
|
||||
() => true,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!embeddable) return;
|
||||
const stage = stageRef.current;
|
||||
if (!stage) return;
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries.some((entry) => entry.isIntersecting)) {
|
||||
setMounted(true);
|
||||
observer.disconnect();
|
||||
}
|
||||
},
|
||||
{ rootMargin: '240px' },
|
||||
);
|
||||
observer.observe(stage);
|
||||
return () => observer.disconnect();
|
||||
}, [embeddable]);
|
||||
|
||||
return (
|
||||
<div className={styles.stage}>
|
||||
<div className={styles.viewport}>
|
||||
<div ref={stageRef} className={styles.stage}>
|
||||
<div className={styles.halo} aria-hidden />
|
||||
<DemoViewport
|
||||
key={`${dark ? 'dark' : 'light'}-${lang}`}
|
||||
dark={dark}
|
||||
lang={lang}
|
||||
mounted={embeddable && mounted}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DemoViewport({
|
||||
dark,
|
||||
lang,
|
||||
mounted,
|
||||
}: {
|
||||
dark: boolean;
|
||||
lang: Language;
|
||||
mounted: boolean;
|
||||
}) {
|
||||
const [frameLoaded, setFrameLoaded] = useState(false);
|
||||
const [revealed, setRevealed] = useState(false);
|
||||
const theme = dark ? 'default-dark' : 'default-light';
|
||||
const demoUrl = `/product-demo/demo/index.html?theme=${theme}&locale=${demoLocale[lang]}`;
|
||||
const posterUrl = `/images/product-demo-${dark ? 'dark' : 'light'}-${lang}.png`;
|
||||
|
||||
useEffect(() => {
|
||||
if (!frameLoaded) return;
|
||||
// iframe onLoad 只代表文档加载完成,等内部应用渲染后再揭开 poster
|
||||
const timer = setTimeout(() => setRevealed(true), 900);
|
||||
return () => clearTimeout(timer);
|
||||
}, [frameLoaded]);
|
||||
|
||||
return (
|
||||
<div className={styles.viewport} data-dark={dark || undefined}>
|
||||
{mounted ? (
|
||||
<iframe
|
||||
title="Cursor BYOK 真实产品界面演示"
|
||||
src="/product-demo/demo/index.html"
|
||||
title={alt[lang]}
|
||||
src={demoUrl}
|
||||
className={styles.demo}
|
||||
onLoad={() => setFrameLoaded(true)}
|
||||
/>
|
||||
</div>
|
||||
<p>真实桌面端组件 · 使用隔离的 Mock 数据,可直接操作菜单、筛选和设置</p>
|
||||
) : null}
|
||||
<Image
|
||||
src={posterUrl}
|
||||
alt={alt[lang]}
|
||||
fill
|
||||
priority
|
||||
unoptimized
|
||||
className={revealed ? `${styles.poster} ${styles.posterHidden}` : styles.poster}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
'use client';
|
||||
|
||||
import { useSyncExternalStore } from 'react';
|
||||
import { Download } from 'lucide-react';
|
||||
import type { Language } from '@/lib/i18n';
|
||||
import { releaseUrl } from '@/lib/shared';
|
||||
|
||||
function detectPlatform(): string | null {
|
||||
const ua = navigator.userAgent;
|
||||
if (/Mac/i.test(ua)) return 'macOS';
|
||||
if (/Win/i.test(ua)) return 'Windows';
|
||||
if (/Linux|X11/i.test(ua)) return 'Linux';
|
||||
return null;
|
||||
}
|
||||
|
||||
const labels: Record<Language, { platform: (platform: string) => string; fallback: string }> = {
|
||||
zh: { platform: (platform) => `下载 ${platform} 版`, fallback: '下载最新版' },
|
||||
en: { platform: (platform) => `Download for ${platform}`, fallback: 'Download' },
|
||||
};
|
||||
|
||||
const subscribeNoop = () => () => {};
|
||||
|
||||
export function DownloadButton({ lang }: { lang: Language }) {
|
||||
// 平台只在客户端可知;SSR 用通用文案,水合后替换为平台文案
|
||||
const platform = useSyncExternalStore(subscribeNoop, detectPlatform, () => null);
|
||||
const label = labels[lang];
|
||||
|
||||
return (
|
||||
<a
|
||||
href={releaseUrl}
|
||||
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"
|
||||
>
|
||||
<Download className="size-4" />
|
||||
{platform ? label.platform(platform) : label.fallback}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
title: Why We Built a New Documentation Site
|
||||
description: Bringing the user guide and development notes back into the repository so docs evolve with the product.
|
||||
---
|
||||
|
||||
cursor-byok has grown from simple model forwarding into multi-protocol model configuration, tool calling, session observability, and a cross-platform desktop app. Information scattered across release notes and discussion threads is no longer enough for first-time users, and it makes it harder for developers to understand the system's boundaries.
|
||||
|
||||
## Documentation is part of the product
|
||||
|
||||
The new documentation site lives in the same repository as the application code:
|
||||
|
||||
```text
|
||||
apps/
|
||||
├── desktop/ # Desktop app
|
||||
└── docs/ # Documentation site
|
||||
├── content/docs/
|
||||
└── content/blog/
|
||||
```
|
||||
|
||||
The user guide answers "how do I use it", while the developer blog records "why it is designed this way". The two kinds of content are maintained separately but share the same build and review pipeline.
|
||||
|
||||
## Why Fumadocs
|
||||
|
||||
Fumadocs provides the documentation layout, full-text search, code highlighting, table of contents, and the MDX content layer. We only need to maintain product information and visual styling instead of reimplementing generic documentation features.
|
||||
|
||||
The docs app stays independent: the desktop app does not take on Next.js or Fumadocs dependencies, and development, builds, and deployments remain separate.
|
||||
|
||||
## What we will write about
|
||||
|
||||
The developer blog will keep covering:
|
||||
|
||||
- Cursor protocol adaptation and model compatibility design.
|
||||
- Trade-offs in Agent tool calling and multi-turn conversations.
|
||||
- Local storage, observability, and performance work.
|
||||
- Cross-platform desktop development and the release process.
|
||||
|
||||
These posts describe the current code. We do not keep compatibility notes for implementations that have been removed.
|
||||
@@ -0,0 +1,93 @@
|
||||
---
|
||||
title: Quick Start
|
||||
description: Install cursor-byok and let Cursor use your own model APIs.
|
||||
icon: Rocket
|
||||
---
|
||||
|
||||
cursor-byok is a Cursor model gateway that runs on your machine. It receives Cursor Agent requests, converts them, and forwards them to the OpenAI- or Anthropic-compatible service you configure.
|
||||
|
||||
<Callout type="warn" title="Before you start">
|
||||
cursor-byok is an independent open-source project and is not affiliated with Cursor or its developers. The software itself is free, but model providers may charge for usage.
|
||||
</Callout>
|
||||
|
||||
## Install and configure
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step>
|
||||
|
||||
### Update Cursor
|
||||
|
||||
Download the latest official Cursor from [cursor.com](https://cursor.com) and install it over your existing copy, so the client stays up to date.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
|
||||
### Download and launch cursor-byok
|
||||
|
||||
Download the latest release for your operating system from [GitHub Releases](https://github.com/leookun/cursor-byok/releases/latest), then launch cursor-byok.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
|
||||
### Initialize and configure a model
|
||||
|
||||
Open **Cursor Configuration**, follow the prompts to initialize the local CA, then add a model. Fill in the server address, API key, and model name, and run the connectivity test until it passes.
|
||||
|
||||

|
||||
|
||||
See [Model Configuration](./model-configuration.mdx) for how to choose the type and protocol for different models.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
|
||||
### Restart Cursor after the first setup
|
||||
|
||||
After completing the configuration for the first time, quit and restart Cursor once so the model list takes effect.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
|
||||
### Use it in Cursor
|
||||
|
||||
Keep cursor-byok running, start a new conversation in Cursor, pick the model you just configured from the model list (do not pick Auto), and start using Agent.
|
||||
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
## Coexists with your official account
|
||||
|
||||
The current design goal is coexistence with the official service — there is no more "stop the service" or account juggling:
|
||||
|
||||
- Sign in to Cursor with your own account. If you previously used a fake account generated by an old version, sign out of it first, then sign in with your own.
|
||||
- If your account has official quota, there is no boundary between official models and your local models — switch and mix freely.
|
||||
- **Auto always routes to official models**: picking Auto without quota results in an error, so select your configured model instead.
|
||||
- Cursor features such as plugins and codebase indexing keep working as usual.
|
||||
|
||||
## Next steps
|
||||
|
||||
<Cards>
|
||||
<Card title="Installation" description="Download, initialize, and run for the first time." href="/docs/installation" />
|
||||
<Card title="Model Configuration" description="Choose a protocol and fill in upstream model parameters." href="/docs/model-configuration" />
|
||||
<Card title="TAB Service" description="Choose how Cursor connects to Tab completion endpoints." href="/docs/tab-service" />
|
||||
<Card title="Troubleshooting" description="Resolve certificate, connection, and model test issues." href="/docs/troubleshooting" />
|
||||
</Cards>
|
||||
|
||||
## How data flows
|
||||
|
||||
```text
|
||||
Cursor client
|
||||
│ Agent requests and tool results
|
||||
▼
|
||||
cursor-byok local service
|
||||
│ OpenAI- / Anthropic-compatible requests
|
||||
▼
|
||||
Your model API
|
||||
```
|
||||
|
||||
API keys, model configurations, and app settings are stored on your machine. Model requests are still sent to the upstream provider you choose.
|
||||
@@ -10,13 +10,21 @@ cursor-byok 是运行在本机的 Cursor 模型网关。它接收 Cursor Agent
|
||||
cursor-byok 是独立开源项目,与 Cursor 及其开发者没有关联。软件本身免费,但模型服务商可能按用量收费。
|
||||
</Callout>
|
||||
|
||||
## 三步开始使用
|
||||
## 安装与配置
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step>
|
||||
|
||||
### 下载并启动
|
||||
### 更新 Cursor
|
||||
|
||||
从 [cursor.com](https://cursor.com) 下载官方最新版 Cursor,直接覆盖安装,保持客户端为最新版本。
|
||||
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
|
||||
### 下载并启动 cursor-byok
|
||||
|
||||
从 [GitHub Releases](https://github.com/leookun/cursor-byok/releases/latest) 下载与你的操作系统对应的最新版本,然后启动 cursor-byok。
|
||||
|
||||
@@ -26,7 +34,19 @@ cursor-byok 是独立开源项目,与 Cursor 及其开发者没有关联。软
|
||||
|
||||
### 初始化并配置模型
|
||||
|
||||
打开 **Cursor 配置**,按界面提示初始化本地 CA,然后添加模型。填写服务地址、API Key 和模型名称,并运行连通性测试。
|
||||
打开 **Cursor 配置**,按界面提示初始化本地 CA,然后添加模型。填写服务地址、API Key 和模型名称,并运行连通性测试,测试通过即可。
|
||||
|
||||

|
||||
|
||||
不同模型如何选择类型与协议,见[模型配置](./model-configuration.mdx)。
|
||||
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
|
||||
### 首次配置后重启 Cursor
|
||||
|
||||
首次完成配置时,完全退出并重新启动一次 Cursor,让模型列表生效。
|
||||
|
||||
</Step>
|
||||
|
||||
@@ -34,19 +54,28 @@ cursor-byok 是独立开源项目,与 Cursor 及其开发者没有关联。软
|
||||
|
||||
### 在 Cursor 中使用
|
||||
|
||||
保持 cursor-byok 运行,打开 Cursor,在模型列表中选择刚刚配置的模型,然后开始使用 Agent。
|
||||
保持 cursor-byok 运行,在 Cursor 中新开一个对话,从模型列表中选择你刚配置的模型(不要选 Auto),开始使用 Agent。
|
||||
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
## 与官方账号并存
|
||||
|
||||
新版的设计目标是与官方服务并存,不再需要“关服务”“切账号”之类的操作:
|
||||
|
||||
- 直接在 Cursor 中登录你自己的账号。如果之前用过旧版生成的 fake 账户,先退出它,再登录自己的账号。
|
||||
- 账号有官方额度时,官方模型和本地模型没有使用上的边界,可以随时切换混用。
|
||||
- **Auto 走的是官方模型**:账号没有额度时选 Auto 会报错,请手动选择你配置的模型。
|
||||
- 插件、代码库索引等 Cursor 功能都可以正常使用。
|
||||
|
||||
## 接下来
|
||||
|
||||
<Cards>
|
||||
<Card title="安装指南" description="下载、初始化和首次运行。" href="/docs/installation" />
|
||||
<Card title="模型配置" description="选择协议并填写上游模型参数。" href="/docs/model-configuration" />
|
||||
<Card title="TAB 服务" description="选择 Cursor Tab 补全接口的连接方式。" href="/docs/tab-service" />
|
||||
<Card title="故障排查" description="处理证书、连接与模型测试问题。" href="/docs/troubleshooting" />
|
||||
<Card title="查看源码" description="了解实现或参与项目开发。" href="https://github.com/leookun/cursor-byok" external />
|
||||
</Cards>
|
||||
|
||||
## 数据如何流转
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
title: Installation
|
||||
description: Download cursor-byok, complete local initialization, and verify it is running.
|
||||
icon: Download
|
||||
---
|
||||
|
||||
## Download the app
|
||||
|
||||
Go to the [latest release page](https://github.com/leookun/cursor-byok/releases/latest) and download the installer for macOS, Windows, or Linux.
|
||||
|
||||
<Callout type="info">
|
||||
Prefer the latest stable release. The release page lists the installers and release notes for each version.
|
||||
</Callout>
|
||||
|
||||
## First launch
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step>
|
||||
|
||||
### Open Cursor Settings
|
||||
|
||||
After launching the app, go to **Cursor Settings**. If the local CA has not been initialized, the page shows an initialization entry.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
|
||||
### Initialize the local CA
|
||||
|
||||
Click **Initialize CA**. The local CA is used to inspect HTTPS requests from Cursor on your device; its files never leave your machine.
|
||||
|
||||
When the system asks for authorization, follow the instructions shown in the app to complete the trust step in your terminal, then return to the app and click **I have initialized, refresh**.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
|
||||
### Add your first model
|
||||
|
||||
Click **Add Model**, choose the OpenAI or Anthropic type, fill in the upstream service parameters, and save. See [Model Configuration](./model-configuration.mdx) for details on each field.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
|
||||
### Run the connectivity test
|
||||
|
||||
Click **Test** on the model. Once the test passes, the model appears in Cursor's model list.
|
||||
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
## Verify the installation
|
||||
|
||||
After configuration, confirm that:
|
||||
|
||||
- The Cursor Settings page no longer shows the CA initialization prompt.
|
||||
- At least one model passes the connectivity test.
|
||||
- cursor-byok stays running.
|
||||
- The configured display name shows up in Cursor's model list.
|
||||
|
||||
If any of these steps fail, head to [Troubleshooting](./troubleshooting.mdx).
|
||||
|
||||
## Updates
|
||||
|
||||
Check for new versions in the app settings, or visit [GitHub Releases](https://github.com/leookun/cursor-byok/releases/latest) directly. You do not need to delete existing model configurations before updating.
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"title": "User Guide",
|
||||
"root": true,
|
||||
"pages": ["index", "installation", "model-configuration", "tab-service", "troubleshooting"]
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"title": "使用指南",
|
||||
"root": true,
|
||||
"pages": ["index", "installation", "model-configuration", "troubleshooting"]
|
||||
"pages": ["index", "installation", "model-configuration", "tab-service", "troubleshooting"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
---
|
||||
title: Model Configuration
|
||||
description: Configure the model protocol, server address, credentials, and generation parameters.
|
||||
icon: Settings2
|
||||
---
|
||||
|
||||
Each model configuration is an independent upstream channel that can use a different provider, protocol, credentials, and generation parameters.
|
||||
|
||||

|
||||
|
||||
## Choosing the type and protocol
|
||||
|
||||
Picking by model family avoids most compatibility issues:
|
||||
|
||||
| Model family | Model type | Request protocol |
|
||||
| --- | --- | --- |
|
||||
| Claude family | **Anthropic** | — |
|
||||
| GPT / OpenAI family | **OpenAI** | **Responses API** |
|
||||
| Everything else | **OpenAI** | **Chat Completions API** |
|
||||
|
||||
<Callout type="warn" title="Always use the Responses API for GPT models">
|
||||
Running GPT models through Chat Completions loses prompt caching, which makes them noticeably slower and more expensive.
|
||||
</Callout>
|
||||
|
||||
## Required fields
|
||||
|
||||
### Model type
|
||||
|
||||
Choose the upstream interface format:
|
||||
|
||||
- **OpenAI**: supports the Responses API and the Chat Completions API.
|
||||
- **Anthropic**: supports Messages API-compatible services.
|
||||
|
||||
### Request protocol
|
||||
|
||||
For the OpenAI type, also choose **Responses API** or **Chat Completions API**. This option determines the request and response format; it does not change the server address you entered.
|
||||
|
||||
### Server address
|
||||
|
||||
You can enter the provider's base URL, or choose to use a full request URL:
|
||||
|
||||
- With a base URL, cursor-byok appends the standard endpoint path for the chosen protocol.
|
||||
- With a full request URL, cursor-byok uses the address as-is.
|
||||
|
||||
Prefer the built-in provider presets in the UI to avoid protocol and endpoint mismatches.
|
||||
|
||||
### API key
|
||||
|
||||
Enter the access key required by the upstream service. The key is stored on your machine and used to send model requests.
|
||||
|
||||
### Model name
|
||||
|
||||
Enter the model identifier accepted by the provider's API. You can also click **Fetch Models** to load the model list returned by the API.
|
||||
|
||||
## Display information
|
||||
|
||||
- **Display name**: the name shown in Cursor's model list; it does not change the model identifier sent upstream.
|
||||
- **Notes**: shown in Cursor's model description.
|
||||
|
||||
## Optional parameters
|
||||
|
||||
Fill in the following fields based on the model's capabilities; when left empty, the app or upstream defaults are used:
|
||||
|
||||
- Context window tokens
|
||||
- Max output tokens
|
||||
- Reasoning or thinking effort
|
||||
- Custom headers
|
||||
- Extra OpenAI or Anthropic parameters
|
||||
|
||||
<Callout type="warn" title="Extra parameter format">
|
||||
Custom headers and extra parameters must be JSON objects. Extra parameters directly affect upstream requests, so only add fields the provider explicitly supports.
|
||||
</Callout>
|
||||
|
||||
## Test the configuration
|
||||
|
||||
Run **Test** after saving. The result shows time to first token, generation speed, total duration, and the model output, which helps you confirm:
|
||||
|
||||
1. The address and protocol match.
|
||||
2. The API key is valid.
|
||||
3. The model identifier exists and is accessible.
|
||||
4. The upstream returns streamed content correctly.
|
||||
|
||||
Once the test passes, switch to Cursor and start using the model.
|
||||
@@ -6,6 +6,22 @@ icon: Settings2
|
||||
|
||||
每个模型配置都是一个独立的上游通道,可以使用不同服务商、协议、凭据和生成参数。
|
||||
|
||||

|
||||
|
||||
## 如何选择类型与协议
|
||||
|
||||
按上游模型系列选择,可以避免绝大多数兼容性问题:
|
||||
|
||||
| 模型系列 | 模型类型 | 请求协议 |
|
||||
| --- | --- | --- |
|
||||
| Claude 系列 | **Anthropic** | — |
|
||||
| GPT / OpenAI 系列 | **OpenAI** | **Responses API** |
|
||||
| 其他所有模型 | **OpenAI** | **Chat Completions API** |
|
||||
|
||||
<Callout type="warn" title="GPT 系列务必用 Responses API">
|
||||
GPT 系列走 Chat Completions 会丢失提示词缓存,速度和费用都会明显变差。
|
||||
</Callout>
|
||||
|
||||
## 必填字段
|
||||
|
||||
### 模型类型
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
title: TAB Service
|
||||
description: Choose how Cursor connects to Tab completion endpoints.
|
||||
icon: Zap
|
||||
---
|
||||
|
||||
Cursor's Tab completion does not go through model channels — it is handled by a separate Tab service. cursor-byok offers three connection modes under **System settings → TAB settings**.
|
||||
|
||||

|
||||
|
||||
## The three modes
|
||||
|
||||
### Use public service (default)
|
||||
|
||||
Uses the public Tab service hosted by the project author. Works out of the box with no configuration.
|
||||
|
||||
### Direct
|
||||
|
||||
Connects directly to the official Tab service of the account signed in to your Cursor client. Suitable if your account has official quota.
|
||||
|
||||
### Custom
|
||||
|
||||
Deploy your own [cursor-tab-server](https://github.com/leookun/cursor-byok/tree/archive/v0.0.49/cursor-tab-server), then enter your service URL in **TAB service address**. The original endpoint path is appended to that address.
|
||||
|
||||
<Callout type="info">
|
||||
After changing TAB settings, restart Cursor and start a new conversation to make sure the connection mode takes effect.
|
||||
</Callout>
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
title: TAB 服务
|
||||
description: 选择 Cursor Tab 补全接口的连接方式。
|
||||
icon: Zap
|
||||
---
|
||||
|
||||
Cursor 的 Tab 补全不走模型通道,而是由单独的 Tab 服务处理。cursor-byok 在 **系统设置 → TAB 设置** 中提供三种连接方式。
|
||||
|
||||

|
||||
|
||||
## 三种模式
|
||||
|
||||
### 使用公益服务(默认)
|
||||
|
||||
使用由项目作者部署的公共 Tab 服务,开箱即用,无需任何配置。
|
||||
|
||||
### 直连
|
||||
|
||||
直接使用你在 Cursor 客户端中登录账号的官方 Tab 服务。适合账号本身有官方额度的用户。
|
||||
|
||||
### 自定义
|
||||
|
||||
自行部署 [cursor-tab-server](https://github.com/leookun/cursor-byok/tree/archive/v0.0.49/cursor-tab-server),然后在 **TAB 服务地址** 中填入你的服务地址。原接口路径会追加到该地址之后。
|
||||
|
||||
<Callout type="info">
|
||||
修改 TAB 设置后,建议重启 Cursor 并新开一个对话,确保连接方式生效。
|
||||
</Callout>
|
||||
@@ -0,0 +1,83 @@
|
||||
---
|
||||
title: Troubleshooting
|
||||
description: Resolve issues with the local CA, the management service, model connections, and Cursor's model list.
|
||||
icon: Wrench
|
||||
---
|
||||
|
||||
## Local CA needs to be initialized
|
||||
|
||||
Go to **Cursor Settings** and click **Initialize CA**. The local CA stays on your machine and is used to inspect HTTPS requests from Cursor.
|
||||
|
||||
## Local CA needs to be trusted by the system
|
||||
|
||||
Click **Open terminal to install CA** and follow the terminal prompts to complete system authorization. When done, return to the app and click **I have initialized, refresh**.
|
||||
|
||||
If the status does not update, quit cursor-byok completely, restart it, and open the Cursor Settings page again.
|
||||
|
||||
## Cannot connect to the local management service
|
||||
|
||||
1. Quit and restart cursor-byok completely.
|
||||
2. Check whether security software is blocking local loopback connections.
|
||||
3. If you changed the management service port, reset it to `0` so the app picks an available port at startup.
|
||||
4. Launch the app again and refresh the page.
|
||||
|
||||
## Model connectivity test fails
|
||||
|
||||
Check based on the test error:
|
||||
|
||||
- **Authentication error**: confirm the API key is valid and the account can access the target model.
|
||||
- **Endpoint not found**: confirm the model type, request protocol, and server address match.
|
||||
- **Model not found**: use **Fetch Models** to check the model identifiers returned by the upstream.
|
||||
- **Invalid parameters**: temporarily remove custom headers and extra parameters, then test again.
|
||||
- **Connection timeout**: check your network, system proxy, and the upstream service status.
|
||||
|
||||
## Models do not show up in Cursor or do not take effect
|
||||
|
||||
First confirm all of the following:
|
||||
|
||||
1. The local CA status is healthy.
|
||||
2. At least one model configuration is saved.
|
||||
3. The model connectivity test passes.
|
||||
4. cursor-byok is running.
|
||||
|
||||
If it still does not work, run the full restart sequence in order:
|
||||
|
||||
1. Quit cursor-byok completely from the tray and start it again.
|
||||
2. Quit and restart Cursor completely.
|
||||
3. Start a new conversation and refresh the model list.
|
||||
4. Pick your own configured model — **do not pick Auto**.
|
||||
|
||||
## Quota or account errors when picking Auto
|
||||
|
||||
Auto only routes to official Cursor models and never uses your locally configured models. If your account has no official quota, picking Auto fails — select your configured model from the model list instead.
|
||||
|
||||
If your account does have quota, official models and local models mix freely with no usage boundary.
|
||||
|
||||
## Still using an old fake account
|
||||
|
||||
The current design coexists with your official account — fake accounts and "stop the service" workflows are no longer needed:
|
||||
|
||||
1. Sign out of the fake account generated by an old version in Cursor.
|
||||
2. Sign in with your own Cursor account.
|
||||
|
||||
Once signed in with your own account, Cursor features such as plugins and codebase indexing work as usual.
|
||||
|
||||
## Requests fail even though the test passes
|
||||
|
||||
The model test only verifies basic connectivity. Agent requests also involve longer context, tool definitions, and streaming responses. Check:
|
||||
|
||||
- Whether the upstream model supports tool calling.
|
||||
- Whether the context window and max output tokens fit the model's limits.
|
||||
- Whether custom parameters are compatible with the actual protocol.
|
||||
- The upstream status code and response body in the call details.
|
||||
|
||||
## Report an issue
|
||||
|
||||
If the problem persists, file an issue on [GitHub Issues](https://github.com/leookun/cursor-byok/issues) and include:
|
||||
|
||||
- Your operating system and cursor-byok version.
|
||||
- The selected model type and request protocol.
|
||||
- The redacted server address and error message.
|
||||
- Steps to reproduce.
|
||||
|
||||
Never share API keys or other credentials publicly.
|
||||
@@ -31,15 +31,36 @@ icon: Wrench
|
||||
- **参数错误**:暂时关闭自定义 Headers 和额外参数,再重新测试。
|
||||
- **连接超时**:检查网络、系统代理和上游服务状态。
|
||||
|
||||
## Cursor 中看不到模型
|
||||
## Cursor 中看不到模型或模型不生效
|
||||
|
||||
确认以下条件全部满足:
|
||||
先确认以下条件全部满足:
|
||||
|
||||
1. 本地 CA 状态正常。
|
||||
2. 已保存至少一个模型配置。
|
||||
3. 模型连通性测试成功。
|
||||
4. cursor-byok 正在运行。
|
||||
5. 重启 Cursor 后重新打开模型列表。
|
||||
|
||||
仍然不生效时,按顺序执行一遍完整的重启流程:
|
||||
|
||||
1. 从托盘完全退出 cursor-byok,重新启动。
|
||||
2. 完全退出并重新启动 Cursor。
|
||||
3. 新开一个对话,刷新模型列表。
|
||||
4. 选择你自己配置的模型,**不要选 Auto**。
|
||||
|
||||
## 选 Auto 时报额度或账户错误
|
||||
|
||||
Auto 只会路由到 Cursor 官方模型,不会使用你配置的本地模型。账号没有官方额度时选 Auto 就会报错——请在模型列表中手动选择你配置的模型。
|
||||
|
||||
如果账号本身有额度,官方模型和本地模型可以随意混用,没有使用上的边界。
|
||||
|
||||
## 还在使用旧版的 fake 账户
|
||||
|
||||
新版的设计是与官方账号并存,不再需要 fake 账户,也没有“关服务”之类的操作:
|
||||
|
||||
1. 在 Cursor 中退出旧版生成的 fake 账户。
|
||||
2. 登录你自己的 Cursor 账号。
|
||||
|
||||
登录自己的账号后,插件、代码库索引等 Cursor 功能都可以正常使用。
|
||||
|
||||
## 请求失败但测试成功
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { loader } from 'fumadocs-core/source';
|
||||
import { pageSchema } from 'fumadocs-core/source/schema';
|
||||
import { defineCollections } from 'fumadocs-mdx/macro';
|
||||
import { htmlLang, i18n, type Language } from './i18n';
|
||||
|
||||
const blog = defineCollections({
|
||||
type: 'doc',
|
||||
@@ -11,6 +12,7 @@ const blog = defineCollections({
|
||||
export const blogSource = loader({
|
||||
baseUrl: '/blog',
|
||||
source: blog.toFumadocsSource(),
|
||||
i18n,
|
||||
});
|
||||
|
||||
export function getBlogDate(path: string): Date {
|
||||
@@ -19,8 +21,8 @@ export function getBlogDate(path: string): Date {
|
||||
return match ? new Date(`${match[1]}T00:00:00Z`) : new Date(0);
|
||||
}
|
||||
|
||||
export function formatBlogDate(path: string): string {
|
||||
return new Intl.DateTimeFormat('zh-CN', {
|
||||
export function formatBlogDate(path: string, lang: Language = i18n.defaultLanguage): string {
|
||||
return new Intl.DateTimeFormat(htmlLang[lang], {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { gitConfig } from './shared';
|
||||
|
||||
export type RepoStats = {
|
||||
stars: number | null;
|
||||
version: string | null;
|
||||
};
|
||||
|
||||
async function fetchJson(url: string): Promise<Record<string, unknown> | null> {
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
headers: { Accept: 'application/vnd.github+json' },
|
||||
next: { revalidate: 3600 },
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
return (await res.json()) as Record<string, unknown>;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getRepoStats(): Promise<RepoStats> {
|
||||
const base = `https://api.github.com/repos/${gitConfig.user}/${gitConfig.repo}`;
|
||||
const [repo, release] = await Promise.all([
|
||||
fetchJson(base),
|
||||
fetchJson(`${base}/releases/latest`),
|
||||
]);
|
||||
|
||||
return {
|
||||
stars: typeof repo?.stargazers_count === 'number' ? repo.stargazers_count : null,
|
||||
version: typeof release?.tag_name === 'string' ? release.tag_name : null,
|
||||
};
|
||||
}
|
||||
|
||||
export function formatStars(stars: number): string {
|
||||
return new Intl.NumberFormat('en', {
|
||||
notation: 'compact',
|
||||
maximumFractionDigits: 1,
|
||||
}).format(stars);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { defineI18n } from 'fumadocs-core/i18n';
|
||||
|
||||
export const i18n = defineI18n({
|
||||
defaultLanguage: 'zh',
|
||||
languages: ['zh', 'en'],
|
||||
hideLocale: 'default-locale',
|
||||
});
|
||||
|
||||
export type Language = (typeof i18n)['languages'][number];
|
||||
|
||||
export function isLanguage(value: string): value is Language {
|
||||
return i18n.languages.some((lang) => lang === value);
|
||||
}
|
||||
|
||||
/** HTML `lang` attribute values. */
|
||||
export const htmlLang: Record<Language, string> = {
|
||||
zh: 'zh-CN',
|
||||
en: 'en',
|
||||
};
|
||||
|
||||
/** Locale ids understood by the desktop app / demo. */
|
||||
export const demoLocale: Record<Language, string> = {
|
||||
zh: 'zh-CN',
|
||||
en: 'en-US',
|
||||
};
|
||||
@@ -1,27 +1,35 @@
|
||||
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 { i18n, type Language } from './i18n';
|
||||
import { appName, gitConfig, releaseUrl } from './shared';
|
||||
|
||||
export const translations = defineTranslations().extend(uiTranslations()).preset(zhCN());
|
||||
export const translations = i18n.translations().extend(uiTranslations()).preset('zh', zhCN());
|
||||
|
||||
const navLabels: Record<Language, { docs: string; blog: string; download: string }> = {
|
||||
zh: { docs: '文档', blog: '开发者博客', download: '下载' },
|
||||
en: { docs: 'Docs', blog: 'Blog', download: 'Download' },
|
||||
};
|
||||
|
||||
export function baseOptions(lang: Language): BaseLayoutProps {
|
||||
const labels = navLabels[lang];
|
||||
const prefix = lang === i18n.defaultLanguage ? '' : `/${lang}`;
|
||||
|
||||
export function baseOptions(): BaseLayoutProps {
|
||||
return {
|
||||
nav: {
|
||||
title: appName,
|
||||
},
|
||||
links: [
|
||||
{
|
||||
text: '文档',
|
||||
url: '/docs',
|
||||
text: labels.docs,
|
||||
url: `${prefix}/docs`,
|
||||
},
|
||||
{
|
||||
text: '开发者博客',
|
||||
url: '/blog',
|
||||
text: labels.blog,
|
||||
url: `${prefix}/blog`,
|
||||
},
|
||||
{
|
||||
text: '下载',
|
||||
text: labels.download,
|
||||
url: releaseUrl,
|
||||
external: true,
|
||||
},
|
||||
|
||||
@@ -3,6 +3,7 @@ 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';
|
||||
import { i18n } from './i18n';
|
||||
|
||||
const docs = defineDocs({
|
||||
dir: 'content/docs',
|
||||
@@ -22,14 +23,22 @@ export const source = loader({
|
||||
baseUrl: docsRoute,
|
||||
source: docs.toFumadocsSource(),
|
||||
plugins: [lucideIconsPlugin()],
|
||||
i18n,
|
||||
});
|
||||
|
||||
/** Locale URL prefix segment; empty for the default (hidden) locale. */
|
||||
function localeSegment(page: (typeof source)['$inferPage']): string | undefined {
|
||||
return page.locale === i18n.defaultLanguage ? undefined : page.locale;
|
||||
}
|
||||
|
||||
export function getPageImageUrl(page: (typeof source)['$inferPage']) {
|
||||
const segments = [...page.slugs, 'image.png'];
|
||||
|
||||
return {
|
||||
segments,
|
||||
url: '/' + [page.locale, ...docsImageRoute.split('/'), ...segments].filter(Boolean).join('/'),
|
||||
url:
|
||||
'/' +
|
||||
[localeSegment(page), ...docsImageRoute.split('/'), ...segments].filter(Boolean).join('/'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -38,7 +47,9 @@ export function getPageMarkdownUrl(page: (typeof source)['$inferPage']) {
|
||||
|
||||
return {
|
||||
segments,
|
||||
url: '/' + [page.locale, ...docsContentRoute.split('/'), ...segments].filter(Boolean).join('/'),
|
||||
url:
|
||||
'/' +
|
||||
[localeSegment(page), ...docsContentRoute.split('/'), ...segments].filter(Boolean).join('/'),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import { defineCloudflareConfig } from '@opennextjs/cloudflare';
|
||||
|
||||
// 站点全部页面在构建期静态生成,无需增量缓存 / 队列等运行时缓存设施。
|
||||
export default defineCloudflareConfig({});
|
||||
@@ -11,16 +11,21 @@
|
||||
"start": "next start",
|
||||
"types:check": "next typegen && tsc --noEmit",
|
||||
"lint": "eslint",
|
||||
"check": "npm run types:check && npm run lint && npm run build"
|
||||
"check": "npm run types:check && npm run lint && npm run build",
|
||||
"preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview",
|
||||
"deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
|
||||
"upload": "opennextjs-cloudflare build && opennextjs-cloudflare upload",
|
||||
"cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fumadocs/language": "^0.2.4",
|
||||
"@opennextjs/cloudflare": "^1.20.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",
|
||||
"next": "16.3",
|
||||
"react": "^19.2.8",
|
||||
"react-dom": "^19.2.8"
|
||||
},
|
||||
@@ -31,9 +36,10 @@
|
||||
"@types/react": "^19.2.18",
|
||||
"@types/react-dom": "^19.2.5",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-config-next": "16.3.2",
|
||||
"eslint-config-next": "16.3",
|
||||
"postcss": "^8.5.26",
|
||||
"tailwindcss": "^4.3.3",
|
||||
"typescript": "^6.0.3"
|
||||
"typescript": "^6.0.3",
|
||||
"wrangler": "^4.127.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { NextRequest, NextResponse, type NextFetchEvent } from 'next/server';
|
||||
import { isMarkdownPreferred, rewritePath } from 'fumadocs-core/negotiation';
|
||||
import { createI18nMiddleware } from 'fumadocs-core/i18n/middleware';
|
||||
import { i18n, type Language } from '@/lib/i18n';
|
||||
import { docsContentRoute, docsRoute } from '@/lib/shared';
|
||||
|
||||
const { rewrite: rewriteDocs } = rewritePath(
|
||||
@@ -11,22 +13,41 @@ const { rewrite: rewriteSuffix } = rewritePath(
|
||||
`${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));
|
||||
const i18nMiddleware = createI18nMiddleware(i18n);
|
||||
|
||||
/** Split the locale prefix (if any) from a pathname. */
|
||||
function splitLocale(pathname: string): { locale: Language; rest: string } {
|
||||
for (const lang of i18n.languages) {
|
||||
if (pathname === `/${lang}`) return { locale: lang, rest: '/' };
|
||||
if (pathname.startsWith(`/${lang}/`)) {
|
||||
return { locale: lang, rest: pathname.slice(lang.length + 1) };
|
||||
}
|
||||
}
|
||||
return { locale: i18n.defaultLanguage, rest: pathname };
|
||||
}
|
||||
|
||||
export default function proxy(request: NextRequest, event: NextFetchEvent) {
|
||||
const { locale, rest } = splitLocale(request.nextUrl.pathname);
|
||||
|
||||
const suffixTarget = rewriteSuffix(rest);
|
||||
if (suffixTarget) {
|
||||
return NextResponse.rewrite(new URL(`/${locale}${suffixTarget}`, request.nextUrl));
|
||||
}
|
||||
|
||||
if (isMarkdownPreferred(request)) {
|
||||
const result = rewriteDocs(request.nextUrl.pathname);
|
||||
const target = rewriteDocs(rest);
|
||||
|
||||
if (result) {
|
||||
return NextResponse.rewrite(new URL(result, request.nextUrl), {
|
||||
if (target) {
|
||||
return NextResponse.rewrite(new URL(`/${locale}${target}`, request.nextUrl), {
|
||||
// this URL has two representations, selected by `Accept`
|
||||
headers: { Vary: 'Accept' },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
return i18nMiddleware(request, event);
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ['/((?!api|_next|favicon.ico|images/|product-demo/|llms\\.txt|llms-full\\.txt).*)'],
|
||||
};
|
||||
|
||||
|
After Width: | Height: | Size: 179 KiB |
|
After Width: | Height: | Size: 172 KiB |
|
After Width: | Height: | Size: 248 KiB |
|
After Width: | Height: | Size: 244 KiB |
|
After Width: | Height: | Size: 144 KiB |
|
After Width: | Height: | Size: 141 KiB |
|
After Width: | Height: | Size: 130 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 130 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 105 KiB |
|
After Width: | Height: | Size: 144 KiB |
|
After Width: | Height: | Size: 248 KiB |