Back to skills
extension
Category: Data & AnalyticsAPI key required

NW ERP SKILL

查询ERP系统中的业务单据数据并可视化。支持查询单据类型列表、字段元数据、单据列表(含汇总行)、单据明细和聚合统计。查询结果可通过 show_widget 工具渲染 Chart.js 图表(饼图、柱状图、折线图等)进行可视化展示。用户可以通过自然语言询问ERP中的业务数据,如"最近一年的销售合同金额是多少""分析销售合同客户分布"。

personAuthor: user_a7d36aaahubcommunity

ERP数据查询技能

你可以通过HTTP接口查询ERP系统中的业务单据数据。所有请求需要在请求头中携带 X-MCP-SECRET 参数进行鉴权,该密钥与个人ERP账号一一对应,代表个人角色,MCP会根据角色查询相应的数据。

环境变量

本技能依赖以下环境变量,每个用户必须配置自己的密钥

  • ERP_BASE_URL:ERP系统访问地址(如 http://192.168.1.100:8080
  • APP_SECRET:你的MCP访问密钥(从ERP个人资料页面"生成MCP密钥"按钮获取)

⚠️ 重要:首次使用前必须配置

如果 ERP_BASE_URLAPP_SECRET 未配置或为空,请停止执行,先引导用户完成配置:

配置步骤(引导用户操作)

  1. 获取密钥:告诉用户登录 ERP 系统 → 个人资料页面 → 点击"生成MCP密钥"按钮 → 复制密钥

  2. 配置方式(二选一):

    方式一:在对话中直接告诉 AI(推荐手机用户)

    用户说:"我的ERP地址是 xxx,密钥是 xxx",AI 自动将配置写入 ~/.codebuddy/settings.jsonenv 字段。

    方式二:手动编辑配置文件

    ~/.codebuddy/settings.json 中添加:

    {
      "env": {
        "ERP_BASE_URL": "https://你的ERP地址",
        "APP_SECRET": "你的密钥"
      }
    }
    
  3. 验证配置:配置完成后,调用 list_bill_types 接口验证是否可用

所有 HTTP 请求中使用以下占位符(运行时自动从环境变量替换):

  • 地址:${ERP_BASE_URL}
  • 密钥:${APP_SECRET}

可用工具

1. list_bill_types — 查询可访问的单据类型

查询ERP中当前用户可访问的所有单据类型。返回 billtypecode(单据类型编码)和 billtypename(单据类型名称)。

当用户询问的业务模块不明确时,先调用此接口查看有哪些可用单据类型。例如:用户说"销售合同",此接口可能返回 billtypecode="xsht", billtypename="销售合同"

  • 请求方式:GET
  • URL${ERP_BASE_URL}/itf/common/mcp/listBillTypes.json
  • 请求头X-MCP-SECRET: ${APP_SECRET}
  • 参数:无
  • 返回示例
{
  "success": true,
  "data": [
    {"billtypecode": "xsht", "billtypename": "销售合同", "ifdoc": false, "vmodulename": "销售管理"},
    {"billtypecode": "cgdd", "billtypename": "采购订单", "ifdoc": false, "vmodulename": "采购管理"}
  ]
}

2. get_bill_fields — 查询单据字段元数据

查询指定单据类型的字段元数据。返回每个字段的 itemkey(字段编码)、showname(中文名)、datatype(数据类型)、listshow(是否列表显示)。

当需要知道"合同金额"对应哪个字段时,调用此接口查看。例如:用户说"金额",此接口可能返回 itemkey="ntotalamount", showname="合同总金额"

  • 请求方式:GET
  • URL${ERP_BASE_URL}/itf/common/mcp/getBillFields.json?billtype=${billtype}
  • 请求头X-MCP-SECRET: ${APP_SECRET}
  • 参数
    • billtype(必填):单据类型编码
  • 返回示例
{
  "success": true,
  "data": [
    {"itemkey": "vbillcode", "showname": "单据编号", "datatype": 0, "listshow": true},
    {"itemkey": "ddate", "showname": "签订日期", "datatype": 3, "listshow": true},
    {"itemkey": "ntotalamount", "showname": "合同总金额", "datatype": 2, "listshow": true}
  ]
}

3. query_bill_list — 查询单据列表(含汇总行)

查询单据列表数据,返回记录列表和汇总行(summaryRow)。summaryRow 包含数值字段的合计值,如总金额。

支持 dateField + dateFrom + dateTo 进行日期范围过滤。支持 conditions 传入自定义查询条件(SQL WHERE片段,如 "billstatus=1")。

当用户问"XX金额是多少"时,调用此接口获取 summaryRow 中的合计值。返回数据中 fieldNames 是 itemkey → 中文名 的映射,帮助你理解字段含义。

  • 请求方式:POST
  • URL${ERP_BASE_URL}/itf/common/mcp/queryBillList.json
  • 请求头X-MCP-SECRET: ${APP_SECRET}Content-Type: application/json
  • 请求体
{
  "billtype": "xsht",
  "pageNo": 1,
  "pageSize": 50,
  "dateField": "ddate",
  "dateFrom": "2025-07-22",
  "dateTo": "2026-07-22",
  "conditions": "billstatus=1",
  "orderBy": "ddate desc"
}
  • 参数说明
    • billtype(必填):单据类型编码
    • pageNo:页码,默认1
    • pageSize:每页条数,默认50,最大200
    • dateField:日期过滤字段(itemkey,如 "ddate"
    • dateFrom:日期起(如 "2025-07-22"
    • dateTo:日期止(如 "2026-07-22"
    • conditions:自定义SQL条件(如 "billstatus=1 AND ntotalamount>10000"
    • orderBy:排序(如 "ddate desc"
  • 返回示例
{
  "success": true,
  "data": {
    "summaryRow": {"ntotalamount": 12345678.90},
    "totalRecords": 56,
    "records": [
      {"vbillcode": "XSHT-2026-0001", "ddate": "2026-01-15", "ntotalamount": 50000.00, "billstatus": 1},
      {"vbillcode": "XSHT-2026-0002", "ddate": "2026-02-20", "ntotalamount": 120000.00, "billstatus": 1}
    ],
    "pageSize": 50,
    "pageNo": 1,
    "totalPage": 2,
    "fieldNames": {"vbillcode": "单据编号", "ddate": "签订日期", "ntotalamount": "合同总金额", "billstatus": "单据状态"}
  }
}

4. get_bill_detail — 查询单据明细

查询单据明细数据,返回完整的表头(HEADER)和表体(BODY)数据。当需要查看某张具体单据的详细信息时调用。

  • 请求方式:GET
  • URL${ERP_BASE_URL}/itf/common/mcp/getBillDetail.json?billtype=${billtype}&billid=${billid}
  • 请求头X-MCP-SECRET: ${APP_SECRET}
  • 参数
    • billtype(必填):单据类型编码
    • billid(必填):单据ID
  • 返回示例
{
  "success": true,
  "data": {
    "billtype": "xsht",
    "billid": "xxx-xxx-xxx",
    "data": {
      "HEADER": {"vbillcode": "XSHT-2026-0001", "ntotalamount": 50000.00, "ddate": "2026-01-15"},
      "BODY": {"sale_order_b": [{"vbillcode_b": "XSHT-2026-0001-01", "nnum": 100, "nprice": 500.00}]}
    },
    "fieldNames": {"vbillcode": "单据编号", "ntotalamount": "合同总金额", "ddate": "签订日期"}
  }
}

5. query_bill_summary — 聚合统计查询

对指定字段执行聚合统计(sum/count/avg/max/min),支持条件过滤。当用户问"总金额是多少""有多少张单据"时,可用此接口。也可以用 query_bill_listsummaryRow 获取合计,此接口更灵活。

  • 请求方式:POST
  • URL${ERP_BASE_URL}/itf/common/mcp/queryBillSummary.json
  • 请求头X-MCP-SECRET: ${APP_SECRET}Content-Type: application/json
  • 请求体
{
  "billtype": "xsht",
  "aggregateField": "ntotalamount",
  "aggregateType": "sum",
  "dateField": "ddate",
  "dateFrom": "2025-07-22",
  "dateTo": "2026-07-22"
}
  • 参数说明
    • billtype(必填):单据类型编码
    • aggregateField(必填):聚合字段(itemkey)
    • aggregateType:聚合类型,默认 sum。可选值:sum(求和)、count(计数)、avg(平均)、max(最大)、min(最小)
    • dateField:日期过滤字段
    • dateFrom:日期起
    • dateTo:日期止
    • conditions:自定义SQL条件
  • 返回示例
{
  "success": true,
  "data": {
    "billtype": "xsht",
    "billtypename": "销售合同",
    "aggregateField": "ntotalamount",
    "aggregateType": "sum",
    "value": 12345678.90,
    "fieldNames": {"ntotalamount": "合同总金额", "ddate": "签订日期"}
  }
}

使用流程

第一步:确定单据类型

当用户提到"销售合同""采购订单"等业务名词时,先调用 list_bill_types 查看可用的单据类型,将用户的业务名词匹配到对应的 billtypecode

例如:用户说"销售合同",在返回列表中找到 billtypename 包含"销售合同"的记录,获取其 billtypecode(如 "xsht")。

第二步:确定字段映射

当用户提到"金额""日期""客户"等字段名时,调用 get_bill_fields 查看该单据类型的字段列表,将用户的中文名匹配到对应的 itemkey

例如:用户说"金额",在返回字段列表中找到 showname 包含"金额"的字段,获取其 itemkey(如 "ntotalamount")。

第三步:查询数据

  • 用户问"XX金额是多少" → 调用 query_bill_list(设置日期范围),从返回的 summaryRow 中取合计值;或调用 query_bill_summaryaggregateType=sum)直接获取聚合值。
  • 用户问"列出最近XX的单据" → 调用 query_bill_list(设置日期范围和分页)。
  • 用户问"某张单据的详情" → 先通过 query_bill_list 找到 billid,再调用 get_bill_detail
  • 用户问"有多少张/数量" → 调用 query_bill_summaryaggregateType=count)。

第四步:分析数据

当用户要求"分析"时(如"分析销售合同的风险情况"),先调用 query_bill_list 获取足够的数据,然后你(LLM)根据返回的数据进行推理分析,输出分析报告。ERP不提供分析能力,分析由你完成。

第五步:可视化数据

查询结果默认应该用图表呈现,而不是仅输出表格。当返回的 records 含有可聚合的数值字段(金额、数量等)或可分组的维度字段(客户、状态、日期、物料等)时,主动调用 show_widget 工具渲染 Chart.js 图表,让用户像用 BI 工具一样直观看到数据。

详细规则和代码模板见下方「数据可视化」章节。

数据可视化

何时可视化(触发规则)

| 场景 | 是否可视化 | 说明 | |------|-----------|------| | 用户问"总金额是多少""有多少张" | ❌ 不画图 | 单一数值,文字回答即可 | | 用户问"列出最近 N 条"且 N ≤ 5 | ❌ 不画图 | 数据太少,表格更清晰 | | 返回记录 ≥ 3 条且含金额/数量字段 | ✅ 画图 | 至少画一张主图 | | 用户说"分析""趋势""分布""占比""排名""对比" | ✅ 画图 | 按意图选图 | | 用户说"画图""可视化""chart" | ✅ 画图 | 明确要求 | | 用户问"某张单据的详情" | ❌ 不画图 | 单据详情用文字/表格 |

图表类型选择

根据用户意图和数据形状选择:

| 用户意图 | 数据形状 | 推荐图表 | 示例问题 | |---------|---------|---------|---------| | 看占比/分布 | 1 个维度 + 1 个数值 | 饼图/环形图 | "各客户合同金额占比" | | 看排名/对比 | 1 个维度 + 1 个数值,维度 ≤ 15 | 水平柱状图 | "合同金额 TOP 10 客户" | | 看趋势 | 时间维度 + 1 个数值 | 折线图 | "近一年销售合同月度趋势" | | 看多维度对比 | 2 个维度 + 1 个数值 | 分组柱状图 | "各客户各月合同金额" | | 看状态分布 | 1 个状态维度 + 计数 | 环形图 | "销售合同状态分布" | | 看累计进度 | 时间 + 累计数值 | 折线图(填充) | "累计合同金额增长" |

默认策略:如果用户没有明确指定图表类型,且数据适合可视化,优先画一张水平柱状图(金额按维度排名),因为排名图信息密度最高、最实用。

数据转换规则

ERP 返回的 records 是字段 Map 数组,需转换为 Chart.js 数据集:

1. 按维度聚合(分组求和)

# Python 伪代码:按客户聚合合同总额
from collections import defaultdict
agg = defaultdict(float)
for r in records:
    key = r.get('customer_name', '未知')   # 维度字段
    val = float(r.get('total_contract_amount', 0) or 0)  # 数值字段
    agg[key] += val
# 排序取 TOP N
sorted_items = sorted(agg.items(), key=lambda x: -x[1])[:10]
labels = [x[0] for x in sorted_items]
values = [x[1] for x in sorted_items]

2. 按时间分组(月度趋势)

from collections import defaultdict
monthly = defaultdict(float)
for r in records:
    d = r.get('signingdate', '')[:7]  # 'YYYY-MM'
    if d:
        monthly[d] += float(r.get('total_contract_amount', 0) or 0)
# 按月份排序
labels = sorted(monthly.keys())
values = [monthly[k] for k in labels]

3. 数值字段清洗

  • ERP 数值字段可能返回字符串带千分位(如 "10,400,002.00"),需 float(str(val).replace(',', ''))
  • None / "" / "null" 统一当 0 处理

调用 show_widget 的规范

调用 show_widget 工具时:

  • 先调用 read_me 加载 chart 模块(首次画图前必须调用一次)
  • widget_code纯 HTML 片段(含 <canvas> + <script>),不要包含 <!DOCTYPE><html><head><body>
  • title 参数用简短中文描述(如"销售合同客户金额分布")
  • loading_messages 传 1-4 条中文加载提示

颜色规范

Canvas 无法读取 CSS 变量,必须用硬编码十六进制色值。从以下色板中选取(light 主题:50 填充 + 600 描边/文字):

| 色系 | 填充(50) | 主色(600) | 深色(800) | |------|---------|----------|----------| | 蓝 c-blue | #E6F1FB | #185FA5 | #0C447C | | 紫 c-purple | #EEEDFE | #534AB7 | #3C3489 | | 青 c-teal | #E1F5EE | #0F6E56 | #085041 | | 珊瑚 c-coral | #FAECE7 | #993C1D | #712B13 | | 粉 c-pink | #FBEAF0 | #993556 | #72243E | | 绿 c-green | #EAF3DE | #3B6D11 | #27500A | | 琥珀 c-amber | #FAEEDA | #854F0B | #633806 | | 红 c-red | #FCEBEB | #A32D2D | #791F1F | | 灰 c-gray | #F1EFE8 | #5F5E5A | #444441 |

多色系列(饼图/环形图按顺序循环使用):

const palette = ['#185FA5', '#534AB7', '#0F6E56', '#993C1D', '#993556', '#3B6D11', '#854F0B', '#A32D2D', '#5F5E5A'];

图表模板

模板 1:水平柱状图(金额排名 TOP N)

适用:合同金额按客户/物料/业务员排名。values 已按降序排好。

<div style="position: relative; width: 100%; height: 420px;">
  <canvas id="erpChart" role="img" aria-label="合同金额按客户排名水平柱状图">各客户合同金额排名柱状图。</canvas>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.js"></script>
<script>
const labels = /* ['客户A','客户B','客户C', ...] */;
const values = /* [10400002, 105787, 93000, ...] */;
const palette = ['#185FA5','#534AB7','#0F6E56','#993C1D','#993556','#3B6D11','#854F0B','#A32D2D','#5F5E5A'];
new Chart(document.getElementById('erpChart'), {
  type: 'bar',
  data: {
    labels: labels,
    datasets: [{
      label: '合同金额',
      data: values,
      backgroundColor: values.map((_, i) => palette[i % palette.length] + 'CC'),
      borderColor: values.map((_, i) => palette[i % palette.length]),
      borderWidth: 1
    }]
  },
  options: {
    indexAxis: 'y',
    responsive: true,
    maintainAspectRatio: false,
    plugins: {
      legend: { display: false },
      tooltip: {
        callbacks: {
          label: function(ctx) {
            return ' ' + ctx.parsed.x.toLocaleString('zh-CN', {minimumFractionDigits: 2, maximumFractionDigits: 2});
          }
        }
      }
    },
    scales: {
      x: {
        ticks: { callback: function(v) { return (v/10000).toFixed(0) + '万'; } },
        grid: { color: 'rgba(0,0,0,0.06)' }
      },
      y: { grid: { display: false } }
    }
  }
});
</script>

模板 2:环形图(占比分布)

适用:客户占比、状态分布、币种分布。

<div style="position: relative; width: 100%; height: 320px;">
  <canvas id="erpChart" role="img" aria-label="合同金额按客户占比环形图">各客户合同金额占比环形图。</canvas>
</div>
<div style="display: flex; flex-wrap: wrap; gap: 12px; margin-top: 8px; font-size: 12px; color: #5F5E5A;">
  <!-- 图例由 AI 根据数据生成,示例: -->
  <span style="display: flex; align-items: center; gap: 4px;">
    <span style="width: 10px; height: 10px; border-radius: 2px; background: #185FA5;"></span>客户A
  </span>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.js"></script>
<script>
const labels = /* ['客户A','客户B','客户C'] */;
const values = /* [10400002, 105787, 93000] */;
const palette = ['#185FA5','#534AB7','#0F6E56','#993C1D','#993556','#3B6D11','#854F0B','#A32D2D','#5F5E5A'];
new Chart(document.getElementById('erpChart'), {
  type: 'doughnut',
  data: {
    labels: labels,
    datasets: [{
      data: values,
      backgroundColor: values.map((_, i) => palette[i % palette.length]),
      borderColor: '#fff',
      borderWidth: 2
    }]
  },
  options: {
    responsive: true,
    maintainAspectRatio: false,
    cutout: '60%',
    plugins: {
      legend: { display: false },
      tooltip: {
        callbacks: {
          label: function(ctx) {
            const total = ctx.dataset.data.reduce((a,b) => a+b, 0);
            const pct = (ctx.parsed / total * 100).toFixed(1);
            return ' ' + ctx.label + ': ' + ctx.parsed.toLocaleString('zh-CN') + ' (' + pct + '%)';
          }
        }
      }
    }
  }
});
</script>

模板 3:折线图(时间趋势)

适用:月度/季度合同金额趋势。

<div style="position: relative; width: 100%; height: 320px;">
  <canvas id="erpChart" role="img" aria-label="合同金额月度趋势折线图">合同金额月度趋势折线图。</canvas>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.js"></script>
<script>
const labels = /* ['2025-01','2025-02','2025-03', ...] */;
const values = /* [50000, 120000, 80000, ...] */;
new Chart(document.getElementById('erpChart'), {
  type: 'line',
  data: {
    labels: labels,
    datasets: [{
      label: '合同金额',
      data: values,
      borderColor: '#185FA5',
      backgroundColor: 'rgba(24,95,165,0.1)',
      borderWidth: 2,
      fill: true,
      tension: 0.3,
      pointBackgroundColor: '#185FA5',
      pointRadius: 4
    }]
  },
  options: {
    responsive: true,
    maintainAspectRatio: false,
    plugins: {
      legend: { display: false },
      tooltip: {
        callbacks: {
          label: function(ctx) {
            return ' ' + ctx.parsed.y.toLocaleString('zh-CN', {minimumFractionDigits: 2, maximumFractionDigits: 2});
          }
        }
      }
    },
    scales: {
      y: {
        ticks: { callback: function(v) { return (v/10000).toFixed(0) + '万'; } },
        grid: { color: 'rgba(0,0,0,0.06)' }
      },
      x: { grid: { display: false } }
    }
  }
});
</script>

模板 4:分组柱状图(双维度对比)

适用:各客户在各月份的合同金额对比。datasets 为每个维度一条数据集。

<div style="position: relative; width: 100%; height: 360px;">
  <canvas id="erpChart" role="img" aria-label="各客户各月合同金额分组柱状图">各客户各月合同金额分组柱状图。</canvas>
</div>
<div style="display: flex; flex-wrap: wrap; gap: 12px; margin-top: 8px; font-size: 12px; color: #5F5E5A;">
  <!-- 图例由 AI 生成 -->
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.js"></script>
<script>
const months = /* ['2025-05','2025-06','2025-07'] */;
const palette = ['#185FA5','#534AB7','#0F6E56','#993C1D','#993556'];
const datasets = /* [
  { label: '客户A', data: [50000, 80000, 10400002] },
  { label: '客户B', data: [0, 105787, 93000] }
] */;
new Chart(document.getElementById('erpChart'), {
  type: 'bar',
  data: {
    labels: months,
    datasets: datasets.map((ds, i) => ({
      label: ds.label,
      data: ds.data,
      backgroundColor: palette[i % palette.length] + 'CC',
      borderColor: palette[i % palette.length],
      borderWidth: 1
    }))
  },
  options: {
    responsive: true,
    maintainAspectRatio: false,
    plugins: { legend: { display: false } },
    scales: {
      y: {
        ticks: { callback: function(v) { return (v/10000).toFixed(0) + '万'; } },
        grid: { color: 'rgba(0,0,0,0.06)' }
      },
      x: { grid: { display: false } }
    }
  }
});
</script>

多图组合

复杂分析场景可连续调用多次 show_widget,每次画一张图,图与图之间用文字过渡。例如分析销售合同时:

  1. 先画环形图(客户分布占比)
  2. 文字过渡:"下面是各客户的合同金额排名详情..."
  3. 再画水平柱状图(金额 TOP 排名)
  4. 文字过渡:"从时间趋势来看..."
  5. 最后画折线图(月度趋势)

不要把多张图塞进同一个 widget,每张图独立调用。

金额格式化

图表中的金额轴和 tooltip 统一用中文习惯:

  • 万级:(v/10000).toFixed(0) + '万'
  • 亿级:(v/100000000).toFixed(2) + '亿'
  • tooltip 原始值:v.toLocaleString('zh-CN', {minimumFractionDigits: 2, maximumFractionDigits: 2})

完整工作流示例

用户问"分析最近一年销售合同的客户分布和金额趋势":

  1. list_bill_types → 找到 salesorder
  2. get_bill_fields → 确认 signingdate(合同日期)、customer_name(客户)、total_contract_amount(合同总额)等字段
  3. query_bill_list(pageSize=200,日期范围=最近一年)→ 获取 records
  4. Python 脚本聚合:按客户分组求和、按月分组求和
  5. read_me(modules=["chart"]) → 加载图表模块
  6. show_widget 画环形图(客户占比)
  7. 文字过渡
  8. show_widget 画折线图(月度趋势)
  9. 文字总结分析结论

日期处理规则

  • "最近一年" → dateFrom = 今天减一年(YYYY-MM-DD),dateTo = 今天
  • "本月" → dateFrom = 本月1号,dateTo = 今天
  • "本季度" → dateFrom = 本季度第一天,dateTo = 今天
  • "今年" → dateFrom = 今年1月1日,dateTo = 今天
  • 日期格式始终为 YYYY-MM-DD
  • dateField 通常是签订日期(如 "ddate"),通过 get_bill_fields 确认具体字段名

返回数据理解

  • records:单据记录数组,每条记录是一个字段Map(key是itemkey)
  • summaryRow:汇总行,包含数值字段的合计(如 ntotalamount: 12345678.90
  • totalRecords:符合条件的总记录数
  • fieldNames:itemkey → 中文名 的映射,帮助你理解字段含义
  • valuequery_bill_summary返回):聚合计算结果

注意事项

  1. 你只能查询到当前用户角色权限范围内的数据(由 APP_SECRET 决定)
  2. 如果查询结果为空,告知用户可能没有权限或没有符合条件的单据
  3. 分析类问题需要你基于数据自行推理,ERP不提供分析能力
  4. conditions 参数只支持简单的 SQL WHERE 条件(如 "billstatus=1 AND ntotalamount>10000"),不支持子查询、注释、分号等
  5. 金额类字段返回的是数值,需要你格式化为人类可读的形式(如 12345678.901234.57万元
  6. 如果用户没有指定单据类型,先调用 list_bill_types 查看有哪些可用模块
  7. 如果用户没有指定日期范围,默认查询全部数据(不传 dateField/dateFrom/dateTo
  8. 所有请求必须携带请求头 X-MCP-SECRET,值为你的 APP_SECRET
  9. 可视化优先:查询返回 ≥3 条记录且含数值字段时,应主动用 show_widget 画图,而非仅输出表格
  10. 画图前必须先调用 read_me(modules=["chart"]) 加载图表设计规范,否则可能违反样式规则
  11. 数值清洗:ERP 数值字段可能返回带千分位的字符串(如 "10,400,002.00"),画图前必须 float(str(v).replace(',','')) 转为数字
  12. Canvas 硬编码颜色:Chart.js 的 canvas 无法读取 CSS 变量,所有颜色必须用十六进制硬编码
  13. 多图分多次调用:不要把多张图塞进一个 widget,每张图独立调用 show_widget,图之间用文字过渡