```markdown # Connectors `live-dashboard` 是一个**动态工件**。它显示的值并非硬编码——而是在运行时从连接器轮询获取。OD 守护进程(0.4.0+)附带一个 Composio 连接器目录,以及一份 `connectors.json` 契约文件,工件会随 `index.html` 一同生成该文件。 当 `inputs.connector === mock`(或守护进程无法解析已配置的连接器)时,工件会回退到预设的样本数据。这保证了截图、选择器预览和离线使用的正常运作。 > **状态说明——与 `skills/live-artifact/` 的关系。** > > 当前正式发布中的动态工件契约规范位于 > [`skills/live-artifact/SKILL.md`](../../live-artifact/SKILL.md):它采用**文件形态**(`artifact.json` + `template.html` + `data.json` + > `provenance.json`),在代理端则采用 **CLI 形态**(代理调用 > `"$OD_NODE_BIN" "$OD_BIN" tools live-artifacts {create,update}` 和 > `tools connectors {list,execute}` 而非 HTTP)。渲染器是仅支持标量的 `html_template_v1`(`apps/daemon/src/live-artifacts/render.ts`)。 > > `live-dashboard` 是一个**互补的**浏览器运行时变体: > 工件以单个自包含 HTML 页面形式渲染,而动态行为(打开时刷新、手动刷新、自动刷新、过期标记)在页面内运行,而非在模板渲染时处理。因此轮询需要一种 HTTP 形态,这正是本文件其余部分所描述的内容(`POST /api/od/connectors/poll`)。 > > 请将下面的 HTTP 形态视为一份**前瞻性提案**,它与文件/CLI 契约并行存在:守护进程目前尚未暴露 > `POST /api/od/connectors/poll`(`apps/daemon/src/server.ts` / > `apps/daemon/src/live-artifacts/`),因此开箱即用时工件会使用预设的样本数据进行渲染,刷新按钮仅对模拟数据进行过渡动画。当守护进程团队的路由就绪后,只需将模板中的 `seedNextChange()` 替换为本文档中说明的 `poll()` > 辅助函数即可——`connectors.json` 形态已经是一份可用的声明式事实来源,下游工具(动态工件 CLI、MCP 封装、审计日志)今天即可读取。 --- ## `connectors.json` 模式 在项目根目录下,`index.html` 旁边生成一个 `connectors.json`: ```json { "schema": "od.connector/1", "primary": "notion", "freshness": { "auto_refresh_seconds": 30, "warn_after_seconds": 90, "fail_after_seconds": 600 }, "bindings": { "notion": { "provider": "composio.notion", "auth_ref": "media-config.json#notion.token", "reads": [ { "id": "tasks_active", "endpoint": "databases.{id}.query", "params": { "id": "${notion.tasks_db_id}", "filter": { "property": "Status", "status": { "does_not_equal": "Done" } }, "sorts": [{ "property": "Updated", "direction": "descending" }] }, "shape": "task[]" }, { "id": "kpi_total", "endpoint": "databases.{id}.query", "params": { "id": "${notion.tasks_db_id}", "page_size": 1 }, "extract": "$.metadata.total_count", "shape": "integer" }, { "id": "activity_recent", "endpoint": "search", "params": { "filter": { "property": "object", "value": "page" }, "sort": { "direction": "descending", "timestamp": "last_edited_time" }, "page_size": 8 }, "shape": "activity[]" } ], "events": [ { "id": "task_changed", "type": "page.updated", "selector": "$.tasks_db_id" } ] } } } ``` 该形态刻意贴近 Notion 的 REST API——守护进程的连接器适配器会将 `endpoint` 和 `params` 重写为实际的提供商调用。其他连接器(Linear、Stripe、Posthog)遵循相同的形态,使用各自提供商特有的 `endpoint` 字符串。 --- ## 解析顺序(守护进程的执行流程) 1. 从工件目录读取 `connectors.json`。 2. 在 Composio 目录中查找 `bindings[primary].provider`。 3. 根据守护进程的 `media-config.json` 解析 `auth_ref`。实际的查找过程是环境感知的(参见 [`apps/daemon/src/media-config.ts`](../../../apps/daemon/src/media-config.ts) 中的 `configFile()`,优先级从高到低): - 当设置了 `OD_MEDIA_CONFIG_DIR` 环境变量时,使用 `/media-config.json`; - 否则,当设置了 `OD_DATA_DIR` 时,使用 `/media-config.json`(相对路径锚定到当前项目根目录,`$HOME` 和 `~` 简写会被展开); - 否则,使用当前项目的 `/.od/media-config.json`。 工件本身从不直接打开这些路径——它始终通过守护进程的轮询端点进行访问,守护进程会强制执行正确的查找顺序。**切勿**在工件中读取令牌。 4. 对于每个 `reads[].endpoint`,守护进程使用解析后的认证信息构建实际的 HTTP 请求,并从已解析的 `media-config.json#.*` 值中替换 `${...}` 占位符。 5. 将响应缓存 `freshness.auto_refresh_seconds` 秒。`Refresh` 按钮会发出一个绕过缓存的显式轮询请求。 --- ## 在 `index.html` 中的接入方式 工件**不**直接调用 Composio,而是调用 OD 守护进程的本地代理: ```js async function poll(readId) { const res = await fetch(`/api/od/connectors/poll`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ project: "", read: readId }) }); if (!res.ok) throw new Error(`poll ${readId} failed: ${res.status}`); return res.json(); } ``` `` 由守护进程在渲染时通过一个 `` 标签注入。工件在挂载时读取一次。 --- ## 回退行为 - `fetch` 错误时:保留之前渲染的值,将动态标记切换为琥珀色的 `Stale · `,在页脚写入一行灰色小提示(`Source: Notion · last good poll 4 min ago`)。 - `inputs.connector === mock` 时:完全跳过 `poll()`,使用 `index.html` 中的 `seedMock()` 函数。动态标记显示灰色的 `Sample data`,不带圆点动画。 - 401/403 错误时:显示一次性提示 `Reconnect Notion in Settings → Connectors`,并停止后续轮询,直到用户手动点击刷新。 --- ## 各提供商速查表 | Connector | provider | 单行数据形态 | 典型 KPI | | --------- | ----------------- | --------------------------------- | ------------------------------------- | | Notion | `composio.notion` | `task = {title, status, assignee, due, prio, updated}` | 总任务 · 本周完成 · 成员 · 待审 | | Linear | `composio.linear` | `issue = {title, state, assignee, priority, updated}` | 积压 · 进行中 · 阻塞 · 迭代进度 | | Stripe | `composio.stripe` | `event = {type, amount, customer, created}` | MRR · 流失 · 新订阅 · 退款 | | Posthog | `composio.posthog` | `event = {name, distinct_id, $current_url, ts}` | DAU · 注册 · 功能采用 · 错误 | 不要自行发明各提供商的数据形态。如果用户需要上表中未列出的内容,请回退到 `mock`,并在页脚显示提示,请用户扩展连接器目录。 ```