名称: glance
描述: "创建、更新和管理 Glance 仪表板小组件。当用户想要:向仪表板添加内容、创建小组件、可视化跟踪数据、显示指标/统计数据、展示 API 数据或监控使用情况时使用。"
元数据:
openclaw:
emoji: "🖥️"
homepage: "https://github.com/acfranzen/glance"
requires:
env: ["GLANCE_URL"]
bins: ["curl"]
primaryEnv: GLANCE_URL
AI 可扩展的个人仪表板。使用自然语言创建自定义小组件——AI 负责数据收集。
# 导航到技能目录(如果通过 ClawHub 安装)
cd "$(clawhub list | grep glance | awk '{print $2}')"
# 或直接克隆
git clone https://github.com/acfranzen/glance ~/.glance
cd ~/.glance
# 安装依赖
npm install
# 配置环境
cp .env.example .env.local
# 使用你的设置编辑 .env.local 文件
# 启动开发服务器
npm run dev
# 或构建并启动生产环境
npm run build && npm start
仪表板运行在 http://localhost:3333
编辑 .env.local 文件:
# 服务器
PORT=3333
AUTH_TOKEN=your-secret-token # 可选:Bearer 令牌认证
# OpenClaw 集成(用于即时小组件刷新)
OPENCLAW_GATEWAY_URL=https://localhost:18789
OPENCLAW_TOKEN=your-gateway-token
# 数据库
DATABASE_PATH=./data/glance.db # SQLite 数据库位置
# 创建 launchd plist 文件
cat > ~/Library/LaunchAgents/com.glance.dashboard.plist << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.glance.dashboard</string>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/bin/npm</string>
<string>run</string>
<string>dev</string>
</array>
<key>WorkingDirectory</key>
<string>~/.glance</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>~/.glance/logs/stdout.log</string>
<key>StandardErrorPath</key>
<string>~/.glance/logs/stderr.log</string>
</dict>
</plist>
EOF
# 加载服务
mkdir -p ~/.glance/logs
launchctl load ~/Library/LaunchAgents/com.glance.dashboard.plist
# 服务命令
launchctl start com.glance.dashboard
launchctl stop com.glance.dashboard
launchctl unload ~/Library/LaunchAgents/com.glance.dashboard.plist
| 变量 | 描述 | 默认值 |
|---|---|---|
PORT |
服务器端口 | 3333 |
AUTH_TOKEN |
API 认证的 Bearer 令牌 | — |
DATABASE_PATH |
SQLite 数据库路径 | ./data/glance.db |
OPENCLAW_GATEWAY_URL |
用于 Webhook 的 OpenClaw 网关 | — |
OPENCLAW_TOKEN |
OpenClaw 认证令牌 | — |
创建和管理仪表板小组件。大多数小组件使用 agent_refresh——你负责收集数据。
# 检查 Glance 是否正在运行(列出小组件)
curl -s -H "Origin: $GLANCE_URL" "$GLANCE_URL/api/widgets" | jq '.custom_widgets[].slug'
# 认证说明:带有 Origin 头的本地请求绕过 Bearer 令牌认证
# 对于外部访问,请使用:-H "Authorization: Bearer $GLANCE_TOKEN"
# 刷新小组件(查找指令,收集数据,POST 到缓存)
sqlite3 $GLANCE_DATA/glance.db "SELECT json_extract(fetch, '$.instructions') FROM custom_widgets WHERE slug = 'my-widget'"
# 按照指令操作,然后:
curl -X POST "$GLANCE_URL/api/widgets/my-widget/cache" \
-H "Content-Type: application/json" \
-H "Origin: $GLANCE_URL" \
-d '{"data": {"value": 42, "fetchedAt": "'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}}'
# 在浏览器中验证
browser action:open targetUrl:"$GLANCE_URL"
生成小组件定义时,请使用 JSON 模式 docs/schemas/widget-schema.json 配合你的 AI 模型的结构化输出模式:
- Anthropic:使用 tool_use 配合模式
- OpenAI:使用 response_format: { type: "json_schema", schema }
该模式在生成时强制执行所有必需字段——无法生成格式错误的小组件。
每个小组件必须包含以下字段(模式会强制执行):
| 字段 | 类型 | 说明 |
|---|---|---|
name |
字符串 | 非空,人类可读 |
slug |
字符串 | 小写 kebab-case (my-widget) |
source_code |
字符串 | 包含 Widget 函数的有效 JSX |
default_size |
{ w: 1-12, h: 1-20 } |
网格单位 |
min_size |
{ w: 1-12, h: 1-20 } |
无法调整到更小 |
fetch.type |
枚举 | "server_code" | "webhook" | "agent_refresh" |
fetch.instructions |
字符串 | 如果类型是 agent_refresh 则必需 |
fetch.schedule |
字符串 | 如果类型是 agent_refresh 则必需 (cron 表达式) |
data_schema.type |
"object" |
始终为 object |
data_schema.properties |
对象 | 定义每个字段 |
data_schema.required |
数组 | 必须包含 "fetchedAt" |
credentials |
数组 | 如果不需要则使用 [] |
{
"name": "我的小组件",
"slug": "my-widget",
"source_code": "function Widget({ serverData }) { return <div>{serverData?.value}</div>; }",
"default_size": { "w": 2, "h": 2 },
"min_size": { "w": 1, "h": 1 },
"fetch": {
"type": "agent_refresh",
"schedule": "*/15 * * * *",
"instructions": "## 数据收集\n收集数据...\n\n## 缓存更新\nPOST 到 /api/widgets/my-widget/cache"
},
"data_schema": {
"type": "object",
"properties": {
"value": { "type": "number" },
"fetchedAt": { "type": "string", "format": "date-time" }
},
"required": ["value", "fetchedAt"]
},
"credentials": []
}
每个小组件在完成前必须完成所有步骤:
□ 步骤 1:创建小组件定义 (POST /api/widgets)
- 包含 Widget 函数的 source_code
- data_schema(验证必需)
- fetch 配置(agent_refresh 的类型 + 指令)
□ 步骤 2:添加到仪表板 (POST /api/widgets/instances)
- custom_widget_id 与定义匹配
- 设置 title 和 config
□ 步骤 3:填充缓存(针对 agent_refresh 小组件)
- 数据与 data_schema 完全匹配
- 包含 fetchedAt 时间戳
□ 步骤 4:设置 cron 任务(针对 agent_refresh 小组件)
- 简单消息:"⚡ WIDGET REFRESH: {slug}"
- 适当的计划(通常为 */15 或 */30)
□ 步骤 5:浏览器验证(强制)
- 打开 http://localhost:3333
- 小组件在仪表板上可见
- 显示实际数据(非加载旋转器)
- 数据值与缓存匹配
- 无错误或布局损坏
⛔ 在步骤 5 通过之前,不要报告小组件已完成!
docs/widget-sdk.md小组件包
├── meta (名称, slug, 描述, 作者, 版本)
├── widget (source_code, default_size, min_size)
├── fetch (server_code | webhook | agent_refresh)
├── dataSchema? (缓存数据的 JSON 模式 - POST 时验证)
├── cache (ttl, staleness, fallback)
├── credentials[] (API 密钥, 本地软件要求)
├── config_schema? (用户选项)
└── error? (重试, 回退, 超时)
数据是否可通过小组件调用的 API 获取?
├── 是 → 使用 server_code
└── 否 → 是否有外部服务推送数据?
├── 是 → 使用 webhook
└── 否 → 使用 agent_refresh(你收集数据)
| 场景 | 获取类型 | 谁收集数据? |
|---|---|---|
| 公共/认证 API | server_code |
小组件在渲染时调用 API |
| 外部服务推送数据 | webhook |
外部服务 POST 到缓存 |
| 本地 CLI 工具 | agent_refresh |
你(代理)通过 PTY/exec |
| 交互式终端 | agent_refresh |
你(代理)通过 PTY |
| 计算/聚合数据 | agent_refresh |
你(代理)按计划 |
⚠️ agent_refresh 意味着你是数据源。 你设置一个 cron 来提醒自己,然后你使用你的工具(exec、PTY、浏览器等)收集数据并 POST 到缓存。
| 方法 | 端点 | 描述 |
|---|---|---|
POST |
/api/widgets |
创建小组件定义 |
GET |
/api/widgets |
列出所有定义 |
GET |
/api/widgets/:slug |
获取单个定义 |
PATCH |
/api/widgets/:slug |
更新定义 |
DELETE |
/api/widgets/:slug |
删除定义 |
| 方法 | 端点 | 描述 |
|---|---|---|
POST |
/api/widgets/instances |
将小组件添加到仪表板 |
GET |
/api/widgets/instances |
列出仪表板小组件 |
PATCH |
/api/widgets/instances/:id |
更新实例(配置、位置) |
DELETE |
/api/widgets/instances/:id |
从仪表板移除 |
| 方法 | 端点 | 描述 |
|---|---|---|
GET |
/api/credentials |
列出凭证 + 状态 |
POST |
/api/credentials |
存储凭证 |
DELETE |
/api/credentials/:id |
删除凭证 |
{
"name": "GitHub PRs",
"slug": "github-prs",
"description": "显示打开的拉取请求",
"source_code": "function Widget({ serverData }) { ... }",
"default_size": { "w": 2, "h": 2 },
"min_size": { "w": 1, "h": 1 },
"refresh_interval": 300,
"credentials": [
{
"id": "github",
"type": "api_key",
"name": "GitHub 个人访问令牌",
"description": "具有 repo 范围的令牌",
"obtain_url": "https://github.com/settings/tokens"
}
],
"fetch": {
"type": "agent_refresh",
"schedule": "*/5 * * * *",
"instructions": "从 GitHub API 获取打开的 PR 并 POST 到缓存端点",
"expected_freshness_seconds": 300,
"max_staleness_seconds": 900
},
"cache": {
"ttl_seconds": 300,
"max_staleness_seconds": 900,
"storage": "sqlite",
"on_error": "use_stale"
},
"setup": {
"description": "配置 GitHub 令牌",
"agent_skill": "通过 /api/credentials 存储 GitHub PAT",
"verification": {
"type": "cache_populated",
"target": "github-prs"
},
"idempotent": true
}
}
| 类型 | 何时使用 | 数据流 |
|---|---|---|
server_code |
小组件可以直接调用 API | 小组件 → server_code → API |
agent_refresh |
代理必须获取/计算数据 | 代理 → POST /cache → 小组件读取 |
webhook |
外部服务推送数据 | 外部 → POST /cache → 小组件读取 |
大多数小组件应使用 agent_refresh——代理按计划获取数据并推送到缓存端点。
POST /api/widgets
Content-Type: application/json
{
"name": "GitHub PRs",
"slug": "github-prs",
"description": "显示打开的拉取请求",
"source_code": "function Widget({ serverData }) { ... }",
"default_size": { "w": 2, "h": 2 },
"credentials": [...],
"fetch": { "type": "agent_refresh", "schedule": "*/5 * * * *", ... },
"data_schema": {
"type": "object",
"properties": {
"prs": { "type": "array", "description": "PR 对象列表" },
"fetchedAt": { "type": "string", "format": "date-time" }
},
"required": ["prs", "fetchedAt"]
},
"cache": { "ttl_seconds": 300, ... }
}
data_schema(必需) 定义了获取器与小组件之间的数据契约。缓存 POST 会针对它进行验证——格式错误的数据返回 400。
⚠️ 创建小组件时始终包含
data_schema。这确保:
1. 缓存 POST 时的数据验证(模式不匹配时返回 400)
2. 预期数据结构的清晰文档
3. AI 代理知道要生成的确切格式
POST /api/widgets/instances
Content-Type: application/json
{
"type": "custom",
"title": "GitHub PRs",
"custom_widget_id": "cw_abc123",
"config": { "owner": "acfranzen", "repo": "libra" }
}
POST /api/widgets/github-prs/cache
Content-Type: application/json
{
"data": {
"prs": [...],
"fetchedAt": "2026-02-03T14:00:00Z"
}
}
⚠️ 如果小组件有 dataSchema,缓存端点会根据它验证你的数据。 错误数据返回 400 并附带详细信息。POST 前始终检查小组件的模式:
GET /api/widgets/github-prs
# 响应包含显示必需字段和类型的 dataSchema
⚠️ 强制:每次小组件创建和刷新必须以浏览器验证结束。
在视觉上确认小组件在仪表板上正确渲染之前,永远不要认为小组件“已完成”。
```javascript
// 必需:打开仪表板并验证小组件渲染
browser({
action: 'open',
targetUrl: 'http://localhost:3333',
profile: 'openclaw'
});
// 截图并检查小组件
browser({ action: 'snapshot' });
// 检查:
// 1. 小组件