fix: make pchome source exports operator friendly
All checks were successful
CD Pipeline / deploy (push) Successful in 1m1s
All checks were successful
CD Pipeline / deploy (push) Successful in 1m1s
This commit is contained in:
@@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
|
||||
# ==========================================
|
||||
# 系統版本與路徑
|
||||
# ==========================================
|
||||
SYSTEM_VERSION = "V10.718"
|
||||
SYSTEM_VERSION = "V10.719"
|
||||
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
|
||||
public_url = PUBLIC_URL # 用於模板顯示
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
> **最後更新**: 2026-06-26 (台北時間)
|
||||
> **狀態**: 🟢 四 AI Agent 自動化閉環已落地;LLM 路由紅線升級為 Ollama-first 三主機級聯;PChome 後台業績匯入韌性已補強;產品定位正名為「PChome 業績成長自動化作戰系統」;外部市場來源正規化層、自動同步、作戰清單與價格參考表優先讀取、CSV 備援預檢、前台操作入口、高可見頁面繁中化守門、比價/作戰 UI 工作台化、跨平台來源治理與商品身份 UI 契約已建立,GCP embedding 熔斷延後處理、110 proxy rescue 與 direct host health skip 已建立
|
||||
> **適用版本**: V10.718
|
||||
> **適用版本**: V10.719
|
||||
|
||||
---
|
||||
|
||||
@@ -803,3 +803,4 @@ POSTGRES_HOST=momo-db
|
||||
| 2026-06-26 | 首頁覆核候選必須標示為待確認商品 | V10.716 起首頁 PChome 覆核區不再使用「PChome 候選」作為可見主語,統一改為「PChome 待確認商品」與「開 PChome 待確認商品」,避免使用者把未確認同款誤認為正式比價結果。 |
|
||||
| 2026-06-26 | 供貨風險頁不得使用資料表或英文模組名作為主語 | V10.717 起缺貨清單與補貨通知頁統一使用「供貨風險、缺貨處理清單、補貨通知紀錄」等營運語言,不再顯示「缺貨資料表、缺貨資料、Vendor Stockout」等資料庫或英文模組感文案。 |
|
||||
| 2026-06-26 | AI 觀測頁不得外露 caller key | V10.718 起 AI 品質診斷與知識召回頁使用「使用情境」作為可見主語,並透過 `obs_label.caller()` 顯示營運名稱;前台不得直接顯示 `<code>{{ caller }}</code>`、`top_k` 或「全部呼叫端」等工程語言。 |
|
||||
| 2026-06-26 | 商品來源頁不得提供 raw JSON 匯出 | V10.719 起 `/pchome_crawler` 改為「PChome 商品監控」營運清單,只提供表格與賣場清單 CSV;前台不得出現 `exportJson`、`JSON.stringify(currentProducts)`、`圖片URL`、`商品URL` 或 raw JSON 檔名。 |
|
||||
|
||||
@@ -78,6 +78,31 @@
|
||||
color: var(--momo-tag-muted-text);
|
||||
}
|
||||
|
||||
.pchome-product-title {
|
||||
color: var(--momo-text-primary);
|
||||
font-weight: 800;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.pchome-product-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-top: 6px;
|
||||
color: var(--momo-text-secondary);
|
||||
font-size: var(--momo-text-small);
|
||||
}
|
||||
|
||||
.pchome-product-meta span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 22px;
|
||||
padding: 2px 7px;
|
||||
border: 1px solid var(--momo-border-light);
|
||||
border-radius: var(--momo-radius-sm);
|
||||
background: var(--momo-bg-paper);
|
||||
}
|
||||
|
||||
.pchome-toast {
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
@@ -127,7 +152,7 @@
|
||||
<div class="container-fluid py-4 pchome-tool-page">
|
||||
<header class="pchome-tool-head mb-4">
|
||||
<h2><i class="fas fa-magnifying-glass-chart me-2"></i>PChome 24h 商品監控</h2>
|
||||
<p class="text-muted">補齊 PChome 商品資料,支援同款與價差判斷。</p>
|
||||
<p class="text-muted">補齊 PChome 商品、售價、庫存與賣場連結,支援同款確認、價差與促銷監控。</p>
|
||||
</header>
|
||||
|
||||
<!-- 資料取得方式選擇 -->
|
||||
@@ -237,8 +262,8 @@
|
||||
<button class="btn btn-sm btn-outline-primary me-2" id="exportExcelBtn">
|
||||
<i class="fas fa-file-excel me-1"></i>下載表格
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-secondary" id="exportJsonBtn">
|
||||
<i class="fas fa-file-alt me-1"></i>下載完整清單
|
||||
<button class="btn btn-sm btn-outline-secondary" id="exportStoreLinksBtn">
|
||||
<i class="fas fa-link me-1"></i>下載賣場清單
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -343,7 +368,7 @@
|
||||
|
||||
// 匯出
|
||||
document.getElementById('exportExcelBtn').addEventListener('click', exportExcel);
|
||||
document.getElementById('exportJsonBtn').addEventListener('click', exportJson);
|
||||
document.getElementById('exportStoreLinksBtn').addEventListener('click', exportStoreLinks);
|
||||
}
|
||||
|
||||
async function crawlRegion() {
|
||||
@@ -465,33 +490,47 @@
|
||||
tbody.innerHTML = '';
|
||||
|
||||
for (const p of products) {
|
||||
const discount = p.discount ? `<span class="pchome-badge is-danger">-${p.discount}%</span>` : '-';
|
||||
const stockBadge = p.stock > 0
|
||||
? `<span class="pchome-badge is-success">${p.stock}</span>`
|
||||
const productName = String(p.name || 'PChome 商品');
|
||||
const productId = String(p.product_id || '').trim();
|
||||
const productUrl = String(p.product_url || '').trim();
|
||||
const imageUrl = String(p.image_url || '').trim();
|
||||
const price = toNumber(p.price);
|
||||
const originalPrice = toNumber(p.original_price);
|
||||
const discountValue = toNumber(p.discount);
|
||||
const stock = toNumber(p.stock);
|
||||
const discount = discountValue > 0 ? `<span class="pchome-badge is-danger">-${discountValue}%</span>` : '-';
|
||||
const stockBadge = stock > 0
|
||||
? `<span class="pchome-badge is-success">${stock}</span>`
|
||||
: `<span class="pchome-badge is-muted">缺貨</span>`;
|
||||
const imageSrc = imageUrl ? `${escapeHtml(imageUrl)}?width=80` : '/static/images/no-image.png';
|
||||
const productTitle = `${escapeHtml(productName.substring(0, 50))}${productName.length > 50 ? '...' : ''}`;
|
||||
const productLink = productUrl
|
||||
? `<a href="${escapeHtml(productUrl)}" target="_blank" rel="noopener noreferrer" class="text-decoration-none pchome-product-title">${productTitle}</a>`
|
||||
: `<span class="pchome-product-title">${productTitle}</span>`;
|
||||
const storeAction = productUrl
|
||||
? `<a href="${escapeHtml(productUrl)}" target="_blank" rel="noopener noreferrer" class="btn btn-sm btn-outline-primary"><i class="fas fa-up-right-from-square me-1"></i>賣場</a>`
|
||||
: `<button type="button" class="btn btn-sm btn-outline-secondary" disabled>待補</button>`;
|
||||
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `
|
||||
<td>
|
||||
<img src="${p.image_url}?width=80" alt="" class="img-thumbnail"
|
||||
<img src="${imageSrc}" alt="${escapeHtml(productName)}" class="img-thumbnail"
|
||||
style="width: 60px; height: 60px; object-fit: cover;"
|
||||
onerror="this.src='/static/images/no-image.png'">
|
||||
</td>
|
||||
<td>
|
||||
<a href="${p.product_url}" target="_blank" class="text-decoration-none">
|
||||
${escapeHtml(p.name.substring(0, 50))}${p.name.length > 50 ? '...' : ''}
|
||||
</a>
|
||||
<br>
|
||||
<small class="text-muted">${p.product_id}</small>
|
||||
${productLink}
|
||||
<div class="pchome-product-meta">
|
||||
<span>商品編號 ${escapeHtml(productId || '待補')}</span>
|
||||
<span>PChome 24h 官方賣場</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-danger fw-bold">$${p.price.toLocaleString()}</td>
|
||||
<td class="text-muted"><s>$${p.original_price.toLocaleString()}</s></td>
|
||||
<td class="text-danger fw-bold">${formatMoney(price)}</td>
|
||||
<td class="text-muted">${originalPrice ? `<s>${formatMoney(originalPrice)}</s>` : '-'}</td>
|
||||
<td>${discount}</td>
|
||||
<td>${stockBadge}</td>
|
||||
<td>
|
||||
<a href="${p.product_url}" target="_blank" class="btn btn-sm btn-outline-primary">
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
</a>
|
||||
${storeAction}
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
@@ -504,31 +543,36 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// 建立 CSV
|
||||
const headers = ['商品ID', '名稱', '售價', '原價', '折扣%', '庫存', '圖片URL', '商品URL'];
|
||||
const headers = ['商品編號', '商品名稱', 'PChome 售價', 'PChome 原價', '折扣', '庫存狀態', '商品圖片', 'PChome 賣場'];
|
||||
const rows = currentProducts.map(p => [
|
||||
p.product_id,
|
||||
`"${p.name.replace(/"/g, '""')}"`,
|
||||
p.price,
|
||||
p.original_price,
|
||||
csvCell(p.product_id || ''),
|
||||
csvCell(p.name || ''),
|
||||
toNumber(p.price),
|
||||
toNumber(p.original_price),
|
||||
p.discount || '',
|
||||
p.stock,
|
||||
p.image_url,
|
||||
p.product_url
|
||||
toNumber(p.stock),
|
||||
csvCell(p.image_url || ''),
|
||||
csvCell(p.product_url || '')
|
||||
]);
|
||||
|
||||
const csv = '\uFEFF' + [headers.join(','), ...rows.map(r => r.join(','))].join('\n');
|
||||
downloadFile(csv, 'pchome_products.csv', 'text/csv;charset=utf-8');
|
||||
}
|
||||
|
||||
function exportJson() {
|
||||
function exportStoreLinks() {
|
||||
if (!currentProducts.length) {
|
||||
showToast('沒有資料可匯出', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const json = JSON.stringify(currentProducts, null, 2);
|
||||
downloadFile(json, 'pchome_products.json', 'application/json');
|
||||
const headers = ['商品編號', '商品名稱', 'PChome 賣場'];
|
||||
const rows = currentProducts.map(p => [
|
||||
csvCell(p.product_id || ''),
|
||||
csvCell(p.name || ''),
|
||||
csvCell(p.product_url || '')
|
||||
]);
|
||||
const csv = '\uFEFF' + [headers.join(','), ...rows.map(r => r.join(','))].join('\n');
|
||||
downloadFile(csv, 'pchome_store_links.csv', 'text/csv;charset=utf-8');
|
||||
}
|
||||
|
||||
function downloadFile(content, filename, type) {
|
||||
@@ -547,6 +591,19 @@
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function toNumber(value) {
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) ? number : 0;
|
||||
}
|
||||
|
||||
function formatMoney(value) {
|
||||
return `$${toNumber(value).toLocaleString()}`;
|
||||
}
|
||||
|
||||
function csvCell(value) {
|
||||
return `"${String(value).replace(/"/g, '""')}"`;
|
||||
}
|
||||
|
||||
function showToast(message, type = 'info') {
|
||||
// 簡易 Toast
|
||||
const toast = document.createElement('div');
|
||||
|
||||
@@ -197,8 +197,16 @@ def test_growth_workflow_pages_hide_raw_export_and_fallback_content():
|
||||
|
||||
assert "PChome 商品監控" in pchome_crawler
|
||||
assert "商品清單" in pchome_crawler
|
||||
assert "下載完整清單" in pchome_crawler
|
||||
assert "下載賣場清單" in pchome_crawler
|
||||
assert "商品編號" in pchome_crawler
|
||||
assert "PChome 24h 官方賣場" in pchome_crawler
|
||||
assert "匯出 JSON" not in pchome_crawler
|
||||
assert "下載完整清單" not in pchome_crawler
|
||||
assert "exportJson" not in pchome_crawler
|
||||
assert "JSON.stringify(currentProducts" not in pchome_crawler
|
||||
assert "pchome_products.json" not in pchome_crawler
|
||||
assert "圖片URL" not in pchome_crawler
|
||||
assert "商品URL" not in pchome_crawler
|
||||
assert "PChome 爬蟲" not in pchome_crawler
|
||||
assert "爬蟲" not in pchome_crawler
|
||||
|
||||
|
||||
Reference in New Issue
Block a user