feat(sdk): 22 个示例插件配齐 skill + 三态启动声明 + 打包期校验

## 用户的三条要求

1. 「所有插件都默认自启会占启动时间,还在 llms 上下文塞大量工具」
   ⇒ 纯工具型改为按需加载,靠 skill 让 agent 知道要先拉起。
2. 「会向 agent 注入输入的部分插件不能这样做」
   ⇒ 9 个必须常驻,其余 13 个按需。
3. 「sdk 是不是没有暴露插件声明自身行为的方案」
   ⇒ 对:auto_start 只在 internal/plugin/manifest.go 有,SDK 零命中。

## 定级依据(代码证据,不是猜描述)

注入 / 钩子 ⇒ 必须常驻(9 个):

	qq         HTTP 监听 NapCat 推送,消息到达要立刻注入
	rss        轮询订阅源,新文章主动推送
	calendar   30s ticker 检查提醒
	memo       5min ticker 检查待办到期
	a2a        TCP 监听外部 Agent 请求
	acp        TCP 监听 ACP 客户端
	browser    ticker + 长命令完成回调
	sanitizer  注册 stage 钩子(无工具,要每轮生效)
	luademo    注册 stage 钩子做示范

纯工具型 ⇒ 按需(13 个):
	weather ocr music bili ai_image files editdoc deepsearch
	plugindev recoverydiag vanblog vikunja commondev

★ commondev 值得单独说:它虽然有 6 处 Inject 调用,但**注入源是自己的
sidecar 完成回投**,而 sidecar 只在 commondev_agent_up 时才拉起
(Start() 里不启 sidecar,通道也在 agent_up 时才注册)。所以没有 agent
调用它时,它不需要做任何事 —— 可以按需。44 个工具 ≈726 tokens 省下来可观。

## 改动

 · 22 个插件各配一个 skill,写明:何时用、先 plugin_start、
   工具清单、**用 plugin_stop 而不是 plugin_disable**
 · 13 个 plg.json 加 `auto_start: false`
 · `sdk/autostart.go`:把三态语义与"按需插件必须有 skill"的理由写进公开 SDK
 · hmapdev 打包期校验:`auto_start=false` 但无 skill/ 目录 ⇒ 直接 exit 1
   (理由:缺 skill 的按需插件**等价于没装**,比没装更糟 —— 占着磁盘让人以为可用)
 · 修 Lua 打包路径:它**完全不走** rawfile/skill 收集,luademo 的 skill 永远打不进去
 · 修 build-examples.sh:原来 `[ ! -x ]` 判"二进制在不在",改了 hmapdev 源码后
   重新打包**仍用旧工具链**(实测:luademo 的 skill 一直是 0)
 · 补 mocksdk.ToolDef.Timeout(上一轮加 ToolDef.Timeout 时漏了镜像)

## 判据

 · TestHasSkillDirRequiresRealSkillMd(空目录/.gitkeep/无 SKILL.md 都不算)
 · TestBuildRejectsOnDemandWithoutSkill(打包期必须真拦住 + exit 1)
 · TestLuaPackagingCollectsSkillAndRawfiles(**验代码行**,不验注释 —— 第一版
   只查符号存在,把 createHmapWithRaw 换回 createHmap 判据照样通过)

## 踩过的坑

 · Lua 分支的结束边界原是 `// Resolve build config`,但那个注释在文件里
   **出现多次**(Go 路径里也有),用它截取会把 Go 分支一起包进来 ⇒
   变异测不出。改成截到 Lua 分支自己的 `return` + `}`。
This commit is contained in:
JianFeeeee
2026-10-18 13:21:47 +08:00
parent e1b458a710
commit 74e2c654ed
42 changed files with 1537 additions and 28 deletions

View File

@ -0,0 +1,45 @@
---
name: a2a-tools
description: Agent-to-Agent 协议通信(TCP 服务端 + 客户端)。本插件**常驻自启**:它在监听外部 Agent 请求。
---
# A2A 通信
## 本插件为什么常驻
a2a 插件在 **TCP 端口上监听**其他 Agent 发来的 Tasks/Send 请求,收到后注入给本 agent 处理。
改成按需加载 ⇒ 端口不监听 ⇒ **外部 Agent 的请求无人应答**,
对方只会看到连接失败或超时,而本机**毫无记录**。
## 工具
| 工具 | 用途 |
|---|---|
| `a2a_query` | 向其他 Agent 发请求 |
| `a2a_discover` | 发现可用的对端 |
| `a2a_configure` | 配置对端地址 |
| `a2a_restart` | 重启监听 |
| `a2a_status` | 查监听状态与已知对端 |
## 注意
- 监听地址/端口在插件配置里。端口冲突时 `a2a_status` 会明确报错。
- 出站 `a2a_query` 需要能连上对端,网络不通时明确报错,不要重试到超时。
## ★ 用 plugin_stop / plugin_start,不要用 plugin_disable / plugin_enable
四个工具看着像同义词,语义差别很大:
| 工具 | 作用 | 跨重启 |
|---|---|---|
| `plugin_stop` | **运行态**关闭,现在停掉 | **不影响**,重启照常加载 |
| `plugin_start` | **运行态**拉起,现在开回来 | 不影响 |
| `plugin_disable` | **持久**禁用 | **会记下来**,开机也不自启 |
| `plugin_enable` | 解除持久禁用 | 会记下来 |
**释放上下文用 `plugin_stop`。** 用 `plugin_disable` 会顺手把"开机自启"
永久关掉 —— 用户下次开机发现插件没了,得再手动开回来。
(本插件声明 `auto_start: false`,所以它本来就不自启;但 `plugin_disable`
仍会写进持久禁用清单,让 `plugin_start` 拒绝拉起,只能再用 `plugin_enable` 解。)

View File

@ -0,0 +1,43 @@
---
name: acp-tools
description: Agent Client Protocol 通信(服务端 + 客户端)。本插件**常驻自启**:它在监听外部 ACP 请求。
---
# ACP 通信
## 本插件为什么常驻
acp 插件在 **TCP 端口上监听** ACP 客户端的 session/new 等请求。
改成按需加载 ⇒ 端口不监听 ⇒ 外部调用方(如编辑器、其他 agent 框架)
连不上,而本机无任何记录。
## 工具
| 工具 | 用途 |
|---|---|
| `acp_query` | 以 ACP 客户端身份向对端发起请求 |
| `acp_configure` | 配置对端 |
| `acp_status` | 查监听与对端状态 |
## 注意
与 a2a 的区别:A2A 是 Agent 对 Agent 的任务协议,ACP 是
"客户端驱动 agent 会话"的协议(更像 IDE 集成)。两者可能同时在跑。
## ★ 用 plugin_stop / plugin_start,不要用 plugin_disable / plugin_enable
四个工具看着像同义词,语义差别很大:
| 工具 | 作用 | 跨重启 |
|---|---|---|
| `plugin_stop` | **运行态**关闭,现在停掉 | **不影响**,重启照常加载 |
| `plugin_start` | **运行态**拉起,现在开回来 | 不影响 |
| `plugin_disable` | **持久**禁用 | **会记下来**,开机也不自启 |
| `plugin_enable` | 解除持久禁用 | 会记下来 |
**释放上下文用 `plugin_stop`。** 用 `plugin_disable` 会顺手把"开机自启"
永久关掉 —— 用户下次开机发现插件没了,得再手动开回来。
(本插件声明 `auto_start: false`,所以它本来就不自启;但 `plugin_disable`
仍会写进持久禁用清单,让 `plugin_start` 拒绝拉起,只能再用 `plugin_enable` 解。)

View File

@ -16,5 +16,6 @@
"outdir": "dist", "outdir": "dist",
"bundle": true, "bundle": true,
"replaces": {}, "replaces": {},
"source_dirs": [] "source_dirs": [],
"auto_start": false
} }

View File

@ -0,0 +1,50 @@
---
name: ai-image-tools
description: AI 图像生成(DALL·E / Stable Diffusion)。按需工具:先 plugin_start ai_image,用完可 plugin_stop。
---
# AI 画图
## 什么时候用
用户要求**生成图片**(画一张、生成插画、做个配图)时。
## 先拉起插件
```
plugin_start name=ai_image
```
| 工具 | 用途 |
|---|---|
| `ai_image_generate` | 按提示词生成图片 |
## 用完可释放
```
plugin_stop name=ai_image
```
## 注意
- 需要配置**上游服务**(DALL·E 或 SD)与各自的密钥;没配时会明确报错,
不要重试或猜一个 URL。
- 生图有成本(按张计费)。一次不满意就调整提示词,不要连续盲试。
- 提示词用**画面描述**(主体/风格/构图/光线),不是命令式句子。
## ★ 用 plugin_stop / plugin_start,不要用 plugin_disable / plugin_enable
四个工具看着像同义词,语义差别很大:
| 工具 | 作用 | 跨重启 |
|---|---|---|
| `plugin_stop` | **运行态**关闭,现在停掉 | **不影响**,重启照常加载 |
| `plugin_start` | **运行态**拉起,现在开回来 | 不影响 |
| `plugin_disable` | **持久**禁用 | **会记下来**,开机也不自启 |
| `plugin_enable` | 解除持久禁用 | 会记下来 |
**释放上下文用 `plugin_stop`。** 用 `plugin_disable` 会顺手把"开机自启"
永久关掉 —— 用户下次开机发现插件没了,得再手动开回来。
(本插件声明 `auto_start: false`,所以它本来就不自启;但 `plugin_disable`
仍会写进持久禁用清单,让 `plugin_start` 拒绝拉起,只能再用 `plugin_enable` 解。)

