自訂報表功能設計
Arova Nexus — Phase 0 Product Definition | 2026-03-31

版本: v1.0 | 日期: 2026-03-26
目標: 讓使用者用自然語言描述需求,系統自動產出客製報表


一、核心概念:對話式報表建立

使用者不需要寫程式或編輯模板。透過 Chat UI 描述需求,AI Agent 自動:

  1. 理解使用者要什麼資料
  2. 判斷現有 Tools 能否滿足
  3. 組裝查詢、產出報表
  4. 如果需要新資料源,引導使用者或標記給開發團隊
使用者:「我想要一份報表,只看 Windows 伺服器的磁碟使用率,
         超過 70% 的用紅色標示,每週一寄給 IT 主管」

Agent:我來幫你建立這份報表。

       資料來源:LibreNMS API → /devices (os=windows) + /storage
       篩選條件:os = windows
       欄位:hostname, IP, 各磁碟使用率
       標示規則:> 85% 紅色, > 70% 黃色, 其餘綠色
       排程:每週一 09:00
       寄送:IT 主管 Email

       [預覽報表] [確認並儲存] [修改條件]

二、四個層次的處理方式

Level 1:重新組合現有資料(AI Agent 直接做)

使用者想要的資料已經在現有 Tools 能取得的範圍內,只是組合方式不同。

範例:

Agent 做法:

  1. 使用現有 Tools 查詢資料
  2. 在記憶體中組合、篩選、排序
  3. 用通用模板(或動態 HTML)渲染
  4. 產出 PDF
// Agent 收到:「我要一份只看交換器的 Port 流量報表」
// Agent 自動執行:

// 1. 查所有設備,篩選交換器
const devices = await list_devices({ status: "active" });
const switches = devices.filter(d => d.os.match(/comware|arubaos|jetstream/));

// 2. 查每台交換器的 Port 流量
const portData = await Promise.all(
  switches.map(sw => get_device_ports({ hostname: sw.hostname }))
);

// 3. 組裝報表資料,傳入模板
const report = await generate_report({
  title: "交換器 Port 流量報表",
  template: "custom",  // 通用模板
  sections: [...]
});

Level 2:現有資料的新視角(AI Agent 動態產模板)

使用者要的資料存在,但想要不同的呈現方式或跨資料源的關聯分析。

範例:

Agent 做法:

  1. 使用現有 Tools 取得原始資料
  2. AI 做運算/關聯分析/綜合評分
  3. 動態產生 HTML(不依賴預設模板)
  4. Puppeteer 轉 PDF
// Agent 收到:「我要每台設備的健康分數」
// Agent 自主設計評分邏輯:

function calculateHealthScore(device) {
  let score = 100;
  if (device.cpu > 90) score -= 30;
  else if (device.cpu > 70) score -= 10;
  if (device.memory > 90) score -= 30;
  else if (device.memory > 70) score -= 10;
  if (device.disk > 85) score -= 20;
  else if (device.disk > 70) score -= 5;
  if (device.status === 0) score = 0;
  return score;
}

// Agent 動態產生 HTML(不需要預設模板)
const html = `
<table>
  <tr><th>設備</th><th>健康分數</th><th>CPU</th><th>Mem</th><th>Disk</th></tr>
  ${devices.map(d => `
    <tr>
      <td>${d.hostname}</td>
      <td style="color:${d.score > 80 ? '#10b981' : d.score > 50 ? '#f59e0b' : '#ef4444'}">
        <strong>${d.score}</strong>/100
      </td>
      ...
    </tr>
  `).join('')}
</table>`;

Level 3:需要新資料源(開發新 Tool)

使用者要的資料超出現有 LibreNMS/Graylog 的範圍。

範例:

處理流程:

使用者:「我要報表裡加入 vCenter 的 VM 資訊」
          │
Agent:  分析需求 → 發現沒有 vCenter Tool
          │
          ▼
Agent:  「目前系統尚未整合 VMware vCenter。
          要加入這個資料源,需要:

          1. vCenter REST API 連線資訊(URL + 帳號密碼)
          2. 開發 vCenter Tool(約 2 天工期)

          我先幫你建立一份需求單:
          ┌──────────────────────────────┐
          │ 需求:vCenter 整合           │
          │ 資料:VM 清單、CPU/Mem 使用率 │
          │ API:vSphere REST API        │
          │ 優先級:[請選擇]              │
          └──────────────────────────────┘

          同時,我可以先用 LibreNMS 的 ESXi 資料
          產出一份近似的報表,要嗎?」

