## 目标与最终效果

这一章完成一个可以在独立窗口打开的 **文本整理效率插件**。用户输入文字，点击按钮后合并连续空白；关闭或停用时移除自己的界面和事件。

示例不请求网络、不读写文件、不依赖模型账号，便于先验证扩展机制。下载源码后，代码与本页引用来自同一份示例。

[下载完整效率插件源码](/official-design-v20261028u/example-tool.zip)

## 建立目录

```text
my-example-tool/
  manifest.json
  main.js
  modules/
    text.js
    view.js
  styles.css
  .tests/text.test.cjs
  README.txt
```

按[环境准备](environment.html)把目录放进开发版对应的工具扩展目录，重新扫描并显式启用。入口文件不能放在多套一层的子目录里。

## 第一步：声明清单


```json
{
  "id": "my-example-tool",
  "name": "文本整理示例",
  "version": "1.0.0",
  "apiVersion": 1,
  "minAppVersion": "2.7.5",
  "author": "Your Name",
  "description": "一个包含完整打开与清理流程的示例工具",
  "category": "tool",
  "type": "thirdparty",
  "main": "main.js",
  "scripts": [
    "modules/text.js",
    "modules/view.js"
  ],
  "styles": [
    "styles.css"
  ],
  "permissions": [],
  "slot": "workshop",
  "button": {
    "label": "文本整理",
    "order": 100
  }
}
```


纯逻辑先加载，视图后加载，最后由 main 注册。`permissions` 留空，因为本例不调用文件或模型能力；不需要的能力不要提前声明。

## 第二步：分离可测试的业务逻辑


```javascript
(function () {
  window.MyExampleTool = window.MyExampleTool || {};
  MyExampleTool.normalize = function (value) {
    return String(value).replace(/\s+/g, ' ').trim();
  };
})();
```


将文本处理与 DOM 分开后，输入边界可以直接用 Node 测试。这里使用自己的 `MyExampleTool` 命名空间，避免覆盖宿主对象。

## 第三步：创建界面并保存清理函数


```javascript
(function () {
  window.MyExampleTool = window.MyExampleTool || {};
  var cleanup = null;

  MyExampleTool.dispose = function () {
    if (cleanup) cleanup();
    cleanup = null;
  };

  MyExampleTool.render = function (host) {
    MyExampleTool.dispose();
    if (!host) throw new Error('没有可用的插件容器');

    var root = document.createElement('section');
    root.className = 'my-example-tool';
    var title = document.createElement('h2');
    title.textContent = '文本整理';
    var input = document.createElement('textarea');
    input.placeholder = '在这里粘贴需要整理的文字';
    input.setAttribute('aria-label', '待整理文本');
    var button = document.createElement('button');
    button.type = 'button';
    button.textContent = '整理空白';
    var status = document.createElement('p');
    status.setAttribute('role', 'status');

    function run() {
      input.value = MyExampleTool.normalize(input.value);
      status.textContent = '整理完成，结果保留在输入框中。';
    }
    button.addEventListener('click', run);
    root.append(title, input, button, status);
    host.appendChild(root);
    cleanup = function () {
      button.removeEventListener('click', run);
      root.remove();
    };
  };
})();
```


请注意四个细节：

1. 只在 `host` 中追加自己的根节点，不替换宿主整个页面。
2. 用户文本通过 `value` 或 `textContent` 处理，不拼成 HTML。
3. 监听函数保留引用，在 `dispose` 中精确移除。
4. 再次 render 前先 dispose，避免重复按钮和重复事件。

## 第四步：注册生命周期


```javascript
(function () {
  Mrite.plugins.register({
    id: 'my-example-tool',
    activate: function (api, hostElement) {
      MyExampleTool.render(hostElement);
    },
    deactivate: function () {
      MyExampleTool.dispose();
    }
  });
})();
```


`activate(api, hostElement)` 由宿主调用，`api` 是当前窗口的实际桥接对象，第二个参数是挂载节点。入口注册阶段不应该提前运行文件操作或启动后台任务。

`deactivate()` 负责回收插件自己的资源。未来增加请求、订阅或编辑器时，也要将其清理放进这一条生命周期链。

## 第五步：限制样式范围


```css
.my-example-tool { padding: 24px; color: #172033; }
.my-example-tool h2 { margin: 0 0 16px; }
.my-example-tool textarea {
  display: block; width: 100%; min-height: 180px;
  box-sizing: border-box; padding: 12px; resize: vertical;
  border: 1px solid #b9c6d8; border-radius: 8px;
  font: inherit; background: #fff; color: #172033;
}
.my-example-tool button {
  margin-top: 12px; padding: 9px 16px;
  border: 0; border-radius: 7px; background: #245dba;
  color: #fff; cursor: pointer;
}
.my-example-tool p { line-height: 1.7; }
```


所有规则从 `.my-example-tool` 开始。避免直接改 `body`、通用 `.panel` 或全局 `button`。长内容在自己的区域中换行或滚动，不能撑宽整个桌面窗口。

## 第六步：运行与验证

1. 扫描、启用并打开 `my-example-tool`。
2. 输入 `  alpha    beta  `，点击后得到 `alpha beta`。
3. 输入空白、中文、多行文字和已有正常文本，观察结果。
4. 关闭后重新打开，确认只有一个按钮。
5. 停用再启用，确认没有旧事件继续触发。

然后在示例目录运行：

```powershell
node --test .tests/text.test.cjs
```

## 接下来增加哪一项能力

需要连接真实文件时，继续阅读[文件接口](file-api.html)；需要保存编辑状态和处理异步回调时，阅读[生命周期](plugin-lifecycle.html)。每次增加一个能力，先核对目标宿主是否存在对应方法，再测试失败和取消路径。

> [!TIP]
> 这份 ZIP 是开发源码。完成测试后，再按[打包与上线](publishing.html)准备正式发行资料，不把示例 ZIP 当成生产插件包。
