feat(enterprise): add CloudCost cloud cost management tools (5 tools)
Trigger auto deployment for soc-langgraph / build-and-deploy (push) Failing after 17s
Deploy LangGraph UI to Azure Static Web Apps / build-and-deploy (push) Failing after 43s

Add cloudcost_dashboard, cloudcost_metering, cloudcost_detail, cloudcost_alerts,
and cloudcost_accounts tools to the enterprise agent with full Gen-UI card support.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
gongzhiyong
2026-04-14 03:29:47 +08:00
co-authored by Claude Sonnet 4.6
parent 5c3e6f18e2
commit d93bb5966c
3 changed files with 348 additions and 1 deletions
@@ -28,6 +28,14 @@ const SYSTEM_PROMPT = `你是企业内部助手,帮助用户查询知识库和
- 当你需要对已有数据做额外维度的可视化(如时间趋势、自定义对比),主动调用 chart_generate 工具
- 知识库返回含数值型数据时,考虑用 chart_generate 生成数值摘要图
## 云费用查询规则
- 用户问「月度费用」→ 必须提供 month 参数(YYYY-MM 格式),默认取当月
- 用户问「超支/告警」→ 优先调用 cloudcost_alerts
- 用户问「费用明细/详情」→ 调用 cloudcost_detail,合理设置日期范围
- 用户问「账号/云账号」→ 调用 cloudcost_accounts
- 费用数据已通过 cost-result 卡片展示,文字回复只提供分析和洞察
- 费用单位为原始货币(USD/CNY),不做汇率换算除非用户要求
## 规则
### 停止查询条件
@@ -42,6 +50,7 @@ const SYSTEM_PROMPT = `你是企业内部助手,帮助用户查询知识库和
3. 网络搜索 — 仅当内部数据不足时使用
4. 代码沙盒 — 仅需要计算或代码执行时
5. 图表生成(chart_generate)— 对已有数据做额外维度的可视化
6. 云费用查询(cloudcost_*)— 用户问云费用/账单/用量/超支/账号时专用
**重要**:当用户问题含"分析/趋势/统计/占比/分布/汇总/对比/多少"时,优先调用 ticket_list(而非 ticket_detail),以触发图表自动生成。获得数据后如需更多维度的可视化,继续调用 chart_generate。
@@ -51,6 +60,7 @@ const SYSTEM_PROMPT = `你是企业内部助手,帮助用户查询知识库和
- 有工单号(如 TK-2026-xxx)→ ticket_detail;无工单号 → ticket_list
- 已有真实数据需可视化 → chart_generate;无数据不得调用
- 需执行代码/计算 → sandbox;其他情况不调用
- 云费用/账单/云用量/超支预算/云账号 → cloudcost_dashboard / cloudcost_metering / cloudcost_detail / cloudcost_alerts / cloudcost_accounts
### 主动输出建议
- 查完工单后,主动给出"建议处理方案"和下一步行动
@@ -23,6 +23,36 @@ export const chartGenerateSchema = z.object({
data: z.array(z.object({ name: z.string(), value: z.number() })).describe("图表数据"),
});
export const cloudcostDashboardSchema = z.object({
month: z.string().describe("统计月份,格式 YYYY-MM,如 2026-04"),
granularity: z.enum(["daily", "weekly", "monthly"]).optional().describe("趋势粒度,默认 daily"),
});
export const cloudcostMeteringSchema = z.object({
date_start: z.string().optional().describe("开始日期 YYYY-MM-DD"),
date_end: z.string().optional().describe("结束日期 YYYY-MM-DD"),
provider: z.string().optional().describe("云厂商,如 aws / gcp / azure"),
product: z.string().optional().describe("产品/服务名"),
});
export const cloudcostDetailSchema = z.object({
date_start: z.string().optional().describe("开始日期 YYYY-MM-DD"),
date_end: z.string().optional().describe("结束日期 YYYY-MM-DD"),
provider: z.string().optional().describe("云厂商,如 aws / gcp / azure"),
product: z.string().optional().describe("产品/服务名过滤"),
page: z.number().optional().describe("页码,从 1 起,默认 1"),
page_size: z.number().optional().describe("每页条数,默认 20,最大 50"),
});
export const cloudcostAlertsSchema = z.object({
month: z.string().optional().describe("查询月份 YYYY-MM,默认当月"),
});
export const cloudcostAccountsSchema = z.object({
provider: z.string().optional().describe("云厂商过滤,如 aws / gcp / azure"),
status: z.string().optional().describe("账号状态:active / inactive / standby"),
});
export const ALL_ENTERPRISE_TOOLS = [
{
name: "kb_search",
@@ -48,6 +78,31 @@ export const ALL_ENTERPRISE_TOOLS = [
"根据已查到的真实数据生成图表(柱状图、饼图、折线图)。适用:已通过其他工具获取数据后需要可视化分析。不适用:无数据时不得调用;ticket_list 的状态/优先级分布图由系统自动生成,无需手动调用。",
schema: chartGenerateSchema,
},
{
name: "cloudcost_dashboard",
description: "查询云费用总览仪表板。适用:用户问「本月/上月云费用多少」「AWS/GCP/Azure 花了多少」「费用环比变化」「哪个云厂商费用最高」「费用趋势」等。返回总费用、环比、趋势图、按厂商/服务拆分。必须提供 month 参数(YYYY-MM 格式)。",
schema: cloudcostDashboardSchema,
},
{
name: "cloudcost_metering",
description: "查询云资源用量与费用汇总。适用:用户问「EC2/存储/数据库用量」「某时间段费用」「按服务拆分费用」「Top N 服务费用」。可按厂商、产品、日期区间过滤。",
schema: cloudcostMeteringSchema,
},
{
name: "cloudcost_detail",
description: "查询云费用原始明细(最细粒度)。适用:用户问「费用明细」「具体资源费用」「区域费用」「某产品的详细账单」。支持分页,适合钻取分析。",
schema: cloudcostDetailSchema,
},
{
name: "cloudcost_alerts",
description: "查询云费用告警规则状态。适用:用户问「有没有超支」「费用告警」「预算执行情况」「哪些账号快超限了」。返回每条规则的实际值 vs 阈值、是否已触发。",
schema: cloudcostAlertsSchema,
},
{
name: "cloudcost_accounts",
description: "查询云服务账号列表。适用:用户问「有哪些云账号」「AWS/GCP 账号」「账号状态」「供应商账号」。可按云厂商、状态过滤。",
schema: cloudcostAccountsSchema,
},
] as const;
export type EnterpriseToolDef = (typeof ALL_ENTERPRISE_TOOLS)[number];
@@ -24,11 +24,23 @@ import {
ticketList,
ticketDetail,
} from "../tools/soc-client.js";
import {
cloudcostDashboard,
cloudcostMetering,
cloudcostDetail,
cloudcostAlerts,
cloudcostAccounts,
} from "../tools/cloudcost-client.js";
import {
kbSearchSchema,
ticketListSchema,
ticketDetailSchema,
chartGenerateSchema,
cloudcostDashboardSchema,
cloudcostMeteringSchema,
cloudcostDetailSchema,
cloudcostAlertsSchema,
cloudcostAccountsSchema,
} from "./tool-defs.js";
import { executeWithRetry, formatToolError } from "@/agent/utils/retry";
import { getToolConfig } from "@/agent/utils/toolConfig";
@@ -41,6 +53,7 @@ import crypto from "crypto";
const CARD_SORT_WEIGHT: Record<string, number> = {
"error-result": 100,
"chart-result": 200,
"cost-result": 250,
"knowledge-result": 300,
"ticket-summary": 400,
"ticket-detail": 450,
@@ -146,6 +159,18 @@ function generateNextActions(
icon: "🔍",
});
}
if (tools.some(t => t.startsWith("cloudcost_"))) {
actions.push({
label: "生成费用报告",
prompt: "基于以上云费用数据生成一份费用分析报告",
icon: "💰",
});
actions.push({
label: "导出明细",
prompt: "查看详细的费用明细数据",
icon: "📊",
});
}
return actions.slice(0, 3);
}
@@ -311,7 +336,7 @@ const VALIDATE_RULES: ValidationRule[] = [
const hasData = state.execution_log?.some(
(e) =>
e.status !== "error" &&
["kb_search", "ticket_list", "ticket_detail"].includes(e.tool),
["kb_search", "ticket_list", "ticket_detail", "cloudcost_dashboard", "cloudcost_metering", "cloudcost_detail", "cloudcost_alerts", "cloudcost_accounts"].includes(e.tool),
);
if (!hasData) return "chart_generate 调用被拦截:当前对话尚无工具数据,无法生成图表";
return null;
@@ -896,6 +921,263 @@ export async function toolExecutorNode(
};
}
case "cloudcost_dashboard": {
const parsed = cloudcostDashboardSchema.parse(args);
const ccDashCfg = getToolConfig("cloudcost_dashboard");
const result = await withTimeout(
() => executeWithRetry(
() => cloudcostDashboard(parsed.month, parsed.granularity),
ccDashCfg.maxRetries,
{ backoffMs: ccDashCfg.backoffMs },
),
ccDashCfg.timeoutMs,
runSignal,
);
statusList.push({ tool: name, status: "ok" });
const dashLogEntry: ExecutionLogEntry = {
tool: name,
toolCallId: id,
status: "success",
summary: `月度云费用总览 ${parsed.month}`,
timestamp: Date.now(),
inputSummary: truncateInput(`month: ${parsed.month}`),
durationMs: Date.now() - startTime,
resultCount: 1,
};
executionLog.push(dashLogEntry);
logToolCall(dashLogEntry);
if (!hasArtifactForToolCall(state.ui, id)) {
ui.push(
{
name: "cost-result",
props: {
mode: "dashboard",
month: parsed.month,
overview: result.overview,
trend: result.trend,
by_provider: result.by_provider,
by_service: result.by_service,
sourceType: "internal_kb",
confidence: "high",
artifact_id: `${id}_cloudcost_dashboard`,
sort_key: (CARD_SORT_WEIGHT["cost-result"] ?? 250) * 1_000_000 + sortKey,
execution_summary: `月度云费用总览 ${parsed.month},共 ${result.by_provider.length} 个云厂商`,
},
} as never,
{ message: lastAiMessage },
);
}
return {
role: "tool" as const,
tool_call_id: id,
content: JSON.stringify({ ok: true, tool: name, summary: `月度云费用总览 ${parsed.month}`, resultCount: 1, data: result }),
};
}
case "cloudcost_metering": {
const parsed = cloudcostMeteringSchema.parse(args);
const ccMeterCfg = getToolConfig("cloudcost_metering");
const result = await withTimeout(
() => executeWithRetry(
() => cloudcostMetering(parsed),
ccMeterCfg.maxRetries,
{ backoffMs: ccMeterCfg.backoffMs },
),
ccMeterCfg.timeoutMs,
runSignal,
);
statusList.push({ tool: name, status: "ok" });
const meterLogEntry: ExecutionLogEntry = {
tool: name,
toolCallId: id,
status: "success",
summary: `用量汇总:总费用 ${result.summary.total_cost?.toFixed(2)}`,
timestamp: Date.now(),
inputSummary: truncateInput(JSON.stringify(parsed)),
durationMs: Date.now() - startTime,
resultCount: result.summary.service_count ?? 0,
};
executionLog.push(meterLogEntry);
logToolCall(meterLogEntry);
if (!hasArtifactForToolCall(state.ui, id)) {
ui.push(
{
name: "cost-result",
props: {
mode: "metering",
summary: result.summary,
by_service: result.by_service,
filters: parsed,
sourceType: "internal_kb",
confidence: "high",
artifact_id: `${id}_cloudcost_metering`,
sort_key: (CARD_SORT_WEIGHT["cost-result"] ?? 250) * 1_000_000 + sortKey,
execution_summary: `用量汇总:总费用 ${result.summary.total_cost?.toFixed(2)},共 ${result.summary.service_count} 个服务`,
},
} as never,
{ message: lastAiMessage },
);
}
return {
role: "tool" as const,
tool_call_id: id,
content: JSON.stringify({ ok: true, tool: name, summary: `用量汇总完成`, resultCount: result.summary.service_count ?? 0, data: result }),
};
}
case "cloudcost_detail": {
const parsed = cloudcostDetailSchema.parse(args);
const ccDetailCfg = getToolConfig("cloudcost_detail");
const result = await withTimeout(
() => executeWithRetry(
() => cloudcostDetail(parsed),
ccDetailCfg.maxRetries,
{ backoffMs: ccDetailCfg.backoffMs },
),
ccDetailCfg.timeoutMs,
runSignal,
);
statusList.push({ tool: name, status: "ok" });
const detailLogEntry: ExecutionLogEntry = {
tool: name,
toolCallId: id,
status: "success",
summary: `费用明细 ${result.items.length} 条(共 ${result.total} 条)`,
timestamp: Date.now(),
inputSummary: truncateInput(JSON.stringify(parsed)),
durationMs: Date.now() - startTime,
resultCount: result.items.length,
};
executionLog.push(detailLogEntry);
logToolCall(detailLogEntry);
if (!hasArtifactForToolCall(state.ui, id)) {
ui.push(
{
name: "cost-result",
props: {
mode: "detail",
items: result.items,
total: result.total,
page: parsed.page ?? 1,
page_size: parsed.page_size ?? 20,
filters: parsed,
sourceType: "internal_kb",
confidence: "high",
artifact_id: `${id}_cloudcost_detail`,
sort_key: (CARD_SORT_WEIGHT["cost-result"] ?? 250) * 1_000_000 + sortKey,
execution_summary: `费用明细 ${result.items.length} 条(共 ${result.total} 条)`,
},
} as never,
{ message: lastAiMessage },
);
}
return {
role: "tool" as const,
tool_call_id: id,
content: JSON.stringify({ ok: true, tool: name, summary: `费用明细查询完成`, resultCount: result.items.length, data: result }),
};
}
case "cloudcost_alerts": {
const parsed = cloudcostAlertsSchema.parse(args);
const ccAlertCfg = getToolConfig("cloudcost_alerts");
const result = await withTimeout(
() => executeWithRetry(
() => cloudcostAlerts(parsed.month),
ccAlertCfg.maxRetries,
{ backoffMs: ccAlertCfg.backoffMs },
),
ccAlertCfg.timeoutMs,
runSignal,
);
statusList.push({ tool: name, status: "ok" });
const triggeredCount = result.filter((r: { triggered: boolean }) => r.triggered).length;
const alertLogEntry: ExecutionLogEntry = {
tool: name,
toolCallId: id,
status: "success",
summary: `共 ${result.length} 条告警规则,${triggeredCount} 条已触发`,
timestamp: Date.now(),
inputSummary: truncateInput(JSON.stringify(parsed)),
durationMs: Date.now() - startTime,
resultCount: result.length,
};
executionLog.push(alertLogEntry);
logToolCall(alertLogEntry);
if (!hasArtifactForToolCall(state.ui, id)) {
ui.push(
{
name: "cost-result",
props: {
mode: "alerts",
alerts: result,
triggered_count: triggeredCount,
sourceType: "internal_kb",
confidence: "high",
artifact_id: `${id}_cloudcost_alerts`,
sort_key: (CARD_SORT_WEIGHT["cost-result"] ?? 250) * 1_000_000 + sortKey,
execution_summary: `共 ${result.length} 条告警规则,${triggeredCount} 条已触发`,
},
} as never,
{ message: lastAiMessage },
);
}
return {
role: "tool" as const,
tool_call_id: id,
content: JSON.stringify({ ok: true, tool: name, summary: `共 ${result.length} 条告警规则,${triggeredCount} 条已触发`, resultCount: result.length, data: result }),
};
}
case "cloudcost_accounts": {
const parsed = cloudcostAccountsSchema.parse(args);
const ccAccCfg = getToolConfig("cloudcost_accounts");
const result = await withTimeout(
() => executeWithRetry(
() => cloudcostAccounts(parsed),
ccAccCfg.maxRetries,
{ backoffMs: ccAccCfg.backoffMs },
),
ccAccCfg.timeoutMs,
runSignal,
);
statusList.push({ tool: name, status: "ok" });
const accountLogEntry: ExecutionLogEntry = {
tool: name,
toolCallId: id,
status: "success",
summary: `共 ${result.length} 个云服务账号`,
timestamp: Date.now(),
inputSummary: truncateInput(JSON.stringify(parsed)),
durationMs: Date.now() - startTime,
resultCount: result.length,
};
executionLog.push(accountLogEntry);
logToolCall(accountLogEntry);
if (!hasArtifactForToolCall(state.ui, id)) {
ui.push(
{
name: "cost-result",
props: {
mode: "accounts",
accounts: result,
sourceType: "internal_kb",
confidence: "high",
artifact_id: `${id}_cloudcost_accounts`,
sort_key: (CARD_SORT_WEIGHT["cost-result"] ?? 250) * 1_000_000 + sortKey,
execution_summary: `共 ${result.length} 个云服务账号`,
},
} as never,
{ message: lastAiMessage },
);
}
return {
role: "tool" as const,
tool_call_id: id,
content: JSON.stringify({ ok: true, tool: name, summary: `共 ${result.length} 个云服务账号`, resultCount: result.length, data: result }),
};
}
default:
return {
role: "tool" as const,