# 🧩 B27｜AI 辅助开发 Office JS 加载项（自定义函数）— 配套资产

> **系列**：AI 办公实战：从小白到达人
> **文章号**：B27（Track 7 办公开发 / Level 3 进阶）
> **配套中文标题**：AI 辅助开发 Office JS 加载项（自定义函数）
> **配套英文标题**：AI-Assisted Office JS Add-in Development

---

## 一、这是给谁的？解决什么问题？

写给**需要写一个"在 Excel 里能像 SUM 一样调用的自定义函数"**的朋友：财务提成规则要在 50 张表里反复用、跨平台又要 Windows + Mac + Web 都能跑、还不想每次都被宏安全策略拦。

主教程讲的是"用 AI 帮你搭 Office JS 加载项骨架"的思路；本目录给你一套**真实可运行**的最小骨架——直接拿 `manifest.xml` + `functions.json` + `functions.js` + `commands.js` 就能侧载进 Excel 试。

| 文件 | 干什么 |
|------|--------|
| `manifest_zh.xml` | 加载项"身份证"：告诉 Excel 哪里加载代码、命名空间是什么（中文注释） |
| `manifest_en.xml` | 同上，英文注释 |
| `functions_zh.json` | 自定义函数的"说明书"：Excel 靠它显示参数提示和类型（中文） |
| `functions_en.json` | 同上，英文 |
| `functions_zh.js` | 真正的计算逻辑：ADD + DISCOUNT 两个示例函数 + `CustomFunctions.associate` 注册（中文注释） |
| `functions_en.js` | 同上，英文注释 |
| `commands_zh.js` | 任务窗格按钮命令：`Office.onReady` + `Office.actions.associate`（中文） |
| `commands_en.js` | 同上，英文 |
| `package.json` | npm 项目说明 + 一键侧载提示 |

跑通本目录，你就有一份能直接打开 Excel 测试的"自定义函数 + 任务窗格按钮"骨架；下回想加新函数，改两个文件（functions.json + functions.js）就够了。

---

## 二、使用方法（5 步侧载进 Excel）

### 第一步：环境检查

- Node.js 18+ 已装（`node --version` 能输出版本号）；
- Office 版本：**Office 365 订阅版**、较新的 **Office 2021 零售版**、**Mac 版**基本都行；**Office 2019** 与 **LTSC** 通常**不支持**自定义函数。动手前去 Excel → 文件 → 账户看 build 号，对照官方 CustomFunctionsRuntime 1.1 支持矩阵。

### 第二步：把骨架文件塞进一个工程目录

把本目录所有文件复制到本地工程目录，比如 `D:\projects\b27-officejs\`。注意：

- `manifest.xml` 里的 `<Id>` 是个 GUID，**必须换成你自己的**（搜"GUID 生成器"随便生成一个，复制替换）；
- 默认端口是 `https://localhost:3000`，改了端口要同步改 manifest 里 `<bt:Urls>` 的 5 个 URL 和 `<bt:Images>` 的 3 个图标 URL；
- 图标文件 `icon-16.png / icon-32.png / icon-80.png` 必须真实放在 `assets/` 目录，否则侧载时报"资源缺失"。

### 第三步：起一个本地 https 服务

最简单的办法（Windows / Mac / Linux 都行）：

```bash
# 进入工程目录
cd D:\projects\b27-officejs

# 用 npx 起一个 https://localhost:3000 静态服务（也可改 http，但 Excel 侧载更认 https）
npx http-server -p 3000 -c-1 --cors .

# 如果浏览器报"证书无效"，第一次访问 https://localhost:3000 时手动信任一下自签证书
```

### 第四步：侧载到 Excel

1. 打开 Excel；
2. **插入 → 加载项 → 我的加载项 → 上传我的加载项**；
3. 选你的 `manifest.xml`；
4. 看到"我的加载项"分组下出现"我的自定义函数示例"，说明加载成功；
5. 在任意单元格输入 **`=TUTORIAL.ADD(3, 5)`**，回车，应该返回 **8**；
6. 输入 **`=TUTORIAL.DISCOUNT(A1:A10, 0.1)`**，按 A1:A10 的数字之和打 9 折。

### 第五步：让 AI 帮你 debug

