diff --git a/CLAUDE.md b/CLAUDE.md index 43060dc..b2ae22c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -52,7 +52,8 @@ sshpass -p xiaohei ssh xiaohei@192.168.30.30 "cd ~/SOC && docker compose --profi ├── Gongdan 工单 API ├── Jina Search/Reader/Rerank ├── Serper Google Search - └── Daytona Sandbox (process/execute endpoint) + ├── Daytona Sandbox (process/execute endpoint) + └── CloudCost 云管系统 (orange-wave-09002e800.7.azurestaticapps.net) ``` ### Agent Graph @@ -93,6 +94,11 @@ src/agent/{name}/ | writer | `doc_create/edit/translate` | `canvas-doc` | CanvasPanel右侧抽屉 | | writer | `report_generate` | `canvas-doc` | 结构化报告模板 | | writer | `reply_draft` | `reply-draft` | customer/internal双模式 | +| enterprise | `cloudcost_dashboard` | `cost-result` (dashboard) | 月度总览:总费用/环比/趋势/按厂商服务拆分 | +| enterprise | `cloudcost_metering` | `cost-result` (metering) | 用量费用汇总,按服务 Top N | +| enterprise | `cloudcost_detail` | `cost-result` (detail) | 费用明细分页,最细粒度 | +| enterprise | `cloudcost_alerts` | `cost-result` (alerts) | 告警规则状态,triggered 红色高亮 | +| enterprise | `cloudcost_accounts` | `cost-result` (accounts) | 云服务账号列表,状态徽章 | | enterprise (自动) | — | `next-actions` | 工具成功后自动附带2-3条推荐动作 | 所有卡片 props 包含 `sourceType`(internal_kb/ticket_system/external_web/code_execution/generated_doc)和 `confidence`(high/medium/low)。 @@ -229,6 +235,9 @@ DOC_CREATOR_API_KEY # PostgreSQL (checkpoint + 持久化) DATABASE_URL + +# CloudCost 云管系统(可选,有默认值) +CLOUDCOST_API_BASE=https://orange-wave-09002e800.7.azurestaticapps.net ``` ## Deployment diff --git a/langgraph/src/agent/enterprise/tools/cloudcost-client.ts b/langgraph/src/agent/enterprise/tools/cloudcost-client.ts new file mode 100644 index 0000000..745df5b --- /dev/null +++ b/langgraph/src/agent/enterprise/tools/cloudcost-client.ts @@ -0,0 +1,159 @@ +/** + * CloudCost 云管系统 API 客户端 + * Base URL: https://orange-wave-09002e800.7.azurestaticapps.net/ + * 只读 GET 接口,无需鉴权(依赖网关/内网策略) + */ + +import { config } from "@/agent/utils/config"; +import { getToolConfig } from "@/agent/utils/toolConfig"; + +function buildUrl(path: string, params: Record): string { + const base = config.cloudcost.apiBase.replace(/\/$/, ""); + const url = new URL(`${base}${path}`); + for (const [k, v] of Object.entries(params)) { + if (v !== undefined && v !== "") url.searchParams.set(k, String(v)); + } + return url.toString(); +} + +async function ccGet(url: string, toolName: string): Promise { + const resp = await fetch(url, { + signal: AbortSignal.timeout(getToolConfig(toolName).timeoutMs), + }); + if (!resp.ok) throw new Error(`CloudCost ${toolName} failed: HTTP ${resp.status}`); + return resp.json() as Promise; +} + +// ─── Types ──────────────────────────────────────────────────────────────────── + +export interface DashboardBundle { + overview: { + total_cost: number; + prev_month_cost: number; + mom_change_pct: number; + active_projects: number; + }; + trend: Array<{ date: string; cost: number; cost_by_provider: Record }>; + by_provider: Array<{ provider: string; cost: number; percentage: number }>; + by_service: Array<{ product: string; cost: number; percentage: number }>; +} + +export interface MeteringSummary { + total_cost: number; + total_usage: number; + record_count: number; + service_count: number; +} + +export interface ServiceUsage { + product: string; + usage_quantity: number; + usage_unit: string; + cost: number; + record_count: number; +} + +export interface MeteringDetail { + id: number; + date: string; + provider: string; + project_id: string; + product: string; + usage_type: string; + region: string; + cost: number; + usage_quantity: number; + usage_unit: string; + currency: string; +} + +export interface AlertRuleStatus { + rule_id: number; + rule_name: string; + threshold_type: string; + threshold_value: number; + actual: number; + pct: number; + triggered: boolean; + account_name: string; + provider: string; + external_project_id: string; +} + +export interface ServiceAccount { + id: number; + name: string; + supply_source_id: number; + supplier_name: string; + provider: string; + external_project_id: string; + status: string; + created_at: string; +} + +// ─── API Functions ───────────────────────────────────────────────────────────── + +/** GET /api/dashboard/bundle — 月度费用总览(overview + trend + by_provider + by_service) */ +export async function cloudcostDashboard( + month: string, + granularity: "daily" | "weekly" | "monthly" = "daily", + serviceLimit = 10, +): Promise { + const url = buildUrl("/api/dashboard/bundle", { month, granularity, service_limit: serviceLimit }); + return ccGet(url, "cloudcost_dashboard"); +} + +/** GET /api/metering/summary + /api/metering/by-service — 用量费用汇总 */ +export async function cloudcostMetering(params: { + date_start?: string; + date_end?: string; + provider?: string; + product?: string; + account_id?: number; +}): Promise<{ summary: MeteringSummary; by_service: ServiceUsage[] }> { + const p = params as Record; + const [summary, by_service] = await Promise.all([ + ccGet(buildUrl("/api/metering/summary", p), "cloudcost_metering"), + ccGet(buildUrl("/api/metering/by-service", p), "cloudcost_metering"), + ]); + return { summary, by_service }; +} + +/** GET /api/metering/detail + /detail/count — 费用明细分页 */ +export async function cloudcostDetail(params: { + date_start?: string; + date_end?: string; + provider?: string; + product?: string; + page?: number; + page_size?: number; +}): Promise<{ items: MeteringDetail[]; total: number }> { + const p = { page: 1, page_size: 20, ...params } as Record; + const countParams = { ...p } as Record; + delete countParams.page; + delete countParams.page_size; + + const [items, countResp] = await Promise.all([ + ccGet(buildUrl("/api/metering/detail", p), "cloudcost_detail"), + ccGet<{ total: number }>(buildUrl("/api/metering/detail/count", countParams), "cloudcost_detail"), + ]); + return { items, total: countResp.total }; +} + +/** GET /api/alerts/rule-status — 告警规则执行态 */ +export async function cloudcostAlerts(month?: string): Promise { + const url = buildUrl("/api/alerts/rule-status", { month }); + return ccGet(url, "cloudcost_alerts"); +} + +/** GET /api/service-accounts/ — 服务账号列表 */ +export async function cloudcostAccounts(params: { + provider?: string; + status?: string; + page?: number; + page_size?: number; +}): Promise { + const p = { page: 1, page_size: 100, ...params } as Record; + const url = buildUrl("/api/service-accounts/", p); + return ccGet(url, "cloudcost_accounts"); +} diff --git a/langgraph/src/agent/utils/config.ts b/langgraph/src/agent/utils/config.ts index c65299a..b7da86f 100644 --- a/langgraph/src/agent/utils/config.ts +++ b/langgraph/src/agent/utils/config.ts @@ -126,4 +126,7 @@ export const config = { apiKey: process.env.DOC_CREATOR_API_KEY ?? "", fallbackEndpoint: process.env.DOC_CREATOR_FALLBACK_ENDPOINT ?? "", }, + cloudcost: { + apiBase: process.env.CLOUDCOST_API_BASE ?? "https://orange-wave-09002e800.7.azurestaticapps.net", + }, } as const; diff --git a/langgraph/src/agent/utils/toolConfig.ts b/langgraph/src/agent/utils/toolConfig.ts index cd514f9..ac13efd 100644 --- a/langgraph/src/agent/utils/toolConfig.ts +++ b/langgraph/src/agent/utils/toolConfig.ts @@ -15,7 +15,12 @@ export const TOOL_CONFIGS: Record = { web_search_deep: { timeoutMs: 45_000, maxRetries: 1 }, sandbox_run: { timeoutMs: 15_000, maxRetries: 1 }, code_execute: { timeoutMs: 15_000, maxRetries: 1 }, - chart_generate: { timeoutMs: 5_000, maxRetries: 1 }, + chart_generate: { timeoutMs: 5_000, maxRetries: 1 }, + cloudcost_dashboard: { timeoutMs: 15_000, maxRetries: 2, backoffMs: 500 }, + cloudcost_metering: { timeoutMs: 15_000, maxRetries: 2, backoffMs: 500 }, + cloudcost_detail: { timeoutMs: 15_000, maxRetries: 2, backoffMs: 500 }, + cloudcost_alerts: { timeoutMs: 10_000, maxRetries: 2, backoffMs: 500 }, + cloudcost_accounts: { timeoutMs: 10_000, maxRetries: 2, backoffMs: 500 }, }; export function getToolConfig(toolName: string): ToolConfig {