# dsh-grafana

[English](./README.md)

一个面向 Agent 的 Grafana 可观测 DeepSeek Harness 插件——读取大盘、查询实时指标、跟踪趋势与告警、跨源对比调查，并能安全、可审查地写回大盘。插件直接操作 Grafana API 与 Dashboard JSON，不需要截图。

> 项目状态：1.0 前版本。核心写回路径已经具备安全保护和自动化测试，但尚未完成 Grafana 12+ 兼容性认证。

## 核心能力

- 通过浏览器 URL 或 UID 获取大盘；超大盘可改用结构化摘要模式，只返回面板/查询/阈值/变量骨架。
- 按标题和标签搜索大盘。
- 粘贴大盘或面板视图 URL，直接查询面板背后的真实数据。
- 把大盘复制为全新大盘，并返回新大盘地址。
- 通过对话调整面板、查询、阈值、变量和布局。
- 写回时自动保持大盘所在文件夹。
- 写入前检测并发修改。
- 每次写入都必须经过 DSH 原生用户审批。
- Service Account 凭证只保存在本机 DSH 凭证库。
- 可配置多个具名 Grafana 源站，每次工具调用按名称指定目标。

## 环境要求

| 组件 | 已支持基线 |
| --- | --- |
| Node.js | 20.11 或更高版本 |
| DeepSeek Harness | 默认开发依赖为 `dsh-tools@0.2.0-rc.2`；另覆盖 `0.2.1-alpha.1` 的自动化依赖兼容检查。历史基线：`0.1.0-rc.6`、`0.1.1-rc.2`、`0.1.2-rc.1`、`0.1.3-alpha.2`、`0.1.5-rc.1`、`0.1.7-rc.2`。真实宿主与浏览器验收另见 `INTEGRATION.md`。 |
| Grafana | Grafana 10/11 文档中的传统 Dashboard HTTP API |

Grafana 12 引入了新 Dashboard API。旧接口可能仍然可用，但 Grafana 12+ 暂未进入本插件的正式兼容矩阵。

标准 npm 依赖解析与 DSH 宿主预检采用不同的预发布匹配方式：宿主 `0.2.0-rc.2` 和 `0.2.1-alpha.1` 的预检启用 `includePrerelease`。插件仍显式声明 `0.2.1` 元组，供标准依赖解析使用；这不表示已完成 alpha 宿主、桌面端或浏览器认证。

本插件无构建步骤：纯 ESM JavaScript，安装即可加载，无需编译或打包。

## 安装

正式使用应安装不可变的 Release tag：

```bash
dsh plugin --profile <profile> add github:guhanfei-ai/dsh-grafana#v<version>
```

仅在测试时安装会持续变化的默认分支：

```bash
dsh plugin --profile <profile> add github:guhanfei-ai/dsh-grafana
```

本地开发：

```bash
npm ci
dsh plugin --profile <profile> add link:/绝对路径/dsh-grafana
```

安装后重启对应 DSH profile。

Windows 可使用 `link:C:/path/to/dsh-grafana` 形式的绝对路径。插件运行本身支持跨平台。

## 升级说明

### 0.12.0：工具改名与旧宿主界面

**两个工具改名。** `grafana_query` 改为 `grafana_panel_query`，`grafana_health` 改为 `grafana_status`。参数、输出、超时与审批行为均不变。

| 旧名 | 新名 |
| --- | --- |
| `grafana_query` | `grafana_panel_query` |
| `grafana_health` | `grafana_status` |

旧名仍以「只报错」的转发 stub 保留在工具列表中：调用 `grafana_query` 或 `grafana_health` 会立即失败并指名新工具，进行中的对话一步即可自愈。自定义 prompt 与保存的工作流仍建议改用新名；按代理配置的工具白名单需要换成新名。

**浏览器设置卡片需要 DSH 0.1.2 及以上。** 在更旧的宿主（0.1.0–0.1.1）上，卡片会显示明确的「宿主过旧」提示而非源站列表——这是版本门槛，不是配置丢失。宿主侧全部工具在这些版本上照常可用，插件本身也仍可安装在 `0.1.0-rc.6` 及以上。

