@fx-data/ai-diagnostics 0.1.20261111112203 → 0.1.20261112024042
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/AGENT_SETUP.md +1 -1
- package/README.md +1 -1
- package/content/knowledge/dashboard/topics/calculation-panorama.md +14 -14
- package/content/scripts/assets/calculation-panorama.html +260 -80
- package/content/scripts/render_calculation_panorama.py +75 -0
- package/content-manifest.json +5 -5
- package/package.json +1 -1
package/AGENT_SETUP.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# FX 数据诊断:Agent 安装与管理
|
|
2
2
|
|
|
3
|
-
本文供执行安装与管理的 Agent 读取,不作为日常诊断的前置上下文。包版本为 `0.1.
|
|
3
|
+
本文供执行安装与管理的 Agent 读取,不作为日常诊断的前置上下文。包版本为 `0.1.20261112024042`,需要 Node.js `>=22` 及 npm。用户只需表达安装、使用、更新或卸载意图,环境准备和客户端登记由 Agent 完成。
|
|
4
4
|
|
|
5
5
|
用户明确要求安装、更新、回退、卸载或检查 FX 数据诊断时,也会触发薄 Skill,并直接执行对应管理命令,不先运行 `context` 或读取诊断规则;只有开始诊断时才按薄 Skill 加载内容。普通聊天不主动加载。
|
|
6
6
|
|
package/README.md
CHANGED
|
@@ -40,4 +40,4 @@
|
|
|
40
40
|
|
|
41
41
|
---
|
|
42
42
|
|
|
43
|
-
**给 Agent:** 按 [Agent 执行说明](https://unpkg.com/@fx-data/ai-diagnostics@0.1.
|
|
43
|
+
**给 Agent:** 按 [Agent 执行说明](https://unpkg.com/@fx-data/ai-diagnostics@0.1.20261112024042/AGENT_SETUP.md) 完成安装与管理。已有本地包时,优先读取包内 `AGENT_SETUP.md`;本地 `.tgz` 只需读取 `package/AGENT_SETUP.md`。按命令结果简短反馈,成功即结束;只有失败或结果不完整时才排错。
|
|
@@ -9,18 +9,18 @@
|
|
|
9
9
|
支持快照的入口在本次输入查询的 `calc.build_query` 阶段写出正文(历史名为 `filter`);V2 查询也使用该阶段名,但不能据此认定已采集计算快照,须检查实际属性。入口保留检索 ID、结果和请求统计,名称、筛选正文与参数作用位置在快照中只保存一份;ID 在引用和检索位置出现不等于重复正文。两种入口的控件摘要统一为 `controls.*`,候选值输出来源另存 `control.option_source.bindings`,它不能代替实际依赖。历史绑定摘要只用于旧记录,不要求新版同时提供。
|
|
10
10
|
|
|
11
11
|
- `calculation.panorama.status=CAPTURED` 表示快照已生成;同一 span 的 `part_count`、`part_0...part_N`、`length`(UTF-16 长度)和 `sha256` 用于核对完整性。`OVERSIZE`、`FAILED`、缺块、长度或摘要不符均不算完整;不能混用不同 span 的分块。
|
|
12
|
-
- `tables`
|
|
13
|
-
- `conditions`
|
|
14
|
-
- 条件的 `fieldTransforms`
|
|
15
|
-
- `tables[].fields`
|
|
16
|
-
- 条件 `parameterIds` 是原始筛选翻译时确认的参数引用,`valueState=PARAMETER` 表示整个比较值就是参数,可直接显示“金额 ≥ 参数「最低金额」”。组合条件自身的参数引用只说明整组涉及该参数,不能分配给未知的子条件。前后 N 项也遵守相同脱敏规则,展示为“金额 取前 参数「展示数量」 项”或“金额 取后 [值已脱敏] 项”。其他结构不补写未知公式。`dynamicDefinitions` 给出命中的动态项名称和默认字段,节点 `dynamicFields`
|
|
17
|
-
- 节点参数 `id` 对应快照 `parameters`
|
|
18
|
-
- `tables[].definition`
|
|
19
|
-
- `scope=QUERY_BUILT`
|
|
20
|
-
- `componentStages`
|
|
21
|
-
-
|
|
22
|
-
|
|
23
|
-
|
|
12
|
+
- `tables` 是本次涉及的业务表。`dependencies` 记录本次查询构建时观察到的来源到使用者关系,包含直接读取的来源,不额外查询祖先血缘。合并等步骤的内联分支折成业务表之间的直接连线,共享来源保留每个使用者。`nodes` 与 `conditionEdges` 保留本次条件对象及传播关系,节点 `id` 区分同表不同实例,`tableId` 对应表。判断是否重算时,以实际扫描和读取方式为准。
|
|
13
|
+
- `conditions` 集中记录表上的筛选正文,节点的 `fieldFilters` 和 `linkageFilters` 通过 ID 引用。节点另有 `parameters` 和 `dynamicFields`。筛选保留字段、运算符与 AND/OR/NOT 组合。`valueState` 的 `REDACTED`、`UNAVAILABLE`、`TRUNCATED` 分别表示脱敏、未取得、截断。
|
|
14
|
+
- 条件的 `fieldTransforms` 保留已知的字段变换,如按年、按月和求和。HTML 把变换写在字段旁,遇到 `UNAVAILABLE`、`TRUNCATED` 时标明细节未记录或未采集完。历史快照缺少该字段时,沿用记录中的条件描述。
|
|
15
|
+
- `tables[].fields` 按表保存本次条件与动态选择引用字段的显示名。先去掉已确认的所属表前缀,再按本表字典解析,不能用别表同名字段补名。`componentName` 是本次请求携带的组件名称。名称为空时可单独标明结果表名称。
|
|
16
|
+
- 条件 `parameterIds` 是原始筛选翻译时确认的参数引用,`valueState=PARAMETER` 表示整个比较值就是参数,可直接显示“金额 ≥ 参数「最低金额」”。组合条件自身的参数引用只说明整组涉及该参数,不能分配给未知的子条件。前后 N 项也遵守相同脱敏规则,展示为“金额 取前 参数「展示数量」 项”或“金额 取后 [值已脱敏] 项”。其他结构不补写未知公式。`dynamicDefinitions` 给出命中的动态项名称和默认字段,节点 `dynamicFields` 给出本次选择。两者以快照内短引用关联,历史快照可能还包含步骤 ID。`controls` 集中保存实际使用的来源名称、原请求 ID 和类别,条件通过短引用定位来源,区分普通控件、行权限、组件联动、值联动、参数控件和动态字段控件。`conditionControls` 通过条件 ID 说明来源;参数及动态字段覆盖项的 `controlIds` 说明由哪个控件改变,同请求键的不同来源不能借用名称。历史快照没有覆盖项来源时不补猜;权限条件用“行权限”说明,业务值继续脱敏。
|
|
17
|
+
- 节点参数 `id` 对应快照 `parameters` 名称表。保留分配给该表的覆盖输入和该表筛选引用的参数。`sources=TABLE` 表示分配给表,`COMPONENT` 表示组件带入,`UNKNOWN` 表示分配来源未确认。结合条件引用与读取方式判断参数的用途。
|
|
18
|
+
- `tables[].definition` 保存本次读取方式。`READ_MATERIALIZED` 表示直接读取已有结果,`EXPANDED` 表示重新展开,`REUSED_INPUT` 表示复用已有查询输入。复用输入的内部构建详情不可得时用 `REUSED_INPUT_DETAILS_UNAVAILABLE:<表 ID>` 标明缺口。同表的其他读取方式保存在 `variants`。读取方式缺失时保持未知。
|
|
19
|
+
- `scope=QUERY_BUILT` 表示查询构建成功,不表示引擎已执行成功。`BUILD_FAILED` 保留失败前确认的事实,图标明未完成。重复构建或失败后重试时,保留预编译输入的参数和控件关联。实际依赖和读取方式来自本轮,参数字典只覆盖保留的条件引用与输入覆盖。
|
|
20
|
+
- 旧 v2 快照可能有 `componentDefinition`、`componentStages` 和 `componentFilters`,分别描述组件追加步骤、请求配置和组件阶段条件。新版快照省去组件自己的筛选、表内原有筛选及仅由它们引用的参数。新版 `componentFilters` 和表定义里的 `filters` 是供旧 v2 读取器识别的空数组。
|
|
21
|
+
- 名称优先复用计算已经加载的元数据,结束时批量补齐缺名项。`TABLE_NAMES_MISSING` / `PARAMETER_NAMES_MISSING` 表示名称尚未补齐。图随 `gaps`、`omittedNodes`、`omittedEdges` 和条件截断标注缺口。表与条件对象各最多 512,关系、筛选内容及覆盖项有采集预算,达到上限标记 `*_LIMIT`,计算继续执行。正文最多 16 块,每块不超过 8000 个 UTF-16 单元,过大记 `OVERSIZE`。
|
|
22
|
+
|
|
23
|
+
早期 `calculation-panorama-v2` 可能含 `INLINE_QUERY` 节点和表内筛选。生成图时按已记录的关系折叠内联分支,省去表内筛选及其独有参数。历史 `calculation-panorama-v1` 的依赖来自逻辑血缘,`INPUT` 是根表输入配置,`PUBLISHED` 仅为发布配置对照,不能当作本次实际展开。更旧记录仍可使用已有[条件树](filter-tree.md#筛选树还原与交付),新观测不能补回历史缺失的事实。已有性能聚合指标保留原用途,不能代替业务关系。
|
|
24
24
|
|
|
25
25
|
## 计算关系图
|
|
26
26
|
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
名称缺失时用 ID,关系不全时展示能证实的局部范围并标注缺口;生成图不需要先确认唯一原因。完全没有可证实的关系或文件生成受阻时,说明具体限制,不编图或声称已经生成。
|
|
30
30
|
|
|
31
31
|
- **范围与关系**:注明业务对象、执行和来源,画全所选范围内已知的节点与边,方向为“上游来源 → 下游使用者 → 当前结果”。同表的不同条件对象保持可区分,共享上游不等于重复执行;旧树的身份、计数与环路按[筛选树](filter-tree.md#选定结构与完整性)核对。条件展开树不冒充完整产品血缘。
|
|
32
|
-
-
|
|
33
|
-
-
|
|
32
|
+
- **图形主视图**:业务表由左到右排列。同一张表画一次,箭头指向每个使用者。只给已确认展开的表加标记,悬停可查看已记录的读取方式。大图可缩放、滚动或查找。
|
|
33
|
+
- **逐表条件**:卡片展示直接作用于该表的字段筛选、联动和动态字段。表级参数覆盖在确认展开时单独标出,参数确实被表上的筛选引用时可连同来源写入该筛选。触发本表展开的覆盖,以及有实际依赖和条件传播指向下游展开的筛选,使用展开原因标记。内部编号用于连接记录,卡片只显示业务含义。同表不同条件对象分组保留,条件作用位置按[名称、绑定与展示](filter-tree.md#名称绑定与展示)核对。
|
|
34
34
|
- **缺失与推测**:随图展示采集缺口、截断和未采集范围,不补造关系。可疑路径或优化建议与已确认关系分开,并注明依据与业务前提;不要求每张图都附原因判断或优化方案。
|
|
35
35
|
|
|
36
36
|
HTML 应能独立打开,数据、样式和脚本内嵌;保存到本次临时产物或客户端交付目录,按[诊断材料边界](../../../AGENTS.md#3-诊断原则)脱敏,不把真实业务材料写入仓库,日志与参数按文本安全转义。生成方法见[计算全景查询与生成](../queries.md#计算全景)。
|
|
@@ -2,20 +2,124 @@
|
|
|
2
2
|
<html lang="zh-CN">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
5
|
-
<title
|
|
5
|
+
<title>计算关系图</title>
|
|
6
6
|
<style>
|
|
7
|
-
:root
|
|
7
|
+
:root {
|
|
8
|
+
color-scheme: light;
|
|
9
|
+
font: 14px/1.5 Inter, system-ui, "Microsoft YaHei", sans-serif;
|
|
10
|
+
color: #1e293b;
|
|
11
|
+
background: #eef1f6;
|
|
12
|
+
--border: #e1e7ef;
|
|
13
|
+
--muted: #607087;
|
|
14
|
+
}
|
|
15
|
+
* { box-sizing: border-box; }
|
|
16
|
+
body { margin: 0; padding: 18px; height: 100vh; height: 100dvh; }
|
|
17
|
+
.workspace {
|
|
18
|
+
display: flex; flex-direction: column; height: 100%; overflow: hidden;
|
|
19
|
+
background: #fff; border: 1px solid #dbe2ed; border-radius: 15px; box-shadow: 0 6px 26px #25375812;
|
|
20
|
+
}
|
|
21
|
+
.masthead {
|
|
22
|
+
display: flex; align-items: center; justify-content: space-between; gap: 10px 20px; flex-wrap: wrap;
|
|
23
|
+
padding: 12px 17px; border-bottom: 1px solid var(--border); flex-shrink: 0;
|
|
24
|
+
}
|
|
25
|
+
.head-title { display: flex; align-items: center; gap: 9px; min-width: 0; }
|
|
26
|
+
h1 { margin: 0; color: #23334a; font-size: 16px; font-weight: 720; }
|
|
27
|
+
.head-mark { color: #127e74; font-size: 19px; }
|
|
28
|
+
.head-meta { min-width: 0; }
|
|
29
|
+
.subtitle { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: min(49vw, 620px); }
|
|
30
|
+
.subtitle, .graph-counts, #match, #zoom { color: var(--muted); font-size: 12px; }
|
|
31
|
+
.graph-counts, #zoom { font-variant-numeric: tabular-nums; }
|
|
32
|
+
.toolbar, .zoom-tools { display: flex; flex-wrap: wrap; align-items: center; gap: 5px; }
|
|
33
|
+
button, input { font: inherit; }
|
|
34
|
+
button, .toolbar input {
|
|
35
|
+
color: #31445d; background: #fff; border: 1px solid var(--border); border-radius: 7px; font-size: 12px; min-height: 32px;
|
|
36
|
+
}
|
|
37
|
+
button { padding: 4px 10px; cursor: pointer; }
|
|
38
|
+
button:hover { border-color: #bdc9dd; background: #f5f7fc; }
|
|
39
|
+
button:focus-visible, input:focus-visible, summary:focus-visible, .node:focus-visible { outline: 2px solid #7061df; outline-offset: 2px; }
|
|
40
|
+
.toolbar input { width: 181px; max-width: 100%; padding: 5px 9px; }
|
|
41
|
+
#zoom { min-width: 34px; text-align: center; }
|
|
42
|
+
#viewport {
|
|
43
|
+
overflow: auto; flex: 1; min-height: 0; position: relative;
|
|
44
|
+
background-color: #f7f8fb;
|
|
45
|
+
background-image: linear-gradient(to right, #e9edf3 1px, transparent 1px), linear-gradient(to bottom, #e9edf3 1px, transparent 1px);
|
|
46
|
+
background-size: 23px 23px;
|
|
47
|
+
}
|
|
48
|
+
#space, #canvas { position: relative; }
|
|
49
|
+
#space { overflow: hidden; }
|
|
50
|
+
#canvas { transform-origin: 0 0; }
|
|
51
|
+
svg { position: absolute; inset: 0; overflow: visible; pointer-events: none; }
|
|
52
|
+
.edge { fill: none; stroke: #a5b2c5; stroke-width: 1.35; }
|
|
53
|
+
.edge.active { stroke: #6d60df; stroke-width: 2.25; }
|
|
54
|
+
.edge.dim { opacity: .19; }
|
|
55
|
+
.node {
|
|
56
|
+
position: absolute; top: 0; left: 0; width: 252px; padding: 11px 12px;
|
|
57
|
+
background: #fff; border: 1px solid #dfe6f0; border-radius: 10px; box-shadow: 0 2px 5px #263b5510;
|
|
58
|
+
overflow-wrap: anywhere; cursor: pointer;
|
|
59
|
+
}
|
|
60
|
+
.node.expanding { border-left: 2px solid #7463ec; }
|
|
61
|
+
.node.has-cause { border-top-color: #b9d9d1; }
|
|
62
|
+
.node.expanding.has-cause { border-left-color: #208779; }
|
|
63
|
+
.node.dim { opacity: .35; }
|
|
64
|
+
.node.selected { outline: 2px solid #7463ec; outline-offset: 3px; }
|
|
65
|
+
.node-top { display: flex; align-items: flex-start; gap: 7px; }
|
|
66
|
+
.node-icon {
|
|
67
|
+
flex: 0 0 22px; height: 22px; text-align: center; line-height: 20px;
|
|
68
|
+
color: #687e9a; background: #f5f7fb; border: 1px solid #e5eaf1; border-radius: 5px; font-size: 13px;
|
|
69
|
+
}
|
|
70
|
+
.expanding .node-icon { color: #6d58cc; background: #f3f0ff; border-color: #e9e1fd; }
|
|
71
|
+
.node h2 { flex: 1; min-width: 0; margin: 1px 0 0; color: #1e293b; font-size: 13px; line-height: 1.43; }
|
|
72
|
+
.tag {
|
|
73
|
+
display: inline-block; width: fit-content; max-width: 100%; padding: 0 5px; border-radius: 4px;
|
|
74
|
+
color: var(--tone); background: var(--tint); border: 1px solid var(--stroke); font-size: 10px; font-weight: 680;
|
|
75
|
+
}
|
|
76
|
+
.expanded { --tone: #5e46b2; --tint: #f2edff; --stroke: #ded3fa; white-space: nowrap; }
|
|
77
|
+
.filter { --tone: #225b93; --tint: #f0f6fd; --stroke: #c9dcef; }
|
|
78
|
+
.parameter { --tone: #603b95; --tint: #f5f0fc; --stroke: #e1d3f2; }
|
|
79
|
+
.dynamic { --tone: #13675d; --tint: #edf8f5; --stroke: #c3e4da; }
|
|
80
|
+
.linkage { --tone: #893c58; --tint: #fbf0f4; --stroke: #eac9d6; }
|
|
81
|
+
.condition { margin: 8px 0 0; padding: 7px 8px; border-radius: 6px; background: var(--tint); font-size: 11px; }
|
|
82
|
+
.condition-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 6px; color: var(--tone); }
|
|
83
|
+
.condition-name { font-weight: 650; color: #26384e; }
|
|
84
|
+
.reason { color: var(--tone); border: 1px solid var(--stroke); border-radius: 4px; padding: 0 4px; font-size: 10px; white-space: nowrap; }
|
|
85
|
+
.condition-change { margin-top: 3px; color: #26384e; white-space: pre-line; }
|
|
86
|
+
.origin { display: flex; align-items: baseline; gap: 6px; margin-top: 3px; color: var(--muted); }
|
|
87
|
+
.origin-label { flex-shrink: 0; font-weight: 650; }
|
|
88
|
+
.param-ref { color: #603b95; font-weight: 680; }
|
|
89
|
+
.case-label { color: var(--muted); font-size: 11px; margin: 7px 0 0; }
|
|
90
|
+
.case+.case { border-top: 1px solid var(--border); margin-top: 8px; padding-top: 3px; }
|
|
91
|
+
.case-label { font-weight: 700; }
|
|
92
|
+
#notes summary { width: fit-content; cursor: pointer; }
|
|
93
|
+
#notes { padding: 6px 17px; border-top: 1px solid var(--border); font-size: 12px; color: var(--muted); max-height: 35vh; overflow: auto; flex-shrink: 0; }
|
|
94
|
+
#notes p { margin: 7px 0; white-space: pre-wrap; }
|
|
95
|
+
.warning { color: #8c480e; font-weight: 650; }
|
|
96
|
+
@media (max-width: 650px) {
|
|
97
|
+
body { padding: 6px; }
|
|
98
|
+
.masthead { padding: 9px 10px; }
|
|
99
|
+
.toolbar input { flex: 1; min-width: 122px; }
|
|
100
|
+
.subtitle { max-width: 77vw; }
|
|
101
|
+
}
|
|
8
102
|
</style>
|
|
9
103
|
</head>
|
|
10
104
|
<body>
|
|
11
|
-
<
|
|
12
|
-
<
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
</div>
|
|
17
|
-
<
|
|
18
|
-
<
|
|
105
|
+
<div class="workspace">
|
|
106
|
+
<header class="masthead">
|
|
107
|
+
<div class="head-title">
|
|
108
|
+
<span class="head-mark" aria-hidden="true">▦</span>
|
|
109
|
+
<div class="head-meta"><h1>计算关系图</h1><div id="subtitle" class="subtitle"></div></div>
|
|
110
|
+
</div>
|
|
111
|
+
<div class="toolbar">
|
|
112
|
+
<span id="graph-counts" class="graph-counts"></span>
|
|
113
|
+
<input id="search" type="search" aria-label="查找表或条件来源" placeholder="查找表或条件来源"><span id="match" aria-live="polite"></span>
|
|
114
|
+
<div class="zoom-tools">
|
|
115
|
+
<button id="minus" aria-label="缩小">−</button><span id="zoom">100%</span><button id="plus" aria-label="放大">+</button>
|
|
116
|
+
</div>
|
|
117
|
+
<button id="fit">查看全图</button><button id="reset">原始大小</button>
|
|
118
|
+
</div>
|
|
119
|
+
</header>
|
|
120
|
+
<main id="viewport"><div id="space"><div id="canvas" aria-label="计算依赖全景"></div></div></main>
|
|
121
|
+
<details id="notes"><summary id="notes-title">计算信息与采集说明</summary><p id="identity"></p><p id="summary"></p><p id="coverage"></p><p id="evidence"></p></details>
|
|
122
|
+
</div>
|
|
19
123
|
<script type="application/json" id="panorama-data">__PANORAMA_DATA__</script>
|
|
20
124
|
<script>
|
|
21
125
|
'use strict';
|
|
@@ -24,10 +128,11 @@ const $=id=>document.getElementById(id);
|
|
|
24
128
|
const el=(tag,text,cls)=>{const n=document.createElement(tag);if(text!==undefined)n.textContent=text;if(cls)n.className=cls;return n};
|
|
25
129
|
const actual=data.schema==='calculation-panorama-v2',legacy=data.schema==='legacy-condition-tree';
|
|
26
130
|
const tables=new Map(data.tables.map(t=>[t.id,t]));
|
|
27
|
-
const parameterNames=new Map((data.parameters||[]).map(p=>[p.id,p.name
|
|
131
|
+
const parameterNames=new Map((data.parameters||[]).map(p=>[p.id,p.name]));
|
|
132
|
+
const parameterLabel=id=>'参数「'+(parameterNames.get(id)||'名称未记录')+'」';
|
|
28
133
|
const controls=new Map((data.controls||[]).map(c=>[c.id,c]));
|
|
29
134
|
const dynamicDefinitions=new Map((data.dynamicDefinitions||[]).map(d=>[JSON.stringify([d.tableId,d.operatorId,d.itemId]),d]));
|
|
30
|
-
const tableName=id=>tables.get(id)?.
|
|
135
|
+
const tableName=id=>tables.get(id)?.name||id;
|
|
31
136
|
const limited=!!(data.omittedNodes||data.omittedEdges||(data.gaps||[]).some(g=>g.includes('_LIMIT')));
|
|
32
137
|
const operators={In:'属于',NotIn:'不属于',Between:'介于',NotBetween:'不在区间',Null:'为空',NotNull:'不为空',LinkageIn:'属于联动选中的记录',SubQueryIn:'属于子查询结果',NumberLarge:'>',NumberSmall:'<',NumberEqual:'=',NumberNoEqual:'≠',NumberLargeOrEqual:'≥',NumberSmallOrEqual:'≤',StringBelong:'属于',StringNoBelong:'不属于',StringContain:'包含',StringNoContain:'不包含'};
|
|
33
138
|
const requestOperators={Large:'>',Small:'<',Equal:'=',NoEqual:'≠',LargeOrEqual:'≥',SmallOrEqual:'≤',Belong:'属于',NotBelong:'不属于',NoBelong:'不属于',Contain:'包含',NoContain:'不包含',After:'晚于',Before:'早于',Between:'介于',NoBetween:'不在区间',Null:'为空',NotNull:'不为空',TopN:'前 N 项',BottomN:'后 N 项',Formula:'公式筛选'};
|
|
@@ -45,77 +150,128 @@ function conditionField(c,tableId){
|
|
|
45
150
|
}
|
|
46
151
|
function conditionText(c,tableId){
|
|
47
152
|
if(c.operator==='LinkageIn')return '联动选中的记录';
|
|
48
|
-
const parameters=(c.parameterIds||[]).map(
|
|
153
|
+
const parameters=(c.parameterIds||[]).map(parameterLabel).join('、');
|
|
49
154
|
if(c.kind==='COMBINED')return '('+(c.children||[]).map(child=>conditionText(child,tableId)).join(c.operator==='OR'?' 或 ':' 且 ')+')'+(parameters?'(涉及'+parameters+')':'')+(c.valueState==='TRUNCATED'?'(条件未采集完)':'');
|
|
50
155
|
const requestType=c.kind==='REQUEST'?(c.operator||'').replace(/^(ComposeDate|Date|String|Number|Json)/,''):null;
|
|
51
156
|
const value=requestType==='Null'||requestType==='NotNull'?'':c.valueState==='PARAMETER'?parameters:c.valueState==='REDACTED'?'[值已脱敏]':c.valueState==='UNAVAILABLE'?'[内容未记录]':c.valueState==='TRUNCATED'?'[条件未采集完]':'';
|
|
52
157
|
if(c.operator==='TOP'||c.operator==='BOTTOM'||requestType==='TopN'||requestType==='BottomN')return [conditionField(c,tableId),c.operator==='TOP'||requestType==='TopN'?'取前':'取后',value||'N','项'].filter(Boolean).join(' ');
|
|
53
158
|
return [conditionField(c,tableId),operators[c.operator]||(requestType?requestOperators[requestType]||'筛选类型未展开':c.operator)||(c.kind==='UNKNOWN'?'条件内容未记录':c.kind),value].filter(Boolean).join(' ')+(parameters&&c.valueState!=='PARAMETER'?'(涉及'+parameters+')':'');
|
|
54
159
|
}
|
|
55
|
-
function controlNames(ids){return [...new Set((ids||[]).map(id=>controls.get(id)).filter(Boolean).map(source=>{
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
function
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
160
|
+
function controlNames(ids){return [...new Set((ids||[]).map(id=>controls.get(id)).filter(Boolean).map(source=>{
|
|
161
|
+
const kinds={CONTROL:'筛选器',COMPONENT_LINKAGE:'组件联动',VALUE_LINKAGE:'值联动',PARAMETER:'参数控件',DYNAMIC_FIELD:'动态字段控件'};
|
|
162
|
+
return source.kind==='ROW_PERMISSION'?'行权限':(kinds[source.kind]||'筛选器')+(source.name?'「'+source.name+'」':'(名称未记录)');
|
|
163
|
+
}))]}
|
|
164
|
+
function dynamicSelection(d,tableId){
|
|
165
|
+
const definition=dynamicDefinitions.get(JSON.stringify([tableId,d.operatorId,d.itemId]));
|
|
166
|
+
const name=definition?.name||d.name||d.itemName||'名称未记录';
|
|
167
|
+
const selection=(definition?.defaultFieldId?fieldName(definition.defaultFieldId,tableId)+' → ':'')+fieldName(d.fieldName||d.fieldId,tableId);
|
|
168
|
+
return {name,selection};
|
|
169
|
+
}
|
|
170
|
+
function knownCount(input,key,oldKey){return Array.isArray(input[key])?input[key].length:input.counts?.[oldKey]===undefined?null:Number(input.counts[oldKey])}
|
|
171
|
+
const filterKeys=[['fieldFilters','字段筛选','field','filter'],['linkageFilters','联动筛选','linkage','linkage']];
|
|
172
|
+
const readLabels={READ_MATERIALIZED:'直读',EXPANDED:'展开',REUSED_INPUT:'复用输入'};
|
|
173
|
+
const nodes=data.tables.map(t=>{
|
|
174
|
+
const modes=[...new Set([t.definition,...(t.variants||[])].filter(Boolean).map(item=>item.state))];
|
|
175
|
+
return {id:t.id,cases:data.nodes.filter(n=>n.tableId===t.id),modes,expanded:actual&&modes.includes('EXPANDED')};
|
|
176
|
+
});
|
|
177
|
+
const conditionNodes=new Map(data.nodes.map(n=>[n.id,n]));
|
|
178
|
+
const edges=legacy?[...new Map(data.conditionEdges.map(e=>{
|
|
179
|
+
const a=conditionNodes.get(e.source),b=conditionNodes.get(e.target);
|
|
180
|
+
return [a.tableId+'\0'+b.tableId,{source:a.tableId,target:b.tableId}];
|
|
181
|
+
})).values()]:data.dependencies;
|
|
64
182
|
const byId=new Map(nodes.map(n=>[n.id,n]));
|
|
65
183
|
const incoming=new Map(nodes.map(n=>[n.id,[]])),outgoing=new Map(nodes.map(n=>[n.id,[]]));
|
|
66
184
|
for(const e of edges){incoming.get(e.target).push(e.source);outgoing.get(e.source).push(e.target)}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
185
|
+
const filtersUsedByExpanded=new Set();
|
|
186
|
+
if(actual)for(const edge of data.conditionEdges){
|
|
187
|
+
const source=conditionNodes.get(edge.source),target=conditionNodes.get(edge.target);
|
|
188
|
+
if(byId.get(target.tableId).expanded&&incoming.get(target.tableId).includes(source.tableId))filtersUsedByExpanded.add(source.id);
|
|
189
|
+
}
|
|
190
|
+
function conditionParameters(condition,ids=new Set()){
|
|
191
|
+
for(const id of condition.parameterIds||[])ids.add(id);
|
|
192
|
+
for(const child of condition.children||[])conditionParameters(child,ids);
|
|
193
|
+
return ids;
|
|
194
|
+
}
|
|
195
|
+
function tableConditions(n){
|
|
196
|
+
const triggersExpansion=use=>n.expanded&&use.sources?.includes('TABLE');
|
|
197
|
+
function describeFilter(condition,label,kind,parameters,reason){
|
|
198
|
+
const controlIds=[...(data.conditionControls?.[condition._traceConditionId]||[])];
|
|
199
|
+
const referenced=conditionParameters(condition);
|
|
200
|
+
for(const id of referenced){
|
|
201
|
+
const uses=(parameters||[]).filter(p=>p.id===id);
|
|
202
|
+
if(uses.length!==1||triggersExpansion(uses[0]))continue;
|
|
203
|
+
controlIds.push(...(uses[0].controlIds||[]));
|
|
204
|
+
}
|
|
205
|
+
return {label,kind,change:conditionText(condition,n.id),sources:controlNames(controlIds),reason,showSource:referenced.size>0};
|
|
75
206
|
}
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
207
|
+
return n.cases.map(input=>{
|
|
208
|
+
const entries=(input.dynamicFields||[]).map(d=>{
|
|
209
|
+
const {name,selection}=dynamicSelection(d,n.id);
|
|
210
|
+
return {kind:'dynamic',label:'动态字段',name,change:selection,sources:controlNames(d.controlIds),
|
|
211
|
+
reason:n.expanded?'触发展开':'',showSource:true};
|
|
212
|
+
});
|
|
213
|
+
const reason=filtersUsedByExpanded.has(input.id)?'影响下游展开':'';
|
|
214
|
+
for(const [key,label,oldKey,kind] of filterKeys){
|
|
215
|
+
for(const condition of input[key]||[]){
|
|
216
|
+
entries.push(describeFilter(condition,label,kind,input.parameters,reason));
|
|
217
|
+
}
|
|
218
|
+
if(!Array.isArray(input[key])){
|
|
219
|
+
const count=knownCount(input,key,oldKey);
|
|
220
|
+
if(count>0)entries.push({label,kind,change:count+' 项,具体内容未记录'});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
for(const p of input.parameters||[]){
|
|
224
|
+
if(!triggersExpansion(p))continue;
|
|
225
|
+
entries.push({kind:'parameter',label:'参数覆盖',change:parameterLabel(p.id),sources:controlNames(p.controlIds),
|
|
226
|
+
reason:'触发展开',showSource:true});
|
|
82
227
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
228
|
+
const count=knownCount(input,'dynamicFields','dynamic');
|
|
229
|
+
if(!Array.isArray(input.dynamicFields)&&count>0)entries.push({label:'动态字段',kind:'dynamic',change:count+' 项,具体内容未记录'});
|
|
230
|
+
return entries;
|
|
231
|
+
}).filter(entries=>entries.length);
|
|
232
|
+
}
|
|
233
|
+
function conditionCard(entry){
|
|
234
|
+
const block=el('section',undefined,'condition '+entry.kind),head=el('div',undefined,'condition-head');
|
|
235
|
+
head.append(el('strong',entry.label));
|
|
236
|
+
if(entry.name)head.append(el('span',entry.name,'condition-name'));
|
|
237
|
+
if(entry.reason)head.append(el('span',entry.reason,'reason'));
|
|
238
|
+
const change=el('div',undefined,'condition-change');
|
|
239
|
+
for(const [i,part] of entry.change.split(/(参数「[^」]+」)/g).entries()){
|
|
240
|
+
change.append(i%2?el('span',part,'param-ref'):document.createTextNode(part));
|
|
241
|
+
}
|
|
242
|
+
block.append(head,change);
|
|
243
|
+
if(entry.sources?.length||entry.showSource||entry.reason){
|
|
244
|
+
const origin=el('div',undefined,'origin');
|
|
245
|
+
origin.append(el('span','来源','origin-label'),el('span',entry.sources?.join('、')||'未记录'));
|
|
246
|
+
block.append(origin);
|
|
94
247
|
}
|
|
95
|
-
|
|
96
|
-
if(n.id===data.rootTableId)for(const stage of data.componentStages||[])if(stage.conditions?.length)row(card,(stage.source==='REQUEST'?'组件请求':'组件')+(stage.stage==='DETAIL'?'明细筛选':'结果筛选'),stage.conditions.map(c=>conditionDescription(c,n.id)).join(';'));
|
|
97
|
-
if(!legacy&&!card.querySelector('.condition'))card.append(el('p',actual?(definitions.some(d=>d.state==='REUSED_INPUT')?'本次复用已有输入,来源筛选未记录':'本次记录中没有附加条件'):'条件未记录','muted'));
|
|
98
|
-
if(legacy&&n.cases.every(c=>filterKeys.every(([key,label,oldKey])=>knownCount(c,key,oldKey)===0)&&knownCount(c,'parameters','parameter')===0&&knownCount(c,'dynamicFields','dynamic')===0))card.append(el('p','本次记录的附加条件计数为 0','muted'));
|
|
248
|
+
return block;
|
|
99
249
|
}
|
|
100
250
|
const canvas=$('canvas'),ns='http://www.w3.org/2000/svg';
|
|
101
251
|
const svg=document.createElementNS(ns,'svg');svg.setAttribute('aria-label','上游到下游依赖连线');
|
|
102
252
|
svg.innerHTML='<defs><marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0 0 L10 5 L0 10Z" fill="#8c9fb9"/></marker></defs>';canvas.append(svg);
|
|
103
253
|
for(const n of nodes){
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if(n.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
254
|
+
const groups=tableConditions(n);
|
|
255
|
+
const hasCause=groups.some(group=>group.some(entry=>entry.reason));
|
|
256
|
+
const card=el('article',undefined,'node'+(n.expanded?' expanding':'')+(hasCause?' has-cause':''));n.card=card;card.dataset.id=n.id;
|
|
257
|
+
const name=tableName(n.id),mode=n.modes.map(read=>readLabels[read]).filter(Boolean).join('、');
|
|
258
|
+
card.tabIndex=0;card.setAttribute('aria-label',mode?name+',读取方式 '+mode:name);
|
|
259
|
+
card.title=mode?name+'\n读取方式 '+mode:name;
|
|
260
|
+
const top=el('div',undefined,'node-top'),icon=el('span','▦','node-icon');
|
|
261
|
+
icon.setAttribute('aria-hidden','true');top.append(icon,el('h2',name));
|
|
262
|
+
if(n.expanded)top.append(el('span','展开','tag expanded'));
|
|
263
|
+
card.append(top);
|
|
264
|
+
groups.forEach((entries,index)=>{
|
|
265
|
+
const group=el('div',undefined,'case');
|
|
266
|
+
if(n.cases.length>1&&groups.length>1)group.append(el('p','条件组合 '+(index+1),'case-label'));
|
|
267
|
+
entries.forEach(entry=>group.append(conditionCard(entry)));
|
|
268
|
+
card.append(group);
|
|
269
|
+
});
|
|
270
|
+
n.searchText=card.textContent;
|
|
271
|
+
const select=()=>{selected=selected===n.id?null:n.id;highlight()};
|
|
272
|
+
card.onclick=select;
|
|
273
|
+
card.onkeydown=e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault();select()}};
|
|
274
|
+
canvas.append(card);
|
|
119
275
|
}
|
|
120
276
|
let graphWidth=700,graphHeight=600,scale=1,selected=null;
|
|
121
277
|
function layout(){
|
|
@@ -124,35 +280,59 @@ function layout(){
|
|
|
124
280
|
function visit(id){indices.set(id,index);low.set(id,index++);stack.push(id);on.add(id);for(const next of outgoing.get(id)){if(!indices.has(next)){visit(next);low.set(id,Math.min(low.get(id),low.get(next)))}else if(on.has(next))low.set(id,Math.min(low.get(id),indices.get(next)))}if(low.get(id)===indices.get(id)){const group=[];let next;do{next=stack.pop();on.delete(next);groupOf.set(next,groups.length);group.push(next)}while(next!==id);groups.push(group)}}
|
|
125
281
|
for(const n of nodes)if(!indices.has(n.id))visit(n.id);
|
|
126
282
|
const rank=new Map();function level(group){if(rank.has(group))return rank.get(group);const parents=[...new Set(groups[group].flatMap(id=>incoming.get(id).map(p=>groupOf.get(p))).filter(p=>p!==group))];const value=parents.length?1+Math.max(...parents.map(level)):0;rank.set(group,value);return value}groups.forEach((g,i)=>level(i));
|
|
127
|
-
const units=new Map(),layers=[];for(const n of nodes){n.rank=rank.get(groupOf.get(n.id));n.w=
|
|
283
|
+
const units=new Map(),layers=[];for(const n of nodes){n.rank=rank.get(groupOf.get(n.id));n.w=n.card.offsetWidth;n.h=n.card.offsetHeight;units.set(n.id,n);(layers[n.rank]??=[]).push(n)}
|
|
128
284
|
const paths=edges.map((e,i)=>{const a=byId.get(e.source),b=byId.get(e.target),chain=[a];for(let r=a.rank+1;r<b.rank;r++){const dummy={id:'edge-'+i+'-'+r,rank:r,w:0,h:18};units.set(dummy.id,dummy);layers[r].push(dummy);chain.push(dummy)}chain.push(b);return {edge:e,chain}});
|
|
129
285
|
const previous=new Map([...units.keys()].map(id=>[id,[]])),next=new Map([...units.keys()].map(id=>[id,[]]));for(const p of paths)for(let i=1;i<p.chain.length;i++){previous.get(p.chain[i].id).push(p.chain[i-1].id);next.get(p.chain[i-1].id).push(p.chain[i].id)}
|
|
130
286
|
const position=new Map();function order(){layers.forEach(layer=>layer.forEach((u,i)=>position.set(u.id,i)))}order();
|
|
131
287
|
for(let pass=0;pass<6;pass++){const forward=pass%2===0,sequence=forward?layers:layers.toReversed(),neighbors=forward?previous:next;for(const layer of sequence){const score=u=>{const ns=neighbors.get(u.id);return ns.length?ns.reduce((s,id)=>s+position.get(id),0)/ns.length:position.get(u.id)};layer.sort((a,b)=>score(a)-score(b));order()}}
|
|
132
|
-
const heights=layers.map(layer=>layer.reduce((s,u)=>s+u.h,0)+Math.max(0,layer.length-1)*
|
|
133
|
-
|
|
288
|
+
const heights=layers.map(layer=>layer.reduce((s,u)=>s+u.h,0)+Math.max(0,layer.length-1)*36);
|
|
289
|
+
graphHeight=Math.max(280,...heights)+56;
|
|
290
|
+
let left=28;
|
|
291
|
+
for(const [r,layer] of layers.entries()){
|
|
292
|
+
let top=(graphHeight-heights[r])/2;
|
|
293
|
+
const columnWidth=Math.max(...layer.map(u=>u.w));
|
|
294
|
+
for(const u of layer){
|
|
295
|
+
u.x=left+(columnWidth-u.w)/2;u.y=top;top+=u.h+36;
|
|
296
|
+
if(u.card){u.card.style.left=u.x+'px';u.card.style.top=u.y+'px'}
|
|
297
|
+
else{u.laneStart=left-4;u.laneEnd=left+columnWidth+4}
|
|
298
|
+
}
|
|
299
|
+
left+=columnWidth+60;
|
|
300
|
+
}
|
|
301
|
+
graphWidth=left-32;
|
|
134
302
|
canvas.style.width=graphWidth+'px';canvas.style.height=graphHeight+'px';svg.setAttribute('width',graphWidth);svg.setAttribute('height',graphHeight);svg.querySelectorAll('.edge').forEach(p=>p.remove());
|
|
135
303
|
for(const p of paths){const first=p.chain[0];let from={x:first.x+first.w,y:first.y+first.h/2},d='M'+from.x+','+from.y;
|
|
136
|
-
if(
|
|
137
|
-
else for(let i=1;i<p.chain.length;i++){
|
|
304
|
+
if(first.rank>=p.chain.at(-1).rank){const b=p.chain.at(-1),lane=Math.max(from.x,b.x+b.w)+20;d+=' H'+lane+' V'+(b.y+b.h/2)+' H'+(b.x+b.w)}
|
|
305
|
+
else for(let i=1;i<p.chain.length;i++){
|
|
306
|
+
const u=p.chain[i],to={x:u.card?u.x:u.laneStart,y:u.y+u.h/2},middle=(from.x+to.x)/2;
|
|
307
|
+
d+=' C'+middle+','+from.y+' '+middle+','+to.y+' '+to.x+','+to.y;
|
|
308
|
+
if(!u.card){to.x=u.laneEnd;d+=' H'+to.x}
|
|
309
|
+
from=to;
|
|
310
|
+
}
|
|
138
311
|
const path=document.createElementNS(ns,'path');path.setAttribute('d',d);path.setAttribute('class','edge');path.setAttribute('marker-end','url(#arrow)');path.dataset.source=p.edge.source;path.dataset.target=p.edge.target;svg.append(path)}
|
|
139
312
|
zoom(scale);highlight();
|
|
140
313
|
}
|
|
141
|
-
function zoom(value){
|
|
314
|
+
function zoom(value){
|
|
315
|
+
scale=Math.max(.12,Math.min(2,value));canvas.style.transform='scale('+scale+')';
|
|
316
|
+
const viewport=$('viewport'),width=Math.max(viewport.clientWidth,Math.ceil(graphWidth*scale)),height=Math.max(viewport.clientHeight,Math.ceil(graphHeight*scale));
|
|
317
|
+
$('space').style.width=width+'px';$('space').style.height=height+'px';
|
|
318
|
+
canvas.style.left=(width-graphWidth*scale)/2+'px';canvas.style.top=(height-graphHeight*scale)/2+'px';
|
|
319
|
+
$('zoom').textContent=Math.round(scale*100)+'%';
|
|
320
|
+
}
|
|
142
321
|
function fit(){zoom(Math.min(1,($('viewport').clientWidth-24)/graphWidth,($('viewport').clientHeight-24)/graphHeight));$('viewport').scrollTo(0,0)}
|
|
143
322
|
function highlight(){const query=$('search').value.trim().toLocaleLowerCase(),matches=new Set(nodes.filter(n=>(tableName(n.id)+' '+n.id+' '+n.searchText).toLocaleLowerCase().includes(query)).map(n=>n.id));const focus=query?matches:selected?new Set([selected,...incoming.get(selected),...outgoing.get(selected)]):null;for(const n of nodes){n.card.classList.toggle('dim',!!focus&&!focus.has(n.id));n.card.classList.toggle('selected',n.id===selected||!!query&&matches.has(n.id))}for(const p of svg.querySelectorAll('.edge')){const active=selected&&(p.dataset.source===selected||p.dataset.target===selected);p.classList.toggle('active',!!active);p.classList.toggle('dim',!!focus&&(!focus.has(p.dataset.source)||!focus.has(p.dataset.target)))}$('match').textContent=query?'找到 '+matches.size+' 张表':''}
|
|
144
|
-
const gapNames={NON_FILTER_STEP_CONDITIONS:'
|
|
145
|
-
|
|
146
|
-
$('subtitle').
|
|
147
|
-
$('
|
|
323
|
+
const gapNames={TABLE_DEFINITION_FILTERS:'图中展示本次取数的附加条件',NON_FILTER_STEP_CONDITIONS:'公式等步骤内部的条件未采集',PHYSICAL_EXECUTION:'依赖关系来自查询构建',RAW_VALUES:'trace 中的业务值已脱敏',COMPONENT_INPUT_REUSED:'组件追加阶段复用了已有输入,未执行准备的追加筛选'};
|
|
324
|
+
$('subtitle').textContent=data.componentName||'结果表 '+tableName(data.rootTableId);
|
|
325
|
+
$('subtitle').title=$('subtitle').textContent;
|
|
326
|
+
$('graph-counts').textContent=nodes.length+' 张表,'+edges.length+' 条依赖';
|
|
327
|
+
$('identity').textContent=[data.traceId?'Trace '+data.traceId:null,data.time].filter(Boolean).join('\n');
|
|
148
328
|
$('summary').textContent=data.summary||'';
|
|
149
|
-
const coverage=[legacy?'旧 trace
|
|
329
|
+
const coverage=[legacy?'旧 trace 只记录了条件传播关系。':actual?'本图依据本次查询构建记录,业务值已脱敏。':'历史快照记录的是逻辑依赖。',...(data.gaps||[]).map(g=>g.startsWith('REUSED_INPUT_DETAILS_UNAVAILABLE:')?'复用输入的来源条件未记录('+tableName(g.slice(g.indexOf(':')+1))+')':gapNames[g]||g)];
|
|
150
330
|
if(data.omittedNodes||data.omittedEdges)coverage.push('已省略 '+data.omittedNodes+' 个条件对象、'+data.omittedEdges+' 条条件关系');
|
|
151
331
|
$('coverage').textContent=coverage.join('\n');$('evidence').textContent=(data.evidence||[]).join('\n');
|
|
152
|
-
if(legacy||data.scope==='BUILD_FAILED'||data.gaps?.length||limited){$('notes-title').textContent='
|
|
332
|
+
if(legacy||data.scope==='BUILD_FAILED'||data.gaps?.length||limited){$('notes-title').textContent='部分计算信息未记录';$('notes-title').classList.add('warning')}
|
|
153
333
|
$('search').oninput=()=>{highlight();if($('search').value.trim())canvas.querySelector('.node.selected')?.scrollIntoView({block:'center',inline:'center'})};$('minus').onclick=()=>zoom(scale/1.2);$('plus').onclick=()=>zoom(scale*1.2);$('fit').onclick=fit;$('reset').onclick=()=>zoom(1);
|
|
154
|
-
|
|
155
|
-
|
|
334
|
+
window.addEventListener('resize',()=>zoom(scale));
|
|
335
|
+
layout();zoom(Math.max(.75,Math.min(1,($('viewport').clientWidth-24)/graphWidth)));
|
|
156
336
|
</script>
|
|
157
337
|
</body>
|
|
158
338
|
</html>
|
|
@@ -235,8 +235,83 @@ def normalize(source, span_id=None, context=None):
|
|
|
235
235
|
return snapshot
|
|
236
236
|
|
|
237
237
|
|
|
238
|
+
def graph_dependencies(tables, dependencies, inline):
|
|
239
|
+
"""历史快照的内联分支只作中转,遍历到下一张业务表就停止。"""
|
|
240
|
+
consumers = {}
|
|
241
|
+
for edge in dependencies:
|
|
242
|
+
consumers.setdefault(edge['source'], []).append(edge['target'])
|
|
243
|
+
result = []
|
|
244
|
+
for table in tables:
|
|
245
|
+
source = table['id']
|
|
246
|
+
pending = list(reversed(consumers.get(source, [])))
|
|
247
|
+
seen = set()
|
|
248
|
+
while pending:
|
|
249
|
+
target = pending.pop()
|
|
250
|
+
if target in seen:
|
|
251
|
+
continue
|
|
252
|
+
seen.add(target)
|
|
253
|
+
if target in inline:
|
|
254
|
+
pending.extend(reversed(consumers.get(target, [])))
|
|
255
|
+
elif target != source:
|
|
256
|
+
if len(result) == 4096:
|
|
257
|
+
return result, True
|
|
258
|
+
result.append({'source': source, 'target': target})
|
|
259
|
+
return result, False
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def graph_data(snapshot):
|
|
263
|
+
"""把已核验快照整理成业务表关系与直接作用于表的条件。"""
|
|
264
|
+
graph = dict(snapshot)
|
|
265
|
+
graph.pop('componentDefinition', None)
|
|
266
|
+
graph.pop('componentStages', None)
|
|
267
|
+
graph['nodes'] = [{key: value for key, value in node.items() if key != 'componentFilters'}
|
|
268
|
+
for node in snapshot['nodes']]
|
|
269
|
+
inline = {table['id'] for table in snapshot['tables'] if table.get('kind') == 'INLINE_QUERY'}
|
|
270
|
+
tables = []
|
|
271
|
+
expanded = set()
|
|
272
|
+
for table in snapshot['tables']:
|
|
273
|
+
if table['id'] in inline:
|
|
274
|
+
continue
|
|
275
|
+
modes = dict.fromkeys(item['state'] for item in [table.get('definition'), *table.get('variants', [])] if item)
|
|
276
|
+
if 'EXPANDED' in modes:
|
|
277
|
+
expanded.add(table['id'])
|
|
278
|
+
definitions = [{'state': mode} for mode in modes]
|
|
279
|
+
tables.append(dict(table, definition=definitions[0] if definitions else None, variants=definitions[1:]))
|
|
280
|
+
graph['tables'] = tables
|
|
281
|
+
if inline:
|
|
282
|
+
graph['dependencies'], limited = graph_dependencies(tables, snapshot['dependencies'], inline)
|
|
283
|
+
if limited:
|
|
284
|
+
graph['gaps'] = list(dict.fromkeys([*snapshot.get('gaps', []), 'SCAN_LIMIT']))
|
|
285
|
+
|
|
286
|
+
if snapshot.get('schema') == 'calculation-panorama-v2':
|
|
287
|
+
references, parameter_ids, control_ids = set(), set(), set()
|
|
288
|
+
for node in graph['nodes']:
|
|
289
|
+
filters = [condition for key in ('fieldFilters', 'linkageFilters') for condition in node.get(key) or []]
|
|
290
|
+
references.update(condition.get('_traceConditionId') for condition in filters)
|
|
291
|
+
filter_parameters = set()
|
|
292
|
+
while filters:
|
|
293
|
+
condition = filters.pop()
|
|
294
|
+
filter_parameters.update(condition.get('parameterIds', []))
|
|
295
|
+
filters.extend(condition.get('children', []))
|
|
296
|
+
node['parameters'] = [item for item in node.get('parameters') or []
|
|
297
|
+
if item['id'] in filter_parameters
|
|
298
|
+
or (node['tableId'] in expanded and 'TABLE' in (item.get('sources') or []))]
|
|
299
|
+
parameter_ids.update(filter_parameters)
|
|
300
|
+
parameter_ids.update(item['id'] for item in node['parameters'])
|
|
301
|
+
for inputs in (node['parameters'], node.get('dynamicFields') or []):
|
|
302
|
+
control_ids.update(source for item in inputs for source in item.get('controlIds', []))
|
|
303
|
+
controls = {ref: ids for ref, ids in snapshot.get('conditionControls', {}).items() if ref in references}
|
|
304
|
+
control_ids.update(source for ids in controls.values() for source in ids)
|
|
305
|
+
graph['parameters'] = [item for item in snapshot['parameters'] if item['id'] in parameter_ids]
|
|
306
|
+
graph['conditionControls'] = controls
|
|
307
|
+
graph['controls'] = [item for item in snapshot.get('controls', []) if item['id'] in control_ids]
|
|
308
|
+
graph.pop('conditions', None) # HTML 使用各作用位置已解析的条件,避免携带被省略的旧正文。
|
|
309
|
+
return graph
|
|
310
|
+
|
|
311
|
+
|
|
238
312
|
def render(snapshot, output):
|
|
239
313
|
"""将已校验的展示数据嵌入离线 HTML;业务文本转义后写入,防止结束 script 标签。"""
|
|
314
|
+
snapshot = graph_data(snapshot)
|
|
240
315
|
template = Path(__file__).with_name('assets').joinpath('calculation-panorama.html').read_text(encoding='utf-8')
|
|
241
316
|
payload = json.dumps(snapshot, ensure_ascii=False).replace('<', '\\u003c').replace('>', '\\u003e').replace('&', '\\u0026')
|
|
242
317
|
document = template.replace('__PANORAMA_DATA__', payload)
|
package/content-manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": 1,
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"source_commit": "
|
|
3
|
+
"version": "0.1.20261112024042",
|
|
4
|
+
"source_commit": "98b379cb3e75bc9b1f389a2ef9d7fe31fcab605a",
|
|
5
5
|
"source_dirty": false,
|
|
6
6
|
"cli_sha256": "4a153c76eb03a90bcec67a5431d471cbdda09a1e7188bb6cefee818d43e89e26",
|
|
7
7
|
"files": {
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"knowledge/dashboard/conclusions.md": "08541d4e3b678c5da0faa348d9ba8bedb23a6ad2ed67a13f4bd3f329df426420",
|
|
46
46
|
"knowledge/dashboard/queries.md": "ef676ae4df22a35ca76c16bb8f155c0e8bd30d7d356d6429ee48c05eb7a06a40",
|
|
47
47
|
"knowledge/dashboard/signals.md": "b968b762a1f5db9bce52e34be14e6870c6bc14322273c9059602caa6652df127",
|
|
48
|
-
"knowledge/dashboard/topics/calculation-panorama.md": "
|
|
48
|
+
"knowledge/dashboard/topics/calculation-panorama.md": "19c5e8e51b1aeacd5adb78a7100aee1905d4832cbaea2ed59854ff7777d35c7c",
|
|
49
49
|
"knowledge/dashboard/topics/component-comparison.md": "c67e699adf92891f973c29bd691a8fec4b3ce1f543a286f08f31be1484b6c4de",
|
|
50
50
|
"knowledge/dashboard/topics/dynamic-fields.md": "2483a89bbeaa49cc317cd7cd69a7157f0254a96daf553183c109dd48cec4bbda",
|
|
51
51
|
"knowledge/dashboard/topics/filter-tree.md": "c373b24fd57204cad492224cd85ef7e017fe6dca3b29054e4fdcacf3d99e9550",
|
|
@@ -83,11 +83,11 @@
|
|
|
83
83
|
"knowledge/update/topics/lifecycle.md": "6fbe2b705226b8de08dd5b960dc2c98b626f12e9837b62604c51d3867bca63b4",
|
|
84
84
|
"knowledge/update/topics/scene-producers.md": "fbe64e070dc2e0f0aaa28d4ad4985626bc758fcaf0c97d6bd2cf72788556f857",
|
|
85
85
|
"knowledge/update/workflow.md": "977d871c7088dfb3b033dba3c361e45d3ca05e673a428ba41e6f7771e9353b25",
|
|
86
|
-
"scripts/assets/calculation-panorama.html": "
|
|
86
|
+
"scripts/assets/calculation-panorama.html": "76b7e2241d4bdc1e03128e75b741262c8012f33425fa284b62e0b9cb310d2943",
|
|
87
87
|
"scripts/enterprise_update_report.template.html": "c589820c0beb117d9663f70b58d721f011d6b55cfe5d853d652961a32ab281ba",
|
|
88
88
|
"scripts/install_signoz_mcp.ps1": "a50ff759df00aa45332aede4ebee55a1778d3ba4d48ed384c8ae08c68e5cd94e",
|
|
89
89
|
"scripts/install_signoz_mcp.py": "ae54d74787ee80b55a1aaa84cf621857698ecf990dbf6f2b1a41379a9215abf7",
|
|
90
|
-
"scripts/render_calculation_panorama.py": "
|
|
90
|
+
"scripts/render_calculation_panorama.py": "728c0663204db6425be0602d3345e679ec92c8554a6f8bab656b71c7946d62dc",
|
|
91
91
|
"scripts/render_enterprise_update_report.py": "8ab58e0803c0a5f46fa4565468a38a6f564b76f39035ed9987f25f11053983c1"
|
|
92
92
|
}
|
|
93
93
|
}
|