mcpField
E
Excel MCP Server (FsOpenXmlDsl)

Excel MCP Server (FsOpenXmlDsl)

本地服务
github.com生产力96 周前更新

关于 Excel MCP Server (FsOpenXmlDsl)

Excel MCP Server (FsOpenXmlDsl) 通过本地 MCP 服务,让 AI Agent 读写和转换 Excel 工作簿。它可将工作簿反编译为 F#、C#、XML 或 JSON,也可从 XML 或 JSON 构建工作簿,支持双向转换流程。

主要功能

  1. 01读写 Excel 工作簿
  2. 02将工作簿反编译为 F# 或 C#
  3. 03将工作簿转换为 XML 或 JSON
  4. 04从 XML 或 JSON 创建工作簿
  5. 05通过 CLI 转换和构建

适用场景

  1. 01将现有工作簿转为源码检查
  2. 02根据 XML 或 JSON 生成工作簿
  3. 03将电子表格重建为 F# 或 C# 脚本

MCP 相关

本地 MCP 服务将工作簿读写、代码生成及 XML/JSON 转换作为工具提供给兼容 MCP 的 Agent。同样的转换能力也可通过 `fsopenxmldsl-mcp convert` 和 `build` 命令使用。

配置

本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。

暂未收录安装方式

我们还没有获取到这个服务的安装包或远程地址,请按项目 README 中的说明安装。

代码仓库
安装包
nugetKookerella.FsOpenXmlDsl.Mcp0.6.4

工具

还没有获取到工具列表

本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。

详细介绍

Kookerella.FsOpenXmlDsl

一个类型安全的 F# DSL,用于构建 Excel 工作簿,并将其解释为对 DocumentFormat.OpenXml SDK 的调用。该 DSL 是一个纯数据模型(具有结构相等性的记录/DUs)——解释器(Writer)将其编译为 OOXML,反向转换器(Reader)则将现有 .xlsx 解析回同一个 DSL。

有关哪些 SpreadsheetML 功能可以一对一映射、哪些只能近似映射,以及哪些尚未建模,请参阅 MAPPING.md。