View File

@ -15,5 +15,6 @@
"outdir": "dist", "outdir": "dist",
"bundle": true, "bundle": true,
"replaces": {}, "replaces": {},
"source_dirs": [] "source_dirs": [],
"auto_start": false
} }

View File

@ -0,0 +1,49 @@
---
name: bili-tools
description: B站视频清晰度查询与下载(yt-dlp)。按需工具:先 plugin_start bili,用完可 plugin_stop。
---
# B站视频
## 什么时候用
用户要**下载 B站视频**,或想知道某个视频有哪些清晰度可选时。
## 先拉起插件
```
plugin_start name=bili
```
| 工具 | 用途 |
|---|---|
| `bili_video` | 查清晰度列表 / 按指定格式下载 |
## 用完可释放
```
plugin_stop name=bili
```
## 注意
- 依赖 **yt-dlp**,未安装时会明确报错。
- 下载目录由插件配置决定,不要假设是当前工作目录。
- 先查清晰度列表再下载,不要盲目选最高(可能体积很大)。
## ★ 用 plugin_stop / plugin_start,不要用 plugin_disable / plugin_enable
四个工具看着像同义词,语义差别很大:
| 工具 | 作用 | 跨重启 |
|---|---|---|
| `plugin_stop` | **运行态**关闭,现在停掉 | **不影响**,重启照常加载 |
| `plugin_start` | **运行态**拉起,现在开回来 | 不影响 |
| `plugin_disable` | **持久**禁用 | **会记下来**,开机也不自启 |
| `plugin_enable` | 解除持久禁用 | 会记下来 |
**释放上下文用 `plugin_stop`。** 用 `plugin_disable` 会顺手把"开机自启"
永久关掉 —— 用户下次开机发现插件没了,得再手动开回来。
(本插件声明 `auto_start: false`,所以它本来就不自启;但 `plugin_disable`
仍会写进持久禁用清单,让 `plugin_start` 拒绝拉起,只能再用 `plugin_enable` 解。)

View File

@ -0,0 +1,55 @@
---
name: browser-tools
description: 统一浏览器能力(搜索/HTTP 抓取/无头渲染/CDP 交互)。本插件**常驻自启**:它有 ticker 与长命令回调,会主动注入。
---
# 浏览器
## 本插件为什么常驻
browser 插件有两处**主动注入**:
1. **30 秒 ticker** —— 检查长任务(渲染、下载)的进度
2. **长时间命令的完成回调** —— 命令跑完后把结果注入回来
改成按需加载 ⇒ 长任务还在跑、插件却被卸载 ⇒ 结果无人接收,
或者在跑的任务变成孤儿进程。
## 工具
| 组 | 工具 |
|---|---|
| 轻量 | `search` `fetch`(HTTP 抓取,快) |
| 渲染 | `render` `html`(无头浏览器,慢) |
| 交互 | `start` `navigate` `screenshot` `click` `type` `scroll` `close` |
| 环境 | `install`(装浏览器依赖) |
## 选哪个
| 需求 | 用 |
|---|---|
| 只要页面文字 | `fetch`(最快,不执行 JS) |
| 页面靠 JS 渲染 | `render` |
| 要点击/填表/截图 | `start` → `navigate` → 交互 → `close` |
## 注意
- 交互式会话**用完必须 `close`**,否则浏览器进程一直占内存。
- `install` 只在缺依赖时用,不要每次都跑。
## ★ 用 plugin_stop / plugin_start,不要用 plugin_disable / plugin_enable
四个工具看着像同义词,语义差别很大:
| 工具 | 作用 | 跨重启 |
|---|---|---|
| `plugin_stop` | **运行态**关闭,现在停掉 | **不影响**,重启照常加载 |
| `plugin_start` | **运行态**拉起,现在开回来 | 不影响 |
| `plugin_disable` | **持久**禁用 | **会记下来**,开机也不自启 |
| `plugin_enable` | 解除持久禁用 | 会记下来 |
**释放上下文用 `plugin_stop`。** 用 `plugin_disable` 会顺手把"开机自启"
永久关掉 —— 用户下次开机发现插件没了,得再手动开回来。
(本插件声明 `auto_start: false`,所以它本来就不自启;但 `plugin_disable`
仍会写进持久禁用清单,让 `plugin_start` 拒绝拉起,只能再用 `plugin_enable` 解。)

View File

@ -0,0 +1,47 @@
---
name: calendar-tools
description: 日历事件与提醒。本插件**常驻自启**:它定时检查提醒并主动通知。
---
# 日历
## 本插件为什么常驻
日历插件有一个 **30 秒的提醒 ticker**。到点的事件会主动注入给 agent,
让 agent 提醒用户。
改成按需加载 ⇒ 没有 ticker ⇒ **提醒永远不会响**。而"提醒不响"比"没有提醒功能"
更糟:用户以为设了闹钟,实际上什么都没发生。
## 工具
| 工具 | 用途 |
|---|---|
| `calendar_event_add` | 新建事件(可带提醒) |
| `calendar_event_list` | 列出事件 |
| `calendar_event_update` | 修改事件 |
| `calendar_event_delete` | 删除事件 |
| `calendar_today` / `week` / `month` | 按范围查看 |
| `calendar_search` | 搜索事件 |
## 注意
- 时间必须确认**时区**。用户说"明天三点"时,先确认是本地时间。
- 重复事件要明确是每天/每周/每月。
## ★ 用 plugin_stop / plugin_start,不要用 plugin_disable / plugin_enable
四个工具看着像同义词,语义差别很大:
| 工具 | 作用 | 跨重启 |
|---|---|---|
| `plugin_stop` | **运行态**关闭,现在停掉 | **不影响**,重启照常加载 |
| `plugin_start` | **运行态**拉起,现在开回来 | 不影响 |
| `plugin_disable` | **持久**禁用 | **会记下来**,开机也不自启 |
| `plugin_enable` | 解除持久禁用 | 会记下来 |
**释放上下文用 `plugin_stop`。** 用 `plugin_disable` 会顺手把"开机自启"
永久关掉 —— 用户下次开机发现插件没了,得再手动开回来。
(本插件声明 `auto_start: false`,所以它本来就不自启;但 `plugin_disable`
仍会写进持久禁用清单,让 `plugin_start` 拒绝拉起,只能再用 `plugin_enable` 解。)

View File

@ -6,7 +6,13 @@
"description": "主管式多仓库开发流水线:登记项目、开工程对话、为每条线拉起内置 pi 并注册一对通道,投递任务给 pi 并在同一会话内续投。", "description": "主管式多仓库开发流水线:登记项目、开工程对话、为每条线拉起内置 pi 并注册一对通道,投递任务给 pi 并在同一会话内续投。",
"author": "HomeAgent", "author": "HomeAgent",
"entry": "plugin.so", "entry": "plugin.so",
"tags": ["commondev", "git", "agent", "develop"], "tags": [
"commondev",
"git",
"agent",
"develop"
],
"targets": "linux/amd64", "targets": "linux/amd64",
"rawfiles": [] "rawfiles": [],
"auto_start": false
} }

View File

@ -7,6 +7,43 @@ author: HomeAgent
# commondev 纪律(主管模式 · 工程对话 · 平台操作) # commondev 纪律(主管模式 · 工程对话 · 平台操作)
## ★ 本插件默认不自启:先拉起再用
commondev 的 44 个工具占约 726 tokens,是本组最大的。因此它**默认不自启**
(`auto_start: false`),不在初始工具清单里。
需要开工程线/派活时:
```
plugin_start name=commondev
```
**用完必须关掉**(44 个工具常驻会持续吃上下文):
```
plugin_stop name=commondev
```
### 什么时候可以关
安全关闭的判据是"**没有在跑的工程线**":
1. 先查有没有活着的 sidecar:`commondev_agent_state`(看 `agent_alive`)
2. 有在跑的线,先停:`commondev_agent_down`(暂停)或
`commondev_agent_interrupt`(只停当前这一轮,保留会话)
3. 确认都不在跑,再 `plugin_stop`
★ **不要在有 sidecar 在跑时直接 disable** —— 进程会变成孤儿。
插件 `Stop()` 里有幂等全清(sidecar / pi 进程一个不留),但那是兜底,
不是正常路径。
### 关闭不影响已做的活
台账(`ledger.json`)、工作目录、分支状态、凭据全部**持久化在磁盘**。
下次 `plugin_start` 后 `commondev_project_list` / `commondev_branch_status`
能看到之前的一切,`commondev_agent_up` 能接着用原来的 session 续投。
本技能三部分,**都可能与你的任务直接相关**: 本技能三部分,**都可能与你的任务直接相关**:
- **第一部分:使用者纪律** —— 怎么开工程、怎么派活。 - **第一部分:使用者纪律** —— 怎么开工程、怎么派活。
@ -853,3 +890,20 @@ pi 进程、sidecar、ssh 会话、workdir(含凭据残留检查)一个不
6. **改了内置插件源码只重启服务** —— 必须重编 homed。 6. **改了内置插件源码只重启服务** —— 必须重编 homed。
7. **手改生成的文档**(`docs/api/*.md` 首行写着「请勿手改」)—— 改 `sdk/*.go` 注释再重新生成。 7. **手改生成的文档**(`docs/api/*.md` 首行写着「请勿手改」)—— 改 `sdk/*.go` 注释再重新生成。
8. **为了让插件生效去改 homed 的源码** —— 先确认它是不是内置插件。 8. **为了让插件生效去改 homed 的源码** —— 先确认它是不是内置插件。
## ★ 用 plugin_stop / plugin_start,不要用 plugin_disable / plugin_enable
四个工具看着像同义词,语义差别很大:
| 工具 | 作用 | 跨重启 |
|---|---|---|
| `plugin_stop` | **运行态**关闭,现在停掉 | **不影响**,重启照常加载 |
| `plugin_start` | **运行态**拉起,现在开回来 | 不影响 |
| `plugin_disable` | **持久**禁用 | **会记下来**,开机也不自启 |
| `plugin_enable` | 解除持久禁用 | 会记下来 |
**释放上下文用 `plugin_stop`。** 用 `plugin_disable` 会顺手把"开机自启"
永久关掉 —— 用户下次开机发现插件没了,得再手动开回来。
(本插件声明 `auto_start: false`,所以它本来就不自启;但 `plugin_disable`
仍会写进持久禁用清单,让 `plugin_start` 拒绝拉起,只能再用 `plugin_enable` 解。)

