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 ? (