mirror of
https://gitcode.com/JianFeeeee/homeagent-sdk.git
synced 2026-09-25 03:18:06 +00:00
feat(docs): 换品牌图标 + 补备案号 + 给 agent 的直读入口
三件事,都是「站点上线后看出来的」问题。 ## ① 图标用的是 Material 默认,不是我们的 站上 favicon 是 mkdocs-material 自带的那张(`assets/images/favicon.png`), 与主站 introduce 不一致。改为引入主站同一份 logo: - `docs/assets/logo.svg` —— 原样(浅色底,适合 favicon) - `docs/assets/logo-mark.svg` —— 去掉底色矩形(否则在靛蓝页头上是个白方块) `theme.favicon` / `theme.logo` 分别指向它们。实测子页面路径也正确 (mkdocs 生成 `../../assets/logo.svg`,不是错误的相对路径)。 ## ② 缺备案号 `introduce` 底部有 ICP + 公安备案,文档站没有。Material 的 footer 只渲染 `config.copyright` 一个字符串,塞不进「两条带链接的备案」——所以覆盖了 `overrides/partials/footer.html`(`theme.custom_dir`),并顺带把许可也写进底部, 读者在任何一页都能看到,不必翻到首页。 ## ③ 没有给 agent 的入口 站点是给人看的(HTML + 主题 + JS 搜索),但越来越多读者是 agent。让它们爬 HTML 既浪费 token(样板占大头)又容易漏内容。 新增 `tools/apidoc/gensite/agent.go`,随构建产出: | 路径 | 内容 | |---|---| | `/llms.txt` | 站点目录:每页一行,带 URL 与一句话说明(4.3KB)| | `/llms-full.txt` | 全部文档正文拼成一份,可一次读完(115KB)| | `/<page>.md` | 每个页面的 Markdown 原文,含生成的 API 页(text/markdown)| 沿用 llms.txt 社区约定:`llms.txt` 读目录、`llms-full.txt` 一次读全。 **一个必须处理的坑**:mkdocs 只把 `.md` **渲染**成 HTML,不会把 Markdown 放进产物目录 —— 那样 `llms.txt` 里的链接会全部 404。所以 `build.sh` 增加了 第 4 步 `copy_agent_files`,在构建后把 23 个 Markdown 复制进 `site_build/`。 `docs/llms-full.txt` 已 gitignore:它是派生件,改任何一页都会整份重写, 进版本库只产生噪声(`llms.txt` 索引小且稳定,仍提交)。 验证:`mkdocs build --strict` 零告警;favicon/logo 可取(200,naturalWidth=400); 三页底部均含两个备案号;4 个 agent 入口均可访问且 content-type 正确。
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@ -40,3 +40,8 @@ tools/plugindev/plugindev
|
||||
site_build/
|
||||
# mkdocs 缓存
|
||||
.cache/
|
||||
|
||||
# agent 入口的「全文汇总」是派生件:由 gensite 把 docs/ 下所有 Markdown 拼成一份,
|
||||
# 每次改任何一页都会整份重写(115KB),进版本库只产生噪声。它由构建产出,
|
||||
# llms.txt(索引,小且稳定)仍提交。
|
||||
docs/llms-full.txt
|
||||
|
||||
27
docs/assets/logo-mark.svg
Normal file
27
docs/assets/logo-mark.svg
Normal file
@ -0,0 +1,27 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400" width="400" height="400">
|
||||
|
||||
<g transform="translate(200,200)">
|
||||
<circle cx="0" cy="0" r="105" fill="none" stroke="#E2E8F0" stroke-width="2.5" stroke-dasharray="8,6"/>
|
||||
<rect x="-130" y="-170" width="36" height="340" rx="8" fill="#3B82F6"/>
|
||||
<rect x="94" y="-170" width="36" height="140" rx="8" fill="#CBD5E1"/>
|
||||
<rect x="94" y="70" width="36" height="100" rx="8" fill="#CBD5E1"/>
|
||||
|
||||
<rect x="-94" y="-18" width="188" height="36" rx="8" fill="#38BDF8"/>
|
||||
|
||||
<polygon points="0,-28 24.2,14 -24.2,14" fill="#F59E0B" stroke="#F59E0B" stroke-width="8" stroke-linejoin="round"/>
|
||||
|
||||
<circle cx="74" cy="-74" r="14" fill="#DBEAFE" stroke="#3B82F6" stroke-width="2.5"/>
|
||||
<line x1="28" y1="-28" x2="63" y2="-63" stroke="#3B82F6" stroke-width="3" stroke-dasharray="6,4" opacity="0.6"/>
|
||||
|
||||
<circle cx="-74" cy="-74" r="14" fill="#EDE9FE" stroke="#8B5CF6" stroke-width="2.5"/>
|
||||
<line x1="-28" y1="-28" x2="-63" y2="-63" stroke="#8B5CF6" stroke-width="3" stroke-dasharray="6,4" opacity="0.6"/>
|
||||
|
||||
<circle cx="74" cy="74" r="14" fill="#CFFAFE" stroke="#06B6D4" stroke-width="2.5"/>
|
||||
<line x1="28" y1="28" x2="63" y2="63" stroke="#06B6D4" stroke-width="3" stroke-dasharray="6,4" opacity="0.6"/>
|
||||
|
||||
<circle cx="-74" cy="74" r="14" fill="#FEF3C7" stroke="#F59E0B" stroke-width="2.5"/>
|
||||
<line x1="-28" y1="28" x2="-63" y2="63" stroke="#F59E0B" stroke-width="3" stroke-dasharray="6,4" opacity="0.6"/>
|
||||
|
||||
<circle cx="0" cy="0" r="42" fill="none" stroke="#F59E0B" stroke-width="4"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
28
docs/assets/logo.svg
Normal file
28
docs/assets/logo.svg
Normal file
@ -0,0 +1,28 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400" width="400" height="400">
|
||||
<rect width="400" height="400" fill="#F8FAFC"/>
|
||||
|
||||
<g transform="translate(200,200)">
|
||||
<circle cx="0" cy="0" r="105" fill="none" stroke="#E2E8F0" stroke-width="2.5" stroke-dasharray="8,6"/>
|
||||
<rect x="-130" y="-170" width="36" height="340" rx="8" fill="#3B82F6"/>
|
||||
<rect x="94" y="-170" width="36" height="140" rx="8" fill="#CBD5E1"/>
|
||||
<rect x="94" y="70" width="36" height="100" rx="8" fill="#CBD5E1"/>
|
||||
|
||||
<rect x="-94" y="-18" width="188" height="36" rx="8" fill="#38BDF8"/>
|
||||
|
||||
<polygon points="0,-28 24.2,14 -24.2,14" fill="#F59E0B" stroke="#F59E0B" stroke-width="8" stroke-linejoin="round"/>
|
||||
|
||||
<circle cx="74" cy="-74" r="14" fill="#DBEAFE" stroke="#3B82F6" stroke-width="2.5"/>
|
||||
<line x1="28" y1="-28" x2="63" y2="-63" stroke="#3B82F6" stroke-width="3" stroke-dasharray="6,4" opacity="0.6"/>
|
||||
|
||||
<circle cx="-74" cy="-74" r="14" fill="#EDE9FE" stroke="#8B5CF6" stroke-width="2.5"/>
|
||||
<line x1="-28" y1="-28" x2="-63" y2="-63" stroke="#8B5CF6" stroke-width="3" stroke-dasharray="6,4" opacity="0.6"/>
|
||||
|
||||
<circle cx="74" cy="74" r="14" fill="#CFFAFE" stroke="#06B6D4" stroke-width="2.5"/>
|
||||
<line x1="28" y1="28" x2="63" y2="63" stroke="#06B6D4" stroke-width="3" stroke-dasharray="6,4" opacity="0.6"/>
|
||||
|
||||
<circle cx="-74" cy="74" r="14" fill="#FEF3C7" stroke="#F59E0B" stroke-width="2.5"/>
|
||||
<line x1="-28" y1="28" x2="-63" y2="63" stroke="#F59E0B" stroke-width="3" stroke-dasharray="6,4" opacity="0.6"/>
|
||||
|
||||
<circle cx="0" cy="0" r="42" fill="none" stroke="#F59E0B" stroke-width="4"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
33
docs/llms.txt
Normal file
33
docs/llms.txt
Normal file
@ -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 位恒为
|
||||
@ -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
|
||||
|
||||
59
overrides/partials/footer.html
Normal file
59
overrides/partials/footer.html
Normal file
@ -0,0 +1,59 @@
|
||||
{#-
|
||||
footer.html 覆盖:在版权行下补**备案号**。
|
||||
|
||||
为什么要覆盖主题文件:Material 的 copyright 只渲染 config.copyright(一个字符串),
|
||||
而备案号必须是**带链接的 HTML**且含两个条目(ICP + 公安),塞不进那个字段。
|
||||
|
||||
另外把「许可」写进 footer:本站内容与主站一致受 AGPL 约束,
|
||||
读者在任何页面底部都能看到,不必翻到首页。
|
||||
-#}
|
||||
<footer class="md-footer">
|
||||
{% if "navigation.footer" in features %}
|
||||
{% if page.previous_page or page.next_page %}
|
||||
{% if page.meta and page.meta.hide %}
|
||||
{% set hidden = "hidden" if "footer" in page.meta.hide %}
|
||||
{% endif %}
|
||||
<nav class="md-footer__inner md-grid" aria-label="{{ lang.t('footer') }}" {{ hidden }}>
|
||||
{% if page.previous_page %}
|
||||
{% set direction = lang.t("footer.previous") %}
|
||||
<a href="{{ page.previous_page.url | url }}" class="md-footer__link md-footer__link--prev" aria-label="{{ direction }}: {{ page.previous_page.title | e }}">
|
||||
<div class="md-footer__button md-icon">
|
||||
{% set icon = config.theme.icon.previous or "material/arrow-left" %}
|
||||
{% include ".icons/" ~ icon ~ ".svg" %}
|
||||
</div>
|
||||
<div class="md-footer__title">
|
||||
<span class="md-footer__direction">{{ direction }}</span>
|
||||
<div class="md-ellipsis">{{ page.previous_page.title }}</div>
|
||||
</div>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if page.next_page %}
|
||||
{% set direction = lang.t("footer.next") %}
|
||||
<a href="{{ page.next_page.url | url }}" class="md-footer__link md-footer__link--next" aria-label="{{ direction }}: {{ page.next_page.title | e }}">
|
||||
<div class="md-footer__title">
|
||||
<span class="md-footer__direction">{{ direction }}</span>
|
||||
<div class="md-ellipsis">{{ page.next_page.title }}</div>
|
||||
</div>
|
||||
<div class="md-footer__button md-icon">
|
||||
{% set icon = config.theme.icon.next or "material/arrow-right" %}
|
||||
{% include ".icons/" ~ icon ~ ".svg" %}
|
||||
</div>
|
||||
</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<div class="md-footer-meta md-typeset">
|
||||
<div class="md-footer-meta__inner md-grid">
|
||||
{% include "partials/copyright.html" %}
|
||||
<div class="md-copyright ha-beian">
|
||||
<a href="https://beian.miit.gov.cn" target="_blank" rel="noopener noreferrer">豫ICP备2024074105号-1</a>
|
||||
<span class="ha-beian-sep">·</span>
|
||||
<a href="https://beian.mps.gov.cn" target="_blank" rel="noopener noreferrer">豫公网安备41070202001579号</a>
|
||||
</div>
|
||||
{% if config.extra.social %}
|
||||
{% include "partials/social.html" %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
@ -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(全文)"
|
||||
|
||||
170
tools/apidoc/gensite/agent.go
Normal file
170
tools/apidoc/gensite/agent.go
Normal file
@ -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)拼成一份,可一次读完
|
||||
// /<page>.md 每个页面的 Markdown 原文(含生成的 API 页)
|
||||
// /<page>.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, "<!--") || strings.HasPrefix(t, "```") ||
|
||||
strings.HasPrefix(t, "!!!") || strings.HasPrefix(t, "|") {
|
||||
continue
|
||||
}
|
||||
// 去掉行内标记,截断到一句话。
|
||||
d := strings.NewReplacer("**", "", "`", "", "\\", "").Replace(t)
|
||||
if idx := strings.IndexAny(d, "。."); idx > 0 {
|
||||
d = d[:idx]
|
||||
}
|
||||
return title, d
|
||||
}
|
||||
}
|
||||
}
|
||||
return title, ""
|
||||
}
|
||||
|
||||
func copyFile(src, dst string) error {
|
||||
data, err := os.ReadFile(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(dst, data, 0o644)
|
||||
}
|
||||
@ -304,6 +304,14 @@ func main() {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// 给 agent 的入口:llms.txt / llms-full.txt(Markdown 正文汇总)。
|
||||
// 每个页面的 .md 副本由 build.sh 在 docs/ 定稿后统一复制进站点产物。
|
||||
pages, err := writeAgentEntrypoints(*outDir, "")
|
||||
if err != nil {
|
||||
log.Fatalf("生成 agent 入口失败: %v", err)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "agent 入口:llms.txt + llms-full.txt(覆盖 %d 个页面)\n", pages)
|
||||
|
||||
sort.Slice(pkg.Symbols, func(i, j int) bool { return pkg.Symbols[i].Name < pkg.Symbols[j].Name })
|
||||
fmt.Fprintf(os.Stderr, "生成 %d 个章节 + 检索索引 %d 条 → %s\n",
|
||||
len(sections), len(index), apiDir)
|
||||
|
||||
Reference in New Issue
Block a user