View File

@ -13,5 +13,6 @@
"retrieval", "retrieval",
"news" "news"
], ],
"targets": "linux/amd64" "targets": "linux/amd64",
"auto_start": false
} }

View File

@ -0,0 +1,58 @@
---
name: deepsearch-tools
description: 真正的联网检索(本地 SearXNG 聚合多引擎)。按需工具:先 plugin_start deepsearch,用完可 plugin_stop。
---
# 联网检索
## 什么时候用
需要**当前**信息(新闻、价格、版本号、事件进展),
而你的训练数据可能过期时。
★ 判断标准:答案是"昨天的"就会出错的问题,就用它。
常识性、不变的知识不用搜。
## 先拉起插件
```
plugin_start name=deepsearch
```
| 工具 | 用途 |
|---|---|
| `deepsearch_search` | 通用检索(标题/URL/摘要/时间) |
| `deepsearch_news` | 新闻检索 |
| `deepsearch_fetch` | 抓取指定 URL 正文 |
| `deepsearch_deep` | 深度检索(多轮、更全) |
| `deepsearch_status` | 查后端服务是否可用 |
## 用完可释放
```
plugin_stop name=deepsearch
```
## 注意
- 后端是**本地 SearXNG**。服务没起时 `deepsearch_status` 会明确报错,
不要编造检索结果。
- 引用检索结果时**带上 URL 和日期**,让用户能自己核对。
- 摘要可能不准,关键事实要 `deepsearch_fetch` 打开原文确认。
## ★ 用 plugin_stop / plugin_start,不要用 plugin_disable / plugin_enable
四个工具看着像同义词,语义差别很大:
| 工具 | 作用 | 跨重启 |
|---|---|---|
| `plugin_stop` | **运行态**关闭,现在停掉 | **不影响**,重启照常加载 |
| `plugin_start` | **运行态**拉起,现在开回来 | 不影响 |
| `plugin_disable` | **持久**禁用 | **会记下来**,开机也不自启 |
| `plugin_enable` | 解除持久禁用 | 会记下来 |
**释放上下文用 `plugin_stop`。** 用 `plugin_disable` 会顺手把"开机自启"
永久关掉 —— 用户下次开机发现插件没了,得再手动开回来。
(本插件声明 `auto_start: false`,所以它本来就不自启;但 `plugin_disable`
仍会写进持久禁用清单,让 `plugin_start` 拒绝拉起,只能再用 `plugin_enable` 解。)

View File

@ -18,5 +18,6 @@
"outdir": "dist", "outdir": "dist",
"bundle": true, "bundle": true,
"replaces": {}, "replaces": {},
"source_dirs": [] "source_dirs": [],
"auto_start": false
} }

View File

@ -0,0 +1,54 @@
---
name: office-tools
description: 办公文档读写转换(docx/xlsx/pptx/md/csv/txt,基于 python-docx 等)。按需工具:先 plugin_start editdoc,用完可 plugin_stop。
---
# 办公文档
## 什么时候用
用户要**新建/读取/编辑/转换** Word、Excel、PPT、Markdown、CSV 文件时。
## 先拉起插件
```
plugin_start name=editdoc
```
| 工具 | 用途 |
|---|---|
| `read_document` | 读文档内容与结构 |
| `create_document` | 新建文档 |
| `create_from_template` | 用模板新建 |
| `edit_document` | 编辑现有文档 |
| `convert_document` | 格式转换(如 docx→md) |
| `office_check` | 检查依赖是否齐全 |
## 用完可释放
```
plugin_stop name=editdoc
```
## 注意
- 依赖 **Python 库**(python-docx / openpyxl / python-pptx)。
先跑 `office_check` 确认,缺库时工具会明确报错。
- 改文档前先 `read_document` 看清结构,避免覆盖掉原有格式与内容。
## ★ 用 plugin_stop / plugin_start,不要用 plugin_disable / plugin_enable
四个工具看着像同义词,语义差别很大:
| 工具 | 作用 | 跨重启 |
|---|---|---|
| `plugin_stop` | **运行态**关闭,现在停掉 | **不影响**,重启照常加载 |
| `plugin_start` | **运行态**拉起,现在开回来 | 不影响 |
| `plugin_disable` | **持久**禁用 | **会记下来**,开机也不自启 |
| `plugin_enable` | 解除持久禁用 | 会记下来 |
**释放上下文用 `plugin_stop`。** 用 `plugin_disable` 会顺手把"开机自启"
永久关掉 —— 用户下次开机发现插件没了,得再手动开回来。
(本插件声明 `auto_start: false`,所以它本来就不自启;但 `plugin_disable`
仍会写进持久禁用清单,让 `plugin_start` 拒绝拉起,只能再用 `plugin_enable` 解。)

View File

@ -6,10 +6,14 @@
"description": "文件系统操作工具集(读取/写入/编辑/列表),提供沙箱化文件访问,支持配置工作目录。", "description": "文件系统操作工具集(读取/写入/编辑/列表),提供沙箱化文件访问,支持配置工作目录。",
"author": "HomeAgent", "author": "HomeAgent",
"entry": "plugin.so", "entry": "plugin.so",
"tags": ["files", "filesystem"], "tags": [
"files",
"filesystem"
],
"targets": "linux/amd64", "targets": "linux/amd64",
"outdir": "dist", "outdir": "dist",
"bundle": true, "bundle": true,
"replaces": {}, "replaces": {},
"source_dirs": [] "source_dirs": [],
"auto_start": false
} }

View File

@ -0,0 +1,54 @@
---
name: file-tools
description: 沙箱内文件读写(read/write/edit/ls)。按需工具:先 plugin_start files,用完可 plugin_stop。
---
# 文件操作
## 什么时候用
需要在**插件配置的工作目录**内读写文件时。
★ 如果你已经有 run_shell / bash 之类的能力,优先用那个 —— 它更直接。
本插件适合**没有 shell 权限**或需要走沙箱路径限制的场景。
## 先拉起插件
```
plugin_start name=files
```
| 工具 | 用途 |
|---|---|
| `files_read` | 读文件内容 |
| `files_write` | 写文件(覆盖) |
| `files_edit` | 局部替换编辑 |
| `files_ls` | 列目录 |
## 用完可释放
```
plugin_stop name=files
```
## 注意
- 只能访问**配置的工作目录**内。走出沙箱会明确报错,不要试图绕。
- 写/改之前先读一遍,确认不是要保留的内容。
## ★ 用 plugin_stop / plugin_start,不要用 plugin_disable / plugin_enable
四个工具看着像同义词,语义差别很大:
| 工具 | 作用 | 跨重启 |
|---|---|---|
| `plugin_stop` | **运行态**关闭,现在停掉 | **不影响**,重启照常加载 |
| `plugin_start` | **运行态**拉起,现在开回来 | 不影响 |
| `plugin_disable` | **持久**禁用 | **会记下来**,开机也不自启 |
| `plugin_enable` | 解除持久禁用 | 会记下来 |
**释放上下文用 `plugin_stop`。** 用 `plugin_disable` 会顺手把"开机自启"
永久关掉 —— 用户下次开机发现插件没了,得再手动开回来。
(本插件声明 `auto_start: false`,所以它本来就不自启;但 `plugin_disable`
仍会写进持久禁用清单,让 `plugin_start` 拒绝拉起,只能再用 `plugin_enable` 解。)

View File