新 Tool 開發流程:

1. 定義 Tool Schema(Zod)
   ↓
2. 實作 API Client(連線 + 認證)
   ↓
3. 建立 Skill YAML(加入 Agent)
   ↓
4. 測試 Tool Calling
   ↓
5. 重啟 Agent → Supervisor 自動納入新 Skill
# skills/vcenter/skill.yaml — 新增的 Skill
name: vcenter_expert
description: "VMware vCenter 虛擬化管理、VM 清單、資源使用率、快照"
model:
  default: ollama/qwen3.5:35b-a3b
tools:
  - list_vms
  - get_vm_metrics
  - get_datastore_usage
  - get_host_status
prompt: |
  你是 VMware vCenter 專家...

Level 4:全新功能(新 Tool + 新概念)

使用者要的不只是新資料,而是一個全新的功能領域。

範例:

處理流程: Agent 引導使用者定義需求,產出 PRD → 開發團隊實作。


三、「報表設定」持久化

使用者建立的自訂報表需要可以儲存和重複使用。

3.1 報表定義格式

# saved-reports/windows-disk-weekly.yaml
name: Windows 伺服器磁碟週報
description: 所有 Windows 伺服器的磁碟使用率,超過 70% 標示警告
created_by: kyle
created_at: 2026-03-26

# 資料查詢
queries:
  - tool: list_devices
    params: { status: active }
    filter: "os CONTAINS 'windows'"
  - tool: get_device_health
    params: { hostname: "$each.hostname", metric: storage }

# 報表設定
report:
  template: device-health-check   # 基於現有模板(或 "custom" 用動態 HTML)
  title: "Windows 伺服器磁碟使用率週報"
  columns:
    - { field: hostname, label: 設備名稱 }
    - { field: ip, label: IP }
    - { field: storage.descr, label: 磁碟 }
    - { field: storage.percent, label: 使用率, format: percent, thresholds: { warn: 70, critical: 85 } }
  sort: storage.percent DESC
  highlight:
    - condition: "storage.percent > 85"
      style: red
    - condition: "storage.percent > 70"
      style: yellow

# 排程
schedule:
  cron: "0 9 * * 1"   # 每週一 09:00
  notify:
    - type: email
      to: it-manager@auras.com.tw
      subject: "[Arova] Windows 磁碟週報 — {{date}}"

3.2 報表管理 API

// Chat UI 中的報表管理
使用者:「列出我儲存的報表」
Agent:
  1. Windows 伺服器磁碟週報 — 每週一 09:00
  2. 交換器 Port 流量日報 — 每天 17:00
  3. AD 特權帳戶月報 — 每月 1 日

使用者:「修改第 1 個,閾值改成 60%」
Agent:已更新。磁碟使用率 > 60% 將標示黃色,> 85% 標示紅色。

使用者:「現在跑一次看看」
Agent:[執行查詢 → 產出報表]
  📎 Windows_磁碟週報_2026-03-26.pdf

  摘要:12 台 Windows 伺服器,3 台超過 60% 閾值:
  - th-ex001 D: 78%
  - mes-db-2 E: 72%
  - th-qmdb C: 65%

3.3 報表建立的對話流程

使用者:「我想建一份新報表」
          │
Agent:  「好的,請告訴我:
          1. 這份報表要看什麼資料?
          2. 要定時產出還是手動觸發?
          3. 要寄給誰?」
          │
使用者:「我要看所有設備的 CPU 趨勢,過去 7 天每天的平均值,
         超過 80% 的用紅色。每週五下午寄給我。」
          │
Agent:  分析需求:
         ├── 資料:LibreNMS → list_devices + get_device_health (processor)
         ├── 時間:7 天,按日聚合
         ├── 閾值:80% = 紅色
         ├── 排程:cron "0 17 * * 5"
         └── 通知:Email → 使用者
          │
Agent:  「我設計了這份報表:

          📊 設備 CPU 7 日趨勢報表
          ─────────────────────
          | 設備     | 週一 | 週二 | ... | 週五 | 平均 |
          |----------|------|------|-----|------|------|
          | server-1 | 45%  | 52%  | ... | 61%  | 53%  |
          | server-2 | 82%  | 85%  |     | 79%  | 82%  | ← 紅色

          要我先跑一次預覽嗎?」
          │
使用者:「好,跑一次看看」
          │
