jbc-dssi 1.1.1 → 1.1.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jbc-dssi",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "type": "module",
5
5
  "description": "鲸编程 - 不只是手机遥控操作电脑,而是将电脑级的Agent能力装进手机",
6
6
  "keywords": [
@@ -99,6 +99,24 @@ function sumUsage(acc, u) {
99
99
  };
100
100
  }
101
101
 
102
+ /**
103
+ * 把落库的「逐步用量」数组合成一行用量 —— **读侧唯一口径**(2026-11-01 存储结构修复)。
104
+ *
105
+ * 只在记录带 `stepUsages`(新格式)时使用。旧记录没这个数组,调用方必须原样使用
106
+ * 历史 `usage`,**不允许**拿它去"猜"或"补" —— 按负责人决定历史不救、不做回填。
107
+ *
108
+ * 返回 null:整轮一条有效用量都没有(老 DSH / 这轮没统计到)→ 手机侧不显示用量项。
109
+ */
110
+ function aggregateStepUsages(stepUsages) {
111
+ if (!Array.isArray(stepUsages)) return null;
112
+ let acc = null;
113
+ for (const one of stepUsages) {
114
+ const u = normalizeUsage(one);
115
+ if (u) acc = sumUsage(acc, u);
116
+ }
117
+ return acc;
118
+ }
119
+
102
120
  /**
103
121
  * @param {object} opts
104
122
  * @param {object} opts.ctx DSH 上下文(sessionController 公开方法)
@@ -158,7 +176,11 @@ export function createRoundEngine({ ctx, state, dssAgent, log, save, broadcast }
158
176
 
159
177
  /**
160
178
  * 投递一轮给一个成员,等回复完成后 settle。
161
- * @returns {Promise<{text?:string, failed?:string, missed?:boolean, usage?:object|null, steps?:number}>}
179
+ *
180
+ * @returns {Promise<{text?:string, failed?:string, missed?:boolean,
181
+ * usage?:object|null, steps?:number, stepUsages?:Array<object>}>}
182
+ * `stepUsages` = 本轮每一步的用量(每一步一条,按到达顺序);它和 `usage`
183
+ * (各步求和)一起进房间日志,供读侧还原与兼容老客户端。
162
184
  */