该工具支持双向往返转换,而大多数 Excel 库(EPPlus、ClosedXML、NPOI 等)都不支持这一点:它们提供命令式 API,让你从头构建工作簿或修改现有工作簿,却无法将现有文件转换回可读的源代码。在这里,Reader 会将真实的 .xlsx/.xlsm 解析回同一个 DSL,而 Workbook.generateScript(F#)/CsCodeGen.Generate(C#)还能更进一步,将该模型重新渲染为独立脚本,以重建等效文件——这是真正的电子表格反编译器,而不只是写入器。另外两种接口 Xml.ofWorkbook/Xml.toWorkbook(见下文 "## XML")和 Json.ofWorkbook/Json.toWorkbook(见下文 "## JSON"),可以基于真实架构在普通 XML 或 JSON 与工作簿之间进行同样的转换——适合更愿意生成或使用数据而非编写代码的调用方,例如通过 XSLT 流程生成报告。Kookerella.FsOpenXmlDsl.Mcp 将所有四种方向都作为 MCP 工具(generate_fsharp_script/generate_csharp_script/generate_xml/create_workbook_from_xml/generate_json/create_workbook_from_json)提供给 AI Agent,也为其他用户提供 fsopenxmldsl-mcp convert/build CLI 命令——无需编写代码,就能试用你已有的任意电子表格:

dotnet tool install -g Kookerella.FsOpenXmlDsl.Mcp
fsopenxmldsl-mcp convert your-file.xlsx --lang csharp

尚未安装 .NET?请从 Releases 页面下载适用于你平台的独立构建版本——无需安装。

演示

上述“反编译后扩展”工作流的完整示例——将同一个发票模板逆向工程为 C#、F#、XSLT 转换和纯 JSON 生成脚本,并为每种方式接入真实数据和真实测试,以证明结果始终符合架构——位于配套仓库中: Kookerella.Demo.DecompileToSource。

布局

  • src/Kookerella.FsOpenXmlDsl — 库。
    • Reference.fs — CellRef 和 "A1" 样式的地址转换。
    • Styles.fs — 单元格格式:Color、FontStyle、FillStyle、BorderStyle、 AlignmentStyle、NumberFormat、CellProtection、CellStyle。
    • Validation.fs — 条件格式和数据验证:ComparisonOperator (两者共用)、ConditionalFormatRule、ValidationKind、ValidationAlert,以及存储在 Worksheet 上的 ConditionalFormatEntry/DataValidationEntry 记录。
    • Hyperlinks.fs — HyperlinkTarget(外部 URL/mailto: 与内部同工作簿引用)以及存储在 Worksheet 上的 HyperlinkEntry 记录。
    • Comments.fs — CommentEntry(经典单元格批注,即当前 Excel 中的“备注”——现代线程式批注的缺口见 MAPPING.md)。
    • Protection.fs — SheetProtection,即存储在 Worksheet 上的工作表级保护标志(与用于单元格级锁定的 CellStyle.Protection 配合使用),以及存储在 Workbook 上的工作簿级结构/窗口保护标志 WorkbookProtection。
    • DefinedNames.fs — DefinedNameScope/DefinedNameEntry,存储在 Workbook 而非 Worksheet 上——这是唯一真正属于工作簿级别的 DSL 概念。
    • PageSetup.fs — 打印设置:PageOrientation、PaperSize、PrintScaling、 PageMargins,以及存储在 Worksheet 上的 PageSetup 记录。
    • Tables.fs — Excel 表格:TableColumn、TableStyle,以及作为列表存储在 Worksheet 上的 TableEntry 记录(一个工作表可以有多个表格)。
    • Sparklines.fs — 单元格内迷你图:SparklineType、SparklineStyle、 SparklineCell,以及作为列表存储在 Worksheet 上的 SparklineGroupEntry 记录 (一个工作表可以有多个样式各异的组)。
    • Charts.fs — 柱形图/条形图/折线图/饼图:ChartType、ChartSeries,以及作为列表存储在 Worksheet 上的 ChartEntry 记录(一个工作表可以有多个图表)。
    • Images.fs — 栅格图像:ImageFormat 和 ImageEntry 记录(原始文件字节及单元格区域锚点), 作为列表存储在 Worksheet 上。
    • PivotTables.fs — PivotAggregation 和 PivotTableEntry 记录(源区域、行/列/值字段、锚点单元格), 作为列表存储在 Worksheet 上。
    • Model.fs — CellValue、Cell、Worksheet、Workbook(包括 Workbook. VbaProject,即启用宏的工作簿中的原始 vbaProject.bin 字节——参见其自身的文档注释;没有单独的 Macros.fs,因为它只是一个不透明字段,而不是新类型)。
    • Xml.fs / Xml.xsd — XML 接口:Xml.toWorkbook/Xml.ofWorkbook 在 Workbook 与 XElement 树之间进行转换,Xml.schemaSet() 则加载配套架构(作为资源嵌入程序集),用于验证任一方向的转换。请参见下文 "## XML"。
    • Json.fs — JSON 接口:Json.toWorkbook/Json.ofWorkbook 在 Workbook 与 System.Text.Json.Nodes.JsonObject 树之间进行转换,涵盖与 Xml.fs 相同的工作表/工作簿级功能集。 架构验证(Json.schema.json)仅用于测试套件,不属于公共 API——请参见下文 "## JSON"。
    • Builders.fs — 便捷辅助工具:用于构造规范模型的纯函数式构造器(cellA1 等),以及 SheetItem/CellEntry 类型(每种类型都只有一个带可选字段的简单 DU 案例)和 sheet 折叠函数—— 一个小型树状“用于构建工作表的 AST”(单元格行,以及列宽、合并、条件格式、数据验证、超链接、批注、自动筛选和 工作表保护等工作表级信息),其结构映射了 SpreadsheetML 本身的嵌套方式。实际编写时使用的是 SheetDsl:cell/row/autoFilter/conditionalFormat/ dataValidation/hyperlink/comment 成员,带有实际的可选参数(?col、 ?style、?index、数据验证警告字段、?tooltip、?author)——无需构造器对象,无需单独的“带样式”函数, 常见情形也无需充斥 None。(Protect 是唯一没有智能构造器的 SheetItem 案例—— SheetProtection 是普通记录,按常规 F# 方式构造:{ SheetProtection.Default with ... }。)
    • Interpreter/StyleRegistry.fs — 将字体/填充/边框/数字格式合并到共享的 OOXML 样式表中(内部实现)。
    • Interpreter/ChartWriter.fs / ChartReader.fs — 图表自身 DSL ↔ DrawingML/ChartML 转换;鉴于此功能的 OOXML 接口远比其他所有功能加起来都庞大,因此从 Writer.fs/Reader.fs 中拆分出来(内部实现)。
    • Interpreter/ImageWriter.fs / ImageReader.fs — 图像自身 DSL ↔ DrawingML 转换(内部实现)。
    • Interpreter/DrawingWriter.fs / DrawingReader.fs — 管理工作表在包含图表和/或图像时所用的唯一 DrawingsPart/<drawing> 关系,因为这两种功能共用同一个绘图画布,而非各自管理(内部实现)。
    • Interpreter/PivotTableWriter.fs / PivotTableReader.fs — 数据透视表自身的分组 + 聚合引擎以及 DSL ↔ OOXML 转换(pivotCacheDefinition/ pivotCacheRecords/pivotTableDefinition);与图表和图像一样,从 Writer.fs/Reader.fs 中拆分出来 (内部实现)。
    • Interpreter/Writer.fs — DSL → OOXML(内部实现)。
    • Interpreter/Reader.fs — OOXML → DSL,反向转换(内部实现)。
    • Interpreter/CodeGen.fs — DSL → F# 源文本:将 Workbook 渲染为独立的 .fsx 脚本,运行后可重建等效文件 (内部实现)。
    • Api.fs — 公共入口:Workbook.save / saveToStream / load / loadFromStream / generateScript。
  • tests/Kookerella.FsOpenXmlDsl.Tests — 每项功能各有一个测试,每个测试都会使用 OOXML 架构 (DocumentFormat.OpenXml.Validation.OpenXmlValidator)验证生成的文件,并断言其精确往返转换回 DSL。每项测试还会将构建的工作簿写入 Examples/<test name>/output.xlsx(已提交到代码库),因此每项功能都有一个真实且可打开的 .xlsx 文件作为示例—— 这是可浏览的示例集,而不只是断言。每个场景还会生成一个 Examples/<test name>/script.fsx——请参见下文 “将文件重新生成为 F# 源代码”——单独的、速度较慢的 Category=Slow 测试组会通过 dotnet fsi 实际运行它,并与已提交的 .xlsx 验证;还会生成一个 Examples/<test name>/workbook.xml——同一工作簿经 Xml.ofWorkbook 转换而来, 并在生成时根据 Xml.xsd 验证(请参见下文“## XML”)——以及一个 Examples/<test name>/workbook.json——同一工作簿经 Json.ofWorkbook 转换而来,并在生成时根据 Json.schema.json 验证(请参见下文“## JSON”)——因此,一个文件夹中始终有同一示例的四种视图:真实文件、 可重建该文件的 F# 源代码,以及同样可重建它的 XML/JSON。 Assets/ 存放唯一一个过大而无法像 Tests.fs 中其他二进制测试资源那样内联为 base64 字面量的测试样本—— 一个从 Excel 实际保存的工作簿中提取的真实 vbaProject.bin,用于宏示例。
  • samples/Kookerella.FsOpenXmlDsl.Sample — 一个小型控制台应用,用于构建工作簿、保存并读回。
  • src/Kookerella.CsOpenXmlDsl — 本库的地道、不可变、流畅式 C# 封装,适合不想直接处理 F# 判别联合/选项类型的调用方。 现在涵盖本库在工作表/工作簿级别建模的所有功能——范围说明和示例请参见其自身 README。 tests/Kookerella.CsOpenXmlDsl.Tests 是独立的 C# xUnit 测试套件,按照真实 C# 调用方的使用方式测试此封装, 而非复用 F# 测试项目。
  • src/Kookerella.FsOpenXmlDsl.Mcp — 一个本地 MCP(Model Context Protocol)服务,将本库的读取/写入/代码生成/XML/JSON 能力作为工具公开,供任何兼容 MCP 的 AI Agent 直接调用;同时也提供相同的转换能力,支持使用普通的 fsopenxmldsl-mcp convert/build CLI 命令,供不通过 MCP 客户端的用户使用——工具列表和配置方法请参见其自身 README。

快速入门

open Kookerella.FsOpenXmlDsl
open type Kookerella.FsOpenXmlDsl.SheetDsl

let headerStyle =
    { CellStyle.Default with
        Font = Some { FontStyle.Default with Bold = true }
        Fill = Some { Color = Rgb(220uy, 220uy, 220uy) } }

let data =
    sheet
        "Sheet1"
        [ row [ cell (Text "Name", style = headerStyle)
                cell (Text "Amount", style = headerStyle) ]
          row [ cell (Text "Widgets")
                cell (Number 42.5, style = { CellStyle.Default with NumberFormat = Some TwoDecimal }) ]
          Freeze(1, 0) ]

workbook [ data ] |> Workbook.save "out.xlsx"

// Reverse transform:
let roundTripped = Workbook.load "out.xlsx"

CellEntry 和 SheetItem 的行分支都是单一的简单 DU 分支,带有可选字段(Col/Index),而不是单独的“带样式”或“显式位置”分支——None 表示“位于前一个条目之后的下一列/行”(从 0 开始),Some n 则显式跳转到该位置,并从其后继续顺序编号。不过,你不必直接构造该分支:SheetDsl.cell/SheetDsl.row 是带有真正可选参数的成员(cell 使用 ?col/?style,row 使用 ?index),在常见情况下会隐藏 None——在 F# 中,普通的 let 函数不能有可选参数,因此 DSL 的这一部分才使用了类型。除了 open Kookerella.FsOpenXmlDsl,再使用 open type Kookerella.FsOpenXmlDsl.SheetDsl,就能像打开模块一样,将 cell/row 引入作用域并直接使用。显式跳转到列/行时仍使用这两个成员,只需提供可选参数:cell (value, col = 2) 和 row (cells, index = 4)。sheet 是唯一一个会将生成的条目列表解释为规范 Worksheet 的折叠函数(这与 Writer 对 OOXML 的处理关系相同)。如果你已有通过 CellRef 预先指定地址、而不是按行分组的单元格,那么 sheetOfCells 可以直接从扁平的 Cell list 构建 Worksheet。

Formula 单元格的形式为 Formula(expression, cachedValue: float option)——本库不会自行计算公式,因此在其他方式计算出结果之前,cachedValue 是该单元格中唯一会存在的数值。 Excel 会在打开文件时重新计算并覆盖该值,因此如果人们总会先在 Excel 中打开结果,将其留为 None 没问题。但对于无界面自动化流程,这样做并不安全——例如生成工作簿后直接将其传递给 PDF 转换器、其他自动化读取器,或任何不会在真正的 Excel 中打开它的程序。下游步骤是否会显示正确的数字、空白还是过期值,完全取决于它是否有自己的公式引擎;有些程序有(Aspose.Cells、Syncfusion、GemBox,以及通过 COM 调用的真正 Excel),而许多轻量级或无界面的转换器则没有,只会渲染单元格中已有的内容。既然你已经有用于计算公式的数字,那么对于任何不能保证先经过 Excel 处理的情况,都应将实际结果作为 cachedValue 传入——这不费任何成本,还能彻底避免该问题,因为即使下游读取器完全没有计算器,也能显示由其他程序计算出的正确值。

条件格式和数据验证也属于 SheetItem:

[ conditionalFormat (
    CellRef.ofA1 "A1",
    CellRef.ofA1 "A10",
    CellValueRule(GreaterThan, "100", None, { CellStyle.Default with Fill = Some { Color = Rgb(255uy, 199uy, 206uy) } })
  )
  dataValidation (CellRef.ofA1 "B1", CellRef.ofA1 "B10", ListValidation [ "Small"; "Medium"; "Large" ]) ]

各自涵盖的规则类型详见 MAPPING.md。

定义的名称属于工作簿级别,因此应添加到 Workbook,而不是 Worksheet:

workbook [ data ]
|> withDefinedNames
    [ definedName "TaxRate" "Sheet1!$A$1"
      sheetScopedDefinedName "Sheet1" "LocalTotal" "Sheet1!$A$2" ]

工作簿级保护(区别于 Worksheet 自带的 SheetProtection)同样属于工作簿级别,采用相同的管道友好形式:

workbook [ data ]
|> withProtection { WorkbookProtection.Default with LockStructure = Some true }

withDefinedNames/withProtection 可以组合使用——将两者都通过管道传给同一个 workbook [...]。

宏也属于工作簿级别,同样采用管道友好的形式——withVbaProject 接受现有 vbaProject.bin 的原始字节(从已有的 .xlsm 中提取,例如使用 System.IO.Compression.ZipFile,或在 Excel 的 VBA 编辑器中编写后以相同方式提取)。Core 不会解码、生成或以其他方式理解 VBA 源代码——它只会嵌入并原样读回你提供的字节,处理方式与栅格图像的 ImageEntry.Data 所采用的“透明负载”方式相同:

workbook [ data ]
|> withVbaProject (System.IO.File.ReadAllBytes("vbaProject.bin"))

请使用 .xlsm 路径保存结果——只要存在 VbaProject,Workbook.save/saveToStream 就会自动将文件自身声明的内容类型切换为 Excel 的启用宏类型,但真正的 Excel 还要求使用 .xlsm 扩展名才会信任并运行宏。未建模的内容(编写宏源代码,以及 Core 写入的默认工作表/工作簿代码名与宏原作者预期不符的一种情况)详见 MAPPING.md。 打印设置也是 SheetItem——PageSetup(DU 情况)接收普通的 PageSetup 记录(类型),没有智能构造函数,与 Protect/SheetProtection 一样。 PrintArea 是一个范围列表(Excel 支持每张工作表包含多个不连续的打印矩形)——在底层,它实际上是一个隐藏的定义名称,但 Writer/Reader 会透明地进行转换,因此它的读写方式与其他 PageSetup 字段一样:

[ PageSetup
    { PageSetup.Default with
        Orientation = Landscape
        Scaling = Some(FitToPage(1, 0)) // 1 page wide, unlimited tall
        PrintArea = [ (CellRef.ofA1 "A1", CellRef.ofA1 "D10") ]
        Header = Some "&C&\"Arial,Bold\"Quarterly Report"
        FirstHeader = Some "&CCover Page" // shown only on page 1
        EvenFooter = Some "&L&F" } ] // shown only on even pages

对于未建模的内容(总计行/无标题表格,以及少数次要的 pageSetup 属性,例如打印页面顺序),请参阅 MAPPING.md。

表格也是 SheetItem——Table(DU 情况)接收普通的 TableEntry 记录(类型),没有智能构造函数,与 Protect/PageSetup 一样。Core 不会为你生成标题行的单元格文本,因此这些文本必须事先作为普通单元格存在——这与条件格式/自动筛选/合并单元格只会描述叠加在你已放置的单元格之上的元数据一样:

sheet
    "Sheet1"
    [ row [ cell (Text "Item"); cell (Text "Quantity") ]
      row [ cell (Text "Widgets"); cell (Number 12.0) ]
      Table
          { TopLeft = CellRef.ofA1 "A1"
            BottomRight = CellRef.ofA1 "B2"
            Name = "Inventory"
            Columns = [ { Name = "Item"; CalculatedFormula = None }; { Name = "Quantity"; CalculatedFormula = None } ]
            Style = TableStyle.Default } ]

结构化引用(Table1[Column])无需特殊处理——它们只是 Formula 单元格中的原始公式文本,与其他公式一样。对于未建模的内容(总计行、无标题表格),请参阅 MAPPING.md。

迷你图遵循相同的形式——SparklineGroup(DU 情况)接收普通的 SparklineGroupEntry 记录:

[ SparklineGroup
    { Style = { SparklineStyle.Default with Type = Column; ShowNegative = true }
      Sparklines =
        [ { Cell = CellRef.ofA1 "E1"; DataTopLeft = CellRef.ofA1 "A1"; DataBottomRight = CellRef.ofA1 "D1" } ] } ]

迷你图是 Microsoft 扩展(位于工作表的 extLst 中),并非核心 SpreadsheetML——与此库的其他部分不同,仅靠架构验证无法确认 Excel 是否能正确呈现迷你图,因此使用时请谨慎,并在依赖它之前通过实际的 Excel 验证。对于未建模的内容(轴设置、除主系列颜色以外的各角色颜色),请参阅 MAPPING.md。

图表也采用相同的形式——EmbeddedChart(不能使用裸 Chart,因为它与 OOXML SDK 自带的类型冲突——请参阅 Builders.fs)接收普通的 ChartEntry 记录。系列的 Name 是对命名单元格(通常是列标题)的引用,会像实际 Excel 图表的系列名称一样实时更新,而不是静态副本:

[ EmbeddedChart
    { Type = ChartColumn
      Title = Some "Sales by Quarter"
      CategoriesTopLeft = CellRef.ofA1 "A2"
      CategoriesBottomRight = CellRef.ofA1 "A4"
      Series = [ { Name = CellRef.ofA1 "B1"; ValuesTopLeft = CellRef.ofA1 "B2"; ValuesBottomRight = CellRef.ofA1 "B4" } ]
      ShowLegend = true
      TopLeftAnchor = CellRef.ofA1 "E1"
      BottomRightAnchor = CellRef.ofA1 "L15" } ]

与迷你图不同,图表属于核心功能,完全由架构驱动的 DrawingML/ChartML 构成——它们和其他功能一样,基于强类型 OOXML SDK 类构建,而不是通过扩展机制实现。对于未建模的内容(柱形/条形/折线/饼图以外的图表类型、各系列样式、堆积分组),请参阅 MAPPING.md。

图像也采用相同的锚定方式——EmbeddedImage 接收普通的 ImageEntry 记录。Data 就是图像文件本身的原始字节(例如,使用 System.IO.File.ReadAllBytes 读取)——此 DSL 不会解码或重新编码任何内容,只会嵌入并原样返回你提供的数据:

[ EmbeddedImage
    { Data = System.IO.File.ReadAllBytes("logo.png")
      Format = Png
      TopLeftAnchor = CellRef.ofA1 "A1"
      BottomRightAnchor = CellRef.ofA1 "C6" } ]

工作表的图表和图像在底层共用一个绘图画布(Excel 每张工作表只提供一个画布),这一点对调用者是透明的——只需将两种 SheetItem 添加到同一张工作表即可。对于未建模的内容(PNG/JPEG/GIF/BMP 以外的格式、自由浮动位置、裁剪、链接而非嵌入的图像),请参阅 MAPPING.md。

数据透视表也是 SheetItem——EmbeddedPivotTable(不能使用裸 PivotTable,同样是为了与 EmbeddedChart/EmbeddedImage 的命名保持一致)接收普通的 PivotTableEntry 记录。与其他所有功能不同,它在写入时会执行实际处理,而非纯粹的转换:按 RowField(以及给定时的 ColumnField)对源范围分组,汇总 ValueField,并同时写入真正的 Excel 数据透视缓存以及计算得到的单元格网格:

[ EmbeddedPivotTable
    { SourceSheet = None // defaults to this sheet; can name another
      SourceTopLeft = CellRef.ofA1 "A1"
      SourceBottomRight = CellRef.ofA1 "C5"
      RowField = "Region"
      ColumnField = Some "Quarter"
      ValueField = "Sales"
      Aggregation = PivotSum
      ValueCaption = Some "Total Sales"
      TopLeftAnchor = CellRef.ofA1 "E1" } ]

源范围的第一行必须是纯 Text 标题单元格,用于命名每个字段。本功能有意限定为每个轴只能表达一个字段——一个行字段、最多一个列字段、一个值字段、表格布局,以及仅包含总计——有关原因以及更丰富的数据透视表(嵌套字段、多个值字段、页面筛选器)还需要哪些支持,请参阅 MAPPING.md。

将文件重新生成为 F# 源代码

给定一个 Workbook(通常是刚通过 Workbook.load 从现有文件加载的对象), Workbook.generateScript 会将其重新生成为一个独立的 .fsx 脚本,运行后即可 重建等效文件——它是 Workbook.load 的代码生成对应功能,比反向转换更进一步: 得到的不是数据,而是 DSL 源文本。它不负责确定脚本如何定位 FsOpenXmlDsl 程序集, 因此需要自行提供 #r 行:

let wb = Workbook.load "input.xlsx"

let referenceLines =
    [ "#r \"path/to/Kookerella.FsOpenXmlDsl.dll\""
      "#r \"path/to/DocumentFormat.OpenXml.dll\"" ]

let script = Workbook.generateScript referenceLines "output.xlsx" wb
System.IO.File.WriteAllText("regenerate.fsx", script)

运行 dotnet fsi regenerate.fsx 会生成 output.xlsx——它与原文件并非字节完全相同 (ZIP 元数据/时间戳不同),但通过本仓库其他测试使用的同一往返验证方式,结构上是等效的。 生成的代码只会提及与 CellStyle.Default/BorderStyle.None/等不同的字段,并且只有在 源数据中确实存在间隙时,才会为行/单元格显式指定 index/col——参见 Interpreter/CodeGen.fs。tests/Kookerella.FsOpenXmlDsl.Tests/Examples/ 下的每个场景 都提交了按此方式生成的 script.fsx;Category=Slow 测试组会通过 dotnet fsi 实际运行每个脚本, 并检查它是否能重现已提交的 .xlsx。

XML

Xml.toWorkbook/Xml.ofWorkbook(位于 Xml.fs)是使用 DSL 的第三种输入和输出方式, 与直接编写 F#/C# 以及代码生成并列:使用符合真实架构的纯 XML(Xml.xsd 嵌入在程序集中)。 此功能面向不想编写代码,而更愿意生成或使用数据的调用者。以下是两个具体用途:

  • 根据转换引擎已生成的 XML 构建 .xlsx——XSLT 流程(或任何能输出 XML 的模板系统) 可以直接生成 Excel 文件,无需了解 OOXML 架构或此库自身的 API。
  • 将现有 .xlsx 转换为 XML 以便进行版本控制——.xlsx 是二进制 ZIP 文件,因此对其使用 git diff 毫无用处;先转换为 XML,就能得到真正可读的差异。无论底层 Workbook 列表的 顺序如何,Xml.ofWorkbook 的输出都会按确定顺序排列(单元格按位置排序,定义名称按名称排序), 因此真实的内容变更会产生小而集中的差异,而不是因行/规则在不同运行之间重新排序而产生的 虚假差异。
open System.Xml.Linq

// XML -> Workbook -> .xlsx
let wb = XElement.Load "report.xml" |> Xml.toWorkbook
Workbook.save "report.xlsx" wb

// .xlsx -> Workbook -> XML
let xml = Workbook.load "report.xlsx" |> Xml.ofWorkbook
xml.Save "report.xml"

带有自身数据的判别联合案例会成为以案例名称命名的 XML 元素(采用 camelCase);若是多个无参数 选项之一,则会成为属性值(同样采用 camelCase)——例如单元格的值:

<cell ref="B2">
  <number>42.5</number>
  <style>
    <numberFormat kind="currency" />
  </style>
</cell>

更丰富的示例——ValidationKind 的六个案例遵循相同约定,而 ValidationAlert 的字段则直接 写为 <dataValidation> 上的属性,而不是嵌套元素:

<dataValidation topLeft="A2" bottomRight="A2" errorTitle="Invalid quantity"
                errorMessage="Quantity must be a positive whole number.">
  <wholeNumberValidation operator="greaterThan" formula1="0" />
</dataValidation>

ConditionalFormatRule 的七个案例也遵循相同约定;规则需要 CellStyle 时会将其完整嵌套—— 注意 <fill> 直接包含 <rgb>/<indexed>/<theme>,没有额外的包装元素:

<conditionalFormat topLeft="A1" bottomRight="A3">
  <cellValueRule operator="greaterThan" formula1="100">
    <style>
      <fill>
        <rgb r="255" g="199" b="206" />
      </fill>
    </style>
  </cellValueRule>
</conditionalFormat>

Chart 的 Series 列表需要自己的包装元素(<series>),并与每个列表项自身的元素名称(<s>) 区分开来,以避免 XML 中存在而 JSON 中不存在的真实歧义——XML 中的列表不像 JSON 数组那样有自身 的结构,因此容器和列表项需要使用不同名称,否则读取方就无法判断列表从何处开始:

<chart type="column" title="Sales by Quarter" showLegend="true"
       anchorTopLeft="E1" anchorBottomRight="L15">
  <categories topLeft="A2" bottomRight="A4" />
  <series>
    <s name="B1" valuesTopLeft="B2" valuesBottomRight="B4" />
    <s name="C1" valuesTopLeft="C2" valuesBottomRight="C4" />
  </series>
</chart>

Excel Table 展示了同一种包装元素/列表项拆分的更常见情形——columns 已有自然的单数形式 (column),因此无需采用 <s> 式的变通方式:

<table topLeft="A1" bottomRight="B4" name="Calc">
  <columns>
    <column name="Qty" />
    <column name="Doubled" calculatedFormula="Calc[Qty]*2" />
  </columns>
  <style name="TableStyleLight9" showFirstColumn="true" showLastColumn="true"
         showColumnStripes="true" />
</table>

SparklineGroup 的 Color 字段会包装在自己的 <color> 子元素中,这与 CellStyle 的 font/fill 使用的约定相同:

<sparklineGroup>
  <style type="column" lineWeight="1.5" showNegative="true">
    <color>
      <rgb r="0" g="112" b="192" />
    </color>
  </style>
  <sparklines>
    <sparkline cell="E1" dataTopLeft="A1" dataBottomRight="D1" />
  </sparklines>
</sparklineGroup>

PivotTable 是这里结构最扁平的类型——只有属性,没有嵌套元素。请注意,这里只会传递 描述信息:通过 Xml.toWorkbook 加载它时不会重新执行聚合,这一点与此架构涵盖的其他所有内容不同:

<pivotTable sourceSheet="Data" sourceTopLeft="A1" sourceBottomRight="C9"
            rowField="Region" columnField="Quarter" valueField="Sales"
            aggregation="average" valueCaption="Avg Sales" anchorTopLeft="F1" />

Image 的原始字节会作为元素自身的 base64 文本内容,这与下文 vbaProject 使用的约定相同:

<image format="gif" topLeft="A1" bottomRight="D6">R0lGODlhAQABAIAAAAAAAP...</image>

Hyperlink 的 Target 嵌套方式与 ValidationKind/ConditionalFormatRule 相同:

<hyperlink topLeft="A1" bottomRight="A1" tooltip="Visit site">
  <externalHyperlink>https://example.com</externalHyperlink>
</hyperlink>
<hyperlink topLeft="A2" bottomRight="B3" display="Go to top">
  <internalHyperlink>Sheet1!A1</internalHyperlink>
</hyperlink>

Comment 的文本也是元素自身的内容,而不是属性;author 为空时会直接省略,而不会写成 author="":

<comment cell="A1" author="Alex">Check this figure</comment>
<comment cell="A2">Unnamed author</comment>

工作表保护和工作簿保护都是扁平的属性集合——由于 SheetProtection/WorkbookProtection 的字段都不是结构化数据,因此不需要嵌套元素:

<protection password="hunter2" sheet="true" formatCells="true" sort="true" autoFilter="true" />
<workbook>
  <sheets>...</sheets>
  <protection password="hunter2" lockStructure="true" />
</workbook>

PageSetup 再次展示了混合 DU 约定——PaperSize 的命名情况会变成 kind 属性,与 NumberFormat 在单元格样式中使用的逃逸方式相同:

<pageSetup orientation="landscape">
  <paperSize kind="a4" />
  <margins left="0.5" right="0.5" top="1" bottom="1" header="0.2" footer="0.2" />
</pageSetup>

PrintScaling 的两种情况、PaperSize 的逃逸方式(other,用于数十种不值得逐一命名的纸张代码)、PrintArea 的区域列表,以及页眉/页脚文本,全部汇集于此:

<pageSetup orientation="portrait">
  <paperSize other="9" />
  <scaling fitWidth="1" fitHeight="0" />
  <margins left="0.7" right="0.7" top="0.75" bottom="0.75" header="0.3" footer="0.3" />
  <printArea>
    <range topLeft="A1" bottomRight="D10" />
  </printArea>
  <header>&amp;C&amp;"Arial,Bold"Report</header>
  <footer>&amp;LPage &amp;P of &amp;N</footer>
</pageSetup>

启用宏的工作簿的 VbaProject 字节位于工作簿级别,与 sheets 并列,而不属于任何一张工作表:

<workbook>
  <sheets>...</sheets>
  <vbaProject>AQIDBA==</vbaProject>
</workbook>

DefinedNameScope 的两种情况展示了与 PaperSize/NumberFormat 的“kind 属性”技巧不同的结构:WorkbookScope 本身不携带任何数据,但它仍然会成为独立的(空)元素,而不是属性值,因为它与携带数据的 SheetScope 一起位于 <choice> 中:

<definedNames>
  <definedName name="LocalTotal" formula="Sheet1!$A$2" hidden="true">
    <sheetScope sheetName="Sheet1" />
  </definedName>
  <definedName name="TaxRate" formula="0.075">
    <workbookScope />
  </definedName>
</definedNames>

较小的区域类字段(MergedRange、FreezePane、AutoFilter、ColumnProps、RowProps)都是直接明了的属性集合或此类集合的列表:

<mergedRanges>
  <mergedRange topLeft="A1" bottomRight="C1" />
</mergedRanges>
<freezePane rows="1" columns="0" />
<autoFilter topLeft="A1" bottomRight="D11" />
<columnProps>
  <columnProp index="0" width="20" />
</columnProps>
<rowProps>
  <rowProp index="0" height="30" />
</rowProps>

Xml.schemaSet() 会加载编译后的架构,供你自行验证任一方向的数据(XDocument.Validate)——tests/Kookerella.FsOpenXmlDsl.Tests/Examples/ 下的每个场景都提交了一个 workbook.xml,并在生成它的同一测试中以这种方式通过架构验证,因此架构和 Xml.fs 本身绝不会悄然发生偏离。Xml.fs 涵盖的工作表/工作簿级功能与本库的其余部分及 C# 包装器相同:单元格值、样式、合并区域、冻结窗格、自动筛选、列/行大小调整、VBA(base64)、定义名称、超链接、批注、工作表/工作簿保护、打印设置、图像(base64)、Excel 表格、迷你图、图表、数据透视表(仅包含描述;与此处其他所有内容不同,加载数据透视表不会重新运行其汇总计算)、条件格式和数据验证。

Kookerella.FsOpenXmlDsl.Mcp 无需编写任何 F# 代码即可提供两个方向的功能:供 AI Agent 使用的 generate_xml/create_workbook_from_xml MCP 工具,以及供其他用户使用的 fsopenxmldsl-mcp convert --lang xml/build CLI 命令——请参阅该项目自己的 README。

JSON

Json.toWorkbook/Json.ofWorkbook(位于 Json.fs 中)是 DSL 的第四种输入和输出方式,与直接编写 F#/C#、代码生成和 XML 并列:纯 JSON,适用于工具链使用 JSON 而非 XML 的调用方。XML 的两种具体用途在这里同样适用:

  • 根据转换/生成流程已有的 JSON 构建 .xlsx——无需学习 OOXML 架构或本库自己的 API。
  • 将现有 .xlsx 转换为 JSON,以便进行版本控制——Xml.ofWorkbook 具有的确定性(按单元格位置排序,定义名称则按名称排序)同样适用于 Json.ofWorkbook 的输出,原因也相同:真实的内容变更会产生小而独立的差异,而不会因列表在不同运行间重新排列而产生无关差异。
open System.Text.Json.Nodes

// JSON -> Workbook -> .xlsx
let wb = JsonNode.Parse(File.ReadAllText "report.json").AsObject() |> Json.toWorkbook
Workbook.save "report.xlsx" wb

// .xlsx -> Workbook -> JSON
let json = Workbook.load "report.xlsx" |> Json.ofWorkbook
File.WriteAllText("report.json", json.ToJsonString())

带有自身数据的判别联合体情况会变成以该情况名称(驼峰命名)为键的单键 JSON 对象;若它是多个无参数选项之一,则会变成一个纯 JSON 字符串(同样采用驼峰命名)——例如,单元格的值:

{
  "ref": "B2",
  "number": 42.5,
  "style": { "numberFormat": "currency" }
}

以上相同的 DataValidation 示例,以 JSON 表示——与将 ValidationAlert 的字段平铺到 <dataValidation> 自身上的 XML 接口不同,JSON 会将 kind 和 alert 都嵌套为独立对象,这种结构更符合此格式的自然表达方式:

{
  "topLeft": "A2",
  "bottomRight": "A2",
  "kind": { "wholeNumberValidation": { "operator": "greaterThan", "formula1": "0" } },
  "alert": {
    "errorTitle": "Invalid quantity",
    "errorMessage": "Quantity must be a positive whole number."
  }
}

与上文相同的 ConditionalFormat 示例,使用 JSON 表示——rule 的嵌套方式与上文的 kind 相同,包含七种情况之一;而 fill(不同于 XML 中没有包裹层的 <fill>)总是通过显式键包裹其 color:

{
  "topLeft": "A1",
  "bottomRight": "A3",
  "rule": {
    "cellValueRule": {
      "operator": "greaterThan",
      "formula1": "100",
      "style": { "fill": { "color": { "rgb": { "r": 255, "g": 199, "b": 206 } } } }
    }
  }
}

与上文相同的 Chart 示例,使用 JSON 表示——series 是一个普通数组,无需像 XML 中那样使用 <series>/<s> 的包装器与项名称技巧,因为 JSON 数组有明确的边界:

{
  "type": "column",
  "title": "Sales by Quarter",
  "showLegend": true,
  "anchorTopLeft": "E1",
  "anchorBottomRight": "L15",
  "categories": { "topLeft": "A2", "bottomRight": "A4" },
  "series": [
    { "name": "B1", "valuesTopLeft": "B2", "valuesBottomRight": "B4" },
    { "name": "C1", "valuesTopLeft": "C2", "valuesBottomRight": "C4" }
  ]
}

与上文相同的 Table 示例,使用 JSON 表示——columns 只是另一个普通数组,与 series 一样:

{
  "topLeft": "A1",
  "bottomRight": "B4",
  "name": "Calc",
  "columns": [
    { "name": "Qty" },
    { "name": "Doubled", "calculatedFormula": "Calc[Qty]*2" }
  ],
  "style": {
    "name": "TableStyleLight9",
    "showFirstColumn": true,
    "showLastColumn": true,
    "showColumnStripes": true
  }
}

与上文相同的 SparklineGroup 示例,使用 JSON 表示——color 是一个普通的嵌套键,与样式的其他字段并列,和 CellStyle 下 fill 的方式相同:

{
  "style": {
    "type": "column",
    "lineWeight": 1.5,
    "showNegative": true,
    "color": { "rgb": { "r": 0, "g": 112, "b": 192 } }
  },
  "sparklines": [
    { "cell": "E1", "dataTopLeft": "A1", "dataBottomRight": "D1" }
  ]
}

与上文相同的 PivotTable 示例,使用 JSON 表示——无论哪种格式都是扁平对象,因为这里没有列表或嵌套结构:

{
  "sourceSheet": "Data",
  "sourceTopLeft": "A1",
  "sourceBottomRight": "C9",
  "rowField": "Region",
  "columnField": "Quarter",
  "valueField": "Sales",
  "aggregation": "average",
  "valueCaption": "Avg Sales",
  "anchorTopLeft": "F1"
}

Image 的字节数据是一个 base64 字符串值,与下方的 vbaProject 相同:

{ "format": "gif", "topLeft": "A1", "bottomRight": "D6", "data": "R0lGODlhAQABAIAAAAAAAP..." }

JSON 格式的 Hyperlink:

{
  "topLeft": "A1",
  "bottomRight": "A1",
  "target": { "externalHyperlink": "https://example.com" },
  "tooltip": "Visit site"
}
{
  "topLeft": "A2",
  "bottomRight": "B3",
  "target": { "internalHyperlink": "Sheet1!A1" },
  "display": "Go to top"
}

Comment——author 是一个普通的可选字段,不传值时会省略,而不是设为空字符串:

{ "cell": "A1", "author": "Alex", "text": "Check this figure" }
{ "cell": "A2", "text": "Unnamed author" }

JSON 格式的工作表和工作簿保护——都是扁平对象,与 XML 相同:

{ "password": "hunter2", "sheet": true, "formatCells": true, "sort": true, "autoFilter": true }
{ "sheets": [ { "name": "Sheet1" } ], "protection": { "password": "hunter2", "lockStructure": true } }

PageSetup——PaperSize 的命名情况是一个裸字符串,与混合 DU 约定中的 NumberFormat 一样:

{
  "orientation": "landscape",
  "paperSize": "a4",
  "margins": { "left": 0.5, "right": 0.5, "top": 1, "bottom": 1, "header": 0.2, "footer": 0.2 }
}

与上文相同的较丰富示例,使用 JSON 表示——PaperSize 的扩展形式是 {"other": 9},PrintScaling 的两种情况都是单键对象,与其他地方相同;printArea 是一个普通数组:

{
  "orientation": "portrait",
  "paperSize": { "other": 9 },
  "scaling": { "fitToPage": { "width": 1, "height": 0 } },
  "margins": { "left": 0.7, "right": 0.7, "top": 0.75, "bottom": 0.75, "header": 0.3, "footer": 0.3 },
  "printArea": [ { "topLeft": "A1", "bottomRight": "D10" } ],
  "header": "&C&\"Arial,Bold\"Report",
  "footer": "&LPage &P of &N"
}

值得了解:System.Text.Json 的默认编码器会将字符串值中的每个 &、<、> 和 " 字符都编码为 \uXXXX 序列(这是针对可能嵌入 HTML 的 JSON 采取的保守做法),因此上方标头对应的 generate_json 实际输出中,这些字符都会以这种方式替换,而不会像这里为便于阅读所展示的那样保留为普通文本。这只是表面差异,并非数据丢失错误(Json.toWorkbook 会将转义形式解析回完全相同的原始字符串,两种形式都已通过对这个确切示例进行往返验证)——在假定 generate_json 的结果已损坏之前,值得了解这一点。尤其是 Excel 的页眉/页脚代码(&C/&L/&R/&P/&N/...)都以 & 开头,因此实际的页眉/页脚文本必定会以这种方式呈现。

工作簿级别的 VbaProject:

{ "sheets": [ { "name": "Sheet1" } ], "vbaProject": "AQIDBA==" }

DefinedNameScope 的两种情况遵循标准 JSON 约定,结构清晰;这不同于 XML 中 <workbookScope />/<sheetScope> 的区分——WorkbookScope 只是一个裸字符串,与其他无参数情况的处理方式相同:

{
  "definedNames": [
    { "name": "LocalTotal", "formula": "Sheet1!$A$2", "scope": { "sheetScope": "Sheet1" }, "hidden": true },
    { "name": "TaxRate", "formula": "0.075", "scope": "workbookScope" }
  ]
}

JSON 格式中较小的类范围字段:

{
  "mergedRanges": [ { "topLeft": "A1", "bottomRight": "C1" } ],
  "freezePane": { "rows": 1, "columns": 0 },
  "autoFilter": { "topLeft": "A1", "bottomRight": "D11" },
  "columnProps": [ { "index": 0, "width": 20 } ],
  "rowProps": [ { "index": 0, "height": 30 } ]
}

与 XML 不同,.NET 没有内置的 JSON Schema 验证器(而 XML 有 System.Xml.Schema),因此 Json.schema.json(位于仓库中,且与此结构匹配)只通过本仓库自身的测试套件进行验证(借助仅供测试使用的 JsonSchema.Net 依赖项),不会像公共的 Json.schemaSet() API 那样对外提供。tests/Kookerella.FsOpenXmlDsl.Tests/Examples/ 下的每个场景都有一个已提交的 workbook.json,并同样通过这种方式进行验证;workbook.xml 则通过 Xml.xsd 验证,因此这里的架构与 Json.fs 本身也绝不会悄然发生偏离。Json.fs 涵盖了与 Xml.fs 相同的工作表/工作簿级功能集——单元格值、样式、合并区域、冻结窗格、自动筛选、列/行尺寸设置、VBA(base64)、定义名称、超链接、批注、工作表/工作簿保护、打印设置、图像(base64)、Excel 表格、迷你图、图表、数据透视表(仅描述——加载数据透视表不会重新运行其聚合,与此处其他所有功能不同)、条件格式和数据验证。

Kookerella.FsOpenXmlDsl.Mcp 提供两个方向的功能,全程无需编写任何 F#: 面向 AI Agent 的 generate_json/create_workbook_from_json MCP 工具,以及供其他用户使用的 fsopenxmldsl-mcp convert --lang json/build CLI 命令——详见该项目自己的 README。

构建和测试

dotnet build
dotnet test --filter "Category!=Slow"
dotnet run --project samples/Kookerella.FsOpenXmlDsl.Sample

上面的默认循环会跳过耗时较长的 Category=Slow 测试。这些测试实际上会对每个生成的 Examples/*/script.fsx 调用 dotnet fsi(每次进程启动都需要数秒,因此总共约需 30–60 秒), 而不只是检查生成的源代码是否能解析。快速测试套件至少生成过一次 .fsx 文件后,再显式运行这些测试:

dotnet test --filter "Category=Slow"

不带筛选条件的普通 dotnet test 会运行两组测试。

赞助

如果这个项目对你有用,赞助该项目 有助于支持持续开发。赞助是对项目的支持,不包含支持服务级别协议(SLA)、功能保证或优先响应时间。无论是否获得赞助,本软件均按 MIT 许可证按原样提供。

评论

支持 Markdown

第一次接触 MCP?

从协议是什么,到在客户端里装上第一个服务,这几条回答覆盖了大多数入门问题。

一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。