@zxh126/pi-qqbot 1.3.1 → 1.3.2

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/README.md CHANGED
@@ -117,12 +117,13 @@ Some extension commands never print into the conversation — they call `ctx.ui.
117
117
 
118
118
  | Command | What happens |
119
119
  |---|---|
120
- | `/usage [range]`, `/cost [range]` | re-dispatched as `/qq usage <range>`: the bridge runs the original `/usage` command, then reads the fresh `customType: "usage-report"` entry from the session and renders it as text (requests, tokens, cache ratio, cost, per-model). **Usage unchanged** — keep sending `/usage today`. |
120
+ | `/usage [range]`, `/cost [range]` | re-dispatched as `/qq usage <range>`: the bridge runs the original `/usage` command, then reads the **fresh** `customType: "usage-report"` entry — first from `sessionManager.getEntries()`, and if that view has not picked up the append yet, straight from the tail of the session file — and renders it as text (requests, tokens, cache ratio, cost, per-model). **Usage unchanged** — keep sending `/usage today`. |
121
+ | `/usage rate <value>`, `/usage rate auto` | really executed (they write `~/.pi/agent/pi-usage-report.json` / refresh the cached rate); the bridge then reads those files back and reports the resulting rate, so an invalid value shows up as "unchanged" |
121
122
  | `/mcp`, `/llama` | answered: they need host-TUI selection dialogs / browser login, so nothing is dispatched |
122
123
 
123
- `/usage rate ...` and `/usage help` change settings or print help on the TUI; the bridge tells you
124
- that instead of silently doing nothing. The `usage-report` entry type is the only coupling point with
125
- [`pi-usage-report`](https://gitee.com/zxh126/pi-usage-report).
124
+ A bare `/usage rate` and `/usage help` are answered locally, without running the command. Coupling with
125
+ [`pi-usage-report`](https://gitee.com/zxh126/pi-usage-report) is limited to the `usage-report` entry type
126
+ plus the two files it keeps in `~/.pi/agent/`.
126
127
 
127
128
  Guards:
128
129
 
@@ -115,11 +115,11 @@ TUI/RPC 前端。在 QQ 里的表现就是「已派发,然后没下文」。
115
115
 
116
116
  | 命令 | 现在发生什么 |
117
117
  |---|---|
118
- | `/usage [范围]`、`/cost [范围]` | 改派为 `/qq usage <范围>`:桥接先重跑原 `/usage` 命令,再从 session 里读出新的 `customType: "usage-report"` 条目,渲染成文本回 QQ(请求数/总 token/缓存比/费用/按模型)。**用法不变**,照旧发 `/usage today` |
118
+ | `/usage [范围]`、`/cost [范围]` | 改派为 `/qq usage <范围>`:桥接先重跑原 `/usage` 命令,再取**本次新写入**的 `customType: "usage-report"` 条目(先读 `sessionManager.getEntries()` 的增量;发现另一个扩展的 `appendEntry` 还没反映到该视图时,直接读会话文件尾部),渲染成文本回 QQ(请求数/总 token/缓存比/费用/按模型)。**用法不变**,照旧发 `/usage today` |
119
119
  | `/mcp`、`/llama` | 直接回执:它们需要主机 TUI 的选择框 / 浏览器登录,不会派发 |
120
- | `/usage rate <值>`、`/usage rate auto` | 会真的执行(写 `~/.pi/agent/pi-usage-report.json` / 刷新汇率缓存),桥接再读回该文件确认新汇率;只发 `/usage rate` 与 `/usage help` 则本地直接回执 |
120
+ | `/usage rate <值>`、`/usage rate auto` | 会真的执行(写 `~/.pi/agent/pi-usage-report.json` / 刷新汇率缓存),桥接再读回文件确认新汇率,无效值会体现在“汇率未变”上;只发 `/usage rate` 与 `/usage help` 则本地直接回执,不执行命令 |
121
121
 
122
- 桥接与 [`pi-usage-report`](https://gitee.com/zxh126/pi-usage-report) 的耦合只有两处:`usage-report` 这个
122
+ 桥接与 [`pi-usage-report`](https://gitee.com/zxh126/pi-usage-report) 的耦合只有三处:`usage-report` 这个
123
123
  custom entry 类型名,以及它写在 `~/.pi/agent/` 下的两个配置文件。
124
124
 
125
125
  约束:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zxh126/pi-qqbot",
3
- "version": "1.3.1",
3
+ "version": "1.3.2",
4
4
  "type": "module",
5
5
  "description": "Bridge the Pi coding agent into QQ private chats, with remote tool-call approval",
6
6
  "license": "MIT",
package/slash.ts CHANGED
@@ -435,6 +435,64 @@ function sessionEntries(ctx: any): any[] {
435
435
  }
436
436
  }
437
437
 
438
+ function sessionFile(ctx: any): string | undefined {
439
+ try {
440
+ return ctx?.sessionManager?.getSessionFile?.() ?? undefined;
441
+ } catch {
442
+ return undefined;
443
+ }
444
+ }
445
+
446
+ /** 兼容三种时间形态:毫秒数字(pi.appendEntry 实际写的就是它)/ ISO 字符串 / Date */
447
+ function entryTime(value: unknown): number {
448
+ if (typeof value === "number") return Number.isFinite(value) ? value : NaN;
449
+ if (value instanceof Date) return value.getTime();
450
+ if (typeof value === "string" && value) return Date.parse(value);
451
+ return NaN;
452
+ }
453
+
454
+ /**
455
+ * 从会话文件尾部找最近一条 usage-report(generatedAt 不早于 since)。
456
+ *
457
+ * 为什么需要这条路:appendEntry 是**另一个扩展**写的,命令上下文里的
458
+ * `sessionManager.getEntries()` 不一定即时反映(实测 `/usage today` 已经写入了
459
+ * 会话文件,但 getEntries 看不到新增项)。数据库不会骗人,文件会。
460
+ */
461
+ function readUsageEntryFromFile(file: string | undefined, since: number): any | undefined {
462
+ if (!file) return undefined;
463
+ try {
464
+ const size = fs.statSync(file).size;
465
+ const chunk = Math.min(size, 512 * 1024);
466
+ if (chunk <= 0) return undefined;
467
+ const fd = fs.openSync(file, "r");
468
+ let text = "";
469
+ try {
470
+ const buf = Buffer.alloc(chunk);
471
+ fs.readSync(fd, buf, 0, chunk, size - chunk);
472
+ text = buf.toString("utf8");
473
+ } finally {
474
+ fs.closeSync(fd);
475
+ }
476
+ const lines = text.split("\n");
477
+ for (let i = lines.length - 1; i >= 0; i--) {
478
+ const line = lines[i].trim();
479
+ if (!line.startsWith("{")) continue;
480
+ let entry: any;
481
+ try {
482
+ entry = JSON.parse(line);
483
+ } catch {
484
+ continue;
485
+ }
486
+ if (entry?.type !== "custom" || entry?.customType !== USAGE_ENTRY_TYPE) continue;
487
+ const when = entryTime(entry?.data?.generatedAt ?? entry?.timestamp);
488
+ return Number.isFinite(when) && when >= since ? entry : undefined;
489
+ }
490
+ } catch {
491
+ return undefined;
492
+ }
493
+ return undefined;
494
+ }
495
+
438
496
  const intText = (value: unknown): string =>
439
497
  typeof value === "number" && Number.isFinite(value) ? Math.round(value).toLocaleString("en-US") : "0";
440
498
 
@@ -619,7 +677,9 @@ export async function runBridge(
619
677
  }
620
678
 
621
679
  const command = `/usage${arg ? ` ${arg}` : ""}`;
680
+ const since = Date.now();
622
681
  const before = new Set(sessionEntries(ctx).map((entry: any) => entry?.id));
682
+ const file = sessionFile(ctx);
623
683
  reply(`正在统计用量(${command})…`);
624
684
  try {
625
685
  await pi.sendUserMessage(command, { expandPromptTemplates: true });
@@ -627,11 +687,14 @@ export async function runBridge(
627
687
  reply(`统计失败:${String((error as any)?.message ?? error).slice(0, 200)}`);
628
688
  return;
629
689
  }
630
- const entry = [...sessionEntries(ctx)]
631
- .reverse()
632
- .find((e: any) => e?.type === "custom" && e?.customType === USAGE_ENTRY_TYPE && !before.has(e?.id));
690
+ // 先看 sessionManager 的增量,再退回读会话文件(appendEntry 可能还没同步到视图)
691
+ const entry =
692
+ [...sessionEntries(ctx)]
693
+ .reverse()
694
+ .find((e: any) => e?.type === "custom" && e?.customType === USAGE_ENTRY_TYPE && !before.has(e?.id)) ??
695
+ readUsageEntryFromFile(file, since);
633
696
  if (!entry) {
634
- reply("没拿到用量报告(命令没有产出数据)。");
697
+ reply("没拿到用量报告:/usage 已执行但找不到报告数据(可重试一次;若反复出现请把 pi-qqbot.log 尾部发我)。");
635
698
  return;
636
699
  }
637
700
  reply(formatUsage(entry.data));