卡住了？把报错贴给 AI 用下面这条提示词：

```
我在 Excel 里输入 =TUTORIAL.ADD(3,5) 返回 #NAME? 错误。
- manifest.xml 的 <Namespace resid="Functions.Namespace" /> 通过 ShortStrings 定义为 "TUTORIAL"
- functions.json 里 id = "ADD"
- functions.js 用了 CustomFunctions.associate("ADD", add)
- manifest 里所有 SourceLocation 都是 https://localhost:3000
请列出 5 个最可能的原因和对应排查步骤。
```

---

## 三、文件清单

| 文件名 | 类型 | 作用 | 行数 |
|--------|------|------|------|
| `B27_OfficeJS_中文README.md` | Markdown | 本文件 | — |
| `B27_OfficeJS_EnglishREADME.md` | Markdown | 英文版 README | — |
| `manifest_zh.xml` | XML | 加载项清单（中文注释） | ~125 |
| `manifest_en.xml` | XML | 加载项清单（英文注释） | ~125 |
| `functions_zh.json` | JSON | 函数元数据（中文，含 ADD + DISCOUNT） | ~45 |
| `functions_en.json` | JSON | 函数元数据（英文） | ~45 |
| `functions_zh.js` | JS | 函数实现 + 注册（中文注释） | ~55 |
| `functions_en.js` | JS | 函数实现 + 注册（英文注释） | ~55 |
| `commands_zh.js` | JS | 任务窗格命令（中文） | ~50 |
| `commands_en.js` | JS | 任务窗格命令（英文） | ~50 |
| `package.json` | JSON | npm 工程说明 + 侧载提示 | ~25 |

---

## 四、实战代码（关键片段逐行解读）

### ① manifest.xml：自定义函数三件套 + Namespace

```xml
<Requirements>
  <Sets>
    <!-- CustomFunctionsRuntime 1.1 是自定义函数的硬性要求 -->
    <Set Name="CustomFunctionsRuntime" MinVersion="1.1" />
  </Sets>
</Requirements>
```

```xml
<!-- AllFormFactors 里挂自定义函数扩展点 -->
<AllFormFactors>
  <ExtensionPoint xsi:type="CustomFunctions">
    <Script>    <SourceLocation resid="Functions.Script.Url" />   </Script>
    <Page>      <SourceLocation resid="Functions.Page.Url" />     </Page>
    <Metadata>  <SourceLocation resid="Functions.Metadata.Url" /> </Metadata>
    <!-- 命名空间用 resid 引用 -->
    <Namespace resid="Functions.Namespace" />
  </ExtensionPoint>
</AllFormFactors>
```

```xml
<!-- 命名空间实际值定义在 Resources 里 -->
<bt:ShortStrings>
  <bt:String id="Functions.Namespace" DefaultValue="TUTORIAL" />
</bt:ShortStrings>
```

**三个要点**：
- `CustomFunctionsRuntime 1.1` 缺它必报 `#NAME?`；
- 三处 `SourceLocation` 都用 **子元素**写法（不是 `DefaultValue` 属性），并通过 `resid` 引用 Resources；
- `<Namespace>` 也用 `resid` 引用（不要写 `<Namespace Name="...">` 这种属性写法，Namespace 元素没有 Name 属性）。

### ② functions.json：函数的"说明书"

```json
{
  "functions": [
    {
      "id": "ADD",
      "name": "ADD",
      "description": "把两个数字相加，返回结果。",
      "parameters": [
        { "name": "first",  "description": "第一个数", "type": "number", "dimensionality": "scalar" },
        { "name": "second", "description": "第二个数", "type": "number", "dimensionality": "scalar" }
      ],
      "result": { "type": "number", "dimensionality": "scalar" }
    }
  ]
}
```

**四个字段**：
- `id`：与 `CustomFunctions.associate("ADD", add)` 第一个参数**逐字符相同**（含大小写）；
- `parameters[].type`：`number / string / boolean / any`；
- `parameters[].dimensionality`：`scalar`（单个值）或 `matrix`（单元格区域）；
- `result`：返回值的类型。

### ③ functions.js：函数实现 + 注册

```javascript
/* global CustomFunctions */

// 把两个数相加 —— 真正干活的就一行
function add(first, second) {
  return first + second;
}

// 注册：第一个参数 "ADD" 必须与 functions.json 的 id 完全一致
CustomFunctions.associate("ADD", add);
```

