diff --git a/.gitea/workflows/cd.yaml b/.gitea/workflows/cd.yaml index 53d05707..28ba5f11 100644 --- a/.gitea/workflows/cd.yaml +++ b/.gitea/workflows/cd.yaml @@ -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 值維持不變)" diff --git a/apps/api/src/api/v1/ai.py b/apps/api/src/api/v1/ai.py index 9e49f551..f353f0eb 100644 --- a/apps/api/src/api/v1/ai.py +++ b/apps/api/src/api/v1/ai.py @@ -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: diff --git a/apps/api/src/services/langfuse_client.py b/apps/api/src/services/langfuse_client.py index 63946aca..2499ede5 100644 --- a/apps/api/src/services/langfuse_client.py +++ b/apps/api/src/services/langfuse_client.py @@ -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", diff --git a/apps/web/src/app/[locale]/alerts/page.tsx b/apps/web/src/app/[locale]/alerts/page.tsx index 2f0073fe..7909dcb0 100644 --- a/apps/web/src/app/[locale]/alerts/page.tsx +++ b/apps/web/src/app/[locale]/alerts/page.tsx @@ -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 */}
- - - - + + + +
{/* Error */} @@ -163,7 +164,7 @@ export default function AlertsPage({ params }: { params: { locale: string } }) {
- {group.length} {group.length === 1 ? 'incident' : 'incidents'} + {tAlerts('incidentCount', { count: group.length })}
diff --git a/apps/web/src/app/[locale]/auto-repair/page.tsx b/apps/web/src/app/[locale]/auto-repair/page.tsx index 32a6982c..80c21141 100644 --- a/apps/web/src/app/[locale]/auto-repair/page.tsx +++ b/apps/web/src/app/[locale]/auto-repair/page.tsx @@ -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(null) const [loading, setLoading] = useState(false) const [executing, setExecuting] = useState(false) @@ -155,8 +157,8 @@ function IncidentEvalRow({ {loading && } {!loading && eval_ && ( eval_.can_auto_repair - ? 可自動修復 - : 不符合條件 + ? {t('canAutoRepair')} + : {t('notEligibleShort')} )} {expanded ? : }
@@ -170,7 +172,7 @@ function IncidentEvalRow({

{eval_.playbook_name ?? '—'}

- 風險等級 + {t('riskLevel')}

{eval_.risk_level} @@ -179,20 +181,20 @@ function IncidentEvalRow({

{eval_.success_rate != null && (
- 成功率 + {t('successRate')}

{(eval_.success_rate * 100).toFixed(1)}%

)} {eval_.total_executions != null && (
- 執行次數 + {t('execCount')}

{eval_.total_executions}

)}
- 決策原因 + {t('decisionReason')}

{eval_.reason}

@@ -207,7 +209,7 @@ function IncidentEvalRow({ ? : } - {result.success ? `執行成功 (${result.execution_time_ms}ms)` : `執行失敗: ${result.error}`} + {result.success ? t('execSuccess', { ms: result.execution_time_ms }) : t('execFailed', { error: result.error })} {result.executed_steps.length > 0 && ( @@ -236,8 +238,8 @@ function IncidentEvalRow({ )} > {executing - ? <>執行中... - : <>執行修復} + ? <>{t('executing')} + : <>{t('execute')}} )} @@ -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(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 }

- {t('nav.autoRepair')} + {tNav('autoRepair')}

- 高品質 Playbook 自動執行 · 風險 ≤ MEDIUM · 成功率 ≥ 95% + {t('subtitle')}

@@ -333,23 +336,23 @@ export default function AutoRepairPage({ params }: { params: { locale: string } {stats && (
)} @@ -367,12 +370,12 @@ export default function AutoRepairPage({ params }: { params: { locale: string } : }

- {stats.auto_repair_eligible ? '自動修復已就緒' : '自動修復未就緒'} + {stats.auto_repair_eligible ? t('ready') : t('notReady')}

{stats.auto_repair_eligible - ? `${stats.high_quality_playbooks} 個高品質 Playbook 可用` - : '需要至少 1 個高品質 Playbook(成功率 ≥ 95%、執行 ≥ 10 次)'} + ? t('readyDesc', { count: stats.high_quality_playbooks }) + : t('notReadyDesc')}

@@ -381,7 +384,7 @@ export default function AutoRepairPage({ params }: { params: { locale: string } {/* Incident evaluation list */}

- 活躍 Incident 評估(P1/P2) + {t('incidentEval')}

{incidentsLoading && ( @@ -393,7 +396,7 @@ export default function AutoRepairPage({ params }: { params: { locale: string } {!incidentsLoading && eligibleIncidents.length === 0 && (
-

目前無符合自動修復條件的 Incident

+

{t('noEligible')}

)} diff --git a/apps/web/src/app/[locale]/settings/page.tsx b/apps/web/src/app/[locale]/settings/page.tsx index 75248588..2e7eab49 100644 --- a/apps/web/src/app/[locale]/settings/page.tsx +++ b/apps/web/src/app/[locale]/settings/page.tsx @@ -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 } })
-

系統設定

+

{t('title')}

@@ -127,7 +131,7 @@ export default function SettingsPage({ params }: { params: { locale: string } }) )} > - {section.title} + {t(section.titleKey)} {activeSection !== section.id && } ))} @@ -140,14 +144,14 @@ export default function SettingsPage({ params }: { params: { locale: string } }) {/* Appearance */} {activeSection === 'appearance' && (
-

外觀設定

- +

{t('appearanceSettings')}

+ - + v7.0 - +
@@ -159,10 +163,10 @@ export default function SettingsPage({ params }: { params: { locale: string } }) {/* Language */} {activeSection === 'language' && (
-

語言設定

+

{t('languageSettings')}

{[ - { 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 => (
-

{lang.label}

-

{lang.sub}

+

{t(lang.labelKey)}

+

{t(lang.subKey)}

{params.locale === lang.code && }
@@ -187,15 +191,15 @@ export default function SettingsPage({ params }: { params: { locale: string } }) {/* Notifications */} {activeSection === 'notify' && (
-

通知設定

- +

{t('notifySettings')}

+ - + - - 後端設定 + + {t('backendConfig')}
)} @@ -203,14 +207,14 @@ export default function SettingsPage({ params }: { params: { locale: string } }) {/* System */} {activeSection === 'system' && (
-

系統資訊

+

{t('systemSettings')}

{[ - { 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 => ( - + {item.value} ))} @@ -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')} {saved && ( - 已儲存 + {t('saved')} )}
diff --git a/apps/web/src/components/ai/openclaw-panel.tsx b/apps/web/src/components/ai/openclaw-panel.tsx index 1ab4ede0..f3490536 100644 --- a/apps/web/src/components/ai/openclaw-panel.tsx +++ b/apps/web/src/components/ai/openclaw-panel.tsx @@ -120,13 +120,7 @@ function NemoClaw({ isActive, isPulsing }: { isActive: boolean; isPulsing: boole // Status Messages (Dot Matrix Style) // ============================================================================= -const STATUS_MESSAGES: Record = { - 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 - | Production + | {tBrand('environment')} {isActive && ( @@ -237,7 +232,7 @@ export function OpenClawPanel({ NemoClaw
- {STATUS_MESSAGES[status]} + {tPanel(status)}
- {section.sectionLabel} + {tSection(section.sectionKey)}
)} {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 ? (