Skip to content
Closed
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
23 changes: 22 additions & 1 deletion .github/workflows/deploy-download-worker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 8 additions & 7 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,20 @@
<link rel="icon" type="image/png" href="/logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- app-meta-start -->
<title>MemohAI - The Agent Orchestrator</title>
<meta name="description" content="Memoh is a self-hosted agent orchestrator for hackers and AI developers, with container isolation, long-term memory, high concurrency, and zero telemetry." />
<!-- 静态 meta 与 scripts/create-spa-fallback.mjs 的 pages.home 保持一致;未知路由(404.html)也用这套兜底 -->
<title>Memoh — The multi-agent platform</title>
<meta name="description" content="Every agent gets its own cloud computer with a desktop, filesystem, and network. Always on, always there." />
<meta name="robots" content="index, follow" />
<link rel="canonical" href="https://memoh.ai/" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="MemohAI" />
<meta property="og:title" content="MemohAI - The Agent Orchestrator" />
<meta property="og:description" content="Self-hosted agent orchestration with container isolation, long-term memory, high concurrency, and zero telemetry." />
<meta property="og:site_name" content="Memoh" />
<meta property="og:title" content="Memoh — The multi-agent platform" />
<meta property="og:description" content="Every agent gets its own cloud computer with a desktop, filesystem, and network. Always on, always there." />
<meta property="og:url" content="https://memoh.ai/" />
<meta property="og:image" content="https://memoh.ai/logo.png" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:title" content="MemohAI - The Agent Orchestrator" />
<meta name="twitter:description" content="Self-hosted agent orchestration with container isolation, long-term memory, high concurrency, and zero telemetry." />
<meta name="twitter:title" content="Memoh — The multi-agent platform" />
<meta name="twitter:description" content="Every agent gets its own cloud computer with a desktop, filesystem, and network. Always on, always there." />
<meta name="twitter:image" content="https://memoh.ai/logo.png" />
<!-- app-meta-end -->
<link rel="preconnect" href="https://fonts.googleapis.com">
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions public/brands/claude-code-color.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions public/brands/codex-blob.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 14 additions & 5 deletions scripts/create-spa-fallback.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,27 @@ const distDir = resolve('dist')
const indexPath = resolve(distDir, 'index.html')
const metaPattern = /<!-- app-meta-start -->[\s\S]*?<!-- app-meta-end -->/

// 静态 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 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
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.',
Expand Down Expand Up @@ -49,7 +57,7 @@ const renderMeta = ({ title, description, socialDescription, url }) => {
<meta name="robots" content="index, follow" />
<link rel="canonical" href="${escapeHtml(url)}" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="MemohAI" />
<meta property="og:site_name" content="Memoh" />
<meta property="og:title" content="${escapeHtml(title)}" />
<meta property="og:description" content="${escapeHtml(socialDescription)}" />
<meta property="og:url" content="${escapeHtml(url)}" />
Expand All @@ -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]))
Expand Down
14 changes: 6 additions & 8 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -80,15 +80,16 @@ 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">
<span aria-hidden="true" class="telegram-icon h-4 w-4"></span>
</a>
<a href="https://x.com/memohai" target="_blank" rel="noopener noreferrer" aria-label="X"
<!-- 官方 X 账号是 @memoh_ai;x.com/memohai 是别人占用或无此账号(404),别改回去 -->
<a href="https://x.com/memoh_ai" target="_blank" rel="noopener noreferrer" aria-label="X"
class="flex items-center justify-center w-9 h-9 rounded-md text-muted-foreground hover:text-foreground hover:bg-accent transition-colors">
<Twitter class="w-4 h-4" />
</a>
</div>
</div>

<!-- Link columns (right) -->
<div class="grid grid-cols-2 sm:grid-cols-3 gap-8 sm:gap-12">
<!-- Link columns (right) — Legal 列撤下后只剩两列,勿改回 sm:grid-cols-3 -->
<div class="grid grid-cols-2 gap-8 sm:gap-12">
<div class="flex flex-col gap-3">
<span class="text-[11px] font-medium uppercase tracking-wider text-muted-foreground/55">{{ t('footer.product') }}</span>
<router-link to="/download" class="text-sm text-muted-foreground hover:text-foreground transition-colors">{{ t('nav.download') }}</router-link>
Expand All @@ -100,11 +101,8 @@ const telecomLicenseUrl = 'https://dxzhgl.miit.gov.cn/'
<router-link to="/blogs" class="text-sm text-muted-foreground hover:text-foreground transition-colors">{{ t('nav.blogs') }}</router-link>
<a href="https://github.com/memohai/Memoh" target="_blank" rel="noopener noreferrer" class="text-sm text-muted-foreground hover:text-foreground transition-colors">{{ t('nav.github') }}</a>
</div>
<div class="flex flex-col gap-3 col-span-2 sm:col-span-1">
<span class="text-[11px] font-medium uppercase tracking-wider text-muted-foreground/55">{{ t('footer.legal') }}</span>
<a href="https://memoh.ai" target="_blank" rel="noopener noreferrer" class="text-sm text-muted-foreground hover:text-foreground transition-colors">{{ t('footer.privacy') }}</a>
<a href="https://memoh.ai" target="_blank" rel="noopener noreferrer" class="text-sm text-muted-foreground hover:text-foreground transition-colors">{{ t('footer.terms') }}</a>
</div>
<!-- Legal 列整列撤下:Privacy/Terms 页面不存在,此前两个链接都指向首页占位。
页面就绪后恢复此列及 footer.privacy / footer.terms 文案。 -->
</div>
</div>

