Evergreen 插件开发指南
一个文件夹 + 一个 manifest.json,就能给平台加一个插件 —— 左侧目录浏览全部,或从快速开始起步。
文档14 个页面 · 4 大模块
上手快速开始 3 步
门槛无需改框架源码
←→ 方向键翻页 左侧目录树 任意跳转 绿意不息 · 叶灵猫
1插件是什么2 / 14
入门 · 插件是什么

Evergreen 是一个本地优先、AI 原生的桌面/移动微工具平台,所有功能都由插件提供。

每个插件都住在仓库的固定位置,平台启动时自动扫描、自动识别:

evg-base/plugins/<你的插件id>/
    └── module/
        └── manifest.json     ← 插件的「身份证」

平台扫到 module/manifest.json,就知道「有这个插件、它叫什么、长什么样、点开去哪里」。

💡当前最重要的事实: 用户侧插件创作已切换为 HTML 为主。你写 HTML / CSS / JS,平台内置的「HTML 插件创作中心」(html-creator)帮你实时预览、AI 辅助生成、一键导出。

两条创作路径

路径你写什么定位
路径一:HTML 插件网页(.html/.css/.js)+ manifest用户侧主路径,普通用户入口
路径二:Dart 插件Dart/JSON 声明(+ 可选 .py / .exe 进程)开发者模式 / 高级能力

两条路径共享同一套「插件目录 + manifest 声明 + 数据中枢」机制,差别只在「界面由谁画」——数据、设置、AI 能力两条路都能用。

2目录结构 · 插件子目录3 / 14
入门 · 目录结构

无论哪条路径,你的插件都是一个文件夹。Evergreen 采用「插件子目录模型」——一个插件目录可同时装多个子目录,互不冲突,平台按子目录自动发现。theme/、module/、data/、agent/、skin/ 对应五类插件形态(theme / module / data-source / agent / skin),config/ 与 skill/ 为辅助目录:

