关于 Word MCP Server (FsWordDsl)
Word MCP Server (FsWordDsl) 将 F# 文档 DSL 的读写和反向转换能力提供给 MCP 客户端,也可作为命令行工具使用。它能解析现有 Word 文件并生成可重建文档的脚本,并支持从 XML 或 JSON 转换。
主要功能
- 01读取和写入 Word 文档
- 02将文档反编译为 F#、C#、XML 或 JSON
- 03从 XML 或 JSON 构建文档
- 04支持富文本、表格、样式和图片
- 05提供命令行转换与构建
适用场景
- 01从现有 DOCX 提取可读模型
- 02生成重建 Word 文档的源代码
- 03通过 XML 或 JSON 创建 Word 文件
- 04在 MCP 客户端中处理 Word 文档
MCP 相关
MCP 服务将 Word 文档的读取、写入和反编译能力作为工具提供给兼容的 AI Agent。它也提供 `convert` 和 `build` 命令行入口。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
我们还没有获取到这个服务的安装包或远程地址,请按项目 README 中的说明安装。
工具
本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。
详细介绍
Kookerella.FsWordDsl
用于构建 Word 文档的类型安全 F# DSL,可解释为针对 DocumentFormat.OpenXml SDK 的调用。该 DSL 是一种纯数据模型(具有结构相等性的记录/DU)——解释器(Writer)将其编译为 OOXML,反向转换器(Reader)则会将现有 .docx 解析回同一 DSL。
这是 Kookerella.FsOpenXmlDsl(Excel/SpreadsheetML 版本)的 WordprocessingML 兄弟项目——目标相同,往返转换理念相同,只是采用了 Word 自身的文档模型。有关哪些 WordprocessingML 功能可以 1:1 映射、哪些是近似映射,以及哪些尚未建模,请参阅 MAPPING.md。
它支持双向往返转换,而大多数 Word 库都做不到这一点:它们提供命令式 API 来从头构建文档,却无法将现有文件转换回可读源码。在这里,Reader 会将真实的 .docx/.docm 解析回同一 DSL,而 Document.generateScript 更进一步,将该模型重新渲染为自包含脚本,以重建等效文件——这不只是 Word 文档写入器,也是反编译器。另外还有两个接口:Xml.toDocument/Xml.ofDocument(参见下文的 "## XML")和 Json.toDocument/Json.ofDocument(参见下文的 "## JSON"),可根据真实架构在普通 XML 或 JSON 与文档之间进行相同的转换,适合希望生成或使用数据而非编写代码的调用者。
流畅的 C# 封装层(Kookerella.CsWordDsl)基于 F# 核心构建,供不想直接接触 F# 判别联合/选项类型的调用者使用——它提供带有 With* 构建器的不可变记录,以及自己的 CsCodeGen 反编译器,可将 Document 渲染为可运行的 C# 源码。参见下文的 "## The C# wrapper"。
MCP 服务(Kookerella.FsWordDsl.Mcp)将相同的读取/写入/反编译能力作为工具提供,供任何兼容 MCP 的 AI Agent 直接调用;它也可用作普通 CLI(fsworddsl-mcp convert/build),适合不通过 MCP 客户端使用的用户——完整工具列表请参阅其 README。
没有安装 .NET? 请从 Releases 页面下载适用于你平台(win/linux/osx、x64/arm64)的 MCP 服务独立构建版本——解压后即可运行,无需安装。
布局
src/Kookerella.FsWordDsl- 库。Units.fs- 在点/英寸/像素与 WordprocessingML 在线路格式中使用的物理单位之间转换(页面几何尺寸/间距使用 twips,图像尺寸使用 EMU)。Styles.fs- 字符和段落格式:Color(Rgb、Auto或相对于主题的Theme颜色,参见ThemeColorKind)、HighlightColor(Word 自带的固定高亮调色板)、UnderlineStyle、RunStyle(包括小型大写字母/全部大写/隐藏文本)、ParagraphAlignment、Indentation、LineSpacingRule、TabStopAlignment/TabLeader/TabStop、ParagraphFormat(包括段落边框、底纹和自定义制表位)、BorderLineStyle、BorderSide、BorderStyle(段落边框和表格边框共用)。NamedStyles.fs-StyleDefinition(段落或字符样式,支持BasedOn继承)和小型BuiltInStyles目录(normal、heading1/2/3、title、listParagraph、hyperlinkCharStyle)。Numbering.fs- 用于编号/项目符号列表(包括多级列表)的NumberFormatKind、ListLevel、NumberingDefinition(每个定义并不限于一个ListLevel,参见Builders.multiLevelNumberedListDef)。Hyperlinks.fs-HyperlinkTarget(外部 URL 或内部书签引用)。Protection.fs-EditRestriction和DocumentProtection,作用于文档级别(Word 没有 Excel 那种按工作表设置的分节保护机制)。Revisions.fs- 用于修订跟踪的RevisionKind/Revision(Inline.TrackedChange、Paragraph.MarkRevision),范围仅限于插入/删除的内容和段落标记;未覆盖的内容请参见MAPPING.md。ContentControls.fs- 用于内容控件(结构化文档标签,w:sdt)的ContentControlType/ContentControlProps:纯文本、富文本、下拉列表/组合框、日期选取器、复选框;未覆盖的内容请参见MAPPING.md。PageSetup.fs-PageOrientation、PageSize、PageMargins、SectionBreakType、NoteNumberRestart/NoteNumberingSettings(分节自身的脚注/尾注编号设置)。Tables.fs-TableBorders、VerticalMergeKind、TableCellProps(包括单元格级别的Margins覆盖值)、TableStyleRef、TableStyleRegion/TableStyleDefinition(自定义表格样式定义,涵盖 OOXML 的全部 13 种条件格式区域),以及CellMargins(用于表格默认边距和单个单元格自身覆盖值的共用结构)。Images.fs-ImageFormat、ImageEntry(原始文件字节和页面上的尺寸),以内嵌方式锚定在 run 中。DocumentProperties.fs-DocumentProperties(Title、Author、Subject、Keywords、Comments、Category、Company),即文档核心元数据;Document.Properties。Model.fs- 递归内容模型:Inline(run、换行、图像、超链接、书签和批注,包括单段落的Bookmark/Comment情况,以及跨段落的BookmarkRangeStart/End/CommentRangeStart/End标记、简单字段、脚注/尾注、用于修订跟踪的TrackedChange,以及用于内容控件的InlineContentControl)、Paragraph(包括MarkRevision)、Block(段落、表格或ContentControlBlock,即InlineContentControl的块级对应项)、TableCell/TableRow(包括RepeatAsHeader)/TableEntry(包括CellMargins)、HeaderFooterSet、SectionProperties(包括BreakType和FootnoteNumbering/EndnoteNumbering)、Section、Document(包括Document.VbaProject,即启用宏的文档中的原始vbaProject.bin字节、Document.Properties和Document.TableStyles)。Xml.fs/Xml.xsd- XML 接口:Xml.toDocument/Xml.ofDocument用于将Document转换为XElement树,或从XElement树转换而来;Xml.schemaSet()会加载配套架构(作为资源嵌入程序集),用于验证任一方向的转换。参见下方的 "## XML"。Json.fs/Json.schema.json- JSON 接口:Json.toDocument/Json.ofDocument用于将Document转换为System.Text.Json.Nodes.JsonObject树,或从该树转换而来。架构验证仅用于测试套件,不属于公共 API。参见下方的 "## JSON"。Builders.fs- 纯函数式构造器(section、document、withStyles、withNumbering、withProtection、withVbaProject、withDocumentProperties、withTableStyles、bulletListDef、numberedListDef、multiLevelNumberedListDef),以及DocumentDsl- 智能构造器(run、para(带markRevision)、hyperlink、bookmark、comment、inserted/deleted(修订跟踪)、contentControl/contentControlBlock(内容控件)、image、footnote、endnote、tableCell、tableRow(带height/repeatAsHeader)、table(带style/borders/cellMargins)),均使用真正的可选参数,是 Excel 同类仓库中SheetDsl的 Word 对应版本。Interpreter/StyleRegistry.fs- 共用 run/段落/边框/颜色转换,以及Document.Styles<->styles.xml(内部)。Interpreter/ImageWriter.fs/ImageReader.fs- 内嵌图像自身的 DSL <-> DrawingML 转换(内部)。Interpreter/Writer.fs- DSL -> OOXML(内部)。Interpreter/Reader.fs- OOXML -> DSL,即反向转换(内部)。Interpreter/CodeGen.fs- DSL -> F# 源文本:将Document重新生成为自包含的.fsx脚本,运行后可重建等效文件(内部)。Api.fs- 公共入口Document.save/saveToStream/load/loadFromStream/generateScript。
src/Kookerella.CsWordDsl- 流畅式 C# 封装(参见下方的 "## C# 封装"):不可变记录/sealed record封闭层次结构,与 F# 核心中的类型一一对应;DocumentConverter.cs(内部,用于双向 F#<->C# 转换)、DocumentIO.cs(Save/Load,本项目唯一执行 I/O 的位置)、CsCodeGen.cs(DSL -> C# 源文本,即Interpreter/CodeGen.fs的 C# 对应版本)。src/Kookerella.FsWordDsl.Mcp- MCP 服务(参见其自身的 README):DocumentTools.fs(工具接口,每个工具对应一个带[<McpServerTool>]标记的成员)、Program.fs(根据argv,分发到 MCP stdio 服务,或普通的convert/buildCLI)。与 Excel 同类项目自身的Kookerella.FsOpenXmlDsl.Mcp一样,以dotnet tool(fsworddsl-mcp)形式分发。tests/Kookerella.FsWordDsl.Tests- 每项功能各有一个测试,用于验证生成的文件是否符合 OOXML 架构(DocumentFormat.OpenXml.Validation.OpenXmlValidator),并断言经过 DSL 往返转换后完全一致。每项测试还会将生成的文档写入Examples/<test name>/output.docx(纳入仓库版本控制),以及script.fsx(用于重新生成文件;另有一个较慢的Category=Slow测试组会通过dotnet fsi实际执行各脚本)、document.xml和document.json;每个文件夹始终包含同一示例的四种视图。tests/Kookerella.CsWordDsl.Tests-DriftGuardTests.cs(基于反射的哨兵测试,用于检查 C# 封装的 DU 镜像是否落后于 F# 核心中各自的情况数量)、DocumentTests.cs(针对各项功能的往返转换断言)、ExampleTests.cs(重新加载 F# 测试套件中已纳入版本控制的Examples/*/output.docx夹具,而不是再次为每种场景重新编写文档)、CsCodeGenTests.cs(通过dotnet run --file实际执行生成的文件,即 F# 测试套件中Category=Slow的dotnet fsi测试组的 C# 对应版本)。samples/Kookerella.FsWordDsl.Sample- 一个小型控制台应用,用于构建文档、保存文档并读回文档。
快速开始
open Kookerella.FsWordDsl
open type Kookerella.FsWordDsl.DocumentDsl
let doc =
document
[ section
[ para ([ run "Quarterly Report" ], styleId = "Title")
para
[ run "This report covers "
run ("Q1 2026", style = { RunStyle.Default with Bold = true })
run ", see the "
hyperlink ("full dataset", ExternalUrl "https://example.com/data")
run " for details." ] ] ]
doc |> Document.save "report.docx"
// Reverse transform:
let roundTripped = Document.load "report.docx"
document 默认将 Styles 设为 BuiltInStyles.all,因此 styleId = "Heading1"(或其他任何内置 ID)无需先注册即可直接使用——之后接上 withStyles 即可替换或扩展该集合。run/para/hyperlink/bookmark/comment/image/tableCell/tableRow/table 是 DocumentDsl 的成员,带有真正的可选参数(open type Kookerella.FsWordDsl.DocumentDsl 会将它们引入当前作用域,无需限定名称,与 Excel 仓库中的 open type SheetDsl 一样)——普通的 F# let 绑定无法带可选参数,因此这部分 DSL 采用了类型。
Paragraph 的 Inlines 自然可以包含多个分别设置样式的文本运行——富文本(同一段落内混合格式)是内置功能,并非像 Excel 单一统一文本运行的 Text 单元格那样存在文档中提到的缺口:
para
[ run "Plain text, "
run ("bold", style = { RunStyle.Default with Bold = true })
run ", and "
run ("colored", style = { RunStyle.Default with Color = Some Color.red }) ]
RunStyle 还支持小型大写字母、全部大写字母和隐藏文本;ParagraphFormat 支持边框(BorderStyle,与表格边框使用相同的结构)和底纹:
para
([ run "ALL CAPS AND SMALL CAPS" ], format =
{ ParagraphFormat.Default with
Borders = Some { BorderStyle.None with Bottom = Some { Style = SingleLine; Width = Some 1.0; Color = Some Color.black } }
Shading = Some(Rgb(0xD9uy, 0xD9uy, 0xD9uy)) })
自定义制表位(TabStop)位于 ParagraphFormat.TabStops 中——右对齐制表位加点状前导符是经典的目录样式:
para
([ run "Introduction"; Tab; run "1" ], format =
{ ParagraphFormat.Default with TabStops = [ { Position = 288.0; Alignment = RightTab; Leader = DotLeader } ] })
Color 除了纯 Rgb/Auto,还接受相对于主题的标记(Theme)——由于此 DSL 没有可供解析的主题部件,因此由真正的 Word 来处理;Fallback 是无主题的阅读器所看到的颜色,这遵循了 Word 自身采用的“始终同时写入计算所得的值”约定:
run ("Accent-colored text", style = { RunStyle.Default with Color = Some(Theme(Accent1Theme, (0x1Fuy, 0x49uy, 0x7Duy), None, None)) })
列表在段落上使用 (numId, level) 引用,并通过附加到文档的 NumberingDefinition 进行解析——NumberingDefinition.Levels 不限于一级,而 multiLevelNumberedListDef 可为你构建常见且链接正确的大纲结构:
document
[ section
[ para ([ run "First bullet" ], numbering = (1, 0))
para ([ run "Second bullet" ], numbering = (1, 0)) ] ]
|> withNumbering [ bulletListDef 1 ]
document
[ section
[ para ([ run "First topic" ], numbering = (1, 0))
para ([ run "First subtopic" ], numbering = (1, 1))
para ([ run "Second topic" ], numbering = (1, 0)) ] ]
|> withNumbering [ multiLevelNumberedListDef 1 3 ]
表格由 tableRow/tableCell 构建,列宽在整个表格中统一指定——单元格未显式指定宽度时,写入时会回退使用所在列的宽度:
table (
[ tableRow [ tableCell [ para [ run "Item" ] ]; tableCell [ para [ run "Qty" ] ] ]
tableRow [ tableCell [ para [ run "Widgets" ] ]; tableCell [ para [ run "12" ] ] ] ],
[ 200.0; 100.0 ],
style = TableStyleRef.Default
)
单元格合并——水平合并(GridSpan)和垂直合并(RestartMerge/ContinueMerge)——彼此独立,也可应用于同一单元格,与真正的 Word 行为一致:
tableCell ([ para [ run "Spans 2 columns" ] ], props = { TableCellProps.Default with GridSpan = Some 2 })
单元格自身的边距会覆盖表格的默认边距;两者使用相同的 CellMargins 结构:
tableCell ([ para [ run "Extra padding" ] ], props = { TableCellProps.Default with Margins = Some { CellMargins.Default with Top = Some 8.0; Bottom = Some 8.0 } })
自定义表格样式(TableStyleDefinition)存放在 Document.TableStyles 中,并按名称应用,与内置样式(如 "TableGrid")的用法相同——此处展示了蓝色背景上的粗体白色标题行、斜体末行和交替行底纹,以及整张表格的默认单元格边距和每页重复的行:
let corporateStyle: TableStyleDefinition =
{ TableStyleDefinition.Default with
Id = "Corporate"
Name = "Corporate"
FirstRow =
{ TableStyleRegion.None with
RunFormat = Some { RunStyle.Default with Bold = true; Color = Some Color.white }
CellShading = Some(Rgb(0x4Fuy, 0x81uy, 0xBDuy)) }
LastRow = { TableStyleRegion.None with RunFormat = Some { RunStyle.Default with Italic = true } }
BandedRow = { TableStyleRegion.None with CellShading = Some(Rgb(0xDCuy, 0xE6uy, 0xF1uy)) } }
document
[ section
[ table (
[ tableRow ([ tableCell [ para [ run "Item" ] ]; tableCell [ para [ run "Qty" ] ] ], repeatAsHeader = true)
tableRow [ tableCell [ para [ run "Widgets" ] ]; tableCell [ para [ run "12" ] ] ] ],
[ 200.0; 100.0 ],
style = { TableStyleRef.Default with Name = "Corporate" },
cellMargins = { Top = Some 4.0; Bottom = Some 4.0; Left = Some 6.0; Right = Some 6.0 }
) ] ]
|> withTableStyles [ corporateStyle ]
TableStyleDefinition 还支持 FirstColumn/LastColumn、BandedColumn 和四个角落单元格(NorthEastCell/NorthWestCell/SouthEastCell/SouthWestCell)——未建模的两个区域是每个带状轴上的第二个带,因为在实际使用中,透出的只是 WholeTable 自身的背景(参见 MAPPING.md)。
节各自携带页面设置——文档由一系列 Section 组成,与真正的 Word 节分隔符一一对应。BreakType 用于指定某节相对于前一节的起始方式——对第一节没有意义,也不会写入:
let landscape = { SectionProperties.Default with Orientation = Landscape }
document [ sectionWith landscape [ para [ run "A landscape-oriented page." ] ] ]
let continuous = { SectionProperties.Default with BreakType = ContinuousBreak }
document
[ section [ para [ run "Section 1." ] ]
sectionWith continuous [ para [ run "Section 2 - no page break from section 1." ] ] ]
脚注和尾注在段落自身的 Inlines 中标记一个位置——content 是注释本身的正文,会写入 word/footnotes.xml/endnotes.xml,其 ID 由 Writer 自动分配(两端的引用标记文本运行都会自动生成):
para
[ run "This claim needs a citation"
footnote "Smith, J. (2023). A Study of Claims."
run ", and this one refers to a fuller discussion"
endnote [ para [ run "See the appendix for the full derivation." ] ] ]
节自身的脚注/尾注编号(w:footnotePr/w:endnotePr)——None 表示使用 Word 自身的默认值(从 1 开始连续递增的十进制编号);此处脚注采用小写罗马数字,并在每页重新开始,符合常见的法律文档惯例:
sectionWith
{ SectionProperties.Default with FootnoteNumbering = Some { Format = LowerRomanFormat; StartAt = None; Restart = RestartEachPage } }
[ para [ run "Body text."; footnote "A footnote numbered i, ii, iii, ... restarting each page." ] ]
页眉和页脚按节设置,并支持 Default/First/Even 变体(Word 所需的 titlePg/evenAndOddHeaders 标志会自动设置):
let footer = { HeaderFooterSet.None with Default = Some [ para [ run "Page "; Field("PAGE", Some "1") ] ] }
sectionWith { SectionProperties.Default with Footer = Some footer } [ para [ run "Body text." ] ]
批注和书签会直接包裹行内内容,适用于常见的单段落情形:
para [ comment ([ run "This figure needs review." ], "Please double check the totals.", author = "Alex") ]
跨越多个段落时,则改用两个独立标记,分别直接放在不同段落中,并共享一个 ID:书签使用 BookmarkRangeStart/BookmarkRangeEnd,批注使用 CommentRangeStart/CommentRangeEnd(批注自身的元数据放在其 Start 上,因为这里没有可供挂载元数据的包裹情况——关于该 ID 仅在写入时使用、而书签自身名称不会如此的原因,请参阅 MAPPING.md):
document
[ section
[ para [ BookmarkRangeStart "Section2"; run "This paragraph starts the bookmark" ]
para [ run "and this one ends it."; BookmarkRangeEnd "Section2" ] ] ]
document
[ section
[ para [ CommentRangeStart("review1", "Alex", None, None, "This section needs review."); run "Comment starts here" ]
para [ run "and ends here."; CommentRangeEnd "review1" ] ] ]
修订(inserted/deleted)也会以相同方式包裹行内内容,并标记其作者和日期;如果整个段落被插入或删除(而非仅部分内容),则改用 para 自身的 markRevision 标记段落的结束标记:
para
[ run "The quick "
inserted ([ run "brown " ], "Alex")
run "fox jumps over the "
deleted ([ run "lazy " ], "Alex")
run "dog." ]
para ([ run "This whole paragraph was inserted." ], markRevision = { Kind = Inserted; Author = "Alex"; Date = None })
内容控件(行级使用 contentControl,块级使用 contentControlBlock)也会以相同方式包裹当前显示的内容,并附带一个 ContentControlType(完整类型列表请参阅 MAPPING.md:纯文本、富文本、下拉列表/组合框、日期选择器、复选框):
para
[ run "Client name: "
contentControl ([ run "Type here" ], PlainTextControl false, alias = "Client Name", tag = "clientName") ]
para
[ run "Favorite color: "
contentControl ([ run "Blue" ], DropDownControl([ "Red", "red"; "Green", "green"; "Blue", "blue" ], false)) ]
contentControlBlock([ para [ run "This whole paragraph is a rich-text content control." ] ], RichTextControl, alias = "Notes")
文档级保护、宏和核心属性都适合使用管道式写法,结构与 Excel 自身的 withProtection/withVbaProject 相同:
document [...] |> withProtection { Edit = Some ReadOnlyRestriction; Password = Some "hunter2" }
document [...] |> withVbaProject (System.IO.File.ReadAllBytes("vbaProject.bin"))
document [...] |> withDocumentProperties { DocumentProperties.Default with Title = Some "Quarterly Report"; Author = Some "Kookerella" }
使用 .docm 路径保存结果——只要存在 VbaProject,Document.save/saveToStream 就会自动将文件自身声明的内容类型切换为 Word 的启用宏类型,但要让真实的 Word 信任并运行宏,还需要 .docm 扩展名。
将文件重新生成为 F# 源代码
给定一个 Document(通常是刚从现有文件中通过 Document.load 加载的),Document.generateScript 会将其重新生成为一个独立的 .fsx 脚本,运行该脚本即可重建等效文件——它是 Document.load 的代码生成对应项:
let doc = Document.load "input.docx"
let referenceLines =
[ "#r \"path/to/Kookerella.FsWordDsl.dll\""
"#r \"path/to/DocumentFormat.OpenXml.dll\"" ]
let script = Document.generateScript referenceLines "output.docx" doc
System.IO.File.WriteAllText("regenerate.fsx", script)
运行 dotnet fsi regenerate.fsx 会生成 output.docx——它与原文件的字节并不完全相同(zip 元数据/时间戳有所不同),但按照本仓库其他测试使用的相同往返验证方式,结构等效。tests/ Kookerella.FsWordDsl.Tests/Examples/ 下的每个场景都提交了一个以此方式生成的 script.fsx;Category=Slow 测试组会通过 dotnet fsi 实际执行每个脚本,并检查它是否能重现已提交的 .docx。
C# 封装
Kookerella.CsWordDsl 是 F# 核心之上的惯用、不可变、流畅的 C# 封装,面向不想直接处理 F# 判别联合或选项类型的调用方。每种 F# 类型都有对应的 C# 类型:产品类型使用普通记录,并通过 With*/工厂方法构建;无参数选项使用 enum;其他所有类型则使用封闭的 sealed record 层次结构(abstract record 基类、私有构造函数、嵌套情况)——这与 Excel 仓库自己的 Kookerella.CsOpenXmlDsl 为 CellValue/ConditionalFormatRule 使用的“封闭层次结构”模式相同。引用 Kookerella.CsWordDsl,而不是 Kookerella.FsWordDsl,即可完全不用接触 FSharpOption:
using Kookerella.CsWordDsl;
var doc = Document.Create(
Section.Of([
Block.Paragraph([new Inline.Run("Quarterly Report")], styleId: "Title"),
Block.Paragraph([
new Inline.Run("This report covers "),
new Inline.Run("Q1 2026", new RunStyle { Bold = true }),
Inline.HyperlinkText("full dataset", new HyperlinkTarget.ExternalUrl("https://example.com/data")),
new Inline.Run(" for details.")
])
]));
DocumentIO.Save(doc, "report.docx");
var loaded = DocumentIO.Load("report.docx");
内容控件、表格、修订、更改、批注以及 F# 核心建模的其他所有功能,都以同样的方式提供;每项功能的完整示例请参阅 tests/Kookerella.CsWordDsl.Tests/DocumentTests.cs。CsCodeGen.Generate 是 C# 版的 Document.generateScript:它将 Document 渲染为独立的 C# 文件,目标是 .NET 的“基于文件的应用”功能(dotnet run --file script.cs),而不是 .fsx 脚本:
var script = CsCodeGen.Generate(["#:project path/to/Kookerella.CsWordDsl.csproj"], "output.docx", loaded);
File.WriteAllText("regenerate.cs", script);
DocumentIO 还直接提供 F# 核心另外两种输入和输出方式——基于架构的 XML/JSON(ToXml/FromXml、ToJson/FromJson),以及 F# 脚本生成(GenerateFSharpScript,即 CsCodeGen.Generate 面向 F# 的对应方法)——因此,C# 调用方无需自行引用 Kookerella.FsWordDsl,就能在 C# 中使用 F# 核心的四种 I/O 形式。
有一点设计说明值得明确指出:此封装的记录使用 IReadOnlyList<T> 属性,而 C# 编译器合成的记录相等性不会对列表内容进行深度比较(两个记录即使包含内容相等但实例不同的列表,通过普通 .Equals() 比较也会判为不相等)——Kookerella.CsOpenXmlDsl 自己的记录也有同样的限制。不要在自己的代码中依赖整个 Document 的相等性;应像此仓库自己的 DocumentTests.cs 那样,比较你关心的具体值。
XML
Xml.toDocument/Xml.ofDocument(位于 Xml.fs)是除直接编写 F# 和代码生成之外,使用 DSL 的第三种输入和输出方式:采用真实架构(嵌入程序集中的 Xml.xsd)的纯 XML。携带数据的 DU 情况会变成以该情况命名的元素;无参数选项的情况则会变成属性值或裸字符串,这与 Excel 仓库自己的 Xml.fs 中记录的约定一致。
open System.Xml.Linq
// XML -> Document -> .docx
let doc = XElement.Load "report.xml" |> Xml.ofDocument
Document.save "report.docx" doc
// .docx -> Document -> XML
let xml = Document.load "report.docx" |> Xml.toDocument
xml.Save "report.xml"
以下是包含直接格式设置和超链接的运行内容,以 XML 表示:
<para>
<run>Visit </run>
<hyperlink tooltip="Kookerella on GitHub">
<externalHyperlink>https://github.com/Kookerella-Ltd</externalHyperlink>
<content>
<run styleId="Hyperlink">Kookerella on GitHub</run>
</content>
</hyperlink>
<run> for more.</run>
</para>
Xml.schemaSet() 会加载已编译的架构,供你自行验证任一方向的数据(XDocument.Validate)。tests/Kookerella.FsWordDsl.Tests/Examples/ 下的每个场景都有一个已提交的 document.xml,并在生成该文件的同一测试中以这种方式进行验证。
toDocument 的输出具有确定的排序(Styles/Numbering/TableStyles 按 Id 排序,不受底层 Document 自身列表顺序影响)——段落/运行内容本身已有真实的文档顺序,无需排序;但这三项是通过 ID 引用的目录,其自身列表顺序并无意义。因此,将 document.xml 提交到版本控制并在不同提交间进行差异比较才真正有意义:真正的内容变更会产生范围小且独立的差异,而不是因为目录在两个其他方面完全相同的文档间重新排列,造成虚假的差异。
JSON
Json.toDocument/Json.ofDocument(位于 Json.fs 中)是 DSL 的第四种输入和输出方式,与直接编写 F#、代码生成和 XML 并列:纯 JSON,适用于工具链使用 JSON 而非 XML 的调用方。这里同样遵循 DU case 约定,只是采用 JSON 自身的惯用形式(携带数据的 case 使用单键对象,不带参数的 case 使用纯字符串):
open System.Text.Json.Nodes
// JSON -> Document -> .docx
let doc = JsonNode.Parse(File.ReadAllText "report.json").AsObject() |> Json.ofDocument
Document.save "report.docx" doc
// .docx -> Document -> JSON
let json = Document.load "report.docx" |> Json.toDocument
File.WriteAllText("report.json", json.ToJsonString())
上文中的同一个超链接示例,用 JSON 表示:
{
"para": {
"inlines": [
{ "run": { "text": "Visit " } },
{
"hyperlink": {
"target": { "externalHyperlink": "https://github.com/Kookerella-Ltd" },
"runs": [ { "run": { "text": "Kookerella on GitHub", "styleId": "Hyperlink" } } ],
"tooltip": "Kookerella on GitHub"
}
},
{ "run": { "text": " for more." } }
]
}
}
Xml.toDocument 具有的确定性(Styles/Numbering/TableStyles 按 Id 排序)同样适用于 Json.toDocument 的输出,原因也相同:真正的内容变更只会产生小范围、独立的差异,而不是因为目录在两份其他内容完全相同的文档之间重新排序而产生虚假差异。
与 XML 不同,.NET 没有内置的 JSON Schema 验证器,因此 Json.schema.json(位于仓库中)仅通过本仓库自己的测试套件进行验证(借助仅用于测试的 JsonSchema.Net 依赖项),而不会作为公共 API 暴露——请参阅 Json.fs 自带的文档注释。
构建与测试
dotnet build
dotnet test --filter "Category!=Slow"
dotnet run --project samples/Kookerella.FsWordDsl.Sample
上面的默认测试循环会跳过较慢的 Category=Slow 测试,这些测试会对每个生成的 Examples/*/script.fsx 实际调用 dotnet fsi(每次进程启动都需要数秒)。在快速测试套件至少生成过一次 .fsx 文件后,再显式运行这些测试:
dotnet test --filter "Category=Slow"
直接运行 dotnet test(不添加筛选条件)会同时运行两组测试。
C# 封装自身的测试套件没有快测和慢测之分——CsCodeGenTests.cs 会自行通过 shell 调用 dotnet run --file,因此单次运行就已涵盖 F# 测试套件中慢速组的 C# 对应测试:
dotnet test tests/Kookerella.CsWordDsl.Tests
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。