V10.614 部署監控頁繁中化
All checks were successful
CD Pipeline / deploy (push) Successful in 1m11s

This commit is contained in:
OoO
2026-06-16 10:03:18 +08:00
parent 239b773288
commit 32b7071ab6
13 changed files with 104 additions and 58 deletions

6
app.py
View File

@@ -230,12 +230,12 @@ csrf.exempt(ai_bp) # ICAIM API 使用內部呼叫,不需要 CSRF
sys_log.info("[Blueprint] ✅ AI 智慧文案系統 Blueprint 已註冊")
# ==========================================
# 🔧 Blueprint 註冊 - CI/CD Dashboard
# 🔧 Blueprint 註冊 - 部署監控
# ==========================================
from routes.cicd_routes import cicd_bp
app.register_blueprint(cicd_bp)
csrf.exempt(cicd_bp) # CI/CD API doesn't need CSRF
sys_log.info("[Blueprint] CI/CD Dashboard Blueprint registered")
csrf.exempt(cicd_bp) # 部署監控 API 使用內部呼叫,不需要 CSRF
sys_log.info("[Blueprint] ✅ 部署監控 Blueprint 已註冊")
# ==========================================
# 🔧 Blueprint 註冊 - Code Review 系統

View File

@@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
# ==========================================
# 系統版本與路徑
# ==========================================
SYSTEM_VERSION = "V10.613"
SYSTEM_VERSION = "V10.614"
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
public_url = PUBLIC_URL # 用於模板顯示

View File

@@ -2,7 +2,7 @@
> **最後更新**: 2026-06-16 (台北時間)
> **狀態**: 🟢 四 AI Agent 自動化閉環已落地LLM 路由紅線升級為 Ollama-first 三主機級聯PChome 後台業績匯入韌性已補強產品定位正名為「PChome 業績成長自動化作戰系統」外部市場來源正規化層、自動同步、作戰清單與價格參考表優先讀取、CSV 備援預檢、前台操作入口與高可見頁面繁中化守門已建立
> **適用版本**: V10.613
> **適用版本**: V10.614
---
@@ -60,6 +60,7 @@
- V10.611 起 `/ai_intelligence` 是營運使用者主入口:頁首提供「今日作戰入口」,依序連到 PChome 成長作戰、補商品對應、MOMO 外部價格參考與外部報價預檢;作戰清單左側會直接顯示今日優先動作與資料來源摘要。
- V10.612 起 `/api/ai/icaim/dashboard` 的「MOMO 外部價格參考」表格也優先讀 `external_offers`,缺資料才 fallback `competitor_prices`;價差與風險改採 PChome 視角,正數代表 PChome 比 MOMO 外部參考價高。
- V10.613 起高可見前台頁面必須以繁體中文呈現程式碼審查、AI 自動化健康檢查、PPT 產線與商品看板操作標籤不得使用英文工程標題或簡體字;測試需防止頁面文案退回英文。
- V10.614 起部署監控、基礎設施生命線與 PPT 產線狀態也納入繁中守門:前台不得顯示 `Dashboard``Pipeline``Runtime` 等工程詞,動態階段需轉成「測試 / 建置 / 部署」。
## 零之一、12 Agent 決策信封2026-05-24

View File

@@ -233,3 +233,9 @@
- 已把 `/code-review/` 顯示文字改成「AI 程式碼審查」「流程進度」「程式碼審查完成」,並將 OpenClaw 模型顯示改成「Ollama 優先」,避免前台誤以為 Gemini 是主路徑。
- 已把 AI 自動化健康檢查頁改為白話繁中命名,狀態顯示改成「正常 / 注意 / 嚴重 / 產生時間」。
- 已把 PPT 觀測台與商品看板高可見英文標籤改成「產線健康度 / 工作隊列 / 視覺問題 / 產線控制台 / 覆蓋率流程」,並新增測試防回歸。
## 17. 2026-06-16 V10.614 部署與基礎設施頁繁中化
- `/cicd` 對使用者顯示為「部署監控」,不再以前台標題顯示 `CI/CD Dashboard`部署流程、部署歷史、GitLab 部署紀錄皆使用白話繁中。
- 部署流程圖會把後端階段代碼 `test / build / deploy` 轉成「測試 / 建置 / 部署」,診斷狀態也轉成「正常 / 注意 / 失敗」。
- `/observability/host_health` 與 PPT 產線視覺狀態把 `Runtime` / `Vision QA` 改成「執行環境」與「視覺檢查」,並更新測試防回歸。

