@yeaft/webchat-agent 0.1.73 → 0.1.75

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/crew/control.js CHANGED
@@ -362,6 +362,11 @@ async function clearSession(session) {
362
362
 
363
363
  session.round = 0;
364
364
 
365
+ // 重置计费统计(clearSession 清除了所有 claudeSessionId,后续 query 全新,费用从零开始)
366
+ session.costUsd = 0;
367
+ session.totalInputTokens = 0;
368
+ session.totalOutputTokens = 0;
369
+
365
370
  const messagesPath = join(session.sharedDir, 'messages.json');
366
371
  await fs.writeFile(messagesPath, '[]').catch(() => {});
367
372
  await cleanupMessageShards(session.sharedDir);
@@ -15,11 +15,40 @@ const getMaxContext = () => ctx.CONFIG?.maxContextTokens || 128000;
15
15
  * 处理角色的流式输出
16
16
  */
17
17
  export async function processRoleOutput(session, roleName, roleQuery, roleState) {
18
+ // 辅助函数:将 lastSeenUsage 结算到 session(用于 abort/error 场景,避免丢失 token)
19
+ function settleLastSeenUsage() {
20
+ if (!roleState.lastSeenUsage) return;
21
+ const { totalCostUsd, inputTokens, outputTokens } = roleState.lastSeenUsage;
22
+ if (totalCostUsd != null) {
23
+ const costDelta = totalCostUsd - roleState.lastCostUsd;
24
+ if (costDelta > 0) session.costUsd += costDelta;
25
+ roleState.lastCostUsd = totalCostUsd;
26
+ }
27
+ if (inputTokens != null || outputTokens != null) {
28
+ const inputDelta = (inputTokens || 0) - (roleState.lastInputTokens || 0);
29
+ const outputDelta = (outputTokens || 0) - (roleState.lastOutputTokens || 0);
30
+ if (inputDelta > 0) session.totalInputTokens += inputDelta;
31
+ if (outputDelta > 0) session.totalOutputTokens += outputDelta;
32
+ roleState.lastInputTokens = inputTokens || 0;
33
+ roleState.lastOutputTokens = outputTokens || 0;
34
+ }
35
+ roleState.lastSeenUsage = null;
36
+ }
37
+
18
38
  try {
19
39
  for await (const message of roleQuery) {
20
40
  // 检查 session 是否已停止或暂停
21
41
  if (session.status === 'stopped' || session.status === 'paused') break;
22
42
 
43
+ // 每次收到带 usage/cost 的消息,暂存到 lastSeenUsage(供 abort/error 结算)
44
+ if (message.total_cost_usd != null || message.usage) {
45
+ roleState.lastSeenUsage = {
46
+ totalCostUsd: message.total_cost_usd,
47
+ inputTokens: message.usage?.input_tokens,
48
+ outputTokens: message.usage?.output_tokens
49
+ };
50
+ }
51
+
23
52
  if (message.type === 'system' && message.subtype === 'init') {
24
53
  roleState.claudeSessionId = message.session_id;
25
54
  console.log(`[Crew] ${roleName} session: ${message.session_id}`);
@@ -70,20 +99,8 @@ export async function processRoleOutput(session, roleName, roleQuery, roleState)
70
99
 
71
100
  endRoleStreaming(session, roleName);
72
101
 
73
- // 更新费用(差值计算)
74
- if (message.total_cost_usd != null) {
75
- const costDelta = message.total_cost_usd - roleState.lastCostUsd;
76
- if (costDelta > 0) session.costUsd += costDelta;
77
- roleState.lastCostUsd = message.total_cost_usd;
78
- }
79
- if (message.usage) {
80
- const inputDelta = (message.usage.input_tokens || 0) - (roleState.lastInputTokens || 0);
81
- const outputDelta = (message.usage.output_tokens || 0) - (roleState.lastOutputTokens || 0);
82
- if (inputDelta > 0) session.totalInputTokens += inputDelta;
83
- if (outputDelta > 0) session.totalOutputTokens += outputDelta;
84
- roleState.lastInputTokens = message.usage.input_tokens || 0;
85
- roleState.lastOutputTokens = message.usage.output_tokens || 0;
86
- }
102
+ // 更新费用(通过 settleLastSeenUsage 统一处理,避免重复逻辑)
103
+ settleLastSeenUsage();
87
104
 
88
105
  // 持久化 sessionId
89
106
  if (roleState.claudeSessionId) {
@@ -175,6 +192,8 @@ export async function processRoleOutput(session, roleName, roleQuery, roleState)
175
192
  } catch (error) {
176
193
  if (error.name === 'AbortError') {
177
194
  console.log(`[Crew] ${roleName} aborted`);
195
+ // 结算 abort 前累积的 usage,避免丢失 token
196
+ settleLastSeenUsage();
178
197
  if (session.status === 'paused' && roleState.accumulatedText) {
179
198
  const routes = parseRoutes(roleState.accumulatedText);
180
199
  if (routes.length > 0 && session.pendingRoutes.length === 0) {
@@ -186,6 +205,9 @@ export async function processRoleOutput(session, roleName, roleQuery, roleState)
186
205
  } else {
187
206
  console.error(`[Crew] ${roleName} error:`, error.message);
188
207
 
208
+ // 结算 error 前累积的 usage,避免丢失 token
209
+ settleLastSeenUsage();
210
+
189
211
  // Step 1: 清理 roleState
190
212
  endRoleStreaming(session, roleName);
191
213
  const errorTurnText = roleState.accumulatedText;
@@ -127,6 +127,11 @@ export async function createRoleQuery(session, roleName) {
127
127
  options: queryOptions
128
128
  });
129
129
 
130
+ // resume 场景:保留已有 roleState 的 baseline,避免双重计算
131
+ // 只有 fresh query(无 savedSessionId)才用 0 初始化
132
+ const existingState = session.roleStates.get(roleName);
133
+ const isResume = !!savedSessionId;
134
+
130
135
  const roleState = {
131
136
  query: roleQuery,
132
137
  inputStream,
@@ -134,9 +139,10 @@ export async function createRoleQuery(session, roleName) {
134
139
  accumulatedText: '',
135
140
  turnActive: false,
136
141
  claudeSessionId: savedSessionId,
137
- lastCostUsd: 0,
138
- lastInputTokens: 0,
139
- lastOutputTokens: 0,
142
+ lastCostUsd: (isResume && existingState?.lastCostUsd) || 0,
143
+ lastInputTokens: (isResume && existingState?.lastInputTokens) || 0,
144
+ lastOutputTokens: (isResume && existingState?.lastOutputTokens) || 0,
145
+ lastSeenUsage: null,
140
146
  consecutiveErrors: 0,
141
147
  lastDispatchContent: null,
142
148
  lastDispatchFrom: null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.73",
3
+ "version": "0.1.75",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",