@ -0,0 +1,47 @@
---
name: luademo-tools
description: Lua 插件全功能示例(工具 + 阶段钩子 + 通道 + 数据 API)。本插件**常驻自启**:它注册 stage 钩子做示范。
---
# Lua 插件示例(luademo)
## 这个插件是什么
**示例工程**,不是实用工具。它演示 Lua 插件能做的四件事:
1. 注册工具(`no_memory` / `cleaner` 两种形态)
2. 注册阶段钩子
3. 注册通道
4. 使用数据类 API
## 本插件为什么常驻
它注册了 stage 钩子(与 sanitizer 同理,钩子要每轮生效)。
若只是想看代码示例,可以关掉;若在**验证 Lua 适配层**,必须让它常驻跑着。
```
plugin_stop name=luademo # 仅看代码时
plugin_start name=luademo # 验证 Lua VM / 钩子时
```
## 开发者看这里
读 `example/luademo/` 的源码 + 本文件即可上手写自己的 Lua 插件。
Lua 侧能力边界由内核 `FacilityLua`(Lua 协议适配 VM)决定。
## ★ 用 plugin_stop / plugin_start,不要用 plugin_disable / plugin_enable
四个工具看着像同义词,语义差别很大:
| 工具 | 作用 | 跨重启 |
|---|---|---|
| `plugin_stop` | **运行态**关闭,现在停掉 | **不影响**,重启照常加载 |
| `plugin_start` | **运行态**拉起,现在开回来 | 不影响 |
| `plugin_disable` | **持久**禁用 | **会记下来**,开机也不自启 |
| `plugin_enable` | 解除持久禁用 | 会记下来 |
**释放上下文用 `plugin_stop`。** 用 `plugin_disable` 会顺手把"开机自启"
永久关掉 —— 用户下次开机发现插件没了,得再手动开回来。
(本插件声明 `auto_start: false`,所以它本来就不自启;但 `plugin_disable`
仍会写进持久禁用清单,让 `plugin_start` 拒绝拉起,只能再用 `plugin_enable` 解。)

View File

@ -0,0 +1,45 @@
---
name: memo-tools
description: 待办与备忘录(到期主动提醒)。本插件**常驻自启**:它定时检查待办到期。
---
# 待办与备忘录
## 本插件为什么常驻
memo 插件有一个 **5 分钟的 ticker**,检查有没有到期的待办,到点主动提醒。
改成按需加载 ⇒ 没有 ticker ⇒ **待办到期不提醒**,
而用户设待办就是为了被提醒。
## 工具
| 工具 | 用途 |
|---|---|
| `memo_todo_add` | 加待办(可带截止时间) |
| `memo_todo_complete` | 完成待办 |
| `memo_todo_list` | 列出待办 |
| `memo_todo_delete` | 删除待办 |
| `memo_create` / `memo_list` / `memo_delete` | 备忘录增删查 |
## 注意
待办 vs 备忘录的区别:**待办**有完成状态、会提醒;**备忘录**只是记着。
用户说"提醒我"时用待办,说"记一下"时用备忘录。
## ★ 用 plugin_stop / plugin_start,不要用 plugin_disable / plugin_enable
四个工具看着像同义词,语义差别很大:
| 工具 | 作用 | 跨重启 |
|---|---|---|
| `plugin_stop` | **运行态**关闭,现在停掉 | **不影响**,重启照常加载 |
| `plugin_start` | **运行态**拉起,现在开回来 | 不影响 |
| `plugin_disable` | **持久**禁用 | **会记下来**,开机也不自启 |
| `plugin_enable` | 解除持久禁用 | 会记下来 |
**释放上下文用 `plugin_stop`。** 用 `plugin_disable` 会顺手把"开机自启"
永久关掉 —— 用户下次开机发现插件没了,得再手动开回来。
(本插件声明 `auto_start: false`,所以它本来就不自启;但 `plugin_disable`
仍会写进持久禁用清单,让 `plugin_start` 拒绝拉起,只能再用 `plugin_enable` 解。)

View File

@ -6,10 +6,16 @@
"description": "音乐搜索插件,支持搜索歌曲和查看歌词(基于网易云音乐)", "description": "音乐搜索插件,支持搜索歌曲和查看歌词(基于网易云音乐)",
"author": "HomeAgent", "author": "HomeAgent",
"entry": "plugin.so", "entry": "plugin.so",
"tags": ["music", "song", "lyrics", "网易云"], "tags": [
"music",
"song",
"lyrics",
"网易云"
],
"targets": "linux/amd64", "targets": "linux/amd64",
"outdir": "dist", "outdir": "dist",
"bundle": true, "bundle": true,
"replaces": {}, "replaces": {},
"source_dirs": [] "source_dirs": [],
"auto_start": false
} }

View File

@ -0,0 +1,48 @@
---
name: music-tools
description: 搜索歌曲与查看歌词(网易云音乐)。按需工具:先 plugin_start music,用完可 plugin_stop。
---
# 音乐搜索
## 什么时候用
用户问某首歌、某个歌手、想找歌词时。
## 先拉起插件
```
plugin_start name=music
```
| 工具 | 用途 |
|---|---|
| `music_search` | 按歌名/歌手搜索 |
| `music_lyrics` | 查指定歌曲的歌词 |
## 用完可释放
```
plugin_stop name=music
```
## 注意
只返回**曲目信息与歌词文本**,不下载音频文件。
## ★ 用 plugin_stop / plugin_start,不要用 plugin_disable / plugin_enable
四个工具看着像同义词,语义差别很大:
| 工具 | 作用 | 跨重启 |
|---|---|---|
| `plugin_stop` | **运行态**关闭,现在停掉 | **不影响**,重启照常加载 |
| `plugin_start` | **运行态**拉起,现在开回来 | 不影响 |
| `plugin_disable` | **持久**禁用 | **会记下来**,开机也不自启 |
| `plugin_enable` | 解除持久禁用 | 会记下来 |
**释放上下文用 `plugin_stop`。** 用 `plugin_disable` 会顺手把"开机自启"
永久关掉 —— 用户下次开机发现插件没了,得再手动开回来。
(本插件声明 `auto_start: false`,所以它本来就不自启;但 `plugin_disable`
仍会写进持久禁用清单,让 `plugin_start` 拒绝拉起,只能再用 `plugin_enable` 解。)

View File

@ -6,10 +6,15 @@
"description": "图片文字识别工具,基于 Tesseract OCR 引擎。支持从 URL 或 base64 图片中提取文字内容。", "description": "图片文字识别工具,基于 Tesseract OCR 引擎。支持从 URL 或 base64 图片中提取文字内容。",
"author": "HomeAgent", "author": "HomeAgent",
"entry": "plugin.so", "entry": "plugin.so",
"tags": ["ocr", "image", "text"], "tags": [
"ocr",
"image",
"text"
],
"targets": "linux/amd64", "targets": "linux/amd64",
"outdir": "dist", "outdir": "dist",
"bundle": true, "bundle": true,
"replaces": {}, "replaces": {},
"source_dirs": [] "source_dirs": [],
"auto_start": false
} }

View File

@ -0,0 +1,51 @@
---
name: ocr-tools
description: 从图片中提取文字(Tesseract OCR)。按需工具:先 plugin_start ocr,用完可 plugin_stop。
---
# 图片文字识别
## 什么时候用
用户给了**图片里带文字**(截图、照片、扫描件),需要把文字读出来时。
★ 如果图片是**给模型看内容**(风景、人物、界面布局),不需要 OCR ——
直接用 see_image 看就行。只有"要文字"才用本插件。
## 先拉起插件
```
plugin_start name=ocr
```
| 工具 | 用途 |
|---|---|
| `ocr_ocr_image` | 从 URL 或 base64 图片提取文字 |
## 用完可释放
```
plugin_stop name=ocr
```
## 注意
- 依赖本机 **Tesseract** 引擎;未安装时工具会明确报错,不要当成"图片里没字"。
- 识别结果可能含错字(尤其手写/低分辨率)。引用原文时提示这一点。
## ★ 用 plugin_stop / plugin_start,不要用 plugin_disable / plugin_enable
四个工具看着像同义词,语义差别很大:
| 工具 | 作用 | 跨重启 |
|---|---|---|
| `plugin_stop` | **运行态**关闭,现在停掉 | **不影响**,重启照常加载 |
| `plugin_start` | **运行态**拉起,现在开回来 | 不影响 |
| `plugin_disable` | **持久**禁用 | **会记下来**,开机也不自启 |
| `plugin_enable` | 解除持久禁用 | 会记下来 |
**释放上下文用 `plugin_stop`。** 用 `plugin_disable` 会顺手把"开机自启"
永久关掉 —— 用户下次开机发现插件没了,得再手动开回来。
(本插件声明 `auto_start: false`,所以它本来就不自启;但 `plugin_disable`
仍会写进持久禁用清单,让 `plugin_start` 拒绝拉起,只能再用 `plugin_enable` 解。)

View File

@ -15,5 +15,6 @@
"outdir": "dist", "outdir": "dist",
"bundle": true, "bundle": true,
"replaces": {}, "replaces": {},
"source_dirs": [] "source_dirs": [],
"auto_start": false
} }

View File

