feat(enterprise): add CloudCost cloud cost management tools (5 tools)
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:
co-authored by
Claude Sonnet 4.6
parent
5c3e6f18e2
commit
d93bb5966c
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user