Files
codex-app-server/cloud/console-react/src/api/taskMeta.ts
T
host 994c4ea5d1 console: 任务详情自动显示成本明细
打开详情就自动拉 GET /tasks/{id}/cost(不再只靠
“查成本”按钮出 JSON),详情顶部新一行:美元、总
token(实际/估算、缓存、无价单列)、轮数、fallback
次数;美元小于 1 美分时用 4 位小数,避免小任务
显示成 $0.00 误导成免费;加载中/失败有各自的
弱化文案,成本拉失败不影响详情其它内容;原
“查成本”按钮不动,仍可手动拉原始 JSON。
服务端不用改:成本接口与 _get_task 的 token 聚合
都已在 #150 / 详情 meta 那单里就位。
2026-10-30 00:39:13 +08:00

93 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { Task } from "./types";
// 任务 meta 计算,列表行与详情共用,口径必须一致,改一处两处同变。
//
// 首字:prompt 第一个非空白字符(按码点取,emoji 不截半);
// prompt 为空或全空白时用任务 id 首字符兜底。只作标识,
// 不改动原 prompt 文案。
//
// 总耗时:只用任务对象已回的时间戳算,绝不编造。
// 终态且有 started_at + finished_at:finished_at - started_at
// (与服务端项目统计 avg_duration_seconds 同口径);
// running 且有 started_at:现在 - started_at,标“进行中”,
// 数值随刷新更新,不是实时 tick;
// 无 started_at 的终态:finished_at - created_at 兜底
// (含排队等待,口径见 title);
// 无 started_at 的 queued/running:现在 - created_at 兜底。
// 诚实边界:worker 每次认领都会重写 started_at,所以多轮续跑
// 任务这里显示的是末次认领的时长,不是历次认领之和;要严格
// 累计总时长,服务端需保留首次 started_at 或累加各轮时长,
// 那是服务端字段缺口,不是前端能算出来的。
//
// token:优先 total_tokens(列表接口由服务端一次聚合回,
// 单任务接口同样聚合回);分项只有 actual/estimated 时加总
// 显示;都没有时显示“token —”,不编造、不估算、不逐个调
// /cost(列表一屏 N 个任务逐个调会拖慢刷新、还容易撞限流)。
export function firstChar(t: Task): string {
const p = (t.prompt || "").trim();
if (p) return [...p][0];
return t.id ? [...t.id][0] : "?";
}
export function formatDuration(seconds: number): string {
const s = Math.max(0, Math.floor(seconds));
if (s < 60) return s + "秒";
if (s < 3600) {
const m = Math.floor(s / 60), r = s % 60;
return r ? m + "分" + r + "秒" : m + "分";
}
if (s < 86400) {
const h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60);
return m ? h + "小时" + m + "分" : h + "小时";
}
const d = Math.floor(s / 86400), h = Math.floor((s % 86400) / 3600);
return h ? d + "天" + h + "小时" : d + "天";
}
function num(v: number | null | undefined): number | null {
if (v === null || v === undefined) return null;
const n = Number(v);
return Number.isFinite(n) ? n : null;
}
// 返回 [显示文案, title 口径说明];算不出时文案为“—”。
export function durationOf(t: Task): [string, string] {
const now = Date.now() / 1000;
const started = num(t.started_at), finished = num(t.finished_at);
const created = num(t.created_at);
if (started !== null && finished !== null && finished >= started) {
return [formatDuration(finished - started), "总耗时:开始→结束(末次认领的执行时长)"];
}
if (started !== null && t.status === "running") {
return ["进行中 " + formatDuration(now - started), "总耗时:进行中,已用时长从开始算起,随刷新更新"];
}
if (created !== null && finished !== null && finished >= created) {
return [formatDuration(finished - created), "总耗时:创建→完成(含排队等待;无开始时间时的兜底口径)"];
}
if (created !== null && (t.status === "queued" || t.status === "running")) {
return [(t.status === "queued" ? "排队中 " : "进行中 ") + formatDuration(now - created), "总耗时:从创建算起的已用时长(无开始时间时的兜底口径)"];
}
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) {
return [total.toLocaleString() + " token", "token:服务端 total_tokens"];
}
const actual = num(t.actual_tokens), estimated = num(t.estimated_tokens);
if (actual !== null || estimated !== null) {
return [((actual || 0) + (estimated || 0)).toLocaleString() + " token", "token:actual + estimated 分项加总"];
}
return ["token —", "token:本任务对象未带 token 字段;单任务真数在详情的成本里可查,不逐个调成本接口、不编造"];
}