feat(web): 字體系統升級 + NemoClaw SVG 還原 + Knowledge Base 設計文件
Some checks failed
E2E Health Check / e2e-health (push) Has been cancelled
CD Pipeline / build-and-deploy (push) Has been cancelled

- 字體:Syne (標題) + DM Mono (內文) + VT323 (品牌點陣),替換 Inter
- Tailwind: fontFamily 更新 + 5 層文字色彩 token (primary→disabled)
- Sidebar: NemoClaw 白瓷龍蝦爪 SVG + AWOOOI 用 VT323 放大
- OpenClaw Panel: 還原 NemoClaw 3D 白瓷龍蝦爪 (替換 NemoNodeAnimation)
- Knowledge Base 設計文件 (B分離/A K8s Job/Phase1跳過向量搜尋)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
OG T
2026-04-02 00:48:42 +08:00
parent 46a346d948
commit 5959855a71
5 changed files with 339 additions and 22 deletions

View File

@@ -1,5 +1,5 @@
import type { Metadata } from 'next'
import { Inter, JetBrains_Mono, VT323 } from 'next/font/google'
import { Syne, DM_Mono, JetBrains_Mono, VT323 } from 'next/font/google'
import { notFound } from 'next/navigation'
import { NextIntlClientProvider } from 'next-intl'
import { getMessages } from 'next-intl/server'
@@ -8,17 +8,26 @@ import '../globals.css'
import { Providers } from '../providers'
import { AutoHealingErrorBoundary } from '@/components/shared/auto-healing-error-boundary'
const inter = Inter({
// Syne - 標題 / Display接近 Nothing Ndot 顯示感)
const syne = Syne({
subsets: ['latin'],
variable: '--font-inter',
variable: '--font-heading',
})
// DM Mono - 內文 / 標籤 / UI 文字(乾淨等寬)
const dmMono = DM_Mono({
weight: ['300', '400', '500'],
subsets: ['latin'],
variable: '--font-body',
})
// JetBrains Mono - 程式碼 / Terminal 專用
const jetbrainsMono = JetBrains_Mono({
subsets: ['latin'],
variable: '--font-mono',
})
// VT323 點陣字體 - 品牌 AI 狀態專用
// VT323 - 點陣像素字體,品牌 AWOOOI 專用
const vt323 = VT323({
weight: '400',
subsets: ['latin'],
@@ -61,7 +70,7 @@ export default async function LocaleLayout({
return (
<html lang={locale}>
<body
className={`${inter.variable} ${jetbrainsMono.variable} ${vt323.variable} font-body bg-nothing-gray-50 text-nothing-black antialiased`}
className={`${syne.variable} ${dmMono.variable} ${jetbrainsMono.variable} ${vt323.variable} font-body bg-nothing-gray-50 text-nothing-black antialiased`}
>
<NextIntlClientProvider messages={messages}>
<AutoHealingErrorBoundary fallbackMessage="Critical Application Error">

View File

@@ -16,8 +16,6 @@
import { useState, useEffect } from 'react'
import { useTranslations } from 'next-intl'
import { cn } from '@/lib/utils'
import { Sparkles } from 'lucide-react'
import { NemoNodeAnimation } from './nemo-node-animation'
// =============================================================================
// Types
@@ -37,6 +35,87 @@ export interface OpenClawPanelProps {
className?: string
}
// =============================================================================
// NemoClaw 3D Ceramic SVG Component (Lab-White Style)
// =============================================================================
function NemoClaw({ isActive, isPulsing }: { isActive: boolean; isPulsing: boolean }) {
return (
<svg viewBox="0 0 140 140" className="w-full h-full drop-shadow-lg" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="nc-ceramic3d" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stopColor="#FFFFFF" />
<stop offset="40%" stopColor="#F8F8F8" />
<stop offset="70%" stopColor="#E8E8E8" />
<stop offset="100%" stopColor="#D8D8D8" />
</linearGradient>
<filter id="nc-coreGlow" x="-100%" y="-100%" width="300%" height="300%">
<feGaussianBlur stdDeviation="6" result="blur" />
<feComposite in="SourceGraphic" in2="blur" operator="over" />
</filter>
<filter id="nc-pulseGlow" x="-100%" y="-100%" width="300%" height="300%">
<feGaussianBlur stdDeviation="8" result="blur">
<animate attributeName="stdDeviation" values="6;10;6" dur="1.5s" repeatCount="indefinite" />
</feGaussianBlur>
<feComposite in="SourceGraphic" in2="blur" operator="over" />
</filter>
<filter id="nc-shadow3d" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="2" dy="4" stdDeviation="3" floodOpacity="0.15" />
</filter>
</defs>
{/* Base shadow */}
<ellipse cx="70" cy="125" rx="35" ry="8" fill="rgba(0,0,0,0.08)" />
{/* Main body - 3D ceramic sphere */}
<circle cx="70" cy="70" r="32" fill="url(#nc-ceramic3d)" filter="url(#nc-shadow3d)" stroke="#E0E0E0" strokeWidth="1" />
{/* Inner ring */}
<circle cx="70" cy="70" r="26" fill="none" stroke="#D0D0D0" strokeWidth="1" opacity="0.5" />
{/* Core LED */}
<circle cx="70" cy="70" r="16" fill={isActive ? '#4A90D9' : '#B0B0B0'} filter={isPulsing ? 'url(#nc-pulseGlow)' : 'url(#nc-coreGlow)'} className="transition-all duration-500">
{isPulsing && <animate attributeName="r" values="14;17;14" dur="1s" repeatCount="indefinite" />}
</circle>
<circle cx="70" cy="70" r="8" fill="white" opacity={isActive ? 0.9 : 0.4} className="transition-opacity duration-300" />
{/* Top arm */}
<g filter="url(#nc-shadow3d)">
<path d="M 70 38 L 70 18 L 58 6 M 70 18 L 82 6" stroke="url(#nc-ceramic3d)" strokeWidth="6" strokeLinecap="round" strokeLinejoin="round" fill="none" />
<path d="M 70 38 L 70 18 L 58 6 M 70 18 L 82 6" stroke={isActive ? '#4A90D9' : '#C0C0C0'} strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" fill="none" opacity="0.6" />
<circle cx="58" cy="6" r="4" fill="url(#nc-ceramic3d)" stroke={isActive ? '#4A90D9' : '#D0D0D0'} strokeWidth="1.5" />
<circle cx="82" cy="6" r="4" fill="url(#nc-ceramic3d)" stroke={isActive ? '#4A90D9' : '#D0D0D0'} strokeWidth="1.5" />
</g>
{/* Left arm */}
<g filter="url(#nc-shadow3d)">
<path d="M 38 70 L 18 70 L 6 58 M 18 70 L 6 82" stroke="url(#nc-ceramic3d)" strokeWidth="6" strokeLinecap="round" strokeLinejoin="round" fill="none" />
<path d="M 38 70 L 18 70 L 6 58 M 18 70 L 6 82" stroke={isActive ? '#4A90D9' : '#C0C0C0'} strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" fill="none" opacity="0.6" />
<circle cx="6" cy="58" r="4" fill="url(#nc-ceramic3d)" stroke={isActive ? '#4A90D9' : '#D0D0D0'} strokeWidth="1.5" />
<circle cx="6" cy="82" r="4" fill="url(#nc-ceramic3d)" stroke={isActive ? '#4A90D9' : '#D0D0D0'} strokeWidth="1.5" />
</g>
{/* Right arm */}
<g filter="url(#nc-shadow3d)">
<path d="M 102 70 L 122 70 L 134 58 M 122 70 L 134 82" stroke="url(#nc-ceramic3d)" strokeWidth="6" strokeLinecap="round" strokeLinejoin="round" fill="none" />
<path d="M 102 70 L 122 70 L 134 58 M 122 70 L 134 82" stroke={isActive ? '#4A90D9' : '#C0C0C0'} strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" fill="none" opacity="0.6" />
<circle cx="134" cy="58" r="4" fill="url(#nc-ceramic3d)" stroke={isActive ? '#4A90D9' : '#D0D0D0'} strokeWidth="1.5" />
<circle cx="134" cy="82" r="4" fill="url(#nc-ceramic3d)" stroke={isActive ? '#4A90D9' : '#D0D0D0'} strokeWidth="1.5" />
</g>
{/* Bottom left arm */}
<g filter="url(#nc-shadow3d)">
<path d="M 48 92 L 28 112 L 16 116" stroke="url(#nc-ceramic3d)" strokeWidth="6" strokeLinecap="round" strokeLinejoin="round" fill="none" />
<path d="M 48 92 L 28 112 L 16 116" stroke={isActive ? '#4A90D9' : '#C0C0C0'} strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" fill="none" opacity="0.6" />
<circle cx="16" cy="116" r="4" fill="url(#nc-ceramic3d)" stroke={isActive ? '#4A90D9' : '#D0D0D0'} strokeWidth="1.5" />
</g>
{/* Bottom right arm */}
<g filter="url(#nc-shadow3d)">
<path d="M 92 92 L 112 112 L 124 116" stroke="url(#nc-ceramic3d)" strokeWidth="6" strokeLinecap="round" strokeLinejoin="round" fill="none" />
<path d="M 92 92 L 112 112 L 124 116" stroke={isActive ? '#4A90D9' : '#C0C0C0'} strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" fill="none" opacity="0.6" />
<circle cx="124" cy="116" r="4" fill="url(#nc-ceramic3d)" stroke={isActive ? '#4A90D9' : '#D0D0D0'} strokeWidth="1.5" />
</g>
{/* Orbit ring when active */}
{isActive && (
<circle cx="70" cy="70" r="42" fill="none" stroke="#4A90D9" strokeWidth="1" strokeDasharray="6 6" opacity="0.4"
className="animate-spin" style={{ animationDuration: '8s', transformOrigin: '70px 70px' }} />
)}
</svg>
)
}
// =============================================================================
// Status Messages (Dot Matrix Style)
// =============================================================================
@@ -146,9 +225,11 @@ export function OpenClawPanel({
)}
</div>
{/* NemoClaw 截圖風格動畫 */}
{/* NemoClaw 3D Ceramic Visualization */}
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 12, padding: '10px 12px' }}>
<NemoNodeAnimation size={72} />
<div className="relative w-20 h-20 flex-shrink-0">
<NemoClaw isActive={isActive} isPulsing={isPulsing} />
</div>
{/* 右側文字 */}
<div style={{ flex: 1 }}>

View File

@@ -168,18 +168,54 @@ export function Sidebar({
)}
style={{ zIndex: Z_INDEX.SIDEBAR }}
>
{/* Logo 區 - 極簡化 */}
{/* Logo 區 - NemoClaw + 品牌名稱 (VT323 點陣字體) */}
<div className={cn(
'flex items-center border-b-[0.5px] border-neutral-200',
'transition-all duration-300',
collapsed ? 'h-16 justify-center px-2' : 'h-16 px-4'
collapsed ? 'h-16 justify-center px-2' : 'h-16 px-4 gap-3'
)}>
{collapsed ? (
<span className="font-mono text-lg font-bold text-neutral-900 tracking-tighter">
A
</span>
) : (
<span className="font-mono text-sm font-bold text-neutral-900 tracking-widest uppercase">
{/* NemoClaw Mini SVG */}
<div style={{ width: 36, height: 36, flexShrink: 0 }}>
<svg width="36" height="36" viewBox="0 0 140 140" className="drop-shadow-sm" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="sb-ceramic" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stopColor="#FFFFFF" />
<stop offset="40%" stopColor="#F8F8F8" />
<stop offset="70%" stopColor="#E8E8E8" />
<stop offset="100%" stopColor="#D8D8D8" />
</linearGradient>
<radialGradient id="sb-led" cx="40%" cy="35%" r="60%">
<stop offset="0%" stopColor="#7AB8F5" />
<stop offset="100%" stopColor="#2B6CB0" />
</radialGradient>
</defs>
{/* Body */}
<circle cx="70" cy="70" r="32" fill="url(#sb-ceramic)" stroke="#E0E0E0" strokeWidth="1" />
<circle cx="70" cy="70" r="16" fill="url(#sb-led)">
<animate attributeName="r" values="14;17;14" dur="2s" repeatCount="indefinite" />
</circle>
<circle cx="70" cy="70" r="8" fill="white" opacity="0.8" />
{/* Top arm */}
<path d="M 70 38 L 70 18 L 58 6 M 70 18 L 82 6" stroke="url(#sb-ceramic)" strokeWidth="6" strokeLinecap="round" fill="none" />
<path d="M 70 38 L 70 18 L 58 6 M 70 18 L 82 6" stroke="#4A90D9" strokeWidth="3" strokeLinecap="round" fill="none" opacity="0.5" />
{/* Left arm */}
<path d="M 38 70 L 18 70 L 6 58 M 18 70 L 6 82" stroke="url(#sb-ceramic)" strokeWidth="6" strokeLinecap="round" fill="none" />
<path d="M 38 70 L 18 70 L 6 58 M 18 70 L 6 82" stroke="#4A90D9" strokeWidth="3" strokeLinecap="round" fill="none" opacity="0.5" />
{/* Right arm */}
<path d="M 102 70 L 122 70 L 134 58 M 122 70 L 134 82" stroke="url(#sb-ceramic)" strokeWidth="6" strokeLinecap="round" fill="none" />
<path d="M 102 70 L 122 70 L 134 58 M 122 70 L 134 82" stroke="#4A90D9" strokeWidth="3" strokeLinecap="round" fill="none" opacity="0.5" />
{/* Bottom arms */}
<path d="M 48 92 L 28 112 L 16 116" stroke="url(#sb-ceramic)" strokeWidth="6" strokeLinecap="round" fill="none" />
<path d="M 92 92 L 112 112 L 124 116" stroke="url(#sb-ceramic)" strokeWidth="6" strokeLinecap="round" fill="none" />
{/* Orbit ring */}
<circle cx="70" cy="70" r="42" fill="none" stroke="#4A90D9" strokeWidth="1" strokeDasharray="6 6" opacity="0.3">
<animateTransform attributeName="transform" type="rotate" from="0 70 70" to="360 70 70" dur="8s" repeatCount="indefinite" />
</circle>
</svg>
</div>
{/* Brand name - VT323 點陣字體 (hidden when collapsed) */}
{!collapsed && (
<span className="font-dot-matrix text-xl text-neutral-900 tracking-widest uppercase">
{tBrand('name')}
</span>
)}

View File

@@ -59,18 +59,24 @@ const config: Config = {
// ==================== Typography ====================
fontFamily: {
display: ['var(--font-mono)', 'JetBrains Mono', 'monospace'],
heading: ['Inter', 'system-ui', 'sans-serif'],
body: ['Inter', 'system-ui', 'sans-serif'],
mono: ['var(--font-mono)', 'JetBrains Mono', 'Fira Code', 'monospace'],
heading: ['var(--font-heading)', 'Syne', 'system-ui', 'sans-serif'],
body: ['var(--font-body)', 'DM Mono', 'monospace'],
display: ['var(--font-heading)', 'Syne', 'system-ui', 'sans-serif'],
mono: ['var(--font-mono)', 'JetBrains Mono', 'monospace'],
// DSEG7 點陣字體 - 用於核心指標數字顯示
'dot-matrix': ['DSEG7-Classic', 'var(--font-mono)', 'monospace'],
},
// Nothing.tech 高對比墨水色
// Nothing.tech 文字層級系統
textColor: {
ink: '#111111',
'ink-secondary': '#525252',
// 5層文字層級
primary: '#111111', // 主標題、最重要資訊
secondary: '#333333', // 大/中標題、正文
tertiary: '#525252', // 小標題、說明文字
muted: '#737373', // 輔助標籤、時間戳
disabled: '#A3A3A3', // 停用狀態
},
fontSize: {

View File

@@ -0,0 +1,185 @@
# Knowledge Base 設計文件
> **版本**: v1.0
> **日期**: 2026-04-02
> **作者**: ogt + Claude Code
> **狀態**: 已批准,待實作
---
## 背景與目標
打造 AWOOOI 知識庫系統,讓工程師與 AI 管理人員可查閱 SRE 知識,同時作為 OpenClaw 決策的 Context 來源。
**已確認決策**:
- 使用者:工程師 + AI 管理人員
- 知識來源AI 自動萃取為主,人工審核補充
- 用途:人工查閱 + AI 自動引用(兩者兼備)
- 資料結構:兩層架構(底層知識條目 + Playbook 為特殊關聯)
- UI左側分類導航 + 頂部搜尋 + 右側詳情
---
## 架構決策
| 項目 | 決策 | 理由 |
|------|------|------|
| Playbook 邊界 | **分離**,用 `related_playbook_id` 關聯 | 執行語意 ≠ 查閱知識,職責不同 |
| Migration | **K8s Job**(沿用現有方式)| 4 個 tableAlembic 是過度工程 |
| Vector Search | **Phase 1 跳過**,關鍵字 + tag | Phase 2 再評估 pgvector |
---
## 資料模型
### KnowledgeEntry新 PostgreSQL table
```sql
CREATE TABLE knowledge_entries (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL, -- Markdown
entry_type VARCHAR(50) NOT NULL, -- 'incident_case' | 'runbook' | 'best_practice' | 'postmortem'
category VARCHAR(100) NOT NULL, -- 對應左側分類樹
tags JSONB DEFAULT '[]', -- string[]
source VARCHAR(20) NOT NULL, -- 'ai_extracted' | 'human'
status VARCHAR(20) NOT NULL DEFAULT 'draft', -- 'draft' | 'review' | 'approved'
related_incident_id UUID REFERENCES incidents(id) ON DELETE SET NULL,
related_playbook_id VARCHAR(255), -- Playbook Redis key非 FK
view_count INTEGER DEFAULT 0,
created_by VARCHAR(100),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_knowledge_entries_type ON knowledge_entries(entry_type);
CREATE INDEX idx_knowledge_entries_category ON knowledge_entries(category);
CREATE INDEX idx_knowledge_entries_status ON knowledge_entries(status);
```
### entry_type 分類
| type | 說明 |
|------|------|
| `incident_case` | AI 從 Incident 萃取的案例分析 |
| `runbook` | 手動建立的操作手冊 |
| `best_practice` | 最佳實踐文章 |
| `postmortem` | 事後分析報告 |
### category 分類樹(左側導航)
```
全部
├── 基礎設施
│ ├── K8s / K3s
│ ├── 資料庫
│ └── 網路
├── 應用層
│ ├── API 服務
│ └── 前端
├── AI 系統
│ ├── OpenClaw
│ └── Playbook
└── 安全 / 合規
```
---
## API 設計
Base path: `/api/v1/knowledge`
| Method | Path | 說明 |
|--------|------|------|
| GET | `/` | 列表(支援 `category`, `type`, `status`, `q` 搜尋, `tags` 篩選)|
| GET | `/{id}` | 單筆(同時 view_count +1|
| POST | `/` | 建立新條目 |
| PATCH | `/{id}` | 更新(含狀態流轉)|
| POST | `/{id}/approve` | 審核通過status: draft/review → approved|
| DELETE | `/{id}` | 軟刪除status → archived|
| GET | `/categories` | 取得分類樹(含各類數量)|
| GET | `/search` | 關鍵字搜尋title + content + tags|
---
## 後端分層架構
遵循 leWOOOgo 積木化原則:
```
apps/api/src/
├── db/models.py # 新增 KnowledgeEntry ORM class
├── models/knowledge.py # Pydantic Schema (Request/Response)
├── repositories/
│ ├── interfaces.py # 新增 IKnowledgeRepository Protocol
│ └── knowledge_repository.py # PostgreSQL CRUD 實作
├── services/
│ └── knowledge_service.py # 業務邏輯搜尋、狀態流轉、view_count
└── api/v1/
└── knowledge.py # Routerprefix /knowledge
```
---
## 前端設計
### 頁面結構
```
/knowledge-base
├── 左側分類導航 (w-52, 固定)
│ ├── 全部 (N)
│ ├── 基礎設施 (N)
│ ├── 應用層 (N)
│ ├── AI 系統 (N)
│ └── 安全/合規 (N)
├── 主區域
│ ├── 頂部搜尋欄 + 篩選 (type / source / status)
│ ├── 結果列表
│ │ └── KnowledgeCard (title, type badge, tags, view_count, 相關 Playbook)
│ └── 空態
└── 右側詳情 Panel (抽屜式,點擊條目開啟)
├── 標題 + meta
├── Markdown 渲染
├── 相關 Playbook 連結
└── 審核操作approve / 回 review
```
### KnowledgeCard
```
┌─────────────────────────────────────────┐
│ [type badge] 標題文字 👁 12 │
│ category · source · 2026-04-01 │
│ [tag] [tag] [tag] │
│ 🔗 相關 Playbook: restart-api-server │
└─────────────────────────────────────────┘
```
### 狀態標示
| status | 顯示 | 顏色 |
|--------|------|------|
| `draft` | 草稿 | gray |
| `review` | 審核中 | warning|
| `approved` | 已批准 | healthy|
---
## Migration K8s Job
參考 `k8s/jobs/migrate-phase18-audit-logs.yaml`,建立:
```
k8s/jobs/migrate-knowledge-entries.yaml
```
執行 `CREATE TABLE knowledge_entries ...` + index。
---
## Phase 2未來
- pgvector 語意搜尋
- OpenClaw 在評估 Incident 時自動 RAG 查詢 Knowledge Base
- Embedding 自動化(建立 entry 時觸發)