@juli-os/ledger 0.0.0-stage → 0.1.0
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/LICENSE +202 -0
- package/README.md +61 -2
- package/dist/contracts/entities.d.ts +64 -0
- package/dist/contracts/statuses.d.ts +34 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +1216 -0
- package/dist/juli-ledger.js +1281 -0
- package/dist/platform/ledger/casefile.d.ts +47 -0
- package/dist/platform/ledger/pricing.d.ts +57 -0
- package/dist/platform/ledger/store.d.ts +98 -0
- package/dist/platform/ledger/usage.d.ts +72 -0
- package/dist/platform/shared/clock.d.ts +9 -0
- package/dist/platform/shared/json.d.ts +10 -0
- package/package.json +39 -3
package/dist/index.js
ADDED
|
@@ -0,0 +1,1216 @@
|
|
|
1
|
+
// src/platform/ledger/store.ts
|
|
2
|
+
import { DatabaseSync } from "node:sqlite";
|
|
3
|
+
|
|
4
|
+
// src/platform/shared/json.ts
|
|
5
|
+
var safeParse = (raw) => {
|
|
6
|
+
if (raw === null || raw === void 0 || raw === "") return null;
|
|
7
|
+
try {
|
|
8
|
+
return JSON.parse(raw);
|
|
9
|
+
} catch {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
var asRecord = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : {};
|
|
14
|
+
|
|
15
|
+
// src/platform/shared/clock.ts
|
|
16
|
+
var systemClock = {
|
|
17
|
+
now: () => /* @__PURE__ */ new Date()
|
|
18
|
+
};
|
|
19
|
+
var hex = (n2) => Array.from({ length: n2 }, () => Math.floor(Math.random() * 16).toString(16)).join("");
|
|
20
|
+
var randomIds = {
|
|
21
|
+
newId: (prefix) => `${prefix}_${hex(12)}`
|
|
22
|
+
};
|
|
23
|
+
var rfc3339 = (d) => d.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
24
|
+
|
|
25
|
+
// src/contracts/entities.ts
|
|
26
|
+
var TERMINAL_WORKFLOW_STATUSES = [
|
|
27
|
+
"completed",
|
|
28
|
+
"failed",
|
|
29
|
+
"cancelled",
|
|
30
|
+
"rejected"
|
|
31
|
+
];
|
|
32
|
+
var AGENT_STEP_KINDS = ["agent", "verify"];
|
|
33
|
+
|
|
34
|
+
// src/contracts/statuses.ts
|
|
35
|
+
var WORKFLOW_STATUSES = [
|
|
36
|
+
{ key: "queued", label: "\u6392\u961F\u4E2D", group: "\u8FDB\u884C\u4E2D", terminal: false },
|
|
37
|
+
{ key: "running", label: "\u8FDB\u884C\u4E2D", group: "\u8FDB\u884C\u4E2D", terminal: false },
|
|
38
|
+
{ key: "completed", label: "\u5DF2\u5B8C\u6210", group: "\u7EC8\u6001", terminal: true },
|
|
39
|
+
{ key: "failed", label: "\u5931\u8D25", group: "\u7EC8\u6001", terminal: true },
|
|
40
|
+
{ key: "cancelled", label: "\u5DF2\u53D6\u6D88", group: "\u7EC8\u6001", terminal: true },
|
|
41
|
+
{ key: "rejected", label: "\u5DF2\u62D2\u7EDD", group: "\u7EC8\u6001", terminal: true }
|
|
42
|
+
];
|
|
43
|
+
var STEP_STATUSES = [
|
|
44
|
+
{ key: "pending", label: "\u5F85\u6392", group: "\u8FDB\u884C\u4E2D", terminal: false },
|
|
45
|
+
{ key: "running", label: "\u8FDB\u884C\u4E2D", group: "\u8FDB\u884C\u4E2D", terminal: false },
|
|
46
|
+
{ key: "waiting_human", label: "\u7B49\u4F60\u51B3\u5B9A", group: "\u7B49\u51B3\u5B9A", terminal: false },
|
|
47
|
+
{ key: "completed", label: "\u5DF2\u5B8C\u6210", group: "\u7EC8\u6001", terminal: true },
|
|
48
|
+
{ key: "failed", label: "\u5931\u8D25", group: "\u7EC8\u6001", terminal: true },
|
|
49
|
+
{ key: "cancelled", label: "\u5DF2\u53D6\u6D88", group: "\u7EC8\u6001", terminal: true },
|
|
50
|
+
{ key: "skipped", label: "\u5DF2\u8DF3\u8FC7", group: "\u7EC8\u6001", terminal: true }
|
|
51
|
+
];
|
|
52
|
+
var ACTIVE_STEP_STATUSES = STEP_STATUSES.filter((s) => !s.terminal).map((s) => s.key);
|
|
53
|
+
var ACTIVE_WORKFLOW_STATUSES = WORKFLOW_STATUSES.filter((s) => !s.terminal).map((s) => s.key);
|
|
54
|
+
var SETTLED_STEP_STATUSES = STEP_STATUSES.filter((s) => s.key === "completed" || s.key === "cancelled").map((s) => s.key);
|
|
55
|
+
var SETTLED_WORKFLOW_STATUSES = WORKFLOW_STATUSES.filter((s) => s.key === "completed" || s.key === "cancelled").map((s) => s.key);
|
|
56
|
+
var TERMINAL_STEP_STATUSES = STEP_STATUSES.filter((s) => s.terminal).map((s) => s.key);
|
|
57
|
+
var DEDUP_WINDOW_STATUSES = ["queued", "running", "waiting_human"];
|
|
58
|
+
var PRUNE_WORKFLOW_STATUSES = ["completed", "cancelled", "rejected"];
|
|
59
|
+
var sqlInList = (keys) => keys.map((k) => `'${k}'`).join(",");
|
|
60
|
+
|
|
61
|
+
// src/platform/ledger/casefile.ts
|
|
62
|
+
var emptyCaseFile = (now) => ({
|
|
63
|
+
original: {},
|
|
64
|
+
extract: null,
|
|
65
|
+
executions: [],
|
|
66
|
+
draft: null,
|
|
67
|
+
decisions: [],
|
|
68
|
+
references: [],
|
|
69
|
+
artifacts: [],
|
|
70
|
+
updatedAt: rfc3339(now)
|
|
71
|
+
});
|
|
72
|
+
var pick = (vals, guard) => {
|
|
73
|
+
const out = [];
|
|
74
|
+
for (const v of vals) {
|
|
75
|
+
if (typeof v === "object" && v !== null && guard(v)) out.push(v);
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
};
|
|
79
|
+
var parseCaseFile = (raw, now) => {
|
|
80
|
+
const base = emptyCaseFile(now);
|
|
81
|
+
const v = safeParse(raw);
|
|
82
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) return base;
|
|
83
|
+
const r = v;
|
|
84
|
+
const str2 = (k) => {
|
|
85
|
+
const x = r[k];
|
|
86
|
+
return typeof x === "string" ? x : "";
|
|
87
|
+
};
|
|
88
|
+
const arr = (k) => {
|
|
89
|
+
const x = r[k];
|
|
90
|
+
return Array.isArray(x) ? x : [];
|
|
91
|
+
};
|
|
92
|
+
return {
|
|
93
|
+
original: Object.fromEntries(
|
|
94
|
+
Object.entries(asRecord(r.original)).filter(([, x]) => typeof x === "string")
|
|
95
|
+
),
|
|
96
|
+
extract: r.extract ?? null,
|
|
97
|
+
executions: arr("executions"),
|
|
98
|
+
draft: r.draft ?? null,
|
|
99
|
+
decisions: pick(arr("decisions"), (d) => "action" in d),
|
|
100
|
+
references: arr("references").filter((x) => typeof x === "string"),
|
|
101
|
+
artifacts: pick(arr("artifacts"), (a) => "id" in a),
|
|
102
|
+
updatedAt: str2("updated_at") || base.updatedAt
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
var serializeCaseFile = (cf) => JSON.stringify(cf);
|
|
106
|
+
|
|
107
|
+
// src/platform/ledger/store.ts
|
|
108
|
+
var MIGRATIONS = `
|
|
109
|
+
CREATE TABLE IF NOT EXISTS workflows (
|
|
110
|
+
id TEXT PRIMARY KEY,
|
|
111
|
+
parent_id TEXT,
|
|
112
|
+
kind TEXT NOT NULL DEFAULT 'adhoc',
|
|
113
|
+
title TEXT NOT NULL DEFAULT '',
|
|
114
|
+
status TEXT NOT NULL DEFAULT 'running',
|
|
115
|
+
session TEXT,
|
|
116
|
+
project TEXT,
|
|
117
|
+
path TEXT,
|
|
118
|
+
meta TEXT,
|
|
119
|
+
case_file TEXT,
|
|
120
|
+
created_at TEXT NOT NULL,
|
|
121
|
+
updated_at TEXT NOT NULL
|
|
122
|
+
);
|
|
123
|
+
CREATE TABLE IF NOT EXISTS workflow_steps (
|
|
124
|
+
id TEXT PRIMARY KEY,
|
|
125
|
+
workflow_id TEXT NOT NULL,
|
|
126
|
+
seq INTEGER NOT NULL,
|
|
127
|
+
kind TEXT NOT NULL,
|
|
128
|
+
title TEXT NOT NULL DEFAULT '',
|
|
129
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
130
|
+
input TEXT,
|
|
131
|
+
output TEXT,
|
|
132
|
+
summary TEXT,
|
|
133
|
+
updated_at TEXT NOT NULL,
|
|
134
|
+
started_at TEXT,
|
|
135
|
+
completed_at TEXT,
|
|
136
|
+
UNIQUE(workflow_id, seq)
|
|
137
|
+
);
|
|
138
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
139
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
140
|
+
entity_type TEXT NOT NULL,
|
|
141
|
+
entity_id TEXT NOT NULL,
|
|
142
|
+
type TEXT NOT NULL,
|
|
143
|
+
payload TEXT,
|
|
144
|
+
ts TEXT NOT NULL
|
|
145
|
+
);
|
|
146
|
+
CREATE TABLE IF NOT EXISTS runs (
|
|
147
|
+
id TEXT PRIMARY KEY,
|
|
148
|
+
workflow_id TEXT NOT NULL,
|
|
149
|
+
session TEXT NOT NULL,
|
|
150
|
+
status TEXT NOT NULL DEFAULT 'running',
|
|
151
|
+
created_at TEXT NOT NULL
|
|
152
|
+
);
|
|
153
|
+
CREATE TABLE IF NOT EXISTS prompts (
|
|
154
|
+
id TEXT PRIMARY KEY,
|
|
155
|
+
run_id TEXT NOT NULL,
|
|
156
|
+
content TEXT NOT NULL DEFAULT '',
|
|
157
|
+
source TEXT NOT NULL DEFAULT '',
|
|
158
|
+
status TEXT NOT NULL DEFAULT 'submitted',
|
|
159
|
+
created_at TEXT NOT NULL
|
|
160
|
+
);
|
|
161
|
+
CREATE INDEX IF NOT EXISTS idx_steps_wf ON workflow_steps(workflow_id, seq);
|
|
162
|
+
CREATE TABLE IF NOT EXISTS relays (
|
|
163
|
+
id TEXT PRIMARY KEY,
|
|
164
|
+
workflow_id TEXT NOT NULL,
|
|
165
|
+
step_id TEXT NOT NULL,
|
|
166
|
+
session TEXT NOT NULL,
|
|
167
|
+
text TEXT NOT NULL,
|
|
168
|
+
marker TEXT NOT NULL,
|
|
169
|
+
created_at TEXT NOT NULL,
|
|
170
|
+
delivered_at TEXT,
|
|
171
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
172
|
+
last_error TEXT
|
|
173
|
+
);
|
|
174
|
+
CREATE INDEX IF NOT EXISTS idx_events_entity ON events(entity_type, entity_id);
|
|
175
|
+
CREATE INDEX IF NOT EXISTS idx_runs_session ON runs(session, status);
|
|
176
|
+
`;
|
|
177
|
+
var str = (v) => typeof v === "string" ? v : v === null || v === void 0 ? "" : String(v);
|
|
178
|
+
var num = (v) => typeof v === "number" ? v : Number(v ?? 0);
|
|
179
|
+
var dedupPattern = (key) => {
|
|
180
|
+
const jsonValue = JSON.stringify(key).slice(1, -1);
|
|
181
|
+
return `%"dedup_key":"${jsonValue.replace(/[%_\\]/g, (c) => `\\${c}`)}"%`;
|
|
182
|
+
};
|
|
183
|
+
var SEND_STEP_LIKE_CUTOFF = "2026-09-19";
|
|
184
|
+
var rowToWorkflow = (r) => ({
|
|
185
|
+
id: str(r["id"]),
|
|
186
|
+
parentId: r["parent_id"] === null ? null : str(r["parent_id"]),
|
|
187
|
+
kind: str(r["kind"]),
|
|
188
|
+
title: str(r["title"]),
|
|
189
|
+
status: str(r["status"]),
|
|
190
|
+
session: str(r["session"]),
|
|
191
|
+
project: str(r["project"]),
|
|
192
|
+
path: str(r["path"]),
|
|
193
|
+
threadId: r["thread_id"] === null || r["thread_id"] === void 0 ? null : str(r["thread_id"]),
|
|
194
|
+
meta: asRecord(safeParse(str(r["meta"]))),
|
|
195
|
+
createdAt: str(r["created_at"]),
|
|
196
|
+
updatedAt: str(r["updated_at"])
|
|
197
|
+
});
|
|
198
|
+
var rowToStep = (r) => ({
|
|
199
|
+
id: str(r["id"]),
|
|
200
|
+
workflowId: str(r["workflow_id"]),
|
|
201
|
+
seq: num(r["seq"]),
|
|
202
|
+
kind: str(r["kind"]),
|
|
203
|
+
title: str(r["title"]),
|
|
204
|
+
status: str(r["status"]),
|
|
205
|
+
input: asRecord(safeParse(str(r["input"]))),
|
|
206
|
+
output: asRecord(safeParse(str(r["output"]))),
|
|
207
|
+
summary: str(r["summary"]),
|
|
208
|
+
updatedAt: str(r["updated_at"]),
|
|
209
|
+
startedAt: str(r["started_at"] ?? ""),
|
|
210
|
+
completedAt: r["completed_at"] === null || r["completed_at"] === void 0 ? null : str(r["completed_at"])
|
|
211
|
+
});
|
|
212
|
+
var rowToEvent = (r) => ({
|
|
213
|
+
id: num(r["id"]),
|
|
214
|
+
entityType: str(r["entity_type"]),
|
|
215
|
+
entityId: str(r["entity_id"]),
|
|
216
|
+
type: str(r["type"]),
|
|
217
|
+
payload: asRecord(safeParse(str(r["payload"]))),
|
|
218
|
+
ts: str(r["ts"])
|
|
219
|
+
});
|
|
220
|
+
var pruneImpl = (db, retentionDays, nowIso) => {
|
|
221
|
+
if (retentionDays <= 0) return { events: 0, workflows: 0 };
|
|
222
|
+
const cutoff = new Date(new Date(nowIso).getTime() - retentionDays * 864e5).toISOString();
|
|
223
|
+
const ev = db.prepare("DELETE FROM events WHERE ts < ?").run(cutoff);
|
|
224
|
+
const wfRows = db.prepare(
|
|
225
|
+
`SELECT id FROM workflows WHERE status IN (${sqlInList(PRUNE_WORKFLOW_STATUSES)}) AND updated_at < ?`
|
|
226
|
+
).all(cutoff);
|
|
227
|
+
let wfCount = 0;
|
|
228
|
+
for (const r of wfRows) {
|
|
229
|
+
db.prepare("DELETE FROM workflow_steps WHERE workflow_id=?").run(r.id);
|
|
230
|
+
db.prepare("DELETE FROM workflows WHERE id=?").run(r.id);
|
|
231
|
+
wfCount++;
|
|
232
|
+
}
|
|
233
|
+
return { events: Number(ev.changes), workflows: wfCount };
|
|
234
|
+
};
|
|
235
|
+
var STEP_COLS = `id, workflow_id, seq, kind, title, status, COALESCE(input,'') AS input,
|
|
236
|
+
COALESCE(output,'') AS output, COALESCE(summary,'') AS summary, updated_at,
|
|
237
|
+
started_at, completed_at`;
|
|
238
|
+
var createLedgerStore = (dbPath, deps = {}) => {
|
|
239
|
+
const clock = deps.clock ?? systemClock;
|
|
240
|
+
const ids = deps.ids ?? randomIds;
|
|
241
|
+
const db = new DatabaseSync(dbPath);
|
|
242
|
+
db.exec(MIGRATIONS);
|
|
243
|
+
const wfCols = db.prepare("PRAGMA table_info(workflows)").all().map((c) => c.name);
|
|
244
|
+
if (!wfCols.includes("owner")) {
|
|
245
|
+
db.exec("ALTER TABLE workflows ADD COLUMN owner TEXT");
|
|
246
|
+
}
|
|
247
|
+
if (!wfCols.includes("thread_id")) {
|
|
248
|
+
db.exec("ALTER TABLE workflows ADD COLUMN thread_id TEXT");
|
|
249
|
+
}
|
|
250
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_workflows_thread ON workflows(thread_id)");
|
|
251
|
+
const stepCols = db.prepare("PRAGMA table_info(workflow_steps)").all().map((c) => c.name);
|
|
252
|
+
if (!stepCols.includes("started_at")) db.exec("ALTER TABLE workflow_steps ADD COLUMN started_at TEXT");
|
|
253
|
+
if (!stepCols.includes("completed_at")) db.exec("ALTER TABLE workflow_steps ADD COLUMN completed_at TEXT");
|
|
254
|
+
const now = () => rfc3339(clock.now());
|
|
255
|
+
const mergeStepJson = (id, col, patch) => {
|
|
256
|
+
const row = db.prepare(`SELECT COALESCE(${col},'{}') AS v FROM workflow_steps WHERE id=?`).get(id);
|
|
257
|
+
const cur = asRecord(safeParse(str(row?.["v"])));
|
|
258
|
+
const merged = { ...cur, ...patch };
|
|
259
|
+
db.prepare(`UPDATE workflow_steps SET ${col}=?, updated_at=? WHERE id=?`).run(JSON.stringify(merged), now(), id);
|
|
260
|
+
};
|
|
261
|
+
const getWorkflowById = (id) => {
|
|
262
|
+
const r = db.prepare("SELECT * FROM workflows WHERE id=?").get(id);
|
|
263
|
+
return r ? rowToWorkflow(r) : null;
|
|
264
|
+
};
|
|
265
|
+
const getStepById = (id) => {
|
|
266
|
+
const r = db.prepare(`SELECT ${STEP_COLS} FROM workflow_steps WHERE id=?`).get(id);
|
|
267
|
+
return r ? rowToStep(r) : null;
|
|
268
|
+
};
|
|
269
|
+
const activeRunOf = (session) => {
|
|
270
|
+
const r = db.prepare(
|
|
271
|
+
`SELECT r.id AS id, r.workflow_id AS workflow_id, r.session AS session,
|
|
272
|
+
r.status AS status, r.created_at AS created_at FROM runs r
|
|
273
|
+
JOIN workflows w ON w.id = r.workflow_id
|
|
274
|
+
WHERE r.session=? AND r.status IN ('running','idle') AND w.status='running'
|
|
275
|
+
ORDER BY r.created_at DESC LIMIT 1`
|
|
276
|
+
).get(session);
|
|
277
|
+
if (!r) return null;
|
|
278
|
+
return {
|
|
279
|
+
id: str(r["id"]),
|
|
280
|
+
workflowId: str(r["workflow_id"]),
|
|
281
|
+
session: str(r["session"]),
|
|
282
|
+
status: "running",
|
|
283
|
+
createdAt: str(r["created_at"])
|
|
284
|
+
};
|
|
285
|
+
};
|
|
286
|
+
return {
|
|
287
|
+
db,
|
|
288
|
+
createWorkflow(w) {
|
|
289
|
+
const id = ids.newId("wf");
|
|
290
|
+
const ts = now();
|
|
291
|
+
db.prepare(
|
|
292
|
+
`INSERT INTO workflows (id, parent_id, kind, title, status, owner, meta, created_at, updated_at)
|
|
293
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
294
|
+
).run(id, w.parentId ?? null, w.kind, w.title, w.status ?? "running", w.owner ?? null, JSON.stringify(w.meta ?? {}), ts, ts);
|
|
295
|
+
return getWorkflowById(id);
|
|
296
|
+
},
|
|
297
|
+
getWorkflow(id) {
|
|
298
|
+
const r = db.prepare("SELECT * FROM workflows WHERE id=?").get(id);
|
|
299
|
+
return r ? rowToWorkflow(r) : null;
|
|
300
|
+
},
|
|
301
|
+
listWorkflows(limit = 100, status) {
|
|
302
|
+
if (status !== void 0 && status !== "") {
|
|
303
|
+
return db.prepare("SELECT * FROM workflows WHERE status=? ORDER BY created_at DESC, rowid DESC LIMIT ?").all(status, limit).map(rowToWorkflow);
|
|
304
|
+
}
|
|
305
|
+
return db.prepare("SELECT * FROM workflows ORDER BY created_at DESC, rowid DESC LIMIT ?").all(limit).map(rowToWorkflow);
|
|
306
|
+
},
|
|
307
|
+
setWorkflowSession(id, session) {
|
|
308
|
+
db.prepare("UPDATE workflows SET session=?, updated_at=? WHERE id=?").run(session, now(), id);
|
|
309
|
+
},
|
|
310
|
+
setWorkflowStatus(id, status) {
|
|
311
|
+
db.prepare("UPDATE workflows SET status=?, updated_at=? WHERE id=?").run(status, now(), id);
|
|
312
|
+
if (TERMINAL_WORKFLOW_STATUSES.includes(status)) {
|
|
313
|
+
const runStatus = status === "failed" ? "failed" : "completed";
|
|
314
|
+
db.prepare(`UPDATE runs SET status=? WHERE workflow_id=? AND status IN ('running','idle')`).run(runStatus, id);
|
|
315
|
+
}
|
|
316
|
+
},
|
|
317
|
+
updateWorkflowMeta(id, patch) {
|
|
318
|
+
const row = db.prepare(`SELECT COALESCE(meta,'{}') AS v FROM workflows WHERE id=?`).get(id);
|
|
319
|
+
const cur = asRecord(safeParse(str(row?.["v"])));
|
|
320
|
+
const merged = { ...cur };
|
|
321
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
322
|
+
if (v === null) delete merged[k];
|
|
323
|
+
else merged[k] = v;
|
|
324
|
+
}
|
|
325
|
+
db.prepare("UPDATE workflows SET meta=?, updated_at=? WHERE id=?").run(JSON.stringify(merged), now(), id);
|
|
326
|
+
return merged;
|
|
327
|
+
},
|
|
328
|
+
setWorkflowRouting(id, session, project) {
|
|
329
|
+
db.prepare("UPDATE workflows SET session=?, project=?, updated_at=? WHERE id=?").run(session || null, project || null, now(), id);
|
|
330
|
+
},
|
|
331
|
+
assignThread(workflowId, threadId) {
|
|
332
|
+
if (threadId === "") return;
|
|
333
|
+
db.prepare("UPDATE workflows SET thread_id=?, updated_at=? WHERE id=?").run(threadId, now(), workflowId);
|
|
334
|
+
},
|
|
335
|
+
latestByMessageId(messageId, excludeWorkflowId) {
|
|
336
|
+
if (messageId === "") return null;
|
|
337
|
+
const r = db.prepare(
|
|
338
|
+
`SELECT * FROM workflows WHERE json_extract(meta,'$.message_id')=?
|
|
339
|
+
AND (? IS NULL OR id != ?)
|
|
340
|
+
ORDER BY created_at DESC, rowid DESC LIMIT 1`
|
|
341
|
+
).get(messageId, excludeWorkflowId ?? null, excludeWorkflowId ?? "");
|
|
342
|
+
return r ? rowToWorkflow(r) : null;
|
|
343
|
+
},
|
|
344
|
+
recentBySender(from, sinceIso, excludeWorkflowId, limit = 200) {
|
|
345
|
+
return db.prepare(
|
|
346
|
+
`SELECT * FROM workflows WHERE status != 'rejected'
|
|
347
|
+
AND lower(COALESCE(json_extract(meta,'$.from'),'')) = lower(?)
|
|
348
|
+
AND created_at >= ?
|
|
349
|
+
AND (? IS NULL OR id != ?)
|
|
350
|
+
ORDER BY created_at DESC, rowid DESC LIMIT ?`
|
|
351
|
+
).all(from, sinceIso, excludeWorkflowId ?? null, excludeWorkflowId ?? "", limit).map(rowToWorkflow);
|
|
352
|
+
},
|
|
353
|
+
loadCaseFile(id) {
|
|
354
|
+
const row = db.prepare("SELECT COALESCE(case_file,'') AS v FROM workflows WHERE id=?").get(id);
|
|
355
|
+
return parseCaseFile(str(row?.["v"]) || null, clock.now());
|
|
356
|
+
},
|
|
357
|
+
saveCaseFile(id, cf) {
|
|
358
|
+
db.prepare("UPDATE workflows SET case_file=?, updated_at=? WHERE id=?").run(serializeCaseFile(cf), now(), id);
|
|
359
|
+
},
|
|
360
|
+
createStep(s) {
|
|
361
|
+
const id = ids.newId("st");
|
|
362
|
+
db.prepare(
|
|
363
|
+
`INSERT INTO workflow_steps (id, workflow_id, seq, kind, title, status, input, updated_at)
|
|
364
|
+
VALUES (?, ?, ?, ?, ?, 'pending', ?, ?)`
|
|
365
|
+
).run(id, s.workflowId, s.seq, s.kind, s.title, JSON.stringify(s.input ?? {}), now());
|
|
366
|
+
return getStepById(id);
|
|
367
|
+
},
|
|
368
|
+
insertStepsAt(workflowId, afterSeq, steps) {
|
|
369
|
+
if (steps.length === 0) return;
|
|
370
|
+
const insert = db.prepare(
|
|
371
|
+
`INSERT INTO workflow_steps (id, workflow_id, seq, kind, title, status, input, updated_at)
|
|
372
|
+
VALUES (?, ?, ?, ?, ?, 'pending', ?, ?)`
|
|
373
|
+
);
|
|
374
|
+
const shift = db.prepare(
|
|
375
|
+
"UPDATE workflow_steps SET seq = seq + ? WHERE workflow_id = ? AND seq >= ?"
|
|
376
|
+
);
|
|
377
|
+
db.exec("BEGIN");
|
|
378
|
+
try {
|
|
379
|
+
shift.run(steps.length, workflowId, afterSeq + 1);
|
|
380
|
+
steps.forEach((st, i) => {
|
|
381
|
+
insert.run(
|
|
382
|
+
ids.newId("st"),
|
|
383
|
+
workflowId,
|
|
384
|
+
afterSeq + 1 + i,
|
|
385
|
+
st.kind,
|
|
386
|
+
st.title,
|
|
387
|
+
JSON.stringify(st.input ?? {}),
|
|
388
|
+
now()
|
|
389
|
+
);
|
|
390
|
+
});
|
|
391
|
+
db.exec("COMMIT");
|
|
392
|
+
} catch (e) {
|
|
393
|
+
db.exec("ROLLBACK");
|
|
394
|
+
throw e;
|
|
395
|
+
}
|
|
396
|
+
},
|
|
397
|
+
getStep(id) {
|
|
398
|
+
const r = db.prepare(`SELECT ${STEP_COLS} FROM workflow_steps WHERE id=?`).get(id);
|
|
399
|
+
return r ? rowToStep(r) : null;
|
|
400
|
+
},
|
|
401
|
+
stepsForWorkflow(id) {
|
|
402
|
+
return db.prepare(`SELECT ${STEP_COLS} FROM workflow_steps WHERE workflow_id=? ORDER BY seq`).all(id).map(rowToStep);
|
|
403
|
+
},
|
|
404
|
+
currentStep(id) {
|
|
405
|
+
const r = db.prepare(
|
|
406
|
+
`SELECT ${STEP_COLS} FROM workflow_steps
|
|
407
|
+
WHERE workflow_id=? AND status IN (${sqlInList(ACTIVE_STEP_STATUSES)}) ORDER BY seq LIMIT 1`
|
|
408
|
+
).get(id);
|
|
409
|
+
return r ? rowToStep(r) : null;
|
|
410
|
+
},
|
|
411
|
+
setStepStatus(id, status) {
|
|
412
|
+
db.prepare(`UPDATE workflow_steps SET status=?, updated_at=?,
|
|
413
|
+
started_at=COALESCE(started_at, CASE WHEN ? NOT IN ('pending') THEN ? END),
|
|
414
|
+
completed_at=CASE WHEN ? IN (${sqlInList(TERMINAL_STEP_STATUSES)})
|
|
415
|
+
THEN COALESCE(completed_at, ?) ELSE completed_at END
|
|
416
|
+
WHERE id=?`).run(status, now(), status, now(), status, now(), id);
|
|
417
|
+
},
|
|
418
|
+
setStepInput(id, patch) {
|
|
419
|
+
mergeStepJson(id, "input", patch);
|
|
420
|
+
},
|
|
421
|
+
insertRelay(r) {
|
|
422
|
+
const id = ids.newId("relay");
|
|
423
|
+
db.prepare(`INSERT INTO relays (id, workflow_id, step_id, session, text, marker, created_at)
|
|
424
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`).run(id, r.workflowId, r.stepId, r.session, r.text, r.marker, now());
|
|
425
|
+
return id;
|
|
426
|
+
},
|
|
427
|
+
pendingRelays() {
|
|
428
|
+
return db.prepare(`SELECT id, workflow_id, step_id, session, text, marker, created_at, attempts,
|
|
429
|
+
COALESCE(last_error,'') AS last_error FROM relays WHERE delivered_at IS NULL ORDER BY created_at`).all().map((r) => ({
|
|
430
|
+
id: str(r["id"]),
|
|
431
|
+
workflowId: str(r["workflow_id"]),
|
|
432
|
+
stepId: str(r["step_id"]),
|
|
433
|
+
session: str(r["session"]),
|
|
434
|
+
text: str(r["text"]),
|
|
435
|
+
marker: str(r["marker"]),
|
|
436
|
+
createdAt: str(r["created_at"]),
|
|
437
|
+
attempts: num(r["attempts"]),
|
|
438
|
+
lastError: str(r["last_error"])
|
|
439
|
+
}));
|
|
440
|
+
},
|
|
441
|
+
markRelayDelivered(id) {
|
|
442
|
+
db.prepare("UPDATE relays SET delivered_at=? WHERE id=?").run(now(), id);
|
|
443
|
+
},
|
|
444
|
+
markRelayAttempt(id, error) {
|
|
445
|
+
db.prepare("UPDATE relays SET attempts=attempts+1, last_error=? WHERE id=?").run(error.slice(0, 300), id);
|
|
446
|
+
},
|
|
447
|
+
setStepOutput(id, patch) {
|
|
448
|
+
mergeStepJson(id, "output", patch);
|
|
449
|
+
},
|
|
450
|
+
appendStepOutputItem(id, key, entry, cap) {
|
|
451
|
+
const row = db.prepare(`SELECT COALESCE(output,'{}') AS v FROM workflow_steps WHERE id=?`).get(id);
|
|
452
|
+
const cur = asRecord(safeParse(str(row?.["v"])));
|
|
453
|
+
const arr = Array.isArray(cur[key]) ? [...cur[key], entry] : [entry];
|
|
454
|
+
const next = cap > 0 && arr.length > cap ? arr.slice(arr.length - cap) : arr;
|
|
455
|
+
db.prepare("UPDATE workflow_steps SET output=?, updated_at=? WHERE id=?").run(JSON.stringify({ ...cur, [key]: next }), now(), id);
|
|
456
|
+
},
|
|
457
|
+
claimRunningStep(id) {
|
|
458
|
+
const res = db.prepare(
|
|
459
|
+
`UPDATE workflow_steps SET status='completed', updated_at=?,
|
|
460
|
+
completed_at=COALESCE(completed_at, ?) WHERE id=? AND status='running'`
|
|
461
|
+
).run(now(), now(), id);
|
|
462
|
+
return Number(res.changes) === 1;
|
|
463
|
+
},
|
|
464
|
+
listSteps(status, limit = 100) {
|
|
465
|
+
return db.prepare(
|
|
466
|
+
`SELECT ${STEP_COLS} FROM workflow_steps WHERE status=? ORDER BY updated_at DESC LIMIT ?`
|
|
467
|
+
).all(status, limit).map(rowToStep);
|
|
468
|
+
},
|
|
469
|
+
runningAgentStepForSession(session) {
|
|
470
|
+
const r = db.prepare(
|
|
471
|
+
`SELECT st.id AS id, st.workflow_id AS workflow_id, st.seq AS seq, st.kind AS kind,
|
|
472
|
+
COALESCE(st.title,'') AS title, st.status AS status,
|
|
473
|
+
COALESCE(st.input,'') AS input, COALESCE(st.output,'') AS output,
|
|
474
|
+
COALESCE(st.summary,'') AS summary, st.updated_at AS updated_at
|
|
475
|
+
FROM workflow_steps st
|
|
476
|
+
JOIN workflows w ON w.id = st.workflow_id
|
|
477
|
+
JOIN runs r ON r.workflow_id = st.workflow_id
|
|
478
|
+
WHERE st.status='running' AND st.kind IN (${sqlInList(AGENT_STEP_KINDS)}) AND r.session=? AND w.status='running'
|
|
479
|
+
ORDER BY st.updated_at DESC LIMIT 1`
|
|
480
|
+
).get(session);
|
|
481
|
+
if (r) return rowToStep(r);
|
|
482
|
+
const r2 = db.prepare(
|
|
483
|
+
`SELECT st.id AS id, st.workflow_id AS workflow_id, st.seq AS seq, st.kind AS kind,
|
|
484
|
+
COALESCE(st.title,'') AS title, st.status AS status,
|
|
485
|
+
COALESCE(st.input,'') AS input, COALESCE(st.output,'') AS output,
|
|
486
|
+
COALESCE(st.summary,'') AS summary, st.updated_at AS updated_at
|
|
487
|
+
FROM workflow_steps st
|
|
488
|
+
JOIN workflows w ON w.id = st.workflow_id
|
|
489
|
+
WHERE st.status='running' AND st.kind IN (${sqlInList(AGENT_STEP_KINDS)}) AND w.status='running'
|
|
490
|
+
AND (w.session=? OR COALESCE(json_extract(w.meta,'$.assigned_session'),'')=?)
|
|
491
|
+
ORDER BY st.updated_at DESC LIMIT 1`
|
|
492
|
+
).get(session, session);
|
|
493
|
+
return r2 ? rowToStep(r2) : null;
|
|
494
|
+
},
|
|
495
|
+
sessionHasOpenWork(session, excludeWorkflowId) {
|
|
496
|
+
const anchor = "(session=?1 OR COALESCE(json_extract(meta,'$.assigned_session'),'')=?1 OR id IN (SELECT workflow_id FROM runs WHERE session=?1))";
|
|
497
|
+
const sql = excludeWorkflowId !== void 0 && excludeWorkflowId !== "" ? `SELECT COUNT(*) AS n FROM workflows WHERE status IN ('queued','running') AND ${anchor} AND id != ?2` : `SELECT COUNT(*) AS n FROM workflows WHERE status IN ('queued','running') AND ${anchor}`;
|
|
498
|
+
const row = excludeWorkflowId !== void 0 && excludeWorkflowId !== "" ? db.prepare(sql).get(session, excludeWorkflowId) : db.prepare(sql).get(session);
|
|
499
|
+
return Number(row["n"]) > 0;
|
|
500
|
+
},
|
|
501
|
+
appendEvent(entityType, entityId, type, payload = {}) {
|
|
502
|
+
const ts = now();
|
|
503
|
+
const res = db.prepare(
|
|
504
|
+
"INSERT INTO events (entity_type, entity_id, type, payload, ts) VALUES (?, ?, ?, ?, ?)"
|
|
505
|
+
).run(entityType, entityId, type, JSON.stringify(payload), ts);
|
|
506
|
+
return {
|
|
507
|
+
id: Number(res.lastInsertRowid),
|
|
508
|
+
entityType,
|
|
509
|
+
entityId,
|
|
510
|
+
type,
|
|
511
|
+
payload,
|
|
512
|
+
ts
|
|
513
|
+
};
|
|
514
|
+
},
|
|
515
|
+
listEvents(limit = 200, types) {
|
|
516
|
+
if (types !== void 0 && types.length > 0) {
|
|
517
|
+
const ph = types.map(() => "?").join(",");
|
|
518
|
+
return db.prepare(`SELECT * FROM events WHERE type IN (${ph}) ORDER BY id DESC LIMIT ?`).all(...types, limit).map(rowToEvent);
|
|
519
|
+
}
|
|
520
|
+
return db.prepare("SELECT * FROM events ORDER BY id DESC LIMIT ?").all(limit).map(rowToEvent);
|
|
521
|
+
},
|
|
522
|
+
eventsSince(afterId, limit = 200) {
|
|
523
|
+
return db.prepare("SELECT * FROM events WHERE id>? ORDER BY id ASC LIMIT ?").all(afterId, limit).map(rowToEvent);
|
|
524
|
+
},
|
|
525
|
+
entityEvents(entityType, entityId, type, limit = 500) {
|
|
526
|
+
return db.prepare(
|
|
527
|
+
"SELECT * FROM events WHERE entity_type=? AND entity_id=? AND type=? ORDER BY id ASC LIMIT ?"
|
|
528
|
+
).all(entityType, entityId, type, limit).map(rowToEvent);
|
|
529
|
+
},
|
|
530
|
+
lastEventAt(entityType, entityId) {
|
|
531
|
+
const r = db.prepare(
|
|
532
|
+
"SELECT MAX(ts) AS m FROM events WHERE entity_type=? AND entity_id=?"
|
|
533
|
+
).get(entityType, entityId);
|
|
534
|
+
const m = r?.["m"];
|
|
535
|
+
return m === null || m === void 0 ? "" : String(m);
|
|
536
|
+
},
|
|
537
|
+
createRun(workflowId, session) {
|
|
538
|
+
const id = ids.newId("run");
|
|
539
|
+
db.prepare(`INSERT INTO runs (id, workflow_id, session, status, created_at)
|
|
540
|
+
VALUES (?, ?, ?, 'running', ?)`).run(id, workflowId, session, now());
|
|
541
|
+
return {
|
|
542
|
+
id,
|
|
543
|
+
workflowId,
|
|
544
|
+
session,
|
|
545
|
+
status: "running",
|
|
546
|
+
createdAt: now()
|
|
547
|
+
};
|
|
548
|
+
},
|
|
549
|
+
activeRunForSession: (session) => activeRunOf(session),
|
|
550
|
+
/** 活跃去重:同 dedup_key 且工作流仍在途 → 跳过重复建流。
|
|
551
|
+
* ⚠ ESCAPE '\' 必须与转义成对:message-id 几乎必含 `_`,漏写 ESCAPE 时
|
|
552
|
+
* `\_` 成了"字面反斜杠+任意字符",模式永不匹配——去重整体失效(09-07 事故)。 */
|
|
553
|
+
hasActiveDedup(key) {
|
|
554
|
+
const r = db.prepare(
|
|
555
|
+
`SELECT 1 FROM workflows WHERE status IN (${sqlInList(DEDUP_WINDOW_STATUSES)})
|
|
556
|
+
AND meta LIKE ? ESCAPE '\\' LIMIT 1`
|
|
557
|
+
).get(dedupPattern(key));
|
|
558
|
+
return r !== void 0;
|
|
559
|
+
},
|
|
560
|
+
/** 已摄取去重(轮询层):同 dedup_key 存在任何非 rejected 工作流 → 这封
|
|
561
|
+
* 邮件已被处理过(含 failed),轮询不再重复拉取。重试走步骤 retry,不走再摄取。 */
|
|
562
|
+
hasSeenDedup(key) {
|
|
563
|
+
const r = db.prepare(
|
|
564
|
+
`SELECT 1 FROM workflows WHERE status != 'rejected'
|
|
565
|
+
AND meta LIKE ? ESCAPE '\\' LIMIT 1`
|
|
566
|
+
).get(dedupPattern(key));
|
|
567
|
+
return r !== void 0;
|
|
568
|
+
},
|
|
569
|
+
/** 自回环守卫查询(2026-09-20 断点十三):message_id 是否命中本引擎已
|
|
570
|
+
* 发送记录——completed send 步的 output.message_id(email_sent 事件同源
|
|
571
|
+
* 落账)。进件层据此丢弃投递副本回灌(引擎给轮询邮箱自己发信的 echo)。
|
|
572
|
+
* LIKE 预筛 + 精确比对:JSON 编码取实际存储字节,%/_/\ 照 dedupPattern
|
|
573
|
+
* 成对转义(message-id 几乎必含 `_`)。 */
|
|
574
|
+
recordSentMessageId(messageId, meta) {
|
|
575
|
+
if (messageId === "") return;
|
|
576
|
+
db.prepare(
|
|
577
|
+
`INSERT INTO events (entity_type, entity_id, type, payload, ts) VALUES ('email', ?, 'email_sent', ?, ?)`
|
|
578
|
+
).run(messageId, JSON.stringify(meta ?? {}), now());
|
|
579
|
+
},
|
|
580
|
+
sentMessageIdExists(messageId) {
|
|
581
|
+
if (messageId === "") return false;
|
|
582
|
+
const registered = db.prepare(
|
|
583
|
+
`SELECT 1 FROM events WHERE type = 'email_sent' AND entity_type = 'email' AND entity_id = ? LIMIT 1`
|
|
584
|
+
).get(messageId);
|
|
585
|
+
if (registered !== void 0) return true;
|
|
586
|
+
const jsonValue = JSON.stringify(messageId).slice(1, -1);
|
|
587
|
+
const pattern = `%"message_id":"${jsonValue.replace(/[%_\\]/g, (c) => `\\${c}`)}"%`;
|
|
588
|
+
const rows = db.prepare(
|
|
589
|
+
`SELECT output FROM workflow_steps WHERE kind = 'send' AND status = 'completed'
|
|
590
|
+
AND updated_at < ? AND output LIKE ? ESCAPE '\\'`
|
|
591
|
+
).all(SEND_STEP_LIKE_CUTOFF, pattern);
|
|
592
|
+
return rows.some((r) => {
|
|
593
|
+
const out = safeParse(str(r["output"]));
|
|
594
|
+
return out !== null && typeof out === "object" && !Array.isArray(out) && out["message_id"] === messageId;
|
|
595
|
+
});
|
|
596
|
+
},
|
|
597
|
+
createPrompt(runId, content, source) {
|
|
598
|
+
const id = ids.newId("pr");
|
|
599
|
+
db.prepare(`INSERT INTO prompts (id, run_id, content, source, status, created_at)
|
|
600
|
+
VALUES (?, ?, ?, ?, 'submitted', ?)`).run(id, runId, content, source, now());
|
|
601
|
+
return { id, runId, content, status: "submitted", createdAt: now() };
|
|
602
|
+
},
|
|
603
|
+
prune(retentionDays) {
|
|
604
|
+
return pruneImpl(db, retentionDays, now());
|
|
605
|
+
},
|
|
606
|
+
taskViews(limit = 500) {
|
|
607
|
+
const all = db.prepare("SELECT * FROM workflows ORDER BY created_at DESC, rowid DESC LIMIT ?").all(limit).map(rowToWorkflow);
|
|
608
|
+
const byId = new Map(all.map((w) => [w.id, w]));
|
|
609
|
+
const parentOf = (w) => {
|
|
610
|
+
const rel = w.meta["related_workflow"];
|
|
611
|
+
if (typeof rel === "string" && rel !== "") return rel;
|
|
612
|
+
return w.parentId ?? "";
|
|
613
|
+
};
|
|
614
|
+
const isTaskCard = (w) => w.meta["mode"] === "nodes";
|
|
615
|
+
const rootCache = /* @__PURE__ */ new Map();
|
|
616
|
+
const rootOf = (w) => {
|
|
617
|
+
const cached = rootCache.get(w.id);
|
|
618
|
+
if (cached !== void 0) return cached;
|
|
619
|
+
const path = [];
|
|
620
|
+
let cur = w;
|
|
621
|
+
while (cur !== void 0 && !path.some((p) => p.id === cur.id)) {
|
|
622
|
+
path.push(cur);
|
|
623
|
+
const pid = parentOf(cur);
|
|
624
|
+
if (pid === "") break;
|
|
625
|
+
cur = byId.get(pid);
|
|
626
|
+
}
|
|
627
|
+
let best = "";
|
|
628
|
+
for (let i = path.length - 1; i >= 0; i--) {
|
|
629
|
+
const p = path[i];
|
|
630
|
+
if (best === "" && isTaskCard(p)) best = p.id;
|
|
631
|
+
rootCache.set(p.id, best);
|
|
632
|
+
}
|
|
633
|
+
return rootCache.get(w.id) ?? "";
|
|
634
|
+
};
|
|
635
|
+
const members = /* @__PURE__ */ new Map();
|
|
636
|
+
const map = {};
|
|
637
|
+
for (const w of all) {
|
|
638
|
+
const root = rootOf(w);
|
|
639
|
+
if (root === "") continue;
|
|
640
|
+
map[w.id] = root;
|
|
641
|
+
const arr = members.get(root);
|
|
642
|
+
if (arr === void 0) members.set(root, [w.id]);
|
|
643
|
+
else arr.push(w.id);
|
|
644
|
+
}
|
|
645
|
+
const views = [...members.entries()].map(([taskId, ids2]) => {
|
|
646
|
+
const card = byId.get(taskId);
|
|
647
|
+
return { taskId, title: card.title, status: card.status, workflowIds: ids2 };
|
|
648
|
+
}).sort((a, b) => {
|
|
649
|
+
const ca = byId.get(a.taskId)?.createdAt ?? "";
|
|
650
|
+
const cb = byId.get(b.taskId)?.createdAt ?? "";
|
|
651
|
+
return cb.localeCompare(ca);
|
|
652
|
+
});
|
|
653
|
+
return { views, map };
|
|
654
|
+
},
|
|
655
|
+
latestPromptForSession(session, statuses) {
|
|
656
|
+
const run = activeRunOf(session);
|
|
657
|
+
if (!run) return null;
|
|
658
|
+
const placeholders = statuses.map(() => "?").join(",");
|
|
659
|
+
const r = db.prepare(
|
|
660
|
+
`SELECT id, run_id, content, status, created_at FROM prompts
|
|
661
|
+
WHERE run_id=? AND status IN (${placeholders}) ORDER BY created_at DESC LIMIT 1`
|
|
662
|
+
).get(run.id, ...statuses);
|
|
663
|
+
if (!r) return null;
|
|
664
|
+
return {
|
|
665
|
+
id: str(r["id"]),
|
|
666
|
+
runId: str(r["run_id"]),
|
|
667
|
+
content: str(r["content"]),
|
|
668
|
+
status: str(r["status"]),
|
|
669
|
+
createdAt: str(r["created_at"])
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
};
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
// src/platform/ledger/usage.ts
|
|
676
|
+
import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
|
|
677
|
+
import { readdirSync, openSync, readSync, fstatSync, closeSync } from "node:fs";
|
|
678
|
+
import { join } from "node:path";
|
|
679
|
+
|
|
680
|
+
// src/platform/ledger/pricing.ts
|
|
681
|
+
var PRICING_SOURCE = "https://docs.bigmodel.cn/cn/guide/start/pricing";
|
|
682
|
+
var PRICING_RETRIEVED_AT = "2026-10-06";
|
|
683
|
+
var K32 = 32e3;
|
|
684
|
+
var K200 = 200;
|
|
685
|
+
var flat = (input, cache, output, note = "\u7EDF\u4E00\u4EF7") => ({ when: {}, input, cache, output, note });
|
|
686
|
+
var BIGMODEL_PRICES = [
|
|
687
|
+
{ id: "glm-5.3", label: "GLM-5.3", tiers: [flat(8, 2, 28)] },
|
|
688
|
+
{ id: "glm-5.3-flash", label: "GLM-5.3-Flash", tiers: [flat(0.8, 0.23, 2.8)] },
|
|
689
|
+
{ id: "glm-5.3-flashx", label: "GLM-5.3-FlashX", tiers: [flat(2, 0.57, 7)] },
|
|
690
|
+
{ id: "glm-5.2", label: "GLM-5.2", tiers: [flat(8, 2, 28)] },
|
|
691
|
+
{
|
|
692
|
+
id: "glm-5.1",
|
|
693
|
+
label: "GLM-5.1",
|
|
694
|
+
tiers: [
|
|
695
|
+
{ when: { inputLt: K32 }, input: 6, cache: 1.3, output: 24, note: "\u5165<32K" },
|
|
696
|
+
{ when: { inputGte: K32 }, input: 8, cache: 2, output: 28, note: "\u5165\u226532K" }
|
|
697
|
+
]
|
|
698
|
+
},
|
|
699
|
+
{
|
|
700
|
+
id: "glm-5-turbo",
|
|
701
|
+
label: "GLM-5-Turbo",
|
|
702
|
+
tiers: [
|
|
703
|
+
{ when: { inputLt: K32 }, input: 5, cache: 1.2, output: 22, note: "\u5165<32K" },
|
|
704
|
+
{ when: { inputGte: K32 }, input: 7, cache: 1.8, output: 26, note: "\u5165\u226532K" }
|
|
705
|
+
]
|
|
706
|
+
},
|
|
707
|
+
{
|
|
708
|
+
id: "glm-5",
|
|
709
|
+
label: "GLM-5",
|
|
710
|
+
tiers: [
|
|
711
|
+
{ when: { inputLt: K32 }, input: 4, cache: 1, output: 18, note: "\u5165<32K" },
|
|
712
|
+
{ when: { inputGte: K32 }, input: 6, cache: 1.5, output: 22, note: "\u5165\u226532K" }
|
|
713
|
+
]
|
|
714
|
+
},
|
|
715
|
+
{
|
|
716
|
+
id: "glm-4.7",
|
|
717
|
+
label: "GLM-4.7",
|
|
718
|
+
tiers: [
|
|
719
|
+
{ when: { inputLt: K32, outputLt: K200 }, input: 2, cache: 0.4, output: 8, note: "\u5165<32K\xB7\u51FA<0.2K" },
|
|
720
|
+
{ when: { inputLt: K32, outputGte: K200 }, input: 3, cache: 0.6, output: 14, note: "\u5165<32K\xB7\u51FA\u22650.2K" },
|
|
721
|
+
{ when: { inputGte: K32 }, input: 4, cache: 0.8, output: 16, note: "\u516532K\u2013200K" }
|
|
722
|
+
]
|
|
723
|
+
},
|
|
724
|
+
{
|
|
725
|
+
id: "glm-4.5-air",
|
|
726
|
+
label: "GLM-4.5-Air",
|
|
727
|
+
tiers: [
|
|
728
|
+
{ when: { inputLt: K32, outputLt: K200 }, input: 0.8, cache: 0.16, output: 2, note: "\u5165<32K\xB7\u51FA<0.2K" },
|
|
729
|
+
{ when: { inputLt: K32, outputGte: K200 }, input: 0.8, cache: 0.16, output: 6, note: "\u5165<32K\xB7\u51FA\u22650.2K" },
|
|
730
|
+
{ when: { inputGte: K32 }, input: 1.2, cache: 0.24, output: 8, note: "\u516532K\u2013128K" }
|
|
731
|
+
]
|
|
732
|
+
},
|
|
733
|
+
{ id: "glm-4.7-flashx", label: "GLM-4.7-FlashX", tiers: [flat(0.5, 0.1, 3)] },
|
|
734
|
+
{ id: "glm-4.7-flash", label: "GLM-4.7-Flash\uFF08\u514D\u8D39\uFF09", tiers: [flat(0, 0, 0, "\u514D\u8D39")] },
|
|
735
|
+
{ id: "glm-4-plus", label: "GLM-4-Plus", tiers: [flat(5, 2.5, 5)] },
|
|
736
|
+
{ id: "glm-4-air-250414", label: "GLM-4-Air-250414", tiers: [flat(0.5, 0.25, 0.5)] },
|
|
737
|
+
{ id: "glm-4-air", label: "GLM-4-Air\uFF08\u540C 250414 \u7248\u4EF7\uFF09", tiers: [flat(0.5, 0.25, 0.5)] },
|
|
738
|
+
{ id: "glm-4-airx", label: "GLM-4-AirX", tiers: [flat(10, null, 10)] },
|
|
739
|
+
{ id: "glm-4-long", label: "GLM-4-Long", tiers: [flat(1, 0.5, 1)] },
|
|
740
|
+
{ id: "glm-4-assistant", label: "GLM-4-Assistant", tiers: [flat(5, null, 5)] },
|
|
741
|
+
{ id: "glm-z1-air", label: "GLM-Z1-Air", tiers: [flat(0.5, null, 0.5)] },
|
|
742
|
+
{ id: "glm-z1-airx", label: "GLM-Z1-AirX", tiers: [flat(5, null, 5)] },
|
|
743
|
+
{ id: "glm-z1-flashx", label: "GLM-Z1-FlashX", tiers: [flat(0.1, null, 0.1)] },
|
|
744
|
+
{ id: "glm-4-flashx-250414", label: "GLM-4-FlashX-250414", tiers: [flat(0.1, 0.05, 0.1)] },
|
|
745
|
+
{ id: "glm-4-flashx", label: "GLM-4-FlashX\uFF08\u540C 250414 \u7248\u4EF7\uFF09", tiers: [flat(0.1, 0.05, 0.1)] },
|
|
746
|
+
{ id: "glm-4-flash-250414", label: "GLM-4-Flash-250414\uFF08\u514D\u8D39\uFF09", tiers: [flat(0, null, 0, "\u514D\u8D39\xB7\u7F13\u5B58\u4E0D\u652F\u6301")] },
|
|
747
|
+
{ id: "glm-4-flash", label: "GLM-4-Flash\uFF08\u514D\u8D39\uFF09", tiers: [flat(0, null, 0, "\u514D\u8D39\xB7\u7F13\u5B58\u4E0D\u652F\u6301")] },
|
|
748
|
+
{ id: "glm-z1-flash", label: "GLM-Z1-Flash\uFF08\u514D\u8D39\uFF09", tiers: [flat(0, null, 0, "\u514D\u8D39\xB7\u7F13\u5B58\u4E0D\u652F\u6301")] },
|
|
749
|
+
{ id: "glm-ocr", label: "GLM-OCR", tiers: [flat(0.2, null, 0.2)] },
|
|
750
|
+
{
|
|
751
|
+
id: "glm-5v-turbo",
|
|
752
|
+
label: "GLM-5V-Turbo",
|
|
753
|
+
tiers: [
|
|
754
|
+
{ when: { inputLt: K32 }, input: 5, cache: 1.2, output: 22, note: "\u5165<32K" },
|
|
755
|
+
{ when: { inputGte: K32 }, input: 7, cache: 1.8, output: 26, note: "\u5165\u226532K" }
|
|
756
|
+
]
|
|
757
|
+
},
|
|
758
|
+
{
|
|
759
|
+
id: "glm-4.6v",
|
|
760
|
+
label: "GLM-4.6V",
|
|
761
|
+
tiers: [
|
|
762
|
+
{ when: { inputLt: K32 }, input: 1, cache: 0.2, output: 3, note: "\u5165<32K" },
|
|
763
|
+
{ when: { inputGte: K32 }, input: 2, cache: 0.4, output: 6, note: "\u516532K\u2013128K\uFF08\u8D85\u4E0A\u754C\u5E76\u5165\u9AD8\u6863\uFF09" }
|
|
764
|
+
]
|
|
765
|
+
},
|
|
766
|
+
{
|
|
767
|
+
id: "glm-4.6v-flashx",
|
|
768
|
+
label: "GLM-4.6V-FlashX",
|
|
769
|
+
tiers: [
|
|
770
|
+
{ when: { inputLt: K32 }, input: 0.15, cache: 0.03, output: 1.5, note: "\u5165<32K" },
|
|
771
|
+
{ when: { inputGte: K32 }, input: 0.3, cache: 0.03, output: 3, note: "\u516532K\u2013128K\uFF08\u8D85\u4E0A\u754C\u5E76\u5165\u9AD8\u6863\uFF09" }
|
|
772
|
+
]
|
|
773
|
+
},
|
|
774
|
+
{ id: "glm-4.6v-flash", label: "GLM-4.6V-Flash\uFF08\u514D\u8D39\uFF09", tiers: [flat(0, 0, 0, "\u514D\u8D39")] },
|
|
775
|
+
{
|
|
776
|
+
id: "glm-4.5v",
|
|
777
|
+
label: "GLM-4.5V",
|
|
778
|
+
tiers: [
|
|
779
|
+
{ when: { inputLt: K32 }, input: 2, cache: 0.4, output: 6, note: "\u5165<32K" },
|
|
780
|
+
{ when: { inputGte: K32 }, input: 4, cache: 0.8, output: 12, note: "\u516532K\u201364K\uFF08\u8D85\u4E0A\u754C\u5E76\u5165\u9AD8\u6863\uFF09" }
|
|
781
|
+
]
|
|
782
|
+
},
|
|
783
|
+
{ id: "glm-4v-plus-0111", label: "GLM-4V-Plus-0111", tiers: [flat(4, 2, 4)] },
|
|
784
|
+
{ id: "glm-4v-plus", label: "GLM-4V-Plus\uFF08\u540C 0111 \u7248\u4EF7\uFF09", tiers: [flat(4, 2, 4)] },
|
|
785
|
+
{ id: "glm-4v-flash", label: "GLM-4V-Flash\uFF08\u514D\u8D39\uFF09", tiers: [flat(0, null, 0, "\u514D\u8D39\xB7\u7F13\u5B58\u4E0D\u652F\u6301")] },
|
|
786
|
+
{ id: "glm-4.1v-thinking-flashx", label: "GLM-4.1V-Thinking-FlashX", tiers: [flat(2, null, 2)] },
|
|
787
|
+
{ id: "glm-4.1v-thinking-flash", label: "GLM-4.1V-Thinking-Flash\uFF08\u514D\u8D39\uFF09", tiers: [flat(0, null, 0, "\u514D\u8D39\xB7\u7F13\u5B58\u4E0D\u652F\u6301")] },
|
|
788
|
+
{ id: "embedding-3", label: "Embedding-3", tiers: [flat(0.5, null, 0.5, "\u5355\u503C\u4EF7\u76EE\xB7input \u8BA1")] },
|
|
789
|
+
{ id: "embedding-2", label: "Embedding-2", tiers: [flat(0.5, null, 0.5, "\u5355\u503C\u4EF7\u76EE\xB7input \u8BA1")] },
|
|
790
|
+
{ id: "rerank", label: "Rerank", tiers: [flat(0.8, null, 0.8, "\u5355\u503C\u4EF7\u76EE")] },
|
|
791
|
+
{ id: "codegeex-4", label: "CodeGeeX-4", tiers: [flat(0.1, null, 0.1, "\u5355\u503C\u4EF7\u76EE")] }
|
|
792
|
+
];
|
|
793
|
+
var findPrice = (model) => {
|
|
794
|
+
const m = model.trim().toLowerCase();
|
|
795
|
+
if (m === "") return null;
|
|
796
|
+
let hit = null;
|
|
797
|
+
for (const p of BIGMODEL_PRICES) {
|
|
798
|
+
if (m === p.id || m.startsWith(p.id + "-") && (hit === null || p.id.length > hit.id.length)) hit = p;
|
|
799
|
+
}
|
|
800
|
+
return hit;
|
|
801
|
+
};
|
|
802
|
+
var round4 = (x) => Math.round(x * 1e4) / 1e4;
|
|
803
|
+
var tierOf = (price, r) => {
|
|
804
|
+
const input = r.inputTokens;
|
|
805
|
+
const output = r.outputTokens;
|
|
806
|
+
for (const t of price.tiers) {
|
|
807
|
+
const w = t.when;
|
|
808
|
+
if (w.inputLt !== void 0 && !(input < w.inputLt)) continue;
|
|
809
|
+
if (w.inputGte !== void 0 && !(input >= w.inputGte)) continue;
|
|
810
|
+
if (w.outputLt !== void 0 && !(output < w.outputLt)) continue;
|
|
811
|
+
if (w.outputGte !== void 0 && !(output >= w.outputGte)) continue;
|
|
812
|
+
return t;
|
|
813
|
+
}
|
|
814
|
+
return price.tiers[price.tiers.length - 1];
|
|
815
|
+
};
|
|
816
|
+
var priceUsage = (rows) => {
|
|
817
|
+
const accs = /* @__PURE__ */ new Map();
|
|
818
|
+
for (const r of rows) {
|
|
819
|
+
if (r.inputTokens === 0 && r.outputTokens === 0) continue;
|
|
820
|
+
const key = r.model === "" ? "\uFF08\u672A\u8BB0\u5F55\u6A21\u578B\uFF09" : r.model;
|
|
821
|
+
let a = accs.get(key);
|
|
822
|
+
if (a === void 0) {
|
|
823
|
+
const p = findPrice(key);
|
|
824
|
+
a = {
|
|
825
|
+
model: key,
|
|
826
|
+
label: p?.label ?? key,
|
|
827
|
+
free: false,
|
|
828
|
+
priced: p !== null,
|
|
829
|
+
calls: 0,
|
|
830
|
+
inputTokens: 0,
|
|
831
|
+
cachedTokens: 0,
|
|
832
|
+
outputTokens: 0,
|
|
833
|
+
tiersHit: /* @__PURE__ */ new Set(),
|
|
834
|
+
cost: { cache: 0, input: 0, output: 0 }
|
|
835
|
+
};
|
|
836
|
+
accs.set(key, a);
|
|
837
|
+
}
|
|
838
|
+
a.calls++;
|
|
839
|
+
a.inputTokens += r.inputTokens;
|
|
840
|
+
a.cachedTokens += r.cachedTokens;
|
|
841
|
+
a.outputTokens += r.outputTokens;
|
|
842
|
+
if (!a.priced) continue;
|
|
843
|
+
const t = tierOf(findPrice(key), r);
|
|
844
|
+
a.tiersHit.add(t.note);
|
|
845
|
+
if (t.input === 0 && t.output === 0) {
|
|
846
|
+
a.free = true;
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
const cached = Math.min(Math.max(r.cachedTokens, 0), r.inputTokens);
|
|
850
|
+
const cachePrice = t.cache ?? t.input;
|
|
851
|
+
a.cost.cache += cached / 1e6 * cachePrice;
|
|
852
|
+
a.cost.input += (r.inputTokens - cached) / 1e6 * t.input;
|
|
853
|
+
a.cost.output += r.outputTokens / 1e6 * t.output;
|
|
854
|
+
}
|
|
855
|
+
const byModel = [...accs.values()].map((a) => ({
|
|
856
|
+
model: a.model,
|
|
857
|
+
label: a.label,
|
|
858
|
+
free: a.free,
|
|
859
|
+
priced: a.priced,
|
|
860
|
+
calls: a.calls,
|
|
861
|
+
inputTokens: a.inputTokens,
|
|
862
|
+
cachedTokens: a.cachedTokens,
|
|
863
|
+
inputTokensChargeable: a.inputTokens - Math.min(a.cachedTokens, a.inputTokens),
|
|
864
|
+
outputTokens: a.outputTokens,
|
|
865
|
+
tiersHit: [...a.tiersHit],
|
|
866
|
+
costCny: {
|
|
867
|
+
cache: round4(a.cost.cache),
|
|
868
|
+
input: round4(a.cost.input),
|
|
869
|
+
output: round4(a.cost.output),
|
|
870
|
+
total: round4(a.cost.cache + a.cost.input + a.cost.output)
|
|
871
|
+
}
|
|
872
|
+
})).sort((x, y) => y.costCny.total - x.costCny.total);
|
|
873
|
+
const total = byModel.reduce(
|
|
874
|
+
(s, m) => ({ cache: s.cache + m.costCny.cache, input: s.input + m.costCny.input, output: s.output + m.costCny.output }),
|
|
875
|
+
{ cache: 0, input: 0, output: 0 }
|
|
876
|
+
);
|
|
877
|
+
return {
|
|
878
|
+
currency: "CNY",
|
|
879
|
+
unit: "per_1m_tokens",
|
|
880
|
+
pricingSource: PRICING_SOURCE,
|
|
881
|
+
pricingRetrievedAt: PRICING_RETRIEVED_AT,
|
|
882
|
+
byModel,
|
|
883
|
+
totalCny: { cache: round4(total.cache), input: round4(total.input), output: round4(total.output), total: round4(total.cache + total.input + total.output) },
|
|
884
|
+
unpricedModels: byModel.filter((m) => !m.priced).map((m) => m.model)
|
|
885
|
+
};
|
|
886
|
+
};
|
|
887
|
+
|
|
888
|
+
// src/platform/ledger/usage.ts
|
|
889
|
+
var MIGRATIONS2 = `
|
|
890
|
+
CREATE TABLE IF NOT EXISTS prompt_usage (
|
|
891
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
892
|
+
source TEXT NOT NULL DEFAULT '',
|
|
893
|
+
note TEXT NOT NULL DEFAULT '',
|
|
894
|
+
model TEXT NOT NULL DEFAULT '',
|
|
895
|
+
input_tokens INTEGER NOT NULL DEFAULT 0,
|
|
896
|
+
-- cached_tokens\uFF1Az.ai \u53E3\u5F84\u7F13\u5B58\u547D\u4E2D\uFF08input \u5B50\u96C6\uFF0Cwf_66a9b632154d\uFF09
|
|
897
|
+
--\uFF08\u5341\u4E09\u8DD1 R1 \u987A\u624B\u4FEE\uFF1A\u539F JS \u98CE\u683C // \u6CE8\u91CA\u5199\u5728 SQL \u4E32\u5185\uFF0CSQLite \u4E0D\u8BA4\uFF0C
|
|
898
|
+
-- db.exec(MIGRATIONS) \u76F4\u63A5\u629B near "/": syntax error\uFF0C\u5168\u4ED3 store \u7EA7\u6D4B\u8BD5\u5C3D\u7EA2\uFF09
|
|
899
|
+
cached_tokens INTEGER NOT NULL DEFAULT 0,
|
|
900
|
+
output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
901
|
+
duration_ms INTEGER NOT NULL DEFAULT 0,
|
|
902
|
+
error TEXT NOT NULL DEFAULT '',
|
|
903
|
+
created_at TEXT NOT NULL
|
|
904
|
+
);
|
|
905
|
+
CREATE INDEX IF NOT EXISTS idx_usage_time ON prompt_usage(created_at);
|
|
906
|
+
CREATE TABLE IF NOT EXISTS claude_sessions (
|
|
907
|
+
claude_session_id TEXT PRIMARY KEY,
|
|
908
|
+
tmux_session TEXT NOT NULL DEFAULT '',
|
|
909
|
+
transcript_path TEXT NOT NULL DEFAULT '',
|
|
910
|
+
cwd TEXT NOT NULL DEFAULT ''
|
|
911
|
+
);
|
|
912
|
+
CREATE TABLE IF NOT EXISTS claude_ingest_offset (
|
|
913
|
+
claude_session_id TEXT PRIMARY KEY,
|
|
914
|
+
byte_offset INTEGER NOT NULL DEFAULT 0,
|
|
915
|
+
last_ingested_at TEXT NOT NULL DEFAULT ''
|
|
916
|
+
);
|
|
917
|
+
CREATE TABLE IF NOT EXISTS usage_meta (
|
|
918
|
+
key TEXT PRIMARY KEY,
|
|
919
|
+
value TEXT NOT NULL DEFAULT ''
|
|
920
|
+
);
|
|
921
|
+
`;
|
|
922
|
+
var n = (v) => typeof v === "number" ? v : Number(v ?? 0);
|
|
923
|
+
var windowUsageRows = (db, tmuxSession, windows) => {
|
|
924
|
+
if (windows.length === 0) return [];
|
|
925
|
+
const conds = windows.map(() => "(created_at >= ? AND created_at <= ?)").join(" OR ");
|
|
926
|
+
const params = [tmuxSession];
|
|
927
|
+
for (const [a, b] of windows) params.push(a, b);
|
|
928
|
+
return db.prepare(
|
|
929
|
+
`SELECT model, input_tokens AS i, output_tokens AS o, cached_tokens AS k
|
|
930
|
+
FROM prompt_usage WHERE source='claude_code' AND tmux_session=? AND (${conds})`
|
|
931
|
+
).all(...params).map((r) => ({
|
|
932
|
+
model: String(r["model"]),
|
|
933
|
+
inputTokens: n(r["i"]),
|
|
934
|
+
outputTokens: n(r["o"]),
|
|
935
|
+
cachedTokens: n(r["k"])
|
|
936
|
+
}));
|
|
937
|
+
};
|
|
938
|
+
var REPLAY_COOLDOWN_MS = 24 * 36e5;
|
|
939
|
+
var createUsageStore = (dbPath, now = () => /* @__PURE__ */ new Date()) => {
|
|
940
|
+
const db = new DatabaseSync2(dbPath);
|
|
941
|
+
db.exec(MIGRATIONS2);
|
|
942
|
+
try {
|
|
943
|
+
db.exec("ALTER TABLE prompt_usage ADD COLUMN cached_tokens INTEGER NOT NULL DEFAULT 0");
|
|
944
|
+
} catch {
|
|
945
|
+
}
|
|
946
|
+
try {
|
|
947
|
+
db.exec(`ALTER TABLE prompt_usage ADD COLUMN tmux_session TEXT NOT NULL DEFAULT ''`);
|
|
948
|
+
} catch {
|
|
949
|
+
}
|
|
950
|
+
try {
|
|
951
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_usage_session ON prompt_usage(tmux_session, id)`);
|
|
952
|
+
} catch {
|
|
953
|
+
}
|
|
954
|
+
return {
|
|
955
|
+
record(r) {
|
|
956
|
+
db.prepare(`INSERT INTO prompt_usage (source, note, model, input_tokens, output_tokens, cached_tokens, duration_ms, error, created_at)
|
|
957
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(
|
|
958
|
+
r.source,
|
|
959
|
+
r.note ?? "",
|
|
960
|
+
r.model ?? "",
|
|
961
|
+
r.inputTokens ?? 0,
|
|
962
|
+
r.outputTokens ?? 0,
|
|
963
|
+
r.cachedTokens ?? 0,
|
|
964
|
+
r.durationMs ?? 0,
|
|
965
|
+
r.error ?? "",
|
|
966
|
+
now().toISOString()
|
|
967
|
+
);
|
|
968
|
+
},
|
|
969
|
+
stats(sinceHours = 24) {
|
|
970
|
+
const since = new Date(now().getTime() - sinceHours * 36e5).toISOString();
|
|
971
|
+
const total = db.prepare(
|
|
972
|
+
`SELECT COUNT(*) AS c, COALESCE(SUM(input_tokens),0) AS i, COALESCE(SUM(output_tokens),0) AS o
|
|
973
|
+
FROM prompt_usage WHERE created_at >= ?`
|
|
974
|
+
).get(since);
|
|
975
|
+
const byModel = db.prepare(
|
|
976
|
+
`SELECT model, COUNT(*) AS c, COALESCE(SUM(input_tokens),0) AS i, COALESCE(SUM(output_tokens),0) AS o
|
|
977
|
+
FROM prompt_usage WHERE created_at >= ? AND model != '' GROUP BY model ORDER BY i DESC`
|
|
978
|
+
).all(since).map((r) => ({
|
|
979
|
+
model: String(r["model"]),
|
|
980
|
+
calls: n(r["c"]),
|
|
981
|
+
inputTokens: n(r["i"]),
|
|
982
|
+
outputTokens: n(r["o"])
|
|
983
|
+
}));
|
|
984
|
+
return {
|
|
985
|
+
totalCalls: n(total?.["c"]),
|
|
986
|
+
totalInputTokens: n(total?.["i"]),
|
|
987
|
+
totalOutputTokens: n(total?.["o"]),
|
|
988
|
+
byModel
|
|
989
|
+
};
|
|
990
|
+
},
|
|
991
|
+
timeline(hours = 24) {
|
|
992
|
+
const since = new Date(now().getTime() - hours * 36e5).toISOString();
|
|
993
|
+
return db.prepare(
|
|
994
|
+
`SELECT substr(created_at,1,13) AS hour, COUNT(*) AS c, COALESCE(SUM(input_tokens),0) AS i, COALESCE(SUM(output_tokens),0) AS o
|
|
995
|
+
FROM prompt_usage WHERE created_at >= ? GROUP BY hour ORDER BY hour`
|
|
996
|
+
).all(since).map((r) => ({
|
|
997
|
+
hour: String(r["hour"]),
|
|
998
|
+
calls: n(r["c"]),
|
|
999
|
+
inputTokens: n(r["i"]),
|
|
1000
|
+
outputTokens: n(r["o"])
|
|
1001
|
+
}));
|
|
1002
|
+
},
|
|
1003
|
+
latestInputTokens(tmuxSession) {
|
|
1004
|
+
const r = db.prepare(
|
|
1005
|
+
`SELECT input_tokens AS i FROM prompt_usage
|
|
1006
|
+
WHERE source='claude_code' AND tmux_session=? ORDER BY id DESC LIMIT 1`
|
|
1007
|
+
).get(tmuxSession);
|
|
1008
|
+
return r === void 0 ? 0 : n(r["i"]);
|
|
1009
|
+
},
|
|
1010
|
+
lastClaudeCodeAt(tmuxSession) {
|
|
1011
|
+
const r = db.prepare(
|
|
1012
|
+
`SELECT created_at AS t FROM prompt_usage
|
|
1013
|
+
WHERE source='claude_code' AND tmux_session=? ORDER BY id DESC LIMIT 1`
|
|
1014
|
+
).get(tmuxSession);
|
|
1015
|
+
return r === void 0 ? "" : String(r["t"] ?? "");
|
|
1016
|
+
},
|
|
1017
|
+
perSessionWindows(tmuxSession, windows) {
|
|
1018
|
+
if (windows.length === 0) return { count: 0, inputTokens: 0, outputTokens: 0 };
|
|
1019
|
+
const conds = windows.map(() => "(created_at >= ? AND created_at <= ?)").join(" OR ");
|
|
1020
|
+
const params = [tmuxSession];
|
|
1021
|
+
for (const [a, b] of windows) params.push(a, b);
|
|
1022
|
+
const r = db.prepare(
|
|
1023
|
+
`SELECT COUNT(*) AS c, COALESCE(SUM(input_tokens),0) AS i, COALESCE(SUM(output_tokens),0) AS o
|
|
1024
|
+
FROM prompt_usage WHERE source='claude_code' AND tmux_session=? AND (${conds})`
|
|
1025
|
+
).get(...params);
|
|
1026
|
+
return { count: n(r["c"]), inputTokens: n(r["i"]), outputTokens: n(r["o"]) };
|
|
1027
|
+
},
|
|
1028
|
+
perSessionWindowsRows(tmuxSession, windows) {
|
|
1029
|
+
return windowUsageRows(db, tmuxSession, windows);
|
|
1030
|
+
},
|
|
1031
|
+
sessionWindowsPriced(sessions, windows) {
|
|
1032
|
+
if (sessions.length === 0 || windows.length === 0) return priceUsage([]);
|
|
1033
|
+
return priceUsage(sessions.flatMap((s) => windowUsageRows(db, s, windows)));
|
|
1034
|
+
},
|
|
1035
|
+
byLocalDay(days = 7) {
|
|
1036
|
+
const since = new Date(now().getTime() - days * 864e5).toISOString();
|
|
1037
|
+
return db.prepare(
|
|
1038
|
+
`SELECT strftime('%Y-%m-%d', created_at, 'localtime') AS day,
|
|
1039
|
+
COUNT(*) AS c, COALESCE(SUM(input_tokens),0) AS i, COALESCE(SUM(output_tokens),0) AS o,
|
|
1040
|
+
COALESCE(SUM(cached_tokens),0) AS k
|
|
1041
|
+
FROM prompt_usage
|
|
1042
|
+
WHERE created_at >= ?
|
|
1043
|
+
GROUP BY day ORDER BY day`
|
|
1044
|
+
).all(since).map((r) => ({
|
|
1045
|
+
day: String(r["day"]),
|
|
1046
|
+
inputTokens: n(r["i"]),
|
|
1047
|
+
outputTokens: n(r["o"]),
|
|
1048
|
+
cachedTokens: n(r["k"]),
|
|
1049
|
+
calls: n(r["c"])
|
|
1050
|
+
}));
|
|
1051
|
+
},
|
|
1052
|
+
needsClaudeReplay() {
|
|
1053
|
+
const mark = db.prepare(`SELECT value FROM usage_meta WHERE key='last_claude_replay_at'`).get();
|
|
1054
|
+
const lastAt = mark === void 0 ? "" : String(mark["value"] ?? "");
|
|
1055
|
+
const lastMs = lastAt === "" ? NaN : Date.parse(lastAt);
|
|
1056
|
+
if (Number.isFinite(lastMs) && now().getTime() - lastMs < REPLAY_COOLDOWN_MS) return false;
|
|
1057
|
+
const r = db.prepare(
|
|
1058
|
+
`SELECT COUNT(*) AS total, SUM(CASE WHEN tmux_session != '' THEN 1 ELSE 0 END) AS filled FROM prompt_usage WHERE source='claude_code'`
|
|
1059
|
+
).get();
|
|
1060
|
+
const total = n(r["total"]);
|
|
1061
|
+
const filled = n(r["filled"]);
|
|
1062
|
+
const mapped = n(db.prepare("SELECT COUNT(*) AS c FROM claude_sessions").get()["c"]);
|
|
1063
|
+
return mapped > 0 && total > 0 && filled * 2 < total;
|
|
1064
|
+
},
|
|
1065
|
+
resetClaudeIngest() {
|
|
1066
|
+
db.exec("DELETE FROM prompt_usage WHERE source='claude_code'");
|
|
1067
|
+
db.exec("DELETE FROM claude_ingest_offset");
|
|
1068
|
+
db.prepare(`INSERT INTO usage_meta (key, value) VALUES ('last_claude_replay_at', ?)
|
|
1069
|
+
ON CONFLICT(key) DO UPDATE SET value=excluded.value`).run(now().toISOString());
|
|
1070
|
+
},
|
|
1071
|
+
recent(limit = 100) {
|
|
1072
|
+
return db.prepare("SELECT * FROM prompt_usage ORDER BY id DESC LIMIT ?").all(limit).map((r) => ({
|
|
1073
|
+
id: n(r["id"]),
|
|
1074
|
+
source: String(r["source"]),
|
|
1075
|
+
note: String(r["note"]),
|
|
1076
|
+
model: String(r["model"]),
|
|
1077
|
+
inputTokens: n(r["input_tokens"]),
|
|
1078
|
+
outputTokens: n(r["output_tokens"]),
|
|
1079
|
+
cachedTokens: n(r["cached_tokens"]),
|
|
1080
|
+
durationMs: n(r["duration_ms"]),
|
|
1081
|
+
error: String(r["error"]),
|
|
1082
|
+
createdAt: String(r["created_at"])
|
|
1083
|
+
}));
|
|
1084
|
+
},
|
|
1085
|
+
recordClaudeSession(claudeSessionId, tmuxSession, transcriptPath, cwd) {
|
|
1086
|
+
if (claudeSessionId === "" || transcriptPath === "") return;
|
|
1087
|
+
db.prepare(`INSERT INTO claude_sessions (claude_session_id, tmux_session, transcript_path, cwd)
|
|
1088
|
+
VALUES (?, ?, ?, ?)
|
|
1089
|
+
ON CONFLICT(claude_session_id) DO UPDATE SET
|
|
1090
|
+
tmux_session=excluded.tmux_session,
|
|
1091
|
+
transcript_path=excluded.transcript_path,
|
|
1092
|
+
cwd=excluded.cwd
|
|
1093
|
+
WHERE tmux_session = '' `).run(claudeSessionId, tmuxSession, transcriptPath, cwd);
|
|
1094
|
+
},
|
|
1095
|
+
recordClaudeSessionPrecise(claudeSessionId, tmuxSession) {
|
|
1096
|
+
if (claudeSessionId === "" || tmuxSession === "") return;
|
|
1097
|
+
db.prepare(`UPDATE claude_sessions SET tmux_session=? WHERE claude_session_id=?`).run(tmuxSession, claudeSessionId);
|
|
1098
|
+
db.prepare(`INSERT INTO claude_sessions (claude_session_id, tmux_session, transcript_path, cwd)
|
|
1099
|
+
VALUES (?, ?, '', '') ON CONFLICT(claude_session_id) DO NOTHING`).run(claudeSessionId, tmuxSession);
|
|
1100
|
+
},
|
|
1101
|
+
ingestProjectTranscripts(claudeProjectsDir) {
|
|
1102
|
+
const mapped = new Set(
|
|
1103
|
+
db.prepare("SELECT claude_session_id FROM claude_sessions").all().map((r) => String(r["claude_session_id"]))
|
|
1104
|
+
);
|
|
1105
|
+
let count = 0;
|
|
1106
|
+
try {
|
|
1107
|
+
const projects = readdirSync(claudeProjectsDir);
|
|
1108
|
+
for (const proj of projects) {
|
|
1109
|
+
const dir = join(claudeProjectsDir, proj);
|
|
1110
|
+
let files = [];
|
|
1111
|
+
try {
|
|
1112
|
+
files = readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
|
|
1113
|
+
} catch {
|
|
1114
|
+
continue;
|
|
1115
|
+
}
|
|
1116
|
+
for (const f of files) {
|
|
1117
|
+
const stem = f.replace(/\.jsonl$/, "");
|
|
1118
|
+
count += ingestFile(db, stem, join(dir, f));
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
} catch {
|
|
1122
|
+
return count;
|
|
1123
|
+
}
|
|
1124
|
+
return count;
|
|
1125
|
+
}
|
|
1126
|
+
};
|
|
1127
|
+
};
|
|
1128
|
+
var tmuxOf = (db, claudeId) => {
|
|
1129
|
+
const r = db.prepare("SELECT tmux_session FROM claude_sessions WHERE claude_session_id=?").get(claudeId);
|
|
1130
|
+
return r?.["tmux_session"] ?? "";
|
|
1131
|
+
};
|
|
1132
|
+
var ingestFile = (db, claudeId, transcriptPath) => {
|
|
1133
|
+
let fd;
|
|
1134
|
+
try {
|
|
1135
|
+
fd = openSync(transcriptPath, "r");
|
|
1136
|
+
} catch {
|
|
1137
|
+
return 0;
|
|
1138
|
+
}
|
|
1139
|
+
try {
|
|
1140
|
+
let offset = 0;
|
|
1141
|
+
const prev = db.prepare("SELECT byte_offset FROM claude_ingest_offset WHERE claude_session_id=?").get(claudeId);
|
|
1142
|
+
offset = Number(prev?.["byte_offset"] ?? 0);
|
|
1143
|
+
const size = fstatSync(fd).size;
|
|
1144
|
+
if (offset > size) {
|
|
1145
|
+
offset = 0;
|
|
1146
|
+
}
|
|
1147
|
+
const length = Math.max(0, size - offset);
|
|
1148
|
+
if (length === 0) return 0;
|
|
1149
|
+
const buf = Buffer.alloc(length);
|
|
1150
|
+
readSync(fd, buf, 0, length, offset);
|
|
1151
|
+
const text = buf.toString("utf8");
|
|
1152
|
+
const lastNL = text.lastIndexOf("\n");
|
|
1153
|
+
if (lastNL < 0) return 0;
|
|
1154
|
+
const complete = text.slice(0, lastNL + 1);
|
|
1155
|
+
let count = 0;
|
|
1156
|
+
for (const line of complete.split("\n")) {
|
|
1157
|
+
if (line === "") continue;
|
|
1158
|
+
const rec = parseAssistantLine(line);
|
|
1159
|
+
if (!rec) continue;
|
|
1160
|
+
const sess = tmuxOf(db, claudeId);
|
|
1161
|
+
db.prepare(`INSERT INTO prompt_usage (source, note, model, input_tokens, output_tokens, cached_tokens, duration_ms, error, created_at, tmux_session)
|
|
1162
|
+
VALUES ('claude_code', ?, ?, ?, ?, ?, 0, '', ?, ?)`).run(
|
|
1163
|
+
`turn=${String(rec.callContext).slice(0, 40)}`,
|
|
1164
|
+
rec.model,
|
|
1165
|
+
rec.inputTokens,
|
|
1166
|
+
rec.outputTokens,
|
|
1167
|
+
rec.cacheRead,
|
|
1168
|
+
rec.timestamp,
|
|
1169
|
+
sess
|
|
1170
|
+
);
|
|
1171
|
+
count++;
|
|
1172
|
+
}
|
|
1173
|
+
const newOffset = offset + Buffer.byteLength(complete, "utf8");
|
|
1174
|
+
db.prepare(`INSERT INTO claude_ingest_offset (claude_session_id, byte_offset, last_ingested_at)
|
|
1175
|
+
VALUES (?, ?, ?)
|
|
1176
|
+
ON CONFLICT(claude_session_id) DO UPDATE SET byte_offset=excluded.byte_offset, last_ingested_at=excluded.last_ingested_at`).run(claudeId, newOffset, (/* @__PURE__ */ new Date()).toISOString());
|
|
1177
|
+
return count;
|
|
1178
|
+
} finally {
|
|
1179
|
+
closeSync(fd);
|
|
1180
|
+
}
|
|
1181
|
+
};
|
|
1182
|
+
var parseAssistantLine = (line) => {
|
|
1183
|
+
try {
|
|
1184
|
+
const v = JSON.parse(line);
|
|
1185
|
+
if (v.type !== "assistant") return null;
|
|
1186
|
+
const u = v.message?.usage;
|
|
1187
|
+
if (!u || !u.input_tokens && !u.output_tokens) return null;
|
|
1188
|
+
const cacheRead = u.cache_read_input_tokens ?? 0;
|
|
1189
|
+
const cacheCreate = u.cache_creation_input_tokens ?? 0;
|
|
1190
|
+
const input = (u.input_tokens ?? 0) + cacheRead + cacheCreate;
|
|
1191
|
+
const output = u.output_tokens ?? 0;
|
|
1192
|
+
return {
|
|
1193
|
+
model: v.message?.model ?? "",
|
|
1194
|
+
inputTokens: input,
|
|
1195
|
+
outputTokens: output,
|
|
1196
|
+
cacheRead,
|
|
1197
|
+
timestamp: typeof v.timestamp === "string" ? v.timestamp : (/* @__PURE__ */ new Date()).toISOString(),
|
|
1198
|
+
callContext: v.uuid ?? ""
|
|
1199
|
+
};
|
|
1200
|
+
} catch {
|
|
1201
|
+
return null;
|
|
1202
|
+
}
|
|
1203
|
+
};
|
|
1204
|
+
export {
|
|
1205
|
+
asRecord,
|
|
1206
|
+
createLedgerStore,
|
|
1207
|
+
createUsageStore,
|
|
1208
|
+
emptyCaseFile,
|
|
1209
|
+
parseCaseFile,
|
|
1210
|
+
priceUsage,
|
|
1211
|
+
randomIds,
|
|
1212
|
+
rfc3339,
|
|
1213
|
+
safeParse,
|
|
1214
|
+
serializeCaseFile,
|
|
1215
|
+
systemClock
|
|
1216
|
+
};
|