### 0.17.x：设置页与配置迁移

支持槽位声明探测及 `settings.section` 的宿主，在设置页左侧提供独立 **Grafana** 导航项，进入后配置卡片默认展开。没有独立分区的宿主回退到插件区 Grafana tab 或旧版内嵌卡片；没有声明探测接口的旧宿主沿用原有兼容注册。入口选择按实际宿主能力，不仅凭版本号；支持探测的宿主始终只注册一个可用入口。此次入口调整不改变配置、源站 UID 或令牌存储。

**配置自动迁移。** 升级前 `settings.yaml.imported` 中的具名源站优先于旧单源凭证；否则旧单源配置会物化为名为 `default` 的源站（Base URL 与已存令牌不变）。导入文件位于 `$DSH_HOME/settings.yaml.imported`，未设置 `DSH_HOME` 时位于 `~/.dsh/settings.yaml.imported`。本次维护继续使用可选依赖 `js-yaml` v4，以保留旧 YAML 的锚点与 `<<` 合并语义；仅该依赖本身缺失时，才由内置解析器处理已知的基础 Grafana 段结构。

文件读取、YAML 解析或导入写入失败时，迁移停止并输出脱敏原因；不会再推断单源配置、修改凭证或改写导入文件。请修复配置或依赖后由维护者重载插件再试。迁移不会覆盖运行期间并发保存的配置。

## 配置

打开 **设置 → 左侧 Grafana**（位于内置插件之后），直接查看已展开的配置面板。如果宿主不支持独立分区，则使用 **设置 → 插件（或内置插件）→ Grafana**；仅支持旧槽位的宿主使用内嵌 Grafana 助手卡片。

Web 版与桌面版的 Grafana 配置页均默认显示简体中文，不随浏览器或系统语言自动切换。若在 DSH 语言设置中明确选择英文，配置页会使用英文；未选择、语言不支持或偏好读取失败时均使用简体中文。

> 提示：独立分区优先于插件 tab，插件 tab 优先于旧版内嵌卡片；兼容入口保留原来的默认收起行为。升级后由维护者重载对应 profile 并刷新页面，使新插件代码生效。是否支持开发目录监听/HMR 取决于实际宿主配置，不能仅凭源码已修改判断当前界面已更新。

需要配置：

- **Service Account Token**：例如 `glsa_...`。
- **Grafana URL**：例如 `https://grafana.example.com` 或 `https://example.com/grafana`。

Token 使用 DSH 仅允许 loopback same-origin 访问的特权凭证 RPC：仅写不读，保存后的值不会被读取或回显。URL 存储在 `grafana` settings namespace 的非 secret 字段中，因此可读回明文并在卡片中显示以便核对。界面支持替换和删除。

HTTP 与 HTTPS 开箱即用，内网未配置证书的环境可直接填写 `http://` 地址，无需额外设置。注意：HTTP 会明文传输服务账号令牌，不可信网络环境请务必使用 HTTPS。如需强制仅允许 HTTPS，可在插件配置中关闭：

```yaml
allowInsecureHttp: false
```

只读模式面向监控排障等不应修改大盘的角色：

```yaml
readOnly: true
```

启用后 `grafana_push` 与 `grafana_clone` 完全不注册——模型没有可调用的写入工具。设置卡片顶部提供**只读模式**开关（开 = 只读，关 = 读写），当前模式一目了然；切换只写插件级 `readOnly` 配置，不影响已配置的源站与默认源站。

切换到只读立即生效：即使写入工具在启动时已注册，审批门也会在运行时拒绝它们。从只读切回读写可能需要重启 DSH（或重新加载插件），取决于插件加载时的模式：以只读模式启动的插件从未注册写入工具，重启或重载之后才会恢复。

settings 中的 `baseUrl` 为权威来源；早期版本存在 `GRAFANA_BASE_URL` 凭证中的 URL 会在启动时自动迁移到 settings，之后凭证值仅作兜底。Token 凭证名默认为 `GRAFANA_TOKEN`，可通过 `tokenRef` 修改。