Agent:  [執行查詢 → 動態產生 HTML → PDF]
         📎 CPU_7日趨勢_2026-03-26.pdf
          │
使用者:「不錯,存起來,以後每週五自動跑」
          │
Agent:  [儲存 YAML → 加入 BullMQ 排程]
         「已儲存為『設備 CPU 7日趨勢報表』,
          每週五 17:00 自動產出並寄送。」

四、技術實作

4.1 Report Builder Tool

新增一個專門處理自訂報表的 Tool:

// src/tools/report-builder.ts
import { tool } from "@langchain/core/tools";
import { z } from "zod";

export const create_custom_report = tool(
  async ({ title, queries, columns, thresholds, sort }) => {
    // 1. 執行每個 query
    const data = await executeQueries(queries);

    // 2. 套用篩選、排序
    const processed = applyFiltersAndSort(data, columns, sort);

    // 3. 套用閾值標色
    const styled = applyThresholds(processed, thresholds);

    // 4. 動態產生 HTML(不依賴固定模板)
    const html = renderDynamicTable(title, styled, columns);

    // 5. 轉 PDF
    const pdfPath = await htmlToPdf(html);

    return JSON.stringify({ htmlPath, pdfPath, summary: generateSummary(styled) });
  },
  {
    name: "create_custom_report",
    description: "根據使用者描述的條件,動態查詢資料並產出客製報表",
    schema: z.object({
      title: z.string(),
      queries: z.array(z.object({
        tool: z.string().describe("要呼叫的 Tool 名稱"),
        params: z.record(z.any()).describe("Tool 參數"),
        filter: z.string().optional().describe("結果篩選條件"),
      })),
      columns: z.array(z.object({
        field: z.string(),
        label: z.string(),
        format: z.enum(["text", "percent", "bytes", "number", "date"]).default("text"),
      })),
      thresholds: z.array(z.object({
        field: z.string(),
        warn: z.number(),
        critical: z.number(),
      })).optional(),
      sort: z.string().optional().describe("排序欄位和方向,如 'cpu DESC'"),
    }),
  }
);

export const save_report_config = tool(
  async ({ name, config, schedule }) => {
    // 儲存 YAML 到 saved-reports/
    const yamlContent = yaml.stringify({ name, ...config, schedule });
    writeFileSync(`./saved-reports/${slugify(name)}.yaml`, yamlContent);

    // 如果有排程,加入 BullMQ
    if (schedule) {
      await reportQueue.add(slugify(name), config, {
        repeat: { pattern: schedule.cron }
      });
    }

    return JSON.stringify({ saved: true, path: `saved-reports/${slugify(name)}.yaml` });
  },
  {
    name: "save_report_config",
    description: "儲存自訂報表設定,可選加入定時排程",
    schema: z.object({
      name: z.string(),
      config: z.any(),
      schedule: z.object({
        cron: z.string(),
        notify: z.array(z.object({
          type: z.enum(["email", "line"]),
          to: z.string(),
        })),
      }).optional(),
    }),
  }
);

export const list_saved_reports = tool(
  async () => {
    const reports = readdirSync("./saved-reports")
      .filter(f => f.endsWith(".yaml"))
      .map(f => yaml.parse(readFileSync(`./saved-reports/${f}`, "utf-8")));
    return JSON.stringify(reports);
  },
  {
    name: "list_saved_reports",
    description: "列出所有已儲存的自訂報表",
    schema: z.object({}),
  }
);

4.2 Report Agent 升級

# skills/report/skill.yaml — 升級版
name: report_generator
description: "產出報表:預設模板或自訂報表。管理已儲存的報表設定和排程。"
tools:
  - generate_report        # 用預設 Handlebars 模板
  - create_custom_report   # 動態自訂報表(無需模板)
  - save_report_config     # 儲存報表設定
  - list_saved_reports     # 列出已儲存報表
  - render_pdf             # HTML → PDF
  - send_email             # Email 寄送
prompt: |
  你是報表產生專家。你有兩種方式產出報表:

  1. 預設模板(daily-security-brief, device-health-check, weekly-analysis,
     monthly-security-report, device-diagnostic)— 用 generate_report

  2. 自訂報表 — 用 create_custom_report 動態產生
     當使用者描述了一個不符合預設模板的需求時,使用此方式。
     你需要:理解需求 → 設計查詢 → 定義欄位和閾值 → 產出報表。

  使用者說「存起來」或「以後自動跑」時,用 save_report_config 儲存設定。
  使用者問「我有哪些報表」時,用 list_saved_reports 列出。

  重要:產出報表前,先跟使用者確認欄位和條件。
  產出後,提供摘要(幾筆資料、幾筆異常)讓使用者快速判斷。

