agent-loft 0.0.0-stage → 0.2.7

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.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +110 -2
  3. package/bin/cli.js +52 -0
  4. package/package.json +64 -4
  5. package/src/agents/claude/account.ts +122 -0
  6. package/src/agents/claude/driver.ts +216 -0
  7. package/src/agents/claude/errors.ts +129 -0
  8. package/src/agents/claude/permissions.ts +260 -0
  9. package/src/agents/claude/probe.ts +54 -0
  10. package/src/agents/claude/session.ts +487 -0
  11. package/src/agents/codex/account.ts +122 -0
  12. package/src/agents/codex/app-server.ts +346 -0
  13. package/src/agents/codex/driver.ts +210 -0
  14. package/src/agents/codex/errors.ts +158 -0
  15. package/src/agents/codex/protocol.ts +284 -0
  16. package/src/agents/codex/refusals.ts +62 -0
  17. package/src/agents/codex/session.ts +773 -0
  18. package/src/channels/feishu/bot.ts +669 -0
  19. package/src/channels/feishu/cards.ts +360 -0
  20. package/src/channels/feishu/client.ts +185 -0
  21. package/src/channels/feishu/envelope.ts +117 -0
  22. package/src/channels/feishu/index.ts +62 -0
  23. package/src/channels/feishu/message.ts +115 -0
  24. package/src/channels/feishu/register.ts +171 -0
  25. package/src/channels/feishu/slash-sync.ts +192 -0
  26. package/src/channels/feishu/turn-stream.ts +98 -0
  27. package/src/channels/web/api.ts +645 -0
  28. package/src/channels/web/auth.ts +204 -0
  29. package/src/channels/web/http.ts +118 -0
  30. package/src/channels/web/push.ts +218 -0
  31. package/src/channels/web/server.ts +446 -0
  32. package/src/cli/doctor.ts +39 -0
  33. package/src/cli/import.ts +155 -0
  34. package/src/cli/pair.ts +35 -0
  35. package/src/cli/passwd.ts +15 -0
  36. package/src/cli/service.ts +137 -0
  37. package/src/core/agent-env.ts +11 -0
  38. package/src/core/command-catalog.ts +112 -0
  39. package/src/core/commands.ts +104 -0
  40. package/src/core/config.ts +285 -0
  41. package/src/core/dedup.ts +16 -0
  42. package/src/core/footers.ts +88 -0
  43. package/src/core/hub.ts +1152 -0
  44. package/src/core/interactions.ts +273 -0
  45. package/src/core/pairing.ts +156 -0
  46. package/src/core/store.ts +178 -0
  47. package/src/core/types.ts +213 -0
  48. package/src/core/uploads.ts +200 -0
  49. package/src/home.ts +43 -0
  50. package/src/main.ts +68 -0
  51. package/web/app.js +3731 -0
  52. package/web/apple-touch-icon.png +0 -0
  53. package/web/icon-maskable.svg +13 -0
  54. package/web/icon.svg +12 -0
  55. package/web/index.html +23 -0
  56. package/web/manifest.webmanifest +15 -0
  57. package/web/style.css +2604 -0
  58. package/web/sw.js +76 -0
@@ -0,0 +1,88 @@
1
+ // 每轮回复的「用时 / 费用 / tokens」脚注。
2
+ //
3
+ // 它只在一轮结束的那一刻随 result 事件出现,agent 自己的会话记录里没有。页面重新加载
4
+ // (手机切回前台、刷新)读的是 agent 的记录,脚注就丢了。所以在这里按会话记下来,
5
+ // 读历史时再按回复正文的结尾对回去。
6
+ import path from "node:path";
7
+ import { dataDir, readJson, writeJsonAtomic } from "../home.js";
8
+ import type { HistoryMessage } from "./types.js";
9
+
10
+ /** t:回复正文(去掉空白后)的结尾;f:脚注 */
11
+ type Entry = { t: string; f: string };
12
+ type Store = Record<string, { at: number; entries: Entry[] }>;
13
+
14
+ const FILE = () => path.join(dataDir, "footers.json");
15
+ const MAX_SESSIONS = 300;
16
+ const MAX_PER_SESSION = 1000;
17
+ /** 存下来比对用的正文结尾长度 */
18
+ const TAIL = 80;
19
+
20
+ let store: Store | null = null;
21
+ let timer: NodeJS.Timeout | null = null;
22
+
23
+ function load(): Store {
24
+ return (store ??= readJson<Store>(FILE(), {}));
25
+ }
26
+
27
+ function persistSoon(): void {
28
+ if (timer) return;
29
+ timer = setTimeout(() => {
30
+ timer = null;
31
+ const s = load();
32
+ // 只留最近用过的会话
33
+ const keys = Object.keys(s).sort((a, b) => s[b]!.at - s[a]!.at);
34
+ for (const k of keys.slice(MAX_SESSIONS)) delete s[k];
35
+ writeJsonAtomic(FILE(), s);
36
+ }, 1000);
37
+ timer.unref();
38
+ }
39
+
40
+ // 空白全去掉:实时的 result 和落盘的记录在换行、空格上可能不完全一致
41
+ const squash = (text: string): string => text.replace(/\s+/g, "");
42
+
43
+ /**
44
+ * Claude 的 result 只是这一轮最后一段文字,而记录里同一轮的多段文字会拼在一起,
45
+ * 所以比的是「记录里的正文以它结尾」。
46
+ */
47
+ function endsWithTail(text: string, tail: string): boolean {
48
+ return squash(text).endsWith(tail);
49
+ }
50
+
51
+ export function recordFooter(sessionId: string | undefined, text: string, footer: string): void {
52
+ const t = squash(text).slice(-TAIL);
53
+ if (!sessionId || !t || !footer) return;
54
+ const s = load();
55
+ const slot = (s[sessionId] ??= { at: 0, entries: [] });
56
+ slot.at = Date.now();
57
+ slot.entries.push({ t, f: footer });
58
+ if (slot.entries.length > MAX_PER_SESSION) slot.entries.splice(0, slot.entries.length - MAX_PER_SESSION);
59
+ persistSoon();
60
+ }
61
+
62
+ /** 给历史里的助手消息按顺序配上脚注。同样的正文(比如两次都回「好的」)按先后一一对应 */
63
+ export function attachFooters(sessionId: string, messages: HistoryMessage[]): HistoryMessage[] {
64
+ const entries = load()[sessionId]?.entries;
65
+ if (!entries?.length) return messages;
66
+ let next = 0;
67
+ return messages.map((m) => {
68
+ if (m.role !== "assistant" || next >= entries.length) return m;
69
+ if (!m.text.trim()) return m;
70
+ const found = entries.findIndex((e, i) => i >= next && endsWithTail(m.text, e.t));
71
+ if (found < 0) return m;
72
+ next = found + 1;
73
+ return { ...m, footer: entries[found]!.f };
74
+ });
75
+ }
76
+
77
+ /** 分叉出的新会话沿用原会话的脚注:它的记录是原会话的前一段,attachFooters 按顺序配得上 */
78
+ export function copyFooters(from: string, to: string): void {
79
+ const src = load()[from];
80
+ if (!src?.entries.length) return;
81
+ load()[to] = { at: Date.now(), entries: src.entries.map((e) => ({ ...e })) };
82
+ persistSoon();
83
+ }
84
+
85
+ /** 测试用 */
86
+ export function resetFootersForTest(): void {
87
+ store = null;
88
+ }