@zxh126/pi-qqbot 1.3.1 → 1.3.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/README.md +5 -4
- package/docs/README.zh-CN.md +3 -3
- package/package.json +1 -1
- package/slash.ts +92 -4
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
|
|
120
|
+
| `/usage [range]`, `/cost [range]` | re-dispatched as `/qq usage <range>`: the bridge runs the original `/usage` command, then **polls** for the fresh `customType: "usage-report"` entry for up to 2 s (the append lands ~100 ms *after* `sendUserMessage()` resolves, so a single read always misses it), falling back to 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
|
|
124
|
-
|
|
125
|
-
|
|
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
|
|
package/docs/README.zh-CN.md
CHANGED
|
@@ -115,11 +115,11 @@ TUI/RPC 前端。在 QQ 里的表现就是「已派发,然后没下文」。
|
|
|
115
115
|
|
|
116
116
|
| 命令 | 现在发生什么 |
|
|
117
117
|
|---|---|
|
|
118
|
-
| `/usage [范围]`、`/cost [范围]` | 改派为 `/qq usage <范围>`:桥接先重跑原 `/usage`
|
|
118
|
+
| `/usage [范围]`、`/cost [范围]` | 改派为 `/qq usage <范围>`:桥接先重跑原 `/usage` 命令,再**轮询等待**本次新写入的 `customType: "usage-report"` 条目(`appendEntry` 比 `sendUserMessage()` 返回晚约 100 ms,只读一次必然落空),最多等 2 秒,仍未命中才退回读会话文件尾部;然后渲染成文本回 QQ(请求数/总 token/缓存比/费用/按模型)。**用法不变**,照旧发 `/usage today` |
|
|
119
119
|
| `/mcp`、`/llama` | 直接回执:它们需要主机 TUI 的选择框 / 浏览器登录,不会派发 |
|
|
120
|
-
| `/usage rate <值>`、`/usage rate auto` | 会真的执行(写 `~/.pi/agent/pi-usage-report.json` /
|
|
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)
|
|
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
package/slash.ts
CHANGED
|
@@ -382,6 +382,13 @@ export function pickerExpired(picker: PickerState | null): boolean {
|
|
|
382
382
|
/** pi-usage-report 写入 session 的 custom entry 类型(耦合点,约定值) */
|
|
383
383
|
const USAGE_ENTRY_TYPE = "usage-report";
|
|
384
384
|
|
|
385
|
+
/**
|
|
386
|
+
* appendEntry 的写入比 `sendUserMessage` 的 resolve 晚(实测 promise 183ms 返回,
|
|
387
|
+
* entry 在之后 ~100ms 才出现在 getEntries 里),所以必须轮询而不是只读一次。
|
|
388
|
+
*/
|
|
389
|
+
const USAGE_WAIT_TIMEOUT_MS = 2000;
|
|
390
|
+
const USAGE_POLL_INTERVAL_MS = 100;
|
|
391
|
+
|
|
385
392
|
/** pi-usage-report 的配置/汇率缓存(读回来确认:命令真的生效了,而不是口头应下) */
|
|
386
393
|
const USAGE_SETTINGS_FILE = "pi-usage-report.json";
|
|
387
394
|
const USAGE_RATE_FILE = "pi-usage-report-rate.json";
|
|
@@ -435,6 +442,64 @@ function sessionEntries(ctx: any): any[] {
|
|
|
435
442
|
}
|
|
436
443
|
}
|
|
437
444
|
|
|
445
|
+
function sessionFile(ctx: any): string | undefined {
|
|
446
|
+
try {
|
|
447
|
+
return ctx?.sessionManager?.getSessionFile?.() ?? undefined;
|
|
448
|
+
} catch {
|
|
449
|
+
return undefined;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** 兼容三种时间形态:毫秒数字(pi.appendEntry 实际写的就是它)/ ISO 字符串 / Date */
|
|
454
|
+
function entryTime(value: unknown): number {
|
|
455
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : NaN;
|
|
456
|
+
if (value instanceof Date) return value.getTime();
|
|
457
|
+
if (typeof value === "string" && value) return Date.parse(value);
|
|
458
|
+
return NaN;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* 从会话文件尾部找最近一条 usage-report(generatedAt 不早于 since)。
|
|
463
|
+
*
|
|
464
|
+
* 为什么需要这条路:appendEntry 是**另一个扩展**写的,命令上下文里的
|
|
465
|
+
* `sessionManager.getEntries()` 不一定即时反映(实测 `/usage today` 已经写入了
|
|
466
|
+
* 会话文件,但 getEntries 看不到新增项)。数据库不会骗人,文件会。
|
|
467
|
+
*/
|
|
468
|
+
function readUsageEntryFromFile(file: string | undefined, since: number): any | undefined {
|
|
469
|
+
if (!file) return undefined;
|
|
470
|
+
try {
|
|
471
|
+
const size = fs.statSync(file).size;
|
|
472
|
+
const chunk = Math.min(size, 512 * 1024);
|
|
473
|
+
if (chunk <= 0) return undefined;
|
|
474
|
+
const fd = fs.openSync(file, "r");
|
|
475
|
+
let text = "";
|
|
476
|
+
try {
|
|
477
|
+
const buf = Buffer.alloc(chunk);
|
|
478
|
+
fs.readSync(fd, buf, 0, chunk, size - chunk);
|
|
479
|
+
text = buf.toString("utf8");
|
|
480
|
+
} finally {
|
|
481
|
+
fs.closeSync(fd);
|
|
482
|
+
}
|
|
483
|
+
const lines = text.split("\n");
|
|
484
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
485
|
+
const line = lines[i].trim();
|
|
486
|
+
if (!line.startsWith("{")) continue;
|
|
487
|
+
let entry: any;
|
|
488
|
+
try {
|
|
489
|
+
entry = JSON.parse(line);
|
|
490
|
+
} catch {
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
if (!isUsageEntry(entry)) continue;
|
|
494
|
+
const when = entryTime(entry?.data?.generatedAt ?? entry?.timestamp);
|
|
495
|
+
return Number.isFinite(when) && when >= since ? entry : undefined;
|
|
496
|
+
}
|
|
497
|
+
} catch {
|
|
498
|
+
return undefined;
|
|
499
|
+
}
|
|
500
|
+
return undefined;
|
|
501
|
+
}
|
|
502
|
+
|
|
438
503
|
const intText = (value: unknown): string =>
|
|
439
504
|
typeof value === "number" && Number.isFinite(value) ? Math.round(value).toLocaleString("en-US") : "0";
|
|
440
505
|
|
|
@@ -446,6 +511,28 @@ function localStamp(value: unknown): string {
|
|
|
446
511
|
return `${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
447
512
|
}
|
|
448
513
|
|
|
514
|
+
const isUsageEntry = (entry: any): boolean =>
|
|
515
|
+
entry?.type === "custom" && entry?.customType === USAGE_ENTRY_TYPE;
|
|
516
|
+
|
|
517
|
+
const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* 等本次 /usage 写的报告出现:每 100ms 查一次 getEntries(最多 2s),
|
|
521
|
+
* 全程未命中再试一次会话文件(custom entry 可能很晚才落盘)。
|
|
522
|
+
*/
|
|
523
|
+
async function waitForUsageEntry(
|
|
524
|
+
ctx: any,
|
|
525
|
+
opts: { before: Set<unknown>; since: number; file?: string },
|
|
526
|
+
): Promise<any | undefined> {
|
|
527
|
+
const deadline = Date.now() + USAGE_WAIT_TIMEOUT_MS;
|
|
528
|
+
while (Date.now() < deadline) {
|
|
529
|
+
const fresh = [...sessionEntries(ctx)].reverse().find((entry: any) => isUsageEntry(entry) && !opts.before.has(entry?.id));
|
|
530
|
+
if (fresh) return fresh;
|
|
531
|
+
await sleep(USAGE_POLL_INTERVAL_MS);
|
|
532
|
+
}
|
|
533
|
+
return readUsageEntryFromFile(opts.file, opts.since);
|
|
534
|
+
}
|
|
535
|
+
|
|
449
536
|
/** 把 pi-usage-report 的 ReportData 渲染成 QQ 可读的纯文本 */
|
|
450
537
|
function formatUsage(data: any): string {
|
|
451
538
|
if (!data?.totals) return "用量报告为空。";
|
|
@@ -619,7 +706,9 @@ export async function runBridge(
|
|
|
619
706
|
}
|
|
620
707
|
|
|
621
708
|
const command = `/usage${arg ? ` ${arg}` : ""}`;
|
|
709
|
+
const since = Date.now();
|
|
622
710
|
const before = new Set(sessionEntries(ctx).map((entry: any) => entry?.id));
|
|
711
|
+
const file = sessionFile(ctx);
|
|
623
712
|
reply(`正在统计用量(${command})…`);
|
|
624
713
|
try {
|
|
625
714
|
await pi.sendUserMessage(command, { expandPromptTemplates: true });
|
|
@@ -627,11 +716,10 @@ export async function runBridge(
|
|
|
627
716
|
reply(`统计失败:${String((error as any)?.message ?? error).slice(0, 200)}`);
|
|
628
717
|
return;
|
|
629
718
|
}
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
.find((e: any) => e?.type === "custom" && e?.customType === USAGE_ENTRY_TYPE && !before.has(e?.id));
|
|
719
|
+
// 先轮询 getEntries 的增量(appendEntry 晚于 await 返回),超时再退回读会话文件
|
|
720
|
+
const entry = await waitForUsageEntry(ctx, { before, since, file });
|
|
633
721
|
if (!entry) {
|
|
634
|
-
reply("
|
|
722
|
+
reply("没拿到用量报告:/usage 已执行但找不到报告数据(可重试一次;若反复出现请把 pi-qqbot.log 尾部发我)。");
|
|
635
723
|
return;
|
|
636
724
|
}
|
|
637
725
|
reply(formatUsage(entry.data));
|