4.3 處理「現有 Tool 不支援」的情境

// Supervisor 的 system prompt 加入此段

const SUPERVISOR_PROMPT = `
...

## 處理使用者要求新資料源

如果使用者要求的資料超出現有 Tools 的能力,按以下步驟處理:

1. 明確告知使用者「目前系統尚未整合 [X]」
2. 說明需要什麼才能整合(API 連線資訊、開發工期)
3. 提出替代方案(「我可以先用 LibreNMS 的 [Y] 資料產出近似報表」)
4. 如果使用者確認要新增,幫他記錄需求:
   - 資料源名稱
   - 需要的欄位
   - 連線方式(REST API / SSH / SNMP)
   - 優先級
5. 儲存為 feature-requests/<name>.yaml 供開發團隊參考
`;

五、已支援的資料維度

使用者建自訂報表時,可以組合的資料維度:

來自 LibreNMS(設備監控)

維度 Tool 可用欄位
設備清單 list_devices hostname, ip, os, version, hardware, status, uptime, location, serial
CPU get_device_health processor_descr, processor_usage
記憶體 get_device_health mempool_descr, mempool_perc, mempool_used, mempool_total
磁碟 get_device_health storage_descr, storage_perc, storage_used, storage_size, storage_free
Port 流量 get_device_ports ifName, ifAlias, ifInOctets_rate, ifOutOctets_rate, ifOperStatus, ifSpeed
告警 get_active_alerts severity, rule, hostname, timestamp, state
網路拓撲 get_links local_device, local_port, remote_hostname, remote_port, protocol
Sensor (可擴充) temperature, voltage, fanspeed, humidity

來自 Graylog(Log 分析)

維度 Tool 可用欄位
防火牆流量 get_firewall_stats action(allow/deny), src_ip, dst_ip, dst_port, application, bytes
AD 登入 get_ad_login_stats EventID, TargetUserName, IpAddress, success/failure
Log 搜尋 search_logs 任意 Graylog 欄位(Lucene 語法)
告警歷史 (可擴充) event_definition, priority, timestamp

可組合範例

使用者需求 資料組合
「各設備的健康評分排行」 list_devices + get_device_health → 計算綜合分數
「被阻擋最多的內部 IP」 get_firewall_stats (deny, 篩選 src_ip=10.20.*)
「磁碟快滿的設備 + 最近的相關 log」 get_device_health (storage>80%) + search_logs
「交換器 Port 使用率 + LLDP 連接對象」 get_device_ports + get_links
「非工作時間登入成功的帳戶清單」 search_logs (EventID:4624 AND hour:[22 TO 6])
「本週新出現的被阻擋 IP」 get_firewall_stats (本週 Top IP) vs (上週 Top IP) → diff

六、使用者旅程

首次使用
  │
  ├─ 使用者:「我要看所有設備的磁碟」
  │  Agent:[用預設 device-health-check 模板]
  │         📎 設備健康巡檢.pdf
  │
  ├─ 使用者:「只看 Windows 的,超過 70% 標紅」
  │  Agent:[用 create_custom_report 動態產生]
  │         📎 Windows_磁碟報表.pdf
  │
  ├─ 使用者:「不錯,每週一自動跑」
  │  Agent:[save_report_config + BullMQ 排程]
  │         ✅ 已儲存,每週一 09:00 自動寄送
  │
  ├─ 使用者:「加入 vCenter 的 VM 資訊」
  │  Agent:「目前沒有 vCenter 整合。
  │          我先用 ESXi 的資料替代,
  │          同時記錄需求給開發團隊。」
  │
進階使用
  │
  ├─ 使用者:「我的報表有哪些?」
  │  Agent:[list_saved_reports] → 列出清單
  │
  ├─ 使用者:「把第 2 個的閾值改成 60%」
  │  Agent:[更新 YAML] → 「已更新」
  │
  └─ 使用者:「幫我設計一份給廠長看的月報」
     Agent:「廠長通常關心:
             1. 整體健康分數(一個數字)
             2. 本月重大事件(如有)
             3. 與上月比較趨勢
             4. 需要投資的項目

             要用這個架構嗎?」
     使用者:「好」
     Agent:[設計模板 → 產出 → 預覽 → 儲存]