feat: prepare GoodBuddy 0.8.0

This commit is contained in:
lofyer
2026-08-05 19:40:40 +08:00
parent 38ac2206f2
commit 39a457ded8
96 changed files with 14221 additions and 1027 deletions
+46
View File
@@ -0,0 +1,46 @@
# GoodBuddy 静态官网
`sites` 是无需构建步骤或额外依赖的静态官网源码,可直接托管整个目录。
## 本地预览
在仓库根目录运行:
```powershell
python -m http.server 4173 --bind 127.0.0.1 --directory sites
```
然后访问 <http://localhost:4173/>。也可以直接用浏览器打开 `sites/index.html`
## 校验
```powershell
node sites/scripts/validate.mjs
node --check sites/app.js
node --check sites/site.config.js
```
校验脚本会检查必需文件、页内链接、本地资源、关键产品文案、主题与响应式规则,以及未发布状态下的下载链接保护。
## Release 配置
未来 v0.8.0 Release 地址集中在 `site.config.js`
```js
window.GOODBUDDY_SITE_CONFIG = Object.freeze({
version: "0.8.0",
releasePublished: false,
releaseUrl: "https://github.com/mesalogo/goodbuddy/releases/tag/v0.8.0",
});
```
正式 Release 确认发布后,将 `releasePublished` 改为 `true`,页面上的下载入口才会指向 Release 页面。官网不配置或猜测具体安装资产名称。
## 文件
- `index.html`:页面结构与简体中文内容
- `styles.css`:语义令牌、浅深主题、焦点与响应式布局
- `app.js`:主题、移动导航、当前章节和 Release 状态
- `site.config.js`:版本与未来 Release 地址
- `assets/favicon.svg`:站点图标
- `scripts/validate.mjs`:无依赖静态检查
+157
View File
@@ -0,0 +1,157 @@
(() => {
"use strict";
const root = document.documentElement;
const header = document.querySelector("[data-site-header]");
const menuToggle = document.querySelector("[data-menu-toggle]");
const navigation = document.querySelector("[data-navigation]");
const themeToggle = document.querySelector("[data-theme-toggle]");
const themeColor = document.querySelector('meta[name="theme-color"]');
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)");
const config = window.GOODBUDDY_SITE_CONFIG;
const getSavedTheme = () => {
try {
const savedTheme = localStorage.getItem("goodbuddy-site-theme");
return savedTheme === "light" || savedTheme === "dark" ? savedTheme : null;
} catch {
return null;
}
};
const applyTheme = (theme, persist = false) => {
root.dataset.theme = theme;
themeToggle?.setAttribute(
"aria-label",
theme === "dark" ? "切换为浅色主题" : "切换为深色主题",
);
themeColor?.setAttribute("content", theme === "dark" ? "#07101f" : "#f6f8fb");
if (persist) {
try {
localStorage.setItem("goodbuddy-site-theme", theme);
} catch {
// The selected theme still applies for the current page.
}
}
};
const closeMenu = () => {
header?.classList.remove("is-menu-open");
menuToggle?.setAttribute("aria-expanded", "false");
menuToggle?.setAttribute("aria-label", "打开导航");
};
const setHeaderState = () => {
header?.classList.toggle("is-scrolled", window.scrollY > 12);
};
const configureReleaseLinks = () => {
const releaseLinks = document.querySelectorAll("[data-release-link]");
const isReady =
config?.releasePublished === true &&
typeof config.releaseUrl === "string" &&
/^https:\/\/github\.com\/mesalogo\/goodbuddy\/releases\/tag\/v0\.8\.0$/.test(
config.releaseUrl,
);
releaseLinks.forEach((link) => {
if (!isReady) {
link.removeAttribute("href");
link.removeAttribute("target");
link.removeAttribute("rel");
link.setAttribute("aria-disabled", "true");
link.classList.add("is-disabled");
link.textContent = "发布后开放";
return;
}
link.href = config.releaseUrl;
link.target = "_blank";
link.rel = "noreferrer";
link.removeAttribute("aria-disabled");
link.classList.remove("is-disabled");
link.innerHTML = `前往 v${config.version} Release<span class="sr-only">(在新窗口打开)</span>`;
});
};
applyTheme(getSavedTheme() ?? (systemTheme.matches ? "dark" : "light"));
configureReleaseLinks();
setHeaderState();
themeToggle?.addEventListener("click", () => {
applyTheme(root.dataset.theme === "dark" ? "light" : "dark", true);
});
systemTheme.addEventListener("change", (event) => {
if (!getSavedTheme()) {
applyTheme(event.matches ? "dark" : "light");
}
});
menuToggle?.addEventListener("click", () => {
const willOpen = !header?.classList.contains("is-menu-open");
header?.classList.toggle("is-menu-open", willOpen);
menuToggle.setAttribute("aria-expanded", String(willOpen));
menuToggle.setAttribute("aria-label", willOpen ? "关闭导航" : "打开导航");
});
navigation?.addEventListener("click", (event) => {
if (event.target instanceof HTMLAnchorElement) {
closeMenu();
}
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape" && header?.classList.contains("is-menu-open")) {
closeMenu();
menuToggle?.focus();
}
});
document.addEventListener("click", (event) => {
if (
header?.classList.contains("is-menu-open") &&
event.target instanceof Node &&
!header.contains(event.target)
) {
closeMenu();
}
});
window.addEventListener("scroll", setHeaderState, { passive: true });
const sections = [...document.querySelectorAll("main section[id]")];
const navLinks = [...document.querySelectorAll('.site-navigation a[href^="#"]')];
if ("IntersectionObserver" in window) {
const observer = new IntersectionObserver(
(entries) => {
const visibleSection = entries
.filter((entry) => entry.isIntersecting)
.sort((left, right) => right.intersectionRatio - left.intersectionRatio)[0];
if (!visibleSection) {
return;
}
navLinks.forEach((link) => {
const isCurrent = link.getAttribute("href") === `#${visibleSection.target.id}`;
if (isCurrent) {
link.setAttribute("aria-current", "true");
} else {
link.removeAttribute("aria-current");
}
});
},
{ rootMargin: "-25% 0px -55%", threshold: [0.05, 0.2, 0.5] },
);
sections.forEach((section) => observer.observe(section));
}
const currentYear = document.querySelector("[data-current-year]");
if (currentYear) {
currentYear.textContent = String(new Date().getFullYear());
}
})();
+12
View File
@@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<defs>
<linearGradient id="g" x1="8" y1="8" x2="56" y2="56" gradientUnits="userSpaceOnUse">
<stop stop-color="#0877e8"/>
<stop offset="1" stop-color="#08b89b"/>
</linearGradient>
</defs>
<rect width="64" height="64" rx="16" fill="#fff"/>
<path d="M9 34a14 14 0 1 1 28 0v12H23A14 14 0 0 1 9 34Z" fill="none" stroke="url(#g)" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M27 34a14 14 0 1 1 28 0 14 14 0 0 1-28 0Z" fill="none" stroke="url(#g)" stroke-width="7"/>
<path d="M32 20v-7M41 17l5-5M23 17l-5-5" fill="none" stroke="url(#g)" stroke-width="4" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 702 B

+538
View File
@@ -0,0 +1,538 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta
name="description"
content="GoodBuddy 是安全可控的桌面智能助手与 Agent 工作空间。0.8.0 即将带来 Subagent、智能路由与 IM 开发者预览。"
/>
<meta name="theme-color" content="#f6f8fb" />
<title>GoodBuddy|安全可控的桌面智能助手</title>
<link rel="icon" href="./assets/favicon.svg" type="image/svg+xml" />
<link rel="stylesheet" href="./styles.css" />
<script>
(() => {
try {
const savedTheme = localStorage.getItem("goodbuddy-site-theme");
const systemDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
document.documentElement.dataset.theme =
savedTheme === "light" || savedTheme === "dark"
? savedTheme
: systemDark
? "dark"
: "light";
} catch {
document.documentElement.dataset.theme = "light";
}
})();
</script>
</head>
<body>
<a class="skip-link" href="#main-content">跳到主要内容</a>
<header class="site-header" data-site-header>
<div class="header-inner">
<a class="brand" href="#home" aria-label="GoodBuddy 首页">
<svg class="brand-mark" viewBox="0 0 40 40" aria-hidden="true">
<path d="M6 21a9 9 0 1 1 18 0v8H15a9 9 0 0 1-9-8Z" />
<path d="M16 21a9 9 0 1 1 18 0 9 9 0 0 1-18 0Z" />
<path d="M20 13V8M25 10l3-3M15 10l-3-3" />
</svg>
<span>GoodBuddy</span>
</a>
<button
class="icon-button menu-toggle"
type="button"
aria-label="打开导航"
aria-expanded="false"
aria-controls="site-navigation"
data-menu-toggle
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M4 7h16M4 12h16M4 17h16" />
</svg>
</button>
<nav class="site-navigation" id="site-navigation" aria-label="主导航" data-navigation>
<a href="#features">功能</a>
<a href="#release">0.8.0</a>
<a href="#download">下载</a>
<a href="#security">安全</a>
</nav>
<div class="header-actions">
<button class="icon-button" type="button" aria-label="切换为深色主题" data-theme-toggle>
<svg class="theme-icon theme-icon--sun" viewBox="0 0 24 24" aria-hidden="true">
<circle cx="12" cy="12" r="4" />
<path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4" />
</svg>
<svg class="theme-icon theme-icon--moon" viewBox="0 0 24 24" aria-hidden="true">
<path d="M20.4 14.6A8.5 8.5 0 0 1 9.4 3.6a8.5 8.5 0 1 0 11 11Z" />
</svg>
</button>
<a
class="button button--quiet header-github"
href="https://github.com/mesalogo/goodbuddy"
target="_blank"
rel="noreferrer"
>
GitHub
<span class="sr-only">(在新窗口打开)</span>
</a>
</div>
</div>
</header>
<main id="main-content">
<section class="hero section" id="home" aria-labelledby="hero-title">
<div class="section-inner hero-grid">
<div class="hero-copy">
<div class="eyebrow">
<span class="status-dot" aria-hidden="true"></span>
GoodBuddy 0.8.0 即将发布
</div>
<h1 id="hero-title">把 AI 放在桌面,<br /><span>也把控制权留在手中。</span></h1>
<p class="hero-lead">
GoodBuddy 是安全可控的桌面智能助手与 Agent 工作空间。连接模型、知识与工具,
在清晰的范围和审批边界内完成真正的工作。
</p>
<div class="hero-actions">
<a class="button button--primary" href="#release">查看 0.8.0 亮点</a>
<a
class="button button--secondary is-disabled"
aria-disabled="true"
data-release-link
>发布后开放</a>
</div>
<ul class="hero-facts" aria-label="产品特性概览">
<li>
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="m5 10 3 3 7-7" /></svg>
Windows / macOS / Linux
</li>
<li>
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="m5 10 3 3 7-7" /></svg>
项目范围隔离
</li>
<li>
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="m5 10 3 3 7-7" /></svg>
工具调用可审批
</li>
</ul>
</div>
<div
class="product-stage"
role="img"
aria-label="GoodBuddy 桌面应用界面示意:在项目范围内对话、引用知识并审批工具调用"
>
<div class="stage-glow stage-glow--one"></div>
<div class="stage-glow stage-glow--two"></div>
<div class="app-window">
<div class="window-bar">
<div class="window-dots" aria-hidden="true"><span></span><span></span><span></span></div>
<div class="window-title">GoodBuddy</div>
<div class="window-status"><span></span> 本地工作区</div>
</div>
<div class="app-layout">
<aside class="app-sidebar" aria-hidden="true">
<div class="mini-brand">
<svg viewBox="0 0 40 40">
<path d="M6 21a9 9 0 1 1 18 0v8H15a9 9 0 0 1-9-8Z" />
<path d="M16 21a9 9 0 1 1 18 0 9 9 0 0 1-18 0Z" />
</svg>
</div>
<div class="side-item is-active"><span></span>对话</div>
<div class="side-item"><span></span>知识库</div>
<div class="side-item"><span></span>智能心跳</div>
<div class="side-item"><span></span>任务与活动</div>
<div class="sidebar-spacer"></div>
<div class="side-item"><span></span>设置</div>
</aside>
<div class="app-content">
<div class="app-content-header">
<div>
<strong>产品发布准备</strong>
<span>项目:GoodBuddy 0.8.0</span>
</div>
<div class="mode-pill">计划模式</div>
</div>
<div class="message-area">
<div class="message message--user">梳理 0.8.0 发布前还需要完成的工作。</div>
<div class="message message--assistant">
<div class="assistant-label">
<span class="assistant-avatar">G</span>
<strong>GoodBuddy</strong>
</div>
<p>我会先核对发布清单与项目知识,再给出不执行变更的计划。</p>
<div class="tool-card">
<div class="tool-icon">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 6h16M4 12h10M4 18h7" /></svg>
</div>
<div><strong>读取项目知识</strong><span>范围:GoodBuddy 0.8.0</span></div>
<span class="tool-state">已完成</span>
</div>
<div class="plan-lines" aria-hidden="true"><span></span><span></span><span></span></div>
</div>
</div>
<div class="composer">
<span>继续补充要求…</span>
<div class="composer-actions"><span>计划</span><b></b></div>
</div>
</div>
</div>
</div>
<div class="floating-card floating-card--approval">
<span class="floating-icon">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3 5 6v5c0 4.5 2.8 8.6 7 10 4.2-1.4 7-5.5 7-10V6l-7-3Z" /><path d="m9 12 2 2 4-4" /></svg>
</span>
<span><strong>执行前确认</strong><small>每次工具调用都清晰可见</small></span>
</div>
<div class="floating-card floating-card--scope">
<span class="scope-dot"></span>
<span><strong>项目范围</strong><small>上下文不会悄悄混用</small></span>
</div>
</div>
</div>
</section>
<section class="proof-strip" aria-label="核心设计原则">
<div class="section-inner proof-grid">
<div><strong>3 种</strong><span>问答 / 计划 / 执行模式</span></div>
<div><strong>2 层</strong><span>全局与项目知识范围</span></div>
<div><strong>明确</strong><span>工具权限与活动记录</span></div>
<div><strong>跨平台</strong><span>x64 与 arm64</span></div>
</div>
</section>
<section class="section features-section" id="features" aria-labelledby="features-title">
<div class="section-inner">
<div class="section-heading">
<div>
<p class="kicker">围绕真实工作流设计</p>
<h2 id="features-title">不是另一个聊天窗口</h2>
</div>
<p>
从上下文组织到执行审批,每一步都让范围、状态和风险保持可见。
</p>
</div>
<div class="feature-grid">
<article class="feature-card feature-card--wide">
<div class="feature-icon">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 3 5 6v5c0 4.5 2.8 8.6 7 10 4.2-1.4 7-5.5 7-10V6l-7-3Z" />
<path d="M9 12h6M12 9v6" />
</svg>
</div>
<span class="feature-number">01</span>
<h3>受控 Agent 运行时</h3>
<p>问答与计划模式在运行时保持只读;执行模式中的工具操作经过现有审批控制,并保留取消、超时与输出边界。</p>
<div class="mode-row" aria-label="三种工作模式">
<span>问答 <small>只读</small></span>
<span>计划 <small>只读</small></span>
<span class="is-accent">执行 <small>需审批</small></span>
</div>
</article>
<article class="feature-card">
<div class="feature-icon">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M4 6.5C4 5.1 5.1 4 6.5 4H10l2 2h5.5C18.9 6 20 7.1 20 8.5v9c0 1.4-1.1 2.5-2.5 2.5h-11A2.5 2.5 0 0 1 4 17.5v-11Z" />
<path d="M8 11h8M8 15h5" />
</svg>
</div>
<span class="feature-number">02</span>
<h3>有范围的知识</h3>
<p>区分全局与项目知识。搜索、引用和创建都围绕当前范围展开,让上下文来源清楚可追溯。</p>
</article>
<article class="feature-card">
<div class="feature-icon">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M4 13h3l2-6 4 12 2-6h5" />
<path d="M4 4h16v16H4z" />
</svg>
</div>
<span class="feature-number">03</span>
<h3>智能心跳与任务</h3>
<p>将周期计划、运行状态、结果与活动记录放在同一条可检查的工作链路中。</p>
</article>
<article class="feature-card">
<div class="feature-icon">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M7 13.5 13.5 7a3.2 3.2 0 0 1 4.5 4.5l-8 8a5 5 0 1 1-7-7l8-8" />
</svg>
</div>
<span class="feature-number">04</span>
<h3>文档与图像输入</h3>
<p>单次最多添加 8 个附件,支持同时传入 5 张图片;在一个会话中汇集任务所需材料。</p>
</article>
<article class="feature-card">
<div class="feature-icon">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M4 5h16v14H4z" />
<path d="m4 16 5-5 3 3 2-2 6 6" />
<circle cx="15.5" cy="8.5" r="1.5" />
</svg>
</div>
<span class="feature-number">05</span>
<h3>可控的图像生成</h3>
<p>生图质量支持 auto、low、medium、high 四档。结果以单张图像呈现,并作为本地工件保存。</p>
</article>
<article class="feature-card feature-card--wide feature-card--accent">
<div class="feature-icon">
<svg viewBox="0 0 24 24" aria-hidden="true">
<circle cx="12" cy="12" r="3" />
<path d="M12 3v3M12 18v3M3 12h3M18 12h3M5.6 5.6l2.1 2.1M16.3 16.3l2.1 2.1M18.4 5.6l-2.1 2.1M7.7 16.3l-2.1 2.1" />
</svg>
</div>
<span class="feature-number">06</span>
<h3>模型与工具,由你连接</h3>
<p>在桌面端管理模型配置、MCP 工具与运行时。密钥留在主进程的加密设置存储中,不交给网页渲染层。</p>
<div class="provider-pills" aria-label="支持的连接类型">
<span>模型提供商</span><span>MCP</span><span>OpenCode</span><span>Continue</span>
</div>
</article>
</div>
</div>
</section>
<section class="section release-section" id="release" aria-labelledby="release-title">
<div class="section-inner">
<div class="release-heading">
<div class="version-lockup" aria-hidden="true">
<span>VERSION</span>
<strong>0.8.0</strong>
</div>
<div>
<p class="kicker">下一站</p>
<h2 id="release-title">0.8.0 更新亮点</h2>
<p>更聪明地组织工作,也更诚实地标注能力边界。以下功能状态以正式 Release 说明为准。</p>
</div>
</div>
<ol class="release-list">
<li class="release-item">
<div class="release-index">01</div>
<div class="release-copy">
<div class="release-label">0.8.0</div>
<h3>Subagent 与智能路由</h3>
<p>面向复杂任务的协作与路由能力归入 0.8.0,不将仍在开发中的路径描述为当前稳定能力。</p>
</div>
<div class="release-visual route-visual" aria-hidden="true">
<span class="route-node route-node--main">主任务</span>
<span class="route-line route-line--one"></span>
<span class="route-line route-line--two"></span>
<span class="route-node route-node--sub-one">研究</span>
<span class="route-node route-node--sub-two">验证</span>
</div>
</li>
<li class="release-item">
<div class="release-index">02</div>
<div class="release-copy">
<div class="release-label release-label--preview">开发者预览</div>
<h3>IM 渠道接入</h3>
<p>钉钉与企业微信以开发者预览提供;个人微信处于实验性边界,不作为面向生产环境的稳定承诺。</p>
</div>
<div class="release-visual channel-visual" aria-label="渠道状态">
<span><b>钉钉</b><small>开发者预览</small></span>
<span><b>企业微信</b><small>开发者预览</small></span>
<span class="is-experimental"><b>个人微信</b><small>实验性边界</small></span>
</div>
</li>
<li class="release-item">
<div class="release-index">03</div>
<div class="release-copy">
<div class="release-label">多模态输入</div>
<h3>更多材料,一次带上</h3>
<p>单次最多 8 个附件,并已验证同时传入 5 张图片。限制保持可见,避免把超出边界的输入静默带入任务。</p>
</div>
<div class="release-visual attachment-visual" aria-hidden="true">
<div class="attachment-stack"><span></span><span></span><span></span></div>
<div><strong>8</strong><small>附件上限</small></div>
<div><strong>5</strong><small>图片上限</small></div>
</div>
</li>
<li class="release-item">
<div class="release-index">04</div>
<div class="release-copy">
<div class="release-label">图像生成</div>
<h3>清晰选择质量档位</h3>
<p>支持 auto、low、medium、high 四档质量。当前按单张结果呈现,不承诺批量多图生成。</p>
</div>
<div class="release-visual quality-visual" aria-label="图像质量档位">
<span>auto</span><span>low</span><span>medium</span><span class="is-selected">high</span>
</div>
</li>
</ol>
</div>
</section>
<section class="section download-section" id="download" aria-labelledby="download-title">
<div class="section-inner">
<div class="section-heading section-heading--center">
<div>
<p class="kicker">原生桌面体验</p>
<h2 id="download-title">准备好,在你的设备上运行</h2>
</div>
<p>
v0.8.0 Release 尚未发布。下载入口将在发布后统一开放,目前不提供虚构的资产名称或下载地址。
</p>
</div>
<div class="download-grid">
<article class="download-card">
<div class="platform-icon">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="m3 5 8-1v8H3V5Zm10-1.3L21 3v9h-8V3.7ZM3 14h8v8l-8-1v-7Zm10 0h8v9l-8-1v-8Z" />
</svg>
</div>
<div><h3>Windows</h3><p>x64 / arm64 · NSIS / 便携版</p></div>
<a class="button button--download is-disabled" aria-disabled="true" data-release-link>发布后开放</a>
</article>
<article class="download-card">
<div class="platform-icon">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M16.8 12.7c0-2.7 2.2-4 2.3-4.1A5 5 0 0 0 15.2 6c-1.7-.2-3.2 1-4.1 1-.9 0-2.2-1-3.6-1-1.8 0-3.5 1.1-4.5 2.7-2 3.5-.5 8.7 1.4 11.5.9 1.4 2 2.8 3.5 2.7 1.4 0 1.9-.9 3.7-.9 1.7 0 2.2.9 3.7.9s2.5-1.4 3.4-2.7a10 10 0 0 0 1.6-3.3 4.6 4.6 0 0 1-3.5-4.2ZM14.1 4.3A4.7 4.7 0 0 0 15.2 1a4.8 4.8 0 0 0-3.1 1.6A4.4 4.4 0 0 0 11 5.8c1.2.1 2.3-.5 3.1-1.5Z" />
</svg>
</div>
<div><h3>macOS</h3><p>x64 / arm64 · DMG / ZIP</p></div>
<a class="button button--download is-disabled" aria-disabled="true" data-release-link>发布后开放</a>
</article>
<article class="download-card">
<div class="platform-icon">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 3c-3 0-4.7 2.5-4.5 5.4-1.3 1.4-2 3.4-2 5.6 0 3.9 2.9 7 6.5 7s6.5-3.1 6.5-7c0-2.2-.7-4.2-2-5.6C16.7 5.5 15 3 12 3Z" />
<path d="M9.3 10.2h.1M14.6 10.2h.1M9.5 15c1.6 1.2 3.4 1.2 5 0M7 19l-2 2M17 19l2 2" />
</svg>
</div>
<div><h3>Linux</h3><p>x64 / arm64 · AppImage / DEB</p></div>
<a class="button button--download is-disabled" aria-disabled="true" data-release-link>发布后开放</a>
</article>
</div>
<div class="release-notice" role="status">
<svg viewBox="0 0 24 24" aria-hidden="true">
<circle cx="12" cy="12" r="9" /><path d="M12 11v5M12 8h.01" />
</svg>
<div>
<strong>Release 状态:尚未发布</strong>
<span>本站下载按钮由单一配置控制;正式发布前不会指向占位资产。</span>
</div>
</div>
</div>
</section>
<section class="section security-section" id="security" aria-labelledby="security-title">
<div class="section-inner security-grid">
<div class="security-intro">
<div class="security-shield" aria-hidden="true">
<svg viewBox="0 0 48 48">
<path d="M24 5 9 11v11c0 9.7 6 18.2 15 21 9-2.8 15-11.3 15-21V11L24 5Z" />
<path d="m17.5 24 4.5 4.5 9-10" />
</svg>
</div>
<p class="kicker">Security by boundary</p>
<h2 id="security-title">安全不是开关,<br />而是每一层的边界</h2>
<p>
GoodBuddy 将桌面渲染、密钥、工具运行与用户数据分层处理。
风险操作保持可见,未受信运行时不会绕过审批边界。
</p>
<a
class="text-link"
href="https://github.com/mesalogo/goodbuddy"
target="_blank"
rel="noreferrer"
>
在 GitHub 查看项目
<span aria-hidden="true"></span>
<span class="sr-only">(在新窗口打开)</span>
</a>
</div>
<div class="security-list">
<article>
<span class="security-number">01</span>
<div><h3>密钥不进入渲染层</h3><p>API 密钥留在主进程,并写入加密设置存储;网页界面不获得直接 Node 访问。</p></div>
</article>
<article>
<span class="security-number">02</span>
<div><h3>跨进程能力明确暴露</h3><p>通过窄化的预加载桥接调用能力,IPC 输入经过共享模式校验,并核验可信发送方。</p></div>
</article>
<article>
<span class="security-number">03</span>
<div><h3>运行时按不可信处理</h3><p>OpenCode 与 Continue 子运行时受环境白名单、沙箱检查及逐工具审批约束。</p></div>
</article>
<article>
<span class="security-number">04</span>
<div><h3>状态与审计语义可见</h3><p>取消、超时、输出边界和活动记录属于执行链路的一部分,不用模糊的“已完成”掩盖风险。</p></div>
</article>
</div>
</div>
</section>
<section class="section final-cta" aria-labelledby="cta-title">
<div class="section-inner">
<div class="cta-card">
<div class="cta-orbit" aria-hidden="true"><span></span><span></span></div>
<div>
<p class="kicker">GoodBuddy 0.8.0</p>
<h2 id="cta-title">一个更能做事,也更懂边界的桌面伙伴。</h2>
<p>关注 Release,第一时间获取正式版本、校验信息与完整更新说明。</p>
</div>
<div class="cta-actions">
<a
class="button button--primary is-disabled"
aria-disabled="true"
data-release-link
>发布后开放</a>
<a
class="button button--secondary"
href="https://github.com/mesalogo/goodbuddy"
target="_blank"
rel="noreferrer"
>
查看 GitHub
<span class="sr-only">(在新窗口打开)</span>
</a>
</div>
</div>
</div>
</section>
</main>
<footer class="site-footer">
<div class="section-inner footer-inner">
<a class="brand brand--footer" href="#home" aria-label="返回 GoodBuddy 首页">
<svg class="brand-mark" viewBox="0 0 40 40" aria-hidden="true">
<path d="M6 21a9 9 0 1 1 18 0v8H15a9 9 0 0 1-9-8Z" />
<path d="M16 21a9 9 0 1 1 18 0 9 9 0 0 1-18 0Z" />
<path d="M20 13V8M25 10l3-3M15 10l-3-3" />
</svg>
<span>GoodBuddy</span>
</a>
<p>安全可控的桌面智能助手与 Agent 工作空间。</p>
<div class="footer-links">
<a href="#features">功能</a>
<a href="#release">0.8.0</a>
<a href="#security">安全</a>
<a href="https://github.com/mesalogo/goodbuddy" target="_blank" rel="noreferrer">
GitHub<span class="sr-only">(在新窗口打开)</span>
</a>
</div>
<small>© <span data-current-year></span> GoodBuddy. 本站不使用第三方统计脚本。</small>
</div>
</footer>
<script src="./site.config.js"></script>
<script src="./app.js"></script>
</body>
</html>
+165
View File
@@ -0,0 +1,165 @@
import { readFile, stat } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
const siteRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const errors = [];
const requiredFiles = [
"index.html",
"styles.css",
"app.js",
"site.config.js",
"assets/favicon.svg",
"README.md",
];
const report = (condition, message) => {
if (!condition) {
errors.push(message);
}
};
const readSiteFile = async (relativePath) => {
try {
return await readFile(path.join(siteRoot, relativePath), "utf8");
} catch {
errors.push(`缺少文件:${relativePath}`);
return "";
}
};
await Promise.all(
requiredFiles.map(async (relativePath) => {
try {
const fileStats = await stat(path.join(siteRoot, relativePath));
report(fileStats.isFile(), `不是普通文件:${relativePath}`);
} catch {
errors.push(`缺少文件:${relativePath}`);
}
}),
);
const [html, css, appJs, configJs] = await Promise.all([
readSiteFile("index.html"),
readSiteFile("styles.css"),
readSiteFile("app.js"),
readSiteFile("site.config.js"),
]);
for (const [relativePath, content] of [
["index.html", html],
["styles.css", css],
["app.js", appJs],
["site.config.js", configJs],
]) {
report(!/[ \t]+$/m.test(content), `${relativePath} 包含行尾空白`);
report(!content.includes("\t"), `${relativePath} 包含 Tab 缩进`);
}
report(/<html\s+lang="zh-CN">/.test(html), "页面语言必须是 zh-CN");
report(/<meta\s+name="viewport"/.test(html), "缺少 viewport 元信息");
report((html.match(/<h1[\s>]/g) ?? []).length === 1, "页面必须且只能包含一个 h1");
report(/class="skip-link"\s+href="#main-content"/.test(html), "缺少跳到主要内容链接");
report(/<main\s+id="main-content">/.test(html), "缺少 main-content 主区域");
report(/aria-label="主导航"/.test(html), "主导航缺少可访问名称");
report(/data-theme-toggle/.test(html), "缺少主题切换控件");
report(/prefers-reduced-motion:\s*reduce/.test(css), "缺少减少动态效果规则");
report(/\[data-theme="dark"\]/.test(css), "缺少深色主题令牌");
for (const breakpoint of ["1199px", "959px", "719px"]) {
report(css.includes(`max-width: ${breakpoint}`), `缺少 ${breakpoint} 响应式断点`);
}
const requiredCopy = [
"Subagent 与智能路由",
"钉钉与企业微信以开发者预览提供",
"个人微信处于实验性边界",
"单次最多添加 8 个附件,支持同时传入 5 张图片",
"auto、low、medium、high",
"当前按单张结果呈现,不承诺批量多图生成",
"发布后开放",
"安全不是开关",
];
for (const copy of requiredCopy) {
report(html.includes(copy), `缺少准确文案:${copy}`);
}
report(
/version:\s*"0\.8\.0"/.test(configJs),
"site.config.js 必须集中配置 0.8.0 版本",
);
report(
/releasePublished:\s*false/.test(configJs),
"Release 未发布前 releasePublished 必须为 false",
);
report(
/releaseUrl:\s*"https:\/\/github\.com\/mesalogo\/goodbuddy\/releases\/tag\/v0\.8\.0"/.test(
configJs,
),
"未来 v0.8.0 Release URL 配置不正确",
);
report(
appJs.includes("config?.releasePublished === true"),
"下载链接必须受 releasePublished 配置保护",
);
const ids = [...html.matchAll(/\sid="([^"]+)"/g)].map((match) => match[1]);
const duplicateIds = ids.filter((id, index) => ids.indexOf(id) !== index);
report(duplicateIds.length === 0, `存在重复 id${[...new Set(duplicateIds)].join(", ")}`);
const attributes = [...html.matchAll(/\s(?:href|src)="([^"]+)"/g)].map((match) => match[1]);
const fragmentLinks = attributes.filter((value) => value.startsWith("#") && value.length > 1);
for (const fragment of fragmentLinks) {
report(ids.includes(fragment.slice(1)), `页内链接目标不存在:${fragment}`);
}
const localAssets = attributes.filter(
(value) =>
!value.startsWith("#") &&
!value.startsWith("https://") &&
!value.startsWith("http://") &&
!value.startsWith("mailto:") &&
!value.startsWith("data:"),
);
for (const asset of localAssets) {
const cleanAsset = asset.split(/[?#]/, 1)[0].replace(/^\.\//, "");
try {
const assetStats = await stat(path.join(siteRoot, cleanAsset));
report(assetStats.isFile(), `本地资源不是文件:${asset}`);
} catch {
errors.push(`本地资源不存在:${asset}`);
}
}
const externalBlankLinks = [
...html.matchAll(/<a\b(?=[^>]*target="_blank")[^>]*>/g),
].map((match) => match[0]);
for (const link of externalBlankLinks) {
report(/rel="[^"]*noreferrer[^"]*"/.test(link), `新窗口链接缺少 noreferrer${link}`);
}
report(
!/<a\b[^>]*href="[^"]+\.(?:exe|dmg|zip|AppImage|deb)(?:[?#][^"]*)?"/i.test(html),
"Release 未发布前不得提供具体安装资产链接",
);
report(
!/(?:react|vue|angular|bootstrap|tailwind)(?:\.min)?\.(?:js|css)/i.test(html),
"静态官网不得引入额外框架资源",
);
if (errors.length > 0) {
console.error(`官网静态检查失败(${errors.length} 项):`);
for (const error of errors) {
console.error(`- ${error}`);
}
process.exitCode = 1;
} else {
console.log(
`官网静态检查通过:${requiredFiles.length} 个必需文件,${ids.length} 个唯一 id${localAssets.length} 个本地资源引用。`,
);
}
+5
View File
@@ -0,0 +1,5 @@
window.GOODBUDDY_SITE_CONFIG = Object.freeze({
version: "0.8.0",
releasePublished: false,
releaseUrl: "https://github.com/mesalogo/goodbuddy/releases/tag/v0.8.0",
});
+2231
View File
File diff suppressed because it is too large Load Diff