@petercjl/taobao-ai-ops 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -22
- package/package.json +2 -7
- package/requirements.txt +1 -0
- package/skills/taobao-ai-operations/SKILL.md +8 -1
- package/src/cli.mjs +16 -6
- package/src/components.mjs +73 -0
- package/src/core.mjs +5 -1
- package/src/runtime.mjs +18 -16
- package/src/skill-manager.mjs +4 -4
- package/src/update.mjs +5 -5
- package/src/workflow.mjs +3 -3
- package/suite-manifest.json +186 -10
- package/skills/taobao-search-product-form/SKILL.md +0 -74
- package/skills/taobao-search-product-form/adapters/codex.json +0 -139
- package/skills/taobao-search-product-form/adapters/sealseek.json +0 -24
- package/skills/taobao-search-product-form/agents/openai.yaml +0 -3
- package/skills/taobao-search-product-form/capabilities.json +0 -148
- package/skills/taobao-search-product-form/references/SCHEMA.md +0 -3
- package/skills/taobao-search-product-form/references/index.md +0 -10
- package/skills/taobao-search-product-form/references/log.md +0 -35
- package/skills/taobao-search-product-form/references/queries/evaluate-white-label-forms.md +0 -3
- package/skills/taobao-search-product-form/references/queries/identify-advertisers.md +0 -5
- package/skills/taobao-search-product-form/references/queries/identify-geography.md +0 -5
- package/skills/taobao-search-product-form/references/queries/identify-price-bands.md +0 -3
- package/skills/taobao-search-product-form/references/queries/identify-product-forms.md +0 -3
- package/skills/taobao-search-product-form/references/queries/identify-title-roots.md +0 -5
- package/skills/taobao-search-product-form/references/report-output.md +0 -23
- package/skills/taobao-search-product-form/references/source-manifest.md +0 -37
- package/skills/taobao-search-product-form/references/title-lexicon.json +0 -55
- package/skills/taobao-search-product-form/references/topics/advertiser-analysis-method.md +0 -43
- package/skills/taobao-search-product-form/references/topics/geography-analysis-method.md +0 -23
- package/skills/taobao-search-product-form/references/topics/price-band-analysis-method.md +0 -27
- package/skills/taobao-search-product-form/references/topics/title-root-analysis-method.md +0 -37
- package/skills/taobao-search-product-form/references/topics/visual-form-method.md +0 -31
- package/skills/taobao-search-product-form/references/topics/white-label-evidence.md +0 -38
- package/skills/taobao-search-product-form/references/workflow-ledger.md +0 -28
- package/skills/taobao-search-product-form/report_runtime/VERSION +0 -1
- package/skills/taobao-search-product-form/report_runtime/renderer.py +0 -485
- package/skills/taobao-search-product-form/report_runtime/templates/workbench.html +0 -67
- package/skills/taobao-search-product-form/report_runtime/validator.py +0 -117
- package/skills/taobao-search-product-form/scripts/analyze.py +0 -119
- package/skills/taobao-search-product-form/scripts/analyze_advertisers.py +0 -352
- package/skills/taobao-search-product-form/scripts/analyze_geography.py +0 -316
- package/skills/taobao-search-product-form/scripts/analyze_price_bands.py +0 -257
- package/skills/taobao-search-product-form/scripts/analyze_title_roots.py +0 -327
- package/skills/taobao-search-product-form/scripts/assemble_opportunity_evidence.py +0 -322
- package/skills/taobao-search-product-form/scripts/build_report_viewmodel.py +0 -822
- package/skills/taobao-search-product-form/scripts/build_visual_review_viewmodel.py +0 -177
- package/skills/taobao-search-product-form/scripts/clean_search_export.mjs +0 -61
- package/skills/taobao-search-product-form/scripts/cluster_visual_forms.py +0 -193
- package/skills/taobao-search-product-form/scripts/compile_style_prototypes.py +0 -115
- package/skills/taobao-search-product-form/scripts/compile_visual_review.py +0 -60
- package/skills/taobao-search-product-form/scripts/compile_visual_selection.py +0 -148
- package/skills/taobao-search-product-form/scripts/prepare.py +0 -180
- package/skills/taobao-search-product-form/scripts/render_report.py +0 -31
- package/skills/taobao-search-product-form/scripts/validate_opportunity_cards.py +0 -84
- package/skills/taobao-search-product-form/scripts/validate_report_viewmodel.py +0 -152
- package/skills/taobao-search-product-form/scripts/visual_form_pipeline.py +0 -247
- package/skills/taobao-search-product-form/scripts/visual_observation_batches.py +0 -117
- package/skills/taobao-search-product-form/scripts/workflow_ledger.py +0 -177
- package/skills/yuce-category-opportunity-report/SKILL.md +0 -151
- package/skills/yuce-category-opportunity-report/adapters/codex.json +0 -113
- package/skills/yuce-category-opportunity-report/adapters/sealseek.json +0 -39
- package/skills/yuce-category-opportunity-report/agents/openai.yaml +0 -3
- package/skills/yuce-category-opportunity-report/assets/chart-views.js +0 -80
- package/skills/yuce-category-opportunity-report/assets/package.json +0 -1
- package/skills/yuce-category-opportunity-report/assets/report-template.html +0 -62
- package/skills/yuce-category-opportunity-report/capabilities.json +0 -48
- package/skills/yuce-category-opportunity-report/references/SCHEMA.md +0 -5
- package/skills/yuce-category-opportunity-report/references/data-contract.md +0 -40
- package/skills/yuce-category-opportunity-report/references/index.md +0 -6
- package/skills/yuce-category-opportunity-report/references/log.md +0 -29
- package/skills/yuce-category-opportunity-report/references/queries/assess-category-opportunity.md +0 -7
- package/skills/yuce-category-opportunity-report/references/source-manifest.md +0 -14
- package/skills/yuce-category-opportunity-report/references/topics/category-opportunity-method.md +0 -39
- package/skills/yuce-category-opportunity-report/scripts/build_report.py +0 -779
- package/skills/yuce-category-opportunity-report/scripts/market_routes.py +0 -139
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="zh-CN">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8">
|
|
5
|
-
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
-
<meta name="color-scheme" content="light">
|
|
7
|
-
<meta name="generator" content="commerce-ui/__CLI_VERSION__">
|
|
8
|
-
<meta name="commerce-ui-contract" content="__CONTRACT__">
|
|
9
|
-
<meta name="commerce-ui-external-images" content="__EXTERNAL_IMAGE_POLICY__">
|
|
10
|
-
<link rel="icon" href="data:,">
|
|
11
|
-
<title>__TITLE__</title>
|
|
12
|
-
<style>
|
|
13
|
-
:root{--primary:#315bd6;--primary-soft:#eaf0ff;--page:#f3f5f7;--panel:#fff;--text:#20242c;--muted:#697386;--line:#dfe4ea;--positive:#16845b;--positive-soft:#eaf8f2;--warning:#9a6500;--warning-soft:#fff7df;--danger:#bd3e3e;--danger-soft:#fff0f0;--info:#315bd6;--info-soft:#eaf0ff;--sidebar:236px;--topbar:68px;--radius:9px;font-family:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;font-size:15px;line-height:1.58;color:var(--text);background:var(--page);font-variant-numeric:tabular-nums}
|
|
14
|
-
*{box-sizing:border-box}html{scroll-behavior:auto}body{margin:0;background:var(--page)}button,input{font:inherit}button{cursor:pointer}.hidden,[hidden]{display:none!important}:focus-visible{outline:2px solid var(--primary);outline-offset:2px}
|
|
15
|
-
.sidebar{position:fixed;inset:0 auto 0 0;width:var(--sidebar);display:flex;flex-direction:column;background:#fafbfc;border-right:1px solid var(--line);z-index:30}.brand{height:var(--topbar);display:flex;align-items:center;gap:11px;padding:0 16px;border-bottom:1px solid var(--line)}.brandmark{width:36px;height:36px;border-radius:9px;display:grid;place-items:center;background:var(--primary);color:#fff;font-weight:800}.brand b{display:block;font-size:15px}.brand small{display:block;font-size:12px;color:var(--muted)}.nav{padding:16px 10px;display:block;overflow:auto}.nav-label{padding:0 9px 7px;color:#8a92a0;font-size:12px;font-weight:700}.nav-group+.nav-group{margin-top:4px}.nav button{width:100%;min-height:40px;border:0;border-radius:7px;padding:8px 10px;display:flex;align-items:center;gap:10px;background:transparent;color:#596274;text-align:left;font-size:13px}.nav button:hover{background:#eef2f7}.nav button.active{background:var(--primary-soft);color:var(--primary);font-weight:750}.nav-parent>span:nth-child(2){min-width:0;flex:1}.nav-dot{width:7px;height:7px;border-radius:2px;background:currentColor;flex:none}.nav-chevron{margin-left:auto;color:#9aa2af;transition:transform .15s}.nav-group.active .nav-chevron{transform:rotate(180deg)}.nav-children{display:none;margin:3px 0 6px 16px;padding-left:9px;border-left:1px solid #d8dee8}.nav-group.active .nav-children{display:grid;gap:2px}.nav .nav-child{min-height:34px;padding:6px 8px;font-size:12px}.nav-child>span:first-child{min-width:0;flex:1}.nav-child-badge{color:#8a92a0;font-size:12px;font-weight:600}.nav-child.active .nav-child-badge{color:inherit}.side-status{margin:auto 12px 16px;padding:13px 12px;border-top:1px solid var(--line);color:var(--muted);font-size:12px}.side-status div+div{margin-top:4px}.side-status div:first-child{color:var(--text);font-weight:700}
|
|
16
|
-
.app{min-height:100vh;margin-left:var(--sidebar)}.topbar{height:var(--topbar);position:sticky;top:0;z-index:20;padding:0 22px;display:flex;align-items:center;justify-content:space-between;gap:16px;background:rgba(255,255,255,.96);backdrop-filter:blur(8px);border-bottom:1px solid var(--line)}.identity b{display:block;font-size:15px}.identity span{display:block;color:var(--muted);font-size:12px}.tools{display:flex;align-items:center;gap:8px}.search{height:38px;min-width:240px;padding:0 12px;border:1px solid var(--line);border-radius:8px;background:#fafbfc;color:var(--text)}.date-chip{height:38px;display:flex;align-items:center;padding:0 12px;border:1px solid var(--line);border-radius:8px;background:#fff;color:#596274;font-size:13px;white-space:nowrap}.menu-toggle{display:none;width:38px;height:38px;border:1px solid var(--line);border-radius:8px;background:#fff}.reading-path{position:sticky;top:var(--actual-topbar,var(--topbar));z-index:19;min-height:48px;padding:8px 22px;display:flex;align-items:center;gap:13px;background:rgba(250,251,252,.97);backdrop-filter:blur(8px);border-bottom:1px solid var(--line)}.reading-path>b{flex:none;color:#596274;font-size:12px}.reading-items{min-width:0;display:flex;align-items:center;gap:8px;overflow-x:auto;scrollbar-width:thin}.reading-path button{flex:none;min-height:31px;padding:4px 9px;border:1px solid #d8dee8;border-radius:999px;background:#fff;color:#596274;font-size:12px;white-space:nowrap}.reading-path button.active{border-color:#bdcafa;background:var(--primary-soft);color:var(--primary);font-weight:700}.reading-step{display:inline-grid;place-items:center;width:18px;height:18px;margin-right:5px;border-radius:50%;background:#eef1f5;font-size:12px}.reading-path button.active .reading-step{background:#dbe5ff}.reading-arrow{color:#a4acb8;font-size:12px}
|
|
17
|
-
main{max-width:1420px;margin:0 auto;padding:22px}.view{outline:none}.page-heading{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:13px}.eyebrow{color:var(--primary);font-size:12px;font-weight:750;margin-bottom:3px}.page-heading h1{margin:0;font-size:25px;line-height:1.25}.page-heading p{margin:5px 0 0;color:var(--muted);font-size:13px;max-width:900px}.tag{display:inline-flex;align-items:center;min-height:27px;padding:3px 9px;border-radius:999px;font-size:12px;font-weight:700;white-space:nowrap}.tone-info{background:var(--info-soft);color:var(--info)}.tone-positive{background:var(--positive-soft);color:var(--positive)}.tone-warning{background:var(--warning-soft);color:var(--warning)}.tone-danger{background:var(--danger-soft);color:var(--danger)}
|
|
18
|
-
.alert{margin-bottom:10px;padding:11px 13px;border:1px solid #ecd08d;border-radius:var(--radius);background:var(--warning-soft);color:#72531a;font-size:13px}.alert strong{color:#57400f}.alert.tone-info{border-color:#cbd7f6}.alert.tone-positive{border-color:#a7dcc8}.alert.tone-danger{border-color:#efb8b8}.card{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);box-shadow:0 1px 2px rgba(25,34,49,.03);min-width:0}.card-head{min-height:49px;padding:0 13px;display:flex;align-items:center;justify-content:space-between;gap:12px;border-bottom:1px solid #edf0f3}.card-head h2{font-size:15px;margin:0}.card-head span{color:var(--muted);font-size:12px}.card-body{padding:13px}
|
|
19
|
-
.metrics{display:grid;gap:9px;margin-bottom:10px}.metrics-1{grid-template-columns:1fr}.metrics-2{grid-template-columns:repeat(2,minmax(0,1fr))}.metrics-3{grid-template-columns:repeat(3,minmax(0,1fr))}.metrics-4{grid-template-columns:repeat(4,minmax(0,1fr))}.metrics-5{grid-template-columns:repeat(5,minmax(0,1fr))}.metrics-6{grid-template-columns:repeat(6,minmax(0,1fr))}.metrics-7{grid-template-columns:repeat(7,minmax(0,1fr))}.metrics-8{grid-template-columns:repeat(8,minmax(0,1fr))}.metric{padding:13px;min-height:108px}.metric-label{display:flex;justify-content:space-between;gap:6px;color:var(--muted);font-size:12px}.metric-badge{font-size:12px;font-weight:700;background:transparent!important}.metric-value{margin:10px 0 5px;font-size:27px;font-weight:780;line-height:1.15}.metric-note{color:var(--muted);font-size:12px}.variant-funnel .metric{min-height:82px;border-top:3px solid var(--primary)}.variant-funnel .metric-value{font-size:22px;margin:7px 0 0}.module-grid{display:grid;gap:10px;margin-bottom:10px}.grid-1{grid-template-columns:1fr}.grid-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-4{grid-template-columns:repeat(4,minmax(0,1fr))}.content-list{margin:0;padding-left:21px;font-size:13px;color:#4f596a}.content-list li+li{margin-top:7px}.card>.card-body>.content-list{padding-left:20px}ol.content-list{list-style:none;counter-reset:rank;padding:0}ol.content-list li{counter-increment:rank;padding:10px 0;border-bottom:1px solid #edf0f3;margin:0}ol.content-list li:last-child{border-bottom:0}ol.content-list li:before{content:counter(rank);display:inline-block;min-width:20px;color:var(--primary);font-weight:800}.callout{padding:14px;border-left:4px solid var(--primary);background:#f6f8ff;border-radius:0 8px 8px 0;font-size:14px}.prose p{font-size:13px;margin:0}.prose p+p{margin-top:8px}.mini-stats{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:7px}.mini-stat{padding:8px;border-radius:7px;background:#f7f8fa}.mini-stat span{display:block;color:var(--muted);font-size:12px}.mini-stat b{font-size:17px}.text-button{border:0;background:transparent;color:var(--primary);padding:9px 0 0;font-size:13px;font-weight:700}.text-button.compact{padding-top:4px;font-size:12px}
|
|
20
|
-
.action-list{margin:-13px}.action-row{display:grid;grid-template-columns:28px 1fr auto;gap:9px;padding:12px 13px;border-bottom:1px solid #edf0f3}.action-row:last-child{border:0}.action-rank{width:28px;height:28px;border-radius:7px;background:var(--primary-soft);display:grid;place-items:center;color:var(--primary);font-weight:700;font-size:12px}.action-row b{font-size:13px}.action-row p{font-size:12px;color:var(--muted);margin:3px 0 0}
|
|
21
|
-
.conclusion-card{margin-bottom:10px}.conclusion-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:10px;padding:13px}.conclusion-item{min-width:0;padding:14px;border:1px solid var(--line);border-top:3px solid var(--info);border-radius:8px;background:#fff;overflow-wrap:anywhere}.conclusion-item.conclusion-tone-positive{border-top-color:var(--positive)}.conclusion-item.conclusion-tone-warning{border-top-color:var(--warning)}.conclusion-item.conclusion-tone-danger{border-top-color:var(--danger)}.conclusion-item h3{margin:0 0 9px;font-size:13px;font-weight:800;color:var(--info)}.conclusion-item.conclusion-tone-positive h3{color:var(--positive)}.conclusion-item.conclusion-tone-warning h3{color:var(--warning)}.conclusion-item.conclusion-tone-danger h3{color:var(--danger)}.conclusion-copy{margin:0;font-size:16px;line-height:1.55;color:var(--text)}.conclusion-segment.emphasis-strong,.conclusion-segment.emphasis-strong-italic{font-weight:800}.conclusion-segment.emphasis-italic,.conclusion-segment.emphasis-strong-italic{font-style:italic}.conclusion-color-info{color:var(--info)}.conclusion-color-positive{color:var(--positive)}.conclusion-color-warning{color:var(--warning)}.conclusion-color-danger{color:var(--danger)}.conclusion-note{margin-top:9px;color:var(--muted);font-size:12px;line-height:1.5}
|
|
22
|
-
.tabs-card{margin-bottom:10px}.tab-buttons{display:flex;gap:7px;flex-wrap:wrap;padding:12px}.sidebar-tabs .tab-buttons{display:none}.sidebar-tabs .tabs-card>.card-head{margin-bottom:12px}.tab-buttons button{min-height:36px;padding:7px 10px;border:1px solid var(--line);border-radius:7px;background:#fff;color:#596274;font-size:13px}.tab-buttons button.active{border-color:#bdcafa;background:var(--primary-soft);color:var(--primary);font-weight:700}.tab-panel{padding:0 13px 13px}.tab-panel>.card:first-child{margin-bottom:10px;background:#f7f8fa;border:0;box-shadow:none}.tab-panel>.card:first-child .card-head{border:0;min-height:44px}.tab-panel>.card:first-child .card-body{padding-top:0}
|
|
23
|
-
.product-grid{display:grid;gap:10px;align-items:start}.product-grid-1{grid-template-columns:1fr}.product-grid-2{grid-template-columns:repeat(2,minmax(0,1fr))}.product-grid-3{grid-template-columns:repeat(3,minmax(0,1fr))}.product-grid-4{grid-template-columns:repeat(4,minmax(0,1fr))}.product-card{overflow:hidden}.product-media-link{display:block;color:inherit;text-decoration:none}.product-media-link:hover .product-media{outline:2px solid var(--primary);outline-offset:-2px}.product-title-link{color:inherit;text-decoration:none}.product-title-link:hover{color:var(--primary);text-decoration:underline}.product-media{position:relative;width:100%;aspect-ratio:var(--product-image-aspect,1/1);overflow:hidden;background:#eef1f4;transition:aspect-ratio .15s ease}.product-card .product-media img,.product-card .product-media>.image-empty{display:block;width:100%;height:100%;aspect-ratio:auto;object-fit:contain;background:#eef1f4}.product-card .product-media>.image-empty{display:grid;place-items:center;color:var(--muted);font-size:13px}.product-body{padding:12px}.product-title{min-height:43px;font-size:13px;font-weight:750;overflow-wrap:anywhere}.product-meta{display:flex;justify-content:space-between;align-items:baseline;gap:8px;margin:9px 0}.price{font-size:20px;color:#c85213;font-weight:800}.facts{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px;margin-bottom:9px}.fact{padding:7px;background:#f7f8fa;border-radius:7px}.fact span{display:block;color:var(--muted);font-size:12px}.fact b{display:block;font-size:13px;overflow-wrap:anywhere}.evidence{padding:8px;border-radius:7px;background:#f5f2ff;color:#544a78;font-size:12px;margin-bottom:8px}.product-actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:7px}.open-button{width:100%;min-height:36px;border:1px solid #cbd5f5;border-radius:7px;background:#fff;color:var(--primary);font-size:13px;font-weight:700}.copy-button{border-color:#d8dde6;color:#596274}.inline-actions{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
|
|
24
|
-
.table-card{margin-bottom:10px}.table-wrap{overflow:auto}.data-table{width:100%;border-collapse:collapse;font-size:13px}.data-table th{height:44px;padding:0 12px;text-align:left;background:#f7f8fa;color:#667082;border-bottom:1px solid var(--line);font-size:12px;white-space:nowrap}.data-table td{padding:10px 12px;border-bottom:1px solid #edf0f3;vertical-align:top}.data-table tr:hover td{background:#fafcff}.cell-product{display:flex;align-items:center;gap:9px;min-width:250px}.cell-thumb{width:48px;height:48px;flex:none;border-radius:7px;overflow:hidden;background:#eef1f4}.cell-thumb img,.cell-thumb .image-empty{width:48px;height:48px;aspect-ratio:1;object-fit:cover;font-size:12px;text-align:center}.empty{padding:24px;text-align:center;color:var(--muted);font-size:13px}.footer-note{padding:14px 0;text-align:right;color:var(--muted);font-size:12px}
|
|
25
|
-
@media(max-width:1180px){.metrics-5,.metrics-6,.metrics-7,.metrics-8{grid-template-columns:repeat(3,1fr)}.product-grid-3,.product-grid-4{grid-template-columns:repeat(2,1fr)}}
|
|
26
|
-
@media(max-width:900px){.sidebar{transform:translateX(-100%);transition:transform .2s}.sidebar.open{transform:translateX(0);box-shadow:0 12px 34px rgba(25,34,49,.18)}.app{margin-left:0}.menu-toggle{display:inline-grid;place-items:center}.topbar{padding:0 14px}.reading-path{padding-inline:14px}.grid-2,.grid-3,.grid-4{grid-template-columns:1fr}.metrics-4{grid-template-columns:repeat(2,1fr)}}
|
|
27
|
-
@media(max-width:600px){main{padding:14px}.topbar{height:auto;min-height:68px;flex-wrap:wrap;padding-block:9px}.tools{width:100%}.search{min-width:0;flex:1}.date-chip{font-size:12px}.reading-path{align-items:flex-start;gap:8px}.reading-path>b{padding-top:6px}.metrics-2,.metrics-3,.metrics-4,.metrics-5,.metrics-6,.metrics-7,.metrics-8{grid-template-columns:repeat(2,1fr)}.product-grid-2,.product-grid-3,.product-grid-4{grid-template-columns:1fr}.mini-stats{grid-template-columns:repeat(2,1fr)}.page-heading{display:block}.page-heading>.tag{margin-top:8px}}
|
|
28
|
-
.gallery-controls{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;padding:10px 12px;margin:12px 0;background:#fff;border:1px solid #dce2ea;border-radius:9px;color:var(--muted);font-size:13px}.gallery-controls label,.gallery-pager{display:flex;align-items:center;gap:8px}.gallery-controls select,.gallery-pager button{padding:7px 10px;border:1px solid #cbd5e1;border-radius:7px;background:#fff;color:var(--text)}.gallery-pager button:disabled{opacity:.45;cursor:not-allowed}.gallery-entry{min-width:0}.gallery-controls-bottom{margin-top:14px}
|
|
29
|
-
@media print{.sidebar,.tools,.menu-toggle,.text-button,.open-button,.reading-path,.gallery-controls{display:none!important}.app{margin:0}.topbar{position:static;height:auto;padding:0 0 12px}.view,.view[hidden]{display:block!important;margin-bottom:22px}.view+.view{break-before:page}.tab-panel,.tab-panel[hidden]{display:block!important}.tab-buttons{display:none}.gallery-entry[hidden]{display:block!important}.card,.alert,.product-card{break-inside:avoid}main{max-width:none;padding:0}}
|
|
30
|
-
</style>
|
|
31
|
-
</head>
|
|
32
|
-
<body class="__BODY_CLASS__">
|
|
33
|
-
<aside class="sidebar" id="sidebar" aria-label="报告导航">
|
|
34
|
-
<div class="brand"><div class="brandmark">__BRAND_MARK__</div><div><b>__BRAND_TITLE__</b><small>__BRAND_SUBTITLE__</small></div></div>
|
|
35
|
-
<nav class="nav" id="nav"><div class="nav-label">__NAV_LABEL__</div>__NAV__</nav>
|
|
36
|
-
<div class="side-status" id="sideStatus">__STATUS__</div>
|
|
37
|
-
</aside>
|
|
38
|
-
<div class="app">
|
|
39
|
-
<header class="topbar"><div style="display:flex;align-items:center;gap:10px"><button class="menu-toggle" id="menuToggle" type="button" aria-label="打开导航">☰</button><div class="identity"><b>__TOP_TITLE__</b><span>__TOP_META__</span></div></div><div class="tools">__TOOLS__</div></header>
|
|
40
|
-
__READING_PATH__
|
|
41
|
-
<main id="workspace">__VIEWS__<div class="footer-note">__FOOTER__</div></main>
|
|
42
|
-
</div>
|
|
43
|
-
<script>
|
|
44
|
-
const viewIds=__VIEW_IDS__,q=(s,r=document)=>r.querySelector(s),qa=(s,r=document)=>[...r.querySelectorAll(s)];
|
|
45
|
-
const navButtons=qa('[data-view-target]'),childButtons=qa('[data-child-view]'),readingButtons=qa('[data-reading-view]');
|
|
46
|
-
function activateTab(viewId,tabId){const view=q(`[data-view="${CSS.escape(viewId)}"]`);if(!view)return null;const target=tabId?q(`[data-tab-target="${CSS.escape(tabId)}"]`,view):q('[data-tab-target].active',view)||q('[data-tab-target]',view);if(!target)return null;const group=target.closest('[data-tabs]'),buttons=qa('[data-tab-target]',group),panels=qa('[data-tab]',group);buttons.forEach(button=>button.classList.toggle('active',button===target));panels.forEach(panel=>panel.hidden=panel.dataset.tab!==target.dataset.tabTarget);childButtons.forEach(button=>button.classList.toggle('active',button.dataset.childView===viewId&&button.dataset.childTab===target.dataset.tabTarget));return target.dataset.tabTarget}
|
|
47
|
-
function showView(id,{tabId=null,historyMode='push',focus=true}={}){const valid=viewIds.includes(id)?id:viewIds[0];qa('[data-view]').forEach(v=>{const on=v.dataset.view===valid;v.hidden=!on;v.classList.toggle('active',on)});navButtons.forEach(b=>{const on=b.dataset.viewTarget===valid;b.classList.toggle('active',on);b.setAttribute('aria-current',on?'page':'false')});qa('[data-nav-group]').forEach(group=>group.classList.toggle('active',group.dataset.navGroup===valid));const activeTab=activateTab(valid,tabId),activeView=q(`[data-view="${CSS.escape(valid)}"]`);qa('[data-tab]:not([hidden]) [data-product-gallery]',activeView).forEach(gallery=>updateGallery(gallery,{reset:true}));readingButtons.forEach(button=>button.classList.toggle('active',button.dataset.readingView===valid));const hash=`#${valid}${activeTab?`/${activeTab}`:''}`;if(historyMode==='push'&&location.hash!==hash)history.pushState({view:valid,tab:activeTab},'',hash);if(historyMode==='replace')history.replaceState({view:valid,tab:activeTab},'',hash);q('#sidebar').classList.remove('open');window.scrollTo(0,0);if(focus)activeView?.focus({preventScroll:true});return valid}
|
|
48
|
-
navButtons.forEach(b=>b.addEventListener('click',()=>showView(b.dataset.viewTarget)));
|
|
49
|
-
childButtons.forEach(b=>b.addEventListener('click',()=>{const viewId=b.dataset.childView,tabId=b.dataset.childTab;showView(viewId,{tabId});const view=q(`[data-view="${CSS.escape(viewId)}"]`),panel=q(`[data-tab="${CSS.escape(tabId)}"]`,view),section=panel?.closest('.tabs-card');if(section)window.scrollTo({top:section.getBoundingClientRect().top+window.scrollY-115,behavior:'auto'})}));
|
|
50
|
-
readingButtons.forEach(b=>b.addEventListener('click',()=>showView(b.dataset.readingView,{tabId:b.dataset.readingTab||null})));
|
|
51
|
-
qa('[data-tabs]').forEach(group=>{const buttons=qa('[data-tab-target]',group);buttons.forEach(button=>button.addEventListener('click',()=>{const view=button.closest('[data-view]');showView(view.dataset.view,{tabId:button.dataset.tabTarget,focus:false})}))});
|
|
52
|
-
async function copyText(value){try{if(navigator.clipboard&&window.isSecureContext){await navigator.clipboard.writeText(value);return true}}catch(error){}const area=document.createElement('textarea');area.value=value;area.setAttribute('readonly','');area.style.position='fixed';area.style.opacity='0';document.body.appendChild(area);area.select();let ok=false;try{ok=document.execCommand('copy')}catch(error){}area.remove();return ok}
|
|
53
|
-
document.addEventListener('click',async event=>{const open=event.target.closest('[data-open-view]');if(open)showView(open.dataset.openView);const copy=event.target.closest('[data-copy]');if(copy){const original=copy.dataset.originalLabel||copy.textContent;copy.dataset.originalLabel=original;const ok=await copyText(copy.dataset.copy);copy.textContent=ok?'已复制':'复制失败';window.setTimeout(()=>{copy.textContent=original},1400);return}const link=event.target.closest('[data-url]');if(link)window.open(link.dataset.url,'_blank','noopener,noreferrer')});
|
|
54
|
-
function hashTarget(){const [view,tab]=decodeURIComponent(location.hash.slice(1)).split('/');return{view,tab}}
|
|
55
|
-
const initial=hashTarget();showView(initial.view,{tabId:initial.tab,historyMode:'replace',focus:false});window.addEventListener('popstate',()=>{const target=hashTarget();showView(target.view,{tabId:target.tab,historyMode:'none',focus:false})});
|
|
56
|
-
function updateGallery(gallery,{reset=false}={}){const grid=q('[data-gallery-grid]',gallery),entries=qa('.gallery-entry',grid),select=q('[data-gallery-sort]',gallery),key=select.value,selected=select.selectedOptions[0],direction=selected?.dataset.direction==='asc'?1:-1,term=(q('#search')?.value||'').trim().toLowerCase();if(reset)gallery.dataset.page='1';const ordered=[...entries].sort((a,b)=>{if(key==='source')return Number(a.dataset.sourceOrder)-Number(b.dataset.sourceOrder);const av=JSON.parse(a.dataset.sortValues||'{}')[key],bv=JSON.parse(b.dataset.sortValues||'{}')[key];if(av==null&&bv==null)return Number(a.dataset.sourceOrder)-Number(b.dataset.sourceOrder);if(av==null)return 1;if(bv==null)return -1;return (Number(av)-Number(bv))*direction||Number(a.dataset.sourceOrder)-Number(b.dataset.sourceOrder)});ordered.forEach(entry=>grid.appendChild(entry));const matches=ordered.filter(entry=>!term||entry.textContent.toLowerCase().includes(term)),size=Number(gallery.dataset.pageSize)||50,pages=Math.max(1,Math.ceil(matches.length/size)),page=Math.min(pages,Math.max(1,Number(gallery.dataset.page)||1));gallery.dataset.page=String(page);ordered.forEach(entry=>entry.hidden=true);matches.slice((page-1)*size,page*size).forEach(entry=>entry.hidden=false);for(const attr of ['data-gallery-count','data-gallery-count-bottom']){const node=q(`[${attr}]`,gallery);if(node)node.textContent=`${matches.length} 件${term?'匹配商品':''}`};for(const attr of ['data-page-status','data-page-status-bottom']){const node=q(`[${attr}]`,gallery);if(node)node.textContent=`第 ${page} / ${pages} 页`};for(const attr of ['data-page-prev','data-page-prev-bottom']){const node=q(`[${attr}]`,gallery);if(node)node.disabled=page<=1};for(const attr of ['data-page-next','data-page-next-bottom']){const node=q(`[${attr}]`,gallery);if(node)node.disabled=page>=pages}}
|
|
57
|
-
qa('[data-product-gallery]').forEach(gallery=>{updateGallery(gallery);q('[data-gallery-sort]',gallery).addEventListener('change',()=>updateGallery(gallery,{reset:true}));for(const attr of ['data-page-prev','data-page-prev-bottom'])q(`[${attr}]`,gallery).addEventListener('click',()=>{gallery.dataset.page=String(Number(gallery.dataset.page)-1);updateGallery(gallery);window.scrollTo({top:gallery.getBoundingClientRect().top+window.scrollY-110,behavior:'smooth'})});for(const attr of ['data-page-next','data-page-next-bottom'])q(`[${attr}]`,gallery).addEventListener('click',()=>{gallery.dataset.page=String(Number(gallery.dataset.page)+1);updateGallery(gallery);window.scrollTo({top:gallery.getBoundingClientRect().top+window.scrollY-110,behavior:'smooth'})})});
|
|
58
|
-
const search=q('#search');if(search)search.addEventListener('input',()=>{const term=search.value.trim().toLowerCase(),active=q('[data-view]:not([hidden])');qa('.searchable',active).filter(node=>!node.closest('[data-product-gallery]')).forEach(node=>node.classList.toggle('hidden',Boolean(term)&&!node.textContent.toLowerCase().includes(term)));qa('[data-product-gallery]',active).forEach(gallery=>updateGallery(gallery,{reset:true}))});
|
|
59
|
-
q('#menuToggle').addEventListener('click',()=>q('#sidebar').classList.toggle('open'));document.addEventListener('keydown',event=>{if((event.metaKey||event.ctrlKey)&&event.key.toLowerCase()==='k'&&search){event.preventDefault();search.focus()}if(event.key==='Escape')q('#sidebar').classList.remove('open')});
|
|
60
|
-
function syncProductImageAspect(image){if(!image.naturalWidth||!image.naturalHeight)return;const media=image.closest('.product-media');if(!media)return;media.style.setProperty('--product-image-aspect',`${image.naturalWidth}/${image.naturalHeight}`);media.dataset.imageAspect=`${image.naturalWidth}:${image.naturalHeight}`}
|
|
61
|
-
qa('.product-media img').forEach(image=>{if(image.complete&&image.naturalWidth)syncProductImageAspect(image);else image.addEventListener('load',()=>syncProductImageAspect(image),{once:true})});
|
|
62
|
-
qa('img[data-remote-image]').forEach(image=>image.addEventListener('error',()=>{image.hidden=true;const fallback=image.nextElementSibling;if(fallback)fallback.hidden=false},{once:true}));
|
|
63
|
-
function syncTopbarHeight(){const topbar=q('.topbar');if(topbar)document.documentElement.style.setProperty('--actual-topbar',`${topbar.offsetHeight}px`)}syncTopbarHeight();window.addEventListener('resize',syncTopbarHeight);if(window.ResizeObserver)new ResizeObserver(syncTopbarHeight).observe(q('.topbar'));
|
|
64
|
-
window.commerceUI={contract:'__CONTRACT__',version:'__CLI_VERSION__',viewIds,showView,activateTab,visibleViews:()=>qa('[data-view]').filter(v=>!v.hidden).map(v=>v.dataset.view)};
|
|
65
|
-
</script>
|
|
66
|
-
</body>
|
|
67
|
-
</html>
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Deterministic contract checks for commerce-ui HTML artifacts."""
|
|
3
|
-
|
|
4
|
-
from __future__ import annotations
|
|
5
|
-
|
|
6
|
-
import pathlib
|
|
7
|
-
import re
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
REQUIRED = {
|
|
11
|
-
"responsive viewport": r'<meta\s+name=["\']viewport["\']',
|
|
12
|
-
"generator metadata": r'<meta\s+name=["\']generator["\']\s+content=["\']commerce-ui/',
|
|
13
|
-
"contract metadata": r'<meta\s+name=["\']commerce-ui-contract["\']\s+content=["\']compact-workbench@1\.0["\']',
|
|
14
|
-
"external image policy": r'<meta\s+name=["\']commerce-ui-external-images["\']\s+content=["\'](?:forbid|allow_https)["\']',
|
|
15
|
-
"left navigation": r'class=["\'][^"\']*sidebar',
|
|
16
|
-
"lower-left status": r'id=["\']sideStatus["\']',
|
|
17
|
-
"upper-right tools": r'class=["\'][^"\']*tools',
|
|
18
|
-
"modular cards": r'class=["\'][^"\']*card',
|
|
19
|
-
"mobile rule": r'@media\s*\(max-width:',
|
|
20
|
-
"print rule": r'@media\s+print',
|
|
21
|
-
"view targets": r'<[^>]+\bdata-view-target=["\'][^"\']+',
|
|
22
|
-
"independent views": r'<[^>]+\bdata-view=["\'][^"\']+',
|
|
23
|
-
"view router": r'function\s+showView\s*\(',
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
FORBIDDEN = {
|
|
27
|
-
"unresolved template marker": r'__[A-Z][A-Z0-9_]+__|\{\{[^}]+\}\}',
|
|
28
|
-
"private home path": r'/Users/[^/]+/|[A-Za-z]:\\Users\\[^\\]+\\',
|
|
29
|
-
"credential-like value": r'(?i)(api[_-]?key|access[_-]?token|secret)\s*[:=]\s*["\'][A-Za-z0-9_\-]{16,}',
|
|
30
|
-
"top-level anchor scrolling": r'scrollIntoView\s*\(',
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def validate_file(path_value: pathlib.Path | str) -> dict:
|
|
35
|
-
path = pathlib.Path(path_value).expanduser().resolve()
|
|
36
|
-
errors: list[str] = []
|
|
37
|
-
if not path.is_file():
|
|
38
|
-
return {"ok": False, "path": str(path), "errors": ["file not found"]}
|
|
39
|
-
text = path.read_text(encoding="utf-8")
|
|
40
|
-
for label, pattern in REQUIRED.items():
|
|
41
|
-
if not re.search(pattern, text, re.I):
|
|
42
|
-
errors.append(f"missing {label}")
|
|
43
|
-
for label, pattern in FORBIDDEN.items():
|
|
44
|
-
if re.search(pattern, text):
|
|
45
|
-
errors.append(f"found {label}")
|
|
46
|
-
|
|
47
|
-
targets = re.findall(r'<[^>]+\bdata-view-target=["\']([^"\']+)', text, re.I)
|
|
48
|
-
views = re.findall(r'<[^>]+\bdata-view=["\']([^"\']+)', text, re.I)
|
|
49
|
-
if len(targets) < 2:
|
|
50
|
-
errors.append("fewer than two top-level view targets")
|
|
51
|
-
if len(views) < 2:
|
|
52
|
-
errors.append("fewer than two independent views")
|
|
53
|
-
if len(targets) != len(set(targets)):
|
|
54
|
-
errors.append("duplicate top-level view targets")
|
|
55
|
-
if len(views) != len(set(views)):
|
|
56
|
-
errors.append("duplicate independent view ids")
|
|
57
|
-
if set(targets) != set(views):
|
|
58
|
-
errors.append(f"view target/view mismatch: targets={sorted(set(targets))}, views={sorted(set(views))}")
|
|
59
|
-
|
|
60
|
-
initially_visible = len(re.findall(r'<section\s+class=["\'][^"\']*\bview\b[^"\']*["\'][^>]*\bdata-view=["\'][^"\']+["\'](?![^>]*\bhidden\b)[^>]*>', text, re.I))
|
|
61
|
-
if initially_visible != 1:
|
|
62
|
-
errors.append(f"expected exactly one initially visible view, found {initially_visible}")
|
|
63
|
-
|
|
64
|
-
if 'class="card conclusion-card"' in text:
|
|
65
|
-
conclusion_items = re.findall(r'<article\s+class="conclusion-item\s+conclusion-tone-(?:info|positive|warning|danger)\s+searchable"', text)
|
|
66
|
-
if len(conclusion_items) < 2:
|
|
67
|
-
errors.append("structured conclusion has fewer than two items")
|
|
68
|
-
if not re.search(r'class="conclusion-segment[^\"]*emphasis-(?:strong|strong-italic)', text):
|
|
69
|
-
errors.append("structured conclusion has no bold emphasis")
|
|
70
|
-
if not re.search(r'class="conclusion-segment[^\"]*emphasis-(?:italic|strong-italic)', text):
|
|
71
|
-
errors.append("structured conclusion has no italic emphasis")
|
|
72
|
-
if not re.search(r'class="conclusion-segment[^\"]*conclusion-color-(?:info|positive|warning|danger)', text):
|
|
73
|
-
errors.append("structured conclusion has no color emphasis")
|
|
74
|
-
|
|
75
|
-
sizes = [float(x) for x in re.findall(r'font-size\s*:\s*(\d+(?:\.\d+)?)px', text, re.I)]
|
|
76
|
-
if sizes and min(sizes) < 12:
|
|
77
|
-
errors.append(f"font size below 12px found: {min(sizes):g}px")
|
|
78
|
-
if not re.search(r'font-size\s*:\s*13px', text, re.I):
|
|
79
|
-
errors.append("missing 13px body/table baseline")
|
|
80
|
-
|
|
81
|
-
policy_match = re.search(r'<meta\s+name=["\']commerce-ui-external-images["\']\s+content=["\']([^"\']+)', text, re.I)
|
|
82
|
-
external_images_approved = bool(policy_match and policy_match.group(1) == "allow_https")
|
|
83
|
-
remote_dependencies = re.findall(r'<([a-z0-9]+)\b[^>]*\b(src|href)=["\'](https?://[^"\']+)', text, re.I)
|
|
84
|
-
invalid_remote = [item for item in remote_dependencies if not item[2].startswith("https://") or
|
|
85
|
-
(item[0].lower(), item[1].lower()) not in {("img", "src"), ("a", "href")}]
|
|
86
|
-
if invalid_remote:
|
|
87
|
-
errors.append(f"found {len(invalid_remote)} unsupported remote rendering dependency(s)")
|
|
88
|
-
|
|
89
|
-
images = re.findall(r'<img\b[^>]*\bsrc=["\']([^"\']+)', text, re.I)
|
|
90
|
-
external_images = [src for src in images if src.startswith("https://")]
|
|
91
|
-
invalid_images = [src for src in images if not src.startswith("data:image/") and not src.startswith("https://")]
|
|
92
|
-
if external_images and not external_images_approved:
|
|
93
|
-
errors.append(f"found {len(external_images)} unapproved external image asset(s)")
|
|
94
|
-
if invalid_images:
|
|
95
|
-
errors.append(f"found {len(invalid_images)} invalid image asset(s)")
|
|
96
|
-
product_links = re.findall(r'<a\b[^>]*class="product-(?:media|title)-link"[^>]*>', text)
|
|
97
|
-
if product_links and any('target="_blank"' not in link or 'rel="noopener noreferrer"' not in link for link in product_links):
|
|
98
|
-
errors.append("product external links require new-tab noopener noreferrer")
|
|
99
|
-
galleries = re.findall(r'<section\s+class="product-gallery"[^>]*\bdata-product-gallery\b[^>]*>', text)
|
|
100
|
-
if galleries:
|
|
101
|
-
if not re.search(r'function\s+updateGallery\s*\(', text):
|
|
102
|
-
errors.append("product gallery pagination runtime missing")
|
|
103
|
-
if text.count('data-gallery-sort') < len(galleries) or text.count('data-page-next') < len(galleries):
|
|
104
|
-
errors.append("product gallery controls missing")
|
|
105
|
-
|
|
106
|
-
return {
|
|
107
|
-
"ok": not errors,
|
|
108
|
-
"path": str(path),
|
|
109
|
-
"contract": "compact-workbench@1.0",
|
|
110
|
-
"view_count": len(views),
|
|
111
|
-
"image_count": len(images),
|
|
112
|
-
"external_image_count": len(external_images),
|
|
113
|
-
"product_link_count": len(product_links),
|
|
114
|
-
"external_images_approved": external_images_approved,
|
|
115
|
-
"product_gallery_count": len(galleries),
|
|
116
|
-
"errors": errors,
|
|
117
|
-
}
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Reconcile visual product labels with organic placement and advertiser evidence."""
|
|
3
|
-
|
|
4
|
-
import argparse
|
|
5
|
-
import json
|
|
6
|
-
import statistics
|
|
7
|
-
from collections import Counter, defaultdict
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def median(values):
|
|
12
|
-
usable = [float(value) for value in values if value is not None]
|
|
13
|
-
return round(statistics.median(usable), 2) if usable else None
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def main():
|
|
17
|
-
parser = argparse.ArgumentParser()
|
|
18
|
-
parser.add_argument("--manifest", required=True)
|
|
19
|
-
parser.add_argument("--labels", required=True)
|
|
20
|
-
parser.add_argument("--output", required=True)
|
|
21
|
-
args = parser.parse_args()
|
|
22
|
-
manifest_path = Path(args.manifest).expanduser().resolve()
|
|
23
|
-
labels_path = Path(args.labels).expanduser().resolve()
|
|
24
|
-
output_path = Path(args.output).expanduser().resolve()
|
|
25
|
-
if output_path.exists():
|
|
26
|
-
parser.error("output already exists; select a new path")
|
|
27
|
-
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
28
|
-
ledger = json.loads(labels_path.read_text(encoding="utf-8"))
|
|
29
|
-
products = {p["product_id"]: p for p in manifest["products"]}
|
|
30
|
-
selected = set(manifest["selected_organic_ids"]) | set(manifest["selected_ad_only_ids"])
|
|
31
|
-
labels = {}
|
|
32
|
-
for item in ledger["labels"]:
|
|
33
|
-
pid = str(item["product_id"])
|
|
34
|
-
if pid in labels or pid not in selected:
|
|
35
|
-
raise ValueError(f"duplicate or out-of-scope visual label: {pid}")
|
|
36
|
-
eligibility = item.get("eligibility")
|
|
37
|
-
if eligibility not in ("target", "non_target", "uncertain"):
|
|
38
|
-
raise ValueError(f"invalid eligibility for {pid}")
|
|
39
|
-
form = item.get("form")
|
|
40
|
-
if eligibility == "target" and (not form or not item.get("visible_evidence")):
|
|
41
|
-
raise ValueError(f"target needs form and visible evidence: {pid}")
|
|
42
|
-
if eligibility != "target" and form:
|
|
43
|
-
raise ValueError(f"non-target/uncertain must not have form: {pid}")
|
|
44
|
-
fetched_path = manifest["fetches"].get(pid, {}).get("path")
|
|
45
|
-
if fetched_path is None or item.get("image_path") != fetched_path:
|
|
46
|
-
raise ValueError(f"label lacks exact fetched-image evidence: {pid}")
|
|
47
|
-
labels[pid] = item
|
|
48
|
-
organic_ids = set(manifest["selected_organic_ids"])
|
|
49
|
-
ad_only_ids = set(manifest["selected_ad_only_ids"])
|
|
50
|
-
organic_labels = {pid: label for pid, label in labels.items() if pid in organic_ids}
|
|
51
|
-
ad_labels = {pid: label for pid, label in labels.items() if pid in ad_only_ids}
|
|
52
|
-
counts = Counter(label["eligibility"] for label in organic_labels.values())
|
|
53
|
-
ad_counts = Counter(label["eligibility"] for label in ad_labels.values())
|
|
54
|
-
by_form = defaultdict(list)
|
|
55
|
-
for pid, label in organic_labels.items():
|
|
56
|
-
if label["eligibility"] == "target":
|
|
57
|
-
by_form[label["form"]].append(products[pid])
|
|
58
|
-
forms = []
|
|
59
|
-
for form, items in by_form.items():
|
|
60
|
-
items.sort(key=lambda p: p["organic_rank"])
|
|
61
|
-
forms.append({
|
|
62
|
-
"form": form,
|
|
63
|
-
"organic_unique_products": len(items),
|
|
64
|
-
"shops": len({p["shop"] for p in items if p["shop"]}),
|
|
65
|
-
"top_100_organic": sum(p["organic_rank"] <= 100 for p in items),
|
|
66
|
-
"top_300_organic": sum(p["organic_rank"] <= 300 for p in items),
|
|
67
|
-
"median_price": median(p["price"] for p in items),
|
|
68
|
-
"median_payment_count": median(p["payment_count"] for p in items),
|
|
69
|
-
"advertised_also": sum(bool(p["ad_positions"]) for p in items),
|
|
70
|
-
"representative_ids": [p["product_id"] for p in items[:8]],
|
|
71
|
-
})
|
|
72
|
-
forms.sort(key=lambda f: (-f["top_100_organic"], -f["organic_unique_products"], f["form"]))
|
|
73
|
-
advertised = [p for p in products.values() if p["ad_positions"]]
|
|
74
|
-
overlap = [p for p in advertised if p["natural_positions"]]
|
|
75
|
-
analysis = {
|
|
76
|
-
"schema_version": 1,
|
|
77
|
-
"source": manifest["source"],
|
|
78
|
-
"search_term": manifest["search_term"],
|
|
79
|
-
"scope": manifest["scope"],
|
|
80
|
-
"source_counts": {
|
|
81
|
-
"display_rows": len(manifest["source_rows"]),
|
|
82
|
-
"unique_products": len(products),
|
|
83
|
-
"organic_unique_products": sum(bool(p["natural_positions"]) for p in products.values()),
|
|
84
|
-
"advertised_unique_products": len(advertised),
|
|
85
|
-
"advertised_distinct_shops": len({p["shop"] for p in advertised if p["shop"]}),
|
|
86
|
-
"advertised_with_natural": len(overlap),
|
|
87
|
-
"ad_only_unique_products": len(advertised) - len(overlap),
|
|
88
|
-
"ad_only_distinct_shops": len({p["shop"] for p in advertised if not p["natural_positions"] and p["shop"]}),
|
|
89
|
-
"ad_display_rows": sum(row["placement"] == "广告位" for row in manifest["source_rows"]),
|
|
90
|
-
},
|
|
91
|
-
"image_fetch": {
|
|
92
|
-
"requested": len(manifest["fetches"]),
|
|
93
|
-
"ok": sum(result["status"] == "ok" for result in manifest["fetches"].values()),
|
|
94
|
-
"failures": {pid: result["status"] for pid, result in manifest["fetches"].items() if result["status"] != "ok"},
|
|
95
|
-
},
|
|
96
|
-
"visual_coverage": {
|
|
97
|
-
"organic_selected": len(organic_ids),
|
|
98
|
-
"organic_labeled": len(organic_labels),
|
|
99
|
-
"organic_target": counts["target"],
|
|
100
|
-
"organic_non_target": counts["non_target"],
|
|
101
|
-
"organic_uncertain": counts["uncertain"],
|
|
102
|
-
"ad_only_selected": len(ad_only_ids),
|
|
103
|
-
"ad_only_labeled": len(ad_labels),
|
|
104
|
-
"ad_only_target": ad_counts["target"],
|
|
105
|
-
"ad_only_non_target": ad_counts["non_target"],
|
|
106
|
-
"ad_only_uncertain": ad_counts["uncertain"],
|
|
107
|
-
},
|
|
108
|
-
"forms": forms,
|
|
109
|
-
"unlabeled_organic_ids": sorted(organic_ids - set(organic_labels)),
|
|
110
|
-
"unlabeled_ad_only_ids": sorted(ad_only_ids - set(ad_labels)),
|
|
111
|
-
}
|
|
112
|
-
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
113
|
-
with output_path.open("x", encoding="utf-8") as handle:
|
|
114
|
-
json.dump(analysis, handle, ensure_ascii=False, indent=2)
|
|
115
|
-
print(json.dumps({"output": str(output_path), "source_counts": analysis["source_counts"], "visual_coverage": analysis["visual_coverage"], "form_count": len(forms)}, ensure_ascii=False))
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if __name__ == "__main__":
|
|
119
|
-
main()
|