initial: Hermes Glass UI personal fork + deployment memory
This commit is contained in:
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
.DS_Store
|
||||
deploy/secrets/
|
||||
*.pem
|
||||
*.key
|
||||
*.htpasswd*
|
||||
.env
|
||||
.env.local
|
||||
145
.memory/deployment-kang.md
Normal file
145
.memory/deployment-kang.md
Normal file
@@ -0,0 +1,145 @@
|
||||
---
|
||||
name: Hermes Glass UI 个人版 · kang-kang.com 部署
|
||||
description: 从公司版 hermes.milejoy.com fork + 合规隔离重建的个人 VPS 部署完整细节
|
||||
type: project
|
||||
---
|
||||
|
||||
# 架构
|
||||
|
||||
```
|
||||
浏览器
|
||||
↓ https://hermes.kang-kang.com/
|
||||
Coolify Traefik (443, letsencrypt 自动签)
|
||||
↓ Host(`hermes.kang-kang.com`) → http://10.0.0.1:8088
|
||||
宿主 nginx 1.24 (listen 10.0.0.1:8088, /etc/nginx/sites-available/hermes.kang-kang.com)
|
||||
├─ /login.html + /_auth/verify + /_auth/logout cookie 门禁
|
||||
├─ / /var/www/hermes-kang/(Glass UI 静态)
|
||||
├─ /v1/ + /api/v1/(rewrite) + /api/jobs 注入 Bearer → LXC
|
||||
├─ /memory/ + /hermes-skills/ + /classic/ 空目录占位
|
||||
└─ /health 免门禁
|
||||
Incus LXC hermes-personal (10.3.73.137, Debian trixie, privileged + nesting)
|
||||
└─ Docker 26.1.5 (hermes-agent container)
|
||||
└─ Hermes Agent v0.7, gateway run, 0.0.0.0:8642
|
||||
└─ OpenRouter → google/gemini-3.1-pro-preview
|
||||
```
|
||||
|
||||
# 关键决策
|
||||
|
||||
## 为什么用 Debian trixie 不用 Ubuntu 24.04
|
||||
- Ubuntu 24.04 + Docker 29 在非/特权 LXC 内启动容器报 `net.ipv4.ip_unprivileged_port_start: permission denied`
|
||||
- Debian trixie + Docker 26 没这个 bug(公司版 hermes-box 用的就是这个组合,直接照搬)
|
||||
- 也加了 `security.syscalls.intercept.mknod/setxattr=true`(跟公司版对齐)
|
||||
|
||||
## 为什么宿主 nginx 听 10.0.0.1:8088 不是 127.0.0.1:8088
|
||||
- Coolify-proxy (Traefik) 跑在 docker coolify 网络,从容器内访问宿主要走 docker0 gateway
|
||||
- 宿主 docker0 IP 是 `10.0.0.1`(非标准,Coolify 自定义)
|
||||
- 127.0.0.1 从 Traefik 容器里访问是容器自己的 localhost,不是宿主
|
||||
- Traefik 容器 /etc/hosts 有 `10.0.0.1 host.docker.internal` —— 用宿主 docker0 IP 直连宿主 nginx
|
||||
|
||||
## 为什么 gitea.yaml 和 notebooklm-mcp.yaml 被改了
|
||||
- Traefik watcher 解析 `dynamic/` 目录时遇到这两个文件里 `dialTimeout/responseHeaderTimeout` **不在 forwardingTimeouts 下**,报 `field not found`
|
||||
- 这个错误**阻止了整个 directory 的 reload**(不是 per-file 隔离),所以我新写的 hermes-kang.yaml 一直没被 pick up
|
||||
- 修复:把两个文件里的 `dialTimeout`/`responseHeaderTimeout` 包到 `forwardingTimeouts:` 下(Traefik v3 正确嵌套)
|
||||
- 备份:`gitea.yaml.bak-<ts>`, `notebooklm-mcp.yaml.bak-<ts>`
|
||||
- gitea/lobehub/notebooklm-mcp 都继续工作(verified via curl 200/302)
|
||||
|
||||
## 为什么不用 Traefik basicauth middleware / forwardAuth sidecar
|
||||
- 保留公司版的自定义 Liquid Glass login.html + cookie 门禁体验
|
||||
- 单用户场景 Authelia sidecar 不划算
|
||||
- nginx 熟悉度高,排障快
|
||||
|
||||
# 凭证
|
||||
|
||||
存 `credentials.md` 不在本文件重复。
|
||||
|
||||
# 文件清单
|
||||
|
||||
## 宿主 76.13.31.179
|
||||
|
||||
| 路径 | 作用 | 备注 |
|
||||
|---|---|---|
|
||||
| `/etc/nginx/sites-available/hermes.kang-kang.com` | nginx 站点,listen 10.0.0.1:8088 | sites-enabled 软链已建 |
|
||||
| `/etc/nginx/.htpasswd-hermes-kang` | bcrypt 密码 | 只 `kang` 一个用户 |
|
||||
| `/data/coolify/proxy/dynamic/hermes-kang.yaml` | Traefik 路由+letsencrypt | owner 9999:root, mode 700 |
|
||||
| `/var/www/hermes-kang/` | Glass UI 静态 | rsync 自本机个人版 src/ |
|
||||
| `/var/www/hermes-memory-kang/` | `/memory/` 路由空目录 | 自己以后填 |
|
||||
| `/var/www/hermes-skills-kang/` | `/hermes-skills/` 路由空目录 | 自己以后填 |
|
||||
| `/opt/hermes-build/` | 镜像 build 源码(rsync 自本机)| 32MB |
|
||||
| `/tmp/hermes-build.log` | build 过程 log | 留作参考 |
|
||||
|
||||
## Incus LXC hermes-personal
|
||||
|
||||
| 路径 | 作用 |
|
||||
|---|---|
|
||||
| `/opt/hermes-agent/docker-compose.yml` | compose |
|
||||
| `/opt/hermes-agent/config.yaml` | Hermes gateway 配置(OpenRouter + gemini-3.1-pro-preview)|
|
||||
| `/opt/hermes-agent/.env` | `OPENROUTER_API_KEY` + `API_SERVER_KEY`, mode 600 |
|
||||
| `/opt/hermes-agent/data/` | Hermes workspace(HERMES_HOME)|
|
||||
|
||||
# 常用操作
|
||||
|
||||
## 改前端代码后同步
|
||||
```bash
|
||||
cd ~/Projects/code/20260421-hermes-glass-ui-personal
|
||||
# 编辑 src/*
|
||||
rsync -az --delete src/ root@76.13.31.179:/var/www/hermes-kang/
|
||||
# sw.js 如需强刷:bump CACHE 版本号
|
||||
```
|
||||
|
||||
## 改后端配置/模型
|
||||
```bash
|
||||
ssh root@76.13.31.179
|
||||
incus exec hermes-personal -- bash
|
||||
cd /opt/hermes-agent
|
||||
vi config.yaml # 改模型
|
||||
vi .env # 改 key
|
||||
docker compose down && docker compose up -d
|
||||
# ⚠️ docker restart 不 reload env_file,必须 down + up
|
||||
```
|
||||
|
||||
## 换 OpenRouter key
|
||||
```bash
|
||||
incus exec hermes-personal -- bash -c "sed -i 's|^OPENROUTER_API_KEY=.*|OPENROUTER_API_KEY=<新key>|' /opt/hermes-agent/.env && cd /opt/hermes-agent && docker compose down && docker compose up -d"
|
||||
```
|
||||
|
||||
## 查日志
|
||||
```bash
|
||||
incus exec hermes-personal -- docker logs hermes-agent --tail 50
|
||||
ssh root@76.13.31.179 tail -f /var/log/nginx/error.log
|
||||
ssh root@76.13.31.179 docker logs coolify-proxy --since 2m 2>&1 | grep -i hermes
|
||||
```
|
||||
|
||||
# 不破坏的约束
|
||||
|
||||
- ✅ 不碰 Coolify 现有 22+ 容器 + Coolify-proxy 本体
|
||||
- ✅ 不碰 `/opt/lobechat-mirror/` 独立 docker compose
|
||||
- ✅ 不碰 `/opt/gitea/` `/opt/postgres/` `/opt/mysql/`
|
||||
- ✅ hermes-kang 走独立 LXC + 独立 nginx 站点 + 独立 Traefik dynamic file
|
||||
- ✅ 宿主 nginx 独自启动(systemd nginx.service 新启用)
|
||||
- ✅ 禁用了 sites-enabled/default 和 sites-enabled/styles.kang-kang.com(冲突 listen 80,且 styles 实际由 style-gallery-nginx docker 容器跑)
|
||||
|
||||
# 与公司版的差异
|
||||
|
||||
| 维度 | 公司版 hermes.milejoy.com | 个人版 hermes.kang-kang.com |
|
||||
|---|---|---|
|
||||
| 宿主 | 公司 VPS 2.24.28.41 | 个人 VPS 76.13.31.179 |
|
||||
| 入口 | 宿主 nginx 直听 443 | Coolify Traefik 443 → 宿主 nginx 10.0.0.1:8088 |
|
||||
| 证书 | certbot ai.milejoy.com 复用 | Traefik letsencrypt certresolver |
|
||||
| LXC | hermes-box, Debian trixie, 10.146.223.10 | hermes-personal, Debian trixie, 10.3.73.137 |
|
||||
| 模型 | Gemini 3 Pro Preview 直连(GOOGLE_API_KEY)| Gemini 3.1 Pro Preview via OpenRouter |
|
||||
| 认证 | basic auth boss/mile | basic auth kang |
|
||||
| Skills/Memory | 78 真实 skill + 同步真实 memory | 空目录(未来按需填充) |
|
||||
|
||||
# 合规边界(离职场景)
|
||||
|
||||
✅ **可搬**:
|
||||
- 个人编写的 Glass UI 前端源码(fork 到 `code/20260421-hermes-glass-ui-personal/`)
|
||||
- Hermes Agent 开源代码(NousResearch 上游)
|
||||
- 架构和 nginx/Traefik 配置思路
|
||||
|
||||
❌ **未搬**:
|
||||
- 公司 API Server Key `ffd2f8af...`(重新用 openssl 生成新的)
|
||||
- 公司 GOOGLE_API_KEY(换 OpenRouter + 个人 key)
|
||||
- 公司 LXC 里的 memory/skills/sessions/对话历史(个人版从零起)
|
||||
- 公司 basic auth 账号 `boss/mile`(改 `kang` 单账号)
|
||||
- 公司 nginx 证书(letsencrypt 新签)
|
||||
9
.project.json
Normal file
9
.project.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "Hermes Glass UI · 个人版",
|
||||
"description": "Fork from 20260414-hermes-glass-ui, 单用户个人 VPS 部署 (hermes.kang-kang.com)",
|
||||
"created": "2026-04-21",
|
||||
"kind": "app",
|
||||
"status": "active",
|
||||
"stack": ["HTML/CSS/JS", "Liquid Glass UI", "LXC", "OpenRouter"],
|
||||
"urls": [{ "label": "个人 VPS", "url": "https://hermes.kang-kang.com" }]
|
||||
}
|
||||
22
README.md
Normal file
22
README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# Hermes Glass UI · 个人版
|
||||
|
||||
基于 `business/20260414-hermes-glass-ui` fork,精简为个人单用户版本。
|
||||
|
||||
## 部署
|
||||
|
||||
- **线上**: https://hermes.kang-kang.com/
|
||||
- **宿主**: Hetzner VPS (`76.13.31.179`)
|
||||
- **入口**: Coolify Traefik (443) → 宿主 nginx (`127.0.0.1:8088`, cookie 门禁) → Incus LXC `hermes-personal` → Docker hermes-agent (8642)
|
||||
- **模型**: Google Gemini 3 Pro Preview (via OpenRouter)
|
||||
- **登录**: 单用户 `kang`
|
||||
|
||||
## 与公司版的差异
|
||||
|
||||
| 维度 | 公司版 (hermes.milejoy.com) | 个人版 (hermes.kang-kang.com) |
|
||||
|---|---|---|
|
||||
| 用户 | `boss` / `mile` 双账号 | `kang` 单账号 |
|
||||
| 后端 | Incus LXC hermes-box + Docker (Gemini 3 Pro via 直连) | Incus LXC hermes-personal + Docker (Gemini 3 Pro via OpenRouter) |
|
||||
| 入口 | 宿主 nginx 直听 443 | Coolify Traefik 443 → 宿主 nginx 内部 8088 |
|
||||
| 证书 | certbot (ai.milejoy.com 复用) | Traefik + letsencrypt certresolver |
|
||||
|
||||
详见 `.memory/deployment-kang.md`。
|
||||
3267
src/app.js
Normal file
3267
src/app.js
Normal file
File diff suppressed because it is too large
Load Diff
16
src/icon.svg
Normal file
16
src/icon.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#ff6900"/>
|
||||
<stop offset="100%" stop-color="#ff8830"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="shine" cx="30%" cy="28%" r="55%">
|
||||
<stop offset="0%" stop-color="rgba(255,255,255,0.55)"/>
|
||||
<stop offset="100%" stop-color="rgba(255,255,255,0)"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<rect width="512" height="512" rx="112" fill="url(#bg)"/>
|
||||
<rect width="512" height="512" rx="112" fill="url(#shine)"/>
|
||||
<text x="256" y="355" font-family="Didot, 'Bodoni 72', 'SF Pro Display', serif" font-size="300" font-weight="800" text-anchor="middle" fill="#1a0f08" letter-spacing="-10">H</text>
|
||||
<text x="256" y="425" font-family="Didot, 'Bodoni 72', 'SF Pro Display', serif" font-size="42" font-weight="700" text-anchor="middle" fill="#1a0f08" letter-spacing="8" opacity="0.85">HERMÈS</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 980 B |
1139
src/index.html
Normal file
1139
src/index.html
Normal file
File diff suppressed because it is too large
Load Diff
508
src/login.html
Normal file
508
src/login.html
Normal file
@@ -0,0 +1,508 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="theme-color" content="#0c0e12">
|
||||
<title>爱马仕 AI · 登录</title>
|
||||
<link rel="icon" type="image/svg+xml" href="./icon.svg">
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
:root {
|
||||
--orange: #ff6900;
|
||||
--orange-2: #ff8830;
|
||||
--orange-3: #ffa85a;
|
||||
--bg-0: #0c0e12;
|
||||
--text: #f5f6f8;
|
||||
--text-dim: rgba(245,246,248,0.72);
|
||||
--text-dim2: rgba(245,246,248,0.48);
|
||||
--text-dim3: rgba(245,246,248,0.28);
|
||||
--line: rgba(255,255,255,0.10);
|
||||
--line-strong: rgba(255,255,255,0.18);
|
||||
--err: #ff5d7a;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "PingFang SC", "Helvetica Neue", Arial, sans-serif;
|
||||
color: var(--text);
|
||||
background: var(--bg-0);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ========== 背景 ========== */
|
||||
.bg {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: -2;
|
||||
background:
|
||||
radial-gradient(ellipse at 10% 10%, #1a2040 0%, var(--bg-0) 55%),
|
||||
radial-gradient(ellipse at 90% 90%, #2a1030 0%, transparent 60%),
|
||||
var(--bg-0);
|
||||
overflow: hidden;
|
||||
}
|
||||
.blob {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(120px);
|
||||
mix-blend-mode: screen;
|
||||
animation: float 60s ease-in-out infinite;
|
||||
}
|
||||
.b1 { width: 720px; height: 720px; top: -180px; left: -120px; background: #3b6eff; opacity: 0.55; }
|
||||
.b2 { width: 820px; height: 820px; bottom: -220px; right: -160px; background: #a33dff; opacity: 0.5; animation-delay: -15s; }
|
||||
.b3 { width: 480px; height: 480px; top: 35%; left: 58%; background: #ff6900; opacity: 0.22; animation-delay: -30s; }
|
||||
.b4 { width: 400px; height: 400px; bottom: 15%; left: 15%; background: #ff3d70; opacity: 0.3; animation-delay: -45s; }
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translate3d(0,0,0) scale(1); }
|
||||
25% { transform: translate3d(70px,-50px,0) scale(1.08); }
|
||||
50% { transform: translate3d(-40px,60px,0) scale(0.94); }
|
||||
75% { transform: translate3d(50px,40px,0) scale(1.05); }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) { .blob { animation: none; } }
|
||||
|
||||
/* 网格噪点 */
|
||||
.grid-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
background-image:
|
||||
linear-gradient(rgba(255,255,255,0.02) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255,255,255,0.02) 1px, transparent 1px);
|
||||
background-size: 60px 60px;
|
||||
mask-image: radial-gradient(ellipse at center, black 30%, transparent 75%);
|
||||
-webkit-mask-image: radial-gradient(ellipse at center, black 30%, transparent 75%);
|
||||
}
|
||||
|
||||
/* ========== 容器 ========== */
|
||||
.wrap {
|
||||
min-height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 100%;
|
||||
max-width: 960px;
|
||||
min-height: 560px;
|
||||
display: grid;
|
||||
grid-template-columns: 1.15fr 1fr;
|
||||
background: rgba(255,255,255,0.045);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 28px;
|
||||
backdrop-filter: blur(36px) saturate(1.35);
|
||||
-webkit-backdrop-filter: blur(36px) saturate(1.35);
|
||||
box-shadow:
|
||||
0 40px 100px rgba(0,0,0,0.6),
|
||||
0 0 0 1px rgba(255,255,255,0.04),
|
||||
inset 0 1px 0 rgba(255,255,255,0.18),
|
||||
inset 0 -1px 0 rgba(255,255,255,0.04);
|
||||
overflow: hidden;
|
||||
animation: cardIn 0.6s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
@keyframes cardIn {
|
||||
from { opacity: 0; transform: translateY(16px) scale(0.98); }
|
||||
to { opacity: 1; transform: none; }
|
||||
}
|
||||
|
||||
/* ========== 左侧品牌区 ========== */
|
||||
.showcase {
|
||||
position: relative;
|
||||
padding: 56px 56px 48px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
background:
|
||||
radial-gradient(ellipse at 20% 30%, rgba(255,105,0,0.18) 0%, transparent 60%),
|
||||
radial-gradient(ellipse at 80% 80%, rgba(255,105,0,0.08) 0%, transparent 60%);
|
||||
border-right: 1px solid var(--line);
|
||||
overflow: hidden;
|
||||
}
|
||||
.showcase::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -20%;
|
||||
left: -20%;
|
||||
width: 140%;
|
||||
height: 140%;
|
||||
background: radial-gradient(circle at 30% 40%, rgba(255,105,0,0.25) 0%, transparent 50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.mega-tag {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 32px 52px 36px;
|
||||
background: var(--orange);
|
||||
color: #111;
|
||||
border-radius: 10px;
|
||||
line-height: 1;
|
||||
font-family: "Didot", "Bodoni 72", "SF Pro Display", serif;
|
||||
font-weight: 700;
|
||||
box-shadow:
|
||||
0 24px 70px rgba(255,105,0,0.55),
|
||||
0 0 0 1px rgba(26,15,8,0.3),
|
||||
inset 0 2px 0 rgba(255,255,255,0.35),
|
||||
inset 0 -2px 0 rgba(0,0,0,0.18);
|
||||
align-self: flex-start;
|
||||
}
|
||||
.mega-tag::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 7px;
|
||||
border: 1.5px solid rgba(26,15,8,0.42);
|
||||
border-radius: 5px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.mega-tag .mt-top {
|
||||
font-size: 52px;
|
||||
letter-spacing: 8px;
|
||||
}
|
||||
.mega-tag .mt-mid {
|
||||
font-size: 13px;
|
||||
margin-top: 10px;
|
||||
letter-spacing: 5.5px;
|
||||
opacity: 0.78;
|
||||
}
|
||||
|
||||
.showcase-text {
|
||||
position: relative;
|
||||
margin-top: 40px;
|
||||
}
|
||||
.mega-eyebrow {
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 4px;
|
||||
color: var(--orange-3);
|
||||
margin-bottom: 14px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.mega-title {
|
||||
font-size: 64px;
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
letter-spacing: -2.5px;
|
||||
background: linear-gradient(135deg, #fff 0%, #ffd8a0 60%, #ff8830 100%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.mega-title .mega-ai {
|
||||
font-family: "Didot", "Bodoni 72", "SF Pro Display", serif;
|
||||
font-weight: 700;
|
||||
font-style: italic;
|
||||
font-size: 0.88em;
|
||||
background: linear-gradient(135deg, #ffa85a 0%, #ff6900 100%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
padding-left: 4px;
|
||||
}
|
||||
.mega-cn {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
margin-bottom: 16px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.mega-sub {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--text-dim2);
|
||||
line-height: 1.75;
|
||||
max-width: 380px;
|
||||
}
|
||||
|
||||
.showcase-foot {
|
||||
position: relative;
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
margin-top: 32px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid var(--line);
|
||||
font-size: 11px;
|
||||
color: var(--text-dim3);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.showcase-foot b { color: var(--orange-3); font-weight: 700; letter-spacing: 0.8px; }
|
||||
|
||||
/* ========== 右侧表单区 ========== */
|
||||
.form-side {
|
||||
padding: 56px 52px 48px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.form-title {
|
||||
font-size: 22px;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.3px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.form-sub {
|
||||
font-size: 13px;
|
||||
color: var(--text-dim2);
|
||||
margin-bottom: 36px;
|
||||
}
|
||||
|
||||
.field { margin-bottom: 18px; }
|
||||
.field label {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
color: var(--text-dim);
|
||||
letter-spacing: 1.2px;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.field input {
|
||||
width: 100%;
|
||||
padding: 15px 18px;
|
||||
background: rgba(0,0,0,0.28);
|
||||
border: 1px solid var(--line-strong);
|
||||
color: var(--text);
|
||||
border-radius: 13px;
|
||||
font-size: 15px;
|
||||
font-family: inherit;
|
||||
outline: none;
|
||||
transition: border-color 0.2s, box-shadow 0.2s, background 0.2s;
|
||||
}
|
||||
.field input:focus {
|
||||
border-color: var(--orange);
|
||||
background: rgba(0,0,0,0.35);
|
||||
box-shadow: 0 0 0 4px rgba(255,105,0,0.13);
|
||||
}
|
||||
.field input::placeholder { color: var(--text-dim3); }
|
||||
|
||||
.err {
|
||||
display: none;
|
||||
margin: 6px 0 16px;
|
||||
padding: 12px 16px;
|
||||
background: rgba(255,93,122,0.1);
|
||||
border: 1px solid rgba(255,93,122,0.32);
|
||||
color: #ffb6c3;
|
||||
font-size: 12.5px;
|
||||
font-weight: 600;
|
||||
border-radius: 11px;
|
||||
}
|
||||
.err.show { display: block; animation: shake 0.32s; }
|
||||
@keyframes shake {
|
||||
0%,100% { transform: translateX(0); }
|
||||
20%,60% { transform: translateX(-5px); }
|
||||
40%,80% { transform: translateX(5px); }
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
padding: 17px;
|
||||
background: linear-gradient(135deg, var(--orange) 0%, var(--orange-2) 100%);
|
||||
color: #1a0f08;
|
||||
border: 0;
|
||||
border-radius: 13px;
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
margin-top: 10px;
|
||||
letter-spacing: 1px;
|
||||
box-shadow:
|
||||
0 12px 32px rgba(255,105,0,0.4),
|
||||
inset 0 1px 0 rgba(255,255,255,0.4),
|
||||
inset 0 -1px 0 rgba(0,0,0,0.15);
|
||||
transition: transform 0.15s, box-shadow 0.2s;
|
||||
}
|
||||
.btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow:
|
||||
0 18px 40px rgba(255,105,0,0.5),
|
||||
inset 0 1px 0 rgba(255,255,255,0.4),
|
||||
inset 0 -1px 0 rgba(0,0,0,0.15);
|
||||
}
|
||||
.btn:active { transform: translateY(0); }
|
||||
.btn:disabled { opacity: 0.55; cursor: wait; transform: none; }
|
||||
|
||||
.shortcuts {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
.shortcut-chip {
|
||||
flex: 1;
|
||||
padding: 9px 14px;
|
||||
font-size: 11.5px;
|
||||
background: rgba(255,255,255,0.04);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 9px;
|
||||
color: var(--text-dim);
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.5px;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.shortcut-chip:hover {
|
||||
background: rgba(255,105,0,0.12);
|
||||
border-color: rgba(255,105,0,0.38);
|
||||
color: var(--orange-3);
|
||||
}
|
||||
.shortcut-chip::before {
|
||||
content: "@ ";
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* ========== 响应式 ========== */
|
||||
@media (max-width: 840px) {
|
||||
.card {
|
||||
grid-template-columns: 1fr;
|
||||
max-width: 500px;
|
||||
min-height: 0;
|
||||
}
|
||||
.showcase {
|
||||
padding: 44px 40px 36px;
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
.mega-tag { padding: 24px 36px 28px; align-self: center; }
|
||||
.mega-tag .mt-top { font-size: 38px; letter-spacing: 6px; }
|
||||
.mega-tag .mt-mid { font-size: 11px; letter-spacing: 4px; }
|
||||
.showcase-text { margin-top: 28px; text-align: center; }
|
||||
.mega-title { font-size: 38px; }
|
||||
.mega-sub { max-width: 100%; }
|
||||
.showcase-foot { justify-content: center; flex-wrap: wrap; }
|
||||
.form-side { padding: 40px 36px 36px; }
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.wrap { padding: 14px; }
|
||||
.showcase { padding: 32px 24px 28px; }
|
||||
.mega-tag { padding: 18px 28px 22px; }
|
||||
.mega-tag .mt-top { font-size: 30px; letter-spacing: 5px; }
|
||||
.mega-title { font-size: 30px; }
|
||||
.form-side { padding: 32px 24px 28px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="bg">
|
||||
<div class="blob b1"></div>
|
||||
<div class="blob b2"></div>
|
||||
<div class="blob b3"></div>
|
||||
<div class="blob b4"></div>
|
||||
</div>
|
||||
<div class="grid-overlay"></div>
|
||||
|
||||
<div class="wrap">
|
||||
<div class="card">
|
||||
|
||||
<!-- 左侧品牌 showcase -->
|
||||
<div class="showcase">
|
||||
<div class="mega-tag">
|
||||
<span class="mt-top">HERMÈS</span>
|
||||
<span class="mt-mid">PARIS</span>
|
||||
</div>
|
||||
|
||||
<div class="showcase-text">
|
||||
<div class="mega-eyebrow">YOUR PRIVATE</div>
|
||||
<div class="mega-title">爱马仕</div>
|
||||
<div class="mega-cn">你的私人 AI 助手</div>
|
||||
<div class="mega-sub">
|
||||
多智能体 · Skill 编排 · 75 真实 Hermes 技能库<br>
|
||||
私密对话不外发 · 24 小时单点登录
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="showcase-foot">
|
||||
<span><b>Kang</b> · 个人版</span>
|
||||
<span>Gemini 3 Pro</span>
|
||||
<span>v0.3</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧表单 -->
|
||||
<div class="form-side">
|
||||
<div class="form-title">欢迎回来</div>
|
||||
<div class="form-sub">请输入你的账号和密码继续</div>
|
||||
|
||||
<form id="loginForm" autocomplete="on">
|
||||
<div class="field">
|
||||
<label for="user">账 号</label>
|
||||
<input type="text" id="user" name="username" placeholder="kang" autocomplete="username" required autofocus>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="pass">密 码</label>
|
||||
<input type="password" id="pass" name="password" placeholder="••••••••••" autocomplete="current-password" required>
|
||||
</div>
|
||||
<div class="err" id="err">账号或密码错误</div>
|
||||
<button type="submit" class="btn" id="btn">登 录</button>
|
||||
</form>
|
||||
|
||||
<div class="shortcuts">
|
||||
<button type="button" class="shortcut-chip" onclick="document.getElementById('user').value='kang';document.getElementById('pass').focus()">kang</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const form = document.getElementById("loginForm");
|
||||
const btn = document.getElementById("btn");
|
||||
const err = document.getElementById("err");
|
||||
|
||||
form.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
err.classList.remove("show");
|
||||
btn.disabled = true;
|
||||
btn.textContent = "验证中…";
|
||||
|
||||
const user = document.getElementById("user").value.trim();
|
||||
const pass = document.getElementById("pass").value;
|
||||
|
||||
try {
|
||||
const res = await fetch("/_auth/verify", {
|
||||
method: "GET",
|
||||
credentials: "same-origin",
|
||||
headers: {
|
||||
"Authorization": "Basic " + btoa(user + ":" + pass),
|
||||
},
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
btn.textContent = "登录成功 →";
|
||||
setTimeout(() => { location.href = "/"; }, 220);
|
||||
} else {
|
||||
showError(res.status === 401 ? "账号或密码错误" : "HTTP " + res.status);
|
||||
}
|
||||
} catch (e) {
|
||||
showError("网络错误: " + (e.message || e));
|
||||
}
|
||||
});
|
||||
|
||||
function showError(msg) {
|
||||
err.textContent = msg;
|
||||
err.classList.add("show");
|
||||
btn.disabled = false;
|
||||
btn.textContent = "登 录";
|
||||
document.getElementById("pass").select();
|
||||
}
|
||||
|
||||
if (document.cookie.split("; ").some(c => c.startsWith("hermes_auth="))) {
|
||||
location.href = "/";
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
15
src/manifest.webmanifest
Normal file
15
src/manifest.webmanifest
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "爱马仕 Hermes",
|
||||
"short_name": "Hermes",
|
||||
"description": "私人 AI 助手 · Liquid Glass UI · Gemini 3 Pro",
|
||||
"start_url": "./",
|
||||
"display": "standalone",
|
||||
"orientation": "any",
|
||||
"background_color": "#0a0f1e",
|
||||
"theme_color": "#0a0f1e",
|
||||
"icons": [
|
||||
{ "src": "./icon.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "any maskable" },
|
||||
{ "src": "./icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" },
|
||||
{ "src": "./icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" }
|
||||
]
|
||||
}
|
||||
3232
src/styles.css
Normal file
3232
src/styles.css
Normal file
File diff suppressed because it is too large
Load Diff
35
src/sw.js
Normal file
35
src/sw.js
Normal file
@@ -0,0 +1,35 @@
|
||||
// 爱马仕 Hermes · 轻量 Service Worker
|
||||
// 只缓存静态壳,API 请求始终联网
|
||||
const CACHE = "hermes-ui-v6";
|
||||
const ASSETS = [
|
||||
"./",
|
||||
"./index.html",
|
||||
"./styles.css",
|
||||
"./app.js",
|
||||
"./manifest.webmanifest",
|
||||
"./icon.svg",
|
||||
];
|
||||
|
||||
self.addEventListener("install", (e) => {
|
||||
e.waitUntil(caches.open(CACHE).then((c) => c.addAll(ASSETS)));
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener("activate", (e) => {
|
||||
e.waitUntil(
|
||||
caches.keys().then((keys) =>
|
||||
Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k)))
|
||||
)
|
||||
);
|
||||
self.clients.claim();
|
||||
});
|
||||
|
||||
self.addEventListener("fetch", (e) => {
|
||||
const url = new URL(e.request.url);
|
||||
// API 请求直通
|
||||
if (url.pathname.startsWith("/api/")) return;
|
||||
// 其他静态资源走 cache-first
|
||||
e.respondWith(
|
||||
caches.match(e.request).then((hit) => hit || fetch(e.request))
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user