Community研究與資料分析github.com

fahadyousuff/pbix-dashboard-skill

以程式方式在 Power BI Desktop .pbix 檔案(PBIR Enhanced Report Format)中新增或編輯視覺效果、頁面和視覺屬性,且不會造成檔案損毀。

pbix-dashboard-skill 是什麼?

此技能專為需自動化處理 Power BI Desktop .pbix 檔案的開發者與 AI 代理設計。它支援以程式方式修改 PBIR Enhanced Report Format,包括新增頁面、新增或編輯視覺效果、調整版面配置,以及重新繫結欄位。 技能的核心價值在於解決手動編輯 .pbix 檔案時常見的致命錯誤,例如 `MashupValidationError`、`Invalid SecurityBindings`,以及因 BOM(位元組順序標記)或 ZIP 條目標題格式不當導致的「檔案損毀」問題。 文件詳細說明了僅使用 .NET 的 `ZipArchive`(透過 PowerShell)處理檔案的必要性,因為 Python 的 `zipfile` 無法設定正確的外部屬性。此外,還提供了如何安全地移除 `SecurityBindings`(Power BI Desktop 會在下一次儲存時自動重新產生)、如何以無 BOM 的 UTF-8 編碼寫入 JSON 檔案,以及如何驗證最終 .pbix 檔案是否有效的明確步驟。 該技能兼容 Claude Code、Cursor、Codex 等支援執行 PowerShell 指令碼和 DAX 查詢的 AI 代理平台,為 CI/CD 流程、批次範本生成和資料驅動報表建立提供了可靠的基礎。

相容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/fahadyousuff/pbix-dashboard-skill/tree/HEAD

Installed? Explore more 研究與資料分析 skills: affaan-m/uspto-database, affaan-m/scholar-evaluation, affaan-m/literature-review · View all 6 →

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

PBIX 儀表板修補程式

原地編輯 Power BI Desktop .pbix 檔案:新增頁面、新增視覺效果、變更版面配置、重新繫結欄位 — 然後重新打包,讓 Power BI Desktop 開啟時不會出現損毀錯誤。

使用時機

  • 使用者有儲存好的 .pbix 檔案,並希望新增視覺效果或產生儀表板。
  • 使用者想要自動化 Power BI 報表編輯(CI、批次範本、資料驅動版面配置)。
  • 先前的編輯嘗試產生 MashupValidationError、Invalid SecurityBindings,或開啟時顯示「檔案損毀」。

核心問題(請先閱讀)

現代的 .pbix 檔案使用 PBIR Enhanced Report Format:報表版面配置儲存為每個視覺效果的 JSON 檔案,位於 Report/definition/pages/<pageId>/visuals/<visualId>/visual.json,而不是舊的單一檔案 Report/Layout。

PBIX 是一個 ZIP 壓縮檔,包含(以及其他檔案):

  • DataModel — Analysis Services 模型(必須不重新壓縮;原樣儲存)。
  • Report/definition/... — PBIR 每視覺效果 JSON 樹狀結構。
  • SecurityBindings — 一個 DPAPI 加密的操作清單,用來簽署檔案版面配置。
  • [Content_Types].xml — 列出 ZIP 中的每個檔案。

導致檔案損毀的陷阱(從經驗中學習):

  1. Python 的 zipfile 設定與 .NET 的 ZipArchive 不同的外部檔案屬性。 Power BI 會驗證這些屬性。請使用 .NET 的 ZipArchive 透過 PowerShell,並將每個條目的 ExternalAttributes 設為 0。
  2. 重新壓縮 DataModel 會破壞它。 它必須使用與來源相同的壓縮方法(通常是 Deflate,但有時是 Stored)來儲存;最安全的方式是如果重新寫入則使用 NoCompression,否則保留原始位元組不變。
  3. 新增任何檔案都會破壞 SecurityBindings。 它是一個已簽署的操作清單,列出了 ZIP 中的內容。如果您新增 Report/definition/pages/<new>/visuals/.../visual.json,簽章將不再匹配,PBI 將拒絕開啟。
  4. 條目名稱不得有前導 /。 開啟時出現 MissingBIPackagePart: /Version 表示您的 Version 條目被壓縮為 /Version(前導斜線)。PowerShell 的 Substring($src.Length + 1) 可能會留下前導反斜線 → 在 zip 中變成 / → PBI 拒絕。從解析的長路徑計算前綴,並對每個條目名稱執行 TrimStart('/')。
  5. Report/definition/**/*.json 中的 UTF-8 BOM 是致命的。 PBI 錯誤:Only text with UTF8 encoding without BOM (byte order marks) is supported. Detected BOM: 'UTF-8'。在 Windows PowerShell 5.1 / .NET Framework 中,Set-Content -Encoding UTF8 和 File.WriteAllText(未指定明確編碼)都會寫入 BOM。始終使用傳遞給 WriteAllText 或 StreamWriter 的 New-Object System.Text.UTF8Encoding($false) 來寫入 JSON。