### 多个 Grafana 源站

设置卡片管理的是一个**具名 Grafana 源站列表**，而不再是单组 URL/令牌。每个源站包含：

- **源站名称**（必填、唯一）：中英文皆可。它就是调用工具时传入 `source` 参数用以指定目标源站的值。
- **UID**（只读）：自动生成、全球唯一，以淡色小字显示在名称正下方。它是内部稳定主键——改名不会改变 UID，也不会影响已存令牌，且用户无法编辑。
- **Grafana URL** 与 **Service Account Token**：每个源站各自独立。令牌以仅写不读的方式存入 DSH 凭证库。新源站使用 `GRAFANA_TOKEN_<uid>` 引用；轮换已保存的令牌时生成新引用。迁移保留原有引用，包括自定义引用。

点击**新增源站**创建，**移除源站**删除，**设为默认**选择省略 `source` 时使用哪一台。**保存当前源站**只保存该卡片，**保存全部源站**保存所有卡片；写入前校验名称和 URL。移除只删除当前 profile 的源站配置，不自动清除关联凭证。多个 profile 可能共用 DSH 凭证库；当前配置无法证明其他 profile 不再使用某个引用，因此插件暂停自动回收，也不提供删除重试入口。

保存前必须成功读取配置。读取失败或返回畸形数据时，卡片保持只读，并提供**重新读取**按钮。宿主提供配置版本号时，写入会携带该版本，防止陈旧页面覆盖其它页面的新修改。

令牌变更通过一次配置写入切换到新凭证引用。宿主明确拒绝时，旧配置继续生效；应答丢失时，不推断写入未生效。所有路径均保留已写入或可能写入的凭证，并提示保留原因；成功轮换后旧凭证也保留。成功保存的令牌草稿会清空，其它未保存的编辑予以保留。保留的凭证可能累积，移除源站不等于在 Grafana 撤销令牌；如需撤销访问，应在 Grafana 中撤销相应令牌。

令牌状态区分 `configured`（已配置）、`missing`（确认未配置）和 `unknown`（无法确认）。凭证接口缺失、读取失败或应答畸形时，设置页显示“状态读取失败”，可随时点击**重新读取**；`grafana_sources` 输出 `token=unknown`，不回显凭证值或 provider 错误。已配置只说明凭证存在，不代表 Grafana 鉴权有效。源站草稿是否曾保存由配置基线判断，不依赖令牌状态。

每个工具都接受可选的 `source` 参数（源站名称），省略则用默认源站。调用 `grafana_sources` 可列出已配置的名称、UID、URL、令牌是否已配以及哪一台是默认源站。写操作的审批文案首行始终标明目标源站名称与 URL，方便确认改的是哪一台；写入与该源站绑定：等待审批期间默认源站若被改掉，写入会被拒绝，而不是发到执行时随手解析到的那一台。读取—审批—写入整条链路还绑定源站的 URL 与凭证引用，改掉源站地址会使其改前取得的快照失效——`id`/`uid`/`version` 相同并不能证明两次响应来自同一实例。早期版本的单源配置会在启动时迁移为一个名为 `default` 的源站，并保留其原有的自定义 `tokenRef`。`allowInsecureHttp` 仍是全局设置，对所有源站生效。

### Grafana 权限

优先使用最小权限 RBAC，只授予目标大盘及文件夹所需范围：

- `dashboards:read`
- `dashboards:write`
- 目标文件夹的 `folders:read`
- `grafana_panel_query` 需要 `datasources:query` 以及对所查数据源的访问权限

不支持细粒度 RBAC 时才使用 Editor 角色，避免使用 Admin token。

各工具所需的具体权限：

