diff --git a/.gitignore b/.gitignore index f248ead..f2effc7 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,8 @@ tools/plugindev/plugindev site_build/ # mkdocs 缓存 .cache/ + +# agent 入口的「全文汇总」是派生件:由 gensite 把 docs/ 下所有 Markdown 拼成一份, +# 每次改任何一页都会整份重写(115KB),进版本库只产生噪声。它由构建产出, +# llms.txt(索引,小且稳定)仍提交。 +docs/llms-full.txt diff --git a/docs/assets/logo-mark.svg b/docs/assets/logo-mark.svg new file mode 100644 index 0000000..31bd759 --- /dev/null +++ b/docs/assets/logo-mark.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/logo.svg b/docs/assets/logo.svg new file mode 100644 index 0000000..e8214ec --- /dev/null +++ b/docs/assets/logo.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/llms.txt b/docs/llms.txt new file mode 100644 index 0000000..21520df --- /dev/null +++ b/docs/llms.txt @@ -0,0 +1,33 @@ +# HomeAgent 插件 SDK + +> 用 Go 或 Lua 为 HomeAgent 编写插件。插件跑在独立进程里,通过公开 SDK 与内核通信: +> 注册工具供模型调用、挂阶段钩子干预流程、读写三层记忆、注册输入输出通道、订阅事件。 +> +> SDK 以 MIT 发布(插件可闭源、可商用,无需回馈)。内核本身是 AGPL-3.0-only。 +> +> 本文件是给 agent 的入口。下列每个链接都是**纯 Markdown 正文**,可直接读, +> 不含 HTML 样板;也可以直接取 https://sdk.homeagent.jianfgit.xyz/llms-full.txt 一次读完全部文档。 + +- [HomeAgent 插件 SDK](https://sdk.homeagent.jianfgit.xyz/index.md): 用 Go 或 Lua 为 HomeAgent 编写插件 +- [桥接装配点(Bridge)](https://sdk.homeagent.jianfgit.xyz/api/bridge.md): 以下方法不是给插件业务代码调的——它们由 hmapdev 生成的运行时在启动时调用,用来把内核能力注入到 SDK 实例 +- [仅内置插件可用的 API](https://sdk.homeagent.jianfgit.xyz/api/builtin-only.md): 这些 API 存在于公开 SDK 包里,但在外部(第三方)插件的运行路径上不可用:要么桥接运行时根本不注入它(拿到 nil),要么内核会拒绝/降级 +- [输入 / 输出通道](https://sdk.homeagent.jianfgit.xyz/api/channels.md): 通道是插件与外界(设备、其他 Agent、外部系统)交换消息的入口 +- [常量与枚举](https://sdk.homeagent.jianfgit.xyz/api/constants.md): SDK 里的取值枚举 +- [事件(Events)](https://sdk.homeagent.jianfgit.xyz/api/events.md): 订阅内核事件 +- [API 参考](https://sdk.homeagent.jianfgit.xyz/api/index.md): 本页所有内容从源码生成(tools/apidoc),签名与说明直接取自 sdk/* +- [生命周期(Lifecycle)](https://sdk.homeagent.jianfgit.xyz/api/lifecycle.md): 插件的启动、停止与卸载回调 +- [LLM 调用](https://sdk.homeagent.jianfgit.xyz/api/llm.md): 让插件自己调用模型(而不是只等模型来调你) +- [记忆(Memory)](https://sdk.homeagent.jianfgit.xyz/api/memory.md): 三层记忆的读写接口:图记忆(三元组关系)、文档记忆(带元数据的文档)、文本记忆(事件流水) +- [其他类型](https://sdk.homeagent.jianfgit.xyz/api/misc.md): 剩余的类型与方法:PluginSDK 本体的访问器、StageContext 的并发控制,以及多模态辅助类型 +- [配置(Settings)](https://sdk.homeagent.jianfgit.xyz/api/settings.md): 声明插件自己的配置项,内核会把它渲染到 WebUI 的设置页,并为每个插件维护独立的配置表 +- [阶段钩子(Stages)](https://sdk.homeagent.jianfgit.xyz/api/stages.md): 在消息处理管道的固定点位插入自己的逻辑 +- [工具(Tools)](https://sdk.homeagent.jianfgit.xyz/api/tools.md): 注册 LLM 可调用的工具 +- [示例插件](https://sdk.homeagent.jianfgit.xyz/examples/index.md): SDK 仓 example/ 下有多个真实可编译的示例插件,覆盖工具注册、通道、记忆读写、LLM 调用、生命周期等常见形态 +- [能力边界:哪些 API 外部插件能用](https://sdk.homeagent.jianfgit.xyz/guide/capability-boundary.md): HomeAgent 有两类插件: +- [第一个 Lua 插件](https://sdk.homeagent.jianfgit.xyz/guide/first-lua-plugin.md): Lua 插件适合轻量、快速原型:不需要 Go 编译环境,改完重启内核即可生效 +- [第一个 Go 插件](https://sdk.homeagent.jianfgit.xyz/guide/first-plugin.md): 以下是一个能直接跑起来的最小插件:注册一个工具、声明一项配置、处理停止与卸载 +- [环境与工具链](https://sdk.homeagent.jianfgit.xyz/guide/getting-started.md): hmapdev 是 SDK 仓提供的统一插件开发工具链,Go 与 Lua 两种插件都用它, +- [多平台构建](https://sdk.homeagent.jianfgit.xyz/guide/multi-platform.md): hmapdev build 默认 bundle 模式,一次产出含三个平台的单个 +- [打包与发布](https://sdk.homeagent.jianfgit.xyz/guide/packaging.md): hmapdev build 一次完成编译与打包,产出 +- [受限 SDK 与安全](https://sdk.homeagent.jianfgit.xyz/guide/security.md): 外部插件与内置插件的区别不只是「能不能调某个函数」,还包含一层安全边界: +- [版本与兼容](https://sdk.homeagent.jianfgit.xyz/versions.md): SDK 版本跟随内核的中版本,patch 位恒为 diff --git a/mkdocs.yml b/mkdocs.yml index 74a8e03..49d8529 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -11,12 +11,18 @@ site_description: 用 Go 或 Lua 为 HomeAgent 编写插件 —— API 参考与 site_url: https://sdk.homeagent.jianfgit.xyz/ copyright: MIT 许可 · JianFeeeee +# 主题覆盖目录:只覆盖 footer.html(补备案号,见该文件里的说明)。 docs_dir: docs site_dir: site_build theme: name: material language: zh + # custom_dir 必须写在 theme 下(顶层会被判为未知配置)。 + custom_dir: overrides + # 品牌图标:与主站 introduce 同一份 logo(曾用 Material 默认,不是我们的)。 + logo: assets/logo-mark.svg + favicon: assets/logo.svg features: - navigation.instant - navigation.instant.progress diff --git a/overrides/partials/footer.html b/overrides/partials/footer.html new file mode 100644 index 0000000..8c1698a --- /dev/null +++ b/overrides/partials/footer.html @@ -0,0 +1,59 @@ +{#- + footer.html 覆盖:在版权行下补**备案号**。 + + 为什么要覆盖主题文件:Material 的 copyright 只渲染 config.copyright(一个字符串), + 而备案号必须是**带链接的 HTML**且含两个条目(ICP + 公安),塞不进那个字段。 + + 另外把「许可」写进 footer:本站内容与主站一致受 AGPL 约束, + 读者在任何页面底部都能看到,不必翻到首页。 +-#} + diff --git a/tools/apidoc/build.sh b/tools/apidoc/build.sh index f912a1c..0c30bc6 100755 --- a/tools/apidoc/build.sh +++ b/tools/apidoc/build.sh @@ -1,10 +1,11 @@ #!/usr/bin/env bash # 生成并构建插件 SDK 文档站。 # -# 两步: -# 1. apidoc —— 从 sdk/*.go 提取公开 API 面(签名/注释/分层)→ JSON -# 2. gensite —— 把 JSON 渲染成 docs/api/*.md + 检索索引 -# 然后 mkdocs 构建静态站。 +# 四步: +# 1. apidoc —— 从 sdk/*.go 提取公开 API 面(签名/注释/分层)→ JSON +# 2. gensite —— 把 JSON 渲染成 docs/api/*.md + 检索索引 + llms.txt +# 3. mkdocs —— 构建静态站 +# 4. copy_agent —— 把 Markdown 源搬进站点产物(mkdocs 只渲染 .md,不复制) # # 为什么要脚本而不是手敲:API 参考是**生成物**,必须与源码同步, # 否则文档会悄悄过时(这是文档站最常见的死法)。 @@ -18,17 +19,44 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" cd "$ROOT" TMP_API="${TMPDIR:-/tmp}/homeagent-sdk-api.json" +SITE=site_build -echo "=== 1/3 提取 API 面 ===" +# copy_agent_files 把 docs/ 下的 Markdown 原样复制进站点产物。 +# +# 为什么必须复制:mkdocs 只把 .md **渲染**成 HTML,不会把它们放进产物目录。 +# 但 agent 需要 Markdown 原文(省 token、不含主题样板),所以 llms.txt 里 +# 指的 /api/tools.md 必须真实可访问。llms.txt 与 llms-full.txt 由 gensite 生成。 +copy_agent_files() { + local n=0 rel dir + while IFS= read -r -d '' f; do + rel="${f#docs/}" + [ "${rel##*/}" = "README.md" ] && continue + dir="$(dirname "$rel")" + [ "$dir" != "." ] && mkdir -p "$SITE/$dir" + cp "$f" "$SITE/$rel" + n=$((n + 1)) + done < <(find docs -name '*.md' -print0) + echo " 复制 $n 个 Markdown 到 $SITE/(供 agent 直读)" +} + +echo "=== 1/4 提取 API 面 ===" go run ./tools/apidoc -pkgdir ./sdk -out "$TMP_API" -echo "=== 2/3 渲染文档页与检索索引 ===" +echo "=== 2/4 渲染文档页、检索索引与 agent 入口 ===" go run ./tools/apidoc/gensite -api "$TMP_API" -out ./docs -examples ./example -echo "=== 3/3 构建静态站 ===" +echo "=== 3/4 构建静态站 ===" if [ "${1:-}" = "serve" ]; then + # 预览模式也要能取到 .md(agent 入口),故先构建一次再起服务。 + mkdocs build --strict >/dev/null + copy_agent_files exec mkdocs serve fi mkdocs build --strict + +echo "=== 4/4 供 agent 直读的 Markdown ===" +copy_agent_files + echo -echo "完成。产物在 site_build/,本地预览:tools/apidoc/build.sh serve" +echo "完成。产物在 $SITE/,本地预览:tools/apidoc/build.sh serve" +echo "agent 入口:$SITE/llms.txt(目录)、$SITE/llms-full.txt(全文)" diff --git a/tools/apidoc/gensite/agent.go b/tools/apidoc/gensite/agent.go new file mode 100644 index 0000000..8e46de3 --- /dev/null +++ b/tools/apidoc/gensite/agent.go @@ -0,0 +1,170 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "sort" + "strings" +) + +// 给 agent 用的入口。 +// +// 为什么需要:文档站是给**人**看的(HTML + 主题 + JS 搜索),但越来越多读者是 +// agent —— 它们要的是「一次拿到结构化事实」,而不是渲染后的页面。让 agent 去 +// 爬 HTML 既浪费 token(主题样板占大头)又容易漏内容。 +// +// 因此额外产出三样东西: +// +// /llms.txt 站点的**目录**:每个页面一行,带 URL 与一句话说明 +// /llms-full.txt 全部文档**正文**(Markdown)拼成一份,可一次读完 +// /.md 每个页面的 Markdown 原文(含生成的 API 页) +// /.json 机器可读版(API 页有结构化符号) +// +// 约定沿用 llms.txt 社区规范(Jeremy Howard 提出):llms.txt 是给「读目录」 +// 用的精简索引,llms-full.txt 是给「一次读全」用的大文件。 +const llmsHeader = `# HomeAgent 插件 SDK + +> 用 Go 或 Lua 为 HomeAgent 编写插件。插件跑在独立进程里,通过公开 SDK 与内核通信: +> 注册工具供模型调用、挂阶段钩子干预流程、读写三层记忆、注册输入输出通道、订阅事件。 +> +> SDK 以 MIT 发布(插件可闭源、可商用,无需回馈)。内核本身是 AGPL-3.0-only。 +> +> 本文件是给 agent 的入口。下列每个链接都是**纯 Markdown 正文**,可直接读, +> 不含 HTML 样板;也可以直接取 %s 一次读完全部文档。 + +` + +// writeAgentEntrypoints 产出 llms.txt 与 llms-full.txt,并把每个页面同时写成 +// `.md`(Markdown 原文)。返回写出的页面数。 +// +// 注意:mkdocs 只会把 `.md` 渲染成 HTML,不会把它们复制到站点产物里。 +// 所以这里除了写 docs/,构建后还要把 Markdown 副本搬进 site_build/ +// (见 build.sh 的 copy_agent_files)。 +func writeAgentEntrypoints(docsDir, siteDir string) (int, error) { + type entry struct { + rel string // 相对 docs/ 的路径,如 api/tools.md + title string + desc string + } + var entries []entry + + err := filepath.Walk(docsDir, func(path string, info os.FileInfo, err error) error { + if err != nil || info.IsDir() { + return nil + } + if !strings.HasSuffix(path, ".md") { + return nil + } + if filepath.Base(path) == "README.md" { + return nil + } + rel, _ := filepath.Rel(docsDir, path) + rel = filepath.ToSlash(rel) + + body, err := os.ReadFile(path) + if err != nil { + return nil + } + title, desc := firstHeadingAndDesc(string(body)) + entries = append(entries, entry{rel: rel, title: title, desc: desc}) + return nil + }) + if err != nil { + return 0, err + } + + // 排序:首页最前,其余按路径。 + sort.Slice(entries, func(i, j int) bool { + if entries[i].rel == "index.md" { + return true + } + if entries[j].rel == "index.md" { + return false + } + return entries[i].rel < entries[j].rel + }) + + base := "https://sdk.homeagent.jianfgit.xyz" + var idx strings.Builder + fmt.Fprintf(&idx, llmsHeader, base+"/llms-full.txt") + for _, e := range entries { + // URL 就是 .md 的落地路径(构建后把 docs/**/*.md 复制进站点产物)。 + // 不要把 index.md 改成 index/ —— 那样指向的是 HTML 页而不是 Markdown 源。 + mdURL := base + "/" + e.rel + fmt.Fprintf(&idx, "- [%s](%s)", e.title, mdURL) + if e.desc != "" { + fmt.Fprintf(&idx, ": %s", e.desc) + } + idx.WriteString("\n") + } + if err := os.WriteFile(filepath.Join(docsDir, "llms.txt"), []byte(idx.String()), 0o644); err != nil { + return 0, err + } + + var full strings.Builder + fmt.Fprintf(&full, "# HomeAgent 插件 SDK — 完整文档\n\n") + full.WriteString("(本文件由 tools/apidoc/gensite 从 docs/ 汇总生成,供 agent 一次读取。)\n\n") + full.WriteString("---\n\n") + for _, e := range entries { + body, err := os.ReadFile(filepath.Join(docsDir, e.rel)) + if err != nil { + continue + } + fmt.Fprintf(&full, "\n\n## <%s>\n\n", e.rel) + full.Write(body) + full.WriteString("\n") + } + fullPath := filepath.Join(docsDir, "llms-full.txt") + if err := os.WriteFile(fullPath, []byte(full.String()), 0o644); err != nil { + return 0, err + } + + // 把 llms.txt / llms-full.txt 也复制进站点产物(mkdocs 不搬运非 md 页面)。 + // 各页面的 .md 副本由 build.sh 统一复制——那时 docs/ 已经定稿。 + if siteDir != "" { + if err := os.MkdirAll(siteDir, 0o755); err == nil { + _ = copyFile(filepath.Join(docsDir, "llms.txt"), filepath.Join(siteDir, "llms.txt")) + _ = copyFile(fullPath, filepath.Join(siteDir, "llms-full.txt")) + } + } + return len(entries), nil +} + +// firstHeadingAndDesc 取首个 `# 标题` 与紧随其后的第一段(作一句话说明)。 +func firstHeadingAndDesc(body string) (title, desc string) { + lines := strings.Split(body, "\n") + for i, l := range lines { + l = strings.TrimSpace(l) + if strings.HasPrefix(l, "# ") && title == "" { + title = strings.TrimSpace(strings.TrimPrefix(l, "# ")) + // 往下找第一段非空、非标题、非注释、非命令的文本。 + for j := i + 1; j < len(lines); j++ { + t := strings.TrimSpace(lines[j]) + if t == "" || strings.HasPrefix(t, "#") || + strings.HasPrefix(t, "