



















该文章是 openclaw/openclaw 仓库中一个 commit 的代码变更摘要,commit 标题为 "fix(sessions): estimate local transcript usage",对应哈希 2c59ea8。变更的核心目的是在会话模块中增加对本地转录使用量的估算功能。
新增了两个函数:extractTranscriptContentEstimatedChars 和 extractTranscriptTokenEstimateFromLine。前者用于估算字符串或数组内容的字符数:若为字符串则移除内联指令标签后调用 estimateStringChars;若为数组则遍历每个对象元素,检查 text 属性和 type(需为 "text"、"output_text" 或 "input_text"),对符合条件的文本累加估算字符数。后者从单行转录记录中提取 token 估算:若行过大则返回 null;解析 JSON 后提取 message 字段,要求 role 为 "user" 或 "assistant";获取 provider 信息;增加过滤条件,当角色为 "assistant"、模型提供商为 "openclaw" 且模型名为 "delivery-mirror" 时返回 null;通过 extractTranscriptContentEstimatedChars 估算字符数,若字符数不大于 0 也返回 null;最终返回包含 estimatedChars 和 hasModelIdentity 的对象。
修改了 extractAggregateUsageFromTranscriptLines 函数:循环中调用新函数,引入变量 estimatedTranscriptChars、sawEstimatedTranscriptContent、sawEstimateModelIdentity,累加估算字符并标记是否遇到估算内容及模型身份。
条件性地使用估算结果:生成最终 snapshot 对象时,若 snapshot.totalTokens 不是数字且 saveEstimatedTranscriptContent 和 sawEstimateModelIdentity 都为真,则调用 estimateTokensFromChars 将累积字符数转换为估算 token 数,填入 snapshot.totalTokens 并标记为新鲜估算。
整体来看,本次提交为会话模块引入了基于本地文本内容的字符和 token 估算能力,通过分析 transcript 行中的消息类型和文本部分,实现更准确的使用量统计,并包含对特定模型 (delivery-mirror) 的过滤逻辑。
@@ -5,6 +5,7 @@ import { jsonUtf8Bytes } from "../infra/json-utf8-bytes.js";
55import { hasInterSessionUserProvenance } from "../sessions/input-provenance.js";
66import { extractAssistantVisibleText } from "../shared/chat-message-content.js";
77import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
8+import { estimateStringChars, estimateTokensFromChars } from "../utils/cjk-chars.js";
89import { stripInlineDirectiveTagsForDisplay } from "../utils/directive-tags.js";
910import { extractToolCallNames, hasToolCall } from "../utils/transcript-tools.js";
1011import { stripEnvelope } from "./chat-sanitize.js";
@@ -1159,6 +1160,85 @@ function resolvePositiveUsageNumber(value: unknown): number | undefined {
11591160return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
11601161}
116111621163+function extractTranscriptContentEstimatedChars(content: unknown): number {
1164+if (typeof content === "string") {
1165+const normalized = stripInlineDirectiveTagsForDisplay(content).text.trim();
1166+return normalized ? estimateStringChars(normalized) : 0;
1167+}
1168+if (!Array.isArray(content)) {
1169+return 0;
1170+}
1171+let chars = 0;
1172+for (const part of content) {
1173+if (!part || typeof part !== "object" || Array.isArray(part)) {
1174+continue;
1175+}
1176+const record = part as Record<string, unknown>;
1177+if (typeof record.text !== "string") {
1178+continue;
1179+}
1180+const type = typeof record.type === "string" ? record.type : "text";
1181+if (type !== "text" && type !== "output_text" && type !== "input_text") {
1182+continue;
1183+}
1184+const normalized = stripInlineDirectiveTagsForDisplay(record.text).text.trim();
1185+if (normalized) {
1186+chars += estimateStringChars(normalized);
1187+}
1188+}
1189+return chars;
1190+}
1191+1192+function extractTranscriptTokenEstimateFromLine(line: string): {
1193+estimatedChars: number;
1194+hasModelIdentity: boolean;
1195+} | null {
1196+if (isOversizedTranscriptLine(line)) {
1197+return null;
1198+}
1199+try {
1200+const parsed = JSON.parse(line) as Record<string, unknown>;
1201+const message =
1202+parsed.message && typeof parsed.message === "object" && !Array.isArray(parsed.message)
1203+ ? (parsed.message as Record<string, unknown>)
1204+ : undefined;
1205+if (!message) {
1206+return null;
1207+}
1208+const role = typeof message.role === "string" ? message.role : undefined;
1209+if (role !== "user" && role !== "assistant") {
1210+return null;
1211+}
1212+const modelProvider =
1213+typeof message.provider === "string"
1214+ ? message.provider.trim()
1215+ : typeof parsed.provider === "string"
1216+ ? parsed.provider.trim()
1217+ : undefined;
1218+const model =
1219+typeof message.model === "string"
1220+ ? message.model.trim()
1221+ : typeof parsed.model === "string"
1222+ ? parsed.model.trim()
1223+ : undefined;
1224+const isDeliveryMirror =
1225+role === "assistant" && modelProvider === "openclaw" && model === "delivery-mirror";
1226+if (isDeliveryMirror) {
1227+return null;
1228+}
1229+const contentChars = extractTranscriptContentEstimatedChars(message.content);
1230+if (contentChars <= 0) {
1231+return null;
1232+}
1233+return {
1234+estimatedChars: contentChars,
1235+hasModelIdentity: role === "assistant" && Boolean(modelProvider || model),
1236+};
1237+} catch {
1238+return null;
1239+}
1240+}
1241+11621242function extractUsageSnapshotFromTranscriptLine(
11631243line: string,
11641244): SessionTranscriptUsageSnapshot | null {
@@ -1261,8 +1341,17 @@ function extractAggregateUsageFromTranscriptLines(
12611341let sawCacheWrite = false;
12621342let costUsdTotal = 0;
12631343let sawCost = false;
1344+let estimatedTranscriptChars = 0;
1345+let sawEstimatedTranscriptContent = false;
1346+let sawEstimateModelIdentity = false;
1264134712651348for (const line of lines) {
1349+const estimate = extractTranscriptTokenEstimateFromLine(line);
1350+if (estimate) {
1351+estimatedTranscriptChars += estimate.estimatedChars;
1352+sawEstimatedTranscriptContent = true;
1353+sawEstimateModelIdentity ||= estimate.hasModelIdentity;
1354+}
12661355const current = extractUsageSnapshotFromTranscriptLine(line);
12671356if (!current) {
12681357continue;
@@ -1318,6 +1407,17 @@ function extractAggregateUsageFromTranscriptLines(
13181407if (sawCost) {
13191408snapshot.costUsd = costUsdTotal;
13201409}
1410+if (
1411+typeof snapshot.totalTokens !== "number" &&
1412+sawEstimatedTranscriptContent &&
1413+sawEstimateModelIdentity
1414+) {
1415+const estimatedTotalTokens = estimateTokensFromChars(estimatedTranscriptChars);
1416+if (estimatedTotalTokens > 0) {
1417+snapshot.totalTokens = estimatedTotalTokens;
1418+snapshot.totalTokensFresh = true;
1419+}
1420+}
13211421return snapshot;
13221422}
13231423此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。