修復 SecurityBindings 的方法: 從 ZIP 中移除它,並從 [Content_Types].xml 中刪除其條目。Power BI Desktop 將在下一次儲存時重新產生 SecurityBindings。檔案將可正常開啟。

程序

1. 檢查檔案

# 使用 .NET 解壓縮到工作目錄,使屬性乾淨
Add-Type -AssemblyName System.IO.Compression.FileSystem
[System.IO.Compression.ZipFile]::ExtractToDirectory("Test.pbix", "pbix_src")

檢查它是 PBIR(新版)還是舊版:

  • PBIR:有 Report\definition\pages\<pageId>\visuals\<visualId>\visual.json。
  • 舊版:只有 Report\Layout(二進位 JSON 字串)。此技能以 PBIR 為目標。

2. 了解 visual.json 結構(版本 2.12.0)

最小卡片視覺效果(KPI 磚):

{
  "$schema": "https://developer.microsoft.com/json-schemas/fabric/item/report/definition/visualContainer/2.12.0/schema.json",
  "name": "<20 個十六進位字元的 ID>",
  "position": {"x": 20, "y": 20, "z": 0, "height": 160, "width": 440, "tabOrder": 0},
  "visual": {
    "visualType": "cardVisual",
    "query": {
      "queryState": {
        "Data": {
          "projections": [{
            "field": {
              "Aggregation": {
                "Expression": {"Column": {"Expression": {"SourceRef": {"Entity": "financials"}}, "Property": "Sales"}},
                "Function": 0
              }
            },
            "queryRef": "Sum(financials.Sales)",
            "nativeQueryRef": "Sum of Sales",
            "active": true
          }]
        }
      },
      "sortDefinition": {
        "sort": [{"field": {"Aggregation": {"Expression": {"Column": {"Expression": {"SourceRef": {"Entity": "financials"}}, "Property": "Sales"}}, "Function": 0}}, "direction": "Descending"}],
        "isDefaultSort": true
      }
    },
    "drillFilterOtherVisuals": true
  }
}

關鍵事實:

  • name 是一個 20 個字元的十六進位 ID;必須在頁面內唯一。
  • visualType 範例:cardVisual、clusteredBarChart、clusteredColumnChart、lineChart、donutChart、pieChart、tableEx、pivotTable。
  • cardVisual 使用角色 Data。長條圖/直方圖/折線圖使用 Category + Y(+ 可選的 Series)。甜甜圈圖/圓餅圖使用 Category + Y。
  • Aggregation.Function:0=Sum、1=Avg、2=Count、3=Min、4=Max、5=CountNonNull、6=Median、7=StDev、8=Variance。
  • drillFilterOtherVisuals: true 是必需的;省略它會損毀檔案。

3. 註冊頁面

Report/definition/pages/pages.json:

{"$schema":"...pagesMetadata/1.1.0/schema.json","pageOrder":["<pageId1>","<pageId2>"],"activePageName":"<pageIdX>"}

每個頁面資料夾都有一個 page.json:

{"$schema":"...page/2.1.0/schema.json","name":"<pageId>","displayName":"Dashboard","displayOption":"FitToPage","height":1080,"width":1920}

4. 寫入 JSON 檔案 — 無 BOM 的 UTF-8

每次編輯 visual.json、page.json、report.json 或 Report/definition/ 下的任何檔案時,請使用無 BOM 的方式寫入。Windows PowerShell 5.1 預設會新增 BOM。

$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
[System.IO.File]::WriteAllText($path, $json, $utf8NoBom)

要移除先前錯誤寫入的 BOM:

$b = [System.IO.File]::ReadAllBytes($path)
if ($b.Length -ge 3 -and $b[0] -eq 0xEF -and $b[1] -eq 0xBB -and $b[2] -eq 0xBF) {
    $text = [System.Text.Encoding]::UTF8.GetString($b, 3, $b.Length - 3)
    [System.IO.File]::WriteAllText($path, $text, (New-Object System.Text.UTF8Encoding($false)))
}

5. 重新打包 — 唯一可靠的方法

