feat(web): community site for deepseek-tui.com (mobile + color refresh) (#1108)
First commit of the Next.js community site that powers deepseek-tui.com, deployed via Cloudflare Workers / OpenNext. This commit lands the scaffold and applies the visual + correctness pass requested by community feedback: - Palette: drop the cream/Anthropic-feel paper (#F4F1E8) for a DeepSeek-aligned cool white + soft gray (#FFFFFF / #F4F6FB), with indigo accents kept. Soften default hairlines so a pure-white background reads clean instead of harsh. - Mobile: add a hamburger menu (mobile-menu.tsx) so phones can reach Install / Docs / Activity / Roadmap / Contribute — previously the link list was hidden on phones with no replacement. Tighter hero, flexible button row, viewport-safe code blocks, columnar grids collapse cleanly under 768px, and the printed-almanac center rule is desktop-only now (it sliced through narrow viewports). - "How it works" diagram: replace the hand-rolled ASCII art (which misaligned under CJK monospace because Han characters take 2 columns vs Latin's 1, per dhh's note in WeChat) with a real mermaid diagram rendered client-side via dynamic import. Uses the mermaid.live standard syntax 庄表伟 recommended. - Issue #1104: the docs listed a `deepseek-cn` provider that the v0.8.16 binary doesn't accept (`ProviderArg` in crates/cli only has 9 variants; the 10th lives only in the legacy tui/config.rs). derive-facts.mjs now omits `deepseek-cn` until that variant is wired through the shared ProviderKind, and the install page's China-network recipe uses `base_url` / `DEEPSEEK_BASE_URL` (which actually works on v0.8.16) instead of the unsupported provider. Auto-deploys via .github/workflows/deploy-web.yml on push to main. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import type { AgentDraft } from "@/lib/community-agent";
|
||||
|
||||
interface Props {
|
||||
drafts: AgentDraft[];
|
||||
posted: AgentDraft[];
|
||||
isZh: boolean;
|
||||
typeLabels: Record<string, { en: string; zh: string }>;
|
||||
}
|
||||
|
||||
export function AdminClient({ drafts, posted, isZh, typeLabels }: Props) {
|
||||
const [items, setItems] = useState(drafts);
|
||||
const [postedItems, setPostedItems] = useState(posted);
|
||||
const [editing, setEditing] = useState<string | null>(null);
|
||||
const [editBody, setEditBody] = useState("");
|
||||
const [loading, setLoading] = useState<string | null>(null);
|
||||
|
||||
const handleAction = async (draftKey: string, action: "post" | "discard", editedBody?: string) => {
|
||||
setLoading(draftKey);
|
||||
try {
|
||||
const res = await fetch("/api/admin/post", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ action, draftKey, editedBody, lang: isZh ? "zh" : "en" }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.ok) {
|
||||
if (action === "discard") {
|
||||
setItems((prev) => prev.filter((d) => `draft:${d.type}:${d.id}` !== draftKey));
|
||||
} else if (action === "post") {
|
||||
const posted = items.find((d) => `draft:${d.type}:${d.id}` === draftKey);
|
||||
if (posted) {
|
||||
setItems((prev) => prev.filter((d) => `draft:${d.type}:${d.id}` !== draftKey));
|
||||
setPostedItems((prev) => [{ ...posted, posted: true }, ...prev]);
|
||||
}
|
||||
}
|
||||
setEditing(null);
|
||||
} else {
|
||||
alert(`Error: ${data.error}`);
|
||||
}
|
||||
} catch (e) {
|
||||
alert(`Network error: ${e}`);
|
||||
} finally {
|
||||
setLoading(null);
|
||||
}
|
||||
};
|
||||
|
||||
const startEdit = (draft: AgentDraft) => {
|
||||
const key = `draft:${draft.type}:${draft.id}`;
|
||||
setEditing(key);
|
||||
setEditBody(draft.bodyEn);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Pending drafts */}
|
||||
{items.length > 0 && (
|
||||
<div className="space-y-6">
|
||||
<h2 className="font-display text-xl mt-8 mb-4">
|
||||
{isZh ? "待审阅" : "Pending"} <span className="font-mono text-sm text-ink-mute ml-2">({items.length})</span>
|
||||
</h2>
|
||||
{items.map((draft) => {
|
||||
const key = `draft:${draft.type}:${draft.id}`;
|
||||
const label = typeLabels[draft.type] ?? { en: draft.type, zh: draft.type };
|
||||
return (
|
||||
<div key={key} className="hairline-t hairline-b hairline-l hairline-r bg-paper">
|
||||
<div className="bg-ink text-paper px-4 py-2 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="font-mono text-xs uppercase tracking-wider text-indigo">
|
||||
{isZh ? label.zh : label.en}
|
||||
</span>
|
||||
{draft.targetNumber && (
|
||||
<span className="font-mono text-xs text-paper-deep/70 tabular">#{draft.targetNumber}</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="font-mono text-xs text-paper-deep/50">
|
||||
{new Date(draft.generatedAt).toISOString().slice(0, 16)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="p-4">
|
||||
{editing === key ? (
|
||||
<div className="space-y-3">
|
||||
<textarea
|
||||
value={editBody}
|
||||
onChange={(e) => setEditBody(e.target.value)}
|
||||
className="w-full h-48 p-3 bg-paper-deep hairline-t hairline-b hairline-l hairline-r font-mono text-sm resize-y"
|
||||
/>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => handleAction(key, "post", editBody)}
|
||||
disabled={loading === key}
|
||||
className="px-4 py-2 bg-indigo text-paper font-mono text-xs uppercase tracking-wider hover:bg-indigo-deep transition-colors disabled:opacity-50"
|
||||
>
|
||||
{isZh ? "确认发布" : "Post edited"}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setEditing(null)}
|
||||
className="px-4 py-2 hairline-t hairline-b hairline-l hairline-r font-mono text-xs uppercase tracking-wider hover:bg-paper-deep transition-colors"
|
||||
>
|
||||
{isZh ? "取消" : "Cancel"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="prose prose-sm max-w-none text-sm text-ink-soft leading-relaxed whitespace-pre-wrap mb-4">
|
||||
{isZh ? draft.bodyZh : draft.bodyEn}
|
||||
</div>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<button
|
||||
onClick={() => handleAction(key, "post")}
|
||||
disabled={loading === key}
|
||||
className="px-4 py-2 bg-ink text-paper font-mono text-xs uppercase tracking-wider hover:bg-indigo transition-colors disabled:opacity-50"
|
||||
>
|
||||
{isZh ? "发布评论" : "Post as comment"}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => startEdit(draft)}
|
||||
className="px-4 py-2 hairline-t hairline-b hairline-l hairline-r font-mono text-xs uppercase tracking-wider hover:bg-paper-deep transition-colors"
|
||||
>
|
||||
{isZh ? "编辑后发布" : "Edit & post"}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleAction(key, "discard")}
|
||||
disabled={loading === key}
|
||||
className="px-4 py-2 font-mono text-xs uppercase tracking-wider text-ink-mute hover:text-indigo transition-colors disabled:opacity-50"
|
||||
>
|
||||
{isZh ? "丢弃" : "Discard"}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Posted drafts */}
|
||||
{postedItems.length > 0 && (
|
||||
<div className="mt-12">
|
||||
<h2 className="font-display text-xl mb-4">
|
||||
{isZh ? "已发布" : "Posted"} <span className="font-mono text-sm text-ink-mute ml-2">({postedItems.length})</span>
|
||||
</h2>
|
||||
{postedItems.map((draft) => {
|
||||
const key = `draft:${draft.type}:${draft.id}`;
|
||||
const label = typeLabels[draft.type] ?? { en: draft.type, zh: draft.type };
|
||||
return (
|
||||
<div key={key} className="hairline-t py-3 px-4 opacity-60">
|
||||
<div className="flex items-center gap-3 mb-1">
|
||||
<span className="font-mono text-xs uppercase tracking-wider text-ink-mute">
|
||||
{isZh ? label.zh : label.en}
|
||||
</span>
|
||||
{draft.targetNumber && (
|
||||
<span className="font-mono text-xs text-ink-mute tabular">#{draft.targetNumber}</span>
|
||||
)}
|
||||
<span className="ml-auto pill pill-jade text-[0.6rem]">{isZh ? "已发布" : "posted"}</span>
|
||||
</div>
|
||||
<p className="text-xs text-ink-mute line-clamp-2">{draft.bodyEn.slice(0, 120)}…</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import { cookies } from "next/headers";
|
||||
import { getAgentEnv, listDrafts, validateSession, type AgentDraft } from "@/lib/community-agent";
|
||||
import { AdminClient } from "./admin-client";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const TYPE_LABELS: Record<string, { en: string; zh: string }> = {
|
||||
triage: { en: "Issue Triage", zh: "议题分类" },
|
||||
"pr-review": { en: "PR Review", zh: "PR 审阅" },
|
||||
stale: { en: "Stale Nudge", zh: "过期提醒" },
|
||||
dupes: { en: "Duplicate", zh: "重复检测" },
|
||||
digest: { en: "Weekly Digest", zh: "每周摘要" },
|
||||
};
|
||||
|
||||
function LoginForm({ locale, error }: { locale: string; error: boolean }) {
|
||||
const isZh = locale === "zh";
|
||||
return (
|
||||
<div className="mx-auto max-w-md px-6 py-20">
|
||||
<h1 className="font-display text-3xl mb-6">
|
||||
{isZh ? "维护者登录" : "Maintainer login"}
|
||||
</h1>
|
||||
<form method="POST" action={`/api/admin/login?locale=${locale}`} autoComplete="off" className="space-y-4">
|
||||
<input type="hidden" name="locale" value={locale} />
|
||||
<label className="block">
|
||||
<span className="eyebrow block mb-2">{isZh ? "令牌" : "Token"}</span>
|
||||
<input
|
||||
type="password"
|
||||
name="token"
|
||||
required
|
||||
autoFocus
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
className="w-full px-3 py-2 hairline-t hairline-b hairline-l hairline-r bg-paper font-mono text-sm focus:outline-none focus:border-indigo"
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full px-5 py-3 bg-ink text-paper font-mono text-sm uppercase tracking-wider hover:bg-indigo transition-colors"
|
||||
>
|
||||
{isZh ? "登录 →" : "Sign in →"}
|
||||
</button>
|
||||
{error && (
|
||||
<p className="text-sm text-indigo font-mono">
|
||||
{isZh ? "令牌错误。" : "Invalid token."}
|
||||
</p>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default async function AdminPage({
|
||||
params,
|
||||
searchParams,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>;
|
||||
searchParams: Promise<{ err?: string }>;
|
||||
}) {
|
||||
const { locale } = await params;
|
||||
const { err } = await searchParams;
|
||||
const isZh = locale === "zh";
|
||||
|
||||
const env = await getAgentEnv();
|
||||
|
||||
if (!env.MAINTAINER_TOKEN) {
|
||||
return (
|
||||
<div className="mx-auto max-w-[1400px] px-6 py-20 text-center">
|
||||
<h1 className="font-display text-3xl mb-4">{isZh ? "未配置" : "Not configured"}</h1>
|
||||
<p className="text-ink-soft">
|
||||
{isZh
|
||||
? "MAINTAINER_TOKEN 未设置。请在部署前配置此环境变量。"
|
||||
: "MAINTAINER_TOKEN is not set. Configure this secret before deployment."}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const cookieStore = await cookies();
|
||||
const sid = cookieStore.get("mt_sid")?.value;
|
||||
const authed = await validateSession(env.CURATED_KV, sid);
|
||||
|
||||
if (!authed) {
|
||||
return <LoginForm locale={locale} error={err === "1"} />;
|
||||
}
|
||||
|
||||
let drafts: AgentDraft[] = [];
|
||||
try {
|
||||
drafts = await listDrafts(env.CURATED_KV);
|
||||
} catch (e) {
|
||||
console.error("failed to list drafts", e);
|
||||
}
|
||||
|
||||
const pending = drafts.filter((d) => !d.posted);
|
||||
const posted = drafts.filter((d) => d.posted);
|
||||
|
||||
return (
|
||||
<section className="mx-auto max-w-[1400px] px-6 pt-12 pb-20">
|
||||
<div className="flex items-baseline justify-between mb-8 hairline-b pb-4">
|
||||
<div>
|
||||
<h1 className="font-display tracking-crisp text-3xl">
|
||||
{isZh ? "社区助理草稿" : "Community Assistant Drafts"}
|
||||
</h1>
|
||||
<p className="mt-2 text-sm text-ink-mute font-mono">
|
||||
{pending.length} pending · {posted.length} posted
|
||||
</p>
|
||||
</div>
|
||||
<form method="POST" action={`/api/admin/logout?locale=${locale}`}>
|
||||
<button
|
||||
type="submit"
|
||||
className="font-mono text-xs text-ink-mute hover:text-indigo uppercase tracking-wider"
|
||||
>
|
||||
{isZh ? "退出 →" : "Sign out →"}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{pending.length === 0 && posted.length === 0 && (
|
||||
<div className="hairline-t hairline-b py-16 text-center">
|
||||
<div className="font-cjk text-indigo text-2xl mb-3">暂无草稿</div>
|
||||
<p className="text-ink-soft">
|
||||
{isZh
|
||||
? "草稿将在 cron 运行后出现。可在 wrangler.jsonc 中配置触发时间。"
|
||||
: "Drafts will appear here after cron runs. Configure triggers in wrangler.jsonc."}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<AdminClient
|
||||
drafts={pending}
|
||||
posted={posted}
|
||||
isZh={isZh}
|
||||
typeLabels={TYPE_LABELS}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
import Link from "next/link";
|
||||
import { Seal } from "@/components/seal";
|
||||
|
||||
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
|
||||
const { locale } = await params;
|
||||
const isZh = locale === "zh";
|
||||
return {
|
||||
title: isZh ? "参与贡献 · DeepSeek TUI" : "Contribute · DeepSeek TUI",
|
||||
description: isZh
|
||||
? "如何提交议题、发送合并请求、加入 deepseek-tui 社区。"
|
||||
: "How to file issues, send pull requests, and join the deepseek-tui community.",
|
||||
};
|
||||
}
|
||||
|
||||
const stepsEn = [
|
||||
{
|
||||
n: "①",
|
||||
title: "Find a thread to pull",
|
||||
cn: "选择切入点",
|
||||
body: "Browse open issues. The good first issue label means the path is clear. The help wanted label means the path is open but contested. Anything else, ask first.",
|
||||
cta: { label: "Open issues", href: "https://github.com/Hmbown/deepseek-tui/issues" },
|
||||
},
|
||||
{
|
||||
n: "②",
|
||||
title: "Fork and branch",
|
||||
cn: "复刻并分支",
|
||||
body: "git clone your fork, then git checkout -b feat/short-name or fix/short-name. We use conventional commits — feat:, fix:, docs:, refactor:, test:, chore:.",
|
||||
cta: { label: "Repo on GitHub", href: "https://github.com/Hmbown/deepseek-tui" },
|
||||
},
|
||||
{
|
||||
n: "③",
|
||||
title: "Match the local checks",
|
||||
cn: "本地检查",
|
||||
body: "CI runs cargo fmt --all -- --check, cargo clippy --workspace --all-targets --all-features --locked -- -D warnings, and cargo test --workspace --all-features --locked. Run them before you push.",
|
||||
cta: { label: "Contributing guide", href: "https://github.com/Hmbown/deepseek-tui/blob/main/CONTRIBUTING.md" },
|
||||
},
|
||||
{
|
||||
n: "④",
|
||||
title: "Open the PR",
|
||||
cn: "提交合并",
|
||||
body: "PR description should explain WHY, not WHAT (the diff covers what). Link the issue. The maintainer reviews everything personally — usually within a day.",
|
||||
cta: { label: "PR template", href: "https://github.com/Hmbown/deepseek-tui/blob/main/.github/PULL_REQUEST_TEMPLATE.md" },
|
||||
},
|
||||
];
|
||||
|
||||
const stepsZh = [
|
||||
{
|
||||
n: "①",
|
||||
title: "选择切入点",
|
||||
cn: "Find a thread",
|
||||
body: "浏览 open issues。good first issue 标签意味着路径清晰。help wanted 标签意味着路径开放但有争议。其他情况请先询问。",
|
||||
cta: { label: "查看议题", href: "https://github.com/Hmbown/deepseek-tui/issues" },
|
||||
},
|
||||
{
|
||||
n: "②",
|
||||
title: "复刻并创建分支",
|
||||
cn: "Fork & branch",
|
||||
body: "git clone 你的复刻,然后 git checkout -b feat/short-name 或 fix/short-name。使用约定式提交——feat:、fix:、docs:、refactor:、test:、chore:。",
|
||||
cta: { label: "GitHub 仓库", href: "https://github.com/Hmbown/deepseek-tui" },
|
||||
},
|
||||
{
|
||||
n: "③",
|
||||
title: "通过本地检查",
|
||||
cn: "Local checks",
|
||||
body: "CI 运行 cargo fmt --all -- --check、cargo clippy --workspace --all-targets --all-features --locked -- -D warnings 和 cargo test --workspace --all-features --locked。推送前请先运行。",
|
||||
cta: { label: "贡献指南", href: "https://github.com/Hmbown/deepseek-tui/blob/main/CONTRIBUTING.md" },
|
||||
},
|
||||
{
|
||||
n: "④",
|
||||
title: "提交 PR",
|
||||
cn: "Open the PR",
|
||||
body: "PR 描述应说明「为什么」而非「做了什么」(diff 已经展示了做了什么)。关联相关 issue。维护者亲自审查所有 PR——通常一天内完成。",
|
||||
cta: { label: "PR 模板", href: "https://github.com/Hmbown/deepseek-tui/blob/main/.github/PULL_REQUEST_TEMPLATE.md" },
|
||||
},
|
||||
];
|
||||
|
||||
export default async function ContributePage({ params }: { params: Promise<{ locale: string }> }) {
|
||||
const { locale } = await params;
|
||||
const isZh = locale === "zh";
|
||||
const steps = isZh ? stepsZh : stepsEn;
|
||||
|
||||
return (
|
||||
<>
|
||||
{isZh ? (
|
||||
<>
|
||||
<section className="mx-auto max-w-[1400px] px-6 pt-12 pb-8">
|
||||
<div className="flex items-baseline gap-4 mb-3">
|
||||
<Seal char="参" />
|
||||
<div className="eyebrow">Section 05 · 参与</div>
|
||||
</div>
|
||||
<h1 className="font-display tracking-crisp">
|
||||
参与贡献 <span className="font-cjk text-indigo text-5xl ml-2">Contribute</span>
|
||||
</h1>
|
||||
<p className="mt-5 max-w-3xl text-ink-soft text-lg leading-[1.9] tracking-wide">
|
||||
无需签署 CLA。没有赞助商优先通道。维护者只有一人;请成为你希望收到的那种贡献者:
|
||||
小而聚焦的 PR、真实的测试覆盖、以及能告诉审查者你在想什么的文字。
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mx-auto max-w-[1400px] px-6 pb-16 hairline-t hairline-b">
|
||||
<ol className="grid md:grid-cols-2 lg:grid-cols-4 gap-0 col-rule">
|
||||
{steps.map((s) => (
|
||||
<li key={s.n} className="p-7">
|
||||
<div className="font-display text-5xl text-indigo mb-3">{s.n}</div>
|
||||
<div className="eyebrow mb-2">{s.cn}</div>
|
||||
<h3 className="font-display text-xl mb-3 leading-tight">{s.title}</h3>
|
||||
<p className="text-sm text-ink-soft leading-[1.9] tracking-wide mb-4">{s.body}</p>
|
||||
<Link href={s.cta.href} className="font-mono text-[0.72rem] uppercase tracking-wider text-indigo hover:underline">
|
||||
{s.cta.label} →
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
{/* 规约 */}
|
||||
<section className="mx-auto max-w-[1400px] px-6 py-16 grid lg:grid-cols-12 gap-10">
|
||||
<div className="lg:col-span-5">
|
||||
<Seal char="规" />
|
||||
<h2 className="font-display text-3xl mt-4">
|
||||
规约 <span className="font-cjk text-indigo text-2xl ml-2">House rules</span>
|
||||
</h2>
|
||||
<p className="text-ink-soft mt-4 leading-[1.9] tracking-wide">
|
||||
简而言之:做实事,别折腾元数据。完整的
|
||||
<Link href="https://github.com/Hmbown/deepseek-tui/blob/main/CODE_OF_CONDUCT.md" className="body-link mx-1">行为准则</Link>
|
||||
是详细版。
|
||||
</p>
|
||||
</div>
|
||||
<div className="lg:col-span-7">
|
||||
<ul className="space-y-3">
|
||||
{[
|
||||
{ k: "欢迎", v: "附带复现步骤的 bug 报告、说明权衡的重构、修复真实歧义的文档 PR。" },
|
||||
{ k: "欢迎", v: "能复现 bug 的测试——甚至比修复本身更有价值。" },
|
||||
{ k: "欢迎", v: "在 Discussions 中提出有深度的问题。带数据更佳。" },
|
||||
{ k: "不欢迎", v: "不理解 diff 的 AI 生成补丁。" },
|
||||
{ k: "不欢迎", v: "在代码库或文档中添加托管 SaaS 依赖、遥测或推荐链接。" },
|
||||
{ k: "不欢迎", v: "按个人偏好跨仓库重命名。" },
|
||||
].map((r, i) => (
|
||||
<li key={i} className="flex gap-4 hairline-b pb-3">
|
||||
<span className={`font-mono text-[0.72rem] uppercase tracking-widest pt-1 w-10 shrink-0 ${r.k === "欢迎" ? "text-jade" : "text-indigo"}`}>
|
||||
{r.k}
|
||||
</span>
|
||||
<span className="text-sm text-ink-soft leading-[1.9] tracking-wide">{r.v}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 开发循环 */}
|
||||
<section className="bg-paper-deep hairline-t hairline-b">
|
||||
<div className="mx-auto max-w-[1400px] px-6 py-16 grid lg:grid-cols-12 gap-10 min-w-0">
|
||||
<div className="lg:col-span-4 min-w-0">
|
||||
<div className="eyebrow mb-3">开发循环 · The dev loop</div>
|
||||
<h2 className="font-display text-3xl">从克隆到合并</h2>
|
||||
<p className="mt-4 text-ink-soft leading-[1.9] tracking-wide">
|
||||
完整流程,可直接复制粘贴。仅限 stable Rust——切勿使用 nightly 特性。
|
||||
</p>
|
||||
</div>
|
||||
<div className="lg:col-span-8 min-w-0">
|
||||
<pre className="code-block">
|
||||
{`# 在 GitHub 上 fork,然后:
|
||||
git clone git@github.com:YOU/deepseek-tui
|
||||
cd deepseek-tui
|
||||
git checkout -b feat/your-thing
|
||||
|
||||
# 本地构建运行
|
||||
cargo build
|
||||
cargo run --bin deepseek
|
||||
|
||||
# 检查(与 CI 完全一致)
|
||||
cargo fmt --all -- --check
|
||||
cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
|
||||
cargo test --workspace --all-features --locked
|
||||
|
||||
# 一致性验证
|
||||
cargo test -p deepseek-tui-core --test snapshot --locked
|
||||
cargo test -p deepseek-protocol --test parity_protocol --locked
|
||||
cargo test -p deepseek-state --test parity_state --locked
|
||||
|
||||
# 提交 + 推送 + PR
|
||||
git commit -m "feat: short subject in conventional-commit form"
|
||||
git push -u origin feat/your-thing
|
||||
gh pr create --fill`}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<section className="mx-auto max-w-[1400px] px-6 pt-12 pb-8">
|
||||
<div className="flex items-baseline gap-4 mb-3">
|
||||
<Seal char="参" />
|
||||
<div className="eyebrow">Section 05 · 参与</div>
|
||||
</div>
|
||||
<h1 className="font-display tracking-crisp">
|
||||
Contribute <span className="font-cjk text-indigo text-5xl ml-2">参与</span>
|
||||
</h1>
|
||||
<p className="mt-5 max-w-3xl text-ink-soft text-lg leading-relaxed">
|
||||
No CLA. No sponsor lockouts. The maintainer is one person; please be the kind of contributor
|
||||
you'd want to receive. Specifically: small focused PRs, real test coverage, and prose that
|
||||
tells the reviewer what you were thinking.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mx-auto max-w-[1400px] px-6 pb-16 hairline-t hairline-b">
|
||||
<ol className="grid md:grid-cols-2 lg:grid-cols-4 gap-0 col-rule">
|
||||
{steps.map((s) => (
|
||||
<li key={s.n} className="p-7">
|
||||
<div className="font-display text-5xl text-indigo mb-3">{s.n}</div>
|
||||
<div className="eyebrow mb-2">{s.cn}</div>
|
||||
<h3 className="font-display text-xl mb-3 leading-tight">{s.title}</h3>
|
||||
<p className="text-sm text-ink-soft leading-relaxed mb-4">{s.body}</p>
|
||||
<Link href={s.cta.href} className="font-mono text-[0.72rem] uppercase tracking-wider text-indigo hover:underline">
|
||||
{s.cta.label} →
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<section className="mx-auto max-w-[1400px] px-6 py-16 grid lg:grid-cols-12 gap-10">
|
||||
<div className="lg:col-span-5">
|
||||
<Seal char="规" />
|
||||
<h2 className="font-display text-3xl mt-4">
|
||||
House rules <span className="font-cjk text-indigo text-2xl ml-2">规约</span>
|
||||
</h2>
|
||||
<p className="text-ink-soft mt-4 leading-relaxed">
|
||||
Short version: build the thing, don't polish the meta. The full
|
||||
<Link href="https://github.com/Hmbown/deepseek-tui/blob/main/CODE_OF_CONDUCT.md" className="body-link mx-1">Code of Conduct</Link>
|
||||
is the long version.
|
||||
</p>
|
||||
</div>
|
||||
<div className="lg:col-span-7">
|
||||
<ul className="space-y-3">
|
||||
{[
|
||||
{ k: "Yes", v: "Bug reports with reproductions, refactors that explain the trade-off, docs PRs that fix a real ambiguity." },
|
||||
{ k: "Yes", v: "Tests that demonstrate the bug — even better than fixes." },
|
||||
{ k: "Yes", v: "Hard questions in Discussions. Even better if you bring data." },
|
||||
{ k: "No", v: "Drive-by AI-generated patches with no understanding of the diff." },
|
||||
{ k: "No", v: "Adding hosted SaaS dependencies, telemetry, or referral links to the codebase or docs." },
|
||||
{ k: "No", v: "Renaming things across the repo to match your preferences." },
|
||||
].map((r, i) => (
|
||||
<li key={i} className="flex gap-4 hairline-b pb-3">
|
||||
<span className={`font-mono text-[0.72rem] uppercase tracking-widest pt-1 w-10 shrink-0 ${r.k === "Yes" ? "text-jade" : "text-indigo"}`}>
|
||||
{r.k}
|
||||
</span>
|
||||
<span className="text-sm text-ink-soft leading-relaxed">{r.v}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="bg-paper-deep hairline-t hairline-b">
|
||||
<div className="mx-auto max-w-[1400px] px-6 py-16 grid lg:grid-cols-12 gap-10 min-w-0">
|
||||
<div className="lg:col-span-4 min-w-0">
|
||||
<div className="eyebrow mb-3">The dev loop · 开发循环</div>
|
||||
<h2 className="font-display text-3xl">From clone to merged</h2>
|
||||
<p className="mt-4 text-ink-soft leading-relaxed">
|
||||
The full sequence, copy-pasteable. Stable Rust only — never reach for nightly features.
|
||||
</p>
|
||||
</div>
|
||||
<div className="lg:col-span-8 min-w-0">
|
||||
<pre className="code-block">
|
||||
{`# fork on github, then:
|
||||
git clone git@github.com:YOU/deepseek-tui
|
||||
cd deepseek-tui
|
||||
git checkout -b feat/your-thing
|
||||
|
||||
# build and run locally
|
||||
cargo build
|
||||
cargo run --bin deepseek
|
||||
|
||||
# checks (matches CI exactly)
|
||||
cargo fmt --all -- --check
|
||||
cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
|
||||
cargo test --workspace --all-features --locked
|
||||
|
||||
# parity gates
|
||||
cargo test -p deepseek-tui-core --test snapshot --locked
|
||||
cargo test -p deepseek-protocol --test parity_protocol --locked
|
||||
cargo test -p deepseek-state --test parity_state --locked
|
||||
|
||||
# commit + push + PR
|
||||
git commit -m "feat: short subject in conventional-commit form"
|
||||
git push -u origin feat/your-thing
|
||||
gh pr create --fill`}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,498 @@
|
||||
import Link from "next/link";
|
||||
import { Seal } from "@/components/seal";
|
||||
import { getFacts } from "@/lib/facts";
|
||||
|
||||
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
|
||||
const { locale } = await params;
|
||||
const isZh = locale === "zh";
|
||||
return {
|
||||
title: isZh ? "文档 · DeepSeek TUI" : "Docs · DeepSeek TUI",
|
||||
description: isZh
|
||||
? "DeepSeek TUI 的工作原理:模式、工具、沙箱、MCP、配置、钩子。"
|
||||
: "How DeepSeek TUI works: modes, tools, sandbox, MCP, config, hooks.",
|
||||
};
|
||||
}
|
||||
|
||||
const sectionsEn = [
|
||||
{ id: "modes", label: "Modes" },
|
||||
{ id: "tools", label: "Tools" },
|
||||
{ id: "approval", label: "Approval & Sandbox" },
|
||||
{ id: "config", label: "Configuration" },
|
||||
{ id: "mcp", label: "MCP" },
|
||||
{ id: "skills", label: "Skills" },
|
||||
{ id: "providers", label: "Providers" },
|
||||
{ id: "shortcuts", label: "Shortcuts" },
|
||||
];
|
||||
|
||||
const sectionsZh = [
|
||||
{ id: "modes", label: "模式" },
|
||||
{ id: "tools", label: "工具" },
|
||||
{ id: "approval", label: "审批与沙箱" },
|
||||
{ id: "config", label: "配置" },
|
||||
{ id: "mcp", label: "MCP" },
|
||||
{ id: "skills", label: "技能" },
|
||||
{ id: "providers", label: "提供商" },
|
||||
{ id: "shortcuts", label: "快捷键" },
|
||||
];
|
||||
|
||||
export default async function DocsPage({ params }: { params: Promise<{ locale: string }> }) {
|
||||
const { locale } = await params;
|
||||
const isZh = locale === "zh";
|
||||
const sections = isZh ? sectionsZh : sectionsEn;
|
||||
const facts = await getFacts();
|
||||
|
||||
return (
|
||||
<>
|
||||
{isZh ? (
|
||||
<>
|
||||
<section className="mx-auto max-w-[1400px] px-6 pt-12 pb-8">
|
||||
<div className="flex items-baseline gap-4 mb-3">
|
||||
<Seal char="文" />
|
||||
<div className="eyebrow">Section 02 · 文档</div>
|
||||
</div>
|
||||
<h1 className="font-display tracking-crisp">
|
||||
文档 <span className="font-cjk text-indigo text-5xl ml-2">Documentation</span>
|
||||
</h1>
|
||||
<p className="mt-5 max-w-3xl text-ink-soft text-lg leading-[1.9] tracking-wide">
|
||||
工作原理简述。完整的架构讲解请参阅仓库中的
|
||||
<Link href="https://github.com/Hmbown/deepseek-tui/blob/main/docs/ARCHITECTURE.md" className="body-link mx-1">docs/ARCHITECTURE.md</Link>。
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mx-auto max-w-[1400px] px-6 pb-16 grid lg:grid-cols-12 gap-10 min-w-0">
|
||||
<aside className="lg:col-span-3 min-w-0">
|
||||
<div className="lg:sticky lg:top-32">
|
||||
<div className="eyebrow mb-3">本页目录 · On this page</div>
|
||||
<ul className="space-y-1.5 hairline-t hairline-b py-3">
|
||||
{sections.map((s) => (
|
||||
<li key={s.id}>
|
||||
<a href={`#${s.id}`} className="text-sm hover:text-indigo block py-0.5">
|
||||
<span className="font-mono text-[0.7rem] text-ink-mute mr-2 tabular">§</span>
|
||||
{s.label}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<article className="lg:col-span-9 space-y-14 min-w-0">
|
||||
|
||||
{/* 模式 */}
|
||||
<section id="modes" className="scroll-mt-32">
|
||||
<h2 className="font-display text-3xl mb-1">
|
||||
模式 <span className="font-cjk text-indigo text-2xl ml-2">Modes</span>
|
||||
</h2>
|
||||
<p className="text-ink-soft mt-3 leading-[1.9] tracking-wide">
|
||||
三种运行模式——与审批系统正交。按{" "}
|
||||
<kbd className="font-mono text-xs px-1.5 py-0.5 hairline-t hairline-b hairline-l hairline-r">Tab</kbd> 切换。
|
||||
</p>
|
||||
<div className="grid md:grid-cols-3 gap-0 col-rule hairline-t hairline-b mt-6">
|
||||
{[
|
||||
{ name: "Plan", cn: "计划", color: "text-cobalt", desc: "只读调查。可以 grep、读文件、列目录、抓取 URL——不能写入或执行 shell。" },
|
||||
{ name: "Agent", cn: "代理", color: "text-jade", desc: "默认模式。多步工具调用。Shell 和有副作用的工具需按 approval_mode 设置审批。" },
|
||||
{ name: "YOLO", cn: "全权", color: "text-indigo", desc: "自动批准所有操作并启用信任模式。工作区边界解除。请谨慎使用。" },
|
||||
].map((m) => (
|
||||
<div key={m.name} className="p-5">
|
||||
<div className={`font-display text-xl ${m.color} mb-1`}>
|
||||
{m.name} <span className="font-cjk text-base ml-1.5">{m.cn}</span>
|
||||
</div>
|
||||
<p className="text-sm text-ink-soft leading-[1.9] tracking-wide">{m.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 工具 */}
|
||||
<section id="tools" className="scroll-mt-32">
|
||||
<h2 className="font-display text-3xl mb-1">
|
||||
工具 <span className="font-cjk text-indigo text-2xl ml-2">Tools</span>
|
||||
</h2>
|
||||
<p className="text-ink-soft mt-3 leading-[1.9] tracking-wide">
|
||||
精选工具集——设计思路详见 <code className="inline">docs/TOOL_SURFACE.md</code>。
|
||||
</p>
|
||||
<div className="hairline-t hairline-b mt-6">
|
||||
{[
|
||||
{ group: "文件操作", tools: "read_file · list_dir · write_file · edit_file · apply_patch" },
|
||||
{ group: "搜索", tools: "grep_files · file_search · web_search · fetch_url" },
|
||||
{ group: "Shell", tools: "exec_shell · exec_shell_wait · exec_shell_interact" },
|
||||
{ group: "Git / 诊断 / 测试", tools: "git_status · git_diff · diagnostics · run_tests" },
|
||||
{ group: "子 Agent", tools: "agent_spawn · agent_wait · agent_result · agent_cancel · agent_list · agent_send_input · agent_resume · agent_assign" },
|
||||
{ group: "递归 LM", tools: "rlm——沙箱 Python REPL,内置 llm_query()/rlm_query() 用于长文本分块处理" },
|
||||
{ group: "MCP", tools: "mcp_<server>_<tool>——从 ~/.deepseek/mcp.json 自动注册" },
|
||||
].map((row) => (
|
||||
<div key={row.group} className="grid md:grid-cols-12 gap-0 hairline-t py-3 px-4 hover:bg-paper-deep transition-colors min-w-0">
|
||||
<div className="md:col-span-3 font-display text-sm font-semibold">{row.group}</div>
|
||||
<div className="md:col-span-9 font-mono text-[0.78rem] text-ink-soft leading-relaxed break-words min-w-0">{row.tools}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 审批 */}
|
||||
<section id="approval" className="scroll-mt-32">
|
||||
<h2 className="font-display text-3xl mb-1">
|
||||
审批与沙箱 <span className="font-cjk text-indigo text-2xl ml-2">Approval</span>
|
||||
</h2>
|
||||
<p className="text-ink-soft mt-3 leading-[1.9] tracking-wide">
|
||||
模式与审批是两个独立的维度。通过 <code className="inline">/config</code> 设置。
|
||||
</p>
|
||||
<div className="hairline-t hairline-b mt-6 grid md:grid-cols-3 col-rule">
|
||||
{[
|
||||
{ name: "suggest", cn: "建议", desc: "默认——按模式规则执行。危险操作前询问。" },
|
||||
{ name: "auto", cn: "自动", desc: "自动批准所有工具调用。等同于无信任的 YOLO。" },
|
||||
{ name: "never", cn: "拒绝", desc: "阻止任何非安全/非只读操作。仅限调查。" },
|
||||
].map((a) => (
|
||||
<div key={a.name} className="p-5">
|
||||
<div className="font-mono text-sm text-indigo uppercase tracking-wider">{a.name} · <span className="font-cjk normal-case tracking-normal">{a.cn}</span></div>
|
||||
<p className="text-sm text-ink-soft mt-2 leading-[1.9] tracking-wide">{a.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-5 text-ink-soft leading-[1.9] tracking-wide">
|
||||
沙箱:{facts.sandboxBackends.join("、")}。工作区边界默认为 <code className="inline">--workspace</code>。
|
||||
<code className="inline">/trust</code> 可解除边界限制。
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* 配置 */}
|
||||
<section id="config" className="scroll-mt-32">
|
||||
<h2 className="font-display text-3xl mb-1">
|
||||
配置 <span className="font-cjk text-indigo text-2xl ml-2">Configuration</span>
|
||||
</h2>
|
||||
<pre className="code-block mt-5">
|
||||
{`# ~/.deepseek/config.toml
|
||||
[api]
|
||||
key = "sk-..."
|
||||
base_url = "https://api.deepseek.com"
|
||||
model = "${facts.defaultModel ?? "deepseek-v4-pro"}" # 默认模型;deepseek-v4-flash 用于快速 / 子智能体
|
||||
|
||||
[ui]
|
||||
default_mode = "agent" # plan | agent | yolo
|
||||
approval_mode = "suggest" # suggest | auto | never
|
||||
reasoning_effort = "high" # off | high | max
|
||||
|
||||
[hooks]
|
||||
enabled = true
|
||||
default_timeout_secs = 30
|
||||
|
||||
[[hooks.hooks]]
|
||||
event = "session_start" # 也支持: tool_call_before / tool_call_after
|
||||
command = "~/.deepseek/hooks/pre.sh" # / message_submit / mode_change / on_error / shell_env`}
|
||||
</pre>
|
||||
<p className="mt-4 text-sm text-ink-soft">
|
||||
完整参考:<Link className="body-link" href="https://github.com/Hmbown/deepseek-tui/blob/main/config.example.toml">config.example.toml</Link>。
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* MCP */}
|
||||
<section id="mcp" className="scroll-mt-32">
|
||||
<h2 className="font-display text-3xl mb-1">
|
||||
MCP 服务器 <span className="font-cjk text-indigo text-2xl ml-2">MCP</span>
|
||||
</h2>
|
||||
<p className="text-ink-soft mt-3 leading-[1.9] tracking-wide">
|
||||
<code className="inline">deepseek</code> 双向支持模型上下文协议(Model Context Protocol):作为客户端从
|
||||
<code className="inline">~/.deepseek/mcp.json</code> 加载服务器,同时也可作为服务器暴露工具
|
||||
(<code className="inline">deepseek mcp</code>)。工具以 <code className="inline">mcp_<server>_<tool></code> 形式呈现。
|
||||
</p>
|
||||
<pre className="code-block mt-5">
|
||||
{`{
|
||||
"mcpServers": {
|
||||
"filesystem": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/me"]
|
||||
},
|
||||
"sqlite": {
|
||||
"command": "uvx",
|
||||
"args": ["mcp-server-sqlite", "--db-path", "./data.db"]
|
||||
}
|
||||
}
|
||||
}`}
|
||||
</pre>
|
||||
</section>
|
||||
|
||||
{/* 技能 */}
|
||||
<section id="skills" className="scroll-mt-32">
|
||||
<h2 className="font-display text-3xl mb-1">
|
||||
技能 <span className="font-cjk text-indigo text-2xl ml-2">Skills</span>
|
||||
</h2>
|
||||
<p className="text-ink-soft mt-3 leading-[1.9] tracking-wide">
|
||||
技能是 <code className="inline">~/.deepseek/skills/<name>/</code> 下的一个文件夹,
|
||||
根目录包含 <code className="inline">SKILL.md</code>。Agent 启动时加载技能名称和描述,
|
||||
在需要时通过 Skill 工具拉取完整内容。
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* 提供商 */}
|
||||
<section id="providers" className="scroll-mt-32">
|
||||
<h2 className="font-display text-3xl mb-1">
|
||||
提供商 <span className="font-cjk text-indigo text-2xl ml-2">Providers</span>
|
||||
</h2>
|
||||
<p className="text-ink-soft mt-3 leading-[1.9] tracking-wide">
|
||||
使用 <code className="inline">deepseek auth set --provider <id></code> 切换。下表为
|
||||
<code className="inline">crates/tui/src/config.rs</code> 中 <code className="inline">ApiProvider</code> 枚举的实时投影
|
||||
,目前共 {facts.providers.length} 个。
|
||||
</p>
|
||||
<div className="hairline-t hairline-b mt-5">
|
||||
{facts.providers.map((p) => (
|
||||
<div key={p.id} className="grid md:grid-cols-12 gap-0 hairline-t py-3 px-4 hover:bg-paper-deep min-w-0">
|
||||
<div className="md:col-span-3 font-display font-semibold">{p.label}</div>
|
||||
<div className="md:col-span-3 font-mono text-[0.78rem] text-ink-soft break-words min-w-0">{p.id}</div>
|
||||
<div className="md:col-span-6 font-mono text-[0.78rem] text-ink-soft break-words min-w-0">{p.env}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 快捷键 */}
|
||||
<section id="shortcuts" className="scroll-mt-32">
|
||||
<h2 className="font-display text-3xl mb-1">
|
||||
快捷键 <span className="font-cjk text-indigo text-2xl ml-2">Shortcuts</span>
|
||||
</h2>
|
||||
<div className="hairline-t hairline-b mt-5 grid md:grid-cols-2 col-rule">
|
||||
{[
|
||||
{ k: "Tab", v: "切换模式(Plan / Agent / YOLO)" },
|
||||
{ k: "Shift+Tab", v: "切换推理强度" },
|
||||
{ k: "Ctrl+L", v: "清屏,保留会话" },
|
||||
{ k: "Ctrl+C", v: "取消当前轮次" },
|
||||
{ k: "Ctrl+D", v: "退出" },
|
||||
{ k: "/help", v: "斜杠命令面板" },
|
||||
{ k: "/config", v: "交互式编辑配置" },
|
||||
{ k: "/trust", v: "解除本会话的工作区边界" },
|
||||
].map((s) => (
|
||||
<div key={s.k} className="p-4 flex items-center gap-4 hairline-t">
|
||||
<kbd className="font-mono text-xs px-2 py-1 hairline-t hairline-b hairline-l hairline-r bg-paper-deep min-w-[5rem] text-center">{s.k}</kbd>
|
||||
<span className="text-sm text-ink-soft">{s.v}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</article>
|
||||
</section>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<section className="mx-auto max-w-[1400px] px-6 pt-12 pb-8">
|
||||
<div className="flex items-baseline gap-4 mb-3">
|
||||
<Seal char="文" />
|
||||
<div className="eyebrow">Section 02 · 文档</div>
|
||||
</div>
|
||||
<h1 className="font-display tracking-crisp">
|
||||
Documentation <span className="font-cjk text-indigo text-5xl ml-2">文档</span>
|
||||
</h1>
|
||||
<p className="mt-5 max-w-3xl text-ink-soft text-lg leading-relaxed">
|
||||
The short version of how it works. For the full architecture walk-through, see
|
||||
<Link href="https://github.com/Hmbown/deepseek-tui/blob/main/docs/ARCHITECTURE.md" className="body-link mx-1">docs/ARCHITECTURE.md</Link>
|
||||
in the repo.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mx-auto max-w-[1400px] px-6 pb-16 grid lg:grid-cols-12 gap-10 min-w-0">
|
||||
<aside className="lg:col-span-3 min-w-0">
|
||||
<div className="lg:sticky lg:top-32">
|
||||
<div className="eyebrow mb-3">On this page · 目录</div>
|
||||
<ul className="space-y-1.5 hairline-t hairline-b py-3">
|
||||
{sections.map((s) => (
|
||||
<li key={s.id}>
|
||||
<a href={`#${s.id}`} className="text-sm hover:text-indigo block py-0.5">
|
||||
<span className="font-mono text-[0.7rem] text-ink-mute mr-2 tabular">§</span>
|
||||
{s.label}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<article className="lg:col-span-9 space-y-14 min-w-0">
|
||||
|
||||
<section id="modes" className="scroll-mt-32">
|
||||
<h2 className="font-display text-3xl mb-1">
|
||||
Modes <span className="font-cjk text-indigo text-2xl ml-2">模式</span>
|
||||
</h2>
|
||||
<p className="text-ink-soft mt-3 leading-relaxed">
|
||||
Three operating modes — orthogonal to the approval system. Cycle with{" "}
|
||||
<kbd className="font-mono text-xs px-1.5 py-0.5 hairline-t hairline-b hairline-l hairline-r">Tab</kbd>.
|
||||
</p>
|
||||
<div className="grid md:grid-cols-3 gap-0 col-rule hairline-t hairline-b mt-6">
|
||||
{[
|
||||
{ name: "Plan", cn: "计划", color: "text-cobalt", desc: "Read-only investigation. The agent can grep, read files, list dirs, fetch URLs — never write or shell out." },
|
||||
{ name: "Agent", cn: "代理", color: "text-jade", desc: "Default. Multi-step tool use. Shell and side-effectful tools require approval per `approval_mode` setting." },
|
||||
{ name: "YOLO", cn: "全权", color: "text-indigo", desc: "Auto-approve everything + enable trust mode. Workspace boundary lifts. Use carefully." },
|
||||
].map((m) => (
|
||||
<div key={m.name} className="p-5">
|
||||
<div className={`font-display text-xl ${m.color} mb-1`}>
|
||||
{m.name} <span className="font-cjk text-base ml-1.5">{m.cn}</span>
|
||||
</div>
|
||||
<p className="text-sm text-ink-soft leading-relaxed">{m.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="tools" className="scroll-mt-32">
|
||||
<h2 className="font-display text-3xl mb-1">
|
||||
Tools <span className="font-cjk text-indigo text-2xl ml-2">工具</span>
|
||||
</h2>
|
||||
<p className="text-ink-soft mt-3 leading-relaxed">
|
||||
Curated surface — see <code className="inline">docs/TOOL_SURFACE.md</code> for design rationale.
|
||||
</p>
|
||||
<div className="hairline-t hairline-b mt-6">
|
||||
{[
|
||||
{ group: "File ops", tools: "read_file · list_dir · write_file · edit_file · apply_patch" },
|
||||
{ group: "Search", tools: "grep_files · file_search · web_search · fetch_url" },
|
||||
{ group: "Shell", tools: "exec_shell · exec_shell_wait · exec_shell_interact" },
|
||||
{ group: "Git / diag / test", tools: "git_status · git_diff · diagnostics · run_tests" },
|
||||
{ group: "Sub-agents", tools: "agent_spawn · agent_wait · agent_result · agent_cancel · agent_list · agent_send_input · agent_resume · agent_assign" },
|
||||
{ group: "Recursive LM", tools: "rlm — sandboxed Python REPL with llm_query()/rlm_query() for chunked processing of long inputs" },
|
||||
{ group: "MCP", tools: "mcp_<server>_<tool> — auto-registered from ~/.deepseek/mcp.json" },
|
||||
].map((row) => (
|
||||
<div key={row.group} className="grid md:grid-cols-12 gap-0 hairline-t py-3 px-4 hover:bg-paper-deep transition-colors min-w-0">
|
||||
<div className="md:col-span-3 font-display text-sm font-semibold">{row.group}</div>
|
||||
<div className="md:col-span-9 font-mono text-[0.78rem] text-ink-soft leading-relaxed break-words min-w-0">{row.tools}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="approval" className="scroll-mt-32">
|
||||
<h2 className="font-display text-3xl mb-1">
|
||||
Approval & Sandbox <span className="font-cjk text-indigo text-2xl ml-2">审批</span>
|
||||
</h2>
|
||||
<p className="text-ink-soft mt-3 leading-relaxed">
|
||||
Mode and approval are independent axes. Set via <code className="inline">/config</code>.
|
||||
</p>
|
||||
<div className="hairline-t hairline-b mt-6 grid md:grid-cols-3 col-rule">
|
||||
{[
|
||||
{ name: "suggest", cn: "建议", desc: "Default — uses per-mode rules. Asks before risky ops." },
|
||||
{ name: "auto", cn: "自动", desc: "Auto-approve all tool calls. Equivalent to YOLO without trust." },
|
||||
{ name: "never", cn: "拒绝", desc: "Block anything not safe/read-only. Investigation only." },
|
||||
].map((a) => (
|
||||
<div key={a.name} className="p-5">
|
||||
<div className="font-mono text-sm text-indigo uppercase tracking-wider">{a.name} · <span className="font-cjk normal-case tracking-normal">{a.cn}</span></div>
|
||||
<p className="text-sm text-ink-soft mt-2 leading-relaxed">{a.desc}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-5 text-ink-soft leading-relaxed">
|
||||
Sandbox: {facts.sandboxBackends.join(", ")}. Workspace boundary defaults to{" "}
|
||||
<code className="inline">--workspace</code>. <code className="inline">/trust</code> lifts the boundary.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="config" className="scroll-mt-32">
|
||||
<h2 className="font-display text-3xl mb-1">
|
||||
Configuration <span className="font-cjk text-indigo text-2xl ml-2">配置</span>
|
||||
</h2>
|
||||
<pre className="code-block mt-5">
|
||||
{`# ~/.deepseek/config.toml
|
||||
[api]
|
||||
key = "sk-..."
|
||||
base_url = "https://api.deepseek.com"
|
||||
model = "${facts.defaultModel ?? "deepseek-v4-pro"}" # default; deepseek-v4-flash is the fast / sub-agent option
|
||||
|
||||
[ui]
|
||||
default_mode = "agent" # plan | agent | yolo
|
||||
approval_mode = "suggest" # suggest | auto | never
|
||||
reasoning_effort = "high" # off | high | max
|
||||
|
||||
[hooks]
|
||||
enabled = true
|
||||
default_timeout_secs = 30
|
||||
|
||||
[[hooks.hooks]]
|
||||
event = "session_start" # or: tool_call_before / tool_call_after
|
||||
command = "~/.deepseek/hooks/pre.sh" # / message_submit / mode_change / on_error / shell_env`}
|
||||
</pre>
|
||||
<p className="mt-4 text-sm text-ink-soft">
|
||||
Full reference: <Link className="body-link" href="https://github.com/Hmbown/deepseek-tui/blob/main/config.example.toml">config.example.toml</Link>.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="mcp" className="scroll-mt-32">
|
||||
<h2 className="font-display text-3xl mb-1">
|
||||
MCP Servers <span className="font-cjk text-indigo text-2xl ml-2">MCP</span>
|
||||
</h2>
|
||||
<p className="text-ink-soft mt-3 leading-relaxed">
|
||||
<code className="inline">deepseek</code> speaks the Model Context Protocol both ways: as a client (loads
|
||||
servers from <code className="inline">~/.deepseek/mcp.json</code>) and as a server
|
||||
(<code className="inline">deepseek mcp</code>). Tools surface as <code className="inline">mcp_<server>_<tool></code>.
|
||||
</p>
|
||||
<pre className="code-block mt-5">
|
||||
{`{
|
||||
"mcpServers": {
|
||||
"filesystem": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/me"]
|
||||
},
|
||||
"sqlite": {
|
||||
"command": "uvx",
|
||||
"args": ["mcp-server-sqlite", "--db-path", "./data.db"]
|
||||
}
|
||||
}
|
||||
}`}
|
||||
</pre>
|
||||
</section>
|
||||
|
||||
<section id="skills" className="scroll-mt-32">
|
||||
<h2 className="font-display text-3xl mb-1">
|
||||
Skills <span className="font-cjk text-indigo text-2xl ml-2">技能</span>
|
||||
</h2>
|
||||
<p className="text-ink-soft mt-3 leading-relaxed">
|
||||
A skill is a folder under <code className="inline">~/.deepseek/skills/<name>/</code>
|
||||
with a <code className="inline">SKILL.md</code> at the root. The agent loads skill names + descriptions on
|
||||
startup and can pull in the full body via the Skill tool when relevant.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="providers" className="scroll-mt-32">
|
||||
<h2 className="font-display text-3xl mb-1">
|
||||
Providers <span className="font-cjk text-indigo text-2xl ml-2">提供商</span>
|
||||
</h2>
|
||||
<p className="text-ink-soft mt-3 leading-relaxed">
|
||||
Switch with <code className="inline">deepseek auth set --provider <id></code>. The
|
||||
table below is a live projection of the <code className="inline">ApiProvider</code> enum
|
||||
in <code className="inline">crates/tui/src/config.rs</code> — currently {facts.providers.length} providers.
|
||||
</p>
|
||||
<div className="hairline-t hairline-b mt-5">
|
||||
{facts.providers.map((p) => (
|
||||
<div key={p.id} className="grid md:grid-cols-12 gap-0 hairline-t py-3 px-4 hover:bg-paper-deep min-w-0">
|
||||
<div className="md:col-span-3 font-display font-semibold">{p.label}</div>
|
||||
<div className="md:col-span-3 font-mono text-[0.78rem] text-ink-soft break-words min-w-0">{p.id}</div>
|
||||
<div className="md:col-span-6 font-mono text-[0.78rem] text-ink-soft break-words min-w-0">{p.env}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="shortcuts" className="scroll-mt-32">
|
||||
<h2 className="font-display text-3xl mb-1">
|
||||
Shortcuts <span className="font-cjk text-indigo text-2xl ml-2">快捷键</span>
|
||||
</h2>
|
||||
<div className="hairline-t hairline-b mt-5 grid md:grid-cols-2 col-rule">
|
||||
{[
|
||||
{ k: "Tab", v: "Cycle mode (Plan / Agent / YOLO)" },
|
||||
{ k: "Shift+Tab", v: "Cycle reasoning effort" },
|
||||
{ k: "Ctrl+L", v: "Clear screen, keep session" },
|
||||
{ k: "Ctrl+C", v: "Cancel current turn" },
|
||||
{ k: "Ctrl+D", v: "Exit" },
|
||||
{ k: "/help", v: "Slash command palette" },
|
||||
{ k: "/config", v: "Edit config interactively" },
|
||||
{ k: "/trust", v: "Lift workspace boundary for session" },
|
||||
].map((s) => (
|
||||
<div key={s.k} className="p-4 flex items-center gap-4 hairline-t">
|
||||
<kbd className="font-mono text-xs px-2 py-1 hairline-t hairline-b hairline-l hairline-r bg-paper-deep min-w-[5rem] text-center">{s.k}</kbd>
|
||||
<span className="text-sm text-ink-soft">{s.v}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</article>
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
import Link from "next/link";
|
||||
import { Seal } from "@/components/seal";
|
||||
import { FeedCard } from "@/components/feed-card";
|
||||
import { fetchFeed } from "@/lib/github";
|
||||
import { getEnv } from "@/lib/kv";
|
||||
import type { FeedItem } from "@/lib/types";
|
||||
|
||||
export const revalidate = 600;
|
||||
|
||||
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
|
||||
const { locale } = await params;
|
||||
const isZh = locale === "zh";
|
||||
return {
|
||||
title: isZh ? "动态 · DeepSeek TUI" : "Activity · DeepSeek TUI",
|
||||
description: isZh
|
||||
? "来自 Hmbown/deepseek-tui GitHub 仓库的议题、合并请求和发布的实时动态。"
|
||||
: "Live feed of issues, pull requests, and releases mirrored from the Hmbown/deepseek-tui GitHub repo.",
|
||||
};
|
||||
}
|
||||
|
||||
export default async function FeedPage({ params }: { params: Promise<{ locale: string }> }) {
|
||||
const { locale } = await params;
|
||||
const isZh = locale === "zh";
|
||||
|
||||
const env = await getEnv();
|
||||
let feed: FeedItem[] = [];
|
||||
try {
|
||||
feed = await fetchFeed(env.GITHUB_TOKEN, 50);
|
||||
} catch (e) {
|
||||
console.error("feed fetch failed", e);
|
||||
}
|
||||
|
||||
const issues = feed.filter((f) => f.kind === "issue");
|
||||
const pulls = feed.filter((f) => f.kind === "pull");
|
||||
|
||||
return (
|
||||
<>
|
||||
{isZh ? (
|
||||
<>
|
||||
<section className="mx-auto max-w-[1400px] px-6 pt-12 pb-8">
|
||||
<div className="flex items-baseline gap-4 mb-3">
|
||||
<Seal char="动" />
|
||||
<div className="eyebrow">Section 03 · 动态</div>
|
||||
</div>
|
||||
<h1 className="font-display tracking-crisp">
|
||||
动态 <span className="font-cjk text-indigo text-5xl ml-2">Activity</span>
|
||||
</h1>
|
||||
<p className="mt-5 max-w-3xl text-ink-soft text-lg leading-[1.9] tracking-wide">
|
||||
来自{" "}
|
||||
<Link href="https://github.com/Hmbown/deepseek-tui" className="body-link">Hmbown/deepseek-tui</Link>
|
||||
{" "}的议题与合并请求实时镜像。每十分钟刷新一次。点击任意条目跳转至 GitHub。
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mx-auto max-w-[1400px] px-6 pb-16 grid lg:grid-cols-12 gap-10">
|
||||
<div className="lg:col-span-6">
|
||||
<div className="hairline-t hairline-b hairline-l hairline-r bg-paper">
|
||||
<div className="bg-indigo text-paper px-4 py-3 flex items-baseline justify-between">
|
||||
<div className="font-cjk text-base tracking-wider">合并请求 · Pull Requests</div>
|
||||
<span className="font-mono text-[0.7rem] uppercase tabular tracking-widest">{pulls.length} 条</span>
|
||||
</div>
|
||||
<div className="px-4">
|
||||
{pulls.length > 0 ? (
|
||||
pulls.map((p) => <FeedCard key={p.url} item={p} />)
|
||||
) : (
|
||||
<div className="py-10 text-center text-sm font-mono text-ink-mute">暂无数据 · feed not loaded</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-6">
|
||||
<div className="hairline-t hairline-b hairline-l hairline-r bg-paper">
|
||||
<div className="bg-ink text-paper px-4 py-3 flex items-baseline justify-between">
|
||||
<div className="font-cjk text-base tracking-wider">议题 · Issues</div>
|
||||
<span className="font-mono text-[0.7rem] uppercase tabular tracking-widest">{issues.length} 条</span>
|
||||
</div>
|
||||
<div className="px-4">
|
||||
{issues.length > 0 ? (
|
||||
issues.map((i) => <FeedCard key={i.url} item={i} />)
|
||||
) : (
|
||||
<div className="py-10 text-center text-sm font-mono text-ink-mute">暂无数据 · feed not loaded</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="bg-paper-deep hairline-t hairline-b">
|
||||
<div className="mx-auto max-w-[1400px] px-6 py-10 grid md:grid-cols-3 gap-6 text-center">
|
||||
<Link href="https://github.com/Hmbown/deepseek-tui/issues/new/choose" className="hairline-t hairline-b hairline-l hairline-r bg-paper p-6 hover:bg-indigo hover:text-paper transition-colors">
|
||||
<div className="font-display text-xl mb-1">提交议题</div>
|
||||
<div className="font-cjk text-sm text-ink-mute">Open an issue</div>
|
||||
</Link>
|
||||
<Link href="https://github.com/Hmbown/deepseek-tui/compare" className="hairline-t hairline-b hairline-l hairline-r bg-paper p-6 hover:bg-indigo hover:text-paper transition-colors">
|
||||
<div className="font-display text-xl mb-1">提交合并请求</div>
|
||||
<div className="font-cjk text-sm text-ink-mute">Open a PR</div>
|
||||
</Link>
|
||||
<Link href="https://github.com/Hmbown/deepseek-tui/discussions/new" className="hairline-t hairline-b hairline-l hairline-r bg-paper p-6 hover:bg-indigo hover:text-paper transition-colors">
|
||||
<div className="font-display text-xl mb-1">发起讨论</div>
|
||||
<div className="font-cjk text-sm text-ink-mute">Start a discussion</div>
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<section className="mx-auto max-w-[1400px] px-6 pt-12 pb-8">
|
||||
<div className="flex items-baseline gap-4 mb-3">
|
||||
<Seal char="动" />
|
||||
<div className="eyebrow">Section 03 · 动态</div>
|
||||
</div>
|
||||
<h1 className="font-display tracking-crisp">
|
||||
Activity <span className="font-cjk text-indigo text-5xl ml-2">动态</span>
|
||||
</h1>
|
||||
<p className="mt-5 max-w-3xl text-ink-soft text-lg leading-relaxed">
|
||||
A live mirror of issues and pull requests from{" "}
|
||||
<Link href="https://github.com/Hmbown/deepseek-tui" className="body-link">Hmbown/deepseek-tui</Link>.
|
||||
Refreshed every ten minutes. Click any item to jump to GitHub.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mx-auto max-w-[1400px] px-6 pb-16 grid lg:grid-cols-12 gap-10">
|
||||
<div className="lg:col-span-6">
|
||||
<div className="hairline-t hairline-b hairline-l hairline-r bg-paper">
|
||||
<div className="bg-indigo text-paper px-4 py-3 flex items-baseline justify-between">
|
||||
<div className="font-cjk text-base tracking-wider">合并 · Pull Requests</div>
|
||||
<span className="font-mono text-[0.7rem] uppercase tabular tracking-widest">{pulls.length} shown</span>
|
||||
</div>
|
||||
<div className="px-4">
|
||||
{pulls.length > 0 ? (
|
||||
pulls.map((p) => <FeedCard key={p.url} item={p} />)
|
||||
) : (
|
||||
<div className="py-10 text-center text-sm font-mono text-ink-mute">暂无数据 · feed not loaded</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-6">
|
||||
<div className="hairline-t hairline-b hairline-l hairline-r bg-paper">
|
||||
<div className="bg-ink text-paper px-4 py-3 flex items-baseline justify-between">
|
||||
<div className="font-cjk text-base tracking-wider">议题 · Issues</div>
|
||||
<span className="font-mono text-[0.7rem] uppercase tabular tracking-widest">{issues.length} shown</span>
|
||||
</div>
|
||||
<div className="px-4">
|
||||
{issues.length > 0 ? (
|
||||
issues.map((i) => <FeedCard key={i.url} item={i} />)
|
||||
) : (
|
||||
<div className="py-10 text-center text-sm font-mono text-ink-mute">暂无数据 · feed not loaded</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="bg-paper-deep hairline-t hairline-b">
|
||||
<div className="mx-auto max-w-[1400px] px-6 py-10 grid md:grid-cols-3 gap-6 text-center">
|
||||
<Link href="https://github.com/Hmbown/deepseek-tui/issues/new/choose" className="hairline-t hairline-b hairline-l hairline-r bg-paper p-6 hover:bg-indigo hover:text-paper transition-colors">
|
||||
<div className="font-display text-xl mb-1">Open an issue</div>
|
||||
<div className="font-cjk text-sm text-ink-mute">提交议题</div>
|
||||
</Link>
|
||||
<Link href="https://github.com/Hmbown/deepseek-tui/compare" className="hairline-t hairline-b hairline-l hairline-r bg-paper p-6 hover:bg-indigo hover:text-paper transition-colors">
|
||||
<div className="font-display text-xl mb-1">Open a PR</div>
|
||||
<div className="font-cjk text-sm text-ink-mute">提交合并</div>
|
||||
</Link>
|
||||
<Link href="https://github.com/Hmbown/deepseek-tui/discussions/new" className="hairline-t hairline-b hairline-l hairline-r bg-paper p-6 hover:bg-indigo hover:text-paper transition-colors">
|
||||
<div className="font-display text-xl mb-1">Start a discussion</div>
|
||||
<div className="font-cjk text-sm text-ink-mute">发起讨论</div>
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
import Link from "next/link";
|
||||
import { GITEE_ENABLED } from "@/lib/i18n/config";
|
||||
import { Seal } from "@/components/seal";
|
||||
import { InstallTabs } from "@/components/install-tabs";
|
||||
|
||||
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
|
||||
const { locale } = await params;
|
||||
const isZh = locale === "zh";
|
||||
return {
|
||||
title: isZh ? "安装 · DeepSeek TUI" : "Install · DeepSeek TUI",
|
||||
description: isZh
|
||||
? "在 macOS、Linux 或 Windows 上通过 Cargo、npm、Homebrew tap 或预编译二进制安装 deepseek-tui。"
|
||||
: "Install deepseek-tui on macOS, Linux, or Windows via Cargo, npm, the Homebrew tap, or pre-built binaries.",
|
||||
};
|
||||
}
|
||||
|
||||
export default async function InstallPage({ params }: { params: Promise<{ locale: string }> }) {
|
||||
const { locale } = await params;
|
||||
const isZh = locale === "zh";
|
||||
|
||||
return (
|
||||
<>
|
||||
{isZh ? (
|
||||
<>
|
||||
<section className="mx-auto max-w-[1400px] px-6 pt-12 pb-8">
|
||||
<div className="flex items-baseline gap-4 mb-3">
|
||||
<Seal char="装" />
|
||||
<div className="eyebrow">Section 01 · 安装</div>
|
||||
</div>
|
||||
<h1 className="font-display tracking-crisp">
|
||||
安装 <span className="font-cjk text-indigo text-5xl ml-2">Install</span>
|
||||
</h1>
|
||||
<p className="mt-5 max-w-3xl text-ink-soft text-lg leading-[1.9] tracking-wide">
|
||||
选择下方适合你平台的安装方式——首次加载时会自动检测。所有方式安装的都是同一个二进制文件:
|
||||
一个静态链接的 <code className="inline">deepseek</code> 可执行文件,交互使用时调用 TUI,同时暴露
|
||||
<code className="inline">doctor</code>、<code className="inline">mcp</code>、
|
||||
<code className="inline">serve</code>、<code className="inline">eval</code> 等子命令。
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<InstallTabs />
|
||||
|
||||
{/* 国内镜像安装 */}
|
||||
<section className="mx-auto max-w-[1400px] px-6 py-16">
|
||||
<div className="flex items-baseline gap-4 mb-8 hairline-b pb-4">
|
||||
<Seal char="镜" />
|
||||
<h2 className="font-display">
|
||||
国内镜像安装 <span className="font-cjk text-ink-mute text-xl ml-2">中国大陆专用</span>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-0 col-rule hairline-t hairline-b min-w-0">
|
||||
{/* npmmirror */}
|
||||
<div className="p-6 min-w-0">
|
||||
<h3 className="font-display text-lg mb-2">npmmirror 镜像</h3>
|
||||
<p className="text-sm text-ink-soft leading-[1.9] tracking-wide mb-3">
|
||||
将 npm 注册表切换至国内镜像,然后全局安装:
|
||||
</p>
|
||||
<pre className="code-block text-[0.78rem]">
|
||||
{`npm config set registry https://registry.npmmirror.com
|
||||
npm install -g deepseek-tui`}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
{/* Tuna Cargo */}
|
||||
<div className="p-6 min-w-0">
|
||||
<h3 className="font-display text-lg mb-2">Tuna Cargo 镜像</h3>
|
||||
<p className="text-sm text-ink-soft leading-[1.9] tracking-wide mb-3">
|
||||
在 <code className="inline">~/.cargo/config.toml</code> 中添加以下配置,即可使用清华 Tuna 源:
|
||||
</p>
|
||||
<pre className="code-block text-[0.78rem]">
|
||||
{`[source.crates-io]
|
||||
replace-with = "tuna"
|
||||
|
||||
[source.tuna]
|
||||
registry = "sparse+https://mirrors.tuna.tsinghua.edu.cn/crates.io-index/"`}
|
||||
</pre>
|
||||
<p className="text-sm text-ink-soft leading-[1.9] tracking-wide mt-3">
|
||||
配置完成后运行 <code className="inline">cargo install deepseek-tui-cli --locked</code> 即可(提供 <code className="inline">deepseek</code> 命令)。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Gitee 二进制 */}
|
||||
{GITEE_ENABLED && <div className="p-6 min-w-0">
|
||||
<h3 className="font-display text-lg mb-2">Gitee 预编译二进制</h3>
|
||||
<p className="text-sm text-ink-soft leading-[1.9] tracking-wide mb-3">
|
||||
从 Gitee 发布页直接下载对应平台的预编译二进制文件,解压后即可使用:
|
||||
</p>
|
||||
<Link href="https://gitee.com/Hmbown/deepseek-tui/releases" className="font-mono text-[0.78rem] text-indigo hover:underline">
|
||||
gitee.com/Hmbown/deepseek-tui/releases →
|
||||
</Link>
|
||||
</div>}
|
||||
|
||||
{/* API 端点 */}
|
||||
<div className="p-6 min-w-0">
|
||||
<h3 className="font-display text-lg mb-2">国内 API 访问</h3>
|
||||
<p className="text-sm text-ink-soft leading-[1.9] tracking-wide mb-3">
|
||||
默认 <code className="inline">https://api.deepseek.com</code> 在国内通常可直连。
|
||||
网络不通时,通过自定义 <code className="inline">base_url</code> 或环境变量
|
||||
<code className="inline">DEEPSEEK_BASE_URL</code> 切换至替代节点:
|
||||
</p>
|
||||
<pre className="code-block text-[0.78rem]">
|
||||
{`# ~/.deepseek/config.toml
|
||||
[api]
|
||||
base_url = "https://<你的节点>"
|
||||
|
||||
# 或环境变量(推荐,便于临时切换):
|
||||
# export DEEPSEEK_BASE_URL=https://<你的节点>`}
|
||||
</pre>
|
||||
<p className="text-sm text-ink-soft leading-[1.9] tracking-wide mt-3">
|
||||
API key 通过 <code className="inline">deepseek auth set --provider deepseek</code>{" "}
|
||||
保存,或设 <code className="inline">DEEPSEEK_API_KEY</code> 环境变量。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{GITEE_ENABLED && <div className="mt-6">
|
||||
<Link href="https://gitee.com/Hmbown/deepseek-tui" className="body-link">
|
||||
Gitee 仓库镜像 →
|
||||
</Link>
|
||||
</div>}
|
||||
</section>
|
||||
|
||||
{/* 安装后 */}
|
||||
<section className="mx-auto max-w-[1400px] px-6 py-16">
|
||||
<div className="flex items-baseline gap-4 mb-8 hairline-b pb-4">
|
||||
<Seal char="后" />
|
||||
<h2 className="font-display">
|
||||
安装之后 <span className="font-cjk text-ink-mute text-xl ml-2">下一步</span>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<ol className="grid md:grid-cols-3 gap-0 col-rule hairline-t hairline-b">
|
||||
<li className="p-6">
|
||||
<div className="font-display text-3xl text-indigo mb-2">①</div>
|
||||
<div className="eyebrow mb-2">获取密钥</div>
|
||||
<h3 className="font-display text-lg mb-2">在 platform.deepseek.com 注册</h3>
|
||||
<p className="text-sm text-ink-soft leading-[1.9] tracking-wide">
|
||||
注册后会获得一个 <code className="inline">sk-...</code> 格式的 API 密钥。粘贴一次后,
|
||||
<code className="inline"> deepseek auth</code> 会将其存储在
|
||||
<code className="inline"> ~/.deepseek/config.toml</code>。
|
||||
</p>
|
||||
</li>
|
||||
<li className="p-6">
|
||||
<div className="font-display text-3xl text-indigo mb-2">②</div>
|
||||
<div className="eyebrow mb-2">运行诊断</div>
|
||||
<h3 className="font-display text-lg mb-2">验证环境</h3>
|
||||
<p className="text-sm text-ink-soft leading-[1.9] tracking-wide">
|
||||
<code className="inline">deepseek doctor</code> 会检查密钥、网络连通性、沙箱可用性、
|
||||
MCP 服务器,并将报告写入 <code className="inline">~/.deepseek/doctor.log</code>。
|
||||
</p>
|
||||
</li>
|
||||
<li className="p-6">
|
||||
<div className="font-display text-3xl text-indigo mb-2">③</div>
|
||||
<div className="eyebrow mb-2">试一试</div>
|
||||
<h3 className="font-display text-lg mb-2">第一个提示</h3>
|
||||
<p className="text-sm text-ink-soft leading-[1.9] tracking-wide">
|
||||
<code className="inline">cd</code> 到某个项目目录,运行 <code className="inline">deepseek</code>,
|
||||
然后提问:<em>"这个代码库是做什么的?"</em> Plan 模式默认只读——按
|
||||
<kbd className="font-mono text-xs px-1 hairline-t hairline-b hairline-l hairline-r">Tab</kbd> 切换到 Agent 模式。
|
||||
</p>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
{/* 配置 */}
|
||||
<section className="bg-paper-deep hairline-t hairline-b">
|
||||
<div className="mx-auto max-w-[1400px] px-6 py-16 grid lg:grid-cols-12 gap-10 min-w-0">
|
||||
<div className="lg:col-span-5 min-w-0">
|
||||
<div className="eyebrow mb-3">配置文件 · Config</div>
|
||||
<h2 className="font-display text-3xl">文件存放位置</h2>
|
||||
<p className="mt-4 text-ink-soft leading-[1.9] tracking-wide">
|
||||
所有配置存放在 <code className="inline">~/.deepseek/</code> 目录下。项目级别的覆盖通过仓库根目录的
|
||||
<code className="inline">.deepseek/</code> 等项目级配置实现。
|
||||
</p>
|
||||
<div className="mt-6 space-y-3">
|
||||
<Link href="/zh/docs" className="body-link inline-block">完整配置参考 →</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="lg:col-span-7 min-w-0">
|
||||
<pre className="code-block text-[0.78rem]">
|
||||
{`~/.deepseek/
|
||||
├── config.toml # API 密钥、模型、钩子、配置集
|
||||
├── mcp.json # MCP 服务器定义
|
||||
├── skills/ # 用户技能(每个含 SKILL.md)
|
||||
├── sessions/ # 检查点 + 离线队列
|
||||
├── tasks/ # 后台任务存储
|
||||
└── audit.log # 凭证 / 审批 / 提权审计日志
|
||||
|
||||
# 项目级别
|
||||
./.deepseek/ # 项目级配置(可选)`}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<section className="mx-auto max-w-[1400px] px-6 pt-12 pb-8">
|
||||
<div className="flex items-baseline gap-4 mb-3">
|
||||
<Seal char="装" />
|
||||
<div className="eyebrow">Section 01 · 安装</div>
|
||||
</div>
|
||||
<h1 className="font-display tracking-crisp">
|
||||
Install <span className="font-cjk text-indigo text-5xl ml-2">安装</span>
|
||||
</h1>
|
||||
<p className="mt-5 max-w-3xl text-ink-soft text-lg leading-relaxed">
|
||||
Pick your platform below — we auto-detect on first load. Every method gives you the same
|
||||
binary: a single static <code className="inline">deepseek</code> executable that
|
||||
dispatches to the TUI for interactive use and exposes subcommands like
|
||||
<code className="inline">doctor</code>, <code className="inline">mcp</code>,
|
||||
<code className="inline">serve</code>, <code className="inline">eval</code>.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<InstallTabs />
|
||||
|
||||
{/* AFTER INSTALL */}
|
||||
<section className="mx-auto max-w-[1400px] px-6 py-16">
|
||||
<div className="flex items-baseline gap-4 mb-8 hairline-b pb-4">
|
||||
<Seal char="后" />
|
||||
<h2 className="font-display">
|
||||
After install <span className="font-cjk text-ink-mute text-xl ml-2">下一步</span>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<ol className="grid md:grid-cols-3 gap-0 col-rule hairline-t hairline-b">
|
||||
<li className="p-6">
|
||||
<div className="font-display text-3xl text-indigo mb-2">①</div>
|
||||
<div className="eyebrow mb-2">Get a key</div>
|
||||
<h3 className="font-display text-lg mb-2">Sign up at platform.deepseek.com</h3>
|
||||
<p className="text-sm text-ink-soft leading-relaxed">
|
||||
You'll get an <code className="inline">sk-...</code> API key. Paste it once and
|
||||
<code className="inline"> deepseek auth</code> will store it in
|
||||
<code className="inline"> ~/.deepseek/config.toml</code>.
|
||||
</p>
|
||||
</li>
|
||||
<li className="p-6">
|
||||
<div className="font-display text-3xl text-indigo mb-2">②</div>
|
||||
<div className="eyebrow mb-2">Run doctor</div>
|
||||
<h3 className="font-display text-lg mb-2">Verify your setup</h3>
|
||||
<p className="text-sm text-ink-soft leading-relaxed">
|
||||
<code className="inline">deepseek doctor</code> checks your key, network,
|
||||
sandbox availability, MCP servers, and writes a report to{" "}
|
||||
<code className="inline">~/.deepseek/doctor.log</code>.
|
||||
</p>
|
||||
</li>
|
||||
<li className="p-6">
|
||||
<div className="font-display text-3xl text-indigo mb-2">③</div>
|
||||
<div className="eyebrow mb-2">Try it out</div>
|
||||
<h3 className="font-display text-lg mb-2">First prompt</h3>
|
||||
<p className="text-sm text-ink-soft leading-relaxed">
|
||||
<code className="inline">cd</code> into a project, run <code className="inline">deepseek</code>,
|
||||
and ask: <em>"What does this codebase do?"</em> Plan mode is read-only by default —
|
||||
press <kbd className="font-mono text-xs px-1 hairline-t hairline-b hairline-l hairline-r">Tab</kbd> to switch to Agent mode.
|
||||
</p>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
{/* CONFIG */}
|
||||
<section className="bg-paper-deep hairline-t hairline-b">
|
||||
<div className="mx-auto max-w-[1400px] px-6 py-16 grid lg:grid-cols-12 gap-10 min-w-0">
|
||||
<div className="lg:col-span-5 min-w-0">
|
||||
<div className="eyebrow mb-3">Config files · 配置</div>
|
||||
<h2 className="font-display text-3xl">Where things live</h2>
|
||||
<p className="mt-4 text-ink-soft leading-relaxed">
|
||||
All configuration goes under <code className="inline">~/.deepseek/</code>. Per-project
|
||||
overrides via project-scoped <code className="inline">.deepseek/</code> config at the repo root.
|
||||
</p>
|
||||
<div className="mt-6 space-y-3">
|
||||
<Link href="/docs" className="body-link inline-block">Full configuration reference →</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="lg:col-span-7 min-w-0">
|
||||
<pre className="code-block text-[0.78rem]">
|
||||
{`~/.deepseek/
|
||||
├── config.toml # api keys, model, hooks, profiles
|
||||
├── mcp.json # MCP server definitions
|
||||
├── skills/ # user skills (each with SKILL.md)
|
||||
├── sessions/ # checkpoints + offline queue
|
||||
├── tasks/ # background task store
|
||||
└── audit.log # credential / approval / elevation audit trail
|
||||
|
||||
# project-local
|
||||
./.deepseek/ # project-scoped config (optional)`}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Nav } from "@/components/nav";
|
||||
import { Footer } from "@/components/footer";
|
||||
import { locales, type Locale } from "@/lib/i18n/config";
|
||||
|
||||
export function generateStaticParams() {
|
||||
return locales.map((locale) => ({ locale }));
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }): Promise<Metadata> {
|
||||
const { locale } = await params;
|
||||
const isZh = locale === "zh";
|
||||
return {
|
||||
title: isZh ? "DeepSeek TUI · 终端原生编程智能体" : "DeepSeek TUI · 深度求索 终端",
|
||||
description: isZh
|
||||
? "基于 DeepSeek V4 的开源终端编程智能体。支持 100 万 token 上下文、MCP 协议、沙箱执行。"
|
||||
: "Terminal-native coding agent built on DeepSeek V4. Open source. Community site for installation, docs, roadmap, and live activity from the Hmbown/deepseek-tui repo.",
|
||||
metadataBase: new URL("https://deepseek-tui.com"),
|
||||
openGraph: {
|
||||
title: isZh ? "DeepSeek TUI · 终端原生编程智能体" : "DeepSeek TUI",
|
||||
description: isZh
|
||||
? "基于 DeepSeek V4 的开源终端编程智能体。"
|
||||
: "Terminal-native coding agent built on DeepSeek V4.",
|
||||
url: "https://deepseek-tui.com",
|
||||
siteName: "DeepSeek TUI",
|
||||
type: "website",
|
||||
},
|
||||
twitter: { card: "summary_large_image" },
|
||||
alternates: {
|
||||
languages: {
|
||||
en: "/en",
|
||||
zh: "/zh",
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function LocaleLayout({
|
||||
children,
|
||||
params,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
params: Promise<{ locale: string }>;
|
||||
}) {
|
||||
const { locale } = await params;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Nav locale={locale as Locale} />
|
||||
<main>{children}</main>
|
||||
<Footer locale={locale as Locale} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,436 @@
|
||||
import Link from "next/link";
|
||||
import { fetchFeed, fetchRepoStats } from "@/lib/github";
|
||||
import { getDispatch, getEnv } from "@/lib/kv";
|
||||
import { getFacts } from "@/lib/facts";
|
||||
import { Ticker } from "@/components/ticker";
|
||||
import { StatGrid } from "@/components/stat-grid";
|
||||
import { FeedCard } from "@/components/feed-card";
|
||||
import { Seal } from "@/components/seal";
|
||||
import { MermaidDiagram } from "@/components/mermaid-diagram";
|
||||
import type { CuratedDispatch, FeedItem, RepoStats } from "@/lib/types";
|
||||
import { GITEE_ENABLED } from "@/lib/i18n/config";
|
||||
|
||||
export const revalidate = 1800;
|
||||
|
||||
const FALLBACK_STATS: RepoStats = {
|
||||
stars: 0,
|
||||
forks: 0,
|
||||
openIssues: 0,
|
||||
openPulls: 0,
|
||||
contributors: 1,
|
||||
fetchedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const FALLBACK_DISPATCH_EN: CuratedDispatch = {
|
||||
generatedAt: new Date().toISOString(),
|
||||
headline: "A small, focused terminal agent — quietly shipping",
|
||||
summary:
|
||||
"DeepSeek TUI is an open-source coding agent that runs in your terminal, talks to the DeepSeek V4 family, and behaves itself around your filesystem. The dispatch below is regenerated by deepseek-v4-flash on a six-hour cron — you'll see actual repo movement here once the cron runs.",
|
||||
highlights: [
|
||||
{ title: "Read the install guide", href: "/install", tag: "shipped", blurb: "Per-OS instructions for Cargo, npm, the Homebrew tap, and release binaries." },
|
||||
{ title: "Browse open issues", href: "https://github.com/Hmbown/deepseek-tui/issues", tag: "opened", blurb: "Triaged on GitHub — start with anything labelled 'good first issue'." },
|
||||
{ title: "Review the roadmap", href: "/roadmap", tag: "discussion", blurb: "What's confirmed, what's being weighed, what's been ruled out." },
|
||||
],
|
||||
movers: [],
|
||||
};
|
||||
|
||||
const FALLBACK_DISPATCH_ZH: CuratedDispatch = {
|
||||
generatedAt: new Date().toISOString(),
|
||||
headline: "一个专注的终端智能体——安静迭代中",
|
||||
summary:
|
||||
"DeepSeek TUI 是一款开源终端编程智能体,运行在你的终端中,接入 DeepSeek V4 系列模型,对文件系统操作保持克制。以下「今日要闻」由 deepseek-v4-flash 每六小时自动生成——仓库有新动态时会实时更新。",
|
||||
highlights: [
|
||||
{ title: "阅读安装指南", href: "/zh/install", tag: "shipped", blurb: "覆盖 macOS、Linux、Windows,支持 Cargo、npm、Homebrew tap 及发布页二进制。" },
|
||||
{ title: "浏览开放议题", href: "https://github.com/Hmbown/deepseek-tui/issues", tag: "opened", blurb: "在 GitHub 上查看——从标记为 good first issue 的议题开始。" },
|
||||
{ title: "查看路线图", href: "/zh/roadmap", tag: "discussion", blurb: "已确认、审议中、以及已排除的功能规划。" },
|
||||
],
|
||||
movers: [],
|
||||
};
|
||||
|
||||
export default async function HomePage({ params }: { params: Promise<{ locale: string }> }) {
|
||||
const { locale } = await params;
|
||||
const isZh = locale === "zh";
|
||||
const env = await getEnv();
|
||||
const facts = await getFacts();
|
||||
|
||||
let stats: RepoStats = FALLBACK_STATS;
|
||||
let feed: FeedItem[] = [];
|
||||
let dispatch: CuratedDispatch = isZh ? FALLBACK_DISPATCH_ZH : FALLBACK_DISPATCH_EN;
|
||||
|
||||
try {
|
||||
[stats, feed] = await Promise.all([
|
||||
fetchRepoStats(env.GITHUB_TOKEN),
|
||||
fetchFeed(env.GITHUB_TOKEN, 12),
|
||||
]);
|
||||
} catch (e) {
|
||||
console.error("github fetch failed", e);
|
||||
}
|
||||
|
||||
try {
|
||||
const cached = await getDispatch();
|
||||
if (cached) dispatch = cached;
|
||||
} catch {
|
||||
/* keep fallback */
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Ticker items={feed} />
|
||||
|
||||
{/* HERO */}
|
||||
<section className="relative overflow-hidden">
|
||||
<div className="margin-glyph right-[-2rem] top-[2rem] hidden lg:block">深</div>
|
||||
|
||||
<div className="mx-auto max-w-[1400px] px-4 sm:px-6 pt-10 sm:pt-14 pb-12 grid lg:grid-cols-12 gap-10">
|
||||
<div className="lg:col-span-8">
|
||||
<div className="flex items-center gap-2 sm:gap-3 mb-6 flex-wrap">
|
||||
<span className="pill pill-hot">v4 · 1M context</span>
|
||||
<span className="pill pill-ghost">MIT licensed</span>
|
||||
</div>
|
||||
|
||||
<h1 className="font-display tracking-crisp">
|
||||
{isZh
|
||||
? "一个住在终端里的编程智能体。"
|
||||
: "A coding agent that lives in your terminal."}
|
||||
</h1>
|
||||
|
||||
<p className="mt-6 text-lg text-ink-soft leading-relaxed max-w-2xl">
|
||||
<span className="font-cjk text-indigo font-semibold">深度求索</span> ·{" "}
|
||||
<strong>DeepSeek TUI</strong>{" "}
|
||||
{isZh
|
||||
? "是一款基于 DeepSeek V4 系列的开源命令行智能体。它编辑文件、执行 Shell、调用 MCP 服务器,并尊重你的沙箱边界。"
|
||||
: "is an open-source command-line agent built on the DeepSeek V4 family. It edits files, runs shells, calls MCP servers, and respects your sandbox."}
|
||||
</p>
|
||||
|
||||
<div className="mt-8 flex flex-wrap items-stretch sm:items-center gap-3">
|
||||
<Link
|
||||
href={isZh ? "/zh/install" : "/install"}
|
||||
className="flex-1 sm:flex-none text-center px-5 py-3 bg-ink text-paper font-mono text-sm uppercase tracking-wider hover:bg-indigo transition-colors"
|
||||
>
|
||||
{isZh ? "30 秒完成安装 →" : "Install in 30 seconds →"}
|
||||
</Link>
|
||||
<Link
|
||||
href="https://github.com/Hmbown/deepseek-tui"
|
||||
className="flex-1 sm:flex-none text-center px-5 py-3 hairline-t hairline-b hairline-l hairline-r font-mono text-sm uppercase tracking-wider hover:bg-paper-deep transition-colors"
|
||||
>
|
||||
★ Star on GitHub
|
||||
</Link>
|
||||
<Link
|
||||
href={isZh ? "/zh/docs" : "/docs"}
|
||||
className="px-5 py-3 font-mono text-sm uppercase tracking-wider text-ink-mute hover:text-indigo transition-colors"
|
||||
>
|
||||
{isZh ? "阅读文档" : "Read the docs"}
|
||||
</Link>
|
||||
<Link
|
||||
href="https://buymeacoffee.com/hmbown"
|
||||
className="px-5 py-3 font-mono text-sm uppercase tracking-wider text-ink-mute hover:text-indigo transition-colors"
|
||||
>
|
||||
{isZh ? "支持项目 ↗" : "Support ↗"}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Trust signals */}
|
||||
<div className="mt-6 flex items-center gap-4 text-xs font-mono text-ink-mute flex-wrap">
|
||||
{isZh ? (
|
||||
<span>独立维护者 Hmbown{GITEE_ENABLED && <> · <a href="https://gitee.com/Hmbown/deepseek-tui" className="text-indigo hover:underline">Gitee 镜像</a></>}</span>
|
||||
) : (
|
||||
<span>Maintained by Hmbown</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* hero side: cargo install card */}
|
||||
<div className="lg:col-span-4">
|
||||
<div className="hairline-t hairline-b hairline-l hairline-r bg-paper p-5 relative">
|
||||
<div className="absolute -top-3 left-4 bg-paper px-2 eyebrow">
|
||||
{isZh ? "最快安装 · 一行搞定" : "quickest path · 一行安装"}
|
||||
</div>
|
||||
<pre className="code-block mt-2">
|
||||
{isZh ? (
|
||||
<>
|
||||
<span className="comment"># macOS / Linux — Cargo</span>{"\n"}
|
||||
<span className="prompt">$</span> cargo install deepseek-tui-cli --locked{"\n"}
|
||||
<span className="prompt">$</span> deepseek{"\n"}
|
||||
<br />
|
||||
<span className="comment"># 或通过 npm 安装</span>{"\n"}
|
||||
<span className="prompt">$</span> npm i -g deepseek-tui{"\n"}
|
||||
<br />
|
||||
<span className="comment"># 首次运行会自动创建 <span className="key">~/.deepseek/</span></span>{"\n"}
|
||||
<br />
|
||||
<span className="comment"># 国内镜像</span>{"\n"}
|
||||
<span className="prompt">$</span> npm config set registry https://registry.npmmirror.com{"\n"}
|
||||
<span className="prompt">$</span> npm i -g deepseek-tui
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="comment"># macOS / Linux — Cargo</span>{"\n"}
|
||||
<span className="prompt">$</span> cargo install deepseek-tui-cli --locked{"\n"}
|
||||
<span className="prompt">$</span> deepseek{"\n"}
|
||||
<br />
|
||||
<span className="comment"># or via npm wrapper</span>{"\n"}
|
||||
<span className="prompt">$</span> npm i -g deepseek-tui{"\n"}
|
||||
<br />
|
||||
<span className="comment"># first run sets up <span className="key">~/.deepseek/</span></span>
|
||||
</>
|
||||
)}
|
||||
</pre>
|
||||
<div className="mt-3 flex items-center justify-between text-[0.7rem] font-mono text-ink-mute">
|
||||
<span>{isZh ? `需要 Rust 1.88+ 或 Node ${facts.nodeEngines ?? ">=18"}` : `requires Rust 1.88+ or Node ${facts.nodeEngines ?? ">=18"}`}</span>
|
||||
<Link href={isZh ? "/zh/install" : "/install"} className="text-indigo hover:underline">{isZh ? "其他系统 →" : "other OSes →"}</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<StatGrid stats={stats} />
|
||||
|
||||
{/* TODAY'S DISPATCH */}
|
||||
<section className="mx-auto max-w-[1400px] px-6 py-16">
|
||||
<div className="flex items-end justify-between mb-6 hairline-b pb-4">
|
||||
<div>
|
||||
<div className="eyebrow mb-2">{isZh ? "今日要闻" : "Today's Dispatch · 今日要闻"}</div>
|
||||
<h2 className="font-display tabular text-ink-mute font-mono text-base">
|
||||
{new Date(dispatch.generatedAt).toISOString().slice(0, 10)}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="eyebrow">{isZh ? "由 … 编辑" : "Curated by"}</div>
|
||||
<div className="font-mono text-sm">deepseek-v4-flash · 6h cron</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid lg:grid-cols-12 gap-10">
|
||||
{/* editorial */}
|
||||
<div className="lg:col-span-7">
|
||||
<article className="space-y-5">
|
||||
<h3 className="font-display text-3xl leading-tight">
|
||||
{dispatch.headline}
|
||||
</h3>
|
||||
<p className={`${isZh ? "text-ink-soft leading-[1.9] tracking-wide text-[1.02rem]" : "text-ink-soft leading-relaxed text-[1.02rem]"}`}>
|
||||
{dispatch.summary}
|
||||
</p>
|
||||
|
||||
<div className="hairline-t pt-5">
|
||||
<div className="eyebrow mb-3">{isZh ? "要点" : "Highlights · 要点"}</div>
|
||||
<ul className="divide-y divide-paper-line/40 hairline-t hairline-b">
|
||||
{dispatch.highlights.map((h, i) => (
|
||||
<li key={i} className="py-3 flex items-start gap-4">
|
||||
<span className="font-mono text-[0.7rem] text-indigo uppercase tracking-widest pt-1 w-20 shrink-0">
|
||||
{h.tag}
|
||||
</span>
|
||||
<div className="flex-1">
|
||||
<Link href={h.href} className="body-link font-display text-lg leading-snug">
|
||||
{h.title}
|
||||
</Link>
|
||||
<p className={`text-sm text-ink-soft mt-1 ${isZh ? "leading-[1.8]" : ""}`}>{h.blurb}</p>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{dispatch.movers.length > 0 && (
|
||||
<div className="pt-2">
|
||||
<div className="eyebrow mb-3">{isZh ? "进展" : "Movers · 进展"}</div>
|
||||
<ul className="space-y-2">
|
||||
{dispatch.movers.map((m) => (
|
||||
<li key={m.number} className="flex items-baseline gap-3 text-sm">
|
||||
<span className="font-mono text-indigo tabular">#{m.number}</span>
|
||||
<Link href={m.href} className="font-medium hover:text-indigo">{m.title}</Link>
|
||||
<span className="text-ink-mute">— {m.reason}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</article>
|
||||
</div>
|
||||
|
||||
{/* recent activity column */}
|
||||
<aside className="lg:col-span-5">
|
||||
<div className="hairline-t hairline-b hairline-l hairline-r bg-paper">
|
||||
<div className="bg-ink text-paper px-4 py-2 flex items-center justify-between">
|
||||
<div className="font-cjk text-sm tracking-wider">{isZh ? "最新活动" : "最新活动 · Recent activity"}</div>
|
||||
<Link href={isZh ? "/zh/feed" : "/feed"} className="font-mono text-[0.7rem] uppercase tracking-wider hover:text-indigo">
|
||||
{isZh ? "全部 →" : "All →"}
|
||||
</Link>
|
||||
</div>
|
||||
<div className="px-4">
|
||||
{feed.slice(0, 5).map((item) => (
|
||||
<FeedCard key={item.url} item={item} />
|
||||
))}
|
||||
{feed.length === 0 && (
|
||||
<div className="py-8 text-center text-sm text-ink-mute font-mono">
|
||||
{isZh ? "暂无数据 · feed not loaded" : "feed not yet loaded · 暂无数据"}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* WHAT IT IS — 3 column */}
|
||||
<section className="bg-paper-deep hairline-t hairline-b">
|
||||
<div className="mx-auto max-w-[1400px] px-6 py-16">
|
||||
<div className="flex items-baseline gap-4 mb-8 hairline-b pb-4">
|
||||
<Seal char="是" />
|
||||
<h2 className="font-display">
|
||||
{isZh ? "它到底是什么" : "What it actually is"}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-0 col-rule hairline-t hairline-b">
|
||||
{isZh ? (
|
||||
<>
|
||||
<div className="p-6">
|
||||
<div className="eyebrow mb-3">01 · 终端智能体</div>
|
||||
<h3 className="font-display text-xl mb-3">编程智能体,不是聊天框</h3>
|
||||
<p className="text-sm text-ink-soft leading-[1.9]">
|
||||
与 Claude Code、Codex CLI 相同的循环。读、改、跑测试、汇报。
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<div className="eyebrow mb-3">02 · 沙箱保护</div>
|
||||
<h3 className="font-display text-xl mb-3">三种模式,一套审批</h3>
|
||||
<p className="text-sm text-ink-soft leading-[1.9]">
|
||||
Plan 只读,Agent 询问,YOLO 自动。沙箱:{facts.sandboxBackends.join("、")}。
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<div className="eyebrow mb-3">03 · 模型自由</div>
|
||||
<h3 className="font-display text-xl mb-3">默认 {facts.defaultModel ?? "DeepSeek V4"}</h3>
|
||||
<p className="text-sm text-ink-soft leading-[1.9]">
|
||||
内建 {facts.providers.length} 个提供商。<code className="inline">deepseek auth set --provider …</code> 切换。
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="p-6">
|
||||
<div className="eyebrow mb-3">01 · 终端代理</div>
|
||||
<h3 className="font-display text-xl mb-3">A coding agent, not a chat box</h3>
|
||||
<p className="text-sm text-ink-soft leading-relaxed">
|
||||
Same loop as Claude Code or Codex CLI. It reads, edits, runs tests, reports back.
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<div className="eyebrow mb-3">02 · 沙箱保护</div>
|
||||
<h3 className="font-display text-xl mb-3">Three modes, one approval system</h3>
|
||||
<p className="text-sm text-ink-soft leading-relaxed">
|
||||
Plan reads, Agent asks, YOLO doesn't. Sandboxed via {facts.sandboxBackends.join(", ")}.
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<div className="eyebrow mb-3">03 · 模型自主</div>
|
||||
<h3 className="font-display text-xl mb-3">{facts.defaultModel ?? "DeepSeek V4"} by default</h3>
|
||||
<p className="text-sm text-ink-soft leading-relaxed">
|
||||
{facts.providers.length} built-in providers. Swap with <code className="inline">deepseek auth set --provider …</code>.
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* HOW IT WORKS — mermaid diagram (replaces brittle ASCII art that
|
||||
misaligned under CJK monospace, per dhh's note) */}
|
||||
<section className="mx-auto max-w-[1400px] px-6 py-16">
|
||||
<div className="flex items-baseline gap-4 mb-8 hairline-b pb-4">
|
||||
<Seal char="作" />
|
||||
<h2 className="font-display">
|
||||
{isZh ? "运作方式" : "How it works"}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="hairline-t hairline-b hairline-l hairline-r bg-paper p-4 sm:p-8">
|
||||
<MermaidDiagram
|
||||
label={isZh ? "DeepSeek TUI 运作方式示意图" : "DeepSeek TUI architecture diagram"}
|
||||
chart={
|
||||
isZh
|
||||
? `flowchart TD
|
||||
A["用户输入<br/>(TUI · ratatui)"] -->|Op channel| B["Engine<br/>turn loop + tools"]
|
||||
B -->|HTTPS / SSE| C["DeepSeek API<br/>V4 family"]
|
||||
C -->|stream events| B
|
||||
B -->|tool call| T["read_file · edit_file · grep<br/>apply_patch · exec_shell<br/>mcp_<server>_<tool>"]
|
||||
T -->|approval Y/N| P["审批对话框<br/>approval dialog"]
|
||||
P --> B
|
||||
T -->|exec| S["沙箱<br/>seatbelt · landlock · win32"]
|
||||
classDef accent fill:#e9eefe,stroke:#0e0e10,stroke-width:1px;
|
||||
classDef api fill:#0e0e10,stroke:#0e0e10,color:#ffffff;
|
||||
class C api;
|
||||
class T,P,S accent;`
|
||||
: `flowchart TD
|
||||
A["You type<br/>(TUI · ratatui)"] -->|Op channel| B["Engine<br/>turn loop + tools"]
|
||||
B -->|HTTPS / SSE| C["DeepSeek API<br/>V4 family"]
|
||||
C -->|stream events| B
|
||||
B -->|tool call| T["read_file · edit_file · grep<br/>apply_patch · exec_shell<br/>mcp_<server>_<tool>"]
|
||||
T -->|approval Y/N| P["Approval<br/>dialog"]
|
||||
P --> B
|
||||
T -->|exec| S["Sandbox<br/>seatbelt · landlock · win32"]
|
||||
classDef accent fill:#e9eefe,stroke:#0e0e10,stroke-width:1px;
|
||||
classDef api fill:#0e0e10,stroke:#0e0e10,color:#ffffff;
|
||||
class C api;
|
||||
class T,P,S accent;`
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p className="mt-3 text-xs font-mono text-ink-mute">
|
||||
{isZh
|
||||
? "示意图使用 mermaid.live 标准格式渲染。"
|
||||
: "Rendered with mermaid.live standard syntax."}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* JOIN IN */}
|
||||
<section className="bg-ink text-paper">
|
||||
<div className="mx-auto max-w-[1400px] px-6 py-16 grid lg:grid-cols-12 gap-10">
|
||||
<div className="lg:col-span-5">
|
||||
<div className="eyebrow text-paper-deep/70 mb-3">{isZh ? "加入" : "Join in"}</div>
|
||||
<h2 className="font-display text-paper text-4xl leading-tight">
|
||||
{isZh ? "这是一个小项目。你的每个补丁都很重要。" : "This is a small project. Your patch matters."}
|
||||
</h2>
|
||||
<p className={`mt-5 text-paper-deep/80 ${isZh ? "leading-[1.9]" : "leading-relaxed"}`}>
|
||||
{isZh
|
||||
? "无 CLA,无赞助商锁定。维护者亲自阅读每一条内容——通常在一天内回复。议题在公开环境下分类。版本从 main 分支发布。"
|
||||
: "No CLA. No sponsor lockouts. The maintainer reads everything personally — usually within a day. Issues triaged in the open. Releases cut from main."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-7 grid sm:grid-cols-3 gap-px bg-paper/15">
|
||||
{(isZh
|
||||
? [
|
||||
{ t: "提交议题", cn: "提 Bug 或功能建议", d: "Bug 报告、功能需求,或一个好问题。", href: "https://github.com/Hmbown/deepseek-tui/issues/new/choose" },
|
||||
{ t: "提交 PR", cn: "贡献代码", d: "Fork、分支、conventional commit、提交 PR。", href: "/zh/contribute" },
|
||||
{ t: "发起讨论", cn: "参与设计", d: "路线图、架构设计、任何非 Bug 的话题。", href: "https://github.com/Hmbown/deepseek-tui/discussions" },
|
||||
]
|
||||
: [
|
||||
{ t: "Open an issue", cn: "提议题", d: "Bug, feature, or just a sharp question.", href: "https://github.com/Hmbown/deepseek-tui/issues/new/choose" },
|
||||
{ t: "Send a PR", cn: "提交合并", d: "Fork, branch, conventional commit, open PR.", href: "/contribute" },
|
||||
{ t: "Start a discussion", cn: "发起讨论", d: "Roadmap, design, anything that's not a bug.", href: "https://github.com/Hmbown/deepseek-tui/discussions" },
|
||||
]
|
||||
).map((c) => (
|
||||
<Link
|
||||
key={c.t}
|
||||
href={c.href}
|
||||
className="bg-ink p-6 hover:bg-indigo group transition-colors"
|
||||
>
|
||||
<div className="font-cjk text-sm text-indigo group-hover:text-paper transition-colors mb-2">
|
||||
{c.cn}
|
||||
</div>
|
||||
<div className="font-display text-xl mb-2">{c.t}</div>
|
||||
<div className="text-sm text-paper-deep/80 group-hover:text-paper">{c.d}</div>
|
||||
<div className="mt-4 font-mono text-[0.7rem] uppercase tracking-widest text-paper-deep/60 group-hover:text-paper">
|
||||
{isZh ? "前往 →" : "Go →"}
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
import Link from "next/link";
|
||||
import { Seal } from "@/components/seal";
|
||||
import { getCachedRoadmap, type RoadmapItem } from "@/lib/roadmap-feed";
|
||||
import { getEnv } from "@/lib/kv";
|
||||
|
||||
export const revalidate = 1800;
|
||||
|
||||
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) {
|
||||
const { locale } = await params;
|
||||
const isZh = locale === "zh";
|
||||
return {
|
||||
title: isZh ? "路线图 · DeepSeek TUI" : "Roadmap · DeepSeek TUI",
|
||||
description: isZh
|
||||
? "已确认、正在评估和已排除的功能规划。"
|
||||
: "What's confirmed, what's being weighed, what's been ruled out for deepseek-tui.",
|
||||
};
|
||||
}
|
||||
|
||||
const tracksEn = [
|
||||
{
|
||||
title: "Shipped",
|
||||
cn: "已完成",
|
||||
color: "jade",
|
||||
items: [
|
||||
{ title: "13-crate workspace split", note: "core, app-server, tui, protocol, config, state, tools, mcp, hooks, execpolicy, agent, tui-core, cli" },
|
||||
{ title: "Mode-gated tool registration", note: "Plan / Agent / YOLO with orthogonal approval modes" },
|
||||
{ title: "MCP client + stdio server", note: "Bidirectional — both consume and expose tools" },
|
||||
{ title: "Sandbox: seatbelt / landlock / AppContainer", note: "Per-platform with workspace boundary; Windows path is best-effort" },
|
||||
{ title: "Background tasks + replayable timelines", note: "Durable task queue under ~/.deepseek/tasks/" },
|
||||
{ title: "Runtime API (HTTP/SSE)", note: "deepseek serve --http with /v1/threads, /v1/tasks" },
|
||||
{ title: "Sub-agent family", note: "agent_spawn / agent_wait / agent_result / agent_resume" },
|
||||
{ title: "rlm tool", note: "Recursive long-context processing in sandboxed Python" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Underway",
|
||||
cn: "进行中",
|
||||
color: "ochre",
|
||||
items: [
|
||||
{ title: "Exa web-search backend", note: "Issue #431 — bundled alternative to the existing DDG + Bing path" },
|
||||
{ title: "Feishu / Lark bot integration", note: "Issue #757 — chat frontend over the existing runtime API" },
|
||||
{ title: "Responses API stabilization", note: "Currently behind EXPERIMENTAL_RESPONSES_API_ENV" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Considered",
|
||||
cn: "考虑中",
|
||||
color: "cobalt",
|
||||
items: [
|
||||
{ title: "Homebrew core formula", note: "Tap exists; pursuing homebrew-core inclusion" },
|
||||
{ title: "Scoop manifest", note: "Mirror of Windows install path" },
|
||||
{ title: "Native installer for Windows", note: "MSI / WinGet — pending" },
|
||||
{ title: "First-class Tauri-based GUI shell", note: "Optional surface; TUI remains canonical" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Ruled out",
|
||||
cn: "暂不考虑",
|
||||
color: "ink-mute",
|
||||
items: [
|
||||
{ title: "Telemetry / phone-home", note: "Not while there's a single maintainer" },
|
||||
{ title: "Hosted SaaS dashboard", note: "The terminal IS the dashboard" },
|
||||
{ title: "Required login / accounts", note: "Bring your own API key, that's it" },
|
||||
{ title: "Sponsored model recommendations", note: "Model picker stays neutral" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const tracksZh = [
|
||||
{
|
||||
title: "已完成",
|
||||
cn: "Shipped",
|
||||
color: "jade",
|
||||
items: [
|
||||
{ title: "13 个 crate 的工作区拆分", note: "core, app-server, tui, protocol, config, state, tools, mcp, hooks, execpolicy, agent, tui-core, cli" },
|
||||
{ title: "按模式注册工具", note: "Plan / Agent / YOLO,审批模式正交" },
|
||||
{ title: "MCP 客户端 + stdio 服务器", note: "双向——既消费也暴露工具" },
|
||||
{ title: "沙箱:seatbelt / landlock / AppContainer", note: "按平台隔离,含工作区边界;Windows 路径为尽力而为" },
|
||||
{ title: "后台任务 + 可回放时间线", note: "持久化任务队列,位于 ~/.deepseek/tasks/" },
|
||||
{ title: "运行时 API(HTTP/SSE)", note: "deepseek serve --http,暴露 /v1/threads、/v1/tasks" },
|
||||
{ title: "子 Agent 体系", note: "agent_spawn / agent_wait / agent_result / agent_resume" },
|
||||
{ title: "rlm 工具", note: "沙箱 Python 中的递归长上下文处理" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "进行中",
|
||||
cn: "Underway",
|
||||
color: "ochre",
|
||||
items: [
|
||||
{ title: "Exa 网页搜索后端", note: "Issue #431——内建 Exa 路由,作为现有 DDG + Bing 路径的备选" },
|
||||
{ title: "飞书 / Lark 机器人集成", note: "Issue #757——通过现有 runtime API 提供聊天前端" },
|
||||
{ title: "Responses API 稳定化", note: "目前通过 EXPERIMENTAL_RESPONSES_API_ENV 启用" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "考虑中",
|
||||
cn: "Considered",
|
||||
color: "cobalt",
|
||||
items: [
|
||||
{ title: "Homebrew 核心仓库", note: "Tap 已有;正在争取进入 homebrew-core" },
|
||||
{ title: "Scoop 清单", note: "Windows 安装路径的镜像" },
|
||||
{ title: "Windows 原生安装器", note: "MSI / WinGet——待定" },
|
||||
{ title: "Tauri GUI 外壳", note: "可选界面;TUI 始终是正统" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "暂不考虑",
|
||||
cn: "Ruled out",
|
||||
color: "ink-mute",
|
||||
items: [
|
||||
{ title: "遥测 / 回传数据", note: "只有一位维护者的情况下不会做" },
|
||||
{ title: "托管 SaaS 面板", note: "终端本身就是面板" },
|
||||
{ title: "强制登录 / 注册", note: "自带 API 密钥即可" },
|
||||
{ title: "赞助商模型推荐", note: "模型选择器保持中立" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const colorFor = (c: string) =>
|
||||
c === "jade" ? "border-jade text-jade" :
|
||||
c === "ochre" ? "border-ochre text-ochre" :
|
||||
c === "cobalt" ? "border-cobalt text-cobalt" :
|
||||
"border-ink-mute text-ink-mute";
|
||||
|
||||
export default async function RoadmapPage({ params }: { params: Promise<{ locale: string }> }) {
|
||||
const { locale } = await params;
|
||||
const isZh = locale === "zh";
|
||||
const baseTracks = isZh ? tracksZh : tracksEn;
|
||||
|
||||
// Live feed: shipped from GitHub Releases; underway/considered/ruled-out from issue labels.
|
||||
// Per-category fallback to the static items so unlabeled categories stay populated.
|
||||
let tracks = baseTracks;
|
||||
try {
|
||||
const env = await getEnv();
|
||||
const feed = await getCachedRoadmap(env.CURATED_KV, env.GITHUB_TOKEN);
|
||||
if (feed) {
|
||||
const liveByCategory: Record<string, RoadmapItem[]> = {
|
||||
Shipped: feed.shipped,
|
||||
Underway: feed.underway,
|
||||
Considered: feed.considered,
|
||||
"Ruled out": feed.ruledOut,
|
||||
已完成: feed.shipped,
|
||||
进行中: feed.underway,
|
||||
考虑中: feed.considered,
|
||||
暂不考虑: feed.ruledOut,
|
||||
};
|
||||
tracks = baseTracks.map((t) => {
|
||||
const live = liveByCategory[t.title];
|
||||
if (live && live.length > 0) {
|
||||
return { ...t, items: live.map((it) => ({ title: it.title, note: it.note })) };
|
||||
}
|
||||
return t;
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
/* keep static fallback */
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{isZh ? (
|
||||
<>
|
||||
<section className="mx-auto max-w-[1400px] px-6 pt-12 pb-8">
|
||||
<div className="flex items-baseline gap-4 mb-3">
|
||||
<Seal char="路" />
|
||||
<div className="eyebrow">Section 04 · 路线</div>
|
||||
</div>
|
||||
<h1 className="font-display tracking-crisp">
|
||||
路线图 <span className="font-cjk text-indigo text-5xl ml-2">Roadmap</span>
|
||||
</h1>
|
||||
<p className="mt-5 max-w-3xl text-ink-soft text-lg leading-[1.9] tracking-wide">
|
||||
已确认的功能、正在权衡的方案、以及已被排除的方向。未列在此页的内容均可在{" "}
|
||||
<Link href="https://github.com/Hmbown/deepseek-tui/discussions/new?category=ideas" className="body-link">
|
||||
Discussions
|
||||
</Link>{" "}
|
||||
中讨论。
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mx-auto max-w-[1400px] px-6 pb-20 grid lg:grid-cols-2 gap-px bg-paper-line">
|
||||
{tracks.map((t) => (
|
||||
<div key={t.title} className="bg-paper p-7">
|
||||
<div className={`hairline-b pb-3 mb-5 flex items-baseline justify-between border-b-2 ${colorFor(t.color)}`}>
|
||||
<div>
|
||||
<h2 className="font-display text-3xl">
|
||||
{t.title} <span className="font-cjk text-2xl ml-2 text-ink-mute">{t.cn}</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div className="font-mono text-xs uppercase tracking-widest tabular text-ink-mute">{t.items.length} 项</div>
|
||||
</div>
|
||||
<ul className="space-y-4">
|
||||
{t.items.map((it, i) => (
|
||||
<li key={i} className="flex gap-4">
|
||||
<span className={`font-display text-xl tabular shrink-0 w-8 ${colorFor(t.color)}`}>{String(i + 1).padStart(2, "0")}</span>
|
||||
<div>
|
||||
<div className="font-display text-base">{it.title}</div>
|
||||
<div className="text-sm text-ink-soft mt-0.5 leading-[1.9] tracking-wide">{it.note}</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
|
||||
<section className="bg-ink text-paper">
|
||||
<div className="mx-auto max-w-[1400px] px-6 py-12 grid lg:grid-cols-12 gap-6 items-center">
|
||||
<div className="lg:col-span-8">
|
||||
<div className="font-cjk text-indigo text-lg mb-2">参与塑造</div>
|
||||
<h2 className="font-display text-paper text-3xl">想影响这份清单?</h2>
|
||||
<p className="mt-3 text-paper-deep/80 leading-[1.9] tracking-wide max-w-2xl">
|
||||
路线图反映的是维护者的计划——但 PR 和有理有据的讨论会不断调整优先级。
|
||||
带一个可运行的原型来,"考虑中"就能变成"进行中"。
|
||||
</p>
|
||||
</div>
|
||||
<div className="lg:col-span-4 flex flex-col gap-3">
|
||||
<Link
|
||||
href="https://github.com/Hmbown/deepseek-tui/discussions/new?category=ideas"
|
||||
className="px-5 py-3 bg-indigo text-paper font-mono text-sm uppercase tracking-wider text-center hover:bg-indigo-deep transition-colors"
|
||||
>
|
||||
提交想法 →
|
||||
</Link>
|
||||
<Link
|
||||
href="https://github.com/Hmbown/deepseek-tui/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22"
|
||||
className="px-5 py-3 hairline-t hairline-b hairline-l hairline-r border-paper-deep/30 font-mono text-sm uppercase tracking-wider text-center hover:bg-paper hover:text-ink transition-colors"
|
||||
>
|
||||
Good first issues →
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<section className="mx-auto max-w-[1400px] px-6 pt-12 pb-8">
|
||||
<div className="flex items-baseline gap-4 mb-3">
|
||||
<Seal char="路" />
|
||||
<div className="eyebrow">Section 04 · 路线</div>
|
||||
</div>
|
||||
<h1 className="font-display tracking-crisp">
|
||||
Roadmap <span className="font-cjk text-indigo text-5xl ml-2">路线图</span>
|
||||
</h1>
|
||||
<p className="mt-5 max-w-3xl text-ink-soft text-lg leading-relaxed">
|
||||
What's confirmed, what's being weighed, what's been ruled out. Anything not on this page
|
||||
is fair game for{" "}
|
||||
<Link href="https://github.com/Hmbown/deepseek-tui/discussions/new?category=ideas" className="body-link">
|
||||
discussion
|
||||
</Link>.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="mx-auto max-w-[1400px] px-6 pb-20 grid lg:grid-cols-2 gap-px bg-paper-line">
|
||||
{tracks.map((t) => (
|
||||
<div key={t.title} className="bg-paper p-7">
|
||||
<div className={`hairline-b pb-3 mb-5 flex items-baseline justify-between border-b-2 ${colorFor(t.color)}`}>
|
||||
<div>
|
||||
<h2 className="font-display text-3xl">
|
||||
{t.title} <span className="font-cjk text-2xl ml-2 text-ink-mute">{t.cn}</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div className="font-mono text-xs uppercase tracking-widest tabular text-ink-mute">{t.items.length} items</div>
|
||||
</div>
|
||||
<ul className="space-y-4">
|
||||
{t.items.map((it, i) => (
|
||||
<li key={i} className="flex gap-4">
|
||||
<span className={`font-display text-xl tabular shrink-0 w-8 ${colorFor(t.color)}`}>{String(i + 1).padStart(2, "0")}</span>
|
||||
<div>
|
||||
<div className="font-display text-base">{it.title}</div>
|
||||
<div className="text-sm text-ink-soft mt-0.5 leading-relaxed">{it.note}</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
|
||||
<section className="bg-ink text-paper">
|
||||
<div className="mx-auto max-w-[1400px] px-6 py-12 grid lg:grid-cols-12 gap-6 items-center">
|
||||
<div className="lg:col-span-8">
|
||||
<div className="font-cjk text-indigo text-lg mb-2">参与塑造</div>
|
||||
<h2 className="font-display text-paper text-3xl">Want to shape this list?</h2>
|
||||
<p className="mt-3 text-paper-deep/80 leading-relaxed max-w-2xl">
|
||||
The roadmap reflects what the maintainer plans to do — but PRs and well-argued
|
||||
discussions reorder it constantly. Show up with a working prototype and watch
|
||||
"Considered" become "Underway".
|
||||
</p>
|
||||
</div>
|
||||
<div className="lg:col-span-4 flex flex-col gap-3">
|
||||
<Link
|
||||
href="https://github.com/Hmbown/deepseek-tui/discussions/new?category=ideas"
|
||||
className="px-5 py-3 bg-indigo text-paper font-mono text-sm uppercase tracking-wider text-center hover:bg-indigo-deep transition-colors"
|
||||
>
|
||||
Propose an idea →
|
||||
</Link>
|
||||
<Link
|
||||
href="https://github.com/Hmbown/deepseek-tui/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22"
|
||||
className="px-5 py-3 hairline-t hairline-b hairline-l hairline-r border-paper-deep/30 font-mono text-sm uppercase tracking-wider text-center hover:bg-paper hover:text-ink transition-colors"
|
||||
>
|
||||
Good first issues →
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getAgentEnv, safeEqual, createSession } from "@/lib/community-agent";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const ALLOWED_LOCALES = new Set(["en", "zh"]);
|
||||
|
||||
function pickLocale(value: string | null | undefined): string {
|
||||
if (!value) return "en";
|
||||
return ALLOWED_LOCALES.has(value) ? value : "en";
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const env = await getAgentEnv();
|
||||
const url = new URL(req.url);
|
||||
const localeFromQuery = pickLocale(url.searchParams.get("locale"));
|
||||
|
||||
if (!env.MAINTAINER_TOKEN) {
|
||||
return new NextResponse("Not configured", {
|
||||
status: 503,
|
||||
headers: { "Cache-Control": "no-store" },
|
||||
});
|
||||
}
|
||||
|
||||
const form = await req.formData();
|
||||
const submitted = String(form.get("token") ?? "");
|
||||
const locale = pickLocale(String(form.get("locale") ?? localeFromQuery));
|
||||
|
||||
const valid = await safeEqual(submitted, env.MAINTAINER_TOKEN);
|
||||
if (!valid) {
|
||||
return NextResponse.redirect(new URL(`/${locale}/admin?err=1`, req.url), {
|
||||
status: 303,
|
||||
headers: { "Cache-Control": "no-store" },
|
||||
});
|
||||
}
|
||||
|
||||
const sid = await createSession(env.CURATED_KV);
|
||||
if (!sid) {
|
||||
return new NextResponse("Session storage unavailable", {
|
||||
status: 503,
|
||||
headers: { "Cache-Control": "no-store" },
|
||||
});
|
||||
}
|
||||
|
||||
const res = NextResponse.redirect(new URL(`/${locale}/admin`, req.url), {
|
||||
status: 303,
|
||||
headers: { "Cache-Control": "no-store" },
|
||||
});
|
||||
res.cookies.set("mt_sid", sid, {
|
||||
path: "/",
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: "strict",
|
||||
maxAge: 60 * 60 * 24,
|
||||
});
|
||||
return res;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getAgentEnv, deleteSession } from "@/lib/community-agent";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const ALLOWED_LOCALES = new Set(["en", "zh"]);
|
||||
|
||||
function pickLocale(value: string | null | undefined): string {
|
||||
if (!value) return "en";
|
||||
return ALLOWED_LOCALES.has(value) ? value : "en";
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const env = await getAgentEnv();
|
||||
const url = new URL(req.url);
|
||||
const locale = pickLocale(url.searchParams.get("locale"));
|
||||
|
||||
const cookieHeader = req.headers.get("cookie") ?? "";
|
||||
let sid: string | undefined;
|
||||
for (const c of cookieHeader.split(";")) {
|
||||
const [name, ...rest] = c.trim().split("=");
|
||||
if (name === "mt_sid") {
|
||||
sid = rest.join("=");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await deleteSession(env.CURATED_KV, sid);
|
||||
|
||||
const res = NextResponse.redirect(new URL(`/${locale}/admin`, req.url), {
|
||||
status: 303,
|
||||
headers: { "Cache-Control": "no-store" },
|
||||
});
|
||||
res.cookies.set("mt_sid", "", {
|
||||
path: "/",
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: "strict",
|
||||
maxAge: 0,
|
||||
});
|
||||
return res;
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getAgentEnv, getDraft, deleteDraft, validateSession, type CommunityAgentEnv } from "@/lib/community-agent";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
async function checkAuth(req: Request, env: CommunityAgentEnv): Promise<{ ok: boolean; status?: number; error?: string }> {
|
||||
if (!env.MAINTAINER_TOKEN) {
|
||||
return { ok: false, status: 503, error: "MAINTAINER_TOKEN not configured" };
|
||||
}
|
||||
|
||||
const cookieHeader = req.headers.get("cookie") ?? "";
|
||||
let sid: string | undefined;
|
||||
for (const c of cookieHeader.split(";")) {
|
||||
const [name, ...rest] = c.trim().split("=");
|
||||
if (name === "mt_sid") {
|
||||
sid = rest.join("=");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!sid || !(await validateSession(env.CURATED_KV, sid))) {
|
||||
return { ok: false, status: 401, error: "unauthorized" };
|
||||
}
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const env = await getAgentEnv();
|
||||
const auth = await checkAuth(req, env);
|
||||
if (!auth.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: auth.error ?? "unauthorized" },
|
||||
{ status: auth.status ?? 401, headers: { "Cache-Control": "no-store" } }
|
||||
);
|
||||
}
|
||||
|
||||
const body = await req.json() as { action: string; draftKey: string; editedBody?: string; lang?: "en" | "zh" };
|
||||
const { action, draftKey, editedBody, lang } = body;
|
||||
|
||||
if (!draftKey) {
|
||||
return NextResponse.json({ error: "missing draftKey" }, { status: 400 });
|
||||
}
|
||||
|
||||
const draft = await getDraft(env.CURATED_KV, draftKey);
|
||||
if (!draft) {
|
||||
return NextResponse.json({ error: "draft not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
if (action === "discard") {
|
||||
await deleteDraft(env.CURATED_KV, draftKey);
|
||||
return NextResponse.json({ ok: true, action: "discarded" });
|
||||
}
|
||||
|
||||
if (action === "post") {
|
||||
if (!env.MAINTAINER_GITHUB_PAT) {
|
||||
return NextResponse.json({ error: "MAINTAINER_GITHUB_PAT not configured" }, { status: 500 });
|
||||
}
|
||||
|
||||
const commentBody = editedBody ?? (lang === "zh" ? draft.bodyZh : draft.bodyEn);
|
||||
|
||||
if (draft.type === "digest") {
|
||||
return NextResponse.json({ ok: true, action: "digest-skipped", note: "Digest pages are not posted as comments" });
|
||||
}
|
||||
|
||||
if (!draft.targetNumber) {
|
||||
return NextResponse.json({ error: "no target number" }, { status: 400 });
|
||||
}
|
||||
|
||||
const repo = env.GITHUB_REPO ?? "Hmbown/deepseek-tui";
|
||||
const commentUrl = `https://api.github.com/repos/${repo}/issues/${draft.targetNumber}/comments`;
|
||||
|
||||
const ghRes = await fetch(commentUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
Authorization: `Bearer ${env.MAINTAINER_GITHUB_PAT}`,
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ body: commentBody }),
|
||||
});
|
||||
|
||||
if (!ghRes.ok) {
|
||||
const text = await ghRes.text();
|
||||
return NextResponse.json({ error: `GitHub ${ghRes.status}: ${text}` }, { status: 502 });
|
||||
}
|
||||
|
||||
// Mark as posted
|
||||
draft.posted = true;
|
||||
await env.CURATED_KV?.put(draftKey, JSON.stringify(draft), { expirationTtl: 60 * 60 * 24 * 7 });
|
||||
|
||||
return NextResponse.json({ ok: true, action: "posted" });
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: "unknown action" }, { status: 400 });
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getEnv } from "@/lib/kv";
|
||||
import {
|
||||
runCurate,
|
||||
runTriage,
|
||||
runPrReview,
|
||||
runStale,
|
||||
runDupes,
|
||||
runDigest,
|
||||
type AgentEnv,
|
||||
} from "@/lib/community-agent-tasks";
|
||||
import { runFactsDrift } from "@/lib/facts-drift";
|
||||
import { runLinkCheck, runSemanticDrift } from "@/lib/content-watch";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const TASKS = ["curate", "triage", "pr-review", "stale", "dupes", "digest", "facts-drift", "linkcheck", "semantic-drift"] as const;
|
||||
type Task = (typeof TASKS)[number];
|
||||
|
||||
/**
|
||||
* Manual trigger surface for community-agent tasks.
|
||||
*
|
||||
* Usage:
|
||||
* GET /api/cron?task=curate
|
||||
* Header: x-cron-secret: <CRON_SECRET>
|
||||
*
|
||||
* Real cron scheduling is handled by worker.ts's scheduled() handler.
|
||||
*/
|
||||
export async function GET(req: Request) {
|
||||
const env = await getEnv();
|
||||
|
||||
// Always require auth
|
||||
if (!env.CRON_SECRET) {
|
||||
return NextResponse.json(
|
||||
{ error: "manual trigger disabled in production" },
|
||||
{ status: 503 }
|
||||
);
|
||||
}
|
||||
|
||||
const auth = req.headers.get("x-cron-secret");
|
||||
if (auth !== env.CRON_SECRET) {
|
||||
return NextResponse.json({ error: "unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(req.url);
|
||||
const task = searchParams.get("task");
|
||||
|
||||
if (!task || !TASKS.includes(task as Task)) {
|
||||
return NextResponse.json(
|
||||
{ error: `missing or invalid task. Allowed: ${TASKS.join(", ")}` },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Build AgentEnv from the same shape expected by the task functions
|
||||
const agentEnv: AgentEnv = {
|
||||
CURATED_KV: env.CURATED_KV,
|
||||
DEEPSEEK_API_KEY: env.DEEPSEEK_API_KEY,
|
||||
GITHUB_TOKEN: env.GITHUB_TOKEN,
|
||||
CRON_SECRET: env.CRON_SECRET,
|
||||
GITHUB_REPO: env.GITHUB_REPO,
|
||||
MAINTAINER_TOKEN: undefined,
|
||||
MAINTAINER_GITHUB_PAT: undefined,
|
||||
};
|
||||
|
||||
try {
|
||||
let result: Record<string, unknown>;
|
||||
switch (task) {
|
||||
case "curate":
|
||||
result = await runCurate(agentEnv);
|
||||
break;
|
||||
case "triage":
|
||||
result = await runTriage(agentEnv);
|
||||
break;
|
||||
case "pr-review":
|
||||
result = await runPrReview(agentEnv);
|
||||
break;
|
||||
case "stale":
|
||||
result = await runStale(agentEnv);
|
||||
break;
|
||||
case "dupes":
|
||||
result = await runDupes(agentEnv);
|
||||
break;
|
||||
case "digest":
|
||||
result = await runDigest(agentEnv);
|
||||
break;
|
||||
case "facts-drift":
|
||||
result = await runFactsDrift(agentEnv) as unknown as Record<string, unknown>;
|
||||
break;
|
||||
case "linkcheck":
|
||||
result = await runLinkCheck(agentEnv) as unknown as Record<string, unknown>;
|
||||
break;
|
||||
case "semantic-drift":
|
||||
result = await runSemanticDrift(agentEnv) as unknown as Record<string, unknown>;
|
||||
break;
|
||||
default:
|
||||
// unreachable — guarded by TASKS check above
|
||||
result = { error: "unknown task" };
|
||||
}
|
||||
return NextResponse.json({ ok: true, task, result });
|
||||
} catch (e) {
|
||||
return NextResponse.json({ ok: false, error: String(e) }, { status: 200 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { fetchFeed } from "@/lib/github";
|
||||
import { getEnv } from "@/lib/kv";
|
||||
|
||||
export const revalidate = 600;
|
||||
|
||||
export async function GET() {
|
||||
const env = await getEnv();
|
||||
const items = await fetchFeed(env.GITHUB_TOKEN, 50);
|
||||
return NextResponse.json({ items, fetchedAt: new Date().toISOString() });
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* ---------- root tokens — DeepSeek-aligned ---------- */
|
||||
:root {
|
||||
--paper: #ffffff;
|
||||
--paper-deep: #f4f6fb;
|
||||
--paper-edge: #e5e8f0;
|
||||
--paper-line: #0e0e10;
|
||||
--paper-line-soft: #d4d8e2;
|
||||
--ink: #0e0e10;
|
||||
--ink-soft: #2e2e33;
|
||||
--ink-mute: #6b7280;
|
||||
--indigo: #4d6bfe;
|
||||
--indigo-deep: #3a52cc;
|
||||
--indigo-pale: #e9eefe;
|
||||
--ochre: #9c7a3f;
|
||||
--jade: #0ab68b;
|
||||
--cobalt: #1f3a8a;
|
||||
}
|
||||
|
||||
/* ---------- base ---------- */
|
||||
html {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
max-width: 100%;
|
||||
overflow-x: clip;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--paper);
|
||||
color: var(--ink);
|
||||
font-family: var(--font-body), "IBM Plex Sans", "Noto Sans SC", system-ui, sans-serif;
|
||||
font-feature-settings: "ss01", "cv11", "tnum";
|
||||
max-width: 100%;
|
||||
overflow-x: clip;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* faint vertical column rule — desktop only, printed-almanac feel.
|
||||
Hidden on phones because it slices visibly through narrow content. */
|
||||
@media (min-width: 1024px) {
|
||||
body::after {
|
||||
content: "";
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background-image: linear-gradient(
|
||||
to right,
|
||||
transparent 0,
|
||||
transparent calc(50% - 0.5px),
|
||||
rgba(14,14,16,0.04) calc(50% - 0.5px),
|
||||
rgba(14,14,16,0.04) calc(50% + 0.5px),
|
||||
transparent calc(50% + 0.5px)
|
||||
);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
}
|
||||
|
||||
main, header, footer, nav { position: relative; z-index: 1; }
|
||||
|
||||
/* ---------- type ---------- */
|
||||
.font-display { font-family: var(--font-display), "Fraunces", "Noto Serif SC", Georgia, serif; }
|
||||
.font-cjk { font-family: "Noto Serif SC", "Source Han Serif SC", serif; }
|
||||
|
||||
/* CJK paragraph rhythm — looser leading, wider tracking for body; tighter for headings */
|
||||
.cjk-body {
|
||||
line-height: 1.9;
|
||||
letter-spacing: 0.02em;
|
||||
word-break: break-all;
|
||||
}
|
||||
.cjk-heading {
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.cjk-prose p {
|
||||
line-height: 1.9;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
/* Full-width punctuation should use CJK spacing */
|
||||
.cjk-prose {
|
||||
font-feature-settings: "halt", "pwid";
|
||||
}
|
||||
.font-mono { font-family: var(--font-mono), "JetBrains Mono", ui-monospace, monospace; }
|
||||
|
||||
h1, h2, h3, h4 {
|
||||
font-family: var(--font-display), "Fraunces", "Noto Serif SC", Georgia, serif;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.018em;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
h1 { font-size: clamp(2.1rem, 5vw, 4.2rem); line-height: 1; word-break: keep-all; overflow-wrap: anywhere; }
|
||||
h2 { font-size: clamp(1.5rem, 2.8vw, 2.4rem); line-height: 1.1; word-break: keep-all; overflow-wrap: anywhere; }
|
||||
h3 { font-size: 1.18rem; line-height: 1.25; }
|
||||
|
||||
@media (max-width: 640px) {
|
||||
h1 .font-cjk {
|
||||
display: inline-block;
|
||||
font-size: clamp(1.6rem, 7.5vw, 2.2rem);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
/* prevent latin/CJK heading from blowing past the viewport */
|
||||
h1, h2 { hyphens: auto; }
|
||||
}
|
||||
|
||||
/* ---------- structural primitives ---------- */
|
||||
/* Hairlines stay charcoal but softened with a touch of opacity so a pure-white
|
||||
background doesn't read as harsh black-on-white office stationery. */
|
||||
.hairline { border-color: rgba(14,14,16,0.18); }
|
||||
.hairline-t { border-top: 1px solid rgba(14,14,16,0.18); }
|
||||
.hairline-b { border-bottom: 1px solid rgba(14,14,16,0.18); }
|
||||
.hairline-l { border-left: 1px solid rgba(14,14,16,0.18); }
|
||||
.hairline-r { border-right: 1px solid rgba(14,14,16,0.18); }
|
||||
|
||||
.double-rule {
|
||||
background-image:
|
||||
linear-gradient(rgba(14,14,16,0.18), rgba(14,14,16,0.18)),
|
||||
linear-gradient(rgba(14,14,16,0.18), rgba(14,14,16,0.18));
|
||||
background-size: 100% 1px, 100% 1px;
|
||||
background-position: top, bottom;
|
||||
background-repeat: no-repeat;
|
||||
padding: 0.45rem 0;
|
||||
}
|
||||
|
||||
.col-rule > * + * {
|
||||
border-left: 1px solid rgba(14,14,16,0.18);
|
||||
}
|
||||
/* Single-column phones: drop the column rules so cards stack flush. */
|
||||
@media (max-width: 767px) {
|
||||
.col-rule > * + * { border-left: 0; border-top: 1px solid rgba(14,14,16,0.18); }
|
||||
}
|
||||
|
||||
/* small-caps eyebrow */
|
||||
.eyebrow {
|
||||
font-family: var(--font-mono), "JetBrains Mono", monospace;
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--ink-mute);
|
||||
}
|
||||
|
||||
/* ---------- the seal — ink-stamped, not vermillion ---------- */
|
||||
.seal {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--ink);
|
||||
color: var(--paper);
|
||||
font-family: "Noto Serif SC", serif;
|
||||
font-weight: 700;
|
||||
width: 2.6rem;
|
||||
height: 2.6rem;
|
||||
border-radius: 1px;
|
||||
letter-spacing: -0.04em;
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(244,241,232,0.18),
|
||||
inset 0 0 0 3px var(--ink);
|
||||
transform: rotate(-1.5deg);
|
||||
position: relative;
|
||||
}
|
||||
.seal::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: -1px;
|
||||
background:
|
||||
radial-gradient(rgba(244,241,232,0.35) 1px, transparent 1px) 0 0 / 4px 4px;
|
||||
mix-blend-mode: screen;
|
||||
border-radius: 1px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* indigo-stamped variant — used sparingly for the brand mark / featured anchor */
|
||||
.seal-indigo {
|
||||
background: var(--indigo);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(244,241,232,0.22),
|
||||
inset 0 0 0 3px var(--indigo);
|
||||
}
|
||||
|
||||
/* ---------- pills / status ---------- */
|
||||
.pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
padding: 0.12rem 0.45rem;
|
||||
font-family: var(--font-mono), monospace;
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
border: 1px solid var(--paper-line);
|
||||
background: var(--paper);
|
||||
color: var(--ink);
|
||||
}
|
||||
.pill-hot { background: var(--indigo); color: var(--paper); border-color: var(--indigo); }
|
||||
.pill-new { background: var(--paper); color: var(--ink); border-color: var(--ink); }
|
||||
.pill-jade { background: var(--jade); color: var(--paper); border-color: var(--jade); }
|
||||
.pill-ochre { background: var(--ochre); color: var(--paper); border-color: var(--ochre); }
|
||||
.pill-ghost { background: transparent; color: var(--ink-mute); border-color: var(--ink-mute); }
|
||||
|
||||
/* ---------- numbers ---------- */
|
||||
.tabular { font-variant-numeric: tabular-nums; }
|
||||
.bignum {
|
||||
font-family: var(--font-display), "Fraunces", serif;
|
||||
font-weight: 600;
|
||||
font-size: 2.2rem;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.04em;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* ---------- code blocks ---------- */
|
||||
pre.code-block {
|
||||
background: #0e0e10;
|
||||
color: #e6e8f0;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
padding: 1rem 1.1rem;
|
||||
font-family: var(--font-mono), monospace;
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.55;
|
||||
border: 1px solid rgba(14,14,16,0.18);
|
||||
overflow-x: auto;
|
||||
position: relative;
|
||||
white-space: pre;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
pre.code-block::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0; left: 0; right: 0;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, var(--indigo) 0 70%, var(--jade) 70% 100%);
|
||||
}
|
||||
pre.code-block .prompt { color: var(--indigo); }
|
||||
pre.code-block .comment { color: #8b8f9a; }
|
||||
pre.code-block .key { color: var(--ochre); }
|
||||
|
||||
@media (max-width: 640px) {
|
||||
pre.code-block { font-size: 0.76rem; padding: 0.85rem 0.95rem; }
|
||||
}
|
||||
|
||||
code.inline {
|
||||
background: var(--paper-deep);
|
||||
border: 1px solid rgba(14,14,16,0.14);
|
||||
padding: 0.05rem 0.32rem;
|
||||
font-family: var(--font-mono), monospace;
|
||||
font-size: 0.85em;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* ---------- nav link ---------- */
|
||||
.nav-link {
|
||||
font-family: var(--font-mono), monospace;
|
||||
font-size: 0.78rem;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--ink);
|
||||
position: relative;
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
.nav-link::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0; right: 0; bottom: -2px;
|
||||
height: 2px;
|
||||
background: var(--indigo);
|
||||
transform: scaleX(0);
|
||||
transform-origin: left;
|
||||
transition: transform 180ms ease;
|
||||
}
|
||||
.nav-link:hover::after, .nav-link[aria-current="page"]::after { transform: scaleX(1); }
|
||||
|
||||
/* ---------- ticker ---------- */
|
||||
.ticker-track {
|
||||
display: inline-flex;
|
||||
gap: 3rem;
|
||||
white-space: nowrap;
|
||||
animation: ticker 80s linear infinite;
|
||||
padding-right: 3rem;
|
||||
}
|
||||
@keyframes ticker {
|
||||
from { transform: translateX(0); }
|
||||
to { transform: translateX(-50%); }
|
||||
}
|
||||
|
||||
/* ---------- decorative big CJK in margin ---------- */
|
||||
.margin-glyph {
|
||||
font-family: "Noto Serif SC", serif;
|
||||
font-weight: 700;
|
||||
color: var(--ink);
|
||||
opacity: 0.04;
|
||||
font-size: 18rem;
|
||||
line-height: 0.9;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
/* ---------- focus + selection ---------- */
|
||||
::selection { background: var(--indigo); color: var(--paper); }
|
||||
:focus-visible { outline: 2px solid var(--indigo); outline-offset: 2px; }
|
||||
|
||||
/* ---------- link reset ---------- */
|
||||
a { color: inherit; text-decoration: none; }
|
||||
a.body-link {
|
||||
color: var(--ink);
|
||||
background-image: linear-gradient(var(--indigo), var(--indigo));
|
||||
background-repeat: no-repeat;
|
||||
background-position: 0 100%;
|
||||
background-size: 100% 1px;
|
||||
transition: background-size 180ms ease;
|
||||
}
|
||||
a.body-link:hover { background-size: 100% 6px; color: var(--ink); }
|
||||
|
||||
/* ---------- mermaid container ----------
|
||||
Mermaid renders its own SVG; we just give it room to breathe and a
|
||||
horizontal scroll on phones so the diagram never overflows the viewport. */
|
||||
.mermaid-frame {
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
.mermaid-frame svg {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* ---------- mobile-only adjustments ---------- */
|
||||
@media (max-width: 640px) {
|
||||
/* Big CJK margin glyph already hidden via tailwind's `hidden lg:block`,
|
||||
but re-assert that nothing hits the viewport edge. */
|
||||
.margin-glyph { display: none; }
|
||||
|
||||
/* Ticker text gets cramped on phones — shrink + tighten gaps */
|
||||
.ticker-track { gap: 1.5rem; padding-right: 1.5rem; }
|
||||
}
|
||||
|
||||
/* Anchor scroll-margin so deep links land below the sticky nav. */
|
||||
[id] { scroll-margin-top: 5rem; }
|
||||
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<rect width="32" height="32" fill="#0E0E10"/>
|
||||
<text x="50%" y="55%" text-anchor="middle" dominant-baseline="middle"
|
||||
font-family="'Noto Serif SC', serif" font-weight="700" font-size="20" fill="#F4F1E8">深</text>
|
||||
<rect x="0" y="29" width="32" height="3" fill="#4D6BFE"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 352 B |
@@ -0,0 +1,56 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Fraunces, IBM_Plex_Sans, JetBrains_Mono, Noto_Serif_SC } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
const display = Fraunces({
|
||||
subsets: ["latin"],
|
||||
weight: ["400", "500", "600", "700"],
|
||||
variable: "--font-display",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const body = IBM_Plex_Sans({
|
||||
subsets: ["latin"],
|
||||
weight: ["400", "500", "600"],
|
||||
variable: "--font-body",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const mono = JetBrains_Mono({
|
||||
subsets: ["latin"],
|
||||
weight: ["400", "500", "600"],
|
||||
variable: "--font-mono",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
// Noto Serif SC is heavy; load only what we need for decorative anchors.
|
||||
const cjk = Noto_Serif_SC({
|
||||
subsets: ["latin"],
|
||||
weight: ["400", "700"],
|
||||
variable: "--font-cjk",
|
||||
display: "swap",
|
||||
preload: false,
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "DeepSeek TUI · 深度求索 终端",
|
||||
description:
|
||||
"Terminal-native coding agent built on DeepSeek V4. Open source. Community site for installation, docs, roadmap, and live activity from the Hmbown/deepseek-tui repo.",
|
||||
metadataBase: new URL("https://deepseek-tui.com"),
|
||||
openGraph: {
|
||||
title: "DeepSeek TUI",
|
||||
description: "Terminal-native coding agent built on DeepSeek V4.",
|
||||
url: "https://deepseek-tui.com",
|
||||
siteName: "DeepSeek TUI",
|
||||
type: "website",
|
||||
},
|
||||
twitter: { card: "summary_large_image" },
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en" className={`${display.variable} ${body.variable} ${mono.variable} ${cjk.variable}`}>
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user