@ -0,0 +1,55 @@
---
name: plugindev-tools
description: 插件开发工具链(脚手架/构建 .hmap/SDK 管理)。按需工具:先 plugin_start plugindev。
---
# 插件开发
## 什么时候用
用户要**写一个新插件**、构建 `.hmap`、或管理 SDK 版本时。
## 先拉起插件
```
plugin_start name=plugindev
```
| 工具 | 用途 |
|---|---|
| `plugindev_status` | 查工具链与 SDK 状态 |
| `plugindev_init` | 生成插件工程脚手架 |
| `plugindev_build` | 构建打包 `.hmap` |
| `plugindev_sdk` | SDK 版本安装/切换 |
| `plugindev_projects` | 列已有插件工程 |
## 开发完可释放
```
plugin_stop name=plugindev
```
## 关键纪律
- 构建产物(`.hmap`)**必须用 `plugin_install` 安装**,
不要手工解包替换 `plugin.bin` —— 那会绕过整包校验、备份回滚、
rawfile 全量解包。
- 声明 `auto_start=false`(按需加载)的插件**必须**配 `skill/`,
否则 agent 不知道它存在,等价于没装。打包时会被拦下。
## ★ 用 plugin_stop / plugin_start,不要用 plugin_disable / plugin_enable
四个工具看着像同义词,语义差别很大:
| 工具 | 作用 | 跨重启 |
|---|---|---|
| `plugin_stop` | **运行态**关闭,现在停掉 | **不影响**,重启照常加载 |
| `plugin_start` | **运行态**拉起,现在开回来 | 不影响 |
| `plugin_disable` | **持久**禁用 | **会记下来**,开机也不自启 |
| `plugin_enable` | 解除持久禁用 | 会记下来 |
**释放上下文用 `plugin_stop`。** 用 `plugin_disable` 会顺手把"开机自启"
永久关掉 —— 用户下次开机发现插件没了,得再手动开回来。
(本插件声明 `auto_start: false`,所以它本来就不自启;但 `plugin_disable`
仍会写进持久禁用清单,让 `plugin_start` 拒绝拉起,只能再用 `plugin_enable` 解。)

View File

@ -0,0 +1,48 @@
---
name: qq-tools
description: QQ 消息收发(NapCat 协议桥接)。本插件**常驻自启**:它在监听外部消息,不能按需加载。
---
# QQ 消息
## 本插件为什么常驻
QQ 插件在 **HTTP 端口上监听 NapCat 推送**。用户发来的 QQ 消息**随时可能到达**,
到达后它立刻注入给 agent。
如果把本插件改成按需加载:
- 内核启动时它不会被拉起
- **没有人会去拉起它** —— 因为没有人知道有消息在等着
- 结果:QQ 消息**静默丢失**,用户以为 agent 不理他
所以 `auto_start` 必须保持默认(true)。这不是性能取舍,是**功能正确性**。
## 工具
| 工具 | 用途 |
|---|---|
| `qq_send`(及消息类工具) | 主动发消息 |
## 注意
- 依赖 **NapCat** 服务在运行且已登录。
- 消息可能过期(NapCat 侧有时效),发送失败时会明确报错。
- **不要在群里 @全体成员**、不要连续刷屏。
## ★ 用 plugin_stop / plugin_start,不要用 plugin_disable / plugin_enable
四个工具看着像同义词,语义差别很大:
| 工具 | 作用 | 跨重启 |
|---|---|---|
| `plugin_stop` | **运行态**关闭,现在停掉 | **不影响**,重启照常加载 |
| `plugin_start` | **运行态**拉起,现在开回来 | 不影响 |
| `plugin_disable` | **持久**禁用 | **会记下来**,开机也不自启 |
| `plugin_enable` | 解除持久禁用 | 会记下来 |
**释放上下文用 `plugin_stop`。** 用 `plugin_disable` 会顺手把"开机自启"
永久关掉 —— 用户下次开机发现插件没了,得再手动开回来。
(本插件声明 `auto_start: false`,所以它本来就不自启;但 `plugin_disable`
仍会写进持久禁用清单,让 `plugin_start` 拒绝拉起,只能再用 `plugin_enable` 解。)

View File

@ -17,5 +17,6 @@
"outdir": "dist", "outdir": "dist",
"bundle": true, "bundle": true,
"replaces": {}, "replaces": {},
"source_dirs": [] "source_dirs": [],
"auto_start": false
} }

View File

@ -0,0 +1,53 @@
---
name: recovery-tools
description: 崩溃取证与自检(退出码/信号/config.db 完整性/日志扫描)。按需工具:先 plugin_start recoverydiag。
---
# 故障取证
## 什么时候用
服务**起不来、反复重启、行为异常**,需要查清原因时。
正常运行时不需要它。
## 先拉起插件
```
plugin_start name=recoverydiag
```
| 工具 | 用途 |
|---|---|
| `diag_triage` | 退出码/信号/存活状态粗分 |
| `diag_db` | config.db 完整性检查 |
| `diag_log_scan` | 扫描日志找错误模式 |
| `diag_delta` | 对比两次快照的差异 |
| `diag_loc` | 定位日志/配置位置 |
## 排查完可释放
```
plugin_stop name=recoverydiag
```
## 注意
这些工具**只读**诊断,不修改任何状态。
要修复请明确告知用户具体动作,不要自行修改系统配置。
## ★ 用 plugin_stop / plugin_start,不要用 plugin_disable / plugin_enable
四个工具看着像同义词,语义差别很大:
| 工具 | 作用 | 跨重启 |
|---|---|---|
| `plugin_stop` | **运行态**关闭,现在停掉 | **不影响**,重启照常加载 |
| `plugin_start` | **运行态**拉起,现在开回来 | 不影响 |
| `plugin_disable` | **持久**禁用 | **会记下来**,开机也不自启 |
| `plugin_enable` | 解除持久禁用 | 会记下来 |
**释放上下文用 `plugin_stop`。** 用 `plugin_disable` 会顺手把"开机自启"
永久关掉 —— 用户下次开机发现插件没了,得再手动开回来。
(本插件声明 `auto_start: false`,所以它本来就不自启;但 `plugin_disable`
仍会写进持久禁用清单,让 `plugin_start` 拒绝拉起,只能再用 `plugin_enable` 解。)

View File

@ -0,0 +1,44 @@
---
name: rss-tools
description: RSS/Atom 订阅与更新推送。本插件**常驻自启**:它定时轮询订阅源,新内容要主动推送。
---
# RSS 订阅
## 本插件为什么常驻
RSS 插件每 **N 分钟轮询**所有订阅源。发现新文章后**主动注入**给 agent,
由 agent 决定要不要告诉用户。
改成按需加载 ⇒ 没人轮询 ⇒ **新文章永远不会被发现**,而且不报错。
## 工具
| 工具 | 用途 |
|---|---|
| `rss_subscribe` | 订阅一个源 |
| `rss_unsubscribe` | 取消订阅 |
| `rss_list` | 列出所有订阅 |
| `rss_check_now` | 立刻检查一次(不等下一次轮询) |
## 注意
- 轮询间隔可在插件配置里调(默认 30 分钟)。
- 订阅源失效会记日志但不会删订阅,需要时用 `rss_list` 看状态。
## ★ 用 plugin_stop / plugin_start,不要用 plugin_disable / plugin_enable
四个工具看着像同义词,语义差别很大:
| 工具 | 作用 | 跨重启 |
|---|---|---|
| `plugin_stop` | **运行态**关闭,现在停掉 | **不影响**,重启照常加载 |
| `plugin_start` | **运行态**拉起,现在开回来 | 不影响 |
| `plugin_disable` | **持久**禁用 | **会记下来**,开机也不自启 |
| `plugin_enable` | 解除持久禁用 | 会记下来 |
**释放上下文用 `plugin_stop`。** 用 `plugin_disable` 会顺手把"开机自启"
永久关掉 —— 用户下次开机发现插件没了,得再手动开回来。
(本插件声明 `auto_start: false`,所以它本来就不自启;但 `plugin_disable`
仍会写进持久禁用清单,让 `plugin_start` 拒绝拉起,只能再用 `plugin_enable` 解。)

View File

@ -0,0 +1,38 @@
---
name: sanitizer-tools
description: 清洗 LLM 输出里的工具调用残留(思维泄漏)。本插件**常驻自启**:它注册的是 stage 钩子,不解包任何工具。
---
# 输出清洗
## 本插件为什么常驻
sanitizer **没有工具**,它注册三个 stage 钩子:
| 钩子 | 时机 | 作用 |
|---|---|---|
| `StageOnInput` | 输入时 | 清理输入里的协议残留 |
| `StageAfterToolcall` | 工具调用后 | 去掉工具结果的格式噪声 |
| `StagePostAction` | 动作后 | 清洗即将发出去的回复 |
stage 钩子**必须在每一轮**都生效。停掉它不影响"能不能用",
但会让脏输出漏给用户(模型把内部工具调用语法当正文吐出来)。
所以它属于"基础设施型"插件:既不占工具槽位,也不能按需加载。
## ★ 用 plugin_stop / plugin_start,不要用 plugin_disable / plugin_enable
四个工具看着像同义词,语义差别很大:
| 工具 | 作用 | 跨重启 |
|---|---|---|
| `plugin_stop` | **运行态**关闭,现在停掉 | **不影响**,重启照常加载 |
| `plugin_start` | **运行态**拉起,现在开回来 | 不影响 |
| `plugin_disable` | **持久**禁用 | **会记下来**,开机也不自启 |
| `plugin_enable` | 解除持久禁用 | 会记下来 |
**释放上下文用 `plugin_stop`。** 用 `plugin_disable` 会顺手把"开机自启"
永久关掉 —— 用户下次开机发现插件没了,得再手动开回来。
(本插件声明 `auto_start: false`,所以它本来就不自启;但 `plugin_disable`
仍会写进持久禁用清单,让 `plugin_start` 拒绝拉起,只能再用 `plugin_enable` 解。)

