深入解析 Glance custom-api 组件模板:把任意 JSON API 变成仪表盘组件
Glance 的 custom-api 组件允许你用一段 Go 模板把任意 HTTP API 的响应直接渲染成仪表盘上的一个组件,本文以仓库中的模板参考文档 docs/custom-api.md 为主线,系统讲解模板上下文、数据访问语法、条件与排序、时间处理、多请求编排等全部官方示例,并结合 组件实现源码 说明每个语法背后的真实行为,读完即可独立编写、调试和复用任意结构的 custom-api 组件。
custom-api 组件是什么
custom-api 是 Glance 中最“万能”的组件:它向一个 URL 发起 HTTP 请求,拿到响应后用你提供的模板生成 HTML。官方配置文档明确指出,编写这类组件需要基础的 HTML、CSS、Go 模板语言知识(见 docs/configuration.md 的 Custom API 一节)。
从源码 internal/glance/widget-custom-api.go 的 initialize() 可以看到几条硬性约束与默认值:
template字段是必填项,为空会直接返回template is required错误;- 组件初始化时会调用
withCacheDuration(1 * time.Hour),即缓存默认 1 小时(可被通用属性cache覆盖); - 模板通过 Go 标准库
html/template解析,并注入customAPITemplateFuncs函数表(源码)。
请求体与请求头的构造在 initialize() 方法 中完成:
- 提供了
body但未指定method时,默认使用POST;没有body时默认GET; - 提供
body时body-type默认为json,仅允许json或string两种取值; body-type: json时会自动追加Content-Type: application/json请求头;parameters会被序列化为查询串写入RawQuery(这会覆盖 URL 中已有的查询参数);basic-auth的username/password会通过SetBasicAuth生效。
模板上下文:.JSON、.Response 与 .Options
模板执行时传入的顶层数据是 customAPITemplateData(源码),它内嵌 customAPIResponseData,因此模板中可用的顶层字段包括:
| 字段 | 含义 |
|---|---|
.JSON |
响应体经 gjson 解析后的结果,提供 String、Int、Array 等方法 |
.Response |
原始 *http.Response,可读取 StatusCode、Status、Header 等 |
.Subrequest "key" |
获取命名子请求的响应数据 |
.JSONLines |
按行拆分响应体的便捷方法(JSON Lines 场景) |
.Options |
组件配置中 options 映射的模板侧访问器 |
其中 .JSON 的底层是 decoratedGJSONResult(源码),它内嵌了 tidwall/gjson 的 Result(依赖见 go.mod),因此所有 gjson 的键路径语法(如 a.b.c、a.0.b、# 过滤、.. 递归)在这里都有效。
数据访问基础语法
读取标量字段
假设 API 返回:
{
"title": "My Title",
"content": "My Content",
}
用 String 方法即可取出字段:
<div>{{ .JSON.String "title" }}</div>
<div>{{ .JSON.String "content" }}</div>
输出:
<div>My Title</div>
<div>My Content</div>
Int、Float、Bool 方法同理,用于读取整数、浮点数和布尔值。
遍历数组
如果响应中包含数组:
{
"author": "John Doe",
"posts": [
{
"title": "My Title",
"content": "My Content"
},
{
"title": "My Title 2",
"content": "My Content 2"
}
]
}
使用 range 遍历 posts:
{{ range .JSON.Array "posts" }}
<div>{{ .String "title" }}</div>
<div>{{ .String "content" }}</div>
{{ end }}
输出:
<div>My Title</div>
<div>My Content</div>
<div>My Title 2</div>
<div>My Content 2</div>
注意循环体内访问字段时不再带 .JSON 前缀——因为 range 会把模板上下文切换为当前数组元素。
在循环中访问根上下文
Go 模板中 $ 始终指代顶层数据。因此可以在循环内通过 $.JSON 回读顶层字段:
{{ range .JSON.Array "posts" }}
<div>{{ .String "title" }}</div>
<div>{{ .String "content" }}</div>
<div>{{ $.JSON.String "author" }}</div>
{{ end }}
输出中每个元素后都会追加一行 John Doe:
<div>My Title</div>
<div>My Content</div>
<div>John Doe</div>
<div>My Title 2</div>
<div>My Content 2</div>
<div>John Doe</div>
基本类型数组:空字符串键
当数组元素本身是字符串这类基本类型时,指定其“类型”的方式是传空字符串作为键:
[
"Apple",
"Banana",
"Cherry",
"Watermelon"
]
{{ range .JSON.Array "" }}
<div>{{ .String "" }}</div>
{{ end }}
输出:
<div>Apple</div>
<div>Banana</div>
<div>Cherry</div>
<div>Watermelon</div>
从源码看,Array("") 直接对当前结果取数组(源码),String("") 则返回结果本身的字符串表示,这就是空键语法的实现依据。
按索引访问
对基本类型数组,也可以用索引字符串直接取元素:
<div>{{ .JSON.String "0" }}</div>
输出:
<div>Apple</div>
点号路径访问深层对象
嵌套对象支持 gjson 风格的点号路径:
{
"user": {
"address": {
"city": "New York",
"state": "NY"
}
}
}
<div>{{ .JSON.String "user.address.city" }}</div>
<div>{{ .JSON.String "user.address.state" }}</div>
输出:
<div>New York</div>
<div>NY</div>
路径中的任意位置都可以混入数组索引,例如:
{
"users": [
{ "name": "John Doe" },
{ "name": "Jane Doe" }
]
}
<div>{{ .JSON.String "users.0.name" }}</div>
<div>{{ .JSON.String "users.1.name" }}</div>
输出:
<div>John Doe</div>
<div>Jane Doe</div>
遍历对象的键值对
用 Entries 方法可以枚举对象内的每个属性:
{
"user": {
"id": 42,
"name": "Alice",
"active": true
}
}
{{ range $key, $value := .JSON.Entries "user" }}
<div>{{ $key }}: {{ $value.String "" }}</div>
{{ end }}
输出:
<div>id: 42</div>
<div>name: Alice</div>
<div>active: true</div>
每个属性以键值对形式暴露:$key 是字符串,$value 是支持全套 JSON 方法的对象。实现上 Entries 返回一个 Go 迭代器(iter.Seq2,源码),底层按 ForEach 顺序逐条 yield。
条件判断:字段是否存在
用 Exists 检查路径是否存在,再决定读取还是展示兜底文案:
{
"user": {
"name": "John Doe",
"age": 30
}
}
{{ if .JSON.Exists "user.age" }}
<div>{{ .JSON.Int "user.age" }}</div>
{{ else }}
<div>Age not provided</div>
{{ end }}
输出:
<div>30</div>
算术运算
模板内置了 add、sub、mul、div、mod 五个运算函数。两个整数运算返回整数,否则返回浮点数;除以零返回 0;对非数值返回 NaN:
{
"price": 100,
"discount": 10
}
<div>{{ sub (.JSON.Int "price") (.JSON.Int "discount") }}</div>
输出:
<div>90</div>
从源码 customAPIDoMathOp 与 doMathOpWithAny 可以看到:div 与 mod 在除数为 0 时显式返回 0 以避免除零异常,而 NaN 的返回则来自类型断言失败(操作数不是数值类型)的分支。
时间处理
解析时间并展示相对时间
{
"posts": [
{
"title": "Exploring the Depths of Quantum Computing",
"date": "2023-10-27T10:00:00Z"
},
{
"title": "A Beginner's Guide to Sustainable Living",
"date": "2023-11-15T14:30:00+01:00"
},
{
"title": "The Art of Baking Sourdough Bread",
"date": "2023-12-03T08:45:22-08:00"
}
]
}
解析日期并展示动态相对时间:
{{ range .JSON.Array "posts" }}
<div>{{ .String "title" }}</div>
<div {{ .String "date" | parseTime "rfc3339" | toRelativeTime }}></div>
{{ end }}
parseTime 接受两个参数:日期布局(layout)和日期字符串本身。布局可使用 "unix"、"RFC3339"、"RFC3339Nano"、"DateTime"、"DateOnly" 等预定义别名(匹配时不区分大小写,见 源码),也可以直接传入 Go 的自定义时间格式串。
输出:
<div>Exploring the Depths of Quantum Computing</div>
<div data-dynamic-relative-time="1698400800"></div>
<div>A Beginner's Guide to Sustainable Living</div>
<div data-dynamic-relative-time="1700055000"></div>
<div>The Art of Baking Sourdough Bread</div>
<div data-dynamic-relative-time="1701621922"></div>
toRelativeTime 的返回值必须作为 HTML 标签的属性使用(div、li、span 等均可),不要直接输出为文本。其内部实现是生成一个 data-dynamic-relative-time="<unix 时间戳>" 属性(dynamicRelativeTimeAttrs),随后由 Glance 前端 JavaScript 在客户端持续刷新显示正确的相对时间(如 2h、1d),无需关心内部机制。
parseRelativeTime 是上面三步的缩写形式,等价于 {{ .String "date" | parseTime "rfc3339" | toRelativeTime }},例如 {{ .String "date" | parseRelativeTime "rfc3339" }}。
此外还有几个与时间相关的辅助函数:
parseLocalTime(layout, s):与parseTime相同,但会转换到服务器时区;无时区信息时使用本地时区而非 UTC;formatTime(layout, t):把time.Time格式化为字符串,布局规则与parseTime一致。注意其参数顺序是刻意翻转的,方便管道写法:{{ now | formatTime "rfc3339" }}(源码注释);now():返回当前时间;offsetNow(offset):返回偏移后的当前时间,偏移量为 Go duration 格式,如"3h"、"-1h"、"2h30m10s";duration(str):把"1h"、"5h30m"之类的字符串解析为time.Duration;startOfDay(t)/endOfDay(t):取某时间所在天的 0 点与 23:59:59。
响应状态码与请求头
模板可以直接判断 HTTP 响应状态,也可以读取响应头:
{{ if eq .Response.StatusCode 200 }}
<p>Success!</p>
{{ else }}
<p>Failed to fetch data</p>
{{ end }}
<div>{{ .Response.Header.Get "Content-Type" }}</div>
处理 JSON Lines(NDJSON)响应
有些 API 每行返回一个独立的 JSON 对象(NDJSON / JSON Lines 格式):
{"name": "Steve", "age": 30}
{"name": "Alex", "age": 25}
{"name": "John", "age": 35}
由于默认情况下 Glance 期望响应体是单个合法 JSON 对象,解析此类响应必须先关闭 JSON 校验:
- type: custom-api
skip-json-validation: true
然后遍历每一行对象:
{{ range .JSONLines }}
<p>{{ .String "name" }} is {{ .Int "age" }} years old</p>
{{ end }}
输出:
<p>Steve is 30 years old</p>
<p>Alex is 25 years old</p>
<p>John is 35 years old</p>
.JSONLines 的实现是对原始响应体调用 gjson 的 ForEachLine 逐行解析(源码)。
由于底层是 gjson,还可以使用更高级的 gjson 选择器从 JSON Lines 中提取数据。例如用 ..#.name 拿到所有行的 name 字段:
{{ range .JSON.Array "..#.name" }}
<p>{{ .String "" }}</p>
{{ end }}
输出:
<p>Steve</p>
<p>Alex</p>
<p>John</p>
(skip-json-validation 开启后,fetchCustomAPIResponse 会跳过 gjson.Valid 校验分支,直接把原始文本交给 gjson 解析。)
多请求场景
subrequests:并发子请求
若一个组件需要同时展示多个接口的数据,可以在配置中声明 subrequests,它们会与主请求并发执行:
- type: custom-api
cache: 2h
subrequests:
another-one:
url: https://uselessfacts.jsph.pl/api/v2/facts/random
title: Random Fact
url: https://uselessfacts.jsph.pl/api/v2/facts/random
template: |
<p class="size-h4 color-paragraph">{{ .JSON.String "text" }}</p>
<p class="size-h4 color-paragraph margin-top-15">{{ (.Subrequest "another-one").JSON.String "text" }}</p>
子请求支持与主请求相同的全部属性(除 subrequests 自身外),例如 headers、parameters 等。从源码 fetchAndRenderCustomAPIRequest 可以看到:存在子请求时,主请求与所有子请求通过 goroutine 并发发出,且任一请求失败会 cancel 整个上下文、本次更新整体失败。为书写方便,可以先定义变量:
template: |
{{ $anotherOne := .Subrequest "another-one" }}
<p>{{ $anotherOne.JSON.String "text" }}</p>
子请求同样可以访问 .Response:
template: |
{{ $anotherOne := .Subrequest "another-one" }}
<p>{{ $anotherOne.Response.StatusCode }}</p>
需要留意:.Subrequest "key" 传入未定义的 key 会 panic(源码),Go 模板引擎会把 panic 捕获并作为模板执行错误返回,即该组件会展示错误信息而不是静默输出空值。
newRequest:在模板内发起连续请求
有时需要做两次有依赖的连续 API 调用:第一个请求的结果要拼进第二个请求。这时可以在模板内部用 newRequest 构造新请求,并用管道链式添加参数、请求头,最后由 getResponse 执行:
- type: custom-api
url: https://api.example.com/get-id-of-something
template: |
{{ $theID := .JSON.String "id" }}
{{
$something := newRequest (concat "https://api.example.com/something/" $theID)
| withParameter "key" "value"
| withHeader "Authorization" "Bearer token"
| getResponse
}}
{{ $something.JSON.String "title" }}
这里 $theID 来自第一次调用的结果,并被拼入第二次调用的 URL。从源码 newRequest 函数 还可以看到:newRequest 支持在 URL 后追加格式化参数(内部走 fmt.Sprintf),例如 {{ newRequest "https://api.example.com/something/%s" $theID }}。
模板内请求还支持 withStringBody(设置字符串 body,会自动以 POST 发送)、withBasicAuth、withAllowInsecure 等链式方法(源码)。
如果 URL 本身需要动态参数,可以完全省略 url 属性,把请求整体放到模板里发起——注意此时需要手动判断状态码:
- type: custom-api
title: Events from the last 24h
template: |
{{
$events := newRequest "https://api.example.com/events"
| withParameter "after" (offsetNow "-24h" | formatTime "rfc3339")
| getResponse
}}
{{ if eq $events.Response.StatusCode 200 }}
{{ range $events.JSON.Array "events" }}
<div>{{ .String "title" }}</div>
<div {{ .String "date" | parseTime "rfc3339" | toRelativeTime }}></div>
{{ end }}
{{ else }}
<p>Failed to fetch data: {{ $events.Response.Status }}</p>
{{ end }}
options:让模板可参数化复用
options 是一个映射,会传入模板并通过 .Options 访问器读取,适合作为模板的“可配置开关”,避免每处使用都去改模板本身。与其在模板里硬编码变量:
- type: custom-api
template: |
{{ /* User configurable options */ }}
{{ $collapseAfter := 5 }}
{{ $showThumbnails := true }}
{{ $showFlairs := false }}
<ul class="list list-gap-10 collapsible-container" data-collapse-after="{{ $collapseAfter }}">
{{ if $showThumbnails }}
<li>
<img src="{{ .JSON.String "thumbnail" }}" alt="thumbnail" />
</li>
{{ end }}
{{ if $showFlairs }}
<li>
<span class="flair">{{ .JSON.String "flair" }}</span>
</li>
{{ end }}
</ul>
不如改为在模板中通过 .Options 带默认值读取:
- type: custom-api
template: |
<ul class="list list-gap-10 collapsible-container" data-collapse-after="{{ .Options.IntOr "collapse-after" 5 }}">
{{ if (.Options.BoolOr "show-thumbnails" true) }}
<li>
<img src="{{ .JSON.String "thumbnail" }}" alt="thumbnail" />
</li>
{{ end }}
{{ if (.Options.BoolOr "show-flairs" false) }}
<li>
<span class="flair">{{ .JSON.String "flair" }}</span>
</li>
{{ end }}
</ul>
然后在组件配置中按需覆盖:
- type: custom-api
options:
collapse-after: 5
show-thumbnails: true
show-flairs: false
配合 YAML 锚点/合并键,同一模板即可复用于多个组件并传入不同取值:
# 注意 `custom-widgets` 不是特殊属性,只是用来定义可复用的 "anchor"
custom-widgets:
- &example-widget
type: custom-api
template: |
{{ .Options.StringOr "custom-option" "not defined" }}
pages:
- name: Home
columns:
- size: full
widgets:
- <<: *example-widget
options:
custom-option: "Value 1"
- <<: *example-widget
options:
custom-option: "Value 2"
.Options 提供的方法有 StringOr、IntOr、FloatOr、BoolOr(键不存在时返回默认值)以及 JSON。实现上是一个泛型函数 customAPIGetOptionOrDefault:类型断言失败或键缺失时同样回落到默认值;而 Options.JSON(key) 会把任意配置值序列化回 JSON 字符串(源码),键缺失时 panic。
组件属性速查
结合 docs/configuration.md 的属性表与 CustomAPIRequest 结构体 的 YAML 标签,custom-api 组件的完整属性如下:
| 属性 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
url |
string | 否 | — | 要请求的 URL,必须能从 Glance 所在服务器访问;省略时可在模板内用 newRequest 发请求 |
headers |
key/value 映射 | 否 | — | 附加请求头 |
method |
string | 否 | GET(有 body 时为 POST) |
GET、POST、PUT、PATCH、DELETE、OPTIONS、HEAD |
body-type |
string | 否 | json |
请求体类型,json 或 string |
body |
any | 否 | — | 请求体,字符串或映射 |
frameless |
boolean | 否 | false |
为 true 时移除组件边框与内边距(渲染模板 据此追加 widget-content-frameless 类) |
allow-insecure |
boolean | 否 | false |
忽略无效/自签名证书 |
skip-json-validation |
boolean | 否 | false |
跳过 JSON 校验,用于 JSON Lines 等响应 |
template |
string | 是 | — | Go html/template 模板 |
options |
map | 否 | — | 传入模板的选项映射,经 .Options 读取 |
parameters |
key (string) & value (string|array) | 否 | — | 查询参数;设置后会覆盖 URL 中已有的查询参数 |
subrequests |
请求映射 | 否 | — | 并发子请求,经 .Subrequest "key" 访问 |
另外两个属性未出现在配置文档的属性表中,但可以从源码结构确认存在:
basic-auth(username/password):结构体中定义为嵌套结构(源码),配置后走SetBasicAuth;mock-response(string):设置后组件不发起真实请求,而是直接把该字符串作为 JSON 响应体、状态码固定 200(源码),适合离线调试模板。
body 的两种写法示例:
body-type: json
body:
key1: value1
key2: value2
multiple-items:
- item1
- item2
body-type: string
body: |
key1=value1&key2=value2
函数参考
JSON 对象方法
以下方法可用在 .JSON(以及 range 循环内的元素、.Subrequest 结果)上:
String(key string) string:按键读取字符串值;Int(key string) int:按键读取整数值;Float(key string) float:按键读取浮点值;Bool(key string) bool:按键读取布尔值;Array(key string) []JSON:按键读取JSON对象数组;Exists(key string) bool:键(或路径)是否存在;Entries(key string):返回迭代器,用于遍历对象属性,{{ range $key, $value := .JSON.Entries "user" }},$key为字符串,$value为JSON对象。
Options 对象方法
StringOr(key string, default string) string:按键读字符串,缺失时返回默认值;IntOr(key string, default int) int:按键读整数,缺失时返回默认值;FloatOr(key string, default float) float:按键读浮点数,缺失时返回默认值;BoolOr(key string, default bool) bool:按键读布尔值,缺失时返回默认值;JSON(key string) JSON:把键对应的配置值序列化为字符串化 JSON;键不存在会抛出错误。
Glance 内置辅助函数
toFloat(i int) float:整数转浮点;toInt(f float) int:浮点转整数;toRelativeTime(t time.Time) template.HTMLAttr:生成动态相对时间(2h、1d 等),返回值必须用作 HTML 属性,如<span {{ toRelativeTime .Time }}></span>;now() time.Time:当前时间;offsetNow(offset string) time.Time:偏移后的当前时间,offset 格式如"3h"、"-1h"、"2h30m10s";duration(str string) time.Duration:解析"1h"、"24h"、"5h30m"等字符串为时长;parseTime(layout string, s string) time.Time:按 Go 时间格式解析字符串;别名可用"unix"、"RFC3339"、"RFC3339Nano"、"DateTime"、"DateOnly",其余情况按 Go 布局字面量处理;formatTime(layout string, s string) time.Time:把时间格式化为字符串,布局规则与parseTime相同;参数顺序经过翻转以支持管道写法;parseLocalTime(layout string, s string) time.Time:同parseTime,但自动转换到服务器时区;无时区时使用本地时区而非 UTC;parseRelativeTime(layout string, s string) time.Time:{{ .String "date" | parseTime "rfc3339" | toRelativeTime }}的简写;add(a, b float) float/sub(a, b float) float/mul(a, b float) float/div(a, b float) float:四则运算;mod(a, b int) int:取余(a % b);formatApproxNumber(n int) string:人类可读数量,如 1000 -> 1k(实现:<1k 原样,<10k 一位小数 k,<1m 整数 k,以上一位小数 m);formatNumber(n float|int) string:千分位格式,如 1000 -> 1,000(底层为message.Printer按英文 locale 输出,源码);trimPrefix(prefix string, str string) string/trimSuffix(suffix string, str string) string/trimSpace(str string) string:去前缀、去后缀、去首尾空白;replaceAll(old string, new string, str string) string:字符串全量替换;replaceMatches(pattern string, replacement string, str string) string:正则全量替换;findMatch(pattern string, str string) string:返回第一个正则匹配;findSubmatch(pattern string, str string) string:返回第一个子匹配;sortByString(key string, order string, arr []JSON) []JSON/sortByInt/sortByFloat:按指定键与asc/desc排序(实现);sortByTime(key string, layout string, order string, arr []JSON) []JSON:按时间键排序,布局同parseTime;concat(strings ...string) string:拼接字符串;unique(key string, arr []JSON) []JSON:按键值去重(实现,按首现顺序保留);percentChange(current float, previous float) float:计算两数的百分比变化(实现);startOfDay(t time.Time) time.Time/endOfDay(t time.Time) time.Time:当天 0 点 / 23:59:59。
Go text/template 内置函数
eq/ne/lt/le/gt/ge:相等、不等、小于、小于等于、大于、大于等于比较;and(args ...bool) bool:全部为真才为真(两个及以上参数);or(args ...bool) bool:任一为真即为真(两个及以上参数);not(a bool) bool:取反;index(a any, b int) any:取数组指定下标元素;len(a any) int:数组长度;printf(format string, a ...any) string:格式化字符串。
源码中额外可用的函数
除了文档列出清单外,模板函数表 还注册了这些函数,编写复杂模板时可用:
randomElement(arr []JSON):随机取数组一个元素(空数组返回空结果);withStringBody(body string, req)/withBasicAuth(username, password string, req)/withAllowInsecure(val, req):模板内构造请求时的链式方法;iconWithClass(name, class string):输出带 CSS 类的内置图标;safeHTML/safeURL/safeCSS:把字符串声明为安全的 HTML/URL/CSS(关闭自动转义),实现;absInt(i int) int、formatPrice(price float)、formatPriceWithPrecision(precision, price):绝对值与价格格式化。
实战示例
docs/configuration.md 给出了三个由简到繁的官方示例,均可直接复制到 glance.yml 参考:
1. 单字段展示(Random Fact)
- type: custom-api
title: Random Fact
cache: 6h
url: https://uselessfacts.jsph.pl/api/v2/facts/random
template: |
<p class="size-h4 color-paragraph">{{ .JSON.String "text" }}</p>
2. 多列统计 + 运算(Immich stats)
演示了请求头、formatNumber 千分位以及单位换算(字节除以 2^30 得 GB):
- type: custom-api
title: Immich stats
cache: 1d
url: https://${IMMICH_URL}/api/server/statistics
headers:
x-api-key: ${IMMICH_API_KEY}
Accept: application/json
template: |
<div class="flex justify-between text-center">
<div>
<div class="color-highlight size-h3">{{ .JSON.Int "photos" | formatNumber }}</div>
<div class="size-h6">PHOTOS</div>
</div>
<div>
<div class="color-highlight size-h3">{{ .JSON.Int "videos" | formatNumber }}</div>
<div class="size-h6">VIDEOS</div>
</div>
<div>
<div class="color-highlight size-h3">{{ div (.JSON.Int "usage" | toFloat) 1073741824 | toInt | formatNumber }}GB</div>
<div class="size-h6">USAGE</div>
</div>
</div>
3. 列表 + 排序条件样式(Steam Specials)
演示了深层路径 specials.items、模板内变量、printf 格式化价格与条件 class:
- type: custom-api
title: Steam Specials
cache: 12h
url: https://store.steampowered.com/api/featuredcategories?cc=us
template: |
<ul class="list list-gap-10 collapsible-container" data-collapse-after="5">
{{ range .JSON.Array "specials.items" }}
<li>
<a class="size-h4 color-highlight block text-truncate" href="https://store.steampowered.com/app/{{ .Int "id" }}/">{{ .String "name" }}</a>
<ul class="list-horizontal-text">
<li>{{ div (.Int "final_price" | toFloat) 100 | printf "$%.2f" }}</li>
{{ $discount := .Int "discount_percent" }}
<li{{ if ge $discount 40 }} class="color-positive"{{ end }}>{{ $discount }}% off</li>
</ul>
</li>
{{ end }}
</ul>
实践注意事项
- 缓存:组件默认缓存 1 小时(源码),频繁变化的数据请用通用
cache属性调小; - JSON 校验:默认要求响应是合法 JSON;非 2xx 状态码会直接以“状态码 状态文本”形式报错,2xx 但 JSON 非法则会记录错误日志(含截断到 100 字符的响应体)并返回
invalid response JSON(校验逻辑)。排查问题时可先看 Glance 日志,或用mock-response离线复现; - parameters 覆盖 URL 查询串:设置
parameters后 URL 中已有的查询参数会被整体替换; - 模板输出即最终 HTML:模板在 custom-api.html 中以
CompiledHTML(template.HTML)形式注入组件主体,因此模板内写什么就渲染什么;类名可复用 Glance 的 CSS 工具类(如size-h4、color-highlight、list、collapsible-container,见 internal/glance/static/css); - 相对时间必须用作属性:
toRelativeTime/parseRelativeTime的返回值是template.HTMLAttr,要放在标签属性位置,客户端脚本会依据data-dynamic-relative-time持续更新显示; - 正则函数有全局缓存:
replaceMatches/findMatch/findSubmatch使用带锁的正则缓存(源码),同一 pattern 只编译一次,可以放心在循环中使用。
掌握以上内容,就覆盖了 custom-api 组件模板语法的全部官方用法:从标量读取、数组与深层路径遍历,到条件判断、算术、时间相对化、JSON Lines、并发子请求与模板内二次请求,再到 options 参数化复用——足以把绝大多数自有或第三方 JSON API 稳定地接入 Glance 仪表盘。
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