| 工具 | Grafana 权限 |
| --- | --- |
| `grafana_get` | `dashboards:read` |
| `grafana_push` | `dashboards:read` + `dashboards:write` |
| `grafana_clone` | `dashboards:read` + `dashboards:write` |
| `grafana_panel_query` | `dashboards:read` + `datasources:query` |
| `grafana_datasources` | `datasources:read` |
| `grafana_metric` | `datasources:read` + `datasources:query` |
| `grafana_compare` | `datasources:read` + `datasources:query`（每台源站各自） |
| `grafana_trend` | `dashboards:read` + `datasources:query` |
| `grafana_alerts` | `alert.instances:read`；`definitions: true` 另需 `alert.provisioning:read`；`ruleStates: true` 另需规则读取权限（被拒时就地报出缺失的 scope） |
| `grafana_search` | `dashboards:read` |
| `grafana_status` | `dashboards:read`（见下注） |
| `grafana_sources` | 无（只读本机插件配置） |

以读为主的配置可用 Viewer 基础角色叠加 Grafana 固定的只读 Alerting 角色；只有跑 `grafana_push` / `grafana_clone` 的令牌才需要追加 `dashboards:write`（或 Editor 基础角色）。关于 `grafana_status`：`/api/health` 无需鉴权，故该工具改为调用 `GET /api/search` 验证凭证，并从 `/api/health` 的 `database` 字段读取实例健康状态（该接口没有 `status` 字段）。

## 能力覆盖

与单一用途的 Grafana 桥接插件相比，本插件的差异点：

- **多具名源站**（至多 50 个）：每个工具都接受可选的 `source` 参数，写入审批会标明目标实例，一个插件即可服务一整套 Grafana。
- **凭证不落入配置文档**：令牌以只写方式存进 DSH 凭证库，经特权回环 RPC 访问，绝不会被回读、展示或同步。
- **浏览器设置卡片**：源站的增删改与校验都在原生设置界面完成，无需手工编辑配置文件。

工具面覆盖从读到写的完整闭环：

| 能力 | 工具 |
| --- | --- |
| 读大盘（完整 JSON 或结构化摘要） | `grafana_get` |
| 改大盘 | `grafana_push` |
| 写 / 新建 | `grafana_push`、`grafana_clone` |
| 克隆大盘 | `grafana_clone` |
| 搜索大盘 | `grafana_search` |
| 面板实时值 | `grafana_panel_query` |
| 大盘序列趋势 | `grafana_trend` |
| 裸查询（PromQL / LogQL） | `grafana_metric` |
| 跨源站指标横向比较 | `grafana_compare` |
| 数据源发现 | `grafana_datasources` |
| 活跃告警与规则定义 | `grafana_alerts` |
| 源站与凭证健康 | `grafana_status`、`grafana_sources` |
| 多源站 | 所有工具经 `source` 参数 |

## 工具