View File

@ -6,6 +6,11 @@
"description": "管理 VanBlog 开源博客系统:文章的增删改查、分类标签管理、草稿发布、备份导出等", "description": "管理 VanBlog 开源博客系统:文章的增删改查、分类标签管理、草稿发布、备份导出等",
"author": "HomeAgent", "author": "HomeAgent",
"entry": "plugin.so", "entry": "plugin.so",
"tags": ["blog", "vanblog", "cms"], "tags": [
"targets": "linux/amd64" "blog",
"vanblog",
"cms"
],
"targets": "linux/amd64",
"auto_start": false
} }

View File

@ -0,0 +1,60 @@
---
name: vanblog-tools
description: 管理 VanBlog 博客(文章/分类/标签/草稿/备份等,28 个工具)。按需工具:先 plugin_start vanblog,用完可 plugin_stop。
---
# VanBlog 博客管理
## 什么时候用
用户要**写/改文章、管分类标签、发布草稿、备份**他们的 VanBlog 站点时。
## 先拉起插件
```
plugin_start name=vanblog
```
工具按用途分组(都带 `vanblog_` 前缀):
| 组 | 工具 |
|---|---|
| 文章 | `list_articles` `get_article` `create_article` `update_article` `delete_article` `search_articles` |
| 草稿 | `manage_drafts` |
| 分类/标签 | `manage_categories` `manage_tags` |
| 媒体/链接 | `manage_images` `manage_links` |
| 站点配置 | `manage_settings` `manage_site` `manage_menu` `manage_about` `manage_social` `manage_rewards` |
| 页面 | `manage_pages` |
| 运维 | `get_analysis` `get_logs` `manage_backup` `manage_caddy` `manage_isr` `manage_pipelines` |
| 协作 | `manage_collaborators` `manage_tokens` `auth` `get_meta` |
## 用完可释放
28 个工具占约 454 tokens,是本组里第二大的。写完博客就关掉:
```
plugin_stop name=vanblog
```
## 关键纪律
- **删除类操作**(`delete_article` 等)不可逆。执行前必须向用户确认是哪一篇。
- 改文章前先 `get_article` 读原文,不要用新内容整篇覆盖。
- 需要 API key 与站点地址;没配时工具会明确报错,不要猜。
## ★ 用 plugin_stop / plugin_start,不要用 plugin_disable / plugin_enable
四个工具看着像同义词,语义差别很大:
| 工具 | 作用 | 跨重启 |
|---|---|---|
| `plugin_stop` | **运行态**关闭,现在停掉 | **不影响**,重启照常加载 |
| `plugin_start` | **运行态**拉起,现在开回来 | 不影响 |
| `plugin_disable` | **持久**禁用 | **会记下来**,开机也不自启 |
| `plugin_enable` | 解除持久禁用 | 会记下来 |
**释放上下文用 `plugin_stop`。** 用 `plugin_disable` 会顺手把"开机自启"
永久关掉 —— 用户下次开机发现插件没了,得再手动开回来。
(本插件声明 `auto_start: false`,所以它本来就不自启;但 `plugin_disable`
仍会写进持久禁用清单,让 `plugin_start` 拒绝拉起,只能再用 `plugin_enable` 解。)

View File

@ -13,5 +13,6 @@
"gtd", "gtd",
"productivity" "productivity"
], ],
"targets": "linux/amd64" "targets": "linux/amd64",
"auto_start": false
} }

View File

@ -0,0 +1,58 @@
---
name: vikunja-tools
description: 管理 Vikunja 任务/项目/看板(28 个工具)。按需工具:先 plugin_start vikunja,用完可 plugin_stop。
---
# Vikunja 任务管理
## 什么时候用
用户要**建/改任务、管项目看板、打标签、看通知**他们的 Vikunja 时。
## 先拉起插件
```
plugin_start name=vikunja
```
工具按用途分组(都带 `vikunja_` 前缀):
| 组 | 工具 |
|---|---|
| 任务 | `tasks` `task_get` `task_create` `task_update` `task_done` `task_delete` `task_bulk` |
| 任务细节 | `task_assignees` `task_labels` `task_comments` `task_relations` `task_attachments` |
| 项目/视图 | `projects` `project_views` |
| 组织 | `labels` `filters` `teams` `sharing` |
| 协作 | `notifications` `subscriptions` `webhooks` `reactions` |
| 其他 | `time_entries` `migrate` `user` `admin` `api` `status` |
## 用完可释放
28 个工具占约 365 tokens。做完任务管理就关掉:
```
plugin_stop name=vikunja
```
## 关键纪律
- **`task_delete` 与 `admin` 不可逆**。执行前向用户确认具体对象。
- `task_bulk` 会一次改多条,先说明影响范围。
- 需要 API token 与实例地址;没配时明确报错,不要猜。
## ★ 用 plugin_stop / plugin_start,不要用 plugin_disable / plugin_enable
四个工具看着像同义词,语义差别很大:
| 工具 | 作用 | 跨重启 |
|---|---|---|
| `plugin_stop` | **运行态**关闭,现在停掉 | **不影响**,重启照常加载 |
| `plugin_start` | **运行态**拉起,现在开回来 | 不影响 |
| `plugin_disable` | **持久**禁用 | **会记下来**,开机也不自启 |
| `plugin_enable` | 解除持久禁用 | 会记下来 |
**释放上下文用 `plugin_stop`。** 用 `plugin_disable` 会顺手把"开机自启"
永久关掉 —— 用户下次开机发现插件没了,得再手动开回来。
(本插件声明 `auto_start: false`,所以它本来就不自启;但 `plugin_disable`
仍会写进持久禁用清单,让 `plugin_start` 拒绝拉起,只能再用 `plugin_enable` 解。)

View File

@ -6,11 +6,15 @@
"description": "天气查询插件(基于 wttr.in),支持实时天气和未来预报", "description": "天气查询插件(基于 wttr.in),支持实时天气和未来预报",
"author": "HomeAgent", "author": "HomeAgent",
"entry": "plugin.so", "entry": "plugin.so",
"tags": ["weather", "forecast", "wttr"], "tags": [
"weather",
"forecast",
"wttr"
],
"targets": "linux/amd64", "targets": "linux/amd64",
"outdir": "dist", "outdir": "dist",
"bundle": true, "bundle": true,
"replaces": {}, "replaces": {},
"source_dirs": [] "source_dirs": [],
"auto_start": false
} }

View File

@ -0,0 +1,58 @@
---
name: weather-tools
description: 查询实时天气与未来预报。按需工具:用到时先 plugin_start weather 拉起,用完可 plugin_stop 释放。
---
# 天气查询
## 什么时候用
用户问**天气**、气温、是否下雨、要不要带伞、未来几天怎么样时。
## 先拉起插件
本插件默认**不自启**(不在初始工具清单里)。调用前先启用:
```
plugin_start name=weather
```
启用后可用工具:
| 工具 | 用途 |
|---|---|
| `weather_current` | 当前实时天气(温度/湿度/风力/天气现象) |
| `weather_forecast` | 未来若干天预报 |
| `weather_set_location` | 设置默认城市(避免每次都说地名) |
## 用完可释放
天气是一次性查询,问完不再需要。连续问同一类问题时保持启用即可;
换了别的话题、且较长时间不会再查天气,可以关掉以释放上下文:
```
plugin_stop name=weather
```
## 注意
- 数据源是 wttr.in,**不需要 API key**。
- 用户没给城市时,先看有没有设置过默认位置;没有就问一句,不要瞎猜。
- 查的是**真实天气**,不要把训练数据里的气温当答案。
## ★ 用 plugin_stop / plugin_start,不要用 plugin_disable / plugin_enable
四个工具看着像同义词,语义差别很大:
| 工具 | 作用 | 跨重启 |
|---|---|---|
| `plugin_stop` | **运行态**关闭,现在停掉 | **不影响**,重启照常加载 |
| `plugin_start` | **运行态**拉起,现在开回来 | 不影响 |
| `plugin_disable` | **持久**禁用 | **会记下来**,开机也不自启 |
| `plugin_enable` | 解除持久禁用 | 会记下来 |
**释放上下文用 `plugin_stop`。** 用 `plugin_disable` 会顺手把"开机自启"
永久关掉 —— 用户下次开机发现插件没了,得再手动开回来。
(本插件声明 `auto_start: false`,所以它本来就不自启;但 `plugin_disable`
仍会写进持久禁用清单,让 `plugin_start` 拒绝拉起,只能再用 `plugin_enable` 解。)

View File