View File

@@ -988,7 +988,7 @@ def agent_orchestration_dashboard():
recommendations.append({
'severity': 'high', 'agent': ag['label'],
'finding': f"錯誤率 {ag['error_rate']:.1f}%{ag['errors']}/{ag['calls']}",
'suggestion': '觸發 Code Review Pipeline 找 regressionai_calls 觀測台一鍵)',
'suggestion': '觸發程式碼審查流程找回歸問題ai_calls 觀測台一鍵)',
})
# 規則 3MCP 編排率 < 5% 但 calls 多 → 建議擴大 MCP 使用
if mcp_calls_table_exists and ag['mcp_rate'] < 5 and ag['calls'] > 50:
@@ -1826,11 +1826,11 @@ def budget_dashboard():
@admin_observability_bp.route('/ai_calls/trigger_code_review', methods=['POST'])
@login_required
def ai_calls_trigger_code_review():
"""Phase 40 D-7 (L2 自動化):對高錯誤率時段觸發 Code Review Pipeline
"""Phase 40 D-7 (L2 自動化):對高錯誤率時段觸發程式碼審查流程
用途admin 在觀測台看到某 caller 錯誤率飆高時,一鍵觸發 5-step
pipeline (read→hermes_scan→openclaw_summary→ea_decision→nemoton_act)
在 daemon thread 自動審查最近 commit 變更檔案,找出可能的 regression
在 daemon thread 自動審查最近 commit 變更檔案,找出可能的回歸問題
"""
try:
import subprocess
@@ -1862,8 +1862,8 @@ def ai_calls_trigger_code_review():
'pipeline_id': pipeline.pipeline_id,
'commit_sha': commit_sha[:8],
'changed_files_count': len(changed),
'message': f'已觸發 Code Review (pipeline_id={pipeline.pipeline_id}) 在背景執行,'
f'5 step 完成後會推 Telegram 通知。',
'message': f'已觸發程式碼審查流程(流程編號:{pipeline.pipeline_id}在背景執行,'
f'5 個步驟完成後會推 Telegram 通知。',
})
except Exception as e:
return jsonify({'ok': False, 'error': f'{type(e).__name__}: {str(e)[:200]}'}), 500

View File

@@ -1,6 +1,6 @@
# =============================================================================
# WOOO TECH - Momo Pro System
# CI/CD Dashboard Routes
# 部署監控路由
# =============================================================================
from flask import Blueprint, jsonify, render_template, request
@@ -119,12 +119,12 @@ ENVIRONMENTS = {
}
# =============================================================================
# Dashboard 頁面
# 部署監控頁面
# =============================================================================
@cicd_bp.route('/cicd')
def cicd_dashboard():
"""CI/CD Dashboard 主頁面"""
"""部署監控主頁面"""
return render_template('cicd_dashboard.html', active_page='cicd')
# =============================================================================
@@ -140,14 +140,14 @@ def get_cicd_status():
latest_pipeline = pipelines[0] if pipelines else None
environments = get_all_environments_status()
# 取得最新 Pipeline 的詳細 Job 信息
# 取得最新部署流程的詳細工作項目資訊
latest_jobs = []
failed_jobs = []
if latest_pipeline:
latest_jobs = get_pipeline_jobs(latest_pipeline['id'])
failed_jobs = [j for j in latest_jobs if j.get('status') == 'failed']
# 如果最新 Pipeline 失敗且有未通知的失敗 Job,發送告警
# 如果最新部署流程失敗且有未通知的失敗工作項目,發送告警
if latest_pipeline.get('status') == 'failed' and failed_jobs:
# 使用緩存避免重複通知
cache_key = f"pipeline_alert_{latest_pipeline['id']}"
@@ -453,7 +453,7 @@ def run_diagnosis(env):
# EwoooC 已撤除舊叢集 runtime這裡只保留現行 Docker Compose 狀態說明。
diagnosis['checks'].append({
'name': 'Runtime 狀態',
'name': '執行環境狀態',
'status': 'ok',
'runtime': 'Docker Compose on 192.168.0.188',
'details': '舊叢集探測已停用;容器狀態請依 DevOps 手冊在 188 查 docker compose / /health。'
@@ -554,19 +554,19 @@ def send_telegram_message(message):
def send_pipeline_failure_alert(pipeline, failed_jobs):
"""Pipeline 失敗時發送告警"""
"""部署流程失敗時發送告警"""
job_details = '\n'.join([
f"{j['name']}: {j.get('failure_reason', '未知原因')}"
for j in failed_jobs
])
message = f"""🚨 *CI/CD Pipeline 失敗*
message = f"""🚨 *部署流程失敗*
📌 *Pipeline:* #{pipeline.get('id')}
📌 *部署編號:* #{pipeline.get('id')}
🌿 *分支:* `{pipeline.get('ref')}`
📝 *Commit:* `{pipeline.get('sha', '')[:8]}`
📝 *提交:* `{pipeline.get('sha', '')[:8]}`
❌ *失敗 Jobs:*
❌ *失敗工作項目:*
{job_details}
🔗 [查看詳情]({pipeline.get('web_url')})
@@ -589,7 +589,7 @@ def send_fix_notification(env, action, results):
for r in results
])
message = f"""{status_emoji} *CI/CD 自動修復執行完成*
message = f"""{status_emoji} *部署監控自動修復執行完成*
{env_icon} *環境:* {env_name}
🔧 *動作:* {action}

View File

@@ -8601,7 +8601,7 @@ def handle_cmd(cmd, arg, chat_id, reply_to):
send_message(chat_id, f"❌ 查詢商業面失敗:{e}", reply_to, parse_mode=None)
elif cmd == 'obs_trigger_review':
# Phase 44 (L2)Telegram inline 觸發 Code Review Pipeline
# Phase 44 (L2)Telegram inline 觸發程式碼審查流程
try:
import subprocess
import threading
@@ -8616,7 +8616,7 @@ def handle_cmd(cmd, arg, chat_id, reply_to):
).decode().strip().split('\n')
changed = [f for f in changed if f]
if not changed:
send_message(chat_id, "⚠️ 最新 commit 無變更檔案,無需 Code Review", reply_to, parse_mode=None)
send_message(chat_id, "⚠️ 最新提交沒有變更檔案,無需程式碼審查", reply_to, parse_mode=None)
return
pipeline = CodeReviewPipeline(
@@ -8627,15 +8627,15 @@ def handle_cmd(cmd, arg, chat_id, reply_to):
)
threading.Thread(target=pipeline.run, daemon=True).start()
ack = (
f"🔬 Code Review Pipeline 已派出\n\n"
f"Pipeline ID: {pipeline.pipeline_id}\n"
f"Commit: {commit_sha[:8]}\n"
f"🔬 程式碼審查流程已派出\n\n"
f"流程編號: {pipeline.pipeline_id}\n"
f"提交: {commit_sha[:8]}\n"
f"變更檔案: {len(changed)}\n\n"
f"5 step 完成後會推 Telegram 通知。"
f"5 個步驟完成後會推 Telegram 通知。"
)
send_message(chat_id, ack, reply_to, parse_mode=None)
except Exception as e:
send_message(chat_id, f"Code Review 觸發失敗:{e}", reply_to, parse_mode=None)
send_message(chat_id, f"程式碼審查觸發失敗:{e}", reply_to, parse_mode=None)
elif cmd == 'obs_force_throttle':
# Phase 41 E-3 (L2)Telegram inline 觸發立即重算 cost throttle

View File

@@ -84,7 +84,7 @@
<div>
<div class="host-lane-top">
<span class="host-name">{{ h.label }}</span>
{% if h.healthy %}<span class="badge bg-success">Runtime 正常</span>{% else %}<span class="badge bg-danger">異常</span>{% endif %}
{% if h.healthy %}<span class="badge bg-success">執行環境正常</span>{% else %}<span class="badge bg-danger">異常</span>{% endif %}
</div>
<div class="host-url"><code>{{ h.host }}</code></div>
{% if h.error %}<div class="text-danger small mt-1">{{ h.error }}</div>{% endif %}

View File

@@ -109,7 +109,7 @@
<section class="ppt-vision-status is-{{ vision_audit_status.status }}" data-ppt-vision-status aria-live="polite">
<div class="ppt-vision-status-main">
<span class="ppt-run-status is-{{ 'ready' if vision_audit_status.status == 'completed' else 'planned' if vision_audit_status.status in ['idle', 'queued', 'running'] else 'error' }}">
<i class="fas fa-eye me-1" aria-hidden="true"></i>Vision QA
<i class="fas fa-eye me-1" aria-hidden="true"></i>視覺檢查
</span>
<div>
<strong data-ppt-vision-status-title>{{ vision_audit_status.status_label }}</strong>
@@ -118,7 +118,7 @@
</div>
<div class="ppt-vision-status-list" data-ppt-vision-status-list>
<div class="ppt-vision-job is-runtime">
<span>Runtime</span>
<span>執行環境</span>
<strong>{{ vision_status.status_label }}</strong>
<small>{{ vision_status.model }} · {{ vision_status.converter or '無轉檔器' }}</small>
</div>

View File

@@ -1,6 +1,6 @@
{% extends "ewoooc_base.html" %}
{% block title %}CI/CD Dashboard - EwoooC{% endblock %}
{% block title %}部署監控 - EwoooC{% endblock %}
{% block extra_head %}
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" rel="stylesheet">
@@ -473,8 +473,8 @@
<div class="container">
<div class="d-flex justify-content-between align-items-center">
<div>
<h1><i class="bi bi-rocket-takeoff me-2"></i>CI/CD Dashboard</h1>
<p class="mb-0 opacity-75">MOMO Pro System - 持續整合與部署監控</p>
<h1><i class="bi bi-rocket-takeoff me-2"></i>部署監控</h1>
<p class="mb-0 opacity-75">PChome 業績成長自動化作戰系統 · 持續整合與部署監控</p>
</div>
<div class="d-flex align-items-center gap-3">
<div class="realtime-badge">
@@ -527,7 +527,7 @@
<!-- Pipeline Flow -->
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="bi bi-diagram-3 me-2"></i>最新 Pipeline</span>
<span><i class="bi bi-diagram-3 me-2"></i>最新部署流程</span>
<span id="pipelineId" class="badge bg-secondary">#--</span>
</div>
<div class="card-body">
@@ -589,7 +589,7 @@
<div class="row g-3">
<div class="col-12">
<a href="http://192.168.0.110:8929/root/momo-pro-system/-/pipelines" target="_blank" class="btn btn-outline-light w-100">
<i class="bi bi-box-arrow-up-right me-2"></i>開啟 GitLab Pipelines
<i class="bi bi-box-arrow-up-right me-2"></i>開啟 GitLab 部署紀錄
</a>
</div>
<div class="col-6">
@@ -716,7 +716,7 @@
<div class="issue-content">
<div class="issue-title">${escapeHtml(issue.message)}</div>
<div class="issue-detail">
${issue.type === 'job' ? `<span class="badge bg-secondary me-1">${issue.stage}</span>` : ''}
${issue.type === 'job' ? `<span class="badge bg-secondary me-1">${displayStageName(issue.stage)}</span>` : ''}
${issue.type === 'runtime' ? `<span class="badge bg-info me-1">${issue.environment?.toUpperCase()}</span>` : ''}
${issue.error ? `<br><code>${escapeHtml(issue.error.substring(0, 100))}</code>` : ''}
</div>
@@ -758,13 +758,13 @@
}
}
// 更新 Pipeline 流程圖
// 更新部署流程圖
function updatePipelineFlow(latestPipeline, latestJobs) {
const container = document.getElementById('pipelineFlow');
const pipelineIdEl = document.getElementById('pipelineId');
if (!latestPipeline) {
container.innerHTML = '<p class="text-center text-muted">暫無 Pipeline 數據</p>';
container.innerHTML = '<p class="text-center text-muted">暫無部署流程資料</p>';
return;
}
@@ -775,7 +775,7 @@
const stages = groupJobsByStage(latestJobs);
renderPipelineFlowWithJobs(container, stages);
} else {
// 否則取得 Pipeline 的 Jobs
// 否則取得部署流程的工作項目
fetch(`/api/cicd/pipeline/${latestPipeline.id}`)
.then(res => res.json())
.then(data => {
@@ -784,13 +784,13 @@
}
})
.catch(err => {
console.error('Failed to load pipeline details:', err);
console.error('部署流程細節讀取失敗:', err);
renderSimplePipelineFlow(container, latestPipeline);
});
}
}
// 將 Jobs 按 Stage 分組
// 將工作項目按階段分組
function groupJobsByStage(jobs) {
const stageOrder = ['test', 'build', 'deploy'];
const stages = {};
@@ -828,7 +828,7 @@
return stageOrder.map(name => stages[name]).filter(s => s);
}
// 渲染 Pipeline 流程圖(帶 Job 詳情)
// 渲染部署流程圖(帶工作詳情)
function renderPipelineFlowWithJobs(container, stages) {
let html = '';
@@ -844,7 +844,7 @@
<div class="pipeline-stage">
<div class="stage-box ${stage.status}" title="${escapeHtml(errorMsg)}">
<span class="stage-icon">${getStatusIcon(stage.status)}</span>
<span class="stage-name">${stage.name}</span>
<span class="stage-name">${displayStageName(stage.name)}</span>
</div>
<span class="stage-duration">${formatDuration(stage.duration)}</span>
${stage.status === 'failed' ? `
@@ -859,7 +859,7 @@
container.innerHTML = html;
}
// 渲染 Pipeline 流程圖
// 渲染部署流程圖
function renderPipelineFlow(container, stages) {
let html = '';
@@ -872,7 +872,7 @@
<div class="pipeline-stage">
<div class="stage-box ${stage.status}">
<span class="stage-icon">${stage.status_icon}</span>
<span class="stage-name">${stage.name}</span>
<span class="stage-name">${displayStageName(stage.name)}</span>
</div>
<span class="stage-duration">${formatDuration(stage.duration)}</span>
</div>
@@ -882,7 +882,7 @@
container.innerHTML = html;
}
// 簡易 Pipeline 流程圖
// 簡易部署流程圖
function renderSimplePipelineFlow(container, pipeline) {
const stages = ['test', 'build', 'deploy'];
let html = '';
@@ -901,7 +901,7 @@
<div class="pipeline-stage">
<div class="stage-box ${status}">
<span class="stage-icon">${getStatusIcon(status)}</span>
<span class="stage-name">${stage}</span>
<span class="stage-name">${displayStageName(stage)}</span>
</div>
</div>
`;
@@ -949,7 +949,7 @@
` : ''}
<div class="env-details mt-2">
<strong class="d-block mb-2">Runtime 狀態:</strong>
<strong class="d-block mb-2">執行環境狀態:</strong>
${renderPods(env.pods, envId)}
</div>
</div>
@@ -963,7 +963,7 @@
// 渲染 runtime 狀態
function renderPods(pods, envId) {
if (!pods || pods.length === 0) {
return '<p class="text-muted small mb-0">Docker Compose runtime;舊叢集資訊不適用</p>';
return '<p class="text-muted small mb-0">Docker Compose 執行環境;舊叢集資訊不適用</p>';
}
return pods.map(pod => `
@@ -978,12 +978,12 @@
`).join('');
}
// 更新 Pipeline 歷史
// 更新部署歷史
function updatePipelineHistory(pipelines) {
const container = document.getElementById('pipelineHistory');
if (!pipelines || pipelines.length === 0) {
container.innerHTML = '<p class="text-center text-muted">暫無 Pipeline 記錄</p>';
container.innerHTML = '<p class="text-center text-muted">暫無部署流程紀錄</p>';
return;
}
@@ -1106,7 +1106,7 @@
return;
}
if (!confirm(`確定要對 ${env.toUpperCase()} 執行完整修復嗎?\n這會重啟 Registry 並執行 runtime 診斷,不會重啟舊叢集。`)) return;
if (!confirm(`確定要對 ${env.toUpperCase()} 執行完整修復嗎?\n這會重啟 Registry 並執行執行環境診斷,不會重啟舊叢集。`)) return;
showNotification('執行中', '正在執行完整修復...');
@@ -1151,12 +1151,12 @@
if (data.success) {
const diagnosis = data.diagnosis;
let message = `診斷結果: ${diagnosis.summary?.overall_status?.toUpperCase()}\n`;
let message = `診斷結果: ${displayStatusText(diagnosis.summary?.overall_status)}\n`;
message += `失敗: ${diagnosis.summary?.failed_count || 0}, 警告: ${diagnosis.summary?.warning_count || 0}\n\n`;
diagnosis.checks?.forEach(check => {
const icon = check.status === 'ok' ? '✅' : (check.status === 'warning' ? '⚠️' : '❌');
message += `${icon} ${check.name}: ${check.status}\n`;
message += `${icon} ${check.name}: ${displayStatusText(check.status)}\n`;
});
if (diagnosis.summary?.recommendations?.length > 0) {
@@ -1184,6 +1184,30 @@
}
// 輔助函數
function displayStageName(stage) {
const labels = {
test: '測試',
build: '建置',
deploy: '部署',
unknown: '未分類',
};
return labels[stage] || stage || '未分類';
}
function displayStatusText(status) {
const labels = {
ok: '正常',
warning: '注意',
failed: '失敗',
success: '成功',
running: '執行中',
pending: '等待中',
canceled: '已取消',
completed: '已完成',
};
return labels[status] || status || '未知';
}
function getStatusIcon(status) {
const icons = {
'success': '✅', 'passed': '✅',

View File

@@ -990,7 +990,7 @@ def test_ppt_audit_history_renders_last_vision_status(client, monkeypatch):
assert 'data-ppt-vision-status-title' in html
assert '最近一次視覺 QA 已完成。' in html
assert '2 份 / 1 問題' in html
assert 'Runtime' in html
assert '執行環境' in html
def test_ppt_audit_trigger_aider_heal_accepts_issue_summary(client, monkeypatch):

View File

@@ -79,7 +79,7 @@ def test_cicd_diagnosis_uses_runtime_note_without_cluster_probe(monkeypatch):
diagnosis = cicd_routes.run_diagnosis("uat")
runtime_checks = [check for check in diagnosis["checks"] if check["name"] == "Runtime 狀態"]
runtime_checks = [check for check in diagnosis["checks"] if check["name"] == "執行環境狀態"]
assert runtime_checks
assert runtime_checks[0]["runtime"] == "Docker Compose on 192.168.0.188"

View File

@@ -55,8 +55,10 @@ def test_high_visibility_pages_use_traditional_chinese_labels():
"templates/code_review.html",
"templates/ai_automation_smoke.html",
"templates/admin/ppt_audit_history.html",
"templates/admin/host_health.html",
"templates/dashboard_v2.html",
"templates/components/_navbar.html",
"templates/cicd_dashboard.html",
]
combined = "\n".join((ROOT / path).read_text(encoding="utf-8") for path in page_paths)
@@ -69,6 +71,10 @@ def test_high_visibility_pages_use_traditional_chinese_labels():
assert "覆蓋率流程" in combined
assert "NemoTron · 派遣器" in combined
assert "同步部署" in combined
assert "部署監控" in combined
assert "最新部署流程" in combined
assert "執行環境正常" in combined
assert "視覺檢查" in combined
forbidden_visible_text = [
"AI Code Review",
@@ -93,6 +99,15 @@ def test_high_visibility_pages_use_traditional_chinese_labels():
"<b>Branch</b>",
"${f.severity}</span>",
"ea.priority.toUpperCase",
"CI/CD Dashboard",
">最新 Pipeline<",
"暫無 Pipeline",
"開啟 GitLab Pipelines",
">Runtime 狀態:<",
"Docker Compose runtime",
"Runtime 正常",
">Runtime<",
"Vision QA",
]
for marker in forbidden_visible_text:
assert marker not in combined