data-query 技能
技能概述
本技能提供自然语言转 SQL(NL→SQL)能力,并支持生成基于 ECharts 的可视化 HTML 页面。
核心能力:
- 将自然语言问题转换为准确的 SQL 查询
- 验证 SQL 可执行性后返回结果
- 生成可嵌入的 HTML 图表页面
典型触发场景:
- 用户问:"某项目的延期任务有哪些?"
- 用户问:"帮我统计一下这个项目的风险分布"
- 用户说:"生成一个资源负载分析页面"
🚨 核心原则一:前置配置检查(必须首先执行)
使用本技能前,必须立刻执行此检查。配置缺失时,停止一切操作,提示用户补充。
配置文件位置: skills/data-query/config.json
必需配置
| 配置项 | 路径 | 说明 | 缺失后果 |
|---|---|---|---|
| API 验证账号 | security.apiAuth.username | 系统账号,用于验证 SQL | ❌ 无法验证 SQL |
| API 验证密码 | security.apiAuth.password | 系统密码,用于验证 SQL | ❌ 无法验证 SQL |
| 数据库类型 | db.type | mysql / dm / oracle | ⚠️ 无法确定方言规则 |
根据 db.type 显示相关配置
当 db.type = dm 时:
| 配置项 | 路径 | 说明 | 缺失后果 |
|---|---|---|---|
| 达梦主机 | db.host | 达梦数据库地址 | ❌ 无法连接达梦 |
| 达梦端口 | db.port | 达梦端口,默认 5236/5237 | ❌ 无法连接达梦 |
| 达梦数据库名 | db.database | 数据库实例名 | ⚠️ 可能连接失败 |
| 达梦用户名 | db.user | 数据库用户名 | ❌ 无法连接达梦 |
| 达梦密码 | db.password | 数据库密码 | ❌ 无法连接达梦 |
当 db.type = oracle 时:
| 配置项 | 路径 | 说明 | 缺失后果 |
|---|---|---|---|
| Oracle 主机 | oracle.host | Oracle 数据库地址 | ❌ 无法连接 Oracle |
| Oracle 端口 | oracle.port | Oracle 端口,默认 1521 | ❌ 无法连接 Oracle |
| Oracle 服务名 | oracle.serviceName | 数据库服务名 | ❌ 无法连接 Oracle |
| Oracle 用户名 | oracle.user | 数据库用户名 | ❌ 无法连接 Oracle |
| Oracle 密码 | oracle.password | 数据库密码 | ❌ 无法连接 Oracle |
API 配置:
| 配置项 | 路径 | 说明 | 缺失后果 |
|---|---|---|---|
| API Base URL | api.base | Dashboard 服务地址 | ❌ 无法调用 API 验证 |
| 登录密码加密 | api.loginPasswordEncrypt | 密码是否加密 | ⚠️ 可能登录失败 |
缺失时提示模板
配置缺失时,按以下格式提示用户,并直接帮用户设置:
⚠️ 技能配置不完整
配置文件:skills/data-query/config.json
缺失项:
❌ security.apiAuth.username — API 验证账号
❌ security.apiAuth.password — API 验证密码
请提供:
1. 系统账号(用于验证 SQL):______
2. 系统密码:______
我将直接帮你写入配置文件。
校验规则
使用技能时,我会按以下顺序检查:
- 检查
security.apiAuth— 缺失则提示用户提供,我直接写入配置文件 - 检查
db.type— 缺失则无法确定方言,提示用户提供 - 根据 db.type 检查对应数据库连接 — 缺失则提示用户提供,我直接写入
- 检查
api.base— 缺失则提示用户提供,我直接写入
注意: verifyLimit(重试次数)、workspace 为可选,不影响核心功能。
工作流程
⚠️ 必读:数据库方言规范
生成 SQL 前,必须先读取对应数据库的规范文档:
| 数据库 | 规范文档位置 |
|---|---|
| 达梦 DM | skills/data-query/database_specs/dialect/DM.md |
| MySQL | skills/data-query/database_specs/dialect/MySQL.md |
| Oracle | skills/data-query/database_specs/dialect/Oracle.md |
| ShardingSphere 路由 | skills/data-query/database_specs/sharding/ROUTING_RULES.md |
| ShardingSphere BUG | knowledge/shared/sharding/KNOWN_BUGS.md |
| 分片表分布 | knowledge/shared/sharding/TABLE_DISTRIBUTION.md |
达梦关键规范(必须遵守):
- ❌ 禁用
TO_CHAR(date, 'YYYY-MM')→ ✅ 用SUBSTR(CAST(date AS VARCHAR), 1, 7) - ❌ 禁用
GROUP BY + COUNT(*)在 ShardingSphere 下 → ✅ 返回原始数据,前端聚合 - ❌ 分片键上禁止使用函数
⚠️ 两套 SQL 必须同步
Cockpit 页面存在两套 SQL,修改时必须同时更新:
| 对象 | 用途 | 怎么改 |
|---|---|---|
ENCRYPTED_SQL[key].ciphertext | 后端实际执行 | 重新加密后替换 |
SQL_PLAIN[key] | 前端参数校验 | 明文替换 |
CHART_CONFIG[id].scope | 参数注入逻辑 | 按需调整 |
修改步骤:
- 确定正确 SQL → 2. 用
encrypt_for_page.js加密 → 3. 同时更新 ENCRYPTED_SQL 和 SQL_PLAIN → 4. 用src/verify/index.js验证
流程一:NL→SQL 查询(预览)
输出:无文件,直接返回 JSON 结果
用户自然语言问题
↓
主 agent 直接生成 SQL(基于知识库,不调用外部 LLM)
↓
调用 src/verify/index.js verify() 验证 SQL 可执行性(unified 模式:DB + API)
↓
返回 SQL + 查询结果给用户预览
流程二:生成单图表 HTML 页面
输出路径:{workspace}/nl2sql_output/{chartTitle}_{timestamp}.html
用户需求(图表类型 + 数据描述)
↓
步骤1:主 agent 直接生成 SQL(基于知识库)
步骤2:调用 src/verify/index.js verify() 验证 SQL(unified 模式)
步骤3:调用 src/security/encryptSql.js 对 SQL 进行 AES-256-CBC 加密
步骤4:填充 html_page_template.html 模板
替换占位符: {{ENCRYPTED_SQL}}、{{IV}}、{{JWT_TOKEN}}、{{CHART_TITLE}} 等
↓
输出完整的 HTML 文件 → {workspace}/nl2sql_output/{chartTitle}_{timestamp}.html
流程三:生成驾驶舱 HTML 页面
驾驶舱是多图表聚合页面,包含 KPI 指标卡、多种 ECharts 图表、项目列表侧边栏和视图切换(仪表盘/看板/甘特图)。
输出路径:两个文件(归集在一起)
- 本地预览版:
{workspace}/nl2sql_output/cockpit_preview.html(CDN URLs) - 部署版:
{workspace}/nl2sql_output/cockpit.html(CDN→本地相对路径,供部署使用)
⚠️ 不得覆盖模板:
templates/cockpit_current.html是模板文件,生成结果必须保存到nl2sql_output/,不得覆盖模板。
用户需求:生成项目驾驶舱
↓
步骤0:resolveCockpitTemplate() 解析模板
├─ cockpit_current.html 存在 → 使用用户当前版本
├─ cockpit_current.html 不存在 → 使用技能基线模板
└─ regenerate=true → 强制重新生成并存档旧版本
↓
步骤1:resolveDomain() × N 生成 sqlMap + spec(自动进化)
└─ 或由主 agent 直接构造(显式配置格式)
↓
步骤2:verify() × N 验证(source=unified:DB 直连 + API 端到端同时验证)
> agent 全程参与:验证失败 → 返回错误信息给 agent → agent 决定是否重新生成 SQL → 重试验证(最多 3 次,全失败则停止)
> ⚠️ **前置条件**:params 数量必须与 SQL 中 `?` 总数完全匹配;禁止 `?` 出现在表达式中
↓
步骤3:parseSqlMap() → buildChartConfig() 生成图表配置
↓
步骤4:encryptSql() 加密所有 SQL
↓
步骤5:注入 cockpit_current.html → 输出 HTML(不修改模板文件)
↓
步骤6(可选):applyTweak() 应用局部微调 → 写回 cockpit_current.html(供下次生成使用)
↓
步骤7:**完整性校验** — src/generate/page.js 内置 `src/generate/validate_page.js`,自动完成以下 6 项检查 + auto-fix
↓
步骤8:**HTML 端到端验证** — src/generate/page.js 内置 `src/verify/index.js` 的 `batchVerifyHtml()`,解析 HTML 中所有加密 SQL,调后端 API 验证全部通过后才写入文件;失败则删除文件并退出
↓
步骤9:输出两个文件
- nl2sql_output/cockpit_preview.html(预览版,保留 CDN URLs)
- nl2sql_output/cockpit.html(部署版,CDN→本地相对路径)
- [deploy=true] 同时拷贝到 acm_www/static/cockpit.html
sqlMap 新格式(显式配置,v2):
sqlMap 每个条目使用显式 spec,不再依赖 key 名称推断类型。
const { generateCockpitChart } = require('{skillDir}/src/generate/page.js');
const sqlMap = {
// ── sidebar:左侧项目列表,不进 CHART_CONFIG ──────────────────
'projectList': {
sql: 'SELECT p.ID, p.NAME, p.COMPLETE_PCT, p.STATUS FROM wsd_plan_project p WHERE p.DEL = 0 AND p.STATUS = \'active\'',
type: 'sidebar', // 显式声明为 sidebar
title: '项目列表'
},
// ── KPI 类型图表 ───────────────────────────────────────────────
'kpi_progress': {
sql: 'SELECT ID, NAME, COMPLETE_PCT, PLAN_SUM, ACT_SUM FROM wsd_plan_project WHERE ID = ? AND DEL = 0',
type: 'kpi',
title: '项目进度',
scope: 'project',
spec: {
kpiType: 'avg_percent',
valueField: 'COMPLETE_PCT',
unit: '%',
thresholds: { green: 80, orange: 60 }
}
},
'kpi_health': {
sql: 'SELECT COUNT(CASE WHEN FEEDBACK_STATUS = \'2\' AND ACT_END_TIME <= PLAN_END_TIME THEN 1 END) * 100.0 / NULLIF(COUNT(*),0) AS health_score FROM wsd_plan_task WHERE PROJECT_ID = ? AND DEL = 0',
type: 'kpi',
title: '任务健康度',
scope: 'project',
spec: {
kpiType: 'health_score',
valueField: 'health_score',
unit: '分',
thresholds: { green: 80, orange: 60 }
}
},
// ── chart 类型图表 ──────────────────────────────────────────────
'riskHeatmap': {
sql: 'SELECT PROBABILITY_LEVEL, AFTERMATH_LEVEL, COUNT(*) AS count FROM wsd_risk_register WHERE PROJECT_ID = ? AND IS_CLOSE = \'N\' GROUP BY PROBABILITY_LEVEL, AFTERMATH_LEVEL',
type: 'chart',
title: '风险四象限',
scope: 'project',
spec: {
chartType: 'scatter',
xAxis: 'aftermath_level',
yAxis: 'probability_level',
valueField: 'count',
width: 'full'
}
},
'milestone': {
sql: 'SELECT t.TASK_NAME, t.PLAN_START_TIME, t.PLAN_END_TIME, t.ACT_END_TIME, t.FEEDBACK_STATUS, u.USER_NAME FROM wsd_plan_task t LEFT JOIN wsd_sys_user u ON t.USER_ID = u.ID WHERE t.PROJECT_ID = ? AND t.TASK_TYPE IN (2,3) AND t.DEL = 0 ORDER BY t.PLAN_END_TIME',
type: 'chart',
title: '里程碑节点',
scope: 'project',
spec: {
chartType: 'timeline',
startField: 'plan_start_time',
endField: 'plan_end_time',
labelField: 'task_name',
width: 'full'
}
},
'resourceLoad': {
sql: 'SELECT r.ORG_ID, o.ORG_NAME, SUM(t.BUDGET_QTY)/8 AS plan_days, COUNT(DISTINCT r.ID) * ? AS capacity_days, SUM(t.BUDGET_QTY)/8 / (COUNT(DISTINCT r.ID) * ?) * 100 AS load_rate FROM wsd_plan_taskrsrc t JOIN wsd_rsrc_user r ON t.RSRC_ID = r.ID LEFT JOIN wsd_sys_org o ON r.ORG_ID = o.ID WHERE r.ORG_ID IS NOT NULL AND t.PLAN_START_TIME >= ? AND t.PLAN_START_TIME <= ? GROUP BY r.ORG_ID, o.ORG_NAME',
type: 'chart',
title: '资源负载分布',
scope: 'global', // 全局统计,不需要 projectId
spec: {
chartType: 'bar',
xAxis: 'org_name',
yAxis: 'load_rate',
valueField: 'plan_days'
}
},
'kanban': {
sql: 'SELECT t.ID, t.TASK_NAME, t.STATUS, t.FEEDBACK_STATUS, t.COMPLETE_PCT, t.PLAN_END_TIME, u.USER_NAME AS assignee_name FROM wsd_plan_task t LEFT JOIN wsd_sys_user u ON t.USER_ID = u.ID WHERE t.PROJECT_ID = ? AND t.DEL = 0 ORDER BY t.PLAN_END_TIME',
type: 'chart',
title: '任务看板',
scope: 'project',
spec: {
chartType: 'kanban',
kanbanColumns: [
{ status: 'EDIT', title: '待开始', color: '#999' },
{ status: 'APPROVAL', title: '审批中', color: '#faad14' },
{ status: 'CONFIRM', title: '进行中', color: '#1890ff' },
{ status: 'RELEASE', title: '已发布', color: '#52c41a' }
]
}
}
};
const html = await generateCockpitChart(sqlMap, {
title: '项目驾驶舱',
projectId: 61,
verify: true
});
sqlMap 旧格式兼容:条目为纯字符串时,内部自动推断 spec 作为兜底。
ChartSpec 字段说明:
| 字段 | 位置 | 类型 | 说明 |
|---|---|---|---|
type | entry | string | kpi / chart / sidebar / table |
scope | entry | string | project(需 projectId)/ global(不需要) |
sql | entry | string | SQL 查询语句 |
title | entry | string | 图表标题 |
spec.chartType | spec | string | bar/line/pie/scatter/timeline/kanban/gantt/number |
spec.kpiType | spec | string | health_score/count/avg_percent/sum_money/ratio_percent |
spec.valueField | spec | string | 值字段名(SQL 结果中的列名) |
spec.unit | spec | string | 单位:%/分/项/元 |
spec.thresholds | spec | object | 颜色阈值:{ green, orange } |
spec.xAxis | spec | string | X 轴字段(热力图:aftermath_level) |
spec.yAxis | spec | string | Y 轴字段(热力图:probability_level) |
spec.width | spec | string | third(1/3宽)/half(半宽)/full(全宽) |
spec.kanbanColumns | spec | array | 看板列定义:[{ status, title, color }] |
spec.startField | spec | string | 时间线开始字段 |
spec.endField | spec | string | 时间线结束字段 |
spec.params | spec | array | 额外业务参数(追加到 projectId 之后的位置),如 ['2024-01-01', '2024-12-31'] |
paramsCount | entry | number | (推荐声明) 声明该 SQL 需要的参数总数(含 projectId),生成器据此校验 ? 占位符数量,提前发现 params 不匹配问题。例:scope=project 的 SQL 含 2 个 ?,则 paramsCount: 3(1 个 projectId + 2 个额外参数) |
params 校验机制(前端运行时)
问题背景:前端生成的 SQL 含 ? 占位符,但 params 数组经常为空或不完整,导致后端报 "parameter not found" 错误。
根本解决方案:技能在生成 HTML 时同时注入两个常量:
| 常量 | 内容 | 用途 |
|---|---|---|
ENCRYPTED_SQL | AES-256-CBC 加密后的 SQL | 传给后端 execute 接口 |
SQL_PLAIN | 明文 SQL(与 ENCRYPTED_SQL key 一一对应) | 供前端 buildParams 运行时校验 ? 数量 |
buildParams 在每次发请求前做以下校验:
- ERROR:SQL 有
?但params=[]→ 控制台立即报错,指明具体图表和原因 - WARN:
params.length > SQL中?数量→ 控制台警告,多余参数被忽略 - WARN:
scope=project但currentProjectId=null→ 控制台警告,图表数据无法加载
生成者规范:sqlMap 条目应声明 paramsCount,与 SQL 中 ? 数量精确匹配,从源头消除歧义。
Schema 源码:src/generate/chartSpecSchema.js
输出文件: {outputPath}/cockpit_{timestamp}.html
核心知识库文件(必须读取)
| 文件 | 路径 | 用途 |
|---|---|---|
| 字段映射 | knowledge/{dbType}/field_mapping.json | 字段→中文名、枚举值映射 |
| KPI公式 | knowledge/shared/kpi_formulas.json | KPI计算公式(挣值、进度偏差等) |
| KPI规格 | knowledge/shared/kpi_spec_schema.json | KPI结构化渲染规范(类型、阈值、颜色) |
| 安全规则 | knowledge/shared/sql_generation_rules.md | SQL生成铁律(必读!) |
| 知识库扫描 | scripts/scanner.js | 自动进化引擎(域→表→SQL+spec) |
达梦数据库知识库生成
技能支持两种方案生成达梦数据库知识库:
方案一:驱动方案(自动连接数据库)
自动连接达梦数据库,查询系统表生成知识库。
要求:
- Linux/Windows: 安装 dmPython 驱动 (
pip install dmPython) - macOS: 安装 pyodbc + 达梦 ODBC 驱动
使用:
# 设置环境变量
export DB_TYPE=dm
export DB_HOST=192.168.1.100
export DB_PORT=5236
export DB_NAME=ACM
export DB_USER=SYSDBA
export DB_PASSWORD=password
# 生成知识库
cd scripts/knowledge
python generate.py
方案二:转换方案(无需驱动,推荐)
通过导出的表结构文件(SQL/CSV/JSON)转换生成知识库,无需安装任何数据库驱动。
适用场景:
- 无法安装驱动的环境(如 macOS、受限服务器)
- 离线环境
- 快速部署
使用方法:
cd scripts/knowledge
# 只转换表结构(生成 tables.json)
python3 convert_knowledge.py --input /path/to/structure.sql
# 转换表结构并生成字段映射(生成 tables.json + field_mapping.json)
python3 convert_knowledge.py --input /path/to/structure.sql --mapping
# 同时导入字典数据
python3 convert_knowledge.py --input /path/to/structure.sql --mapping --dict /path/to/dict.csv
# 强制覆盖现有文件
python3 convert_knowledge.py --input /path/to/structure.sql --mapping --force
# 合并多个文件
python3 convert_knowledge.py --inputs file1.sql file2.sql --mapping --force
支持的输入格式:
| 格式 | 说明 | 示例来源 |
|---|---|---|
.sql | 达梦 CREATE TABLE 脚本 | DM Manager 导出 |
.csv | 表结构 CSV 文件 | DM Manager 导出 |
.json | 自定义 JSON 格式 | 其他工具导出 |
输出文件(自动覆盖):
knowledge/dm/tables.json- 表结构信息knowledge/dm/field_mapping.json- 字段映射和枚举值
Python 接口(供 agent 调用):
import sys
sys.path.append('.')
from scripts.knowledge.convert_knowledge import convert
# 只转换表结构
result = convert('/path/to/structure.sql')
# 转换表结构并生成字段映射
result = convert('/path/to/structure.sql', generate_mapping=True)
# 同时导入字典数据
result = convert('/path/to/structure.sql', generate_mapping=True, dict_file='/path/to/dict.csv')
if result['success']:
print(f"✅ 转换成功: {result['tables_count']} 个表")
if 'field_count' in result:
print(f" 字段映射: {result['field_count']} 个字段")
else:
print(f"❌ 转换失败: {result['message']}")
多数据库支持
技能支持 MySQL、Oracle、达梦(DM)三种数据库的 SQL 生成,通过 DB_TYPE 环境变量配置。
配置方式
| 环境变量 | 可选值 | 默认值 | 说明 |
|---|---|---|---|
DB_TYPE | mysql / oracle / dm | mysql | 目标数据库类型 |
DB_HOST | IP/域名 | 192.168.3.25 | 数据库地址 |
DB_PORT | 端口号 | 3306 | 数据库端口 |
DB_NAME | 库名 | acm_cloud_acm | 数据库名称 |
DB_USER | 用户名 | root | 数据库用户 |
DB_PASSWORD | 密码 | Wisdom83248380 | 数据库密码 |
ORACLE_HOST | IP/域名 | 192.168.3.25 | Oracle 主机 |
ORACLE_PORT | 端口号 | 1521 | Oracle 端口 |
ORACLE_SERVICE | 服务名 | orcl | Oracle 服务名 |
ORACLE_USER | 用户名 | acm | Oracle 用户 |
ORACLE_PASSWORD | 密码 | acm | Oracle 密码 |
方言差异说明
| 语法 | MySQL | Oracle | 达梦 |
|---|---|---|---|
| 标识符引号 | \field`` | "field" | "field" |
| 分页 | LIMIT offset, count | 三层 ROWNUM 子查询 | LIMIT count OFFSET offset |
| 字符串拼接 | CONCAT(a, b) / a || b | a || b | a || b |
| 当前时间 | NOW() | SYSDATE | SYSTIMESTAMP |
| NULL 替换 | IFNULL(a, b) | NVL(a, b) | IFNULL(a, b) |
| 日期减法 | DATE_SUB(col, INTERVAL n DAY) | col - n | col - n |
| 日期转字符串 | DATE_FORMAT(col, '%Y-%m') | TO_CHAR(col, 'YYYY-MM') | SUBSTR(CAST(col AS VARCHAR), 1, 7) ⚠️ |
| GROUP BY + COUNT(*) | ✅ | ✅ | ⚠️ ShardingSphere 归并 bug,前端聚合 |
⚠️ 达梦特别警示:
TO_CHAR在达梦中不适用于日期格式转换,必须用SUBSTR(CAST(... AS VARCHAR), 1, 7)GROUP BY + COUNT(*)在 ShardingSphere 分片下有归并 bug,应返回原始数据由前端聚合
使用示例
# MySQL(默认)
DB_TYPE=mysql node src/verify/index.js "SELECT * FROM wsd_plan_task LIMIT 3"
# Oracle
DB_TYPE=oracle node src/verify/index.js "SELECT * FROM wsd_plan_task WHERE ROWNUM <= 3"
# 达梦
DB_TYPE=dm node src/verify/index.js "SELECT * FROM wsd_plan_task LIMIT 3 OFFSET 0"
生成脚本时的数据库指定
# 指定 Oracle 重新生成知识库
DB_TYPE=oracle python3 scripts/knowledge/generate.py --apply --target tables
内部 Dialect 架构
src/core/dialect.js 封装了所有数据库特定语法:
const { createDialect } = require('./core/dialect.js');
const dialect = createDialect('oracle');
dialect.quote('field_name'); // "field_name"
dialect.applyPagination(sql, 0, 20); // Oracle 三层分页
dialect.stripPagination(sql); // 还原为 base SQL
dialect.limit('SELECT * FROM t', 0, 3); // LIMIT 3
输入参数
必需参数
| 参数 | 类型 | 说明 |
|---|---|---|
question | string | 用户的自然语言问题(中文) |
projectId | number | 项目上下文ID |
可选参数
| 参数 | 类型 | 说明 |
|---|---|---|
epsId | number | EPS上下文ID |
chartType | string | 图表类型:line/bar/pie/heatmap/table |
chartTitle | string | 图表标题 |
outputPath | string | HTML输出路径(默认 ./nl2sql_output/) |
鉴权说明
JWT 获取方式:
POST /api/auth/jwt/token
Body: { "userName": "admin@wisdomidata", "password": "<加密后的密码>" }
密码加密方式(AES-128-CBC):
// 加密算法见 acm_www/utils/AesUtil.js
const encryptedPassword = AesUtil.aesEncrypt('123456');
// 结果如: "fGs/2dJVyyTryPB4pNB8nQ=="
请求头格式(重要!):
Authorization: <JWT字符串>
⚠️ 注意:是直接 Authorization: <JWT>,不是 Authorization: Bearer <JWT>!
Token 缓存要求(重要!) 生成的 HTML 页面必须内置 token 缓存机制,确保:
- 页面生命周期内只请求一次 token
- 后续请求复用缓存的 token,避免重复调用
- 使用 Promise 缓存防止并发请求时产生多个 token 请求
// 正确示例
let jwtToken = null;
let tokenPromise = null;
async function getToken() {
if (jwtToken) return jwtToken;
if (tokenPromise) return tokenPromise; // 防止并发请求时产生多个 token 请求
tokenPromise = fetch(...).then(r => r.json()).then(d => jwtToken = d.data).finally(() => tokenPromise = null);
return tokenPromise;
}
后端通过 HttpClientUtil.getLoginUserVo().getTenantId() 从 Gateway 设置的请求头中获取租户ID,无需客户端传入。
输入方式
方式1:直接提问
用户:请帮我查一下A项目延期未完成的任务有哪些?
→ question="A项目的延期未完成任务有哪些"
方式2:生成图表页面
用户:请帮我生成一个风险热力图页面
→ chartType="heatmap", chartTitle="风险热力图", question="项目风险四象限分布"
动态参数传递
前端调用 executeQuery 时,通过 params 数组传递动态参数:
// 无参数
executeQuery('staticSql')
// 有参数(按顺序对应 SQL 中的 ?)
executeQuery('dynamicSql', [projectId])
executeQuery('dateRangeSql', ['2026-01-01', '2026-03-27'])
// 动态日期示例
const today = new Date().toISOString().split('T')[0] // '2026-03-27'
const startOfYear = today.substring(0, 4) + '-01-01' // '2026-01-01'
executeQuery('resourceLoad', [startOfYear, today])
注意:
- SQL 中
?的数量必须与 params 数组长度一致 - 参数顺序对应 SQL 中
?的出现顺序
查询模式:单项目 vs 全局
SQL 分为两种模式,生成前必须先判断用户需求属于哪种:
| 模式 | 特征关键词 | 典型问题 | projectId 参数 |
|---|---|---|---|
| 单项目 | "某项目"、"这个项目"、"项目61" | "项目61的延期任务有哪些" | 必须传入 |
| 全局/跨项目 | "全部项目"、"所有任务"、"公司级"、"全公司"、"本月汇总" | "本月全公司延期任务有哪些"、"查询所有延期未完成的任务" | 不需要 projectId |
识别规则(优先级从高到低):
- 显式指定项目名称/ID → 单项目
- 包含"全部"、"所有"、"全局"、"公司级"、"本月"、"本季"、"本年" → 全局
- 无明确范围修饰词 → 默认单项目(需 projectId)
全局查询 SQL 示例:
-- 查询全公司延期未完成的任务(无 projectId 过滤)
SELECT t.ID, t.TASK_NAME, t.STATUS, t.FEEDBACK_STATUS, t.PLAN_END_TIME,
p.NAME AS project_name, u.USER_NAME AS assignee_name
FROM wsd_plan_task t
LEFT JOIN wsd_plan_project p ON t.PROJECT_ID = p.ID AND p.DEL = 0
LEFT JOIN wsd_sys_user u ON u.ID = t.USER_ID
WHERE t.DEL = 0
AND t.FEEDBACK_STATUS IN ('0', '1')
AND t.PLAN_END_TIME < CURDATE()
ORDER BY t.PLAN_END_TIME
LIMIT 20
-- 按组织统计本月延期任务数(GROUP BY org)
SELECT o.ORG_NAME, COUNT(*) AS delayed_count
FROM wsd_plan_task t
JOIN wsd_sys_org o ON t.ORG_ID = o.ID
WHERE t.DEL = 0
AND t.FEEDBACK_STATUS IN ('0', '1')
AND t.PLAN_END_TIME >= DATE_FORMAT(CURDATE(), '%Y-%m-01')
GROUP BY o.ORG_NAME, o.ID
ORDER BY delayed_count DESC
-- EPS级汇总(wsd_plan_project.PARENT_ID = epsId)
SELECT p.ID, p.NAME, COUNT(t.ID) AS task_count,
SUM(CASE WHEN t.FEEDBACK_STATUS = '2' THEN 1 ELSE 0 END) AS completed_count
FROM wsd_plan_project p
LEFT JOIN wsd_plan_task t ON t.PROJECT_ID = p.ID AND t.DEL = 0
WHERE p.DEL = 0 AND p.PARENT_ID = ?
GROUP BY p.ID, p.NAME
cockpit sqlMap 中的全局 SQL 标记:
当驾驶舱需要包含全局数据(如"公司风险TOP10")时,sqlMap 条目标记 scope: 'global',表示不注入 projectId:
const sqlMap = {
// 项目级看板(需要 projectId)
'kanban': {
sql: 'SELECT t.ID, t.TASK_NAME, ... FROM wsd_plan_task t WHERE t.PROJECT_ID = ? AND t.DEL = 0 ...',
scope: 'project' // 默认
},
// 全局统计(不需要 projectId)
'companyRiskCount': {
sql: 'SELECT risk_level, COUNT(*) FROM wsd_risk_register WHERE IS_CLOSE = \'N\' GROUP BY risk_level',
scope: 'global'
}
};
自动进化流程(推荐)
当用户的业务需求在 sqlMap 中没有预定义时,使用 knowledgeBaseScanner.js 从知识库动态生成完整的 sqlMap 条目(SQL + 显式 spec)。
自动化入口
const { resolveDomain } = require('{skillDir}/scripts/scanner.js');
// 用户需求:"帮我生成项目驾驶舱,重点关注任务延期和资源负载"
const domain1 = '任务延期情况';
const domain2 = '资源负载分布';
const result1 = resolveDomain(domain1, { projectId: 61 });
const result2 = resolveDomain(domain2, { projectId: 61 });
if (!result1.found) {
// 返回明确提示,让用户知道哪些不支持
return { supported: false, suggestion: result1.suggestion };
}
// 合并到 sqlMap
const sqlMap = {
...(result1.found ? { [result1.entry.key]: result1.entry } : {}),
...(result2.found ? { [result2.entry.key]: result2.entry } : {}),
};
// 调用 cockpit 生成器
const { generateCockpitChart } = require('{skillDir}/src/generate/page.js');
const html = await generateCockpitChart(sqlMap, { projectId: 61, verify: true });
resolveDomain 内部流程
用户业务域描述(如"任务延期情况")
↓
classifyDomain()
├── DOMAIN_TABLE_MAP 精确/模糊匹配 → 表名
└── 匹配失败 → searchTables() 扫描 tables.json
↓
getTableSchema() → 读取表字段结构
↓
inferSpecFromDomain() → 推断 type / chartType / kpiType / thresholds
↓
buildSQL() → 生成 SQL(自动 JOIN、WHERE、LIMIT)
↓
返回完整 entry: { key, sql, type, title, scope, spec }
域 → 表 映射(DOMAIN_TABLE_MAP)
| 域关键词 | 表 | 说明 |
|---|---|---|
| 任务/延期/task | wsd_plan_task | 任务主表 |
| 项目/project | wsd_plan_project | 项目主表 |
| 风险/risk | wsd_risk_register | 风险登记台账 |
| 资源/负载/resource | wsd_plan_taskrsrc | 资源分配表 |
| 里程碑/milestone | wsd_plan_task (task_type IN 2,3) | 里程碑是任务子集 |
| 会议/meeting | wsd_comu_meeting | 会议表 |
| 交付物/deliverable | wsd_plan_delvassign | 交付物表 |
spec 推断规则
| 域关键词 | type | chartType/kpiType | 说明 |
|---|---|---|---|
| 健康度/health | kpi | health_score | 健康度评分 |
| 风险数/风险敞口 | kpi | count | 风险项数计数 |
| 完成率指标 | kpi | avg_percent | 完成率百分比 |
| 热力/四象限 | chart | scatter | 风险热力图 |
| 时间线/里程碑 | chart | timeline | 里程碑时间线 |
| 看板/kanban | chart | kanban | 任务看板 |
| 甘特/gantt | chart | gantt | 甘特图 |
| 资源负载 | chart | bar | 部门资源柱图 |
| 占比/分布/pie | chart | pie | 饼图 |
| 趋势/trend | chart | line | 趋势折线图 |
未知域处理(found: false)
const r = resolveDomain('XYZ业务域', {});
if (!r.found) {
// r.suggestion 包含原因
// 仍然可以尝试 searchTables(keyword) 手动查找相关表
const alternatives = searchTables('XYZ关键词');
if (alternatives.length > 0) {
// 可以让用户选择对应的表
}
}
手动扩展 DOMAIN_TABLE_MAP
如需支持新的业务域,在 knowledgeBaseScanner.js 的 DOMAIN_TABLE_MAP 中添加:
const DOMAIN_TABLE_MAP = {
// ... 现有映射
'设备保修期': { table: 'wsd_equip_warranty', comment: '设备保修' },
'采购合同': { table: 'wsd_bud_contract', comment: '合同' },
};
说明:knowledgeBaseScanner.js 同时扫描 tables.json,即使不在 DOMAIN_TABLE_MAP 中,也可以通过关键词搜索定位到对应表(searchTables 支持表名/表注释/字段名搜索)。
auto-evolution 持久化
resolveDomain() 首次推理成功时会自动将新条目写入 knowledge/shared/evolved_domains.json,下次相同域请求直接命中,无需重新推理。
如需清除沉淀记录,删除 knowledge/shared/evolved_domains.json 即可(技能会重新初始化为空对象)。
SQL 生成方法
主 agent 直接生成(不调用外部 LLM)
主 agent 基于知识库上下文直接生成 SQL:
- 读取安全规则(sql_generation_rules.md)— 了解铁律
- 读取字段映射(field_mapping.json)— 了解字段名和枚举值
- 读取 KPI 公式(kpi_formulas.json)— 了解健康度、挣值等计算逻辑
- 生成 SQL — 根据用户问题结合知识库上下文构造查询
- 验证 SQL — 调用
src/verify/index.js的verify()确认可执行(默认 unified 模式:DB + API 同时验证) - 返回结果 — 输出 SQL 和查询结果
何时使用动态参数
核心原则:
- 凡是前端需要动态传入的值,一律用
?占位 - 凡是用户意图包含动态时间(今天、本月、本年),用
?占位 - 静态的枚举值、确定的项目ID,可直接写死
| 场景 | 示例 | SQL 写法 |
|---|---|---|
| 用户说"当前" | "截至今天的计划工时" | WHERE plan_time >= ? |
| 用户说"本月" | "本月风险分布" | WHERE create_time >= ? |
| 用户选择项目 | 项目下任务统计 | WHERE project_id = ? |
| 前端需动态计算 | 当前日期范围 | AND plan_time >= ? AND plan_time <= ? |
常见错误:
- ❌
WHERE create_time >= '2026-01-01'— 硬编码日期,不灵活 - ✅
WHERE create_time >= ?— 前端传参,动态适配
SQL 生成示例
问题: 某项目的延期未完成任务有哪些?
生成的 SQL:
SELECT t.id, t.task_name, t.status, t.feedback_status, t.plan_end_time,
u.user_name AS assignee_name, t.complete_pct
FROM wsd_plan_task t
LEFT JOIN wsd_sys_user u ON u.id = t.user_id
WHERE t.project_id = ?
AND t.DEL = 0
AND t.feedback_status IN ('0', '1')
AND t.plan_end_time < CURDATE()
ORDER BY t.plan_end_time
LIMIT 20
带动态参数的 SQL 示例
问题:截至今天的资源负载分布
分析:
- 用户说"今天" → 需要动态日期
- 资源负载按部门统计 → 不用按项目过滤
- 前端需要传入起始日期和结束日期
生成的 SQL:
SELECT r.ORG_ID, o.ORG_NAME as dept_name,
COUNT(DISTINCT r.ID) as resource_count,
SUM(t.BUDGET_QTY) / 8 as total_plan_days,
COUNT(DISTINCT r.ID) * ? as total_capacity_days,
SUM(t.BUDGET_QTY) / 8 / (COUNT(DISTINCT r.ID) * ?) * 100 as load_rate
FROM wsd_plan_taskrsrc t
JOIN wsd_rsrc_user r ON t.RSRC_ID = r.ID
LEFT JOIN wsd_sys_org o ON r.ORG_ID = o.ID
WHERE r.ORG_ID IS NOT NULL
AND t.PLAN_START_TIME >= ?
AND t.PLAN_START_TIME <= ?
GROUP BY r.ORG_ID, o.ORG_NAME
ORDER BY load_rate DESC
LIMIT 10
前端调用:
const today = new Date()
const startOfYear = new Date(today.getFullYear() + '-01-01')
const totalDays = Math.ceil((today - startOfYear) / (1000 * 60 * 60 * 24))
const workingDays = Math.round(totalDays * 252 / 365) // 工作日 = 总天数 × 252/365
executeQuery('resourceLoad', [workingDays, workingDays, startOfYear.toISOString().split('T')[0], today.toISOString().split('T')[0]])
// params[0] → working_days(容量公式)
// params[1] → working_days(负载率公式)
// params[2] → 起始日期
// params[3] → 结束日期
资源负载算法(2026-03-30 更新):
负载率 = Y / (N × X) × 100%
Y = 预估工时人天 = SUM(BUDGET_QTY) / 8
N = 资源人员数量 = COUNT(DISTINCT RSRC_ID)
X = 工作日天数 = 日期范围内总天数 × (252/365)
阈值:<70% 空闲 / 70-90% 正常 / >90% 过载
调用脚本
src/verify/index.js — SQL 验证(统一验证引擎)
提供三种验证模式,默认使用 unified(DB 直连 + API 端到端同时验证):
# 默认:unified(DB 直连 + API 同时验证,以 DB 为准)
node {skillDir}/src/verify/index.js --sql "SELECT ..."
# 仅 MySQL 直连
node {skillDir}/src/verify/index.js --sql "SELECT ..." --source db
# 仅 HTTP API
node {skillDir}/src/verify/index.js --sql "SELECT ..." --source api
批量 HTML 端到端验证(Cockpit 页面生成后自动调用,无需手动):
const { batchVerifyHtml } = require('{skillDir}/src/verify/index.js');
const result = await batchVerifyHtml('/path/to/cockpit.html', { projectId: 61 });
// result: { passed, failed, skipped, results }
if (result.failed > 0) throw new Error('HTML 端到端验证失败');
统一返回格式:
{
"ok": true,
"rows": 3,
"fields": ["ID", "TASK_NAME", "PLAN_END_TIME"],
"sampleRows": [...],
"sources": {
"mysql": { "ok": true, "rows": 3, "fields": [...], "error": null },
"api": { "ok": true, "rows": 3, "error": null }
},
"error": null
}
src/security/encryptSql.js — AES-256-CBC 加密
对 SQL 进行加密,用于嵌入 HTML 页面:
node {skillDir}/src/security/encryptSql.js encrypt "SELECT * FROM ..."
返回:
{
"ciphertext": "a1b2c3...",
"iv": "x9y8z7..."
}
⚠️ 重要:加密逻辑不得自行重写
加密算法为 AES-256-CBC + HMAC-SHA256(Encrypt-then-MAC),实现细节在 src/security/encryptSql.js 中。其他脚本需要批量加密 SQL 时,必须 require 该模块,禁止自行实现加密逻辑。
// ✅ 正确:从 src/security/encryptSql.js 引入
const { encrypt } = require('{skillDir}/src/security/encryptSql.js');
const { ciphertext, iv } = encrypt(sql);
// ❌ 错误:自行实现(会导致 HMAC 验签失败)
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
// ...
src/generate/page.js — 页面生成器(流程二 + 流程三)
支持单图表和驾驶舱两种生成模式:
单图表模式(流程二):
node {skillDir}/src/generate/page.js "<sql>" --title "<标题>" --type <chartType>
驾驶舱模式(流程三):
node {skillDir}/src/generate/page.js \
--template cockpit \
--charts projectList.sql,kpi_health.sql,riskHeatmap.sql \
--title "项目驾驶舱" \
--project-id 61 \
--output cockpit.html
注意:
--charts参数接收的是 SQL 文件路径,文件内容由主 agent 预先准备。
模板管理
模板文件位置
| 文件 | 路径 | 说明 |
|---|---|---|
| 单图表模板 | skills/templates/html_page_template.html | 技能自带,始终不变 |
| 驾驶舱基线模板 | skills/templates/cockpit_template.html | 首次生成后存入,布局重构时更新 |
| 驾驶舱当前模板 | workspace/templates/cockpit_current.html | 用户当前最新版本,实时跟随用户调整 |
模板加载优先级:
workspace/templates/cockpit_current.html ← 优先(用户当前版本)
↓ 不存在时
skills/templates/cockpit_template.html ← 降级(技能基线)
模板生命周期
| 场景 | 触发条件 | 结果 |
|---|---|---|
| 首次生成 | 用户首次提出驾驶舱需求(附带材料) | 生成 → 存入 cockpit_current.html + cockpit_template.html |
| 数据更新 | 用户只换了数据范围/时间 | 读取 cockpit_current.html → 注入新数据 |
| 局部微调 | 用户说"左侧调窄/颜色调深" | 修改 cockpit_current.html → 下次输出即反映调整 |
| 布局重构 | 用户换了布局设计或加 --regenerate | 重新分析材料 → 覆盖 cockpit_current.html + cockpit_template.html |
🚨 模板强制规范(所有模板必须遵守)
无论内置模板还是 regenerate 新生成的模板,无论驾驶舱还是单图表,都必须包含鉴权组件。 这是技能层面的设计约束,不满足则生成流程报错退出。
通用规范(所有模板类型)
必须包含的 JS 函数
| 函数 | 说明 |
|---|---|
getToken() | 三级优先级获取 token:currentToken 缓存 → sessionStorage → 弹窗登录 |
doLogin() | 调用 ${API_AUTH_BASE}/jwt/token 登录,写入 sessionStorage.setItem('token') |
必须包含的 DOM / 变量
| 组件 | 说明 |
|---|---|
id="loginModal" | 登录弹窗 DOM |
const API_AUTH_BASE = '{{API_AUTH_BASE}}' | 认证接口变量声明 |
| CryptoJS CDN | <script src="...crypto-js...">,用于 AES 密码加密 |
sessionStorage.setItem('token', ...) | 登录成功后必须写入 |
sessionStorage.getItem('token') | getToken() 必须读取以复用 ACM token |
tokenPromise 并发保护 | if (tokenPromise) return tokenPromise |
禁止事项
- ❌ 在
doLogin()的 body 中硬编码 password 字符串 - ❌ 使用字符串拼接构造
${API_BASE}/jwt/token(须用${API_AUTH_BASE}变量)
驾驶舱模板专用
占位符
| 占位符 | 说明 |
|---|---|
{{ENCRYPTED_SQL}} | AES-256-CBC 加密后的 SQL,JSON 格式(key → {ciphertext, iv}) |
{{SQL_PLAIN}} | 明文 SQL(供 buildParams 运行时校验 ? 数量),JSON 格式 |
{{CHART_CONFIG}} | 图表配置数组,JSON 格式 |
{{API_BASE}} | API 基础路径 |
{{API_AUTH_BASE}} | 认证 API 基础路径 |
JS 函数
| 函数 | 说明 |
|---|---|
executeQuery(key, params) | 调后端 execute 接口,携带 Authorization header |
buildParams(cfg) | 组装参数数组(含 ? 占位符数量校验),projectId 置于最前面 |
loadAllData() | 加载所有图表数据 |
单图表模板专用
占位符
| 占位符 | 说明 |
|---|---|
{{ENCRYPTED_SQL}} | AES-256-CBC 加密后的 SQL |
{{IV}} | AES 加密 IV |
{{API_BASE}} | API 基础路径 |
{{API_AUTH_BASE}} | 认证 API 基础路径 |
{{CHARTS_CONFIG}} | 图表配置 |
JS 函数
| 函数 | 说明 |
|---|---|
fetchData() | 调 API 获取数据 |
校验机制
| 场景 | 校验方式 |
|---|---|
| 内置 cockpit 模板 | validateTemplates() → cockpit_template.html |
| 内置单图表模板 | validateTemplates() → html_page_template.html |
| regenerate 驾驶舱新模板 | validateTemplateContent(html, { type: 'cockpit' }) |
| regenerate 单图表新模板 | validateTemplateContent(html, { type: 'single-chart' }) |
模板生成流程
触发:首次生成驾驶舱 或 布局重构
用户提供材料(cockpit.png + cockpit.xlsx 或详细文字描述)
↓
agent 分析材料
├─ 布局结构(顶部/左侧/右侧/中间区块)
├─ 组件类型(KPI gauge / ECharts 类型 / 侧边栏样式)
└─ 样式定义(配色/字体/间距)
↓
生成完整 HTML(含 CSS/ECharts/鉴权逻辑 + 占位符)
↓
存入 workspace/templates/cockpit_current.html
同时更新 skills/templates/cockpit_template.html(基线)
↓
[deploy=true] 自动部署到 acm_www/static/(不存在时兜底到 nl2sql_output/;CDN 路径替换为本地相对路径)
局部微调
触发:用户说"把左侧列表宽度调窄一点"
const html = await generateCockpitChart(sqlMap, {
projectId: 61,
tweak: '左侧宽度调窄',
deploy: true // 自动部署到 acm_www/static/(不存在时兜底到 nl2sql_output/)
});
// cockpit_current.html 被更新,下次直接生效
支持的微调关键词:
| 关键词 | 效果 |
|---|---|
| 左侧宽度调窄 | 侧边栏宽度 → 180px |
| 左侧宽度调宽 | 侧边栏宽度 → 280px |
| KPI放大 | KPI 卡片尺寸放大 |
| KPI缩小 | KPI 卡片尺寸缩小 |
| 颜色加深 | 主色调 → #1a3a5c |
| 颜色调浅 | 主色调 → #4a90d9 |
| 去掉右侧 | 隐藏右侧活动流区块 |
| 去掉顶部 | 隐藏顶部导航区块 |
微调机制:修改直接作用于 cockpit_current.html,不影响基线模板 cockpit_template.html。用户每次微调都实时更新同一个文件。
src/generate/page.js 调用方式
const { generateCockpitChart, resolveCockpitTemplate, applyTweak } = require('{skillDir}/src/generate/page.js');
// 正常使用(读取 cockpit_current.html)
const html = await generateCockpitChart(sqlMap, {
title: '项目驾驶舱',
projectId: 61,
verify: true,
deploy: true // 生成后自动部署(兜底目录:nl2sql_output/)
});
// 局部微调(应用 tweak 并更新 cockpit_current.html)
const html2 = await generateCockpitChart(sqlMap, {
title: '项目驾驶舱',
projectId: 61,
tweak: '左侧宽度调窄'
});
// 强制重新生成模板(覆盖 cockpit_current.html + 存档旧版本)
const html3 = await generateCockpitChart(sqlMap, {
title: '项目驾驶舱',
projectId: 61,
regenerate: true,
templateContent: newTemplateHtml // 从用户新材料生成
});
占位符规范
| 占位符 | 注入内容 | 格式 |
|---|---|---|
{{PAGE_TITLE}} | 页面标题 | 字符串 |
{{CHART_CONFIG}} | 图表配置数组 | JSON 字符串 |
{{ENCRYPTED_SQL}} | SQL 加密结果映射 | JSON 字符串 {"key": {"ciphertext","iv"}} |
{{DEFAULT_PROJECT_ID}} | 默认项目 ID | 数字字符串 |
{{API_BASE}} | API 基础路径 | URL 字符串 |
输出
NL→SQL 预览输出
{
"sql": "SELECT t.id, t.task_name, t.status, t.feedback_status, t.plan_end_time FROM wsd_plan_task t WHERE t.project_id = ? AND t.DEL = 0 AND t.feedback_status IN ('0','1') AND t.plan_end_time < CURDATE() LIMIT 20",
"explanation": "已根据您的问题生成SQL查询,共返回5条延期未完成的任务。延期判定条件:feedback_status IN ('0','1') 且 plan_end_time < 当前日期。",
"data": [
{"id": 1, "task_name": "XXX任务", "status": "RELEASE", "feedback_status": "1", "plan_end_time": "2026-03-01"},
...
],
"total": 5,
"ok": true
}
结构化 KPI 输出(给驾驶舱用)
KPI 数据建议返回结构化格式,前端根据 status 直接渲染颜色,无需二次判断:
{
"kpi_progress": {
"value": 87.50,
"formatted": "87.50%",
"status": "normal",
"trend": "+2.3%",
"thresholds": { "warning": 80, "danger": 60 }
},
"kpi_health": {
"value": 85,
"formatted": "85",
"status": "normal",
"thresholds": { "warning": 80, "danger": 60 }
},
"kpi_risk": {
"value": 10,
"formatted": "10项",
"status": "danger",
"thresholds": { "warning": 8, "danger": 3 }
}
}
status 颜色映射:
| status | 颜色 | 含义 | 典型阈值(健康度) |
|---|---|---|---|
normal | 🟢 绿色 | 正常/达标 | ≥80分 |
warning | 🟡 橙色 | 轻度风险 | 60-80分 |
danger | 🔴 红色 | 严重问题 | <60分 |
SQL 层返回原始聚合值(不含 status),status 由驾驶舱 JS 根据 kpi_spec_schema.json 的阈值规则计算得出。
详细 Schema 定义见:knowledge/shared/kpi_spec_schema.json
HTML 页面输出
生成完整的自包含 HTML 文件,包含:
- ECharts 5.x(CDN加载)
- 已加密的 SQL(
{{ENCRYPTED_SQL}}) - AES-256-CBC IV(
{{IV}}) - JWT 鉴权令牌(
{{JWT_TOKEN}}) - 指定的图表类型和配置
- 数据获取和渲染逻辑(自动携带 Authorization header 请求后端)
输出路径:{outputPath}/{chartTitle}_{timestamp}.html
图表类型与配置
| 图表类型 | 适用场景 | ECharts类型 |
|---|---|---|
line | 趋势图、时间序列 | line |
bar | 对比图、柱状图 | bar |
pie | 占比图、饼图 | pie |
heatmap | 风险热力图(X/Y轴) | scatter(热力配色) |
table | 明细数据列表 | 无(纯HTML表格) |
数值显示规范
生成 HTML 页面时,数值字段必须遵循以下格式规范:
| 字段类型 | 显示格式 | 示例 |
|---|---|---|
百分比字段(_PCT、_RATE、COMPLETE) | 2位小数 + % | 87.50% |
金额字段(_SUM、_BUDGET、_COST) | 2位小数 | ¥1,234.56 |
比率字段(_RATIO) | 2位小数 + % | 95.00% |
计数字段(_COUNT) | 整数(千分位分隔) | 1,234 |
| 普通数值 | 2位小数 | 123.45 |
百分比统计规则:
- 百分比字段统计应计算 平均值(AVG),而非求和(SUM)
- 示例:
COMPLETE_PCT的统计应显示"平均"而非"合计"
金额统计规则:
- 金额字段统计应计算 求和(SUM)
- 示例:
PLAN_SUM的统计应显示"合计"
实现参考(src/generate/page.js):
const NUMERIC_FIELD_PATTERNS = [
{ pattern: /_PCT$/i, type: 'numeric', agg: 'avg' }, // 百分比→平均
{ pattern: /_RATE$/i, type: 'numeric', agg: 'avg' }, // 比率→平均
{ pattern: /COMPLETE/i, type: 'numeric', agg: 'avg' }, // 完成率→平均
{ pattern: /_SUM$/i, type: 'numeric', agg: 'sum' }, // 金额→求和
{ pattern: /_BUDGET$/i, type: 'numeric', agg: 'sum' }, // 预算→求和
{ pattern: /_COUNT$/i, type: 'numeric', agg: 'sum' }, // 计数→求和
];
SQL 生成铁律
生成 SQL 前必须遵守:
- 永远不猜测,先求证事实 — 字段名/列名必须对照
tables.json确认实际名称,字段映射文件(如field_mapping.json)中的字段名若有误,以tables.json为准 - 只生成 SELECT 查询 — 禁止 UPDATE/DELETE/INSERT/DROP/TRUNCATE
- 强制租户隔离 — 后端通过
getLoginUserVo().getTenantId()自动注入租户ID,Skill 生成的 SQL 不需要包含AND TENANT_ID = ?,也不需要传 tenantId 参数。src/verify/index.js本地验证时不需要--tenant-id参数。 - 强制 DEL=0 — 几乎所有表加
AND DEL = 0,但wsd_risk_register、wsd_base_dict、wsd_comu_meeting、wsd_comu_meetingaction、wsd_plan_taskrsrc等无 DEL 字段的表禁止加此条件 - 参数化查询 —
- 前端动态值用
?占位:WHERE project_id = ? - 动态日期范围用
?占位:AND plan_time >= ? AND plan_time <= ? - 前端传入 params 数组替换占位符
- 禁止字符串拼接 SQL
- 前端动态值用
- LIMIT 限制 — 默认 20 条,最大不超过 100 条
- 枚举值正确格式 —
feedback_status='0'(字符串)、status='RELEASE'(大写) - 禁止字段 — 不得查询 CREATOR/PASSWORD/TOKEN 等敏感字段
SQL 验证失败处理流程:
验证失败 → 分析错误原因 → 重新生成 SQL → 再次验证 → 直至通过 → 加密嵌入
详细规则见:knowledge/shared/sql_generation_rules.md
错误处理
| 错误类型 | 处理方式 |
|---|---|
| SQL 验证失败 | 返回 valid: false + 错误信息,不执行 |
| 数据库连接失败 | 返回错误,不生成页面 |
| 加密失败 | 返回错误,不生成页面 |
目录结构
skills/data-query/
├── SKILL.md # 本文件
├── database_specs/ # 数据库规范文档
│ ├── dialect/
│ │ ├── DM.md # 达梦数据库规范
│ │ ├── MySQL.md # MySQL 数据库规范
│ │ └── Oracle.md # Oracle 数据库规范
│ └── sharding/
│ └── ROUTING_RULES.md # 分片路由规则
├── knowledge/ # 知识库数据
│ ├── dm/ # 达梦数据库
│ │ ├── field_mapping.json # 字段映射
│ │ └── tables.json # 表结构
│ ├── mysql/ # MySQL 数据库
│ │ ├── field_mapping.json # 字段映射
│ │ └── tables.json # 表结构
│ ├── oracle/ # Oracle 数据库
│ │ ├── field_mapping.json # 字段映射
│ │ └── tables.json # 表结构
│ └── shared/ # 共享配置
│ ├── evolved_domains.json # 自动进化域映射
│ ├── kpi_formulas.json # KPI计算公式
│ ├── kpi_spec_schema.json # KPI规格定义
│ ├── sql_generation_rules.md # SQL生成规则
│ └── sharding/
│ ├── KNOWN_BUGS.md # 已知分片Bug
│ └── TABLE_DISTRIBUTION.md # 表分布
├── prompts/
│ └── nl2sql_prompt.md # agent 提示词模板
├── scripts/ # 独立工具(不引用 src/ 模块)
│ ├── encrypt_for_page.js # SQL 加密工具(Node)
│ ├── scanner.js # 知识库扫描器(Node)
│ └── knowledge/ # 知识库构建脚本(Python)
│ ├── convert_knowledge.py # 通用知识库转换脚本
│ ├── generate.py # 知识库生成脚本
│ └── requirements.txt # Python依赖
├── src/
│ ├── core/
│ │ ├── config.js # 配置加载(读取 config.json)
│ │ ├── dialect.js # 数据库方言封装
│ │ ├── errors.js # 错误处理
│ │ └── validator.js # 验证工具
│ ├── deploy/
│ │ └── index.js # 部署(优先 acm_www/static/,兜底 nl2sql_output/)
│ ├── generate/
│ │ ├── chartSpecSchema.js # 图表配置Schema
│ │ ├── page.js # 页面生成器(CLI 入口)
│ │ └── validate_page.js # HTML 完整性校验 + auto-fix
│ ├── security/
│ │ ├── encryptSql.js # AES-256-CBC 加密核心
│ │ └── passwordEncrypt.js # 密码加密
│ ├── templates/
│ │ ├── index.js # 模板管理
│ │ └── validate.js # 模板验证
│ ├── verify/
│ │ └── index.js # SQL 验证引擎
│ └── index.js # 主入口
├── templates/
│ ├── html_page_template.html # 单图表页面模板(流程二)
│ └── cockpit_template.html # 驾驶舱页面模板(流程三)
├── .gitignore # Git忽略文件
├── CHANGES.md # 变更记录
├── README.md # 项目说明
├── config.json # 配置文件
├── package-lock.json # NPM依赖锁定
└── package.json # NPM配置