Evergreen 是一个本地优先、AI 原生的桌面/移动微工具平台,所有功能都由插件提供。
每个插件都住在仓库的固定位置,平台启动时自动扫描、自动识别:
evg-base/plugins/<你的插件id>/
└── module/
└── manifest.json ← 插件的「身份证」
平台扫到 module/manifest.json,就知道「有这个插件、它叫什么、长什么样、点开去哪里」。
| 路径 | 你写什么 | 定位 |
|---|---|---|
| 路径一:HTML 插件 | 网页(.html/.css/.js)+ manifest | 用户侧主路径,普通用户入口 |
| 路径二:Dart 插件 | Dart/JSON 声明(+ 可选 .py / .exe 进程) | 开发者模式 / 高级能力 |
两条路径共享同一套「插件目录 + manifest 声明 + 数据中枢」机制,差别只在「界面由谁画」——数据、设置、AI 能力两条路都能用。
无论哪条路径,你的插件都是一个文件夹。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.json | SettingsLoader → SharedPreferences | 声明设置项 |
theme/ | theme.json | ThemeLoader → ThemeStore | 一套主题配色 |
skill/ | *.md | SkillLoader → SkillIndex | 给 AI 的技能包 |
skin/ | manifest.json(type:"skin")+ 资源 | SkinLoader | AI 视图皮肤包(DIY 外观) |
"id" 字段。强烈建议让文件夹名与 id 一致(文件夹 my-tool,id 也用 "my-tool")。id 一旦定下别乱改,改了等于换新插件。扫描规则来自 evg-base/lib/core/module/module_loader.dart:
plugins/<id>/module/manifest.json 这个文件。"type" 必须是 "module",否则被忽略。仓库副本在 d:\evg-workplace\ 时,你的插件最终落在 d:\evg-workplace\evg-base\plugins\my-tool\module\manifest.json。
Evergreen 有三种视图模式(左栏顶部视图图标 → 扇形菜单切换,已持久化记忆):
| 模式 | 枚举 | 主区 | 说明 |
|---|---|---|---|
| AI 视图 | AppMode.ai | AI 助手 | 默认模式 |
| 开发者模式 | AppMode.developer | /dev-hub | 创作中心聚集地 |
| 插件视图 | AppMode.plugins | 全部插件侧栏 | 浏览/使用所有插件 |
/dev-hub?plugin=html-creator。html-creator 本身也是一个插件(template:"html",路由 /html-creator),它是三栏 IDE:
plugins/<id>/module/,同时同步到内置资产目录 assets/plugins_bundle/。你的插件界面就是一个普通网页,平台用内置 WebView 显示,并注入 platform.* 接口读数据、调 AI、读设置。
{
"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。
在 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>
--evg-*(--evg-background、--evg-text、--evg-accent 等),CSS 里直接用,主题切换时实时更新。网页里可用的 platform 对象,统一兼容 Windows 与 Android,不用管底层差异。
| 方法 | 作用 | 示例 |
|---|---|---|
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() |
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('数据源名字'); —— 这就是插件「有数据」的起点。不管路径一还是路径二,要展示真实数据,都来自数据中枢(DataOrchestrator)。两种方式:
platform.data.get('某名字')。plugins/<id>/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,纯标准库优先)放同目录。
get 读缓存 / 过期重拉;模型 B(legacy)为 HTTP 常驻进程(PORT: 发现 + /health 探活 + endpoint)。数据源的 HTTP 常驻后端(模型 B · legacy)要满足的契约(模块 process 常驻进程同样适用);数据源推荐主路径是上页的模型 A 适配壳(fetch.py / scraper.py),无需常驻服务:
127.0.0.1 上的某个端口。PORT:12345(必须 flush)。GET /health → 返回 200 OK。endpoint 写的路径),返回 200 OK + JSON。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()
net/http、Node.js http 同样可以。模型 A 适配壳更简单:--type <typeArg> 拉取一次 → stdout 纯 JSON + exit 0,参考 docs/plugin-registry/examples/example-data-zju_grades/。这条路径用 Dart/JSON「声明」界面。核心认知:
template 字段决定用哪个渲染器(默认 'v4'),不是靠 renderMode 字段。清单见 evg-base/lib/renderer/templates/templates_index.json:
template 值 | 渲染器 | 用途 |
|---|---|---|
v4(默认可不写) | V4ModleTemplate | 通用组件式模块:多页 + 槽位 |
html | HtmlModleTemplate | 网页容器(路径一) |
scraper | ScraperTemplate | 所见即所得爬虫 |
theme-creator / skill-creator | 对应创作中心模板 | 主题 / Skill 创作 |
dsh | DshModleTemplate | DeepSeek Harness |
zju / classroom / zdbk | ZjuModleTemplate | 浙大校园场景 |
用 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://<数据源名>" 把数据灌进组件。除了 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":标准流通信)。
127.0.0.1 随机端口。PORT:<端口>(flush=True),平台捕获后知道端口。net/http、Node.js http 同样可以。平台启动 6 个微服务,每个绑定 127.0.0.1 随机端口,并把端口写进文件。你的进程(.py / .exe)读这些文件就能发现所有上游服务——不需要 consul / etcd / 环境变量:
| 端口文件 | 服务 | 职责 |
|---|---|---|
.agent_port | AgentHttpServer | AI 对话 / 会话 / 工具 / 记忆 / 技能 |
.config_port | ConfigHttpServer | 设置 / 权限 / 插件源 |
.data_port | DataHttpServer | 数据中枢 |
.module_port | ModuleHttpServer | 模块查询 / 导航 / 路由 |
.theme_port | ThemeHttpServer | 主题 |
.core_port | CoreHttpServer | 安装 / 卸载 / 更新 / OCR |
模块级进程(.py / .exe)常充当聚合代理:读这些端口文件发现上游服务,转发/聚合数据请求,成为整个模块的 API 中枢。
manifest.json → ModuleDescriptor.fromJson() → registry.register() → scanAndLoadModules() 启动所有声明的 .py / .exe 进程 → discoverCapabilities() → registry.setCapabilities() → registry.seal() ← 锁定后只读,写入抛 StateError → ModuleHttpServer 启动(仅在 seal 后)
seal 时会校验依赖:任何模块声明了不存在的依赖 id,启动即失败(而非运行时静默异常)。
平台是「扫描式」加载,一般重启应用生效。template:"html" 的插件改 index.html 通常热刷新可见;Dart 声明改动多数也要重启/重载。
以 / 开头、全局唯一,如 /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() 确认。
不一定。扩展名是 .py 或显式 runtime:"python" → 自动用 Python 解释器跑;其它(.exe)→ 直接当可执行文件跑。
能走「复用 v4」档位:只写 manifest 声明 pages,0 行 Dart。只有自写全新模板才必须写 Dart。
平台内置了上架协议规范和五类示例插件(theme / module / data-source / agent / skin),可作为起手模板:
evg-base/docs/plugin-registry/plugin-registry-spec-v1.md —— 定义外部插件如何被市场发现、下载、加载。
examples/example-theme-warm_study/ —— theme/theme.json 声明一套配色。
examples/example-html-view/ —— module/index.html + manifest.json。
examples/example-data-zju_grades/ —— data/manifest.json + scraper.py + config/config.json。
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 内部实现。在 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。在 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 里直接用。重启应用,左侧栏「自定义」分组里就能看到你的插件,点开即是你的网页。
切到 开发者模式 → 插件制作(html-creator),三栏 IDE 里写网页 + 实时预览 + AI 生成,一键导出,不用手动建文件夹。
「HTML 插件入门」「platform 桥接」「数据中枢」都在左侧目录树里。