Expand Down
3 changes: 1 addition & 2 deletions src/components/TopBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLElement | null>(null)
Expand Down Expand Up @@ -69,7 +68,7 @@ const selectLang = (lang: string) => {
</RouterLink>
<nav class="hidden md:flex items-center gap-1 sm:gap-2">
<a :href="docsUrl" target="_blank" rel="noopener noreferrer" class="font-medium text-sm rounded-md px-3 py-2 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 whitespace-nowrap" :class="linkClass">{{ $t('nav.docs') }}</a>
<a :href="pricingUrl" class="font-medium text-sm rounded-md px-3 py-2 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 whitespace-nowrap" :class="linkClass">{{ $t('nav.pricing') }}</a>
<!-- Pricing 入口先撤下:/pricing 路由不存在,点进去是空白页。定价页上线时连同 nav.pricing 文案一起恢复。 -->
<RouterLink to="/blogs" class="font-medium text-sm rounded-md px-3 py-2 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 whitespace-nowrap" :class="linkClass" :active-class="overlay ? 'text-white bg-white/10' : 'text-foreground bg-accent'">{{ $t('nav.blogs') }}</RouterLink>
</nav>
</div>
Expand Down
121 changes: 121 additions & 0 deletions src/components/proof/ProofAgents.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<script setup lang="ts">
// S1-4「谁住在电脑里」:倾斜三卡扇形构图(参考稿样式),微缩进 proof 卡竖向视觉位。
// 布局按参考稿比例换算为 cqw(相对舞台宽度),随卡位整体缩放。
// 迷你卡只保留 图标+名字:类目/描述在小尺寸下不可读,文字信息由卡片头部 title/desc 承担。
const sideCards = [
{ name: 'Claude', icon: '/brands/claude-code-color.svg', side: 'left' as const },
{ name: 'Codex', icon: '/brands/codex-blob.svg', side: 'right' as const },
]
</script>

<template>
<div class="fan-stage">
<!-- 左右白卡:严格对称,旋转 ±8deg,位于中卡后方 -->
<div
v-for="card in sideCards"
:key="card.name"
class="mini-card mini-side"
:class="`mini-${card.side}`"
>
<div class="mini-visual">
<img :src="card.icon" :alt="card.name" />
</div>
<p class="mini-name">{{ card.name }}</p>
</div>

<!-- 中央黑卡:自家 agent,位置更高、层级最高 -->
<div class="mini-card mini-center">
<div class="mini-visual mini-center-visual">
<img src="/logo.png" alt="Memoh" class="mini-mascot" />
</div>
<p class="mini-name mini-center-name">Memoh</p>
</div>
</div>
</template>

<style scoped>
.fan-stage {
container-type: inline-size;
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.1);
background: #0b0b0e;
font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}

.mini-card {
position: absolute;
display: flex;
flex-direction: column;
gap: 2cqw;
padding: 2.5cqw;
border-radius: 2cqw;
}

/* 左右白卡 */
.mini-side {
top: 44cqw;
z-index: 1;
width: 32.2cqw;
height: 45.5cqw;
background: #ffffff;
border: 1px solid #e7e7e7;
box-shadow:
0 22px 34px rgba(20, 24, 30, 0.25),
0 4px 8px rgba(20, 24, 30, 0.12);
}
.mini-left {
left: 8cqw;
transform: rotate(-8deg);
}
.mini-right {
left: 59.8cqw;
transform: rotate(8deg);
}

.mini-visual {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
border-radius: 1.5cqw;
background: #f0f0ef;
}
.mini-visual img {
width: 42%;
height: auto;
}
.mini-name {
font-size: 3.4cqw;
line-height: 1.2;
font-weight: 650;
letter-spacing: -0.02em;
color: #111418;
}

/* 中央黑卡:left = 50 - 宽/2,严格居中(与参考稿中卡位于构图轴线一致) */
.mini-center {
left: 32.75cqw;
top: 35.8cqw;
z-index: 3;
width: 34.5cqw;
height: 49cqw;
background: #262626;
border: 1px solid rgba(255, 255, 255, 0.08);
box-shadow:
0 18px 32px rgba(0, 0, 0, 0.5),
0 3px 6px rgba(0, 0, 0, 0.3);
}
.mini-center-visual {
background: #1c1c1c;
}
.mini-mascot {
width: 52% !important;
}
.mini-center-name {
color: #f5f5f5;
}
</style>
18 changes: 15 additions & 3 deletions src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
"nav": {
"docs": "Docs",
"github": "GitHub",
"pricing": "Pricing",
"blogs": "Blogs",
"supermarket": "Supermarket",
"desktop": "Desktop",
Expand Down Expand Up @@ -69,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."
}
Expand Down Expand Up @@ -219,5 +222,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"
}
}
Loading