每个工具都接受可选的 `source` 参数（已配置的源站名称）用以指定目标 Grafana 实例；省略则用默认源站。详见[多个 Grafana 源站](#多个-grafana-源站)。

| 工具 | 行为 |
| --- | --- |
| `grafana_get` | 获取完整大盘，并保存一个短期可信的版本和目录快照。传 `summary: true` 时改为返回结构化摘要（面板、查询、阈值、变量），不记录写快照，适合超大盘。 |
| `grafana_push` | 在审批、身份校验、版本校验和目录保持后写回最近读取的大盘。 |
| `grafana_clone` | 把大盘复制为全新大盘（全新 UID、版本 1），默认留在源文件夹，并返回新大盘完整地址。同样需要审批，继续写入前必须先调用 `grafana_get`。 |
| `grafana_panel_query` | 执行粘贴的大盘或面板视图 URL（`?viewPanel=` 限定单面板；沿用 URL 里的 `from`/`to` 时间范围）背后的面板数据源查询，返回有界的实时数据摘要。模板变量默认使用大盘保存状态，可通过 `variables` 参数覆盖——单值（`{"env":"prod"}`）、多值（`{"host":["www","m"]}`，按查询中使用的格式修饰符展开）或 adhoc 过滤（见[模板变量覆盖](#模板变量覆盖grafana_panel_query)）。adhoc 过滤按数据源类型翻译：Elasticsearch 拼进各 target 的 Lucene 查询串，Prometheus/Loki 向每个 vector/stream selector 注入 label matcher，SQL 数据源替换 `rawSql` 中的 `${__adhoc}` 占位符；其它数据源类型存在生效的 adhoc 时显式报错并列出支持矩阵。adhoc 覆盖为整体替换保存态，`[]` 表示清空，并按 target 的数据源 uid 逐个生效——绑定某个数据源的变量不会影响其它数据源。不支持的运算符/数据源组合显式报错，绝不静默忽略。仅支持 `query`/`custom`/`interval`/`adhoc`/`textbox`/`constant`/`datasource` 类型变量覆盖（datasource 型变量传 uid 字符串），不支持的类型会显式报错。Prometheus/Loki target 中裸多值变量渲染为 `(a|b)` 以便用于 `=~` matcher。旧格式 datasource 引用自动解析：纯字符串 uid 与 `{"uid":"$datasource"}` 型 datasource 变量引用经 `GET /api/datasources` 解析（保存值 `"default"` 映射到默认数据源）。服务端表达式（`__expr__`，如 `$A / 60`）原样透传；变量插值失败的面板只跳过不阻断整盘——全部跳过时列出每个面板的 id、标题与原因；批量请求失败时自动降级为逐面板查询（正常路径始终整选区单次批量 POST，保证 `$A` 式表达式引用不断链）。只读，不记录写快照。 |
| `grafana_datasources` | 列出源站上已配置的数据源（uid、插件类型、显示名、是否默认、访问模式、以及配置的 URL——`url="(empty)"` 的行通常意味着该数据源配置有误、一查即失败，请换用其它 uid）。可按精确插件类型或大小写不敏感的名称子串过滤。结果分页返回（默认每页 40 行；`limit` 调整页大小，`page` 翻页），末尾一行披露当前页、总数与如何继续。调用 `grafana_metric` 前先用它确认要查询的 uid 或名称。只读。 |
| `grafana_metric` | 无需大盘，直接对 Prometheus 或 Loki 数据源执行一条裸文本查询（PromQL 如 `up` 或 `rate(http_requests_total[5m])`，或 LogQL 流选择器），数据源按 uid 或精确显示名寻址。`mode: "instant"`（默认）在区间末端求值一次；`mode: "range"` 对序列采样并给出每条序列的统计、上升/下降/持平判定与火花线——首行披露实际采样步长（`step=`），每条序列披露实际返回点数（`points=`），回答的精度与覆盖范围可见（对 Loki 的 range 查询返回的是日志行而非数值采样点，故这类序列只报行数与末行；Loki 的 instant 模式只接受 metric 查询，日志流选择器需用 range）。其它插件类型与服务端表达式会被拒绝并指向 `grafana_panel_query`。只读，不记录写快照。 |
| `grafana_compare` | 在多台已配置 Grafana 源站（一次 2-10 台）上并发执行同一 PromQL 查询，按源站独立解析数据源（同名会分别解到各自的 UID），并返回紧凑的横向比较结果；任一源站失败/无数据/多 series 都不会拖累其它成功源站，所有错误均经脱敏截断。全部请求源站都成功且各自返回单个可比较标量时，尾部附数学 summary（最高/最低/均值/比率）；range 模式的「可比较标量」指带完整 first/last/min/max/avg 的序列，无时间轴的表格或日志结果只按 per-source 详情呈现并说明不可直接比较。只读，不记写快照也不进审批门。 |
| `grafana_trend` | 一次调用回答大盘的「在涨还是在跌」：把每个可见查询目标以较粗的区间查询重跑，每条序列给出实际返回点数、桶数、首/末/最小/最大/均值、方向判定与火花线；首行披露实际采样步长。表格型结果报告行数与统计并标 `trend=n/a`，不伪造方向。与 `grafana_panel_query` 共用同一套面板管线（变量、adhoc 过滤、旧格式数据源引用、逐面板降级）。时间范围最长 90 天。只读，不记录写快照。 |
| `grafana_alerts` | 从内置 Alertmanager 列出源站当前正在告警的条目（默认 `state=firing`；`"suppressed"` 表示被静默/抑制，`"all"` 两者都要）。上游的 `active` 状态（Alertmanager v2 的活动告警状态）统一报为 `state=firing`，正常告警不会被当成未知状态漏掉。可按文件夹、跨标签与注解的大小写不敏感子串、或大盘 URL/uid 过滤。活跃告警按 `limit` 参数封顶（默认 30、上限 100），丢弃的部分在末尾预算行披露。`definitions: true` 另发一次请求追加 provisioning 的规则定义（独立权限、独立故障隔离）。`ruleStates: true` 再追加每条规则的评估状态（`rule-state` 行：`inactive`、`pending`、`firing`、`recording`、`unknown`，来自 Prometheus 兼容规则接口）——`pending` 表示条件已满足但 `for` 时长未满，这是 Alertmanager 视角回答不了的；该段可用 `ruleState` 过滤。两个规则段共用 `rulesPage` 分页（每页 100 条），并披露总数与下一页参数。只读；告警文本是不可信数据。 |
| `grafana_search` | 按标题和精确标签搜索，最多返回 50 条。 |
| `grafana_status` | 检查 Grafana 连通性与 Service Account 凭证。 |
| `grafana_sources` | 列出已配置的 Grafana 源站：每个源站的名称、只读 UID、URL、令牌是否已配，以及哪一台是默认源站。只读，绝不返回令牌值。在向其它工具传 `source` 之前，可用它发现合法的源站名称。 |

### 跨源站比较示例（`grafana_compare`）

`grafana_compare` 在所列每台源站上对同一 PromQL 查询发请求，汇总成一张紧凑的横向比较视图——把原本三次工具调用 + 手动对比缩成一次调用。常见用法：

Region 对比——同一 P99 延迟在多个区域：

```json
{
  "sources": ["tokyo", "singapore", "us"],
  "datasource": "Prometheus",
  "query": "histogram_quantile(0.99, sum by (le) (rate(http_request_duration_seconds_bucket{service=\"payment\"}[5m])))"
}
```

环境对比——production 与 staging 之间的错误率：

```json
{
  "sources": ["prod", "staging"],
  "datasource": "Prometheus",
  "query": "sum(rate(http_requests_total{status=~\"5..\"}[5m])) / sum(rate(http_requests_total[5m]))"
}
```

集群对比——跨 Grafana 集群 A/B/C 的节点 CPU，带更宽的窗口：

```json
{
  "sources": ["cluster-a", "cluster-b", "cluster-c"],
  "datasource": "Prometheus",
  "query": "100 - (avg by (instance) (rate(node_cpu_seconds_total{mode=\"idle\"}[2m])) * 100)",
  "mode": "range",
  "from": "now-15m",
  "to": "now",
  "points": 60
}
```

全部请求源站都成功且各自返回单个可比较标量时，输出末尾会附数学 summary（最高、最低、均值、max/min 比率）；`mode: "range"` 下这要求每台源站的序列都带完整的 first/last/min/max/avg。一旦任一源站多 series、失败、无数据，或返回的形状在当前 mode 下不可比较（range 模式遇到无时间轴的表格/日志帧），summary 就不出，每个源站按各自的 per-series 摘要或清洗后的错误单独呈现，并说明不可直接比较的原因——高基数或部分失败的结果永远不会被当成直接标量比较读出来。

### 模板变量覆盖（`grafana_panel_query`）

`variables` 参数是一个以变量名为键的 JSON 对象。大盘中所有可覆盖变量（`query`/`custom`/`interval`/`adhoc`/`textbox`/`constant`/`datasource` 类型）均可覆盖；不支持的类型显式报错。

单值——替换该变量出现的所有位置（`$env`、`${env}`）：

```json
{ "env": "prod" }
```

多值——传数组，展开方式遵循查询里使用的 Grafana 格式修饰符，为多选变量编写的大盘无需改动即可工作：

```json
{ "host": ["www.example.com", "m.example.com"] }
```

**Prometheus / Loki target** 里无修饰符的裸多值引用（`$host`）渲染为 `(www.example.com|m.example.com)`——即 `=~` label matcher 里可用的交替形式，与 Grafana 自身渲染一致。值不做正则转义（Grafana 也不转义；双引号 PromQL 字符串里把 `.` 转义成 `\.` 是语法错误）。需要精确匹配时使用显式 `${host:regex}` 修饰符。

| 查询占位符 | 展开结果 |
| --- | --- |
| `$host` / `${host}` | `www.example.com,m.example.com`（CSV，Grafana 默认） |
| `${host:csv}` | `www.example.com,m.example.com` |
| `${host:doublequote}` | `"www.example.com","m.example.com"` |
| `${host:singlequote}` | `'www.example.com','m.example.com'` |
| `${host:json}` | `["www.example.com","m.example.com"]` |
| `${host:raw}` | `www.example.com,m.example.com` |
| `${host:pipe}` | `www.example.com\|m.example.com` |
| `${host:percent}` | 逐值 URL 编码后逗号连接（`www.example.com,m.example.com`；`["a b"]` → `a%20b`） |
| `${host:querystring}` | `host=www.example.com&host=m.example.com`（以变量名为键） |
| `${host:regex}` | `www\.example\.com\|m\.example\.com`（逐值正则转义后以 `\|` 连接） |
| `${host:lucene}` | 逐值 Lucene 转义后以空格连接 |
| `${host:sqlstring}` | `'www.example.com','m.example.com'`（值内单引号翻倍） |

无修饰符的单值变量展开为裸值（与 `String(value)` 逐字节一致）；修饰符对单值同样生效（`${host:json}` → `"www.example.com"`，`${host:pipe}` → `www.example.com`）。未知格式修饰符显式报错。内建变量（`$__interval`、`$__rate_interval`、`${__from:date}` 等）始终原样透传。

adhoc 过滤覆盖——整体替换大盘保存的 adhoc filters（`[]` 表示清空）：

```json
{
  "adhoc": [
    { "key": "host.keyword", "operator": "=", "value": "www.example.com" },
    { "key": "status", "operator": "!=", "value": "404" }
  ]
}
```

未绑定的 adhoc 条目对所有数据源生效；加 `"datasourceUid": "<uid>"` 可绑定到单个数据源。翻译方式取决于数据源类型：

| 数据源类型 | 翻译方式 | 支持的运算符 |
| --- | --- | --- |
| Elasticsearch | Lucene 条件拼进各 target 的查询串（`host.keyword:"www.example.com"`；面板自带查询串非空时括号包裹后以 `AND` 连接） | `=` `!=` 恒可；`>` `<` 仅数字；`=~` `!~` 映射为 Lucene 正则 `field:/pattern/`（模式内的 `/` 会转义；空模式报错） |
| Prometheus | label matcher 注入每个 vector selector（`host="www.example.com"`；裸 metric 名补上 `{...}`） | `=` `!=` `=~` `!~`；`>` `<` 报错 |
| Loki | matcher 注入 stream selector（`{app="api", host="www.example.com"}`）；pipeline 阶段不动 | `=` `!=` `=~` `!~`；`>` `<` 报错 |
| SQL（MySQL/Postgres/MSSQL/MariaDB/SQLite/ClickHouse） | `rawSql` 中的 `${__adhoc}` / `$__adhoc` 占位符替换为 WHERE 风格条件（`host = 'www.example.com'`；值做单引号转义） | `=` `!=` `>` `<`（数字）、`=~` `!~`（映射为 `LIKE`/`NOT LIKE`） |
| 其它类型 | 显式报错并列出支持类型 | — |

datasource 型变量——用数据源 uid 字符串覆盖：

```json
{ "datasource": "prom-prod" }
```

datasource 引用了该变量的面板（`{"type":"prometheus","uid":"$datasource"}`）会改查指定的 uid。传入非字符串值（数字、数组）会显式报错。

### 旧格式 datasource 引用（`grafana_panel_query`）

旧版大盘的 datasource 引用形状无法直接用于 `/api/ds/query`，`grafana_panel_query` 会透明解析：

| 面板 datasource 形状 | 解析方式 |
| --- | --- |
| 纯字符串 uid（Grafana 8 及更早） | 经 `GET /api/datasources` 查找，随查询发送解析出的 `{type, uid}` |
| `{"uid":"$datasource"}` / `{"type":"prometheus","uid":"$datasource"}`（datasource 型模板变量） | 用该变量保存的 `current` 值插值进 uid |
| 保存值为 `"default"` | 映射到服务端默认数据源（`"default"` 是 `/api/ds/query` 不接受的保留伪 uid） |
| 索引不可用（403）或 uid 未知 | 裸 `{uid}` 透传，由 Grafana 自行报错；若此时还有生效的 adhoc 过滤无法对未知类型翻译，则显式报错 |

数据源索引按需懒加载——仅当大盘确实存在需要解析的引用时才请求一次。当所有面板都被跳过时，报错会列出每个面板的 id、标题与跳过原因，而不是一句干巴巴的 "no executable query"。

真机验收矩阵（变量覆盖 × 运算符 × 数据源类型、旧格式大盘形状）记录于 [INTEGRATION.md](INTEGRATION.md)。

### 安全写回流程

1. 让 DSH 获取大盘 URL 或 UID。
2. 描述需要修改的内容。
3. 检查审批窗口中的大盘身份和变更摘要。
4. 批准或拒绝写回。
5. 刷新 Grafana；再次写入前重新获取大盘。

`grafana_push` 默认使用 `overwrite: false`。写回前会立即重新读取大盘并拒绝过期版本，同时自动保持当前目录。移动目录需要 `allowFolderMove: true`；强制覆盖需要 `forceOverwrite: true`，且仍会触发审批。

审批弹窗中展示的大盘身份（UID、标题、版本、所在文件夹）以及「快照于 X 分钟前获取」全部来自 `grafana_get` 保存的服务端可信快照，而不是模型提交的大盘 JSON，因此模型幻觉或被篡改的标题不会误导审批。没有可信快照时，弹窗会直接说明写回会被拒绝并要求先 `grafana_get`。审批弹出前插件还会实时核对 Grafana 端当前状态：版本或文件夹与快照不一致时，弹窗会给出醒目警示并列出两侧版本号；无法连接 Grafana 时，弹窗会注明「无法确认当前状态」。实时复核只用于丰富审批文案，写入前的最终校验始终在写回时重新执行。实时复核成功时，弹窗还会附上有界且经过清洗的内容 diff（新增/删除/修改的面板、模板变量与顶层字段），让审批人核对真实改动，而不只是模型自述的变更摘要。

## 安全与数据边界

- Token 不会进入工具参数、模型消息、日志或 Git。
- 带凭证的请求拒绝 HTTP 重定向，避免凭证被转发到其他来源。
- 非本机明文 HTTP 默认允许（可通过配置关闭）。
- 请求支持取消、超时、响应大小限制和 Dashboard 输入大小限制。
- API 错误只暴露有限的状态和消息。
- Grafana 返回内容始终作为不可信数据处理，而不是模型指令。

Dashboard JSON 仍可能包含 SQL、内部域名、链接、标签和业务信息。获取大盘会把这些 JSON 作为工具上下文发送给当前模型提供商。在处理机密大盘前，请先确认模型提供商的数据政策。

漏洞报告和支持策略参见 [SECURITY.md](./SECURITY.md)。

## 开发验证

```bash
npm ci
npm run verify
npm pack --dry-run --ignore-scripts
```

自动化测试使用 Node 内置测试运行器和模拟 Grafana 响应。CI 的 `0.2.0-rc.2` 基线覆盖 Node 20、22 和 24，另用 Node 22 验证 `dsh-tools@0.2.1-alpha.1`。CI 实际生成 `.tgz` 并扫描包内标识；这些检查不替代真实 DSH/Grafana 验收。

更多信息参见 [CONTRIBUTING.md](./CONTRIBUTING.md) 和 [CHANGELOG.md](./CHANGELOG.md)。

## 许可证

[MIT](./LICENSE)