**关键点**：
- 这个文件**只放纯函数**，不要写 `Office.onReady`——后者属于 UI 生命周期，应该在 `commands.js`；
- `CustomFunctions.associate("ADD", add)` 的第一个参数与 `functions.json` 的 `id` 必须完全一致，否则 `#NAME?`；
- 没有 `Excel.customfunction` 这种对象；注册要么走 JSON 元数据 + `CustomFunctions.associate`，要么走 JSDoc `@customfunction`（新写法）。

### ④ commands.js：任务窗格按钮的回调

```javascript
Office.onReady(function (info) {
  if (info.host === Office.HostType.Excel) {
    console.log("Office JS 加载项已就绪");
  }
});

function showUsage(event) {
  try {
    // 干点实际的事，比如弹个对话框、写控制台
    console.log("=TUTORIAL.ADD(3,5) → 8");
  } finally {
    event.completed();   // 必须调，否则按钮一直转圈
  }
}

// 让 manifest 里的按钮能找到这个函数
Office.actions.associate("showUsage", showUsage);
```

**两个铁律**：
- 按钮回调里**必须** `event.completed()`，否则按钮一直转圈、Excel 卡死；
- `Office.onReady` 属于 UI 侧代码，必须放 `commands.js`，**不要**混进 `functions.js`。

---

## 五、常见问题 / 避坑指南

1. **`=TUTORIAL.ADD(3,5)` 返回 `#NAME?`**
   四种最常见：① Excel 版本不支持 CustomFunctionsRuntime 1.1；② Namespace 与 functions.json 的 id 对不上；③ localhost 端口与 manifest 不一致；④ 图标文件不存在，侧载时已经失败。

2. **侧载时报"资源无效"**
   manifest 引用的图标 png 缺失。把 `icon-16/32/80.png` 真放到 `https://localhost:3000/assets/` 下，并在浏览器里能直接访问到。

3. **按钮点击后 Excel 卡死**
   `event.completed()` 没调 / 没放进 `finally`。加 `finally { event.completed(); }` 一劳永逸。

4. **`Office is not defined`**
   没引 `@microsoft/office-js` 库，或者 `commands.html` 里漏了 `<script src="https://appsforoffice.microsoft.com/lib/1/hosted/office.js"></script>`。

5. **改了 functions.js 没生效**
   浏览器有缓存。改完强制刷新（`Ctrl + Shift + R`），或者重新侧载一次 manifest。

---

## 六、下一步

跑通本文档，你就有了"自定义函数 + 任务窗格按钮"的双能力骨架。下一步推荐：

- **想写得"现代化"一点**：换成 JSDoc `@customfunction` 新写法，让构建工具自动从 `functions.ts` 生成元数据，省掉手维护 `functions.json`。
- **异步 / 调接口**：把 `add` 函数改成 `async function`，从 `fetch` 取数据再返回；注意加 `CustomFunctions.associate("ADD", add, { stream: true })` 开启流式返回。
- **加 UI 按钮**：在 `taskpane.html` 里加 `<button onclick="...">` 触发 `Office.context.ui.displayDialogAsync(...)` 弹对话框，是更友好的"说明书"形式。
- **回到 L2 路线**：如果你只是想批量处理 Excel，未必要写加载项——回看 **B25（Python + openpyxl 批量处理）**、**B26（Power Query M 清洗）** 更轻。

---

> 📌 **快速核查清单**（侧载之前再确认一次）：
> - [ ] Excel 是 Office 365 / 2021 零售版 / Mac 版之一；
> - [ ] `manifest.xml` 里 `<Id>` 换成自己的 GUID；
> - [ ] `manifest.xml` 里所有 `localhost:3000` 与 dev server 端口一致；
> - [ ] 图标 `icon-16/32/80.png` 真实存在于 `assets/`；
> - [ ] `functions.json` 的 `id` 与 `functions.js` 里 `CustomFunctions.associate` 第一参数逐字符一致；
> - [ ] `commands.js` 里按钮回调有 `event.completed()`；
> - [ ] `functions.js` 没混进 `Office.onReady`、`commands.js` 没混进纯函数。