@zxh126/pi-qqbot 1.3.0 → 1.3.1

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/constants.ts CHANGED
@@ -16,5 +16,7 @@ export const DISPATCH_RETRY_MS = 3000;
16
16
  export const CANCEL_SETTLE_GRACE_MS = 8000;
17
17
 
18
18
  const PI_DIR = path.join(os.homedir(), ".pi", "agent");
19
+ /** pi 的 agent 目录(其他扩展也往里写东西,如 pi-usage-report) */
20
+ export const PI_AGENT_DIR = PI_DIR;
19
21
  export const CONFIG_PATH = () => path.join(PI_DIR, "pi-qqbot.json");
20
22
  export const LOCK_PATH = (appId: string) => path.join(PI_DIR, `pi-qqbot-${appId}.lock`);
@@ -117,10 +117,10 @@ TUI/RPC 前端。在 QQ 里的表现就是「已派发,然后没下文」。
117
117
  |---|---|
118
118
  | `/usage [范围]`、`/cost [范围]` | 改派为 `/qq usage <范围>`:桥接先重跑原 `/usage` 命令,再从 session 里读出新的 `customType: "usage-report"` 条目,渲染成文本回 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
121
 
121
- `/usage rate ...` 与 `/usage help` 会改汇率或打印帮助(只在 TUI 可见),桥接会明确告知。
122
- 桥接与 [`pi-usage-report`](https://gitee.com/zxh126/pi-usage-report) 的唯一耦合是 `usage-report` 这个
123
- custom entry 类型名。
122
+ 桥接与 [`pi-usage-report`](https://gitee.com/zxh126/pi-usage-report) 的耦合只有两处:`usage-report` 这个
123
+ custom entry 类型名,以及它写在 `~/.pi/agent/` 下的两个配置文件。
124
124
 
125
125
  约束:
126
126
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zxh126/pi-qqbot",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
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
@@ -26,6 +26,8 @@
26
26
  import fs from "node:fs";
27
27
  import path from "node:path";
28
28
 
29
+ import { PI_AGENT_DIR } from "./constants";
30
+
29
31
  export interface SlashCommandText {
30
32
  /** command name without the leading slash, lower-cased (`skill:name` keeps its colon) */
31
33
  name: string;
@@ -380,6 +382,51 @@ export function pickerExpired(picker: PickerState | null): boolean {
380
382
  /** pi-usage-report 写入 session 的 custom entry 类型(耦合点,约定值) */
381
383
  const USAGE_ENTRY_TYPE = "usage-report";
382
384
 
385
+ /** pi-usage-report 的配置/汇率缓存(读回来确认:命令真的生效了,而不是口头应下) */
386
+ const USAGE_SETTINGS_FILE = "pi-usage-report.json";
387
+ const USAGE_RATE_FILE = "pi-usage-report-rate.json";
388
+
389
+ const USAGE_HELP = [
390
+ "/usage(默认 today)",
391
+ "/usage today | yesterday | week | lastweek | month | lastmonth | all",
392
+ "/usage 7d | 12h | 2w | 3m | 10天 滚动窗口",
393
+ "/usage 2026-09-01..2026-09-30 本地日期区间(含首尾)",
394
+ "/usage 今天 | 昨天 | 本周 | 上周 | 本月 | 上月 | 全部",
395
+ "/usage rate <值> | rate auto 固定 USD→本币汇率,或恢复自动(会真的写配置)",
396
+ ].join("\n");
397
+
398
+ function readAgentJson(file: string): any {
399
+ try {
400
+ return JSON.parse(fs.readFileSync(path.join(PI_AGENT_DIR, file), "utf8"));
401
+ } catch {
402
+ return undefined;
403
+ }
404
+ }
405
+
406
+ const CURRENCY_SYMBOLS: Record<string, string> = { CNY: "¥", USD: "$", EUR: "€", JPY: "¥", GBP: "£", HKD: "HK$" };
407
+
408
+ /** 复刻 pi-usage-report 的 readSettings 优先级:fixed(usdRate) > 缓存的发布汇率 > 无 */
409
+ function usageMoney(): { code: string; symbol: string; rate: number | null; source: string } {
410
+ const raw = readAgentJson(USAGE_SETTINGS_FILE) ?? {};
411
+ const code = String(raw.currency ?? "CNY").toUpperCase();
412
+ const symbol = typeof raw.symbol === "string" ? raw.symbol : (CURRENCY_SYMBOLS[code] ?? code);
413
+ const pinned = typeof raw.usdRate === "number" && raw.usdRate > 0 ? raw.usdRate : undefined;
414
+ const fixed = raw.rateMode === "auto" ? undefined : pinned;
415
+ if (fixed !== undefined) return { code, symbol, rate: fixed, source: "fixed,/usage rate 手动固定" };
416
+ const cache = readAgentJson(USAGE_RATE_FILE);
417
+ if (cache && cache.code === code && typeof cache.rate === "number" && cache.rate > 0) {
418
+ const ageMinutes = Math.max(0, Math.round((Date.now() - Number(cache.fetchedAt ?? 0)) / 60000));
419
+ return { code, symbol, rate: cache.rate, source: `cache,${ageMinutes} 分钟前抓取` };
420
+ }
421
+ return { code, symbol, rate: null, source: "无,可用 /usage rate <值>" };
422
+ }
423
+
424
+ function moneyText(money: { code: string; rate: number | null; source: string }): string {
425
+ return money.rate === null
426
+ ? `USD/${money.code}:无可用汇率(${money.source})`
427
+ : `USD/${money.code} = ${money.rate}(${money.source})`;
428
+ }
429
+
383
430
  function sessionEntries(ctx: any): any[] {
384
431
  try {
385
432
  return ctx?.sessionManager?.getEntries?.() ?? [];
@@ -536,17 +583,41 @@ export async function runBridge(
536
583
  }
537
584
  case "usage": {
538
585
  // /qq usage [...]:重跑原 /usage 命令,再从 session 里读回它写的 custom entry
539
- if (first === "rate" || first === "help" || first === "?") {
540
- reply(
541
- `/usage ${arg} 会修改汇率或打印帮助,那些输出只在主机 TUI 可见;\n发 /usage today|week|month|all|7d 可在 QQ 里看统计。`,
542
- );
586
+ const rest = arg.slice(first.length).trim();
587
+
588
+ // help:不执行命令(它在 TUI 打文本),直接回本地帮助
589
+ if (first === "help" || first === "?") {
590
+ reply(USAGE_HELP);
543
591
  return;
544
592
  }
593
+
545
594
  const commands = (pi?.getCommands?.() ?? []) as { name: string }[];
546
- if (!commands.some((c) => ["usage", "cost"].includes(String(c.name).toLowerCase()))) {
595
+ const hasUsage = commands.some((c) => ["usage", "cost"].includes(String(c.name).toLowerCase()));
596
+ if (!hasUsage) {
547
597
  reply("主机上没有 /usage 命令(需要安装 pi-usage-report),无法统计。");
548
598
  return;
549
599
  }
600
+
601
+ // rate:会写配置/联网刷新汇率,必须真的执行,再读回状态确认(不能只回一句“TUI 可见”)
602
+ if (first === "rate") {
603
+ if (!rest) {
604
+ reply(moneyText(usageMoney()));
605
+ return;
606
+ }
607
+ const rateCommand = `/usage rate ${rest}`;
608
+ try {
609
+ await pi.sendUserMessage(rateCommand, { expandPromptTemplates: true });
610
+ } catch (error) {
611
+ reply(`执行 ${rateCommand} 失败:${String((error as any)?.message ?? error).slice(0, 200)}`);
612
+ return;
613
+ }
614
+ const after = usageMoney();
615
+ reply(
616
+ `已执行 ${rateCommand}\n${moneyText(after)}\n(下一步 /usage today 的 ¥/USD 换算就用它;无效值会被忽略)`,
617
+ );
618
+ return;
619
+ }
620
+
550
621
  const command = `/usage${arg ? ` ${arg}` : ""}`;
551
622
  const before = new Set(sessionEntries(ctx).map((entry: any) => entry?.id));
552
623
  reply(`正在统计用量(${command})…`);