子目录里面放被谁加载作用
module/manifest.json(HTML 插件还要 index.html)ModuleLoader → ModuleRegistry一个「页面/模块」,最重要
agent/manifest.json + .py(tool.py;.exe 仅存量 legacy)PluginBridge → toolRegistry给 AI 加一个可调用的工具
data/manifest.json + 适配壳(fetch.py / scraper.py)DataSourceLoader → DataOrchestrator自带一个数据源
config/config.jsonSettingsLoader → SharedPreferences声明设置项
theme/theme.jsonThemeLoader → ThemeStore一套主题配色
skill/*.mdSkillLoader → SkillIndex给 AI 的技能包
skin/manifest.json(type:"skin")+ 资源SkinLoaderAI 视图皮肤包(DIY 外观)
⚠️身份证规则: 正式 id 是 manifest 里的 "id" 字段。强烈建议让文件夹名与 id 一致(文件夹 my-tool,id 也用 "my-tool")。id 一旦定下别乱改,改了等于换新插件。

平台怎么发现你的插件

扫描规则来自 evg-base/lib/core/module/module_loader.dart:

  • 只认 plugins/<id>/module/manifest.json 这个文件。
  • manifest 里 "type" 必须是 "module",否则被忽略。
  • 解析失败只在日志里警告,不会让程序崩溃——写错了能安全重试。
  • 未知字段一律静默忽略(容错设计,不是 bug)。

仓库副本在 d:\evg-workplace\ 时,你的插件最终落在 d:\evg-workplace\evg-base\plugins\my-tool\module\manifest.json。

3开发者模式 & AI 创作中心4 / 14
入门 · 开发者模式

Evergreen 有三种视图模式(左栏顶部视图图标 → 扇形菜单切换,已持久化记忆):

模式枚举主区说明
AI 视图AppMode.aiAI 助手默认模式
开发者模式AppMode.developer/dev-hub创作中心聚集地
插件视图AppMode.plugins全部插件侧栏浏览/使用所有插件
🔧开发者模式(/dev-hub)里有 5 个创作中心:主题创作(theme-creator)· 插件制作(html-creator) · 数据爬取(scraper)· DSH · Skill 创作(skill-creator)。深链可直接进入 /dev-hub?plugin=html-creator。

AI 插件创作中心 = html-creator

html-creator 本身也是一个插件(template:"html",路由 /html-creator),它是三栏 IDE:

  • 左栏 · 数据中枢:浏览所有已注册数据源,把数据接进你的插件。
  • 中栏 · 编辑器:写 HTML / CSS / JS(可拖拽分栏、窄屏自动变 Tab)。
  • 右栏 · 实时预览:WebView 即时渲染,主题色自动注入。
  • AI 面板:用自然语言让 AI 生成 / 改稿整个插件。
  • 一键导出:把工程写到 plugins/<id>/module/,同时同步到内置资产目录 assets/plugins_bundle/。
✅对新手最重要的结论: 想加插件 → 切到 开发者模式 → 插件制作(html-creator) → 写网页或让 AI 生成 → 导出,完事。根本不需要懂 Dart。
4HTML 插件入门5 / 14
路径一 · HTML 插件入门

你的插件界面就是一个普通网页,平台用内置 WebView 显示,并注入 platform.* 接口读数据、调 AI、读设置。

步骤一:写 manifest(关键字段 template:"html")

{
  "schemaVersion": "2.0",
  "type": "module",          ← 必须
  "id": "my-first-tool",     ← 全局唯一,建议与文件夹同名
  "name": "我的数据面板",
  "template": "html",        ← 关键:告诉平台「用网页画我」
  "version": "1.0.0",
  "route": "/my-first-tool", ← 以 / 开头,全局唯一
  "nav": { "sidebar": { "section": "自定义", "order": 99 } }
}

这个文件放在 plugins/my-first-tool/module/manifest.json。

步骤二:写 index.html

在 module/ 里放 index.html。完整网页,任意前端写法:

<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>我的面板</title></head>
<body>
  <h1>你好,Evergreen</h1>
  <button onclick="loadData()">加载数据</button>
  <pre id="out"></pre>
  <script>
    async function loadData() {
      const d = await platform.data.get('my_source');
      document.getElementById('out').textContent = JSON.stringify(d, null, 2);
    }
  </script>
</body>
</html>
💡主题色会自动注入为 CSS 变量 --evg-*(--evg-background、--evg-text、--evg-accent 等),CSS 里直接用,主题切换时实时更新。
5platform.* 桥接速查6 / 14
路径一 · platform 桥接

网页里可用的 platform 对象,统一兼容 Windows 与 Android,不用管底层差异。

读数据 platform.data

方法作用示例
data.get(name)拉取数据源(优先缓存)await platform.data.get('my_source')
data.list()列出所有已注册数据源await platform.data.list()
data.refresh(name)强制重新拉取await platform.data.refresh('my_source')
data.subscribe(name, cb)订阅变化(约 5 秒轮询)platform.data.subscribe('my_source', evt => render(evt.data))
data.testConnectivity()测试数据源连通性await platform.data.testConnectivity()

调 AI · 调接口 · 设置 · 存储 · 主题 · 事件

const reply = await platform.ai.chat('帮我总结这段数据');
// 可选风格:explanatory / learning / concise / socratic

const r = await platform.api.call('core', '/core/plugins');
// service 取值:agent / module / data / theme / config / core

const v = await platform.settings.get('someKey');
await platform.settings.set('someKey', 'newValue');

const colors = await platform.theme.getColors();

platform.emit('my-event', { value: 1 });
platform.on('my-event', (payload) => console.log(payload));

// 模块私有键值存储(storage.json),同时注册为数据源 <id>_storage
await platform.storage.set('pref', 'warm');
const pv = await platform.storage.get('pref');

// 运行本插件 manifest `process` 里声明的进程(.py / .exe,白名单 fail-closed)
const res = await platform.process.run('my-tool', { args: ['-u', 'alice'] });
// 常驻进程(scope:"long")另有 start / write / stop / read / onOutput / onExit
🔑最关键的一行: const data = await platform.data.get('数据源名字'); —— 这就是插件「有数据」的起点。
6数据中枢7 / 14
路径一 · 数据中枢

不管路径一还是路径二,要展示真实数据,都来自数据中枢(DataOrchestrator)。两种方式:

  • (a) 复用已有数据源:直接用别人/平台注册好的名字,如 platform.data.get('某名字')。
  • (b) 自己注册一个新数据源:写 plugins/<id>/data/manifest.json + 一个后端程序。

data/manifest.json 长什么样

{
  "type": "data-source",
  "id": "my-first-tool",
  "name": "我的数据源",
  "script": "fetch.py",          ← 适配壳(模型 A:CLI 一次性脚本,推荐)
  "runtime": "python",
  "dataTypes": [
    {
      "name": "my_source",        ← 全局唯一,platform.data.get('my_source') 用它
      "typeArg": "my_source",     ← 传给适配壳的 --type 值
      "category": "自定义",
      "displayName": "我的数据",
      "ttl": "5m",
      "persistentKey": "my_source_cache"
    }
  ]
}

放在 plugins/my-first-tool/data/manifest.json,适配壳(fetch.py / scraper.py,纯标准库优先)放同目录。

📌数据源两种模型:模型 A(推荐)每次拉取执行一次适配壳,stdout 输出顶层 JSON → 注册进数据中枢缓存,get 读缓存 / 过期重拉;模型 B(legacy)为 HTTP 常驻进程(PORT: 发现 + /health 探活 + endpoint)。
7数据源后端契约8 / 14
路径一 · 数据源后端

数据源的 HTTP 常驻后端(模型 B · legacy)要满足的契约(模块 process 常驻进程同样适用);数据源推荐主路径是上页的模型 A 适配壳(fetch.py / scraper.py),无需常驻服务:

  1. 监听 127.0.0.1 上的某个端口。
  2. 启动后往标准输出打印一行 PORT:12345(必须 flush)。
  3. 提供 GET /health → 返回 200 OK。
  4. 提供数据端点(就是 endpoint 写的路径),返回 200 OK + JSON。
  5. 收到终止信号后干净退出。

最小可用的 Python 后端(零第三方依赖)

from http.server import BaseHTTPRequestHandler, HTTPServer
import json

class H(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == '/health':
            self.send_response(200); self.end_headers()
            self.wfile.write(b'{"status":"ok"}')
        elif self.path == '/api/data':
            body = json.dumps({"items": [1, 2, 3]}).encode()
            self.send_response(200)
            self.send_header('Content-Type', 'application/json')
            self.end_headers()
            self.wfile.write(body)
        else:
            self.send_response(404); self.end_headers()

srv = HTTPServer(("127.0.0.1", 0), H)
port = srv.server_address[1]
print(f"PORT:{port}", flush=True)   ← 端口上报,平台靠它发现你
srv.serve_forever()
🌐任何能起 HTTP Server + 打印端口号的语言都能写插件:Python stdlib 零依赖,Go net/http、Node.js http 同样可以。模型 A 适配壳更简单:--type <typeArg> 拉取一次 → stdout 纯 JSON + exit 0,参考 docs/plugin-registry/examples/example-data-zju_grades/。
8Dart 插件 · 模板与 v49 / 14
路径二 · 模板与 v4

这条路径用 Dart/JSON「声明」界面。核心认知:

🧭平台靠 manifest 里的 template 字段决定用哪个渲染器(默认 'v4'),不是靠 renderMode 字段。

平台内置的模板渲染器

清单见 evg-base/lib/renderer/templates/templates_index.json:

template 值渲染器用途
v4(默认可不写)V4ModleTemplate通用组件式模块:多页 + 槽位
htmlHtmlModleTemplate网页容器(路径一)
scraperScraperTemplate所见即所得爬虫
theme-creator / skill-creator对应创作中心模板主题 / Skill 创作
dshDshModleTemplateDeepSeek Harness
zju / classroom / zdbkZjuModleTemplate浙大校园场景

复用 v4:写 0 行 Dart 就能有数据面板

用 pages 声明页面 + 布局 + 组件,全是 JSON:

{
  "schemaVersion": "2.0", "type": "module",
  "id": "my-data-panel", "name": "我的数据面板",
  "icon": "storage", "route": "/my-data-panel",
  "nav": { "sidebar": { "section": "base系统", "order": 20 } },
  "pages": [
    { "id": "main", "label": "📊 我的面板", "default": true,
      "layout": { "type": "grid", "preset": { "columns": 1, "gap": 0 },
        "slots": { "dashboard": { "component": {
          "type": "data-dashboard",
          "config": { "dataSource": { "endpoint": "orch://my_source" } }
        } } } } }
  ]
}
  • 没写 template → 默认 v4 → 走 pages 调度。
  • component.type 是组件名(35+ 种:chat、data-table、chart、form、calendar…)。
  • endpoint:"orch://<数据源名>" 把数据灌进组件。
9Dart 插件 · 进程(.py / .exe)10 / 14
路径二 · 进程(.py/.exe)

除了 pages 纯声明,Dart 插件还能给不同层级挂独立的进程(.py 推荐 / .exe legacy)跑业务逻辑:

层级manifest 路径生命周期
① 模块级process[]模块加载 → 卸载
② 页面级pages[].globalProcess页面激活 → 切走
③ 栏位/组件级pages[].layout.slots.<key>.process栏可见 → 隐藏
④ 动作级actionButtons[].process按钮触发 → 完成即退出

每个进程声明入口(.py + runtime:"python",或 .exe + runtime:"native")和 protocol("http":首行 PORT: + /health;"stdio":标准流通信)。

常驻进程契约(http 协议 · PORT 发现)

  1. 启动 HTTP 服务,监听 127.0.0.1 随机端口。
  2. stdout 首行打印 PORT:<端口>(flush=True),平台捕获后知道端口。
  3. 之后全部 HTTP + JSON 通信,支持 GET/POST。
  4. 组件可见时启动、隐藏时终止,平台统一管理生命周期。
🔌任何能起 HTTP Server + 打印端口号的语言都能写插件:Python stdlib 零依赖,Go net/http、Node.js http 同样可以。
10端口发现 · 文件系统即注册中心11 / 14
路径二 · 端口发现

平台启动 6 个微服务,每个绑定 127.0.0.1 随机端口,并把端口写进文件。你的进程(.py / .exe)读这些文件就能发现所有上游服务——不需要 consul / etcd / 环境变量:

端口文件服务职责
.agent_portAgentHttpServerAI 对话 / 会话 / 工具 / 记忆 / 技能
.config_portConfigHttpServer设置 / 权限 / 插件源
.data_portDataHttpServer数据中枢
.module_portModuleHttpServer模块查询 / 导航 / 路由
.theme_portThemeHttpServer主题
.core_portCoreHttpServer安装 / 卸载 / 更新 / OCR

模块级进程(.py / .exe)常充当聚合代理:读这些端口文件发现上游服务,转发/聚合数据请求,成为整个模块的 API 中枢。

模块生命周期:register → seal

manifest.json → ModuleDescriptor.fromJson() → registry.register()
→ scanAndLoadModules() 启动所有声明的 .py / .exe 进程
→ discoverCapabilities() → registry.setCapabilities()
→ registry.seal()          ← 锁定后只读,写入抛 StateError
→ ModuleHttpServer 启动(仅在 seal 后)

seal 时会校验依赖:任何模块声明了不存在的依赖 id,启动即失败(而非运行时静默异常)。

F常见问题12 / 14
参考 · 常见问题
🔄

改了 manifest,界面没变?

平台是「扫描式」加载,一般重启应用生效。template:"html" 的插件改 index.html 通常热刷新可见;Dart 声明改动多数也要重启/重载。

🧭

route 填什么?

以 / 开头、全局唯一,如 /my-first-tool。别和已占用路由撞(/ai-assistant、/data-dashboard、/html-creator、/dev-hub 等)。

🗂️

左侧栏分组怎么选?

nav.sidebar.section 填 "base主功能"、"base系统" 或 "自定义"。新插件建议先用 "自定义"。

✨

完全不会编程能做插件吗?

能。走 开发者模式 → 插件制作(html-creator)→ AI 面板,用自然语言让 AI 生成插件,再导出即可。这是平台「AI 原生」的核心体验。

🔍

数据名写错会怎样?

platform.data.get('错误名字') 取不到数据,界面可能空白。先用 platform.data.list() 确认。

⚙️

后端程序一定要 .exe 吗?

不一定。扩展名是 .py 或显式 runtime:"python" → 自动用 Python 解释器跑;其它(.exe)→ 直接当可执行文件跑。

🎯

我不懂 Dart,能走路径二吗?

能走「复用 v4」档位:只写 manifest 声明 pages,0 行 Dart。只有自写全新模板才必须写 Dart。

12示例与规范13 / 14
参考 · 示例与规范

平台内置了上架协议规范和五类示例插件(theme / module / data-source / agent / skin),可作为起手模板:

📜

上架协议规范

evg-base/docs/plugin-registry/plugin-registry-spec-v1.md —— 定义外部插件如何被市场发现、下载、加载。

🎨

主题示例

examples/example-theme-warm_study/ —— theme/theme.json 声明一套配色。

📄

HTML 模块示例

examples/example-html-view/ —— module/index.html + manifest.json。

🗃️

数据源示例

examples/example-data-zju_grades/ —— data/manifest.json + scraper.py + config/config.json。

🤖

Agent 工具示例

examples/example-agent-current_time/ —— agent/manifest.json + tool.py(.py 统一主路径)。

🎭

皮肤示例

examples/example-skin-evergreen-logo/ —— skin/manifest.json(type:"skin")+ SVG 资源。

分层红线(复述项目约定)

⚠️core/ 不引用 Flutter Widget;renderer/ 只消费 core 的 barrel 公共 API、不写业务逻辑;插件不能反向依赖 renderer 内部实现。
快速开始
3 步做出你的第一个 HTML 插件,几分钟内跑起来。
步骤共 3 步
耗时约 5 分钟
门槛无需懂 Dart
⚡建文件夹 + manifestStep 01 / 03

在 evg-base/plugins/ 下建一个文件夹,写一个 manifest:

evg-base/plugins/my-first-tool/
    └── module/
        └── manifest.json
{
  "schemaVersion": "2.0",    ← HTML 模块必需(缺失则不渲染)
  "type": "module",
  "id": "my-first-tool",
  "name": "我的第一个插件",
  "template": "html",        ← 关键
  "route": "/my-first-tool",
  "nav": { "sidebar": { "section": "自定义" } }
}
💡文件夹名 my-first-tool 和 id 保持一致,别乱改 id。
⚡写 index.htmlStep 02 / 03

在 module/ 里放一个网页,用 platform.data.get() 读数据:

<h1>你好,Evergreen</h1>
<button onclick="loadData()">加载数据</button>
<pre id="out"></pre>
<script>
  async function loadData() {
    const d = await platform.data.get('my_source');
    document.getElementById('out').textContent = JSON.stringify(d);
  }
</script>
💡主题色自动注入为 --evg-* CSS 变量,CSS 里直接用。
⚡重启 · 用起来Step 03 / 03

重启应用,左侧栏「自定义」分组里就能看到你的插件,点开即是你的网页。

🚀

更省事的方式

切到 开发者模式 → 插件制作(html-creator),三栏 IDE 里写网页 + 实时预览 + AI 生成,一键导出,不用手动建文件夹。

📖

想看更多

「HTML 插件入门」「platform 桥接」「数据中枢」都在左侧目录树里。

全部完成!
你已经掌握了「文件夹 + manifest + index.html」的完整闭环。
✓ 建文件夹
✓ 写 index.html
✓ 重启使用