graphify 增量内容接入指南:/graphify add 抓取 URL 与 --watch 目录监听自动重建图谱
graphify 默认构建流程只处理「磁盘上已有的语料」,而 add 与 watch 是两种非默认构建能力:前者让 Agent 把一条 URL(网页、论文、推文、视频、PDF、图片)拉取进 ./raw 语料并触发增量更新,后者让目录监听守护进程在文件变化时自动重建 graph.json 与 GRAPH_REPORT.md。本文以技能参考文档 add-watch.md 为主干,结合 ingest.py、watch.py、detect.py 等源码,完整讲解两种能力的命令用法、URL 分派规则、错误处理契约与底层实现,帮助你在 Agent 工作流中正确调用并理解其行为边界。
一、能力定位:不属于默认构建的可选路径
在 graphify 的技能体系中,参考文档 add-watch.md 仅在两种场景下被加载:
- 用户执行了
/graphify add <url>; - 用户为某个目录传入了
--watch参数。
也就是说,常规的「对已有文件夹做全量提取、建图、聚类」流程不会触达该文档;它解决的是语料增量供给问题:既要让外部 URL 成为可查询图谱的一部分,也要让运行中不断变化的文件夹(尤其是 Agent 多轮写入的目录)自动保持图谱新鲜。
从 skill-agents.md 可以看到,graphify 技能安装阶段会把当前解释器路径写入 graphify-out/.graphify_python(对应 sys.executable),随后所有 bash 片段都通过 $(cat graphify-out/.graphify_python) 取回正确解释器。add-watch 参考文档中的命令同样遵循这一约定——它保证脚本始终跑在安装 graphify 的同一个 Python 环境里,而不是 shell 默认的任意 python3。
二、/graphify add:抓取 URL 并入语料
2.1 命令模板与参数替换
参考文档给出的核心命令如下:
$(cat graphify-out/.graphify_python) -c "
import sys
from graphify.ingest import ingest
from pathlib import Path
try:
out = ingest('URL', Path('./raw'), author='AUTHOR', contributor='CONTRIBUTOR')
print(f'Saved to {out}')
except ValueError as e:
print(f'error: {e}', file=sys.stderr)
sys.exit(1)
except RuntimeError as e:
print(f'error: {e}', file=sys.stderr)
sys.exit(1)
"
执行前需要做三处替换:
| 占位符 | 含义 | 说明 |
|---|---|---|
URL |
要抓取的真实链接 | 交给 ingest() 自动判定类型(见下文) |
AUTHOR |
用户名 | 用户提供了姓名时填写,未提供时为可选 |
CONTRIBUTOR |
贡献者名 | 团队图谱场景下记录是谁把内容加进来的 |
成功后命令会打印 Saved to <路径>;若失败则打印 error: ... 到 stderr 并以退出码 1 结束。参考文档明确了失败契约:出错必须向用户说明原因,不能静默继续。
抓取成功并落盘后,Agent 需要自动在 ./raw 上运行 --update 增量管线,把新文件合并进已有图谱。这与 update.md 描述的增量流程衔接:detect_incremental 只挑出「新增/变更」文件做重提取(AST 层不消耗 LLM token),再经 build_merge 合并、save_manifest 落盘清单,保证下一次 update 的 diff 基线正确。换句话说,add 只是「把内容搬进语料」,真正把内容变成图谱节点/边的是随后的 --update。
2.2 支持 URL 类型与自动识别
参考文档列出六类 URL(自动检测),并与 ingest.py 中 _detect_url_type()(第 64–81 行)的判定顺序一一对应:
| URL 类型 | 识别规则 | 抓取与落盘方式 |
|---|---|---|
| Twitter/X | 域名含 twitter.com 或 x.com |
经 oEmbed 接口抓取,存为 .md,含推文正文与作者 |
| arXiv | 域名含 arxiv.org |
抓取摘要页并存为 .md(摘要 + 元数据) |
| YouTube | 域名含 youtube.com / youtu.be |
yt-dlp 下载纯音频,下次运行时转写为 .txt |
路径以 .pdf 结尾 |
二进制下载,保存为 .pdf |
|
| 图片 | 后缀为 .png/.jpg/.jpeg/.webp/.gif |
二进制下载,下次运行时由 Claude vision 提取 |
| 普通网页 | 其余所有情况 | html2text 风格转换存为 .md |
需要留意的实现细节:_detect_url_type 还会把含 github.com 的链接单独标记为 github 分支,但 ingest() 主流程并未为它走专用提取器,而是落到通用网页抓取。此外图片/PDF 这类二进制文件只是「下载进语料」,真正的语义理解发生在下一次 --update/提取运行时——文档明确说明了这一点,这也是为什么文档要求 add 成功后自动补跑 update 管线。
2.3 分类型抓取的实现依据
推文(tweet):_fetch_tweet()(ingest.py 第 103–133 行)先把 x.com 规范化为 twitter.com,再请求 https://publish.twitter.com/oembed?url=...&omit_script=true;从返回的 html 字段剥离标签取正文、用 author_name 记作者。oEmbed 失败时降级保存一条「无法获取内容」的占位标记,不会让 add 命令整体崩溃。落盘文件带 YAML frontmatter:source_url、type: tweet、author、captured_at、contributor。
网页(webpage):_fetch_webpage()(ingest.py 第 136–162 行)用正则抓 <title>,正文经 _html_to_markdown() 转换——先无条件剥离 <script>/<style>(避免脚本文本泄漏进图谱),再优先用 markdownify(heading_style="ATX"、bullets="-"、strip=["img"]);未安装 markdownify 时回退到基础标签剥离并把正文截断到 8000 字符。正文主体最终截断至 12000 字符。
arXiv:_fetch_arxiv()(ingest.py 第 165–207 行)用正则 (\d{4}\.\d{4,5}) 提取论文编号,命中则请求 https://export.arxiv.org/abs/<id>,再以选择器正则分别抽取标题、摘要、作者;文件名按 arxiv_<id 以_替换.>.md 生成,frontmatter 中记录 type: paper 与 paper_authors。若 URL 中识别不到编号则回退为普通网页抓取。
YouTube/视频:ingest() 走 download_audio()(transcribe.py 第 50–92 行)。文件名不取 yt-dlp 的 %(title)s(可能含怪异字符),而是以 URL 的 SHA-1 前 12 位生成稳定文件名 yt_<hash>.<ext>,并先探测 .m4a/.opus/.mp3/.ogg/.wav/.webm 缓存避免重复下载。yt-dlp 选项固定为 bestaudio[ext=m4a]/bestaudio/best、noplaylist、不做 ffmpeg 后处理。后续转写由 faster-whisper 完成,依赖需要在 pyproject.toml 的可选依赖中按 pip install 'graphifyy[video]' 安装(对应 video = ["faster-whisper; python_version >= '3.11'", "yt-dlp>=2026.6.9"])。
PDF 与图片:_download_binary()(ingest.py 第 210–215 行)经 safe_fetch 直接写字节,图片会补全缺失的扩展名。
2.4 健壮性设计:安全、命名、防覆盖
- 安全校验:
ingest()在任何网络请求前调用validate_url(url)(security.py 第 103 行),拦截私有 IP、非法 scheme 等恶意目标;抓取统一经safe_fetch/safe_fetch_text(默认超时 15 秒、字节上限受限)完成。 - YAML 注入防护:抓取回来的标题/作者是「外部不可信字符串」,直接拼进 frontmatter 可能导致 YAML 逃逸。
_yaml_str()(ingest.py 第 13–52 行)手工实现双引号标量的完整转义,覆盖\\、"、换行、TAB、NUL、U+2028/U+2029 以及全部控制字符,且刻意不依赖 PyYAML(它不在项目运行时依赖中)。 - 文件名安全:
_safe_filename()(ingest.py 第 55–61 行)取netloc + path,把非\w-字符折叠为_并截断到 80 字符。 - 防覆盖:落盘前若同名文件已存在,则循环追加
_1、_2… 计数器(上限 1000),保证重复 add 同一类内容不会互相覆盖。
顺带一提,ingest.py 还支持直接以模块方式运行:python -m graphify.ingest <url> [target_dir] [--author X] [--contributor Y],与技能内嵌命令行为一致,适合在无 Agent 环境下调试。
三、--watch:目录监听与自动更新
3.1 命令模板
$(cat graphify-out/.graphify_python) -m graphify.watch INPUT_PATH --debounce 3
INPUT_PATH 替换为要监听的目录。watch.py 的 __main__ 解析参数:path 默认 .,--debounce 为浮点数、默认 3.0 秒(watch.py 第 2240–2247 行)。进程启动后会打印监听路径与去抖窗口,按 Ctrl+C 优雅退出(watch() 捕获 KeyboardInterrupt 后调用 observer.stop()/join())。
3.2 触发后的两类行为
参考文档把监听行为按变更文件类型分成两条路径,源码 watch.py 第 2149–2159 行 的 docstring 与之完全对应:
- 纯代码变更(.py/.ts/.go 等):立即重跑 AST 提取 + 重建 + 聚类,全程不需要 LLM;
graph.json与GRAPH_REPORT.md自动更新。重建完成时会打印[graphify watch] Rebuilt: N nodes, M edges, C communities,并列出本轮更新的产物(graph.json、可选的 graph.html、GRAPH_REPORT.md、以及此前生成过的*-callflow.html)。 - 文档/论文/图片变更:不立即重建,而是在
graphify-out/needs_update写入标记并打印通知,提示运行/graphify --update——因为这类文件需要 LLM 语义重提取,watcher 本身不承担。
判定逻辑在 _batch_triggers_rebuild()(watch.py 第 2107–2118 行):批内存在代码文件或存在删除事件即触发重建——删除无需 LLM,纯文档删除也能被 reconcile 清扫掉,不会空等标记;_batch_needs_llm_flag() 则只对「仍然存活于磁盘」的非代码文件写 needs_update 标记,纯删除批不会留下脏标记。
3.3 去抖(Debounce)机制
参考文档强调去抖默认 3 秒:等待文件活动静止后再触发,从而避免 Agent 并行写入的一波文件按文件逐个重建。源码中的实现是事件处理器只记录 last_trigger = time.monotonic() 并把路径放入 changed 集合;主循环每 0.5 秒轮询一次,仅当 pending 为真且距最后一次事件超过 debounce 秒时才取出整个批处理(watch.py 第 2221–2232 行)。整波并行写入因此被合并为一次重建。
3.4 监听范围与过滤器
watcher 并非监听目录下一切变化,而是受三个层次约束(见 watch.py 第 2183–2208 行 的 Handler):
- 扩展名白名单:
_WATCHED_EXTENSIONS = CODE_EXTENSIONS | DOC_EXTENSIONS | PAPER_EXTENSIONS | IMAGE_EXTENSIONS,这些集合定义在 detect.py 第 44–47 行。其中代码扩展覆盖 .py/.ts/.js/.go/.rs/.java/.c/.cpp/.h/.swift/.kt/.cs/.rb/.php/.sql/.sh/.bash/.json/.tf/.xaml/.razor/.sln 等数十种;文档为.md/.mdx/.qmd/.skill/.txt/.rst/.html/.yaml/.yml;论文为.pdf;图片为.png/.jpg/.jpeg/.gif/.webp/.svg。 - 忽略规则:启动时一次性加载
.graphifyignore(并尊重持久化的 gitignore 设置),事件回调里不再反复解析——避免忙卷上每个 OS 事件都做一次完整解析而烧满 CPU。点开头文件(如.git/、隐藏目录)与graphify-out输出目录本身也被排除,防止 watcher 监听自己写入的产物造成死循环。 - 只读事件过滤:Linux 上 inotify(watchdog ≥ 2.3)会对每次 open/close 上报
opened、closed_no_write,若不加过滤,watcher 自身的重建读文件就会触发下一次重建,无限自激。_is_read_only_event()(watch.py 第 2121–2136 行)只把创建/修改/移动/删除/写后关闭视为变更;macOS 因使用 PollingObserver 不产生此类事件,过滤成为空操作。
3.5 重建的并发与资源约束
--watch 重建不是「裸跑」,而是经过一整套守护机制(见 watch.py 第 161–246 行):
- 仓库级重建锁:基于
fcntl.flock的_rebuild_lock()保证同一输出目录同时只有一个重建进程,锁文件.rebuild.lock内写入持有者 PID;进程被杀锁会自动释放,无需清理残留。Windows(无 fcntl)退化为空操作。 - 待处理变更排空:拿不到锁的进程不会丢变更集——
_queue_pending()把路径追加到.pending_changes(append 模式、逐行写入保证并发不互相覆盖),持锁进程在重建前后排空该文件并去重合并(_drain_pending/_merge_changed_paths,对应 issue #1059)。 - 资源限制:重建进程 best-effort
os.nice(10)降优先级,并可通过环境变量GRAPHIFY_REBUILD_MEMORY_LIMIT_MB设置内存上限(Linux 用RLIMIT_AS,macOS 用RLIMIT_DATA,因为 Apple libmalloc 下 AS 限制不可靠)。 - 构建配置持久化:
--exclude/gitignore等语料整形选项会写入输出目录旁的.graphify_build.json,让 update/watch/hook 触发的重建复用同一套排除规则,避免增量重建悄悄把被排除路径重新纳入(对应 #1886)。
3.6 对语义文件的延迟语义化处理
对新增/变更的视频文件,参考 transcribe.md:raw .mp4/.mp3 无法被语义子代理直接读取,update 管线会先经 transcribe_all() 转为 .txt 并当作 document 处理。watcher 不承担这一层——它只负责对代码事件即时重建、对非代码事件举手示意,语义化统一交给 /graphify --update。check_update()(watch.py 第 2077–2089 行)则是一个 cron 安全的探针函数:检测到 needs_update 标记就提示运行 update,且永远返回 True,不会让定时任务误报。
四、Agentic 工作流中的组合用法
参考文档最后给出 agentic 场景的实战指引,是理解 add + watch 设计动机的关键:
把
--watch跑在后台终端里。Agent 每波写入之间的代码变更会被自动拾取;若 Agent 同时也在写文档或笔记,则在那几波结束后需要手动/graphify --update。
由此形成一套完整的「自维护语料」闭环:
- 代码波:Agent 多进程并行改动
.py/.ts/...→ watcher 去抖合并为一次 AST 重建(无 LLM、零 token 成本),graph.json/GRAPH_REPORT.md即时保持可用。 - 文档波:Agent 新增笔记、接入外部资料时 → watcher 写入
needs_update标记并提示;人工或上层编排在波结束后执行一次/graphify --update,对这批文档/图片/视频做 LLM 语义提取。 - 外部 URL:想纳入不在磁盘上的资料时 →
/graphify add <url>把网页/推文/论文/PDF/图片拉进./raw(视频先下载音频),随后紧跟一次--update,让它进入图谱。
这样安排的本质原因是 graphify 的双层提取模型:AST 提取是确定性的、可离线即时完成的,而文档/图片/视频的语义提取依赖 LLM,应当批量、集中、按需触发。add 负责供给、watch 负责感知、--update 负责语义化,三者各司其职,正好拼出增量更新的完整链路。
五、实践要点小结
add与watch是非默认能力,技能在检测到/graphify add或--watch时才加载 add-watch.md,不要把它们当全量构建的替代品。- 命令中的解释器一律用
$(cat graphify-out/.graphify_python),确保与安装 graphify 的环境一致;/graphify add出错时向用户报告原因并以退出码 1 结束,不得静默吞掉失败。 - add 成功落盘后必须补跑一次
--update(参考 update.md 的增量流程),否则新文件不会进入图谱。 - 视频转写需先
pip install 'graphifyy[video]'(faster-whisper + yt-dlp),转写提示词经GRAPHIFY_WHISPER_PROMPT环境变量传入,模型名用GRAPHIFY_WHISPER_MODEL(默认base)。 --watch只对代码变更即时重建;文档/论文/图片变更只落graphify-out/needs_update标记。等待这类更新时检查该标记即可,不必反复轮询进程输出。- 去抖默认 3 秒、
--debounce可调;macOS 走 PollingObserver(防 FSEvents 漏事件),Linux 用原生 watchdog;两者都内置了只读事件、点开文件与graphify-out的自排除,避免自激循环。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00