From cadf0e48639b51c6a15b6532cbaa90c9adbd7b29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B8=85=E5=87=A4?= Date: Mon, 20 Jul 2026 08:19:15 -0700 Subject: [PATCH 1/5] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=85=A8=E7=AB=99?= =?UTF-8?q?=E9=93=BE=E6=8E=A5=E4=B8=8E=E6=96=87=E6=A1=88=E5=AE=A1=E8=AE=A1?= =?UTF-8?q?=E5=8F=91=E7=8E=B0=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TopBar 撤下 Pricing 入口:/pricing 路由不存在,点击后正文空白 - footer X 链接改回真实账号 x.com/memoh_ai(原 x.com/memohai 为 404) - footer 撤下 Privacy/Terms 占位链接(均指向首页,无实际页面) - zh.json 补齐英文残留:博客/加入等候名单/抢先体验等 6 处 - 新增 404 页面与 catch-all 路由,替代原来的空白正文 - create-spa-fallback.mjs 补 /download 静态入口(此前线上返回 404 状态码) - index.html 与 fallback 脚本的 meta 更新为当前产品定位 --- index.html | 15 ++++++------- scripts/create-spa-fallback.mjs | 19 ++++++++++++----- src/App.vue | 10 ++++----- src/components/TopBar.vue | 3 +-- src/locales/en.json | 10 ++++++++- src/locales/zh.json | 22 +++++++++++++------- src/pages/NotFoundPage.vue | 37 +++++++++++++++++++++++++++++++++ src/router.ts | 3 +++ 8 files changed, 91 insertions(+), 28 deletions(-) create mode 100644 src/pages/NotFoundPage.vue diff --git a/index.html b/index.html index 5daeb8f..b62ea7c 100644 --- a/index.html +++ b/index.html @@ -5,19 +5,20 @@ - MemohAI - The Agent Orchestrator - + + Memoh — The multi-agent platform + - - - + + + - - + + diff --git a/scripts/create-spa-fallback.mjs b/scripts/create-spa-fallback.mjs index 01187c0..9dbd06a 100644 --- a/scripts/create-spa-fallback.mjs +++ b/scripts/create-spa-fallback.mjs @@ -5,19 +5,27 @@ const distDir = resolve('dist') const indexPath = resolve(distDir, 'index.html') const metaPattern = /[\s\S]*?/ +// 静态 meta 必须与 src/locales 里的 seo.* 文案保持同一定位;home 同时是 index.html 的兜底内容 const pages = { home: { - title: 'MemohAI - The Agent Orchestrator', - description: 'Memoh is a self-hosted agent orchestrator for hackers and AI developers, with container isolation, long-term memory, high concurrency, and zero telemetry.', - socialDescription: 'Self-hosted agent orchestration with container isolation, long-term memory, high concurrency, and zero telemetry.', + title: 'Memoh — The multi-agent platform', + description: 'Every agent gets its own computer with a desktop, filesystem, and network. Always on, always there.', + socialDescription: 'Every agent gets its own computer with a desktop, filesystem, and network. Always on, always there.', url: 'https://memoh.ai/', }, + // /desktop 已重定向到 /download,但保留静态入口让旧链接拿到 200 而不是 404 desktop: { title: 'Memoh Desktop - Download for macOS, Windows & Linux', description: 'Download the Memoh desktop app for macOS, Windows, and Linux. Run your agent workspace natively on your computer.', socialDescription: 'Download Memoh Desktop for macOS, Windows, and Linux.', url: 'https://memoh.ai/desktop', }, + download: { + title: 'Download Memoh Desktop - macOS, Windows & Linux', + description: 'Pick the Memoh desktop installer for your system. Available for macOS (Apple Silicon & Intel), Windows, and Linux.', + socialDescription: 'Download Memoh Desktop for macOS, Windows, and Linux.', + url: 'https://memoh.ai/download', + }, waitlist: { title: 'Join the Memoh waitlist', description: 'Join the Memoh waitlist for early access to cloud agent workspaces.', @@ -49,7 +57,7 @@ const renderMeta = ({ title, description, socialDescription, url }) => { - + @@ -73,7 +81,8 @@ const indexHtml = await readFile(indexPath, 'utf8') await writeFile(indexPath, withMeta(indexHtml, pages.home)) await writeFile(resolve(distDir, '404.html'), withMeta(indexHtml, pages.home)) -for (const route of ['desktop', 'waitlist', 'blogs']) { +// 每个 SPA 路由都必须在这里有静态入口,否则 GitHub Pages 用 404.html 兜底、返回 404 状态码(SEO 直接判死) +for (const route of ['desktop', 'download', 'waitlist', 'blogs']) { const routeDir = resolve(distDir, route) await mkdir(routeDir, { recursive: true }) await writeFile(resolve(routeDir, 'index.html'), withMeta(indexHtml, pages[route])) diff --git a/src/App.vue b/src/App.vue index c40839f..d409f99 100644 --- a/src/App.vue +++ b/src/App.vue @@ -80,7 +80,8 @@ const telecomLicenseUrl = 'https://dxzhgl.miit.gov.cn/' class="flex items-center justify-center w-9 h-9 rounded-md text-muted-foreground hover:text-foreground hover:bg-accent transition-colors"> - + @@ -100,11 +101,8 @@ const telecomLicenseUrl = 'https://dxzhgl.miit.gov.cn/' {{ t('nav.blogs') }} {{ t('nav.github') }} -
- {{ t('footer.legal') }} - {{ t('footer.privacy') }} - {{ t('footer.terms') }} -
+ diff --git a/src/components/TopBar.vue b/src/components/TopBar.vue index 45add05..5cbbc5e 100644 --- a/src/components/TopBar.vue +++ b/src/components/TopBar.vue @@ -29,7 +29,6 @@ const { isDark, toggle: toggleDark } = useThemePreference() const { locale } = useI18n() const docsUrl = computed(() => locale.value === 'zh' ? 'https://docs.memoh.ai/zh' : 'https://docs.memoh.ai') -const pricingUrl = 'https://memoh.ai/pricing' const isLangOpen = ref(false) const langMenuRef = ref(null) @@ -69,7 +68,7 @@ const selectLang = (lang: string) => { diff --git a/src/locales/en.json b/src/locales/en.json index b5d756e..c2d654c 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -2,7 +2,6 @@ "nav": { "docs": "Docs", "github": "GitHub", - "pricing": "Pricing", "blogs": "Blogs", "supermarket": "Supermarket", "desktop": "Desktop", @@ -219,5 +218,14 @@ "egg": { "hint": "Pet me", "petted": "Petted {n} times" + }, + "notFound": { + "seo": { + "title": "Page not found — Memoh", + "description": "The page you are looking for does not exist." + }, + "title": "Page not found", + "desc": "The link may be broken or the page has moved.", + "back": "Back to home" } } diff --git a/src/locales/zh.json b/src/locales/zh.json index 1b5e07b..6286340 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -2,8 +2,7 @@ "nav": { "docs": "文档", "github": "GitHub", - "pricing": "Pricing", - "blogs": "Blogs", + "blogs": "博客", "supermarket": "应用超市", "desktop": "桌面版", "download": "下载" @@ -58,7 +57,7 @@ "hero": { "title": "陪着你的%TA%", "subtitle": "Memoh 是多 Agent 平台,每个%TA%都住在自己的电脑里\n在这里一直等着你", - "ctaPrimary": "Join waitlist", + "ctaPrimary": "加入等候名单", "ctaSecondary": "GitHub", "cta": "开始", "desktop": "桌面版", @@ -167,7 +166,7 @@ }, "cta_bottom": { "title": "试试 Memoh", - "btn": "Join waitlist", + "btn": "加入等候名单", "docs": "查看文档" }, "waitlist": { @@ -175,7 +174,7 @@ "title": "加入 Memoh Waitlist", "description": "加入 Memoh waitlist,获取云端 Agent 工作区的早期访问资格。" }, - "kicker": "Early access", + "kicker": "抢先体验", "title": "加入 Memoh waitlist。", "subtitle": "留下邮箱,等 hosted Memoh 开放更多名额时我们会联系你。", "notes": { @@ -197,7 +196,7 @@ "subtitle": "一个邮箱就够了。", "email": "邮箱", "placeholder": "邮箱地址", - "submit": "Join waitlist", + "submit": "加入等候名单", "submitting": "提交中...", "joined": "已加入", "success": "你已经加入 waitlist,我们会后续联系你。", @@ -211,7 +210,7 @@ "privacy": "隐私政策", "terms": "服务条款", "tagline": "多 Agent 平台\n每个%TA%都有自己的电脑,一直在", - "getStarted": "Join waitlist", + "getStarted": "加入等候名单", "product": "产品", "resources": "资源", "legal": "法律" @@ -219,5 +218,14 @@ "egg": { "hint": "摸摸我", "petted": "已经摸了 {n} 下" + }, + "notFound": { + "seo": { + "title": "页面不存在 — Memoh", + "description": "你要找的页面不存在。" + }, + "title": "页面不存在", + "desc": "链接可能已失效,或者页面被移动了。", + "back": "回到首页" } } diff --git a/src/pages/NotFoundPage.vue b/src/pages/NotFoundPage.vue new file mode 100644 index 0000000..9e39e35 --- /dev/null +++ b/src/pages/NotFoundPage.vue @@ -0,0 +1,37 @@ + + + diff --git a/src/router.ts b/src/router.ts index 1ecf054..ad41c89 100644 --- a/src/router.ts +++ b/src/router.ts @@ -4,6 +4,7 @@ const HomePage = () => import('./pages/HomePage.vue') const DownloadPage = () => import('./pages/DownloadPage.vue') const BlogsPage = () => import('./pages/BlogsPage.vue') const WaitlistPage = () => import('./pages/WaitlistPage.vue') +const NotFoundPage = () => import('./pages/NotFoundPage.vue') export const router = createRouter({ history: createWebHistory(), @@ -15,6 +16,8 @@ export const router = createRouter({ { path: '/blogs/:slug', name: 'blog-post', component: BlogsPage }, // /desktop has been merged into /download; keep a redirect for old links. { path: '/desktop', redirect: '/download' }, + // Catch-all: without it, unknown URLs render an empty body between TopBar and footer. + { path: '/:pathMatch(.*)*', name: 'not-found', component: NotFoundPage }, ], scrollBehavior(_to, _from, savedPosition) { if (savedPosition) return savedPosition From 16bb0691b2f7282423adae1aa72c32796777edb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B8=85=E5=87=A4?= Date: Mon, 20 Jul 2026 08:19:15 -0700 Subject: [PATCH 2/5] =?UTF-8?q?fix:=20=E4=B8=8B=E8=BD=BD=E4=BB=A3=E7=90=86?= =?UTF-8?q?=E5=9C=A8=20GitHub=20token=20=E5=A4=B1=E6=95=88=E6=97=B6?= =?UTF-8?q?=E9=99=8D=E7=BA=A7=E4=B8=BA=E5=8C=BF=E5=90=8D=E8=AF=B7=E6=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 线上 GITHUB_TOKEN 过期导致 /downloads/desktop/* 全部 502(manifest 与 安装包均不可下载)。公开仓库的 release 元数据匿名 API 即可读,token 被 拒(401)或触限(403)时去掉 Authorization 重试一次,下载功能不再单点 依赖 token 有效性;匿名限流由 release 缓存(5 分钟)稀释。 --- workers/desktop-download-proxy.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/workers/desktop-download-proxy.js b/workers/desktop-download-proxy.js index 660a9cd..d254a53 100644 --- a/workers/desktop-download-proxy.js +++ b/workers/desktop-download-proxy.js @@ -72,11 +72,22 @@ const githubHeaders = (env, accept = 'application/vnd.github+json') => { } const githubFetch = async (url, env, accept) => { - const response = await fetch(url, { + let response = await fetch(url, { headers: githubHeaders(env, accept), redirect: 'follow', }) + // Token 过期/失效不应让下载功能整体瘫痪(2026-07 曾因此全站 /downloads/* 502): + // 公开仓库的 release 元数据匿名即可读,token 被拒(401)或触限(403)时去掉 + // Authorization 重试一次。匿名限流 60 次/时/IP,由 release 缓存(5 分钟)稀释, + // 实际请求量远低于阈值。token 换新后此路径自动不再触发。 + if (!response.ok && env.GITHUB_TOKEN && (response.status === 401 || response.status === 403)) { + response = await fetch(url, { + headers: githubHeaders({ ...env, GITHUB_TOKEN: undefined }, accept), + redirect: 'follow', + }) + } + if (!response.ok) { const detail = await response.text().catch(() => '') const message = detail ? ` ${detail.slice(0, 240)}` : '' From 083626b5ffa17d7c3b7d2583ed69c14d20b734ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B8=85=E5=87=A4?= Date: Mon, 20 Jul 2026 08:30:34 -0700 Subject: [PATCH 3/5] =?UTF-8?q?fix:=20=E9=9D=99=E6=80=81=20meta=20?= =?UTF-8?q?=E4=B8=8E=20seo.description=20=E5=AF=B9=E9=BD=90,footer=20?= =?UTF-8?q?=E9=93=BE=E6=8E=A5=E5=88=97=E6=94=B9=E4=B8=BA=E4=B8=A4=E5=88=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - home 描述补回 cloud computer,与 src/locales/en.json seo.description 逐字一致(Codex review P2):爬虫/链接预览拿到的静态 HTML 与 SPA 水合后 的 meta 不再发布不一致的产品描述 - Legal 列撤下后 footer 链接区只剩两列,grid 从 sm:grid-cols-3 改为固定 两列,消除 sm 以上断点的空列 --- index.html | 6 +++--- scripts/create-spa-fallback.mjs | 4 ++-- src/App.vue | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/index.html b/index.html index b62ea7c..5440f6c 100644 --- a/index.html +++ b/index.html @@ -7,18 +7,18 @@ Memoh — The multi-agent platform - + - + - + diff --git a/scripts/create-spa-fallback.mjs b/scripts/create-spa-fallback.mjs index 9dbd06a..cb5cd86 100644 --- a/scripts/create-spa-fallback.mjs +++ b/scripts/create-spa-fallback.mjs @@ -9,8 +9,8 @@ const metaPattern = /[\s\S]*?/ const pages = { home: { title: 'Memoh — The multi-agent platform', - description: 'Every agent gets its own computer with a desktop, filesystem, and network. Always on, always there.', - socialDescription: 'Every agent gets its own computer with a desktop, filesystem, and network. Always on, always there.', + description: 'Every agent gets its own cloud computer with a desktop, filesystem, and network. Always on, always there.', + socialDescription: 'Every agent gets its own cloud computer with a desktop, filesystem, and network. Always on, always there.', url: 'https://memoh.ai/', }, // /desktop 已重定向到 /download,但保留静态入口让旧链接拿到 200 而不是 404 diff --git a/src/App.vue b/src/App.vue index d409f99..509b786 100644 --- a/src/App.vue +++ b/src/App.vue @@ -88,8 +88,8 @@ const telecomLicenseUrl = 'https://dxzhgl.miit.gov.cn/' - -
+ +
{{ t('footer.product') }} {{ t('nav.download') }} From ef794404b8fa8b7505903e11a428777f58aa45e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=A8=E8=8B=92?= <16112591+chen-ran@users.noreply.github.com> Date: Wed, 29 Jul 2026 19:08:27 +0800 Subject: [PATCH 4/5] fix: resolve desktop assets from release metadata --- .github/workflows/deploy-download-worker.yml | 23 +++- README.md | 2 + package.json | 3 +- workers/desktop-download-proxy.js | 54 ++++---- workers/desktop-download-proxy.test.js | 133 +++++++++++++++++++ 5 files changed, 186 insertions(+), 29 deletions(-) create mode 100644 workers/desktop-download-proxy.test.js diff --git a/.github/workflows/deploy-download-worker.yml b/.github/workflows/deploy-download-worker.yml index e3a72ae..124181c 100644 --- a/.github/workflows/deploy-download-worker.yml +++ b/.github/workflows/deploy-download-worker.yml @@ -7,17 +7,38 @@ on: paths: - 'workers/**' - '.github/workflows/deploy-download-worker.yml' + pull_request: + paths: + - 'workers/**' + - '.github/workflows/deploy-download-worker.yml' workflow_dispatch: permissions: contents: read concurrency: - group: download-worker + group: download-worker-${{ github.ref }} cancel-in-progress: false jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Test download worker + run: node --test workers/*.test.js + deploy: + if: github.event_name != 'pull_request' + needs: test runs-on: ubuntu-latest steps: diff --git a/README.md b/README.md index 4655e65..34d7a9b 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,8 @@ Desktop installers are served through a Cloudflare Worker at: The Worker resolves the latest `memohai/Memoh` release, redirects to a same-domain versioned URL, then caches the release asset at Cloudflare edge on first download. Users never download from a GitHub release URL directly. +Release assets are discovered by the stable platform suffix in each route and proxied through the `browser_download_url` returned by GitHub. Do not reconstruct asset URLs or couple the Worker to a product-name prefix such as `Memoh` or `Memoh-Local`. + Deploy the Worker after configuring the `memoh.ai/downloads/desktop/*` route: ```bash diff --git a/package.json b/package.json index 858fd3f..0b3fe76 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "scripts": { "dev": "vite", "build": "vue-tsc -b && vite build && node scripts/create-spa-fallback.mjs", - "preview": "vite preview" + "preview": "vite preview", + "test:worker": "node --test workers/*.test.js" }, "dependencies": { "@iconify/vue": "^5.0.0", diff --git a/workers/desktop-download-proxy.js b/workers/desktop-download-proxy.js index d254a53..aa220b3 100644 --- a/workers/desktop-download-proxy.js +++ b/workers/desktop-download-proxy.js @@ -3,46 +3,32 @@ const DOWNLOAD_PREFIX = '/downloads/desktop' const LATEST_TTL_SECONDS = 300 const ASSET_TTL_SECONDS = 60 * 60 * 24 * 30 -const assetPattern = (slug) => new RegExp(`^Memoh-Local-[0-9][0-9A-Za-z.-]*-${slug.replaceAll('.', '\\.')}$`, 'i') - const ASSETS = { 'mac-arm64.dmg': { key: 'macArm', - pattern: assetPattern('mac-arm64.dmg'), }, 'mac-x64.dmg': { key: 'macIntel', - pattern: assetPattern('mac-x64.dmg'), }, 'win-x64-setup.exe': { key: 'win', - pattern: assetPattern('win-x64-setup.exe'), }, 'linux-amd64.deb': { key: 'linuxDebAmd64', - pattern: assetPattern('linux-amd64.deb'), }, 'linux-x86_64.AppImage': { key: 'linuxAppImageX86', - pattern: assetPattern('linux-x86_64.AppImage'), }, } -const sanitizeFileNamePart = (value) => { - return String(value).replace(/[\\/:*?"<>|]+/g, '-').replace(/\s+/g, '-') -} - -const downloadFileName = (tag, slug) => { - return `Memoh-Local-${sanitizeFileNamePart(tag)}-${sanitizeFileNamePart(slug)}` +const assetMatchesSlug = (asset, slug) => { + return typeof asset?.name === 'string' + && typeof asset?.browser_download_url === 'string' + && asset.name.toLowerCase().endsWith(`-${slug.toLowerCase()}`) } -const githubAssetFileName = (tag, slug) => { - const version = tag.replace(/^v/i, '') - return `Memoh-Local-${version}-${slug}` -} - -const githubAssetUrl = (repo, tag, slug) => { - return `https://github.com/${repo}/releases/download/${encodeURIComponent(tag)}/${encodeURIComponent(githubAssetFileName(tag, slug))}` +const contentDispositionFileName = (name) => { + return String(name).replace(/[\r\n"]/g, '_') } const jsonResponse = (body, init = {}) => { @@ -134,16 +120,26 @@ const getRelease = async (tag, env, ctx) => { } const repo = env.MEMOH_RELEASE_REPO || RELEASE_REPO + const cache = caches.default + const cacheKey = new Request(`https://memoh.internal/downloads/releases/${encodeURIComponent(repo)}/${encodeURIComponent(tag)}`) + const cached = await cache.match(cacheKey) + if (cached) return cached.json() + const response = await githubFetch(`https://api.github.com/repos/${repo}/releases/tags/${encodeURIComponent(tag)}`, env) + const release = await response.json() + + ctx.waitUntil(cache.put(cacheKey, jsonResponse(release, { + headers: cacheHeaders(ASSET_TTL_SECONDS), + }))) - return response.json() + return release } const findAsset = (release, slug) => { const definition = ASSETS[slug] if (!definition) return undefined - const asset = release.assets?.find((candidate) => definition.pattern.test(candidate.name)) + const asset = release.assets?.find((candidate) => assetMatchesSlug(candidate, slug)) if (!asset) return undefined return { @@ -164,7 +160,7 @@ const buildManifest = (requestUrl, release, repo) => { assets[definition.key] = { path: `${requestUrl.origin}${DOWNLOAD_PREFIX}/${encodeURIComponent(release.tag_name)}/${slug}`, - name: downloadFileName(release.tag_name, slug), + name: asset.name, originalName: asset.name, size: asset.size, contentType: asset.content_type, @@ -209,8 +205,12 @@ const proxyAsset = async (request, env, ctx, tag, slug) => { if (!ASSETS[slug]) return notFound() - const repo = env.MEMOH_RELEASE_REPO || RELEASE_REPO - const assetResponse = await fetch(githubAssetUrl(repo, tag, slug), { + const release = await getRelease(tag, env, ctx) + const match = findAsset(release, slug) + if (!match) return new Response('Release asset not found', { status: 404 }) + + const { asset } = match + const assetResponse = await fetch(asset.browser_download_url, { method: request.method === 'HEAD' ? 'HEAD' : 'GET', headers: { 'user-agent': 'memoh-landing-download-proxy', @@ -229,8 +229,8 @@ const proxyAsset = async (request, env, ctx, tag, slug) => { headers: assetResponse.headers, }) response.headers.set('cache-control', `public, max-age=${ASSET_TTL_SECONDS}, immutable`) - response.headers.set('content-disposition', `attachment; filename="${downloadFileName(tag, slug)}"`) - response.headers.set('x-memoh-release-tag', tag) + response.headers.set('content-disposition', `attachment; filename="${contentDispositionFileName(asset.name)}"`) + response.headers.set('x-memoh-release-tag', release.tag_name || tag) response.headers.set('x-memoh-cache', 'MISS') response.headers.delete('set-cookie') diff --git a/workers/desktop-download-proxy.test.js b/workers/desktop-download-proxy.test.js new file mode 100644 index 0000000..3ebafb8 --- /dev/null +++ b/workers/desktop-download-proxy.test.js @@ -0,0 +1,133 @@ +import assert from 'node:assert/strict' +import test from 'node:test' + +import worker from './desktop-download-proxy.js' + +const release = { + name: 'v0.16.0', + tag_name: 'v0.16.0', + html_url: 'https://github.com/memohai/Memoh/releases/tag/v0.16.0', + published_at: '2026-07-11T16:56:45Z', + assets: [ + { + name: 'Memoh-0.16.0-mac-arm64.dmg', + browser_download_url: 'https://downloads.example.test/assets/mac-arm64', + size: 123, + content_type: 'application/x-apple-diskimage', + updated_at: '2026-07-11T16:56:45Z', + }, + { + name: 'Any-Future-Product-0.16.0-linux-amd64.deb', + browser_download_url: 'https://downloads.example.test/assets/linux-amd64', + size: 456, + content_type: 'application/vnd.debian.binary-package', + updated_at: '2026-07-11T16:56:45Z', + }, + ], +} + +const createRuntime = () => { + const cacheEntries = new Map() + globalThis.caches = { + default: { + async match(request) { + const response = cacheEntries.get(request.url) + return response?.clone() + }, + async put(request, response) { + cacheEntries.set(request.url, response.clone()) + }, + }, + } + + const pending = [] + return { + ctx: { + waitUntil(promise) { + pending.push(promise) + }, + }, + flush: () => Promise.all(pending), + } +} + +const requestUrl = (input) => input instanceof Request ? input.url : String(input) + +test('falls back to the public API and discovers assets without a product-name prefix', async (t) => { + const originalFetch = globalThis.fetch + t.after(() => { + globalThis.fetch = originalFetch + }) + + const calls = [] + globalThis.fetch = async (input, init = {}) => { + const url = requestUrl(input) + calls.push({ url, authorization: init.headers?.authorization }) + + if (calls.length === 1) { + return new Response('Bad credentials', { status: 401 }) + } + + return Response.json([release]) + } + + const runtime = createRuntime() + const response = await worker.fetch( + new Request('https://memoh.ai/downloads/desktop/latest/manifest.json'), + { MEMOH_RELEASE_REPO: 'memohai/Memoh', GITHUB_TOKEN: 'expired-token' }, + runtime.ctx, + ) + await runtime.flush() + + assert.equal(response.status, 200) + assert.deepEqual(calls.map((call) => call.authorization), [ + 'Bearer expired-token', + undefined, + ]) + + const manifest = await response.json() + assert.equal(manifest.tag, 'v0.16.0') + assert.equal(manifest.assets.macArm.name, 'Memoh-0.16.0-mac-arm64.dmg') + assert.equal(manifest.assets.linuxDebAmd64.name, 'Any-Future-Product-0.16.0-linux-amd64.deb') +}) + +test('proxies the asset URL returned by GitHub instead of constructing a filename', async (t) => { + const originalFetch = globalThis.fetch + t.after(() => { + globalThis.fetch = originalFetch + }) + + const calls = [] + globalThis.fetch = async (input) => { + const url = requestUrl(input) + calls.push(url) + + if (url.includes('/releases/tags/v0.16.0')) { + return Response.json(release) + } + + if (url === release.assets[0].browser_download_url) { + return new Response('installer-bytes', { + headers: { 'content-type': 'application/x-apple-diskimage' }, + }) + } + + return new Response('Unexpected URL', { status: 500 }) + } + + const runtime = createRuntime() + const response = await worker.fetch( + new Request('https://memoh.ai/downloads/desktop/v0.16.0/mac-arm64.dmg'), + { MEMOH_RELEASE_REPO: 'memohai/Memoh' }, + runtime.ctx, + ) + await runtime.flush() + + assert.equal(response.status, 200) + assert.equal(await response.text(), 'installer-bytes') + assert.equal(response.headers.get('content-disposition'), 'attachment; filename="Memoh-0.16.0-mac-arm64.dmg"') + assert.deepEqual(calls, [ + 'https://api.github.com/repos/memohai/Memoh/releases/tags/v0.16.0', + release.assets[0].browser_download_url, + ]) +}) From 6de36b1033ce268838c644ac2c09a1328f585e65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B8=85=E5=87=A4?= Date: Wed, 5 Aug 2026 07:31:46 -0700 Subject: [PATCH 5/5] =?UTF-8?q?feat:=20S1=20=E6=96=B0=E5=A2=9E=E3=80=8C?= =?UTF-8?q?=E4=BD=A0=E7=86=9F=E6=82=89=E7=9A=84=20Agent=E3=80=8D=E5=8D=A1,?= =?UTF-8?q?=E5=B1=95=E7=A4=BA=20Claude=20Code=20/=20Codex=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - grid 三卡变四卡,Agent 卡第一位;桌面端四列、中屏 2x2、移动端单列 - 卡内为倾斜三卡扇形构图(Claude / Memoh / Codex),cqw 随卡位整体缩放,中卡严格居中 - S1 内容容器 1080 → 1200 保证四卡宽度;描述块固定三行最小高度,四卡 proof 面板顶对齐 - 图标字节级复制自主仓 packages/icons @f256bff;codex 变体去白底以适配浅灰展示底 --- public/brands/claude-code-color.svg | 2 + public/brands/codex-blob.svg | 2 + src/components/proof/ProofAgents.vue | 121 +++++++++++++++++++++++++++ src/locales/en.json | 8 +- src/locales/zh.json | 8 +- src/pages/HomePage.vue | 13 ++- 6 files changed, 147 insertions(+), 7 deletions(-) create mode 100644 public/brands/claude-code-color.svg create mode 100644 public/brands/codex-blob.svg create mode 100644 src/components/proof/ProofAgents.vue diff --git a/public/brands/claude-code-color.svg b/public/brands/claude-code-color.svg new file mode 100644 index 0000000..e07cdcc --- /dev/null +++ b/public/brands/claude-code-color.svg @@ -0,0 +1,2 @@ + +Claude Code diff --git a/public/brands/codex-blob.svg b/public/brands/codex-blob.svg new file mode 100644 index 0000000..01e22dc --- /dev/null +++ b/public/brands/codex-blob.svg @@ -0,0 +1,2 @@ + +Codex diff --git a/src/components/proof/ProofAgents.vue b/src/components/proof/ProofAgents.vue new file mode 100644 index 0000000..63a9b21 --- /dev/null +++ b/src/components/proof/ProofAgents.vue @@ -0,0 +1,121 @@ + + + + + diff --git a/src/locales/en.json b/src/locales/en.json index c2d654c..87d5718 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -68,14 +68,18 @@ "title": "A computer of its own", "subtitle": "Every agent gets a desktop, a filesystem, and a network — not a chat window, a real computer that stays on.", "c1": { + "title": "Agents you already know", + "desc": "Claude Code or Codex\ncan move in — your pick." + }, + "c2": { "title": "Its own desktop", "desc": "A full graphical desktop where it browses and works." }, - "c2": { + "c3": { "title": "Its own files", "desc": "A persistent filesystem of its own — notes, downloads, workspace." }, - "c3": { + "c4": { "title": "Its own network", "desc": "Isolated networking. It goes online on its own terms." } diff --git a/src/locales/zh.json b/src/locales/zh.json index 6286340..0471f9b 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -68,14 +68,18 @@ "title": "一台属于%TA%的电脑", "subtitle": "每个 Agent 都有自己的桌面、文件和网络\n不是聊天框,是一台真正开着的电脑", "c1": { + "title": "你熟悉的 Agent", + "desc": "Claude Code、Codex 都能住进来\n用哪个,由你选" + }, + "c2": { "title": "%TA%自己的桌面", "desc": "完整的图形界面\n%TA%在里面浏览、工作——不只是一个对话框" }, - "c2": { + "c3": { "title": "%TA%自己的文件", "desc": "一套属于 Agent 的持久文件系统\n%TA%的笔记、%TA%的下载、%TA%的工作区" }, - "c3": { + "c4": { "title": "%TA%自己的网络", "desc": "独立的网络环境\n%TA%按自己的节奏上网,获取需要的东西" } diff --git a/src/pages/HomePage.vue b/src/pages/HomePage.vue index 06e543f..096c542 100644 --- a/src/pages/HomePage.vue +++ b/src/pages/HomePage.vue @@ -8,6 +8,7 @@ import HeroSection from '../components/HeroSection.vue' import ProofDesktop from '../components/proof/ProofDesktop.vue' import ProofFiles from '../components/proof/ProofFiles.vue' import ProofNetwork from '../components/proof/ProofNetwork.vue' +import ProofAgents from '../components/proof/ProofAgents.vue' import ProofSchedule from '../components/proof/ProofSchedule.vue' import ProofProactive from '../components/proof/ProofProactive.vue' import ProofMemory from '../components/proof/ProofMemory.vue' @@ -61,7 +62,11 @@ useHead({ ], }) +// S1 四张卡:第一张讲谁住在里面(agent runtime),后三张讲电脑的部件 +// (桌面/文件/网络)。runtime 轴不是渠道轴,故属于 S1 而非 S3; +// 作为 grid 里的卡存在,不单独占行。 const s1Proofs = [ + { key: 'agents', comp: ProofAgents }, { key: 'desktop', comp: ProofDesktop }, { key: 'files', comp: ProofFiles }, { key: 'network', comp: ProofNetwork }, @@ -109,17 +114,19 @@ const moreLogos = [
-
+

-
+

-

+ +