@ -77,7 +77,26 @@ fi
# 发版路径传的是旧名(build.sh:92)故一直没暴露——正是「兼容」二字 # 发版路径传的是旧名(build.sh:92)故一直没暴露——正是「兼容」二字
# 写在注释里、却没在代码里做到的那种缺陷。 # 写在注释里、却没在代码里做到的那种缺陷。
HMAPDEV="${HMAPDEV:-${PLUGINDEV:-$SDK_ROOT/build/hmapdev}}" HMAPDEV="${HMAPDEV:-${PLUGINDEV:-$SDK_ROOT/build/hmapdev}}"
# ★ 工具链二进制**过期就必须重编**,不能只看"文件在不在"。
#
# 原来的判据是 `[ ! -x "$HMAPDEV" ]`:只要二进制存在就跳过构建。
# 于是改了 hmapdev 源码后重新打包,用的仍是**旧工具链** ——
# 而日志看起来完全正常(每个包都"成功")。
#
# 实测代价(2026-10-18):给 Lua 打包路径加上了 skill 收集,
# 但 build/hmapdev 还是前一天编的,重建 22 个包后 luademo 里
# skill 条目仍然为 0。我一度以为修复没生效,实际是工具链没换。
#
# 现在:源码比二进制新就重编。
needs_build=0
if [ ! -x "$HMAPDEV" ]; then if [ ! -x "$HMAPDEV" ]; then
needs_build=1
elif [ -n "$(find "$SDK_ROOT/tools/hmapdev" -name '*.go' -newer "$HMAPDEV" -print -quit 2>/dev/null)" ]; then
echo "[examples] hmapdev 源码比二进制新,重新构建 ..."
needs_build=1
fi
if [ "$needs_build" = "1" ]; then
echo "[examples] 先构建 hmapdev ..." echo "[examples] 先构建 hmapdev ..."
( cd "$SDK_ROOT/tools/hmapdev" && "$GO" build -o "$HMAPDEV" . ) || { ( cd "$SDK_ROOT/tools/hmapdev" && "$GO" build -o "$HMAPDEV" . ) || {
echo "[examples] hmapdev 构建失败,无法继续" >&2; exit 1; } echo "[examples] hmapdev 构建失败,无法继续" >&2; exit 1; }

82
sdk/autostart.go Normal file
View File

@ -0,0 +1,82 @@
package sdk
// 插件**启动形态**的声明契约(plg.json 的 auto_start / requires_facilities)。
//
// ## 三种启动形态
//
// 自动启动(缺省) auto_start 不写或 true
// 启用但不自动启动 auto_start: false ⇒ 待 agent 经 plugin_enable 拉起
// 禁用 registry 的持久开关 ⇒ 连 enable 都不允许
//
// 中间那种是本文件的主要目的:**按需加载**。
//
// ## 为什么要有"启用但不自动启动"
//
// 全部插件默认自启有两个具体代价(2026-10-18 实测):
//
// · 启动时间:22 个示例插件全部拉起(20 个是子进程),冷启动因此多出
// 数百毫秒到秒级,并常驻 24 个进程。
// · 上下文占用:173 个工具定义每轮请求都重发,实测约 2,809 tokens。
//
// 对**纯工具型**插件(weather / ocr / music / bili …)这些代价是白付的:
// 它们只在被调用时才做事,没人调用时只是静静占着槽位。
//
// ## ★ 但有一类插件**不能**按需加载
//
// 会**主动向 agent 注入输入**的插件必须常驻。判断依据是它是否调用了
// Inject* 系列 API 且由**外部事件**驱动(而不是 agent 调用触发):
//
// qq 监听 NapCat 推送,QQ 消息到达要立刻注入
// rss 轮询订阅源,新文章要主动推送
// calendar 定时检查提醒
// memo 定时检查待办到期
// a2a / acp 监听 TCP,外部 Agent 请求要应答
// browser 长命令回调 + ticker
// commondev sidecar 完成结果回投
//
// 把它们改成按需加载 ⇒ 功能**静默失效**:QQ 不响、提醒不来、
// 外部请求无人应答,而且**不报任何错**。这正是本仓反复吃亏的降级形态。
//
// 所以 auto_start 不是一个"性能调优开关",而是**行为契约**:
// 声明 false 等于说"没有人调用我时,我不需要做任何事"。
//
// ## 用法
//
// 本文件只提供**常量与说明**,实际声明写在插件的 plg.json:
//
// {
// "name": "weather",
// "auto_start": false, // ← 纯工具型:按需加载
// "requires_facilities": ["llm"] // 可选:依赖就绪即可提前启动
// }
//
// ★ 只声明 auto_start 而没有配套 skill,agent 就不知道"要先把插件拉起来",
// 结果是把一个能用的插件变成了永远不被使用的插件。**两者必须成对出现**:
// skill 里写清楚"本工具需 plugin_enable 后调用,用完可 plugin_disable"。
// AutoStart 是三种启动形态的取值说明(供文档与工具链共用)。
//
// plg.json 里写的是 **bool**(true/false),这里给的是它的语义名,
// 避免各处用裸 true/false 导致读者不知道 false 到底意味着
// "禁用"还是"启用但不自启"。
const (
// AutoStartOnKernel 表示伴随内核启动(缺少该字段时的默认形态)。
AutoStartOnKernel = true
// AutoStartOnDemand 表示启用但不自启:待 agent 显式拉起。
//
// ★ 用它的插件必须配 skill,否则 agent 无从得知要先拉起。
AutoStartOnDemand = false
)
// OnDemandNeedsSkill 说明 auto_start=false 的插件为什么必须有 skill。
//
// 这条不是"建议"而是**功能完整性的一部分**:
//
// 插件写了 auto_start=false 但没配 skill
// ⇒ 该插件的工具不出现在工具的初始清单里
// ⇒ 模型不知道它存在
// ⇒ 它永远不会被调用,也永远不会被拉起
//
// 也就是说,缺少 skill 的按需插件等价于**没装**,但比没装更糟 ——
// 它占着磁盘和安装记录,让人以为它可用。
const OnDemandNeedsSkill = true

View File

@ -0,0 +1,102 @@
package main
// 打包期校验:`auto_start=false` 的插件**必须**带 skill。
//
// ## 为什么这条是必须的,而不是"建议"
//
// 逻辑链是闭合的:
//
// auto_start=false ⇒ 内核启动时不拉起 ⇒ 工具不在初始清单里
// ⇒ 模型不知道它存在 ⇒ 永远不会被调用、也永远不会被拉起
//
// 也就是说:缺 skill 的按需插件**等价于没装**,但比没装更糟 ——
// 它占着磁盘和安装记录,让人以为它可用(静默降级)。
//
// 所以校验放在**打包期**:比用户装完再困惑早得多。
import (
"os"
"path/filepath"
"strings"
"testing"
)
// ★★★ 判据:有真实 SKILL.md 才算"带 skill"。
func TestHasSkillDirRequiresRealSkillMd(t *testing.T) {
// ① 完全没有 skill/ 目录
root1 := t.TempDir()
if hasSkillDir(root1) {
t.Fatal("★ 没有 skill/ 目录却判定为有 skill")
}
// ② 有空 skill/ 目录
root2 := t.TempDir()
if err := os.MkdirAll(filepath.Join(root2, "skill"), 0o755); err != nil {
t.Fatal(err)
}
if hasSkillDir(root2) {
t.Fatal("★ 空 skill/ 目录被判为有 skill —— 校验形同虚设")
}
// ③ 只有 .gitkeep 的脚手架目录
root3 := t.TempDir()
sk3 := filepath.Join(root3, "skill")
if err := os.MkdirAll(sk3, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(sk3, ".gitkeep"), nil, 0o644); err != nil {
t.Fatal(err)
}
if hasSkillDir(root3) {
t.Fatal("★ 只有 .gitkeep 的脚手架目录被判为有 skill")
}
// ④ 有目录但没有 SKILL.md(空壳)
root4 := t.TempDir()
if err := os.MkdirAll(filepath.Join(root4, "skill", "myskill"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(root4, "skill", "myskill", "README.md"), []byte("x"), 0o644); err != nil {
t.Fatal(err)
}
if hasSkillDir(root4) {
t.Fatal("★ 只有目录没有 SKILL.md 却被判为有 skill —— 注册到 skillmgr 也是空壳")
}
// ⑤ 真有一个 skill
root5 := t.TempDir()
if err := os.MkdirAll(filepath.Join(root5, "skill", "myskill"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(root5, "skill", "myskill", "SKILL.md"), []byte("# x"), 0o644); err != nil {
t.Fatal(err)
}
if !hasSkillDir(root5) {
t.Fatal("★ 有真实 SKILL.md 却判定为没有 skill")
}
}
// ★★★ 判据:cmd_build 里必须真的拦住 auto_start=false 且无 skill 的情况。
func TestBuildRejectsOnDemandWithoutSkill(t *testing.T) {
b, err := os.ReadFile("cmd_build.go")
if err != nil {
t.Fatal(err)
}
src := string(b)
i := strings.Index(src, `if plg.AutoStart != nil && !*plg.AutoStart {`)
if i < 0 {
t.Fatal("找不到 auto_start 的写入分支")
}
body := src[i:]
if j := strings.Index(body[1:], "\n\t}\n"); j > 0 {
body = body[:j+3]
}
if !strings.Contains(body, "hasSkillDir") {
t.Fatal("★ auto_start=false 的分支没有校验 skill —— " +
"按需插件缺 skill 等价于没装(agent 不知道它存在)")
}
if !strings.Contains(body, "os.Exit(1)") {
t.Fatal("★ 校验没有中断打包(os.Exit(1))—— 报错了还继续产出不可用包")
}
}

View File

@ -377,6 +377,24 @@ func writePluginJSON(plg *PlgConfig, platforms []string, entry string) {
} }
// auto_start:只有**显式 false** 才写(缺省 true 与旧行为一致,不写字段最兼容) // auto_start:只有**显式 false** 才写(缺省 true 与旧行为一致,不写字段最兼容)
if plg.AutoStart != nil && !*plg.AutoStart { if plg.AutoStart != nil && !*plg.AutoStart {
// ★★ auto_start=false 的插件**必须**带 skill,否则等价于没装。
//
// 逻辑链:
// auto_start=false ⇒ 内核启动时不拉起它 ⇒ 它的工具不出现在
// 工具的初始清单里 ⇒ 模型不知道它存在 ⇒ 永远不会被调用,
// 也永远不会被拉起。
//
// 也就是说缺 skill 的按需插件**等价于没装**,但比没装更糟:
// 它占着磁盘和安装记录,让人以为它可用。
//
// 所以这不是"建议配 skill",而是"没有 skill 就不该声明按需加载"。
// 在**打包期**拦住,比等用户装上后困惑早得多。
if !hasSkillDir(".") {
fmt.Printf(" error: auto_start=false 但没有 skill/ 目录 —— " +
"按需加载的插件必须配 skill 说明\"先 plugin_enable 再调用\"" +
",否则 agent 不知道它存在(等价于没装)\n")
os.Exit(1)
}
m["auto_start"] = false m["auto_start"] = false
} }
data, _ := json.MarshalIndent(m, "", " ") data, _ := json.MarshalIndent(m, "", " ")
@ -687,8 +705,32 @@ func buildTarget(plg *PlgConfig, target, outDir, sdkPath string) {
} }
} }
hmapName := fmt.Sprintf("%s_lua.hmap", toSnake(plg.NameEn)) hmapName := fmt.Sprintf("%s_lua.hmap", toSnake(plg.NameEn))
createHmap(filepath.Join(outDir, hmapName), pkgFiles)
// ★ Lua 插件以前**完全不走** rawfile/skill 收集(直接 createHmap),
// 于是 luademo 这类 Lua 插件永远带不上 skill —— 而 skill 是 Lua
// 插件唯一能向 agent 说明自己的途径(它没有 Go 侧 RegisterTool 那么
// 直观的文档位)。实测:加了 skill/luademo-tools/SKILL.md 后,
// 打出的 linux_amd64_lua_demo_lua.hmap 里 skill 条目数为 0。
//
// 与 Go 路径对齐:收集 rawfile(工程根 rawfile/ 默认全收)与 skill。
raws, rerr := collectRawFiles(".", plg.RawFiles)
if rerr != nil {
fmt.Printf(" error: rawfiles: %v\n", rerr)
return
}
skills, serr := collectSkillFiles(".")
if serr != nil {
fmt.Printf(" error: skills: %v\n", serr)
return
}
createHmapWithRaw(filepath.Join(outDir, hmapName), pkgFiles, append(raws, skills...))
fmt.Printf(" packaged %s\n", hmapName) fmt.Printf(" packaged %s\n", hmapName)
if len(raws) > 0 {
fmt.Printf(" 已打包 %d 个 rawfile\n", len(raws))
}
if len(skills) > 0 {
fmt.Printf(" 已打包 %d 个 skill 文件\n", len(skills))
}
return return
} }

