用 Onyx(danswer)内置 google-drive 技能的 Docs 命令精修 Google 文档
【免费下载链接】danswerOpen Source AI Platform - AI Chat with advanced features that works with every LLM项目地址: https://gitcode.com/GitHub_Trending/da/danswer
在 Onyx(danswer)的技能体系中,Google Docs 命令指南 专门讲解如何通过gdrive_api.py脚本,借助 Google Docs API(https://docs.googleapis.com/v1/,注意与 Drive API 是不同的主机)对 Google Doc 进行基于字符索引的精细化编辑:插入/删除文本、重设段落样式、添加项目符号,以及从零创建新文档。读完本文,你将掌握get-doc、insert-text、append-text、batch-update、create-doc五个子命令的完整用法、其底层请求构造原理,以及如何用仓库中的单元测试验证这些行为。
为什么需要独立的 Docs 命令
Google Drive 的命令(search、list、read等)走的是https://www.googleapis.com/drive/v3/,它们擅长“按文件粒度”操作:查找文件、读取内容、上传下载。但Doc 内部是结构化的——一段文字、一个段落、一个列表项都对应一个可寻址的字符区间。gdrive_api.py read只能把整篇文档“导出成文本”,无法告诉你“第 10 个字符在哪一段、该往哪个索引插内容”。
Docs 命令解决的就是这个缺口:
- 先通过
get-doc拉取文档的结构骨架(body.content,每个元素带startIndex/endIndex); - 再用这些索引构造
insertText、deleteContentRange、updateParagraphStyle等请求,通过batchUpdate精确落笔。
在源码中,这一设计直接体现为两个不同的 API 基地址常量,见 gdrive_api.py:
_BASE = "https://www.googleapis.com/drive/v3/" _UPLOAD_BASE = "https://www.googleapis.com/upload/drive/v3/" # The Google Docs API lives on a different host than Drive; surgical edits to an # existing Doc (get structure / batchUpdate) go here, not through the Drive base. _DOCS_BASE = "https://docs.googleapis.com/v1/"单元测试 test_gdrive_api_helper.py 专门断言了这一点:test_docs_base_targets_docs_host验证 Docs 命令必须命中docs.googleapis.com/v1/documents/D1:batchUpdate而非 Drive 主机。
重要约定:所有 Docs 命令中的
<document_id>就是该 Doc 在 Drive 中的文件 ID,不是文档标题。
运行方式与前置知识
脚本的通用调用形式为(所有子命令一致):
python .opencode/skills/google-drive/gdrive_api.py <command> [args]在会话工作区中按上述路径运行;运行python gdrive_api.py <command> -h可查看具体命令的全部参数。
鉴权与写操作审批
gdrive_api.py本身不处理任何鉴权:连接用户的 bearer token 由沙箱出口代理(sandbox egress proxy)在请求时注入;而写操作(create/upload/edit/delete)可能在代理层暂停等待用户批准,这在脚本的模块 docstring 中有明确说明(见 gdrive_api.py)。因此在使用编辑类命令时,应预期可能出现一次用户确认环节。
文档 ID 参数:裸 ID 与完整 URL 均可
所有document_id类参数都经过_id_arg归一化(gdrive_api.py):接受裸 ID,也接受形如https://docs.google.com/document/d/<id>/edit、https://drive.google.com/drive/folders/<id>或?id=<id>分享链接,自动提取其中的 ID 段。这意味着 Agent 可以直接把用户粘贴的链接原样传给命令。参数化测试 test_id_arg_accepts_urls_and_bare_ids 覆盖了 7 种 URL 形态。
输出瘦身
所有命令默认会对返回 JSON 做空字段裁剪(_prune,递归丢弃None/""/[]/{},保留布尔值与 0),见 gdrive_api.py;传入--raw可跳过裁剪。
get-doc:先读结构,拿到编辑所需的字符索引
python gdrive_api.py get-doc <document_id> [--fields "documentId,body,title"]get-doc返回文档body.content结构,每个结构元素都带startIndex/endIndex——这是后续所有精确编辑的“坐标系统”。--fields用于裁剪返回字段,默认返回整篇文档。
多标签页文档(tabs)
Docs 支持多标签页(tab)结构,而body字段只承载第一个 tab 的内容。若需读取每个 tab 的内容,加--tabs参数,所有 tab 的内容会出现在tabs[]下:
python gdrive_api.py get-doc <document_id> --tabs其底层实现(gdrive_api.py)会:
- 设置查询参数
includeTabsContent=true(没有它,响应里的body只含第一个 tab); - 若同时指定了
--fields而字段掩码中不含tabs,会自动追加tabs,否则字段掩码会把请求到的 tab 内容过滤掉。
这一点由测试 test_get_doc_tabs_flag_requests_tab_content 与 test_get_doc_tabs_extends_a_caller_field_mask 双重验证:前者断言请求参数为{"includeTabsContent": "true"},后者验证字段掩码"documentId,title"会被扩展为"documentId,title,tabs"且不会重复追加。
insert-text:在指定字符索引处插入文本
python gdrive_api.py insert-text <document_id> --index N --text "..."这是最直接的写操作:向documents/<id>:batchUpdate发送一条insertText请求,把文本插到字符索引N处(N从get-doc获取)。其请求构造见 gdrive_api.py:
requests = [{"insertText": {"location": {"index": a.index}, "text": a.text}}] resp = _batch_update(a.document_id, requests)注意命令行的--index帮助文本将其描述为“字符索引”(1-based character index),实际语义以get-doc返回的startIndex/endIndex为准。测试 test_insert_text_builds_batch_update_request 验证了insert-text D1 --index 5 --text hello会生成{"requests": [{"insertText": {"location": {"index": 5}, "text": "hello"}}]}并 POST 到documents/D1:batchUpdate。
append-text:免手算索引的“追加到末尾”
python gdrive_api.py append-text <document_id> --text "..."如果不关心具体位置、只想把内容加到正文末尾,append-text会自动完成两件事:先get-doc取结构,计算正文结束索引,再在该处插入——完全不需要手动指定索引。
其实现(gdrive_api.py)有一个值得注意的边界处理:
doc = _get_doc(a.document_id, fields="body(content(endIndex))") end = _doc_end_index(doc) # Insert just before the final newline of the body to stay in range. index = max(1, end - 1)_doc_end_index(gdrive_api.py)取正文最后一个结构元素的endIndex作为文档末端,然后在end - 1处插入:因为 Docs 正文末尾固定存在一个换行符,越过它插入会越界报错,所以落在其紧前位置才是合法的插入点。空文档时索引退化为max(1, 1 - 1) = 1。
测试 test_append_text_computes_end_index_from_get_doc 精确验证了这一逻辑:当get-doc返回的最后一个元素endIndex为 42 时,最终在索引 41 插入,并且结果会附带index字段回显计算值;test_doc_end_index_defaults_to_one_for_empty_body 则确认空文档时索引为 1。
batch-update:直接下发原始 Docs 请求(万能逃生通道)
# 内联 JSON 数组 python gdrive_api.py batch-update <document_id> '[<request>, ...]' # 或从文件读取请求数组 python gdrive_api.py batch-update <document_id> --file requests.json当insert-text/append-text不够用时,batch-update让你直接发送任意Docs API 请求对象数组,脚本会原样包装为{"requests": [...]}发到documents/<id>:batchUpdate(gdrive_api.py)。它支持完整的 Docs 请求集合,例如:
insertText— 插入文本deleteContentRange— 删除某字符区间updateParagraphStyle— 重设段落样式createParagraphBullets— 添加项目符号updateTextStyle— 修改文字样式
官方示例:加粗一段文字并转成项目符号
python gdrive_api.py batch-update <document_id> '[ {"updateTextStyle": {"range": {"startIndex": 1, "endIndex": 10}, "textStyle": {"bold": true}, "fields": "bold"}}, {"createParagraphBullets": {"range": {"startIndex": 1, "endIndex": 10}, "bulletPreset": "BULLET_DISC_CIRCLE_SQUARE"}} ]'两条请求作用于同一个字符区间[1, 10):第一条把区间内文字设为粗体(fields指定只更新bold属性),第二条把该区间对应的段落套用BULLET_DISC_CIRCLE_SQUARE预设的项目符号样式。
请求校验与错误返回
batch-update在下发前会做两项防御性校验(gdrive_api.py):请求体缺失时返回{"ok": false, "error": "no_requests"};JSON 解析失败或不是数组时分别返回invalid requests json: ...与requests_not_array,且不会触发网络调用。测试 test_batch_update_rejects_non_array 用'{"a": 1}'验证了这一点;test_batch_update_passes_through_requests_array 与 test_batch_update_reads_requests_from_file 则分别覆盖了内联 JSON 与--file两种输入路径。
create-doc:创建空白新文档
python gdrive_api.py create-doc --title "My Doc"通过 Docs API 的POST documents创建一个空白 Doc 并返回其documentId(实现见 gdrive_api.py,请求体仅含{"title": ...},测试 test_create_doc_posts_title 验证了这一点)。拿到新 ID 后,再组合insert-text/append-text/batch-update填入内容即可完成“从零造文档”的完整流程。
两种建文档方式的取舍
| 场景 | 推荐方式 |
|---|---|
| 已有 Markdown / HTML 内容,想直接得到渲染好的 Doc | upload --convert-to application/vnd.google-apps.document(Drive 命令,详见 drive.md) |
| 内容需要程序化、按索引精雕细琢地写入 | create-doc+insert-text/batch-update |
upload --convert-to走 Drive 的上传转换路径(_UPLOAD_BASE,multipart/related 上传,见 gdrive_api.py),适合“批量导入既有文档”;而 Docs 命令适合“在会话中逐步构造内容”。注意_CONTENT_TYPES映射(gdrive_api.py)中.md显式映射为text/markdown——因为标准库mimetypes依赖平台且常常漏掉.md,这正是 Drive 把 Markdown 转成 Google Doc 所必需的。
输出格式约定
两类返回结构(详见原文档):
get-doc/create-doc返回:{"ok": true, "document": {...}}insert-text/append-text/batch-update返回:{"ok": true, "data": {...}};其中append-text额外回显计算出的插入位置:{"ok": true, "index": N, "data": {...}}
所有输出均以 JSON 打印到 stdout,通过{"ok": ...}表达成败;错误信息打印到 stderr 并以非零码退出(gdrive_api.py)。main中的异常处理对不同失败类型做了区分:
- HTTP 错误会打印
HTTP <code> calling <Google Docs | Google Drive>: <detail>,并按请求目标自动区分 API 归属; - 对 Drive 的 404 会附加一条提示:Drive 对“不存在的 ID”和“当前授权不可见的文件”都返回 404,此时该文件可能仍可通过其专属 API(如 Docs 的
get-doc、gsheets_api.py、gslides_api.py)访问; - 网络错误、非 JSON 响应、文件缺失也各有明确的 stderr 信息与退出码。
在技能体系中的位置
google-drive是 Onyx 的内置外部应用技能(ExternalAppBuiltInProvider,见 built_in.py),其SKILL.md.template的快速参考表把任务分派给四个指南文件(SKILL.md.template):
| 任务 | 指南 |
|---|---|
| 查找、读取、上传、整理、删除任意 Drive 文件 | drive.md —gdrive_api.py |
| 精细化编辑或创建 Google Doc | 本文(docs.md)—gdrive_api.pyDocs 命令 |
| 读写表格单元格、新建 Sheet | sheets.md —gsheets_api.py |
| 读取或编辑 Slides 演示文稿 | slides.md —gslides_api.py |
如果只是想快速把某个 Google 原生文件读成文本,gdrive_api.py read <file_id>就够(Docs→Markdown、Sheets→CSV、Slides→纯文本,见 drive.md);只有当需要结构、ID、索引或编辑时才需要打开各专属 API 指南——这正是本文所讲 Docs 命令的用武之地。
小结:一套“先坐标、后落笔”的编辑范式
Docs 命令的核心理念可以概括为三步:get-doc取索引坐标 → 构造 Docs 请求对象 →batch-update批量落笔。insert-text与append-text是这条路径的两个便捷封装(前者要你给索引,后者自动算末尾),batch-update则是完整的通用入口,支持样式、列表、删除等全部请求类型;create-doc则为“从空白开始构造”提供了起点。仓库中的单元测试(test_gdrive_api_helper.py)对这些命令的请求路径、请求体、索引计算与参数归一化做了完整覆盖,可作为实现细节的权威参考。
【免费下载链接】danswerOpen Source AI Platform - AI Chat with advanced features that works with every LLM项目地址: https://gitcode.com/GitHub_Trending/da/danswer
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考