Commit 1027cb42 authored by allen.wang's avatar allen.wang

fix:优化报表效果

parent d90a2b4d
--- ---
name: vip-report name: vip-report
description: 协调 VIP 月报所有页的来源与渲染,通过固定配置与模板确保生成版与基线完全一致。 description: Coordinate VIP monthly report data collection, asset generation, PowerPoint replacement, and final comparison so the generated report stays aligned with the template.
--- ---
# VIP Report # VIP Report
## 概览 ## Overview
这个总 skill 串联 VIP 月报的数据采集、素材生成、模板替换、比对验证四步,确保最终 `Report.pptx` 一模一样。 This skill coordinates the end-to-end VIP monthly report workflow:
## 数据来源约定 - collect source data
- generate slide assets
- replace template content
- compare the generated PPT with the baseline/template
- 所有页的来源映射记录在 `C:\workspace\cursor\output\vip-report\slide-source-map.yaml`,按 slide_code 映射到 Tableau view、MySQL 表或第三方系统。 ## Source Rules
- 依赖 Tableau (`tableau.charleskeith.cn`) 时统一走 `config.yaml` 里的账号(目前是 `ec_user01`),优先尝试下载/导出,若必须截屏则按照脚本裁切。
- 某些页面会引用 `vip.com`,凭据不驻留仓库;调用 `config.yaml` 中的 `vip.login_endpoint` 让第三方系统自动处理登录。
- 需要数据库的页面使用测试库 `ckc_cep_db_test``config.yaml``mysql` 段),会参考 `s11-source-validation.md``s11-sql-hypothesis.sql` 等文件保持口径。
- S11 的活动口径已切换到 `oms_shop_report``memo` 维护活动/非活动,`single='Y'` 维护独家。
## 关键路径 - Slide-to-source mapping lives in `C:\workspace\cursor\output\vip-report\slide-source-map.yaml`.
- Tableau source of truth is `tableau.charleskeith.cn`, using the credentials in `config.yaml`.
- Some pages may depend on `vip.com`; if needed, use `vip.login_endpoint` from config rather than hardcoding login logic.
- Test MySQL for VIP-report remains the `config.yaml` `mysql` section unless the task explicitly asks for production verification.
- Production MySQL default for future VIP-report sessions:
`jdbc:mysql://erp.charleskeith.cn:30004/ckc_cep_db?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true`
username: `allen`
password: `wangjun@123`
- When a future session says “生产地址 mysql” for VIP-report, default to the production MySQL above.
- S11 campaign logic is maintained separately through memo / validation files and must stay consistent with its confirmed source logic.
- 配置文件:`C:\Users\niuniu\.codex\vip-report\config.yaml` ## Key Paths
- 配置样例:`C:\Users\niuniu\.codex\vip-report\config.yaml.example`
- 模板 PPT:`C:\Users\niuniu\Desktop\Report.pptx`
- slide 来源表:`C:\workspace\cursor\output\vip-report\slide-source-map.yaml`
- 素材与输出目录:`C:\workspace\cursor\output\vip-report`
## 渲染策略 - Config: `C:\Users\niuniu\.codex\vip-report\config.yaml`
- Config example: `C:\Users\niuniu\.codex\vip-report\config.yaml.example`
- Template PPT: `C:\Users\niuniu\Desktop\Report.pptx`
- Slide source map: `C:\workspace\cursor\output\vip-report\slide-source-map.yaml`
- Output root: `C:\workspace\cursor\output\vip-report`
- 目前 Windows 可以成功创建 `PowerPoint.Application`,建议优先用 COM 改写模板(如 `render_template_com.ps1`)。 ## Rendering Strategy
- `OpenXML` 只在无法运行 COM 或需包级比较时作为补充手段。
- 所有替换完成后必须运行 `compare_pptx.py``render-ops.*.json` 也可辅助)确认差异限定在预期的 `ppt/media/image*.png``ppt/slides/slide*.xml` 中。
## 子技能与流程 - On Windows, prefer COM-based rendering when PowerPoint automation is available.
- Use OpenXML only as a fallback or for package-level comparisons.
- After replacement, compare with `compare_pptx.py` and any `render-ops.*.json` outputs to confirm only expected files changed.
- `/vip-report overview`:整理 `slide-source-map.yaml``shape-inventory.json``template-manifest.json`,确认每页 shape_id/名称。 ## Sub-skills
- `/vip-report data-sync`:按 slide 逐个源头从 Tableau 或 SQL 拉数据并生成 `render-ops.*.json`
- `/vip-report presentation`:调用 `vip-report-render.ps1` 等脚本把素材替回 `Report.pptx` 并输出比对结果。
- 当前拆出的专题 skill:`/vip-report monthly-sales``/vip-report inventory-monthly``/vip-report top-products``/vip-report campaign-s11``/vip-report warehouse-100060`,它们分别对应 `slide-source-map.yaml` 中 S02-S03、S04-S10、S11、S13 等页的来源。
## 辅助命令 - `/vip-report overview`
- `/vip-report data-sync`
- `/vip-report presentation`
- Specialized report skills for monthly sales, inventory monthly, top products, campaign S11, and warehouse 100060
- 生成模板 manifest(用于审查 slide 结构):`powershell -ExecutionPolicy Bypass -File C:\Users\niuniu\.codex\skills\vip-report\bin\vip-report-manifest.ps1` ## Helper Commands
- 生成 shape inventory(查 shape_id/名称):`powershell -ExecutionPolicy Bypass -File C:\Users\niuniu\.codex\skills\vip-report\bin\vip-report-inventory.ps1`
- 生成基线副本与比对(验证模板/生成文件差异):`powershell -ExecutionPolicy Bypass -File C:\Users\niuniu\.codex\skills\vip-report\bin\vip-report-baseline.ps1`
- 生成 S02/S03 素材并渲染:`powershell -ExecutionPolicy Bypass -File C:\Users\niuniu\.codex\skills\vip-report\bin\vip-report-monthly-sales-sync.ps1 -Render`
## 备注 - Manifest: `powershell -ExecutionPolicy Bypass -File C:\Users\niuniu\.codex\skills\vip-report\bin\vip-report-manifest.ps1`
- Shape inventory: `powershell -ExecutionPolicy Bypass -File C:\Users\niuniu\.codex\skills\vip-report\bin\vip-report-inventory.ps1`
- Baseline compare: `powershell -ExecutionPolicy Bypass -File C:\Users\niuniu\.codex\skills\vip-report\bin\vip-report-baseline.ps1`
- Tableau 登录凭据只保存在 `config.yaml`,请勿复制到技能文档。 ## Notes
- vip.com 登录(若展开)只需配置 `vip.login_endpoint` 让第三方服务处理。
- 每次渲染完成后务必与模板做 `compare_pptx.py` 比较,确认差异仅限预期替换的图片和 `docProps/*``ppt/slides/*.xml` - Keep Tableau / MySQL / vip.com credentials in config or approved skill docs only.
- Do not silently change business filtering logic when validating report mismatches.
- Always verify generated output against the template before calling the task done.
...@@ -13,15 +13,27 @@ param( ...@@ -13,15 +13,27 @@ param(
[int]$ShopId = 20, [int]$ShopId = 20,
[switch]$DisableStrictJanuaryCheck, [switch]$DisableStrictJanuaryCheck,
[switch]$Render, [switch]$Render,
[string]$OutputDir = "",
[string]$OutputPath = "C:\workspace\cursor\output\vip-report\generated-campaign-s11-live.pptx", [string]$OutputPath = "C:\workspace\cursor\output\vip-report\generated-campaign-s11-live.pptx",
[string]$CompareOutputPath = "C:\workspace\cursor\output\vip-report\generated-campaign-s11-live.compare.json" [string]$CompareOutputPath = "C:\workspace\cursor\output\vip-report\generated-campaign-s11-live.compare.json"
) )
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
$root = Split-Path -Parent $PSScriptRoot $root = Split-Path -Parent $PSScriptRoot
$opsPath = "C:\workspace\cursor\output\vip-report\render-ops.campaign-s11.live.json" $resolvedOutputDir = if ($OutputDir) { [System.IO.Path]::GetFullPath($OutputDir) } else { "C:\workspace\cursor\output\vip-report" }
$opsPath = Join-Path $resolvedOutputDir "render-ops.campaign-s11.live.json"
$templatePath = "C:\Users\niuniu\Desktop\Report.pptx" $templatePath = "C:\Users\niuniu\Desktop\Report.pptx"
if (-not (Test-Path -LiteralPath $resolvedOutputDir)) {
New-Item -ItemType Directory -Path $resolvedOutputDir -Force | Out-Null
}
if ($OutputDir -and $OutputPath -eq "C:\workspace\cursor\output\vip-report\generated-campaign-s11-live.pptx") {
$OutputPath = Join-Path $resolvedOutputDir "generated-campaign-s11-live.pptx"
}
if ($OutputDir -and $CompareOutputPath -eq "C:\workspace\cursor\output\vip-report\generated-campaign-s11-live.compare.json") {
$CompareOutputPath = Join-Path $resolvedOutputDir "generated-campaign-s11-live.compare.json"
}
$pythonArgs = @( $pythonArgs = @(
"$root\scripts\sync_campaign_s11_assets.py", "$root\scripts\sync_campaign_s11_assets.py",
"--config", "$ConfigPath", "--config", "$ConfigPath",
...@@ -40,6 +52,7 @@ if ($CompareYear -gt 0) { ...@@ -40,6 +52,7 @@ if ($CompareYear -gt 0) {
if ($DisableStrictJanuaryCheck) { if ($DisableStrictJanuaryCheck) {
$pythonArgs += @("--disable-strict-january-check") $pythonArgs += @("--disable-strict-january-check")
} }
$pythonArgs += @("--output-dir", "$resolvedOutputDir")
python @pythonArgs python @pythonArgs
if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -ne 0) {
......
...@@ -4,12 +4,18 @@ param( ...@@ -4,12 +4,18 @@ param(
[string]$ReportMonth = "", [string]$ReportMonth = "",
[int]$ReportYear = 0, [int]$ReportYear = 0,
[int]$CompareYear = 0, [int]$CompareYear = 0,
[string]$OutputDir = "",
[switch]$DisableTemplateLock [switch]$DisableTemplateLock
) )
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
$root = Split-Path -Parent $PSScriptRoot $root = Split-Path -Parent $PSScriptRoot
$opsPath = "C:\workspace\cursor\output\vip-report\render-ops.inventory-monthly.live.json" $resolvedOutputDir = if ($OutputDir) { [System.IO.Path]::GetFullPath($OutputDir) } else { "C:\workspace\cursor\output\vip-report" }
$opsPath = Join-Path $resolvedOutputDir "render-ops.inventory-monthly.live.json"
if (-not (Test-Path -LiteralPath $resolvedOutputDir)) {
New-Item -ItemType Directory -Path $resolvedOutputDir -Force | Out-Null
}
$pythonArgs = @( $pythonArgs = @(
"$root\scripts\sync_inventory_monthly_assets.py", "$root\scripts\sync_inventory_monthly_assets.py",
...@@ -28,6 +34,7 @@ if ($CompareYear -gt 0) { ...@@ -28,6 +34,7 @@ if ($CompareYear -gt 0) {
if ($DisableTemplateLock.IsPresent) { if ($DisableTemplateLock.IsPresent) {
$pythonArgs += "--disable-template-lock" $pythonArgs += "--disable-template-lock"
} }
$pythonArgs += @("--output-dir", "$resolvedOutputDir")
python @pythonArgs python @pythonArgs
Write-Output $opsPath Write-Output $opsPath
...@@ -5,16 +5,27 @@ param( ...@@ -5,16 +5,27 @@ param(
[int]$ReportYear = 0, [int]$ReportYear = 0,
[int]$CompareYear = 0, [int]$CompareYear = 0,
[switch]$Render, [switch]$Render,
[string]$OutputDir = "",
[string]$OutputPath = "C:\workspace\cursor\output\vip-report\generated-monthly-sales-live.pptx", [string]$OutputPath = "C:\workspace\cursor\output\vip-report\generated-monthly-sales-live.pptx",
[string]$CompareOutputPath = "C:\workspace\cursor\output\vip-report\generated-monthly-sales-live.compare.json" [string]$CompareOutputPath = "C:\workspace\cursor\output\vip-report\generated-monthly-sales-live.compare.json"
) )
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
$root = Split-Path -Parent $PSScriptRoot $root = Split-Path -Parent $PSScriptRoot
$workdir = "C:\workspace\cursor" $resolvedOutputDir = if ($OutputDir) { [System.IO.Path]::GetFullPath($OutputDir) } else { "C:\workspace\cursor\output\vip-report" }
$opsPath = "C:\workspace\cursor\output\vip-report\render-ops.monthly-sales.live.json" $opsPath = Join-Path $resolvedOutputDir "render-ops.monthly-sales.live.json"
$templatePath = "C:\Users\niuniu\Desktop\Report.pptx" $templatePath = "C:\Users\niuniu\Desktop\Report.pptx"
if (-not (Test-Path -LiteralPath $resolvedOutputDir)) {
New-Item -ItemType Directory -Path $resolvedOutputDir -Force | Out-Null
}
if ($OutputDir -and $OutputPath -eq "C:\workspace\cursor\output\vip-report\generated-monthly-sales-live.pptx") {
$OutputPath = Join-Path $resolvedOutputDir "generated-monthly-sales-live.pptx"
}
if ($OutputDir -and $CompareOutputPath -eq "C:\workspace\cursor\output\vip-report\generated-monthly-sales-live.compare.json") {
$CompareOutputPath = Join-Path $resolvedOutputDir "generated-monthly-sales-live.compare.json"
}
# Forward optional report-period args; config.yaml defaults are used when omitted. # Forward optional report-period args; config.yaml defaults are used when omitted.
$pythonArgs = @( $pythonArgs = @(
"$root\scripts\sync_monthly_sales_assets.py", "$root\scripts\sync_monthly_sales_assets.py",
...@@ -30,6 +41,7 @@ if ($ReportYear -gt 0) { ...@@ -30,6 +41,7 @@ if ($ReportYear -gt 0) {
if ($CompareYear -gt 0) { if ($CompareYear -gt 0) {
$pythonArgs += @("--compare-year", "$CompareYear") $pythonArgs += @("--compare-year", "$CompareYear")
} }
$pythonArgs += @("--output-dir", "$resolvedOutputDir")
python @pythonArgs python @pythonArgs
if ($Render) { if ($Render) {
......
param( param(
[string]$TemplatePath = "C:\Users\niuniu\Desktop\Report.pptx", [string]$TemplatePath = "",
[string]$OutputPath = "C:\workspace\cursor\output\vip-report\generated-from-template.pptx", [string]$OutputPath = "",
[string]$OperationsPath = "" [string]$OperationsPath = ""
) )
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
$root = Split-Path -Parent $PSScriptRoot $root = Split-Path -Parent $PSScriptRoot
function Resolve-ProjectPath {
param([string]$PathValue)
if (-not $PathValue) {
return $PathValue
}
if ([System.IO.Path]::IsPathRooted($PathValue)) {
return $PathValue
}
return [System.IO.Path]::GetFullPath((Join-Path $root $PathValue))
}
if (-not $TemplatePath) {
$TemplatePath = "Report.pptx"
}
if (-not $OutputPath) {
$OutputPath = "output\generated-from-template.pptx"
}
$TemplatePath = Resolve-ProjectPath $TemplatePath
$OutputPath = Resolve-ProjectPath $OutputPath
$OperationsPath = Resolve-ProjectPath $OperationsPath
powershell -ExecutionPolicy Bypass -File "$root\scripts\render_template_com.ps1" -TemplatePath "$TemplatePath" -OutputPath "$OutputPath" -OperationsPath "$OperationsPath" powershell -ExecutionPolicy Bypass -File "$root\scripts\render_template_com.ps1" -TemplatePath "$TemplatePath" -OutputPath "$OutputPath" -OperationsPath "$OperationsPath"
This diff is collapsed.
param( param(
[string]$ConfigPath = "C:\Users\niuniu\.codex\vip-report\config.yaml", [string]$ConfigPath = "",
[string]$Slides = "S09,S10", [string]$Slides = "S09,S10",
[string]$ReportMonth = "", [string]$ReportMonth = "",
[int]$ReportYear = 0, [int]$ReportYear = 0,
[int]$CompareYear = 0, [int]$CompareYear = 0,
[switch]$Render, [switch]$Render,
[string]$OutputDir = "",
[string]$TemplatePath = "", [string]$TemplatePath = "",
[string]$OutputPath = "C:\workspace\cursor\output\vip-report\generated-top-products.pptx", [string]$OutputPath = "",
[string]$CompareOutputPath = "C:\workspace\cursor\output\vip-report\generated-top-products.compare.json" [string]$CompareOutputPath = ""
) )
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
$root = Split-Path -Parent $PSScriptRoot $root = Split-Path -Parent $PSScriptRoot
$workdir = "C:\workspace\cursor" $workdir = if ($OutputDir) { [System.IO.Path]::GetFullPath($OutputDir) } else { Join-Path $root "output" }
$opsPath = "C:\workspace\cursor\output\vip-report\render-ops.top-products.live.json" $opsPath = Join-Path $workdir "render-ops.top-products.live.json"
$fallbackTemplatePath = Join-Path $root "output\Report.clean.pptx" $workspaceRoot = Split-Path -Parent (Split-Path -Parent $root)
$workspacePython = Join-Path $workspaceRoot "tools\python.exe"
function Resolve-ProjectPath {
param([string]$PathValue)
if (-not $PathValue) {
return $PathValue
}
if ([System.IO.Path]::IsPathRooted($PathValue)) {
return $PathValue
}
return [System.IO.Path]::GetFullPath((Join-Path $root $PathValue))
}
function Resolve-PythonCommand {
if (Test-Path -LiteralPath $workspacePython) {
return $workspacePython
}
$pythonCmd = Get-Command python -ErrorAction SilentlyContinue
if ($pythonCmd) {
return $pythonCmd.Source
}
throw "Python executable not found. Checked PATH and $workspacePython"
}
if (-not $ConfigPath) {
$ConfigPath = "config.yaml"
}
if (-not $TemplatePath) { if (-not $TemplatePath) {
$TemplatePath = $fallbackTemplatePath $TemplatePath = "Report.pptx"
}
if (-not $OutputPath) {
$OutputPath = if ($OutputDir) { Join-Path $workdir "generated-top-products.pptx" } else { "output\generated-top-products.pptx" }
}
if (-not $CompareOutputPath) {
$CompareOutputPath = if ($OutputDir) { Join-Path $workdir "generated-top-products.compare.json" } else { "output\generated-top-products.compare.json" }
}
$ConfigPath = Resolve-ProjectPath $ConfigPath
$TemplatePath = Resolve-ProjectPath $TemplatePath
$OutputPath = Resolve-ProjectPath $OutputPath
$CompareOutputPath = Resolve-ProjectPath $CompareOutputPath
if (-not (Test-Path -LiteralPath $workdir)) {
New-Item -ItemType Directory -Path $workdir -Force | Out-Null
}
if (-not (Test-Path -LiteralPath $ConfigPath)) {
throw "ConfigPath not found: $ConfigPath"
} }
if (-not (Test-Path -LiteralPath $TemplatePath)) { if (-not (Test-Path -LiteralPath $TemplatePath)) {
throw "TemplatePath not found: $TemplatePath" throw "TemplatePath not found: $TemplatePath"
} }
$pythonExe = Resolve-PythonCommand
$pythonArgs = @( $pythonArgs = @(
"$root\scripts\sync_top_products_assets.py", "$root\scripts\sync_top_products_assets.py",
"--config", "$ConfigPath", "--config", "$ConfigPath",
...@@ -38,15 +89,16 @@ if ($ReportYear -gt 0) { ...@@ -38,15 +89,16 @@ if ($ReportYear -gt 0) {
if ($CompareYear -gt 0) { if ($CompareYear -gt 0) {
$pythonArgs += @("--compare-year", "$CompareYear") $pythonArgs += @("--compare-year", "$CompareYear")
} }
$pythonArgs += @("--output-dir", "$workdir")
python @pythonArgs & $pythonExe @pythonArgs
if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -ne 0) {
throw "sync_top_products_assets.py failed with exit code $LASTEXITCODE" throw "sync_top_products_assets.py failed with exit code $LASTEXITCODE"
} }
if ($Render) { if ($Render) {
powershell -ExecutionPolicy Bypass -File "$root\bin\vip-report-render.ps1" -TemplatePath "$TemplatePath" -OutputPath "$OutputPath" -OperationsPath "$opsPath" | Out-Null powershell -ExecutionPolicy Bypass -File "$root\bin\vip-report-render.ps1" -TemplatePath "$TemplatePath" -OutputPath "$OutputPath" -OperationsPath "$opsPath" | Out-Null
python "$root\scripts\compare_pptx.py" "$TemplatePath" "$OutputPath" --output "$CompareOutputPath" & $pythonExe "$root\scripts\compare_pptx.py" "$TemplatePath" "$OutputPath" --output "$CompareOutputPath"
if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -ne 0) {
throw "compare_pptx.py failed with exit code $LASTEXITCODE" throw "compare_pptx.py failed with exit code $LASTEXITCODE"
} }
......
...@@ -5,15 +5,25 @@ param( ...@@ -5,15 +5,25 @@ param(
[int]$ReportYear = 0, [int]$ReportYear = 0,
[int]$CompareYear = 0, [int]$CompareYear = 0,
[switch]$Render, [switch]$Render,
[string]$OutputDir = "",
[string]$OutputPath = "C:\workspace\cursor\output\vip-report\generated-warehouse-100060-live.pptx", [string]$OutputPath = "C:\workspace\cursor\output\vip-report\generated-warehouse-100060-live.pptx",
[string]$CompareOutputPath = "C:\workspace\cursor\output\vip-report\generated-warehouse-100060-live.compare.json" [string]$CompareOutputPath = "C:\workspace\cursor\output\vip-report\generated-warehouse-100060-live.compare.json"
) )
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
$root = Split-Path -Parent $PSScriptRoot $root = Split-Path -Parent $PSScriptRoot
$workdir = "C:\workspace\cursor" $resolvedOutputDir = if ($OutputDir) { [System.IO.Path]::GetFullPath($OutputDir) } else { "C:\workspace\cursor\output\vip-report" }
$opsPath = "C:\workspace\cursor\output\vip-report\render-ops.warehouse-100060.live.json" $opsPath = Join-Path $resolvedOutputDir "render-ops.warehouse-100060.live.json"
$localConfigPath = Join-Path $root "config.yaml" $localConfigPath = Join-Path $root "config.yaml"
if (-not (Test-Path -LiteralPath $resolvedOutputDir)) {
New-Item -ItemType Directory -Path $resolvedOutputDir -Force | Out-Null
}
if ($OutputDir -and $OutputPath -eq "C:\workspace\cursor\output\vip-report\generated-warehouse-100060-live.pptx") {
$OutputPath = Join-Path $resolvedOutputDir "generated-warehouse-100060-live.pptx"
}
if ($OutputDir -and $CompareOutputPath -eq "C:\workspace\cursor\output\vip-report\generated-warehouse-100060-live.compare.json") {
$CompareOutputPath = Join-Path $resolvedOutputDir "generated-warehouse-100060-live.compare.json"
}
if (-not (Test-Path -LiteralPath $ConfigPath) -and (Test-Path -LiteralPath $localConfigPath)) { if (-not (Test-Path -LiteralPath $ConfigPath) -and (Test-Path -LiteralPath $localConfigPath)) {
$ConfigPath = $localConfigPath $ConfigPath = $localConfigPath
} }
...@@ -42,6 +52,7 @@ if ($ReportYear -gt 0) { ...@@ -42,6 +52,7 @@ if ($ReportYear -gt 0) {
if ($CompareYear -gt 0) { if ($CompareYear -gt 0) {
$pythonArgs += @("--compare-year", "$CompareYear") $pythonArgs += @("--compare-year", "$CompareYear")
} }
$pythonArgs += @("--output-dir", "$resolvedOutputDir")
python @pythonArgs python @pythonArgs
if ($Render) { if ($Render) {
......
tableau:
base_url: "http://tableau.charleskeith.cn"
username: "ec_user01"
password: "1!Cu&euRo17b6yu@R40gr7"
vip:
login_endpoint: ""
enabled: false
mysql:
host: "erp.charleskeith.cn"
port: 30004
database: "ckc_cep_db"
username: "allen"
password: "wangjun@123"
paths:
template_pptx: "./Report.pptx"
slide_source_map: "./output/slide-source-map.yaml"
workdir: "./output"
render:
mode: "com"
baseline_output: "./output/generated-baseline.pptx"
manifest_output: "./output/template-manifest.json"
inventory_output: "./output/shape-inventory.json"
compare_output: "./output/baseline-compare.json"
report:
# 月份支持输入中文月份(如:二月)或数字(如:2/02),脚本会统一处理。
month_cn: "一月"
year: 2026
compare_year: 2025
...@@ -7,11 +7,11 @@ description: Use when fetching or validating VIP report source data from Tableau ...@@ -7,11 +7,11 @@ description: Use when fetching or validating VIP report source data from Tableau
## Focus ## Focus
这个子 skill 负责“拿什么数据”: This skill covers source-data work before rendering:
- 读取配置中的 Tableau / MySQL / vip.com 登录入口 - read Tableau / MySQL / vip.com endpoints and credentials
- `slide-source-map.yaml` 的 source group 拉取数据 - fetch data by slide source group
- 把中间结果落成可复用的 JSON / CSV / 图片 - persist reusable intermediate JSON / CSV / image outputs
## Inputs ## Inputs
...@@ -24,14 +24,19 @@ description: Use when fetching or validating VIP report source data from Tableau ...@@ -24,14 +24,19 @@ description: Use when fetching or validating VIP report source data from Tableau
- `C:\workspace\cursor\output\vip-report\data\*.csv` - `C:\workspace\cursor\output\vip-report\data\*.csv`
- `C:\workspace\cursor\output\vip-report\assets\*.png` - `C:\workspace\cursor\output\vip-report\assets\*.png`
## Current Scope ## Guardrails
- 已确认的非 `vip.com` 页优先 - Read credentials from config or approved skill docs only.
- `S11` 仅实现已确认来源的部分 - Every fetched dataset must remain traceable to `slide-source-map.yaml`.
- `vip.com` 页保留接口位,等 `login_endpoint` 接入后再补 - Default test DB is `ckc_cep_db_test`.
- Default production MySQL for VIP-report validation:
`jdbc:mysql://erp.charleskeith.cn:30004/ckc_cep_db?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true`
username: `allen`
password: `wangjun@123`
- When the user explicitly asks for production verification, use the production MySQL above unless they override it.
## Guardrails ## Current Scope
- 账号密码只从配置文件读取 - Tableau-first validation and capture
- 所有数据都要能追溯回 `slide-source-map.yaml` - MySQL validation for report consistency checks
- 测试库默认使用 `ckc_cep_db_test` - `vip.com` kept as an extension point until `login_endpoint` is required
{
"replace_images": [
{
"slide": 2,
"shape_id": 5,
"shape_name": "图片 4",
"image_path": "C:\\workspace\\skills\\vip-report\\output\\assets\\monthly-sales\\s02_monthly_sales_chart.png"
},
{
"slide": 2,
"shape_id": 7,
"shape_name": "图片 6",
"image_path": "C:\\workspace\\skills\\vip-report\\output\\assets\\monthly-sales\\s02_monthly_sales_overview_table.png"
},
{
"slide": 2,
"shape_id": 8,
"shape_name": "图片 7",
"image_path": "C:\\workspace\\skills\\vip-report\\output\\assets\\monthly-sales\\s02_monthly_sales_summary_strip.png"
},
{
"slide": 3,
"shape_id": 2,
"shape_name": "图片 1",
"image_path": "C:\\workspace\\skills\\vip-report\\output\\assets\\monthly-sales\\s03_kpi_lfl_quad.png"
},
{
"slide": 4,
"shape_id": 2,
"shape_name": "图片 1",
"image_path": "C:\\workspace\\skills\\vip-report\\output\\assets\\inventory-monthly\\s04_category_overall_top.png"
},
{
"slide": 4,
"shape_id": 4,
"shape_name": "图片 3",
"image_path": "C:\\workspace\\skills\\vip-report\\output\\assets\\inventory-monthly\\s04_category_overall_mid.png"
},
{
"slide": 4,
"shape_id": 5,
"shape_name": "图片 4",
"image_path": "C:\\workspace\\skills\\vip-report\\output\\assets\\inventory-monthly\\s04_category_overall_bottom.png"
},
{
"slide": 5,
"shape_id": 2,
"shape_name": "图片 1",
"image_path": "C:\\workspace\\skills\\vip-report\\output\\assets\\inventory-monthly\\s05_bags_top.png"
},
{
"slide": 5,
"shape_id": 3,
"shape_name": "图片 2",
"image_path": "C:\\workspace\\skills\\vip-report\\output\\assets\\inventory-monthly\\s05_bags_bottom.png"
},
{
"slide": 6,
"shape_id": 3,
"shape_name": "图片 2",
"image_path": "C:\\workspace\\skills\\vip-report\\output\\assets\\inventory-monthly\\s06_shoes_top.png"
},
{
"slide": 6,
"shape_id": 7,
"shape_name": "图片 6",
"image_path": "C:\\workspace\\skills\\vip-report\\output\\assets\\inventory-monthly\\s06_shoes_bottom.png"
},
{
"slide": 7,
"shape_id": 3,
"shape_name": "图片 2",
"image_path": "C:\\workspace\\skills\\vip-report\\output\\assets\\inventory-monthly\\s07_discount.png"
},
{
"slide": 8,
"shape_id": 2,
"shape_name": "图片 1",
"image_path": "C:\\workspace\\skills\\vip-report\\output\\assets\\inventory-monthly\\s08_subcategory.png"
},
{
"slide": 10,
"shape_id": 2,
"shape_name": "Picture 1",
"image_path": "C:\\workspace\\skills\\vip-report\\output\\assets\\top-products\\s10_top_products_chart.png"
},
{
"slide": 10,
"shape_id": 3,
"shape_name": "Picture 2",
"image_path": "C:\\workspace\\skills\\vip-report\\output\\assets\\top-products\\s10_top_product_01_CK1-60361556.png"
},
{
"slide": 10,
"shape_id": 4,
"shape_name": "Picture 2",
"image_path": "C:\\workspace\\skills\\vip-report\\output\\assets\\top-products\\s10_top_product_02_CK1-60280377.png"
},
{
"slide": 10,
"shape_id": 6,
"shape_name": "Picture 4",
"image_path": "C:\\workspace\\skills\\vip-report\\output\\assets\\top-products\\s10_top_product_03_CK1-60361352.png"
},
{
"slide": 10,
"shape_id": 7,
"shape_name": "Picture 6",
"image_path": "C:\\workspace\\skills\\vip-report\\output\\assets\\top-products\\s10_top_product_04_CK1-70381169.png"
},
{
"slide": 10,
"shape_id": 8,
"shape_name": "Picture 8",
"image_path": "C:\\workspace\\skills\\vip-report\\output\\assets\\top-products\\s10_top_product_05_CK1-70920197.png"
},
{
"slide": 10,
"shape_id": 9,
"shape_name": "Picture 10",
"image_path": "C:\\workspace\\skills\\vip-report\\output\\assets\\top-products\\s10_top_product_06_CK1-61720177.png"
},
{
"slide": 10,
"shape_id": 10,
"shape_name": "Picture 12",
"image_path": "C:\\workspace\\skills\\vip-report\\output\\assets\\top-products\\s10_top_product_07_CK1-70381133.png"
},
{
"slide": 10,
"shape_id": 11,
"shape_name": "Picture 14",
"image_path": "C:\\workspace\\skills\\vip-report\\output\\assets\\top-products\\s10_top_product_08_CK1-60361613.png"
},
{
"slide": 10,
"shape_id": 13,
"shape_name": "Picture 16",
"image_path": "C:\\workspace\\skills\\vip-report\\output\\assets\\top-products\\s10_top_product_09_CK1-60280245-1.png"
},
{
"slide": 10,
"shape_id": 14,
"shape_name": "Picture 18",
"image_path": "C:\\workspace\\skills\\vip-report\\output\\assets\\top-products\\s10_top_product_10_CK1-70900382-2.png"
},
{
"slide": 13,
"shape_id": 5,
"shape_name": "图片 4",
"image_path": "C:\\workspace\\skills\\vip-report\\output\\assets\\warehouse-100060\\s13_top.png"
},
{
"slide": 13,
"shape_id": 6,
"shape_name": "图片 5",
"image_path": "C:\\workspace\\skills\\vip-report\\output\\assets\\warehouse-100060\\s13_left.png"
},
{
"slide": 13,
"shape_id": 10,
"shape_name": "图片 9",
"image_path": "C:\\workspace\\skills\\vip-report\\output\\assets\\warehouse-100060\\s13_mid.png"
}
],
"replace_text": [
]
}
...@@ -8,10 +8,12 @@ from pathlib import Path ...@@ -8,10 +8,12 @@ from pathlib import Path
def sha256_bytes(data: bytes) -> str: def sha256_bytes(data: bytes) -> str:
"""函数说明:封装sha256_bytes方法的核心处理流程。"""
return hashlib.sha256(data).hexdigest() return hashlib.sha256(data).hexdigest()
def package_index(path: Path) -> dict[str, str]: def package_index(path: Path) -> dict[str, str]:
"""函数说明:封装package_index方法的核心处理流程。"""
index: dict[str, str] = {} index: dict[str, str] = {}
with zipfile.ZipFile(path) as archive: with zipfile.ZipFile(path) as archive:
for name in sorted(archive.namelist()): for name in sorted(archive.namelist()):
...@@ -20,6 +22,7 @@ def package_index(path: Path) -> dict[str, str]: ...@@ -20,6 +22,7 @@ def package_index(path: Path) -> dict[str, str]:
def compare_packages(left: Path, right: Path) -> dict: def compare_packages(left: Path, right: Path) -> dict:
"""函数说明:封装compare_packages方法的核心处理流程。"""
left_index = package_index(left) left_index = package_index(left)
right_index = package_index(right) right_index = package_index(right)
...@@ -44,10 +47,12 @@ def compare_packages(left: Path, right: Path) -> dict: ...@@ -44,10 +47,12 @@ def compare_packages(left: Path, right: Path) -> dict:
def main() -> None: def main() -> None:
"""函数说明:脚本主入口,负责串联参数解析与执行流程。"""
parser = argparse.ArgumentParser(description="Compare two pptx packages entry-by-entry.") parser = argparse.ArgumentParser(description="Compare two pptx packages entry-by-entry.")
parser.add_argument("left", help="Left pptx file") parser.add_argument("left", help="Left pptx file")
parser.add_argument("right", help="Right pptx file") parser.add_argument("right", help="Right pptx file")
parser.add_argument("--output", help="Optional json output path") parser.add_argument("--output", help="Optional json output path")
# 关键行注释:解析命令行参数,生成本次运行配置。
args = parser.parse_args() args = parser.parse_args()
result = compare_packages(Path(args.left), Path(args.right)) result = compare_packages(Path(args.left), Path(args.right))
...@@ -60,5 +65,6 @@ def main() -> None: ...@@ -60,5 +65,6 @@ def main() -> None:
print(payload) print(payload)
# 关键行注释:脚本直接运行时,从这里进入主流程。
if __name__ == "__main__": if __name__ == "__main__":
main() main()
...@@ -17,15 +17,18 @@ NS = { ...@@ -17,15 +17,18 @@ NS = {
def natural_slide_number(name: str) -> int: def natural_slide_number(name: str) -> int:
"""函数说明:封装natural_slide_number方法的核心处理流程。"""
match = re.search(r"(\d+)", name) match = re.search(r"(\d+)", name)
return int(match.group(1)) if match else 0 return int(match.group(1)) if match else 0
def read_xml(root: Path, relative_path: str) -> etree._ElementTree: def read_xml(root: Path, relative_path: str) -> etree._ElementTree:
"""函数说明:封装read_xml方法的核心处理流程。"""
return etree.parse(str(root / relative_path)) return etree.parse(str(root / relative_path))
def rel_map(rel_tree: etree._ElementTree) -> dict[str, dict[str, str]]: def rel_map(rel_tree: etree._ElementTree) -> dict[str, dict[str, str]]:
"""函数说明:封装rel_map方法的核心处理流程。"""
mapping: dict[str, dict[str, str]] = {} mapping: dict[str, dict[str, str]] = {}
for node in rel_tree.xpath("//pr:Relationship", namespaces=NS): for node in rel_tree.xpath("//pr:Relationship", namespaces=NS):
mapping[node.get("Id")] = { mapping[node.get("Id")] = {
...@@ -36,6 +39,7 @@ def rel_map(rel_tree: etree._ElementTree) -> dict[str, dict[str, str]]: ...@@ -36,6 +39,7 @@ def rel_map(rel_tree: etree._ElementTree) -> dict[str, dict[str, str]]:
def chart_workbook_target(root: Path, chart_target: str) -> str | None: def chart_workbook_target(root: Path, chart_target: str) -> str | None:
"""函数说明:封装chart_workbook_target方法的核心处理流程。"""
chart_path = Path("ppt/slides") / chart_target chart_path = Path("ppt/slides") / chart_target
chart_tree = read_xml(root, str(chart_path)) chart_tree = read_xml(root, str(chart_path))
external = chart_tree.xpath("//c:externalData", namespaces=NS) external = chart_tree.xpath("//c:externalData", namespaces=NS)
...@@ -52,6 +56,7 @@ def chart_workbook_target(root: Path, chart_target: str) -> str | None: ...@@ -52,6 +56,7 @@ def chart_workbook_target(root: Path, chart_target: str) -> str | None:
def extract_manifest(unpacked_root: Path) -> dict: def extract_manifest(unpacked_root: Path) -> dict:
"""函数说明:封装extract_manifest方法的核心处理流程。"""
ppt_root = unpacked_root / "ppt" ppt_root = unpacked_root / "ppt"
slides_dir = ppt_root / "slides" slides_dir = ppt_root / "slides"
notes_dir = ppt_root / "notesSlides" notes_dir = ppt_root / "notesSlides"
...@@ -130,9 +135,11 @@ def extract_manifest(unpacked_root: Path) -> dict: ...@@ -130,9 +135,11 @@ def extract_manifest(unpacked_root: Path) -> dict:
def main() -> None: def main() -> None:
"""函数说明:脚本主入口,负责串联参数解析与执行流程。"""
parser = argparse.ArgumentParser(description="Extract a machine-readable manifest from an unpacked PPTX.") parser = argparse.ArgumentParser(description="Extract a machine-readable manifest from an unpacked PPTX.")
parser.add_argument("unpacked_root", help="Root directory of the unpacked pptx package") parser.add_argument("unpacked_root", help="Root directory of the unpacked pptx package")
parser.add_argument("--output", help="Optional output json path. Prints to stdout when omitted.") parser.add_argument("--output", help="Optional output json path. Prints to stdout when omitted.")
# 关键行注释:解析命令行参数,生成本次运行配置。
args = parser.parse_args() args = parser.parse_args()
manifest = extract_manifest(Path(args.unpacked_root)) manifest = extract_manifest(Path(args.unpacked_root))
...@@ -145,5 +152,6 @@ def main() -> None: ...@@ -145,5 +152,6 @@ def main() -> None:
print(payload) print(payload)
# 关键行注释:脚本直接运行时,从这里进入主流程。
if __name__ == "__main__": if __name__ == "__main__":
main() main()
...@@ -5,6 +5,8 @@ param( ...@@ -5,6 +5,8 @@ param(
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
# 关键行注释:这里是关键处理节点。
# 函数说明:封装Get-ShapeNode方法的核心处理流程。
function Get-ShapeNode { function Get-ShapeNode {
param( param(
[Parameter(Mandatory = $true)]$Shape, [Parameter(Mandatory = $true)]$Shape,
...@@ -12,6 +14,7 @@ function Get-ShapeNode { ...@@ -12,6 +14,7 @@ function Get-ShapeNode {
) )
$text = $null $text = $null
# 关键行注释:进入核心执行区,并在 finally 中释放 PowerPoint 资源。
try { try {
if ($Shape.HasTextFrame -eq -1 -and $Shape.TextFrame.HasText -eq -1) { if ($Shape.HasTextFrame -eq -1 -and $Shape.TextFrame.HasText -eq -1) {
$text = $Shape.TextFrame.TextRange.Text -replace "`r`n", " " $text = $Shape.TextFrame.TextRange.Text -replace "`r`n", " "
...@@ -24,6 +27,7 @@ function Get-ShapeNode { ...@@ -24,6 +27,7 @@ function Get-ShapeNode {
$chartCount = 0 $chartCount = 0
$imageCount = 0 $imageCount = 0
# 关键行注释:进入核心执行区,并在 finally 中释放 PowerPoint 资源。
try { try {
if ($Shape.HasChart -eq -1) { if ($Shape.HasChart -eq -1) {
$chartCount = 1 $chartCount = 1
...@@ -35,6 +39,7 @@ function Get-ShapeNode { ...@@ -35,6 +39,7 @@ function Get-ShapeNode {
$imageCount = 1 $imageCount = 1
} }
# 关键行注释:进入核心执行区,并在 finally 中释放 PowerPoint 资源。
try { try {
$groupCount = $Shape.GroupItems.Count $groupCount = $Shape.GroupItems.Count
if ($groupCount -gt 0) { if ($groupCount -gt 0) {
...@@ -49,6 +54,7 @@ function Get-ShapeNode { ...@@ -49,6 +54,7 @@ function Get-ShapeNode {
} }
$hasTable = $false $hasTable = $false
# 关键行注释:进入核心执行区,并在 finally 中释放 PowerPoint 资源。
try { try {
$hasTable = ($Shape.HasTable -eq -1) $hasTable = ($Shape.HasTable -eq -1)
} catch { } catch {
...@@ -77,6 +83,7 @@ function Get-ShapeNode { ...@@ -77,6 +83,7 @@ function Get-ShapeNode {
$ppt = $null $ppt = $null
$pres = $null $pres = $null
# 关键行注释:进入核心执行区,并在 finally 中释放 PowerPoint 资源。
try { try {
$ppt = New-Object -ComObject PowerPoint.Application $ppt = New-Object -ComObject PowerPoint.Application
$pres = $ppt.Presentations.Open($TemplatePath, -1, 0, 0) $pres = $ppt.Presentations.Open($TemplatePath, -1, 0, 0)
...@@ -94,6 +101,7 @@ try { ...@@ -94,6 +101,7 @@ try {
} }
$title = $null $title = $null
# 关键行注释:进入核心执行区,并在 finally 中释放 PowerPoint 资源。
try { try {
if ($slide.Shapes.HasTitle -eq -1) { if ($slide.Shapes.HasTitle -eq -1) {
$title = $slide.Shapes.Title.TextFrame.TextRange.Text -replace "`r`n", " " $title = $slide.Shapes.Title.TextFrame.TextRange.Text -replace "`r`n", " "
......
...@@ -6,9 +6,11 @@ from pathlib import Path ...@@ -6,9 +6,11 @@ from pathlib import Path
def main() -> None: def main() -> None:
"""函数说明:脚本主入口,负责串联参数解析与执行流程。"""
parser = argparse.ArgumentParser(description="Create a byte-identical baseline render by copying the PPTX template.") parser = argparse.ArgumentParser(description="Create a byte-identical baseline render by copying the PPTX template.")
parser.add_argument("template", help="Template pptx path") parser.add_argument("template", help="Template pptx path")
parser.add_argument("output", help="Output pptx path") parser.add_argument("output", help="Output pptx path")
# 关键行注释:解析命令行参数,生成本次运行配置。
args = parser.parse_args() args = parser.parse_args()
template = Path(args.template) template = Path(args.template)
...@@ -18,5 +20,6 @@ def main() -> None: ...@@ -18,5 +20,6 @@ def main() -> None:
print(output) print(output)
# 关键行注释:脚本直接运行时,从这里进入主流程。
if __name__ == "__main__": if __name__ == "__main__":
main() main()
...@@ -6,9 +6,12 @@ param( ...@@ -6,9 +6,12 @@ param(
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
# 关键行注释:这里是关键处理节点。
# 函数说明:封装Get-ShapeChildren方法的核心处理流程。
function Get-ShapeChildren { function Get-ShapeChildren {
param([Parameter(Mandatory = $true)]$Shape) param([Parameter(Mandatory = $true)]$Shape)
$children = @() $children = @()
# 关键行注释:进入核心执行区,并在 finally 中释放 PowerPoint 资源。
try { try {
$count = $Shape.GroupItems.Count $count = $Shape.GroupItems.Count
for ($i = 1; $i -le $count; $i++) { for ($i = 1; $i -le $count; $i++) {
...@@ -21,6 +24,8 @@ function Get-ShapeChildren { ...@@ -21,6 +24,8 @@ function Get-ShapeChildren {
return $children return $children
} }
# 关键行注释:这里是关键处理节点。
# 函数说明:封装Find-Shape方法的核心处理流程。
function Find-Shape { function Find-Shape {
param( param(
[Parameter(Mandatory = $true)]$Slide, [Parameter(Mandatory = $true)]$Slide,
...@@ -46,6 +51,7 @@ function Find-Shape { ...@@ -46,6 +51,7 @@ function Find-Shape {
} }
$text = $null $text = $null
# 关键行注释:进入核心执行区,并在 finally 中释放 PowerPoint 资源。
try { try {
if ($shape.HasTextFrame -eq -1 -and $shape.TextFrame.HasText -eq -1) { if ($shape.HasTextFrame -eq -1 -and $shape.TextFrame.HasText -eq -1) {
$text = $shape.TextFrame.TextRange.Text $text = $shape.TextFrame.TextRange.Text
...@@ -66,6 +72,8 @@ function Find-Shape { ...@@ -66,6 +72,8 @@ function Find-Shape {
return $null return $null
} }
# 关键行注释:这里是关键处理节点。
# 函数说明:封装Set-ShapeText方法的核心处理流程。
function Set-ShapeText { function Set-ShapeText {
param( param(
[Parameter(Mandatory = $true)]$Shape, [Parameter(Mandatory = $true)]$Shape,
...@@ -79,6 +87,8 @@ function Set-ShapeText { ...@@ -79,6 +87,8 @@ function Set-ShapeText {
$Shape.TextFrame.TextRange.Text = $NewText $Shape.TextFrame.TextRange.Text = $NewText
} }
# 关键行注释:这里是关键处理节点。
# 函数说明:封装Replace-PictureShape方法的核心处理流程。
function Replace-PictureShape { function Replace-PictureShape {
param( param(
[Parameter(Mandatory = $true)]$Slide, [Parameter(Mandatory = $true)]$Slide,
...@@ -102,6 +112,7 @@ function Replace-PictureShape { ...@@ -102,6 +112,7 @@ function Replace-PictureShape {
while ($newShape.ZOrderPosition -lt $z) { while ($newShape.ZOrderPosition -lt $z) {
$newShape.ZOrder(1) | Out-Null $newShape.ZOrder(1) | Out-Null
} }
# 关键行注释:进入核心执行区,并在 finally 中释放 PowerPoint 资源。
try { try {
$newShape.Name = $name $newShape.Name = $name
} catch { } catch {
...@@ -159,6 +170,7 @@ if ($replaceText.Count -eq 0 -and $replaceTables.Count -eq 0 -and $replaceCharts ...@@ -159,6 +170,7 @@ if ($replaceText.Count -eq 0 -and $replaceTables.Count -eq 0 -and $replaceCharts
$ppt = $null $ppt = $null
$pres = $null $pres = $null
# 关键行注释:进入核心执行区,并在 finally 中释放 PowerPoint 资源。
try { try {
$ppt = New-Object -ComObject PowerPoint.Application $ppt = New-Object -ComObject PowerPoint.Application
$pres = $ppt.Presentations.Open($OutputPath, 0, 0, 0) $pres = $ppt.Presentations.Open($OutputPath, 0, 0, 0)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment