简体中文
English 简体中文 繁體中文
Methodox 首页

扩展

扩展为 Methodox Office 添加命令、工作表函数和脚本库,面向所有安装它的人,而不是仅针对某一个文档。扩展使用与文档自身的脚本相同的语言编写,即 Lua、C# 或 Divooka 图,并遵循相同的规则运行。

扩展可以添加什么

内容 出现位置
菜单命令 在扩展所针对的应用程序(Impulse、Formula、Expo 和 Nu)中,作为 Tools ▸ Extensions 的条目出现,每个命令都在窗口的文档上运行,并作为 Edit ▸ Undo 中的一个步骤
工作表函数 在每个 Formula 工作簿中以 =PREFIX.NAME(...) 形式使用,列在 Insert Function 中;前缀属于扩展,例如 TOOLS. 或 FX.,并且永远不会替换工作簿中已有的名称
脚本库 可以在任何脚本中按名称加载:Lua 中为 require("rates"),C# 中为 #load "rates"

扩展能做的就只有这些。它不能处理文档事件,不能添加自己的菜单、工具栏按钮或按键,也不能改变打开、保存或重新计算的行为,因此在有人运行扩展所添加的内容之前,安装扩展绝不会改变文档的行为。语言包将来会成为第四种扩展。

扩展的代码可以做什么

与文档自身的代码可以做的完全相同(参见编程模型):命令在一个事务中编辑其运行所在的文档,失败时事务会回滚;函数根据其实参进行计算,不改变任何内容,并受到与文档的 LUA. 函数相同的时间限制;库在加载它的脚本内部运行。与文档一样,由版本决定图可以使用什么。

扩展只能通过宿主访问网络(Lua 中为 host.getJson(...),C# 中为 Host.GetJson(...)),并且只能访问其清单中列出的网站。你在安装之前就能看到这些网站,安装即授予访问权限,并且权限授予的是该版本的确切包:代码不同的更新是另一个包。扩展无法获得文件访问权限和完全信任。

安装和启用

在 Start Centre 的 Extensions 页面上:使用 Install from File…,或将 .dvb 拖放到 Start Centre 上,或从目录安装:官方目录 Methodox Office Extensions 对所有人列出,Add Catalogue… 可添加其他目录。每个扩展的卡片上,为其适用的每个应用程序各有一个复选框;取消勾选即可在该应用程序中禁用它。Uninstall 会将其删除。

扩展只为你自己安装,位于 %AppData%\DivookaOffice\extensions,因此不需要管理员权限,并且一个人的扩展不会影响另一个人。

签名的包

包可以由其发布者签名,无论包来自何处(目录、文件或拖放),Methodox Office 都会在安装任何内容之前检查签名。

  • Verified(已验证):包使用 Methodox Office 信任的发布者密钥签名,并且签名中注明的是该发布者。发布者会在扩展卡片上以及安装前的询问中带勾号显示。
  • Not verified(未验证):包未签名,或者使用 Methodox Office 不认识的密钥签名。显示的发布者只是包对自身的声明;请阅读扩展的用途及其可能访问的内容,并且只有在信任其来源时才安装。
  • Refused(已拒绝):包已签名,但其文件与签名不再匹配,或者签名中注明的发布者与其密钥的发布者不同。这说明包在签名后被更改过,因此不会安装。

Methodox Office 信任的密钥内置于程序中。每个发布者自行生成并保管自己的私钥,只把公钥部分提供给 Methodox;Methodox 自己的私钥由 Methodox 保管,从不放在套件或其代码仓库中。目前列表中只有 Methodox 的密钥,因此 Methodox 签名的包会显示为已验证,而来自任何其他发布者的包即使已签名也会显示为未验证;随 Extensions 文件夹附带的官方扩展之所以受信任,是因为它们随 Office 一起提供。如果某个发布者的密钥后来从列表中移除,其包会重新显示为未验证。目录的索引还包含每个包的 SHA-256,下载的文件如果不匹配,会被删除而不是安装。

Office Tools

Office Tools 是随 Methodox Office 提供的官方扩展。每个人第一次启动某个应用程序时,它会被安装并启用;它可以被禁用,但不能卸载。

位置 作用
Summary Sheet Formula ▸ Tools ▸ Extensions 按第一列对第一个表格(或 A1 处的数据块)进行分组,将数值列汇总到 Summary 工作表上,并在每列下方放置一个实时的 SUM
Format All Tables Impulse ▸ Tools ▸ Extensions 为每个表格设置细灰色边框、带底纹的粗体标题行,并在每页重复标题行
TOOLS.SLUG、TOOLS.WORDCOUNT、TOOLS.INITIALS、TOOLS.SPELLNUMBER Formula 单元格 =TOOLS.SPELLNUMBER(1234.5) 的结果为 "One thousand two hundred thirty-four and 50/100"
tools Lua 和 C# 脚本 local tools = require("tools");在 C# 中先 #load "tools",然后使用 Tools.Slug(...)

通过命令行

mos 无需 Start Centre 即可管理扩展,还可以构建扩展:

命令 作用
mos extension pack <folder> [-o <file.dvb>] 检查清单以及其中列出的每个脚本是否存在,然后将扩展的文件夹打包为包;打印包的 SHA-256
mos extension index <folder> [--title <name>] 为包含包的文件夹写入 index.json,使该文件夹成为目录
mos extension install <file.dvb> 为你安装一个包
mos extension install <catalogue> <id> 从目录地址下载并安装扩展
mos extension list 列出已安装的扩展及其启用位置
mos extension enable <id> [<app>...]、disable 在指定的应用程序中(或在其适用的所有应用程序中)启用或禁用扩展
mos extension uninstall <id> 删除你安装的扩展

包

扩展是一个 Divooka 应用包,即 .dvb 文件:一个 zip 文件,根目录下有 divooka.json,脚本位于 OfficeExtensions/ 下,按惯例每个应用程序一个文件夹,库则放在 Shared/ 中。这与 Divooka 用于其自身库的包格式相同。

ExchangeRates/
  divooka.json
  README.md
  OfficeExtensions/
    Formula/InsertRates.lua      a command
    Formula/functions.lua        the FX. functions
    Impulse/RatesTable.csx       a command, in C#
    Shared/rates.lua             the "rates" library

divooka.json 声明所有内容;它没有列出的脚本永远不会运行。

{
  "format": "divooka-app-bundle/1",
  "kind": "library",
  "title": "Exchange Rates",
  "package": {
    "name": "exchange-rates",
    "version": "1.0.0",
    "description": "Today's exchange rates in Formula and Impulse: a Rates sheet, a rates table, and functions that format an amount in a currency.",
    "authors": [ "Example Publisher" ],
    "license": "MIT",
    "readme": "README.md",
    "requires": { "methodox-office": ">=0.4.0" }
  },
  "office": {
    "id": "example/exchange-rates",
    "publisher": "Example Publisher",
    "apps": [ "Formula", "Impulse" ],
    "commands": [
      { "id": "insert-rates", "title": "Insert Exchange Rates", "app": "Formula", "script": "OfficeExtensions/Formula/InsertRates.lua", "description": "Writes today's rates against the base currency onto a Rates sheet." },
      { "id": "rates-table", "title": "Insert Rates Table", "app": "Impulse", "script": "OfficeExtensions/Impulse/RatesTable.csx", "description": "Adds a table of today's rates for the major currencies at the end of the document." }
    ],
    "functions": [ { "prefix": "FX", "script": "OfficeExtensions/Formula/functions.lua" } ],
    "libraries": [ { "name": "rates", "script": "OfficeExtensions/Shared/rates.lua" } ],
    "capabilities": [ "network.get https://api.frankfurter.dev" ]
  }
}
字段 含义
kind 对于扩展,始终为 library
package.name、package.version 包的名称及其版本,格式为 major.minor.patch
package.requires methodox-office 及其可运行的 Methodox Office 版本,例如 >=0.4.0;在安装和加载时检查
office.id owner/name,扩展的唯一标识
office.publisher 制作者,显示在其卡片上
office.apps 扩展适用的应用程序:Impulse、Formula、Expo、Nu
office.commands[] 每个命令的 id、其在菜单上的 title、命令所在 Tools ▸ Extensions 所属的 app、其 script,以及作为工具提示显示的 description
office.functions[] 一个 prefix 和一个函数脚本,其中名称为大写的函数会成为 PREFIX.NAME
office.libraries[] 库的 name 及其 script;同一个名称可以同时有 Lua 脚本和 C# 脚本
office.capabilities[] 其代码可以从中获取数据的网站,每项格式为 network.get https://<origin>

函数脚本的写法与工作簿自身的工作表函数相同:每个名称为大写的顶层函数都是一个函数,其上方的 --- 注释是它的说明。

构建扩展:Exchange Rates 示例

Exchange Rates 使用了所有扩展点:Formula 中的一个 Lua 命令、Impulse 中的一个 C# 命令、两个函数和一个库,以及一个网站。先按上面的结构创建文件夹和清单,然后编写四个脚本。

库 OfficeExtensions/Shared/rates.lua 通过宿主获取数据,因此只能访问清单中列出的网站:

local rates = {}

local symbols = { USD = "$", EUR = "€", GBP = "£", JPY = "¥", CNY = "¥", KRW = "₩", INR = "₹", CAD = "C$", AUD = "A$", CHF = "CHF " }

--- Today's rates against a base currency: { date = "2026-10-30", base = "USD", rates = { EUR = 0.89, ... } }.
function rates.latest(base)
  base = (base or "USD"):upper()
  return host.getJson(host.url("https://api.frankfurter.dev/v1/latest?base={0}", base))
end

--- The currency's symbol, or its code and a space when it has none here.
function rates.symbol(code)
  code = tostring(code or ""):upper()
  return symbols[code] or (code .. " ")
end

--- An amount written in a currency, rounded to its usual places: 1234.5 in EUR is "€1,234.50".
function rates.format(amount, code)
  local places = (code == "JPY" or code == "KRW") and 0 or 2
  local text = string.format("%." .. places .. "f", math.abs(amount))
  local whole, fraction = text:match("^(%d+)(%.?%d*)$")
  whole = whole:reverse():gsub("(%d%d%d)", "%1,"):reverse():gsub("^,", "")
  return (amount < 0 and "-" or "") .. rates.symbol(code) .. whole .. fraction
end

return rates

函数脚本 OfficeExtensions/Formula/functions.lua 中的函数会成为 =FX.FORMAT(1234.5, "EUR") 和 =FX.SYMBOL("GBP")。它们只格式化工作表提供的数据,不获取任何内容:

local rates = require("rates")

--- An amount written in a currency: =FX.FORMAT(1234.5, "EUR") is "€1,234.50".
---@param amount number
---@param code string
function FORMAT(amount, code)
  return rates.format(amount, code:upper())
end

--- A currency's symbol: =FX.SYMBOL("GBP") is "£".
---@param code string
function SYMBOL(code)
  return (rates.symbol(code):gsub(" $", ""))
end

Formula 命令 OfficeExtensions/Formula/InsertRates.lua 将今日汇率写入 Rates 工作表;如果文档有 Base 参数,则以该参数为基准货币:

local rates = require("rates")

-- Reading a parameter the document does not have is an error, so it is read under pcall.
local has, base = pcall(function() return param.Base end)
if not has or base == nil or base == "" then base = "USD" end
local today = rates.latest(base)

local codes = {}
for code in pairs(today.rates) do codes[#codes + 1] = code end
table.sort(codes)

local rows = {}
for _, code in ipairs(codes) do rows[#rows + 1] = { code, today.rates[code] } end

local sheet = doc.sheets:find("Rates")
if sheet then sheet.usedRange:clear() else sheet = doc.sheets:add("Rates") end
local written = sheet:range("A1"):write(rows, { header = { "Currency", "Per " .. today.base } })
written:resize(1, 2).format.bold = true
sheet:cells(2, 2):resize(#rows, 1).format.numberFormat = "0.0000"
sheet:cells(#rows + 3, 1).value = "Rates of " .. today.date .. " from frankfurter.dev"

print(string.format("Wrote %d rates against %s for %s.", #rows, today.base, today.date))

Impulse 命令 OfficeExtensions/Impulse/RatesTable.csx 用 C# 编写,其中 Doc 是 Impulse 文档,Host 负责获取数据:

string[] wanted = { "EUR", "GBP", "JPY", "CNY", "CAD", "AUD" };
JsonNode today = Host.GetJson("https://api.frankfurter.dev/v1/latest?base=USD")!;

object?[,] cells = new object?[wanted.Length + 1, 2];
cells[0, 0] = "Currency";
cells[0, 1] = "Per US dollar";
for (int i = 0; i < wanted.Length; i++)
{
    cells[i + 1, 0] = wanted[i];
    cells[i + 1, 1] = today["rates"]![wanted[i]]!.GetValue<double>().ToString("0.0000", System.Globalization.CultureInfo.InvariantCulture);
}

Doc.Paragraphs.Add("Exchange rates on " + (string?)today["date"]);
var table = Doc.AddTable(wanted.Length + 1, 2, cells);
table.SetBorders();
table.BoldRow(1);
Print("Added rates for " + wanted.Length + " currencies.");

然后打包、安装并试用:

mos extension pack ExchangeRates -o exchange-rates-1.0.0.dvb
mos extension install exchange-rates-1.0.0.dvb
mos extension list

将相同的文件打包两次,会写出完全相同的字节。打开一个工作簿并输入 =FX.FORMAT(1234.5, "EUR"),或者选择 Tools ▸ Extensions ▸ Insert Exchange Rates。在编写扩展代码时,可以先在 Scripts 窗格中编写脚本,等它实现了你想要的功能后再打包。

为扩展签名

office-extension-signer 是 Parcel NExT 代码仓库中的一个工具(Parcel.NExT/Utilities/OfficeExtensionSigner),它可以生成发布者的密钥对、为包签名,并以与 Office 完全相同的方式检查包:

office-extension-signer keygen --publisher "Example Publisher" --out <folder outside any repository>
office-extension-signer sign exchange-rates-1.0.0.dvb --key <private.pem> --publisher "Example Publisher"
office-extension-signer verify exchange-rates-1.0.0.dvb

keygen 会写出一个私钥文件和一个公钥文件。私钥只能由你自己保管:任何拥有私钥的人都可以以你的名义签名,因此切勿提交它、将其放入构建文件夹或发送给他人。发布者名称就是 Office 显示为已验证的名称,因此请完全按照希望显示的样子填写。要让 Office 将你的包显示为已验证,公钥必须在 Office 信任的密钥之中,这些密钥内置于每个发行版本中。

签名是包内的 divooka.signature.json,覆盖发布者名称、密钥以及包中的所有其他文件。因此签名随包一起传递,将相同的文件重新压缩会保留签名,而更改、添加、删除或重命名任何文件都会使签名失效。请在写入目录索引之前签名,因为签名会添加一个文件并改变包的 SHA-256。对于已验证的包,verify 的退出代码为 0;对于未签名或由 Office 不信任的密钥签名的包,退出代码为 3;对于签名不匹配的包,退出代码为 4。

通过目录提供扩展

目录是一个包含若干包的文件夹,旁边有一个 index.json,可以由任何 Web 服务器或共享提供;无需运行任何服务。官方目录就是这样一个目录,位于 https://office.methodox.io/extensions/;Start Centre 会将其列在首位,直到你移除它,再次添加其地址即可恢复。要提供你自己的目录,请将包放入一个文件夹,并在旁边写入索引:

mos extension index <folder> --title "My extensions"

通过任何 Web 服务器提供该文件夹或将其共享,然后在 Start Centre 中使用 Add Catalogue… 添加其地址;在命令提示符下运行 mos extension install <address> example/exchange-rates 即可从中安装。

尚未支持

语言包、Math 中的扩展命令、为套件中附带的官方扩展签名、获取数据的函数(获取数据的函数会返回 #N/A),以及具有文件访问权限或完全信任的扩展。

另请参阅