不要使用 Python 的 zipfile。請使用 PowerShell 搭配 .NET 的 ZipArchive。關鍵細節:

  • 條目名稱使用正斜線,且沒有前導 /。
  • 從 (Get-Item $src).FullName.TrimEnd('\').Length + 1 計算來源前綴長度(與 Get-ChildItem 項目上 FullName 的大小寫/長路徑形式相同)。對每個條目名稱執行 TrimStart('/') 作為保險。
  • 每個文字條目(包括 [Content_Types].xml)都使用無 BOM 的 UTF-8 StreamWriter 寫入。
Add-Type -AssemblyName System.IO.Compression

$src = "pbix_src"
$dst = "Test_patched.pbix"
if (Test-Path $dst) { Remove-Item $dst }

$srcFull = (Get-Item $src).FullName.TrimEnd('\')
$prefixLen = $srcFull.Length + 1
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)

$fs = [System.IO.File]::Open($dst, 'Create')
$zip = New-Object System.IO.Compression.ZipArchive($fs, [System.IO.Compression.ZipArchiveMode]::Create)

Get-ChildItem $src -Recurse -File | ForEach-Object {
    $full = $_.FullName
    $rel = $full.Substring($prefixLen).Replace('\','/').TrimStart('/')

    # 移除 SecurityBindings — PBI 會在儲存時重新產生
    if ($rel -eq 'SecurityBindings') { return }

    # 編輯 Content_Types.xml 以移除 SecurityBindings 參考
    if ($rel -eq '[Content_Types].xml') {
        $xml = [System.IO.File]::ReadAllText($full) -replace '<Override[^/]*PartName="/SecurityBindings"[^/]*/>',''
        $entry = $zip.CreateEntry($rel, [System.IO.Compression.CompressionLevel]::Optimal)
        $entry.ExternalAttributes = 0
        $sw = New-Object System.IO.StreamWriter($entry.Open(), $utf8NoBom)
        $sw.Write($xml); $sw.Close()
        return
    }

    # DataModel 必須不重新壓縮
    $level = if ($rel -eq 'DataModel') {
        [System.IO.Compression.CompressionLevel]::NoCompression
    } else {
        [System.IO.Compression.CompressionLevel]::Optimal
    }

    $entry = $zip.CreateEntry($rel, $level)
    $entry.ExternalAttributes = 0   # 關鍵 — Python 會將其設為非零值,PBI 拒絕
    $es = $entry.Open()
    $sfs = [System.IO.File]::OpenRead($full)
    $sfs.CopyTo($es)
    $sfs.Close(); $es.Close()
}
$zip.Dispose(); $fs.Close()

6. 開啟前驗證

四個檢查,依序執行:

Add-Type -AssemblyName System.IO.Compression.FileSystem
$z = [System.IO.Compression.ZipFile]::OpenRead("Test_patched.pbix")

# a) 必要的部分存在,SecurityBindings 不存在
$z.Entries | Where-Object { $_.FullName -match '^(Version|DataModel|Metadata|Settings|SecurityBindings|\[Content_Types\]\.xml)$' } |
    Select-Object FullName, Length

# b) 沒有條目有前導 '/'
$z.Entries | Where-Object { $_.FullName.StartsWith('/') } | ForEach-Object { "錯誤:$($_.FullName)" }

# c) 任何文字元資料或 Report/definition JSON 中沒有 BOM
foreach ($e in $z.Entries) {
    if ($e.FullName -match '\.(json|xml)$|^Version$|^Metadata$|^Settings$') {
        $s = $e.Open(); $ms = New-Object System.IO.MemoryStream; $s.CopyTo($ms); $s.Close()
        $b = $ms.ToArray()
        if ($b.Length -ge 3 -and $b[0] -eq 0xEF -and $b[1] -eq 0xBB -and $b[2] -eq 0xBF) {
            Write-Host "找到 BOM:$($e.FullName)"
        }
    }
}

# d) 每個 Report/definition JSON 都能解析
foreach ($e in $z.Entries) {
    if ($e.FullName -like 'Report/definition/*.json' -or $e.FullName -like 'Report/definition/**/*.json') {
        $s = $e.Open(); $sr = New-Object System.IO.StreamReader($s)
        $txt = $sr.ReadToEnd(); $sr.Close()
        try { $null = $txt | ConvertFrom-Json } catch { "無效的 JSON:$($e.FullName) — $($_.Exception.Message)" }
    }
}
$z.Dispose()

預期:DataModel、Version、Metadata、Settings、[Content_Types].xml 存在;SecurityBindings 不存在;無前導斜線;無 BOM;所有 JSON 皆可解析。

連接到執行中的 Power BI Desktop 工作階段(可選 — 用於資料探索)

如果 PBI Desktop 已在執行中,它會在 localhost 上公開一個 Analysis Services 端點:

# 發現連接埠
Get-Process msmdsrv | ForEach-Object {
    $port = Get-NetTCPConnection -OwningProcess $_.Id -State Listen |
            Where-Object { $_.LocalAddress -eq '127.0.0.1' } |
            Select-Object -First 1 -ExpandProperty LocalPort
    Write-Host "Analysis Services 連接埠:$port"
}

然後透過 ADOMD 連接(安裝 Power BI Desktop;DLL 隨之提供):

# pip install pyadomd pythonnet
import clr
clr.AddReference(r"C:\Program Files\Microsoft Power BI Desktop\bin\Microsoft.PowerBI.AdomdClient.dll")
from Microsoft.AnalysisServices.AdomdClient import AdomdConnection  # 使用真實的命名空間,而非檔名

conn = AdomdConnection(f"Data Source=localhost:{port}")
conn.Open()
# 執行 DAX:EVALUATE SUMMARIZECOLUMNS(financials[Segment], "Sales", SUM(financials[Sales]))

僅用於讀取結構和預覽數字;最終的視覺效果會以 JSON 檔案寫入,並按照上述步驟重新打包。

設計視覺效果樣式 — 實際有效的屬性名稱

Power BI Desktop 中的格式化窗格標籤不匹配 JSON 屬性名稱。如果您猜測,PBI 會在下一次儲存時靜默地移除錯誤的屬性 — 檔案可以正常開啟,但您的樣式會消失。始終先在 PBI 中建立一次真實的變更、儲存、解壓縮,然後從儲存的 JSON 中複製屬性名稱。

樣式在 visual.json 中的位置

  • visual.visualContainerObjects — 視覺效果磚的外部框架(背景、邊框、投影、標題列)。適用於每種視覺效果類型。
  • visual.objects — 視覺效果的內部(繪圖區域、座標軸、圖例、資料點、卡片值、卡片內部填色)。此處的屬性名稱因視覺效果類型而異,且必須與佈景主題的 visualStyles[visualType] 結構匹配。

容器框架(適用於所有視覺效果)

"visualContainerObjects": {
  "background": [{"properties":{
    "color":{"solid":{"color":{"expr":{"Literal":{"Value":"'#EEF2FF'"}}}}},
    "transparency":{"expr":{"Literal":{"Value":"0D"}}}
  }}],
  "border": [{"properties":{
    "show":{"expr":{"Literal":{"Value":"true"}}},
    "color":{"solid":{"color":{"expr":{"Literal":{"Value":"'#E2E8F0'"}}}}},
    "radius":{"expr":{"Literal":{"Value":"12D"}}}
  }}],
  "dropShadow": [{"properties":{
    "show":{"expr":{"Literal":{"Value":"true"}}},
    "color":{"solid":{"color":{"expr":{"Literal":{"Value":"'#0F172A'"}}}}},
    "transparency":{"expr":{"Literal":{"Value":"85D"}}},
    "position":{"expr":{"Literal":{"Value":"'Outer'"}}},
    "preset":{"expr":{"Literal":{"Value":"'BottomRight'"}}}
  }}]
}

cardVisual(KPI 磚)— 陷阱

大數字不是 callout.color 也不是 value.color。PBI 會靜默地忽略兩者。

  • 大字型顏色:value.fontColor(不是 color)。
  • 大字型字體/大小/粗細:value.fontFamily、value.fontSize、value.bold。
  • 數字下方的類別標籤:label.fontColor、label.fontFamily、label.fontSize。
  • 數字後面的色調填色:cardCalloutArea.backgroundFillColor,並帶有一個 selector,用於命名量值投影的中繼資料 — 例如 "selector":{"metadata":"Sum(financials.Profit)"}。從 visual.query.queryState.Data.projections[0].queryRef 讀取該字串。
"objects": {
  "value": [{"properties":{
    "fontColor":{"solid":{"color":{"expr":{"Literal":{"Value":"'#4F46E5'"}}}}},
    "fontFamily":{"expr":{"Literal":{"Value":"'Segoe UI Semibold'"}}},
    "fontSize":{"expr":{"Literal":{"Value":"40D"}}},
    "bold":{"expr":{"Literal":{"Value":"true"}}}
  }}],
  "label": [{"properties":{
    "fontColor":{"solid":{"color":{"expr":{"Literal":{"Value":"'#64748B'"}}}}}
  }}],
  "cardCalloutArea": [{
    "properties": {
      "show":{"expr":{"Literal":{"Value":"true"}}},
      "backgroundFillColor":{"solid":{"color":{"expr":{"Literal":{"Value":"'#E0E7FF'"}}}}}
    },
    "selector":{"metadata":"Sum(financials.Sales)"}
  }]
}

長條圖 / 直方圖 / 折線圖 / 甜甜圈圖 — 資料顏色

  • dataPoint.defaultColor — 備用色

相關技能