Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/components/home/LiteBand.astro
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
// The Lite band on the home page: the local gateway, its four points, where it
// The Lite band on the home page: the local gateway, its main points, where it
// runs, and a download, beside its overview screen. Each page shows the
// interface in its own language; see LitePage.astro.
import { getLang, localePath } from "~/i18n";
Expand Down Expand Up @@ -72,6 +72,10 @@ const zh = lang === "zh-CN";
.points {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
/* an odd point out takes the whole last row, so no cell stays empty */
.points > :last-child:nth-child(odd) {
grid-column: 1 / -1;
}
}
.pills {
margin-top: 28px;
Expand Down
118 changes: 114 additions & 4 deletions src/components/pages/LitePage.astro
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,27 @@ const remoteDocsHref = localePath(lang, "/docs/lite/remote-core");
const serverDocsHref = localePath(lang, "/docs/core/server-deployment");
const jsonLd = await liteLd(lang);
const changelogHref = localePath(lang, "/changelog");
/** Feature copy by id: the story and the bento pick their text from it */
const feat = Object.fromEntries(c.features.items.map((item) => [item.id, item])) as Record<string, (typeof c.features.items)[number]>;
/** Feature copy by id: the story and the bento pick their text from it. Plugins have no screenshot, so no alt. */
const feat = Object.fromEntries(c.features.items.map((item) => [item.id, item])) as Record<
string,
{ id: string; title: string; body: string; alt?: string }
>;
/** Permissions a plugin declares, as named in the plugins doc */
const pluginPermissions = ["system", "messages", "tools", "params", "reply.text", "reply.tool_calls"];
/** The short plugin on the plugins tile, as highlighted lines: the doc's example, cut down */
const escHtml = (s: string) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
const pluginCode = [
`<span class="k">export const</span> manifest = {`,
` name: <span class="s">"${escHtml(c.motion.bento.plugin.name)}"</span>,`,
` api: <span class="n">1</span>,`,
`<span class="hl"> permissions: [<span class="s">"system"</span>],</span>`,
`};`,
``,
`<span class="k">export function</span> onRequest(req) {`,
` req.system += <span class="s">"\\n\\n${escHtml(c.motion.bento.plugin.note)}"</span>;`,
` <span class="k">return</span> req;`,
`}`,
].join("\n");
/** The four stops of the scroll story, in the order a request meets them */
const storyIds = ["routing", "security", "traffic", "overview"] as const;
const clients = [
Expand Down Expand Up @@ -308,6 +327,19 @@ const archLabel = { x64: "x64", arm64: "ARM64" } as const;
<h3>{c.motion.bento.noticesTitle}</h3>
<p>{c.menubar.notices}</p>
</article>
<article class="tile t-plugins lp-in">
<div class="tile-text">
<div class="chips perms" aria-hidden="true">
{pluginPermissions.map((p) => <span>{p}</span>)}
</div>
<h3>{feat.plugins.title}</h3>
<p>{feat.plugins.body}</p>
</div>
<div class="code-win" aria-hidden="true">
<div class="code-top"><span class="lights"><i></i><i></i><i></i></span><span>project-notes.js</span></div>
<pre><code set:html={pluginCode} /></pre>
</div>
</article>
</div>
</div>
</section>
Expand Down Expand Up @@ -1687,6 +1719,69 @@ const archLabel = { x64: "x64", arm64: "ARM64" } as const;
.chips span:nth-child(4) {
animation-delay: 3s;
}
/* The plugins tile: the whole last row, the copy beside a short plugin */
.t-plugins {
grid-column: span 6;
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 40px;
align-items: center;
}
.perms span {
font-family: var(--font-mono);
animation-duration: 6s;
}
.perms span:nth-child(5) {
animation-delay: 4s;
}
.perms span:nth-child(6) {
animation-delay: 5s;
}
.code-win {
min-width: 0;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
background: #0b0c0e;
box-shadow: 0 30px 80px -30px rgba(0, 0, 0, 0.8);
overflow: hidden;
}
.code-top {
display: flex;
align-items: center;
gap: 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
padding: 9px 14px;
font-family: var(--font-mono);
font-size: 11.5px;
color: var(--color-dim);
}
.code-win pre {
margin: 0;
padding: 14px 18px 16px;
overflow-x: auto;
font-family: var(--font-mono);
font-size: 12.5px;
line-height: 1.75;
color: #c3c7ce;
}
.code-win :global(.k) {
color: rgb(var(--a2));
}
.code-win :global(.s) {
color: rgb(var(--a1));
}
.code-win :global(.n) {
color: #8fb8ff;
}
/* the line that declares what the plugin may read and change */
.code-win :global(.hl) {
display: inline-block;
min-width: calc(100% + 36px);
margin: 0 -18px;
padding: 0 18px;
background: rgb(var(--a1) / 0.07);
box-shadow: inset 2px 0 rgb(var(--a1) / 0.7);
}
.keyrows {
margin: 0;
padding: 0;
Expand Down Expand Up @@ -1841,7 +1936,8 @@ const archLabel = { x64: "x64", arm64: "ARM64" } as const;
}
.t-menubar,
.t-mcp,
.t-notices {
.t-notices,
.t-plugins {
grid-column: span 2;
}
.t-clients,
Expand All @@ -1861,10 +1957,24 @@ const archLabel = { x64: "x64", arm64: "ARM64" } as const;
.t-formats,
.t-keys,
.t-dry,
.t-notices {
.t-notices,
.t-plugins {
grid-column: auto;
grid-row: auto;
}
.t-plugins {
grid-template-columns: 1fr;
gap: 24px;
}
.code-win pre {
padding: 12px 14px 14px;
font-size: 12px;
}
.code-win :global(.hl) {
min-width: calc(100% + 28px);
margin: 0 -14px;
padding: 0 14px;
}
.t-menubar {
grid-template-columns: 1fr;
gap: 0;
Expand Down
2 changes: 2 additions & 0 deletions src/i18n/pages/home.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export const homeCopy = {
{ t: "Protection against relays", b: "Credentials can be replaced before a request leaves, so a relay never holds them, and dangerous tool calls a relay slips into an answer can be cut off before the client runs them. MCP servers, skills and hooks are scanned as well." },
{ t: "Every request traceable", b: "The matched rule, each upstream attempt and the cost of every request, with replay against another upstream." },
{ t: "Costs stated as they are", b: "Estimated amounts are marked, and requests without a price are counted separately rather than as zero." },
{ t: "Plugins that adapt requests and answers", b: "Short JavaScript plugins adjust requests and answers, such as asking for answers in a chosen language. They run in a sandbox, see placeholders instead of keys, and every change they make is recorded." },
],
pills: ["Available", "macOS (Apple silicon)", "Windows (x64, ARM64)", "Linux (x86_64, aarch64)"],
shotAlt:
Expand Down Expand Up @@ -214,6 +215,7 @@ export const homeCopy = {
{ t: "防范中转站", b: "请求发出前可替换其中的凭据,中转站拿不到原值;中转站在回答中塞入的危险工具调用,可在客户端执行前切断。MCP 服务器、技能与钩子也会被扫描。" },
{ t: "每个请求都可追溯", b: "每个请求命中的规则、尝试过的上游与费用都有记录,也可以重放到另一个上游对比。" },
{ t: "费用如实计算", b: "估算的金额单独标注,无法计价的请求单独计数,不按零计入。" },
{ t: "插件改写请求与回答", b: "简短的 JavaScript 插件可以调整请求与回答,例如要求用指定的语言回答。插件在沙箱中运行,看到的是占位符而不是密钥,每一处改动都有记录。" },
],
pills: ["已发布", "macOS(Apple silicon)", "Windows(x64、ARM64)", "Linux(x86_64、aarch64)"],
shotAlt:
Expand Down
14 changes: 14 additions & 0 deletions src/i18n/pages/lite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ export const liteCopy = {
},
],
now: "now",
// The short plugin on the plugins tile: the example in the plugins doc, cut down
plugin: { name: "Project notes", note: "Use pnpm." },
},
},
status: {
Expand Down Expand Up @@ -167,6 +169,12 @@ export const liteCopy = {
body: "Connecting a client gives it a key of its own, so traffic and cost are counted per client. Each key has its own route, visible models and concurrency limit, and a rotated key is written into its client's configuration.",
alt: "The Keys page: the default key and one key each for Claude Code, Codex and Cursor, with the route each key uses, the models it may use, and its requests and cost over the last 24 hours",
},
// No screenshot: the bento shows a short plugin instead, so there is no alt.
{
id: "plugins",
title: "Plugins that adapt requests and answers",
body: "Short JavaScript plugins adapt requests and answers to a particular setup; the app includes two, one for answers in a chosen language and one for converting file paths between WSL and Windows. Each plugin runs in a sandbox without network or file access, reads and changes only the parts its permissions declare, and receives placeholders instead of keys; every change is recorded on the request and checked by the same protections as anything a client sends.",
},
],
},
remote: {
Expand Down Expand Up @@ -331,6 +339,7 @@ export const liteCopy = {
{ title: "与 homelab 的连接已断开", body: "正在重新连接。连接恢复前,应用中的内容停留在断开时的状态。" },
],
now: "现在",
plugin: { name: "附加项目说明", note: "使用 pnpm。" },
},
},
status: {
Expand Down Expand Up @@ -402,6 +411,11 @@ export const liteCopy = {
body: "接入客户端时为它生成专用密钥,流量与费用按客户端分开统计。每把密钥可单独设置路由、可见模型与并发上限,更换后的新密钥自动写入客户端配置。",
alt: "密钥页:默认密钥与 Claude Code、Codex、Cursor 各自的密钥,列出各自使用的路由、可用模型,以及最近 24 小时的请求数与费用",
},
{
id: "plugins",
title: "插件改写请求与回答",
body: "简短的 JavaScript 插件让请求与回答适应具体的使用场景;应用自带两个插件,分别用于指定回答语言、在 WSL 与 Windows 之间转换路径。插件在没有网络与文件访问的沙箱中运行,只能读取和改动权限中声明的部分,看到的是占位符而不是密钥;每一处改动都记录在请求上,并与客户端发来的内容一样经过各项防护的检查。",
},
],
},
remote: {
Expand Down
17 changes: 12 additions & 5 deletions src/lib/structured-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,18 @@ export async function liteLd(lang: Lang): Promise<JsonLd[]> {
.filter(([name]) => /(\.dmg|-setup\.exe|\.AppImage)$/.test(name))
.map(([, href]) => href);
// Screenshots in the page's language; the English ones carry "-en", as on LitePage.astro.
const screenshot = c.features.items.map((item) => ({
"@type": "ImageObject",
url: abs(`/lite/${item.id}${lang === "zh-CN" ? "" : "-en"}.webp`),
caption: item.alt,
}));
// A feature without alt text (plugins) has no screenshot.
const screenshot = c.features.items.flatMap((item) =>
"alt" in item
? [
{
"@type": "ImageObject",
url: abs(`/lite/${item.id}${lang === "zh-CN" ? "" : "-en"}.webp`),
caption: item.alt,
},
]
: [],
);
return [
{
"@context": CONTEXT,
Expand Down
Loading