TimeLens 第三方小组件开发指南
本文档介绍如何为 TimeLens v1.1.0 开发第三方 JavaScript 小组件。通过简单的 manifest.json + JS 入口文件,即可创建功能丰富的桌面悬浮组件。
功能范围(v1.1.0)
当前支持:
- 本地目录加载(
manifest.json+ JS 入口文件) - 运行时从应用数据目录
widgets自动发现 - 渲染契约:
createWidget()工厂函数 或mount()直接导出 - 只读数据 API(屏幕时间、活跃窗口、目标进度等)
- 基于权限的访问控制
尚未支持:
- 远程应用商店 / 云端分发
- 代码签名验证
- 自动更新机制
目录结构
每个第三方小组件在应用数据目录的 widgets 文件夹下拥有独立目录:
%APPDATA%\com.timelens.app\widgets\
my-widget/
manifest.json ← 小组件描述文件
index.js ← 入口 JS(ESM 模块)
assets/
icon.png ← 可选:小组件图标
styles.css ← 可选:样式文件
manifest.json 详解
清单文件向 TimeLens 注册表描述你的小组件。以下是完整示例:
{
"widget_type": "my_awesome_widget",
"name": "我的超棒小组件",
"description": "这是一个演示用途的第三方小组件",
"version": "1.0.0",
"author": "你的名字 ",
"entry": "index.js",
"icon": "assets/icon.png",
"default_size": {
"width": 360,
"height": 240
},
"permissions": [
"screen-time:read",
"active-window:subscribe",
"goals:read"
]
}
必填字段
| 字段 | 类型 | 说明 |
|---|---|---|
widget_type | string | 唯一标识符,仅允许 a-zA-Z0-9_- |
name | string | 在小组件中心显示的名称 |
entry | string | 入口 JS 文件路径(相对于 manifest 所在目录) |
可选字段
| 字段 | 类型 | 说明 |
|---|---|---|
description | string | 简短描述 |
version | string | 语义化版本号 |
author | string | 作者名或邮箱 |
icon | string | 图标路径(相对路径) |
default_size.width | number | 默认窗口宽度(像素) |
default_size.height | number | 默认窗口高度(像素) |
permissions | string[] | 所需权限列表 |
权限系统
第三方小组件在沙箱 iframe 中运行,必须在 manifest 中声明所需权限。用户首次创建小组件时会被提示授权。
| 权限 | 访问范围 |
|---|---|
screen-time:read | 读取屏幕时间统计数据(今日、小时分布、趋势) |
active-window:subscribe | 订阅前台窗口变更事件 |
goals:read | 读取使用目标和进度 |
focus:read | 读取专注模式状态 |
widgets:read | 读取小组件注册表信息 |
渲染接口契约
TimeLens 将入口文件作为 ESM 模块加载。支持两种导出模式:
模式 A — createWidget() 工厂函数
适合需要在 mount/unmount 之间保持状态的小组件。
export function createWidget() {
let root;
let intervalId;
return {
mount(container, context) {
root = document.createElement("div");
root.style.cssText = `
padding: 16px;
color: #e6edf3;
font-family: Inter, system-ui, sans-serif;
`;
root.innerHTML = `${context.displayName}
`;
container.appendChild(root);
intervalId = setInterval(() => {
root.querySelector("h3").textContent =
new Date().toLocaleTimeString();
}, 1000);
},
unmount() {
clearInterval(intervalId);
if (root && root.parentNode) {
root.parentNode.removeChild(root);
}
},
};
}
模式 B — 直接导出 mount()
适合简单的一次性渲染组件。
let root;
export function mount(container, context) {
root = document.createElement("div");
root.innerHTML = `
`;
container.appendChild(root);
}
export function unmount() {
if (root && root.parentNode) {
root.parentNode.removeChild(root);
}
}
Context 上下文对象
| 属性 | 类型 | 说明 |
|---|---|---|
widgetType | string | 小组件类型标识符(同 manifest 中的 widget_type) |
displayName | string | 显示名称(同 manifest 中的 name) |
widgetId | string | 唯一实例 ID |
permissions | string[] | 用户已授予的权限列表 |
数据通道 API
小组件通过 window.parent.postMessage 与 TimeLens 核心通信。父窗口提供了请求/响应桥接。
请求格式
window.parent.postMessage({
type: "timelens:request",
id: "req-" + Date.now(), // 唯一请求 ID
method: "getTodayAppTotals", // 调用的 API 方法名
params: {} // 参数对象
}, "*");
响应监听
window.addEventListener("message", (e) => {
if (e.data.type === "timelens:response") {
if (e.data.error) {
console.error("API 错误:", e.data.error);
} else {
console.log("API 结果:", e.data.result);
}
}
});
常用数据方法
所有来自 API 参考 的只读方法都可通过数据通道调用:
getTodayAppTotals— 今日各应用使用时长getTodayHourly— 今日小时分布getRecentDailyTotals— 最近 N 天每日总时长getMonitorStatus— 当前监控状态getGoalProgress— 目标进度列表getCategoryTotalsInRange— 分类统计
样式设计规范
小组件在透明背景的 iframe 中运行。为保证与 TimeLens 视觉风格一致,建议遵循以下规范:
| 属性 | 推荐值 |
|---|---|
| 背景色 | rgba(22, 27, 34, 0.75) + backdrop-filter: blur(12px) |
| 主文字 | #e6edf3 |
| 次级文字 | #8b949e |
| 强调色 | #58a6ff |
| 边框 | 1px solid rgba(48, 54, 61, 0.6) |
| 圆角 | 12px 或 8px |
| 字体 | Inter, system-ui, -apple-system, sans-serif |
| 阴影 | 0 4px 12px rgba(0, 0, 0, 0.3) |
完整示例:今日应用排行小组件
// index.js
export function createWidget() {
let root;
let interval;
let unlisten;
function requestData(method, params = {}) {
return new Promise((resolve, reject) => {
const reqId = "req-" + Date.now() + "-" + Math.random();
const handler = (e) => {
if (e.data.type === "timelens:response" && e.data.id === reqId) {
window.removeEventListener("message", handler);
if (e.data.error) reject(e.data.error);
else resolve(e.data.result);
}
};
window.addEventListener("message", handler);
window.parent.postMessage({
type: "timelens:request",
id: reqId,
method,
params
}, "*");
});
}
async function renderStats() {
try {
const stats = await requestData("getTodayAppTotals");
const top5 = stats.slice(0, 5);
const html = top5.map(s => {
const mins = Math.round(s.total_seconds / 60);
return `
${s.app_name}
${mins} 分钟
`;
}).join("");
const el = root.querySelector("#content");
if (el) el.innerHTML = html || "暂无数据
";
} catch (err) {
console.error("获取数据失败:", err);
}
}
return {
async mount(container, context) {
root = document.createElement("div");
root.style.cssText = `
padding: 16px;
color: #e6edf3;
font-family: Inter, system-ui, sans-serif;
background: rgba(22, 27, 34, 0.8);
backdrop-filter: blur(12px);
border-radius: 12px;
border: 1px solid rgba(48, 54, 61, 0.5);
height: 100%;
box-sizing: border-box;
`;
root.innerHTML = `
${context.displayName}
加载中...
`;
container.appendChild(root);
await renderStats();
interval = setInterval(renderStats, 60000);
},
unmount() {
clearInterval(interval);
if (root && root.parentNode) {
root.parentNode.removeChild(root);
}
}
};
}
调试技巧
- 在小组件窗口上右键 → 检查,打开 DevTools 调试 JavaScript。
- 如果数据请求失败,检查 小组件中心 → 权限标签页,确认已授予相应权限。
- 使用任意 JSON 验证工具检查
manifest.json语法。 - 小组件中的
console.log输出会显示在主窗口的 DevTools 控制台中。 - 在
window.parent.postMessage调用前添加日志,确认请求已发送。
官方模板
TimeLens 仓库提供了官方示例模板:
examples/third-party-widget-template/
├── manifest.json
├── index.js
└── README.md
建议以此模板为基础开始你的小组件开发。
最后更新:2025-05-03 · TimeLens v1.1.0