timarai-dashboard-mcp 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +105 -0
- package/README.md +266 -0
- package/dist/charts/build.js +130 -0
- package/dist/charts/build.js.map +1 -0
- package/dist/charts/format.js +101 -0
- package/dist/charts/format.js.map +1 -0
- package/dist/charts/specs.js +708 -0
- package/dist/charts/specs.js.map +1 -0
- package/dist/charts/svg.js +371 -0
- package/dist/charts/svg.js.map +1 -0
- package/dist/client.js +284 -0
- package/dist/client.js.map +1 -0
- package/dist/config.js +164 -0
- package/dist/config.js.map +1 -0
- package/dist/date-range.js +190 -0
- package/dist/date-range.js.map +1 -0
- package/dist/format.js +369 -0
- package/dist/format.js.map +1 -0
- package/dist/index.js +63 -0
- package/dist/index.js.map +1 -0
- package/dist/output.js +46 -0
- package/dist/output.js.map +1 -0
- package/dist/registry.js +231 -0
- package/dist/registry.js.map +1 -0
- package/dist/reports/html.js +84 -0
- package/dist/reports/html.js.map +1 -0
- package/dist/reports/templates.js +113 -0
- package/dist/reports/templates.js.map +1 -0
- package/dist/resources.js +265 -0
- package/dist/resources.js.map +1 -0
- package/dist/server.js +588 -0
- package/dist/server.js.map +1 -0
- package/dist/setup.js +344 -0
- package/dist/setup.js.map +1 -0
- package/dist/signature.js +35 -0
- package/dist/signature.js.map +1 -0
- package/docs/CAPABILITIES.md +275 -0
- package/docs/CONTRACT.md +170 -0
- package/docs/DESIGN.md +367 -0
- package/docs/DISTRIBUTION.md +214 -0
- package/docs/VERIFICATION.md +120 -0
- package/package.json +42 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# 变更记录
|
|
2
|
+
|
|
3
|
+
## v1.0.3 — 2026-09-21(去掉写死的默认值)
|
|
4
|
+
|
|
5
|
+
**三个必填项(`URL` / `ClientId` / `Secret`)一律声明为「无默认值」**,仓库里不再出现任何
|
|
6
|
+
真实环境域名或 clientId。预填值的害处是「看起来能跑」——照抄示例会连到错误的环境,
|
|
7
|
+
而错环境往往能通、只是数据不对,比直接报错更难发现。
|
|
8
|
+
|
|
9
|
+
| 位置 | 处理 |
|
|
10
|
+
|------|------|
|
|
11
|
+
| README / DISTRIBUTION / `.env.example` | 示例全部改占位符(`https://<你的接口域名>` / `<你的 clientId>`),并注明向平台管理员索取 |
|
|
12
|
+
| 运行期首配引导(`config.ts`) | 列出三个变量名的同时说明「无默认值」;npm 用户看不到仓库,这里是他唯一的指引 |
|
|
13
|
+
| `setup.ts` 向导 | 两项提示去掉「例:xxx」,改为「无默认值,问平台管理员要」 |
|
|
14
|
+
| `e2e.mjs` / `live-render.mjs` | 删掉 clientId 硬编码兜底,改为沿用 `.env` 或 `--base=`——跑的是哪个环境要一眼可见 |
|
|
15
|
+
| `VERIFICATION.md` | 去掉被测地址,说明地址与凭证不入库 |
|
|
16
|
+
|
|
17
|
+
验证:`tsc` 零错误 · smoke 46/46 · e2e **84/84**(去掉兜底后仍从 `.env` 正常取值)。
|
|
18
|
+
|
|
19
|
+
## v1.0.2 — 2026-09-21(npm 包体验)
|
|
20
|
+
|
|
21
|
+
对齐「和其它 MCP 一样」的使用方式:**`npm install -g` 装完,在客户端里填 URL + 凭证即可**。
|
|
22
|
+
|
|
23
|
+
| 改动 | 说明 |
|
|
24
|
+
|------|------|
|
|
25
|
+
| 安装方式收敛 | 使用者只需 `npm i -g timarai-dashboard-mcp`;源码安装降级为「改代码时」的路线 |
|
|
26
|
+
| 新增 `TIMARAI_PLATFORM_KEY`(**可选**) | `clientId:secret` 一个变量顶两个;不填也完全不影响,原三项必填保持不变 |
|
|
27
|
+
| 新增命令行参数 `--url=` / `--key=` / `--out=` | 少数客户端不传 `env` 时的兜底;命令行参数优先于环境变量 |
|
|
28
|
+
| 首配引导改写 | 直接列出三个必填变量名与示例,不再只指向 `npm run setup`(装包的人没有仓库可跑向导) |
|
|
29
|
+
| `docs/DISTRIBUTION.md` 增补维护者发布流程 | `npm login` → `publish --dry-run` → `publish` → 打 tag;含不公开的三种替代方案 |
|
|
30
|
+
|
|
31
|
+
守卫 smoke 41 → **46**:KEY 可拆分、分开写优先于 KEY、KEY 格式错时给可行动提示、
|
|
32
|
+
命令行参数能注入、首配引导列全三个变量名(均只测纯函数,绕过 `.env` 避免断言恒真)。
|
|
33
|
+
|
|
34
|
+
验证:`tsc` 零错误 · smoke 46/46。
|
|
35
|
+
|
|
36
|
+
## v1.0.1 — 2026-09-21(可分发)
|
|
37
|
+
|
|
38
|
+
让这套服务能从「本机跑得起来」变成「别的电脑、别的 Agent 客户端也能装上」。
|
|
39
|
+
|
|
40
|
+
| 改动 | 说明 |
|
|
41
|
+
|------|------|
|
|
42
|
+
| 去掉 `private`、加 `prepack` | 支持 `npm pack` / `npm publish`;打包前自动构建,避免把旧 `dist/` 打进去 |
|
|
43
|
+
| `dist/index.js` 补 shebang | 安装包后可直接用 `timarai-dashboard-mcp` 命令启动(原先只能 `node dist/index.js`) |
|
|
44
|
+
| 产物目录按 **进程工作目录** 解析 | 原先按包安装目录解析;全局安装时会把图/报表写进 `node_modules`,用户根本找不到。相对路径现在按 cwd 解析,**建议直接填绝对路径** |
|
|
45
|
+
| 新增 `MCP_HTTP_HOST` | HTTP 传输可监听非回环地址(默认仍 `127.0.0.1`);监听非回环时启动日志打印无鉴权告警 |
|
|
46
|
+
| 新增 `docs/DISTRIBUTION.md` | 三条分发路线(命令行包 / 远程 HTTP / 源码)、各客户端配置文件路径、凭证注入与排障 |
|
|
47
|
+
|
|
48
|
+
行为变化提示:**`TIMARAI_OUTPUT_DIR` 相对路径的落点变了**(仓库根 → 运行目录)。
|
|
49
|
+
源码方式在仓库根跑 `npm run setup` 时行为不变;用绝对路径则完全不受影响。
|
|
50
|
+
|
|
51
|
+
验证:`tsc` 零错误 · smoke 41/41 · 打包后实装验证(缺配置时正确打印引导并以退出码 2 结束)。
|
|
52
|
+
|
|
53
|
+
## v1.0.0 — 2026-09-21(第一版封板)
|
|
54
|
+
|
|
55
|
+
### 封板范围
|
|
56
|
+
|
|
57
|
+
驾驶舱 OpenAPI 的**只读 MCP 服务器**,STDIO 传输。
|
|
58
|
+
|
|
59
|
+
| 能力 | 数量 | 说明 |
|
|
60
|
+
|------|------|------|
|
|
61
|
+
| 语义化工具 | 6 | `dashboard_overview` / `dashboard_trend` / `dashboard_top_merchants` / `dashboard_platform_metrics` / `dashboard_filters` / `render_chart` / `build_report`(后两者另计) |
|
|
62
|
+
| `call_endpoint` 逃生阀 | 23 条路由 | 接口侧新增路由时无需等本仓库发版 |
|
|
63
|
+
| 预置图表 | 39 | 覆盖 CEO / 运营 / 财务三舱 |
|
|
64
|
+
| 报表模板 | 5 | 单文件可打印 HTML |
|
|
65
|
+
| Resource | 3 | `timarai://dashboard/{endpoints,charts,field-glossary,limits}` |
|
|
66
|
+
| Prompt | 3 | 无 argsSchema |
|
|
67
|
+
|
|
68
|
+
### 验证基线
|
|
69
|
+
|
|
70
|
+
`tsc` 零错误 · smoke **41/41** · e2e **84/84**(含 26 项接口侧能力守卫) ·
|
|
71
|
+
live-check **7/7** · 真实数据 **16 图 + 2 报表**。
|
|
72
|
+
|
|
73
|
+
守卫覆盖的全是「失效了也不报错」的能力,详见 [`docs/VERIFICATION.md`](docs/VERIFICATION.md)。
|
|
74
|
+
|
|
75
|
+
26 项守卫盯的都不是"能不能调通",而是**失效了也不报错**的那类能力:
|
|
76
|
+
HTTP 状态码是否真实、错误是否脱敏、翻页是否返回 `total`、缺失日是否标注、
|
|
77
|
+
缓存是否可见、金额是否按币种拆分、`share` 是否全局归一、
|
|
78
|
+
以及 3 项**双方已确认的语义**(`topN` 全局截断 / `isStale` 基准 / `metricType` 空白容错)。
|
|
79
|
+
|
|
80
|
+
### 与接口侧的契约状态:**0 条未解决**
|
|
81
|
+
|
|
82
|
+
九轮审计累计 23 条问题全部闭环。按性质分:
|
|
83
|
+
静默失败 6 · 值不自描述 9 · 缺元信息 5 · 真正的功能缺口 / 性能 3——
|
|
84
|
+
**87% 不是"功能没做",是"做了但没说清楚"**。
|
|
85
|
+
|
|
86
|
+
规范条款见 [`docs/CONTRACT.md`](docs/CONTRACT.md);问题跟踪与逐轮记录不入库,
|
|
87
|
+
见桌面《驾驶舱API前后端协作规范.md》与《驾驶舱API前后端群聊记录.md》。
|
|
88
|
+
|
|
89
|
+
### 已知边界(刻意不改,不是缺陷)
|
|
90
|
+
|
|
91
|
+
| 边界 | 现状 |
|
|
92
|
+
|------|------|
|
|
93
|
+
| `granularity` | 只放行 `day`。`hour` 被接口以 400 明确拒绝,实现后放开一行枚举即可 |
|
|
94
|
+
| `topN` | 按币种拆行**之后**的全局截断,不是「每币种各 N」。要每币种需接口新开参数(如 `perCurrencyTopN`) |
|
|
95
|
+
| 金额形态 | 现状 v1(拆行 + `currency`)。目标 v2(`money` 信封)接口侧已原则同意、单独排期;**存量不迁移**,仅作新增金额路由默认规范 |
|
|
96
|
+
| `isStale` | 基准是 `lastRefreshTime`。测试环境刷新任务不常驻,恒 `true` 是预期,不代表数据错 |
|
|
97
|
+
| `ceo/overview` 顶层 `currency=MIXED` | 是「已拆分」形态(金额标量 `null`、明细在 `amountsByCurrency`),**不是错值** |
|
|
98
|
+
|
|
99
|
+
### 上线前置动作(运营侧,非本仓库缺陷)
|
|
100
|
+
|
|
101
|
+
1. **轮换 test 凭证**——调试过程中明文出现在过运行日志,建议发布前轮换。
|
|
102
|
+
2. **开具门禁专用只读 key / secret**——23 条路由全只读,复用现有凭证即可;
|
|
103
|
+
单独一组更便于定位与轮换。不贴明文、不走群聊。
|
|
104
|
+
3. **确认接口侧发版门禁落地**——`npm run e2e -- --base=<预发环境>`,
|
|
105
|
+
对方回复"本周落地"。
|
package/README.md
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
# TimarAI-Dashboard-MCP
|
|
2
|
+
|
|
3
|
+
把 TimarAI 平台**数据驾驶舱**接进 AI Agent 的只读 MCP 服务器。
|
|
4
|
+
|
|
5
|
+
驾驶舱接口只负责给数据,本服务负责把它变成 Agent 真正能用的东西:**查数据、出图、出报表**。
|
|
6
|
+
Agent 接入后可以直接问「上周哪个商户增长最快」「财务周报给我一份」,自己完成取数→成图→结论的全过程。
|
|
7
|
+
|
|
8
|
+
**一句话边界**:只读消费方。不启动后端、不建库、不写数据——后端 API 怎么部署是平台侧的职责,
|
|
9
|
+
本仓库只关心「给我一个可达地址」。
|
|
10
|
+
|
|
11
|
+
> **版本 v1.0.0**(2026-09-21 封板):与接口侧的契约问题 **0 条未解决**,
|
|
12
|
+
> 验证基线 smoke 41/41 · e2e 84/84 · live-check 7/7。
|
|
13
|
+
> 已知边界与上线前置动作见 [`CHANGELOG.md`](CHANGELOG.md)。
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 能干什么
|
|
18
|
+
|
|
19
|
+
### 三层能力
|
|
20
|
+
|
|
21
|
+
驾驶舱接口返回的是 JSON;本服务把它变成 Agent 能直接交付的成品。
|
|
22
|
+
|
|
23
|
+
| 层 | 能力 | 工具 | 产物 |
|
|
24
|
+
|----|------|------|------|
|
|
25
|
+
| ① 查数据 | 三舱(CEO / 运营 / 财务)的结构化查询,中文 semantic 参数 | `dashboard_overview` `dashboard_trend` `dashboard_breakdown` `dashboard_top_merchants` `dashboard_platform_metrics` `dashboard_filters` `call_endpoint` | 文本 / JSON |
|
|
26
|
+
| ② 出图 | 39 张预置图表(折线 / 柱状 / 条形 / 环形 / 组合 / KPI 卡),口径与需求文档逐条对齐 | `render_chart` | 自包含 `.svg` 落盘 |
|
|
27
|
+
| ③ 出报表 | 5 套可打印 HTML 报表模板,图表内联,`Ctrl+P` 直接导出 PDF | `build_report` | 单文件 `.html` |
|
|
28
|
+
|
|
29
|
+
**给谁用**:
|
|
30
|
+
|
|
31
|
+
| 角色 | 关心 | 用哪套 |
|
|
32
|
+
|------|------|--------|
|
|
33
|
+
| 老板 / CEO | 生意怎么样、哪条线赚钱、大户集中度 | `ceo-briefing` |
|
|
34
|
+
| 运营负责人 | 成功率掉了是谁的锅、异常为什么多 | `ops-anomaly-triage` |
|
|
35
|
+
| 财务负责人 | 钱进出多少、结算跟上没、对账差在谁 | `finance-weekly-review` |
|
|
36
|
+
| 中台 / 全员 | 每天固定看一眼 | `daily-briefing` |
|
|
37
|
+
| 管理层会议 | 三舱全过一遍 | `cockpit-full`(32 图) |
|
|
38
|
+
|
|
39
|
+
### 覆盖度
|
|
40
|
+
|
|
41
|
+
- **23 条**只读驾驶舱路由全部注册
|
|
42
|
+
- **39 张**预置图,按《高管 / 运营 / 财务驾驶舱》需求文档对齐:三舱各 1 张 KPI 卡,
|
|
43
|
+
图 36 张(CEO 16 / 运营 10 / 财务 10),需求侧九个 KPI 全部有趋势可出
|
|
44
|
+
- **5 套**报表模板:`ceo-briefing` / `finance-weekly-review` / `ops-anomaly-triage` / `daily-briefing` / `cockpit-full`
|
|
45
|
+
- **19 项**运营平台宽表指标(`operation/platform-metrics`;需求文档写 21 项,实际以接口返回为准)
|
|
46
|
+
|
|
47
|
+
> **每个工具查什么、每张图回答什么问题、每套报表含哪些图** —— 见
|
|
48
|
+
> [`docs/CAPABILITIES.md`](docs/CAPABILITIES.md)。本文只做入口。
|
|
49
|
+
|
|
50
|
+
### Agent 怎么知道能用哪些
|
|
51
|
+
|
|
52
|
+
4 个 Resource 让 Agent **自主发现**能力边界,不用猜参数:
|
|
53
|
+
|
|
54
|
+
| Resource | 内容 |
|
|
55
|
+
|---|---|
|
|
56
|
+
| `timarai://dashboard/endpoints` | 23 条只读路由全表(含入参与示例) |
|
|
57
|
+
| `timarai://dashboard/charts` | 39 张预置图目录(id、口径、绑定路由) |
|
|
58
|
+
| `timarai://dashboard/field-glossary` | 字段口径字典(哪些 `value` 其实是笔数、哪些 `share` 是占比) |
|
|
59
|
+
| `timarai://dashboard/limits` | 使用纪律(90 天跨度、币种不可相加、时效性判断) |
|
|
60
|
+
|
|
61
|
+
3 个提示词开箱即用:`daily-briefing`、`ops-anomaly-triage`、`finance-weekly-review`。
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## 安装
|
|
66
|
+
|
|
67
|
+
### 方式一:npm 安装(使用者走这条)
|
|
68
|
+
|
|
69
|
+
前置:Node.js ≥ 20。
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npm install -g timarai-dashboard-mcp
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
装完就是一个命令 `timarai-dashboard-mcp`,**不需要 clone 仓库、不需要 build**。
|
|
76
|
+
接下来只要在 Agent 客户端里配上接口地址和凭证(见下方「接进 Agent」)。
|
|
77
|
+
|
|
78
|
+
### 方式二:源码安装(改代码时走这条)
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
git clone https://github.com/carl929362980/TimarAI-Dashboard-MCP.git
|
|
82
|
+
cd TimarAI-Dashboard-MCP
|
|
83
|
+
|
|
84
|
+
npm install
|
|
85
|
+
npm run build
|
|
86
|
+
npm run setup # 首次使用:向导会分步问你三项配置,写入 .env 并当场验证连通性
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### 首次配置的友好引导
|
|
90
|
+
|
|
91
|
+
`npm run setup` 会依次问你三件事,每一项都说明「是什么、去哪拿、填错会怎样」:
|
|
92
|
+
|
|
93
|
+
| 步骤 | 问什么 | 说明 |
|
|
94
|
+
|------|--------|------|
|
|
95
|
+
| ① | 接口地址 | 驾驶舱 OpenAPI 的根地址(不含 `/swagger`、不带路径)。**无默认值**,问平台管理员要 |
|
|
96
|
+
| ② | ClientId | 平台数据字典里为该项 OpenAPI 登记的客户端标识。**无默认值** |
|
|
97
|
+
| ③ | Secret | 与 ClientId 配对的明文密钥,输入不回显 |
|
|
98
|
+
|
|
99
|
+
之后是 8 项可选配置,全部给默认值,**一路回车即可**。
|
|
100
|
+
|
|
101
|
+
向导结束时会做的事:
|
|
102
|
+
|
|
103
|
+
1. 写入 `.env`(一次写入,中途 Ctrl+C 不会留下残缺文件);
|
|
104
|
+
2. **当场连一次真实接口**验证——地址不通、凭证不对,在这一步就告诉你原因,而不是等到 Agent 问数据时才失败;
|
|
105
|
+
3. 打印可直接粘贴进 Agent 客户端的 JSON 片段。
|
|
106
|
+
|
|
107
|
+
> 没配过就启动 Server 也不会默默失败:会先输出这段引导并指回 `npm run setup`。
|
|
108
|
+
> 手动配置则 `cp .env.example .env` 后填写。`.env` 已被 `.gitignore` 排除,不要提交。
|
|
109
|
+
|
|
110
|
+
## 配置
|
|
111
|
+
|
|
112
|
+
| 环境变量 | 必填 | 默认 | 说明 |
|
|
113
|
+
|----------|------|------|------|
|
|
114
|
+
| `TIMARAI_API_BASE_URL` | ✅ | 无 | 驾驶舱 API 地址(不含 `/swagger`)。**无默认值**,按环境由平台侧给出 |
|
|
115
|
+
| `TIMARAI_PLATFORM_CLIENT_ID` | ✅ | 无 | 数据字典里的 clientId。**无默认值** |
|
|
116
|
+
| `TIMARAI_PLATFORM_SECRET` | ✅ | 无 | 对应的明文密钥。**无默认值** |
|
|
117
|
+
| `TIMARAI_PLATFORM_KEY` | | 无 | **可选简写**:`clientId:secret`,一个变量顶上面两个 |
|
|
118
|
+
| `TIMARAI_OUTPUT_DIR` | | `out` | 图表与报表落盘目录(相对路径按**运行目录**解析,建议填绝对路径) |
|
|
119
|
+
| `TIMARAI_HTTP_TIMEOUT_MS` | | `15000` | 单次请求超时 |
|
|
120
|
+
| `TIMARAI_MAX_RETRIES` | | `1` | 5xx 退避重试次数 |
|
|
121
|
+
| `TIMARAI_MAX_DATE_SPAN_DAYS` | | `90` | 工具层前置校验的跨度上限 |
|
|
122
|
+
| `TIMARAI_ALLOW_RAW_ENDPOINT` | | `true` | 是否注册 `call_endpoint` 逃生阀 |
|
|
123
|
+
| `TIMARAI_FILTER_CACHE_TTL_MS` | | `300000` | 筛选项缓存时长(该数据变动极低频) |
|
|
124
|
+
| `MCP_TRANSPORT` | | `stdio` | `stdio` 或 `http` |
|
|
125
|
+
| `MCP_HTTP_HOST` | | `127.0.0.1` | 仅 http 模式;监听地址,改 `0.0.0.0` 才能被别的机器连(须前置反代鉴权) |
|
|
126
|
+
| `MCP_HTTP_PORT` | | `8899` | 仅 http 模式 |
|
|
127
|
+
|
|
128
|
+
凭证也可由宿主进程注入(见下方 `env` 字段),不必落在 `.env`。
|
|
129
|
+
|
|
130
|
+
验证:`npm run smoke`(离线 46 项)· `npm run live-check`(联网 7 项)· `npm run e2e`(协议层 84 项)。
|
|
131
|
+
命令矩阵见 [`docs/VERIFICATION.md`](docs/VERIFICATION.md)。
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## 分发:在别的电脑、别的 Agent 里用
|
|
136
|
+
|
|
137
|
+
三条路线,按需选(完整步骤、客户端配置文件路径、发布流程见 [`docs/DISTRIBUTION.md`](docs/DISTRIBUTION.md)):
|
|
138
|
+
|
|
139
|
+
| 路线 | 怎么做 | 适合 |
|
|
140
|
+
|------|--------|------|
|
|
141
|
+
| **命令行包** ⭐ | 目标机 `npm i -g timarai-dashboard-mcp` → 客户端填 `command` + 三个凭证变量 | 团队几台机器,各自本地跑、能出图落盘 |
|
|
142
|
+
| **远程 HTTP 共享** | 服务端 `MCP_TRANSPORT=http MCP_HTTP_HOST=0.0.0.0` 起一个,大家填 `url` | 人多、统一版本(⚠️ 需前置反代鉴权,产物落在服务端) |
|
|
143
|
+
| **源码安装** | `git clone` + `npm run build` + `npm run setup` | 开发者,要改代码 |
|
|
144
|
+
|
|
145
|
+
> 全局安装时**没有 `.env`**,凭证必须写在客户端配置的 `env` 字段里;
|
|
146
|
+
> `TIMARAI_OUTPUT_DIR` 建议填绝对路径,否则产物会落在 Agent 拉起进程时的当前目录。
|
|
147
|
+
|
|
148
|
+
## 调用
|
|
149
|
+
|
|
150
|
+
### 1. 接进 Agent(二选一)
|
|
151
|
+
|
|
152
|
+
**STDIO** —— 本地 Agent 逐进程拉起,默认推荐:
|
|
153
|
+
|
|
154
|
+
```json
|
|
155
|
+
{
|
|
156
|
+
"mcpServers": {
|
|
157
|
+
"timarai-dashboard": {
|
|
158
|
+
"command": "timarai-dashboard-mcp",
|
|
159
|
+
"args": [],
|
|
160
|
+
"env": {
|
|
161
|
+
"TIMARAI_API_BASE_URL": "https://<你的接口域名>",
|
|
162
|
+
"TIMARAI_PLATFORM_CLIENT_ID": "<你的 clientId>",
|
|
163
|
+
"TIMARAI_PLATFORM_SECRET": "<明文密钥>",
|
|
164
|
+
"TIMARAI_OUTPUT_DIR": "C:/Users/<你>/TimarAI-Out"
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
四个变量的说明:
|
|
172
|
+
|
|
173
|
+
| 变量 | 必填 | 说明 |
|
|
174
|
+
|------|------|------|
|
|
175
|
+
| `TIMARAI_API_BASE_URL` | ✅ | 接口地址。**无默认值**,由平台侧给出(各环境不同) |
|
|
176
|
+
| `TIMARAI_PLATFORM_CLIENT_ID` | ✅ | clientId。**无默认值**,平台数据字典里为该项 OpenAPI 登记的那个 |
|
|
177
|
+
| `TIMARAI_PLATFORM_SECRET` | ✅ | 配对的明文密钥。**无默认值** |
|
|
178
|
+
| `TIMARAI_OUTPUT_DIR` | 建议 | 图表/报表落盘目录,**填绝对路径**(相对路径按运行目录解析,Agent 拉起时 cwd 不固定) |
|
|
179
|
+
|
|
180
|
+
> 三个必填项**都没有内置默认值**——填错环境比不填更危险,所以示例里一律是占位符,
|
|
181
|
+
> 请向平台管理员索取你所在环境的地址与凭证。
|
|
182
|
+
|
|
183
|
+
> 凭证也可以合并成一个:`"TIMARAI_PLATFORM_KEY": "<你的 clientId>:<明文密钥>"`(英文冒号分隔)。
|
|
184
|
+
> 若客户端不传 `env`,可改写进 args:`"args": ["--url=https://<host>", "--key=<clientId>:<secret>"]`。
|
|
185
|
+
> Windows 路径用正斜杠 `/`,避免 JSON 转义问题。
|
|
186
|
+
> 源码安装时凭证也可放在仓库根目录 `.env`(npm 安装的包里没有 `.env`,配了不生效)。
|
|
187
|
+
|
|
188
|
+
**Streamable HTTP** —— 一个服务给多个使用者复用:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
MCP_TRANSPORT=http MCP_HTTP_PORT=8899 npm start
|
|
192
|
+
# [TimarAI-Dashboard-MCP] Streamable HTTP listening on http://127.0.0.1:8899/mcp
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
```json
|
|
196
|
+
{
|
|
197
|
+
"mcpServers": {
|
|
198
|
+
"timarai-dashboard": {
|
|
199
|
+
"url": "http://127.0.0.1:8899/mcp"
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
> HTTP 模式下凭证走服务进程自身的环境(`.env` 或系统环境变量),不随请求传递。
|
|
206
|
+
> 单响应 JSON,无 SSE 长连接。
|
|
207
|
+
|
|
208
|
+
### 2. 直接用
|
|
209
|
+
|
|
210
|
+
对 Agent 说自然语言即可,工具与参数是中文语义化的:
|
|
211
|
+
|
|
212
|
+
| 你说 | Agent 会调 | 拿到什么 |
|
|
213
|
+
|------|-----------|----------|
|
|
214
|
+
| 「上周 GMV 趋势怎么样」 | `dashboard_trend(subject="gmv")` | 按天 JSON 序列 |
|
|
215
|
+
| 「哪些商户 GMV 排前十」 | `dashboard_top_merchants(subject="gmv")` | 商户排行 |
|
|
216
|
+
| 「把上周经营出海报给我」 | `render_chart(chart="ceo.gmv_trend")` → `build_report(template="ceo-briefing")` | `out/*.svg`、`out/*.html` |
|
|
217
|
+
| 「财务周报整理一份」 | `build_report(template="finance-weekly-review")` | 可打印 HTML(Ctrl+P 出 PDF) |
|
|
218
|
+
|
|
219
|
+
工具面一览:
|
|
220
|
+
|
|
221
|
+
| 工具 | 覆盖 |
|
|
222
|
+
|------|------|
|
|
223
|
+
| `dashboard_filters` | 取合法筛选值,过滤前先调用 |
|
|
224
|
+
| `dashboard_overview` | 三舱汇总 KPI(`cabin`) |
|
|
225
|
+
| `dashboard_trend` | 7 个趋势路由(`subject`) |
|
|
226
|
+
| `dashboard_breakdown` | 7 个分解路由(`subject`,维度构成与占比) |
|
|
227
|
+
| `dashboard_top_merchants` | 4 个排行路由(`subject`) |
|
|
228
|
+
| `dashboard_platform_metrics` | 19 项运营宽表(商户生命周期 / VA 与 U 卡 / 渠道) |
|
|
229
|
+
| `call_endpoint` | 逃生阀,任意已注册路由;可 `TIMARAI_ALLOW_RAW_ENDPOINT=false` 关闭 |
|
|
230
|
+
| `render_chart` | 按 spec id 渲染单图(39 张预置)→ `.svg` |
|
|
231
|
+
| `build_report` | 按模板组装多图报告(5 套)→ `.html` |
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## 边界
|
|
236
|
+
|
|
237
|
+
- **只读**:不含任何写操作。统计产出的重建、封板与作业调度均不在能力范围内。
|
|
238
|
+
- **身份隔离**:只用平台 HMAC 凭证(`X-Platform-*`)。商户 `X-Api-Key` 调这些路由必然失败。
|
|
239
|
+
- **日期跨度 ≤ 90 天**,按报表时区 `Asia/Shanghai` 解释。
|
|
240
|
+
- 契约之外的能力一律「参数照发正确的」:接口侧能力一旦就位,本仓库零改动即生效。
|
|
241
|
+
|
|
242
|
+
## 排障
|
|
243
|
+
|
|
244
|
+
日志全部走 **stderr**(stdio 模式下 stdout 被协议占用),并自动抹除 Secret。
|
|
245
|
+
|
|
246
|
+
| 现象 | 定位 |
|
|
247
|
+
|------|------|
|
|
248
|
+
| `[401]` 签名不匹配 | Secret 错 / 时间戳不是毫秒 / rawBody 被改写 |
|
|
249
|
+
| `[400]` 含 `Timestamp` | 本地时钟漂移超 ±30 秒,开 NTP |
|
|
250
|
+
| `[403]` 无权限 | clientId 不存在、未启用或缺 `dashboard.read` |
|
|
251
|
+
| `[500] Service unavailable` | 服务端数据字典 `PlatformDashboardOpenApi` 未配置 |
|
|
252
|
+
|
|
253
|
+
每次调用都带 `X-Platform-Request-Id`,可与平台侧日志逐条对应。
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## 文档
|
|
258
|
+
|
|
259
|
+
| 文档 | 内容 |
|
|
260
|
+
|------|------|
|
|
261
|
+
| [`docs/CAPABILITIES.md`](docs/CAPABILITIES.md) | **能力详解**:23 条路由 / 39 张图 / 5 套报表逐条说明,角色与典型问题对照 |
|
|
262
|
+
| [`docs/DESIGN.md`](docs/DESIGN.md) | 架构设计:工具面取舍、签名方案、图表与报表口径 |
|
|
263
|
+
| [`docs/VERIFICATION.md`](docs/VERIFICATION.md) | 验证脚本矩阵(离线 / 需接口可达)、当前基线 |
|
|
264
|
+
| [`docs/DISTRIBUTION.md`](docs/DISTRIBUTION.md) | **分发与接入**:三条路线、各客户端配置文件路径、凭证注入与排障 |
|
|
265
|
+
| [`docs/CONTRACT.md`](docs/CONTRACT.md) | **契约规范**:接口侧与调用方共同遵守的 6 条条款(每条配判据)、金额形态统一路线 |
|
|
266
|
+
| [`CHANGELOG.md`](CHANGELOG.md) | 版本记录:封板范围、验证基线、已知边界、上线前置动作 |
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { getEndpointById } from "../registry.js";
|
|
2
|
+
import { previousPeriod, resolveDateRange } from "../date-range.js";
|
|
3
|
+
import { FILTER_CODE_LABELS } from "../format.js";
|
|
4
|
+
import { renderChart } from "./svg.js";
|
|
5
|
+
import { getChartSpec } from "./specs.js";
|
|
6
|
+
import { esc } from "./format.js";
|
|
7
|
+
function toNumber(value) {
|
|
8
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
9
|
+
return Number.isFinite(n) ? n : 0;
|
|
10
|
+
}
|
|
11
|
+
/** 把 meta 翻成人能读的一行:数据截止 / 是否封板 / 是否过期 / 被忽略的维度。 */
|
|
12
|
+
export function metaSummary(payload) {
|
|
13
|
+
const meta = (payload?.meta ?? {});
|
|
14
|
+
const raw = meta.dataAsOf;
|
|
15
|
+
let asOf = "无";
|
|
16
|
+
if (typeof raw === "string" && /^\d{4}-\d{2}-\d{2}T/.test(raw))
|
|
17
|
+
asOf = raw.slice(0, 16).replace("T", " ");
|
|
18
|
+
else if (typeof raw === "number" && raw > 0)
|
|
19
|
+
asOf = new Date(raw).toISOString().slice(0, 16).replace("T", " ");
|
|
20
|
+
const isFinal = meta.isFinal === true;
|
|
21
|
+
const isStale = meta.isStale === true;
|
|
22
|
+
const ignored = Array.isArray(meta.ignoredFilters) ? meta.ignoredFilters : [];
|
|
23
|
+
const ignoredText = ignored.length
|
|
24
|
+
? `不支持筛选:${ignored.map((c) => FILTER_CODE_LABELS[String(c)] ?? String(c)).join("、")}`
|
|
25
|
+
: "";
|
|
26
|
+
const line = [
|
|
27
|
+
`数据截止 ${asOf}`,
|
|
28
|
+
isFinal ? "已封板" : "未封板",
|
|
29
|
+
isStale ? "刷新滞后(非数据错)" : null,
|
|
30
|
+
ignoredText,
|
|
31
|
+
]
|
|
32
|
+
.filter(Boolean)
|
|
33
|
+
.join(" | ");
|
|
34
|
+
return { line, isFinal };
|
|
35
|
+
}
|
|
36
|
+
/** 数据摘要:给 Agent 回传紧凑的表格,供它写结论时引用。 */
|
|
37
|
+
function summarize(labels, series, limit = 12) {
|
|
38
|
+
if (labels.length === 0)
|
|
39
|
+
return "_(该区间无数据点)_";
|
|
40
|
+
const only = series.length === 1 ? series[0] : undefined;
|
|
41
|
+
if (only && labels.length > limit) {
|
|
42
|
+
const s = only;
|
|
43
|
+
const first = `${labels[0]}=${s.values[0]}`;
|
|
44
|
+
const last = `${labels[labels.length - 1]}=${s.values[s.values.length - 1]}`;
|
|
45
|
+
const max = Math.max(...s.values);
|
|
46
|
+
const min = Math.min(...s.values);
|
|
47
|
+
return `${s.name}:${first} … ${last} | 峰值 ${max} | 谷值 ${min} | 共 ${labels.length} 点`;
|
|
48
|
+
}
|
|
49
|
+
const rows = labels.slice(0, limit).map((label, i) => {
|
|
50
|
+
const cells = series.map((s) => `${s.name}=${s.values[i]}`);
|
|
51
|
+
return `${label}: ${cells.join(", ")}`;
|
|
52
|
+
});
|
|
53
|
+
return rows.join("\n");
|
|
54
|
+
}
|
|
55
|
+
export async function buildChart(client, cfg, chartId, args) {
|
|
56
|
+
const spec = getChartSpec(chartId);
|
|
57
|
+
if (!spec)
|
|
58
|
+
throw new Error(`未知图表:${chartId}`);
|
|
59
|
+
const endpoint = getEndpointById(spec.endpointId);
|
|
60
|
+
if (!endpoint)
|
|
61
|
+
throw new Error(`图表 ${chartId} 绑定的路由缺失:${spec.endpointId}`);
|
|
62
|
+
// 需求文档给每张图指定了默认窗口(趋势类多为近 30 天);调用方显式传 preset 时以其为准。
|
|
63
|
+
const range = resolveDateRange({
|
|
64
|
+
preset: (args.preset ?? spec.defaultPreset),
|
|
65
|
+
startDate: args.startDate,
|
|
66
|
+
endDate: args.endDate,
|
|
67
|
+
maxSpanDays: cfg.TIMARAI_MAX_DATE_SPAN_DAYS,
|
|
68
|
+
});
|
|
69
|
+
const body = { startDate: range.startDate, endDate: range.endDate, ...(spec.extraBody ?? {}) };
|
|
70
|
+
if (args.businessTypes?.length)
|
|
71
|
+
body.businessTypes = args.businessTypes;
|
|
72
|
+
if (args.merchantId)
|
|
73
|
+
body.merchantId = args.merchantId;
|
|
74
|
+
if (args.channelCodes?.length)
|
|
75
|
+
body.channelCodes = args.channelCodes;
|
|
76
|
+
if (args.currencies?.length)
|
|
77
|
+
body.currencies = args.currencies;
|
|
78
|
+
if (args.accountTypes?.length)
|
|
79
|
+
body.accountTypes = args.accountTypes;
|
|
80
|
+
const result = await client.call(endpoint, body);
|
|
81
|
+
const { labels, series } = spec.extract(result.data);
|
|
82
|
+
const { line, isFinal } = metaSummary(result.data);
|
|
83
|
+
// 环比:KPI 卡默认取上一等长区间,其他图默认不取(避免折线被两条线挤爆)。
|
|
84
|
+
let previous;
|
|
85
|
+
const wantCompare = spec.kind === "kpi" && args.compare !== false;
|
|
86
|
+
if (wantCompare) {
|
|
87
|
+
const prev = previousPeriod(range.startDate, range.endDate);
|
|
88
|
+
try {
|
|
89
|
+
const prevResult = await client.call(endpoint, { ...body, ...prev });
|
|
90
|
+
previous = spec.extract(prevResult.data).series[0]?.values;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
previous = undefined; // 上期取不到就算了,卡上显示「无上期对照」
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const chart = {
|
|
97
|
+
kind: spec.kind,
|
|
98
|
+
title: args.title ?? spec.title,
|
|
99
|
+
subtitle: `${range.startDate} ~ ${range.endDate}(${range.resolvedFrom})`,
|
|
100
|
+
labels,
|
|
101
|
+
series,
|
|
102
|
+
previous,
|
|
103
|
+
formats: spec.formats,
|
|
104
|
+
footnote: `${line} | 来源 ${endpoint.path}`,
|
|
105
|
+
};
|
|
106
|
+
return {
|
|
107
|
+
spec,
|
|
108
|
+
svg: renderChart(chart, args.width ?? 880),
|
|
109
|
+
meta: result.meta,
|
|
110
|
+
labels,
|
|
111
|
+
summary: summarize(labels, series),
|
|
112
|
+
metaLine: line,
|
|
113
|
+
isFinal,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/** 报告里给图表分区标题用。 */
|
|
117
|
+
export function cabinHeading(cabin) {
|
|
118
|
+
switch (cabin) {
|
|
119
|
+
case "ceo":
|
|
120
|
+
return "CEO 经营";
|
|
121
|
+
case "operation":
|
|
122
|
+
return "运营";
|
|
123
|
+
case "finance":
|
|
124
|
+
return "财务";
|
|
125
|
+
default:
|
|
126
|
+
return "公共";
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
export { esc };
|
|
130
|
+
//# sourceMappingURL=build.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build.js","sourceRoot":"","sources":["../../src/charts/build.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 图表数值与颜色格式化。
|
|
3
|
+
*
|
|
4
|
+
* 两条纪律:
|
|
5
|
+
* 1. **涨红跌绿**——中国业务看板惯例(与欧美相反)。上升用红、下降用绿。
|
|
6
|
+
* 对「异常订单数上升」这类坏消息,红色同时充当警示色,语义仍然自洽。
|
|
7
|
+
* 2. 中文金额用「万 / 亿」缩写,不要直接甩一长串数字给读者。
|
|
8
|
+
*/
|
|
9
|
+
export const PALETTE = ["#378ADD", "#1D9E75", "#BA7517", "#7F77DD", "#D4537E", "#D85A30"];
|
|
10
|
+
export const INK = {
|
|
11
|
+
title: "#2C2C2A",
|
|
12
|
+
text: "#444441",
|
|
13
|
+
muted: "#5F5E5A",
|
|
14
|
+
axis: "#B4B2A9",
|
|
15
|
+
grid: "#EFEDE6",
|
|
16
|
+
cardBg: "#FFFFFF",
|
|
17
|
+
cardBorder: "#E6E4DC",
|
|
18
|
+
up: "#E24B4A",
|
|
19
|
+
down: "#639922",
|
|
20
|
+
flat: "#888780",
|
|
21
|
+
};
|
|
22
|
+
export function esc(value) {
|
|
23
|
+
return value
|
|
24
|
+
.replace(/&/g, "&")
|
|
25
|
+
.replace(/</g, "<")
|
|
26
|
+
.replace(/>/g, ">")
|
|
27
|
+
.replace(/"/g, """);
|
|
28
|
+
}
|
|
29
|
+
/** 中文习惯的大数缩写:亿 / 万。 */
|
|
30
|
+
export function compact(value, digits = 2) {
|
|
31
|
+
const abs = Math.abs(value);
|
|
32
|
+
if (abs >= 1e8)
|
|
33
|
+
return `${trim(value / 1e8, digits)}亿`;
|
|
34
|
+
if (abs >= 1e4)
|
|
35
|
+
return `${trim(value / 1e4, digits)}万`;
|
|
36
|
+
return trim(value, digits);
|
|
37
|
+
}
|
|
38
|
+
function trim(value, digits) {
|
|
39
|
+
if (digits <= 0)
|
|
40
|
+
return value.toFixed(0);
|
|
41
|
+
// 只削减小数部分末尾的 0。
|
|
42
|
+
// 旧写法 /\.?0+$/ 会连整数位的 0 一起吃掉:trim(20,0) 得到 "2",
|
|
43
|
+
// 于是成功率图的 Y 轴把 20% 印成 2%、100% 印成 1%——图是对的,轴是错的,而且没人会怀疑轴。
|
|
44
|
+
return value
|
|
45
|
+
.toFixed(digits)
|
|
46
|
+
.replace(/(\.\d*?)0+$/, "$1")
|
|
47
|
+
.replace(/\.$/, "");
|
|
48
|
+
}
|
|
49
|
+
export function fmtInt(value) {
|
|
50
|
+
return Math.round(value).toLocaleString("zh-CN");
|
|
51
|
+
}
|
|
52
|
+
export function fmtMoney(value) {
|
|
53
|
+
return `¥${fmtInt(value)}`;
|
|
54
|
+
}
|
|
55
|
+
/** 接口返回的成功率是小数(0.9832),展示成 98.32%。 */
|
|
56
|
+
export function fmtPercent(value, digits = 2) {
|
|
57
|
+
return `${(value * 100).toFixed(digits)}%`;
|
|
58
|
+
}
|
|
59
|
+
export function fmtSeconds(value) {
|
|
60
|
+
if (value < 60)
|
|
61
|
+
return `${trim(value, 1)}秒`;
|
|
62
|
+
const m = Math.floor(value / 60);
|
|
63
|
+
const s = Math.round(value % 60);
|
|
64
|
+
return s === 0 ? `${m}分` : `${m}分${s}秒`;
|
|
65
|
+
}
|
|
66
|
+
export function formatValue(value, format = "number") {
|
|
67
|
+
if (!Number.isFinite(value))
|
|
68
|
+
return "-";
|
|
69
|
+
switch (format) {
|
|
70
|
+
case "money":
|
|
71
|
+
return fmtMoney(value);
|
|
72
|
+
case "percent":
|
|
73
|
+
return fmtPercent(value);
|
|
74
|
+
case "seconds":
|
|
75
|
+
return fmtSeconds(value);
|
|
76
|
+
default:
|
|
77
|
+
return fmtInt(value);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** 坐标轴刻度用缩写,避免长数字撑爆画布。 */
|
|
81
|
+
export function formatTick(value, format) {
|
|
82
|
+
if (format === "percent")
|
|
83
|
+
return `${trim(value * 100, 0)}%`;
|
|
84
|
+
return compact(value);
|
|
85
|
+
}
|
|
86
|
+
/** 环比:涨红跌绿持平灰。上期为 0 时不计算(除零无意义)。 */
|
|
87
|
+
export function delta(current, previous) {
|
|
88
|
+
if (previous === undefined || !Number.isFinite(previous) || previous === 0)
|
|
89
|
+
return null;
|
|
90
|
+
const change = (current - previous) / Math.abs(previous);
|
|
91
|
+
if (!Number.isFinite(change))
|
|
92
|
+
return null;
|
|
93
|
+
if (Math.abs(change) < 0.00005)
|
|
94
|
+
return { text: "持平", color: INK.flat };
|
|
95
|
+
const arrow = change > 0 ? "▲" : "▼";
|
|
96
|
+
return {
|
|
97
|
+
text: `${arrow} ${Math.abs(change * 100).toFixed(1)}%`,
|
|
98
|
+
color: change > 0 ? INK.up : INK.down,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=format.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format.js","sourceRoot":"","sources":["../../src/charts/format.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,CAAU,CAAC;AAEnG,MAAM,CAAC,MAAM,GAAG,GAAG;IACjB,KAAK,EAAE,SAAS;IAChB,IAAI,EAAE,SAAS;IACf,KAAK,EAAE,SAAS;IAChB,IAAI,EAAE,SAAS;IACf,IAAI,EAAE,SAAS;IACf,MAAM,EAAE,SAAS;IACjB,UAAU,EAAE,SAAS;IACrB,EAAE,EAAE,SAAS;IACb,IAAI,EAAE,SAAS;IACf,IAAI,EAAE,SAAS;CACP,CAAC;AAEX,MAAM,UAAU,GAAG,CAAC,KAAa;IAC/B,OAAO,KAAK;SACT,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;AAC7B,CAAC;AAED,uBAAuB;AACvB,MAAM,UAAU,OAAO,CAAC,KAAa,EAAE,MAAM,GAAG,CAAC;IAC/C,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAC5B,IAAI,GAAG,IAAI,GAAG;QAAE,OAAO,GAAG,IAAI,CAAC,KAAK,GAAG,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC;IACvD,IAAI,GAAG,IAAI,GAAG;QAAE,OAAO,GAAG,IAAI,CAAC,KAAK,GAAG,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC;IACvD,OAAO,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;AAC7B,CAAC;AAED,SAAS,IAAI,CAAC,KAAa,EAAE,MAAc;IACzC,IAAI,MAAM,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IACzC,gBAAgB;IAChB,gDAAgD;IAChD,yDAAyD;IACzD,OAAO,KAAK;SACT,OAAO,CAAC,MAAM,CAAC;SACf,OAAO,CAAC,aAAa,EAAE,IAAI,CAAC;SAC5B,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;AACxB,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,KAAa;IAClC,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC;AACnD,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,KAAa;IACpC,OAAO,IAAI,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;AAC7B,CAAC;AAED,sCAAsC;AACtC,MAAM,UAAU,UAAU,CAAC,KAAa,EAAE,MAAM,GAAG,CAAC;IAClD,OAAO,GAAG,CAAC,KAAK,GAAG,GAAG,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC;AAC7C,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,KAAa;IACtC,IAAI,KAAK,GAAG,EAAE;QAAE,OAAO,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,GAAG,CAAC;IAC5C,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,EAAE,CAAC,CAAC;IACjC,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,EAAE,CAAC,CAAC;IACjC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC;AAC1C,CAAC;AAID,MAAM,UAAU,WAAW,CAAC,KAAa,EAAE,SAAsB,QAAQ;IACvE,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,GAAG,CAAC;IACxC,QAAQ,MAAM,EAAE,CAAC;QACf,KAAK,OAAO;YACV,OAAO,QAAQ,CAAC,KAAK,CAAC,CAAC;QACzB,KAAK,SAAS;YACZ,OAAO,UAAU,CAAC,KAAK,CAAC,CAAC;QAC3B,KAAK,SAAS;YACZ,OAAO,UAAU,CAAC,KAAK,CAAC,CAAC;QAC3B;YACE,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;IACzB,CAAC;AACH,CAAC;AAED,0BAA0B;AAC1B,MAAM,UAAU,UAAU,CAAC,KAAa,EAAE,MAAmB;IAC3D,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,GAAG,IAAI,CAAC,KAAK,GAAG,GAAG,EAAE,CAAC,CAAC,GAAG,CAAC;IAC5D,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC;AACxB,CAAC;AAOD,oCAAoC;AACpC,MAAM,UAAU,KAAK,CAAC,OAAe,EAAE,QAA4B;IACjE,IAAI,QAAQ,KAAK,SAAS,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,QAAQ,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACxF,MAAM,MAAM,GAAG,CAAC,OAAO,GAAG,QAAQ,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACzD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1C,IAAI,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,OAAO;QAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,CAAC,IAAI,EAAE,CAAC;IACvE,MAAM,KAAK,GAAG,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC;IACrC,OAAO;QACL,IAAI,EAAE,GAAG,KAAK,IAAI,IAAI,CAAC,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG;QACtD,KAAK,EAAE,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI;KACtC,CAAC;AACJ,CAAC"}
|