fix: show product evidence on ai picks
Some checks failed
CD Pipeline / deploy (push) Has been cancelled
Some checks failed
CD Pipeline / deploy (push) Has been cancelled
This commit is contained in:
@@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
|
||||
# ==========================================
|
||||
# 系統版本與路徑
|
||||
# ==========================================
|
||||
SYSTEM_VERSION = "V10.723"
|
||||
SYSTEM_VERSION = "V10.724"
|
||||
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
|
||||
public_url = PUBLIC_URL # 用於模板顯示
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
> **最後更新**: 2026-06-27 (台北時間)
|
||||
> **狀態**: 🟢 四 AI Agent 自動化閉環已落地;LLM 路由紅線升級為 Ollama-first 三主機級聯;PChome 後台業績匯入韌性已補強;產品定位正名為「PChome 業績成長自動化作戰系統」;外部市場來源正規化層、自動同步、作戰清單與價格參考表優先讀取、CSV 備援預檢、前台操作入口、高可見頁面繁中化守門、比價/作戰 UI 工作台化、跨平台來源治理與商品身份 UI 契約已建立,GCP embedding 熔斷延後處理、110 proxy rescue 與 direct host health skip 已建立
|
||||
> **適用版本**: V10.723
|
||||
> **適用版本**: V10.724
|
||||
|
||||
---
|
||||
|
||||
@@ -808,3 +808,4 @@ POSTGRES_HOST=momo-db
|
||||
| 2026-06-27 | 監控來源 API 不回傳前台不需要的內部設定欄位 | V10.721 起 `/api/crawlers` 的設定頁 response 會移除 `function`、`page_type`、`status`、`paused_date`,只保留前台需要的名稱、說明、啟用狀態、頻率、活動代碼與營運提示。 |
|
||||
| 2026-06-27 | 服務更新監控頁不得以內部工具名當主語 | V10.722 起 `/cicd` 可見文字使用「測試站、正式站、監控圖表、自動化流程、今日更新」;前台模板不得用 `issue.error_log` 判斷顯示診斷資料,也不得顯示 `UAT 狀態`、`PROD 狀態`、`Grafana`、`n8n` 作為主按鈕文字。 |
|
||||
| 2026-06-27 | 系統事件頁不得顯示或下載原始工程紀錄 | V10.723 起 `/logs` 改為「系統事件紀錄」,前台只顯示事件等級、營運判讀與建議處置;不得以 raw log 變數、舊下載檔名或「系統日誌/下載日誌」作為使用者可見介面。 |
|
||||
| 2026-06-27 | AI 挑品必須直接顯示商品證據與賣場入口 | V10.724 起商品看板 AI 挑品清單會在 selection 階段合併最新 PChome 同款證據,卡片需顯示 MOMO 商品ID、PChome 商品ID、同款信心與兩邊賣場入口;商品圖需使用 PChome 圖片作為 fallback,不得只給一段建議理由。 |
|
||||
|
||||
@@ -853,7 +853,7 @@ def _load_competitor_review_page(
|
||||
return {'items': [], 'total': 0, 'page': page, 'per_page': per_page}
|
||||
|
||||
|
||||
def _parse_agent_footprint(value):
|
||||
def _parse_model_footprint(value):
|
||||
if not value:
|
||||
return {}
|
||||
if isinstance(value, str):
|
||||
@@ -863,16 +863,24 @@ def _parse_agent_footprint(value):
|
||||
return {}
|
||||
if not isinstance(value, dict):
|
||||
return {}
|
||||
return value
|
||||
|
||||
|
||||
def _parse_agent_footprint(value):
|
||||
value = _parse_model_footprint(value)
|
||||
agent = value.get('agent')
|
||||
return agent if isinstance(agent, dict) else {}
|
||||
|
||||
|
||||
def _ai_pick_evidence_fields(model_footprint):
|
||||
agent = _parse_agent_footprint(model_footprint)
|
||||
footprint = _parse_model_footprint(model_footprint)
|
||||
agent = footprint.get('agent') if isinstance(footprint.get('agent'), dict) else {}
|
||||
competitor = footprint.get('competitor') if isinstance(footprint.get('competitor'), dict) else {}
|
||||
missing_evidence = agent.get('missing_evidence') or []
|
||||
if isinstance(missing_evidence, str):
|
||||
missing_evidence = [missing_evidence]
|
||||
missing_evidence = [str(item) for item in missing_evidence if item]
|
||||
footprint_pchome_id = str(competitor.get('product_id') or '').strip()
|
||||
return {
|
||||
'opportunity_score': _to_float(agent.get('opportunity_score')) or 0,
|
||||
'evidence_quality': _to_float(agent.get('evidence_quality')) or 0,
|
||||
@@ -880,6 +888,10 @@ def _ai_pick_evidence_fields(model_footprint):
|
||||
'missing_evidence': missing_evidence,
|
||||
'missing_evidence_text': '、'.join(missing_evidence),
|
||||
'margin_rate': _to_float(agent.get('margin_rate')),
|
||||
'footprint_pchome_id': footprint_pchome_id,
|
||||
'footprint_pchome_name': str(competitor.get('product_name') or '').strip(),
|
||||
'footprint_pchome_url': _build_pchome_product_url(footprint_pchome_id) if footprint_pchome_id else '',
|
||||
'footprint_match_score': _to_float(competitor.get('match_score')),
|
||||
}
|
||||
|
||||
|
||||
@@ -1490,24 +1502,54 @@ def _load_pchome_growth_command_center(session):
|
||||
def _load_ai_pick_selection(session, limit=PRODUCT_PICK_LIST_LIMIT):
|
||||
"""讀取商品看板 AI 挑品清單排序,供列表篩選使用。"""
|
||||
sql = text("""
|
||||
WITH valid_competitor AS (
|
||||
SELECT DISTINCT ON (cp.sku)
|
||||
cp.sku,
|
||||
cp.competitor_product_id,
|
||||
cp.competitor_product_name,
|
||||
cp.competitor_product_url,
|
||||
cp.competitor_image_url,
|
||||
cp.match_score,
|
||||
cp.crawled_at
|
||||
FROM competitor_prices cp
|
||||
WHERE cp.source = 'pchome'
|
||||
AND (cp.expires_at IS NULL OR cp.expires_at > CURRENT_TIMESTAMP)
|
||||
AND cp.price IS NOT NULL
|
||||
AND cp.price > 0
|
||||
AND COALESCE(cp.match_score, 0) >= :match_score_floor
|
||||
AND COALESCE(cp.tags, '[]'::jsonb) ? 'identity_v2'
|
||||
ORDER BY cp.sku, cp.crawled_at DESC NULLS LAST
|
||||
)
|
||||
SELECT
|
||||
sku,
|
||||
confidence,
|
||||
reason,
|
||||
momo_price,
|
||||
pchome_price,
|
||||
gap_pct,
|
||||
model_footprint,
|
||||
created_at
|
||||
FROM ai_price_recommendations
|
||||
WHERE strategy = 'product_pick'
|
||||
AND status = 'pending'
|
||||
ORDER BY confidence DESC NULLS LAST, gap_pct DESC NULLS LAST, created_at DESC
|
||||
ar.sku,
|
||||
ar.confidence,
|
||||
ar.reason,
|
||||
ar.momo_price,
|
||||
ar.pchome_price,
|
||||
ar.gap_pct,
|
||||
ar.model_footprint,
|
||||
ar.created_at,
|
||||
p.url AS momo_url,
|
||||
vc.competitor_product_id,
|
||||
vc.competitor_product_name,
|
||||
vc.competitor_product_url,
|
||||
vc.competitor_image_url,
|
||||
vc.match_score,
|
||||
vc.crawled_at
|
||||
FROM ai_price_recommendations ar
|
||||
LEFT JOIN products p ON p.i_code = ar.sku
|
||||
LEFT JOIN valid_competitor vc ON vc.sku = ar.sku
|
||||
WHERE ar.strategy = 'product_pick'
|
||||
AND ar.status = 'pending'
|
||||
ORDER BY ar.confidence DESC NULLS LAST, ar.gap_pct DESC NULLS LAST, ar.created_at DESC
|
||||
LIMIT :limit
|
||||
""")
|
||||
|
||||
try:
|
||||
rows = session.execute(sql, {"limit": limit}).mappings().all()
|
||||
rows = session.execute(
|
||||
sql,
|
||||
{"limit": limit, "match_score_floor": PCHOME_MATCH_SCORE_FLOOR},
|
||||
).mappings().all()
|
||||
except Exception as exc:
|
||||
sys_log.warning(f"[Dashboard] AI 挑品清單讀取略過: {exc}")
|
||||
try:
|
||||
@@ -1522,6 +1564,13 @@ def _load_ai_pick_selection(session, limit=PRODUCT_PICK_LIST_LIMIT):
|
||||
sku = str(row.get('sku') or '')
|
||||
if not sku or sku in pick_map:
|
||||
continue
|
||||
evidence = _ai_pick_evidence_fields(row.get('model_footprint'))
|
||||
pchome_id = str(row.get('competitor_product_id') or evidence.get('footprint_pchome_id') or '').strip()
|
||||
pchome_url = (
|
||||
row.get('competitor_product_url')
|
||||
or evidence.get('footprint_pchome_url')
|
||||
or _build_pchome_product_url(pchome_id)
|
||||
)
|
||||
skus.append(sku)
|
||||
pick_map[sku] = {
|
||||
'rank': idx,
|
||||
@@ -1530,8 +1579,15 @@ def _load_ai_pick_selection(session, limit=PRODUCT_PICK_LIST_LIMIT):
|
||||
'momo_price': _to_float(row.get('momo_price')) or 0,
|
||||
'pchome_price': _to_float(row.get('pchome_price')) or 0,
|
||||
'gap_pct': _to_float(row.get('gap_pct')) or 0,
|
||||
'momo_url': normalize_momo_product_url(row.get('momo_url'), sku) or _build_momo_product_url(sku),
|
||||
'pchome_id': pchome_id,
|
||||
'pchome_name': row.get('competitor_product_name') or evidence.get('footprint_pchome_name') or '',
|
||||
'pchome_url': pchome_url,
|
||||
'pchome_image_url': row.get('competitor_image_url') or '',
|
||||
'pchome_match_score': _to_float(row.get('match_score')) or evidence.get('footprint_match_score'),
|
||||
'pchome_crawled_at': _format_dashboard_dt(row.get('crawled_at')),
|
||||
'created_at': _format_dashboard_dt(row.get('created_at')),
|
||||
**_ai_pick_evidence_fields(row.get('model_footprint')),
|
||||
**evidence,
|
||||
}
|
||||
return skus, pick_map
|
||||
|
||||
|
||||
@@ -665,7 +665,8 @@
|
||||
{% set competitor = item.pchome_competitor %}
|
||||
{% set decision = item.competitor_decision %}
|
||||
{% set match_status = item.pchome_match_status %}
|
||||
{% set image_url = product.image_url or ('https://m.momoshop.com.tw/moscdn/goods/' ~ product.i_code ~ '_m.webp') %}
|
||||
{% set pchome_fallback_image_url = competitor.image_url if competitor and competitor.image_url else (item.ai_pick.pchome_image_url if item.ai_pick and item.ai_pick.pchome_image_url else (item.pchome_match_attempt.competitor_image_url if item.pchome_match_attempt and item.pchome_match_attempt.competitor_image_url else '')) %}
|
||||
{% set image_url = product.image_url or pchome_fallback_image_url or ('https://m.momoshop.com.tw/moscdn/goods/' ~ product.i_code ~ '_m.webp') %}
|
||||
{% set safe_product_url = item.safe_momo_url or '#' %}
|
||||
{% if current_filter == 'pchome_review' %}
|
||||
{% set review = item.pchome_review %}
|
||||
@@ -677,7 +678,7 @@
|
||||
<div class="dashboard-review-product-stack">
|
||||
<div class="dashboard-product-cell">
|
||||
<span class="dashboard-product-thumb-frame">
|
||||
<img class="dashboard-product-thumb" src="{{ image_url }}" alt="{{ product.name }}" loading="lazy" referrerpolicy="no-referrer" onerror="this.hidden=true; this.nextElementSibling.hidden=false;">
|
||||
<img class="dashboard-product-thumb" src="{{ image_url }}" data-fallback-src="{{ pchome_fallback_image_url }}" alt="{{ product.name }}" loading="lazy" referrerpolicy="no-referrer" onerror="if (this.dataset.fallbackSrc && this.src !== this.dataset.fallbackSrc) { this.src = this.dataset.fallbackSrc; this.dataset.fallbackSrc = ''; } else { this.hidden=true; this.nextElementSibling.hidden=false; }">
|
||||
<span class="dashboard-product-thumb-missing" hidden aria-label="待補商品圖"><i class="fas fa-image" aria-hidden="true"></i></span>
|
||||
</span>
|
||||
<div>
|
||||
@@ -714,6 +715,7 @@
|
||||
<span>{{ review.candidate_count }} 筆候選</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<a class="dashboard-platform-link is-pchome" href="https://24h.pchome.com.tw/prod/{{ review.candidate_pc_id }}" target="_blank" rel="noopener noreferrer">開 PChome 待確認商品</a>
|
||||
{% elif competitor and competitor.product_id %}
|
||||
<a class="dashboard-review-candidate-title" href="{{ competitor.product_url or ('https://24h.pchome.com.tw/prod/' ~ competitor.product_id) }}" target="_blank" rel="noopener noreferrer">
|
||||
{{ competitor.product_name or ('PChome ' ~ competitor.product_id) }}
|
||||
@@ -837,7 +839,7 @@
|
||||
<td>
|
||||
<div class="dashboard-product-cell">
|
||||
<span class="dashboard-product-thumb-frame">
|
||||
<img class="dashboard-product-thumb" src="{{ image_url }}" alt="{{ product.name }}" loading="lazy" referrerpolicy="no-referrer" onerror="this.hidden=true; this.nextElementSibling.hidden=false;">
|
||||
<img class="dashboard-product-thumb" src="{{ image_url }}" data-fallback-src="{{ pchome_fallback_image_url }}" alt="{{ product.name }}" loading="lazy" referrerpolicy="no-referrer" onerror="if (this.dataset.fallbackSrc && this.src !== this.dataset.fallbackSrc) { this.src = this.dataset.fallbackSrc; this.dataset.fallbackSrc = ''; } else { this.hidden=true; this.nextElementSibling.hidden=false; }">
|
||||
<span class="dashboard-product-thumb-missing" hidden aria-label="待補商品圖"><i class="fas fa-image" aria-hidden="true"></i></span>
|
||||
</span>
|
||||
{% set safe_product_url = item.safe_momo_url or '#' %}
|
||||
@@ -949,11 +951,27 @@
|
||||
{% if current_filter == 'ai_picks' %}
|
||||
<td>
|
||||
{% if item.ai_pick %}
|
||||
{% set ai_momo_url = item.ai_pick.momo_url or safe_product_url or '#' %}
|
||||
{% set ai_pchome_id = item.ai_pick.pchome_id or (competitor.product_id if competitor else (item.pchome_match_attempt.best_competitor_product_id if item.pchome_match_attempt else '')) %}
|
||||
{% set ai_pchome_name = item.ai_pick.pchome_name or (competitor.product_name if competitor else (item.pchome_match_attempt.best_competitor_product_name if item.pchome_match_attempt else '')) %}
|
||||
{% set ai_pchome_url = item.ai_pick.pchome_url or (competitor.product_url if competitor else (item.pchome_match_attempt.competitor_product_url if item.pchome_match_attempt else '')) %}
|
||||
{% set ai_pchome_score = item.ai_pick.pchome_match_score or (competitor.match_score if competitor else (item.pchome_match_attempt.best_match_score if item.pchome_match_attempt else none)) %}
|
||||
<div class="dashboard-ai-pick-card">
|
||||
<div class="dashboard-ai-pick-head">
|
||||
<span class="dashboard-ai-pick-rank">#{{ item.ai_pick.rank }}</span>
|
||||
<span class="dashboard-ai-pick-confidence is-{{ item.ai_pick.confidence_band | replace('_', '-') }}">信心 {{ (item.ai_pick.confidence * 100) | round(0) | int }}%</span>
|
||||
</div>
|
||||
<div class="dashboard-ai-product-evidence" aria-label="AI 挑品商品證據">
|
||||
<span>MOMO 商品ID {{ product.i_code }}</span>
|
||||
{% if ai_pchome_id %}
|
||||
<span title="{{ ai_pchome_name or ai_pchome_id }}">PChome 商品ID {{ ai_pchome_id }}</span>
|
||||
{% else %}
|
||||
<span class="is-pending">PChome 商品ID 待補</span>
|
||||
{% endif %}
|
||||
{% if ai_pchome_score %}
|
||||
<span>同款信心 {{ (ai_pchome_score * 100) | round(0) | int }}%</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="dashboard-ai-evidence-line">
|
||||
<span>機會 {{ item.ai_pick.opportunity_score | round(0) | int }}</span>
|
||||
<span>證據 {{ item.ai_pick.evidence_quality | round(0) | int }}%</span>
|
||||
@@ -963,8 +981,8 @@
|
||||
</div>
|
||||
<div class="dashboard-ai-pick-reason">{{ item.ai_pick.reason }}</div>
|
||||
<div class="dashboard-ai-action-row" aria-label="商品賣場連結">
|
||||
<a class="dashboard-platform-link is-momo momo-tracked-link" href="{{ safe_product_url or '#' }}" target="_blank" rel="noopener noreferrer"
|
||||
data-momo-original-url="{{ safe_product_url or '#' }}"
|
||||
<a class="dashboard-platform-link is-momo momo-tracked-link" href="{{ ai_momo_url or '#' }}" target="_blank" rel="noopener noreferrer"
|
||||
data-momo-original-url="{{ ai_momo_url or '#' }}"
|
||||
data-track-platform="momo"
|
||||
data-track-source="dashboard-v2-ai-pick-card"
|
||||
data-track-product-id="{{ product.id }}"
|
||||
@@ -972,10 +990,12 @@
|
||||
data-track-product-name="{{ product.name|e }}">
|
||||
開 MOMO 賣場
|
||||
</a>
|
||||
{% if competitor and competitor.product_url %}
|
||||
<a class="dashboard-platform-link is-pchome" href="{{ competitor.product_url }}" target="_blank" rel="noopener noreferrer">開 PChome 賣場</a>
|
||||
{% elif item.pchome_match_attempt and item.pchome_match_attempt.competitor_product_url %}
|
||||
<a class="dashboard-platform-link is-pchome" href="{{ item.pchome_match_attempt.competitor_product_url }}" target="_blank" rel="noopener noreferrer">開 PChome 待確認商品</a>
|
||||
{% if ai_pchome_url %}
|
||||
<a class="dashboard-platform-link is-pchome" href="{{ ai_pchome_url }}" target="_blank" rel="noopener noreferrer">開 PChome 賣場</a>
|
||||
{% elif ai_pchome_id %}
|
||||
<a class="dashboard-platform-link is-pchome" href="https://24h.pchome.com.tw/prod/{{ ai_pchome_id }}" target="_blank" rel="noopener noreferrer">開 PChome 賣場</a>
|
||||
{% else %}
|
||||
<span class="dashboard-platform-muted">PChome 連結待補</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if item.ai_pick.missing_evidence %}
|
||||
|
||||
@@ -444,6 +444,12 @@ def test_dashboard_v2_is_production_default_and_uses_real_dashboard_data():
|
||||
assert "商品ID {{ product.i_code }}" in dashboard
|
||||
assert "開 MOMO 賣場" in dashboard
|
||||
assert "開 PChome 賣場" in dashboard
|
||||
assert "dashboard-ai-product-evidence" in dashboard
|
||||
assert "PChome 商品ID {{ ai_pchome_id }}" in dashboard
|
||||
assert "PChome 商品ID 待補" in dashboard
|
||||
assert "data-fallback-src" in dashboard
|
||||
assert "item.ai_pick.pchome_url" in dashboard
|
||||
assert ".dashboard-ai-product-evidence" in dashboard_css
|
||||
assert "is-ai-picks-wrap" in dashboard
|
||||
assert "dashboard-product-thumb-missing" in dashboard
|
||||
assert "dashboard-product-identity" in dashboard_css
|
||||
@@ -460,6 +466,10 @@ def test_dashboard_v2_is_production_default_and_uses_real_dashboard_data():
|
||||
assert "_summarize_ai_pick_selection(ai_pick_map)" in route_source
|
||||
assert "{{ ai_pick_list_limit }} 品" in dashboard
|
||||
assert "_load_ai_pick_selection(session, PRODUCT_PICK_LIST_LIMIT)" in route_source
|
||||
assert "WITH valid_competitor AS" in route_source
|
||||
assert "vc.competitor_product_url" in route_source
|
||||
assert "'pchome_id': pchome_id" in route_source
|
||||
assert "'pchome_url': pchome_url" in route_source
|
||||
assert "PRODUCT_PICK_LIST_LIMIT = 50" in route_source
|
||||
assert "ui='v2'" not in dashboard
|
||||
assert 'name="ui" value="v2"' not in dashboard
|
||||
|
||||
@@ -1691,6 +1691,36 @@
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.dashboard-ai-product-evidence {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.dashboard-ai-product-evidence span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
max-width: 210px;
|
||||
min-height: 22px;
|
||||
padding: 2px 7px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--momo-border-light);
|
||||
border-radius: var(--momo-radius-pill);
|
||||
background: var(--momo-bg-paper);
|
||||
color: var(--momo-text-secondary);
|
||||
font-family: var(--momo-font-family-mono);
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dashboard-ai-product-evidence span.is-pending {
|
||||
color: var(--momo-warning-text);
|
||||
background: var(--momo-warning-bg);
|
||||
border-color: rgba(161, 111, 35, 0.18);
|
||||
}
|
||||
|
||||
.dashboard-ai-evidence-chip {
|
||||
display: inline-flex;
|
||||
max-width: 180px;
|
||||
|
||||
Reference in New Issue
Block a user