
Insight Diagram
- 338 installs
- 199 repo stars
- Updated July 21, 2026
- smallnest/goal-workflow
Helps with ai & agent building tasks.
About
insight-diagram is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- insight-diagram
- AI & Agent Building
- AI-coding skill
Insight Diagram by the numbers
- 338 all-time installs (skills.sh)
- +39 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #2,169 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/smallnest/goal-workflow --skill insight-diagramAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 338 |
|---|---|
| repo stars | ★ 199 |
| Last updated | July 21, 2026 |
| Repository | smallnest/goal-workflow ↗ |
What it does
Helps with ai & agent building tasks.
Files
Insight Diagram — 项目图表生成技能
分析任意代码库,自动生成 UML 14种图 + 架构图 + 流程图,使用 /architecture-diagram 渲染为 HTML+SVG。
图表分类与清单
结构性图形 (Structural Diagrams — 静态)
描述系统的物理组成和静态结构。
| 编号 | 图表类型 | 英文标识 | 关注点 |
|---|---|---|---|
| 1 | 系统架构图 | architecture | 组件关系、全局视角(非UML,最常用) |
| 2 | 类图 | class | 定义类、属性、操作及关系 |
| 3 | 对象图 | object | 特定时刻的对象实例及其关系 |
| 4 | 组件图 | component | 系统组件及其依赖关系 |
| 5 | 部署图 | deployment | 物理硬件、节点及软件部署 |
| 6 | 包图 | package | 将模型元素分组组织 |
| 7 | 复合结构图 | composite-structure | 类的内部结构 |
| 8 | 剖面图 | profile | 扩展UML元模型、自定义构造型 |
行为性图形 (Behavioral Diagrams — 动态)
描述系统与外部参与者或系统内部的交互过程。
| 编号 | 图表类型 | 英文标识 | 关注点 |
|---|---|---|---|
| 9 | 流程图 | flowchart | 主流程与分支(非UML,最常用) |
| 10 | 用例图 | usecase | 从用户角度展示系统功能 |
| 11 | 活动图 | activity | 过程的流程或步骤 |
| 12 | 状态机图 | state-machine | 对象生命周期的状态变迁 |
| 13 | 序列图 | sequence | 按时间顺序展示对象间交互 |
| 14 | 通信图 | communication | 侧重于对象间的组织关系 |
| 15 | 定时图 | timing | 侧重于状态变化的时间约束 |
| 16 | 交互概览图 | interaction-overview | 结合活动图和时序图 |
| 17 | 泳道图 | swimlane | 跨组件/角色职责流程(活动图变体) |
示例参考
本技能的 examples/ 目录包含 13 个已完成的图表 HTML 文件,作为视觉样式和内容结构的参考模板。生成任何图表前,必须先阅读对应的示例文件,以确保风格一致、结构规范。
示例文件清单
| 文件 | 图表类型 | 英文标识 |
|---|---|---|
examples/architecture.html | 系统架构图 | architecture |
examples/class.html | 类图 | class |
examples/object.html | 对象图 | object |
examples/component.html | 组件图 | component |
examples/deployment.html | 部署图 | deployment |
examples/flowchart.html | 流程图 | flowchart |
examples/usecase.html | 用例图 | usecase |
examples/activity.html | 活动图 | activity |
examples/sequence.html | 序列图 | sequence |
examples/communication.html | 通信图 | communication |
examples/dfd.html | 数据流图 | dfd |
examples/interaction-overview.html | 交互概览图 | interaction-overview |
examples/swimlane.html | 泳道图 | swimlane |
参考规则
1. 生成前必读: 调用 /architecture-diagram 前,先用 Read 工具阅读对应类型的示例文件,从中提取:
- SVG 布局策略(节点间距、分组方式、箭头走向)
- 节点样式层级(核心节点 accent 高亮、普通节点实线边框、可选节点虚线边框)
- 标注风格(阶段标签、Legend 图例、卡片摘要)
- 信息密度(每个节点显示多少字段/属性)
2. 结构对齐: 生成的图表应与示例保持相同的结构层次:
- 页面顶部:标题 + 副标题 + 图表类型说明
- 中间主体:SVG 图表区域(带浅色边框容器)
- 底部:信息摘要卡片 + 页脚
3. 内容替换而非照搬: 示例中的业务数据(NovaShield 风控系统)是虚构的参考案例,生成时需替换为目标项目的真实架构信息。只参考布局和样式,不复制业务内容。
4. 无对应示例的类型: 对于包图 (package)、复合结构图 (composite-structure)、剖面图 (profile)、状态机图 (state-machine)、定时图 (timing) 这 5 种没有示例文件的图表类型,参考最相近的已有示例(如包图参考组件图,状态机图参考活动图),并沿用相同的视觉语言。
执行流程
步骤 1:分析代码库
读取项目关键文件,提取架构信息:
1. 读取项目根目录的 CLAUDE.md(如存在)获取项目概览 2. 读取各子目录的 CLAUDE.md(如存在)获取模块细节 3. 用 Glob 扫描源码文件结构(**/*.go, **/*.py, **/*.ts 等) 4. 读取入口文件(main.go, app.py, index.ts 等)识别顶层组件 5. 用 Grep 搜索关键模式:接口定义、函数签名、依赖注入、配置项
从以上信息中提炼出:
- 组件清单: 服务、模块、外部依赖
- 关系图: 谁调用谁、谁依赖谁、数据流向
- 核心类型: 结构体/类、接口、枚举
- 流程: 主业务流程、异常处理流程
- 部署: 进程、中间件、外部服务
步骤 2:选择图表
使用 AskUserQuestion 让用户选择要生成的图表(multiSelect: true),分4组展示:
第1组 — 结构性图形(静态):
- 系统架构图 (architecture)
- 类图 (class)
- 对象图 (object)
- 组件图 (component)
第2组 — 结构性图形续 + 部署:
- 部署图 (deployment)
- 包图 (package)
- 复合结构图 (composite-structure)
- 剖面图 (profile)
第3组 — 行为性图形(动态):
- 流程图 (flowchart)
- 用例图 (usecase)
- 活动图 (activity)
- 状态机图 (state-machine)
第4组 — 交互图 + 常用非UML:
- 序列图 (sequence)
- 通信图 (communication)
- 交互概览图 (interaction-overview)
- 泳道图 (swimlane)
- 全部生成 (all)
默认推荐:architecture + sequence + flowchart
步骤 3:逐个生成
对每个选中的图表类型:
1. 先读示例: 用 Read 工具阅读 examples/<标识>.html(如 examples/architecture.html),提取布局模式、节点样式、标注方式 2. 根据步骤 1 提取的架构信息,整理出该图表应展示的元素和关系 3. 调用 /architecture-diagram skill,传入图表类型、标题、内容描述、输出路径,必须指定 light 风格 4. 输出文件保存到 docs/<标识>.html(如 docs/architecture.html) 5. 简要报告完成状态
生成规则:
- 风格: 必须使用 light Claude 风格(暖白背景 #FAF9F6、terracotta/sage/plum/rose 配色、Inter 字体、白色卡片容器),与 Anthropic Claude 品牌视觉一致
- 防遮盖: 所有 SVG 元素(节点、箭头、标签)不得互相遮盖。具体做法:
- 计算每个元素的边界框,确保无重叠
- 箭头绘制在节点下方(SVG 中先画箭头再画节点)
- 节点间留足间距(垂直最少 40px,水平最少 30px)
- 文字不超出所在节点边界,超长文字截断或换行
- 连接线的标签放置在线段中点偏移处,避免覆盖线段或节点
- 如果元素过多导致图表拥挤,拆分为多个子图或缩小元素尺寸
批量生成顺序(宏观→微观): architecture → component → deployment → package → composite-structure → profile → class → object → usecase → flowchart → activity → state-machine → swimlane → sequence → communication → timing → interaction-overview
步骤 4:报告
全部完成后输出:
- 生成的文件列表
- 每个图表的简要描述
各图表的内容指南
系统架构图 (architecture) — 非UML,最常用
- 展示系统顶层组件及其连接关系
- 区分内部模块与外部依赖
- 标注核心数据流方向
类图 (class)
- 核心类型为类节点(名称+字段+方法)
- 继承、组合、依赖关系
- 接口与实现分离
- 限制在 10-15 个核心类型
对象图 (object)
- 选取一个典型运行时场景
- 展示对象实例及其属性值
- 对象间的链接关系
组件图 (component)
- 每个组件为一个节点
- 箭头表示依赖/调用方向
- 标注接口名称
部署图 (deployment)
- 物理节点(服务器、容器、Serverless)
- 中间件(消息队列、缓存、数据库)
- 外部服务(第三方 API)
- 标注通信协议
包图 (package)
- 按模块/命名空间分组
- 包间依赖关系
- 体现分层架构
复合结构图 (composite-structure)
- 类/组件的内部结构
- 部件(Part)与连接器(Connector)
- 端口(Port)与接口
剖面图 (profile)
- 自定义构造型(Stereotype)
- 扩展元模型的标签定义(Tagged Values)
- 领域特定建模约束
流程图 (flowchart) — 非UML,最常用
- 主流程 + 关键分支
- 失败/异常路径
- 起止节点清晰
用例图 (usecase)
- 参与者(人/外部系统)
- 用例椭圆
- include/extend 关系
活动图 (activity)
- 阶段/步骤为活动节点
- 并行分支用 fork/join
- 决策点用菱形
状态机图 (state-machine)
- 对象的关键状态
- 触发状态变迁的事件
- 动作/守卫条件
- 初始态和终态
序列图 (sequence)
- 参与者为纵向生命线
- 水平箭头为消息调用
- 标注关键返回值
- 关注 2-5 个核心交互场景
通信图 (communication)
- 组件为节点,消息为连线
- 标注消息序号
- 强调协作关系而非时序
定时图 (timing)
- 时间轴横向展开
- 状态变化的时间约束
- 持续时间标注
交互概览图 (interaction-overview)
- 控制流节点内嵌交互片段
- 展示条件分支和循环
- 宏观概览各交互场景
泳道图 (swimlane) — 活动图变体
- 按组件/角色分泳道
- 流程步骤在对应泳道内
- 跨泳道箭头表示交互
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NovaShield 风控活动图</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Geist', sans-serif;
background: #faf6f0;
min-height: 100vh;
padding: 2.5rem 2rem;
color: #1c1917;
}
.container { max-width: 960px; margin: 0 auto; }
.header { margin-bottom: 2rem; }
.eyebrow {
font-family: 'Geist Mono', monospace;
font-size: 0.6875rem;
font-weight: 500;
letter-spacing: 0.18em;
text-transform: uppercase;
color: #78716c;
margin-bottom: 0.375rem;
}
h1 {
font-family: 'Instrument Serif', serif;
font-size: 1.75rem;
font-weight: 400;
color: #1c1917;
}
.subtitle {
color: #78716c;
font-size: 0.8125rem;
margin-top: 0.375rem;
}
.diagram-desc {
color: #a8a29e;
font-size: 0.6875rem;
margin-top: 0.5rem;
line-height: 1.5;
font-style: italic;
}
.diagram-container {
background: #f0ebe3;
border: 1px solid rgba(28,25,23,0.12);
border-radius: 8px;
padding: 1.5rem;
overflow-x: auto;
}
svg { width: 100%; min-width: 680px; display: block; }
.cards {
display: grid;
grid-template-columns: 1.2fr 0.8fr;
gap: 1rem;
margin-top: 1.5rem;
}
.card {
background: #ffffff;
border: 1px solid rgba(28,25,23,0.12);
border-radius: 6px;
padding: 1.25rem;
}
.card-eyebrow {
font-family: 'Geist Mono', monospace;
font-size: 0.625rem;
font-weight: 500;
letter-spacing: 0.18em;
text-transform: uppercase;
color: #a8a29e;
margin-bottom: 0.5rem;
}
.card-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.card-dot { width: 7px; height: 7px; border-radius: 50%; }
.card-dot.ink { background: #1c1917; }
.card-dot.accent { background: #d97706; }
.card-dot.muted { background: #78716c; }
.card h3 { font-size: 0.8125rem; font-weight: 600; color: #1c1917; }
.card ul {
list-style: none;
color: #78716c;
font-size: 0.75rem;
line-height: 1.6;
}
.card li { margin-bottom: 0.25rem; }
.footer {
margin-top: 1.5rem;
padding-top: 0.75rem;
border-top: 1px solid rgba(28,25,23,0.08);
text-align: center;
font-family: 'Geist Mono', monospace;
color: #a8a29e;
font-size: 0.625rem;
letter-spacing: 0.04em;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<p class="eyebrow">Activity Diagram</p>
<h1>风控消息处理活动</h1>
<p class="subtitle">RabbitMQ 消息到达 → 去重 → 采集 → 并行预处理 → 诊断 → 上报</p>
<p class="diagram-desc"><b>活动图 (Activity Diagram)</b> — 行为性图形。展示业务逻辑或算法流程,侧重于动作和执行过程。</p>
</div>
<div class="diagram-container">
<svg viewBox="0 0 700 1030">
<defs>
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#78716c"/>
</marker>
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#d97706"/>
</marker>
<marker id="arrow-link" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#c05621"/>
</marker>
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.9" fill="rgba(28,25,23,0.10)"/>
</pattern>
</defs>
<!-- Background -->
<rect width="100%" height="100%" fill="#faf6f0"/>
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.6"/>
<!-- ============================================================
ARROWS (drawn first — z-order behind boxes)
============================================================ -->
<!-- 1. Start → 接收订单 -->
<line x1="260" y1="52" x2="260" y2="84" stroke="#78716c" stroke-width="1" marker-end="url(#arrow)"/>
<!-- 2. 接收订单 → 去重? -->
<line x1="260" y1="128" x2="260" y2="164" stroke="#78716c" stroke-width="1" marker-end="url(#arrow)"/>
<!-- 3. 去重? → skip (重复) -->
<line x1="320" y1="196" x2="440" y2="196" stroke="#a8a29e" stroke-width="1" stroke-dasharray="4,3" marker-end="url(#arrow)"/>
<rect x="360" y="180" width="36" height="14" rx="2" fill="#faf6f0"/>
<text x="378" y="191" fill="#a8a29e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">重复</text>
<text x="448" y="200" fill="#a8a29e" font-size="8" font-family="'Geist Mono', monospace">跳过</text>
<!-- 4. 去重? → 获取订单信息 (新消息) -->
<line x1="260" y1="228" x2="260" y2="264" stroke="#78716c" stroke-width="1" marker-end="url(#arrow)"/>
<rect x="228" y="232" width="64" height="14" rx="2" fill="#faf6f0"/>
<text x="260" y="243" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">新消息</text>
<!-- 5. 获取订单信息 → 拉取交易数据 -->
<line x1="260" y1="308" x2="260" y2="340" stroke="#78716c" stroke-width="1" marker-end="url(#arrow)"/>
<!-- 6. 拉取交易数据 → 验证? -->
<line x1="260" y1="384" x2="260" y2="416" stroke="#78716c" stroke-width="1" marker-end="url(#arrow)"/>
<!-- 7. 验证? → 上报失败 (失败) -->
<path d="M196,448 L120,448 L120,876 L156,876" fill="none" stroke="#c05621" stroke-width="1" stroke-dasharray="5,4" marker-end="url(#arrow-link)"/>
<rect x="128" y="432" width="36" height="14" rx="2" fill="#faf6f0"/>
<text x="146" y="443" fill="#c05621" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">失败</text>
<!-- 8. 验证? → Fork bar (通过) -->
<line x1="260" y1="480" x2="260" y2="516" stroke="#78716c" stroke-width="1" marker-end="url(#arrow)"/>
<rect x="240" y="484" width="40" height="14" rx="2" fill="#faf6f0"/>
<text x="260" y="495" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">通过</text>
<!-- 9. Fork bar → 预处理 (left branch) -->
<line x1="220" y1="520" x2="180" y2="560" stroke="#78716c" stroke-width="1"/>
<!-- 10. Fork bar → 规则查询 (right branch) -->
<line x1="300" y1="520" x2="340" y2="560" stroke="#78716c" stroke-width="1"/>
<!-- 11. 预处理 → Join bar -->
<line x1="180" y1="604" x2="220" y2="644" stroke="#78716c" stroke-width="1"/>
<!-- 12. 规则查询 → Join bar -->
<line x1="340" y1="604" x2="300" y2="644" stroke="#78716c" stroke-width="1"/>
<!-- 13. Join bar → 诊断 -->
<line x1="260" y1="648" x2="260" y2="680" stroke="#d97706" stroke-width="1.2" marker-end="url(#arrow-accent)"/>
<!-- 14. 诊断 → 诊断成功? -->
<line x1="260" y1="724" x2="260" y2="760" stroke="#78716c" stroke-width="1" marker-end="url(#arrow)"/>
<!-- 15. 诊断成功? → Fallback (失败) -->
<path d="M320,792 L500,792 L500,856" fill="none" stroke="#c05621" stroke-width="1" stroke-dasharray="5,4" marker-end="url(#arrow-link)"/>
<rect x="340" y="776" width="36" height="14" rx="2" fill="#faf6f0"/>
<text x="358" y="787" fill="#c05621" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">失败</text>
<!-- 16. Fallback → 发送告警 -->
<line x1="420" y1="878" x2="364" y2="878" stroke="#78716c" stroke-width="1" marker-end="url(#arrow)"/>
<!-- 17. 诊断成功? → 发送告警 (成功) -->
<line x1="260" y1="824" x2="260" y2="856" stroke="#78716c" stroke-width="1" marker-end="url(#arrow)"/>
<rect x="240" y="832" width="40" height="14" rx="2" fill="#faf6f0"/>
<text x="260" y="843" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">成功</text>
<!-- 18. 发送告警 → End -->
<line x1="260" y1="900" x2="260" y2="940" stroke="#78716c" stroke-width="1" marker-end="url(#arrow)"/>
<!-- ============================================================
NODES (drawn after arrows — z-order above)
============================================================ -->
<!-- Start node -->
<rect x="200" y="20" width="120" height="32" rx="16" fill="#faf6f0" stroke="#78716c" stroke-width="1"/>
<text x="260" y="41" fill="#1c1917" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">消息到达</text>
<!-- 接收订单 -->
<rect x="156" y="84" width="208" height="44" rx="6" fill="#faf6f0" stroke="#1c1917" stroke-width="1"/>
<text x="260" y="111" fill="#1c1917" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">接收订单</text>
<!-- 去重? diamond -->
<polygon points="260,164 320,196 260,228 200,196" fill="#faf6f0" stroke="#1c1917" stroke-width="1"/>
<text x="260" y="200" fill="#1c1917" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">去重?</text>
<!-- 获取订单信息 -->
<rect x="156" y="264" width="208" height="44" rx="6" fill="#faf6f0" stroke="#1c1917" stroke-width="1"/>
<text x="260" y="291" fill="#1c1917" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">获取订单信息</text>
<!-- 拉取交易数据 -->
<rect x="156" y="340" width="208" height="44" rx="6" fill="#faf6f0" stroke="#1c1917" stroke-width="1"/>
<text x="260" y="360" fill="#1c1917" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">拉取交易数据</text>
<text x="260" y="376" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">dbcli export</text>
<!-- 验证? diamond -->
<polygon points="260,416 320,448 260,480 200,448" fill="#faf6f0" stroke="#1c1917" stroke-width="1"/>
<text x="260" y="452" fill="#1c1917" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">验证?</text>
<!-- FORK BAR (parallel split) -->
<rect x="200" y="516" width="120" height="8" rx="4" fill="#1c1917"/>
<!-- 预处理 (left branch) -->
<rect x="100" y="560" width="160" height="44" rx="6" fill="#faf6f0" stroke="#1c1917" stroke-width="1"/>
<text x="180" y="587" fill="#1c1917" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">预处理</text>
<!-- 规则查询 (right branch) -->
<rect x="260" y="560" width="160" height="44" rx="6" fill="#faf6f0" stroke="#1c1917" stroke-width="1"/>
<text x="340" y="587" fill="#1c1917" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">规则查询</text>
<!-- JOIN BAR (parallel merge) -->
<rect x="200" y="640" width="120" height="8" rx="4" fill="#1c1917"/>
<!-- 诊断 (FOCAL — accent) -->
<rect x="156" y="680" width="208" height="44" rx="6" fill="#faf6f0"/>
<rect x="156" y="680" width="208" height="44" rx="6" fill="rgba(217,119,6,0.08)" stroke="#d97706" stroke-width="1.2"/>
<text x="260" y="707" fill="#1c1917" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">GPT 风控评分</text>
<!-- 诊断成功? diamond -->
<polygon points="260,760 320,792 260,824 200,792" fill="#faf6f0" stroke="#1c1917" stroke-width="1"/>
<text x="260" y="796" fill="#1c1917" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">成功?</text>
<!-- Fallback (optional, dashed) -->
<rect x="420" y="856" width="160" height="44" rx="6" fill="#faf6f0"/>
<rect x="420" y="856" width="160" height="44" rx="6" fill="rgba(28,25,23,0.02)" stroke="rgba(28,25,23,0.20)" stroke-width="1" stroke-dasharray="4,3"/>
<text x="500" y="883" fill="#78716c" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Fallback 降级</text>
<!-- 发送告警 -->
<rect x="156" y="856" width="208" height="44" rx="6" fill="#faf6f0" stroke="#1c1917" stroke-width="1"/>
<text x="260" y="883" fill="#1c1917" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">发送告警</text>
<!-- End node -->
<circle cx="260" cy="948" r="8" fill="none" stroke="#1c1917" stroke-width="1"/>
<circle cx="260" cy="948" r="4" fill="#1c1917"/>
<!-- ============================================================
STAGE ANNOTATIONS (left margin)
============================================================ -->
<text x="28" y="108" fill="#a8a29e" font-size="7" font-family="'Geist Mono', monospace" letter-spacing="0.08em">CONSUME</text>
<text x="28" y="196" fill="#a8a29e" font-size="7" font-family="'Geist Mono', monospace" letter-spacing="0.08em">IDEMPOTENT</text>
<text x="28" y="288" fill="#a8a29e" font-size="7" font-family="'Geist Mono', monospace" letter-spacing="0.08em">STAGE 1</text>
<text x="28" y="364" fill="#a8a29e" font-size="7" font-family="'Geist Mono', monospace" letter-spacing="0.08em">STAGE 2</text>
<text x="28" y="452" fill="#a8a29e" font-size="7" font-family="'Geist Mono', monospace" letter-spacing="0.08em">STAGE 3</text>
<text x="28" y="584" fill="#a8a29e" font-size="7" font-family="'Geist Mono', monospace" letter-spacing="0.08em">STAGE 4</text>
<text x="28" y="704" fill="#a8a29e" font-size="7" font-family="'Geist Mono', monospace" letter-spacing="0.08em">STAGE 5</text>
<text x="28" y="880" fill="#a8a29e" font-size="7" font-family="'Geist Mono', monospace" letter-spacing="0.08em">STAGE 6</text>
<!-- Fork/Join annotation -->
<text x="200" y="510" fill="#78716c" font-size="7" font-family="'Geist Mono', monospace" letter-spacing="0.06em">FORK</text>
<text x="200" y="660" fill="#78716c" font-size="7" font-family="'Geist Mono', monospace" letter-spacing="0.06em">JOIN</text>
<!-- ============================================================
LEGEND (horizontal strip at bottom)
============================================================ -->
<line x1="30" y1="976" x2="670" y2="976" stroke="rgba(28,25,23,0.10)" stroke-width="0.8"/>
<text x="30" y="1000" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">LEGEND</text>
<rect x="100" y="988" width="24" height="14" rx="7" fill="#faf6f0" stroke="#78716c" stroke-width="1"/>
<text x="132" y="999" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace">开始/结束</text>
<rect x="200" y="988" width="24" height="14" rx="4" fill="#faf6f0" stroke="#1c1917" stroke-width="1"/>
<text x="232" y="999" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace">活动</text>
<polygon points="300,988 312,995 300,1002 288,995" fill="#faf6f0" stroke="#1c1917" stroke-width="1"/>
<text x="320" y="999" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace">判断</text>
<rect x="380" y="991" width="24" height="8" rx="4" fill="#1c1917"/>
<text x="412" y="999" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace">并行分叉/汇合</text>
<rect x="520" y="988" width="24" height="14" rx="4" fill="rgba(217,119,6,0.08)" stroke="#d97706" stroke-width="1"/>
<text x="552" y="999" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace">关键活动</text>
</svg>
</div>
<!-- Summary Cards -->
<div class="cards">
<div class="card">
<p class="card-eyebrow">PARALLEL</p>
<div class="card-header">
<div class="card-dot ink"></div>
<h3>并行活动</h3>
</div>
<ul>
<li>• Stage 4: 预处理与 规则查询并行执行</li>
<li>• asyncio 并发,任一失败则整体失败</li>
<li>• Fork/Join 条表示并行分叉与汇合</li>
<li>• 规则查询 30 分钟缓存,关键词去重排序</li>
</ul>
</div>
<div class="card">
<p class="card-eyebrow">FALLBACK</p>
<div class="card-header">
<div class="card-dot accent"></div>
<h3>异常分支</h3>
</div>
<ul>
<li>• 验证失败 → 直接告警失败 (scan_status=3)</li>
<li>• GPT 风控评分失败 → Fallback 降级</li>
<li>• 降级走 rule_engine.py,结果仍上报</li>
<li>• 非-alert 阶段失败统一走 fail() 出口</li>
</ul>
</div>
</div>
<div class="footer">
nova-shield · github.com/novashield/risk-engine
</div>
</div>
</body>
</html>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NovaShield 风控系统架构图</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
background: #FAF9F6;
min-height: 100vh;
padding: 2.5rem 2rem;
color: #1a1a1a;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.header {
margin-bottom: 2rem;
}
.header-row {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.375rem;
}
.pulse-dot {
width: 10px;
height: 10px;
background: #D97757;
border-radius: 50%;
animation: pulse 2.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.6; transform: scale(0.9); }
}
h1 {
font-size: 1.375rem;
font-weight: 700;
letter-spacing: -0.02em;
color: #1a1a1a;
}
.subtitle {
color: #8B8680;
font-size: 0.8125rem;
margin-left: 1.625rem;
letter-spacing: 0.01em;
}
.diagram-desc {
color: #B0AAA4;
font-size: 0.6875rem;
margin-top: 0.5rem;
margin-left: 1.625rem;
line-height: 1.5;
font-style: italic;
}
.diagram-container {
background: #FFFFFF;
border-radius: 12px;
border: 1px solid #E8E4DE;
padding: 1.5rem;
overflow-x: auto;
box-shadow: 0 1px 3px rgba(0,0,0,0.04), 0 4px 12px rgba(0,0,0,0.02);
}
svg {
width: 100%;
min-width: 900px;
display: block;
}
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1rem;
margin-top: 1.5rem;
}
.card {
background: #FFFFFF;
border-radius: 10px;
border: 1px solid #E8E4DE;
padding: 1.125rem 1.25rem;
box-shadow: 0 1px 2px rgba(0,0,0,0.03);
}
.card-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.625rem;
}
.card-dot {
width: 7px;
height: 7px;
border-radius: 50%;
}
.card-dot.terracotta { background: #D97757; }
.card-dot.sage { background: #5B8A72; }
.card-dot.slate { background: #6B7280; }
.card-dot.plum { background: #8B6F8A; }
.card-dot.rose { background: #C96464; }
.card h3 {
font-size: 0.8125rem;
font-weight: 600;
color: #1a1a1a;
}
.card ul {
list-style: none;
color: #6B6560;
font-size: 0.75rem;
line-height: 1.6;
}
.card li {
margin-bottom: 0.25rem;
}
.footer {
text-align: center;
margin-top: 1.5rem;
color: #B0AAA4;
font-size: 0.6875rem;
letter-spacing: 0.02em;
}
</style>
</head>
<body>
<div class="container">
<!-- Header -->
<div class="header">
<div class="header-row">
<div class="pulse-dot"></div>
<h1>NovaShield 风控系统架构</h1>
</div>
<p class="subtitle">CloudMart 电商平台 · 订单风险智能检测 · 5 阶段 Pipeline</p>
<p class="diagram-desc"><b>架构图 (Architecture Diagram)</b> — 结构性图形。描述软件系统的整体结构,包括组件、连接及其依赖关系。</p>
</div>
<!-- Main Diagram -->
<div class="diagram-container">
<svg viewBox="0 0 1100 920">
<!-- Definitions -->
<defs>
<marker id="arrowhead" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#B0AAA4" />
</marker>
<marker id="arrowhead-terracotta" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#D97757" />
</marker>
<marker id="arrowhead-rose" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#C96464" />
</marker>
<marker id="arrowhead-sage" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#5B8A72" />
</marker>
<pattern id="dots" width="24" height="24" patternUnits="userSpaceOnUse">
<circle cx="2" cy="2" r="0.6" fill="#E0DCD6"/>
</pattern>
</defs>
<!-- Background dot pattern -->
<rect width="100%" height="100%" fill="url(#dots)" />
<!-- ================================================================
TRIGGER SOURCES (Left side)
================================================================ -->
<!-- RabbitMQ Queue -->
<rect x="30" y="120" width="150" height="55" rx="8" fill="#FFF5F0" stroke="#D97757" stroke-width="1.2"/>
<text x="105" y="143" fill="#1a1a1a" font-size="11" font-weight="600" text-anchor="middle">RabbitMQ Queue</text>
<text x="105" y="159" fill="#D97757" font-size="9" font-weight="500" text-anchor="middle">orderRiskCheck</text>
<!-- Manual Trigger API -->
<rect x="30" y="200" width="150" height="55" rx="8" fill="#F5F8F6" stroke="#5B8A72" stroke-width="1.2"/>
<text x="105" y="223" fill="#1a1a1a" font-size="11" font-weight="600" text-anchor="middle">Manual Trigger</text>
<text x="105" y="239" fill="#8B8680" font-size="9" text-anchor="middle">JobID / Natural Lang</text>
<!-- Local Dir -->
<rect x="30" y="280" width="150" height="55" rx="8" fill="#F7F6F4" stroke="#C5C0BA" stroke-width="1.2"/>
<text x="105" y="303" fill="#1a1a1a" font-size="11" font-weight="600" text-anchor="middle">Local Dir</text>
<text x="105" y="319" fill="#8B8680" font-size="9" text-anchor="middle">SkipEnrichment=true</text>
<!-- ================================================================
INGEST & ROUTING
================================================================ -->
<!-- Subscriber -->
<rect x="240" y="110" width="150" height="75" rx="8" fill="#F5F8F6" stroke="#5B8A72" stroke-width="1.2"/>
<text x="315" y="133" fill="#1a1a1a" font-size="11" font-weight="600" text-anchor="middle">Subscriber</text>
<text x="315" y="149" fill="#8B8680" font-size="9" text-anchor="middle">ingest/rabbitmq</text>
<text x="315" y="165" fill="#8B8680" font-size="8" text-anchor="middle">AMQP TLS</text>
<text x="315" y="177" fill="#5B8A72" font-size="7" font-weight="500" text-anchor="middle">手动确认消息</text>
<!-- Dedup Store -->
<rect x="240" y="210" width="150" height="55" rx="8" fill="#F5F3F8" stroke="#8B6F8A" stroke-width="1.2"/>
<text x="315" y="233" fill="#1a1a1a" font-size="11" font-weight="600" text-anchor="middle">IdempotentStore</text>
<text x="315" y="249" fill="#8B8680" font-size="9" text-anchor="middle">Redis / Memory</text>
<!-- Trigger Router -->
<rect x="240" y="290" width="150" height="75" rx="8" fill="#F5F8F6" stroke="#5B8A72" stroke-width="1.2"/>
<text x="315" y="313" fill="#1a1a1a" font-size="11" font-weight="600" text-anchor="middle">TriggerRouter</text>
<text x="315" y="329" fill="#8B8680" font-size="9" text-anchor="middle">trigger</text>
<text x="315" y="345" fill="#8B8680" font-size="8" text-anchor="middle">4种触发 → ScanInput</text>
<text x="315" y="357" fill="#5B8A72" font-size="7" font-weight="500" text-anchor="middle">ParseMessage + Route</text>
<!-- ================================================================
6-STAGE PIPELINE (Main area)
================================================================ -->
<!-- Pipeline Boundary -->
<rect x="450" y="40" width="620" height="645" rx="12" fill="#FDFCFB" stroke="#5B8A72" stroke-width="1" stroke-dasharray="6,4"/>
<text x="464" y="58" fill="#5B8A72" font-size="10" font-weight="600">Pipeline</text>
<text x="530" y="58" fill="#8B8680" font-size="9">10min global timeout</text>
<!-- Stage 1: Fetch -->
<rect x="490" y="80" width="250" height="55" rx="8" fill="#F5F8F6" stroke="#5B8A72" stroke-width="1.2"/>
<text x="510" y="100" fill="#5B8A72" font-size="8" font-weight="600">STAGE 1</text>
<text x="615" y="103" fill="#1a1a1a" font-size="11" font-weight="600" text-anchor="middle">Fetch Order</text>
<text x="615" y="119" fill="#8B8680" font-size="9" text-anchor="middle">OrderClient · 30s polling · 5min</text>
<!-- Stage 2: Download -->
<rect x="490" y="155" width="250" height="55" rx="8" fill="#F5F8F6" stroke="#5B8A72" stroke-width="1.2"/>
<text x="510" y="175" fill="#5B8A72" font-size="8" font-weight="600">STAGE 2</text>
<text x="615" y="178" fill="#1a1a1a" font-size="11" font-weight="600" text-anchor="middle">Enrich Data</text>
<text x="615" y="194" fill="#8B8680" font-size="9" text-anchor="middle">DataFetcher · tar.gz · 10min</text>
<!-- Stage 3: Validate -->
<rect x="490" y="230" width="250" height="55" rx="8" fill="#F5F8F6" stroke="#5B8A72" stroke-width="1.2"/>
<text x="510" y="250" fill="#5B8A72" font-size="8" font-weight="600">STAGE 3</text>
<text x="615" y="253" fill="#1a1a1a" font-size="11" font-weight="600" text-anchor="middle">Validate</text>
<text x="615" y="269" fill="#8B8680" font-size="9" text-anchor="middle">behavior/ 必需 · 可选 fraud_behavior/profile/timeline</text>
<!-- Stage 4a/4b: Sequential -->
<rect x="490" y="310" width="250" height="140" rx="10" fill="#FAFBF9" stroke="#5B8A72" stroke-width="1" stroke-dasharray="4,3"/>
<text x="504" y="326" fill="#5B8A72" font-size="8" font-weight="600">STAGE 4 — sequential</text>
<rect x="510" y="335" width="210" height="45" rx="6" fill="#F5F8F6" stroke="#5B8A72" stroke-width="1.2"/>
<text x="530" y="355" fill="#5B8A72" font-size="7" font-weight="600">4a</text>
<text x="615" y="355" fill="#1a1a1a" font-size="10" font-weight="600" text-anchor="middle">Preprocess</text>
<text x="615" y="369" fill="#8B8680" font-size="8" text-anchor="middle">fraud_log:log:event:device = 8:4:2:1</text>
<!-- 4a → 4b arrow inside the group -->
<line x1="615" y1="380" x2="615" y2="393" stroke="#5B8A72" stroke-width="1" marker-end="url(#arrowhead-sage)"/>
<rect x="510" y="395" width="210" height="45" rx="6" fill="#F5F3F8" stroke="#8B6F8A" stroke-width="1.2"/>
<text x="530" y="415" fill="#8B6F8A" font-size="7" font-weight="600">4b</text>
<text x="615" y="415" fill="#1a1a1a" font-size="10" font-weight="600" text-anchor="middle">Rule Query</text>
<text x="615" y="429" fill="#8B8680" font-size="8" text-anchor="middle">GPT API · riskKB · 30min cache</text>
<!-- Stage 5: Risk Score -->
<rect x="490" y="470" width="250" height="70" rx="8" fill="#F5F8F6" stroke="#5B8A72" stroke-width="1.2"/>
<text x="510" y="490" fill="#5B8A72" font-size="8" font-weight="600">STAGE 5</text>
<text x="615" y="493" fill="#1a1a1a" font-size="11" font-weight="600" text-anchor="middle">Risk Score</text>
<text x="615" y="509" fill="#8B8680" font-size="9" text-anchor="middle">RiskScorer → Fallback</text>
<text x="615" y="523" fill="#8B8680" font-size="8" text-anchor="middle">GPT API · --format=json · 5min</text>
<!-- Stage 6: Alert -->
<rect x="490" y="560" width="250" height="55" rx="8" fill="#F5F8F6" stroke="#5B8A72" stroke-width="1.2"/>
<text x="510" y="580" fill="#5B8A72" font-size="8" font-weight="600">STAGE 6</text>
<text x="615" y="583" fill="#1a1a1a" font-size="11" font-weight="600" text-anchor="middle">Alert</text>
<text x="615" y="599" fill="#8B8680" font-size="9" text-anchor="middle">AlertClient · 3 retries</text>
<!-- Pipeline vertical arrows -->
<line x1="615" y1="135" x2="615" y2="153" stroke="#5B8A72" stroke-width="1.2" marker-end="url(#arrowhead-sage)"/>
<line x1="615" y1="210" x2="615" y2="228" stroke="#5B8A72" stroke-width="1.2" marker-end="url(#arrowhead-sage)"/>
<line x1="615" y1="285" x2="615" y2="308" stroke="#5B8A72" stroke-width="1.2" marker-end="url(#arrowhead-sage)"/>
<line x1="615" y1="450" x2="615" y2="468" stroke="#5B8A72" stroke-width="1.2" marker-end="url(#arrowhead-sage)"/>
<line x1="615" y1="540" x2="615" y2="558" stroke="#5B8A72" stroke-width="1.2" marker-end="url(#arrowhead-sage)"/>
<!-- ================================================================
EXTERNAL DEPENDENCIES (Right side)
================================================================ -->
<!-- OrderService -->
<rect x="800" y="80" width="170" height="55" rx="8" fill="#FFF5F0" stroke="#D97757" stroke-width="1.2"/>
<text x="885" y="103" fill="#1a1a1a" font-size="11" font-weight="600" text-anchor="middle">OrderService</text>
<text x="885" y="119" fill="#8B8680" font-size="9" text-anchor="middle">GET /order/:id/detail</text>
<!-- CustomerDB -->
<rect x="800" y="155" width="170" height="55" rx="8" fill="#FFF5F0" stroke="#D97757" stroke-width="1.2"/>
<text x="885" y="178" fill="#1a1a1a" font-size="11" font-weight="600" text-anchor="middle">CustomerDB</text>
<text x="885" y="194" fill="#8B8680" font-size="9" text-anchor="middle">交易数据 CSV 导出</text>
<!-- GPT API (shared by Rules + Scoring) -->
<rect x="800" y="380" width="170" height="80" rx="8" fill="#F5F8F6" stroke="#5B8A72" stroke-width="1.2"/>
<text x="885" y="403" fill="#1a1a1a" font-size="11" font-weight="600" text-anchor="middle">GPT API</text>
<text x="885" y="419" fill="#8B8680" font-size="9" text-anchor="middle">AI 推理引擎</text>
<text x="885" y="435" fill="#5B8A72" font-size="8" text-anchor="middle">--format=json</text>
<text x="885" y="449" fill="#5B8A72" font-size="8" text-anchor="middle">--auto-approve</text>
<!-- Rules Knowledge Base -->
<rect x="800" y="480" width="170" height="45" rx="8" fill="#F5F3F8" stroke="#8B6F8A" stroke-width="1.2"/>
<text x="885" y="500" fill="#1a1a1a" font-size="11" font-weight="600" text-anchor="middle">Risk KB</text>
<text x="885" y="514" fill="#8B8680" font-size="9" text-anchor="middle">risk-rules-kb</text>
<!-- Fallback -->
<rect x="800" y="540" width="170" height="45" rx="8" fill="#FFF5F5" stroke="#C96464" stroke-width="1.2"/>
<text x="885" y="560" fill="#1a1a1a" font-size="11" font-weight="600" text-anchor="middle">Fallback</text>
<text x="885" y="574" fill="#8B8680" font-size="9" text-anchor="middle">rule_engine.py</text>
<!-- AlertService -->
<rect x="800" y="600" width="170" height="55" rx="8" fill="#FFF5F0" stroke="#D97757" stroke-width="1.2"/>
<text x="885" y="623" fill="#1a1a1a" font-size="11" font-weight="600" text-anchor="middle">AlertService</text>
<text x="885" y="639" fill="#8B8680" font-size="9" text-anchor="middle">POST /risk/alert</text>
<!-- ================================================================
CONNECTION ARROWS
================================================================ -->
<!-- RabbitMQ → Subscriber -->
<line x1="180" y1="148" x2="238" y2="148" stroke="#D97757" stroke-width="1.2" marker-end="url(#arrowhead-terracotta)"/>
<text x="209" y="142" fill="#8B8680" font-size="8" text-anchor="middle">SASL</text>
<!-- Subscriber ↔ Dedup -->
<line x1="315" y1="185" x2="315" y2="208" stroke="#8B6F8A" stroke-width="1.2" marker-end="url(#arrowhead)"/>
<!-- Manual → Router -->
<line x1="180" y1="228" x2="238" y2="310" stroke="#5B8A72" stroke-width="1.2" marker-end="url(#arrowhead-sage)"/>
<!-- Local → Router -->
<line x1="180" y1="308" x2="238" y2="328" stroke="#B0AAA4" stroke-width="1.2" marker-end="url(#arrowhead)"/>
<!-- Subscriber → Router (bypass IdempotentStore to the left) -->
<path d="M 240 148 H 208 Q 196 148 196 160 V 316 Q 196 328 208 328 H 238" fill="none" stroke="#5B8A72" stroke-width="1.2" marker-end="url(#arrowhead-sage)"/>
<!-- Router → Pipeline -->
<path d="M 390 328 L 440 328 Q 460 328 460 308 L 460 108 Q 460 88 480 88 L 488 88" fill="none" stroke="#5B8A72" stroke-width="1.2" marker-end="url(#arrowhead-sage)"/>
<text x="425" y="290" fill="#5B8A72" font-size="8" font-weight="500">ScanInput</text>
<!-- Fetch → OrderService -->
<line x1="740" y1="108" x2="798" y2="108" stroke="#D97757" stroke-width="1.2" marker-end="url(#arrowhead-terracotta)"/>
<text x="769" y="102" fill="#8B8680" font-size="8" text-anchor="middle">REST</text>
<!-- Download → CustomerDB -->
<line x1="740" y1="183" x2="798" y2="183" stroke="#D97757" stroke-width="1.2" marker-end="url(#arrowhead-terracotta)"/>
<text x="769" y="177" fill="#8B8680" font-size="8" text-anchor="middle">HTTP</text>
<!-- Rules → GPT API -->
<line x1="720" y1="418" x2="798" y2="418" stroke="#8B6F8A" stroke-width="1.2" marker-end="url(#arrowhead)"/>
<!-- Risk Score → GPT API -->
<line x1="740" y1="498" x2="798" y2="448" stroke="#5B8A72" stroke-width="1.2" marker-end="url(#arrowhead-sage)"/>
<!-- GPT API → Risk KB -->
<line x1="885" y1="460" x2="885" y2="478" stroke="#8B6F8A" stroke-width="1" marker-end="url(#arrowhead)"/>
<text x="900" y="472" fill="#8B8680" font-size="7">wiki/index.md</text>
<!-- Risk Score → Fallback (dashed) -->
<path d="M 740 520 L 770 520 Q 780 520 780 540 L 780 558 L 798 558" fill="none" stroke="#C96464" stroke-width="1" stroke-dasharray="4,3" marker-end="url(#arrowhead-rose)"/>
<text x="792" y="548" fill="#C96464" font-size="7" font-weight="500">fallback</text>
<!-- Alert → AlertService -->
<line x1="740" y1="588" x2="798" y2="628" stroke="#D97757" stroke-width="1.2" marker-end="url(#arrowhead-terracotta)"/>
<!-- ================================================================
DATA FLOW / MESSAGE FORMAT (Bottom area)
================================================================ -->
<!-- RabbitMQ Message Format -->
<rect x="30" y="400" width="380" height="140" rx="10" fill="#FDFCFB" stroke="#E8E4DE" stroke-width="1"/>
<text x="50" y="422" fill="#D97757" font-size="10" font-weight="600">RabbitMQ Message</text>
<text x="50" y="440" fill="#6B6560" font-size="8">{"region", "queue", "jobId",</text>
<text x="50" y="454" fill="#6B6560" font-size="8"> "tenant", "version", "triggerType",</text>
<text x="50" y="468" fill="#6B6560" font-size="8"> "kind", "region", "userNotes"}</text>
<text x="50" y="490" fill="#B0AAA4" font-size="8">triggerType: fail | ft_restart | ft_resubmit</text>
<text x="50" y="504" fill="#B0AAA4" font-size="8"> | ft_replace | manual</text>
<text x="50" y="522" fill="#B0AAA4" font-size="8">kind: pytorch | mpijob | rayjob</text>
<text x="50" y="536" fill="#B0AAA4" font-size="8">SASL: PLAIN (可选)</text>
<!-- RiskResult Format -->
<rect x="30" y="560" width="380" height="130" rx="10" fill="#FDFCFB" stroke="#E8E4DE" stroke-width="1"/>
<text x="50" y="582" fill="#5B8A72" font-size="10" font-weight="600">RiskResult</text>
<text x="50" y="600" fill="#6B6560" font-size="8">risk_category string 必填 风险大类</text>
<text x="50" y="614" fill="#6B6560" font-size="8">risk_subtype string 必填 风险子类</text>
<text x="50" y="628" fill="#6B6560" font-size="8">is_known_pattern bool 必填 true/false</text>
<text x="50" y="642" fill="#6B6560" font-size="8">confidence int 必填 0-5</text>
<text x="50" y="656" fill="#6B6560" font-size="8">evidence string 必填 关键证据</text>
<text x="50" y="670" fill="#6B6560" font-size="8">root_cause string 选填 原因分析</text>
<text x="50" y="684" fill="#6B6560" font-size="8">remediation string 必填 处置建议</text>
<!-- Pipeline Result / Status -->
<rect x="30" y="710" width="380" height="85" rx="10" fill="#FDFCFB" stroke="#E8E4DE" stroke-width="1"/>
<text x="50" y="732" fill="#6B7280" font-size="10" font-weight="600">Pipeline Result</text>
<text x="50" y="750" fill="#6B6560" font-size="8">Status: success | failed | timeout</text>
<text x="50" y="764" fill="#6B6560" font-size="8">TraceID: {jobID}-{unixMilli}</text>
<text x="50" y="778" fill="#6B6560" font-size="8">ScanStatus: 1=扫描中 2=扫描完成 3=扫描失败</text>
<text x="50" y="792" fill="#6B6560" font-size="8">Platform: web | mobile | api</text>
<!-- ================================================================
MAIN ENTRY
================================================================ -->
<rect x="30" y="60" width="150" height="40" rx="8" fill="#F5F8F6" stroke="#5B8A72" stroke-width="1.2"/>
<text x="105" y="83" fill="#1a1a1a" font-size="11" font-weight="600" text-anchor="middle">nova-shield</text>
<text x="105" y="95" fill="#8B8680" font-size="8" text-anchor="middle">cmd/nova-shield</text>
<!-- nova-shield → Subscriber -->
<line x1="180" y1="80" x2="238" y2="130" stroke="#5B8A72" stroke-width="1" stroke-dasharray="3,3" marker-end="url(#arrowhead-sage)"/>
<!-- nova-shield → Router -->
<line x1="180" y1="85" x2="238" y2="320" stroke="#5B8A72" stroke-width="1" stroke-dasharray="3,3" marker-end="url(#arrowhead-sage)"/>
<!-- ================================================================
LEGEND
================================================================ -->
<text x="30" y="850" fill="#1a1a1a" font-size="10" font-weight="600">Legend</text>
<rect x="30" y="862" width="16" height="10" rx="3" fill="#FFF5F0" stroke="#D97757" stroke-width="1"/>
<text x="52" y="870" fill="#6B6560" font-size="8">Message Bus (RabbitMQ)</text>
<rect x="180" y="862" width="16" height="10" rx="3" fill="#F5F8F6" stroke="#5B8A72" stroke-width="1"/>
<text x="202" y="870" fill="#6B6560" font-size="8">Backend Service</text>
<rect x="330" y="862" width="16" height="10" rx="3" fill="#FFF5F0" stroke="#D97757" stroke-width="1"/>
<text x="352" y="870" fill="#6B6560" font-size="8">External API (OrderService)</text>
<rect x="490" y="862" width="16" height="10" rx="3" fill="#F5F3F8" stroke="#8B6F8A" stroke-width="1"/>
<text x="512" y="870" fill="#6B6560" font-size="8">Storage / Rules</text>
<rect x="610" y="862" width="16" height="10" rx="3" fill="#FFF5F5" stroke="#C96464" stroke-width="1"/>
<text x="632" y="870" fill="#6B6560" font-size="8">Fallback</text>
<rect x="710" y="862" width="16" height="10" rx="3" fill="#F7F6F4" stroke="#C5C0BA" stroke-width="1"/>
<text x="732" y="870" fill="#6B6560" font-size="8">Generic</text>
<line x1="800" y1="867" x2="816" y2="867" stroke="#C96464" stroke-width="1" stroke-dasharray="3,3"/>
<text x="822" y="870" fill="#6B6560" font-size="8">Fallback Flow</text>
<rect x="910" y="862" width="16" height="10" rx="3" fill="transparent" stroke="#5B8A72" stroke-width="1" stroke-dasharray="3,3"/>
<text x="932" y="870" fill="#6B6560" font-size="8">Pipeline Boundary</text>
</svg>
</div>
<!-- Info Cards -->
<div class="cards">
<div class="card">
<div class="card-header">
<div class="card-dot terracotta"></div>
<h3>订单入口</h3>
</div>
<ul>
<li>• RabbitMQ Queue: orderRiskCheck</li>
<li>• AMQP TLS 认证 (可选)</li>
<li>• 手动确认消息 (prefetchCount=1)</li>
<li>• 4种触发: RabbitMQ / OrderID / Query / CSV</li>
<li>• context.WithoutCancel 保证优雅关闭</li>
</ul>
</div>
<div class="card">
<div class="card-header">
<div class="card-dot sage"></div>
<h3>Pipeline 6 阶段</h3>
</div>
<ul>
<li>• Fetch → Enrich → Validate → Extract → Rules → Score → Alert</li>
<li>• 全局 5min 超时, 各阶段独立 timeout</li>
<li>• Stage 4 顺序: 先预处理,再 Rules (使用预处理结果)</li>
<li>• Rules + Scoring 共享 GPT API</li>
<li>• 任意非 alert 阶段失败走 fail() 统一出口</li>
</ul>
</div>
<div class="card">
<div class="card-header">
<div class="card-dot plum"></div>
<h3>去重 & AI 依赖</h3>
</div>
<ul>
<li>• IdempotencyKey: nova:risk:{orderId}:{version}</li>
<li>• Redis SET 优先, 降级 MemoryIdempotentStore</li>
<li>• CheckAndMark → MarkComplete 防重复消费</li>
<li>• GPT API: 风控评分 + 规则查询</li>
<li>• Fallback: rule_engine.py 降级分析</li>
</ul>
</div>
</div>
<!-- Footer -->
<p class="footer">
nova-shield · CloudMart 电商平台订单风控 · github.com/novashield/risk-engine
</p>
</div>
</body>
</html>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NovaShield 风控类图</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Geist', sans-serif;
background: #faf6f0;
min-height: 100vh;
padding: 2.5rem 2rem;
color: #1c1917;
}
.container { max-width: 1120px; margin: 0 auto; }
.header { margin-bottom: 2rem; }
.eyebrow {
font-family: 'Geist Mono', monospace;
font-size: 0.6875rem;
font-weight: 500;
letter-spacing: 0.18em;
text-transform: uppercase;
color: #78716c;
margin-bottom: 0.375rem;
}
h1 {
font-family: 'Instrument Serif', serif;
font-size: 1.75rem;
font-weight: 400;
color: #1c1917;
}
.subtitle {
color: #78716c;
font-size: 0.8125rem;
margin-top: 0.375rem;
}
.diagram-desc {
color: #a8a29e;
font-size: 0.6875rem;
margin-top: 0.5rem;
line-height: 1.5;
font-style: italic;
}
.diagram-container {
background: #f0ebe3;
border: 1px solid rgba(28,25,23,0.12);
border-radius: 8px;
padding: 1.5rem;
overflow-x: auto;
}
svg { width: 100%; min-width: 880px; display: block; }
.cards {
display: grid;
grid-template-columns: 1.2fr 0.8fr;
gap: 1rem;
margin-top: 1.5rem;
}
.card {
background: #ffffff;
border: 1px solid rgba(28,25,23,0.12);
border-radius: 6px;
padding: 1.25rem;
}
.card-eyebrow {
font-family: 'Geist Mono', monospace;
font-size: 0.625rem;
font-weight: 500;
letter-spacing: 0.18em;
text-transform: uppercase;
color: #a8a29e;
margin-bottom: 0.5rem;
}
.card-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.card-dot { width: 7px; height: 7px; border-radius: 50%; }
.card-dot.ink { background: #1c1917; }
.card-dot.accent { background: #d97706; }
.card-dot.muted { background: #78716c; }
.card h3 { font-size: 0.8125rem; font-weight: 600; color: #1c1917; }
.card ul {
list-style: none;
color: #78716c;
font-size: 0.75rem;
line-height: 1.6;
}
.card li { margin-bottom: 0.25rem; }
.footer {
margin-top: 1.5rem;
padding-top: 0.75rem;
border-top: 1px solid rgba(28,25,23,0.08);
text-align: center;
font-family: 'Geist Mono', monospace;
color: #a8a29e;
font-size: 0.625rem;
letter-spacing: 0.04em;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<p class="eyebrow">Class Diagram</p>
<h1>NovaShield 风控类图</h1>
<p class="subtitle">8 个核心类型及其字段、方法与组合关系,覆盖输入→处理→输出全链路</p>
<p class="diagram-desc"><b>类图 (Class Diagram)</b> — 结构性图形。核心图,展现系统的静态结构,包括类、接口、属性和关系。</p>
</div>
<div class="diagram-container">
<svg viewBox="0 0 1080 690">
<defs>
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#78716c"/>
</marker>
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#d97706"/>
</marker>
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.9" fill="rgba(28,25,23,0.10)"/>
</pattern>
</defs>
<!-- Background -->
<rect width="100%" height="100%" fill="#faf6f0"/>
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.6"/>
<!-- ============================================================
RELATIONSHIP LINES (drawn first — z-order behind boxes)
============================================================ -->
<!-- ScanInput → OrderMessage (association: message *msg) -->
<line x1="272" y1="216" x2="272" y2="192" stroke="#78716c" stroke-width="1" marker-end="url(#arrow)"/>
<rect x="280" y="198" width="40" height="12" rx="2" fill="#faf6f0"/>
<text x="300" y="208" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">0..1</text>
<!-- ScanResult → StageResult (composition: stageResults map) — L-path -->
<path d="M 636 232 H 660 Q 672 232 672 244 V 334 Q 672 346 684 346 H 776" fill="none" stroke="#78716c" stroke-width="1" marker-end="url(#arrow)"/>
<rect x="624" y="283" width="36" height="12" rx="2" fill="#faf6f0"/>
<text x="642" y="293" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">1..*</text>
<!-- Composition diamond -->
<polygon points="620,232 628,226 636,232 628,238" fill="#78716c"/>
<!-- AlertReport → RiskResult (composition: results []) -->
<line x1="280" y1="420" x2="440" y2="420" stroke="#d97706" stroke-width="1.2" marker-end="url(#arrow-accent)"/>
<rect x="340" y="408" width="28" height="12" rx="2" fill="#faf6f0"/>
<text x="354" y="418" fill="#d97706" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">1..*</text>
<!-- Composition diamond -->
<polygon points="280,420 288,414 296,420 288,426" fill="#d97706"/>
<!-- AlertReport → AlertNote (composition: warnings []) -->
<line x1="152" y1="508" x2="152" y2="556" stroke="#78716c" stroke-width="1" marker-end="url(#arrow)"/>
<rect x="160" y="522" width="28" height="12" rx="2" fill="#faf6f0"/>
<text x="174" y="532" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">0..*</text>
<!-- Composition diamond -->
<polygon points="152,492 146,500 152,508 158,500" fill="#78716c"/>
<!-- OrderInfoResponse → DBConfig (composition) -->
<line x1="840" y1="124" x2="840" y2="200" stroke="#78716c" stroke-width="1" marker-end="url(#arrow)"/>
<rect x="848" y="152" width="20" height="12" rx="2" fill="#faf6f0"/>
<text x="858" y="162" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">0..1</text>
<!-- Composition diamond -->
<polygon points="840,108 834,116 840,124 846,116" fill="#78716c"/>
<!-- ============================================================
CLASS BOXES (drawn after lines — z-order above)
============================================================ -->
<!-- ── OrderMessage (row 1, left) ── -->
<rect x="40" y="12" width="232" height="180" rx="6" fill="#faf6f0"/>
<rect x="40" y="12" width="232" height="24" rx="6" fill="rgba(28,25,23,0.04)"/>
<rect x="40" y="32" width="232" height="4" fill="rgba(28,25,23,0.04)"/>
<rect x="40" y="12" width="232" height="180" rx="6" fill="none" stroke="#1c1917" stroke-width="1"/>
<line x1="40" y1="36" x2="272" y2="36" stroke="rgba(28,25,23,0.12)" stroke-width="0.8"/>
<!-- Header -->
<rect x="48" y="16" width="52" height="12" rx="2" fill="transparent" stroke="rgba(28,25,23,0.40)" stroke-width="0.8"/>
<text x="74" y="25" fill="rgba(28,25,23,0.80)" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">STRUCT</text>
<text x="156" y="28" fill="#1c1917" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">OrderMessage</text>
<!-- Fields -->
<text x="52" y="52" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace">region string</text>
<text x="52" y="68" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace">queue string</text>
<text x="52" y="84" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace">jobId string</text>
<text x="52" y="100" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace">tenant string</text>
<text x="52" y="116" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace">version string</text>
<text x="52" y="132" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace">triggerType string</text>
<text x="52" y="148" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace">kind string</text>
<text x="52" y="164" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace">region string</text>
<text x="52" y="180" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace">userNotes string</text>
<!-- ── ScanInput (row 1, center-left) ── -->
<rect x="40" y="216" width="232" height="148" rx="6" fill="#faf6f0"/>
<rect x="40" y="216" width="232" height="24" rx="6" fill="rgba(28,25,23,0.04)"/>
<rect x="40" y="236" width="232" height="4" fill="rgba(28,25,23,0.04)"/>
<rect x="40" y="216" width="232" height="148" rx="6" fill="none" stroke="#1c1917" stroke-width="1"/>
<line x1="40" y1="240" x2="272" y2="240" stroke="rgba(28,25,23,0.12)" stroke-width="0.8"/>
<rect x="48" y="220" width="32" height="12" rx="2" fill="transparent" stroke="rgba(28,25,23,0.40)" stroke-width="0.8"/>
<text x="64" y="229" fill="rgba(28,25,23,0.80)" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">IN</text>
<text x="156" y="232" fill="#1c1917" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">ScanInput</text>
<text x="52" y="256" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace">→ message *OrderMessage</text>
<text x="52" y="272" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace"> jobId string</text>
<text x="52" y="288" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace"> version string</text>
<text x="52" y="304" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace"> workDir string</text>
<text x="52" y="320" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace"> skipEnrichment bool</text>
<text x="52" y="336" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace"> tenant string</text>
<text x="52" y="352" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace"> region string</text>
<!-- ── OrderInfoResponse (row 1, right) ── -->
<rect x="720" y="12" width="240" height="96" rx="6" fill="#faf6f0"/>
<rect x="720" y="12" width="240" height="24" rx="6" fill="rgba(28,25,23,0.04)"/>
<rect x="720" y="32" width="240" height="4" fill="rgba(28,25,23,0.04)"/>
<rect x="720" y="12" width="240" height="96" rx="6" fill="none" stroke="#1c1917" stroke-width="1"/>
<line x1="720" y1="36" x2="960" y2="36" stroke="rgba(28,25,23,0.12)" stroke-width="0.8"/>
<rect x="728" y="16" width="52" height="12" rx="2" fill="transparent" stroke="rgba(28,25,23,0.40)" stroke-width="0.8"/>
<text x="754" y="25" fill="rgba(28,25,23,0.80)" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">STRUCT</text>
<text x="840" y="28" fill="#1c1917" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">OrderInfoResponse</text>
<text x="732" y="52" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace">→ storageConfig *DBConfig</text>
<text x="732" y="68" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace"> status string</text>
<text x="732" y="84" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace"> replicas int</text>
<text x="732" y="100" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace"> payMethod string</text>
<!-- ── DBConfig (row 2, far right) ── -->
<rect x="744" y="200" width="192" height="84" rx="6" fill="rgba(28,25,23,0.05)"/>
<rect x="744" y="200" width="192" height="24" rx="6" fill="rgba(28,25,23,0.05)"/>
<rect x="744" y="220" width="192" height="4" fill="rgba(28,25,23,0.05)"/>
<rect x="744" y="200" width="192" height="84" rx="6" fill="none" stroke="#78716c" stroke-width="1"/>
<line x1="744" y1="224" x2="936" y2="224" stroke="rgba(28,25,23,0.12)" stroke-width="0.8"/>
<rect x="752" y="204" width="44" height="12" rx="2" fill="transparent" stroke="rgba(120,113,108,0.40)" stroke-width="0.8"/>
<text x="774" y="213" fill="rgba(120,113,108,0.80)" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">STORE</text>
<text x="840" y="216" fill="#1c1917" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">DBConfig</text>
<text x="756" y="240" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace">region string</text>
<text x="756" y="256" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace">ak string</text>
<text x="756" y="272" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace">sk string</text>
<!-- ── ScanResult (row 2, center) ── -->
<rect x="380" y="156" width="240" height="112" rx="6" fill="#faf6f0"/>
<rect x="380" y="156" width="240" height="24" rx="6" fill="rgba(28,25,23,0.04)"/>
<rect x="380" y="176" width="240" height="4" fill="rgba(28,25,23,0.04)"/>
<rect x="380" y="156" width="240" height="112" rx="6" fill="none" stroke="#1c1917" stroke-width="1"/>
<line x1="380" y1="180" x2="620" y2="180" stroke="rgba(28,25,23,0.12)" stroke-width="0.8"/>
<rect x="388" y="160" width="32" height="12" rx="2" fill="transparent" stroke="rgba(28,25,23,0.40)" stroke-width="0.8"/>
<text x="404" y="169" fill="rgba(28,25,23,0.80)" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">OUT</text>
<text x="500" y="172" fill="#1c1917" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">ScanResult</text>
<text x="392" y="196" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace">jobId string</text>
<text x="392" y="212" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace">traceID string</text>
<text x="392" y="228" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace">status string</text>
<text x="392" y="244" fill="#d97706" font-size="9" font-family="'Geist Mono', monospace">→ riskResults []RiskResult</text>
<text x="392" y="260" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace">→ stageResults map[string]*StageResult</text>
<!-- ── StageResult (row 2-3, far right) ── -->
<rect x="776" y="304" width="192" height="84" rx="6" fill="rgba(28,25,23,0.05)"/>
<rect x="776" y="304" width="192" height="24" rx="6" fill="rgba(28,25,23,0.05)"/>
<rect x="776" y="324" width="192" height="4" fill="rgba(28,25,23,0.05)"/>
<rect x="776" y="304" width="192" height="84" rx="6" fill="none" stroke="#78716c" stroke-width="1"/>
<line x1="776" y1="328" x2="968" y2="328" stroke="rgba(28,25,23,0.12)" stroke-width="0.8"/>
<rect x="784" y="308" width="40" height="12" rx="2" fill="transparent" stroke="rgba(120,113,108,0.40)" stroke-width="0.8"/>
<text x="804" y="317" fill="rgba(120,113,108,0.80)" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">VALUE</text>
<text x="872" y="320" fill="#1c1917" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">StageResult</text>
<text x="788" y="344" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace">name string</text>
<text x="788" y="360" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace">status string</text>
<text x="788" y="376" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace">duration time.Duration</text>
<!-- ── RiskResult (FOCAL — accent, row 3 center) ── -->
<rect x="440" y="320" width="280" height="180" rx="6" fill="#faf6f0"/>
<rect x="440" y="320" width="280" height="24" rx="6" fill="rgba(217,119,6,0.10)"/>
<rect x="440" y="340" width="280" height="4" fill="rgba(217,119,6,0.10)"/>
<rect x="440" y="320" width="280" height="180" rx="6" fill="rgba(217,119,6,0.06)" stroke="#d97706" stroke-width="1.2"/>
<line x1="440" y1="344" x2="720" y2="344" stroke="rgba(217,119,6,0.25)" stroke-width="0.8"/>
<rect x="448" y="324" width="32" height="12" rx="2" fill="transparent" stroke="rgba(217,119,6,0.40)" stroke-width="0.8"/>
<text x="464" y="333" fill="rgba(217,119,6,0.80)" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">CORE</text>
<text x="580" y="336" fill="#1c1917" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">RiskResult</text>
<text x="452" y="360" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace"># risk_category string</text>
<text x="452" y="376" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace"># risk_subtype string</text>
<text x="452" y="392" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace"># is_known_pattern bool</text>
<text x="452" y="408" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace"># confidence int (0-5)</text>
<text x="452" y="424" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace"># evidence string</text>
<text x="452" y="440" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace"># remediation string</text>
<text x="452" y="456" fill="#a8a29e" font-size="9" font-family="'Geist Mono', monospace"> risk_detail string</text>
<text x="452" y="472" fill="#a8a29e" font-size="9" font-family="'Geist Mono', monospace"> extra_metadata map[string]string</text>
<!-- Method section separator -->
<line x1="440" y1="476" x2="720" y2="476" stroke="rgba(217,119,6,0.20)" stroke-width="0.8"/>
<text x="452" y="492" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace">+ Validate() error</text>
<!-- ── AlertReport (row 3, left) ── -->
<rect x="40" y="380" width="240" height="112" rx="6" fill="#faf6f0"/>
<rect x="40" y="380" width="240" height="24" rx="6" fill="rgba(28,25,23,0.04)"/>
<rect x="40" y="400" width="240" height="4" fill="rgba(28,25,23,0.04)"/>
<rect x="40" y="380" width="240" height="112" rx="6" fill="none" stroke="#1c1917" stroke-width="1"/>
<line x1="40" y1="404" x2="280" y2="404" stroke="rgba(28,25,23,0.12)" stroke-width="0.8"/>
<rect x="48" y="384" width="52" height="12" rx="2" fill="transparent" stroke="rgba(28,25,23,0.40)" stroke-width="0.8"/>
<text x="74" y="393" fill="rgba(28,25,23,0.80)" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">STRUCT</text>
<text x="160" y="396" fill="#1c1917" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">AlertReport</text>
<text x="52" y="420" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace"># jobId string</text>
<text x="52" y="436" fill="#d97706" font-size="9" font-family="'Geist Mono', monospace">→ results []RiskResult</text>
<text x="52" y="452" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace">→ warnings []AlertNote</text>
<text x="52" y="468" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace"> createdAt time.Time</text>
<line x1="40" y1="476" x2="280" y2="476" stroke="rgba(28,25,23,0.12)" stroke-width="0.8"/>
<text x="52" y="488" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace">+ Validate() error</text>
<!-- ── AlertNote (row 4, left) ── -->
<rect x="56" y="556" width="192" height="68" rx="6" fill="rgba(28,25,23,0.05)"/>
<rect x="56" y="556" width="192" height="24" rx="6" fill="rgba(28,25,23,0.05)"/>
<rect x="56" y="576" width="192" height="4" fill="rgba(28,25,23,0.05)"/>
<rect x="56" y="556" width="192" height="68" rx="6" fill="none" stroke="#78716c" stroke-width="1"/>
<line x1="56" y1="580" x2="248" y2="580" stroke="rgba(28,25,23,0.12)" stroke-width="0.8"/>
<rect x="64" y="560" width="44" height="12" rx="2" fill="transparent" stroke="rgba(120,113,108,0.40)" stroke-width="0.8"/>
<text x="86" y="569" fill="rgba(120,113,108,0.80)" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">VALUE</text>
<text x="152" y="572" fill="#1c1917" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">AlertNote</text>
<text x="68" y="596" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace">type string</text>
<text x="68" y="612" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace">message string</text>
<!-- ============================================================
LEGEND (horizontal strip at bottom)
============================================================ -->
<line x1="30" y1="644" x2="1050" y2="644" stroke="rgba(28,25,23,0.10)" stroke-width="0.8"/>
<text x="30" y="668" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">LEGEND</text>
<!-- Focal class -->
<rect x="100" y="656" width="24" height="16" rx="4" fill="rgba(217,119,6,0.06)" stroke="#d97706" stroke-width="0.8"/>
<text x="132" y="668" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace">核心类</text>
<!-- Normal class -->
<rect x="200" y="656" width="24" height="16" rx="4" fill="#ffffff" stroke="#1c1917" stroke-width="0.8"/>
<text x="232" y="668" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace">普通类</text>
<!-- Value class -->
<rect x="300" y="656" width="24" height="16" rx="4" fill="rgba(28,25,23,0.05)" stroke="#78716c" stroke-width="0.8"/>
<text x="332" y="668" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace">值类型</text>
<!-- Composition -->
<polygon points="412,672 404,664 412,656 420,664" fill="#78716c"/>
<line x1="420" y1="664" x2="440" y2="664" stroke="#78716c" stroke-width="1" marker-end="url(#arrow)"/>
<text x="448" y="668" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace">组合</text>
<!-- Required field -->
<text x="512" y="668" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace"># 必填</text>
<!-- Optional field -->
<text x="572" y="668" fill="#a8a29e" font-size="8" font-family="'Geist Mono', monospace"> 选填</text>
<!-- Reference -->
<text x="632" y="668" fill="#d97706" font-size="8" font-family="'Geist Mono', monospace">→ 引用</text>
<!-- Cardinality -->
<text x="700" y="668" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace">0..1 / 1..* 基数</text>
</svg>
</div>
<!-- Summary Cards -->
<div class="cards">
<div class="card">
<p class="card-eyebrow">RELATIONSHIPS</p>
<div class="card-header">
<div class="card-dot ink"></div>
<h3>6 条组合关系</h3>
</div>
<ul>
<li>• ScanInput → OrderMessage (0..1, RabbitMQ 路径持有引用)</li>
<li>• ScanResult → RiskResult (1..*, 风控产出)</li>
<li>• ScanResult → StageResult (1..*, 每阶段执行记录)</li>
<li>• AlertReport → RiskResult (1..*, 按 confidence 降序)</li>
<li>• AlertReport → AlertNote (0..*, unknown_code 等)</li>
<li>• OrderInfoResponse → DBConfig (0..1, 数据库配置)</li>
</ul>
</div>
<div class="card">
<p class="card-eyebrow">RISKRESULT</p>
<div class="card-header">
<div class="card-dot accent"></div>
<h3>核心风控结果</h3>
</div>
<ul>
<li>• 6 个必填字段 (# 前缀) + 2 个选填</li>
<li>• is_known_pattern: bool (true=已知, false=未知)</li>
<li>• confidence: 0-5 整数, 0 合法</li>
<li>• Validate() 校验必填 + credibility 范围</li>
<li>• JSON tag 使用 snake_case</li>
</ul>
</div>
</div>
<div class="footer">
nova-shield · github.com/novashield/risk-engine
</div>
</div>
</body>
</html>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NovaShield 风控通信图</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Geist', sans-serif;
background: #faf6f0;
min-height: 100vh;
padding: 2.5rem 2rem;
color: #1c1917;
}
.container { max-width: 960px; margin: 0 auto; }
.header { margin-bottom: 2rem; }
.eyebrow {
font-family: 'Geist Mono', monospace;
font-size: 0.6875rem;
font-weight: 500;
letter-spacing: 0.18em;
text-transform: uppercase;
color: #78716c;
margin-bottom: 0.375rem;
}
h1 {
font-family: 'Instrument Serif', serif;
font-size: 1.75rem;
font-weight: 400;
color: #1c1917;
}
.subtitle {
color: #78716c;
font-size: 0.8125rem;
margin-top: 0.375rem;
}
.diagram-desc {
color: #a8a29e;
font-size: 0.6875rem;
margin-top: 0.5rem;
line-height: 1.5;
font-style: italic;
}
.diagram-container {
background: #f0ebe3;
border: 1px solid rgba(28,25,23,0.12);
border-radius: 8px;
padding: 1.5rem;
overflow-x: auto;
}
svg { width: 100%; min-width: 780px; display: block; }
.cards {
display: grid;
grid-template-columns: 1.2fr 0.8fr;
gap: 1rem;
margin-top: 1.5rem;
}
.card {
background: #ffffff;
border: 1px solid rgba(28,25,23,0.12);
border-radius: 6px;
padding: 1.25rem;
}
.card-eyebrow {
font-family: 'Geist Mono', monospace;
font-size: 0.625rem;
font-weight: 500;
letter-spacing: 0.18em;
text-transform: uppercase;
color: #a8a29e;
margin-bottom: 0.5rem;
}
.card-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.card-dot { width: 7px; height: 7px; border-radius: 50%; }
.card-dot.ink { background: #1c1917; }
.card-dot.accent { background: #d97706; }
.card-dot.muted { background: #78716c; }
.card-dot.link { background: #c05621; }
.card h3 { font-size: 0.8125rem; font-weight: 600; color: #1c1917; }
.card ul {
list-style: none;
color: #78716c;
font-size: 0.75rem;
line-height: 1.6;
}
.card li { margin-bottom: 0.25rem; }
.footer {
margin-top: 1.5rem;
padding-top: 0.75rem;
border-top: 1px solid rgba(28,25,23,0.08);
text-align: center;
font-family: 'Geist Mono', monospace;
color: #a8a29e;
font-size: 0.625rem;
letter-spacing: 0.04em;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<p class="eyebrow">Communication Diagram</p>
<h1>诊断通信协作</h1>
<p class="subtitle">9 个参与者 × 8 条编号消息,展示 Pipeline 运行时对象间的协作顺序与结构关系</p>
<p class="diagram-desc"><b>通信图 (Communication Diagram)</b> — 行为性图形·交互图。侧重于参与交互的对象间的结构组织,展示协作与消息传递。</p>
</div>
<div class="diagram-container">
<svg viewBox="0 0 960 496">
<defs>
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#78716c"/>
</marker>
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#d97706"/>
</marker>
<marker id="arrow-link" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#c05621"/>
</marker>
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.9" fill="rgba(28,25,23,0.10)"/>
</pattern>
</defs>
<!-- Background -->
<rect width="100%" height="100%" fill="#faf6f0"/>
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.6"/>
<!-- ============================================================
MESSAGE ARROWS (drawn first — z-order behind boxes)
============================================================ -->
<!-- 1. Subscriber → IdempotentStore: CheckAndMark (horizontal) -->
<line x1="230" y1="80" x2="410" y2="80" stroke="#78716c" stroke-width="1" marker-end="url(#arrow)"/>
<!-- 2. Subscriber → Pipeline: Run (diagonal down-right) -->
<line x1="230" y1="100" x2="410" y2="220" stroke="#78716c" stroke-width="1" marker-end="url(#arrow)"/>
<!-- 3. Pipeline → OrderClient: FetchMetadata (diagonal up-right, link) -->
<line x1="550" y1="220" x2="730" y2="100" stroke="#c05621" stroke-width="1" marker-end="url(#arrow-link)"/>
<!-- 4. Pipeline → DataFetcher: Download (horizontal right, link) -->
<line x1="550" y1="240" x2="730" y2="240" stroke="#c05621" stroke-width="1" marker-end="url(#arrow-link)"/>
<!-- 5a. Pipeline → FeatureExtractor: Preprocess (diagonal down-left) -->
<line x1="440" y1="268" x2="270" y2="372" stroke="#78716c" stroke-width="1" marker-end="url(#arrow)"/>
<!-- 5b. Pipeline → RuleQuerier: Query (vertical down) -->
<line x1="480" y1="268" x2="480" y2="372" stroke="#78716c" stroke-width="1" marker-end="url(#arrow)"/>
<!-- 6. Pipeline → RiskScorer: Score (horizontal left, ACCENT) -->
<line x1="410" y1="240" x2="230" y2="240" stroke="#d97706" stroke-width="1.2" marker-end="url(#arrow-accent)"/>
<!-- 7. Pipeline → AlertClient: Alert (diagonal down-right, link) -->
<line x1="540" y1="268" x2="700" y2="372" stroke="#c05621" stroke-width="1" marker-end="url(#arrow-link)"/>
<!-- 8. Pipeline → IdempotentStore: MarkComplete (vertical up, dashed return) -->
<line x1="480" y1="212" x2="480" y2="108" stroke="#78716c" stroke-width="1" stroke-dasharray="5,4" marker-end="url(#arrow)"/>
<!-- ============================================================
MESSAGE LABELS (with masking rects)
============================================================ -->
<!-- 1: CheckAndMark -->
<rect x="276" y="64" width="88" height="12" rx="2" fill="#faf6f0"/>
<text x="320" y="74" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">1: CheckAndMark</text>
<!-- 2: Run -->
<rect x="296" y="144" width="48" height="12" rx="2" fill="#faf6f0"/>
<text x="320" y="154" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">2: Run</text>
<!-- 3: FetchMetadata -->
<rect x="600" y="144" width="88" height="12" rx="2" fill="#faf6f0"/>
<text x="644" y="154" fill="#c05621" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">3: FetchMetadata</text>
<!-- 4: Download -->
<rect x="612" y="224" width="56" height="12" rx="2" fill="#faf6f0"/>
<text x="640" y="234" fill="#c05621" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">4: Download</text>
<!-- 5a: Preprocess -->
<rect x="300" y="304" width="76" height="12" rx="2" fill="#faf6f0"/>
<text x="338" y="314" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">5a: Preprocess</text>
<!-- 5b: Query -->
<rect x="492" y="304" width="56" height="12" rx="2" fill="#faf6f0"/>
<text x="520" y="314" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">5b: Query</text>
<!-- Parallel annotation -->
<rect x="444" y="284" width="72" height="12" rx="2" fill="#faf6f0"/>
<text x="480" y="294" fill="#a8a29e" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">asyncio</text>
<!-- 6: Score (ACCENT) -->
<rect x="292" y="248" width="64" height="12" rx="2" fill="#faf6f0"/>
<text x="324" y="258" fill="#d97706" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">6: Score</text>
<!-- 7: Alert -->
<rect x="600" y="304" width="56" height="12" rx="2" fill="#faf6f0"/>
<text x="628" y="314" fill="#c05621" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">7: Alert</text>
<!-- 8: MarkComplete (dashed) -->
<rect x="492" y="148" width="80" height="12" rx="2" fill="#faf6f0"/>
<text x="532" y="158" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">8: MarkComplete</text>
<!-- ============================================================
OBJECT BOXES (drawn after arrows — z-order above)
============================================================ -->
<!-- Row 1: Subscriber, IdempotentStore, OrderClient -->
<!-- Subscriber -->
<rect x="90" y="52" width="140" height="56" rx="6" fill="#faf6f0"/>
<rect x="90" y="52" width="140" height="56" rx="6" fill="#ffffff" stroke="#1c1917" stroke-width="1"/>
<rect x="98" y="58" width="44" height="12" rx="2" fill="transparent" stroke="rgba(28,25,23,0.40)" stroke-width="0.8"/>
<text x="120" y="67" fill="rgba(28,25,23,0.80)" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">RABBITMQ</text>
<text x="160" y="84" fill="#1c1917" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Subscriber</text>
<text x="160" y="100" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">pika</text>
<!-- IdempotentStore (store type) -->
<rect x="410" y="52" width="140" height="56" rx="6" fill="rgba(28,25,23,0.05)"/>
<rect x="410" y="52" width="140" height="56" rx="6" fill="rgba(28,25,23,0.05)" stroke="#78716c" stroke-width="1"/>
<rect x="418" y="58" width="44" height="12" rx="2" fill="transparent" stroke="rgba(120,113,108,0.40)" stroke-width="0.8"/>
<text x="440" y="67" fill="rgba(120,113,108,0.80)" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">STORE</text>
<text x="480" y="84" fill="#1c1917" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">IdempotentStore</text>
<text x="480" y="100" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">Redis SET</text>
<!-- OrderClient (external API) -->
<rect x="730" y="52" width="140" height="56" rx="6" fill="#faf6f0"/>
<rect x="730" y="52" width="140" height="56" rx="6" fill="rgba(28,25,23,0.03)" stroke="rgba(28,25,23,0.30)" stroke-width="1"/>
<rect x="738" y="58" width="32" height="12" rx="2" fill="transparent" stroke="rgba(28,25,23,0.30)" stroke-width="0.8"/>
<text x="754" y="67" fill="rgba(28,25,23,0.60)" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">API</text>
<text x="800" y="84" fill="#1c1917" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">OrderClient</text>
<text x="800" y="100" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">GET /job/{id}</text>
<!-- Row 2: RiskScorer, Pipeline (FOCAL), DataFetcher -->
<!-- RiskScorer (FOCAL — accent) -->
<rect x="90" y="212" width="140" height="56" rx="6" fill="#faf6f0"/>
<rect x="90" y="212" width="140" height="56" rx="6" fill="rgba(217,119,6,0.08)" stroke="#d97706" stroke-width="1.2"/>
<rect x="98" y="218" width="28" height="12" rx="2" fill="transparent" stroke="rgba(217,119,6,0.40)" stroke-width="0.8"/>
<text x="112" y="227" fill="rgba(217,119,6,0.80)" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">LLM</text>
<text x="160" y="244" fill="#1c1917" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">RiskScorer</text>
<text x="160" y="260" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">GPT API</text>
<!-- Pipeline (FOCAL — accent) -->
<rect x="410" y="212" width="140" height="56" rx="6" fill="#faf6f0"/>
<rect x="410" y="212" width="140" height="56" rx="6" fill="rgba(217,119,6,0.08)" stroke="#d97706" stroke-width="1.2"/>
<rect x="418" y="218" width="36" height="12" rx="2" fill="transparent" stroke="rgba(217,119,6,0.40)" stroke-width="0.8"/>
<text x="436" y="227" fill="rgba(217,119,6,0.80)" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">ORCH</text>
<text x="480" y="244" fill="#1c1917" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Pipeline</text>
<text x="480" y="260" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">5-stage</text>
<!-- DataFetcher (external CLI) -->
<rect x="730" y="212" width="140" height="56" rx="6" fill="#faf6f0"/>
<rect x="730" y="212" width="140" height="56" rx="6" fill="rgba(28,25,23,0.03)" stroke="rgba(28,25,23,0.30)" stroke-width="1"/>
<rect x="738" y="218" width="28" height="12" rx="2" fill="transparent" stroke="rgba(28,25,23,0.30)" stroke-width="0.8"/>
<text x="752" y="227" fill="rgba(28,25,23,0.60)" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">CLI</text>
<text x="800" y="244" fill="#1c1917" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">DataFetcher</text>
<text x="800" y="260" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">dbcli export</text>
<!-- Row 3: FeatureExtractor, RuleQuerier, AlertClient -->
<!-- FeatureExtractor -->
<rect x="130" y="372" width="140" height="56" rx="6" fill="#faf6f0"/>
<rect x="130" y="372" width="140" height="56" rx="6" fill="#ffffff" stroke="#1c1917" stroke-width="1"/>
<rect x="138" y="378" width="36" height="12" rx="2" fill="transparent" stroke="rgba(28,25,23,0.40)" stroke-width="0.8"/>
<text x="156" y="387" fill="rgba(28,25,23,0.80)" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">FUNC</text>
<text x="200" y="404" fill="#1c1917" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">FeatureExtractor</text>
<text x="200" y="420" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">Orchestrator</text>
<!-- RuleQuerier -->
<rect x="410" y="372" width="140" height="56" rx="6" fill="#faf6f0"/>
<rect x="410" y="372" width="140" height="56" rx="6" fill="#ffffff" stroke="#1c1917" stroke-width="1"/>
<rect x="418" y="378" width="36" height="12" rx="2" fill="transparent" stroke="rgba(28,25,23,0.40)" stroke-width="0.8"/>
<text x="436" y="387" fill="rgba(28,25,23,0.80)" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">RULES</text>
<text x="480" y="404" fill="#1c1917" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">RuleQuerier</text>
<text x="480" y="420" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">risk-kb</text>
<!-- AlertClient (external API) -->
<rect x="690" y="372" width="140" height="56" rx="6" fill="#faf6f0"/>
<rect x="690" y="372" width="140" height="56" rx="6" fill="rgba(28,25,23,0.03)" stroke="rgba(28,25,23,0.30)" stroke-width="1"/>
<rect x="698" y="378" width="32" height="12" rx="2" fill="transparent" stroke="rgba(28,25,23,0.30)" stroke-width="0.8"/>
<text x="714" y="387" fill="rgba(28,25,23,0.60)" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">API</text>
<text x="760" y="404" fill="#1c1917" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">AlertClient</text>
<text x="760" y="420" fill="#78716c" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">POST /result</text>
<!-- ============================================================
LEGEND (horizontal strip at bottom)
============================================================ -->
<line x1="30" y1="448" x2="930" y2="448" stroke="rgba(28,25,23,0.10)" stroke-width="0.8"/>
<text x="30" y="472" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">LEGEND</text>
<!-- Internal message -->
<line x1="100" y1="468" x2="128" y2="468" stroke="#78716c" stroke-width="1" marker-end="url(#arrow)"/>
<text x="136" y="472" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace">内部消息</text>
<!-- Focal message -->
<line x1="208" y1="468" x2="236" y2="468" stroke="#d97706" stroke-width="1.2" marker-end="url(#arrow-accent)"/>
<text x="244" y="472" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace">风控消息</text>
<!-- External message -->
<line x1="328" y1="468" x2="356" y2="468" stroke="#c05621" stroke-width="1" marker-end="url(#arrow-link)"/>
<text x="364" y="472" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace">外部调用</text>
<!-- Return message -->
<line x1="448" y1="468" x2="476" y2="468" stroke="#78716c" stroke-width="1" stroke-dasharray="5,4" marker-end="url(#arrow)"/>
<text x="484" y="472" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace">返回消息</text>
<!-- Focal object -->
<rect x="568" y="460" width="24" height="16" rx="4" fill="rgba(217,119,6,0.08)" stroke="#d97706" stroke-width="0.8"/>
<text x="600" y="472" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace">核心对象</text>
<!-- Store object -->
<rect x="672" y="460" width="24" height="16" rx="4" fill="rgba(28,25,23,0.05)" stroke="#78716c" stroke-width="0.8"/>
<text x="704" y="472" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace">存储</text>
<!-- External object -->
<rect x="752" y="460" width="24" height="16" rx="4" fill="rgba(28,25,23,0.03)" stroke="rgba(28,25,23,0.30)" stroke-width="0.8"/>
<text x="784" y="472" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace">外部服务</text>
</svg>
</div>
<!-- Summary Cards -->
<div class="cards">
<div class="card">
<p class="card-eyebrow">MESSAGE FLOW</p>
<div class="card-header">
<div class="card-dot ink"></div>
<h3>8 条编号消息</h3>
</div>
<ul>
<li>• 1: CheckAndMark → 幂等检查 (pending/completed)</li>
<li>• 2: Run → Pipeline 启动 5 阶段流水线</li>
<li>• 3: FetchMetadata → OrderService 轮询 (30s 间隔, 3min 超时)</li>
<li>• 4: Download → dbcli export 日志目录</li>
<li>• 5a/5b: Preprocess + Query → asyncio 并行</li>
<li>• 6: Score → GPT Engine 风控评分</li>
<li>• 7: Alert → POST 上报风控结果</li>
<li>• 8: MarkComplete → 标记幂等完成 (dashed)</li>
</ul>
</div>
<div class="card">
<p class="card-eyebrow">PARALLEL</p>
<div class="card-header">
<div class="card-dot accent"></div>
<h3>并行与串行</h3>
</div>
<ul>
<li>• 5a/5b 使用 asyncio 并行执行</li>
<li>• 任一 goroutine 失败不取消另一个</li>
<li>• Rules 失败仅记日志,不阻塞流程</li>
<li>• Preprocess 失败则 Pipeline 整体失败</li>
<li>• 6 必须等 5a/5b 全部完成后执行</li>
</ul>
</div>
</div>
<div class="footer">
nova-shield · github.com/novashield/risk-engine
</div>
</div>
</body>
</html>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NovaShield 风控组件图</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Geist', sans-serif;
background: #faf6f0;
min-height: 100vh;
padding: 2.5rem 2rem;
color: #1c1917;
}
.container { max-width: 1080px; margin: 0 auto; }
.header { margin-bottom: 2rem; }
.eyebrow {
font-family: 'Geist Mono', monospace;
font-size: 0.6875rem;
font-weight: 500;
letter-spacing: 0.18em;
text-transform: uppercase;
color: #78716c;
margin-bottom: 0.375rem;
}
h1 {
font-family: 'Instrument Serif', serif;
font-size: 1.75rem;
font-weight: 400;
color: #1c1917;
}
.subtitle {
color: #78716c;
font-size: 0.8125rem;
margin-top: 0.375rem;
}
.diagram-desc {
color: #a8a29e;
font-size: 0.6875rem;
margin-top: 0.5rem;
line-height: 1.5;
font-style: italic;
}
.diagram-container {
background: #f0ebe3;
border: 1px solid rgba(28,25,23,0.12);
border-radius: 8px;
padding: 1.5rem;
overflow-x: auto;
}
svg { width: 100%; min-width: 880px; display: block; }
.cards {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
margin-top: 1.5rem;
}
.card {
background: #ffffff;
border: 1px solid rgba(28,25,23,0.12);
border-radius: 6px;
padding: 1.25rem;
}
.card-eyebrow {
font-family: 'Geist Mono', monospace;
font-size: 0.625rem;
font-weight: 500;
letter-spacing: 0.18em;
text-transform: uppercase;
color: #a8a29e;
margin-bottom: 0.5rem;
}
.card-header {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.card-dot { width: 7px; height: 7px; border-radius: 50%; }
.card-dot.ink { background: #1c1917; }
.card-dot.accent { background: #d97706; }
.card-dot.muted { background: #78716c; }
.card h3 { font-size: 0.8125rem; font-weight: 600; color: #1c1917; }
.card ul {
list-style: none;
color: #78716c;
font-size: 0.75rem;
line-height: 1.6;
}
.card li { margin-bottom: 0.25rem; }
.footer {
margin-top: 1.5rem;
padding-top: 0.75rem;
border-top: 1px solid rgba(28,25,23,0.08);
text-align: center;
font-family: 'Geist Mono', monospace;
color: #a8a29e;
font-size: 0.625rem;
letter-spacing: 0.04em;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<p class="eyebrow">Component Diagram</p>
<h1>NovaShield 风控组件依赖</h1>
<p class="subtitle">Pipeline 编排 5 阶段组件,通过接口解耦,依赖注入测试</p>
<p class="diagram-desc"><b>组件图 (Component Diagram)</b> — 结构性图形。描述软件模块及接口之间的物理依赖关系。</p>
</div>
<div class="diagram-container">
<svg viewBox="0 0 960 580">
<defs>
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#78716c"/>
</marker>
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#d97706"/>
</marker>
<marker id="arrow-link" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#c05621"/>
</marker>
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
<circle cx="1" cy="1" r="0.9" fill="rgba(28,25,23,0.10)"/>
</pattern>
</defs>
<!-- Background -->
<rect width="100%" height="100%" fill="#faf6f0"/>
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.6"/>
<!-- ============================================================
ARROWS (drawn first — z-order behind boxes)
============================================================ -->
<!-- Subscriber → Pipeline -->
<line x1="216" y1="120" x2="372" y2="120" stroke="#78716c" stroke-width="1" marker-end="url(#arrow)"/>
<rect x="264" y="104" width="56" height="14" rx="2" fill="#faf6f0"/>
<text x="292" y="115" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">handler</text>
<!-- Subscriber → IdempotentStore -->
<line x1="144" y1="148" x2="144" y2="220" stroke="#78716c" stroke-width="1" marker-end="url(#arrow)"/>
<rect x="100" y="176" width="56" height="14" rx="2" fill="#faf6f0"/>
<text x="128" y="187" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">IdempotentStore</text>
<!-- Pipeline → DataEnricher -->
<line x1="420" y1="148" x2="420" y2="220" stroke="#78716c" stroke-width="1" marker-end="url(#arrow)"/>
<rect x="396" y="176" width="48" height="14" rx="2" fill="#faf6f0"/>
<text x="420" y="187" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">fetch+enrich</text>
<!-- Pipeline → FeatureExtractor -->
<line x1="372" y1="136" x2="216" y2="316" stroke="#78716c" stroke-width="1" marker-end="url(#arrow)"/>
<rect x="264" y="216" width="64" height="14" rx="2" fill="#faf6f0"/>
<text x="296" y="227" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">preprocess</text>
<!-- Pipeline → RiskScorer (FOCAL — accent) -->
<line x1="468" y1="136" x2="624" y2="316" stroke="#d97706" stroke-width="1.2" marker-end="url(#arrow-accent)"/>
<rect x="516" y="216" width="56" height="14" rx="2" fill="#faf6f0"/>
<text x="544" y="227" fill="#d97706" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">score</text>
<!-- Pipeline → AlertClient -->
<line x1="504" y1="120" x2="660" y2="120" stroke="#78716c" stroke-width="1" marker-end="url(#arrow)"/>
<rect x="556" y="104" width="48" height="14" rx="2" fill="#faf6f0"/>
<text x="580" y="115" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">report</text>
<!-- RiskScorer → RuleScorer (optional, dashed) -->
<line x1="756" y1="344" x2="792" y2="344" stroke="rgba(28,25,23,0.20)" stroke-width="1" stroke-dasharray="4,3" marker-end="url(#arrow)"/>
<!-- ============================================================
COMPONENT NODES
============================================================ -->
<!-- Subscriber -->
<rect x="72" y="88" width="144" height="60" rx="6" fill="#faf6f0"/>
<rect x="72" y="88" width="144" height="60" rx="6" fill="#ffffff" stroke="#1c1917" stroke-width="1"/>
<rect x="80" y="94" width="56" height="12" rx="2" fill="transparent" stroke="rgba(28,25,23,0.40)" stroke-width="0.8"/>
<text x="108" y="103" fill="rgba(28,25,23,0.80)" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">INGEST</text>
<text x="144" y="124" fill="#1c1917" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Subscriber</text>
<text x="144" y="138" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">pika consumer</text>
<!-- IdempotentStore (interface / store) -->
<rect x="72" y="220" width="144" height="60" rx="6" fill="rgba(28,25,23,0.05)" stroke="#78716c" stroke-width="1"/>
<rect x="80" y="226" width="48" height="12" rx="2" fill="transparent" stroke="rgba(120,113,108,0.40)" stroke-width="0.8"/>
<text x="104" y="235" fill="rgba(120,113,108,0.80)" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">STORE</text>
<text x="144" y="256" fill="#1c1917" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">IdempotentStore</text>
<text x="144" y="270" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">Redis / Memory</text>
<!-- Pipeline (FOCAL — accent) -->
<rect x="372" y="88" width="132" height="60" rx="6" fill="#faf6f0"/>
<rect x="372" y="88" width="132" height="60" rx="6" fill="rgba(217,119,6,0.08)" stroke="#d97706" stroke-width="1.2"/>
<rect x="380" y="94" width="68" height="12" rx="2" fill="transparent" stroke="rgba(217,119,6,0.40)" stroke-width="0.8"/>
<text x="414" y="103" fill="rgba(217,119,6,0.80)" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">ORCHESTRATOR</text>
<text x="438" y="124" fill="#1c1917" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Pipeline</text>
<text x="438" y="138" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">5 stages · 10min</text>
<!-- DataEnricher -->
<rect x="348" y="220" width="144" height="60" rx="6" fill="#faf6f0"/>
<rect x="348" y="220" width="144" height="60" rx="6" fill="#ffffff" stroke="#1c1917" stroke-width="1"/>
<rect x="356" y="226" width="56" height="12" rx="2" fill="transparent" stroke="rgba(28,25,23,0.40)" stroke-width="0.8"/>
<text x="384" y="235" fill="rgba(28,25,23,0.80)" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">ENRICH</text>
<text x="420" y="256" fill="#1c1917" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">DataEnricher</text>
<text x="420" y="270" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">OrderService + CustomerDB + Validate</text>
<!-- FeatureExtractor -->
<rect x="72" y="316" width="144" height="60" rx="6" fill="#faf6f0"/>
<rect x="72" y="316" width="144" height="60" rx="6" fill="#ffffff" stroke="#1c1917" stroke-width="1"/>
<rect x="80" y="322" width="56" height="12" rx="2" fill="transparent" stroke="rgba(28,25,23,0.40)" stroke-width="0.8"/>
<text x="108" y="331" fill="rgba(28,25,23,0.80)" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">EXTRACT</text>
<text x="144" y="352" fill="#1c1917" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">FeatureExtractor</text>
<text x="144" y="366" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">Orchestrator + Rules</text>
<!-- RiskScorer -->
<rect x="624" y="316" width="132" height="60" rx="6" fill="#faf6f0"/>
<rect x="624" y="316" width="132" height="60" rx="6" fill="#ffffff" stroke="#1c1917" stroke-width="1"/>
<rect x="632" y="322" width="48" height="12" rx="2" fill="transparent" stroke="rgba(28,25,23,0.40)" stroke-width="0.8"/>
<text x="656" y="331" fill="rgba(28,25,23,0.80)" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">SCORE</text>
<text x="690" y="352" fill="#1c1917" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">RiskScorer</text>
<text x="690" y="366" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">GPT API · 5min</text>
<!-- RuleScorer (optional, dashed) -->
<rect x="792" y="320" width="128" height="48" rx="6" fill="#faf6f0"/>
<rect x="792" y="320" width="128" height="48" rx="6" fill="rgba(28,25,23,0.02)" stroke="rgba(28,25,23,0.20)" stroke-width="1" stroke-dasharray="4,3"/>
<text x="856" y="348" fill="#78716c" font-size="11" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">RuleScorer</text>
<text x="856" y="360" fill="#a8a29e" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">rule_engine.py</text>
<!-- AlertClient -->
<rect x="660" y="88" width="144" height="60" rx="6" fill="#faf6f0"/>
<rect x="660" y="88" width="144" height="60" rx="6" fill="#ffffff" stroke="#1c1917" stroke-width="1"/>
<rect x="668" y="94" width="52" height="12" rx="2" fill="transparent" stroke="rgba(28,25,23,0.40)" stroke-width="0.8"/>
<text x="694" y="103" fill="rgba(28,25,23,0.80)" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">ALERT</text>
<text x="732" y="124" fill="#1c1917" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">AlertClient</text>
<text x="732" y="138" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">3 retries · 30s</text>
<!-- ============================================================
EXTERNAL CALLOUTS (annotations on right side)
============================================================ -->
<!-- RabbitMQ external -->
<rect x="36" y="88" width="28" height="16" rx="2" fill="rgba(28,25,23,0.03)" stroke="rgba(28,25,23,0.30)" stroke-width="0.8"/>
<text x="50" y="100" fill="#78716c" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle">RabbitMQ</text>
<line x1="64" y1="100" x2="72" y2="108" stroke="rgba(28,25,23,0.20)" stroke-width="0.8"/>
<!-- OrderService external -->
<rect x="504" y="232" width="28" height="16" rx="2" fill="rgba(28,25,23,0.03)" stroke="rgba(28,25,23,0.30)" stroke-width="0.8"/>
<text x="518" y="244" fill="#78716c" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle">OrderService</text>
<line x1="492" y1="244" x2="496" y2="244" stroke="rgba(28,25,23,0.20)" stroke-width="0.8"/>
<!-- Rules external -->
<rect x="36" y="340" width="28" height="16" rx="2" fill="rgba(28,25,23,0.03)" stroke="rgba(28,25,23,0.30)" stroke-width="0.8"/>
<text x="50" y="352" fill="#78716c" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle">Rules</text>
<line x1="64" y1="348" x2="72" y2="348" stroke="rgba(28,25,23,0.20)" stroke-width="0.8"/>
<!-- AlertService external -->
<rect x="816" y="100" width="40" height="16" rx="2" fill="rgba(28,25,23,0.03)" stroke="rgba(28,25,23,0.30)" stroke-width="0.8"/>
<text x="836" y="112" fill="#78716c" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle">API</text>
<line x1="804" y1="112" x2="808" y2="112" stroke="rgba(28,25,23,0.20)" stroke-width="0.8"/>
<!-- ============================================================
INTERFACE LOLLIPOPS (small circles on component edges)
============================================================ -->
<!-- Pipeline provided interface: Run(ctx, ScanInput) -->
<circle cx="438" cy="88" r="4" fill="#d97706" stroke="#d97706" stroke-width="0.8"/>
<text x="438" y="80" fill="#d97706" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">Run()</text>
<!-- IdempotentStore interface: CheckAndMark / MarkComplete -->
<circle cx="144" cy="220" r="4" fill="#78716c" stroke="#78716c" stroke-width="0.8"/>
<text x="144" y="216" fill="#78716c" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">CheckAndMark()</text>
<!-- DataEnricher interface -->
<circle cx="420" cy="220" r="4" fill="#78716c" stroke="#78716c" stroke-width="0.8"/>
<text x="420" y="216" fill="#78716c" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">FetchAndEnrich()</text>
<!-- RiskScorer interface -->
<circle cx="690" cy="316" r="4" fill="#78716c" stroke="#78716c" stroke-width="0.8"/>
<text x="690" y="312" fill="#78716c" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">Score()</text>
<!-- AlertClient interface -->
<circle cx="732" cy="88" r="4" fill="#78716c" stroke="#78716c" stroke-width="0.8"/>
<text x="732" y="80" fill="#78716c" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">Alert()</text>
<!-- ============================================================
LEGEND (horizontal strip at bottom)
============================================================ -->
<line x1="30" y1="420" x2="930" y2="420" stroke="rgba(28,25,23,0.10)" stroke-width="0.8"/>
<text x="30" y="444" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">LEGEND</text>
<rect x="100" y="432" width="24" height="14" rx="4" fill="#ffffff" stroke="#1c1917" stroke-width="1"/>
<text x="132" y="443" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace">组件</text>
<rect x="176" y="432" width="24" height="14" rx="4" fill="rgba(217,119,6,0.08)" stroke="#d97706" stroke-width="1"/>
<text x="208" y="443" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace">编排器</text>
<rect x="256" y="432" width="24" height="14" rx="4" fill="rgba(28,25,23,0.05)" stroke="#78716c" stroke-width="1"/>
<text x="288" y="443" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace">存储</text>
<rect x="336" y="432" width="24" height="14" rx="4" fill="rgba(28,25,23,0.02)" stroke="rgba(28,25,23,0.20)" stroke-width="1" stroke-dasharray="4,3"/>
<text x="368" y="443" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace">可选</text>
<line x1="440" y1="439" x2="464" y2="439" stroke="#78716c" stroke-width="1" marker-end="url(#arrow)"/>
<text x="472" y="443" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace">依赖</text>
<line x1="520" y1="439" x2="544" y2="439" stroke="#d97706" stroke-width="1.2" marker-end="url(#arrow-accent)"/>
<text x="552" y="443" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace">关键依赖</text>
<circle cx="656" cy="439" r="4" fill="#78716c" stroke="#78716c" stroke-width="0.8"/>
<text x="668" y="443" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace">接口</text>
<rect x="720" y="432" width="24" height="14" rx="2" fill="rgba(28,25,23,0.03)" stroke="rgba(28,25,23,0.30)" stroke-width="0.8"/>
<text x="752" y="443" fill="#78716c" font-size="8" font-family="'Geist Mono', monospace">外部服务</text>
</svg>
</div>
<!-- Summary Cards -->
<div class="cards">
<div class="card">
<p class="card-eyebrow">DECOUPLED</p>
<div class="card-header">
<div class="card-dot accent"></div>
<h3>依赖注入</h3>
</div>
<ul>
<li>• ScanDeps 提供所有组件的依赖注入</li>
<li>• 每个组件通过接口暴露方法,测试注入 fake</li>
<li>• Subscriber: fake messageConsumer</li>
<li>• RiskScorer: fake gptRunner</li>
<li>• AlertClient: httpx mock</li>
</ul>
</div>
<div class="card">
<p class="card-eyebrow">COMPONENTS</p>
<div class="card-header">
<div class="card-dot ink"></div>
<h3>7 个组件</h3>
</div>
<ul>
<li>• Subscriber — RabbitMQ 消费 + IdempotentStore 去重</li>
<li>• Pipeline — 6 阶段编排,全局 5min 超时</li>
<li>• DataEnricher — OrderService 元数据 + 数据库导出 + 验证</li>
<li>• FeatureExtractor — 日志预处理 + 规则查询</li>
<li>• RiskScorer — GPT Engine 诊断</li>
<li>• RuleScorer — 降级 rule_engine.py</li>
<li>• AlertClient — 告警发送,3 次重试</li>
</ul>
</div>
</div>
<div class="footer">
nova-shield · github.com/novashield/risk-engine
</div>
</div>
</body>
</html>
图表类型内容提取策略
每种图表需要从代码库中提取不同维度的信息。以下是通用的提取策略,适用于任何语言/框架的项目。
通用提取规则
组件识别
- 入口文件中的初始化/注册代码
- 依赖注入容器(Wire, Spring, 等)
- 包/模块的公开接口
- 配置文件中引用的外部服务
关系识别
- import/require 语句
- 函数调用链(谁调用了谁)
- 接口实现关系
- 事件发布/订阅
数据流识别
- 函数参数和返回值
- 消息队列的 topic/producer/consumer
- API endpoint 的 request/response
- 数据库读写操作
流程识别
- 主循环 / 事件循环
- 中间件链 / handler 链
- 状态机转换
- 错误处理 / 重试逻辑
按语言的搜索模式
Go
- 接口:
type \w+ interface - 结构体:
type \w+ struct - 函数签名:
func \([^)]+\) \w+ - 依赖注入:
New\w+\(.*\w+Client - Goroutine/Channel:
go func,chan - 错误处理:
if err != nil
Python
- 类:
class \w+ - 函数:
def \w+ - 装饰器:
@\w+(路由、依赖注入) - 异步:
async def,await - 导入:
from .* import,import
TypeScript/JavaScript
- 类:
class \w+ - 接口:
interface \w+ - 导入:
import .* from - 路由:
app\.(get|post|put|delete) - 中间件:
\.use\(
信息提取深度
- 架构图/组件图/部署图: 只需包级/模块级信息,读 CLAUDE.md + 入口文件即可
- 序列图/通信图: 需要函数调用链,读关键源码文件
- 类图/对象图: 需要类型定义,读 types/model 文件
- 流程图/活动图/泳道图: 需要主流程代码,读 pipeline/orchestrator/handler 文件
- 数据流图: 需要数据结构 + 变换逻辑,读 processor/converter 文件
- 用例图: 读 CLAUDE.md + router/api 文件