console: 任务详情自动显示成本明细
打开详情就自动拉 GET /tasks/{id}/cost(不再只靠
“查成本”按钮出 JSON),详情顶部新一行:美元、总
token(实际/估算、缓存、无价单列)、轮数、fallback
次数;美元小于 1 美分时用 4 位小数,避免小任务
显示成 $0.00 误导成免费;加载中/失败有各自的
弱化文案,成本拉失败不影响详情其它内容;原
“查成本”按钮不动,仍可手动拉原始 JSON。
服务端不用改:成本接口与 _get_task 的 token 聚合
都已在 #150 / 详情 meta 那单里就位。
This commit is contained in:
1 parent
0f7162934b
commit
994c4ea5d1
7 files changed
+57
-31
No files matched your search
@@ -70,6 +70,15 @@ export function durationOf(t: Task): [string, string] {
|
||||
return ["—", "总耗时:无可用时间戳"];
|
||||
}
|
||||
|
||||
// 成本美元:usd_micros 是微美元(1 美元 = 1,000,000)。
|
||||
// 小于 1 美分时用 4 位小数,否则 2 位——小任务直接 toFixed(2)
|
||||
// 会把 $0.003 显示成 $0.00,误导成免费。
|
||||
export function fmtCostUsd(micros: number | null | undefined): string {
|
||||
if (micros === null || micros === undefined || !Number.isFinite(Number(micros))) return "—";
|
||||
const v = Number(micros) / 1000000;
|
||||
return "$" + (v > 0 && v < 0.01 ? v.toFixed(4) : v.toFixed(2));
|
||||
}
|
||||
|
||||
export function tokenOf(t: Task): [string, string] {
|
||||
const total = num(t.total_tokens);
|
||||
if (total !== null) {
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface Task {
|
||||
child_model?: string; child_effort?: string; approval?: string;
|
||||
result_text?: string; error_text?: string;
|
||||
}
|
||||
export interface TaskCost { task_id?: string; project_id?: string; total_tokens?: number; usd_micros?: number; turns?: number; fallback_count?: number; actual_tokens?: number; estimated_tokens?: number; cached_input_tokens?: number; unpriced_actual_tokens?: number }
|
||||
export interface Review { id: string; target_type?: string; branch?: string; sha?: string; instructions?: string; status?: string; created_at?: number; findings?: string }
|
||||
export interface Approval { id: string; kind?: string; status?: string; summary?: string; decision?: string; decided_by?: string; decided_at?: number }
|
||||
export interface Followup { mode?: string; status?: string; sender_task_id?: string; body?: string }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createContext, useContext } from "react";
|
||||
import type { Automation, Env, Member, Project, Review, Approval, Followup, PeerMessage, Task, ThreadInfo, Triage, Worker, SkillItem, McpServer, Agent, DispatchBody, ReviewBody, TraceEntry, ChatHistory } from "./api/types";
|
||||
import type { Automation, Env, Member, Project, Review, Approval, Followup, PeerMessage, Task, TaskCost, ThreadInfo, Triage, Worker, SkillItem, McpServer, Agent, DispatchBody, ReviewBody, TraceEntry, ChatHistory } from "./api/types";
|
||||
|
||||
export interface ConsoleCtx {
|
||||
keySaved: boolean; keyState: string;
|
||||
@@ -23,7 +23,7 @@ export interface ConsoleCtx {
|
||||
statusFilter: string; setStatusFilter: (v: string) => void; executorFilter: string; setExecutorFilter: (v: string) => void; searchText: string; setSearchText: (v: string) => void;
|
||||
filteredTasks: Task[];
|
||||
currentTask: Task | null; detailBody: string; actionMsg: string; actionReasons: string;
|
||||
showDetail: (id: string) => void; doAction: (kind: string) => void; taskCost: () => void; costBox: string;
|
||||
showDetail: (id: string) => void; doAction: (kind: string) => void; taskCost: () => void; costBox: string; costSummary: TaskCost | null | undefined;
|
||||
forkVisible: boolean; setForkVisible: (v: boolean) => void; forkTask: (msg: string) => Promise<void>; forkMsg: string;
|
||||
// Continue input is ContinuePanel (ChatInput); its send path is sendChatMessage below.
|
||||
coordChildren: Task[]; coordText: string; agents: Agent[]; agentsNote: string;
|
||||
|
||||
@@ -130,7 +130,7 @@ export function useConsoleStore(): ConsoleCtx {
|
||||
filteredTasks: taskList.filteredTasks,
|
||||
currentTask: detail.currentTask, detailBody: detail.detailBody, actionMsg: detail.actionMsg, actionReasons: detail.actionReasons,
|
||||
showDetail: (id: string) => void detail.showDetail(id),
|
||||
doAction: detail.doAction, taskCost: detail.taskCost, costBox: detail.costBox,
|
||||
doAction: detail.doAction, taskCost: detail.taskCost, costBox: detail.costBox, costSummary: detail.costSummary,
|
||||
forkVisible: detail.forkVisible, setForkVisible: detail.setForkVisible, forkTask: detail.forkTask, forkMsg: detail.forkMsg,
|
||||
coordChildren: detail.coordChildren, coordText: "", agents: detail.agents, agentsNote: detail.agentsNote,
|
||||
reviews: detail.reviews, reviewMsg: detail.reviewMsg, reviewError: detail.reviewError,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { api, getKey } from "../api/client";
|
||||
import { paths } from "../api/paths";
|
||||
import { TRACE_TERMINAL } from "../api/types";
|
||||
import type { Agent, Approval, ChatHistory, Followup, PeerMessage, Review, Task, ThreadInfo, TraceEntry } from "../api/types";
|
||||
import type { Agent, Approval, ChatHistory, Followup, PeerMessage, Review, Task, TaskCost, ThreadInfo, TraceEntry } from "../api/types";
|
||||
|
||||
// Task detail state: the selected task, its trace SSE/one-shot stream,
|
||||
// and every structured sub-list. Chat sending lives here too:
|
||||
@@ -13,6 +13,9 @@ export function useTaskDetail(getRole: (pid?: string) => string | null, onTasksR
|
||||
const [actionMsg, setActionMsg] = useState("");
|
||||
const [actionReasons, setActionReasons] = useState("");
|
||||
const [costBox, setCostBox] = useState("");
|
||||
// 成本明细:打开详情就自动加载(不再只靠“查成本”按钮出 JSON)。
|
||||
// undefined = 加载中,null = 加载失败,对象 = 已加载。
|
||||
const [costSummary, setCostSummary] = useState<TaskCost | null | undefined>(undefined);
|
||||
const [forkVisible, setForkVisible] = useState(false);
|
||||
const [forkMsg, setForkMsg] = useState("");
|
||||
const [coordChildren, setCoordChildren] = useState<Task[]>([]);
|
||||
@@ -68,6 +71,13 @@ export function useTaskDetail(getRole: (pid?: string) => string | null, onTasksR
|
||||
} catch { setCoordChildren([]); }
|
||||
}, []);
|
||||
|
||||
const loadCost = useCallback(async (taskId: string) => {
|
||||
try {
|
||||
const c = await api<TaskCost>(paths.taskCost(taskId));
|
||||
setCostSummary(c);
|
||||
} catch { setCostSummary(null); }
|
||||
}, []);
|
||||
|
||||
const loadFollowups = useCallback(async (taskId: string) => {
|
||||
try { const d = await api<{ followups: Followup[] }>(paths.taskFollowups(taskId)); setFollowups(d.followups || []); }
|
||||
catch { setFollowups([]); }
|
||||
@@ -215,6 +225,8 @@ export function useTaskDetail(getRole: (pid?: string) => string | null, onTasksR
|
||||
if (!body) body = "(暂无 result/error 输出)";
|
||||
setDetailBody(body);
|
||||
setCostBox("");
|
||||
setCostSummary(undefined);
|
||||
void loadCost(taskId);
|
||||
setForkVisible(false);
|
||||
setForkMsg("");
|
||||
setPeerMsg("");
|
||||
@@ -247,7 +259,7 @@ export function useTaskDetail(getRole: (pid?: string) => string | null, onTasksR
|
||||
setCurrentTask(null);
|
||||
setDetailBody("加载失败:" + e.message);
|
||||
}
|
||||
}, [stopTrace, startTrace, loadAgents, loadCoordChildren, loadChatHistory, loadFollowups, loadPeerMessages, loadThread, loadThreads, loadReviews, loadApprovals, getRole]);
|
||||
}, [stopTrace, startTrace, loadAgents, loadCoordChildren, loadChatHistory, loadFollowups, loadPeerMessages, loadThread, loadThreads, loadReviews, loadApprovals, loadCost, getRole]);
|
||||
|
||||
const refreshDetailLists = useCallback(async () => {
|
||||
const t = currentTaskRef.current;
|
||||
@@ -269,7 +281,8 @@ export function useTaskDetail(getRole: (pid?: string) => string | null, onTasksR
|
||||
void loadPeerMessages(t.id);
|
||||
void loadThread(t.id);
|
||||
void loadThreads(t.id);
|
||||
}, [loadFollowups, loadChatHistory, loadReviews, loadApprovals, loadCoordChildren, loadAgents, loadPeerMessages, loadThread, loadThreads]);
|
||||
void loadCost(t.id);
|
||||
}, [loadFollowups, loadChatHistory, loadReviews, loadApprovals, loadCoordChildren, loadAgents, loadPeerMessages, loadThread, loadThreads, loadCost]);
|
||||
|
||||
const doAction = useCallback(async (kind: string) => {
|
||||
const t = currentTaskRef.current;
|
||||
@@ -409,7 +422,7 @@ export function useTaskDetail(getRole: (pid?: string) => string | null, onTasksR
|
||||
useEffect(() => () => stopTrace(), [stopTrace]);
|
||||
|
||||
return {
|
||||
currentTask, detailBody, actionMsg, actionReasons, costBox, forkVisible, setForkVisible, forkMsg,
|
||||
currentTask, detailBody, actionMsg, actionReasons, costBox, costSummary, forkVisible, setForkVisible, forkMsg,
|
||||
coordChildren, agents, agentsNote, reviews, reviewMsg, reviewError, traceEntries, traceNote, traceDone,
|
||||
traceKind, setTraceKind, traceMsg, followups, approvals, approvalMsg, peerMessages, peerMsg,
|
||||
threadText, threadGateNote, threadMsg, threadList, chatMsg, chatHistory,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useConsole } from "../context";
|
||||
import { Card } from "../components/ui";
|
||||
import { shortId, fmt } from "../api/client";
|
||||
import { STATUS_EXPLAIN } from "../api/types";
|
||||
import { firstChar, durationOf, tokenOf } from "../api/taskMeta";
|
||||
import { firstChar, durationOf, tokenOf, fmtCostUsd } from "../api/taskMeta";
|
||||
import DetailActions from "./detail/DetailActions";
|
||||
import ReviewPanel from "./detail/ReviewPanel";
|
||||
import { CoordPanel, AgentsPanel, ApprovalPanel, PeerPanel, ThreadPanel } from "./detail/DetailLists";
|
||||
@@ -22,6 +22,9 @@ export default function TaskDetailPanel() {
|
||||
<div>
|
||||
状态 <b className={"st-" + t.status}>{t.status}</b>({STATUS_EXPLAIN[t.status] || "未知状态"})
|
||||
<br /><span style={{ display: "inline-flex", alignItems: "center", gap: 6 }}><span className="issue-first" title={"任务首字:" + fc} aria-hidden="true">{fc}</span><span title={durTitle}>耗时 {dur}</span><span> · </span><span title={tokTitle}>{tok}</span></span>
|
||||
<br />{ctx.costSummary === undefined ? <span className="muted">成本加载中…</span>
|
||||
: ctx.costSummary === null ? <span className="muted">成本明细加载失败(“查成本”按钮仍可手动拉取原始 JSON)</span>
|
||||
: <span title="成本明细:GET /tasks/{id}/cost 聚合;美元只计有价行,无价 token 单列;缓存是输入 token 的子集,不另计">成本 {fmtCostUsd(ctx.costSummary.usd_micros)} | token {(ctx.costSummary.total_tokens ?? 0).toLocaleString()}(实际 {(ctx.costSummary.actual_tokens ?? 0).toLocaleString()} / 估算 {(ctx.costSummary.estimated_tokens ?? 0).toLocaleString()},缓存 {(ctx.costSummary.cached_input_tokens ?? 0).toLocaleString()},无价 {(ctx.costSummary.unpriced_actual_tokens ?? 0).toLocaleString()})| 轮数 {ctx.costSummary.turns ?? 0} | fallback {ctx.costSummary.fallback_count ?? 0}</span>}
|
||||
<br />任务 {t.id} | turn {shortId(t.turn_id)} | root turn {shortId(t.root_turn_id)}
|
||||
<br />executor {t.executor || ""} | 主机 {t.worktree_host || ""}
|
||||
<br />模型 {t.model || "默认"} | 强度 {t.effort || "默认"}
|
||||
|
||||
Reference in new issue
Block a user