163
185
  function deliver(group, member, prompt, attachments = {}) {
164
186
  return new Promise((resolve) => {
@@ -191,6 +213,14 @@ export function createRoundEngine({ ctx, state, dssAgent, log, save, broadcast }
191
213
  * 于是「共 N 步」配的是"最后一步"的 token,步数越多的轮次少算越多。
192
214
  */
193
215
  let turnUsage = null;
216
+ /*
217
+ * 本轮**逐步**用量(2026-11-01 新增):每一步一条,与 turnUsage 同源。
218
+ *
219
+ * 为什么存这个:旧存储只留聚合值,刷新/重进后无法还原「每一步花了多少」,
220
+ * 历史记录也就永远只能信当时写下的那个数。逐步入库后,读侧可以用它求和还原 ——
221
+ * 面板在实时、刷新、重进、换端任何时刻都是同一份数据的计算结果。
222
+ */
223
+ const stepUsages = [];
194
224
  /*
195
225
  * 「共 N 步」的账:turn → 这一轮出现过的 step 号集合。
196
226
  *
@@ -261,13 +291,21 @@ export function createRoundEngine({ ctx, state, dssAgent, log, save, broadcast }
261
291
  // 工具调用那几步没有 text,不能拿它们当回答;有正文才覆盖。
262
292
  const text = assistantText(event);
263
293
  if (text) lastText = text;
264
- const usage = normalizeUsage(data.usage);
265
- if (usage) turnUsage = sumUsage(turnUsage, usage);
266
294
  /*
267
295
  * 步数:**所有** assistant/message 都要记,包括只调工具、不画块的那几步
268
296
  * (与单聊一致 —— token 大头正是这些步骤花掉的)。
269
297
  */
270
298
  const step = typeof data.step === 'number' ? data.step : -1;
299
+ const usage = normalizeUsage(data.usage);
300
+ if (usage) {
301
+ /*
302
+ * 逐步入库:步号 + 这一步的五个 token 字段。
303
+ * 同一步重试会各存一条(用量按 attempt 累加,与单聊/Web 一致),
304
+ * 步数仍由 turnStepNums 按 step 号去重,两者互不影响。
305
+ */
306
+ stepUsages.push({ step, ...usage });
307
+ turnUsage = sumUsage(turnUsage, usage);
308
+ }
271
309
  if (step >= 0 && typeof data.turn === 'number') {
272
310
  if (!turnStepNums.has(data.turn)) turnStepNums.set(data.turn, new Set());
273
311
  turnStepNums.get(data.turn).add(step);
@@ -283,7 +321,7 @@ export function createRoundEngine({ ctx, state, dssAgent, log, save, broadcast }
283
321
  */
284
322
  const endTurn = typeof data.turn === 'number' ? data.turn : lastTurn;
285
323
  const steps = turnStepNums.get(endTurn)?.size ?? 0;
286
- settle({ text: lastText, usage: turnUsage, steps });
324
+ settle({ text: lastText, usage: turnUsage, steps, stepUsages });
287
325
  }
288
326
  ac.abort();
289
327
  } else if (event?.type === 'turn/error' || event?.type === 'turn/failed') {
@@ -342,7 +380,21 @@ export function createRoundEngine({ ctx, state, dssAgent, log, save, broadcast }
342
380
  });
343
381
  }
344
382
 
345
- /** 把一次投递的结果落成房间行(成员/主管同一套:手机侧按 memberId 显示名字与主管标)。 */
383
+ /**
384
+ * 把一次投递的结果落成房间行(成员/主管同一套:手机侧按 memberId 显示名字与主管标)。
385
+ *
386
+ * 房间行里的「本轮用量」有两种格式,**读侧必须都认**:
387
+ *
388
+ * 旧格式(2026-10-31 及以前落盘的行,只保留聚合值、无法重算):
389
+ * { usage: {inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, totalTokens}, steps: 10 }
390
+ * 新格式(2026-11-01 起,多一个逐步数组):
391
+ * { usage: {...恒等于逐步求和...}, steps: 10,
392
+ * stepUsages: [{ step: 1, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, totalTokens }, ...] }
393
+ *
394
+ * `usage` 新格式里照写:给还没升级的老手机读(它们只认这个字段)。
395
+ * 新手机的 GroupStore 优先用 `stepUsages` 求和 —— 刷新/重进后与实时同源。
396
+ * 旧行没有 `stepUsages`:读侧原样显示历史值,不补 0、不重算、不报错(历史不救)。
397
+ */
346
398
  function emitReplyLine(group, round, member, result) {
347
399
  const text = String(result.text ?? '').trim() || '[未回应]';
348
400
  emitLine(group.id, {
@@ -353,6 +405,10 @@ export function createRoundEngine({ ctx, state, dssAgent, log, save, broadcast }
353
405
  text,
354
406
  usage: result.usage ?? null,
355
407
  steps: result.steps ?? 0,
408
+ // 只有真的收到过 usage 才写这个字段;否则 JSON 里不出现(= 老格式,读侧回落)
409
+ stepUsages: Array.isArray(result.stepUsages) && result.stepUsages.length > 0
410
+ ? result.stepUsages
411
+ : undefined,
356
412
  round: round?.rounds ?? 0,
357
413
  });
358
414
  round.totalTurns += 1;
@@ -594,9 +650,21 @@ export function createRoundEngine({ ctx, state, dssAgent, log, save, broadcast }
594
650
  }
595
651
  }
596
652
 
597
- /** 打开群聊页时拉房间历史。 */
653
+ /**
654
+ * 打开群聊页时拉房间历史。
655
+ *
656
+ * 【读侧口径(2026-11-01)】
657
+ * 带 `stepUsages` 的新记录:用量由逐步数据**当场求和**再下发 —— 刷新/重进拿到的
658
+ * 值和实时那一刻完全同源(这份和也是 `usage` 字段的来源,二者恒等)。
659
+ * 没有 `stepUsages` 的旧记录:**原样返回**,显示当时写下的历史值。按负责人决定
660
+ * 历史不救 —— 这里不猜、不补 0、不拿别的字段假装重算。
661
+ */
598
662
  function roomSnapshot(group) {
599
- return (state.rooms[group.id] ?? []).slice(-ROOM_LOG_MAX);
663
+ return (state.rooms[group.id] ?? []).slice(-ROOM_LOG_MAX).map((line) => {
664
+ if (!Array.isArray(line?.stepUsages) || line.stepUsages.length === 0) return line;
665
+ const usage = aggregateStepUsages(line.stepUsages);
666
+ return usage ? { ...line, usage } : line;
667
+ });
600
668
  }
601
669
 
602
670
  return { handleUserMessage, roomSnapshot, ensureGroupSession };