mirror of
https://gitcode.com/JianFeeeee/homeagent-sdk.git
synced 2026-10-03 15:44:11 +00:00
三件事,都是「站点上线后看出来的」问题。 ## ① 图标用的是 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 正确。
171 lines
5.7 KiB
Go
171 lines
5.7 KiB
Go
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)
|
||
}
|