View File

@ -0,0 +1,84 @@
package main
// Lua 插件的打包路径也必须收集 skill。
//
// ## 缺陷(2026-10-18 实测)
//
// `buildTarget` 里 Go 路径会调 collectRawFiles + collectSkillFiles,
// 而 **Lua 路径是另一段代码**,直接 `createHmap(out, pkgFiles)` ——
// 完全不走 rawfile/skill 收集。
//
// 实测:给 luademo 加了 skill/luademo-tools/SKILL.md 后,
// 打出的 `linux_amd64_lua_demo_lua.hmap` 里 skill 条目数为 **0**。
//
// 这对 Lua 插件伤害更大:Go 插件至少有 RegisterTool 的 Description
// 作为文档位,而 Lua 插件向 agent 说明自己的**唯一**途径就是 skill。
// 缺了它,一个功能完整的 Lua 插件对模型来说是隐形的。
//
// ## 判据
//
// 断言 Lua 分支里出现 collectRawFiles + collectSkillFiles,
// 并且最终走 createHmapWithRaw(而不是只包 pkgFiles 的 createHmap)。
import (
"os"
"strings"
"testing"
)
// ★★★ 判据:Lua 打包分支必须收集 rawfile 与 skill。
func TestLuaPackagingCollectsSkillAndRawfiles(t *testing.T) {
b, err := os.ReadFile("cmd_build.go")
if err != nil {
t.Fatal(err)
}
src := string(b)
// 定位 Lua 分支:`if target == "lua" {` 之后到 `// Resolve build config`
i := strings.Index(src, `if target == "lua" {`)
if i < 0 {
t.Fatal("找不到 Lua 打包分支")
}
// ★ 结束边界必须是 Lua 分支自己的 `return` + `}`,不能用
// "// Resolve build config" —— 实测那个注释在文件里**出现多次**
// (Go 路径里也有),用它截取会把 Go 分支一起包进来,
// 于是"Lua 分支没传 skills"这种变异测不出来。
endMark := "\t\tfmt.Printf(\" packaged %s\\n\", hmapName)"
j := strings.Index(src[i:], "\t\treturn\n\t}")
if j < 0 {
t.Fatal("找不到 Lua 分支的结束位置")
}
// 更稳妥:截到该分支的第一个 `\n\t}\n\n\t// Resolve` 之前。
lua := src[i : i+j]
_ = endMark
// ★★ 只查符号存在是不够的(实测:把 createHmapWithRaw 换回 createHmap,
// 判据依然通过 —— 因为那行 `createHmapWithRaw` 的字符串还在注释里)。
// 必须断言:收集到的 raws/skills 确实**传给了** createHmapWithRaw。
for _, want := range []struct{ sym, why string }{
{"collectRawFiles", "Lua 插件带不上 rawfile(随包资源全部丢失)"},
{"collectSkillFiles", "Lua 插件带不上 skill(它对模型来说是隐形的)"},
} {
if !strings.Contains(lua, want.sym) {
t.Fatalf("★ Lua 打包分支缺少 %s —— %s", want.sym, want.why)
}
}
// 必须是**代码行**在调用 createHmapWithRaw,且参数里带上了 skills。
// 逐行查(跳过注释),避免"注释里有、代码里没有"。
callOK := false
for _, ln := range strings.Split(lua, "\n") {
code := strings.TrimSpace(ln)
if strings.HasPrefix(code, "//") {
continue
}
if strings.Contains(code, "createHmapWithRaw(") && strings.Contains(code, "skills") {
callOK = true
break
}
}
if !callOK {
t.Fatal("★ Lua 打包没有把 skill 传给 createHmapWithRaw —— " +
"收集到了却没写进包,Lua 插件对模型来说仍是隐形的")
}
}

View File

@ -127,3 +127,24 @@ func skillFilesSummary(ents []rawEntry) string {
return fmt.Sprintf("%d 个 skill(%s,共 %d 个文件)", return fmt.Sprintf("%d 个 skill(%s,共 %d 个文件)",
len(list), strings.Join(list, ", "), len(ents)) len(list), strings.Join(list, ", "), len(ents))
} }
// hasSkillDir 判断工程根是否有**至少一个真实 skill**(供打包期校验用)。
//
// 为什么不用简单的 os.Stat(SkillDir):空目录、只有 .gitkeep 的脚手架目录
// 都会让它返回 true,而那种情况下 agent 一样拿不到任何说明 ⇒ 校验形同虚设。
// 这里复用 collectSkillFiles 的实际收集结果,与打包时的判定**同源**,
// 避免"校验说有、打包说没有"的分叉。
func hasSkillDir(root string) bool {
files, err := collectSkillFiles(root)
if err != nil {
return false
}
// 收集到的条目里必须真的有 SKILL.md 才算一个可用 skill:
// 只有目录没有说明文件的条目,注册到 skillmgr 也是空壳。
for _, f := range files {
if strings.HasSuffix(f.zip, "SKILL.md") {
return true
}
}
return false
}

View File

@ -99,6 +99,14 @@ type ToolDef struct {
// TestMockSDKToolDefMatchesSDK 钉住这一致。 // TestMockSDKToolDefMatchesSDK 钉住这一致。
ParallelSafe bool `json:"parallel_safe,omitempty"` ParallelSafe bool `json:"parallel_safe,omitempty"`
Serial bool `json:"serial,omitempty"` Serial bool `json:"serial,omitempty"`
// Timeout 是工具自声明的超时(如 "5m")。
//
// 与公共 SDK 的 ToolDef.Timeout 逐字对齐:内核按
// max(声明值, 参数里的 timeout) 计算,并夹到 10 分钟上限。
// 缺这个字段,本地 yaegi 调试时写 Timeout 不报错,
// 装到内核后却发现声明被忽略(超时仍是一刀切的默认值)。
Timeout string `json:"timeout,omitempty"`
} }
// 上下文策略取值,与公共 SDK 一致。 // 上下文策略取值,与公共 SDK 一致。