关于 Excel MCP Server (FsOpenXmlDsl)
Excel MCP Server (FsOpenXmlDsl) 通过本地 MCP 服务,让 AI Agent 读写和转换 Excel 工作簿。它可将工作簿反编译为 F#、C#、XML 或 JSON,也可从 XML 或 JSON 构建工作簿,支持双向转换流程。
主要功能
- 01读写 Excel 工作簿
- 02将工作簿反编译为 F# 或 C#
- 03将工作簿转换为 XML 或 JSON
- 04从 XML 或 JSON 创建工作簿
- 05通过 CLI 转换和构建
适用场景
- 01将现有工作簿转为源码检查
- 02根据 XML 或 JSON 生成工作簿
- 03将电子表格重建为 F# 或 C# 脚本
MCP 相关
本地 MCP 服务将工作簿读写、代码生成及 XML/JSON 转换作为工具提供给兼容 MCP 的 Agent。同样的转换能力也可通过 `fsopenxmldsl-mcp convert` 和 `build` 命令使用。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
我们还没有获取到这个服务的安装包或远程地址,请按项目 README 中的说明安装。
工具
本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。
详细介绍
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/buildCLI 命令,供不通过 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>&C&"Arial,Bold"Report</header>
<footer>&LPage &P of &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 许可证按原样提供。
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。