Glance 扩展机制详解:用一个 HTTP 请求和几个特殊响应头打造自己的第三方 Widget
Glance 的扩展(Extension)功能允许你把自己部署的任何 HTTP 服务接入仪表盘,只需在响应中携带几个约定的 Widget-* 头,Glance 就会把返回内容渲染成一个标准 widget。本文基于 docs/extensions.md 和 docs/configuration.md 中扩展相关章节,结合 internal/glance/widget-extension.go 等源码,完整讲解扩展的请求模型、全部配置项、四个响应头的精确语义,以及如何复用 Glance 内置的 CSS 类名与前端脚本,让你的扩展 HTML 获得与内置 widget 一致的外观和交互。
一、扩展的本质:一次普通的 HTTP 请求
Glance 设计扩展功能的目标是"让开发者以最低的知识门槛参与开发"(with the intention of requiring minimal knowledge in order to develop extensions)。它没有定义任何复杂的私有协议,扩展就是:Glance 向一个 URL 发起一次 HTTP GET 请求,服务器返回一段内容加几个特殊响应头。整个交互过程如下图所示:
因此,只要你懂得搭建一个 HTTP 服务器、会写一点 HTML 和 CSS,就可以开始开发自己的扩展。
需要特别注意,原文档在开头明确声明:扩展功能以及该文档本身都是"进行中"(work in progress)状态,API 未来可能变化,你自己负责维护自己的扩展。这意味着本文描述的所有头名、配置项和类名均以当前仓库版本为准,升级 Glance 后建议对照仓库文档重新核对。
[!TIP] 默认情况下扩展 widget 的缓存时间为 30 分钟。为了避免每次修改扩展后都要重启 Glance 才能看到效果,可以在配置中把该 widget 的缓存时间设为 1 秒:
- type: extension url: http://localhost:8081 cache: 1s
二、widget 配置:type: extension 的完整参数
在配置文件中添加一个扩展 widget 的最小写法如下(引自 docs/configuration.md 的 Extension 章节):
- type: extension
url: https://domain.com/widget/display-a-message
allow-potentially-dangerous-html: true
parameters:
message: Hello, world!
完整属性表如下:
| 属性 | 类型 | 必填 | 默认值 |
|---|---|---|---|
| url | string | 是 | |
| fallback-content-type | string | 否 | |
| allow-potentially-dangerous-html | boolean | 否 | false |
| headers | key & value | 否 | |
| parameters | key & value | 否 | |
| cache | string(时长) | 否 | 30m(扩展默认值) |
url
扩展服务的地址。注意:URL 中自带的 query 会被剥离,实际生效的 query 由 parameters 定义(原文档加粗强调)。从源码 internal/glance/widget-extension.go#L119-L123 可以看到,一旦 parameters 非空,就会执行 request.URL.RawQuery = options.Parameters.toQueryString() 直接替换掉原有 query。
源码中 url 也是扩展 widget 唯一必填的字段,且在 initialize() 阶段 通过 url.Parse 校验合法性:URL 为空会报 URL is required,格式非法会报 parsing URL: ...。
fallback-content-type
当扩展服务没有返回合法的 Widget-Content-Type 头时使用的回退内容类型。目前该属性唯一支持的取值是 html。源码中的回退链路与之一致:先查响应头 Widget-Content-Type,查不到再查 fallback-content-type,仍查不到则按未知类型处理(见下文第四节的 convertExtensionContent 逻辑)。
headers
可选,指定随请求一起发送的自定义请求头,典型用法是携带 API 密钥:
headers:
x-api-key: ${SECRET_KEY}
源码 fetchExtension 中对每个 key-value 依次调用 request.Header.Add(key, value) 注入请求。
allow-potentially-dangerous-html
是否允许扩展渲染 HTML。原文档给出了一条很直白的警告:
这个属性名字之所以听起来吓人是有原因的。它面向的是有能力开发并使用自己扩展的开发者。如果你不清楚它意味着什么,或者没有绝对把握你使用的扩展 URL 是安全的,就不要启用它。
从源码看它的确是一个安全开关:当扩展返回 html 内容类型但该属性为 false 时,内容会被整体 HTML 转义后放进 <pre> 标签以纯文本展示(见 convertExtensionContent)。因此该属性本质上是"是否信任该扩展提供的 HTML"的声明。
parameters
以键值对形式发送给扩展的 query 参数。从 queryParametersField 的 YAML 解析实现 看,值支持多种写法:
parameters:
param1: value1 # 字符串
count: 10 # 数字,会被转成字符串
enabled: true # 布尔值
param2:
- item1 # 列表:同一 key 对应多个值
- item2
解析后内部统一为 map[string][]string,再由 toQueryString() 用标准库 url.Values.Encode() 编码为 query string。列表写法对应同 key 多值的 query 参数。
cache 与时长格式
cache 是 widgetBase 的公共字段,扩展 widget 在 initialize() 中通过 withCacheDuration(time.Minute * 30) 设定 30 分钟默认值;若你显式配置了 cache,则用户配置优先(见 withCacheDuration 中 w.CustomCacheDuration 的判断分支)。
Glance 的时长字段使用自定义格式(durationField):数字 + 单位,单位为 s(秒)、m(分)、h(时)、d(天)、w(周)、mo(月,按 30 天)、y(年,按 365 天)。所以 cache: 1s 表示 1 秒。
三、四个特殊响应头:扩展的"API"
扩展返回的 HTTP 响应中,Glance 读取以下四个头来描述 widget 的元信息(头名常量定义于 widget-extension.go#L84-L89):
| 响应头 | 作用 | 不返回时的行为 |
|---|---|---|
Widget-Title |
widget 标题 | 标题为 "Extension" |
Widget-Title-URL |
点击 widget 标题时打开的 URL | 标题不可点击 |
Widget-Content-Type |
内容类型 | 内容按纯文本展示 |
Widget-Content-Frameless |
设为 true 时,内容去掉默认背景和边框(frame) |
内容带默认"边框"样式 |
Widget-Title
指定 widget 标题。若不提供,标题回退为 "Extension"。源码中有两处兜底:请求侧在 fetchExtension 中检查该头为空时直接赋 "Extension";widget 侧在 update() 中仅当用户没有在配置里自定义 title 时才采用扩展传来的标题——也就是说配置中的 title 优先于 Widget-Title 头。
Widget-Title-URL
指定点击 widget 标题时打开的链接。原文档明确:如果用户在配置里指定了 title-url,它以优先。源码验证了这一点——update() 中的条件是 if widget.TitleURL == "",只有配置未设置时才回填头的值。
有 TitleURL 时,widget-base.html 模板 会把标题渲染为 <h2><a href="..." target="_blank">...</a></h2>,即新标签页打开。
Widget-Content-Type
指定扩展返回的内容类型。当前唯一受支持的取值是 html(见 extensionStringToType 映射表)。未提供或取值不受支持时,内容按纯文本展示。
Widget-Content-Frameless
当该头的值是 true 时,widget 内容区域会去掉默认背景与边框,用于展示自带完整样式的扩展内容。源码解析细节有两点值得注意:
- 头值通过 stringToBool 解析,只有
"true"和"yes"(小写)会被识别为真,1、True、YES都不行; - 解析结果最终落到 extension.html 模板:
Frameless为真时,widget-content容器额外获得widget-content-framelessCSS 类。
四、内容类型:目前只有 html,以及它的回退链
原文档的 NOTE 说明了现状与长期目标:
目前
html是唯一支持的内容类型。长期目标是支持videos、forum-posts、markets、streams这类通用内容类型——扩展只需返回 JSON 数据,由 Glance 使用内置样式和功能渲染,开发者专注于从自己的数据源取数,即可获得原生外观。
内容处理的判定链路
结合 fetchExtension 与 convertExtensionContent,一次响应内容的处理顺序是:
- 读
Widget-Content-Type头,若映射表中不存在(当前映射表只有html),则回退到配置项fallback-content-type; - 仍不存在则为
extensionContentUnknown; - 进入
convertExtensionContent做最终转换:- 类型为
html且allow-potentially-dangerous-html: true→ 原样输出为template.HTML(即不转义地注入页面); - 类型为
html但未允许 HTML →fallthrough到默认分支,内容经html.EscapeString转义后包进<pre>标签,以纯文本形式展示; - 未知类型 → 同样走转义 +
<pre>分支。
- 类型为
这条链路解释了文档中"如果不设置 allow-potentially-dangerous-html,HTML 内容会被当作纯文本显示"的原因:不是丢弃内容,而是转义后原样展示源码文本。
渲染位置
扩展内容最终由 templates/extension.html 注入标准 widget 骨架:它复用 widget-base.html 模板的 widget-header(标题、WIP 图标、错误/通知图标)结构,并把 .Extension.Content 放在 widget-content 容器中。因此扩展自动获得了与内置 widget 一致的标题样式、错误提示(拉取失败时显示 ERROR 区块及错误信息)等基础能力。
五、复用 Glance 现有样式:一份可直接运行的完整 HTML 示例
Glance 内置 widget 的大部分视觉效果来自一组工具类 CSS 和少量前端脚本,这些在扩展的 HTML 中可以直接复用。原文档给出了一份覆盖面很广的示例,这里完整保留:
<p class="color-subdue">Text with subdued color</p>
<p>Text with base color</p>
<p class="color-highlight">Text with highlighted color</p>
<p class="color-primary">Text with primary color</p>
<p class="color-positive">Text with positive color</p>
<p class="color-negative">Text with negative color</p>
<hr class="margin-block-15">
<p class="size-h1">Font size 1</p>
<p class="size-h2">Font size 2</p>
<p class="size-h3">Font size 3</p>
<p class="size-h4">Font size 4</p>
<p class="size-base">Font size base</p>
<p class="size-h5">Font size 5</p>
<p class="size-h6">Font size 6</p>
<hr class="margin-block-15">
<a class="visited-indicator" href="#notvisitedprobably">Link with visited indicator</a>
<hr class="margin-block-15">
<a class="color-primary-if-not-visited" href="#notvisitedprobably">Link with primary color if not visited</a>
<hr class="margin-block-15">
<p>Event happened <span data-dynamic-relative-time="<unix timestamp>"></span> ago</p>
<hr class="margin-block-15">
<ul class="list-horizontal-text">
<li>horizontal</li>
<li>list</li>
<li>with</li>
<li>multiple</li>
<li>text</li>
<li>items</li>
</ul>
<hr class="margin-block-15">
<ul class="list list-gap-10 list-with-separator">
<li>list</li>
<li>with</li>
<li>gap</li>
<li>and</li>
<li>horizontal</li>
<li>lines</li>
</ul>
<hr class="margin-block-15">
<ul class="list collapsible-container" data-collapse-after="3">
<li>collapsible</li>
<li>list</li>
<li>with</li>
<li>many</li>
<li>items</li>
<li>that</li>
<li>will</li>
<li>appear</li>
<li>when</li>
<li>you</li>
<li>click</li>
<li>the</li>
<li>button</li>
<li>below</li>
</ul>
<hr class="margin-bottom-15">
<p class="margin-bottom-10">Lazily loaded image:</p>
<img src="https://picsum.photos/200" alt="" loading="lazy">
<hr class="margin-block-15">
<p class="margin-bottom-10">List of posts:</p>
<ul class="list list-gap-14 collapsible-container" data-collapse-after="5">
<li>
<a class="size-h3 color-primary-if-not-visited" href="#link">Lorem ipsum dolor, sit amet consectetur adipisicing elit. Voluptatum, ipsa?</a>
<ul class="list-horizontal-text">
<li data-dynamic-relative-time="<unix timestamp>"></li>
<li>3,321 points</li>
<li>139 comments</li>
</ul>
</li>
<li>
<a class="size-h3 color-primary-if-not-visited" href="#link">Lorem ipsum dolor, sit amet consectetur adipisicing elit. Voluptatum, ipsa?</a>
<ul class="list-horizontal-text">
<li data-dynamic-relative-time="<unix timestamp>"></li>
<li>3,321 points</li>
<li>139 comments</li>
</ul>
</li>
<li>
<a class="size-h3 color-primary-if-not-visited" href="#link">Lorem ipsum dolor, sit amet consectetur adipisicing elit. Voluptatum, ipsa?</a>
<ul class="list-horizontal-text">
<li data-dynamic-relative-time="<unix timestamp>"></li>
<li>3,321 points</li>
<li>139 comments</li>
</ul>
</li>
</ul>
渲染效果如下(把 <unix timestamp> 替换为真实 Unix 时间戳后):
示例中各功能的源码出处
这些能力并非扩展专属,全部来自 Glance 的全局静态资源,因此在扩展 HTML 中天然可用:
- 颜色与字体类(
color-subdue、color-highlight、color-primary、color-positive、color-negative、size-h1~size-h6、size-base、margin-block-15、margin-bottom-10等):定义在 static/css/utils.css,属于纯 CSS 工具类; visited-indicator/color-primary-if-not-visited:已访问链接的样式标记。Glance 会在本地记录已打开的链接并据此切换样式,扩展中的链接同样参与该机制;data-dynamic-relative-time="<unix timestamp>":由 static/js/page.js#L210 附近 的document.querySelectorAll("[data-dynamic-relative-time]")统一处理,将时间戳渲染为"相对时间"(如 3 小时前)并定期刷新,适用于"事件发生于何时"这类元信息;collapsible-container+data-collapse-after="N":由 page.js 的 attachExpandToggleButton 处理,脚本会在列表后自动插入"展开/收起"按钮,点击后切换container-expanded类;list-horizontal-text、list、list-gap-10、list-with-separator等列表类:均为 utils.css 中的通用列表样式,示例末尾的"帖子列表"组合(标题链接 + 水平元信息行 + 可折叠列表)正是 RSS、论坛类 widget 的常见排版模式,值得在自己的扩展中照搬。
六、缓存与失败重试:扩展拉不到数据时会发生什么
除了 cache 时长,理解扩展的更新与重试行为有助于调试:
- 默认 30 分钟:initialize() 中
withCacheDuration(time.Minute * 30),nextUpdate到点后下一次页面刷新触发重新拉取; - 拉取失败会触发提前重试:update() 将错误交给
canContinueUpdateAfterHandlingErr处理(widget.go#L297-L329)——出现错误时调用scheduleEarlyUpdate(),按重试次数的平方分钟数递增提前重试,且封顶 5 次(widget.go#L354-L370);期间 widget 会展示错误区块而不是旧内容; - 标题/链接的合并时机:
Widget-Title、Widget-Title-URL只在新一次拉取成功返回头之后才回填到 widget(且让位于用户配置),所以扩展服务的标题是动态跟随服务响应的。
调试时把 cache: 1s 设为最小值、同时保证扩展服务对 HEAD/GET 都能快速返回,是原文档 TIP 之外的实用组合。
七、适用边界与维护责任
把原文档的告诫汇总成检查清单,开发扩展前请确认:
- API 未稳定:扩展功能的头名、取值和默认值可能随版本变化,升级 Glance 后需重新核对 docs/extensions.md;
- 样式类名不承诺稳定:第五节复用的
color-*、size-*、list-*等类名以及data-dynamic-relative-time、collapsible-container等脚本约定"可能会被修改",原文档明确要求你自己负责维护自己的扩展; - 安全自担:启用
allow-potentially-dangerous-html后,扩展返回的 HTML 会不经过转义注入页面。扩展服务泄露或被篡改,等同于向所有 Glance 用户浏览器投递任意 HTML/脚本,务必只信任自己的扩展源; - 内容类型有限:当前仅
html一种内容类型,无法像内置 widget 那样复用 Glance 的 JSON 数据渲染管线;通用内容类型(videos、forum-posts等)是文档中声明的长期目标,当前版本尚不可用。
如果你需要一个"非扩展"的替代方案,仓库中的 custom-api widget(docs/custom-api.md) 支持用 Go template 直接对接口响应做服务端渲染,两者可以按"是否需要自己部署服务"来取舍。
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 StartedRust0623
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

