fix: 首席架構師審查修復 — i18n/CD/時區/死碼清理
P0 前端 i18n 合規 (6 檔案):
- settings/page.tsx: 全面改用 useTranslations('settings')
- auto-repair/page.tsx: 30+ 處硬編碼改用 t('autoRepair.*')
- sidebar.tsx: sectionLabel 改用 tSection(),aria-label 國際化
- openclaw-panel.tsx: STATUS_MESSAGES 改用 tPanel(),Production 改用 tBrand
- alerts/page.tsx: StatPill label 改用 t('incident.severity.*')
P1 CD Pipeline:
- cd.yaml: runs-on 改 self-hosted (ADR-039)
- Telegram Secret 注入失敗改為 exit 1 (ADR-035)
- kubectl patch op:replace → op:add (首次部署相容)
P2 後端:
- langfuse_client.py: 移除 v4.x 死碼分支 (SDK 鎖定 <3.0.0)
- ai.py: 標記 TODO(R4) Router 瘦身
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -30,7 +30,8 @@ env:
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
# 2026-04-02 Claude Code: 修正為 self-hosted (ADR-039 鐵律 + feedback_github_billing.md)
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -151,16 +152,16 @@ jobs:
|
||||
|
||||
# 注入 Telegram Secrets (ADR-035 鐵律)
|
||||
sudo kubectl patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[
|
||||
{"op":"replace","path":"/data/OPENCLAW_TG_BOT_TOKEN","value":"'$(echo -n "${TG_BOT_TOKEN}" | base64 -w 0)'"},
|
||||
{"op":"replace","path":"/data/OPENCLAW_TG_CHAT_ID","value":"'$(echo -n "${TG_CHAT_ID}" | base64 -w 0)'"}
|
||||
]' || echo "⚠️ Telegram Secrets patch 跳過"
|
||||
{"op":"add","path":"/data/OPENCLAW_TG_BOT_TOKEN","value":"'$(echo -n "${TG_BOT_TOKEN}" | base64 -w 0)'"},
|
||||
{"op":"add","path":"/data/OPENCLAW_TG_CHAT_ID","value":"'$(echo -n "${TG_CHAT_ID}" | base64 -w 0)'"}
|
||||
]' || { echo "❌ Telegram Secrets patch 失敗 — ADR-035 鐵律"; exit 1; }
|
||||
|
||||
# 2026-03-31 ogt: 注入 AI API Keys (修復 NVIDIA/Gemini mock_fallback)
|
||||
# 2026-04-01 Claude Code: base64 -w 0 防止長 key 換行破壞 JSON
|
||||
# NVIDIA NIM (免費 tier)
|
||||
if [ -n "${NVIDIA_API_KEY}" ] && [ "${NVIDIA_API_KEY}" != "" ]; then
|
||||
sudo kubectl patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[
|
||||
{"op":"replace","path":"/data/NVIDIA_API_KEY","value":"'$(echo -n "${NVIDIA_API_KEY}" | base64 -w 0)'"}
|
||||
{"op":"add","path":"/data/NVIDIA_API_KEY","value":"'$(echo -n "${NVIDIA_API_KEY}" | base64 -w 0)'"}
|
||||
]' && echo "✅ NVIDIA_API_KEY 已注入" || echo "⚠️ NVIDIA_API_KEY patch 失敗"
|
||||
else
|
||||
echo "⚠️ NVIDIA_API_KEY 未設定,跳過"
|
||||
@@ -169,7 +170,7 @@ jobs:
|
||||
# Gemini (備援)
|
||||
if [ -n "${GEMINI_API_KEY}" ] && [ "${GEMINI_API_KEY}" != "" ]; then
|
||||
sudo kubectl patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[
|
||||
{"op":"replace","path":"/data/GEMINI_API_KEY","value":"'$(echo -n "${GEMINI_API_KEY}" | base64 -w 0)'"}
|
||||
{"op":"add","path":"/data/GEMINI_API_KEY","value":"'$(echo -n "${GEMINI_API_KEY}" | base64 -w 0)'"}
|
||||
]' && echo "✅ GEMINI_API_KEY 已注入" || echo "⚠️ GEMINI_API_KEY patch 失敗"
|
||||
else
|
||||
echo "⚠️ GEMINI_API_KEY 未設定,跳過"
|
||||
@@ -178,8 +179,8 @@ jobs:
|
||||
# 2026-04-01 Claude Code: Langfuse LLMOps keys (補齊 CD 注入,之前只有手動設定)
|
||||
if [ -n "${LANGFUSE_PUBLIC_KEY}" ] && [ -n "${LANGFUSE_SECRET_KEY}" ]; then
|
||||
sudo kubectl patch secret awoooi-secrets -n awoooi-prod --type='json' -p='[
|
||||
{"op":"replace","path":"/data/LANGFUSE_PUBLIC_KEY","value":"'$(echo -n "${LANGFUSE_PUBLIC_KEY}" | base64 -w 0)'"},
|
||||
{"op":"replace","path":"/data/LANGFUSE_SECRET_KEY","value":"'$(echo -n "${LANGFUSE_SECRET_KEY}" | base64 -w 0)'"}
|
||||
{"op":"add","path":"/data/LANGFUSE_PUBLIC_KEY","value":"'$(echo -n "${LANGFUSE_PUBLIC_KEY}" | base64 -w 0)'"},
|
||||
{"op":"add","path":"/data/LANGFUSE_SECRET_KEY","value":"'$(echo -n "${LANGFUSE_SECRET_KEY}" | base64 -w 0)'"}
|
||||
]' && echo "✅ LANGFUSE keys 已注入" || echo "⚠️ LANGFUSE keys patch 失敗"
|
||||
else
|
||||
echo "⚠️ LANGFUSE_PUBLIC_KEY/SECRET_KEY 未設定,跳過 (現有 K8s secret 值維持不變)"
|
||||
|
||||
@@ -40,6 +40,8 @@ logger = get_logger("awoooi.ai")
|
||||
|
||||
# =============================================================================
|
||||
# Helper Functions
|
||||
# TODO(R4): 移入 approval_service — Router 層不應包含業務邏輯 (feedback_lewooogo_modular_enforcement)
|
||||
# 2026-04-02 Claude Code: 架構審查標記,待 Phase R4 Router 瘦身時處理
|
||||
# =============================================================================
|
||||
|
||||
def _map_risk_level(ai_risk: AIRiskLevel) -> RiskLevel:
|
||||
|
||||
@@ -123,27 +123,14 @@ class LangfuseTraceContext:
|
||||
self._otel_trace_id
|
||||
)
|
||||
|
||||
# Langfuse v4.x API: 使用 create_trace_id() 取代舊的 trace()
|
||||
# 舊 API (v2.x): self._client.trace(name=..., metadata=...)
|
||||
# 新 API (v4.x): 透過 OTEL 整合或裝飾器
|
||||
if hasattr(self._client, "trace"):
|
||||
# Legacy API (v2.x)
|
||||
self.trace = self._client.trace(
|
||||
name=self.name,
|
||||
metadata=enriched_metadata,
|
||||
)
|
||||
if self.trace:
|
||||
self._langfuse_trace_id = self.trace.id
|
||||
else:
|
||||
# Langfuse v4.x - 使用 create_trace_id() 並記錄
|
||||
# 完整追蹤依賴 OTEL 整合
|
||||
self._langfuse_trace_id = self._client.create_trace_id()
|
||||
logger.debug(
|
||||
"langfuse_v4_trace_created",
|
||||
trace_id=self._langfuse_trace_id,
|
||||
name=self.name,
|
||||
note="Full tracing via OTEL integration",
|
||||
)
|
||||
# 2026-04-02 Claude Code: 移除 v4.x 死碼分支 — SDK 已鎖定 <3.0.0
|
||||
# v2.x API: client.trace() 建立追蹤
|
||||
self.trace = self._client.trace(
|
||||
name=self.name,
|
||||
metadata=enriched_metadata,
|
||||
)
|
||||
if self.trace:
|
||||
self._langfuse_trace_id = self.trace.id
|
||||
|
||||
logger.debug(
|
||||
"langfuse_trace_started",
|
||||
|
||||
@@ -75,6 +75,7 @@ function StatPill({ label, value, highlight }: { label: string; value: number; h
|
||||
|
||||
export default function AlertsPage({ params }: { params: { locale: string } }) {
|
||||
const t = useTranslations()
|
||||
const tAlerts = useTranslations('alerts')
|
||||
|
||||
const { incidents, isLoading, error, refresh } = useIncidents({
|
||||
pollInterval: 15000,
|
||||
@@ -120,10 +121,10 @@ export default function AlertsPage({ params }: { params: { locale: string } }) {
|
||||
|
||||
{/* Severity Stats */}
|
||||
<div className="grid grid-cols-4 gap-3 mb-6">
|
||||
<StatPill label="P0 CRITICAL" value={p0Count} highlight />
|
||||
<StatPill label="P1 HIGH" value={p1Count} />
|
||||
<StatPill label="P2 MEDIUM" value={p2Count} />
|
||||
<StatPill label="P3 LOW" value={p3Count} />
|
||||
<StatPill label={t('incident.severity.P0')} value={p0Count} highlight />
|
||||
<StatPill label={t('incident.severity.P1')} value={p1Count} />
|
||||
<StatPill label={t('incident.severity.P2')} value={p2Count} />
|
||||
<StatPill label={t('incident.severity.P3')} value={p3Count} />
|
||||
</div>
|
||||
|
||||
{/* Error */}
|
||||
@@ -163,7 +164,7 @@ export default function AlertsPage({ params }: { params: { locale: string } }) {
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<SeverityBadge severity={sev} />
|
||||
<span className="text-[11px] font-mono text-nothing-gray-400">
|
||||
{group.length} {group.length === 1 ? 'incident' : 'incidents'}
|
||||
{tAlerts('incidentCount', { count: group.length })}
|
||||
</span>
|
||||
<div className="flex-1 h-px bg-nothing-gray-200" />
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
* GET /api/v1/incidents (活躍 incident 清單)
|
||||
*
|
||||
* @updated 2026-04-01 ogt - 從佔位符升級為完整頁面
|
||||
* @updated 2026-04-02 Claude Code - i18n 合規修復 (零硬編碼)
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||
@@ -101,6 +102,7 @@ function StatCard({
|
||||
function IncidentEvalRow({
|
||||
incidentId, severity,
|
||||
}: { incidentId: string; severity: string }) {
|
||||
const t = useTranslations('autoRepair')
|
||||
const [eval_, setEval] = useState<EvaluateResponse | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [executing, setExecuting] = useState(false)
|
||||
@@ -155,8 +157,8 @@ function IncidentEvalRow({
|
||||
{loading && <RefreshCw className="w-4 h-4 animate-spin text-nothing-gray-400" />}
|
||||
{!loading && eval_ && (
|
||||
eval_.can_auto_repair
|
||||
? <span className="flex items-center gap-1 text-[11px] font-mono text-status-healthy"><CheckCircle2 className="w-3.5 h-3.5" />可自動修復</span>
|
||||
: <span className="flex items-center gap-1 text-[11px] font-mono text-nothing-gray-400"><XCircle className="w-3.5 h-3.5" />不符合條件</span>
|
||||
? <span className="flex items-center gap-1 text-[11px] font-mono text-status-healthy"><CheckCircle2 className="w-3.5 h-3.5" />{t('canAutoRepair')}</span>
|
||||
: <span className="flex items-center gap-1 text-[11px] font-mono text-nothing-gray-400"><XCircle className="w-3.5 h-3.5" />{t('notEligibleShort')}</span>
|
||||
)}
|
||||
{expanded ? <ChevronUp className="w-4 h-4 text-nothing-gray-400" /> : <ChevronDown className="w-4 h-4 text-nothing-gray-400" />}
|
||||
</div>
|
||||
@@ -170,7 +172,7 @@ function IncidentEvalRow({
|
||||
<p className="font-mono text-nothing-black mt-0.5">{eval_.playbook_name ?? '—'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-[10px] font-mono text-nothing-gray-500 uppercase">風險等級</span>
|
||||
<span className="text-[10px] font-mono text-nothing-gray-500 uppercase">{t('riskLevel')}</span>
|
||||
<p className="mt-0.5">
|
||||
<span className={cn('px-2 py-0.5 rounded border text-[11px] font-mono font-bold', RISK_STYLE[eval_.risk_level] ?? RISK_STYLE.MEDIUM)}>
|
||||
{eval_.risk_level}
|
||||
@@ -179,20 +181,20 @@ function IncidentEvalRow({
|
||||
</div>
|
||||
{eval_.success_rate != null && (
|
||||
<div>
|
||||
<span className="text-[10px] font-mono text-nothing-gray-500 uppercase">成功率</span>
|
||||
<span className="text-[10px] font-mono text-nothing-gray-500 uppercase">{t('successRate')}</span>
|
||||
<p className="font-mono text-status-healthy font-bold mt-0.5">{(eval_.success_rate * 100).toFixed(1)}%</p>
|
||||
</div>
|
||||
)}
|
||||
{eval_.total_executions != null && (
|
||||
<div>
|
||||
<span className="text-[10px] font-mono text-nothing-gray-500 uppercase">執行次數</span>
|
||||
<span className="text-[10px] font-mono text-nothing-gray-500 uppercase">{t('execCount')}</span>
|
||||
<p className="font-mono text-nothing-black mt-0.5">{eval_.total_executions}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="p-3 bg-nothing-gray-50 rounded-lg">
|
||||
<span className="text-[10px] font-mono text-nothing-gray-500 uppercase">決策原因</span>
|
||||
<span className="text-[10px] font-mono text-nothing-gray-500 uppercase">{t('decisionReason')}</span>
|
||||
<p className="text-sm font-mono text-nothing-gray-700 mt-1">{eval_.reason}</p>
|
||||
</div>
|
||||
|
||||
@@ -207,7 +209,7 @@ function IncidentEvalRow({
|
||||
? <CheckCircle2 className="w-4 h-4 text-status-healthy" />
|
||||
: <XCircle className="w-4 h-4 text-status-critical" />}
|
||||
<span className={cn('text-sm font-mono font-bold', result.success ? 'text-status-healthy' : 'text-status-critical')}>
|
||||
{result.success ? `執行成功 (${result.execution_time_ms}ms)` : `執行失敗: ${result.error}`}
|
||||
{result.success ? t('execSuccess', { ms: result.execution_time_ms }) : t('execFailed', { error: result.error })}
|
||||
</span>
|
||||
</div>
|
||||
{result.executed_steps.length > 0 && (
|
||||
@@ -236,8 +238,8 @@ function IncidentEvalRow({
|
||||
)}
|
||||
>
|
||||
{executing
|
||||
? <><RefreshCw className="w-4 h-4 animate-spin" />執行中...</>
|
||||
: <><Play className="w-4 h-4" />執行修復</>}
|
||||
? <><RefreshCw className="w-4 h-4 animate-spin" />{t('executing')}</>
|
||||
: <><Play className="w-4 h-4" />{t('execute')}</>}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -251,7 +253,9 @@ function IncidentEvalRow({
|
||||
// =============================================================================
|
||||
|
||||
export default function AutoRepairPage({ params }: { params: { locale: string } }) {
|
||||
const t = useTranslations()
|
||||
const t = useTranslations('autoRepair')
|
||||
const tNav = useTranslations('nav')
|
||||
const tCommon = useTranslations('common')
|
||||
|
||||
const [stats, setStats] = useState<AutoRepairStats | null>(null)
|
||||
const [statsLoading, setStatsLoading] = useState(true)
|
||||
@@ -263,7 +267,6 @@ export default function AutoRepairPage({ params }: { params: { locale: string }
|
||||
enablePolling: true,
|
||||
})
|
||||
|
||||
// P0/P1/P2 只有這些才評估自動修復(嚴重度限制)
|
||||
const eligibleIncidents = (incidents ?? []).filter(i =>
|
||||
i.severity === 'P1' || i.severity === 'P2'
|
||||
)
|
||||
@@ -300,10 +303,10 @@ export default function AutoRepairPage({ params }: { params: { locale: string }
|
||||
<div>
|
||||
<h2 className="font-heading text-2xl font-bold text-nothing-black flex items-center gap-2">
|
||||
<Wrench className="w-6 h-6" />
|
||||
{t('nav.autoRepair')}
|
||||
{tNav('autoRepair')}
|
||||
</h2>
|
||||
<p className="mt-1 text-sm text-nothing-gray-500 font-mono">
|
||||
高品質 Playbook 自動執行 · 風險 ≤ MEDIUM · 成功率 ≥ 95%
|
||||
{t('subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
@@ -317,7 +320,7 @@ export default function AutoRepairPage({ params }: { params: { locale: string }
|
||||
)}
|
||||
>
|
||||
<RefreshCw className={cn('w-3.5 h-3.5', statsLoading && 'animate-spin')} />
|
||||
{t('common.refresh')}
|
||||
{tCommon('refresh')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -333,23 +336,23 @@ export default function AutoRepairPage({ params }: { params: { locale: string }
|
||||
{stats && (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-6">
|
||||
<StatCard
|
||||
label="已批准 Playbooks"
|
||||
label={t('approvedPlaybooks')}
|
||||
value={stats.approved_playbooks}
|
||||
/>
|
||||
<StatCard
|
||||
label="高品質 Playbooks"
|
||||
label={t('highQualityPlaybooks')}
|
||||
value={stats.high_quality_playbooks}
|
||||
sub="成功率 ≥ 95% · 執行 ≥ 10 次"
|
||||
sub={t('highQualitySub')}
|
||||
highlight
|
||||
/>
|
||||
<StatCard
|
||||
label="總執行次數"
|
||||
label={t('totalExecutions')}
|
||||
value={stats.total_executions}
|
||||
/>
|
||||
<StatCard
|
||||
label="整體成功率"
|
||||
label={t('overallSuccessRate')}
|
||||
value={`${(stats.overall_success_rate * 100).toFixed(1)}%`}
|
||||
sub={stats.auto_repair_eligible ? '✓ 可啟用自動修復' : '尚無高品質 Playbook'}
|
||||
sub={stats.auto_repair_eligible ? t('eligible') : t('notEligible')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -367,12 +370,12 @@ export default function AutoRepairPage({ params }: { params: { locale: string }
|
||||
: <ShieldAlert className="w-5 h-5 text-nothing-gray-400" />}
|
||||
<div>
|
||||
<p className={cn('text-sm font-mono font-semibold', stats.auto_repair_eligible ? 'text-status-healthy' : 'text-nothing-gray-600')}>
|
||||
{stats.auto_repair_eligible ? '自動修復已就緒' : '自動修復未就緒'}
|
||||
{stats.auto_repair_eligible ? t('ready') : t('notReady')}
|
||||
</p>
|
||||
<p className="text-xs font-mono text-nothing-gray-400">
|
||||
{stats.auto_repair_eligible
|
||||
? `${stats.high_quality_playbooks} 個高品質 Playbook 可用`
|
||||
: '需要至少 1 個高品質 Playbook(成功率 ≥ 95%、執行 ≥ 10 次)'}
|
||||
? t('readyDesc', { count: stats.high_quality_playbooks })
|
||||
: t('notReadyDesc')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -381,7 +384,7 @@ export default function AutoRepairPage({ params }: { params: { locale: string }
|
||||
{/* Incident evaluation list */}
|
||||
<div>
|
||||
<h3 className="text-[11px] font-mono text-nothing-gray-500 uppercase tracking-widest mb-3">
|
||||
活躍 Incident 評估(P1/P2)
|
||||
{t('incidentEval')}
|
||||
</h3>
|
||||
|
||||
{incidentsLoading && (
|
||||
@@ -393,7 +396,7 @@ export default function AutoRepairPage({ params }: { params: { locale: string }
|
||||
{!incidentsLoading && eligibleIncidents.length === 0 && (
|
||||
<div className="text-center py-10 border border-dashed border-nothing-gray-200 rounded-lg">
|
||||
<CheckCircle2 className="w-8 h-8 text-status-healthy mx-auto mb-2" />
|
||||
<p className="font-mono text-sm text-nothing-gray-500">目前無符合自動修復條件的 Incident</p>
|
||||
<p className="font-mono text-sm text-nothing-gray-500">{t('noEligible')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -7,9 +7,11 @@
|
||||
* 設定值持久化至 localStorage
|
||||
*
|
||||
* @updated 2026-04-01 ogt - 從佔位符升級為完整頁面
|
||||
* @updated 2026-04-02 Claude Code - i18n 合規修復 (零硬編碼)
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { AppLayout } from '@/components/layout'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { cn } from '@/lib/utils'
|
||||
@@ -25,17 +27,10 @@ import {
|
||||
interface SettingSection {
|
||||
id: string
|
||||
icon: typeof Settings
|
||||
title: string
|
||||
description: string
|
||||
titleKey: string
|
||||
descKey: string
|
||||
}
|
||||
|
||||
const SECTIONS: SettingSection[] = [
|
||||
{ id: 'appearance', icon: Monitor, title: '外觀', description: '主題、字體、密度' },
|
||||
{ id: 'language', icon: Globe, title: '語言', description: '介面語言設定' },
|
||||
{ id: 'notify', icon: Bell, title: '通知', description: 'Telegram / 瀏覽器通知偏好' },
|
||||
{ id: 'system', icon: Settings, title: '系統資訊', description: '版本與 API 端點' },
|
||||
]
|
||||
|
||||
// =============================================================================
|
||||
// Setting Row
|
||||
// =============================================================================
|
||||
@@ -76,10 +71,19 @@ function Toggle({ value, onChange }: { value: boolean; onChange: (v: boolean) =>
|
||||
// =============================================================================
|
||||
|
||||
export default function SettingsPage({ params }: { params: { locale: string } }) {
|
||||
const t = useTranslations('settings')
|
||||
const tNav = useTranslations('nav')
|
||||
const pathname = usePathname()
|
||||
const [activeSection, setActiveSection] = useState('appearance')
|
||||
const [saved, setSaved] = useState(false)
|
||||
|
||||
const SECTIONS: SettingSection[] = [
|
||||
{ id: 'appearance', icon: Monitor, titleKey: 'appearance', descKey: 'appearanceDesc' },
|
||||
{ id: 'language', icon: Globe, titleKey: 'language', descKey: 'languageDesc' },
|
||||
{ id: 'notify', icon: Bell, titleKey: 'notify', descKey: 'notifyDesc' },
|
||||
{ id: 'system', icon: Settings, titleKey: 'system', descKey: 'systemDesc' },
|
||||
]
|
||||
|
||||
// Settings state — loaded from / persisted to localStorage
|
||||
const [notifyBrowser, setNotifyBrowser] = useState(false)
|
||||
const [notifyP0Only, setNotifyP0Only] = useState(false)
|
||||
@@ -108,7 +112,7 @@ export default function SettingsPage({ params }: { params: { locale: string } })
|
||||
<AppLayout locale={params.locale}>
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<Settings className="w-6 h-6 text-nothing-black" />
|
||||
<h2 className="font-heading text-2xl font-bold text-nothing-black">系統設定</h2>
|
||||
<h2 className="font-heading text-2xl font-bold text-nothing-black">{t('title')}</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-6">
|
||||
@@ -127,7 +131,7 @@ export default function SettingsPage({ params }: { params: { locale: string } })
|
||||
)}
|
||||
>
|
||||
<section.icon className="w-4 h-4 flex-shrink-0" />
|
||||
<span className="text-sm font-mono">{section.title}</span>
|
||||
<span className="text-sm font-mono">{t(section.titleKey)}</span>
|
||||
{activeSection !== section.id && <ChevronRight className="w-3 h-3 ml-auto text-nothing-gray-400" />}
|
||||
</button>
|
||||
))}
|
||||
@@ -140,14 +144,14 @@ export default function SettingsPage({ params }: { params: { locale: string } })
|
||||
{/* Appearance */}
|
||||
{activeSection === 'appearance' && (
|
||||
<div>
|
||||
<h3 className="font-mono text-sm font-bold text-nothing-black mb-4 uppercase tracking-wider">外觀設定</h3>
|
||||
<SettingRow label="緊湊模式" description="減少各元件間距,顯示更多資訊">
|
||||
<h3 className="font-mono text-sm font-bold text-nothing-black mb-4 uppercase tracking-wider">{t('appearanceSettings')}</h3>
|
||||
<SettingRow label={t('compactMode')} description={t('compactModeDesc')}>
|
||||
<Toggle value={compactMode} onChange={setCompactMode} />
|
||||
</SettingRow>
|
||||
<SettingRow label="設計系統" description="Nothing.tech 純白工業風(固定)">
|
||||
<SettingRow label={t('designSystem')} description={t('designSystemValue')}>
|
||||
<span className="text-xs font-mono text-nothing-gray-400 bg-nothing-gray-100 px-2 py-1 rounded">v7.0</span>
|
||||
</SettingRow>
|
||||
<SettingRow label="主題色" description="OpenClaw Blue + 橘紅 Accent(固定)">
|
||||
<SettingRow label={t('themeColor')} description={t('themeColorValue')}>
|
||||
<div className="flex gap-1.5">
|
||||
<div className="w-5 h-5 rounded-full bg-[#4A90D9] border-2 border-white shadow" />
|
||||
<div className="w-5 h-5 rounded-full bg-[#d97757] border-2 border-white shadow" />
|
||||
@@ -159,10 +163,10 @@ export default function SettingsPage({ params }: { params: { locale: string } })
|
||||
{/* Language */}
|
||||
{activeSection === 'language' && (
|
||||
<div>
|
||||
<h3 className="font-mono text-sm font-bold text-nothing-black mb-4 uppercase tracking-wider">語言設定</h3>
|
||||
<h3 className="font-mono text-sm font-bold text-nothing-black mb-4 uppercase tracking-wider">{t('languageSettings')}</h3>
|
||||
{[
|
||||
{ code: 'zh-TW', label: '繁體中文', sub: 'Traditional Chinese' },
|
||||
{ code: 'en', label: 'English', sub: 'English (US)' },
|
||||
{ code: 'zh-TW', labelKey: 'zhTW', subKey: 'zhTWSub' },
|
||||
{ code: 'en', labelKey: 'en', subKey: 'enSub' },
|
||||
].map(lang => (
|
||||
<div
|
||||
key={lang.code}
|
||||
@@ -175,8 +179,8 @@ export default function SettingsPage({ params }: { params: { locale: string } })
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-nothing-black">{lang.label}</p>
|
||||
<p className="text-xs font-mono text-nothing-gray-400">{lang.sub}</p>
|
||||
<p className="text-sm font-medium text-nothing-black">{t(lang.labelKey)}</p>
|
||||
<p className="text-xs font-mono text-nothing-gray-400">{t(lang.subKey)}</p>
|
||||
</div>
|
||||
{params.locale === lang.code && <CheckCircle2 className="w-5 h-5 text-claw-blue" />}
|
||||
</div>
|
||||
@@ -187,15 +191,15 @@ export default function SettingsPage({ params }: { params: { locale: string } })
|
||||
{/* Notifications */}
|
||||
{activeSection === 'notify' && (
|
||||
<div>
|
||||
<h3 className="font-mono text-sm font-bold text-nothing-black mb-4 uppercase tracking-wider">通知設定</h3>
|
||||
<SettingRow label="瀏覽器推播通知" description="新 Incident 時顯示系統通知">
|
||||
<h3 className="font-mono text-sm font-bold text-nothing-black mb-4 uppercase tracking-wider">{t('notifySettings')}</h3>
|
||||
<SettingRow label={t('browserNotify')} description={t('browserNotifyDesc')}>
|
||||
<Toggle value={notifyBrowser} onChange={setNotifyBrowser} />
|
||||
</SettingRow>
|
||||
<SettingRow label="僅 P0 CRITICAL 通知" description="過濾低嚴重度告警,減少噪音">
|
||||
<SettingRow label={t('p0Only')} description={t('p0OnlyDesc')}>
|
||||
<Toggle value={notifyP0Only} onChange={setNotifyP0Only} />
|
||||
</SettingRow>
|
||||
<SettingRow label="Telegram 通知" description="由 OpenClaw Bot 推送(需後端設定)">
|
||||
<span className="text-xs font-mono text-nothing-gray-400 bg-nothing-gray-100 px-2 py-1 rounded">後端設定</span>
|
||||
<SettingRow label={t('telegramNotify')} description={t('telegramNotifyDesc')}>
|
||||
<span className="text-xs font-mono text-nothing-gray-400 bg-nothing-gray-100 px-2 py-1 rounded">{t('backendConfig')}</span>
|
||||
</SettingRow>
|
||||
</div>
|
||||
)}
|
||||
@@ -203,14 +207,14 @@ export default function SettingsPage({ params }: { params: { locale: string } })
|
||||
{/* System */}
|
||||
{activeSection === 'system' && (
|
||||
<div>
|
||||
<h3 className="font-mono text-sm font-bold text-nothing-black mb-4 uppercase tracking-wider">系統資訊</h3>
|
||||
<h3 className="font-mono text-sm font-bold text-nothing-black mb-4 uppercase tracking-wider">{t('systemSettings')}</h3>
|
||||
{[
|
||||
{ label: '前端版本', value: 'v1.0.0' },
|
||||
{ label: 'API 端點', value: process.env.NEXT_PUBLIC_API_URL ?? '(未設定)' },
|
||||
{ label: '設計系統', value: 'Nothing.tech v7.0' },
|
||||
{ label: 'Phase', value: 'Phase 23 (2026-04-01)' },
|
||||
{ labelKey: 'frontendVersion', value: 'v1.0.0' },
|
||||
{ labelKey: 'apiEndpoint', value: process.env.NEXT_PUBLIC_API_URL ?? t('notConfigured') },
|
||||
{ labelKey: 'designSystem', value: 'Nothing.tech v7.0' },
|
||||
{ labelKey: 'phase', value: 'Phase 23 (2026-04-01)' },
|
||||
].map(item => (
|
||||
<SettingRow key={item.label} label={item.label}>
|
||||
<SettingRow key={item.labelKey} label={t(item.labelKey)}>
|
||||
<span className="text-sm font-mono text-nothing-gray-600">{item.value}</span>
|
||||
</SettingRow>
|
||||
))}
|
||||
@@ -224,12 +228,12 @@ export default function SettingsPage({ params }: { params: { locale: string } })
|
||||
onClick={saveSettings}
|
||||
className="px-5 py-2.5 rounded-lg bg-nothing-black text-white font-mono text-sm font-semibold hover:bg-nothing-black/90 transition-colors"
|
||||
>
|
||||
儲存設定
|
||||
{t('save')}
|
||||
</button>
|
||||
{saved && (
|
||||
<span className="flex items-center gap-1.5 text-sm font-mono text-status-healthy">
|
||||
<CheckCircle2 className="w-4 h-4" />
|
||||
已儲存
|
||||
{t('saved')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -120,13 +120,7 @@ function NemoClaw({ isActive, isPulsing }: { isActive: boolean; isPulsing: boole
|
||||
// Status Messages (Dot Matrix Style)
|
||||
// =============================================================================
|
||||
|
||||
const STATUS_MESSAGES: Record<OpenClawStatus, string> = {
|
||||
patrolling: '[AGENT] patrolling...',
|
||||
intercepting: '[SYS] Intercepting anomaly...',
|
||||
analyzing: '[SYS] Analyzing blast radius...',
|
||||
generating: '[SYS] Generating proposed action...',
|
||||
complete: '[SYS] Analysis complete',
|
||||
}
|
||||
// 2026-04-02 Claude Code: STATUS_MESSAGES 已移除,改用 i18n 鍵值 openclawPanel.*
|
||||
|
||||
|
||||
// =============================================================================
|
||||
@@ -165,13 +159,14 @@ export function OpenClawPanel({
|
||||
onAnalysisComplete,
|
||||
className,
|
||||
}: OpenClawPanelProps) {
|
||||
const _t = useTranslations('ai')
|
||||
const tPanel = useTranslations('openclawPanel')
|
||||
const tBrand = useTranslations('brand')
|
||||
// Phase 8.0 #16: 移除 cursorVisible state,改用 CSS animate-pulse
|
||||
|
||||
const isActive = status !== 'patrolling'
|
||||
const isPulsing = status === 'intercepting' || status === 'analyzing'
|
||||
|
||||
const statusMessage = STATUS_MESSAGES[status]
|
||||
const statusMessage = tPanel(status)
|
||||
const displayText = useTypewriter(statusMessage, 40)
|
||||
|
||||
// Notify when complete
|
||||
@@ -212,7 +207,7 @@ export function OpenClawPanel({
|
||||
AWOOOI v1.0.0
|
||||
</span>
|
||||
<span className="font-dot-matrix text-xs text-nothing-gray-400">
|
||||
| Production
|
||||
| {tBrand('environment')}
|
||||
</span>
|
||||
{isActive && (
|
||||
<span className="w-2 h-2 rounded-full bg-claw-blue animate-ping" />
|
||||
@@ -237,7 +232,7 @@ export function OpenClawPanel({
|
||||
NemoClaw
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: '#87867f', marginBottom: 6 }}>
|
||||
{STATUS_MESSAGES[status]}
|
||||
{tPanel(status)}
|
||||
</div>
|
||||
<span style={{
|
||||
fontSize: 11, padding: '2px 8px',
|
||||
|
||||
@@ -69,8 +69,8 @@ type NavSection = {
|
||||
|
||||
const NAV_SECTIONS: NavSection[] = [
|
||||
{
|
||||
sectionKey: 'ai-core',
|
||||
sectionLabel: 'AI 核心',
|
||||
sectionKey: 'aiCore',
|
||||
sectionLabel: '',
|
||||
items: [
|
||||
{ id: 'ai-center', href: '/', labelKey: 'dashboard', Icon: LayoutDashboard },
|
||||
{ id: 'authorizations', href: '/authorizations', labelKey: 'approvals', Icon: ShieldCheck, badge: true },
|
||||
@@ -79,7 +79,7 @@ const NAV_SECTIONS: NavSection[] = [
|
||||
},
|
||||
{
|
||||
sectionKey: 'monitoring',
|
||||
sectionLabel: '監控與安全',
|
||||
sectionLabel: '',
|
||||
items: [
|
||||
{ id: 'monitoring', href: '/monitoring', labelKey: 'monitoring', Icon: Monitor },
|
||||
{ id: 'apm', href: '/apm', labelKey: 'apm', Icon: Activity },
|
||||
@@ -91,7 +91,7 @@ const NAV_SECTIONS: NavSection[] = [
|
||||
},
|
||||
{
|
||||
sectionKey: 'ops',
|
||||
sectionLabel: '運維管理',
|
||||
sectionLabel: '',
|
||||
items: [
|
||||
{ id: 'auto-repair', href: '/auto-repair', labelKey: 'autoRepair', Icon: Wrench },
|
||||
{ id: 'deployments', href: '/deployments', labelKey: 'deployments', Icon: Package },
|
||||
@@ -103,7 +103,7 @@ const NAV_SECTIONS: NavSection[] = [
|
||||
},
|
||||
{
|
||||
sectionKey: 'knowledge',
|
||||
sectionLabel: '知識與工具',
|
||||
sectionLabel: '',
|
||||
items: [
|
||||
{ id: 'knowledge-base', href: '/knowledge-base', labelKey: 'knowledge', Icon: BookOpen },
|
||||
{ id: 'terminal', href: '/terminal', labelKey: 'terminal', Icon: Terminal },
|
||||
@@ -133,6 +133,8 @@ export function Sidebar({
|
||||
}: SidebarProps) {
|
||||
const t = useTranslations('nav')
|
||||
const tBrand = useTranslations('brand')
|
||||
const tSection = useTranslations('navSection')
|
||||
const tSidebar = useTranslations('sidebar')
|
||||
const pathname = usePathname()
|
||||
|
||||
// Phase 8.0 #15: 改用 SSE 驅動的 pending count (移除 30s polling)
|
||||
@@ -236,7 +238,7 @@ export function Sidebar({
|
||||
padding: '8px 12px 3px',
|
||||
fontFamily: 'monospace',
|
||||
}}>
|
||||
{section.sectionLabel}
|
||||
{tSection(section.sectionKey)}
|
||||
</div>
|
||||
)}
|
||||
{section.items.map(item => {
|
||||
@@ -338,7 +340,7 @@ export function Sidebar({
|
||||
'hover:bg-neutral-50',
|
||||
'transition-all duration-150'
|
||||
)}
|
||||
aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
||||
aria-label={collapsed ? tSidebar('expand') : tSidebar('collapse')}
|
||||
>
|
||||
{collapsed ? (
|
||||
<ChevronRight className="w-3 h-3 text-neutral-400" />
|
||||
|
||||
Reference in New Issue
Block a user